Linux x86 IO-APIC:SMP 中断路由机制与 pirq= 手工重定向实战
【免费下载链接】linuxLinux kernel source tree项目地址: https://gitcode.com/GitHub_Trending/li/linux
本文基于 Linux 内核文档 IO-APIC.rst(Ingo Molnar 著)展开,讲解 Intel-MP 兼容 SMP 主板上 IO-APIC 增强中断控制器的工作原理、/proc/interrupts的验证方法,以及当主板 MP 表损坏时如何用pirq=启动参数手工构造 PCI IRQ 路由条目,并结合内核源码 arch/x86/kernel/apic/io_apic.c 印证参数解析、RTE 路由寄存器与 i8259 级联的实现细节。
IO-APIC 是什么:把中断从单核分散到多核
绝大多数(实际上所有)Intel-MP 兼容的 SMP 主板都带有所谓IO-APIC——一个增强型中断控制器。它使硬件中断可以被路由到多个 CPU,或路由到 CPU 组;没有 IO-APIC 时,硬件中断只会投递给启动操作系统的 CPU(通常是 CPU#0)。
Linux 支持所有合规 SMP 主板的变体,包括带多个 IO-APIC的主板。高端服务器使用多个 IO-APIC 来进一步分散 IRQ 负载。从源码结构看,这一点体现在 io_apic.c 的静态数组与结构体上:
static struct ioapic { int nr_registers; /* IRQ 路由寄存器数量 */ struct IO_APIC_route_entry *saved_registers; /* 挂起/恢复或中断重映射时保存的状态 */ struct mpc_ioapic mp_config; /* 来自 MP 表的 IO-APIC 配置 */ struct mp_ioapic_gsi gsi_config; struct ioapic_domain_cfg irqdomain_cfg; struct irq_domain *irqdomain; struct resource *iomem_res; } ioapics[MAX_IO_APICS];每个 IO-APIC 实例记录其寄存器(pin)数量、MP 表配置、GSI 路由区间和 irq domain;内核用for_each_ioapic(idx)宏在 0 到nr_ioapics之间遍历所有 IO-APIC。
值得注意的是,某些较老主板上存在已知的硬件缺陷,这类 bug 通常由内核自行规避(workaround)。如果你的 MP 兼容 SMP 主板无法启动 Linux,建议先查阅 linux-smp 邮件列表存档——原文档即以此作为排查起点。
验证 IO-APIC 是否正常工作:/proc/interrupts
如果你的机器启用了 IO-APIC 中断且能正常启动,那么/proc/interrupts应当形如原文档给出的样例:
hell:~> cat /proc/interrupts CPU0 0: 1360293 IO-APIC-edge timer 1: 4 IO-APIC-edge keyboard 2: 0 XT-PIC cascade 13: 1 XT-PIC fpu 14: 1448 IO-APIC-edge ide0 16: 28232 IO-APIC-level Intel EtherExpress Pro 10/100 Ethernet 17: 51304 IO-APIC-level eth0 NMI: 0 ERR: 0 hell:~>解读要点:
IO-APIC-edge/IO-APIC-level:中断经由 IO-APIC 投递,分别表示边沿触发与电平触发。网络设备(如以太网)通常是电平触发。XT-PIC:仍走传统 8259 可编程中断控制器的中断。少量中断(如cascade级联、fpu协处理器异常)留在 XT PIC 上没有问题——这些中断源都不在性能关键路径上。- 级联机制在内核中同样有对应逻辑:io_apic.c 中的
enable_IO_APIC()会扫描所有 IO-APIC pin,找出处于 ExtINT(External Interrupt)模式的 pin,从而定位 8259 接入 IO-APIC 的位置;若硬件中未配置 ExtINT 而 MP 表报告了,则回退信任 MP 表;两者不一致时打印ExtINT in hardware and MP table differ警告。最后通过clear_IO_APIC()清空所有路由表项——内核不信任启动时 IO-APIC 里的残留状态。
路由寄存器 RTE:中断投递的底层配置
IO-APIC 对每个中断 pin 用一个 64 位**路由表项(RTE, Route Table Entry)**描述投递行为。内核在 arch/x86/include/asm/io_apic.h 中给出了完整的位域定义:
struct IO_APIC_route_entry { union { struct { u64 vector : 8, /* 中断向量 */ delivery_mode : 3, /* 投递模式(含 ExtINT/Fixed 等) */ dest_mode_logical : 1, /* 0=物理, 1=逻辑目标 */ delivery_status : 1, active_low : 1, /* 低有效 */ irr : 1, is_level : 1, /* 电平触发 */ masked : 1, /* 屏蔽位 */ reserved_0 : 15, reserved_1 : 17, virt_destid_8_14 : 7, destid_0_7 : 8; /* 目标 CPU 的 APIC ID */ }; ... }; } __attribute__ ((packed));从字段含义可以印证前文概念:destid_*决定中断投递到哪个 CPU(即"路由到多个 CPU"的落点),masked/is_level/active_low决定 pin 是否可用及其触发极性,delivery_mode中的 ExtINT 模式正是 8259 级联所用的投递方式。
主板 MP 表损坏时:用 pirq= 手工构造 IRQ 条目
在极小概率的情况下,你的主板不会生成可用的 MP 表(MP table)。此时可以用pirq=启动参数手工"构造"IRQ 条目。这不是一个可以自动化的简单操作,原文档以 LILO 配置为例:
append="pirq=15,11,10"具体数值取决于你的系统、你的 PCI 卡以及它们的 PCI 插槽位置。
PCI 槽位的菊花链(daisy chain)结构
通常 PCI 槽位在接入 PCI 芯片组 IRQ 路由设施(PIRQ1-4 输入线)之前会先做"菊花链"串联:
,-. ,-. ,-. ,-. ,-. PIRQ4 ----| |-. ,-| |-. ,-| |-. ,-| |--------| | |S| \ / |S| \ / |S| \ / |S| |S| PIRQ3 ----|l|-. `/---|l|-. `/---|l|-. `/---|l|--------|l| |o| \/ |o| \/ |o| \/ |o| |o| PIRQ2 ----|t|-./`----|t|-./`----|t|-./`----|t|--------|t| |1| /\ |2| /\ |3| /\ |4| |5| PIRQ1 ----| |- `----| |- `----| |- `----| |--------| | `-' `-' `-' `-' `-'每个 PCI 卡都发出一条 PCI IRQ,可能是 INTA、INTB、INTC 或 INTD 四条线之一:
,-. INTD--| | |S| INTC--|l| |o| INTB--|t| |x| INTA--| | `-'这些 INTA-D 信号总是"卡内局部"的,其真实含义取决于卡所在的槽位。对照菊花链图:槽位 4 的卡若发出 INTA,最终会落到 PCI 芯片组的 PIRQ4 上。大多数卡发出的是 INTA,这会在 PIRQ 线之间形成最优的分布(合理分配 IRQ 源并非硬性要求——PCI IRQ 本来就可以随意共享,但非共享中断对性能更有利)。槽位 5 一般留给显卡:显卡通常不使用中断,因此也不参与菊花链。
组合示例:如何得出你的 pirq= 行
假设你的 SCSI 卡(IRQ11)在槽 1,Tulip 网卡(IRQ9)在槽 2,那么需要指定:
append="pirq=11,9"原文档还提供了一个从 PCI 配置自动猜测默认 pirq= 行的脚本(scanpci属于已下线的旧工具,仅作示意):
echo -n pirq=; echo `scanpci | grep T_L | cut -c56-` | sed 's/ /,/g'注意:如果中间跳过了若干槽位,或者主板没有采用默认菊花链(又或者 IO-APIC 的 PIRQ 引脚被接得很奇怪),该脚本会失效。例如在上面场景中 SCSI 卡(IRQ11)实际在槽 3、而槽 1 是空的,则应写:
append="pirq=0,9,11"其中0 是一个通用占位符,表示空槽位(或不发 IRQ 的槽位)。
一般而言,通过正确地全排列所有 IRQ 编号,总归能找到正确的 pirq= 配置——只是比较费时间。一条"不正确"的 pirq 行会导致启动过程挂起,或某个设备不能正常工作(例如该设备以模块形式加载时)。
如果主板有 2 条 PCI 总线,最多可以使用8 个pirq 值;不过这类主板通常配置得比较好。也可能遇到一些奇怪的 pirq 行:
append="pirq=0,0,0,0,0,0,9,11"用明智的试错法找出正确的 pirq 行即可。若本文档未覆盖你遇到的问题,可致信 linux-smp@vger.kernel.org 或 linux-kernel@vger.kernel.org。
源码印证:pirq= 的解析与生效位置
pirq=在内核中的解析入口是 io_apic.c 中的ioapic_pirq_setup(),通过__setup("pirq=", ...)注册:
#ifdef CONFIG_X86_32 /* * support for broken MP BIOSs, enables hand-redirection of PIRQ0-7 to * specific CPU-side IRQs. */ #define MAX_PIRQS 8 static int pirq_entries[MAX_PIRQS] = { [0 ... MAX_PIRQS - 1] = -1 }; static int __init ioapic_pirq_setup(char *str) { int i, max, ints[MAX_PIRQS+1]; get_options(str, ARRAY_SIZE(ints), ints); apic_pr_verbose("PIRQ redirection, working around broken MP-BIOS.\n"); max = MAX_PIRQS; if (ints[0] < MAX_PIRQS) max = ints[0]; for (i = 0; i < max; i++) { apic_pr_verbose("... PIRQ%d -> IRQ %d\n", i, ints[i + 1]); /* PIRQs are mapped upside down, usually */ pirq_entries[MAX_PIRQS-i-1] = ints[i+1]; } return 1; } __setup("pirq=", ioapic_pirq_setup); #endif /* CONFIG_X86_32 */由此可以确认几个与原文档相互印证的事实:
- 上限 8 个值:
MAX_PIRQS 8,对应"2 条 PCI 总线最多 8 个 pirq 值"的说法(第一条 PCI 总线 4 条 PIRQ 线 × 2 条总线)。 - 反转映射:注释"PIRQs are mapped upside down, usually"表明第一个命令行数字对应最高编号的 PIRQ 线——这正是需要按槽位顺序仔细推导的原因。
- 适用前提:整段代码包在
#ifdef CONFIG_X86_32中,即pirq=是 32 位 x86 内核专属的手工重定向机制,适用于"broken MP BIOS"场景;64 位内核没有这条路径。 - 生效点:在
pin_2_irq()中,当 pin 落在 16~23 区间(即 PCI 芯片组接入 IO-APIC 的 PIRQ 线)且对应pirq_entries被赋值时,内核直接返回重定向目标 IRQ(值为 0 则打印Disabling PIRQn并禁用该线),否则才走 MP 表映射mp_map_pin_to_irq()。这解释了为何错误的 pirq 行会导致设备中断彻底丢失、模块加载的设备"不能正常工作"。
此外,驱动通过IO_APIC_get_PCI_irq_vector(bus, slot, pin)(同为 io_apic.c 中导出符号)查询 PCI 设备到 Linux IRQ 号的映射;该函数遍历 MP 表 IRQ source 条目并按 srcbusirq 中的设备号/INT 引脚匹配,找不到精确匹配时还保留"除 pin 外全部匹配"的模糊结果以容忍损坏的 MP 表——这与文档中"broken mptables"的容忍策略一脉相承。
相关启动参数小结
| 参数 | 作用 | 适用前提 |
|---|---|---|
pirq=N,N,...(最多 8 个) | MP 表损坏时手工将 PIRQ0-7 重定向到指定 CPU 侧 IRQ;0 为占位符 | 仅 CONFIG_X86_32(32 位 x86 内核) |
noapic | 完全禁用 IO-APIC 支持(early_param,置ioapic_is_disabled) | 所有 x86 内核 |
排查顺序建议:先用/proc/interrupts确认中断是否由IO-APIC-*投递;若设备无中断计数再怀疑 MP 表;最后才考虑pirq=试错——每条错误的 pirq 行都可能直接挂起启动,务必逐槽位推导。
【免费下载链接】linuxLinux kernel source tree项目地址: https://gitcode.com/GitHub_Trending/li/linux
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考