RT thread linked list heap manager of practical algorithm series

Time:2020-10-31

[guide] the previous article described the basic concept of stack. This article is to talk about how heap works. RT thread is very popular in the community. After reading its kernel code and realizing heap management, the code design is very clear and readable. Therefore, on the one hand, we can understand the implementation of RT thread kernel, on the other hand, we can make clear the internal implementation of its heap. I hope to have a deeper understanding of the heap.

Note, the code analysis is based on rt-thread-v4.0.2.

What is a heap?

C language heap is a mechanism of dynamic memory acquisition by malloc(), calloc(), realloc() and other functions. After using, the programmer calls free() and other functions to release. When used, the stdlib. H header file needs to be included.

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

Note: This article only describes the implementation of C’s heap manager.

Take C language as an example, translate the above description into a figure

To dynamically manage a piece of memory, and need to dynamically allocate and release, such a requirement. Obviously, C language needs to describe the dynamic memory area abstractly and realize dynamic management. In fact, the essence of heap manager in C language is to use data structure to describe the heap area abstractly

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

Then, a heap manager is realized by using the corresponding algorithm to dynamically manage such data structure objects.

Often see a variety of algorithmic books, only talk about the principle of the algorithm, and do not talk about application examples, often do not understand. I think we can make some improvements. If you can’t use it, you need to study hard. So it’s not that obscure algorithms are useless, but they don’t really combine with applications. It can be further thought that if the algorithm has no application scenario, it will be gradually forgotten by the world in the process of technological development. Therefore, it is suggested that when learning to read algorithm books, find some examples to have a look, which will deepen the understanding of algorithms.This is an important digression. I would like to share with you.

Therefore, in essence, the heap manager is a dynamic memory manager implemented by data structure + algorithm, which manages the dynamic allocation and release of memory.

Why pile up?

C programming language for memory management has static, automatic or dynamic three ways. Static memory allocation variables are usually allocated in the main memory together with the executable code of the program and are valid throughout the life cycle of the program. The variables that automatically allocate memory are allocated on the stack and are applied or released with the call and return of the function. For statically allocated memory and auto allocated memory lifecycles, the allocated size must be a compile time constant (except for variable length auto array [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 is not sufficient. Imagine that even if you all know how much memory you want, such as there are so many applications under Windows / Linux, each application will be loaded with the required memory sampling static allocation strategy, then the memory of multiple programs will be exhausted quickly.

The lifetime of allocated memory can also be a concern. Neither static nor automatic allocation can satisfy all cases. Automatic allocation of memory cannot be preserved between multiple function calls, while static data is bound to be retained throughout the life cycle of the program, whether it is really needed or not. In many cases, programmers have more flexibility in managing the life cycle of allocated memory.

These limitations / drawbacks are avoided by using dynamic memory allocation, in which memory is managed more explicitly (but more flexibly), usually by allocating memory (an area of memory constructed for this purpose) from free storage (informally called “heap”). In C language, 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 memory so that it can be used for other purposes.

Who implements heap

If you ask this question, you will immediately say C compiler. It’s true that C compiler implements heap manager. In fact, it’s not the compiler that implements dynamic memory manager in the process of compilation, but the C library implemented by C compiler implements heap manager. For example, ANSI C, VC, IAR, C compiler, GNU C and so on all need the support of some C libraries, so there is such a heap manager hidden in these libraries. Seeing is believing. Take IAR arm 8.40.1 as an example, and its heap manager is implemented in:

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

With so many source codes, what options need to be configured for application development?

Four options are supported:

  • Automatic:
    • If your application has a call to the heap memory allocation routine, but no call to the heap release routine, the linker automatically selects no free heap.
    • If you have a call to the heap memory allocation routine in your application, the linker automatically selects the advanced heap.
    • For example, if a heap memory allocation routine is called in the library, the link program automatically selects the basic heap.
  • Advanced heap: Advanced heap_ Heap) provides efficient memory management for applications that use the heap extensively. In particular, applications that repeatedly allocate and free memory may gain 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 the heap frequently. In particular, it can be used in applications that allocate only heap memory and never free heap memory. The basic heap is not particularly fast, and using it in applications that repeatedly free memory can lead to unnecessary heap fragmentation. The code for the base heap is much smaller than the size of the advanced heap.
  • No free heap: no heap available (- – no)_ free_ Heap) with this option, you can use the smallest heap implementation. Because this heap does not support deallocation or reallocation, it is only applicable to applications that allocate heap memory for various buffers during the startup phase, and applications that never free memory.

However, if we think that only the standard C library is responsible for implementing the heap manager, this understanding is not comprehensive. Back to the essence of things, heap manager is to use data structure and algorithm to dynamically manage the allocation and release of a piece of memory. Where there is such a requirement, 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 main types of memory managers to implement

  • Applications, which require a heap memory manager, are obvious. For example, common applications under Windows / Linux need heap memory manager. The above mentioned cortex m or other MCU programs need heap memory manager when they are programmed with C / C + +.
  • The operating system kernel, which needs to allocate memory like an application. However, the implementation of malloc in the kernel is usually quite different from that of C library. For example, the memory buffer may need to meet the special restrictions imposed by DMA, or the memory allocation function may be invoked from the 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 the kernel version of the heap manager to provide kmalloc / vmalloc memory application and kfree / vfree to release memory.

How to realize heap

For the RT thread kernel, it also implements a kernel heap manager. Here we will comb the implementation of the small heap manager in the RT thread kernel version, and at the same time to understand the linked list data structure and the example application of algorithm operation.

Its heap manager implementation is located in MEM. C, memheap. C and MemPool.

Key data structure

The main data structure of heap manager is heap_ mem。

  • heap_mem

Heap manager initialization

The initialization entry of the heap manager is in MEM. C, and the function is as follows:

void rt_system_heap_init(void *begin_addr, void *end_addr)
{
    struct heap_mem *mem;
    /*Translate address by 4-byte alignment*/
    /*For example, from 0x2000 0001 to 0x2000 0003, it turns to 0x2000 0004*/
    rt_ubase_t begin_align = RT_ALIGN((rt_ubase_t)begin_addr, RT_ALIGN_SIZE);
    /*For example, if it is from 0x30000001 to 0x30000003, it will be changed to 0x30000000*/
    rt_ubase_t end_align   = RT_ALIGN_DOWN((rt_ubase_t)end_addr, RT_ALIGN_SIZE);
    
    /*Debug information, function cannot be used inside interrupt*/
    RT_DEBUG_NOT_IN_INTERRUPT;

    /*The address range can store at least two heaps_ mem */
    if ((end_align > (2 * SIZEOF_STRUCT_MEM)) &&
        ((end_align - 2 * SIZEOF_STRUCT_MEM) >= begin_align))
    {
        /*Compute available heap, 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 points to the 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));

    /*Initialize 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

    /*Initialize 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);

    /*The initialization release pointer points to the beginning of the heap*/
    lfree = (struct heap_mem *)heap_ptr;
}

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

The above RT_ system_ 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 as follows:

Tips:

  • Converting memory data to struct heap by type casting_ mem *。 The static double linked list is created
mem      = (struct heap_mem *)heap_ptr;
heap_end = (struct heap_mem *)&heap_ptr[mem->next];
  • Define heap_ MEM does not define how many bytes are used as the number of user data bytes of the block, which saves memory. It is a better way to deal with it.
  • Alignment configurable, RT_ ALIGN_ Size is 4 bytes by default.

Request memory to heap

User calls RT_ Malloc is used to request the allocation of dynamic memory.

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;
    /*Apply according to 4-byte alignment. If 5-byte is applied, 8-byte is actually applied*/
    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));

    /*Apply according to 4-byte alignment. If 5-byte is applied, 8-byte is actually applied*/
    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 piece must be at least min in length_ SIZE_ 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 not used and meets the size requirements*/
        if ((!mem->used) && (mem->next - (ptr + SIZEOF_STRUCT_MEM)) >= size)
        {
            /*MEM has not been used, at least a perfect fit is possible:
             * mem->next - (ptr + SIZEOF_ STRUCT_ MEM) to calculate the "user data size" of mem*/
            if (mem->next - (ptr + SIZEOF_STRUCT_MEM) >=
                (size + SIZEOF_STRUCT_MEM + MIN_SIZE_ALIGNED))
            {
                /*(in addition to the above, we test another structure, heap_ mem (SIZEOF_ STRUCT_ MEM)
                 *Does it contain at least min_ SIZE_ Aligned data is also suitable for 'MEM' user data space.)
                 *- > split large blocks to 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 be suitable. It is 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 other than 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 list. If a block is found to be free and meets the application size and the remaining space can at least store the descriptor, then the following memory header will be generated to generate a description, update the pointer before and after, mark the magic number and the block has been used, and insert the block into the linked list. Return the memory address of successful application. If it cannot be retrieved, a null pointer is returned, indicating that the request failed and that the heap currently does not have enough memory available for use. In fact, the above code dynamically maintains the heap memory area at runtime according to the following diagram.

In summary:

  • heap_ PTR always points to the heap start address, heap_ End always points to the last block. The combination of the two can achieve boundary protection and is used when releasing memory.
  • Lfree always points to the smallest free block, so when dynamically applying for memory, it always searches from the block whether there is a memory block that meets the requirements of the application.
  • Used = 1 indicates that the block is occupied and not idle. Used = 0 indicates that the block is idle.
  • Magic field magic, starting with a special marker word, is used to detect exceptions with used = 0. Imagine if only used = 0 is used to judge whether the block is idle, it is easy to make mistakes, or other auxiliary codes need to be added to ensure the robustness of the code.
  • Dynamic memory management applications are slow, need to retrieve linked list, and additional memory overhead.
  • Rt_ Realloc and RT_ Calloc doesn’t do the analysis

Free memory

Free memory by 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));
    /*The release application address is not in the heap*/
    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);

    /*The block descriptor of the memory to be released must be in use state*/
    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 mark*/
    mem->used  = 0;
    mem->magic = HEAP_MAGIC;
#ifdef RT_USING_MEMTRACE
    rt_mem_setname(mem, "    ");
#endif

    if (mem < lfree)
    {
        /*Update free 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 free 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 finishing*/
    nmem = (struct heap_mem *)&heap_ptr[mem->next];
    if (mem != nmem &&
        nmem->used == 0 &&
        (rt_uint8_t *)nmem != (rt_uint8_t *)heap_end)
    {
        /*If MEM > next is idle and not tail node, then merge*/
        if (lfree == nmem)
        {
            lfree = mem;
        }
        mem->next = nmem->next;
        ((struct heap_mem *)&heap_ptr[nmem->next])->prev = (rt_uint8_t *)mem - heap_ptr;
    }

    /*Backward finishing*/
    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;
    }
}

Dynamic memory release is relatively simple, its main idea is to determine whether the incoming address is in the heap, and if it is heap memory, determine whether its block information is legal. If it is legal, the flag is cleared. At the same time, if the adjacent block is idle, plug is used_ Holes merge the free blocks into a large free block.

Memory leak

Failure to free memory will lead to the accumulation of non reusable memory, which is no longer used by the program. This wastes memory resources and can cause allocation failures when they are exhausted.

How to use heap

Configuration of heap area

For STM32, it is located on board. H

/* configure heap size, which can be modified according to actual use*/
#define HEAP_BEGIN   STM32_SRAM1_START
#define HEAP_END     STM32_SRAM1_END

/*Used for board level initialization heap*/
void rt_system_heap_init(void *begin_addr, void *end_addr)

Interface function of heap

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

Memory allocation does not guarantee success, but may return a null pointer. Using the returned value without checking whether the assignment was successful, an undefined behavior is called. This usually leads to a crash, but there is no guarantee that it will, so relying on it can cause problems as well.

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 mistakes!!

To sum up

By combing the implementation of RT thread’s small heap manager, we can understand the following points step by step

  • Why heap is needed and why heap is one of the foundations of C / C + + runtime. Heap can realize the diversity of dynamic memory management. It can provide the utilization of memory and solve the problem of insufficient memory to a certain extent at the expense of certain overhead (application / release cost and memory cost).
  • Can be more in-depth understanding of the practical value of the linked list, understand some of the static implementation of some skills.
  • Through a deeper understanding of the implementation of the heap, you can better use the heap.
  • Understand where the heap manager is implemented. The C / C + + standard library and the operating system kernel may implement the heap manager.
  • The small heap implementation of RT thread is a relatively simple and good example of learning heap management. In fact, there are more complex scenarios for heap implementation, such as the implementation of slab heap manager and the implementation of Library heap in IAR also need to use the data structure of tree.

Common errors in heap usage

  • Failed to check allocation before use: memory allocation does not guarantee success. A null pointer is returned if unsuccessful. Use the null pointer returned and manipulate the null pointer directly. May cause the program to crash.
  • Memory leak: using free to free memory may also fail. Failure will lead to the accumulation of non reusable memory, which can no longer be used in the heap. This wastes memory resources and may run out of all heap memory as the program runs.
  • Logic error: all allocation must use the same pattern: use malloc to request allocation of memory, use free to free memory. If not released after use. For example, using memory after calling free or before calling malloc, or calling free twice to free memory (“double free”), etc., usually will lead to segment errors and program crash. These errors can be sporadic and difficult to debug to detect.

The article is from WeChat official account: embedded Inn, more contents, please pay attention to my official account, strictly prohibit commercial use, and illegal.

Recommended Today

GPS timing products, NTP time calibration, time synchronization server, Beidou time service equipment

GPS timing products, NTP time calibration, time synchronization server, Beidou time service equipment GPS timing products, NTP time calibration, time synchronization server, Beidou time service equipment Long term production and supply of Beijing Zhun Electronic Technology Co., Ltd. ahjzsz summary NTP network time server is a high-tech clock product with high precision, large capacity and […]