目录
[线程 IRQ(Threaded IRQ)](#线程 IRQ(Threaded IRQ))
[使用 request_irq() 注册共享中断](#使用 request_irq() 注册共享中断)
序言
在Linux驱动开发中,有着许许多多的进程,每一个线程可以代表需要执行的函数(任务),而CPU总是在这些线程中来回切换执行不同的线程(详情看OSTEP 第四章:线程),每一个线程都需要CPU执行。
先简单的理解一下中断的概念,比如你正在玩着游戏,突然有人敲门告诉你外卖到了,这时你就需要暂时离开你的电脑,到门外去拿外卖,然后回来继续玩游戏。
这里就是我们现实中的一个中断,由外界(硬件),内部(软件)打断当前事务,有限处理紧急事务,中断是硬件或软件发出的信号,通知 CPU 需要立即处理某个事件。硬件中断由外设(如网卡、键盘)触发,软件中断由程序调用(如系统调用)触发。
1.中断的概念
在Kernel内核中,中断被分为上半部和下半部(其实也不是一定要分成两部分,实际上简短的就直接上半部执行完了,或者直接将不紧急任务放入下半部分)
上半部:用于处理比较紧急的任务,例如简单地读取寄存器中的中断状态并清除中断标志后就进行"登记中断"的工作,但是上半部分不允许被中断!
ps:为什么上半部分不能被中断?因为没有其对应的中断上下文,当其进入中断后就无法被唤醒了,轻则死锁,严重时甚至会造成内核崩溃。
下半部:用于处理时间较长的任务(是中断的重心,基本大部分任务都在这执行),下半部几乎做了中断处理程序所有的事情,而且可以被新的中断打断。
2.如何使用中断
很多文章都喜欢把被注册(被调用)的函数放在前面去讲解,这样看起来其实还是有些奇怪的,这里我将先讲解注册,再去讲解实际的调用函数。
中断处理流程
-
中断触发:设备向 CPU 发送中断信号。
-
中断服务例程(ISR) :执行注册的中断处理函数(上半部)。
-
下半部机制 :处理耗时操作(如
tasklet
、workqueue
)。
中断上下文限制
-
不可睡眠 :禁止调用可能阻塞的函数(如
kmalloc(GFP_KERNEL),但其他参数部分可用
)。PS:为什么不能使用kmalloc(GFP_KERNEL)呢?因为当内存不足时,内核会对分配的内存进行回收,会触发中断 -
快速执行:ISR 应尽可能简短,避免延迟其他中断。
统一的irqreturn_t的返回值:
-
IRQ_NONE
:未处理该中断。 -
IRQ_HANDLED
:已处理中断。
屏蔽中断/使能
屏蔽中断是指该下半部中断程序在运行时不允许设备再次触发中断。
为什么要屏蔽中断?
-
防止中断重入:在处理当前中断时,避免同一中断反复触发
-
硬件约束:某些设备要求先禁用中断才能清除状态寄存器
-
避免中断风暴(如按键抖动导致的连续中断)
当上半部处理完后,将屏蔽中断,等待下半部处理完后使能中断,允许设备再次触发中断程序。
在 Linux 内核层面,还提供了以下 API 用于中断管理:
cpp
void disable_irq(unsigned int irq); // 禁用指定中断线(同步等待未完成中断)
void disable_irq_nosync(unsigned int irq); // 异步禁用
void enable_irq(unsigned int irq); // 重新启用中断
实际驱动中的典型用法:
cpp
static irqreturn_t my_interrupt_handler(int irq, void *dev_id)
{
struct my_device *dev = dev_id;
/* 1. 禁用设备硬件中断 */
writel(dev->reg_base + INT_ENABLE_REG, 0x0);
/* 2. 禁用内核中断线(可选)*/
disable_irq_nosync(irq);
/* 3. 调度下半部 */
schedule_work(&dev->work);
return IRQ_HANDLED;
}
static void my_work_fn(struct work_struct *work)
{
struct my_device *dev = container_of(work, struct my_device, work);
/* 处理实际任务... */
/* 重新启用中断 */
writel(dev->reg_base + INT_ENABLE_REG, INT_ENABLE_BIT);
enable_irq(dev->irq); // 如果之前调用了 disable_irq
}
关键区别与选择
操作对象 | 硬件级控制 | 内核级控制 |
---|---|---|
作用范围 | 只影响当前设备 | 影响共享该中断线的所有设备 |
延迟 | 立即生效 | 可能有延迟(依赖内核调度) |
典型使用场景 | 防止设备自身的中断风暴 | 协调共享中断线的多设备 |
是否需要成对调用 | 必须成对(enable/disable) | 必须成对(enable/disable) |
上半部中断
当我们要实现一个中断或者调用中断处理函数的时候,第一步应该先要在模块中注册该中断及其处理函数。
在 Linux 设备驱动中,使用中断的设备需要申请和释放对应的中断 ,分别使用内核提供的 **request_irq()和 free_irq()**函数,该注册只注册上半部分。
cpp
int request_irq(unsigned int irq, void (*handler)(int irq, void *dev_id, struct pt_regs *regs),
unsigned long irqflags,
const char * devname,
void *dev_id);
函数返回值: 0 表示成功,或返回一个负的错误码,如 -EBUSY 表示另一个驱动已经占用了你所请求的中断线。
参数详解:
-
unsigned int irq:要申请的中断号
-
irqreturn_t (*handler)(int,void *,struct pt_regs *): handler是向系统登记的中断处理函数,是一个回调函数,中断发生时,系统调用这个函数,dev_id 参数将被传递给它。
cppirqreturn_t my_irq_handler(int irq, void *dev_id) { printk(KERN_INFO "IRQ %d triggered\n", irq); /* 快速处理中断,如读取硬件状态、清除中断标志 */ return IRQ_HANDLED; }
-
unsigned long flags: flags 是中断处理的属性
-
const char *dev_name:传递给request_irq的字符串,用来在/proc/interrupts显示中断的拥有者。
-
void *dev_id:用于共享的中断信号线,它是唯一的标识,在中断线空闲时可以使用它,驱动程序也可以用它来指向自己的私有数据区(来标识哪个设备产生中断)。若中断没有被共享,dev_id 可以设置为 NULL,但推荐用它指向设备的数据结构。
释放IRQ函数,request_irq()相对应的函数为 free_irq(),free_irq()的原型如下:
void free_irq(unsigned int irq,void *dev_id);
free_irq()中参数的定义与 request_irq()相同。
下半部中断
用于处理大部分耗时任务,有多种api和参数适应不同的场景
软中断(SoftIRQ)
-
概念
软中断是内核中实现延迟处理的一种机制,它的思想是将一些不需要在硬中断上下文中立即完成的工作延后处理。软中断是在中断上下文中执行的,属于内核调度的下半部,但仍然运行于中断上下文中,因此不允许调用可能导致睡眠的函数。
-
局限性
软中断不能睡眠,因为它运行在中断上下文中。同时,软中断通常处理的任务比较简单,比如网络包的快速处理、定时器更新等。
软中断是内核提供的一种延迟处理机制,主要用于处理网络、定时器等高频任务。
-
主要 API:
-
open_softirq()
:注册软中断处理函数。 -
raise_softirq()
:触发软中断。
-
cpp
void open_softirq(int nr, void (*action)(struct softirq_action *))
local_bh_disable() 和 local_bh_enable() 用于临界区保护,禁止/允许当前 CPU 上的软中断和 tasklet 调度。
-
功能
这两个函数用于在当前 CPU 上禁止或使能软中断和 tasklet 底半部的执行:
-
local_bh_disable():禁止当前 CPU 上的软中断和 tasklet 执行,通常用于保护临界区。
-
local_bh_enable():恢复软中断和 tasklet 的执行。
-
-
使用场景
在某些情况下,你需要在执行临界区代码时确保不会被软中断打断,这时可以调用 local_bh_disable();代码执行完毕后,调用 local_bh_enable() 恢复软中断处理。
注意:
软中断通常由内核内部使用,驱动开发者很少直接使用软中断,更多是使用其上层的 Tasklet 或工作队列。
小任务(Tasklet)
Tasklet 是基于软中断实现的轻量级延迟处理机制,适合处理简单且较短的延迟任务。
其中还可以引用一个**tasklet_schedule()**函数就能使系统在适当的时候进行调度运行。
主要 API:
-
初始化:
-
tasklet_init(struct tasklet_struct *t, void (*func)(unsigned long), unsigned long data);
-
或使用宏
DECLARE_TASKLET(name, func, data);
-
-
调度执行:
tasklet_schedule(struct tasklet_struct *t);
-
禁用/使能:
-
tasklet_disable(struct tasklet_struct *t);
-
tasklet_enable(struct tasklet_struct *t);
-
-
销毁:
tasklet_kill(struct tasklet_struct *t);
cpp
#include <linux/module.h>
#include <linux/interrupt.h>
static void my_tasklet_handler(unsigned long data)
{
printk(KERN_INFO "Tasklet handler executed, data=%lu\n", data);
}
/* 声明并初始化 tasklet */
DECLARE_TASKLET(my_tasklet, my_tasklet_handler, 100);
static irqreturn_t my_irq_handler(int irq, void *dev_id)
{
printk(KERN_INFO "IRQ %d received, scheduling tasklet\n", irq);
tasklet_schedule(&my_tasklet);
return IRQ_HANDLED;
}
static int __init my_driver_init(void)
{
int ret;
ret = request_irq(42, my_irq_handler, IRQF_SHARED | IRQF_TRIGGER_RISING,
"my_tasklet_driver", (void *)"my_tasklet_driver");
if (ret) {
printk(KERN_ERR "Failed to request IRQ\n");
return ret;
}
printk(KERN_INFO "Driver initialized\n");
return 0;
}
static void __exit my_driver_exit(void)
{
free_irq(42, (void *)"my_tasklet_driver");
/* 确保 tasklet 结束 */
tasklet_kill(&my_tasklet);
printk(KERN_INFO "Driver exited\n");
}
module_init(my_driver_init);
module_exit(my_driver_exit);
MODULE_LICENSE("GPL");
说明:
上例中,中断上半部收到中断后调用 tasklet_schedule()
,调度下半部 tasklet 执行 my_tasklet_handler()
,在下半部中可以执行稍长的、允许休眠的操作(尽管 tasklet 本身仍在软中断上下文中,不能调用可能休眠的函数)。
工作队列(Workqueue)
工作队列提供一种更灵活的下半部机制,它在进程上下文中运行,可以调用可能休眠的函数。
主要 API:
-
初始化工作项:
INIT_WORK(struct work_struct *work, void (*func)(struct work_struct *));
-
调度工作项:
schedule_work(struct work_struct *work);
-
等待工作完成:
flush_work(struct work_struct *work);
-
创建和销毁自定义工作队列:
-
create_workqueue()
-
destroy_workqueue()
-
cpp
#include <linux/module.h>
#include <linux/workqueue.h>
#include <linux/interrupt.h>
static struct work_struct my_work;
/* 工作项处理函数 */
static void my_work_handler(struct work_struct *work)
{
printk(KERN_INFO "Workqueue handler executed\n");
}
static irqreturn_t my_irq_handler(int irq, void *dev_id)
{
printk(KERN_INFO "IRQ %d received, scheduling workqueue\n", irq);
schedule_work(&my_work);
return IRQ_HANDLED;
}
static int __init my_driver_init(void)
{
int ret;
INIT_WORK(&my_work, my_work_handler);
ret = request_irq(42, my_irq_handler, IRQF_SHARED | IRQF_TRIGGER_RISING,
"my_wq_driver", (void *)"my_wq_driver");
if (ret) {
printk(KERN_ERR "Failed to request IRQ\n");
return ret;
}
printk(KERN_INFO "Driver with workqueue initialized\n");
return 0;
}
static void __exit my_driver_exit(void)
{
free_irq(42, (void *)"my_wq_driver");
/* 等待所有工作完成 */
flush_work(&my_work);
printk(KERN_INFO "Driver with workqueue exited\n");
}
module_init(my_driver_init);
module_exit(my_driver_exit);
MODULE_LICENSE("GPL");
说明:
这里,INIT_WORK()
初始化工作项,schedule_work()
用于调度工作项在进程上下文中执行,从而可以调用会休眠的函数(比如内存分配、I/O 处理等)。
线程 IRQ(Threaded IRQ)
线程 IRQ 是一种特殊的中断处理方式,通过 request_threaded_irq()
将中断处理分为两部分:
-
上半部 :快速响应中断,返回
IRQ_WAKE_THREAD
,由内核调度一个专用线程执行下半部。 -
下半部:在线程上下文中运行,允许休眠,执行复杂或耗时的操作。
-
功能:支持线程化中断处理,将中断处理分为上半部(快速响应)和下半部(在内核线程中执行耗时操作)。
-
优势:
-
下半部在独立内核线程中运行,允许使用阻塞操作(如睡眠或互斥锁)。
-
减少中断禁用时间,提升系统实时性。
-
适用场景:需要复杂处理或长时间任务的中断,例如涉及协议栈处理或硬件状态轮询
cpp
int request_threaded_irq(unsigned int irq,
irq_handler_t handler, // 上半部
irq_handler_t thread_fn, // 下半部线程函数
unsigned long flags,
const char *name,
void *dev_id);
- irq 要注册的中断号(通常从设备树或平台数据中获取)。
- handler 上半部中断处理函数,也称为主 ISR。它在中断上下文中运行,要求执行尽可能快,不允许睡眠。如果中断处理逻辑非常简单,可以在此阶段完成。如果需要较长时间的处理,则应返回
IRQ_WAKE_THREAD
以唤醒线程化的下半部处理函数。也可以将此参数设为NULL
,这样内核会默认使用一个内置的"dummy"处理函数(相当于直接返回IRQ_WAKE_THREAD
),从而所有的处理都转到线程化部分执行。 - thread_fn下半部线程处理函数,即线程化 IRQ 处理函数。它在一个内核线程(线程上下文)中运行,因此允许调用可能会睡眠或进行耗时操作的函数。
- 当主 ISR 返回
IRQ_WAKE_THREAD
时,内核会调度此线程处理函数执行。 - 线程函数的原型与上半部类似:
irqreturn_t thread_fn(int irq, void *dev_id);
- flags 用于指定中断注册的属性。常见的标志包括:
IRQF_SHARED
:允许该中断线被多个设备共享。如果设备共享中断,dev_id
必须非 NULL,并且在释放中断时需要传入相同的dev_id
。触发方式标志,如IRQF_TRIGGER_RISING
、IRQF_TRIGGER_FALLING
、IRQF_TRIGGER_HIGH
、IRQF_TRIGGER_LOW
等(根据硬件要求选择)。 - name 用于标识中断的名称,通常会显示在
/proc/interrupts
中。 - dev_id设备标识指针。对于共享中断,此参数用于区分不同设备的中断处理程序,也便于在释放中断时准确匹配。
释放IRQ函数,也相对应的函数为 free_irq()。
下半部机制对比
机制 | 执行上下文 | 并发性 | 延迟 | 适用场景 |
---|---|---|---|---|
softirq | 中断上下文 | 可跨 CPU 并行 | 极低 | 高频核心任务(网络、块设备) |
tasklet | 中断上下文 | 同一 tasklet 串行 | 低 | 驱动中的通用异步任务 |
workqueue | 进程上下文 | 可睡眠、调度灵活 | 较高 | 复杂或需睡眠的任务 |
3.其余中断的一些概念
中断共享
中断共享(Interrupt Sharing)是指多个设备(或驱动实例)在同一条物理中断线上注册中断处理程序 ,这样当该中断发生时,内核会依次调用所有注册的处理函数。下面详细讲解其原理、实现方式、注意事项和常见问题:
-
物理限制与成本考量
在硬件设计中,中断资源通常有限(例如旧的 x86 系统可能只有 16 条 IRQ 线),为了充分利用这些有限资源,多个设备往往共享同一条 IRQ 信号线。
-
共享中断的工作机制
当一个共享中断发生时,内核会在对应的中断处理链表中调用所有注册的处理程序。每个处理程序需要检查硬件状态或标识信息,判断这次中断是否真正与自己的设备相关。如果不相关,则返回IRQ_NONE
;如果相关,则执行必要操作后返回IRQ_HANDLED
。
使用 request_irq() 注册共享中断
在注册中断时,必须在 flags 参数中加入 IRQF_SHARED
标志,并且 dev_id 参数不能为 NULL。dev_id 用于在共享中断时标识调用者,确保在释放中断时能够正确匹配。
cpp
int ret;
ret = request_irq(irq_number, my_irq_handler,
IRQF_SHARED | <其他标志>,
"my_device", &my_device_data);
if (ret) {
printk(KERN_ERR "Failed to request IRQ %d, error %d\n", irq_number, ret);
return ret;
}
中断处理函数中的注意事项
由于共享中断时,不同设备可能会在同一 IRQ 线上产生中断,每个处理程序必须:
-
检查中断来源 :在 ISR 中需要读取硬件状态寄存器或者其他标识,判断这次中断是否是自己设备产生的。如果不是,则立即返回
IRQ_NONE
。 -
返回值约定 :如果一个处理程序确定中断确实是由其设备产生,应返回
IRQ_HANDLED
;如果不确定或不是目标设备,则返回IRQ_NONE
。这有助于内核统计"伪中断"(spurious interrupt)。
示例:
cpp
irqreturn_t my_irq_handler(int irq, void *dev_id)
{
struct my_device *dev = dev_id;
/* 检查硬件状态判断是否是本设备中断 */
if (!is_my_device_interrupt(dev)) {
return IRQ_NONE;
}
/* 处理中断:例如读取数据、清除中断状态等 */
handle_my_interrupt(dev);
return IRQ_HANDLED;
}
释放共享中断
在释放中断时,必须调用 free_irq()
并传入与注册时相同的 irq
和 dev_id
:
cpp
free_irq(irq_number, &my_device_data);
内核会遍历共享该中断号的所有处理程序,并释放 dev_id 与传入参数匹配的那一个。
常见问题与注意事项
-
dev_id 必须非 NULL :
当使用
IRQF_SHARED
时,如果传入 NULL,内核将无法区分不同设备的处理程序,可能会导致释放中断资源时出现错误。 -
竞态条件 :
由于同一中断可能被多个设备共享,因此各个 ISR 必须小心设计,确保对共享硬件寄存器或数据的访问正确同步。
-
性能问题 :
如果共享的中断上注册了大量处理程序,每次中断发生时内核都会调用所有处理程序,这可能会影响响应时间,因此共享中断应尽量控制注册数目,确保每个处理程序尽快返回。
-
调试 :
检查
/proc/interrupts
文件,可以看到各个 IRQ 线的使用情况和共享状态,这有助于确认设备是否正确共享了中断。
中断驱动I/O
优势
-
提高效率:CPU 只在设备有数据时被中断,不需要持续轮询设备状态,从而节省 CPU 资源。
-
响应及时:外设立即通知 CPU,有助于快速响应实时事件(如网络数据、输入设备事件)。
-
异步处理:中断驱动 I/O 允许数据处理在中断发生时异步进行,使系统整体性能更高。
当与驱动程序管理的硬件间的数据传送可能因为某种原因而延迟,驱动编写者应当实现缓存。一个好的缓存机制需采用中断驱动的I/O,一个输入缓存在中断时被填充,并由读取设备的进程取走缓冲区的数据,一个输出缓存由写设备的进程填充,并在中断时送出数据。
为正确进行中断驱动的数据传送,硬件应能够按照下列语义产生中断:
- 输入:当新数据到达时并处理器准备好接受时,设备中断处理器。
- 输出:当设备准备好接受新数据或确认一个成功的数据传送时,设备产生中断。
4.总结
实际开发中的常见流程:
-
设备初始化
在驱动的 probe() 中,根据平台数据或设备树获取中断号,调用
request_irq()
或request_threaded_irq()
注册中断处理函数。 -
中断处理
上半部 ISR 快速响应中断,必要时调度下半部处理(如 tasklet 或工作队列)完成复杂操作。
-
数据传输
根据设备特性,中断处理可能涉及读取硬件数据,将数据存入驱动内部缓冲区,等待用户空间通过 read() 接口获取。
-
资源释放
在设备关闭或驱动卸载时,调用
free_irq()
释放中断资源,并确保下半部任务(如 tasklet 或工作队列)已结束。
具体的调试需要查看 /proc/interrupts
:可以验证 IRQ 是否正确注册、共享情况及中断计数,或者使用printk(不推荐)输出调试信息,记录ISR和下半部分执行情况,最后就是使用动态调试机制,如pr_debug()和内核动态调试。
最后还是一句话,实践出真理,不自己实验一下永远只是概念,如果将来内核有朝一日更新了其中的API可以告诉我。