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.c、heap.c、process.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 和堆的并发分配不再损坏数据。