440 lines · 13.3 KB · C
RawDownload
// 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);}