接下来我们来探讨一下关于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;
到目前为止整个内存池的初始化工作就算是完成了,接下来我们将分析是如何分配内存和释放内存。
Moving to github
9 年前
没有评论:
发表评论