【免费下载链接】linux-insides-zh
Linux 内核揭秘
导读:本文是《Linux 内核揭秘》(linux-insides-zh)中"内核数据结构(KernelStructures)"章节的完整技术指南,聚焦 Linux 内核在 x86/x86_64 架构下的内部系统数据结构,尤其是中断描述符表(Interrupt Descriptor Table,IDT)与全局描述符表(Global Descriptor Table,GDT)。读完本文,你将掌握中断与异常的来源分类、中断向量编号体系、处理器错误代码(Error Code)的两种格式、三种门描述符的位布局,以及 Linux 内核中与之对应的 C 结构体表示(
gate_struct64、ldttss_desc64),并能沿着仓库中的相关章节继续深入内核中断子系统的实现细节。
什么是内核"内部系统数据结构"
在《Linux 内核揭秘》这本书中,大多数章节沿着内核启动、初始化和子系统实现的时间线展开叙述。但 KernelStructures/README.md 所对应的是一个特殊的专题章节:它不再以"某个阶段的初始化流程"为主线,而是聚焦于 Linux 内核中面向体系结构(architecture-specific)的内部系统数据结构,例如:
- 中断描述符表(IDT):保存中断与异常处理程序入口指针的表格,CPU 依据中断向量号在表中查找对应的门(Gate);
- 全局描述符表(GDT):保存段描述符的系统表,用于描述内核代码段、数据段等内存段的基址、界限与访问权限。
这类数据结构的位布局、语义与行为,绝大部分由处理器厂商的官方手册定义。本仓库的这一章节也明确指出,其内容主要来源于 Intel 与 AMD 的软件开发者手册(Software Developer Manuals)——换句话说,读懂这些结构,就拿到了理解内核在 x86 平台上"如何处理中断"的钥匙。
在 SUMMARY.md 的目录结构中,该章节位于"理论"(Theory)与"杂项"(Misc)之间,其下目前包含一篇正文:
- KernelStructures/linux-kernelstructure-1.md —— 中断描述符表(IDT)
以下内容即以这篇正文为主体展开。
中断与异常的来源:同步与异步
内核要处理的事件来源共有三类,它们决定了 CPU 进入处理程序的时机与方式:
| 来源 | 同步/异步 | 说明 |
|---|---|---|
| 异常(Exception) | 同步 | 由 CPU 在执行指令时检测到错误产生,与当前指令流同步 |
| 软中断(Software Interrupt) | 同步 | 由软件显式触发,通常用于系统调用等场景 |
| 外部中断(External Interrupt) | 异步 | 由外部硬件设备通过中断线或 APIC 异步触发 |
其中异常根据报告时机又可细分为三种类型:
- 故障(Fault):在导致异常的指令执行之前被准确报告,
%rip保存的指针指向那条引发故障的指令; - 陷阱(Trap):在导致异常的指令执行之后被准确报告,
%rip保存的指针同样指向引发陷阱的指令; - 终止(Abort):不明确的异常,由于无法精确定位引发异常的指令,系统通常不允许程序可靠地再次启动。
理解三者的区别很重要:故障(如缺页#PF)在修正条件后允许被中断的程序继续执行,陷阱(如断点#BP)也允许继续,而终止(如双重错误#DF)通常意味着不可恢复。
可屏蔽与不可屏蔽中断
处理器通过标志寄存器中的RFLAGS.IF位控制普通中断的递送:
- 可屏蔽中断(Maskable Interrupt):仅当
RFLAGS.IF = 1时,可屏蔽中断才会触发中断处理程序;若该位被清零,它们将持续处于等待处理状态。 - 不可屏蔽中断(Non-Maskable Interrupt,NMI):不受
RFLAGS.IF位的影响。无论怎样,一个 NMI 的发生都会进一步屏蔽之后的其他 NMI,直到执行IRET(中断返回)指令为止。
在 x86_64 上,控制IF位的典型指令是sti(置位)与cli(清除),Linux 内核通过native_irq_enable/native_irq_disable内联函数封装它们。NMI 与普通外部中断在 Linux 内核中的处理路径差异极大——NMI 处理程序拥有独立入口(nmi)并搭配专用的中断堆栈表(IST)条目,这一点在本文"中断堆栈表(IST)"一节展开。
中断向量:0 到 255 的编号体系
处理器为每一种具体的异常和中断来源分配固定的向量标识号(vector number,简称向量)。中断处理程序使用该向量定位异常或中断,从而分发到对应的系统软件服务处理程序。可用的中断向量最多256 个,其中前 32 个被保留,用于预定义的异常和中断条件。
Linux 内核在设置中断门时会校验向量号的合法性:
BUG_ON((unsigned)n > 0xFF);这段检查可在set_intr_gate等中断设置路径中找到(参见 Initialization/linux-initialization-2.md 对arch/x86/include/asm/desc.h的剖析)。前 32 个保留向量在arch/x86/include/asm/traps.h中以枚举常量定义,内核源码对应的定义如下:
/* 中断/异常 */ enum { X86_TRAP_DE = 0, /* 0, 除零错误 */ X86_TRAP_DB, /* 1, 调试 */ X86_TRAP_NMI, /* 2, 不可屏蔽中断 */ X86_TRAP_BP, /* 3, 断点 */ X86_TRAP_OF, /* 4, 溢出 */ X86_TRAP_BR, /* 5, 超出范围 */ X86_TRAP_UD, /* 6, 操作码无效 */ X86_TRAP_NM, /* 7, 设备不可用 */ X86_TRAP_DF, /* 8, 双精度浮点错误 */ X86_TRAP_OLD_MF, /* 9, 协处理器段溢出 */ X86_TRAP_TS, /* 10, 无效的 TSS */ X86_TRAP_NP, /* 11, 段不存在 */ X86_TRAP_SS, /* 12, 堆栈段故障 */ X86_TRAP_GP, /* 13, 一般保护故障 */ X86_TRAP_PF, /* 14, 页错误 */ X86_TRAP_SPURIOUS, /* 15, 伪中断 */ X86_TRAP_MF, /* 16, x87 浮点异常 */ X86_TRAP_AC, /* 17, 对齐检查 */ X86_TRAP_MC, /* 18, 机器检测 */ X86_TRAP_XF, /* 19, SIMD (单指令多数据结构浮点)异常 */ X86_TRAP_IRET = 32, /* 32, IRET (中断返回)异常 */ };仓库的 Initialization/linux-initialization-2.md 还给出了一张更完整的0-31号异常速查表,包含每个向量的助记符(如#DE、#DF、#GP、#PF)、类型(Fault/Trap/Abort)、是否携带错误代码以及典型产生来源,是阅读本节的极佳配套资料。向量32到255则设计为用户定义中断,通常分配给外部 I/O 设备使用。
错误代码(Error Code)格式
处理器异常处理程序使用错误代码报告某些异常的错误和状态信息。在把控制权交给异常处理程序期间,异常处理装置会将错误代码推送到堆栈中。错误代码有两种格式:
- 多数异常错误报告格式(选择子错误代码);
- 页错误格式。
选择子错误代码格式
31 16 15 3 2 1 0 +-------------------------------------------------------------------------------+ | | | T | I | E | | Reserved | Selector Index | - | D | X | | | | I | T | T | +-------------------------------------------------------------------------------+各字段含义如下:
EXT:若该位为 1,异常源在处理器外部;若为 0,异常源位于处理器内部;IDT:若该位为 1,错误代码的选择子索引字段引用的是"中断描述符表"中的门描述符;若为 0,选择子索引字段引用的是"全局描述符表(GDT)"或"本地描述符表(LDT)"中的描述符,具体由TI位指示;TI:若该位为 1,选择子索引字段引用 LDT 中的描述符;若为 0,选择子索引字段引用 GDT 中的描述符;Selector Index:选择子索引字段指定索引是 GDT、LDT 还是 IDT,由IDT与TI位共同指定。
页错误代码格式
31 4 3 2 1 0 +-------------------------------------------------------------------------------+ | | | R | U | R | - | | Reserved | I/D | S | - | - | P | | | | V | S | W | - | +-------------------------------------------------------------------------------+各字段含义如下:
I/D:若该位为 1,造成页错误的访问是取指(instruction fetch);RSV:若该位为 1,页错误是处理器从保留给分页表的区域中读取的结果;U/S:若该位为 0,是管理员模式(CPL = 0、1 或 2)的访问导致页错误;若为 1,是用户模式(CPL = 3)的访问导致页错误;R/W:若该位为 0,导致页错误的是内存读取;若为 1,导致页错误的是内存写入;P:若该位为 0,页错误是由不存在的页面引起的;若为 1,页错误是由于违反页保护引起的。
在 Linux 内核的早期启动阶段,#PF(向量 14)是最重要的异常之一:内核初始化早期通过early_idt_handler_common检查栈上的向量号是否为14,若是则读取cr2寄存器中的故障地址并调用early_make_pgtable动态建立页表。完整的处理逻辑分析见 Initialization/linux-initialization-2.md 的"缺页中断处理程序"一节。
中断控制传输:三种门描述符
IDT 可以包含三种门描述符中的任何一种:
- 任务门(Task Gate):包含用于异常与/或中断处理程序任务的 TSS 段选择子;
- 中断门(Interrupt Gate):包含处理器用于将程序执行转移到中断处理程序的段选择子和偏移量;
- 陷阱门(Trap Gate):包含处理器用于将程序执行转移到异常处理程序的段选择子和偏移量。
中断门与陷阱门的唯一区别在于 CPU 对IF标志的处理方式:经由中断门进入处理程序时,CPU 会清除IF标志位,从而在当前中断处理程序执行期间屏蔽其他中断;而经由陷阱门进入时不会自动屏蔽。中断和陷阱门都包含一个指向处理程序的远指针(far pointer)。
门描述符的通用位布局(x86_64,16 字节)如下:
127 96 +-------------------------------------------------------------------------------+ | | | Reserved | | | +-------------------------------------------------------------------------------- 95 64 +-------------------------------------------------------------------------------+ | | | Offset 63..32 | | | +-------------------------------------------------------------------------------+ 63 48 47 46 44 42 39 34 32 +-------------------------------------------------------------------------------+ | | | D | | | | | | | | Offset 31..16 | P | P | 0 |Type |0 0 0 | 0 | 0 | IST | | | | L | | | | | | | -------------------------------------------------------------------------------+ 31 16 15 0 +-------------------------------------------------------------------------------+ | | | | Segment Selector | Offset 15..0 | | | | +-------------------------------------------------------------------------------+各字段说明如下:
Selector:目标代码段的段选择子;Offset:处理程序入口点的偏移量(在 64 位模式下被拆成三段:Offset 15..0、Offset 31..16、Offset 63..32);DPL:描述符权限级别(Descriptor Privilege Level);P:当前段存在标志(Present);IST:中断堆栈表(Interrupt Stack Table)索引;TYPE:描述符类型,可为 LDT 段描述符、TSS 描述符、调用门描述符、中断门描述符、陷阱门描述符或任务门描述符之一。
IDT 的表项在 x86 上为 8 字节门,在 x86_64 上为 16 字节门。与 GDT 强制要求第一个元素为 NULL 描述符不同,IDT 的第一个元素可以直接是一个门(例如早期引导阶段就曾用 NULL 门加载 IDT)。IDT 可以位于线性地址空间的任意位置,只需在 x86 上按 8 字节对齐、在 x86_64 上按 16 字节对齐即可。其基址保存在专门的IDTR 寄存器中,由两条指令协同维护:
LIDT:加载 IDT 基址到 IDTR;SIDT:将 IDTR 内容读出存储到指定操作数。
IDTR 在 x86 上是 48 位,由 16 位 Limit 与 32 位 Base 组成(x86_64 下 Base 扩展为 64 位)。引导阶段的setup_idt就通过lidtl指令加载了一个null_idt来临时禁用中断,详见 Interrupts/linux-interrupts-1.md。
Linux 内核中的门描述符结构体
IDT在 Linux 内核中由gate_desc数组描述(extern gate_desc idt_table[];),在 x86_64 架构下gate_desc即struct gate_struct64(定义于arch/x86/include/asm/desc_defs.h):
struct gate_struct64 { u16 offset_low; u16 segment; unsigned ist : 3, zero0 : 5, type : 5, dpl : 2, p : 1; u16 offset_middle; u32 offset_high; u32 zero1; } __attribute__((packed));这个结构体与上文的门位布局一一对应:offset_low/offset_middle/offset_high分别承载处理程序入口偏移量的低 16 位、中 16 位与高 32 位;segment承载段选择子(在内核中通常为__KERNEL_CS);位域ist、type、dpl、p对应 IST 索引、门类型、描述符权限级与存在标志。内核使用pack_gate与PTR_LOW/PTR_MIDDLE/PTR_HIGH宏将处理函数地址拆分填入三个偏移字段,再通过_set_gate/set_intr_gate写入idt_table,这段写入逻辑在 Initialization/linux-initialization-2.md 中有逐步讲解。
任务门描述符不包含IST字段,其格式与中断/陷阱门不同,内核中对应的结构体为struct ldttss_desc64:
struct ldttss_desc64 { u16 limit0; u16 base0; unsigned base1 : 8, type : 5, dpl : 2, p : 1; unsigned limit1 : 4, zero0 : 3, g : 1, base2 : 8; u32 base3; u32 zero1; } __attribute__((packed));值得注意的是,在 x86_64 长模式下,只有中断门和陷阱门可以被引用,任务门不再用于实际的任务切换。gate_struct64与ldttss_desc64这两个结构体,正是"内核内部系统数据结构"这一章节主题最直接的体现——它们是硬件手册中的位布局在 Linux 内核 C 代码中的精确映射。
任务切换期间的异常
任务切换在加载段选择子期间可能会发生异常,页错误也可能在访问 TSS 时出现。在这些情况下,由硬件任务切换机构先从 TSS 加载新的任务状态,然后再触发适当的异常处理。
但需要注意:在长模式下,由于硬件任务切换机构被禁用,因而在任务切换期间不会发生这类异常。这也解释了为什么 x86_64 上任务门不再扮演实际切换角色的原因——长模式下的任务切换与栈切换由软件(内核)配合 TSS 中的 IST 指针完成。
中断堆栈表(IST)与 NMI:从"未完待续"到仓库中的实现
KernelStructures/linux-kernelstructure-1.md 末尾将"不可屏蔽中断(Nonmaskable interrupt)"、"API"、"中断堆栈表(Interrupt Stack Table)"三个小节标注为未完待续。要补全这些主题,仓库的 Interrupts 章节提供了完整的实现级讲解,这里给出要点式衔接:
- **中断堆栈表(IST)**是 x86_64 新增的机制,用于取代传统的栈切换模式:在使能后可无条件地切换栈,并且可以被任何与确定中断关联的 IDT 条目启用。TSS 中提供7 个 IST 指针,内核为双重错误、NMI、调试异常、机器检测等保留专用栈,例如
DOUBLEFAULT_STACK 1、NMI_STACK 2、DEBUG_STACK 3、MCE_STACK 4。使用 IST 的门描述符由set_intr_gate_ist初始化,如set_intr_gate_ist(X86_TRAP_NMI, &nmi, NMI_STACK)(见 Interrupts/linux-interrupts-1.md 与 Interrupts/linux-interrupts-6.md)。 - NMI 处理是内核中最"小心谨慎"的路径之一:由于嵌套 NMI 会破坏前一个 NMI 的栈帧,内核在栈上保存"复制栈帧"并维护"NMI 执行中"标志变量,配合
paranoid_entry检查MSR_GS_BASE、必要时执行swapgs,并在调用do_nmi前后保护cr2寄存器。完整流程见 Interrupts/linux-interrupts-6.md。 - 每个 CPU 还拥有 per-cpu 的中断栈(
irq_stack_union,默认 16KB),其入口在引导早期通过写MSR_GS_BASE建立gs基址,这部分细节同样收录于 Interrupts/linux-interrupts-1.md。
在仓库中继续深入
本专题作为内核"系统数据结构"的入口,与仓库中多个章节形成阅读闭环:
- 想了解引导阶段如何临时加载 IDT 与 GDT:阅读 Booting/linux-bootstrap-2.md;
- 想了解早期异常处理程序(
early_idt_handler_array、set_intr_gate、缺页处理):阅读 Initialization/linux-initialization-2.md; - 想系统学习中断与异常处理的全貌(PIC/APIC、中断优先级、IST、softirq 等):进入 Interrupts/README.md 从 Interrupts/linux-interrupts-1.md 开始。
掌握 IDT 与 GDT 这两张"系统表格"的位级语义,是理解内核中断子系统、异常处理与栈切换机制的必备前提——这正是本仓库设置"内核数据结构"专题的初衷。
【免费下载链接】linux-insides-zh
Linux 内核揭秘
相关推荐
Linux 内核揭秘:中断描述符表(IDT)——门描述符、错误代码与 x86_64 中断处理机制全解
Linux 内核揭秘:中断描述符表(IDT)——门描述符、错误代码与 x86_64 中断处理机制全解 中断描述符表(Interrupt Descriptor T
Linux内核揭秘:文件描述符管理的终极指南
Linux内核揭秘:文件描述符管理的终极指南 在Linux系统中,文件描述符是连接用户程序与内核资源的重要桥梁。无论是打开文件、创建管道还是建立网络连接,都离不
文档教程操作系统10 篇深度剖析:Linux 内核揭秘(linux-insides-zh)中断、异常、IDT 与 Softirq 处理机制全景图解
10 篇深度剖析:Linux 内核揭秘(linux insides zh)中断、异常、IDT 与 Softirq 处理机制全景图解 文章导语 按一次键盘按键,系
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考