文档教程操作系统【免费下载链接】linux-insidesA book-in-progress about the Linux kernel and its insides.项目地址https://gitcode.com/gh_mirrors/li/linux-insides点击查看免费下载导读本文是 linux-insides 仓库《Interrupts and Interrupt Handling》章节第八篇深入讲解 x86_64 架构下外部硬件中断的非早期初始化流程。上篇我们通过early_irq_init完成了irq_desc中断描述符的早期初始化本篇将沿着init/main.c的启动路径逐行剖析init_IRQ如何填充 per-CPU 的vector_irq数组、native_init_IRQ如何完成 ISA 中断与 Local APIC 初始化、以及中断门interrupt gate如何通过alloc_intr_gate与used_vectors位图批量建立。读完本文你将掌握 x86 中断向量空间的分配规则、legacy 中断0x30~0x3f的登记机制、test_bit的编译期/运行期优化原理以及 IRQ2 级联线与setup_irq的完整工作链路。从 early_irq_init 到 init_IRQ非早期初始化登场在 Linux 内核启动流程中init/main.c里紧接着early_irq_init函数之后调用的就是init_IRQ函数。如果说early_irq_init位于kernel/irq/irqdesc.c负责的是irq_desc结构的早期初始化——它是 Linux 内核中断管理代码的基石代表一个中断描述符——那么init_IRQ则把工作推进到了非早期阶段它初始化vector_irq这个 per-CPU 变量为后续外部硬件中断的实际处理铺路。关于早期部分可以回顾本仓库 Interrupts/linux-interrupts-7.md那里展示了irq_desc的数组定义struct irq_desc irq_desc[NR_IRQS]预置handle_bad_irq处理器、depth 1与自旋锁、NR_IRQS在CONFIG_X86_IO_APIC下的取值逻辑例如dmesg | grep NR_IRQS常见输出NR_IRQS:4352以及CONFIG_SPARSE_IRQ开启时arch_probe_nr_irqs对预分配 IRQ 数目的探测。本篇承接的就是这个时间点之后的工作。init_IRQ是架构相关函数定义在arch/x86/kernel/irqinit.c。它要初始化的vector_irqper-CPU 变量同样定义在该文件中... DEFINE_PER_CPU(vector_irq_t, vector_irq) { [0 ... NR_VECTORS - 1] -1, }; ...vector_irq_t类型定义于arch/x86/include/asm/hw_irq.h展开后是一个长度固定的 int 数组typedef int vector_irq_t[NR_VECTORS];其中NR_VECTORS是向量号的总数。正如本仓库 Interrupts/linux-interrupts-1.md 中介绍的理论向量号是 IDTInterrupt Descriptor Table的索引取值范围为 0~255#define NR_VECTORS 256也就是说每个 CPU 都拥有一份 256 个 int 的vector_irq数组初始值全部为-1用于记录某个中断向量当前对应哪个 IRQ 号。关于 per-CPU 变量的实现机制DEFINE_PER_CPU、per_cpu宏、__per_cpu_offset等可进一步阅读本仓库 Concepts/linux-cpu-1.md。填充 vector_irqlegacy 中断向量登记init_IRQ函数开头的核心动作是把 legacy传统中断的向量号写入 CPU0 的vector_irq数组void __init init_IRQ(void) { int i; for (i 0; i nr_legacy_irqs(); i) per_cpu(vector_irq, 0)[IRQ0_VECTOR i] i; ... ... ... }这里有两个关键点需要展开1.nr_legacy_irqs()的取值来源。它定义在arch/x86/include/asm/i8259.h只是一个转发函数返回legacy_pic结构体中的nr_legacy_irqs字段static inline int nr_legacy_irqs(void) { return legacy_pic-nr_legacy_irqs; }legacy_pic结构体同样定义在该头文件中它抽象了一个非现代的可编程中断控制器PIC例如经典的 Intel 8259Astruct legacy_pic { int nr_legacy_irqs; struct irq_chip *chip; void (*mask)(unsigned int irq); void (*unmask)(unsigned int irq); void (*mask_all)(void); void (*restore_mask)(void); void (*init)(int auto_eoi); int (*irq_pending)(unsigned int irq); void (*make_irq)(unsigned int irq); };而 legacy 中断数目的实际上限由宏NR_IRQS_LEGACY给出定义在arch/x86/include/asm/irq_vectors.h#define NR_IRQS_LEGACY 162.IRQ0_VECTOR为什么是 0x30。循环里以IRQ0_VECTOR i为下标写入向量号IRQ0_VECTOR定义在同一个irq_vectors.h头文件中#define FIRST_EXTERNAL_VECTOR 0x20 #define IRQ0_VECTOR ((FIRST_EXTERNAL_VECTOR 16) ~15)即IRQ0_VECTOR为0x30十进制 32。原因要回到中断向量空间的分配约定见本仓库 Interrupts/linux-interrupts-1.md 与 Interrupts/linux-interrupts-6.md向量号0~310x00~0x1f被处理器保留用于处理架构定义的异常和中断向量号0x30~0x3f则被保留给 ISAIndustry Standard Architecture。因此这段循环实际上是把向量0x30~0x3f即 IRQ0~IRQ15登记进vector_irq[0]对应关系为vector_irq[0][IRQ0_VECTOR i] i。补充说明现代系统上的所有中断实际上都由 I/O APICAdvanced Programmable Interrupt Controller 的 I/O 部分处理。之所以称这些为 legacy 中断是因为0x30~0x3f这段向量空间传统上由 8259A 这类老式 PIC 控制器服务如果它们改由 I/O APIC 处理这段向量空间会被释放并重新利用。填充好的vector_irq会在外部硬件中断处理的第一步被消费——在do_IRQ函数位于arch/x86/kernel/irq.c中内核通过__this_cpu_read读取当前 CPU 上该向量对应的 IRQ 号__visible unsigned int __irq_entry do_IRQ(struct pt_regs *regs) { ... ... ... irq __this_cpu_read(vector_irq[vector]); if (!handle_irq(irq, regs)) { ... ... ... } exiting_irq(); ... ... return 1; }可以看到vector_irq是向量号 → IRQ 号的映射表是中断从硬件向量到内核 IRQ 语义的关键桥梁。x86_init.irqs.intr_init()进入架构相关的中断初始化init_IRQ函数末尾调用了x86_init.irqs.intr_init();x86_init是x86_init_ops类型的结构体定义于arch/x86/kernel/x86_init.c。如果你读过本仓库 Initialization 章节会记得这个结构体包含多个指向平台设置函数的字段resources与内存资源相关、mpparse与 MultiProcessor Configuration Table 解析相关等。与中断相关的irqs字段包含三个成员struct x86_init_ops x86_init __initdata { ... ... ... .irqs { .pre_vector_init init_ISA_irqs, .intr_init native_init_IRQ, .trap_init x86_init_noop, }, ... ... ... }我们关注的是native_init_IRQ。native_前缀表示它是架构特定实现该函数定义在arch/x86/kernel/irqinit.c负责执行 Local APIC 的通用初始化和 ISA 中断的初始化。它的执行路径大致如下native_init_IRQ ├── x86_init.irqs.pre_vector_init() → init_ISA_irqs() │ ├── legacy_pic-init(0) → init_8259A() │ └── irq_set_chip_and_handler(...) 设置 legacy 中断的 chip 与 handler ├── apic_intr_init() → 分配 SMP 专用中断门 ├── 循环设置 FIRST_EXTERNAL_VECTOR 起的中断门irq_entries_start ├── 用 spurious_interrupt 填充剩余未使用向量 └── setup_irq(2, irq2) → 注册 IRQ2 级联条件满足时init_ISA_irqsISA 中断与 Local APIC 初始化native_init_IRQ的第一步是执行x86_init.irqs.pre_vector_init()它指向init_ISA_irqs函数定义在同一文件arch/x86/kernel/irqinit.c。从函数名就能看出它做的是与 ISA 相关中断的初始化。函数开头先从legacy_pic取出芯片描述void __init init_ISA_irqs(void) { struct irq_chip *chip legacy_pic-chip; ... ... ...irq_chip结构体定义于include/linux/irq.h表示硬件中断芯片的描述符包含多个关键回调字段name—— 设备名称会出现在/proc/interrupts的最后一列。例如$ cat /proc/interrupts CPU0 CPU1 CPU2 CPU3 CPU4 CPU5 CPU6 CPU7 0: 16 0 0 0 0 0 0 0 IO-APIC 2-edge timer 1: 2 0 0 0 0 0 0 0 IO-APIC 1-edge i8042 8: 1 0 0 0 0 0 0 0 IO-APIC 8-edge rtc0(*irq_mask)(struct irq_data *data)—— 屏蔽一个中断源(*irq_ack)(struct irq_data *data)—— 一个中断的开始确认acknowledge(*irq_startup)(struct irq_data *data)—— 启动中断(*irq_shutdown)(struct irq_data *data)—— 关闭中断以及其他若干字段。注意irq_data结构体表示传递给芯片函数的 per-irq 芯片数据集合包含mask预计算的、用于访问芯片寄存器的位掩码、irq中断号、hwirq硬件中断号局部于中断域芯片的底层硬件访问编号等字段。接下来取决于CONFIG_X86_64与CONFIG_X86_LOCAL_APIC这两个内核配置选项init_ISA_irqs会调用init_bsp_APIC定义于arch/x86/kernel/apic/apic.c#if defined(CONFIG_X86_64) || defined(CONFIG_X86_LOCAL_APIC) init_bsp_APIC(); #endifinit_bsp_APIC负责初始化引导处理器bootstrap processor即最先启动的那个 CPU的 APIC。它首先检查是否发现了 SMP 配置、以及处理器是否具备 APICif (smp_found_config || !cpu_has_apic) return;若不满足条件则直接返回。否则接下来调用clear_local_APIC同一源文件关闭本地 APIC然后通过设置APIC_SPIVSpurious Interrupt Vector Register的APIC_SPIV_APIC_ENABLED位来启用第一个处理器的 APICvalue apic_read(APIC_SPIV); value ~APIC_VECTOR_MASK; value | APIC_SPIV_APIC_ENABLED;并用apic_write写回寄存器apic_write(APIC_SPIV, value);BSP 的 APIC 启用之后回到init_ISA_irqs下一步是初始化 legacy 可编程中断控制器并为每个 legacy IRQ 设置芯片与高层处理器high-level handlerlegacy_pic-init(0); for (i 0; i nr_legacy_irqs(); i) irq_set_chip_and_handler(i, chip, handle_level_irq);这里legacy_pic-init指向哪里legacy_pic全局变量定义于arch/x86/kernel/i8259.cstruct legacy_pic *legacy_pic default_legacy_pic;而default_legacy_pic的init成员指向init_8259Astruct legacy_pic default_legacy_pic { ... ... ... .init init_8259A, ... ... ... }init_8259A定义在同一源文件中执行 Intel 8259 可编程中断控制器的初始化更完整的 8259A/APIC 细节将在专门章节展开。注意此处调用参数为0auto_eoi为 0表示不使用自动 EOI 模式。随后循环把chip即legacy_pic-chip与handle_level_irq处理器绑定到 0~15 号 legacy IRQ 上。apic_intr_init 与 alloc_intr_gateSMP 专用中断门的分配init_ISA_irqs完成工作后native_init_IRQ继续调用apic_intr_init为 SMP 架构使用的处理器间中断Inter-processor InterruptIPI分配特殊中断门。分配动作通过alloc_intr_gate宏完成它定义于arch/x86/include/asm/desc.h#define alloc_intr_gate(n, addr) \ do { \ alloc_system_vector(n); \ set_intr_gate(n, addr); \ } while (0)首先展开为alloc_system_vector(n)调用该函数在used_vectors位图中检查给定向量号是否已被使用若未被置位则置位并更新first_system_vector若已被占用则触发 BUGif (!test_bit(vector, used_vectors)) { set_bit(vector, used_vectors); if (first_system_vector vector) first_system_vector vector; } else { BUG(); }used_vectors位图与first_system_vector在上一部分 Interrupts/linux-interrupts-7.md 中已有铺垫这里它保证系统向量互不冲突。随后set_intr_gate把向量n对应的 IDT 门设为入口地址addr。test_bit 的两种实现编译期常量与运行期变量的优化博弈上文用到了test_bit宏它定义于arch/x86/include/asm/bitops.h#define test_bit(nr, addr) \ (__builtin_constant_p((nr)) \ ? constant_test_bit((nr), (addr)) \ : variable_test_bit((nr), (addr)))这是一个基于 GCC 内建函数__builtin_constant_p的三目运算符如果nr在编译期是已知常量就调用constant_test_bit否则调用variable_test_bit。为了直观理解__builtin_constant_p可以编译一个最小测试程序#include stdio.h #define PREDEFINED_VAL 1 int main() { int i 5; printf(__builtin_constant_p(i) is %d\n, __builtin_constant_p(i)); printf(__builtin_constant_p(PREDEFINED_VAL) is %d\n, __builtin_constant_p(PREDEFINED_VAL)); printf(__builtin_constant_p(100) is %d\n, __builtin_constant_p(100)); return 0; }编译并运行$ gcc test.c -o test $ ./test __builtin_constant_p(i) is 0 __builtin_constant_p(PREDEFINED_VAL) is 1 __builtin_constant_p(100) is 1可见普通局部变量i的值在编译期未知返回 0而宏常量与字面量在编译期已知返回 1。回到test_bit。当__builtin_constant_p返回非零时走constant_test_bitstatic inline int constant_test_bit(int nr, const void *addr) { const u32 *p (const u32 *)addr; return ((1UL (nr 31)) (p[nr 5])) ! 0; }否则走variable_test_bit它用 x86 的btbit test指令配合setc实现单指令位测试static inline int variable_test_bit(int nr, const void *addr) { u8 v; const u32 *p (const u32 *)addr; asm(btl %2,%1; setc %0 : qm (v) : m (*p), Ir (nr)); return v; }两者的目的相同区别在于优化。用一个简单例子对比两者产生的汇编代码#define CONST 25 int main() { int nr 24; variable_test_bit(nr, (int*)0x10000000); constant_test_bit(CONST, (int*)0x10000000) return 0; }constant_test_bit版本生成的汇编x86_64System V AMD64 ABI 调用约定参数依次放入edi/esi等寄存器pushq %rbp movq %rsp, %rbp movl $268435456, %esi movl $25, %edi call constant_test_bit而variable_test_bit版本pushq %rbp movq %rsp, %rbp subq $16, %rsp movl $24, -4(%rbp) movl -4(%rbp), %eax movl $268435456, %esi movl %eax, %edi call variable_test_bit两段代码开头相同把当前栈帧基址保存到%rbp。随后分道扬镳第一段直接向%esi装入第二参数$268435456即0x10000000向%edi装入第一参数$25然后调用constant_test_bit。因为参数是编译期常量编译器可以直接代入值无需任何栈操作。第二段nr是运行期变量编译器无法代入必须把它放到程序的栈帧上subq $16, %rsp为局部变量腾出 16 字节栈空间把$24nr的当前值写到%rbp - 4的位置再装入%eax最后才把两个参数放入寄存器调用variable_test_bit。此时栈帧布局如下- stack grows %[rbp] | ---------- --------- --------- -------- | | | | | return | | | | nr |-| |-| |-| argc | | | | | | address | | | ---------- --------- --------- -------- | %[rsp]结论很清晰当向量号是编译期常量时编译器可零成本完成位测试只有运行期变量才需要走寄存器 bt指令的路径。这正是内核在test_bit上做两层封装的意义所在。建立外部中断门irq_entries_start 与 spurious_interruptapic_intr_init完成之后下一步是从FIRST_EXTERNAL_VECTOR即0x20到0x100设置中断门i FIRST_EXTERNAL_VECTOR; #ifndef CONFIG_X86_LOCAL_APIC #define first_system_vector NR_VECTORS #endif for_each_clear_bit_from(i, used_vectors, first_system_vector) { set_intr_gate(i, irq_entries_start 8 * (i - FIRST_EXTERNAL_VECTOR)); }这里的关键是使用了for_each_clear_bit_from辅助宏它只遍历used_vectors位图中尚未置位的向量号为每个空闲向量在 IDT 中设置门入口地址为irq_entries_start加上偏移量每个中断入口占 8 字节。也就是说那些已经被占用例如前 32 个异常向量、或上面alloc_system_vector分配掉的系统向量不会在这里被重复设置。随后在CONFIG_X86_LOCAL_APIC开启的情况下用同样的for_each_clear_bit_from把中断表IDT中仍然空缺的门全部填充为spurious_interrupt伪中断处理器用于处理虚假中断#ifdef CONFIG_X86_LOCAL_APIC for_each_clear_bit_from(i, used_vectors, NR_VECTORS) set_intr_gate(i, spurious_interrupt); #endifused_vectors是一个unsigned long位图记录已经初始化的中断门。前 32 个中断向量是在trap_init函数arch/x86/kernel/setup.c中被预先置位的for (i 0; i FIRST_EXTERNAL_VECTOR; i) set_bit(i, used_vectors);这一过程正是本仓库 Interrupts/linux-interrupts-6.md 所描述的内容——0~31 号向量为处理器异常保留因此在内核早期就已登记。IRQ2 级联setup_irq(2, irq2)native_init_IRQ函数末尾有一个值得单独讲解的条件与调用if (!acpi_ioapic !of_ioapic nr_legacy_irqs()) setup_irq(2, irq2);先拆解条件中的三个变量acpi_ioapic表示是否存在 ACPI 描述的 I/O APIC定义于arch/x86/kernel/acpi/boot.c。它会在处理 Multiple APIC Description TableMADT时由acpi_set_irq_model_ioapic函数置位。注意该变量受CONFIG_ACPI与CONFIG_X86_LOCAL_APIC配置影响若两者未开启它退化为常量 0#define acpi_ioapic 0of_ioapic表示是否使用 Open Firmware设备树描述的 I/O APIC定义于arch/x86/kernel/devicetree.c由dtb_ioapic_setup函数初始化。它同样依赖配置选项CONFIG_OF未开启时也是 0#ifdef CONFIG_OF extern int of_ioapic; ... ... ... #else #define of_ioapic 0 ... ... ... #endifnr_legacy_irqs()前文已介绍返回 legacy 中断数量。综合来看该条件为真意味着系统没有ACPI I/O APIC、没有设备树 I/O APIC并且存在 legacy 中断控制器——即机器仍然依赖传统 PIC 架构。此时才需要注册 IRQ2 级联线。irq2是一个irqaction结构体定义于arch/x86/kernel/irqinit.c代表 IRQ 2 这条用于查询级联设备的线路static struct irqaction irq2 { .handler no_action, .name cascade, .flags IRQF_NO_THREAD, };为什么需要cascade级联在早期的双 8259A 架构中第二块 PIC 芯片通过 IRQ2 线路挂接到第一块芯片上第二块芯片服务 8~15 号线之后才轮到第一块芯片的线路。以经典的 Intel 8259A 为例其 IRQ 线分配如下IRQ 0—— 系统时钟system timerIRQ 1—— 键盘IRQ 2—— 用于级联连接的设备IRQ 8—— RTC实时时钟IRQ 9—— 保留IRQ 10—— 保留IRQ 11—— 保留IRQ 12—— PS/2 鼠标IRQ 13—— 协处理器IRQ 14—— 硬盘控制器IRQ 1—— 保留IRQ 3—— COM2 和 COM4IRQ 4—— COM1 和 COM3IRQ 5—— LPT2IRQ 6—— 软盘驱动器控制器IRQ 7—— LPT1。注意原表把IRQ 1同时列在键盘与保留两处这正体现了传统 PC 中断分配历史上兼容性约定与文档叙述的差异以键盘占用 IRQ1 为准其他未列出明确设备的线路视为保留。setup_irq函数定义于kernel/irq/manage.c接收两个参数中断向量号与对应的irqaction结构。它首先通过向量号拿到中断描述符struct irq_desc *desc irq_to_desc(irq);然后调用__setup_irq完成实际设置期间对描述符加锁保护chip_bus_lock(desc); retval __setup_irq(irq, desc, act); chip_bus_sync_unlock(desc); return retval;__setup_irq是一个工作量很大的函数当提供了线程函数且该中断不会嵌套进其他中断线程时它会创建 handler 线程设置芯片标志填充irqaction结构等等。整个过程完成后还会创建/proc/irq/vector_number目录并填充内容。不过在现代机器上由于中断基本由 APIC 处理这些 proc 文件中的值通常都是 0$ cat /proc/irq/2/node 0 $cat /proc/irq/2/affinity_hint 00 cat /proc/irq/2/spurious count 0 unhandled 0 last_unhandled 0 ms这也从侧面印证了条件判断的意义只有在系统真的依赖 legacy PIC 时IRQ2 级联的注册才有实际价值。小结与后续阅读本篇对应仓库 Interrupts/linux-interrupts-8.md完整走通了外部硬件中断的非早期初始化链路init_IRQ把 legacy 中断向量0x30~0x3f登记进 CPU0 的 per-CPUvector_irq数组该数组在do_IRQ中承担向量号 → IRQ 号的映射职责通过x86_init.irqs.intr_init()进入native_init_IRQ先由init_ISA_irqs完成 ISA 中断初始化含init_bsp_APIC启用 Local APIC、init_8259A初始化 8259A、irq_set_chip_and_handler绑定芯片与处理器apic_intr_init配合alloc_intr_gate/alloc_system_vector为 SMP 处理器间中断分配中断门test_bit的常量/变量双实现保证了位图查询在编译期可优化用for_each_clear_bit_from从FIRST_EXTERNAL_VECTOR起批量建立外部中断门并用spurious_interrupt兜底填充其余向量在无 ACPI/设备树 I/O APIC 的 legacy 环境下通过setup_irq(2, irq2)注册 IRQ2 级联线。后续内容请继续阅读本仓库 Interrupts/README.md 中列出的后续部分Softirq, Tasklets and WorkqueuesInterrupts/linux-interrupts-9.md将讲解软中断与下半部机制最后一篇 Interrupts/linux-interrupts-10.md 会以一个真实硬件驱动收尾整个中断处理章节。整个章节在全书目录中的位置见 SUMMARY.md。赞分享文档教程操作系统【免费下载链接】linux-insidesA book-in-progress about the Linux kernel and its insides.项目地址https://gitcode.com/gh_mirrors/li/linux-insides点击查看免费下载相关推荐Linux 内核初始化揭秘二早期中断与异常处理——从 IDT 建立到缺页中断处理器Linux 内核初始化揭秘二早期中断与异常处理——从 IDT 建立到缺页中断处理器 本篇技术指南聚焦于 x86_64 架构下 Linux 内核在 解压完成Linux 内核中断与异常处理四非早期中断门的初始化与缺页异常处理Linux 内核中断与异常处理四非早期中断门的初始化与缺页异常处理 导读 本文是《Linux 内核揭秘》中断与中断处理系列的第四篇承接前文对早期 DBlinux-insides 解读四Linux x86_64 内核非早期中断门初始化与 trap_init 全流程linux insides 解读四Linux x86_64 内核非早期中断门初始化与 trap_init 全流程 本文是 linux insides 开源文档教程操作系统上一篇NetBox IPSec Proposal 模型详解IPSec 隧道加密与认证参数建模实战下一篇GitHub1s终极指南为什么开发者纷纷抛弃传统GitHub浏览改用VS Code界面创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考