說起malloc,但凡對C/C++有點基礎的人在編寫代碼的時候都用過。我們調用malloc介面分配一段連續的記憶體空間,不使用時使用free可以釋放這段記憶體空間。這些我們都已經比較的熟悉了。但是你知道malloc背後的調用機制嗎。
C語言程式員都知道,malloc只是C語言庫標準提供的一個普通函數,我們實現的malloc和庫函數比起來效率要低很多,但是可以通過編寫一個簡單的malloc來體現C庫的精髓,我們實現的malloc和庫的實現原理上市一致的。
一,malloc的定義:
根據標準C的定義,malloc的函數原型是這樣的:
<span style="font-family:Microsoft YaHei;font-size:14px;">void* malloc(size_t size);</span>
函數要求如下:
malloc函數分配的記憶體大小至少為size參數所指定的位元組數。 malloc的傳回值是一個void類型的指標,我們必須強制轉化為我們需要的類型的指標。 多次調用malloc所分配的地址不能有重疊部分,除非某次molloc所分配的地址被free釋放掉了。 malloc應該儘快的完成記憶體額分配並且返回。 實現malloc的同時實現calloc和realloc和free。 如果是子啊Linux環境下,可以使用
<span style="font-size:14px;">man malloc</span>
查看malloc的具體定義。
二,Linux的記憶體管理 1.虛擬記憶體地址與實體記憶體地址的關係: 現代作業系統在處理記憶體位址時普遍的採用虛擬記憶體地址技術,什麼事虛擬記憶體技術呢。 這種技術使每個進程“彷彿獨享”一塊2N位元組的記憶體(N是機器的位元),例如在64位的作業系統下,每個進程的虛擬記憶體空間是264B。這種虛擬記憶體空間的作用是簡化程式的編寫並且方便作業系統對進程之間的隔離管理。 虛擬記憶體技術是由
MMU和頁表構成的,MMU是一種映射演算法,它從虛擬記憶體地址映射到實體記憶體地址上,單位是頁 2.什麼是頁表。 在現代作業系統中,不管是虛擬記憶體地址還是實體記憶體地址,
都是以頁尾單位管理的,而不是大家以為的位元組。(一個記憶體頁是一段固定的地址,典型的記憶體頁的大小是4K)。所以記憶體位址可以分為頁號和頁內位移量 。
三,Linux進程級的記憶體管理 首先,我們可以瞭解一下一個進程的核心空間:
可以看到一個進程地址空間的主要成分為:
本文:這是整個使用者空間的最低地址部分,存放的是指令(也就是程式所編譯成的可執行機器碼)
初始化資料區段:這裡存放的是初始化過的全域變數
未初始化資料區段:這裡存放的是未初始化的全域變數
Heap:堆,這是我們本文重點關注的地方,堆自低地址向高地址增長,後面要講到的brk相關的系統調用就是從這裡分配記憶體
Stack:這是棧地區,自高地址向低地址增長
命令列參數和環境變數:使用者調用的最底層。
我們都知道,在malloc分配空間時是在Heap上分配的,實質上, Linux維護一個break指標,這個指標指向堆空間的某個地址。從堆起始地址到break之間的地址空間為映射好的,可以供進程訪問;而從break往上,是未映射的地址空間,如果訪問這段空間則程式會報錯。
由上文知道,要增加一個進程實際的可用堆大小,就需要將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 24void *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,其中一些圖片和代碼直接引用了文中的內容。。