1. 程式人生 > 其它 >認真分析mmap:是什麼 為什麼 怎麼用【轉】

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

mmap基礎概念

mmap是一種記憶體對映檔案的方法,即將一個檔案或者其它物件對映到程序的地址空間,實現檔案磁碟地址和程序虛擬地址空間中一段虛擬地址的一一對映關係。實現這樣的對映關係後,程序就可以採用指標的方式讀寫操作這一段記憶體,而系統會自動回寫髒頁面到對應的檔案磁碟上,即完成了對檔案的操作而不必再呼叫read,write等系統呼叫函式。相反,核心空間對這段區域的修改也直接反映使用者空間,從而可以實現不同程序間的檔案共享。如下圖所示:

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

linux核心使用vm_area_struct結構來表示一個獨立的虛擬記憶體區域,由於每個不同質的虛擬記憶體區域功能和內部機制都不同,因此一個程序使用多個vm_area_struct結構來分別表示不同型別的虛擬記憶體區域。各個vm_area_struct結構使用連結串列或者樹形結構連結,方便程序快速訪問,如下圖所示:

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和常規檔案操作的區別

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

  1. 程序發起讀檔案請求。
  2. 核心通過查詢程序檔案符表,定位到核心已開啟檔案集上的檔案資訊,從而找到此檔案的inode。
  3. inode在address_space上查詢要請求的檔案頁是否已經快取在頁快取中。如果存在,則直接返回這片檔案頁的內容。
  4. 如果不存在,則通過inode定位到檔案磁碟地址,將資料從磁碟複製到頁快取。之後再次發起讀頁面過程,進而將頁快取中的資料發給使用者程序。

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

而使用mmap操作檔案中,建立新的虛擬記憶體區域和建立檔案磁碟地址和虛擬記憶體區域對映這兩步,沒有任何檔案拷貝操作。而之後訪問資料時發現記憶體中並無資料而發起的缺頁異常過程,可以通過已經建立好的對映關係,只使用一次資料拷貝,就從磁碟中將資料傳入記憶體的使用者空間中,供程序使用。

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

mmap優點總結

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

  1. 對檔案的讀取操作跨過了頁快取,減少了資料的拷貝次數,用記憶體讀寫取代I/O讀寫,提高了檔案讀取效率。
  2. 實現了使用者空間和核心空間的高效互動方式。兩空間的各自修改操作可以直接反映在對映的區域內,從而被對方空間及時捕捉。
  3. 提供程序間共享記憶體及相互通訊的方式。不管是父子程序還是無親緣關係的程序,都可以將自身使用者空間對映到同一個檔案或匿名對映到同一片區域。從而通過各自對對映區域的改動,達到程序間通訊和程序間共享的目的。

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

  1. 可用於實現高效的大規模資料傳輸。記憶體空間不足,是制約大資料操作的一個方面,解決方案往往是藉助硬碟空間協助操作,補充記憶體的不足。但是進一步會造成大量的檔案I/O操作,極大影響效率。這個問題可以通過mmap對映很好的解決。換句話說,但凡是需要用磁碟空間代替記憶體的時候,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被設為以下的某個值:

EACCES:訪問出錯
EAGAIN:檔案已被鎖定,或者太多的記憶體已被鎖定
EBADF:fd不是有效的檔案描述詞
EINVAL:一個或者多個引數無效
ENFILE:已達到系統對開啟檔案的限制
ENODEV:指定檔案所在的檔案系統不支援記憶體對映
ENOMEM:記憶體不足,或者程序已超出最大記憶體對映數量
EPERM:權能不足,操作不允許
ETXTBSY:已寫的方式開啟檔案,同時指定MAP_DENYWRITE標誌
SIGSEGV:試著向只讀區寫入
SIGBUS:試著訪問不屬於程序的記憶體區

返回錯誤型別

引數

start:對映區的開始地址

length:對映區的長度

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

1 PROT_EXEC :頁內容可以被執行
2 PROT_READ :頁內容可以被讀取
3 PROT_WRITE :頁可以被寫入
4 PROT_NONE :頁不可訪問

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

MAP_FIXED //使用指定的對映起始地址,如果由start和len引數指定的記憶體區重疊於現存的對映空間,重疊部分將會被丟棄。如果指定的起始地址不可用,操作將會失敗。並且起始地址必須落在頁的邊界上。
MAP_SHARED //與其它所有對映這個物件的程序共享對映空間。對共享區的寫入,相當於輸出到檔案。直到msync()或者munmap()被呼叫,檔案實際上不會被更新。
MAP_PRIVATE //建立一個寫入時拷貝的私有對映。記憶體區域的寫入不會影響到原檔案。這個標誌和以上標誌是互斥的,只能使用其中一個。
MAP_DENYWRITE //這個標誌被忽略。
MAP_EXECUTABLE //同上
MAP_NORESERVE //不要為這個對映保留交換空間。當交換空間被保留,對對映區修改的可能會得到保證。當交換空間不被保留,同時記憶體不足,對對映區的修改會引起段違例訊號。
MAP_LOCKED //鎖定對映區的頁面,從而防止頁面被交換出記憶體。
MAP_GROWSDOWN //用於堆疊,告訴核心VM系統,對映區可以向下擴充套件。
MAP_ANONYMOUS //匿名對映,對映區不與任何檔案關聯。
MAP_ANON //MAP_ANONYMOUS的別稱,不再被使用。
MAP_FILE //相容標誌,被忽略。
MAP_32BIT //將對映區放在程序地址空間的低2GB,MAP_FIXED指定時會被忽略。當前這個標誌只在x86-64平臺上得到支援。
MAP_POPULATE //為檔案對映通過預讀的方式準備好頁表。隨後對對映區的訪問不會被頁違例阻塞。
MAP_NONBLOCK //僅和MAP_POPULATE一起使用時才有意義。不執行預讀,只為已存在於記憶體中的頁面建立頁表入口。

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()實現磁碟上檔案內容與共享記憶體區的內容一致。

mmap使用細節

  1. 使用mmap需要注意的一個關鍵點是,mmap對映區域大小必須是物理頁大小(page_size)的整倍數(32位系統中通常是4k位元組)。原因是,記憶體的最小粒度是頁,而程序虛擬地址空間和記憶體的對映也是以頁為單位。為了匹配記憶體的操作,mmap從磁碟到虛擬地址空間的對映也必須是頁。
  2. 核心可以跟蹤被記憶體對映的底層物件(檔案)的大小,程序可以合法的訪問在當前檔案大小以內又在記憶體對映區以內的那些位元組。也就是說,如果檔案的大小一直在擴張,只要在對映區域範圍內的資料,程序都可以合法得到,這和對映建立時檔案的大小無關。具體情形參見“情形三”。
  3. 對映建立之後,即使檔案關閉,對映依然存在。因為對映的是磁碟的地址,不是檔案本身,和檔案控制代碼無關。同時可用於程序間通訊的有效地址空間不完全受限於被對映檔案的大小,因為是按頁對映。

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

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

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

此時:

  1. 讀/寫前5000個位元組(0~4999),會返回操作檔案內容。
  2. 讀位元組5000~8191時,結果全為0。寫5000~8191時,程序不會報錯,但是所寫的內容不會寫入原檔案中 .
  3. 讀/寫8192以外的磁碟部分,會返回一個SIGSECV錯誤。

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

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

此時:

  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都可以對應操作相同的大小。

這樣,方便隨時擴充檔案空間,隨時寫入檔案,不造成空間浪費