我研究了这个自旋锁的实现,特别是获取和释放功能:
void
acquire(struct spinlock *lk)
{
pushcli(); // disable interrupts to avoid deadlock.
if(holding(lk))
panic("acquire");
// The xchg is atomic.
// It also serializes, so that reads after acquire are not
// reordered before it.
while(xchg(&lk->locked, 1) != 0)
;
lk->cpu = cpu;
getcallerpcs(&lk, lk->pcs);
}
// Release the lock.
void
release(struct spinlock *lk)
{
if(!holding(lk))
panic("release");
lk->pcs[0] = 0;
lk->cpu = 0;
xchg(&lk->locked, 0);
popcli();
}
如果xchg函数是原子函数,那么pushcli()
和popcli()
的目的是什么?原子性难道不能确保在任何实例中只有一个线程能够更改锁的值而不会被中断吗?为什么我们必须明确禁用中断来防止死锁?
原子性难道不能确保在任何实例上只有一个线程能够更改锁的值而不会被中断吗?
是的,但这不是问题所在。该操作确保了线程之间的原子性,但中断处理程序仍然是一个问题。
中断(特别是硬件中断(是异步的,可以在任何时候发生,这意味着代码在生成中断时可以执行任何操作。
当CPU捕捉到中断时,任何正在运行的代码都将被"挂起",并输入中断处理程序。只有在中断处理程序完成其工作后,其他任何操作才能正常进行。
现在,假设您的代码需要acquire()
一个spinlock,函数定义如下:
void acquire(struct spinlock *lk) {
while(xchg(&lk->locked, 1) != 0)
;
lk->cpu = cpu;
}
如果中断发生在上述while
之后(release()
之前(的任何时间,并且中断处理程序需要获取相同的自旋锁,那么它将尝试做同样的事情:再次调用acquire()
,进入while
循环。但是,由于自旋锁已经被持有,循环将永远不会退出。中断处理程序将继续旋转,试图获取一个永远不会释放的锁,因为根据定义,中断处理程序必须执行到它结束,CPU才能继续执行其他操作。这会导致死锁。此时,除了关闭电源并重新启动CPU之外,没有其他事情可做了。
这就是为什么在显示的代码中需要禁用中断的原因。