[TOC]

include/linux/interrupt.h

or_softirq_pending 软中断待处理状态

1
2
3
4
5
6
7
8
DEFINE_PER_CPU_ALIGNED(irq_cpustat_t, irq_stat);
EXPORT_PER_CPU_SYMBOL(irq_stat);

#define local_softirq_pending_ref irq_stat.__softirq_pending

#define local_softirq_pending() (__this_cpu_read(local_softirq_pending_ref))
#define set_softirq_pending(x) (__this_cpu_write(local_softirq_pending_ref, (x)))
#define or_softirq_pending(x) (__this_cpu_or(local_softirq_pending_ref, (x)))

raise_timer_softirq 定时器软中断

1
2
3
4
5
6
7
8
static inline void raise_timer_softirq(unsigned int nr)
{
lockdep_assert_in_irq();
if (force_irqthreads())
raise_ktimers_thread(nr);
else
__raise_softirq_irqoff(nr);
}

include/linux/irq.h

irq_alloc_domain_generic_chips 分配中断域

1
2
3
4
5
6
7
#define irq_alloc_domain_generic_chips(d, irqs_per_chip, num_ct, name,	\
handler, clr, set, flags) \
({ \
MAYBE_BUILD_BUG_ON(irqs_per_chip > 32); \
__irq_alloc_domain_generic_chips(d, irqs_per_chip, num_ct, name,\
handler, clr, set, flags); \
})

irq_set_chained_handler 为给定的 IRQ 设置高电平链式流处理程序

1
2
3
4
5
6
7
8
9
/* 为给定的IRQ设置高级链式流处理程序。
(链式处理程序会自动启用并设置为
IRQ_NOREQUEST、IRQ_NOPROBE和IRQ_NOTHREAD)
*/
static inline void
irq_set_chained_handler(unsigned int irq, irq_flow_handler_t handle)
{
__irq_set_handler(irq, handle, 1, NULL);
}

kernel/irq/devres.c

devm_request_irqdevm_request_threaded_irq: 自动管理中断资源的注册

这一组函数是Linux内核中断请求API的”设备资源管理”(devm)版本。它们的核心原理是将中断的注册(request)和释放(free)操作与设备的生命周期进行绑定, 从而实现中断资源的自动管理。当一个驱动程序使用这些函数来请求中断时, 它无需在退出或错误处理路径中显式地调用free_irq, 内核的设备模型会在驱动卸载时自动完成这个清理工作。

这极大地简化了驱动程序的编写, 尤其是在有多个中断和复杂错误处理逻辑的probe函数中, 能有效防止因忘记释放中断而导致的资源泄漏。


devm_request_irq (内联辅助函数)

这是一个简单的内联函数, 作为devm_request_threaded_irq的便捷封装。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/*
* static inline: 定义一个静态内联函数.
* "static" 使其作用域仅限于当前文件.
* "inline" 建议编译器将函数体直接嵌入调用处, 减少函数调用开销, 这对于性能敏感的嵌入式系统很有益.
* __must_check: GCC属性, 强制调用者检查此函数的返回值, 否则编译器会发出警告.
*/
static inline int __must_check
devm_request_irq(struct device *dev, unsigned int irq, irq_handler_t handler,
unsigned long irqflags, const char *devname, void *dev_id)
{
/*
* 此函数直接调用 devm_request_threaded_irq.
* 它的作用是为那些不需要"线程化中断处理"的简单中断提供一个更简洁的API.
* 它将线程处理函数(thread_fn)参数直接设置为 NULL.
*/
return devm_request_threaded_irq(dev, irq, handler, NULL, irqflags,
devname, dev_id);
}

devres 相关的内部实现

这三个部分是devm框架的内部组件, 共同实现了中断资源的生命周期管理。

1
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
/*
* irq_devres: devres 的私有数据结构.
* 它的作用是作为一个"票据", 保存足够的信息以便将来能够释放这个中断.
*/
struct irq_devres {
unsigned int irq; // 中断号
void *dev_id; // 传递给中断处理程序的唯一标识符 (cookie)
};

/*
* devm_irq_release: devres 的释放回调函数.
* @dev: 拥有此资源的设备.
* @res: 指向 devres 管理的 irq_devres 实例的指针.
*
* 当设备被卸载时, devres 核心会自动调用此函数.
*/
static void devm_irq_release(struct device *dev, void *res)
{
/*
* 将 void* 指针转换回 irq_devres 类型.
*/
struct irq_devres *this = res;

/*
* 调用标准的 free_irq 函数, 使用保存的中断号和 dev_id 来精确地释放之前请求的中断.
* 这是实现自动资源回收的关键步骤.
*/
free_irq(this->irq, this->dev_id);
}

/*
* devm_irq_match: devres 的匹配函数.
* 用于在设备资源列表中查找一个特定的中断资源 (通常由 devm_free_irq 调用).
*/
static int devm_irq_match(struct device *dev, void *res, void *data)
{
struct irq_devres *this = res, *match = data;

/*
* 只有当中断号(irq)和设备ID(dev_id)完全相同时, 才认为匹配成功.
* 这确保了可以唯一地标识和操作一个中断注册实例.
*/
return this->irq == match->irq && this->dev_id == match->dev_id;
}

devm_request_threaded_irq (核心实现)

这是整个功能的核心函数, 它执行了”申请-注册-管理”的完整流程。

1
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
67
68
69
70
71
72
73
74
75
76
77
78
79
80
/**
* devm_request_threaded_irq - 为一个受管设备分配一条中断线
* @dev: 请求中断的设备
* @irq: 要分配的中断线
* @handler: 当IRQ发生时调用的函数(硬中断上下文)
* @thread_fn: 在线程化中断上下文中调用的函数. 如果所有处理都在@handler中完成, 则为NULL.
* @irqflags: 中断类型标志
* @devname: 声明此中断的设备名称, 如果为NULL则使用dev_name(dev)
* @dev_id: 传递回处理函数的 cookie
*
* 除了额外的@dev参数外, 此函数与 request_threaded_irq() 接受相同的参数并执行相同的功能.
* 用此函数请求的中断将在驱动程序分离时被自动释放.
*
* 如果需要单独释放用此函数分配的IRQ, 必须使用 devm_free_irq().
*/
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)
{
struct irq_devres *dr; // 指向我们自定义的 devres 数据结构
int rc; // 用于存储返回值

/*
* 1. 分配一个 devres 资源记录.
* 将释放函数 devm_irq_release 与这个记录关联起来.
*/
dr = devres_alloc(devm_irq_release, sizeof(struct irq_devres),
GFP_KERNEL);
if (!dr)
return -ENOMEM;

/*
* 如果调用者没有提供设备名, 就使用设备的默认名称.
*/
if (!devname)
devname = dev_name(dev);

/*
* 2. 调用底层的、非资源管理的 request_threaded_irq 函数来实际注册中断.
* 这是与内核中断子系统交互的实际点.
*/
rc = request_threaded_irq(irq, handler, thread_fn, irqflags, devname,
dev_id);
/*
* 3. 错误处理.
*/
if (rc) {
/*
* 如果中断注册失败, 那么我们第一步分配的 devres 记录就不需要了,
* 调用 devres_free 将其释放, 防止内存泄漏.
*/
devres_free(dr);
/*
* 将原始的错误码返回.
*/
return rc;
}

/*
* 4. 成功后, 填充 devres 记录.
* 将中断号和 dev_id 保存到 dr 指向的结构体中, 为将来的自动释放做准备.
*/
dr->irq = irq;
dr->dev_id = dev_id;
/*
* 5. 将填充好的 devres 记录正式添加到设备的资源列表中.
* 从这一刻起, 内核就接管了这个中断资源的生命周期.
*/
devres_add(dev, dr);

/*
* 注册成功, 返回 0.
*/
return 0;
}
/*
* 将函数导出, 使其对其他内核模块可用.
*/
EXPORT_SYMBOL(devm_request_threaded_irq);

kernel/irq/handle.c

set_handle_irq 设置中断处理函数

1
2
3
4
5
6
7
8
int __init set_handle_irq(void (*handle_irq)(struct pt_regs *))
{
if (handle_arch_irq)
return -EBUSY;

handle_arch_irq = handle_irq;
return 0;
}

generic_handle_arch_irq 通用中断处理函数

1
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
67
68
69
70
71
/*
* 每 CPU 当前帧指针 - 最后一个异常帧的位置
* 堆栈
*/
DECLARE_PER_CPU(struct pt_regs *, __irq_regs);
static inline struct pt_regs *set_irq_regs(struct pt_regs *new_regs)
{
struct pt_regs *old_regs;

old_regs = __this_cpu_read(__irq_regs);
__this_cpu_write(__irq_regs, new_regs);
return old_regs;
}


/**
* generic_handle_arch_irq - 对于那些不自己进行入口记账的体系结构,
* 这是根级的irq处理函数。
* @regs: 从底层汇编处理代码传过来的寄存器文件。
*/
/*
* asmlinkage: 这是一个编译器指令,告诉GCC这个函数是从汇编代码调用的,
* 因此它的参数应该从栈上传递,而不是通过寄存器。
* void: 函数没有返回值。
* noinstr: 告诉编译器不要对此函数进行代码插桩(instrumentation),
* 因为中断处理路径非常敏感,不应被ftrace等工具追踪。
*/
asmlinkage void noinstr generic_handle_arch_irq(struct pt_regs *regs)
{
/* 定义一个指针,用于保存旧的(上一层嵌套中断的)寄存器帧地址。*/
struct pt_regs *old_regs;

/*
* Step 1: 进入中断上下文记账。
* irq_enter() 会增加一个per-cpu的计数器,并处理抢占计数等,
* 明确地标记CPU现在正处于中断处理状态。
*/
irq_enter();

/*
* Step 2: 设置并保存当前中断的寄存器帧。
* set_irq_regs() 会将全局的中断寄存器指针设置为当前的'regs',
* 并返回之前保存的指针(在中断嵌套时,这个返回值非NULL)。
* old_regs 保存了这个返回值。
*/
old_regs = set_irq_regs(regs);

/*
* Step 3: 调用真正的中断分发和处理函数。
* handle_arch_irq() 是一个与具体CPU架构相关的函数。它会
* 读取中断控制器,确定是哪个硬件设备触发了中断,然后调用
* 由该设备驱动程序注册的特定中断服务例程(ISR)。
* 这是实际的中断处理发生的地方。
*/
handle_arch_irq(regs);

/*
* Step 4: 恢复上一层的寄存器帧指针。
* 在中断处理完毕后,将全局的中断寄存器指针恢复为之前保存的
* 'old_regs'。如果是最外层中断,old_regs为NULL。
*/
set_irq_regs(old_regs);

/*
* Step 5: 退出中断上下文记账。
* irq_exit() 会减少per-cpu的计数器。在退出最外层中断时,
* 它还会检查是否有待处理的工作,如软中断(softirq)或调度请求
* (在“大”Linux中,这是检查TIF_NEED_RESCHED并可能调用schedule()的地方)。
*/
irq_exit();
}

__irq_wake_thread 唤醒中断线程

1
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
/*
* __irq_wake_thread - 唤醒与一个action关联的中断线程
* @desc: 中断描述符
* @action: 需要唤醒其线程的特定action
*/
void __irq_wake_thread(struct irq_desc *desc, struct irqaction *action)
{
/*
* **健壮性检查**:如果中断线程已经崩溃或被杀死(例如,驱动模块正在被卸载),
* 它的task_struct会被标记为PF_EXITING。在这种情况下,我们什么也不做,
* 只是假装中断已经被处理了。注释提到,硬中断处理器已经禁用了设备中断,
* 所以不用担心会因为无人处理而引发“中断风暴”。
*/
if (action->thread->flags & PF_EXITING)
return;

/*
* **防止重复唤醒**:这是关键的门控机制。
* test_and_set_bit 是一个原子操作,它会:
* 1. 测试 IRQTF_RUNTHREAD 位是否已经被设置。
* 2. 无论结果如何,都将该位置为1。
* 3. 返回该位在操作之前的旧值。
* 如果旧值是1(真),说明已经有另一个中断实例正在处理或排队等待唤醒线程,
* 我们就无需重复唤醒,直接返回。这可以有效地将多次快速到来的中断合并为
* 对线程的一次唤醒,极大提升了效率。
*/
if (test_and_set_bit(IRQTF_RUNTHREAD, &action->thread_flags))
return;

/*
* 以下代码的注释解释了为什么可以无锁地对 threads_oneshot 进行“或”操作。
* 这是一个非常复杂的同步问题,其安全性依赖于多个层次的保护:
* 1. 硬中断上下文:对于同一个IRQ,硬中断处理在多核之间是不会并行执行的。
* 2. 线程上下文:中断线程之间通过 desc->lock 进行串行化。
* 3. 硬中断与线程之间:通过 IRQS_INPROGRESS 状态位进行同步。当硬中断
* 正在处理时(设置了此状态),线程会自旋等待;反之,硬中断在获取
* desc->lock 失败时也会等待线程释放。
* 这段注释里的伪代码清晰地展示了这种精妙的“双向同步”舞蹈。
*/
/*
* 对于ONESHOT类型的中断,一个IRQ可能被多个线程化句柄共享。
* 这个掩码(threads_oneshot)用于告诉内核,具体是哪个(或哪些)
* action的线程需要被执行。当中断线程醒来时,会检查这个掩码。
*/
desc->threads_oneshot |= action->thread_mask;

/*
* **生命周期管理**:原子地增加 threads_active 计数器。
* 这个计数器用于 synchronize_irq() 函数。当一个驱动被卸载时,它会调用
* synchronize_irq() 来确保所有与该中断相关的处理(包括线程化的下半部)
* 都已全部完成。synchronize_irq() 会等待这个计数器变为零。
* 在这里增加计数,就好像是在说:“有一个新的中断线程任务开始了”。
* 当中断线程完成工作后,它会自己减少这个计数。
*/
atomic_inc(&desc->threads_active);

/*
* **最终唤醒**:这是实际的唤醒操作。
* wake_up_process() 是一个标准的内核调度器函数,它会接收一个进程
* (在Linux中,线程本质上也是一个进程)的task_struct,并将其状态
* 从 TASK_INTERRUPTIBLE (或类似休眠状态) 改变为 TASK_RUNNING,
* 然后把它放入对应CPU的运行队列中。在下一次调度时机,这个中断线程
* 就有机会被执行了。
*/
wake_up_process(action->thread);
}

__handle_irq_event_percpu 遍历并执行一个中断的所有action

1
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
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
/*
* __handle_irq_event_percpu - 遍历并执行一个中断的所有action
* @desc: 目标中断描述符
*
* 返回值: irqreturn_t, 所有action返回值的“或”运算结果
*/
irqreturn_t __handle_irq_event_percpu(struct irq_desc *desc)
{
/* 初始化返回值为IRQ_NONE。如果没有任何ISR处理此中断,将返回此值。*/
irqreturn_t retval = IRQ_NONE;
/* 获取中断号,用于跟踪和传递给ISR。*/
unsigned int irq = desc->irq_data.irq;
/* 定义一个指向irqaction结构体的指针,用于遍历。*/
struct irqaction *action;

/* 记录当前时间,用于计算中断处理的延迟,为内核性能分析提供数据。*/
record_irq_time(desc);

/*
* **核心循环**:这是一个宏,它会遍历desc->action链表中的每一个action。
* 一个中断号可能被多个设备共享(通过IRQF_SHARED标志),所以可能有多个action。
*/
for_each_action_of_desc(desc, action) {
irqreturn_t res; // 用于存储当前单个ISR的返回值。

/*
* 这段是为中断线程化服务的锁调试(lockdep)代码。
* 如果系统强制线程化(force_irqthreads),并且当前中断可以被线程化,
* 它会通知锁调试器,我们现在即将进入一个“伪 hardirq”上下文。
*/
if (irq_settings_can_thread(desc) &&
!(action->flags & (IRQF_NO_THREAD | IRQF_PERCPU | IRQF_ONESHOT)))
lockdep_hardirq_threaded();

/*
* **内核跟踪点**:在调用ISR之前,触发一个跟踪事件。
* 使用ftrace等工具可以捕获此事件,用于性能分析和调试,精确知道哪个ISR何时被调用。
*/
trace_irq_handler_entry(irq, action);

/*
* ===================================================================
* **最终的ISR调用**:这里是整个旅程的终点。
* action->handler 就是驱动程序通过 request_irq() 注册的那个函数。
* 内核将中断号(irq)和设备ID(action->dev_id)作为参数传递给它。
* 这个ISR会去操作硬件,读取数据,完成设备相关的任务。
* ===================================================================
*/
res = action->handler(irq, action->dev_id);

/* **内核跟踪点**:在ISR返回后,触发另一个跟踪事件,记录其返回值。*/
trace_irq_handler_exit(irq, action, res);

/*
* **健壮性检查**:一个严格的规则是,硬件中断处理程序(hardirq)绝不能重新使能中断。
* WARN_ONCE 宏会检查这个条件,如果发现中断被错误地使能了,它会打印一次内核警告,
* 并立即调用 local_irq_disable() 来纠正这个错误,防止系统崩溃。
*/
if (WARN_ONCE(!irqs_disabled(),"irq %u handler %pS enabled interrupts\n",
irq, action->handler))
local_irq_disable();

/* 根据ISR的返回值进行后续操作。*/
switch (res) {
case IRQ_WAKE_THREAD:
/*
* ISR请求唤醒对应的中断线程来完成耗时的工作。
* 这是中断处理“下半部”(bottom-half)机制的一种。
*/
/*
* 检查驱动是否犯了错误:返回了WAKE_THREAD,但没有提供线程处理函数。
*/
if (unlikely(!action->thread_fn)) {
warn_no_thread(irq, action); // 打印警告。
break;
}
/* 调用内部函数,将action对应的内核线程放入调度队列,使其能够运行。*/
__irq_wake_thread(desc, action);
break;

default:
/* 对于 IRQ_HANDLED 或 IRQ_NONE,不执行任何额外操作。*/
break;
}

/*
* 将当前ISR的返回值通过“或”运算合并到总的返回值中。
* 只要有一个ISR返回了IRQ_HANDLED,最终的retval就不会是IRQ_NONE。
*/
retval |= res;
}

/* 返回所有ISR处理结果的合并值。*/
return retval;
}

handle_irq_event_percpu 处理中断事件

1
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
/*
* handle_irq_event_percpu - 包装了实际事件处理的核心函数
* @desc: 目标中断描述符
*
* 返回值: irqreturn_t, 来自实际ISR的返回值
*/
irqreturn_t handle_irq_event_percpu(struct irq_desc *desc)
{
irqreturn_t retval; // 定义变量,用于存储ISR的返回值。

/*
* **核心委托**:调用内部函数 __handle_irq_event_percpu。
* 这才是真正干活的函数。它会遍历与此中断关联的action链表(desc->action),
* 并逐个调用其中注册的ISR函数,直到有一个返回IRQ_HANDLED。
* 整个中断处理的“业务逻辑”就在这一步被执行。
*/
retval = __handle_irq_event_percpu(desc);

/*
* --- 从这里开始,是系统级的后处理工作 ---
*/

/*
* **增加中断随机性**:将本次中断的信息(特别是发生的时间)加入到
* 内核的熵池(entropy pool)中。硬件中断的发生时机通常是不可预测的
* (受网络流量、用户输入等外部因素影响),因此它们是产生高质量随机数的
* 绝佳来源。这个熵池最终服务于 /dev/random 和 /dev/urandom,
* 为加密等安全相关的操作提供随机数支持。
*/
add_interrupt_randomness(desc->irq_data.irq);

/*
* 检查此中断是否设置了“不进行调试”的标志。对于一些性能极度敏感或
* 频率极高的中断,可以设置此标志以跳过下面的统计开销。
*/
if (!irq_settings_no_debug(desc))
/*
* **记录中断信息**:调用note_interrupt来更新内核的调试和统计数据。
* 这个函数会:
* 1. 检查retval,如果没有任何ISR处理该中断(IRQ_NONE),它会增加
* “未处理中断”(unhandled)或“伪中断”(spurious)的计数器。
* 2. 这些统计数据最终会展现在 /proc/stat 和 /proc/interrupts 文件中,
* 为系统管理员和内核开发者提供了极具价值的监控和调试信息。
*/
note_interrupt(desc, retval);

/* 将来自驱动ISR的原始返回值向上层层返回。*/
return retval;
}

handle_irq_event 处理中断事件

1
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
/*
* handle_irq_event - 处理一个中断描述符的事件链
* @desc: 目标中断描述符
*
* 返回值: irqreturn_t,通常是 IRQ_HANDLED 或 IRQ_NONE
*/
irqreturn_t handle_irq_event(struct irq_desc *desc)
{
irqreturn_t ret; // 定义一个变量,用于存放最终ISR的返回值。

/*
* 清除IRQS_PENDING标志位。这个标志位意味着一个中断正在等待处理
* (例如,在CPU亲和性变更时,中断需要被重新投递)。
* 在我们即将处理它之前,清除此标志,表示“我已收到,正在处理”。
*/
desc->istate &= ~IRQS_PENDING;

/*
* 设置IRQD_IRQ_INPROGRESS标志。这是一个非常重要的状态,它告诉内核的
* 其他部分(如电源管理、CPU热插拔代码),这个中断正在被积极地处理中,
* 请不要在此刻对它进行修改或禁用等操作。
*/
irqd_set(&desc->irq_data, IRQD_IRQ_INPROGRESS);

/*
* **关键步骤**:释放中断描述符的自旋锁 (desc->lock)。
* 为什么?因为接下来要调用的驱动程序ISR (在handle_irq_event_percpu内)
* 可能是耗时的。长时间持有自旋锁会严重影响系统性能,甚至导致死锁。
* 因此,内核框架在调用驱动代码前,明智地释放了这把锁,只保留
* IRQD_IRQ_INPROGRESS 状态作为“正在处理”的标记。
*/
raw_spin_unlock(&desc->lock);

/*
* **核心委托**:调用 per-CPU 的事件处理器。这个函数会查找与此中断关联的
* action链表 (desc->action),并执行其中注册的handler函数,
* 这就是设备驱动程序通过 request_irq() 提供的最终ISR。
* 函数的返回值(IRQ_HANDLED/IRQ_NONE)被保存在ret中。
*/
ret = handle_irq_event_percpu(desc);

/*
* 驱动程序的ISR执行完毕,现在重新获取锁,以便安全地更新中断描述符的状态。
*/
raw_spin_lock(&desc->lock);

/*
* 清除IRQD_IRQ_INPROGRESS标志,向内核的其他部分表明,对此中断的处理已完成。
*/
irqd_clear(&desc->irq_data, IRQD_IRQ_INPROGRESS);

/* 将驱动ISR的执行结果返回给上层调用者(如 handle_fasteoi_irq)。*/
return ret;
}

kernel/irq/irqdesc.c

generic_handle_domain_irq 处理域中断

1
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
int handle_irq_desc(struct irq_desc *desc)
{
struct irq_data *data;

if (!desc)
return -EINVAL;

data = irq_desc_get_irq_data(desc); //&desc->irq_data;
/* irqd_is_handle_enforce_irqctx()
__irqd_to_state(d) & IRQD_HANDLE_ENFORCE_IRQCTX; */
if (WARN_ON_ONCE(!in_hardirq() && irqd_is_handle_enforce_irqctx(data)))
return -EPERM;

generic_handle_irq_desc(desc); //desc->handle_irq(desc)
return 0;
}

/**
* generic_handle_domain_irq - Invoke the handler for a HW irq belonging
* to a domain.
* @domain: The domain where to perform the lookup
* @hwirq: The HW irq number to convert to a logical one
*
* Returns: 0 on success, or -EINVAL if conversion has failed
*
* This function must be called from an IRQ context with irq regs
* initialized.
*/
int generic_handle_domain_irq(struct irq_domain *domain, unsigned int hwirq)
{
return handle_irq_desc(irq_resolve_mapping(domain, hwirq));
}
EXPORT_SYMBOL_GPL(generic_handle_domain_irq);

alloc_desc 分配中断描述符

1
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
67
68
69
70
71
72
73
74
75
static void desc_set_defaults(unsigned int irq, struct irq_desc *desc, int node,
const struct cpumask *affinity, struct module *owner)
{
int cpu;

desc->irq_common_data.handler_data = NULL;
desc->irq_common_data.msi_desc = NULL;

desc->irq_data.common = &desc->irq_common_data;
desc->irq_data.irq = irq;
desc->irq_data.chip = &no_irq_chip;
desc->irq_data.chip_data = NULL;
irq_settings_clr_and_set(desc, ~0, _IRQ_DEFAULT_INIT_FLAGS);
irqd_set(&desc->irq_data, IRQD_IRQ_DISABLED);
irqd_set(&desc->irq_data, IRQD_IRQ_MASKED);
desc->handle_irq = handle_bad_irq;
desc->depth = 1;
desc->irq_count = 0;
desc->irqs_unhandled = 0;
desc->tot_count = 0;
desc->name = NULL;
desc->owner = owner;
for_each_possible_cpu(cpu)
*per_cpu_ptr(desc->kstat_irqs, cpu) = (struct irqstat) { };
desc_smp_init(desc, node, affinity);
}

static int init_desc(struct irq_desc *desc, int irq, int node,
unsigned int flags,
const struct cpumask *affinity,
struct module *owner)
{
desc->kstat_irqs = alloc_percpu(struct irqstat);
if (!desc->kstat_irqs)
return -ENOMEM;

if (alloc_masks(desc, node)) {
free_percpu(desc->kstat_irqs);
return -ENOMEM;
}

raw_spin_lock_init(&desc->lock);
lockdep_set_class(&desc->lock, &irq_desc_lock_class);
mutex_init(&desc->request_mutex);
init_waitqueue_head(&desc->wait_for_threads);
desc_set_defaults(irq, desc, node, affinity, owner);
irqd_set(&desc->irq_data, flags);
irq_resend_init(desc);
#ifdef CONFIG_SPARSE_IRQ
kobject_init(&desc->kobj, &irq_kobj_type);
init_rcu_head(&desc->rcu);
#endif

return 0;
}

static struct irq_desc *alloc_desc(int irq, int node, unsigned int flags,
const struct cpumask *affinity,
struct module *owner)
{
struct irq_desc *desc;
int ret;

desc = kzalloc_node(sizeof(*desc), GFP_KERNEL, node);
if (!desc)
return NULL;

ret = init_desc(desc, irq, node, flags, affinity, owner);
if (unlikely(ret)) {
kfree(desc);
return NULL;
}

return desc;
}

sparse_irqs 稀疏中断锁与映射树的定义

这两行代码共同定义了Linux内核中用于管理“稀疏中断”(Sparse IRQs)的核心数据结构及其同步锁。稀疏中断是指中断号(IRQ number)不连续、分布范围很广的场景。使用传统的数组来管理这种情况会浪费大量内存,因此内核采用了一种名为“枫树”(Maple Tree)的高效数据结构。

  1. struct maple_tree sparse_irqs: 定义并初始化一个枫树。这个树的数据结构被特别设计用于高效地存储和查找基于大范围索引(此处是中断号)的数据。
    • RCU保护: MT_FLAGS_USE_RCU标志使得对这棵树的读取操作(如在中断处理时查找中断描述符)可以无锁、非常快速地进行,这对于性能至关重要。
    • 外部锁: MT_FLAGS_LOCK_EXTERN标志告诉枫树的实现代码,写操作的同步将由外部提供的sparse_irq_lock来负责。
1
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
/*
* static 关键字表示下面定义的变量仅在当前文件内可见, 属于内部变量.
* DEFINE_MUTEX 是一个宏, 用于静态地定义并初始化一个名为 'sparse_irq_lock' 的互斥锁 (struct mutex).
* 这个锁将被用来保护下面定义的 sparse_irqs 树, 在对树进行修改(写操作)时必须持有此锁,
* 以防止多任务并发访问导致数据结构损坏.
*/
static DEFINE_MUTEX(sparse_irq_lock);
/*
* static 关键字表示下面定义的变量仅在当前文件内可见.
* struct maple_tree sparse_irqs: 定义一个名为 sparse_irqs 的枫树结构体变量.
* 枫树是Linux内核中的一种高效的、对缓存友好的B-树变种, 特别适合用于管理稀疏的、
* 基于整数索引的数据, 如此处的中断号.
*
* MTREE_INIT_EXT 是一个宏, 用于以扩展参数来静态初始化一个枫树.
* 第一个参数 'sparse_irqs' 是要初始化的树的名称.
* MT_FLAGS_ALLOC_RANGE: 标志位, 表示该树支持范围分配. 当需要一个新的中断号时,
* 可以高效地从树中找到一个未使用的范围.
* MT_FLAGS_LOCK_EXTERN: 标志位, 告诉枫树的内部实现, 它的写同步是由一个外部锁来管理的.
* 这个外部锁就是上面定义的 sparse_irq_lock.
* MT_FLAGS_USE_RCU: 标志位, 表示该树的读操作(查找)是受RCU(Read-Copy-Update)机制保护的.
* 这允许无锁遍历, 极大地提高了读性能.
* 最后一个参数 'sparse_irq_lock' 是当 MT_FLAGS_LOCK_EXTERN 被设置时, 指定的外部锁.
*/
static struct maple_tree sparse_irqs = MTREE_INIT_EXT(sparse_irqs,
MT_FLAGS_ALLOC_RANGE |
MT_FLAGS_LOCK_EXTERN |
MT_FLAGS_USE_RCU,
sparse_irq_lock);

irq_insert_desc 将中断描述符插入到稀疏中断数组中

1
2
3
4
5
static void irq_insert_desc(unsigned int irq, struct irq_desc *desc)
{
MA_STATE(mas, &sparse_irqs, irq, irq);
WARN_ON(mas_store_gfp(&mas, desc, GFP_KERNEL) != 0);
}

irq_to_desc 获取中断描述符

1
2
3
4
struct irq_desc *irq_to_desc(unsigned int irq)
{
return mtree_load(&sparse_irqs, irq);
}

early_irq_init 早期中断初始化

1
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
int __init early_irq_init(void)
{
int i, initcnt, node = first_online_node;
struct irq_desc *desc;

init_irq_default_affinity();

/* 让 arch 更新 nr_irqs 并返回预分配的 irq 的 nr */
initcnt = arch_probe_nr_irqs();
/* NR_IRQS: 16, nr_irqs: 16, preallocated irqs: 16 */
printk(KERN_INFO "NR_IRQS: %d, nr_irqs: %d, preallocated irqs: %d\n",
NR_IRQS, nr_irqs, initcnt);

if (WARN_ON(nr_irqs > MAX_SPARSE_IRQS))
nr_irqs = MAX_SPARSE_IRQS;

if (WARN_ON(initcnt > MAX_SPARSE_IRQS))
initcnt = MAX_SPARSE_IRQS;

if (initcnt > nr_irqs)
nr_irqs = initcnt;

for (i = 0; i < initcnt; i++) {
desc = alloc_desc(i, node, 0, NULL, NULL);
irq_insert_desc(i, desc);
}
return arch_early_irq_init();
}

irq_sysfs_init: 初始化中断相关的sysfs接口

此函数在内核启动的后核心阶段被调用, 其核心作用是在sysfs文件系统中创建并初始化用于中断(IRQ)管理的目录结构, 即/sys/kernel/irq/。它会遍历系统中所有已经分配的中断描述符, 并为每一个中断号在/sys/kernel/irq/下创建一个对应的子目录, 其中包含了描述该中断状态和属性的文件。

1
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
/*
* irq_sysfs_init: 中断 sysfs 接口的初始化函数.
* 这是一个静态函数, 标记为 __init, 表示它仅在内核初始化期间执行,
* 在初始化完成后, 其占用的内存可能会被回收.
* @return: 成功时返回0, 失败时返回负值的错误码.
*/
static int __init irq_sysfs_init(void)
{
/*
* desc: 一个指向 struct irq_desc 的指针. irq_desc 是内核中描述一个中断线所有属性的核心数据结构.
* irq: 一个整型变量, 用于在循环中存储当前的中断号.
*/
struct irq_desc *desc;
int irq;

/*
* 使用 guard(mutex) 宏来自动管理互斥锁的加锁和解锁.
* 这是一个现代C的RAII(资源获取即初始化)风格的写法.
* 它在进入作用域时对 sparse_irq_lock 进行加锁, 并在退出作用域时自动解锁.
* sparse_irq_lock 保护着用于分配和释放中断号的内核数据结构.
* 这里的目的是防止在遍历已分配中断列表时, 有其他代码并发地修改这个列表.
*/
guard(mutex)(&sparse_irq_lock);
/*
* 调用 kobject_create_and_add, 在 sysfs 中创建一个名为 "irq" 的目录.
* @ "irq": 目录的名称.
* @ kernel_kobj: 父 kobject 的指针, kernel_kobj 代表 /sys/kernel 目录.
* 因此, 这行代码会创建 /sys/kernel/irq/ 目录.
* 返回的 kobject 指针被存入全局变量 irq_kobj_base.
*/
irq_kobj_base = kobject_create_and_add("irq", kernel_kobj);
/*
* 检查目录是否创建成功. 如果失败(通常因为内存不足), 函数返回 NULL.
*/
if (!irq_kobj_base)
/*
* 如果创建失败, 返回 -ENOMEM (内存不足) 错误码.
*/
return -ENOMEM;

/* 添加已经分配了的中断 */
/*
* 使用 for_each_irq_desc 宏来遍历系统中所有有效的中断描述符.
* 这个宏会从 irq 0 循环到系统定义的最大中断号 (nr_irqs).
* 在每次循环中, irq 变量会得到当前的中断号, desc 指针会指向对应的 irq_desc 结构体.
*/
for_each_irq_desc(irq, desc)
/*
* 调用 irq_sysfs_add 函数, 为当前的中断号 irq 在 sysfs 中创建对应的条目.
* 这个函数会在 /sys/kernel/irq/ 下创建一个名为 irq (例如, "17") 的子目录,
* 并根据 desc 中的信息, 在该子目录下填充各种属性文件.
*/
irq_sysfs_add(irq, desc);
/*
* 所有操作成功, 返回0.
*/
return 0;
}
/*
* 使用 postcore_initcall 宏来注册 irq_sysfs_init 函数.
* 这会将该函数放入一个特定的初始化函数列表中, 确保它在内核启动过程中的
* "post-core" 阶段被调用. 这个阶段晚于核心子系统的初始化, 早于大部分设备驱动的初始化.
*/
postcore_initcall(irq_sysfs_init);

kernel/irq/irqdomain.c

中断域(IRQ Domain)的创建与实例化

这组函数共同构成了Linux内核irqchip子系统中创建和实例化中断域的底层核心机制。中断域的根本原理是提供一个通用的、可配置的软件对象(struct irq_domain), 作为硬件中断控制器在内核中的抽象表示。它负责将硬件特定的中断号(hwirq)翻译成内核全局统一的Linux IRQ号(virq), 从而实现了中断处理逻辑与具体硬件的解耦。

irq_domain_add_linear是这一组函数中最高层的API, 它为中断控制器驱动提供了一个便捷的接口来创建一个”线性映射”的中断域。线性映射是最简单的一种映射方式, 它假设硬件中断号(hwirq)从0到size-1是连续的, 并且内核可以为它们分配一段同样大小、连续的Linux IRQ号(virq)。

在STM32H750这样的系统中, 顶层的中断控制器驱动(如EXTI驱动)会使用这类函数来向内核注册自己, 建立起硬件和内核中断子系统之间的桥梁。


__irq_domain_create: 创建基础中断域对象

此函数是所有域创建流程的最底层基础, 负责分配内存并对struct irq_domain对象进行最基本的初始化。

1
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
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
/**
* __irq_domain_create - 创建一个基础的irq_domain对象
* @info: 指向包含此域所有配置信息的 irq_domain_info 结构体的指针.
* @return: 成功时返回一个指向新创建的 irq_domain 的指针, 失败时返回错误指针.
*/
static struct irq_domain *__irq_domain_create(const struct irq_domain_info *info)
{
/*
* 定义一个指向 irq_domain 的指针 domain, 用于存储新创建的域.
*/
struct irq_domain *domain;
/*
* 定义一个整型变量 err, 用于存储错误码.
*/
int err;

/*
* WARN_ON 是一个内核调试宏, 如果其条件为真, 会打印一条警告信息.
* 这里用于检查 info 结构体中是否存在逻辑上矛盾的配置.
* 例如, direct_max (直接映射) 与 size (需要线性映射表) 不能同时存在.
* 在STM32H750这样的系统上, 通常不使用直接映射(direct_max).
*/
if (WARN_ON((info->size && info->direct_max) ||
(!IS_ENABLED(CONFIG_IRQ_DOMAIN_NOMAP) && info->direct_max) ||
(info->direct_max && info->direct_max != info->hwirq_max)))
return ERR_PTR(-EINVAL); /* 如果配置有误, 返回"无效参数"错误. */

/*
* 使用 kzalloc_node 分配内存.
* struct_size(domain, revmap, info->size): 这是一个灵活数组技巧. 如果 info->size > 0,
* 它会计算 struct irq_domain 的大小, 再加上一个能容纳 info->size 个条目的 revmap 数组的大小.
* revmap (反向映射表) 用于从Linux IRQ号快速查找到硬件中断号.
* GFP_KERNEL: 标准的内存分配标志, 在此上下文中表示如果内存不足可以休眠等待.
* of_node_to_nid(to_of_node(info->fwnode)): 用于NUMA(非统一内存访问)系统, 将内存分配在与设备
* 亲和的节点上. 对于STM32H750这种单芯片系统, 这通常会解析为节点0.
*/
domain = kzalloc_node(struct_size(domain, revmap, info->size),
GFP_KERNEL, of_node_to_nid(to_of_node(info->fwnode)));
if (!domain)
return ERR_PTR(-ENOMEM); /* 内存分配失败, 返回"内存不足"错误. */

/*
* 调用 irq_domain_set_name 为这个新域设置一个易于调试的名称.
*/
err = irq_domain_set_name(domain, info);
if (err) {
kfree(domain); /* 如果设置名称失败, 释放之前分配的内存. */
return ERR_PTR(err);
}

/*
* 获取对固件节点(fwnode, 通常是设备树节点)的引用计数, 防止其被提前释放.
*/
domain->fwnode = fwnode_handle_get(info->fwnode);
/*
* 标记该固件节点已被一个设备(即此irq_domain)初始化, 防止重复使用.
*/
fwnode_dev_initialized(domain->fwnode, true);

/* 填充结构体的其余字段 */
/*
* 初始化用于动态映射的基数树(radix tree). 对于线性映射, 这个树可能不被使用.
*/
INIT_RADIX_TREE(&domain->revmap_tree, GFP_KERNEL);
/*
* 将驱动提供的操作函数集(ops)关联到域.
*/
domain->ops = info->ops;
/*
* 将驱动提供的私有数据(host_data)关联到域, 以便在回调函数中可以访问.
*/
domain->host_data = info->host_data;
/*
* 总线令牌(bus_token), 用于某些总线的特殊识别.
*/
domain->bus_token = info->bus_token;
/*
* 此域能管理的最大硬件中断号.
*/
domain->hwirq_max = info->hwirq_max;

/*
* 如果使用了直接映射(no-map)模式, 则设置相应的标志.
*/
if (info->direct_max)
domain->flags |= IRQ_DOMAIN_FLAG_NO_MAP;

/*
* 线性反向映射表的大小.
*/
domain->revmap_size = info->size;

/*
* 锁与层次结构初始化:
* 初始化此域自身的互斥锁. 在单核抢占式系统上, 它可以防止任务切换导致的竞态条件.
*/
mutex_init(&domain->mutex);
/*
* 对于非层次化域, 其根域就是它自己. 这使得 &domain->root->mutex 总是指向正确的锁.
*/
domain->root = domain;

/*
* 执行一次层次结构检查, 确保配置正确.
*/
irq_domain_check_hierarchy(domain);

/*
* 返回成功创建的基础域对象. 此时它仅存在于内存中, 尚未对内核其他部分可见.
*/
return domain;
}

__irq_domain_publish: 发布中断域至全局

此函数负责将一个已经创建好的域”发布”出去, 使其对内核可见。

1
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
/**
* __irq_domain_publish - 将一个已创建的域发布到全局
* @domain: 要发布的 irq_domain 对象.
*/
static void __irq_domain_publish(struct irq_domain *domain)
{
/*
* 获取一个全局的互斥锁 irq_domain_mutex, 用于保护全局的 irq_domain_list 链表.
* 这确保了链表添加操作的原子性.
*/
mutex_lock(&irq_domain_mutex);
/*
* 在debugfs文件系统(/sys/kernel/debug/irq/domains/)中为该域创建一个调试目录.
*/
debugfs_add_domain_dir(domain);
/*
* 将该域添加到全局的 irq_domain_list 链表中.
* 从这一刻起, 内核的其他部分(如 irq_find_host)就可以找到这个域了.
*/
list_add(&domain->link, &irq_domain_list);
/*
* 释放全局互斥锁.
*/
mutex_unlock(&irq_domain_mutex);

/*
* 打印一条调试信息, 宣告域已添加.
*/
pr_debug("Added domain %s\n", domain->name);
}

__irq_domain_instantiate: 实例化并配置中断域

这是核心的实例化函数, 它调用基础创建函数, 并执行所有高级配置, 如建立层次化关系、调用驱动初始化回调等。

1
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
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
/**
* __irq_domain_instantiate - 实例化并配置一个新的 irq_domain 数据结构
* @info: 指向此域配置信息的指针.
* @cond_alloc_descs: 是否有条件地分配 irq_desc 描述符.
* @force_associate: 是否强制进行静态的hwirq-virq关联.
* @return: 指向实例化的irq域的指针或一个错误指针.
*/
static struct irq_domain *__irq_domain_instantiate(const struct irq_domain_info *info,
bool cond_alloc_descs, bool force_associate)
{
/*
* 定义一个指向 irq_domain 的指针 domain.
*/
struct irq_domain *domain;
/*
* 定义一个整型变量 err, 用于存储错误码.
*/
int err;

/*
* 步骤1: 创建基础的域对象.
*/
domain = __irq_domain_create(info);
if (IS_ERR(domain))
return domain;

/*
* 步骤2: 从info结构体中复制域的标志和可选的exit回调函数.
*/
domain->flags |= info->domain_flags;
domain->exit = info->exit;
domain->dev = info->dev;

/*
* 步骤3: 如果这是一个层次化域 (即info中指定了父域), 则建立父子关系.
* 这个宏只在内核配置了 CONFIG_IRQ_DOMAIN_HIERARCHY 时才存在.
*/
#ifdef CONFIG_IRQ_DOMAIN_HIERARCHY
if (info->parent) {
/*
* 关键: 子域的根域(root)必须与其父域的根域相同, 这样它们共享同一个锁.
*/
domain->root = info->parent->root;
/*
* 设置父域指针.
*/
domain->parent = info->parent;
}
#endif

/*
* 步骤4: 如果驱动提供了 dgc_info, 则为该域分配一组通用的 irq_chip.
*/
if (info->dgc_info) {
err = irq_domain_alloc_generic_chips(domain, info->dgc_info);
if (err)
goto err_domain_free;
}

/*
* 步骤5: 调用驱动提供的自定义初始化回调函数 (init), 以执行特定于硬件的设置.
*/
if (info->init) {
err = info->init(domain);
if (err)
goto err_domain_gc_remove; /* 如果失败, 跳转到清理路径. */
}

/*
* 步骤6: 将完全配置好的域发布出去, 使其对内核可见.
*/
__irq_domain_publish(domain);

/*
* 步骤7 (可选): 如果内核配置了稀疏IRQ(SPARSE_IRQ), 并且指定了virq基地址,
* 则预先为该域分配 irq_desc 描述符.
*/
if (cond_alloc_descs && info->virq_base > 0)
irq_domain_instantiate_descs(info);

/*
* 步骤8 (可选, 主要用于旧式驱动):
* 如果强制要求或指定了virq基地址, 则执行一个静态的、一对一的 hwirq 到 virq 的关联.
*/
if (force_associate || info->virq_base > 0) {
irq_domain_associate_many(domain, info->virq_base, info->hwirq_base,
info->size - info->hwirq_base);
}

/*
* 所有步骤成功, 返回指向完全实例化的域的指针.
*/
return domain;

/*
* 错误处理的清理路径. 使用goto可以确保在任何步骤失败时,
* 所有已经成功分配的资源都会被以相反的顺序正确地释放.
*/
err_domain_gc_remove:
if (info->dgc_info)
irq_domain_remove_generic_chips(domain);
err_domain_free:
irq_domain_free(domain);
return ERR_PTR(err);
}

irq_domain_instantiate: 实例化中断域 (公共API)

这是一个简单的封装函数, 向驱动程序提供了一个更简洁的API。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* irq_domain_instantiate() - 实例化一个新的 irq domain 数据结构
* @info: 指向此域配置信息的指针
*
* 返回: 一个指向实例化的irq域的指针或一个ERR_PTR值.
*/
struct irq_domain *irq_domain_instantiate(const struct irq_domain_info *info)
{
/*
* 调用核心实现函数, 并为内部使用的标志 cond_alloc_descs 和 force_associate 传递 false.
*/
return __irq_domain_instantiate(info, false, false);
}
EXPORT_SYMBOL_GPL(irq_domain_instantiate);

irq_domain_add_linear: 创建并注册一个线性中断域

这是一个提供给驱动使用的高层便捷API, 专门用于创建线性的、非层次化的中断域。

1
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
/**
* irq_domain_add_linear() - 分配并注册一个线性反向映射(revmap)的 irq_domain.
* @of_node: 指向中断控制器设备树节点的指针.
* @size: 域中中断的数量.
* @ops: map/unmap域的回调函数集.
* @host_data: 控制器的私有数据指针.
*/
static inline struct irq_domain *irq_domain_add_linear(struct device_node *of_node,
unsigned int size,
const struct irq_domain_ops *ops,
void *host_data)
{
/*
* 步骤1: 将所有传入的参数打包到一个临时的 irq_domain_info 结构体中.
* 这种方式非常清晰, 且利用了C99的指定初始化语法.
*/
struct irq_domain_info info = {
.fwnode = of_node_to_fwnode(of_node),
.size = size,
.hwirq_max = size,
.ops = ops,
.host_data = host_data,
};
/*
* 定义一个指向 irq_domain 的指针 d.
*/
struct irq_domain *d;

/*
* 步骤2: 调用通用的实例化函数来完成所有的创建、配置和发布工作.
*/
d = irq_domain_instantiate(&info);
/*
* 步骤3: 转换返回值.
* 内核内部通常使用 IS_ERR() 和 PTR_ERR() 来处理错误指针,
* 但为了给API调用者提供更简洁的接口, 这里将错误指针转换为标准的NULL.
* 调用者只需检查返回值是否为NULL即可判断操作是否成功.
*/
return IS_ERR(d) ? NULL : d;
}

IRQ Domain Instantiation and Hierarchy Creation

此代码片段是Linux内核中断子系统(irqchip)中用于**实例化(instantiate)和创建层级化中断域(hierarchical IRQ domain)**的核心函数。它的核心原理是提供一套结构化的API, 允许中断控制器驱动程序(如STM32的EXTI或GPIO驱动)将其硬件能力注册到内核中, 创建一个名为irq_domain的软件对象。这个对象是硬件和内核通用中断处理逻辑之间的关键翻译层, 负责将硬件中断号(hwirq)映射到内核可以识别和使用的、全局唯一的Linux IRQ号(virq)。


irq_domain_create_hierarchy: (API) 创建一个层级化中断域

这是一个提供给驱动程序使用的高级内联函数, 也是我们之前分析的stm32_gpiolib_register_bank函数实际调用的API。它的作用是创建一个新的中断域, 并将其明确地链接到一个已存在的父域(parent)之下, 形成一个层次结构。

原理: 此函数是一个便捷的封装器(wrapper)。它并不执行复杂的逻辑, 而是将调用者传入的所有独立参数(如父域、标志、回调函数等)打包进一个标准的struct irq_domain_info结构体中, 然后调用更底层的irq_domain_instantiate来完成真正的实例化工作。这种设计简化了API, 使得创建子域的代码更清晰、更不易出错。在STM32中, GPIO Bank的中断域就是作为EXTI主中断域的子域来创建的。

1
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
/*
* 静态内联函数: irq_domain_create_hierarchy - 将一个irqdomain添加到层次结构中
* @parent: 要与新域关联的父irq域.
* @flags: 与此域关联的Irq domain标志.
* @size: 域的大小.
* @fwnode: 中断控制器的可选fwnode句柄.
* @ops: 指向中断域回调函数集的指针.
* @host_data: 控制器私有数据指针.
*/
static inline struct irq_domain *irq_domain_create_hierarchy(struct irq_domain *parent,
unsigned int flags, unsigned int size,
struct fwnode_handle *fwnode,
const struct irq_domain_ops *ops,
void *host_data)
{
/* 使用C99的指定初始化语法, 将所有传入的参数打包到一个临时的info结构体中. */
const struct irq_domain_info info = {
.fwnode = fwnode,
.size = size,
.hwirq_max = size ? : ~0U, /* 如果size不为0, 最大hwirq就是size, 否则是最大无符号整数. */
.ops = ops, /* 核心操作函数集. */
.host_data = host_data, /* 驱动的私有数据. */
.domain_flags = flags, /* 域的标志. */
.parent = parent, /* 关键: 设置父域. */
};
/* 调用实例化函数来创建域. */
struct irq_domain *d = irq_domain_instantiate(&info);

/* 将可能返回的错误指针(ERR_PTR)转换为更传统的NULL, 以简化调用者的错误检查. */
return IS_ERR(d) ? NULL : d;
}

irq_domain_instantiate & __irq_domain_instantiate: (核心) 实例化中断域

irq_domain_instantiate是一个通用的API, 它调用内部核心函数__irq_domain_instantiate来执行中断域的创建和初始化全过程。

原理: __irq_domain_instantiate是一个复杂的多阶段构造函数。它按照严格的顺序执行一系列操作, 并带有健壮的错误回滚机制:

  1. 分配与链接: 分配irq_domain结构体, 并根据info中的parent指针建立与父域的链接。
  2. 分配芯片(Chips): 如果需要, 为这个域分配并关联一组通用的irq_chip结构。irq_chip包含了真正操作硬件的函数指针(如irq_mask, irq_unmask)。
  3. 驱动回调: 调用驱动程序通过info->init提供的自定义初始化回调函数, 允许驱动执行特定的硬件设置。
  4. 发布(Publish): 将新创建的域添加到一个全局链表中, 使其对内核其他部分可见。
  5. 分配描述符: 调用irq_domain_instantiate_descs为该域将要管理的Linux IRQ号预先分配irq_desc数据结构。
  6. 关联映射: 如果需要, 调用irq_domain_associate_many预先建立硬件中断号到Linux IRQ号的映射关系。
1
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
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
static void __irq_domain_publish(struct irq_domain *domain)
{
mutex_lock(&irq_domain_mutex);
debugfs_add_domain_dir(domain);
list_add(&domain->link, &irq_domain_list);
mutex_unlock(&irq_domain_mutex);

pr_debug("Added domain %s\n", domain->name);
}

/*
* __irq_domain_instantiate: 实例化新irq域的核心内部函数.
*/
static struct irq_domain *__irq_domain_instantiate(const struct irq_domain_info *info,
bool cond_alloc_descs, bool force_associate)
{
struct irq_domain *domain;
int err;

/* 步骤1: 分配irq_domain结构体并进行基础设置. */
domain = __irq_domain_create(info);
if (IS_ERR(domain))
return domain;

/* 从info结构体中复制基本属性. */
domain->flags |= info->domain_flags;
domain->exit = info->exit;
domain->dev = info->dev;

#ifdef CONFIG_IRQ_DOMAIN_HIERARCHY
/* 步骤2: 建立层次结构链接. */
if (info->parent) {
domain->root = info->parent->root; /* 继承根域. */
domain->parent = info->parent; /* 设置直接父域. */
}
#endif

/* 步骤3: 如果info中提供了dgc_info, 则分配并关联通用的irq_chip. */
if (info->dgc_info) {
err = irq_domain_alloc_generic_chips(domain, info->dgc_info);
if (err)
goto err_domain_free; /* 失败则跳转到清理流程. */
}

/* 步骤4: 如果驱动提供了init回调, 执行它. */
if (info->init) {
err = info->init(domain);
if (err)
goto err_domain_gc_remove;
}

/* 步骤5: 将域发布到全局列表, 使其可见. */
__irq_domain_publish(domain);

/* 步骤6: 根据条件, 为此域分配irq_desc描述符. */
if (cond_alloc_descs && info->virq_base > 0)
irq_domain_instantiate_descs(info);

/* 步骤7: 对于旧式或静态映射的域, 立即创建hwirq到virq的映射. */
if (force_associate || info->virq_base > 0) {
irq_domain_associate_many(domain, info->virq_base, info->hwirq_base,
info->size - info->hwirq_base);
}

return domain;

/* 健壮的错误处理: 如果任何步骤失败, 按相反顺序撤销已完成的操作. */
err_domain_gc_remove:
if (info->dgc_info)
irq_domain_remove_generic_chips(domain);
err_domain_free:
irq_domain_free(domain);
return ERR_PTR(err);
}

/**
* irq_domain_instantiate() - 实例化一个新的irq域数据结构
*/
struct irq_domain *irq_domain_instantiate(const struct irq_domain_info *info)
{
/* 调用核心实现, 使用默认的非旧式(non-legacy)参数. */
return __irq_domain_instantiate(info, false, false);
}
EXPORT_SYMBOL_GPL(irq_domain_instantiate);

irq_domain_instantiate_descs: 分配中断描述符

原理: 在配置了CONFIG_SPARSE_IRQ的内核中, irq_desc(内核中代表一个Linux IRQ号的核心数据结构)不是在启动时为所有可能的IRQ号静态分配的, 而是按需动态分配, 以节省内存。此函数就负责为新创建的irq_domain将要使用的一段连续的Linux IRQ号(virq_basevirq_base + size)动态分配所需的irq_desc结构体。这对于资源受限的STM32H750等MCU系统是重要的内存优化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/*
* irq_domain_instantiate_descs: 实例化(分配)中断描述符.
*/
static void irq_domain_instantiate_descs(const struct irq_domain_info *info)
{
/* 此功能仅在CONFIG_SPARSE_IRQ内核配置下有效. */
if (!IS_ENABLED(CONFIG_SPARSE_IRQ))
return;

/* 调用irq_alloc_descs为指定的virq范围分配irq_desc结构体. */
if (irq_alloc_descs(info->virq_base, info->virq_base, info->size,
of_node_to_nid(to_of_node(info->fwnode))) < 0) {
/* 如果分配失败, 打印一条信息, 并假设它们已被预分配(作为一种容错). */
pr_info("Cannot allocate irq_descs @ IRQ%d, assuming pre-allocated\n",
info->virq_base);
}
}

Linux中断域查找系列函数

这一系列函数是Linux内核中断子系统的核心组成部分, 它们提供了一套API来根据设备树节点(device_node)或更通用的固件节点句柄(fwnode_handle)查找并返回已注册的irq_domain(中断域)。irq_domain的主要职责是作为一个翻译层, 将硬件特定的中断信息(如中断线编号、触发类型)转换为内核全局唯一的IRQ号。


irq_find_matching_fwspec: 根据固件规约查找匹配的中断域

这是该系列函数中的核心和基础。它遍历系统中所有已注册的irq_domain, 并根据每个域驱动提供的匹配方法, 寻找第一个能够响应该中断请求的域。

原理:
该函数的核心是遍历一个全局的链表 irq_domain_list, 这个链表包含了系统中所有通过irq_domain_add_*系列函数注册的中断域。为了保证链表操作的线程安全(因为中断域可能在运行时被动态添加或移除), 整个遍历过程都被一个互斥锁 irq_domain_mutex 保护。

对于链表中的每一个中断域, 它会按以下优先级顺序尝试进行匹配:

  1. select() 方法: 如果域的操作函数集(ops)提供了select回调, 并且请求中指定了具体的总线类型(bus_token), 则优先使用select方法。这是最现代、最精确的匹配方式, 允许一个域根据详细的固件规约(fwspec)和总线类型来决定是否处理该中断。
  2. match() 方法: 如果没有select方法或bus_token为任意匹配, 则尝试使用传统的match回调。这个方法主要基于设备树节点(device_node)和总线类型进行匹配, 兼容了旧的驱动。
  3. 直接比较: 如果上述两个回调函数都未提供, 则执行一个最简单的默认匹配: 检查请求的固件节点句柄(fwnode)是否与中断域自身注册时保存的固件节点句柄(h->fwnode)完全相同。

一旦找到匹配项, 立即停止搜索并返回该中断域。

在STM32H750这样的单核抢占式系统上, mutex_lock(&irq_domain_mutex) 依然是必需的。它能防止一个正在遍历链表的任务被另一个更高优先级的任务抢占, 而后者可能会通过加载或卸载模块来修改irq_domain_list, 从而避免数据竞争和链表损坏。

1
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
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
/**
* irq_find_matching_fwspec() - 为给定的固件规约(fwspec)定位一个中断域
* @fwspec: 指向中断的固件规约(FW specifier)的指针.
* @bus_token: 特定于域的数据, 用于区分总线类型(如直连、MSI等).
*/
struct irq_domain *irq_find_matching_fwspec(struct irq_fwspec *fwspec,
enum irq_domain_bus_token bus_token)
{
/*
* h: 用作循环变量, 遍历 irq_domain_list.
* found: 存储找到的中断域, 初始化为 NULL.
*/
struct irq_domain *h, *found = NULL;
/*
* 从 fwspec 中获取固件节点句柄.
*/
struct fwnode_handle *fwnode = fwspec->fwnode;
/*
* rc: 存储匹配函数的返回值.
*/
int rc;

/*
* 对全局的 irq_domain_mutex 互斥锁进行加锁.
* 这确保了在遍历 irq_domain_list 链表时, 不会有其他内核任务(如加载/卸载驱动)同时修改这个链表.
* 在单核抢占式内核上, 这可以防止竞态条件.
*/
mutex_lock(&irq_domain_mutex);
/*
* 遍历全局的 irq_domain_list 链表, h 指向链表中的每一个中断域实例.
*/
list_for_each_entry(h, &irq_domain_list, link) {
/*
* 优先级1: 检查中断域的操作集(ops)是否定义了 select 方法,
* 并且调用者请求的不是任意总线类型 (DOMAIN_BUS_ANY).
*/
if (h->ops->select && bus_token != DOMAIN_BUS_ANY)
/*
* 如果满足条件, 调用 select 方法进行匹配. 这是最精确的匹配方式.
*/
rc = h->ops->select(h, fwspec, bus_token);
/*
* 优先级2: 如果 select 方法不适用, 检查是否定义了 match 方法.
*/
else if (h->ops->match)
/*
* 调用 match 方法进行匹配. to_of_node 将通用的 fwnode 句柄转换为设备树的 device_node.
* 这是传统的匹配方式.
*/
rc = h->ops->match(h, to_of_node(fwnode), bus_token);
else
/*
* 优先级3: 如果 select 和 match 都没有定义, 执行默认的直接比较.
* 条件: fwnode 必须有效, 且与中断域 h 注册的 fwnode 相同,
* 并且 bus_token 是任意匹配, 或者与中断域 h 注册的 bus_token 相同.
*/
rc = ((fwnode != NULL) && (h->fwnode == fwnode) &&
((bus_token == DOMAIN_BUS_ANY) ||
(h->bus_token == bus_token)));

/*
* 如果匹配成功 (rc 返回值为真).
*/
if (rc) {
/*
* 将找到的中断域赋值给 found.
*/
found = h;
/*
* 退出循环, 因为已经找到了第一个匹配项.
*/
break;
}
}
/*
* 解锁互斥锁.
*/
mutex_unlock(&irq_domain_mutex);
/*
* 返回找到的中断域 (如果没找到, 则返回 NULL).
*/
return found;
}
/*
* 将 irq_find_matching_fwspec 函数导出, 使其对其他内核模块可用 (遵循GPL许可证).
*/
EXPORT_SYMBOL_GPL(irq_find_matching_fwspec);

irq_find_matching_fwnode 与 irq_find_matching_host (内联函数)

这两个 static inline 函数是基于 irq_find_matching_fwspec 的便捷封装。在STM32这样的嵌入式系统中, inline关键字建议编译器将函数体直接嵌入到调用处, 从而消除函数调用的开销, 提高执行效率。

1
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
/*
* irq_find_matching_fwnode: 根据一个通用的固件节点句柄(fwnode)查找中断域.
* 这是一个内联封装函数.
* @fwnode: 指向设备固件节点的句柄.
* @bus_token: 域的总线类型.
* @return: 查找到的 irq_domain 指针或 NULL.
*/
static inline struct irq_domain *irq_find_matching_fwnode(struct fwnode_handle *fwnode,
enum irq_domain_bus_token bus_token)
{
/*
* 在栈上创建一个临时的 irq_fwspec 结构体.
*/
struct irq_fwspec fwspec = {
/*
* 将传入的 fwnode 赋值给 fwspec 的 fwnode 成员.
*/
.fwnode = fwnode,
};

/*
* 调用核心函数 irq_find_matching_fwspec 进行查找, 并返回其结果.
* 这样调用者就不需要自己手动创建 irq_fwspec 结构体.
*/
return irq_find_matching_fwspec(&fwspec, bus_token);
}

/*
* irq_find_matching_host: 根据一个设备树节点(device_node)查找中断域.
* 这是针对设备树系统最常用的内联封装函数.
* @node: 指向设备树节点的指针.
* @bus_token: 域的总线类型.
* @return: 查找到的 irq_domain 指针或 NULL.
*/
static inline struct irq_domain *irq_find_matching_host(struct device_node *node,
enum irq_domain_bus_token bus_token)
{
/*
* of_fwnode_handle(node) 将一个 device_node 指针转换为通用的 fwnode_handle 指针,
* 然后调用上一层的封装函数.
*/
return irq_find_matching_fwnode(of_fwnode_handle(node), bus_token);
}

irq_find_host: 查找中断主控制器

这是最高层的封装函数, 也是在设备驱动中最常被调用的函数 (如上一轮问题中的stm32_pctrl_get_irq_domain就调用了它)。它提供了一个带回退机制的简单接口来查找中断域。

原理:
它体现了一种”先精确, 后模糊”的查找策略。首先, 它尝试查找一个总线类型为DOMAIN_BUS_WIRED的域, 这通常代表一个与设备直接硬线连接的、标准的片上中断控制器(如STM32的EXTI)。如果查找失败, 它会放宽条件, 再次尝试查找任意总线类型(DOMAIN_BUS_ANY)的域。这种策略提高了在复杂系统中查找成功率, 同时优先选择最匹配的控制器。

1
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
/*
* irq_find_host: 查找与设备树节点关联的中断主控制器(irq_domain).
* 这是一个常用的高级API.
* @node: 指向设备树节点的指针, 该节点通常是中断控制器本身.
* @return: 查找到的 irq_domain 指针或 NULL.
*/
static inline struct irq_domain *irq_find_host(struct device_node *node)
{
struct irq_domain *d;

/*
* 第一次尝试: 精确查找.
* 查找总线类型为 DOMAIN_BUS_WIRED (硬线连接) 的中断域.
*/
d = irq_find_matching_host(node, DOMAIN_BUS_WIRED);
/*
* 检查第一次尝试是否失败 (d 为 NULL).
*/
if (!d)
/*
* 第二次尝试: 模糊查找.
* 如果第一次失败, 则查找任意总线类型 (DOMAIN_BUS_ANY) 的中断域.
*/
d = irq_find_matching_host(node, DOMAIN_BUS_ANY);

/*
* 返回最终找到的结果.
*/
return d;
}

IRQ Domain 数据关联与设置函数

这两个函数是Linux内核中断域(irq_domain)框架中用于数据查找和配置的核心API。它们协同工作, 在一个层级化的中断控制器结构中, 确保一个虚拟的Linux IRQ号(virq)能够被正确地关联到其在特定硬件层级上的表示(irq_data), 并能够被正确地配置其硬件属性(如硬件中断号hwirq和操作函数集irq_chip)。


irq_domain_get_irq_data: 在指定域中查找中断数据

此函数的核心作用是: 在一个可能存在多级中断控制器的层级结构中, 为一个给定的Linux IRQ号 (virq), 查找并返回它在某一个特定中断域 (domain) 内的上下文表示 (struct irq_data)

在层级化的中断系统中(例如, GPIO Bank -> EXTI -> NVIC), 一个单一的virq会对应一个irq_data的链表, 链表中的每个节点都代表了该virq在某一硬件层级上的视图。此函数的原理就是遍历这个表示层级关系的链表, 找到属于我们目标domain的那个节点。

1
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
/**
* irq_domain_get_irq_data - 获取与@virq和@domain关联的irq_data
* @domain: 要匹配的域
* @virq: 要获取irq_data的IRQ号
*/
struct irq_data *irq_domain_get_irq_data(struct irq_domain *domain,
unsigned int virq)
{
/* 定义一个指向 irq_data 的指针, 用作循环变量. */
struct irq_data *irq_data;

/*
* 这是一个for循环, 用于在一个中断的层级结构中向上遍历.
*
* 初始化: irq_data = irq_get_irq_data(virq)
* irq_get_irq_data()返回与virq关联的最顶层(也就是最具体的子控制器)的irq_data.
* 在STM32的例子中, 这会是属于GPIO Bank域的irq_data.
*
* 循环条件: irq_data
* 只要当前的irq_data指针不为NULL, 就继续循环.
*
* 迭代: irq_data = irq_data->parent_data
* 这是遍历的核心. irq_data结构体中有一个指向其父域对应irq_data的指针.
* 通过这个指针, 循环可以从子域(GPIO)走到父域(EXTI), 再到父域的父域(NVIC)...
*/
for (irq_data = irq_get_irq_data(virq); irq_data;
irq_data = irq_data->parent_data)
/*
* 在每一层, 检查当前irq_data所属的域(irq_data->domain)是否
* 与我们正在寻找的目标域(domain)相同.
*/
if (irq_data->domain == domain)
/* 如果找到了匹配的域, 就返回这个irq_data指针. */
return irq_data;

/* 如果遍历完整个层级链表都没有找到匹配的域, 返回NULL. */
return NULL;
}
/* 将此函数导出, 使其对其他遵循GPL许可证的内核模块可用. */
EXPORT_SYMBOL_GPL(irq_domain_get_irq_data);

irq_domain_set_hwirq_and_chip: 为域中的中断设置硬件号和芯片

此函数的核心作用是: 为一个已经分配好的Linux IRQ号(virq), 在其指定的域(domain)内, 填充其最重要的硬件属性。这是在中断分配流程(.alloc回调)中的一个关键配置步骤。

其原理非常直接:

  1. 它首先必须找到正确的”配置表格”, 即调用irq_domain_get_irq_data来获取virq在该domain内的irq_data结构体。
  2. 找到后, 它就将调用者提供的硬件信息——硬件中断号(hwirq)、硬件操作函数集(chip)以及私有数据(chip_data)——直接填入到这个irq_data结构体的相应字段中。

完成这一步后, 内核就拥有了管理这个中断在当前硬件层级上所需的所有信息。

1
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
/**
* irq_domain_set_hwirq_and_chip - 为@virq在@domain中设置hwirq和irqchip
* @domain: 要匹配的中断域
* @virq: IRQ号
* @hwirq: 硬件中断号
* @chip: 关联的中断芯片(操作函数集)
* @chip_data: 关联的芯片私有数据
*/
int irq_domain_set_hwirq_and_chip(struct irq_domain *domain, unsigned int virq,
irq_hw_number_t hwirq,
const struct irq_chip *chip,
void *chip_data)
{
/*
* 步骤1: 获取virq在当前domain下的上下文表示(irq_data).
* 这是为了确保我们配置的是正确的层级.
*/
struct irq_data *irq_data = irq_domain_get_irq_data(domain, virq);

/* 如果返回NULL, 说明virq与此domain无关, 这是一个错误. */
if (!irq_data)
return -ENOENT; /* 返回"无此条目"错误. */

/*
* 步骤2: 填充硬件信息.
*
* 将硬件中断号(例如, GPIO引脚号0-15)存入irq_data.
* 这建立了 virq -> hwirq 的映射.
*/
irq_data->hwirq = hwirq;
/*
* 将中断芯片操作函数集(irq_chip)存入irq_data.
* irq_chip包含了像 .irq_mask, .irq_unmask 这样实际操作硬件的函数指针.
* (chip ? chip : &no_irq_chip) 是一个安全检查: 如果传入的chip为NULL,
* 则使用一个空的、无操作的 no_irq_chip 来防止后续空指针解引用.
*/
irq_data->chip = (struct irq_chip *)(chip ? chip : &no_irq_chip);
/*
* 将芯片私有数据存入irq_data.
* 当调用chip内的函数时, 这个数据会作为参数传回, 使得函数知道要操作哪个硬件实例.
* 在STM32 GPIO驱动中, 这通常是指向 stm32_gpio_bank 结构体的指针.
*/
irq_data->chip_data = chip_data;

/* 配置成功, 返回0. */
return 0;
}

中断反向映射查找函数

此代码片段展示了Linux内核中断子系统中用于反向查找(reverse lookup)的核心函数。其根本原理是提供一个高效的机制, 根据中断控制器域(domain)和硬件中断号(hwirq), 快速地找到与之关联的Linux IRQ号(virq)以及管理该IRQ的核心数据结构struct irq_desc

这个功能是irqchip子系统能够防止重复映射、正确管理中断资源的基础。例如, 在调用irq_create_mapping之前, 内核必须先调用irq_find_mapping来检查是否已经为某个hwirq创建了映射。


__irq_resolve_mapping: 从hwirq解析映射的核心实现

这是该系列中最低层的核心函数, 负责执行实际的查找操作。它设计用来支持中断域的多种不同映射策略。

1
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
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
/**
* __irq_resolve_mapping() - 从一个hw irq号查找一个linux irq.
* @domain: 拥有此硬件中断的域
* @hwirq: 该域空间中的硬件irq号
* @irq: (可选)用于返回Linux irq号的指针
*
* 返回: 中断描述符(irq_desc).
*/
struct irq_desc *__irq_resolve_mapping(struct irq_domain *domain,
irq_hw_number_t hwirq,
unsigned int *irq)
{
struct irq_desc *desc = NULL; // 初始化返回的描述符为NULL
struct irq_data *data;

/* 如果没有提供域, 尝试使用全局默认域. */
if (domain == NULL)
domain = irq_default_domain;
if (domain == NULL)
return desc;

/*
* 情况1: 直接映射域 (NOMAP domain).
* 在这种模式下, hwirq 和 virq 的值是相等的.
* 这种模式在某些嵌入式系统中用于简化映射, 但在STM32中不常用.
*/
/*
static bool irq_domain_is_nomap(struct irq_domain *domain)
{
return IS_ENABLED(CONFIG_IRQ_DOMAIN_NOMAP) &&
(domain->flags & IRQ_DOMAIN_FLAG_NO_MAP);
}
*/
if (irq_domain_is_nomap(domain)) {
if (hwirq < domain->hwirq_max) {
/* 直接通过hwirq获取irq_data. */
data = irq_domain_get_irq_data(domain, hwirq);
if (data && data->hwirq == hwirq)
desc = irq_data_to_desc(data);
if (irq && desc)
*irq = hwirq; // virq就等于hwirq
}

return desc;
}

/*
* 情况2: 线性映射或动态映射域 (这是最常见的情况).
* 查找操作必须在RCU(读-拷贝-更新)的读端临界区内进行, 以防止
* 在我们读取时, 有其他CPU正在修改映射表.
*/
rcu_read_lock();

/*
* 优化路径: 线性反向映射表 (Linear Revmap).
* 如果hwirq小于线性表的大小, 我们就乐观地假设可以在这个数组中直接找到映射.
* 这是最高效的查找方式. 在__irq_domain_create时, 如果size>0, 就会分配这个表.
*/
if (hwirq < domain->revmap_size)
data = rcu_dereference(domain->revmap[hwirq]);
else /*
* 回退路径: 基数树 (Radix Tree).
* 对于稀疏的、非线性的hwirq, 映射关系存储在一个基数树中.
*/
data = radix_tree_lookup(&domain->revmap_tree, hwirq);

/*
* 如果找到了有效的irq_data (data不为NULL).
* likely()是一个编译器提示, 告诉编译器这个分支更有可能被执行.
*/
if (likely(data)) {
/* 从irq_data中获取irq_desc. */
desc = irq_data_to_desc(data);
/* 如果调用者需要, 通过指针返回Linux irq号. */
if (irq)
*irq = data->irq;
}

rcu_read_unlock(); // 退出RCU读端临界区
return desc; // 返回找到的描述符, 或NULL
}
EXPORT_SYMBOL_GPL(__irq_resolve_mapping);

上层便捷API

这组内联函数为内核其他部分提供了更友好、更特定于需求的接口, 它们在内部都调用了__irq_resolve_mapping

irq_resolve_mapping

此函数只关心获取核心的irq_desc结构体, 而不关心Linux IRQ号本身。

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* irq_resolve_mapping - 从一个hw irq号查找一个linux irq.
* @domain: 拥有此硬件中断的域
* @hwirq: 该域空间中的硬件irq号
*
* 返回: 中断描述符
*/
static inline struct irq_desc *irq_resolve_mapping(struct irq_domain *domain,
irq_hw_number_t hwirq)
{
/* 这是一个简单的封装, 它调用核心实现, 并为irq参数传递NULL. */
return __irq_resolve_mapping(domain, hwirq, NULL);
}

irq_find_mapping

此函数是irq_create_mapping的反操作, 它只关心获取Linux IRQ号, 如果找不到则返回0。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* irq_find_mapping() - 从一个hw irq号查找一个linux irq.
* @domain: 拥有此硬件中断的域
* @hwirq: 该域空间中的硬件irq号
*
* 返回: Linux irq号, 如果未找到则返回0
*/
static inline unsigned int irq_find_mapping(struct irq_domain *domain,
irq_hw_number_t hwirq)
{
unsigned int irq; // 用于接收返回的Linux IRQ号

/* 调用核心实现函数, 并传入irq变量的地址. */
if (__irq_resolve_mapping(domain, hwirq, &irq))
return irq; // 如果核心函数返回了一个有效的desc, 说明查找成功, 返回irq.

return 0; // 否则, 返回0表示未找到.
}

中断映射创建函数

此代码片段展示了Linux内核中断子系统中用于创建硬件中断号(hwirq)到Linux IRQ号(virq)映射的核心函数。这是irqchip子系统的基石, 也是gpio_to_irq()这类翻译功能最终依赖的底层机制。其核心原理是为一个特定的硬件中断号, 在全局的Linux IRQ编号空间中动态地分配一个唯一的、未使用的IRQ号, 然后在这两者之间建立一个持久的、双向的关联关系

这个过程必须是原子性的, 并且能够防止重复映射, 因此它严重依赖锁和内部数据结构来保证正确性。


irq_create_mapping_affinity_locked: 在锁定下执行映射创建的核心逻辑

这是一个内部函数, 负责执行实际的映射创建工作。它的命名后缀_locked明确告知调用者, 此函数必须在一个已经获取了中断域的root->mutex锁的上下文中被调用。

1
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
/* irq_create_mapping_affinity_locked: 在已持有锁的情况下, 为一个hwirq创建一个带亲和性的映射. */
static unsigned int irq_create_mapping_affinity_locked(struct irq_domain *domain,
irq_hw_number_t hwirq,
const struct irq_affinity_desc *affinity)
{
struct device_node *of_node = irq_domain_get_of_node(domain);
int virq; // 用于存储新分配的Linux IRQ号(virtual irq)

pr_debug("irq_create_mapping(0x%p, 0x%lx)\n", domain, hwirq);

/*
* 步骤1: 分配一个Linux IRQ号 (virq).
* irq_domain_alloc_descs会动态地寻找一个空闲的Linux IRQ号, 并为其分配一个
* irq_desc(中断描述符)结构体.
* -1 表示动态分配.
* affinity 参数(如果提供)可以建议内核将此IRQ分配给特定的CPU核心.
*/
virq = irq_domain_alloc_descs(-1, 1, hwirq, of_node_to_nid(of_node),
affinity);
if (virq <= 0) {
pr_debug("-> virq allocation failed\n");
return 0; // 分配失败, 返回0 (在旧API中0表示无效IRQ)
}

/*
* 步骤2: 在中断域中建立关联.
* irq_domain_associate_locked会执行以下操作:
* 1. 调用domain->ops->map()回调函数, 让驱动程序配置硬件.
* 2. 在domain的revmap(反向映射表)或radix树中记录: virq <-> hwirq.
* 这是一个双向绑定.
*/
if (irq_domain_associate_locked(domain, virq, hwirq)) {
/*
* 如果关联失败(例如,驱动的.map回调返回错误),
* 必须释放刚刚分配的irq_desc, 以避免资源泄漏.
*/
irq_free_desc(virq);
return 0;
}

pr_debug("irq %lu on domain %s mapped to virtual irq %u\n",
hwirq, of_node_full_name(of_node), virq);

/* 成功, 返回新创建的Linux IRQ号. */
return virq;
}

irq_create_mapping_affinity: 创建带CPU亲和性的中断映射 (公共API)

这是一个供内核其他部分(如irqchip驱动)使用的API函数, 它封装了锁的获取/释放以及对已存在映射的检查。

1
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
/**
* irq_create_mapping_affinity() - 将一个硬件中断映射到Linux irq空间
* @domain: 拥有此硬件中断的域, 或为NULL以使用默认域
* @hwirq: 该域空间中的硬件irq号
* @affinity: irq亲和性描述符
*/
unsigned int irq_create_mapping_affinity(struct irq_domain *domain,
irq_hw_number_t hwirq,
const struct irq_affinity_desc *affinity)
{
int virq;

/* 如果没有提供域, 尝试使用全局默认域. */
if (domain == NULL)
domain = irq_default_domain;
if (domain == NULL) {
WARN(1, "%s(, %lx) called with NULL domain\n", __func__, hwirq);
return 0;
}

/*
* 获取域的根锁. 对于层次化域, 所有子域共享根域的锁, 保证了整个层次结构操作的原子性.
* 在STM32H750这样的单核系统上, mutex_lock可以防止任务抢占导致的竞态条件.
*/
mutex_lock(&domain->root->mutex);

/*
* 关键检查: 在创建新映射之前, 必须先检查是否已存在映射.
* irq_find_mapping会查询域的反向映射表.
*/
virq = irq_find_mapping(domain, hwirq);
if (virq) {
pr_debug("existing mapping on virq %d\n", virq);
goto out; // 如果已存在, 直接跳转到解锁步骤并返回现有的virq.
}

/* 如果不存在映射, 调用带锁的核心函数来创建它. */
virq = irq_create_mapping_affinity_locked(domain, hwirq, affinity);
out:
/* 释放锁. */
mutex_unlock(&domain->root->mutex);

return virq;
}
EXPORT_SYMBOL_GPL(irq_create_mapping_affinity);

irq_create_mapping: 创建中断映射 (最常用的便捷API)

这是一个最常用的内联函数, 它为irq_create_mapping_affinity提供了一个更简单的接口, 用于不需要指定CPU亲和性的情况。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* irq_create_mapping - 将一个硬件中断映射到Linux irq空间
* @domain: 拥有此硬件中断的域, 或为NULL以使用默认域
* @hwirq: 该域空间中的硬件irq号
*
* 每个硬件中断只允许一个映射.
* 如果需要指定触发方式, 应对返回的irq号调用set_irq_type().
* 返回: Linux irq号, 或在错误时返回0
*/
static inline unsigned int irq_create_mapping(struct irq_domain *domain, irq_hw_number_t hwirq)
{
/*
* 这是一个简单的封装, 它直接调用irq_create_mapping_affinity,
* 并为affinity参数传递NULL.
*/
return irq_create_mapping_affinity(domain, hwirq, NULL);
}

kernel/irq/manage.c 中断请求管理(Interrupt Request Management) 内核中断线路的生命周期控制

历史与背景

这项技术是为了解决什么特定问题而诞生的?

kernel/irq/manage.c 内的代码是为了解决操作系统中一个基础且关键的硬件资源管理问题:如何对数量有限的硬件中断线(IRQ Lines)进行仲裁、抽象和生命周期管理

在没有一个集中化管理机制的情况下,会面临诸多问题:

  • 资源冲突:两个不同的设备驱动程序可能尝试使用同一个硬件中断号,导致中断信号无法被正确地分发,引发不可预测的系统行为。
  • 缺乏抽象:驱动程序需要编写与具体中断控制器(如x86的APIC,ARM的GIC)高度耦合的代码来使能(enable)、禁用(disable)、屏蔽(mask)中断。这使得驱动代码变得不可移植。
  • 动态性差:在不支持可加载模块和热插拔设备的早期系统中,中断的分配是静态的。但在现代系统中,当中断的属主(驱动模块)被加载和卸载时,系统必须有能力动态地分配和回收中断资源。

manage.c提供了一个中央管理器,它强制所有驱动程序必须通过一套标准的API(request_irq, free_irq, enable_irq, disable_irq等)来与中断子系统交互,从而解决了上述所有问题。

它的发展经历了哪些重要的里程碑或版本迭代?

Linux中断管理子系统的演进是其走向平台无关和高性能的关键一步。

  • irq_desc 结构体的引入:这是一个决定性的里程碑。内核不再将中断看作一个简单的数字,而是为每一个中断线(IRQ number)创建了一个struct irq_desc(中断描述符)。这个结构体成为描述一个中断所有属性(状态、锁、中断控制器、处理函数链表等)的中央对象。
  • 通用IRQ子系统 (generic_irq):为了将驱动与具体的中断控制器硬件解耦,内核开发了通用IRQ子系统。manage.c是其上层,而下层则通过struct irq_chip回调函数集来抽象中断控制器的具体硬件操作(如mask, unmask, ack, eoi)。这使得驱动代码可以完全平台无关。
  • 中断域(IRQ Domains):在复杂的SoC中,可能有多个级联的中断控制器,硬件中断号可能在局部是唯一的,但在全局不是。IRQ Domain提供了一个强大的映射机制,能将任意硬件中断号映射到一个全局唯一的、线性的Linux IRQ号空间中。
  • 线程化中断 (Threaded IRQs):为了进一步降低硬中断处理程序(上半部)的延迟,manage.c中加入了request_threaded_irq()接口。它允许将中断处理分为一个极快的上半部和一个运行在专用内核线程中的下半部,使得耗时的工作可以在一个可睡眠的、可抢占的上下文中完成,极大地提升了系统的实时响应能力。

目前该技术的社区活跃度和主流应用情况如何?

kernel/irq/manage.c是内核中最核心、最稳定的组件之一。它的代码不会频繁地进行大规模重构,但会持续进行性能优化(如减少irq_desc的锁竞争)和功能增强以支持新的硬件特性(如MSI中断的虚拟化)。
它是每一个需要处理硬件中断的设备驱动程序都必须使用的基础框架,是整个Linux驱动生态的基石。

核心原理与设计

它的核心工作原理是什么?

manage.c的核心是围绕**struct irq_desc(中断描述符)**进行的一系列生命周期管理操作。

  1. 中断描述符 (irq_desc):内核中有一个全局的irq_desc数组或基数树,为系统中每一个可能的Linux IRQ号都准备了一个描述符。这个描述符是管理该中断线路的“控制中心”。

  2. 申请中断 (request_irq / request_threaded_irq)

    • 一个设备驱动调用此API来声明它要使用某个IRQ号。
    • manage.c中的代码会找到对应的irq_desc
    • 它会分配一个struct irq_action结构体,用于保存驱动提供的中断处理函数(handler)、中断标志、驱动名和私有数据。
    • 这个irq_action会被添加到irq_desc的一个链表中。一个链表可以有多个irq_action,这正是实现**共享中断(Shared Interrupts)**的基础。
    • 最后,它会调用底层irq_chip的回调函数(如irq_startupirq_enable)来在硬件层面真正地使能这个中断。
  3. 禁用/使能中断 (disable_irq / enable_irq)

    • 这些函数通常用于驱动需要在一段代码中临时屏蔽中断的场景。
    • 它们会操作irq_desc中的一个深度计数器(depth),以支持嵌套调用。
    • 当计数器从0变为1时,disable_irq会调用底层irq_chip->irq_mask来屏蔽硬件中断。当计数器从1变回0时,enable_irq会调用irq_chip->irq_unmask来取消屏蔽。
  4. 释放中断 (free_irq)

    • 当驱动模块被卸载时,必须调用此函数。
    • 它会在irq_desc的链表中找到并移除属于该驱动的irq_action
    • 如果这是该中断线上的最后一个irq_actionmanage.c会调用irq_chip->irq_shutdown来彻底禁用该硬件中断,并清理相关状态。

它的主要优势体现在哪些方面?

  • 安全性与稳定性:通过集中的仲裁机制,从根本上防止了驱动间的IRQ资源冲突。
  • 抽象与可移植性:驱动程序面向标准的API编程,无需关心底层中断控制器的型号和实现细节。
  • 灵活性与功能性:原生支持中断共享、线程化中断、动态开关等高级功能,简化了复杂驱动的编写。

它存在哪些已知的劣势、局限性或在特定场景下的不适用性?

  • 合作式模型manage.c的正确运行依赖于所有驱动都“遵守规则”。一个有缺陷的驱动(例如,在中断处理函数中长时间占用CPU,或忘记调用free_irq)仍然会影响整个系统的稳定性。
  • 抽象的开销:通用IRQ层的存在,相较于一个为特定硬件写死的、高度优化的中断处理路径,会带来微小的性能开销。但在现代复杂的系统中,这种为可移植性和稳定性付出的代价是完全值得的。

使用场景

在哪些具体的业务或技术场景下,它是首选解决方案?

它是Linux内核中管理和注册硬件中断处理函数的唯一且标准的解决方案。

  • 任何物理设备驱动
    • 网卡驱动:在其.probe函数中,会从PCI配置空间读取分配到的IRQ号,然后调用request_threaded_irq()来注册一个能快速处理硬件状态的上半部和一个能处理网络包的下半部线程。
    • 键盘/鼠标驱动:会申请相应的IRQ,中断处理函数被触发时,它会读取按键或位移数据,并将其上报给输入子系统。
    • 磁盘控制器驱动:当一次DMA传输完成后,磁盘控制器会产生中断,驱动的中断处理函数会被调用,以通知上层I/O操作已完成。
  • GPIO中断:当一个GPIO引脚被配置为中断模式时,GPIO驱动框架内部会使用request_irq来将这个GPIO中断与一个处理函数关联起来。

是否有不推荐使用该技术的场景?为什么?

该技术高度特化,只用于物理硬件中断

  • 不用于软件触发的事件:对于由软件内部逻辑触发的、需要延迟执行的任务,应该使用softirq, taskletworkqueue,而不是request_irq。这些是纯软件的异步执行机制。
  • 轮询式驱动:对于一些没有中断能力的简单硬件,或者在某些特殊场景下(如启动早期),驱动只能通过循环**轮询(Polling)**设备的状态寄存器来判断事件是否发生。这种方式完全绕过了中断管理子系统。

对比分析

请将其 与 其他内核异步事件处理机制进行详细对比。

manage.c所管理的硬中断与内核的软中断进行对比,可以清晰地看出它们在内核事件处理层次结构中的不同角色。

| 特性 | IRQ管理 (Hard IRQ / Top-Half) | 软中断 (Softirq / Bottom-Half) |
| :— | :— | :— | :— |
| 事件来源 | 外部硬件。由物理设备产生的电信号触发。 | 内部软件。通常由硬中断处理程序在内部通过raise_softirq()调用来触发。 |
| 执行上下文 | 硬中断上下文。这是一个非常受限的环境,会屏蔽当前CPU上的至少同级中断。 | 软中断上下文。在这个环境中,硬件中断是打开的,但它仍然是原子上下文(不可睡眠)。 |
| 响应优先级 | 最高。CPU必须立即响应硬件中断。 | 。但低于硬中断,在硬中断返回后等时机执行。 |
| 注册/管理API | 动态。通过request_irq()在运行时注册和注销。由manage.c管理。 | 静态。软中断的类型在编译时就已固定,通过一个静态数组注册处理函数。 |
| 并发模型 | 可通过IRQF_SHARED标志允许多个处理函数共享同一中断线。 | 可并发。同一种类型的软中断(如NET_RX_SOFTIRQ)可以在多个CPU上同时并行执行。 |
| 核心目的 | 对硬件进行最快速的响应和交互(如ACK中断、从FIFO读数据),并将耗时工作推迟。 | 执行由硬中断推迟的耗时工作。它是硬中断的“下半部”,是处理任务的主体。 |
| 关系 | 生产者-消费者关系。硬中断处理程序是软中断任务的生产者。 | 软中断处理程序是硬中断任务的消费者。 |

irq_setup_forced_threading: 强制将中断处理线程化

此函数是Linux内核实时补丁集(PREEMPT_RT)中的一项关键机制, 其核心作用是根据一个全局的内核配置(通常是threadirqs内核启动参数), 强行将一个传统的、在硬中断上下文(hardirq context)中运行的中断处理程序(handler), 转换为在专门的内核线程(kthread)中运行

这个机制的根本原理和目标是为了提高系统的实时性(real-time performance)。在标准的Linux内核中, 中断处理程序在运行时会禁用中断, 这会增加系统中其他中断的延迟。对于一个需要确定性、低延迟响应的实时系统, 任何长时间运行在硬中断上下文的代码都是不可接受的。irq_setup_forced_threading就是一把”大锤”, 它将那些可能编写得不够”实时友好”的驱动程序中断处理, 强制迁移到可抢占、可调度的内核线程中, 从而将禁用中断的时间缩减到极致。

此函数通过修改irqaction结构体来巧妙地实现这一转换:

  1. 检查与豁免: 函数首先会进行一系列检查, 以确定是否应该进行强制转换:

    • !force_irqthreads(): 检查全局的threadirqs开关是否打开。如果没打开, 函数直接返回, 不做任何事。
    • IRQF_NO_THREAD, IRQF_PERCPU: 驱动程序可以通过这些标志明确表示其中断处理不应被线程化(例如, 高频率的定时器中断或每CPU中断), 函数会尊重这些请求。
    • handler == irq_default_primary_handler: 如果驱动程序已经将handler设置为默认的占位符, 说明它本来就是一个纯线程化的中断, 无需强制转换。
  2. 安全保障 (IRQF_ONESHOT): 这是最关键的安全步骤。函数会给irqaction强制添加IRQF_ONESHOT标志。这个标志告诉内核中断核心: 在硬中断处理程序返回后, 必须保持硬件中断线被屏蔽(masked), 直到对应的中断线程执行完毕才能重新使能。这可以完美地防止中断风暴, 尤其是对于电平触发的中断, 如果不这样做, 在线程被调度运行之前, 中断会立即再次触发。

  3. 处理程序“搬家”:

    • 简单情况 (只有handler): 这是最常见的转换。
      • new->thread_fn = new->handler;: 将驱动程序原本的硬中断处理函数(handler)的指针, “搬到”线程处理函数(thread_fn)的槽位里。
      • new->handler = irq_default_primary_handler;: 将原来的硬中断处理函数槽位用一个内核预设的、极简的irq_default_primary_handler来填充。这个默认处理程序的唯一工作就是返回IRQ_WAKE_THREAD, 以唤醒中断线程。
    • 复杂情况 (同时有handlerthread_fn): 如果驱动本身就是一个”分离式”处理模型。函数会更进一步:
      • 它会分配一个次级的(secondary) irqaction结构。
      • 将驱动原本的thread_fn“搬到”这个次级irqactionthread_fn槽位里。
      • 然后, 按照简单情况的逻辑, 将驱动原本的handler“搬到”主irqactionthread_fn槽位里。
      • 最终, 原始的一个中断请求被转换成了两个串联的纯线程化中断请求。
1
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
static int irq_setup_forced_threading(struct irqaction *new)
{
// 检查全局的 "threadirqs" 内核参数是否设置. 如果没有, 则不进行任何强制操作.
if (!force_irqthreads())
return 0;
// 如果驱动明确请求了 IRQF_NO_THREAD(不线程化), IRQF_PERCPU(每CPU中断),
// 或已经是 IRQF_ONESHOT, 则尊重驱动的设置, 不进行强制转换.
if (new->flags & (IRQF_NO_THREAD | IRQF_PERCPU | IRQF_ONESHOT))
return 0;

/*
* 如果 handler 已经是 irq_default_primary_handler,
* 说明这个中断请求原本就是一个纯线程化的中断, 无需再强制.
*/
if (new->handler == irq_default_primary_handler)
return 0;

// *** 关键安全措施 ***: 强制设置 IRQF_ONESHOT 标志.
// 这确保了在线程处理函数完成前, 该中断线在硬件层面保持被屏蔽,
// 以防止中断风暴, 特别是对于电平触发的中断.
new->flags |= IRQF_ONESHOT;

/*
* 处理复杂情况: 驱动已经提供了一个主处理函数(handler)和一个线程处理函数(thread_fn).
* 我们通过创建一个次级 action 来强制将两者都线程化.
*/
if (new->handler && new->thread_fn) {
/* 分配一个次级的 irqaction 结构体 */
new->secondary = kzalloc(sizeof(struct irqaction), GFP_KERNEL);
if (!new->secondary)
return -ENOMEM;
// 将次级 action 的主处理函数设置为一个占位符
new->secondary->handler = irq_forced_secondary_handler;
// 将驱动原本的 thread_fn "搬到" 次级 action 的 thread_fn
new->secondary->thread_fn = new->thread_fn;
// 复制其他必要信息
new->secondary->dev_id = new->dev_id;
new->secondary->irq = new->irq;
new->secondary->name = new->name;
}
// --- 执行核心的“搬家”操作 ---
// 在 action 的线程标志中设置一个位, 标记它是一个被强制线程化的中断.
set_bit(IRQTF_FORCED_THREAD, &new->thread_flags);
// 将驱动原本的主处理函数 handler "搬到" 线程处理函数 thread_fn 的位置.
new->thread_fn = new->handler;
// 将主处理函数 handler 替换为内核的默认占位符.
// 这个默认函数唯一的职责就是返回 IRQ_WAKE_THREAD 来唤醒线程.
new->handler = irq_default_primary_handler;
return 0;
}

__irq_set_trigger: 设置中断线的硬件触发模式

这是Linux内核中断子系统中一个至关重要的底层函数, 负责将一个抽象的中断触发类型请求(如边沿触发、电平触发)转换为对具体中断控制器(irq_chip)硬件寄存器的编程操作。当request_irq被调用时, 或者当驱动程序需要动态改变中断触发方式时, 最终都会通过这个函数来与硬件交互。

此函数的核心原理是充当通用中断核心与**特定硬件驱动(irq_chip)**之间的桥梁, 并实施一套关键的安全规程:

  1. 委托给irq_chip: 函数首先会检查与该中断线关联的irq_chip驱动是否实现了irq_set_type这个回调函数。如果没有实现, 意味着该硬件不支持动态配置触发模式, 函数会直接返回。如果实现了, 实际的硬件编程工作将完全委托给这个函数来完成。

  2. “先屏蔽, 再配置, 后恢复”的安全序列: 这是此函数最重要的安全机制。某些中断控制器硬件(如STM32上的EXTI)要求在修改其触发模式配置时, 对应的中断线必须先被屏蔽(masked/disabled)。如果在中断使能的状态下修改配置, 可能会导致不确定的行为或产生伪中断(spurious IRQ)。

    • 函数会检查irq_chip的标志位, 看它是否声明了IRQCHIP_SET_TYPE_MASKED
    • 如果声明了, 函数会在调用irq_set_type之前, 先调用mask_irq在硬件上屏蔽该中断。
    • irq_set_type调用完成之后, 如果之前屏蔽了中断, 它会再调用unmask_irq来恢复中断线之前的使能状态。
    • 这个”屏蔽-配置-恢复”的序列确保了硬件配置更改的原子性和安全性。
  3. 内核状态同步: 在成功配置硬件后, 函数会更新内核内部维护的、与该中断线相关的多个状态描述符(irq_datairq_settings), 确保内核的软件状态与硬件的实际状态保持严格一致。特别是, 它会明确地设置或清除IRQD_LEVEL标志, 这个标志对于内核后续应该使用哪种中断流处理程序(flow handler)至关重要。

在STM32H750上, 这个函数是配置GPIO外部中断(EXTI)触发方式(上升沿、下降沿、双边沿)的核心。当驱动请求一个GPIO中断时, __irq_set_trigger会被调用, 它的irq_chip就是STM32的EXTI控制器驱动, chip->irq_set_type最终会去修改EXTI控制器的RTSR(上升沿触发选择寄存器)和FTSR(下降沿触发选择寄存器)等硬件寄存器。

1
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
67
68
69
70
71
72
73
74
75
76
77
78
79
80
/*
* __irq_set_trigger - 设置一个中断描述符(desc)的触发模式
* @desc: 要配置的中断线的描述符
* @flags: 包含了所需触发模式的标志位 (例如 IRQ_TYPE_EDGE_RISING)
*/
int __irq_set_trigger(struct irq_desc *desc, unsigned long flags)
{
// 获取与此中断线关联的 irq_chip (中断控制器驱动) 的操作函数表
struct irq_chip *chip = desc->irq_data.chip;
int ret, unmask = 0; // ret 用于保存返回值, unmask 用于标记是否需要在最后解除屏蔽

// 检查 irq_chip 是否存在, 以及它是否实现了 irq_set_type 回调函数
if (!chip || !chip->irq_set_type) {
/*
* 请求了 IRQF_TRIGGER_* 但中断控制器不支持多种触发类型?
*/
// 打印一条调试信息, 然后直接返回成功. 意味着无法配置, 但不认为是一个错误.
pr_debug("No set_type function for IRQ %d (%s)\n",
irq_desc_get_irq(desc),
chip ? (chip->name ? : "unknown") : "unknown");
return 0;
}

// *** 关键的安全检查 ***
// 检查 irq_chip 是否要求在设置类型前必须先屏蔽中断
if (chip->flags & IRQCHIP_SET_TYPE_MASKED) {
// 如果中断当前未被屏蔽, 则调用 mask_irq() 在硬件上屏蔽它
if (!irqd_irq_masked(&desc->irq_data))
mask_irq(desc);
// 如果中断在调用此函数前不是被完全禁用的状态, 则设置 unmask 标志,
// 以便在函数结束时恢复其中断使能状态.
if (!irqd_irq_disabled(&desc->irq_data))
unmask = 1;
}

// 清理 flags, 只保留与触发模式相关的位 (例如: 上升沿/下降沿/高电平/低电平)
flags &= IRQ_TYPE_SENSE_MASK;
// *** 核心调用 ***: 调用特定硬件驱动的回调函数来实际配置硬件寄存器
ret = chip->irq_set_type(&desc->irq_data, flags);

// 根据 irq_chip 回调函数的返回值进行处理
switch (ret) {
case IRQ_SET_MASK_OK:
case IRQ_SET_MASK_OK_DONE:
// 硬件配置成功. 现在更新内核的软件状态以保持同步.
// 清除 irq_data 中旧的触发模式标志
irqd_clear(&desc->irq_data, IRQD_TRIGGER_MASK);
// 设置 irq_data 中新的触发模式标志
irqd_set(&desc->irq_data, flags);
fallthrough; // 顺势执行下一个 case 的代码

case IRQ_SET_MASK_OK_NOCOPY:
// 同样是成功的情况.
// 从 irq_data 中重新获取最终确定的触发类型 (可能与请求的略有不同)
flags = irqd_get_trigger_type(&desc->irq_data);
// 在中断设置(settings)中也更新触发类型掩码
irq_settings_set_trigger_mask(desc, flags);
// 清除旧的电平触发状态
irqd_clear(&desc->irq_data, IRQD_LEVEL);
irq_settings_clr_level(desc);
// 如果新的触发模式是电平触发
if (flags & IRQ_TYPE_LEVEL_MASK) {
// 则设置相应的电平标志. 这个标志对中断流控至关重要.
irq_settings_set_level(desc);
irqd_set(&desc->irq_data, IRQD_LEVEL);
}

ret = 0; // 将返回值标准化为 0 (成功)
break;
default:
// irq_set_type 返回了一个错误码.
pr_err("Setting trigger mode %lu for irq %u failed (%pS)\n",
flags, irq_desc_get_irq(desc), chip->irq_set_type);
}
// 如果在函数开始时屏蔽了中断, 现在就解除屏蔽
if (unmask)
unmask_irq(desc);

return ret; // 返回最终的操作结果
}

__setup_irq: 中断处理程序安装的核心引擎

这是Linux内核中断子系统中的一个底层核心函数, 是request_threaded_irq等上层API的最终执行者。它的核心原理是以一种高度同步化和原子化的方式, 将一个代表中断处理请求的irqaction结构体, 安全地安装到指定中断线(irq_desc)的动作链表上, 并根据请求的标志和硬件的能力, 对中断控制器(irqchip)进行必要的配置

1
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
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
static int
setup_irq_thread(struct irqaction *new, unsigned int irq, bool secondary)
{
struct task_struct *t;

if (!secondary) {
t = kthread_create(irq_thread, new, "irq/%d-%s", irq,
new->name);
} else {
t = kthread_create(irq_thread, new, "irq/%d-s-%s", irq,
new->name);
}

if (IS_ERR(t))
return PTR_ERR(t);

/*
* We keep the reference to the task struct even if
* the thread dies to avoid that the interrupt code
* references an already freed task_struct.
*/
new->thread = get_task_struct(t);
/*
* Tell the thread to set its affinity. This is
* important for shared interrupt handlers as we do
* not invoke setup_affinity() for the secondary
* handlers as everything is already set up. Even for
* interrupts marked with IRQF_NO_BALANCE this is
* correct as we want the thread to move to the cpu(s)
* on which the requesting code placed the interrupt.
*/
set_bit(IRQTF_AFFINITY, &new->thread_flags);
return 0;
}

/*
* Primary handler for nested threaded interrupts. Should never be
* called.
*/
static irqreturn_t irq_nested_primary_handler(int irq, void *dev_id)
{
WARN(1, "Primary handler called for nested irq %d\n", irq);
return IRQ_NONE;
}

static int irq_request_resources(struct irq_desc *desc)
{
struct irq_data *d = &desc->irq_data;
struct irq_chip *c = d->chip;

return c->irq_request_resources ? c->irq_request_resources(d) : 0;
}

/* __setup_irq: 安装一个 irqaction 的内部核心函数 */
static int
__setup_irq(unsigned int irq, struct irq_desc *desc, struct irqaction *new)
{
struct irqaction *old, **old_ptr;
unsigned long flags, thread_mask = 0;
int ret, nested, shared = 0;

if (!desc)
return -EINVAL;

// 如果中断线没有关联到有效的 irq_chip, 则无法处理
if (desc->irq_data.chip == &no_irq_chip)
return -ENOSYS;
// 尝试增加 irqchip 驱动模块的引用计数, 防止其在使用中被卸载
if (!try_module_get(desc->owner))
return -ENODEV;

new->irq = irq;

// 如果调用者未指定触发类型, 则使用该中断的默认类型
if (!(new->flags & IRQF_TRIGGER_MASK))
new->flags |= irqd_get_trigger_type(&desc->irq_data);

// 检查是否为嵌套中断, 并进行相应处理
nested = irq_settings_is_nested_thread(desc);
if (nested) {
if (!new->thread_fn) { // 嵌套中断必须有线程处理函数
ret = -EINVAL;
goto out_mput;
}
// 将主处理函数替换为警告函数, 因为嵌套中断不应执行主处理
new->handler = irq_nested_primary_handler;
} else {
// 检查是否可以强制线程化, 以提高实时性
if (irq_settings_can_thread(desc)) {
ret = irq_setup_forced_threading(new);
if (ret)
goto out_mput;
}
}

// 如果提供了线程处理函数且不是嵌套中断, 则创建内核线程
if (new->thread_fn && !nested) {
ret = setup_irq_thread(new, irq, false);
if (ret)
goto out_mput;
if (new->secondary) {
ret = setup_irq_thread(new->secondary, irq, true);
if (ret)
goto out_thread;
}
}

// 如果 irqchip 硬件本身是 oneshot 安全的, 则移除软件 ONESHOT 标志以优化
if (desc->irq_data.chip->flags & IRQCHIP_ONESHOT_SAFE)
new->flags &= ~IRQF_ONESHOT;

// 1. 获取最外层锁: request_mutex, 串行化 request_irq/free_irq
mutex_lock(&desc->request_mutex);

// 2. 获取总线锁, 用于保护慢速总线上的 irqchip 操作
chip_bus_lock(desc);

// 如果这是该中断线上的第一个 action, 需要请求硬件资源
if (!desc->action) {
ret = irq_request_resources(desc);
if (ret) {
pr_err("Failed to request resources for %s (irq %d) on irqchip %s\n",
new->name, irq, desc->irq_data.chip->name);
goto out_bus_unlock;
}
}

// 3. 获取最内层锁: desc->lock, 禁用本地中断, 保护 action 链表
raw_spin_lock_irqsave(&desc->lock, flags);
old_ptr = &desc->action;
old = *old_ptr;
if (old) { // --- 处理共享中断 ---
unsigned int oldtype;

// NMI (不可屏蔽中断) 不能被共享
if (irq_is_nmi(desc)) {
ret = -EINVAL;
goto out_unlock;
}

// 如果之前未设置触发类型, 则继承第一个请求者的类型
if (irqd_trigger_type_was_set(&desc->irq_data)) {
oldtype = irqd_get_trigger_type(&desc->irq_data);
} else {
oldtype = new->flags & IRQF_TRIGGER_MASK;
irqd_set_trigger_type(&desc->irq_data, oldtype);
}

// 检查共享兼容性: 必须都声明共享, 且触发类型一致
if (!((old->flags & new->flags) & IRQF_SHARED) ||
(oldtype != (new->flags & IRQF_TRIGGER_MASK)))
goto mismatch;

// 检查 ONESHOT 标志兼容性
if ((old->flags & IRQF_ONESHOT) &&
(new->flags & IRQF_COND_ONESHOT))
new->flags |= IRQF_ONESHOT;
else if ((old->flags ^ new->flags) & IRQF_ONESHOT)
goto mismatch;

// 检查 PERCPU 标志兼容性
if ((old->flags & IRQF_PERCPU) !=
(new->flags & IRQF_PERCPU))
goto mismatch;

// 遍历到链表末尾, 并计算所有已存在 action 的 thread_mask
do {
thread_mask |= old->thread_mask;
old_ptr = &old->next;
old = *old_ptr;
} while (old);
shared = 1;
}

// 为 ONESHOT 中断设置 thread_mask
if (new->flags & IRQF_ONESHOT) {
if (thread_mask == ~0UL) { // 位掩码已用完
ret = -EBUSY;
goto out_unlock;
}
// 分配第一个可用的比特位给新的 action
new->thread_mask = 1UL << ffz(thread_mask);

} else if (new->handler == irq_default_primary_handler &&
!(desc->irq_data.chip->flags & IRQCHIP_ONESHOT_SAFE)) {
// 危险组合: handler=NULL, 且 !ONESHOT. 对于电平中断可能导致中断风暴. 拒绝.
ret = -EINVAL;
goto out_unlock;
}

if (!shared) { // --- 处理第一个(非共享)中断 ---
// 设置硬件触发类型
if (new->flags & IRQF_TRIGGER_MASK) {
ret = __irq_set_trigger(desc,
new->flags & IRQF_TRIGGER_MASK);

if (ret)
goto out_unlock;
}

// 激活中断(关联资源)
ret = irq_activate(desc);
if (ret)
goto out_unlock;

// 清理/设置各种状态位
desc->istate &= ~(IRQS_AUTODETECT | IRQS_SPURIOUS_DISABLED | \
IRQS_ONESHOT | IRQS_WAITING);
irqd_clear(&desc->irq_data, IRQD_IRQ_INPROGRESS);

if (new->flags & IRQF_ONESHOT)
desc->istate |= IRQS_ONESHOT;

// 如果不要求手动使能(NO_AUTOEN), 则启动并使能中断
if (!(new->flags & IRQF_NO_AUTOEN) &&
irq_settings_can_autoenable(desc)) {
irq_startup(desc, IRQ_RESEND, IRQ_START_COND);
} else {
desc->depth = 1; // 否则, 标记为已禁用
}

} else if (new->flags & IRQF_TRIGGER_MASK) {
// 对于共享中断, 检查新请求的触发类型是否与已配置的匹配
//... (代码省略)
}

// *** 关键步骤: 将新的 action 链接到链表中 ***
*old_ptr = new;

// ... (清理和状态更新)

// 释放最内层锁
raw_spin_unlock_irqrestore(&desc->lock, flags);
// 释放总线锁
chip_bus_sync_unlock(desc);
// 释放最外层锁
mutex_unlock(&desc->request_mutex);

// ... (注册 procfs 条目等收尾工作)

return 0;

mismatch: // 标志不匹配的错误处理
ret = -EBUSY;

out_unlock: // 清理路径1: 释放内层锁
raw_spin_unlock_irqrestore(&desc->lock, flags);
if (!desc->action)
irq_release_resources(desc);
out_bus_unlock: // 清理路径2: 释放总线锁
chip_bus_sync_unlock(desc);
mutex_unlock(&desc->request_mutex);

out_thread: // 清理路径3: 停止已创建的线程
//... (代码省略)
out_mput: // 清理路径4: 释放模块引用
module_put(desc->owner);
return ret;
}

request_threaded_irq: 注册中断处理程序 (核心函数)

这是Linux内核中用于为一个驱动程序申请和注册中断处理程序的根本性函数。当一个硬件设备(如STM32上的DMA、UART或GPIO)需要通过中断信号通知CPU有事件发生时, 其驱动程序必须调用此函数来建立硬件中断信号与特定软件处理函数之间的连接。

此函数的核心原理是实现了现代Linux内核的中断处理”两阶段”或”分离式处理”模型, 将中断处理分为两个部分:

  1. 硬中断处理程序 (handler): 这是”上半部”(Top Half)。当硬件中断发生时, CPU会立即跳转到这里执行。此函数的运行环境非常受限:

    • 它在原子上下文中运行, 意味着它不能睡眠(不能调用任何可能导致进程调度的函数, 如kmallocmutex_lock)。
    • 在单核系统(如STM32H750)上, 它运行时本地中断是关闭的, 以防止被其他中断嵌套。
    • 它的职责必须是最小化和快速的: 检查中断是否真的由其设备产生(在共享中断的情况下), 读取/清除中断状态寄存器, 禁用设备的中断源以防中断风暴, 如果有”下半部”, 则唤醒它。
    • 如果它返回IRQ_WAKE_THREAD, 内核就会唤醒对应的”下半部”线程。
  2. 线程化中断处理程序 (thread_fn): 这是”下半部”(Bottom Half)。它在一个普通的内核线程上下文中运行。这意味着:

    • 它可以被抢占, 也可以睡眠。
    • 它可以调用所有标准的内核服务, 如内存分配、加锁、与用户空间交互等。
    • 它负责执行所有耗时较长的中断处理工作, 如数据拷贝、协议处理、唤醒等待队列等。

此函数的工作流程是:

  1. 参数验证: 首先进行一系列严格的健全性检查。最重要的是, 如果中断被声明为共享的 (IRQF_SHARED), 那么dev_id参数必须是一个唯一的非NULL值。这是因为当多个设备共享同一条IRQ线时, 内核需要dev_id来区分应该释放哪一个具体的中断处理程序。
  2. 分配irqaction: 它分配一个irqaction结构体。这个结构体就像一个”中断注册表单”, 包含了驱动程序提供的所有信息: 两个处理函数指针、标志位、名称和dev_id
  3. 安装irqaction: 它调用内部函数__setup_irq, 将这个irqaction结构体添加到内核为该IRQ号维护的irq_desc(中断描述符)的动作链表中。对于非共享中断, 这个链表只有一个节点; 对于共享中断, 则有多个。
  4. 使能中断: __setup_irq最终会通过irqchip(中断控制器驱动)回调, 在硬件层面(如NVIC)解除对该中断的屏蔽(unmask), 使其能够被CPU响应。
1
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
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
/*
* 线程中断的默认主中断处理程序。是
* 在调用 request_threaded_irq 时被分配为主处理程序
* 与处理程序 == NULL 一起使用。对于一次性中断很有用。
*/
static irqreturn_t irq_default_primary_handler(int irq, void *dev_id)
{
return IRQ_WAKE_THREAD;
}

/**
* request_threaded_irq - 分配一条中断线
* @irq: 要分配的中断线编号
* @handler: 当IRQ发生时调用的函数。对于线程化中断, 这是主处理程序。
* 如果 handler 为 NULL 且 thread_fn 不为 NULL, 则安装默认的主处理程序。
* @thread_fn: 从irq处理线程中调用的函数。如果为NULL, 则不创建irq线程。
* @irqflags: 中断类型标志
* @devname: 声明此中断的设备的ascii名称
* @dev_id: 一个传递回处理函数的 cookie
* ... (其余注释为详细说明)
*/
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; // 返回值

// 检查是否为无效的IRQ号
if (irq == IRQ_NOTCONNECTED)
return -ENOTCONN;

/*
* 健全性检查:
* 1. 共享中断(IRQF_SHARED)必须提供一个唯一的 dev_id, 否则以后无法正确释放.
* 2. 共享中断不能和 IRQF_NO_AUTOEN (不自动使能) 一起使用.
* 3. IRQF_COND_SUSPEND (条件性挂起) 只对共享中断有意义.
* 4. IRQF_NO_SUSPEND (不挂起) 和 IRQF_COND_SUSPEND 互斥.
*/
if (((irqflags & IRQF_SHARED) && !dev_id) ||
((irqflags & IRQF_SHARED) && (irqflags & IRQF_NO_AUTOEN)) ||
(!(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;

// 如果调用者只提供了线程处理函数 thread_fn 而没有提供主处理函数 handler
if (!handler) {
if (!thread_fn)
return -EINVAL; // 两者都为NULL是无效的
// 安装一个默认的主处理函数. 这个函数通常只做最少的工作, 然后返回 IRQ_WAKE_THREAD
handler = irq_default_primary_handler;
}

// 为 irqaction 结构体分配内存
action = kzalloc(sizeof(struct irqaction), GFP_KERNEL);
if (!action)
return -ENOMEM;

// 填充 action 结构体, 将所有传入的参数保存起来
action->handler = handler;
action->thread_fn = thread_fn;
action->flags = irqflags;
action->name = devname;
action->dev_id = dev_id;

// 获取中断控制器的电源管理引用 (运行时PM相关)
retval = irq_chip_pm_get(&desc->irq_data);
if (retval < 0) {
kfree(action);
return retval;
}

// 调用 __setup_irq, 这是实际安装和使能中断的核心内部函数.
// 它会将 action 添加到 desc 的动作链表中, 并通过 irqchip 回调来操作硬件.
retval = __setup_irq(irq, desc, action);

// 如果 __setup_irq 失败
if (retval) {
// 释放之前获取的PM引用和分配的内存, 进行清理
irq_chip_pm_put(&desc->irq_data);
kfree(action->secondary);
kfree(action);
}

#ifdef CONFIG_DEBUG_SHIRQ_FIXME
// 这是一个调试特性: 如果是共享中断, 注册成功后立即触发一次伪中断.
// 目的是为了检查驱动的 handler 是否能正确处理并非由自己设备触发的中断.
if (!retval && (irqflags & IRQF_SHARED)) {
unsigned long flags;

disable_irq(irq); // 暂时禁用真正的硬件中断
local_irq_save(flags); // 保存当前中断状态

handler(irq, dev_id); // 直接调用 handler, 模拟一次中断

local_irq_restore(flags); // 恢复中断状态
enable_irq(irq); // 重新使能硬件中断
}
#endif
return retval; // 返回操作结果
}
// 将函数导出, 使其对其他内核模块可用
EXPORT_SYMBOL(request_threaded_irq);

kernel/irq/proc.c

init_irq_proc: 初始化/proc/irq中断信息接口

此函数在内核启动期间被调用,其核心作用是在/proc虚拟文件系统中创建用于监控和管理硬件中断的目录结构。它首先创建了一个顶层目录/proc/irq/,然后遍历系统中所有已注册的中断描述符(IRQ descriptor),为每一个中断号(IRQ number)创建一个对应的子目录(例如/proc/irq/16/),并在其中填充用于显示该中断状态的属性文件。

1
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
/*
* 函数 init_irq_proc
* 此函数没有参数和返回值.
* 它的作用是在 /proc 文件系统中创建用于展示中断信息的目录和文件.
*/
void init_irq_proc(void)
{
/*
* 定义一个无符号整型变量 irq, 用于在循环中存储中断号.
*/
unsigned int irq;
/*
* 定义一个指向 struct irq_desc 的指针 desc.
* irq_desc 结构体是内核中描述一个硬件中断的所有信息的载体.
*/
struct irq_desc *desc;

/*
* 调用 proc_mkdir 在 /proc 目录下创建一个名为 "irq" 的新目录.
* 将返回的 proc_dir_entry 指针保存在全局变量 root_irq_dir 中,
* 以便后续在此目录下创建子目录.
*/
root_irq_dir = proc_mkdir("irq", NULL);
/*
* 检查目录是否创建成功. 如果 proc_mkdir 因内存不足等原因失败, 它会返回NULL.
*/
if (!root_irq_dir)
/*
* 如果创建失败, 直接返回, 后续操作无法进行.
*/
return;

/*
* 调用 register_default_affinity_proc 函数.
* 该函数会在 /proc/irq/ 目录下创建一个名为 "default_smp_affinity" 的文件.
* 这个文件显示了新注册中断的默认CPU亲和性掩码.
* 在单核系统中, 其内容总是 "1", 代表CPU 0.
*/
register_default_affinity_proc();

/*
* 遍历系统中所有已经存在的中断描述符.
* for_each_irq_desc 是一个宏, 它会遍历内核的中断描述符表.
* 在每次循环中, irq 会被赋值为当前的中断号, desc 会指向对应的 irq_desc 结构体.
* 在STM32H750上, 这将遍历NVIC管理的所有中断.
*/
for_each_irq_desc(irq, desc)
/*
* 对每一个存在的中断, 调用 register_irq_proc 函数.
* 这个函数负责在 /proc/irq/ 目录下创建一个以中断号命名的子目录 (例如, /proc/irq/42),
* 并在该子目录中创建一系列文件 (如 smp_affinity, chip_name 等),
* 用于显示该特定中断的详细信息.
*/
register_irq_proc(irq, desc);
}

register_irq_proc: 为指定的中断号创建/proc接口

此函数的核心作用是为一个具体的中断(IRQ)在/proc/irq/目录下创建一个对应的子目录(例如/proc/irq/42/),并在这个子目录中填充一系列的虚拟文件,用于展示和(在多核系统中)控制该中断的属性。这个函数通常在中断处理程序首次被注册时调用,而不是在中断描述符被创建时调用。

1
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
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
/*
* 定义一个宏, 用于限制文件名缓冲区的最大长度.
* 一个无符号整数最大为 4294967295 (10位), 所以长度10足够.
*/
#define MAX_NAMELEN 10

/*
* 函数 register_irq_proc
* @irq: 要为其创建proc接口的中断号.
* @desc: 指向该中断对应的中断描述符(irq_desc)的指针.
*/
void register_irq_proc(unsigned int irq, struct irq_desc *desc)
{
/*
* 使用 DEFINE_MUTEX 宏定义一个静态的互斥锁.
* 'static' 关键字意味着这个锁只被创建一次, 并在所有对本函数的调用之间共享.
* 它用于防止多个任务并发地为同一个IRQ注册proc目录.
*/
static DEFINE_MUTEX(register_lock);
/*
* 将中断号 'irq' 强制类型转换为一个 void 指针.
* 这是向proc文件系统的回调函数传递简单整数数据的常用技巧.
* __maybe_unused 属性告诉编译器, 如果这个变量在某些编译配置下(如CONFIG_SMP禁用时)
* 没有被使用, 不要产生警告.
*/
void __maybe_unused *irqp = (void *)(unsigned long) irq;
/*
* 定义一个字符数组, 用作将中断号转换为字符串时的缓冲区.
*/
char name [MAX_NAMELEN];

/*
* 进行前置条件检查. 如果 /proc/irq 根目录(root_irq_dir)不存在,
* 或者中断描述符使用的是一个无效的/虚拟的irq_chip, 则直接返回, 不进行任何操作.
*/
if (!root_irq_dir || (desc->irq_data.chip == &no_irq_chip))
return;

/*
* 内核注释: irq目录只有在添加处理程序时才被注册, 而不是在创建描述符时,
* 因此多个任务可能同时尝试注册.
*
* guard(mutex)(&register_lock);
* 这是一个C语言中实现RAII(资源获取即初始化)模式的宏. 它在进入作用域时
* 自动获取 'register_lock' 锁, 并在函数退出(无论是正常返回还是通过goto)时
* 自动释放该锁. 这比手动调用 mutex_lock/unlock 更安全, 能有效防止忘记解锁.
*/
guard(mutex)(&register_lock);

/*
* 再次检查. 在获取锁之后, 检查该中断的proc目录是否已经被其他任务创建了.
* 如果 desc->dir 指针已经有效, 说明目录已存在, 直接返回即可.
* 这是防止竞争条件的关键步骤.
*/
if (desc->dir)
return;

/*
* 将中断号 'irq' 格式化为一个字符串.
*/
sprintf(name, "%u", irq);
/*
* 在 /proc/irq/ (由root_irq_dir代表) 目录下, 创建一个以中断号命名的新子目录.
* 将返回的 proc_dir_entry 指针保存在中断描述符的 'dir' 字段中,
* 标记该目录已创建, 并供将来移除时使用.
*/
desc->dir = proc_mkdir(name, root_irq_dir);
/*
* 如果目录创建失败 (例如, 内存不足), proc_mkdir 会返回 NULL.
*/
if (!desc->dir)
return;

/*
* 创建 /proc/irq/<irq>/spurious 文件.
* 这个文件不受 CONFIG_SMP 宏的影响, 因此在单核系统上也会被创建.
* 它用于显示该中断线上发生的伪中断(spurious interrupt)的统计信息.
* proc_create_single_data 用于创建只有一个回调函数(show)的只读proc文件.
*/
proc_create_single_data("spurious", 0444, desc->dir,
irq_spurious_proc_show, (void *)(long)irq);

}