1 簡介
Linux和類Linux系統下進程間通信(Inter-Process Communication, IPC)有很多種方式,包括套接字(socket),共享內存(shared memory),管道(pipe),消息隊列(message queue)等,各自有各自的一些應用場景和用途,本次來介紹消息隊列。
消息隊列的機制如下圖所示,Linux系統會維護一個隊列,消息發送者通過系統API向這個隊列發送消息,存入隊列中,然後消息接收者通過系統API從中取出消息,消息隊列具有一定的FIFO特性,但它同時也具有根據優先級來出隊的功能。
消息隊列的好處在於:實現進程間通信時,可以直接利用系統直接包裝好的同步機制和簡單協議,而不需要再去設計通信協議和同步的各種鎖。其缺點在於:每個隊列長度的大小都有系統的固定限制,而且由於隊列是單向設計的,一個發送一個接收,若發送者想收到接收者的返回,就比較困難。
2 系統API(C語言)[1]
Linux提供了一系列消息隊列的函數接口來讓我們方便地使用它來實現進程間的通信。它的用法與其他兩個System V IPC機制,即信號量和共享內存相似。
頭文件:
#include <sys/msg.h>
- msgget函數
該函數用來創建和訪問一個消息隊列。它的原型爲:
int msgget(key_t key, int msgflg);
與其他的IPC機制一樣,程序必須提供一個鍵來命名某個特定的消息隊列。msgflg是一個權限標誌,表示消息隊列的訪問權限,它與文件的訪問權限一樣。msgflg可以與IPC_CREAT做或操作,表示當key所命名的消息隊列不存在時創建一個消息隊列,如果key所命名的消息隊列存在時,IPC_CREAT標誌會被忽略,而只返回一個標識符。
它返回一個以key命名的消息隊列的標識符(非零整數),失敗時返回-1.
- msgsnd函數
該函數用來把消息添加到消息隊列中。它的原型爲:
int msgsend(int msgid, const void *msg_ptr, size_t msg_sz, int msgflg);
msgid是由msgget函數返回的消息隊列標識符。
msg_ptr是一個指向準備發送消息的指針,但是消息的數據結構卻有一定的要求,指針msg_ptr所指向的消息結構一定要是以一個長整型成員變量開始的結構體,接收函數將用這個成員來確定消息的類型。所以消息結構要定義成這樣:
struct my_message{
long int message_type;
/* The data you wish to transfer*/
};
msg_sz是msg_ptr指向的消息的長度,注意是消息的長度,而不是整個結構體的長度,也就是說msg_sz是不包括長整型消息類型成員變量的長度。
msgflg用於控制當前消息隊列滿或隊列消息到達系統範圍的限制時將要發生的事情。
如果調用成功,消息數據的一分副本將被放到消息隊列中,並返回0,失敗時返回-1.
- msgrcv函數
該函數用來從一個消息隊列獲取消息,它的原型爲
int msgrcv(int msgid, void *msg_ptr, size_t msg_st, long int msgtype, int msgflg);
msgid, msg_ptr, msg_st的作用也函數msgsnd函數的一樣。
msgtype可以實現一種簡單的接收優先級。如果msgtype爲0,就獲取隊列中的第一個消息。如果它的值大於零,將獲取具有相同消息類型的第一個信息。如果它小於零,就獲取類型等於或小於msgtype的絕對值的第一個消息。
msgflg用於控制當隊列中沒有相應類型的消息可以接收時將發生的事情。
調用成功時,該函數返回放到接收緩存區中的字節數,消息被複制到由msg_ptr指向的用戶分配的緩存區中,然後刪除消息隊列中的對應消息。失敗時返回-1.
- msgctl函數
該函數用來控制消息隊列,它與共享內存的shmctl函數相似,它的原型爲:
int msgctl(int msgid, int command, struct msgid_ds *buf);
command是將要採取的動作,它可以取3個值,
IPC_STAT:把msgid_ds結構中的數據設置爲消息隊列的當前關聯值,即用消息隊列的當前關聯值覆蓋msgid_ds的值。
IPC_SET:如果進程有足夠的權限,就把消息列隊的當前關聯值設置爲msgid_ds結構中給出的值
IPC_RMID:刪除消息隊列
buf是指向msgid_ds結構的指針,它指向消息隊列模式和訪問權限的結構。msgid_ds結構至少包括以下成員:
struct msgid_ds
{
uid_t shm_perm.uid;
uid_t shm_perm.gid;
mode_t shm_perm.mode;
};
成功時返回0,失敗時返回-1.
3 例子代碼
這裏我們用C語言利用系統API來寫一個簡單的例子,一個生產者進程(producer)來生產消息,一個消費者進程(consumer)來消費消息,就是將消息打印出來,利用消息隊列來實現這兩個進程之間的通信。代碼共包含3個文件,msg_queue_common.h,msg_queue_consumer.c,msg_queue_producer.c。
公共頭文件 msg_queue_common.h
#include <stdio.h>
#include <stdlib.h>
#include <sys/msg.h>
#define MSG_STR_LENGTH 128
#define MSG_KEY 1234
// user-defined message struct
struct my_msg{
// first member should always be long int to specify the message type
long int msg_type;
// use command flag to identity when to close the message-queue
int command;
char message[MSG_STR_LENGTH];
};
生產者進程 message_queue_producer.c
#include "msg_queue_common.h"
int main(int argc, char **argv) {
// get message id
int msgid;
if((msgid = msgget((key_t)MSG_KEY, 0666 | IPC_CREAT)) == -1) {
fprintf(stderr, "Fail to create message queue %d", MSG_KEY);
return -1;
}
struct my_msg msg;
long int mes_type = 0;
int flag = 1;
while (flag) {
printf("Receiving ...\n");
int len = sizeof(msg) - sizeof(long int);
if (msgrcv(msgid, (void *)&msg, len, mes_type, 0) == -1) {
fprintf(stderr, "Failed to receive message.\n");
continue;
}
printf("Received: %s\n", msg.message);
if (msg.command == 0) {
flag = 0;
printf("Received command = 0 to exit.\n");
}
}
if (msgctl(msgid, IPC_RMID, 0) == -1) {
fprintf(stderr, "Failed to remove message queue %d\n", MSG_KEY);
return -1;
}
return 0;
}
消費者進程 message_queue_consumer.c
#include "msg_queue_common.h"
int main(int argc, char **argv) {
// get message id
int msgid;
if((msgid = msgget((key_t)MSG_KEY, 0666 | IPC_CREAT)) == -1) {
fprintf(stderr, "Fail to create message queue %d", MSG_KEY);
return -1;
}
struct my_msg msg;
long int mes_type = 0;
int flag = 1;
while (flag) {
printf("Receiving ...\n");
int len = sizeof(msg) - sizeof(long int);
if (msgrcv(msgid, (void *)&msg, len, mes_type, 0) == -1) {
fprintf(stderr, "Failed to receive message.\n");
continue;
}
printf("Received: %s\n", msg.message);
if (msg.command == 0) {
flag = 0;
printf("Received command = 0 to exit.\n");
}
}
if (msgctl(msgid, IPC_RMID, 0) == -1) {
fprintf(stderr, "Failed to remove message queue %d\n", MSG_KEY);
return -1;
}
return 0;
}
運行
啓動兩個shell,分別啓動./consumer和./producer,在producer下輸入信息,將在consumer中接收並顯示,輸入end結束。
4 系統命令
上面提到過,消息隊列在程序進程退出時,系統並不會自動回收,那麼除了寫出很魯棒的程序外,有時候也不可避免存在消息隊列泄露的情況。並且,有時候我們的確需要看到系統當前存在消息隊列的情況,於是,利用系統命令來顯示,刪除消息隊列就很有用了。
顯示當前消息隊列命令爲:
$ipcs -q
實際上,ipcs是顯示各種進程間通信狀態的命令,-q只不過讓它顯示消息隊列(queue)的情況。還可以進行ipcs -qa來顯示消息隊列的詳細情況:
$ipcs -qa
ipcs的參數簡單介紹:
-q, --queues 消息隊列
Write information about active message queues.
-m, --shmems 共享內存
Write information about active shared memory segments.
-s, --semaphores 信號量
Write information about active semaphore sets.
最重要的是,發生泄漏的時候刪除命令:
$ipcrm -q ID
ID即爲這個消息的ID,對應-q顯示時的每行第二項
批量刪除所有消息隊列命令:
$ipcs -q | awk 'NR>3{print $2}' | xargs -n1 ipcrm -q
其中NR>3表示awk腳本只從第4行開始處理,xargs -n1表示一行一行的處理傳遞過來的參數,更加詳細的可以參考shell中awk,xargs等用法。
5 一些問題探討
私有隊列
對於鍵值,也就是key_t類型的參數key,可以設置爲特殊鍵值IPC_PRIVATE,用於創建私有隊列,理論上來說,它應該只能被當前進程訪問,但實際情況是很多Linux系統下其實並非私有,而且私有隊列的用處並不大[1],所以這個也不是很嚴重的問題,這裏就不再展開討論。
消息隊列清除與回收
程序中創建的隊列,沒有通過調用msgctl函數來執行顯式的刪除隊列的話,即使在進程退出時,操作系統也不會刪除該消息隊列,所以寫代碼時一定要考慮到這個問題,在合適的情況下刪除消息隊列,避免消息隊列的泄露問題。
非阻塞發送
在實踐中發現,使用msgsnd函數來發送消息時,設定了其msgflg參數爲IPC_NOWAIT,當函數返回-1時,並不完全是該隊列滿的情況,也有可能是當前操作系統不滿足可操作隊列的條件,這個就需要具體代碼具體分析了。
參考文獻
[1] Linux程序設計(第4版)