您的位置:首页 > 数据库

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

2014-01-12 23:36 309 查看
转载: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)

    {

        /* Normal case: allocate
the node in TopMemoryContext */

        node = (MemoryContext) MemoryContextAlloc(TopMemoryContext,

                                                 needed);

    }

    else

    {

        /* Special case for startup: use
good ol' malloc */

        node = (MemoryContext) malloc(needed);

        Assert(node != NULL);

    }

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

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


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

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

   

/*

 * This is the virtual function table for AllocSet
contexts.

 */

static MemoryContextMethods AllocSetMethods = {

    AllocSetAlloc,

    AllocSetFree,

    AllocSetRealloc,

    AllocSetInit,

    AllocSetReset,

    AllocSetDelete,

    AllocSetGetChunkSpace,

    AllocSetIsEmpty,

    AllocSetStats

#ifdef MEMORY_CONTEXT_CHECKING

    ,AllocSetCheck

#endif

};

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

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

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;

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 size is 8
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 size for */

    #define ALLOC_CHUNK_FRACTION 4

    /* We allow chunks to be at most 1/4
of maxBlockSize (less overhead) */ 

       context->allocChunkLimit = ALLOC_CHUNK_LIMIT;

        while ((Size) (context->allocChunkLimit + ALLOC_CHUNKHDRSZ) >

             (Size) ((maxBlockSize - ALLOC_BLOCKHDRSZ) /ALLOC_CHUNK_FRACTION))

            context->allocChunkLimit >>= 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),然后员工用,员工归还的时候(AllocSetFree),分仓库真的将这10000米网线归还给总仓库(free)。

    block就是分仓库批发过来的很长的网线,既然是批发,就要有规则,不可能今天去总仓库取1米,明天去取3米,公司总仓库烦都烦死了。maxBlockSize是分仓库一次最多取的长度,nextBlockSize记录的是下一次我应该去总仓库取多少米。以Postmastercontext为例,刚初始化的时候,nextBlockSize=8K,maxBlockSize=8M。这个分仓库刚开始的时候,他取的是8K,因为员工用完了还会归还,所以,一旦发生货源不足的话,下一次进货,应该是nextBlockSize×2。
请看分仓库去总仓库申请长网线的代码:

    if (block == NULL)

    {

        Size        required_size;

        /*

         * 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;    
//下一次去总仓库取网线,要多取1倍

        if (set->nextBlockSize > set->maxBlockSize)

            set->nextBlockSize = set->maxBlockSize;//取网线最多不能超过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;            
//如果当前要网线的员工要的太多,超过了本次应该的取的长度,则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 = set->freelist[fidx];

    if (chunk != NULL)

    {

        Assert(chunk->size >= size);

        set->freelist[fidx] = (AllocChunk) chunk->aset;//长度16的网线,是链接在一起的。

        chunk->aset = (void *) set;

#ifdef MEMORY_CONTEXT_CHECKING

        chunk->requested_size = size;

        /* set mark to catch
clobber of "unused" space */

        if (size < chunk->size)

            ((char *) AllocChunkGetPointer(chunk))[size] = 0x7E;

#endif

#ifdef RANDOMIZE_ALLOCATED_MEMORY

        /* fill the allocated space with junk */

        randomize_mem((char *) AllocChunkGetPointer(chunk), size);

#endif

        AllocAllocInfo(set, chunk);

        return AllocChunkGetPointer(chunk);

    }

      很不幸,没有16米的网线,则去看下上次从总仓库那回来的网线还有多长。
    1 剩余的网线超过16米 ,则可以在这个剩余的网线上面截取。
    2 如果不够长的话,比如从总仓库带回的网线已经只剩下13米了,此时分仓库管理员会将13米的网线截成1米 4米 8米,放入Freelist中,共员工来申请使用。同时去总仓库再次申请,当然不是申请16米,好不容易去一次总仓库,不可能只申请16米,而是申请nextBlocksize,如前所述。
    剩余网线长度不够长,被分仓库管理员截断成规整的长度代码如下:

if ((block = set->blocks) != NULL)

    {

        Size        availspace = block->endptr - block->freeptr;

        if (availspace < (chunk_size + ALLOC_CHUNKHDRSZ))//剩余长度不够长

        {

            /*

             * The existing active (top) block does not have
enough room for

             * the requested allocation, but it might still have a useful

             * amount of space in it. Once
we push it down in the block list,

             * we'll never try to allocate more space from
it. So, before we

             * do that, carve up its free space into
chunks that we can put on

             * the set's freelists.

             *

             * Because we can only get here when there's less than

             * ALLOC_CHUNK_LIMIT left in the block, this loop cannot
iterate

             * more than ALLOCSET_NUM_FREELISTS-1 times.

             */

            while (availspace >= ((1 << ALLOC_MINBITS) + ALLOC_CHUNKHDRSZ))

            {

                Size        availchunk = availspace - ALLOC_CHUNKHDRSZ;

                int            a_fidx = AllocSetFreeIndex(availchunk);

                /*

                 * In most cases, we'll get back
the index of the next larger

                 * freelist than the one we need to put this chunk on.    The

                 * exception is when availchunk is exactly a power of 2.

                 */

                if (availchunk != ((Size) 1 << (a_fidx + ALLOC_MINBITS)))

                {

                    a_fidx--;

                    Assert(a_fidx >= 0);

                    availchunk = ((Size) 1 << (a_fidx + ALLOC_MINBITS));

                }

                chunk = (AllocChunk) (block->freeptr);

                block->freeptr += (availchunk + ALLOC_CHUNKHDRSZ);

                availspace -= (availchunk + ALLOC_CHUNKHDRSZ);

                chunk->size = availchunk;

#ifdef MEMORY_CONTEXT_CHECKING

                chunk->requested_size = 0;        /* mark
it free */

#endif

                chunk->aset = (void *) set->freelist[a_fidx];

                set->freelist[a_fidx] = chunk;

            }

            /* Mark that we need to create a new block */

            block = NULL;  //block = NULL,需要分仓库去总仓库申请一卷长网线回来。

        }

    }

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

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 freed memory for debugging purposes */

        memset(pointer, 0x7F, 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;

    }

     AllocSetRealloc部分的代码也很好理解,只要用这个网线申请理论去理解,这个内存上下文其实是比较简单的。

参考文献:
1 PostgreSQL数据库内核分析
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: