malloc內存分配原理 [linux]--malloc、brk、mmap


本文轉自:https://blog.csdn.net/gfgdsg/article/details/42709943

----借花獻佛,以留后用----


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 是用戶空間,0xc0000000~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 來查看
address sizes   : 40 bits physical, 48 bits virtual
2、其中,0x0000000000000000~0x00007fffffffffff 表示用戶空間, 0xFFFF800000000000~ 0xFFFFFFFFFFFFFFFF 表示內核空間,共提供 256TB(2^48) 的尋址空間。
這兩個區間的特點是,第 47 位與 48~63 位相同,若這些位為 0 表示用戶空間,否則表示內核空間。
3、用戶空間由低地址到高地址仍然是只讀段、數據段、堆、文件映射區域和棧;

二、malloc和free是如何分配和釋放內存?

如何查看進程發生缺頁中斷的次數?

         用ps -o majflt,minflt -C program命令查看。

          majflt代表major fault,中文名叫大錯誤,minflt代表minor fault,中文名叫小錯誤。

          這兩個數值表示一個進程自啟動以來所發生的缺頁中斷的次數。

發成缺頁中斷后,執行了那些操作?

當一個進程發生缺頁中斷的時候,進程會陷入內核態,執行以下操作:
1、檢查要訪問的虛擬地址是否合法
2、查找/分配一個物理頁
3、填充物理頁內容(讀取磁盤,或者直接置0,或者啥也不干)
4、建立映射關系(虛擬地址到物理地址)
重新執行發生缺頁中斷的那條指令
如果第3步,需要讀取磁盤,那么這次缺頁中斷就是majflt,否則就是minflt。

malloc內存分配的原理
從操作系統角度來看,進程分配內存有兩種方式,分別由兩個系統調用完成: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大於128k的內存

             使用mmap分配內存,在堆和棧之間找一塊空閑內存分配(對應獨立內存,而且初始化為0),如下圖:
    

4、進程調用C=malloc(200K)以后,內存空間如圖4:
      默認情況下,malloc函數分配內存,如果請求內存大於128K(可由M_MMAP_THRESHOLD選項調節),那就不是去推_edata指針了,而是利用mmap系統調用,從堆和棧的中間分配一塊虛擬內存。
      這樣子做主要是因為::
      brk分配的內存需要等到高地址內存釋放以后才能釋放(例如,在B釋放之前,A是不可能釋放的,這就是內存碎片產生的原因,什么時候 緊縮 看下面),而mmap分配的內存可以單獨釋放。
      當然,還有其它的好處,也有壞處,再具體下去,有興趣的同學可以去看glibc里面malloc的代碼了。
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 后,重新申請會產生更多的缺頁中斷。例如使用 mmap 分配 1M 空間,第一次調用產生了大量缺頁中斷 (1M/4K 次 ) ,當munmap 后再次分配 1M 空間,會再次產生大量缺頁中斷。缺頁中斷是內核行為,會導致內核態CPU消耗較大。另外,如果使用 mmap 分配小內存,會導致地址空間的分片更多,內核的管理負擔更大。
        同時堆是一個連續空間,並且堆內碎片由於沒有歸還 OS ,如果可重用碎片,再次訪問該內存很可能不需產生任何系統調用和缺頁中斷,這將大大降低 CPU 的消耗。 因此, glibc 的 malloc 實現中,充分考慮了 sbrk 和 mmap 行為上的差異及優缺點,默認分配大塊內存 (128k) 才使用 mmap 獲得地址空間,也可通過 mallopt(M_MMAP_THRESHOLD, <SIZE>) 來修改這個臨界值。

四、如何查看進程的缺頁中斷信息?
  可通過以下命令查看缺頁中斷信息 :
    ps -o majflt,minflt -C <program_name>
    ps -o majflt,minflt -p <pid>


  其中:: majflt 代表 major fault ,指大錯誤;
                 minflt 代表 minor fault ,指小錯誤。
  這兩個數值表示一個進程自啟動以來所發生的缺頁中斷的次數。 majflt 與 minflt 的不同是:: majflt 表示需要讀寫磁盤,可能是內存對應頁面在磁盤中需要load 到物理內存中,也可能是此時物理內存不足,需要淘汰部分物理頁面至磁盤中。


五、C語言的內存分配方式與malloc

 
C語言跟內存分配方式
(1) 從靜態存儲區域分配。內存在程序編譯的時候就已經分配好,這塊內存在程序的整個運行期間都存在。例如全局變量,static變量。
(2) 在棧上創建。在執行函數時,函數內局部變量的存儲單元都可以在棧上創建,函數執行結束時這些存儲單元自動被釋放。棧內存分配運
算內置於處理器的指令集中,效率很高,但是分配的內存容量有限。
(3)從堆上分配,亦稱動態內存分配。程序在運行的時候用malloc或new申請任意多少的內存,程序員自己負責在何時用free或delete釋放內存。動態內存的生存期由我們決定,使用非常靈活,但問題也最多
     

      C語言跟內存申請相關的函數主要有 alloc,calloc,malloc,free,realloc,sbrk等.其中alloc是向棧申請內存,因此無需釋放. malloc分配的內存是位於堆中的,並且沒有初始化內存的內容,因此基本上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內存管理的基本思想之一,是只有在真正訪問一個地址的時候才建立這個地址的物理映射。


————————————————
版權聲明:本文為CSDN博主「gfgdsg」的原創文章,遵循CC 4.0 BY-SA版權協議,轉載請附上原文出處鏈接及本聲明。
原文鏈接:https://blog.csdn.net/gfgdsg/java/article/details/42709943

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM