| // SPDX-License-Identifier: GPL-2.0-only | |
| /* | |
| * Copyright 2023 Red Hat | |
| */ | |
| #include <linux/delay.h> | |
| #include <linux/mm.h> | |
| #include <linux/sched/mm.h> | |
| #include <linux/slab.h> | |
| #include <linux/vmalloc.h> | |
| #include "logger.h" | |
| #include "memory-alloc.h" | |
| #include "permassert.h" | |
| /* | |
| * UDS and VDO keep track of which threads are allowed to allocate memory freely, and which threads | |
| * must be careful to not do a memory allocation that does an I/O request. The 'allocating_threads' | |
| * thread_registry and its associated methods implement this tracking. | |
| */ | |
| static struct thread_registry allocating_threads; | |
| static inline bool allocations_allowed(void) | |
| { | |
| return vdo_lookup_thread(&allocating_threads) != NULL; | |
| } | |
| /* | |
| * Register the current thread as an allocating thread. | |
| * | |
| * An optional flag location can be supplied indicating whether, at any given point in time, the | |
| * threads associated with that flag should be allocating storage. If the flag is false, a message | |
| * will be logged. | |
| * | |
| * If no flag is supplied, the thread is always allowed to allocate storage without complaint. | |
| * | |
| * @new_thread: registered_thread structure to use for the current thread | |
| * @flag_ptr: Location of the allocation-allowed flag | |
| */ | |
| void vdo_register_allocating_thread(struct registered_thread *new_thread, | |
| const bool *flag_ptr) | |
| { | |
| if (flag_ptr == NULL) { | |
| static const bool allocation_always_allowed = true; | |
| flag_ptr = &allocation_always_allowed; | |
| } | |
| vdo_register_thread(&allocating_threads, new_thread, flag_ptr); | |
| } | |
| /* Unregister the current thread as an allocating thread. */ | |
| void vdo_unregister_allocating_thread(void) | |
| { | |
| vdo_unregister_thread(&allocating_threads); | |
| } | |
| /* | |
| * We track how much memory has been allocated and freed. When we unload the module, we log an | |
| * error if we have not freed all the memory that we allocated. Nearly all memory allocation and | |
| * freeing is done using this module. | |
| * | |
| * We do not use kernel functions like the kvasprintf() method, which allocate memory indirectly | |
| * using kmalloc. | |
| * | |
| * These data structures and methods are used to track the amount of memory used. | |
| */ | |
| /* | |
| * We allocate very few large objects, and allocation/deallocation isn't done in a | |
| * performance-critical stage for us, so a linked list should be fine. | |
| */ | |
| struct vmalloc_block_info { | |
| void *ptr; | |
| size_t size; | |
| struct vmalloc_block_info *next; | |
| }; | |
| static struct { | |
| spinlock_t lock; | |
| size_t kmalloc_blocks; | |
| size_t kmalloc_bytes; | |
| size_t vmalloc_blocks; | |
| size_t vmalloc_bytes; | |
| size_t peak_bytes; | |
| struct vmalloc_block_info *vmalloc_list; | |
| } memory_stats __cacheline_aligned; | |
| static void update_peak_usage(void) | |
| { | |
| size_t total_bytes = memory_stats.kmalloc_bytes + memory_stats.vmalloc_bytes; | |
| if (total_bytes > memory_stats.peak_bytes) | |
| memory_stats.peak_bytes = total_bytes; | |
| } | |
| static void add_kmalloc_block(size_t size) | |
| { | |
| unsigned long flags; | |
| spin_lock_irqsave(&memory_stats.lock, flags); | |
| memory_stats.kmalloc_blocks++; | |
| memory_stats.kmalloc_bytes += size; | |
| update_peak_usage(); | |
| spin_unlock_irqrestore(&memory_stats.lock, flags); | |
| } | |
| static void remove_kmalloc_block(size_t size) | |
| { | |
| unsigned long flags; | |
| spin_lock_irqsave(&memory_stats.lock, flags); | |
| memory_stats.kmalloc_blocks--; | |
| memory_stats.kmalloc_bytes -= size; | |
| spin_unlock_irqrestore(&memory_stats.lock, flags); | |
| } | |
| static void add_vmalloc_block(struct vmalloc_block_info *block) | |
| { | |
| unsigned long flags; | |
| spin_lock_irqsave(&memory_stats.lock, flags); | |
| block->next = memory_stats.vmalloc_list; | |
| memory_stats.vmalloc_list = block; | |
| memory_stats.vmalloc_blocks++; | |
| memory_stats.vmalloc_bytes += block->size; | |
| update_peak_usage(); | |
| spin_unlock_irqrestore(&memory_stats.lock, flags); | |
| } | |
| static void remove_vmalloc_block(void *ptr) | |
| { | |
| struct vmalloc_block_info *block; | |
| struct vmalloc_block_info **block_ptr; | |
| unsigned long flags; | |
| spin_lock_irqsave(&memory_stats.lock, flags); | |
| for (block_ptr = &memory_stats.vmalloc_list; | |
| (block = *block_ptr) != NULL; | |
| block_ptr = &block->next) { | |
| if (block->ptr == ptr) { | |
| *block_ptr = block->next; | |
| memory_stats.vmalloc_blocks--; | |
| memory_stats.vmalloc_bytes -= block->size; | |
| break; | |
| } | |
| } | |
| spin_unlock_irqrestore(&memory_stats.lock, flags); | |
| if (block != NULL) | |
| vdo_free(block); | |
| else | |
| vdo_log_info("attempting to remove ptr %px not found in vmalloc list", ptr); | |
| } | |
| /* | |
| * Determine whether allocating a memory block should use kmalloc or __vmalloc. | |
| * | |
| * vmalloc can allocate any integral number of pages. | |
| * | |
| * kmalloc can allocate any number of bytes up to a configured limit, which defaults to 8 megabytes | |
| * on some systems. kmalloc is especially good when memory is being both allocated and freed, and | |
| * it does this efficiently in a multi CPU environment. | |
| * | |
| * kmalloc usually rounds the size of the block up to the next power of two, so when the requested | |
| * block is bigger than PAGE_SIZE / 2 bytes, kmalloc will never give you less space than the | |
| * corresponding vmalloc allocation. Sometimes vmalloc will use less overhead than kmalloc. | |
| * | |
| * The advantages of kmalloc do not help out UDS or VDO, because we allocate all our memory up | |
| * front and do not free and reallocate it. Sometimes we have problems using kmalloc, because the | |
| * Linux memory page map can become so fragmented that kmalloc will not give us a 32KB chunk. We | |
| * have used vmalloc as a backup to kmalloc in the past, and a follow-up vmalloc of 32KB will work. | |
| * But there is no strong case to be made for using kmalloc over vmalloc for these size chunks. | |
| * | |
| * The kmalloc/vmalloc boundary is set at 4KB, and kmalloc gets the 4KB requests. There is no | |
| * strong reason for favoring either kmalloc or vmalloc for 4KB requests, except that tracking | |
| * vmalloc statistics uses a linked list implementation. Using a simple test, this choice of | |
| * boundary results in 132 vmalloc calls. Using vmalloc for requests of exactly 4KB results in an | |
| * additional 6374 vmalloc calls, which is much less efficient for tracking. | |
| * | |
| * @size: How many bytes to allocate | |
| */ | |
| static inline bool use_kmalloc(size_t size) | |
| { | |
| return size <= PAGE_SIZE; | |
| } | |
| /* | |
| * Allocate storage based on memory size and alignment, logging an error if the allocation fails. | |
| * The memory will be zeroed. | |
| * | |
| * @size: The size of an object | |
| * @align: The required alignment | |
| * @what: What is being allocated (for error logging) | |
| * @ptr: A pointer to hold the allocated memory | |
| * | |
| * Return: VDO_SUCCESS or an error code | |
| */ | |
| int vdo_allocate_memory(size_t size, size_t align, const char *what, void *ptr) | |
| { | |
| /* | |
| * The __GFP_RETRY_MAYFAIL flag means the VM implementation will retry memory reclaim | |
| * procedures that have previously failed if there is some indication that progress has | |
| * been made elsewhere. It can wait for other tasks to attempt high level approaches to | |
| * freeing memory such as compaction (which removes fragmentation) and page-out. There is | |
| * still a definite limit to the number of retries, but it is a larger limit than with | |
| * __GFP_NORETRY. Allocations with this flag may fail, but only when there is genuinely | |
| * little unused memory. While these allocations do not directly trigger the OOM killer, | |
| * their failure indicates that the system is likely to need to use the OOM killer soon. | |
| * The caller must handle failure, but can reasonably do so by failing a higher-level | |
| * request, or completing it only in a much less efficient manner. | |
| */ | |
| const gfp_t gfp_flags = GFP_KERNEL | __GFP_ZERO | __GFP_RETRY_MAYFAIL; | |
| unsigned int noio_flags; | |
| bool allocations_restricted = !allocations_allowed(); | |
| unsigned long start_time; | |
| void *p = NULL; | |
| if (unlikely(ptr == NULL)) | |
| return -EINVAL; | |
| if (size == 0) { | |
| *((void **) ptr) = NULL; | |
| return VDO_SUCCESS; | |
| } | |
| if (allocations_restricted) | |
| noio_flags = memalloc_noio_save(); | |
| start_time = jiffies; | |
| if (use_kmalloc(size) && (align < PAGE_SIZE)) { | |
| p = kmalloc(size, gfp_flags | __GFP_NOWARN); | |
| if (p == NULL) { | |
| /* | |
| * It is possible for kmalloc to fail to allocate memory because there is | |
| * no page available. A short sleep may allow the page reclaimer to | |
| * free a page. | |
| */ | |
| fsleep(1000); | |
| p = kmalloc(size, gfp_flags); | |
| } | |
| if (p != NULL) | |
| add_kmalloc_block(ksize(p)); | |
| } else { | |
| struct vmalloc_block_info *block; | |
| if (vdo_allocate(1, __func__, &block) == VDO_SUCCESS) { | |
| /* | |
| * It is possible for __vmalloc to fail to allocate memory because there | |
| * are no pages available. A short sleep may allow the page reclaimer | |
| * to free enough pages for a small allocation. | |
| * | |
| * For larger allocations, the page_alloc code is racing against the page | |
| * reclaimer. If the page reclaimer can stay ahead of page_alloc, the | |
| * __vmalloc will succeed. But if page_alloc overtakes the page reclaimer, | |
| * the allocation fails. It is possible that more retries will succeed. | |
| */ | |
| for (;;) { | |
| p = __vmalloc(size, gfp_flags | __GFP_NOWARN); | |
| if (p != NULL) | |
| break; | |
| if (jiffies_to_msecs(jiffies - start_time) > 1000) { | |
| /* Try one more time, logging a failure for this call. */ | |
| p = __vmalloc(size, gfp_flags); | |
| break; | |
| } | |
| fsleep(1000); | |
| } | |
| if (p == NULL) { | |
| vdo_free(block); | |
| } else { | |
| block->ptr = p; | |
| block->size = PAGE_ALIGN(size); | |
| add_vmalloc_block(block); | |
| } | |
| } | |
| } | |
| if (allocations_restricted) | |
| memalloc_noio_restore(noio_flags); | |
| if (unlikely(p == NULL)) { | |
| vdo_log_error("Could not allocate %zu bytes for %s in %u msecs", | |
| size, what, jiffies_to_msecs(jiffies - start_time)); | |
| return -ENOMEM; | |
| } | |
| *((void **) ptr) = p; | |
| return VDO_SUCCESS; | |
| } | |
| /* | |
| * Allocate storage based on memory size, failing immediately if the required memory is not | |
| * available. The memory will be zeroed. | |
| * | |
| * @size: The size of an object. | |
| * @what: What is being allocated (for error logging) | |
| * | |
| * Return: pointer to the allocated memory, or NULL if the required space is not available. | |
| */ | |
| void *vdo_allocate_memory_nowait(size_t size, const char *what __maybe_unused) | |
| { | |
| void *p = kmalloc(size, GFP_NOWAIT | __GFP_ZERO); | |
| if (p != NULL) | |
| add_kmalloc_block(ksize(p)); | |
| return p; | |
| } | |
| void vdo_free(void *ptr) | |
| { | |
| if (ptr != NULL) { | |
| if (is_vmalloc_addr(ptr)) { | |
| remove_vmalloc_block(ptr); | |
| vfree(ptr); | |
| } else { | |
| remove_kmalloc_block(ksize(ptr)); | |
| kfree(ptr); | |
| } | |
| } | |
| } | |
| /* | |
| * Reallocate dynamically allocated memory. There are no alignment guarantees for the reallocated | |
| * memory. If the new memory is larger than the old memory, the new space will be zeroed. | |
| * | |
| * @ptr: The memory to reallocate. | |
| * @old_size: The old size of the memory | |
| * @size: The new size to allocate | |
| * @what: What is being allocated (for error logging) | |
| * @new_ptr: A pointer to hold the reallocated pointer | |
| * | |
| * Return: VDO_SUCCESS or an error code | |
| */ | |
| int vdo_reallocate_memory(void *ptr, size_t old_size, size_t size, const char *what, | |
| void *new_ptr) | |
| { | |
| int result; | |
| char *temp_ptr; | |
| if (size == 0) { | |
| vdo_free(ptr); | |
| *(void **) new_ptr = NULL; | |
| return VDO_SUCCESS; | |
| } | |
| result = vdo_allocate(size, what, &temp_ptr); | |
| if (result != VDO_SUCCESS) | |
| return result; | |
| *(void **) new_ptr = temp_ptr; | |
| if (ptr != NULL) { | |
| if (old_size < size) | |
| size = old_size; | |
| memcpy(*((void **) new_ptr), ptr, size); | |
| vdo_free(ptr); | |
| } | |
| return VDO_SUCCESS; | |
| } | |
| int vdo_duplicate_string(const char *string, const char *what, char **new_string) | |
| { | |
| int result; | |
| u8 *dup; | |
| result = vdo_allocate(strlen(string) + 1, what, &dup); | |
| if (result != VDO_SUCCESS) | |
| return result; | |
| memcpy(dup, string, strlen(string) + 1); | |
| *new_string = dup; | |
| return VDO_SUCCESS; | |
| } | |
| void vdo_memory_init(void) | |
| { | |
| spin_lock_init(&memory_stats.lock); | |
| vdo_initialize_thread_registry(&allocating_threads); | |
| } | |
| void vdo_memory_exit(void) | |
| { | |
| VDO_ASSERT_LOG_ONLY(memory_stats.kmalloc_bytes == 0, | |
| "kmalloc memory used (%zd bytes in %zd blocks) is returned to the kernel", | |
| memory_stats.kmalloc_bytes, memory_stats.kmalloc_blocks); | |
| VDO_ASSERT_LOG_ONLY(memory_stats.vmalloc_bytes == 0, | |
| "vmalloc memory used (%zd bytes in %zd blocks) is returned to the kernel", | |
| memory_stats.vmalloc_bytes, memory_stats.vmalloc_blocks); | |
| vdo_log_debug("peak usage %zd bytes", memory_stats.peak_bytes); | |
| } | |
| void vdo_get_memory_stats(u64 *bytes_used, u64 *peak_bytes_used) | |
| { | |
| unsigned long flags; | |
| spin_lock_irqsave(&memory_stats.lock, flags); | |
| *bytes_used = memory_stats.kmalloc_bytes + memory_stats.vmalloc_bytes; | |
| *peak_bytes_used = memory_stats.peak_bytes; | |
| spin_unlock_irqrestore(&memory_stats.lock, flags); | |
| } | |
| /* | |
| * Report stats on any allocated memory that we're tracking. Not all allocation types are | |
| * guaranteed to be tracked in bytes (e.g., bios). | |
| */ | |
| void vdo_report_memory_usage(void) | |
| { | |
| unsigned long flags; | |
| u64 kmalloc_blocks; | |
| u64 kmalloc_bytes; | |
| u64 vmalloc_blocks; | |
| u64 vmalloc_bytes; | |
| u64 peak_usage; | |
| u64 total_bytes; | |
| spin_lock_irqsave(&memory_stats.lock, flags); | |
| kmalloc_blocks = memory_stats.kmalloc_blocks; | |
| kmalloc_bytes = memory_stats.kmalloc_bytes; | |
| vmalloc_blocks = memory_stats.vmalloc_blocks; | |
| vmalloc_bytes = memory_stats.vmalloc_bytes; | |
| peak_usage = memory_stats.peak_bytes; | |
| spin_unlock_irqrestore(&memory_stats.lock, flags); | |
| total_bytes = kmalloc_bytes + vmalloc_bytes; | |
| vdo_log_info("current module memory tracking (actual allocation sizes, not requested):"); | |
| vdo_log_info(" %llu bytes in %llu kmalloc blocks", | |
| (unsigned long long) kmalloc_bytes, | |
| (unsigned long long) kmalloc_blocks); | |
| vdo_log_info(" %llu bytes in %llu vmalloc blocks", | |
| (unsigned long long) vmalloc_bytes, | |
| (unsigned long long) vmalloc_blocks); | |
| vdo_log_info(" total %llu bytes, peak usage %llu bytes", | |
| (unsigned long long) total_bytes, (unsigned long long) peak_usage); | |
| } |