2. 临界区与保护机制:临界区概念、保护原则、原子操作与锁的引入

2.1 临界区概念

在并发编程中,临界区(Critical Section)是指访问共享资源的代码片段,该片段在同一时刻只能被一个执行单元(进程、线程或CPU核心)执行,否则会导致数据不一致或系统崩溃。

临界区的本质: 一段代码,而非一段数据。保护的是对共享资源的访问路径,而非资源本身。

考虑一个简单的驱动场景:两个CPU核心同时执行驱动中的 write() 回调,更新同一个设备寄存器映射的内存变量 dev->count。如果没有保护,最终结果取决于两个核心的指令交错顺序,这就是典型的竞态条件(Race Condition)。

临界区竞态条件示意图 CPU 0 CPU 1 共享内存 dev->count 读-改-写 读-改-写 ⚠ 交错执行导致数据丢失或状态不一致 两个CPU同时进入临界区,最终结果不可预测

2.2 保护原则

临界区保护遵循以下核心原则,这些原则是设计任何锁机制的基础:

原则 描述 违反后果
互斥 同一时刻最多只有一个执行单元进入临界区 数据竞争、内存损坏
前进 没有执行单元在临界区时,请求进入的单元必须能立即进入 活锁、饥饿
有限等待 等待进入临界区的单元不会无限期阻塞 死锁、优先级反转
可重入性 同一执行单元可安全地多次进入同一临界区(取决于锁类型) 自旋锁递归死锁
驱动开发黄金法则: 临界区越小越好。只在真正访问共享资源的那几行代码周围加锁,不要在锁内执行耗时的I/O操作或内存分配。

2.3 原子操作:最轻量的保护

原子操作(Atomic Operation)是不可分割的操作,要么全部执行完成,要么完全不执行。在单指令周期内完成对内存的读-改-写,无需加锁。

Linux 内核提供两类原子操作接口:

  • 原子整数操作:atomic_t 类型及 atomic_inc()、atomic_dec()、atomic_add_return() 等
  • 原子位操作:set_bit()、clear_bit()、test_and_set_bit() 等
// 使用原子操作保护计数器,无需锁
#include <linux/atomic.h>

struct device_data {
    atomic_t usage_count;
    void __iomem *reg_base;
};

// 在中断上下文或任意CPU上安全调用
void increment_usage(struct device_data *dev) {
    atomic_inc(&dev->usage_count);
}

// 读取当前值
int get_usage(struct device_data *dev) {
    return atomic_read(&dev->usage_count);
}
原子操作的局限性: 原子操作只能保护单个整数或单个位。当临界区包含多条指令、复杂数据结构(链表、树)或需要条件判断时,原子操作无能为力,必须使用锁。
原子操作 vs 非原子操作 非原子操作(可中断) 1. 读取 count 到寄存器 2. 寄存器 +1 3. 写回 count ← 步骤之间可能被中断抢占 原子操作(不可分割) 1. 锁定内存总线 2. 读取-修改-写入(单指令) 3. 释放内存总线 ← 整个操作不可被中断 关键区别 非原子操作:CPU 0 执行到步骤2时被抢占,CPU 1 读取旧值,导致计数丢失 原子操作:硬件保证整个读-改-写序列不被任何中断或CPU核心打断 vs

2.4 锁的引入:从原子操作到通用保护

当临界区无法用单个原子操作表达时,就需要引入锁(Lock)机制。锁是一种高级抽象,它利用底层的原子操作(如 test_and_set)来实现互斥访问。

Linux 内核提供两大类锁:

锁类型 等待行为 适用上下文 典型场景
自旋锁(spinlock) 忙等待(spin) 中断上下文、SMP 保护短临界区,不可睡眠
互斥锁(mutex) 睡眠等待 进程上下文 保护长临界区,允许睡眠
读写锁(rwlock/rwsem) 读共享/写互斥 读多写少场景 路由表、配置数据
选择锁的核心依据: 临界区是否允许睡眠?如果允许,使用互斥锁;如果不允许(如中断处理程序),必须使用自旋锁。
锁类型选择决策流程 需要保护临界区 临界区是否允许睡眠? 否 是 使用自旋锁 中断上下文 / 短临界区 使用互斥锁 进程上下文 / 长临界区 ⚠ 选错锁类型会导致死锁或系统崩溃,后续章节将详细分析

2.5 临界区保护实战原则

在编写驱动代码时,遵循以下原则可以避免大多数并发问题:

  1. 识别共享数据:明确哪些变量、队列、描述符被多个执行单元访问
  2. 划定临界区边界:只包裹真正访问共享数据的代码行
  3. 选择正确的锁:根据上下文(中断/进程)和临界区长度选择自旋锁或互斥锁
  4. 保持锁层次一致:多个锁同时使用时,所有路径必须按相同顺序获取
  5. 避免锁内睡眠:持有自旋锁时绝对不能睡眠,持有互斥锁时避免长时间睡眠
调试提示: 在开发初期,可以使用 lockdep 内核锁依赖检测工具自动发现潜在的死锁路径。在 .config 中启用 CONFIG_PROVE_LOCKING 即可。