程式-IPC 共享記憶體和訊息佇列 (三)

張哲BJUT發表於2014-11-25

詳見:https://github.com/ZhangzheBJUT/linux/blob/master/IPC(%E4%B8%89).md

五 共享記憶體

5.1. 共享記憶體簡介

共享記憶體指多個程式共享同一塊實體記憶體,它只能用於同一臺機器上的兩個程式之間的通訊。在程式的邏輯地址空間中有一段地址範圍是用來進行記憶體對映使用的,該段邏輯地址空間可以對映到共享的實體記憶體地址上(程式空間介紹:http://blog.csdn.net/zhangzhebjut/article/details/39060253)。

大多數共享記憶體的具體實現,都是把由不同程式之間共享的記憶體對映為同一段實體記憶體。 多個程式都把該實體記憶體區域對映到自己的虛擬地址空間,這些程式就都可以直接訪問該共享記憶體區域,從而可以通過該區域進行通訊。

共享記憶體允許兩個不相關的程式訪問同一段實體記憶體, 由於資料不需要在不同的程式間複製,所以它是在兩個正在執行的程式之間傳遞資料的一種非常有效的方式,一個程式向共享記憶體區域寫入資料,共享該區域的所有程式就可以立刻看到其中的資料內容。

注意:
1.如上圖所示,共享虛擬記憶體的頁面,出現在每一個共享該頁面的程式的頁表中。但是它不需要在所有程式的虛擬記憶體中都有相同的虛擬地址。
2.共享記憶體的同步控制必須由程式設計師來負責。用共享記憶體來提供對大塊記憶體區域的有效訪問,同時通過傳遞小道訊息來同步對該記憶體的訪問。

5.2 共享記憶體函式

函式原型:

#include <sys/types.h> 
#include <sys/ipc.h> 
#include <sys/shm.h> 
int   shmget(key_t key, int size, int flag); 
void* shmat(int shmid,  const void *addr, int flag); 
int   shmdt(char *shmaddr);
int   shmctl(int shmid, int cmd, struct shmid_ds *buf);

函式描述:

shmget函式:用於開闢或指向一塊共享記憶體,返回獲得共享記憶體區域的ID,如果不存在指定的共享區域就建立相應的區域。   
           keyt key: 共享記憶體的識別符號。如果是父子關係的程式間通訊的話,這個識別符號用IPC_PRIVATE來代替。
                      如果兩個程式沒有任何關係,所以就用ftok()算出來一個識別符號(或者自己定義一個)使用了。
           int size: 以位元組為單位指定需要共享的記憶體容量。  
           int flag: 包含9個位元的許可權標誌,它是這塊記憶體的模式(mode)以及許可權標識。  
                      模式可取如下值:        
                        IPC_CREAT 新建(如果已建立則返回目前共享記憶體的id)  
                        IPC_EXCL   與 IPC_CREAT結合使用,如果已建立則返回錯誤  
                      將“模式” 和“許可權標識”進行或運算,做為第三個引數。如:IPC_CREAT | IPC_EXCL | 0640 
                      其中0640為許可權標識,4/2/1 分別表示讀/寫/執行3種許可權,第一個0是UID,第一個6(4+2)表示擁
                      有者的許可權,第二個4表示同組許可權,第3個0表示他人的許可權。
          函式呼叫成功時返回共享記憶體的ID,失敗時返回-1。
          注:建立共享記憶體時,shmflg引數至少需要 IPC_CREAT | 許可權標識,如果只有IPC_CREAT 則申請的地址都是
             k=0xffffffff,不能使用;

shmat函式:用來允許本程式訪問一塊共享記憶體的函式。
          第一次建立共享記憶體時,它不能任何程式訪問,要想啟用對該共享記憶體的訪問,必須將其連線到一個程式的地址空間中。
          shmat函式就是用來完成此工作的。
          int   shmid  : 共享記憶體的ID,即共享記憶體的標識。     
          char *shmaddr: 共享記憶體連線到程式中的起始地址,如果shmaddr為NULL,核心會把共享記憶體對映到系統選定的地
                          址空間中;如果shmaddr不為NULL,核心會把共享記憶體對映到shmaddr指定的位置。
                          注:一般情況下我們很少需要控制共享記憶體連線的地址,通常都是讓系統來選擇一個地址,否則就會使應
                             用程式對硬體的依賴性過高。所以一般把shmaddr設為NULL。
          int shmflag :  本程式對該記憶體的操作模式,可以由兩個取值:SHM_RND和SHM_RDONLY。SHM_RND為讀寫模式,
                          SHM_RDONLY是隻讀模式。需要注意的是,共享記憶體的讀寫許可權由它的屬主、它的訪問許可權和當
                          前程式的屬主共同決定。如果當shmflg & SM_RDONLY為true時,即使該共享記憶體的訪問許可權允許寫操
                          作,它也不能被寫入。該引數通常會被設為0。
         函式呼叫成功時,返回共享記憶體的起始地址,失敗時返回-1。

shmdt函式:用於函式刪除本程式對這塊記憶體的使用。
          shmdt()與shmat()相反,是用來禁止本程式訪問一塊共享記憶體的函式。 
          char *shmaddr  是那塊共享記憶體的起始地址。
          函式呼叫成功時返回0,失敗時返回-1。

shmctl函式: 控制對這塊共享記憶體的使用。
            int shmid: 共享記憶體的ID,即共享記憶體標識。
            int cmd  : 控制命令,表示要採取的動作,可取值如下:
                IPC_STAT  得到共享記憶體的狀態:把shmid_ds結構中的資料設定為共享記憶體的當前關聯值
                IPC_SET   改變共享記憶體的狀態:把共享記憶體的當前關聯值設定為shmid_ds結構中給出的值
                IPC_RMID  刪除共享記憶體段
              shmid_ds結構至少包含以下成員:
                  struct shmid_ds {
                       uid_t shm_perm.uid;
                       uid_t shm_perm.gid;
                       uid_t shm_perm.mode;
                  }
            struct shmid_ds *buf: 一個結構體指標。IPC_STAT的時候,取得的狀態放在這個結構體中。
                                  如果要改變共享記憶體的狀態,用這個結構體指定。
         函式呼叫成功時返回0,失敗時返回-1。

5.3 使用例項

shm1.c
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>

#include <sys/shm.h>
#include "shm_com.h"

int main()
{
    int running  = 1;
    void *shared_memory = (void*) 0;
    struct shared_use_st *shared_stuff;
    int shmid;

    srand((unsigned int)getpid());

    shmid = shmget((key_t)1234,sizeof(struct shared_use_st),0666|IPC_CREAT);

    if(shmid == -1)
    {
        fprintf(stderr,"shmget failed\n");
        exit(EXIT_FAILURE);
    }

    shared_memory = shmat(shmid,(void*)0,0);
    if(shared_memory==(void*)-1) {
        fprintf(stderr,"shmat failed\n");
        exit(EXIT_FAILURE);
    }

    printf("Memory attached at %X\n",(int)shared_memory);

    shared_stuff = (struct shared_use_st*)shared_memory;
    shared_stuff->written_by_you = 0;

    while(running) 
    {
        if(shared_stuff->written_by_you)
        {
            printf("You wrote:%s",shared_stuff->some_text);
            sleep(rand()%4);
            shared_stuff->written_by_you = 0;
            if (strncmp(shared_stuff->some_text,"end",3) == 0) 
            {
                    running = 0;
            }
        }
    }

    if (shmdt(shared_memory) == -1)
    {
        fprintf(stderr,"shmdt faied\n");
    }

    if (shmctl(shmid,IPC_RMID,0) == -1)
    {
        fprintf(stderr,"shmctl(IPC_RMID) failed\n");
        exit(EXIT_FAILURE);
    }


    exit(EXIT_FAILURE);

}

shm2.c
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>

#include <sys/shm.h>
#include "shm_com.h"

int main()
{
    int running = 1;
    void *shared_memory = (void*)0;
    struct shared_use_st *shared_stuff;
    char buffer[BUFSIZ];

    int shmid;

    shmid = shmget((key_t)1234,sizeof(struct shared_use_st),0666|IPC_CREAT);
    if(shmid == -1)
    {
        fprintf(stderr,"shmget failed.\n");
        exit(EXIT_FAILURE);
    }

    shared_memory = shmat(shmid,(void*)0,0);
    if(shared_memory == (void*)-1)
    {
        fprintf(stderr,"shmat failed.\n");
        exit(EXIT_FAILURE);
    }

    printf("Memory attached at %X\n",(int)shared_memory);

    shared_stuff = (struct shared_use_st*)shared_memory;
    while(running)
    {
        while(shared_stuff->written_by_you==1)
        {
            sleep(1);
            printf("Waiting for client...\n");
        }

        printf("Enter some text:");
        fgets(buffer,BUFSIZ,stdin);

        strncpy(shared_stuff->some_text,buffer,TEXT_SZ);
        shared_stuff->written_by_you = 1;

        if(strncmp(buffer,"end",3) == 0) 
        {
            running  = 0;
        }

    }

    if (shmdt(shared_memory) == -1)
    {
        fprintf(stderr,"shmdt failed.\n");
        exit(EXIT_FAILURE);
    }

    exit(EXIT_FAILURE);
}

可以使用ipc -m 命令來檢視系統的共享記憶體情況:

5.4 小結

共享記憶體允許一個或多個程式通過同時出現在它們虛擬地址空間中的記憶體來通訊,此虛擬記憶體的頁面出現在每個共享程式頁表中。但此頁面並不一定位於所有共享程式虛擬記憶體的相同位置。和其它系統V IPC物件的使用方法一樣,對共享記憶體區域的訪問是通過鍵和訪問許可權檢驗來控制的。一旦記憶體被共享,則再不會檢驗程式對物件的使用方式。它依賴於其它機制,如系統V訊號燈,來同步對共享記憶體的訪問。

每個新建立的共享記憶體區域由一個shmid_ds資料結構來表示。它們被儲存在shm_segs陣列中。 shmid_ds資料結構描敘共享記憶體的大小,程式如何使用以及共享記憶體對映到其各自地址空間的方式。由共享記憶體建立者控制對此記憶體的存取許可權以及其鍵是公有還是私有。如果它由足夠許可權,它還可以將此共享記憶體載入到實體記憶體中。

每個使用此共享記憶體的程式必須通過系統呼叫將其連線到虛擬記憶體上。這時程式建立新的vm_area_struct來描敘此共享記憶體。程式可以決定此共享記憶體在其虛擬地址空間的位置,或者讓Linux選擇一塊足夠大的區域。 新的vm_area_struct結構將被放到由shmid_ds指向的vm_area_struct連結串列中。通過vm_next_shared和vm_prev_shared 指標將它們連線起來。虛擬記憶體在連線時並沒有建立,程式訪問它時才建立。

當程式首次訪問共享虛擬記憶體中的頁面時將產生缺頁錯誤。當取回此頁面後,Linux找到了描敘此頁面的vm_area_struct資料結構。它包含指向使用此種型別虛擬記憶體的處理函式地址指標。共享記憶體頁面錯誤處理程式碼將在此shmid_ds對應的頁表入口連結串列中尋找是否存在此共享虛擬記憶體頁面。如果不存在,則它將分配物理頁面併為其建立頁表入口。同時還將它放入當前程式的頁表中,此入口被儲存在shmid_ds結構中。這意味著下個試圖訪問此記憶體的程式還會產生頁面錯誤,共享記憶體錯誤處理函式將為此程式使用其新建立的物理頁面。這樣,第一個訪問虛擬記憶體頁面的程式建立這塊記憶體,隨後的程式把此頁面加入到各自的虛擬地址空間中。

當程式不再共享此虛擬記憶體時,程式和共享記憶體的連線將被斷開。如果其它程式還在使用這個記憶體,則此操作隻影響當前程式。其對應的vm_area_struct結構將從shmid_ds結構中刪除並回收。當前程式對應此共享記憶體地址的頁表入口也將被更新並置為無效。當最後一個程式斷開與共享記憶體的連線時,當前位於實體記憶體中的共享記憶體頁面將被釋放,同時釋放的還有此共享記憶體的shmid_ds結構。

六 訊息佇列

6.1. 訊息佇列簡介

訊息佇列用於執行於同一臺機器上的程式間通訊,它和管道很相似。訊息佇列提供了一種在兩個不相關的程式之間傳遞資料的相當簡單而且有效的方法,與命名管道相比,訊息佇列的優勢在於,它獨立於傳送和接收程式而存在,這消除了在同步命名管道的開啟和關閉時產生的一些困難。

通過使用訊息佇列,傳送訊息時幾乎可以完全避免命名管道的同步和阻塞問題,不再需要程式來提供同步方法,而且還可以用一些方法提前檢視緊急訊息。與管道一樣,使用訊息佇列的限制是,每個資料塊都有一個最大長度限制,系統中所有佇列所包含的全部資料塊的總長度也有一個上限。

訊息佇列提供了一種從一個程式向另一個程式傳送一個資料塊的方法。而且每個資料塊必須以一個長整型成員變數開始,該變數成員可以用來標識訊息的種類。接收程式可以獨立的接收含有不同型別的資料塊,從而實現對訊息的過濾。

6.2 訊息佇列函式

函式原型:

#include <sys/types.h> 
#include <sys/ipc.h> 
#include <sys/msg.h> 
int   msgget(key_t key,int msgflg); 
int   msgsnd(int msqid,const void *msg_ptr,size_t msg_sz,int msgflg);
int   msgrcv(int msqid,void *msg_ptr,size_t msg_sz,long int msgtype,int msgflg);
int   msgctl(int msqid,int cmd, struct msqid_ds *buf);

函式描述:

msgget函式:系統呼叫用來建立一個訊息佇列。
           key_t key : 是一個長整型,可以自己設定或通過 ftok() 獲得。
           int msgflg: 八進位制的訊息佇列操作權和控制命令的組合。
                          其中操作權定義為:
                            使用者可讀0400
                            使用者可寫0200
                            同組可讀0040
                            同組可寫0020
                            其它可讀0004
                            其它可寫0002
                            操作權可相加而派生,如使用者可"讀"、"寫"的許可權為:0400|0200=0600
                         控制命令可以取:IPC_CREAT或IPC_EXCL
                      如果要建立一個key=888且屬主和同組可讀寫的訊息佇列,執行以下系統呼叫msgget(0x888,0660|IPC_CREAT)。
            函式呼叫成功時返回一個正整數,即佇列識別符號,用來識別或引用相關的訊息佇列和資料結構。失敗時返回-1。

msgsnd函式:用於向訊息佇列中傳送一條資訊。
           int msqid:訊息佇列描述符,由 msgget 函式呼叫建立。
           const void *msg_ptr: 指向訊息佇列的指標,該指標所指的結構含有訊息的型別和要傳送或接受訊息內容。
                                struct msgbuf {
                                   longmtype;       /*訊息型別*/
                                   charmtext[2048]; /*訊息正文*/
                                } 
                                注:訊息必須要以一個長整型變數開始。

           size_t msg_sz:訊息的長度。
                          注:它不包括訊息結構體中的長整型成員變數的長度。
           int msgflg:控制當前訊息佇列滿或佇列訊息達到系統範圍的限制時將要發生的事情。
                       當訊息佇列滿時(佇列中無空閒空間):
                                如果 msgflg&IPC_NOWAIT= 真,呼叫程式立即返回,不傳送該訊息。
                                如果 msgflg&IPC_NOWAIT= 假,呼叫程式暫停執行,處於"掛起"狀態,且不傳送該訊息。直到下列情況之一出現:
                                    引起暫停的條件不再存在,如佇列出現空閒。
                                    呼叫程式接收到一個要捕捉的訊號,如中斷訊號,此時不傳送訊息,呼叫程式按signal中描述的方式執行。
            函式呼叫成功返回0,失敗返回-1,具體錯誤型別可具體檢視errno。

msgrcv函式:用於從訊息佇列中獲取一條訊息。
           int msgid    : 訊息佇列描述符,由 msgget 函式呼叫建立。
           void *msg_ptr: 指向訊息佇列的指標,該指標所指的結構含有訊息的型別和要傳送或接受訊息內容。
           size_t mgs_sz: 訊息的長度,它不包括訊息結構體中的長整型成員變數的長度。
                           注:如果所接收的訊息比msg_sz大且msgflg&MSG_NOERROR為真,則按msgsz的大小截斷而不通知呼叫程式。
           long int msgtype:用於指定接受訊息的型別,它可以實現一種簡單形式的接受優先順序。
                             可取引數型別如下:
                                     msgtyp=0 接收訊息佇列中的第一個訊息,即獲取佇列中的第一個可用訊息。
                                     msgtyp>0 接收訊息佇列中的型別為msgtyp 的第一個訊息。
                                     msgtyp<0 接收訊息佇列中小於等於msgtyp 絕對值的第一個訊息。
                             如果只是想按照訊息傳送的順序來接受它們,就把msgtype設定為0。如果只是想獲取某一特定型別的訊息,就把
                             msgtype設定為相應型別值。如果想接受型別等於或小於n的訊息,就把msgtype設定為-n。
           int msgflg: 用於控制當佇列上沒有所期望型別的訊息或訊息佇列為空時呼叫程式要採取的行動。
                        如果 msgflg&IPC_NOWAIT 為真,則呼叫程式立即結束並返回-1。
                        如果 msgflg&IPC_NOWAIT 為假,則呼叫程式暫停執行,直至出現:
                                      佇列中放入所需型別的訊息,呼叫程式接收該訊息
                                      msqid訊息佇列從系統中刪除
                                      呼叫程式接收到捕獲的訊號,此時不接收訊息,呼叫程式按signal描述的方式執行
            函式呼叫成功,返回放入接收快取區中的位元組數,失敗返回-1,具體錯誤型別可查errno。

msgctl函式:用於訊息佇列的控制。
           int msqid: 訊息佇列描述符,由 msgget 函式呼叫建立。
           int cmd  : 指出將要採取的動作:
                            IPC_STAT  檢視訊息佇列的狀態,將msqid_ds結構中的資料設定為訊息佇列的當前關聯值。
                            IPC_SET   設定訊息佇列的狀態,將訊息佇列中的關聯值設定為msqid_ds結構中給出的值。
                            IPC_RMID  刪除指定的msqid以及相關的訊息佇列和結構
           struct msqid_ds *buf:結構體指標,用於儲存訊息佇列的訪問控制模式和狀態。
           函式呼叫成功返回0,失敗返回-1.如果刪除訊息佇列時,某個程式正在msgsnd和msgrcv函式中等待,這兩個函式將失敗。 

6.3 使用例項

msg1.c
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>


#include <sys/msg.h>


struct my_msg_st {
    long int my_msg_type;
    char some_text[BUFSIZ];
};


int main()
{
    int running = 1;
    int msgid;
    struct my_msg_st some_data;
    long int msg_to_receive = 0;

    msgid = msgget((key_t)1234,0666 | IPC_CREAT);
    if (msgid == -1)
    {
        fprintf(stderr,"msgget failed with error: %d\n",errno);
        exit(EXIT_FAILURE);
    }


    while(running)
    {
        if(msgrcv(msgid,(void*)&some_data,BUFSIZ,msg_to_receive,0) == -1)
        {
            fprintf(stderr,"msgrcv failed with error: %d\n",errno);
            exit(EXIT_FAILURE);
        }

        printf("You wrote:%s",some_data.some_text);

        if(strncmp(some_data.some_text,"end",3) == 0)
        {
            running = 0;
        }

    }

    if (msgctl(msgid,IPC_RMID,0) == -1 ) 
    {
        fprintf(stderr,"msgctl(IPC_RMID) failed.\n");
        exit(EXIT_FAILURE);
    }
    exit(EXIT_FAILURE);
}

msg2.c
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>


#include <sys/msg.h>

#define MAX_TEXT 512

struct my_msg_st {
    long int my_msg_type;
    char some_text[BUFSIZ];
};


int main()
{
    int running = 1;
    int msgid;
    struct my_msg_st some_data;
    char buffer[BUFSIZ];

    msgid = msgget((key_t)1234,0666 | IPC_CREAT);
    if (msgid == -1)
    {
        fprintf(stderr,"msgget failed with error: %d\n",errno);
        exit(EXIT_FAILURE);
    }


    while(running)
    {
        printf("Enter som text:");
        fgets(buffer,BUFSIZ,stdin);
        some_data.my_msg_type = 1;
        strcpy(some_data.some_text,buffer);

        if(msgsnd(msgid,(void*)&some_data,MAX_TEXT,0) == -1)
        {
            fprintf(stderr,"msgsnd failed.\n"); 
            exit(EXIT_FAILURE);
        }


        if(strncmp(some_data.some_text,"end",3) == 0)
        {
            running = 0;
        }

    }

   exit(EXIT_FAILURE);
}

可以使用ipc -q 命令來檢視系統的訊息佇列:

6.4 小結

訊息佇列就是一個訊息的連結串列。可以把訊息看作一個個記錄或者報文,這些記錄具有特定的格式以及特定的優先順序。對訊息佇列有寫許可權的程式可以向其中按照一定的規則新增新訊息;對訊息佇列有讀許可權的程式則可以從訊息佇列中讀走訊息。每個訊息的最大長度有一個上限值,由MSGMAX定義,每個訊息佇列的總的位元組數有上限制,由MSGMNB定義,系統中訊息佇列總數有一個上限值,由MSGMNI定義。

Linux採用訊息佇列的方式來實現訊息傳遞。這種訊息的傳送方式是:傳送方不必等待接收方檢查它所收到的訊息就可以繼續工作下去,而接收方如果沒有收到訊息也不需等待。新的訊息總是放在佇列的末尾,接收的時候並不總是從頭來接收,可以從中間來接收。雖然這種通訊機制相對簡單,但是應用程式使用起來就需要使用相對複雜的方式來應付了。

訊息佇列是隨核心持續的並和程式相關,只有在核心重起或者顯示刪除一個訊息佇列時,該訊息佇列才會真正被刪除。因此係統中記錄訊息佇列的資料結構 (struct ipc_ids msg_ids) 位於核心中,系統中的所有訊息佇列都可以在結構msg_ids中找到訪問入口。

訊息佇列允許一個或者多個程式向它寫入與讀取訊息。Linux維護著一個msgque訊息佇列連結串列,其中每個元素 指向一個描敘訊息佇列的msqid_ds結構。當建立新的訊息佇列時,系統將從系統記憶體中分配一個msqid_ds結構,同時將其插入到陣列中。

每個msqid_ds結構包含一個ipc_perm結構和指向已經進入此佇列訊息的指標。另外,Linux保留有關佇列修改時間資訊,如上次系統向佇列中寫入的時間等。msqid_ds包含兩個等待佇列:一個為佇列寫入程式使用而另一個由佇列讀取程式使用。

每次程式試圖向寫入佇列寫入訊息時,系統將把其有效使用者和組標誌符與此佇列的ipc_perm結構中的模式進行比較。如果允許寫入操作,則把此訊息從此程式的地址空間拷貝到msg資料結構中,並放置到此訊息佇列尾部。由於 Linux嚴格限制可寫入訊息的個數和長度,佇列中可能容納不下這個訊息。此時,此寫入程式將被新增到這個訊息佇列的等待佇列中,同時呼叫排程管理器選擇新程式執行。當由訊息從此佇列中釋放時,該程式將被喚醒。

從佇列中讀的過程與之類似。程式對這個寫入佇列的訪問許可權將被再次檢驗。讀取程式將選擇佇列中第一個訊息(不管是什麼型別)或者第一個某特定型別的訊息。如果沒有訊息可以滿足此要求,讀取程式將被新增 到訊息佇列的讀取等待佇列中,然後系統執行排程管理器。當有新訊息寫入佇列時,程式將被喚醒繼續執行。

相關文章