系统编程_进程间通信机制_消息队列与共享内存
消息队列概述
- 消息有类型:每条消息都有一个类型,就像每封信都有一个标签,方便分类和查找。
- 消息有格式:消息的内容有固定的格式,就像每封信都有固定的信纸格式。
- 随机查询:你可以按类型读取消息,不一定要按顺序读取,就像你可以按标签找信,而不是按收到的顺序。
- 多进程读写:多个进程可以同时向消息队列写入或读取消息,就像多个邮递员可以同时投递或取走信件。
- 读出即删除:从消息队列中读出消息后,消息就会被删除,就像你从信箱里取出信后,信箱里就没有这封信了。
- 唯一标识符:每个消息队列都有一个唯一的标识符,就像每个信箱都有一个唯一的编号。
- 持久存在:消息队列会一直存在,直到内核重启或你手动删除它,就像信箱会一直在,除非你把它拆掉。
Ubuntu 12.04 中的消息队列限制
- 每条消息最多可以有 8K 字节,就像每封信最多可以写 8K 字节的内容。
- 每个消息队列最多可以存储 16K 字节的消息,就像每个信箱最多可以放 16K 字节的信件。
- 系统中最多可以有 1609 个消息队列,就像一个城市最多可以有 1609 个信箱。
- 系统中最多可以有 16384 条消息,就像一个城市最多可以有 16384 封信。
消息队列的使用方法一般是:
发送者(邮递员)的步骤
-
获取消息队列的键值:
- 就像邮递员要投递,首先需要知道信箱的编号(消息队列的键值)。
- 通过
msgget
函数获取消息队列的消息队列的键值。
-
将数据放入结构体并发送:
- 你要发送一个信件(消息),需要把信件装进一个贴有邮票(类型)的盒子(结构体)。
- 使用
msgsnd
函数将这个带有标识的消息发送到消息队列。
接收者(拿信的人)的步骤
-
获取消息队列的 ID:
- 就像你要从信箱取信件,首先需要知道信箱的编号(消息队列的键值)。
- 通过
msgget
函数获取消息队列的键值。
-
读取指定标识的消息:
- 你要取出特定邮票(类型)的包裹(消息)。
- 使用
msgrcv
函数从消息队列中读取带有指定类型的消息。
查看系统中已创建的消息队列的指令
使用 ipcs
命令可以查看系统中已经创建的消息队列,以及其他IPC资源(共享内存和信号量)。
ipcs
命令参数
-m
:查看系统共享内存信息。-q
:查看系统消息队列信息。-s
:查看系统信号量信息。-a
:显示系统内所有的IPC信息。
# 查看系统中所有的消息队列
ipcs -q
移除IPC资源
使用 ipcrm
命令可以移除指定的IPC资源,包括共享内存段、信号量和消息队列。
ipcrm
命令参数
-m shmid
:移除用shmid
标识的共享内存段。-s semid
:移除用semid
标识的信号量。-q msgid
:移除用msgid
标识的消息队列。
ipcrm -q 12345 # 删除消息队列 12345 12345 是共享内存段的标识符(msgid)
让我们来区分一下消息队列中的“标识”(msgid)和“类型”(mtype)。
标识(msgid)
- 定义:
msgid
是消息队列的标识符,用于唯一标识一个消息队列。- 作用:当你创建或获取一个消息队列时,系统会返回一个
msgid
,你可以通过这个msgid
来进行后续的消息发送、接收和删除操作。- 示例:就像一个快递柜的编号,你需要知道这个编号才能往里面放包裹或取包裹。
类型(mtype)
- 定义:
mtype
是消息的类型,用于标识消息的类别。- 作用:在发送和接收消息时,可以根据
mtype
来区分不同类型的消息。接收消息时,可以指定接收某一类型的消息。- 示例:就像包裹上的标签,用于区分不同的包裹类型,比如“普通包裹”、“快递包裹”等。
区别
- msgid 是消息队列的唯一标识符,用于操作整个消息队列。
- mtype 是消息的类型,用于区分消息队列中的不同消息。
消息队列函数
ftok
函数
ftok
函数用于生成一个唯一的键值(key),这个键值用于标识IPC(进程间通信)资源,比如消息队列、共享内存和信号量。这个键值就像一个独特的“身份证号”,确保每个IPC资源都有一个唯一的标识。
函数原型
#include <sys/types.h>
#include <sys/ipc.h>key_t ftok(const char *pathname, int proj_id);
参数解释
- pathname:这是一个文件路径名。
ftok
函数会使用这个路径名来生成键值。 - proj_id:这是一个项目ID,是一个非0的整数(只有低8位有效)。(随便填)
返回值
- 成功:返回一个唯一的键值(key),就像生成了一个独特的身份证号。
- 失败:返回
-1
,表示生成键值失败。
msgget
函数的作用
msgget
函数用于创建一个新的消息队列或打开一个已经存在的消息队列。消息队列就像一个共享的邮箱,不同的进程可以通过这个邮箱来发送和接收消息。
函数原型
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>int msgget(key_t key, int msgflg);
参数解释
- key:这是消息队列的“钥匙”。只要不同的进程使用相同的钥匙(key),它们就能访问同一个消息队列。这个钥匙可以通过
ftok
函数生成。 - msgflg:这是一个标志,决定了函数的行为和消息队列的权限。它可以是以下几种值的组合:
- IPC_CREAT:如果消息队列不存在,就创建一个新的。
- IPC_EXCL:如果消息队列已经存在,并且同时指定了
IPC_CREAT
,那么函数会返回错误。 - 权限位:设置消息队列的访问权限,比如读写权限,类似于文件的权限设置。
返回值
- 成功:返回消息队列的标识符(ID),这是一个非负整数。
- 失败:返回
-1
,并设置errno
以指示错误类型。
msgsnd
函数的作用
msgsnd
函数用于将新消息添加到消息队列中。你可以把它想象成把一封信放进一个共享的邮箱里,其他进程可以从这个邮箱里取出这封信。
函数原型
#include <sys/msg.h>int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
参数解释
- msqid:消息队列的标识符。就像邮箱的编号,你需要知道这个编号才能往里面放信。
- msgp:待发送消息结构体的地址。就像你要发送的信的地址。
- msgsz:消息正文的字节数。就像信的内容有多长。
- msgflg:函数的控制属性,决定了消息发送的行为。可以是以下值之一:
- 0:如果消息队列已满,
msgsnd
会阻塞(等待),直到有空间可以放入消息。就像你要等到邮箱有空位才能放信。 - IPC_NOWAIT:如果消息队列已满,
msgsnd
会立即返回,而不是等待。就像你看到邮箱满了,直接走开,不等空位。
- 0:如果消息队列已满,
返回值
- 成功:返回
0
,表示消息成功发送。 - 失败:返回
-1
,并设置errno
以指示错误类型。
msgrcv
函数的作用
msgrcv
函数用于从消息队列中接收一个消息。一旦成功接收消息,这条消息就会从消息队列中删除。你可以把它想象成从共享的邮箱里取出一封信,取出后这封信就不在邮箱里了。
函数原型
#include <sys/types.h>
#inclde <sys/ipc.h>
#include <sys/msg.h>
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);
参数解释
- msqid:消息队列的标识符。就像邮箱的编号,你需要知道这个编号才能从里面取信。
- msgp:存放消息结构体的地址。就像你要把取出的信放到哪里。
- msgsz:消息正文的字节数。就像信的内容有多长。
- msgtyp:消息的类型,决定你要取哪种类型的消息。可以有以下几种情况:
- msgtyp = 0:取队列中的第一个消息。
- msgtyp > 0:取队列中类型为
msgtyp
的消息。 - msgtyp < 0:取队列中类型值小于或等于
msgtyp
绝对值的消息,如果有多个这样的消息,则取类型值最小的消息。
msgflg:函数的控制属性
- 0:
msgrcv
调用会阻塞,直到成功接收消息为止。就像你一直等到邮箱里有信为止。 - MSG_NOERROR:如果返回的消息字节数比
msgsz
多,则消息会被截短到msgsz
字节,不通知发送进程。 - IPC_NOWAIT:调用进程会立即返回。如果没有收到消息则立即返回
-1
。就像你看到邮箱里没有信,直接走开,不等信。
注意事项
- 如果消息队列中有多种类型的消息,
msgrcv
会按消息类型获取,而不是按先进先出的顺序。 - 在获取某类型消息时,如果队列中有多条此类型的消息,则获取最先添加的消息,即先进先出。
返回值
- 成功:返回读取消息的长度。
- 失败:返回
-1
,并设置errno
以指示错误类型。
msgctl
函数的作用
msgctl
函数就像是一个多功能的管理员,可以对消息队列进行各种操作,比如查看消息队列的状态、修改它的属性,或者直接删除它。
函数原型
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
intmsgctl(intmsqid,intcmd,structmsqid_ds*buf);
参数解释
-
msqid:这是消息队列的编号,就像每个邮箱都有一个编号一样,你需要这个编号来操作特定的消息队列。
-
cmd:这是你告诉管理员要做什么的指令。常见的指令有:
- IPC_RMID:删除消息队列。就像把邮箱整个拆掉,里面的信件也都没了。
- IPC_STAT:查看消息队列的当前状态。就像你让管理员把邮箱的详细信息告诉你,比如有多少信件、谁发的等等。
- IPC_SET:修改消息队列的属性。就像你让管理员把邮箱的某些设置改一下,比如谁可以往里面放信,谁可以取信。
-
buf:这是一个结构体的地址,用来存放或修改消息队列的属性。就像你给管理员一个表格,他会把邮箱的信息填到这个表格里,或者根据这个表格里的信息修改邮箱的设置。
返回值
- 成功:返回 0,表示操作成功。
- 失败:返回 -1,并设置
errno
来指示错误类型。就像管理员告诉你操作失败了,并给你一个错误代码,说明哪里出了问题。
练习:消息队列实现多人聊天程序
下面 我们来根据这个流程图来完善这个练习吧。
主函数
根据流程图可以看到主函数首先检查命令行参数的数量。程序需要两个参数:用户名和接收类型。如果参数数量不对,程序会打印使用说明并退出。
argv[1]
是用户名,例如 "Tom"。argv[2]
是接收类型,例如 "1"。
如果成功接下来,程序将这两个参数传递给 chat_process
函数,开始聊天进程。
主函数部分代码
// 主函数,程序的入口点 int main(int argc, char *argv[]) {// 检查命令行参数数量是否正确if (argc != 3) {// 参数数量不正确时,输出使用说明并退出程序fprintf(stderr, "Usage: %s <name> <recv_type>\n", argv[0]);exit(1);}// 获取命令行参数:姓名和接收类型const char *name = argv[1];long recv_type = atol(argv[2]);// 调用函数处理聊天逻辑chat_process(name, recv_type);// 程序正常结束return 0; }
详细解释:
这段代码的主要功能是作为聊天程序的入口点,负责解析命令行参数,检查参数的正确性,并启动聊天进程。具体来说,它:
- 检查命令行参数的数量是否正确。
- 从命令行参数中提取用户名和接收类型。
- 调用
chat_process
函数,启动聊天进程,实现消息的发送和接收。通过这种方式,程序能够根据用户输入的参数启动相应的聊天功能,实现用户之间的消息传递。
结构体
typedef struct msg {long type; // 接收者类型char text[100]; // 发送内容char name[20]; // 发送者姓名
} MSG;
聊天进程函数
接下面 我们就要开始创建消息队列,创建进程了。
基本逻辑就是
1. 创建消息队列
- 使用
ftok
生成一个唯一的键值。
2. 创建子进程
- 使用
fork
创建一个子进程。 - 如果
fork
失败,打印错误信息并退出。
3. 子进程逻辑
- 子进程调用
receive_message
函数,负责从消息队列中接收并打印消息。
4. 父进程逻辑
- 父进程在一个无限循环中调用
send_message
函数,负责发送消息到消息队列。 - 父进程在循环结束后,调用
wait
等待子进程结束。
/*** 聊天处理函数* 根据提供的名称和接收类型,初始化消息队列,并根据进程类型处理消息* * @param name 用户名,用于标识消息的发送者或接收者* @param recv_type 接收消息的类型,用于区分不同类型的接收逻辑*/ void chat_process(const char *name, long recv_type) {// 生成消息队列的键值,用于后续的消息队列识别key_t key = ftok("/tmp", 65);// 使用键值创建或获取消息队列,确保消息队列的访问权限int msgid = msgget(key, 0666 | IPC_CREAT);if (msgid < 0) {// 错误处理:获取消息队列失败perror("msgget");exit(1);}// 创建子进程,用于并行处理接收和发送消息pid_t pid = fork();if (pid < 0) {// 错误处理:进程创建失败perror("fork");exit(1);} else if (pid == 0) {// 子进程负责接收消息receive_message(msgid, name, recv_type);} else {// 父进程负责发送消息while (1) {send_message(msgid, name);}// 等待子进程结束,避免僵尸进程wait(NULL);} }
详细讲解:
1. 生成消息队列键值
key_t key = ftok("/tmp", 65);
ftok
函数用于生成一个唯一的键值,用于创建消息队列。"/tmp"
是一个路径名,65
是一个项目 ID。这两个参数的组合确保了键值的唯一性。2. 创建或获取消息队列
int msgid = msgget(key, 0666 | IPC_CREAT); if (msgid < 0) {perror("msgget");exit(1); }
msgget
函数用于创建或获取一个消息队列。key
是之前生成的键值,0666 | IPC_CREAT
指定了权限和标志。0666
表示所有用户都有读写权限,IPC_CREAT
表示如果消息队列不存在则创建。如果msgget
失败,程序会打印错误信息并退出。3. 创建子进程
pid_t pid = fork(); if (pid < 0) {perror("fork");exit(1); } else if (pid == 0) {// 子进程负责接收消息receive_message(msgid, name, recv_type); } else {// 父进程负责发送消息while (1) {send_message(msgid, name);}wait(NULL); }
fork
函数用于创建一个子进程。fork
函数会返回两次:在父进程中,返回子进程的 PID。
在子进程中,返回 0。
如果
fork
失败,程序会打印错误信息并退出。子进程
子进程调用
receive_message
函数,负责接收消息。receive_message
函数会不断从消息队列中读取属于当前用户的消息,并打印出来。父进程
父进程在一个无限循环中调用
send_message
函数,负责发送消息。send_message
函数会提示用户输入接收者名字和消息内容,然后根据接收者名字确定消息类型,并将消息发送到消息队列。父进程在发送消息的循环结束后,调用
wait
函数等待子进程结束。
发送消息函数
这个呢就是send_message
函数,首先提示用户输入接收者名字和消息内容。然后根据接收者名字确定消息类型,并将消息内容和发送者名字填充到 MSG
结构体中。最后,使用 msgsnd
函数将消息发送到消息队列。
// 发送消息到指定接收者 // 参数: // msgid: 消息队列标识符 // name: 发送者姓名 void send_message(int msgid, const char *name) {MSG snd_msg; // 定义待发送的消息结构体char recipient[20]; // 用于存储接收者姓名的字符数组// 提示并获取用户输入的接收者姓名printf("Enter recipient name: ");fgets(recipient, sizeof(recipient), stdin);recipient[strcspn(recipient, "\n")] = 0; // 去掉输入中的换行符// 根据接收者名字确定消息类型if (strcmp(recipient, "Tom") == 0) {snd_msg.type = 1;} else if (strcmp(recipient, "Lili") == 0) {snd_msg.type = 2;} else if (strcmp(recipient, "Luxi") == 0) {snd_msg.type = 3;} else {// 如果接收者未知,则打印错误信息并返回printf("Unknown recipient: %s\n", recipient);return;}// 设置消息结构体中的发送者姓名strcpy(snd_msg.name, name);// 提示并获取用户输入的消息内容printf("Enter message to send: ");fgets(snd_msg.text, sizeof(snd_msg.text), stdin);snd_msg.text[strcspn(snd_msg.text, "\n")] = 0; // 去掉输入中的换行符// 发送消息到消息队列if (msgsnd(msgid, &snd_msg, sizeof(snd_msg) - sizeof(long), 0) < 0) {// 如果发送失败,则打印错误信息并退出程序perror("msgsnd");exit(1);} }
函数声明和变量定义
void send_message(int msgid, const char *name) {MSG snd_msg;char recipient[20];
send_message
函数接受两个参数:消息队列的标识符msgid
和发送者的名字name
。- 定义了一个
MSG
结构体变量snd_msg
用于存储要发送的消息。- 定义了一个字符数组
recipient
用于存储接收者的名字。输入接收者名字
printf("Enter recipient name: "); fgets(recipient, sizeof(recipient), stdin); recipient[strcspn(recipient, "\n")] = 0; // 去掉换行符
- 提示用户输入接收者的名字。
- 使用
fgets
从标准输入读取接收者的名字,并存储在recipient
数组中。- 使用
strcspn
去掉输入字符串末尾的换行符。确定消息类型
if (strcmp(recipient, "Tom") == 0) {snd_msg.type = 1; } else if (strcmp(recipient, "Lili") == 0) {snd_msg.type = 2; } else if (strcmp(recipient, "Luxi") == 0) {snd_msg.type = 3; } else {printf("Unknown recipient: %s\n", recipient);return; }
- 根据接收者的名字确定消息类型:
- 如果接收者是 "Tom",消息类型设为 1。
- 如果接收者是 "Lili",消息类型设为 2。
- 如果接收者是 "Luxi",消息类型设为 3。
- 如果接收者名字不在上述列表中,打印错误信息并返回。
填充消息内容
strcpy(snd_msg.name, name); printf("Enter message to send: "); fgets(snd_msg.text, sizeof(snd_msg.text), stdin); snd_msg.text[strcspn(snd_msg.text, "\n")] = 0; // 去掉换行符
- 将发送者的名字复制到
snd_msg.name
中。- 提示用户输入要发送的消息内容。
- 使用
fgets
从标准输入读取消息内容,并存储在snd_msg.text
中。- 使用
strcspn
去掉输入字符串末尾的换行符。发送消息
if (msgsnd(msgid, &snd_msg, sizeof(snd_msg) - sizeof(long), 0) < 0) {perror("msgsnd");exit(1); }
- 使用
msgsnd
系统调用将消息发送到消息队列。- 如果发送失败,打印错误信息并退出程序。
接收消息函数
函数持续监听指定的消息队列,接收指定类型的消患当接收到消息时,将打印出接收者名称,消息发送者名称以及消息内容函数通过调用msgrcv接收消息,如果接收失败则输出错误信息并终止程序
void receive_message(int msgid, const char *name, long type) {MSG recv_msg;while (1) {if (msgrcv(msgid, &recv_msg, sizeof(recv_msg) - sizeof(long), type, 0) < 0) {perror("msgrcv");exit(1);}printf("%s received message from %s: %s\n", name, recv_msg.name, recv_msg.text);} }
函数声明
void receive_message(int msgid, const char *name, long type)
这个函数接收三个参数:
msgid
:消息队列的标识符。name
:接收者的名字。type
:消息类型,用于过滤接收到的消息。变量声明
MSG recv_msg;
CopyInsert
声明一个
MSG
类型的变量recv_msg
,用于存储接收到的消息。无限循环
while (1) {
使用一个无限循环来持续接收消息。
接收消息
if (msgrcv(msgid, &recv_msg, sizeof(recv_msg) - sizeof(long), type, 0) < 0) {perror("msgrcv");exit(1); }
调用
msgrcv
函数从消息队列中接收消息。参数解释如下:
msgid
:消息队列的标识符。&recv_msg
:指向接收消息的缓冲区。sizeof(recv_msg) - sizeof(long)
:接收消息的大小,减去long
类型的大小是因为msgrcv
函数需要的是消息数据的大小,而不是整个结构体的大小。type
:消息类型,用于过滤消息。0
:标志位,这里设置为0表示默认行为。如果
msgrcv
调用失败,打印错误信息并退出程序。打印接收到的消息
printf("%s received message from %s: %s\n", name, recv_msg.name, recv_msg.text);
打印接收到的消息,包括接收者的名字、发送者的名字和消息内容。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <sys/wait.h>typedef struct msg {long type; // 接收者类型char text[100]; // 发送内容char name[20]; // 发送者姓名
} MSG;void send_message(int msgid, const char *name) {MSG snd_msg;char recipient[20];printf("Enter recipient name: ");fgets(recipient, sizeof(recipient), stdin);recipient[strcspn(recipient, "\n")] = 0; // 去掉换行符// 根据接收者名字确定消息类型if (strcmp(recipient, "Tom") == 0) {snd_msg.type = 1;} else if (strcmp(recipient, "Lili") == 0) {snd_msg.type = 2;} else if (strcmp(recipient, "Luxi") == 0) {snd_msg.type = 3;} else {printf("Unknown recipient: %s\n", recipient);return;}strcpy(snd_msg.name, name);printf("Enter message to send: ");fgets(snd_msg.text, sizeof(snd_msg.text), stdin);snd_msg.text[strcspn(snd_msg.text, "\n")] = 0; // 去掉换行符if (msgsnd(msgid, &snd_msg, sizeof(snd_msg) - sizeof(long), 0) < 0) {perror("msgsnd");exit(1);}
}void receive_message(int msgid, const char *name, long type) {MSG recv_msg;while (1) {if (msgrcv(msgid, &recv_msg, sizeof(recv_msg) - sizeof(long), type, 0) < 0) {perror("msgrcv");exit(1);}printf("%s received message from %s: %s\n", name, recv_msg.name, recv_msg.text);}
}void chat_process(const char *name, long recv_type) {key_t key = ftok("/tmp", 65);int msgid = msgget(key, 0666 | IPC_CREAT);if (msgid < 0) {perror("msgget");exit(1);}pid_t pid = fork();if (pid < 0) {perror("fork");exit(1);} else if (pid == 0) {// 子进程负责接收消息receive_message(msgid, name, recv_type);} else {// 父进程负责发送消息while (1) {send_message(msgid, name);}wait(NULL);}
}int main(int argc, char *argv[]) {if (argc != 3) {fprintf(stderr, "Usage: %s <name> <recv_type>\n", argv[0]);exit(1);}const char *name = argv[1];long recv_type = atol(argv[2]);chat_process(name, recv_type);return 0;
}
运行的时候把打开多个程序就可以了。