在互聯網的服務中,C++常用于搭建高性能、高并發、大流量、低延時的后端服務。如何合理的分配內存滿足系統高性能需求是一個高頻且重要的話題,而且因為內存自身的特點和實際問題的復雜,組合出了諸多難題。
我們可以對內存進行多種類型的劃分,從內存申請大小來看:
小對象分配:小于4倍內存頁大小的內存分配,在4KiB頁大小情況下,<16KiB算作小對象分配;
大對象分配:大于等于4倍內存頁大小的內存分配,在4KiB頁大小情況下,>=16KiB算作大對象分配。
從一塊內存的被持有時長來看:
后端一次請求內甚至更短時間申請和釋放
任意時間窗口內內存持有和更新
幾乎與應用進程等長的內存持有和更新
某個進程消亡后一段時間內,由該進程申請的仍具有意義的內存持有和釋放
當然還可以按照內存申請釋放頻率、讀寫頻率進行進一步的分類。
內存管理服務于應用系統,目的是協助系統更好的解決瓶頸問題,比如對于『如何降低后端響應的延遲和提高穩定性』內存管理可能要考慮的是:
處理內存讀寫并發(讀頻繁or寫頻繁)降低響應時間和CPU消耗
應用層的內存的池化復用
底層內存向系統申請的內存塊大小及內存碎片化
每一個問題展開可能都是一個比較大的話題,本文作為系列文章《探秘C++內存管理》的開篇,先介紹Linux C++程序內存管理的理論基礎。后續會繼續解密C++程序常用的內存管理庫的實現原理,包括ptmalloc,jemalloc,tcmalloc等,介紹當前業界流行的內存分配器如何管理C++程序的內存。了解內存分配器原理,更有助于工程師在實踐中降低處理內存使用問題的成本,根據系統量身打造應用層的內存管理體系。
一、Linux內存管理
GEEK TALK
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] :
應用程序通過調用內存分配函數,系統調用brk或者mmap進行內存分配,申請虛擬內存地址空間。
虛擬內存至物理內存映射處理過程,通過請求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只有在大量的時候才會對性能產生影響。
二、總結
GEEK TALK
通過對Linux內存管理的介紹,我們可以看到內存管理需要解決的問題:
調用系統提供的有限接口操作虛存讀寫
權衡單次分配較大內存和多次分配較少內存帶來成本:控制缺頁中斷(尤其是majflt)vs 進程占用過多內存
降低內存碎片
降低內存管理庫自身帶來的額外損耗
在接下來的幾篇文章將就ptmalloc,jemalloc,tcmalloc幾個經典內存管理庫,與大家進一步探討C++程序常用的內存管理庫的實現原理。
審核編輯:湯梓紅
評論
查看更多