ch40 里 AP 还没开中断,原因是页表修改没有保护。这章解决这个问题:实现 TLB Shootdown,让多核下的页表修改安全可见。

问题:每颗 CPU 有自己的 TLB

TLB(Translation Lookaside Buffer)是每颗核心内置的页表缓存,把虚拟地址→物理地址的映射缓存起来,避免每次都走四级页表。

问题在于,当一颗核心修改了页表(比如 fork 的 CoW、exec 建立新地址空间),其他核心的 TLB 里可能还缓存着旧的映射:

CPU0 修改页表:VA 0x1000 → 新物理页 0xABCD000
CPU1 的 TLB:  VA 0x1000 → 旧物理页 0x1234000  ← 未失效!

CPU1 访问 VA 0x1000 → 访问了错误的物理页 → 数据错误

解决方法:修改页表后,给所有其他核心发一个 IPI,让它们执行 invlpg 使对应 TLB 条目失效。这个过程叫 TLB Shootdown

实现

全局协调变量

volatile uint64_t tlb_shootdown_addr = 0;
volatile int      tlb_ack_count      = 0;

发起方(修改页表的核心)

void tlb_shootdown(uint64_t vaddr) {
    int online = ap_online_count;
    if (online <= 0) {
        // 单核路径,直接本地 invlpg
        __asm__ volatile("invlpg (%0)" :: "r"(vaddr) : "memory");
        return;
    }

    tlb_shootdown_addr = vaddr;
    __sync_synchronize();   // 写屏障:确保其他核能看到 addr

    tlb_ack_count = 0;
    __sync_synchronize();

    lapic_send_ipi_others(TLB_SHOOTDOWN_VECTOR);   // 广播 IPI

    while (tlb_ack_count < online)   // 等所有 AP 应答
        __asm__ volatile("pause");

    __asm__ volatile("invlpg (%0)" :: "r"(vaddr) : "memory");  // 本核也刷新
}

ap_online_count 只有在 AP 真正开中断后才增加,确保发 IPI 时 AP 能响应。

接收方(AP 的中断处理)

void tlb_shootdown_handler(void) {
    uint64_t addr = tlb_shootdown_addr;
    if (addr == TLB_FLUSH_ALL_VADDR)
        __asm__ volatile("mov %%cr3, %%rax; mov %%rax, %%cr3" ::: "rax", "memory");
    else
        __asm__ volatile("invlpg (%0)" :: "r"(addr) : "memory");
    __sync_fetch_and_add(&tlb_ack_count, 1);   // 原子 ack
    lapic_eoi();
}

IPI 广播

LAPIC ICR 支持 “All Excluding Self” shorthand(ICR[19:18] = 11b),一条写操作发给所有其他核,不需要循环:

void lapic_send_ipi_others(uint8_t vector) {
    lapic_write(LAPIC_ICR_HI, 0);
    lapic_write(LAPIC_ICR_LO, 0x000C0000 | vector);
}

页表锁

TLB Shootdown 只解决了"失效通知"的问题,还需要一把锁防止并发修改页表:

spinlock_t vmm_lock = SPINLOCK_INIT;

exec 整个页表构建过程持锁:

spin_lock(&vmm_lock);
new_pml4 = vmm_create_page_table();
elf_load_into(new_pml4, ...);
current->pml4 = new_pml4;
spin_unlock(&vmm_lock);

sched_tickvmm_switch 前也持锁,防止切换到正在构建中的页表。

AP 开中断的时序

AP 必须先开中断,再增加 ap_online_count

void ap_main(void) {
    // ...初始化...
    lapic_timer_init();
    __asm__ volatile("sti");                      // 先开中断
    __sync_fetch_and_add(&ap_online_count, 1);    // 再增加计数
    for (;;) __asm__ volatile("hlt");
}

顺序不能反。如果先增加计数,BSP 可能立刻发 TLB shootdown IPI,但 AP 还没开中断,无法响应,BSP 就会一直等待 ack 而死锁。

内存屏障

__sync_synchronize() 是全内存屏障,相当于 mfence,作用有两个:

  1. 编译器屏障:防止编译器把屏障前后的读写重排
  2. 硬件屏障:确保屏障前的写操作对其他核心可见

在 TLB Shootdown 里,必须保证"写 tlb_shootdown_addr“对所有 AP 可见后,才发送 IPI。没有屏障的话,IPI 可能先到,AP 读到的 addr 还是旧值。

调试过程

实现过程中遇到了一个死锁:lapic_init() 里调用 vmm_map_page 映射 LAPIC MMIO,而 vmm_map_page 里有 TLB shootdown 逻辑。AP 还没开中断,BSP 发 IPI 等 AP ack,AP 没法响应,死锁。

修复:tlb_shootdown 检查 ap_online_count,为 0 时直接走单核路径,不发 IPI。AP 开中断并增加计数后,才会进入多核路径。

验收

[smp] all APs online
/ # ls
ls

两核均上线,AP 开中断,ls 正常运行。fork+exec 在多核下不再出现页表不一致崩溃。