1. 程式人生 > >PostgreSQL中的記憶體管理

PostgreSQL中的記憶體管理

最近參與一個跟postgresql相關的開發,因專案需要有對工程中記憶體洩漏的問題做過一些調查,研究了一下postgresql記憶體的管理機制,覺得這篇博文寫的很好,轉來做個分享微笑

本文將介紹PostgreSQL中獨特的記憶體管理,它一般根據分配塊的大小,決定如何分配,如果相對較大的塊,進行直接分配(呼叫malloc),如果相對較小的塊,則是可能在已經分配的空間裡面取出(不需要呼叫malloc)。而且在記憶體釋放的時候,小的記憶體塊不用釋放,只有大的記憶體塊才需要呼叫free操作。如此情況下,記憶體的分配與釋放會比較快。

接下來進行詳細的分析,首先給出它的資料結構。

typedef struct AllocSetContext
{
   MemoryContextDataheader;   暫時不用管該域
   
   AllocBlockData*blocks;           
   AllocChunkData*freelist[ALLOCSET_NUM_FREELISTS];       (ALLOCSET_NUM_FREELISTS==11)
   bool       isReset;       
   
   Size       initBlockSize;   
   Size       maxBlockSize;   
   Size       nextBlockSize;   
   Size       allocChunkLimit;   
   AllocBlock   keeper;           
} AllocSetContext;

其中最重要的兩個部分是blocks連結串列頭和freelist陣列,其中freelist陣列是空閒塊連結串列陣列,同一個空閒塊連結串列中塊大小相同,不同空閒塊連結串列大小不相同,從8Byte(索引為0)開始,每次往後移動一個位置,大小變成原來的兩倍,所以最大為8K。(上述的值是系統中定義的,可以更改)

在詳細分析之前,先看看AllocBlock和AllocChuck相關的資料結構

typedef struct AllocBlockData
{
   AllocSetContext*aset;           
   AllocBlockData*next;           
   char      *freeptr;       
   char      *endptr;           
} AllocBlockData;

可以看出,在每個BlockData(大記憶體塊的頭部)中有指向空閒位置的指標,該記憶體塊尾部的指標,指向下一個記憶體塊Block的指標,對於AllocSetContext中的blocks連結串列中,只有第一個塊是含有空閒塊的,其他的塊的空閒空間移到了freelist中去。(說明:為了區分,在下面的說明中,AllocBlockData稱為大塊,AllocChunkData稱為小塊,大塊裡面包含一個或者數個小塊)

typedef struct AllocChunkData
{
   
   void      *aset;
   
   Size       size;
} AllocChunkData;

對於第一個域有兩個用途,如果是空閒的,未分配出去的塊,它在freelist陣列中對應大小的塊鏈上,此時aset相當於大塊中的next。如果是被分配出去的,則指向了擁有該記憶體塊的Context。所有的分配的記憶體塊(小塊),都是從大塊中分配得到的,所以小塊前面都會有這個頭部,它會在free時會被用到,因為對於free來說,只有一個指向要被刪除塊的指標,而通過塊的起始指標找到塊的頭部,相當於傳遞了擁有該記憶體塊的Context,便可以針對該context進行free操作。

下面給出分配ContextAlloc流程:

輸入:分配塊的大小

輸出:對應的記憶體塊

image

這裡的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
{
   NodeTag       type;          
   MemoryContextMethods methods;
   MemoryContextData*parent;    
   MemoryContextData *firstchild;
   MemoryContextData *nextchild; 
   char         *name;          
} MemoryContextData, *MemoryContext;

這個資料結構本質上可以看成是抽象超類,methods對應的是個C++類似的虛擬函式表,特定Context,MemoryContextData的內容是在頭部的,設想一個記憶體塊,是特定的記憶體塊,第一部分的資料就是MemoryContextData,後面的內容是特定Context補充的。

typedef struct MemoryContextMethodsData
{
   Pointer    (*alloc) (MemoryContext c, Size size);
   void       (*free_p) (Pointer chunk);
   Pointer    (*realloc) (Pointer chunk, Size newsize);
   void       (*reset) (MemoryContext c);
   void       (*delete) (MemoryContext c);
} MemoryContextMethodsData, *MemoryContextMethods

可以看到,上篇文中介紹的AllocSet實現了MemoryContextMethodsData對應的所有函式。

在實現中,需要注意的是刪除和重置操作,它需要遞迴地對它所有的後代處理。具體的過程,不再作具體分析。感興趣的您可以讀一下相應的這部分程式碼。它在postgresql-8.4.2\src\backend\utils\mmgr\mcxt.c中