System V IPC:Linux进程通信的标准方案
前言:欢迎各位光临本博客,这里小编带你直接手撕**,文章并不复杂,愿诸君**耐其心性,忘却杂尘,道有所长!!!!
《C语言》
《C++深度学习》
《Linux》
《数据结构》
《数学建模》
文章目录
- 消息队列:带类型的进程通信
- 1. 消息队列的核心逻辑
- 2. 关键特性:按“消息类型”收发
- 3. 消息队列的核心结论
- (1)通信本质
- (2)OS 的管理逻辑
- (3)进程互通的前提
- 4. 消息队列的删除(Linux 命令)
- 5. 消息队列的内核数据结构
- 6. 与共享内存的对比:逻辑一致,细节不同
- 7. 消息队列的核心函数(代码)
- 函数参数解读:
- 自定义消息结构体示例:
- 8. Linux 查看消息队列(命令)
- 9. 关键细节:消息类型必须 > 0
- 信号量:保护共享资源的“锁”
- 1. 背景:共享内存的痛点与解决方案
- (1)共享内存的优缺点
- (2)两个核心概念
- (3)解决方案:信号量
- 2. 保护临界区的两种需求
- (1)互斥(Mutual Exclusion)
- (2)同步(Synchronization)
- 3. 关键概念:原子性
- 4. 信号量是什么?
- 5. 理解信号量:资源预定机制
- 6. 信号量的使用场景:分块保护共享资源
- 7. 信号量的核心操作:P 操作与 V 操作
- (1)伪代码思路
- (2)P、V 操作的原子性保证
- 8. 特殊信号量:二元信号量(互斥锁)
- 9. 复盘:访问共享资源的两种方式
- 10. 信号量的本质:不是整数,是数据结构
- 11. 信号量与“通信”的关系
- 12. 信号量的系统调用接口(代码)
- (1)创建信号量:semget 函数
- (2)信号量个数:nsems 参数说明
- (3)删除信号量:semctl 函数(IPC_RMID)
- (4)信号量操作:semop 函数(P/V 操作)
- (5)信号量初始化:semctl 函数(SETVAL)
- 13. 信号量的内核数据结构与 IPC 统一管理
- (1)信号量的内核结构体:semid_ds
- (2)获取信号量状态:semctl(IPC_STAT)
- (3)OS 对 IPC 资源的统一管理
- (4)IPC 资源的内核组织方式
- (5)访问结构体其他字段:指针强转
- (6)C 语言实现“多态”的技巧
System V 是 Linux 中进程间通信(IPC) 的经典标准,核心包含三大组件:共享内存、消息队列、信号量。它们的核心逻辑一致——由操作系统维护一份“公共资源”,让不同进程能“看到同一份资源”,从而实现数据传递或协同操作。下面我们逐个拆解核心组件,结合图片和代码讲透用法。
消息队列:带类型的进程通信
消息队列是 Linux 提供的 IPC 方案(虽现在用得少,但理解其设计很重要),本质是操作系统维护的一个“消息链表”:进程 A 把消息按“类型”放进队列,进程 B 按“类型”从队列取消息,实现定向通信。
1. 消息队列的核心逻辑
IPC 的关键是“进程看到同一份资源”:
- 步骤1:进程向 OS 申请创建一个消息队列;
- 步骤2:进程 A 发送消息时,会给消息打上“类型标签”,然后把消息存入队列;
- 步骤3:进程 B 接收消息时,可指定“只看某类消息”,忽略其他类型;
- 步骤4:双方通过同一个队列通信,队列由 OS 统一管理。
下图清晰展示了这个流程:
2. 关键特性:按“消息类型”收发
消息队列和普通队列的最大区别是消息带类型,这让通信更灵活:
- 每个消息都有一个“类型标志”(比如 int 类型的数字);
- 接收进程可以“按需筛选”:只接收自己需要的类型,不用处理无关消息(比如进程 B 只收类型为 2 的消息,忽略类型 1、3 的消息)。
下图展示了“按类型筛选消息”的逻辑:
3. 消息队列的核心结论
(1)通信本质
消息队列提供了“进程间传递带类型数据块”的方式——重点是“按类型区分消息”,确保进程只处理自己关心的数据。
(2)OS 的管理逻辑
OS 对消息队列的管理遵循“先描述,再组织”:
- 先描述:用一个结构体(比如内核中的
msgid_ds
)记录队列的信息(所有者、权限、消息数量等); - 再组织:用链表或数组把这些结构体管理起来,方便查询和操作。
(3)进程互通的前提
两个进程要通信,必须“找到同一个消息队列”,关键是约定同一个 Key 值(类似门锁的钥匙,只有钥匙对了才能打开门)。
下图展示了“Key 关联消息队列”的逻辑:
4. 消息队列的删除(Linux 命令)
消息队列创建后会一直存在(除非主动删除),若不删除会占用系统资源。删除需用 ipcrm
命令,指定消息队列的 ID(msgid):
下图是删除消息队列的操作示例:
示例命令:
ipcrm -q 123456
(-q 表示操作消息队列,123456 是消息队列的 ID)
5. 消息队列的内核数据结构
OS 用 msgid_ds
结构体管理消息队列,下图展示了该结构体的核心字段(比如权限、所有者、消息链表指针等):
核心字段说明:
msg_perm
:存队列的权限和 Key 值;msg_first
/msg_last
:指向队列中第一个/最后一个消息的指针;msg_qnum
:队列中当前的消息数量。
6. 与共享内存的对比:逻辑一致,细节不同
消息队列和共享内存的核心逻辑完全一致(都是 OS 维护资源,进程通过 Key 访问),最大区别在“数据收发方式”:
- 共享内存:进程直接读写同一块内存,速度快;
- 消息队列:进程通过“发送/接收函数”操作队列,速度慢,但自带“按类型筛选”功能。
这就是 System V 标准的设计思路——统一框架,差异化细节。
7. 消息队列的核心函数(代码)
消息队列的收发依赖两个关键函数:msgsnd
(发送)和 msgrcv
(接收),下图展示了函数原型和参数说明:
函数参数解读:
msqid
:消息队列的 ID(由msgget
函数创建时返回);msgp
:消息缓冲区指针(需自定义结构体,第一个字段必须是“消息类型”,比如long mtype
);msgsz
:消息体的长度(不包含“消息类型”字段的长度);msgflg
:标志位(比如IPC_NOWAIT
表示“非阻塞”,没消息时直接返回错误,不等待)。
自定义消息结构体示例:
// 消息结构体:第一个字段必须是 long 类型的消息类型
struct msgbuf {long mtype; // 消息类型(必须 > 0)char mtext[1024]; // 消息体(存实际数据)
};
8. Linux 查看消息队列(命令)
用 ipcs -q
命令可查看系统中所有的消息队列,包括队列 ID、所有者、消息数量等信息,下图是命令输出示例:
输出字段说明:
msqid
:消息队列 ID;owner
:队列所有者(用户名);message size
:单个消息的最大长度;messages
:队列中当前的消息数量。
9. 关键细节:消息类型必须 > 0
发送消息时,mtype
(消息类型)必须是正整数(若 <= 0,msgsnd
函数会返回错误)。下图展示了“消息类型”的约束:
信号量:保护共享资源的“锁”
共享内存虽快,但有个致命问题——没有保护机制:若多个进程同时读写同一块内存,会导致数据不一致(比如两个进程同时给同一个变量加 1,最终结果可能少加一次)。信号量就是解决这个问题的“锁”。
1. 背景:共享内存的痛点与解决方案
(1)共享内存的优缺点
- 优点:进程直接访问同一份内存,通信速度极快;
- 缺点:无保护机制,多个进程同时操作“共享资源”会导致数据不一致。
(2)两个核心概念
要解决问题,先明确两个定义:
- 临界资源:被多个进程共享的资源(比如共享内存中的变量、文件);
- 临界区:进程中“访问临界资源的代码段”(比如读写共享内存的那几行代码)。
下图展示了“临界区与临界资源”的关系:两个进程的临界区都访问同一个临界资源,若无保护会出问题:
(3)解决方案:信号量
信号量的作用是“保护临界区”——通过控制临界区的访问权限,间接保护临界资源,避免数据不一致。
2. 保护临界区的两种需求
信号量要实现两种核心功能:
(1)互斥(Mutual Exclusion)
“任何时候,只有一个进程能进入临界区”——类似银行 ATM 机:一次只能一个人用,其他人必须排队。
(2)同步(Synchronization)
“多个进程按约定顺序进入临界区”——比如进程 A 先往共享内存写数据,进程 B 才能读,不能反过来。
3. 关键概念:原子性
信号量能实现保护的核心是“原子操作”:
- 定义:操作要么“全做完”,要么“全不做”,没有中间状态(比如取钱时,“扣钱 + 吐钞”要么都成功,要么都失败,不会只扣钱不吐钞);
- 为什么需要?因为信号量本身也是“共享资源”(多个进程都要操作它),原子性确保信号量的操作不会被打断,避免“锁失效”。
4. 信号量是什么?
教材中也叫“信号灯”,本质是一个计数器,用来记录“临界资源的可用数量”。比如:
- 电影院有 10 个座位(临界资源),信号量初始值就是 10;
- 买一张票(进程申请资源):信号量减 1(从 10→9);
- 退一张票(进程释放资源):信号量加 1(从 9→10);
- 若信号量为 0,说明资源已用完,进程需等待。
5. 理解信号量:资源预定机制
信号量的核心逻辑是“先预定,再使用”:进程要访问临界资源,必须先“预定”(申请信号量),预定成功才能访问,访问完再“释放”(归还信号量)。
下图展示了“信号量预定流程”:
6. 信号量的使用场景:分块保护共享资源
信号量可将共享内存“分块保护”:把共享内存分成多个小块,每个小块对应一个信号量,进程访问某块时,只需要申请对应块的信号量,不影响其他块的使用。
下图展示了“分块保护”的逻辑:
7. 信号量的核心操作:P 操作与 V 操作
信号量的所有逻辑都围绕两个原子操作:P 操作(申请资源) 和 V 操作(释放资源)。
(1)伪代码思路
下图展示了 P、V 操作的伪代码逻辑:
-
P 操作(申请):
- 检查信号量是否 > 0(有资源可用);
- 若 > 0,信号量减 1,进入临界区;
- 若 = 0,进程阻塞(排队等待),直到有其他进程释放资源。
-
V 操作(释放):
- 信号量加 1;
- 若有进程在阻塞等待,唤醒其中一个进程。
(2)P、V 操作的原子性保证
下图明确了“P、V 操作必须原子执行”——任何时候都不能被打断,否则会导致信号量计数错误:
8. 特殊信号量:二元信号量(互斥锁)
若信号量的取值只有 0 或 1,则称为“二元信号量”,本质就是“互斥锁”:
- 初始值为 1(表示资源可用);
- 进程申请时(P 操作):1→0,进入临界区;
- 进程释放时(V 操作):0→1,允许其他进程进入;
- 场景:临界资源只能被一个进程访问(比如共享内存中的单个变量)。
下图用“超级 VIP 通道”举例:一次只能一个人进,就是二元信号量的逻辑:
9. 复盘:访问共享资源的两种方式
结合信号量,访问共享资源有两种常见方式,下图清晰展示了区别:
-
方式 1:整体使用(二元信号量)
- 整个共享资源用一个信号量(初始值 1),一次只能一个进程访问;
- 场景:资源不可分割(比如一个文件的写操作)。
-
方式 2:区块使用(计数信号量)
- 资源分块,每个块对应一个信号量(初始值 = 块数),多个进程可同时访问不同块;
- 场景:资源可分割(比如共享内存分成 5 块,可 5 个进程同时访问)。
10. 信号量的本质:不是整数,是数据结构
很多人误以为信号量是“整数”,其实它是一个包含“计数器 + 等待队列”的数据结构:
- 计数器(sem_val):记录资源可用数量;
- 等待队列(sem_queue):存“申请资源失败而阻塞的进程”,释放资源时唤醒。
下图展示了信号量的数据结构:
11. 信号量与“通信”的关系
信号量不直接传递数据,但也是 IPC 的一种:
- 通信的本质是“进程间的协同”,不只是“传数据”;
- 信号量通过“控制进程访问资源的顺序”,实现进程间的“同步/互斥协同”,这也是一种通信。
下图展示了“信号量实现进程协同”的逻辑:
12. 信号量的系统调用接口(代码)
信号量的操作依赖 3 个核心函数:semget
(创建)、semctl
(控制/初始化/删除)、semop
(P/V 操作)。
(1)创建信号量:semget 函数
函数作用:创建一个“信号量集”(可包含多个信号量),返回信号量集的 ID(semid)。
下图展示了 semget
的函数原型和参数:
参数解读:
key
:约定的键值(进程互通的关键,和消息队列的 Key 逻辑一致);nsems
:要创建的信号量个数(比如创建 3 个信号量,就设为 3);semflg
:标志位(比如IPC_CREAT|0666
,表示“创建信号量集,并设置权限为 666”)。
示例:创建一个包含 2 个信号量的集合:
key_t key = ftok(".", 66); // 生成 Key(当前目录 + 66 作为种子)
int semid = semget(key, 2, IPC_CREAT | 0666); // 创建 2 个信号量
if (semid == -1) { perror("semget"); exit(1); }
(2)信号量个数:nsems 参数说明
nsems
表示“信号量集中包含的信号量数量”,每个信号量有独立的计数器。下图展示了“信号量集与信号量的关系”:
示例:
nsems=5
表示创建一个包含 5 个信号量的集合,编号分别为 0~4。
(3)删除信号量:semctl 函数(IPC_RMID)
函数作用:删除整个信号量集(删除后,所有进程都无法访问)。
下图展示了 semctl
删除信号量的用法:
参数解读:
semid
:信号量集 ID;semnum
:信号量编号(删除整个集合时,该参数可忽略,设为 0 即可);cmd
:操作命令(IPC_RMID
表示“删除信号量集”)。
示例:删除信号量集:
int ret = semctl(semid, 0, IPC_RMID); // 删除整个信号量集
if (ret == -1) { perror("semctl"); exit(1); }
(4)信号量操作:semop 函数(P/V 操作)
函数作用:对信号量集中的某个信号量执行 P 操作(-1)或 V 操作(+1)。
下图展示了 semop
的函数原型和核心参数:
关键是 struct sembuf
结构体(定义操作细节):
struct sembuf {unsigned short sem_num; // 信号量编号(0~nsems-1)short sem_op; // 操作:-1(P操作),+1(V操作)short sem_flg; // 标志位:0(阻塞),IPC_NOWAIT(非阻塞)
};
示例:对编号 0 的信号量执行 P 操作:
struct sembuf sop;
sop.sem_num = 0; // 操作编号 0 的信号量
sop.sem_op = -1; // P 操作(申请资源)
sop.sem_flg = 0; // 阻塞等待int ret = semop(semid, &sop, 1); // 1 表示执行 1 个操作
if (ret == -1) { perror("semop"); exit(1); }
(5)信号量初始化:semctl 函数(SETVAL)
信号量创建后,计数器初始值是随机的,需用 semctl
的 SETVAL
命令初始化。
下图展示了初始化的用法:
参数解读:
cmd=SETVAL
:表示“设置信号量的初始值”;- 第 4 个参数(可选):是一个
union semun
结构体,用来传递初始值(val)。
union semun
结构体定义(需自己声明):
union semun {int val; // 用于 SETVAL:设置信号量的初始值struct semid_ds *buf; // 用于 IPC_STAT/IPC_SET:获取/设置信号量集状态unsigned short *array; // 用于 GETALL/SETALL:获取/设置所有信号量的初始值
};
示例:将编号 0 的信号量初始化为 1(二元信号量):
union semun su;
su.val = 1; // 初始值设为 1
int ret = semctl(semid, 0, SETVAL, su); // 初始化编号 0 的信号量
if (ret == -1) { perror("semctl"); exit(1); }
注意:SETVAL
操作不是原子的,若多个进程同时初始化同一个信号量,可能出问题,需确保只有一个进程执行初始化(比如用父进程初始化,子进程只操作)。
13. 信号量的内核数据结构与 IPC 统一管理
(1)信号量的内核结构体:semid_ds
OS 用 semid_ds
结构体管理信号量集,核心字段包含“权限、信号量个数、最后操作时间”等,下图展示了结构体的组成:
核心字段:
sem_perm
:存信号量集的 Key、ID、权限;sem_nsems
:信号量集中的信号量个数;sem_otime
:最后一次执行semop
的时间。
(2)获取信号量状态:semctl(IPC_STAT)
用 semctl
的 IPC_STAT
命令可获取信号量集的状态(比如权限、个数),需传入 struct semid_ds
结构体存储结果。
下图展示了用法:
示例:获取信号量集状态:
struct semid_ds sem_buf;
int ret = semctl(semid, 0, IPC_STAT, &sem_buf); // 获取状态存入 sem_buf
if (ret == -1) { perror("semctl"); exit(1); }// 打印信号量个数和最后操作时间
printf("信号量个数:%d\n", sem_buf.sem_nsems);
printf("最后操作时间:%ld\n", sem_buf.sem_otime);
(3)OS 对 IPC 资源的统一管理
System V 中的共享内存、消息队列、信号量,被 OS 当作“同一种资源”管理——它们的内核结构体都包含一个 kern_ipc_perm
结构体(作为第一个成员),用来存储“Key、ID、权限”等公共信息。
下图展示了三种 IPC 资源的结构体共性:
(4)IPC 资源的内核组织方式
OS 用一个“指针数组(id_ary)”管理所有 IPC 资源:
- 数组的每个元素是
kern_ipc_perm*
指针,指向一个 IPC 资源(共享内存/消息队列/信号量); - 资源的 ID(比如 shmid、msgid、semid)就是数组的“下标”(比如 semid=5,表示数组下标 5 的指针指向该信号量集)。
下图是 IPC 资源的内核组织图:
下图展示了“ID 与数组下标的关系”:
(5)访问结构体其他字段:指针强转
因为 kern_ipc_perm
是每个 IPC 结构体的“第一个成员”,所以可以将 kern_ipc_perm*
指针强转为具体的结构体指针,从而访问其他字段。
下图展示了强转逻辑:
示例:将 kern_ipc_perm*
强转为 semid_ds*
:
// id_ary[semid] 是指向该信号量集的 kern_ipc_perm* 指针
struct kern_ipc_perm *perm_p = id_ary[semid];
// 强转为 semid_ds*,访问 sem_nsems 字段
struct semid_ds *sem_ds_p = (struct semid_ds *)perm_p;
printf("信号量个数:%d\n", sem_ds_p->sem_nsems);
下图展示了强转的代码细节:
(6)C 语言实现“多态”的技巧
这种“用公共结构体指针管理不同类型资源,强转后访问具体字段”的技术,是 C 语言实现“多态”的经典方式——用统一的接口(kern_ipc_perm*
)管理不同的 IPC 资源,需要时再转为具体类型。
下图总结了这种技巧: