从零写 OS 内核-第十八篇:多核支持 —— 让你的 OS 真正并行起来!

"单核调度只是伪并发,真正的性能飞跃在多核!
今天,我们唤醒所有 CPU 核心,实现 SMP(对称多处理)支持!"

在上一篇中,我们实现了抢占式调度器,但所有任务仍在单个 CPU 核心上切换。
而现代处理器普遍是多核(双核、四核甚至更多),
真正的操作系统必须能利用所有核心,实现真正的并行计算

今天,我们就来:
解析多核启动协议(AP 启动流程)
实现核间通信与同步原语
改造调度器支持多核调度

让你的 OS 在多核 CPU 上真正"火力全开"!


🧩 一、x86 多核启动基础:BSP 与 AP

x86 多处理器系统启动时:

  • BSP(Bootstrap Processor):第一个启动的核心(通常是 Core 0)
  • AP(Application Processor):其他核心,初始处于休眠状态

启动 AP 的标准协议:MP SpecificationACPI MADT

为简化,我们使用 Intel MultiProcessor Specification (MP Spec)

关键步骤:

  1. BSP 解析 MP 表,获取 CPU 核心数量和 LAPIC ID
  2. BSP 通过 LAPIC 发送 INIT/SIPI 消息 唤醒 AP
  3. 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 从实模式开始执行,需要:

  1. 设置保护模式
  2. 启用分页
  3. 跳转到 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!


⚠️ 七、多核编程的陷阱

  1. 伪共享(False Sharing)
    • 不同核心修改同一缓存行的不同变量
    • 解决方案:缓存行对齐__attribute__((aligned(64)))
  2. 内存屏障
    • 多核下 CPU 和编译器会重排序指令
    • 关键操作需加 mfence/sfence/lfence
  3. 死锁
    • 多个锁的获取顺序必须一致
    • 使用锁层级(Lock Ordering)避免
  4. 中断亲和性
    • 将设备中断绑定到特定核心,减少跨核同步

💡 多核编程的黄金法则:尽量减少共享状态,用消息传递代替共享内存!


💬 写在最后

多核支持是操作系统从"玩具"走向"生产级"的关键一步。
它不仅是性能的提升,更是并发思维的革命

今天你唤醒的第二个 CPU 核心,
正是现代数据中心数百万核心并行计算的起点。

🌟 并行不是选择,而是必然——因为单核性能早已停滞。


📬 动手挑战
在 QEMU 中启动 4 核,创建 4 个计算密集型进程,验证加速比。
欢迎在评论区分享你的多核性能测试结果!

👇 下一篇你想看:进程间通信(管道/Pipe),还是 信号(Signal)机制


#操作系统 #内核开发 #多核 #SMP #LAPIC #调度器 #并行计算 #从零开始


📢 彩蛋:关注后回复关键词 "smp",获取:

  • 完整多核启动代码(MP Spec 解析 + AP 启动)
  • 自旋锁与原子操作实现
  • 多核调度器改造模板