Appearance
可重入锁
似乎可以通过使用可重入锁来避免一些死锁和锁排序挑战,可重入锁也称为递归锁。其思想是,如果锁由一个进程持有,并且该进程再次尝试获取该锁,那么内核可以允许这样做(因为该进程已经拥有该锁),而不是像 xv6 内核那样调用 panic。
然而,事实证明,可重入锁使得对并发的推理更加困难:可重入锁打破了锁使临界区相对于其他临界区是原子的直觉。考虑以下函数 f
和 g
,以及一个假设的函数 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
恰好调用了 g
,call_once
将被调用两次。
如果不允许可重入锁,那么 h
调用 g
会导致死锁,这也不是很好。但是,假设调用 call_once
两次是一个严重的错误,那么死锁是更可取的。内核开发人员会观察到死锁(内核 panic)并可以修复代码以避免它,而调用 call_once
两次可能会悄无声息地导致一个难以追踪的错误。
因此,xv6 使用更容易理解的非可重入锁。然而,只要程序员牢记锁定规则,任何一种方法都可以工作。如果 xv6 要使用可重入锁,就需要修改 acquire
来注意锁当前正被调用线程持有。还需要在 struct spinlock 中添加一个嵌套获取的计数,风格类似于接下来讨论的 push_off
。