进程间通信方式(二):System V 消息队列

本文深入解析SystemV消息队列,涵盖概念、特点、数据结构及内核表示,详细讲解msgget、msgctl、msgsnd、msgrcv等核心函数,并提供综合应用实例,帮助读者掌握消息队列的使用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、概念

  消息队列又叫做报文队列,是一条由消息连接而成的链式队列,它保存在内核中,并使用消息队列标识符进行标识。
  消息队列提供了一种从一个进程向另一个进程发送一个数据块的方法,并且每个数据块都被认为含有一个类型,接收进程可以独立地接收含有不同类型的数据块。对消息队列有写权限的进程可以向消息队列中按照一定的规则添加新消息,对消息队列有读权限的进程则可以从消息队列中读走消息。
  目前主要有两种类型的消息队列:System V 消息队列POSIX 消息队列,本文仅对前者展开讲述。

二、特点

  1. 消息队列中的每一个消息都具有特定的格式以及特定的优先级

  2. 消息队列在内核中使用特定的标识符进行标识,从而保证了消息队列在整个系统中的唯一性,所以可以通过标识符来访问消息队列;

  3. 消息队列具有一定的 FIFO(先入先出)特性,但是它也可以实现消息的随机查询,比 FIFO 具有更大的优势;

  4. System V 消息队列是随内核持续的,只有在内核重启或者显式删除一个消息队列时,该消息队列才会被真正删除;

  5. 在 Linux 中,消息队列有如下限制:

    (1)MSGMNI:系统中允许存在的消息队列的最大数量
    (2)MSGMAX:单个消息的最大字节数
    (3)MSGMNB:单个消息队列的最大总字节容量(所有消息的总和)

    查看方法如下:
    方法一:通过 ipcs -lq 命令查看

    在这里插入图片描述
    方法二:查看系统内核信息

    在这里插入图片描述

三、IPC 对象数据结构

  内核为每个 IPC 对象维护一个数据结构,该结构说明了 IPC 对象的权限和所有者,详细信息可参阅文件 /usr/include/linux/ipc.h

struct ipc_perm {
	__kernel_key_t  key;   /* Key supplied to ***get(2) */
	__kernel_uid_t  uid;   /* Effective UID of owner */
	__kernel_gid_t  gid;   /* Effective GID of owner */
	__kernel_uid_t  cuid;  /* Effective UID of creator */
	__kernel_gid_t  cgid;  /* Effective GID of creator */
	__kernel_mode_t mode;  /* Permissions  */
	unsigned short  seq;   /* Sequence number */
};

  结构中的 mode 类似于创建文件时使用的 mode,但是没有执行权限,具体取值如下:

操作者只读权限只写权限可读可写权限
所有者 (owner)040002000600
用户组 (group)004000200060
其他 (others)000400020006

  当调用 IPC 对象的创建函数(msgget、semget、shmget )时,会对 ipc_perm 结构体中的每一项赋值,在后续的操作中若要修改这几项则调用相应的控制函数(msgctl、semctl、shmctl),需要注意的是,只有超级用户或者创建 IPC 对象的进程才有权改变 ipc_perm 结构的值。

四、消息队列数据结构

  在内核中,每个消息队列都有一个 msqid_ds 结构与其关联,该结构描述了消息队列的一些属性和状态信息等,详细信息可参阅文件 /usr/include/linux/msg.h

struct msqid_ds {
	struct ipc_perm msg_perm;	/* Ownership and permissions */
	struct msg *msg_first;		/* first message on queue,unused  */
	struct msg *msg_last;		/* last message in queue,unused */
	__kernel_time_t msg_stime;	/* last msgsnd time */
	__kernel_time_t msg_rtime;	/* last msgrcv time */
	__kernel_time_t msg_ctime;	/* last change time */
	unsigned long  msg_lcbytes;	/* Reuse junk fields for 32 bit */
	unsigned long  msg_lqbytes;	/* ditto */
	unsigned short msg_cbytes;	/* current number of bytes on queue */
	unsigned short msg_qnum;	/* number of messages in queue */
	unsigned short msg_qbytes;	/* max number of bytes on queue */
	__kernel_ipc_pid_t msg_lspid;	/* pid of last msgsnd */
	__kernel_ipc_pid_t msg_lrpid;	/* last receive pid */
};

五、消息队列在内核中的表示

  在前面,我们说过消息队列是一条由消息连接而成的链式队列,所以也可以说每一个消息都是队列中的一个节点,而每个节点的结构类似于下面这样:

struct msg {
    Type msg_type;
    Length msg_len;
    Data msg_data;
    struct msg *next;
}

  假设现在有一个含有三个消息的消息队列,这些消息的类型分别为 10、20 和 30,长度分别为1字节、2字节和3字节,那么该消息队列在内核中将以如下图所示的结构来表示。
在这里插入图片描述
  从上图来看,系统中的每一个消息队列都有一个唯一的标识符,并且都由一个 msqid_ds 结构来表示,其中 msqid_ds.msg_firstmsqid_ds.msg_last 分别指向这个链式队列的头部和尾部。
  当发送一个消息到该消息队列时,先把发送的消息构造成一个 msg 结构对象,然后再将该对象添加到由 msqid_ds.msg_firstmsqid_ds.msg_last 维护的队列中去;而当从消息队列中接收一个消息时,先根据消息的类型 msg_type 有选择地从队列中遵照 FIFO 原则读取特定类型的消息,再将该消息从队列中删除并修改 msqid_ds 结构对象的数据。

六、消息队列相关函数

1. msgget

  • 【头文件】:#include <sys/types.h>#include <sys/ipc.h>#include <sys/msg.h>
  • 【函数原型】:int msgget(key_t key, int msgflg);
  • 【功能】:创建或打开一个消息队列,这里创建的消息队列数量会受到系统消息队列数量的限制
  • 【参数】:

(1)key:消息队列的键值,其中有一个特殊值 IPC_PRIVATE,它用于创建当前进程的私有消息队列
(2)msgflg:权限标志位,由两部分组成,一部分为IPC对象存取权限(含义同 ipc_perm 中的 mode),另一部分为IPC对象创建模式标志(IPC_CREAT、IPC_EXCL),一般会将这两部分进行 | 运算,从而完成对IPC对象创建的管理

  • 【返回值】:成功返回一个消息队列标识符(非负整数);失败则返回 -1,并将 errno 设置为错误标识符

在这里插入图片描述

创建或打开一个 IPC 对象的逻辑流程图

【示例】:演示 IPC_CREAT|IPC_EXCL 的使用效果
【代码】

#include <stdio.h>    
#include <sys/types.h>    
#include <sys/ipc.h>    
#include <sys/msg.h>    
#include <errno.h>    
    
int main() {    
    extern int errno;    
    
    int msqid = msgget(0x123, IPC_CREAT|IPC_EXCL|0644);                               
    if(msqid == -1) {    
        perror("msgget");    
        printf("errno: %d\n", errno);    
        return -1;    
    }    
    
    printf("msqid: %d\n", msqid);    
    
    return 0;    
} 

【执行结果】

在这里插入图片描述
【分析】
  从执行结果来看,当我们第一次运行程序时,成功创建了消息队列。但是,当第二次运行程序后,发现创建失败,errno 被置为17,也就是 EEXIST,错误信息为 File exists,出现这样的结果是因为我们在第一次程序运行后,该消息队列就已经在系统中存在了,而我们在创建消息队列又使用了 IPC_EXCL,这才导致 msgget 出错返回。

2. msgctl

  • 【头文件】:#include <sys/types.h>#include <sys/ipc.h>#include <sys/msg.h>
  • 【函数原型】:int msgctl(int msqid, int cmd, struct msqid_ds *buf);
  • 【功能】:获取或设置消息队列的有关信息
  • 【参数】:

(1)msqid:消息队列标识符
(2)cmd:对消息队列要采取的动作

取值说明
IPC_STAT读取消息队列的 msqid_ds 结构信息,并将其存储在 buf 指定的地址空间中
IPC_SET设置消息队列的 msqid_ds 结构信息,这些取值来自于 buf 指定地址空间中的参数(需要足够权限)
IPC_RMID将消息队列从内核中删除

(3)buf:描述消息队列 msqid_ds 数据结构的变量

  • 【返回值】:成功返回 0;失败则返回 -1,并将errno设置为错误标识符

【示例】:演示 IPC_STAT 的使用效果
【代码】

#include <stdio.h>    
#include <sys/types.h>    
#include <sys/ipc.h>    
#include <sys/msg.h>    
    
int main() {
    int msqid = msgget(0x123, IPC_CREAT|0644);
    if(msqid == -1) {
        perror("msgget");
        return -1;
    }
    printf("msqid: %d\n", msqid);

    struct msqid_ds buf;
    if(msgctl(msqid, IPC_STAT, &buf) == -1) {
        perror("msgctl");
        return -1;
    }

    printf("key: %#x\n", buf.msg_perm.__key);
    printf("mode: %#o\n", buf.msg_perm.mode);
    
    return 0;
}

【执行结果】

在这里插入图片描述

3. 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);
  • 【功能】:将指定消息添加到已打开消息队列的队尾
  • 【参数】:

(1)msqid:消息队列标识符
(2)msgp:消息结构指针,指向准备发送的消息

消息结构如下:

struct msgbuf {    
    long mtype; //type of message,必须大于0,msgrcv 将利用这个长整数确定消息的类型
    char mtext[1]; //message text,可以设定为更多的字节数但必须小于 MSGMAX
};

(3)msgsz:msgp 所指向消息的正文长度,这个长度不包含保存消息类型的那个 long int 型整数
(4)msgflg:控制着当前消息队列满或到达系统上限时将要发生的事情

取值说明
IPC_NOWAIT若消息无法立即发送(比如当前队列已满),那么函数会立即返回,并把 errno 置为 EAGAIN
0函数进入阻塞状态,直到发送成功为止
  • 【返回值】:成功返回 0;失败则返回 -1,并将errno设置为错误标识符

4. msgrcv

  • 【头文件】:#include <sys/types.h>#include <sys/ipc.h>#include <sys/msg.h>
  • 【函数原型】:ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);
  • 【功能】:从消息队列中取走一条消息,与 FIFO 不同的是,这里可以取走指定类型的消息
  • 【参数】:

(1)msqid:消息队列标识符
(2)msgp:消息结构指针,指向消息接收缓冲区
(3)msgsz:msgp 所指向消息的正文长度,这个长度不包含保存消息类型的那个 long int 型整数
(4)msgtyp:指定接收消息的类型

取值说明
msgtyp>0返回消息队列中第一条类型为 msgtyp 的消息
msgtyp=0返回消息队列中的第一条消息
msgtyp<0返回消息队列中一条类型小于或等于 msgtyp 绝对值的消息,并且在满足条件的所有消息中其又是类型最小的消息

(5)msgflg:控制着队列中没有相应类型的消息可供接收时将要发生的事

取值说明
MSG_NOERROR若返回的消息大小超过 msgsz,那么消息就会截短到 msgsz 字节,并且不会报错
IPC_NOWAIT若在消息队列中没有相应类型的消息可以接受,那么函数会立即返回,并把 error 置为 ENOMSG
0函数进入阻塞状态,直到接收到一条相应类型的消息为止
  • 【返回值】:成功返回实际放到接收缓冲区中的字节数;失败则返回 -1,并将errno设置为错误标识符

七、综合应用

【示例】:利用消息队列实现服务端与客户端通信
【代码】

/*
 * 服务端:server.c
 */
 
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <unistd.h>
#include <string.h>

#define PATHNAME "."
#define PROJ_ID 123

#define SERVER_TYPE 1
#define CLIENT_TYPE 2

int main() {
    //生成IPC键值
    key_t key = ftok(PATHNAME, PROJ_ID);
    if(key == -1) {
        perror("ftok");
        return -1;
    }

    //创建并打开消息队列
    int msqid = msgget(key, IPC_CREAT|IPC_EXCL|0644);
    if(msqid == -1) {
        perror("msgget");    
        return -1;    
    }    
    
    //利用消息队列通信    
    int count = 3; //通信次数    
    while(count--) {    
        struct {    
            long mtype;    
            char mtext[32];               
        } sndmsg, rcvmsg;                   
                                                                                      
        //从消息队列中读取客户端消息    
        printf("# Please wait ...\n");    
        if(msgrcv(msqid, &rcvmsg, sizeof(rcvmsg.mtext), CLIENT_TYPE, 0) == -1) {
            perror("msgrcv");
           	return -1;
        }
        printf("> Client: %s", rcvmsg.mtext);

        //向消息队列中添加服务端消息
        printf("# Please input: ");
        fflush(stdout);
        char text[32] = {0};
        if(read(0, text, sizeof(text)) == -1) {
            perror("read");
            return -1;
        }
        sndmsg.mtype = SERVER_TYPE;
        strcpy(sndmsg.mtext, text);
        if(msgsnd(msqid, &sndmsg, sizeof(sndmsg.mtext), 0) == -1) {
            perror("msgsnd");
            return -1;
        }
    }

    return 0;
}
/*
 * 客户端:client.c
 */

#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <unistd.h>
#include <string.h>

#define PATHNAME "."
#define PROJ_ID 123

#define SERVER_TYPE 1
#define CLIENT_TYPE 2

int main() {
    //生成IPC键值
    key_t key = ftok(PATHNAME, PROJ_ID);
    if(key == -1) {
        perror("ftok");
        return -1;
    }

    //打开消息队列
    int msqid = msgget(key, 0);
    if(msqid == -1) {
        perror("msgget");
        return -1;
    }

    //利用消息队列通信
    int count = 3; //通信次数
    while(count--) {    
        struct {    
            long mtype;    
            char mtext[32];    
        } sndmsg, rcvmsg;    
                                                                                      
        //向消息队列中添加客户端消息    
        printf("# Please input: ");    
        fflush(stdout);    
        char text[32] = {0};
        if(read(0, text, sizeof(text)) == -1) {    
            perror("read");
            break;
        }
        sndmsg.mtype = CLIENT_TYPE;
        strcpy(sndmsg.mtext, text);
        if(msgsnd(msqid, &sndmsg, sizeof(sndmsg.mtext), 0) == -1) {
            perror("msgsnd");
            break;
        }

        //从消息队列中读取服务端消息
        printf("# Please wait ...\n");
        if(msgrcv(msqid, &rcvmsg, sizeof(rcvmsg.mtext), SERVER_TYPE, 0) == -1) {
            perror("msgrcv");
            break;                      
        }
        printf("> Server: %s", rcvmsg.mtext);
    }

    //删除消息队列
    if(msgctl(msqid, IPC_RMID, NULL) == -1) {
        perror("msgctl");
        return -1;
    }

    return 0;
}

【执行结果】

在这里插入图片描述




【知识点扩展】

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值