一、从一次诡异的传感器数据读取说起
上周调试一个工业温控模块,遇到了奇怪的现象:温度采集线程偶尔会读到“跳变”的异常值,比如从25.3℃突然变成-12.7℃。逻辑上看,数据写入只在中断服务函数里进行,读取则在用户线程,中间加了读写锁保护,按理说不该出问题。
用ftrace抓了调度情况才发现症结所在:高温时中断频率飙升,读线程频繁被写者阻塞,实时性受影响。更麻烦的是,某些架构上读写锁的开销比想象中大——特别是那些读多写少的场景,锁竞争成了瓶颈。
这时候就该请出今天要聊的两位:顺序锁(Seqlock)和RCU(Read-Copy-Update)。它们解决的都是同一个核心问题:如何让读操作几乎不受写操作的影响。
二、顺序锁:为读多写少而生的乐观锁
先看顺序锁,它的设计思想很巧妙:读操作不加锁,只检查序列号;写操作加锁并更新序列号。读之前和读之后各读一次序列号,如果两次值相同且为偶数,说明数据一致。
// 典型使用模式(伪代码示意)seqlock_ttemp_lock;inttemperature=25;// 读者侧do{seq=read_seqbegin(&temp_lock);// 记住序列号temp=temperature;// 读数据}while(read_seqretry(&temp_lock,seq));// 检查序列号是否变化// 写着侧write_seqlock(&temp_lock);// 获取写锁temperature=read_sensor();// 更新数据write_sequnlock(&temp_lock);// 释放并递增序列号关键点在这里:顺序锁允许读操作与写操作并发执行,但如果检测到写操作正在进行(通过序列号变化),读者就重试。这属于“乐观锁”思想——假设冲突很少发生,发生时再重试。
但有几个坑得注意:
- 写者会饿死读者:如果写操作非常频繁,读者可能反复重试。所以顺序锁只适用于写很少的场景(比如配置更新、传感器低频采样)。
- 数据不能有指针依赖:因为读到的可能是中间状态,如果数据包含多个关联字段(比如链表),可能读到不一致的组合。所以顺序锁保护的数据最好是单一标量或结构体。
- 中断上下文要注意:Linux内核里写操作会禁用抢占,中断里用要小心。
我在那个温控项目里试过顺序锁,中断频率低时效果很好,但后来采样率提高后,重试次数明显增多,这时候就得考虑更高级的方案了。
三、RCU:读操作完全零开销的魔法
RCU就更神奇了——读操作完全不需要任何原子操作、内存屏障或锁。它的核心思想是:写者先创建新副本,修改副本,然后原子替换指针,最后等待所有老读者退出后回收旧数据。
// 经典RCU更新流程(以链表删除为例)// 1. 读者侧:完全无锁!rcu_read_lock();// 只是标记进入读侧临界区node=rcu_dereference(head->next);// 受保护的指针访问// ... 使用node数据rcu_read_unlock();// 标记退出// 2. 写着侧删除节点old=head->next;new=old->next;rcu_assign_pointer(head->next,new);// 原子替换指针synchronize_rcu();// 等待所有读者退出kfree(old);// 安全释放旧数据RCU的精髓在于“等待”:synchronize_rcu()会阻塞直到所有在替换前开始的读操作都完成。这个等待是通过“宽限期”(Grace Period)实现的,内核会跟踪所有CPU上的读侧临界区。
几个实战经验:
- 别在RCU保护的链表里嵌套另一个RCU链表,回收顺序会出问题,我在这栽过跟头。
rcu_dereference()和rcu_assign_pointer()不是可选的,它们包含了必要的内存屏障。- 用户态也有RCU实现(liburcu),做高性能服务器时很有用。
四、选择困难症:什么时候用哪个?
| 场景 | 推荐机制 | 理由 |
|---|---|---|
| 配置参数更新(几秒一次) | 顺序锁 | 实现简单,读者几乎无开销 |
| 路由表、进程列表查询 | RCU | 读极频繁,写较少,需要零开销读取 |
| 传感器数据(高频写入) | 读写锁或原子变量 | 顺序锁会导致读者重试过多 |
| 小结构体(如统计计数) | 顺序锁 | 单一变量,无指针依赖 |
个人踩坑建议:
- 先明确读写比例:写频率超过每秒几十次就别用顺序锁了。
- RCU的学习曲线较陡,先从链表操作开始练手,理解宽限期机制。
- 调试RCU问题可以用
rcu_read_lock_held()做断言,能早点发现锁使用错误。 - 在实时性要求高的读侧,RCU是利器——它连内存屏障都不需要,确定性更好。
五、最后聊点实在的
驱动开发里,同步机制选型往往比算法优化更影响性能。早年我也喜欢无脑用自旋锁,后来在千兆网卡驱动里吃到苦头——锁竞争导致吞吐量卡在600Mbps上不去。换成RCU后,读路径彻底无锁,性能直接跑满线速。
现在我的习惯是:新驱动先用读写锁,性能测试时看锁竞争情况。如果读侧热点明显,就考虑RCU;如果是配置类数据,改用顺序锁。这种渐进式优化,比一开始就追求复杂方案更稳妥。
记住,同步机制是手段不是目的。最终目标是让数据流动得更顺畅,而不是展示锁技巧。有时候,重新设计数据流,减少共享,比换任何锁都有效。