進程-IPC 共享內存和消息隊列 (三)

詳見: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嚴格限制可寫入消息的個數和長度,隊列中可能容納不下這個消息。此時,此寫入進程將被添加到這個消息隊列的等待隊列中,同時調用調度管理器選擇新進程運行。當由消息從此隊列中釋放時,該進程將被喚醒。

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章