
大家好,欢迎来到 huangjin007_ 的博客
⭐
个人主页:huangjin007_
🔥
文章收录专栏:Linux 内功修炼手册(系统篇)
总会有一些坚持能从冰封的土地里培育出十万朵怒放的蔷薇
Linux 系统篇(三十四) —— 消息队列、信号量与 IPC 资源管理
文章目录
- Linux 系统篇(三十四) —— 消息队列、信号量与 IPC 资源管理
-
- 一、前言
- 二、System V 消息队列
-
- 2.1 为什么需要消息队列?
- 2.2 消息队列的特点
- 2.3 消息队列的基本接口
- 2.4 消息结构是什么样的?
- 2.5 消息队列如何被管理?
- 2.6 消息队列的查看与删除
- 三、从共享内存引出同步与互斥
-
- 3.1 共享资源与临界资源
- 3.2 什么是临界区?
- 3.3 什么是互斥?
- 3.4 什么是同步?
- 四、System V 信号量
-
- 4.1 什么是信号量?
- 4.2 P 操作和 V 操作
- 4.3 为什么 P/V 操作必须具有原子性?
- 4.4 二元信号量与计数信号量
- 4.5 信号量与共享内存的关系
- 五、信号量的基本接口
-
- 5.1 创建或获取信号量
- 5.2 对信号量进行操作
- 5.3 控制信号量
- 六、Linux 内核如何管理 IPC 资源?
-
- 6.1 先描述:使用数据结构管理 IPC 资源
- 6.2 再组织:如何管理大量 IPC 对象?
- 6.3 key 和 id 有什么区别?
- 6.4 公共数据结构:kern_ipc_perm
- 6.5 共享内存与进程地址空间的关系
- 七、三种 System V IPC 的区别
- 结语:
一、前言
在上一篇文章中,我们学习了 Linux 进程间通信中的共享内存。
共享内存允许多个进程访问同一块物理内存,避免了数据在内核与用户空间之间反复拷贝,因此具有非常高的通信效率。
不过,共享内存虽然解决了进程之间的数据共享问题,却也带来了一个新的问题:
多个进程同时访问同一块共享内存,会不会出现数据混乱?
例如,进程 A 正在修改共享内存中的数据,进程 B 恰好也在读取或者修改这块数据,那么最终结果可能就不是我们预期的结果。
这就引出了两个新的概念:同步与互斥。
而 System V IPC 中的信号量,正是用来解决这类问题的重要机制。
当然,System V IPC 不只有共享内存,还包括消息队列和信号量。
本篇就从这几个方面展开,了解它们各自的作用,以及操作系统是如何组织和管理这些 IPC 资源的。
二、System V 消息队列
2.1 为什么需要消息队列?
之前学习了管道和共享内存,它们都能够实现进程间通信。
那么,能不能让进程之间直接发送一条消息,而不需要自己管理一块共享内存呢?
消息队列(Message Queue) 是由操作系统内核维护的一个消息链表,进程可以向队列中发送消息,也可以从队列中接收消息。
可以把它理解成一个公共邮箱:
- 进程 A 负责发送消息,相当于往邮箱里投递信件。
- 进程 B 负责接收消息,相当于从邮箱里取信。
- 操作系统负责维护这个邮箱以及其中的消息。
消息队列中的每条消息都带有一个类型,接收进程可以根据消息类型选择自己需要的消息。
例如:
- 进程 A 发送一条类型为 1 的消息。
- 进程 B 发送一条类型为 2 的消息。
- 进程 C 可以指定只接收类型为 1 的消息。
因此,消息队列不仅能够传递数据,还能够通过消息类型对数据进行区分。
2.2 消息队列的特点
2.3 消息队列的基本接口
使用消息队列,需要包含以下头文件:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
常用接口如下:
| ftok() | 生成 IPC key |
| msgget() | 创建或获取消息队列 |
| msgsnd() | 向消息队列发送消息 |
| msgrcv() | 从消息队列接收消息 |
| msgctl() | 控制消息队列 |
其中,前两个函数与共享内存中的使用方式基本一致。
1. 创建或获取消息队列
int msgget(key_t key, int msgflg);
- key:消息队列的标识键。
- msgflg:创建标志和权限。
常见的标志:
- IPC_CREAT:不存在则创建,存在则获取。
- IPC_CREAT | IPC_EXCL:不存在则创建,存在则报错。
2. 发送消息
int msgsnd(int msqid, const void *msgp,
size_t msgsz, int msgflg);
- msqid:消息队列标识符。
- msgp:指向待发送消息的指针。
- msgsz:消息正文的大小,不包括消息类型。
- msgflg:发送标志。
3. 接收消息
ssize_t msgrcv(int msqid, void *msgp,
size_t msgsz, long msgtyp,
int msgflg);
其中 msgtyp 用来指定接收消息的类型。
- msgtyp = 0:接收队列中的第一条消息。
- msgtyp > 0:接收指定类型的第一条消息。
- msgtyp < 0:按照消息类型的规则选择符合条件的消息。
4. 控制消息队列
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
常用命令:
- IPC_RMID:删除消息队列。
- IPC_STAT:获取消息队列的状态信息。
2.4 消息结构是什么样的?
消息队列中的消息不仅包含正文,还需要包含一个消息类型。
例如:
struct msgbuf
{
long mtype; // 消息类型,必须大于 0
char mtext[100]; // 消息正文
};
这里需要注意两个细节:
也就是说,发送消息时,消息类型用于区分消息,而正文才是真正需要传递的数据。
2.5 消息队列如何被管理?
既然消息队列由操作系统内核维护,那么内核就需要知道:
- 消息队列的权限是什么?
- 当前队列中有多少条消息?
- 队列中占用了多少字节?
- 最后一次发送和接收消息的时间是什么时候?
因此,Linux 内核使用 struct msqid_ds 描述消息队列的相关信息。
其中比较重要的成员有:
| msg_perm | IPC 权限信息 |
| msg_stime | 最后一次发送消息的时间 |
| msg_rtime | 最后一次接收消息的时间 |
| msg_ctime | 最后一次修改队列的时间 |
| msg_qnum | 当前消息数量 |
| msg_qbytes | 队列允许的最大字节数 |
| msg_lspid | 最后发送消息的进程 PID |
| msg_lrpid | 最后接收消息的进程 PID |
这里我们不需要死记所有成员,知道内核会通过一个数据结构描述和管理消息队列即可。
2.6 消息队列的查看与删除
Linux 提供了两个常用命令:
ipcs -q
查看当前系统中的消息队列。
ipcrm -q msqid
删除指定的消息队列。
注意:System V IPC 资源不会因为创建它的进程退出而自动消失,因此使用完毕后要记得清理。
三、从共享内存引出同步与互斥
消息队列介绍完了,接下来重点了解信号量。
不过,在学习信号量之前,需要先弄清楚几个并发编程中的基本概念。
3.1 共享资源与临界资源
假设现在有两个进程 A 和 B。
它们都可以访问同一块共享内存:

这块共享内存就是它们共同能够访问的资源。
共享资源:多个执行流能够访问的公共资源。
如果这个共享资源需要进行保护,避免多个执行流同时访问时出现问题,那么它就成为需要保护的临界资源。
临界资源:一次只允许一个执行流访问,或者需要按照特定规则进行访问的共享资源。
3.2 什么是临界区?
既然临界资源需要保护,那么具体应该保护什么?
假设有一段代码:
int count = 10;
// 对共享资源进行操作
count++;
这里的 count 是共享资源,而 count++ 就是访问共享资源的代码。
临界区:进程中访问临界资源的代码段。
与之对应,不访问临界资源的代码段叫作非临界区。
因此,一个进程的代码可以简单划分为:

这里有一个非常重要的理解:
保护共享资源,本质上就是保护访问共享资源的代码。
因为真正可能产生并发问题的,是多个执行流同时执行访问共享资源的代码。
3.3 什么是互斥?
假设进程 A 和进程 B 同时访问同一个临界资源。
如果两个进程同时修改数据,就可能导致数据不一致。
那么,我们能不能规定:
同一时刻,只允许一个进程访问临界资源?
这就是互斥。
互斥(Mutual Exclusion):任何时刻,只允许一个执行流访问临界资源。
可以把它理解成房间只有一把钥匙:
- A 拿到钥匙,进入房间。
- B 没有钥匙,只能等待。
- A 使用完毕,归还钥匙。
- B 才能进入。
通过这种方式,保证同一时间只有一个执行流访问临界资源。
3.4 什么是同步?
互斥解决的是多个执行流同时访问资源的问题。
但是,有时候我们不仅要求不能同时访问,还要求多个执行流按照一定的顺序执行。
例如:
进程 A 负责生产数据,进程 B 负责消费数据。
如果 A 还没有生产数据,B 就开始消费,那么显然会出现问题。
所以我们希望:
这就是同步。
同步(Synchronization):多个执行流访问共享资源时,按照一定的顺序或条件进行协同。
可以简单记忆:
- 互斥:你用的时候我不能用。
- 同步:你完成之后,我才能继续。
四、System V 信号量
4.1 什么是信号量?
前面我们已经知道,共享资源可能出现并发访问问题,因此需要互斥和同步机制。
那么,操作系统如何实现这些机制呢?
System V IPC 提供了一种重要的工具:信号量(Semaphore)。
信号量本质上是一个计数器,用来表示可用资源的数量。
可以把它理解成电影院的座位管理系统。
假设电影院有 16 个座位:
- 初始时,计数器为 16。
- 每卖出一张票,可用座位数量减 1。
- 每退回一张票,可用座位数量加 1。
- 当没有空座位时,新的观众只能等待。
它实际上是一种资源预订机制:
想要使用资源,必须先申请;使用完毕之后,再释放资源。
4.2 P 操作和 V 操作
信号量主要通过两种操作来管理资源。
P 操作:申请资源。
申请资源时,信号量的值减 1。
如果没有足够的资源,执行流就需要等待,直到资源可用。
V 操作:释放资源。
释放资源时,信号量的值加 1。
例如:
// 假设初始有 16 个可用资源
int sem = 16;
// P 操作:申请资源
sem—;
// 使用资源
// …
// V 操作:释放资源
sem++;
上面只是帮助理解的伪代码,真正的信号量 P/V 操作必须由操作系统保证原子性,不能直接使用普通的 sem++ 和 sem– 代替。
4.3 为什么 P/V 操作必须具有原子性?
这里又出现了一个问题。
假设两个进程同时申请资源:
sem—;
这看起来只是一个简单的减法,但在实际执行时,它通常包含读取、修改、写回等多个步骤。
如果两个进程同时执行,就可能发生:
- A 读取到 sem = 1。
- B 也读取到 sem = 1。
- A 将 sem 修改为 0。
- B 也将 sem 修改为 0。
原本只有一个资源,却让两个进程都认为自己成功申请到了资源。
这就出现了问题。
所以,P/V 操作必须保证原子性。
原子性:一个操作要么完整执行,要么完全不执行,中间不能被其他执行流干扰。
也就是说,申请资源和释放资源的过程必须是一个不可分割的整体。
4.4 二元信号量与计数信号量
根据资源数量的不同,信号量可以分为两种常见形式。
1. 二元信号量
信号量的值只有 0 和 1 两种状态。
可以用来实现互斥:
- 1:资源可以使用。
- 0:资源已经被占用。
2. 计数信号量
信号量的值可以大于 1,用来管理多个同类资源。
例如:
一个电影院有 16 个座位,那么就可以使用计数信号量管理这 16 个座位。
简单总结:
- 二元信号量:控制一个资源是否可用。
- 计数信号量:控制多个资源的可用数量。
4.5 信号量与共享内存的关系
现在可以把前面的知识联系起来了。
共享内存负责解决数据共享的问题,而信号量负责解决多个进程访问共享资源时的同步与互斥问题。
两者配合使用,可以让进程间通信更加安全。
例如:

共享内存本身并不提供同步和互斥机制,信号量也不负责传递实际业务数据。
它们各自负责不同的工作,通常需要配合使用。
五、信号量的基本接口
使用 System V 信号量,需要包含:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
常用接口如下:
| ftok() | 生成 IPC key |
| semget() | 创建或获取信号量集合 |
| semop() | 对信号量进行 P/V 操作 |
| semctl() | 控制信号量 |
5.1 创建或获取信号量
int semget(key_t key, int nsems, int semflg);
参数:
- key:信号量集合的标识键。
- nsems:集合中信号量的数量。
- semflg:创建标志和权限。
返回值:成功返回信号量集合的标识符,失败返回 -1。
注意:这里创建的是信号量集合,一个集合中可以包含多个信号量。
5.2 对信号量进行操作
int semop(int semid, struct sembuf *sops,
size_t nsops);
其中,struct sembuf 用来描述具体的操作:
struct sembuf
{
unsigned short sem_num; // 信号量编号,从 0 开始
short sem_op; // 操作类型
short sem_flg; // 操作标志
};
最重要的是 sem_op:
- sem_op = -1:P 操作,申请资源。
- sem_op = +1:V 操作,释放资源。
sem_num 用来指定对集合中的哪个信号量进行操作。
5.3 控制信号量
int semctl(int semid, int semnum,
int cmd, ...);
常见命令:
| IPC_RMID | 删除信号量集合 |
| IPC_STAT | 获取信号量集合状态 |
| SETVAL | 设置指定信号量的值 |
| GETVAL | 获取指定信号量的值 |
这里需要特别注意一个问题:
信号量创建之后,需要显式设置初始值。
通常使用:
semctl(semid, 0, SETVAL, arg);
将集合中编号为 0 的信号量设置为指定值。
其中 arg 是 semctl 所需的联合体参数,通常需要根据系统头文件定义 union semun。
六、Linux 内核如何管理 IPC 资源?
前面学习了消息队列、共享内存和信号量。
现在换一个角度:
假设操作系统中存在大量消息队列、共享内存和信号量,内核应该如何管理它们?
总不能把所有资源随意放在内存中,然后每次使用时都到处寻找吧?
这就涉及操作系统中的一个重要思想:
先描述,再组织。
6.1 先描述:使用数据结构管理 IPC 资源
操作系统需要管理资源,就必须知道资源的各种属性。
例如:
- 资源的标识是什么?
- 谁创建了这个资源?
- 当前资源的权限是什么?
- 资源是否已经被删除?
因此,内核会使用不同的数据结构描述不同的 IPC 对象。
例如:
| 消息队列 | msg_queue |
| 共享内存 | shmid_kernel |
| 信号量 | sem_array |
每种 IPC 资源都有自己的数据结构,用来记录对应的信息。
6.2 再组织:如何管理大量 IPC 对象?
只有描述还不够。
如果系统中有很多 IPC 对象,内核还需要将这些对象组织起来,方便查找和管理。
在 Linux 2.6.11 的相关实现中,System V IPC 使用了统一的管理机制。
可以简单理解为:

虽然三种 IPC 资源的具体功能不同,但它们具有一些共同属性:
- key:用于标识 IPC 对象。
- uid/gid:所有者信息。
- mode:访问权限。
- 序列号:辅助管理对象标识。
6.3 key 和 id 有什么区别?
前面创建 IPC 资源时,我们经常使用:
key_t key = ftok(".", 0x66);
然后通过:
int shmid = shmget(key, 4096, IPC_CREAT | 0666);
创建共享内存。
这里出现了两个不同的概念:
key:用户层用于查找和约定 IPC 对象的键值。
id:内核创建或获取 IPC 对象之后返回的标识符。
例如:

不同进程只要使用相同的 key,就可以尝试获取同一个 IPC 对象。
不过,key 并不保证绝对唯一,ftok() 也可能发生碰撞,因此不能把它理解成永远不会重复的唯一编号。
另外,shmid、msqid 和 semid 都是内核返回的标识符,不应该简单地把它们当作 key 使用。
6.4 公共数据结构:kern_ipc_perm
在 Linux 2.6.11 的源码实现中,三种 IPC 对象都有一个共同的管理信息结构:
struct kern_ipc_perm
{
spinlock_t lock;
int deleted;
key_t key;
uid_t uid;
gid_t gid;
uid_t cuid;
gid_t cgid;
mode_t mode;
unsigned long seq;
};
它主要记录:
- lock:用于保护相关数据。
- deleted:删除状态。
- key:IPC 对象的键值。
- uid/gid:所有者信息。
- cuid/cgid:创建者信息。
- mode:访问权限。
- seq:序列号。
为什么要设计一个公共的数据结构?
因为消息队列、共享内存和信号量虽然用途不同,但它们都需要管理权限、标识和删除状态。
把共同的部分抽取出来,就可以使用统一的方式进行管理。
可以把它理解成面向对象中的继承思想,只不过这里是通过 C 语言结构体组合等方式实现的。
6.5 共享内存与进程地址空间的关系
最后再回顾一下上一篇文章中的共享内存。
共享内存为什么能够让多个进程访问同一块物理内存?
因为内核会将同一块共享内存映射到不同进程的虚拟地址空间中。
从内核管理的角度看,可以简单理解为:

这里涉及几个重要结构:
- shm_file:shmid_kernel 中用于关联内核文件对象的成员,指向一个 struct file 对象。Linux 内核借助它支持共享内存的映射管理。
- mm_struct:描述和管理进程的虚拟地址空间。
- vm_area_struct:描述进程虚拟地址空间中的一段连续区域。
通过这些结构,内核能够管理共享内存与各个进程地址空间之间的映射关系。
其中,shm_nattch 可以用来记录当前关联到该共享内存段的进程数量。
以上内核数据结构主要参考 Linux 2.6.11 的实现,不同 Linux 内核版本的具体字段和组织方式可能存在差异。
七、三种 System V IPC 的区别
前面分别介绍了消息队列、共享内存和信号量,现在把它们放在一起比较。
| 主要作用 | 传递消息 | 共享数据 | 同步与互斥 |
| 是否传递数据 | 是 | 是 | 否 |
| 通信方式 | 内核消息队列 | 直接访问共享内存 | 通过计数器控制资源 |
| 通信效率 | 需要内核参与消息传递 | 效率最高 | 主要用于控制 |
| 同步机制 | 有阻塞与唤醒机制 | 本身不提供 | 本身用于同步与互斥 |
| 生命周期 | 随内核 | 随内核 | 随内核 |
简单记忆:
- 消息队列:把消息放进队列,其他进程来取。
- 共享内存:多个进程共同访问一块内存。
- 信号量:通过计数器管理资源,控制访问顺序。
结语:
今天的内容到这里就结束了,希望你能有所收获~
干货整理到手抖,觉得有用的话,赏个三连回回血?__(:ᗤ」ㄥ)_ _
网硕互联帮助中心





评论前必须登录!
注册