這篇文章講的很詳細(xì)很好产镐。
Android-內(nèi)存映射mmap_mcryeasy的博客-CSDN博客
一虎谢、引言
說到內(nèi)存映射函數(shù)mmap大家可能覺得陌生刽沾,其實Android中的Binder機制就是mmap來實現(xiàn)的魄咕。不僅如此,微信的MMKV?key-value組件振劳、美團的?Logan的日志組件 都是基于mmap來實現(xiàn)的摇锋。mmap強大的地方在于通過內(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組件、美團的?Logan的日志組件 都是基于mmap來實現(xiàn)的撰茎。在微信的?MMKV/Android/MMKV/mmkv/src/main/cpp/MMKV.cpp?和美團的?Meituan-Dianping/Logan/blob/master/Logan/Clogan/mmap_util.c?的這兩個文件中你都可以看到對mmap函數(shù)的使用嵌牺,有興趣的小伙伴可以自行查閱。