Linux进程间通信 - 消息队列
1 简介
Linux和类Linux系统下进程间通信(Inter-Process Communication, IPC)有很多种方式,包括套接字(socket),共享内存(shared memory),管道(pipe),消息队列(message queue)等,各自有各自的一些应用场景和用途,这次就来聊一聊消息队列这个方式。
消息队列的机制如下图所示,Linux系统会维护一个队列,消息发送者通过系统API向这个队列发送消息,存入队列中,然后消息接收者通过系统API从中取出消息,消息队列具有一定的FIFO特性,但它同时也具有根据优先级来出队的功能。
目前个人觉得消息队列的好处在于:实现进程间通信时,可以直接利用系统直接包装好的同步机制和简单协议,而不需要再去设计通信协议和同步的各种锁。其缺点在于:每个队列长度的大小都有系统的固定限制,而且由于队列是单向设计的,一个发送一个接收,若发送者想收到接收者的返回,就比较困难。
2 系统API(C语言)[1]
引用头文件:
#include <sys/msg.h>
创建和访问一个消息队列:
int msgget(key_t key, int msgflg);
第一个参数key,与其他IPC机制一样,程序提供一个键值key来命名某个特定的消息队列;
第二个参数msgflg,是由9个权限标志组成,分别是当前用户、组用户和其他用户,每类用户有读、写、执行3个权限,而IPC_CREAT定义的一个特殊位必须和权限标志位进行“或”运算才能创建一个新的消息队列,在设置IPC_CREATE标志时,如果给出的是一个已有消息队列的键,也不会产生错误。即如果对应key的消息队列已经存在,IPC_CREATE标志就被悄悄地忽略掉。
调用成功时,msgget函数返回一个正整数,即队列标识符,失败时返回-1,用法例如:
int msgid = msgget((key_t)1234,0666|IPC_CREATE);
将消息发送,添加至消息队列中:
int msgsnd(int msqid, const void* msg_ptr, size_t msg_sz, int msgflg)
第一个参数msqid就是通过msgget获得的队列标识符;
第二个参数msg_ptr是消息体指针;
第三个参数msg_sz是消息体大小;
第四个参数msgflg控制在当前队列满或队列消息到达系统范围的限制时将要发生的事情,如果msgflg中设置了IPC_NOWAIT标志,函数将立刻返回,不发送消息并且返回值为-1。如果msgflg中的IPC_NOWAIT标志被清除,则发送进程将挂起以等待队列中腾出可用空间。所以,设置IPC_NOWAIT与否就决定你发送消息方式是非阻塞的还是阻塞的。
另外,对于消息体的数据结构,是自定义的,但其受到两个方面的约束:一是长度必须小于系统规定的上限,二是必须满足其第一个成员变量是long int类型,消息接收函数将用这个成员来确定消息的类型。即如下面代码的结构:
struct my_message{
long int msg_type;
/* The data you wish to transfer */
...
}成功时,返回0,失败时,返回-1。
从消息队列中获取消息:
int msgrcv(int msqid, void* msg_ptr, size_t msg_sz, long int msgtype, int msgflg)
msgrcv函数可以从制定的消息队列中获取消息:
第一个参数即消息队列标识符,指定从哪个消息队列;
第二个参数即接收消息的结构体指针;
第三个参数msg_sz是msg_ptr指向的结构体的大小,这里注意,它是不包括长整型消息类型成员变量的长度;
第四个参数是实现了一个简单形式的接收优先级,如果其值等于0,就按照FIFO原则获取队列中的第一个可用消息,如果其值大于0,就接收消息类型为该值的第一个消息,如果它的值小于0,将获取消息类型等于或小于msgtype的绝对值的第一个消息;
第五个参数msgflg类似msgsnd中的msgflg,用于控制接收的方式是阻塞还是非阻塞的,如果设置为IPC_NOWAIT,因为某些原因无法获取消息体时,函数会马上返回,如果设置未0,则函数阻塞(进程挂起)着一直等到有消息体可以获取才返回。
成功时,返回放到接收缓存区中的字节数,失败时,返回-1。
消息队列控制函数:
int msgctl(int msqid, int command, struct msqid_ds* buf)
对于这个函数,我们使用的最多的就是用它来删除某个消息队列。
第一个参数是msgget返回的消息队列标识符;
第二个参数command是将要采取的动作,它可以取3个值,如下表[1]所示
command值 说明 IPC_STAT 把msqid_ds结构中的数据设置为消息队列的当前关联值 IPC_SET 如果进程有足够的权限,就把消息队列的当前关联值设置未msqid_ds结构中给出的值 IPC_RMID 删除消息队列 第三个参数是msqid_ds结构的指针,改结构至少包括以下成员:
struct msqid_ds{
uid_t msg_perm.uid;
uid_t msg_perm.gid;
uid_t msg_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 <sys/msg.h>
#define MSG_STR_LENGTH 128
#define MSG_KEY 1234
//user-defined message struct
struct my_msg{
long int msg_type; // first member should always be long int to specify the message type
int command;
char message[MSG_STR_LENGTH];
};
消费者进程 msg_queue_consumer.c
#include <stdio.h>
#include "msg_queue_common.h"
int main(){
// Get message queue id
int msgid = msgget((key_t)MSG_KEY,0666|IPC_CREAT);
if(msgid == -1){
fprintf(stderr,"Error! Fail to get message id for key %d\n",MSG_KEY);
return -1;
}
struct my_msg msg;
int ifExit = 1;
while(ifExit!=0){
printf("Receiving ...\n");
fflush(stdout);
int ret = msgrcv(msgid,(void*)&msg,sizeof(struct my_msg)-sizeof(long int),0,0);
if(ret == -1){
fprintf(stderr,"Failed to receive message.\n");
continue;
}
msg.message[MSG_STR_LENGTH-1]='\0';
printf("Received: %s\n",msg.message);
// Check the command, decide if it exits while loop
if(msg.command==0){
ifExit = 0;
printf("Received command=0 to exit.\n");
continue;
}
}
// Delete the message queue
if(msgctl(msgid,IPC_RMID,0) == -1){
fprintf(stderr,"Error! msgctl failed to remove message queue of key %d\n",MSG_KEY);
return -1;
}
return 0;
}
生产者进程 msg_queue_producer.c
#include <stdio.h>
#include <string.h>
#include "msg_queue_common.h"
int main(){
// Get message queue id
int msgid = msgget((key_t)MSG_KEY,0666);
if(msgid == -1){
fprintf(stderr,"Error! Fail to get message id for key %d\n",MSG_KEY);
return -1;
}
struct my_msg msg;
printf("Input information to consumer, enter \"bye\" to end both consumer and producer.\n");
int ifExit = 1;
while(ifExit!=0){
printf("Enter information to consumer: ");
fgets(msg.message,MSG_STR_LENGTH,stdin);
// Check the command, decide if it exits while loop
if(strcmp(msg.message,"bye\n")==0){
msg.command = 0;
ifExit = 0;
}else{
msg.command = 1;
}
int ret = msgsnd(msgid,(void*)&msg,sizeof(struct my_msg)-sizeof(long int),0);
if(ret == -1){
fprintf(stderr,"Fail to send the message:%s\n",msg.message);
continue;
}
}
printf("producer exit.\n");
return 0;
}
编译生成
gcc msg_queue_consumer.c -o consumer
gcc msg_queue_producer.c -o producer
运行
启动两个shell,先启动./consumer,再启动./producer,在producer下输入信息,将在consumer中接收并显示,输入bye,则将二者均结束。
$ ./producer
Input information to consumer, enter "bye" to end both consumer and producer.
Enter information to consumer: hello! Allen Junyu
Enter information to consumer: you are such a nice guy
Enter information to consumer: bye
producer exit.
$ ./consumer
Receiving ...
Received: hello! Allen Junyu
Receiving ...
Received: you are such a nice guy
Receiving ...
Received: bye
Received command=0 to exit.
4 系统命令
正如上面提到过的,消息队列在程序进程退出时,系统并不会自动回收,那么除了写出很鲁棒的程序外,有时候也不可避免存在消息队列泄露的情况。并且,有时候我们的确需要看到系统当前存在消息队列的情况,于是,利用系统命令来显示,删除消息队列就很有用了。
显示当前消息队列命令为:
ipcs -q
实际上,ipcs是显示各种进程间通信状态的命令,-q只不过让它显示消息队列(queue)的情况。还可以进行ipcs -qa来显示消息队列的详细情况:
$ ipcs -qa
IPC status from <running system> as of Mon Dec 28 19:34:57 CST 2015
T ID KEY MODE OWNER GROUP CREATOR CGROUP CBYTES QNUM QBYTES LSPID LRPID STIME RTIME CTIME
Message Queues:
q 524288 0x00002175 --rw-rw-rw- junyu staff junyu staff 0 0 2048 2760 2754 16:42:20 16:42:20 16:41:44
q 393217 0x000004d2 --rw-rw-rw- junyu staff junyu staff 0 0 2048 0 0 no-entry no-entry 19:34:53
其中,CBYTES表示在该消息队列中的现存消息所占的字节数,QNUM表示当前队列现存消息的数量,QBYTES表示当前队列最多占用的字节数,其他的解释可以man ipcs看一下。
最重要的是,发生泄漏的时候删除命令:
ipcrm -q ID
ID即为这个消息的ID,对应-qa显示时的每行第二项
批量删除所有消息队列命令:
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函数来执行显式的删除队列的话,即使在进程退出时,操作系统也不会删除该消息队列,所以写代码时一定要考虑到这个问题,在合适的情况下删除消息队列,避免消息队列的泄露问题。
非阻塞发送
在实践中发现(OS X 10.9.5),使用msgsnd函数来发送消息时,设定了其msgflg参数为IPC_NOWAIT,当函数返回-1时,并不完全是该队列满的情况,也有可能是当前操作系统不满足可操作队列的条件,这个就需要具体代码具体分析了。
参考文献
[1] Linux程序设计(第4版)