消息队列再整理二
一个消息队列是一个消息的链表,存储在内核里,并由一个消息队列标识符标识。我们将只称消息队列为一个队列,称它的标识符为一个队列ID。
SUS包含了一个替代的IPC消息队列的实现,在实时扩展的消息传递选项里。我们不在本文包含实现扩展。
一 个新的队列被创建或一个已有队列被打开,通过msgget。新的消息被加入到一个队列的尾部,通过msgsnd。每个消息有一个正的长整型类型域,一个非 负长度,和真实数据字节(对应于长度),这些所有在消息加入到一个队列时由msgsnd指定。消息通过msgrcv从一个队列获取。我们不必以先进先出的 方式得到消息。相反,我们可以基于它们的类型域来获取消息。
每个队列有以下的msqid_ds结构体与它相关:
struct msqid_ds {
struct ipc_perm msg_perm; /* see Section 15.6.2 */
msgqnum_t msg_qnum; /* # of messages on queue */
msglen_t msg_qbytes; /* max # of bytes on queue */
pid_t msg_lspid; /* pid of last msgsnd() */
pid_t msg_lrpid; /* pid of last msgrcv() */
time_t msg_stime; /* last-msgsnd() time */
time_t msg_rtime; /* last-msgrcv() time */
time_t msg_ctime; /* last-change time */
...
};
这个结构体定义了队列的当前状态。展示的成员是由SUS定义的。实现包含不被标准覆盖的补充域。
下 表列出了影响消息队列的系统限量。我们在平台不支持这个特性的地方显示“不支持”。每当一个限量从其它限量继承时我们显示“继承”。例如,Linux系统 里的消息数量的最大值是基于队列的最大数量和队列上允许的最大数据量。如果最小消息尺寸为1字节,那么它将限制系统范围的消息数为最大队列数乘以一个队列 的最大尺寸。给定下表的限量,Linux在默认配置里有262144消息的上限。(即使一个消息可以包含0字节的数据,但是Linux视它为1字节,来限 制排队消息的数量。)
描述 | 典型值 | |||
FreeBSD | Linux | Mac | Solaris | |
我们可以发送的最大消息的字节尺寸 | 16384 | 8192 | 不支持 | 2048 |
一个特定队列的最大字节尺寸(也就是说,队列上所有消息的和。) | 2048 | 16384 | 不支持 | 4096 |
系统范围的消息队列的最大数量。 | 40 | 16 | 不支持 | 50 |
系统范围的消息最大数量。 | 40 | 继承 | 不支持 | 40 |
回想15.1节,Mac OS X 10.3不支持XSI消息队列。因为Mac基于一部分的FreeBSD,而FreBSD支持消息队列,所以Mac也有可能支持它们。事实上,一个好的因特网搜索引擎会提供Mac的XSI消息队列的一个第三方端口的指针。
通常第一个被调用的函数是msgget,以打开一个存在的队列或创建一个新的队列。
-
#include <sys/msg.h>
-
-
int msgget (key_t key, int flag);
-
- 成功返回消息队列ID。错误返回-1。
在15.6.1节,我们描述了把关键字转换为一个描述符的规则并讨论了是一个新的队列被创建还是一个存在的队列被引用。当一个新的队列被创建时,以下的msqid_ds结构体的成员被初始化:
1、ipc_perm结构体被初始化,如15.6.2节描述的。这个结构体的mode成员被设置为标志的对应的权限位。这些权限由15.6.2节的表里的值指定。
2、msg_qnum、msg_lspid、msg_lrpid、msg_stime、和msg_rtime都被设置为0。
3、msg_ctime被设为当前时间。
4、msg_qbytes被设置为系统限量。
成功的话,msgget返回非负的队列ID。这个值然后和其它三个消息队列函数一起使用。
msgctl函数在一个队列上执行各种操作。这个函数和相关的信号量和共享内存的函数(semctl和shmctl)都是XSI IPC的类ioctl函数(也就是说,垃圾桶(garbage-can)函数)。
-
#include <sys/msg.h>
-
-
int msgctl (int msqid, int cmd, struct
msqid_ds *buf);
-
- 成功返回0,错误返回-1。
cmd参数指定要在由msqid指定的队列上执行的命令。
IPC_STAT:得到这个队列的msqid_ds结构体,把它存储在buf指向的结构体里。
IPC_SET: 把buf指向的结构体的以下域拷贝到和这个队列相关的msqid_ds结构体里:msg_perm.uid、msg_perm.gid、 msg_perm.mode、和msg_qbytes。这个命令只能被一个有效用户ID等于msg_perm.cuid或msg_perm.uid或有超 级用户权限的进程执行。只有超级用户可以增加msg_qbytes的值。
IPC_RMID:从系统中删除这个消息队列和任何仍在这个队列上 的数据。这个删除是立即的。任何仍在使用这个消息队列的进程在下次尝试在这个队列上操作时会得到一个EIDRM错误。这个命令只能被有效用户ID等于 msg_perm.cuid或msg_perm.uid或有超级用户权限的进程执行。
我们将看到这三个命令(IPC_STAT、IPC_SET和IPC_RMID)也为信号量和共享内存提供。
数据被放到一个消息队列,通过调用msgsnd。
-
#include <sys/msg.h>
-
-
int msgsnd(int msqid, const void *ptr, size_t
nbytes, int flag);
-
- 成功返回0,错误返回-1。
正如我们之前描述的,每个消息由一个正的长整型类型域、一个非负长度(nbytes)、和真实数据字节(对应于长度)组成。消息总是被放到队列末尾。
ptr参数指向一个包含正整型消息类型的长整型,后面紧接着消息数据。(如果nbytes为0则没有消息数据。)如果我们发送的最大的消息是152字节,那么我们可以定义如下结构体:
struct mymesg {
long mtype; /* positive message type */
char mtext[512]; /* message data, of length nbytes */
};
ptr参数然后是一个指向mymesg结构体的指针。消息类型可以被接收者用来以不是先进先出的顺序得到消息。
一 些平台同时支持32位和64位环境。这影响了长整型和指针的尺寸。例如在64位SPARC系统上,Solaris允许32位和64位应用共存。如果一个 32位应用是通过一个管道或套接字和一个64位应用交换数据,那么会出现问题,因为长整型尺寸在32位应用里是4字节,而在64位应用里是8字节。这表示 一个32位应用将会期望mtext域在结构体开始后的8字节处开始。在这种情况里,64位应用的mtype域的一部分将作为32位应用的mtext域的一 部分出现,32位应用的mtext域的前4个字节将被64位应用解释为mtype域的一部分。
然而这个问题不会在XSI消息队列里出现。 Solaris实现32位版本的IPC系统调用,用和64位版本的IPC系统调用不同的进入点。这些系统调用知道如何处理32位应用和64位应用之间的通 信,并特殊对待类型域以避免消息的数据部分的影响。唯一潜在的问题是信息丢失,当一个64位应用发送一个消息,使用8字节的类型域的值,它比32位应用的 4字节域的所能容的值大。在这种情况下,32位应用会看到一个裁切的类型值。
一个IPC_NOWAIT的flag值可以被指定。这和文件 I/O的非阻塞I/O标志(14.2节)相似。如果消息队列满了(队列上的消息数等于系统限量,或者队列上的字节数等于系统限量),那么指定 IPC_NOWAIT导致msgsnd立即返回一个EAGAIN的错误。如果IPC_NOWAIT没有被指定,那么我们被阻塞,直到有消息的空间、队列在 系统上被删除、或一个信号被捕获且信号处理机返回。在第二种情况里,EIDRM的错误被返回(“标识符被删除”,identifier removed);在最后一情况下,返回的错误是EINTR。
注意一个消息队列被删除是如何被笨拙地处理。因为没有引用计数随同每个消息队 列被维护(如打开的文件有的那样),一个队列的删除在仍使用这个队列的0程在下次队列操作时简单地产生错误。信号量用相同的方式处理这个删除。相比之下, 当一个文件被删除时,文件的内容不会被删除,直到为文件打开的最后的描述符被关闭。
当msgsnd成功返回时,和消息队列相关的msqid_ds结构体被更新,以指定执行这个调用的进程ID(msg_lspid)、调用被执行的时间(msg_stime)、和在队列里还有多了一个的消息(msg_qnum00)。
消息可以通过msgrcv从一个队列得到。
-
#include <sys/msg.h>
-
-
ssize_t msgrcv(int msqid, void *ptr, size_t
nbytes, long type, int flag);
-
- 成功返回消息的数据部分的尺寸,错误返回-1。
和 msgsnd一样,ptr参数指向一个长整型(返回消息的消息类型存在这里)接着一个真实消息数据的一个数据缓冲。nbytes指定数据缓冲的尺寸。如果 返回的消息比nbytes大而MSG_NOERROR位在flag里被设置,那么消息被裁切。(在这种情况下,没有消息告诉我们消息被裁切,以及剩余的消 息被舍弃。)如果消息太大且这个flag值没有被指定,那么E2BIG的错误被返回(而且消息被留在队列上)。
type参数让我们指定想要哪个消息。
type == 0:返回队列上的第一个消息。
type > 0:队列上消息类型等于type的第一个消息被返回。
type < 0:队列上消息类型小于等于type的绝对值的最小值的消息被返回。
一个非零type被用来以不是先进先出的方式来得到消息。例如,type可以是一个优先级值,如果应用分配给消息分配优先级。这个域的另一个用法是包含客户的进程ID,如果单个消息队列被多个客户和单个服务器使用(只要一个进程ID适合放在一个长整型里)。
我 们可以指定一个IPC_NOWAIT的flag值来让操作非阻塞,导致msgrcv返回-1并设置errno为ENOMSG,如果一个指定类型的消息不可 用。如果IPC_NOWAIT没有被指定,那么操作阻塞,直到一个指定类型的消息可用、消息从系统上被删除( 返回-1并设置errno为EIRM)、或一个信号被捕获且信号处理机返回(导致msgrcv返回-1并设置errno为EINTR)。
当msgrcv成功时,内核更新和消息队列相关的msqid_ds结构体来指明调用者的进程ID(msg_lrpid)、调用的时间(msg_rtime)、和在队列上的更少一个的消息(msg_qnum)。
例子--消息队列和流管道的计时比较
如果我们需要客户和服务器间双向流动的数据,那么我们可以使用消息队列或全双工管道。(回想15.1节,全双工管道通过UNIX域套接字机制可用(17.3节),尽管一些平台通过pipe函数提供一个全双工管道机制。)
下 表展示了Solaris上这三种技术的计时比较:消息队列、基于STREAMS的管道、和UNIX域套接字。这些测试由一个创建IPC渠道、调用 fork、然后从父进程到子进程发送大约200M的数据的程序组成。数据使用100000个msgsnd的调用发送,为消息队使用长度为2000字节的消 息。以及100000次的write调用,为基于STREAMS的管道和UNIX域套接字使用长度为2000字节的数据。计时都以秒为单位。
操作 | 用户 | 系统 | 时钟 |
---|---|---|---|
消息队列 | 0.57 | 3.63 | 4.22 |
STREAMS管道 | 0.50 | 3.21 | 3.71 |
UNIX域套接字 | 0.43 | 4.45 | 5.59 |
这些数值向我们展示了消息队列,最开始的实现是为了提供高于常速的IPC,已经不再比其它形式的IPC快很多了(事实上,基于STREAMS的管道比消息队 列要快)。(当消息队列被实现时,另一个可用的唯一的IPC形式是半双工管道。)当我们考虑使用消息队列的问题时(15.6.4节),我们得到结论:不应 该在新的应用里使用它们。