自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

我做了個(gè)實(shí)驗(yàn)-malloc如何動(dòng)態(tài)內(nèi)存分配?

系統(tǒng) Linux
在 Linux 操作系統(tǒng)中,虛擬地址空間的內(nèi)部又被分為內(nèi)核空間和用戶空間兩部分,不同位數(shù)的系統(tǒng),地址空間的范圍也不同。

最近想多寫一些內(nèi)存管理的文章,這次我們就以 malloc 動(dòng)態(tài)內(nèi)存分配為切入點(diǎn),我在文中也做了小實(shí)驗(yàn):

  • malloc 是如何分配內(nèi)存的?
  • malloc 分配的是物理內(nèi)存嗎?
  • malloc(1) 會(huì)分配多大的內(nèi)存?
  • free 釋放內(nèi)存,會(huì)歸還給操作系統(tǒng)嗎?

free() 函數(shù)只傳入一個(gè)內(nèi)存地址,為什么能知道要釋放多大的內(nèi)存?

發(fā)車!

Linux 進(jìn)程的內(nèi)存分布長(zhǎng)什么樣?

在 Linux 操作系統(tǒng)中,虛擬地址空間的內(nèi)部又被分為內(nèi)核空間和用戶空間兩部分,不同位數(shù)的系統(tǒng),地址空間的范圍也不同。比如最常見的 32 位和 64 位系統(tǒng),如下所示:

通過這里可以看出:

  • 32 位系統(tǒng)的內(nèi)核空間占用 1G,位于最高處,剩下的 3G 是用戶空間;
  • 64 位系統(tǒng)的內(nèi)核空間和用戶空間都是 128T,分別占據(jù)整個(gè)內(nèi)存空間的最高和最低處,剩下的中間部分是未定義的。

再來說說,內(nèi)核空間與用戶空間的區(qū)別:

  • 進(jìn)程在用戶態(tài)時(shí),只能訪問用戶空間內(nèi)存
  • 只有進(jìn)入內(nèi)核態(tài)后,才可以訪問內(nèi)核空間的內(nèi)存;

雖然每個(gè)進(jìn)程都各自有獨(dú)立的虛擬內(nèi)存,但是每個(gè)虛擬內(nèi)存中的內(nèi)核地址,其實(shí)關(guān)聯(lián)的都是相同的物理內(nèi)存。這樣,進(jìn)程切換到內(nèi)核態(tài)后,就可以很方便地訪問內(nèi)核空間內(nèi)存。

接下來,進(jìn)一步了解虛擬空間的劃分情況,用戶空間和內(nèi)核空間劃分的方式是不同的,內(nèi)核空間的分布情況就不多說了。

我們看看用戶空間分布的情況,以 32 位系統(tǒng)為例,我畫了一張圖來表示它們的關(guān)系:

通過這張圖你可以看到,用戶空間內(nèi)存從低到高分別是 6 種不同的內(nèi)存段:

  • 程序文件段,包括二進(jìn)制可執(zhí)行代碼
  • 已初始化數(shù)據(jù)段,包括靜態(tài)常量
  • 未初始化數(shù)據(jù)段,包括未初始化的靜態(tài)變量;
  • 堆段,包括動(dòng)態(tài)分配的內(nèi)存,從低地址開始向上增長(zhǎng);
  • 文件映射段,包括動(dòng)態(tài)庫、共享內(nèi)存等,從低地址開始向上增長(zhǎng)(跟硬件和內(nèi)核版本有關(guān) )
  • 棧段,包括局部變量和函數(shù)調(diào)用的上下文等。棧的大小是固定的,一般是 8 MB。當(dāng)然系統(tǒng)也提供了參數(shù),以便我們自定義大小

在這 6 個(gè)內(nèi)存段中,堆和文件映射段的內(nèi)存是動(dòng)態(tài)分配的。比如說,使用 C 標(biāo)準(zhǔn)庫的 malloc() 或者 mmap() ,就可以分別在堆和文件映射段動(dòng)態(tài)分配內(nèi)存。

malloc 是如何分配內(nèi)存的?

實(shí)際上,malloc() 并不是系統(tǒng)調(diào)用,而是 C 庫里的函數(shù),用于動(dòng)態(tài)分配內(nèi)存。

malloc 申請(qǐng)內(nèi)存的時(shí)候,會(huì)有兩種方式向操作系統(tǒng)申請(qǐng)堆內(nèi)存。

  • 方式一:通過 brk() 系統(tǒng)調(diào)用從堆分配內(nèi)存
  • 方式二:通過 mmap() 系統(tǒng)調(diào)用在文件映射區(qū)域分配內(nèi)存

方式一實(shí)現(xiàn)的方式很簡(jiǎn)單,就是通過 brk() 函數(shù)將「堆頂」指針向高地址移動(dòng),獲得新的內(nèi)存空間。如下圖:

方式二通過 mmap() 系統(tǒng)調(diào)用中「私有匿名映射」的方式,在文件映射區(qū)分配一塊內(nèi)存,也就是從文件映射區(qū)“偷”了一塊內(nèi)存。如下圖:

什么場(chǎng)景下 malloc() 會(huì)通過 brk() 分配內(nèi)存?又是什么場(chǎng)景下通過 mmap() 分配內(nèi)存?

malloc() 源碼里默認(rèn)定義了一個(gè)閾值:

  • 如果用戶分配的內(nèi)存小于 128 KB,則通過 brk() 申請(qǐng)內(nèi)存;
  • 如果用戶分配的內(nèi)存大于 128 KB,則通過 mmap() 申請(qǐng)內(nèi)存;

malloc() 分配的是物理內(nèi)存嗎?

不是的,malloc() 分配的是虛擬內(nèi)存。

如果分配后的虛擬內(nèi)存沒有被訪問的話,是不會(huì)將虛擬內(nèi)存不會(huì)映射到物理內(nèi)存,這樣就不會(huì)占用物理內(nèi)存了。

只有在訪問已分配的虛擬地址空間的時(shí)候,操作系統(tǒng)通過查找頁表,發(fā)現(xiàn)虛擬內(nèi)存對(duì)應(yīng)的頁沒有在物理內(nèi)存中,就會(huì)觸發(fā)缺頁中斷,然后操作系統(tǒng)會(huì)建立虛擬內(nèi)存和物理內(nèi)存之間的映射關(guān)系。

malloc(1) 會(huì)分配多大的虛擬內(nèi)存?

malloc() 在分配內(nèi)存的時(shí)候,并不是老老實(shí)實(shí)按用戶預(yù)期申請(qǐng)的字節(jié)數(shù)來分配內(nèi)存空間大小,而是會(huì)預(yù)分配更大的空間作為內(nèi)存池。

具體會(huì)預(yù)分配多大的空間,跟 malloc 使用的內(nèi)存管理器有關(guān)系,我們就以 malloc 默認(rèn)的內(nèi)存管理器(Ptmalloc2)來分析。

接下里,我們做個(gè)實(shí)驗(yàn),用下面這個(gè)代碼,通過 malloc 申請(qǐng) 1字節(jié)的內(nèi)存時(shí),看看操作系統(tǒng)實(shí)際分配了多大的內(nèi)存空間。

#include <stdio.h>
#include <malloc.h>

int main() {
printf("使用cat /proc/%d/maps查看內(nèi)存分配\n",getpid());

//申請(qǐng)1字節(jié)的內(nèi)存
void *addr = malloc(1);
printf("此1字節(jié)的內(nèi)存起始地址:%x\n", addr);
printf("使用cat /proc/%d/maps查看內(nèi)存分配\n",getpid());

//將程序阻塞,當(dāng)輸入任意字符時(shí)才往下執(zhí)行
getchar();

//釋放內(nèi)存
free(addr);
printf("釋放了1字節(jié)的內(nèi)存,但heap堆并不會(huì)釋放\n");

getchar();
return 0;
}

執(zhí)行代碼:

我們可以通過 /proc//maps 文件查看進(jìn)程的內(nèi)存分布情況。我在 maps 文件通過此 1 字節(jié)的內(nèi)存起始地址過濾出了內(nèi)存地址的范圍。

[root@xiaolin ~]# cat /proc/3191/maps | grep d730
00d73000-00d94000 rw-p 00000000 00:00 0

這個(gè)例子分配的內(nèi)存小于 128 KB,所以是通過 brk() 系統(tǒng)調(diào)用向堆空間申請(qǐng)的內(nèi)存,因此可以看到最右邊有 [heap] 的標(biāo)識(shí)。

可以看到,堆空間的內(nèi)存地址范圍是 00d73000-00d94000,這個(gè)范圍大小是 132KB,也就說明了 malloc(1) 實(shí)際上預(yù)分配 132K 字節(jié)的內(nèi)存。

可能有的同學(xué)注意到了,程序里打印的內(nèi)存起始地址是 d73010,而 maps 文件顯示堆內(nèi)存空間的起始地址是 d73000,為什么會(huì)多出來 0x10 (16字節(jié))呢?這個(gè)問題,我們先放著,后面會(huì)說。

free 釋放內(nèi)存,會(huì)歸還給操作系統(tǒng)嗎?

我們?cè)谏厦娴倪M(jìn)程往下執(zhí)行,看看通過 free() 函數(shù)釋放內(nèi)存后,堆內(nèi)存還在嗎?

從下圖可以看到,通過 free 釋放內(nèi)存后,堆內(nèi)存還是存在的,并沒有歸還給操作系統(tǒng)。

這是因?yàn)榕c其把這 1 字節(jié)釋放給操作系統(tǒng),不如先緩存著放進(jìn) malloc 的內(nèi)存池里,當(dāng)進(jìn)程再次申請(qǐng) 1 字節(jié)的內(nèi)存時(shí)就可以直接復(fù)用,這樣速度快了很多。

當(dāng)然,當(dāng)進(jìn)程退出后,操作系統(tǒng)就會(huì)回收進(jìn)程的所有資源。

上面說的 free 內(nèi)存后堆內(nèi)存還存在,是針對(duì) malloc 通過 brk() 方式申請(qǐng)的內(nèi)存的情況。

如果 malloc 通過 mmap 方式申請(qǐng)的內(nèi)存,free 釋放內(nèi)存后就會(huì)歸歸還給操作系統(tǒng)。

我們做個(gè)實(shí)驗(yàn)驗(yàn)證下, 通過 malloc 申請(qǐng) 128 KB 字節(jié)的內(nèi)存,來使得 malloc 通過 mmap 方式來分配內(nèi)存。

#include <stdio.h>
#include <malloc.h>

int main() {
//申請(qǐng)1字節(jié)的內(nèi)存
void *addr = malloc(128*1024);
printf("此128KB字節(jié)的內(nèi)存起始地址:%x\n", addr);
printf("使用cat /proc/%d/maps查看內(nèi)存分配\n",getpid());

//將程序阻塞,當(dāng)輸入任意字符時(shí)才往下執(zhí)行
getchar();

//釋放內(nèi)存
free(addr);
printf("釋放了128KB字節(jié)的內(nèi)存,內(nèi)存也歸還給了操作系統(tǒng)\n");

getchar();
return 0;
}

執(zhí)行代碼:

查看進(jìn)程的內(nèi)存的分布情況,可以發(fā)現(xiàn)最右邊沒有 [head] 標(biāo)志,說明是通過 mmap 以匿名映射的方式從文件映射區(qū)分配的匿名內(nèi)存。

然后我們釋放掉這個(gè)內(nèi)存看看:

再次查看該 128 KB 內(nèi)存的起始地址,可以發(fā)現(xiàn)已經(jīng)不存在了,說明歸還給了操作系統(tǒng)。

對(duì)于 「malloc 申請(qǐng)的內(nèi)存,free 釋放內(nèi)存會(huì)歸還給操作系統(tǒng)嗎?」這個(gè)問題,我們可以做個(gè)總結(jié)了:

  • malloc 通過 brk() 方式申請(qǐng)的內(nèi)存,free 釋放內(nèi)存的時(shí)候,并不會(huì)把內(nèi)存歸還給操作系統(tǒng),而是緩存在 malloc 的內(nèi)存池中,待下次使用;
  • malloc 通過 mmap() 方式申請(qǐng)的內(nèi)存,free 釋放內(nèi)存的時(shí)候,會(huì)把內(nèi)存歸還給操作系統(tǒng),內(nèi)存得到真正的釋放。

為什么不全部使用 mmap 來分配內(nèi)存?

因?yàn)橄虿僮飨到y(tǒng)申請(qǐng)內(nèi)存,是要通過系統(tǒng)調(diào)用的,執(zhí)行系統(tǒng)調(diào)用是要進(jìn)入內(nèi)核態(tài)的,然后在回到用戶態(tài),運(yùn)行態(tài)的切換會(huì)耗費(fèi)不少時(shí)間。

所以,申請(qǐng)內(nèi)存的操作應(yīng)該避免頻繁的系統(tǒng)調(diào)用,如果都用 mmap 來分配內(nèi)存,等于每次都要執(zhí)行系統(tǒng)調(diào)用。

另外,因?yàn)?mmap 分配的內(nèi)存每次釋放的時(shí)候,都會(huì)歸還給操作系統(tǒng),于是每次 mmap 分配的虛擬地址都是缺頁狀態(tài)的,然后在第一次訪問該虛擬地址的時(shí)候,就會(huì)觸發(fā)缺頁中斷。

也就是說,頻繁通過 mmap 分配的內(nèi)存話,不僅每次都會(huì)發(fā)生運(yùn)行態(tài)的切換,還會(huì)發(fā)生缺頁中斷(在第一次訪問虛擬地址后),這樣會(huì)導(dǎo)致 CPU 消耗較大。

為了改進(jìn)這兩個(gè)問題,malloc 通過 brk() 系統(tǒng)調(diào)用在堆空間申請(qǐng)內(nèi)存的時(shí)候,由于堆空間是連續(xù)的,所以直接預(yù)分配更大的內(nèi)存來作為內(nèi)存池,當(dāng)內(nèi)存釋放的時(shí)候,就緩存在內(nèi)存池中。

等下次在申請(qǐng)內(nèi)存的時(shí)候,就直接從內(nèi)存池取出對(duì)應(yīng)的內(nèi)存塊就行了,而且可能這個(gè)內(nèi)存塊的虛擬地址與物理地址的映射關(guān)系還存在,這樣不僅減少了系統(tǒng)調(diào)用的次數(shù),也減少了缺頁中斷的次數(shù),這將大大降低 CPU 的消耗。

既然 brk 那么牛逼,為什么不全部使用 brk 來分配?

前面我們提到通過 brk 從堆空間分配的內(nèi)存,并不會(huì)歸還給操作系統(tǒng),那么我們那考慮這樣一個(gè)場(chǎng)景。

如果我們連續(xù)申請(qǐng)了 10k,20k,30k 這三片內(nèi)存,如果 10k 和 20k 這兩片釋放了,變?yōu)榱丝臻e內(nèi)存空間,如果下次申請(qǐng)的內(nèi)存小于 30k,那么就可以重用這個(gè)空閑內(nèi)存空間。

但是如果下次申請(qǐng)的內(nèi)存大于 30k,沒有可用的空閑內(nèi)存空間,必須向 OS 申請(qǐng),實(shí)際使用內(nèi)存繼續(xù)增大。

因此,隨著系統(tǒng)頻繁地 malloc 和 free ,尤其對(duì)于小塊內(nèi)存,堆內(nèi)將產(chǎn)生越來越多不可用的碎片,導(dǎo)致“內(nèi)存泄露”。而這種“泄露”現(xiàn)象使用 valgrind 是無法檢測(cè)出來的。

所以,malloc 實(shí)現(xiàn)中,充分考慮了 sbrk 和 mmap 行為上的差異及優(yōu)缺點(diǎn),默認(rèn)分配大塊內(nèi)存 (128KB) 才使用 mmap 分配內(nèi)存空間。

free() 函數(shù)只傳入一個(gè)內(nèi)存地址,為什么能知道要釋放多大的內(nèi)存?

還記得,我前面提到, malloc 返回給用戶態(tài)的內(nèi)存起始地址比進(jìn)程的堆空間起始地址多了 16 字節(jié)嗎?

這個(gè)多出來的 16 字節(jié)就是保存了該內(nèi)存塊的描述信息,比如有該內(nèi)存塊的大小。

這樣當(dāng)執(zhí)行 free() 函數(shù)時(shí),free 會(huì)對(duì)傳入進(jìn)來的內(nèi)存地址向左偏移 16 字節(jié),然后從這個(gè) 16 字節(jié)的分析出當(dāng)前的內(nèi)存塊的大小,自然就知道要釋放多大的內(nèi)存了。

責(zé)任編輯:武曉燕 來源: 小林coding
相關(guān)推薦

2022-01-13 10:30:21

C語言內(nèi)存動(dòng)態(tài)

2022-01-07 15:10:53

C++動(dòng)態(tài)內(nèi)存

2024-10-11 10:00:20

2010-08-18 10:05:28

Hyper-V動(dòng)態(tài)內(nèi)存

2023-12-27 13:55:00

C++內(nèi)存分配機(jī)制new

2010-12-09 10:03:17

Hyper-V R2

2010-03-02 08:53:59

Windows 8動(dòng)態(tài)內(nèi)存

2010-03-01 09:09:21

Windows 8動(dòng)態(tài)內(nèi)存

2024-01-26 16:28:28

C++動(dòng)態(tài)內(nèi)存開發(fā)

2022-04-26 06:21:59

編程動(dòng)態(tài)內(nèi)存

2025-02-10 03:00:00

2012-04-01 14:38:06

Windows Ser虛擬化

2025-03-26 00:00:05

2018-06-06 08:28:37

Spark內(nèi)存管理

2011-05-24 16:39:09

Cfree()

2011-07-28 10:03:53

Hyper-V動(dòng)態(tài)內(nèi)存

2011-07-20 13:47:14

CC++

2015-11-26 11:02:37

微軟LinuxHyper-V

2022-02-22 20:35:22

公鑰私鑰數(shù)據(jù)

2025-02-10 07:30:00

malloc內(nèi)存分配器內(nèi)存
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)