4. 中断上下文编程:原子操作与自旋锁、中断处理函数注册、中断标志
在蓝牙驱动中,中断处理程序(Interrupt Handler)运行于中断上下文,该上下文中不可睡眠、不可调度、不可使用可能引起阻塞的锁(如 mutex)。本章聚焦中断上下文编程的三个核心支柱:原子操作与自旋锁(保护共享数据)、中断处理函数注册(request_irq / devm_request_irq)、以及中断标志(IRQF_SHARED / IRQF_TRIGGER_*)。
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 | 唤醒中断线程化处理 | 需要将耗时操作推迟到线程上下文 |
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)。边沿触发适合脉冲式中断,电平触发适合需要保持中断信号直到被处理的场景。
cat /proc/interrupts 查看中断计数,若计数不增加,说明触发方式配置错误。
4.4 中断上下文编程综合架构
下图展示了蓝牙驱动中断处理中原子操作、自旋锁、中断注册与标志的协作关系:
4.5 中断上下文编程避坑清单
- 禁止睡眠:中断上下文中不能调用
kmalloc(GFP_KERNEL)、mutex_lock、copy_from_user等可能睡眠的函数。使用GFP_ATOMIC分配内存。 - 自旋锁必须关中断:若锁在进程上下文中也被使用,中断处理函数中必须使用
spin_lock_irqsave。 - 共享中断必须检查设备状态:返回 IRQ_NONE 让内核继续分发中断给其他设备。
- 中断处理时间要短:蓝牙数据搬运应尽量在底半部(tasklet/threaded irq)中完成,顶半部只做最小必要操作。
- 触发方式匹配:边沿触发丢失中断的风险较高,电平触发需确保中断信号在清除前保持有效。
- 使用 devm_request_irq:避免手动 free_irq 遗漏导致资源泄漏。
cat /proc/interrupts — 查看中断计数和分布cat /proc/softirqs — 查看软中断统计echo t > /proc/sysrq-trigger — 打印所有任务状态,排查死锁