4. 内核并发控制:自旋锁、互斥锁、信号量与读写锁

在 Linux 内核驱动开发中,尤其是 V4L2 摄像头驱动,多个执行路径(进程上下文、软中断、硬中断、多核 SMP)会同时访问共享资源(如硬件寄存器、视频缓冲区队列、设备状态标志)。并发控制是保证数据一致性与系统稳定性的基石。本章深入剖析四种核心同步机制:自旋锁、互斥锁、信号量和读写锁,并结合 V4L2 场景给出选型指南。

核心原则:锁的选择取决于临界区的执行时间、上下文(是否可睡眠)以及读写访问模式。错误的锁选择会导致死锁或性能急剧下降。
四种内核锁的适用场景对比 自旋锁 临界区极短 不可睡眠上下文 中断/软中断中 互斥锁 临界区较长 进程上下文可睡眠 V4L2 核心调用 信号量 资源计数管理 生产者-消费者 有限资源池 读写锁 读多写少场景 设备配置读取 状态查询 选型决策要点 • 自旋锁:不睡眠,适合保护硬件寄存器操作(如设置摄像头帧尺寸),持有时间应 < 几十微秒。 • 互斥锁:允许睡眠,适合保护 V4L2 文件操作(如 ioctl)中的复杂逻辑。 • 信号量:用于计数资源,如限制同时访问视频缓冲区的进程数。 • 读写锁:允许多个读者并发,适合设备能力查询,写者独占。

4.1 自旋锁 (spinlock)

自旋锁是最轻量级的锁,当锁被持有时,等待的线程会忙等待(循环检查锁状态),不会睡眠。这决定了它只能在不可睡眠的上下文中使用,如中断处理程序、软中断、以及持有锁期间不能调用任何可能睡眠的函数。

4.1.1 核心 API 与变体

API 函数说明禁用中断
spin_lock()获取自旋锁否
spin_unlock()释放自旋锁否
spin_lock_irqsave()获取锁并保存本地中断状态是(保存状态)
spin_unlock_irqrestore()释放锁并恢复中断状态是(恢复状态)
spin_lock_bh()获取锁并禁用下半部(软中断)仅下半部
spin_unlock_bh()释放锁并启用下半部仅下半部
V4L2 驱动陷阱:在摄像头驱动的中断处理函数中访问共享的 DMA 缓冲区描述符时,必须使用 spin_lock_irqsave(),因为中断处理程序可能在任何时刻被更高优先级的中断打断。使用 spin_lock() 会导致死锁。

4.1.2 典型用法:保护硬件寄存器访问

/* 摄像头驱动中保护 I2C 寄存器配置 */
static DEFINE_SPINLOCK(sensor_lock);
static struct sensor_priv {
    unsigned long flags;
    u8 reg_value;
} sensor_data;

void sensor_write_reg(u8 reg, u8 val)
{
    unsigned long irq_flags;
    spin_lock_irqsave(&sensor_lock, irq_flags);
    /* 临界区:写硬件寄存器 */
    i2c_smbus_write_byte_data(client, reg, val);
    sensor_data.reg_value = val;
    spin_unlock_irqrestore(&sensor_lock, irq_flags);
}

4.2 互斥锁 (mutex)

互斥锁是睡眠锁,当锁被持有时,等待的进程会进入睡眠状态,直到锁被释放。它适用于临界区较长、可能调用 copy_to_user()、kmalloc() 等可能睡眠函数的场景。在 V4L2 驱动中,v4l2_fh 和 video_device 的核心操作通常使用互斥锁保护。

4.2.1 核心 API

API 函数说明
DEFINE_MUTEX(name)静态定义互斥锁
mutex_init()动态初始化互斥锁
mutex_lock()获取互斥锁(可睡眠)
mutex_unlock()释放互斥锁
mutex_trylock()尝试获取锁,失败立即返回

4.2.2 V4L2 驱动中的互斥锁模式

/* V4L2 设备结构体中的互斥锁 */
struct v4l2_device {
    struct mutex lock;  /* 设备级互斥锁 */
    /* ... 其他成员 */
};

/* 在 open/ioctl 中使用 */
static int sensor_open(struct file *filp)
{
    struct v4l2_device *v4l2_dev = video_drvdata(filp);
    /* 互斥锁保护设备打开计数与初始化 */
    mutex_lock(&v4l2_dev->lock);
    /* 初始化硬件、分配缓冲区等 */
    mutex_unlock(&v4l2_dev->lock);
    return 0;
}
最佳实践:V4L2 框架推荐在 v4l2_device 或自定义驱动结构体中嵌入一个 struct mutex,用于序列化所有 ioctl 调用。这可以避免多个用户空间线程同时操作摄像头配置导致的竞态。

4.3 信号量 (semaphore)

信号量是一个计数器,允许多个线程同时访问有限数量的资源。在 Linux 内核中,信号量主要用于计数型同步,而非简单的互斥。虽然互斥锁可以看作计数为 1 的信号量,但内核社区已明确推荐使用 struct mutex 替代信号量实现互斥。

4.3.1 核心 API

API 函数说明
sema_init()初始化信号量并设置初始计数
down()获取信号量(可睡眠,可被中断)
down_interruptible()获取信号量,允许被信号中断
up()释放信号量

4.3.2 应用场景:视频缓冲区池管理

/* 使用信号量管理有限的 DMA 缓冲区 */
struct video_buffer_pool {
    struct semaphore buf_sem;  /* 初始化为可用缓冲区数量 */
    struct buffer *buffers;
};

/* 生产者:硬件填充完成一个缓冲区 */
void buffer_done(struct video_buffer_pool *pool)
{
    /* 增加可用缓冲区计数 */
    up(&pool->buf_sem);
}

/* 消费者:应用程序请求一个缓冲区 */
struct buffer *get_buffer(struct video_buffer_pool *pool)
{
    if (down_interruptible(&pool->buf_sem))
        return ERR_PTR(-ERESTARTSYS);
    /* 返回一个已准备好的缓冲区 */
    return get_ready_buffer(pool);
}

4.4 读写锁 (rwlock)

读写锁允许多个读者同时持有锁,但写者必须独占。当临界区的访问模式是读远多于写时,读写锁可以显著提升并发性能。在 V4L2 驱动中,摄像头的能力查询(如支持的帧格式、分辨率列表)通常是只读操作,而配置更新(如设置帧率)是写操作。

4.4.1 核心 API

API 函数读者/写者说明
read_lock()读者获取读锁
read_unlock()读者释放读锁
write_lock()写者获取写锁(等待所有读者释放)
write_unlock()写者释放写锁
rwlock_init()-动态初始化读写锁

4.4.2 典型用法:保护设备能力描述符

/* 摄像头能力描述符 */
struct camera_caps {
    struct rwlock caps_lock;
    u32 supported_formats[10];
    u32 current_width;
    u32 current_height;
};

/* 读者:查询支持的分辨率列表 */
u32 *get_supported_resolutions(struct camera_caps *caps)
{
    u32 *list;
    read_lock(&caps->caps_lock);
    list = caps->supported_formats;  /* 只读访问 */
    read_unlock(&caps->caps_lock);
    return list;
}

/* 写者:更新当前分辨率 */
void set_current_resolution(struct camera_caps *caps, u32 w, u32 h)
{
    write_lock(&caps->caps_lock);
    caps->current_width = w;
    caps->current_height = h;
    write_unlock(&caps->caps_lock);
}

4.5 锁的选择决策表

场景特征推荐锁理由
临界区极短(< 几十条指令)自旋锁避免上下文切换开销
临界区可能睡眠互斥锁允许进程睡眠,释放 CPU
中断上下文中访问共享数据自旋锁 (irqsave)唯一能在中断中使用的锁
资源计数管理(有限资源池)信号量计数语义天然匹配
读操作远多于写操作读写锁读者并发,提升吞吐量
V4L2 ioctl 序列化互斥锁ioctl 可能睡眠,且需要互斥
DMA 描述符环保护自旋锁中断上下文中操作,必须原子
内核锁选择决策流程图 开始:需要保护共享资源 是否在中断/软中断上下文? 自旋锁 (irqsave) 临界区可能睡眠? 互斥锁 读操作远多于写操作? 读写锁 需要资源计数管理? 信号量 是 否 是 否 是 否 是

4.6 V4L2 驱动中的锁使用规范

  1. 设备结构体锁:每个 v4l2_device 或自定义驱动结构体应包含一个 struct mutex,用于保护所有 ioctl 操作。V4L2 核心提供了 v4l2_fh->lock 字段,驱动可以直接使用。
  2. 中断与下半部:在 vb2_ops 的回调中(如 buf_queue),如果涉及硬件操作,使用自旋锁保护。V4L2 的 vb2_queue 内部使用自旋锁管理缓冲区队列。
  3. 避免锁嵌套:不要在持有互斥锁时再去获取另一个互斥锁,除非有严格的锁层级定义。V4L2 框架的锁顺序是:设备锁 > 队列锁 > 硬件寄存器锁。
  4. 使用锁验证工具:开启内核配置 CONFIG_DEBUG_SPINLOCK、CONFIG_DEBUG_MUTEXES 和 CONFIG_PROVE_LOCKING,在开发阶段检测死锁与锁使用错误。
V4L2 锁使用速查:
• v4l2_device->lock → 互斥锁,保护设备打开/关闭/ioctl
• vb2_queue->lock → 自旋锁,保护缓冲区队列操作
• video_device->lock → 互斥锁,保护文件操作(可选)
• 硬件寄存器访问 → 自旋锁 (irqsave),保护与硬件交互的临界区

4.7 死锁预防与调试

死锁场景原因解决方案
自旋锁中调用睡眠函数忙等待的 CPU 永远无法释放锁使用互斥锁替代,或确保临界区不睡眠
中断中获取已持有的自旋锁中断处理程序与进程上下文争用同一锁使用 spin_lock_irqsave() 禁用本地中断
锁顺序不一致线程 A 持有锁 1 等待锁 2,线程 B 持有锁 2 等待锁 1定义全局锁层级,始终按相同顺序获取
递归获取互斥锁同一线程重复锁定非递归互斥锁检查代码路径,避免递归调用;或使用 mutex_lock_nested()

在 V4L2 摄像头驱动开发中,正确选择和使用锁是驱动稳定性的基石。自旋锁保护微秒级的硬件访问,互斥锁保护毫秒级的配置流程,信号量管理有限的视频缓冲区,读写锁优化读多写少的查询场景。理解每种锁的语义和限制,结合 V4L2 框架的锁使用规范,才能编写出既高效又安全的摄像头驱动。