Linux 消息队列和信号量初了解
文章目录
- System V 消息队列(Message Queue)
- 信号量与并发控制
- 信号量(Semaphore)的本质
- 为什么需要信号量
- 引入通信机制的原因
- 并发访问带来的新问题
- 公共资源(Shared Resource)
- 临界资源(Critical Resource)
- 临界区(Critical Section)
- 非临界区(Non-Critical Section)
- 并发程序的结构
- 并发控制思想总结
- 互斥(Mutual Exclusion)
- 原子性(Atomicity)
- 管道与共享内存
- 并发控制的五个核心概念
- 为什么不能用普通全局变量作为计数器
- 信号量存在的意义
- 信号量的特点
- 进程访问共享资源时的基本流程
- 信号量的预定机制
- 信号量必须是共享资源
- 信号量自身安全性问题
- 信号量计数值的含义
- 信号量与共享资源划分
- 信号量的基本操作
- 信号量的核心作用
- 资源分配与资源定位问题
- System V 信号量编程接口
- 结构体特性
- 类型区别
- 底层原因
- Linux IPC 可以这样设计的原因
System V 消息队列(Message Queue)
消息队列概念
消息队列(Message Queue) 是操作系统在内核中提供的一种 进程间通信(IPC, Inter-Process Communication)机制。
它在 内核空间维护一个队列结构,用于在不同进程之间传递数据。
消息队列具有以下特点
- 由 操作系统内核维护
- 数据以 消息(message)为单位 进行传递
- 消息按照 队列(FIFO)顺序存储
- 支持 多个进程读写
- 消息具有 类型(type),可按类型选择性读取
因此,消息队列可以理解为:
由操作系统提供的内核级 FIFO 队列,用于进程之间传递结构化消息。
消息队列通信模型
通信过程如下:
进程A (Sender)
│
│ msgsnd()
▼
┌───────────┐
│ Message │
│ Queue │ ← 内核维护
└───────────┘
▲
│ msgrcv()
│
进程B (Receiver)
- 发送进程 将消息写入消息队列
- 接收进程 从消息队列读取消息
不同进程通过 同一个消息队列 ID 访问同一队列,从而实现通信。
消息结构
消息队列中的数据以 消息块(message block) 的形式存储。
在用户空间中通常定义如下结构:
struct msgbuf
{
long mtype; // 消息类型
char mtext[SIZE]; // 消息数据
};
字段说明:
| 字段 | 含义 |
|---|---|
mtype | 消息类型(必须 > 0) |
mtext | 实际数据内容 |
消息类型的作用
消息队列支持 按类型读取消息。
例如:
| 类型 | 含义 |
|---|---|
| 1 | Client 发送 |
| 2 | Server 发送 |
| 这样: |
- Server 可以只读取 type = 1
- Client 可以只读取 type = 2
从而实现 双向通信。
创建或获取消息队列
使用 msgget() 系统调用:
#include <sys/msg.h>
int msgget(key_t key, int msgflg);
参数说明:
| 参数 | 说明 |
|---|---|
| key | 消息队列的唯一标识 |
| msgflg | 创建标志 |
| 常见标志: |
IPC_CREAT
IPC_EXCL
0666
例如:
key_t key = ftok("./", 1);
int msgid = msgget(key, IPC_CREAT | 0666);
返回值:
msgid(消息队列标识符)
发送消息
使用 msgsnd():
int msgsnd(int msgid,
const void *msgp,
size_t msgsz,
int msgflg);
参数说明:
| 参数 | 含义 |
|---|---|
| msgid | 消息队列ID |
| msgp | 消息结构体 |
| msgsz | 消息数据大小 |
| msgflg | 标志 |
| 示例: |
struct msgbuf msg;
msg.mtype = 1;
strcpy(msg.mtext, "hello");
msgsnd(msgid, &msg, sizeof(msg.mtext), 0);
接收消息
使用 msgrcv():
ssize_t msgrcv(int msgid,
void *msgp,
size_t msgsz,
long msgtyp,
int msgflg);
参数说明:
| 参数 | 含义 |
|---|---|
| msgid | 消息队列ID |
| msgp | 接收消息结构 |
| msgsz | 数据大小 |
| msgtyp | 读取指定类型 |
| msgflg | 标志 |
| 示例: |
struct msgbuf msg;
msgrcv(msgid, &msg, sizeof(msg.mtext), 1, 0);
表示:
只读取 type = 1 的消息
控制消息队列
使用 msgctl():
int msgctl(int msgid, int cmd, struct msqid_ds *buf);
常见操作:
删除消息队列
msgctl(msgid, IPC_RMID, NULL);
获取消息队列属性
msgctl(msgid, IPC_STAT, &buf);
内核中的消息队列结构
操作系统需要管理所有消息队列,因此在内核中会维护一个数据结构
struct msqid_ds
其主要内容包括:
| 字段 | 含义 |
|---|---|
| msg_perm | 权限信息 |
| msg_stime | 最后发送时间 |
| msg_rtime | 最后接收时间 |
| msg_ctime | 最后修改时间 |
| msg_qnum | 队列中消息数量 |
| msg_qbytes | 队列最大字节数 |
| 其中: |
msg_perm
内部包含:
key
uid
gid
mode
用于描述 IPC 权限信息。
System V IPC 统一设计思想
System V IPC(共享内存 / 信号量 / 消息队列)具有相同的设计模式:
1 描述资源
ipc_perm
2 使用 key 标识资源
ftok()
3 获取资源
xxxget()
4 控制资源
xxxctl()
| IPC 类型 | 创建 | 控制 |
|---|---|---|
| 共享内存 | shmget | shmctl |
| 消息队列 | msgget | msgctl |
| 信号量 | semget | semctl |
| 消息队列的核心特征: |
- 内核维护的 FIFO 队列
- 以 消息块 为单位传输数据
- 支持 按类型读取消息
- 支持 多进程通信
- 属于 *System V IPC 机制
信号量与并发控制
信号量(Semaphore)的本质
信号量(Semaphore)是一种用于进程或线程之间进行同步与互斥控制的同步原语。
信号量通过原子操作(P/V 操作)实现对临界资源的访问控制,从而保证多个执行流在访问共享资源时满足互斥性、同步性以及原子性要求。
信号量(Semaphore)本质上是一种计数器,用于描述系统中某类资源的可用数量。也就是公共资源中资源数量多少的问,
其核心作用是:
用于实现进程或线程之间的同步与互斥控制。
信号量通常表示:
- 当前资源总量
- 当前可用资源数量
- 对共享资源访问的许可数量
例如:
sem = 3
表示:
系统中当前有 3 个可用资源。
为什么需要信号量
在多进程或多线程环境中,多个执行单元可能会 同时访问同一资源。
如果没有任何保护机制,就可能出现 数据不一致问题(Data Inconsistency)。
例如:
进程 A 正在向共享内存写入:
ABCD1234
如果写入过程尚未完成:
ABCD
此时进程 B 读取该数据,就会得到 不完整的数据。
这种情况称为:
并发访问导致的数据不一致问题
引入通信机制的原因
进程具有 地址空间独立性,默认情况下无法直接访问彼此的数据。
因此,为了实现进程之间的数据交换与协作,需要引入 进程间通信(IPC)机制。
通信机制的核心思想是:
让多个进程能够访问同一份资源,从而实现数据交换。
常见 IPC 机制包括:
- 管道(Pipe)
- 共享内存(Shared Memory)
- 消息队列(Message Queue)
- 信号量(Semaphore)
并发访问带来的新问题
虽然 IPC 机制解决了 进程通信问题,但同时也带来了新的问题:
并发访问冲突。
多个进程如果 同时访问共享资源,可能会导致:
- 数据不一致
- 数据覆盖
- 状态错误
因此需要引入 同步与互斥机制。
公共资源(Shared Resource)
在并发系统中:
可以被多个进程或线程同时访问的资源称为公共资源(Shared Resource)。
典型的公共资源包括:
- 共享内存
- 文件
- 网络连接
- 管道
- 消息队列
当多个执行单元同时访问这些资源时,就会产生 并发问题。
临界资源(Critical Resource)
当共享资源需要通过同步机制进行保护时,该资源称为:
临界资源(Critical Resource)
即:
一次只允许一个执行单元访问的共享资源。
临界区(Critical Section)
访问临界资源的代码区域称为临界区(Critical Section)。
例如:
shared_memory = data;
这段代码如果访问共享资源,就属于 临界区代码。
系统必须保证:
同一时刻只有一个执行单元进入临界区。
非临界区(Non-Critical Section)
程序中 不访问临界资源的代码区域 称为:
非临界区(Non-Critical Section)
例如:
printf("hello");
sleep(1);
return 0;
这些代码不涉及共享资源,因此不需要同步控制。
并发程序的结构
一个典型的并发程序通常包含两类代码:
程序代码
├─ 非临界区代码(Non-Critical Section)
├─ 临界区代码(Critical Section)
└─ 非临界区代码
其中:
- 临界区代码需要同步保护
- 非临界区代码可以并发执行
并发控制思想总结
并发系统中的设计逻辑通常如下:
- 进程具有独立性,无法直接通信
- 为实现通信,引入 IPC 机制
- IPC 使多个进程能够访问共享资源
- 共享资源带来并发访问冲突
- 为解决冲突,引入同步机制
- 信号量用于实现资源访问控制
互斥(Mutual Exclusion)
当多个执行单元需要访问同一临界资源时,需要保证:
在任意时刻仅允许一个执行单元进入临界区。
这种访问控制策略称为:
互斥(Mutual Exclusion)
互斥通常通过 锁(Lock)机制 实现,例如:
- 互斥锁(Mutex)
- 信号量(Semaphore)
- 自旋锁(Spinlock)
现实生活中的类比:
一个自习室一次只允许一个人进入学习,进入后将门锁上,其余人员必须等待当前使用者离开。
原子性(Atomicity)
原子性概念
原子性(Atomicity) 指的是:
一个操作要么完全执行成功,要么完全不执行,不会出现中间状态。
即:
Only two states:
完成 / 未执行
不存在“部分完成”的情况。
原子性示例:银行转账
银行转账通常包含两个操作:
- 从转出账户扣除金额
- 向转入账户增加金额
例如:
A账户:1000 → 800
B账户:1000 → 1200
如果第一步完成而第二步失败,就会造成数据错误。
因此转账操作必须保证:
要么全部成功
要么全部回滚
这就是 原子性保证。
原子性与并发控制的关系
在并发系统中:
- 临界区操作通常需要 原子性保证
- 原子性通常依赖 互斥机制
因此三者之间存在密切关系:
共享资源
↓
临界资源
↓
临界区访问
↓
互斥控制
↓
保证原子性
管道与共享内存
管道通信
| 概念 | 对应对象 |
|---|---|
| 公共资源 | 管道缓冲区 |
| 临界资源 | 被内核保护的管道 |
| 临界区 | read() / write() 操作 |
| 由于 Linux 管道内部已经实现同步机制,因此开发者通常无需手动控制互斥。 |
共享内存通信
| 概念 | 对应对象 |
|---|---|
| 公共资源 | 共享内存区域 |
| 临界资源 | 加锁后的共享内存 |
| 临界区 | 读写共享内存的代码 |
| 共享内存 默认不具备同步机制,因此需要配合: |
- 信号量
- 互斥锁
进行访问控制。
并发控制的五个核心概念
在并发程序设计中,需要理解以下关键术语:
| 概念 | 含义 |
|---|---|
| 公共资源 | 可被多个执行单元访问的资源 |
| 临界资源 | 需要保护的共享资源 |
| 临界区 | 访问临界资源的代码 |
| 互斥 | 同一时刻只允许一个执行单元访问资源 |
| 原子性 | 操作要么完成,要么不执行 |
| 此外还有一个重要概念: | |
| 同步(Synchronization) | |
| 该概念主要用于描述 执行顺序约束问题,通常在多线程模型中进一步讨论。 | |
| 整体逻辑关系如下: |
进程通信
↓
共享资源
↓
并发访问问题
↓
临界资源
↓
临界区
↓
互斥控制
↓
原子性保证
为什么不能用普通全局变量作为计数器
在进程环境下,直接使用普通变量(如全局整数)无法实现跨进程同步,原因包括:
- 进程地址空间隔离
不同进程拥有独立的虚拟地址空间,因此普通全局变量在不同进程中并不共享。 - 写时拷贝(Copy-on-Write)机制
在fork()创建子进程后,父子进程的变量虽然初始值相同,但在写操作发生时会产生副本,因此无法作为共享计数器。 - 缺乏原子操作保证
普通变量的自增或自减操作并非原子操作,在并发访问情况下可能产生竞态条件(Race Condition)。
因此,如果需要在多个进程之间维护一个共享计数器并保证操作原子性,必须借助操作系统提供的同步机制,如信号量。
信号量存在的意义
信号量主要解决两个核心问题:
- 控制对共享资源的并发访问
在多进程或多线程系统中,多个执行流可能同时访问同一共享资源。
如果缺乏控制,将会导致:- 数据竞争(Race Condition)
- 数据不一致
- 程序逻辑错误
信号量通过计数机制,限制同时访问资源的执行流数量。
信号量的特点
信号量是一种重要的并发控制机制,其核心特点包括:
- 本质是一个整型计数器
- 用于表示系统中可用资源数量
- 通过原子操作保证并发安全
- 用于实现互斥和同步
- 支持资源预约与访问控制
在多进程或多线程系统中,信号量是实现共享资源管理和执行流协作的重要工具。
进程访问共享资源时的基本流程
在多进程或多线程并发环境中,如果执行流需要访问某一部分共享资源(Shared Resource),通常需要借助**信号量(Semaphore)**进行资源管理。基本流程如下:
- **申请资源(Acquire Resource)**进程在访问共享资源之前,必须首先对信号量执行 P 操作(wait / down):
P(sem)
该操作会将信号量计数值减 1:
sem = sem - 1
如果操作成功,表示当前进程已经成功预定一个资源单位。
2. 访问共享资源(Critical Section Execution)
当 P 操作成功后,进程即可进入临界区(Critical Section),访问共享资源。
3. 释放资源(Release Resource)
当进程完成资源访问后,需要执行 V 操作(signal / up):
V(sem)
该操作会将信号量计数值加 1:
sem = sem + 1
表示当前进程释放一个资源单位,使其他等待进程能够继续访问资源。
因此,进程访问共享资源的典型模式为:
P(sem); // 申请资源
访问共享资源; // 临界区
V(sem); // 释放资源
其中:
- P 操作:表示资源申请或资源预定
- V 操作:表示资源释放
信号量的预定机制
信号量机制本质上提供了一种资源预约(Reservation)机制。
当进程成功执行 P 操作 时,意味着:
- 系统中一定存在可用资源
- 该进程已经成功占用或预定了一个资源单位
即使该进程尚未立即使用该资源,系统也保证该资源已经为其保留。
如果 P 操作失败,则说明: - 当前系统中已经没有可用资源
- 该进程必须进入等待状态
这种机制保证了: - 系统资源不会被超额分配
- 每个资源单位只能被一个执行流占用
信号量必须是共享资源
在并发系统中,所有进程在访问共享资源之前,都必须先对同一个信号量进行操作。
因此必须满足一个条件:
所有相关进程必须能够访问同一个信号量对象。
换言之:
信号量本身也必须是一个共享资源。
否则,不同进程无法对同一个计数器进行操作,也就无法实现并发控制。
因此,在操作系统实现中:
- System V 信号量
- POSIX 信号量
都提供了跨进程共享的信号量机制。
信号量自身安全性问题
所有进程在访问公共资源之前都必须先申请信号量,必须先申请信号量的前提。所有进程必须先看到同一个信号量,信号量是公共资源
由于信号量本身会被多个执行流同时访问,因此会产生一个关键问题:
信号量自身的数据操作是否安全?
信号量内部主要涉及的操作包括:
- 计数器自减(申请资源)
- 计数器自增(释放资源)
如果这些操作不是原子操作(Atomic Operation),就可能出现: - 多个进程同时修改计数器
- 计数器值错误
- 资源分配失控
因此,操作系统必须保证:
信号量的 P 操作和 V 操作是原子操作。
即:
- 在执行过程中不会被其他进程打断
- 操作不可分割
这也是信号量能够安全管理资源的关键。
信号量初始值为1,代表使用整体,为二元信号量
信号量计数值的含义
信号量的值通常表示:
当前系统中可用资源的数量
例如:
| 信号量值 | 含义 |
|---|---|
| N | 系统有 N 个可用资源 |
| 0 | 没有可用资源 |
| <0 | 有进程正在等待资源 |
电影院例子对应操作系统中的资源管理模型:
| 现实世界 | 操作系统概念 |
|---|---|
| 放映厅 | 共享资源 |
| 座位 | 资源单位 |
| 观众 | 进程 / 线程 |
| 电影票 | 信号量 |
| 购票 | 获取信号量 |
| 入座 | 访问资源 |
| 系统中存在一个计数器: |
count = 座位总数
当观众购票时:
count--
当座位数为 0 时:
- 说明资源已经被全部占用
- 不再允许新的观众购票
这一机制保证: - 资源不会被超额分配
- 每个资源单位只会被一个执行流占用
这正是信号量在操作系统中的典型应用场景。
信号量与共享资源划分
在并发系统中,共享资源的使用方式通常有两种:
整体使用(互斥访问)
资源只能被一个执行流独占访问,例如:
- 管道(Pipe)
- 打印机
- 文件写操作
这种情况下通常使用:
二值信号量(Binary Semaphore)或互斥锁(Mutex)
分块使用(资源池)
共享资源可以划分为多个独立部分,例如:
- 共享内存的多个数据块
- 连接池
- 线程池
- 缓冲区
此时可以使用:
计数信号量(Counting Semaphore)
表示系统中有 N 个可用资源单元,允许最多 N 个执行流同时访问。
信号量的基本操作

信号量用于解决 临界资源访问冲突问题。
其核心机制包括:
P 操作(Wait)
减少信号量值:
sem--
如果资源不足,则阻塞等待。
V 操作(Signal)
增加信号量值:
sem++
释放资源并唤醒等待进程。
P/V 操作的原子性
在操作系统中,信号量的两种基本操作为:
P 操作(Proberen / Wait)
作用:
- 申请资源
- 减少信号量计数值
逻辑如下:
P(S):
S = S - 1
if S < 0:
阻塞当前进程
V 操作(Verhogen / Signal)
作用:
- 释放资源
- 增加信号量计数值
逻辑如下:
V(S):
S = S + 1
if S <= 0:
唤醒等待进程
这两个操作必须由操作系统保证其原子性。
信号量的核心作用
信号量可以实现两种并发控制机制:
互斥(Mutual Exclusion)
如果信号量初始值为:
sem = 1
则表示系统中只有 1 个资源单位。
当某个进程执行:
P(sem)
后:
sem = 0
此时其他进程再执行 P 操作将会被阻塞
因此,同一时间只能有一个进程访问共享资源。
这种信号量称为:
二元信号量(Binary Semaphore)
其作用等价于互斥锁(Mutex)。
资源计数(Counting Semaphore)
如果信号量初始值为:
sem = N
表示系统中存在 N 个资源单位。
最多允许 N 个进程同时访问资源。
这种信号量称为:
计数信号量(Counting Semaphore)。
资源分配与资源定位问题
需要注意的是:
信号量只负责解决资源数量控制问题,即:
是否存在可用资源。
但信号量并不会决定:
具体使用哪一个资源。
例如:
- 在共享内存中有多个数据块
- 在缓冲区中有多个缓冲单元
当进程获得信号量后,还需要通过程序逻辑确定: - 实际访问的资源位置
- 避免多个进程访问同一资源单元
这一部分通常通过: - 索引管理
- 队列
- 环形缓冲区
- 其他同步机制
由程序员在代码中实现。
System V 信号量编程接口
在 Linux System V IPC 机制中,信号量由内核统一管理,并通过一组系统调用进行操作。
相关头文件:
#include <sys/ipc.h>
#include <sys/sem.h>
semget()
int semget(key_t key, int nsems, int semflg);
作用:
创建或获取一个信号量集合(Semaphore Set)。
参数说明:
| 参数 | 含义 |
|---|---|
| key | IPC 资源标识符 |
| nsems | 信号量集合中信号量的数量 |
| semflg | 创建标志(如 IPC_CREAT) |
| 注意: |
- System V 信号量是以“集合”的形式存在
- 一次可以创建多个信号量
例如:
semget(key, 1, IPC_CREAT | 0666);
表示:
- 创建一个包含 1 个信号量 的信号量集合。
返回值: - 成功:返回 信号量集合 ID
- 失败:返回
-1
semctl()
int semctl(int semid, int semnum, int cmd, ...);
作用:
对信号量集合执行控制操作。
参数说明:
| 参数 | 含义 |
|---|---|
| semid | 信号量集合 ID |
| semnum | 信号量下标 |
| cmd | 控制命令 |
| 常见命令: |
| 命令 | 作用 |
|---|---|
| IPC_RMID | 删除信号量 |
| SETVAL | 设置初始值 |
| GETVAL | 获取当前值 |
| IPC_STAT | 获取信号量状态 |
| 示例: | |
| 删除信号量: |
semctl(semid, 0, IPC_RMID);
信号量的数据结构
System V 信号量在内核中使用如下结构:
struct semid_ds {
struct ipc_perm sem_perm; // 权限信息
time_t sem_otime; // 最后操作时间
time_t sem_ctime; // 最后修改时间
unsigned long sem_nsems; // 信号量数量
};
其中:
struct ipc_perm
表示 IPC 权限信息,包括:
- UID
- GID
- 访问权限
这一结构与: - 共享内存
- 消息队列
的 IPC 权限结构类似。
信号量的 P/V 操作
System V 信号量通过 semop() 函数执行 P/V 操作。
semop()
int semop(int semid, struct sembuf *sops, size_t nsops);
作用:
对信号量集合执行操作(P/V)。
sembuf 结构
struct sembuf {
unsigned short sem_num; // 信号量编号
short sem_op; // 操作类型
short sem_flg; // 操作标志
};
参数说明:
| 字段 | 含义 |
|---|---|
| sem_num | 信号量编号 |
| sem_op | 操作类型 |
| sem_flg | 操作标志 |
sem_op 的取值
| sem_op | 含义 |
|---|---|
| -1 | P 操作(申请资源) |
| +1 | V 操作(释放资源) |
| 示例: | |
| P 操作: |
struct sembuf op;
op.sem_num = 0;
op.sem_op = -1;
op.sem_flg = 0;
semop(semid, &op, 1);
V 操作:
op.sem_op = 1;
semop(semid, &op, 1);
批量信号量操作
System V 信号量支持:
一次对多个信号量执行操作
例如:
struct sembuf ops[10];
semop(semid, ops, 10);
这样可以在一次系统调用中对多个信号量执行 P/V 操作。
这种机制具有以下优势:
- 减少系统调用次数
- 保证多个操作的原子性
- 提高并发控制效率
结构体特性
简介
在 System V IPC 机制 中,
不同类型的 IPC 资源:
- 共享内存(Shared Memory)
- 消息队列(Message Queue)
- 信号量(Semaphore)
虽然功能不同,但 其内核数据结构的第一个成员均为 struct ipc_perm。
因此,Linux 内核可以利用 结构体首成员地址与结构体对象地址数值相同的特性,
通过 struct ipc_perm* 指针统一管理所有 IPC 资源对象。
结构体对象的地址,和结构体第一个成员的地址,在数值上是相同的。
例如:
struct Obj
{
int a;
char b;
};
struct Obj myobj;
printf("%p\n", &myobj);
printf("%p\n", &myobj.a);
打印出来的地址数值是一样的。
更严谨的表达应该是:
在 C 语言中,结构体对象的地址与其第一个成员的地址在数值上相同,但类型不同。
或者更标准一点可以写成:
The address of a structure object is equal (in value) to the address of its first member, although the pointer types are different.
中文专业描述:
结构体对象的起始地址等于其第一个成员的起始地址(数值相同),但两者的指针类型不同。
类型区别
虽然地址值一样,但类型不同:
| 表达式 | 类型 |
|---|---|
&myobj | struct Obj * |
&myobj.a | int * |
所以:
&myobj // struct Obj*
&myobj.a // int*
地址值相同,但类型不同。
底层原因
因为 结构体在内存中是连续存储的。
结构体布局:
struct Obj
{
int a;
char b;
};
内存布局:
地址1000 -> a (4字节)
地址1004 -> b (1字节)
假设:
&myobj = 1000
&myobj.a = 1000
&myobj.b = 1004
所以:
&myobj == &myobj.a (数值相同)
Linux IPC 可以这样设计的原因

比如:
共享内存结构
struct shmid_ds {
struct ipc_perm shm_perm;
...
};
消息队列
struct msqid_ds {
struct ipc_perm msg_perm;
...
};
信号量
struct semid_ds {
struct ipc_perm sem_perm;
...
};
它们 第一个成员都是
struct ipc_perm
所以:
&shmid_ds == &shmid_ds.shm_perm
&msqid_ds == &msqid_ds.msg_perm
&semid_ds == &semid_ds.sem_perm
于是 Linux 内核可以统一管理:
struct ipc_perm *perms[]
数组里放的其实是:
&shm.shm_perm
&msg.msg_perm
&sem.sem_perm
但通过地址偏移又可以还原回完整结构体。
这就是:
Linux 内核的面向对象设计技巧(。
这种设计在内核中通常叫:
结构体首成员复用(Struct embedding)
或者
伪继承(Pseudo inheritance in C)
Linux 内核大量使用这种方式。
例如:
- 设备驱动
- 文件系统
- 网络协议栈
- IPC
为什么 System V IPC 三种资源可以统一管理?
可以这样回答:
因为 System V IPC 的核心数据结构(
shmid_ds、msqid_ds、semid_ds)的第一个成员都是struct ipc_perm。
根据 C 语言内存布局规则,结构体对象地址与第一个成员地址数值相同,因此内核可以使用ipc_perm*统一管理不同类型的 IPC 对象,实现类似面向对象的“基类管理子类”的效果。
访问具体资源的方法
当需要访问某个 IPC 资源时,可以:
1️.先从数组中取出指针
perms[i]
2️.根据资源类型进行 类型转换
例如访问共享内存:
struct shmid_ds *shm =
(struct shmid_ds *)perms[i];
由于:
&shm == &shm.shm_perm
因此强制类型转换后即可访问完整结构体。
类型识别机制
为了避免类型不确定的问题,可以额外维护 资源类型字段:
struct ipc_object
{
int type;
struct ipc_perm *perm;
};
其中:
type表示资源类型perm指向ipc_perm
访问流程:
1 判断 type
2 根据 type 进行强转
3 获取具体资源结构
Linux 内核真实实现

Linux 内核中通过 ipc_ids 结构 来管理 IPC 资源:
struct ipc_ids {
struct ipc_id *entries;
};
其中:
entries → IPC 对象数组
每个对象内部都包含:
ipc_perm
因此可以通过统一结构管理:
- 共享内存
- 消息队列
- 信号量
本质设计思想
这种设计在系统编程中属于:
- 结构体嵌入(Struct Embedding)
将 公共结构体作为第一个成员嵌入到不同结构体中。 - 面向对象模拟(C 语言)
类似:
基类: ipc_perm
子类: shmid_ds / msqid_ds / semid_ds
实现 类似继承的效果。
3. 多态管理
通过:
ipc_perm *
统一管理不同资源类型
| 面向对象概念 | Linux IPC 实现 |
|---|---|
| 基类 | ipc_perm |
| 子类 | shmid_ds / msqid_ds / semid_ds |
| 基类指针 | ipc_perm* |
| 子类对象 | 具体 IPC 资源对象 |
| 这种模式等价于 基类指针指向子类对象。 |
与 C++ 多态的类比
在 C++ 中类似:
class IPCPerm {};
class Shm : public IPCPerm {};
class Msg : public IPCPerm {};
class Sem : public IPCPerm {};
然后:
IPCPerm* objs[];
不同对象存入同一数组,通过基类指针统一管理。
Linux 内核采用 C 语言结构体嵌入 + 指针转换 实现了类似机制
如果在结构体中加入 函数指针:
struct ipc_perm {
void (*operate)(void *);
};
不同 IPC 类型可以绑定不同实现:
shm_operate()
msg_operate()
sem_operate()
调用方式:
perm->operate(obj);
这样即可实现:
C 语言中的动态分发(类似虚函数机制)。
与 Linux “一切皆文件”的设计一致
Linux 内核大量使用这种设计,例如:
- 文件系统
- 设备驱动
- 网络协议栈
- IPC 系统
典型实现方式:
结构体嵌入 + 函数指针
用于实现 - 多态
- 抽象接口
- 统一资源管理
因此:Linux 如何统一管理 System V IPC?
Linux 内核在 System V IPC 的实现中,将
ipc_perm作为共享内存、消息队列和信号量结构体的第一个成员。由于结构体首成员地址与结构体对象地址数值相同,因此内核可以通过ipc_perm*指针统一管理不同类型的 IPC 对象,并在访问时通过类型转换恢复具体结构体,从而实现统一资源管理。
更多推荐


所有评论(0)