malloc 背后的系统知识(虚拟内存地址)
內(nèi)存優(yōu)化總結(jié):ptmalloc、tcmalloc和jemalloc:https://blog.csdn.net/junlon2006/article/details/77854898??
malloc的底層實現(xiàn)(ptmalloc):https://blog.csdn.net/z_ryan/article/details/79950737
從操作系統(tǒng)層面來說,malloc 是考察面試者對操作系統(tǒng)底層的存儲管理理解的一個很好的方式,涉及到虛擬內(nèi)存、分頁/分段等。下面逐個細說。
1. 虛擬內(nèi)存
首先需要知道的是程序運行起來的話需要被加載的物理內(nèi)存中,具體到計算機硬件就是內(nèi)存條。操作系統(tǒng)啟動的時候先把自己加載到物理內(nèi)存的固定位置(一般為底部),物理內(nèi)存的其他位置就用來運行用戶程序。程序就是一堆指令,程序運行可以簡單抽象為把指令加載到內(nèi)存中,然后 CPU 將指令從內(nèi)存載入執(zhí)行。
1. 為什么需要虛擬內(nèi)存?
CPU 對內(nèi)存的尋址最簡單的方式就是直接使用物理內(nèi)存地址,這種方式一般叫做物理尋址。早期的 PC 使用物理尋址,而且像數(shù)字信號處理器、嵌入式微控制器也使用物理尋址。物理尋址的好處是簡單,壞處也有很多,比如:
不安全:操作系統(tǒng)的地址直接暴露給用戶程序,用戶程序可以破壞操作系統(tǒng)。這種解決方案是采用特殊的硬件保護。
同時運行多個程序比較困難:多個用戶程序如果都直接引用物理地址,很容易互相干擾。那么是不是可以通過不斷交換物理內(nèi)存和磁盤來保證物理內(nèi)存某一時間自由一個程序在運行呢?當時是可以的,但是這引入很多不必要和復雜的工作。
用戶程序大小受限:受制于物理內(nèi)存大小。我們現(xiàn)在的錯覺是應(yīng)用程序大小都小于物理內(nèi)存,這主要是因為現(xiàn)在 PC 的物理內(nèi)存都比較大。實際上只有 1G 物理內(nèi)存的 PC 是可以運行 2G 的應(yīng)用程序的。
?
綜合上面各種缺點,虛擬內(nèi)存出現(xiàn)了。
2. 虛擬內(nèi)存概覽
虛擬內(nèi)存的基本思想是:每個程序擁有獨立的地址空間(也就是虛擬內(nèi)存地址,或者稱作虛擬地址),互不干擾。地址空間被分割成多個塊,每一塊稱作一頁(page),每一頁有連續(xù)的地址范圍。虛擬地址的頁被映射到物理內(nèi)存(通過 MMU,Memory Management Unit),但是并不是所有的頁都必須在內(nèi)存中才能運行程序。當程序引用到一部分在物理內(nèi)存中的地址空間時,由硬件立刻執(zhí)行必要的映射。當程序引用到一部分不在物理內(nèi)存中的地址空間時,由操作系統(tǒng)負責將確實的部分裝入物理內(nèi)存。虛擬地址尋址(也叫做虛擬尋址)的示意圖如下。
3.虛擬內(nèi)存實現(xiàn)
1.虛擬內(nèi)存大小
一般是和 CPU 字長相關(guān),比如 32 位對應(yīng)的虛擬地址空間大小為:0 ~ 2^31。
2. MMU
CPU 將虛擬地址發(fā)送給 MMU,然后 MMU 將虛擬地址翻譯成物理地址,再尋址物理內(nèi)存。那么虛擬地址和物理地址具體是怎么映射的呢?完成映射還需要另一個重要的數(shù)據(jù)結(jié)構(gòu)的參與:頁表(page table)。頁表完成虛擬地址和物理地址的映射,MMU 每次翻譯的時候都需要讀取頁表。頁表的一種簡單表示如下。
這里頁大小為 p 位。虛擬內(nèi)存的頁和物理內(nèi)存的頁大小一樣。虛擬地址的高 n-p 位,又叫做虛擬頁號(Virtual Page Number, VPN),用來索引物理頁號(Physical Page Number,PPN),最后將 PPN 和低 p 位組合在一起就得到了物理地址。
3. 頁表的兩個問題
前面說到用 VPN 來做頁表索引,也就是說頁表的大小為虛擬地址位數(shù) / 頁的大小。比如 32 位機器,頁大小一般為 4K ,則頁表項有 2^32 / 2^12 = 2^20 條目。如果機器字長 64 位,頁表項就更多了。那么怎么解決呢?一般有兩種方法:
倒排頁表。物理頁號做索引,映射到多個虛擬地址。通過虛擬地址查找的時候就需要通過虛擬地址的中間幾位來做索引了。
多級頁表。以兩級頁表為例。一級頁表中的每個 PTE (page table entry)映射虛擬地址空間的一個 4MB 的片,每一片由1024 個連續(xù)的頁面組成。一級 PTE 指向二級頁表的基址。這樣 32 位地址空間使用 1024 個一級 PTE 就可以表示。需要的二級頁表總條目還是 2^32 / 2^12 = 2^20 個。這里的關(guān)鍵在于如果一級 PTE i 中的頁面都未被分配,一級 PTE 就為空。多級頁面的一個簡單示意圖如下。
多級頁表減少內(nèi)存占用的關(guān)鍵在于:
如果一級頁表中的一個 PTE 為空,那么相應(yīng)的二級頁表就根本不會存在。這是一種巨大的潛在節(jié)約。
只有一級頁表才需要常駐內(nèi)存。虛擬內(nèi)存系統(tǒng)可以在需要時創(chuàng)建、頁面調(diào)入或者調(diào)出二級頁表,從而減輕內(nèi)存的壓力。
第二個問題是頁表是在內(nèi)存中,而 MMU 位于 CPU 芯片中,這樣每次地址翻譯可能都需要先訪問一次內(nèi)存中的頁表(CPU L1,L2,L3 Cache Miss 的時候訪問內(nèi)存),效率非常低下。對應(yīng)的解決方案是引入頁表的高速緩存:TLB(Translation Lookaside Buffer)。加入 TLB,整個虛擬地址翻譯的過程如下兩圖所示。
什么是page fault
? ? ? ?當進程訪問它的虛擬地址空間中的PAGE時,如果這個PAGE目前還不在物理內(nèi)存中,此時CPU是不能干活的,Linux會產(chǎn)生一個hard page fault中斷。系統(tǒng)需要從慢速設(shè)備(如磁盤)將對應(yīng)的數(shù)據(jù)PAGE讀入物理內(nèi)存,并建立物理內(nèi)存地址與虛擬地址空間PAGE的映射關(guān)系。然后進程才能訪問這部分虛擬地址空間的內(nèi)存。
page fault 又分為幾種,major page fault、 minor page fault、 invalid(segment fault)。
major page fault 也稱為 hard page fault, 指需要訪問的內(nèi)存不在虛擬地址空間,也不在物理內(nèi)存中,需要從慢速設(shè)備載入。從swap 回到物理內(nèi)存也是 hard page fault。
minor page fault 也稱為 soft page fault, 指需要訪問的內(nèi)存不在虛擬地址空間,但是在物理內(nèi)存中,只需要MMU建立物理內(nèi)存和虛擬地址空間的映射關(guān)系即可。
invalid fault 也稱為 segment fault,指進程需要訪問的內(nèi)存地址不在它的虛擬地址空間范圍內(nèi),屬于越界訪問,內(nèi)核會報 segment fault錯誤。
抖動在分頁存儲管理系統(tǒng)中,內(nèi)存中只存放了那些經(jīng)常使用的頁面, 而其它頁面則存放在外存中,當進程運行需要的內(nèi)容不在內(nèi)存時, 便啟動磁盤讀操作將所需內(nèi)容調(diào)入內(nèi)存,若內(nèi)存中沒有空閑物理塊, 還需要將內(nèi)存中的某頁面置換出去。也就是說,系統(tǒng)需要不斷地在內(nèi)外存之間交換信息。 若在系統(tǒng)運行過程中,剛被淘汰出內(nèi)存的頁面,過后不久又要訪問它, 需要再次將其調(diào)入。而該頁面調(diào)入內(nèi)存后不久又再次被淘汰出內(nèi)存,然后又要訪問它。 如此反復,使得系統(tǒng)把大部分時間用在了頁面的調(diào)入/換出上, 而幾乎不能完成任何有效的工作,這種現(xiàn)象稱為抖動。
2. 分段
1. 分段概述
前面介紹了分頁內(nèi)存管理,可以說通過多級頁表,TLB 等,分頁內(nèi)存管理方法已經(jīng)相當不錯了。那么分頁有什么缺點呢?
共享困難:通過共享頁面來實現(xiàn)共享當然是可以的。這里的問題在于我們要保證頁面上只包含可以共享的內(nèi)容并不是一件容易的事兒,因為進程空間是直接映射到頁面上的。這樣一個頁面上很可能包含不能共享的內(nèi)容(比如既包含代碼又包含數(shù)據(jù),代碼可以共享,而數(shù)據(jù)不能共享)。早期的 PDP-11 實現(xiàn)的一種解決方法是為指令和數(shù)據(jù)設(shè)置分離的地址空間,分別稱為 I 空間和 D 空間(其實這已經(jīng)和分段很像了)。
程序地址空間受限于虛擬地址:我們將程序全部映射到一個統(tǒng)一的虛擬地址的問題在于不好擴張。不如我們程序的地址按先代碼放在一起,然后把數(shù)據(jù)放在一起,然后再放 XXX,這樣其中某一部分的空間擴張起來都會影響到相鄰的空間,非常不方便。
上面的問題一個比較直觀的解決方法是提供多個獨立的地址空間,也就是段(segment)。每個段的長度視具體的段不同而不同,而且是可以在運行期動態(tài)改變的。因為每個段都構(gòu)成了一個獨立的地址空間,所以它們可以獨立的增長或者減小而不會影響到其他的段。如果一個段比較大,把它整個保存到內(nèi)存中可能很不方便甚至是不可能的,因此可以對段采用分頁管理,只有那些真正需要的頁面才會被調(diào)入內(nèi)存。
采用分段和分頁結(jié)合的方式管理內(nèi)存,一個地址由兩個部分組成:段和段內(nèi)地址。段內(nèi)地址又進一步分為頁號和頁偏移。在進行內(nèi)存訪問時,過程如下:
根據(jù)段號找到段描述符(存放段基址)。
檢查該段的頁表是否在內(nèi)存中。如果在,則找到它的位置,如果不在,則產(chǎn)生段錯誤。
檢查所請求的虛擬頁面的頁表項,如果該頁面不在內(nèi)存中則產(chǎn)生缺頁中斷,如果在內(nèi)存中就從頁表項中取出這個頁面在內(nèi)存中的起始地址。
將頁面起始地址和偏移量進行拼接得到物理地址,然后完成讀寫。
2. 進程的段
每個 Linux 程序都有一個運行時內(nèi)存映像,也就是各個段的布局,簡單如下圖所示。
注意上圖只是一個相對位置圖,實際上這些段并不是相鄰的。主要的段包括只讀代碼段、讀寫段、運行時堆、映射區(qū)、用戶棧。在分配棧、堆段運行時地址的時候,鏈接器會使用空間地址空間布局隨機化(ASLR),但是相對位置不會變。上圖中 .data 等是對應(yīng)進程中的不同數(shù)據(jù)的 section ,或者叫做節(jié)。簡介如下。
-
.text: 已編譯程序的機器代碼。
-
.rodata: 只讀數(shù)據(jù)。
-
.data: 已初始化的全局和靜態(tài)變量。局部變量保存在棧上。
-
.bss: 未初始化的全局和靜態(tài)變量,以及所有被初始化為 0 的全局或者靜態(tài)變量。在目標文件中這個節(jié)不占據(jù)實際的空間,它僅僅是一個占位符。
3. malloc 實現(xiàn)
1. 堆內(nèi)存管理
我們常說的 malloc 函數(shù)是 glibc 提供的庫函數(shù)。
glibc 的內(nèi)存管理使用的方法是 ptmalloc,除此之后還有很多其他內(nèi)存管理方案,比如 tcmalloc (golang 使用的類似 tcmalloc)。
ptmalloc 對于申請內(nèi)存小于 128KB 時,分配是在堆段,使用系統(tǒng)調(diào)用 brk() 或者 sbrk()。如果大于 128 KB 的話,分配在映射區(qū),使用系統(tǒng)調(diào)用 mmap()。
2. brk, sbrk
在堆段申請的話,使用系統(tǒng)調(diào)用?brk?或者?sbrk。
int?brk(const?void?*addr);
void?*sbrk(intptr_t?incr);
brk?將 brk 指針放置到指定地址處,成功返回 0,否則返回 -1。sbrk?將 brk 指針向后移動指定字節(jié),返回依賴于系統(tǒng)實現(xiàn),或者返回移動前的 brk 位置,或者返回移動后的 brk 位置。下面使用?sbrk?實現(xiàn)一個巨簡單的 malloc。
void?*malloc(size_t?size)?{
????void?*p?=?sbrk(0);
????void?*request?=?sbrk(size);
????if?(request?==?(void*)?-1)?{
????????return?NULL;?// sbrk failed.
????}?else?{
????????assert(p?==?request);?// Not thread safe.
????????return?p;
????}
}
3. mmap
linux 系統(tǒng)調(diào)用 mmap 將一個文件或者其它對象映射進內(nèi)存。
#include <sys/mman.h>
void?*mmap(void?*addr,?size_t?length,?int?prot,?int?flags,?int?fd,?off_t?offset);
mmap 的 flags 可選多種參數(shù),當選擇?MAP_ANONYMOUS?時,不需要傳入文件描述符,malloc 使用的就是?MAP_ANONYMOUS?模式。mmap 申請的內(nèi)存在操作系統(tǒng)的映射區(qū)。比如 32 位系統(tǒng),映射區(qū)從 3G 虛擬地址粗向下生長,但是因為程序的其他段也會占用空間(比如代碼段必須以特定的地址開始),所以并不能申請 3G 的大小。
4. malloc 和物理內(nèi)存有關(guān)系嗎?
可以說沒關(guān)系,malloc 申請的地址是線性地址,申請的時候并沒有進行映射。訪問到的時候觸發(fā)缺頁異常,這個時候才會進行物理地址映射。
5. ptmalloc
ptmalloc 只是 glibc 使用的內(nèi)存管理策略,篇幅有限,這里就不細說了。
4. 參考:
《深入理解計算機系統(tǒng)》
《現(xiàn)代操作系統(tǒng)》
StackOverFlow
mmap manpage
tcmalloc 介紹
a malloc tutorial
malloc manpage
understanding glibc malloc
advanced memory allocation
總結(jié)
以上是生活随笔為你收集整理的malloc 背后的系统知识(虚拟内存地址)的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: C语言内存字节对齐小结
- 下一篇: 从内核文件系统看文件读写过程