并发与同步机制面试题:自旋锁、互斥锁、信号量、RCU 使用场景与区别

本专题考察你对 Linux 内核同步原语的理解深度。面试中常从适用场景、睡眠/忙等、上下文限制、性能代价四个维度展开追问,需要你不仅知道区别,更能结合项目经验说明选型理由。

核心加分点:能说出每种锁在 SMP/UP 下的行为差异、中断上下文特殊要求,以及 RCU 的 grace period 机制。
临界区是否可睡眠? 不可睡眠(中断上下文) 自旋锁(关中断) 原子操作 / raw_spinlock RCU(读多写少) 可睡眠(进程上下文) 互斥锁(mutex) 信号量(sem) 完成量 (completion) 关键选择维度: • 临界区长度 → 短用自旋锁,长用互斥锁 • 读/写负载 → 读多写少用 RCU,写多读写用读写锁 • CPU 数量 → UP 环境自旋锁退化为抢占禁用(注意死锁风险)

面试问答卡

面试问题:自旋锁与互斥锁在使用场景上有何区别?

考察点:是否理解忙等待 vs 睡眠等待的适用场景;能否区分中断上下文与进程上下文;是否清楚 SMP 与 UP 下的行为差异。

回答框架:先给出核心区别(是否导致睡眠);再按临界区长度 / 上下文 / CPU 数量三个维度对比;最后用一个代码/项目例子说明选型依据。

示例回答:自旋锁忙等待,适合短临界区(<10us)且不可睡眠的中断上下文。互斥锁睡眠等待,适合长临界区、进程上下文。例如在网卡驱动 NAPI 轮询中保护描述符环用自旋锁,而块设备请求队列用互斥锁。

可能追问:① 自旋锁在单核 CPU 上会怎样?② 中断上下文使用自旋锁后如何避免死锁?③ spin_lock_irqsave 与 spin_lock 的差异。

避坑提醒:不要回答“自旋锁只能用于 SMP”。单核 Uniprocessor 上 spin_lock 仅关抢占,不会无限循环,但若中断上下文与进程上下文共享锁则需关中断版本。

面试问题:信号量与互斥锁能否互相替代?使用场景有何本质区别?

考察点:是否理解信号量的计数量与互斥锁的排他性;能否区分同步 vs 互斥;是否知道信号量在 Linux 内核中的演进(被 mutex 部分替代)。

回答框架:先说不可以替代;原因:互斥锁有所有者概念、支持递归/优先级继承等;信号量更轻量且可用于计数同步;最后问场景举例。

示例回答:互斥锁是排他锁,有 owner、支持 PI(优先级继承),适合保护临界区;信号量可初始化为任意计数,常用于生产者-消费者模式。例如 USB 驱动中 urb 的释放同步使用 completion 类似信号量,而块设备请求队列保护用互斥锁。

可能追问:① 为什么内核后来引入 mutex 而减少 semaphore 的使用?② 说说 down_interruptible 与 down 的区别。③ 信号量能否在中断上下文使用?

避坑提醒:别混淆“信号量”与“互斥锁”的经典定义。内核中 mutex 是一种更安全的特殊信号量,但不能说“mutex 就是 count=1 的信号量”,因为实现机制完全不同(如 fast path 优化、owner 追踪)。

面试问题:RCU(Read-Copy-Update)适用于什么场景?与普通读写锁相比有什么优势?

考察点:是否理解 RCU 的无锁读端原理;能否说出 grace period 和宽限期;是否了解 RCU 对数据结构(如链表)的约束;能否讲出性能测试对比。

回答框架:先定义 RCU 核心:读者无需锁、写者通过延迟回收实现更新;再用读多写少场景举例;最后对比读端延迟与写端内存开销。

示例回答:RCU 适合读负载远大于写负载的场景,如路由表、文件系统 dentry 缓存。读端仅需要 rcu_read_lock,几乎无开销;写端通过 rcu_assign_pointer 和 synchronize_rcu 等待所有读者离开后回收旧版本。相比读写锁,读者性能提升 5~10 倍。

可能追问:① RCU 的 grace period 如何实现?② 为什么 RCU 不能保护通用数据结构(如链表插入中间)?③ 如何调试 RCU 死锁或宽限期堆积?

避坑提醒:不要泛泛说“RCU 无锁”,写端需要加锁保护更新操作(通常再加 spinlock)。另外,RCU 禁止在原子上下文 sleep,新手常犯错误是在 rcu_read_lock 内调用可能睡眠的函数。

项目案例:多核嵌入式网络驱动并发控制

项目名称双千兆以太网驱动并发优化(ARM 双核 Cortex-A72)
业务背景设备作为边缘网关,需要同时处理 2 个 1Gbps 端口的数据转发。多个 CPU 核心共享描述符环(TX/RX ring)、统计计数器、MAC 寄存器等资源,原始驱动使用大粒度自旋锁,导致 CPU 0 软中断时其他核心等待,吞吐量只有 1.2Gbps。
规模指标4 个 DMA 通道,256 个描述符/通道;吞吐量目标 1.8Gbps(满负载);CPU 主频 1.8GHz;中断合并延迟 < 200us。
技术栈Linux 5.10 LTS;自旋锁 + 关中断版本;RCU 保护路由表;per-CPU 变量 + 原子操作(统计计数器);kmem_cache 分配描述符。
核心难点① 多个 CPU 同时访问 TX 描述符环导致锁竞争严重;② 统计计数器频繁更新,普通 spinlock 成为瓶颈;③ 路由表需实时更新但读操作占 95% 以上,如何无锁读;④ 中断上下文与进程上下文共享锁时的死锁预防。
面试高频追问① 为什么选择自旋锁而不是互斥锁?② 如何设计 per-CPU 变量解决统计计数器冲突?③ RCU 与 rwlock 在该项目中的实测性能差异?④ 描述你为每个核心分配专用描述符环的机制。
可迁移回答话术“在项目中,我首先通过 perf top 和 ftrace 定位到锁竞争热点,发现自旋锁在 4 核下占用 30% CPU 空转。于是对读多写少的路由表采用 RCU,读端仅需 rcu_read_lock,写端用 seqlock 串行化更新,吞吐量提升 40%。对计数器使用 per-CPU 变量 + 原子累加,避免写者间的竞争。核心思路是‘区分读写场景,让锁尽可能局部化’。”
面试加分建议:准备一个你实际用 RCU 或 per-CPU 变量解决性能问题的微项目,配合数字(如提升百分比)讲述,比单纯背概念更有说服力。