4. 内核并发与同步:自旋锁、互斥锁、读写锁、原子操作在驱动中的应用

在 Linux 内核驱动开发中,并发访问共享资源是导致竞态条件(Race Condition)和安全漏洞的主要根源。中断处理、内核抢占、多核 SMP 并发以及用户空间访问都可能同时进入驱动临界区。本章系统讲解内核提供的四种核心同步机制,并结合安全驱动开发场景分析其选型与使用规范。

安全要点: 不正确的同步会导致数据损坏、内核崩溃(Oops)或权限提升漏洞。选择错误的锁原语(如在中断上下文使用互斥锁)会引发死锁。本章所有示例均遵循内核安全编码规范。

4.1 并发场景与竞态条件

驱动中的并发来源可归纳为以下四类,理解这些场景是正确选择同步机制的前提。

并发来源 典型场景 安全风险
SMP 多核并行 两个 CPU 核同时执行驱动中的同一个 ioctl 函数 共享变量写-写冲突
内核抢占 进程 A 持有锁时被高优先级进程 B 抢占 锁持有者被抢占导致死锁
中断处理 进程上下文与中断处理程序共享设备状态寄存器 中断打断进程导致数据不一致
进程睡眠与唤醒 等待队列操作与中断完成标志的同步 丢失唤醒事件
驱动并发竞态场景示意图 CPU 0 CPU 1 中断处理 内核抢占 共享资源(设备寄存器/全局数据) 竞态条件 → 数据损坏 / 安全漏洞 驱动必须保证:任何时刻最多只有一个执行路径操作共享资源

4.2 原子操作:最轻量的同步原语

原子操作(Atomic Operations)是硬件级保证的不可分割的读-改-写序列,适用于计数器、标志位等简单整型变量的并发安全操作。它不涉及锁的获取与释放,不会导致睡眠或上下文切换。

4.2.1 原子类型与 API

API 函数 功能描述 安全上下文
atomic_set(v, i) 设置原子变量 v 的值为 i 进程/中断
atomic_read(v) 读取原子变量 v 的值 进程/中断
atomic_add(i, v) 原子地将 i 加到 v 进程/中断
atomic_inc(v) 原子递增 v 进程/中断
atomic_dec_and_test(v) 原子递减 v,若结果为 0 返回 true 进程/中断
atomic_cmpxchg(v, old, new) 比较交换:若 v==old 则交换为 new 进程/中断
安全实践: 原子操作是中断安全的,可在中断处理程序中直接使用。对于引用计数、设备打开计数等场景,优先使用 atomic_t 而非普通 int。

4.2.2 驱动示例:设备打开计数

#include <linux/atomic.h>

static atomic_t device_open_count = ATOMIC_INIT(0);

static int my_device_open(struct inode *inode, struct file *file)
{
    if (!atomic_add_unless(&device_open_count, 1, MAX_OPEN_COUNT))
        return -EBUSY;  /* 已达最大打开数 */
    return 0;
}

static int my_device_release(struct inode *inode, struct file *file)
{
    atomic_dec(&device_open_count);
    return 0;
}

4.3 自旋锁:短临界区的首选

自旋锁(Spinlock)是一种忙等锁,当锁被持有时,等待的 CPU 会原地循环检测锁状态。它适用于临界区执行时间极短(微秒级)且不允许睡眠的场景,如中断上下文。

4.3.1 自旋锁变体与选择

变体 禁用抢占 禁用本地中断 适用场景
spin_lock() 是 否 进程上下文,无中断共享
spin_lock_irq() 是 是 进程与中断上下文共享
spin_lock_irqsave() 是 是(保存状态) 不确定中断是否已禁用
spin_trylock() 是 否 非阻塞尝试获取
死锁陷阱: 在持有自旋锁时调用 copy_from_user()、kmalloc(GFP_KERNEL) 等可能睡眠的函数,会导致内核崩溃。自旋锁临界区必须原子执行,不可睡眠。

4.3.2 驱动示例:中断与进程共享寄存器

static DEFINE_SPINLOCK(dev_lock);
static u32 device_status;

/* 中断处理程序 */
static irqreturn_t my_interrupt(int irq, void *dev_id)
{
    spin_lock(&dev_lock);
    device_status = readl(REG_STATUS);
    /* 处理中断 */
    spin_unlock(&dev_lock);
    return IRQ_HANDLED;
}

/* 进程上下文读状态 */
static ssize_t my_read(struct file *file, char __user *buf, size_t len, loff_t *off)
{
    u32 status;
    unsigned long flags;

    spin_lock_irqsave(&dev_lock, flags);
    status = device_status;
    spin_unlock_irqrestore(&dev_lock, flags);

    if (copy_to_user(buf, &status, sizeof(status)))
        return -EFAULT;
    return sizeof(status);
}
自旋锁在中断与进程上下文中的使用流程 进程上下文 spin_lock_irqsave 读/写共享数据 spin_unlock_irqrestore 中断上下文 spin_lock 读/写共享数据 spin_unlock 共享资源 device_status

4.4 互斥锁:允许睡眠的长期锁

互斥锁(Mutex)是一种睡眠等待锁,当锁被持有时,等待的进程会进入睡眠状态,不会占用 CPU。它适用于临界区执行时间较长(毫秒级或更长)或需要睡眠的场景,但不能在中断上下文使用。

4.4.1 Mutex 与 Spinlock 对比

特性 Mutex Spinlock
等待行为 睡眠等待,不占 CPU 忙等,占用 CPU
中断上下文 禁止使用 允许使用
临界区时长 任意长度 极短(微秒级)
递归锁定 不支持(会死锁) 不支持
优先级反转 内核已处理(PI) 不涉及

4.4.2 驱动示例:配置操作保护

static DEFINE_MUTEX(config_mutex);
static struct device_config config_data;

static long my_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
{
    struct device_config __user *uconfig = (struct device_config __user *)arg;

    switch (cmd) {
    case SET_CONFIG:
        if (mutex_lock_interruptible(&config_mutex))
            return -ERESTARTSYS;
        
        if (copy_from_user(&config_data, uconfig, sizeof(config_data))) {
            mutex_unlock(&config_mutex);
            return -EFAULT;
        }
        /* 应用配置到硬件 */
        apply_config(&config_data);
        
        mutex_unlock(&config_mutex);
        return 0;

    case GET_CONFIG:
        mutex_lock(&config_mutex);
        if (copy_to_user(uconfig, &config_data, sizeof(config_data))) {
            mutex_unlock(&config_mutex);
            return -EFAULT;
        }
        mutex_unlock(&config_mutex);
        return 0;
    }
    return -ENOTTY;
}
安全提示: 使用 mutex_lock_interruptible() 而非 mutex_lock() 允许进程在等待锁时被信号中断,避免驱动导致用户空间进程无法被杀死的“D 状态”问题。

4.5 读写锁:区分读/写访问模式

读写锁(rwlock / rwsem)允许多个读者并发访问,但写者必须独占。当读操作远多于写操作时,读写锁能显著提升并发性能。

4.5.1 自旋读写锁(rwlock_t)

rwlock_t 是自旋锁的读写变体,适用于中断上下文中的读多写少场景。

static DEFINE_RWLOCK(my_rwlock);
static u32 statistics[64];

/* 多个读者可同时进入 */
static u32 read_statistic(int index)
{
    u32 val;
    read_lock(&my_rwlock);
    val = statistics[index];
    read_unlock(&my_rwlock);
    return val;
}

/* 写者必须独占 */
static void write_statistic(int index, u32 val)
{
    write_lock(&my_rwlock);
    statistics[index] = val;
    write_unlock(&my_rwlock);
}

4.5.2 信号量读写锁(rwsem)

struct rw_semaphore 是互斥锁的读写变体,允许读者睡眠,适用于进程上下文。

操作 读者 API 写者 API
获取锁 down_read() down_write()
可中断获取 down_read_interruptible() down_write_interruptible()
释放锁 up_read() up_write()
尝试获取 down_read_trylock() down_write_trylock()
读写锁并发访问模型 读者 1 读者 2 读者 N 写者(独占) 共享数据 读锁:可多个同时持有 多个读者可同时持有读锁;写者必须等待所有读者释放,且后续读者必须等待写者完成

4.6 同步机制选型决策树

在驱动开发中选择同步机制时,可按照以下决策流程快速定位合适的原语:

  1. 临界区是否在中断上下文执行?
    • 是 → 只能使用自旋锁或原子操作
    • 否 → 进入下一步
  2. 临界区是否可能睡眠?
    • 是(如 copy_from_user、kmalloc 等)→ 使用 Mutex 或 rwsem
    • 否 → 进入下一步
  3. 临界区执行时间是否极短(微秒级)?
    • 是 → 自旋锁
    • 否 → Mutex
  4. 读操作是否远多于写操作?
    • 是 → 考虑读写锁(rwlock / rwsem)
    • 否 → 使用基本锁即可
  5. 是否仅操作简单整型变量?
    • 是 → 优先使用原子操作,避免锁开销
安全驱动开发原则: 锁的粒度要尽量小,持有时间要尽量短。优先使用原子操作,其次自旋锁,最后互斥锁。避免在持有锁时调用外部函数或执行复杂操作,减少死锁和优先级反转风险。

4.7 死锁预防与调试

死锁是驱动开发中最危险的并发问题之一。内核提供了多种工具帮助检测和预防死锁:

工具/机制 功能 启用方式
Lockdep 运行时锁依赖验证,检测潜在死锁 内核配置 CONFIG_LOCKDEP=y
Spinlock 调试 检测自旋锁使用错误(如睡眠时持有锁) CONFIG_DEBUG_SPINLOCK=y
Mutex 调试 检测互斥锁使用错误 CONFIG_DEBUG_MUTEXES=y
原子操作调试 检测原子变量溢出或未初始化 CONFIG_DEBUG_ATOMIC_SLEEP=y
安全警告: 生产环境中应关闭调试选项以提升性能,但开发阶段必须启用 Lockdep。Lockdep 报告的“possible deadlock”警告必须视为严重 bug 修复,不可忽略。

4.7.1 常见死锁场景与规避

  • AB-BA 死锁: 线程 1 持有锁 A 等待锁 B,线程 2 持有锁 B 等待锁 A。规避:所有代码路径按相同顺序获取锁。
  • 自死锁: 递归调用或同一个函数再次获取已持有的非递归锁。规避:明确锁的层次,避免递归。
  • 中断死锁: 进程持有锁时被中断打断,中断处理程序尝试获取同一锁。规避:在进程上下文使用 spin_lock_irqsave() 禁用本地中断。
  • 优先级反转: 低优先级进程持有锁,高优先级进程等待,中优先级进程抢占低优先级进程。规避:使用支持优先级继承的 Mutex(内核默认支持)。

4.8 本章小结

内核并发同步是安全驱动开发的基石。原子操作为简单计数提供无锁方案;自旋锁保护中断上下文中的短临界区;互斥锁适用于进程上下文中可能睡眠的长操作;读写锁优化读多写少场景。选择同步机制时,必须结合执行上下文、临界区时长和访问模式综合判断。启用 Lockdep 等调试工具,在开发阶段彻底消除死锁风险,是生产级驱动的基本要求。