对于实时操作系统,好的内存分配算法会使系统的稳定性增色不少。实际使用中,如果用户在代码的使用方法和系统内存管理算法相悖,会引起许多问题,甚至使系统变得不可靠。RTEMS提供了堆(Heap)、工作空间(Workspace)、内存区域(Region)和固定尺寸的内存分配算法(Partition)。RTEMS内核使用的内存从工作空间申请,而用户应用程序使用的内存从堆申请。堆与工作空间使用相同的算法管理内存,故放在一起讨论。内存区域与堆的管理算法类似,略有区别,会和固定尺寸的内存分配算法一起在后续的章节中讨论。

 

RTEMS堆使用的内存的分配策略是最先适合法。最开始,一块整块的内存被初始化成一个堆块,经过若干次分配释放后,形成大小不一的堆块(Heap Block)。分配算法从第一个堆块开始寻找,只要堆块的空间比申请的空间大,则将该块分配给用户,停止继续搜索。如果该堆块比申请的空间大到一定的程度,分配算法会将该堆块划分成两个堆块,一个堆块是刚好满足用户申请的空间返回给用户,剩下的空间成为另外一个堆块,放入堆中等待后续的操作。

 

有分配必然有回收,RTEMS堆回收算法的思路是尽可能合并邻接的空闲堆块,形成大的堆块,供分配算法使用。回收发生在每一次释放内存时,回收算法会检查释放的内存邻接的堆块,如果左右邻接的堆块都空闲,则将左右堆块从堆中取出,一起合并成一个大的堆块放入堆中;若只有一个堆块空闲,那就合并一个;若左右邻接的堆块均不空闲,算法放弃合并,只将释放的内存变为堆块放入堆中。

 

RTEMS 的堆(上)_算法

 

这种内存分配回收的算法比较简单,速度快;可以精确分配用户指定的内存,并可对邻接的空间进行合并;然而这个算法也存在着显著的缺点,经过多次分配和释放后,堆中会产生大量的小堆块,产生资源的浪费,如上图所示,虽然空间中存在着相当余量的内存,却不能作为一个整体分配出来;随着堆块的数量增多,资源的链表也变长,搜索合适的内存块的时间也变得不确定,这会对实时性造成危害。

 

通常的,如果系统对分配时间没有特别的要求,分配的尺寸上没有剧烈的波动,释放频度不是很高,使用默认的分配算法是最简洁的。另外一些特殊情况,如分配了内存就不释放,直至系统运行结束,或分配的内存全部释放完再开始下一轮分配,也是适用这种算法的。但是如果系统对分配的实时性要求高,比较频繁的分配释放,系统连续运行时间长,分配的尺寸变化大,那么最佳的选择莫过于固定尺寸的内存分配。所
以选定内存分配方法前,要弄清楚“内存指纹”,权衡利弊,按照应用量体裁衣。

 

堆算法的实现是依靠双向链表完成的,为减少不必要的管理空间,并未直接使用上节讨论到的链表(Chain_Node)。堆和堆块的定义代码如下:

 

    /*堆的定义*/
    typedef struct {
        Heap_Block  free_list;     /*自由堆块链表的头、尾指针*/
        uint32_t    page_size;     /*最小分配尺寸和对齐要求*/
        uint32_t    min_block_size;/*最小堆块的尺寸*/
        void       *begin;         /*堆的开始地址*/
        void       *end;           /*堆的结束地址*/
        Heap_Block *start;         /*第一个合法的堆块地址*/
        Heap_Block *final;         /*最后一个合法的堆块地址*/
        Heap_Statistics stats;     /*堆运行时刻的参数统计*/
    } Heap_Control;
    /*堆块的定义*/
    typedef struct Heap_Block_struct Heap_Block;
    struct Heap_Block_struct {
        uint32_t  prev_size;       /*如果前驱堆块是自由块,表示其尺寸*/
        uint32_t  size;            /*本堆块的尺寸和前驱堆块的状态*/
        Heap_Block *next;          /*指向前驱自由块*/
        Heap_Block *prev;          /*指向后继自由块*/
    };

 

 

注意堆块中的定义:size既表示本堆块的尺寸,又表示前驱堆块的状态。RTEMS规定堆块只有使用和自由两种状态。由于堆块的尺寸都做了对齐处理,size变量的最低比特位是不会被用到的。那它的最低比特位用于存储前驱堆块的状态,当最低比特位(HEAP_PREV_USED)置1时,表示前驱堆块正在使用中;清除时,表示前驱堆块处于自由状态,等待分配。next和prev指针域只有在在本堆块是自由状态时,才是有效的;被申请后作为用户数据存储区。prev_size只有当size最低比特位(HEAP_PREV_USED)清除时,才是有效的;如果size最低比特位置1了,prev_size所在的空间会作为上一个堆块的用户可访问空间,堆的管理代码应避免访问该区域,防止造成错误。这个技巧会使管理堆块所需的额外内存减少到4个字节(只用于保存size域)。

RTEMS 的堆(上)_算法_02

 

 

如图/ref{heapinit}所示,一个totalsize大小的内存块被初始化为一个堆后的存储结构。这个内存块被划分为两个堆块,首堆块(first block)和尾堆块(last dummy block)。尾堆块的尺寸(size1)只有8个字节,只记录它前驱堆块的状态与尺寸,没有双向链表指针域的存储空间,所以堆管理代码不能对尾堆块进行指针链的操作。首堆块的前驱是不存在的,RTEMS就标记首堆块的前驱堆块为使用状态(size的HEAP_PREV_USED比特位置1),故prev_size的值无意义。堆结构free_list中的双向链表指针(next和prev)都指向了首堆块的地址。首堆块的prev指向了free_list,next指针应该指向首堆块的后继,初始化后有两个堆块,由于尾堆块是一个“伪”堆块,所以管理代码直接将next指向free_list,整个堆块形成环形双向链表。首堆块从next的地址到尾堆块的prev_size,是可申请被使用的空间。故堆管理程序以next的地址按对齐尺寸(page_size)对齐,而不是以prev_size的地址对齐。

因为对齐,内存初始化成堆后,开始和结束的位置有可能存在小于对齐尺寸(page_size)的间隙内存,它在整个堆的工作中都不会被使用。如果对齐尺寸与开始地址和内存尺寸配合恰当,这两个间隙将不会存在。

申请内存分配成功后,堆的存储结构会发生的变化。假设从堆中申请alloc_size个字节的内存(alloc_size能被page_size整除)。如图/ref{heapalloc}所示,满足空间要求的堆块已经被分割成两个堆块,一个大小是alloc_size个字节,另一个尺寸为size0- alloc_size个字节。第二个堆块的尺寸大于堆控制块中的min_block_size,否则堆管理程序不会分割堆块,直接将堆块地址返回。用户可访问的内存从used block + 8 一直到free block + 4的位置。处在使用状态的used block只有一个size域是用于堆管理的,free block中的prev_size域已经贡献给used block作为用户可访问的存储空间了。由于used block原来是首堆块,它前面没有堆块了,所以它的prev_size只是单纯的占个位置,不做任何用途。

 

RTEMS 的堆(上)_struct_03

 

堆结构体最后一个变量的定义是统计堆运行时刻的一些参数。用户可以根据这些参数优化自己的设计。Heap_Statistics的定义如下:

 

    typedef struct {
      uint32_t instance;        /*当前堆的实例数*/
      uint32_t size;            /*用于堆的内存尺寸*/
      uint32_t free_size;       /*当前的自由内存尺寸*/
      uint32_t min_free_size;   /*最小自由内存尺寸*/
      uint32_t free_blocks;     /*当前自由块的数量*/
      uint32_t max_free_blocks; /*最大自由块的数量*/
      uint32_t used_blocks;     /*当前使用块的数量*/
      uint32_t max_search;      /*每次申请内存需要最大搜索的堆块数*/
      uint32_t allocs;          /*成功调用 allocs 的总次数*/
      uint32_t searches;        /*申请内存时搜索堆块的总数*/
      uint32_t frees;           /*成功调用 free 的总次数*/
      uint32_t resizes;         /*成功调用 resizes 的总次数*/
    } Heap_Statistics;