What does SuspendThread really do?

Windows Research Kernel @ HPI

Recently, I asked myself (and all people around me): When you invoke SuspendCall, will it really suspend the thread immediately, or might it continue to run for some time before it gets suspended? (The same question can be asked for TerminateThread, and, as we will see, it has a similar answer)

In the case of a single-processor system, it is quite plausible that SuspendThread suspends the thread right away: the thread-to-be-suspended is clearly not running at this point. In the multi-processor case, things might be more difficult, as the thread might be running on a different processor - then, how long might it take to suspend it, and: when SuspendThread returns, will it already be suspended?

Let's look at the code: NtSuspendThread (in psspnd.c) first checks whether the thread to be suspended is the current thread, and then calls KeSuspendThread immediately. If it is a remote thread, it first acquires Thread->RundownProtect, to prevent the thread from being deleted while it is operated on. KiSuspendThread (in thredobj.c) checks Thread->SuspendCount to determine whether more than MAXIMUM_SUSPEND_COUNT suspend calls have been issued, and gives up in this case (STATUS_SUSPEND_COUNT_EXCEEDED). Otherwise, it suspends the thread with the code

02179     //
02180     // Don't suspend the thread if APC queuing is disabled. In this case the
02181     // thread is being deleted.
02182     //
02184     if (Thread->ApcQueueable == TRUE) {
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         //
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();
02201             } else {
02202                 Thread->SuspendApc.Inserted = TRUE;
02203                 KiInsertQueueApc(&Thread->SuspendApc, RESUME_INCREMENT);
02204             }
02205         }
02206     }

With that done, it just returns (first releasing the dispatcher database which it had
acquired on entry).

The surprising thing here is that SuspendThread just schedules an
Asynchronous Procedure Call (APC), instead of doing any real work. Normally, one would expect that the APC gets considered the next time the system dispatches the thread; in the case of a thread running on a different processor, that would be when the quantum of the thread ends.

But let's look further. What is the value of Thread-SuspendApc? It is initialized in KeInitThread, with the code

00178     //
00179     // Initialize the kernel mode suspend APC and the suspend semaphore object.
00180     // and the builtin wait timeout timer object.
00181     //
00183     KeInitializeApc(&Thread->SuspendApc,
00184                     Thread,
00185                     OriginalApcEnvironment,
00186                     (PKKERNEL_ROUTINE)KiSuspendNop,
00187                     (PKRUNDOWN_ROUTINE)KiSuspendRundown,
00188                     KiSuspendThread,
00189                     KernelMode,
00190                     NULL);
00192     KeInitializeSemaphore(&Thread->SuspendSemaphore, 0L, 2L);

So this registers three functions for the APC: the kernel routine KiSuspendNop (which does nothing), KiSuspendRundown (which clears the APC should the thread be terminated before the APC runs), KiSuspendThread, which does the actual suspension. This is defined as

01667 {
01669     PKTHREAD Thread;
01671     UNREFERENCED_PARAMETER(NormalContext);
01672     UNREFERENCED_PARAMETER(SystemArgument1);
01673     UNREFERENCED_PARAMETER(SystemArgument2);
01675     //
01676     // Get the address of the current thread object and Wait nonalertable on
01677     // the thread's builtin suspend semaphore.
01678     //
01680     Thread = KeGetCurrentThread();
01681     KeWaitForSingleObject(&Thread->SuspendSemaphore,
01682                           Suspended,
01683                           KernelMode,
01684                           FALSE,
01685                           NULL);
01687     return;
01688 }

So this essentially just blocks on the thread's suspend semaphore. To support multiple interleaving calls to SuspendThread, a counter is provided, so that the first suspender will schedule the APC, and the last resumer will signal the semaphore.

Still, the question is: will this happen immediately, or only when APCs get processed? KiInsertQueueApc first does what one would expect: insert the APC into the thread's APC queue. However, then it goes on with code that looks like it tries to run the APC immediately. That starts with

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     //
00494     if (Apc->ApcStateIndex == Thread->ApcStateIndex) {

What is the "APC state index", and what values will it have? I don't really know; for the APC, it will be OriginalApcEnvironment. This should also be the ApcStateIndex of the target thread (so the condition should be true normally), but I fail to understand the concept of APC environments (and couldn't find any reasonable explanation anywhere).

In any case, assuming the condition is true, it then goes on testing whether we schedule an APC for the current thread, and run an APC_LEVEL software interrupt right away if it is. Otherwise (i.e. scheduling a remote APC), we lock the dispatcher database, start with RequestInterrupt=FALSE, and do this complex piece of code

00531         if (ApcMode == KernelMode) {
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             //
00551             Thread->ApcState.KernelApcPending = TRUE;
00552             KeMemoryBarrier();
00553             ThreadState = Thread->State;
00554             if (ThreadState == Running) {
00555                 RequestInterrupt = TRUE;
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)))) {
00564                 KiUnwaitThread(Thread, STATUS_KERNEL_APC, Increment);
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)))) {
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                     }
00583                     Thread->WaitStatus = STATUS_KERNEL_APC;
00584                     KiInsertDeferredReadyList(Thread);
00585                 }
00587                 KiReleaseThreadLock(Thread);
00588             }
00590         } else if ((Thread->State == Waiting) &&
00591                   (Thread->WaitMode == UserMode) &&
00592                   (Thread->Alertable || Thread->ApcState.UserApcPending)) {
00594             Thread->ApcState.UserApcPending = TRUE;
00595             KiUnwaitThread(Thread, STATUS_USER_APC, Increment);
00596         }

So multiple conditions get evaluated; let's see which one are true:

  • ApcMode == Kernel? Yes, the suspend APC was created as a kernel APC
  • ThreadState == Running? Might be, if the thread runs on a different processor. We set RequestInterrupt to true in this case.
  • ThreadState == Waiting? Might be if the thread-to-be-suspended is blocked in a system call.
  • ThreadState == GateWait? No clue what this is.
  • Thread->SpecialApcDisable==0? It looks like this should normally be the case, i.e. threads normally allow such APCs to happen.
  • Apc->NormalRoutine == NULL? No, we do have a NormalRoutine (KiSuspendThread)
  • the other conditions (WaitIrql, KernelApcDisable, KernelApcInProgress)? Might evaluate to false if the target thread is doing something critical in kernel mode right now.

So if the target thread is in kernel mode, it may continue to do its stuff for a moment; if it is really just blocked, it will awake to run its APCs.

The code then releases the dispatcher lock, and runs

00604         if (RequestInterrupt == TRUE) {
00605             KiRequestApcInterrupt(Thread->NextProcessor);
00606         }

KiRequestApcInterrupt then checks whether it is the same processor (which it can't be in our case), and send an APC_LEVEL inter-processor interrupt to the processor running the thread to be suspended (through KiIpiSend, which invokes the HAL). This will cause the remote processor to enter kernel mode, and find that APCs should be run for the target thread.

So, in summary:

  • If the target thread is blocked in kernel mode, it may continue to do its kernel activity for a while. Before returning in user mode, the APC queue will be processed and thus the thread suspended.
  • If the target thread is running on a different processor, an IPI will interrupt it, and it becomes suspended.

In either case, the SuspendThread call itself will return before the target thread is actually suspended - either because the thread just gets "unwaited", and still needs to process its APC queue, or because just the IPI has been issued, but the end of the interrupt processing is not waited for.

As I indicated at the beginning: TerminateThread works quite similarly; except that different APC routines are used.


Comments are closed.