理解在试图绕过规则时规则背后的原理
摘要
本文来自微软的《老东西》博客,解释了Windows内核回调函数最佳实践背后的原理,特别是为什么阻塞或等待工作项会违背其目的,并通过一个关于驱动程序导致系统挂起的警示故事来说明。
<p>在<a href="https://learn.microsoft.com/en-us/windows-hardware/drivers/kernel/windows-kernel-mode-process-and-thread-manager#best-practices-for-implementing-process-and-thread-related-callback-functions">实现进程和线程相关回调函数的最佳实践</a>文档中,明确指出:</p>
<blockquote class="q">
<ul>
<li>保持例程简短而简单。</li>
<li>不要调用用户模式服务来验证进程、线程或映像。</li>
<li>不要进行注册表调用。</li>
<li>不要进行阻塞和/或进程间通信(IPC)函数调用。</li>
<li>不要与其他线程同步,因为这可能导致重入死锁。</li>
</ul>
</blockquote>
<p>到目前为止一切都好。这些回调函数似乎需要快速运行,并且不能阻塞。这些回调是在进程启动或退出、线程启动或退出、DLL或EXE加载或卸载以及各种其他低级事件时调用的。</p>
<p>上述各种禁止项表明,这些回调是在进程创建/终止序列期间被调用的,因此如果你花费很长时间处理它们,就会拖慢整个系统。而像“不要进行注册表调用”这样相当严格的要求,暗示它们甚至可能在系统持有内部锁时被调用。</p>
<p>最佳实践列表继续写道:</p>
<blockquote class="q">
<ul>
<li>使用系统工作线程来排队工作,特别是涉及以下情况的工作:
<ul>
<li>慢速API或调用其他进程的API。</li>
<li>任何可能中断核心服务线程的阻塞行为。</li>
</ul>
</li>
</ul>
</blockquote>
<p>好吧,这提供了一个建议,说明如何将耗时的工作卸载到回调外部运行的代码中。这再次强调回调本身需要快速且尽量不阻塞。</p>
<p>我在企业支持部门的同事经常遇到系统挂起的原因是由于驱动程序违反了回调必须快速返回的规则。例如,一个常见的反模式是驱动程序的回调开始时遵循上述指导将工作排队到系统工作线程,但随后它们阻塞直到工作项完成。</p>
<p>这是一个遵守规则却不理解规则存在原因的例子。</p>
<p>规则是回调需要快速执行并尽快返回。驱动程序遵循了规则的文字,将工作委托给系统工作线程,而且没有规则说“不要等待工作项”,所以他们一定认为这为他们执行同步长时间运行的工作提供了漏洞。</p>
<p>但规则“不要进行阻塞和/或进程间通信(IPC)函数调用”和“不要与其他线程同步,因为这可能导致重入死锁”明确表示你不应该长时间阻塞在回调中。这些“禁止项”只是指出了你的回调可能阻塞的一些常见方式。</p>
<p>文档似乎在2020年进行了更新,明确指出了这种情况:</p>
<blockquote>
<ul>
<li>如果使用系统工作线程,不要等待工作完成。这样做会破坏将工作排队以异步完成的目的。</li>
</ul>
</blockquote>
<p>有人可能会争辩说,这条规则已经被“不要与其他线程同步”这条规则涵盖了,但我猜驱动程序供应商将其解释为“但我不是在与其他线程同步。我是在同步一个事件!”但当然,事件是由另一个线程设置的,所以你实际上是在与其他线程同步。</p>
<p>我在企业支持部门的同事将此描述为“不是我,是我兄弟”的借口。父母告诉你不要打开电视机,所以你让你兄弟去做。从技术上讲,你没有打开电视机,但实际上你开了,因为你兄弟是按你的指示行事的。(这就是为什么合同中经常包含“不得披露或导致披露”这样的措辞,这样你就不能说“不,我完全没有披露。我把信息给了鲍勃,是鲍勃披露的!”)</p>
<p>文档应该以这样的内容开头:</p>
<blockquote class="q"><p>回调函数必须快速执行其工作,不得阻塞。如果需要执行复杂工作或与其他线程或进程同步,请异步完成工作,例如使用系统工作线程。</p></blockquote>
<p>然后可以列出一些被视为阻塞的例子。</p>
<blockquote class="q"><p>从回调函数中不允许的一些阻塞示例:</p></blockquote>
<p>然后可以补充附加约束。</p>
<blockquote class="q"><p>此外,回调函数不得执行以下任何操作:</p></blockquote>
<p>本文首发于<a href="https://devblogs.microsoft.com/oldnewthing">The Old New Thing</a>,原文链接:<a href="https://devblogs.microsoft.com/oldnewthing/20260611-00/?p=112415">Understanding the rationale behind a rule when trying to circumvent it</a>。</p>
查看缓存全文
缓存时间: 2026/06/12 14:50
# 理解规则背后的逻辑,以便在规避时更有依据 - The Old New Thing
来源:https://devblogs.microsoft.com/oldnewthing/20260611-00?p=112415
在[实现进程和线程相关回调函数的最佳实践](https://learn.microsoft.com/en-us/windows-hardware/drivers/kernel/windows-kernel-mode-process-and-thread-manager#best-practices-for-implementing-process-and-thread-related-callback-functions)文档中,明确指出:
> - 保持例程简短精炼。
> - 不要调用用户模式服务来验证进程、线程或映像。
> - 不要进行注册表调用。
> - 不要进行阻塞和/或进程间通信(IPC)函数调用。
> - 不要与其他线程同步,因为这可能导致重入死锁。
到此为止,一切都很合理。这些回调函数似乎需要快速执行,且不能阻塞。这些回调在进程启动或退出、线程启动或退出、DLL或EXE加载或卸载,以及其他各种底层事件发生时被调用。
上述各种禁止项表明,这些回调是在进程创建/终止序列期间被调用的,因此如果处理时间过长,就会拖慢整个系统的速度。而像“不要进行注册表调用”这样极端的限制,则暗示调用时系统可能持有内部锁。
最佳实践列表继续写道:
> - 使用系统工作者线程来排队工作,尤其是涉及以下情况的工作:
> - 慢速API或调用其他进程的API。
> - 任何可能中断核心服务线程的阻塞行为。
好的,这提供了一种建议:如何将耗时工作卸载到回调之外运行的代码中。这再次强调了回调本身必须快速且最小化阻塞。
我在企业支持部门的同事经常遇到系统挂起的情况,原因就是驱动程序违反了这些回调必须快速返回的规则。例如,一个常见的反模式是:驱动程序的回调一开始确实遵循上述指导,将工作排队给系统工作者线程,但随后它一直阻塞,直到该工作项完成。
这是“只遵守规则却不理解规则背后的原因”的典型案例。
规则要求回调必须快速且尽快返回。驱动程序表面遵守了法律条文,把工作委托给了系统工作者线程,而规则并没有明确说“不要等待工作项”,所以他们可能认为自己找到了一个执行同步长时间工作的漏洞。
但是,“不要进行阻塞和/或进程间通信(IPC)函数调用”以及“不要与其他线程同步,因为这可能导致重入死锁”这两条规则已经清楚地表明:你不应该在回调中长时间阻塞。“不要”只是列举了一些常见的造成阻塞的方式。
看起来文档在2020年已经更新,专门指出了这个具体案例:
> - 如果使用系统工作者线程,请不要等待工作完成。这样做会破坏将工作异步排队的初衷。
有人可能会争辩说,这条规则已经被“不要与其他线程同步”这条规则涵盖了,但估计驱动程序供应商将其理解为:“但我不是在与其他线程同步,我只是在等待一个事件!” 当然,事件是由另一个线程设置的,所以实际上你就是在与其他线程同步。
我的企业支持同事将这种行为描述为“不是我,是我兄弟”的借口。父母不让你开电视,于是你让你兄弟去开。严格来说,你没有开电视,但实际上是你开的,因为你兄弟是按你的指令行事。(这就是为什么合同里经常出现“不得披露或导致披露”这样的措辞,免得有人说“不,我根本没披露。我只是把信息给了Bob,是Bob披露的!”)
文档的开头应该这样写:
> 回调函数必须快速执行其工作,不得阻塞。如果需要执行复杂工作或与其他线程/进程同步,请异步完成工作,例如使用系统工作者线程。
然后可以列出一些被视为阻塞的例子。
> 从回调函数中不允许进行的阻塞示例如下:
接着再补充其他约束。
> 此外,回调函数不得执行以下任何操作:
### 分类
### 主题
## 作者
Raymond Chen
Raymond已参与Windows的演进超过30年。2003年,他创办了一个名为“The Old New Thing”的网站,其受欢迎程度远超他最疯狂的想象——这一进展至今仍让他感到毛骨悚然。该网站后来还衍生出一本同名书籍(Addison Wesley, 2007)。他偶尔会在Windows Dev Docs的Twitter账号上讲故事,但这些故事并不传达任何有用信息。
相似文章
如果C#和JavaScript允许我多次等待Windows Runtime异步操作,为什么C++/WinRT不行?
Raymond Chen解释了为什么C++/WinRT不像C#、JavaScript和Python那样允许多次等待异步操作,其原因是没有标准库的task类型,以及不为你未使用的功能付费的原则。
为何不根据链接的SDK来改变API行为?
本文以Windows的CoInitializeSecurity为例,探讨了根据链接的SDK版本改变API行为的陷阱。讨论了DLL版本不匹配和尾调用优化等问题使这种方法复杂化。
用户更改键盘布局时程序挂起的问题
一个调试故事,讲述了当用户更改键盘布局(例如使用 Win+Space 快捷键)时,Windows 程序挂起的原因,是由于一个后台线程创建了窗口但没有泵送消息。修复方法是要么泵送消息,要么销毁窗口。
除了解决问题,什么办法都试过了
一篇博客文章,描述了行业资深人士发出的代码滥用警告如何导致关键工作流失败,但同事们没有修复滥用问题,而是提出了各种变通方案,比如添加更多输出处理器或抑制警告——这凸显了工程领域普遍回避解决根本问题的倾向。
如果你只是坐在那里什么都不做,至少也要正确地什么都不做
这篇文章来自 The Old New Thing,解释了使API变得'惰性'的概念——以一种不破坏现有应用的方式'什么都不做'——并使用在Xbox上支持打印和淘汰widget API等例子。