在实模式 DOS 环境下实现多核对称多处理(SMP)支持是一项极具挑战性的底层编程任务。与现代操作系统不同,DOS 缺乏内核级别的多处理器抽象,开发者必须直接与硬件交互,手动完成 APIC(Advanced Programmable Interrupt Controller)初始化、处理器间中断(IPI)发送以及内存一致性管理等关键步骤。
实模式 SMP 的独特约束
DOS 实模式 SMP 编程面临的首要约束是缺乏操作系统支持。在现代 Linux 或 Windows 系统中,内核负责处理 CPU 启动、调度、同步和内存管理;而在 DOS 环境下,这些职责完全由应用程序承担。实模式的 640KB 常规内存限制、16 位寄存器操作以及缺乏保护机制,进一步增加了实现复杂度。
Bootstrap Processor(BSP,引导处理器)是唯一在启动时自动运行的 CPU,其余 Application Processors(APs,应用处理器)处于等待状态,需要通过特定的 INIT-SIPI-SIPI 序列唤醒。这一机制要求开发者精确控制时序,处理不同 CPU 步进和芯片组之间的差异。
Local APIC 初始化流程
实现 SMP 的第一步是定位并初始化 Local APIC。在 x86 架构中,Local APIC 的基地址可以通过读取 MSR(Model Specific Register)0x1B 获得,或者通过解析 ACPI 表的 MADT(Multiple APIC Description Table)条目。在实模式下,通常使用以下方法获取 APIC 基址:
; 读取APIC_BASE MSR (0x1B)
mov ecx, 0x1B
rdmsr
; EDX:EAX 包含APIC基地址,低12位为标志位
and eax, 0xFFFFF000 ; 清除标志位,获得4KB对齐基址
获得基址后,需要配置 APIC 的 Spurious Interrupt Vector Register(SVR),将 APIC 使能位(第 8 位)置 1。这是发送任何 IPI 之前的必要步骤,否则 APIC 将拒绝处理中断命令。
INIT-SIPI-SIPI 启动序列
唤醒 AP 的核心机制是通过 Local APIC 的 ICR(Interrupt Command Register)发送 INIT-SIPI-SIPI 序列。ICR 是一个 64 位寄存器,在 x86 架构中通常映射到 APIC 基址 + 0x300(低 32 位)和 + 0x310(高 32 位)。
启动序列遵循严格的时序要求:
- INIT IPI:向目标 APIC ID 发送初始化信号,将 AP 重置为实模式等待状态
- 延迟等待:等待至少 10 毫秒,确保 AP 完成初始化
- 第一个 SIPI:发送 Startup IPI,携带 4KB 对齐的启动向量
- 短暂延迟:等待约 200 微秒
- 第二个 SIPI(可选):如果 AP 未响应,发送第二个 SIPI
SIPI 向量的编码规则要求启动地址必须是 4KB 对齐,向量值等于物理地址右移 12 位。例如, trampoline 代码位于 0x80000 时,SIPI 向量为 0x80。
Trampoline 跳板代码设计
AP 从 SIPI 指定的地址开始执行时处于实模式,但此时缺乏有效的栈指针和 BSS 段初始化。Trampoline 代码必须完成以下任务:
首先,设置临时栈。由于 AP 无法预知系统内存布局,通常将栈设置在 0x0000-0x1000 范围内的安全区域,或使用 BSP 预先分配的 per-CPU 栈空间。
其次,建立与 BSP 的同步机制。最常见的方法是在内存中设置一个共享标志变量,AP 启动后将其状态写入该位置,BSP 通过轮询检测 AP 是否成功启动。为避免竞态条件,每个 AP 应有独立的标志位或采用原子操作。
最后, trampoline 需要处理模式切换。如果目标代码运行在保护模式或长模式, trampoline 负责完成从实模式到目标模式的完整转换,包括 GDT 加载、CR0 寄存器设置和远跳转。
内存一致性屏障处理
实模式下缺乏高级同步原语,内存一致性屏障的实现依赖特定的序列化指令。CPUID指令是最可靠的序列化指令,它确保之前的所有内存操作在后续指令执行前完成。在 AP 启动流程中,关键同步点应插入 CPUID 调用:
; AP写入完成标志前执行序列化
cpuid
mov [ap_ready_flag], 1
对于不支持 CPUID 的旧处理器,可以使用LOCK前缀的内存操作或IN/OUT指令作为替代方案。在多核环境下,还需注意缓存一致性问题 ——APIC 访问通常是强序的,但常规内存写入可能被 CPU 缓存,需要适当的缓存刷新指令确保 BSP 及时看到 AP 的状态更新。
调试与验证策略
SMP 启动代码的调试极具挑战性,因为 AP 缺乏标准输出能力。推荐采用以下策略:
单核验证优先:首先确保单个 AP 能可靠启动,再扩展到多核并行启动。并行启动会放大时序和同步问题,单核验证有助于隔离根本原因。
内存映射 I/O 调试:使用 VGA 文本缓冲区(0xB8000)或串口(0x3F8)作为调试输出。每个 AP 在关键步骤向固定偏移写入字符或状态码,BSP 通过读取这些位置判断 AP 进度。
超时与重试机制:SIPI 序列可能因硬件差异而失败,实现应包含超时检测和重试逻辑。如果 AP 在预期时间内未设置就绪标志,BSP 可以尝试重新发送 SIPI 或跳过该 CPU。
CPU 计数同步:启动完成后,BSP 和 APs 应对在线 CPU 数量达成一致。使用独立的计数器变量,每个 AP 原子地递增计数器,BSP 验证最终计数与预期核心数匹配。
硬件差异与兼容性
不同代的 Intel 和 AMD 处理器对 APIC 行为存在细微差异。早期 Pentium 处理器的 APIC 实现较为简单,而 Core 系列引入了 x2APIC 模式(通过 MSR 而非 MMIO 访问)。在纯实模式 DOS 环境下,通常使用兼容模式 APIC(MMIO 访问),避免依赖可能不存在的 x2APIC 支持。
芯片组的差异也会影响 SMP 启动。某些主板 BIOS 在启动时可能禁用部分 CPU 或配置特定的 APIC ID 映射,程序应通过 MP 表(MultiProcessor Specification Table)或 ACPI MADT 解析实际的 APIC 拓扑,而非假设连续的 APIC ID 分配。
参考资料
- OSDev Wiki: Symmetric Multiprocessing - AP 启动序列与 Trampoline 设计指南
- Intel SDM Volume 3: Local APIC 编程与 IPI 发送机制
- MultiProcessor Specification (MPS) 1.4: DOS 时代 SMP 配置标准
内容声明:本文无广告投放、无付费植入。
如有事实性问题,欢迎发送勘误至 i@hotdrydog.com。