RT-Thread Chain List Heap Manager for a Series of Practical Algorithms

The previous section describes the basic concept of stack. This article will talk about how stack works.RT-Thread is so popular in the community that it reads its kernel code, manages the heap, and has clear code design and readability.Therefore, on the one hand, you can understand the RT-Thread kernel implementation, and on the other hand, you can understand the internal implementation of its heap.Share the learning experience records in the hope of a deeper understanding of the heap and its implementation.

Note that the code analysis in this article is based on rt-thread-v4.0.2.

What is a heap?

The C language heap is a mechanism by which functions such as malloc(),calloc(),realloc() dynamically acquire memory.After use, functions such as free() are called by the programmer to release.When using, you need to include the stdlib.h header file.

The heap management predicted by C++ uses the new operator to request dynamic memory allocation from the heap manager, and the delete operator to release the used up memory to the heap manager.

Note: This article only describes heap manager implementation for C.

For example, in C, translate the above description into a picture:

To manage a piece of memory dynamically and to allocate the release dynamically, this is a requirement.Clearly, C language needs to abstract the dynamic memory area and manage it dynamically.In fact, heap managers in C are essentially abstract descriptions of heap areas using data structures, which need to be described:

  • Memory available for allocation
  • Memory block in use
  • Released memory blocks

The heap manager is implemented by using the corresponding algorithm to manage such data structure objects dynamically.

It is common to see that many of the algorithmic books only talk about the principles of the algorithmic, not about the application examples, and often do not have a deep understanding.I thought I could do something better.Why bother learning when you can't apply it?So instead of obscure algorithms being useless, they are not really combined.It can be further thought that if the algorithm has no application scenario, it will be gradually forgotten in the development of technology.Therefore, when learning to read algorithm books, find some examples to see, will certainly deepen the understanding of the algorithm.This is an important off-topic topic, and it's for your mutual encouragement.

So, in essence, heap manager is a dynamic memory manager implemented by data structure + algorithm to manage the dynamic allocation and release of memory.

Why heap?

The C programming language has static, automatic, or dynamic memory management methods.Variables for static memory allocation are usually allocated in the main memory along with the program's executable code and are valid throughout the program's life cycle.Variables that automatically allocate memory are allocated on the stack and requested or released as functions are called and returned.For the life cycle of statically allocated and automatically allocated memory, the allocated size must be a compile-time constant (except for variable-length automatic arrays [5]).If the required memory size is not known until runtime (for example, if you want to read data of any size from a user or disk file), using a fixed-size data object will not be sufficient.Imagine that even if you assume you know how much memory you want, such as so many applications under windows/Linux, each application loads with a static allocation policy that will sample the memory needed to run, and multiple programs run out of memory quickly.

The life cycle of allocated memory may also be of concern.Neither static nor automatic allocation can satisfy all cases.Automatic memory allocation cannot be maintained between multiple function calls, and static data must be maintained throughout the life cycle of the program, whether it is really needed or not (so such a strategy is inevitably wasteful).In many cases, programmers have more flexibility in managing the life cycle of allocated memory.

By using dynamic memory allocation, these limitations/shortcomings are avoided. In dynamic memory allocation, memory is managed more explicitly (but more flexibly), usually by allocating memory (a region of memory constructed for this purpose) from free storage (informally known as a heap).In C, the library function malloc is used to allocate a block of memory on the heap.The program accesses the memory block through the pointer returned by malloc.When memory is no longer needed, the pointer is passed to free, freeing up memory so that it can be used for other purposes.

Who implements heap

If you ask this question, you'll immediately talk about the C compiler.Good C compilers implement heap managers, not dynamic memory managers during compilation, but C libraries implemented by C compilers implement heap managers, such as ANSI C, VC, IAR C compilers, GNU C, and so on. These libraries actually need support from some C libraries, so the internal structure of these libraries hides such a heap manager.Seeing is believing, or taking IAR ARM 8.40.1 as an example, its heap manager is implemented in:

.\IAR Systems\Embedded Workbench 8.3\arm\src\lib\dlib\heap

With so many sources, what options do you need to configure for application development?

Four options are supported:

  • Automatic:
    • If your application has calls to heap memory allocation routines but no calls to heap release routines, the linker automatically selects no free heap.
    • If your application has calls to the heap memory allocation routine, the linker automatically selects the advanced heap.
    • For example, if a heap memory allocation routine is called in a library, the linker automatically selects the base heap.
  • Advanced heap: Advanced heap (--advanced_heap) Provides effective memory management for applications that use the heap extensively.In particular, applications that repeatedly allocate and release memory may incur less space and time overhead.The code for the advanced heap is significantly larger than that for the base heap.
  • Basic heap: Basic heap (--basic_heap) is a simple heap allocator for applications that do not use heaps very often.In particular, it can be used in applications that allocate only heap memory but never release it.The base heap is not particularly fast, and using it in applications that repeatedly free memory can potentially lead to unnecessary heap fragmentation.The code for the basic heap is much smaller than the size of the advanced heap.
  • No-free heap: No heap available (--no_free_heap) Use this option to use the smallest heap implementation.Since this heap does not support release or reallocation, it is only applicable to applications that allocate heap memory for various buffers during startup and applications that never release memory.

But this understanding is incomplete if you think that only standard C libraries are responsible for implementing heap managers.Back to the nature of things, heap managers use data structures and algorithms to dynamically manage the allocation and release of a piece of memory.Where this is the case, you may need to implement a heap manager.

The implementation of heap manager largely depends on the operating system and hardware architecture.There are two broad categories of heap memory managers that generally need to be implemented:

  • It is obvious that applications require a heap memory manager.For example, common applications under windows/Linux require a heap memory manager.Heap memory manager is required when cortex M or other MCU programs are programmed with C/C++.
  • The operating system kernel, which allocates memory like an application.However, the implementation of malloc in the kernel is usually very different from the implementation used by the C library.For example, memory buffers may need to comply with special restrictions imposed by DMA, or memory allocation functions may be invoked from an interrupt context.This requires a malloc implementation that is tightly integrated with the virtual memory subsystem of the operating system kernel.For example, the Linux kernel needs to implement a kernel version of the heap manager, provide external kmalloc/vmalloc request memory, and kfree/vfree is used to free memory.

How to implement heap

For the kernel of RT-Thread, a kernel heap manager is also implemented. Here, comb the implementation of the small heap manager of the RT-Thread kernel version, and learn about the chain table data structure and the application of algorithm operations.

Its heap manager implementation is located at mem.c, memheap.c, and mempool.c under. \rt-thread-v4.0.2\rt-thread\src.

Key data structures

The main data structure of its heap manager is heap_mem.

  • heap_mem

Heap Manager Initialization

The heap manager initialization entry is in mem.c. The function is:

void rt_system_heap_init(void *begin_addr, void *end_addr)
{
    struct heap_mem *mem;
    /*Align converted addresses by 4 bytes*/
    /*If 0x2000 0001-0x2000 0003, then 0x2000 0004*/
    rt_ubase_t begin_align = RT_ALIGN((rt_ubase_t)begin_addr, RT_ALIGN_SIZE);
    /*If 0x30000001-0x30000003, then 0x30000000*/
    rt_ubase_t end_align   = RT_ALIGN_DOWN((rt_ubase_t)end_addr, RT_ALIGN_SIZE);
    
    /*Debugging information, function not available inside interrupt*/
    RT_DEBUG_NOT_IN_INTERRUPT;

    /* Allocate address ranges to store at least two heaps_MEM */
    if ((end_align > (2 * SIZEOF_STRUCT_MEM)) &&
        ((end_align - 2 * SIZEOF_STRUCT_MEM) >= begin_align))
    {
        /* Compute available heap area, 4 byte alignment */
        mem_size_aligned = end_align - begin_align - 2 * SIZEOF_STRUCT_MEM;
    }
    else
    {
        rt_kprintf("mem init, error begin address 0x%x, and end address 0x%x\n",
                   (rt_ubase_t)begin_addr, (rt_ubase_t)end_addr);

        return;
    }

    /* heap_ptr Point to heap start address */
    heap_ptr = (rt_uint8_t *)begin_align;

    RT_DEBUG_LOG(RT_DEBUG_MEM, ("mem init, heap begin address 0x%x, size %d\n",
                                (rt_ubase_t)heap_ptr, mem_size_aligned));

    /* Initialization heap start descriptor */
    mem        = (struct heap_mem *)heap_ptr;
    mem->magic = HEAP_MAGIC;
    mem->next  = mem_size_aligned + SIZEOF_STRUCT_MEM;
    mem->prev  = 0;
    mem->used  = 0;
#ifdef RT_USING_MEMTRACE
    rt_mem_setname(mem, "INIT");
#endif

    /* Initialization heap end descriptor */
    heap_end        = (struct heap_mem *)&heap_ptr[mem->next];
    heap_end->magic = HEAP_MAGIC;
    heap_end->used  = 1;
    heap_end->next  = mem_size_aligned + SIZEOF_STRUCT_MEM;
    heap_end->prev  = mem_size_aligned + SIZEOF_STRUCT_MEM;
#ifdef RT_USING_MEMTRACE
    rt_mem_setname(heap_end, "INIT");
#endif

    rt_sem_init(&heap_sem, "heap", 1, RT_IPC_FLAG_FIFO);

    /* Initial release pointer points to the start of the heap */
    lfree = (struct heap_mem *)heap_ptr;
}

The memory start address and end address of the incoming link heap area.Take STM32 as an example, passing in 0x20000000--0x20018000, 96k bytes

Rt_aboveSystem_Heap_Init (0x20000000,0x20018000), mainly does the following thing.

The heap management header and tail descriptors are initialized and point to the corresponding memory address.Translate it with pictures:

Tip points:

  • Convert memory data to struct heap_using type castMEM *.Creation of static double-chain table is implemented
mem      = (struct heap_mem *)heap_ptr;
heap_end = (struct heap_mem *)&heap_ptr[mem->next];
  • Define heap_The MEM does not define how many bytes to use for the user data bytes of the block, saving memory.Is a better way to handle it.
  • Configurable alignment, RT_ALIGN_SIZE defaults to 4 bytes.

Request memory from heap

User calls rt_malloc is used to request dynamic memory allocation.

void *rt_malloc(rt_size_t size)
{
    rt_size_t ptr, ptr2;
    struct heap_mem *mem, *mem2;

    if (size == 0)
        return RT_NULL;

    RT_DEBUG_NOT_IN_INTERRUPT;
    /*Align requests by four bytes, if 5 bytes are requested, 8 bytes are actually requested*/
    if (size != RT_ALIGN(size, RT_ALIGN_SIZE))
        RT_DEBUG_LOG(RT_DEBUG_MEM, ("malloc size %d, but align to %d\n",
                                    size, RT_ALIGN(size, RT_ALIGN_SIZE)));
    else
        RT_DEBUG_LOG(RT_DEBUG_MEM, ("malloc size %d\n", size));

    /* Align requests by four bytes, if 5 bytes are requested, 8 bytes are actually requested */
    size = RT_ALIGN(size, RT_ALIGN_SIZE);

    if (size > mem_size_aligned)
    {
        RT_DEBUG_LOG(RT_DEBUG_MEM, ("no memory\n"));
        return RT_NULL;
    }

    /* Each block must be at least MIN_longSIZE_ALIGNED=12 STM32*/
    if (size < MIN_SIZE_ALIGNED)
        size = MIN_SIZE_ALIGNED;

    /* Get heap protection semaphore */
    rt_sem_take(&heap_sem, RT_WAITING_FOREVER);

    for (ptr = (rt_uint8_t *)lfree - heap_ptr;
         ptr < mem_size_aligned - size;
         ptr = ((struct heap_mem *)&heap_ptr[ptr])->next)
    {
        mem = (struct heap_mem *)&heap_ptr[ptr];

        /*If the block is unused and meets size requirements*/
        if ((!mem->used) && (mem->next - (ptr + SIZEOF_STRUCT_MEM)) >= size)
        {
            /* mem Not used, at least perfect fit is possible:
             * mem->next - (ptr + SIZEOF_STRUCT_MEM) Calculate "User Data Size" for mem */
            if (mem->next - (ptr + SIZEOF_STRUCT_MEM) >=
                (size + SIZEOF_STRUCT_MEM + MIN_SIZE_ALIGNED))
            {
                /* (In addition to the above, let's test another structure, heap_mem (SIZEOF_STRUCT_MEM)
                 * Does it contain at least MIN_SIZE_ALIGNED's data is also suitable for'mem'user data space'
                 * -> Divide large blocks, create empty remainder,
                 * The remainder must be large enough to contain MIN_SIZE_ALIGNED size data:
                 * If mem->next - (ptr + (2*SIZEOF_STRUCT_MEM) == size,
                 * struct heap_mem Will fit, not used in Mem2 and mem2->next
                 */
                ptr2 = ptr + SIZEOF_STRUCT_MEM + size;

                /* create mem2 struct */
                mem2       = (struct heap_mem *)&heap_ptr[ptr2];
                mem2->magic = HEAP_MAGIC;
                mem2->used = 0;
                mem2->next = mem->next;
                mem2->prev = ptr;
#ifdef RT_USING_MEMTRACE
                rt_mem_setname(mem2, "    ");
#endif
                /*Insert ptr2 between mem and mem->next */
                mem->next = ptr2;
                mem->used = 1;

                if (mem2->next != mem_size_aligned + SIZEOF_STRUCT_MEM)
                {
                    ((struct heap_mem *)&heap_ptr[mem2->next])->prev = ptr2;
                }
#ifdef RT_MEM_STATS
                used_mem += (size + SIZEOF_STRUCT_MEM);
                if (max_mem < used_mem)
                    max_mem = used_mem;
#endif
            }
            else
            {
                mem->used = 1;
#ifdef RT_MEM_STATS
                used_mem += mem->next - ((rt_uint8_t *)mem - heap_ptr);
                if (max_mem < used_mem)
                    max_mem = used_mem;
#endif
            }
            /* Set Block Magic Number */
            mem->magic = HEAP_MAGIC;
#ifdef RT_USING_MEMTRACE
            if (rt_thread_self())
                rt_mem_setname(mem, rt_thread_self()->name);
            else
                rt_mem_setname(mem, "NONE");
#endif

            if (mem == lfree)
            {
                /* Find the next free block and update the lfree pointer*/
                while (lfree->used && lfree != heap_end)
                    lfree = (struct heap_mem *)&heap_ptr[lfree->next];

                RT_ASSERT(((lfree == heap_end) || (!lfree->used)));
            }

            rt_sem_release(&heap_sem);
            RT_ASSERT((rt_ubase_t)mem + SIZEOF_STRUCT_MEM + size <= (rt_ubase_t)heap_end);
            RT_ASSERT((rt_ubase_t)((rt_uint8_t *)mem + SIZEOF_STRUCT_MEM) % RT_ALIGN_SIZE == 0);
            RT_ASSERT((((rt_ubase_t)mem) & (RT_ALIGN_SIZE - 1)) == 0);

            RT_DEBUG_LOG(RT_DEBUG_MEM,
                         ("allocate memory at 0x%x, size: %d\n",
                          (rt_ubase_t)((rt_uint8_t *)mem + SIZEOF_STRUCT_MEM),
                          (rt_ubase_t)(mem->next - ((rt_uint8_t *)mem - heap_ptr))));

            RT_OBJECT_HOOK_CALL(rt_malloc_hook,
                                (((void *)((rt_uint8_t *)mem + SIZEOF_STRUCT_MEM)), size));

            /* Returns the memory address in addition to the mem structure */
            return (rt_uint8_t *)mem + SIZEOF_STRUCT_MEM;
        }
    }
    /* Release heap protection semaphore */
    rt_sem_release(&heap_sem);

    return RT_NULL;
}

The basic idea is to retrieve the memory block from the free block chain table. If a free block is retrieved and the size of the application is satisfied and the remaining space can store descriptors at least, the request can be satisfied, then the subsequent memory header is generated to generate a description, the before and after pointers are updated, the magic number of markers and the block has been tagged, and the block is inserted into the chain table.Returns the memory address where the request was successful.If it cannot be retrieved, a null pointer is returned, indicating that the request failed and that the heap does not currently have memory available that meets the requirements.In fact, the code above maintains the heap memory area dynamically at run time as illustrated below.

To summarize:

  • heap_ptr always points to heap start address, heap_The end always points to the last block, which works together to achieve boundary protection and is used when memory is freed.
  • lfree always points to the free block with the smallest address, so when dynamically requesting memory, it always retrieves from that block whether there is a memory block available that meets the request requirements.
  • used=1 indicates that the block is occupied and not idle.used=0 means the block is idle.
  • Magic field magic numbers, which start with a special tag word, work with used=0 to detect anomalies. Imagine that if you just use=0 to determine if a block is idle, it is prone to error, or you need additional auxiliary code to ensure code robustness.
  • Dynamic memory management requests are slow, requiring chain table retrieval and additional memory overhead.
  • rt_realloc and rt_calloc is not analyzed

Release memory

Release memory from rt_free implementation:

void rt_free(void *rmem)
{
    struct heap_mem *mem;

    if (rmem == RT_NULL)
        return;

    RT_DEBUG_NOT_IN_INTERRUPT;

    RT_ASSERT((((rt_ubase_t)rmem) & (RT_ALIGN_SIZE - 1)) == 0);
    RT_ASSERT((rt_uint8_t *)rmem >= (rt_uint8_t *)heap_ptr &&
              (rt_uint8_t *)rmem < (rt_uint8_t *)heap_end);

    RT_OBJECT_HOOK_CALL(rt_free_hook, (rmem));
    /* Request release address not in heap area */
    if ((rt_uint8_t *)rmem < (rt_uint8_t *)heap_ptr ||
        (rt_uint8_t *)rmem >= (rt_uint8_t *)heap_end)
    {
        RT_DEBUG_LOG(RT_DEBUG_MEM, ("illegal memory\n"));

        return;
    }

    /* Get Block Descriptor */
    mem = (struct heap_mem *)((rt_uint8_t *)rmem - SIZEOF_STRUCT_MEM);

    RT_DEBUG_LOG(RT_DEBUG_MEM,
                 ("release memory 0x%x, size: %d\n",
                  (rt_ubase_t)rmem,
                  (rt_ubase_t)(mem->next - ((rt_uint8_t *)mem - heap_ptr))));


    /* Get heap protection semaphore */
    rt_sem_take(&heap_sem, RT_WAITING_FOREVER);

    /* Memory to be freed, its block descriptor needs to be in use */
    if (!mem->used || mem->magic != HEAP_MAGIC)
    {
        rt_kprintf("to free a bad data block:\n");
        rt_kprintf("mem: 0x%08x, used flag: %d, magic code: 0x%04x\n", mem, mem->used, mem->magic);
    }
    RT_ASSERT(mem->used);
    RT_ASSERT(mem->magic == HEAP_MAGIC);
    /* Clear Use Flag */
    mem->used  = 0;
    mem->magic = HEAP_MAGIC;
#ifdef RT_USING_MEMTRACE
    rt_mem_setname(mem, "    ");
#endif

    if (mem < lfree)
    {
        /* Update idle block lfree pointer */
        lfree = mem;
    }

#ifdef RT_MEM_STATS
    used_mem -= (mem->next - ((rt_uint8_t *)mem - heap_ptr));
#endif

    /* If adjacent blocks are also idle, they are merged into a larger block */
    plug_holes(mem);
    rt_sem_release(&heap_sem);
}
RTM_EXPORT(rt_free);

Merge idle block plug_holes

static void plug_holes(struct heap_mem *mem)
{
    struct heap_mem *nmem;
    struct heap_mem *pmem;

    RT_ASSERT((rt_uint8_t *)mem >= heap_ptr);
    RT_ASSERT((rt_uint8_t *)mem < (rt_uint8_t *)heap_end);
    RT_ASSERT(mem->used == 0);

    /* Forward Collation */
    nmem = (struct heap_mem *)&heap_ptr[mem->next];
    if (mem != nmem &&
        nmem->used == 0 &&
        (rt_uint8_t *)nmem != (rt_uint8_t *)heap_end)
    {
        /*Merge if mem->next is idle and not a tail node*/
        if (lfree == nmem)
        {
            lfree = mem;
        }
        mem->next = nmem->next;
        ((struct heap_mem *)&heap_ptr[nmem->next])->prev = (rt_uint8_t *)mem - heap_ptr;
    }

    /* Backward Collation */
    pmem = (struct heap_mem *)&heap_ptr[mem->prev];
    if (pmem != mem && pmem->used == 0)
    {
        /* If mem->prev is idle, merge MEM with mem->prev */
        if (lfree == mem)
        {
            lfree = pmem;
        }
        pmem->next = mem->next;
        ((struct heap_mem *)&heap_ptr[mem->next])->prev = (rt_uint8_t *)pmem - heap_ptr;
    }
}

Releasing dynamic memory is relatively simple. The main idea is to determine whether the incoming address is in the heap area, and if it is heap memory, to determine whether the block information is legal.If legal, the flag will be used to clear.Also, if the adjacent block is idle, use plug_holes merges the free blocks into one large free block.

Memory leak

Failure to free memory can lead to the accumulation of non-reusable memory that programs will no longer use.This wastes memory resources and may cause allocation failures when these resources are exhausted.

How to use heap

Heap Area Configuration

For STM32, at board.h

/ * Configure heap size, which can be modified based on actual use */
#define HEAP_BEGIN   STM32_SRAM1_START
#define HEAP_END     STM32_SRAM1_END

/* For board level initialization heap area */
void rt_system_heap_init(void *begin_addr, void *end_addr)

Interface functions for heaps

For dynamic memory request
void *rt_malloc(rt_size_t size)
/*Append the request memory, this function will change the previously allocated memory block.*/
void *rt_realloc(void *rmem, rt_size_t newsize)
/* Requested memory is initialized to 0 */
void *rt_calloc(rt_size_t count, rt_size_t size)

Memory allocation is not guaranteed to succeed, but may return a null pointer.Undefined behavior is invoked using the returned value without checking whether the assignment was successful.This usually results in a crash, but there is no guarantee that a crash will occur, so relying on it can also cause problems.

For the requested memory, the return value must be judged before use, otherwise the application will fail and continue to be used.There will be unexpected errors!!

To summarize

By combing through the implementation of RT-Thread's heap manager, you can gain a deeper understanding of the following points layer by layer:

  • Why heaps are needed and why heaps are one of the foundations of the C/C++ runtime.Heaps allow for a variety of dynamic memory management, providing memory utilization at the expense of certain overhead (request/release overhead, and memory overhead) and resolving memory shortages to some extent.
  • You can gain a deeper understanding of the practical value of the chain table and some techniques for static implementation.
  • A better understanding of heap implementation allows you to use heaps.
  • Understanding exactly where heap managers are implemented, C/C++ standard libraries, and the operating system kernel may all implement them.
  • The small heap implementation of RT-Thread is a simpler and better example of learning heap management. In fact, there are more complex scenarios for heap implementation, such as the implementation based on SBA heap manager, and the heap implementation of Libraries in IAR also needs to use the tree data structure.

Common heap usage errors

  • Failure to check allocation before use: Memory allocation is not guaranteed to succeed and a null pointer is returned if unsuccessful.Use the returned null pointer and operate directly on it.This may cause the program to crash.
  • Memory leak: Free may also fail to free memory, which can lead to the accumulation of non-reusable memory that will no longer be available in the heap.This wastes memory resources and may exhaust all heap memory as the program runs.
  • Logical error: All allocations must be in the same mode: use malloc to request allocation of memory, and use free to free it.If used without release.For example, using memory after calling free release or before calling malloc, or calling free twice to release memory ("double free"), can often lead to segment errors and program crashes.These errors can be sporadic and difficult to detect by debugging.

Article from WeChat Public Number: Embedded inn, more content, please pay attention to my public number, commercial use is strictly prohibited, illegal must be prosecuted

Tags: C Linux Linker Windows

Posted on Sun, 24 May 2020 14:03:06 -0400 by pug