malloc做爲標準c的一個內存分配調用想必每個搞過C語言的都用過,然而在這個很經常使用的統一接口下面卻有着N種不一樣的實現,linux的glibc有本身的實現,windows的crt有本身的實現,這些實現都有着本身的策略,特別是glibc的實現讓人看的頭暈,crt的實現雖然簡單可是有着策略感受很傻,最原始並且最能說明本質的實現我認爲仍是貝爾實驗室的實現,很簡單,先後不超過60行代碼,讓人讀後心曠神怡,連同free的實現一同構成了一幅美麗的圖景,本質上貝爾實驗室的malloc使用了一個線性的鏈表來表示能夠分配的內存塊,熟悉夥伴系統和slab的應該知道這是這麼回事,可是仍然和slab和夥伴系統有所不一樣,slab中分配的都是相同大小的內存塊,而夥伴系統中分配的是不一樣肯定大小的內存塊,好比肯定的1,2,4,8...的內存,貝爾試驗室的malloc版本是一種隨意的分配,自己遵循碎片最少化,利用率最大化的原則分配,其實想一想slab的初衷,其實就是一個池的概念,省去了分配時的開銷,而夥伴系統的提出就是爲了消除碎片,貝爾malloc版本一箭雙鵰,雖然如今不少人已經遺忘了這個版本或者你能夠說glibc或者crt的實現已經超越了這個版本,可是這些實現或多或少的給蛇添了足,沒有貝爾malloc實現的那麼純粹。如今首先看一下基本的數據結構:linux
typedef struct mem{ windows
struct mem *next; 數據結構
Unsigned len; 併發
}mem; ide
這個數據結構表示一切的內存塊slot,每個slot表明一塊內存,其中的len表示其長度,這個結構體其實就是一個頭的概念,真正的數據附着在這個結構體的後面相鄰的地方,這個結構體設計的巧妙之處在於它可讓malloc和free更簡單的實現,正式因爲它的第一個字段是一個mem*類型的,妙在哪裏呢?只有看了malloc才知道:函數
mem *F; //一個全局的mem*鏈表,F的含義就是Free,它指向當前空閒鏈表的頭,該鏈表是一個線性鏈表佈局
void *malloc(unsigned size)操作系統
{ 設計
register mem *p, *q, *r, *s; 指針
unsigned register k, m;
extern char *sbrk(int);
char *top, *top1;
size = (size+7) & ~7; //字節對齊
r = (mem *) &F; //取鏈表頭的地址,該頭自己就是一個指針類型,所以r其實是指針的指針,正是因爲mem的第一個字段是一個mem*類型,它才能夠轉化爲mem*類型的,F取地址後就是F的地址,存儲的是F,而F是一個men指針,所以&F地址處的數據就是一個mem*類型,將之轉化爲mem*以後,其數據整好是mem的next字段,注意此時它的len字段無效,這個轉化的意義就在於r的next就是F
for (p = F, q = 0; p; r = p, p = p->next) //遍歷空閒鏈表,直到F的前驅爲NULL
{
if ((k = p->len) >= size && ( !q || m > k)) //&以前的確保當前的空閒塊知足分配要求,以後的確保找到大於size的空閒塊的最小的那一塊
{
m = k; //m記錄當前找到的知足要求的最小塊
q = p; //q記錄當前知足要求的那一塊
s = r; //s記錄當前知足要求的前一塊
}
}
if (q) //若是找了知足要求的塊,要麼直接分配,要麼分割後分配而後重將分配後沒有使用的那一塊加入空閒鏈表
{
if (q->len - size >= MINBLK) //知足分割要求,也就是最小粒度要求
{
p = (mem *)(((char *)(q+1)) + size); //設置q的大小,這一塊將從空閒鏈表移除,返回使用
p->next = q->next; //將分割後的新的空閒塊的next設置爲分割前的q的next
p->len = q->len - size - sizeof(mem); //分割後的p的len顯然減小了size和mem的加和的大小
s->next = p; //將知足要求的前一塊的next指向q的後一塊
q->len = size; //將q分配出去,也就是從空閒鏈表移除
}
else //若是不能分割,那麼直接將這一塊q分配出去
s->next = q->next;
}
else //若是沒有找到合適的空閒塊能夠分配,那麼就要向操做系統要了
{
top = (char *)(((long)sbrk(0) + 7) & ~7); //找到當前的堆的頂部
if (F && (char *)(F+1) + F->len == top) //若是有空閒塊而且空閒塊就是堆頂的那一塊
{
q = F; //將空閒塊首指針賦給q
F = F->next; //向後推動空閒塊
}
else //不然將堆頂賦給q
q = (mem *) top;
top1 = (char *)(q+1) + size; //找到新的堆頂,可是要預先分配SBGULP
if (sbrk((int)(top1-top+SBGULP)) == (Char *) -1)
return 0;
r = (mem *)top1; //r記錄新的將要分配的slot的next
r->len = SBGULP - sizeof(mem);
r->next = F; //將原來的空閒鏈表賦給新的slot的next的next
F = r; //新的slot的下一個slot賦給新的空閒鏈表
q->len = size;
}
return (char *)(q+1); //返回q,q就是分配的內存
}
以上就是malloc的操做,頗有條理,而且很清晰,關鍵就是指針的使用,看了這個實現就會發現原來指針還能這麼使用,若是mem結構體的第一個字段不是mem類型的指針,那麼上述的malloc實現根本就不可能有這麼簡單。注意「下一個元素」有兩個概念,一個是邏輯上的概念,用next字段獲得,另外一個是物理上的概念,用qn = (char *)f + q->len這種方式獲得,用next指針獲得的下一個元素物理上不必定相鄰,全部的用next連在一塊兒的元素都是空閒元素,而用偏移獲得的下一個元素是物理上相鄰的內存塊,也就是虛擬內存相鄰的內存塊,用此方式獲得的內存塊不必定是空閒塊。分配也就是上面這個malloc函數,很簡單的一個函數,釋放其實也是頗有意思的,其實就是free函數:
void free(char *f)
{
mem *p, *q, *r;
char *pn, *qn;
if (!f)
return;
q = (mem *) ((char *)f - sizeof(mem)); //獲得要釋放的f所在的mem頭的位置,雖然q已經從空閒鏈表摘除,可是它仍是本質存在的
qn = (char *)f + q->len; //在線性鏈表中獲得q的下一個元素,不必定是空閒元素,由於它是靠內存位置來遊歷的
for (p = F, r = (mem *) &F; ; r = p, p = p->next)
{
if (qn == (Char *) p) //若是q的下一個元素就是p的話,那麼吞併掉它,注意p必定是空閒元素,由於它是靠next指針來遊歷的
{
q->len += p->len + sizeof(mem); //更新q的len字段,這就是吞併p的行爲
p = p->next; //因爲p被將要釋放的q吞併,致使p進入q內部而不復存在,其自己也就成爲了一個將要被釋放的內存塊的一部分
}
pn = p ? ((char *) (p+1)) + p->len : 0; //獲得p的下一個元素,不必定空閒
if (pn == (char *) q) //若是p的後面相鄰元素就是將要釋放的q,那麼q就和p合併做爲一個更大的空閒塊存在
{
p->len += sizeof(mem) + q->len; //合併p和q
q->len = 0; //丟掉q
q->next = p; //迴環,實際上已經丟棄了q
r->next = p; //這一個頗有意思,下面會專門說
break;
}
if (pn < (char *) q) //若是不相鄰而且p後面相鄰的元素地址比q小的話就將q連接進空閒鏈表
{
r->next = q; //更新連接指針
q->next = p;
break;
}
}
}
到此爲止,全部的分配和釋放就說完了,是否是很簡單呢?上面的合併操做的目的和夥伴系統的同樣,只不過夥伴系統合併的是大小固定的內存塊,而這裏的合併是隻要相鄰有合併的可能就合併而無論內存塊的大小。注意上述的代碼沒有考慮併發和須要鎖的狀況,可是這就是最純真,也是最本質的東西,不是嗎?在這種簡單而又能夠說明本質以後,我會寫兩篇關於malloc的改進版本,分別是微軟的和glibc的版本。
附:關於指針的一個問題
前面說過,若是不是mem結構設計得如此巧妙,那麼AT&T的malloc不會這麼簡單,最重要的就是能夠經過&F而後將之轉化爲mem*類型,這樣這個指針就是F的前一個元素,若是不這麼設計mem結構,那麼可能除了next指針以外還須要一個prev指針了。注意,設F爲mem指針,&F並非一個真正的mem指針,而是因爲mem的第一個字段爲一個mem指針,而&F在內存中應該是一個mem指針的指針,可是該指針的指針不論如何也是一個指針類型,其指向的數據正好也是一個指針,後者是mem指針類型,這正好符合mem結構體的佈局,mem結構體的第一個字段就是一個mem指針類型,所以咱們能夠將&F理解成F的前一個元素,由於&F的第一個字段是F,這僅僅是能夠這麼理解,若是不將next做爲第一個字段,那麼就沒有這樣的事,而且任何改變&F的next字段的行爲都會改變F自己,除了&F是F的前一個元素這件事成立以外,它們還有別的千絲萬縷的聯繫,這就是指針的偉大,同時也可能帶來更多的困惑。