动态内存管理

动态内存管理是一个真实的堆(Heap)内存管理模块,可以在当前资源满足的情况下,根据用户的需求分配任意大小的内存块。而当用户不需要再使用这些内存块时,又可以释放回堆中供其他应用分配使用。RT-Thread系统为了满足不同的需求,提供了两套不同的动态内存管理算法,分别是小堆内存管理算法和SLAB内存管理算法。

小堆内存管理模块主要针对系统资源比较少,一般用于小于2M内存空间的系统;而SLAB内存管理模块则主要是在系统资源比较丰富时,提供了一种近似多内存池管理算法的快速算法。两种内存管理模块在系统运行时只能选择其中之一或者完全不使用动态堆内存管理器。这两种管理模块提供的API接口完全相同。

  • 警告:因为动态内存管理器要满足多线程情况下的安全分配,会考虑多线程间的互斥问题,所以请不要在中断服务例程中分配或释放动态内存块。因为它可能会引起当前上下文被挂起等待。

    小内存管理模块

小内存管理算法是一个简单的内存分配算法。初始时,它是一块大的内存。当需要分配内存块时,将从这个大的内存块上分割出相匹配的内存块,然后把分割出来的空闲内存块还回给堆管理系统中。每个内存块都包含一个管理用的数据头,通过这个头把使用块与空闲块用双向链表的方式链接起来,如 内存块链表 图所示:

内存块链表

每个内存块(不管是已分配的内存块还是空闲的内存块)都包含一个数据头,其中包括:

magic – 变数(或称为幻数),它会被初始化成0x1ea0(即英文单词heap),用于标记这个内存块是一个内存管理用的内存数据块;

used - 指示出当前内存块是否已经分配。

magic变数不仅仅用于标识这个数据块是一个内存管理用的内存数据块,实质也是一个内存保护字:如果这个区域被改写,那么也就意味着这块内存块被非法改写(正常情况下只有内存管理器才会去碰这块内存)。

内存管理的在表现主要体现在内存的分配与释放上,小型内存管理算法可以用以下例子体现出来。

小内存管理算法链表结构示意图

小内存管理算法链表结构示意图 所示的内存分配情况,空闲链表指针lfree初始指向32字节的内存块。当用户线程要再分配一个64字节的内存块时,但此lfree指针指向的内存块只有32字节并不能满足要求,内存管理器会继续寻找下一内存块,当找到再下一块内存块,128字节时,它满足分配的要求。因为这个内存块比较大,分配器将把此内存块进行拆分,余下的内存块(52字节)继续留在lfree链表中,如下 分配64 字节后的链表结构 所示。

分配64 字节后的链表结构

另外,在每次分配内存块前,都会留出12字节数据头用于magic,used信息及链表节点使用。返回给应用的地址实际上是这块内存块12字节以后的地址,前面的12字节数据头是用户永远不应该碰的部分。(注:12字节数据头长度会与系统对齐差异而有所不同)

释放时则是相反的过程,但分配器会查看前后相邻的内存块是否空闲,如果空闲则合并成一个大的空闲内存块。

SLAB内存管理模块

RT-Thread的SLAB分配器是在DragonFly BSD创始人Matthew Dillon实现的SLAB分配器基础上,针对嵌入式系统优化的内存分配算法。最原始的SLAB算法是Jeff Bonwick为Solaris 操作系统而引入的一种高效内核内存分配算法。

RT-Thread的SLAB分配器实现主要是去掉了其中的对象构造及析构过程,只保留了纯粹的缓冲型的内存池算法。SLAB分配器会根据对象的类型(主要是大小)分成多个区(zone),也可以看成每类对象有一个内存池,如 SLAB 内存分配器结构 所示:

SLAB 内存分配器结构

一个zone的大小在32k ~ 128k字节之间,分配器会在堆初始化时根据堆的大小自动调整。系统中最多包括72种对象的zone,最大能够分配16k的内存空间,如果超出了16k那么直接从页分配器中分配。每个zone上分配的内存块大小是固定的,能够分配相同大小内存块的zone会链接在一个链表中,而72种对象的zone链表则放在一个数组(zone arry)中统一管理。

下面是动态内存分配器主要的两种操作:

  • 内存分配: 假设分配一个32字节的内存,SLAB内存分配器会先按照32字节的值,从zone array链表表头数组中找到相应的zone链表。如果这个链表是空的,则向页分配器分配一个新的zone,然后从zone中返回第一个空闲内存块。如果链表非空,则这个zone链表中的第一个zone节点必然有空闲块存在(否则它就不应该放在这个链表中),那么就取相应的空闲块。如果分配完成后,zone中所有空闲内存块都使用完毕,那么分配器需要把这个zone节点从链表中删除。
  • 内存释放:分配器需要找到内存块所在的zone节点,然后把内存块链接到zone的空闲内存块链表中。如果此时zone的空闲链表指示出zone的所有内存块都已经释放,即zone是完全空闲的,那么当zone链表中全空闲zone达到一定数目后,系统就会把这个全空闲的zone释放到页面分配器中去。

    动态内存接口

初始化系统堆空间

在使用堆内存时,必须要在系统初始化的时候进行堆内存的初始化,可以通过下面的函数接口完成:

  1. void rt_system_heap_init(void* begin_addr, void* end_addr);

这个函数会把参数begin_addr,end_addr区域的内存空间作为内存堆来使用。

函数参数


  1. 参数 描述

  1. begin_addr 堆内存区域起始地址;
  2.  
  3. end_addr 堆内存区域结束地址。

函数返回

分配内存块

从内存堆上分配用户指定大小的内存块,函数接口如下:

  1. void* rt_malloc(rt_size_t nbytes);

rt_malloc函数会从系统堆空间中找到合适大小的内存块,然后把内存块可用地址返回给用户。

函数参数


  1. 参数 描述

  1. nbytes 申请的内存大小。

函数返回

成功时返回分配的内存块地址,失败时返回RT_NULL。

重分配内存块

在已分配内存块的基础上重新分配内存块的大小(增加或缩小),可以通过下面的函数接口完成:

  1. void *rt_realloc(void *rmem, rt_size_t newsize);

在进行重新分配内存块时,原来的内存块数据保持不变(缩小的情况下,后面的数据被自动截断)。

函数参数


  1. 参数 描述

  1. rmem 指向已分配的内存块;
  2.  
  3. newsize 重新分配的内存大小。

函数返回

返回重新分配的内存块地址;

分配多内存块

从内存堆中分配连续内存地址的多个内存块,可以通过下面的函数接口完成:

  1. void *rt_calloc(rt_size_t count, rt_size_t size);

函数参数


  1. 参数 描述

  1. count 内存块数量;
  2.  
  3. size 内存块容量。

函数返回

返回的指针指向第一个内存块的地址,并且所有分配的内存块都被初始化成零。

释放内存块

用户线程使用完从内存分配器中申请的内存后,必须及时释放,否则会造成内存泄漏,释放内存块的函数接口如下:

  1. void rt_free (void *ptr);

rt_free函数会把待释放的内存还回给堆管理器中。在调用这个函数时用户需传递待释放的内存块指针,如果是空指针直接返回。

函数参数


  1. 参数 描述

  1. ptr 待释放的内存块指针。

函数返回

设置分配钩子函数

在分配内存块过程中,用户可设置一个钩子函数,调用的函数接口如下:

  1. void rt_malloc_sethook(void (*hook)(void *ptr, rt_size_t size));

设置的钩子函数会在内存分配完成后进行回调。回调时,会把分配到的内存块地址和大小做为入口参数传递进去。

函数参数


  1. 参数 描述

  1. hook 钩子函数指针。

函数返回

其中hook函数接口如下:

  1. void hook(void *ptr, rt_size_t size);

函数参数


  1. 参数 描述

  1. ptr 分配到的内存块指针;
  2.  
  3. size 分配到的内存块的大小。

函数返回

设置内存释放钩子函数

在释放内存时,用户可设置一个钩子函数,调用的函数接口如下:

  1. void rt_free_sethook(void (*hook)(void *ptr));

设置的钩子函数会在调用内存释放完成前进行回调。回调时,释放的内存块地址会做为入口参数传递进去(此时内存块并没有被释放)。

函数参数


  1. 参数 描述

  1. hook 钩子函数指针。

函数返回

其中hook函数接口如下:

  1. void hook(void *ptr);

函数参数


  1. 参数 描述

  1. ptr 待释放的内存块指针。

函数返回

动态内存堆使用 的例程如下所示:

  1. /* 线程TCB和栈*/
  2. struct rt_thread_t thread1;
  3. char thread1_stack[512];
  4.  
  5. /* 线程入口*/
  6. void thread1_entry(void* parameter)
  7. {
  8. int i;
  9. char *ptr[20]; /* 用于放置20个分配内存块的指针*/
  10.  
  11. /* 对指针清零*/
  12. for (i = 0; i < 20; i ++) ptr[i] = RT_NULL;
  13.  
  14. while(1)
  15. {
  16. for (i = 0; i < 20; i++)
  17. {
  18. /* 每次分配(1 << i)大小字节数的内存空间*/
  19. ptr[i] = rt_malloc(1 << i);
  20.  
  21. /* 如果分配成功*/
  22. if (ptr[i] != RT_NULL)
  23. {
  24. rt_kprintf("get memory: 0x%x\n", ptr[i]);
  25. /* 释放内存块*/
  26. rt_free(ptr[i]);
  27. ptr[i] = RT_NULL;
  28. }
  29. }
  30. }
  31. }
  32.  
  33. int rt_application_init()
  34. {
  35. rt_err_t result;
  36.  
  37. /* 初始化线程对象*/
  38. result = rt_thread_init(&thread1,
  39. "thread1",
  40. thread1_entry, RT_NULL,
  41. &thread1_stack[0], sizeof(thread1_stack),
  42. 200, 100);
  43.  
  44. if (result == RT_EOK)
  45. rt_thread_startup(&thread1);
  46.  
  47. return 0;
  48. }