内存管理数据结构
Postgresql中的内存使用是通过一种“上下文机制(context)”实现的,内存上下文管理并跟踪内存的申请,使用,释放。内存上下文之间构成一种树形结构,整个环境有且仅一个树根(TopMemoryContext),其父为空,除此之外其它上下文均需要指定父上下文(parent)。下图是内存上下文结构的示意图:
以图中红色上下文作为观察点,可以看到其包含三个指针,分别指向其父(parnet),拥有相同父上下文的下一个上下文(nextchild)以及其第一个子上下文(firstchild)。MemoryContext结构定义如下:
typedef struct MemoryContextData
{
NodeTag type; /* identifies exact kind of context */
MemoryContextMethods *methods; /* virtual function table */
MemoryContext parent; /* NULL if no parent (toplevel context) */
MemoryContext firstchild; /* head of linked list of children */
MemoryContext nextchild; /* next child of same parent */
char *name; /* context name (just for debugging) */
bool isReset; /* T = no space alloced since last reset */
} MemoryContextData;
需要特别说明的几个属性,*methods
用来记录对该上下文中进行内存操作的接口的回调函数,isReset
用来标记该上下文自上次重置之后是否进行过内存分配。
上图中MemoryContext结构描述了不同上下文之间的关系,那么内存又是如何管理的呢?实际上上图中的结构内嵌在AllocSetContext
结构中,定义如下,该结构中通过header
持有了MemoryContext的内容。
typedef struct AllocSetContext
{
MemoryContextData header; /* Standard memory-context fields */
/* Info about storage allocated in this context: */
AllocBlock blocks; /* head of list of blocks in this set */
AllocChunk freelist[ALLOCSET_NUM_FREELISTS]; /* free chunk lists */
/* Allocation parameters for this context: */
Size initBlockSize; /* initial block size */
Size maxBlockSize; /* maximum block size */
Size nextBlockSize; /* next block size to allocate */
Size allocChunkLimit; /* effective chunk size limit */
AllocBlock keeper; /* if not NULL, keep this block over resets */
} AllocSetContext;
理解好AllocSetContext
结构对于弄懂内存管理非常重要,因为实际使用的内存就由该结构来管理,我们先看一个简化的示意图:
左侧freelist是一个指针数组,包含在AllocSetContext中,使用虚线区别于指针,freelist数组包含11个指针,每一个指针指向一个特定大小内存块构成的链表,对应关系如下,索引010分别对应块大小8B,16B,32B...8KB,至此我们知道freelist数组实际上是持有最小8字节,到最大8K字节的内存块的,这些内存块在申请内存时会根据锁申请的大小选择合适的内存块进行分配。(看到此处,大家会不会联想到linux系统的内存管理,没错,这不就是和slab分配器异曲同工吗)那么问题来了,这些内存块又从何而来呢?
我们对右侧的Block部分进行研究,AllocSetContext使用其block指针指向一个内存块,块头为AllocBlockData,每当我们新创建一个内存上下文时,都会默认创建一个以AllocBlockData为头的内存块,该块大小由创建时参数
initBlockSize
指定。上图中红色部分标记了部分该结构属性的作用,AllocBlockData详细结构如下,其中各个指针的含义都是显而易见的,prev与next用于构成Block的双向链表,上图中由于只包含一个Block因此未标识。
typedef struct AllocBlockData
{
AllocSet aset; /* aset that owns this block */
AllocBlock prev; /* prev block in aset's blocks list, if any */
AllocBlock next; /* next block in aset's blocks list, if any */
char *freeptr; /* start of free space in this block */
char *endptr; /* end of space in this block */
} AllocBlockData;
我们注意到,在以AllocBlockData为头的Block中,后面紧跟着AllocChunkData块,该结构块就是实际上已被分配使用的内存块,我们先看一下该块结构,如下,当前重点关注的时size表示该chunk块中内存空间的大小,aset指针根据该chunk块是否已被分配有两个作用,如果chunk块已分配,则指向AllocSetContext,反之,aset用于构成freelist链表。也就是说,如果当前chunk还未被分配,那么这个内存块则是通过aset指针串联在对应大小的freelist链表中的。
typedef struct AllocChunkData
{
/* aset is the owning aset if allocated, or the freelist link if free */
void *aset;
/* size is always the size of the usable space in the chunk */
Size size;
#ifdef MEMORY_CONTEXT_CHECKING
/* when debugging memory usage, also store actual requested size */
/* this is zero in a free chunk */
Size requested_size;
#endif
} AllocChunkData;
至此我们已经对内存管理的基本数据结构有了一定了解,基于此,我们便可以理解内存的动态分配以及使用过程。
内存分配过程
- 初始化一个新的MemoryContext时,首先创建AllocSetContext数据结构,完成默认初始化,同时将该结构加入到前文描述的Context树中,此时该上下文包含一个Block,所有freelist指针皆为空
- 当在该上下文中申请内存时,使用接口
MemoryContextAlloc
,该接口通过调用AllocSetAlloc
完成内存的实际分配,该接口也是内存申请的核心接口(大家最好逐行理解代码,这段代码写的也是很有水平) - 申请内存时,首先在freelist中进行查找,如果freelist中不存在合适块,则需要去block中申请。freelist中查找的方法十分简单,1)根据申请空间大小可以计算出对应的期望查找的freelist链表的索引(索引号与块大小对应关系前面已经介绍了),2)此时freelist[index]如果非空,则表示存在符合要求的块,直接返回该块地址即可,同时将该块从freelist数组中移除。
- 初次申请时,freelist数组皆为空,所以我们会去Block中申请。首先判断block中的剩余空间是否充足(freeptr与endptr指向的大小与申
请size进行对比),如果空间足够,则按照申请大小从freeptr指向位置开始,创建chunk,然后更新freeptr指针,注意freeptr向后挪动(申请空间大小+chunk头结构大小),然后返回chunk指针。现在回看前面的示意图,我们可以看到此时已成功分配了两个大小不同的chunk块,freeptr指针指向最新的可用空间起始位置 -
如果,再次申请内存时,发现剩余空间不足以满足申请要求,那么此时则需要做两件事:1) 将当前Block剩余的空间进行拆分加入到freelist数组中,拆分逻辑也很直观,从大到小选择满足2的幂次块进行拆分,最小块需要满足8B大小(如果拆到最后还剩5B空间,则这个空间会被浪费掉);2) 创建一个新的Block块,更新block指针指向该块,以后的新申请都会在此块上进行,原Block块已被拆分由freelist进行索引啦,下图展示了此时的内存结构示意图,结合该图可以更好理解Block与freelist之间的关系,AllocBlockData2是新申请的Block,原Block剩余空间已被拆分通过freelist进行索引。
相似的,当AllocBlockData2中无法满足新的空间申请时,则会扩展新的Block,此时的内存关系图如下,现在应该很清楚内存管理机制时如何工作的了吧。
结合上图与描述希望大家能够粗略理解内存的分配机制,下面补充介绍内存分配中的一些细节
initBlockSize/maxBlockSize/nextBlockSize
以上前两个参数是一个AllocSetContext创建时需要指定的参数,nextBlockSize是动态计算的参数
MemoryContext
AllocSetContextCreate(MemoryContext parent,
const char *name,
Size minContextSize,
Size initBlockSize,
Size maxBlockSize)
initBlockSize指定了初始化时Block的大小。maxBlockSize指定了后续申请新的Block时申请大小的上限。nextBlockSize在初始化时被设置为与initBlockSize相同。
当申请新的Block时,nextBlockSize首先会设置为之前的一倍,如果此时该值小于maxBlockSize,则按照当前大小申请新的Block空间,如果此时该值大于maxBlockSize,则按照maxBlockSize申请新的Block空间。从代码中我们可以看到两个常用宏,代表了初始化时的8KB以及最大8MB的块大小。
#define ALLOCSET_DEFAULT_INITSIZE (8 * 1024)
#define ALLOCSET_DEFAULT_MAXSIZE (8 * 1024 * 1024)
下面截取了用于计算申请Block空间部分的代码,其中可以看出
/*
* The first such block has size initBlockSize, and we double the
* space in each succeeding block, but not more than maxBlockSize.
*/
blksize = set->nextBlockSize;
set->nextBlockSize <<= 1;
if (set->nextBlockSize > set->maxBlockSize)
set->nextBlockSize = set->maxBlockSize;
/*
* If initBlockSize is less than ALLOC_CHUNK_LIMIT, we could need more
* space... but try to keep it a power of 2.
*/
required_size = chunk_size + ALLOC_BLOCKHDRSZ + ALLOC_CHUNKHDRSZ;
while (blksize < required_size)
blksize <<= 1;
/* Try to allocate it */
block = (AllocBlock) malloc(blksize);
allocChunkLimit
allocChunkLimit是AllocSetContext初始化时默认配置的参数,默认初始化为8KB,用于识别申请空间是否需要单独申请(实际上是一种大块分配策略)。当一次申请的空间大于该值时,上下文不会从当前Block中找空间(因为已经大于),而是单独申请一个指定大小的Block并将其挂在活跃block(AllocSetContext的block指针指向的Block)之后。这种大块单独申请机制也是与freelist机制相配合的,在后面内存释放由具体描述。相关代码请参考:
if (size > set->allocChunkLimit)
{
chunk_size = MAXALIGN(size);
blksize = chunk_size + ALLOC_BLOCKHDRSZ + ALLOC_CHUNKHDRSZ;
block = (AllocBlock) malloc(blksize);
if (block == NULL)
{
MemoryContextStats(TopMemoryContext);
ereport(ERROR,
(errcode(ERRCODE_OUT_OF_MEMORY),
errmsg("out of memory"),
errdetail("Failed on request of size %zu.", size)));
}
block->aset = set;
block->freeptr = block->endptr = ((char *) block) + blksize;
chunk = (AllocChunk) (((char *) block) + ALLOC_BLOCKHDRSZ);
chunk->aset = set;
chunk->size = chunk_size;
#ifdef MEMORY_CONTEXT_CHECKING
/* Valgrind: Will be made NOACCESS below. */
chunk->requested_size = size;
/* set mark to catch clobber of "unused" space */
if (size < chunk_size)
set_sentinel(AllocChunkGetPointer(chunk), size);
#endif
#ifdef RANDOMIZE_ALLOCATED_MEMORY
/* fill the allocated space with junk */
randomize_mem((char *) AllocChunkGetPointer(chunk), size);
#endif
/*
* Stick the new block underneath the active allocation block, if any,
* so that we don't lose the use of the space remaining therein.
*/
if (set->blocks != NULL)
{
block->prev = set->blocks;
block->next = set->blocks->next;
if (block->next)
block->next->prev = block;
set->blocks->next = block;
}
else
{
block->prev = NULL;
block->next = NULL;
set->blocks = block;
}
AllocAllocInfo(set, chunk);
/*
* Chunk header public fields remain DEFINED. The requested
* allocation itself can be NOACCESS or UNDEFINED; our caller will
* soon make it UNDEFINED. Make extra space at the end of the chunk,
* if any, NOACCESS.
*/
VALGRIND_MAKE_MEM_NOACCESS((char *) chunk + ALLOC_CHUNK_PUBLIC,
chunk_size + ALLOC_CHUNKHDRSZ - ALLOC_CHUNK_PUBLIC);
return AllocChunkGetPointer(chunk);
}
内存上下文释放内存
释放指定内存块通过接口AllocSetFree
实现,内存释放分两种情况,1)如果内存块大小小于allocChunkLimit(也就是说不是单独分配的大块内存),这种情况下实际上不释放内存,直接修改chunk块的aset指针,将该chunk块挂接到对应大小的freelist数组中,以便下次使用,2)如果是单独分配的大内存块,则需要将该Block块从Block链表中取出(修改前后块的prev,next指针),然后调用free释放掉该Block。
static void
AllocSetFree(MemoryContext context, void *pointer)
{
AllocSet set = (AllocSet) context;
AllocChunk chunk = AllocPointerGetChunk(pointer);
AllocFreeInfo(set, chunk);
#ifdef MEMORY_CONTEXT_CHECKING
VALGRIND_MAKE_MEM_DEFINED(&chunk->requested_size,
sizeof(chunk->requested_size));
/* Test for someone scribbling on unused space in chunk */
if (chunk->requested_size < chunk->size)
if (!sentinel_ok(pointer, chunk->requested_size))
elog(WARNING, "detected write past chunk end in %s %p",
set->header.name, chunk);
#endif
if (chunk->size > set->allocChunkLimit)
{
/*
* Big chunks are certain to have been allocated as single-chunk
* blocks. Just unlink that block and return it to malloc().
*/
AllocBlock block = (AllocBlock) (((char *) chunk) - ALLOC_BLOCKHDRSZ);
/*
* Try to verify that we have a sane block pointer: it should
* reference the correct aset, and freeptr and endptr should point
* just past the chunk.
*/
if (block->aset != set ||
block->freeptr != block->endptr ||
block->freeptr != ((char *) block) +
(chunk->size + ALLOC_BLOCKHDRSZ + ALLOC_CHUNKHDRSZ))
elog(ERROR, "could not find block containing chunk %p", chunk);
/* OK, remove block from aset's list and free it */
if (block->prev)
block->prev->next = block->next;
else
set->blocks = block->next;
if (block->next)
block->next->prev = block->prev;
#ifdef CLOBBER_FREED_MEMORY
wipe_mem(block, block->freeptr - ((char *) block));
#endif
free(block);
}
else
{
/* Normal case, put the chunk into appropriate freelist */
int fidx = AllocSetFreeIndex(chunk->size);
chunk->aset = (void *) set->freelist[fidx];
#ifdef CLOBBER_FREED_MEMORY
wipe_mem(pointer, chunk->size);
#endif
#ifdef MEMORY_CONTEXT_CHECKING
/* Reset requested_size to 0 in chunks that are on freelist */
chunk->requested_size = 0;
#endif
set->freelist[fidx] = chunk;
}
}
内存上下文重置(reset)
内存上下文重置接口为AllocSetReset
,所谓内存重置主要完成两件事1)对当前上下文的freelist数组清零,2)遍历当前上下文的Block结构(Block组成了一个链表),除了第一个Block(也叫活跃Block)外其余Block全部调用free接口进行内存释放。
针对第一个Block,在上下文结构中通过Keeper关键字进行记录,对于此块Block,只进行指针重置(freeptr),不释放内存,目的是避免使用上下文时需要频繁申请释放Block。大家可自行阅读对应代码,此处不再赘述。
内存上下文删除
对应接口AllocSetDelete
,删除操作与重置操作唯一的区别就是,删除操作会将上下文中所有的Block均释放掉。
对外接口
以上介绍了内存上下文中内存相关操作的实现,以上接口实际上封装在上下文的methods字段中,在我们实际使用中,我们通过对外接口完成对应操作,这些接口屏蔽了内存操作的具体实现,通过回调的方式对内存进行操作,这些接口包括palloc
,pfree
,repalloc
等,当前postgresql使用的上下文操作接口只有前文介绍的这一套,如果日后有新增的实现,那么只需要创建上下文时将methods字段赋值为新接口即可,对外则透明。