4、中断处理基础:Linux中断子系统、中断请求(IRQ)、中断处理函数(ISR)、底半部机制(tasklet/workqueue)
在V4L2驱动与I2C时钟同步的协同调试中,中断处理是连接硬件事件与软件响应的核心纽带。本章深入剖析Linux中断子系统的分层架构,详细讲解中断请求(IRQ)的注册与触发流程、中断处理函数(ISR)的编写规范,以及底半部机制(tasklet与workqueue)的设计原则与适用场景。
4.1 Linux中断子系统架构
Linux中断子系统采用分层设计,将硬件中断从产生到软件处理的完整路径划分为三个层次:硬件中断控制器层、通用中断处理层(Generic IRQ)以及设备驱动层。这种分层架构使得驱动开发者无需关心底层中断控制器的具体实现,只需通过标准API完成中断注册与处理。
4.2 中断请求(IRQ)
中断请求是硬件设备向CPU发送异步事件信号的基本机制。在Linux内核中,每个中断源对应一个唯一的IRQ号,驱动通过该IRQ号完成中断的注册、使能与屏蔽操作。
4.2.1 IRQ号获取方式
| 获取方式 | 适用场景 | API/方法 |
|---|---|---|
| 设备树解析 | 嵌入式平台(ARM、RISC-V等) | irq_of_parse_and_map() |
| ACPI解析 | x86/ARM64 ACPI平台 | acpi_register_gsi() |
| 平台数据传递 | 旧式板级代码 | platform_get_irq() |
| PCI中断 | PCI/PCIe设备 | pci_irq_vector() |
4.2.2 中断注册API
/* 标准中断注册函数 */
int request_irq(unsigned int irq,
irq_handler_t handler,
unsigned long flags,
const char *name,
void *dev);
/* 设备管理版中断注册(推荐) */
int devm_request_irq(struct device *dev,
unsigned int irq,
irq_handler_t handler,
unsigned long flags,
const char *name,
void *dev_id);
devm_request_irq(),它会在设备移除时自动释放中断资源,避免资源泄漏导致的调试干扰。
4.2.3 中断标志位(flags)
| 标志位 | 含义 | 典型使用场景 |
|---|---|---|
IRQF_TRIGGER_RISING |
上升沿触发 | GPIO中断、I2C时钟同步信号 |
IRQF_TRIGGER_FALLING |
下降沿触发 | 帧同步信号(VSYNC) |
IRQF_TRIGGER_HIGH |
高电平触发 | 传感器就绪信号 |
IRQF_SHARED |
共享中断 | 多个设备共用同一中断线 |
IRQF_ONESHOT |
中断线单次触发 | 中断线程化场景 |
4.3 中断处理函数(ISR)
中断处理函数(Interrupt Service Routine)是响应硬件中断时立即执行的代码。ISR运行在中断上下文中,必须遵循严格的约束条件:快速执行、不可睡眠、不可调用可能引起阻塞的函数。
4.3.1 ISR函数原型与返回值
/* ISR函数原型 */
irqreturn_t sensor_isr(int irq, void *dev_id)
{
struct sensor_device *sensor = (struct sensor_device *)dev_id;
/* 读取中断状态寄存器 */
u32 status = i2c_smbus_read_byte(sensor->client);
if (status & SENSOR_INT_VSYNC) {
/* 帧同步中断处理 */
sensor->vsync_count++;
/* 调度底半部处理 */
tasklet_schedule(&sensor->vsync_tasklet);
}
/* 返回值:IRQ_HANDLED 或 IRQ_NONE */
return IRQ_HANDLED;
}
/* 返回值定义 */
enum irqreturn {
IRQ_NONE = (0 << 0), /* 中断不属于本设备 */
IRQ_HANDLED = (1 << 0), /* 中断已处理 */
IRQ_WAKE_THREAD = (1 << 1), /* 唤醒中断线程 */
};
4.3.2 ISR编写黄金法则
- 快速执行:ISR应尽可能短,通常只做状态读取、标志置位和底半部调度。
- 不可睡眠:中断上下文中禁止调用
msleep()、wait_event()等阻塞函数。 - 不可使用信号量:信号量可能引起睡眠,应使用自旋锁(spinlock)保护共享数据。
- 不可访问用户空间:中断上下文与用户进程无关联,无法使用
copy_to_user()。 - 不可调用可能导致调度的函数:如
kmalloc(GFP_KERNEL),应使用GFP_ATOMIC。
i2c_smbus_read_byte()等I2C传输函数。I2C传输可能因总线忙而阻塞,导致系统响应延迟甚至死锁。正确的做法是在ISR中仅记录中断状态,将I2C操作推迟到底半部或中断线程中执行。
4.4 底半部机制
底半部(Bottom Half)机制用于将中断处理中耗时或可能阻塞的操作从ISR中分离出来,在更宽松的上下文中执行。Linux内核提供两种主要的底半部机制:tasklet和workqueue。
4.4.1 tasklet机制
tasklet运行在软中断上下文(SoftIRQ),基于原子上下文执行,不可睡眠。tasklet适合执行时间短、不需要睡眠的底半部任务。
/* tasklet定义与使用 */
#include <linux/interrupt.h>
/* 定义tasklet处理函数 */
void vsync_tasklet_handler(unsigned long data)
{
struct sensor_device *sensor = (struct sensor_device *)data;
/* 执行I2C读取操作(注意:仍不可睡眠) */
u32 frame_info = i2c_smbus_read_word_data(sensor->client,
REG_FRAME_INFO);
/* 更新帧信息 */
sensor->frame_width = (frame_info >> 16) & 0xFFFF;
sensor->frame_height = frame_info & 0xFFFF;
/* 唤醒等待队列 */
wake_up_interruptible(&sensor->frame_wait);
}
/* 声明并初始化tasklet */
DECLARE_TASKLET(vsync_tasklet, vsync_tasklet_handler,
(unsigned long)sensor);
/* 在ISR中调度tasklet */
tasklet_schedule(&sensor->vsync_tasklet);
4.4.2 workqueue机制
workqueue运行在进程上下文,可以睡眠,适合执行需要等待I/O、获取信号量或执行长时间操作的底半部任务。
/* workqueue定义与使用 */
#include <linux/workqueue.h>
/* 定义工作队列处理函数 */
void i2c_sync_work_handler(struct work_struct *work)
{
struct sensor_device *sensor = container_of(work,
struct sensor_device,
sync_work);
/* 执行I2C时钟同步操作(可以睡眠) */
mutex_lock(&sensor->sync_lock);
/* 发送I2C时钟同步命令 */
i2c_smbus_write_byte_data(sensor->client,
REG_SYNC_CMD, SYNC_START);
msleep(10); /* 等待同步完成 */
i2c_smbus_read_byte_data(sensor->client, REG_SYNC_STATUS);
mutex_unlock(&sensor->sync_lock);
}
/* 初始化工作队列 */
INIT_WORK(&sensor->sync_work, i2c_sync_work_handler);
/* 在ISR中调度工作队列 */
schedule_work(&sensor->sync_work);
4.4.3 tasklet vs workqueue 对比
| 对比维度 | tasklet | workqueue |
|---|---|---|
| 执行上下文 | 软中断(原子上下文) | 进程上下文(可睡眠) |
| 是否可睡眠 | 否 | 是 |
| 是否可获取信号量 | 否 | 是 |
| 执行时间要求 | 极短(微秒级) | 可较长(毫秒级甚至更久) |
| 并发性 | 同一tasklet串行执行 | 可并行执行(多worker线程) |
| 适用场景 | 状态更新、标志置位、唤醒等待队列 | I2C/SPI传输、硬件配置、内存分配 |
| 调度API | tasklet_schedule() |
schedule_work() |
4.5 中断线程化(Threaded IRQ)
中断线程化是Linux内核提供的一种将ISR自动运行在进程上下文中的机制。通过request_threaded_irq()注册中断,内核会为中断处理创建一个内核线程,ISR在进程上下文中执行,可以自然地使用睡眠操作。
/* 中断线程化注册 */
int request_threaded_irq(unsigned int irq,
irq_handler_t handler, /* 上半部,可为NULL */
irq_handler_t thread_fn, /* 线程化处理函数 */
unsigned long flags,
const char *name,
void *dev);
/* 示例:V4L2传感器中断线程化 */
static irqreturn_t sensor_irq_top(int irq, void *dev_id)
{
/* 上半部:仅返回IRQ_WAKE_THREAD唤醒线程 */
return IRQ_WAKE_THREAD;
}
static irqreturn_t sensor_irq_thread(int irq, void *dev_id)
{
struct sensor_device *sensor = dev_id;
/* 线程化处理:可以安全执行I2C操作 */
mutex_lock(&sensor->i2c_lock);
u32 status = i2c_smbus_read_word_data(sensor->client,
REG_INT_STATUS);
if (status & INT_VSYNC) {
sensor->vsync_count++;
complete(&sensor->frame_complete);
}
mutex_unlock(&sensor->i2c_lock);
return IRQ_HANDLED;
}
/* 注册中断 */
ret = request_threaded_irq(sensor->irq,
sensor_irq_top, /* 上半部 */
sensor_irq_thread, /* 线程化处理 */
IRQF_TRIGGER_RISING | IRQF_ONESHOT,
"sensor_irq", sensor);
4.6 中断调试与排查
在V4L2驱动与I2C时钟同步的协同调试中,中断相关问题是常见的故障点。以下列出典型的中断调试手段和排查清单。
4.6.1 中断调试接口
| 调试接口 | 路径/命令 | 查看内容 |
|---|---|---|
| procfs中断统计 | /proc/interrupts |
每个IRQ的中断次数、CPU亲和性 |
| procfs中断信息 | /proc/irq/{irq}/ |
中断名称、触发类型、驱动信息 |
| sysfs中断控制 | /sys/class/irq/ |
中断使能/屏蔽、亲和性设置 |
| 动态追踪 | tracepoint:irq_handler_entry/exit |
ISR进入/退出时间戳、执行耗时 |
| ftrace | echo function > current_tracer |
跟踪中断处理函数调用栈 |
4.6.2 中断调试排查清单
- 中断未触发:检查设备树中断配置是否正确,使用
cat /proc/interrupts确认中断号是否注册。 - 中断风暴:ISR中未清除中断状态寄存器导致重复触发,使用ftrace查看中断频率。
- ISR执行超时:在ISR中执行了I2C传输或
msleep(),使用tracepoint测量ISR执行时间。 - 底半部未执行:检查
tasklet_schedule()或schedule_work()是否被正确调用,确认底半部函数指针有效。 - 共享中断冲突:多个设备共享同一IRQ时,ISR必须正确判断中断源,返回
IRQ_NONE避免误处理。 - 中断线程饿死:高优先级中断频繁触发导致中断线程无法获得CPU,检查中断优先级和亲和性设置。
video_ioctl2()、vb2_buffer_done()等V4L2核心API是严格禁止的。这些函数可能触发内存分配、文件系统操作或用户空间通知,必须在进程上下文中执行。正确的做法是在底半部或中断线程中通过工作队列间接调用V4L2 API。