2. Linux内核同步机制概览:自旋锁、互斥锁、信号量、读写锁、RCU的定位与对比

在嵌入式Linux驱动开发中,并发访问共享资源是导致竞态(Race Condition)的根本原因。内核提供了多种同步原语,每种原语在性能、适用场景、休眠行为与复杂度上各有取舍。理解它们的定位差异,是正确选择RCU的前提。

2.1 同步原语核心维度对比

下表从等待行为、上下文限制、读者-写者公平性、临界区耗时四个维度,对五种主要同步机制进行横向对比:

同步机制 等待行为 允许上下文 读者/写者模型 临界区耗时建议 典型开销(相对)
自旋锁 (spinlock) 忙等(原地旋转) 进程上下文、中断上下文 互斥(无区分) 极短(微秒级) 低
互斥锁 (mutex) 休眠等待 仅进程上下文(可睡眠) 互斥(无区分) 较长(毫秒级或更长) 中
信号量 (semaphore) 休眠等待 仅进程上下文(可睡眠) 计数互斥(允许多个持有者) 较长 中
读写锁 (rwlock/rwsem) 读者共享,写者独占 rwlock:中断上下文;rwsem:仅进程 读者-写者 读者临界区短,写者临界区短 中
RCU (Read-Copy-Update) 读者无锁,写者延迟回收 读者:任意上下文;写者:进程上下文 读者-写者(读者零开销) 读者无限制,写者需宽限期 读者极低,写者较高
关键结论: RCU 是唯一一种让读者完全不使用锁的同步机制。读者路径上既无原子操作,也无内存屏障(在大多数架构上),这使得 RCU 在读多写少的场景下拥有无可比拟的性能优势。

2.2 同步机制选择决策树

在实际驱动开发中,可以按照以下逻辑快速选定同步原语:

  1. 临界区是否可能在中断上下文执行?
    • 是 → 只能使用 自旋锁 或 RCU(读者侧)。
    • 否 → 进入下一步。
  2. 临界区耗时是否超过几十微秒?
    • 是 → 必须使用可休眠机制:互斥锁、信号量 或 读写信号量。
    • 否 → 进入下一步。
  3. 读操作频率是否远高于写操作?
    • 是 → 考虑 RCU(读者无锁)或 读写锁(读者共享)。
    • 否 → 使用 自旋锁 或 互斥锁。
  4. 是否需要保护链表/树等动态数据结构,且写操作允许延迟回收?
    • 是 → 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 同步机制性能与适用场景定位图

下图从读者并发度和写者开销两个维度,直观展示五种同步机制的定位差异:

同步机制定位图:读者并发度 vs 写者开销 读者并发度 → 写者开销 → 低 中 高 低 中 高 自旋锁 互斥锁 信号量 读写锁 RCU RCU 位于右上角:读者并发度最高,写者开销也最高(需宽限期和延迟回收)

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 在“读多写少、读者路径极简、数据结构动态”的三位一体场景中,是无可替代的最优解。