最近参与一个跟postgresql相关的开发,因项目需要有对工程中内存泄漏的问题做过一些调查,研究了一下postgresql内存的管理机制,觉得这篇博文写的很好,转来做个分享
转自: 酒中仙(点击打开链接)
本文将介绍Postgresql中独特的内存管理,它一般根据分配块的大小,决定如何分配,如果相对较大的块,进行直接分配(调用malloc),如果相对较小的块,则是可能在已经分配的空间里面取出(不需要调用malloc)。而且在内存释放的时候,小的内存块不用释放,只有大的内存块才需要调用free操作。如此情况下,内存的分配与释放会比较快。
接下来进行详细的分析,首先给出它的数据结构。
typedef struct AllocSetContext
{
} AllocSetContext;
其中最重要的两个部分是blocks链表头和freelist数组,其中freelist数组是空闲块链表数组,同一个空闲块链表中块大小相同,不同空闲块链表大小不相同,从8Byte(索引为0)开始,每次往后移动一个位置,大小变成原来的两倍,所以最大为8K。(上述的值是系统中定义的,可以更改)
在详细分析之前,先看看AllocBlock和AllocChuck相关的数据结构
typedef struct AllocBlockData
{
} AllocBlockData;
可以看出,在每个BlockData(大内存块的头部)中有指向空闲位置的指针,该内存块尾部的指针,指向下一个内存块Block的指针,对于AllocSetContext中的blocks链表中,只有第一个块是含有空闲块的,其他的块的空闲空间移到了freelist中去。(说明:为了区分,在下面的说明中,AllocBlockData称为大块,AllocChunkData称为小块,大块里面包含一个或者数个小块)
typedef struct AllocChunkData
{
} AllocChunkData;
对于第一个域有两个用途,如果是空闲的,未分配出去的块,它在freelist数组中对应大小的块链上,此时aset相当于大块中的next。如果是被分配出去的,则指向了拥有该内存块的Context。所有的分配的内存块(小块),都是从大块中分配得到的,所以小块前面都会有这个头部,它会在free时会被用到,因为对于free来说,只有一个指向要被删除块的指针,而通过块的起始指针找到块的头部,相当于传递了拥有该内存块的Context,便可以针对该context进行free操作。
下面给出分配ContextAlloc流程:
输入:分配块的大小
输出:对应的内存块
这里的context指的是AllocSetContext,块是否太大依据freelist中最大的块大小(Postgresql中为8K)来划分的。除了第一步中太大时候的情况,其他的分配内存的大小都是2的幂。对于红色字体部分的分配块的大小是按照AllocSetContext中的nextblockSize和需求空间(转换成2的幂)的较小值进行的,nextblockSize的值每用一次,值就变成原来的两倍。比如开始分配16BYTE的块,下次分配的就是32BYTE的块。
对于ContextDelete来说:
输入:删除块的指针,包含块的Context。
输出:无。
如果块比较大(大的含义和上述类似),则是在Context的blocks链中找到对应的块,从链表中删除,并且调用系统的free操作。如果块比较小,在freelist中找到对应的空闲块链表,并且插入到其中。
对于ContextRealloc来说:
输入:原来块的指针,要分配更大的块的大小,包含块的Context。
输出:返回对应的地址。
对于原来块的大小都是很大的,那么重新分配的块会是更大的。所以从Context的blocks中找到对应块,进行系统的realloc操作。这里需要修改空闲起始指针和块尾指针,以及在链表中要重新进行链接操作,因为,realloc得到的地址可能和原来的地址不同了。而在其他情况下,则按照先ContextAlloc再ContextDelete操作进行,还需要内存内容的拷贝。
啊,终于,关于内存管理的部分代码已经分析完成,接下来还有另一部分的内容,待续…
上篇文章中写到针对单个上下文(MemoryContext)而言的内存操作,在上下文中,可以分配内存块,释放以及重分配内存块,创建Context,删除Context,以及重设Context,关于使用Context的好处,我们引用源代码中的src/backend/utils/mmgr/Readme中的一段话,“相比于单纯地使用malloc,free,使用Context可以更容易释放分配出来的内存,而不用逐个释放里面的每个内存块;相对于对每个块做记录(比如说命名)而言,比较快而且比较稳定,不容易产生内存泄露,它可以忽略这些命名,从而不管具体的名字。”
介绍完了单个Context的内容之后,介绍多个Context的管理问题。在创建Context的时候,会传递一个父亲Context的参数。根据源码中的内容,Context之间是按照“森林”数据结构的方式组织的,一个Context可以有多个儿子Context,但是只有一个父亲Context。如此组织Context可以有很多的好处,其中,一个最大好处在于它能处理内存生命期嵌套方式,只要确保短生命期的内存是在相应Context的后代Context中分配的。这个特点在数据库里面是尤其有用的。当删除或者重置一个Context时,它会作用到它以及它所有的后代Context上面上去。
这里简要的给出相应的数据结构
typedef struct MemoryContextData
{
} MemoryContextData,*MemoryContext;
这个数据结构本质上可以看成是抽象超类,methods对应的是个C++类似的虚函数表,特定Context,MemoryContextData的内容是在头部的,设想一个内存块,是特定的内存块,第一部分的数据就是MemoryContextData,后面的内容是特定Context补充的。
typedef struct MemoryContextMethodsData
{
} MemoryContextMethodsData
可以看到,上篇文中介绍的AllocSet实现了MemoryContextMethodsData
在实现中,需要注意的是删除和重置操作,它需要递归地对它所有的后代处理。具体的过程,不再作具体分析。感兴趣的您可以读一下相应的这部分代码。它在postgresql-8.4.2\src\backend\utils\mmgr\mcxt.c中