來源:微信公眾號:cpp軟件架構獅
要想回答這個問題,就得刨根問底,內存到底是怎樣分配的?
在內核態的角度來看,進程需要分配內存的方式有兩種:brk和mmap這兩個系統調用。
- brk是數據段的最高地址指針_edata往高地址增長;
- mmap是建立了頁到用戶進程的虛擬空間映射,在進程的虛擬地址空間中,堆和棧之間的大空間中找一塊空閑的地址。
這兩種方式分配到的都是虛擬內存,并還沒有分配真正的物理地址。會在第一次訪問的時候,內核判斷有沒有物理地址,如果沒有回發生缺頁中斷,然后分配物理地址,建立虛擬地址和物理地址之間的映射關系。
但在用戶態申請動態內存,是不會直接調用這兩個系統調用接口。一般情況下是通過C庫的malloc/free來申請和釋放。而C庫的實現也正是基于這兩個系統調用接口,進行上層的封裝和管理。
當內核發生缺頁中斷的時候,到底會做哪些事情呢?
首先進程會從用戶態陷入到內核態運行,會執行以下步驟:
- 檢查需要訪問的虛擬地址是否合法;
- 查找,分配物理頁;
- 填充物理頁的內容;
- 建立映射關系(虛擬地址->物理地址);
重新執行發生缺頁中斷的那條指令。
如果第3步,需要讀取磁盤,那么這次缺頁中斷就是majflt,否則就是minflt。
注意:
用ps -o majflt,minflt -C program命令查看。
majflt代表major fault,中文名叫主錯誤,minflt代表minor fault,中文名叫次錯誤。
這兩個數值表示一個進程自啟動以來所發生的缺頁中斷的次數。
下面我們用實例來解釋下內存申請的原理
情況一、malloc小于128k的內存,使用brk分配內存,將_edata往高地址推(只分配虛擬空間,不對應物理內存(因此沒有初始化),第一次讀/寫數據時,引起內核缺頁中斷,內核才分配對應的物理內存,然后虛擬地址空間建立映射關系),如下圖:
1、進程啟動的時候,其(虛擬)內存空間的初始布局如第一幅圖所示。其中,mmap內存映射文件是在堆和棧的中間(例如libc-2.2.93.so,其它數據文件等),為了簡單起見,省略了內存映射文件。_edata指針(glibc里面定義)指向數據段的最高地址。
2、進程調用A=malloc(30K)以后,內存空間如第二幅圖:malloc函數會調用brk系統調用,將_edata指針往高地址推30K,就完成虛擬內存分配。你可能會問:只要把_edata+30K就完成內存分配了?
事實是這樣的,_edata+30K只是完成虛擬地址的分配,A這塊內存現在還是沒有物理頁與之對應的,等到進程第一次讀寫A這塊內存的時候,發生缺頁中斷,這個時候,內核才分配A這塊內存對應的物理頁。也就是說,如果用malloc分配了A這塊內容,然后從來不訪問它,那么,A對應的物理頁是不會被分配的。
3、進程調用B=malloc(40K)以后,內存空間如第三幅圖。
情況二、malloc大于128k的內存,使用mmap分配內存,在堆和棧之間找一塊空閑內存分配(對應獨立內存,而且初始化為0),如下圖:
4、進程調用C=malloc(200K)以后,內存空間第一幅圖:默認情況下,malloc函數分配內存,如果請求內存大于128K(可由M_MMAP_THRESHOLD選項調節),那就不是去推_edata指針了,而是利用mmap系統調用,從堆和棧的中間分配一塊虛擬內存。
這樣子做主要是因為:brk分配的內存需要等到高地址內存釋放以后才能釋放(例如,在B釋放之前,A是不可能釋放的,這就是內存碎片產生的原因,什么時候緊縮看下面),而mmap分配的內存可以單獨釋放。
當然,還有其它的好處,也有壞處,再具體下去,有興趣的同學可以去看glibc里面malloc的代碼了。
5、進程調用D=malloc(100K)以后,內存空間如圖5;
6、進程調用free(C)以后,C對應的虛擬內存和物理內存一起釋放。
7、進程調用free(B)以后,如第一幅圖所示:B對應的虛擬內存和物理內存都沒有釋放,因為只有一個_edata指針,如果往回推,那么D這塊內存怎么辦呢?
當然,B這塊內存,是可以重用的,如果這個時候再來一個40K的請求,那么malloc很可能就把B這塊內存返回回去了。
8、進程調用free(D)以后,如第二幅圖所示:B和D連接起來,變成一塊140K的空閑內存。
9、默認情況下:當最高地址空間的空閑內存超過128K(可由M_TRIM_THRESHOLD選項調節)時,執行內存緊縮操作(trim)。在上一個步驟free的時候,發現最高地址空閑內存超過128K,于是內存緊縮,變成第三幅圖所示。
如果這個時候我們想要申請一塊50K的內存E,就會出現下圖的情況。
有沒有發現,并沒有從中間40K的內存分配,因為不滿足我們申請50K大小的要求,而是向下偏移指針_edata分配50K,到這里就能發現就產生了內存碎片,這塊碎片正是那40K。
系統運行的越久,出現上述這樣的情況越多,整個系統的小內存就會很多,導致的結果是:查看系統的剩余內存比較可觀,但申請大塊內存會返回失敗。