VxWorks系统中内存池的应用
2018-10-13安景新赵昶宇
安景新,赵昶宇
VxWorks系统中内存池的应用
安景新1,赵昶宇2
(1.海军驻天津八三五七所军事代表室,天津 300308;2.天津津航计算技术研究所,天津 300308)
针对嵌入式软件内存管理具有快速性、可靠性和高效性的特点,分析了VxWorks内存管理机制,阐述了内存池结构和工作原理,深入剖析了内存池的分配和释放机制,并对VxWorks系统下的内存管理提出了建议。
嵌入式系统;VxWorks;内存管理;内存池
利用默认的内存管理函数new/delete或malloc/free在堆上分配和释放内存会有一些额外的开销。如果应用程序频繁地在堆上分配和释放内存,则会导致性能的降低,并且会使系统中出现大量的内存碎片,降低内存的利用率。
经典的内存池(MemPool)技术是一种用于分配大量大小相同的小对象的技术。该技术可以极大地加快内存分配/释放过程。如果合理地规划内存池的大小以及数量,可以减少动态分配、释放内存时的消耗,并且可以有效减少内存碎片,避免内存泄漏。
本文以VxWorks操作系统为例,重点分析了内存池管理机制,并对内存分配和释放给出了解决方案。
1 VxWorks内存管理机制
VxWorks采用用户程序、内核处于同一个地址空间的内存管理策略,软件开发人员在开发程序时必须保证不侵犯其他程序和内核的地址空间,以免破坏系统的正常工作,或导致其他程序异常运行。内核负责为程序分配内存、动态分配内存和回收内存。VxWorks为用户提供2种内存区域,即内存域region和内存分区partition.region是可变长的内存区,可以从创建的region中再分配段segment,region的特点是容易产生碎片,但灵活、不浪费;partition是定长的内存区,用户可以从创建的partition中分配内存块,或在某个内存分区中再创建一个内存分区。
partition的特点是无碎片、效率高,但存在浪费。通常情况下,VxWorks内核和应用程序对内存的操作是基于内存分区进行的。内存池是一块连续的内存区域,包含1个或多个内存块。内存分区包含分区自身的描述信息(1个结构体)和1个或多个内存池,描述信息保存在系统内存分区中,内存池是该分区实际拥有的内存空间。内存分区刚创建完毕时,只有1个内存池,以后用户程序可往该分区中添加内存池。内存池之间的地址不一定连续,VxWorks在启动过程中会创建一个包含系统内存池的系统内存分区,如图1所示。
图1 VxWorks的内存布局
2 基于内存池的内存分配方案
应用程序可以通过系统的内存分配调用预先一次性申请适当大小的内存作为1个内存池,之后应用程序对内存的分配和释放则可以通过内存池来完成。内存池技术申请内存/释放内存均极其快,其内存分配过程多数情况下复杂度为O(1),内存释放过程复杂度为O(1)。主要开销在生成新的空闲内存单元。
2.1 内存池结构
内存池的结构如图2所示。
定义内存池控制块T_MemoryPool数据结构如下:
Typedef struct MemoryPool{
MemoryBlock* pBlock; /* 第一个block 的指针 */
Unsigned short nUnitSize; /* 每个小内存块的字节数 */
Unsigned short nInitSize; /* 初始的Block 的内存块数目 */
Unsigned short nGrowSize; /* 增加的Block 的内存块数目 */
}T_MemoryPool;
定义内存块T_MemoryBlock数据结构如下:
Typedef struct MemoryBlock{
Unsigned short nSize; /* 内存块的大小 */
Unsigned short nFree; /* 空闲块数 */
Unsigned short nFirst; /* 第一个空闲块 */
MemoryBlock* pNext; /* 下一个Block */
char aData[1]; /* 数据的初始位置 */
}T_MemoryBlock;
图2 内存池结构
2.2 内存池工作原理
在运行过程中,MemoryPool内存池可能会有多个用来满足内存申请请求的内存块,这些内存块是从进程堆中开辟的一个较大的连续内存区域,它由一个MemoryBlock结构体和多个可供分配的空闲内存单元组成,所有内存块组成了一个内存块链表,MemoryPool的pBlock是这个链表的头。对每个内存块,都可以通过其头部的MemoryBlock结构体的pNext成员访问紧跟在其后面的内存块。
每个内存块由2部分组成,即1个MemoryBlock结构体和多个内存分配单元。这些内存分配单元大小固定(由MemoryPool的nUnitSize表示),MemoryBlock结构体有2个成员比较重要,即nFree和nFirst.
nFree记录这个内存块中还有多少个空闲单元,而nFirst则记录下一个空闲单元的编号。每一个空闲单元的前两个字节记录了紧跟它之后的下一个空闲单元的编号。在此情况下,通过利用每个空闲单元的前两个字节,一个MemoryBlock中的所有空闲单元被连接起来。
当有新的内存请求到来时,MemoryPool会通过pBlock遍历MemoryBlock链表,直到找到某个MemoryBlock所在的内存块,及其中的空闲单元(通过检测MemoryBlock结构体的nFree 成员是否大于0)。如果找到这样的内存块,取得其MemoryBlock的nFirst值(此为该内存块中第一个空闲单元的编号),则根据这个编号定位到该空闲单元的起始位置(所有单元大小固定,因此,每个单元的起始位置都可以通过编号分配单元大小来偏移定位),这个位置就是用来满足此次内存申请请求的内存起始地址。但在返回这个地址前,需要先将该位置开始的前两个字节的值(这两个字节值记录其之后的下一个空闲单元的编号)赋给本内存块的MemoryBlock的nFirst成员。在此情况下,下一次的请求就可被这个编号对应的内存单元来满足,同时,将此内存块的MemoryBlock的nFree递减1,然后将定位到的内存单元的起始位置作为此次内存请求的返回地址返回给调用者。
如果从现有的内存块中找不到一个自由的内存分配单元(当第一次请求内存以及现有的所有内存块中的内存单元都已经被分配时,会发生这种情况),MemoryPool就会从进程堆中申请1个内存块。
当某个被分配的单元因free操作需要回收时,该单元并不会返回给进程堆,而是返回给MemoryPool。返回时,MemoryPool能获取该单元的起始地址。此时,MemoryPool开始遍历其所维护的内存块链表,判断该单元的起始地址是否落在某个内存块的地址范围内。如果不在所有内存地址的范围内,则此被回收的单元不属于这个MemoryPool,将整个内存块返回给内存堆;如果在某个内存块的地址范围内,则它会将刚刚回收的分配单元加到这个内存块的MemoryBlock所维护的空闲单元链表头部。
2.3 内存分配机制
进行内存分配前,先判断内存池当前内存块链表是否为空。如果为空,则意味着这是第一次内存申请请求。此时,从进程堆中申请一个分配单元个数为nInitSize的内存块,并初始化该内存块。初始化的操作包括设置MemoryBlock的nSize为所有内存分配单元的大小、nFree为-1、nFirst为1,并将编号为0的分配单元之后的所有空闲单元连接起来,即从aData位置开始,每隔nUnitSize大小取其最前面的两个字节,并记录之后空闲单元的编号。
如果该内存块申请成功,并初始化完毕,返回第一个分配单元给调用函数。第一个分配单元以MemoryBlock结构体内的最后一个字节为起始地址。
当内存池中已有内存块时,遍历该内存块链表,寻找有空闲单元的内存块。如果找到有空闲单元的内存块,则“定位”该内存块为可用的空闲单元处,“定位”以MemoryBlock结构体内的最后一个字节位置aData为起始位置,以MemoryPool的nUnitSize为步长来进行;修改MemoryBlock nFree信息,以及修改此内存块的空闲单元链表信息。在找到的内存块中,nFirst为该内存块中自由存储单元链表的表头,其下一个空闲单元的编号存放在nFirst指示的空闲单元的前两个字节。通过定位得到的位置,取其前两个字节的值赋给nFirst,这就是此内存块空闲单元链表的新表头,即下一次分配的空闲单元的编号。
如果没有找到有空闲单元的内存块,则需要重新向进程堆申请一个内存块。此时,由于不是第一次申请内存块,所以,申请的内存块包含的分配单元个数为nGrowSize,而不再是nInitSize。初始化这个新申请的内存块,并将此内存块插入MemoryPool的内存块链表的头部,再将此内存块的第一个分配单元返回给调用函数。将此新内存块插入内存块链表头部的原因是该内存块还有很多可供分配的空闲单元,放在头部可以使下次接收到内存申请时,减少对内存块的遍历时间。
2.4 内存释放机制
遍历内存池的内存块链表,确定该待回收分配单元(pFree)落在哪一个内存块的指针范围内,可通过比较指针值来确定。找到的包含pFree所指向的待回收分配单元的内存块后,修改该内存块的空闲单元链表信息,将这个待回收分配单元的前两个字节的值指向该内存块原先的第一个可分配的空闲单元的编号,并将nFirst值改变为指向这个待回收分配单元的编号,该操作将待回收分配单元放在了空闲单元链表的头部。需要注意的是,该单元的内存地址并没有改变,改变的只是其状态(已分配/空闲),以及当其处于空闲状态时在空闲单元链表中的位置。
回收后,考虑到资源的有效利用及后续操作的性能,内存池的操作会继续判断:如果此内存块所有分配单元都是空闲的,则这个内存块就会从MemoryPool中被移出,并作为一个整体返回给进程堆;如果该内存块中还有非空闲单元,则不将此内存块返回给进程堆。但是因为刚刚有一个分配单元返回给了这个内存块,即这个内存块有空闲单元可供下次分配,则它会被移到MemoryPool维护的内存块的头部。在此情况下,内存请求到来,MemoryPool遍历其内存块链表寻找空闲单元时,第一次寻找就会找到这个内存块。因为这个内存块确实有空闲单元,这样可以减少MemoryPool的遍历次数。
3 问题和建议
在使用内存池机制管理内存时,需要注意以下几点:①判断内存块的所有单元是否都处于空闲状态时,不用遍历其所有单元,只需要判断nFree乘以nUnitSize是否等于nSize、nSize内存块中所有分配单元的大小(不包括头部MemoryBlock结构体的大小),这样可快速检查某个内存块中所有分配单元是否全部处于空闲状态。②不能通过比较nFree与nInitSize或nGrowSize的大小来判断某个内存块中所有分配单元都为空闲状态,这是因为第一次分配的内存块(分配单元个数为nInitSize)可能被移到链表的后面,甚至有可能在移动到链表后面之后,因为某个时间其所有单元都处于空闲状态而被整个返回给进程堆,即在回收分配单元时,无法判定某个内存块中的分配单元个数是nInitSize还是nGrowSize,也就无法通过比较nFree与nInitSize或nGrowSize的大小,来判断内存块的所有分配单元是否都为空闲状态。③在进行内存释放操作后,虽然pFree被“回收”,但是pFree仍然指向当前已“回收”的单元,这个单元在回收过程中,其前两个字节被覆写,但其他部分的内容并没有改变。且从整个进程的内存使用角度看,该“回收”的单元的状态仍然是“有效的”。因为这里的“回收”只是回收给了内存池,而并没有回收给进程堆,因此,程序仍然可以通过pFree访问此单元。但这是一个很危险的操作,因为该单元在回收过程中的前两个字节已被覆写,且该单元可能很快会被内存池重新分配。因此,回收后通过pFree指针对这个单元的访问都是错误的,读操作会读到错误的数据,写操作则可能会破坏程序中的其他数据,需要注意此问题。
4 结束语
内存的申请和释放对一个应用程序的整体性能影响极大,甚至在很多时候会成为某个应用程序的瓶颈。消除内存申请和释放瓶颈的方法往往是针对内存使用的实际情况,提供合适的内存池。通过性能测试表明,内存池具有malloc/free进行内存申请/释放的方式快、不会产生或很少产生堆碎片、可避免内存泄漏等明显特性。以上所描述的基于内存池的内存管理方法具有很大的实用价值,希望能给嵌入式软件工作者启发,从而设计出更好的内存管理方法。
[1]顾胜元,杨丹,黄海伦.嵌入式实时动态内存管理机制研究与应用[J].重庆工学院学报(自然科学),2009(01): 117-121.
[2]许健,于鸿洋.一种Linux多线程应用下内存池的设计与实现[J].电子技术应用,2012,38(11):146-149.
[3]何煦岚,何晓岚.基于多链表结构的嵌入式系统内存管理[J].计算机应用与软件,2008,25(04):58-60.
〔编辑:张思楠〕
2095-6835(2018)19-0145-03
TP316.2
A
10.15913/j.cnki.kjycx.2018.19.145
安景新(1990—),男,工学硕士,助理工程师,从事装备质量监督与检验验收方面的工作与研究。赵昶宇(1982—),男,陕西汉中人,工学硕士,高级工程师,主要从事嵌入式系统软件测试方面的研究。