4. 中断请求(request_irq)流程:源码分析与注册机制
中断处理程序的注册是中断子系统的核心入口。Linux 内核通过 request_irq() 函数将设备驱动的中断处理逻辑与特定的中断号绑定。本章节深入分析 request_irq() 的源码执行路径、中断号的分配策略以及处理函数的注册细节,帮助理解中断负载均衡的底层基础。
4.1 request_irq 函数原型与参数
request_irq() 是驱动开发者最常用的中断注册接口,定义在 include/linux/interrupt.h 中。其函数原型如下:
static inline int __must_check
request_irq(unsigned int irq, irq_handler_t handler,
unsigned long flags, const char *name, void *dev)
{
return request_threaded_irq(irq, handler, NULL, flags, name, dev);
}
参数说明:
| 参数 | 类型 | 说明 |
|---|---|---|
irq |
unsigned int |
中断号,标识硬件中断源 |
handler |
irq_handler_t |
中断处理函数指针,类型为 irqreturn_t (*)(int, void *) |
flags |
unsigned long |
中断标志位,控制触发方式、共享属性等 |
name |
const char * |
设备名称,用于 /proc/interrupts 显示 |
dev |
void * |
设备标识,共享中断时用于区分设备 |
关键点:
request_irq() 实际上是 request_threaded_irq() 的封装,将线程化处理函数设为 NULL,表示使用传统的中断上下文处理方式。若需要线程化中断,可直接调用 request_threaded_irq()。
4.2 request_threaded_irq 核心流程
request_threaded_irq() 是中断注册的真正入口,位于 kernel/irq/manage.c。其执行流程可分为以下关键步骤:
- 参数校验与权限检查:验证中断号有效性、处理函数非空、标志位合法性。
- 中断描述符获取:通过
irq_to_desc(irq)获取struct irq_desc,该结构体是中断管理的核心数据结构。 - 中断号分配确认:若中断号尚未分配
struct irq_desc,则调用irq_alloc_desc_at()动态分配。 - 中断触发类型设置:根据
flags中的IRQF_TRIGGER_*位设置硬件触发方式。 - 处理函数注册:将
handler和dev_id封装为struct irqaction,挂入irq_desc的action链表。 - 中断控制器使能:调用
irq_startup()或__irq_set_handler()激活中断线。
性能关注点:步骤 5 中,若中断号已存在其他处理函数(共享中断),新
irqaction 会被添加到链表尾部。中断触发时,内核会遍历整个链表,逐个调用处理函数直到某个函数返回 IRQ_HANDLED。共享中断过多会显著增加中断延迟。
4.3 中断号分配机制
中断号是连接硬件中断源与软件处理函数的桥梁。Linux 内核支持两种中断号分配方式:
| 分配方式 | 函数 | 适用场景 | 特点 |
|---|---|---|---|
| 静态分配 | irq_alloc_desc_at() |
传统 x86 PIC、固定中断号设备 | 中断号预先定义,不可动态更改 |
| 动态分配 | irq_alloc_desc_from() |
PCIe MSI/MSI-X、平台设备 | 中断号由内核自动分配,避免冲突 |
| 层次化分配 | irq_domain_alloc_irqs() |
ARM GIC、RISC-V PLIC | 通过 irq_domain 映射硬件中断到 Linux 中断号 |
负载均衡影响:动态分配的中断号通常与 CPU 亲和性设置相关。在 SMP 系统中,
irq_alloc_desc_from() 会优先选择当前 CPU 所在节点(NUMA)的中断描述符,这为后续的 irqbalance 提供了基础。
4.4 irqaction 结构体与处理函数注册
每个中断处理函数被封装为 struct irqaction,其核心字段如下:
struct irqaction {
irq_handler_t handler; // 中断处理函数
unsigned long flags; // 中断标志
void *dev_id; // 设备标识
struct irqaction *next; // 链表指针(共享中断)
int irq; // 中断号
irq_handler_t thread_fn; // 线程化处理函数
struct task_struct *thread; // 内核线程
struct irqaction *secondary; // 辅助处理
// ... 其他字段
};
注册过程的核心操作是将 irqaction 插入到 irq_desc 的 action 链表:
// kernel/irq/manage.c 中 setup_irq() 的核心逻辑
static int
setup_irq(unsigned int irq, struct irqaction *new)
{
struct irq_desc *desc = irq_to_desc(irq);
struct irqaction *old, **old_ptr;
unsigned long flags;
raw_spin_lock_irqsave(&desc->lock, flags);
old_ptr = &desc->action;
old = *old_ptr;
if (old) {
// 共享中断:遍历到链表尾部
while (old->next)
old = old->next;
old_ptr = &old->next;
}
// 插入新 irqaction
*old_ptr = new;
raw_spin_unlock_irqrestore(&desc->lock, flags);
return 0;
}
性能分析:共享中断链表的遍历发生在中断上下文中。当
irqaction 数量超过 3-4 个时,遍历开销会显著增加中断响应延迟。在负载均衡设计中,应尽量避免非必要的共享中断,或使用 MSI-X 为每个设备分配独立中断号。
4.5 中断标志位 flags 详解
flags 参数控制中断的行为特性,常用的标志位如下:
| 标志位 | 值 | 说明 | 负载均衡影响 |
|---|---|---|---|
IRQF_SHARED |
0x00000001 | 允许共享中断线 | 增加链表遍历开销,降低确定性 |
IRQF_TRIGGER_RISING |
0x00000002 | 上升沿触发 | 边缘触发易丢失,不适合负载均衡 |
IRQF_TRIGGER_FALLING |
0x00000004 | 下降沿触发 | 同上 |
IRQF_TRIGGER_HIGH |
0x00000008 | 高电平触发 | 电平触发可共享,适合多设备 |
IRQF_TRIGGER_LOW |
0x00000010 | 低电平触发 | 同上 |
IRQF_PERCPU |
0x00000020 | 中断仅在本 CPU 处理 | 不参与全局负载均衡 |
IRQF_NOBALANCING |
0x00000080 | 禁止 irqbalance 迁移 | 固定 CPU 亲和性,绕过均衡器 |
IRQF_NO_THREAD |
0x00010000 | 禁止线程化处理 | 保持硬中断上下文,降低延迟 |
最佳实践:对于高性能设备(如网卡、NVMe SSD),建议使用
IRQF_NOBALANCING 结合手动设置 CPU 亲和性,避免 irqbalance 在运行时动态迁移中断导致缓存抖动。对于普通设备,保留默认标志位让 irqbalance 自动优化。
4.6 中断处理函数的执行上下文
注册后的中断处理函数在以下上下文中执行:
- 硬中断上下文(Hard IRQ Context):
handler直接执行,禁止睡眠,禁止持有锁(除非使用 spin_lock)。执行时间应极短(通常 < 100μs)。 - 软中断上下文(SoftIRQ Context):硬中断返回后触发,用于处理耗时较长的下半部(如网络协议栈)。
- 线程化中断上下文(Threaded IRQ Context):通过
request_threaded_irq()的thread_fn参数注册,运行在内核线程中,允许睡眠,适合复杂设备驱动。
// 示例:注册线程化中断
int dev_probe(struct platform_device *pdev)
{
int irq = platform_get_irq(pdev, 0);
return request_threaded_irq(irq,
hard_irq_handler, // 硬中断:快速处理,返回 IRQ_WAKE_THREAD
thread_irq_handler, // 线程化:处理耗时操作
IRQF_TRIGGER_HIGH | IRQF_ONESHOT,
"my_device", dev);
}
延迟分析:线程化中断将处理函数从硬中断上下文迁移到内核线程,虽然增加了调度延迟(通常 10-100μs),但避免了硬中断长时间占用 CPU,提高了系统整体的实时性和可预测性。在负载均衡场景中,线程化中断可以像普通进程一样被调度器迁移到不同 CPU。
4.7 中断注册的返回值与错误处理
request_irq() 的返回值需要驱动开发者仔细检查:
| 返回值 | 含义 | 常见原因 |
|---|---|---|
0 |
注册成功 | - |
-EINVAL |
参数无效 | 中断号超出范围、handler 为 NULL、flags 非法 |
-EBUSY |
中断号已被占用且不允许共享 | 未设置 IRQF_SHARED 但中断线已被注册 |
-ENOMEM |
内存分配失败 | 无法分配 irqaction 或 irq_desc |
调试建议:当
request_irq() 返回 -EBUSY 时,可通过 cat /proc/interrupts 查看当前中断号的使用情况,确认是否与其他设备冲突。对于 PCIe 设备,可使用 lspci -vvv 查看 MSI/MSI-X 中断分配状态。
通过以上分析,可以看到 request_irq() 不仅仅是简单的函数注册,它涉及中断描述符管理、链表操作、硬件触发配置等多个子系统。理解这些底层细节,是后续进行中断负载均衡优化和调度延迟分析的基础。