4. 中断处理基础:Linux中断上下文、顶半部与底半部机制、中断注册API
在蓝牙HID驱动中,中断处理是连接硬件事件与软件协议栈的核心桥梁。蓝牙控制器通过中断通知主机有数据到达(如HID报告),而驱动必须高效、安全地响应这些中断。本章深入剖析Linux中断处理的三层架构:中断上下文、顶半部(Top Half)与底半部(Bottom Half),以及中断注册API的工程实践。
4.1 中断上下文:硬中断与软中断的边界
Linux内核将中断处理分为两种执行上下文:中断上下文(Interrupt Context)和进程上下文(Process Context)。中断上下文具有以下关键特性:
- 不可睡眠:中断上下文不能调用可能导致睡眠的函数(如
kmalloc(GFP_KERNEL)、mutex_lock())。 - 不可调度:当前执行流不能被其他进程抢占,直到中断处理完成。
- 栈空间有限:通常只有4KB~8KB的独立中断栈,需避免深度递归或大局部变量。
- 原子性要求:访问共享数据时必须使用自旋锁(spinlock)而非互斥锁(mutex)。
工程提示:蓝牙HID中断处理中,如果尝试在中断上下文中调用
wait_for_completion()或msleep(),内核会触发BUG: scheduling while atomic错误,导致系统不稳定。这是新手最常见的陷阱之一。
4.2 顶半部与底半部机制
为了平衡中断响应速度和系统吞吐量,Linux将中断处理拆分为两个阶段:
| 特性 | 顶半部(Top Half) | 底半部(Bottom Half) |
|---|---|---|
| 执行时机 | 中断触发后立即执行 | 顶半部返回后,由内核调度执行 |
| 中断状态 | 当前CPU中断被禁用 | 中断已重新使能 |
| 执行时间 | 极短(通常 < 50μs) | 相对较长(ms级) |
| 典型操作 | 读取硬件寄存器、清除中断标志、调度底半部 | 数据拷贝、协议解析、唤醒等待队列 |
| 实现机制 | request_irq() 注册的处理函数 |
软中断、tasklet、工作队列 |
4.2.1 顶半部:快速响应硬件
顶半部是中断处理的第一阶段,直接由硬件中断触发。在蓝牙HID驱动中,顶半部通常完成以下工作:
- 读取中断状态寄存器,确认中断源(如HCI事件、ACL数据、SCO数据)。
- 从硬件FIFO中读取数据到临时缓冲区。
- 清除硬件中断标志位,使能后续中断。
- 调度对应的底半部机制(如触发tasklet或唤醒工作队列)。
性能关键点:顶半部执行期间,当前CPU无法响应其他中断。如果顶半部耗时过长,会导致中断丢失或系统延迟增加。蓝牙HID驱动中,顶半部应仅做“最小必要操作”,将数据解析和协议处理推迟到底半部。
4.2.2 底半部:延迟处理与数据流
底半部在顶半部返回后、中断重新使能的情况下执行。Linux提供三种主要的底半部机制:
| 机制 | 执行上下文 | 可睡眠 | 并发性 | 蓝牙HID适用场景 |
|---|---|---|---|---|
| 软中断(SoftIRQ) | 中断上下文 | 否 | 同类型可并行(需注意重入) | 高吞吐量数据路径(如HCI ACL数据) |
| Tasklet | 中断上下文 | 否 | 同类型串行执行 | HID报告解析、事件分发 |
| 工作队列(Workqueue) | 进程上下文 | 是 | 可并行,可调度 | 需要睡眠的操作(如USB URB提交、内存分配) |
4.3 中断注册API:从硬件到驱动的桥梁
Linux内核提供标准API用于注册和注销中断处理函数。蓝牙HID驱动通常使用以下接口:
4.3.1 request_irq() 与 devm_request_irq()
// 传统方式
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);
| 参数 | 说明 | 蓝牙HID典型值 |
|---|---|---|
irq |
中断号,可通过platform_get_irq()或gpio_to_irq()获取 |
UART IRQ 或 GPIO IRQ |
handler |
顶半部处理函数指针 | btusb_interrupt() 或 hci_uart_interrupt() |
flags |
中断触发类型和共享属性 | IRQF_TRIGGER_FALLING | IRQF_ONESHOT |
name |
中断名称,显示在/proc/interrupts |
"bt_hid" 或 "hci_uart" |
dev_id |
传递给处理函数的设备标识,用于共享中断区分 | &hdev->stat 或 struct uart_port |
关键注意事项:
- IRQF_ONESHOT:对于线程化中断(threaded IRQ),必须设置此标志,确保中断线在顶半部处理期间被屏蔽,直到底半部完成。
- 共享中断:蓝牙控制器可能与其他设备共享中断线,必须使用
IRQF_SHARED标志,并在处理函数中检查中断是否来自本设备。 - 中断号获取:对于USB蓝牙适配器,中断号由USB子系统管理,驱动通过URB完成回调处理数据,而非直接注册IRQ。
4.3.2 中断处理函数原型
static irqreturn_t bt_hid_interrupt(int irq, void *dev_id)
{
struct bt_hid_device *bthid = (struct bt_hid_device *)dev_id;
unsigned long flags;
// 1. 读取中断状态寄存器
uint32_t status = readl(bthid->regs + INT_STATUS);
// 2. 检查中断是否来自本设备
if (!(status & INT_PENDING))
return IRQ_NONE; // 非本设备中断,共享中断场景
// 3. 清除中断标志
writel(INT_PENDING, bthid->regs + INT_CLEAR);
// 4. 从FIFO读取数据到临时缓冲区
spin_lock_irqsave(&bthid->lock, flags);
bthid->rx_count = readl(bthid->regs + RX_FIFO_COUNT);
memcpy_fromio(bthid->rx_buf, bthid->regs + RX_FIFO, bthid->rx_count);
spin_unlock_irqrestore(&bthid->lock, flags);
// 5. 调度底半部(使用tasklet)
tasklet_schedule(&bthid->rx_tasklet);
return IRQ_HANDLED;
}
4.3.3 底半部注册与实现
// 使用tasklet(推荐用于HID报告解析)
DECLARE_TASKLET_OLD(bthid->rx_tasklet, bt_hid_rx_tasklet);
static void bt_hid_rx_tasklet(unsigned long data)
{
struct bt_hid_device *bthid = (struct bt_hid_device *)data;
// 解析HCI事件或HID报告
// 此函数运行在中断上下文,不可睡眠
bt_hid_process_rx_data(bthid);
}
// 使用工作队列(用于需要睡眠的场景)
static struct workqueue_struct *bt_hid_wq;
INIT_WORK(&bthid->rx_work, bt_hid_rx_work);
static void bt_hid_rx_work(struct work_struct *work)
{
struct bt_hid_device *bthid = container_of(work,
struct bt_hid_device, rx_work);
// 可以安全调用kmalloc(GFP_KERNEL)、submit_urb()等
bt_hid_submit_urb(bthid);
}
4.4 中断处理性能优化要点
工程实践清单:
- 顶半部最小化:只做寄存器读写和标志清除,所有数据解析推迟到底半部。
- 选择正确的底半部:HID报告解析使用tasklet;涉及USB URB提交或内存分配使用工作队列。
- 避免自旋锁滥用:在顶半部中使用
spin_lock_irqsave(),在底半部中使用spin_lock_bh()。 - 使用
devm_request_irq():自动管理中断注销,避免驱动卸载时遗漏。 - 监控中断延迟:通过
/proc/interrupts和trace_irq_handler_entry/exit跟踪中断处理耗时。