3. 中断上半部与下半部:为什么需要下半部、上半部与下半部的分工

3.1 中断处理的核心矛盾:快与全

中断服务程序(ISR)直接运行在中断上下文中,默认关闭本地 CPU 中断(或至少关闭当前中断线)。这意味着:

  • 响应延迟敏感:ISR 执行时间越长,CPU 响应其他中断的延迟就越大。
  • 上下文受限:中断上下文不能睡眠、不能调度、不能使用可能引起阻塞的锁。
  • 硬件约束:某些硬件设备要求必须在极短时间内完成 ACK 或数据读取,否则会丢失数据。

然而,很多设备驱动需要完成的工作量远超过“读一个寄存器”或“清一个标志位”。例如网络驱动收到一个数据包后,需要将数据从 DMA 缓冲区拷贝到 sk_buff、解析协议头、统计计数、唤醒等待队列——这些操作如果全部放在 ISR 中,会严重拖慢系统。

核心矛盾:中断处理必须 快(减少关中断时间),但驱动逻辑往往需要 全(完成复杂处理)。上半部与下半部的分工正是为了解决这一矛盾。

3.2 上半部与下半部的分工原则

Linux 将中断处理拆分为两个阶段:

特性 上半部(Top Half) 下半部(Bottom Half)
执行上下文 中断上下文(不可睡眠) 软中断 / tasklet / 工作队列(部分可睡眠)
中断状态 当前中断线关闭(或全部中断关闭) 中断全部开启
执行时机 硬件中断触发后立即执行 上半部返回后,由内核调度执行
典型工作 读硬件状态、清中断标志、拷贝少量数据 协议处理、数据拷贝、唤醒进程、复杂计算
执行时间要求 极短(微秒级) 相对宽松(毫秒级甚至更长)
可睡眠 否 工作队列可睡眠;tasklet/软中断不可
经验法则:如果某个操作可以在不关闭中断的情况下延迟执行,就把它放到下半部。上半部只做“必须立即做”的事。

3.3 上半部与下半部的协作流程

下图展示了从硬件中断触发到上下半部协作完成的完整流程:

上半部与下半部协作流程 硬件中断触发 上半部(ISR)执行 读状态 · 清标志 · 记录数据 · 调度下半部 是否需要下半部? 中断处理结束 调度下半部 tasklet / workqueue 下半部执行 协议解析 · 数据拷贝 · 唤醒进程 说明:上半部执行期间本地中断关闭,下半部执行时中断已重新开启。 下半部由内核在合适时机调度,不会阻塞其他中断响应。

3.4 为什么不能全部在上半部完成?

假设一个网络网卡驱动在 ISR 中完成所有工作:

/* 错误示例:所有工作都在上半部完成 */
irqreturn_t bad_net_isr(int irq, void *dev_id) {
    struct net_device *dev = dev_id;
    // 1. 读硬件状态寄存器
    u32 status = readl(dev->regs + STATUS);
    // 2. 从 DMA 缓冲区拷贝数据到 sk_buff
    struct sk_buff *skb = netdev_alloc_skb(dev, length);
    memcpy(skb_put(skb, length), dma_buf, length);
    // 3. 解析以太网协议头
    eth_type_trans(skb, dev);
    // 4. 统计计数
    dev->stats.rx_packets++;
    // 5. 递交到协议栈
    netif_rx(skb);
    // 6. 清中断标志
    writel(INT_CLEAR, dev->regs + INT_STATUS);
    return IRQ_HANDLED;
}

这段代码存在严重问题:

  • 关中断时间过长:memcpy 拷贝大包(如 1500 字节)可能耗时数十微秒,期间 CPU 无法响应其他中断。
  • 实时性破坏:如果系统中有更高优先级的中断(如定时器、传感器),它们会被延迟响应。
  • 嵌套中断风险:即使开启嵌套中断,ISR 中执行复杂逻辑也容易导致栈溢出或锁竞争。
性能警示:实测表明,在 1GHz ARM Cortex-A72 平台上,一次 1500 字节的 memcpy 约耗时 3-5μs。如果网络吞吐量达到 1Gbps,每秒约 81,000 个包,仅拷贝操作就占用 CPU 超过 400ms/s——这还不包括协议解析和统计开销。全部放在上半部将导致系统几乎无法响应其他中断。

3.5 正确的分工:上半部做最少,下半部做最多

将上述网络驱动重构为上下半部分工:

/* 上半部:只做必须立即做的事 */
irqreturn_t good_net_isr(int irq, void *dev_id) {
    struct net_device *dev = dev_id;
    // 1. 读状态,确认中断来源
    u32 status = readl(dev->regs + STATUS);
    if (!(status & RX_DONE))
        return IRQ_NONE;
    // 2. 保存必要信息(如 DMA 地址、长度)
    dev->priv->rx_dma_addr = readl(dev->regs + DMA_ADDR);
    dev->priv->rx_length   = readl(dev->regs + DMA_LEN);
    // 3. 调度下半部(tasklet)
    tasklet_schedule(&dev->priv->rx_tasklet);
    // 4. 清中断标志
    writel(INT_CLEAR, dev->regs + INT_STATUS);
    return IRQ_HANDLED;
}

/* 下半部:完成剩余所有工作 */
void net_rx_tasklet(unsigned long data) {
    struct net_device *dev = (struct net_device *)data;
    // 1. 从 DMA 缓冲区拷贝数据
    struct sk_buff *skb = netdev_alloc_skb(dev, dev->priv->rx_length);
    memcpy(skb_put(skb, dev->priv->rx_length),
           dev->priv->rx_dma_addr, dev->priv->rx_length);
    // 2. 解析协议头
    eth_type_trans(skb, dev);
    // 3. 统计计数
    dev->stats.rx_packets++;
    // 4. 递交到协议栈
    netif_rx(skb);
}

重构后的优势:

  • 上半部仅执行 3-4 次寄存器读写 + 一次 tasklet 调度,耗时 < 1μs。
  • 下半部在中断开启后执行,不阻塞其他中断响应。
  • 下半部可以被更高优先级的中断抢占,系统实时性得到保障。

3.6 下半部的三种实现机制对比

Linux 内核提供了三种下半部实现机制,适用于不同场景:

特性 软中断(Softirq) Tasklet 工作队列(Workqueue)
执行上下文 中断上下文 中断上下文 进程上下文
可睡眠 否 否 是
并发性 同一类型可多 CPU 并行 同一类型串行执行 由 worker pool 管理
使用复杂度 高(需静态声明) 低(动态创建) 中(需创建工作项)
典型用途 网络 RX/TX、定时器 驱动下半部通用机制 需要睡眠的复杂操作
调度开销 最低 低 较高(涉及进程切换)
选择建议:驱动开发中优先使用 tasklet,它简单安全且性能足够;如果下半部需要访问用户空间、使用可睡眠锁或执行文件系统操作,则必须使用工作队列。

3.7 上半部与下半部的数据传递

上下半部之间通过共享数据结构传递信息。常见模式:

  • 设备私有结构体:在 struct net_device->priv 或自定义结构体中保存上半部采集的数据。
  • 原子变量:使用 atomic_t 传递简单计数或状态。
  • 环形缓冲区:上半部写入,下半部读取,适用于批量数据场景。
  • 指针传递:通过 tasklet 或工作项的数据参数传递结构体指针。
/* 典型数据传递模式 */
struct my_device {
    void __iomem *regs;
    struct tasklet_struct rx_tasklet;
    /* 上半部填充,下半部消费 */
    u32    rx_dma_addr;
    u32    rx_length;
    atomic_t rx_count;
};

/* 上半部 */
irqreturn_t my_isr(int irq, void *dev_id) {
    struct my_device *mydev = dev_id;
    mydev->rx_dma_addr = readl(mydev->regs + DMA_ADDR);
    mydev->rx_length   = readl(mydev->regs + DMA_LEN);
    atomic_inc(&mydev->rx_count);
    tasklet_schedule(&mydev->rx_tasklet);
    return IRQ_HANDLED;
}

/* 下半部 */
void my_rx_tasklet(unsigned long data) {
    struct my_device *mydev = (struct my_device *)data;
    /* 使用 mydev->rx_dma_addr 和 mydev->rx_length 处理数据 */
    /* 处理完成后清零或更新 */
    atomic_dec(&mydev->rx_count);
}
关键要点:上半部与下半部共享的数据必须使用适当的同步机制(如原子操作、spinlock、memory barrier),防止竞争条件。tasklet 在同一 CPU 上不会与自身并发,但可能与其他 CPU 上的 tasklet 或上半部并发。