4. 内核并发控制:自旋锁、互斥锁、信号量与读写锁
在 Linux 内核驱动开发中,尤其是 V4L2 摄像头驱动,多个执行路径(进程上下文、软中断、硬中断、多核 SMP)会同时访问共享资源(如硬件寄存器、视频缓冲区队列、设备状态标志)。并发控制是保证数据一致性与系统稳定性的基石。本章深入剖析四种核心同步机制:自旋锁、互斥锁、信号量和读写锁,并结合 V4L2 场景给出选型指南。
4.1 自旋锁 (spinlock)
自旋锁是最轻量级的锁,当锁被持有时,等待的线程会忙等待(循环检查锁状态),不会睡眠。这决定了它只能在不可睡眠的上下文中使用,如中断处理程序、软中断、以及持有锁期间不能调用任何可能睡眠的函数。
4.1.1 核心 API 与变体
| API 函数 | 说明 | 禁用中断 |
|---|---|---|
spin_lock() | 获取自旋锁 | 否 |
spin_unlock() | 释放自旋锁 | 否 |
spin_lock_irqsave() | 获取锁并保存本地中断状态 | 是(保存状态) |
spin_unlock_irqrestore() | 释放锁并恢复中断状态 | 是(恢复状态) |
spin_lock_bh() | 获取锁并禁用下半部(软中断) | 仅下半部 |
spin_unlock_bh() | 释放锁并启用下半部 | 仅下半部 |
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_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 描述符环保护 | 自旋锁 | 中断上下文中操作,必须原子 |
4.6 V4L2 驱动中的锁使用规范
- 设备结构体锁:每个
v4l2_device或自定义驱动结构体应包含一个struct mutex,用于保护所有ioctl操作。V4L2 核心提供了v4l2_fh->lock字段,驱动可以直接使用。 - 中断与下半部:在
vb2_ops的回调中(如buf_queue),如果涉及硬件操作,使用自旋锁保护。V4L2 的vb2_queue内部使用自旋锁管理缓冲区队列。 - 避免锁嵌套:不要在持有互斥锁时再去获取另一个互斥锁,除非有严格的锁层级定义。V4L2 框架的锁顺序是:设备锁 > 队列锁 > 硬件寄存器锁。
- 使用锁验证工具:开启内核配置
CONFIG_DEBUG_SPINLOCK、CONFIG_DEBUG_MUTEXES和CONFIG_PROVE_LOCKING,在开发阶段检测死锁与锁使用错误。
•
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 框架的锁使用规范,才能编写出既高效又安全的摄像头驱动。