从零写OS(四十一):TLB Shootdown —— 多核下的页表一致性

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 能响应。 ...

June 4, 2026 · 2 min · 大飞

从零写OS(三十九):自旋锁 —— 多核下的第一道防线

ch38 把两颗 CPU 都启动了,但这带来了一个新问题:两颗核心同时访问同一份数据会怎样? 考虑这个场景:CPU0 和 CPU1 同时调用 pmm_alloc() 申请物理页,都扫描到了同一个空闲位,都标记为"已用",于是把同一个物理页分配给了两个不同的进程。这两个进程会互相覆盖对方的内存,然后崩溃。 这章实现自旋锁(spinlock),让同一时间只有一颗核心能进入临界区。 为什么普通变量不能做锁 直觉上,可以用一个整数变量做锁: int lock = 0; if (lock == 0) { // CPU0 读到 0 lock = 1; // CPU1 也读到 0,同时进入! // 临界区 } 问题在于"读-判断-写"不是原子操作。两颗核心在读和写之间有一个竞争窗口,都能同时通过检查。 xchg:原子交换 x86 提供了 xchg 指令,它原子地交换寄存器和内存的值——读和写在硬件层面是不可分割的: static inline void spin_lock(spinlock_t *lock) { uint32_t val = 1; __asm__ volatile ( "1: xchgl %0, %1\n" // 原子:把 1 写入 lock,把旧值读到 val " testl %0, %0\n" // 旧值为 0? " jz 2f\n" // 是 → 获锁成功 " pause\n" // 否 → 稍等,再试 " jmp 1b\n" "2:\n" : "+r"(val), "+m"(lock->locked) :: "memory" ); } xchg 隐含 lock 前缀,直接是总线级原子操作。如果拿到的旧值是 0,说明锁之前是空闲的,现在已经被我们锁上了。如果拿到的旧值是 1,说明别人持有锁,自旋等待。 ...

June 4, 2026 · 2 min · 大飞

从零写OS(三十五):getdents64 + chdir + 内存屏障 Bug

上一章 ls 能运行了,但打印出来的目录内容是空的——因为还没有实现 getdents64。这一章把目录列表、cd、pwd 全部补齐,同时碰到了一个 -O2 下内存乱序导致的调度器死循环 Bug。 SYS_GETDENTS64 Linux 接口 int getdents64(int fd, struct linux_dirent64 *dirp, unsigned int count); 每个条目的结构: struct linux_dirent64 { uint64_t d_ino; // inode 号 int64_t d_off; // 到下一条目的偏移(可用递增序号代替) uint16_t d_reclen; // 本条目总字节数(含填充,8 字节对齐) uint8_t d_type; // 文件类型(4=目录,8=普通文件) char d_name[]; // 文件名(null 结尾) }; d_reclen 必须 8 字节对齐,计算方式: uint16_t reclen = (uint16_t)((19 + namelen + 1 + 7) & ~7); // ↑固定头部 ↑名字 ↑null ↑对齐 内核实现 在 VFS 层实现 vfs_getdents,遍历 ext2 目录的所有条目,逐个填写 linux_dirent64 并写入用户缓冲区: int vfs_getdents(int fd, uint64_t uva, uint32_t count) { // 1. 检查 fd 是否为目录 // 2. 从 offset 开始遍历 ext2 目录 // 3. 跳过 "." 和 ".."(busybox ls 不显示它们) // 4. 每个条目:填 d_ino、d_type、d_name、d_reclen // 5. 通过 copy_to_user 写入用户空间 // 6. 更新 file->offset,返回写入的总字节数 } 关键:返回 0 表示目录已读完(EOF),busybox ls 据此停止调用。 ...

May 22, 2026 · 3 min · 大飞
京ICP备14031575号-3