我一直认为内核不可抢占。也就是说,内核代码运行完成,仅在返回用户空间时才进行调度。我现在很好奇在设计preemptable内核时需要进行的更改。
到目前为止我的思考过程:
假设内核正在代表某些用户空间进程运行,并且突然被抢占。我们将当前处理器信息存储在进程的内核堆栈中。然后我们将该过程标记为可运行。在安排该过程时,我们获取的信息不会将我们带回用户空间,而是让我们继续运行内核任务(例如系统调用)。
但是,有时候内核没有在进程上下文中运行。我们需要以某种方式使内核在这些点上不可抢占。能够在行动中停止调度程序听起来像是疯狂。
我通过锁定闻到了一些可疑的东西:假设我们正在运行一个获取锁定A的系统调用。如果它被预先占用,我们可能会遇到一个问题,如果调度程序需要A。
我想知道我的推理是否有任何错误,或者我是否未能考虑一些关键性的错误。
谢谢
答案 0 :(得分:0)
FreeBSD对这个主题有一些很好的说明,可能是一个很好的案例研究。你可以在这里阅读更多内容:
http://www.freebsd.org/doc/en/books/arch-handbook/smp-design.html
关于锁定的主题,他们更谨慎地使用更细粒度的锁定,这样抢占可能只会在持有锁之外发生。
在抢占锁定线程会导致正确性问题的情况下,他们提到他们有一个API用于指示该线程暂时位于不可中断区域:
虽然锁定可以在抢占的情况下保护大多数数据,但不是全部 内核是抢占安全的。例如,如果一个线程持有 旋转互斥锁被抢占,新线程试图抓住同样的旋转 互斥,新线程可能永远旋转,因为被中断的线程可能 永远不会有机会执行。另外,一些代码如代码 在执行期间为Alpha分配进程的地址空间编号 不需要被抢占,因为它支持实际的上下文切换 码。使用a禁用这些代码段的抢占 关键部分。
和
关键部分API的职责是防止上下文 在关键部分内部切换。使用完全抢先的内核, 除当前线程之外的每个线程的每个setrunqueue都是a 先发制人点。一个实现是critical_enter设置a 每个线程标志由其对应方清除。如果是setrunqueue 使用此标志设置调用,无论如何都不会抢占 新线程相对于当前线程的优先级。然而, 因为关键部分用于旋转互斥体以防止上下文 可以获得开关和多个自旋互斥体,这是至关重要的 section API必须支持嵌套。出于这个原因目前 实现使用嵌套计数而不是单个每个线程 标志。
因此,在您的示例中,如果您有一个可抢占的线程,其中包含对调度程序很重要的锁,则可能会将该线程标记为暂时不可抢占。
即使在应用程序级软件中,您也可以找到此方法的相似之处。 .Net有一个名为约束执行区[1] [2] [3]的概念,虽然它们与调度无关,但它们用于向VM发信号通知某些代码块即将执行必须执行'原子',并且VM应该推迟它可能执行的任何Thread.Abort(),以及确保代码可以完成(确保方法已经JIT并且有足够的堆栈空间)。不同的目的,但一个类似的粗略想法 - 告诉调度霸主“如果你以一种奇怪的方式打断我,你可能会打破正确性。”
当然,在内核抢占或.Net CERs的情况下,开发人员可以正确识别正在执行crtical执行区域的所有区域,以确保强制执行某些锁定不变量。
FreeBSD拥有用于帮助调试这些问题的工具,以帮助识别例如死锁。一种特殊的技术是锁定 - 每个锁具有特定的优先级;当您获取锁定时,您会记录当前锁定优先级。然后,如果您尝试获取低于当前优先级的锁,则表示您已违反锁定顺序,并且应通过记录操作系统故障通知用户。
对锁定顺序的需求可能不会立即显现,但请考虑一个流行的死锁示例 - 受两个锁保护的2个资源:
线程A想要获取锁1和2。 线程B想要获取锁定1和2 ..但是:
线程没有以一致的顺序获取锁。线程B实际上应该注意到他在试图抓住锁1时持有锁2;因为1< 2,他正在抓不上锁,应该中止。中提琴,死锁,或至少被发现和修复。