V4L2驱动中断处理:多核系统负载均衡与实时响应调优 - 第2节

2.1 Linux 中断子系统架构概览

在深入 V4L2 驱动中断处理之前,必须理解 Linux 内核中断子系统的分层设计。中断从硬件触发到驱动处理函数执行,经历了多个抽象层,每一层都承担特定的职责。

核心概念: Linux 中断子系统将硬件中断请求(IRQ)从设备驱动解耦,通过通用中断层(Generic IRQ Layer)提供统一的 API,使得驱动开发者无需关心底层中断控制器的具体实现。
Linux 中断子系统分层架构 硬件层 外设设备(Sensor、DMA、I2C) → 中断控制器(GIC、APIC、INTC) 通用中断层(Generic IRQ Layer) 中断号映射、irq_desc 管理、中断流控制、中断亲和性 中断处理框架 request_irq / free_irq / 中断线程化 / 中断共享 驱动层(V4L2 驱动) 中断处理函数(Top Half) + Tasklet / Workqueue(Bottom Half)

从上图可以看出,V4L2 驱动开发者主要与中断处理框架和驱动层交互。硬件中断经过通用中断层分发后,最终调用驱动注册的中断处理函数。

2.2 中断注册与注销:request_irq / free_irq

驱动通过 request_irq() 向内核注册中断处理函数,在模块卸载或设备移除时通过 free_irq() 注销。这是中断处理的入口和出口。

2.2.1 request_irq 函数原型

#include <linux/interrupt.h>

int request_irq(unsigned int irq,
                irq_handler_t handler,
                unsigned long flags,
                const char *name,
                void *dev);
参数 含义 典型值 / 说明
irq 中断号 通过 platform_get_irq() 或设备树获取
handler 中断处理函数指针 类型为 irq_handler_t,即 irqreturn_t (*handler)(int, void *)
flags 中断标志位 IRQF_TRIGGER_RISING、IRQF_SHARED、IRQF_NO_SUSPEND 等
name 设备名称 用于 /proc/interrupts 显示,如 "v4l2_camera"
dev 设备标识 共享中断时用于区分,通常传 struct device 指针

2.2.2 中断标志位 flags 详解

  • IRQF_TRIGGER_RISING / FALLING / HIGH / LOW:指定中断触发方式,必须与硬件匹配。
  • IRQF_SHARED:允许共享中断线,多个设备共用同一 IRQ 号。
  • IRQF_NO_SUSPEND:系统休眠时该中断不屏蔽,用于唤醒源。
  • IRQF_ONESHOT:中断处理期间自动屏蔽该中断线,处理完再重新使能,常用于线程化中断。
常见错误: 共享中断时如果 dev 参数为 NULL,内核将无法区分中断属于哪个设备,导致 free_irq() 失败或误处理。务必传入唯一的设备指针。

2.2.3 free_irq 函数原型

void free_irq(unsigned int irq, void *dev);

在驱动移除或错误回滚路径中调用。如果中断是共享的,dev 参数必须与 request_irq 时传入的值一致,否则内核无法正确释放。

2.2.4 典型注册流程示例

static irqreturn_t my_camera_irq_handler(int irq, void *dev_id)
{
    /* Top Half:快速处理,标记中断发生 */
    struct camera_dev *cam = (struct camera_dev *)dev_id;
    cam->irq_count++;
    /* 调度 Bottom Half */
    tasklet_schedule(&cam->dma_tasklet);
    return IRQ_HANDLED;
}

static int camera_probe(struct platform_device *pdev)
{
    struct camera_dev *cam;
    int irq, ret;

    cam = devm_kzalloc(&pdev->dev, sizeof(*cam), GFP_KERNEL);
    irq = platform_get_irq(pdev, 0);
    if (irq < 0)
        return irq;

    ret = request_irq(irq, my_camera_irq_handler,
                      IRQF_TRIGGER_RISING | IRQF_SHARED,
                      "v4l2_camera", cam);
    if (ret) {
        dev_err(&pdev->dev, "request_irq failed: %d\n", ret);
        return ret;
    }

    cam->irq = irq;
    platform_set_drvdata(pdev, cam);
    return 0;
}

static int camera_remove(struct platform_device *pdev)
{
    struct camera_dev *cam = platform_get_drvdata(pdev);
    free_irq(cam->irq, cam);
    return 0;
}

2.3 中断处理函数:Top Half

Top Half 是中断处理的上半部,在中断上下文(原子上下文)中执行。它的设计原则是:快进快出。

2.3.1 Top Half 的约束

  • 不可睡眠(不能调用 msleep()、wait_event()、mutex_lock())。
  • 不可执行长时间操作(如大量数据拷贝、复杂计算)。
  • 不可访问用户空间内存。
  • 执行期间当前 CPU 的中断被屏蔽(取决于中断控制器配置)。

2.3.2 返回值

返回值 含义
IRQ_NONE 中断不是本设备产生的,用于共享中断检测
IRQ_HANDLED 中断已被正确处理
IRQ_WAKE_THREAD 唤醒中断线程化处理函数(配合 request_threaded_irq)

2.3.3 Top Half 典型职责

  1. 读取硬件状态寄存器,确认中断源。
  2. 清除中断挂起标志(避免重复触发)。
  3. 保存关键时间戳或 DMA 完成状态。
  4. 调度 Bottom Half(Tasklet / Workqueue / 中断线程)。
Top Half 执行流程 硬件中断触发 读取中断状态寄存器 清除中断挂起标志 调度 Bottom Half(tasklet_schedule)

2.4 Bottom Half:Tasklet 机制

Tasklet 是 Linux 内核中最常用的 Bottom Half 实现机制之一。它在软中断上下文中执行,允许延迟处理 Top Half 中不适合完成的工作。

2.4.1 Tasklet 的特点

  • 在软中断上下文执行,可以睡眠吗?不可以,仍然不能调用阻塞函数。
  • 同一 Tasklet 在任意时刻只能在一个 CPU 上执行,但不同 Tasklet 可以并行。
  • Tasklet 执行期间,硬件中断仍然可以触发(除非显式屏蔽)。
  • Tasklet 是 串行化 的:同一个 Tasklet 不会在多个 CPU 上同时运行。

2.4.2 Tasklet 生命周期

阶段 API / 操作 说明
声明 DECLARE_TASKLET(name, func, data) 静态声明,默认使能
初始化 tasklet_init(&t, func, data) 动态初始化
调度 tasklet_schedule(&t) 将 Tasklet 加入调度队列,触发执行
执行 内核在软中断处理中调用 func 不可预测的延迟,但通常很快
禁用 tasklet_disable(&t) 同步等待当前执行完成,然后禁止
启用 tasklet_enable(&t) 恢复调度能力
销毁 tasklet_kill(&t) 等待 Tasklet 完成并释放资源

2.4.3 V4L2 驱动中的 Tasklet 典型用法

/* 在 camera_dev 结构体中定义 tasklet */
struct camera_dev {
    struct tasklet_struct dma_tasklet;
    /* 其他成员 */
};

/* Bottom Half 处理函数 */
static void camera_dma_tasklet_handler(unsigned long data)
{
    struct camera_dev *cam = (struct camera_dev *)data;
    /* 处理 DMA 完成事件:拷贝数据、唤醒等待队列、更新帧状态 */
    cam->frame_ready = 1;
    wake_up_interruptible(&cam->wait_queue);
}

/* 在 probe 中初始化 */
tasklet_init(&cam->dma_tasklet, camera_dma_tasklet_handler,
             (unsigned long)cam);

/* 在 Top Half 中调度 */
static irqreturn_t my_camera_irq_handler(int irq, void *dev_id)
{
    struct camera_dev *cam = (struct camera_dev *)dev_id;
    /* 快速处理硬件寄存器 */
    writel(0x0, cam->base + IRQ_CLEAR_REG);
    /* 调度 Bottom Half */
    tasklet_schedule(&cam->dma_tasklet);
    return IRQ_HANDLED;
}

/* 在 remove 中销毁 */
tasklet_kill(&cam->dma_tasklet);

2.5 Top Half 与 Bottom Half 的分工策略

合理划分 Top Half 和 Bottom Half 的职责,是中断处理性能优化的关键。以下表格总结了典型的分工原则:

操作类型 Top Half Bottom Half(Tasklet)
读取/清除中断状态寄存器 ✅ 必须 ❌ 不推荐
保存时间戳 ✅ 推荐 ❌ 延迟可能导致误差
DMA 描述符处理 ⚠️ 仅更新指针 ✅ 完整处理
数据拷贝(memcpy) ❌ 禁止(耗时) ✅ 适合
唤醒等待队列 ⚠️ 简单唤醒 ✅ 更安全
调用 V4L2 帧完成回调 ❌ 禁止 ✅ 必须
获取/释放锁(spinlock) ✅ spin_lock ✅ spin_lock / mutex
经验法则: 如果 Top Half 的执行时间超过 50 微秒,就应该考虑将部分工作移到 Bottom Half。在 V4L2 摄像头驱动中,DMA 完成中断的 Top Half 通常只做三件事:读状态、清标志、调度 Tasklet。

2.6 中断处理流程全景图

以下 SVG 展示了从硬件中断触发到驱动 Bottom Half 完成的完整流程:

V4L2 驱动中断处理完整流程 硬件中断触发 通用中断层分发 Top Half 执行 tasklet_schedule() 软中断处理 Tasklet Bottom Half 处理 Top Half 职责(原子上下文): 1. 读取中断状态寄存器 2. 清除中断挂起标志 3. 保存必要硬件状态 4. 调度 Bottom Half Bottom Half 职责(软中断上下文): 1. 处理 DMA 描述符链 2. 拷贝帧数据到缓冲区 3. 更新 V4L2 帧状态 4. 唤醒等待队列 / 触发 DQBUF 5. 调用 vb2_buffer_done()

2.7 中断处理中的注意事项与调试技巧

2.7.1 常见陷阱

  • 中断丢失: 如果在 Top Half 中未及时清除中断标志,同一中断源可能无法再次触发。
  • Tasklet 重入: 虽然同一 Tasklet 不会并行执行,但如果连续多次调度,只会执行一次。需要确保每次中断都有对应的处理。
  • 共享中断误判: 在共享中断中,必须检查硬件状态确认中断是否属于本设备,否则返回 IRQ_NONE。
  • 内存屏障: 在 DMA 和 CPU 共享内存时,需要使用 wmb() / rmb() 保证数据一致性。

2.7.2 调试手段

方法 命令 / 工具 作用
查看中断统计 cat /proc/interrupts 显示每个 CPU 上各中断号的触发次数
查看中断亲和性 cat /proc/irq/{irq}/smp_affinity 显示中断可以路由到哪些 CPU
动态追踪 trace-cmd record -e irq:* 记录所有中断相关事件
ftrace 函数追踪 echo function > /sys/kernel/debug/tracing/current_tracer 追踪中断处理函数的调用栈
延迟测量 cyclictest / ftrace 测量中断响应延迟
调试案例: 如果 /proc/interrupts 中某个中断号在对应设备操作时计数不增长,说明中断未正确触发或注册失败。检查设备树中断配置、硬件连接和 request_irq 返回值。

2.8 本章小结

  • Linux 中断子系统采用分层架构,V4L2 驱动通过 request_irq / free_irq 注册和注销中断处理函数。
  • Top Half 在原子上下文中执行,必须快速完成,只做必要的硬件操作和 Bottom Half 调度。
  • Tasklet 是常用的 Bottom Half 机制,在软中断上下文中执行,适合处理数据拷贝、帧状态更新等操作。
  • 合理划分 Top Half 和 Bottom Half 的职责,是保证中断响应实时性和系统吞吐量的基础。
  • 通过 /proc/interrupts 和 ftrace 等工具可以有效调试中断处理流程。