
大家好,接下来的一段时间我将开始学习野火的Linux 系统课程并将学习到的干货逐步更新到我的CSDN博客中。没时间刷课的同学可以把我的博客喂给AI 突击一下。
目录
[一、System-V IPC 特点](#一、System-V IPC 特点)
[三、API 函数详解](#三、API 函数详解)
[1. ftok () ------ 生成 key](#1. ftok () —— 生成 key)
[2. msgget () ------ 创建 / 获取消息队列,得到 msqid](#2. msgget () —— 创建 / 获取消息队列,得到 msqid)
[3. msgsnd () ------ 发送消息到队列](#3. msgsnd () —— 发送消息到队列)
[4. msgrcv () ------ 从队列读取消息](#4. msgrcv () —— 从队列读取消息)
[5. msgctl () ------ 控制消息队列](#5. msgctl () —— 控制消息队列)
一、System-V IPC 特点
System-V IPC:包含消息队列、共享内存、信号量,属于经典进程间通信方式
- 独立于进程:创建之后,就算创建进程退出,IPC 对象仍然在内核中存在,不会自动销毁,需要手动删除。
- 没有文件名、没有文件描述符,不能用 read/write 操作。
- IPC 对象拥有两套标识:
key:用户层键值,用于不同进程之间打开同一个 IPC 对象ID:内核返回的对象 id,本进程操作时使用
二、消息队列完整使用流程
- 用
ftok()生成唯一key msgget():创建 / 获取消息队列,拿到队列 IDmsgsnd():向队列发送消息(带消息类型 mtype)msgrcv():按指定消息类型读取消息msgctl():获取属性 / 设置属性 / 删除消息队列
三、API 函数详解
1. ftok () ------ 生成 key
功能 :根据文件路径 + 项目 id,生成唯一的key_t键值,供 msgget 使用
key_t ftok(const char *path, int proj_id);
- 参数
path:存在的合法文件路径(一般用当前目录)proj_id:项目编号,仅低 8bit 有效,填一个整数即可
- 返回值
- 成功:返回合法 key
- 失败:
-1
注意:同一个 path+proj_id,生成的 key 相同,多个进程就可以打开同一个消息队列
2. msgget () ------ 创建 / 获取消息队列,得到 msqid
功能:使用 key,创建消息队列或者获取已存在队列的 ID
int msgget(key_t key, int msgflg);
- 参数
key:ftok 产生的键值msgflg:标志位,IPC_CREAT | 权限IPC_CREAT:队列不存在则创建;存在则直接打开- mode:访问权限,如
0664
- 返回值
- 成功:消息队列 ID(msqid)
- 失败:
-1
3. msgsnd () ------ 发送消息到队列
功能:往消息队列写入一条消息
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
- 参数
-
msqid:消息队列 ID -
msgp:消息缓冲区,固定结构体模板struct msgbuf{ long mtype; // 消息类型标识,必须>0 char mtext[1]; // 消息正文(柔性数组,存放数据) }; -
msgsz:消息正文 mtext 的字节长度,不包含 mtype -
msgflg:0:阻塞发送;队列满时,进程阻塞等待IPC_NOWAIT:非阻塞;队列满直接返回失败,不等待
-
- 返回值
- 成功:
0 - 失败:
-1
- 成功:
4. msgrcv () ------ 从队列读取消息
功能:从消息队列读取指定类型的消息
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);
- 参数
msqid:消息队列 IDmsgp:接收消息缓冲区,和发送端 msgbuf 结构一致msgsz:缓冲区能存放正文的最大字节数msgtyp:要接收的消息类型>0:读取 mtype 等于 msgtyp 的消息
msgflg0:阻塞读取,没有对应消息就阻塞等待IPC_NOWAIT:非阻塞,无消息直接返回 - 1MSG_NOERROR:消息正文过长时,截断超出部分,不报错
- 返回值
- 成功:读到的消息正文字节数
- 失败:
-1
5. msgctl () ------ 控制消息队列
功能 :获取、修改消息队列属性 ,删除消息队列
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
- 参数
msqid:消息队列 IDcmd控制命令:IPC_STAT:获取队列属性,存入 bufIPC_SET:修改队列属性(buf 中设置)IPC_RMID:删除消息队列,buf 传 NULL 即可
buf:msqid_ds 结构体,存放属性信息;IPC_RMID 时填 NULL
- 返回值
- 成功:
0 - 失败:
-1
- 成功:
四、实战演练
发送端代码:
cpp
/* msgsnd.c:System V 消息队列发送端 */
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#define BUFFER_SIZE 512
struct message {
long msg_type; /* 消息类型必须大于 0 */
char msg_text[BUFFER_SIZE]; /* 消息正文 */
};
int main(void)
{
int qid;
key_t key;
struct message msg;
/*
* 根据一个真实存在的路径和项目编号生成 key。
* /tmp 必须存在;项目编号实际只使用低 8 位。
*/
key = ftok("/tmp", 11);
if (key == (key_t)-1) {
perror("ftok");
return EXIT_FAILURE;
}
/*
* 获取或创建消息队列:
* IPC_CREAT:不存在时创建
* 0666:请求读写权限,最终权限还会受 umask 影响
*/
qid = msgget(key, IPC_CREAT | 0666);
if (qid == -1) {
perror("msgget");
return EXIT_FAILURE;
}
printf("Open queue %d\n", qid);
for (;;) {
printf("Enter some message to the queue: ");
fflush(stdout);
if (fgets(msg.msg_text, sizeof msg.msg_text, stdin) == NULL) {
if (feof(stdin)) {
puts("\nEnd of input");
break;
}
perror("fgets");
return EXIT_FAILURE;
}
/*
* 消息类型必须是正整数。
* 使用发送进程 PID,接收端可按 PID 选择消息。
*/
msg.msg_type = (long)getpid();
/*
* msgsz 只计算消息正文长度,
* 不能把前面的 long msg_type 计算进去。
*/
if (msgsnd(qid,
&msg,
strlen(msg.msg_text),
0) == -1) {
perror("msgsnd");
return EXIT_FAILURE;
}
puts("Message posted");
/*
* quit 消息已经发送到消息队列,
* 然后发送端结束。
*
* fgets() 会保留换行符,因此输入 quit 后,
* 缓冲区一般是 "quit\n",比较前 4 个字符即可。
*/
if (strncmp(msg.msg_text, "quit", 4) == 0) {
break;
}
}
return EXIT_SUCCESS;
}
编译:
gcc -Wall -Wextra -Wpedantic -std=c11 msgsnd.c -o msgsnd
运行:
./msgsnd
接收端代码:
cpp
/* msgrcv.c:System V 消息队列接收端 */
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define BUFFER_SIZE 512
struct message {
long msg_type; /* 消息类型,必须大于 0 */
char msg_text[BUFFER_SIZE]; /* 消息正文 */
};
int main(void)
{
int qid;
key_t key;
struct message msg;
/*
* 必须和发送端使用相同的路径及项目编号,
* 否则生成的 key 不同,双方无法打开同一个消息队列。
*/
key = ftok("/tmp", 11);
if (key == (key_t)-1) {
perror("ftok");
return EXIT_FAILURE;
}
/*
* 获取或创建消息队列。
* 发送端和接收端使用相同的 key,因此会得到同一个 qid。
*/
qid = msgget(key, IPC_CREAT | 0666);
if (qid == -1) {
perror("msgget");
return EXIT_FAILURE;
}
printf("Open queue %d\n", qid);
for (;;) {
ssize_t received;
/*
* 先清空消息正文。
* 最多接收 BUFFER_SIZE - 1 字节,
* 为字符串末尾的 '\0' 保留空间。
*/
memset(msg.msg_text, 0, sizeof msg.msg_text);
/*
* msgtyp == 0:
* 接收消息队列中的第一条消息,不限制消息类型。
*
* flags == 0:
* 如果队列中没有消息,接收进程阻塞等待。
*/
received = msgrcv(
qid,
&msg,
sizeof msg.msg_text - 1,
0,
0
);
if (received == -1) {
perror("msgrcv");
return EXIT_FAILURE;
}
/*
* msgrcv() 返回消息正文的实际字节数,
* 显式补上字符串结束符。
*/
msg.msg_text[received] = '\0';
printf(
"The message from process %ld: %s",
msg.msg_type,
msg.msg_text
);
/*
* 发送端使用 fgets() 读取,
* 所以输入 quit 后消息一般是 "quit\n"。
* 比较前四个字符即可。
*/
if (strncmp(msg.msg_text, "quit", 4) == 0) {
break;
}
}
/*
* 将消息队列从内核中删除。
* IPC_RMID 会使该队列不能再用于发送或接收。
*/
if (msgctl(qid, IPC_RMID, NULL) == -1) {
perror("msgctl");
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
}
编译:
gcc -Wall -Wextra -Wpedantic -std=c11 msgrcv.c -o msgrcv
先运行接收端:
./msgrcv
再打开另一个终端运行发送端:
./msgsnd
执行关系图:
cpp
发送端 消息队列 接收端
ftok("/tmp", 11) ─────┐ ┌──── ftok("/tmp", 11)
↓ ↓
相同的 key
↓
msgget() 得到相同 qid
↓
msgsnd(qid, ...) ──→ [ 消息 ] ──→ msgrcv(qid, ...)
↓
输入 quit 并发送 ───────────────→ 接收 quit
↓
msgctl(IPC_RMID)
↓
删除队列

五、核心特点总结
- 消息自带
mtype类型标签,可以按类型选择性接收消息,不用按先进先出顺序读取,这是消息队列最大特点。 - 内核持久化:进程退出,消息队列和里面消息仍然保留,必须 msgctl (IPC_RMID) 手动删除,否则一直占用内核资源。
- 阻塞 / 非阻塞模式由 msgsnd、msgrcv 的 msgflg 控制。
六、命令行调试命令
ipcs -q # 查看系统所有消息队列
ipcrm -q msqid # 删除指定消息队列