14.3 消息队列
我们现在来学习第三个也是最后一个System VIPC机制:消息队列(message queue)。消息队列与命名管道有许多相似之处,但少了在打开和关闭管道方面的复杂性。但使用消息队列并未解决我们在使用命名管道时遇到的一些问题,比如管道满时的阻塞问题。
消息队列提供了一种在两个不相关的进程之间传递数据的相当简单且有效的方法。与命名管道相比,消息队列的优势在于,它独立于发送和接收进程而存在,这消除了在同步命名管道的打开和关闭时可能产生的一些困难。
消息队列提供了一种从一个进程向另一个进程发送一个数据块的方法。而且,每个数据块都被认为含有一个类型,接收进程可以独立地接收含有不同类型值的数据块。好消息是,我们可以通过发送消息来几乎完全避免命名管道的同步和阻塞问题。更好的是,我们可以用一些方法来提前查看紧急消息。坏消息是:与管道一样,每个数据块都有一个最大长度的限制,系统中所有队列所包含的全部数据块的总长度也有一个上限。
虽然X/Open规范说明这些限制是强制的,但它并未提供发现这些限制的方法,只是告诉我们超过这些限制是引起一些消息队列函数失败的原因之一。Linux系统有两个宏定义MSGMAX和MSGMNB,它们以字节为单位分别定义了一条消息的最大长度和一个队列的最大长度。其他系统中的这些宏定义可能会不一样或甚至根本就不存在。
消息队列函数的定义如下所示:
与信号量和共享内存一样,头文件sys/types.h和sys/ipc.h通常被msg.h自动包含进程序。
14.3.1 msgget函数
我们用msgget函数来创建和访问一个消息队列:
与其他IPC机制一样,程序必须提供一个键值来命名某个特定的消息队列。特殊键值IPC_PRIVATE用于创建私有队列,从理论上来说,它应该只能被当前进程访问,但同信号量和共享内存的情况一样,消息队列在某些Linux系统中事实上并非私有。由于私有队列没有什么用处,所以这并不是一个很严重的问题。与以前一样,第二个参数msgflg由9个权限标志组成。由IPC_CREAT定义的一个特殊位必须和权限标志按位或才能创建一个新的消息队列。在设置IPC_CREAT标志时,如果给出的是一个已有消息队列的键也不会产生错误。如果消息队列已有,则IPC_CREAT标志就被悄悄地忽略掉。
成功时msgget函数返回一个正整数,即队列标识符,失败时返回-1。
14.3.2 msgsnd函数
msgsnd函数用来把消息添加到消息队列中:
消息的结构受到两方面的约束。首先,它的长度必须小于系统规定的上限;其次,它必须以一个长整型成员变量开始,接收函数将用这个成员变量来确定消息的类型。当使用消息时,最好把消息结构定义为下面这样:
由于在消息的接收中要用到message_type,所以你不能忽略它。你必须在声明自己的数据结构时包含它,并且最好将它初始化为一个已知值。
第一个参数msqid是由msgget函数返回的消息队列标识符。
第二个参数msg_ptr是一个指向准备发送消息的指针,消息必须像刚才说的那样以一个长整型成员变量开始。
第三个参数msg_sz是msg_ptr指向的消息的长度。这个长度不能包括长整型消息类型成员变量的长度。
第四个参数msgflg控制在当前消息队列满或队列消息到达系统范围的限制时将要发生的事情。如果msgflg中设置了IPC_NOWAIT标志,函数将立刻返回,不发送消息并且返回值为-1。如果msgflg中的IPC_NOWAIT标志被清除,则发送进程将挂起以等待队列中腾出可用空间。
成功时这个函数返回0,失败时返回-1。如果调用成功,消息数据的一份副本将被放到消息队列中。
14.3.3 msgrcv函数
msgrcv函数从一个消息队列中获取消息:
第一个参数msqid是由msgget函数返回的消息队列标识符。
第二个参数msg_ptr是一个指向准备接收消息的指针,消息必须像前面msgsnd函数中介绍的那样以一个长整型成员变量开始。
第三个参数msg_sz是msg_ptr指向的消息的长度,它不包括长整型消息类型成员变量的长度。
第四个参数msgtype是一个长整数,它可以实现一种简单形式的接收优先级。如果msgtype的值为0,就获取队列中的第一个可用消息。如果它的值大于零,将获取具有相同消息类型的第一个消息。如果它的值小于零,将获取消息类型等于或小于msgtype的绝对值的第一个消息。
这个函数看起来好像很复杂,但实际应用时很简单。如果只想按照消息发送的顺序来接收它们,就把msgtype设置为0。如果只想获取某一特定类型的消息,就把msgtype设置为相应的类型值。如果想接收类型等于或小于n的消息,就把msgtype设置为-n。
第五个参数msgflg用于控制当队列中没有相应类型的消息可以接收时将发生的事情。如果msgflg中的IPC_NOWAIT标志被设置,函数将会立刻返回,返回值是-1。如果msgflg中的IPC_NOWAIT标志被清除,进程将会挂起以等待一条相应类型的消息到达。
成功时msgrcv函数返回放到接收缓存区中的字节数,消息被复制到由msg_ptr指向的用户分配的缓存区中,然后删除消息队列中的对应消息。失败时返回-1。
14.3.4 msgctl函数
最后一个消息队列函数是msgctl,它的作用与共享内存的控制函数非常相似:
msqid_ds结构至少包含以下成员:
第一个参数msqid是由msgget返回的消息队列标识符。
第二个参数command是将要采取的动作。它可以取3个值,如表14-3所示。
表 14-3
成功时它返回0,失败时返回-1。如果删除消息队列时,某个进程正在msgsnd或msgrcv函数中等待,这两个函数将失败。
实 验 消息队列
介绍完消息队列的定义后,我们来看它的实际工作情况。与前面一样,我们将编写两个程序:msg1.c用于接收消息,msg2.c用于发送消息。我们将允许两个程序都可以创建消息队列,但只有接收者在接收完最后一个消息之后可以删除它。
(1)下面是接收者程序msg1.c的代码:
(2)首先建立消息队列:
(3)然后从队列中获取消息,直到遇见end消息为止。最后,删除消息队列:
(4)发送者程序msg2.c与msg1.c很相似。在main函数的变量定义部分,删除了对msg_to_receive的定义并把它替换为buffer[BUFSIZ]。去掉删除消息队列的语句,在running循环中做如下的改动。我们现在通过调用msgsnd来发送用户输入的文本到消息队列中。下面是msg2.c的代码,阴影部分是与msg1.c不同的地方:
与管道例子不同,这里不再需要由进程自己来提供同步方法。这是消息相对于管道的一个明显优势。
假设消息队列中有空间,发送者可以创建队列,放一些数据到队列中,然后在接收者启动之前就退出。我们将先运行发送者msg2。下面是一些样本输出:
实验解析
发送者程序通过msgget来创建一个消息队列,然后用msgsnd向队列中增加消息。接收者用msgget获得消息队列标识符,然后开始接收消息,直到接收到特殊的文本end为止。然后它用msgctl来删除消息队列以完成清理工作。