管道和FIFO都是字节流的模型,这种模型不存在记录边界。如果从管道里面读出100字节,你无法确认这100字节是单次写入的100字节,还是分10次写入的。管道或FIFO里的数据如何解读,完全取决于写入进程和读取进程之间的约定。
从这个角度上讲,System V消息队列和POSIX消息队列都是由于管道和FIFO的。原因是消息队列机制中,双方是通过消息来通信的,无需花费精力从字节流中解析出完整的消息。
System V消息队列比管道或FIFO优越的第二个地方在于每条消息都有type字段,消息的读取进程可以通过type字段来选择自己感兴趣的消息,也可以根据type字段来实现按消息的优先级进行读取,而不一定要按照消息生成的顺序依次读取。
内核为每一个System V消息队列分配了一个msg_queue类型的结构体,其成员变量和各自的含义如下所示:
struct msg_queue {
struct kern_ipc_perm q_perm;
time_t q_stime; /* 上一次 msgsnd的时间*/
time_t q_rtime; /* 上一次 msgrcv的时间 */
time_t q_ctime; /* 属性变化时间 */
unsigned long q_cbytes; /* 队列当前字节总数*/
unsigned long q_qnum; /*队列当前消息总数*/
unsigned long q_qbytes; /*一个消息队列允许的最大字节数*/
pid_t q_lspid; /*上一个调用msgsnd的进程ID*/
pid_t q_lrpid; /*上一个调用msgrcv的进程ID*/
struct list_head q_messages;
struct list_head q_receivers;
struct list_head q_senders;
};
1. 创建或打开一个消息队列
消息队列的创建或打开是由msgget函数来完成的,成功后,获得消息队列的标识符ID,函数接口定义如下:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgget(key_t key, int msgflg)
当调用成功时,返回消息队列的标识符,后续的msgsnd、msgrcv和msgctl函数都通过该标识符来操作消息队列。当函数调用失败时,返回-1,并且设置相应的errno。
关于创建消息队列,一个很容易想到的问题是:操作系统到底允许创建多少个消息队列?
有三种方法可以查看系统消息队列个数的上限,如下所示。
方法一:通过procfs查看:
cat /proc/sys/kernel/msgmni
3969
方法二:通过sysctl查看:
sysctl kernel.msgmni
kernel.msgmni = 3969
方法三:通过ipcs命令查看:
ipcs -q -l
------ Messages Limits --------
max queues system wide = 3969
max size of message (bytes) = 8192
default max size of queue (bytes) = 16384.
操作系统会根据系统的硬件情况(主要是内存大小),计算出一个合理的上限值,因此不同的硬件环境下,该值是不同的。当然无论该值设置为多少,内核都存在硬上限IPCMNI(32768)
当然可以通过如下的手段,修改msgmni的值,从而允许创建更多的消息队列。
方法一:通过procfs来修改
echo 20000 > /proc/sys/kernel/msgmni
cat /proc/sys/kernel/msgmni
20000
方法二:通过sysctl-w来修改
sysctl -w kernel.msgmni=20000
上述两种方法都是立即生效,但是一旦系统重启,设置就失去了。要想确保重启后依然有效,需要将配置写入/etc/sysctl.conf。
kernel.msgmni=20000
注意写入/etc/sysctl.conf并不会立即生效,需要执行sysctl-p重新加载,才能生效。
2. 发送消息
获取到消息队列的标识符之后,可以通过调用msgsnd函数向队列插入消息。内核会负责将消息维护在消息队列中,等待另外的进程取走消息,从而完成通信的全过程。
msgsnd函数的定义如下:
#include <sys/types.h>
#include <sys/ipc.h>
include <sys/msg.h>
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
其中msgqid是由msgget返回的标识符ID。
参数msgp指向用户定义的缓冲区。它的第一个成员必须是一个指定消息类型的long型,后面跟着消息文本的内容。通常其定义如下:
struct msgbuf {
long mtype; /*消息类型,必须大于0*/
char mtext[1]; /*消息体,不一定是字符数组,可以是任意结构*/
};
每条消息只能存放一个字符?并非如此。事实上可以是任意结构,mtext是由程序员定义的结构,其长度和内容都是程序员控制的,只要发送方很接收方约定好即可。比如可以将结构体定义如下:
struct private_buf {
long mtype;
struct pirate_info {
/*定义你需要的成员变量*/
} info;
};
第三个参数msgsz制定了mtetx字段中包含的字节数。消息队列单条消息的大小是有上限的,上限值为MSGMAX,,记录在/proc/sys/kernel/msgmax中:
cat /proc/sys/kernel/msgmax
8192
sysctl kernel.msgmax
kernel.msgmax = 8192
如果消息的长度超过了MSGMAX,那么msgsnd函数返回-1,并置errno为EINVAL。
下面以发送字符串消息为例,介绍msgsnd函数所需的步骤:
- 因为glibc没有定义msgbuf结构体,因此首先要定义msgbuf结构体
- 分配一个类型为msgbuf,长度足以容纳字符串的缓冲区mbuf。
- 将message的内容拷贝到mbuf->mtext中去。
- 在mbuf->mtype中设置消息类型
- 调用msgsnd发送消息
- 释放mbuf
注意两点,既要对msgsnd进行错误检测和及时释放mbuf,以防止内存泄漏。
最后一个参数msgflg是一组标志位的位掩码,用于控制msgsnd的行为。目前只定义了IPC_NOWAIT一个标志位。
IPC_NOWAIT表示执行一个无阻塞的发送操作。当没有设置IPC_NOWAIT标志位,如果消息队列满了,那么msgsnd函数就会陷入阻塞,直到队列有足够的空间来存放这条消息为止。但是如果设置了IPC_NOWAIT标志位,那么msgsnd函数就不会陷入阻塞了,而是立刻返回失败,并置errno为EAGAIN。
那么什么情况下,消息队列才能被称为是满的?
任何一个消息队列,容纳的字节数是有有上限的。这个上限值为MSGMNB,该值被记录在/proc/sys/kernel/msgmnb中:
cat /proc/sys/kernel/msgmnb
16384
sysctl kernel.msgmnb
kernel.msgmnb = 16384
内核中消息队列对应的数据结构msg_queue中维护有当前字节数、当前消息数及允许的最大字节数等信息:
struct msg_queue{
...
time_t q_stime; /*最后调用msgsnd的时间*/
unsigned long q_cbytes; /*消息队列当前字节的总数*/
unsigned long q_qnum; /*消息队列当前消息的个数*/
unsigned long q_qbytes; /*消息队列允许的消息最大字节数*/
pid_t q_lspid; /*最后调用msgsnd的进程ID*/
...}
检查消息队列是否满的逻辑非常简单,内核判断能否立刻发送消息的逻辑如下:
if (msgsz + msq->q_cbytes <= msq->q_qbytes &&
1 + msq->q_qnum <= msq->q_qbytes) {
break;
}
3. 接收消息
有发送就要有接收,没有接收者的消息是没有意义的。System V消息队列用msgrcv函数来接收消息。
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz,
long msgtyp,int msgflg);
其中前三个参数与msgsnd的含义是一致的。msgrcv调用进程也需要定义结构体,而结构体的定义要和发送端的定义一致,并且第一个字段必须是long类型,代码如下所示:
struct private_buf {
long mtype;
struct pirate_info {
/*定义你需要的成员变量*/
} info;
};
对于具有固定长度的消息体来讲,只要发送方和接收方的结构体达成一致,就不会存在风险。但是如果消息体是变长的,情况就复杂了点。因为不能预先得知收到消息体的长度,因此接收端的缓冲区要足够大,防止消息队列中的消息长度大于缓冲区的大小。
msgrcv函数的第四个参数msgtyp是消息队列的精华,提取消息时,可以选择进程感兴趣的消息类型。正是基于这个参数,读取消息的顺序才无须和发送顺序一致,然后就可以演化出很多用法。
第5个参数是可选标志位。msgrcv函数有3个可选标志位。
·IPC_NOWAIT:如果消息队列中不存在满足msgtyp要求的消
息,默认情况是阻塞等待,但是一旦设置了IPC_NOWAIT标志位,
则立即返回失败,并且设置errno为ENOMSG。
·MSG_EXCEPT:这个标志位是Linux特有的,只有当msgtyp大
于0时才有意义,含义是选择mtype!=msgtyp的第一条消息。
·MSG_NOERROR:前面也提到过,在消息体变长的情况下,可
能事前并不知道消息体的大小,尽管要求maxmsgsz应尽可能地大,
但是仍然存在maxmsgsz小于消息体大小的可能。如果发生这种情
况,默认情况是返回错误E2BIG,但是如果设置了MSG_NOERROR
标志位,情况就不同了,此时会将消息体截断并返回。
msgrcv函数调用成功时,返回消息体的大小;失败时返回-1,并且设置errno。另外msgrcv函数和msgsnd函数一样,如果被信号中断,则不会重启系统调用,哪怕安装信号时设置了SA_RESTART标志位。
System V消息队列存在一个问题,即当消息队列中有消息到来时,无法通知到进程。消息队列的读取者进程,要么以阻塞的方式调用msgrcv函数,阻塞在消息队列上直到消息出现;要么以非阻塞(IPC_NOWAIT)的方式调用msgrcv函数,失败返回,过段时间再重试。
4. 控制消息队列
msgctl函数可以控制消息队列的属性,其接口定义如下:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
该函数提供的功能取决于cmd字段,msgctl支持的操作