malloc 和mmap


從操作系統角度來看,進程分配內存有兩種方式,分別由兩個系統調用完成:brk 和 mmap(不考慮共享內存)。

  1. brk 的實現方式是將 Data Segment 的最高地址指針 _edata 往高地址推(分配的內存小於 128k )。
  2. mmap 的實現方式是在 Memory Mapping Segment 找一塊空閑的虛擬內存(分配的內存大於 128k )。

(Data segment 和 Memory Mapping Segment 的相關內容查看這里。)

這兩種方式分配的都是虛擬內存,沒有分配物理內存。在第一次訪問已分配的虛擬地址空間的時候,發生缺頁中斷,操作系統負責分配物理內存,然后建立虛擬內存和物理內存之間的映射關系。

在標准 C 庫中,提供了 malloc / free 函數分配釋放內存,這兩個函數底層是由 brk,mmap,munmap 這些系統調用實現的。

example 1

1、進程調用 A = malloc ( 30k ) 以后,內存空間如下圖所示。malloc 函數會調用 brk 系統調用,將 _edata 指針往高地址推 30K,就完成虛擬內存分配。

你可能會問:只要把_edata + 30K 就完成內存分配了?

事實是這樣的,_edata + 30K 只是完成虛擬地址的分配,A 這塊內存現在還是沒有物理頁與之對應的,等到進程第一次讀寫 A 這塊內存的時候,發生缺頁中斷,這個時候,內核才分配 A 這塊內存對應的物理頁。也就是說,如果用 malloc 分配了 A 這塊內容,然后從來不訪問它,那么,A 對應的物理頁是不會被分配的。 

 

 

 

example2

進程調用 B = malloc(40K) 以后,內存空間如下圖所示。

 

 

 

example 3

3、當 malloc 分配大於 128k 的內存時,使用 mmap 分配內存。在堆和棧之間找一塊空閑內存分配(對應獨立內存,而且初始化為 0 )。

這么做的原因是 brk 分配的內存需要等到高地址內存釋放以后才能釋放(例如,在 B 釋放之前,A 是不可能釋放的,這就是內存碎片產生的原因,什么時候收縮看下面),而 mmap 分配的內存可以單獨釋放。,如下圖所示,這里分配 200k 。

 

 

example 4

 4、進程調用 D = malloc(100k) 以后,內存空間如下圖所示。

 

 

 

example 5

 5、進程調用 free(C) 以后,C 對應的虛擬內存和物理內存一起釋放

 

 

example 6

6、進程調用 free(B) 以后,如下圖所示,B 對應的虛擬內存和物理內存都沒有釋放,因為只有一個 _edata 指針,如果往回推,那么 D 這塊內存怎么辦呢?當然,B 這塊內存是可以重用的,如果這個時候再來一個 40K 的請求,那么 malloc 很可能就將 B 這塊內存返回的。 

 

 

example 7

 

7、進程調用 free(D) 以后,如下圖所示,B 和 D 連接起來變成一塊 140K 的空閑內存。當最高地址空間的空閑內存超過128K(可由 M_TRIM_THRESHOLD 選項調節)時,執行內存緊縮操作(trim)。在上一個步驟 free 的時候,發現最高地址空閑內存超過 128 K,於是內存緊縮,如下圖所示。

 

 

 

2 mmap
了解完 虛擬內存 ,再回過頭來講一下 mmap ,也就是內存映射 。內存映射是將一個虛擬內存區域與一個磁盤上的對象關聯起來,以初始化這個虛擬內存區域的內容的過程。

2.1 基礎概念
先講下內存映射里的一些概念。

映射對象類型

虛擬內存區域可以映射以下兩種類型的對象:

普通文件:即磁盤文件中的一塊 連續 的區域。
匿名文件:一個由內核創建的全為 二進制零 的文件。當CPU首次引用此區域時,將以二進制零填充到頁表中。
共享對象

在上一節 虛擬內存 可得知,系統為每個進程提供了單獨的頁表,從而也實現了進程間數據訪問權限的管理以及數據的保護。但同時,通過內存映射的機制,將對象作為 共享對象 映射到兩個進程的虛擬內存亦可實現數據的共享。

 

 

 

2.2 使用方式
然后先講下如果我們應該如何通過內存映射的方式來訪問文件。 mmap() 的函數定義如下:

 

void * mmap(void *start, size_t length, int prot, int flags, int fd, off_t offset)

 



其中參數的含義分別是:

start: 期望的進程虛擬內存起始位置,填 NULL 時由內核來決定起始位置
length: 需要映射的對象字節大小
fd: 文件句柄
offset: 距離文件開始處的偏移量
prot: 映射對象的訪問權限,用於可指定是否可讀寫、執行。
flags: 映射對象的類型,例如指定是映射普通文件還是請求二進制零、映射共享對象還是私有的寫時復制對象等。
前4項地含義可通過下圖更直觀地了解:

 

 

 

而在 iOS 開發中,當我們需要的數據類型是 NSData 時,可以更簡便地通過調用以下方法

 
 

2.3 讀取過程
當我們通過 mmap 讀取文件時,將經歷以下步驟:

在當前用戶虛擬內存空間中分配一片 指定映射大小 的虛擬內存區域。
將磁盤中的文件映射到這片內存區域,等待后續 按需 進行頁面調度。
當CPU真正訪問數據時,觸發 缺頁異常 將所需的數據頁從磁盤拷貝到物理內存,並將物理頁地址記錄到頁表。
進程通過頁表得到的物理頁地址訪問文件數據。

 

 


而作為對比,當通過 標准IO 讀取一個文件時,步驟為:

將 完整 的文件從磁盤拷貝到物理內存(內核空間)。
將完整文件數據從 內核空間 拷貝到 用戶空間 以供進程訪問。

 

 


2.4 優劣
通過上面 mmap 與 標准IO 的對比,不難發現調用mmap具有以下的優勢:

物理內存占用延后:數據直到真正被使用時才會發生拷貝。
物理內存占用減少:對於同一份文件無需在物理內存中存放兩份,且文件區被划分成片,缺頁異常時只將所需的頁拷貝到物理內存。
方便實現跨進程數據交互、共享:當映射到虛擬內存的對象被設置為共享對象,則不同進程對映射對象的寫操作相互可見。
然而也能發現 mmap 存在以下 劣勢 :

無法映射變長文件:調用mmap()時需指定要映射的文件位置和需要映射的大小范圍。
如果需要映射的文件過大,會導致過度占用虛擬內存:在調用mmap()后,虛擬內存空間就創建了,此時雖然不會占用物理內存,但依然會占用虛擬內存。此時可考慮只映射文件中自己需要的部分。
由此,當我們需要訪問一個比較大的文件,尤其是當我們只需要訪問其中的一小部分數據的時候,我們可以嘗試通過 mmap 的方式來進行訪問,減少由於該文件過大而對物理內存的過度占用。
 

 

 


免責聲明!

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



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