linux c语言 glibc 错误 munmap,Linux内存分配小结--malloc、brk、mmap
Linux的虛擬內存管理有幾個關鍵概念:
1、每個進程都有獨立的虛擬地址空間,進程訪問的虛擬地址并不是真正的物理地址;
2、虛擬地址可通過每個進程上的頁表(在每個進程的內核虛擬空間地址)與物理地址進行映射,獲得真正的物理地址;
3、如果虛擬地址對應物理地址不在物理內存中,則產生缺頁中斷,真正分配物理地址,同時更新進程的頁表如果此時物理內存已耗盡,則根據內存替換算法淘汰部分頁面至物理磁盤中。
基于以上認識,進行了如下分析:
一、Linux虛擬地址空間如何分布?
Linux使用虛擬地址空間,大大增加了進程的尋址空間,由低地址到高地址分別為:
1、 只讀段: 該部分空間只能讀,不可寫;(包括:代碼段、rodata段(C常量字符串和#define定義的常量))
2、數據段:保存全局變量和靜態變量的空間;
3、堆:就是平時所說的動態內存,malloc和new大部分都源于此。其中堆頂的位置可通過函數brk和sbrk進行動態調整;
4、文件映射區域: 如動態庫、共享內存等映射物理空間的內存,一般是mmap函數所分配的虛擬地址空間;
5、棧: 用于維護函數調用的上下文空間,一般為8M,可通過ulimit -s查看
6、內核虛擬空間: 用戶代碼不可見的內存區域,由內核管理(頁表就存放在內核虛擬空間)。
下面是32位系統典型的虛擬地址空間分布(來自《深入理解計算機系統》)
32位系統有4G的地址空間
其中0x08048000~0xbfffffff 是用戶空間空間, 0xc00000000~0xffffffff是內核空間,包括內核代碼和數據、與進程相關的數據結構(如頁表、內核棧)等。另外,%esp執行棧頂,往低地址方向變化;brk/sbrk函數控制堆頂_edata往高地址方向變化
64位系統結果怎樣呢?64位系統是否擁有2^64的地址空間嗎?
事實上,64位系統的虛擬地址空間劃分發生了改變:
1、地址空間大小不是2^32,也不是2^64,而一般是2^48。因為并不需要2^64這么大的尋址空間,過大的尋址空間只會導致資源的浪費。64位Linux一般使用48位來表示虛擬地址空間,40位表示物理地址,這可通過/proc/cpuinfo來查看
2、其中,0x0000000000000000~0x00007fffffffffff 表示用戶空間,0xFFFF800000000000~ 0xFFFFFFFFFFFFFFFF 表示內核空間,共提供 256TB(2^48) 的尋址空間。這兩個區間的特點是,第47位與48~63位相同,若這些位為0表示用戶空間,否則表示內核空間。
3、用戶空間由低地址到高地址仍然是只讀段、數據段、堆、文件映射區域和棧;
二、malloc
如何查看進程發生缺頁中斷的次數?
用ps -o majflt, minflt -C program命令查看。
majflt 代表major fault,中文名叫大錯誤, minflt代表minor fault ,中文名叫小錯誤。
這兩個數值表示一個進程自啟動以來所發生的缺頁中斷的次數。
發生缺頁中斷后,執行了那些操作?
當一個進程發生缺頁中斷的時候,進程會陷入內核態,執行以下操作:
1.? 檢查要訪問的虛擬地址是否合法
2. 查找/分配一個物理頁
3. 填充物理頁內容(讀取磁盤,或者直接置0,或者啥也不干)
4. 建立映射關系(虛擬地址到物理地址)
重新執行發生缺頁中斷的那條指令
如果第三步,需要讀取磁盤,那么這次缺頁中斷就是majflt,否則就是minflt。
內存分配的原理
從操作系統角度來看,進程分配內存有兩種方式,分別由兩個系統調用完成:brk和mmap(不考慮共享內存)
1. brk是將數據段(.data)的最高地址指針_edata往高地址上推;
2. mmap是在進程的虛擬地址空間中,(堆和棧中間,稱為文件映射區域的地方)找一塊空閑的虛擬內存。
這兩種分配方式都是虛擬內存,沒有分配物理內存。在第一次訪問已分配的虛擬地址空間的時候,發生缺頁中斷,操作系統負責分配物理內存,然后建立虛擬內存和物理內存之間的映射關系。
在標準的C庫中,提供了malloc/free函數分配釋放內存,這兩個函數底層是由brk,mmap,munmap這些系統調用實現的。
下面以一個例子來說明內存分配的原理:
情況一、malloc小于128k的內存,使用brk分配內存,將_edata往高地址推(只分配虛擬空間,不對應物理內存(因此沒有初始化),第一次讀/寫數據時,引起內核缺頁中斷,內核才分配對應的物理內存,然后虛擬地址空間建立映射關系),如下圖:
1、進程啟動的時候,其(虛擬)內存空間的初始布局如圖1所示。
其中,mmap內存映射文件是在堆和棧的中間(例如libc-2.2.93.so,其他數據文件等),為了簡單起見,省略了內存映射文件。
_edata指針(glibc里面定義)指向數據段的最高地址。
2、進程調用A = malloc(30K)以后,內存空間如圖2:
malloc函數會調用brk系統調用,將_edata指針往高地址推30K,就完成虛擬內存的分配。
你可能會問:只要把_edata+30K就完成內存分配了?
事實是這樣的,_edata+30K只是完成虛擬內存地址的分配,A這塊內存現在還是沒有物理頁與之對應的,等到進程第一次讀寫A這塊內存的時候,發生缺頁中斷,這個時候,內核才分配A這塊內存對應的物理頁。也就是說,如果用malloc分配了A這塊內存,然而從來不訪問它,那么A對應的物理頁是不會被分配的。
3、進程調用B = malloc(40K)以后,內存空間如圖3。
情況二、 malloc大于128的內存,使用mmap分配內存,在堆和棧之間找一塊空閑內存分配(對應獨立內存,而且初始化為0),如下圖:
4、進程調用C= malloc(200K)以后,內存空間如圖4:
默認情況下,malloc函數分配內存,如果請求內存大于128K(可由M_MMAP_THRESHOLD選項調節),那就不是去推_edata指針了,而是利用mmap系統調用,從堆和棧的中間分配一塊虛擬內存。
這樣子做主要是因為:
brk分配的內存需要等到高地址內存釋放以后才能釋放(例如,在B釋放之前,A是不可能釋放的,這就是內存碎片產生的原因,什么時候緊縮看下面),而mmap分配內存可以單獨釋放。
5、進程調用D= malloc(100K)以后,內存空間如圖5;
6、進程調用free(C)以后,C對應的虛擬內存和物理內存一起釋放。
7、 進程調用free(B)以后,如圖7所示:
B對應的虛擬內存和物理內存都沒有釋放,因為只有一個_edata指針,如果往回推,那么D這塊內存怎么辦呢?當然,B這塊內存,是可以重用的,如果這個時候再來一個40K的請求,那么malloc很可能就把B這塊內存返回回去了。
8、進程調用free(D)以后,如圖8所示:
B和D連接起來,變成一塊140K的空閑內存。
9、默認情況下:
當最高地址空間的空閑內存超過128K(可由M_TRIM_THRESHOLD選項調節)時,執行內地緊縮操作(trim)。在上一個步驟free的時候,發現最高地址空閑內存超過128K,于是內存緊縮,變成圖9所示。
三、既然堆內內存brk和sbrk不能直接釋放,為什么不全部使用mmap來分配,munmap直接釋放呢?
既然堆內碎片不能直接釋放,導致疑似內存泄露問題,為什么malloc不全部使用mmap來實現呢(mmap分配的內存可以通過munmap進行free,實現真正的釋放)?而僅僅對大于128K的大塊內存才使用mmap?
其實,進程向OS申請和釋放地址空間的接口sbrk/mmap/munmap 都是系統調用,頻繁調用系統調用都比較消耗系統資源。并且,mmap申請的內存被munmap后,重新申請會產生更多的缺頁中斷。缺頁中斷是內核行為,會導致內核態CPU消耗較大。另外如果使用mmap分配小內存,會導致地址空間的分片更多,內核的管理負擔更大。
同時堆是一個連續空間,并且堆內碎片由于沒有歸還OS,如果可重用碎片,再次訪問該內存很可能不需產生任何系統調用和缺頁中斷,這將大大降低CPU的消耗。因此,glibc的malloc實現中,充分考慮了sbrk和mmap行為上的差異及優缺點,默認分配大塊內存(128K)才使用mmap獲得地址空間,也可通過mallopt(M_MMAP_THRESHOLD, )來修改這個臨界值。
四、 如何查看進程的缺頁中斷信息?
可通過以下命令查看缺頁中斷信息
ps -o majflt,minflt -C
ps -o majflt,minflt -p
其中:: majflt 代表major fault,指大錯誤
minflt代表minor fault,指小錯誤
這兩個數值表示一個進程自啟動以來所發生的缺頁中斷的次數。
其中majflt與minflt的不同是:
majflt表示需要讀寫磁盤,可能是內存對應頁面在磁盤中需要load到物理內存中,也可能是此時物理內存不足,需要淘汰部分物理頁面到磁盤中。
五、 C語言的內存分配方式與malloc
C語言跟內存分配方式
(1)從靜態存儲區分配。內存在程序編譯的時候就已經分配好,這塊內存在程序的整個運行期間內都存在。例如全局變量和靜態變量。
(2)在棧上創建。在執行函數時,函數內局部變量的存儲單元都可以在棧上創建,函數執行結束時,這些存儲單元自動釋放。棧內存分配運算內置于處理器的指令集中,效率很高,但是分配的內存容量有限。
(3)從堆上分配,亦稱為動態內存分配。程序在運行的時候用malloc和new申請任意多少的內存,程序員自己負責何時free或delete釋放內存。動態內存的生存期由我們決定,使用非常靈活,但問題也最多。
C語言跟內存申請相關的函數主要有alloc,calloc,malloc,free,realloc,sbrk等。其中alloc是向棧申請內存,因此無需釋放。malloc分配的內存是位于堆中的,并且沒有初始化內存的內容,調用函數memset來初始化這部分的內存空間。calloc則將初始化這部分內存,設置為0。而realloc則對malloc申請的內存進行大小的調整。申請的內存最終需要通過函數free來釋放。而sbrk則是增加數據段的大小;
malloc/calloc/free 基本上都是C函數庫實現的,跟OS無關。C函數庫內部通過一定的結構來保存當前有多少可用內存。如果程序malloc的大小超出了庫里所留存的空間,那么將首先調用brk系統調用來增加可用空間,然后再分配空間.free時,釋放的內存并不立即返回給os,而是保留在內部結構中。打嗝比方:brk類似于批發,一次性的向os申請大的內存,而malloc等函數則類似于零售,滿足程序運行時的要求,這套機制類似于緩沖。
使用這套機制的原因:系統調用不能支持任意大小的內存分配(有的系統調用只支持固定大小以及其倍數的內存申請,這樣的話,對于小內存的分配會造成浪費;系統調用申請內存代價昂貴,涉及到用戶態和核心態的轉換)
函數malloc()和calloc() 都可以用來分配動態內存空間,但兩者稍有區別。
在Linux系統上,程序被載入內存時,內核為用戶進程地址建立了代碼段、數據段和堆棧段,在數據段和堆棧段之間的空閑區域用于動態內存分配。
內核數據結構mm_struct中的成員變量start_code 和end_code是進程代碼段的起始和終止地址,start_data 和end_data是進程數據段的起始和終止地址,start_stack是進程堆棧段起始地址,start_brk是進程動態內存分配起始地址,還有一個brk,就是動態內存分配當前的終止地址。
C語言的動態內存分配的基本函數是malloc,在Linux上的基本實現是通過內核brk系統調用。brk()是一個非常簡單的系統調用,只是簡單改變mm_struct結構的成員變量brk的值
mmap系統調用實現了更有用的動態內存分配功能,可以將一個磁盤文件的全部或部分內容映射到用戶空間中,進程讀寫文件的操作變成了讀寫內存的操作。在linux/mm/mmap.c文件的do_mmap_pgoff()函數,是mmap系統調用實現的核心。do_mmap_pgoff()的代碼,只是新建了一個vm_area_struct結構,并把file結構的參數賦值給其他成員變量m_file,并沒有把文件內容實際裝入內存。
Linux內存管理的基本思想之一,是只有在真正訪問一個地址的時候才建立這個地址的物理映射。
總結
以上是生活随笔為你收集整理的linux c语言 glibc 错误 munmap,Linux内存分配小结--malloc、brk、mmap的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: linux卸载minicom,ubunt
- 下一篇: linux x window syste