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驱动中,摄像头传感器产生的中断信号(如帧同步、行结束、错误标志)通常按此模型处理。

中断处理:上半部与下半部拆分模型 硬件中断源 上半部(Top Half) 立即响应,快速完成 清除中断标志 保存关键硬件状态 下半部(Bottom Half) 延迟执行,可被中断 拷贝帧数据 唤醒等待队列 调用V4L2回调 下半部实现方式 • 软中断(SoftIRQ) • Tasklet(基于软中断) • 工作队列(Workqueue) 时间线:中断发生 → 上半部执行(微秒级) → 下半部调度 → 下半部执行(毫秒级) 上半部关闭当前中断线,下半部使能所有中断,保证系统实时性

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驱动中的中断上下文约束总结

V4L2驱动中断上下文操作约束 ✅ 允许的操作(上半部) • 读写硬件寄存器(ioread/iowrite) • 清除中断标志位 • 调度下半部(tasklet_schedule / queue_work) • 更新原子变量或自旋锁保护的数据 • 调用 ktime_get() 记录时间戳 • 唤醒等待队列(wake_up_interruptible) • 返回 IRQ_HANDLED 或 IRQ_NONE ❌ 禁止的操作(上半部) • 调用 schedule() 或 msleep() • 使用 mutex_lock()(应使用 spinlock) • 调用 kmalloc(GFP_KERNEL) • 访问用户空间内存(copy_from_user) • 执行可能导致页面错误的操作 • 调用 printk() 大量输出(影响性能) • 获取或释放信号量

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(),必须成对出现且注意释放顺序。
  • 共享数据保护必须使用自旋锁,并根据上下文选择正确的加锁变体。