程序崩潰時,mmap映射的文件會怎樣?
在程序的復雜運行機制中,mmap 作為一種內(nèi)存映射文件的關(guān)鍵方法,發(fā)揮著獨特作用。它將文件或其他對象映射到進程的地址空間,讓進程能以指針方式讀寫操作對應(yīng)內(nèi)存區(qū)域,系統(tǒng)會自動將臟頁面回寫至文件磁盤,極大簡化了文件操作流程。然而,程序運行并非總是一帆風順,崩潰情況時有發(fā)生。
當程序崩潰這一意外降臨,mmap映射的文件瞬間陷入未知之境。是能維持原狀,還是會數(shù)據(jù)丟失、損壞,又或是引發(fā)其他復雜問題?這背后牽扯到操作系統(tǒng)內(nèi)存管理機制、文件系統(tǒng)同步策略,以及mmap映射方式、文件打開模式等多方面因素。接下來,讓我們層層剖析,探尋程序崩潰時mmap映射文件的真實遭遇 。
一、mmap技術(shù)簡介
mmap 即 memory map,也就是內(nèi)存映射。mmap 是一種內(nèi)存映射文件的方法,即將一個文件或者其它對象映射到進程的地址空間,實現(xiàn)文件磁盤地址和進程虛擬地址空間中一段虛擬地址的一一對映關(guān)系。實現(xiàn)這樣的映射關(guān)系后,進程就可以采用指針的方式讀寫操作這一段內(nèi)存,而系統(tǒng)會自動回寫臟頁面到對應(yīng)的文件磁盤上,即完成了對文件的操作而不必再調(diào)用 read、write 等系統(tǒng)調(diào)用函數(shù)。相反,內(nèi)核空間對這段區(qū)域的修改也直接反映用戶空間,從而可以實現(xiàn)不同進程間的文件共享。
如下圖所示:
圖片
mmap的作用,在應(yīng)用這一層,是讓你把文件的某一段,當作內(nèi)存一樣來訪問。將文件映射到物理內(nèi)存,將進程虛擬空間映射到那塊內(nèi)存。這樣,進程不僅能像訪問內(nèi)存一樣讀寫文件,多個進程映射同一文件,還能保證虛擬空間映射到同一塊物理內(nèi)存,達到內(nèi)存共享的作用。
mmap 是 Linux 中用處非常廣泛的一個系統(tǒng)調(diào)用,它將一個文件或者其它對象映射進內(nèi)存。文件被映射到多個頁上,如果文件的大小不是所有頁的大小之和,最后一個頁不被使用的空間將會清零。mmap 必須以 PAGE_SIZE 為單位進行映射,而內(nèi)存也只能以頁為單位進行映射,若要映射非 PAGE_SIZE 整數(shù)倍的地址范圍,要先進行內(nèi)存對齊,強行以 PAGE_SIZE 的倍數(shù)大小進行映射。
其函數(shù)原型為:void *mmap (void start, size_t length, int prot, int flags, int fd, off_t offset);int munmap(void start, size_t length);。下面介紹一下內(nèi)存映射的步驟:
- 用 open 系統(tǒng)調(diào)用打開文件,并返回描述符 fd。
- 用 mmap 建立內(nèi)存映射,并返回映射首地址指針 start。
- 對映射(文件)進行各種操作,如顯示(printf)、修改(sprintf)等。
- 用 munmap (void *start, size_t length) 關(guān)閉內(nèi)存映射。
- 用 close 系統(tǒng)調(diào)用關(guān)閉文件 fd。
二、mmap工作原理
mmap函數(shù)創(chuàng)建一個新的vm_area_struct結(jié)構(gòu),并將其與文件/設(shè)備的物理地址相連。
vm_area_struct:linux使用vm_area_struct來表示一個獨立的虛擬內(nèi)存區(qū)域,一個進程可以使用多個vm_area_struct來表示不用類型的虛擬內(nèi)存區(qū)域(如堆,棧,代碼段,MMAP區(qū)域等)。
vm_area_struct結(jié)構(gòu)中包含了區(qū)域起始地址。同時也包含了一個vm_opt指針,其內(nèi)部可引出所有針對這個區(qū)域可以使用的系統(tǒng)調(diào)用函數(shù)。從而,進程可以通過vm_area_struct獲取操作這段內(nèi)存區(qū)域所需的任何信息。
進程通過vma操作內(nèi)存,而vma與文件/設(shè)備的物理地址相連,系統(tǒng)自動回寫臟頁面到對應(yīng)的文件磁盤上(或?qū)懭氲皆O(shè)備地址空間),實現(xiàn)內(nèi)存映射文件。
內(nèi)存映射文件的原理:
首先創(chuàng)建虛擬區(qū)間并完成地址映射,此時還沒有將任何文件數(shù)據(jù)拷貝至主存。當進程發(fā)起讀寫操作時,會訪問虛擬地址空間,通過查詢頁表,發(fā)現(xiàn)這段地址不在物理頁上,因為只建立了地址映射,真正的數(shù)據(jù)還沒有拷貝到內(nèi)存,因此引發(fā)缺頁異常。缺頁異常經(jīng)過一系列判斷,確定無非法操作后,內(nèi)核發(fā)起請求調(diào)頁過程。
最終會調(diào)用nopage函數(shù)把所缺的頁從文件在磁盤里的地址拷貝到物理內(nèi)存。之后進程便可以對這片主存進行讀寫,如果寫操作修改了內(nèi)容,一定時間后系統(tǒng)會自動回寫臟頁面到對應(yīng)的磁盤地址,完成了寫入到文件的過程。另外,也可以調(diào)用msync()來強制同步,這樣所寫的內(nèi)存就能立刻保存到文件中。
mmap內(nèi)存映射的實現(xiàn)過程,總的來說可以分為三個階段:
⑴進程啟動映射過程,并在虛擬地址空間中為映射創(chuàng)建虛擬映射區(qū)域
- 進程在用戶空間調(diào)用庫函數(shù)mmap,原型:void *mmap(void *start, size_t length, int prot, int flags, int fd, off_t offset);
- 在當前進程的虛擬地址空間中,尋找一段空閑的滿足要求的連續(xù)的虛擬地址
- 為此虛擬區(qū)分配一個vm_area_struct結(jié)構(gòu),接著對這個結(jié)構(gòu)的各個域進行了初始化
- 將新建的虛擬區(qū)結(jié)構(gòu)(vm_area_struct)插入進程的虛擬地址區(qū)域鏈表或樹中
⑵調(diào)用內(nèi)核空間的系統(tǒng)調(diào)用函數(shù)mmap(不同于用戶空間函數(shù)),實現(xiàn)文件物理地址和進程虛擬地址的一一映射關(guān)系
- 為映射分配了新的虛擬地址區(qū)域后,通過待映射的文件指針,在文件描述符表中找到對應(yīng)的文件描述符,通過文件描述符,鏈接到內(nèi)核“已打開文件集”中該文件的文件結(jié)構(gòu)體(struct file),每個文件結(jié)構(gòu)體維護著和這個已打開文件相關(guān)各項信息。
- 通過該文件的文件結(jié)構(gòu)體,鏈接到file_operations模塊,調(diào)用內(nèi)核函數(shù)mmap,其原型為:int mmap(struct file *filp, struct vm_area_struct *vma),不同于用戶空間庫函數(shù)。
- 內(nèi)核mmap函數(shù)通過虛擬文件系統(tǒng)inode模塊定位到文件磁盤物理地址。
- 通過remap_pfn_range函數(shù)建立頁表,即實現(xiàn)了文件地址和虛擬地址區(qū)域的映射關(guān)系。此時,這片虛擬地址并沒有任何數(shù)據(jù)關(guān)聯(lián)到主存中。
⑶進程發(fā)起對這片映射空間的訪問,引發(fā)缺頁異常,實現(xiàn)文件內(nèi)容到物理內(nèi)存(主存)的拷貝
注:前兩個階段僅在于創(chuàng)建虛擬區(qū)間并完成地址映射,但是并沒有將任何文件數(shù)據(jù)的拷貝至主存。真正的文件讀取是當進程發(fā)起讀或?qū)懖僮鲿r。
- 進程的讀或?qū)懖僮髟L問虛擬地址空間這一段映射地址,通過查詢頁表,發(fā)現(xiàn)這一段地址并不在物理頁面上。因為目前只建立了地址映射,真正的硬盤數(shù)據(jù)還沒有拷貝到內(nèi)存中,因此引發(fā)缺頁異常。
- 缺頁異常進行一系列判斷,確定無非法操作后,內(nèi)核發(fā)起請求調(diào)頁過程。
- 調(diào)頁過程先在交換緩存空間(swap cache)中尋找需要訪問的內(nèi)存頁,如果沒有則調(diào)用nopage函數(shù)把所缺的頁從磁盤裝入到主存中。
- 之后進程即可對這片主存進行讀或者寫的操作,如果寫操作改變了其內(nèi)容,一定時間后系統(tǒng)會自動回寫臟頁面到對應(yīng)磁盤地址,也即完成了寫入到文件的過程。
注:修改過的臟頁面并不會立即更新回文件中,而是有一段時間的延遲,可以調(diào)用msync()來強制同步, 這樣所寫的內(nèi)容就能立即保存到文件里了。
三、mmap的 I/O模型
mmap 也是一種零拷貝技術(shù),其 I/O 模型如下圖所示:、
圖片
#include <sys/mman.h>
void *mmap(
void *start,
size_t length,
int prot,
int flags,
int fd, off_t offset
)
圖片
mmap 技術(shù)有如下特點:
- 利用 DMA 技術(shù)來取代 CPU 來在內(nèi)存與其他組件之間的數(shù)據(jù)拷貝,例如從磁盤到內(nèi)存,從內(nèi)存到網(wǎng)卡;
- 用戶空間的 mmap file 使用虛擬內(nèi)存,實際上并不占據(jù)物理內(nèi)存,只有在內(nèi)核空間的 kernel buffer cache 才占據(jù)實際的物理內(nèi)存;
- mmap() 函數(shù)需要配合 write() 系統(tǒng)調(diào)動進行配合操作,這與 sendfile() 函數(shù)有所不同,后者一次性代替了 read() 以及 write();因此 mmap 也至少需要 4 次上下文切換;
- mmap 僅僅能夠避免內(nèi)核空間到用戶空間的全程 CPU 負責的數(shù)據(jù)拷貝,但是內(nèi)核空間內(nèi)部還是需要全程 CPU 負責的數(shù)據(jù)拷貝;
利用 mmap() 替換 read(),配合 write() 調(diào)用的整個流程如下:
- 用戶進程調(diào)用 mmap(),從用戶態(tài)陷入內(nèi)核態(tài),將內(nèi)核緩沖區(qū)映射到用戶緩存區(qū);
- DMA 控制器將數(shù)據(jù)從硬盤拷貝到內(nèi)核緩沖區(qū)(可見其使用了 Page Cache 機制);
- mmap() 返回,上下文從內(nèi)核態(tài)切換回用戶態(tài);
- 用戶進程調(diào)用 write(),嘗試把文件數(shù)據(jù)寫到內(nèi)核里的套接字緩沖區(qū),再次陷入內(nèi)核態(tài);
- CPU 將內(nèi)核緩沖區(qū)中的數(shù)據(jù)拷貝到的套接字緩沖區(qū);
- DMA 控制器將數(shù)據(jù)從套接字緩沖區(qū)拷貝到網(wǎng)卡完成數(shù)據(jù)傳輸;
- write() 返回,上下文從內(nèi)核態(tài)切換回用戶態(tài)。
通過mmap實現(xiàn)的零拷貝I/O進行了4次用戶空間與內(nèi)核空間的上下文切換,以及3次數(shù)據(jù)拷貝;其中3次數(shù)據(jù)拷貝中包括了2次DMA拷貝和1次CPU拷貝
3.1mmap與常規(guī)文件操作的區(qū)別
常規(guī)文件操作在進行讀寫時,為了提高效率和保護磁盤,采用了頁緩存機制。讀文件時,首先將文件頁從磁盤拷貝到頁緩存中,由于頁緩存處于內(nèi)核空間,用戶進程無法直接尋址,所以還需要將頁緩存中的數(shù)據(jù)頁再次拷貝到內(nèi)存對應(yīng)的用戶空間中。寫操作也類似,待寫入的 buffer 在內(nèi)核空間不能直接訪問,必須先拷貝至內(nèi)核空間對應(yīng)的主存,再寫回磁盤,同樣需要兩次數(shù)據(jù)拷貝。
而 mmap 在創(chuàng)建虛擬內(nèi)存區(qū)域和建立映射時無文件拷貝操作。當后續(xù)訪問數(shù)據(jù)引發(fā)缺頁異常時,僅需一次數(shù)據(jù)拷貝,就可以從磁盤中將數(shù)據(jù)傳入內(nèi)存的用戶空間中,供進程使用。
例如,在使用 mmap 操作文件中,以實現(xiàn)進程間通信為例,多個進程可以使用 mmap 來共享內(nèi)存段,通過已經(jīng)建立好的映射關(guān)系,在訪問數(shù)據(jù)時只進行一次數(shù)據(jù)拷貝,實現(xiàn)進程間快速通信。
總而言之,常規(guī)文件操作需要從磁盤到頁緩存再到用戶主存的兩次數(shù)據(jù)拷貝。而 mmap 操控文件,只需要從磁盤到用戶主存的一次數(shù)據(jù)拷貝過程。mmap 的關(guān)鍵點在于實現(xiàn)了用戶空間和內(nèi)核空間的數(shù)據(jù)直接交互,省去了空間不同數(shù)據(jù)不通的繁瑣過程,因此效率更高。
3.2mmap不是銀彈
mmap 不是銀彈,這意味著 mmap 也有其缺陷,在相關(guān)場景下的性能存在缺陷:
- 由于 MMAP 使用時必須實現(xiàn)指定好內(nèi)存映射的大小,因此 mmap 并不適合變長文件;
- 如果更新文件的操作很多,mmap 避免兩態(tài)拷貝的優(yōu)勢就被攤還,最終還是落在了大量的臟頁回寫及由此引發(fā)的隨機 I/O 上,所以在隨機寫很多的情況下,mmap 方式在效率上不一定會比帶緩沖區(qū)的一般寫快;
- 讀/寫小文件(例如 16K 以下的文件),mmap 與通過 read 系統(tǒng)調(diào)用相比有著更高的開銷與延遲;同時 mmap 的刷盤由系統(tǒng)全權(quán)控制,但是在小數(shù)據(jù)量的情況下由應(yīng)用本身手動控制更好;
- mmap 受限于操作系統(tǒng)內(nèi)存大小:例如在 32-bits 的操作系統(tǒng)上,虛擬內(nèi)存總大小也就 2GB,但由于 mmap 必須要在內(nèi)存中找到一塊連續(xù)的地址塊,此時你就無法對 4GB 大小的文件完全進行 mmap,在這種情況下你必須分多塊分別進行 mmap,但是此時地址內(nèi)存地址已經(jīng)不再連續(xù),使用 mmap 的意義大打折扣,而且引入了額外的復雜性;
四、mmap技術(shù)的優(yōu)勢
4.1簡化用戶進程編程
在用戶空間看來,通過 mmap 機制以后,磁盤上的文件仿佛直接就在內(nèi)存中,把訪問磁盤文件簡化為按地址訪問內(nèi)存。這樣一來,應(yīng)用程序自然不需要使用文件系統(tǒng)的 write(寫入)、read(讀取)、fsync(同步)等系統(tǒng)調(diào)用,因為現(xiàn)在只要面向內(nèi)存的虛擬空間進行開發(fā)。但是,這并不意味著我們不再需要進行這些系統(tǒng)調(diào)用,而是說這些系統(tǒng)調(diào)用由操作系統(tǒng)在 mmap 機制的內(nèi)部封裝好了。
①基于缺頁異常的懶加載
出于節(jié)約物理內(nèi)存以及 mmap 方法快速返回的目的,mmap 映射采用懶加載機制。具體來說,通過 mmap 申請 1000G 內(nèi)存可能僅僅占用了 100MB 的虛擬內(nèi)存空間,甚至沒有分配實際的物理內(nèi)存空間。當你訪問相關(guān)內(nèi)存地址時,才會進行真正的 write、read 等系統(tǒng)調(diào)用。CPU 會通過陷入缺頁異常的方式來將磁盤上的數(shù)據(jù)加載到物理內(nèi)存中,此時才會發(fā)生真正的物理內(nèi)存分配。
②數(shù)據(jù)一致性由 OS 確保
當發(fā)生數(shù)據(jù)修改時,內(nèi)存出現(xiàn)臟頁,與磁盤文件出現(xiàn)不一致。mmap 機制下由操作系統(tǒng)自動完成內(nèi)存數(shù)據(jù)落盤(臟頁回刷),用戶進程通常并不需要手動管理數(shù)據(jù)落盤。
4.2避免只讀操作時的 swap 操作
虛擬內(nèi)存帶來了種種好處,但是一個最大的問題在于所有進程的虛擬內(nèi)存大小總和可能大于物理內(nèi)存總大小,因此當操作系統(tǒng)物理內(nèi)存不夠用時,就會把一部分內(nèi)存 swap 到磁盤上。
在 mmap 下,如果虛擬空間沒有發(fā)生寫操作,那么由于通過 mmap 操作得到的內(nèi)存數(shù)據(jù)完全可以通過再次調(diào)用 mmap 操作映射文件得到。但是,通過其他方式分配的內(nèi)存,在沒有發(fā)生寫操作的情況下,操作系統(tǒng)并不知道如何簡單地從現(xiàn)有文件中(除非其重新執(zhí)行一遍應(yīng)用程序,但是代價很大)恢復內(nèi)存數(shù)據(jù),因此必須將內(nèi)存 swap 到磁盤上。
高效的 I/O 操作方式,尤其在處理大文件或頻繁訪問文件內(nèi)容時性能優(yōu)勢明顯。
在 Linux 系統(tǒng)中,mmap 是一種非常高效的 I/O 操作方式。當處理大文件或需要頻繁訪問文件內(nèi)容時,能夠帶來很大的性能優(yōu)勢。例如,當一個進程通過 mmap 映射一個文件時,操作系統(tǒng)會在進程的地址空間中創(chuàng)建一個映射區(qū)域,使得進程可以直接訪問這個文件而不需要進行 read 或 write 系統(tǒng)調(diào)用。這種直接內(nèi)存訪問的方式,避免了傳統(tǒng)文件訪問中多次系統(tǒng)調(diào)用和數(shù)據(jù)復制的開銷,提高了文件訪問的效率。
減少 CPU 和內(nèi)存開銷,具有更好的內(nèi)核態(tài)數(shù)據(jù)傳輸效率。
mmap 技術(shù)可以減少 CPU 和內(nèi)存的開銷。它通過將文件或設(shè)備映射到進程的地址空間中,實現(xiàn)了直接內(nèi)存訪問,避免了內(nèi)核緩沖區(qū)和用戶空間緩沖區(qū)之間的數(shù)據(jù)復制。此外,mmap 還具有更好的內(nèi)核態(tài)數(shù)據(jù)傳輸效率,有助于減少數(shù)據(jù)傳輸時的內(nèi)存拷貝。例如,在 Kafka 中,Consumer 端對稀疏索引的操作使用了 mmap,將稀疏索引文件進行內(nèi)存映射,不會招致系統(tǒng)調(diào)用以及額外的內(nèi)存復制開銷,從而提高了文件讀取效率。
提升系統(tǒng)整體性能,改善用戶體驗。
合理地利用 mmap 技術(shù),能夠提升系統(tǒng)的整體性能,改善用戶體驗。在開發(fā)應(yīng)用程序時,可以考慮使用 mmap 技術(shù)來加速文件訪問、減少內(nèi)存拷貝、提高數(shù)據(jù)傳輸效率等方面。例如,在處理大文件時,mmap 可以不用把全部數(shù)據(jù)都加載到內(nèi)存,可以通過 MappedByteBuffer 的 position 來設(shè)置獲取數(shù)據(jù)的位置,還可以使用虛擬內(nèi)存來映射超過物理內(nèi)存大小的大文件。同時,mmap 也支持多進程訪問和文件的共享,多個進程可以共享同一個文件的內(nèi)容,從而減少內(nèi)存的使用,提高系統(tǒng)的性能。
五、mmap技術(shù)的應(yīng)用場景
5.1內(nèi)存映射 I/O,加速文件讀寫操作,適合處理大文件。
mmap 可以將文件直接映射到進程的虛擬地址空間,避免了傳統(tǒng)文件讀寫中的多次系統(tǒng)調(diào)用和數(shù)據(jù)拷貝。在處理大文件時,這種方式尤其有效。例如,當需要對一個大型數(shù)據(jù)文件進行頻繁的讀寫操作時,使用 mmap 可以大大提高效率。通過內(nèi)存映射,進程可以像訪問內(nèi)存一樣訪問文件數(shù)據(jù),減少了磁盤 I/O 的開銷。
參考資料中提到,進程讀寫數(shù)據(jù)時,使用 mmap 進行文件映射可以減少一次拷貝操作。磁盤文件直接加載到用戶空間,進程可以通過指針直接操作文件,理論上比傳統(tǒng)的 read 和 write 操作要快。雖然在讀寫過程中可能會觸發(fā)大量中斷,但對于大文件的處理,mmap 仍然具有很大的優(yōu)勢。
5.2進程間通信,多個進程可通過共享內(nèi)存實現(xiàn)快速通信。
多個進程可以通過共享內(nèi)存的方式,使用 mmap 來共享內(nèi)存段,實現(xiàn)進程間快速通信。例如,在父子進程或無親緣關(guān)系的進程中,都可以將自身用戶空間映射到同一個文件或匿名映射到同一片區(qū)域,從而實現(xiàn)進程間通信。
參考資料中提到,在進程間通信的場景下,可以使用 mmap 將文件映射到內(nèi)存,多個進程通過對同一文件的讀寫達到進程間通信的目的。同時,共享匿名內(nèi)存也可以讓相關(guān)進程共享一塊內(nèi)存區(qū)域,通常用于父子進程。
5.3內(nèi)存分配,匿名映射可提供比 malloc 更靈活的內(nèi)存管理機制。
當需要大塊的內(nèi)存,或者特定對齊要求的內(nèi)存時,mmap 的匿名映射可以提供比 malloc 更靈活的內(nèi)存管理機制。例如,當需要分配的內(nèi)存大于一定閾值(如 128KB)時,glibc 會默認使用 mmap 代替 brk 來分配內(nèi)存。
私有匿名映射最常見的用途是在 glibc 分配大塊的內(nèi)存中。同時,共享匿名映射也可以讓相關(guān)進程共享一塊內(nèi)存區(qū)域,為內(nèi)存分配提供了更多的靈活性。
六、如何使用mmap技術(shù)
6.1mmap使用細節(jié)
使用mmap需要注意的一個關(guān)鍵點是,mmap映射區(qū)域大小必須是物理頁大小(page_size)的整倍數(shù)(32位系統(tǒng)中通常是4k字節(jié))。原因是,內(nèi)存的最小粒度是頁,而進程虛擬地址空間和內(nèi)存的映射也是以頁為單位。為了匹配內(nèi)存的操作,mmap從磁盤到虛擬地址空間的映射也必須是頁。
內(nèi)核可以跟蹤被內(nèi)存映射的底層對象(文件)的大小,進程可以合法的訪問在當前文件大小以內(nèi)又在內(nèi)存映射區(qū)以內(nèi)的那些字節(jié)。也就是說,如果文件的大小一直在擴張,只要在映射區(qū)域范圍內(nèi)的數(shù)據(jù),進程都可以合法得到,這和映射建立時文件的大小無關(guān)。
映射建立之后,即使文件關(guān)閉,映射依然存在。因為映射的是磁盤的地址,不是文件本身,和文件句柄無關(guān)。同時可用于進程間通信的有效地址空間不完全受限于被映射文件的大小,因為是按頁映射。
在上面的知識前提下,我們下面看看如果大小不是頁的整倍數(shù)的具體情況:
情形一:一個文件的大小是5000字節(jié),mmap函數(shù)從一個文件的起始位置開始,映射5000字節(jié)到虛擬內(nèi)存中。
分析:因為單位物理頁面的大小是4096字節(jié),雖然被映射的文件只有5000字節(jié),但是對應(yīng)到進程虛擬地址區(qū)域的大小需要滿足整頁大小,因此mmap函數(shù)執(zhí)行后,實際映射到虛擬內(nèi)存區(qū)域8192個 字節(jié),5000~8191的字節(jié)部分用零填充。映射后的對應(yīng)關(guān)系如下圖所示:
圖片
此時:(1)讀/寫前5000個字節(jié)(0~4999),會返回操作文件內(nèi)容。(2)讀字節(jié)50008191時,結(jié)果全為0。寫50008191時,進程不會報錯,但是所寫的內(nèi)容不會寫入原文件中 。(3)讀/寫8192以外的磁盤部分,會返回一個SIGSECV錯誤。
情形二:一個文件的大小是5000字節(jié),mmap函數(shù)從一個文件的起始位置開始,映射15000字節(jié)到虛擬內(nèi)存中,即映射大小超過了原始文件的大小。
分析:由于文件的大小是5000字節(jié),和情形一一樣,其對應(yīng)的兩個物理頁。那么這兩個物理頁都是合法可以讀寫的,只是超出5000的部分不會體現(xiàn)在原文件中。由于程序要求映射15000字節(jié),而文件只占兩個物理頁,因此8192字節(jié)~15000字節(jié)都不能讀寫,操作時會返回異常。如下圖所示:
圖片
此時:(1)進程可以正常讀/寫被映射的前5000字節(jié)(0~4999),寫操作的改動會在一定時間后反映在原文件中。(2)對于5000~8191字節(jié),進程可以進行讀寫過程,不會報錯。但是內(nèi)容在寫入前均為0,另外,寫入后不會反映在文件中。(3)對于8192~14999字節(jié),進程不能對其進行讀寫,會報SIGBUS錯誤。(4)對于15000以外的字節(jié),進程不能對其讀寫,會引發(fā)SIGSEGV錯誤。
情形三:一個文件初始大小為0,使用mmap操作映射了10004K的大小,即1000個物理頁大約4M字節(jié)空間,mmap返回指針ptr。
分析:如果在映射建立之初,就對文件進行讀寫操作,由于文件大小為0,并沒有合法的物理頁對應(yīng),如同情形二一樣,會返回SIGBUS錯誤。但是如果,每次操作ptr讀寫前,先增加文件的大小,那么ptr在文件大小內(nèi)部的操作就是合法的。例如,文件擴充4096字節(jié),ptr就能操作ptr ~ [ (char)ptr + 4095]的空間。只要文件擴充的范圍在1000個物理頁(映射范圍)內(nèi),ptr都可以對應(yīng)操作相同的大小。這樣,方便隨時擴充文件空間,隨時寫入文件,不造成空間浪費。
6.2函數(shù)定義及參數(shù)解釋
在 Linux 中,mmap 函數(shù)定義如下:void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);。參數(shù)解釋如下:
- addr:希望映射的起始地址,通常為 NULL,表示由內(nèi)核決定映射的地址。
- length:映射區(qū)域的大小(以字節(jié)為單位)。
- prot:映射區(qū)域的保護權(quán)限,決定映射的頁面是否可讀、可寫等。常見的權(quán)限選項包括:PROT_READ(可讀)、PROT_WRITE(可寫)、PROT_EXEC(可執(zhí)行)、PROT_NONE(無權(quán)限)。
- flags:映射的類型和行為控制。常見的標志包括:MAP_SHARED(共享映射,對該內(nèi)存的修改會同步到文件)、MAP_PRIVATE(私有映射,對該內(nèi)存的修改不會影響原文件,寫時拷貝)、MAP_ANONYMOUS(匿名映射,不涉及文件,通常用于分配未初始化的內(nèi)存)。
- fd:文件描述符,指向要映射的文件。如果使用匿名映射,應(yīng)將 fd 設(shè)置為 -1,并且需要設(shè)置 MAP_ANONYMOUS 標志。
- offset:文件映射的偏移量,必須是頁面大小的整數(shù)倍(通常為 4096 字節(jié))。
返回值:返回映射區(qū)域的起始地址,如果映射失敗,則返回 MAP_FAILED。
6.3mmap映射
在內(nèi)存映射的過程中,并沒有實際的數(shù)據(jù)拷貝,文件沒有被載入內(nèi)存,只是邏輯上被放入了內(nèi)存,具體到代碼,就是建立并初始化了相關(guān)的數(shù)據(jù)結(jié)構(gòu)(struct address_space),這個過程有系統(tǒng)調(diào)用mmap()實現(xiàn),所以建立內(nèi)存映射的效率很高。既然建立內(nèi)存映射沒有進行實際的數(shù)據(jù)拷貝,那么進程又怎么能最終直接通過內(nèi)存操作訪問到硬盤上的文件呢?
那就要看內(nèi)存映射之后的幾個相關(guān)的過程了。mmap()會返回一個指針ptr,它指向進程邏輯地址空間中的一個地址,這樣以后,進程無需再調(diào)用read或write對文件進行讀寫,而只需要通過ptr就能夠操作文件。但是ptr所指向的是一個邏輯地址,要操作其中的數(shù)據(jù),必須通過MMU將邏輯地址轉(zhuǎn)換成物理地址,這個過程與內(nèi)存映射無關(guān)。前面講過,建立內(nèi)存映射并沒有實際拷貝數(shù)據(jù),這時,MMU在地址映射表中是無法找到與ptr相對應(yīng)的物理地址的,也就是MMU失敗,將產(chǎn)生一個缺頁中斷,缺頁中斷的中斷響應(yīng)函數(shù)會在swap中尋找相對應(yīng)的頁面,如果找不到(也就是該文件從來沒有被讀入內(nèi)存的情況),則會通過mmap()建立的映射關(guān)系,從硬盤上將文件讀取到物理內(nèi)存中,如圖1中過程3所示。這個過程與內(nèi)存映射無關(guān)。如果在拷貝數(shù)據(jù)時,發(fā)現(xiàn)物理內(nèi)存不夠用,則會通過虛擬內(nèi)存機制(swap)將暫時不用的物理頁面交換到硬盤上,這個過程也與內(nèi)存映射無關(guān)。
mmap內(nèi)存映射的實現(xiàn)過程:
- 進程啟動映射過程,并在虛擬地址空間中為映射創(chuàng)建虛擬映射區(qū)域
- 調(diào)用內(nèi)核空間的系統(tǒng)調(diào)用函數(shù)mmap(不同于用戶空間函數(shù)),實現(xiàn)文件物理地址和進程虛擬地址的一一映射關(guān)系
- 進程發(fā)起對這片映射空間的訪問,引發(fā)缺頁異常,實現(xiàn)文件內(nèi)容到物理內(nèi)存(主存)的拷貝
適合的場景
- 您有一個很大的文件,其內(nèi)容您想要隨機訪問一個或多個時間
- 您有一個小文件,它的內(nèi)容您想要立即讀入內(nèi)存并經(jīng)常訪問。這種技術(shù)最適合那些大小不超過幾個虛擬內(nèi)存頁的文件。(頁是地址空間的最小單位,虛擬頁和物理頁的大小是一樣的,通常為4KB。)
- 您需要在內(nèi)存中緩存文件的特定部分。文件映射消除了緩存數(shù)據(jù)的需要,這使得系統(tǒng)磁盤緩存中的其他數(shù)據(jù)空間更大 當隨機訪問一個非常大的文件時,通常最好只映射文件的一小部分。映射大文件的問題是文件會消耗活動內(nèi)存。如果文件足夠大,系統(tǒng)可能會被迫將其他部分的內(nèi)存分頁以加載文件。將多個文件映射到內(nèi)存中會使這個問題更加復雜。
不適合的場景
- 您希望從開始到結(jié)束的順序從頭到尾讀取一個文件
- 這個文件有幾百兆字節(jié)或者更大。將大文件映射到內(nèi)存中會快速地填充內(nèi)存,并可能導致分頁,這將抵消首先映射文件的好處。對于大型順序讀取操作,禁用磁盤緩存并將文件讀入一個小內(nèi)存緩沖區(qū)
- 該文件大于可用的連續(xù)虛擬內(nèi)存地址空間。對于64位應(yīng)用程序來說,這不是什么問題,但是對于32位應(yīng)用程序來說,這是一個問題
- 該文件位于可移動驅(qū)動器上
- 該文件位于網(wǎng)絡(luò)驅(qū)動器上
示例代碼
//
// ViewController.m
// TestCode
//
// Created by zhangdasen on 2020/5/24.
// Copyright ? 2020 zhangdasen. All rights reserved.
//
#import "ViewController.h"
#import <sys/mman.h>
#import <sys/stat.h>
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
NSString *path = [NSHomeDirectory() stringByAppendingPathComponent:@"test.data"];
NSLog(@"path: %@", path);
NSString *str = @"test str2";
[str writeToFile:path atomically:YES encoding:NSUTF8StringEncoding error:nil];
ProcessFile(path.UTF8String);
NSString *result = [NSString stringWithContentsOfFile:path encoding:NSUTF8StringEncoding error:nil];
NSLog(@"result:%@", result);
}
int MapFile(const char * inPathName, void ** outDataPtr, size_t * outDataLength, size_t appendSize)
{
int outError;
int fileDescriptor;
struct stat statInfo;
// Return safe values on error.
outError = 0;
*outDataPtr = NULL;
*outDataLength = 0;
// Open the file.
fileDescriptor = open( inPathName, O_RDWR, 0 );
if( fileDescriptor < 0 )
{
outError = errno;
}
else
{
// We now know the file exists. Retrieve the file size.
if( fstat( fileDescriptor, &statInfo ) != 0 )
{
outError = errno;
}
else
{
ftruncate(fileDescriptor, statInfo.st_size + appendSize);
fsync(fileDescriptor);
*outDataPtr = mmap(NULL,
statInfo.st_size + appendSize,
PROT_READ|PROT_WRITE,
MAP_FILE|MAP_SHARED,
fileDescriptor,
0);
if( *outDataPtr == MAP_FAILED )
{
outError = errno;
}
else
{
// On success, return the size of the mapped file.
*outDataLength = statInfo.st_size;
}
}
// Now close the file. The kernel doesn’t use our file descriptor.
close( fileDescriptor );
}
return outError;
}
void ProcessFile(const char * inPathName)
{
size_t dataLength;
void * dataPtr;
char *appendStr = " append_key2";
int appendSize = (int)strlen(appendStr);
if( MapFile(inPathName, &dataPtr, &dataLength, appendSize) == 0) {
dataPtr = dataPtr + dataLength;
memcpy(dataPtr, appendStr, appendSize);
// Unmap files
munmap(dataPtr, appendSize + dataLength);
}
}
@end
6.5解除映射的方法
使用 mmap 后,必須調(diào)用 munmap 來解除映射,釋放分配的虛擬內(nèi)存。其函數(shù)定義如下:int munmap(void *addr, size_t length);。
- addr:要解除映射的內(nèi)存區(qū)域的起始地址。
- length:要解除映射的大小。
返回值:成功返回 0,失敗返回 -1。
⑴利用 mmap 訪問硬件,減少數(shù)據(jù)拷貝次數(shù)
mmap 可以將文件、設(shè)備等外部資源映射到內(nèi)存地址空間,進程可以像訪問內(nèi)存一樣訪問文件數(shù)據(jù)或硬件資源。當使用 mmap 訪問硬件時,數(shù)據(jù)可以直接從硬件設(shè)備通過 DMA 拷貝到內(nèi)核緩沖區(qū),然后進程可以直接訪問這個緩沖區(qū),減少了數(shù)據(jù)拷貝的次數(shù)。例如,在嵌入式系統(tǒng)中,可以使用 mmap 將物理地址映射到用戶虛擬地址空間,實現(xiàn)對硬件設(shè)備的直接訪問。在進行數(shù)據(jù)傳輸時,避免了傳統(tǒng)方式中從內(nèi)核空間到用戶空間的多次數(shù)據(jù)拷貝,提高了數(shù)據(jù)傳輸?shù)男省?/span>
⑵通過 mmap 實現(xiàn)將物理地址映射到用戶虛擬地址空間
可以通過以下步驟實現(xiàn)將物理地址映射到用戶虛擬地址空間:
- 打開 /dev/mem 文件獲得文件描述符 dev_mem_fd。
- 使用 mmap 函數(shù)進行映射,將物理地址映射到用戶虛擬地址空間。例如,定義一個函數(shù) dma_mmap 來實現(xiàn)這個功能,函數(shù)原型為 int dma_mmap(unsigned long addr_p, unsigned int len, unsigned char** addr_v)。在這個函數(shù)中,首先打開 /dev/mem 文件,然后使用 mmap 函數(shù)進行映射,最后返回虛擬地址。
- 使用映射后的虛擬地址進行操作,例如讀寫硬件設(shè)備。
- 在使用完后,調(diào)用 dma_munmap 函數(shù)解除映射,釋放資源。函數(shù)原型為 unsigned int dma_munmap(unsigned char* addr_v, unsigned long addr_p, unsigned int len)。
⑶在嵌入式系統(tǒng)中,還可以通過以下方式實現(xiàn)物理地址到用戶虛擬地址空間的映射:
- 在驅(qū)動程序中,實現(xiàn) mmap 方法,建立虛擬地址到物理地址的頁表。例如,可以使用 remap_pfn_range 函數(shù)一次建立所有頁表,或者使用 nopage VMA 方法每次建立一個頁表。
- 在用戶空間程序中,使用 mmap 函數(shù)進行映射,將文件描述符、映射大小、保護權(quán)限等參數(shù)傳入,獲得映射后的虛擬地址。然后可以通過這個虛擬地址對硬件設(shè)備進行操作。