LINUX 消息队列的容量探讨

本文探讨了Linux中POSIX消息队列的使用,包括通过mq_open函数创建消息队列,并讨论了消息队列的数量限制、单条消息的最大字节数以及如何通过mq_attr设置属性。文章提到了RLIMIT_MSGQUEUE限制,它是控制用户在POSIX消息队列中可分配的最大字节数,以及如何通过ulimit或setrlimit进行调整。

在LINUX进程间通信方法有很多种,其中有消息队列、信号量、共享内存,而这三种进程间通信方法又分为System V IPC 和 POSIX IPC 两类。

接下来要探讨的是POSIX的消息对列(实际编程中用得相对多一些)。


先将POSIX 消息对列函数列表放出来:

#include <mqueue.h>
//打开/创建一个消息队列
mqd_t mq_open(const char *name, int oflag, ...)
mqd_t mq_open(const char *name, int oflag, mode_t mode, mq_attr* attr)  
//关闭消息队列
int mq_close(mqd_t mqdes); 
//销毁消息队列
int mq_unlink(const char *name);
//发送消息 
int mq_send(mqd_t mqdes, const char *msg_ptr, size_t msg_len, unsigned msg_prio);
//接收消息 
ssize_t mq_receive(mqd_t mqdes, char *msg_ptr, size_t msg_len, unsigned *msg_prio);
//通知进程获取消息 
int mq_notify(mqd_t mqdes, const struct sigevent *sevp);
//获取消息属性
int mq_getattr(mqd_t mqdes, struct mq_attr *attr); 
//设置消息属性
int mq_setattr(mqd_t mqdes, struct mq_attr *newattr, struct mq_attr *oldattr);

mq_open用于打开或创建一个消息队列,通过一个消息队列描述符(类型是mqd_t)建立一个进程与一个消息队列的连接.

代码片段:

mqd = mq_open("mq_0", flags, S_IRUSR | S_IWUSR, NULL);
if(mqd == -1)
{
    fprintf(stderr, "mq_open failed (%s)\n", strerror(errno));
    return -2;
}

while(1)
    sleep(11);
mq_close(mqd);

在/proc/PID/fd目录下,也可以看到消息队列对应的文件描述符:

可以看到,消息队列mq_0已经创建出来了。

那么,一个进程允许我们创建多少个消息队列呢?每个消息队列携带的消息的最大字节数是多少呢?

其实标准并没有严格限定,这点是由具体的实现来决定的。SUSv3 标准要求这个限制最小为_POSIX_MQ_OPEN_MAX(8)。
Linux没有定义这个限制。在LINUX中,因为消息描述符被实现成了文件描述符,因此其必须遵循文件描述符的限制。

进程允许打开的消息队列个数是否仅仅受限于进程打开的最大文件个数?事实上并非如此。资源限制中有一项RLIMIT_MSGQUEUE,用于限制用户在POSIX消息队列中可以分配的最大字节数。这个后面再讲。

mq_open函数第四个参数是mq_attr类型的,表示消息队列的属性。创建时可以指定消息队列的属性,设置为NULL则采用系统默认属性值,POSIX消息队列也提供 了mq_setattr函数来改变消息队列的属性。


属性类型定义:

/include/uapi/linux/mqueue.h

struct mq_attr {
    __kernel_long_t    mq_flags;    /* message queue flags            */
    __kernel_long_t    mq_maxmsg;    /* maximum number of messages        */
    __kernel_long_t    mq_msgsize;    /* maximum message size            */
    __kernel_long_t    mq_curmsgs;    /* number of messages currently queued    */
    __kernel_long_t    __reserved[4];    /* ignored for input, zeroed for output */
};

 .mq_flags:0或设置了O_NONBLOCK。 
·mq_maxmsg:消息队列中的最大消息个数。 
·mq_msgsize:单条消息允许的最大字节数。 
·mq_curmsgs:消息队列当前的消息个数。


可以使用如下代码来获取默认属性:

int ret = mq_getattr(mqd, &attr);
if(ret != 0)
{
    fprintf(stderr, "failed to get attr(%d: %s)\n", errno, strerror(errno));
    return 2;
}
fprintf(stdout, "the default mq_maxmsg = %ld\nthe default mq_msgsize = %ld\n", attr.mq_maxmsg, attr.mq_msgsize);


其输出如下: 

the default mq_maxmsg = 10 
the default mq_msgsize = 8192 

从输出可以看出,默认情况下,消息队列的最大消息数为10,单条消息的最大字节数为8192字节。


我们也可以从系统文件中查看,其中消息队列的相关信息记录在如下位置:

其中消息队列的最大消息数的默认值记录在msg_default:

cat /proc/sys/fs/mqueue/msg_default


 默认每个消息队列最大可以有10条消息。

单条消息的最大字节数的默认值记录在如下位置: 

cat /proc/sys/fs/mqueue/msgsize_default


 单条消息的最大字节数8192。

消息队列中只能存放10条消息,这明显太少了,此外单条消息的最大字节数8192可能也无法满足我们的需要。因此创建消息队列的时候需 要定制属性,定制方法如下所示:

attr.mq_maxmsg = atoi(argv[2]);
attr.mq_msgsize = atoi(argv[3]);

mqd_t mqd = mq_open(argv[1], O_RDWR | O_CREAT | O_EXCL, S_IRUSR | S_IWUSR, &attr);
if(mqd == -1)
{
    fprintf(stderr, "failed to get mqueue (%d: %s)\n", errno, strerror(errno))    ;
    return 1;
}

但是消息队列的最大消息数和单条消息的最大字节数并不能被随意指定。它受限于多个控制选项。 


对于普通用户(非特权用户)而言,内核提供了两个控制选项:

cat /proc/sys/fs/mqueue/msg_max 
10
cat /proc/sys/fs/mqueue/msgsize_max 
8192

这两个值分别是最大消息数的上限和单条消息最大字节数的上限。

普通用户在定制消息队列属性的时候不能超越这个上限。这两条限制是 针对普通用户而言的,对于特权用户而言可以忽视这两条限制。 

很明显,这个上限值并不大,特权用户可以调整这两项的值:

sysctl -w fs.mqueue.msg_max=4096 
fs.mqueue.msg_max = 4096 


 可以看到最大消息数已改成了4096, 单条消息最大字节数也一样的修改方法如下:

sysctl -w fs.mqueue.msgsize_max=65536 
fs.mqueue.msgsize_max = 65536


 当然也可以直接修改这两个文件,如下:

echo 1024 > /proc/sys/fs/mqueue/msg_max
cat /proc/sys/fs/mqueue/msg_max
1024

 
但是这两个最大值(msg_max, msgsize_max)不能随意设置上限值,
对于/proc/sys/fs/mqueue/msg_max,系统提供了硬上限HARD_MSGMAX,
对于/proc/sys/fs/mqueue/msgsize_max,系统也提供了硬上限HARD_MSGSIZEMAX:

/include/linux/ipc_namespace.h    [内核5.4.56]

通过调整对应的控制选项,可以让消息队列容纳更多的消息,或者让每条消息可以容纳更多的内容。 
理论上总消息数不超过65536,每条消息长度不超过16M字节就行,可是事实上,除了上述控制选项外,还存在其他限制。

将调整msg_max控制选项到4096,调整msgsize_max控制选项到65536字节:

 写一个测试程序:

#include <errno.h>
#include <sys/types.h>
#include <sys/times.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <time.h>
#include <utime.h>
#include <dirent.h>
#include <signal.h>
#include <sys/wait.h>
#include <pthread.h>
#include <sched.h>
#include <mqueue.h>

int main(int argc , char** argv)
{
	int flags;
	mqd_t mqd;
	flags = O_RDWR | O_CREAT;

	struct mq_attr attr;
	attr.mq_maxmsg = 4096;
	attr.mq_msgsize = 64;

	char ia[10] = {"\0"};
	sprintf(ia, "/mq_%d", 1);
	mqd = mq_open(ia, flags, S_IRUSR | S_IWUSR, &attr);
	if(mqd == -1) {
		fprintf(stderr, "mq_open failed (%s)\n", strerror(errno));
		return -2;
	}
	fprintf(stdout, "success, %s\n", ia);

	if(mq_getattr(mqd, &attr) != 0) {
		fprintf(stderr, "failed to get attr(%d,%s)\n", errno, strerror(errno));
		return 2;
	}

	fprintf(stdout, "the default mq maxmsg=%ld\nthe defult mq_msglen=%ld\nmq_curmsgs=%ld\n", attr.mq_maxmsg, attr.mq_msgsize, attr.mq_curmsgs);

	mq_close(mqd);
	mq_unlink(ia);
	return 0;
}

设置消息个数和长度:
attr.mq_maxmsg = 4096;
attr.mq_msgsize = 64;
运行:

修改消息个数和长度:
attr.mq_maxmsg = 2;
attr.mq_msgsize = 65536;

重新编译运行:

 修改消息个数和长度:
attr.mq_maxmsg = 4096;
attr.mq_msgsize = 65536;

重新编译运行:

 运行失败!


调整msg_max控制选项到4096,调整msgsize_max控制选项到65536字节,那么可以创建出能容纳4096条消息,每条消息的最大长度为64字节的消息队列;也可以创建出只容纳2条消息,每条消息最大长度为65536字节的消息 队列。但是无法创建出既可以容纳4096条消息,每条消息的最大长度又为65536字节的消息队列。

这表明除了上述两条控制外,还存在其他限制。

该限制就是介绍mq_open时提到的RLIMIT_MSGQUEUE。RLIMIT_MSGQUEUE属于资源限制的范畴。它限制了用户可以在POSIX消息队列中 分配的最大字节数。

注意不是单个消息队列的最大字节数,也不是一个进程能分配的最大字节数,而是该用户创建的所有的消息队列的最大字 节数。如果新建消息队列会导致所有消息队列的字节数超出此限制,那么调用mq_open函数时会返回EMFILE错误。

在内核中搜索RLIMIT_MSGQUEUE,可查到:

/include/uapi/asm-generic/resource.h

 进一步搜索相关引用:

/include/asm-generic/resource.h

/*
 * boot-time rlimit defaults for the init task:
 */
#define INIT_RLIMITS                            \
{                                    \
    [RLIMIT_CPU]        = {  RLIM_INFINITY,  RLIM_INFINITY },    \
    [RLIMIT_FSIZE]        = {  RLIM_INFINITY,  RLIM_INFINITY },    \
    [RLIMIT_DATA]        = {  RLIM_INFINITY,  RLIM_INFINITY },    \
    [RLIMIT_STACK]        = {       _STK_LIM,  RLIM_INFINITY },    \
    [RLIMIT_CORE]        = {              0,  RLIM_INFINITY },    \
    [RLIMIT_RSS]        = {  RLIM_INFINITY,  RLIM_INFINITY },    \
    [RLIMIT_NPROC]        = {              0,              0 },    \
    [RLIMIT_NOFILE]        = {   INR_OPEN_CUR,   INR_OPEN_MAX },    \
    [RLIMIT_MEMLOCK]    = {    MLOCK_LIMIT,    MLOCK_LIMIT },    \
    [RLIMIT_AS]        = {  RLIM_INFINITY,  RLIM_INFINITY },    \
    [RLIMIT_LOCKS]        = {  RLIM_INFINITY,  RLIM_INFINITY },    \
    [RLIMIT_SIGPENDING]    = {         0,           0 },    \
    [RLIMIT_MSGQUEUE]    = {   MQ_BYTES_MAX,   MQ_BYTES_MAX },    \
    [RLIMIT_NICE]        = { 0, 0 },                \
    [RLIMIT_RTPRIO]        = { 0, 0 },                \
    [RLIMIT_RTTIME]        = {  RLIM_INFINITY,  RLIM_INFINITY },    \
}

RLIMIT_MSGQUEUE=12  看上去是个下标,对应的值就是消息队列最大的值,这是在系统启动时的默认值。

/include/uapi/linux/mqueue.h

/* per-uid limit of kernel memory used by mqueue, in bytes */
#define MQ_BYTES_MAX    819200

所有消息队列总字节数不能超过800K。

[RLIMIT_MSGQUEUE]默认为819200字节,可以通过如下指令来查看: 

ulimit -q 819200


再来一个测试,修改消息个数和长度,连续创建10个消息队列:

attr.mq_maxmsg = 10;
attr.mq_msgsize = 8192;

//连续创建10个消息队列:

for(int i = 0; i < 10; i++)
{
    sprintf(ia, "/mq_%d", i);
    mqd = mq_open(ia, flags, S_IRUSR | S_IWUSR, &attr);
    if(mqd == -1) {
        fprintf(stderr, "mq_open failed (%s)\n", strerror(errno));
        return -2;
    }
    fprintf(stdout, "success,%d, %s\n", i, ia);
}

重新编译运行:

 默认情况下,单个消息队列最多有10条消息,每条消息的最大字节数为8192。这就意味着该消息队列满载的时候,会占用81920字节。 按照RLIMIT_MSGQUEUE的含义,应该可以创建10个消息队列,可是为什么却只能创建9个默认属性的消息队列呢

原因是消息队列消耗的空间,不能仅仅计算消息体(payload),还要考虑额外的开销。可以从内核的mqueue_get_inode函数中找到答案:

static struct inode *mqueue_get_inode(struct super_block *sb,
        struct ipc_namespace *ipc_ns, umode_t mode,
        struct mq_attr *attr)
{
    struct user_struct *u = current_user();
    struct inode *inode;
    int ret = -ENOMEM;

    inode = new_inode(sb);
    if (!inode)
        goto err;

    inode->i_ino = get_next_ino();
    inode->i_mode = mode;
    inode->i_uid = current_fsuid();
    inode->i_gid = current_fsgid();
    inode->i_mtime = inode->i_ctime = inode->i_atime = current_time(inode);

    if (S_ISREG(mode)) {
        struct mqueue_inode_info *info;
        unsigned long mq_bytes, mq_treesize;

        inode->i_fop = &mqueue_file_operations;
        inode->i_size = FILENT_SIZE;
        /* mqueue specific info */
        info = MQUEUE_I(inode);
        spin_lock_init(&info->lock);
        init_waitqueue_head(&info->wait_q);
        INIT_LIST_HEAD(&info->e_wait_q[0].list);
        INIT_LIST_HEAD(&info->e_wait_q[1].list);
        info->notify_owner = NULL;
        info->notify_user_ns = NULL;
        info->qsize = 0;
        info->user = NULL;    /* set when all is ok */
        info->msg_tree = RB_ROOT;
        info->msg_tree_rightmost = NULL;
        info->node_cache = NULL;
        memset(&info->attr, 0, sizeof(info->attr));
        info->attr.mq_maxmsg = min(ipc_ns->mq_msg_max,
                       ipc_ns->mq_msg_default);
        info->attr.mq_msgsize = min(ipc_ns->mq_msgsize_max,
                        ipc_ns->mq_msgsize_default);
        if (attr) {
            info->attr.mq_maxmsg = attr->mq_maxmsg;
            info->attr.mq_msgsize = attr->mq_msgsize;
        }
        /*
         * We used to allocate a static array of pointers and account
         * the size of that array as well as one msg_msg struct per
         * possible message into the queue size. That's no longer
         * accurate as the queue is now an rbtree and will grow and
         * shrink depending on usage patterns.  We can, however, still
         * account one msg_msg struct per message, but the nodes are
         * allocated depending on priority usage, and most programs
         * only use one, or a handful, of priorities.  However, since
         * this is pinned memory, we need to assume worst case, so
         * that means the min(mq_maxmsg, max_priorities) * struct
         * posix_msg_tree_node.
         */

        ret = -EINVAL;
        if (info->attr.mq_maxmsg <= 0 || info->attr.mq_msgsize <= 0)
            goto out_inode;
        if (capable(CAP_SYS_RESOURCE)) {
            if (info->attr.mq_maxmsg > HARD_MSGMAX ||
                info->attr.mq_msgsize > HARD_MSGSIZEMAX)
                goto out_inode;
        } else {
            if (info->attr.mq_maxmsg > ipc_ns->mq_msg_max ||
                    info->attr.mq_msgsize > ipc_ns->mq_msgsize_max)
                goto out_inode;
        }
        ret = -EOVERFLOW;
        /* check for overflow */
        if (info->attr.mq_msgsize > ULONG_MAX/info->attr.mq_maxmsg)
            goto out_inode;
        mq_treesize = info->attr.mq_maxmsg * sizeof(struct msg_msg) +
            min_t(unsigned int, info->attr.mq_maxmsg, MQ_PRIO_MAX) *
            sizeof(struct posix_msg_tree_node);
        mq_bytes = info->attr.mq_maxmsg * info->attr.mq_msgsize;
        if (mq_bytes + mq_treesize < mq_bytes)
            goto out_inode;
        mq_bytes += mq_treesize;
        spin_lock(&mq_lock);
        if (u->mq_bytes + mq_bytes < u->mq_bytes ||
            u->mq_bytes + mq_bytes > rlimit(RLIMIT_MSGQUEUE)) {
            spin_unlock(&mq_lock);
            /* mqueue_evict_inode() releases info->messages */
            ret = -EMFILE;
            goto out_inode;
        }
        u->mq_bytes += mq_bytes;
        spin_unlock(&mq_lock);

        /* all is ok */
        info->user = get_uid(u);
    } else if (S_ISDIR(mode)) {
        inc_nlink(inode);
        /* Some things misbehave if size == 0 on a directory */
        inode->i_size = 2 * DIRENT_SIZE;
        inode->i_op = &mqueue_dir_inode_operations;
        inode->i_fop = &simple_dir_operations;
    }

    return inode;
out_inode:
    iput(inode);
err:
    return ERR_PTR(ret);
}

从上面的代码不难看出,一个消息队列消耗的总空间为: 
mq_treesize = info->attr.mq_maxmsg * sizeof(struct msg_msg) +
            min_t(unsigned int, info->attr.mq_maxmsg, MQ_PRIO_MAX) *
            sizeof(struct posix_msg_tree_node);

        mq_bytes = info->attr.mq_maxmsg * info->attr.mq_msgsize;

        mq_bytes += mq_treesize;

总字节数里被mq_treesize占用了一部分,因此当RLIMIT_MSGQUEUE的值为819200字节时,单个用户是无法创建出10个默认属性的消息队列的。

注意 上述计算消息队列消耗空间的计算公式仅仅适用于某些内核版本,并不能当成绝对的公式。对于不同的内核版本,需要查看 内核的mqueue_get_inode函数来确定。

要想让消息队列中容纳足够多的消息,每条消息也足够大,那就需要同时修改RLIMIT_MSGQUEUE的值。可以通过ulimit命令来修改,也可 以通过setrlimit函数来修改。

查看一下命令

ulimit -a

 

设置消息队列资源上限值[RLIMIT_MSGQUEUE]

ulimit -q 835584   //819200+8192*2


重新运行一下程序:

 看,现在能创建10个消息队列,每个消息队列最大有10个消息 ,每个消息长度为8192字节。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值