2. 中断处理基础:Linux内核中断上下文、上半部与下半部机制、中断注册与释放API
2.1 中断上下文与进程上下文
在Linux内核中,代码执行分为两种截然不同的上下文:进程上下文(Process Context)和中断上下文(Interrupt Context)。V4L2驱动的中断处理必须严格区分这两种上下文,因为中断上下文中的操作受到严格限制。
| 对比维度 | 进程上下文 | 中断上下文 |
|---|---|---|
| 可睡眠 | 是(可调用 schedule()) | 否(禁止睡眠) |
| 可访问用户空间 | 是(copy_from/to_user) | 否(无法访问) |
| 持有锁类型 | 互斥锁(mutex)、自旋锁 | 仅自旋锁(spinlock) |
| 当前进程指针 | 有效(current 可用) | 无效(current 指向被中断进程) |
| 抢占状态 | 可被抢占 | 不可被抢占(除非中断嵌套) |
| 典型执行时间 | 毫秒~秒级 | 微秒级(必须极短) |
⚠️ 关键约束:V4L2驱动在中断上下文中绝对不能调用 kmalloc(GFP_KERNEL)、copy_from_user()、mutex_lock() 等可能引起睡眠的函数。违反此规则会导致内核死锁或系统崩溃。
2.2 上半部与下半部机制
为了在中断响应速度和系统吞吐量之间取得平衡,Linux内核将中断处理拆分为两个阶段:上半部(Top Half)和下半部(Bottom Half)。V4L2驱动中,摄像头传感器产生的中断信号(如帧同步、行结束、错误标志)通常按此模型处理。
2.3 下半部实现方式对比
V4L2驱动开发者需要根据实时性要求和数据量大小选择下半部实现方式。以下是三种主流机制的对比:
| 特性 | SoftIRQ(软中断) | Tasklet | Workqueue(工作队列) |
|---|---|---|---|
| 执行上下文 | 中断上下文 | 中断上下文 | 进程上下文(内核线程) |
| 是否可睡眠 | 否 | 否 | 是 |
| 实时性 | 最高(立即执行) | 高(下一个调度点) | 低(依赖调度器) |
| 并发性 | 同类型可多CPU并行 | 同类型串行执行 | 可多CPU并行 |
| 典型V4L2用途 | 网络摄像头、高速帧 | 传感器配置、状态更新 | 帧数据拷贝、DMA清理 |
| 注册API | open_softirq() | tasklet_init() | INIT_WORK() / queue_work() |
💡 V4L2实践建议:对于摄像头帧中断,推荐使用工作队列处理数据拷贝和V4L2缓冲区操作,因为这部分可能涉及睡眠操作(如等待DMA完成)。对于传感器状态变化等轻量级事件,使用Tasklet即可。
2.4 中断注册与释放API
Linux内核提供标准API用于中断的注册和释放。V4L2驱动通常在 probe() 函数中注册中断,在 remove() 函数中释放中断。
2.4.1 中断注册:request_irq()
#include <linux/interrupt.h>
/**
* request_irq() - 注册中断处理函数
* @irq: 中断号(从设备树或ACPI获取)
* @handler: 上半部处理函数指针
* @flags: 中断标志位
* @name: 中断名称(/proc/interrupts 中显示)
* @dev: 设备标识(传递给处理函数,通常为设备结构体)
*
* 返回值:0 成功,负值 失败
*/
int request_irq(unsigned int irq, irq_handler_t handler,
unsigned long flags, const char *name, void *dev);
常用flags标志位:
IRQF_TRIGGER_RISING— 上升沿触发(摄像头传感器常用)IRQF_TRIGGER_FALLING— 下降沿触发IRQF_SHARED— 共享中断线(多个设备共用)IRQF_ONESHOT— 中断处理期间屏蔽中断线(防止嵌套)IRQF_NO_SUSPEND— 系统休眠时不关闭中断
2.4.2 中断处理函数原型
/**
* 上半部中断处理函数
* @irq: 中断号
* @dev_id: request_irq() 传入的 dev 参数
*
* 返回值:IRQ_NONE(非本设备中断)或 IRQ_HANDLED(已处理)
*/
static irqreturn_t sensor_irq_handler(int irq, void *dev_id)
{
struct sensor_device *sensor = (struct sensor_device *)dev_id;
/* 1. 读取中断状态寄存器,确认中断源 */
u32 status = ioread32(sensor->base + REG_INT_STATUS);
if (!(status & INT_VSYNC)) {
/* 不是本设备的中断,返回 IRQ_NONE */
return IRQ_NONE;
}
/* 2. 清除中断标志(必须在上半部完成) */
iowrite32(INT_VSYNC, sensor->base + REG_INT_CLEAR);
/* 3. 调度下半部(以 Tasklet 为例) */
tasklet_schedule(&sensor->frame_tasklet);
/* 4. 返回已处理 */
return IRQ_HANDLED;
}
2.4.3 中断释放:free_irq()
/**
* free_irq() - 释放已注册的中断
* @irq: 中断号
* @dev_id: 必须与 request_irq() 传入的 dev 一致
*
* 注意:必须在确保中断处理函数不再执行后调用
*/
void free_irq(unsigned int irq, void *dev_id);
2.4.4 完整注册/释放流程示例
/* probe() 函数中注册中断 */
static int sensor_probe(struct platform_device *pdev)
{
struct sensor_device *sensor;
int ret, irq;
sensor = devm_kzalloc(&pdev->dev, sizeof(*sensor), GFP_KERNEL);
if (!sensor)
return -ENOMEM;
/* 从设备树获取中断号 */
irq = platform_get_irq(pdev, 0);
if (irq < 0)
return irq;
/* 初始化下半部(Tasklet) */
tasklet_init(&sensor->frame_tasklet, sensor_frame_bottom_half,
(unsigned long)sensor);
/* 注册中断:上升沿触发,非共享 */
ret = request_irq(irq, sensor_irq_handler,
IRQF_TRIGGER_RISING,
"sensor_vsync", sensor);
if (ret) {
dev_err(&pdev->dev, "Failed to request IRQ %d\n", irq);
return ret;
}
sensor->irq = irq;
platform_set_drvdata(pdev, sensor);
return 0;
}
/* remove() 函数中释放中断 */
static int sensor_remove(struct platform_device *pdev)
{
struct sensor_device *sensor = platform_get_drvdata(pdev);
/* 1. 先禁用设备中断(硬件层面) */
iowrite32(0, sensor->base + REG_INT_ENABLE);
/* 2. 释放中断 */
free_irq(sensor->irq, sensor);
/* 3. 清理下半部 */
tasklet_kill(&sensor->frame_tasklet);
return 0;
}
🔑 关键要点:
- 中断处理函数必须快速返回(微秒级),复杂操作交给下半部。
- 共享中断时,处理函数必须检查中断状态寄存器,确认是否为本设备中断。
- 释放中断前,必须先禁用硬件中断产生,防止释放后仍有中断到达。
- 使用
devm_request_irq()可以自动管理中断释放,简化错误处理路径。
2.5 V4L2驱动中的中断上下文约束总结
2.6 中断上下文中的自旋锁使用
当中断上下文和进程上下文需要共享数据时,必须使用自旋锁(spinlock)而非互斥锁。自旋锁在中断上下文中使用时,需要关闭本地中断以防止死锁。
/* 定义自旋锁 */
spinlock_t sensor_lock;
/* 初始化 */
spin_lock_init(&sensor->lock);
/* 进程上下文中使用 */
spin_lock_irqsave(&sensor->lock, flags);
/* 访问共享数据 */
sensor->frame_count++;
spin_unlock_irqrestore(&sensor->lock, flags);
/* 中断上下文中使用(无需保存/恢复中断状态) */
spin_lock(&sensor->lock);
/* 访问共享数据 */
sensor->frame_count++;
spin_unlock(&sensor->lock);
⚠️ 死锁风险:如果在中断上下文中使用
spin_lock_irqsave(),而该中断正是当前处理的中断,会导致递归自旋锁死锁。正确的做法是:中断上下文中使用 spin_lock(),进程上下文中使用 spin_lock_irqsave()。
2.7 本章小结
- 中断上下文是内核中最高优先级的执行环境,禁止睡眠和阻塞操作。
- 上半部负责快速响应硬件事件(清除标志、保存状态),下半部负责延迟处理(数据拷贝、唤醒等待)。
- V4L2驱动中,Tasklet适用于轻量级传感器事件,工作队列适用于帧数据搬运等可能睡眠的操作。
- 中断注册使用
request_irq(),释放使用free_irq(),必须成对出现且注意释放顺序。 - 共享数据保护必须使用自旋锁,并根据上下文选择正确的加锁变体。