最近在工作中遇到一個mmap使用相關(guān)的問題薄扁,造成了一定的困惑胁黑,于是花了些時間補了下 mmap
的功課涩盾,在這里分享給大家,錯誤和不足之處大家多指教捕捂。
相關(guān)背景知識
說到mmap的使用瑟枫,我們首先要了解一下進程的虛擬進程地址空間的概念。Linux上為了作進程隔離指攒,每個進程都運行在自己的單獨的虛擬進程空間慷妙,同時物理機上內(nèi)存有限,每個進程使用虛擬內(nèi)存地址來隔離又共享物理內(nèi)存允悦。我們平時在代碼里獲取的地址就是虛擬地址;
-
放一張進程虛擬地址空間草圖膝擂,網(wǎng)上也可以很容易找到更精美的 :)
5e6793157b730fab2ff932dbfcc0a0183.jpg 我們在程序中申請內(nèi)存的操作,實際上只是在進程地址空間相應部分申請了一段虛擬地址隙弛,當實際對這段虛擬地址進行讀寫操作時架馋,才會分配真正的物理內(nèi)存;
通常x86 Linux采用段頁式的內(nèi)存管理模式,這塊不具體展開全闷,簡單來說就是CPU訪問的邏輯地址叉寂,然后經(jīng)過分段機制轉(zhuǎn)換成線性地址(你可以簡單理解成等價于上面說的虛擬地址),再經(jīng)過分頁機制轉(zhuǎn)換成物理地址室埋,第一次訪問的時候由于實現(xiàn)物理地址還沒有分配办绝,會產(chǎn)生缺頁中斷來分配物理地址,用它來填充對應的頁表項;
-
通過
read
系統(tǒng)調(diào)用來讀取磁盤上的文件時姚淆,文件內(nèi)容會先被讀到內(nèi)存的page cache部分孕蝉,然后再從page cache中拷貝到應用層的讀緩存buffer中;對于打開的文件,內(nèi)核都會在內(nèi)存中維護一個inode
結(jié)構(gòu)體(對于同一個文件腌逢,即使被open多次降淮,內(nèi)核也僅維護這一個inode),其有一個成員是struct address_space *i_mapping
, 它用來維護這個文件被讀取的所有部分在內(nèi)存中的緩存搏讶,其使用xarray
(全新封裝了基數(shù)樹的操作)來存儲這個物理頁(struct page), 如下圖:
048bffd2fdbba353a06eef23bcbde3e8.jpg
mmap簡介
- 先看原型:
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset)
- 功能:
- 分配一塊新的連續(xù)的進程虛擬地址段(對應內(nèi)核中的結(jié)構(gòu)體就是 vm_area_struct)并返回其起始地址佳鳖,如果給定了第一個參數(shù),就優(yōu)先從這個地址開始分配進程虛擬地址;
- 如果提供了fd文件句枘媒惕,則映射文件內(nèi)容到進程虛擬地址;
- mmap的參數(shù)較多系吩,其中
prot
和flags
的可選項也比較多,具體大家可以使用man
命令查看;
mmap的幾種典型應用
-
不同進程(可以是非父子進程)間共享映射
- 這種情況需要借助磁盤文件妒蔚,實際上是共享這個磁盤文件穿挨,將這個磁盤文件映射到各自的進程虛擬地址空間月弛,但是其虛擬地址空間分頁轉(zhuǎn)換后其頁表項對應的物理內(nèi)存是相同的;
- 典型用法是需提供一個打開的文件句柄,使用
MAP_SHARED
flag
int fd = open ("[filepath]", O_RDWR)) void *addr = mmap (NULL, [mmaping length], PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0)
-
非子進程間通訊
- 父進程使用
fork
創(chuàng)建子進程科盛,父子進程間可以使用mmap來通讀; - 典型用法是無需提供打開的文件句柄, 使用
MAP_SHARED | MAP_ANONYMOUS
flag,
void *addr = mmap (NULL, BUF_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0);
- 父進程使用
-
進程通過 mmap 來讀寫文件
- 從上面
相關(guān)背景知識
一節(jié)可知使用read
系統(tǒng)調(diào)用讀文件時帽衙,數(shù)據(jù)需經(jīng)過 磁盤拷貝到page cache, page cache再拷貝到應用層緩存bufffer, 這兩個數(shù)據(jù)拷貝; - 使用
mmap
時,磁盤數(shù)據(jù)也是先讀到page cache中贞绵,然后會將mmap返回的虛擬地址最終對應的頁項表內(nèi)容設(shè)定為和前面的page chache相同的物理頁厉萝, 這樣一來就免去了第二次的數(shù)據(jù)拷貝; -
用個示意圖來說明一下:
47e79c5a782b3f6c756725eb9e7f0c51.jpg
- 從上面
-
用作glibc中malloc申請內(nèi)存
- 通常我們都說是通過調(diào)用
malloc
來申請堆上內(nèi)存,但實際上其內(nèi)部實現(xiàn)使用了brk
和mmap
兩種系統(tǒng)調(diào)用榨崩,當申請的內(nèi)存大于128K時谴垫,使用mmap
- 典型用法是無需提供打開的文件句柄, 使用
MAP_PRIVATE | MAP_ANONYMOUS
flag
void *addr = mmap (NULL, buffer_size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
- 通常我們都說是通過調(diào)用
-
mmap的寫時拷貝
- 如果我們在調(diào)用mmap時提供一個打開的文件句兩,但使用
MAP_PRIVATE
的flags, 那這時對其的寫操作并不能真正修改對應的磁盤文件母蛛,它會作寫時拷貝弹渔,退化成匿名映射
- 如果我們在調(diào)用mmap時提供一個打開的文件句兩,但使用
mmap作磁盤文件映射時的特別說明
- mmap映射的虛擬地址長度(即mmap的第二個參數(shù))需要對齊到物理頁大小,在32位系統(tǒng)上通常是4K, 這一特點會導致一些有趣的事情發(fā)生,我們來看一下:假如一個文件的大小是5000Byte, 剛好比4K大一些溯祸,我們用mmap來從文件開始的位置來映射它,mmap的第二個參數(shù)給5000, 因為需要頁面對齊舞肆,實現(xiàn)映射的虛擬地址長度將是兩個4k,即8192, 8192 - 5000 = 3192的部分用0填充焦辅,也是可以被訪問到;
- 如果用mmap映射某個文件時,這個文件大小為0, 不會分配任何的物理內(nèi)存椿胯,也不能作任何的讀寫訪問筷登;當向文件中寫入數(shù)據(jù)后,通過mmap返回的虛擬地址可以訪問這部分文件內(nèi)容;
mmap與內(nèi)存換入換出
- 由前面的介紹我們知道m(xù)map不管是映射磁盤文件哩盲,還是作匿名映射前方,最終都會分配物理內(nèi)存頁,因此這個物理內(nèi)存頁在內(nèi)存緊張時就有備換出的可能廉油,當然mmap提供了
MAP_LOCKED
惠险,可以鎖定內(nèi)存不被換出,我們不考慮這種情況; - 如果使用mmap映射的是磁盤文件抒线,其存在物理頁的內(nèi)容會被清空班巩,pte將記錄這種情況,再次需要訪問時嘶炭,會重新讀取磁盤文件抱慌,緩存在page cache中;
- 如果使用mmap作匿名映射,沒有相關(guān)聯(lián)的磁盤文件(或者使用MAP_PRIVATE方式映射磁盤文件)眨猎,發(fā)生內(nèi)存換出時抑进,將被交換到swap中,swap實際上也對應著磁盤塊睡陪,最終也是寫在磁盤上;
關(guān)于mmap我們這次就先介紹到這里~