Linux 驱动开发中,中断机制是最核心、最基础,也最容易暴露设计问题的部分之一。外设产生中断后,CPU 需要立即暂停当前工作,进入中断处理流程;但中断处理又要求尽可能快,否则会影响系统实时性,甚至造成中断丢失。因此,Linux 将中断处理拆分为“上半部”和“下半部”:上半部快速响应硬件,下半部延后处理复杂逻辑。
围绕这一思想,Linux 提供了多种机制:硬中断处理函数、softirq、tasklet、工作队列、线程化中断等。
文章以 Linux 内核通用驱动模型为基础进行说明,适用于字符设备驱动、平台驱动、GPIO 中断、I2C/SPI 设备中断等常见场景。
中断是硬件或软件向 CPU 发出的一种异步事件通知。典型硬件中断来自外设,例如:
CPU 收到中断后,会暂停当前正在执行的代码,保存现场,然后跳转到对应的中断处理入口。处理完成后,再恢复原来的执行流程。
在 Linux 中,中断通常用 IRQ 号表示。IRQ 号并不一定直接等于硬件引脚号,而是经过中断控制器、设备树、irqchip 框架映射后的 Linux 虚拟中断号。
一个典型的中断传递路径如下:
外设 | v中断控制器(GIC / APIC / GPIO irqchip 等) | vCPU 中断入口 | vLinux 中断核心框架 | virq_desc / irq_chip / irqaction | v驱动注册的 ISRLinux 内核代码运行在不同上下文中:
中断上下文中不允许调用以下类型函数:
msleep();schedule();mutex_lock();copy_from_user();copy_to_user();GFP_KERNEL;中断处理函数必须短小、快速、确定性强。
Linux 将中断处理划分为两个阶段:
+----------------------+| 硬件中断 || 上半部 || 快速响应硬件 |+----------------------+ | v+----------------------+| 下半部 || 延后处理复杂逻辑 |+----------------------+上半部通常只做最少量的工作:
下半部负责后续复杂处理:
常见下半部机制包括:
在平台驱动中,中断号通常来自设备树。常见获取方式:
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。
注册中断处理函数最经典的 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 |
常见 flags:
IRQF_SHARED | |
IRQF_TRIGGER_RISING | |
IRQF_TRIGGER_FALLING | |
IRQF_TRIGGER_HIGH | |
IRQF_TRIGGER_LOW | |
IRQF_ONESHOT | |
IRQF_NO_SUSPEND |
触发方式通常由设备树或硬件平台决定,驱动中不建议随意硬编码,除非明确硬件要求。
name 通常用于 /proc/interrupts 中显示:
cat /proc/interruptsdev_id 在共享中断中非常关键。共享中断下,多个设备可能注册同一个 IRQ,内核需要依靠 dev_id 区分不同设备。
释放共享中断时,必须传入与注册时相同的 dev_id:
free_irq(irq, dev_id);因此,dev_id 通常传入设备私有结构体指针。
一个标准中断申请流程如下:
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;}共享中断下,中断处理函数必须能够判断中断是否来自本设备。如果无法判断,就可能出现以下问题:
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;}非 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;}除了传统下半部机制,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 是 Linux 中一种基于 softirq 的延迟处理机制。它运行在软中断上下文,不能睡眠,但比工作队列更轻量,延迟更低。
tasklet 的特点:
tasklet 本质上属于 softirq 的一种,对应:
TASKLET_SOFTIRQ因此,在 /proc/softirqs 中可以看到 TASKLET 相关计数。
较新的内核推荐使用:
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() 获取私有数据。
下面给出一个完整结构示例。
#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;}tasklet 虽然使用简单,但有几个限制必须注意:
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 是 Linux 内核中更底层的软中断机制。tasklet 就是建立在 softirq 之上的封装。
softirq 的特点:
内核中常见的 softirq 类型包括:
HI_SOFTIRQTIMER_SOFTIRQNET_TX_SOFTIRQNET_RX_SOFTIRQBLOCK_SOFTIRQIRQ_POLL_SOFTIRQTASKLET_SOFTIRQSCHED_SOFTIRQHRTIMER_SOFTIRQRCU_SOFTIRQ可以通过以下命令查看 softirq 统计:
cat /proc/softirqssoftirq 通常在以下时机执行:
local_bh_enable() 被调用时;ksoftirqd 内核线程中。当 softirq 数量过多或持续触发时,内核会交给 ksoftirqd 线程处理,避免长时间占用 CPU 导致用户态任务饿死。
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 执行。
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 统计中。
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);}softirq 适合内核核心路径,不适合普通外设驱动。原因包括:
因此,驱动开发中如果需要 softirq 级别的延迟,通常优先考虑 tasklet;如果需要睡眠,则选择工作队列或线程化中断。
工作队列将任务交给内核工作线程执行。工作线程运行在进程上下文中,因此工作队列可以睡眠、可以阻塞、可以调用可能睡眠的函数。
工作队列特点:
mutex_lock();GFP_KERNEL 分配内存;常见内核工作线程名称:
ps -ef | grep kworker例如:
kworker/0:1kworker/1:2kworker/u8:3基本 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);#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;}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);工作队列虽然灵活,但也需要注意:
work_struct 不应在未取消时重复修改;queue_work() 如果任务已经 pending,会返回 false;flush_work() 只等待完成,不会取消任务;cancel_work_sync() 可能睡眠,不能在中断上下文调用;WQ_HIGHPRI。三者关系可以这样理解:
softirq | +-- tasklet 是 softirq 的一种封装 |workqueue | +-- 独立于 softirq,运行在内核线程中tasklet 基于 softirq,但比 softirq 更易用。工作队列则完全运行在进程上下文中。
open_softirq()raise_softirq() | tasklet_setup()tasklet_schedule() | INIT_WORK()schedule_work() |
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 中不能这样做。
softirq 并发最强:
CPU0: softirq handler runningCPU1: softirq handler runningCPU2: softirq handler runningtasklet 对单个实例更友好:
同一个 tasklet 实例不会同时执行两次但不同 tasklet 实例仍可能并发:
tasklet A on CPU0tasklet B on CPU1工作队列并发取决于工作队列类型和配置:
bound workqueue:通常绑定 CPUunbound workqueue:不绑定 CPUmax_active:控制并发数量实际驱动开发中,可以按以下原则选择:
下面示例展示一个驱动同时使用 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;}该结构适合以下场景:
cat /proc/interrupts输出示例:
CPU0 CPU1 29: 12345 0 GICv3 29 Level timer 33: 100 0 GICv3 33 Level demo_irq可以观察:
cat /proc/softirqs可以观察:
如果 TASKLET 计数持续增长,说明 tasklet 被频繁调度。
ps -ef | grep kworker可以观察工作队列线程是否活跃。
常见问题包括:
IRQ_NONE;Linux 中断机制的核心目标是快速响应硬件事件,同时避免长时间占用 CPU。为此,内核将中断处理拆分为上半部和下半部。上半部运行在硬中断上下文,必须短小快速;下半部用于延后处理复杂逻辑。
本文梳理了中断机制的关键内容:
request_irq()、devm_request_irq()、request_threaded_irq() 是注册中断的核心 API;实际驱动开发中,推荐遵循以下原则:
硬中断中只做最少工作;短小不可睡眠的延迟任务用 tasklet;需要睡眠或复杂处理用工作队列或线程化中断;softirq 仅在内核核心高性能路径中谨慎使用。掌握这些机制,有助于设计出既稳定又高效的 Linux 驱动,也能在调试中断丢失、系统卡顿、死锁、资源释放异常等问题时快速定位根因。