前言:管道只能传字节流,共享内存快却裸奔------没有同步互斥,多进程并发就乱。System V 标准把 IPC 收成三件套:共享内存、消息队列、信号量。上篇啃完共享内存,这篇拆剩下两件:消息队列怎么传有类型数据块,信号量又如何给裸资源套上保护。
一、System V IPC 三件套(共享内存 / 消息队列 / 信号量)
篇26 讲了共享内存,它是 System V IPC 三件套之一。另外两件是消息队列 和信号量。
于是有了 System V 标准,它把 IPC 归纳成三件套:共享内存、消息队列、信号量(有的记作"共享内存、消息队列、一数组",那个"数组"其实就是信号量集)。
1.1 三件套的共同点
三者都用 key 在内核里找对象、都用 IPC_CREAT 创建、生命周期都随内核(用 ipcs 看、ipcrm 删)。接口形态也高度一致------记住"换前缀"就行:shm* / msg* / sem*。
1.2 本篇定位
先吃透消息队列,再点一下信号量:它是共享内存缺的那层保护,概念下篇展开。
二、消息队列是什么(有类型数据块,管道是字节流)
2.1 定义
消息队列是内核维护的一条消息链表。进程往里放"消息",另一个进程按类型取走。
它的定位原话是:消息队列提供了一种一个进程给另一个进程发送有类型数据块的方式。

2.2 和管道的区别
- 管道:无结构字节流。write 进去什么,对方 read 就按字节顺序出来,没有"消息边界",收发双方得自己约定怎么切包。
- 消息队列:有类型的数据块。每条消息带一个类型 mtype,接收方可以按类型挑,天然有边界。
- 一句话:管道是"自来水任其流",消息队列是"贴了标签的快递柜,按标签取"。
三、内核如何管理消息队列(先描述再组织)
OS 管理任何资源都遵循"先描述,再组织":先用结构体描述它,再用某种结构组织起来。
3.1 描述结构 struct msqid_ds
cpp
struct msqid_ds {
struct ipc_perm msg_perm; /* 权限等,和 shmid_ds 开头一样 */
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; /* 最后发送进程 */
pid_t msg_lrpid; /* 最后接收进程 */
};
3.2 权限头 struct ipc_perm
cpp
struct ipc_perm {
key_t __key; /* 用户层给的 key */
uid_t uid;
gid_t gid;
unsigned short mode; /* 0666 那种 */
...
};
回指篇26:共享内存用 shmid_ds,消息队列用 msqid_ds,信号量用 semid_ds------三者开头都是 struct ipc_perm,这是 System V IPC 的统一头。记住这点,三件套接口长得像,根子就在这。
3.3 组织方式
内核把这些 msqid_ds 用数组/链表挂起来,ipcs -q 就是遍历给你看(ipcs / ipcrm 就是配套的查看 / 删除命令)。
四、消息队列四个接口(msgget / msgctl / msgsnd / msgrcv)
和共享内存接口高度同构,换前缀即可。
4.1 msgget(key, msgflg)
创建/获取队列,返回 msqid。msgflg 用 IPC_CREAT 或 IPC_CREAT | IPC_EXCL,语义和 shmget 完全一致。
cpp
int msgget(key_t key, int msgflg);
int shmget(key_t key, size_t size, int shmflg);
4.2 msgctl(msqid, cmd, buf)
控制。cmd 用 IPC_RMID 删除、IPC_STAT 取状态、IPC_SET 改权限。生命周期随内核,不手动删就泄漏。
cpp
int msgctl(int msqid, int op, struct msqid_ds *buf);
int shmctl(int shmid, int op, struct shmid_ds *buf);
4.3 msgsnd(msqid, &msgp, msgsz, msgflg)
cpp
int msgsnd(int msqid, const void msgp[.msgsz], size_t msgsz,int msgflg);
发送一条消息。msgp 指向你定义的消息结构体,msgsz 是数据长度。
4.4 msgrcv(msqid, &msgp, msgsz, msgtyp, msgflg)
cpp
ssize_t msgrcv(int msqid, void msgp[.msgsz], size_t msgsz, long msgtyp,int msgflg);
接收。msgtyp 指定要收哪种类型的消息,这就是"按标签取"。
注意 msgsnd / msgrcv 不是名字像就能乱用------它们要求你先定义消息结构,见下一节。
五、msgbuf 与消息类型
5.1 为什么自己定义结构
内核不认识你的业务数据,它只认"long mtype + 一段字节"。所以发送前你得自己定义结构:
cpp
struct msgbuf {
long mtype; /* 消息类型,必须 > 0 */
char mtext[1]; /* 实际数据,长度你定 */
};
5.2 mtype 与 msgtyp
- mtype 就是给每条消息"打标签",让不同进程/不同类型的消息区分开。比如 server 发 mtype=1,client 发 mtype=2,接收方用 msgrcv 的 msgtyp 参数挑 1 还是 2,实现"按类型分发"。
- mtext 才是真正的数据,长度由你定(数组或柔性数组)。
- msgsz 传的是 mtext 的长度,不含 mtype。
- 易错点:mtype 必须 > 0;msgrcv 的 msgtyp 为 0 收最早一条,>0 收该类型,<0 收类型小于等于 |msgtyp| 中最小的------这块面试爱问,先记住"按类型收"即可。
六、key 如何保证看到同一队列
6.1 ftok 算 key
两个进程各自 ftok 算出同一个 key。
6.2 key 与 msqid 的区别
msgget拿 key 去内核查:key 相同 → 同一个消息队列对象 → 拿到同一个 msqid(或各自拿到的 msqid 指向同一对象)。
再强调两个 ID:key 是跨进程约定的"名字"(用户态决定);msqid 是内核返回的本地句柄,只在本进程用。一句话:共享内存靠 key 接头,消息队列也靠 key 接头,思想上没区别。
七、为什么还要信号量(共享内存无保护 → 数据不一致)
这一段接住篇26 补的坑。
7.1 共享内存的致命伤
篇26 补讲过:共享内存最快,但无同步互斥、无访问控制。
7.2 问题出在哪:先认两个词
问题出在两个前提------① 存在一份没有被保护的共享资源;② 多个进程各自无序地访问它。把这两个前提翻译成两个词:
- 临界资源:就是那份"被多个进程共享、又没被保护"的资源。共享内存那块物理内存就是典型的临界资源。
- 临界区:进程里"访问临界资源"的那段代码。不在临界区里的代码叫非临界区。"保护机制保护的是临界代码,即变相保护临界资源"------保护临界代码,本质就是在保护临界资源。
后果很直接:server 先跑、client 还没写,server 读到空白/旧数据;多个进程并发写互相覆盖------数据不一致。
7.3 怎么保护:互斥、同步、原子性、加锁解锁
病根是"多个进程无序进临界区",那保护机制的目标就清楚了------给临界区立规矩。"怎么保护"拆开就是四条:
- 互斥:同一时刻只允许一个进程进临界区。这是底线------只要保证临界区里永远只有一个执行流,并发写互相覆盖的问题就消失。
- 同步:互斥只管"别撞车",但有些场景还要求"按约定顺序来"。比如生产者没放数据,消费者就不能读------同步就是让多个进程按约定次序访问资源,而不是各干各的。
- 加锁 / 解锁:互斥/同步落到代码上,就是进临界区前加锁、离开后解锁。锁就是一块"我正在用"的标记,别人看到就等着。
- 原子性:关键在于------加锁、解锁这步操作必须原子:要么全做完、要么根本没做,执行中途绝不能被别的进程打断。如果加锁本身能被打断,它就保不住互斥了。
7.4 信号量:把这套规矩变成工具
答案就是------信号量。它不是用来"传数据"的,是用来"保护"的:给共享内存那类裸资源套上一层秩序。
它本质是一个"保护共享资源的计数器":进程进临界区前做 P 操作(申请,相当于加锁),离开后做 V 操作(释放,相当于解锁)。而 P/V 之所以能当锁用,正是因为这两个操作本身是原子的。
八、信号量三个接口(semget / semctl / semop,详细下篇讲)
8.1 semget(key, nsems, semflg)
cpp
int semget(key_t key, int nsems, int semflg);
创建/获取一个信号量集(注意是"集",可含多个信号量)。
8.2 semctl(semid, semnum, cmd, ...)
cpp
int semctl(int semid, int semnum, int op, ...);
控制/初始化/删除,cmd 用 IPC_RMID 删除。
8.3 semop(semid, &sops, nsops)
cpp
int semop(int semid, struct sembuf *sops, size_t nsops);
真正干活的函数,做 P 操作(申请资源)和 V 操作(释放资源)。
8.4 一句话串起来
前面 7.3 已经讲清了"保护临界资源靠互斥/同步,手段是原子地加锁解锁";semop 的 P/V 就是干这个的------P 加锁、V 解锁,且原子执行。具体怎么用、P/V 内部咋保证原子。
九、消息队列与信号量怎么选
| 维度 | 消息队列 | 信号量 |
|---|---|---|
| 用途 | 传数据 | 护资源 |
| 数据形态 | 有类型数据块 | 不传数据,只传"许可" |
| 自带同步 | 有(按类型排队) | 无(它就是同步本身) |
| 典型搭配 | 进程间交数据 | 配共享内存防乱 |
| 一句话 | 管"传" | 管"守" |
实战:两个进程要"交换结构化数据" → 消息队列;要"多人改同一块共享内存" → 共享内存 + 信号量。
十、面试追问
| 问题 | 答案 |
|---|---|
| 消息队列和管道本质区别? | 管道是无边界字节流,消息队列是带类型的有边界数据块,可按 mtype 挑消息。 |
| msgsnd / msgrcv 为什么要有消息类型? | 让接收方按类型分发,不同标签对应不同处理逻辑,不必自己拆字节流。 |
| 消息队列生命周期随内核怎么理解? | 进程退出不自动删,需 ipcrm -q 或 msgctl(IPC_RMID),否则泄漏。 |
| 信号量和消息队列什么关系? | 同属 System V IPC 三件套,都靠 key+id、都随内核;一个传数据,一个管保护。 |
| 共享内存为什么要配信号量? | 共享内存无同步,并发读写会乱;信号量提供互斥/同步保护。 |
| struct msqid_ds 和 shmid_ds 为什么长得像? | 两者开头都是 struct ipc_perm,是 System V IPC 统一头。 |
十一、小结
- 消息队列:内核维护的有类型消息链表;接口
msgget / msgctl / msgsnd / msgrcv;用msgbuf+ mtype 实现按类型收发;靠 key 找到同一队列;描述结构msqid_ds(含ipc_perm)。 - 信号量:共享内存缺的那层保护,接口
semget / semctl / semop。
💬 哪句没懂,或和前面篇串不起来,评论区说,我补例子。
