linux 程式間通訊之System V 訊息佇列

Wu_XMing發表於2018-10-20

1.概述

  • 用來引用訊息佇列的控制程式碼是一個由msgget()呼叫返回的識別符號。這些識別符號與檔案描述是不同的。
  • 訊息佇列進行的通訊是面向訊息的,即讀者接收到寫者寫入的整條訊息。也就是說,不能只讀取一條訊息的一部分也不能一次性讀取多條訊息。
  • 除了包含資料之外,每條訊息還有一個用整數表示的型別。

2.建立或開啟一個訊息佇列

#include<sys/types.h>
#include<sys/msg.h>
int msgget(key_t key,int msgflg);
//return message queue identifier on success,or -1 on error
複製程式碼
  • key: 引數可以設定值為IPC_PRIVATE,系統會建立一個全新的IPC物件,或者使用ftok()函式生成一個(接近唯一)key,key相同的情況下,系統會返回一個已建立相同key的IPC物件。

  • msgflg:指定施加於新訊息佇列之上的許可權和檢查一個既有佇列的許可權的位掩碼(類似與檔案許可權)

    • IPC_CREAT: 如果沒有與指定的key對應的訊息佇列,那麼就建立一個新佇列,否則返回已建立的佇列
    • IPC_EXCL:與IPC_CREAT一起使用,如果指定的key對應的佇列已經存在,那麼呼叫就會失敗並返回EEXIST錯誤。

建立一個訊息佇列

int msqid=msgget(IPC_PRIVATE,IPC_CREAT|S_IRUSR|S_IWUSR);//Read and Write by owner
if(msqid==-1){
    errExit("msgget");
}
複製程式碼

3.交換訊息

msgsnd()和msgrcv()系統呼叫執行訊息佇列上的I/O。這兩個系統呼叫接收的第一個引數是訊息佇列識別符號(msqid)。第二個是引數msgp是一個由程式設計師定義的結構的指標,該結構用於存放被髮送或接受的訊息,結構的常規形式如下:

struct msg{
    long type;//message type
    void* body;//message body
    ...
}
複製程式碼

訊息的第一個部分必須指明瞭訊息的型別,它用一個型別為long的整數來表示,而訊息的剩餘部分則是自定義的一個結構,其長度、內容、欄位名和型別都都可以是任意的,也可以多個欄位。

需特別指出:訊息的大小是除了type欄位外的所有欄位的大小

3.1 傳送訊息

從訊息佇列中寫入訊息需要佇列上的寫許可權

#include<sys/types.h>
#include<sys/msg.h>
int msgsnd(int msqid,const void *msgp,size_t msgsz, int msgflg);
//return 0 on success,or -1 on error
複製程式碼

使用msgsnd()傳送訊息必須要將訊息結構中的type欄位值設為一個大於0的值並將需要傳遞的資訊複製到自定義的body欄位中。

  • msqid佇列的識別符號

  • msgp訊息的結構體指標

  • msgsz引數指定了body欄位包含的位元組數,即訊息的大小。

  • msgflg是一組標記的位掩碼,用於控制msgsnd()操作

    • IPC_NOWAIT:執行一個非阻塞的傳送操作:當訊息佇列滿時,msgsnd()會阻塞直到佇列中有足夠的空間來存放這條訊息。如果指定了這條訊息,那麼msgsnd()就會立即返回EAGAIN錯誤

使用msgsnd()傳送一條訊息

struct mbuf{
    long mtype; //message type
    char mtext[1024]; //message body
}
struct mbuf msg;
int msqid,msgLen;

...

msgLen=strlen(msg.mtext);
if(msgsnd(msqid,&msg,msgLen,IPC_NOWAIT)==-1){
    errExit("msgsnd");
}
...
複製程式碼

3.2 接收訊息

從訊息佇列中讀取訊息需要佇列上的讀許可權

#include<sys/types.h>
#include<sys/msg.h>
ssize_t msgrcv(int msqid,void *msgp,size_t maxmsgsz,long msgtyp,int msgflg);
//return number of bytes copied into body field, or -1 on error
複製程式碼
  • msqid 佇列的識別符號

  • maxmsgsz 引數值要大於或等於需讀取的訊息的大小

  • msgp 緩衝區中訊息的最大可用空間是通過maxmsgz引數來指定的。如果佇列中待刪除的訊息體的大小超過了maxmsgsz位元組,那麼就不會從佇列中刪除訊息,並且會返回錯誤E2BIG。

  • mtype 讀取訊息的順序可以根據mtype欄位來選擇

    • mshtyp==0 將會刪除佇列中的第一條訊息並將其返回給呼叫程式。

    • msgtyoe>0 將佇列中第一條訊息裡的type欄位值等於msgtype的訊息刪除並返回給程式。可以用於讓各個程式選取與自己的程式ID匹配的訊息,這樣就會競爭讀取同一條訊息。

    • msgtype<0 將佇列變成優先佇列即最小堆形式。佇列中訊息type最小並且其值小於或等於msgtype的絕對值的第一條訊息刪除並返回給呼叫程式,如果沒有,則堵塞直到出現匹配的訊息為止。

  • msgflg 控制msgrcv()操作

    • IPC_NOWAIT 執行一個非阻塞接收。如果佇列中沒有匹配的訊息將會堵塞。設定該標記則會立即返回ENOMSG錯誤。
    • MSG_EXCEPT 需msgtype>0 才起作用,使得佇列返回第一條type不等於msgtype的訊息刪除並返回給呼叫程式(Linux特有)。
    • MSG_NOERROR 如果需讀取的訊息的大小超過了可用空間將會返回E2BIG錯誤。設定該標記將會把訊息的大小截短為maxmsgsz位元組,然會返回給呼叫者,其餘的訊息將丟棄。

使用msgrcv()讀取一條訊息

struct mbuf{
    long mtype;
    char mtext[1024];
}
int msqid,msgLen;
struct mbuf msg;
....

msgLen=msgrcv(msqid,&msg,1024,0,IPC_NOWAIT);
if(msgLen==-1){
    errExit("msgrcv");
}
複製程式碼

4.訊息佇列控制操作

#include<sys/types.h>
#include<sys/msg.h>
int msgctl(int msqid,int cmd,struct msqid_ds *buf);
// return  0 on success,or -1 on error
複製程式碼
  • cmd 引數制定了在佇列上執行的操作。
    • IPC_RMID 立即刪除訊息佇列物件及其關聯的msqid_ds資料結構,並且佇列中的訊息都會丟失,被阻塞的讀者和寫者程式會立即醒來,msgsnd()和msgrcv會失敗並返回錯誤EIDRM。這個操作會忽略傳遞進來的buf引數。
    • IPC_STAT 將訊息佇列關聯的msqid_ds 資料結構的副本放到buf中。
    • IPC_SET 使用buf提供的值更新佇列關聯的msqid_ds資料結構的欄位。

還有一些其他的cmd引數,感興趣可以網上檢視下,這裡只列出常見引數。

使用msgctl刪除System V 訊息佇列

...
if(msgctl(msqid,IPC_RMID,NULL)==-1){
    errExit("msgctl");
}
複製程式碼

4.1 訊息佇列關聯的資料結構

struct msqid_ds
{
  struct ipc_perm msg_perm;	/* structure describing operation permission */
  __time_t msg_stime;		/* time of last msgsnd command */
  __time_t msg_rtime;		/* time of last msgrcv command */
  __time_t msg_ctime;		/* time of last change */
  __syscall_ulong_t __msg_cbytes; /* current number of bytes on queue */
  msgqnum_t msg_qnum;		/* number of messages currently on queue */
  msglen_t msg_qbytes;		/* max number of bytes allowed on queue */
  __pid_t msg_lspid;		/* pid of last msgsnd() */
  __pid_t msg_lrpid;		/* pid of last msgrcv() */
  __syscall_ulong_t __glibc_reserved4;
  __syscall_ulong_t __glibc_reserved5;
};

struct ipc_perm
  {
    __key_t __key;			/* Key.  */
    __uid_t uid;			/* Owner's user ID.  */
    __gid_t gid;			/* Owner's group ID.  */
    __uid_t cuid;			/* Creator's user ID.  */
    __gid_t cgid;			/* Creator's group ID.  */
    unsigned short int mode;		/* Read/write permission.  */
    unsigned short int __pad1;
    unsigned short int __seq;		/* Sequence number.  */
    unsigned short int __pad2;
    __syscall_ulong_t __glibc_reserved1;
    __syscall_ulong_t __glibc_reserved2;
  };

複製程式碼

修改一個System V 訊息佇列的msg_qbytes 設定

...
struct msqid_ds ds;
int msqid;
...
if(msgctl(msqid,IPC_STAT,&ds)==-1){
    errExit("msgctl");
}
ds.msg_qbytes=1048576 //1MB
if(msgctl(msqid,IPC_SET,&ds)==-1){
    errExit("msgctl");
}
複製程式碼

5.訊息佇列的限制

  • MSGMNI 規定了系統中所能建立訊息佇列的數量。
  • MSGMAX 規定了單條訊息中最多可寫入的位元組數(寫入訊息超過該值,返回EINVAL錯誤)。
  • MSGMNB 規定了訊息佇列中最多能儲存的位元組數,並用來初始化msqid_ds資料結構的msg_qbytes欄位。如果達到了一個佇列的msg_qbytes限制,那麼msgsnd()會阻塞或在設定IPC_NOWAIT時返回EAGAIN錯誤。

linux 程式間通訊之System V 訊息佇列

還有一些其他的限制,感興趣可以網上檢視下,這裡只列出常見限制。

Linux 特有的msgctl() IPC_INFO 操作能夠獲取一個型別為msginfo的結構,其中包含了各種訊息佇列限制的值

struct msginfo buf;
msgctl(0, IPC_INFO,(struct msqid_ds *)&buf);

/* buffer for msgctl calls IPC_INFO, MSG_INFO */
struct msginfo
  {
    int msgpool;
    int msgmap;
    int msgmax;
    int msgmnb;
    int msgmni;
    int msgssz;
    int msgtql;
    unsigned short int msgseg;
  };

複製程式碼

6. 使用訊息佇列實現檔案伺服器應用程式

linux 程式間通訊之System V 訊息佇列
一個客戶端使用一個訊息佇列的客戶端/伺服器IPC

伺服器端核心程式碼

...
for(;;){
    msgLen=msgcrv(serverId,&req,REQ_MSG_SIZE,0,0);
    if(msgLen==-1){
        if(errno==EINTR)//Interrupted by SIGCHLD handler?
        continue;
        errMsg("msgrcv");
        break;
    }
    pid=fork();
    if(pid==-1){
        errMsg("fork");
        break;
    }
    if(pid==0){
        serveRequest(&req);
        _exit(EXIT_SUCCESS);
    }
}
...
複製程式碼

客戶端核心程式碼

...
clientId=msgget(IPC_PRIVATE,S_IRUSR|S_IWUSR|S_IWGRP);//確保服務端能夠有寫許可權
...
msgLen=msgrcv(clientId,&req,RESP_MSG_SIZE,0,0);
if(msgLen==-1){
    errExit("msgrcv");
}
...
for(;;){
    msgLen=msgrcv(clientId,&resp,RESP_MSG_SIZE,0,0);
    if(msgLen==-1){
        errExit("msgrcv");
    }
    ...
}
...
複製程式碼

7.System V 訊息佇列的缺點

  • 訊息佇列是通過識別符號引用的,而不是像大多數其他UNIX I/O機制那樣使用檔案描述符。這意味這在各種基於檔案描述符的I/O技術(如select()、poll()以及epoll)將無法應用於訊息佇列上。此外,在程式中編寫同時處理訊息佇列的輸入和基於檔案描述符的I/O機制的程式碼要比編寫只處理檔案描述符的程式碼要更加複雜。

  • 使用鍵而不是檔名來標識訊息佇列會增加額外的程式設計複雜性。ftok()函式通常能產生一個唯一的鍵,但卻無法保證,使用IPC_PRIVATE鍵能確保產生唯一的佇列識別符號,但需要使這個識別符號對需要用到它的其他程式可見。

  • 訊息佇列是無連線的,核心不會對待管道、FIFO以及socket那樣維護引用佇列的程式數,會帶來幾個問題:

    • 一個應用程式何時能夠安全地刪除一個訊息佇列?
    • 應用程式如何確保不再使用的佇列會被刪除呢?
  • 訊息佇列的總數、訊息的大小以及單個佇列的容量都是有限制的。這些限制都是可配置的,但如果一個應用程式超出了這些預設限制的範圍,那麼安裝應用程式的時候就需要完成一些額外的工作了。

相關文章