linux环境内存分配原理
Linux的虛擬內(nèi)存管理有幾個(gè)關(guān)鍵概念:
Linux 虛擬地址空間如何分布?malloc和free是如何分配和釋放內(nèi)存?如何查看堆內(nèi)內(nèi)存的碎片情況?既然堆內(nèi)內(nèi)存brk和sbrk不能直接釋放,為什么不全部使用 mmap 來分配,munmap直接釋放呢 ?
Linux 的虛擬內(nèi)存管理有幾個(gè)關(guān)鍵概念:
1、每個(gè)進(jìn)程都有獨(dú)立的虛擬地址空間,進(jìn)程訪問的虛擬地址并不是真正的物理地址;
2、虛擬地址可通過每個(gè)進(jìn)程上的頁表(在每個(gè)進(jìn)程的內(nèi)核虛擬地址空間)與物理地址進(jìn)行映射,獲得真正物理地址;
3、如果虛擬地址對(duì)應(yīng)物理地址不在物理內(nèi)存中,則產(chǎn)生缺頁中斷,真正分配物理地址,同時(shí)更新進(jìn)程的頁表;如果此時(shí)物理內(nèi)存已耗盡,則根據(jù)內(nèi)存替換算法淘汰部分頁面至物理磁盤中。
??
一、Linux 虛擬地址空間如何分布?
Linux 使用虛擬地址空間,大大增加了進(jìn)程的尋址空間,由低地址到高地址分別為:
1、只讀段:該部分空間只能讀,不可寫;(包括:代碼段、rodata 段(C常量字符串和#define定義的常量) )
2、數(shù)據(jù)段:保存全局變量、靜態(tài)變量的空間;
3、堆 :就是平時(shí)所說的動(dòng)態(tài)內(nèi)存, malloc/new 大部分都來源于此。其中堆頂?shù)奈恢每赏ㄟ^函數(shù) brk 和 sbrk 進(jìn)行動(dòng)態(tài)調(diào)整。
4、文件映射區(qū)域 :如動(dòng)態(tài)庫、共享內(nèi)存等映射物理空間的內(nèi)存,一般是 mmap 函數(shù)所分配的虛擬地址空間。
5、棧:用于維護(hù)函數(shù)調(diào)用的上下文空間,一般為 8M ,可通過 ulimit –s 查看。
6、內(nèi)核虛擬空間:用戶代碼不可見的內(nèi)存區(qū)域,由內(nèi)核管理(頁表就存放在內(nèi)核虛擬空間)。
下圖是 32 位系統(tǒng)典型的虛擬地址空間分布(來自《深入理解計(jì)算機(jī)系統(tǒng)》)。
32 位系統(tǒng)有4G 的地址空間::
??????其中 0x08048000~0xbfffffff 是用戶空間,0xc0000000~0xffffffff 是內(nèi)核空間,包括內(nèi)核代碼和數(shù)據(jù)、與進(jìn)程相關(guān)的數(shù)據(jù)結(jié)構(gòu)(如頁表、內(nèi)核棧)等。另外,%esp 執(zhí)行棧頂,往低地址方向變化;brk/sbrk 函數(shù)控制堆頂_edata往高地址方向變化。
64位系統(tǒng)結(jié)果怎樣呢? 64 位系統(tǒng)是否擁有 2^64 的地址空間嗎?
事實(shí)上, 64 位系統(tǒng)的虛擬地址空間劃分發(fā)生了改變:
1、地址空間大小不是2^32,也不是2^64,而一般是2^48。
因?yàn)椴⒉恍枰?2^64 這么大的尋址空間,過大空間只會(huì)導(dǎo)致資源的浪費(fèi)。64位Linux一般使用48位來表示虛擬地址空間,40位表示物理地址,
這可通過#cat? /proc/cpuinfo 來查看:
2、其中,0x0000000000000000~0x00007fffffffffff 表示用戶空間, 0xFFFF800000000000~ 0xFFFFFFFFFFFFFFFF 表示內(nèi)核空間,共提供 256TB(2^48) 的尋址空間。
這兩個(gè)區(qū)間的特點(diǎn)是,第 47 位與 48~63 位相同,若這些位為 0 表示用戶空間,否則表示內(nèi)核空間。
3、用戶空間由低地址到高地址仍然是只讀段、數(shù)據(jù)段、堆、文件映射區(qū)域和棧;
二、malloc和free是如何分配和釋放內(nèi)存?
如何查看進(jìn)程發(fā)生缺頁中斷的次數(shù)?
?????????用# ps -o majflt,minflt -C program 命令查看
????????? majflt代表major fault,中文名叫大錯(cuò)誤,minflt代表minor fault,中文名叫小錯(cuò)誤。
????????? 這兩個(gè)數(shù)值表示一個(gè)進(jìn)程自啟動(dòng)以來所發(fā)生的缺頁中斷的次數(shù)。
可以用命令ps -o majflt minflt -C program來查看進(jìn)程的majflt, minflt的值,這兩個(gè)值都是累加值,從進(jìn)程啟動(dòng)開始累加。在對(duì)高性能要求的程序做壓力測試的時(shí)候,我們可以多關(guān)注一下這兩個(gè)值。?
如果一個(gè)進(jìn)程使用了mmap將很大的數(shù)據(jù)文件映射到進(jìn)程的虛擬地址空間,我們需要重點(diǎn)關(guān)注majflt的值,因?yàn)橄啾萴inflt,majflt對(duì)于性能的損害是致命的,隨機(jī)讀一次磁盤的耗時(shí)數(shù)量級(jí)在幾個(gè)毫秒,而minflt只有在大量的時(shí)候才會(huì)對(duì)性能產(chǎn)生影響。
發(fā)成缺頁中斷后,執(zhí)行了那些操作?
當(dāng)一個(gè)進(jìn)程發(fā)生缺頁中斷的時(shí)候,進(jìn)程會(huì)陷入內(nèi)核態(tài),執(zhí)行以下操作:
1、檢查要訪問的虛擬地址是否合法
2、查找/分配一個(gè)物理頁
3、填充物理頁內(nèi)容(讀取磁盤,或者直接置0,或者啥也不干)
4、建立映射關(guān)系(虛擬地址到物理地址)
重新執(zhí)行發(fā)生缺頁中斷的那條指令
如果第3步,需要讀取磁盤,那么這次缺頁中斷就是majflt,否則就是minflt。
內(nèi)存分配的原理
從操作系統(tǒng)角度來看,進(jìn)程分配內(nèi)存有兩種方式,分別由兩個(gè)系統(tǒng)調(diào)用完成:brk和mmap(不考慮共享內(nèi)存)。
1、brk是將數(shù)據(jù)段(.data)的最高地址指針_edata往高地址推;
2、mmap是在進(jìn)程的虛擬地址空間中(堆和棧中間,稱為文件映射區(qū)域的地方)找一塊空閑的虛擬內(nèi)存。
?????這兩種方式分配的都是虛擬內(nèi)存,沒有分配物理內(nèi)存。在第一次訪問已分配的虛擬地址空間的時(shí)候,發(fā)生缺頁中斷,操作系統(tǒng)負(fù)責(zé)分配物理內(nèi)存,然后建立虛擬內(nèi)存和物理內(nèi)存之間的映射關(guān)系。
在標(biāo)準(zhǔn)C庫中,提供了malloc/free函數(shù)分配釋放內(nèi)存,這兩個(gè)函數(shù)底層是由brk,mmap,munmap這些系統(tǒng)調(diào)用實(shí)現(xiàn)的。
下面以一個(gè)例子來說明內(nèi)存分配的原理:
情況一、malloc小于128k的內(nèi)存,使用brk分配內(nèi)存,將_edata往高地址推(只分配虛擬空間,不對(duì)應(yīng)物理內(nèi)存(因此沒有初始化),第一次讀/寫數(shù)據(jù)時(shí),引起內(nèi)核缺頁中斷,內(nèi)核才分配對(duì)應(yīng)的物理內(nèi)存,然后虛擬地址空間建立映射關(guān)系),如下圖:
1、進(jìn)程啟動(dòng)的時(shí)候,其(虛擬)內(nèi)存空間的初始布局如圖1所示。 ????? 其中,mmap內(nèi)存映射文件是在堆和棧的中間(例如libc-2.2.93.so,其它數(shù)據(jù)文件等),為了簡單起見,省略了內(nèi)存映射文件。 ????? _edata指針(glibc里面定義)指向數(shù)據(jù)段的最高地址。2、進(jìn)程調(diào)用A=malloc(30K)以后,內(nèi)存空間如圖2: ??????malloc函數(shù)會(huì)調(diào)用brk系統(tǒng)調(diào)用,將_edata指針往高地址推30K,就完成虛擬內(nèi)存分配。 ????? 你可能會(huì)問:只要把_edata+30K就完成內(nèi)存分配了? ??????事實(shí)是這樣的,_edata+30K只是完成虛擬地址的分配,A這塊內(nèi)存現(xiàn)在還是沒有物理頁與之對(duì)應(yīng)的,等到進(jìn)程第一次讀寫A這塊內(nèi)存的時(shí)候,發(fā)生缺頁中斷,這個(gè)時(shí)候,內(nèi)核才分配A這塊內(nèi)存對(duì)應(yīng)的物理頁。也就是說,如果用malloc分配了A這塊內(nèi)容,然后從來不訪問它,那么,A對(duì)應(yīng)的物理頁是不會(huì)被分配的。
3、進(jìn)程調(diào)用B=malloc(40K)以后,內(nèi)存空間如圖3。
情況二、malloc大于128k的內(nèi)存,使用mmap分配內(nèi)存,在堆和棧之間找一塊空閑內(nèi)存分配(對(duì)應(yīng)獨(dú)立內(nèi)存,而且初始化為0),如下圖:
4、進(jìn)程調(diào)用C=malloc(200K)以后,內(nèi)存空間如圖4: ??????默認(rèn)情況下,malloc函數(shù)分配內(nèi)存,如果請(qǐng)求內(nèi)存大于128K(可由M_MMAP_THRESHOLD選項(xiàng)調(diào)節(jié)),那就不是去推_edata指針了,而是利用mmap系統(tǒng)調(diào)用,從堆和棧的中間分配一塊虛擬內(nèi)存。 ????? 這樣子做主要是因?yàn)?: ????? brk分配的內(nèi)存需要等到高地址內(nèi)存釋放以后才能釋放(例如,在B釋放之前,A是不可能釋放的,這就是內(nèi)存碎片產(chǎn)生的原因,什么時(shí)候緊縮看下面),而mmap分配的內(nèi)存可以單獨(dú)釋放。 ??????當(dāng)然,還有其它的好處,也有壞處,再具體下去,有興趣的同學(xué)可以去看glibc里面malloc的代碼了。5、進(jìn)程調(diào)用D=malloc(100K)以后,內(nèi)存空間如圖5;
6、進(jìn)程調(diào)用free(C)以后,C對(duì)應(yīng)的虛擬內(nèi)存和物理內(nèi)存一起釋放。 7、進(jìn)程調(diào)用free(B)以后,如圖7所示: ??????? B對(duì)應(yīng)的虛擬內(nèi)存和物理內(nèi)存都沒有釋放,因?yàn)橹挥幸粋€(gè)_edata指針,如果往回推,那么D這塊內(nèi)存怎么辦呢? 當(dāng)然,B這塊內(nèi)存,是可以重用的,如果這個(gè)時(shí)候再來一個(gè)40K的請(qǐng)求,那么malloc很可能就把B這塊內(nèi)存返回回去了。
8、進(jìn)程調(diào)用free(D)以后,如圖8所示: ??????? B和D連接起來,變成一塊140K的空閑內(nèi)存。 9、默認(rèn)情況下: ?????? 當(dāng)最高地址空間的空閑內(nèi)存超過128K(可由M_TRIM_THRESHOLD選項(xiàng)調(diào)節(jié))時(shí),執(zhí)行內(nèi)存緊縮操作(trim)。在上一個(gè)步驟free的時(shí)候,發(fā)現(xiàn)最高地址空閑內(nèi)存超過128K,于是內(nèi)存緊縮,變成圖9所示。
真相大白
說完內(nèi)存分配的原理,那么被測模塊在內(nèi)核態(tài)cpu消耗高的原因就很清楚了:每次請(qǐng)求來都malloc一塊2M的內(nèi)存,默認(rèn)情況下,malloc調(diào)用mmap分配內(nèi)存,請(qǐng)求結(jié)束的時(shí)候,調(diào)用munmap釋放內(nèi)存。假設(shè)每個(gè)請(qǐng)求需要6個(gè)物理頁,那么每個(gè)請(qǐng)求就會(huì)產(chǎn)生6個(gè)缺頁中斷,在2000的壓力下,每秒就產(chǎn)生了10000多次缺頁中斷,這些缺頁中斷不需要讀取磁盤解決,所以叫做minflt;缺頁中斷在內(nèi)核態(tài)執(zhí)行,因此進(jìn)程的內(nèi)核態(tài)cpu消耗很大。缺頁中斷分散在整個(gè)請(qǐng)求的處理過程中,所以表現(xiàn)為分配語句耗時(shí)(10us)相對(duì)于整條請(qǐng)求的處理時(shí)間(1000us)比重很小。
解決辦法
將動(dòng)態(tài)內(nèi)存改為靜態(tài)分配,或者啟動(dòng)的時(shí)候,用malloc為每個(gè)線程分配,然后保存在threaddata里面。但是,由于這個(gè)模塊的特殊性,靜態(tài)分配,或者啟動(dòng)時(shí)候分配都不可行。另外,Linux下默認(rèn)棧的大小限制是10M,如果在棧上分配幾M的內(nèi)存,有風(fēng)險(xiǎn)。?
禁止malloc調(diào)用mmap分配內(nèi)存,禁止內(nèi)存緊縮。
在進(jìn)程啟動(dòng)時(shí)候,加入以下兩行代碼:
mallopt(M_MMAP_MAX, 0); // 禁止malloc調(diào)用mmap分配內(nèi)存
mallopt(M_TRIM_THRESHOLD, -1); // 禁止內(nèi)存緊縮
效果:加入這兩行代碼以后,用ps命令觀察,壓力穩(wěn)定以后,majlt和minflt都為0。進(jìn)程的系統(tǒng)態(tài)cpu從20降到10。
三、如何查看堆內(nèi)內(nèi)存的碎片情況??
glibc 提供了以下結(jié)構(gòu)和接口來查看堆內(nèi)內(nèi)存和 mmap 的使用情況。
struct mallinfo {
? int arena;?????????? ?/* non-mmapped space allocated from system */
? int ordblks;? ?????? /* number of free chunks */
? int smblks;???????? ?/* number of fastbin blocks */
? int hblks;??????????? ?/* number of mmapped regions */
? int hblkhd;???????? ??/* space in mmapped regions */
? int usmblks;????? ??/* maximum total allocated space */
? int fsmblks;? ?????? /* space available in freed fastbin blocks */
? int uordblks; ?????? /* total allocated space */
? int fordblks; ??????? /* total free space */
? int keepcost; ????? /* top-most, releasable (via malloc_trim) space */
};
/*返回heap(main_arena)的內(nèi)存使用情況,以 mallinfo 結(jié)構(gòu)返回 */
struct mallinfo mallinfo();
/* 將heap和mmap的使用情況輸出到stderr*/
void malloc_stats();
可通過以下例子來驗(yàn)證mallinfo和malloc_stats輸出結(jié)果。
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/mman.h>
#include <malloc.h>
size_t? heap_malloc_total, heap_free_total,mmap_total, mmap_count;
void print_info()
{
??? struct mallinfo mi = mallinfo();
?
??? printf("count by itself:\n");
??? printf("\theap_malloc_total=%lu heap_free_total=%lu heap_in_use=%lu\n\tmmap_total=%lu mmap_count=%lu\n",
????????????? heap_malloc_total*1024, heap_free_total*1024, heap_malloc_total*1024-heap_free_total*1024,
???????????? ?mmap_total*1024, mmap_count);
?
?printf("count by mallinfo:\n");
?printf("\theap_malloc_total=%lu heap_free_total=%lu heap_in_use=%lu\n\tmmap_total=%lu mmap_count=%lu\n",
???????????? mi.arena, mi.fordblks, mi.uordblks,
???????????? mi.hblkhd, mi.hblks);
?
?printf("from malloc_stats:\n");
?malloc_stats();
}
#define ARRAY_SIZE 200
int main(int argc, char** argv)
{
??? char** ptr_arr[ARRAY_SIZE];
??? int i;?
????for( i = 0; i < ARRAY_SIZE; i++)
??? {
??????????? ptr_arr[i] = malloc(i * 1024);?
????????????if ( i < 128)??????????????????????????????????????//glibc默認(rèn)128k以上使用mmap
??????????? {
??????????????????? heap_malloc_total += i;
?????????? ?}
??????????? else
??????????? {
????????????????????mmap_total += i;
????????????????? ?mmap_count++;
?????????? ?}
??? }?
??? print_info();?
??? for( i = 0; i < ARRAY_SIZE; i++)
??? {
?????????? if ( i % 2 == 0)
??????????????? continue;
?????????? free(ptr_arr[i]);
?????????? if ( i < 128)
?????????? {
?????????????????? heap_free_total += i;
?????????? }
?????????? else
?????????? {
????????????????? mmap_total -= i;
????????????????? mmap_count--;
?????????? }
??? }?
????
????printf("\nafter free\n");
??? print_info();?
??? return 1;
}
該例子第一個(gè)循環(huán)為指針數(shù)組每個(gè)成員分配索引位置 (KB) 大小的內(nèi)存塊,并通過 128 為分界分別對(duì) heap 和 mmap 內(nèi)存分配情況進(jìn)行計(jì)數(shù);
第二個(gè)循環(huán)是 free 索引下標(biāo)為奇數(shù)的項(xiàng),同時(shí)更新計(jì)數(shù)情況。通過程序的計(jì)數(shù)與mallinfo/malloc_stats 接口得到結(jié)果進(jìn)行對(duì)比,并通過 print_info打印到終端。
?
下面是一個(gè)執(zhí)行結(jié)果:
count by itself:
??????? heap_malloc_total=8323072 heap_free_total=0 heap_in_use=8323072
??????? mmap_total=12054528 mmap_count=72
??
count by mallinfo:
??????? heap_malloc_total=8327168 heap_free_total=2032 heap_in_use=8325136
??????? mmap_total=12238848 mmap_count=72
from malloc_stats:
Arena 0:
system bytes???? =??? 8327168
in use bytes???? =??? 8325136
Total (incl. mmap):
system bytes???? =?? 20566016
in use bytes???? =?? 20563984
max mmap regions =???????? 72
max mmap bytes?? =?? 12238848
after free
count by itself:
??????? heap_malloc_total=8323072 heap_free_total=4194304 heap_in_use=4128768
??????? mmap_total=6008832 mmap_count=36
count by mallinfo:
??????? heap_malloc_total=8327168 heap_free_total=4197360 heap_in_use=4129808
??????? mmap_total=6119424 mmap_count=36
from malloc_stats:
Arena 0:
system bytes???? =??? 8327168
in use bytes???? =??? 4129808
Total (incl. mmap):
system bytes???? =?? 14446592
in use bytes???? =?? 10249232
max mmap regions =???????? 72
max mmap bytes?? =?? 12238848
由上可知,程序統(tǒng)計(jì)和mallinfo 得到的信息基本吻合,其中 heap_free_total 表示堆內(nèi)已釋放的內(nèi)存碎片總和。?
?
?????? 如果想知道堆內(nèi)究竟有多少碎片,可通過 mallinfo 結(jié)構(gòu)中的 fsmblks 、smblks 、ordblks 值得到,這些值表示不同大小區(qū)間的碎片總個(gè)數(shù),這些區(qū)間分別是 0~80 字節(jié),80~512 字節(jié),512~128k。如果 fsmblks 、 smblks 的值過大,那碎片問題可能比較嚴(yán)重了。
??? 不過, mallinfo 結(jié)構(gòu)有一個(gè)很致命的問題,就是其成員定義全部都是 int ,在 64 位環(huán)境中,其結(jié)構(gòu)中的 uordblks/fordblks/arena/usmblks 很容易就會(huì)導(dǎo)致溢出,應(yīng)該是歷史遺留問題,使用時(shí)要注意!
?
四、既然堆內(nèi)內(nèi)存brk和sbrk不能直接釋放,為什么不全部使用 mmap 來分配,munmap直接釋放呢??
????????既然堆內(nèi)碎片不能直接釋放,導(dǎo)致疑似“內(nèi)存泄露”問題,為什么 malloc 不全部使用 mmap 來實(shí)現(xiàn)呢(mmap分配的內(nèi)存可以會(huì)通過 munmap 進(jìn)行 free ,實(shí)現(xiàn)真正釋放)?而是僅僅對(duì)于大于 128k 的大塊內(nèi)存才使用 mmap ??
??????? 其實(shí),進(jìn)程向 OS 申請(qǐng)和釋放地址空間的接口 sbrk/mmap/munmap 都是系統(tǒng)調(diào)用,頻繁調(diào)用系統(tǒng)調(diào)用都比較消耗系統(tǒng)資源的。并且, mmap 申請(qǐng)的內(nèi)存被 munmap 后,重新申請(qǐng)會(huì)產(chǎn)生更多的缺頁中斷。例如使用 mmap 分配 1M 空間,第一次調(diào)用產(chǎn)生了大量缺頁中斷 (1M/4K 次 ) ,當(dāng)munmap 后再次分配 1M 空間,會(huì)再次產(chǎn)生大量缺頁中斷。缺頁中斷是內(nèi)核行為,會(huì)導(dǎo)致內(nèi)核態(tài)CPU消耗較大。另外,如果使用 mmap 分配小內(nèi)存,會(huì)導(dǎo)致地址空間的分片更多,內(nèi)核的管理負(fù)擔(dān)更大。
????????同時(shí)堆是一個(gè)連續(xù)空間,并且堆內(nèi)碎片由于沒有歸還 OS ,如果可重用碎片,再次訪問該內(nèi)存很可能不需產(chǎn)生任何系統(tǒng)調(diào)用和缺頁中斷,這將大大降低 CPU 的消耗。 因此, glibc 的 malloc 實(shí)現(xiàn)中,充分考慮了 sbrk 和 mmap 行為上的差異及優(yōu)缺點(diǎn),默認(rèn)分配大塊內(nèi)存 (128k) 才使用 mmap 獲得地址空間,也可通過 mallopt(M_MMAP_THRESHOLD, <SIZE>) 來修改這個(gè)臨界值。
?
五、如何查看進(jìn)程的缺頁中斷信息?
可通過以下命令查看缺頁中斷信息
ps -o majflt,minflt -C <program_name>
ps -o majflt,minflt -p <pid>
其中::?majflt 代表 major fault ,指大錯(cuò)誤;
???????????minflt 代表 minor fault ,指小錯(cuò)誤。
這兩個(gè)數(shù)值表示一個(gè)進(jìn)程自啟動(dòng)以來所發(fā)生的缺頁中斷的次數(shù)。
其中 majflt 與 minflt 的不同是::
??????? majflt 表示需要讀寫磁盤,可能是內(nèi)存對(duì)應(yīng)頁面在磁盤中需要load 到物理內(nèi)存中,也可能是此時(shí)物理內(nèi)存不足,需要淘汰部分物理頁面至磁盤中。
參看:: http://blog.163.com/xychenbaihu@yeah/blog/static/132229655201210975312473/
?
六、除了 glibc 的 malloc/free ,還有其他第三方實(shí)現(xiàn)嗎?
??????? 其實(shí),很多人開始詬病 glibc 內(nèi)存管理的實(shí)現(xiàn),特別是高并發(fā)性能低下和內(nèi)存碎片化問題都比較嚴(yán)重,因此,陸續(xù)出現(xiàn)一些第三方工具來替換 glibc 的實(shí)現(xiàn),最著名的當(dāng)屬 google 的tcmalloc和facebook 的jemalloc 。
??????? 網(wǎng)上有很多資源,可以自己查(只用使用第三方庫,代碼不用修改,就可以使用第三方庫中的malloc)。
?
參考資料:
《深入理解計(jì)算機(jī)系統(tǒng)》第 10 章
http://www.kernel.org/doc/Documentation/x86/x86_64/mm.txt
https://www.ibm.com/developerworks/cn/linux/l-lvm64/
http://www.kerneltravel.net/journal/v/mem.htm
http://blog.csdn.net/baiduforum/article/details/6126337
http://www.nosqlnotes.net/archives/105
原文地址:http://blog.163.com/xychenbaihu@yeah/blog/static/132229655201210975312473/
總結(jié)
以上是生活随笔為你收集整理的linux环境内存分配原理的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: GDB attach到进程
- 下一篇: Linux内存管理大图(第三稿)