mmgr是postgresql的記憶體管理模組,其代碼分布在aset.c, mctx.c和portalmem.c這三個檔案之中。這裡主要分析它的堆記憶體的管理機制,也就是aset.c檔案中的內容。
AllocSetContext是堆記憶體管理的主要結構,其定義如下:
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;
其中的成員可以分為三個部分,我用三種不同的顏色標記了出來:
1) 黃色部分是AllocSetContext的頭部,如果用物件導向的觀點來看,可以說AllocSetContext是繼承自MemoryContextData的,這裡我們不關心MemoryContextData的定義,它提供了對AllocSetContext更高層次的管理。
2)粉色部分是一些配置參數。
3) 紫色部分是我們這篇部落格的主角。blocks和freelist對應於堆記憶體管理的兩個方面。下面詳細描述。
AllocSetContext作為malloc/free與更上層應用的中介層,向下需要管理使用malloc從作業系統申請到的記憶體,對上則需要提供記憶體的擷取與釋放的介面。我們先說第一個方面,也就是它是如何管理malloc來的記憶體的。
AllocSetContext將從作業系統中申請到的記憶體放在一下鏈表中進行管理,這個鏈表就是紫色部分的blocks。blocks中的每一項的結構叫做AllocBlockData,它的定義如下:
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
AllocBlockData是向作業系統申請和釋放記憶體的基本單位,為了簡便起見,下面就直接叫它Block了。在接收到記憶體配置請求的時候,AllocSetContext如果發現blocks頭部的那個Block沒有足夠的空間,就再從作業系統中申請一個更大的Block作為blocks的頭。以後的分配出的記憶體就是從這個更大的Block上割出來的。這個更大是多大呢,pg將它設為前一個的2倍,也就是說blocks這個鏈表從尾部到頭部,每向前一個Block的大小就會增長一倍。這裡說的是通常情況,也會有非通常情況的,比如這個Block是不可能無限制地增大的,它有一個最大的可以分配的大小AllocSetContext::maxBlockSize,也還有來自其它方面的限制,要瞭解這個限制就得先說一說 AllocSetContext的另外一個主要功能了--對外提供擷取記憶體和釋放記憶體的介面。
AllocSetContext提供給外部使用的主要介面為:
?
static void *AllocSetAlloc(MemoryContext context, Size size); static void AllocSetFree(MemoryContext context, void *pointer); static void *AllocSetRealloc(MemoryContext context, void *pointer, Size size); |
你可以調用AllocSetAlloc(cxt,sizeof(XX)) 來獲得一塊可用的特定大小的記憶體,但是如果你天真地以為你需要的是15個位元組的空間,它就會精確地給你15個位元組,你就錯了,實際上,為了你的這次內在請求,它給了你16個位元組的空間,外加一個資訊頭。也就是說它給你返回的記憶體有統一的結構,大小也有特定地限制。
這個統一的記憶體結構的定義如下:
?
typedef struct AllocChunkData { /* aset is the owning aset if allocated, or the freelist link if free */ void *aset; /* size is always the size of the usable space in the chunk */ Size size; #ifdef MEMORY_CONTEXT_CHECKING /* when debugging memory usage, also store actual requested size */ /* this is zero in a free chunk */ Size requested_size; #endif } AllocChunkData; |
AllocChunkData是返回記憶體的基本單位,下面叫它Chunk。如果你申請到一個大小為16位元組的記憶體,你實際拿到是像的這麼個東西,返回給你的地址前面是有一個Chunk頭的:
為了加快分配的速度和方便管理,每個Chunk裡的可用空間都是2的整數次冪。同時還維護了一個叫做freelist的數組,這個數組的每一項都是一個Chunk的隊列,每個隊列內的Chunk大小都是一樣的。下面是某時刻free list的記憶體布局圖:
這個free list的Chunk的來源有兩個方面:
1) 調用AllocSetFree回收來的Chunk;
2) 調用AllocSetAlloc時,如果發現blocks鏈表的第一個Block沒有足夠的空間,這時候會分配一個更大的Block,但是在做這樣事之前,首先要將頭上那個Block中還剩下的空間酌情格式化為一些Chunk加入到free list這中。
由於這個free list數組的大小是固定的,所以在free list之中最大的Chunk的可用空間也是有限的,如果申請比最大的Chunk所能提供的空間還要大的記憶體時,就會被認為這是在申請一塊大記憶體,會直接從作業系統中malloc出來給你,這種記憶體對這個堆管理器來說只是個過客,free時是不會加入到free list中的,也就是說這種不會被堆管理器直接重利用。
好了, 堆記憶體的管理的基本結構資料結構就介紹這麼多,是時候看看幾個主要方法的實現了,這裡只說一下幾個最常用 的介面: AllocSetAlloc , AllocSetFree, AllocSetRealloc
下面這個圖解釋了AllocSetAlloc的流程:
1) 首先判斷是不是在申請大記憶體?如果是轉入2),否則轉入3);
2) 直接malloc出大小為: size 加上Block和Chunk頭的大小,調整好指標位置,返回;
3) 計算出實際分配的記憶體大小,這個大小是比size大的最小的2^k;
4) free list中是否有這種大小的Chunk? 若有轉入5),否則轉入6);
5) 從free list中取出一個滿足條件的Chunk, 返回;
6) blocks鏈表的第一個Block是不是足夠的空間,若有轉入9),否則繼續
7) 將第一個Block中剩餘的空間格式化為一些Chunk,加入到free list中;
8) malloc出一個更大的Block,加入到blocks中,
9) 從Block中格式化一個滿足條件的Chunk,返回。
AllocSetFree的操作比較的簡單,如,其步驟為:
1) 根據傳入的指標算出Chunk頭的位置 ( p - sizeof(AllocChunkData));
2) 根據Chunk中的size資訊判斷是不是大記憶體,若不是,轉入4),否則繼續;
3) 直接free掉(是個過客,不再利用)返回。
4) 將Chunk入到合適的free list之中。
AllocSetRealloc的步驟如下:
1) 調整指標p的位置得到Chunk頭的地址;
2) 如果再分配的大小比Chunk的要小,直接返回;
3) 如果是大記憶體,則直接調用realloc分配一個更大的Block,並更新blocks中對應的項(所有的Block在使用期間都被blocks管理著), 然後返回.
4) 否則則先調用 AllocSetFree,再調用 AllocSetAlloc.
這個堆管理器的最主要的功能和實現就敘述完了,最後看一下它提供的更進階的一些調試功能:
1) CLOBBER_FREED_MEMORY 野指標對任何一個C/C++程式員來說都是豺狼虎豹,如果定義了這個宏,在面對野指標的時候你會從容不少,因為這個宏啟用了一項功能:AllocSetFree的記憶體都會被置為0x7f, 這樣在你第一次使用野指標的時候就會立馬發現它。
2) MEMORY_CONTEXT_CHECKING 由於在大多數情況下,mmgr返回的記憶體大小大於申請大小的最小的2次冪。所以記憶體或多或少總是有浪費的。但這也是提高記憶體配置所帶來必然的損失,為了心理平衡一下,我們可以利用這多出來的記憶體,做其他的事,比如這個宏就會啟用這樣一項功能:在實際大小後面填充一0x7e,這樣就可以檢測是否有記憶體越界的事情發生了。