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_tick 在 vmm_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,作用有两个:
- 编译器屏障:防止编译器把屏障前后的读写重排
- 硬件屏障:确保屏障前的写操作对其他核心可见
在 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 在多核下不再出现页表不一致崩溃。