9.9 ACPI 规范与高级中断控制
在早期个人计算机架构中,底层硬件的设备枚举、即插即用以及电源节能状态(如 APM 规范)主要受控于主板 BIOS 固件的黑盒逻辑,操作系统难以精确获知物理拓扑,更无法协同调度外设功耗。
高级配置与电源管理接口(Advanced Configuration and Power Interface,简称 ACPI) 的确立,标志着计算机系统步入“操作系统导向的电源管理”(OS-directed Configuration and Power Management,简称 OSPM)时代。在 ACPI 体系下,固件退居为硬件拓扑与电源能力的静态描述者,通过将主板硬件参数编排为结构化数据表提供给内核;而真正的能耗调整、设备启停与全局电源决策,则完全交由操作系统内核根据负载动态掌控。
ACPI 覆盖了系统运行期的多维硬件交互需求:
- 层次化电源状态控制:统一协调全局系统睡眠状态(S0~S5)、设备功耗状态(D0~D3)以及处理器休眠与性能状态(C 状态与 P 状态);
- 动态热量与温控治理:监测处理器与主板温度传感器,按需阶梯式调节风扇转速或实行温度节流;
- 总线与即插即用枚举:标准化呈现嵌入式控制器(EC)、系统管理总线(SMBus)以及板载外设的资源占用;
- 平台拓扑与高级中断路由:在多核(SMP)环境下,ioapic 的开启以及 SMP 多处理器的开启也需要 ACPI 提供底层硬件描述。
1. ACPI 描述表体系结构
ACPI 的数据交换依托一套层次化的系统描述表(ACPI Tables)。固件在自举阶段完成硬件自检后,会将这些表格构建在物理内存中,供内核引导后查阅。
1.1 寻址根基:RSDP 结构指针
操作系统探查 ACPI 的起点是根系统描述指针(Root System Description Pointer,简称 RSDP):
- 在传统 Legacy BIOS 机器上,内核通过在
0xE0000至0xFFFFF物理内存区间内以 16 字节边界扫描包含"RSD PTR "签名的结构; - 在 UEFI 环境下,固件则直接在系统配置表(System Table)中明确传递 RSDP 的物理地址。
RSDP 内部存储了 8 字节签名、单字节累加校验和,以及指向下级主目录表的物理基地址。
1.2 核心系统描述表分类
依据 RSDP 中的指针,系统可以顺藤摸瓜检索到 RSDT(32 位物理指针目录表) 或 XSDT(64 位物理指针目录表)。主目录表中的每个条目均指向一个具备统一 36 字节标准表头的特定描述表:
- FADT(Fixed ACPI Description Table):固定 ACPI 描述表。记录了控制芯片的核心 I/O 端口与 MMIO 寄存器块(如系统控制中断 SCI 号、电源管理控制块
PM1a_CNT_BLK等),并内嵌了指向 DSDT 的指针,是实现系统软关机与睡眠的核心表; - MADT(Multiple APIC Description Table):多 APIC 描述表(签名为
APIC)。该表是操作系统启动多核处理器(SMP)以及配置现代中断路由的唯一基准,记录了全机 Local APIC、I/O APIC 以及中断源覆盖(Interrupt Source Override)的物理配置; - DSDT 与 SSDT(Differentiated / Secondary System Description Table):差分与次级系统描述表。与前述静态纯数据表不同,DSDT 与 SSDT 内部存放着经过编译的 ACPI 机器语言(AML)字节码。内核若要实现完整的即插即用设备树管理与高级温控事件响应,需内嵌 AML 字节码解释器来执行这些设备方法;
- 其他专用辅助表:如描述开机图形资源的 BGRT、描述非易失性内存的 NFIT 以及描述局部距离与亲和性的 SLIT/SRAT 表等。
2. APIC 体系与多处理器中断拓扑
高级可编程中断控制器(Advanced Programmable Interrupt Controller,简称 APIC)是替代老旧 8259A PIC 芯片组的现代中断架构,广泛应用于 Intel/AMD 平台。
APIC 架构由两类协同运转的核心部件构成:
- Local APIC(本地 APIC):内置于每一个 CPU 核心内部。负责接收本核的硬件定时器中断、向其他 CPU 核心投递核间中断(IPI,常用于跨核任务调度、TLB 射落刷表及多核自举唤醒),并响应来自外部中断控制器的信号分发;
- I/O APIC(输入输出 APIC):部署于主板芯片组上。其拥有多条可编程中断引脚,能够通过总线消息将外设硬件中断以可配置的优先级和亲和性,均匀且并发地分发到指定 CPU 核心的 Local APIC 上。
在单核运行时代,系统往往可以仅依赖 8259A 芯片完成基础的 IRQ 响应;但在多核对称多处理(SMP)系统下,8259A 的串行瓶颈使其无法适应核间通信与动态负载均衡。正是借助 ACPI MADT 表提供的硬件映射,内核才能在自举时精确探测出当前物理机安装的核心总数、提取每个核心的 APIC ID,进而将传统的外部 IRQ 引脚重定向至 I/O APIC 的全局系统中断(GSI)线上,构筑起现代化的高性能多核中断网络。
3. 习题
这一页把 ACPI 说成了一个框架:一组描述表、一套电源状态、以及靠 MADT 才能开起来的多处理器。正文没有给出任何一段可运行的代码,因为真正的 ACPI 解析要在引导之后读物理内存。下面四道题把这块补上:前两道在主机上遍历固件给出的表结构,后两道把表里的字段接到实际动作(对 IO APIC 编程、通过 PM1 寄存器关机)。整套固定数据在 code/项目/mdrOS/exercises/9_9/acpi_fixture.h,所有题目都用它,不用真机。
3.1 巩固
ACPI 的入口是 RSDP(Root System Description Pointer)。固件把它放在 0xE0000~0xFFFFF 之间的某处,每个 16 字节边界上都要查一遍。下面是从 0xE0000 起的 128 字节:
static const uint8_t rsdp_region[128] = {
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x52, 0x53, 0x44, 0x20, 0x50, 0x54, 0x52, 0x20,
0xC3, 0x44, 0x45, 0x43, 0x4F, 0x59, 0x20, 0x00, 0xAD, 0xDE, 0x00, 0x00,
0x00, 0x52, 0x53, 0x44, 0x20, 0x50, 0x54, 0x52, 0x20, 0x56, 0x4D, 0x44,
0x52, 0x54, 0x53, 0x54, 0x00, 0xEF, 0xBE, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x52, 0x53, 0x44, 0x20, 0x50, 0x54, 0x52, 0x20, 0x0E, 0x4D, 0x44, 0x52,
0x54, 0x53, 0x54, 0x00, 0x00, 0xF4, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
};2
3
4
5
6
7
8
9
10
11
12
13
(1) RSDP 的签名是 "RSD PTR "(末尾有一个空格,共 8 字节)。逐字节扫描这 128 个字节,哪几个偏移上出现了这个签名?把它们各自的 20 字节(从签名起)抄出来。
(2) RSDP 的第 8 字节是校验和:从签名开始的 20 个字节按 8 位相加必须为 0。对三个候选逐个算,哪几个通过、哪一个不通过?通过的两个里,RSDT 地址字段各是多少?
(3) 三个候选里有两个是故意放的干扰项。规范要求在 0xE0000~0xFFFFF 之间按 16 字节边界扫描,0x25 处的签名校验和是过的,却仍然不该被采用。说明扫描循环为什么要写 off += 16 而不是 off++。再指出下面三种实现各会选中哪个候选:只查签名、逐字节扫描;只查签名、按 16 字节步长;查签名与校验和、按 16 字节步长。
(4) 偏移 0x10 那个候选的 RSDT 地址字段是 0xDEAD,明显是无效值,但它的校验和也不对。如果只检查签名不检查校验和,会拿这个假地址去做什么?写出 32 位下这个地址落在哪一段物理内存(按 PC 的内存布局:低 640 KiB、0xA0000~0xFFFFF 是固件区、0xC0000000 起是 PCI 空间)。
验收:第 (2) 问的三个校验和依次是 0x01、0x00、0x00;第 (3) 问三种实现依次选中 0x10、0x10、0x60;第 (4) 问的 0xDEAD 落在低 640 KiB。
RSDT 是一张「表目录」:它的表头之后是一串 4 字节物理地址,每个指向另一张表。给定 RSDT 在 0x1F400,内容如下:
static const uint8_t rsdt_table[56] = {
0x52, 0x53, 0x44, 0x54, 0x38, 0x00, 0x00, 0x00, 0x01, 0x85, 0x4D, 0x44,
0x52, 0x54, 0x53, 0x54, 0x52, 0x53, 0x44, 0x54, 0x44, 0x45, 0x4D, 0x4F,
0x01, 0x00, 0x00, 0x00, 0x4D, 0x44, 0x52, 0x20, 0x01, 0x00, 0x00, 0x00,
0x00, 0xF0, 0x01, 0x00, 0x00, 0xF1, 0x01, 0x00, 0x00, 0xF2, 0x01, 0x00,
0x00, 0xF3, 0x01, 0x00, 0x00, 0xF5, 0x01, 0x00,
};2
3
4
5
6
7
五张表分别放在 0x1F000(FADT,116 字节)、0x1F100(MADT,108 字节)、0x1F200(DSDT,42 字节)、0x1F300(SSDT,36 字节)与 0x1F500(不存在)。所有表的表头布局相同:
| 偏移 | 长度 | 字段 |
|---|---|---|
| 0 | 4 | 签名 |
| 4 | 4 | 表长(含表头) |
| 8 | 1 | 修订号 |
| 9 | 1 | 校验和 |
| 10 | 6 | OEM ID |
| 16 | 8 | OEM 表 ID |
| 24 | 4 | OEM 修订号 |
| 28 | 4 | 创建者 ID |
| 32 | 4 | 创建者修订号 |
(1) RSDT 的表长字段是多少?表头占 36 字节,算出一共有几个表地址。把这五个地址写成十六进制。
(2) 打一份「表清单」:对每个地址读出 36 字节表头,打印签名、表长、修订号、OEM 表 ID 与校验和字段。0x1F500 应当报「未映射」,不要当成读到了 0。
(3) 校验一:把整张表(长度取自表长字段)的字节按 8 位求和,结果为 0 才有效。四张存在的表里哪一张通不过?它的校验和字段应改成多少才通过?
(4) 校验二:表长字段必须是 4 的倍数且不小于 36,否则后面按表长做的扫描会越界。写一个 table_check(addr, max_len),把「未映射」「表长非法」「校验和错」三种情况分别返回不同的错误码,并在 main 里逐表打印结论。
(5) 为什么 RSDT 里能出现一个指向未映射地址的项?固件写这张表时不会犯这种错,那这一项在这个习题里代表了什么真实情况的简化?
验收:第 (2) 问五行的签名依次是 FACP、APIC、DSDT、SSDT 与「未映射」;第 (3) 问指出 SSDT 的校验和应改成 0x95;第 (4) 问的三种错误码各不相同。
3.2 编程
解析 MADT,把它变成「这台机器有几个核、每个核的 APIC ID 是多少、中断怎么路由」。
MADT 在 0x1F100,108 字节。表头之后从偏移 44 开始是一串变长条目,每条的首字节是类型、次字节是长度(含这两字节):
| 类型 | 名称 | 关键字段 |
|---|---|---|
| 0 | Processor Local APIC | 偏移 2:ACPI 处理器 ID;偏移 3:APIC ID;偏移 4:4 字节标志,bit 0 为 1 表示启用 |
| 1 | I/O APIC | 偏移 2:I/O APIC ID;偏移 4:4 字节 MMIO 基址;偏移 8:4 字节 GSI 基址 |
| 2 | Interrupt Source Override | 偏移 2:总线;偏移 3:源 IRQ;偏移 4:4 字节 GSI;偏移 8:2 字节标志(bit 1 极性 0 高有效 / 1 低有效,bit 3 触发方式 0 边沿 / 1 电平) |
要求:
(1) 实现 madt_walk():从偏移 44 开始按长度字段前进,遇到长度小于 2 或越过表长就停下并报错。逐条打印类型与长度,验证条目边界是 44, 52, 60, 68, 76, 88, 98(最后一个在 0x62,长度 10)。
(2) 打印每个处理器的 ACPI ID、APIC ID 与启用状态。统计总核数与启用核数,以及最大的 APIC ID。
(3) 打印 I/O APIC 的 MMIO 基址与 GSI 基址,再打印两条中断源覆盖:源 IRQ 0 覆盖到哪个 GSI,源 IRQ 9 覆盖到哪个 GSI,各自的极性与触发方式。两条记录的标志字分别是 0x0000 和 0x000F,按上表的位定义解出结论。
(4) 写 gsi_of(irq):先查覆盖表,没有覆盖就返回 irq + gsi_base。对 IRQ 0、IRQ 2、IRQ 9、IRQ 11 各求一次,列出结果。
(5) 回答:为什么 ioapic 的开启以及 SMP 多处理器的开启也需要 ACPI(正文原话)?如果不用 MADT,内核能靠什么知道有几个核?从 cpuid、BIOS 数据区、写死的常量三条路里各举一个,并说明为什么 9.1 项目概述 的 7.3 节说不要用 cpuid 取多核代号。
验收:第 (1) 问的七个偏移与第 (2) 问的四个数字(4 核、2 启用、最大 APIC ID 5)全部吻合;第 (4) 问 IRQ 0 得到 GSI 2、IRQ 9 得到 GSI 9、IRQ 11 得到 GSI 11。
把 FADT 与 APIC 接到实际动作上,做两个能跑的实验。
实验一:对 I/O APIC 编程,把一条 GSI 接到指定向量。
(1) 从 FADT 读出 SCI_INT、PM1a_EVT_BLK、PM1a_CNT_BLK、PM_TMR_BLK、GPE0_BLK 与各自的长度字段,打印成一张表。字段偏移:SCI_INT 在 0x2E(2 字节)、PM1a_EVT_BLK 在 0x38、PM1a_CNT_BLK 在 0x40、PM_TMR_BLK 在 0x4C、GPE0_BLK 在 0x50,长度在 0x58、0x59、0x5B、0x5C。
(2) I/O APIC 的寄存器通过两个 MMIO 双字访问:偏移 0x00 写寄存器号,偏移 0x10 读写数据。中断重定向表项(redirection table entry,RTE)的寄存器号是 0x10 + 2 * gsi,低 32 位在偶数号、高 32 位在奇数号。低 32 位的 bit 0~7 是向量,bit 13 是极性(1 为低有效),bit 15 是触发方式(1 为电平),bit 16 是屏蔽位。按 MADT 的覆盖表,写出两个 RTE 的低 32 位:
- 定时器走的是源 IRQ 0,经覆盖落到 GSI 2,向量取
0x20 + 0; - 源 IRQ 9 经覆盖落到 GSI 9,向量取
0x20 + 9,极性与触发方式按覆盖表那条记录的标志字来。
实测两个值分别是 0x00000020 与 0x0000A029。再写出这两个 RTE 对应的寄存器号(各要写几个寄存器)。
(3) 说明为什么系统时钟的中断要经过 IRQ 0 → GSI 2 的覆盖,而内核代码里看到的却是「定时器接在 GSI 2 上、向量 0x20」。如果不做覆盖,直接按 GSI = IRQ 把定时器编到 GSI 0,会收到哪个中断?
实验二:用 PM1a_CNT 关机。
(4) 补全 code/项目/mdrOS/exercises/9_9/acpi_pm_starter.c 里的 acpi_enable():向 SMI_CMD 写 ACPI_ENABLE,然后轮询 PM1a_CNT 的 bit 0(SCI_EN),最多 1000 次。模拟器要读三次才置位。跑出来的 polls 应当是 3。
(5) 补全 acpi_shutdown():读 PM1a_CNT,保留低 13 位,把 SLP_TYP(bit 10~12)置 5、SLP_EN(bit 13)置 1,写回。模拟器把 PM1a_CNT 初值设为 0x0000,写完应当是 0x3401。说明为什么必须先置 SCI_EN 再写 SLP_EN,顺序反过来会怎样。
(6) PM1_CNT_LEN 在 FADT 里的作用是告诉内核这个寄存器有多宽(2 字节还是 4 字节)。如果按 4 字节去写而硬件只实现了 2 字节,读回来的高 16 位是什么?为什么内核要用 PM1_CNT_LEN 而不是写死 uint16_t?
验收:第 (2) 问两个 RTE 值与上面一致;第 (5) 问程序输出的 after shutdown: PM1a_CNT=0x3401 与 after enable 行的 polls=3 都要对上;第 (6) 问能说出读未实现位时的两种可能结果(全 0 或全 1,取决于芯片)。