自制工业控制系统:CPU信息与多核实战——CPUID、INIT-SIPI-SIPI唤醒与双核分工
Contents
起因
工控主板上插的是多核 CPU,我们的系统到现在只用了一颗核——固件唤醒的 BSP,第二颗核从开机起就在那儿睡觉。这篇干两件事:一是把 CPUID 用起来,CPU 什么型号、几个核、缓存多大、支持哪些指令,一条指令全问出来——自检页要显示的 CPU 信息、页帧分配器要的物理地址宽度,数据源都是它;二是把第二颗核叫醒干活,INIT-SIPI-SIPI 唤醒、每核自己的自举,然后给两颗核分好工——Core0 跑实时、Core1 跑服务,物理上的隔离比任何软件优先级方案都硬。
两样东西都有来历。CPUID 是九十年代中期 Intel 加的指令,背景是 x86 兼容厂商遍地——软件想知道自己跑在谁的 CPU 上、有什么特性,只能问 CPU 本身。INIT-SIPI-SIPI 更老,是 Intel 多处理器规范(MPS)时代定下的握手,那时要多处理器就得主板插两颗物理 CPU;2005 年前后 Pentium D 和 Athlon 64 X2 把两颗核封进一个壳,双核沿用同一套唤醒协议,SIPI 的目标从插座上的另一颗 CPU 变成了同一块硅上的邻居。
一、CPUID:一条指令问清 CPU 的家底
CPUID 一条指令答所有"CPU 是谁、会什么"。Rust 包装:
|
|
常用的叶(leaf)清单:
| 叶 | 回答什么 | 关键字段 |
|---|---|---|
| 0x0 | 厂商字符串 + 最大基本叶 | EBX/ECX/EDX 拼 “GenuineIntel” / “AuthenticAMD” |
| 0x1 | 家族/型号/步进 + 特性位 | EAX bits4-7 族、bits8-11 型号;EDX bit4 TSC、bit5 MSR、bit9 APIC;ECX bit31 hypervisor(跑在虚拟机里!QEMU 调试时认它) |
| 0x2 / 0x4 | 缓存描述 / 确定性缓存参数 | 每级缓存的组数/路数/行大小 |
| 0x16 | 基础频率(MHz) | 可用时直接显示;不可用回退品牌串解析 |
| 0x80000002–4 | 品牌字符串 | “Intel(R) Core(TM) i7-12700…",自检页那行字 |
| 0x80000007 | invariant TSC 确认 | EDX bit8(APIC 篇用过的判据) |
| 0x80000008 | 物理地址位宽 | EAX bits0-7——identity 分页能覆盖多少物理内存的理论上限 |
两个实战点:缓存行大小(leaf 1 的 EBX bits8-15 ×8,典型 64 字节)是 DMA 缓冲对齐的依据——跨缓存行的 DMA 缓冲碰到驱逐会有一致性惊讶,PCIe 篇的 DMA 池按它对齐;拓扑用 leaf 0xB 读,走法是 subleaf 从 0 逐层往上问,每层 ECX bits15-8 是层类型(1=硬件线程、2=核心),EBX bits15-0 是该层的逻辑处理器数,EAX bits4-0 是本层 APIC ID 的宽度增量——问到 EBX 返回 0 为止,把各层宽度累起来就是 APIC ID 总宽,线程数也就出来了。比 leaf 1 的老字段(HTT 位 + APIC ID 挖位)可靠得多;更新的 CPU 上 leaf 0x1F 是它的扩展版,有就优先。MSR(rdmsr/wrmsr)是 CPUID 的姊妹通道,本系列已多次使用(0x1B APIC 基址、0x19C 温度),此处不重复。
二、多核模型:BSP、AP 和 MADT 拓扑
x86 多核的启动不对称:固件只唤醒一颗 BSP(就是跑我们内核的那颗),其余核(AP)上电后停在等待状态,等 BSP 用"星际唤醒指令"叫醒。谁可以被唤醒、各自的 LAPIC ID 是多少——答案在 ACPI 的 MADT(APIC 篇解析过同一张表):每个 Type 0 条目一颗逻辑核,含 LAPIC ID 和使能标志。
唤醒协议是 x86 上最有仪式感的硬件接口:INIT-SIPI-SIPI,通过 BSP 的 Local APIC 广播。ICR(中断命令寄存器,LAPIC 偏移 0x310 高字/0x300 低字,先写高后写低触发):
|
|
SIPI 的向量字节是实模式段号——AP 收到后从物理地址 向量×0x1000 开始执行 16 位实模式代码。这意味着我们需要一段"蹦床”:放在 1MB 以下的低位内存(比如 0x8000),AP 落地后沿这条路径爬回 64 位。
三、蹦床与每核自举
蹦床是一小段 16 位汇编,垫在低位内存,做 AP 的"产道":
|
|
细节坑四个,每个都有真实症状:
- 发 SIPI 之前,蹦床、identity 页表、GDT 必须已经写完。 广播出去 AP 立刻就开始读了,BSP 写一半的蹦床等于让 AP 执行垃圾——症状是"有的核能起来有的起不来,还每次不一样"。发 SIPI 前对共享数据做一次全量 store 屏障
- AP 的 LAPIC 是独立一份,落地后要自己使能:SVR 写 bit8(APIC 篇同款写法)。忘这步的症状是 AP 上第一个定时器中断永远不来,核"活着"但不干活
- 每颗核的定时器各自 LAPIC 独立——只有承担实时任务的核配 LVT Timer,别的核关着,别让两颗核同时打实时节拍,否则插补周期会随机叠加出两倍速的伪节拍
- 同步原语只信
lock前缀指令(cmpxchg/xadd),跨核变量一律AtomicU32起;普通变量的跨核可见性在 x86 上靠 store 顺序保证,但编译器重排照样能坑人——共享结构体的字段声明用AtomicPtr/AtomicUsize,别拿volatile当同步用
会师与超时:BSP 发完 SIPI 后轮询原子计数(每次 lock xadd 后 AP 自增),等它等于 MADT 里使能的核数;给个 100ms 量级的超时,到点缺谁就当谁缺席继续跑——工控系统不为"唤醒一颗顽固的核"无限期停机,缺核降级记日志(日志篇),自检页标黄。
四、双核分工:工控的最优解
多核怎么用,通用 OS 的答案是调度器排队——我们的答案恰好相反:不给多核做通用调度,给两颗核固定分工(延续"不用那么复杂"):
| Core 0(实时核) | Core 1(服务核) | |
|---|---|---|
| 跑什么 | 放电/插补 ISR + 实时协作循环 | GUI、网络协议栈、文件系统、日志 |
| 中断 | APIC 高优先区(安全/放电/插补) | 设备区(网卡/USB/NVMe) |
| 互通 | SPSC 运动指令队列(服务核→实时核) | SPSC 状态回传队列(实时核→服务核) |
| 最坏情况 | 界面卡死、网络风暴都碰不到实时节拍 | 实时核怎么忙,界面照常刷新 |
这就是把调度篇的两级模型从"软件优先级"升级成"物理隔离":GUI 再怎么失控(哪怕死循环),Core 0 的中断节拍一微秒不少——不靠任何调度算法,靠的是两颗物理核心。要诚实记一笔:物理隔离挡住的是干扰执行,挡不住共享资源——两颗核通常共享 L3 和内存总线,Core1 疯狂压测内存时 Core0 的延迟底噪会抬一点,这个量要测(实时性验证篇的烤机场景里专门有"Core1 满载"一项),好在实时核工作集小、代码路径全在 L1/L2 里,受影响幅度通常有限——但"通常"不算数,测过的数才算。MSI 的中断路由天然支持按核投递(消息地址里带 LAPIC ID,PCIe 篇的接线在这里多填一个字段);跨核紧急通知(急停广播、致命错误)用 IPI(就是唤醒用过的 ICR,向量挂到对方核的 IDT)。核心数多于两颗(现代 CPU 8 核 16 线程起步):多余的核报到后直接 hlt 停机省电——hlt 会被中断唤醒,醒来检查无事可做就再睡,保持一个最小 NMI/IPI 入口即可;或留给未来的 3D 模拟篇当算力。
总结
实战清单:CPUID 包装函数 + 叶速查表(厂商/品牌串/频率/拓扑/缓存行/物理位宽/hypervisor 位);MADT Type 0 拿核清单;ICR 广播 INIT-SIPI-SIPI 三连;16 位蹦床爬回长模式(PAE→EFER.LME→PG→远跳);每核 GDT/IDT 共表、TSS/IST/LAPIC 各自私有、原子计数会师 + 100ms 缺席超时;双核固定分工(实时核/服务核)+ 队列互通 + IPI 兜底;闲核 hlt。跑通标志:已启动核数 等于 MADT 条目数,Core 1 的 GUI 刻意写死循环时 Core 0 的脉冲计数一拍不丢——那一刻,这套系统的实时性承诺才算兑现到了硅片级。
参考链接
- Intel SDM Vol.3A Chapter 8(多处理器初始化)——INIT-SIPI-SIPI 与 AP 启动状态的权威定义
- OSDev Wiki: Symmetric Multiprocessing / CPUID
- 本系列调度篇:中断驱动的硬实时层与协作式主循环、APIC 篇:定时器与系统时间实战、PCIe 篇(MSI 按核路由):ECAM配置空间、BAR探测
- 本系列第一篇:自制工业控制系统:从UEFI到内核
Author 软件开发大郭
LastMod 2026-09-11