linux heap堆分配


heap堆分配在用戶層面:malloc函數用於heap內存分配

void* malloc(size_t size);            

進程的虛擬內存地址布局:

 

對用戶來說,主要關注的空間是User Space。將User Space放大后,可以看到里面主要分為如下幾段:

  • Code:這是整個用戶空間的最低地址部分,存放的是指令(也就是程序所編譯成的可執行機器碼)
  • Data:這里存放的是初始化過的全局變量
  • BSS:這里存放的是未初始化的全局變量
  • Heap:堆,這是我們本文重點關注的地方,堆自低地址向高地址增長,后面要講到的brk相關的系統調用就是從這里分配內存
  • Mapping Area:這里是與mmap系統調用相關的區域。大多數實際的malloc實現會考慮通過mmap分配較大塊的內存區域,本文不討論這種情況。這個區域自高地址向低地址增長
  • Stack:這是棧區域,自高地址向低地址增長

 

heap內存從低地址向高地址生長:malloc函數主要是用於虛擬內存線性地址的分配

Linux進程堆管理

另外需要注意的是,由於Linux是按頁進行內存映射的,所以如果break被設置為沒有按頁大小對齊,則系統實際上會在最后映射一個完整的頁,從而實際已映射的內存空間比break指向的地方要大一些。但是使用break之后的地址是很危險的(盡管也許break之后確實有一小塊可用內存地址)

進程所面對的虛擬內存地址空間,只有按頁映射到物理內存地址,才能真正使用。受物理存儲容量限制,整個堆虛擬內存空間不可能全部映射到實際的物理內存。Linux維護一個break指針,這個指針指向堆空間的某個地址(線性地址空間)。從堆起始地址到break之間的地址空間為映射好的,可以供進程訪問;而從break往上,是未映射的地址空間,如果訪問這段空間則程序會報錯,即是經典的segmentation fault。

 

從操作系統角度來看,進程分配內存有兩種方式,分別由兩個系統調用完成:brk()和mmap()(不考慮共享內存)。

1、brk是將數據段(.data)的最高地址指針_edata往高地址推;

2、mmap是在進程的虛擬地址空間中(堆和棧中間,稱為文件映射區域的地方)找一塊空閑的虛擬內存。

  這兩種方式分配的都是虛擬內存,沒有分配物理內存(不准確,系統調用會執行內核函數,分配內存),在第一次訪問已分配的虛擬地址空間的時候,發生缺頁中斷,操作系統負責分配物理內存,然后建立虛擬內存和物理內存之間的映射關系。

 

這兩種進程分配內存方式的區別:

1、對於大塊內存申請,glibc直接使用mmap系統調用為其划分出另一塊虛擬地址,供進程單獨使用;在該塊內存釋放時,使用unmmap系統調用將這塊內存釋放(虛擬和物理內存都釋放),這個過程中間不會產生內存碎塊等問題。

2、針對小塊內存的申請,在程序啟動之后,進程會獲得一個heap底端的地址,進程每次進行內存申請時,glibc會將堆頂向上增長來擴展內存空間,也就是我們所說的堆地址向上增長。在對這些小塊內存進行操作時,便會產生內存碎塊的問題。實際上brk和sbrk系統調用,就是調整heap頂地址指針(break指針)。

(注意這里所說的內存碎片還是根據物理內存所說的)

由brk分配的heap堆內存是什么時候釋放呢?

當glibc發現堆頂有連續的128k的空間是空閑的時候,它就會通過brk或sbrk系統調用,來調整heap頂的位置,將占用的內存返回給系統。這時,內核會通過刪除相應的線性區,來釋放占用的物理內存。

下面我要講一個內存空洞的問題:

一個場景,堆頂有一塊正在使用的內存,而下面有很大的連續內存已經被釋放掉了,那么這塊內存是否能夠被釋放?其對應的物理內存是否能夠被釋放?

很遺憾,不能。

這也就是說,只要堆頂的部分申請內存還在占用,我在下面釋放的內存再多,都不會被返回到系統中,仍然占用着物理內存。為什么會這樣呢?

根源:這主要是與內核在處理堆的時候,過於簡單,它只能通過調整堆頂指針的方式來調整調整程序占用的線性區;而又只能通過調整線性區的方式,來釋放內存。所以只要堆頂不減小,占用的內存就不會釋放。

 

A和D之間的B已經通過free(B),但是此時C的物理內存和線性內存都沒有被釋放,只是被標記為已經釋放的空間,但是break指針沒有移動,edata==break?沒有回溯。在大多數malloc實現中,free函數釋放的內存並不直接歸還操作系統(也就是釋放物理內存),而是掛接到freelist數組中。  B對應的虛擬內存和物理內存都沒有釋放,因為只有一個_edata指針,如果往回推,那么D這塊內存怎么辦呢

當然,B這塊內存,是可以重用的,如果這個時候再來一個40K的請求(與之前B的大小相同),那么malloc很可能就把B這塊內存返回回去了。 

所以如果下次有新的虛擬內存地址分配:首先會查看freelist數組中有沒有用過的但是被free的合適空間,如果有,就返還這個線性地址空間。如果沒有就從break指針位置開始分配

 

綜上:虛擬線性地址空間也有可能產生碎片(這里所說的碎片就是由於free的內存的虛擬空間沒有釋放,導致下次分配虛擬空間時候,不能被使用),線性空間和物理內存是一起釋放的

內存碎片和內存空洞都是一個意思

問題:既然堆內內存brk和sbrk不能直接釋放,為什么不全部使用 mmap 來分配,munmap直接釋放呢? 
        既 然堆內碎片不能直接釋放,導致疑似“內存泄露”問題,為什么 malloc 不全部使用 mmap 來實現呢(mmap分配的內存可以會通過 munmap 進行 free ,實現真正釋放)?而是僅僅對於大於 128k 的大塊內存才使用 mmap ? 

其實,進程向 OS 申請和釋放地址空間的接口 sbrk/mmap/munmap 都是系統調用,頻繁調用系統調用都比較消耗系統資源的。並且, mmap 申請的內存被 munmap 后,重新申請會產生更多的缺頁中斷。例如使用 mmap 分配 1M 空間,第一次調用產生了大量缺頁中斷 (1M/4K 次 ) ,當munmap 后再次分配 1M 空間,會再次產生大量缺頁中斷。缺頁中斷是內核行為,會導致內核態CPU消耗較大。另外,如果使用 mmap 分配小內存,會導致地址空間的分片更多,內核的管理負擔更大。同時堆是一個連續空間,並且堆內碎片由於沒有歸還 OS ,如果可重用碎片,再次訪問該內存很可能不需產生任何系統調用和缺頁中斷,這將大大降低 CPU 的消耗。 因此, glibc 的 malloc 實現中,充分考慮了 sbrk 和 mmap 行為上的差異及優缺點,默認分配大塊內存 (128k) 才使用 mmap 獲得地址空間,也可通過 mallopt(M_MMAP_THRESHOLD, <SIZE>) 來修改這個臨界值。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM