linux中断:顶半部与底半部

中断可以分成两个部分,顶半部和底半部

一、中断为何分为两部分

进入中断处理函数时,CPU 通常处于关闭中断(或屏蔽同级中断)的状态。

这意味着:

其他设备的中断进不来

系统调度被暂停

整个系统的响应速度会急剧下降

如果你的中断处理函数里:

循环处理大量数据

延时等待

打印一堆 log

做复杂计算

那系统就会变卡、丢数据,甚至看起来像 "死机"。

所以内核设计者提出一个原则:

中断处理必须快进快出,不能耗时。

但现实是:

很多设备确实需要快速响应硬件,又需要做一些耗时的后续处理。

矛盾怎么解决?

于是就有了:上半部 + 下半部 的分离设计。

二、顶半部

顶半部 = 硬件中断处理函数本身。

顶半部只负责 "紧急且快速" 的事,其余一律甩给下半部

它的特点:

执行时机最紧急

中断上下文

不能睡眠、不能阻塞

必须极快完成

它只适合干三件事:

1、读取硬件状态 / 清除中断标志

告诉硬件 "中断我收到了,你别再重复触发"。

2、做必须立即完成的极简操作

比如简单寄存器读写、标记事件发生。

3、启动底半部

把真正耗时的工作丢出去。

三、底半部

底半部 = 被顶半部调度出来的、延后执行的处理流程。

下半部处理不紧急、但耗时间的逻辑。

它的特点:

不那么紧急

执行时中断已经打开

可以被新中断打断

部分机制允许睡眠(比如 workqueue)

适合干这些事:

数据拷贝、数据解析

网络包 / 串口数据处理

大量打印日志

控制外设的慢操作

上报数据到应用层

补充:同步调用:事情要一件一件做完,不做完就不往下走

异步调用:不用等,你先去干别的,等好了通知你

因此底半部是异步调用的

四、顶半部vs底半部

对比项 上半部 top half 下半部 bottom half
执行时机 中断触发立即执行 延后执行
中断状态 关中断 / 屏蔽中断 开中断,可被新中断打断
上下文 中断上下文 软中断 / 进程上下文
能否睡眠 绝对不能 workqueue 可以睡,tasklet 不行
执行速度 必须极快 可以适当耗时
主要任务 应答硬件、清中断、调度下半部 真正的数据处理、业务逻辑

五、Linux 中有哪些下半部实现

1. tasklet(小任务)(做不耗时任务)

是一种软中断

1、特点

也要较短小

基于软中断

不能睡眠

执行快、轻量

适合简单、快速的延后处理

同一个 tasklet 不会在多个 CPU 上同时跑

2、适用场景

快速、简单、非阻塞的延后工作

不需要睡眠

中断后马上要处理的轻量任务

3、使用步骤

  • 定义 tasklet 结构体
  • 写 tasklet 回调函数
  • 初始化 tasklet
  • 在中断上半部调度 tasklet

2. workqueue(工作队列)(做耗时任务)

就是一个普通的内核线程

1、特点

运行在内核线程上下文

可以睡眠、可以阻塞

写驱动最安全、最常用

适合稍微复杂的处理逻辑

2、适用场景

需要延时

需要睡眠

需要复杂处理

需要操作 I2C、SPI、GPIO 等慢设备

3、使用步骤

定义 work_struct

写工作函数

初始化 work

在上半部调度

3、二者对比

机制 适用场景 核心特性 你的选择建议
Tasklet 轻量级延时任务 基于软中断,不能睡眠,执行效率极高 处理速度极快、短小的任务
Workqueue 重量级延后处理 运行在内核线程上下文可以睡眠 最常用!适合读写文件、复杂逻辑、延时操作
对比项 tasklet workqueue
运行环境 软中断上下文 进程上下文
能否睡眠 不能 可以
能否阻塞 不能 可以
执行速度 很快 稍慢
安全性 一般 非常安全
抢占 不可抢占 可被抢占
驱动推荐 轻量任务 绝大多数驱动场景
最典型用途 快速清标志、简单处理 数据处理、延时、设备操作

六、完整驱动代码(上半部 + tasklet + workqueue)

cpp 复制代码
#include <linux/init.h>
#include <linux/module.h>
#include <linux/interrupt.h>
#include <linux/workqueue.h>
#include <linux/gpio.h>
#include <linux/of_gpio.h>
#include <linux/platform_device.h>

/* 本驱动演示:
 * 中断上半部    → 快速处理、清中断
 * tasklet       → 轻量级下半部,不能睡眠
 * workqueue     → 通用下半部,可以睡眠
 */

//=============================
// 1. 定义 tasklet(软中断下半部)
//=============================
struct tasklet_struct my_tasklet;

void tasklet_func(unsigned long data)
{
    printk("=== 下半部:tasklet 执行(不能睡眠)===\n");
}

//=============================
// 2. 定义 workqueue(可睡眠下半部)
//=============================
struct work_struct my_work;

void work_func(struct work_struct *work)
{
    printk("=== 下半部:workqueue 执行(可以睡眠)===\n");
    
    // workqueue 可以睡眠!tasklet 绝对不能这么写!
    msleep(50); 
}

//=============================
// 3. 中断上半部(中断处理函数)
//=============================
irqreturn_t my_irq_handler(int irq, void *dev_id)
{
    printk("\n★ 进入中断上半部:快速处理\n");

    // 1. 清中断(硬件操作,必须快)
    // 这里省略硬件寄存器操作...

    // 2. 调度 tasklet 下半部
    tasklet_schedule(&my_tasklet);

    // 3. 调度 workqueue 下半部
    schedule_work(&my_work);

    printk("★ 退出中断上半部\n");

    return IRQ_HANDLED;
}

//=============================
// 4. 驱动入口(初始化)
//=============================
static int __init my_drv_init(void)
{
    int irq;

    // 初始化 tasklet
    tasklet_init(&my_tasklet, tasklet_func, 0);

    // 初始化 workqueue
    INIT_WORK(&my_work, work_func);

    // 模拟获取一个中断号(真实驱动从设备树获取)
    // 这里假设你用的是 GPIO 中断
    irq = gpio_to_irq(101);  // 101 是示例 GPIO

    // 注册中断
    request_irq(irq, my_irq_handler, IRQF_TRIGGER_RISING,
               "my_irq", NULL);

    printk("=== 中断驱动初始化完成 ===\n");
    return 0;
}

//=============================
// 驱动出口
//=============================
static void __exit my_drv_exit(void)
{
    int irq = gpio_to_irq(101);
    free_irq(irq, NULL);
    tasklet_kill(&my_tasklet);
    printk("=== 驱动卸载 ===\n");
}

module_init(my_drv_init);
module_exit(my_drv_exit);
MODULE_LICENSE("GPL");

七、补充

1、中断上下文(不可被打断)

不可打断,不做休眠,不做耗时任务,不被操作系统调度

这里的不可被打断是同级之间的,同级之间不可打断,高优先级可以打断

中断上下文是 CPU 响应硬件中断后进入的执行环境,它并非属于任何一个用户进程或内核进程,没有对应的进程控制块(task_struct),也没有独立的进程 ID。当硬件触发中断时,CPU 会立即暂停当前正在执行的任务,跳转到中断处理函数(即中断上半部),此时就进入了中断上下文;此外,基于软中断实现的 tasklet,其执行环境也属于中断上下文的范畴。中断上下文的核心要求是 "快进快出",因为在这种上下文下,CPU 会屏蔽同级或更低优先级的中断,若执行时间过长,会导致系统调度暂停、其他设备中断无法响应,甚至引发系统卡顿或崩溃。同时,中断上下文有严格的限制:绝对不能睡眠、不能进行进程调度,也不能调用任何可能导致阻塞的函数(如 msleep、mutex_lock、copy_from_user 等),而且其栈空间非常有限,不能进行递归调用或定义过大的数组,只能执行最紧急、最简短的操作,比如清除中断标志、读取硬件寄存器状态,以及调度下半部任务,完成后立即退出,恢复系统正常运行。

包含:中断服务程序/软中断/tasklet

2、进程上下文(可以被打断)

可被打断、可休眠、可阻塞、可做耗时任务、可被操作系统调度
workqueue属于进程上下文

进程上下文是 CPU 执行用户进程、内核线程或驱动初始化 / 卸载函数时所处的执行环境,它明确属于某个具体的进程或内核线程,拥有完整的进程控制块,有独立的进程 ID。无论是用户程序调用系统调用(如 open、read、write)进入内核,还是内核线程(比如 workqueue 对应的内核线程)执行任务,或是驱动的 init、exit 函数运行时,都处于进程上下文。与中断上下文不同,进程上下文没有 "必须快速执行" 的强制要求,支持进程调度和睡眠 ------ 内核可以根据调度策略,将当前进程切换出去,执行其他优先级更高的任务,待条件满足后再切换回来继续执行。因此,在进程上下文下,我们可以调用绝大多数内核 API,包括需要延时的 msleep、用于同步的互斥锁、用于数据拷贝的 copy_from_user/copy_to_user,以及等待队列等,执行时间也可以相对较长,能够处理复杂的数据解析、外设慢操作等耗时任务。对于驱动开发而言,workqueue 之所以能支持睡眠和阻塞操作,核心就是因为它运行在进程上下文(内核线程)中,这也是它成为驱动开发中最常用下半部机制的关键原因。

包含:open/read/write等等系统调用

3、硬中断

硬中断是可以打断tasklet的

在 Linux 内核架构中,硬中断对应的就是中断上半部,运行在严格的中断上下文环境中。为了不阻塞整个系统,硬中断处理有非常严格的限制:执行时间必须极短,不能睡眠、不能阻塞、不能进行耗时操作,核心任务只包括快速应答硬件、清除中断标志,避免硬件持续触发中断,然后将复杂、耗时的处理逻辑交给软中断、tasklet 或 workqueue 等下半部机制去异步执行。

相关推荐
辞旧 lekkk2 小时前
【Git】远程操作与标签管理
linux·git·学习·萌新
web守墓人2 小时前
【linux】Mubuntu发布,将完整的ubuntu arm装进手机应用中
linux·arm开发·ubuntu
重生的黑客2 小时前
Linux 开发工具:Git 版本控制与 GDB 调试入门
linux·运维·git
敲上瘾2 小时前
Docker核心要点和指令速通
linux·运维·docker·容器
zhixingheyi_tian2 小时前
Hadoop 之 native 库
大数据·linux·hadoop·分布式
米饭不加菜2 小时前
PLC编程基础知识
运维·服务器
末日汐2 小时前
网络层IP
服务器·网络·tcp/ip
Soari2 小时前
Ziggo-Device软件构建(On device)教程
运维·服务器·bash·tsn 交换机
倔强的胖蚂蚁2 小时前
Gemma4 优势与 Ollama 更新
运维·云原生