Skip to content

spinlock.c

TIP

互斥自旋锁 (Mutual exclusion spin locks) xv6 使用自旋锁来保护多个 CPU 同时访问的关键数据结构。 自旋锁是一种简单的锁,它通过在一个循环中持续检查锁的状态(“自旋”)直到锁可用为止。 在持有自旋锁时,代码不能放弃 CPU,因此自旋锁只应在持有时间非常短的情况下使用。 此外,在持有锁时必须禁用中断,以防止中断处理程序尝试获取同一个锁而导致死锁。

#include "types.h"
#include "param.h"
#include "memlayout.h"
#include "spinlock.h"
#include "riscv.h"
#include "proc.h"
#include "defs.h"

TIP

初始化一个自旋锁。 @param lk: 指向 spinlock 结构的指针。 @param name: 锁的名称,用于调试。

void
initlock(struct spinlock *lk, char *name)
{
  lk->name = name;
  lk->locked = 0;
  lk->cpu = 0;
}

TIP

获取锁。 在一个循环中“自旋”,直到成功获取锁。 这个函数在返回前必须确保锁已经被当前 CPU 持有。 @param lk: 指向 spinlock 结构的指针。

void
acquire(struct spinlock *lk)
{

TIP

在尝试获取锁之前禁用中断。 这是为了防止当前线程被中断,而中断处理程序又尝试获取同一个锁,从而导致死锁。 例如:CPU1 持有锁 L,然后发生中断。中断处理程序也尝试获取锁 L, 但由于 L 已经被持有,处理程序会一直自旋等待,永远无法返回, 导致 CPU1 也无法释放锁。

  push_off();

TIP

检查当前 CPU 是否已经持有该锁,防止重入(一个线程重复获取同一个锁)。

  if(holding(lk))
    panic("acquire");

TIP

使用 GCC 内置的原子操作 __sync_lock_test_and_set 来尝试获取锁。 这个函数会原子地将 lk->locked 设置为 1,并返回 lk->locked 的旧值。 在 RISC-V 上,它通常被编译为 amoswap.w.aq 指令。 amoswap 指令原子地将一个新值写入内存地址,并加载该地址的旧值。 .aq (acquire) 后缀是一个内存屏障,确保后续的内存操作不会被重排序到此指令之前。 循环会一直执行,直到 __sync_lock_test_and_set 返回 0, 这意味着在原子操作之前 lk->locked 的值是 0 (未锁定),现在我们成功地将其设置为 1 (锁定)。

  while(__sync_lock_test_and_set(&lk->locked, 1) != 0)
    ;

TIP

使用一个完全的内存屏障 __sync_synchronize。 这个屏障确保在它之前的内存写操作(即锁的获取)对所有 CPU 可见, 并且在它之后的内存读/写操作不会被编译器或 CPU 重排序到它之前。 这对于保证临界区内的代码严格在锁被获取之后执行至关重要。 在 RISC-V 上,这会生成一个 fence 指令。

  __sync_synchronize();

TIP

记录是哪个 CPU 获取了锁。 这对于调试和 holding() 函数的实现非常重要。

  lk->cpu = mycpu();
}

TIP

释放锁。 @param lk: 指向 spinlock 结构的指针。

void
release(struct spinlock *lk)
{

TIP

检查当前 CPU 是否真的持有该锁。如果一个 CPU 尝试释放一个它不持有的锁, 这通常是一个严重的 bug,所以直接 panic。

  if(!holding(lk))
    panic("release");

TIP

清除持有锁的 CPU 信息。

  lk->cpu = 0;

TIP

再次使用一个完全的内存屏障 __sync_synchronize。 这确保了在临界区内所有的内存写操作都已完成,并对其他 CPU 可见, 然后才真正释放锁。同样,这也防止了锁的释放操作被重排序到临界区代码之前。

  __sync_synchronize();

TIP

使用 GCC 内置的原子操作 __sync_lock_release 来释放锁。 这个函数原子地将 lk->locked 设置为 0。 它之所以被使用,是因为一个简单的 C 语言赋值 lk->locked = 0; 不保证是原子的。 在 RISC-V 上,它被编译为 amoswap.w.rl 指令。 .rl (release) 后缀是一个内存屏障,确保在此指令之前的内存操作不会被重排序到它之后。

  __sync_lock_release(&lk->locked);

TIP

恢复之前的中断状态。

  pop_off();
}

TIP

检查当前 CPU 是否持有该锁。 调用此函数时中断必须是禁用的,以确保检查的原子性。 @param lk: 指向 spinlock 结构的指针。 @return: 如果当前 CPU 持有锁,则返回 1,否则返回 0。

int
holding(struct spinlock *lk)
{
  int r;

TIP

只有当锁被标记为 locked 并且持有锁的 CPU 是当前 CPU 时,才认为当前 CPU 持有该锁。

  r = (lk->locked && lk->cpu == mycpu());
  return r;
}

TIP

push_off/pop_off: 用于管理中断状态的嵌套调用。 push_off 会禁用中断,并将 noff 计数器加一。 如果这是第一次调用 push_off(即 noff 从 0 变为 1),它会保存当前的中断状态(开启或关闭)。 pop_off 会将 noff 计数器减一。 只有当 noff 变回 0 时,它才会根据之前保存的状态决定是否要重新启用中断。 这种设计允许 acquirerelease 的临界区可以嵌套。例如: acquire(&lock1); acquire(&lock2); ... release(&lock2); release(&lock1); 在这个过程中,中断只会在最外层的 acquire 中被禁用,并在最外层的 release 中被恢复。

TIP

压入一层中断禁用。

void
push_off(void)
{
  int old = intr_get();

  intr_off();

TIP

如果这是此 CPU 上的第一次 push_off 调用

  if(mycpu()->noff == 0)
    mycpu()->intena = old;
  mycpu()->noff += 1;
}

TIP

弹出一层中断禁用。

void
pop_off(void)
{
  struct cpu *c = mycpu();
  if(intr_get())
    panic("pop_off - interruptible");
  if(c->noff < 1)
    panic("pop_off");
  c->noff -= 1;

TIP

如果嵌套层数回到 0,并且之前的中断状态是开启的,那么就恢复中断

  if(c->noff == 0 && c->intena)
    intr_on();
}