2011年12月20日 星期二

Linux環境進程間通信(五): 共用記憶體(下)


系統調用mmap()通過映射一個普通檔實現共用記憶體。系統V則是通過映射特殊檔案系統shm中的檔實現進程間的共用記憶體通信。也就是說,每個共用記憶體區域對應特殊檔案系統shm中的一個檔(這是通過shmid_kernel結構聯繫起來的),後面還將闡述。

1、系統V共用記憶體原理

進程間需要共用的資料被放在一個叫做IPC共用記憶體區域的地方,所有需要訪問該共用區域的進程都要把該共用區域映射到本進程的位址空間中去。系統V共用記憶體通過shmget獲得或創建一個IPC共用記憶體區域,並返回相應的識別字。內核在保證shmget獲得或創建一個共用記憶體區,初始化該共用記憶體區相應的shmid_kernel結構注同時,還將在特殊檔案系統shm中,創建並打開一個同名檔,並在記憶體中建立起該檔的相應dentryinode結構,新打開的檔不屬於任何一個進程(任何進程都可以訪問該共用記憶體區)。所有這一切都是系統調用shmget完成的。

注:每一個共用記憶體區都有一個控制結構struct shmid_kernelshmid_kernel是共用記憶體區域中非常重要的一個資料結構,它是存儲管理和檔案系統結合起來的橋樑,定義如下:

struct shmid_kernel /* private to the kernel */
{      
        struct kern_ipc_perm   shm_perm;
        struct file *          shm_file;
        int                    id;
        unsigned long          shm_nattch;
        unsigned long          shm_segsz;
        time_t                 shm_atim;
        time_t                 shm_dtim;
        time_t                 shm_ctim;
        pid_t                  shm_cprid;
        pid_t                  shm_lprid;
};



該結構中最重要的一個域應該是shm_file,它存儲了將被映射檔的位址。每個共用記憶體區物件都對應特殊檔案系統shm中的一個檔,一般情況下,特殊檔案系統shm中的檔是不能用read()write()等方法訪問的,當採取共用記憶體的方式把其中的檔映射到進程位址空間後,可直接採用訪問記憶體的方式對其訪問。

這裡我們採用[1]中的圖表給出與系統V共用記憶體相關資料結構:


正如訊息佇列和信號燈一樣,內核通過資料結構struct ipc_ids shm_ids維護系統中的所有共用記憶體區域。上圖中的shm_ids.entries變數指向一個ipc_id結構陣列,而每個ipc_id結構陣列中有個指向kern_ipc_perm結構的指標。到這裡讀者應該很熟悉了,對於系統V共用記憶體區來說,kern_ipc_perm的宿主是shmid_kernel結構,shmid_kernel是用來描述一個共用記憶體區域的,這樣內核就能夠控制系統中所有的共用區域。同時,在shmid_kernel結構的file類型指標shm_file指向檔案系統shm中相應的檔,這樣,共用記憶體區域就與shm檔案系統中的檔對應起來。

在創建了一個共用記憶體區域後,還要將它映射到進程位址空間,系統調用shmat()完成此項功能。由於在調用shmget()時,已經創建了檔案系統shm中的一個同名檔與共用記憶體區域相對應,因此,調用shmat()的過程相當於映射檔案系統shm中的同名檔過程,原理與mmap()大同小異。


2、系統V共用記憶體API

對於系統V共用記憶體,主要有以下幾個APIshmget()shmat()shmdt()shmctl()

#include <sys/ipc.h>
#include <sys/shm.h>



shmget()用來獲得共用記憶體區域的ID,如果不存在指定的共用區域就創建相應的區域。shmat()把共用記憶體區域映射到調用進程的位址空間中去,這樣,進程就可以方便地對共用區域進行訪問操作。shmdt()調用用來解除進程對共用記憶體區域的映射。shmctl實現對共用記憶體區域的控制操作。這裡我們不對這些系統調用作具體的介紹,讀者可參考相應的手冊頁面,後面的範例中將給出它們的調用方法。

注:shmget的內部實現包含了許多重要的系統V共用記憶體機制;shmat在把共用記憶體區域映射到進程空間時,並不真正改變進程的頁表。當進程第一次訪問記憶體映射區域訪問時,會因為沒有物理頁表的分配而導致一個缺頁異常,然後內核再根據相應的存儲管理機制為共用記憶體映射區域分配相應的頁表。


3、系統V共用記憶體限制

/proc/sys/kernel/目錄下,記錄著系統V共用記憶體的一下限制,如一個共用記憶體區的最大位元組數shmmax,系統範圍內最大共用記憶體區識別字數shmmni等,可以手工對其調整,但不推薦這樣做。

[2]中,給出了這些限制的測試方法,不再贅述。


4、系統V共用記憶體範例

本部分將給出系統V共用記憶體API的使用方法,並對比分析系統V共用記憶體機制與mmap()映射普通檔實現共用記憶體之間的差異,首先給出兩個進程通過系統V共用記憶體通信的範例:

/***** testwrite.c *******/
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <unistd.h>
typedef struct{
        char name[4];
        int age;
} people;
main(int argc, char** argv)
{
        int shm_id,i;
        key_t key;
        char temp;
        people *p_map;
        char* name = "/dev/shm/myshm2";
        key = ftok(name,0);
        if(key==-1)
               perror("ftok error");
        shm_id=shmget(key,4096,IPC_CREAT);   
        if(shm_id==-1)
        {
               perror("shmget error");
               return;
        }
        p_map=(people*)shmat(shm_id,NULL,0);
        temp='a';
        for(i = 0;i<10;i++)
        {
               temp+=1;
               memcpy((*(p_map+i)).name,&temp,1);
               (*(p_map+i)).age=20+i;
        }
        if(shmdt(p_map)==-1)
               perror(" detach error ");
}
/********** testread.c ************/
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <unistd.h>
typedef struct{
        char name[4];
        int age;
} people;
main(int argc, char** argv)
{
        int shm_id,i;
        key_t key;
        people *p_map;
        char* name = "/dev/shm/myshm2";
        key = ftok(name,0);
        if(key == -1)
               perror("ftok error");
        shm_id = shmget(key,4096,IPC_CREAT); 
        if(shm_id == -1)
        {
               perror("shmget error");
               return;
        }
        p_map = (people*)shmat(shm_id,NULL,0);
        for(i = 0;i<10;i++)
        {
        printf( "name:%s\n",(*(p_map+i)).name );
        printf( "age %d\n",(*(p_map+i)).age );
        }
        if(shmdt(p_map) == -1)
               perror(" detach error ");
}



testwrite.c創建一個系統V共用記憶體區,並在其中寫入格式化資料;testread.c訪問同一個系統V共用記憶體區,讀出其中的格式化資料。分別把兩個程式編譯為testwritetestread,先後執行./testwrite./testread ./testread輸出結果如下:

name: b age 20; name: c age 21; name: d age 22; name: e age 23; name: f age 24;
name: g age 25; name: h age 26; name: I age 27; name: j age 28; name: k age 29;



通過對試驗結果分析,對比系統Vmmap()映射普通檔實現共用記憶體通信,可以得出如下結論:

1、 系統V共用記憶體中的資料,從來不寫入到實際磁片檔中去;而通過mmap()映射普通檔實現的共用記憶體通信可以指定何時將資料寫入磁片檔中。注:前面講到,系統V共用記憶體機制實際是通過映射特殊檔案系統shm中的檔實現的,檔案系統shm的安裝點在交換分區上,系統重新引導後,所有的內容都丟失。

2、 系統V共用記憶體是隨內核持續的,即使所有訪問共用記憶體的進程都已經正常終止,共用記憶體區仍然存在(除非顯式刪除共用記憶體),在內核重新引導之前,對該共用記憶體區域的任何改寫操作都將一直保留。

3、 通過調用mmap()映射普通檔進行進程間通信時,一定要注意考慮進程何時終止對通信的影響。而通過系統V共用記憶體實現通信的進程則不然。注:這裡沒有給出shmctl的使用範例,原理與訊息佇列大同小異。


結論:

共用記憶體允許兩個或多個進程共用一給定的存儲區,因為資料不需要來回複製,所以是最快的一種進程間通信機制。共用記憶體可以通過mmap()映射普通檔(特殊情況下還可以採用匿名映射)機制實現,也可以通過系統V共用記憶體機制實現。應用介面和原理很簡單,內部機制複雜。為了實現更安全通信,往往還與信號燈等同步機制共同使用。

共用記憶體涉及到了存儲管理以及檔案系統等方面的知識,深入理解其內部機制有一定的難度,關鍵還要緊緊抓住內核使用的重要資料結構。系統V共用記憶體是以檔的形式組織在特殊檔案系統shm中的。通過shmget可以創建或獲得共用記憶體的識別字。取得共用記憶體識別字後,要通過shmat將這個記憶體區映射到本進程的虛擬位址空間。


沒有留言: