到目前为止,内核一直跑在单核上。这一章迈出多核的第一步:让所有 CPU 核心都进入内核。
验证结果:
[acpi] MADT found, 2 CPU(s)
[smp] booting AP lapic=1
[cpu0] online
[cpu1] online
[smp] all APs online, total CPUs=2
/ #
x86 多核启动的规则
x86 多核启动有一套固定规则。系统上电后只有一颗 CPU 跑起来,叫做 BSP(Bootstrap Processor)。其余的核心叫 AP(Application Processor),处于等待状态,需要 BSP 主动唤醒。
唤醒的方式是通过 LAPIC(Local APIC) 发送 IPI(Inter-Processor Interrupt)。每颗核心内置一个 LAPIC,是核间通信的硬件基础。
第一步:找到所有 CPU
CPU 的信息藏在 ACPI MADT 表里。MADT(Multiple APIC Description Table)是固件写好放在内存里的一张表,描述了系统上有多少颗 CPU 以及每颗 CPU 的 LAPIC ID。
内核启动时扫描 MADT,把所有有效 CPU 的 LAPIC ID 记下来:
void acpi_parse_madt(void) {
madt_t *madt = acpi_find_table("APIC");
// 遍历所有条目,找 type=0 的 Processor Local APIC 条目
// 记录 lapic_id → cpu_lapic_ids[],ncpus++
}
第二步:发送 INIT + STARTUP IPI
唤醒 AP 的序列是固定的(来自 Intel SDM):
- 发送 INIT IPI:复位 AP
- 等待 10ms
- 发送两次 STARTUP IPI:告诉 AP 从哪个物理地址开始执行
STARTUP IPI 的向量字段(8 位)是 AP 起始物理地址 » 12。例如,让 AP 从 0x8000 开始,向量就是 0x08。这个起始地址必须在低 1MB(0x000xx000),这是 CPU 的硬件限制。
第三步:蹦床代码
AP 收到 STARTUP IPI 后,从实模式开始执行。内核需要在低地址(0x8000)放一段汇编代码,把 AP 从实模式引导到 64 位长模式:
[实模式 0x8000]
→ 加载临时 GDT,进入保护模式
→ 开启 PAE + IA32e,加载 kernel_pml4,开启分页
→ 跳入 64 位代码
→ 调用 ap_main()
页表直接复用 BSP 的 kernel_pml4,所以 AP 进入 64 位后就和 BSP 共享内核地址空间。
第四步:ap_main
每颗 AP 进入 C 代码后,执行自己的初始化:
void ap_main(void) {
gdt_init(); // 加载内核 GDT
idt_init(); // 加载 IDT
lapic_init(); // 初始化自己的 LAPIC
tss_init(...); // 设置自己的内核栈
int id = cpu_id(); // 通过 LAPIC ID 查表得到逻辑编号
kprintf("[cpu%d] online\n", id);
__sync_fetch_and_add(&ap_online_count, 1);
for (;;) __asm__ volatile("hlt"); // 暂时空转
}
注意 __sync_fetch_and_add:这是原子加操作,保证多颗 AP 同时上线时 ap_online_count 不会出现竞争。
per-CPU 数据结构
多核的一个核心问题是:哪些数据是全局共享的,哪些数据每颗核心要有自己的副本?
最基础的是 current_proc——当前核心在运行哪个进程。如果还是全局变量,两颗核心会互相踩:
// 危险:全局 current_proc,两核同时写
int current_proc = 0;
// 正确:每核独立
typedef struct { int cpu_id; int current_proc; } cpu_t;
cpu_t cpus[MAX_CPUS];
cpu_id() 通过读 LAPIC ID 确定当前是哪颗核,再查 cpu_lapic_ids[] 数组返回逻辑编号。
这一章只是开始
这一章 AP 上线后就挂在 hlt 里空转,还没参与调度。后续章节会逐步:
- ch39:加锁保护共享数据结构
- ch40:per-CPU 调度器,让 AP 真正跑进程
- ch41:TLB shootdown,保证多核下页表一致性