【翻译】【多线程】SuspendThread到底是如何把线程暂停住的?
原文网址:https://www.dcl.hpi.uni-potsdam. ... ndthread-really-do/作者:Martin von Löwis
(作者名字音译:马丁·凡·路易斯)
感谢原作者对此做出的努力。
Thanks to Martin, we got the answers.
最近,包括我在内的很多人,问了我一个问题:当你调用SuspendThread的时候,它是立即将线程暂停住,还是得让线程运行一段时间后才能被暂停住?(同样的问题也有TerminateThread是否立即将线程干掉等,答案和现在的话题差不多)
在单线程系统下我们可以说SuspendThread会立即把指定线程暂停住:至少要被暂停的那个线程(如果不是当前线程)此时肯定没有在运行。但是在多处理器(多核、多线程)的情况下,事情就会变得没那么简单,首先线程就有可能在不同的处理器中运行——那么,我们得花多久才能把别的线程暂停住,并且,当SuspendThread返回的时候,目标线程真的已经被暂停了吗?
让我们看一下NtSuspendThread的代码(在psspnd.c里面)首先它检查要被暂停的线程是不是当前线程,然后立即调用KeSuspendThread。如果是其它线程,它首先取得Thread->RundownProtect,来防止这个正在被操作的线程被删除。KiSuspendThread (在thredobj.c)检查Thread->SuspendCount的值来判断它是否超出了MAXIMUM_SUSPEND_COUNT的值,超出的话报错退出(STATUS_SUSPEND_COUNT_EXCEEDED)。否则,它用以下的代码来暂停目标线程:
02179 //
02180 // Don't suspend the thread if APC queuing is disabled. In this case the
02181 // thread is being deleted.
02182 //
02183
02184 if (Thread->ApcQueueable == TRUE) {
02185
02186 //
02187 // Increment the suspend count. If the thread was not previously
02188 // suspended, then queue the thread's suspend APC.
02189 //
02190 // N.B. The APC MUST be queued using the internal interface so
02191 // the system argument fields of the APC do not get written.
02192 //
02193
02194 Thread->SuspendCount += 1;
02195 if ((OldCount == 0) && (Thread->FreezeCount == 0)) {
02196 if (Thread->SuspendApc.Inserted == TRUE) {
02197 KiLockDispatcherDatabaseAtSynchLevel();
02198 Thread->SuspendSemaphore.Header.SignalState -= 1;
02199 KiUnlockDispatcherDatabaseFromSynchLevel();
02200
02201 } else {
02202 Thread->SuspendApc.Inserted = TRUE;
02203 KiInsertQueueApc(&Thread->SuspendApc, RESUME_INCREMENT);
02204 }
02205 }
02206 }
当它完成的时候,它直接就返回了(并且在返回之前把之前入口处取得的调度数据库释放了)。
正要被暂停的线程在这里只是被SuspendThread安排了一个用来暂停它的“异步处理调用”(APC),它并没有真正的直接去暂停目标线程。通常,我们可以预料到APC会在下一次系统调度线程的时候被调用;此时线程已经在一个不同的处理器上运行了,然后它就进入了暂停的状态。
但让我们再进一步观察,Thread->SuspendApc的值是多少?它被KeInitThread用以下的代码初始化:
00178 //
00179 // Initialize the kernel mode suspend APC and the suspend semaphore object.
00180 // and the builtin wait timeout timer object.
00181 //
00182
00183 KeInitializeApc(&Thread->SuspendApc,
00184 Thread,
00185 OriginalApcEnvironment,
00186 (PKKERNEL_ROUTINE)KiSuspendNop,
00187 (PKRUNDOWN_ROUTINE)KiSuspendRundown,
00188 KiSuspendThread,
00189 KernelMode,
00190 NULL);
00191
00192 KeInitializeSemaphore(&Thread->SuspendSemaphore, 0L, 2L);
于是这里注册了这个APC的三个功能:内核过程KiSuspendNop(它啥也不做),KiSuspendRundown(如果线程在APC运行之前终止,则清除APC),KiSuspendThread,这个负责实际的暂停操作,它是这样定义的:
01667 {
01668
01669 PKTHREAD Thread;
01670
01671 UNREFERENCED_PARAMETER(NormalContext);
01672 UNREFERENCED_PARAMETER(SystemArgument1);
01673 UNREFERENCED_PARAMETER(SystemArgument2);
01674
01675 //
01676 // Get the address of the current thread object and Wait nonalertable on
01677 // the thread's builtin suspend semaphore.
01678 //
01679
01680 Thread = KeGetCurrentThread();
01681 KeWaitForSingleObject(&Thread->SuspendSemaphore,
01682 Suspended,
01683 KernelMode,
01684 FALSE,
01685 NULL);
01686
01687 return;
01688 }
所以这个基本上就是在遇到暂停信号后不返回了,直到收到了继续信号。为了支持多重交织的SuspendThread的调用,它用了一个计数器,所以第一次调用SuspendThread会把暂停信号安排到APC,而最后一次ResumeThread则会吧继续信号发过去。
回到主题,我们的问题是:它是立即暂停,还是只有在APC得到处理后才会暂停?KiInsertQueueApc首先做了一件我们能理解的事情:插入APC到线程的APC队列。然而,然后它运行的代码看起来是想让APC立即运行。它是这样开始的:
00489 // If the APC index from the APC object matches the APC Index of
00490 // the thread, then check to determine if the APC should interrupt
00491 // thread execution or sequence the thread out of a wait state.
00492 //
00493
00494 if (Apc->ApcStateIndex == Thread->ApcStateIndex) {
什么是“APC state index”(APC状态索引),它的值到底是多少?我并不真的知道;对于APC,它应该是OriginalApcEnvironment。这也应该是目标线程的ApcStateIndex的值(所以这种情况通常是对的),但我并没有搞懂APC环境的概念(也找不到任何能说得过去的解释)。
任何情况,假定条件为True,它就会去检测我们是否给当前线程安排了APC,如果有这个APC,它就立即执行一个APC_LEVEL软中断。否则(比如给别的线程安排APC),我们就锁定调度数据库,从RequestInterrupt=FALSE开始,执行下面的这一段复杂的代码:
00531 if (ApcMode == KernelMode) {
00532
00533 //
00534 // Thread transitions from the standby state to the running
00535 // state can occur from the idle thread without holding the
00536 // dispatcher lock. Reading the thread state after setting
00537 // the kernel APC pending flag prevents the code from not
00538 // delivering the APC interrupt in this case.
00539 //
00540 // N.B. Transitions from gate wait to running are synchronized
00541 // using the thread lock. Transitions from running to gate
00542 // wait are synchronized using the APC queue lock.
00543 //
00544 // N.B. If the target thread is found to be in the running state,
00545 // then the APC interrupt request can be safely deferred to
00546 // after the dispatcher lock is released even if the thread
00547 // were to be switched to another processor, i.e., the APC
00548 // would be delivered by the context switch code.
00549 //
00550
00551 Thread->ApcState.KernelApcPending = TRUE;
00552 KeMemoryBarrier();
00553 ThreadState = Thread->State;
00554 if (ThreadState == Running) {
00555 RequestInterrupt = TRUE;
00556
00557 } else if ((ThreadState == Waiting) &&
00558 (Thread->WaitIrql == 0) &&
00559 (Thread->SpecialApcDisable == 0) &&
00560 ((Apc->NormalRoutine == NULL) ||
00561 ((Thread->KernelApcDisable == 0) &&
00562 (Thread->ApcState.KernelApcInProgress == FALSE)))) {
00563
00564 KiUnwaitThread(Thread, STATUS_KERNEL_APC, Increment);
00565
00566 } else if (Thread->State == GateWait) {
00567 KiAcquireThreadLock(Thread);
00568 if ((Thread->State == GateWait) &&
00569 (Thread->WaitIrql == 0) &&
00570 (Thread->SpecialApcDisable == 0) &&
00571 ((Apc->NormalRoutine == NULL) ||
00572 ((Thread->KernelApcDisable == 0) &&
00573 (Thread->ApcState.KernelApcInProgress == FALSE)))) {
00574
00575 GateObject = Thread->GateObject;
00576 KiAcquireKobjectLock(GateObject);
00577 RemoveEntryList(&Thread->WaitBlock.WaitListEntry);
00578 KiReleaseKobjectLock(GateObject);
00579 if ((Queue = Thread->Queue) != NULL) {
00580 Queue->CurrentCount += 1;
00581 }
00582
00583 Thread->WaitStatus = STATUS_KERNEL_APC;
00584 KiInsertDeferredReadyList(Thread);
00585 }
00586
00587 KiReleaseThreadLock(Thread);
00588 }
00589
00590 } else if ((Thread->State == Waiting) &&
00591 (Thread->WaitMode == UserMode) &&
00592 (Thread->Alertable || Thread->ApcState.UserApcPending)) {
00593
00594 Thread->ApcState.UserApcPending = TRUE;
00595 KiUnwaitThread(Thread, STATUS_USER_APC, Increment);
00596 }
所以让我们来分析一下状况:
[*]ApcMode == Kernel? 是,暂停线程的APC是作为一个内核APC创建的。
[*]ThreadState == Running? 仅当这个线程在另一个处理器上运行的时候,是这样。此时我们把RequestInterrupt设为true。
[*]ThreadState == Waiting? 如果要暂停的线程已经卡在WaitForSingleObject的时候是这样。
[*]ThreadState == GateWait? 不知道这是啥。
[*]Thread->SpecialApcDisable==0? 看起来平时一般都应该是这样,至少线程一般都允许有这样的APC产生。
[*]Apc->NormalRoutine == NULL? 并不,我们有一个NormalRoutine(就是KiSuspendThread)
[*]其他情况 (WaitIrql, KernelApcDisable, KernelApcInProgress)? 如果目标线程此时正在内核的临界区做点什么的话这些应该是false。
所以如果目标线程在内核模式运行的话,它还是要继续运行一下的;如果它卡住了,它会醒来执行一下它的APC。
然后它释放调度锁,并且运行下面的代码:
00604 if (RequestInterrupt == TRUE) {
00605 KiRequestApcInterrupt(Thread->NextProcessor);
00606 }
KiRequestApcInterrupt接下来会检测它是不是在同一个处理器上(我们的情况是它可以不在同一个处理器上),然后发送一个APC_LEVEL的“处理器间中断”到线程所在的处理器(通过KiIpiSend来发送,KiIpiSend调用了HAL)。这将会让目标处理器进入内核模式,然后找到这个APC并处理它。
所以总而言之:
[*]如果目标线程卡在了内核模式,它会先执行完内核模式的代码,然后在回到用户模式之前,它处理APC队列的时候,线程就暂停了。
[*]如果目标线程在另一个处理器上,一个IPI会把它中断,此时它就暂停了。
在这两种情况,SuspendThread会在目标线程真的暂停之前就立即返回了——因为它并没有专门去等这个线程中断了才会返回,因为线程仍然需要处理其APC队列,或者另一种情况就是它发出了IPI,但并不等待中断处理,就立即返回了。
2009.1.19
所以SuspendThread()并不是用来进行线程管理的,不然非常容易造成临界区死锁。它只是在调试的时候,你不得不把一个线程停住的时候才用上的。而自己对自己的程序进行多线程调度管理的时候,是不应该用这个函数来暂停自己的线程的——用Sleep()、SwitchToThread()、WaitForSingleObject()或者WaitForMultipleObjects()才是正确的选择。
MSDN上的内容:
This function is primarily designed for use by debuggers. It is not intended to be used for thread synchronization. Calling SuspendThread on a thread that owns a synchronization object, such as a mutex or critical section, can lead to a deadlock if the calling thread tries to obtain a synchronization object owned by a suspended thread. To avoid this situation, a thread within an application that is not a debugger should signal the other thread to suspend itself. The target thread must be designed to watch for this signal and respond appropriately.
参考资料:
https://blogs.msdn.microsoft.com/oldnewthing/20150205-00/?p=44743 本帖最后由 tangptr@126.com 于 2017-2-21 00:21 编辑
简单说,就是SuspendThread这个函数通过插入APC的方式命令指定线程等待Semaphore
话说von是德国人和匈牙利人特有的名字,就像荷兰人特有van一样,前者是“冯”,后者是“范”
比如冯诺依曼:von Neumann
再比如范佩西:van Persie 老湿!我想知道这个源代码是从哪里来的?是自己逆向的还是微软公开的? tangptr@126.com 发表于 2017-2-21 00:18
简单说,就是SuspendThread这个函数通过插入APC的方式命令指定线程等待Semaphore
话说von是德国人和匈牙利 ...
我以为是法国人的,所以翻译了个“凡”
页:
[1]