进程间通信之消息队列
目录
四个关键函数
基本原理
int msgget(key_t key, int msgflg)
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg)
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg)
int msgctl(int msqid, int cmd, struct msqid_ds *buf)
示例代码
测试msgrcv
内核数据结构msqid_ds
四个关键函数
- int msgget(key_t key, int msgflg);用于创建一个新的消息队列或者获取一个已有的消息队列标识符。成功时返回消息队列的标识符(类似于信箱的编号),失败时返回
-1
。 - int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);用于向指定的消息队列发送一条消息。成功时返回
0
,失败时返回-1
。 - ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);从指定的消息队列中接收一条消息。成功时返回接收到消息的大小(不包括
mtype
成员的大小),失败时返回-1
。 - int msgctl(int msqid, int cmd, struct msqid_ds *buf);用于对消息队列执行各种控制操作,比如删除消息队列、获取或设置消息队列的属性等。成功时返回
0
,失败时返回-1
。
基本原理
- msgsnd每次发送数据时,都必须给数据标记一个类型,例如类型为2的数据
- 然后这个有类型的数据进入消息队列的最后一个位置
- msgrcv在接收数据时也要说明自己想要接收那种数据,如上图此时msgrcv想要接收类型为2的消息,于是msgrcv会从消息队列中取出第一个类型为2的消息
- 细节问题后续会讨论
int msgget(key_t key, int msgflg)
key
:是一个用来标识消息队列的键值。它可以是一个任意的整数值,但通常使用ftok
函数生成。就好比是给 “信箱” 一个独特的编号,方便找到对应的 “信箱”。例如,通过ftok
函数可以基于文件路径和一个项目 ID 生成一个唯一的键值,这样不同进程只要使用相同的文件路径和项目 ID 就能找到同一个消息队列。msgflg
:用于指定消息队列的创建标志和访问权限。例如,IPC_CREAT
表示如果消息队列不存在就创建它,0666
表示设置消息队列的读写权限为所有用户可读可写。
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg)
msqid
:是由msgget
函数返回的消息队列标识符,指定要往哪个 “信箱” 投递消息。msgp
:是一个指向要发送消息结构的指针。这个消息结构必须以一个long
类型的成员开头,用来表示消息的类型msgsz
:指定要发送消息的大小(不包括mtype
成员的大小),也就是信的内容长度。msgflg
:用于指定发送消息的方式。比如IPC_NOWAIT
表示如果消息队列已满,不等待直接返回。默认情况下,发送消息时如果消息队列满了,则msgsnd将阻塞。若IPC_NOWAIT标志被指定,则msgsnd将立即返回并设置errno为EAGAIN。- 发送消息的结构如下:
struct mymsgbuf {long mtype;char mtext[100];
};
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg)
msqid
:消息队列标识符,指定从哪个 “信箱” 取信。msgp
:指向一个用来接收消息的结构的指针,结构类型要和发送时一致。msgsz
:指定接收消息的最大长度(不包括mtype
成员的大小)。msgtyp
:指定要接收的消息类型。如果为0
,则接收消息队列中的第一条消息;如果大于0
,则接收类型等于msgtyp
的第一条消息;如果小于0
,则接收类型小于等于msgtyp
绝对值的第一条消息。msgflg
:用于指定接收消息的方式,如IPC_NOWAIT
表示如果没有符合条件的消息,不等待直接返回,并设置errno为ENOMSG。MSG_EXCEPT:如果msgtype大于0,则接收消息队列中第一个非msgtype类型的消息。MSG_NOERROR:如果消息数据部分的长度超过了msg_sz,就将它截断。
int msgctl(int msqid, int cmd, struct msqid_ds *buf)
msqid
:消息队列标识符。cmd
:指定要执行的控制命令。常见的有IPC_STAT
(获取消息队列的状态信息并存储在buf
中)、IPC_SET
(设置消息队列的属性,属性值取自buf
)、IPC_RMID
(删除消息队列)。buf
:指向一个msqid_ds
结构的指针,用于获取或设置消息队列的属性。当cmd
为IPC_STAT
或IPC_SET
时使用,IPC_RMID
时可设为NULL
。
示例代码
该代码仅用于展示消息队列的基本逻辑,针对没有测试的其他情况,大家需要使用时自行测试即可。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <error.h>
#include <errno.h>#define MAX_TEXT 512// 定义消息结构
struct my_msg_st {long my_msg_type;char some_text[MAX_TEXT];
};void handle_errors(const char *msg) {perror(msg);exit(EXIT_FAILURE);
}int main() {int msgid;key_t key;struct my_msg_st some_data;long msg_to_receive = 0;// 创建一个唯一的键值key = ftok(".", 'a');if (key == -1) {handle_errors("ftok");}// 创建消息队列msgid = msgget(key, 0666 | IPC_CREAT);if (msgid == -1) {handle_errors("msgget");}// 发送不同类型消息的示例// 发送类型为1的消息some_data.my_msg_type = 1;strcpy(some_data.some_text, "This is a type 1 message");//if (msgsnd(msgid, (void *)&some_data, strlen(some_data.some_text), 0) == -1) {handle_errors("msgsnd");}// 发送类型为2的消息some_data.my_msg_type = 2;strcpy(some_data.some_text, "This is a type 2 message");if (msgsnd(msgid, (void *)&some_data, strlen(some_data.some_text), 0) == -1) {handle_errors("msgsnd");}// 接收消息的各种情况示例// 接收类型为1的消息msg_to_receive = 1;if (msgrcv(msgid, (void *)&some_data, MAX_TEXT, msg_to_receive, 0) == -1) {handle_errors("msgrcv");}printf("Received type 1 message: %s\n", some_data.some_text);// 接收类型为0的消息(即队列中的第一条消息)msg_to_receive = 0;if (msgrcv(msgid, (void *)&some_data, MAX_TEXT, msg_to_receive, 0) == -1) {handle_errors("msgrcv");}printf("Received first message in the queue: %s\n", some_data.some_text);// 使用IPC_NOWAIT标志接收消息,如果队列为空则不等待msg_to_receive = 1;if (msgrcv(msgid, (void *)&some_data, MAX_TEXT, msg_to_receive, IPC_NOWAIT) != -1) {printf("Received message with IPC_NOWAIT: %s\n", some_data.some_text);} else if (errno == ENOMSG) {printf("No message of type %ld in the queue with IPC_NOWAIT.\n", msg_to_receive);} else {handle_errors("msgrcv with IPC_NOWAIT");}// 删除消息队列if (msgctl(msgid, IPC_RMID, 0) == -1) {handle_errors("msgctl");}return 0;
}
测试msgrcv
- msgrcv默认是阻塞的,如果消息队列中没有自己想要的数据会被阻塞
// 发送类型为1的消息some_data.my_msg_type = 1;strcpy(some_data.some_text, "This is a type 1 message");//if (msgsnd(msgid, (void *)&some_data, strlen(some_data.some_text), 0) == -1) {handle_errors("msgsnd");}// 发送类型为2的消息some_data.my_msg_type = 2;strcpy(some_data.some_text, "This is a type 2 message");if (msgsnd(msgid, (void *)&some_data, strlen(some_data.some_text), 0) == -1) {handle_errors("msgsnd");}// 接收类型为3的消息msg_to_receive = 3;if (msgrcv(msgid, (void *)&some_data, MAX_TEXT, msg_to_receive, 0) == -1) {handle_errors("msgrcv");}printf("Received type 1 message: %s\n", some_data.some_text);
-
msgrcv设置了
IPC_NOWAIT
后,如果消息队列中没有自己想要的数据,不等待直接返回,并设置errno为ENOMSG。// 发送类型为1的消息some_data.my_msg_type = 1;strcpy(some_data.some_text, "This is a type 1 message");//if (msgsnd(msgid, (void *)&some_data, strlen(some_data.some_text), 0) == -1) {handle_errors("msgsnd");}// 发送类型为2的消息some_data.my_msg_type = 2;strcpy(some_data.some_text, "This is a type 2 message");if (msgsnd(msgid, (void *)&some_data, strlen(some_data.some_text), 0) == -1) {handle_errors("msgsnd");}// 使用IPC_NOWAIT标志接收消息,如果队列为空则不等待msg_to_receive = 3;if (msgrcv(msgid, (void *)&some_data, MAX_TEXT, msg_to_receive, IPC_NOWAIT) != -1) {printf("Received message with IPC_NOWAIT: %s\n", some_data.some_text);} else if (errno == ENOMSG) {printf("No message of type %ld in the queue with IPC_NOWAIT.\n", msg_to_receive);} else {handle_errors("msgrcv with IPC_NOWAIT");}
-
其他情况例如MSG_EXCEPT标志等情况,大家自行测试即可,重在了解消息队列的逻辑。
内核数据结构msqid_ds
如果msgget用于创建消息队列,则与之关联的内核数据结构msqid_ds将被创建开初始化。msqid ds结构体的定义如下:
struct msqid_ds {struct ipc_perm msg_perm; /* 消息队列的权限结构 */time_t msg_stime; /* 最后一次发送消息的时间 */time_t msg_rtime; /* 最后一次接收消息的时间 */time_t msg_ctime; /* 最后一次修改消息队列状态的时间 */unsigned long __msg_cbytes; /* 消息队列中当前数据的字节数 */msgqnum_t msg_qnum; /* 消息队列中的消息数量 */msglen_t msg_qbytes; /* 消息队列允许的最大字节数 */pid_t msg_lspid; /* 最后一次发送消息的进程ID */pid_t msg_lrpid; /* 最后一次接收消息的进程ID */
};
msgsnd成功时将修改内核数据结构msqid_ds的部分字段,如下所示:
- 将msg_qnum加1。
- 将msg_lspid设置为调用进程的PID。
- 将msg_stime设置为当前的时间。
msgrcv成功时将修改内核数据结构msqid_ds的部分字段,如下所示:
- 将msg_qnum减1。
- 将msg_Irpid设置为调用进程的PID。
- 将msg_rtime设置为当前的时间。
msqid_ds数据结构就是内核实现消息队列的底层逻辑,大家简单了解即可。