4. 中断上下文编程:原子操作与自旋锁、中断处理函数注册、中断标志

在蓝牙驱动中,中断处理程序(Interrupt Handler)运行于中断上下文,该上下文中不可睡眠、不可调度、不可使用可能引起阻塞的锁(如 mutex)。本章聚焦中断上下文编程的三个核心支柱:原子操作与自旋锁(保护共享数据)、中断处理函数注册(request_irq / devm_request_irq)、以及中断标志(IRQF_SHARED / IRQF_TRIGGER_*)。

核心原则:中断上下文是“快路径”,任何延迟或阻塞都会导致系统响应恶化。蓝牙 HCI UART 或 USB 中断处理中,数据搬运必须使用无睡眠锁,且处理时间应控制在微秒级。

4.1 原子操作与自旋锁:中断上下文同步基石

中断上下文与进程上下文、其他中断上下文之间可能并发访问共享数据结构(如蓝牙 HCI 接收队列、DMA 描述符环)。此时必须使用原子操作或自旋锁来保证数据一致性。

4.1.1 原子操作

原子操作由 CPU 指令级保证,适用于简单的整型计数器、位操作。在蓝牙驱动中常用于统计中断次数、标记 DMA 传输完成状态。

// 原子变量定义与操作
static atomic_t hci_irq_count = ATOMIC_INIT(0);

irqreturn_t bt_irq_handler(int irq, void *dev_id) {
    atomic_inc(&hci_irq_count);          // 原子递增,无需锁
    // ... 其他处理
    return IRQ_HANDLED;
}

// 进程上下文读取
unsigned long count = atomic_read(&hci_irq_count);

4.1.2 自旋锁

自旋锁在等待锁时忙等,不睡眠,因此适合中断上下文。但需注意:在中断上下文中使用自旋锁,必须关闭本地中断,否则可能死锁。

场景推荐锁变体说明
中断上下文 vs 进程上下文spin_lock_irqsave / spin_unlock_irqrestore保存并关闭本地中断,防止死锁
中断上下文 vs 中断上下文(同 CPU)spin_lock_irqsave / spin_unlock_irqrestore必须关闭中断,否则同一 CPU 上嵌套中断会自旋死锁
中断上下文 vs 中断上下文(不同 CPU)spin_lock仅需自旋,无需关中断(但通常仍用 irqsave 版本保持统一)
底半部(tasklet/softirq)spin_lock_bh / spin_unlock_bh关闭底半部,防止与进程上下文冲突
// 自旋锁保护蓝牙接收队列
static DEFINE_SPINLOCK(hci_rx_lock);
static struct sk_buff_head hci_rx_queue;

irqreturn_t bt_irq_handler(int irq, void *dev_id) {
    unsigned long flags;
    struct sk_buff *skb;

    // 必须使用 irqsave 变体
    spin_lock_irqsave(&hci_rx_lock, flags);
    skb = skb_dequeue(&hci_rx_queue);
    spin_unlock_irqrestore(&hci_rx_lock, flags);

    if (skb) {
        // 将数据传递给 HCI 核心层
        hci_recv_frame(hdev, skb);
    }
    return IRQ_HANDLED;
}
常见陷阱:在中断处理函数中调用 spin_lock(非 irqsave 版本)且该锁在进程上下文中也被使用,若进程持有锁时被中断打断,中断处理函数再次申请同一锁,则同一 CPU 上发生自旋死锁。务必使用 spin_lock_irqsave。

4.2 中断处理函数注册:request_irq 与 devm_request_irq

Linux 内核提供两种主要的中断注册接口:request_irq(传统)和 devm_request_irq(设备管理资源版本)。蓝牙驱动通常使用后者,以简化错误路径和移除操作。

4.2.1 request_irq 原型与参数

int request_irq(unsigned int irq,
                irq_handler_t handler,
                unsigned long flags,
                const char *name,
                void *dev);
参数说明
irq中断号,可通过 platform_get_irq 或 gpio_to_irq 获取
handler中断处理函数指针,返回类型 irqreturn_t
flags中断标志,见 4.3 节
name设备名称,在 /proc/interrupts 中显示
dev传递给处理函数的设备 ID,用于共享中断时区分设备

4.2.2 devm_request_irq:资源管理版本

devm_request_irq 是 devres 框架的一部分,当设备驱动被卸载或设备移除时,内核自动释放中断。蓝牙驱动推荐使用此接口,避免遗漏 free_irq。

// 蓝牙 HCI UART 驱动典型注册
static int bt_uart_probe(struct platform_device *pdev) {
    struct bt_uart_dev *btdev;
    int irq, ret;

    btdev = devm_kzalloc(&pdev->dev, sizeof(*btdev), GFP_KERNEL);
    // ... 初始化 btdev

    irq = platform_get_irq(pdev, 0);
    if (irq < 0)
        return irq;

    ret = devm_request_irq(&pdev->dev, irq, bt_uart_irq_handler,
                           IRQF_TRIGGER_FALLING | IRQF_SHARED,
                           "bt_uart", btdev);
    if (ret) {
        dev_err(&pdev->dev, "failed to request IRQ %d\n", irq);
        return ret;
    }

    // ... 注册 HCI 设备
    return 0;
}

// 无需显式 free_irq,devres 自动处理

4.2.3 中断处理函数返回值

返回值含义使用场景
IRQ_NONE中断不是本设备产生的共享中断中,检测到非本设备中断
IRQ_HANDLED中断已被正确处理正常处理完成
IRQ_WAKE_THREAD唤醒中断线程化处理需要将耗时操作推迟到线程上下文
蓝牙实践:蓝牙 HCI 中断处理通常返回 IRQ_HANDLED。若数据量较大(如 A2DP 音频流),可使用 request_threaded_irq 将数据搬运放在中断线程中,避免占用中断上下文过长时间。

4.3 中断标志:IRQF_SHARED 与 IRQF_TRIGGER_*

中断标志(flags)控制中断的行为特性。蓝牙驱动中常用的标志包括共享中断支持和触发方式配置。

4.3.1 中断标志分类

标志作用蓝牙驱动典型使用
IRQF_SHARED允许多个设备共享同一中断线蓝牙 UART 与其它外设共用中断线
IRQF_TRIGGER_RISING上升沿触发蓝牙中断引脚上升沿有效
IRQF_TRIGGER_FALLING下降沿触发蓝牙中断引脚下降沿有效
IRQF_TRIGGER_HIGH高电平触发电平敏感中断
IRQF_TRIGGER_LOW低电平触发电平敏感中断
IRQF_ONESHOT中断处理期间屏蔽中断线,处理完自动重新使能配合 request_threaded_irq 使用,防止中断风暴
IRQF_NO_SUSPEND系统休眠时不关闭中断蓝牙唤醒主机场景

4.3.2 IRQF_SHARED 使用要点

共享中断要求所有共享设备的中断处理函数都能正确识别中断是否属于自己的设备。处理函数必须检查硬件状态寄存器,若中断不是本设备产生,返回 IRQ_NONE。

// 共享中断处理函数模板
irqreturn_t bt_shared_irq_handler(int irq, void *dev_id) {
    struct bt_uart_dev *btdev = dev_id;

    // 1. 读取硬件中断状态寄存器
    u32 int_status = readl(btdev->regs + INT_STATUS);

    // 2. 检查是否为本设备中断
    if (!(int_status & BT_INT_PENDING)) {
        return IRQ_NONE;  // 不是本设备中断
    }

    // 3. 清除中断标志
    writel(BT_INT_PENDING, btdev->regs + INT_CLEAR);

    // 4. 处理数据
    // ...

    return IRQ_HANDLED;
}

4.3.3 触发方式选择

蓝牙芯片通常支持边沿触发(Edge Triggered)或电平触发(Level Triggered)。边沿触发适合脉冲式中断,电平触发适合需要保持中断信号直到被处理的场景。

调试建议:若遇到“irq %d: nobody cared”错误,检查中断触发方式是否与硬件匹配。使用 cat /proc/interrupts 查看中断计数,若计数不增加,说明触发方式配置错误。

4.4 中断上下文编程综合架构

下图展示了蓝牙驱动中断处理中原子操作、自旋锁、中断注册与标志的协作关系:

蓝牙中断上下文编程架构图 蓝牙驱动中断上下文编程架构 蓝牙硬件中断源 中断注册 devm_request_irq / request_irq 中断标志 flags 中断处理函数 handler IRQF_SHARED IRQF_TRIGGER_* IRQF_ONESHOT 检查中断状态 → 清除中断 中断上下文同步机制 原子操作 / 自旋锁 (spin_lock_irqsave) 共享数据:HCI RX 队列 / DMA 描述符 ⚠ 中断上下文不可睡眠 不可使用 mutex

4.5 中断上下文编程避坑清单

  1. 禁止睡眠:中断上下文中不能调用 kmalloc(GFP_KERNEL)、mutex_lock、copy_from_user 等可能睡眠的函数。使用 GFP_ATOMIC 分配内存。
  2. 自旋锁必须关中断:若锁在进程上下文中也被使用,中断处理函数中必须使用 spin_lock_irqsave。
  3. 共享中断必须检查设备状态:返回 IRQ_NONE 让内核继续分发中断给其他设备。
  4. 中断处理时间要短:蓝牙数据搬运应尽量在底半部(tasklet/threaded irq)中完成,顶半部只做最小必要操作。
  5. 触发方式匹配:边沿触发丢失中断的风险较高,电平触发需确保中断信号在清除前保持有效。
  6. 使用 devm_request_irq:避免手动 free_irq 遗漏导致资源泄漏。
调试命令速查:
cat /proc/interrupts — 查看中断计数和分布
cat /proc/softirqs — 查看软中断统计
echo t > /proc/sysrq-trigger — 打印所有任务状态,排查死锁