6.4 死锁和锁排序

如果在内核中执行的代码路径必须同时持有数个锁,那么所有代码路径以相同的顺序获取这些锁是很重要的。如果它们不这样做,就有死锁的风险。假设xv6中的两个代码路径需要锁A和B,但是代码路径1按照先A后B的顺序获取锁,另一个路径按照先B后A的顺序获取锁。假设线程T1执行代码路径1并获取锁A,线程T2执行代码路径2并获取锁B。接下来T1将尝试获取锁B,T2将尝试获取锁A。两个获取都将无限期阻塞,因为在这两种情况下,另一个线程都持有所需的锁,并且不会释放它,直到它的获取返回。为了避免这种死锁,所有代码路径必须以相同的顺序获取锁。全局锁获取顺序的需求意味着锁实际上是每个函数规范的一部分:调用者必须以一种使锁按照约定顺序被获取的方式调用函数。

由于sleep的工作方式(见第7章),Xv6有许多包含每个进程的锁(每个struct proc中的锁)在内的长度为2的锁顺序链。例如,consoleintr (kernel/console.c:138)是处理键入字符的中断例程。当换行符到达时,任何等待控制台输入的进程都应该被唤醒。为此,consoleintr在调用wakeup时持有cons.lockwakeup获取等待进程的锁以唤醒它。因此,全局避免死锁的锁顺序包括必须在任何进程锁之前获取cons.lock的规则。文件系统代码包含xv6最长的锁链。例如,创建一个文件需要同时持有目录上的锁、新文件inode上的锁、磁盘块缓冲区上的锁、磁盘驱动程序的vdisk_lock和调用进程的p->lock。为了避免死锁,文件系统代码总是按照前一句中提到的顺序获取锁。

遵守全局死锁避免的顺序可能会出人意料地困难。有时锁顺序与逻辑程序结构相冲突,例如,也许代码模块M1调用模块M2,但是锁顺序要求在M1中的锁之前获取M2中的锁。有时锁的身份是事先不知道的,也许是因为必须持有一个锁才能发现下一个要获取的锁的身份。这种情况在文件系统中出现,因为它在路径名称中查找连续的组件,也在waitexit代码中出现,因为它们在进程表中寻找子进程。最后,死锁的危险通常是对细粒度锁定方案的限制,因为更多的锁通常意味着更多的死锁可能性。避免死锁的需求通常是内核实现中的一个主要因素。