六、进阶 | 中断和中断处理(7)
中断和中断处理。第7部分。
外部中断介绍
这是Linux内核中断和中断处理章节的第七部分,上一个部分我们完成了由处理器生成的异常处理。在这一部分,我们将继续深入中断处理,并从外部硬件中断处理开始。如你所记得的,在上一部分我们以arch/x86/kernel/trap.c中的trap_init
函数结束,下一步是调用init/main.c中的early_irq_init
函数。
中断是由硬件或软件通过IRQ或中断请求线
发送的信号。外部硬件中断允许设备如键盘、鼠标等,指示它需要处理器的注意。一旦处理器接收到中断请求
,它将暂时停止运行程序并调用一个特殊程序,这个程序取决于中断类型。我们已经知道这个程序被称为中断处理程序(或者从这部分开始我们将其称为ISR
或中断服务例程
)。ISR
或中断处理程序
可以在位于内存中固定地址的中断向量表中找到。处理完中断后,处理器恢复被中断的进程。在启动/初始化时,Linux内核识别机器中的所有设备,并将适当的中断处理程序加载到中断表中。正如我们在前几部分看到的,大多数异常简单地通过向被中断的进程发送Unix信号来处理。这就是内核可以快速处理异常的方式。不幸的是,我们不能使用这种方法来处理外部硬件中断,因为它们通常在它们相关的进程被挂起之后(有时是很久之后)到达。所以向当前进程发送Unix信号是没有意义的。外部中断处理取决于中断的类型:
I/O
中断;- 定时器中断;
- 处理器间中断。
我将尝试在这本书中描述所有类型的中断。
一般来说,一个I/O
中断的处理程序必须足够灵活,能够同时为多个设备提供服务。例如,在PCI总线架构中,多个设备可能共享相同的IRQ
线。在最简单的情况下,当一个I/O
中断发生时,Linux内核必须做以下事情:
- 保存
IRQ
的值和寄存器的内容到内核栈上; - 向服务
IRQ
线的硬件控制器发送确认; - 执行与设备相关联的中断服务例程(接下来我们将称之为
ISR
); - 恢复寄存器并从中断返回;
好的,我们了解了一点理论,现在让我们从early_irq_init
函数开始。early_irq_init
函数的实现在kernel/irq/irqdesc.c中。这个函数对irq_desc
结构进行早期初始化。irq_desc
结构是Linux内核中断管理代码的基础。这个结构的数组,名字也是irq_desc
,跟踪Linux内核中的每一个中断请求源。这个结构定义在include/linux/irqdesc.h中,如你所见,它依赖于CONFIG_SPARSE_IRQ
内核配置选项。这个内核配置选项启用了稀疏IRQ的支持。irq_desc
结构包含许多不同的字段:
irq_common_data
- 每个中断和芯片数据传递给芯片函数;status_use_accessors
- 包含中断源的状态,这是来自include/linux/irq.h中的enum
值和在同一源代码文件中定义的不同宏的组合;kstat_irqs
- 每个CPU的IRQ统计;handle_irq
- 高级irq事件处理程序;action
- 确定在IRQ发生时要调用的中断服务例程;irq_count
- IRQ线上中断发生次数的计数器;depth
- 如果IRQ线被启用则为0
,如果它至少被禁用过一次则为正值;last_unhandled
- 未处理计数的老化计时器;irqs_unhandled
- 未处理中断的计数;lock
- 用于序列化对IRQ
描述符访问的自旋锁;pending_mask
- 待平衡的中断;owner
- 中断描述符的所有者。中断描述符可以从模块分配。这个字段需要提供对提供中断的模块的引用计数;- 等等。
当然,这不是irq_desc
结构的所有字段,因为它太长了,无法描述这个结构的每个字段,但我们很快就会看到所有的。现在让我们开始深入early_irq_init
函数的实现。
早期外部中断初始化
现在,让我们看看early_irq_init
函数的实现。注意early_irq_init
函数的实现依赖于CONFIG_SPARSE_IRQ
内核配置选项。现在我们考虑当CONFIG_SPARSE_IRQ
内核配置选项未设置时early_irq_init
函数的实现。这个函数从声明以下变量开始:irq
描述符计数器、循环计数器、内存节点和irq_desc
描述符:
int __init early_irq_init(void)
{
int count, i, node = first_online_node;
struct irq_desc *desc;
...
...
...
}
node
是一个在线的NUMA节点,它取决于MAX_NUMNODES
值,该值取决于CONFIG_NODES_SHIFT
内核配置参数:
#define MAX_NUMNODES (1 << NODES_SHIFT)
...
...
...
#ifdef CONFIG_NODES_SHIFT
#define NODES_SHIFT CONFIG_NODES_SHIFT
#else
#define NODES_SHIFT 0
#endif
正如我已经写过的,first_online_node
宏的实现取决于MAX_NUMNODES
值:
#if MAX_NUMNODES > 1
#define first_online_node first_node(node_states[N_ONLINE])
#else
#define first_online_node 0
node_states
是在include/linux/nodemask.h中定义的枚举,代表节点状态的集合。在我们的情况下,我们正在寻找一个在线节点,如果MAX_NUMNODES
是一或零,它将是0
。如果MAX_NUMNODES
大于一,node_states[N_ONLINE]
将返回1
,first_node
宏将展开为调用__first_node
函数,它将返回最小
的或第一个在线节点:
#define first_node(src) __first_node(&(src))
static inline int __first_node(const nodemask_t *srcp)
{
return min_t(int, MAX_NUMNODES, find_first_bit(srcp->bits, MAX_NUMNODES));
}
更多关于这方面的内容将在另一章关于NUMA
中介绍。在声明了这些局部变量之后的下一步是调用:
init_irq_default_affinity();
函数。init_irq_default_affinity
函数定义在同一个源代码文件中,并且依赖于CONFIG_SMP
内核配置选项为给定的cpumask结构(在我们的情况下是irq_default_affinity
)分配空间:
#if defined(CONFIG_SMP)
cpumask_var_t irq_default_affinity;
static void __init init_irq_default_affinity(void)
{
alloc_cpumask_var(&irq_default_affinity, GFP_NOWAIT);
cpumask_setall(irq_default_affinity);
}
#else
static void __init init_irq_default_affinity(void)
{
}
#endif
我们知道,当硬件,如磁盘控制器或键盘,需要处理器的注意时,它会抛出一个中断。中断告诉处理器发生了一些事情,处理器应该中断当前进程并处理即将到来的事件。为了防止多个设备发送相同的中断,建立了IRQ系统,其中计算机系统中的每个设备都被分配了自己特殊的IRQ,以便其中断是唯一的。Linux内核可以为特定的处理器分配某些IRQs
。这被称为SMP IRQ affinity
,它允许您控制系统将如何响应各种硬件事件(这就是为什么它只有在设置了CONFIG_SMP
内核配置选项时才有特定的实现)。在我们分配了irq_default_affinity
cpumask之后,我们可以看到printk
输出:
printk(KERN_INFO "NR_IRQS:%d\n", NR_IRQS);
它打印出NR_IRQS
:
~$ dmesg | grep NR_IRQS
[ 0.000000] NR_IRQS:4352
NR_IRQS
是irq
描述符或换句话说最大中断数的数量。它的值取决于CONFIG_X86_IO_APIC
内核配置选项的状态。如果CONFIG_X86_IO_APIC
未设置,并且Linux内核使用旧的PIC芯片,NR_IRQS
是:
#define NR_IRQS_LEGACY 16
#ifdef CONFIG_X86_IO_APIC
...
...
...
#else
# define NR_IRQS NR_IRQS_LEGACY
#endif
在另一种情况下,当设置了CONFIG_X86_IO_APIC
内核配置选项时,NR_IRQS
取决于处理器的数量和中断向量的数量:
#define CPU_VECTOR_LIMIT (64 * NR_CPUS)
#define NR_VECTORS 256
#define IO_APIC_VECTOR_LIMIT ( 32 * MAX_IO_APICS )
#define MAX_IO_APICS 128
# define NR_IRQS \
(CPU_VECTOR_LIMIT > IO_APIC_VECTOR_LIMIT ? \
(NR_VECTORS + CPU_VECTOR_LIMIT) : \
(NR_VECTORS + IO_APIC_VECTOR_LIMIT))
...
...
...
我们记得从前面的部分,我们可以在Linux内核配置过程中用CONFIG_NR_CPUS
配置选项设置处理器的数量:
在第一种情况(CPU_VECTOR_LIMIT > IO_APIC_VECTOR_LIMIT
)中,NR_IRQS
将是4352
,在第二种情况(CPU_VECTOR_LIMIT < IO_APIC_VECTOR_LIMIT
)中,NR_IRQS
将是768
。在我的情况下,NR_CPUS
是8
,正如你在我的配置中所看到的,CPU_VECTOR_LIMIT
是512
,IO_APIC_VECTOR_LIMIT
是4096
。所以我的配置的NR_IRQS
是4352
:
~$ dmesg | grep NR_IRQS
[ 0.000000] NR_IRQS:4352
接下来,我们将IRQ描述符数组分配给在early_irq_init
函数开始时定义的irq_desc
变量,并使用ARRAY_SIZE
宏计算irq_desc
数组的计数:
desc = irq_desc;
count = ARRAY_SIZE(irq_desc);
irq_desc
数组定义在同一个源代码文件中,如下所示:
struct irq_desc irq_desc[NR_IRQS] __cacheline_aligned_in_smp = {
[0 ... NR_IRQS-1] = {
.handle_irq = handle_bad_irq,
.depth = 1,
.lock = __RAW_SPIN_LOCK_UNLOCKED(irq_desc->lock),
}
};
irq_desc
是irq
描述符的数组。它有三个已经初始化的字段:
handle_irq
- 如上所述,这个字段是高级irq事件处理程序。在我们的情况下,它用定义在kernel/irq/handle.c源代码文件中的handle_bad_irq
函数初始化,该函数处理杂散和未处理的IRQ;depth
- 如果IRQ线被启用则为0
,如果它至少被禁用过一次则为正值;lock
- 用于序列化对IRQ
描述符访问的自旋锁。
由于我们已经计算了中断的数量并初始化了我们的irq_desc
数组,我们开始在循环中填充描述符:
for (i = 0; i < count; i++) {
desc[i].kstat_irqs = alloc_percpu(unsigned int);
alloc_masks(&desc[i], GFP_KERNEL, node);
raw_spin_lock_init(&desc[i].lock);
lockdep_set_class(&desc[i].lock, &irq_desc_lock_class);
desc_set_defaults(i, &desc[i], node, NULL);
}
我们将遍历所有中断描述符,并执行以下操作:
首先,我们使用alloc_percpu
宏为irq
内核统计分配每个CPU变量。这个宏为系统中的每个处理器分配一个给定类型的一个对象实例。你可以通过/proc/stat
从用户空间访问内核统计信息:
~$ cat /proc/stat
cpu 207907 68 53904 5427850 14394 0 394 0 0 0
cpu0 25881 11 6684 679131 1351 0 18 0 0 0
cpu1 24791 16 5894 679994 2285 0 24 0 0 0
cpu2 26321 4 7154 678924 664 0 71 0 0 0
cpu3 26648 8 6931 678891 414 0 244 0 0 0
...
...
...
其中第六列是服务中断的数量。在这之后,我们为给定的IRQ描述符亲和性分配cpumask,并为给定的中断描述符初始化自旋锁。在这之后,在临界区之前,锁将通过调用raw_spin_lock
获取,并通过调用raw_spin_unlock
解锁。下一步,我们调用lockdep_set_class
宏,为给定中断描述符的锁设置锁验证器irq_desc_lock_class
类。关于lockdep
、spinlock
和其他同步原语的更多信息将在单独的章节中描述。
在循环的最后,我们调用kernel/irq/irqdesc.c中的desc_set_defaults
函数。这个函数接受四个参数:
- 中断号;
- 中断描述符;
- 在线
NUMA
节点; - 中断描述符的所有者。中断描述符可以从模块分配。这个字段需要提供对提供中断的模块的引用计数;
并填充剩余的irq_desc
字段。desc_set_defaults
函数填充中断号、irq
芯片、特定于平台的每个芯片私有数据用于芯片方法、每个irq_chip
方法的每个IRQ数据和每个irq
和irq
芯片数据的MSI描述符:
desc->irq_data.irq = irq;
desc->irq_data.chip = &no_irq_chip;
desc->irq_data.chip_data = NULL;
desc->irq_data.handler_data = NULL;
desc->irq_data.msi_desc = NULL;
...
...
...
irq_data.chip
结构提供了通用API
,如irq_set_chip
、irq_set_irq_type
等,用于irq控制器驱动程序。你可以在kernel/irq/chip.c源代码文件中找到它。
在这之后,我们为给定的描述符设置访问器的状态,并设置中断的禁用状态:
...
...
...
irq_settings_clr_and_set(desc, ~0, _IRQ_DEFAULT_INIT_FLAGS);
irqd_set(&desc->irq_data, IRQD_IRQ_DISABLED);
...
...
...
接下来,我们将高级中断处理程序设置为handle_bad_irq
,它处理杂散和未处理的IRQ(由于硬件尚未初始化,我们设置这个处理程序),将irq_desc.desc
设置为1
,这意味着一个IRQ
被禁用,重置未处理中断的计数,以及一般中断的计数:
...
...
...
desc->handle_irq = handle_bad_irq;
desc->depth = 1;
desc->irq_count = 0;
desc->irqs_unhandled = 0;
desc->name = NULL;
desc->owner = owner;
...
...
...
在这之后,我们遍历所有可能的处理器,使用for_each_possible_cpu辅助函数,并将给定中断描述符的kstat_irqs
设置为零:
for_each_possible_cpu(cpu)
*per_cpu_ptr(desc->kstat_irqs, cpu) = 0;
并调用kernel/irq/irqdesc.c中的desc_smp_init
函数,该函数初始化给定中断描述符的NUMA
节点,设置默认的SMP
亲和性,并根据CONFIG_GENERIC_PENDING_IRQ
内核配置选项的值清除给定中断描述符的pending_mask
:
static void desc_smp_init(struct irq_desc *desc, int node)
{
desc->irq_data.node = node;
cpumask_copy(desc->irq_data.affinity, irq_default_affinity);
#ifdef CONFIG_GENERIC_PENDING_IRQ
cpumask_clear(desc->pending_mask);
#endif
}
在early_irq_init
函数的末尾,我们返回arch_early_irq_init
函数的返回值:
return arch_early_irq_init();
这个函数定义在kernel/apic/vector.c中,只包含一个调用kernel/apic/io_apic.c中的arch_early_ioapic_init
函数的调用。正如我们从arch_early_ioapic_init
函数的名称可以理解的,这个函数对I/O APIC进行早期初始化。首先,它通过调用nr_legacy_irqs
函数检查传统中断的数量。如果我们没有使用Intel 8259可编程中断控制器的传统中断,我们将io_apic_irqs
设置为0xffffffffffffffff
:
if (!nr_legacy_irqs())
io_apic_irqs = ~0UL;
在这之后,我们遍历所有的I/O APICs
并为寄存器分配空间,通过调用alloc_ioapic_saved_registers
:
for_each_ioapic(i)
alloc_ioapic_saved_registers(i);
在arch_early_ioapic_init
函数的末尾,我们遍历所有的传统IRQ(从IRQ0
到IRQ15
)并在循环中为irq_cfg
分配空间,它表示给定NUMA
节点上的IRQ配置:
for (i = 0; i < nr_legacy_irqs(); i++) {
cfg = alloc_irq_and_cfg_at(i, node);
cfg->vector = IRQ0_VECTOR + i;
cpumask_setall(cfg->domain);
}
就这些。
稀疏IRQ
我们已经在本部分的开头看到,early_irq_init
函数的实现依赖于CONFIG_SPARSE_IRQ
内核配置选项。之前我们看到了当CONFIG_SPARSE_IRQ
配置选项未设置时early_irq_init
函数的实现,现在让我们看看当这个选项设置时它的实现。这个函数的实现非常相似,但略有不同。我们可以看到在early_irq_init
函数的开头有相同的变量定义和init_irq_default_affinity
的调用:
#ifdef CONFIG_SPARSE_IRQ
int __init early_irq_init(void)
{
int i, initcnt, node = first_online_node;
struct irq_desc *desc;
init_irq_default_affinity();
...
...
...
}
#else
...
...
...
但在这之后我们可以看到以下调用:
initcnt = arch_probe_nr_irqs();
arch_probe_nr_irqs
函数定义在arch/x86/kernel/apic/vector.c中,计算预先分配的IRQ数量,并用这个数字更新nr_irqs
。但是,停下来。为什么会有预先分配的IRQ呢?有一种称为消息信号中断的替代形式的中断在PCI中可用。与分配固定数量的中断请求不同,设备被允许在RAM的特定地址记录一条消息,实际上是在本地APIC上显示。MSI
允许设备分配1
、2
、4
、8
、16
或32
个中断,而MSI-X
允许设备分配多达2048
个中断。现在我们知道IRQ可以预先分配。关于MSI
的更多信息将在下一部分中介绍,但现在让我们看看arch_probe_nr_irqs
函数。我们可以看到检查哪个将系统中每个处理器的中断向量数量分配给nr_irqs
如果它更大,并计算代表MSI
中断数量的nr
:
int nr_irqs = NR_IRQS;
if (nr_irqs > (NR_VECTORS * nr_cpu_ids))
nr_irqs = NR_VECTORS * nr_cpu_ids;
nr = (gsi_top + nr_legacy_irqs()) + 8 * nr_cpu_ids;
看看gsi_top
变量。每个APIC
都通过自己的ID
和其IRQ
开始的偏移量来识别。它被称为GSI
基址或全局系统中断
基址。所以gsi_top
代表它。我们从多处理器配置表中获取全局系统中断
基址(你可以记得我们在Linux内核初始化过程的第六章中解析了这个表)。
在这之后,我们根据gsi_top
的值更新nr
:
#if defined(CONFIG_PCI_MSI) || defined(CONFIG_HT_IRQ)
if (gsi_top <= NR_IRQS_LEGACY)
nr += 8 * nr_cpu_ids;
else
nr += gsi_top * 16;
#endif
更新nr_irqs
如果它小于nr
并返回传统IRQ的数量:
if (nr < nr_irqs)
nr_irqs = nr;
return nr_legacy_irqs();
}
arch_probe_nr_irqs
之后的下一步是打印关于IRQ数量的信息:
printk(KERN_INFO "NR_IRQS:%d nr_irqs:%d %d\n", NR_IRQS, nr_irqs, initcnt);
我们可以在dmesg输出中找到它:
$ dmesg | grep NR_IRQS
[ 0.000000] NR_IRQS:4352 nr_irqs:488 16
在这之后,我们做一些检查以确保nr_irqs
和initcnt
的值不大于最大允许的IRQ数量:
if (WARN_ON(nr_irqs > IRQ_BITMAP_BITS))
nr_irqs = IRQ_BITMAP_BITS;
if (WARN_ON(initcnt > IRQ_BITMAP_BITS))
initcnt = IRQ_BITMAP_BITS;
其中IRQ_BITMAP_BITS
等于NR_IRQS
如果CONFIG_SPARSE_IRQ
未设置,另一种情况下是NR_IRQS + 8196
。下一步是遍历循环中需要分配的所有中断描述符,并为描述符分配空间并插入到irq_desc_tree
基数树中:
for (i = 0; i < initcnt; i++) {
desc = alloc_desc(i, node, NULL);
set_bit(i, allocated_irqs);
irq_insert_desc(i, desc);
}
在early_irq_init
函数的末尾,我们像之前在CONFIG_SPARSE_IRQ
选项未设置的情况下已经做过的那样,返回arch_early_irq_init
函数的调用值:
return arch_early
_init();
这就是全部。
结论
这是中断和中断处理章节的第七部分的结尾,我们在这一部分开始深入外部硬件中断。我们看到了irq_desc
结构的早期初始化,它代表一个外部中断的描述,并包含有关它的信息,如irq动作列表、中断处理程序的信息、中断的所有者、未处理中断的计数等。在下一部分,我们将继续研究外部中断。
如果您有任何问题或建议,请发表评论或在twitter上联系我。
请注意,英语不是我的第一语言,如果有任何不便,我深表歉意。如果您发现任何错误,请向我发送PR到linux-insides。
链接
#嵌入式##面经#"《Linux嵌入式必考必会》专栏,专为嵌入式开发者量身打造,聚焦Linux环境下嵌入式系统面试高频考点。涵盖基础架构、内核原理、驱动开发、系统优化等核心技能,实战案例与理论解析并重。助你快速掌握面试关键,提升竞争力。