FreeBSD 有一些关于这个主题的精彩笔记,可能是一个很好的案例研究。你可以在这里阅读更多:
http://www.freebsd.org/doc/en/books/arch-handbook/smp-design.html
在锁定的主题上,他们更小心地使用更细粒度的锁定,这样抢占可能只会在持有锁之外发生。
在抢占锁持有线程会导致正确性问题的情况下,他们提到他们有一个 API 用于指示线程暂时处于不可中断区域:
虽然锁可以在抢占的情况下保护大多数数据,但并非所有内核都是抢占安全的。例如,如果一个持有自旋互斥锁的线程被抢占,而新线程试图抢占同一个自旋互斥锁,则新线程可能永远自旋,因为被中断的线程可能永远没有机会执行。此外,某些代码(例如在 Alpha 上的 exec 期间为进程分配地址空间号的代码)不需要被抢占,因为它支持实际的上下文切换代码。通过使用临界区禁用这些代码段的抢占。
和:
临界区 API 的职责是防止临界区内部的上下文切换。对于完全抢占式内核,除当前线程之外的线程的每个 setrunqueue 都是一个抢占点。一种实现是为critical_enter 设置一个由其对应项清除的每个线程标志。如果 setrunqueue 使用此标志集调用,则无论新线程相对于当前线程的优先级如何,它都不会抢占。但是,由于自旋互斥体中使用临界区来防止上下文切换并且可以获得多个自旋互斥体,因此临界区 API 必须支持嵌套。出于这个原因,当前实现使用嵌套计数而不是单个每个线程标志。
因此,在您的示例中,如果您有一个可抢占的线程持有对调度程序很重要的锁,您可能会将该线程标记为暂时不可抢占。
即使在应用程序级软件中,您也可以找到这种方法的相似之处。.Net 有一个称为约束执行区域[1] [2] [3]的概念,虽然它们与调度无关,但用于向 VM 发出信号,表明某些即将执行的代码块必须'原子地'执行,并且 VM 应该推迟它可能执行的任何 Thread.Abort()s,并确保代码可以完成(确保方法已经 JIT'd 并且有足够的堆栈空间)。不同的目的,但类似的粗略想法 - 告诉调度霸主“如果你以奇怪的方式打断我,你可能会破坏正确性”。
当然,在内核抢占或 .Net CER 的情况下,开发人员需要正确识别出现关键执行区域的所有区域,以确保强制执行某些锁定不变量。
FreeBSD 有他们用来帮助调试这类问题的工具,以帮助识别例如死锁。一种特殊的技术是锁排序——每个锁都有一个特定的优先级;当你抓住锁时,你记录下当前的锁优先级。然后,如果您尝试获取低于当前优先级的锁,您就知道您违反了锁顺序,您应该通过记录操作系统故障来通知用户。
对锁排序的需求可能不会立即显现出来,但考虑一下流行的死锁示例 - 由两个锁保护的 2 个资源:
线程 A 想要获取锁 1 和 2。线程 B 想要获取锁 1 和 2.. 但是:
- 线程 A 获取锁 1
- 线程 B 获取锁 2
- 线程 A 试图抓住锁 2,但不能
- 线程 B 试图抓住锁 1,但不能。
- 实现了死锁。
线程没有以一致的顺序获取锁。线程 B 实际上应该注意到他在尝试获取锁 1 时持有锁 2;从 1 < 2 开始,他的锁就乱了,应该中止。Viola,可以避免死锁,或者至少可以发现和修复。