Sleep locks

注释:该内容基于原版课程的textbook和源代码,在大模型辅助翻译的基础上进行的整理。

有时 xv6 需要长时间持有锁。例如,文件系统(第 8 章)在磁盘上读取和写入文件内容时保持文件被锁住,这些磁盘操作可能需要几十毫秒。
如果在这么长的时间内持有自旋锁,当另一个进程尝试获取锁时,就会造成浪费,因为获取锁的进程在自旋期间会长时间循环检测锁的状态从而浪费CPU的计算资源。

自旋锁的另一个缺点是,进程在保留自旋锁的情况下无法让出 CPU;我们希望进程在等待磁盘时能够让出 CPU,以便其他进程可以使用 CPU。
在持有自旋锁的情况下让出是非法的,因为如果第二个线程随后尝试获取自旋锁,可能会导致死锁; 由于获取锁的过程不会让出 CPU,第二个线程的自旋可能会阻止第一个线程运行并释放锁。
在持有锁的情况下让出还会违反自旋锁持有时必须关闭中断的要求。因此,我们希望有一种锁,在等待获取时可以让出 CPU,并允许在持有锁的同时让出(以及允许中断)。

Xv6 提供了一种这样的锁,即睡眠锁(sleep-lock)。acquiresleep(kernel/sleeplock.c:22)在等待时让出 CPU,使用的技术将在第 7 章中解释。
从整体上看,睡眠锁有一个受自旋锁保护的 locked 字段,acquiresleep 中调用的 sleep 会以原子方式让出 CPU 并释放自旋锁。
其结果是,在 acquiresleep 等待时,其他线程可以执行。

由于睡眠锁在持有时不会禁用中断,因此不能在中断处理程序中使用。
由于 acquiresleep 可能会让出 CPU,睡眠锁不能在自旋锁的关键区内使用(尽管自旋锁可以在睡眠锁的关键区内使用)。

自旋锁适用于短时间的关键区,因为等待它会浪费 CPU 时间;而睡眠锁适合用于较长的操作。


riscv-xv6\xv6-labs-2024\kernel\sleeplock.h
// Long-term locks for processes
struct sleeplock {
  uint locked;       // Is the lock held? 表示锁是否被持有。0 表示未持有,1 表示已持有。
  struct spinlock lk; // spinlock protecting this sleep lock  用于保护此睡眠锁的自旋锁。自旋锁用于在进入临界区时提供保护,防止多个进程同时修改 locked 字段。
  
  // For debugging:
  char *name;        // Name of lock.  锁的名称,用于调试目的。
  int pid;           // Process holding lock 持有锁的进程的 pid,用于记录哪个进程当前持有锁。
};


void
acquiresleep(struct sleeplock *lk)
{
  acquire(&lk->lk); // 获取用于保护 locked 字段的自旋锁,进入临界区。这样可以确保对 locked 字段的修改是线程安全的。
  while (lk->locked) { //检查锁是否已被持有。如果 lk->locked 为 1,说明锁已被其他进程持有,当前进程需要等待。
    sleep(lk, &lk->lk); //调用 sleep 函数,当前进程进入睡眠状态,直到锁可用。此调用会自动释放 lk->lk 自旋锁,并挂起当前进程,使其他进程可以运行。
  }
  lk->locked = 1; //一旦锁可用,当前进程被唤醒并重新获取 lk->lk 自旋锁。将 locked 设置为 1,表示锁已被当前进程持有。
  lk->pid = myproc()->pid;
  release(&lk->lk);
}

void
releasesleep(struct sleeplock *lk)
{
  acquire(&lk->lk);
  lk->locked = 0;
  lk->pid = 0;
  wakeup(lk);
  release(&lk->lk);
}