malloc底層機(jī)制:brk與mmap如何選內(nèi)存分配?
brk 與 mmap 作為內(nèi)存分配的「雙引擎」,各自擁有獨特的運行機(jī)制和適用場景。brk 通過線性擴(kuò)展堆區(qū),在小額內(nèi)存分配場景中表現(xiàn)出輕快靈活的特點,能夠高效地滿足程序?qū)πK內(nèi)存的頻繁需求。而 mmap 則憑借其獨立映射的特性,在大塊內(nèi)存分配以及需要共享內(nèi)存、文件映射的復(fù)雜場景中展現(xiàn)出強(qiáng)大的穩(wěn)定性和靈活性 。理解這兩種內(nèi)存分配方式的底層機(jī)制,是開發(fā)者優(yōu)化程序性能、排查內(nèi)存泄漏問題的關(guān)鍵。
在工程實踐中,我們可以看到各種基于 brk 和 mmap 的優(yōu)化策略和內(nèi)存管理技術(shù)。內(nèi)存池技術(shù)通過預(yù)先分配和復(fù)用內(nèi)存,減少了系統(tǒng)調(diào)用的次數(shù),提高了內(nèi)存分配的效率,尤其適用于高頻小額內(nèi)存分配的場景 。碎片整理技術(shù)則通過定期整理內(nèi)存碎片,提高了內(nèi)存的利用率,減少了內(nèi)存碎片化對程序性能的影響 。這些技術(shù)的應(yīng)用,進(jìn)一步展示了 brk 和 mmap 在實際開發(fā)中的重要性和實用性 。
一、內(nèi)存分配的「雙引擎」:brk 與 mmap 核心原理
在深入探討brk與mmap之前,我們先來明確一個概念:在 Linux 系統(tǒng)中,內(nèi)存分配的核心系統(tǒng)調(diào)用主要就是brk和mmap ,它們是進(jìn)程獲取內(nèi)存的兩種關(guān)鍵方式,就像程序猿伸向內(nèi)存的兩只手,各自有著獨特的分工和技巧。接下來,就讓我們揭開它們神秘的面紗,看看它們是如何在內(nèi)存的舞臺上翩翩起舞的。
1.1 brk:堆區(qū)的線性擴(kuò)展引擎
brk 系統(tǒng)調(diào)用是進(jìn)程堆內(nèi)存管理的重要工具,其核心機(jī)制在于通過移動進(jìn)程堆頂指針(program break)來動態(tài)擴(kuò)展內(nèi)存空間。進(jìn)程啟動時,堆區(qū)位于數(shù)據(jù)段末端,隨著程序運行,當(dāng)需要更多內(nèi)存時,brk 會將堆頂指針向高地址移動,新分配的內(nèi)存便緊接在已有堆內(nèi)存之后,形成連續(xù)的線性區(qū)域。這一過程就好比在現(xiàn)有土地上進(jìn)行擴(kuò)建,不斷拓展可使用的空間。
圖片
從內(nèi)存分配的實際過程來看,brk 有著獨特的優(yōu)勢和特性。首先,brk 在進(jìn)行內(nèi)存分配時,僅修改虛擬內(nèi)存邊界,并不會立即分配物理內(nèi)存。只有當(dāng)進(jìn)程首次訪問新分配的虛擬內(nèi)存區(qū)域時,才會觸發(fā)缺頁中斷,此時操作系統(tǒng)才會真正分配物理內(nèi)存,并建立虛擬內(nèi)存與物理內(nèi)存之間的映射關(guān)系。這種按需分配的策略有效地避免了內(nèi)存的提前浪費,提高了內(nèi)存使用效率 。
其次,brk 分配的內(nèi)存是連續(xù)的,這在許多場景下都極為重要。例如,對于一些需要頻繁讀寫大塊連續(xù)數(shù)據(jù)的應(yīng)用,如數(shù)據(jù)庫緩存,連續(xù)的內(nèi)存空間可以顯著提高數(shù)據(jù)訪問速度,減少緩存未命中的次數(shù),因為連續(xù)內(nèi)存有利于提高緩存命中率,使得數(shù)據(jù)能夠更高效地在內(nèi)存與緩存之間傳輸 。此外,glibc 的 sbrk 函數(shù)對 brk 進(jìn)行了封裝,提供了更為便捷的增量分配接口。通過 sbrk,開發(fā)者可以直接指定增加或減少的內(nèi)存大小,而無需手動計算新的堆頂?shù)刂罚蟠蠛喕藘?nèi)存操作流程。
不過,brk 也存在一些局限性。由于其分配的內(nèi)存依賴堆頂指針的移動,釋放內(nèi)存時需按順序進(jìn)行,即高地址的內(nèi)存先釋放,低地址的內(nèi)存后釋放。這就導(dǎo)致如果中間部分的內(nèi)存被釋放,會形成內(nèi)存空洞,而這些空洞在后續(xù)的內(nèi)存分配中可能無法被充分利用,從而產(chǎn)生內(nèi)存碎片化問題。隨著程序不斷地進(jìn)行內(nèi)存分配和釋放操作,內(nèi)存碎片化可能會越來越嚴(yán)重,最終導(dǎo)致即使堆區(qū)還有足夠的空閑內(nèi)存,但由于碎片的存在,無法滿足較大內(nèi)存塊的分配需求,影響程序的正常運行。
這種分配方式有著自己獨特的特點:
- 虛擬內(nèi)存與物理內(nèi)存的延遲綁定:brk僅修改虛擬內(nèi)存邊界,并不會立即分配物理內(nèi)存。只有當(dāng)程序首次訪問這片新分配的虛擬內(nèi)存時,才會觸發(fā)缺頁中斷,操作系統(tǒng)這時才會真正分配物理內(nèi)存給進(jìn)程。這就像是你先規(guī)劃好了新房間的位置(設(shè)置虛擬內(nèi)存邊界),但還沒真正開始砌墻(分配物理內(nèi)存),直到有人要住進(jìn)去(首次訪問)才開始動工。
- 堆區(qū)內(nèi)存的順序釋放:分配的內(nèi)存屬于進(jìn)程堆區(qū),在釋放的時候需要按順序來,后分配的先釋放。就好比你擴(kuò)建的房間,你要拆除的時候,得從最后建的那間開始拆。
- 封裝與增量分配:在 glibc 中,sbrk函數(shù)是brk的封裝,它提供了增量分配接口。例如,sbrk(n)會將堆頂指針移動n個字節(jié),實現(xiàn)增量式的內(nèi)存分配。這就像是你每次可以一小部分一小部分地擴(kuò)建你的房子,非常靈活。
brk這種方式特別適合小塊內(nèi)存的快速分配,比如幾 KB 到幾十 KB 的內(nèi)存分配,因為它操作簡單,只需要移動一下堆頂指針就可以完成內(nèi)存分配的 “規(guī)劃”,速度非???。但它也有自己的局限性,比如容易產(chǎn)生內(nèi)存碎片,就像你不斷地在房子后面擴(kuò)建小房間,拆了又建,建了又拆,最后可能會剩下很多不規(guī)則的小塊空地(內(nèi)存碎片),很難再利用起來。
1.2 mmap:虛擬空間的獨立映射器
mmap 系統(tǒng)調(diào)用則開辟了另一種內(nèi)存分配的途徑,它在堆與棧之間的 “文件映射區(qū)” 創(chuàng)建獨立的內(nèi)存區(qū)域。當(dāng)調(diào)用 mmap 時,進(jìn)程可以指定映射的長度、權(quán)限(如設(shè)置為 MAP_ANONYMOUS 表示匿名映射,不與任何文件關(guān)聯(lián))等參數(shù),內(nèi)核會據(jù)此生成獨立的內(nèi)存管理單元(vm_area_struct)。這個內(nèi)存管理單元就像是一個獨立的 “小房間”,與堆區(qū)的內(nèi)存管理相互獨立,擁有自己的地址空間和權(quán)限設(shè)置 。參考這篇《超硬核,基于mmap和零拷貝實現(xiàn)高效的內(nèi)存共享》
圖片
mmap 的獨特優(yōu)勢使其在特定場景下表現(xiàn)出色。一方面,mmap 支持非連續(xù)內(nèi)存分配,這對于需要分配大塊內(nèi)存的場景尤為重要。當(dāng)程序需要申請一塊較大的內(nèi)存時,mmap 可以直接在文件映射區(qū)找到合適的空閑區(qū)域進(jìn)行分配,而不受堆區(qū)連續(xù)內(nèi)存的限制,避免了因堆區(qū)連續(xù)擴(kuò)容導(dǎo)致的整體膨脹和內(nèi)存碎片化問題。在大數(shù)據(jù)處理、圖形渲染等需要大量內(nèi)存的應(yīng)用中,mmap 能夠高效地滿足內(nèi)存需求,確保程序的穩(wěn)定運行。
另一方面,mmap 具有零拷貝特性,特別是在文件映射場景中,它可以直接將文件內(nèi)容映射到內(nèi)存中,進(jìn)程對文件的讀寫操作就如同對內(nèi)存的讀寫一樣,減少了數(shù)據(jù)在用戶空間和內(nèi)核空間之間的搬運開銷。例如,在文件傳輸過程中,傳統(tǒng)的 read/write 方式需要多次數(shù)據(jù)拷貝和系統(tǒng)調(diào)用,而 mmap 通過內(nèi)存映射,讓數(shù)據(jù)直接在內(nèi)存中進(jìn)行處理,大大提高了數(shù)據(jù)傳輸效率,減少了 CPU 的負(fù)載 。此外,mmap 分配的內(nèi)存釋放時不依賴堆區(qū)順序,通過 munmap 函數(shù)可以獨立地將內(nèi)存歸還給系統(tǒng),無論該內(nèi)存塊在映射區(qū)域中的位置如何,都能直接釋放,這使得內(nèi)存管理更加靈活,進(jìn)一步減少了內(nèi)存碎片化的風(fēng)險。
mmap的特點也十分顯著:
- 非連續(xù)內(nèi)存分配與大塊內(nèi)存支持:它支持非連續(xù)內(nèi)存分配,特別適合大塊內(nèi)存的分配,在大多數(shù)系統(tǒng)中,默認(rèn)超過 128KB 的內(nèi)存分配就會使用mmap。這就像你要建設(shè)一個大型工業(yè)園區(qū),不需要在已有的城市區(qū)域里一點點拼湊,而是可以直接在郊區(qū)劃出一大塊獨立的土地來建設(shè)。
- 零拷貝特性提升效率:mmap具有零拷貝特性,尤其是在文件映射場景中,它減少了數(shù)據(jù)搬運開銷。比如在讀取大文件時,傳統(tǒng)的read方式需要將數(shù)據(jù)從內(nèi)核緩沖區(qū)拷貝到用戶緩沖區(qū),而mmap可以直接將文件映射到用戶空間,進(jìn)程直接訪問映射內(nèi)存,就像你可以直接在工業(yè)園區(qū)里工作,而不需要把工業(yè)園區(qū)的產(chǎn)品先搬到家里再進(jìn)行處理,大大提高了效率。像 Kafka 在 Broker 讀寫 index 文件時就用了 mmap 零復(fù)制技術(shù),大大提升了數(shù)據(jù)處理的效率。
- 獨立釋放避免碎片化:mmap通過munmap可以獨立釋放內(nèi)存,避免了內(nèi)存碎片化問題。每個通過mmap分配的內(nèi)存區(qū)域就像一個獨立的小區(qū),你可以隨時拆除(釋放)任何一個小區(qū),而不會影響其他區(qū)域,不像brk分配的內(nèi)存,釋放時受到順序限制,容易產(chǎn)生碎片。
1.3 brk與 mmap 二者之間區(qū)別
brk(及 sbrk)和 mmap 是操作系統(tǒng)提供的兩種內(nèi)存分配相關(guān)的系統(tǒng)調(diào)用,主要區(qū)別體現(xiàn)在作用范圍、適用場景、內(nèi)存管理方式等方面。
- 操作的內(nèi)存區(qū)域不同。brk/sbrk 僅用于調(diào)整進(jìn)程堆的邊界,通過修改堆頂指針改變堆的大??;而 mmap 用于在虛擬地址空間中創(chuàng)建獨立的內(nèi)存區(qū)域,該區(qū)域與堆、棧等現(xiàn)有區(qū)域不連續(xù)。
- 適用的內(nèi)存大小場景不同。brk/sbrk 適合小塊內(nèi)存分配(如幾 KB 到幾十 KB),因堆內(nèi)存連續(xù),分配釋放開銷小,且釋放后可被 malloc 緩存復(fù)用;mmap 更適合大塊內(nèi)存分配(通常超 128KB),能避免堆內(nèi)存碎片,且釋放后直接歸還給操作系統(tǒng)。
- 內(nèi)存釋放與回收機(jī)制不同。brk 分配的堆內(nèi)存釋放后,會進(jìn)入 malloc 的空閑列表供后續(xù)復(fù)用,僅當(dāng)堆頂連續(xù)空閑內(nèi)存足夠大時才歸還給系統(tǒng);mmap 分配的匿名內(nèi)存釋放時,通過 munmap 直接歸還操作系統(tǒng),不再被進(jìn)程占用。
- 內(nèi)存地址連續(xù)性不同。brk 分配的內(nèi)存屬于堆的一部分,地址連續(xù);mmap 分配的內(nèi)存是獨立區(qū)域,地址與堆、棧等不連續(xù),各 mmap 區(qū)域間也可能離散。
二、核心區(qū)別:為什么 malloc 選擇「大小有別」?
通過對brk和mmap原理的剖析,我們已經(jīng)了解到它們各自的特點和工作方式。接下來,讓我們深入探討它們之間的核心區(qū)別,以及為什么malloc會根據(jù)內(nèi)存大小來選擇不同的底層實現(xiàn)機(jī)制。這就像是一個精密的儀器,不同的部件在不同的情況下發(fā)揮著最佳的作用,而malloc就像是這個儀器的智能控制系統(tǒng),根據(jù)不同的 “任務(wù)”(內(nèi)存分配需求)來選擇最合適的 “工具”(brk或mmap) 。
2.1 內(nèi)存布局與碎片化對比
從內(nèi)存布局的角度來看,brk和mmap有著顯著的區(qū)別。brk分配的內(nèi)存位于堆區(qū),是在數(shù)據(jù)段末端進(jìn)行線性擴(kuò)展的,就像一條不斷延伸的直線,所有分配的內(nèi)存塊緊密相連,依賴堆頂指針來管理內(nèi)存的邊界。這種方式在內(nèi)存釋放時,必須按照分配的順序,從高地址的內(nèi)存塊開始釋放。這就好比你在書架上依次擺放書籍,要拿走書籍時,也得從最后放上去的那本開始拿。如果中間釋放了某個內(nèi)存塊,就會在堆區(qū)留下一個空洞,后續(xù)的內(nèi)存分配如果大小不合適,就無法利用這個空洞,從而導(dǎo)致內(nèi)存碎片化。就像書架上拿走了中間的幾本書,留下了幾個不規(guī)則的空位,很難再找到合適大小的書籍來填補(bǔ)。
而mmap則在文件映射區(qū)創(chuàng)建獨立的內(nèi)存映射區(qū)域,每個區(qū)域就像一個獨立的小房間,它們之間可以是非連續(xù)的。在釋放內(nèi)存時,每個區(qū)域可以獨立進(jìn)行,不會受到其他區(qū)域的影響。這就好比你有多個獨立的小倉庫,你可以隨時關(guān)閉(釋放)任何一個倉庫,而不會影響其他倉庫的使用,大大降低了內(nèi)存碎片化的風(fēng)險。 就像你在不同的地方有多個小書架,每個書架都可以獨立管理,拿走某個書架上的書不會影響其他書架的布局,避免了出現(xiàn)像brk那樣的整體布局混亂(內(nèi)存碎片化)問題。
對比維度 | 堆區(qū)(brk) | 文件映射區(qū)(mmap) |
內(nèi)存釋放順序 | 順序釋放(高地址優(yōu)先) | 獨立釋放(任意區(qū)域) |
內(nèi)存碎片化程度 | 高(中間釋放形成空洞) | 低(單區(qū)域釋放無影響) |
內(nèi)存分配開銷 | 低(僅修改指針) | 中(創(chuàng)建映射結(jié)構(gòu)) |
2.2 系統(tǒng)調(diào)用開銷與適用場景
在系統(tǒng)調(diào)用開銷方面,brk和mmap也有著各自的特點。brk每次調(diào)用僅僅修改一個指針值,就像是你只需要在地圖上移動一個標(biāo)記來表示新的邊界,這種操作非常簡單快捷,系統(tǒng)調(diào)用開銷極低,大約只需要 100 納秒。這使得它非常適合高頻、小塊內(nèi)存的分配場景,比如鏈表節(jié)點的創(chuàng)建,這些節(jié)點通常只需要很小的內(nèi)存空間,而且可能會頻繁地創(chuàng)建和銷毀;還有臨時緩沖區(qū)的分配,這些緩沖區(qū)在程序運行過程中臨時使用,大小通常也不大,使用brk可以快速地分配和釋放內(nèi)存,提高程序的運行效率。
相比之下,mmap在創(chuàng)建內(nèi)存映射時,需要創(chuàng)建vm_area_struct結(jié)構(gòu)并建立復(fù)雜的映射關(guān)系,這就像是你要建立一個新的社區(qū),需要進(jìn)行詳細(xì)的規(guī)劃和建設(shè)。這種初始化過程的開銷較高,大約需要 500 納秒。但是,mmap的優(yōu)勢在于它的強(qiáng)大功能和穩(wěn)定性。它支持靈活的內(nèi)存釋放方式,適合大塊內(nèi)存的分配,比如緩沖區(qū)的創(chuàng)建,當(dāng)你需要一個較大的連續(xù)內(nèi)存空間來存儲大量數(shù)據(jù)時,mmap可以很好地滿足需求;在動態(tài)庫加載時,也通常會使用mmap,它可以將動態(tài)庫文件映射到進(jìn)程的虛擬地址空間,實現(xiàn)高效的共享和調(diào)用。此外,mmap在文件映射場景中表現(xiàn)出色,比如數(shù)據(jù)庫索引的加載,通過mmap可以直接將索引文件映射到內(nèi)存中,進(jìn)程可以像訪問內(nèi)存一樣快速訪問索引數(shù)據(jù),大大提高了數(shù)據(jù)庫的查詢效率。
2.3 malloc 的「策略選擇」
在 glibc 的malloc實現(xiàn)中,內(nèi)存分配大小與一個關(guān)鍵閾值(默認(rèn)是 128KB)的比較決定了底層采用的內(nèi)存分配機(jī)制。參考這篇《glibc堆內(nèi)存管理:原理、機(jī)制與實戰(zhàn)》
當(dāng)分配的內(nèi)存較?。ㄐ∮?128KB)時,malloc會選擇走brk擴(kuò)展堆區(qū)的方式。這是因為小塊內(nèi)存的分配和釋放操作可能會非常頻繁,而brk的低系統(tǒng)調(diào)用開銷可以很好地應(yīng)對這種高頻操作。同時,malloc利用空閑塊鏈表來復(fù)用內(nèi)存,比如 ptmalloc 的 fastbin 機(jī)制,它會將一些小塊內(nèi)存塊組織成一個快速分配鏈表,當(dāng)有新的小塊內(nèi)存分配請求時,優(yōu)先從這個鏈表中查找合適的內(nèi)存塊進(jìn)行分配,避免了頻繁的系統(tǒng)調(diào)用,進(jìn)一步提高了分配效率。這就像是你有一個小工具盒,里面有一些常用的小工具,每次需要使用小工具時,你可以直接從工具盒里快速找到,而不需要每次都去大倉庫(系統(tǒng)內(nèi)存)里尋找。
當(dāng)分配的內(nèi)存較大(大于等于 128KB)時,malloc會直接調(diào)用mmap來創(chuàng)建獨立的內(nèi)存映射。這是因為大塊內(nèi)存的分配如果使用brk,很容易導(dǎo)致堆區(qū)的碎片化,影響后續(xù)的內(nèi)存分配效率。而mmap的獨立內(nèi)存管理方式可以避免這個問題,雖然它的初始化開銷較高,但對于大塊內(nèi)存的一次性分配來說,這點開銷是可以接受的。這就好比你要建造一座大型建筑,雖然前期的規(guī)劃和準(zhǔn)備工作(初始化開銷)比較繁瑣,但建成后可以獨立使用,不會影響其他區(qū)域,也不會因為后續(xù)的一些小改動(內(nèi)存釋放和再分配)而導(dǎo)致整體結(jié)構(gòu)的混亂(內(nèi)存碎片化)。
三、實戰(zhàn)指南:如何正確使用 brk 與 mmap?
在了解了brk和mmap的原理以及它們在malloc中的應(yīng)用之后,接下來我們進(jìn)入實戰(zhàn)環(huán)節(jié),看看在實際編程中如何正確地使用它們,以及在使用過程中有哪些性能優(yōu)化技巧和常見陷阱需要注意。這就像是我們學(xué)會了理論知識之后,要親自上手實踐,在實踐中掌握這些內(nèi)存分配工具的使用技巧,讓它們?yōu)槲覀兊某绦蚋咝н\行保駕護(hù)航。
3.1 基礎(chǔ) API 使用示例
(1)brk/sbrk 調(diào)用
在 C 語言中,brk和sbrk函數(shù)用于操作堆內(nèi)存。brk函數(shù)直接設(shè)置堆頂指針,而sbrk函數(shù)則是在當(dāng)前堆頂指針的基礎(chǔ)上進(jìn)行增量調(diào)整。下面是一個簡單的示例,展示了如何使用sbrk來分配和釋放內(nèi)存:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main() {
// 使用sbrk分配1024字節(jié)內(nèi)存
char *p = (char *)sbrk(1024);
if (p == (void *)-1) {
perror("sbrk");
return 1;
}
// 使用分配的內(nèi)存
for (int i = 0; i < 1024; i++) {
p[i] = i;
}
// 輸出內(nèi)存中的前10個字節(jié)
for (int i = 0; i < 10; i++) {
printf("%d ", p[i]);
}
printf("\n");
// 使用sbrk釋放內(nèi)存(將堆頂指針回退1024字節(jié))
if (sbrk(-1024) == (void *)-1) {
perror("sbrk");
return 1;
}
return 0;
}在這個示例中,我們首先使用sbrk(1024)分配了 1024 字節(jié)的內(nèi)存,然后對這塊內(nèi)存進(jìn)行了初始化操作,最后通過sbrk(-1024)將堆頂指針回退 1024 字節(jié),實現(xiàn)了內(nèi)存的釋放。需要注意的是,在實際應(yīng)用中,直接使用brk和sbrk進(jìn)行內(nèi)存管理的情況比較少見,因為它們的操作相對底層,容易出錯,通常會使用更高級的內(nèi)存分配函數(shù),如malloc和free 。
(2)mmap/munmap 調(diào)用
mmap函數(shù)用于創(chuàng)建內(nèi)存映射,munmap函數(shù)則用于取消內(nèi)存映射。下面是一個使用mmap映射文件的示例:
#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main() {
int fd;
char *file_content;
struct stat file_stat;
// 打開文件
fd = open("test.txt", O_RDWR);
if (fd == -1) {
perror("open");
return 1;
}
// 獲取文件大小
if (fstat(fd, &file_stat) == -1) {
perror("fstat");
close(fd);
return 1;
}
// 使用mmap映射文件
file_content = (char *)mmap(0, file_stat.st_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (file_content == MAP_FAILED) {
perror("mmap");
close(fd);
return 1;
}
// 輸出文件內(nèi)容
printf("File content:\n%s\n", file_content);
// 修改文件內(nèi)容
strcpy(file_content, "This is a modified content.");
printf("Modified file content:\n%s\n", file_content);
// 取消映射
if (munmap(file_content, file_stat.st_size) == -1) {
perror("munmap");
}
// 關(guān)閉文件
close(fd);
return 0;
}在這個示例中,我們首先打開一個文件,獲取其大小,然后使用mmap將文件內(nèi)容映射到內(nèi)存中。通過返回的指針,我們可以像訪問普通內(nèi)存一樣訪問文件內(nèi)容,對其進(jìn)行讀取和修改。最后,使用munmap取消內(nèi)存映射,并關(guān)閉文件描述符。這個示例展示了mmap在文件映射場景中的基本用法,通過這種方式,可以大大提高文件的讀寫效率,尤其是在處理大文件時。
3.2 性能優(yōu)化技巧
在實際使用brk和mmap時,合理的性能優(yōu)化可以顯著提升程序的運行效率。以下是一些常見的性能優(yōu)化技巧:
小塊內(nèi)存復(fù)用:對于頻繁分配釋放的小塊內(nèi)存,如網(wǎng)絡(luò)請求臨時數(shù)據(jù),直接調(diào)用brk會導(dǎo)致大量的系統(tǒng)調(diào)用開銷。此時,改用內(nèi)存池(如 tcmalloc 的 thread - local cache)是一個很好的選擇。內(nèi)存池通過預(yù)先分配一塊較大的內(nèi)存,然后在內(nèi)部維護(hù)一個或多個鏈表,用于快速分配和回收小塊內(nèi)存。這樣可以減少系統(tǒng)調(diào)用的次數(shù),提高內(nèi)存分配和釋放的效率。例如,在一個高并發(fā)的網(wǎng)絡(luò)服務(wù)器中,每個網(wǎng)絡(luò)請求可能只需要幾 KB 的內(nèi)存來存儲臨時數(shù)據(jù),如果每次都直接調(diào)用brk來分配內(nèi)存,會導(dǎo)致大量的系統(tǒng)調(diào)用開銷,影響服務(wù)器的性能。而使用內(nèi)存池,就可以在內(nèi)存池中快速分配和回收這些小塊內(nèi)存,避免了頻繁的系統(tǒng)調(diào)用。
大塊內(nèi)存對齊:在使用mmap分配大塊內(nèi)存時,可以指定MAP_POPULATE標(biāo)志來預(yù)分配物理內(nèi)存。這樣可以減少后續(xù)訪問這些區(qū)域時觸發(fā)缺頁中斷的可能性,提高訪問速度。另外,利用大頁(Huge Pages)也是一種優(yōu)化方式。大頁可以減少頁表的大小,降低 TLB(Translation Lookaside Buffer)缺失的概率,從而提升內(nèi)存訪問效率。在一些對內(nèi)存訪問性能要求極高的應(yīng)用中,如數(shù)據(jù)庫系統(tǒng),經(jīng)常會使用大頁來提高內(nèi)存訪問速度。例如,在 MySQL 數(shù)據(jù)庫中,可以通過配置參數(shù)來啟用大頁,提高數(shù)據(jù)庫的性能。
碎片整理:對于brk分配的內(nèi)存,可以通過mallopt(M_TRIM_THRESHOLD, 0)強(qiáng)制brk釋放超過閾值的空閑內(nèi)存,減少內(nèi)存碎片的產(chǎn)生。對于mmap分配的內(nèi)存,可以使用madvise函數(shù)對內(nèi)存區(qū)域進(jìn)行預(yù)取 / 丟棄優(yōu)化。例如,當(dāng)應(yīng)用程序知道將來會使用某些數(shù)據(jù)時,可以通過madvise建議操作系統(tǒng)提前加載這些數(shù)據(jù)到內(nèi)存中,提高數(shù)據(jù)訪問的效率;當(dāng)應(yīng)用程序不再需要某些數(shù)據(jù)時,可以通過madvise告知內(nèi)核釋放內(nèi)存,優(yōu)化內(nèi)存使用。在一個視頻播放應(yīng)用中,在播放視頻前,可以通過madvise預(yù)取視頻數(shù)據(jù),避免播放過程中出現(xiàn)卡頓;在視頻播放結(jié)束后,可以通過madvise釋放不再需要的內(nèi)存,提高系統(tǒng)的內(nèi)存利用率。
3.3 常見陷阱與避坑
在使用brk和mmap的過程中,也存在一些常見的陷阱,如果不注意,可能會導(dǎo)致程序出現(xiàn)內(nèi)存泄漏、性能下降等問題。以下是一些常見的陷阱及避免方法:
brk 的「釋放限制」:在使用brk釋放堆內(nèi)存時,只能回退到最近一次分配的高地址,若中間有未釋放塊則無法縮減。例如,先進(jìn)行 A 分配,再進(jìn)行 B 分配,然后釋放 A,此時會導(dǎo)致內(nèi)存空洞,只有等 B 釋放后才能整體回收。這就像是你在書架上依次放書,先放了 A 書,再放了 B 書,當(dāng)你想拿走 A 書時,如果 B 書還在,就無法直接拿走 A 書所在的那一層空間,必須先拿走 B 書,才能真正釋放 A 書占用的空間。為了避免這種情況,在設(shè)計內(nèi)存分配策略時,需要充分考慮內(nèi)存的釋放順序,盡量避免出現(xiàn)中間有未釋放塊的情況。
mmap 的「泄漏風(fēng)險」:如果忘記調(diào)用munmap取消內(nèi)存映射,會導(dǎo)致虛擬內(nèi)存泄漏。雖然這種泄漏不會占用物理內(nèi)存,但會消耗地址空間,最終可能導(dǎo)致進(jìn)程無法再分配新的內(nèi)存??梢酝ㄟ^pmap pid命令查看進(jìn)程的映射情況,排查是否存在未釋放的內(nèi)存映射。這就像是你租了一間房子,住完后卻忘記退房,雖然房子里沒有人住,但別人也無法再租用這間房子,造成了資源的浪費。在編寫程序時,一定要確保在不再需要內(nèi)存映射時,及時調(diào)用munmap進(jìn)行釋放。
權(quán)限控制:使用mmap時,可以設(shè)置PROT_NONE權(quán)限來創(chuàng)建一個保護(hù)區(qū)域,捕獲非法訪問,如緩沖區(qū)溢出。但是,這需要配合信號處理(SIGSEGV)使用,當(dāng)發(fā)生非法訪問時,系統(tǒng)會發(fā)送SIGSEGV信號,程序可以通過捕獲這個信號來進(jìn)行相應(yīng)的處理,如記錄錯誤日志、進(jìn)行錯誤恢復(fù)等。這就像是你在房子周圍設(shè)置了一圈警戒線,當(dāng)有人非法闖入時,就會觸發(fā)警報,你可以根據(jù)警報進(jìn)行相應(yīng)的處理。在使用mmap時,合理設(shè)置權(quán)限和信號處理,可以提高程序的安全性和穩(wěn)定性。
四、內(nèi)存分配之道
4.1 操作系統(tǒng)的「平衡之道」
brk與mmap的設(shè)計精妙地體現(xiàn)了操作系統(tǒng)在「效率」與「靈活」之間的權(quán)衡智慧。brk通過簡單地移動堆頂指針來分配內(nèi)存,這種方式雖然在靈活性上有所欠缺,例如它只能分配連續(xù)的內(nèi)存空間,釋放內(nèi)存時也受到嚴(yán)格的順序限制,但它卻在高頻內(nèi)存分配操作中展現(xiàn)出了極高的效率。就像在一個小型工廠里,所有的生產(chǎn)流程都非常簡單直接,雖然不能生產(chǎn)出非常復(fù)雜多樣的產(chǎn)品,但是對于一些常規(guī)的、大量需求的簡單產(chǎn)品,卻能以最快的速度生產(chǎn)出來。這種特性使得brk特別適合那些對內(nèi)存分配速度要求極高,且內(nèi)存需求相對較小且連續(xù)的場景,比如在一個頻繁創(chuàng)建和銷毀小對象的程序中,brk能夠快速地為這些小對象分配內(nèi)存,保證程序的高效運行。
而mmap則走向了另一個方向,它放棄了單一連續(xù)空間的限制,允許創(chuàng)建獨立的內(nèi)存映射區(qū)域。這就好比一個大型的綜合性工廠,它可以生產(chǎn)各種復(fù)雜的、多樣化的產(chǎn)品,每個產(chǎn)品的生產(chǎn)流程都可以獨立進(jìn)行。mmap的這種特性賦予了它強(qiáng)大的靈活性,它可以實現(xiàn)內(nèi)存的獨立管理和共享,特別適合那些對內(nèi)存管理靈活性要求較高,且內(nèi)存需求較大的場景,比如在跨進(jìn)程通信中,mmap可以創(chuàng)建共享內(nèi)存區(qū)域,讓多個進(jìn)程能夠高效地共享數(shù)據(jù);在處理大文件時,mmap可以將文件直接映射到內(nèi)存中,實現(xiàn)高效的文件讀寫操作。
這種在不同維度上的權(quán)衡與設(shè)計,不僅僅體現(xiàn)在內(nèi)存分配領(lǐng)域,在整個操作系統(tǒng)的設(shè)計中都有著廣泛的體現(xiàn)。以 TCP/IP 協(xié)議棧為例,BSD socket 和 raw socket 就是這種分層設(shè)計思想的典型體現(xiàn)。BSD socket 為應(yīng)用層提供了一個相對高層、抽象的接口,它隱藏了底層網(wǎng)絡(luò)協(xié)議的許多細(xì)節(jié),使得應(yīng)用程序可以方便快捷地進(jìn)行網(wǎng)絡(luò)通信,就像使用一個已經(jīng)組裝好的工具,只需要簡單操作就能完成任務(wù),這體現(xiàn)了對效率的追求。而 raw socket 則允許開發(fā)者直接訪問底層的網(wǎng)絡(luò)協(xié)議,能夠?qū)W(wǎng)絡(luò)數(shù)據(jù)包進(jìn)行更加精細(xì)的控制,雖然使用起來相對復(fù)雜,但卻提供了極大的靈活性,適用于一些對網(wǎng)絡(luò)通信有特殊需求的場景,比如網(wǎng)絡(luò)協(xié)議分析工具的開發(fā)。
同樣,在文件系統(tǒng)的設(shè)計中,ext4 和 f2fs 也展現(xiàn)了類似的分層設(shè)計思想。ext4 是一種廣泛使用的文件系統(tǒng),它在設(shè)計上注重兼容性和穩(wěn)定性,采用了傳統(tǒng)的文件系統(tǒng)結(jié)構(gòu),對于大多數(shù)常規(guī)的文件存儲和訪問需求,都能提供高效的支持,這體現(xiàn)了對效率的保障。而 f2fs 則是一種專門為閃存設(shè)備設(shè)計的文件系統(tǒng),它針對閃存的特性進(jìn)行了優(yōu)化,采用了更加靈活的結(jié)構(gòu),能夠更好地適應(yīng)閃存的讀寫特點,提高閃存設(shè)備的使用壽命和性能,這體現(xiàn)了對特定場景下靈活性的追求。
4.2 現(xiàn)代內(nèi)存分配器的「融合創(chuàng)新」
以 glibc 的 ptmalloc、Google 的 tcmalloc 為代表的現(xiàn)代內(nèi)存分配器,巧妙地采用了「brk + mmap 混合策略」,將brk和mmap的優(yōu)勢發(fā)揮到了極致。
對于小對象(一般小于 64KB) ,這些分配器通常會利用線程本地緩存來進(jìn)行分配。以 TCMalloc 的 thread cache 為例,每個線程都有自己獨立的緩存,當(dāng)線程需要分配小對象時,可以直接從自己的緩存中獲取內(nèi)存,避免了鎖競爭。這就好比每個員工都有自己的小工具盒,當(dāng)需要使用小工具時,直接從自己的工具盒里拿取,不需要和其他員工爭搶大倉庫里的工具,大大提高了分配的效率。
當(dāng)面對中對象(64KB - 128KB)時,分配器會通過brk從堆區(qū)分配內(nèi)存。在這個過程中,分配器會利用空閑塊合并的技術(shù),將相鄰的空閑內(nèi)存塊合并成更大的內(nèi)存塊,減少內(nèi)存碎片的產(chǎn)生。這就像是在整理倉庫時,將相鄰的小空位合并成一個大空位,以便更好地利用空間。例如,ptmalloc 會維護(hù)不同大小的空閑塊鏈表,當(dāng)有新的內(nèi)存分配請求時,會首先在合適的鏈表中查找是否有可用的空閑塊,如果有則直接分配,否則會嘗試合并相鄰的空閑塊來滿足請求。
而對于大對象(大于 128KB),分配器會直接使用mmap來分配內(nèi)存。由于大對象的內(nèi)存需求較大,如果使用brk分配,很容易導(dǎo)致堆區(qū)的碎片化,影響后續(xù)的內(nèi)存分配效率。而mmap的獨立內(nèi)存管理方式可以避免這個問題,雖然它的初始化開銷較高,但對于大對象的一次性分配來說,這點開銷是可以接受的。這就好比建造大型建筑,雖然前期的規(guī)劃和準(zhǔn)備工作比較繁瑣,但建成后可以獨立使用,不會影響其他區(qū)域,也不會因為后續(xù)的一些小改動而導(dǎo)致整體結(jié)構(gòu)的混亂。
這種混合策略的設(shè)計,不僅兼顧了性能與穩(wěn)定性,還將底層系統(tǒng)調(diào)用的細(xì)節(jié)封裝起來,向上層應(yīng)用提供了統(tǒng)一的malloc/free接口。應(yīng)用程序在進(jìn)行內(nèi)存分配時,不需要關(guān)心底層到底是使用brk還是mmap,只需要調(diào)用malloc函數(shù)即可,這大大簡化了開發(fā)者的工作,同時也提高了程序的可移植性和可維護(hù)性。
4.3 開發(fā)者的「選擇原則」
在實際的開發(fā)過程中,開發(fā)者需要根據(jù)不同的業(yè)務(wù)場景和需求,合理地選擇內(nèi)存分配方式。
優(yōu)先使用標(biāo)準(zhǔn)庫接口:在大多數(shù)情況下,開發(fā)者應(yīng)該優(yōu)先使用標(biāo)準(zhǔn)庫提供的malloc/free接口。這些接口經(jīng)過了大量的測試和優(yōu)化,能夠根據(jù)內(nèi)存分配的大小自動選擇合適的底層實現(xiàn)機(jī)制(brk或mmap) 。除非開發(fā)者有特殊的需求,例如需要實現(xiàn)自定義的內(nèi)存分配器,如在游戲引擎中,為了提高內(nèi)存管理的效率和性能,常常會實現(xiàn)自己的內(nèi)存池,否則直接使用標(biāo)準(zhǔn)庫接口是最簡便、最安全的選擇。
關(guān)注業(yè)務(wù)場景:業(yè)務(wù)場景是選擇內(nèi)存分配方式的重要依據(jù)。對于高頻小塊內(nèi)存的分配場景,例如在一個網(wǎng)絡(luò)服務(wù)器中,每個網(wǎng)絡(luò)請求可能只需要幾 KB 的內(nèi)存來存儲臨時數(shù)據(jù),這種情況下應(yīng)該盡量避免使用mmap,因為mmap的高開銷會嚴(yán)重影響系統(tǒng)的性能,而brk則是更好的選擇。相反,對于大塊內(nèi)存的分配或者需要共享內(nèi)存的場景,比如在跨進(jìn)程通信中,需要創(chuàng)建共享內(nèi)存區(qū)域來實現(xiàn)數(shù)據(jù)的共享,這時就應(yīng)該優(yōu)先使用mmap,因為它能夠提供獨立的內(nèi)存管理和共享能力,滿足業(yè)務(wù)的需求。
性能 profiling:為了確保內(nèi)存分配的效率,開發(fā)者可以通過性能分析工具來監(jiān)控系統(tǒng)調(diào)用的頻率。例如,可以使用perf trace -e mmap或strace -f -e brk,mmap命令來監(jiān)控mmap和brk系統(tǒng)調(diào)用的頻率,從而定位到內(nèi)存分配的低效點。通過分析這些數(shù)據(jù),開發(fā)者可以針對性地優(yōu)化內(nèi)存分配策略,提高程序的性能。比如,如果發(fā)現(xiàn)某個模塊中頻繁地調(diào)用mmap來分配小塊內(nèi)存,就可以考慮優(yōu)化該模塊的內(nèi)存分配方式,改為使用brk或者內(nèi)存池,以降低系統(tǒng)調(diào)用的開銷,提高程序的運行效率。































