1. 程式人生 > >malloc記憶體分配過程詳解

malloc記憶體分配過程詳解

由上文知道,要增加一個程序實際的可用堆大小,就需要將break指標向高地址移動。Linux通過brk和sbrk系統呼叫操作break指標。兩個系統呼叫的原型如下:

int brk(void *addr);
void *sbrk(intptr_t increment);


brk將break指標直接設定為某個地址,而sbrk將break從當前位置移動increment所指定的增量。brk在執行成功時返回0,否則返回-1並設定errno為ENOMEM;sbrk成功時返回break移動之前所指向的地址,否則返回(void *)-1。 

 一個小技巧是,如果將increment設定為0,則可以獲得當前break的地址。  

另外需要注意的是,由於Linux是按頁進行記憶體對映的,所以如果break被設定為沒有按頁大小對齊,則系統實際上會在最後對映一個完整的頁,從而實際已對映的記憶體空間比break指向的地方要大一些。但是使用break之後的地址是很危險的(儘管也許break之後確實有一小塊可用記憶體地址)。

有了上面的知識,我們可以實現一個最簡單的malloc(沒什麼用,像個玩具)

/* 一個玩具malloc */
#include <sys/types.h>
#include <unistd.h>
void *malloc(size_t size)
{
    void *p;
    p = sbrk(0);
    if (sbrk(size) == (void *)-1)
        return NULL;
    return p;
}
這個malloc由於對所分配的記憶體缺乏記錄,不便於記憶體釋放,所以無法用於真實場景。

四,開始實現正式的malloc

一個方案是將堆記憶體空間以塊(Block)的形式組織起來,每個塊由meta區和資料區組成,meta區記錄資料塊的元資訊(資料區大小、空閒標誌位、指標等等),資料區是真實分配的記憶體區域,並且資料區的第一個位元組地址即為malloc返回的地址。

以下是一個塊的結構:

typedef struct s_block *t_block;
struct s_block {
    size_t size;  /* 資料區大小 */
    t_block next; /* 指向下個塊的指標 */
    int free;     /* 是否是空閒塊 */
    int padding;  /* 填充4位元組,保證meta塊長度為8的倍數 */
    char data[1]  /* 這是一個虛擬欄位,表示資料塊的第一個位元組,長度不應計入meta */
};

現在考慮如何在block鏈中查詢合適的block。一般來說有兩種查詢演算法:

  • First fit:從頭開始,使用第一個資料區大小大於要求size的塊所謂此次分配的塊

  • Best fit:從頭開始,遍歷所有塊,使用資料區大小大於size且差值最小的塊作為此次分配的塊

兩種方法各有千秋,best fit具有較高的記憶體使用率(payload較高),而first fit具有更好的執行效率。這裡我們採用first fit演算法。

* First fit */
t_block find_block(t_block *last, size_t size) {
    t_block b = first_block;
    while(b && !(b->free && b->size >= size)) {
        *last = b;
        b = b->next;
    }
    return b;
}
ind_block從frist_block開始,查詢第一個符合要求的block並返回block起始地址,如果找不到這返回NULL。這裡在遍歷時會更新一個叫last的指標,這個指標始終指向當前遍歷的block。這是為了如果找不到合適的block而開闢新block使用的。

如果現有block都不能滿足size的要求,則需要在連結串列最後開闢一個新的block。這裡關鍵是如何只使用sbrk建立一個struct:

#define BLOCK_SIZE 24 /* 由於存在虛擬的data欄位,sizeof不能正確計算meta長度,這裡手工設定 */
 
t_block extend_heap(t_block last, size_t s) {
    t_block b;
    b = sbrk(0);
    if(sbrk(BLOCK_SIZE + s) == (void *)-1)
        return NULL;
    b->size = s;
    b->next = NULL;
    if(last)
        last->next = b;
    b->free = 0;
    return b;
}
First fit有一個比較致命的缺點,就是可能會讓很小的size佔據很大的一塊block,此時,為了提高payload,應該在剩餘資料區足夠大的情況下,將其分裂為一個新的block:
void split_block(t_block b, size_t s) {
    t_block new;
    new = b->data + s;
    new->size = b->size - s - BLOCK_SIZE ;
    new->next = b->next;
    new->free = 1;
    b->size = s;
    b->next = new;
}

有了上面的程式碼,我們可以利用它們整合成一個簡單但初步可用的malloc。注意首先我們要定義個block連結串列的頭first_block,初始化為NULL;另外,我們需要剩餘空間至少有BLOCK_SIZE + 8才執行分裂操作。  

由於我們希望malloc分配的資料區是按8位元組對齊,所以在size不為8的倍數時,我們需要將size調整為大於size的最小的8的倍數:


size_t align8(size_t s) {
    if(s & 0x7 == 0)
        return s;
    return ((s >> 3) + 1) << 3;
}
#define BLOCK_SIZE 24
void *first_block=NULL;

void *malloc(size_t size) {
    t_block b, last;
    size_t s;
    /* 對齊地址 */
    s = align8(size);
    if(first_block) {
        /* 查詢合適的block */
        last = first_block;
        b = find_block(&last, s);
        if(b) {<pre name="code" class="cpp">         /* 如果可以,則分裂 */
            if ((b->size - s) >= ( BLOCK_SIZE + 8))
                split_block(b, s);
            b->free = 0;
        } else {
            /* 沒有合適的block,開闢一個新的 */
            b = extend_heap(last, s);
            if(!b)
                return NULL;
        }
    } else {
        b = extend_heap(NULL, s);
        if(!b)
            return NULL;
        first_block = b;
    }
    return b->data;
}

這篇文章大量參考了A malloc Tutorial,其中一些圖片和程式碼直接引用了文中的內容!!