3. 中断处理函数(ISR)设计原则:上半部与下半部分离、中断上下文限制、最小化临界区
在 V4L2 摄像头驱动中,中断处理函数(ISR)的设计直接决定了帧采集的实时性与系统整体吞吐量。摄像头传感器通常以行或帧为单位触发中断,若 ISR 处理时间过长,轻则导致帧丢失(FIFO 溢出),重则引发系统调度延迟甚至崩溃。本章围绕三条核心原则展开:上半部与下半部分离、中断上下文限制、最小化临界区。
3.1 上半部与下半部分离
Linux 内核将中断处理拆分为两个阶段:上半部(Top Half)在关闭当前中断线(或所有中断)的上下文中执行,要求极短且不可阻塞;下半部(Bottom Half)在开启中断的上下文中执行,允许延迟、调度和阻塞。V4L2 摄像头驱动中典型的分离策略如下表所示:
| 阶段 | 执行上下文 | 典型操作 | 耗时要求 |
|---|---|---|---|
| 上半部(ISR) | 中断上下文(不可睡眠) | 读取状态寄存器、清除中断标志、记录时间戳、调度下半部 | < 10 µs(推荐) |
| 下半部(Tasklet/Workqueue) | 软中断或进程上下文(可睡眠) | DMA 描述符处理、帧数据拷贝、V4L2 buffer 完成通知、统计更新 | < 1 ms(推荐) |
下面是一个典型的 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 中。这种分离保证了中断响应时间在微秒级。
3.2 中断上下文限制
中断上下文(Interrupt Context)是 Linux 内核中执行环境最严格的区域。在 V4L2 摄像头驱动的 ISR 中,必须遵守以下限制:
- 禁止睡眠: 不能调用
msleep()、wait_event()、mutex_lock()等可能引起调度的函数。 - 禁止页面错误: 不能访问用户空间内存(如
copy_to_user()),因为缺页处理可能睡眠。 - 禁止长时间循环: 避免遍历大量链表或进行复杂计算,防止阻塞其他中断。
- 禁止使用
printk()频繁输出: 调试时可临时使用,但生产代码中必须移除或使用trace_printk()。 - 禁止动态内存分配(GFP_KERNEL): 必须使用
GFP_ATOMIC或预分配内存池。
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 |
3.3 最小化临界区
临界区(Critical Section)是指必须互斥访问的共享数据区域。在 V4L2 摄像头驱动中,ISR 与下半部、ISR 与进程上下文之间可能竞争访问以下资源:
- 硬件描述符队列: DMA 描述符的入队与出队。
- 缓冲区链表:
done_list、pending_list、irq_list等。 - 状态标志:
streaming、buffer_ready等。 - 寄存器映射: 部分控制寄存器可能被多个上下文访问。
最小化临界区的核心策略包括:
- 使用无锁数据结构: 如
kfifo(无锁 FIFO)、atomic_t、READ_ONCE/WRITE_ONCE。 - 将锁粒度降到最低: 只保护真正共享的变量,避免大锁包裹整个处理流程。
- 在 ISR 中使用 spinlock 的 irqsave 变体: 确保关闭本地中断,防止死锁。
- 将数据生产与消费解耦: ISR 只负责“放数据”,下半部负责“取数据”,通过无锁队列衔接。
下面是一个使用 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 的原子递增指针操作,天然支持单生产者/单消费者场景,临界区缩小到几乎为零。
3.4 本章小结
V4L2 摄像头驱动中断处理优化的三大原则相互关联:
- 上半部与下半部分离是基础架构,决定了实时性与吞吐量的平衡。
- 中断上下文限制是安全红线,违反将导致内核崩溃或数据损坏。
- 最小化临界区是性能关键,直接影响帧率稳定性和 CPU 效率。