2. Linux内核同步机制概览:自旋锁、互斥锁、信号量、读写锁、RCU的定位与对比
在嵌入式Linux驱动开发中,并发访问共享资源是导致竞态(Race Condition)的根本原因。内核提供了多种同步原语,每种原语在性能、适用场景、休眠行为与复杂度上各有取舍。理解它们的定位差异,是正确选择RCU的前提。
2.1 同步原语核心维度对比
下表从等待行为、上下文限制、读者-写者公平性、临界区耗时四个维度,对五种主要同步机制进行横向对比:
| 同步机制 | 等待行为 | 允许上下文 | 读者/写者模型 | 临界区耗时建议 | 典型开销(相对) |
|---|---|---|---|---|---|
| 自旋锁 (spinlock) | 忙等(原地旋转) | 进程上下文、中断上下文 | 互斥(无区分) | 极短(微秒级) | 低 |
| 互斥锁 (mutex) | 休眠等待 | 仅进程上下文(可睡眠) | 互斥(无区分) | 较长(毫秒级或更长) | 中 |
| 信号量 (semaphore) | 休眠等待 | 仅进程上下文(可睡眠) | 计数互斥(允许多个持有者) | 较长 | 中 |
| 读写锁 (rwlock/rwsem) | 读者共享,写者独占 | rwlock:中断上下文;rwsem:仅进程 | 读者-写者 | 读者临界区短,写者临界区短 | 中 |
| RCU (Read-Copy-Update) | 读者无锁,写者延迟回收 | 读者:任意上下文;写者:进程上下文 | 读者-写者(读者零开销) | 读者无限制,写者需宽限期 | 读者极低,写者较高 |
关键结论: RCU 是唯一一种让读者完全不使用锁的同步机制。读者路径上既无原子操作,也无内存屏障(在大多数架构上),这使得 RCU 在读多写少的场景下拥有无可比拟的性能优势。
2.2 同步机制选择决策树
在实际驱动开发中,可以按照以下逻辑快速选定同步原语:
- 临界区是否可能在中断上下文执行?
- 是 → 只能使用 自旋锁 或 RCU(读者侧)。
- 否 → 进入下一步。
- 临界区耗时是否超过几十微秒?
- 是 → 必须使用可休眠机制:互斥锁、信号量 或 读写信号量。
- 否 → 进入下一步。
- 读操作频率是否远高于写操作?
- 是 → 考虑 RCU(读者无锁)或 读写锁(读者共享)。
- 否 → 使用 自旋锁 或 互斥锁。
- 是否需要保护链表/树等动态数据结构,且写操作允许延迟回收?
- 是 → RCU 是最佳选择。
- 否 → 回到常规锁机制。
经验法则: 在嵌入式驱动中,如果共享数据是一个被频繁遍历的链表,且更新操作不频繁(如路由表、设备列表、状态数组),优先考虑 RCU。如果临界区只有一条赋值语句且必须在中断中执行,自旋锁是唯一选择。
2.3 五种同步机制的内核API速览
以下代码片段展示了每种机制最核心的API使用模式,便于直观对比:
自旋锁
spinlock_t my_lock;
spin_lock_init(&my_lock);
spin_lock(&my_lock);
/* 临界区:极短,不可睡眠 */
spin_unlock(&my_lock);
/* 中断上下文版本 */
unsigned long flags;
spin_lock_irqsave(&my_lock, flags);
/* 临界区 */
spin_unlock_irqrestore(&my_lock, flags);
互斥锁
struct mutex my_mutex;
mutex_init(&my_mutex);
mutex_lock(&my_mutex);
/* 临界区:可睡眠,允许较长时间 */
mutex_unlock(&my_mutex);
信号量
struct semaphore my_sem;
sema_init(&my_sem, 1); /* 初始值为1,当作互斥锁使用 */
down(&my_sem);
/* 临界区 */
up(&my_sem);
读写锁(rwlock_t)
rwlock_t my_rwlock;
rwlock_init(&my_rwlock);
/* 读者 */
read_lock(&my_rwlock);
/* 只读访问 */
read_unlock(&my_rwlock);
/* 写者 */
write_lock(&my_rwlock);
/* 修改数据 */
write_unlock(&my_rwlock);
RCU(读者侧)
rcu_read_lock();
/* 通过 rcu_dereference() 访问受保护指针 */
struct my_data *p = rcu_dereference(g_ptr);
if (p)
do_something_with(p);
rcu_read_unlock();
注意: RCU 的读者临界区(
rcu_read_lock() ~ rcu_read_unlock())内不允许睡眠、阻塞或调用可能引起调度的函数。这是 RCU 读者侧的核心约束,与自旋锁类似,但 RCU 读者不会忙等。
2.4 同步机制性能与适用场景定位图
下图从读者并发度和写者开销两个维度,直观展示五种同步机制的定位差异:
2.5 RCU的独特定位:读者零锁与写者延迟回收
从上图可以清晰看到,RCU 处于读者并发度最高且写者开销最高的极端位置。这种特性来源于其核心设计哲学:
- 读者路径: 不使用任何原子操作、内存屏障或锁指令。读者仅通过
rcu_read_lock()标记临界区开始,通过rcu_dereference()读取指针。在大多数CPU架构上,这两个操作编译后仅为普通内存访问指令。 - 写者路径: 写者需要执行“复制-更新-发布”三步操作,然后等待所有在读读者退出临界区(即宽限期),最后才能安全回收旧数据。这个等待过程可能涉及
synchronize_rcu()或call_rcu()回调,开销远高于普通锁的写操作。
RCU 的黄金法则: 当读操作比写操作多出 100 倍以上,且写操作可以容忍毫秒级延迟时,RCU 的性能优势会完全碾压其他所有同步机制。在嵌入式网络驱动、文件系统缓存、设备状态监控等场景中,RCU 是事实上的标准选择。
2.6 何时不应使用RCU
RCU 并非万能药,以下情况应避免使用:
- 写操作频繁且要求低延迟: 如果每秒写操作超过数千次,RCU 的宽限期等待和内存回收开销会显著增加,此时读写锁或自旋锁可能更合适。
- 临界区需要睡眠: RCU 读者侧禁止睡眠,如果读者需要执行可能阻塞的操作(如内存分配、文件I/O),必须使用互斥锁或读写信号量。
- 保护简单共享变量: 对于单个整型或布尔值的读写,原子操作(
atomic_t)或自旋锁的开销远低于 RCU 的写者延迟回收机制。 - 实时性要求极高的系统: RCU 的宽限期长度不可预测(取决于所有CPU上读者临界区的最大持续时间),在硬实时系统中可能引入不可接受的延迟抖动。
总结: 选择同步机制时,先判断上下文限制(能否睡眠),再评估读写比例,最后考虑临界区粒度。RCU 在“读多写少、读者路径极简、数据结构动态”的三位一体场景中,是无可替代的最优解。