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错误,导致系统不稳定。这是新手最常见的陷阱之一。
中断上下文 vs 进程上下文 中断上下文 • 不可睡眠 • 不可调度 • 栈空间 4KB~8KB • 使用自旋锁 • 执行时间 < 100μs • 典型:顶半部、软中断、tasklet 进程上下文 • 可以睡眠 • 可以被调度 • 栈空间 8KB+ • 使用互斥锁 • 执行时间无严格限制 • 典型:系统调用、工作队列、内核线程 切换

4.2 顶半部与底半部机制

为了平衡中断响应速度和系统吞吐量,Linux将中断处理拆分为两个阶段:

特性 顶半部(Top Half) 底半部(Bottom Half)
执行时机 中断触发后立即执行 顶半部返回后,由内核调度执行
中断状态 当前CPU中断被禁用 中断已重新使能
执行时间 极短(通常 < 50μs) 相对较长(ms级)
典型操作 读取硬件寄存器、清除中断标志、调度底半部 数据拷贝、协议解析、唤醒等待队列
实现机制 request_irq() 注册的处理函数 软中断、tasklet、工作队列

4.2.1 顶半部:快速响应硬件

顶半部是中断处理的第一阶段,直接由硬件中断触发。在蓝牙HID驱动中,顶半部通常完成以下工作:

  1. 读取中断状态寄存器,确认中断源(如HCI事件、ACL数据、SCO数据)。
  2. 从硬件FIFO中读取数据到临时缓冲区。
  3. 清除硬件中断标志位,使能后续中断。
  4. 调度对应的底半部机制(如触发tasklet或唤醒工作队列)。
性能关键点:顶半部执行期间,当前CPU无法响应其他中断。如果顶半部耗时过长,会导致中断丢失或系统延迟增加。蓝牙HID驱动中,顶半部应仅做“最小必要操作”,将数据解析和协议处理推迟到底半部。

4.2.2 底半部:延迟处理与数据流

底半部在顶半部返回后、中断重新使能的情况下执行。Linux提供三种主要的底半部机制:

机制 执行上下文 可睡眠 并发性 蓝牙HID适用场景
软中断(SoftIRQ) 中断上下文 否 同类型可并行(需注意重入) 高吞吐量数据路径(如HCI ACL数据)
Tasklet 中断上下文 否 同类型串行执行 HID报告解析、事件分发
工作队列(Workqueue) 进程上下文 是 可并行,可调度 需要睡眠的操作(如USB URB提交、内存分配)
蓝牙HID中断处理流程:顶半部 → 底半部 硬件中断触发 顶半部(Top Half) 读状态寄存器 → 读FIFO → 清中断标志 → 调度底半部(tasklet/工作队列) Tasklet 中断上下文 不可睡眠 HID报告解析 工作队列 进程上下文 可睡眠 USB URB提交 软中断 中断上下文 不可睡眠 HCI数据帧处理

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跟踪中断处理耗时。
中断注册与处理完整流程 驱动初始化 devm_request_irq() 硬件中断触发 CPU执行中断向量 顶半部执行 读状态 → 清标志 → 调度底半部 底半部执行(tasklet/工作队列) 数据解析 → 协议处理 → 唤醒应用层 性能监控点 /proc/interrupts 查看中断计数 | tracepoint 跟踪耗时

4.5 本章小结