百度工程師帶你探祕C++記憶體管理(理論篇)

語言: CN / TW / HK

圖片

作者 | daydreamer

在網際網路的服務中,C++常用於搭建高效能、高併發、大流量、低延時的後端服務。如何合理的分配記憶體滿足系統高效能需求是一個高頻且重要的話題,而且因為記憶體自身的特點和實際問題的複雜,組合出了諸多難題。

我們可以對記憶體進行多種型別的劃分,從記憶體申請大小來看

  1. 小物件分配:小於4倍記憶體頁大小的記憶體分配,在4KiB頁大小情況下,<16KiB算作小物件分配;

  2. 大物件分配:大於等於4倍記憶體頁大小的記憶體分配,在4KiB頁大小情況下,>=16KiB算作大物件分配。

從一塊記憶體的被持有時長來看

  1. 後端一次請求內甚至更短時間申請和釋放

  2. 任意時間視窗內記憶體持有和更新

  3. 幾乎與應用程序等長的記憶體持有和更新

  4. 某個程序消亡後一段時間內,由該程序申請的仍具有意義的記憶體持有和釋放

當然還可以按照記憶體申請釋放頻率、讀寫頻率進行進一步的分類。

記憶體管理服務於應用系統,目的是協助系統更好的解決瓶頸問題,比如對於『如何降低後端響應的延遲和提高穩定性』記憶體管理可能要考慮的是:

  1. 處理記憶體讀寫併發(讀頻繁or寫頻繁)降低響應時間和CPU消耗

  2. 應用層的記憶體的池化複用

  3. 底層記憶體向系統申請的記憶體塊大小及記憶體碎片化

每一個問題展開可能都是一個比較大的話題,本文作為系列文章《探祕C++記憶體管理》的開篇,先介紹Linux C++程式記憶體管理的理論基礎。後續會繼續解密C++程式常用的記憶體管理庫的實現原理,包括ptmalloc,jemalloc,tcmalloc等,介紹當前業界流行的記憶體分配器如何管理C++程式的記憶體。瞭解記憶體分配器原理,更有助於工程師在實踐中降低處理記憶體使用問題的成本,根據系統量身打造應用層的記憶體管理體系。

一、Linux記憶體管理

Linux自底向上大致可以被劃分為:

  • 硬體(Physical Hardware)

  • 核心層(Kernel Space)

  • 使用者層(User Space)

圖片

△圖1:Linux結構

核心模組在核心空間中執行,應用程式在使用者空間中執行,二者的記憶體地址空間不重疊。這種方法確保在使用者空間中執行的應用程式具有一致的硬體檢視,而與硬體平臺無關。使用者空間通過使用系統呼叫以可控的方式使核心服務,如:陷入核心態,處理缺頁中斷。

Linux的記憶體管理系統自底向上大致可以被劃分為:

  • 核心層記憶體管理 : 在 Linux 核心中 , 通過記憶體分配函式管理記憶體:

  • kmalloc()/__get_free_pages():申請較小記憶體(kmalloc()以位元組為單位,__get_free_pages()以一頁128K為單位),申請的記憶體位於實體記憶體的對映區域,而且在物理上也是連續的,它們與真實的實體地址只有一個固定的偏移。

  • vmalloc():申請較大記憶體,虛擬記憶體空間給出一塊連續的記憶體區,但不保證實體記憶體連續,開銷遠大於__get_free_pages(),需要建立新的頁表。

  • 使用者層記憶體管理:通過呼叫系統呼叫函式(brk、mmap等),實現常用的記憶體管理介面(malloc, free, realloc, calloc)管理記憶體;經典記憶體管理庫ptmalloc2、tcmalloc、jemalloc。

  • 應用程式通過記憶體管理庫或直接呼叫系統記憶體管理函式分配記憶體,根據應用程式本身的程式特性進行使用,如:單個變數記憶體申請和釋放、記憶體池化複用等。

至此單個程序可以使用Linux提供的記憶體劃分順利的執行,從使用者程式來看Linux程序的記憶體模型大致如下所示:

圖片

△圖2:Linux程序的記憶體模型

  • 棧區(Stack):儲存程式執行期間的本地變數和函式的引數,從高地址向低地址生長

  • 堆區(Heap): 動態記憶體分配區域,通過malloc、new、free和delete等函式管理

在標準C庫中,提供了malloc/free函式分配釋放記憶體,這些函式的底層是基於brk/mmap這些系統呼叫實現的,對照圖2來看:

  • brk(): 用於申請和釋放小記憶體。資料段的末尾,堆記憶體的開始,叫做brk(program break)。通過設定heap的結束地址,將該地址向高或低移動實現堆記憶體的擴張或收縮。低地址記憶體必須在高地址記憶體的釋放之後才能得到的釋放,被標記為空閒區的低地址,無法被合併,如果後續再來記憶體空間的請求大於此空閒區,這部分將成為記憶體空洞。預設情況下,當最高地址空間的空閒記憶體超過128K(可由M_TRIM_THRESHOLD選項調節)時,執行記憶體緊縮操作(trim)。

  • mmap():用於申請大記憶體。mmap(memory map)是一種記憶體對映檔案的方法,即將一個檔案或者其它物件對映到程序的虛擬地址空間中(堆和棧中間的檔案對映區域 Memory Mapping Segment),實現檔案磁碟地址和程序虛擬地址空間中一段虛擬地址的一一對映關係。實現這樣的對映關係後,程序就可以採用指標的方式讀寫操作這一段記憶體,而系統會自動回寫髒頁面到對應的檔案磁碟上,核心空間對這段區域的修改也直接反映使用者空間,從而可以實現不同程序間的檔案共享。大於 128 K 的記憶體,使用系統呼叫mmap()分配記憶體。與 brk() 分配記憶體不同的是,mmap() 分配的記憶體可以單獨釋放。

  • munmp():釋放有mmap()建立的這段記憶體空間。

但在對於多個同時執行的程序,系統仍需處理有限的實體記憶體和增長的記憶體地址等問題。那麼當Linux存在多個同時執行的程序時,一次記憶體的分配過程具體都經過哪些過程呢?現代Linux系統上記憶體的分配主要過程如下[1] :

  1. 應用程式通過呼叫記憶體分配函式,系統呼叫brk或者mmap進行記憶體分配,申請虛擬記憶體地址空間。

  2. 虛擬記憶體至實體記憶體對映處理過程,通過請求MMU分配單元,根據虛擬地址計算出該地址所屬的頁面,再根據頁面對映表的起始地址計算出該頁面對映表(PageTable)項所在的實體地址,根據實體地址在快取記憶體的TLB中尋找該表項的內容,如果該表項不在TLB中,就從記憶體將其內容裝載到TLB中。

圖片

△圖3:Linux記憶體分配機制(虛擬+物理對映)

對於記憶體分配過程中涉及到工具進一步剖析:

  • 虛擬記憶體(Virtual Memory):現代作業系統普遍使用的一種技術,每個程序有用獨立的邏輯地址空間,記憶體被分為大小相等的多個塊,稱為頁(Page)。每個頁都是一段連續的地址,對應實體記憶體上的一塊稱為頁框,通常頁和頁框大小相等。虛擬記憶體使得多個虛擬頁面共享同一個物理頁面,而核心和使用者程序、不同使用者程序隔離。

  • MMU(Memory-Management Unit):記憶體管理單元,負責管理虛擬地址到實體地址的記憶體對映,實現各個使用者程序都擁有自己的獨立的地址空間,提供硬體機制的記憶體訪問許可權檢查,保護每個程序所用的記憶體不會被其他的程序所破壞。

  • PageTable:虛擬記憶體至實體記憶體頁面對映關係儲存單元。

  • TLB(Translation Lookaside Buffer):高速虛擬地址對映快取, 主要為了提升MMU地址對映處理效率,加了快取機制,如果存在即可直接取出對映地址供使用。

這裡要提到一個很重要的概念,記憶體的延遲分配,只有在真正訪問一個地址的時候才建立這個地址的物理對映,這是 Linux 記憶體管理的基本思想之一。Linux 核心在使用者申請記憶體的時候,只是分配了虛擬記憶體,並沒有分配實際實體記憶體;當用戶第一次使用這塊記憶體的時候,核心會發生缺頁中斷,分配實體記憶體,建立虛擬記憶體和實體記憶體之間的對映關係。當一個程序發生缺頁中斷的時候,程序會陷入核心態,執行以下操作:

  • 檢查要訪問的虛擬地址是否合法

  • 查詢/分配一個物理頁

  • 填充物理頁內容

  • 建立對映關係(虛擬地址到實體地址)

  • 重新執行觸發缺頁中斷的指令

如果填充物理頁的過程需要讀取磁碟,那這次缺頁中斷是majflt,否則是minflt。我們需要重點關注majflt的值,因為majflt對於效能的損害是致命的,隨機讀一次磁碟的耗時數量級在幾個毫秒,而minflt只有在大量的時候才會對效能產生影響。

二、總結

通過對Linux記憶體管理的介紹,我們可以看到記憶體管理需要解決的問題:

  • 呼叫系統提供的有限介面操作虛存讀寫

  • 權衡單次分配較大記憶體和多次分配較少記憶體帶來成本:控制缺頁中斷(尤其是majflt)vs 程序佔用過多記憶體

  • 降低記憶體碎片

  • 降低記憶體管理庫自身帶來的額外損耗

在接下來的幾篇文章將就ptmalloc,jemalloc,tcmalloc幾個經典記憶體管理庫,與大家進一步探討C++程式常用的記憶體管理庫的實現原理。

---------- END ----------

相關參考:

[1] 《Linux透明大頁機制在雲上大規模叢集實踐介紹》:https://mp.weixin.qq.com/s/hGjADS9tdHeqS9XR4pkh_w

[2] Writing a Linux Kernel Module — Part 1: Introduction: http://derekmolloy.ie/writing-a-linux-kernel-module-part-1-introduction/

[3] https://blog.csdn.net/alimingh/article/details/111942297

[4] https://zhuanlan.zhihu.com/p/469124915

[5] https://zhuanlan.zhihu.com/p/442426370

[6] https://blog.csdn.net/agonie201218/article/details/123791047

推薦閱讀【技術加油站】系列:

從零到一瞭解APP速度測評

百度工程師教你玩轉設計模式(工廠模式)

揭祕百度智慧測試在測試分析領域實踐

百度使用者產品流批一體的實時數倉實踐

ffplay影片播放原理分析

百度工程師眼中的雲原生可觀測性追蹤技術