从零写 OS 内核-第十八篇:多核支持 —— 让你的 OS 真正并行起来!
"单核调度只是伪并发,真正的性能飞跃在多核!
今天,我们唤醒所有 CPU 核心,实现 SMP(对称多处理)支持!"
在上一篇中,我们实现了抢占式调度器,但所有任务仍在单个 CPU 核心上切换。
而现代处理器普遍是多核(双核、四核甚至更多),
真正的操作系统必须能利用所有核心,实现真正的并行计算。
今天,我们就来:
✅ 解析多核启动协议(AP 启动流程)
✅ 实现核间通信与同步原语
✅ 改造调度器支持多核调度
让你的 OS 在多核 CPU 上真正"火力全开"!
🧩 一、x86 多核启动基础:BSP 与 AP
x86 多处理器系统启动时:
- BSP(Bootstrap Processor):第一个启动的核心(通常是 Core 0)
- AP(Application Processor):其他核心,初始处于休眠状态
启动 AP 的标准协议:MP Specification 或 ACPI MADT
为简化,我们使用 Intel MultiProcessor Specification (MP Spec)。
关键步骤:
- BSP 解析 MP 表,获取 CPU 核心数量和 LAPIC ID
- BSP 通过 LAPIC 发送 INIT/SIPI 消息 唤醒 AP
- AP 从指定地址开始执行(通常是 0x10000)
💡 LAPIC(Local APIC) 是每个核心的中断控制器,也是核间通信的关键。
🔌 二、LAPIC 初始化与 AP 启动
1. 启用 LAPIC
void lapic_init() {
// 1. 检查 CPU 是否支持 APIC
if (!(cpuid_features() & CPUID_FEAT_EDX_APIC)) {
panic("APIC not supported!");
}
// 2. 启用 APIC(设置 IA32_APIC_BASE MSR)
uint64_t apic_base = rdmsr(MSR_IA32_APIC_BASE);
apic_base |= APIC_ENABLE_BIT;
wrmsr(MSR_IA32_APIC_BASE, apic_base);
// 3. 映射 LAPIC 寄存器到虚拟地址
lapic_base_vaddr = 0xFEE00000; // 默认物理地址
map_kernel_page(lapic_base_vaddr, lapic_base_vaddr, PAGE_PRESENT | PAGE_RW);
}
2. 解析 MP Floating Pointer
struct mp_floating_pointer {
char signature[4]; // "_MP_"
uint32_t mp_table;
// ... 其他字段
};
void mp_parse() {
// 在 0x0009FC00 - 0x0009FFFF 或 0xF0000 - 0xFFFFF 扫描
for (uint32_t addr = 0x9FC00; addr < 0xA0000; addr += 16) {
struct mp_floating_pointer *fp = (void*)addr;
if (memcmp(fp->signature, "_MP_", 4) == 0) {
mp_table = (void*)fp->mp_table;
break;
}
}
// 解析 CPU 条目
cpu_count = 0;
for (struct mp_config_table_entry *entry = mp_table->entries;
(void*)entry < (void*)mp_table + mp_table->length;
entry = next_entry(entry)) {
if (entry->type == MP_CPU) {
cpu_info[cpu_count].apic_id = entry->cpu.apic_id;
cpu_info[cpu_count].bsp = entry->cpu.cpu_flags & 1;
cpu_count++;
}
}
}
3. 启动 AP 核心
void ap_start(uint8_t apic_id, uint32_t startup_addr) {
// 1. 发送 INIT IPI
lapic_write(LAPIC_ICR1, apic_id << 24);
lapic_write(LAPIC_ICR, APIC_ICR_INIT | APIC_ICR_ASSERT | APIC_ICR_LEVEL_TRIGGER);
delay(10000); // 等待 INIT 完成
// 2. 发送 SIPI(Startup IPI)
uint8_t vector = startup_addr >> 12; // Startup 页(4KB 对齐)
lapic_write(LAPIC_ICR1, apic_id << 24);
lapic_write(LAPIC_ICR, APIC_ICR_STARTUP | APIC_ICR_ASSERT | vector);
}
🧱 三、AP 启动代码(汇编)
AP 从实模式开始执行,需要:
- 设置保护模式
- 启用分页
- 跳转到 C 代码
section .text
align 4096
ap_startup:
; 实模式下,CS=0x1000, IP=0
cli
lgdt [gdtr] ; 加载 GDT(与 BSP 共享)
; 进入保护模式
mov eax, cr0
or eax, 1
mov cr0, eax
jmp 0x08:ap_pm ; 远跳转刷新 CS
[bits 32]
ap_pm:
mov ax, 0x10
mov ds, ax
mov es, ax
mov fs, ax
mov gs, ax
mov ss, ax
; 启用分页(使用 BSP 的页目录)
mov eax, cr3_bsp ; BSP 的页目录物理地址
mov cr3, eax
mov eax, cr0
or eax, 0x80000000 ; 设置 PG 位
mov cr0, eax
; 跳转到 C 入口
jmp ap_main
🔑 AP 使用与 BSP 相同的 GDT 和页目录(简化设计)。
🔒 四、核间同步:自旋锁与原子操作
多核并发访问共享数据时,必须同步!
1. 原子操作(内联汇编)
static inline bool atomic_cas(volatile uint32_t *ptr, uint32_t expected, uint32_t desired) {
uint32_t result;
asm volatile (
"lock cmpxchg %2, %1"
: "=a"(result), "+m"(*ptr)
: "r"(desired), "a"(expected)
: "memory"
);
return result == expected;
}
2. 自旋锁(Spinlock)
typedef struct spinlock {
volatile uint32_t lock;
} spinlock_t;
#define SPINLOCK_INIT {0}
static inline void spinlock_init(spinlock_t *lock) {
lock->lock = 0;
}
static inline void spinlock_acquire(spinlock_t *lock) {
while (!atomic_cas(&lock->lock, 0, 1)) {
// 自旋等待
asm volatile ("pause" ::: "memory");
}
}
static inline void spinlock_release(spinlock_t *lock) {
asm volatile ("" ::: "memory"); // 内存屏障
lock->lock = 0;
}
⚠️ 自旋锁持有期间必须关闭中断(避免死锁)!
🔄 五、多核调度器改造
1. 每个核心独立的运行队列
typedef struct percpu_data {
task_t *current_task;
task_t *run_queue;
spinlock_t run_queue_lock;
// ... 其他 per-CPU 数据
} percpu_data_t;
// 每个核心一个
percpu_data_t percpu[MAX_CPUS];
2. 获取当前核心 ID
static inline uint8_t get_cpu_id() {
uint32_t lo, hi;
asm volatile ("rdtscp" : "=a"(lo), "=d"(hi) :: "rcx");
return (hi >> 24) & 0xFF; // 从 TSC_AUX 获取
}
3. 调度器函数改造
void schedule() {
uint8_t cpu = get_cpu_id();
percpu_data_t *pcpu = &percpu[cpu];
spinlock_acquire(&pcpu->run_queue_lock);
// 从本核心运行队列取任务
task_t *next = get_next_task(pcpu->run_queue);
if (next) {
pcpu->current_task = next;
switch_to_task(next);
}
spinlock_release(&pcpu->run_queue_lock);
}
4. 负载均衡(可选)
- 定期检查其他核心的运行队列
- 如果负载不均,迁移任务
📞 六、核间中断(IPI):核心间通信
LAPIC 支持核间中断(IPI),用于:
- 调度请求:一个核心唤醒另一个核心
- TLB 刷新:一个核心修改页表后通知其他核心
- 关机/重启
void send_ipi(uint8_t target_apic_id, uint8_t vector) {
lapic_write(LAPIC_ICR1, target_apic_id << 24);
lapic_write(LAPIC_ICR, APIC_ICR_FIXED | APIC_ICR_ASSERT | vector);
}
示例:TLB 刷新
void flush_tlb_others() {
for (int i = 0; i < cpu_count; i++) {
if (i != get_cpu_id()) {
send_ipi(cpu_info[i].apic_id, TLB_FLUSH_IPI_VECTOR);
}
}
}
🧪 七、测试:多核并行计算
用户程序:
void worker() {
uint64_t sum = 0;
for (volatile int i = 0; i < 100000000; i++) {
sum += i;
}
printf("Core %d: sum = %llu\n", get_core_id(), sum);
}
内核:
- 启动 4 个进程,绑定到不同核心
- 观察 QEMU 的
-smp 4输出
运行效果:
Core 0: sum = 4999999950000000
Core 1: sum = 4999999950000000
Core 2: sum = 4999999950000000
Core 3: sum = 4999999950000000
✅ 四个核心同时计算,总时间约为单核的 1/4!
⚠️ 七、多核编程的陷阱
- 伪共享(False Sharing)
- 不同核心修改同一缓存行的不同变量
- 解决方案:缓存行对齐(
__attribute__((aligned(64))))
- 内存屏障
- 多核下 CPU 和编译器会重排序指令
- 关键操作需加
mfence/sfence/lfence
- 死锁
- 多个锁的获取顺序必须一致
- 使用锁层级(Lock Ordering)避免
- 中断亲和性
- 将设备中断绑定到特定核心,减少跨核同步
💡 多核编程的黄金法则:尽量减少共享状态,用消息传递代替共享内存!
💬 写在最后
多核支持是操作系统从"玩具"走向"生产级"的关键一步。
它不仅是性能的提升,更是并发思维的革命。
今天你唤醒的第二个 CPU 核心,
正是现代数据中心数百万核心并行计算的起点。
🌟 并行不是选择,而是必然——因为单核性能早已停滞。
📬 动手挑战:
在 QEMU 中启动 4 核,创建 4 个计算密集型进程,验证加速比。
欢迎在评论区分享你的多核性能测试结果!
👇 下一篇你想看:进程间通信(管道/Pipe),还是 信号(Signal)机制?
#操作系统 #内核开发 #多核 #SMP #LAPIC #调度器 #并行计算 #从零开始
📢 彩蛋:关注后回复关键词 "smp",获取:
- 完整多核启动代码(MP Spec 解析 + AP 启动)
- 自旋锁与原子操作实现
- 多核调度器改造模板