转自:http://blog.csdn.net/u014539401/article/details/51893272
PostgreSQL在7.1版本引入了内存上下文机制来解决日益严重的内存泄漏的问题,在引入了这种“内存池”机制后,数据库中的内存分配改为在“内存上下文中”进行,对用户来说,对内存的申请由原来的malloc、free变成了palloc、pfree。对内存上下文的常用操作包括:
这里引入两个概念:
#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)) #define ALLOC_CHUNK_FRACTION 4 /* Size of largest chunk that we use a fixed size for */ 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; /* keep this block over resets */ /* freelist this context could be put in, or -1 if not a candidate: */ int freeListIndex; /* index in context_freelists[], or -1 */ } AllocSetContext; typedef AllocSetContext *AllocSet;
AllocSetContext是内存上下文的核心的控制结构,我们在代码中经常看到的内存上下文TopMemoryContext的定义为:
MemoryContext TopMemoryContext = NULL;
可以看到这个内存上下文的类型是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 */ /* CDB: Lifetime cumulative stats for this context and all descendants */ …… } MemoryContextData; typedef struct MemoryContextData *MemoryContext;
那么,MemoryContextData和AllocSetContext是什么样的关系呢?请看下图左半部分。
AllocSetContext结构的第一个指针用于指向MemoryContextData,也就是说TopMemoryContext实际上是一个AllocSetContext结构,但是使用时通常将类型转换为MemoryContextData,实际上这也是PG中最常用的技巧之一,在代码中可以看到这样的写法:
AllocSet set = (AllocSet) context;
由于AllocSetContext结构中的首部存放着MemoryContextData结构成员,所以这种转换可以成功。这样的使用方法有些类似与类的继承:MemoryContextData代表父类,AllocSetContext在父类(头部的指针)的基础上增加了一些新的功能。实际上PG就是使用了这种机制实现了interface(MemoryContextData作为interface),而后面的实现可以有很多种(AllocSetContext是内存上下文的一种实现)。
言归正传,继续介绍MemoryContextData数据结构的功能:
MemoryContextData使内存上下文形成了一个二叉树的结构,这样的数据结构增加了内存上下文的易用性,即,在重置或删除内存上下文时,所有当前上下文的子节点也会被递归的删除或重置,避免错删或漏删上下文。methods中保存的全部为函数指针,在内存上下文创建时,这些指针会被赋予具体函数地址。
下面继续介绍AllocSetContext数据结构:
Chunk存在于Block以内,是Block分割后形成的一段空间,Chunk空间的头部为AllocChunkData结构体,后面跟着该Chunk的空间,实际上palloc返回的就是指向这段空间首地址的指针。Chunk有两种状态:
注意:两种状态的Chunk都存在于Block中,被回收只是改变Chunk的aset指针,形成链表保存在freelist中;在使用中的Chunk的aset指针指向所属的AllocSetContext。
typedef struct AllocChunkData { void *aset; Size size; } AllocChunkData;
在palloc时会发生两种情况:
Chunk的数据结构相对简单,空指针aset是一个复用的指针,当Chunk正在使用时,aset指向它属于的AllocSet结构,当Chunk被释放后,Chunk被freelist数组回收,aset作为实现链表的指针,用于形成Chunk的链式结构。
typedef struct AllocBlockData { AllocSet aset; /* aset that owns this block */ AllocBlock next; /* next block in aset's blocks list */ char *freeptr; /* start of free space in this block */ char *endptr; /* end of space in this block */ } AllocBlockData; typedef struct AllocBlockData *AllocBlock;
Block是内存上下文向操作系统申请的连续的一块内存空间,申请后将AllocBlockData结构置于空间的首部,其中freeptr和endptr用与指向当前Block中空闲空间的首地址和当前Block的尾地址,见第二章示意图的“连续内存段(内存块)”。aset指向控制结构AllocSetContext,next指针形成Block的链式结构。
AllocSetContext结构中的一个重要的数组freelist,这是一个定长数组:
#define ALLOCSET_NUM_FREELISTS 11 typedef struct AllocSetContext { …… AllocChunk freelist[ALLOCSET_NUM_FREELISTS]; /* free chunk lists */ …… } AllocSetContext;
这是一个存放Chunk指针的数组,数组中每一个元素都是一个Chunk指针,就像前面提到的,空闲Chunk会形成链表结构,而链表的头结点的指针就存放在这个数组中。从长度来看,这个数组可以保存11个Chunk的链表,每一个链表都保存着特定大小的Chunk:
上图描述的就是freelist数组的结构,数组下标0位置保存8字节的Chunk,下标1位置保存16字节的Chunk,以此类推,freelist中可以保存的最大的Chunk为8k字节。
相同大小的Chunk会串在同一个链表中,放在freelist中指定的位置,数组下标的计算按照公式: $$ \log(Size) - 3 $$ 例如,大小为512字节的Chunk被释放了,套用公式 log(512) - 3 = 5,那么这个Chunk就会维护到freelist[5]指向的链表中。(具体计算过程见AllocSetFreeIndex函数)。
log对数计算,GP使用了查表法:
#define LT16(n) n, n, n, n, n, n, n, n, n, n, n, n, n, n, n, n static const unsigned char LogTable256[256] = { 0, 1, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4, LT16(5), LT16(6), LT16(6), LT16(7), LT16(7), LT16(7), LT16(7), LT16(8), LT16(8), LT16(8), LT16(8), LT16(8), LT16(8), LT16(8), LT16(8) }; static inline int AllocSetFreeIndex(Size size) { int idx; unsigned int t, tsize; if (size > (1 << ALLOC_MINBITS)) { tsize = (size - 1) >> ALLOC_MINBITS; /* * At this point we need to obtain log2(tsize)+1, ie, the number of * not-all-zero bits at the right. We used to do this with a * shift-and-count loop, but this function is enough of a hotspot to * justify micro-optimization effort. The best approach seems to be * to use a lookup table. Note that this code assumes that * ALLOCSET_NUM_FREELISTS <= 17, since we only cope with two bytes of * the tsize value. */ t = tsize >> 8; idx = t ? LogTable256[t] + 8 : LogTable256[tsize]; Assert(idx < ALLOCSET_NUM_FREELISTS); } else idx = 0; return idx; }
MemoryContext AllocSetContextCreate(MemoryContext parent, const char *name, Size minContextSize, Size initBlockSize, Size maxBlockSize)
内存上下文创建需要传入几个参数:
让我们看几个数据库中最常见的上下文创建时的参数,结合具体值在说说创建时参数的作用:
申请内存的流程图:
需要重点关注的有几点:
这个函数会被MemoryContextStats递归调用,遍历内存上下文树的内个节点,并获取当前节点的信息。
GDB调试时这一个非常好用的函数,可以直接在log中打印内存上下文树,指令:
gdb > p MemoryContextStats(TopMemoryContext)