操作系統 — 淺析基於glibc的malloc

淺析基於glibc的malloc





Linux的虛擬內存管理有幾個關鍵概念:

- 每個進程都有獨立的虛擬地址空間,進程訪問的虛擬地址並不是真正的物理地址.

- 虛擬地址可通過每個進程上的頁表與物理地址進行映射,獲得真正的物理地址.

- 如果虛擬地址對應物理地址不在物理內存中,則產生缺頁中斷,真正分配物理地址,同時更新進程的頁表; 

  如果此時物理內存已經耗盡了,則根據內存替換算法淘汰部分頁面至物理磁盤當中.


Linux使用虛擬地址,大大增加了進程的尋址空間,由低地址到高地址分別爲:

1.只讀段:該部分空間只能讀,不可寫(包括代碼段,rodata段(C常量字符串和#define定義的常量))

2.數據段:保存全局變量,靜態變量的空間.

3.堆:就是平時所說的動態內存,malloc/new大部分都來源於此,其中堆定的位置可通過函數brk和sbrk進行動態調整.

4.文件映射區域:如動態庫,共享內存等映射物理空間的內存,一般是mmap函數所分配的虛擬地址空間.

5.棧:用於維護函數調用的上下文空間,一般爲8M,可通過ulimit -s查看.

6.內核虛擬空間:用戶代碼不可見的內存區域,由內核管理(頁表就存放在內核虛擬空間).

32位系統由4G的地址空間:

其中0x08048000~0xbffffff是用戶空間,0xc000000~0xffffffff是內核空間,包括內核代碼和數據,與進程相關的數據結構

(如頁表,內核棧)等. 另外%esp執行棧頂,往低地址方向變化; brk/sbrk函數控制堆頂_edata往高地址方向變化.

malloc和free由誰提供?

一般來說,他們是C Standard Library提供的而不是由操作系統的內核實現. 例如微軟的是msvcrt,Linux下是glibc. 這兩個

是主流的.今天我們着重瞭解Linux/glibc.

malloc的基本原理

linux採用的是glibc中堆內存管理ptmalloc實現,虛擬內存的佈局規定了malloc申請位置以及大小,malloc一次性能申請小內存(小於128

Kb),分配在堆區(heap),用sbrk()進行對齊生長,而malloc一次性能申請大內存(大於128kb)分配到的是在共享映射區,而不是在堆區,

採用的mmap()系統調用進行映射. malloc的實現與物理內存是無關的,內核爲每個進程維護一張頁表,頁表存儲進程空間內每頁的虛擬

,頁表項中有的虛擬內存頁對應着某個物理內存頁面,也有的虛擬內存頁沒有實際的物理頁面對應. 無論malloc通過sbrk還是mmap實

現,分配到的內存只是虛擬內存,而且只是虛擬內存的頁號,代表這塊空間進程可以用,實際上還沒有分配到實際的物理頁面.等你的進

訪問到這個新分配的內存空間的時候,如果其還沒有分配到實際的物理頁面,就會產生缺頁中斷,內核這個時候會給進程分配實際的

頁面,以與這個未被映射的虛擬頁面對應起來.

ptmalloc中的chunk




glibc中的malloc實現是基於鏈表的思想,但遠遠不止上面那麼簡單. glibc中是用ptmalloc來管理內存的,不管內存在哪裏分配,用什麼

辦法分配用戶請求分配的空間在ptmalloc中都是用一個chunk來表示.用戶調用free()之後釋放的內存也不會立即返回給操作系統,

他們會被表示成一個chunk,ptmalloc是用特定的數據結構來管理這些chunk,一個正在使用的chunk結構大概如下:


chunk指針指向一個chunk的開始,一個chunk中包含了用戶請求的內存區域和相關的控制信息. 圖中的mem指針纔是真正返回給用戶的內存

指針.chunk的第二個區域的最低一位爲p,它表示前一個塊是否在使用中,P爲0則表示前一個chunk爲空閒,這時chunk的第一個域

prev_size纔有效,prev_size表示前一個chunk的size,程序可以使用這個值來找到前一個chunk的開始地址,當p爲1時,表示前一個

chunk正在使用中,prev_size無效,程序也就不可以得到前一個chunk的大小. 不能對前一個chunk進行任何操作. ptmalloc分配的第一個

總是將P設爲1,以防止程序引用到不存在的區域.

Chunk的第二個域的倒數第二個位爲M,他表示當前chunk是從哪個內存區域獲得的虛擬內存. M爲1表示該chunk是從mmap映射區域分配的,

否則是從heap區域分配的. 

Chunk地第二個區域的倒數第三個位爲A,表示該chunk屬於主分配區還是非主分配區. 前者置爲1否則置爲0.

空閒chunk在內存中的結構如圖所示



當chunk空閒時,其M狀態不存在,只有AP狀態,原本是用戶數據區的地方存儲了四個指針,指針fd指向後一個空閒的chunk.而bk指向前一

個空閒的chunk,ptmalloc通過這兩個指針將大小相近的chunk連成一個雙向鏈表.  對於large bin中的空閒chunk,還有兩個指針,

fd_nextsize和bk_nextsize. 這兩個指針用於加快在large bin中查找最近匹配的空閒chunk,不同的chunk鏈表又是通過bins或者

fastbins來組織的.(上面說的相當詳細)

2.chunk中的空間複用

爲了使得chunk所佔用的空間最小,ptmalloc使用了空間複用,一個chunk正在被使用或者已經被free掉,所以chunk中的一些域可以在使

狀態和空閒狀態表示不同的意義,來達到空間複用的效果. 以32位系統爲例,空閒的時候,一個chunk中至少需要4個size_t大小的空

,用來存儲prev_size,size,fd和bk,也就是16kb. chunk的大小要對齊到8kb,當一個chunk處於使用狀態時,它的下一個chunk的

prev_size域肯定是無效的,所以實際上,這個空間可以被當前chunk使用,這聽起來有點不可思議,但確實是合理空間複用的例子. 所以

實際上,一個使用中的chunk的大小的計算公式應該是: in_use_size = (用戶請求大小+8-4) 這裏加8是因爲需要存儲prev_size和size

,但又因爲向下一個chunk"借"了4B,所以要減去4. 最後,因爲空閒的chunk和使用中的chunk使用的是同一塊空間,所以肯定要取其中

最大者作爲實際的分配空間. 既最終的分配空間chunk_size = max(in_use_size,16). 這就是當用戶請求內存分配時,ptmalloc實際

需要分配的內存大小.


主分配區 非主分配區:


每個進程只有一個主分配區,但可能存在多個非主分配區,ptmalloc根據系統對分配去的爭用情況動態增加非主分配區的數量,分配區

的數量一旦增加,就不會在減少了. 主分配區可以訪問進程的heap區域和mmap映射區域,也就是說主分配可以使用sbrk和mmap向操作系

統申請虛擬空間. 而非主分配區只能訪問檢查的mmap映射區域,非主分配區每次使用mmap()向操作系統"批發"HEAP_MAX_SIZE(32位默

認1MB,64位系統默認64MB)大小的虛擬內存,當用戶向非主分配區請求分配內存時再切割成小塊"零售出去",畢竟系統調用是相對低

的,直接從用戶空間分配內存快多了. 所以ptmalloc在必要的情況下才會調用mmap()函數向操作系統申請虛擬內存.

主分配區可以訪問heap區域,如果用戶不調用brk()和sbrk()函數,分配程序就可以保證分配到連續的虛擬地址空間,因爲每個進程只

有一個主分配區使用sbrk()分配heap區域的虛擬地址. 內核對brk實習可以看成mmap的一個精簡版本,相對高效益一點. 如果主分配

區的內存是通過mmap()向系統分配的,當free該內存時,主分配區會直接調用munmap()將該內存歸還給系統.、

當某一個線程需要調用malloc()分配內存空間時,該線程先查看線程私有變量是否已經存在一個分配區,如果存在,嘗試對該分配區加

鎖,如果加鎖成功,使用該分配區分配內存,如果失敗,該線程搜索循環鏈表試圖獲得一個沒有加鎖的分配區. 如果所有分配區都已經加

鎖,那麼malloc()會開闢一個新的分配區,把該分配區加入到全局分配區循環鏈表並加鎖,然後使用該分配前進行分配內存操作,在釋

放過程中,線程同樣試圖獲得待釋放內存所在分區的鎖,如果該分配區正在被別的線程使用,則需要等待直到其他線程釋放該分配區的

互斥鎖之後纔可以進行釋放操作.

申請小塊內存時會產生很多內存碎片,ptmalloc在整理是也需要對分配區加鎖操作,每個加速大概需要5~10個cpu指令,而且程序線程

很多的情況下,鎖等待時間就會延長,導致malloc性能下降. 一次加鎖操作需要消耗100ms左右,正是鎖的緣故,導致ptmalloc在多線

程競爭情況下遠遠落後於tcmalloc.


空閒chunk的容器:


用戶free掉的內存並不是都會馬上歸還給系統,ptmalloc會統一管理heap和mmap映射區域中的空閒的chunk,當用戶進行下一次分配請求

時,ptmalloc會首先試圖在空閒的chunk挑選一塊給用戶,這樣就避免了頻繁的系統調用,降低了內存分配的開銷, ptmalloc將相似大小

的chunk用雙向鏈表維護起來,這樣一個鏈表被稱爲一個bin. ptmalloc一共維護128個bin,並使用一個數組來存儲這些bin.


數組中的第一個爲unsorted bin,數組中從2開始編號前64個bin稱爲small bins,同一個small bin中的chunk具有相同的大小. 兩個

相鄰的small bin中的chunk大小相差爲8bytes. small bins中的chunk按照最近使用的順序進行排列,最後釋放的chunk被鏈接到鏈表

的頭部,而申請chunk是從鏈表尾部開始,這樣,每一個chunk都有相同的機會被ptmalloc選中. small bins後面的bin被稱爲large bins

large bins中的麼一個bin分別包含了一個給定範圍內的chunk,其中的chunk按大小序列排序. 相同大小的chunk同樣按照最近使用順序

排列.ptmalloc使用 "smallest-first,best-fit"原則在空閒large bins中查找合適的chunk.

當空閒的chunk被鏈接到bin中的時候,ptmalloc會把表示該chunk是否處於使用中的標誌P設置爲0(注意,這個標誌位實際上處於下一個

chunk當中),同時ptmalloc還會檢查它前後的chunk是否也是空閒的,如果是的話,ptmalloc會首先把他們合併成爲一個大的chunk.

然後將合併後的chunk放到unsorted bin當中. 要注意的是,並不是所有的chunk被釋放後就立即被放到bin當中. ptmalloc爲了提高分配

的速度,會把一些小的chunk先放到一個叫做fast bins的容器內.


2.Fast Bins


一般的情況是,程序在運行時會經常需要申請和釋放一些較小的內存空間. 當分配器合併了相鄰的幾個較小的內存空間. 當分配器合併了

相鄰的幾個小的chunk之後,也許馬上就會有另一個小塊內存的請求,這樣分配器又需要從大的空閒內存中切分出一塊,這樣無疑是低效

的. 故而 ptmalloc中在分配過程中引入了fast bins,不大於max_fast(默認爲64kb)的chunk被釋放後,首先會被放到fast bins中,

fast bins中的chunk並不會改變他的使用標誌P,這樣也就無法將他們合併,當需要給用戶分配的chunk小於或等於max_fast時,

ptmalloc首先會在fast bins中查找相應的空閒塊,然後纔會去查找bins中的空閒chunk. 某個特定的時候,ptmalloc會遍歷fast bin中的

chunk將相鄰的空閒chunk進行合併,並將合併後的chunk加入unsorted bin當中,然後再將usorted bin裏的chunk 加入到bins中.


3.Unsorted Bin


Unsorted bin的隊列使用bins數組的第一個,如果被用戶釋放的chunk大於max_fast,或者fast bins中的空閒chunk合併後,這些chunk首先

會被放到unsorted bin隊列中,在進行malloc操作時,如果在fast bins中沒有找到合適的chunk,則ptmalloc會先在unsorted bin中查找

合適的空閒空間 chunk,然後才查找bins. 如果unsorted bin不能滿足分配要求. malloc便會將unsorted bin中的chunk加入bins中,然

後再從bins中繼續進行查找和分配過程,從這個過程可以看出來,unsorted bin可以看做是bins的一個緩衝區,增加它只是爲了加快分配

的速度.


4.Top chunk


並不是所有的chunk都按照上面的方式來組織,實際上,有三種例外情況. Top chunk,mmaped chunk和last remainder. 下面會分別介紹

這三類

特殊的chunk。 top chunk對於主分配區和非分配區是不一樣的.

對於非主分配區會預先從mmap區域分配一塊較大的空閒內存模擬sub-heap. 通過管理sub-heap來向應用於的需求,因爲內存是按地址從

到高進行分配的,在空閒內存的最高處,必然存在一塊空閒的chunk,叫做top chunk. 當bins和fast bins都不能滿足分配需要的時候,

ptmalloc會設法在top chunk分配一塊內存給用戶,如果top chunk上分配所需的內存以滿足分配的需要時. 實際上,top chunk在分配時

是在fast bins和bins之後被考慮,所以,不論top chunk有多大,它都不會被放到fast bins或者bins中. top chunk的大小是隨着分配

和回收不停變換的. 如果從top chunk分配內存會導致top chunk減小,如果回收的chunk恰好與top chunk相鄰,那麼這兩個chunk就會

合併成新的top cchunk,從而使top chunk變大. 如果在free時回收的內存大於某個閾值,並且top chunk的大小也超過了收縮閾值.

ptmalloc會收縮sub-heap. 如果top-chunk包含了整個sub-heap,ptmalloc會調用munmap把整個sub-heap的內存返回給操作系統.


由於主分配區是唯一能夠映射進程heap區域的分配區,它可以通過sbrk()來增大或是收縮進程heap的大小. ptmalloc在開始時會預先分配

一塊較大的空閒內存(heap).主分配區的top chunk在第一次調用malloc時會分配一塊(chunk_size + 128kb)align 4kb大小的空間作爲初

的heap,用戶從top chunk分配內存時,可以直接取出一塊內存給用戶. 在回收內存時,回收的內存恰好與top chunk 相鄰則合併

top chunk,當該次回收的空閒內存大小達到某個閾值,並且top chunk的大小也超過了收縮閾值,會執行內存收縮,減小top chunk大

小,但至少要保留一個頁大小的空閒內存,從而把內存歸還給操作系統. 如果向主分配區的top chunk申請內存,而top chunk中沒有空

,ptmalloc會調用sbrk()將的進程heap的邊界brk上移,然後修改top chunk的大小.


5.mmaped chunk


當需要分配的chunk足夠大,而且fast bins和bins都不能滿足要求,甚至top chunk本身也不能滿足分配需求時,ptmalloc會直接使用

mmap來直接使用內存映射來將頁映射到進程空間. 這樣分配的chunk在被free時將直接解除映射,於是就將內存歸還給操作系統了,再

次對這樣的內存區的引用將導致segmentation fault錯誤. 這樣的chunk也不會包含任何bin中.


6.Last remainder


Last remainder是另外一種特殊的chunk,就像top chunk和mmaped chunk一樣,不會在任何bins中找到這種chunk,當需要分配一個

small chunk,但在small bins中找不到合適的chunk,如果Last remainder chunk的大小大於所需的small chunk大小,

last remainder chunk被分裂成兩個chunk,其中一個chunk返回給用戶,另一個chunk變成新的last remainder chunk.


ptmalloc的響應用戶內存分配要求的具體步驟:


1).獲取分配區的鎖,爲了防止多個線程同時訪問同一分配區,在進行分配之前需要取得分配區的鎖. 線程先查看線程私有實例中是否已

經存在一個分配區,如果存在嘗試對該分配區加鎖. 如果加鎖成功,使用該分配區分配內存,否則,該線程搜索分配區循環鏈表試圖獲

得一個空閒的分配區. 如果所有的分配區都已經加鎖了,那麼ptmalloc會開闢一個新的分配區,把該分配區加入到全局分配區循環鏈表

線程私有實例中並加鎖,然後使用該分配區進行分配操作. 開闢出來的新分配區一定爲非主分配區,因爲主分配區是從父進程哪裏繼承

來的,開闢非主分配會調用mmap()創建一個sub-heap,並設置好top chunk.

2).將用戶的請求大小轉換爲實際需要分配的chunk空間大小.

3).判斷所需分配chunk的大小是否滿足chunk_size <= max_fast(max_fast 默認爲 64kb)

   如果是的話,則轉下一步,否則跳到第5步.

4).首先嚐試在fast bins中取一個所需大小的chunk分配給用戶,如果可以找到,則分配結束.

    否則跳到下一步.

5).判斷所需大小是否處於small bins中,既判斷chunk_size < 512kb是否成立,如果chunk大小

    處於small bins中 進行下一步,否則轉到第7步.

6).根據所需分配的chunk的大小,找到具體所在的某個small bin,從該bin的尾部摘取一個恰好滿足大小的small bins中

,若成功,則分配結束. 否則,轉到下一步.

7).到了這一步,說明需要分配的是一塊大的內存,或者small bins中找不到合適的chunk. 於是,ptmalloc首先會遍歷fast bins中的

chunk,將相鄰chunk進行合併,並鏈接到unsorted bin中,然後遍歷unsorted bin中的chunk,如果unsorted bin只有一個chunk,並且

這個chunk在上次分配時被使用過,並且所需分配的chunk大小屬於small bins,並且chunk的大小等於需要分配的大小,這種情況就

接將該chunk進行分割分配結束,否則將根據chunk的空間大小將其放入small bins或是large bins中,遍歷完成後,進入下一步.

8).到了這一步,說明需要分配的是一塊大的內存,或者small bins和unsorted bin中都找不到合適的chunk,並且fast bins和

unsorted bin中所有chunk都清除乾淨了. 從large bins中按照"smallest-first,best-fit"原則,找一個合適的chunk,從中劃分一塊

所需大小的chunk,並將剩下的部分鏈接回到bins中,若操作成功,則分配結束,否則轉到下一步.

9).如果搜索fast bins和bins都沒有找到合適的chunk,那麼就需要操作top chunk來進行分配了,判斷top chunk大小是否滿足所需的

大小,如果是則從top chunk中分出一塊來,否則轉到下一步.

10).到了這一步,說明top chunk也不能滿足分配要求,所以,於是就有兩個選擇:如果是主分配區,調用sbrk(),增加top chunk大小. 

如果是非分配區,調用mmap來分配一個新的sub-heap,增加top chunk大小; 或者使用mmap()來直接分配,在這裏需要依靠chunk的大

小決定到底使用那種方法,判斷所需分配的chunk大小是否大於等於mmap分配閾值,如果是的話,則轉到下一步,調用mmap分配,否則

跳到第12步,增加top chunk大小.

11).使用mmap系統調用用爲程序的內存空間映射一塊chunk_size align 4kb大小的空間,然後將內存指針返回給用戶.

12).判斷是否爲第一次調用malloc,若是主分配區,則需要進行一次初始化工作,分配一塊大小爲(chunk_size + 128kb)align 4kb大小

的空間作爲初始的heap. 若已經初始化過了,主分配區則調用sbrk()調用heap空間,豬糞分配區則在top chunk中切割出一個chunk,使

之滿足分配需求,並將內存指針返回給用戶.

總結一下:根據用戶請求分配的內存的大小,ptmalloc有可能會在兩個地方爲用戶分配內存空間. 在第一次分配內存時,一般情況下只存

在一個主分配區,但也有可能從父進程哪裏繼承了多個非主分配區,在這裏主要討論主分配區的情況,brk值等於start_brk,所以實際上

heap大小爲0,top chunk大小也是0,top chunk大小也是0,這時,如果不增加heap大小,就不能滿足任何分配要求,所以,若用戶的請

的內存大小小於mmap分配閾值,則ptmalloc會初始化heap。 然後在heap中分配空間給用戶,以後的分配就基於這個heap進行. 若是第

次用戶的請求大於mmap分配閾值,則ptmalloc直接使用mmap()分配一塊內存給用戶,而heap也就沒有被初始化,直到用戶第一次請求小於

mmap分配閥值的內存分配. 第一次以後的分配就比較複雜了,簡單來說,ptmalloc首先會查找fast bins,如果不能找到匹配的chunk,則

查找small bins,若還是不行,把unsorted bin中的chunk全部加入large bins中查找,並查找large bins. 在fast bins和small bins

中查找都需要精確匹配,而在large bins中查找時,則遵循"smallest-first,best-fit"的原則,不需要精確匹配. 若以上的方法都失

,則ptmalloc會考慮使用top chunk,若top chunk也不能滿足分配要求. 而且所需chunk大於mmap分配閾值,則使用mmap進行分配,否

則增加heap,增大top chunk以滿足分配要求.

內存回收概述:

free()函數接受一個指向分配區域的指針作爲參數,釋放該指針所指向的chunk.而具體的釋放方法則看該chunk所處的位置和該chunk的大

小,free()函數的工作步驟如下:

1).free()函數同樣首先需要獲取分配區的鎖,來保證線程安全.

2).判斷傳入的指針是否爲0,如果爲0,則什麼都不做,直接return,否則轉下一步.

3).判斷所需釋放的chunk是否爲mmaped chunk,如果是,則調用munmap()釋放mmaped chunk,解除內存空間映射,該空間不再有效. 如果

開啓閾值設定位mmap分配閾值的2倍,釋放完成,否則跳到下一步.

4).判斷chunk的大小和所處的位置,若chunk_size <= max_fast,並且chunk並不位於heap的頂部,也就是說並不與top chunk相鄰,則轉

到下一步,否則跳到第六步.(因爲與top chunk相鄰的小chunk會和 top chunk進行合併,所以這裏不僅需要判斷大小,還需要判斷相鄰情況)

5).將chunk放到fast bins中,chunk 放入到fast bins中時,並不修改該chunk使用狀態位p,也不會和相鄰的chunk進行合併,知識放進

去,如此而已,這一步做完之後釋放便結束了,程序從free()函數中返回.

6).判斷前一個chunk是否處於使用中,如果前一個塊也是空閒塊,則合併. 並轉下一步.

7).判斷前一個chunk的下一個塊是否爲top chunk,如果是,則轉到第9步,否則下一步.

8).判斷下一個chunk是否處於使用中,如果下一個chunk也是空閒的,則合併,並將合併後的chunk放到unsorted bin中. 注意,這裏在合

並的構成中,要更新chunk的大小,以反映合併後的chunk的大小,並轉到第10步.

9).如果執行到這一步,說明釋放了一個與top chunk相鄰的chunk,則無論它有多大,都將它與top chunk合併,並更新top chunk的大小

信息,轉下一步

10).判斷top chunk的大小是否大於FASTBIN_CONSOLIDATION_THRESHOLD(默認64kb),如果是的話,則會觸發進行fast bins的合併操作,

fast bins中的chunk將被遍歷,並與相鄰的空間chunk進行合併,合併後的chunk會被放到unsorted bin中,fast bins將爲空,操作完

成之後轉下一步.

11).判斷top chunk的大小是否大於mmap收縮閾值,如果是的話,對於主分配區,則會試圖歸還top chunk中的一部分給操作系統. 但是最

先分配的128kb是不會歸還的,ptmalloc會一直管理這部分內存,用於響應用戶的分配請求:如果爲非主分配區,會進行sub-heap

收縮,將top chunk的一部分返回給操作系統. 如果top chunk爲整個sub-heap,會把整個sub-heap還回給操作系統,做完這一步之後,

釋放結束,從free()函數退出,可以看出,收縮堆的條件是當前free的chunk大小加上前後能合併chunk的大小大於64kb,並且

top chunk的大小要達到mmap收縮閾值,纔有可能收縮堆.

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章