1. 程式人生 > >Linux環境程序間通訊——共享記憶體

Linux環境程序間通訊——共享記憶體

原文連結

    原文連結:http://www.ibm.com/developerworks/cn/linux/l-ipc/part5/index1.html

概述

    Android系統中大量使用了mmap實現的共享記憶體,所以這裡需要介紹一下LInux程序間通訊機制——共享記憶體。共享記憶體可以說是最有用的程序間通訊方式,也是最快的IPC形式。兩個不同程序A、B共享記憶體的意思是,同一塊實體記憶體被對映到程序A、B各自的程序地址空間。程序A可以即時看到程序B對共享記憶體中資料的更新,反之亦然。由於多個程序共享同一塊記憶體區域,必然需要某種同步機制,互斥鎖和訊號量都可以。     採用共享記憶體通訊的一個顯而易見的好處是效率高,因為程序可以直接讀寫記憶體,而不需要任何資料的拷貝。對於像管道和訊息佇列等通訊方式,則需要在核心和使用者空間進行四次的資料拷貝。而共享記憶體則只需要拷貝兩次資料:[1] 一次從輸入檔案到共享記憶體區 [2] 從共享記憶體區到輸出檔案。實際上,程序之間在共享記憶體時,並不總是讀寫少量資料後就解除對映,有新的通訊時,再重新建立共享記憶體區域。而且保持共享記憶體區域,直到通訊完畢為止。這樣,資料記憶體一直保持在共享記憶體中,並沒有寫回檔案。共享記憶體中的內容往往是在解除對映時才寫回檔案的。因此,採用共享記憶體的通訊方式效率是非常高的。

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

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()及其相關係統呼叫

    mmap()系統呼叫使得程序之間通過對映同一個普通檔案實現共享記憶體。普通檔案被對映到程序地址空間後,程序可以像訪問普通記憶體一樣對檔案進行訪問,不必在呼叫read()、write()等操作。(注:實際上,mmap()系統呼叫並不是完全為了用於共享記憶體而設計的。它本身提供了一種不同於一般對普通檔案的訪問方式,程序可以像讀寫記憶體一樣對普通檔案進行操作。)

1. mmap()系統呼叫形式

    void* mmap(void *addr, size_t len, int prot, int flags, int fd, off_t offset)
  1. 引數fd為即將對映到程序空間的檔案描述符,一般由open()返回,同時,fd可以指定為-1,此時須指定flags引數中的MAP_ANON,表明進行的是匿名對映(不涉及具體的檔名,避免了檔案的建立及開啟,很顯然只能用於具有親緣關係的程序間通訊)。
  2. len對映到呼叫程序地址空間的位元組數,它從被對映檔案開頭offset個位元組開始算起。
  3. prot引數指定共享記憶體的訪問許可權。可取如下幾個值的或:PROT_READ(可讀)、PROT_WRITE(可寫)、PROT_EXEC(可執行)、PROT_NONE(不可訪問)。
  4. flags由以下幾個常量值指定:MAP_SHARED,MAP_PRIVATE,MAP_FIXED。其中,MAP_SHARED,MAP_PRIVATE必選其一。
  5. offset引數一般設定為0,表示從檔案頭開始對映。
  6. 引數addr指定檔案應被對映到程序空間的起始地址,一般被指定一個空指標,此時選擇起始地址的任務留給核心來完成。
  7. 函式的返回值為最後檔案對映到程序空間的地址,程序可以直接操作起始地址為該值的有效地址。

2. 系統呼叫mmap()用於共享記憶體的兩種方式

    1. 使用普通檔案提供的記憶體對映,適用於任何程序之間。此時,需要開啟或建立一個檔案,然後再呼叫mmap()。典型的呼叫程式碼如下:
fd = open(name, flag, mode);
if (fd < 0) {
    return -1;
}
ptr = mmap(NULL, len, PROT_READ|PROT_WRITE, MAP_SHAREDM, fd, 0);
    2. 使用特殊檔案提供匿名對映(基本沒遇到過這種情況,這裡不介紹了)

3. 系統呼叫munmap()

    int munmap(void *addr, size_t len)     該呼叫在程序地址空間中解除一個對映關係,addr是呼叫mmap()時返回的地址,len是對映區的大小。當對映關係解除後,對原來對映地址的訪問將導致段錯誤發生。

4. 系統呼叫msync()

    int msync(void *addr, size_t len, int flags)     一般說來,程序在對映空間的對共享內容的改變並不直接寫回到磁碟檔案中,往往在呼叫munmap()後才執行該操作。可以通過呼叫msync()實現磁碟上檔案內容與共享記憶體區的內容一致。

mmap()範例

    下面給出使用兩個程序通過對映普通檔案實現共享記憶體通訊的示例程式碼。示例包括兩個子程式:map_normalfile1.c和map_normalfile2.c。編譯兩個程式,可執行檔案分別為map_normalfile1及map_normalfile2。兩個檔案通過命令列引數指定同一個檔案來實現共享記憶體方式的程序間通訊。map_normalfile1試圖開啟命令列引數指定的一個普通檔案,把該檔案對映到程序的地址空間,並對對映後的地址空間進行寫操作。map_normalfile2把命令列引數指定的檔案對映到程序地址空間,然後對對映後的地址空間執行讀操作。這樣,兩個程序通過命令列引數指定同一個檔案來實現共享記憶體方式的程序間通訊。     map_normalfile1.c程式碼如下:
#include <sys/mman.h>
#include <sys/types.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>

typedef struct {
    char name[4];
    int age;
} people;

int main(int argc, char** argv)
{
    int fd, i;
    people *p_map;
    char temp;

    fd = open(argv[1], O_CREAT|O_RDWR|O_TRUNC, 0777);
    lseek(fd, sizeof(people) * 5 - 1, SEEK_SET);
    write(fd, "", 1);

    p_map = (people*)mmap(NULL, sizeof(people) * 10, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
    close(fd);

    temp = 'a';
    for (i = 0; i < 10; i ++) {
        temp += 1;
        memcpy((*(p_map + i)).name, &temp, 2);
        (*(p_map + i)).age = 20 + i;
    }

    printf(" Initialize over \n");
    sleep(10);
    munmap(p_map, sizeof(people) * 10);
    printf(" Unmap is ok \n");

    return 0;
}
    map_normalfile2.c程式碼如下:
#include <sys/mman.h>
#include <sys/types.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>

typedef struct {
    char name[4];
    int age;
} people;

int main(int argc, char** argv)
{
    int fd, i;
    people *p_map;
    fd = open(argv[1], O_CREAT|O_RDWR, 0777);
    p_map = (people*) mmap(NULL, sizeof(people) * 10, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
    
    for (i = 0; i < 10; i ++) {
        printf("name : %s age %d;\n", (*(p_map + i)).name, (*(p_map + i)).age);
    }

    munmap(p_map, sizeof(people) * 10);

    return 0;
}
    map_normalfile1.c首先定義了一個people資料結構,(在這裡採用資料結構的方式是因為,共享記憶體區的資料往往是有固定格式的,這由通訊的各個程序決定,採用結構的方式有普遍代表性)。map_normfile1首先開啟或建立一個檔案,並把檔案的長度設定為5個people結構大小。然後從mmap()的返回地址開始,設定了10個people結構。然後,程序睡眠10秒鐘,等待其他程序對映同一個檔案,最後解除對映。
    map_normfile2.c只是簡單的對映一個檔案,並以people資料結構的格式從mmap()返回的地址處讀取10個people結構,並輸出讀取的值,然後解除對映。
    編譯執行結果如下圖所示:

    從執行結果可以得出的結論:
  1. 最終被對映檔案的內容的長度不會超過檔案本身的初始大小,即對映不能改變檔案的大小。
  2. 可以用於程序通訊的有效地址空間大小大體上受限於被對映檔案的大小,但不完全受限於檔案大小。開啟檔案被截斷為5個people結構大小,而在mmap_normalfile1中初始化了10個people資料結構,在恰當時候(map_normalfile1輸出initialize over 之後,輸出umap ok之前)呼叫map_normalfile2會發現map_normalfile2將輸出全部10個people結構的值,後面將給出詳細討論。
  3. 檔案一旦被對映後,呼叫mmap()的程序對返回地址的訪問是對某一記憶體區域的訪問,暫時脫離了磁碟檔案的影響。所有對mmap()返回地址空間的操作只在記憶體中有意義,只有在呼叫了munmap()或者msync()後,才把記憶體中的相應內容寫回磁碟檔案,所寫內容仍然不能超過檔案的大小。

對mmap()返回地址的訪問

    前面對示例執行結構的討論中已經提到,linux採用的是頁式管理機制。對於用mmap()對映普通檔案來說,程序會在自己的地址空間新增一塊空間,空間大小由mmap()的len引數指定,注意,程序並不一定能夠對全部新增空間都能程序有效訪問。程序能夠訪問的有效地址大小取決於檔案被對映部分的大小。簡單的說,能夠容納檔案被對映部分大小的最少頁面個數決定了程序從mmap()返回的地址開始,能夠有效訪問的地址空間大小。超過這個空間大小,核心會根據超過的嚴重程式返回傳送不同的訊號給程序。可用如下圖示說明:
    注:檔案被對映部分而不是整個檔案決定了程序能夠訪問的空間大小。另外,如果指定檔案的偏移部分,一定要注意為頁面大小的整數倍。下面是對程序對映地址空間的訪問示例程式碼:
#include <sys/mman.h>
#include <sys/types.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>

typedef struct {
    char name[4];
    int age;
}people;

int main(int argc, char** argv)
{
    int fd, i;
    int pagesize, offset;
    people *p_map;

    pagesize = sysconf(_SC_PAGESIZE);
    printf("pagesize is %d\n", pagesize);

    fd = open(argv[1], O_CREAT|O_RDWR|O_TRUNC, 0777);
    lseek(fd, pagesize * 2 - 100, SEEK_SET);
    write(fd, "", 1);

    // 版本1:offset = 0
    // 版本2:offset = pagesize
    offset = 0;
    
    p_map = (people*)mmap(NULL, pagesize * 3, PROT_READ|PROT_WRITE, MAP_SHARED, fd, offset);
    close(fd);

    for (i = 1; i < 10; i ++) {
        (*(p_map + pagesize / sizeof(people) * i -2)).age = 100;
        printf("access page %d over\n", i);

        (*(p_map + pagesize / sizeof(people) * i - 1)).age = 100;
        printf("access page %d edge over, now begin to access page %d\n",i, i + 1);

        (*(p_map + pagesize / sizeof(people) * i)).age = 100;
        printf("access page %d over\n", i + 1);
    }

    munmap(p_map, sizeof(people) * 10);

    return 0;
}
    如程式碼註釋的那樣,把程式根據offset編譯成兩個版本,兩個版本主要體現在檔案被對映部分的大小不同。檔案的大小介於一個頁面與兩個頁面之間(大小為:pagesize * 2 - 99),版本1的被對映部分是整個檔案,版本2的檔案被對映部分是檔案大小減去一個頁面後的剩餘部分,不到一個頁面大小(pagesize - 99)。程式中試圖訪問每一個頁面邊界,兩個版本都試圖在程序空間中對映pagesize * 3的位元組數。
    版本1的輸出結果如下:
    版本2的輸出結果如下:
    結論:採用系統呼叫mmap()實現程序間通訊是很方便的,在應用層上介面非常簡潔。