RTEMS 的堆(下)

 

堆的核心操作主要有初始化、申请、释放、扩展。由于工作空间(Workspace),是供内核使用的,所以相关函数使用的全部是“_”前缀。主要操作分列如下:

  •  _Heap_Initialize,将指定的内存初始化成堆;
  •  _Heap_Allocate,从指定的堆中申请指定字节数的内存;
  •  _Heap_Allocate_aligned,从指定的堆中申请指定字节的内存,并且返回的地址还按照指定的对齐要求对齐;
  •  _Heap_Free,将指定地址的内存释放到指定的堆中;
  •  _Heap_Extend,对堆栈空间进行扩展。


堆的整个实现代码非常简洁,但由于对齐的问题,稍微显得有些复杂。可以先不管_Heap_Align_down、_Heap_Align_up、_Heap_Align_up_uptr等函数,并假设所有的地址都是对齐的,可以减少相关代码的阅读难度。

 

内存初始化成堆的函数(_Heap_Initialize)比较简单,参考上一小节理解,这里不做赘述。内存申请主要通过_Heap_Allocate和_Heap_Allocate_aligned两个函数完成。_Heap_Allocate函数的原型如下:

 

    void *_Heap_Allocate(Heap_Control *the_heap, size_t size);

 

该函数首先调用_Heap_Calc_block_size计算实际应该申请的内存尺寸,实际使用的内存尺寸不会比size小,至少还要多申请4个字节,用于存储本堆块的尺寸和上一个堆块的使用状态,尺寸还要按照the_heap->page_size对齐。如果size + 4对齐后仍小于本堆块的可申请的最小尺寸(堆定义中的min_block_size),那么直接按这个最小尺寸申请内存。然后程序从the_heap中的第一个堆块开始寻找可以容纳申请尺寸的堆块。找到这个堆块后,则调用_Heap_Block_allocate执行分配工作。特别注意,分配返回的堆块指针立即被后移了8(HEAP_BLOCK_USER_OFFSET)个字节,然后统计相关的参数并将这个地址返回给用户。计算实际申请空间只是至少多算了4个字节,如果实际申请空间真只是多了4个字节,而申请到的指针被后移了8个字节,那么正如上面小节所说,下方邻接堆块的prev_size域被征用了。

 

_Heap_Allocate_aligned函数原型如下:

    void *_Heap_Allocate_aligned(Heap_Control *the_heap,
                                 size_t size,
                                 uint32_t alignment);

从函数的申明上看,仅比_Heap_Block_allocate多了一个对齐尺寸的参数alignment,正因为这个对齐参数的存在,不能再忽略对齐函数。对齐函数主要分为两类,一个是向上对齐,如果地址不能被对齐因子整除,则增大地址,增加的值小于对齐因子却刚好使得地址能被对齐因子整除;另外一个是向下对齐,与向上对齐不同,它是减小地址,减小的值小于对齐因子却刚好使得地址能被对齐因子整除。

 

如图/ref{heap_aligned},整个计算遵循以下步骤:

  1.  从第一个堆块到最后一个堆块,选择尺寸大于等于size的堆块进行分配计算;
  2.  aligned_user_addr指向block_end - (size + 4)按alignment向下对齐的地址(加4使下方邻接堆块的prev_size可存储数据);
  3.  程序将user_addr指向aligned_user_addr的地址,再使user_addr指向地址按堆块的page_size向下对齐,aligned_user_addr指向地址大于等于user_addr指向地址;
  4.  如果user_addr指向地址小于user_area指向地址,分配失败,转第一步继续寻找合适的堆块进行分配;
  5.  如果user_addr在堆的用户区内,并且user_addr与user_area之间的内存尺寸小于一个最小堆块(堆定义中的min_block_size),则进行下一步;否则,即为分配成功,进入实际分配程序;
  6.  aligned_user_addr和user_area之间的内存尺寸小于page_size,则分配算是成功,进入实际分配程序。如果大于page_size,则转下一步;
  7.  将aligned_user_addr指向user_area指向地址,将其按alignment向上对齐,再次检查其与user_area之间的内存尺寸是否小
  8. 于page_size,如果小于,那么本次分配算是成功,进行实际分配程序;否则即为失败,转第一步继续在堆中寻找下一个合适堆块进行分配。

 

 

 

内存分配调用block_allocate函数完成,该函数的执行过程较为简单。只需注意,如果申请的尺寸小于堆块的尺寸超过min_block_size,需要将堆块分割成两个堆块。由于对齐分配从堆块的尾部开始计算的,分割成的两个堆块,底部的堆块作为申请堆块返回给用户,顶部堆块留在堆中等待后续操作。这个行为与_Heap_Allocate函数正好相反。

从第五个步骤看,程序并未使用对齐操作保证aligned_user_addr与user_area之间的尺寸差小于page_size。注意进入实际分配程序前,程序申请的内存尺寸是block_end - (user_addr - 8),那么可知block_allocate函数分配的堆块地址是从user_addr - 8开始,可使用的用户区从user_addr开始,第三步已保证该地址与aligned_user_addr之间的差小于page_size。

从第六、七步骤看,内存申请成功后,会有一个内存间隙aligned_gap,如图/ref{heap_aligned},这个间隙的值在[0, page_size)区间内。

block_allocate函数返回值只是作为一个检查参数用于校验分配的结果是否满足最终的分配要求。用户最后得到的分配地址是直接指向aligned_user_addr指向的地址。aligned_user_addr指向地址是大于等于user_area指向地址的,而_Heap_Allocate分配结果是等于分配块的user_area指向地址。对于_Heap_Free函数来说,用户释放的地址都是从申请函数中得到的地址,两个申请函数的地址和控制数据的位置不一样,释放函数需要对释放地址做一个按page_size向下对齐的操作,将指针地址转换成分配块的user_area,这样即可得到分配块尺寸的数据地址,即是user_area - 4。释放函数即可按照正确的参数对用户的指针地址进行释放了。这就是对齐分配函数用aligned_user_addr与user_area指向地址差是否小于page_size作为分配成功的条件的原因了。

 

 

_Heap_Free函数的原型如下:

    bool _Heap_Free(
        Heap_Control        *the_heap,
        void                *starting_address);

 

这个函数只是将starting_address开始的内存释放到堆the_heap中。合并成功返回TRUE,失败返回FALSE。函数首先检查释放地址是否为这个堆中的地址,然后对地址按堆的对齐因子向下对齐,并计算得到此堆块的真正起始地址。检查上方邻接堆块是否为空闲,检查下方邻接堆块是否为空闲,将上方下方空闲邻接堆块合并在一起放入堆中。

 

_Heap_Extend函数原型如下:

    Heap_Extend_status _Heap_Extend(
        Heap_Control        *the_heap,
        void                *starting_address,
        size_t               size,
        uint32_t            *amount_extended);

该函数将以starting_address开始的大小为size的内存块变为堆块放入堆the_heap中。用amount_extended返回实际加入堆的内存尺寸。加入成功,函数返回HEAP_EXTEND_SUCCESSFUL。并不是所有的内存块都可以加入到堆块中,即将加入的内存块和堆的位置关系有五种情况,分列如下:

  1.  内存块与堆块地址不连续,内存块末地址小于堆顶地址;
  2.  内存块与堆地址连续,内存块末地址等于堆顶地址;
  3.  内存块的地址部分或全部处于堆内部;
  4.  内存块与堆地址连续,内存块首地址等于堆底地址;
  5.  内存块与堆地址不连续,内存块首地址大于堆底地址。



第一、二、五种情况,系统算法不支持;第三种情况是参数错误。只有第四种情况才可以扩展成功。从以上的分析看,第一、五种情况以堆当前的管理算法是无论如何也支持不了的,但是第二种应该与第四中类似,应该是可以扩展成功的,可能在扩展时,堆顶的堆块已经被用户申请使用,操作起来恐有不便,故而未实现。而堆块的尾部是有一个不被使用的伪堆块,可以很简单的做扩展操作。如果读者有兴趣,可以自行实现对第二种情况的支持。

_Heap_Extend函数首先检查内存块是否为第四种情况,如果不是,失败返回;如果是,则调整堆的结束地址到内存块的结束地址。如果增加的内存块和堆原末尾产生的对齐间隙尺寸之和小于最小堆块尺寸,那么不做任何操作,返回成功;如果大于最小堆块尺寸,则移动伪堆块到内存块尾部,将原伪堆块地址到新伪堆块地址变为一个堆块,调用_Heap_Free释放到堆中并返回成功。

 

 

你可能感兴趣的:(算法,工作,user,存储,扩展,alignment)