1. 程式人生 > >程序間通訊之-共享記憶體Shared Memory--linux核心剖析(十一)

程序間通訊之-共享記憶體Shared Memory--linux核心剖析(十一)

共享記憶體

共享記憶體是程序間通訊中最簡單的方式之一。

共享記憶體是系統出於多個程序之間通訊的考慮,而預留的的一塊記憶體區。

共享記憶體允許兩個或更多程序訪問同一塊記憶體,就如同 malloc() 函式向不同程序返回了指向同一個實體記憶體區域的指標。當一個程序改變了這塊地址中的內容的時候,其它程序都會察覺到這個更改。

關於共享記憶體

當一個程式載入進記憶體後,它就被分成叫作頁的塊。

通訊將存在記憶體的兩個頁之間或者兩個獨立的程序之間。

總之,當一個程式想和另外一個程式通訊的時候,那記憶體將會為這兩個程式生成一塊公共的記憶體區域。這塊被兩個程序分享的記憶體區域叫做共享記憶體

因為所有程序共享同一塊記憶體,共享記憶體在各種程序間通訊方式中具有最高的效率。訪問共享記憶體區域和訪問程序獨有的記憶體區域一樣快,並不需要通過系統呼叫或者其它需要切入核心的過程來完成。同時它也避免了對資料的各種不必要的複製。

如果沒有共享記憶體的概念,那一個程序不能存取另外一個程序的記憶體部分,因而導致共享資料或者通訊失效。因為系統核心沒有對訪問共享記憶體進行同步,您必須提供自己的同步措施。

解決這些問題的常用方法是通過使用訊號量進行同步。不過,我們的程式中只有一個程序訪問了共享記憶體,因此在集中展示了共享記憶體機制的同時,我們避免了讓程式碼被同步邏輯搞得混亂不堪。

為了簡化共享資料的完整性和避免同時存取資料,核心提供了一種專門存取共享記憶體資源的機制。這稱為互斥體

或者mutex物件

例如,在資料被寫入之前不允許程序從共享記憶體中讀取資訊、不允許兩個程序同時向同一個共享記憶體地址寫入資料等。

當一個程序想和另外一個程序通訊的時候,它將按以下順序執行:

  • 獲取mutex物件,鎖定共享區域。

  • 將要通訊的資料寫入共享區域。

  • 釋放mutex物件。

當一個程序從從這個區域讀資料時候,它將重複同樣的步驟,只是將第二步變成讀取。

記憶體模型

要使用一塊共享記憶體

  • 程序必須首先分配

  • 隨後需要訪問這個共享記憶體塊的每一個程序都必須將這個共享記憶體繫結到自己的地址空間中

  • 當完成通訊之後,所有程序都將脫離共享記憶體,並且由一個程序釋放該共享記憶體塊

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

這裡寫圖片描述

理解 Linux 系統記憶體模型可以有助於解釋這個繫結的過程。

linux系統記憶體模型

在 Linux 系統中,每個程序的虛擬記憶體是被分為許多頁面的。這些記憶體頁面中包含了實際的資料。每個程序都會維護一個從記憶體地址到虛擬記憶體頁面之間的對映關係。儘管每個程序都有自己的記憶體地址,不同的程序可以同時將同一個記憶體頁面對映到自己的地址空間中,從而達到共享記憶體的目的。

分配一個新的共享記憶體塊會建立新的記憶體頁面。因為所有程序都希望共享對同一塊記憶體的訪問,只應由一個程序建立一塊新的共享記憶體。再次分配一塊已經存在的記憶體塊不會建立新的頁面,而只是會返回一個標識該記憶體塊的識別符號。

一個程序如需使用這個共享記憶體塊,則首先需要將它繫結到自己的地址空間中。

這樣會建立一個從程序本身虛擬地址到共享頁面的對映關係。當對共享記憶體的使用結束之後,這個對映關係將被刪除。

當再也沒有程序需要使用這個共享記憶體塊的時候,必須有一個(且只能是一個)程序負責釋放這個被共享的記憶體頁面。

所有共享記憶體塊的大小都必須是系統頁面大小的整數倍。系統頁面大小指的是系統中單個記憶體頁面包含的位元組數。在 Linux 系統中,記憶體頁面大小是4KB,不過您仍然應該通過呼叫 getpagesize 獲取這個值。

共享記憶體的實現分為兩個步驟:

  • 建立共享記憶體,使用shmget函式。

  • 對映共享記憶體,將這段建立的共享記憶體對映到具體的程序空間去,使用shmat函式。

用於共享記憶體的函式

共享記憶體的使用,主要有以下幾個API:ftok()shmget()shmat()shmdt()及shmctl()。

#include <sys/shm.h>
void *shmat(int shm_id, const void *shm_addr, int shmflg);
int shmctl(int shm_id, int cmd, struct shmid_ds *buf);
int shmdt(const void *shm_addr);
int shmget(key_t key, size_t size, int shmflg);

這裡寫圖片描述

與訊號量相類似,通常需要在包含shm.h檔案之前包含sys/types.h與sys/ipc.h這兩個標頭檔案。

用ftok()函式獲得一個ID號

應用說明,在IPC中,我們經常用用key_t的值來建立或者開啟訊號量,共享記憶體和訊息佇列。

key_t ftok(const char *pathname, int proj_id);
引數 描述
pathname 一定要在系統中存在並且程序能夠訪問的
proj_id 一個1-255之間的一個整數值,典型的值是一個ASCII值。

當成功執行的時候,一個key_t值將會被返回,否則-1被返回。我們可以使用strerror(errno)來確定具體的錯誤資訊。

考慮到應用系統可能在不同的主機上應用,可以直接定義一個key,而不用ftok獲得:

#define IPCKEY 0x344378

建立共享記憶體

程序通過呼叫shmget(Shared Memory GET,獲取共享記憶體)來分配一個共享記憶體塊。

int shmget(key_t key ,int size,int shmflg)
引數 描述
key 一個用來標識共享記憶體塊的鍵值
size 指定了所申請的記憶體塊的大小
shmflg 操作共享記憶體的標識

返回值:如果成功,返回共享記憶體表示符,如果失敗,返回-1。

  • 該函式的第二個引數key是一個用來標識共享記憶體塊的鍵值。

彼此無關的程序可以通過指定同一個鍵以獲取對同一個共享記憶體塊的訪問。不幸的是,其它程式也可能挑選了同樣的特定值作為自己分配共享記憶體的鍵值,從而產生衝突。

用特殊常量IPC_PRIVATE作為鍵值可以保證系統建立一個全新的共享記憶體塊。|

key標識共享記憶體的鍵值:0/IPC_PRIVATE。當key的取值為IPC_PRIVATE,則函式shmget將建立一塊新的共享記憶體;如果key的取值為0,而引數中又設定了IPC_PRIVATE這個標誌,則同樣會建立一塊新的共享記憶體。

  • 該函式的第二個引數size指定了所申請的記憶體塊的大小。

因為這些記憶體塊是以頁面為單位進行分配的,實際分配的記憶體塊大小將被擴大到頁面大小的整數倍。

  • 第三個引數shmflg是一組標誌,通過特定常量的按位或操作來shmget。這些特定常量包括:

IPC_CREAT:這個標誌表示應建立一個新的共享記憶體塊。通過指定這個標誌,我們可以建立一個具有指定鍵值的新共享記憶體塊。

IPC_EXCL:這個標誌只能與 IPC_CREAT 同時使用。當指定這個標誌的時候,如果已有一個具有這個鍵值的共享記憶體塊存在,則shmget會呼叫失敗。也就是說,這個標誌將使執行緒獲得一個“獨有”的共享記憶體塊。如果沒有指定這個標誌而系統中存在一個具有相同鍵值的共享記憶體塊,shmget會返回這個已經建立的共享記憶體塊,而不是重新建立一個。

模式標誌:這個值由9個位組成,分別表示屬主、屬組和其它使用者對該記憶體塊的訪問許可權。

其中表示執行許可權的位將被忽略。指明訪問許可權的一個簡單辦法是利用

對映共享記憶體

shmat()是用來允許本程序訪問一塊共享記憶體的函式,將這個記憶體區對映到本程序的虛擬地址空間。

int shmat(int shmid,char *shmaddr,int flag)
引數 描述
shmid 那塊共享記憶體的ID,是shmget函式返回的共享儲存識別符號
shmaddr 是共享記憶體的起始地址,如果shmaddr為0,核心會把共享記憶體映像到呼叫程序的地址空間中選定位置;如果shmaddr不為0,核心會把共享記憶體映像到shmaddr指定的位置。所以一般把shmaddr設為0。
shmflag 是本程序對該記憶體的操作模式。如果是SHM_RDONLY的話,就是隻讀模式。其它的是讀寫模式

成功時,這個函式返回共享記憶體的起始地址。失敗時返回-1。

要讓一個程序獲取對一塊共享記憶體的訪問,這個程序必須先呼叫 shmat(SHared Memory Attach,繫結到共享記憶體)。

將 shmget 返回的共享記憶體識別符號 SHMID 傳遞給這個函式作為第一個引數。

該函式的第二個引數是一個指標,指向您希望用於對映該共享記憶體塊的程序記憶體地址;如果您指定NULL則Linux會自動選擇一個合適的地址用於對映。第三個引數是一個標誌位,包含了以下選項:

SHM_RND表示第二個引數指定的地址應被向下靠攏到記憶體頁面大小的整數倍。如果您不指定這個標誌,您將不得不在呼叫shmat的時候手工將共享記憶體塊的大小按頁面大小對齊。
SHM_RDONLY表示這個記憶體塊將僅允許讀取操作而禁止寫入。 如果這個函式呼叫成功則會返回繫結的共享記憶體塊對應的地址。通過 fork 函式建立的子程序同時繼承這些共享記憶體塊;

如果需要,它們可以主動脫離這些共享記憶體塊。 當一個程序不再使用一個共享記憶體塊的時候

共享記憶體解除對映

當一個程序不再需要共享記憶體時,需要把它從程序地址空間中多裡。

int shmdt(char *shmaddr)
引數 描述
shmaddr 那塊共享記憶體的起始地址

成功時返回0。失敗時返回-1。

應通過呼叫 shmdt(Shared Memory Detach,脫離共享記憶體塊)函式與該共享記憶體塊脫離。將由 shmat 函式返回的地址傳遞給這個函式。如果當釋放這個記憶體塊的程序是最後一個使用該記憶體塊的程序,則這個記憶體塊將被刪除。對 exit 或任何exec族函式的呼叫都會自動使程序脫離共享記憶體塊。

控制釋放

shmctl控制對這塊共享記憶體的使用

函式原型

int  shmctl( int shmid , int cmd , struct shmid_ds *buf );
引數 描述
shmid 是共享記憶體的ID。
cmd 控制命令
buf 一個結構體指標。IPC_STAT的時候,取得的狀態放在這個結構體中。如果要改變共享記憶體的狀態,用這個結構體指定。

其中cmd的取值如下

cmd 描述
IPC_STAT 得到共享記憶體的狀態
IPC_SET 改變共享記憶體的狀態
IPC_RMID 刪除共享記憶體

返回值: 成功:0 失敗:-1

呼叫 shmctl(”Shared Memory Control”,控制共享記憶體)函式會返回一個共享記憶體塊的相關資訊。同時 shmctl 允許程式修改這些資訊。

該函式的第一個引數是一個共享記憶體塊標識。
要獲取一個共享記憶體塊的相關資訊,則為該函式傳遞 IPC_STAT 作為第二個引數,同時傳遞一個指向一個 struct shmid_ds 物件的指標作為第三個引數。

要刪除一個共享記憶體塊,則應將 IPC_RMID 作為第二個引數,而將 NULL 作為第三個引數。當最後一個繫結該共享記憶體塊的程序與其脫離時,該共享記憶體塊將被刪除。

您應當在結束使用每個共享記憶體塊的時候都使用 shmctl 進行釋放,以防止超過系統所允許的共享記憶體塊的總數限制。呼叫 exit 和 exec 會使程序脫離共享記憶體塊,但不會刪除這個記憶體塊。 要檢視其它有關共享記憶體塊的操作的描述,請參考shmctl函式的手冊頁。

示例

簡單對映一塊共享記憶體

#include <stdio.h>
#include <stdlib.h>

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


#define IPCKEY 0x366378



typedef struct st_setting
{
    char agen[10];
    unsigned char file_no;
}st_setting;

int main(int argc, char** argv)
{
    int         shm_id;
    //key_t       key;
    st_setting  *p_setting;

    //  首先檢查共享記憶體是否存在,存在則先刪除
    shm_id = shmget(IPCKEY , 1028, 0640);
    if(shm_id != -1)
    {
        p_setting = (st_setting *)shmat(shm_id, NULL, 0);

        if (p_setting != (void *)-1)
        {
            shmdt(p_setting);

            shmctl(shm_id,IPC_RMID,0) ;
        }
    }

    //  建立共享記憶體
    shm_id = shmget(IPCKEY, 1028, 0640 | IPC_CREAT | IPC_EXCL);
    if(shm_id == -1)
    {
        printf("shmget error\n");
        return -1;
    }

    //  將這塊共享記憶體區附加到自己的記憶體段
    p_setting = (st_setting *)shmat(shm_id, NULL, 0);

    strncpy(p_setting->agen, "gatieme", 10);
    printf("agen : %s\n", p_setting->agen);

    p_setting->file_no = 1;
    printf("file_no : %d\n",p_setting->file_no);

    system("ipcs -m");//  此時可看到有程序關聯到共享記憶體的資訊,nattch為1

    //  將這塊共享記憶體區從自己的記憶體段刪除出去
    if(shmdt(p_setting) == -1)
       perror(" detach error ");

    system("ipcs -m");//  此時可看到有程序關聯到共享記憶體的資訊,nattch為0

    //  刪除共享記憶體
    if (shmctl( shm_id , IPC_RMID , NULL ) == -1)
    {
        perror(" delete error ");
    }

    system("ipcs -m");//  此時可看到有程序關聯到共享記憶體的資訊,nattch為0


    return EXIT_SUCCESS;
}

這裡寫圖片描述

ipcrm命令刪除共享記憶體

在使用共享記憶體,結束程式退出後。如果你沒在程式中用shmctl()刪除共享記憶體的話,一定要在命令列下用ipcrm命令刪除這塊共享記憶體。你要是不管的話,它就一直在那兒放著了。
簡單解釋一下ipcs命令和ipcrm命令。

取得ipc資訊:

usage : ipcs -asmq -tclup 
    ipcs [-s -m -q] -i id
    ipcs -h for help.
m      輸出有關共享記憶體(shared memory)的資訊
-q      輸出有關資訊佇列(message queue)的資訊
-s      輸出有關“遮斷器”(semaphore)的資訊

刪除ipc

usage: ipcrm [ [-q msqid] [-m shmid] [-s semid]
          [-Q msgkey] [-M shmkey] [-S semkey] ... ]

兩端通訊的程式

讀者程式

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <signal.h>
#include <string.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>

#define N 64

typedef struct
{
    pid_t pid;
    char buf[N];
} SHM;

void handler(int signo)
{
    //printf("get signal\n");
    return;
}

int main()
{
    key_t key;
    int shmid;
    SHM *p;
    pid_t pid;

    if ((key = ftok(".", 'm')) < 0)
    {
        perror("fail to ftok");
        exit(-1);
    }

    signal(SIGUSR1, handler);//註冊一個訊號處理函式
    if ((shmid = shmget(key, sizeof(SHM), 0666|IPC_CREAT|IPC_EXCL)) < 0)
    {
        if (EEXIST == errno)//存在則直接開啟
        {
            shmid = shmget(key, sizeof(SHM), 0666);
            p = (SHM *)shmat(shmid, NULL, 0);
            pid = p->pid;
            p->pid = getpid();//把自己的pid寫到共享記憶體
            kill(pid, SIGUSR1);
        }
        else//出錯
        {
            perror("fail to shmget");
            exit(-1);
        }
    }
    else//成功
    {
        p = (SHM *)shmat(shmid, NULL, 0);
        p->pid = getpid();
        pause();
        pid = p->pid;//得到寫端程序的pid
    }

    while ( 1 )
    {
        pause();//阻塞,等待訊號
        if (strcmp(p->buf, "quit\n") == 0) exit(0);//輸入"quit結束"
        printf("read from shm : %s", p->buf);
        kill(pid, SIGUSR1);//向寫程序發SIGUSR1訊號
    }

    return 0;


}

寫者程式

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <signal.h>
#include <string.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>

#define N 64

typedef struct
{
    pid_t pid;
    char buf[N];
} SHM;

void handler(int signo)
{
    //printf("get signal\n");
    return;
}

int main()
{
    key_t key;
    int shmid;
    SHM *p;
    pid_t pid;

    if ((key = ftok(".", 'm')) < 0)
    {
        perror("fail to ftok");
        exit(-1);
    }

    signal(SIGUSR1, handler);               //  註冊一個訊號處理函式
    if ((shmid = shmget(key, sizeof(SHM), 0666 | IPC_CREAT | IPC_EXCL)) < 0)
    {
        if (EEXIST == errno)                //  存在則直接開啟
        {
            shmid = shmget(key, sizeof(SHM), 0666);

            p = (SHM *)shmat(shmid, NULL, 0);

            pid = p->pid;
            p->pid = getpid();
            kill(pid, SIGUSR1);
        }
        else//出錯
        {
            perror("fail to shmget");
            exit(-1);
        }
    }
    else//成功
    {

        p = (SHM *)shmat(shmid, NULL, 0);
        p->pid = getpid();                  //  把自己的pid寫到共享記憶體
        pause();
        pid = p->pid;                       //  得到讀端程序的pid

    }

    while ( 1 )
    {
        printf("write to shm : ");
        fgets(p->buf, N, stdin);            //  接收輸入
        kill(pid, SIGUSR1);                 //  向讀程序發SIGUSR1訊號
        if (strcmp(p->buf, "quit\n") == 0) break;
        pause();                            //  阻塞,等待訊號
    }
    shmdt(p);
    shmctl(shmid, IPC_RMID, NULL);          //  刪除共享記憶體

    return 0;
}

這裡寫圖片描述