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版)