技术宅的结界

 找回密码
 立即注册→加入我们

QQ登录

只需一步,快速开始

搜索
热搜: 下载 VB C 实现 编写
查看: 1072|回复: 5
收起左侧

【翻译】【多线程】SuspendThread到底是如何把线程暂停住的?

[复制链接]

995

主题

2207

帖子

5万

积分

用户组: 管理员

一只技术宅

UID
1
精华
197
威望
261 点
宅币
16463 个
贡献
32443 次
宅之契约
0 份
在线时间
1565 小时
注册时间
2014-1-26
发表于 2017-2-20 21:14:14 | 显示全部楼层 |阅读模式

欢迎访问技术宅的结界,请注册或者登录吧。

您需要 登录 才可以下载或查看,没有帐号?立即注册→加入我们

x
原文网址: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[0].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

995

主题

2207

帖子

5万

积分

用户组: 管理员

一只技术宅

UID
1
精华
197
威望
261 点
宅币
16463 个
贡献
32443 次
宅之契约
0 份
在线时间
1565 小时
注册时间
2014-1-26
 楼主| 发表于 2017-2-20 21:19:42 | 显示全部楼层
所以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

23

主题

63

帖子

1532

积分

用户组: 管理员

UID
1043
精华
7
威望
29 点
宅币
1349 个
贡献
27 次
宅之契约
0 份
在线时间
252 小时
注册时间
2015-8-15
发表于 2017-2-21 00:18:53 | 显示全部楼层
本帖最后由 tangptr@126.com 于 2017-2-21 00:21 编辑

简单说,就是SuspendThread这个函数通过插入APC的方式命令指定线程等待Semaphore
话说von是德国人和匈牙利人特有的名字,就像荷兰人特有van一样,前者是“冯”,后者是“范”
比如冯诺依曼:von Neumann
再比如范佩西:van Persie
flowers for Broken spirits - a woman turned into stake will hold the world in the basin of fire.

0

主题

5

帖子

187

积分

用户组: 技术宅的结界VIP成员

UID
2265
精华
0
威望
0 点
宅币
182 个
贡献
0 次
宅之契约
0 份
在线时间
112 小时
注册时间
2017-2-21
发表于 2017-2-21 00:23:31 | 显示全部楼层
老湿!我想知道这个源代码是从哪里来的?是自己逆向的还是微软公开的?

点评

自行搜索WRK,React OS  发表于 2017-2-21 00:32

995

主题

2207

帖子

5万

积分

用户组: 管理员

一只技术宅

UID
1
精华
197
威望
261 点
宅币
16463 个
贡献
32443 次
宅之契约
0 份
在线时间
1565 小时
注册时间
2014-1-26
 楼主| 发表于 2017-2-21 20:20:31 | 显示全部楼层
tangptr@126.com 发表于 2017-2-21 00:18
简单说,就是SuspendThread这个函数通过插入APC的方式命令指定线程等待Semaphore
话说von是德国人和匈牙利 ...

我以为是法国人的,所以翻译了个“凡”

本版积分规则

QQ|申请友链|Archiver|手机版|小黑屋|技术宅的结界 ( 滇ICP备16008837号|网站地图

GMT+8, 2018-9-24 00:51 , Processed in 0.104323 second(s), 40 queries , Gzip On.

Powered by Discuz! X3.2

© 2001-2013 Comsenz Inc.

快速回复 返回顶部 返回列表