当前位置:首页>Linux>Linux 中断机制与下半部设计

Linux 中断机制与下半部设计

  • 2026-09-09 17:06:47
Linux 中断机制与下半部设计

Linux 驱动开发中,中断机制是最核心、最基础,也最容易暴露设计问题的部分之一。外设产生中断后,CPU 需要立即暂停当前工作,进入中断处理流程;但中断处理又要求尽可能快,否则会影响系统实时性,甚至造成中断丢失。因此,Linux 将中断处理拆分为“上半部”和“下半部”:上半部快速响应硬件,下半部延后处理复杂逻辑。

围绕这一思想,Linux 提供了多种机制:硬中断处理函数、softirq、tasklet、工作队列、线程化中断等。

文章以 Linux 内核通用驱动模型为基础进行说明,适用于字符设备驱动、平台驱动、GPIO 中断、I2C/SPI 设备中断等常见场景。


一、Linux 中断机制的含义与总体流程

1.1 中断是什么

中断是硬件或软件向 CPU 发出的一种异步事件通知。典型硬件中断来自外设,例如:

  • UART 接收到数据;
  • GPIO 引脚电平变化;
  • 网卡收到报文;
  • 定时器到期;
  • DMA 传输完成;
  • 按键按下或释放。

CPU 收到中断后,会暂停当前正在执行的代码,保存现场,然后跳转到对应的中断处理入口。处理完成后,再恢复原来的执行流程。

在 Linux 中,中断通常用 IRQ 号表示。IRQ 号并不一定直接等于硬件引脚号,而是经过中断控制器、设备树、irqchip 框架映射后的 Linux 虚拟中断号。

一个典型的中断传递路径如下:

外设  |  v中断控制器(GIC / APIC / GPIO irqchip 等)  |  vCPU 中断入口  |  vLinux 中断核心框架  |  virq_desc / irq_chip / irqaction  |  v驱动注册的 ISR

1.2 中断上下文与进程上下文

Linux 内核代码运行在不同上下文中:

  • 进程上下文:运行在内核线程或系统调用中,可以睡眠、可以阻塞、可以访问用户空间;
  • 中断上下文:运行在中断处理路径中,不能睡眠,不能长时间执行,不能调用可能阻塞的函数;
  • 软中断上下文:介于硬中断和进程上下文之间,也不能睡眠。

中断上下文中不允许调用以下类型函数:

  • msleep();
  • schedule();
  • mutex_lock();
  • copy_from_user();
  • copy_to_user();
  • 可能阻塞的内存分配,例如普通 GFP_KERNEL;
  • 可能睡眠的等待函数。

中断处理函数必须短小、快速、确定性强。

1.3 上半部与下半部

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

+----------------------+|      硬件中断        ||      上半部          ||  快速响应硬件        |+----------------------+           |           v+----------------------+|      下半部          ||  延后处理复杂逻辑    |+----------------------+

上半部通常只做最少量的工作:

  • 读取中断状态寄存器;
  • 判断中断是否属于本设备;
  • 清除中断源;
  • 读取少量关键数据;
  • 调度下半部;
  • 返回。

下半部负责后续复杂处理:

  • 解析数据;
  • 上报输入事件;
  • 网络包处理;
  • 文件写入;
  • 唤醒进程;
  • 启动 DMA;
  • 调用可能睡眠的函数。

常见下半部机制包括:

  • softirq;
  • tasklet;
  • 工作队列;
  • 线程化中断;
  • 软中断相关的 ksoftirqd 内核线程。

二、Linux 中断基本 API 与申请流程

2.1 获取中断号

在平台驱动中,中断号通常来自设备树。常见获取方式:

int irq;irq = platform_get_irq(pdev, 0);if (irq < 0)return irq;

platform_get_irq() 返回 Linux IRQ 号,失败时返回负错误码。

其他获取方式还包括:

int irq = platform_get_irq_byname(pdev, "event");int irq = of_irq_get(np, index);int irq = gpio_to_irq(gpio);

现代驱动更推荐使用 platform_get_irq() 或 platform_get_irq_byname(),而不是直接解析设备树或手动转换 GPIO。

2.2 request_irq 与 devm_request_irq

注册中断处理函数最经典的 API 是:

int request_irq(unsigned int irq,irq_handler_t handler,unsigned long flags,const char *name,void *dev_id);

资源管理版本为:

int devm_request_irq(struct device *dev,unsigned int irq,irq_handler_t handler,unsigned long flags,const char *name,void *dev_id);

devm_request_irq() 会在设备生命周期结束时自动释放中断,适合大多数平台驱动。

中断处理函数原型为:

typedef irqreturn_t (*irq_handler_t)(int irq, void *dev_id);

返回值类型为:

irqreturn_t

常见返回值:

返回值
含义
IRQ_NONE
当前中断不是本设备产生,常见于共享中断
IRQ_HANDLED
中断已处理
IRQ_WAKE_THREAD
请求唤醒线程化中断处理线程

2.3 flags 参数

常见 flags:

flags
说明
IRQF_SHARED
允许多个设备共享同一个中断号
IRQF_TRIGGER_RISING
上升沿触发
IRQF_TRIGGER_FALLING
下降沿触发
IRQF_TRIGGER_HIGH
高电平触发
IRQF_TRIGGER_LOW
低电平触发
IRQF_ONESHOT
线程化中断执行期间保持中断关闭
IRQF_NO_SUSPEND
系统挂起期间不关闭该中断

触发方式通常由设备树或硬件平台决定,驱动中不建议随意硬编码,除非明确硬件要求。

2.4 name 与 dev_id

name 通常用于 /proc/interrupts 中显示:

cat /proc/interrupts

dev_id 在共享中断中非常关键。共享中断下,多个设备可能注册同一个 IRQ,内核需要依靠 dev_id 区分不同设备。

释放共享中断时,必须传入与注册时相同的 dev_id:

free_irq(irq, dev_id);

因此,dev_id 通常传入设备私有结构体指针。

2.5 中断申请基本流程

一个标准中断申请流程如下:

1. 获取 IRQ 号2. 初始化设备私有数据3. 编写中断处理函数4. 调用 request_irq / devm_request_irq5. 注册成功后等待硬件中断6. 驱动卸载时释放中断

示例:

#include <linux/interrupt.h>#include <linux/platform_device.h>#include <linux/slab.h>struct demo_dev {struct device *dev;int irq;};static irqreturn_t demo_isr(int irq, void *dev_id){struct demo_dev *dev = dev_id;/*     * 实际驱动中需要读取硬件状态寄存器,     * 判断中断是否由本设备产生。     */if (!device_has_pending_event(dev))return IRQ_NONE;/* 清除中断源 */    ack_device_event(dev);/* 调度下半部 */return IRQ_HANDLED;}static int demo_probe(struct platform_device *pdev){struct demo_dev *dev;int irq;int ret;    dev = devm_kzalloc(&pdev->dev, sizeof(*dev), GFP_KERNEL);if (!dev)return -ENOMEM;    irq = platform_get_irq(pdev, 0);if (irq < 0)return irq;    dev->dev = &pdev->dev;    dev->irq = irq;    ret = devm_request_irq(&pdev->dev, irq, demo_isr,                           IRQF_SHARED, "demo_irq", dev);if (ret)return ret;    platform_set_drvdata(pdev, dev);return 0;}

2.6 共享中断注意事项

共享中断下,中断处理函数必须能够判断中断是否来自本设备。如果无法判断,就可能出现以下问题:

  • 其他设备的中断被误认为已处理;
  • 本设备中断被忽略;
  • 系统出现中断风暴;
  • 内核统计大量 IRQ_NONE。

共享中断处理函数的一般写法:

static irqreturn_t demo_isr(int irq, void *dev_id){struct demo_dev *dev = dev_id;if (!is_my_device_interrupt(dev))return IRQ_NONE;    clear_my_device_interrupt(dev);return IRQ_HANDLED;}

2.7 中断释放

非 devm 方式注册的中断需要手动释放:

free_irq(dev->irq, dev);

如果使用 devm_request_irq(),通常不需要显式调用 free_irq()。但需要注意:如果驱动中还使用了 tasklet、工作队列、定时器等机制,应在 remove 函数中先停止这些异步任务,避免访问已释放资源。

例如:

static int demo_remove(struct platform_device *pdev){struct demo_dev *dev = platform_get_drvdata(pdev);    tasklet_kill(&dev->rx_tasklet);    cancel_work_sync(&dev->work);return 0;}

2.8 线程化中断

除了传统下半部机制,Linux 还支持线程化中断。线程化中断将复杂处理放到内核线程中执行,可以睡眠。

注册接口:

int request_threaded_irq(unsigned int irq,irq_handler_t handler,irq_handler_t thread_fn,unsigned long irqflags,const char *devname,void *dev_id);

devm 版本:

int devm_request_threaded_irq(struct device *dev,unsigned int irq,irq_handler_t handler,irq_handler_t thread_fn,unsigned long irqflags,const char *devname,void *dev_id);

示例:

static irqreturn_t demo_isr(int irq, void *dev_id){struct demo_dev *dev = dev_id;if (!device_has_pending_event(dev))return IRQ_NONE;    ack_device_event(dev);return IRQ_WAKE_THREAD;}static irqreturn_t demo_thread_fn(int irq, void *dev_id){struct demo_dev *dev = dev_id;/*     * 这里运行在进程上下文中,     * 可以睡眠、可以加 mutex、可以分配 GFP_KERNEL 内存。     */return IRQ_HANDLED;}static int demo_probe(struct platform_device *pdev){struct demo_dev *dev;int irq;int ret;    dev = devm_kzalloc(&pdev->dev, sizeof(*dev), GFP_KERNEL);if (!dev)return -ENOMEM;    irq = platform_get_irq(pdev, 0);if (irq < 0)return irq;    dev->irq = irq;    ret = devm_request_threaded_irq(&pdev->dev, irq,                                    demo_isr,                                    demo_thread_fn,                                    IRQF_SHARED | IRQF_ONESHOT,"demo_irq",                                    dev);if (ret)return ret;    platform_set_drvdata(pdev, dev);return 0;}

线程化中断非常适合 I2C、SPI 等总线设备,因为这些设备的中断后续处理往往需要访问寄存器,而寄存器访问可能睡眠。


三、下半部机制之一:tasklet

3.1 tasklet 的基本含义

tasklet 是 Linux 中一种基于 softirq 的延迟处理机制。它运行在软中断上下文,不能睡眠,但比工作队列更轻量,延迟更低。

tasklet 的特点:

  • 运行在软中断上下文;
  • 不能睡眠;
  • 适合短小、快速、不需要阻塞的处理;
  • 同一个 tasklet 实例不会并发执行;
  • 不同 tasklet 实例可以在不同 CPU 上并发执行;
  • 可以在硬中断处理函数中调度。

tasklet 本质上属于 softirq 的一种,对应:

TASKLET_SOFTIRQ

因此,在 /proc/softirqs 中可以看到 TASKLET 相关计数。

3.2 tasklet 常用 API

较新的内核推荐使用:

void tasklet_setup(struct tasklet_struct *t,void (*callback)(struct tasklet_struct *));void tasklet_schedule(struct tasklet_struct *t);void tasklet_hi_schedule(struct tasklet_struct *t);void tasklet_kill(struct tasklet_struct *t);

旧内核中常见:

DECLARE_TASKLET(name, func, data);tasklet_init(&tasklet, func, data);

新接口中,tasklet 回调函数参数为:

struct tasklet_struct *

驱动通常使用 container_of() 获取私有数据。

3.3 tasklet 示例

下面给出一个完整结构示例。

#include <linux/interrupt.h>#include <linux/platform_device.h>#include <linux/slab.h>struct demo_dev {struct device *dev;int irq;struct tasklet_struct rx_tasklet;int event_count;};static void demo_tasklet_fn(struct tasklet_struct *t){struct demo_dev *dev = container_of(t, struct demo_dev, rx_tasklet);/*     * tasklet 运行在软中断上下文,     * 不能睡眠,不能调用 mutex_lock、msleep 等函数。     */    dev->event_count++;/* 做少量快速处理 */}static irqreturn_t demo_isr(int irq, void *dev_id){struct demo_dev *dev = dev_id;if (!device_has_pending_event(dev))return IRQ_NONE;    ack_device_event(dev);/* 调度 tasklet 下半部 */    tasklet_schedule(&dev->rx_tasklet);return IRQ_HANDLED;}static int demo_probe(struct platform_device *pdev){struct demo_dev *dev;int irq;int ret;    dev = devm_kzalloc(&pdev->dev, sizeof(*dev), GFP_KERNEL);if (!dev)return -ENOMEM;    irq = platform_get_irq(pdev, 0);if (irq < 0)return irq;    dev->dev = &pdev->dev;    dev->irq = irq;    tasklet_setup(&dev->rx_tasklet, demo_tasklet_fn);    ret = devm_request_irq(&pdev->dev, irq, demo_isr,                           IRQF_SHARED, "demo_irq", dev);if (ret)return ret;    platform_set_drvdata(pdev, dev);return 0;}static int demo_remove(struct platform_device *pdev){struct demo_dev *dev = platform_get_drvdata(pdev);/*     * 确保 tasklet 不再运行,     * 防止释放设备结构后 tasklet 仍访问内存。     */    tasklet_kill(&dev->rx_tasklet);return 0;}

3.4 tasklet 使用注意事项

tasklet 虽然使用简单,但有几个限制必须注意:

  1. 不能睡眠;
  2. 不能访问用户空间;
  3. 不能调用可能阻塞的函数;
  4. 不适合长时间运行;
  5. 共享数据需要使用自旋锁保护;
  6. 驱动卸载前必须调用 tasklet_kill() 确保 tasklet 停止。

如果 tasklet 中需要访问进程上下文和软中断上下文共享的数据,通常使用:

spin_lock_bh(&lock);...spin_unlock_bh(&lock);

如果数据还会被硬中断访问,则应使用:

unsigned long flags;spin_lock_irqsave(&lock, flags);...spin_unlock_irqrestore(&lock, flags);

四、下半部机制之二:softirq

4.1 softirq 的基本含义

softirq 是 Linux 内核中更底层的软中断机制。tasklet 就是建立在 softirq 之上的封装。

softirq 的特点:

  • 静态定义,数量有限;
  • 不能动态创建新类型;
  • 运行在软中断上下文;
  • 不能睡眠;
  • 可以在多个 CPU 上并发执行;
  • 性能高,延迟低;
  • 通常用于内核核心子系统,不建议普通驱动随意使用。

内核中常见的 softirq 类型包括:

HI_SOFTIRQTIMER_SOFTIRQNET_TX_SOFTIRQNET_RX_SOFTIRQBLOCK_SOFTIRQIRQ_POLL_SOFTIRQTASKLET_SOFTIRQSCHED_SOFTIRQHRTIMER_SOFTIRQRCU_SOFTIRQ

可以通过以下命令查看 softirq 统计:

cat /proc/softirqs

4.2 softirq 的执行时机

softirq 通常在以下时机执行:

  1. 硬中断返回前;
  2. local_bh_enable() 被调用时;
  3. ksoftirqd 内核线程中。

当 softirq 数量过多或持续触发时,内核会交给 ksoftirqd 线程处理,避免长时间占用 CPU 导致用户态任务饿死。

4.3 softirq 常用 API

softirq 相关 API 包括:

void open_softirq(int nr, void (*action)(struct softirq_action *));void raise_softirq(unsigned int nr);

查看 softirq 状态:

cat /proc/softirqs

禁止/恢复软中断执行:

local_bh_disable();local_bh_enable();

需要注意:local_bh_disable() 并不会禁止硬中断,它只是推迟当前 CPU 上的 softirq/tasklet 执行。

4.4 softirq 示例

softirq 的编号是静态分配的,主线内核中常见 softirq 编号基本已被核心子系统使用。普通驱动通常不应该随意注册或替换 softirq。

下面给出一个原理性示例,用于说明 softirq 的注册和触发方式。实际产品中,除非内核中确实存在空闲 softirq 编号,或者经过内核源码修改,否则不应这样使用。

#include <linux/interrupt.h>#include <linux/module.h>#include <linux/atomic.h>static atomic_t demo_softirq_count = ATOMIC_INIT(0);/* * DEMO_SOFTIRQ 为示意编号。 * 主线内核 softirq 编号静态定义, * 普通模块通常不应占用已有 softirq。 */static void demo_softirq_fn(struct softirq_action *h){atomic_inc(&demo_softirq_count);}static int __init demo_softirq_init(void){    open_softirq(DEMO_SOFTIRQ, demo_softirq_fn);return 0;}static void demo_trigger_softirq(void){    raise_softirq(DEMO_SOFTIRQ);}module_init(demo_softirq_init);

更现实的 softirq 使用方式是观察内核已有 softirq:

watch -n 1 cat /proc/softirqs

例如网络收发包、块设备、定时器、tasklet 等都会体现在 softirq 统计中。

4.5 softirq 的并发模型

softirq 的并发模型比 tasklet 更复杂。

同一个 softirq 可以在多个 CPU 上同时执行:

CPU0: NET_RX_SOFTIRQ runningCPU1: NET_RX_SOFTIRQ runningCPU2: NET_RX_SOFTIRQ running

因此,softirq 处理函数必须可重入,必须自行处理并发保护。

常见保护方式:

static DEFINE_SPINLOCK(demo_lock);static void demo_softirq_fn(struct softirq_action *h){unsigned long flags;    spin_lock_irqsave(&demo_lock, flags);/* 访问共享数据 */    spin_unlock_irqrestore(&demo_lock, flags);}

4.6 softirq 使用注意事项

softirq 适合内核核心路径,不适合普通外设驱动。原因包括:

  1. softirq 编号静态固定;
  2. 不能动态扩展;
  3. 并发模型复杂;
  4. 调试难度较高;
  5. 容易影响系统整体延迟;
  6. 普通驱动很难比 tasklet 或工作队列获得明显收益。

因此,驱动开发中如果需要 softirq 级别的延迟,通常优先考虑 tasklet;如果需要睡眠,则选择工作队列或线程化中断。


五、下半部机制之三:工作队列

5.1 工作队列的基本含义

工作队列将任务交给内核工作线程执行。工作线程运行在进程上下文中,因此工作队列可以睡眠、可以阻塞、可以调用可能睡眠的函数。

工作队列特点:

  • 运行在进程上下文;
  • 可以睡眠;
  • 可以调用 mutex_lock();
  • 可以使用 GFP_KERNEL 分配内存;
  • 可以访问用户空间,但通常需要合理场景;
  • 延迟比 softirq/tasklet 高;
  • 适合复杂、耗时、可能阻塞的任务。

常见内核工作线程名称:

ps -ef | grep kworker

例如:

kworker/0:1kworker/1:2kworker/u8:3

5.2 工作队列常用 API

基本 API:

INIT_WORK(work, func);schedule_work(work);queue_work(wq, work);flush_work(work);cancel_work_sync(work);

延迟工作:

INIT_DELAYED_WORK(dwork, func);schedule_delayed_work(dwork, delay);queue_delayed_work(wq, dwork, delay);cancel_delayed_work_sync(dwork);

创建工作队列:

struct workqueue_struct *alloc_workqueue(const char *fmt,unsigned int flags,int max_active, ...);

常用系统默认工作队列:

system_wqsystem_long_wqsystem_unbound_wq

普通驱动简单任务可以直接使用:

schedule_work(&dev->work);

5.3 工作队列示例

#include <linux/interrupt.h>#include <linux/platform_device.h>#include <linux/slab.h>#include <linux/workqueue.h>struct demo_dev {struct device *dev;int irq;struct work_struct work;int event_count;};static void demo_work_fn(struct work_struct *work){struct demo_dev *dev = container_of(work, struct demo_dev, work);/*     * 工作队列运行在进程上下文,     * 可以睡眠,可以加 mutex,     * 可以使用 GFP_KERNEL 分配内存。     */    dev->event_count++;    msleep(10);    mutex_lock(&some_mutex);/* 复杂处理 */    mutex_unlock(&some_mutex);}static irqreturn_t demo_isr(int irq, void *dev_id){struct demo_dev *dev = dev_id;if (!device_has_pending_event(dev))return IRQ_NONE;    ack_device_event(dev);/* 调度工作队列下半部 */    schedule_work(&dev->work);return IRQ_HANDLED;}static int demo_probe(struct platform_device *pdev){struct demo_dev *dev;int irq;int ret;    dev = devm_kzalloc(&pdev->dev, sizeof(*dev), GFP_KERNEL);if (!dev)return -ENOMEM;    irq = platform_get_irq(pdev, 0);if (irq < 0)return irq;    dev->dev = &pdev->dev;    dev->irq = irq;    INIT_WORK(&dev->work, demo_work_fn);    ret = devm_request_irq(&pdev->dev, irq, demo_isr,                           IRQF_SHARED, "demo_irq", dev);if (ret)return ret;    platform_set_drvdata(pdev, dev);return 0;}static int demo_remove(struct platform_device *pdev){struct demo_dev *dev = platform_get_drvdata(pdev);/*     * 等待工作队列任务结束,     * 防止 remove 后工作函数继续访问 dev。     */    cancel_work_sync(&dev->work);return 0;}

5.4 延迟工作示例

struct demo_dev {struct delayed_work dwork;};static void demo_delayed_work_fn(struct work_struct *work){struct delayed_work *dwork = to_delayed_work(work);struct demo_dev *dev = container_of(dwork, struct demo_dev, dwork);/* 延迟任务处理 */}static void demo_start_delayed(struct demo_dev *dev){    schedule_delayed_work(&dev->dwork, msecs_to_jiffies(100));}

初始化:

INIT_DELAYED_WORK(&dev->dwork, demo_delayed_work_fn);

取消:

cancel_delayed_work_sync(&dev->dwork);

5.5 工作队列使用注意事项

工作队列虽然灵活,但也需要注意:

  1. 同一个 work_struct 不应在未取消时重复修改;
  2. queue_work() 如果任务已经 pending,会返回 false;
  3. flush_work() 只等待完成,不会取消任务;
  4. cancel_work_sync() 可能睡眠,不能在中断上下文调用;
  5. 工作函数可以睡眠,但不应无限阻塞;
  6. 驱动卸载前必须取消或 flush 工作队列任务;
  7. 高优先级任务可考虑专用工作队列或 WQ_HIGHPRI。

六、tasklet、softirq、工作队列的区别

6.1 三者关系

三者关系可以这样理解:

softirq  |  +-- tasklet 是 softirq 的一种封装  |workqueue  |  +-- 独立于 softirq,运行在内核线程中

tasklet 基于 softirq,但比 softirq 更易用。工作队列则完全运行在进程上下文中。

6.2 核心区别表

维度
softirq
tasklet
工作队列
运行上下文
软中断上下文
软中断上下文
进程上下文
是否可睡眠
否
否
是
是否可动态创建
否,静态编号
是,可动态初始化
是,可动态创建 work
并发模型
同一 softirq 可在多 CPU 并发
同一 tasklet 实例不并发
由工作队列配置决定
延迟
很低
低
相对较高
编程复杂度
高
中
低
是否适合普通驱动
通常不适合
适合短小快速任务
适合复杂可阻塞任务
典型场景
网络、块设备、定时器核心路径
驱动简单下半部
需要睡眠、I/O、复杂处理
常用 API
open_softirq()
、raise_softirq()
tasklet_setup()
、tasklet_schedule()
INIT_WORK()
、schedule_work()

6.3 运行上下文差异

softirq 和 tasklet 都运行在软中断上下文:

硬中断返回    |    v检查 pending softirq    |    v执行 softirq / tasklet

工作队列不同:

硬中断中调用 schedule_work()    |    v唤醒 kworker 内核线程    |    v进程上下文执行 work 函数

因此,工作队列可以调用:

msleep(10);mutex_lock(&lock);kmalloc(size, GFP_KERNEL);

而 softirq/tasklet 中不能这样做。

6.4 并发差异

softirq 并发最强:

CPU0: softirq handler runningCPU1: softirq handler runningCPU2: softirq handler running

tasklet 对单个实例更友好:

同一个 tasklet 实例不会同时执行两次

但不同 tasklet 实例仍可能并发:

tasklet A on CPU0tasklet B on CPU1

工作队列并发取决于工作队列类型和配置:

bound workqueue:通常绑定 CPUunbound workqueue:不绑定 CPUmax_active:控制并发数量

6.5 选型建议

实际驱动开发中,可以按以下原则选择:

  1. 中断处理必须极快,只做确认和清中断;
  2. 如果后续处理很短且不能睡眠,选择 tasklet;
  3. 如果后续处理需要睡眠、加 mutex、访问 I2C/SPI 寄存器,选择工作队列或线程化中断;
  4. 如果是网络、块设备等核心高性能路径,且具备充分理由,才考虑 softirq;
  5. 普通外设驱动不建议直接注册 softirq;
  6. 新驱动设计中,线程化中断和工作队列往往比 tasklet 更易维护。

七、一个综合示例:中断 + tasklet + 工作队列

下面示例展示一个驱动同时使用 tasklet 和工作队列的设计思路。

#include <linux/interrupt.h>#include <linux/platform_device.h>#include <linux/slab.h>#include <linux/workqueue.h>struct demo_dev {struct device *dev;int irq;struct tasklet_struct fast_tasklet;struct work_struct slow_work;int fast_events;int slow_events;};static void demo_tasklet_fn(struct tasklet_struct *t){struct demo_dev *dev = container_of(t, struct demo_dev, fast_tasklet);    dev->fast_events++;/*     * 快速处理少量数据。     * 如果后续需要复杂处理,可以再调度工作队列。     */    schedule_work(&dev->slow_work);}static void demo_work_fn(struct work_struct *work){struct demo_dev *dev = container_of(work, struct demo_dev, slow_work);    dev->slow_events++;/*     * 进程上下文中可以做复杂处理:     * - 睡眠     * - 加 mutex     - - GFP_KERNEL 分配     * - 访问 I2C/SPI 设备     */}static irqreturn_t demo_isr(int irq, void *dev_id){struct demo_dev *dev = dev_id;if (!device_has_pending_event(dev))return IRQ_NONE;    ack_device_event(dev);    tasklet_schedule(&dev->fast_tasklet);return IRQ_HANDLED;}static int demo_probe(struct platform_device *pdev){struct demo_dev *dev;int irq;int ret;    dev = devm_kzalloc(&pdev->dev, sizeof(*dev), GFP_KERNEL);if (!dev)return -ENOMEM;    irq = platform_get_irq(pdev, 0);if (irq < 0)return irq;    dev->dev = &pdev->dev;    dev->irq = irq;    tasklet_setup(&dev->fast_tasklet, demo_tasklet_fn);    INIT_WORK(&dev->slow_work, demo_work_fn);    ret = devm_request_irq(&pdev->dev, irq, demo_isr,                           IRQF_SHARED, "demo_irq", dev);if (ret)return ret;    platform_set_drvdata(pdev, dev);return 0;}static int demo_remove(struct platform_device *pdev){struct demo_dev *dev = platform_get_drvdata(pdev);    tasklet_kill(&dev->fast_tasklet);    cancel_work_sync(&dev->slow_work);return 0;}

该结构适合以下场景:

  • 硬中断中快速清中断;
  • tasklet 中做低延迟简单处理;
  • 工作队列中做耗时或可睡眠处理。

八、调试与观测方法

8.1 查看中断统计

cat /proc/interrupts

输出示例:

           CPU0       CPU1  29:      12345          0     GICv3  29 Level     timer  33:        100          0     GICv3  33 Level     demo_irq

可以观察:

  • 中断是否触发;
  • 中断是否集中在某个 CPU;
  • 是否存在中断风暴;
  • 共享中断是否有多个设备注册。

8.2 查看 softirq 统计

cat /proc/softirqs

可以观察:

  • NET_RX;
  • NET_TX;
  • TIMER;
  • SCHED;
  • TASKLET;
  • RCU。

如果 TASKLET 计数持续增长,说明 tasklet 被频繁调度。

8.3 查看工作线程

ps -ef | grep kworker

可以观察工作队列线程是否活跃。

8.4 常见问题

常见问题包括:

  1. 中断没有触发;
  2. IRQ 号错误;
  3. 设备树 interrupts 属性配置错误;
  4. 中断触发方式错误;
  5. 共享中断未正确返回 IRQ_NONE;
  6. 中断源未清除;
  7. tasklet 或工作队列访问已释放资源;
  8. 在中断上下文中调用睡眠函数;
  9. 锁使用错误导致死锁;
  10. 下半部处理时间过长导致系统卡顿。

Linux 中断机制的核心目标是快速响应硬件事件,同时避免长时间占用 CPU。为此,内核将中断处理拆分为上半部和下半部。上半部运行在硬中断上下文,必须短小快速;下半部用于延后处理复杂逻辑。

本文梳理了中断机制的关键内容:

  1. 中断是硬件事件通知机制,Linux 通过 IRQ 号、中断控制器、irq_desc 和驱动 ISR 完成中断响应;
  2. request_irq()、devm_request_irq()、request_threaded_irq() 是注册中断的核心 API;
  3. 中断申请流程包括获取 IRQ、编写 ISR、注册中断、释放中断;
  4. tasklet 是基于 softirq 的轻量下半部机制,运行在软中断上下文,不能睡眠,适合短小快速任务;
  5. softirq 是更底层的软中断机制,静态编号、并发能力强,主要用于内核核心子系统,普通驱动不建议直接使用;
  6. 工作队列运行在进程上下文,可以睡眠,适合复杂、耗时、可能阻塞的任务;
  7. 三者选型关键看是否需要睡眠、延迟要求、并发模型和处理复杂度。

实际驱动开发中,推荐遵循以下原则:

硬中断中只做最少工作;短小不可睡眠的延迟任务用 tasklet;需要睡眠或复杂处理用工作队列或线程化中断;softirq 仅在内核核心高性能路径中谨慎使用。

掌握这些机制,有助于设计出既稳定又高效的 Linux 驱动,也能在调试中断丢失、系统卡顿、死锁、资源释放异常等问题时快速定位根因。

阅读其他文章

Linux 内核互斥技术
读写锁和RCU
OpenWrt的动态库与静态库

OpenWrt 补丁(Patch)机制详解

OpenWrt 启动过程深度解析

LuCI的Web UI开发实践

丢包?连接失败?如何对嵌入式Linux做网络诊断

内网穿透 -- 公网访问OpenWRT

OpenWrt的Makefile构建体系

ESP32解析GPS 定位数据

WSL:Windows下的Linux开发利器

OpenWrt 启动过程深度解析

最新文章

随机文章