[TOC]
參考
- malloc()之后,內(nèi)核發(fā)生了什么?
- 進(jìn)程分配內(nèi)存的兩種方式--brk() 和mmap()(不涉及共享內(nèi)存)
- Linux內(nèi)存分配小結(jié)--malloc、brk、mmap
- 詳解缺頁中斷-----缺頁中斷處理(內(nèi)核、用戶)
1. 缺頁中斷
1.1. 什么是缺頁中斷
malloc和mmap等內(nèi)存分配函數(shù)只是建立進(jìn)程的虛擬地址空間,并沒有分配實(shí)際的物理內(nèi)存。當(dāng)進(jìn)程訪問沒有建立映射關(guān)系的虛擬內(nèi)存時(shí)會(huì)自動(dòng)的觸發(fā)一個(gè)缺頁中斷。
請求分頁的系統(tǒng)當(dāng)中,可以查詢頁表當(dāng)前的狀態(tài)位來查詢當(dāng)前頁是否在內(nèi)存當(dāng)中,如果不在內(nèi)存當(dāng)中可以通過頁表當(dāng)中的外存地址將缺的一頁讀到內(nèi)存當(dāng)中。比如mmap映射文件。
1.2. 如何查看缺頁中斷
用ps -o majflt,minflt -C program命令查看。
majflt代表major fault(需要讀取磁盤),中文名叫大錯(cuò)誤,minflt代表minor fault(不需要讀取磁盤),中文名叫小錯(cuò)誤。
這兩個(gè)數(shù)值表示一個(gè)進(jìn)程自啟動(dòng)以來所發(fā)生的缺頁中斷的次數(shù)。
1.3. 缺頁異常發(fā)生后的操作
當(dāng)一個(gè)進(jìn)程發(fā)生缺頁中斷的時(shí)候,進(jìn)程會(huì)陷入內(nèi)核態(tài),執(zhí)行以下操作:
- 檢查要訪問的虛擬地址是否合法
- 查找/分配一個(gè)物理頁
- 填充物理頁內(nèi)容(讀取磁盤,或者直接置0,或者啥也不干)
- 建立映射關(guān)系(虛擬地址到物理地址)
- 重新執(zhí)行發(fā)生缺頁中斷的那條指令
如果第3步,需要讀取磁盤,那么這次缺頁中斷就是majflt,否則就是minflt。
2. 內(nèi)存分配過程(malloc)
從操作系統(tǒng)角度來看,進(jìn)程分配內(nèi)存有兩種方式,分別由兩個(gè)系統(tǒng)調(diào)用完成:brk和mmap(不考慮共享內(nèi)存)。
brk是將數(shù)據(jù)段(.data)的最高地址指針_edata往高地址推;
mmap是在進(jìn)程的虛擬地址空間中(堆和棧中間,稱為文件映射區(qū)域的地方)找一塊空閑的虛擬內(nèi)存。
這兩種方式分配的都是虛擬內(nèi)存,沒有分配物理內(nèi)存。在第一次訪問(讀/寫)已分配的虛擬地址空間的時(shí)候,發(fā)生缺頁中斷,操作系統(tǒng)負(fù)責(zé)分配物理內(nèi)存,然后建立虛擬內(nèi)存和物理內(nèi)存之間的映射關(guān)系。
在標(biāo)準(zhǔn)C庫中,提供了malloc/free函數(shù)分配釋放內(nèi)存,這兩個(gè)函數(shù)底層是由brk,mmap,munmap這些系統(tǒng)調(diào)用實(shí)現(xiàn)的。
2.1. brk分配
malloc小于128k的內(nèi)存,使用brk分配內(nèi)存,將_edata往高地址推

- 進(jìn)程啟動(dòng)的時(shí)候,其(虛擬)內(nèi)存空間的初始布局如圖1所示。其中,mmap內(nèi)存映射文件是在堆和棧的中間(例如libc-2.2.93.so,其它數(shù)據(jù)文件等),為了簡單起見,省略了內(nèi)存映射文件。_edata指針(glibc里面定義)指向數(shù)據(jù)段的最高地址。
- 進(jìn)程調(diào)用A=malloc(30K)以后,內(nèi)存空間如圖2:malloc函數(shù)會(huì)調(diào)用brk系統(tǒng)調(diào)用,將_edata指針往高地址推30K,就完成虛擬內(nèi)存分配。(注:_edata+30K只是完成虛擬地址的分配,A這塊內(nèi)存現(xiàn)在還是沒有物理頁與之對應(yīng)的,等到進(jìn)程第一次讀寫A這塊內(nèi)存的時(shí)候,發(fā)生缺頁中斷,這個(gè)時(shí)候,內(nèi)核才分配A這塊內(nèi)存對應(yīng)的物理頁。也就是說,如果用malloc分配了A這塊內(nèi)容,然后從來不訪問它,那么,A對應(yīng)的物理頁是不會(huì)被分配的。)
- 進(jìn)程調(diào)用B=malloc(40K)以后,內(nèi)存空間如圖3。
2.2. mmap分配內(nèi)存
malloc大于128k的內(nèi)存,使用mmap分配內(nèi)存,在堆和棧之間找一塊空閑內(nèi)存分配(對應(yīng)獨(dú)立內(nèi)存,而且初始化為0)

- 進(jìn)程調(diào)用C=malloc(200K)以后,內(nèi)存空間如圖4:默認(rèn)情況下,malloc函數(shù)分配內(nèi)存,如果請求內(nèi)存大于128K(可由M_MMAP_THRESHOLD選項(xiàng)調(diào)節(jié)),那就不是去推_edata指針了,而是利用mmap系統(tǒng)調(diào)用,從堆和棧的中間分配一塊虛擬內(nèi)存。
這樣子做主要是因?yàn)椋篵rk分配的內(nèi)存需要等到高地址內(nèi)存釋放以后才能釋放(例如,在B釋放之前,A是不可能釋放的,這就是內(nèi)存碎片產(chǎn)生的原因,什么時(shí)候緊縮看下面),而mmap分配的內(nèi)存可以單獨(dú)釋放。 - 進(jìn)程調(diào)用D=malloc(100K)以后,內(nèi)存空間如圖5;
- 進(jìn)程調(diào)用free(C)以后,C對應(yīng)的虛擬內(nèi)存和物理內(nèi)存一起釋放。
2.3. 釋放內(nèi)存
上一小節(jié)已經(jīng)介紹了mmap的內(nèi)存釋放,這里主要看brk的內(nèi)存釋放

- 進(jìn)程調(diào)用free(B)以后,如圖7所示:B對應(yīng)的虛擬內(nèi)存和物理內(nèi)存都沒有釋放,因?yàn)橹挥幸粋€(gè)_edata指針,如果往回推,那么D這塊內(nèi)存怎么辦呢?當(dāng)然,B這塊內(nèi)存,是可以重用的,如果這個(gè)時(shí)候再來一個(gè)40K的請求,那么malloc很可能就把B這塊內(nèi)存返回回去了。
- 進(jìn)程調(diào)用free(D)以后,如圖8所示:B和D連接起來,變成一塊140K的空閑內(nèi)存。
- 默認(rèn)情況下:當(dāng)最高地址空間的空閑內(nèi)存超過128K(可由M_TRIM_THRESHOLD選項(xiàng)調(diào)節(jié))時(shí),執(zhí)行內(nèi)存緊縮操作(trim)。在上一個(gè)步驟free的時(shí)候,發(fā)現(xiàn)最高地址空閑內(nèi)存超過128K,于是內(nèi)存緊縮,變成圖9所示。
3. malloc 測試
- 循環(huán)new分配64K * 2048的內(nèi)存空間,寫入臟數(shù)據(jù)后,循環(huán)調(diào)用delete釋放。top看進(jìn)程依然使用131M內(nèi)存,沒有釋放。 —— 此時(shí)用brk
- 循環(huán)new分配128K * 2048的內(nèi)存空間,寫入臟數(shù)據(jù)后,循環(huán)調(diào)用delete釋放。top看進(jìn)程使用,2960字節(jié)內(nèi)存,完全釋放。 —— 此時(shí)用mmap
- 設(shè)置M_MMAP_THRESHOLD 256k,循環(huán)new分配128k * 2048 的內(nèi)存空間,寫入臟數(shù)據(jù)后,循環(huán)調(diào)用delete釋放,而后調(diào)用malloc_trim(0)。top看進(jìn)程使用,2348字節(jié),完全釋放。 ——此時(shí)用brk
4. 簡單思考
既然堆內(nèi)內(nèi)存brk不能直接釋放,為什么不全部使用 mmap 來分配,munmap直接釋放呢?
其實(shí),進(jìn)程向 OS 申請和釋放地址空間的接口 sbrk/mmap/munmap 都是系統(tǒng)調(diào)用,頻繁調(diào)用系統(tǒng)調(diào)用都比較消耗系統(tǒng)資源的。并且, mmap 申請的內(nèi)存被 munmap 后,重新申請會(huì)產(chǎn)生更多的缺頁中斷。例如使用 mmap 分配 1M 空間,第一次調(diào)用產(chǎn)生了大量缺頁中斷 (1M/4K 次 ) ,當(dāng)munmap 后再次分配 1M 空間,會(huì)再次產(chǎn)生大量缺頁中斷。缺頁中斷是內(nèi)核行為,會(huì)導(dǎo)致內(nèi)核態(tài)CPU消耗較大。另外,如果使用 mmap 分配小內(nèi)存,會(huì)導(dǎo)致地址空間的分片更多,內(nèi)核的管理負(fù)擔(dān)更大。
同時(shí)堆是一個(gè)連續(xù)空間,并且堆內(nèi)碎片由于沒有歸還 OS ,如果可重用碎片,再次訪問該內(nèi)存很可能不需產(chǎn)生任何系統(tǒng)調(diào)用和缺頁中斷,這將大大降低 CPU 的消耗。 因此, glibc 的 malloc 實(shí)現(xiàn)中,充分考慮了 brk 和 mmap 行為上的差異及優(yōu)缺點(diǎn),默認(rèn)分配大塊內(nèi)存 (128k) 才使用 mmap 獲得地址空間,也可通過 mallopt(M_MMAP_THRESHOLD, <SIZE>) 來修改這個(gè)臨界值。