探索Linux操作系統(tǒng):利用malloc提升程序性能
在 Linux 操作系統(tǒng)的廣袤天地里,內(nèi)存管理宛如一場(chǎng)精密的舞蹈,而 malloc 函數(shù)無(wú)疑是其中最為關(guān)鍵的舞者。當(dāng)我們編寫(xiě) C 或 C++ 程序時(shí),常常會(huì)遇到需要在運(yùn)行時(shí)動(dòng)態(tài)分配內(nèi)存的情況,比如創(chuàng)建一個(gè)大小不確定的數(shù)組,或者構(gòu)建復(fù)雜的數(shù)據(jù)結(jié)構(gòu)。
此時(shí),malloc 就像一位神奇的工匠,為我們打造出所需的內(nèi)存空間。它允許程序在運(yùn)行過(guò)程中根據(jù)實(shí)際需求靈活地獲取內(nèi)存,大大增強(qiáng)了程序的適應(yīng)性和靈活性。但你是否曾好奇,malloc 究竟是如何在幕后運(yùn)作,精準(zhǔn)地分配出我們所需的內(nèi)存呢?接下來(lái),就讓我們一同揭開(kāi) malloc 的神秘面紗,深入探尋其內(nèi)存分配的奧秘。
一、Malloc基礎(chǔ)入門(mén)
1.1malloc 函數(shù)簡(jiǎn)介
在 C 語(yǔ)言的標(biāo)準(zhǔn)庫(kù)中,malloc 函數(shù)如同一位神秘的工匠,為程序提供著動(dòng)態(tài)內(nèi)存分配的關(guān)鍵服務(wù)。它的定義簡(jiǎn)潔而有力:void *malloc(size_t size); ,這個(gè)函數(shù)接受一個(gè)參數(shù) size,用于指定需要分配的內(nèi)存字節(jié)數(shù)。其返回值是一個(gè)指向所分配內(nèi)存起始地址的指針 ,類(lèi)型為 void*,這意味著它可以被轉(zhuǎn)換為任何類(lèi)型的指針,以適應(yīng)不同的數(shù)據(jù)存儲(chǔ)需求。
malloc 函數(shù)在動(dòng)態(tài)內(nèi)存分配領(lǐng)域占據(jù)著核心地位。當(dāng)我們?cè)诰帉?xiě)程序時(shí),常常會(huì)遇到一些場(chǎng)景,比如需要處理用戶(hù)輸入的數(shù)據(jù),但在程序編寫(xiě)階段并不知道數(shù)據(jù)的具體大小;或者構(gòu)建動(dòng)態(tài)的數(shù)據(jù)結(jié)構(gòu),如鏈表、樹(shù)等,這些結(jié)構(gòu)的節(jié)點(diǎn)數(shù)量會(huì)隨著程序的運(yùn)行而動(dòng)態(tài)變化。在這些情況下,靜態(tài)內(nèi)存分配就顯得力不從心,而 malloc 函數(shù)則能夠在程序運(yùn)行時(shí),根據(jù)實(shí)際需求靈活地分配內(nèi)存,使得程序能夠高效地處理各種復(fù)雜的情況。
1.2與操作系統(tǒng)的關(guān)系
雖然 malloc 函數(shù)在 C 庫(kù)中扮演著重要角色,但它并非直接與操作系統(tǒng)的底層內(nèi)存管理機(jī)制打交道。在 Linux 系統(tǒng)中,malloc 函數(shù)是通過(guò) glibc(GNU C Library)來(lái)實(shí)現(xiàn)的,glibc 作為一個(gè)功能強(qiáng)大的 C 庫(kù),為程序員提供了豐富的函數(shù)和工具,malloc 便是其中之一。
當(dāng)我們?cè)诔绦蛑姓{(diào)用 malloc 函數(shù)時(shí),它會(huì)首先在 glibc 維護(hù)的內(nèi)存池中查找是否有足夠的空閑內(nèi)存來(lái)滿(mǎn)足請(qǐng)求。如果內(nèi)存池中有足夠的空閑內(nèi)存,malloc 函數(shù)會(huì)直接從內(nèi)存池中分配內(nèi)存,并返回相應(yīng)的指針。這樣做的好處是可以減少系統(tǒng)調(diào)用的開(kāi)銷(xiāo),提高內(nèi)存分配的效率。因?yàn)橄到y(tǒng)調(diào)用涉及到用戶(hù)態(tài)和內(nèi)核態(tài)的切換,這種切換會(huì)帶來(lái)一定的性能損耗。
然而,如果內(nèi)存池中的空閑內(nèi)存不足以滿(mǎn)足請(qǐng)求,malloc 函數(shù)就會(huì)借助系統(tǒng)調(diào)用與操作系統(tǒng)進(jìn)行交互。在 Linux 系統(tǒng)中,主要涉及到兩個(gè)系統(tǒng)調(diào)用:brk和mmap 。brk系統(tǒng)調(diào)用通過(guò)移動(dòng)程序數(shù)據(jù)段的結(jié)束地址(即 “堆頂” 指針)來(lái)增加堆的大小,從而分配新的內(nèi)存。而mmap系統(tǒng)調(diào)用則是通過(guò)在文件映射區(qū)域分配一塊內(nèi)存來(lái)滿(mǎn)足請(qǐng)求。通常情況下,當(dāng)請(qǐng)求的內(nèi)存大小小于一定閾值(在大多數(shù)系統(tǒng)中,這個(gè)閾值通常為 128KB)時(shí),malloc 函數(shù)會(huì)優(yōu)先使用brk系統(tǒng)調(diào)用來(lái)分配內(nèi)存;當(dāng)請(qǐng)求的內(nèi)存大小大于這個(gè)閾值時(shí),則會(huì)使用mmap系統(tǒng)調(diào)用。通過(guò)這種方式,malloc 函數(shù)能夠根據(jù)不同的內(nèi)存需求,選擇最合適的方式與操作系統(tǒng)進(jìn)行交互,實(shí)現(xiàn)高效的內(nèi)存分配。
二、Malloc函數(shù)的實(shí)現(xiàn)原理
2.1空閑鏈表機(jī)制
在malloc函數(shù)背后,有著依靠空閑鏈表來(lái)管理內(nèi)存的一套機(jī)制。當(dāng)我們調(diào)用malloc函數(shù)時(shí),它會(huì)沿著空閑鏈表去查找滿(mǎn)足用戶(hù)請(qǐng)求大小的內(nèi)存塊。比如說(shuō),鏈表上有多個(gè)不同大小的空閑內(nèi)存塊,它就會(huì)依次遍歷這些塊來(lái)找到合適的那一個(gè)。
找到合適的內(nèi)存塊后,如果這個(gè)內(nèi)存塊比用戶(hù)請(qǐng)求的大小要大,那么就會(huì)按需將其分割成兩部分,一部分的大小剛好與用戶(hù)請(qǐng)求的大小相等,這部分就會(huì)分配給用戶(hù)使用,而剩下的那部分則會(huì)被放回空閑鏈表中,等待后續(xù)其他的內(nèi)存分配請(qǐng)求再進(jìn)行分配。例如,空閑鏈表中有一個(gè) 50 字節(jié)的空閑塊,而用戶(hù)請(qǐng)求分配 20 字節(jié)的內(nèi)存,這時(shí)malloc就會(huì)把這個(gè) 50 字節(jié)的塊分成 20 字節(jié)(分配給用戶(hù))和 30 字節(jié)(放回空閑鏈表)兩塊。
而當(dāng)我們使用free函數(shù)釋放內(nèi)存時(shí),相應(yīng)被釋放的內(nèi)存塊又會(huì)被重新連接到空閑鏈上。這樣,整個(gè)空閑鏈表就處于一個(gè)動(dòng)態(tài)變化的過(guò)程,不斷地有內(nèi)存塊被分配出去,也不斷地有釋放的內(nèi)存塊回歸鏈表,以實(shí)現(xiàn)內(nèi)存的循環(huán)利用,避免浪費(fèi)。不過(guò),隨著程序不斷地分配和釋放內(nèi)存,空閑鏈有可能會(huì)被切成很多的小內(nèi)存片段,要是后續(xù)用戶(hù)申請(qǐng)一個(gè)較大的內(nèi)存片段時(shí),空閑鏈上可能暫時(shí)沒(méi)有可以滿(mǎn)足要求的片段了,這時(shí)malloc函數(shù)可能就需要進(jìn)行一些整理操作,比如對(duì)這些小的空閑塊嘗試合并等,以便能滿(mǎn)足較大內(nèi)存請(qǐng)求的情況。
虛擬內(nèi)存地址和物理內(nèi)存地址
為了簡(jiǎn)單,現(xiàn)代操作系統(tǒng)在處理物理內(nèi)存地址時(shí),普遍采用虛擬內(nèi)存地址技術(shù)。即在匯編程序?qū)用妫?dāng)涉及內(nèi)存地址時(shí),都是使用的虛擬內(nèi)存地址。采用這種技術(shù)時(shí),每個(gè)進(jìn)程仿佛自己獨(dú)享一片2N字節(jié)的內(nèi)存,其中N是機(jī)器位數(shù)。例如在64位CPU和64位操作系統(tǒng)下每個(gè)進(jìn)程的虛擬地址空間為264Byte。
這種虛擬地址空間的作用主要是簡(jiǎn)化程序的編寫(xiě)及方便操作系統(tǒng)對(duì)進(jìn)程間內(nèi)存的隔離管理,真實(shí)中的進(jìn)程不太可能如此大的空間,實(shí)際能用到的空間大小取決于物理內(nèi)存的大小。由于在機(jī)器語(yǔ)言層面都是采用虛擬地址,當(dāng)實(shí)際的機(jī)器碼程序涉及到內(nèi)存操作時(shí),需要根據(jù)當(dāng)前進(jìn)程運(yùn)行的實(shí)際上下文將虛擬地址轉(zhuǎn)化為物理內(nèi)存地址,才能實(shí)現(xiàn)對(duì)內(nèi)存數(shù)據(jù)的操作。這個(gè)轉(zhuǎn)換一般由一個(gè)叫MMU的硬件完成。
頁(yè)與地址構(gòu)成
在現(xiàn)代操作系統(tǒng)中,不論是虛擬內(nèi)存還是物理內(nèi)存,都不是以字節(jié)為單位進(jìn)行管理的,而是以頁(yè)為單位。一個(gè)內(nèi)存頁(yè)是一段固定大小的連續(xù)的連續(xù)內(nèi)存地址的總稱(chēng),具體到Linux中,典型的內(nèi)存頁(yè)大小為4096 Byte。所以?xún)?nèi)存地址可以分為頁(yè)號(hào)和頁(yè)內(nèi)偏移量。下面以64位機(jī)器,4G物理內(nèi)存,4K頁(yè)大小為例,虛擬內(nèi)存地址和物理內(nèi)存地址的組成如下:
圖片
上面是虛擬內(nèi)存地址,下面是物理內(nèi)存地址。由于頁(yè)大小都是4k,所以頁(yè)內(nèi)偏移都是用低12位表示,而剩下的高地址表示頁(yè)號(hào) MMU映射單位并不是字節(jié),而是頁(yè),這個(gè)映射通過(guò)差一個(gè)常駐內(nèi)存的數(shù)據(jù)結(jié)構(gòu)頁(yè)表來(lái)實(shí)現(xiàn)?,F(xiàn)在計(jì)算機(jī)具體的內(nèi)存地址映射比較復(fù)雜,為了加快速度會(huì)引入一系列緩存和優(yōu)化,例如TLB等機(jī)制,下面給出一個(gè)經(jīng)過(guò)簡(jiǎn)化的內(nèi)存地址翻譯示意圖:
圖片
內(nèi)存頁(yè)與磁盤(pán)頁(yè)
我們知道一般將內(nèi)存看做磁盤(pán)的緩存,有時(shí)MMU在工作時(shí),會(huì)發(fā)現(xiàn)頁(yè)表表名某個(gè)內(nèi)存頁(yè)不在物理內(nèi)存頁(yè)不在物理內(nèi)存中,此時(shí)會(huì)觸發(fā)一個(gè)缺頁(yè)異常,此時(shí)系統(tǒng)會(huì)到磁盤(pán)中相應(yīng)的地方將磁盤(pán)頁(yè)載入到內(nèi)存中,然后重新執(zhí)行由于缺頁(yè)而失敗的機(jī)器指令。關(guān)于這部分,因?yàn)榭梢钥醋鰧?duì)malloc實(shí)現(xiàn)是透明的,所以不再詳述。
真實(shí)地址翻譯流程:
圖片
Linux進(jìn)程級(jí)內(nèi)存管理
內(nèi)存排布:明白了虛擬內(nèi)存和物理內(nèi)存的關(guān)系及相關(guān)的映射機(jī)制,下面看一下具體在一個(gè)進(jìn)程內(nèi)是如何排布內(nèi)存的。以Linux 64位系統(tǒng)為例。理論上,64bit內(nèi)存地址空間為0x0000000000000000-0xFFFFFFFFFFFFFFF,這是個(gè)相當(dāng)龐大的空間,Linux實(shí)際上只用了其中一小部分。具體分布如圖所示:
圖片
對(duì)用戶(hù)來(lái)說(shuō)主要關(guān)心的是User Space。將User Space放大后,可以看到里面主要分成如下幾段:
- Code:這是整個(gè)用戶(hù)空間的最低地址部分,存放的是指令(也就是程序所編譯成的可執(zhí)行機(jī)器碼) Data:這里存放的是初始化過(guò)的全局變量
- BSS:這里存放的是未初始化的全局變量
- Heap:堆,這是我們本文主要關(guān)注的地方,堆自底向上由低地址向高地址增長(zhǎng)
- Mapping Area:這里是與mmap系統(tǒng)調(diào)用相關(guān)區(qū)域。大多數(shù)實(shí)際的malloc實(shí)現(xiàn)會(huì)考慮通過(guò)mmap分配較大塊的內(nèi)存空間,本文不考慮這種情況,這個(gè)區(qū)域由高地址像低地址增長(zhǎng)Stack:棧區(qū)域,自高地址像低地址增長(zhǎng) 。
- Heap內(nèi)存模型:一般來(lái)說(shuō),malloc所申請(qǐng)的內(nèi)存主要從Heap區(qū)域分配,來(lái)看看Heap的結(jié)構(gòu)是怎樣的。
圖片
Linux維護(hù)一個(gè)break指針,這個(gè)指針執(zhí)行堆空間的某個(gè)地址,從堆開(kāi)始到break之間的地址空間為映射好的,可以供進(jìn)程訪問(wèn),而從break往上,是未映射的地址空間,如果訪問(wèn)這段空間則程序會(huì)報(bào)錯(cuò)。
2.2在操作系統(tǒng)中的實(shí)現(xiàn)
以常見(jiàn)的操作系統(tǒng)為例,malloc函數(shù)需要通過(guò)系統(tǒng)調(diào)用來(lái)從內(nèi)核申請(qǐng)內(nèi)存,像brk(用于堆內(nèi)存)或者mmap(用于內(nèi)存映射)就是常用的手段。
對(duì)于brk系統(tǒng)調(diào)用,它主要的作用是調(diào)整堆頂?shù)奈恢?,使得堆?nèi)存可以從低地址向高地址增長(zhǎng),以此來(lái)擴(kuò)大進(jìn)程在運(yùn)行時(shí)的堆大小。一般來(lái)說(shuō),如果分配的內(nèi)存小于 128K 時(shí),就常常會(huì)使用brk調(diào)用來(lái)獲得虛擬內(nèi)存。比如在一些小型的數(shù)據(jù)結(jié)構(gòu)動(dòng)態(tài)分配場(chǎng)景中,brk就能很好地滿(mǎn)足需求。當(dāng)使用brk分配了一段新的虛擬內(nèi)存區(qū)域后,要注意這并不會(huì)立即分配物理內(nèi)存哦,實(shí)際的物理內(nèi)存分配通常是在訪問(wèn)新分配的虛擬內(nèi)存區(qū)域時(shí),如果發(fā)生了缺頁(yè)異常,操作系統(tǒng)才會(huì)開(kāi)始分配并映射相應(yīng)的物理內(nèi)存頁(yè)面。
而mmap系統(tǒng)調(diào)用則是在進(jìn)程的虛擬地址空間中尋找一塊空閑的虛擬內(nèi)存,從而獲得一塊可以操作的堆內(nèi)存,當(dāng)需要分配較大塊的內(nèi)存(通常大于 128K 時(shí)),就會(huì)更多地借助mmap來(lái)完成申請(qǐng)操作。一旦通過(guò)mmap建立了內(nèi)存映射關(guān)系,進(jìn)程就可以通過(guò)指針的方式來(lái)讀寫(xiě)這塊內(nèi)存了,并且系統(tǒng)會(huì)自動(dòng)將臟頁(yè)(被修改的頁(yè))回寫(xiě)到相應(yīng)的磁盤(pán)文件上。
在內(nèi)存分配程序初始化時(shí),要完成諸如將分配程序標(biāo)識(shí)為已經(jīng)初始化,找到系統(tǒng)中最后一個(gè)有效內(nèi)存地址,然后建立起指向管理的內(nèi)存的指針等操作,這些都是為了后續(xù)能更好地追蹤要分配和回收哪些內(nèi)存。在整個(gè)過(guò)程中,會(huì)不斷地去記錄內(nèi)存的分配和回收情況,比如哪些內(nèi)存塊已經(jīng)分配出去被使用了,哪些又被釋放回到了可分配的狀態(tài)等,通過(guò)這些精細(xì)的管理,才能讓內(nèi)存資源在程序運(yùn)行過(guò)程中得到合理的調(diào)配。
⑴brk與sbrk
由上文知道,要增加一個(gè)進(jìn)程實(shí)際上的可用堆大小,就需要將break指針向高地址移動(dòng)。Linux通過(guò)brk和sbrk系統(tǒng)調(diào)用操作break指針。兩個(gè)系統(tǒng)調(diào)用的原型如下:
int brk(void *addr);
void *sbrk(inptr_t increment);
brk將break指針直接設(shè)置為某個(gè)地址,而sbrk將break從當(dāng)前位置移動(dòng)increment所指定的增量。brk在執(zhí)行成功時(shí)返回0,否則返回-1并設(shè)置為errno為ENOMEM,sbrk成功時(shí)返回break移動(dòng)之前所指向的地址,否則返回(void*)-1;
⑵資源限制和rlimirt
系統(tǒng)為每一個(gè)進(jìn)程所分配的資源不是無(wú)限的,包括可映射的空間,因此每個(gè)進(jìn)程有一個(gè)rlimit表示當(dāng)前進(jìn)程可用的資源上限,這個(gè)限制可以通過(guò)getrlimit系統(tǒng)調(diào)用得到,下面代碼獲取當(dāng)前進(jìn)程虛擬內(nèi)存空間的rlimit 其中rlimt是一個(gè)結(jié)構(gòu)體
struct rlimit
{
rlimt_t rlim_cur;
rlim_t rlim_max;
};
每種資源有硬限制和軟限制,并且可以通過(guò)setrlimit對(duì)rlimit進(jìn)行有條件限制作為軟限制的上限,非特權(quán)進(jìn)程只能設(shè)置軟限制,且不能超過(guò)硬限制
2.3ptmalloc 工作原理
在涉及ptmalloc模塊的情況下,先來(lái)了解一下它的軟件架構(gòu)。ptmalloc中有幾個(gè)關(guān)鍵概念,比如malloc_state、malloc_chunk等。
malloc_state結(jié)構(gòu)用于統(tǒng)一管理內(nèi)存分配相關(guān)的諸多信息,它里面包含了像fastbinsY(這是用于存儲(chǔ) 16 - 160 字節(jié)chunk的空閑鏈表)、top(代表著頂部的內(nèi)存塊,也就是當(dāng)其他空閑鏈表中沒(méi)有匹配的chunk分配給用戶(hù)程序時(shí),會(huì)從這里裁剪出可用的chunk分配給用戶(hù))、bins(又可細(xì)分為unsortedbins、smallbins、largebins,unsortedbins是chunk緩存區(qū),用于存儲(chǔ)從fastbins合并的空閑chunk;smallbins用于存儲(chǔ) 32 - 1024 字節(jié)的chunk;largebins則用于存儲(chǔ)大于 1024 字節(jié)大小的空閑chunk)以及binmap(可用bins位圖,方便快速查找可用的bin)等重要成員。
而malloc_chunk則是以其為單位來(lái)進(jìn)行內(nèi)存的申請(qǐng)和釋放操作。每個(gè)malloc_chunk結(jié)構(gòu)體中有記錄前一個(gè)chunk大小的mchunk_prev_size成員、表示當(dāng)前chunk大小的mchunk_size成員,還有像fd(鏈表后驅(qū)指針)、bk(鏈表前驅(qū)指針)等指針成員(當(dāng)chunk處于空閑狀態(tài)時(shí),會(huì)借助內(nèi)存區(qū)域前 16 個(gè)字節(jié)作為鏈表指針,將chunk插入到相應(yīng)的空閑鏈表中)。
在內(nèi)存管理方面,不同大小的內(nèi)存塊有著不同的管理方式。對(duì)于小于 160 字節(jié)的內(nèi)存申請(qǐng),malloc函數(shù)會(huì)從fastbins空閑鏈表中查找匹配的chunk進(jìn)行分配;對(duì)于 32 - 1024 字節(jié)的內(nèi)存請(qǐng)求,就會(huì)去smallbins中尋找合適的空閑chunk;大于 1024 字節(jié)的則在largebins里查找。
當(dāng)用戶(hù)申請(qǐng)內(nèi)存時(shí),malloc會(huì)按照對(duì)應(yīng)的大小范圍去相應(yīng)的鏈表中尋找可用的chunk,找到就分配給用戶(hù)使用。而當(dāng)用戶(hù)釋放內(nèi)存時(shí),釋放的chunk會(huì)依據(jù)其大小等情況,被合理地放回fastbins、bins等相應(yīng)的鏈表中,比如從smallbins中釋放的chunk可能會(huì)先進(jìn)入unsortedbins緩存,后續(xù)再根據(jù)具體情況進(jìn)行合并或者重新分配等操作,以此來(lái)維持整個(gè)內(nèi)存分配和回收體系的高效、有序運(yùn)行。
三、內(nèi)存分配方式
3.1brk 系統(tǒng)調(diào)用分配內(nèi)存
brk 原理剖析:在 Linux 系統(tǒng)中,進(jìn)程的內(nèi)存空間布局包含多個(gè)段,其中堆(heap)是用于動(dòng)態(tài)內(nèi)存分配的重要區(qū)域。brk 系統(tǒng)調(diào)用的核心原理是通過(guò)移動(dòng)堆頂指針(_edata)來(lái)擴(kuò)大堆的空間。當(dāng)程序調(diào)用 brk 系統(tǒng)調(diào)用并傳入一個(gè)新的地址時(shí),如果這個(gè)新地址大于當(dāng)前堆頂指針的位置,內(nèi)核會(huì)嘗試將堆頂指針移動(dòng)到新的地址,從而擴(kuò)大堆的范圍。
在這個(gè)過(guò)程中,有一個(gè)關(guān)鍵的概念需要理解,那就是虛擬內(nèi)存與物理內(nèi)存的映射關(guān)系。當(dāng) brk 系統(tǒng)調(diào)用擴(kuò)大堆空間時(shí),實(shí)際上只是分配了虛擬內(nèi)存,并沒(méi)有立即分配物理內(nèi)存。這種映射是延遲的,直到程序第一次訪問(wèn)新分配的虛擬內(nèi)存區(qū)域時(shí),才會(huì)觸發(fā)缺頁(yè)中斷(page fault) 。此時(shí),操作系統(tǒng)會(huì)負(fù)責(zé)分配物理內(nèi)存,并建立虛擬內(nèi)存與物理內(nèi)存之間的映射關(guān)系。這一機(jī)制有效地提高了內(nèi)存使用效率,避免了過(guò)早分配物理內(nèi)存造成的浪費(fèi)。
小內(nèi)存分配示例:假設(shè)我們?cè)诔绦蛑姓{(diào)用malloc函數(shù)申請(qǐng)一塊小于 128KB 的內(nèi)存,例如申請(qǐng) 30KB 的內(nèi)存。在這種情況下,malloc函數(shù)通常會(huì)調(diào)用 brk 系統(tǒng)調(diào)用來(lái)完成內(nèi)存分配。
當(dāng)程序執(zhí)行到malloc(30 * 1024)時(shí),malloc函數(shù)會(huì)向內(nèi)核發(fā)起 brk 系統(tǒng)調(diào)用。內(nèi)核接收到這個(gè)調(diào)用后,會(huì)檢查當(dāng)前堆頂指針的位置,假設(shè)當(dāng)前堆頂指針為_(kāi)edata,其值為 0x10000000。內(nèi)核會(huì)將_edata指針向高地址方向移動(dòng) 30KB,即移動(dòng)到 0x10007800(30 * 1024 = 30720,十六進(jìn)制表示為 0x7800)。此時(shí),從 0x10000000 到 0x10007800 這一段虛擬內(nèi)存空間就被分配給了程序。
需要注意的是,雖然虛擬內(nèi)存已經(jīng)分配,但在程序第一次訪問(wèn)這部分內(nèi)存之前,并沒(méi)有實(shí)際的物理內(nèi)存與之對(duì)應(yīng)。例如,如果程序執(zhí)行*(int*)0x10000000 = 10; ,這是對(duì)新分配內(nèi)存的第一次訪問(wèn),此時(shí)會(huì)觸發(fā)缺頁(yè)中斷。操作系統(tǒng)捕獲到這個(gè)缺頁(yè)中斷后,會(huì)為該虛擬內(nèi)存頁(yè)分配物理內(nèi)存頁(yè),并建立兩者之間的映射關(guān)系,然后程序才能繼續(xù)正常執(zhí)行。
這種分配方式在處理小內(nèi)存分配時(shí)具有一定的優(yōu)勢(shì),它相對(duì)簡(jiǎn)單高效,減少了系統(tǒng)調(diào)用的開(kāi)銷(xiāo)。但也存在一些潛在的問(wèn)題,例如隨著內(nèi)存的頻繁分配和釋放,可能會(huì)導(dǎo)致堆內(nèi)存中出現(xiàn)大量的碎片,降低內(nèi)存的利用率。
3.2mmap 系統(tǒng)調(diào)用分配內(nèi)存
mmap 原理闡述:mmap 系統(tǒng)調(diào)用是另一種重要的內(nèi)存分配方式,它與 brk 系統(tǒng)調(diào)用有著本質(zhì)的區(qū)別。mmap 系統(tǒng)調(diào)用主要用于在文件映射區(qū)域分配內(nèi)存,它通過(guò)在進(jìn)程的虛擬地址空間中(堆和棧中間,稱(chēng)為文件映射區(qū)域的地方)找一塊空閑的虛擬內(nèi)存來(lái)滿(mǎn)足內(nèi)存分配請(qǐng)求。
具體來(lái)說(shuō),mmap 系統(tǒng)調(diào)用可以創(chuàng)建一個(gè)新的虛擬內(nèi)存區(qū)域,并將一個(gè)文件或設(shè)備的內(nèi)容映射到這個(gè)區(qū)域中。當(dāng)用于內(nèi)存分配時(shí),通常會(huì)使用私有匿名映射(private anonymous mapping) ,即創(chuàng)建一個(gè)匿名的虛擬內(nèi)存區(qū)域,這個(gè)區(qū)域與任何文件都沒(méi)有關(guān)聯(lián),并且對(duì)該區(qū)域的寫(xiě)入操作不會(huì)影響到其他進(jìn)程。在這種情況下,mmap 系統(tǒng)調(diào)用會(huì)在文件映射區(qū)域中找到一塊合適的空閑虛擬內(nèi)存,并將其分配給調(diào)用者。
與 brk 系統(tǒng)調(diào)用不同,mmap 系統(tǒng)調(diào)用分配的內(nèi)存是獨(dú)立的,它不會(huì)與堆內(nèi)存連續(xù),并且在釋放時(shí)可以單獨(dú)釋放,不會(huì)受到其他內(nèi)存塊的影響。這使得 mmap 在處理大內(nèi)存分配和需要頻繁分配和釋放內(nèi)存的場(chǎng)景中具有明顯的優(yōu)勢(shì)。
大內(nèi)存分配示例:當(dāng)程序調(diào)用malloc函數(shù)申請(qǐng)一塊大于 128KB 的內(nèi)存時(shí),例如申請(qǐng) 200KB 的內(nèi)存,malloc函數(shù)通常會(huì)使用 mmap 系統(tǒng)調(diào)用來(lái)完成分配。
當(dāng)程序執(zhí)行到malloc(200 * 1024)時(shí),malloc函數(shù)會(huì)向內(nèi)核發(fā)起 mmap 系統(tǒng)調(diào)用。內(nèi)核接收到這個(gè)調(diào)用后,會(huì)在文件映射區(qū)域中查找一塊大小為 200KB 的空閑虛擬內(nèi)存。假設(shè)找到的空閑虛擬內(nèi)存區(qū)域起始地址為 0x20000000,內(nèi)核會(huì)將這塊虛擬內(nèi)存分配給程序。
與 brk 系統(tǒng)調(diào)用類(lèi)似,此時(shí)分配的只是虛擬內(nèi)存,在程序第一次訪問(wèn)這部分內(nèi)存時(shí)才會(huì)觸發(fā)缺頁(yè)中斷,操作系統(tǒng)會(huì)為其分配物理內(nèi)存并建立映射關(guān)系。但與 brk 不同的是,mmap 分配的內(nèi)存具有更好的獨(dú)立性和可管理性。例如,當(dāng)程序不再需要這塊內(nèi)存時(shí),調(diào)用free函數(shù)會(huì)直接通過(guò) munmap 系統(tǒng)調(diào)用將這塊內(nèi)存釋放回操作系統(tǒng),不會(huì)影響到其他內(nèi)存塊的使用,有效地避免了內(nèi)存碎片的問(wèn)題。在一些對(duì)內(nèi)存管理要求較高的場(chǎng)景,如數(shù)據(jù)庫(kù)系統(tǒng)、大型游戲開(kāi)發(fā)等,mmap 系統(tǒng)調(diào)用的這種優(yōu)勢(shì)能夠顯著提高系統(tǒng)的性能和穩(wěn)定性。
四、Malloc內(nèi)存分配策略
4.1閾值設(shè)定與分配決策
在 malloc 的源碼世界里,有一個(gè)至關(guān)重要的閾值設(shè)定,它如同一個(gè)精密的開(kāi)關(guān),決定著內(nèi)存分配的走向。這個(gè)閾值通常被設(shè)定為 128KB ,成為了 brk 和 mmap 兩種內(nèi)存分配方式的分水嶺。當(dāng)我們?cè)诔绦蛑姓{(diào)用 malloc 函數(shù)申請(qǐng)內(nèi)存時(shí),它會(huì)首先對(duì)申請(qǐng)的內(nèi)存大小進(jìn)行判斷。
如果申請(qǐng)的內(nèi)存大小小于 128KB,malloc 函數(shù)會(huì)傾向于選擇 brk 系統(tǒng)調(diào)用來(lái)分配內(nèi)存。這是因?yàn)?brk 系統(tǒng)調(diào)用在處理小內(nèi)存分配時(shí)具有獨(dú)特的優(yōu)勢(shì),它通過(guò)簡(jiǎn)單地移動(dòng)堆頂指針來(lái)擴(kuò)大堆空間,這種方式相對(duì)高效,減少了系統(tǒng)調(diào)用的開(kāi)銷(xiāo),能夠快速地滿(mǎn)足小內(nèi)存的分配需求。
而當(dāng)申請(qǐng)的內(nèi)存大小大于或等于 128KB 時(shí),malloc 函數(shù)則會(huì)啟用 mmap 系統(tǒng)調(diào)用。mmap 系統(tǒng)調(diào)用在處理大內(nèi)存分配時(shí)表現(xiàn)出色,它能夠在文件映射區(qū)域?yàn)槌绦蚍峙湟粔K獨(dú)立的內(nèi)存空間。這樣做的好處是可以避免在堆區(qū)產(chǎn)生大量的內(nèi)存碎片,因?yàn)?mmap 分配的內(nèi)存塊在釋放時(shí)可以單獨(dú)釋放,不會(huì)受到其他內(nèi)存塊的影響,從而提高了內(nèi)存的利用率和管理效率。
需要注意的是,這個(gè) 128KB 的閾值并非一成不變,不同的 glibc 版本可能會(huì)根據(jù)實(shí)際情況對(duì)其進(jìn)行調(diào)整。例如,在某些特定的系統(tǒng)環(huán)境或應(yīng)用場(chǎng)景下,為了優(yōu)化內(nèi)存分配的性能,glibc 版本可能會(huì)將閾值設(shè)定為其他值。因此,在深入研究 malloc 的內(nèi)存分配機(jī)制時(shí),我們需要關(guān)注具體的 glibc 版本及其配置,以準(zhǔn)確把握內(nèi)存分配的策略和行為。
4.2內(nèi)存池與預(yù)分配機(jī)制
在 malloc 的內(nèi)存分配策略中,內(nèi)存池與預(yù)分配機(jī)制起著至關(guān)重要的作用,以默認(rèn)內(nèi)存管理器 Ptmalloc2 為例,當(dāng)我們調(diào)用 malloc 函數(shù)申請(qǐng)內(nèi)存時(shí),它并非僅僅按照我們請(qǐng)求的字節(jié)數(shù)來(lái)分配內(nèi)存,而是會(huì)預(yù)分配更大的空間作為內(nèi)存池。
以主進(jìn)程(主線(xiàn)程)下的內(nèi)存分配為例,當(dāng)我們申請(qǐng) 1 字節(jié)的內(nèi)存時(shí),Ptmalloc2 會(huì)預(yù)分配 132KB 的內(nèi)存,這個(gè)預(yù)分配的內(nèi)存區(qū)域被稱(chēng)為 Main Arena。在這個(gè) 132KB 的內(nèi)存池中,Ptmalloc2 會(huì)根據(jù)后續(xù)的內(nèi)存申請(qǐng)請(qǐng)求,從其中切割出合適大小的內(nèi)存塊分配給用戶(hù)。當(dāng)用戶(hù)釋放內(nèi)存時(shí),Ptmalloc2 并不會(huì)立即將內(nèi)存歸還給操作系統(tǒng),而是會(huì)根據(jù)一些策略來(lái)判斷是否釋放。如果不釋放,這塊內(nèi)存會(huì)被重新放回內(nèi)存池中,供下次申請(qǐng)時(shí)使用。這種機(jī)制大大提高了內(nèi)存分配的效率,減少了頻繁向操作系統(tǒng)申請(qǐng)內(nèi)存的開(kāi)銷(xiāo)。
在子線(xiàn)程下,內(nèi)存分配的策略又有所不同。每個(gè)子線(xiàn)程會(huì)預(yù)先分配 HEAP_MAX_SIZE 大小的內(nèi)存(64 位系統(tǒng)下為 64MB,32 位系統(tǒng)下為 1MB),這被稱(chēng)為 Thread Arena。并且,單個(gè)子線(xiàn)程的內(nèi)存池?cái)?shù)量最大可以達(dá)到 8 倍的 CPU 數(shù)。雖然這種預(yù)分配機(jī)制會(huì)占用一定的內(nèi)存空間,但它在多線(xiàn)程環(huán)境下能夠顯著加快內(nèi)存分配的速度,減少線(xiàn)程之間的競(jìng)爭(zhēng)和等待時(shí)間,提高程序的整體性能。
內(nèi)存池的預(yù)分配機(jī)制并非完美無(wú)缺。在多線(xiàn)程環(huán)境下,如果線(xiàn)程數(shù)量眾多,每個(gè)線(xiàn)程都預(yù)分配了大量的內(nèi)存,可能會(huì)導(dǎo)致系統(tǒng)內(nèi)存資源的緊張。例如,在一個(gè)擁有 100 個(gè)線(xiàn)程的程序中,每個(gè)線(xiàn)程預(yù)分配 64MB 的內(nèi)存,那么僅僅內(nèi)存池就會(huì)占用 6GB 的內(nèi)存空間。這可能會(huì)對(duì)系統(tǒng)的其他進(jìn)程產(chǎn)生影響,甚至導(dǎo)致系統(tǒng)性能下降。因此,在實(shí)際應(yīng)用中,我們需要根據(jù)程序的特點(diǎn)和運(yùn)行環(huán)境,合理地調(diào)整內(nèi)存池的預(yù)分配策略,以平衡內(nèi)存使用和性能之間的關(guān)系。
五、Free釋放內(nèi)存機(jī)制
5.1brk 方式申請(qǐng)內(nèi)存的釋放
當(dāng)我們使用 free 函數(shù)釋放通過(guò) brk 方式申請(qǐng)的內(nèi)存時(shí),內(nèi)存并不會(huì)立即歸還給操作系統(tǒng)。這是因?yàn)?brk 系統(tǒng)調(diào)用分配的內(nèi)存是在堆空間中,free 函數(shù)將內(nèi)存釋放后,這塊內(nèi)存會(huì)被緩存在 malloc 的內(nèi)存池中。這樣做的目的是為了提高內(nèi)存的復(fù)用效率,當(dāng)程序后續(xù)再次申請(qǐng)內(nèi)存時(shí),如果內(nèi)存池中有合適大小的空閑內(nèi)存塊,就可以直接從內(nèi)存池中分配,而無(wú)需再次向操作系統(tǒng)發(fā)起 brk 系統(tǒng)調(diào)用,從而減少了系統(tǒng)調(diào)用的開(kāi)銷(xiāo)和時(shí)間成本。
內(nèi)存池復(fù)用機(jī)制在一定程度上提高了內(nèi)存分配的效率,但也帶來(lái)了潛在的內(nèi)存碎片問(wèn)題。隨著程序中頻繁地進(jìn)行內(nèi)存分配和釋放操作,堆內(nèi)存中可能會(huì)出現(xiàn)許多不連續(xù)的空閑內(nèi)存塊,這些小塊內(nèi)存由于大小和位置的限制,可能無(wú)法滿(mǎn)足后續(xù)較大內(nèi)存分配的需求,從而導(dǎo)致內(nèi)存利用率降低。例如,假設(shè)程序先申請(qǐng)了 10KB、20KB 和 30KB 的三塊內(nèi)存,然后釋放了 10KB 和 20KB 的內(nèi)存塊。此時(shí),堆內(nèi)存中會(huì)出現(xiàn)兩個(gè)空閑的內(nèi)存塊,但如果后續(xù)需要申請(qǐng) 40KB 的內(nèi)存,由于這兩個(gè)空閑內(nèi)存塊不連續(xù),無(wú)法合并成一個(gè)足夠大的內(nèi)存塊,就會(huì)導(dǎo)致雖然堆內(nèi)存中有空閑空間,但仍然無(wú)法滿(mǎn)足分配需求的情況,這就是內(nèi)存碎片問(wèn)題的體現(xiàn)。
5.2mmap 方式申請(qǐng)內(nèi)存的釋放
與 brk 方式不同,當(dāng)使用 free 函數(shù)釋放通過(guò) mmap 方式申請(qǐng)的內(nèi)存時(shí),內(nèi)存會(huì)立即歸還給操作系統(tǒng)。這是因?yàn)?mmap 系統(tǒng)調(diào)用分配的內(nèi)存是在文件映射區(qū)域,與堆內(nèi)存相互獨(dú)立。當(dāng)調(diào)用 free 函數(shù)時(shí),實(shí)際上是通過(guò) munmap 系統(tǒng)調(diào)用來(lái)取消內(nèi)存映射,將內(nèi)存從進(jìn)程的虛擬地址空間中移除,并將其歸還給操作系統(tǒng),使得這部分內(nèi)存可以被其他進(jìn)程使用。
mmap 方式申請(qǐng)內(nèi)存的釋放機(jī)制使得內(nèi)存的管理更加靈活和高效,尤其是在處理大內(nèi)存塊的分配和釋放時(shí),能夠有效地避免內(nèi)存碎片的產(chǎn)生。例如,在一個(gè)需要頻繁分配和釋放大內(nèi)存塊的程序中,如果使用 brk 方式,隨著內(nèi)存的不斷分配和釋放,堆內(nèi)存中很容易產(chǎn)生大量的碎片,導(dǎo)致內(nèi)存利用率下降。而使用 mmap 方式,每次釋放內(nèi)存時(shí)都能將其完整地歸還給操作系統(tǒng),不會(huì)產(chǎn)生碎片問(wèn)題,保證了內(nèi)存的高效利用和系統(tǒng)的穩(wěn)定性。
六、為何不全部使用 brk 或 mmap
6.1不全部使用 brk 的原因
在內(nèi)存分配的世界里,brk 系統(tǒng)調(diào)用雖然在處理小內(nèi)存分配時(shí)展現(xiàn)出一定的優(yōu)勢(shì),但如果全部使用 brk 來(lái)分配內(nèi)存,會(huì)帶來(lái)一系列嚴(yán)重的問(wèn)題。其中最突出的問(wèn)題便是內(nèi)存碎片的產(chǎn)生。
讓我們通過(guò)一個(gè)具體的場(chǎng)景來(lái)深入理解這個(gè)問(wèn)題。假設(shè)我們有一個(gè)程序,它需要頻繁地進(jìn)行小內(nèi)存的分配和釋放操作。程序首先通過(guò) brk 系統(tǒng)調(diào)用申請(qǐng)了一塊 10KB 的內(nèi)存,用于存儲(chǔ)一些臨時(shí)數(shù)據(jù);接著又申請(qǐng)了一塊 20KB 的內(nèi)存,用于其他任務(wù);隨后再申請(qǐng)一塊 30KB 的內(nèi)存。此時(shí),堆內(nèi)存的布局是連續(xù)的三塊內(nèi)存區(qū)域,分別為 10KB、20KB 和 30KB。
隨著程序的運(yùn)行,當(dāng)不再需要第一塊 10KB 和第二塊 20KB 的內(nèi)存時(shí),我們調(diào)用 free 函數(shù)將它們釋放。由于 brk 系統(tǒng)調(diào)用分配的內(nèi)存是在堆空間中,釋放后的內(nèi)存并不會(huì)立即歸還給操作系統(tǒng),而是緩存在 malloc 的內(nèi)存池中。這就導(dǎo)致堆內(nèi)存中出現(xiàn)了兩塊空閑的內(nèi)存區(qū)域,它們分別是 10KB 和 20KB,并且這兩塊空閑內(nèi)存區(qū)域之間被一塊正在使用的 30KB 內(nèi)存隔開(kāi)。
當(dāng)程序后續(xù)需要申請(qǐng)一塊 40KB 的內(nèi)存時(shí),盡管堆內(nèi)存中總的空閑內(nèi)存大小是足夠的(10KB + 20KB = 30KB),但由于這兩塊空閑內(nèi)存不連續(xù),無(wú)法合并成一個(gè)足夠大的內(nèi)存塊來(lái)滿(mǎn)足 40KB 的分配需求。這樣,就會(huì)出現(xiàn)雖然堆內(nèi)存中有空閑空間,但仍然無(wú)法滿(mǎn)足分配需求的情況,這就是典型的內(nèi)存碎片問(wèn)題。
隨著程序中這種頻繁的內(nèi)存分配和釋放操作不斷進(jìn)行,堆內(nèi)存中會(huì)逐漸產(chǎn)生越來(lái)越多這樣不連續(xù)的小空閑內(nèi)存塊,這些內(nèi)存碎片會(huì)占據(jù)大量的內(nèi)存空間,卻無(wú)法被有效地利用,導(dǎo)致內(nèi)存利用率急劇下降。這種情況在一些長(zhǎng)時(shí)間運(yùn)行且需要頻繁進(jìn)行小內(nèi)存分配和釋放的程序中尤為明顯,例如數(shù)據(jù)庫(kù)管理系統(tǒng)中的緩存模塊、圖形渲染引擎中的資源分配模塊等。如果這些系統(tǒng)全部使用 brk 系統(tǒng)調(diào)用進(jìn)行內(nèi)存分配,隨著時(shí)間的推移,內(nèi)存碎片問(wèn)題會(huì)越來(lái)越嚴(yán)重,最終可能導(dǎo)致系統(tǒng)性能大幅下降,甚至出現(xiàn)內(nèi)存耗盡的錯(cuò)誤。
6.2不全部使用 mmap 的原因
雖然 mmap 系統(tǒng)調(diào)用在處理大內(nèi)存分配時(shí)具有明顯的優(yōu)勢(shì),能夠有效避免內(nèi)存碎片問(wèn)題,但如果全部使用 mmap 來(lái)分配內(nèi)存,同樣會(huì)面臨一些嚴(yán)重的問(wèn)題,主要體現(xiàn)在系統(tǒng)調(diào)用開(kāi)銷(xiāo)和缺頁(yè)中斷方面。
從系統(tǒng)調(diào)用開(kāi)銷(xiāo)的角度來(lái)看,mmap 系統(tǒng)調(diào)用涉及到用戶(hù)態(tài)和內(nèi)核態(tài)的切換,這種切換會(huì)帶來(lái)一定的性能損耗。當(dāng)我們?cè)诔绦蛑姓{(diào)用 mmap 系統(tǒng)調(diào)用時(shí),CPU 需要保存當(dāng)前用戶(hù)態(tài)的上下文信息,然后切換到內(nèi)核態(tài)執(zhí)行 mmap 的相關(guān)操作。在內(nèi)核態(tài)完成內(nèi)存映射等操作后,又需要將上下文信息恢復(fù),切換回用戶(hù)態(tài)。這個(gè)過(guò)程需要消耗一定的時(shí)間和 CPU 資源,如果頻繁地進(jìn)行 mmap 系統(tǒng)調(diào)用,系統(tǒng)調(diào)用的開(kāi)銷(xiāo)將會(huì)顯著增加,導(dǎo)致程序的整體性能下降。
缺頁(yè)中斷也是一個(gè)不可忽視的問(wèn)題。當(dāng)使用 mmap 系統(tǒng)調(diào)用分配內(nèi)存時(shí),雖然虛擬內(nèi)存會(huì)被立即分配,但在程序第一次訪問(wèn)這些虛擬內(nèi)存時(shí),會(huì)觸發(fā)缺頁(yè)中斷。這是因?yàn)榇藭r(shí)物理內(nèi)存尚未分配,操作系統(tǒng)需要捕獲這個(gè)缺頁(yè)中斷,為該虛擬內(nèi)存頁(yè)分配物理內(nèi)存,并建立虛擬內(nèi)存與物理內(nèi)存之間的映射關(guān)系。如果全部使用 mmap 來(lái)分配內(nèi)存,并且程序頻繁地進(jìn)行內(nèi)存分配和訪問(wèn)操作,那么就會(huì)頻繁地觸發(fā)缺頁(yè)中斷。每一次缺頁(yè)中斷都需要操作系統(tǒng)進(jìn)行一系列的處理操作,這會(huì)消耗大量的 CPU 資源,導(dǎo)致 CPU 的利用率升高,程序的執(zhí)行效率降低。
在一個(gè)需要頻繁進(jìn)行內(nèi)存分配和釋放的高性能計(jì)算程序中,如果全部使用 mmap 系統(tǒng)調(diào)用,系統(tǒng)調(diào)用的開(kāi)銷(xiāo)和頻繁的缺頁(yè)中斷會(huì)使得 CPU 忙于處理這些中斷和上下文切換,而無(wú)法專(zhuān)注于程序的核心計(jì)算任務(wù),從而導(dǎo)致程序的運(yùn)行速度大幅減慢,無(wú)法滿(mǎn)足高性能計(jì)算的需求。因此,在實(shí)際的內(nèi)存分配中,不能全部使用 mmap 系統(tǒng)調(diào)用,而是需要根據(jù)內(nèi)存分配的大小和具體的應(yīng)用場(chǎng)景,合理地選擇 brk 和 mmap 系統(tǒng)調(diào)用,以實(shí)現(xiàn)高效的內(nèi)存管理。
七、Malloc使用中的常見(jiàn)問(wèn)題與注意事項(xiàng)
7.1內(nèi)存泄漏風(fēng)險(xiǎn)
在使用 malloc 進(jìn)行內(nèi)存分配時(shí),內(nèi)存泄漏是一個(gè)需要特別關(guān)注的問(wèn)題。內(nèi)存泄漏就像是程序中的一個(gè)隱藏漏洞,它會(huì)逐漸吞噬系統(tǒng)的內(nèi)存資源,導(dǎo)致程序性能下降,甚至可能引發(fā)系統(tǒng)崩潰。在實(shí)際的編程中,尤其是在一些復(fù)雜的代碼邏輯中,內(nèi)存泄漏的問(wèn)題很容易被忽視。
假設(shè)我們有一個(gè)處理用戶(hù)數(shù)據(jù)的函數(shù),函數(shù)內(nèi)部使用 malloc 分配了一塊內(nèi)存來(lái)存儲(chǔ)用戶(hù)輸入的數(shù)據(jù)。代碼如下:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
void processUserData() {
char *userInput = (char *)malloc(100);
if (userInput == NULL) {
// 處理內(nèi)存分配失敗的情況
printf("Memory allocation failed\n");
return;
}
// 模擬獲取用戶(hù)輸入
strcpy(userInput, "Some user data");
// 假設(shè)這里有一些復(fù)雜的邏輯處理用戶(hù)數(shù)據(jù)
// 錯(cuò)誤示范:忘記釋放內(nèi)存
// free(userInput);
}
在這個(gè)例子中,processUserData函數(shù)使用 malloc 分配了 100 字節(jié)的內(nèi)存來(lái)存儲(chǔ)用戶(hù)輸入。在函數(shù)執(zhí)行過(guò)程中,我們對(duì)這塊內(nèi)存進(jìn)行了一些操作,比如模擬獲取用戶(hù)輸入并存儲(chǔ)到這塊內(nèi)存中。但是,在函數(shù)結(jié)束時(shí),我們忘記了調(diào)用 free 函數(shù)來(lái)釋放這塊內(nèi)存。隨著程序的不斷運(yùn)行,如果這個(gè)函數(shù)被頻繁調(diào)用,每次調(diào)用都會(huì)分配新的內(nèi)存而不釋放舊的內(nèi)存,那么系統(tǒng)的內(nèi)存資源會(huì)逐漸被耗盡,最終導(dǎo)致內(nèi)存泄漏。
為了檢測(cè)內(nèi)存泄漏問(wèn)題,我們可以借助一些工具,如 Valgrind 和 AddressSanitizer。Valgrind 是一款功能強(qiáng)大的內(nèi)存調(diào)試工具,它可以詳細(xì)地檢測(cè)出程序中的內(nèi)存泄漏情況,并給出具體的泄漏位置和相關(guān)的調(diào)用棧信息。使用 Valgrind 非常簡(jiǎn)單,只需要在命令行中運(yùn)行valgrind --leak-check=full your_program,其中your_program是你要檢測(cè)的可執(zhí)行程序。AddressSanitizer 則是一個(gè)由 LLVM 和 GCC 支持的內(nèi)存錯(cuò)誤檢測(cè)工具,它可以在編譯時(shí)啟用,通過(guò)在代碼中插入一些檢測(cè)代碼來(lái)實(shí)時(shí)檢測(cè)內(nèi)存泄漏和其他內(nèi)存相關(guān)的錯(cuò)誤。在 GCC 中,可以使用-fsanitize=address選項(xiàng)來(lái)啟用 AddressSanitizer 進(jìn)行編譯。
為了避免內(nèi)存泄漏,我們需要養(yǎng)成良好的編程習(xí)慣。在使用 malloc 分配內(nèi)存后,一定要記得在不再需要這塊內(nèi)存時(shí)調(diào)用 free 函數(shù)進(jìn)行釋放。可以在分配內(nèi)存的地方添加注釋?zhuān)嵝炎约涸谶m當(dāng)?shù)臅r(shí)候釋放內(nèi)存。同時(shí),對(duì)于一些復(fù)雜的函數(shù)邏輯,要仔細(xì)檢查是否存在分支路徑導(dǎo)致內(nèi)存沒(méi)有被釋放的情況。例如,在上面的processUserData函數(shù)中,如果在獲取用戶(hù)輸入時(shí)發(fā)生錯(cuò)誤,導(dǎo)致函數(shù)提前返回,那么之前分配的內(nèi)存也會(huì)泄漏。因此,在可能提前返回的地方,也需要確保釋放已經(jīng)分配的內(nèi)存。
7.2訪問(wèn)已釋放內(nèi)存
訪問(wèn)已釋放內(nèi)存(UAF,Use - After - Free)是一種在內(nèi)存管理中非常危險(xiǎn)的錯(cuò)誤行為。它指的是程序在使用 free 函數(shù)釋放了一塊內(nèi)存之后,仍然通過(guò)指向這塊內(nèi)存的指針來(lái)訪問(wèn)它。這種錯(cuò)誤就像是在拆除了一座房子后,還試圖進(jìn)入房子里尋找東西,其結(jié)果是不可預(yù)測(cè)的,可能會(huì)導(dǎo)致程序崩潰、數(shù)據(jù)損壞,甚至引發(fā)安全漏洞,讓攻擊者有機(jī)會(huì)執(zhí)行惡意代碼。
當(dāng)我們使用 brk 系統(tǒng)調(diào)用分配內(nèi)存時(shí),假設(shè)我們有以下代碼:
#include <stdio.h>
#include <stdlib.h>
int main() {
char *ptr = (char *)malloc(100);
if (ptr == NULL) {
printf("Memory allocation failed\n");
return 1;
}
// 使用ptr指向的內(nèi)存
strcpy(ptr, "Some data");
printf("Data: %s\n", ptr);
// 釋放內(nèi)存
free(ptr);
// 錯(cuò)誤示范:訪問(wèn)已釋放的內(nèi)存
printf("Data after free: %s\n", ptr);
return 0;
}
在這段代碼中,我們首先使用 malloc 分配了 100 字節(jié)的內(nèi)存,并將其賦值給指針ptr。然后,我們?cè)谶@塊內(nèi)存中存儲(chǔ)了一些數(shù)據(jù)并打印出來(lái)。接著,我們調(diào)用 free 函數(shù)釋放了這塊內(nèi)存。然而,在釋放內(nèi)存之后,我們又試圖通過(guò)ptr指針來(lái)訪問(wèn)這塊已經(jīng)被釋放的內(nèi)存,并打印其中的數(shù)據(jù)。由于內(nèi)存已經(jīng)被釋放,ptr指向的內(nèi)存區(qū)域已經(jīng)不再屬于我們的程序,此時(shí)訪問(wèn)這塊內(nèi)存會(huì)導(dǎo)致未定義行為。在某些情況下,程序可能會(huì)崩潰,提示段錯(cuò)誤(Segmentation fault);而在另一些情況下,可能會(huì)打印出一些隨機(jī)的數(shù)據(jù),因?yàn)檫@塊內(nèi)存可能已經(jīng)被操作系統(tǒng)重新分配給其他程序使用,其內(nèi)容已經(jīng)被修改。
如果是通過(guò) mmap 系統(tǒng)調(diào)用分配內(nèi)存,同樣會(huì)出現(xiàn)類(lèi)似的問(wèn)題。例如:
#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
int main() {
int fd = open("test.txt", O_RDWR | O_CREAT, 0666);
if (fd == -1) {
perror("open");
return 1;
}
// 擴(kuò)展文件大小
if (lseek(fd, 100 - 1, SEEK_SET) == -1) {
perror("lseek");
close(fd);
return 1;
}
if (write(fd, "", 1) == -1) {
perror("write");
close(fd);
return 1;
}
char *ptr = (char *)mmap(0, 100, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (ptr == MAP_FAILED) {
perror("mmap");
close(fd);
return 1;
}
// 使用ptr指向的內(nèi)存
strcpy(ptr, "Some data");
printf("Data: %s\n", ptr);
// 釋放內(nèi)存
if (munmap(ptr, 100) == -1) {
perror("munmap");
close(fd);
return 1;
}
// 錯(cuò)誤示范:訪問(wèn)已釋放的內(nèi)存
printf("Data after free: %s\n", ptr);
close(fd);
return 0;
}
在這個(gè)例子中,我們通過(guò) mmap 系統(tǒng)調(diào)用將文件映射到內(nèi)存中,并獲取了一個(gè)指向映射內(nèi)存區(qū)域的指針ptr。在使用完內(nèi)存后,我們調(diào)用 munmap 函數(shù)釋放了這塊內(nèi)存。但之后又試圖訪問(wèn)已經(jīng)釋放的內(nèi)存,這同樣會(huì)導(dǎo)致未定義行為。與 brk 方式不同的是,mmap 分配的內(nèi)存通常與文件映射相關(guān),訪問(wèn)已釋放的 mmap 內(nèi)存可能會(huì)導(dǎo)致更復(fù)雜的問(wèn)題,比如影響文件系統(tǒng)的一致性,因?yàn)槲募成涞膬?nèi)存與文件的內(nèi)容是關(guān)聯(lián)的。如果在釋放后還繼續(xù)訪問(wèn),可能會(huì)導(dǎo)致文件內(nèi)容被錯(cuò)誤地修改,從而引發(fā)數(shù)據(jù)損壞的問(wèn)題。