Skip to content

可重入锁

似乎可以通过使用可重入锁来避免一些死锁和锁排序挑战,可重入锁也称为递归锁。其思想是,如果锁由一个进程持有,并且该进程再次尝试获取该锁,那么内核可以允许这样做(因为该进程已经拥有该锁),而不是像 xv6 内核那样调用 panic。

然而,事实证明,可重入锁使得对并发的推理更加困难:可重入锁打破了锁使临界区相对于其他临界区是原子的直觉。考虑以下函数 fg,以及一个假设的函数 h

c
struct spinlock lock;
int data = 0; // 受 lock 保护

f() {
  acquire(&lock);
  if(data == 0){
    call_once();
    h();
    data = 1;
  }
  release(&lock);
}

g() {
  acquire(&lock);
  if(data == 0){
    call_once();
    data = 1;
  }
  release(&lock);
}

h() {
    ...
}

看这段代码,直觉是 call_once 只会被调用一次:要么由 f 调用,要么由 g 调用,但不会被两者都调用。

但是如果允许可重入锁,并且 h 恰好调用了 gcall_once 将被调用两次

如果不允许可重入锁,那么 h 调用 g 会导致死锁,这也不是很好。但是,假设调用 call_once 两次是一个严重的错误,那么死锁是更可取的。内核开发人员会观察到死锁(内核 panic)并可以修复代码以避免它,而调用 call_once 两次可能会悄无声息地导致一个难以追踪的错误。

因此,xv6 使用更容易理解的非可重入锁。然而,只要程序员牢记锁定规则,任何一种方法都可以工作。如果 xv6 要使用可重入锁,就需要修改 acquire 来注意锁当前正被调用线程持有。还需要在 struct spinlock 中添加一个嵌套获取的计数,风格类似于接下来讨论的 push_off