转载:http://blog.chinaunix.net/uid-24774106-id-3547274.html
前言
Postgresql是我们项目采用的数据库,从来没读过数据库的代码,尽管也曾写过一些应用层的代码,希望今年能粗读一遍Postgresql的源码,提高自己对数据库的理解。
本系列以Postgresql的最新版本9.2.3源码为例,学习Postgresql。
Postgresql从7.1开始,引入了内存上下文(MemoryContent)机制,片汤话我不多说,简单的理解,内存上下文提供了一种管理内存的机制。我们通过图表和代码分析来理解Postgresql的内存上下文。
内存上下文的主要数据结构都在上图中了,主要是4个基本数据结构之间的关系分别是
- MemoryContextData
- AllocSetContext
- AllocBlockData
- AllocChunkData
这四个数据结构中核心的数据结构是AllocSetContext,从上图中也可以清楚的看出来。下面我们详细讲述之
1 内存上下文的创建
任何一个Postgresql进程使用内存上下文之前,都必须首先进行初始化,这句话是一句废话,呵呵。内存上下文的初始化是PostMasterMain函数里面开头调用的MemoryContextInit完成的。这个函数干了两件事情:
- 创建了内存上下文的根 TopMemoryContext
- 创建了TopMemoryContext的第一个子节点ErrorContext。
创建内存上下文的工作是由AllocSetContextCreate函数完成的。这个很有意思。MemoryContext明明是上面提到的第一种数据结构,他的创建函数偏偏是AllocSetContextCreate,这个函数顾名思义也知道是创建第二个数据结构的。这其实很好理解,看上面绘制的图片可以看出,MemoryContextData不过是核心数据结构AllocSetContext的第一个成员变量(更严格的说是它的一个指针类型的成员变量指向这个MemoryContextData)。
AllocSetContextCreate这个函数其实是分成两部分的
- MemoryContextCreate ,创建MemoryContext
- 创建AllocSetContxt剩余的部分,主要是确定initBlockSize,nextBlockSize,maxBlockSize和allocChunkLimit的大小。
对于MemoryContextCreate这个函数,TopMemoryContext是没有parent的,所以他的parent指针是NULL;另外一个需注意的点是method,在TopMemoryContext创建methods指针指向了一个结构体,这个结构体内是一系列分配释放相关的函数,都画在了上图的右上角。因为TopMemeoryContext是根,所以他的分配 需要用malloc,其他的MemoryContext创建的时候,就不需要调用系统函数malloc了,直接用method函数指针系列里面AllocSetAlloc函数分配就行了。见如下代码
- if(TopMemoryContext!=NULL)
- {
- /*Normalcase:allocate the nodeinTopMemoryContext*/
- node(MemoryContext)MemoryContextAlloc,
- needed);
- }
- else
- *Specialforstartup:use good ol'malloc)malloc(needed;
- Assert(node}
确定initBlockSize,nextBlockSize等的代码比较简单,我就不赘述了。allocChunkLimit这个参数的含义是在这个内存上下文中,大内存块的门限值。比如如果maxBlockSize=8K,那么系统认为1KB是比较大的内存块,低于1KB的这个门限值的,都认为是小块内存。在分配策略和释放策略上,是不同的。我们认为大内存的分配不频繁,所以我们采用直接malloc的方法,如果释放的话,就真的调用free,将内存返还给系统。但是小内存块则不同,我们认为小内存块的分配是频繁的,而且频繁的malloc/free会造成内存碎片,所以当用户调用AllocSetFree的时候,我们并不真正的返还给系统,而是挂在可用chunk列表中。等待下一次的分配。
OK,我们已经透露了一些分配和释放的原则,那么,我们就看下分配和释放部分的代码吧。
2 内存上下文中的内存操作
在Postgresql中,内存的分配,重分配,释放都是在内存上下文中进行的,不再直接调用系统提供的malloc/realloc/free。Postgresql提供了一个系列的函数,来管理内存
下面我们重点介绍Alloc Free Realloc 这几个函数。
前面我们提到过,在AllocSetContext这个结构体中有个很重要的成员变量:allocChunkLimit,如下所示:
如果Postgresql需要在内存上下文分配大于allocChunkLimit的内存区域,那么内存上下文认为这是分配较大的内存,采用malloc的方法,同时将分配出来的block链入内存上下文的block链表中。如果用户释放该内存区域(实际上是chunk ),那么内存上下文会真正的free,返还给操作系统。
如果Postgresql需要在内存上下文分配小于allocChunkLimit的内存区域,那么行为是不同,往根本上将,这些小块内存当用户选择释放的时候,并不真正的调用free,而是将小块内存作为free chunk,根据大小链接在freelist。freelist的概念和伙伴内存系统有些类似,有11条链表,每条链表的chunk大小是不同的。分别是8/16/32/64/128/256/512/1024/2048/4096/8192。当进程调用 AllocSetFree 去释放这些小块内存的时候,内存上下文会将这些内存块放到freelist对应的链表中,以待下一次分配。 这么做的好处是防止小块内存的不停malloc/free造成大量的碎片产生。
这么看起来allocChunkLimit这个值很重要,那么这个值是怎么算出来的呢。首先需要说allocChunkLimit,不同的内存上下文,其大小可能是不同的。它的值大小是在 AllocSetContextCreate 函数里面计算出来的。
- context->allocChunkLimit=ALLOC_CHUNK_LIMITwhile((Size(context+ALLOC_CHUNKHDRSZ>
- (maxBlockSize-ALLOC_BLOCKHDRSZ/ALLOC_CHUNK_FRACTION)
- context>=1;
讲完了allocChunkLimit这个参数,nextBlockSize也很重要。block和chunk是这个内存上下文的比较重要的概念。这个概念简单理解就是大公司管理网线(因为内存有申请和释放,网线不用之后,还可以归还回去)。操作系统是个全公司总仓库,它的有点是货源充足(仓库里有大量的内存空间可用),缺点是提货不方便,你可以想想,几万人要1米 2 米的网线都要去千里之外的全公司总仓库,我们有多烦,不光我们烦躁,网线管理员也很烦躁,因为短则1米,长则上千米网线频繁的切割,会造成仓库的混乱。对应操作系统来讲,就是小块内存的频繁申请和释放,会造成内存碎片,仓库空间虽大,但是横七竖八的小网线弄得在也分配不了长网线了。 那么怎么办呢。很简单,成立分仓库。分仓库就是内存上下文。分仓库负责申请一段很长的网线,然后给公司员工用。员工用完了网线,再还给分仓库,就不用归还到全公司总仓库了,直接归还分仓库,分仓库会按照网线长短放在11个地方,存放网线,下次员工来取了,直接向对应的房间(对应的freelist)去取。有时候员工可能会取比较长的网线,比如这个员工要10000米的网线,分仓库去总仓库去取(malloc),然后员工用,员工归还的时候( ),分仓库 真的将这10000米网线归还给总仓库(free )。
block就是分仓库批发过来的很长的网线,既然是批发,就要有规则,不可能今天去总仓库取1米,明天去取3米,公司总仓库烦都烦死了。maxBlockSize是分仓库一次最多取的长度,nextBlockSize记录的是下一次我应该去总仓库取多少米。以Postmastercontext为例,刚初始化的时候,nextBlockSize=8K,maxBlockSize=8M。这个分仓库刚开始的时候,他取的是8K,因为员工用完了还会归还,所以,一旦发生货源不足的话,下一次进货,应该是nextBlockSize×2。 请看分仓库去总仓库申请长网线 的代码: (block{
block->freeptr = ((char *) block) + ALLOC_BLOCKHDRSZ;
block->endptr = ((char *) block) + blksize;
}