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