Linux 0.11中进程睡眠和唤醒机制思考

时间:2021-02-26 14:55:23

在Linux 0.11中,当进程尝试访问一个边界数据时,有可能由于资源已经被占用而进入睡眠状态。当资源被释放后,就需要把睡眠的进程唤醒。

我们先来看一个包括睡眠和唤醒步骤的实际情况:
1. 进程A在访问硬盘某区块bh时发现这一块并不在告诉缓存中,进而发起请求读取硬盘(此时不一定会立即调度)。
2. 随后进程调度运行进程B(这里存在两种前提,一种是A自己必须要等待该资源bh从而主动挂起,这时A会把自己挂在该资源bh的等待队列中,此时A进程的唤醒可以参考后续讨论;另一种是A进程的时间片为0,则A暂时被系统挂起,那么再运行A的情况可能非常复杂,这里不再讨论),进程B同样要使用该资源bh,发现该资源被锁定后调用sleep_on()进行睡眠,并继续调度并运行C。此时隐式等待队列为*p=B(p=&bh->b_wait,即p指向资源bh的等待队列b_wait)
3. C也要访问该资源,睡眠后再调度运行D。此时隐式等待队列为*p=C, C->tmp=B
4. D同样要访问该资源,睡眠后再调度其他进程。这时,组成了一个等待队列*p=D, D->tmp=C, C->tmp=B
5. 当硬盘资源请求结束后,CPU调用end_request()函数,其中的wake_up()就开始唤醒p指向的头进程,即进程D。但此时是在当前运行进程的中断过程中,并不会直接调度执行D,而是会返回正在运行的进程。
6. 过了一段时间后进程调度到D时,D会从挂起的地方,也就是从sleep_on()调用schedule()返回后的地方开始执行,然后,在D进程运行到sleep_on()的164行时,会唤醒C进程。同样,此时不会立即调度运行C。
7. 又过了一段时间,进程调度到C时,C会从挂起的地方,同样是sleep_on()调用schedule()返回后的地方开始执行。同样,C会在运行到sleep_on()的164行时,唤醒B进程。同样此时不会立即调度运行B。至此,所有之前睡眠的进程都已经被唤醒(如果考虑步骤2中两种前提中的第一种,此时还有A进程在睡眠状态,而A进程的唤醒要在B进程被调度执行时完成)。

下面考虑一个更复杂的情况:
如果在步骤5和6之间,有其他进程修改该资源,导致该资源又被锁定。然后运行步骤6,那么此时的等待队列是如何排列的呢?

在《Linux内核完全注释》中,作者说Linux 0.11在sleep_on()和wake_up()中各有一些错误,在原代码的基础上,wake_up()需要删除一行代码*p=NULL,sleep_on()需要加入一行代码*p=tmp
按照这种改法,在sleep_on()退出后,等待队列就变为*p=C, C->tmp=B。而且在调用sleep_on()时,都有用while循环检查锁定标志,发现被锁定后,继续调用sleep_on(),会重新睡眠进程D,把其加入等待队列的头部,此时等待队列看似和步骤4中的一致,同样都是*p=D, D->tmp=C, C->tmp=B,但在schedule()之前,C是被唤醒的状态,这点和步骤4大不一样,后续有可能先调度C。假如C被调度,那么就会唤醒B,但如果此时资源依旧被锁定,那么C会重新加入等待队列,队列就变成了 *p=C, C->tmp=D, D->tmp=C, 此时C就在队列中出现了2次。

而我认为作者所说的改动没有必要,而且wake_up()中的代码*p=NULL是整套机制中一个很关键的地方。
在步骤5中通过wake_up()唤醒进程D时,*p=NULL清空等待队列。这样后续步骤中,等待队列为空,也就不需要在进程唤醒时把*p指向当前进程的tmp,即下一个需要唤醒的进程。而且进程D,C,B之间依旧保持一种隐性的级联关系,即D->tmp=C, C->tmp=B,唤醒的顺序依旧不变。
就算在步骤6之前,有人锁定了资源bh,那么在D唤醒后重新检查资源的锁定标志,发现被锁定后重新调用sleep_on()睡眠进程D,此时就变成了*p=D, D->tmp=NULL。而该逻辑不影响C,即C已经在就绪态,很可能被先调度。在调度执行C时,发现该资源被锁定,也会调用sleep_on()睡眠,此时*p=C, C->tmp=D, D->tmp=NULL。这样C进程就不会像之前那样在等待队列中出现2次。
而且,按照这样的逻辑,如果B被调度时同样发现资源被锁定,那么等待队列就变为*p=B, B->tmp=C, C->tmp=D, D->tmp=NULL,整个等待队列和步骤4中的相比完全翻转了。

根据上述讨论,可以看到整套睡眠和唤醒机制中的关键如下:
1. 睡眠隐性队列
2. 隐性队列顺序反转
3. 唤醒时清空等待队列,却不影响唤醒顺序
4. 进程重新执行时仍需查看锁定标志

本文目前只讨论了sleep_on()和wake_up()函数,暂未考虑Linux 0.11中的函数interruptible_sleep_on()。刚在阅读oldlinux论坛中的帖子时,看到这一篇中关于sleep_on()和wake_up()的讨论,讨论的实际情况和我说的稍有不同,不过同样也是支持Linus原代码的设计。
其中给出了一个链接http://www.oldlinux.org/oldlinux/viewthread.php?tid=9466。在这个帖子里,楼主和管理员讨论了interruptible_sleep_on()中的唤醒逻辑bug。我看完后赞同管理员的逻辑。

但是除去interruptible_sleep_on()的bug不说,如果只讨论sleep_on()和wake_up(),我认为还是Linus的原代码是正确的。