3. 中断处理函数(ISR)设计原则:上半部与下半部分离、中断上下文限制、最小化临界区

在 V4L2 摄像头驱动中,中断处理函数(ISR)的设计直接决定了帧采集的实时性与系统整体吞吐量。摄像头传感器通常以行或帧为单位触发中断,若 ISR 处理时间过长,轻则导致帧丢失(FIFO 溢出),重则引发系统调度延迟甚至崩溃。本章围绕三条核心原则展开:上半部与下半部分离、中断上下文限制、最小化临界区。

核心观点: ISR 不是“完成所有工作”的地方,而是“记录发生了什么、尽快唤醒下半部”的地方。在 V4L2 驱动中,上半部通常只做:读取状态寄存器、清除中断标志、将缓冲区入队、调度 tasklet/workqueue;而帧数据处理、协议解析、用户空间通知等耗时操作必须推迟到下半部。

3.1 上半部与下半部分离

Linux 内核将中断处理拆分为两个阶段:上半部(Top Half)在关闭当前中断线(或所有中断)的上下文中执行,要求极短且不可阻塞;下半部(Bottom Half)在开启中断的上下文中执行,允许延迟、调度和阻塞。V4L2 摄像头驱动中典型的分离策略如下表所示:

阶段 执行上下文 典型操作 耗时要求
上半部(ISR) 中断上下文(不可睡眠) 读取状态寄存器、清除中断标志、记录时间戳、调度下半部 < 10 µs(推荐)
下半部(Tasklet/Workqueue) 软中断或进程上下文(可睡眠) DMA 描述符处理、帧数据拷贝、V4L2 buffer 完成通知、统计更新 < 1 ms(推荐)
实践建议: 对于高帧率摄像头(如 60 fps 以上),优先使用 tasklet 作为下半部机制,因为它运行在软中断上下文,延迟更低;若下半部需要执行阻塞操作(如 I2C 读取传感器寄存器),则必须使用 workqueue。

下面是一个典型的 V4L2 摄像头 ISR 上半部代码片段,展示了最小化操作原则:

static irqreturn_t camera_isr(int irq, void *dev_id)
{
    struct camera_device *cam = dev_id;
    u32 status;

    /* 1. 读取中断状态寄存器 */
    status = readl(cam->regs + CAM_IRQ_STATUS);

    /* 2. 清除中断标志(必须优先,防止重复触发) */
    writel(status, cam->regs + CAM_IRQ_CLEAR);

    /* 3. 记录硬件时间戳(用于帧同步) */
    cam->frame_timestamp = ktime_get();

    /* 4. 检查是否为帧完成中断 */
    if (status & CAM_IRQ_FRAME_DONE) {
        /* 将当前硬件缓冲区入队到下半部处理列表 */
        list_add_tail(&cam->done_list, &cam->pending_list);
        /* 调度 tasklet 处理下半部 */
        tasklet_schedule(&cam->frame_tasklet);
    }

    /* 5. 返回 IRQ_HANDLED 表示已处理 */
    return IRQ_HANDLED;
}

上述 ISR 中没有任何循环、内存分配、锁等待或 I/O 操作,所有耗时逻辑都推迟到了 frame_tasklet 中。这种分离保证了中断响应时间在微秒级。

上半部与下半部分离流程图 上半部与下半部分离流程(V4L2 摄像头中断) 硬件中断触发 上半部(ISR) 读状态 → 清标志 → 记录时间戳 → 调度下半部 下半部(Tasklet / Workqueue) DMA 处理 → 帧拷贝 → V4L2 buffer 完成 → 唤醒用户 用户空间 dequeue 中断上下文 不可睡眠 / 不可阻塞 软中断 / 进程上下文 可睡眠 / 可加锁

3.2 中断上下文限制

中断上下文(Interrupt Context)是 Linux 内核中执行环境最严格的区域。在 V4L2 摄像头驱动的 ISR 中,必须遵守以下限制:

  • 禁止睡眠: 不能调用 msleep()、wait_event()、mutex_lock() 等可能引起调度的函数。
  • 禁止页面错误: 不能访问用户空间内存(如 copy_to_user()),因为缺页处理可能睡眠。
  • 禁止长时间循环: 避免遍历大量链表或进行复杂计算,防止阻塞其他中断。
  • 禁止使用 printk() 频繁输出: 调试时可临时使用,但生产代码中必须移除或使用 trace_printk()。
  • 禁止动态内存分配(GFP_KERNEL): 必须使用 GFP_ATOMIC 或预分配内存池。
常见陷阱: 在 ISR 中调用 i2c_transfer() 或 regmap_read() 时,如果底层总线实现依赖 mutex 或 wait,将导致内核崩溃(BUG: scheduling while atomic)。务必确认所用 API 在中断上下文中是安全的,或使用 regmap_read_poll_timeout_atomic() 等原子变体。

下表总结了中断上下文与进程上下文的典型差异:

特性 中断上下文 进程上下文
可睡眠 ❌ 否 ✅ 是
可访问用户空间 ❌ 否 ✅ 是
可持有 mutex ❌ 否 ✅ 是
可持有 spinlock ✅ 是(需 irqsave 变体) ✅ 是
调度延迟要求 < 10 µs < 1 ms(软实时)
典型下半部机制 tasklet / threaded IRQ workqueue / kthread
中断上下文限制示意图 中断上下文限制:允许与禁止操作 ❌ 禁止操作 • msleep() / usleep_range() • mutex_lock() / down_interruptible() • copy_to_user() / copy_from_user() • kmalloc(GFP_KERNEL) • printk() 频繁调用 • 长时间循环(> 100 µs) ✅ 允许操作 • readl() / writel() 寄存器访问 • spin_lock_irqsave() / spin_unlock_irqrestore() • ktime_get() / do_gettimeofday() • tasklet_schedule() / workqueue 调度 • kmalloc(GFP_ATOMIC)(短小分配) • 原子操作 / 位操作

3.3 最小化临界区

临界区(Critical Section)是指必须互斥访问的共享数据区域。在 V4L2 摄像头驱动中,ISR 与下半部、ISR 与进程上下文之间可能竞争访问以下资源:

  • 硬件描述符队列: DMA 描述符的入队与出队。
  • 缓冲区链表: done_list、pending_list、irq_list 等。
  • 状态标志: streaming、buffer_ready 等。
  • 寄存器映射: 部分控制寄存器可能被多个上下文访问。

最小化临界区的核心策略包括:

  1. 使用无锁数据结构: 如 kfifo(无锁 FIFO)、atomic_t、READ_ONCE/WRITE_ONCE。
  2. 将锁粒度降到最低: 只保护真正共享的变量,避免大锁包裹整个处理流程。
  3. 在 ISR 中使用 spinlock 的 irqsave 变体: 确保关闭本地中断,防止死锁。
  4. 将数据生产与消费解耦: ISR 只负责“放数据”,下半部负责“取数据”,通过无锁队列衔接。
性能数据: 在某 1080p@30fps 摄像头驱动中,将临界区从 15 µs 缩小到 3 µs(通过使用 kfifo 替代链表+spinlock),帧丢失率从 2.3% 降至 0.1% 以下,CPU 占用率降低 12%。

下面是一个使用 kfifo 实现 ISR 与下半部无锁通信的示例:

/* 驱动初始化时分配 kfifo */
#define FIFO_SIZE 64
DECLARE_KFIFO(cam_fifo, struct frame_desc, FIFO_SIZE);

/* ISR 上半部:只将描述符写入 kfifo */
static irqreturn_t camera_isr(int irq, void *dev_id)
{
    struct camera_device *cam = dev_id;
    struct frame_desc desc;

    desc.dma_addr = readl(cam->regs + CAM_DMA_ADDR);
    desc.timestamp = ktime_get();
    desc.status = readl(cam->regs + CAM_FRAME_STATUS);

    /* kfifo_in 在无锁路径下使用,仅需 atomic 操作 */
    if (!kfifo_in(&cam->cam_fifo, &desc, 1)) {
        /* FIFO 满,丢弃帧(统计溢出) */
        cam->overflow_count++;
        return IRQ_HANDLED;
    }

    tasklet_schedule(&cam->frame_tasklet);
    return IRQ_HANDLED;
}

/* 下半部 tasklet:从 kfifo 取出描述符并处理 */
static void frame_tasklet_handler(unsigned long data)
{
    struct camera_device *cam = (struct camera_device *)data;
    struct frame_desc desc;

    while (kfifo_out(&cam->cam_fifo, &desc, 1) == 1) {
        /* 处理帧:DMA 同步、V4L2 buffer 完成 */
        process_frame(cam, &desc);
    }
}

上述设计中,ISR 与 tasklet 之间通过 kfifo 传递数据,无需任何显式锁。kfifo 内部使用 __kfifo_put 和 __kfifo_get 的原子递增指针操作,天然支持单生产者/单消费者场景,临界区缩小到几乎为零。

最小化临界区架构图 最小化临界区:无锁 kfifo 架构 硬件中断 传感器帧完成 ISR 上半部 kfifo_in(无锁写入) kfifo 无锁队列 原子指针递增 / 无锁 下半部 Tasklet kfifo_out(无锁读取) 帧处理 & V4L2 通知 临界区范围:仅 kfifo 内部原子指针操作(约 2-3 条指令),无 spinlock、无内存屏障、无上下文切换 适用于单生产者(ISR) + 单消费者(Tasklet)场景,多核环境下需额外考虑内存屏障

3.4 本章小结

V4L2 摄像头驱动中断处理优化的三大原则相互关联:

  • 上半部与下半部分离是基础架构,决定了实时性与吞吐量的平衡。
  • 中断上下文限制是安全红线,违反将导致内核崩溃或数据损坏。
  • 最小化临界区是性能关键,直接影响帧率稳定性和 CPU 效率。