4、中断处理基础:Linux中断子系统、中断请求(IRQ)、中断处理函数(ISR)、底半部机制(tasklet/workqueue)

在V4L2驱动与I2C时钟同步的协同调试中,中断处理是连接硬件事件与软件响应的核心纽带。本章深入剖析Linux中断子系统的分层架构,详细讲解中断请求(IRQ)的注册与触发流程、中断处理函数(ISR)的编写规范,以及底半部机制(tasklet与workqueue)的设计原则与适用场景。

4.1 Linux中断子系统架构

Linux中断子系统采用分层设计,将硬件中断从产生到软件处理的完整路径划分为三个层次:硬件中断控制器层、通用中断处理层(Generic IRQ)以及设备驱动层。这种分层架构使得驱动开发者无需关心底层中断控制器的具体实现,只需通过标准API完成中断注册与处理。

核心概念:中断子系统通过irq_domain将硬件中断号映射为Linux内核的IRQ号,驱动层使用IRQ号进行中断请求与释放操作。
Linux中断子系统分层架构 硬件中断控制器层 GIC(ARM)、APIC(x86)、龙芯INTC、RISC-V PLIC 通用中断处理层(Generic IRQ) irq_domain映射 | IRQ号管理 | 中断流控 | 中断线程化 设备驱动层 request_irq() | devm_request_irq() | ISR注册 | 底半部调度 驱动仅需关注设备驱动层

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);
调试建议:在V4L2驱动调试初期,建议使用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。
常见错误:在ISR中直接调用i2c_smbus_read_byte()等I2C传输函数。I2C传输可能因总线忙而阻塞,导致系统响应延迟甚至死锁。正确的做法是在ISR中仅记录中断状态,将I2C操作推迟到底半部或中断线程中执行。
ISR执行流程与约束检查 硬件中断触发 ISR入口:读取状态寄存器 属于本设备? 是 否 记录状态 + 调度底半部 return IRQ_HANDLED return IRQ_NONE 约束:不可睡眠 | 不可使用信号量 | 不可访问用户空间 | 不可调用阻塞I2C传输 推荐操作:读取寄存器 → 置位标志 → tasklet_schedule() / schedule_work()

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()
V4L2驱动最佳实践:对于帧同步中断(VSYNC),建议使用tasklet快速更新帧计数并唤醒等待队列;对于I2C时钟同步配置、传感器寄存器批量读写等操作,应使用workqueue确保可以安全地执行I2C传输和睡眠等待。
中断处理完整流程:ISR + 底半部调度 硬件中断触发 上半部(ISR):中断上下文 读取状态 → 置位标志 → 调度底半部 底半部:tasklet 软中断上下文(原子) 快速更新帧计数 唤醒等待队列 底半部:workqueue 进程上下文(可睡眠) I2C时钟同步配置 传感器寄存器批量读写 返回进程上下文 / 用户空间 关键原则:ISR只做最少必要操作,耗时任务下沉到底半部

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);
调试技巧:在V4L2驱动调试阶段,优先使用中断线程化可以简化底半部设计,避免因tasklet中误用睡眠函数导致的系统崩溃。生产环境中再根据性能需求决定是否切换为tasklet+workqueue的组合方案。

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,检查中断优先级和亲和性设置。
V4L2调试红线:在ISR中直接调用video_ioctl2()、vb2_buffer_done()等V4L2核心API是严格禁止的。这些函数可能触发内存分配、文件系统操作或用户空间通知,必须在进程上下文中执行。正确的做法是在底半部或中断线程中通过工作队列间接调用V4L2 API。