System V消息队列

本文介绍了System V消息队列的创建与打开、发送和接收消息的过程,以及如何控制消息队列。相比于管道和FIFO,消息队列通过消息进行通信,具有type字段,允许按类型或优先级读取。此外,文章还详细讲解了msgget、msgsnd、msgrcv和msgctl等函数的使用,并探讨了消息队列的容量限制和管理策略。

管道和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函数所需的步骤:

  1. 因为glibc没有定义msgbuf结构体,因此首先要定义msgbuf结构体
  2. 分配一个类型为msgbuf,长度足以容纳字符串的缓冲区mbuf。
  3. 将message的内容拷贝到mbuf->mtext中去。
  4. 在mbuf->mtype中设置消息类型
  5. 调用msgsnd发送消息
  6. 释放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支持的操作

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值