【Linux驱动开发】第15天:中断顶半部/底半部 + tasklet VS 工作队列

目录

  1. 为什么中断必须分顶半部与底半部?
  2. [顶半部与底半部 核心区别与分工](#顶半部与底半部 核心区别与分工)
  3. [底半部的两种主流实现:tasklet VS 工作队列](#底半部的两种主流实现:tasklet VS 工作队列)
  4. 终极对比表:12个维度全面对比
  5. 适用场景与选择原则(开发必背)
  6. 完整代码示例:标准中断驱动写法
  7. 常见误区澄清
  8. 核心知识点汇总

1. 为什么中断必须分顶半部与底半部?

中断处理函数有一个不可妥协的铁律
必须快!越快越好!

因为中断上下文是整个系统优先级最高的执行环境,一旦进入中断处理函数:

  • 整个系统的所有其他代码都会被暂停
  • 同优先级和更低优先级的中断无法响应
  • 执行时间过长会导致后续中断丢失、外设数据丢包
  • 严重时会触发看门狗复位,导致系统重启

但在实际硬件处理中,任何一个中断的完整处理流程都包含两类完全不同的工作:

  1. 必须立刻做的紧急工作:如清除硬件中断标志(不清除会无限触发中断)、读取硬件寄存器中的原始数据(不读会被下一次数据覆盖)
  2. 可以延后做的非紧急工作:如数据解析、日志打印、状态更新、业务逻辑处理

为了解决"中断必须快"和"处理需要时间"的矛盾,Linux内核将中断处理拆分为两个阶段:

  • 顶半部(上半部) :快速处理紧急工作,运行在中断上下文
  • 底半部(下半部) :延时处理非紧急工作,运行在可调度上下文

用公司紧急会议类比理解

  • 顶半部:10分钟紧急故障排查会,只做最紧急的事:确认故障现象、记录关键数据、通知相关人员
  • 底半部:会议结束后的后续工作:写故障报告、修复代码、发布补丁、复盘总结

这样拆分的好处是:

  • 紧急会议(顶半部)很短,不会耽误大家太多时间
  • 耗时的修复工作(底半部)可以在不影响正常工作的情况下慢慢做
  • 整个公司(系统)的运转不会因为一次故障而完全停滞

2. 顶半部与底半部 核心区别与分工

2.1 核心区别对比表

对比项 顶半部(上半部) 底半部(下半部)
运行环境 中断上下文 软中断上下文(tasklet)/ 内核线程上下文(工作队列)
能否睡眠 ❌ 绝对不能 ❌ 不能(tasklet) / ✅ 可以(工作队列)
执行速度要求 必须极快(微秒级) 可稍慢(毫秒级)
主要职责 清中断标志、读取硬件数据、调度底半部 数据处理、日志打印、状态更新、业务逻辑
实现方式 中断处理函数 tasklet / 工作队列 / 软中断

2.2 顶半部的铁律

顶半部是中断处理中最关键的部分,必须严格遵守以下规则:

  • 只做最紧急、必须立刻做的事
  • 绝对不能睡眠、不能阻塞
  • 绝对不能执行任何耗时操作
  • 执行时间越短越好,最好控制在100微秒以内

2.3 底半部的核心价值

底半部的存在就是为了承接顶半部中所有不紧急但又必须做的工作,让顶半部能够尽快返回,恢复系统的正常运行。

一句话总结两者的分工:
顶半部只做紧急必须做的;底半部做剩下所有不紧急但耗时的。


3. 底半部的两种主流实现:tasklet VS 工作队列

Linux内核提供了多种底半部实现机制,其中最常用的是tasklet工作队列(Workqueue) 。很多新手会误以为"底半部只能用tasklet",这是一个非常常见的误区。实际上,工作队列是现代内核最推荐的通用底半部方案

3.1 底层原理深度解析

tasklet:基于软中断的轻量级实现

tasklet是对内核软中断的轻量级封装。内核预先定义了10个软中断,其中两个专门用于tasklet:

  • TASKLET_SOFTIRQ:普通优先级tasklet
  • HI_SOFTIRQ:高优先级tasklet

所有tasklet都挂在这两个软中断的全局链表上。当你调用tasklet_schedule()时,内核会把这个tasklet加入到对应CPU的链表中。在中断返回前,内核会检查软中断链表,依次执行所有待处理的tasklet。

核心特性

  • 运行在软中断上下文,不属于任何进程
  • 一旦开始执行,就会一直执行到完成,不会被其他进程抢占
  • 同一个tasklet永远只会在一个CPU上执行,不会在多核并行
  • 绝对不能睡眠,不能调用任何阻塞函数
工作队列:基于内核线程的通用实现

工作队列是内核创建的一组通用内核线程池 。在现代内核中,每个CPU都有一个对应的kworker内核线程(如kworker/0:0kworker/1:0)。

当你调用schedule_work()时,内核会把这个工作加入到某个kworker的任务队列中。kworker线程在进程上下文中运行,会被内核调度器正常调度,依次执行队列中的所有工作。

核心特性

  • 运行在内核线程上下文 ,属于kworker进程
  • 执行过程中可以被更高优先级的进程抢占
  • 同一个工作可以在多个CPU上并行执行
  • 可以做任何事,可以睡眠、可以调用任何阻塞函数

4. 终极对比表:12个维度全面对比

对比项 tasklet 工作队列(Workqueue)
底层实现机制 基于软中断 基于内核线程池
运行上下文 软中断上下文 内核线程上下文(进程上下文)
能否睡眠 ❌ 绝对不能 ✅ 完全可以
能否被抢占 ❌ 不能 ✅ 可以
执行时机 中断返回前立即执行 内核调度器调度执行(有微小延迟)
典型延迟 微秒级(<100μs) 毫秒级(1-2ms)
能否调用阻塞函数 ❌ 不可以 ✅ 可以
能否用GFP_KERNEL分配内存 ❌ 不可以 ✅ 可以
能否用msleep/mutex ❌ 不可以 ✅ 可以
多核并行性 同一个tasklet不会多核并行 同一个工作可以在多核并行
内核开销 极小 较小
内核推荐度 特定场景使用 通用推荐

5. 适用场景与选择原则(开发必背)

5.1 选择口诀(记住这一句就够了)

能不用tasklet就不用tasklet,除非你明确知道为什么必须用它。

5.2 必须用tasklet的场景

只有当你的底半部满足所有以下条件时,才应该使用tasklet:

  1. 执行时间极短:通常不超过100微秒
  2. 绝对无阻塞:不需要等待任何硬件或软件资源
  3. 延迟要求极高:必须在中断触发后立即执行
  4. 不需要睡眠:不需要调用任何可能睡眠的函数

典型应用场景

  • 网卡驱动:顶半部读取数据包,tasklet快速解析头部并交给网络协议栈
  • 高速串口驱动:顶半部读取串口数据,tasklet将数据放入环形缓冲区
  • GPIO按键驱动:顶半部读取按键状态,tasklet上报输入事件(无消抖需求时)

5.3 必须用工作队列的场景

只要你的底半部满足任何一个以下条件,就必须使用工作队列:

  1. 需要睡眠 :需要调用msleepwait_eventschedule_timeout等函数
  2. 需要互斥 :需要用mutex_lock保护共享资源
  3. 需要内存分配 :需要用kmalloc(GFP_KERNEL)分配内存
  4. 执行时间较长:超过1毫秒的操作
  5. 需要用户态交互 :需要通过copy_to_user/copy_from_user和用户态通信
  6. 需要复杂数据处理:需要进行大量计算或数据转换

典型应用场景

  • 块设备驱动:处理磁盘IO请求,需要等待硬件完成
  • USB驱动:处理USB传输,需要等待设备响应
  • 传感器驱动:读取传感器数据,需要等待AD转换完成
  • 按键驱动:需要进行按键消抖(必须睡眠10-20ms)
  • 任何需要复杂业务逻辑的驱动

6. 完整代码示例:标准中断驱动写法

6.1 通用设备树节点

dts 复制代码
demo_device {
    compatible = "demo,irq";
    interrupts = <0 0 0>; // 根据你的硬件实际中断号修改
    status = "okay";
};

6.2 标准顶半部 + tasklet底半部 代码

这是最经典的中断驱动写法,适合短耗时、无睡眠需求的场景:

c 复制代码
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/interrupt.h>
#include <linux/of.h>
#include <linux/io.h>

// 设备私有数据结构体
struct demo_dev {
    int irq;
    void __iomem *reg_base; // 寄存器基地址
    struct tasklet_struct tlet;
    u32 hw_data; // 保存从硬件读取的数据
};

static struct demo_dev *dev;

// ==============================================
//  底半部:tasklet 处理函数
// ==============================================
static void demo_tasklet(unsigned long data)
{
    struct demo_dev *dev = (struct demo_dev *)data;

    printk("=== 底半部执行:数据处理、日志打印、耗时操作 ===\n");
    printk("从硬件读取的数据:0x%08x\n", dev->hw_data);

    // 这里可以做:
    // 数据解析、状态更新、日志、上报事件
    // 但仍然不能睡眠!不能调用msleep、mutex_lock等
}

// ==============================================
//  顶半部:中断处理函数(越快越好)
// ==============================================
static irqreturn_t demo_irq_handler(int irq, void *dev_id)
{
    struct demo_dev *dev = dev_id;

    // 1. 清中断标志(必须!否则会无限触发中断)
    writel(0x01, dev->reg_base + 0x04); // 假设0x04是中断清除寄存器

    // 2. 读取硬件数据(必须!否则会被下一次数据覆盖)
    dev->hw_data = readl(dev->reg_base + 0x00); // 假设0x00是数据寄存器

    // 3. 调度底半部(tasklet)
    tasklet_schedule(&dev->tlet);

    printk("顶半部:中断处理完成 → 调度底半部\n");
    return IRQ_HANDLED;
}

// ==============================================
//  驱动 probe:注册中断 + 初始化 tasklet
// ==============================================
static int demo_probe(struct platform_device *pdev)
{
    struct resource *mem_res;
    int irq, ret;

    // 分配设备结构体
    dev = devm_kzalloc(&pdev->dev, sizeof(*dev), GFP_KERNEL);
    if (!dev) {
        return -ENOMEM;
    }
    dev_set_drvdata(&pdev->dev, dev);

    // 1. 映射寄存器地址
    mem_res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
    if (!mem_res) {
        dev_err(&pdev->dev, "获取内存资源失败\n");
        return -ENODEV;
    }

    dev->reg_base = devm_ioremap_resource(&pdev->dev, mem_res);
    if (IS_ERR(dev->reg_base)) {
        return PTR_ERR(dev->reg_base);
    }

    // 2. 获取中断号
    irq = platform_get_irq(pdev, 0);
    if (irq < 0) {
        dev_err(&pdev->dev, "获取中断号失败\n");
        return irq;
    }
    dev->irq = irq;

    // 3. 初始化 tasklet
    tasklet_init(&dev->tlet, demo_tasklet, (unsigned long)dev);

    // 4. 注册中断(顶半部)
    ret = devm_request_irq(&pdev->dev, irq, demo_irq_handler,
                  IRQF_TRIGGER_RISING, "demo_irq", dev);
    if (ret) {
        dev_err(&pdev->dev, "注册中断失败\n");
        return ret;
    }

    printk("=== 驱动 probe 成功,中断注册完成 ===\n");
    return 0;
}

// ==============================================
//  驱动 remove:清理资源
// ==============================================
static int demo_remove(struct platform_device *pdev)
{
    struct demo_dev *dev = dev_get_drvdata(&pdev->dev);
    
    // 销毁 tasklet,等待正在执行的tasklet完成
    tasklet_kill(&dev->tlet);
    
    printk("驱动移除,tasklet 销毁\n");
    return 0;
}

// ==============================================
//  设备树匹配表
// ==============================================
static const struct of_device_id demo_ids[] = {
    { .compatible = "demo,irq" },
    {}, // 必须以空结构体结尾
};
MODULE_DEVICE_TABLE(of, demo_ids);

// ==============================================
//  platform 驱动结构体
// ==============================================
static struct platform_driver demo_driver = {
    .probe = demo_probe,
    .remove = demo_remove,
    .driver = {
        .name = "irq_demo",
        .of_match_table = demo_ids,
    },
};

module_platform_driver(demo_driver);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Linux Driver");
MODULE_DESCRIPTION("标准中断驱动示例(顶半部+tasklet底半部)");
MODULE_VERSION("1.0");

6.3 工作队列版本底半部代码

如果你的底半部需要睡眠(比如按键消抖),只需要把tasklet替换成工作队列即可:

c 复制代码
// 设备私有数据结构体
struct demo_dev {
    int irq;
    void __iomem *reg_base;
    struct work_struct work; // 替换tasklet
    u32 hw_data;
};

// 底半部:工作队列处理函数
static void demo_work(struct work_struct *work)
{
    struct demo_dev *dev = container_of(work, struct demo_dev, work);

    // 这里可以安全地睡眠!
    msleep(10); // 例如:按键消抖

    printk("=== 工作队列底半部执行 ===\n");
    printk("从硬件读取的数据:0x%08x\n", dev->hw_data);
}

// 顶半部:中断处理函数
static irqreturn_t demo_irq_handler(int irq, void *dev_id)
{
    struct demo_dev *dev = dev_id;

    writel(0x01, dev->reg_base + 0x04);
    dev->hw_data = readl(dev->reg_base + 0x00);

    // 调度工作队列
    schedule_work(&dev->work);

    printk("顶半部:中断处理完成 → 调度工作队列\n");
    return IRQ_HANDLED;
}

// probe函数中初始化工作
INIT_WORK(&dev->work, demo_work);

// remove函数中取消工作
cancel_work_sync(&dev->work);

7. 常见误区澄清

❌ 误区1:底半部只能用tasklet

错误。工作队列是现在最主流的底半部实现,绝大多数驱动都使用工作队列。

❌ 误区2:工作队列比tasklet慢很多

错误。在现代多核CPU上,工作队列的典型延迟在1-2毫秒左右,对于99%的驱动来说完全足够。只有对延迟要求极高的场景(如10Gbps网卡、高速ADC)才需要考虑使用tasklet。

❌ 误区3:tasklet可以在多核上并行执行

错误。同一个tasklet永远只会在一个CPU上执行,不会在多核上并行。这是tasklet的设计特性,目的是保证线程安全,不需要额外的同步机制。

❌ 误区4:工作队列不能在中断上下文中调度

错误schedule_work()可以在任何上下文中调用,包括中断上下文、软中断上下文和进程上下文。


8. 核心知识点汇总

8.1 顶半部(中断处理函数)

  • 运行在中断上下文
  • 不能睡眠、不能耗时
  • 只做三件事:清中断 → 读数据 → 调度底半部
  • 函数返回 IRQ_HANDLED

8.2 底半部

  • 运行在软中断上下文(tasklet)内核线程上下文(工作队列)
  • 处理耗时但不紧急的逻辑
  • 两种主流实现:tasklet和工作队列

8.3 tasklet核心要点

  • 基于软中断实现,轻量级
  • 不能睡眠,执行快
  • 适合极短、无阻塞的操作
  • 核心API:
    • DECLARE_TASKLET / tasklet_init
    • tasklet_schedule
    • tasklet_kill

8.4 工作队列核心要点

  • 基于内核线程实现,通用型
  • 可以睡眠,功能完整
  • 适合长耗时、需要阻塞的操作
  • 核心API:
    • INIT_WORK
    • schedule_work
    • cancel_work_sync

8.5 标准驱动流程

硬件触发中断

→ 顶半部快速处理(清中断、读数据)

→ 调度底半部(tasklet/工作队列)

→ 底半部执行延时处理

→ 驱动卸载销毁底半部资源

相关推荐
扛枪的书生8 小时前
Linux 网络管理器用法速查
linux
SkyWalking中文站9 小时前
认识 Horizon UI · 1/17:SkyWalking 新一代可观测性控制台
运维·前端·监控
顺风尿一寸10 小时前
Java Socket 内核之旅:从 SocketChannel.read() 到 tcp_recvmsg 与 epoll 的完整调用链路
linux
雪梨酱QAQ12 小时前
Kubeneters HA Cluster部署
运维
江华森17 小时前
Spring Cloud 微服务全栈实战:从 Eureka 到 Docker Compose 一文贯通
运维
江华森17 小时前
Matplotlib 数据绘图基础入门
运维
XIAOHEZIcode17 小时前
Ubuntu 终端美化全栈指南:Bash 到 Kitty 踩坑实录
linux·ubuntu·命令行
江华森17 小时前
NumPy 数值计算基础入门
运维
唐青枫19 小时前
别再只会用 cron:Linux systemd Timer 定时任务实战详解
linux
AlfredZhao3 天前
生产环境里,为什么不建议把普通端口直接暴露到公网?
linux·https·443·80