天天看點

Linux程序間通信--mmap()共享記憶體(二)

核心怎樣保證各個程序尋址到同一個共享記憶體區域的記憶體頁面

1、page cache及swap cache中頁面的區分:一個被通路檔案的實體頁面都駐留在page cache或swap cache中,一個頁面的所有資訊由struct page來描述。struct page中有一個域為指針mapping ,它指向一個struct address_space類型結構。page cache或swap cache中的所有頁面就是根據address_space結構以及一個偏移量來區分的。

2、檔案與address_space結構的對應:一個具體的檔案在打開後,核心會在記憶體中為之建立一個struct inode結構,其中的i_mapping域指向一個address_space結構。這樣,一個檔案就對應一個address_space結構,一個address_space與一個偏移量能夠确定一個page cache 或swap cache中的一個頁面。是以,當要尋址某個資料時,很容易根據給定的檔案及資料在檔案内的偏移量而找到相應的頁面。

3、程序調用mmap()時,隻是在程序空間内新增了一塊相應大小的緩沖區,并設定了相應的通路辨別,但并沒有建立程序空間到實體頁面的映射。是以,第一次通路該空間時,會引發一個缺頁異常。

4、對于共享記憶體映射情況,缺頁異常處理程式首先在swap cache中尋找目标頁(符合address_space以及偏移量的實體頁),如果找到,則直接傳回位址;如果沒有找到,則判斷該頁是否在交換區(swap area),如果在,則執行一個換入操作;如果上述兩種情況都不滿足,處理程式将配置設定新的實體頁面,并把它插入到page cache中。程序最終将更新程序頁表。

注:對于映射普通檔案情況(非共享映射),缺頁異常處理程式首先會在page cache中根據address_space以及資料偏移量尋找相應的頁面。如果沒有找到,則說明檔案資料還沒有讀入記憶體,處理程式會從磁盤讀入相應的頁面,并傳回相應位址,同時,程序頁表也會更新。

5、所有程序在映射同一個共享記憶體區域時,情況都一樣,在建立線性位址與實體位址之間的映射之後,不論程序各自的傳回位址如何,實際通路的必然是同一個共享記憶體區域對應的實體頁面。

注:一個共享記憶體區域可以看作是特殊檔案系統shm中的一個檔案,shm的安裝點在交換區上。

                 mmap()系統調用使得程序之間通過映射同一個普通檔案實作共享記憶體。普通檔案被映射到程序位址空間後,程序可以向通路普通記憶體一樣對檔案進行通路,不必再調用read(),write()等操作。

注:實際上,mmap()系統調用并不是完全為了用于共享記憶體而設計的。它本身提供了不同于一般對普通檔案的通路方式,程序可以像讀寫記憶體一樣對普通檔案的操作。而Posix或系統V的共享記憶體IPC則純粹用于共享目的,當然mmap()實作共享記憶體也是其主要應用之一。

  (PS;我們再來粗略的分析一下核心如何保證各個程序尋址到同一個共享記憶體區域的頁面的。首先我們要知道對于記憶體中的也,作業系統也是有相應的緩沖的,也就是swap cache和page cache,而且我們使用mmap()系統調用時,隻是簡單的把虛拟位址空間的某塊區域分出來,傳回給使用者使用,并沒有将磁盤檔案映射到實體記憶體中,隻有在使用mmap傳回的虛拟位址空間的指針的時候才會有缺頁中斷來完成這個事情,而且在中斷處理的過程總首先看swap cache和page cache有沒有這個檔案。這個時候是如何判斷的呢?因為對于每一個檔案作業系統都有一個Inode與之對應,如果這個inode中的mmaping和偏移量已經決定了這個頁面的資訊。如果在caceh找到,那麼就放到實體頁面,如果沒有找到,就從磁盤中讀取,然後再放回到實體記憶體中)        

系統調用mmap()用于共享記憶體的兩種方式:

(1)使用普通檔案提供的記憶體映射:适用于任何程序之間;此時,需要打開或建立一個檔案,然後再調用mmap();典型調用代碼如下:

fd=open(name, flag, mode);
if(fd<0)
	...      

ptr=mmap(NULL, len , PROT_READ|PROT_WRITE, MAP_SHARED , fd , 0);通過mmap()實作共享記憶體的通信方式有許多特點和要注意的地方,我們将在範例中進行具體說明。

(2)使用特殊檔案提供匿名記憶體映射:适用于具有親緣關系的程序之間;由于父子程序特殊的親緣關系,在父程序中先調用mmap(),然後調用fork()。那麼在調用fork()之後,子程序繼承父程序匿名映射後的位址空間,同樣也繼承mmap()傳回的位址,這樣,父子程序就可以通過映射區域進行通信了。注意,這裡不是一般的繼承關系。一般來說,子程序單獨維護從父程序繼承下來的一些變量。而mmap()傳回的位址,卻由父子程序共同維護。

對于具有親緣關系的程序實作共享記憶體最好的方式應該是采用匿名記憶體映射的方式。此時,不必指定具體的檔案,隻要設定相應的标志即可

說起共享記憶體,一般來說會讓人想起下面一些方法:

1、多線程。線程之間的記憶體都是共享的。更确切的說,屬于同一程序的線程使用的是同一個位址空間,而不是在不同位址空間之間進行記憶體共享;

2、父子程序間的記憶體共享。父程序以MAP_SHARED|MAP_ANONYMOUS選項mmap一塊匿名記憶體,fork之後,其子孫程序之間就能共享這塊記憶體。這種共享記憶體由于受到程序父子關系的限制,一般較少使用;

3、mmap檔案。多個程序mmap到同一個檔案,實際上就是大家在共享檔案page cache中的記憶體。不過檔案牽涉到磁盤的讀寫,用來做共享記憶體顯然十分笨重,是以就有了不跟磁盤扯上關系的記憶體檔案,也就是我們這裡要讨論的tmpfs和shmem;

tmpfs是一套虛拟的檔案系統,在其中建立的檔案都是基于記憶體的,機器重新開機即消失。

shmem是一套ipc,通過相應的ipc系統調用shmget能夠以指定key建立一塊的共享記憶體。需要使用這塊記憶體的程序可以通過shmat系統調用來獲得它。

雖然是兩套不同的接口,但是在核心裡面的實作卻是同一套。shmem内部挂載了一個tmpfs分區(使用者不可見),shmget就是在該分區下擷取名為"SYSV${key}"的檔案。然後shmat就相當于mmap這個檔案。

是以我們接下來就把tmpfs和shmem當作同一個東西來讨論了。

tmpfs/shmem是一個介于檔案和匿名記憶體之間的東西。

一方面,它具有檔案的屬性,能夠像操作檔案一樣去操作它。它有自己inode、有自己的page cache;

另一方面,它也有匿名記憶體的屬性。由于沒有像磁盤這樣的外部存儲媒體,核心在記憶體緊缺時不能簡單的将page從它們的page cache中丢棄,而需要swap-out;(參閱《linux頁面回收淺析》)

對tmpfs/shmem記憶體的讀寫,就是對page cache中相應位置的page所代表的記憶體進行讀寫,這一點跟普通的檔案映射沒有什麼不同。

如果程序位址空間的相應位置尚未映射,則會建立到page cache中相應page的映射;

如果page cache中的相應位置還沒有配置設定page,則會配置設定一個。當然,由于不存在磁盤上的源資料,新配置設定的page總是空的(特别的,通過read系統調用去讀一個尚未配置設定page的位置時,并不會配置設定新的page,而是共享ZERO_PAGE);

如果page cache中相應位置的page被回收了,則會先将其恢複;

對于第三個“如果”,tmpfs/shmem和普通檔案的page回收及其恢複方式是不同的:

page回收時,跟普通檔案的情況一樣,核心會通過prio_tree反向映射找到映射這個page的每一個page table,然後将其中對應的pte清空。

不同之處是普通檔案的page在確定與磁盤同步(如果page為髒的話需要刷回磁盤)之後就可以丢棄了,而對于tmpfs/shmem的page則需要進行swap-out。

注意,匿名page在被swap-out時,并不是将映射它的pte清空,而是得在pte上填寫相應的swap_entry,以便知道page被換出到哪裡去,否則再需要這個page的時候就沒法swap-in了。

而tmpfs/shmem的page呢?page table中對應的pte被清空,swap_entry會被存放在page cache的radix_tree的對應slot上。

等下一次通路觸發page fault時,page需要恢複。

普通檔案的page恢複跟page未配置設定時的情形一樣,需要新配置設定page、然後根據映射的位置重新從磁盤讀出相應的資料;

而tmpfs/shmem則是通過映射的位置找到radix_tree上對應的slot,從中得到swap_entry,進而進行swap-in,并将新的page放回page cache;

這裡就有個問題了,在page cache的radix_tree的某個slot上,怎麼知道裡面存放着的是正常的page?還是swap-out後留下的swap_entry?

如果是swap_entry,那麼slot上的值将被加上RADIX_TREE_EXCEPTIONAL_ENTRY标記(值為2)。swap_entry的值被左移兩位後OR上RADIX_TREE_EXCEPTIONAL_ENTRY,填入slot。

也就是說,如果${slot} & RADIX_TREE_EXCEPTIONAL_ENTRY != 0,則它代表swap_entry,且swap_entry的值是${slot} >> 2;否則它代表page,${slot}就是指向page的指針,當然其值可能是NULL,說明page尚未配置設定。

那麼顯然,page的位址值其末兩位肯定是0,否則就可能跟RADIX_TREE_EXCEPTIONAL_ENTRY标記沖突了;而swap_entry的值最大隻能是30bit或62bit(對應32位或64位機器),否則左移兩位就溢出了。

最後以一張圖說明一下匿名page、檔案映射page、tmpfs/shmem page的回收及恢複過程:

Linux程式間通信--mmap()共享記憶體(二)

繼續閱讀