news 2026/9/7 21:58:34

Linux系统篇28——通信(五):信号量专篇:从电影院订票到内核一张大表,把信号量一次讲透

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Linux系统篇28——通信(五):信号量专篇:从电影院订票到内核一张大表,把信号量一次讲透

前言:篇27 给信号量只留了两句话——本质是计数器,P 加锁 V 解锁,三个接口也只点了名。这篇补上整块拼图:用电影院订票讲透"预订"本质,拆开 P/V 的三种语义和整套用法,再往内核走一步,看 OS 怎么用一张大表统一管理三件套。


目录

​编辑

一、信号量是什么:本质是一个计数器

二、电影院订票:理解信号量的最佳类比

2.1 买票就是预订:先占座,再使用,用完归还

2.2 不买票就抢座的后果:数据互相覆盖

2.3 记住这套类比:后面每个概念都靠它解释

三、共享内存为什么非配它不可

3.1 危险在哪:太多进程同时乱写同一块内存

3.2 解决思路:把共享内存划分成固定区域

3.3 核心规则:访问任何一块之前,先申请

3.4 完整流程:一次规范使用要走六步

3.5 资源用完了怎么办:阻塞等待,不空转

四、申请(P)和释放(V)具体做了什么

4.1 内核给每个信号量记的四笔账

4.2 一次操作怎么填:写一张"操作单"交给内核

4.3 操作值的三种语义:申请 / 释放 / 等归零

4.4 两个重要标志:不等待 / 自动回滚

4.5 为什么要原子执行:要么全做,要么全不做

五、怎么用:创建、初始化、加减、删除

5.1 创建:怎么拿到一个信号量集(semget)

5.2 控制:初始化、查状态、删除都靠它(semctl)

5.3 传参用的联合体:为什么必须自己定义(semun)

5.4 真正的 P/V:一次能同时操作一批信号量(semop)

六、一次为什么要管多个信号量

6.1 两个参数别搞混:建几个 vs 动第几个

6.2 为什么要打包成"集":多个锁一次原子操作

6.3 系统上限:一个集最多能放多少信号量

七、进程退出它还在:生命周期与清理

7.1 怎么查看和删除:命令行 + 代码调用

7.2 进程退出它还在:用完必须自己删

7.3 删除的副作用:阻塞中的进程会被一并唤醒

八、内核怎么存放、又怎么统一管理三件套

8.1 内核怎么"描述"信号量:一张档案表(semid_ds)

8.2 档案开头的权限头:谁建的、权限多大(ipc_perm)

8.3 容易踩坑:同名结构,用户态快照 ≠ 内核实体

8.4 三类资源共用的"桥接头"(kern_ipc_perm)

8.5 组织方式:把描述结构统一挂进一张表

8.6 更正:现代内核已不用数组,换成 IDR 基数树

8.7 三个容易忽略的细节

8.8 一张图看懂全局:从内核命名空间到实际资源

九、信号 / 信号量 / 互斥锁:易混概念一次分清

9.1 信号 vs 信号量:一个"叫你一下",一个"数你一下"

9.2 信号量 vs 互斥锁:谁都能解 vs 只有主人能解

9.3 二元 vs 多元:一次放一个,还是放 N 个

9.4 三种档案结构对比:分别管什么、差在哪

十、面试官高频追问与参考答案

十一、小结


一、信号量是什么:本质是一个计数器

篇27 第七节已经把铺垫做完了:共享内存快是快,但没有同步互斥,多个进程一起写,数据当场就不一致。临界资源、临界区、互斥、同步这些词当时都拆过,这里不再重复。

现在正面回答那个被跳过的问题:信号量到底是什么?

信号量(semaphore)翻译过来就是"信号灯"。它的本质,是一个计数器。

这个计数器计的不是别的,就是临界资源里还剩几份资源可用。信号量保护的不是数据本身,是"资源还有几份"这个数——谁想用一份资源,先过计数器这一关。

用信号灯来想:

  • 灯亮(计数 > 0):还有资源,可以进
  • 灯灭(计数 = 0):资源用完了,等着

有种常见说法是"信号量本身不是共享资源"。这句话得按语境理解——它想强调的是"信号量不是被保护的那个临界资源",而不是说它不被共享。

事实正好相反:信号量本身必须由多个进程共享访问,否则同步根本不成立

semval 是内核里的一份数据,所有进程通过同一个 semid 操作的是同一个值。A 进程 P 完减 1,B 进程必须看到这个减过的值——如果两个进程各看各的私有计数器,那 P/V 就纯粹是自我安慰,谁也拦不住谁。

还有个更硬的证据:内核专门给信号量加了自旋锁、把 semop 做成原子操作。如果它不被并发访问,这些保护根本没必要存在。需要保护,本身就是"它是共享的"的证明。

所以准确的分法是两层:

  • 信号量共享对象(内核里多进程可见可改,安全由内核锁兜底)
  • 但它不是它保护的那份共享资源——被保护的临界资源是共享内存那块数据,信号量是管它的人

面试问"信号量是共享资源吗",只答一个"不是"会露怯;把这两层都讲出来才站得住脚。

那多个进程怎么找到同一个信号量?System V 的做法很直接:让多个进程通过同一个 key 看到同一个信号量。key 机制篇25、篇27 都讲过(ftok 生成、semget 拿它换 semid),三件套在这一层是同一套逻辑。

顺带埋个伏笔:信号量按取值分两种——

  • 二元信号量:只有 0 和 1,一次只放一个进程进(互斥的极致)
  • 多元信号量:初值 N,一次最多放 N 个(资源池)

这两种的差别后面易混点对比里细说。


二、电影院订票:理解信号量的最佳类比

讲信号量最好用的例子是电影院订票。把它讲透,后面的接口全是它的翻译。

把信号量想成一个电影院的票池:

电影院信号量
放映厅的座位临界资源里的一份份资源
座位总数信号量初值 N
买票P 操作(申请,计数 -1)
看完退场V 操作(释放,计数 +1)
票卖光了计数归零,后来的人等着

2.1 买票就是预订:先占座,再使用,用完归还

买票这个动作,本质是先占号,再使用,用完归还

  • 你买到票,座位就被你预订了——哪怕你还没进场坐
  • 你看完离场,这个座位重新回到票池,别人才能买
  • 全程座位总数不变,变的只是"还剩几张票"这个数

信号量干的就是这件事:P 一次,等于从资源池里预订一份;V 一次,等于归还一份。

2.2 不买票就抢座的后果:数据互相覆盖

如果没有票,人人都直接冲进放映厅找座——两个人同时看上同一个座,都坐下,这就撞了。

放到进程世界:没有信号量,多个进程同时扑向同一块共享内存,你写你的我写我的,互相覆盖,数据不一致。这就是篇27 说的共享内存"裸奔"问题。

电影院不让你抢座,靠的是"必须先买票";OS 不让进程乱抢资源,靠的是"必须先申请信号量"。

2.3 记住这套类比:后面每个概念都靠它解释

记住这个电影院,它不止管这一节:

  • sem_op 为什么分正负 → 买票是 -1,退票是 +1
  • SEM_UNDO 是干嘛的 → 人跑了(进程崩了),票自动退回
  • 信号量集是什么 → 多个影厅,每厅一套票

后面每节都会回来对一次表。


三、共享内存为什么非配它不可

有了"预订"这个概念,现在看它具体怎么落到共享内存上。

3.1 危险在哪:太多进程同时乱写同一块内存

问题起点很明确:最怕过多进程访问同一块内存

共享内存快,就快在数据不用在内核和用户态之间倒手。但它没有任何访问控制——谁都能往同一块上写,写得越多,覆盖得越狠。所以有两个"不要":

  • 不要多个进程访问同一个位置
  • 不要过多进程同时压上来

3.2 解决思路:把共享内存划分成固定区域

信号量介入的思路,是把共享内存划分成不同区域,每块区域一次只给一个进程用。划分之后每块的大小是固定的——固定,才好配一张票(一个信号量单位)管一块。

3.3 核心规则:访问任何一块之前,先申请

核心规则一句话:所有进程访问临界资源的一小块,必须先申请信号量。

翻译成电影院:想坐座位,先买票。没有例外。

3.4 完整流程:一次规范使用要走六步

一次规矩的信号量使用,全流程六步:

  1. 创建/获取信号量集(semget)
  2. 初始化:设初值 = 座位总数(semctl SETVAL)
  3. 申请资源,计数 -1(semop 做 P)
  4. 使用共享资源
  5. 释放资源,计数 +1(semop 做 V)
  6. 删除信号量集(semctl IPC_RMID)

3.5 资源用完了怎么办:阻塞等待,不空转

计数归零时再来进程,P 操作会让它阻塞等待——注意是睡眠那种等,不是原地转圈的忙等(忙等就是 while 死循环白白烧 CPU)。等到有人 V(退票),内核把它唤醒接着干。

这也解释了为什么信号量叫"同步"工具:它不光拦人,还负责排队叫号。


四、申请(P)和释放(V)具体做了什么

前面一直说"P 减 1、V 加 1",但内核里这套操作到底长什么样?这一节拆开。

4.1 内核给每个信号量记的四笔账

先看 man page(semop(2))给的答案,集合里每一个信号量都有四个关联值:

unsigned short semval; /* 信号量当前值 */ unsigned short semzcnt; /* 等它变成 0 的进程数 */ unsigned short semncnt; /* 等它变大的进程数 */ pid_t sempid; /* 最后一个操作它的进程 PID */

买票视角:semval 是剩余票数;semncnt 是排着队等放票的人数;semzcnt 是等着"票全部卖完"这个状态的人数;sempid 记着最后一张票是谁买的。

4.2 一次操作怎么填:写一张"操作单"交给内核

对信号量的每次操作,你都要填一张操作单(struct sembuf)交给 semop:

struct sembuf { unsigned short sem_num; /* 操作集合里第几个信号量(从 0 开始) */ short sem_op; /* 操作值:正 / 负 / 零,三种语义 */ short sem_flg; /* 标志:IPC_NOWAIT / SEM_UNDO */ };

注意类型:sem_op 是 short(有符号),所以它能带正负号;另外两个是 unsigned short。这个类型细节别忽略,它就是三种语义的物理基础。

4.3 操作值的三种语义:申请 / 释放 / 等归零

操作值含义行为电影院
< 0P 操作,申请 |sem_op| 份资源够减就减,不够就阻塞(semncnt++)买票,没票就排队
> 0V 操作,释放 sem_op 份资源直接加,永不阻塞退票
= 0等待计数变成 0已是 0 就过;不是 0 就阻塞(semzcnt++)等"票全部卖完"再进场收场

三种里只有 V 永不阻塞——退票永远是允许的,天经地义。

操作值 = 0 平时用得少,典型场景是等资源被"全部用完"再统一回收,比如管理进程等所有使用者散场。

4.4 两个重要标志:不等待 / 自动回滚

IPC_NOWAIT:不阻塞。拿不到资源直接返回错误,errno 置 EAGAIN。而且 man page 明文写了:整个操作数组一个都不会执行。对应电影院里"看没票了,不等了,走人"。

SEM_UNDO:进程退出时,内核自动回滚它做过的 P/V。对应"人跑了(进程崩了),票自动退回池子"。为什么需要它、内核怎么实现,第十节追问里专门讲——这里先记住电影院版本就够。

4.5 为什么要原子执行:要么全做,要么全不做

一次 semop 传进去的整个操作数组,要么全部执行完,要么一个都不执行,中间不会被别的进程插队。这也是系统调用进内核后由内核加锁保证的。为什么必须这样,第六节讲完信号量集再回来看。


五、怎么用:创建、初始化、加减、删除

System V 三件套的用法形态高度一致,信号量这套,就是篇27 消息队列那套的"信号量版"。逐个拆。

5.1 创建:怎么拿到一个信号量集(semget)

int semget(key_t key, int nsems, int semflg);
  • key:哪个信号量集(ftok 生成,篇25 讲过)
  • nsems:这个集合里放几个信号量
  • semflg:IPC_CREAT / IPC_EXCL / 权限位(0666 那套)
  • 返回:semid

注意它创建的是"",不是单个信号量——哪怕你只要一个,也是 nsems = 1 的集合。为什么设计成集,下一节讲。

5.2 控制:初始化、查状态、删除都靠它(semctl)

int semctl(int semid, int semnum, int cmd, ...);

第四个参数是可变参数,类型是 union semun。

常用 cmd:

cmd干什么第四参用哪个成员
SETVAL把第 semnum 个信号量设成指定值arg.val
GETVAL读第 semnum 个信号量的值不用
IPC_STAT / IPC_SET取/改整个集的属性arg.buf
IPC_RMID删掉整个信号量集不用
GETALL / SETALL一次读/写集合里全部值arg.array

SETVAL 是初始化的唯一手段——创建完,信号量值不会自动是你想要的数,必须手动设。忘了设初值就用,是新手第一大坑。

5.3 传参用的联合体:为什么必须自己定义(semun)

这是信号量接口最出名的坑。semctl 第四参的类型 union semun,系统头文件不提供定义,必须你自己写。man page 里写得很明确("The calling program must define this union as follows"):

union semun { int val; /* SETVAL 操作使用的信号量值 */ struct semid_ds *buf; /* IPC_STAT、IPC_SET 操作使用的缓冲区 */ unsigned short *array; /* GETALL、SETALL 操作使用的数组 */ struct seminfo *__buf; /* IPC_INFO 操作使用的缓冲区 (Linux 系统特有) */ };

四个成员一个不能少,第四个名字是__buf(双下划线开头)。为什么 POSIX 不给定义?历史遗留——各家 UNIX 实现对这个联合体的定义打架,标准干脆甩给应用程序自己声明。结果就是每一份 System V 信号量代码开头都贴着同一段。

5.4 真正的 P/V:一次能同时操作一批信号量(semop)

int semop(int semid, struct sembuf *sops, size_t nsops);
  • sops:struct sembuf数组
  • nsops:数组里有几个操作

传数组意味着一次系统调用可以对集合里多个信号量同时操作,而且整体原子(第四节 4.5 说过)。


六、一次为什么要管多个信号量

前面创建接口埋的问题在这:为什么 semget 创建的是"集"?

6.1 两个参数别搞混:建几个 vs 动第几个

  • nsems:创建时指定,这个集合里有几个信号量(内核里就是按数组来放的)
  • semnum:操作时指定,动的是集合里第几个(从 0 数)

一个是"盖楼时定几层",一个是"进楼时去几层",别混。

6.2 为什么要打包成"集":多个锁一次原子操作

一个像样的并发场景往往不止一把锁。经典的生产者-消费者就要三个信号量:

  • empty:空位还剩几个(生产者看它)
  • full:货物攒了几个(消费者看它)
  • mutex:缓冲区本身的互斥锁

如果只能一个一个创建,三个信号量三次创建、三次管理。设计成集:一次创建、一个 semid 全管,而且 semop 传数组可以一次原子地动它们仨——要么三个信号量的状态一起改成功,要么都不动。这在多锁场景是防死锁的关键:先拿全再动,不给人插队制造"拿了 A 等 B、拿了 B 等 A"的机会。

电影院版:多个影厅各一套票,可以一次买齐两场连映的票——要么两场都到手,要么一张都不买,不会出现"买了 1 号厅的票、2 号厅卖光了、票退不掉"的尴尬。

6.3 系统上限:一个集最多能放多少信号量

semget(2) man page 写得明白,几个关键限制:

限制含义老默认(<3.19)新默认(≥3.19)
SEMMSL单个集最多几个信号量25032000
SEMMNI系统最多几个集12832000
SEMMNS系统信号量总数= SEMMNI × SEMMSL同左
SEMOPM单次 semop 最多几个操作32500
SEMVMX信号量最大值32767(硬编码,改不了)同左

实测方法(可以直接跑):

$ cat /proc/sys/kernel/sem 250 32000 32 128

注意这四个数的顺序:SEMMSL SEMMNS SEMOPM SEMMNI——不是字母序,也不是"集在前"的直觉序。面试官就爱拿这个顺序钓人,答错顺序等于没查过。


七、进程退出它还在:生命周期与清理

7.1 怎么查看和删除:命令行 + 代码调用

$ ipcs -s # 查看系统里所有信号量集(-s = semaphore) $ ipcrm -s semid # 命令行删一个

代码里删:semctl(semid, 0, IPC_RMID)(对应第五节讲的删除用法)。

7.2 进程退出它还在:用完必须自己删

和共享内存、消息队列一个脾气:信号量的生命周期随内核,不随进程。进程退了,信号量还在;重启才彻底清。所以:

  • 用完必须显式删(IPC_RMID),忘了删就是内核资源泄漏

7.3 删除的副作用:阻塞中的进程会被一并唤醒

man page 里一条容易被忽略的细节:IPC_RMID 删集合时,所有阻塞在这个集上 semop 的进程会被一起唤醒,它们的 semop 返回失败,errno 置 EIDRM。不是让它们永远睡下去。

对应电影院:影厅直接拆了,排队的人当场散伙,各自收到通知。


八、内核怎么存放、又怎么统一管理三件套

问老问题:内核里这么多信号量,OS 怎么管?

老答案:先描述,再组织(篇25 讲共享内存时的 shmid_ds、篇27 的 msqid_ds,同一个套路)。信号量的"描述"就是 struct semid_ds。

8.1 内核怎么"描述"信号量:一张档案表(semid_ds)

篇27 只点了个名,这里给全(semctl(2) man page 原文):

struct semid_ds { struct ipc_perm sem_perm; /* 所有权与权限 */ time_t sem_otime; /* 最后一次 semop 时间 */ time_t sem_ctime; /* 创建 / 最后一次 semctl 改动时间 */ unsigned long sem_nsems; /* 集合里信号量个数 */ };

三个时间戳 + 一个计数,管的是"整个集"的档案。单个信号量的 semval / semncnt 那四个值(4.1 节)是集合内部每个成员各自挂的,不在这个结构里。

8.2 档案开头的权限头:谁建的、权限多大(ipc_perm)

sem_perm 的类型,用户态完整定义(man page 原文):

struct ipc_perm { key_t __key; /* 你传给 semget 的那个 key */ uid_t uid; /* 所有者 */ gid_t gid; uid_t cuid; /* 创建者 */ gid_t cgid; unsigned short mode; /* 权限位,低 9 位有效 */ unsigned short __seq; /* 序列号(内核用) */ };

对信号量来说,权限里的"写"实际含义是alter(改动)——能 P/V 就算"写",读 semval 算"读"。

8.3 容易踩坑:同名结构,用户态快照 ≠ 内核实体

注意区分两个名字很像的东西:

  • 用户态 struct ipc_perm:IPC_STAT 拷出来给你看的"快照",字段是 uid_t、unsigned short mode
  • 内核 struct kern_ipc_perm:内核里真正挂在资源头上的那个,字段是 kuid_t、umode_t,还带锁和引用计数

前者是给你看的报表,后者是仓库里真实的货架标签。长得很像,不是一个东西。8.4 的主角就是后者。

规律回指(篇27 3.2 讲过):shmid_ds / msqid_ds / semid_ds 三兄弟,开头都是 ipc_perm——三件套接口长得像,根子在这。

8.4 三类资源共用的"桥接头"(kern_ipc_perm)

说完了单个信号量的"描述",再看三类资源怎么"组织"起来:内核里躺着大量信号量集合、消息队列、共享内存段,它靠什么统一管这些?还是先描述,再组织

  • 描述:共享内存用 struct shmid_kernel,消息队列用 struct msg_queue,信号量用 struct sem_array
  • 组织:把所有描述结构挂进同一张表统一管理

这三类描述结构体,第一个成员都是 struct kern_ipc_perm

这就是"桥梁头":内核拿到一个 kern_ipc_perm 指针,不用管它背后是信号量、消息队列还是共享内存,就能统一挂表、统一按 key/id 查找、统一做权限检查。学过 C++ 的多态再看这个会非常眼熟——这就是拿"基类指针"管所有派生类的内核版。

8.5 组织方式:把描述结构统一挂进一张表

内核早期的组织方式(2.6 时代的代码):

struct ipc_id_ary { int size; /* 数组容量 */ struct kern_ipc_perm *p[0]; /* 柔性数组:每个元素指向一个资源 */ }; struct ipc_ids { int in_use; /* 当前在用几个 */ int max_id; unsigned short seq; /* 序列号,防 ID 重用 */ unsigned short seq_max; struct mutex mutex; struct ipc_id_ary *entries; /* ← 指向那张表 */ };

p[0] 是 C 里的柔性数组技巧:结构体末尾留一个零长度数组,实际分配时多给一截内存,p 就成了"长度可变的指针数组"。全局一张这样的表,ID 就是数组下标,查资源 = 取下标,O(1)。

8.6 更正:现代内核已不用数组,换成 IDR 基数树

写这篇时查了内核源码,必须说清楚:ipc_id_ary 是 2007 年之前的模型,Linux 2.6.23 起被删除

证据是 2007 年 LKML 上 Nadia Derbey 的补丁系列("Storing ipcs into IDRs"):struct ipc_id_ary 整个删掉,连配套的扩容函数 grow_ary() 一并移除,换成 IDR 基数树。现代内核(5.x / 6.x)的 struct ipc_ids 长这样:

struct ipc_ids { int in_use; unsigned short seq; unsigned short seq_max; struct rw_semaphore rw_mutex; struct idr ipcs_idr; /* ← IDR 基数树取代了柔性数组 */ };

IDR 是内核里专门干"拿一个整数 ID 快速找对象"的树形结构(基数树的封装)。换它的原因很实际:IPC 对象数量动态增减,静态数组要么开大了浪费、要么不够了频繁扩容搬内存;基数树按需生长,查改删都高效。

但注意——思想一个字没变:换成 IDR 之后,树里存的仍然是指向 struct kern_ipc_perm 的指针。容器从数组换成了树,"统一桥接头 + 一张表管三种资源"的设计原封不动。

所以常见的讲法是一个简化模型:思想是真的(先描述再组织、kern_ipc_perm 统一挂表),容器是演进的(柔性数组 → IDR 基数树)。面试里能主动讲出"简化模型和真实内核差在这一步",是实打实的加分项。

8.7 三个容易忽略的细节

ID 不是下标。用户拿到的 semid = seq × SEQ_MULTIPLIER + 内核内部 id,其中 SEQ_MULTIPLIER = IPCMNI = 32768。seq 每创建一个对象加 1,内部 id 可以被复用——但 seq 不重用,所以同一个内部 id 在不同"轮次"拼出的用户 ID 不同。这样进程手里就算攥着过期的旧 ID,也几乎不可能错拿到复用后的新资源。

三类各一张表。struct ipc_namespace 里有 ids[3],下标顺序是:0 信号量、1 消息队列、2 共享内存。semid、msqid、shmid 各走各的表,互不干扰。

kern_ipc_perm 里都有什么(4.16 源码,字段挑重点):

struct kern_ipc_perm { spinlock_t lock; /* 自旋锁保护自己 */ bool deleted; int id; key_t key; /* 你给的那个 key */ kuid_t uid; kgid_t gid; kuid_t cuid; kgid_t cgid; umode_t mode; /* 权限 */ unsigned long seq; void *security; /* 4.15 起还加了 RCU、引用计数等并发相关成员,现阶段不用深究 */ };

对照 8.2 的用户态 ipc_perm:字段名几乎一一对应,类型全换了内核私有的(kuid_t / umode_t),还多了锁。快照 vs 实体,区别就在这。

8.8 一张图看懂全局:从内核命名空间到实际资源

一张表(现在是树)、一个共同的头、三种资源。这就是"先描述,再组织"在 System V IPC 上的完整形态。


九、信号 / 信号量 / 互斥锁:易混概念一次分清

这一节全是高频混淆点,每个都给答案,不只摆问题。

9.1 信号 vs 信号量:一个"叫你一下",一个"数你一下"

信号信号量
是什么异步通知机制同步互斥的计数器
干什么告诉进程"某事件发生了"(如 SIGKILL、SIGSEGV)管"资源还剩几份、谁能进临界区"
用法kill 发送 / signal 注册处理函数semget / semop P、V

名字只差一个字,机制完全无关。面试口头回答就一句:信号是"叫你一下",信号量是"数你一下"。

9.2 信号量 vs 互斥锁:谁都能解 vs 只有主人能解

二元信号量互斥锁
取值0 / 1锁上 / 没锁
所有权:谁都能 V:谁加的锁谁解
典型误用A 拿了信号量,B 手滑 V 了一下——合法,但逻辑已乱B 解 A 的锁——直接报错拦住

二元信号量初值设 1,行为上很像锁,但没有所有权概念。互斥锁锁定了 owner,别的线程解不了;信号量谁都能 V。这就是"二元信号量 ≈ 互斥锁但不等价"的标准答案——大厂高频追问。

9.3 二元 vs 多元:一次放一个,还是放 N 个

  • 二元(初值 1):互斥,一次放一个
  • 多元(初值 N):资源池 / 限流,一次放 N 个(比如最多 5 个并发连接)

判断依据就一条:你的资源到底有几份。

9.4 三种档案结构对比:分别管什么、差在哪

shmid_dsmsqid_dssemid_ds
管什么共享内存段消息队列信号量集
核心差异字段分离/挂接时间、大小队列字节数/条数、收发进程sem_nsems(集里几个)
开头ipc_permipc_permipc_perm
角色管数据管数据管同步

一句话收束:共享内存和消息队列管的是"数据怎么过去",信号量管的是"谁有资格动"。


十、面试官高频追问与参考答案

Q1:信号量的本质是什么?

计数器,表明临界资源中剩余资源的数量。P 减 V 加,操作原子。

Q2:信号量本身是共享资源吗?

是,但要分两层答(只答一个"不是"会露怯)。

  • 它是共享对象:semval 在内核里,多进程通过同一个 semid 操作同一个值。不共享就没法同步——A 进程 P 完,B 必须看到减过的值。反证它确实是共享的:内核给它加自旋锁、把 semop 做成原子,正是因为它要被并发访问。
  • 但它不是"被保护的那个共享资源":在这套方案里它是管理员,被保护的临界资源是共享内存那块数据。

"信号量本身不是共享资源"这句话,讲的是第二层的角色分工,不能拿来否认第一层的物理事实。补充一点:它自己不需要再套一层信号量来保护,因为内核的原子 semop 已经兜底了。

Q3:P/V 为什么必须原子?内核怎么保证?

P/V 是"读值-判断-改值"三步,如果不原子,两个进程同时读到同一个旧值、都判断通过、各自减 1——一个名额放进俩人,互斥直接失效。保证手段:semop 是系统调用,进入内核后由内核加锁(自旋锁/信号量)串行执行,用户态插不了队。

Q4:sem_op 为正 / 为负 / 为 0 分别是什么语义?哪种永远不阻塞?

负 = P(申请),值不够就阻塞;正 = V(释放),永不阻塞;0 = 等计数变 0(wait-for-zero),不是 0 就阻塞。

Q5:semop 传数组(nsops > 1)有什么用?

一次系统调用原子地操作集合里多个信号量,要么全做要么全不做。多锁场景防死锁的关键:避免"拿了一半等另一半"给别的进程插队制造环形等待的机会(生产者-消费者的 empty/full/mutex 三件套就是典型应用)。

Q6:IPC_NOWAIT 失败时,数组里其他操作执行了吗?

一个都没执行,整体作废,errno = EAGAIN。原子性保证的"全或无"。

Q7:进程持有信号量时崩了怎么办?

两个层面答。用户态:sem_flg 设 SEM_UNDO,进程退出时内核自动回滚它的 P/V。系统态:就算没设 UNDO,其他进程也能对信号量做 V(无所有权),不至于永久死锁——但语义上已经靠人肉补偿了,工程上必须靠 SEM_UNDO。

Q8:SEM_UNDO 内核里到底怎么实现的?

机制(已对 v5.6 内核源码核实):

  • 每个进程的 task_struct 里有 sysvsem.undo_list,挂一条"我做过的可撤销操作"链表
  • 进程动过的每个信号量集对应一个 struct sem_undo:记着 semid 和一个 semadj 数组(集合里每个信号量一个调整值)
  • 做 P(sem_op < 0)时,|sem_op| 累加进 semadj;做 V 时从 semadj 里减掉——semadj 记的是"我净欠了多少份资源"
  • 进程退出时 do_exit() 调 exit_sem():遍历 undo_list,把每个 semadj[i] 加回对应信号量的 semval,结果夹在 [0, 32767] 区间,然后唤醒等资源的进程

电影院版一句话:影院给每个观众记一笔账(买了没退的票数),人不管怎么走的,散场时按账本把票全部回收进票池。

Q9:union semun 为什么要自己定义?

POSIX 标准把定义甩给了应用程序(各家 UNIX 实现历史分歧,标准索性不统一),Linux 头文件也就不给。所以每个程序要自己声明这四成员联合体:val / buf / array / __buf。手写不出来说明没真写过 System V 信号量代码。

Q10:/proc/sys/kernel/sem 里四个数是什么?什么顺序?

SEMMSL(每集最多信号量数)、SEMMNS(系统总信号量数)、SEMOPM(单次 semop 最多操作数)、SEMMNI(系统最多集数)。顺序就是 SEMMSL SEMMNS SEMOPM SEMMNI,反直觉,别按字母序背。

Q11:用户看到的 semid 是数组下标吗?

不是。semid = seq × 32768 + 内核内部 id。seq 防 ID 复用撞车。

Q12:ipc_perm 和 kern_ipc_perm 什么区别?

用户态的 ipc_perm 是 IPC_STAT 拷给应用程序看的快照(uid_t、unsigned short mode);kern_ipc_perm 是内核里真正挂在资源开头的实体(kuid_t、umode_t,带自旋锁、RCU、引用计数)。

Q13:三类 IPC 在内核里怎么统一管理?现代内核还是 ipc_id_ary 吗?

都是"描述结构第一个成员放 kern_ipc_perm"——内核拿这个桥梁头统一挂表、查找、做权限检查。但 ipc_id_ary 是老模型,2007 年 2.6.23 起换成 struct ipc_ids 里的 IDR 基数树(ipcs_idr),存的还是 kern_ipc_perm 指针。能答出"柔性数组模型 vs 现代内核的 IDR"这一层差异,就是加分项。

Q14:信号和信号量什么关系?

没关系。信号是异步事件通知,信号量是同步计数器。一个字之差,两个世界。


十一、小结

  • 本质:计数器,数的是"资源还剩几份";它自己也是内核里的共享对象(由内核锁保证安全),但不是被它保护的那份共享资源
  • 核心类比:电影院订票——买票是 P、退票是 V、票池初值是座位数、SEM_UNDO 是"人跑了票自动退"
  • 保护思路:共享内存划分固定区域,所有进程访问一小块前必须先申请信号量
  • 操作:sembuf 三字段,sem_op 正负零三种语义,原子执行
  • 接口:semget(创建集)→ semctl SETVAL(初始化,union semun 自己定义)→ semop(P/V,可传数组)→ semctl IPC_RMID(删除);ipcs -s 查看、ipcrm -s 清场
  • 内核组织:semid_ds 描述 → kern_ipc_perm 当桥梁头 → ipc_ids 统一管三件套;ipc_id_ary 是 2.6 早期模型,现代内核用 IDR 基数树,思想不变

信号量不难,难的是把"计数器"和"预订"这两层意思焊死在脑子里——想不通的时候回电影院看一眼,买票退票,就那么点事。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/7 21:54:33

从Openclaw迁移到影刀RPA:自动化效率提升实践

1. 从Openclaw到新一代RPA工具的迁移之旅作为一名长期使用Openclaw的自动化工程师&#xff0c;最近半年我逐渐将工作重心转向了更现代的RPA解决方案。Openclaw确实是一款优秀的开源自动化工具&#xff0c;特别是在网页自动化方面表现出色。但随着项目复杂度提升&#xff0c;我遇…

作者头像 李华
网站建设 2026/9/7 21:53:43

GJB9001C-2017内审员考试备考:高频考点与答题逻辑详解

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/7 21:52:08

【LitCTF2026】lit_ezsql

【LitCTF2026】lit_ezsql【LitCTF2026】lit_ezsql——GBK 宽字节 SQL 注入利用一、前置基础&#xff1a;字符型 SQL 注入二、黑盒发现查询参数三、黑盒测试普通 SQL 注入1. 测试数字型表达式2. 测试单引号闭合四、黑盒定位宽字节注入1. 为什么会想到尝试宽字节2. 测试宽字节单引…

作者头像 李华
网站建设 2026/9/7 21:50:59

软件开发中的初始化问题解析与最佳实践

1. 初始化问题解析与解决方案 在软件开发过程中&#xff0c;初始化问题是一个看似简单却经常引发各种bug的常见问题。作为从业十多年的工程师&#xff0c;我见过太多因为初始化不当导致的线上事故。今天就来深入剖析这个基础但重要的话题。 初始化问题通常表现为变量未初始化、…

作者头像 李华
网站建设 2026/9/7 21:48:10

看完就会:盘点2026年好评如潮的AI论文写作软件

一天写完毕业论文在2026年已不再是天方夜谭。以下是2026年最炸裂、实测能大幅提速的AI论文写作软件&#xff0c;覆盖选题构思、文献综述、数据整理、格式排版等核心场景&#xff0c;帮你高效搞定论文写作难题。 一、全流程王者&#xff1a;一站式搞定论文全链路&#xff08;一天…

作者头像 李华