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,说明别人持有锁,自旋等待。

pause 指令告诉 CPU 这是自旋等待,可以降低功耗并避免超线程场景下的内存顺序问题。

解锁只需把值写回 0:

static inline void spin_unlock(spinlock_t *lock) {
    __asm__ volatile (
        "movl $0, %0\n"
        : "=m"(lock->locked)
        :: "memory"
    );
}

光有自旋锁还不够

考虑这个场景:

CPU0 持有 pmm_lock,正在 pmm_alloc()
→ PIT 中断打进来
→ 中断处理程序调 kmalloc → kmalloc 调 pmm_alloc → 尝试拿 pmm_lock
→ 死锁!CPU0 在等自己释放锁

单核上,持锁期间被中断打断,中断里又想拿同一把锁,就死锁了。

解决方法是:进入临界区前关中断,出来后恢复

static inline void spin_lock_irqsave(spinlock_t *lock, uint64_t *flags) {
    __asm__ volatile ("pushfq; popq %0; cli" : "=r"(*flags) :: "memory");
    spin_lock(lock);
}

static inline void spin_unlock_irqrestore(spinlock_t *lock, uint64_t *flags) {
    spin_unlock(lock);
    __asm__ volatile ("pushq %0; popfq" :: "r"(*flags) : "memory", "cc");
}

pushfq 把 RFLAGS 保存到 flags(包含中断使能位 IF),cli 关中断。解锁时恢复 RFLAGS,而不是直接 sti——这样可以保留调用者原本的中断状态(如果调用者进来时就是关中断的,出去还是关中断)。

给共享数据结构加锁

pmm.cheap.cprocess.c 都有共享数据,全部加上锁:

// pmm.c
static spinlock_t pmm_lock = SPINLOCK_INIT;

void *pmm_alloc() {
    uint64_t flags;
    spin_lock_irqsave(&pmm_lock, &flags);
    // 位图查找...
    spin_unlock_irqrestore(&pmm_lock, &flags);
    return page;
}

进程表分配的临界区有个细节:找到空闲 slot 后要立即标记为非 UNUSED,然后再释放锁,否则两颗核心可能同时找到同一个 slot:

spin_lock_irqsave(&proc_table_lock, &flags);
for (int i = 0; i < MAX_PROCS; i++) {
    if (procs[i].state == PROC_UNUSED) {
        procs[i].state = PROC_READY;   // 立即占位,再释放锁
        slot = i;
        break;
    }
}
spin_unlock_irqrestore(&proc_table_lock, &flags);

验收

[tss] init ok
[smp] booting AP lapic=1
[cpu1] online
[smp] all APs online, total CPUs=2
/ #

两核同时运行,shell 正常启动,PMM 和堆的并发分配不再损坏数据。