linux共享內存機制(三)

mmap基礎概念

mmap是一種內存映射文件的方法,即將一個文件或者其它對象映射到進程的地址空間,實現文件磁盤地址和進程虛擬地址空間中一段虛擬地址的一一對映關係。實現這樣的映射關係後,進程就可以採用指針的方式讀寫操作這一段內存,而系統會自動回寫髒頁面到對應的文件磁盤上,即完成了對文件的操作而不必再調用read,write等系統調用函數。相反,內核空間對這段區域的修改也直接反映用戶空間,從而可以實現不同進程間的文件共享。如下圖所示:
在這裏插入圖片描述
由上圖可以看出,進程的虛擬地址空間,由多個虛擬內存區域構成。虛擬內存區域是進程的虛擬地址空間中的一個同質區間,即具有同樣特性的連續地址範圍。上圖中所示的text數據段(代碼段)、初始數據段、BSS數據段、堆、棧和內存映射,都是一個獨立的虛擬內存區域。而爲內存映射服務的地址空間處在堆棧之間的空餘部分。

linux內核使用vm_area_struct結構來表示一個獨立的虛擬內存區域,由於每個不同質的虛擬內存區域功能和內部機制都不同,因此一個進程使用多個vm_area_struct結構來分別表示不同類型的虛擬內存區域。各個vm_area_struct結構使用鏈表或者樹形結構鏈接,方便進程快速訪問,如下圖所示:
在這裏插入圖片描述
vm_area_struct結構中包含區域起始和終止地址以及其他相關信息,同時也包含一個vm_ops指針,其內部可引出所有針對這個區域可以使用的系統調用函數。這樣,進程對某一虛擬內存區域的任何操作需要用要的信息,都可以從vm_area_struct中獲得。mmap函數就是要創建一個新的vm_area_struct結構,並將其與文件的物理磁盤地址相連。具體步驟請看下一節。

mmap內存映射原理

mmap內存映射的實現過程,總的來說可以分爲三個階段:

1、進程啓動映射過程,並在虛擬地址空間中爲映射創建虛擬映射區域

  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)插入進程的虛擬地址區域鏈表或樹中

2、調用內核空間的系統調用函數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函數建立頁表,即實現了文件地址和虛擬地址區域的映射關係。此時,這片虛擬地址並沒有任何數據關聯到主存中。

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

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

  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的相同頁面時,雖然也會產生缺頁異常,但是不再需要從磁盤中複製文件過來,而可直接使用已經保存在內存中的文件數據。

  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被設爲以下的某個值:

1 EACCES:訪問出錯
2 EAGAIN:文件已被鎖定,或者太多的內存已被鎖定
3 EBADF:fd不是有效的文件描述詞
4 EINVAL:一個或者多個參數無效
5 ENFILE:已達到系統對打開文件的限制
6 ENODEV:指定文件所在的文件系統不支持內存映射
7 ENOMEM:內存不足,或者進程已超出最大內存映射數量
8 EPERM:權能不足,操作不允許
9 ETXTBSY:已寫的方式打開文件,同時指定MAP_DENYWRITE標誌
10 SIGSEGV:試着向只讀區寫入
11 SIGBUS:試着訪問不屬於進程的內存區

參數

start:映射區的開始地址

length:映射區的長度

prot:期望的內存保護標誌,不能與文件的打開模式衝突。是以下的某個值,可以通過or運算合理地組合在一起

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

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

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

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

這樣,方便隨時擴充文件空間,隨時寫入文件,不造成空間浪費。

原文鏈接:https://www.cnblogs.com/huxiao-tee/p/4660352.html

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