04、Linux 中断的申请流程及原理分析
一、request_irq 函数详细分析
request_threaded_irq是Linux内核用来管理中断的核心函数,它的主要作用是把硬件中断和软件处理逻辑绑定起来。
- 申请中断权限 这个函数会向系统申请一个特定编号的硬件中断。比如,当某个硬件(如网卡、USB设备)需要通知CPU时,就会触发这个编号的中断。
- 绑定处理函数 通过handler参数,把你要写的中断处理函数和这个中断号绑定。当硬件触发中断时,系统就会自动执行你指定的函数。
- 支持后台线程处理 特别之处在于可以设置第二个线程函数(thread_fn)。这样,快速简单的任务可以直接在中断上下文处理,复杂的耗时操作则交给后台线程慢慢执行,避免卡住其他任务。
- 设置中断行为规则 用irqflags参数告诉系统这个中断的触发方式(比如是电平触发还是边沿触发),还能指定中断是共享的还是独占的。
- 关联设备信息 通过dev_id参数可以把中断和具体设备绑定。这样在处理中断时,就能直接获取到设备的信息,方便操作对应的硬件资源。
- 返回申请结果 申请成功返回0,失败返回错误码。比如如果这个中断已经被其他驱动占用了,就会返回错误提示。
这个函数的代码在内核源码的kernel/irq/manage.c文件里,它把硬件中断、处理逻辑、设备信息三者整合在一起,是驱动开发中连接硬件和软件的关键桥梁。
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)
{
struct irqaction *action; //中断动作结构体
struct irq_desc *desc; // 中断描述符
int retval;
if (irq == IRQ_NOTCONNECTED)
return -ENOTCONN;
/*
* Sanity-check: shared interrupts must pass in a real dev-ID,
* otherwise we'll have trouble later trying to figure out
* which interrupt is which (messes up the interrupt freeing
* logic etc).
*
* Also IRQF_COND_SUSPEND only makes sense for shared interrupts and
* it cannot be set along with IRQF_NO_SUSPEND.
*/
if (((irqflags & IRQF_SHARED) && !dev_id) ||
(!(irqflags & IRQF_SHARED) && (irqflags & IRQF_COND_SUSPEND)) ||
((irqflags & IRQF_NO_SUSPEND) && (irqflags & IRQF_COND_SUSPEND)))
return -EINVAL;
//根据中断号获取中断描述符
desc = irq_to_desc(irq);
if (!desc)
return -EINVAL;
if (!irq_settings_can_request(desc) ||
WARN_ON(irq_settings_is_per_cpu_devid(desc)))
return -EINVAL;
// 如果未指定中断处理函数,则赋值默认的函数
if (!handler) {
if (!thread_fn)
return -EINVAL;
handler = irq_default_primary_handler;
}
action = kzalloc(sizeof(struct irqaction), GFP_KERNEL);
if (!action)
return -ENOMEM;
action->handler = handler; //服务程序
action->thread_fn = thread_fn; //NULL 线程服务
action->flags = irqflags; //中断标记
action->name = devname; //设备名称
action->dev_id = dev_id; //ID
retval = irq_chip_pm_get(&desc->irq_data);
if (retval < 0) {
kfree(action);
return retval;
}
//
retval = __setup_irq(irq, desc, action);
if (retval) {
irq_chip_pm_put(&desc->irq_data);
kfree(action->secondary);
kfree(action);
}
To be determined
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
1.1、调用 irq_to_desc函数
irq_to_desc
函数是用来根据中断号找到对应的中断描述符结构的。它的查找方式分两种情况:
- 常规情况:当所有中断号都是连续排列时,系统会把这些
struct irq_desc
结构体统一存放在一个数组里。这时候只需要用中断号作为索引直接查找,就像查电话号码簿一样简单。 - 特殊情况:如果系统配置了非连续的中断号(比如有些中断号被跳过),这时候用数组存放就不高效了。这种情况下,系统会把中断描述符存放在一个叫"基数树"的数据结构里。这个结构就像图书馆的索引系统,能通过中断号这个"书号"快速定位到对应的结构体,即使编号是跳跃的也能很快找到。
简单来说,就是:连续编号用数组直接找,不连续就用基数树快速查。这种设计让系统既能处理简单场景,也能灵活应对复杂的中断号分配情况。
kernel/irq/irqdesc.c
1.2、调用 __setup_irq 函数
目录:irq/manage.c
__setup_irq
函数用于设置中断处理程序。它的核心逻辑是这样的:
当某个中断描述符(irq_desc)已经有已注册的中断处理动作(irqaction)时,新添加的处理动作会被追加到动作链表的最后面。如果配置了需要以独立线程方式运行处理函数,就会通过 setup_irq_thread
创建对应的内核线程,并用 wake_up_process
唤醒这个线程开始工作。
简单来说就是:把新中断处理挂到最后,如果有线程需求就启动线程并激活它。
中断线程话的原理:
kernel/sched/core.c
二、中断的描述结构分析
- irq_desc 结构体 描述符
- irqaction 结构体
2.1、struct irq_desc
struct irq_desc {
struct irq_common_data irq_common_data;
struct irq_data irq_data;
unsigned int __percpu *kstat_irqs;
irq_flow_handler_t handle_irq; //中断处理函数
#ifdef CONFIG_IRQ_PREFLOW_FASTEOI
irq_preflow_handler_t preflow_handler;
#endif
struct irqaction *action; /* IRQ action list */
unsigned int status_use_accessors;
unsigned int core_internal_state__do_not_mess_with_it;
unsigned int depth; /* nested irq disables */
unsigned int wake_depth; /* nested wake enables */
unsigned int tot_count;
unsigned int irq_count; /* For detecting broken IRQs */
unsigned long last_unhandled; /* Aging timer for unhandled count */
unsigned int irqs_unhandled;
atomic_t threads_handled;
int threads_handled_last;
raw_spinlock_t lock; //自旋锁
struct cpumask *percpu_enabled; //中断使能掩码
const struct cpumask *percpu_affinity; //中断的CPU亲和性
#ifdef CONFIG_SMP
const struct cpumask *affinity_hint;
struct irq_affinity_notify *affinity_notify;
#ifdef CONFIG_GENERIC_PENDING_IRQ
cpumask_var_t pending_mask;
#endif
#endif
unsigned long threads_oneshot;
atomic_t threads_active;
wait_queue_head_t wait_for_threads;
#ifdef CONFIG_PM_SLEEP
unsigned int nr_actions;
unsigned int no_suspend_depth;
unsigned int cond_suspend_depth;
unsigned int force_resume_depth;
#endif
#ifdef CONFIG_PROC_FS
struct proc_dir_entry *dir;
#endif
#ifdef CONFIG_GENERIC_IRQ_DEBUGFS
struct dentry *debugfs_file;
const char *dev_name;
#endif
#ifdef CONFIG_SPARSE_IRQ
struct rcu_head rcu;
struct kobject kobj;
#endif
struct mutex request_mutex;
int parent_irq;
struct module *owner;
const char *name;
} ____cacheline_internodealigned_in_smp;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
struct irq_desc
是 Linux 内核中用于描述中断的核心数据结构之一。每个硬件中断都对应一个 irq_desc
实例,内核通过该结构体记录与特定中断相关的各种信息和状态。作为中断管理的基石,irq_desc
的设计体现了内核对中断处理的高度抽象化和模块化。它不仅提供了统一的接口来管理中断处理函数、行为及状态,还为性能分析、调试和优化提供了必要的支持。
2.1.1、中断处理函数管理:handle_irq
irq_desc
中的 handle_irq
字段是一个函数指针,指向具体的中断处理函数。当中断发生时,内核会根据硬件触发的中断号找到对应的 irq_desc
实例,并调用 handle_irq
所指向的函数来执行中断处理逻辑。这个字段的设计使得中断处理流程具有高度的灵活性:不同的中断控制器可以注册不同的处理函数,从而适配多种硬件架构的需求。
例如,在简单的边缘触发(Edge-Triggered)中断场景下,handle_irq
可能直接调用一个预定义的通用处理函数。
2.1.2、中断行为管理:action
action
字段是 irq_desc
结构体中的另一个关键成员,它指向一个中断行为链表。每个节点代表一个注册到该中断上的回调函数及其相关联的数据。这种设计允许多个设备共享同一个中断号(即“中断共享”机制),这是现代多设备系统中常见的需求。
例如,在 PCI 总线中,多个设备可能被分配到同一个中断号上。当该中断触发时,内核会依次调用 action
链表中的每个回调函数,直到找到实际产生中断的设备并完成处理。这种链式调用机制不仅提高了中断资源的利用率,还简化了设备驱动程序的开发。
值得注意的是,action
链表的管理涉及同步问题。为了确保中断处理的安全性,irq_desc
提供了一个自旋锁(lock
字段)。在修改 action
链表时(如注册或注销中断处理函数),必须持有该锁以避免竞态条件的发生。
2.1.3、中断统计信息:kstat_irqs
kstat_irqs
字段是一个每 CPU 的计数器数组,用于记录每个中断的触发次数。这些统计信息对于分析系统的中断负载分布、识别潜在的瓶颈以及优化性能至关重要。例如,管理员可以通过 /proc/interrupts
文件查看各个中断的触发次数,进而调整中断的 CPU 亲和性(Affinity),以实现更好的负载均衡。
此外,irq_desc
还包含一些辅助字段用于跟踪未处理的中断事件(如 irqs_unhandled
和 last_unhandled
)。这些字段帮助内核检测和诊断“假中断”(Spurious Interrupts),即那些由于硬件故障或其他原因而没有实际来源的中断信号。
2.1.4、中断数据管理:irq_data
irq_data
字段保存了与中断相关的硬件信息,如中断号、中断类型以及关联的中断控制器等。这些数据由底层的中断子系统初始化,并在整个中断生命周期中保持不变。它们为上层的中断管理逻辑提供了必要的元信息。
与此同时,irq_common_data
字段则存储了与中断处理相关的通用数据,例如中断屏蔽状态、唤醒能力等。这些数据通常由中断控制器驱动程序设置,并在中断处理过程中动态更新。例如,当某个中断被禁用时,其屏蔽状态会被标记在 irq_common_data
中,从而防止后续的中断请求进入处理流程。
2.1.5、中断状态管理
irq_desc
中的多个字段共同构成了中断状态管理系统,其中包括:
- 嵌套中断禁用计数(
**depth**
):用于跟踪当前中断是否被显式禁用。每次调用disable_irq()
时,depth
增加;每次调用enable_irq()
时,depth
减少。只有当depth
归零时,中断才会重新启用。 - 唤醒使能计数(
**wake_depth**
):用于管理中断在低功耗模式下的唤醒能力。例如,在挂起(Suspend)期间,某些关键中断需要保持激活状态以支持设备唤醒。 - 活动线程计数(
**active_thread_count**
):用于跟踪当前正在运行的中断线程数量。在 Linux 内核中,某些中断处理逻辑可以通过线程化(Threaded IRQ)的方式执行,以减少硬中断上下文中的处理时间并提高系统的响应能力。每当一个中断线程被调度执行时,该计数器会递增;当线程完成处理后,计数器则递减。这一机制确保了内核能够准确地监控中断线程的状态,并避免因并发问题导致的资源竞争或死锁。
此外,irq_desc
结构体还包含一些标志位字段(如 status_use_accessors
),这些字段通过位掩码的形式记录中断的当前状态。例如,是否处于挂起状态、是否支持共享、是否已被激活等。这些标志位的设计不仅节省了内存空间,还提高了状态检查和更新的效率。通过内联函数(Accessors)对这些标志位进行操作,进一步增强了代码的可读性和安全性。
2.1.6、中断亲和性与负载均衡:affinity
现代多核处理器系统中,中断的 CPU 亲和性管理是优化性能的重要手段。irq_desc
中的 affinity
字段存储了一个 CPU 掩码,用于指示哪些 CPU 核心可以处理该中断。通过合理配置中断亲和性,管理员可以将高频率的中断分配到特定的 CPU 核心上,从而避免单个核心过载,同时提升整体系统的吞吐量。
为了动态调整中断亲和性,Linux 内核提供了一系列接口(如 /proc/irq/<irq_number>/smp_affinity
)。当用户修改这些接口时,内核会更新 irq_desc
的 affinity
字段,并通知底层中断控制器重新配置硬件路由。这种灵活性使得系统能够在运行时根据工作负载的变化动态优化中断分布。
2.2、**irqaction**
的核心作用
struct irqaction
是 Linux 内核中用于描述中断行为的数据结构之一。它的主要功能是将中断号(IRQ)与具体的中断处理逻辑绑定在一起。每个中断源都可以注册一个或多个 irqaction
实例,这些实例通过链表的形式连接起来,从而支持共享中断线的能力。所谓共享中断线,是指多个设备可以使用同一个中断号,但各自提供独立的处理函数。这种设计在现代硬件环境中尤为重要,因为它能够有效地利用有限的中断资源。
从代码层面来看,struct irqaction
的定义如下:
struct irqaction {
irq_handler_t handler; // 中断处理函数
void *dev_id;
void __percpu *percpu_dev_id;
struct irqaction *next;
irq_handler_t thread_fn;
struct task_struct *thread;
struct irqaction *secondary;
unsigned int irq; // 中断号
unsigned int flags; // 中断的标记
unsigned long thread_flags;
unsigned long thread_mask;
const char *name;
struct proc_dir_entry *dir;
} ____cacheline_internodealigned_in_smp;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2.2.1、handler
:中断处理的核心回调函数
handler
是 struct irqaction
中最重要的成员之一,它指向一个函数指针类型 irq_handler_t
,即中断处理函数。当某个中断被触发时,内核会调用该函数来执行具体的中断处理逻辑。例如,在网络设备驱动中,handler
可能负责处理接收到的数据包;在存储设备驱动中,它可能负责处理完成的 I/O 请求。
需要注意的是,handler
函数通常运行在中断上下文中,这意味着它必须尽可能快地完成任务,避免长时间占用 CPU 资源。因此,复杂或耗时的操作通常会被推迟到下半部(如软中断或工作队列)中执行。
2.2.2、next
:支持共享中断的链表结构
next
指针的存在揭示了 struct irqaction
的一个重要特性:它是一个链表节点。对于共享中断线的情况,多个 irqaction
实例会通过 next
指针串联在一起,形成一个链表。当某个中断被触发时,内核会遍历该链表,并依次调用每个 irqaction
的 handler
函数。
这种设计的优点在于灵活性和扩展性。例如,在多设备共享同一中断线的情况下,每个设备可以独立注册自己的 irqaction
,而无需担心干扰其他设备的中断处理逻辑。此外,这种链表结构也为动态添加或移除中断处理逻辑提供了便利。
2.2.3、dev_id
和 percpu_dev_id
:设备标识与每 CPU 数据
dev_id
是一个通用的设备标识符,通常用于区分共享中断线上的不同设备。在中断处理过程中,内核会将 dev_id
传递给 handler
函数,以便后者能够识别当前中断的具体来源。
percpu_dev_id
则是一个每 CPU 数据指针,用于支持多核系统中的中断处理。在某些场景下,中断处理逻辑需要访问与特定 CPU 相关的数据结构,此时 percpu_dev_id
就显得尤为重要。
2.2.4、thread_fn
和 thread
:线程化中断的支持
现代 Linux 内核引入了线程化中断(Threaded IRQ)的概念,允许将中断处理逻辑放在内核线程中执行。这种设计的主要目的是将中断处理从硬中断上下文中分离出来,从而减少对中断上下文的依赖,提高系统的实时性和可维护性。
thread_fn
是线程化中断的回调函数,而 thread
则指向与该中断相关联的内核线程。当某个中断被触发时,内核会唤醒对应的线程,并调用 thread_fn
来执行中断处理逻辑。这种方式特别适用于需要执行复杂操作的中断处理场景。
2.2.5、flags
:中断行为的控制标志
flags
是一个无符号整数字段,用于定义中断处理的行为和特性。它通过一系列位掩码来指定中断的属性,例如是否支持共享中断线、是否启用线程化中断、是否需要唤醒调度器等。这些标志为内核提供了灵活的机制,以适应不同硬件设备和驱动程序的需求。
例如,IRQF_SHARED
标志表示该中断可以与其他设备共享同一中断线;IRQF_ONESHOT
则指示内核在执行完线程化中断处理函数后手动重新启用中断。此外,IRQF_NO_THREAD
标志明确禁止将中断处理逻辑线程化,这在某些对延迟敏感的场景中尤为重要。
通过对 flags
的合理配置,驱动开发者能够精确控制中断的行为,从而在性能、实时性和资源利用率之间找到最佳平衡点。
2.2.6、name
:中断源的可读标识
name
是一个字符串指针,用于为中断源提供一个可读的名称。这个字段的主要作用是增强调试和监控的便利性。当系统管理员或开发人员需要分析中断的来源时,name
提供了一个直观的标识符,帮助快速定位问题。
例如,在 /proc/interrupts
文件中,每个中断号旁边都会显示其对应的 name
值。这种设计不仅提高了系统的透明度,还使得复杂的中断管理变得更加直观和高效。
2.2.7、dir
:与 /proc
文件系统的集成
dir
是一个指向 struct proc_dir_entry
的指针,用于将中断信息暴露给 /proc
文件系统。通过这种方式,内核允许用户空间工具(如 cat /proc/interrupts
)访问和显示中断的统计信息、状态以及其他相关数据。
这种设计体现了 Linux 内核一贯的设计哲学:通过简单的接口将底层复杂性抽象出来,同时为用户提供强大的调试和监控能力。对于驱动开发者而言,正确设置 dir
字段可以进一步提升驱动的可观测性和可维护性。
2.2.8、secondary
:辅助中断的支持
secondary
是一个指向另一个 struct irqaction
实例的指针,用于支持辅助中断(Secondary IRQ)。辅助中断通常用于处理主中断无法完全覆盖的特殊情况,或者为某些硬件设备提供额外的中断处理路径。
例如,在某些复杂的硬件架构中,主中断可能仅负责通知设备的状态变化,而辅助中断则用于处理具体的事件细节。通过 secondary
字段,内核能够将主中断和辅助中断关联起来,从而实现更精细的中断管理。
2.2.9、thread_flags
和 thread_mask
:线程化中断的高级控制
thread_flags
和 thread_mask
是两个与线程化中断相关的字段,它们共同定义了内核线程的行为和调度策略。thread_flags
包含一组标志位,用于指定线程的优先级、调度策略以及其他运行时特性;而 thread_mask
则是一个位掩码,用于限制线程在特定 CPU 上的运行。
这些字段的存在表明,Linux 内核在线程化中断的设计上不仅考虑了功能的实现,还充分关注了性能优化和资源分配的灵活性。通过合理配置这些字段,开发者可以在多核系统中实现高效的中断负载均衡,从而最大限度地提升系统的整体性能。