這篇文章講的很詳細(xì)很好。
Android-內(nèi)存映射mmap_mcryeasy的博客-CSDN博客
一、引言
說到內(nèi)存映射函數(shù)mmap大家可能覺得陌生,其實Android中的Binder機(jī)制就是mmap來實現(xiàn)的。不僅如此,微信的MMKV?key-value組件、美團(tuán)的?Logan的日志組件 都是基于mmap來實現(xiàn)的。mmap強(qiáng)大的地方在于通過內(nèi)存映射直接對文件進(jìn)行讀寫,減少了對數(shù)據(jù)的拷貝次數(shù),大大的提高了IO讀寫的效率。
由于Android是基于Linux系統(tǒng),因此在介紹mmap之前,不得不先介紹下Linux的文件系統(tǒng)。
類似于網(wǎng)絡(luò)的分層結(jié)構(gòu),下圖顯示了 Linux 系統(tǒng)中對于磁盤的一次讀請求在核心空間中所要經(jīng)歷的層次模型:
虛擬文件系統(tǒng)層:作用是屏蔽下層具體文件系統(tǒng)操作的差異,為上層的操作提供一個統(tǒng)一的接口。
文件系統(tǒng)層?:具體的文件系統(tǒng)層,一個文件系統(tǒng)一般使用塊設(shè)備上一個獨立的邏輯分區(qū)。
Page Cache (層頁高速緩存層):引入 Cache 層的目的是為了提高 Linux 操作系統(tǒng)對磁盤訪問的性能。
通用塊層:作用是接收上層發(fā)出的磁盤請求,并最終發(fā)出 I/O 請求。
I/O 調(diào)度層:作用是管理塊設(shè)備的請求隊列。
塊設(shè)備驅(qū)動層?:利用驅(qū)動程序,驅(qū)動具體的物理塊設(shè)備。
物理塊設(shè)備層:具體的物理磁盤塊。
其他層暫不細(xì)講,主要說說Page Cache層 (頁高速緩存)這一層。引入 Cache 層的目的是為了提高 Linux 操作系統(tǒng)對磁盤訪問的性能。Cache 層在內(nèi)存中緩存了磁盤上的部分?jǐn)?shù)據(jù)。當(dāng)數(shù)據(jù)的請求到達(dá)時,如果在 Cache 中存在該數(shù)據(jù)且是的,則直接將數(shù)據(jù)傳遞給用戶程序,免除了對底層磁盤的操作,提高了性能。
Page Cache層實際上是內(nèi)核中的物理內(nèi)存,在磁盤和用戶空間之間多了一層緩存層,由內(nèi)核負(fù)責(zé)管理控制。由于物理內(nèi)存的速度遠(yuǎn)遠(yuǎn)快于磁盤的速度,有了這一層的存在,數(shù)據(jù)放入Page Cache中可以更快的進(jìn)行訪問。而且數(shù)據(jù)一旦被訪問后,短時間內(nèi)有極大會再一次被訪問,短時間內(nèi)集中訪問同一數(shù)據(jù)的原理就叫做局部性原理。因此經(jīng)常需要被訪問的數(shù)據(jù),如果將其放入緩存中,那就有可能再次被頁高速緩存命中,這也是Page Cache所帶來的性能提升!
在Linux上我們可以通過/proc/meminfo文件查看Cache的大小 :
Cache是可以被回收的,尤其當(dāng)系統(tǒng)內(nèi)存空間不足的時候,會把Cache中臟數(shù)據(jù)寫入到磁盤中。所以在統(tǒng)計Linux空閑內(nèi)存大小的時候通常是?MemFree+?Cached的總和!
Android系統(tǒng)中通過ActivityManager#getMemoryInfo#availMem查看可用內(nèi)存的大小的時候也是這么計算的,在/frameworks/base/core/jni/android_util_Process.cpp中可以看到其計算方式:
由于有了Cache Page的存在,read/write系統(tǒng)調(diào)用會有以下的操作,我們那Read來進(jìn)行說明:
用戶進(jìn)程向內(nèi)核發(fā)起讀取文件的請求,這涉及到用戶態(tài)到內(nèi)核態(tài)的轉(zhuǎn)換。
內(nèi)核讀取磁盤文件中的對應(yīng)數(shù)據(jù),并把數(shù)據(jù)讀取到Cache Page中。
由于Page Cache處在內(nèi)核空間,不能被用戶進(jìn)程直接尋址 ,所以需要從Page Cache中拷貝數(shù)據(jù)到用戶進(jìn)程的堆空間中。
注意,這里涉及到了兩次拷貝:第一次拷貝磁盤到Page Cache,第二次拷貝Page Cache到用戶內(nèi)存。最后物理內(nèi)存的內(nèi)容是這樣的,同一個文件內(nèi)容存在了兩份拷貝,一份是頁緩存,一份是用戶進(jìn)程的內(nèi)存空間。
整個流程如下圖所示:
可見我們平時所使用的read/write操作作對文件操作的過程中會涉及到兩次拷貝的操作!這是因為有了Cache Page的存在為了提高讀寫效率和保護(hù)磁盤。而我們本章要講的mmap操作,它讀寫效率更高,而且只涉及一次拷貝操作,IO讀寫效率遠(yuǎn)遠(yuǎn)高于read/write!
mmap是一種內(nèi)存映射文件的方法,它將一個文件映射到進(jìn)程的地址空間中,實現(xiàn)文件磁盤地址和進(jìn)程虛擬地址空間中一段虛擬地址的一一對映關(guān)系。實現(xiàn)這樣的映射關(guān)系后,進(jìn)程就可以采用指針的方式讀寫操作這一段內(nèi)存,而系統(tǒng)會自動回寫臟頁面到對應(yīng)的文件磁盤上,即完成了對文件的操作而不必再調(diào)用read,write等系統(tǒng)調(diào)用函數(shù)。相反,內(nèi)核空間對這段區(qū)域的修改也直接反映用戶空間,從而可以實現(xiàn)不同進(jìn)程間的文件共享。如下圖所示:
mmap內(nèi)存映射具體流程如下:
1、用戶進(jìn)程調(diào)用內(nèi)存映射函數(shù)庫mmap,當(dāng)前進(jìn)程在虛擬地址空間中,尋找一段空閑的滿足要求的虛擬地址。
2、此時內(nèi)核收到相關(guān)請求后會調(diào)用內(nèi)核的mmap函數(shù),注意,不同于用戶空間庫函數(shù)。內(nèi)核mmap函數(shù)通過虛擬文件系統(tǒng)定位到文件磁盤物理地址,既實現(xiàn)了文件地址和虛擬地址區(qū)域的映射關(guān)系。 此時,這片虛擬地址并沒有任何數(shù)據(jù)關(guān)聯(lián)到主存中。
注意,前兩個階段僅在于創(chuàng)建虛擬區(qū)間并完成地址映射,但是并沒有將任何文件數(shù)據(jù)的拷貝至主存。真正的文件讀取是當(dāng)進(jìn)程發(fā)起讀或?qū)懖僮鲿r。
3、進(jìn)程的讀或?qū)懖僮髟L問虛擬地址空間這一段映射地址,現(xiàn)這一段地址并不在物理頁面上。因為目前只建立了地址映射,真正的硬盤數(shù)據(jù)還沒有拷貝到內(nèi)存中,因此引發(fā)缺頁中斷。
4、由于引發(fā)了缺頁中斷,內(nèi)核則調(diào)用nopage函數(shù)把所缺的頁從磁盤裝入到主存中。
5、之后用戶進(jìn)程即可對這片主存進(jìn)行讀或者寫的操作,如果寫操作改變了其內(nèi)容,一定時間后系統(tǒng)會自動回寫臟頁面到對應(yīng)磁盤地址,也即完成了寫入到文件的過程。
注意:這里拷貝磁盤內(nèi)容到主存,這里的主存是指處于內(nèi)核空間的Page Cache,而不是用戶空間的內(nèi)存。用戶地址要訪問內(nèi)核空間中的數(shù)據(jù),需使用MMU把虛擬地址映射到內(nèi)核的內(nèi)存地址中,即可對數(shù)據(jù)進(jìn)行操作。整個mmap工作流程大體如下:
這里我們可以看出mmap系統(tǒng)調(diào)用與read/write調(diào)用的區(qū)別在于:
mmap只需要一次系統(tǒng)調(diào)用(一次拷貝),后續(xù)操作不需要系統(tǒng)調(diào)用。
訪問的數(shù)據(jù)不需要在page cache和用戶緩沖區(qū)之間拷貝。 訪問的數(shù)據(jù)不需要在page cache和用戶緩沖區(qū)之間拷貝。
從上所述,當(dāng)頻繁對一個文件進(jìn)行讀取操作時,mmap會比read/write更高效。
mmap的函數(shù)位于 <sys/mman.h> 頭文件中,它的函數(shù)原型如下:

mmap?函數(shù)用于將文件映射到內(nèi)存 。
munmap?函數(shù)用于取消映射,進(jìn)程在映射空間的對共享內(nèi)容的改變并不直接寫回到磁盤文件中,往往在調(diào)用 munmap() 后才執(zhí)行該操作。
msync?函數(shù)用于實現(xiàn)磁盤文件內(nèi)容與共享內(nèi)存區(qū)中的內(nèi)容一致,即同步操作,除了調(diào)用munmap取消映射,我們也可以調(diào)用msync()實現(xiàn)磁盤上文件內(nèi)容與共享內(nèi)存區(qū)的內(nèi)容一致。
mmap的函數(shù)的使用網(wǎng)上有很多教程,這里每個參數(shù)的作用就不再細(xì)講,主要講講mmap使用過程中的幾個細(xì)節(jié)點:
細(xì)節(jié)點一:?mmap映射區(qū)域大小必須是物理頁大小(page_size)的整倍數(shù)(在Linux中內(nèi)存頁通常是4k)。原因是,內(nèi)存的最小粒度是頁,而進(jìn)程虛擬地址空間和內(nèi)存的映射也是以頁為單位。為了匹配內(nèi)存的操作,mmap從磁盤到虛擬地址空間的映射也必須是頁。
例如,有一個文件的大小是5K,mmap函數(shù)從文件的起始位置映射5K到虛擬內(nèi)存中,由于內(nèi)存物理頁是4K,雖然映射的文件只有5K,但是實際上映射到內(nèi)存區(qū)域的內(nèi)存是8K,以便滿足物理頁大小的整數(shù)倍。映射后對5~8K的內(nèi)存區(qū)域用零填充,對這部分的操作不會報錯也不會寫入到原文件中。
細(xì)節(jié)點二?:?映射建立之后,即使文件關(guān)閉,映射依然存在。因為映射的是磁盤的地址,不是文件本身,和文件句柄無關(guān)。同時可用于進(jìn)程間通信的有效地址空間不完全受限于被映射文件的大小,因為是按頁映射。
六、mmap的應(yīng)用場景
mmap在Linux、Android系統(tǒng)上有非常多的應(yīng)用場景。
1、Linux進(jìn)程的創(chuàng)建
Linux執(zhí)行一個程序,這個程序在磁盤上,為了執(zhí)行這個程序,需要把程序加載到內(nèi)存中,這時也是采用的是mmap。你可以從/proc/pid/maps看到每個進(jìn)程的mmap狀態(tài)。
2、內(nèi)存分配
我們使用c庫的malloc申請內(nèi)存,malloc的分配內(nèi)存有兩個系統(tǒng)調(diào)用,一個brk,另一個就是mmap。其實mmap不僅可以映射文件,也可以映射內(nèi)存,當(dāng)mmap使用的flag是MAP_ANONYMOUS,稱為建立匿名映射,此時會忽略參數(shù)fd,不涉及文件,而且映射區(qū)域無法和其他進(jìn)程共享。匿名映射存儲的數(shù)據(jù)就是在物理內(nèi)存上,不屬于任何文件。malloc分配內(nèi)存底層就是用mmap的匿名映射來操作的。
3、Binder進(jìn)程間通信
了解進(jìn)程間通信的人都知道Android使用的是Binder進(jìn)行進(jìn)程間通信,它的效率高于Linux其他傳統(tǒng)的進(jìn)程間通信,因為它只要一次拷貝,而之所以只需要進(jìn)行一次拷貝的原因就在于使用了mmap!
一次完整的 Binder IPC 通信過程通常是這樣:
Server端在啟動之后,調(diào)用對/dev/binder設(shè)備調(diào)用mmap。
內(nèi)核中的binder_mmap函數(shù)進(jìn)行對應(yīng)的處理:申請一塊物理內(nèi)存,然后在Server端的用戶空間和內(nèi)核空間同時進(jìn)行映射。內(nèi)核中的binder_mmap函數(shù)進(jìn)行對應(yīng)的處理:申請一塊物理內(nèi)存,然后在Server端的用戶空間和內(nèi)核空間同時進(jìn)行映射
Client發(fā)送請求,這個請求將先到驅(qū)動中,同時需要將數(shù)據(jù)從Client進(jìn)程的用戶空間拷貝(Client發(fā)送請求,這個請求將先到驅(qū)動中,同時需要將數(shù)據(jù)從Client進(jìn)程的用戶空間拷貝(copy_from_user)到內(nèi)核空間。
驅(qū)動通過請求通知Server端有人發(fā)出請求,Server進(jìn)行處理。由于內(nèi)核空間和Server端進(jìn)程的用戶空間存在內(nèi)存映射,因此Server進(jìn)程的代碼可以直接訪問。這樣便完成了一次進(jìn)程間的通信。
4、IO讀寫效率
mmap最主要的功能就是提高了IO讀寫的效率,微信的MMKV?key-value組件、美團(tuán)的?Logan的日志組件 都是基于mmap來實現(xiàn)的。在微信的?MMKV/Android/MMKV/mmkv/src/main/cpp/MMKV.cpp?和美團(tuán)的?Meituan-Dianping/Logan/blob/master/Logan/Clogan/mmap_util.c?的這兩個文件中你都可以看到對mmap函數(shù)的使用,有興趣的小伙伴可以自行查閱。