徹底理解mmap()

徹底理解mmap()


最近起的標題總是這麼標題黨!

什麼是 mmap()

mmap, 從函數名就可以看出來這是memory map, 即地址的映射, 是一種內存映射文件的方法, (其他的還有mmap()系統調用,Posix共享內存,以及系統V共享內存,這些我們有機會在後續的文章討論,今天的男主角是mmap),將一個文件或者其它對象映射到進程的地址空間,實現文件磁盤地址和進程虛擬地址空間中一段虛擬地址的一一對映關係。mmap()系統調用使得進程之間通過映射同一個普通文件實現共享內存。普通文件被映射到進程地址空間後,進程可以向訪問普通內存一樣對文件進行訪問,不必再調用read(),write()等操作。

注:實際上,mmap()系統調用並不是完全爲了用於共享內存而設計的。它本身提供了不同於一般對普通文件的訪問方式,進程可以像讀寫內存一樣對普通文件的操作。而Posix或系統V的共享內存IPC則純粹用於共享目的,當然mmap()實現共享內存也是其主要應用之一。


爲什麼使用 mmap()

Linux通過內存映像機制來提供用戶程序對內存直接訪問的能力。內存映像的意思是把內核中特定部分的內存空間映射到用戶級程序的內存空間去。也就是說,用戶空間和內核空間共享一塊相同的內存。這樣做的直觀效果顯而易見:內核在這塊地址內存儲變更的任何數據,用戶可以立即發現和使用,根本無須數據拷貝。舉個例子理解一下,使用mmap方式獲取磁盤上的文件信息,只需要將磁盤上的數據拷貝至那塊共享內存中去,用戶進程可以直接獲取到信息,而相對於傳統的write/read IO系統調用, 必須先把數據從磁盤拷貝至到內核緩衝區中(頁緩衝),然後再把數據拷貝至用戶進程中。兩者相比,mmap會少一次拷貝數據,這樣帶來的性能提升是巨大的。

使用內存訪問來取代read()和write()系統調用能夠簡化一些應用程序的邏輯。
在一些情況下,它能夠比使用傳統的I/O系統調用執行文件I/O這種做法提供更好的性能。
原因是:

  1. 正常的read()或write()需要兩次傳輸:一次是在文件和內核高速緩衝區之間,另一次是在高速緩衝區和用戶空間緩衝區之間。使用mmap()就不需要第二次傳輸了。對於輸入來講,一旦內核將相應的文件塊映射進內存之後,用戶進程就能夠使用這些數據了;對於輸出來講,用戶進程僅僅需要修改內核中的內容,然後可以依靠內核內存管理器來自動更新底層的文件。
  2. 除了節省內核空間和用戶空間之間的一次傳輸之外,mmap()還能夠通過減少所需使用的內存來提升性能。當使用read()或write()時,數據將被保存在兩個緩衝區中:一個位於用戶空間,另個一位於內核空間。當使用mmap()時,內核空間和用戶空間會共享同一個緩衝區。此外,如果多個進程正在同一個文件上執行I/O,那麼它們通過使用mmap()就能夠共享同一個內核緩衝區,從而又能夠節省內存的消耗。

如何使用mmap()

 #include <sys/mman.h>

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

Arguments Describes (參數描述)

  • 參數addr指定文件應被映射到進程空間的起始地址,一般被指定一個空指針,此時選擇起始地址的任務留給內核來完成。
  • len是映射到調用進程地址空間的字節數,它從被映射文件開頭offset個字節開始算起。
  • prot 參數指定共享內存的訪問權限。可取如下幾個值的或:PROT_READ(可讀) , PROT_WRITE (可寫), PROT_EXEC (可執行), PROT_NONE(不可訪問)。
  • flags由以下幾個常值指定:MAP_SHARED , MAP_PRIVATE , MAP_FIXED,其中,MAP_SHARED , MAP_PRIVATE必選其一,而MAP_FIXED則不推薦使用。offset參數一般設爲0,表示從文件頭開始映射。
  • 參數fd爲即將映射到進程空間的文件描述字,一般由open()返回,同時,fd可以指定爲-1,此時須指定flags參數中的MAP_ANON,表明進行的是匿名映射(不涉及具體的文件名,避免了文件的創建及打開,很顯然只能用於具有親緣關係的進程間通信)。
  • offset參數一般設爲0,表示從文件頭開始映射, 代表偏移量。

Return Value (返回值)

函數的返回值爲最後文件映射到進程空間的地址,進程可直接操作起始地址爲該值的有效地址。


兩種映射方式


1. 基於文件的映射:

適用於任何進程之間, 此時,需要打開或創建一個文件,然後再調用mmap(), 典型調用代碼如下:

...
fd = open (name, flag, mode);
if(fd<0)
{
	printf("error!\n");
}
        
/* 這塊內存可讀可寫可執行 */
ptr = mmap(NULL, len , PROT_READ|PROT_WRITE|PROT_EXEC, MAP_SHARED , fd , 0); 

這樣用戶進程就可以像讀取內存一樣讀取文件了,效率非常高。


2. 匿名映射

匿名映射是一種沒用對應文件的一種映射,是使用特殊文件提供的匿名內存映射:
一個匿名映射沒有對應的文件,這種映射的分頁會被初始化爲0。可以把它看成是一個內容總是被初始化爲0的虛擬文件映射,比如在具有血緣關係的進程之間,如父子進程之間, 當一個進程調用mmap().之後又調用了fork(), 之後子進程會繼承(拷貝)父進程映射後的空間,同時也繼承了mmap()的返回地址,通過修改數據共享內存裏的數據, 父子進程夠可以感知到數據的變化,這樣一來,父子進程就可以通過這塊共享內存來實現進程間通信。


/* 例如一些網絡套接字進行共享*/
ptr = mmap(NULL, len , PROT_READ|PROT_WRITE, MAP_SHARED , fd , 0); 

pid = fork();
switch (pid)
{
	case pid < 0:
		printf ("err\n");
	case pid = 0:
     	/* 使用互斥的方式訪問共享內存 */
	 	lock(ptr)
	 	修改數據;
	 	unlock(ptr);
	case pid > 0:
	 	/* 使用互斥的方式訪問共享內存 */
	 	lock(ptr)
	 	修改數據;
	 	unlock(ptr);
}


mmap 具體原理

/* 摘自網絡 有修改*/

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(不同於用戶空間函數),實現文件物理地址和進程虛擬地址的一一映射關係

  1. 爲映射分配了新的虛擬地址區域後,通過待映射的文件指針,在文件描述符表中找到對應的文件描述符,通過文件描述符,鏈接到內核“已打開文件集”中該文件的文件結構體(struct file),每個文件結構體維護着和這個已打開文件相關各項信息。

  2. 通過該文件的文件結構體,鏈接到file_operations模塊,調用內核函數mmap,其原型爲:int mmap(struct file *filp, struct vm_area_struct *vma),不同於用戶空間庫函數。

  3. 內核mmap函數通過虛擬文件系統inode模塊定位到文件磁盤物理地址。

  4. 通過remap_pfn_range函數建立頁表,即實現了文件地址和虛擬地址區域的映射關係。此時,這片虛擬地址並沒有任何數據關聯到主存中。


(三)進程發起對這片映射空間的訪問,引發缺頁異常,實現文件內容到物理內存(主存)的拷貝

注:前兩個階段僅在於創建虛擬區間並完成地址映射,但是並沒有將任何文件數據的拷貝至主存。真正的文件讀取是當進程發起讀或寫操作時。

  1. 進程的讀或寫操作訪問虛擬地址空間這一段映射地址,通過查詢頁表,發現這一段地址並不在物理頁面上。因爲目前只建立了地址映射,真正的硬盤數據還沒有拷貝到內存中,因此引發缺頁異常。

  2. 缺頁異常進行一系列判斷,確定無非法操作後,內核發起請求調頁過程。

  3. 調頁過程先在交換緩存空間(swap cache)中尋找需要訪問的內存頁,如果沒有則調用nopage函數把所缺的頁從磁盤裝入到主存中。

  4. 之後進程即可對這片主存進行讀或者寫的操作,如果寫操作改變了其內容,一定時間後系統會自動回寫髒頁面到對應磁盤地址,也即完成了寫入到文件的過程(是不是有點像寫時複製技術呢,哦,這篇博客拖了好久了)。

注:修改過的髒頁面並不會立即更新迴文件中,而是有一段時間的延遲,可以調用msync()來強制同步, 這樣所寫的內容就能立即保存到文件裏了。


mmap()優缺點總結

mmap()的優點

  1. 對文件的讀取操作跨過了頁緩存,減少了數據的拷貝次數,用內存讀寫取代I/O讀寫,提高了文件讀取效率。

  2. 實現了用戶空間和內核空間的高效交互方式。兩空間的各自修改操作可以直接反映在映射的區域內,從而被對方空間及時捕捉。
    mmap映射的頁和其它的頁並沒有本質的不同.
    所以得益於主要的3種數據結構的高效,其頁映射過程也很高效:
    (1) radix tree,用於查找某頁是否已在緩存.
    (2) red black tree ,用於查找和更新vma結構.
    (3) 雙向鏈表,用於維護active和inactive鏈表,支持LRU類算法進行內存回收.

  3. 提供進程間共享內存及相互通信的方式。不管是父子進程還是無親緣關係的進程,都可以將自身用戶空間映射到同一個文件或匿名映射到同一片區域。從而通過各自對映射區域的改動,達到進程間通信和進程間共享的目的。同時,如果進程A和進程B都映射了區域C,當A第一次讀取C時通過缺頁從磁盤複製文件頁到內存中;但當B再讀C的相同頁面時,雖然也會產生缺頁異常,但是不再需要從磁盤中複製文件過來,而可直接使用已經保存在內存中的文件數據。

  4. 可用於實現高效的大規模數據傳輸。內存空間不足,是制約大數據操作的一個方面,解決方案往往是藉助硬盤空間協助操作,補充內存的不足。但是進一步會造成大量的文件I/O操作,極大影響效率。這個問題可以通過mmap映射很好的解決。換句話說,但凡是需要用磁盤空間代替內存的時候,mmap都可以發揮其功效。

mmap()的缺點

  1. 對變長文件不適合.
  2. 如果更新文件的操作很多,mmap避免兩態拷貝的優勢就被攤還,最終還是落在了大量的髒頁回寫及由此引發的隨機IO上. 所以在隨機寫很多的情況下,mmap方式在效率上不一定會比帶緩衝區的一般寫快.

參考內容

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

共享內存實現原理

linux內核空間與用戶空間信息交互方法

Linux IPC之內存映射mmap()

周明德,保護方式下的80386及其編程,清華大學出版社,1993

感謝

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章