鑒於上次領導告訴一個解決方案,讓我把它寫成文檔,結果自己腦子裡知道如何操作和解決,但就是不知道如何用語言文字把它給描述出來。決定以後多寫一些筆記和微博來鍛煉自己的文字功底和培養邏輯思維,不然只會是一個敲代碼的,永遠到不了管理的層面。
把《C程序設計語言》細讀了一遍後,到第8章UNIX系統接口的最後兩節——“目錄列表”和“存儲分配程序”,看了一遍都沒看懂。智商不過高啊。把存儲分配重新看了一遍,才有了眉頭。這兩天還要找時間把目錄列表再看一遍,確保掌握。(前幾章節有些地方看了很多遍也糊裡糊塗,就找谷歌百度幫忙了)
這裡的存儲分配程序,講的就是標准庫中malloc函數的實現原理。首先要了解針對malloc的內存存儲結構。malloc不像全局變量一樣,不是在編譯器編譯的時候就會分配內存空間,而是在調用到malloc函數時才會分配空間。有時還會中途調用free函數釋放空間出來。所以:
1、malloc在第一次被調用時,從系統中獲取最小為一個單元的空閒空間(eg:最小單元為1024個最受限單元塊。當x<=1024,獲取1024個,否則獲取x個),再進行分配;
2、malloc所剩下的空閒空間一般都不是連續的,而是分散的。這樣也提高了空間的利用率。
為了管理malloc的空閒空間,每一個獨立塊的最前面都包含了一個“頭部”信息:一個指向下一個空閒塊的指針、一個本身獨立塊的長度(書上說還有一個指向自身存儲空間的指針,但每個存儲空間都有自身的指針,為什麼還要這個呢。後看英語版原著,這麼寫的:Each block contains a size, a pointer to nextblock, and the space itself.)。下一個空閒塊是按存儲地址升序排列,離本空閒塊最近的一個空閒塊,若本空閒塊在最後,則指向最前的空閒塊。這樣所有屬於malloc的空閒空間都被串在了一起。如下圖所示:
注:中文版的此圖解釋翻譯搞錯了,如下圖:
因為後面的free函數已經把相鄰的空閒鏈表給整合成一塊了,所以我的圖沒有出現相鄰的空閒鏈表。
每塊由malloc控制的空間都包含一個“頭部”信息,為了方便管理,每塊的空間大小都是頭部大小的整數倍。而頭部長度=指向下一個空閒塊的指針的長度+自身空間大小的unsigned長度。但為了確保由malloc函數返回的存儲空間滿足將要保存的對象的對齊要求。每個機器都有一個最受限的類型:如果最受限的類型可以存儲在某一個特定的地址中,則其他所有的類型也可以存放在此地址中。有的是double型,有的是long型,甚至還有的是int型。因此,頭部結構將與最受限的類型進行聯合,來確保對齊。
因為有頭部信息,頭部信息裡的本塊空間size也是包括頭部的大小,所以每次申請malloc空閒塊的時候,都要加上一個單元,最後返回給用戶的時候,再去掉頭部。
每次調用malloc申請空間時,malloc有一個專門指向當前空閒塊鏈表的靜態指針freep。從當前開始掃描剩下的空閒塊鏈表,直到掃到一個足夠大的空閒塊。此算法成為“首次適應”(first fit),與之相對的是“最佳適應”(best fit):它將掃描出滿足條件最小的塊。這裡的代碼是“首次適應”算法。結果將出現三種情況:
1)找到一塊剛好合適的空閒塊,則此塊空間從鏈表中移走並將此塊的地址返回給用戶,並把靜態指針freep指向前一空閒塊地址;
2)找到一塊比需求大的空閒塊,則從此空閒塊中的後部取一塊與需求一樣的空間給用戶,前部改變空閒塊大小便可;
注(直到寫本博文才發現自己錯了):
①一直都認為返回給用戶地址前一單元的頭部,應該把空間退還給前面的空閒塊,不然就閒著了。其實,裡面記錄了一個重要信息:空間塊大小(包括頭部和返回給用戶的單元大小),在free釋放空間的時候,就必須用到此頭部的信息;
②後面的free程序以為是專供系統申請空間後插入空閒塊鏈表用的,其實它就是我們平常用malloc、realloc、或calloc申請空間後,再釋放的程序。
3)如果掃描了一遍,都沒有找到足夠大的空閒塊,則向系統再申請一塊新的空間。
上面都是在malloc已經有了空閒塊的前提下,但第一次申請的時候,malloc是沒有空閒塊空間的。因此,在預編譯時,就建立了一個單元的空閒塊鏈表base來當做空閒鏈表的入口。當第一次調用malloc時,空閒鏈表的靜態指針freep為NULL,那將它指向base,大小設為0(這樣這塊base空間將一直存在,且不被申請,確保了之後freep一直指向有效的空閒塊鏈表),且指向它自己,同時向系統申請空閒空間(每次向系統申請的空間都是一塊連續的空閒塊)。
typedef long Align; /*按照long類型的邊界對齊,即以long作為最受限類型*/ union header{ /*頭部信息*/ struct { union header *ptr; /*指向下一個空閒塊*/ unsigned size; /*本空閒塊大小*/ }s; Align x; /*強制對齊*/ }; typedef union header Header; static Header base; /*第一次調用malloc的空閒塊鏈表入口,大小為0的空鏈表(按照上面邏輯的來說,這裡的size應該為1)*/ static Header *freep = NULL; /*靜態的空閒塊鏈表指針,初始化為NULL。第一次申請後才會指向base*/ /*malloc函數:通用存儲分配函數*/ void *malloc(unsigned nbytes) { Header *p,*prevp; /*定義一個當前空閒塊指針變量,和前一個空閒塊指針變量*/ Header *morecore(unsigned); /*用於向系統申請空閒空間函數*/ unsigned nunits; /*需要申請的實際單元大小,即上面圖中的z*/ nunits = (nbytes + sizeof(Header) - 1)/sizeof(Header) + 1; /*與上圖對應,把字節大小轉換為單元大小,向上取整,並加上一個單元(頭部)*/ if((prevp = freep) == NULL){ /*沒有空閒鏈表,第一次申請*/ base.s.ptr = prevp = freep = &base; /*freep指向base,base的下一個空閒塊指針指向自己*/ base.s.size =0; /*設置大小為0*/ } for(p = prevp->s.ptr;;prevp = p, p = p->s.ptr){ if(p->s.size >= nunits){ if(p->s.size == nunits) /*大小剛好合適*/ prevp->s.ptr = p->s.ptr; /*移走此塊空閒區域*/ else{ /*比實際需求大,從空閒塊尾部分配*/ p->s.size -= nunits; /*縮小空閒塊大小*/ p += p->s.size; /*指針指向被申請的空間的頭部*/ p->s.size = nunits; /*設置被申請的空閒塊大小*/ } freep =prevp; /*當前靜態指針指向前一空閒塊,如果當前塊還有空閒區域,下次將繼續從此處開始掃描,節省時間*/ return (void *)(p+1); /*返回去頭部單元的空閒空間*/ } if(p == freep) /*閉環的空閒鏈表,第一次調用malloc申請,或掃描一遍,未發現足夠大的空間*/ if((p = morecore(nunits)) == NULL) /*向系統申請空間*/ return NULL; /*未申請成功,*/ } }
通過下面的morecore()和free()函數的程序分析可知,在向系統成功申請空間後,p將指向有足夠空間的空閒塊。但在此代碼中,進入下一此空閒塊掃描前,p將指向下一塊不足的空閒塊,導致多掃描了一遍。個人覺得,如果空間足夠,可以多申請一個靜態指針beforefreep,指向freep的前一個空閒塊。這樣上面代碼可添加一句,提高效率:
if(p == freep){ /*這樣要添加大括號*/ if((p = morecore(nunits)) == NULL) return NULL; p = beforefreep; } 或 if(p == freep) /*這裡可以不用加大括號,else與最近的if匹配*/ if((p = morecore(nunits)) == NULL) return NULL; else p = beforefreep;