:中断与异常的分类 — 从触发源到 IDT 门)
〇、全景CPU 的打断分三类CPU 正在执行指令突然被打断去处理别的事。这个打断从哪里来决定了它是什么来源同步/异步叫什么例子CPU自己执行指令出错同步异常exception除零、缺页、段错误外部设备发来信号异步中断interrupt网卡收包、时钟 tick、磁盘完成软件主动执行INT n同步软中断software interruptint3断点、int 0x80系统调用一个常见的混淆先说清中文「中断」有广义和狭义两层——广义指所有打断 CPU 的事件含异常狭义只指外部设备发的异步中断。本文用狭义把异常和中断分开讲。CPU 被打断① 异常同步执行指令出错② 外部中断异步设备发信号③ 软中断同步INT n 指令fault / trap / abort三种处理方式INTx 引脚 vs MSI 消息边沿触发 vs 电平触发NMI vs 可屏蔽中断x86 承载vector 号分区0-31 异常 / 32-255 中断IDT 门决定进入时关中断 / 权限 / 栈一句话主线先按触发源分三类异常/中断/软中断再按处理方式细分fault/trap/abort、边沿/电平、NMI/可屏蔽最后 x86 用 vector 号 IDT 门把分类落到硬件。一、三大类异常 / 中断 / 软中断1.1 异常exceptionCPU 自己出错同步CPU 执行某条指令时发现这条指令执行不了——除零、访问不存在的内存、遇到非法操作码——当场停下来报告。特点是同步、精确异常和引发它的指令一一对应CPU 知道是哪条指令、哪个地址出的错。1.2 中断interrupt设备异步到达外部设备网卡、磁盘、时钟要 CPU 注意它通过 local APIC 发一个信号。特点是异步设备什么时候发CPU 完全不知道和 CPU 正在执行的指令无关。1.3 软中断software interrupt软件主动INT n程序或内核主动执行INT n指令触发比如调试器的int3断点、老的int 0x80系统调用。和异常一样是同步的由指令触发但错不是 CPU 发现的是软件主动请求的。为什么这样分同步异常/软中断和异步中断的处理方式完全不同——同步事件在指令边界、上下文精确、可重试异步事件随时到达、必须保存完整现场、处理完再恢复被打断的指令流。这一刀切下去后续所有处理逻辑都跟着分叉。二、异常的三类fault / trap / abort异常不是铁板一块按能不能恢复、恢复点在哪再分三类类型保存的 RIP 指向能否重试典型例子fault故障引发异常的那条指令✅ 处理完可重执行缺页page faulttrap陷阱引发异常的下一条指令✅ 继续往下执行断点int3、调试abort中止不精确❌ 通常无法恢复机器检查、double faultfault 最关键缺页是最典型的 fault——CPU 访问一个还没映射的地址触发#PF内核把页映射好重新执行刚才那条指令就像没发生过一样。这也是按需分配内存demand paging能成立的底层原因。abort 最危险机器检查Machine Check这种硬件级错误CPU 无法告诉你精确的出错指令程序状态已经不可信只能终止。对应到 x86 的 vector 号arch/x86/include/asm/trapnr.h#defineX86_TRAP_DE0/* Divide-by-zero */#defineX86_TRAP_NMI2/* Non-maskable Interrupt */#defineX86_TRAP_BP3/* Breakpoint */#defineX86_TRAP_DF8/* Double Fault */#defineX86_TRAP_GP13/* General Protection Fault */#defineX86_TRAP_PF14/* Page Fault */#defineX86_TRAP_MC18/* Machine Check */三、外部中断三种切法外部中断再往下有三种正交的分类维度3.1 按传递方式INTx 引脚 vs MSI 消息INTxMSI / MSI-X机制4 根物理引脚INTA#~INTD#电平设备写内存地址消息共享多设备共享一条线要逐个查独立无需共享触发电平触发边沿触发类似现状遗留PCIe 上靠 INTx 模拟现代默认MSI/MSI-X 的细节见 PCIe 系列 #4这里只取「分类」维度。3.2 按触发方式边沿触发 vs 电平触发边沿触发电平触发信号从 0→1 的跳变保持高/低电平中断源跳变那一刻触发一次电平持续期间一直触发丢失风险处理期间再跳变可能丢失处理前一直保持不会丢共享风险适合独享多个共享会互相干扰典型MSI、APIC timerINTx低电平有效为什么 MSI 是边沿触发MSI 是写一次内存地址写动作本身就是个瞬间跳变天然是边沿语义——这也是为什么 MSI 中断不共享、也共享不了。3.3 按能否屏蔽NMI vs 可屏蔽中断可屏蔽中断NMI不可屏蔽受IF位控制✅cli清零 IF 就屏蔽❌ 无视 IF用途普通设备中断严重事件硬件错误、看门狗、调试嵌套可被更高优先级打断一次 NMI 后屏蔽后续 NMI直到iretNMI 的 vector 号是 2NMI_VECTOR 0x02它不受IF控制所以内核用 NMI 做最后一道防线——比如硬锁死检测hardlockup detector靠 NMI 在看门狗超时后强制打断。四、x86 承载分类的第一层vector 号分区上面所有分类最终都要落到一个数字上——CPU 用这个数字去 IDT 里查处理函数。这个数字叫vector向量号0~255 共 256 个NR_VECTORS 256irq_vectors.h:102。x86 把 256 个 vector 分了区arch/x86/include/asm/irq_vectors.hvector 号分区256 个 ┌──────────────┬────────────────────────┬──────────────────────┐ │ 0 ~ 31 │ 32 ~ 235 │ 236 ~ 255 │ │ 异常/陷阱 │ 外部中断 │ 系统专用 │ │ 硬编码 │ 设备中断 syscall │ IPI / 本地定时器 / │ │ │ 0x80int80 │ reschedule / 错误上报 │ │ │ 0x30-0x3f 是 ISA 中断 │ │ └──────────────┴────────────────────────┴──────────────────────┘ 0x20 FIRST_EXTERNAL_VECTOR 0xec FIRST_SYSTEM_VECTOR 外部中断从这里开始 系统专用从这里开始关键常量// arch/x86/include/asm/irq_vectors.h (v6.6)#defineNMI_VECTOR0x02// line 30#defineFIRST_EXTERNAL_VECTOR0x20// line 36 外部中断起始#defineIA32_SYSCALL_VECTOR0x80// line 38 老 int80 系统调用#defineLOCAL_TIMER_VECTOR0xec// line 100 本地 APIC 定时器#defineFIRST_SYSTEM_VECTORLOCAL_TIMER_VECTOR// line 105#defineNR_VECTORS256// line 1020~31 是硬编码的异常trapnr.h里那批32 起FIRST_EXTERNAL_VECTOR是外部中断设备中断 128 号的 syscall236 起FIRST_SYSTEM_VECTORLOCAL_TIMER_VECTOR是系统专用IPI、本地定时器、调度、错误上报等。这一刀就是「异常 vs 中断」在硬件上的体现0~31 归 CPU 自己32 归外部世界。五、x86 承载分类的第二层IDT 门类型CPU 拿到 vector 号去IDT中断描述符表查第 vector 项每项是一个8 字节的门描述符gate里面写着处理函数的地址、权限、以及门类型。5.1 三种门类型硬件定义// arch/x86/include/asm/desc_defs.h (v6.6, line 40-45)enum{GATE_INTERRUPT0xE,// 中断门GATE_TRAP0xF,// 陷阱门GATE_CALL0xC,// 调用门GATE_TASK0x5,// 任务门};门类型决定进入处理函数时 CPU 的硬件动作核心区别在是否自动关中断清 IF门类型进入时清 IF关中断用途中断门interrupt gate✅ 关常规中断/异常处理陷阱门trap gate❌ 不关允许中断嵌套如调试任务门task gate切换 TSS老式已被 IST 取代5.2 关键洞察Linux v6.6 几乎只用中断门这是很多人包括不少博客搞错的地方。看 v6.6 实际建 IDT 的宏arch/x86/kernel/idt.c// arch/x86/kernel/idt.c (v6.6, line 32-52)/* Interrupt gate */#defineINTG(_vector,_addr)\G(_vector,_addr,DEFAULT_STACK,GATE_INTERRUPT,DPL0,__KERNEL_CS)/* System interrupt gate用户态可访问 */#defineSYSG(_vector,_addr)\G(_vector,_addr,DEFAULT_STACK,GATE_INTERRUPT,DPL3,__KERNEL_CS)/* Interrupt gate with interrupt stack */#defineISTG(_vector,_addr,_ist)\G(_vector,_addr,_ist1,GATE_INTERRUPT,DPL0,__KERNEL_CS)/* Task gate */#defineTSKG(_vector,_gdt)\G(_vector,NULL,DEFAULT_STACK,GATE_TASK,DPL0,_gdt3)四种宏里INTG/SYSG/ISTG 全都是GATE_INTERRUPT只有 TSKG 是GATE_TASK。看 def_idts 表里 double fault 那一项按位数分叉idt.c:99-103#ifdefCONFIG_X86_32TSKG(X86_TRAP_DF,GDT_ENTRY_DOUBLEFAULT_TSS),// 32 位任务门#elseISTG(X86_TRAP_DF,asm_exc_double_fault,IST_INDEX_DF),// 64 位中断门 独立栈#endif32 位内核double fault 用任务门切到专用 TSS避免栈坏了没法处理。64 位内核完全不用任务门double fault 改用中断门 IST 独立栈ISTG。GATE_TRAP陷阱门则只出现在 boot 压缩内核的临时 IDTarch/x86/boot/compressed/idt_64.c:16正式主内核的 IDT 不用。所以64 位 v6.6 的真相是几乎所有门都是中断门。区分「异常 vs 中断」「用户态 vs 内核态」靠的不是门类型而是vector 号0~31 是异常、32 是中断第四节DPLDPL0只有内核能触发DPL3用户态也能如int3断点、int80syscall 用 SYSGISTNMI、double fault、machine check 用独立栈ISTG防止栈溢出时无法处理这一节的要点是别再把「陷阱门」当成 Linux 区分异常的方式——那是 Intel 手册的视角Linux 实际实现里门类型几乎不分分的是 vector 号 DPL IST。六、从声明到触发DECLARE_IDTENTRY → handler 定义 → CPU 触发前面讲的是「分类」和「承载」还差最后一环门描述符里的地址最终跳到哪个函数那个函数在哪定义的CPU 怎么触发它6.1 一个宏声明三样东西DECLARE_IDTENTRY// arch/x86/include/asm/idtentry.h (v6.6, line 31-34)#defineDECLARE_IDTENTRY(vector,func)\asmlinkagevoidasm_##func(void);\asmlinkagevoidxen_asm_##func(void);\__visiblevoidfunc(structpt_regs*regs)DECLARE_IDTENTRY(X86_TRAP_DE, exc_divide_error)idtentry.h:548展开成三个声明声明是什么asm_exc_divide_error()汇编入口IDT 门描述符里填的地址idt.c:84 引用它xen_asm_exc_divide_error()Xen 虚拟化变体exc_divide_error(struct pt_regs *regs)C 处理函数关键点门描述符指向的是asm_*汇编入口不是 C 函数——asm 入口先保存寄存器、切内核栈再调 C 函数。6.2 同一个宏汇编里是另一种展开DECLARE_IDTENTRY在 idtentry.h 里有两份定义靠#ifndef __ASSEMBLY__区分// arch/x86/include/asm/idtentry.h (v6.6, line 429-430)#else/* !__ASSEMBLY__ */#defineDECLARE_IDTENTRY(vector,func)\idtentry vector asm_##func func has_error_code0entry_64.S 末尾#include asm/idtentry.hentry_64.S:614所以汇编器看到的 DECLARE_IDTENTRY 是另一份——它展开成idtentry汇编宏的调用真正生成asm_exc_divide_error那段汇编入口; arch/x86/entry/entry_64.S (v6.6, line 383-415) .macro idtentry vector asmsym cfunc has_error_code:req SYM_CODE_START(\asmsym) ; 生成 asm_exc_* 符号 ...保存现场、切内核栈... idtentry_body \cfunc ... ; 里面 call C 函数 SYM_CODE_END(\asmsym)一个宏名两份展开C 编译器拿到「函数声明」汇编器拿到「生成入口 stub 的指令」——这就是asm_exc_*汇编入口和exc_*C 函数怎么配对起来的。6.3 定义 C handlerDEFINE_IDTENTRY// arch/x86/include/asm/idtentry.h (v6.6, line 49-62)#defineDEFINE_IDTENTRY(func)\static__always_inlinevoid__##func(structpt_regs*regs);\\__visible noinstrvoidfunc(structpt_regs*regs)\{\irqentry_state_tstateirqentry_enter(regs);\instrumentation_begin();\__##func(regs);\instrumentation_end();\irqentry_exit(regs,state);\}\\static__always_inlinevoid__##func(structpt_regs*regs)DEFINE_IDTENTRY(exc_divide_error)定义 C handler 的骨架进入先irqentry_enter()关抢占、记状态等公共准备退出前irqentry_exit()公共收尾真正的处理逻辑写在__exc_divide_error里。具体到除零traps.c:193// arch/x86/kernel/traps.c (v6.6, line 193-197)DEFINE_IDTENTRY(exc_divide_error){do_error_trap(regs,0,divide error,X86_TRAP_DE,SIGFPE,FPE_INTDIV,error_get_trap_addr(regs));}do_error_trap就是给当前进程发SIGFPE信号——除零错误最终变成「进程收到一个浮点异常信号」。6.4 触发链路从出错到 handlerCPU 执行 div 指令除数为 0 → 硬件产生 vector 0#DE → 读 IDT[0] 的门描述符指向 asm_exc_divide_erroridt.c:84 装的 → 硬件压栈 RFLAGS/CS/RIPRIP 最后压、在栈顶 → 跳转到 asm_exc_divide_erroridtentry 宏生成的汇编入口 ├─ 保存通用寄存器到 pt_regs ├─ 切到内核栈 └─ call exc_divide_error(regs) ├─ irqentry_enter() ├─ do_error_trap() → 发 SIGFPE 给当前进程 ├─ irqentry_exit() └─ 返回 → 恢复寄存器iret 返回fault 重执行指令trap 跳下一条这就是「分类 → vector 号 → 门描述符 → asm 入口 → C handler」的完整落地链也是第二节 fault/trap/abort 区别的物理基础iret 返回的 RIP 指向哪由 handler 决定fault 重执行、trap 跳过。七、对比总表一图记住所有分类维度类别 A类别 B类别 C触发源异常CPU 同步中断设备异步软中断INT n异常恢复fault可重试trap指令后abort不可恢复中断传递INTx引脚MSI消息—触发方式边沿跳变触发电平持续触发—屏蔽性可屏蔽受 IFNMI无视 IF—八、函数索引什么情况下调用函数/宏位置何时调用作用idt_setup_trapsidt.c:225启动早期装异常门def_idts 表idt_setup_apic_and_irq_gatesidt.c:274启动后期装 APIC/IPI 门idt_setup_early_trapsidt.c:215最早装早期异常门load_idtset_intr_gateidt.c:199上面内部装单个中断门init_idt_datadesc.h:407上面内部填充 idt_data默认 GATE_INTERRUPTINTG/SYSG/ISTGidt.c:32-47建表生成 idt_data中断门 DPL/ISTDECLARE_IDTENTRYidtentry.h编译期声明异常处理入口九、完整调用链函数级助记【启动时建 IDT】start_kernel() └─ trap_init() traps.c:1371 └─ idt_setup_traps() idt.c:225 装异常门 └─ idt_setup_from_table() idt.c:187 遍历 def_idts 表 ├─ idt_init_desc() desc.h:420 填 gate_descoffset/segment/bits └─ write_idt_entry() 写进 idt_table[vector] └─ init_IRQ() → native_init_IRQ() irqinit.c:94 └─ idt_setup_apic_and_irq_gates() idt.c:274 装设备/IPI 门 【运行时触发】预览详见 #3 投递篇 设备发中断/CPU 出错 → 查 IDT[vector] → 读门描述符 → 中断门清 IF关中断→ 跳 handler分界点「分类」是静态的vector 号 门类型「建表」是启动时一次性写 IDT「触发」是运行时 CPU 查表——三条线本文讲前两条分类 承载触发留给 #3。十、记住三点① 先按触发源分三类异常CPU 同步出错、中断设备异步、软中断INT n 主动触发。同步 vs 异步是后续所有处理逻辑分叉的根。② 异常再分三类fault可重试如缺页、trap指令后如断点、abort不可恢复如机器检查。③ x86 用三层落地vector 号分区0-31 异常 / 32 中断→ IDT 门64 位几乎全用中断门靠 DPL 区分权限、IST 区分栈不是靠 trap gate→ DECLARE_IDTENTRY 声明的 asm 入口 C handlerCPU 触发时查 IDT →asm_*→exc_*。十一、关键文件索引文件v6.6关键内容arch/x86/include/asm/trapnr.h异常 vector 号X86_TRAP_*arch/x86/include/asm/irq_vectors.hvector 分区、NR_VECTORS、各特殊 vectorarch/x86/include/asm/desc_defs.h门类型GATE_INTERRUPT/TRAP/TASK、gate_struct 位域arch/x86/include/asm/desc.hinit_idt_data、idt_init_descarch/x86/kernel/idt.cINTG/SYSG/ISTG/TSKG 宏、def_idts 表、idt_setup_*arch/x86/include/asm/idtentry.hDECLARE_IDTENTRY 声明、异常处理入口