
1. 應(yīng)用程序內(nèi)存布局
在 Linux 系統(tǒng)中,應(yīng)用程序的內(nèi)存被分為若干個邏輯段,如圖:

其中,各個分段的意義是:
- 代碼段:程序編譯后的可執(zhí)行代碼(指令)存放區(qū)域,在編譯時確定了,同一個程序在不同機(jī)器上、在同一個機(jī)器上的不同次運(yùn)行,同一個方法的入口地址都是確定的
- 數(shù)據(jù)段:存放程序已初始化的靜態(tài)常量和全局變量
- BSS 段:存放未初始化的靜態(tài)常量和全局變量
- 堆:動態(tài)分配的內(nèi)存區(qū)域,從低地址向高地址增長,大小不固定
- 棧:存放局部變量、函數(shù)調(diào)用上下文等,棧的大小在程序啟動時就固定了,一般是 8MB,系統(tǒng)提供了參數(shù)可以修改
在上述分段中,文件映射段和堆的內(nèi)存是由程序動態(tài)分配的,通常使用 C 標(biāo)準(zhǔn)庫的malloc或mmap方法來執(zhí)行
2. malloc 是如何分配內(nèi)存的
2.1 malloc 概述
實(shí)際上,malloc 是 C 標(biāo)準(zhǔn)庫函數(shù),而不是系統(tǒng)調(diào)用,mmap 和 brk 是系統(tǒng)調(diào)用,malloc 申請內(nèi)存時有兩種方式:
- 方式一:通過系統(tǒng)調(diào)用
brk從堆分配內(nèi)存,具體方法是將堆空間的最高地址指針往高地址擴(kuò)展,擴(kuò)充堆區(qū)的大小 - 方式二:通過系統(tǒng)調(diào)用
mmap從文件映射區(qū)分配內(nèi)存,具體方法是在文件映射區(qū)中找一塊足夠的空間,進(jìn)行分配
需要注意的是,此兩種方式分配的都是 虛擬內(nèi)存,并沒有分配物理內(nèi)存,那么什么時候進(jìn)行物理內(nèi)存的分配呢?在第一次訪問虛擬空間時,查找頁表失敗,產(chǎn)生缺頁中斷,會進(jìn)行物理分配的內(nèi)存,并建立虛擬內(nèi)存地址和物理內(nèi)存地址的映射關(guān)系(生成頁表項(xiàng))
C 標(biāo)準(zhǔn)庫中提供
malloc / free函數(shù)分配和釋放內(nèi)存,這兩個函數(shù)底層由brk / mmap /unmap等系統(tǒng)調(diào)用實(shí)現(xiàn)。
分別什么情況用 brk 和 mmap 呢?
malloc 源碼中定義了一個閾值 M_MMAP_THRESHOLD,默認(rèn)為 128K
- 當(dāng)分配的值小于該閾值時,調(diào)用
brk - 否則,調(diào)用
mmap
2.2 一個例子
2.2.1 首先看看 brk 內(nèi)存分配

- 程序啟動后,虛擬內(nèi)存空間初始布局如圖1 所示
- 程序執(zhí)行
A = malloc(30K)后,執(zhí)行系統(tǒng)調(diào)用brk,將堆頂指針王高地址增加 30K,得到圖2所示的內(nèi)存布局。注意,此時只是完成了虛擬內(nèi)存的分配,對應(yīng)的物理內(nèi)存還沒分配,頁表項(xiàng)也沒創(chuàng)建,等到程序第一次讀取 A 這塊內(nèi)存時,發(fā)生缺頁中斷,內(nèi)核才會分配物理內(nèi)存并建立對應(yīng)頁表項(xiàng) - 程序執(zhí)行
B = malloc(40K)后,同樣的堆頂指針往高地址增加,如圖3 所示
2.2.2 當(dāng) mmap 分配較大內(nèi)存

程序執(zhí)行
C = malloc(200K),待分配的值超過了閾值,使用mmap分配,在堆和棧中間找一塊空閑內(nèi)存,并初始化為0,如圖4所示。這樣做的一個重要原因是,使用brk移動堆頂?shù)刂返姆绞椒峙鋬?nèi)存,只有高地址的內(nèi)存被釋放后,才能釋放低地址的內(nèi)存,對于內(nèi)存釋放的順序有依賴,如圖4中,必須先釋放 B ,才能釋放 A,對于大塊內(nèi)存分配如果使用此種方式,將造成很多大塊內(nèi)存無法按需釋放,而mmap分配的內(nèi)存無依賴,可以單獨(dú)釋放程序執(zhí)行
D=malloc(100k)后,內(nèi)存空間如圖5所示程序調(diào)用
free(C)釋放內(nèi)存,將 C 對應(yīng)的 虛擬內(nèi)存和物理內(nèi)存一起釋放,得到圖6所示
2.2.3 內(nèi)存的釋放

- 調(diào)用
free(B)后的內(nèi)存布局如圖7,B 對應(yīng)的虛擬內(nèi)存和物理內(nèi)存都沒有釋放,因?yàn)橹挥幸粋€棧頂指針,由于 D 內(nèi)存的存在,無法回推。當(dāng)然,B 部分的內(nèi)存是可重用的,此時如果來一個 40K 的分配請求,很可能就把 B 給分配返回了。 - 程序執(zhí)行
free(D)后,內(nèi)存布局如圖 8 所示,B和D構(gòu)成了一塊 140K 的空閑內(nèi)存 - 系統(tǒng)默認(rèn)當(dāng)高地址空間的空閑內(nèi)存超過 128K(由
M_TRIM_THRESHOLD選項(xiàng)調(diào)節(jié))時,會自動執(zhí)行內(nèi)存緊縮操作 (trim),在上一步free(D)完成后,進(jìn)行內(nèi)存緊縮,內(nèi)存布局變成圖9所示,棧頂?shù)刂方档停尫艑?yīng)的虛擬內(nèi)存和物理內(nèi)存
3. 總結(jié)
malloc 細(xì)節(jié)包括以下幾點(diǎn):
- 當(dāng)分配請求超過閾值時,使用
mmap進(jìn)行分配 - 分配請求小于閾值時,使用
brk分配 - 分配時并沒有立即分配物理地址,只是分配了虛擬地址,第一次訪問時才建立頁表項(xiàng),分配物理地址
- 釋放
mmap分配的地址時,可以立即釋放 - 釋放
brk分配的內(nèi)存時,不會立即釋放,但可以重用,執(zhí)行釋放時會檢查堆頂指針附近的最大空閑塊,如果超過閾值,則會執(zhí)行內(nèi)存緊縮策略,真正釋放物理地址