在 Linux 内核中,中断处理被划分为两个主要部分:
- 顶半部 (Top Half) / 硬中断 (Hard IRQ): 这是中断发生时首先执行的部分。它必须非常快速地完成,因为它会禁用中断(至少是同优先级或更低优先级的中断),过长的执行时间会影响系统的响应性和实时性。顶半部的主要任务是完成对硬件的最小必要操作(如清除中断状态、读取关键数据)并调度底半部。
- 底半部 (Bottom Half): 在顶半部完成后,由内核在稍后、更安全(中断通常是开启的)的上下文中执行。底半部可以执行更耗时、更复杂的操作,例如处理大部分中断数据、与进程上下文交互等。Linux 内核提供了几种不同的机制来实现底半部,包括软中断 (SoftIRQ)、Tasklets 和工作队列 (Workqueues)。
下面详细介绍与这些中断处理机制相关的函数及其使用。
1. 中断请求与释放
在设备驱动程序中使用中断之前,需要向内核注册中断处理函数,并在不再需要时释放它。
request_irq()
- 功能: 注册一个中断处理程序。
- 头文件:
<linux/interrupt.h>
- 使用场景: 设备驱动程序在初始化时,需要为设备的中断线注册一个处理函数,以便在设备产生中断时内核能够调用这个函数。
- 函数原型:
int request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags, const char *name, void *dev_id);
- 参数说明:
irq
: 要注册的中断号。handler
: 中断处理函数的指针。这个函数就是顶半部。flags
: 中断处理的标志位,用于指定中断的特性和行为。常用的标志包括:IRQF_SHARED
: 允许多个设备共享同一个中断线。如果设置此标志,dev_id
参数必须是唯一的,用于区分共享中断的设备。IRQF_TRIGGER_RISING
: 中断由上升沿触发。IRQF_TRIGGER_FALLING
: 中断由下降沿触发。IRQF_TRIGGER_HIGH
: 中断由高电平触发。IRQF_TRIGGER_LOW
: 中断由低电平触发。IRQF_NO_SUSPEND
: 在系统挂起期间不禁用此中断。IRQF_ONESHOT
: 如果中断处理程序返回IRQ_WAKE_THREAD
,中断线会被禁用,直到中断线程完成。
name
: 中断的名称字符串,通常是设备名称,用于/proc/interrupts
文件中显示。dev_id
: 一个唯一的指针,当使用IRQF_SHARED
标志时,用于在中断处理函数中区分不同的设备。在释放中断时也需要提供这个指针。如果不是共享中断,可以传递NULL
。
- 返回值: 成功返回 0,失败返回负的错误码。
- 中断处理函数原型 (
irq_handler_t
):typedef irqreturn_t (*irq_handler_t)(int, void *);
- 第一个参数是中断号。
- 第二个参数是
request_irq
传递的dev_id
。 - 返回值类型
irqreturn_t
:IRQ_NONE
: 中断不是由本设备产生的(通常用于共享中断)。IRQ_HANDLED
: 中断已由本设备处理。IRQ_WAKE_THREAD
: 中断已处理,并且需要唤醒中断线程(与IRQF_ONESHOT
标志配合使用)。
free_irq()
- 功能: 释放之前注册的中断处理程序。
- 头文件:
<linux/interrupt.h>
- 使用场景: 设备驱动程序在卸载或设备移除时,需要释放占用的中断资源。
- 函数原型:
void free_irq(unsigned int irq, void *dev_id);
- 参数说明:
irq
: 要释放的中断号。dev_id
: 注册时使用的dev_id
。如果注册时dev_id
是NULL
,这里也传NULL
。对于共享中断,这个参数是必需的,用于找到对应的处理函数。
2. 中断的启用与禁用
有时需要在特定的代码段内临时禁用或启用中断,以保护临界区。
local_irq_disable()
- 功能: 禁用当前 CPU 的本地中断。
- 头文件:
<asm/irqflags.h>
(通常通过<linux/interrupt.h>
或<linux/irqflags.h>
间接包含) - 使用场景: 在访问只被当前 CPU 访问但可能被中断处理程序修改的数据时,或者在获取自旋锁之前(如果锁可能被中断处理程序获取)。
- 使用方式:
local_irq_disable(); // 临界区代码,中断在此期间被禁用 local_irq_enable();
- 注意: 禁用中断会阻止所有中断的发生,包括时钟中断,可能影响系统的调度和时间精度。应尽量缩短禁用中断的时间。
local_irq_enable()
- 功能: 启用当前 CPU 的本地中断。
- 头文件:
<asm/irqflags.h>
- 使用场景: 与
local_irq_disable()
配对使用,在临界区结束后恢复中断。 - 使用方式: 见
local_irq_disable()
。
local_irq_save(flags)
- 功能: 保存当前 CPU 的中断状态(是否启用)到
flags
变量,并禁用本地中断。 - 头文件:
<asm/irqflags.h>
- 使用场景: 当你不确定进入某个代码段时中断是启用还是禁用的,但需要在该代码段内禁用中断,并在退出时恢复到进入前的状态。这比简单的
local_irq_disable
/local_irq_enable
更安全,尤其是在函数嵌套调用的情况下。常与自旋锁结合使用 (spin_lock_irqsave
)。 - 使用方式:
unsigned long flags; local_irq_save(flags); // 临界区代码,中断在此期间被禁用 local_irq_restore(flags);
- 功能: 保存当前 CPU 的中断状态(是否启用)到
local_irq_restore(flags)
- 功能: 恢复当前 CPU 的中断状态到
flags
变量保存的值。 - 头文件:
<asm/irqflags.h>
- 使用场景: 与
local_irq_save(flags)
配对使用,恢复中断状态。 - 使用方式: 见
local_irq_save(flags)
。
- 功能: 恢复当前 CPU 的中断状态到
disable_irq(irq)
- 功能: 禁用特定的中断线(在所有 CPU 上)。这个函数会等待当前正在执行的中断处理程序完成。
- 头文件:
<linux/interrupt.h>
- 使用场景: 在进行可能干扰特定设备中断的硬件配置或状态修改时。
- 使用方式:
disable_irq(MY_DEVICE_IRQ); // 进行设备配置 enable_irq(MY_DEVICE_IRQ);
- 注意: 这个函数可能会睡眠,因此不能在中断上下文或持有自旋锁时调用。
disable_irq_nosync(irq)
- 功能: 禁用特定的中断线(在所有 CPU 上),但不等待当前正在执行的中断处理程序完成。
- 头文件:
<linux/interrupt.h>
- 使用场景: 当你确定不需要等待当前中断处理程序完成时,可以使用这个函数以避免潜在的睡眠。
- 使用方式: 与
disable_irq
类似,但通常在调用后需要额外的同步机制来确保中断处理程序确实已经退出。
enable_irq(irq)
- 功能: 启用特定的中断线(在所有 CPU 上)。
- 头文件:
<linux/interrupt.h>
- 使用场景: 与
disable_irq
或disable_irq_nosync
配对使用,重新启用中断线。 - 使用方式: 见
disable_irq
。
3. 底半部机制相关函数
底半部处理允许将中断处理中耗时的部分推迟到更合适的时机执行。
3.1 软中断 (SoftIRQ)
软中断是一组静态定义的中断处理机制,运行在特定的软中断上下文中。它们是所有底半部机制中执行最快的,但使用起来也最复杂,通常只用于时间要求最严格的场景(如网络子系统)。
DECLARE_SOFTIRQ(nr, handler)
- 功能: 静态声明一个软中断。
- 头文件:
<linux/interrupt.h>
- 使用场景: 定义新的软中断类型(不常见,通常使用内核已有的软中断类型)。
- 使用方式:
// 声明一个名为 MY_SOFTIRQ 的软中断,编号为 MY_SOFTIRQ_NR,处理函数为 my_softirq_handler DECLARE_SOFTIRQ(MY_SOFTIRQ_NR, my_softirq_handler);
raise_softirq(nr)
- 功能: 标记一个软中断为待处理状态。
- 头文件:
<linux/interrupt.h>
- 使用场景: 在顶半部中断处理程序中,完成快速操作后,调度相应的软中断底半部执行。
- 使用方式:
// 在顶半部中调用,标记 MY_SOFTIRQ_NR 软中断待处理 raise_softirq(MY_SOFTIRQ_NR);
- 注意:
raise_softirq
只是设置一个标志,实际的软中断处理会在合适的时机(如中断返回、进程调度等)由内核触发。
local_bh_disable()
- 功能: 禁用当前 CPU 的底半部(包括软中断和 Tasklets)。
- 头文件:
<linux/interrupt.h>
- 使用场景: 在访问可能被软中断或 Tasklet 修改的数据时,保护临界区。
- 使用方式:
local_bh_disable(); // 临界区代码,底半部在此期间被禁用 local_bh_enable();
local_bh_enable()
- 功能: 启用当前 CPU 的底半部。
- 头文件:
<linux/interrupt.h>
- 使用场景: 与
local_bh_disable()
配对使用,恢复底半部执行。 - 使用方式: 见
local_bh_disable()
。
3.2 Tasklets
Tasklets 是构建在软中断之上的更易用的底半部机制。它们是动态创建的,同一个 Tasklet 不会在多个 CPU 上同时运行,但不同的 Tasklet 可以在不同的 CPU 上并行运行。
DECLARE_TASKLET(name, func, data)
- 功能: 静态声明并初始化一个 Tasklet。
- 头文件:
<linux/interrupt.h>
- 使用场景: 定义一个 Tasklet 变量,通常用于在模块加载时初始化。
- 参数说明:
name
: Tasklet 变量的名称。func
: Tasklet 的处理函数指针。data
: 传递给处理函数的数据。
- 使用方式:
// 声明一个名为 my_tasklet 的 Tasklet,处理函数为 my_tasklet_handler,传递数据 0 DECLARE_TASKLET(my_tasklet, my_tasklet_handler, 0);
DECLARE_TASKLET_DISABLED(name, func, data)
- 功能: 静态声明并初始化一个禁用的 Tasklet。
- 头文件:
<linux/interrupt.h>
- 使用场景: 初始化时不需要立即运行的 Tasklet。需要调用
tasklet_enable()
才能使其可调度。
tasklet_init(t, func, data)
- 功能: 动态初始化一个 Tasklet。
- 头文件:
<linux/interrupt.h>
- 使用场景: 在运行时动态创建 Tasklet(例如在设备插入时)。
- 参数说明:
t
: 指向 Tasklet 结构体的指针。func
: Tasklet 的处理函数指针。data
: 传递给处理函数的数据。
- 使用方式:
struct tasklet_struct *my_tasklet_ptr;
my_tasklet_ptr = kmalloc(sizeof(*my_tasklet_ptr), GFP_KERNEL);
if (my_tasklet_ptr)
{
tasklet_init(my_tasklet_ptr, my_tasklet_handler, (unsigned long)my_device_data);
}
tasklet_schedule(t)
- 功能: 调度一个 Tasklet 执行。如果 Tasklet 当前没有运行且未被调度,它会被标记为待处理,并在合适的时机(通常是软中断上下文)执行。
- 头文件:
<linux/interrupt.h>
- 使用场景: 在顶半部中断处理程序中,将需要推迟执行的任务调度给 Tasklet。
- 使用方式:
// 在顶半部中调用,调度 my_tasklet 执行 tasklet_schedule(&my_tasklet);
- 注意: 可以从中断上下文或进程上下文调用
tasklet_schedule
。如果 Tasklet 已经在运行,再次调用tasklet_schedule
不会立即启动新的实例,而是在当前实例完成后再次调度。
tasklet_hi_schedule(t)
- 功能: 调度一个高优先级的 Tasklet 执行。高优先级 Tasklet 会在普通 Tasklet 之前执行。
- 头文件:
<linux/interrupt.h>
- 使用场景: 对延迟要求较高的底半部任务。
tasklet_kill(t)
- 功能: 停止一个 Tasklet 的执行,并等待它完成。
- 头文件:
<linux/interrupt.h>
- 使用场景: 在模块卸载或设备移除时,确保 Tasklet 不再运行。
- 使用方式:
tasklet_kill(&my_tasklet);
- 注意: 这个函数可能会睡眠,因此不能在中断上下文或持有自旋锁时调用。
tasklet_disable(t)
- 功能: 禁用一个 Tasklet。被禁用的 Tasklet 即使被调度也不会执行。
- 头文件:
<linux/interrupt.h>
- 使用场景: 临时阻止 Tasklet 执行,例如在修改 Tasklet 使用的数据时。
- 使用方式:
tasklet_disable(&my_tasklet); // 修改 Tasklet 使用的数据 tasklet_enable(&my_tasklet);
- 注意: 如果 Tasklet 当前正在运行,
tasklet_disable
会等待其完成。
tasklet_enable(t)
- 功能: 启用一个 Tasklet。
- 头文件:
<linux/interrupt.h>
- 使用场景: 与
tasklet_disable
配对使用,重新允许 Tasklet 执行。
3.3 工作队列 (Workqueues)
工作队列是另一种底半部机制,它将任务提交到一个或多个内核线程中执行。工作队列的优势在于可以在进程上下文执行,因此可以睡眠(例如获取互斥锁、阻塞 I/O)。这使得工作队列适合执行更复杂、可能阻塞的任务。
struct work_struct
- 功能: 表示一个工作队列中的任务。
- 头文件:
<linux/workqueue.h>
- 使用场景: 定义一个结构体成员,用于表示需要推迟执行的任务。
DECLARE_WORK(name, func)
- 功能: 静态声明并初始化一个工作。
- 头文件:
<linux/workqueue.h>
- 使用场景: 定义一个全局或静态的工作变量。
INIT_WORK(work, func)
- 功能: 动态初始化一个工作。
- 头文件:
<linux/workqueue.h>
- 使用场景: 在运行时动态创建工作。
- 参数说明:
work
: 指向struct work_struct
的指针。func
: 工作处理函数的指针。
- 使用方式:
struct work_struct *my_work_ptr;
my_work_ptr = kmalloc(sizeof(*my_work_ptr), GFP_KERNEL);
if (my_work_ptr)
{
INIT_WORK(my_work_ptr, my_work_handler);
}
- 工作处理函数原型:
void (*work_func_t)(struct work_struct *work);
- 参数是工作结构体本身的指针。
schedule_work(work)
- 功能: 将一个工作提交到默认的工作队列中执行。
- 头文件:
<linux/workqueue.h>
- 使用场景: 在顶半部中断处理程序或 Tasklet 中,将需要推迟到进程上下文执行的任务提交给工作队列。
- 使用方式:
// 在顶半部或 Tasklet 中调用,调度 my_work 执行 schedule_work(&my_work);
schedule_delayed_work(work, delay)
- 功能: 将一个工作延迟一段时间后提交到默认的工作队列中执行。
- 头文件:
<linux/workqueue.h>
- 使用场景: 需要延迟执行的任务。
- 参数说明:
work
: 指向struct delayed_work
的指针(struct delayed_work
包含struct work_struct
)。delay
: 延迟的时间,以 jiffies 为单位。
cancel_work_sync(work)
- 功能: 取消一个待处理的工作,并等待它完成(如果已经在执行)。
- 头文件:
<linux/workqueue.h>
- 使用场景: 在模块卸载或设备移除时,确保工作队列中的任务不再运行。
- 注意: 这个函数可能会睡眠,不能在中断上下文或持有自旋锁时调用。
cancel_delayed_work_sync(dwork)
- 功能: 取消一个待处理的延迟工作,并等待它完成。
- 头文件:
<linux/workqueue.h>
- 使用场景: 取消之前调度的延迟工作。
- 自定义工作队列: 除了默认工作队列,还可以创建自己的工作队列,以便更好地管理和控制任务的执行。
create_workqueue(name)
: 创建一个普通工作队列。create_singlethread_workqueue(name)
: 创建一个单线程工作队列。queue_work(wq, work)
: 将工作提交到指定的工作队列。destroy_workqueue(wq)
: 销毁工作队列。
4. 中断上下文中的同步
在中断处理程序(特别是顶半部)中,由于不能睡眠,只能使用自旋锁或原子操作进行同步。
- 自旋锁 (Spinlock)
spinlock_t
: 自旋锁数据结构。spin_lock_init(lock)
: 初始化自旋锁。spin_lock(lock)
: 获取自旋锁。如果锁已被占用,忙等待。spin_unlock(lock)
: 释放自旋锁。spin_lock_irqsave(lock, flags)
: 获取自旋锁并保存中断状态,然后禁用本地中断。spin_unlock_irqrestore(lock, flags)
: 释放自旋锁并恢复中断状态。spin_lock_irq(lock)
: 获取自旋锁并禁用本地中断(不保存状态)。spin_unlock_irq(lock)
: 释放自旋锁并启用本地中断(不恢复状态)。spin_lock_bh(lock)
: 获取自旋锁并禁用底半部。spin_unlock_bh(lock)
: 释放自旋锁并启用底半部。- 使用场景: 在中断处理程序和进程上下文之间,或在多个中断处理程序之间保护共享数据。
spin_lock_irqsave
/spin_unlock_irqrestore
是最常用的,因为它能处理进程与中断之间的同步问题。 - 注意: 在中断处理程序中(特别是顶半部),绝对不能使用会睡眠的锁(如互斥锁
mutex
、信号量semaphore
、读写信号量rw_semaphore
)。
- 原子操作 (Atomic Operations)
- 提供对整数或位进行原子操作的函数,如
atomic_inc()
,atomic_dec()
,test_and_set_bit()
等。 - 头文件:
<asm/atomic.h>
或<asm/bitops.h>
- 使用场景: 对单个整数或位进行简单的、无需锁的原子修改。
- 注意: 原子操作只能保证单个操作的原子性,如果需要对多个变量进行修改或进行更复杂的操作,仍然需要使用锁。
- 提供对整数或位进行原子操作的函数,如
5. 其他相关函数
in_interrupt()
- 功能: 检查当前是否处于中断上下文(硬中断或软中断)。
- 头文件:
<linux/hardirq.h>
- 使用场景: 在编写函数时,需要根据当前的上下文(中断上下文或进程上下文)采取不同的行为。
- 返回值: 如果在中断上下文返回非零,否则返回 0。
in_irq()
- 功能: 检查当前是否处于硬中断上下文。
- 头文件:
<linux/hardirq.h>
- 使用场景: 需要区分硬中断和软中断上下文时。
synchronize_irq(irq)
- 功能: 等待指定中断线上的所有中断处理程序(包括顶半部和底半部)完成。
- 头文件:
<linux/interrupt.h>
- 使用场景: 在释放中断处理程序之前,确保没有正在执行的中断处理任务。
- 注意: 这个函数可能会睡眠,不能在中断上下文或持有自旋锁时调用。
6. 中断调试工具
/proc/interrupts
:显示中断计数和归属 CPUcat /proc/softirqs
:显示软中断处理统计ftrace
/trace-cmd
:跟踪中断处理路径perf irq
:统计中断频率irqbalance
:用户空间中断分布调度工具
本文版权归原作者zhaofujian所有,采用 CC BY-NC-ND 4.0 协议进行许可,转载请注明出处。