2009年2月8日星期日

Nebula3内存模块-内存池(一)

接下来我们来探讨一下关于n3内存池的实现。n3的内存池由两个类来实现MemoryPool和MemoryPoolPage。MemoryPool是线程安全的,而MemoryPoolPage却不是。

从大的结构上来讲,一个内存池(MemoryPool)中包含有多个内存页(MemoryPoolPage),每个内存页里面包含有多个的块(block),每个块包含有块头和内容。

n3的内存池是如何运作的呢?我们一步一步地来解释:

1.创建内存池,如果调用的是MemeoryPool默认的构造函数,那么我们还必须调用MemoryPool::Setup(const char*,SizeT,SizeT)方法来初始化内存池中的第一个内存页。如果我们使用另外一个相同参数的构造函数就不用了。代码如下:

MemoryPool::MemoryPool(const char* _name, SizeT _blockSize, SizeT _blocksPerPage) :
name(_name),
blockSize(_blockSize),
blocksPerPage(_blocksPerPage)
{
n_assert(_name != 0);
n_assert(_blockSize > 0);
n_assert(_blocksPerPage > 0);
this->AllocPage();
}

void
MemoryPool::AllocPage()
{
MemoryPoolPage* newPage = new MemoryPoolPage(this->blockSize, this->blocksPerPage);
this->pages.Append(newPage);
this->roomyPages.Append(newPage);
}

这边有三个参数,第一个说明内存池的名称,第二个是说明内存池中的内存页所包含的块的大小,第三个参数说明的内存池中的内存页所包含有几个块。创建完内存页之后把内存页放入一个包含内存页的pages数组:

this->pages.Append(newPage);

如果这个内存页有可以分配的内存块,那么该内存页同时也被放入另外一个roomyPages数组:

this->roomyPages.Append(newPage);

这样在分配内存块的时候,可以直接从roomyPages中取得内存页来分配内存块。

2.内存池是创建完了,那么里面的内存页是如何创建的呢?完整代码可以看MemoryPoolPage的构造函数。

第一步:计算实际内存块的大小:传入的内存块的大小+内存块头部的大小,并通过运算让实际内存块的大小正好是16的倍数。

SizeT blockSizeWithHeader = this->blockSize + sizeof(BlockHeader);
const int align = 16;
SizeT padding = (align - (blockSizeWithHeader % align)) % align;
this->blockStride = blockSizeWithHeader + padding;
n_assert(0 == (this->blockStride & (align - 1)));

第二步:计算一个内存页所需的总内存:实际内存块的大小*每个内存页有几个内存块。

this->pageSize = this->numBlocks * this->blockStride;

第三步:在PoolHeap类型的堆上分配实际需要的内存:

this->pageStart = (ubyte*) Memory::Alloc(Memory::PoolHeap, this->pageSize);
this->pageEnd = this->pageStart + this->pageSize;

第四步:设置内存块的头部信息:

内存块的头部信息如下定义:

struct BlockHeader
{
// keep size of this structure at 16 bytes!
uint headerStartCanary; // used for detecting overflow errors
BlockIndex prevBlockIndex; // index of prev block (InvalidBlockIndex if first block)
BlockIndex nextBlockIndex; // index of next block (InvalidBlockIndex if last block)
MemoryPoolPage* pageObject; // pointer to memory pool page which owns this block
uint dataStartCanary; // used for detecting overflow errors
};

这个内存块头部信息主要包括内存块前置索引信息prevBlockIndex,内存块的后置索引信息nextBlockIndex,还有指向包含该头部信息的内存页pageObject。并且保持了这个内存块头部信息的大小刚好是16bytes。

通过程序让内存块的前后索引关联起来,并让第一个内存块的前置索引指向ListHeadIndex,最后一个内存块的后置索引指向ListTailIndex。代码如下:

for (blockIndex = 0; blockIndex <>numBlocks; blockIndex++)
{
BlockHeader* curBlockHeader = this->BlockIndexToPointer(blockIndex);
curBlockHeader->headerStartCanary = HeaderStartCanary;
curBlockHeader->dataStartCanary = DataStartCanary;
curBlockHeader->pageObject = 0;
if (0 == blockIndex)
{
curBlockHeader->prevBlockIndex = ListHeadIndex;
}
else
{
curBlockHeader->prevBlockIndex = blockIndex - 1;
}
if (blockIndex == (this->numBlocks - 1))
{
curBlockHeader->nextBlockIndex = ListTailIndex;
}
else
{
curBlockHeader->nextBlockIndex = blockIndex + 1;
}
}

第五步:初始化指向第一个未分配的内存块的索引firstFreeBlockIndex,和指向第一个已分配内存块的索引firstAllocBlockIndex。代码如下:

this->firstFreeBlockIndex = 0;
this->firstAllocBlockIndex = ListTailIndex;

到目前为止整个内存池的初始化工作就算是完成了,接下来我们将分析是如何分配内存和释放内存。

没有评论:

发表评论