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 中断上半部处理流程:
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 驱动的中断上半部不会成为系统实时性的瓶颈,为下半部的复杂处理提供干净、可靠的硬件状态快照。