不可能同步的RING0->RING3调用
 

作者:陆麟
转载请征得作者同意.
2001.6.29



在今年的开发中, 曾经接到过要求: 在文件系统调用中, 停止当前线程的运行, 将CPU交付给另外进程, 当另外进程进行处理后, 恢复当前线程执行.
当时这个课题花费了我1个月时间. 最终得出结论. 不可能.
现在, 几乎每个月都会有相同的问题出现在BBS, NEWSGROUP中. 现在我们就看一下这个问题的的实际情况.
在本主页的47.WINDOWS9X文件读写Internal 中, 我已经详细讲述了基本问题所在. 由于文件系统调用中都会出现UNDOCUMENTED的EnterMustComplete调用.所以这是第一个问题所在. 但是KERNEL的EnterMustComplete实际上是可以被BlockOnID搞定的, 所以, 问题并不大. 而且从SuspendThread函数的实际实现和IFSMgr_Block的实际实现来看, 他们都是通过对lockOnID函数的调用实现对线程的阻塞的. (注,VWIN32在某些特定情况下通过UNDOCUMENTED KERNEL APC调用来确保线程在任何情况下都能被BlockOnID阻塞)所以,阻塞线程是没有问题的.但是在80%情况下, 当前线程被阻塞后CPU控制却没有转移到其他线程或进程. 这是因为线程调度本身由2个调度器决定. VMM本身控制着基本线程调度. 但是同时 KERNEL32模块提供了WIN32运行时的动态线程调度. 它是RING3级的线程调度员. 如果KERNEL32模块中有某些全局资源被当前线程占有, 就会阻止线程切换.
这一点我们可以通过试验证明:
如果我们尝试打开一个位于软盘上的文件, 可能会等待很长的时间. 我们从IOS和的VFAT运行代码中可以看到. VFAT会调用IFSMgr_Block来停止当前线程执行. 该调用的本意是让其他线程可以开始执行. 如果有其他线程需要CPU,就将CPU让给其他线程,减少用户等待. 但是实际上从用户这里看来, 除了等待文件被打开, 根本没有其他动作可以进行. 因为KERNEL32在打开文件时会将某全局资源锁定给当前线程. 这时, 即使Primary Scheduler将CPU切换给其他线程, KERNEL32将阻止该线程运行, 因为其他线程调用API后,会因为在尝试占有该全局资源时被阻塞.其他线程只有在原先的线程被VFAT解放后才有机会得到CPU执行时间.
如果我们强行调用BlockOnID,会导致死锁.甚至在RING3我们同样会遇到该问题. 我在拦截了CreateFileA时, 当调度一个事件,尝试切换到其他线程, 同样导致了死锁. 有时甚至导致系统崩溃. 这是个十分复杂的情况. 假定API 1占有资源A,B. 而API 2会按照B,A的顺序占有资源. 这时,系统不仅会死锁, 而且进入不稳定状态, 在任意的时刻就会崩溃...
接下来看看世界顶级大师们的见解:
这段来自于Walter Oney, Windows 95 system Programming的作者.顶尖人物之一. 此信是回给Ian Ash的, Ian Ash同样遇到了我的问题, 但是他求助了NEWSGROUP...
As I told the original poster in response to his private e-mail, you're pretty much guaranteed to deadlock if you try to dive back into ring 3 and block while handling a file system request. KERNEL32 owns a Mutex across its call to ring-0 to handle file system requests, and the same Mutex is required in order to process the APC in some cases. It's simply not safe to rely on ring-3 routines to act as a subroutine for a VxD. Microsoft never designed the system to work that way, and it's silly to try.

下面这段来自微软:"Martin Borve" <[email protected]>,乃是权威的解释.
FSDs should never call back into Win32 to complete an I/O request.  Win32 will sometimes take ownership of an internal mutex (not the Win16mutex) before making I/O requests, and not release the mutex until after the I/O operation completes.  One effect of holding this mutex is that it prevents any thread switches, which means you will deadlock when you try to call
back into Win32.  FSDs should only complete an I/O request in ring 0, or in V86 mode (such as the real-mode mapper).  FSDs should never rely on Win32 code.
注意, IFS HOOK同样被视为FSD的扩展.
当然, 这些解释并不仅仅能应用于文件系统实践中. 其实在任何时候, 一个RING0->RING3其他线程的同步依赖关系都有可能面对死锁的问题. 当RING3会有调用WIN API的时候, 几乎无需考虑, 一定会有这类问题出现.
在同一线程下, 问题会小点, 但是由于VMM仅支持有限的重入, 如何确保自己每次被调用时VMM都处于没有重入状态呢? 无法确保. 所以, RING0->RING3的同一线程的同步调用同样存在问题, 只不过关键转移为VMM重入了而已...