[Previous] [Next]

Системные программные потоки

Процесс в операционной системе Windows является единицей владения. Процессу принадлежат, в частности, программные потоки, которые уже и являются единицами исполнения. Для каждого потока установлен независимый программный счетчик и контекст исполнения, который включает состояние регистров центрального процессора, значение уровня IRQL, от которого зависит, когда этот поток получит возможность распорядиться системным процессором.

Потоки пользовательского режима хорошо известны программистам пользовательских приложений. Несколько отличаются от них потоки режима ядра. Системный поток есть такой поток, который выполняется исключительно в режиме ядра. Он не имеет контекста пользовательского режима и не может получить доступ в пользовательское адресное пространство. Соответственно, программный код рабочих процедур (в частности, код процедуры обработки IOCTL запросов, который может пользоваться виртуальными адресами пользовательского приложения) не может считаться системным потоком, хотя и относится к коду драйвера режима ядра. Системные программные потоки созданы специальными вызовами и не имеют возможности интерпретировать виртуальные пользовательские адреса (ниже 0x80000000) ни в одном из пользовательских контекстов. Системный поток не имеет корней в пользовательском режиме. Данная особенность накладывает основное ограничение, свойственное системным программным потокам, — они могут пользоваться только адресами системного адресного пространства. Все остальные адреса (виртуальные адреса пользовательских приложений), переданные им каким-нибудь способом, будут не просто бесполезны — их использование может привести к краху операционной системы.

Вторая особенность системных программных потоков состоит в особом статусе программного кода режима ядра. Если пользовательское приложение снимается, например, из панели Диспетчера задач, то происходит приостановка работы всех потоков снимаемого процесса и соответствующая очистка ресурсов, им принадлежащих. В режиме ядра такую работу за программный код системного потока никто выполнять не будет. Системный программный поток должен самостоятельно завершить свою работу и самостоятельно освободить занимаемые ресурсы.

Системные потоки выполняются обычно на уровне приоритета IRQL APC_LEVEL или PASSIVE_LEVEL (если системный программный поток искусственно не повысил свой приоритет определенными вызовами). Соответственно, эти потоки соревнуется за использование центрального процессора наряду с программными потоками пользовательского режима.

Основная причина использования программных потоков в режиме ядра та же, что и в пользовательских приложениях — обслуживание медленных операций либо событий, наступление которых плохо прогнозируется. Сюда относятся и длительные операции инициализации некоторых специфических устройств. Диспетчер ввод/вывода отводит процедуре DriverEntry около полуминуты, после чего загрузка драйвера прекращается. Вполне приемлемым решением мог бы быть запуск программного потока, который продолжал бы инициализацию устройства, а процедура DriverEntry быстро возвратила бы код успешного завершения.

Таблица 10.1. Прототип вызова PsCreateSystemThread

NTSTATUS PsCreateSystemThread IRQL == PASSIVE_LEVEL
Параметры Создает системный программный поток
OUT PHANDLE pThreadHandle Указатель на переменную для сохранения дескриптора нового программного потока
IN ULONG DesiredAccess THREAD_ALL_ACCESS (или 0L) для создаваемого драйвером потока
IN POBJECT_ATTRIBUTES Attrib NULL для создаваемого драйвером потока
IN HANDLE ProcessHandle NULL для создаваемого драйвером потока
OUT PCLIENT_ID ClientId NULL для создаваемого драйвером потока
IN PKSTART_ROUTINE StartAddr Стартовая функция потока — точка входа в поток
IN PVOID Context Аргумент, передаваемый в стартовую функцию
Возвращаемое значение

• STATUS_SUCCESS — поток создан
• STATUS_Xxx — код ошибки

Не последнюю роль в потребности использовать программные потоки в драйвере играет и тот факт, что (стандартно) их код выполняется на уровне PASSIVE_LEVEL. Как поступить, если разработчик драйвера желает протоколировать события в драйвере, записывая их в файл на диске, включая события, происходящие при повышенных приоритетах IRQL? Ведь функция ZwCreateFile и ZwWriteFile работают только на уровне PASSIVE_LEVEL, следовательно, о протоколировании из ISR и DPC процедур следует забыть? Подобная задача достаточно легко решается, если высокоприоритетный код будет помещать свои записи в промежуточный буфер, который будет сброшен на диск позже системным программным потоком, выполняющимся на подходящем для этого уровне PASSIVE_LEVEL.

Системные программные потоки создаются вызовом PsCreateSystemThread (таблица 10.1), а завершиться они должны самостоятельно — выполнением вызова PsTerminateSystemThread.

Таблица 10.2. Прототип вызова PsTerminateSystemThread

NTSTATUS PsTerminateSystemThread IRQL == PASSIVE_LEVEL
Параметры Вызывается системным программным потоком при окончании работы
IN NTSTATUS ExitStatus Код завершения потока
Возвращаемое значение

STATUS_SUCCESS — поток прекращен

Когда драйвер выгружается, он должен быть уверен, что все системные потоки, созданные драйвером, завершены. Зачастую, это означает, что должен быть использован какой-либо из механизмов сигнализации, при помощи которого можно было уведомить поток о необходимости завершить свою работу и дождаться этого момента.

Создаваемые драйвером системные программные потоки имеют при создании уровень IQRL равный PASSIVE_LEVEL в диапазоне приоритетов Normal (см. таблицу 6.2), хотя могут иметь любой приоритет в диапазоне Normal и RealTime.

В общем случае системный поток, запущенный из драйвера, должен выполняться при приоритете, находящемся вблизи нижней границы диапазона real-time. Изменить текущий приоритет потока можно в нем самом, используя вызов KeSetPriorityThread, как демонстрируется в следующем фрагменте:

VOID ThreadStartRoutine (PVOID pContext)
{
	:
	KeSetPriorityThread ( KeGetCurrentThread(),
                         LOW_REALTIME_PRIORITY);
} 

Заметим, что численное значение LOW_REALTIME_PRIORITY в заголовочных файлах DDK установлено равным 16 (это нижняя граница диапазона RealTime).

Следует помнить, что потоки RealTime не имеют квантования по времени. Это означает, что процессор перестанет заниматься данным потоком только тогда, когда поток добровольно перейдет в состояние ожидания или его не "перебьет" поток более высокого приоритета. Таким образом, здесь драйвер не может рассчитывать на обычный для пользовательского режима циклический подход в планировании заданий.

Системные рабочие потоки

Для нерегулярных коротких операций на уровне IRQL, равном PASSIVE_LEVEL, использование полноценных потоков, которые создаются и тут же завершаются, вряд ли будет эффективным. Альтернативой этому может быть создание системных рабочих потоков, system worker threads.

Для того чтобы проделать какую-нибудь несложную и не очень продолжительную работу, драйвер должен выделить память под структуру типа WORK_QUEUE_ITEM, затем инициализировать ее вызовом ExInitializeWorkItem, связав с ней собственную функцию (callback-функцию), и поместить ее в очередь объектов WORK_QUEUE_ITEM вызовом ExQueueWorkItem. Приоритет, на котором будет работать код вызываемой callback-функции, зависит от второго параметра вызова ExQueueWorkItem, QueueType, то есть от того, в какую очередь помещен данный объект WORK_QUEUE_ITEM, например, DelayedWorkQueue. В конце своей работы вызванная callback-функция должна освободить память, занятую под объектом типа WORK_QUEUE_ITEM (указатель на него поступает в callback-функцию при вызове).

Перечисленные функции ExInitializeWorkItem и ExQueueWorkItem считаются устаревшими (предлагаемые теперь функции будут рассмотрены ниже), однако они были удобны тем, что позволяли использовать в качестве объекта-посредника структуры данных большего размера, например, при следующем техническом приеме. Описываем структуру:

typedef struct _MY_WORK_ITEM {
	WORK_QUEUE_ITEM Item;
	char AdditionalData[64];
} MY_WORK_ITEM, *PMY_WORK_ITEM; 

Данная структура создается и удаляется драйвером, что позволяет ей иметь нестандартную длину — главное, что начальный блок используется обычным для системы способом (как для WORK_QUEUE_ITEM).

Таблица 10.3. Прототип вызова IoAllocateWorkItem

PIO_WORKITEM IoAllocateWorkItem IRQL<=DISPATCH_LEVEL
Параметры Создает объект рабочего потока
IN PDEVICE_OBJECT pDevObject Объект устройства инициатора вызова
Возвращаемое значение

Указатель на созданный объект или NULL в случае неудачи

Новые (поддерживаются в Windows Me/2000/XP/2003) предлагаемые вызовы IoAllocateWorkItem, IoQueueWorkItem и IoFreeWorkItem перераспределили обязанности. Теперь вызов IoAllocateWorkItem (таблица 10.3) создает структуры типа IO_WORKITEM (разумеется, только размером sizeof(IO_WORKITEM)), которые "записываются" за соответствующим объектом устройства. Объект IO_WORKITEM инициализируется вызовом IoQueueWorkItem, который связывает с ним callback-процедуру драйвера и передаваемый при ее вызове контекстный аргумент. Вызов IoQueueWorkItem также помещает объект IO_WORKITEM в очередь объектов, тип которой определяется значением третьего параметра, то есть QueueType. В конце работы callback-процедура драйвера должна выполнить освобождение созданного объекта IO_WORKITEM вызовом IoFreeWorkItem.

Таблица 10.4. Прототип вызова IoQueueWorkItem

VOID IoQueueWorkItem IRQL<=DISPATCH_LEVEL
Параметры Инициализирует объект рабочего потока и помещает его в очередь (обычно используется сразу после вызова IoAllocateWorkItem)
IN PIO_WORKITEM pWorkItem Объект рабочего потока, созданный вызовом IoAllocateWorkItem
IN PIO_WORKITEM_ROUTINE
pWorkRoutine
Callback-процедура, предоставляемая драйвером (ее прототип описан в таблице 10.5)
IN WORK_QUEUE_TYPE QueueType Тип очереди. Драйвер должен предоставить одно из значений:
CriticalWorkQueue
DelayedWorkQueue
IN PVOID pContext Контекстный аргумент. Его получит callback-функция при вызове
Возвращаемое значение void

В том случае, если параметр QueueType при вызове будет равен CriticalWorkQueue, то объект IO_WORKITEM будет помещен в очередь для объектов с приоритетом в диапазоне RealTime (таблица 6.2), при значении DelayedWorkQueue — в очередь объектов с приоритетом Normal.

Следует помнить, что число объектов IO_WORKITEM, которые операционная система позволяет получить каждому объекту устройства (соответственно, разместить в своих очередях) не бесконечно, поэтому следует проверять результат вызова IoAllocateWorkItem на равенство NULL. Кроме того, не рекомендуется надолго задерживаться в callback-функции, поскольку это может затормозить извлечение из соответствующей очереди других IO_WORKITEM объектов, принадлежащих другим драйверам. В частности, не рекомендуется из таких потоков обращаться к другим драйверам вызовом IoCallDriver. Для выполнения продолжительных операций рекомендуется использовать полноценные системные программные потоки, создаваемые вызовом PsCreateSystemThread.

Термин 'системные рабочие потоки' (system worker threads) нельзя считать удачным, потому что он в точности копирует термин API пользовательского режима, обозначающий программные потоки пользовательского режима, которые уже никакими временными ограничениями не стеснены. Кроме того, лексическое отличие от "нормальных" системных программных потоков, с описания которых началась данная глава, просто неуловимо.

Таблица 10.5. Прототип callback-функции рабочего потока

VOID workCallback IRQL == PASSIVE_LEVEL
Параметры Функция, предоставляемая драйвером, которая будет вызвана при извлечении из очереди объекта IO_WORKITEM (вызывается в контексте, как для системного программного потока, см. выше)
IN PDEVICE_OBJECT pDevObject Объект устройства, которому принадлежит извлеченный из очереди объект IO_WORKITEM
IN PVOID pContext Контекстный аргумент — для получения дополнительной информации, "запланированной" при вызове IoQueueWorkItem (например, указатель на IRP пакет и т.п.)
Возвращаемое значение void

Вызываемая callback-функция должна освобождать IO_WORKITEM объект вызовом IoFreeWorkItem, таблица 10.6.

Таблица 10.6. Прототип вызова IoFreeWorkItem

VOID IoFreeWorkItem IRQL<=DISPATCH_LEVEL
Параметры Удаляет объект рабочего потока
PIO_WORKITEM pWorkItem Объект рабочего потока, созданный вызовом ранее IoAllocateWorkItem
Возвращаемое значение void

Драйвер не должен делать какие-либо предположения о внутренней организации объектов IO_WORKITEM и изменять данные внутри. Для работы с этими объектами следует использовать только описанные выше вызовы.