system V消息队列
一、是什么?
System V 消息队列 = 内核里的一个消息链表
- 进程把带类型的消息放进去
- 另一个进程按类型取
- 自带同步、排队、阻塞
二、核心原理
- 内核维护一个消息队列链表
- 消息 = 类型 + 数据
- 发送:把消息挂到队列尾部
- 接收:可按类型取(只取我想要的)
- 生命周期:内核常驻,进程退出不删


三、相关接口
1. ftok
c
#include <sys/types.h>
#include <sys/ipc.h>
与共享内存创建差不多,都需要提前约定好一个key键值。
c
// 用户指明
#define PATHNAME "."
#define PROJ_ID 66
key_t Getkey() {
return ftok(PATHNAME, PROJ_ID);
}
// 1. 构建键值
key_t k = Getkey();
if (k < 0) {
std::cerr << "获取key键值失败:" << strerror(errno) << std::endl;
exit(1);
}
消息队列系列接口的头文件为
c
#include <sys/types.h> // 基本类型定义
#include <sys/ipc.h> // IPC通用(ftok也用这个)
#include <sys/msg.h> // 消息队列专用
2. msgget
c
int msgget(key_t key, int msgflg);
msgflg:
-
IPC_CREAT | 0666:不存在则创建,已存在就用之------获取 -
IPC_CREAT | IPC_EXCL | 0666:不存在创建,已存在则报错------创建 -
0664:权限(和文件权限一样)
返回:成功返回 msgid(共享内存 ID),失败 -1
3. msgctl
c
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
常用:
c
// 直接删,第三个参数给 NULL
if (msgctl(msqid, IPC_RMID, NULL) == -1) {
perror("msgctl delete failed");
exit(1);
}
4. msgsnd
c
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
**作用:**往消息队列里发送一条消息
返回值:成功 → 返回 0;失败 → 返回 -1。
参数2 const void* msgp
指向自己定义的消息结构体指针 ,结构体必须以 long mtype 开头
c
// 固定格式!!!
struct msgbuf {
long mtype; // 消息类型,必须 > 0
char mtext[4096]; // 消息内容
};
参数3 size_t msgsz
消息数据部分的大小,只算内容,不算 mtype! ,例如 sizeof(msg.mtext)
参数4 int msgflg
- 通常填 0 :队列满了就阻塞等待
- 填
IPC_NOWAIT:队列满了不等待,直接报错
5. msgrcv
c
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);
作用:从消息队列里取出一条消息;最大特点:可以按类型接收,只收我想要的
返回值:成功,返回接收到的数据字节数;失败,**返回 **-1。
参数2 void *msgp
存放消息的结构体指针(和发送时用同一个结构体)
参数3 size_t msgsz
- 要接收的最大数据长度
- 写法:
sizeof(msg.mtext) - 只算内容,不算 mtype!
参数4 long msgtyp(最关键:消息类型)
决定你收哪条消息:
- 0 → 拿走队列里最早的一条(不挑类型)
- > 0 → 只拿走类型等于这个数的第一条
- < 0 → 取小于等于绝对值的最小类型(极少用)
参数5 int msgflg
- 一般填 0 :没消息就阻塞等待
IPC_NOWAIT:没消息直接返回,不等待
示例
c
// 1. 定义消息结构体
struct msgbuf {
long mtype;
char mtext[4096];
};
struct msgbuf msg;
// 2. 接收:只收类型为 1 的消息
msgrcv(
msqid, // 队列ID
&msg, // 存到哪里
sizeof(msg.mtext), // 最大接收长度
1, // 只收类型=1
0 // 阻塞等待
);
// 3. 打印收到的内容
printf("收到:%s\n", msg.mtext);
system V信号量
一、补充概念
- 多个执行流,能同时看到并访问的公共资源------共享资源
- 任何时候,只能有一个执行流访问公共资源------互斥;互斥的存在就是保护共享资源
- 被保护的共享资源------临界资源
- 临界资源就像要使用的东西,访问临界资源的代码------临界区;那么一段代码就能分为:临界区VS非临界区
- 计算机中要么不做,要不做完(最小操作元)------原子性
- 按顺序、有先后、你干完我再干,不是同时干,而是排队干------同步
二、什么是信号量
信号量(semaphore) = 用来控制多个进程 / 线程 "同时能有多少人" 访问共享资源的计数器。
最形象的例子:信号量 = 厕所坑位计数器
- 厕所一共 2 个坑位 → 信号量初始值sem = 2
- 有人进去 → P 操作(计数器 -1)
- 有人出来 → V 操作(计数器 +1)
- 计数器变成 0 → 其他人必须等待
信号量就是管 "能进几个人" 的!
信号量也可以想象成一个带计数功能的门卫:
P() :我要进去(申请资源)
↓
门卫检查还有没有名额
V() :我出来了(释放资源)
↓
门卫把名额还回去
↓
通知排队的人进来
二元信号量 :当sem = 1时,一次只能放进来一个人,实现了互斥;
多元信号量:sem > 1。
三、相关接口
1. semget
c
int semget(key_t key, int nsems, int semflg);
作用:拿到一个信号量集合(可以包含多个计数器)
- key:调用ftok
- nsems :要几个信号量(通常填 1)
- semflg:IPC_CREAT |(IPC_EXCLL)| 0666
- 返回:成功 → semid(信号量 ID),失败 → -1
nsems参数:说明了信号量可以有多个,都被存放在了一个信号量集数组中。
2. semctl
c
int semctl(int semid, int semnum, int cmd, ...);
作用:设置初始值、删除、获取信息
参数1int semid
- 信号量集合的 ID
- 来自
semget()的返回值
参数2**semnum**:第几个信号量(在信号量集合数组中的下标)
参数3**cmd**:
-
SETVAL:设置初始值
-
IPC_RMID:删除信号量
-
返回:成功 0,失败 -1
参数4...初始化信号量时要填的联合体
c
// Linux中,必须用自己定义的联合体进行初始化
union semun
{
int val;
struct semid_ds *buf;
unsigned short *array;
struct seminfo *__buf;
};
union semun arg;
arg.val = 1;
semctl(semid, 0, SETVAL, arg);
c
// 删除0号信号量
semctl(semid, 0, IPC_RMID);
3. semop
c
int semop(int semid, struct sembuf *sops, unsigned int nsops);
作用 :执行 P (-1) 申请 或 V (+1) 释放
参数3 :unsigned int nsops
- 要执行 多少个 sembuf 操作
- 我们一次只做一个 P 或 V → 固定填 1
参数2 :struct sembuf *sops (最重要的参数)
c
struct sembuf {
short sem_num; // 信号量下标(第几个信号量)
short sem_op; // 操作:-1=P,+1=V
short sem_flg; // 标志:填 0 → 阻塞(最常用);填 IPC_NOWAIT → 不阻塞,失败直接返回
};
struct sembuf 是头文件 <sys/sem.h> 中自带的一个结构体,在使用时不用再去定义这个结构体,只需要声明一个这种结构体变量就行。
一般写法
c
// P 操作(申请资源 -1)
struct sembuf sem_buff = {
.sem_num = 0, // 第 0 个信号量
.sem_op = -1, // P 操作
.sem_flg = 0 // 阻塞
};
semop(semid, &sem_buff, 1); // 执行
c
// V 操作(释放资源 +1)
struct sembuf sem_buff = {
.sem_num = 0, // 第 0 个
.sem_op = 1, // V 操作
.sem_flg = 0
};
semop(semid, &sem_buff, 1);
四、P/V操作的原子性
假设信号量初始值:sem = 1
有两个进程同时执行 P 操作:P(sem);
如果 sem-- 不是原子的,可能发生:
时间线:
A读取 sem=1
B读取 sem=1
A计算 1-1=0
B计算 1-1=0
A写回 sem=0
B写回 sem=0
最终:sem = 0
看起来没问题,但实际上:两个进程都认为自己获得了资源
结果:两个进程同时进入临界区
同步机制彻底失效。
所以实际上内核做的是:
c
P()
{
原子地 {
sem--;
if(sem < 0)
{
将当前进程加入等待队列;
阻塞当前进程;
}
}
}
c
V()
{
原子地 {
sem++;
if(有等待进程)
{
唤醒一个等待进程;
}
}
}