(我首先给出我的问题的背景,问题本身位于底部,其中以粗体显示“问题”)。
以两个进程 A 和 B 为例。A 检查条件,发现条件不满足,然后进入睡眠/阻塞状态。 B满足条件并唤醒A。如果一切都按这个顺序发生,那么我们就没有问题。
现在如果调度程序执行以下操作:
- A 检查条件,不满足
- B满足条件,唤醒A
- A 进入睡眠状态/阻塞
那么我们就失去了 B 为 A 执行的唤醒。
我在实现阻塞信号量(即将 wait()ing 线程置于睡眠/阻止它而不是让它旋转等待的信号量)的上下文中遇到了这个问题。多个来源对此提供了解决方案,其中:
Andrew Tanenbaum,《现代操作系统》,第 4 版,第 14 页。 130:
这里问题的本质是发送给尚未休眠的进程的唤醒丢失了。如果没有丢失,一切都会顺利。快速解决方法是修改规则以添加唤醒等待位到图片。当唤醒发送到仍处于唤醒状态的进程时,该位被设置。稍后,当进程尝试进入睡眠状态时,如果唤醒等待位打开,则会将其关闭,但进程将保持唤醒状态。唤醒等待位是用于存储唤醒信号的存钱罐。消费者在循环的每次迭代中清除唤醒等待位。
这篇文章发表在 Linux 杂志上(“Kernel Korner - Sleeping in the Kernel”,Linux Journal #137)提到了类似的内容:
该代码避免了唤醒丢失的问题。如何?在测试条件之前,我们已将当前状态更改为 TASK_INTERRUPTIBLE。那么,发生了什么变化呢?变化在于,每当状态为 TASK_INTERRUPTIBLE 或 TASK_UNINTERRUPTIBLE 的进程调用wake_up_process,并且该进程尚未调用schedule()时,该进程的状态将更改回TASK_RUNNING。
因此,在上面的示例中,即使在检查 list_empty 之后的任何时候进程 B 发出唤醒,A 的状态也会自动更改为 TASK_RUNNING。因此,调用schedule()不会使进程A进入睡眠状态;正如前面所讨论的,它只是将其安排一段时间。因此,唤醒不再丢失。
据我了解,这基本上是说“您可以将一个进程标记为想要进入睡眠/阻止状态,以便稍后的唤醒可以取消稍后的睡眠/阻止调用”。
最后这些讲义在底部的几段中,从“下面的伪代码显示了这种信号量的实现,称为阻塞信号量:”给出了阻塞信号量的代码并使用原子操作“Release_mutex_and_block (csem.mutex);”。他们声称:
请注意,P()ing 进程必须自动变得不可运行并释放互斥体。这是因为存在唤醒丢失的风险。想象一下这是两个不同操作的情况:release_mutex(xsem.mutex) 和 sleep()。如果在release_mutex()和sleep()之间发生上下文切换,则另一个进程可能会执行V()操作并尝试使第一个进程出列_and_wakeup()。不幸的是,第一个进程尚未睡眠,因此它错过了唤醒 - 相反,当它再次运行时,它立即进入睡眠状态,没有人唤醒它。
操作系统通常以 sleep() 系统调用的形式提供这种支持,该调用将互斥体作为参数。然后,内核可以释放互斥锁,并使进程在没有中断(或以其他方式受到保护)的环境中进入睡眠状态。
问题:UNIX 中的进程是否有某种方式将它们标记为“我正计划进入睡眠状态”,或者如 Tanenbaum 所说的“唤醒等待位”?是否有一个系统调用 sleep(mutex) 可以原子地释放互斥体,然后使进程进入睡眠/阻止它?
很明显,我不熟悉系统调用和一般操作系统内部结构;如果我的问题中有任何明显的错误假设或术语的误用,我很乐意让他们向我指出。
答案1
经典的 Unixy 解决方案是让“唤醒”信号包括将一个字节写入等待进程将尝试从中读取的管道。
(如果进程没有可以帮助设置此功能的共同祖先,则可以使用命名管道)。
然后,等待进程可以使用 select() 系统调用(或其最近重新设计的替代方案之一,例如 pselect 或 poll/epoll)原子地执行“等待直到准备好从这样那样的文件描述符中读取”。当它醒来时,它会读取并丢弃所有要读取的内容,并且然后检查哪些工作已准备好进行。