加入收藏 | 设为首页 | 会员中心 | 我要投稿 李大同 (https://www.lidatong.com.cn/)- 科技、建站、经验、云计算、5G、大数据,站长网!
当前位置: 首页 > 百科 > 正文

转载:PostgreSQL源码分析之内存上下文

发布时间:2020-12-13 17:32:34 所属栏目:百科 来源:网络整理
导读:转载:http://blog.chinaunix.net/uid-24774106-id-3547274.html 前言 PostgreSQL是我们项目采用的数据库,从来没读过数据库的代码,尽管也曾写过一些应用层的代码,希望今年能粗读一遍PostgreSQL的源码,提高自己对数据库的理解。 本系列以PostgreSQL的最新

转载: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函数分配就行了。见如下代码

  1. if(TopMemoryContext!=NULL)
  2. {
  3. /*Normalcase:allocate the nodeinTopMemoryContext*/
  4. node(MemoryContext)MemoryContextAlloc,
  5. needed);
  6. }
  7. else
  8. *Specialforstartup:use good ol'malloc)malloc(needed;
  9. Assert(node}

确定initBlockSize,nextBlockSize等的代码比较简单,我就不赘述了。allocChunkLimit这个参数的含义是在这个内存上下文中,大内存块的门限值。比如如果maxBlockSize=8K,那么系统认为1KB是比较大的内存块,低于1KB的这个门限值的,都认为是小块内存。在分配策略和释放策略上,是不同的。我们认为大内存的分配不频繁,所以我们采用直接malloc的方法,如果释放的话,就真的调用free,将内存返还给系统。但是小内存块则不同,我们认为小内存块的分配是频繁的,而且频繁的malloc/free会造成内存碎片,所以当用户调用AllocSetFree的时候,我们并不真正的返还给系统,而是挂在可用chunk列表中。等待下一次的分配。

OK,我们已经透露了一些分配和释放的原则,那么,我们就看下分配和释放部分的代码吧。

2 内存上下文中的内存操作

在PostgreSQL中,内存的分配,重分配,释放都是在内存上下文中进行的,不再直接调用系统提供的malloc/realloc/free。PostgreSQL提供了一个系列的函数,来管理内存

*
  • *Thisisthe virtualfunctiontableforAllocSet contexts.
  • /
  • static MemoryContextMethods AllocSetMethods{
  • AllocSetAlloc
  • AllocSetFree
  • AllocSetRealloc
  • AllocSetInit
  • AllocSetReset
  • AllocSetDelete
  • AllocSetGetChunkSpace
  • AllocSetIsEmpty
  • AllocSetStats
  • #ifdef MEMORY_CONTEXT_CHECKING
  • #endif
  • };

  • 下面我们重点介绍Alloc Free Realloc 这几个函数。

    前面我们提到过,在AllocSetContext这个结构体中有个很重要的成员变量:allocChunkLimit,如下所示:

    typedef struct AllocSetContext
  • {
  • MemoryContextData header;*Standard memory-context fields/
  • *Info about storage allocatedinthis context:/
  • AllocBlockblocks*head of list of blocksinthisset/
  • AllocChunkfreelist[ALLOCSET_NUM_FREELISTS]*free chunk lists*Allocation parametersforthis context/
  • SizeinitBlockSize*initial block size/
  • SizemaxBlockSize*maximum block size/
  • SizenextBlockSizenextblock sizetoallocate/
  • SizeallocChunkLimit*effective chunk size limit/
  • AllocBlockkeeperifnot}AllocSetContext;

  • typedef AllocSetContext*AllocSet;
  • 这个allocChunkLimit的是内容上下文中一个很重要的参数,这个参数的含义上面也曾提及到,含义是大小chunk的门限值。
    如果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 函数里面计算出来的。
    #define ALLOC_MINBITS 3*smallest chunk sizeis8 bytes/
  • #define ALLOCSET_NUM_FREELISTS 11
  • #define ALLOC_CHUNK_LIMIT(1<(ALLOCSET_NUM_FREELISTS-1+ALLOC_MINBITS)
  • *Size of largest chunk that we use a fixed sizefor/
  • #define ALLOC_CHUNK_FRACTION 4
  • *We allow chunkstobe at most 1/4 of maxBlockSize(less overhead/

    1. context->allocChunkLimit=ALLOC_CHUNK_LIMITwhile((Size(context+ALLOC_CHUNKHDRSZ>
    2. (maxBlockSize-ALLOC_BLOCKHDRSZ/ALLOC_CHUNK_FRACTION)
    3. context>=1;
    ALLOC_CHUNK_LIMIT的值为8K,也就说内存上下文的allocChunkLimit最大就是8K,但是实际的context - > allocChunkLimit,还需要根据 maxBlockSize来计算。下面这个while的含义是一个最大的block的应该不小于4倍的allocChunkLimit。以TopMemoryContext为例 maxBlockSize = 8K,那么allocChunkLimit应该是小于2K,所以最终计算的结果是allocChunkLimit = 1K。 TopMemoryContextu作为根内存上下文,从这里分配的内存多是用来存储子内存上下文,而子内存上下文对应的数据结构非常的小,不会超过1K,所以allocChunkLimit=1K 是合理的。而PostmasterContext的 maxBlockSize = 8M,所以PostmasterContext的allocChunkLimit=8K。

    讲完了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{
  • Sizerequired_size;

  • *
  • *The first such block has size initBlockSizeandwe double the
  • *spaceineachsucceeding blocknotmore than maxBlockSize/
  • blksize>nextBlockSize; //下一次去总仓库取网线,要多取1倍
  • >maxBlockSize;//取网线最多不能超过maxBlockSize

  • IfinitBlockSizeisless than ALLOC_CHUNK_LIMIT
  • ..but trytokeep it a power of 2/
  • required_size=chunk_size+ALLOC_BLOCKHDRSZ(blksize<required_size)
  • blksize; //如果当前要网线的员工要的太多,超过了本次应该的取的长度,则double

  • block = (AllocBlock) malloc(blksize);
  • .....

  • block->aset = set;
    block->freeptr = ((char *) block) + ALLOC_BLOCKHDRSZ;
    block->endptr = ((char *) block) + blksize;
  • ....
  • }

    有了很长的网线,就能满足当前员工的需求了。但是去总仓库申请来的 很长的网线是不是立刻就截断成1米 2米 4米 8米这种长度呢?答案是否定的。
    我们来看下员工申请网线的情况。员工申请14米的网线1根,那么仓库管理员首先干的事情是看下有没有16米的网线。对应freelist的某个chunk。如果有的话,皆大欢喜,员工拿了网线走人。对应的代码如下:
    fidx=AllocSetFreeIndex(size;
  • chunk>freelist[fidx(chunk{
  • Assert>size=size(AllocChunk)chunk>aset;//长度16的网线,是链接在一起的。

  • chunk(void;

  • #ifdef MEMORY_CONTEXT_CHECKING
  • chunk>requested_sizesetmarktocatch clobber of"unused"<chunk(char)AllocChunkGetPointer[size=0x7E;
  • #endif
  • #ifdef RANDOMIZE_ALLOCATED_MEMORY
  • *fill the allocatedspacewith junk/
  • randomize_mem;
  • #endif

  • AllocAllocInfo;
  • return AllocChunkGetPointer}
  • 很不幸,没有16米的网线,则去看下上次从总仓库那回来的网线还有多长。
    1 剩余的网线超过16米 ,则可以在这个剩余的网线上面截取。
    2 如果不够长的话,比如从总仓库带回的网线已经只剩下13米了,此时分仓库管理员会将13米的网线截成1米 4米 8米,放入Freelist中,共员工来申请使用。同时去总仓库再次申请,当然不是申请16米,好不容易去一次总仓库,不可能只申请16米 ,而是申请nextBlocksize,如前所述。
    剩余网线长度不够长,被分仓库管理员截断成规整的长度代码如下:
    >blocks{
  • Sizeavailspace=block>endptr-block>freeptr(availspace(chunk_size)//剩余长度不够长
  • *The existing active(top)block doesnothave enough roomfor
  • *the requested allocation
  • *amount ofinit.Once we push it downinthe block list
  • *we'll never trytoallocate morespacefrom it.So
  • dothatspaceinto chunks that we can puton
  • *the's freelists*Because we can onlygethere when there's less than
  • *ALLOC_CHUNK_LIMITleftinthe blockloopcannot iterate
  • *more than ALLOCSET_NUM_FREELISTS-1 times<ALLOC_MINBITS{
  • Sizeavailchunk=availspace-ALLOC_CHUNKHDRSZ;
  • inta_fidx(availchunkInmost cases'llgetback the index of thenextlarger
  • *freelist than the one we needtoput this chunkon.The
  • *exceptioniswhen availchunkisexactly a power of 2)1(a_fidx{
  • a_fidx=0;
  • availchunk}

  • chunk;

  • block+;
  • availspace;

  • chunk=availchunk;
  • #ifdef MEMORY_CONTEXT_CHECKING
  • chunk*mark it free/
  • #endif
  • chunk[a_fidx=chunk}

  • *Mark that we needtocreate a new block/
  • block; //block = NULL,需要分仓库去总仓库申请一卷长网线回来。
  • freelist上面可供分配的chunk是从哪里来的呢?上面的代码是一个途径即剩余长度不能满足员工本次需求的时候,分仓库管理员会将剩余的网线截断成8米4米这种和freelist匹配的长度,放入对应的freelist中。另外一个途径是员工归还,即AllocSetFree.
    AllocSetFree和AllocSetAlloc一样,也是分情况的。如果超过allocChunkLimit,表明员工要归还长网线,那么分仓库会将长网线亲自归还到总仓库(free)。如果员工归还的网线是16米的网线1根,直接放到16米对应的freelist中去。
    else
  • {
  • /*Normal caseintoappropriate freelistintfidxsize)set;

  • #ifdef CLOBBER_FREED_MEMORY
  • /*Wipe freed memoryfordebugging purposes/
  • memset(pointer;
  • #endif

  • #ifdef MEMORY_CONTEXT_CHECKING
  • /*Reset requested_size to 0inchunks that are on freelist/
  • chunkendif
  • set;
  • }
  • AllocSetRealloc部分的代码也很好理解,只要用这个网线申请理论去理解,这个内存上下文其实是比较简单的。
    参考文献:
    1 PostgreSQL数据库内核分析

    (编辑:李大同)

    【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容!

      推荐文章
        热点阅读