什麼是堆linux
光有棧對於面向過程的程序設計還遠遠不夠,由於棧上的數據在函數返回的時候就會被釋放掉,因此沒法將數據傳遞至函數外部。而全局變量沒有辦法動態地產生,只能在編譯的時候定義,有不少狀況下缺少表現力。在這種狀況下,堆是惟一的選擇。程序員
堆是一塊巨大的內存空間,經常佔據着整個虛擬空間的絕大部分。在這片空間裏,程序能夠請求一塊連續內存,並自由地使用,這塊內存在程序主動放棄以前都會一直保持有效。下面是申請空間最簡單的例子。 算法
int main() { char *p = (char*)malloc(1000); free (p)' }
上面的程序用malloc申請了1000個字節的空間後,程序能夠自由地使用這1000個字節,直到程序用free函數釋放它。數組
進程的內存管理並無交給操做系統內核管理,這樣作性能較差,由於每次程序申請或者釋放對空間都要進行系統調用。咱們知道系統調用的性能開銷是很大的,當程序對堆的操做比較頻繁時,這樣作的結果是會嚴重影響程序性能的。比較好的作法就是程序向操做系統申請一塊適當大小的堆空間,而後由程序本身管理這塊空間,而具體來說,管理着堆空間分配每每是程序的運行庫。函數
運行庫至關於向操做系統批發了一塊較大的堆空間,而後「零售」給程序用。當所有「售完」或程序有大量的內存需求時,在根據實際需求向操做系統「進貨」。固然運行庫在向零售堆空間時,必須管理它批發來的堆空間,不能把同一塊地址出售兩次,致使地址的衝突。咱們首先來了解運行庫是怎麼向操做系統批發內存的。咱們以linux爲例。性能
Linux進程堆管理ui
進程地址空間中,除了可執行文件、共享庫和棧以外,剩餘的未分配的空間均可以被用來做爲堆空間。Linux下的進程管理稍微有些複雜,由於它提供了兩種堆分配方式,即兩個系統調用:一個是brk()系統調用,另一個是mmap()。brk()的C語言形式聲明以下:spa
int brk(void* end_data_segment)
brk()的做用實際上就是設置進程數據段的結束地址,即它能夠擴大或者縮小數據段(Linux下數據段和BSS合併在一塊兒統稱爲數據段)。若是咱們將數據段的結束地址向高地址移動,那麼擴大的那部分空間就能夠被咱們使用,把這塊空間拿來做爲堆空間是最多見的作法之一。Giblic中還有一個函數叫作sbrk,它的功能與brk相似,只不過參數和返回值略有不一樣。sbrk以一個增量做爲參數,即須要增長(負數爲減小)的空間大小,返回值是增長(或減小)後數據段結束地址,這個函數其實是對brk系統調用的包裝,它經過brk()實現的。操作系統
mmap()的做用和Windows系統下的VirtualAlloc很類似,它的做用就是向操做系統申請一段虛擬地址空間,固然這塊虛擬地址空間能夠映射到某個文件(這也是系統調用的最初的做用),當它不將地址空間映射到某個文件時,咱們又稱這塊空間爲匿名空間,匿名空間就能夠拿來作堆空間。它的聲明以下:設計
void *mmap{void *start, size_t length, int prot, int flags, int fd,off_t offset);
mmap的前兩個參數分別用於指定須要申請的空間的起始地址和長度,若是起始地址設置爲0,那麼linux系統會自動挑選合適的起始地址。prot/flags這兩個參數用於設置申請的空間的權限(可讀,可寫,可執行)以及映像類型(文件映射、匿名空間等),最後兩個參數用於文件映射時指定文件描述符和文件偏移的,咱們在這裏並不關心它們。
glibc的malloc函數是這樣處理用戶空間請求的:對於小於128kb的請求來講,它會在現有的堆空間裏面,按照堆分配算法爲它分配一塊空間並返回;對於大於128KB的請求來講,它會使用mmap()函數爲它分配一塊匿名空間,而後再這個匿名空間中爲用戶分配空間。固然咱們直接使用mmap也能夠垂手可得地實現malloc函數:
void *malloc(size_t nbytes) { void *ret = mmap(0, nbytes, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, 0, 0); if (ret == MAP_FAILED) return 0; return ret; }
因爲mmap()函數與VirtualAlloc()相似,它們都是系統虛擬空間申請函數,它們申請的空間起始地址和大小都必須是系統頁的大小的整數倍。
堆空間管理
在動態分配內存後,那麼咱們就要來思考如何管理這塊大的內存。主要有三種方法,空閒鏈表和位圖法以及對象池。
空閒鏈表
空閒鏈表(Free List)的方法實際上就是把堆中各個空閒的塊按照鏈表的方式鏈接起來,當用戶請求一塊空間時,能夠遍歷整個鏈表,直到找到合適大小的塊而且將它拆分;當用戶釋放空間時將它合併到空閒鏈表中。
空閒鏈表是這樣一種結構,在堆裏的每個空閒空間的開頭(或結尾)有一個頭,頭結構裏記錄了上一個和下一個空閒塊的地址,也就是說,全部的空閒塊造成了一個鏈表。以下所示:
在這樣的結構下如何分配空間呢?首先在空閒鏈表查找足夠容納請求大小的一個空閒塊,而後將這個塊分爲兩部分,一部分爲程序請求的空間,另外一部分爲剩餘下來的空閒鏈表。下面將鏈表裏對應原來空閒塊的結構更新爲新的剩下的空閒塊,若是剩下的空閒塊大小爲0,則直接將這個結構從鏈表裏刪除。下圖演示了用戶請求一塊和空閒塊2剛好相等的內存空間後堆的狀態。
當按照地址順序在鏈表中存放進程和空閒區時,有幾種算法能夠用來爲建立的進程(從磁盤換入的已存在的內存)分配內存。當存儲管理器知道要爲進程分配多大的內存時,有以下幾種算法。
首次適配(first fit)算法
存儲管理器沿着段鏈表進行搜索,直到找到一個足夠大的空閒區,除非空閒區大小和要分配的空間大小正好同樣,不然將該空閒去分爲兩部分,一部分供進程使用,另外一部分造成新的空閒區。首次適配算法是一種速度很快的算法,由於它儘量少地搜索鏈表節點。
下次適配(next fit)算法
它的工做方式和首次適配算法不一樣,不一樣點是每次找到合適的區間都記錄當時的位置。以便在下次尋訪空閒區時從上次結束的地方開始搜索,而不是像首次適配算法那樣每次從頭開始。下次適配算法的性能略低於首次適配算法。
最佳適配(best fit)算法
最佳適配算法搜索整個鏈表,找出可以容納進程的最小的空閒區。最佳適配算法師徒找出最接近實際須要的空閒區,以最好地匹配請求和可用空閒區,而不是先拆分一個之後可能會用到的最大的空閒區。可是它的缺點是產生較多的業內碎片
最差適配(worst fit)算法
老是分配最大的可用空閒區。
快速適配(quick fit)算法
它爲那些經常使用大小的空閒區維護單獨的鏈表。例如,有一個n項的鏈表,該表的第一項指向大小爲4KB的空閒區鏈表表頭的指針,第二項是指向大小爲8KB的空閒區鏈表表頭的指針,第三項是指向大小爲12KB的空閒區鏈表表頭的指針,以此類推。像21KB這樣的空閒區便可以放在20KB的鏈表中也能夠放在一個專門存放大小比較特別的空閒區的鏈表中。
快速適配算法尋找一個指定大小的空閒區是十分快速的,但它和全部將空閒區按大小排序的方案同樣都有一個共同的缺點,即在一個進程終止或被換出時,尋找它的鄰塊,查看是否能夠合併的過程是很是耗時的。若是不進行合併,內存將會很快分裂出大量的進程沒法利用的小空閒區。
位圖
位圖的核心思想是將整個堆劃分爲大量的塊,每一個塊的大小相同。當用戶請求內存的時候,老是分配整數個塊的空間給用戶,第一個塊咱們稱之爲已分配區域的頭,其他的稱爲已分配區域的主體。而咱們可使用一個整數數組來記錄塊的使用狀況。因爲每一個塊只有頭/主體/空閒三種狀態,所以僅僅須要兩位便可表示一個塊,所以稱爲位圖。假設堆的大小爲1MB,那麼讓一個塊大小爲128字節,那麼總共就有1M/128=8k個塊,能夠用8k/(32/2)=512個int來存儲。這有512個int的數組就是一個位圖,其中每兩位表明一個塊。當用戶請求300字節的內存時,堆分配給用戶3個塊,並將相應的位圖的相應位置標記爲頭或軀體。
下面是一個實例:
這個堆分配了3片內存,分別有2/4/1個塊,用虛線標出。其對應的位圖將是:
(HIGH) 11 00 00 10 10 10 11 00 00 00 00 00 00 00 10 11 (LOW)
其中11表示H(頭),10表示主體(Body),00表示空閒(Free)。
對象池:
以上介紹的堆管理方法是最爲基本的兩種,實際上在一些場合,被分配對象的大小是較爲固定的幾個值,這時候咱們能夠針對這樣的特徵設計一個更爲高效的堆算法,稱爲對象池。
對象池的思路很簡單,若是每一次分配的空間大小都同樣,那麼就能夠按照這個每次請求分配的大小做爲一個單位,把整個堆空間劃分爲大量的小塊,每次請求的時候只須要找到一個小塊就能夠了。
對象池的管理方法能夠採用空閒鏈表,也能夠採用位圖,與它們的區別僅僅在於它假定了每次請求的都是一個固定的大小,所以實現起來比較容易。因爲每次老是隻請求一個單位的內存,所以請求獲得知足的速度很是快,無須查找一個足夠大的空間。
實際上不少現實應用中,堆的分配算法每每是採用多種算法複合而成。好比對於glibc來講,它對於小於64字節的空間申請時採用相似於對象池的方法;而對於大於512字節的空間申請採用的是最佳適配算法;對於大於64字節而小於512字節的,它會根據狀況採用上述方法中的折中策略;對於大於128KB的申請,它會使用mmap機制直接向操做系統申請空間。
參考資料:
1. 《程序員的自我修養》--連接、裝載與庫
2. 《現代操做系統》3.2.3
3. 《深刻理解計算機系統》