内存受限设备的应用层内存管理(三)
Raveendran Vadakkoot and Neeraj S. Sharma
译 By 郭世龙
6.C++框架
这里讨论的C++内存管理框架是MemManager类。必须保证的是在任何时间点,只有一个MemMangager类的实例(使MemManager类成为一个单实例类)是活动的。使用友元函数的一个比较好的老技巧是是这个类成为单实例,因为不但要保证它是一个单实例而且MemManager类唯一的实例仅能当用户要动态分配一些空间时候才能创建。其他的技术,像用一个静态成员变量来构造一个单实例,都不像使用友元函数一样有效。
注意:这个框架没有讨论是这个类线程安全。
class MemManager { private: MemManager(); // suppress the constructor and copy the // constructor MemManager (MemManager&); void operator= (MemManager&); // and the overloaded '=' operator public: friend MemManager& getInstance(); };
在getInatance()方法实现中,用户必须创建一静态成员并且返回一个如下代码段所示的一个引用。
MemManager::MemManager() { init(); } MemManager& getInstance() { static MemManager theInstance; return theInstance; }
这个单实例模式框架一旦具备,用户必须提供一个基本的分配和回收函数。分配和回收函数完成必要的内存管理工作。init()函数初始化数据结构。
一旦用户保证了在程序执行期间只有一个内存管理实例活动,用户必须确保在代码执行期间不会有意或无意地发生不当使用。设置构造函数、拷贝构造函数、“=”操作符为私有类型(private)来限制他们的行为。这将保证用户不能在任何时候创建这个类的任何其他的实例。
class MemManager { private: MemManager(); // suppress the constructor // and copy the constructor MemManager(MemManager&); void operator=(MemManager&); // and the overloaded '=' operator public: friend MemManager& getInstance(); void* allocate(size_t s); void deAllocate(void* ptr); };
C++提供了一种机制来拦截所有的new和delete调用。通过重载“全局”(global)new,new[],delete,delete[]操作符,用户可以重定向所有的对操作系统(OS)内存管理系统的调用为这个内存管理框架的allocate()和deallocate()函数。
void* operator new(size_t s) { return getInstance().allocate(s); } void operator delete(void* ptr) { return getInstance().deAllocate(ptr); } void* operator new[](size_t s) { return getInstance().allocate(s); } void operator delete(void* ptr) { return getInstance().deAllocate(ptr); }
所有关于页的信息都保持在Cell类中。Cell类的基本结构如下代码所示。
struct Cell { long dataSize; void *data; long noOfElements; long bitmap; Cell *prev; Cell *next; Cell *header; // boundary tag };
data指向实际的内存位置,在这个位置保存了新分配的对象。bitmap指明了单元/页中的特定位置是否被新对象(被创建)占用。dataSize说明页包含的每个对象的大小(它总是2的倍数)。header域被用来在多页块情况下到达第一页。
在实际分配和回收发生之前,init()函数必须做很多工作。其中包括:
1)创建空闲链表(空闲链表是系统才开始时从操作系统分配到的一大块内存,进一步的分配和回收将在这个空闲块上发生)。
2)初始化薄记用到的数据结构。
3)将空闲链表分成等大小的单元/页。

图16:薄记的数据结构
Static char buffer[MAX_BUFFER]; // align it to a word boundary if desired Const int noOfPages = MAX_BUFFER / PAGESIZE; Void MemManager::init() { // 1. Allocate a big chunk. This has to be a preferably // a static block. For example:- buffer; // 2. Divide it into cells/pages of equal size. This can // be done by setting the 'Cell' datastructure point // to the bufferfor(int i=0;i<noOfPages;i++) Cell[i].data = &buffer + i*PAGESIZE; Cell[0].dataSize = noOfPages*PAGESIZE; // This is required because intially, it's a big chunk/block // of data. Later it gets fragmented into smaller chunks. // 3. Also set the category list to null, as it would // initially not contain any page for(int j=0; j<noOfCategories;j++) CellCategory[j].head = CellCategory[j].tail = 0; }
图17:位图指示空闲和已分配的块
考虑这种情况,单元/页最多有四个对象。图17中位图清晰的显示了四个单元中,单元1和3被使用,单元2和4空闲。假设每个单元的大小是256个字节,这种情况下,每一个内分割大小是256/4=64个字节。分配给每个分割的大小应该是33到64个字节。在以比较好的粒度分割情况下,最坏的情况是对象的大小是33个字节(这将浪费31个字节)。
同大小的页的分类和链接通过使用CellCategory类来完成。
class CellCategory { Cell *head; Cell *tail; };
图18: 内存结构
任何动态内存的调用,比如说new char[17],都会被这个内存管理器中的allocate()函数拦截。allocate函数首先识别出这个对象落入那一个类别。如图18所示,这个类别是2,换句话说就是这是一个大小为17到32的对象。如果这个类别的头(head)节点有空闲空间,这个新对象将被赋给这个特定的位置并返回其地址。另外,位图(bitmap)也相应的标识。
图19:内存结构和位图显示了空闲和已分配的块
一旦头(head)页/单元是满的,它将被移动到这个分类链表的末端。这就保证了或者这个链表的开始位置可用或者内存管理器必须从空闲链表中分配一个新页。
一般地,allocate()函数做如下一些事:
Void* MemManager::allocate(size_t s) { // 1. Find the category to which the object falls. // 2. If a free 'partition' is available at the 'head', // grab it immediately. // 3. If not, then grab a new cell/page from the free list. // Put it at the 'head' of the category list. // 4. Allocate space for the object, from the cell // 5. Set the bitmap accordingly. // 6. Return the address. }
多页/多单元块和单页块必须分别处理
Void* MemManager::allocate(size_t s) { // 1. Find the category to which the object falls. // 2. If the object is a multi-page or a single-page object, // then grab fresh cells/pages. // 3. Add them to the category list. // 4. If the object doesn't span the whole page/multi page, // then if a free 'partition' is available at the 'head', // grab it immediately. // 3. If not, grab a new cell/page from the free list. // Put it at the 'head' of the category list. // 4. Allocate space for the object, from the cell // 5. Set the bitmap accordingly. // 6. Return the address. }
任何动态分配的内存的释放,比如说delete[] obj ,都会被这个内存管理器中的deAllocate()函数拦截。deAllocate()函数首先识别对象落入那个类别。这很容易因为单元/页总是固定的。单元的边界地址能通过简单算术运算获得。
CellNum = (objectAddress - baseAddress) / CELLSIZE
活动的单元边界可以通过如下算式获得
boundaryAddress = baseAddress) + CellNum*CELLSIZE;
具备了所有的这些信息,释放一个分配的空间就很简单了,设置空间对应的位图(bitmap)为0。

图20:释放前的内存结构和位图

图21:释放后的内存结构和位图
一旦一个对象从单元中释放,整个单元/页都是空闲的,这是十分可能的情况。一旦一个单元/页完全是空闲的,那么它必须被归还给空闲链表。这也可以通过deAllocate()函数来完成。
Void MemManager::deAllocate(void* ptr) { // 1. Find the category and the cell to which the // object falls. // 2. Set the bitmap to '0' for the partition being freed. // 3. If the whole cell/page is free, delete the page from // the category list and return it to the free list. // 4. If not, put the object at the 'head' of the list as // it guarantees that there is always a free cell at // the head of the list } Void MemManager::deAllocate(void* ptr) { // 1. Find the category and the cell to which the object falls. // 2. Set the bitmap to '0' for the partition being freed. // 3. If the whole cell/page is free, delete the page from // the category list and return it to the free list. // 3-1. During the return to the free list, try to coalesce // it with the adjacent blocks. // 4. If not, put the object at the 'head' of the list as // it guarantees that there is always a free cell at the // head of the list } Void MemManager::coalesce(void* ptr) { // 1. Check if the immediate previous block is free. // Let us call that 'p'. // 2. If its free, it's quite simple. Just add the size of // the current block (lets call this 'curr') to the // previous block's capacity. // ie. p->size += curr->size; curr = p; // 3. Next, check if the adjacent 'next' block is free. // Let us call that 'n' // 4. If the next block is free, it gets quite complicated. // 4-1. Find the 'next' block of 'n'. Make that the 'next' // of the 'curr' block. // ie curr->next = n->next; // curr->size += n->size; }
7.总结
总之,"2的幂伙伴系统(power of 2 buddy-system)"算法,与特定大小对象的分离链表一起都能使应用程序获得非常好的性能和内存的管理。
8.参考文献
- Dynamic Storage Allocation For Realtime Systems by M. Masmano, I. Repoll, and A. Crespo.
- Dynamic Storage Allocation—A Survey and Critical Review by Paul R. Wilson, Mark S. Johnstone, Michael Neely, and David Boles.
- Art of Computer Programming—Fundamentals of Computer Algorithms by Donald E Knuth.
- More Effective C++ by Scott Meyers.