記憶體對映

weixin_34120274發表於2017-10-05

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

閱讀目錄
mmap基礎概念
mmap記憶體對映原理
mmap和常規檔案操作的區別
mmap優點總結
mmap相關函式
mmap使用細節

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

6925065-5602aabb59c6cfdb.png

由上圖可以看出,程式的虛擬地址空間,由多個虛擬記憶體區域構成。虛擬記憶體區域是程式的虛擬地址空間中的一個同質區間,即具有同樣特性的連續地址範圍。上圖中所示的text資料段(程式碼段)、初始資料段、BSS資料段、堆、棧和記憶體對映,都是一個獨立的虛擬記憶體區域。而為記憶體對映服務的地址空間處在堆疊之間的空餘部分。
linux核心使用vm_area_struct結構來表示一個獨立的虛擬記憶體區域,由於每個不同質的虛擬記憶體區域功能和內部機制都不同,因此一個程式使用多個vm_area_struct結構來分別表示不同型別的虛擬記憶體區域。各個vm_area_struct結構使用連結串列或者樹形結構連結,方便程式快速訪問,如下圖所示:
6925065-c38ff5390ad15b6b.png

vm_area_struct結構中包含區域起始和終止地址以及其他相關資訊,同時也包含一個vm_ops指標,其內部可引出所有針對這個區域可以使用的系統呼叫函式。這樣,程式對某一虛擬記憶體區域的任何操作需要用要的資訊,都可以從vm_area_struct中獲得。mmap函式就是要建立一個新的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(不同於使用者空間函式),實現檔案實體地址和程式虛擬地址的一一對映關係
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()來強制同步, 這樣所寫的內容就能立即儲存到檔案裡了。

回到頂部
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的相同頁面時,雖然也會產生缺頁異常,但是不再需要從磁碟中複製檔案過來,而可直接使用已經儲存在記憶體中的檔案資料。
4、可用於實現高效的大規模資料傳輸。記憶體空間不足,是制約大資料操作的一個方面,解決方案往往是藉助硬碟空間協助操作,補充記憶體的不足。但是進一步會造成大量的檔案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被設為以下的某個值:

6925065-964ce5be6219b128.gif
返回錯誤型別
引數
start:對映區的開始地址
length:對映區的長度
prot:期望的記憶體保護標誌,不能與檔案的開啟模式衝突。是以下的某個值,可以通過or運算合理地組合在一起
6925065-964ce5be6219b128.gif
prot
flags:指定對映物件的型別,對映選項和對映頁是否可以共享。它的值可以是一個或者多個以下位的組合體
6925065-964ce5be6219b128.gif
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的位元組部分用零填充。對映後的對應關係如下圖所示:

6925065-a21395feb4a1917a.png

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

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

6925065-66afce3d15c09463.png

此時:
(1)程式可以正常讀/寫被對映的前5000位元組(0~4999),寫操作的改動會在一定時間後反映在原檔案中。
(2)對於5000~8191位元組,程式可以進行讀寫過程,不會報錯。但是內容在寫入前均為0,另外,寫入後不會反映在檔案中。
(3)對於8192~14999位元組,程式不能對其進行讀寫,會報SIGBUS錯誤。
(4)對於15000以外的位元組,程式不能對其讀寫,會引發SIGSEGV錯誤。

情形三:一個檔案初始大小為0,使用mmap操作對映了10004K的大小,即1000個物理頁大約4M位元組空間,mmap返回指標ptr。*
分析:如果在對映建立之初,就對檔案進行讀寫操作,由於檔案大小為0,並沒有合法的物理頁對應,如同情形二一樣,會返回SIGBUS錯誤。
但是如果,每次操作ptr讀寫前,先增加檔案的大小,那麼ptr在檔案大小內部的操作就是合法的。例如,檔案擴充4096位元組,ptr就能操作ptr ~ [ (char)ptr + 4095]的空間。只要檔案擴充的範圍在1000個物理頁(對映範圍)內,ptr都可以對應操作相同的大小。
這樣,方便隨時擴充檔案空間,隨時寫入檔案,不造成空間浪費。

相關文章