Планировщик, потоки и процессы

Дата публикации 6 дек 2008

Планировщик, потоки и процессы — Архив WASM.RU

В этот раз мы рассмотрим, как операционная система Windows XP распоряжается потоками и процессами и осуществляет их планирование. Коснемся контекстов, их переключения, снятия и установки.

Базовая единица, которая выполняется в системе, это поток. Что же из себя представляет поток? Можно сказать, что каждый поток в системе представлен структурой ядра KTHREAD (и ее расширением исполнительной системы ETHREAD). Структура (K/E)THREAD сильно варьируется от версии к версии ядра, поэтому нет смысла приводить полное ее определение. Могу с уверенностью сказать, что в следующей версии оно будет уже другим.

Однако, отдельные ее поля и общий вид стоит рассмотреть. Итак, структура KTHREAD включает в себя (попутно я буду так же писать смещения от начала структуры для версии Windows XP SP2 Build 2600):

  • /* +0x0 */ DISPATCHER_HEADER Header;
    С этой структуры начинаются все "ожидабельные" объекты ядра, то есть те объекты, которые могут быть переданы функциям ожидания KeWaitFor***. Подробно ожидание на объектах мы рассмотрим далее в этой статье. В том числе можно ожидать и на объекте потока - ожидание удовлетворится тогда, когда поток закончится.
  • /* +0x18 */ PVOID InitialStack, StackLimit;
    /* +0x28 */ PVOID KernelStack;
    Это соответственно начальный стек, лимит стека и текущий ядерный стек потока. Обычно потокам предоставляется три страницы на стек, что составляет 12 килобайт. Так же есть недокументированная функция MmGrowKernelStack.
  • /* +0x2d */ KTHREAD_STATE State;
    Этот байт описывает состояние потока. Каждый поток может находиться в одном из 7 состояний: инициализирован, готов, на выполнении, приостановлен, завершен, в ожидании, переходный.

    Код (Text):
    1. typedef enum _KTHREAD_STATE {
    2.         Initialized,   
    3.         Ready,  
    4.         Running,    
    5.         Standby,    
    6.         Terminated,    
    7.         Waiting,    
    8.         Transition
    9.     } KTHREAD_STATE, *PKTHREAD_STATE;
  • /* +0x2E */ BOOLEAN Alerted[2];
    Два флага, определяющих было ли прервано ожидание потока в режиме ядра и в режиме пользователя соответственно( Alerted[KernelMode], Alerted[UserMode] )
  • /* +0x34 */ KAPC_STATE ApcState;
    Структура, описывающая состояние APC для данного потока - какому процессу он принадлежит, находятся ли в ожидании пользовательские или ядерные APC
  • /* +0x54 */ NTSTATUS WaitStatus;
    Это поле описывает статус ожидания потока. Например, если ожидание было прервано по получению специальной APC режима ядра, статус будет STATUS_KERNEL_APC. Если по истечению времени - STATUS_TIMEOUT.
  • /* +0x58 */ KIRQL WaitIrql;
    Тут все понятно из названия - IRQL, на котором вызывающий поток стал ожидать на объекте.
  • /* +0x59 */ KPROCESSOR_MODE WaitMode;
    Это поле определяет режим ожидания. Туда аккурат записывается соответствующий параметр, переданный функции ожидания. Соответственно это KernelMode или UserMode. Если он равен UserMode, то поток может принимать UserMode APC.
  • /* +0x5a */ BOOLEAN WaitNext;
    Если передать функции KeSetEvent параметр Wait=TRUE, показывая тем самым, что мы собираемся далее ожидать атомарно на каком-то объекте и снимать блокировку с базы данных планировщика вовсе не стоит, то в это поле помещается еденица, а в WaitIrql помещается IRQL, на котором эта база данных была заблокирована. Иначе это поле равно нулю.
  • /* +0x5b */ KWAIT_REASON WaitReason;
    Определяет причину ожидания, являясь элементом перечисления KWAIT_REASON. Может принимать значения от Executive, FreePage, PageIn, ... до конца WrVirtualMemory, WrPageOut, ... К примеру, если поток замораживается, то ему устанавливается причина ожидания Suspended, при ожидании чтения страницы - PageIn, при задержке - DelayExecution. MSDN DDK рекомендует во всех рядовых случаях ожидания в драйверах указывать Executive, т.к. остальные причины используются самим ядром.
  • /* +0x5c */ KWAIT_BLOCK* WaitBlockList;
    Указывает на используемый в текущем ожидании элемент массива WaitBlock, о котором речь пойдет далее. Фактически, связывает потоки, ожидающие на одном объекте (соответствующие связывающие ссылки хранятся в DISPATCHER_HEADER).
  • /* +0x70 */ KWAIT_BLOCK WaitBlock[4];
    Массив из 4х блоков ожидания. По блоку на каждый из трех максимально допустимых объекта для ожидания к KeWaitForMultipleObjects (если аргумент WaitBlockArray=NULL) + один блок для ожидания при задержках (KeDelayExecution) и для таймеров.
  • /* +0xD4 */ BOOLEAN KernelApcDisable;
    Флаг, показывающий не находится ли поток в критическом регионе (KeEnterCriticalRegion устанавливает этот флаг, запрещая тем самым доставку специальных APC режима ядра).
  • /* +0xE0 */ PVOID* ServiceTable;
    Очень интересное поле, определяющее сервисную таблицу для потока. Обычно там лежит адрес KeServiceDescriptorTable или KeServiceDescriptorTableShadow, однако, никто не пешает определить потоку собственную таблицу, которой он будет пользоваться (до поры - до времени, пока не придется ее менять).
  • /* +0xE4 */ KQUEUE* Queue; /* +0x118 */ LIST_ENTRY QueueListEntry;
    Используется при операциях с очередями функциями KeInsertQueue, KeRemoveQueue. Queue хранит в себе указатель на последнюю обрабатываемую потоком очередь, а ссылки из QueueListEntry образуют двусвязанный список потоков, желающих оперировать с этой очередь. Хотите гарантированно увидеть такие потоки - загляните в потоки процесса System, где все время кто-то будет ждать на очереди. Это происходит ввиду наличия в контексте System т.н. рабочих потоков (worker threads), которые получают задания (work items) через KeRemoveQueue из очереди, куда их добавляет ExQueueWorkItem. Рабочие потоки используются, например, для загрузки драйвера - NtLoadDriver ставит в очередь задач задачу загрузки драйвера. Когда появляется свободный рабочий поток, он вынимает эту задачу из очереди и приступает к выполнению, вызывая IopLoadUnloadDriver. Поэтому всегда найдутся потоки, ожидающие на очереди - рабочие потоки.
  • /* +0x130 */ PVOID Win32Thread;
    Указывает на структуру потока оконной подсистемы win32k. Равно нулю, если поток ни разу не вызывал функций win32k
  • /* +0x134 */ KTRAP_FRAME* TrapFrame;
    Если поток находится в режиме ядра то содержит указатель на фрейм ловушки, содержащий контекст, с которым поток вошел в режим ядра.
  • /* +0x140 */ KPROCESSOR_MODE PreviousMode;
    Содержит режим процессора, в которым поток был до вхождения в режим ядра.
  • /* +0x164 */ BOOLEAN Alertable;
    Хранит в себе одноименный параметр, переданный функциям ожидания, и определяющий вместе с WaitMode поведение системы при попытке доставки пользовательской APC.
  • /* +0x166 */ BOOLEAN ApcQueueable;
    Исключающий флаг доставки APC. Если он сброшен - никакие APC не могут быть доставлены потоку (KeInsertQueueApc возвратит FALSE)
  • /* +0x16c */ KAPC SuspendApc;
    /* +0x19c */ KSEMAPHORE SuspendSemaphore;

    Используется при приостановке (Suspend) потока, подробности далее в статье.
  • /* +0x1b0 */ LIST_ENTRY ThreadListEntry;
    Пожалуй, одно из важных полей. Связывает потоки в двусвязанный кольцевой список потоков процесса. Вершина списка лежит в EPROCESS::ThreadListHead
  • /* +0x60 */ LIST_ENTRY WaitListEntry;
    Еще одно важнейшее поле, связывает все потоки системы в единые списки готовых к выполнению или ожидающих потоков (в зависимости от State, соответственно, списки KiDispatcherReadyListHead или KiWaitInListHead/KiWaitOutListHead)
  • /* +0xe0 */ void* ServiceTable;
    Указатель на системную таблицу сервисов - KeServiceDescriptorTable или KeServiceDescriptorTableShadow. Если подменить указатель на свой, то будут вызываться наши сервисы. Изменяется в PsConvertToGuiThread.

Теперь, после краткого знакомства, рассмотрим потоки поподробнее. Точнее, сперва обозначим план нашего дальнейшего повествования - сперва будет рассказано про создание потока, про обычную "жизнь" потока и переключение контекстов, потом про ожидание, далее про APC и приостановку (Suspend).

Итак, создание потока. Функция, ответственная за создание потока - внутренняя апи ядра PspCreateThread. Она вызывается из NtCreateThread и из PsCreateSystemThread. Соответственно, логично предположить, что она может создать поток, который начнет работать в режиме ядра, либо в режиме пользователя.

Создание начинается с создания самого объекта ETHREAD с помощью диспетчера объектов и его апи ObCreateObject. Обнуляются блоки различных связанных списков, создается стек ядра для потока. Если создается поток пользовательского режима, то выделяется TEB. Завершается инициализация других полей, в том числе поле State потока устанавливается в Initialized, после чего новый поток начинает выполнение соответственно либо с функции PspUserThreadStartup, либо с PspSystemThreadStartup. PspSystemThreadStartup просто передает управление на нужную точку входа, тогда как PspUserThreadStartup создает, инициализирует и доставляет потоку специальную APCрежима пользователя, после чего поток начинает выполняться в пользовательском режиме. Состояние потока на этот момент уже Running.

Вдруг поток выполнялся - выполнялся, а у него закончился квант времени. Это могло случиться либо изза того, что пришло прерывание тика таймера, либо поток впал в ожидание. Так или иначе, контекст поток сохранился в стеке ядра в структуре KTRAP_FRAME (если открыть hal.dll и найти там, например, HalpClockInterrupt, то можно легко найтиё сохранение в стеке KTRAP_FRAME по обилию инструкций PUSH), а поток принимает состояние Ready или Wait, далее ищется следующий готовый к выполнению (Ready) поток, который становится выполняющимся (Running), происходит переключение контекста на него и он начинает отрабатывать новый квант. Замечу, что потоков, готовых к выполнению, может быть сколько угодно. Хотя, впрочем, обычно их довольно мало - большинство потоков в системе ожидают наступления какого-либо события, например реакции от пользователя, но всегда есть как минимум один поток, готовый к выполнению - KiIdleThread (в общем случае многопроцессорной системы число таких потоков равно числу процессоров. В этом можно убедиться по цифре в поле Threads у процесса System Idle Process в окне Task Manager). Если в системе больше нет готовых к выполнению потоков, квант времени получает он, это реализовано за счет выставления ему наинизшего приоритета (напомню, что приоритет потока определяет то, когда поток получит свой квант если в системе есть другие готовые потоки. Стоит отмеить распространенное заблуждение, что если поток имеет приоритет Low, то он не будет загружать процессор. Отнюдь - если в системе только этот поток, не считая idle, готов к выполнению и он имеет приоритет Low, то все процессорное время будет отдано ему), и этот "пустой" поток просто выполняет пересчет счетчиков производительности системы. Итак, потоков, готовых к выполнению может быть сколько угодно - это потоки, активно производящие какието вычисления, поскольку State==Ready означает, что у потока квант времени был отобран ПРИНУДИТЕЛЬНО. В то же время выполняющихся потоков может быть не больше, чем имеется в системе процессоров. State==Running означает, что поток в данный момент выполняется на конкретном процессоре.

Итак, поток выполнялся (был в состоянии Running). Если поток добровольно отдает квант времени и впадает в ожидание, то он получает состояние Wait. Если же у него принудительно отнимают квант, то он получает состояние Ready и ему будет передано управление в следующий раз (правда, после того, как квант будет передан потокам с большим приоритетом). Кстати, снова о приоритетах - если поток Ready имеет высокий приоритет, то это может печально сказаться на отклике системы - поток будет отнимать все процессорное время, не давая выполняться потокам с меньшим приоритетом и система будет заметно "тормозить".

Углубимся в детали. Переключение контекстов инициируется функцией KiSwapThread, которая ищет следующий готовый к выполнению поток и передает ему управление. Явное переключение контекстов потоком осуществляется функцией SwapContext, про которую, вероятно, некоторые читатели уже слышали. Стоит отметить, что фактической перезагрузки большинства регистров она НЕ выполняет. Это произошло еще гораздо раньше, когда обработчик исключения сохранил в стеке KTRAP_FRAME. На выходе из обработчика KTRAP_FRAME восстанавливается из стека, как будто ничего и не было.

Функции SwapContext передаются следующие аргументы: esi = KTHREAD следующего потока edi = KTHREAD текущего потока cl = старый IRQL (из поля CurrentThread->WaitIrql) ebx = KPCR процессора. Соответственно она возвращает al = KernelApcPending ebx = KPCR esi = KTHREAD текущего потока Сначала SwapContext проверяет, активна ли DPC в данный момент (KPRCB.DpcRoutineActive). Если да - показывается синий экран ATTEMPTED_SWITCH_FROM_DPC.

Сохраняется состояние сопроцессора. Далее происходит переключение стеков, текущий ESP сохраняется в CurrentThread->KernelStack. Редактируется дескриптор сегмента GDT, отвечающего за юзермодный TEB. Если новый поток принадлежит другому процессу, происходит переключение адресных пространств посредством смены регистра CR3 (PDBR), так же перезагружается LDT. Если новый поток ожидает APC режима ядра (ApcState.KernelApcPending), то APC доставляется. На этом переключение потока завершается.

А инициируется само переключение контекста при завершении аппаратного прерывания, если позволяет IRQL. Посмотрим на примере прерывания таймера. Начинается его обработка во внутренней функции HalpClockInterrupt. Сначала идет сохранение в стеке KTRAP_FRAME. Потом вызывается экспортируемая функция hal - HalBeginSystemInterrupt, задача которой состоит в повышении IRQL до требуемого уровня. Затем обработчик выполняет некоторые манипуляции с различными счетчиками и делает JMP на экспортируемую функцию ядра KeUpdateSystemTime. Она проверяет, не истекли ли таймеры для установленных DPC с таймаутом, если истекли - вызывается HalRequestSoftwareInterrupt с параметром 2 (DPC/Dispatch) для их выполнения. Далее идет вызов HalEndSystemInterrupt, которая понижает IRQL до старого уровня и, если он вдруг оказался ниже DPC/Dispatch, вызывает KiDispatchInterrupt, которая как раз и производит поиск нового потока для выполнения. Далее управление возвращается только тогда, когда поток снова получает квант. Происходит возврат из KiDispatchInterrupt/HalEndSystemInterrupt обратно в KeUpdateSystemTime, которая завершается прыжком на KeI386EoiHelper. Она поднимает IRQL до еденицы (APC_LEVEL), доставляет APC если это нужно и снижает IRQL обратно. Из стека извлекается сохраненный KTRAP_FRAME и происходит возврат из прерывания (iretd). Прерванный поток продолжает выполнение как ни в чем не бывало.

А как же GetThreadContext/SetThreadContext достает контекст потока?

GetThreadContext->NtGetContextThread->PsGetContextThread доставляют потоку специальную APC режима ядра (PspGetSetContextSpecialApc), которая извлекает из стека ядра KTRAP_FRAME и преобразовывает его в структуру CONTEXT функцией KeContextFromKframes. Аналогично, установка контекста потока копирует переданный CONTEXT в KTRAP_FRAME функцией KeContextToKframes.

KTRAP_FRAME хранится прямо в начале (если считать от начального значения esp потока) стека ядра, на которое (на начало) указывает KTHREAD::InitialStack. Сам же адрес KTRAP_FRAME вычисляется по нехитрой формуле - если не равно нулю поле KTHREAD::TrapFrame, то адрес берется оттуда. Иначе - из стека ядра:

Код (Text):
  1.  
  2. #define PspGetBaseTrapFrame(Thread) (PKTRAP_FRAME)((ULONG_PTR)Thread->Tcb.InitialStack - \
  3.                                                     PSPALIGN_UP(sizeof(KTRAP_FRAME),KTRAP_FRAME_ALIGN) - \
  4.                                                     sizeof(FX_SAVE_AREA))

то есть в начальном стеке ядра резервируется место под KTRAP_FRAME и под FX_SAVE_AREA. Поле KTHREAD::TrapFrame заполняется при вызове апи из режима пользователя, заполнение происходит прямо в KiFastCallEntry. Посмотрим на откомментированный код:

Код (Text):
  1.  
  2. .text:00465930 _KiFastCallEntry proc near
  3. .text:00465930                 mov     ecx, 23h
  4. .text:00465935                 push    30h
  5. .text:00465937                 pop     fs
  6. .text:00465939                 mov     ds, cx
  7. .text:0046593B                 mov     es, cx
  8. .text:0046593D                 mov     ecx, ds:0FFDFF040h ;  TSS -> Esp0
  9. .text:00465943                 mov     esp, [ecx+4]
  10. .text:00465946                 push    23h             ; HardwareSegSs
  11. .text:00465948                 push    edx             ; HardwareEsp
  12. .text:00465949                 pushf                   ; Eflags
  13. .text:0046594A                 push    2
  14. .text:0046594C                 add     edx, 8
  15. .text:0046594F                 popf
  16. .text:00465950                 or      byte ptr [esp+1], 2
  17. .text:00465955                 push    1Bh             ; SegCs
  18. .text:00465957                 push    dword ptr ds:0FFDF0304h ; Eip = KUSER_SHARED_DATA.SystemCallReturn (KiFastSystemCallRet)
  19. .text:0046595D                 push    0               ; ErrCode
  20. .text:0046595F                 push    ebp             ; Ebp
  21. .text:00465960                 push    ebx             ; Ebx
  22. .text:00465961                 push    esi             ; Esi
  23. .text:00465962                 push    edi             ; Edi
  24. .text:00465963                 mov     ebx, ds:PCR_SELF
  25. .text:00465969                 push    3Bh             ; SegFs
  26. .text:0046596B                 mov     esi, [ebx+PCR_CURRENTHREAD]
  27. .text:00465971                 push    dword ptr [ebx] ; ExceptionList
  28. .text:00465973                 mov     dword ptr [ebx], 0FFFFFFFFh
  29. .text:00465979                 mov     ebp, [esi+KTHREAD_InitialStack]
  30. .text:0046597C                 push    1               ; PreviousMode
  31. .text:0046597E                 sub     esp, 48h        ; Eax ->> DbgEbp
  32. .text:00465981                 sub     ebp, SIZEOF_KTRAP_FRAME_AND_FX_SAVE_AREA
  33. .text:00465987                 mov     byte ptr [esi+_KTHREAD.PreviousMode], 1
  34. .text:0046598E                 cmp     ebp, esp
  35. .text:00465990                 jnz     short loc_46592C
  36. .text:00465992                 and     dword ptr [ebp+_KTRAP_FRAME.Dr7], 0
  37. .text:00465996                 test    byte ptr [esi+2Ch], 0FFh     ; KTHREAD::Teb
  38. .text:0046599A                 mov     [esi+KTHREAD_TrapFrame], ebp                 ; KTHREAD::TrapFrame = TrapFrame    // !!
  39. .text:004659A0                 jnz     Dr_FastCallDrSave             ; saves DRx registers & returns to loc_4659A6
  40. .text:004659A6 loc_4659A6:
  41. .text:004659A6                 mov     ebx, [ebp+_KTRAP_FRAME._Ebp]
  42. .text:004659A9                 mov     edi, [ebp+_KTRAP_FRAME._Eip]
  43. .text:004659AC                 mov     [ebp+_KTRAP_FRAME.DbgArgPointer], edx
  44. .text:004659AF                 mov     [ebp+_KTRAP_FRAME.DbgArgMark], 0BADB0D00h
  45. .text:004659B6                 mov     [ebp+_KTRAP_FRAME.DbgEbp], ebx
  46. .text:004659B9                 mov     [ebp+_KTRAP_FRAME.DbgEip], edi
  47. .text:004659BC                 ......

В обозначенном месте в структуре KTHREAD сохраняется указатель на заполненный TrapFrame. Его можно забрать в любое время вызовом PsGetContextThread с параметром UserMode.

Если поле TrapFrame не заполнено, значит, что поток был приостановлен из режима ядра. Тогда контекст забирается напрямую из стека ядра с помощью описанного выше макроса, куда он сохраняется аналогичным кодом при входе в прерывание. Важно отметить: если поток отдал свое процессорное время "добровольно" вызовом одной из функций ожидания, то никакого сохранения контекста не происходит. KTRAP_FRAME не обновляется, и если попробовать получить контекст потока, который находится в состоянии ожидания в режиме ядра, мы получим "старый" контекст, который был у него в момент последнего прерывания его по таймеру. В стеке же отдельно сохраняются важные регистры esi,edi,ebx,ebp и потоки переключаются. Потом эти четыре регистра восстанавливаются. Если, например, перед входом в ожидание поставить ESI=12345678, впасть в ожидание и из другого потока получить контекст только что ушедшего в ожидание, то никаких 12345678 в ESI мы не получим - их не было в момент последнего прерывания потока аппаратным прерыванием. Соответственно, имеют смысл лишь контексты режима ядра ready-потоков. В самом деле, посмотрим экспериментально. Мало того, что мы не увидим нигде в цепочке KeWaitForSingleObject -> KiSwapThread -> KiSwapContext -> SwapContext явного сохранения TRAP_FRAME, так можно даже попробовать записать некое сигнальное значение в "долгоиграющие" регистры esi/edi/ebx и посмотреть, удастся ли их изъять из контекста когда поток впадает в ожидание или же у него квант отнимают принудительно.

Возьмем поток:

Код (Text):
  1. VOID SchedThread (PVOID)
  2. {
  3.     __asm mov ebx, 0x12003486
  4.     __asm mov edi, 0x21004366
  5.  
  6.     LARGE_INTEGER SystemTimeStart, SystemTime;
  7.     KeQuerySystemTime (&SystemTimeStart);
  8.     SystemTime = SystemTimeStart;
  9.  
  10.     // Wait for three seconds.
  11.     for ( ; (SystemTime.LowPart - SystemTimeStart.LowPart < 10000 * 3000) ; )
  12.         KeQuerySystemTime (&SystemTime);
  13.  
  14.     PsTerminateSystemThread (0);
  15. }

Если, пока поток спит, получить его контекст с помощью PsGetContextThread (Thread, &ctx, KernelMode), то можно обнаружить заветные числа в полях ctx.Edi, ctx.Ebx.

Если же заменить такое ожидание на KeDelayExecutionThread или KeWaitForSingleObject, то этих чисел в контексте мы, увы, не найдем. Поговорим теперь об ожидании потоков. Вызывом одной из функций ожидания (KeWaitForSingleObject, KeWaitForMultipleObjects, KeDelayExecutionThread, KeRemoveQueue), поток может приостановить свое выполнение, пока указанный объект не перейдет в сигнальное состояние (не истечет таймаут, не появятся новые элементы в очереди, соответственно). Одно ожидание представляется в ядре структурой KWAIT_BLOCK, которая содержит обратные ссылки на ожидающий поток, на ожидаемый объект, а так же имеет связь со всеми такими структурами, относящимися к одному объекту, в двусвязанном кольцевом списке, вершина которого лежит в DISPATCHER_HEADER.WaitListHead.

Соответственно, имея объект, можно сказать, какие потоки его ожидают, просматривая соответствующий список. Выше я говорил о "загадочных" полях WaitBlockList и WaitBlock структуры KTHREAD. Они имеют самое непосредственное отношение к этому - WaitBlockList содержит указатель на текущий используемый KWAIT_BLOCK (либо на массив таких, если ожидается более одного объекта), а WaitBlock[] представляет собой выделенное место в структуре KTHREAD под 4 структуры KWAIT_BLOCK - три штуки для ожидания трех объектов функцией KeWaitForMultipleObjects (если нужно ждать на большем количестве объектов, нужно передавать свой массив блоков ожидания, который нужно выделить заранее перед ее вызовом; об этом написано в MSDN в описании к KeWaitForMultipleObjects), четвертый - для таймера (используется при ожидании с таймаутом и в KeDelayExecutionThread).

KeWaitForSingleObject производит следующие манипуляции:

1) получает адрес Thread->WaitBlock[0], который будет использоваться для ожидания на объекте 2) сохраняет этот адрес в Thread->WaitBlockList 3) заполняет KWAIT_BLOCK указателем Object, в поле WaitType заносит WaitAny (это вообщем-то не имеет особого логического смысла, потому что объект один) 4) заносит в NextWaitBlock собственный адрес. Это поле образует односвязанный циклический список KWAIT_BLOCK, которых ожидает один поток. 5) KWAIT_BLOCK добавляется в список Object->Header.WaitListHead 6) заполняет поля Alertable, WaitMode, WaitReason соответствующими параметрами, а так же поля WaitTime (текущим временем) и State (Waiting). 7) поток помещается в очередь ожидающих потоков KiWaitInListHead (KiWaitOutListHead). Выбор используемой очереди осуществляется по следующему критерию - если WaitMode==KernelMode || Th->EnableStackSwap==FALSE || (_Thread)->Priority >= (LOW_REALTIME_PRIORITY + 9), то поток помещается в KiWaitOutListHead, иначе в KiWaitInListHead. Я не очень понимаю смысла этого разделения, судя по всему, KiWaitOutListHead представляет собой список "важных" ожидающих потоков. 8) выбирает следующий готовый поток и переключается на него с помощью KiSwapThread. Управление далее возвращается в трех случаях: ожидание прошло успешно, истек таймаут или пришла Kernel-Mode special APC. В последнем случае пересчитывается таймаут и цикл входа в ожидание повторяется. В случае ожидания с таймаутом инициализируется последний, четвертый KWAIT_BLOCK в массиве KTHREAD::WaitBlock[]

APC

APC (Asynchronous Procedure Call) - это что-то типа "сообщений", которые могут доставляться потоку для выполнения определенных функций, причем поток о них может ничего не знать. Доставляющий код определяет свою callback-функцию для APC, которая будет выполнена в контексте требуемого потока. Когда требуемый поток завершает квант времени и если текущий IRQL == PASSIVE_LEVEL, то проверяется очередь сообщений APC. Если там есть какие-то APC, они ставятся на выполнение. APC могут быть User-Mode и Kernel-Mode, соответственно выполняемые в этих режимах.

APC имеет несколько каллбек-функций: - Normal routine - функция, вызываемая на заданном режиме работы процессора (user/kernel) и выполняющая основную работу - Kernel routine - вспомогательная функция, вызываемая в режиме ядра. - Rundown routine - вспомогательная функция, вызываемая в режиме ядра, когда APC не была доставлена, а поток уже завершается. Вызывается из PspExitThread()

Все начинается с KiCheckForSoftwareInterrupt, выполняемой при переключении контекстов. Если есть ожидающие APC, то вызывается KiDispatchSoftwareInterrupt(APC_LEVEL) -> KiDeliverApc. Сначала она проверяет список Kernel-Mode APC. Там могут быть APC двух типов - normal kernel APC и special kernel APC. У special kernel APC заполнено только поле Kernel Routine и она вызывается в любом случае несмотря на флаги KernelApcInProgress или KernelApcDisable. Используется внутри ядра для служебных целей.

Normal APC содержит и normal и kernel каллбеки, которые вызываются в последовательности - сначала kernel routine, потом normal routine (если TargetMode у APC == KernelMode).

Наконец, последний вариант - это usermode APC. Сначала вызывается KernelRoutine в контексте того же потока в режиме ядра, а NormalRoutine юзермодная готовится к выполнению.

Важно: не определен порядок вызова KernelRoutine и NormalRoutine в этом случае. Ошибку, связанную с этим, содержит известный код http://www.codeproject.com/KB/system/KernelExec.aspx (Starting a Process from KernelMode By Stan Alex), где сначала выделяется место под юзермодную APC функцию, она вызывается, а KernelRoutine освобождает место (рассчитывалось, видимо, что KernelRoutine вызовется после NormalRoutine, однако это не всегда так. в результате KernelRoutine освобождала место, занимаемое кодом NormalRoutine в то время, как она еще выполнялась. результатом был STATUS_ACCESS_VIOLATION. В идеале там нужна синхронизация по Event'у) Механизм APC используется в еще одном важном механизме потоков Windows - приостановка и восстановление (suspend/resume) потока.

У потока есть два списка APC - ApcState.ApcListHead[2], ApcState.ApcListHead[KernelMode] содержит список APC режима ядра, а другой ApcState.ApcListHead[UserMode] - список APC пользовательского режима.

Suspend/Resume

Для конечного пользователя регулируется функциями NtSuspendThread/NtResumeThread, в конечном итоге выполнение сводится к KeSuspendThread/KeResumeThread. Приостановка и восстановление потока основаны на механизме APC: в двух словах, потоку посылается APC (Thread->SuspendApc), которая ожидает на событии (Thread->SuspendSemaphore) - это приостановка, а сигнализирование этого события - восстановление.

Посмотрим на создание потока в KeInitThread - там инициализируется эта APC и семафор следующим образом:

Код (Text):
  1.  
  2.     //
  3.     // Initialize the kernel mode suspend APC and the suspend semaphore object.
  4.     // and the builtin wait timeout timer object.
  5.     //
  6.  
  7.     KeInitializeApc(&Thread->SuspendApc,
  8.                     Thread,
  9.                     OriginalApcEnvironment,
  10.                     (PKKERNEL_ROUTINE)KiSuspendNop,
  11.                     (PKRUNDOWN_ROUTINE)KiSuspendRundown,
  12.                     KiSuspendThread,
  13.                     KernelMode,
  14.                     NULL);
  15.  
  16.     KeInitializeSemaphore(&Thread->SuspendSemaphore, 0L, 2L);
  17.  

А KeSuspendThread просто делает KiInsertQueueApc для этого объекта. Смотрим дальше - что выполняется уже в контексте нужного потока. Когда его выполнение прерывается доставленной APC, управление принимает KiSuspendThread. Все, что она делает, это:

Код (Text):
  1.  
  2.     Thread = KeGetCurrentThread();
  3.     KeWaitForSingleObject(&Thread->SuspendSemaphore,
  4.                           Suspended,
  5.                           KernelMode,
  6.                           FALSE,
  7.                           NULL);
  8.  

Нехитро, правда? KeResumeThread декрементирует SuspendCount и, когда он станет нулём, сигналит семафор. Поток возобновляет выполнение, как будто APC вообще не доставляли.

Послесловия не будет. Не о чем тут писать, не из чего и не для чего делать выводы.)

Приложение к статье

Три проекта:

  1. sched1 - проект с потоком SchedThread (См. статью)
  2. susprsme - моя реализация множественого суспенда потоков, когда нужно приостановить много потоков и потом разом все восстановить.
  3. waitblks - перечисление wait block'ов потока и объекта.
© Great

0 2.924
archive

archive
New Member

Регистрация:
27 фев 2017
Публикаций:
532