1. 程式人生 > >malloc 背後的系統知識

malloc 背後的系統知識

面試的時候經常會被問到 malloc 的實現。從作業系統層面來說,malloc 確實是考察面試者對作業系統底層的儲存管理理解的一個很好的方式,涉及到虛擬記憶體、分頁/分段等。下面逐個細說。

1. 虛擬記憶體

首先需要知道的是程式執行起來的話需要被載入的實體記憶體中,具體到計算機硬體就是記憶體條。作業系統啟動的時候先把自己載入到實體記憶體的固定位置(一般為底部),實體記憶體的其他位置就用來執行使用者程式。程式就是一堆指令,程式執行可以簡單抽象為把指令載入到記憶體中,然後 CPU 將指令從記憶體載入執行。

1. 為什麼需要虛擬記憶體?

CPU 對記憶體的定址最簡單的方式就是直接使用實體記憶體地址,這種方式一般叫做物理定址。早期的 PC 使用物理定址,而且像數字訊號處理器、嵌入式微控制器也使用物理定址。物理定址的好處是簡單,壞處也有很多,比如:

不安全:作業系統的地址直接暴露給使用者程式,使用者程式可以破壞作業系統。這種解決方案是採用特殊的硬體保護。

同時執行多個程式比較困難:多個使用者程式如果都直接引用實體地址,很容易互相干擾。那麼是不是可以通過不斷交換實體記憶體和磁碟來保證實體記憶體某一時間自由一個程式在執行呢?當時是可以的,但是這引入很多不必要和複雜的工作。

使用者程式大小受限:受制於實體記憶體大小。我們現在的錯覺是應用程式大小都小於實體記憶體,這主要是因為現在 PC 的實體記憶體都比較大。實際上只有 1G 實體記憶體的 PC 是可以執行 2G 的應用程式的。

綜合上面各種缺點,虛擬記憶體出現了。

2. 虛擬記憶體概覽

虛擬記憶體的基本思想是:每個程式擁有獨立的地址空間(也就是虛擬記憶體地址,或者稱作虛擬地址),互不干擾。地址空間被分割成多個塊,每一塊稱作一頁(page),每一頁有連續的地址範圍。虛擬地址的頁被對映到實體記憶體(通過 MMU,Memory Management Unit),但是並不是所有的頁都必須在記憶體中才能執行程式。當程式引用到一部分在實體記憶體中的地址空間時,由硬體立刻執行必要的對映。當程式引用到一部分不在實體記憶體中的地址空間時,由作業系統負責將確實的部分裝入實體記憶體。虛擬地址定址(也叫做虛擬定址)的示意圖如下。

3.虛擬記憶體實現

1.虛擬記憶體大小

一般是和 CPU 字長相關,比如 32 位對應的虛擬地址空間大小為:0 ~ 2^31。

2. MMU

CPU 將虛擬地址傳送給 MMU,然後 MMU 將虛擬地址翻譯成實體地址,再定址實體記憶體。那麼虛擬地址和實體地址具體是怎麼對映的呢?完成對映還需要另一個重要的資料結構的參與:頁表(page table)。頁表完成虛擬地址和實體地址的對映,MMU 每次翻譯的時候都需要讀取頁表。頁表的一種簡單表示如下。

這裡頁大小為 p 位。虛擬記憶體的頁和實體記憶體的頁大小一樣。虛擬地址的高 n-p 位,又叫做虛擬頁號(Virtual Page Number, VPN),用來索引物理頁號(Physical Page Number,PPN),最後將 PPN 和低 p 位組合在一起就得到了實體地址。

3. 頁表的兩個問題

前面說到用 VPN 來做頁表索引,也就是說頁表的大小為虛擬地址位數 / 頁的大小。比如 32 位機器,頁大小一般為 4K ,則頁表項有 2^32 / 2^12 = 2^20 條目。如果機器字長 64 位,頁表項就更多了。那麼怎麼解決呢?一般有兩種方法:

  1. 倒排頁表。物理頁號做索引,對映到多個虛擬地址。通過虛擬地址查詢的時候就需要通過虛擬地址的中間幾位來做索引了。
  2. 多級頁表。以兩級頁表為例。一級頁表中的每個 PTE (page table entry)對映虛擬地址空間的一個 4MB 的片,每一片由1024 個連續的頁面組成。一級 PTE 指向二級頁表的基址。這樣 32 位地址空間使用 1024 個一級 PTE 就可以表示。需要的二級頁表總條目還是 2^32 / 2^12 = 2^20 個。這裡的關鍵在於如果一級 PTE i 中的頁面都未被分配,一級 PTE 就為空。多級頁面的一個簡單示意圖如下。

多級頁表減少記憶體佔用的關鍵在於:

  1. 如果一級頁表中的一個 PTE 為空,那麼相應的二級頁表就根本不會存在。這是一種巨大的潛在節約。
  2. 只有一級頁表才需要常駐記憶體。虛擬記憶體系統可以在需要時建立、頁面調入或者調出二級頁表,從而減輕記憶體的壓力。

第二個問題是頁表是在記憶體中,而 MMU 位於 CPU 晶片中,這樣每次地址翻譯可能都需要先訪問一次記憶體中的頁表(CPU L1,L2,L3 Cache Miss 的時候訪問記憶體),效率非常低下。對應的解決方案是引入頁表的快取記憶體:TLB(Translation Lookaside Buffer)。加入 TLB,整個虛擬地址翻譯的過程如下兩圖所示。

關於虛擬記憶體還有一些內容比如 page fault 處理,這裡就不再贅述了。

2. 分段

1. 分段概述

前面介紹了分頁記憶體管理,可以說通過多級頁表,TLB 等,分頁記憶體管理方法已經相當不錯了。那麼分頁有什麼缺點呢?

  1. 共享困難:通過共享頁面來實現共享當然是可以的。這裡的問題在於我們要保證頁面上只包含可以共享的內容並不是一件容易的事兒,因為程序空間是直接對映到頁面上的。這樣一個頁面上很可能包含不能共享的內容(比如既包含程式碼又包含資料,程式碼可以共享,而資料不能共享)。早期的 PDP-11 實現的一種解決方法是為指令和資料設定分離的地址空間,分別稱為 I 空間和 D 空間(其實這已經和分段很像了)。
  2. 程式地址空間受限於虛擬地址:我們將程式全部對映到一個統一的虛擬地址的問題在於不好擴張。不如我們程式的地址按先程式碼放在一起,然後把資料放在一起,然後再放 XXX,這樣其中某一部分的空間擴張起來都會影響到相鄰的空間,非常不方便。

上面的問題一個比較直觀的解決方法是提供多個獨立的地址空間,也就是段(segment)。每個段的長度視具體的段不同而不同,而且是可以在執行期動態改變的。因為每個段都構成了一個獨立的地址空間,所以它們可以獨立的增長或者減小而不會影響到其他的段。如果一個段比較大,把它整個儲存到記憶體中可能很不方便甚至是不可能的,因此可以對段採用分頁管理,只有那些真正需要的頁面才會被調入記憶體。

採用分段和分頁結合的方式管理記憶體,一個地址由兩個部分組成:段和段內地址。段內地址又進一步分為頁號和頁偏移。在進行記憶體訪問時,過程如下:

  1. 根據段號找到段描述符(存放段基址)。
  2. 檢查該段的頁表是否在記憶體中。如果在,則找到它的位置,如果不在,則產生段錯誤。
  3. 檢查所請求的虛擬頁面的頁表項,如果該頁面不在記憶體中則產生缺頁中斷,如果在記憶體中就從頁表項中取出這個頁面在記憶體中的起始地址。
  4. 將頁面起始地址和偏移量進行拼接得到實體地址,然後完成讀寫。

2. 程序的段

每個 Linux 程式都有一個執行時記憶體映像,也就是各個段的佈局,簡單如下圖所示。

注意上圖只是一個相對位置圖,實際上這些段並不是相鄰的。主要的段包括只讀程式碼段、讀寫段、執行時堆、使用者棧。在分配棧、堆段執行時地址的時候,連結器會使用空間地址空間佈局隨機化(ASLR),但是相對位置不會變。上圖中 .data 等是對應程序中的不同資料的 section ,或者叫做節。簡介如下。

  • .text: 已編譯程式的機器程式碼。
  • .rodata: 只讀資料。
  • .data: 已初始化的全域性和靜態變數。區域性變數儲存在棧上。
  • .bss: 未初始化的全域性和靜態變數,以及所有被初始化為 0 的全域性或者靜態變數。在目標檔案中這個節不佔據實際的空間,它僅僅是一個佔位符。

3. malloc 實現

1. 堆記憶體管理

我們常說的 malloc 函式是 glibc 提供的庫函式。glibc 的記憶體管理使用的方法是 ptmalloc,除此之後還有很多其他記憶體管理方案,比如 tcmalloc (golang 使用的就是 tcmalloc)。

ptmalloc 對於申請記憶體小於 128KB 時,分配是在堆段,使用系統呼叫 brk() 或者 sbrk()。如果大於 128 KB 的話,分配在對映區,使用系統呼叫 mmap()。

2. brk, sbrk

在堆段申請的話,使用系統呼叫 brk 或者 sbrk

12 intbrk(constvoid*addr);void*sbrk(intptr_t incr);

brk 將 brk 指標放置到指定地址處,成功返回 0,否則返回 -1。sbrk 將 brk 指標向後移動指定位元組,返回依賴於系統實現,或者返回移動前的 brk 位置,或者返回移動後的 brk 位置。下面使用 sbrk 實現一個巨簡單的 malloc。

12345678910 void*malloc(size_t size){void*p=sbrk(0);void*request=sbrk(size);if(request==(void*)-1){returnNULL;// sbrk failed.}else{assert(p==request);// Not thread safe.returnp;}}

3. mmap

linux 系統呼叫 mmap 將一個檔案或者其它物件對映進記憶體。

123 #include <sys/mman.h>void*mmap(void*addr,size_t length,intprot,intflags,intfd,off_t offset);

mmap 的 flags 可選多種引數,當選擇 MAP_ANONYMOUS 時,不需要傳入檔案描述符,malloc 使用的就是 MAP_ANONYMOUS 模式。mmap 申請的記憶體在作業系統的對映區。比如 32 位系統,對映區從 3G 虛擬地址粗向下生長,但是因為程式的其他段也會佔用空間(比如程式碼段必須以特定的地址開始),所以並不能申請 3G 的大小。

4. malloc 和實體記憶體有關係嗎?

可以說沒關係,malloc 申請的地址是線性地址,申請的時候並沒有進行對映。訪問到的時候觸發缺頁異常,這個時候才會進行實體地址對映。

5. ptmalloc

ptmalloc 只是 glibc 使用的記憶體管理策略,篇幅有限,這裡就不細說了。我之前寫了一篇 tcmalloc 的介紹,大家可以對比著看。

4. 參考:

打賞支援我寫出更多好文章,謝謝!

打賞作者

打賞支援我寫出更多好文章,謝謝!

任選一種支付方式