Объект синхронизации
Таблица 9
Объект синхронизации Уровень IRQL, на котором может работать запрашивающий синхронизацию поток Уровень IRQL, на котором будет работать запросивший синхронизацию поток при освобождении объекта синхронизации или его переходе в сигнальное состояние
Запрос без блокирования потока Запрос с блокированием потока.
Стандартная спин-блокировка (Standard Spin Lock) <= DISPATCH_LEVEL DISPATCHJLEVEL
Спин-блокировка для ISR, определенная по умолчанию (Default ISR Spin Lock) <= DIRQL DIRQL
Спин-блокировка для синхронизации с ISR (ISR Synchronize Spin Lock) <= Specified DIRQL Specified DIRQL
Мьютекс (Mutex) <=DISPATCH_LEVEL <DISPATCH LEVEL <=DISPATCH_LEVEL
Семафор (Semaphore) <=DISPATCKLLEVEL <DISPATCH_LEVEL <=DISPATCH_LEVEL
Событие синхронизации (Synchronization Event) <=DISPATCH_LEVEL <DISPATCH_LEVEL <=DISPATCH_LEVEL
Событие уведомления (Notification Event) <=DISPATCH_LEVEL <DISPATCH_LEVEL <=DISPATCH_LEVEL
Таймер синхронизации (Synchronization Timer) <=DISPATCH_LEVEL <DISPATCH_LEVEL -
Таймер уведомления (Notification Timer) <=DISPATCH_LEVEL <DISPATCH_LEVEL -
Процесс (Process) <=DISPATCH_LEVEL <DISPATCH_LEVEL -
Поток (Thread) <=DISPATCH_LEVEL <DISPATCH_LEVEL -
Файл (File) <=DISPATCH_LEVEL <DISPATCfi_LEVEL -
Ресурсы (Resources) < DISPATCH_LEVEL <DISPATCH_LEVEL <=DISPATCH_LEVEL
2.4.6. Рабочие потоки
2.4.6.1. Необходимость в создании рабочих потоков
Любой исполняемый код, как и код драйвера, работает в контексте некоторого потока. Мы пока не обсуждали способы, с помощью которых драйвер может создать собственный поток, поэтому предполагается, что поток, в котором выполняется код драйвера, принадлежит некоторой прикладной программе. Это означает, что прикладная программа создала такой поток для выполнения своего кода, а не кода нашего драйвера. Если код драйвера производит длительную обработку, либо драйвер использует механизм синхронизации с ожиданием освобождения некоторого ресурса, код прикладной программы, для выполнения которого и создавался поток, не выполняется.
Если этот поток единственный в прикладном процессе, то прикладная программа «висит».
Если описанная ситуация имеет место в диспетчерской функции драйвера верхнего уровня, мы «всего лишь» «подвесили» прикладную программу, непосредственно взаимодействующую с драйвером. В этом случае прикладная программа знает о такой возможности, и может поместить операции взаимодействия с драйвером (чтение, запись, отправка кодов управления) в отдельный поток. В этом случае драйвер может не беспокоиться о прикладной программе. Однако, такая ситуация довольно редка. Очень часто код драйвера работает в контексте случайного потока, то есть любого произвольного потока в системе. Такой поток ничего не знает о нашем драйвере и вышеописанная ситуация неприемлема. В этом случае драйвер должен создать свой собственный поток, в котором и производить длительную обработку, либо ожидание освобождения ресурсов.
Возможна другая ситуация, требующая обязательного создания потоков, когда драйверу необходимо выполнить операции на уровне IRQL меньшем DISPATCHJLEVEL, а код драйвера работает на повышенных уровнях IRQL, больших или равных DISPATCH_LEVEL.
2.4.6.2. Системные рабочие потоки
В процессе системной инициализации NT создает несколько потоков в процессе System. Эти потоки служат исключительно для выполнения работы, затребованной другими потоками. Такие потоки наиболее удобны в случаях, когда потоку с повышенным уровнем IRQL требуется выполнить работу на уровне IRQL PASSIVEJLEVEL.
В принципе, можно создать новый поток, однако создание нового потока и его планирование планировщиком является более ресурсоемким, чем использование существующего потока. Большинство стандартных компонент ОС, таких как компоненты файловой системы, используют для своих нужд готовые системные рабочие потоки.
Имеются ситуации, при которых использование системных рабочих потоков неприемлемо в силу их организации. Такими ситуациями являются необходимость в дли-
тельной (несколько сотен микросекунд) обработке внутри потока, либо длительное ожидание освобождения ресурса или наступления события.
В этих ситуациях драйвер должен создавать свой собственный поток.
Как организованы системные рабочие потоки? Как уже было сказано, в процессе системной инициализации NT создает несколько системных рабочих потоков. Число этих потоков фиксировано. Для всех потоков существует единая очередь, из которой поток выбирает адрес функции драйвера, которая должна быть выполнена в данном потоке. Такая функция называется рабочим элементом (Workltem). Функция выполняется в потоке до своего завершения, после чего поток выбирает из очереди следующий рабочий элемент. Если очередь пуста, поток блокируется до появления в очереди очередного рабочего элемента.
Существует три типа системных рабочих потоков: Delayed (замедленные), Critical (критические) и Hypercritical (сверхкритические). Все типы потоков создаются на уровне IRQL PASSIVE_LEVEL. Для каждого типа потоков будут различны:
• число потоков данного типа;
• базовый приоритет планирования потока, относящегося к данному типу;
• очередь рабочих элементов.
Число потоков каждого типа зависит от объема памяти и типа ОС. В таблице 10 указано число потоков и базовый приоритет планирования для ОС Win2000 Professional и Server.
Таблица 10. Число Системных Рабочих Потоков
Тип рабочего потока Объем системной памяти Базовый приоритет планирования
12-19 MB 20-64 MB >64MB
Delayed 3 3 3 Значение в диапазоне динамических приоритетов
Critical 3 Professional: 3 Server: 6 Professional: 5 Server: 10 Значение в диапазоне приоритетов реального времени
HyperCritical 1 1 1 Не документирован
Следует отметить, что использование единственного потока типа HyperCritical не документировано. ОС использует этот поток для выполнения функции - чистильщика, которая освобождает потоки при их завершении.
При постановке рабочего элемента в очередь указывается тип потока, которому предназначен рабочий элемент.
Для работы с системными рабочими потоками существует два набора функций -функции с префиксом Ех, и функции с префиксом 1о. Функции с префиксом Ех использовались в ОС NT 4.0 и более ранних версиях, и в Win2000 считаются устаревшими.
В любом случае, вначале драйвер должен инициализировать рабочий элемент с
помощью функций ExInitializeWorkltemQ или IoAllocateWorkItem(), поместить рабо- чий элемент в очередь с помощью функций ExQueueWorkltemQ или loQueueWorkltemQ, а при запуске функции, указанной в рабочем элементе, эта функция обязана освобо- дить занимаемые рабочим элементом ресурсы с помощью функций ExFreePoolQ или loFreeWorkltemQ.
2.4.6.3. Создание потоков драйвером
В случае, когда использование системных рабочих потоков невозможно, драйвер должен создать свой собственный поток. Для создания нового потока используется функция PsCreateSystemThreadQ. В качестве одного из параметров функция имеет описатель процесса, в контексте которого нужно создать поток. Чтобы правильно использовать описатель, код драйвера должен выполняться в контексте процесса, таблица описателей которого содержит описатель процесса, в контексте которого мы хотим создать поток. Если описатель процесса не указан (значение NULL), новый поток будет создан в контексте процесса System.
Для уничтожения потока из драйвера используется функция PsTerminate SystemThreadQ. Эта функция должна быть вызвана из самого уничтожаемого потока, так как она уничтожает текущий поток и не позволяет указать поток, который нужно уничтожить.
Вновь созданный поток будет работать на уровне IRQL PASSIVE_LEVEL и иметь базовое значение приоритета планирования равным 8 (динамический диапазон приоритетов, базовое значение для класса NORMAL). После создания код потока может изменить базовое значение приоритета планирования на любое значение в диапазоне динамических приоритетов либо приоритетов реального времени. Это делается с помощью функции KeSetPriorityThreadQ. Отметим, что это не повышение уровня приоритета планирования, после которого уровень приоритета постепенно снизится до базового значения, а именно установка нового базового значения приоритета.
Код потока может не только изменить значение приоритета планирования при уровне IRQL PASSIVE_LEVEL, но и повысить уровень IRQL.
Для этого служит функция KeRaiselrqlQ. Работа потока на повышенном уровне IRQL должна быть завершена как можно скорее, после чего должно быть восстановлено первоначальное значение IRQL с помощью функции KeLowerlrqlQ. Использование функции KeRaiselrqlQ для понижения IRQL и функции KeLowerlrqlQ для повышения IRQL не допускается, так как это приведет к возникновению синего экрана.
2.4.6.4. Потоки как диспетчерские объекты
Как говорилось в разделе, посвященном механизмам синхронизации, поток является диспетчерским объектом, который переходит в сигнальное состояние при своем завершении. Следующий пример демонстрирует способ синхронизации с помощью объекта-потока.
NTSTATUS DriverEntry( .... )
status = PsCreateSystemThread(&thread_handle,
0,
NULL,
0,
NULL,
thread_func,
pDevExt~>thread_context) ; if (status != STATUS_SUCCESS)
{
//обработка ошибки } else
{
status = ObReferenceobjectByHandle (thread_handle, THREAD_ALL_ACCESS, NULL,
KernelMode,
(PVOID*) &pDevExt->pThreadObject, NULL) ; if (status != STATUS_SUCCESS)
{ ' ' ': ' " ' ' ' ' '
//обработка ошибки
Функция потока:
VOID thread_func(PVOID Context)
{ ' ' ' '; , ' -
//Рабочий код потока
//Завершение потока PsTerminateSystemThread'(STATUS_SUCCESS) ;
Функция, ожидающая завершение работы потока: .... SomeFunc( .... )
status = KeWaitForSingleObject (pDevExt->pThreadObject,
Executive,
KernelMode,
FALSE ,
NULL) ; ObDereferenceObject (pDevExt->pThreadObject) ;
Прокомментируем этот пример. При создании потока с помощью функции PsCreateSystemThread() возвращается описатель потока в контексте процесса, в котором поток был создан. Важно понимать, что это может быть совершенно не тот процесс, в контексте которого была вызвана функция PsCreateSystem ThreadQ. В этом случае мы не можем напрямую воспользоваться функцией ObReference ObjectByHandle() для получения указателя на объект-поток по его описателю.
Существует простейший способ решения этой проблемы, основанный на том факте, что функция - точка входа в драйвер DriverEntry, всегда вызывается в контексте потока System.
Вызов функции PsCreateSystemThreadQ следует производить из DriverEntry, и при этом указывать создавать поток в контексте процесса System. Получив описатель созданного потока, можно получить указатель на объект-поток с помощью ObReferenceObjectByHandleQ, и в дальнейшем пользоваться этим указателем в контексте любого процесса и потока. При завершении использования объекта-потока надо обязательно освободить его с помощью вызова ObDereferenceObjectQ. Все вышесказанное иллюстрируется Рисунок 13.
Если драйверу все же необходимо по каким-либо причинам создать поток в контексте процесса, отличного от процесса System, либо создать поток в контексте процесса System, находясь в контексте другого потока, ему нужно каким-то образом попасть в контекст памяти процесса, в таблице описателей которого хранится информация о нужном процессе. Для этого служит недокументированная функция KeAttachProcessQ. После необходимой обработки необходимо восстановить предыдущее состояние с помощью вызова KeDetachProcessQ.
Вышесказанное относилось только к ОС Windows NT 4.0. В ОС Win2000 появилась специальная таблица описателей, называемая таблицей описателей ядра (kernel handle table), которая может быть доступна с помощью экспортируемого имени ObpKernelHandleTable.
Если этот поток единственный в прикладном процессе, то прикладная программа «висит».
Если описанная ситуация имеет место в диспетчерской функции драйвера верхнего уровня, мы «всего лишь» «подвесили» прикладную программу, непосредственно взаимодействующую с драйвером. В этом случае прикладная программа знает о такой возможности, и может поместить операции взаимодействия с драйвером (чтение, запись, отправка кодов управления) в отдельный поток. В этом случае драйвер может не беспокоиться о прикладной программе. Однако, такая ситуация довольно редка. Очень часто код драйвера работает в контексте случайного потока, то есть любого произвольного потока в системе. Такой поток ничего не знает о нашем драйвере и вышеописанная ситуация неприемлема. В этом случае драйвер должен создать свой собственный поток, в котором и производить длительную обработку, либо ожидание освобождения ресурсов.
Возможна другая ситуация, требующая обязательного создания потоков, когда драйверу необходимо выполнить операции на уровне IRQL меньшем DISPATCHJLEVEL, а код драйвера работает на повышенных уровнях IRQL, больших или равных DISPATCH_LEVEL.
2.4.6.2. Системные рабочие потоки
В процессе системной инициализации NT создает несколько потоков в процессе System. Эти потоки служат исключительно для выполнения работы, затребованной другими потоками. Такие потоки наиболее удобны в случаях, когда потоку с повышенным уровнем IRQL требуется выполнить работу на уровне IRQL PASSIVEJLEVEL.
В принципе, можно создать новый поток, однако создание нового потока и его планирование планировщиком является более ресурсоемким, чем использование существующего потока. Большинство стандартных компонент ОС, таких как компоненты файловой системы, используют для своих нужд готовые системные рабочие потоки.
Имеются ситуации, при которых использование системных рабочих потоков неприемлемо в силу их организации. Такими ситуациями являются необходимость в дли-
тельной (несколько сотен микросекунд) обработке внутри потока, либо длительное ожидание освобождения ресурса или наступления события.
В этих ситуациях драйвер должен создавать свой собственный поток.
Как организованы системные рабочие потоки? Как уже было сказано, в процессе системной инициализации NT создает несколько системных рабочих потоков. Число этих потоков фиксировано. Для всех потоков существует единая очередь, из которой поток выбирает адрес функции драйвера, которая должна быть выполнена в данном потоке. Такая функция называется рабочим элементом (Workltem). Функция выполняется в потоке до своего завершения, после чего поток выбирает из очереди следующий рабочий элемент. Если очередь пуста, поток блокируется до появления в очереди очередного рабочего элемента.
Существует три типа системных рабочих потоков: Delayed (замедленные), Critical (критические) и Hypercritical (сверхкритические). Все типы потоков создаются на уровне IRQL PASSIVE_LEVEL. Для каждого типа потоков будут различны:
• число потоков данного типа;
• базовый приоритет планирования потока, относящегося к данному типу;
• очередь рабочих элементов.
Число потоков каждого типа зависит от объема памяти и типа ОС. В таблице 10 указано число потоков и базовый приоритет планирования для ОС Win2000 Professional и Server.
Таблица 10. Число Системных Рабочих Потоков
Тип рабочего потока Объем системной памяти Базовый приоритет планирования
12-19 MB 20-64 MB >64MB
Delayed 3 3 3 Значение в диапазоне динамических приоритетов
Critical 3 Professional: 3 Server: 6 Professional: 5 Server: 10 Значение в диапазоне приоритетов реального времени
HyperCritical 1 1 1 Не документирован
Следует отметить, что использование единственного потока типа HyperCritical не документировано. ОС использует этот поток для выполнения функции - чистильщика, которая освобождает потоки при их завершении.
При постановке рабочего элемента в очередь указывается тип потока, которому предназначен рабочий элемент.
Для работы с системными рабочими потоками существует два набора функций -функции с префиксом Ех, и функции с префиксом 1о. Функции с префиксом Ех использовались в ОС NT 4.0 и более ранних версиях, и в Win2000 считаются устаревшими.
В любом случае, вначале драйвер должен инициализировать рабочий элемент с
помощью функций ExInitializeWorkltemQ или IoAllocateWorkItem(), поместить рабо- чий элемент в очередь с помощью функций ExQueueWorkltemQ или loQueueWorkltemQ, а при запуске функции, указанной в рабочем элементе, эта функция обязана освобо- дить занимаемые рабочим элементом ресурсы с помощью функций ExFreePoolQ или loFreeWorkltemQ.
2.4.6.3. Создание потоков драйвером
В случае, когда использование системных рабочих потоков невозможно, драйвер должен создать свой собственный поток. Для создания нового потока используется функция PsCreateSystemThreadQ. В качестве одного из параметров функция имеет описатель процесса, в контексте которого нужно создать поток. Чтобы правильно использовать описатель, код драйвера должен выполняться в контексте процесса, таблица описателей которого содержит описатель процесса, в контексте которого мы хотим создать поток. Если описатель процесса не указан (значение NULL), новый поток будет создан в контексте процесса System.
Для уничтожения потока из драйвера используется функция PsTerminate SystemThreadQ. Эта функция должна быть вызвана из самого уничтожаемого потока, так как она уничтожает текущий поток и не позволяет указать поток, который нужно уничтожить.
Вновь созданный поток будет работать на уровне IRQL PASSIVE_LEVEL и иметь базовое значение приоритета планирования равным 8 (динамический диапазон приоритетов, базовое значение для класса NORMAL). После создания код потока может изменить базовое значение приоритета планирования на любое значение в диапазоне динамических приоритетов либо приоритетов реального времени. Это делается с помощью функции KeSetPriorityThreadQ. Отметим, что это не повышение уровня приоритета планирования, после которого уровень приоритета постепенно снизится до базового значения, а именно установка нового базового значения приоритета.
Код потока может не только изменить значение приоритета планирования при уровне IRQL PASSIVE_LEVEL, но и повысить уровень IRQL.
Для этого служит функция KeRaiselrqlQ. Работа потока на повышенном уровне IRQL должна быть завершена как можно скорее, после чего должно быть восстановлено первоначальное значение IRQL с помощью функции KeLowerlrqlQ. Использование функции KeRaiselrqlQ для понижения IRQL и функции KeLowerlrqlQ для повышения IRQL не допускается, так как это приведет к возникновению синего экрана.
2.4.6.4. Потоки как диспетчерские объекты
Как говорилось в разделе, посвященном механизмам синхронизации, поток является диспетчерским объектом, который переходит в сигнальное состояние при своем завершении. Следующий пример демонстрирует способ синхронизации с помощью объекта-потока.
NTSTATUS DriverEntry( .... )
status = PsCreateSystemThread(&thread_handle,
0,
NULL,
0,
NULL,
thread_func,
pDevExt~>thread_context) ; if (status != STATUS_SUCCESS)
{
//обработка ошибки } else
{
status = ObReferenceobjectByHandle (thread_handle, THREAD_ALL_ACCESS, NULL,
KernelMode,
(PVOID*) &pDevExt->pThreadObject, NULL) ; if (status != STATUS_SUCCESS)
{ ' ' ': ' " ' ' ' ' '
//обработка ошибки
Функция потока:
VOID thread_func(PVOID Context)
{ ' ' ' '; , ' -
//Рабочий код потока
//Завершение потока PsTerminateSystemThread'(STATUS_SUCCESS) ;
Функция, ожидающая завершение работы потока: .... SomeFunc( .... )
status = KeWaitForSingleObject (pDevExt->pThreadObject,
Executive,
KernelMode,
FALSE ,
NULL) ; ObDereferenceObject (pDevExt->pThreadObject) ;
Прокомментируем этот пример. При создании потока с помощью функции PsCreateSystemThread() возвращается описатель потока в контексте процесса, в котором поток был создан. Важно понимать, что это может быть совершенно не тот процесс, в контексте которого была вызвана функция PsCreateSystem ThreadQ. В этом случае мы не можем напрямую воспользоваться функцией ObReference ObjectByHandle() для получения указателя на объект-поток по его описателю.
Существует простейший способ решения этой проблемы, основанный на том факте, что функция - точка входа в драйвер DriverEntry, всегда вызывается в контексте потока System.
Вызов функции PsCreateSystemThreadQ следует производить из DriverEntry, и при этом указывать создавать поток в контексте процесса System. Получив описатель созданного потока, можно получить указатель на объект-поток с помощью ObReferenceObjectByHandleQ, и в дальнейшем пользоваться этим указателем в контексте любого процесса и потока. При завершении использования объекта-потока надо обязательно освободить его с помощью вызова ObDereferenceObjectQ. Все вышесказанное иллюстрируется Рисунок 13.
Если драйверу все же необходимо по каким-либо причинам создать поток в контексте процесса, отличного от процесса System, либо создать поток в контексте процесса System, находясь в контексте другого потока, ему нужно каким-то образом попасть в контекст памяти процесса, в таблице описателей которого хранится информация о нужном процессе. Для этого служит недокументированная функция KeAttachProcessQ. После необходимой обработки необходимо восстановить предыдущее состояние с помощью вызова KeDetachProcessQ.
Вышесказанное относилось только к ОС Windows NT 4.0. В ОС Win2000 появилась специальная таблица описателей, называемая таблицей описателей ядра (kernel handle table), которая может быть доступна с помощью экспортируемого имени ObpKernelHandleTable.