本章将介绍posix消息队列,它允许进程之间以消息的形式交换数据。POSIX消息队列与System V消息队列的相似之处在于数据交换的单位时整个消息。但他们仍然存在一些显著的差异。
- POSIX 消息队列是引用计数的,只有当所有当前使用都一列的进程都关闭了队列之后才会对队列进行标记以便删除。
- 每个System V 消息都有一个整数类型,并且通过nsgrecv()可以以各种方式来选择消息。与之形成鲜明对比的是,POSIX消息有一个关联的优先级,并且消息之间是严格按照优先级顺序排队的(以及接受)。
- POSIX消息队列提供了一个特性允许在队列中的一条消息可用时异步地通知进程。
POSIX消息队列被添加到Linux中的事件相对来讲是比较短的,所需的实现支持在内核2.6.6中才被加入(此外还需要glibc2.3.4或之后的版本)。
- POSIX消息队列被支持是一个人通过CONFIG_POSIX_VMQUEUE选项配置的可选内核组件。
52.1概述
POSIX消息队列API中的主要函数如下。
- mq_open()函数创建一个新消息队列或打开一个既有队列,返回后徐调用中会用大的消息队列描述符。
- mq_send()函数向队列中写入一条消息。
- mq_receive()函数从队列中读取一条消息。
- mq_close()函数关闭进程之前打开的一个消息队列
- mq_unlink()函数删除一个消息队列并当所有进程关闭该队列时对队列进行标记以便删除。
上面的函数所完成的功能是相当明显的。此外POSIX消息队列API还具备一些特别的特性。
- 每个消息队列都有一组关联的特性,其中一些特性可以在使用mq_open()创建或打开队列时进行设置。获取和修改队列特性的工作是由两个函数来完成的:mq_getattr()和mq_setattr()。
- mq_notify()函数允许一个进程像一个队列注册接收消息通知。在注册完毕之后,当一条消息可用时会通过发送一个信号或在一个单独的线程中调用一个函数来通知进程。
52.2 打开、关闭和断开链接消息队列
打开一个消息队列
mq_open()函数创建一个新消息队列或打开一个既有队列。
#include <fcntl.h> /*Define 0_*constant*/
#include <sys/stat.h> /*Define mode constants*/
#Include <mqueue.h>
mqd_t mq_queue(const char *name,int oflag,.../*mode_t mode,struct mq_attr *attr*/);
Returns a message queue descriptor on success, or(mqd_t)-1 on error
name参数标识除了消息队列,其取值要遵循51.1节给出的规则。
oflag参数是一个位掩码,它控制着mq_open()操作的各个方面。表52-2对这个掩码可以包含的值进行了总结。
- oflag参数的其中一个用途是,确定是打开一个既有队列还是创建一个新队列。如果在oflag中不包括O_CREAT,那么将会打开一个既有队列。如果在oflag中包含了O_CREAT,并且与给定的name对应的队列不存在,那么就会创建一个新的空队列。如果在oflag中同时包含O_CREAT和O_EXCL,并且与给定的name对应的队列已经村子啊,那么mq_open()就会失败。
oflag参数还能够包含O_RDONLY、O_WRONLY以及O_RDWR这三个值中的一个来表明调用进程在消息队列上的访问方式。
剩下的一个标记值O_NONBLOCK将会导致以非非阻塞的模式打开队列。如果后续的mq_receive()和mq_send()调用无法在不阻塞的情况下执行,那么调用就会立即返回EAGAIN错误。
mq_open()通常用来打开一个既有消息队列,这种调用只需要两个参数,但如果在oflag中指定了O_CREAT,那么就还需要另外两个参数:mode和attr。(如果通过name指定的队列已经存在,那么这两个参数会被忽略。)这些参数的用法如下。
- mode参数是一个位掩码,它制定了施加于新消息队列之上的权限。这个参数可取的位值与文件上的掩码值是一样的,并且与open()一样,mode中的值会与进程的umask取掩码。要从一个队列中读取消息(mq-receive())就必须要将读权限赋予相应的用户,要想消息队列中写入消息(mq_send())就需要写权限。
- attr参数是一个mq_attr结构,它指定了新消息队列的特性。如果attr为NULL,那么将使用实现定义的默认特性创建队列。在52.4节中将会对mqd_t结构进行介绍。
mq_open()在成功结束时会返回一个消息队列描述符,他是一个类型为mqd_t的值,灾后虚的调用中将会使用他来引用这个打开这个消息队列。SUSv3对这个数据类型的唯一约束是他不能是一个数组,即需要确保这个类型是一个能在赋值语句中使用或者能作为函数参数传递的类型。
关闭一个消息队列
mq_close函数关闭消息队列描述符mqdes。
#include <mqueue.h>
int mq_close(mqd_t mqdes);
Returns 0 on success, or -1 on error
如果调用进程已经通过mqdes在队列上注册了消息通知(52.6节),那么通知注册会自动被删除,并且另一个进程可以随后向该队列注册消息通知。
当进程终止或调用exec()时,消息队列描述符会被自动关闭。与文件描述符一样,应用程序应该在不使用该消息队列描述符的时候显式地关闭消息队列描述符以防止出现进程耗尽消息队列描述符的情况。
与文件上的close(0一样,关闭一个消息队列不会删除该队列。要删除队列需要使用mq_unlink(),他是unlink在消息队列上的版本。
删除一个消息队列
mq_unlink()函数删除通过name标识的消息队列,并将队列标记为在所有进程使用完该队列之后销毁该队列(这意味着会立即删除,前提是所有打开该队列的进程已经关闭了该队列)。
#include <mqueue.h>
int mq_unlink(const char * name);
Return 0 on success, or -1 on error
程序清单52-1:使用mq_unlink()断开一个POSIX消息队列的链接---pmsg_unlink.c
#include <mqueue.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
int main(int argc, char *argv[])
{
if(argc !=2 || strcmp(argv[1],"--help") == 0)
{
printf("%s mq-name\n",argv[0]);
return -1;
}
if(mq_unlink(argv[1])==-1)
{
perror("mq_unlink\n");
return -1;
}
exit(EXIT_SUCCESS);
}
// 编译方法 gcc pmsg_unlink.c -o pmsg_unlink -lrt
52.3 描述符和消息队列之间的关系
消息队列描述符和大开着的消息队列之前的关系与文件描述符和大开着的文件描述符之间的关系类似(见图5-2)。消息队列描述符是一个进程级别的句柄,他应用了系统层面的打开着的消息队列描述表中的一个条目,而该条目则引用了一个消息队列对象。图52-1对这种滚新进行了描绘。
在Linux上,POSIX消息队列被是现成了虚拟文件系统上的i-node,并且消息队列描述符和大开着的消息队列描述符分别实现成了文件描述符和打开着的描述符。然而SUsV3没有对实现细节进行规定,并且一些UNIX实现也并灭有采用这种实现方式。在52.7节中将会对这个话题进行讨论,因为Linux正式由于采用了这种实现方式才得以提供了一些非标准的特性。
图52-1有助于阐明消息队列描述符的使用细节方面的问题(所有这些斗鱼文件描述符的使用类似)。
- 一个打来的消息队列描述拥有一组关联的标记。SUSv3只规定了一种这样的标记,即MPMBLOCK,他确定了I/O是否是非阻塞的。
- 两个进程能够持有引用同一个打开的队列描述符的消息队列描述符(途中的描述x).当一个进程再打开了一个消息队列之后调用fork()时就会发生这种情况。这些描述符会共享O_NONBLOCK标记的状态。
- 两个进程能够持有引用不同消息队列描述(他们引用了同一个消息队列)的打开的消息队列描述(如进程A中的描述符z和进程B中的描述符y都应用了/mq-r)。当两个几次呢很难过分别使用mq_open()打开同一个队列时就会发生这种情况。
52.4 消息队列特性
mq_open()、mq_getattr()以及mq_setattr函数都会接受一个参数,他是指向mq_attr结构的指针。这个结构是在<mqueue.h>中定义的,其形式如下。
struct{
long mq_flags; /*Message queue description:0 or
O_NONBLOCK[mq_getattr(),mq_setattr()]*/
long mq_maxmsg;/*Maxmum number of messages on queue
[mq_open(),mq_getattr()]*/
long mq_msgsize;/*Maximum message size (in bytes)
[mq_open(),mq_getattr()]*/
long mq_curmsgs;/*Number of messages currently in queue*
[mq_getattr()]*/
};
在开始深入介绍mq_attr的细节之前有必要指出以下几点。
- 这三个函数中的每个函数都只用到了其中几个字段。上面给出的结构定义中的注释指出了哥哥函数所用到的字段。
- 这个结构包含了与一个消息描述符相关联的打开的消息队列描述(mq_flags)的相关信息以及该描述符所引用的队列的相关信息(mq_maxmsg、mq_msgsize、mq_curmsgs)。
- 其中一些字段中包含的信息在使用mq_open()创建队列时就已经确定下来了(mq_maxmsg和mq_msgsize);其他字段则会返回消息队列描述(mq_flags)或消息队列(mq_curmsgs)的当前状态的相关信息。
在创建队列时设置消息队列特性
在使用mq_open()创建消息队列时可以通过下列mq_attr字段来确定消息队列的特性。
- mq_maxmsg字段定义了使用mq_send()向消息队列添加消息的数量上限,其取值必须大于0。
- mq_msgsize字段定义了加入消息队列的每条消息的大小的上线,其取值必须大于0。
mq_maxmsg和mq_msgsize特性是在消息队列被创建时就确定下来的。并且之后也无法下u修改这两个特性。在52.8节中会介绍两个/proc文件,他们为mq_maxmsg和mq_msgsize特性的取值设定了一个系统层面的限制。
程序清单52-2的程序为mq_open函数提供了一个命令行界面并展示了mq_open()中如何使用mq_attr结构。
消息队列特性可以通过两个命令函参数来指定:-m用于指定mq_maxmsg,-s 用于指定mq_msgsize。只要指定了其中一个选项,那么一个非NULL的attrp参数就会被传递给mq_open()。如果在命令函中之制定了-m和-s选项中的其中一个,那么attrp只想的mq_attr结构中的一些字段机会去默认值,如果两个选项都没有被指定,那么在调用mq_open()时会将attrp指定为NULL;这将会导致使用由实现定义的队列特性的某人之来创建队列。
程序52-2:创建一个POSIX消息队列
#include <mqueue.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
static void usageError(const char * progName)
{
fprintf(stderr,"Usage:%s [-cx][-m maxmsg] [-s msgsize] mq-name\
[octal-perms]\n",progName);
fprintf(stderr," -c Create queue(O_CREATE)\n");
fprintf(stderr," -m maxmsg Set maxmum # of messages\n");
fprintf(stderr," -s msgsize Set maximum message size\n");
fprintf(stderr,"-x Create exclusively(O_EXCL)\n");
exit(EXIT_FAILURE);
}
int main(int argc,char *argv[])
{
int flags,opt;
mode_t perms;
mqd_t mqd;
struct mq_attr attr,*attrp;
attrp = NULL;
attr.mq_maxmsg = 50;
attr.mq_msgsize = 2048;
flags = O_RDWR;
//Parse command-line options
while((opt = getopt(argc,argv,"cm:s:x")) == -1)
{
switch(opt){
case 'c':
flags |= O_CREAT;
break;
case 'm':
attr.mq_maxmsg=atoi(optarg);
attrp = &attr;
break;
case 's':
attr.mq_msgsize = atoi(optarg);
attrp = &attr;
break;
case 'x':
flags |= O_EXCL;
break;
default:
(argv[0]);
}
}
if(optind >= argc)
{
usageError(argv[0]);
}
perms = (argc <=optind+1)?(S_IRUSR | S_IWUSR):atoi(argv[optind+1]);
mqd = mq_open(argv[optind],flags,perms,attrp);
if(mqd == (mqd_t)-1)
{
perror("mq_open:");
return -1;
}
exit(EXIT_SUCCESS);
}
获取消息队列特性
mq_getattr()函数返回一个包含于描述符mqdes相关联的消息队列描述和消息队列的相关信息的mq_attr结构。
#include <mqueue.h>
int mq_getatt(mqdes,struct mq_attr*attr);
Returns 0 on success,or -1 on error
除了上面已经介绍的mq_maxmsg和mq_msgsoze字段之外,attr指向的返回结构中还包含以下字段。
mq_flags
这些是与描述符mqdes相关联的打开的消息队列描述符的标记,其取值只有一个:O_NONBLOCK。这个标记是根据mq_open()的oflag参数来初始化的,并且使用mq_setattr()可以修改这个标记。
mq_curmsgs
这个当前位于队列中的消息数。这个信息在mq_getattr()来获取通过命令行参数指定的消息队列的特性,然后在标准输出中显示这些特性。
程序52-3 获取POSIX消息队列特性------pmsg_getattr.c
#include <mqueue.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
static void usageError(const char * progName)
{
fprintf(stderr,"Usage:%s [-cx][-m maxmsg] [-s msgsize] mq-name\
[octal-perms]\n",progName);
fprintf(stderr," -c Create queue(O_CREATE)\n");
fprintf(stderr," -m maxmsg Set maxmum # of messages\n");
fprintf(stderr," -s msgsize Set maximum message size\n");
fprintf(stderr,"-x Create exclusively(O_EXCL)\n");
exit(EXIT_FAILURE);
}
int main(int argc,char *argv[])
{
int flags,opt;
mode_t perms;
mqd_t mqd;
struct mq_attr attr,*attrp;
attrp = NULL;
attr.mq_maxmsg = 10;
attr.mq_msgsize = 2048;
flags = O_RDWR;
//Parse command-line options
while ((opt = getopt(argc, argv, "cm:s:x")) != -1) {
switch (opt) {
case 'c':
flags |= O_CREAT;
break;
case 'm':
attr.mq_maxmsg = atoi(optarg);
attrp = &attr;
break;
case 's':
attr.mq_msgsize = atoi(optarg);
attrp = &attr;
break;
case 'x':
flags |= O_EXCL;
break;
default:
usageError(argv[0]);
}
}
if(optind >= argc)
{
usageError(argv[0]);
}
perms = (argc <=optind+1)?(S_IRUSR | S_IWUSR):atoi(argv[optind+1]);
mqd = mq_open(argv[optind],flags,perms,attrp);
if(mqd == (mqd_t)-1)
{
perror("mq_open:");
return -1;
}
exit(EXIT_SUCCESS);
}
下面的shell绘画使用了程序清单来创建一个消息队列并使用实现定义的默认值类初始化器特性(即传入mq_open()的最后一个参数为NULL),然后使用程序清单52-3中的程序来显示队列特性,这样就能够看到Linux上的默认设置了。
从上面的输出中可以看出Linux上mq_maxmsg和mq_msgsize的默认值分别为10和8192.
mq_maxmsg和mq_msgsize的默认值取值在不同的实现上差异很大。可移植的应用程序一般都需要显示地为这两个特性选取相应的值,而不是依赖默认值。
修改消息队列特性
mq_setattr()函数这只与消息队列描述符mqdes相关联的消息队列描述的特性,并可选地返回与消息队列相关的信息。
#include <mqueue.h>
int mq_setattr(mqd_t mqdes,const struct mq_attr *newattr,
struct mq_attr *oldattr);
Return 0 on success, or -1 on error
mq_setattr()函数执行下列任务。
- 它使用newattr指向的mq_attr结构中的mq_flags字段来修改与描述符mqdes相关联的消息队列描述的标记。
- 如果oldattr不为NULL,那么久返回一个包含之前的消息队列描述符标记和消息队列特性的mq_attr结构(即与mq_getattr()执行的任务一样)。
SUSv3规定使用mq_set_attr()能够修改的唯一特性是O_NONBLOCK标记的状态。
为支持一个特定的实现可能会定义其他可修改的标记或SUSv3后面可能能回增加新的biaoji-,一个可移植的应用程序应该通过使用mq_getattr来获取mq_flags值并修改O_NONBLOCK位来修改O_NONBLOCK标记的状态以及调用mq_setattr()来修改mq_flags设置。如为启用O_NONBLOCK需要编写下列代码:
if(mq_getattr(mqd,&attr) == -1)
{
perror("mq_getattr:");
return -1;
}
attr.mq_flags |= O_NONBLICK;
if(mq_setattr(mqd,&attr,NULL) == -1)
{
perror("mq_setattr:");
return -1;
}
52.5 交换消息
52.5.1发送消息
#include <mqueue.h>
int mq_send(mqd_t mqdes,const char *msg_ptr,size_t msg_len,
unsigned int msg_prio);
Returns 0 on success, or -1 on error
msg_len参数制定了msg_ptr指向的消息的长度,其值必须小于或等于队列的mq_msgsize特性,否则mq_send()就会返回EMSGSIZE错误。长度为零的消息是允许的。
每条消息都拥有一个非负整数表示的优先级,它通过msg_prio参数指定。消息在队列中是按照优先级倒序排列的(即0表示优先级最低)。当一条消息被添加到队列中时,他会被防止在队列中相同的优先级所有消息之后。如果一个应用程序无需使用消息优先级,那么只需要将msg_prio指定为0即可。
本章开头部分提及过System V消息的类型特性的功能是不同的。System V消息总是按照FIFO的顺序排列,但msgrcv()能够按照多种方式来选择消息:按照FIFO的顺序、根据类型了选择、或者选取类型值小于或等于某个特定值的消息中类型的最大的消息。
SUSv3允许一个实现为消息优先级规定一个上限,这可以通过定义常量MQ_PRIO_MAX或通过规定sysconf(_SC_MQ_PRIO_MAX)的返回值来完成。SUSv3要求这个上限至少是32(_POSIX_MQ_PRIO_MAX),即优先级的取值范围至少为0到31,但各个市县固定的实际取值范围存在着很大的差异,如在Linux上,这个常量值为32768,而在Solaris上这个常量为32
如果消息队列已经满了(即已到达了队列的mq_maxmsg限制),那么后续的mq_send()调用会阻塞直到队列中存在可用空间为止或者在O_NONBLOCK标记起作用时立即失败并返回EAGAIN错误。
程序清单52_4:向POSIX消息队列写入一条消息 --pmsg_send.c
#include <mqueue.h>
#include <fcntl.h>
#include <string.h>
#include <stdio.h>
#include <errno.h>
#include <unistd.h>
#include <stdlib.h>
static void usageError(const char * progName)
{
fprintf(stderr,"Usage:%s [-n] name msg [prio]\n",progName);
fprintf(stderr,"-n Use O_NONBLOCK flags\n");
exit(EXIT_FAILURE);
}
int main(int argc,char *argv[])
{
int flags,opt;
mqd_t mqd;
unsigned int prio;
flags = O_WRONLY;
while ((opt = getopt(argc,argv,"n")) !=-1){
switch(opt){
case 'n':flags |= O_NONBLOCK; break;
default: usageError(argv[0]);
}
}
if(optind +1>argc)
usageError(argv[0]);
mqd = mq_open(argv[optind],flags);
if(mqd == (mqd_t)-1)
{
perror("mq_open:");
return -1;
}
prio = (argc >optind+2)?atoi(argv[optind +2]):2;
if(mq_send(mqd,argv[optind+1],strlen(argv[optind+1]),prio)==-1)
{
perror("mq_send:");
return -1;
}
exit(EXIT_SUCCESS);
}
52.5.2 接收消息
mq_receive()函数从 mqdes引用的消息队列中删除一条优先级最高、存在时间最长的而消息并将删除的消息放置在msg_ptr所指向的缓冲区。
#include <mqueue.h>
ssize_t mq_receive(mqd_t mqdes,char *msg_ptr,sizeOt msg_len,
unsigned int *msg_prio);
Returns number of bytes in received message on success,or -1 on error
调用者使用msg_len参数来指定msg_ptr指向的缓冲区中的可用字节数。
不管消息的实际大小是什么,msg_len(即msg_ptr指向的缓冲区的大小)必须要大于会等于队列的mq_msgsize特性,否则mq_receive(0就会失败并返回EMSGSIZE错误。如果不清楚一个ui列的mq_msgsize特性的值,那么可以使用mq_getattr()来获取这个值。(在一个包含多个协作进程的应用程序中一般无需使用mq_getattr(),因为应用程序通常能够提前确定队列的mq_msgsize设置。)
如果msg_prio不为NULL,那么接收到的消息的优先级会被复制msg_prio指向的位置处。
如果消息队列当前为空,那么mq_receive()会阻塞知道存在可用的消息或在O+NONBLOCK标记起作用时会立即失败并返回EAGAIN错误。(管道就不存在类似的行为,即当一端不存在写者时读者不会看到文件结束。)
下面shell 会话演示了程序清单52-4和程序清单52-5中的程序的用法。首先创建了一个消息队列并向其发送了一些具备不同优先级的消息
从上面的输出可以看出,消息的读取是按照优先级来进行的。
此刻-这个队列时空的。当再次执行阻塞式接收时,操作就会阻塞。
另一方面,如果执行了一个非阻塞接收,那么调用就会立即返回一个失败状态。
程序清单52-5:从POSIX消息队列中读取一条消息 ---pmsg_receive.c
#include <mqueue.h>
#include <fcntl.h>
#include <errno.h>
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
static void usageError(const char * progName)
{
fprintf(stderr,"Usage:%s [-n] name msg [prio]\n",progName);
fprintf(stderr,"-n Use O_NONBLOCK flags\n");
exit(EXIT_FAILURE);
}
int main(int argc,char *argv[])
{
int flags,opt;
mqd_t mqd;
unsigned int prio;
void *buffer;
struct mq_attr attr;
ssize_t numRead;
flags = O_RDONLY;
while ((opt = getopt(argc,argv,"n")) !=-1){
switch(opt){
case 'n':flags |= O_NONBLOCK; break;
default: usageError(argv[0]);
}
}
if(optind >= argc)
usageError(argv[0]);
mqd = mq_open(argv[optind],flags);
if(mqd == (mqd_t)-1)
{
perror("mq_open:");
return -1;
}
if(mq_getattr(mqd,&attr) == -1)
{
perror("mq_getattr:");
return -1;
}
buffer = malloc(attr.mq_msgsize);
if(buffer == NULL)
{
perror("malloc:");
return -1;
}
numRead = mq_receive(mqd,buffer,attr.mq_msgsize,&prio);
if(numRead == -1)
{
perror("mq_receive:");
return -1;
}
printf("Read %ld bytes; priority = %u\n",(long)numRead,prio);
if(write(STDOUT_FILENO,buffer,numRead) == -1)
{
perror("write:");
return -1;
}
write(STDOUT_FILENO,"\n",1);
exit(EXIT_SUCCESS);
}
52.5.3 在发送和接收消息时设置超时时间
mq_timedsend()和mq_timedreceive()函数与mq_send()和mq_receive()几乎是完全一样的,他们之间唯一的差别在于如果操作无法立即被执行,并且该消息队列描述上的O+NONBLOCK标记不起作用,那么abs_timeout参数会为调用阻塞的时间指定一个上限。
#define _XOPEN_SOURCE 600
#include <mqueue.h>
#include <time.h>
ssize_t mq_timedsend(mqd_t mqdes,char *msg_ptr,size_t msg_len,
unsigned int *msg_prio,const struct timespec *abs_timeout);
Returns 0 on success,or -1 on error
ssize_t mq_timedreceive(mqd_t mqdes,char *msg_ptr,sizeOt msg_len,
unsigned int *msg_prio,const struct timespec *abs_timeout);
Returns number of bytes in received message on success,or -1 on error
abs_timeout参数是一个timespec结构(23.4.2节),他将超时时间描述为自新纪元到现在的一个绝对值,其单位为秒数和纳秒数。要制定一个相对超时则可以使用clock_gettime()来获取CLOCK_REALTIME时钟的当前值并在该值加上所需的时间量来生成一个恰当初始化过的timespec结构。
如果mq_timedsend()或mq_timedreceive()调用因超时而无法完成操作,那么调用就会失败并返回ETIMEDOUT错误。
在Linux上将abs_timeout指定为NULL表示永远不会超时,但这种行为并没有在SUSv3中得到规定,因此可移植的应用程序不应该依赖这种行为。
mq_timedsend()和mq_timedreceive()函数最初源自POSIX.1d(1999),所以UNIX实现都没有提供这两个函数。
52.6 消息通知
POSIX消息队列区别于System V 消息队列的一个特性是POSIX消息队列能够接受之前为空的队列上有可用消息的异步通知(即从空队列变成了非空)。这个特性意味着已经无需执行一个阻塞的调用或将消息队列描述符标记为非阻塞并在队列上执行mq_receive()调用(“拉”)了,因为一个进程能够请求消息到达通知,然后继续执行其他任务知道收到通知为止。进程可以选择通过信号的形式或通过在一个但难度的线程调用一个函数的形式来接受通知。
POSIX消息队列的通知特性与23.6节中介绍的POSIX定时器通知工具类似。
mq_notify()函数注册调用进程在一条消息进入描述符mqdes引用的空队列时接收通知。
#include <mqueue.h>
int mq_notify(mqd_t mqdes,const struct sigevent * notification);
Returns 0 on success, 0r -1 on error
notification参数制定了进程接受=收通知的机制。在深入介绍notification参数的细节之前,欧冠消息通知需要注意以下几点。
- 在任何一个时刻都只有一个进程(“注册进程”)能够向一个特定的消息队列注册接受通知。如果有一个消息队列上已经存在注册进城了,那么后续在该队列上的注册请求将会失败(mq_notify()返回EBUSY错误)。
- 只有当一条新消息进入之前为空的队列时注册进程才会收到通知。如果在注册的时候队列中已包含消息,那么只有当队列被清空之后有一条新消息到达时才会发出通知。
- 当向注册进程发送了一个通知之后就会删除注册信息,之后任何进程就能够向队列注册接收通知了。换句话说,只要一个进程想要持续地接收通知,那么他就必须在每次接收到通知之后再次调用mq_notify()来注册自己。
- 注册进程只有在当前不存在其他队列上调用mq_receive()e而发生阻塞的进程时才会收到通知。如果其他进程在mq_receive()调用中被阻塞了,那么该进程就会读取消息,注册进程会保持注册状态。
- 一个几次呢很难过可以通过在调用mq_notify()传入一个值为NULL的notification参数来撤销自己在消息通知上的注册信息。
在23.6.1节中已经对notification参数的类型sigevent结构进行了介绍。下面给出的是该结构的一个简化版本呢,它只列出了mq_notify()相关的字段。
union sigval{
int sival_int; /*Integer value for accompanying data*/
void *sival_int; /*Pointer value for accompanying data*/
}
struct sigevent{
int sigev_notify; /* Notifucation method*/
int sigev_signo; /*Notification signal for SIGEV_SIGNAL*/
unionnsigval sigev_value; /* Value passed to signal handler or
thread function*/
void (*sigev_notify_function) (union sigval) ;
/*Thread notification function*/
void *sigev_notify_attributes; /*Really 'pthread_attr_t'*/
}
这个结构的sigev_notify字段将会被设置成下列值中的一个。
SIGEV_NONE
注册这个进程接收通知,但当一条消息进入之前为空的队列时不通知该进程。与往常一样,当新消息进入空队列之后注册消息会被删除。
SIGEV_SIGNAL
通过生成一个在sigev_signo字段中指定的信号来通知进程。如果sigev_signo是一个实时信号,那么sigev_value字段将会指定信号都带的数据(22.8.1节)。通过传入信号处理器的siginfo_t结构中的si_value字段或通过调用sigwaitinfo()或sigtimedwait()返回值能够取得这部分数据。siginfo_t结构中的下列字段也会被填充:si_code,其值为SI_MESGO;si_signo,其值是信号编号;si_pid,其值是发送消息的进程的进程ID;以及si_uid,其值是发送消息的进程真实用户ID.(si_pid和si_uid字段在其他大多数实现上不会被设置。)
SIGEV_THREAD
通过调用在sigev_notify_function中指定的函数来通知进程,就像在一个新线程中启动该函数一样。sigev_notify_attributes字段可以为NULL或是一个指向定义了线程的特性的pthread_attr_t结构的指针(29.8节)。sigev_value中指定的联合sigval值将会作为参数传入这个函数。
52.6.1通过信号接收通知
程序清单52-6 提供了一个使用信号来通知的例子。这个程序执行了下列任务。
- 以非阻塞模式打开了一个通过命令行指定名称的消息队列,确定了该队列的mq_msgsize特性的值,并分配了一个大小为该值的缓冲区来接收消息
- 阻塞通知信号(SIGUSR1)并为其建立一个处理器
- 首次调用mq_notify()来注册进程接收消息通知。
- 执行一个无线循环,讯汉中执行下列任务。
(a) 调用sigsuspend(),该函数会接触通知信号的阻塞状态并等待直到信号被捕获。从这个系统调用中返回表示已经发生了一个消息通知。此刻,进程会撤销消息通知的注册信息。
(b)调用mq_notify()重新注册进程接收消息通知
(c)执行一个while循环从队列中尽可能多的读取消息以便清空队列
程序清单52-6:通过信号接收消息通知
#include <signal.h>
#include <mqueue.h>
#include <fcntl.h>
#include <string.h>
#include <errno.h>
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#define NOTIFY_SIG SIGUSR1
static void handler(int sig)
{
/*Just interrupt sigsuspend()*/
}
int main(int argc,char *argv[])
{
struct sigevent sev;
mqd_t mqd;
struct mq_attr attr;
void *buffer;
ssize_t numRead;
sigset_t blockMask,emptyMask;
struct sigaction sa;
if(argc !=2 || strcmp(argv[1],"--help") ==0)
{
printf("%s mq_name\n",argv[0]);
return -1;
}
mqd=mq_open(argv[1],O_RDONLY|O_NONBLOCK);
if(mqd == (mqd_t)-1)
{
perror("mq_open:");
return -1;
}
if(mq_getattr(mqd,&attr) ==-1)
{
perror("mq_getattr:");
return -1;
}
buffer = malloc(attr.mq_msgsize);
if(buffer == NULL)
{
perror("malloc:");
return -1;
}
sigemptyset(&blockMask);
sigaddset(&blockMask,NOTIFY_SIG);
if(sigprocmask(SIG_BLOCK,&blockMask,NULL) == -1)
{
perror("sigprocmack:");
return -1;
}
sigemptyset(&sa.sa_mask);
sa.sa_flags =0;
sa.sa_handler = handler;
if(sigaction(NOTIFY_SIG,&sa,NULL) == -1)
{
perror("sigaction:");
return -1;
}
sev.sigev_notify = SIGEV_SIGNAL;
sev.sigev_signo = NOTIFY_SIG;
if(mq_notify(mqd,&sev) == -1)
{
perror("mq_notify:");
return -1;
}
sigemptyset(&emptyMask);
for(;;){
sigsuspend(&emptyMask); /*wait for notification signal*/
if(mq_notify(mqd,&sev) == -1)
{
perror("mq_notify:");
return -1;
}
while((numRead = mq_receive(mqd,buffer,attr.mq_msgsize,NULL)) >0)
printf("Read %ld bytes\n",(long)numRead);
if(errno != EAGAIN)
{
perror("mq_receive:");
return -1;
}
}
}
程序清单52-6中的程序中存在很多方便值得详细解释。
- 程序阻塞了通知信号并使用sigsuspend()来等待该信号,而没有使用pause(),这是为了防止出现程序在执行for循环中的其他代码(即没有因等待信号而阻塞)时错过信号的情况。如果发生了这种情况,并且使用了pause()来等待信号,那么下次调用pause()时会阻塞,即使系统已经发出了一个信号。
- 程序以非阻塞模式打开了队列,并且当一个通知发生之后使用一个while循环来读取队列中的所有消息。通过这种方式来清空队列能够确保当一条新消息到达之后会产生一个新通知。使用非阻塞模式意味着while循环在队列在被清空之后就会终止(ma_receive()会失败并返回EAGAIN)。(这种做法与63.1.1节中介绍的采用边界出发I/O通知的非阻塞I/O类似,而这里之所以采用这种做法的原因也是类似的。)
- 在for循环中比较重要的一点时在读取队列中的所有消息之前重新注册接收消息通知。如果颠倒了顺序,如按照下面的顺序:队列中的所有消息都被读取了,while循环终一个终止;另一个消息被添加到了队列中;mq_notify()被调用以重新注册接收消息通知。此刻,系统将不会产生新的通知信号,因为队列已经非空了,其结果是程序在下次调用sigsuspend()时会永远阻塞。
52.6.2 通过线程接收通知
程序清单52-7提供了一个使用线程来发布消息通知的例子。这个程序与程序清单52-6中的程序具备一些共同的设计特点。
- 当消息通知发生时,程序会在清空低劣之前重新启用通知
- 采用了非阻塞模式使得在接收到一个通知之后可以在无需阻塞的情况下完全清空队列
程序清单52-7:通过线程来接收通知----mq_notify thread.c
#include <signal.h>
#include <mqueue.h>
#include <fcntl.h>
#include <string.h>
#include <errno.h>
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
static void notifySetup(mqd_t *mqdp);
static void threadFunc(union sigval sv)
{
void *buffer;
ssize_t numRead;
mqd_t *mqdp;
struct mq_attr attr;
mqdp= sv.sival_ptr;
if(mq_getattr(*mqdp,&attr) ==-1)
{
perror("mq_getattr:");
return ;
}
buffer = malloc(attr.mq_msgsize);
if(buffer == NULL)
{
perror("malloc:");
return ;
}
notifySetup(mqdp);
while((numRead = mq_receive(*mqdp,buffer,attr.mq_msgsize,NULL)) >0)
printf("Read %ld bytes\n",(long)numRead);
if(errno != EAGAIN)
{
perror("mq_receive:");
return ;
}
free(buffer);
pthread_exit(NULL);
}
static void notifySetup(mqd_t *mqdp)
{
struct sigevent sev;
sev.sigev_notify = SIGEV_THREAD;
sev.sigev_notify_function = threadFunc;
sev.sigev_notify_attributes = NULL;
/*Could be pointer to pthread_attr_t structure*/
sev.sigev_value.sival_ptr = mqdp;
if(mq_notify(*mqdp,&sev) == -1)
{
perror("mq_notify:");
return;
}
}
int main(int argc,char *argv[])
{
mqd_t mqd;
if(argc !=2 || strcmp(argv[1],"--help") ==0)
{
printf("%s mq_name\n",argv[0]);
return -1;
}
mqd=mq_open(argv[1],O_RDONLY|O_NONBLOCK);
if(mqd == (mqd_t)-1)
{
perror("mq_open:");
return -1;
}
notifySetup(&mqd);
pause();
}
有关程序清单52-7中的程序的设计还需要注意以下几点。
- 程序通过一个线程来请求通知需要将传入mq_notify()的sigevent结构的sigev_notify字段值指定为SIGEV_THREAD。线程的启动函数threadFunc()是通过sigev_notify_function字来指定的
- 在启用消息通知之后,主程序会永远终止;定时器通知是通过在一个单独的先策划那个中调用threadFunc来分发的
- 本来可以通过消息队列描述符mqd变成一个全局变量使之对threadFunc()可见,但这里采用了一种不同的做法:将消息队列描述符的地址放在了传给mq_notify()的sigev_value.sival_ptr字段中。当后面调用threadFunc()时,这个参数会作为其参数被传入到该函数中。
必须要把指向消息队列描述符的指针赋给sigev_value.sival_ptr,而不是白描述符本身(可能需要某种转换)赋给sigev_value.sival_ptr,因为SUSv3 除了规定他不是一个数组类型之外并没有对其性质和用来表示mqd_t数据类型的大小予以规定。
52.7 Linux 特有特性
POSIX消息队列在Linux上的实现提供了一些非标准的却相当有用的特性。
通过命令行显示和删除消息队列
在51章提到POSIX IPC对象被实现成了虚拟文件系统中的文件,可以使用ls和rm来列出和删除这些文件。为列出和删除POSIX消息队列就必须要使用形如下面的命令
# mount -t queue source target
source 可以使任意一个名字(通常将其指定为字符串none),其唯一的意义是他将出现在/proc/mount中并且mount和df命令会显示出这个名字。target是消息队列文件系统的挂载点。
下面的shell会话显示了如何挂在消息队列文件系统和显示其内容。首先为文件系统创建一个挂载点并挂载他。它
在ls命令的输出中需要注意的一点是消息队列文件系统在挂载时会自动为挂载目录设置粘滞位。(从ls输出中的other-execute权限字段中有一个t就可以看出这一点。)这意味着非特权进程只能在它所拥有的消息队列上执行断开连接的操作。
接着创建一个消息队列,使用ls来表明他在文件系统中史可鉴的,然后删除消息队列。
获取消息队列的相关信息
可以显示消息队列文件系统中文件的内容,每个虚拟文件都包含了其关联的消息队列的相关信息。
QSIZE字段的值为队列中所有数据的总字节数,剩下的字段则与消息通知线管。如果NOTIFY_PID为非零,那么进程ID为该值的进程已经向该队列注册接收消息通知了,剩下的字段则提供了与这种通知相关的消息。
- NOTIFY是一个于其中一个sigev_notify常:量对应的值:0表示SIGEV_SIGNAL.1表示SIGEV_NON,2表示SIGEV_THREAD.
- 如果通知方式是SIGEV_SIGNAL,那么SIGNO字段指出了哪个信号会用来分发消息通知。
下面的shell会话对这些字段中包含的信息也作出了说明:
使用另一种I/O模型操作消息队列
在Linux实现上,消息队列描述符实际上是一个文件描述符,因此可以使用I/O多路复用系统调用(select()和poll())或epoll API来监控这个文件描述符。这样就能够避免使用system V 消息队列时同时等待一个消息队列和一个文件描述符上的输入的困难局面的出现。但这不是标准特性,SUSv3并不要求将消息队列实现成文件描述符。
52.8 消息队列限制
SUSv3为POSIX消息队列定义了两个限制
MQ_PRIO_MAX
在52.5.1已经对这个限制进行了介绍,它定义了一条消息的最大优先级。
MQ_OPEN_MAX
一个实现可以定义这个限制来指明一个进程最多能打开的消息队列数量。SUSv3要求这个仙子最小为_POSIX_MQ_OPEN_MAX(8)。Linux 并没有定义这个限制,相反,由于Linux将消息队列描述符是实现成了文件描述符(52.7),因此适用于文件描述符的限制将适用于消息队列描述符。(换句话说,在Linux上,每个进程以及系统所能打开的文件描述符的数量限制实际上会应用于文件描述符数量和消息队列描述符数量之和。)
msg_max
这个限制为新消息队列的mq_maxmsg特性的取值规定了一个上限(即使用mq_open()创建队列时attr_mq_maxmsg字段的上限值)。这个限制的默认值是10,最小值是1(在早于2.6.28的内核是10),最大值由内核常量HARD_MSGMAX定义,该常量的值是通过公式(131072/sizeof(void *))计算的来的,在Linux/x86-32上其值为32768.当一个特权进程(CAP_SYS_RESOURCE)调用mq_open()时msg_max限制会被忽略,但HARD_MSGMAX仍然担当着attr.mq_maxmsg的上限值的角色。
msgsize_max
这个限制为非特权进程创建的新消息队列的mq_msgsize特性的取值规定了一个上限(即mq_open()创建队列时attr.mq_msgsize字段的上限值)。这个限制默认值时8192,最小值的128(在早于2.6.28的内核中是8192),最大值是1048576(在早于2.6.28的内核中式INT_MAX)。当一个非特权进程(CAP_SYS_RESOURCE)调用mq_open()时会忽略这个限制。
queues_max
这是一个系统级别的限制,它规定了系统上最多能够创建的消息队列的数量。一旦达到这个限制,就只有特权进程(CAP_SYS_RESOURCE)才能创建新队列。这个限制默认值是256,其取值可以为范围从0到INT_MAX之间的任意一个值
Linux还提供了RLIMIT_MSGQUUE资源限制,它可以用来为属于调用进程的真实用户IKD的所有消息队列所消耗的空间规定一个上限。
52.9 POSIX和 Ssystem V消息队列比较
51.2节列出了POSIX IPC与Sstem V IPC接口相比存在的各种优势:POSIX IPC接口更加简单并且与传统的UNIX文件模型更加一致,同时POSIX IPC 是引用计数的、,这样就简化了确定合适删除一个对象的任务。POSIX 消息队列也同样具有这些优势。
POSIX消息队列与System V消息队列相比还具备下列优势。
- 消息通知特性允许一个(单个)进程能够在一条消息进入之前为空的队列时异步地通过信号或咸亨地实例化来接收通知。
- 在Linux(不包括其他UNIX实现)上可以使用poll()、select()以及epoll来监控POSIX消息队列System V消息队列并没有这个特性。
但与System V 消息队列相比,POSIX消息队列也具备以下劣势
- POSIX消息队列地可移植性差,即使在不同地Linux系统上也存在这个问题,因为知道内核2.6.6才提供了对消息队列地支持。
- 与POSIX消息队列严格按照优先级排序相比,System V消息队列能够根据类型来选择消息地功能灵活性更强
52.10 总结
POSIX消息队列允许进程以消息地形式交换数据。每条消息都有一个关联地整数优先级,消息按照优先级顺序排列(从而会按照这个顺序接收消息)。
POSIX消息队列与System V消息队列相比具备一些优势,特别是他们是引用技术的并且一个进程在一条消息进入空队列时能够异步地收到通知,但POSIX消息队列地移植性要比System V消息队列稍差。