天天看點

認真分析mmap:是什麼 為什麼 怎麼用

mmap是一種記憶體映射檔案的方法,即将一個檔案或者其它對象映射到程序的位址空間,實作檔案磁盤位址和程序虛拟位址空間中一段虛拟位址的一一對映關系。實作這樣的映射關系後,程序就可以采用指針的方式讀寫操作這一段記憶體,而系統會自動回寫髒頁面到對應的檔案磁盤上,即完成了對檔案的操作而不必再調用read,write等系統調用函數。相反,核心空間對這段區域的修改也直接反映使用者空間,進而可以實作不同程序間的檔案共享。如下圖所示:

認真分析mmap:是什麼 為什麼 怎麼用

由上圖可以看出,程序的虛拟位址空間,由多個虛拟記憶體區域構成。虛拟記憶體區域是程序的虛拟位址空間中的一個同質區間,即具有同樣特性的連續位址範圍。上圖中所示的text資料段(代碼段)、初始資料段、BSS資料段、堆、棧和記憶體映射,都是一個獨立的虛拟記憶體區域。而為記憶體映射服務的位址空間處在堆棧之間的空餘部分。

linux核心使用vm_area_struct結構來表示一個獨立的虛拟記憶體區域,由于每個不同質的虛拟記憶體區域功能和内部機制都不同,是以一個程序使用多個vm_area_struct結構來分别表示不同類型的虛拟記憶體區域。各個vm_area_struct結構使用連結清單或者樹形結構連結,友善程序快速通路,如下圖所示:

認真分析mmap:是什麼 為什麼 怎麼用

vm_area_struct結構中包含區域起始和終止位址以及其他相關資訊,同時也包含一個vm_ops指針,其内部可引出所有針對這個區域可以使用的系統調用函數。這樣,程序對某一虛拟記憶體區域的任何操作需要用要的資訊,都可以從vm_area_struct中獲得。mmap函數就是要建立一個新的vm_area_struct結構,并将其與檔案的實體磁盤位址相連。具體步驟請看下一節。

mmap記憶體映射的實作過程,總的來說可以分為三個階段:

(一)程序啟動映射過程,并在虛拟位址空間中為映射建立虛拟映射區域

1、程序在使用者空間調用庫函數mmap,原型:void *mmap(void *start, size_t length, int prot, int flags, int fd, off_t offset);

2、在目前程序的虛拟位址空間中,尋找一段空閑的滿足要求的連續的虛拟位址

3、為此虛拟區配置設定一個vm_area_struct結構,接着對這個結構的各個域進行了初始化

4、将建立的虛拟區結構(vm_area_struct)插入程序的虛拟位址區域連結清單或樹中

(二)調用核心空間的系統調用函數mmap(不同于使用者空間函數),實作檔案實體位址和程序虛拟位址的一一映射關系

5、為映射配置設定了新的虛拟位址區域後,通過待映射的檔案指針,在檔案描述符表中找到對應的檔案描述符,通過檔案描述符,連結到核心“已打開檔案集”中該檔案的檔案結構體(struct file),每個檔案結構體維護着和這個已打開檔案相關各項資訊。

6、通過該檔案的檔案結構體,連結到file_operations子產品,調用核心函數mmap,其原型為:int mmap(struct file *filp, struct vm_area_struct *vma),不同于使用者空間庫函數。

7、核心mmap函數通過虛拟檔案系統inode子產品定位到檔案磁盤實體位址。

8、通過remap_pfn_range函數建立頁表,即實作了檔案位址和虛拟位址區域的映射關系。此時,這片虛拟位址并沒有任何資料關聯到主存中。

(三)程序發起對這片映射空間的通路,引發缺頁異常,實作檔案内容到實體記憶體(主存)的拷貝

注:前兩個階段僅在于建立虛拟區間并完成位址映射,但是并沒有将任何檔案資料的拷貝至主存。真正的檔案讀取是當程序發起讀或寫操作時。

9、程序的讀或寫操作通路虛拟位址空間這一段映射位址,通過查詢頁表,發現這一段位址并不在實體頁面上。因為目前隻建立了位址映射,真正的硬碟資料還沒有拷貝到記憶體中,是以引發缺頁異常。

10、缺頁異常進行一系列判斷,确定無非法操作後,核心發起請求調頁過程。

11、調頁過程先在交換緩存空間(swap cache)中尋找需要通路的記憶體頁,如果沒有則調用nopage函數把所缺的頁從磁盤裝入到主存中。

12、之後程序即可對這片主存進行讀或者寫的操作,如果寫操作改變了其内容,一定時間後系統會自動回寫髒頁面到對應磁盤位址,也即完成了寫入到檔案的過程。

注:修改過的髒頁面并不會立即更新回檔案中,而是有一段時間的延遲,可以調用msync()來強制同步, 這樣所寫的内容就能立即儲存到檔案裡了。

對linux檔案系統不了解的朋友,請參閱我之前寫的博文《從核心檔案系統看檔案讀寫過程》,我們首先簡單的回顧一下正常檔案系統操作(調用read/fread等類函數)中,函數的調用過程:

1、程序發起讀檔案請求。

2、核心通過查找程序檔案符表,定位到核心已打開檔案集上的檔案資訊,進而找到此檔案的inode。

3、inode在address_space上查找要請求的檔案頁是否已經緩存在頁緩存中。如果存在,則直接傳回這片檔案頁的内容。

4、如果不存在,則通過inode定位到檔案磁盤位址,将資料從磁盤複制到頁緩存。之後再次發起讀頁面過程,進而将頁緩存中的資料發給使用者程序。

總結來說,正常檔案操作為了提高讀寫效率和保護磁盤,使用了頁緩存機制。這樣造成讀檔案時需要先将檔案頁從磁盤拷貝到頁緩存中,由于頁緩存處在核心空間,不能被使用者程序直接尋址,是以還需要将頁緩存中資料頁再次拷貝到記憶體對應的使用者空間中。這樣,通過了兩次資料拷貝過程,才能完成程序對檔案内容的擷取任務。寫操作也是一樣,待寫入的buffer在核心空間不能直接通路,必須要先拷貝至核心空間對應的主存,再寫回磁盤中(延遲寫回),也是需要兩次資料拷貝。

而使用mmap操作檔案中,建立新的虛拟記憶體區域和建立檔案磁盤位址和虛拟記憶體區域映射這兩步,沒有任何檔案拷貝操作。而之後通路資料時發現記憶體中并無資料而發起的缺頁異常過程,可以通過已經建立好的映射關系,隻使用一次資料拷貝,就從磁盤中将資料傳入記憶體的使用者空間中,供程序使用。

總而言之,正常檔案操作需要從磁盤到頁緩存再到使用者主存的兩次資料拷貝。而mmap操控檔案,隻需要從磁盤到使用者主存的一次資料拷貝過程。說白了,mmap的關鍵點是實作了使用者空間和核心空間的資料直接互動而省去了空間不同資料不通的繁瑣過程。是以mmap效率更高。

由上文讨論可知,mmap優點共有一下幾點:

1、對檔案的讀取操作跨過了頁緩存,減少了資料的拷貝次數,用記憶體讀寫取代I/O讀寫,提高了檔案讀取效率。

2、實作了使用者空間和核心空間的高效互動方式。兩空間的各自修改操作可以直接反映在映射的區域内,進而被對方空間及時捕捉。

3、提供程序間共享記憶體及互相通信的方式。不管是父子程序還是無親緣關系的程序,都可以将自身使用者空間映射到同一個檔案或匿名映射到同一片區域。進而通過各自對映射區域的改動,達到程序間通信和程序間共享的目的。

     同時,如果程序A和程序B都映射了區域C,當A第一次讀取C時通過缺頁從磁盤複制檔案頁到記憶體中;但當B再讀C的相同頁面時,雖然也會産生缺頁異常,但是不再需要從磁盤中複制檔案過來,而可直接使用已經儲存在記憶體中的檔案資料。

4、可用于實作高效的大規模資料傳輸。記憶體空間不足,是制約大資料操作的一個方面,解決方案往往是借助硬碟空間協助操作,補充記憶體的不足。但是進一步會造成大量的檔案I/O操作,極大影響效率。這個問題可以通過mmap映射很好的解決。換句話說,但凡是需要用磁盤空間代替記憶體的時候,mmap都可以發揮其功效。

函數原型

void *mmap(void *start, size_t length, int prot, int flags, int fd, off_t offset);

傳回說明

成功執行時,mmap()傳回被映射區的指針。失敗時,mmap()傳回MAP_FAILED[其值為(void *)-1], error被設為以下的某個值:

認真分析mmap:是什麼 為什麼 怎麼用
認真分析mmap:是什麼 為什麼 怎麼用

傳回錯誤類型

參數

start:映射區的開始位址

length:映射區的長度

prot:期望的記憶體保護标志,不能與檔案的打開模式沖突。是以下的某個值,可以通過or運算合理地組合在一起

認真分析mmap:是什麼 為什麼 怎麼用
認真分析mmap:是什麼 為什麼 怎麼用

prot

flags:指定映射對象的類型,映射選項和映射頁是否可以共享。它的值可以是一個或者多個以下位的組合體

認真分析mmap:是什麼 為什麼 怎麼用
認真分析mmap:是什麼 為什麼 怎麼用

flag

fd:有效的檔案描述詞。如果MAP_ANONYMOUS被設定,為了相容問題,其值應為-1

offset:被映射對象内容的起點

相關函數

int munmap( void * addr, size_t len ) 

成功執行時,munmap()傳回0。失敗時,munmap傳回-1,error傳回标志和mmap一緻;

該調用在程序位址空間中解除一個映射關系,addr是調用mmap()時傳回的位址,len是映射區的大小;

當映射關系解除後,對原來映射位址的通路将導緻段錯誤發生。 

int msync( void *addr, size_t len, int flags )

一般說來,程序在映射空間的對共享内容的改變并不直接寫回到磁盤檔案中,往往在調用munmap()後才執行該操作。

可以通過調用msync()實作磁盤上檔案内容與共享記憶體區的内容一緻。

1、使用mmap需要注意的一個關鍵點是,mmap映射區域大小必須是實體頁大小(page_size)的整倍數(32位系統中通常是4k位元組)。原因是,記憶體的最小粒度是頁,而程序虛拟位址空間和記憶體的映射也是以頁為機關。為了比對記憶體的操作,mmap從磁盤到虛拟位址空間的映射也必須是頁。

2、核心可以跟蹤被記憶體映射的底層對象(檔案)的大小,程序可以合法的通路在目前檔案大小以内又在記憶體映射區以内的那些位元組。也就是說,如果檔案的大小一直在擴張,隻要在映射區域範圍内的資料,程序都可以合法得到,這和映射建立時檔案的大小無關。具體情形參見“情形三”。

3、映射建立之後,即使檔案關閉,映射依然存在。因為映射的是磁盤的位址,不是檔案本身,和檔案句柄無關。同時可用于程序間通信的有效位址空間不完全受限于被映射檔案的大小,因為是按頁映射。

在上面的知識前提下,我們下面看看如果大小不是頁的整倍數的具體情況:

情形一:一個檔案的大小是5000位元組,mmap函數從一個檔案的起始位置開始,映射5000位元組到虛拟記憶體中。

分析:因為機關實體頁面的大小是4096位元組,雖然被映射的檔案隻有5000位元組,但是對應到程序虛拟位址區域的大小需要滿足整頁大小,是以mmap函數執行後,實際映射到虛拟記憶體區域8192個 位元組,5000~8191的位元組部分用零填充。映射後的對應關系如下圖所示:

認真分析mmap:是什麼 為什麼 怎麼用

此時:

(1)讀/寫前5000個位元組(0~4999),會傳回操作檔案内容。

(2)讀位元組5000~8191時,結果全為0。寫5000~8191時,程序不會報錯,但是所寫的内容不會寫入原檔案中 。

(3)讀/寫8192以外的磁盤部分,會傳回一個SIGSECV錯誤。

情形二:一個檔案的大小是5000位元組,mmap函數從一個檔案的起始位置開始,映射15000位元組到虛拟記憶體中,即映射大小超過了原始檔案的大小。

分析:由于檔案的大小是5000位元組,和情形一一樣,其對應的兩個實體頁。那麼這兩個實體頁都是合法可以讀寫的,隻是超出5000的部分不會展現在原檔案中。由于程式要求映射15000位元組,而檔案隻占兩個實體頁,是以8192位元組~15000位元組都不能讀寫,操作時會傳回異常。如下圖所示:

認真分析mmap:是什麼 為什麼 怎麼用

(1)程序可以正常讀/寫被映射的前5000位元組(0~4999),寫操作的改動會在一定時間後反映在原檔案中。

(2)對于5000~8191位元組,程序可以進行讀寫過程,不會報錯。但是内容在寫入前均為0,另外,寫入後不會反映在檔案中。

(3)對于8192~14999位元組,程序不能對其進行讀寫,會報SIGBUS錯誤。

(4)對于15000以外的位元組,程序不能對其讀寫,會引發SIGSEGV錯誤。

情形三:一個檔案初始大小為0,使用mmap操作映射了1000*4K的大小,即1000個實體頁大約4M位元組空間,mmap傳回指針ptr。

分析:如果在映射建立之初,就對檔案進行讀寫操作,由于檔案大小為0,并沒有合法的實體頁對應,如同情形二一樣,會傳回SIGBUS錯誤。

但是如果,每次操作ptr讀寫前,先增加檔案的大小,那麼ptr在檔案大小内部的操作就是合法的。例如,檔案擴充4096位元組,ptr就能操作ptr ~ [ (char)ptr + 4095]的空間。隻要檔案擴充的範圍在1000個實體頁(映射範圍)内,ptr都可以對應操作相同的大小。

這樣,友善随時擴充檔案空間,随時寫入檔案,不造成空間浪費。

繼續閱讀