显示标签为“源码分析”的博文。显示所有博文
显示标签为“源码分析”的博文。显示所有博文

2009年4月3日星期五

Nebula3基础层-App


1.控制台程序需要继承ConsoleApplication类,如果需要做一些初始化可以覆写ConsoleApplication::Open()方法,在覆写该方法时要记得调用父类相同的方法来初始化。程序的逻辑代码可以在ConsoleApplication::Run()方法中实现。

2.ConsoleApplication::Open()方法的主要是初始化核心子系统,IO子系统和脚本系统。

2009年2月10日星期二

关于Nebula3的单例

Nebula3中有两种形式的单例:

1.使用__DeclareSingleton宏定义的单例,这是一种在线程中的单例。在每个线程中只有一个对象,但在整个应用程序中可能会有多个。

把__DeclareSingleton宏展开如下:

public:
ThreadLocal static type * Singleton;
static type * Instance() { n_assert(0 != Singleton); return Singleton; };
static bool HasInstance() { return 0 != Singleton; };

可以看见我们定义了一个由ThreadLocal修饰的静态属性,在types.h中,我们可以看到ThreadLocal是如下定义的:

#if __WIN32__
#define ThreadLocal __declspec(thread)

简单地说__declspec(thread)声明一个线程局部变量并具有线程存储时限,以便链接器安排在创建线程时自动分配的存储。

在Nebual3引擎中大量地使用这种形式的单例,这和整个的Nebula3的多线程架构设计相关。

2.使用__DeclareInterfaceSingleton宏定义单例,这就是全局单例了,在整个应用程序中只存在一个对象。

展开__DeclareInterfaceSingleton宏:

static type * Singleton;
static type * Instance() { n_assert(0 != Singleton); return Singleton; };
static bool HasInstance() { return 0 != Singleton; };

2009年2月9日星期一

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

在上一篇blog中我们已经讨论完关于内存池的初始化,这里我们来看看是如何从内存池中取得所需的内存块。要从内存池中分配一块内存块,我们需要调用void* MemoryPool::Alloc()方法,接下来我们详细介绍该方法是如何执行的:

1.首先判断存放可分配内存块的内存页的数组roomyPages是否为空,如果为空就调用void MemoryPool::AllocPage()创建一个新的内存页。

if (this->roomyPages.IsEmpty())
{
// all pages are full, or no pages exist yet, need to allocate new page
this->AllocPage();
}

2.从roomyPages中取得一个内存页,并调用void* MemoryPoolPage::Alloc()方法实际分配一块内存块。

MemoryPoolPage* page = this->roomyPages.Back();
n_assert(page->GetNumFreeBlocks() > 0);

// note: the page pointer is guaranteed to be the one
// from the end of the roomyPages array!
void* ptr = page->Alloc();

3.判断刚才取得的内存页是否还有可以分配的内存块。如果没有就把它从roomyPages中删除掉。

if (0 == page->GetNumFreeBlocks())
{
this->roomyPages.EraseIndex(this->roomyPages.Size() - 1);
}

4.返回指向刚才分配的内存块的指针。

我们更加详细的看一下void* MemoryPoolPage::Alloc()是如何分配一个内存块的。

1.找第一个还没被使用的内存块,并把该内存块块头的pageObject指向本内存页,代码如下:

BlockIndex newBlockIndex = this->firstFreeBlockIndex;
BlockHeader* newBlock = this->BlockIndexToPointer(newBlockIndex);
n_assert(0 == newBlock->pageObject);
newBlock->pageObject = this;

2.设置该内存块的前置和后置,让已分配的内存块和未分配的内存块通过前置和后置关系形成两个逻辑上的列表。并让firstFreeBlockIndex索引指向未分配内存块逻辑列表的第一位,firstAllocBlockIndex索引指向已分配内存块逻辑列表的第一位。并且把当前已分配的内存块放在已分配逻辑列表的第一位。

this->RemoveBlockFromList(this->firstFreeBlockIndex, newBlockIndex);
this->InsertBlockIntoList(this->firstAllocBlockIndex, newBlockIndex);

3.移动指针,让指针指向内存块体。

void* dataPtr = (void*) (newBlock + 1);

4.把已分配内存块的记录数numAllocBlocks加1,返回指向内存块体的指针。

this->numAllocBlocks++;

以上就完成了从内存池中分配一个内存块。接下来我们探讨一下如何释放一个内存块。

我们调用void MemoryPool::Free(void* ptr)来释放一个从内存池分配的内存块。执行步骤如下:

1.通过传入的指针,我们找到分配该内存块的内存页。

MemoryPoolPage* page = MemoryPoolPage::GetMemoryPoolPageFromDataPointer(ptr);

2.调用内存页的void MemoryPoolPage::Free(void* ptr)来真正释放掉内存块。

3.如果该内存页包含一个可以分配的内存块就把该内存页放入roomyPages中。如果该内存页所有的内存块都没被分配,并且内存不止一个内存页,那么就把该内存页从内存池中删除掉。

if (page->GetNumFreeBlocks() == 1)
{
// we've been full previously, but are roomy now
this->roomyPages.Append(page);
}
else if (page->GetNumAllocatedBlocks() == 0)
{
// the page no longer contains any allocated blocks, free
// the entire page, unless this is the last page to
// prevent expensive allocation/deallocations if only
// one block is allocated/freed in this memory pool
if (this->pages.Size() > 1)
{
this->FreePage(page);
}
}

void
MemoryPool::FreePage(MemoryPoolPage* page)
{
n_assert(page->GetNumAllocatedBlocks() == 0);
IndexT pagesIndex = this->pages.FindIndex(page);
n_assert(pagesIndex != InvalidIndex);
IndexT roomyPagesIndex = this->roomyPages.FindIndex(page);
n_assert(roomyPagesIndex != InvalidIndex);

// note: we don't use EraseIndexSwap to keep the page order
// in the order of allocations
this->pages.EraseIndex(pagesIndex);
this->roomyPages.EraseIndex(roomyPagesIndex);
delete page;
}

同样接下我们看看内存页是如何释放掉内存块的void MemoryPoolPage::Free(void* ptr)。

1.取得要删除内存块的索引。

BlockHeader* block = ((BlockHeader*)ptr) - 1;
this->VerifyBlockHeader(block);
BlockIndex blockIndex = this->BlockPointerToIndex(block);

2.把该内存块块头pageObject指针设置为0。

n_assert(this == block->pageObject);
block->pageObject = 0;

3.把该内存块从已分配内存块的逻辑列表断开,并把它放入未分配内存块逻辑列表中。

this->RemoveBlockFromList(this->firstAllocBlockIndex, blockIndex);
this->InsertBlockIntoList(this->firstFreeBlockIndex, blockIndex);

4.已分配内存块的数量减1。

this->numAllocBlocks--;

以上就是n3内存池实现的一个大概讨论。

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;

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

2009年2月7日星期六

Nebula3内存模块-memory.h

Nebula3的内存模块提供了一些操作内存的函数,要使用这些函数就要引入memory.h头文件。打开memory.h头文件:

#include "core/config.h"

#if (__WIN32__ || __XBOX360__)
#include "memory/win360/win360memory.h"
#elif __WII__
#include "memory/wii/wiimemory.h"
#else
#error "UNKNOWN PLATFORM"
#endif
#endif

对于win32平台我们只关心win360memory.h这个头文件就可以了。进一步打开win360memory.h文件,在文件的顶部有如下定义:

#if __WIN32__
#include "memory/win32/win32memory.h"
#elif __XBOX360__
#include "memory/xbox360/xbox360memory.h"
#endif

如果是win32平台,该文件还引入win32memory.h文件。打开win32memory.h后,我们看到几个针对win32平台操作内存的函数。这个几个方法是:

1.void Memory::Copy(const void* , void* , size_t ):调用操作系统的
void CopyMemory(PVOID ,const VOID *,SIZE_T),拷贝内存中的数据。注意void Memory::Copy(void*,void*,size_t)方法的前面两个参数刚好是和void CompyMemory(PVOID,VOID,SIZE_T)方法前面两个参数对调。

2.void Memory::CopyToGraphicsMemory(const void* , void* , size_t ):从系统内存中拷贝数据到显卡内存,这在有些平台上需要这样处理。在win32上就不需要了,该方法内部直接调用void Memory::Copy(void*,void*,size_t)。

3.void Memory::Clear(void* ptr, size_t numBytes):调用操作系统的void ZeroMemory(PVOID,SIZE_T);用0覆盖指定的内存。

4.void Memory::Fill(void* ptr, size_t numBytes, unsigned char value):调用操作系统的void FillMemory(PVOID ,SIZE_T,BYTE Fill);用特定的值覆盖指定的内存。

我们再次回到win360memeory.h文件中,看看这个文件中的几个重要的方法:

1.void* Memory::Alloc(HeapType heapType, size_t size):很明显这又是个内存分配的方法,但第一参数HeapType是什么呢?打开win360memoryconfig.h文件,我们看到 HeapType是一个枚举的定义,定义12种不同类型的堆。使用不同的堆类型是为了降低内存碎片和把相同的数据放在一起提高缓存的使用。

在 win360memoryconfig.h中还通过extern HANDLE volatile Heaps[NumHeapTypes];定义一个包含指向内存堆指针的数组。并使用void SetupHeaps()方法在程序启动的时候,对这12中类型的内存堆进行初始化。

从上面的分析中,我门知道Alloc(HeapType, size_t)函数就是在指定的内存堆上分配一个给定大小的内存空间。

2.void * Memory::Realloc(HeapType, void* , size_t):在指定的内存堆上重新分配内存空间。

3.void Memory::Free(HeapType, void* ):在指定的内存堆上释放已分配的内存空间。

4.void* __cdecl operator new(size_t):替换全局new操作,在ObjectHeap类型的堆上分配对象内存空间。

5.void* __cdecl operator new[](size_t):替换全局new[]操作,在ObjectArrayHeap类型的堆上分配内存空间。

6.void __cdecl operator delete(void* ):替代全局delete操作,在ObjectHeap类型的堆上删除内存空间。

7.void __cdecl operator delete[](void* ):替代全局delete[]操作,在在ObjectArrayHeap类型的堆上删除内存空间。

以上就是memory.h头文件提供给我们操作内存的主要方法。

2009年2月5日星期四

Nebula3内存模块-Heap类

要使用Heap类必须包含heap.h头文件,让我们看看heap.h中包含有哪些东西。

打开heap.h我们发现对于win32和xbox360平台有如下定义typedef Win360::Win360Heap Heap,说明起实际作用的是Win360Heap类。

接下来我们抛开有关内存的调试信息来分析一下Win360Heap类到底做了哪些事情。首先从Win360Heap类的构造函数开始,构造函数在一开始就通过HeapCreate(DWORD,SIZE_T,SIZE_T创建一个自动增长的堆,并且判断如果是在win32平台下默认开启low-fragmentatio-heap

Win360Heap类中有三个主要的方法是Win360Heap::Alloc(size_t size),Win360Heap::Realloc(void* ptr, size_t newSize),Win360Heap::Free(void* ptr)。我们逐一来看看这三个方法的功能。

1.Win360Heap::Alloc(size_t size)方法是调用操作系统HeapAlloc(HANDLE,DWORD,SIZE_T)函数在构造函数创建的堆上分配一块内存空间。

2.Win360Heap::Realloc(void* ptr, size_t newSize)方法是调用操作系统HeapReAlloc(HANDLE,DWORD,LPVOID,SIZE_T)函数在构造函数创建的堆上重新分配一个内存空间。

3.Win360Heap::Free(void* ptr)是调用操作系统HeapFree(HANDLE,DWORD,LPVOID)方法释放在构造函数创建的堆上分配的内存空间。

从上我们可以大概了解到Nebula3的Heap类允许我们创建一个动态增长的堆,并从该堆上分配所需要的内存。