4. 中断上半部设计原则:最小化执行时间、禁止睡眠、快速记录硬件状态

在 V4L2 驱动中,中断上半部(Top Half)是硬件中断到来后最先执行的上下文。它运行在中断上下文(IRQ context)中,具有最高的抢占优先级。设计不当的上半部会直接导致帧丢失、系统响应延迟飙升,甚至触发内核实时性警告。本节围绕三条核心原则展开:最小化执行时间、禁止睡眠、快速记录硬件状态。

4.1 原则一:最小化执行时间

中断上半部执行期间,当前 CPU 核上的所有其他进程(包括实时进程)都被阻塞。执行时间越长,系统的调度延迟越大。对于 V4L2 设备,典型的上半部耗时来源包括:

  • 寄存器轮询等待:等待硬件状态位翻转。
  • 大量数据搬运:在中断中直接做 DMA 缓冲区拷贝。
  • 复杂条件分支:遍历链表或执行耗时算法。

业界推荐的上半部执行时间上限为 10–50 µs。超过此阈值,必须将耗时操作推迟到下半部(Bottom Half)或工作队列中执行。

经验法则: 上半部只做“读状态、清中断、记录最小必要信息”三件事。任何涉及循环、等待、分配内存的操作都应移至下半部。

4.2 原则二:禁止睡眠

中断上下文不允许调用任何可能引起睡眠的函数,包括但不限于:

  • kmalloc(GFP_KERNEL) — 可能触发页面回收而睡眠。
  • mutex_lock() — 互斥锁可能引起调度。
  • copy_to_user() / copy_from_user() — 用户空间访问可能缺页。
  • msleep() / usleep_range() — 显式睡眠。

违反此原则会导致内核报出 BUG: sleeping function called from invalid context,严重时造成系统死锁或崩溃。

函数/操作 中断上下文安全性 替代方案
kmalloc(GFP_ATOMIC) 安全 使用 GFP_ATOMIC 标志
spin_lock() 安全 不可用 mutex_lock()
wake_up() 安全 唤醒等待队列
tasklet_schedule() 安全 触发软中断下半部
mutex_lock() 不安全 改用 spin_lock()
copy_to_user() 不安全 推迟到进程上下文
常见陷阱: 在中断上半部中调用 printk() 虽然不会睡眠,但会极大增加执行时间(串口输出可能耗时数百微秒)。调试时请使用 trace_printk() 或仅在条件编译下启用。

4.3 原则三:快速记录硬件状态

上半部的核心职责是“快照”硬件状态,然后立即清除中断标志,使硬件能够继续产生下一帧中断。记录的内容应精简到最小集合:

  • 中断状态寄存器:判断中断类型(帧完成、错误、溢出)。
  • 当前帧序号或时间戳:用于后续帧同步。
  • 错误标志位:如 FIFO 溢出、CRC 错误。

这些信息通常被写入一个预分配的、位于缓存行对齐的 状态结构体 中,下半部通过读取该结构体进行后续处理。

4.4 上半部典型设计流程

以下 SVG 图展示了一个符合三条原则的 V4L2 中断上半部处理流程:

V4L2 中断上半部设计流程 V4L2 中断上半部设计流程 硬件中断到达 读取中断状态寄存器 判断中断类型 帧完成 → 记录时间戳 错误/溢出 → 置错误标志 清除中断标志(写寄存器) 触发下半部(tasklet/workqueue) 上半部结束 设计要点 • 无睡眠调用 • 无内存分配 • 无用户空间访问 • 执行时间 < 50 µs

4.5 上半部代码模板

以下是一个符合三条原则的 V4L2 中断上半部处理函数模板:

static irqreturn_t sensor_irq_handler(int irq, void *dev_id)
{
    struct sensor_dev *sensor = (struct sensor_dev *)dev_id;
    u32 status;
    
    /* 1. 读取中断状态寄存器(最小化寄存器访问) */
    status = ioread32(sensor->base + REG_IRQ_STATUS);
    
    /* 2. 快速判断中断类型 */
    if (status & IRQ_FRAME_DONE) {
        /* 记录帧完成时间戳(使用 ktime_get() 而非 do_gettimeofday()) */
        sensor->frame_ts = ktime_get();
        sensor->frame_count++;
    }
    
    if (status & IRQ_FIFO_OVERFLOW) {
        /* 仅置位错误标志,不在此处做复杂处理 */
        sensor->error_flags |= ERR_FIFO_OVERFLOW;
    }
    
    /* 3. 清除中断标志(必须立即执行,使硬件能产生下一中断) */
    iowrite32(status, sensor->base + REG_IRQ_CLEAR);
    
    /* 4. 触发下半部处理 */
    tasklet_schedule(&sensor->dma_tasklet);
    
    return IRQ_HANDLED;
}
性能提示: 使用 ioread32()/iowrite32() 而非 readl()/writel() 可确保内存屏障的正确插入。对于实时性要求极高的系统,可考虑使用 __raw_readl() 配合显式屏障以进一步减少开销。

4.6 三条原则的权衡与检查清单

在实际编码中,三条原则有时会相互制约。例如,为了“快速记录硬件状态”,可能需要读取多个寄存器,但这又增加了执行时间。以下检查清单可帮助设计者做出合理权衡:

检查项 原则 通过标准
上半部中是否存在循环等待硬件状态? 最小化执行时间 无轮询循环;使用硬件超时或状态机
是否调用了 kmalloc(GFP_KERNEL)? 禁止睡眠 使用 GFP_ATOMIC 或预分配缓冲区
是否在中断中更新了用户空间可见的数据? 快速记录硬件状态 仅更新内核态状态结构体
中断标志清除是否在函数末尾之前执行? 快速记录硬件状态 清除操作位于所有读取之后、触发下半部之前
上半部执行时间是否超过 50 µs? 最小化执行时间 使用 ftrace 或 tracepoint 测量

遵循以上三条原则,可以确保 V4L2 驱动的中断上半部不会成为系统实时性的瓶颈,为下半部的复杂处理提供干净、可靠的硬件状态快照。