在互聯(lián)網(wǎng)的服務(wù)中,C++常用于搭建高性能、高并發(fā)、大流量、低延時的后端服務(wù)。如何合理的分配內(nèi)存滿足系統(tǒng)高性能需求是一個高頻且重要的話題,而且因為內(nèi)存自身的特點和實際問題的復(fù)雜,組合出了諸多難題。
我們可以對內(nèi)存進(jìn)行多種類型的劃分,從內(nèi)存申請大小來看:
小對象分配:小于4倍內(nèi)存頁大小的內(nèi)存分配,在4KiB頁大小情況下,<16KiB算作小對象分配;
大對象分配:大于等于4倍內(nèi)存頁大小的內(nèi)存分配,在4KiB頁大小情況下,>=16KiB算作大對象分配。
從一塊內(nèi)存的被持有時長來看:
后端一次請求內(nèi)甚至更短時間申請和釋放
任意時間窗口內(nèi)內(nèi)存持有和更新
幾乎與應(yīng)用進(jìn)程等長的內(nèi)存持有和更新
某個進(jìn)程消亡后一段時間內(nèi),由該進(jìn)程申請的仍具有意義的內(nèi)存持有和釋放
當(dāng)然還可以按照內(nèi)存申請釋放頻率、讀寫頻率進(jìn)行進(jìn)一步的分類。
內(nèi)存管理服務(wù)于應(yīng)用系統(tǒng),目的是協(xié)助系統(tǒng)更好的解決瓶頸問題,比如對于『如何降低后端響應(yīng)的延遲和提高穩(wěn)定性』內(nèi)存管理可能要考慮的是:
處理內(nèi)存讀寫并發(fā)(讀頻繁or寫頻繁)降低響應(yīng)時間和CPU消耗
應(yīng)用層的內(nèi)存的池化復(fù)用
底層內(nèi)存向系統(tǒng)申請的內(nèi)存塊大小及內(nèi)存碎片化
每一個問題展開可能都是一個比較大的話題,本文介紹Linux C++程序內(nèi)存管理的理論基礎(chǔ)。了解內(nèi)存分配器原理,更有助于工程師在實踐中降低處理內(nèi)存使用問題的成本,根據(jù)系統(tǒng)量身打造應(yīng)用層的內(nèi)存管理體系。
一、Linux內(nèi)存管理
GEEK TALK
Linux自底向上大致可以被劃分為:
硬件(Physical Hardware)
內(nèi)核層(Kernel Space)
用戶層(User Space)
△圖1:Linux結(jié)構(gòu)
內(nèi)核模塊在內(nèi)核空間中運行,應(yīng)用程序在用戶空間中運行,二者的內(nèi)存地址空間不重疊。這種方法確保在用戶空間中運行的應(yīng)用程序具有一致的硬件視圖,而與硬件平臺無關(guān)。用戶空間通過使用系統(tǒng)調(diào)用以可控的方式使內(nèi)核服務(wù),如:陷入內(nèi)核態(tài),處理缺頁中斷。
Linux的內(nèi)存管理系統(tǒng)自底向上大致可以被劃分為:
內(nèi)核層內(nèi)存管理 :?在 Linux 內(nèi)核中 , 通過內(nèi)存分配函數(shù)管理內(nèi)存:
kmalloc()/__get_free_pages():申請較小內(nèi)存(kmalloc()以字節(jié)為單位,__get_free_pages()以一頁128K為單位),申請的內(nèi)存位于物理內(nèi)存的映射區(qū)域,而且在物理上也是連續(xù)的,它們與真實的物理地址只有一個固定的偏移。
vmalloc():申請較大內(nèi)存,虛擬內(nèi)存空間給出一塊連續(xù)的內(nèi)存區(qū),但不保證物理內(nèi)存連續(xù),開銷遠(yuǎn)大于__get_free_pages(),需要建立新的頁表。
用戶層內(nèi)存管理:通過調(diào)用系統(tǒng)調(diào)用函數(shù)(brk、mmap等),實現(xiàn)常用的內(nèi)存管理接口(malloc, free, realloc, calloc)管理內(nèi)存;經(jīng)典內(nèi)存管理庫ptmalloc2、tcmalloc、jemalloc。
應(yīng)用程序通過內(nèi)存管理庫或直接調(diào)用系統(tǒng)內(nèi)存管理函數(shù)分配內(nèi)存,根據(jù)應(yīng)用程序本身的程序特性進(jìn)行使用,如:單個變量內(nèi)存申請和釋放、內(nèi)存池化復(fù)用等。
至此單個進(jìn)程可以使用Linux提供的內(nèi)存劃分順利的運行,從用戶程序來看Linux進(jìn)程的內(nèi)存模型大致如下所示:
△圖2:Linux進(jìn)程的內(nèi)存模型
棧區(qū)(Stack):存儲程序執(zhí)行期間的本地變量和函數(shù)的參數(shù),從高地址向低地址生長
堆區(qū)(Heap): 動態(tài)內(nèi)存分配區(qū)域,通過malloc、new、free和delete等函數(shù)管理
在標(biāo)準(zhǔn)C庫中,提供了malloc/free函數(shù)分配釋放內(nèi)存,這些函數(shù)的底層是基于brk/mmap這些系統(tǒng)調(diào)用實現(xiàn)的,對照圖2來看:
brk(): 用于申請和釋放小內(nèi)存。數(shù)據(jù)段的末尾,堆內(nèi)存的開始,叫做brk(program break)。通過設(shè)置heap的結(jié)束地址,將該地址向高或低移動實現(xiàn)堆內(nèi)存的擴(kuò)張或收縮。低地址內(nèi)存必須在高地址內(nèi)存的釋放之后才能得到的釋放,被標(biāo)記為空閑區(qū)的低地址,無法被合并,如果后續(xù)再來內(nèi)存空間的請求大于此空閑區(qū),這部分將成為內(nèi)存空洞。默認(rèn)情況下,當(dāng)最高地址空間的空閑內(nèi)存超過128K(可由M_TRIM_THRESHOLD選項調(diào)節(jié))時,執(zhí)行內(nèi)存緊縮操作(trim)。
mmap():用于申請大內(nèi)存。mmap(memory map)是一種內(nèi)存映射文件的方法,即將一個文件或者其它對象映射到進(jìn)程的虛擬地址空間中(堆和棧中間的文件映射區(qū)域 Memory Mapping Segment),實現(xiàn)文件磁盤地址和進(jìn)程虛擬地址空間中一段虛擬地址的一一對映關(guān)系。實現(xiàn)這樣的映射關(guān)系后,進(jìn)程就可以采用指針的方式讀寫操作這一段內(nèi)存,而系統(tǒng)會自動回寫臟頁面到對應(yīng)的文件磁盤上,內(nèi)核空間對這段區(qū)域的修改也直接反映用戶空間,從而可以實現(xiàn)不同進(jìn)程間的文件共享。大于 128 K 的內(nèi)存,使用系統(tǒng)調(diào)用mmap()分配內(nèi)存。與 brk() 分配內(nèi)存不同的是,mmap() 分配的內(nèi)存可以單獨釋放。
munmp():釋放有mmap()創(chuàng)建的這段內(nèi)存空間。
但在對于多個同時運行的進(jìn)程,系統(tǒng)仍需處理有限的物理內(nèi)存和增長的內(nèi)存地址等問題。那么當(dāng)Linux存在多個同時運行的進(jìn)程時,一次內(nèi)存的分配過程具體都經(jīng)過哪些過程呢?現(xiàn)代Linux系統(tǒng)上內(nèi)存的分配主要過程如下[1] :
應(yīng)用程序通過調(diào)用內(nèi)存分配函數(shù),系統(tǒng)調(diào)用brk或者mmap進(jìn)行內(nèi)存分配,申請?zhí)摂M內(nèi)存地址空間。
虛擬內(nèi)存至物理內(nèi)存映射處理過程,通過請求MMU分配單元,根據(jù)虛擬地址計算出該地址所屬的頁面,再根據(jù)頁面映射表的起始地址計算出該頁面映射表(PageTable)項所在的物理地址,根據(jù)物理地址在高速緩存的TLB中尋找該表項的內(nèi)容,如果該表項不在TLB中,就從內(nèi)存將其內(nèi)容裝載到TLB中。
△圖3:Linux內(nèi)存分配機(jī)制(虛擬+物理映射)
對于內(nèi)存分配過程中涉及到工具進(jìn)一步剖析:
虛擬內(nèi)存(Virtual Memory):現(xiàn)代操作系統(tǒng)普遍使用的一種技術(shù),每個進(jìn)程有用獨立的邏輯地址空間,內(nèi)存被分為大小相等的多個塊,稱為頁(Page)。每個頁都是一段連續(xù)的地址,對應(yīng)物理內(nèi)存上的一塊稱為頁框,通常頁和頁框大小相等。虛擬內(nèi)存使得多個虛擬頁面共享同一個物理頁面,而內(nèi)核和用戶進(jìn)程、不同用戶進(jìn)程隔離。
MMU(Memory-Management Unit):內(nèi)存管理單元,負(fù)責(zé)管理虛擬地址到物理地址的內(nèi)存映射,實現(xiàn)各個用戶進(jìn)程都擁有自己的獨立的地址空間,提供硬件機(jī)制的內(nèi)存訪問權(quán)限檢查,保護(hù)每個進(jìn)程所用的內(nèi)存不會被其他的進(jìn)程所破壞。
PageTable:虛擬內(nèi)存至物理內(nèi)存頁面映射關(guān)系存儲單元。
TLB(Translation Lookaside Buffer):高速虛擬地址映射緩存, 主要為了提升MMU地址映射處理效率,加了緩存機(jī)制,如果存在即可直接取出映射地址供使用。
這里要提到一個很重要的概念,內(nèi)存的延遲分配,只有在真正訪問一個地址的時候才建立這個地址的物理映射,這是 Linux 內(nèi)存管理的基本思想之一。Linux 內(nèi)核在用戶申請內(nèi)存的時候,只是分配了虛擬內(nèi)存,并沒有分配實際物理內(nèi)存;當(dāng)用戶第一次使用這塊內(nèi)存的時候,內(nèi)核會發(fā)生缺頁中斷,分配物理內(nèi)存,建立虛擬內(nèi)存和物理內(nèi)存之間的映射關(guān)系。
當(dāng)一個進(jìn)程發(fā)生缺頁中斷的時候,進(jìn)程會陷入內(nèi)核態(tài),執(zhí)行以下操作:
檢查要訪問的虛擬地址是否合法
查找/分配一個物理頁
填充物理頁內(nèi)容
建立映射關(guān)系(虛擬地址到物理地址)
重新執(zhí)行觸發(fā)缺頁中斷的指令
如果填充物理頁的過程需要讀取磁盤,那這次缺頁中斷是majflt,否則是minflt。我們需要重點關(guān)注majflt的值,因為majflt對于性能的損害是致命的,隨機(jī)讀一次磁盤的耗時數(shù)量級在幾個毫秒,而minflt只有在大量的時候才會對性能產(chǎn)生影響。
二、總結(jié)
GEEK TALK
通過對Linux內(nèi)存管理的介紹,我們可以看到內(nèi)存管理需要解決的問題:
調(diào)用系統(tǒng)提供的有限接口操作虛存讀寫
權(quán)衡單次分配較大內(nèi)存和多次分配較少內(nèi)存帶來成本:控制缺頁中斷(尤其是majflt)vs 進(jìn)程占用過多內(nèi)存
降低內(nèi)存碎片
降低內(nèi)存管理庫自身帶來的額外損耗
審核編輯:湯梓紅
評論
查看更多