[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);
}

irq_set_irq_wake: 中断唤醒功能的启用与禁用

本代码片段展示了Linux内核中用于控制单个中断请求(IRQ)作为系统唤醒源的核心机制。其主要功能由irq_set_irq_wake函数实现,该函数提供了一个统一的接口,允许设备驱动程序将其使用的中断配置为能够将系统从低功耗睡眠状态(如挂起到内存)中唤醒。为了支持共享中断,该机制内部实现了一个引用计数器。

实现原理分析

该机制是内核电源管理与中断子系统交互的关键部分,它通过分层设计将通用API与具体的硬件中断控制器驱动解耦。

  1. API与封装:

    • irq_set_irq_wake是核心的API函数,接受中断号和开关标志(on)作为参数。
    • enable_irq_wakedisable_irq_wake是两个便利的内联函数,它们分别通过向irq_set_irq_wake传递10来简化调用,提高了代码的可读性。
  2. 引用计数 (wake_depth):

    • 这是支持共享中断的关键。一个物理中断线可能被多个设备驱动共享。每个驱动都可以独立请求将该中断设置为唤醒源。
    • struct irq_desc 结构中的 wake_depth 成员作为一个引用计数器。
    • 当调用enable_irq_wake时,wake_depth会递增。只有在wake_depth0变为1时(即第一次请求唤醒),才会真正调用硬件相关的函数set_irq_wake_real来配置硬件。
    • 当调用disable_irq_wake时,wake_depth会递减。只有在wake_depth1变为0时(即最后一次释放唤醒请求),才会调用set_irq_wake_real来取消硬件的唤醒设置。
    • 这种设计确保了一个驱动禁用唤醒功能不会影响到同样共享该中断且仍需要唤醒功能的另一个驱动。
  3. 硬件抽象 (irq_chip->irq_set_wake):

    • 实际的硬件配置操作被抽象出来,由set_irq_wake_real函数执行。
    • 该函数通过irq_to_desc找到对应中断的描述符irq_desc,然后访问其中的中断控制器芯片描述irq_chip
    • 最终,它会调用irq_chip结构体中的irq_set_wake函数指针。这个函数指针指向的是具体中断控制器驱动(例如ARM GIC或STM32 EXTI的驱动)中实现的、用于配置硬件唤醒功能的函数。这种方式使得上层代码与具体硬件实现完全分离。
  4. 状态管理与同步:

    • scoped_irqdesc_get_and_buslock宏用于安全地获取中断描述符并锁定它。这个锁(通常是自旋锁)确保了对wake_depth计数器的读-改-写操作是原子的,防止了在多核系统或中断上下文中的竞争条件。
    • IRQD_WAKEUP_STATE标志位被用来在irq_data的状态中标记当前中断是否处于可唤醒状态,便于内核其他部分快速查询。

特定场景分析:单核、无MMU的STM32H750平台

硬件交互

  1. 目标硬件: 在STM32H750平台上,中断控制器由ARM Cortex-M7内核内置的NVIC(嵌套向量中断控制器)和STM32片上外设EXTI(外部中断/事件控制器)共同组成。能够将系统从STOP等低功耗模式唤醒的通常是EXTI。

  2. 函数指针的实现: 当代码调用desc->irq_data.chip->irq_set_wake时,它实际会调用到STM32 EXTI中断控制器驱动中的一个函数。这个函数会执行以下操作:

    • 将EXTI对应线路配置为中断模式(而不是事件模式)。
    • 清除该线路的挂起位。
    • 通过设置EXTI的唤醒中断屏蔽寄存器(EXTI_WIMR),使能该线路的唤醒能力。
    • 可能还需要配置电源控制器(PWR)以允许特定的唤醒源(如EXTI)生效。
  3. 触发流程: 当一个外设驱动(如UART驱动)在执行uart_suspend_port时,如果发现该串口被配置为唤醒源,就会调用enable_irq_wake。这将触发上述硬件配置流程,使得当STM32H750进入STOP模式后,如果UART的RX引脚上出现电平变化,EXTI能够产生一个唤醒信号,让微控制器恢复运行。

单核环境影响

在单核处理器上,scoped_irqdesc_get_and_buslock宏获取的锁(bus_lock)的主要作用是禁用本地中断。这可以防止在修改wake_depth计数器的过程中,被一个中断服务程序抢占。如果中断服务程序中也尝试修改同一个或另一个中断的唤醒状态,就可能导致数据不一致。因此,即使在单核系统上,这种锁机制对于保证操作的原子性和数据完整性也是至关重要的。

无MMU影响

中断唤醒机制完全工作在内核的特权级别,它处理的是中断号、中断描述符和对硬件寄存器的直接操作。这些操作与内存的虚拟化和地址翻译无关。因此,没有MMU对这段代码的逻辑和执行没有任何影响。它可以在无MMU的STM32H750上正确地与硬件交互,配置中断唤醒功能。

代码分析

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
/* 声明外部函数 irq_set_irq_wake */
extern int irq_set_irq_wake(unsigned int irq, unsigned int on);

/**
* @brief 启用一个中断的唤醒功能。
* @param irq 要启用唤醒功能的中断号。
* @return int 调用 irq_set_irq_wake 的返回值。
*/
static inline int enable_irq_wake(unsigned int irq)
{
return irq_set_irq_wake(irq, 1);
}

/**
* @brief 禁用一个中断的唤醒功能。
* @param irq 要禁用唤醒功能的中断号。
* @return int 调用 irq_set_irq_wake 的返回值。
*/
static inline int disable_irq_wake(unsigned int irq)
{
return irq_set_irq_wake(irq, 0);
}

/**
* @brief 真正执行设置中断唤醒硬件操作的函数。
* @param irq 中断号。
* @param on 1 表示启用,0 表示禁用。
* @return int 成功返回0,失败返回错误码。
*/
static int set_irq_wake_real(unsigned int irq, unsigned int on)
{
/* 获取中断号对应的中断描述符。 */
struct irq_desc *desc = irq_to_desc(irq);
int ret = -ENXIO; /* 默认为设备无效错误。 */

/* 如果中断控制器芯片标志指示应跳过设置唤醒,则直接成功返回。 */
if (irq_desc_get_chip(desc)->flags & IRQCHIP_SKIP_SET_WAKE)
return 0;

/* 如果中断控制器芯片实现了irq_set_wake回调函数,则调用它。 */
if (desc->irq_data.chip->irq_set_wake)
ret = desc->irq_data.chip->irq_set_wake(&desc->irq_data, on);

return ret;
}

/**
* @brief 清除 irq_data 状态中的指定标志位。
* @param d 指向 irq_data 结构的指针。
* @param mask 要清除的标志位掩码。
*/
static inline void irqd_clear(struct irq_data *d, unsigned int mask)
{
__irqd_to_state(d) &= ~mask;
}

/**
* @brief 设置 irq_data 状态中的指定标志位。
* @param d 指向 irq_data 结构的指针。
* @param mask 要设置的标志位掩码。
*/
static inline void irqd_set(struct irq_data *d, unsigned int mask)
{
__irqd_to_state(d) |= mask;
}

/**
* @brief 控制中断的电源管理唤醒功能。
* @param irq 要控制的中断号。
* @param on 启用(1)或禁用(0)电源管理唤醒。
* @return int 成功返回0,失败返回错误码。
*
* @details 启用或禁用电源管理唤醒模式,该模式默认是禁用的。
* 启用和禁用必须配对使用。唤醒模式允许此中断将系统
* 从睡眠状态(如“挂起到内存”)中唤醒。
* 注意:中断的启用/禁用状态与唤醒的启用/禁用状态是完全正交的。
* 一个中断可以被 disable_irq() 禁用,但只要其唤醒功能被启用,
* 它仍然可以唤醒系统。
*/
int irq_set_irq_wake(unsigned int irq, unsigned int on)
{
/* 获取中断描述符并加锁,确保操作的原子性。 */
scoped_irqdesc_get_and_buslock(irq, IRQ_GET_DESC_CHECK_GLOBAL) {
struct irq_desc *desc = scoped_irqdesc;
int ret = 0;

/* 不应使用NMI(不可屏蔽中断)作为唤醒中断。 */
if (irq_is_nmi(desc))
return -EINVAL;

/*
* 支持唤醒的中断可以在具有不同睡眠模式行为的驱动之间共享。
* 这里使用 wake_depth 作为引用计数器。
*/
if (on) {
/* 如果是第一次启用唤醒功能(计数器从0到1)。 */
if (desc->wake_depth++ == 0) {
ret = set_irq_wake_real(irq, on);
/* 如果硬件设置失败,则将计数器恢复为0。 */
if (ret)
desc->wake_depth = 0;
else
/* 硬件设置成功,在状态中设置唤醒标志。 */
irqd_set(&desc->irq_data, IRQD_WAKEUP_STATE);
}
} else {
/* 检查是否出现不配对的禁用操作。 */
if (desc->wake_depth == 0) {
WARN(1, "不配对的IRQ %d唤醒禁用\n", irq);
} else if (--desc->wake_depth == 0) {
/* 如果是最后一次禁用(计数器从1到0)。 */
ret = set_irq_wake_real(irq, on);
/* 如果硬件禁用失败,则将计数器恢复为1。 */
if (ret)
desc->wake_depth = 1;
else
/* 硬件禁用成功,清除状态中的唤醒标志。 */
irqd_clear(&desc->irq_data, IRQD_WAKEUP_STATE);
}
}
return ret;
}
/* 如果获取中断描述符失败,返回无效参数错误。 */
return -EINVAL;
}
/* 将该函数导出,以便内核模块可以使用。 */
EXPORT_SYMBOL(irq_set_irq_wake);

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);

}