Beyond Memory Limits: An in-depth exploration of how memory pools work and are implemented

This article is shared from the Huawei Cloud Community "Beyond Memory Limits: In-depth Exploration of the Working Principles and Implementation of Memory Pools" by Lion Long.

I. Introduction

Why do you need a memory pool?

At the system application level, program development uses virtual memory. Physical memory is the bottom layer and can only be accessed by underlying programs (such as drivers, firmware, etc.).

The memory that programs can usually manage is mainly heap and shared memory (mmap). The so-called memory management at the application layer mainly manages the memory pool on the heap.

When a program uses memory, it needs to apply for memory by calling malloc() / callol(); after use, it needs to release the memory and call free(). When the program is running, it will continuously apply for and release memory. You will find that the memory may become uncontrollable later. For example, there is still total available memory, but it cannot be allocated. This is memory fragmentation. There are many small windows in the memory.

Therefore, memory management is required, so a memory pool exists. Avoid memory fragmentation and frequent memory allocation and release through memory management.

The relationship between new and malloc/callol: new is a keyword, and malloc/callol is called internally. Delete, like free, releases memory.

2. Memory management method

When memory is allocated, the allocated size and when it is allocated and when it is released are uncertain. Therefore, there are different memory management methods for different common situations.

(1) Regardless of the required memory size, a fixed size of memory is allocated each time. This can effectively avoid memory fragmentation, but has low memory utilization.
image.png

(2) Accumulate the memory pool with 2n. It can improve memory utilization, but recycling is a big project, and there is no way to combine two adjacent pieces of memory.

image.png

(3) Large and small pieces. The memory pool is divided into large and small blocks. The applied memory size is larger than a certain value and is classified as large blocks. Otherwise, it is small blocks. Linked lists are used internally for concatenation.
image.png

 

3. posix_memalign() and malloc()

malloc/alloc function prototype:

#include <stdlib.h>

void *malloc(size_t size);
void free(void *ptr);
void *calloc(size_t nmemb, size_t size);
void *realloc(void *ptr, size_t size);

describe:

The role of the malloc function is to allocate size bytes and return a pointer to the allocated memory. The allocated memory is not initialized. size=0, malloc returns NULL or a unique pointer value, which can be successfully passed to free() later.

The free function releases the memory space pointed to by ptr, which must be returned by a previous call to malloc(), calloc() or realloc(). Otherwise, or if free(ptr) has been called previously, undefined behavior occurs. If ptr is empty, no action is performed.

The calloc function allocates memory for each size byte array of nmemb elements and returns a pointer to the allocated memory. Memory is initialized to zero. If nmemb or size is 0, calloc() returns NULL or a unique pointer value that can later be successfully passed to free().

The realloc function changes the size of the memory block pointed to by ptr to size bytes. The content will remain unchanged from the start of the area to the minimum of the old and new dimensions. If the new size is larger than the old size, the added memory will not be initialized. If ptr is empty, the call is equivalent to malloc(size) for all values ​​of size; if size is equal to zero and ptr is not empty, the call is equivalent to free(ptr). Unless ptr is empty, it must have been returned by a previous call to malloc(), calloc(), or realloc(). If the pointed area is moved, free(ptr) is executed.

return value:

The malloc() and calloc() functions return a pointer to allocated memory suitable for any built-in type. When an error occurs, these functions return NULL. NULL may also be returned if a successful call to malloc() with a size of zero, or a successful call to nmemb or calloc() with a size equal to zero.

The free() function does not return any value.

realloc() returns a pointer to newly allocated memory suitable for any built-in type, which may be different from ptr, or NULL if the request fails. If size=0, returns NULL or a pointer suitable for passing to free(). If realloc() fails, the original block remains unchanged; it is not freed or moved.

mistake:

calloc(), malloc(), and realloc() may fail with the following error:

ENOMEM, insufficient memory. An application may reach the RLIMIT_AS or RLIMIT-DATA limit described in getrlimit().

There are limits to the memory allocated by malloc/alloc. It may not be able to allocate more than 4k of memory. In order to allocate large memory, you need to use the posix_memalign function.

posix_memalign function prototype:

#include <stdlib.h>

int posix_memalign(void **memptr, size_t alignment, size_t size);
void *aligned_alloc(size_t alignment, size_t size);
void *valloc(size_t size);

#include <malloc.h>

void *memalign(size_t alignment, size_t size);
void *pvalloc(size_t size);

describe:

The function posix_memalign allocates size bytes and places the address of the allocated memory in memptr. The address at which memory is allocated will be a multiple of alignment, which must be a power of 2 and a multiple of sizeof(void). If size is 0, the value placed in *memptr is either null or a unique pointer value that can later be successfully passed to free().

return:

posix_memalign() returns zero on success, or an error value on failure. After calling posix_memalign(), the value of errno is undefined.

Error value:

  • EINVAL: The alignment parameter is not a power of 2, or is not a multiple of sizeof(void*).
  • ENOMEM: Not enough memory to complete the allocation request.

4. Alignment calculation

To allocate memory aligned at a specified size, use the following formula:

Assume that the allocation size is n and the alignment is x, then size=(n+(x-1)) & (~(x-1)). 

for example:

n=17, x=4. That is, the application size is 17 and the alignment is 4. Then the calculated size after alignment should be
(17+4-1)&(~(4-1))=20;

Calculated in binary, (0001 0001 + 0011) & (1111 1100) = 0001 0100

// align
#define mp_align(n, alignment) (((n)+(alignment-1)) & ~(alignment-1))
#define mp_align_ptr(p, alignment) (void *)((((size_t)p)+(alignment-1)) & ~(alignment-1))

5. Specific implementation of memory pool

5.1. Definition of memory pool

typedef struct mp_large_s {
	struct mp_large_s *next;
	void *alloc;

}mp_large_t;

typedef struct mp_node_s {
	unsigned char *last; // last is the used memory before
	unsigned char *end; // Allocable memory between last and end
	struct mp_node_s *next;
	size_t failed;
}mp_node_t;

typedef struct mp_pool_s {
	size_t max;

	mp_node_t* current;
	mp_large_t* large;

	mp_node_t head[0];

}mp_pool_t;

5.2. Creation of memory pool

mp_pool_t *mp_create_pool(size_t size)
{
	mp_pool_t *p;
	// malloc cannot allocate more than 4k of memory, size + sizeof(mp_pool_t) + sizeof(mp_node_s) guarantees that size is available
	int ret = posix_memalign((void*)&p, MP_ALIGNMENT, size + sizeof(mp_pool_t) + sizeof(mp_node_t));
	if (ret)
		return NULL;

	p->max = size;
	p->current = p->head;
	p->large = NULL;

	//(unsigned char*)(p + 1)
	// (unsigned char*)p + sizeof(mp_pool_t)
	p->head->last = (unsigned char*)p + sizeof(mp_pool_t)+sizeof(mp_node_t);
	p->head->end = p->head->last + size;
	p->head->failed = 0;

	return p;
}

5.3. Destruction of memory pool

void mp_destory_pool(mp_pool_t *pool) 
{
	mp_node_t *h, *n;
	mp_large_t *l;

	for (l = pool->large; l; l = l->next) {
		if (l->alloc) {
			free(l->alloc);
		}
	}

	h = pool->head->next;

	while (h) {
		n = h->next;
		free(h);
		h = n;
	}

	free(pool);
}

5.4. Memory pool reset

void mp_reset_pool(mp_pool_t *pool) 
{

	mp_node_t *h;
	mp_large_t *l;

	for (l = pool->large; l; l = l->next) {
		if (l->alloc) {
			free(l->alloc);
		}
	}

	pool->large = NULL;

	for (h = pool->head; h; h = h->next) {
		h->last = (unsigned char *)h + sizeof(mp_node_t);
	}

}

5.5. Memory pool allocates small blocks

void *mp_alloc_small(mp_pool_t *pool, size_t size)
{
	unsigned char *m;

	struct mp_node_s *h = pool->head;
	size_t psize = (size_t)(h->end - (unsigned char *)h);
	int ret = posix_memalign((void*)&m, MP_ALIGNMENT, psize);
	if (ret)
		return NULL;

	mp_node_t *p, *new_node, *current;

	new_node = (mp_node_t *)m;
	new_node->next = NULL;
	new_node->end = m + psize;
	new_node->failed = 0;
	m += sizeof(mp_node_t);
	m = mp_align_ptr(m, MP_ALIGNMENT);
	new_node->last += size;

	current = pool->current;
	for (p = current; p->next; p = p->next)
	{
		// If there are multiple allocation failures, current no longer points to this node.
		if (p->failed++ > 4)
		{
			current = p->next;
		}
	}
	p->next = new_node;
	pool->current = current ? current : new_node;

	return m;
}

5.6. Memory pool allocates large blocks

static void *mp_alloc_large(mp_pool_t *pool, size_t size) 
{
	void *p = NULL;
	int ret = posix_memalign((void*)&p, MP_ALIGNMENT, size);
	if (ret)
		return NULL;
	
	mp_large_t *large;
	
	//Check whether there is a large that has been released, and find a null node in the large list
	size_t n = 0;
	for (large = pool->large; large; large = large->next)
	{
		if (large->alloc == NULL)
		{
			large->alloc = p;
			return p;
		}
		// Avoid traversing too long a chain
		if (n++ > 3)
			break;
	}

	//The header of the large memory block is stored in small as a small block
	large = mp_alloc_small(pool, sizeof(mp_large_t));

	//Head insertion method
	large->alloc = p;
	large->next = pool->large;
	pool->large = large;
}

5.7. Apply for memory

void *mp_malloc(mp_pool_t *pool, size_t size)
{
	if (size > pool->max)
		return mp_alloc_large(pool, size);
	mp_node_t *p = pool->current;
	while (p)
	{
		
		if (p->end - p->last < size)
		{
			p = p->next;
			continue;
		}

		unsigned char *m = mp_align_ptr(p->last, MP_ALIGNMENT);
		p->last = m + size;
		return m;
	}
	
	return mp_alloc_small(pool, size);
}

void *mp_calloc(mp_pool_t *pool, size_t size) 
{

	void *p = mp_malloc(pool, size);
	if (p) {
		memset(p, 0, size);
	}

	return p;

}

5.8. Release memory

void mp_free(mp_pool_t *pool, void *p)
{
	mp_large_t *l;
	for (l = pool->large; l; l = l->next)
	{
		if (p == l->alloc)
		{
			free(l->alloc);
			l->alloc = NULL;
			return;
		}
	}
}

5.9. Complete sample code

In order to avoid the article being too long, the complete code has been uploaded to gitee: memory pool complete sample code .

Summarize

Designing a memory pool can effectively avoid memory fragmentation and avoid frequent memory creation and release. The memory that programs can usually manage is mainly heap and shared memory (mmap). The so-called memory management at the application layer mainly manages the memory pool on the heap.

The most commonly used memory management method is 2n stacked memory pool and large and small block management. nginx uses the large and small block method to manage memory; it establishes its own memory pool for each IO, and releases the memory after the IO life cycle ends.

Click to follow and learn about Huawei Cloud’s new technologies as soon as possible~

Fined 200 yuan and more than 1 million yuan confiscated You Yuxi: The importance of high-quality Chinese documents Musk’s hard-core migration of servers TCP congestion control saved the Internet Apache OpenOffice is a de facto “unmaintained” project Google celebrates its 25th anniversary Microsoft open source windows-drivers-rs, use Rust to develop Windows drivers Raspberry Pi 5 will be released at the end of October, priced from $60 macOS Containers: Use Docker to run macOS images on macOS IntelliJ IDEA 2023.3 EAP released
{{o.name}}
{{m.name}}

Guess you like

Origin my.oschina.net/u/4526289/blog/10114391