學習mmap
- 2019 年 10 月 25 日
- 筆記
最近在工作中遇到一個mmap使用相關的問題,造成了一定的困惑,於是花了些時間補了下 mmap
的功課,在這裡分享給大家,錯誤和不足之處大家多指教。
相關背景知識
- 說到mmap的使用,我們首先要了解一下進程的虛擬進程地址空間的概念。Linux上為了作進程隔離,每個進程都運行在自己的單獨的虛擬進程空間,同時物理機上內存有限,每個進程使用虛擬內存地址來隔離又共享物理內存。我們平時在代碼里獲取的地址就是虛擬地址;
- 放一張進程虛擬地址空間草圖,網上也可以很容易找到更精美的 🙂

5e6793157b730fab2ff932dbfcc0a0183.jpg
- 我們在程序中申請內存的操作,實際上只是在進程地址空間相應部分申請了一段虛擬地址,當實際對這段虛擬地址進行讀寫操作時,才會分配真正的物理內存;
- 通常x86 Linux採用段頁式的內存管理模式,這塊不具體展開,簡單來說就是CPU訪問的邏輯地址,然後經過分段機制轉換成線性地址(你可以簡單理解成等價於上面說的虛擬地址),再經過分頁機制轉換成物理地址,第一次訪問的時候由於實現物理地址還沒有分配,會產生缺頁中斷來分配物理地址,用它來填充對應的頁表項;
- 通過
read
系統調用來讀取磁盤上的文件時,文件內容會先被讀到內存的page cache部分,然後再從page cache中拷貝到應用層的讀緩存buffer中;對於打開的文件,內核都會在內存中維護一個inode
結構體(對於同一個文件,即使被open多次,內核也僅維護這一個inode),其有一個成員是struct address_space *i_mapping
, 它用來維護這個文件被讀取的所有部分在內存中的緩存,其使用xarray
(全新封裝了基數樹的操作)來存儲這個物理頁(struct page), 如下圖:

048bffd2fdbba353a06eef23bcbde3e8.jpg
mmap簡介
- 先看原型:
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset)
- 功能:
- 分配一塊新的連續的進程虛擬地址段(對應內核中的結構體就是 vm_area_struct)並返回其起始地址,如果給定了第一個參數,就優先從這個地址開始分配進程虛擬地址;
- 如果提供了fd文件句枘,則映射文件內容到進程虛擬地址;
- mmap的參數較多,其中
prot
和flags
的可選項也比較多,具體大家可以使用man
命令查看;
mmap的幾種典型應用
- 不同進程(可以是非父子進程)間共享映射
- 這種情況需要藉助磁盤文件,實際上是共享這個磁盤文件,將這個磁盤文件映射到各自的進程虛擬地址空間,但是其虛擬地址空間分頁轉換後其頁表項對應的物理內存是相同的;
- 典型用法是需提供一個打開的文件句柄,使用
MAP_SHARED
flag
int fd = open ("[filepath]", O_RDWR)) void *addr = mmap (NULL, [mmaping length], PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0)
- 非子進程間通訊
- 父進程使用
fork
創建子進程,父子進程間可以使用mmap來通讀; - 典型用法是無需提供打開的文件句柄, 使用
MAP_SHARED | MAP_ANONYMOUS
flag,
void *addr = mmap (NULL, BUF_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0);
- 父進程使用
- 進程通過 mmap 來讀寫文件
- 從上面
相關背景知識
一節可知使用read
系統調用讀文件時,數據需經過 磁盤拷貝到page cache, page cache再拷貝到應用層緩存bufffer, 這兩個數據拷貝; - 使用
mmap
時,磁盤數據也是先讀到page cache中,然後會將mmap返回的虛擬地址最終對應的頁項表內容設定為和前面的page chache相同的物理頁, 這樣一來就免去了第二次的數據拷貝; - 用個示意圖來說明一下:
47e79c5a782b3f6c756725eb9e7f0c51.jpg
- 從上面
- 用作glibc中malloc申請內存
- 通常我們都說是通過調用
malloc
來申請堆上內存,但實際上其內部實現使用了brk
和mmap
兩種系統調用,當申請的內存大於128K時,使用mmap
- 典型用法是無需提供打開的文件句柄, 使用
MAP_PRIVATE | MAP_ANONYMOUS
flag
void *addr = mmap (NULL, buffer_size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
- 通常我們都說是通過調用
- mmap的寫時拷貝
- 如果我們在調用mmap時提供一個打開的文件句兩,但使用
MAP_PRIVATE
的flags, 那這時對其的寫操作並不能真正修改對應的磁盤文件,它會作寫時拷貝,退化成匿名映射
- 如果我們在調用mmap時提供一個打開的文件句兩,但使用
mmap作磁盤文件映射時的特別說明
- mmap映射的虛擬地址長度(即mmap的第二個參數)需要對齊到物理頁大小,在32位系統上通常是4K, 這一特點會導致一些有趣的事情發生,我們來看一下:假如一個文件的大小是5000Byte, 剛好比4K大一些,我們用mmap來從文件開始的位置來映射它,mmap的第二個參數給5000, 因為需要頁面對齊,實現映射的虛擬地址長度將是兩個4k,即8192, 8192 – 5000 = 3192的部分用0填充,也是可以被訪問到;
- 如果用mmap映射某個文件時,這個文件大小為0, 不會分配任何的物理內存,也不能作任何的讀寫訪問;當向文件中寫入數據後,通過mmap返回的虛擬地址可以訪問這部分文件內容;
mmap與內存換入換出
- 由前面的介紹我們知道mmap不管是映射磁盤文件,還是作匿名映射,最終都會分配物理內存頁,因此這個物理內存頁在內存緊張時就有備換出的可能,當然mmap提供了
MAP_LOCKED
,可以鎖定內存不被換出,我們不考慮這種情況; - 如果使用mmap映射的是磁盤文件,其存在物理頁的內容會被清空,pte將記錄這種情況,再次需要訪問時,會重新讀取磁盤文件,緩存在page cache中;
- 如果使用mmap作匿名映射,沒有相關聯的磁盤文件(或者使用MAP_PRIVATE方式映射磁盤文件),發生內存換出時,將被交換到swap中,swap實際上也對應着磁盤塊,最終也是寫在磁盤上;
關於mmap我們這次就先介紹到這裡~