Базовый приоритет. Класс приоритета и относительный приоритет
Базовый приоритет. Класс приоритета и относительный приоритет
Ядро NT предоставляет функции для назначения потоку любого из 31 уровня приоритетов (кроме зарезервированного нулевого уровня). Программно назначенное потоку значение приоритета называют базовым приоритетом.
Подсистема Win32 не позволяет непосредственно назначать потоку базовое значение приоритета. Вместо этого используется комбинация двух значений:
Класс приоритета процесса, назначаемый процессу при его создании (дальше мы будем ссылаться на этот термин как на класс приоритета).
Относительное значение приоритета потока внутри класса приоритета процесса (относительный приоритет).
На Рисунок 4 показаны группы взаимосвязанных приоритетов. Максимальный (31) и минимальный (1) из возможных приоритетов определяются в Win32 как THREAD_ PRIORITY_TIME_CRITICAL и THREAD_PRIORITY_IDLE соответственно.
В таблице 3 приведены все возможные классы приоритетов. По умолчанию, процесс имеет класс NORMAL_PRIORITY_CLASS, если при вызове функции CreateProcess() не было указано другого. Прикладная программа может получать/изменять класс приоритета процесса с помощью функций Win32-API GetPriotityClass() / SetPriorityClass() .
Быстрые мьютексы
Быстрые мьютексы
Быстрый мьютекс являются урезанным вариантом мьютекса, который не может быть рекурсивно захвачен. Поскольку быстрый мьютекс не является диспетчерским объектом, он не может использоваться функцией KeWaitForSingleObject() или KeWaitForMultipleObjects(). Вместо этого нужно использовать функцию ExAcquireFast Mutex(). Эквивалента быстрым мьютексам на пользовательском уровне нет, поэтому они могут использоваться только для синхронизации кода режима ядра.
Функции работы с быстрыми мьютексами:
1. VOID ExInitializeFastMutex(IN PFAST_MUTEX FastMutex);
2. VOID ExAcquireFastMutex(IN PFAST_MUTEX FastMutex);
3. BOOLEAN ExTryToAcquireFastMutex(IN PFAST_MUTEX FastMutex);
4. VOID ExReleaseFastMutex(IN PFAST_MUTEX FastMutex);
5. VOID ExAcquireFastMutexUnsafe(IN PFAST_MUTEX FastMutex);
6. VOID ExReleaseFastMutexUnsafe (IN PFAST_MUTEX FastMutex).
Целевой процессор для DPC (DPC Target Processor)
В дополнение к важности, каждый DPC Объект имеет целевой процессор (target processor). Это значение хранится в поле Number Объекта DPC. Целевой процессор показывает, ограничено ли выполнение DPC заданным процессором в системе, и, если да, то каким процессором. По умолчанию, Reinitialize Dpc() не определяет целевой процессор. Следовательно, по умолчанию, процедуры DPC будут работать на процессоре, на котором они запрошены (то есть, DPC будет вызван на процессоре, на котором была вызвана подпрограмма KelnsertQueueDpc()).
DPC может быть ограничено выполнением на указанном процессоре, используя функцию KeSetTargetProcessorDpc(), прототип которой:
VOID KeSetTargetProcessorDpc(IN PKDPC Dpc,
IN CCHAR Number);
Где:
Dpc - Указывает на объект DPC, для которого должен быть установлен целевой процессор;
Number - отсчитываемый от нуля номер процессора, на котором должен быть выполнен DPC.
Подобно важности DPC, целевой процессор DPC почти никогда не устанавливается драйвером устройства. Заданное по умолчанию значение, которое служит для выполнения DPC на текущем процессоре, почти всегда желательно.
Когда для Объекта DPC установлен конкретный целевой процессор, такой Объект DPC будет всегда ставиться в Очередь DPC указанного процессора. Таким образом, например, даже когда KelnsertQueueDpc() вызывается на процессоре 0, Объект DPC с установленным в качестве целевого процессора Процессором 1 будет вставлен в Очередь DPC Процессора 1.
Динамические приоритеты и приоритеты реального времени
Динамические приоритеты и приоритеты реального времени
Приоритеты планирования делятся на две главных группы:
динамические приоритеты (dynamic priorities);
приоритеты реального времени (real-time priorities).
Динамические приоритеты имеют значения в диапазоне 1-15. Они названы динамическими, потому что ОС может динамически изменять приоритет потока в этом диапазоне.
Приоритеты реального времени имеют значения в диапазоне 16-31. ОС не может изменять значение приоритета потока, находящееся в этом диапазоне.
Имеется два важных отличия между динамическими приоритетами и приоритетами реального времени.
Поток с приоритетом реального времени может сохранять контроль над процессором до тех пор, пока не появится поток с большим или равным значением приоритета. Таким образом, пока выполняется поток реального времени, потоки с меньшим значением приоритета никогда не получат шанса исполниться (механизм вытесняющей многозадачности не задействован). Такой поток должен сам освободить процессор.
Однако в любом случае при появлении потока с большим или равным значением приоритета задействуется механизм вытесняющей многозадачности.
В случае потоков с динамическими приоритетами, потоки с меньшими приоритетами также не могут получить шанса на исполнение, пока готовы к исполнению потоки с большими приоритетами.
Однако, в ряде случаев планировщик повышает приоритет потоков в диапазоне динамических приоритетов. Это дает возможность рано или поздно выполниться любому потоку с приоритетом в этом диапазоне.
К механизму повышения приоритетов применимы следующие утверждения:
1. Система никогда не меняет приоритет потоков из диапазона приоритетов реального времени.
2. Повышение приоритета не может вызвать его переход в диапазон приоритетов реального времени, то есть превысить значение 15.
3. Повышение приоритета операционной системой является временным. Каждый раз, когда поток исчерпывает отведенный ему квант времени, значение его приоритета уменьшается на единицу. Так\происходит до достижения значения базового приоритета.
4. Операционная система не может снизить приоритет ниже уровня базового приоритета.
5. Повышение приоритета может происходить несколько раз подряд.
DIRQLs
DIRQLs
Все уровни IRQL выше Dispatch Level относятся к аппаратным прерываниям. Аппаратные прерывания периферийных устройств системы (например, дисков, клавиатур, последовательных портов) отображаются на уровни IRQL в диапазоне Device Level. Из Таблицы 5 вы можете видеть, что на процессорах Intel этот диапазон лежит от 3 до 26, а на машинах Alpha - от 3 до 4. Тот факт, что существует такая разница между двумя диапазонами, имеет следствием то, что NT в действительности не располагает обычные прерывания устройств в соответствии с приоритетом. Даже на процессорах Intel, где аппаратные прерывания могут иметь различные значения IRQL, назначение IRQL является случайным.
Так как это может быть важным моментом для разработчиков драйвера устройства в некоторых системах, необходимо повторить снова: связь между двумя IRQ, назначенными двум определенным устройствам не обязательно сохраняется, когда IRQL назначены этим устройствам. Назначен ли устройству с более важным IRQ более высокий (то есть более важный) уровень IRQL, полностью зависит от HAL. В действительности, в большинстве HAL стандартных многопроцессорных систем х86, для систем, которые используют архитектуры APIC, связь между IRQ и IRQL не сохраняется.
Уровни IRQL выше Device Level имеют предопределенные связи с определенными прерываниями. Profile Level относится к таймеру профилирования ядра (механизму измерения производительности системы), Clock Level относится к такту системных часов, IPI Level относится к сигналам, посылаемым от одного CPU к другому, и Power Level относится к событиям сбоя в питании.
NT резервирует, но в настоящий момент не использует IRQL High Level.
IRQL highjevel всегда определяется как самый высокий уровень IRQL в системе Windows NT. Этот уровень IRQL используется для NMI (Немаскируемого Прерывания) и других прерываний очень высокого приоритета. В редких случаях, когда драйвер устройства нуждается в блокировании прерываний на конкретном процессоре на короткий период, драйвер может поднимать IRQL до уровня high_level, но такое повышение уровня IRQL драйвером устройства считается очень решительным шагом, и в Windows NT это почти не требуется.
Подъем IRQL до уровня HIGH_LEVEL большинству драйверов Windows NT желательно никогда не делать. Блокирование прерываний является широко используемым методом для достижения синхронизации на других операционных системах (типа DOS или Win9x). Однако, в Windows NT, простое поднятие до IRQL HIGH_LEVEL в целях синхронизации не будет работать на многопроцессорных системах. Код режима ядра выполняет сериализацию, используя спин-блокировки, которые подробно описаны в разделе "Механизмы синхронизации".
Диспетчер Объектов
Диспетчер Объектов
Диспетчер Объектов (object manager), который является вероятно наименее известной из подсистем Исполнительной Системы NT, является также одним из наиболее важных. Главная роль операционной системы - это управление физическими и логическими ресурсами компьютера. Другие подсистемы Исполнительной Системы используют Диспетчер Объектов, чтобы определять и управлять объектами, которые представляют ресурсы.
Диспетчером объектов нельзя манипулировать из пользовательского режима напрямую, а его пространство имен является невидимым.
В таблице 2 приведен список объектов, определенных в NT 4.0, и подсистем исполнительной системы, которые управляют ими.
Диспетчер Объектов исполняет обязанности:
Поддержание единого пространства имен для всех именованных объектов системы.
Отвечает за создание, удаление и управление именованными и неименованными объектами ОС, представляющими системные ресурсы.
Обязанности по управлению объектами включают в себя идентификацию и подсчет ссылок. Когда прикладная программа открывает ресурс, Диспетчер Объектов или определяет местонахождение связанного с ресурсом объекта, или создает новый объект. Вместо возвращения прикладной программе, которая открыла ресурс, указателя на объект, Диспетчер Объектов возвращает непрозрачный (не имеющий смысла) идентификатор, называемый дескриптором. Значение дескриптора уникально в рамках прикладной программы, которая открыла ресурс, но не уникально между различными прикладными программами.
Диспетчерские объекты
Диспетчерские объекты
Спин-блокировки абсолютно необходимы в случаях, когда требуется синхронизация кода, работающего на повышенных уровнях IRQL. Но основное правило в NT -работать на повышенных уровнях IRQL в течение как можно более короткого времени значит, использование спин-блокировок следует по возможности избегать. Диспетчерские объекты NT - это набор механизмов синхронизации, рассчитанных на применение в основном для уровня IRQL PASSIVE_LEVEL.
Базой для любого диспетчерского объекта является структура DISPAT-CHER_HEADER (определена в ntddk.h). Общим свойством любого диспетчерского объекта является то, что в каждый момент времени такой объект находится в одном из двух состояний - сигнальном или несигнальном, а также то, что поток, ожидающий захвата диспетчерского объекта, блокирован и помещен в список ожидания, находящийся в структуре DISPATCHER_HEADER.
Блокирование потока означает его особое состояние, при котором он не занимает время процессора. Блокированный поток не будет поставлен планировщиком в очередь на исполнение до тех пор, пока не будет выведен из состояния блокирования. Это фундаментально отличает ожидание освобождения любого диспетчерского объекта от попытки захвата спин-блокировки. В последнем случае, как было сказано, поток, захватывающий спин-блокировку, «крутится» в бесконечном цикле до момента успешного захвата (отсюда и название спин-блокировка - блокировка вращения в бесконечном цикле).
Единственным отличием одного диспетчерского объекта от другого является правило, в соответствии с которым меняется состояние объекта (переход в сигнальное или несигнальное состояние). В таблице 8 перечислены диспетчерские объекты и моменты изменения их состояния.
Диспетчерские точки входа драйвера
Диспетчерские точки входа драйвера
Информация, требуемая для выполнения запроса ввода/вывода, содержится в различных элементах как фиксированной части IRP, так и стека размещения ввода/вывода. Рассмотрим эти элементы. Структура поля Parameters в стеке размещения ввода/ вывода зависит от кода главной и второстепенной функции ввода/вывода. Нас в основном будет интересовать структура поля Parameters для запросов чтения, записи и пользовательских запросов ввода/вывода:
1. IRPJVIJ_READ. Параметры для этого функционального кода содержат следующее:
Parameters.Read.Length (ULONG) содержит размер в байтах буфера инициатора запроса.
Parameters. Read.Key (ULONG) содержит ключевое значение, которое нужно использовать при чтении. Обычно представляет интерес только для драйверов файловой системы.
Parameters .Read.ByteOfFset (LARGE_INTEGER) содержит смещение (обычно в файле), с которого должна начаться операция чтения.
2. IRP_MJ_WRITE. Параметры для этого функционального кода следующие:
Parameters. Write.Length (ULONG) содержит размер в байтах буфера инициатора запроса.
Parameters.Write.Key (ULONG) содержит ключевое значение, которое нужно использовать при записи. Обычно представляет интерес только для драйверов файловой системы.
Parameters.Write.ByteOffset (LARGE_INTEGER) содержит смещение (обычно в файле) с которого должна начаться операция записи.
3. IRPJMJ_DEVICE_CONTROL. Параметры для этого функционального кода следующие:
Parameters.DeviceloControl.OutputBufferLength (ULONG) содержит длину в байтах буфера OutBuffer.
Parameters.DeviceloControl.InputBufferLength (ULONG) содержит длину в байтах буфера InBuffer.
Parameters. DeviceloControl.ControlCode (ULONG) содержит код управления вводом/выводом, идентифицирующий запрашиваемую функцию управления устройством. Этот управляющий код обычно предварительно определен драйвером с использованием макрокоманды CTL_CODE.
Parameters.DeviceloControl.TypeSInputBuffer (PVOID) содержит виртуальный адрес буфера инициатора запроса InBuffer (см. функцию Win32 API DeviceloControl()). Адрес обычно используется только тогда, когда IOCTL использует METHOD_NEITHER.
DPC на многопроцессорных системах
DPC на многопроцессорных системах
Вопреки тому, что утверждалось в некоторых других источниках, и, как должно быть очевидно из предшествующего обсуждения, одна и та же подпрограмма DPC может выполняться на нескольких процессорах одновременно. Нет абсолютно никакого блокирования со стороны Микроядра, чтобы предотвратить это.
Рассмотрим случай драйвера устройства, который в одно и то же время имеет несколько запросов, ожидающих обработки. Устройство драйвера прерывается на Процессоре 0, выполняется программа обработки прерывания драйвера и запрашивает DPC для завершения обработки прерывания. Это стандартный путь, которому следуют драйверы в Windows NT. Когда завершается программа обработки прерывания, и система готова возвратиться к прерванному потоку пользователя, уровень IRQL процессора О понижается от DIRQL, на котором выполнялась ISR, до IRQL dispatch_level. В результате, Микроядро обслуживает Очередь DPC, удаляя Объект DPC драйвера и вызывая указанную в нем подпрограмму DPC. На Процессоре 0 теперь выполняется подпрограмма DPC драйвера.
Сразу после вызова подпрограммы DPC драйвера, устройство генерирует прерывание еще раз. Однако на этот раз, по причинам, известным только аппаратуре, прерывание обслуживается на Процессоре 1. Снова, программа обработки прерывания драйвера запрашивает DPC. И, снова, когда программа обработки прерывания закончится, система (Процессор 1) готова возвратиться к прерванному потоку пользователя. При этом IRQL процессора 1 понижается до уровня IRQL dispatch_level, и Микроядро обслуживает Очередь DPC. Делая так (и по-прежнему выполняясь на Процессоре 1), микроядро удаляет Объект DPC драйвера, и вызывает подпрограмму DPC драйвера. Подпрограмма DPC драйвера теперь выполняется на Процессоре 1. Предполагая, что подпрограмма DPC драйвера еще не завершила выполнение на Процессоре 0, заметим, что та же самая подпрограмма DPC теперь выполняется параллельно на обоих процессорах.
Этот пример подчеркивает важность использования в драйверах надлежащего набора механизмов многопроцессорной синхронизации. В особенности, в функции DPC должны использоваться спин-блокировки для сериализации доступа к любым структурам данных, к которым нужно обратиться как к единому целому, при условии, что конструкция драйвера такая, что одновременно может произойти несколько вызовов DPC.
DPC-объекты Вызов DPC описывается
Многочисленные обращения к DPC
Каждый DPC описан конкретным Объектом DPC. В результате всякий раз, когда вызывается функция KelnsertQueueDpc() и выясняется, что переданный ей Объект DPC уже находится в той же самой Очереди DPC, функция KelnsertQueueDpcQ просто возвращается (не выполняя никаких действий). Таким образом, всякий раз, когда Объект DPC уже находится в Очереди DPC, любые последующие попытки постановки в очередь того же самого Объекта DPC, осуществляемые до удаления Объекта DPC из очереди, игнорируются. Это имеет смысл, так как Объект DPC может физически быть включен только в одну Очередь DPC одновременно.
Может возникнуть очевидный вопрос: Что произойдет, когда сделан запрос постановки Объекта DPC в очередь, но система уже выполняет подпрограмму DPC, указанную этим Объектом DPC (на этом же или другом процессоре)? Ответ на этот вопрос может быть найден при внимательном чтении предыдущего раздела. Когда Микроядро обслуживает Очередь DPC, оно удаляет Объект DPC из головы очереди, и только потом вызывает подпрограмму DPC, указанную Объектом DPC. Таким образом, когда подпрограмма DPC вызвана, Объект DPC уже удален из Очереди DPC процессора. Поэтому, когда сделан запрос на постановку Объекта DPG в очередь и система находится внутри подпрограммы DPC, заданной в этом Объекте DPC, DPC ставится в очередь как обычно.
DpcForlsr
DpcForlsr
Как уже было сказано ранее в этой главе, наиболее часто DPC используются для завершения Программы Обработки Прерывания (ISR). Для того, чтобы упростить драйверам устройств запросы DPC для завершения ISR из их функций ISR, Диспетчер ввода/вывода определяет специальный DPC, который может использоваться для этой цели. Этот DPC называется DpcForlsr.
Диспетчер ввода/вывода вставляет Объект DPC в каждый Объект Устройство, который он создает. Этот внедренный Объект DPC инициализируется драйвером устройства, обычно при первой загрузке драйвера, посредством вызова функции IoInitializeDpcRequest().
IoInitializeDpcRequest() принимает на входе указатель на Объект Устройство, в который внедрен Объект DPC, указатель на функцию драйвера для вызова, и значение контекста для передачи этой функции. IoInitializeDpcRequest(), в свою очередь, вызывает KelnitializeDpc(), чтобы инициализировать внедренный Объект DPC, передавая указатель на функцию драйвера как параметр DeferredRoutine, и значение контекста как параметр DeferredContext.
Чтобы запросить DPC из ISR, драйвер просто вызывает loRequestDpc(), передавая указатель на Объект Устройство. IoRequestDpc(), в свою очередь, вызывает KelnsertQueueDpc() для Объекта DPC, внедренного в Объект-Устройство.
Поскольку все драйверы устройства имеют Объекты-Устройства, и все драйверы, которые используют прерывания, также используют DPC, использование механизма DpcForlsr Диспетчера ввода/вывода очень удобно. Фактически, большинство драйверов устройств в Windows NT никогда напрямую не вызывают функции KelnitializeDpc() или KelnsertQueueDpc(), а вместо этого вызывают loInitializeDpcRequest() и IoRequestDpc().
Функции работы с памятью
Функции работы с памятью
Из всего вышеизложенного нужно выделить основные моменты:
1. Все адресное пространство процесса (контекст памяти) делится на две области
системное адресное пространство (верхние 2 Гб) и пользовательское адресное пространство (нижние 2 Гб):
Системное адресное пространство всегда одинаково, вне зависимости от текущего контекста памяти.
Пользовательское адресное пространство разное для каждого контекста памяти.
2. Код пользовательского режима и режима ядра пользуется для доступа к памяти разными селекторами:
Для одного контекста памяти селекторы адресуют одну и ту же физическую память, но с разными режимами доступа.
Селекторы не имеют ничего общего с контекстом памяти.
По селекторам для ядра можно получить доступ (по крайней мере, для чтения) ко всему адресному пространству в данном контексте памяти, а по селекторам для ядра - только для пользовательской области памяти.
3. Код в пользовательском адресном пространстве всегда вытесняемый, если не предприняты меры по созданию новой записи в каталоге страниц, указывающих на невытесняемую память.
4. Код, работающий на уровне IRQL большем или равном DISPATCH_LEVEL, может использовать только невыгружаемую память (соответственно, сам код должен находиться в невыгружаемой памяти).
5. В ОС существуют функции для выполнения любой работы с памятью:
Выделение/освобождение памяти в выгружаемой/невыгружаемой, кешируемой/некешируемой памяти.
Преобразование адресов памяти (виртуальный в физический, физический в виртуальный).
Проверка доступности памяти. Рассмотрим эти функции более подробно.
Функции управления очередью низкого уровня
Функции управления очередью низкого уровня
Для организации очереди с помощью функций низкого уровня используется стандартная структура LISTJENTRY.
typedef struct _LIST_ENTRY {
struct _LISTJ5NTRY ^volatile Flink; // Указатель
// на следующий элемент списка
struct _LIST_ENTRY ^volatile Blink; // Указатель
// на предыдущий элемент списка
} LIST_ENTRY, *PLIST_ENTRY;
Очередь, как это видно из определения структуры, двунаправленная.
В структуре DeviceExtension обычно создается экземпляр структуры LISTJENTRY, представляющей голову очереди, который затем инициализируется с помощью функции InitializeListHead(). После этого можно добавлять или удалять записи в очередь. Для этого используются функции InsertHeadList(), InsertTailList(), RemoveHeadList(), RemoveTailList(), RemoveEntryList().
Пакеты IRP добавляются в очередь в диспетчерской функции, скорее всего работающей на уровне IRQL равном PASSIVE_LEVEL. При этом выниматься из очереди они могут функциями, работающими на любом уровне IRQL, причем функции, выбирающие IRP из очереди, могут вытеснять функции, помещающие IRP в очередь. Возникает проблема синхронизации. Если ее не решить, незаконченная операция помещения IRP в очередь может быть прервана операцией выборки IRP из очереди, что приведет к появлению синего экрана.
Синхронизация доступа к разделяемому ресурсу производится с помощью спин-блокировки (механизмы синхронизации будут рассмотрены в следующем разделе). Поскольку операции добавления и удаления записей в очередь на уровнях IRQL PASSIVE_LEVEL и DISPATCH_LEVEL очень распространены, для их безопасного
осуществления предусмотрена специальная функция: ExInterlocked...List(). Для использования этой функции должна быть создана и инициализирована спин-блокировка. Создается она обычно там же, где и голова очереди (обычно в DeviceExtension), и инициализируется после инициализации головы очереди. Например:
typedef struct _DEVICE_EXTENSION
LIST__ENTRY ListHead; KSPIN^LOCK ListLock;
}DEVICE_EXTENSION, *PDEVICE_EXTENSION;
//В функции DriverEntry
InitializeListHead (& (pDeviceExtension->ListHead) ) ;
KelnitializeSpinLock (& (pDeviceExtension->ListLock) ) ;
//после этого можно добавлять и удалять записи
PLIST_ENTRY pOldHead = ExInterlockedlnsertHeadList ( & (pDeviceExtension->ListHead) , pNewListEntry, & (pDeviceExte'nsion->ListLock) ) ;
Как видно из определения структуры LIST_ENTRY, она не содержит полей для хранения собственно данных (например, указателя на пакет IRP). Поэтому распространенный способ использования структуры LIST_ENTRY - включение ее экземпляра в состав более общей структуры.
Для организации очереди пакетов IRP, в каждом пакете IRP в поле Tail.Over-lay.ListEntry содержится экземпляр структуры LIST_ENTRY. При этом встает вопрос, как, зная указатель на структуру LIST_ENTRY, получить указатель на структуру IRP, в состав которой входит LIST_ENTRY. Для этого DDK предоставляет специальный макрос CONTAINING_RECORD:
#define CONTAINING_RECORD (address, type, field) \
((type *) ( (PCHAR) (address) - (ULONG_PTR) (&((type *) 0) ->f ield) ) )
Где: Address - Известный адрес некоторого поля структуры, адрес которой необходимо получить;
Туре - Тип структуры, адрес которой необходимо получить;
Field- Имя поля внутри искомой структуры, адрес этого поля передан в параметре address.
Применительно к IRP, мы должны будем написать что-то вроде:
PListEntry = ExInterlockedRemoveHeadList ( & (pDeviceExtension->ListHead) , & (pDeviceExtension->ListLock) ) ;
plrp = CONTAINING_RECORD(pListEntry, IRP, Tail. Overlay. ListEntry) ; //далее - обработка IRP
Организация очереди пакетов IRP показана на Рисунок 12.
Функции управления очередью высокого уровня - «Очередь Устройства» (Device Queue)
Функции управления очередью высокого уровня - «Очередь Устройства» (Device Queue)
Драйвер создает дополнительные Очереди Устройства с помощью выделения памяти из невыгружаемой памяти под дополнительные объекты-Очереди Устройства (KDEVICE_QUEUE) и инициализирует эти объекты с помощью функции Kelnitia-HzeDeviceQueue(). Добавление пакетов IRP в эти очереди производится с помощью функции KelnsertDevieeQueue() или KelnsertByKeyDeviceQueue(), а выборка пакетов из очереди - KeRemoveDeviceQueue(), KeRemoveByKeyDeviceQueue() или KeRemoveEntryDeviceQueue().
Для организации очереди пакетов IRP используется структура типа KDEVICE_ QUEUE_ENTRY, указатель на которую содержится в пакете IRP в поле Tail.Over-lay.DeviceQueueEntry.
Общая архитектура Windows NT
Глава 2. Общая архитектура Windows NT
Архитектура памяти
Пакет запроса ввода/вывода (IRP)
Рабочие потоки
Характеристики Объекта DPC
Характеристики Объекта DPC
Объекты DPC имеют две характеристики, которые влияют на путь, которым они обрабатываются. Этими характеристиками являются поля Importance и Number.
Характеристики подсистемы ввода/вывода
Характеристики подсистемы ввода/вывода
В предыдущем разделе мы рассмотрели схему использования системных сервисов, то есть прохождение запроса ввода/вывода от приложения к драйверу и обратно. Компонентом ОС, отвечающим за реализацию этой схемы, является Диспетчер ввода/ вывода. Диспетчер ввода/вывода является компонентом более общей модели - подсистемы ввода/вывода. Подсистема ввода/вывода включает в себя все компоненты, которые обеспечивают возможность осуществления ввода/вывода. В число этих компонент входит Диспетчер ввода/вывода и все драйверы режима ядра. В числе характеристик подсистемы ввода/вывода NT принято выделять следующие:
1. согласованность и высокая структурированность;
2. переносимость между процессорными архитектурами;
3. конфигурируемость;
4. вытесняемость и прерываемость;
5. поддержка многопроцессорности;
6. объектная базированность (но не объектная ориентированность);
7. асинхронность;
8. подсистема ввода/вывода управляется пакетами;
9. подсистема ввода/вывода многоуровневая (послойная модель).
Как уже говорилось, подсистема ввода/вывода NT управляется пакетами. При таком подходе каждый запрос ввода/вывода описывается своим собственным пакетом запроса ввода/вывода (I/O Request Packet - IRP). При задействовании системного сервиса (например, при запросе на чтение или запись в файл) Диспетчер ввода/вывода обрабатывает этот запрос путем создания пакета IRP, Описывающего запрос, и затем передает указатель на этот пакет драйверу для обработки.
Имя устройства и символическая связь
Имя устройства и символическая связь
При создании объекта-устройства также может быть указано его имя, которое будет видимо в директории «\Device» пространства имен диспетчера объектов. Объекты- устройства используются в Windows NT как точки входа в пространства имен, не контролируемые менеджером объектов. Если при разборе имени объекта диспетчер объектов встречает объект-устройство, то он вызывает метод разбора, связанный с этим устройством.
Если имя не указано, объект-устройство может быть использован только внутри драйвера и недоступен извне. Если быть более точным, такой объект-устройство можно использовать, если иметь указатель на описывающую его структуру.
Именованный объект-устройство доступен для использования через вызов системного сервиса NtCreateFile().
Для обращения к именованному объекту-устройству из подсистемы Win32 посредством функции CreateFile() должны быть предприняты дополнительные действия. Функция CreateFile() ищет имя устройства в директории Диспетчера Объектов «\??» (NT 4.0 и Win2000), либо «\DosDevices»(NT 3.51). Поэтому в соответствующей директории, а для большей совместимости это должна быть «\DosDevices», должен быть создан объект (символическая связь), указывающий на имя устройства в директории «\Device». Обычно связь создается в самом драйвере, хотя это можно сделать и из прикладной программы пользовательского режима с помощью Win32- функции DefmeDosDevice().
Интегрированная поддержка сети
Интегрированная поддержка сети
Windows NT разработана со встроенной сетевой поддержкой и включает широкую поддержку сети, интегрированную с системой ввода/вывода и интерфейсом Win32 API.
Четырьмя основными типами сетевого программного обеспечения являются сетевые сервисы, сетевые API, протоколы и драйверы сетевых карт, располагающиеся друг под другом, формируя сетевой стек.
Windows NT предоставляет хорошо определенные интерфейсы для каждого слоя в стеке, чтобы в дополнение к поставляющемуся с Windows NT множеству различных сетевых интерфейсов API, протоколов и драйверов сетевых карт, пользователи могли расширять сетевые возможности ОС путем разработки собственного сетевого программного обеспечения.
IRQL PASSIVE_LEVEL, APC_LEVEL и DISPATCH_LEVEL
IRQL PASSIVE_LEVEL, APC_LEVEL и DISPATCH_LEVEL
Наименьший приоритет IRQL в таблице 5 - Passive Level. Этот уровень является обычным уровнем IRQL, на котором производится работа в операционной системе, как в пользовательском режиме, так и в режиме ядра. Когда процессор находится в этом состоянии, не происходит никакой Деятельности по обработке прерываний. Подпрограмма, выполняющаяся на уровне IRQL passive_level, может быть подвергнута прерыванию и вытеснению почти всем, чем угодно еще случившемся в системе. Так, потоки, выполняющиеся на IRQL passive_level, подвергаются вытеснению Диспетчером (планировщиком) по истечении их кванта времени.
Большинство подпрограмм уровня исполнительной системы Windows NT (то есть подпрограммы режима ядра, не принадлежащие Микроядру и HAL) стремятся держать уровень IRQL как можно более низким. В большинстве случаев, это приводит к выполнению большинства подпрограмм на уровне IRQL passive_level. Эта стратегия повышает возможность выполнения действий с высоким уровнем IRQL.
Следующие два уровня IRQL выше Passive Level (APC Level и Dispatch Level) -программные уровни прерываний, связанные с планировщиком.
Когда система находится на уровне APC Level, исполняющийся поток не будет получать запросы АРС, которые NT обычно использует для операций завершения ввода/вывода.
IRQL dispatch_level используется внутри Windows NT для двух различных действий:
Обработка Отложенных Вызовов Процедур (DPCs);
Выполнение Диспетчера (планировщик NT).
DPC обработка обсуждена позже в этой главе, в собственном разделе. Следовательно, мы ограничимся обсуждением Диспетчера. Диспетчер, является планировщиком потоков Windows NT. Он отвечает за реализацию алгоритма планирования, который выбирает, какой поток будет выполняться, и осуществляет приоритетное прерывание (выгрузку) в конце кванта времени.
Диспетчер (планировщик) Windows NT получает запросы, чтобы выполнить операцию перепланирования на уровне IRQL dispatch_level. Когда операционная система решает заменить поток, который выполняется на текущем процессоре, она иногда может вызывать Диспетчер напрямую.
Однако, когда система выполняется на уровне IRQL выше, чем dispatchjevel, она запрашивает программное прерывание уровня dispatch_level. Результатом явится запуск на текущем процессоре Диспетчера в следующий раз, когда уровень dispatch_level станет наиболее приоритетным для обслуживания системой.
Рассмотрим, например, случай потока, выполняющегося в режиме пользователя. Так как он выполняется в режиме пользователя, этот поток, конечно, выполняется на IRQL passive_level. В то время как поток выполняется, часы периодически генерируют прерывания, чтобы указать операционной системе прохождение промежутка времени. С каждым переданным тиком часов, программа обработки прерывания часов уменьшает остающийся у выполняющегося в данный момент потока квант времени. Когда оставшийся у потока квант уменьшается до нуля, программа обработки прерывания часов генерирует прерывание уровня dispatchjevel, чтобы запросить запуск Диспетчера и выбор им следующего потока для выполнения. Так как программа обработки прерывания часов выполняется на уровне IRQL большем, чем dispateh_level (она выполняется на уровне IRQL CLOCK2_LEVEL на х86 процессорах), обработка запроса для Диспетчера откладывается.
После генерирования прерывания уровня dispatch_level, программа обработки прерывания часов заканчивает любую другую работу, которую она должна сделать и управление возвращается Микроядру.
Затем Микроядро распознает следующее самое высоко приоритетное прерывание, которое находится в режиме ожидания. Каждое прерывание обслуживается по очереди. Когда для обслуживания не остается никаких прерываний уровня выше dispatchjevel, выполняется программа обработки прерывания уровня dispatch_level. Эта программа обработки прерывания обрабатывает список DPC (обсуждаемый позже), и задействует Диспетчер, чтобы выбрать новый поток выполнения.
Когда задействуется Диспетчер, он обращает внимание, что квант времени текущего потока был уменьшен до нуля. Затем Диспетчер осуществляет алгоритм планирования Windows NT, чтобы определить следующий поток, который нужно запланировать.Если выбран новый поток (мог бы быть перепланирован предыдущий поток), происходит переключение контекста. Если нет никаких ожидающих вызовов АРС для вновь выбранного потока, код потока будет выполнен, когда система возвратится обратно к уровню IRQL PASSIVE_LEVEL.
Исполнительная система (The Executive)
Исполнительная система (The Executive)
Надо отметить, что в разных источниках понятие исполнительной системы интерпретируется по-разному. Например, в документации DDK исполнительная система - это совокупность компонентов, исполняющихся в привилегированном режиме - режиме ядра, и формирующих законченную ОС за исключением пользовательского интерфейса. В данном случае к компонентам исполнительной системы относятся также само ядро и слой абстрагирования от оборудования (HAL). В других источниках ядро и HAL рассматриваются как отдельные модули. В этой книге решено было следовать именно этому разделению, хотя бы потому, что HAL не предоставляет системных сервисов, к которым могут обращаться защищенные подсистемы.
Подсистемы Исполнительной Системы NT составляют наиболее существенный слой в режиме ядра, и они исполняют больщую часть функций, традиционно связанных с операционными системами. В Таблице 1 перечислены подсистемы Исполнительной Системы NT, и Рисунок 2 показывает их позицию в архитектуре NT. Эти подсистемы имеют разные обязанности и названия, так что Вы могли бы подумать, что они являются различными процессами. Например, когда программа типа Microsoft Word запрашивает обслуживание операционной системы типа распределения памяти, поток управления передается от программы Word в режим ядра через «родной» интерфейс системных сервисов NT. Тогда обработчик системного сервиса для распределения памяти напрямую вызывает соответствующую функцию Диспетчера Виртуальной Памяти. Запрошенное распределение памяти выполняется в контексте процесса Word, который запросил его, то есть нет никакого переключения контекста к другому системному процессу.
Драйверы Windows NT, включая драйверы устройств, промежуточные драйверы и драйверы файловых систем, после загрузки рассматриваются как часть исполнительной системы, а точнее как часть системы ввода/вывода.
Все компоненты, кроме диспетчера кэша, предоставляют определенное множество системных сервисов, к которым могут обращаться защищенные подсистемы.
И каждый компонент исполнительной системы реализует множество внутренних процедур, доступных только компонентам исполнительной системы.
Префиксы в названиях внутренних процедур соответствуют названиям компонентов исполнительной системы, обеспечивающих эти процедуры, например: «Ех» для функций, реализуемых компонентом Ex(ecutive) Support — исполнительным модулем, Ps - диспетчером процессов, Ob — диспетчером объектов, Iо — диспетчером ввода/ вывода, Mm - диспетчером памяти, Сс - диспетчером кэша, Se - монитором безопасности.
Исполнительная система не исполняется постоянно в собственном процессе, а работает в контексте некоторого существующего процесса, завладевая выполняющимся потоком, когда происходит важное системное событие. Например, когда поток вызывает системный сервис, в результате чего происходит программное прерывание, или когда внешнее устройство генерирует прерывание, ядро получает управление потоком, который выполнялся процессором. Оно выпдлняет соответствующий системный код для обработки события и затем возвращает управление коду, выполнявшемуся перед прерыванием.
Исполнительный модуль (executive support) - это особый компонент исполнительной системы ОС Windows NT, давший свое имя целой группе модулей операционной системы. Он отвечает за многие разнообразные функции, включая управление очередями (их блокирование), управление резидентной и нерезидентной системной областью памяти, увеличение/уменьшение значения глобальной переменной и др. Этот компонент обеспечивает также системные рабочие потоки, которые драйверы NT, особенно драйверы файловых систем, используют для выполнения необходимой работы.
Использование обычных спин-блокировок
Использование обычных спин-блокировок
1. VOID KeInitializeSpinLock(IN PKSPIN_LOCK SpinLock); Эта функция инициализирует объект ядра KSPIN_LOCK. Память под спин-блокировку уже должна быть выделена в невыгружаемой памяти.
2. VOID KeAcquireSpinLock(IN PKSPIN_LOGK SpinLock, OUT PKIRQL Oldlrql); Эта функция захватывает спин-блокировку. Функция не вернет управление до успеха захвата блокировки. При завершении функции уровень IRQL повышается до уровня DISPATCH_LEVEL. Во втором параметре возвращается уровень IRQL, который был до захвата блокировки (он должен быть <= DISPATCH_LEVEL).
3. VOID KeReleaseSpinLock(IN PKSPINJLOCK SpinLock, OUT PKIRQL Newlrql); Эта функция освобождает спин-блокировку и устанавливает уровень IRQL в значение параметра Newlrql. Это должно быть то значение, которое вернула функция KeAcquireSpinLock() в параметре Oldlrql.
4. VOID KeAcquireLockAtDpcLevel(IN PKSPIN_LOCK SpinLock); Эта оптимизированная функция захватывает спин-блокировку кодом, уже работающем на уровне IRQL DISPATCH_LEVEL. В этом случае изменение уровня IRQL не требуется. На однопроцессорной платформе эта функция вообще ничего не делает, так как синхронизация обеспечивается самой архитектурой IRQL.
5. VOID KeReleaseLockFromDpcLevel(IN PKSPIN_LOCK SpinLock); Эта функция освобождает спин-блокировку кодом, захватившим блокировку с помощью функции KeAcquireLockAtDpcLevel(). На однопроцессорной платформе эта функция ничего не делает.
Пример использования обычных спин-блокировок:
typedef struct _DEVICE_EXTENSION
KSPIN_LOCK spinlock }DEVICE_EXTENSION, *PDEVICE_EXTENSION;
*
NTSTATUS DriverEntry (....)
KelnitializeSpinLock(&extension->spinlock); }
NTSTATUS DispatchReadWrite( ...)
{
KIRQL Oldlrql;
KeAcquireSpinLock(&extension->spinlock, &01dlrql); // произвести обработку данных, // защищенных спин-блокировкой
KeReleaseSpinLock(&extension->spinlock, Oldlrql); }
Ядро
Ядро
Ядро ОС Windows NT реагирует на прерывания и исключения, занимается планированием потоков, сохранением и восстановлением контекстов потоков, направляет потоки на выполнение, выполняет межпроцессорную синхронизацию, предоставляет набор сервисов, элементарных объектов и интерфейсов, используемых компонентами исполнительной системы. Большая часть ядра зависит от типа процессора.
Драйверы NT и компоненты исполнительной системы вызывают внутренние процедуры, обеспечиваемые ядром, названия которых начинаются с префикса Ke(rnel) -ядро.
Ядро экспортирует два основных типа объектов ядра: объекты-диспетчеры (dispatcher objects) и управляющие объекты (control objects). Объекты ядра отличаются от объектов исполнительного уровня, создаваемых и управляемых менеджером объектов, и зачастую являются базисом для них.
Объект-диспетчер используется для планирования и синхронизации и имеет атрибут, определяющий его состояние - «занят» или «свободен». Объектами-диспетчерами являются: события, мьютексы, семафоры и таймеры.
Управляющие объекты используются для управления системными операциями. Управляющими объектами являются: АРС-объект (Asynchronous Procedure Call), содержащий адрес процедуры асинхронного вызова и указатель на объект-поток, который будет исполнять данный вызов; DPC-объект (Deferred Procedure Call), содержащий адрес процедуры отложенного вызова; объект-прерывание, отвечающий за установление соответствия между определенным вектором прерывания и процедурой обработки прерывания (Interrupt Service Routine, ISR) драйвера устройства.
Эмуляция нескольких ОС
Эмуляция нескольких ОС
Подсистемы окружения операционной системы NT реализованы как системы типа клиент/сервер. Как часть процесса компиляции, прикладные программы прикрепляются на этапе компоновки к API операционной системы, который экспортируют подсистемы окружения ОС NT . Связывание на этапе компоновки подключает прикладную программу к клиентским DLLs подсистем окружения, которые осуществляют экспорт API. Например, Win32 программа - это клиент подсистемы окружения Win32, поэтому она связана с клиентскими DLL Win32, включая Kerael32.dll, gdi32.dll, и user32.dll. Программа POSIX связана с клиентской DLL POSIX - psxdll.dll.
Клиентские DLL выполняют задачи от имени их серверов, но они выполняются, как часть клиентского процесса. Как показано Рисунок 1, в некоторых случаях пользовательская DLL может полностью реализовывать API без необходимости обращения к помощи сервера; в других случаях сервер должен помочь. Помощь сервера обычно необходима только когда должна быть модифицирована общая информация, связанная с подсистемой окружения. Когда пользовательская DLL требует помощи от сервера, DLL посылает сообщение, известное, как вызов локальной процедуры (LPC) на сервер. Когда сервер завершает указанный запрос и возвращает ответ, DLL может завершить функцию и возвратить управление клиенту. И пользовательская DLL и сервер могут использовать «родной» API, когда это необходимо. API подсистем окружения дополняют «родной» API специфическими функциональными возможностями или семантикой.
Как используются IRQL
Как используются IRQL
NT управляет прерываниями путем отображения уровней прерывания контроллера прерываний в собственную аппаратно-независимую таблицу уровней прерываний. Осуществляет отображение слой абстрагирования от оборудования (HAL - модуль NT, специально написанный для конкретных контроллеров прерываний, материнских плат, либо наборов микросхем процессоров). В мультипроцессорных системах принять прерывание может любой процессор, поэтому NT поддерживает независимый IRQL для каждого процессора. IRQL процессора представляет уровень прерывания, который маскируется в данный момент процессором и прямо соответствует (аналогичен) прерываниям, которые маскирует контроллер прерываний CPU. Поскольку уровни IRQL NT не привязаны к какой-либо спецификации оборудования, NT также может отображать в свою иерархию приоритетов неаппаратные типы прерываний. Операционная система использует программные прерывания в основном для запуска операций планирования, таких как переключение потоков или обработка завершения ввода/вывода.
Когда NT обслуживает аппаратное прерывание, NT устанавливает IRQL процессора в соответствующее значение таблицы IRQL NT. NT программирует контроллер прерываний так, чтобы он маскировал прерывания с более низким приоритетом, и драйверы устройств (так же как и NT) могут запрашивать IRQL для определения его значения. (Как мы увидим позднее, NT позволяет выполнять некоторые операции только когда IRQL меньше определенных значений.)
Размер таблицы IRQL разнится между архитектурами процессоров (Intel, Alpha и др.) для того, чтобы лучше отображать уровни прерываний, предоставляемые контроллерами прерываний, однако уровни прерываний, которые могут найти интересными разработчики драйверов устройств и разработчики NT, имеют символические имена.
Коды функции ввода/вывода
Коды функции ввода/вывода
NT использует коды функции ввода/вывода для определения конкретной операции ввода/вывода, которая будет иметь место для конкретного объекта-файла. Коды функции ввода/вывода Windows NT разделены на коды главной и второстепенной функции ввода/вывода. Оба кода находятся в IRP в Стеке Размещения Ввода/вывода драйвера. Главные функциональные коды определены символами, которые начинаются с IRP_MJ_. Перечислим некоторые из главных кодов функции ввода/вывода:
1. IRP_MJ_CREATE. Этот главный функциональный код соответствует созданию нового объекта-файла, либо при обращении к существующему устройству или файлу, либо при создании нового файла. Этот функциональный код представляет запросы, идущие через функцию Win32 CreateFile() или базовый системный сервис NtCreateFile().
2. IRP_MJ_CLOSE. Этот главный функциональный код соответствует уничтожению предварительно созданного объекта-файла. Этот функциональный код представляет запросы, идущие через функцию Win32 CloseHandle() или базовый системный сервис NtClose(). К появлению этого запроса ввода/вывода может привести не каждый вызов CloseHandle(), так как на соответствующий файловый объект могут ссылаться другие, еще не закрытые описатели. Объект не может быть уничтожен, пока для него есть описатели. Кроме того, для каждого объекта диспетчер объектов ведет подсчет ссылок, и объект не может быть уничтожен, пока его число ссылок не равно нулю.
3. IRP_MJ_READ. Этот главный функциональный код выполняет операцию чтения для существующего объекта-файла. Этот функциональный код представляет запросы, идущие через функцию Win32 ReadFile() или базовый системный сервис NtReadFile().
4. IRP_MJ_WRITE. Этот главный функциональный код выполняет операцию записи для существующего объекта-файла. Этот функциональный код представляет запросы, идущие через функцию Win32 WriteFile() или системный сервис NtWriteFile().
5. IRP_MJ_DEVICE_CONTROL. Этот главный функциональный код выполняет определенную драйвером функцию для существующего объекта-файла. Этот функциональный код представляет запросы, идущие через функцию Win32 DeviceloControl() или базовый системный сервис NtDeviceloControlFile().
6. IRP_MJ_INTERNAL_DEVICE_CONTROL. Этот главный функциональный код выполняет определенную драйвером функцию для существующего объекта-файла. Никаких API уровня пользователя, соответствующих этой функции, нет. Эта функция используется, когда один драйвер посылает запрос ввода/вывода другому драйверу.
Законченный список кодов функции ввода/вывода представлен в NTDDK.H. Второстепенные коды функции ввода/вывода в NT определены символами, которые начинаются с IRP_MN_. NT обычно избегает использование второстепенных функциональных кодов для перезагрузки главной функции для драйверов устройства, приветствуя вместо этого использование Кодов Управления вводом/выводом (I/O Control Codes, IOCTL). Поэтому, почти все IRP, полученные драйверами устройства, имеют второстепенный функциональный код IRP_MN_NORMAL (который имеет значение 0x00). Вообще, второстепенные коды функции ввода/вывода используются исключительно файловыми системами и сетевыми транспортами. Например, одним из второстепенных кодов функции ввода/вывода, специфичным для файловой системы, является IRP_MN_COMPRESSED, указывающий, что данные должны быть записаны на том в сжатом формате.
Главные и второстепенные коды функции ввода/вывода, связанные с конкретным IRP, сохранены в полях MajorFunction и MinorFunction текущего Стека Размещения Ввода/вывода в IRP. На эти поля можно ссылаться, как показано в примере проверки главных и второстепенных функциональных кодов IRP:
loStack = loGetCurrentlrpStackLocation (Irp) ;
if (IoStack->MajorFunction == IRP_MJ_READ)
{
if (IoStack->MinorFunction == IRP_MN_NORMAL)
{
// что-то делать
}
}
Контекст исполнения и уровень IRQL
Контекст исполнения и уровень IRQL
Говоря о точках входа в драйвер, необходимо отметить контекст, при котором эти точки входа могут быть вызваны.
Вначале необходимо определиться с тем, что мы подразумеваем под контекстом исполнения?
Контекст исполнения определяется двумя составляющими:
исполняемый в настоящее время поток (контекст планирования потока - thread scheduling context);
контекст памяти процесса, которому принадлежит поток.
Текущий контекст исполнения может принадлежать одному из трех классов:
контекст процесса «System» (далее - системный контекст);
контекст конкретного потока и процесса;
контекст случайного потока и процесса (далее - случайный контекст).
Различные точки входа в драйвер могут вызываться в контексте, принадлежащем одному из этих классов.
DriverEntry всегда вызывается в системном контексте.
Диспетчерские функции для драйверов верхнего уровня (то есть получающих запрос от прикладных программ) вызываются в контексте инициирующего запрос потока.
Диспетчерские функции драйверов остальных уровней, получающие запрос от драйвера верхнего уровня, вызываются в случайном контексте.
Все точки входа, связанные с сериализацией запросов ввода/вывода или с обработкой прерываний и DPC, вызываются в случайном контексте.
DriverEntry всегда вызывается на IRQL, равным PASSIVE_LEVEL.
Диспетчерские точки входа вызываются на IRQL, равным PASSIVE_LEVEL или APC_LEVEL.
Вызов отложенных процедур - на DISPATCH_LEVEL
Функции обработки прерываний - на одном из DIRQL.
Механизмы синхронизации
Механизмы синхронизации
В операционной системе с вытесняющей многозадачностью, да еще поддерживающей несколько процессоров, остро встает задача синхронизации доступа к совместно используемым ресурсам компьютера, будь то аппаратное устройство или структура в памяти.
Многопоточность
Многопоточность
Каждая исполняющаяся в NT программа представляется как процесс.
Процесс (process) - это программа (статическая последовательность команд и данные) и системные ресурсы, необходимые для ее работы. ОС предоставляет каждому процессу адресное пространство, выделенное для программы, и гарантирует, что программа каждого процесса будет направляться на выполнение в определенном порядке и в нужное время. Чтобы процесс смог заработать, он должен включать, по крайней мере, один поток исполнения (thread of execution).
Поток (thread) - единица исполнения в NT. Поток - это сущность внутри процесса, которую ядро направляет на исполнение, он может принадлежать только одному процессу. Поток состоит из указателя текущей команды, пользовательского стека, стека ядра и набора значений регистров. Все потоки процесса имеют одинаковый доступ к его адресному пространству, описателям объектов и другим ресурсам. Потоки реализованы как объекты-потоки.
Начальный поток возникает при создании процесса, и затем он может создать дополнительные потоки.
Каждый поток имеет свой собственный приоритет, в соответствии с которым, ОС будет принимать решение о его запуске. При этом принадлежность потока к конкретному процессу не учитывается.
Многоуровневая модель драйверов
Многоуровневая модель драйверов
Ранее в качестве одной из характеристик подсистемы ввода/вывода упоминалась ее многоуровневость. Что это такое?
NT позволяет выстраивать драйверы в соответствии с некоторой функциональной иерархией. При этом, например, одни драйверы имеют своей единственной целью обмен данными с некоторым физическим устройством. Что это за данные и что с ними делать, такие драйвера не знают. Другие драйвера выполняют обработку данных, но не знают в точности, как эти данные получены и как будут отправлены. Такая концепция разделения полномочий драйверов носит название многоуровневой (или послойной) модели драйверов (layered driver model), а сами драйвера - уровневыми драйверами (layered drivers).
В NT 4.0 концепция многоуровневых драйверов занимает важное место, но ее использование не является обязательным требованием.
В Win2000 все драйвера, считающиеся родными, будут уровневыми (для того, чтобы драйвер считался родным для Win2000, он должен как минимум поддерживать управление питанием, а для этого он должен быть уровневым). Большинство драйверов, которые в NT4 мы считали монолитными, в Win2000 будут по своей сути уровневыми. Будем выделять следующие типы драйверов:
1.драйверы, представляющие некоторый уровень в многоуровневой архитектуре. Далее именно эти драйверы мы будем называть уровневыми драйверами;
2. драйверы-фильтры;
3. драйверы файловой системы (File System Driver, FSD);
4. мини-драйверы (mini-driver).
Для каждого типа драйверов существует свой протокол реализации многоуровневой структуры. Мы рассмотрим только уровневые драйверы и драйверы-фильтры.
Функции работы с мьютексами ядра:
1) VOID KeInitializeMutex(IN PKMUTEX Mutex, IN ULONG Level); Эта функция инициализирует мьютекс. Память под мьютекс уже должна быть выделена. После инициализации мьютекс находится в сигнальном состоянии.
2) LONG KeReleaseMutex(IN PKMUTEX Mutex, IN BOOLEAN Wait); Эта функция освобождает мьютекс, с указанием того, последует ли сразу после этого вызов функции ожидания мьютекса.
Если параметр Wait равен TRUE, сразу за вызовом KeReleaseMutexQ должен следовать вызов одной из функций ожидания KeWaitXxxQ. В этом случае гарантируется, что пара функций - освобождение мьютекса и ожидание - будет выполнена как одна операция, без возможного в противном случае переключения контекста потока. Возвращаемым значением будет 0, если мьютекс был освобожден, то есть переведен из несигнального состояния в сигнальное. В противном случае возвращается ненулевое значение.
3) LONG KeReadStateMutex(IN PKMUTEX Mutex); Эта функция возвращает состояние мьютекса - сигнальное или несигнальное.
2.4.5.2.3. Семафоры
Семафоры являются более гибкой формой мьютексов. В отличие от мьютексов, программа имеет контроль над тем, сколько потоков одновременно могут захватывать семафор.
Семафор инициализируется с помощью функции KeInitializeSemaphore(): VOID KelnitializeSemaphore( IN PKSEMAPHORE Semaphore, IN LONG Count, IN LONG Limit); Где:
Count - начальное значение, присвоенное семафору, определяющее число свободных в данный момент ресурсов. Если Count=0, семафор находится в несигнальном состоянии (свободных ресурсов нет), если >0 - в сигнальном;
Limit - максимальное значение, которое может достигать Count (максимальное число свободных ресурсов).
Функция KeReleaseSemaphoreQ увеличивает счетчик семафора Count на указанное в параметре функции значение, то есть освобождает указанное число ресурсов. Если при этом значение Count превышает значение Limit, значение Count не изменяется и генерируется исключение STATUS_SEMAPHORE_COUNT_EXCEEDED.
При вызове функции ожидания счетчик семафора уменьшается на 1 для каждого разблокированного потока (число свободных ресурсов уменьшается). Когда он достигает значения 0, семафор переходит в несигнальное состояние (свободных ресурсов нет). Использование семафора не зависит от контекста потока или процесса в том смысле, что занять ресурс семафора может один поток, а освободить его - другой, но драйвер не должен использовать семафоры в случайном контексте потока, так как в этом случае будет заблокирован случайный поток, не имеющий к драйверу никакого отношения.
Семафоры следует использовать в ситуациях, когда драйвер создал собственные системные потоки.
2.4.5.2.4. События
События(еуеп1з) позволяют проводить синхронизацию исполнения различных потоков, то есть один или несколько потоков могут ожидать перевода события в сигнальное состояние другим потоком.
При этом события могут быть двух видов:
• События, при переводе которых в сигнальное состояние будет разблокирован только один поток, после чего событие автоматически переходит в не сигнальное состояние. Такие события носят название события синхронизации (synchronization events).
• События, при переводе которых в сигнальное состояние будут разблокированы все ожидающие их потоки. Событие должно быть переведено в несигнальное состояние вручную. Такие события носят название оповещающих (notification event).
Функции работы с событиями:
1) KelnitializeEventQ инициализирует событие. Память под событие уже должна быть выделена. При инициализации указывается тип - синхронизация или оповещение, а также начальное состояние - сигнальное или несигнальное. Имя события задать нельзя. Функция может быть использована в случайном контексте памяти на уровне IRQL PASSIVE_LEVEL.
2) IoCreateNotificationEvent(), IoCreateSynchronizationEvent() создают новое или открывает существующее событие с заданным именем. Если объект с таким именем существует, он открывается, если не существует, то создается. Имя события обычно указывается в директории диспетчера объектов «\BaseNamedObjects». Именно в этой директории содержатся имена событий, создаваемых или открываемых \?т32-функциями CreateEventQ/OpenEventQ.
Функция возвращает как указатель на объект-событие, так и его описатель в таблице описателя текущего процесса. Для уничтожения объекта необходимо использовать функцию ZwCloseQ с описателем в качестве параметра. Описатель должен быть использован в контексте того процесса, в котором он был получен на уровне IRQL PASSIVE_LEVEL.
3) KeClearEventQ и KeResetEvent() сбрасывают указанное событие в несигнальное состояние.
Отличие между функциями в том, что KeResetEventQ возвращает состояние события до сброса. Функции могут быть вызваны на уровне IRQL меньшем или равном DISPATCHJLEVEL.
4) KeSetEventQ переводит событие в сигнальное состояние и получает предыдущее состояние. Одним из параметров является логическая переменная, указывающая, будет ли за вызовом KeSetEventQ немедленно следовать вызов функции ожидания. Если параметр TRUE, то гарантируется, что вызов этих двух функций будет выполнен как одна операция.
В случае событий оповещения сброс события в несигнальное состояние должен быть сделан вручную. Обычно это делает тот же код, который перевел событие в сигнальное состояние.
Следующий код корректно уведомляет все блокированные потоки о наступлении ожидаемого ими события:
KeSetEvent(&DeviceExt->Event, О, NULL);
KeClearEvent(&DeviceExt->Event);
2.4.5.2.5. Быстрые мыотексы
Быстрый мьютекс являются урезанным вариантом мьютекса, который не может быть рекурсивно захвачен. Поскольку быстрый мьютекс не является диспетчерским объектом, он не может использоваться функцией KeWaitForSingleObjectQ или KeWaitForMultipleObjectsQ. Вместо этого нужно использовать функцию ExAcquireFast MutexQ. Эквивалента быстрым мьютексам на пользовательском уровне нет, поэтому они могут использоваться только для синхронизации кода режима ядра.
Функции работы с быстрыми мьютексами:
1) VOID ExInitializeFastMutex(IN PFAST_MUTEX FastMutex);
2) VOID ExAcquireFastMutex(IN PFAST_MUTEX FastMutex);
3) BOOLEAN ExTryToAcquireFastMutex(IN PFAST_MUTEX FastMutex);
4) VOID ExReleaseFastMutex(IN PFAST_MUTEX FastMutex);
5) VOID ExAcquireFastMutexUnsafe(IN PFAST_MUTEX FastMutex);
6) VOID ExReleaseFastMutexUnsafe (IN PFAST_MUTEX FastMutex).
2.4.5.3. Ресурсы Исполнительной системы
Ресурсы являются вариантом быстрого мьютекса. Ресурсы не являются диспетчерскими объектами, поэтому они не могут иметь имя и использоваться в функции
KeWaitForSingleObject() или KeWaitForMultipleObjectsQ. Ресурсы предоставляют две формы захвата:
• Эксклюзивный - в этом случае ресурс ведет себя как обычный мьютекс - поток, который попытается захватить такой ресурс для эксклюзивного или совместного использования, будет блокирован.
• Совместно используемый - в этом случае ресурс может быть одновременно захвачен для совместного использования любым числом потоков.
Ресурсы идеально подходят для защиты структур данных, которые могут одновременно читаться несколькими потоками, но должны модифицироваться в каждый момент времени только одним потоком.
Для работы с ресурсами существуют функции запроса эксклюзивного доступа, неэксклюзивного доступа и преобразования уже полученного неэксклюзивного доступа в эксклюзивный и, наоборот, без промежуточных операций освобождения ресурса и запроса нового режима доступа. Все функции должны вызываться на уровне IRQL меньшем DISPATCH_LEVEL.
Функции работы с ресурсами:
1) NTSTATUS ExInitializeResourceLite(IN PERESOURCE Resource);
2) VOID ExReinitializeResourceLite(IN PERESOURCE Resource);
3) BOOLEAN ExAcquireResourceExclusiveLite(IN PERESOURCE Resource^ BOOLEAN Wait);
4) BOOLEAN ExTryToAcquireResourceExclusiveLite(IN PERESOURCE Resource);
5) BOOLEAN ExAcquireResourceSharedLite(IN PERESOURCE Resource^ BOOLEAN Wait);
6) BOOLEAN ExAcquireSharedStarveExclusive(IN PERESOURCE Resource^ BOOLEAN Waif);
7) BOOLEAN ExAcquireSharedWaitForExclusive(IN PERESOURCE Resource,®* BOOLEAN Waif);
8) VOID ExConvertExclusiveToSharedLite(IN PERESOURCE Resource);
9) BOOLEAN ExIsResourceAcquiredExclusiveLite(IN PERESOURCE Resource);
10) USHORT ExIsResourceAcquiredSharedLite(IN PERESOURCE Resource);
11) ULONG ExGetExclusiveWaiterCount(IN PERESOURCE Resource);
12) ULONG ExGetSharedWaiterCount(IN PERESOURCE Resource);
13) NTSTATUS ExDeleteResourceLite(IN PERESOURCE Resource);
14) VOID ExReleaseResourceForThreadLite(IN PERESOURCE Resource;
15) IN ERESOURCEJTHREAD ResourceThreadld).
2.4.5.4. Обобщенная таблица механизмов синхронизации
В таблице 9 представлены механизмы синхронизации и особенности использования каждого из них.
Модель модифицированного микроядра
Модель модифицированного микроядра
На NT иногда ссылаются как на операционную систему на основе микроядра (microkernel-based operating system). Идея, лежащая в основе концепции микроядра, состоит в том, что все компоненты ОС за исключением небольшой основы (собственно, микроядра) исполняются как процессы пользовательского режима. Базовые компоненты в микроядре исполняются в привилегированном режиме.
Архитектура микроядра обеспечивает в системе возможность конфигурации и устойчивость к ошибкам. Поскольку подсистемы ОС типа Диспетчера Виртуальной Памяти исполняются как отдельные программы в архитектуре микроядра, их можно заменить различными реализациями, экспортирующими такой же интерфейс. Если в Диспетчере Виртуальной Памяти происходит ошибка, то, благодаря устойчивости к ошибкам в дизайне микроядра, операционная система может перезапустить его с минимальным воздействием на остальную систему.
Недостаток чистой архитектуры микроядра - низкая производительность. Любое взаимодействие между компонентами ОС при такой схеме нуждается в межпроцессном сообщении с длительными переключениями между задачами.
NT использует уникальный подход, известный как модифицированное микроядро. Он является промежуточным между чистым микроядром и монолитной структурой.
При этом подходе в пользовательском режиме работают прикладные программы и набор подсистем, относящихся к одному из двух классов - подсистемы окружения и неотъемлемые подсистемы. Подсистемы и прикладные программы реализованы как процессы, однако способ создания подсистем и интеграции их с ОС не документирован.
Подсистемы окружения предоставляют прикладным программам интерфейс программирования, специфичный для некоторых ОС (WIN32, POSIX, OS/2, DOS).
Неотъемлемые подсистемы исполняют важные функции ОС. Среди таких подсистем - подсистема безопасности, служба рабочей станции и служба сервера.
При выполнения задач, которые не могут быть выполнены в пользовательском режиме, все подсистемы ОС NT полагаются на системные сервисы, экспортируемые режимом ядра. Эти сервисы известны как «родной» API. Такой API состоит примерно из 250 функций, доступных через модуль ntdll.dll.
Прикладная программа использует интерфейс программирования, предоставляемый какой-либо одной подсистемой окружения, либо использует напрямую собственный интерфейс программирования.
В режиме ядра работает Исполнительная система NT (NT Executive). Она, сама по себе, является законченной ОС со своим интерфейсом программирования, как для пользовательского режима, так и для режима ядра.
Исполнительная система состоит из набора подсистем, Микроядра и Слоя Абстрагирования от Оборудования (HAL). Подсистемы Исполнительной системы и Микроядро находятся в едином модуле - ntoskrnl.exe. Слой Абстрагирования от Оборудования находится в модуле hal.dll. Все загруженные системой драйверы также являются частью исполнительной системы.
Каждый компонент исполнительной системы экспортирует набор функций для использования другими компонентами. Кроме того, каждый компонент исполнительной системы, за исключением диспетчера Кэша и Слоя Абстрагирования от Оборудования, реализует набор системных сервисов.
Некоторые понятия защищенного режима
Некоторые понятия защищенного режима
Защищенный режим является основным и наиболее естественным режимом работы 32-разрядных процессоров. Этот режим был в полной мере реализован в процессорах серии i386 и с тех пор существенных изменений не претерпел.
Защищенный режим 32-разрядных процессоров реализует поддержку следующих механизмов:
Организация памяти, при которой используются два механизма преобразования памяти: сегментация и разбиение на страницы.
Четырехуровневая система защиты пространства памяти и ввода/вывода.
Переключение задач.
Сегмент - это блок пространства памяти определенного назначения, внутри которого применяется линейная адресация. Максимальный размер сегмента при 32-разрядной адресации составляет 4 Гб (232 байт). Максимальное число таких сегментов равно 213 (8192). Сегмент может иметь произвольную длину в допустимых границах.
Каждый сегмент характеризуется 8-байтной структурой данных - дескриптором сегмента, в котором, в числе прочего, указаны:
Права доступа, которые определяют возможность чтения, записи и исполнения сегмента.
Уровень привилегий (относится к четырехуровневой системе защиты).
На сегментации основана защита памяти. При этом не допускается:
использовать сегменты не по назначению (нарушение прав доступа);
обращаться к сегменту, не имея достаточных привилегий;
адресоваться к элементам, выходящим за границы сегмента.
Страничная организация памяти позволяет использовать большее пространство памяти. При этом базовым объектом памяти служит блок фиксированного размера 4 Кб.
Физический адрес памяти, получаемый на выходе сегментного и страничного преобразования памяти, является 32-разрядным, позволяя адресовать, таким образом, до 4 Гб реально доступной физической памяти.
Четырехуровневая система привилегий предназначена для управления использованием привилегированных инструкций, а также для защиты пространства памяти и ввода/вывода.
Уровни привилегий нумеруются от 0 до 3, нулевой уровень соответствует максимальным (неограниченным) возможностям доступа и отводится для ядра ОС, Уровень 3 имеет самые ограниченные права и обычно предоставляется прикладным задачам.
Систему защиты обычно изображают в виде колец, соответствующих уровням привилегий, а сами уровни привилегий иногда называют кольцами защиты.
В зависимости от уровня привилегий осуществляется защита по доступу к привилегированным командам, по доступу к данным с более высоким уровнем привилегий и по передаче управления коду с уровнем привилегий, отличным от текущего.
Защищенный режим предоставляет средства переключения задач. Состояние каждой задачи (значения всех связанных с ней регистров процессора) может быть сохранено в специальном сегменте состояния задачи. Там же хранится карта разрешения ввода/вывода, указывающая для каждого из 64К адресов портов ввода/вывода возможность обращения к нему.
ОС NT использует два кольца защиты - 0 и 3, имея соответственно режим работы в 0 кольце - kernel mode, в 3 кольце — user mode.
Необходимость в создании рабочих потоков
Необходимость в создании рабочих потоков
Любой исполняемый код, как и код драйвера, работает в контексте некоторого потока. Мы пока не обсуждали способы, с помощью которых драйвер может создать собственный поток, поэтому предполагается, что поток, в котором выполняется код драйвера, принадлежит некоторой прикладной программе. Это означает, что прикладная программа создала такой поток для выполнения своего кода, а не кода нашего драйвера. Если код драйвера производит длительную обработку, либо драйвер использует механизм синхронизации с ожиданием освобождения некоторого ресурса, код прикладной программы, для выполнения которого и создавался поток, не выполняется. Если этот поток единственный в прикладном процессе, то прикладная программа «висит».
Если описанная ситуация имеет место в диспетчерской функции драйвера верхнего уровня, мы «всего лишь» «подвесили» прикладную программу, непосредственно взаимодействующую с драйвером. В этом случае прикладная программа знает о такой возможности, и может поместить операции взаимодействия с драйвером (чтение, запись, отправка кодов управления) в отдельный поток. В этом случае драйвер может не беспокоиться о прикладной программе. Однако, такая ситуация довольно редка. Очень часто код драйвера работает в контексте случайного потока, то есть любого произвольного потока в системе. Такой поток ничего не знает о нашем драйвере и вышеописанная ситуация неприемлема. В этом случае драйвер должен создать свой собственный поток, в котором и производить длительную обработку, либо ожидание освобождения ресурсов.
Возможна другая ситуация, требующая обязательного создания потоков, когда драйверу необходимо выполнить операции на уровне IRQL меньшем DISPATCHJLEVEL, а код драйвера работает на повышенных уровнях IRQL, больших или равных DISPATCH_LEVEL.
Неотъемлемые подсистемы
Неотъемлемые подсистемы
Другой тип защищенных подсистем - неотъемлемые подсистемы - это серверы, выполняющие важные функции ОС. Примером неотъемлемой подсистемы является подсистема защиты, исполняющаяся в пользовательском режиме и реализующая правила контроля доступа, определенные для локального компьютера. Некоторые компоненты сетевого обеспечения Windows NT также реализованы как защищенные подсистемы, например, сервис рабочей станции реализует API для доступа и управления сетевым редиректором.
Независимость от архитектуры процессора
Независимость от архитектуры процессора
Микроядро и Слой Абстрагирования от Оборудования (HAL) изолируют подсистемы Исполнительной Системы от конкретной архитектуры процессора.
Другой аспект независимости от архитектуры состоит в том, что правильно написанный драйвер (общающийся с внешним миром только посредством функций, предоставляемых различными компонентами исполнительной системы) переносим между всеми поддерживаемыми NT платформами на уровне исходных текстов.
Микроядро OS Windows NT обеспечивает единый интерфейс для использования ресурсов, общих для определенной аппаратной платформы, на которой может работать OS. Например, микроядро обеспечивает интерфейсы к обработке и управлению прерываниями, сохранению и восстановлению контекста потоков и мультипроцессорной синхронизации.
HAL обеспечивает поддержку и отвечает за предоставление стандартного интерфейса к ресурсам процессора, которые могут меняться в зависимости от модели внутри одного семейства процессоров. Возможность замены слоя HAL обеспечивает всем вышележащим слоям операционной системы независимость от аппаратной архитектуры.
Объединение драйверов в стек
Объединение драйверов в стек
Для объединения драйверов в стек обычно используется функция loGetDeviceObject Pointer(). Функция вызывается драйвером вышележащего уровня для получения указателя на объект-устройство драйвера нижележащего уровня по его имени.
Функция имеет следующий прототип:
NTSTATUS loGetDeviceObjectPointer(
IN PUNICODE_STRING ObjectName,
IN ACCESS_MASK DesiredAccess,
OUT PFILE_OBJECT FileObject,
OUT PDEVICE_OBJECT DeviceObjct);
Где:
ObjectName - Имя требуемого Объекта-устройства;
DesiredAccess - Требуемый доступ к указанному Объекту-устройству;
FileObject - Указатель на Объект-файл, который будет использоваться для обращения к устройству;
DeviceObject - Указатель на Объект-устройство с именем ObjectName.
Функция IoGetDeviceObjectPointer() принимает имя Объекта-устройства, и возвращает указатель на Объект-устройство с этим именем. Функция работает, посылая запрос CREATE на названное устройство. Этот запрос будет неудачным, если никакого устройства по имени ObjectName не существует, или вызывающая программа не может предоставить доступ, указанный в параметре DesiredAccess. Если запрос CREATE успешен, создается Объект-файл, что увеличивает счетчик ссылок Объекта-устройства, с которым связан Объект-файл. Затем Диспетчер ввода/вывода искусственно увеличивает счетчик ссылок на Объект-файл на единицу, и посылает на устройство запрос CLOSE. В результате всего этого процесса, Объект-устройство (чей указатель возвращен в DeviceObject) не может быть удален, пока не обнулится счетчик ссылок соответствующего ему Объекта-файла. Таким образом, Объект-устройство нижнего уровня не может быть удален, в то время как драйвер вышележащего уровня имеет указатель на него.
Выделим из всего вышесказанного, что функция предусматривает в качестве выходного параметра указатель на Объект-файл специально для того, чтобы при выгрузке стека драйверов освободить устройство нижележащего уровня. Это должно быть сделано в функции Unload драйвера вышележащего уровня с помощью вызова функции ObDereferenceObject ( FileObject).
После получения указателя на объект-устройство драйвера нижележащего уровня, драйвер вышележащего уровня должен установить корректное значение полей Characteristics, StackSize, Flags и AlignmentRequirement своего объекта-устройства. Поля Characteristics, Flags и AlignmentRequirement объектов-устройств всех драйверов в стеке должны совпадать, а значение поля StackSize вышележащего устройства должно быть на 1 больше значения этого поля у нижележащего устройства.
Объект-драйвер
Объект-драйвер
Драйверы скрыты от программ пользовательского режима. Устройства (физические, логические и виртуальные), создаваемые и управляемые драйверами, видны программам пользовательского режима как именованные файловые объекты. Как уже отмечалось ранее, код пользовательского режима может получить доступ к устройству только через описатель, возвращаемый менеджером ввода/вывода во время открытия/ создания файлового объекта, представляющего устройство. (В структуре FILE_OBJECT есть указатель на DEVICE_OBJECT, ассоциированный с данным файловым объектом.)
Объект-драйвер представляет в системе некоторый драйвер Windows NT и хранит для диспетчера ввода/вывода адреса стандартных процедур (точки входа), которые драйвер может или должен иметь в зависимости от того, является ли он драйвером верхнего или нижнего уровней. Объект-драйвер описывает также, где драйвер загружен в физическую память и размер драйвера. Объект-драйвер описывается частично документированной структурой данных DRIVER_OBJECT.
Диспетчер ввода/вывода определяет тип объект-драйвер и использует экземпляры этого типа для регистрации и отслеживания информации о загруженных образах драйверов. Этот объект создается менеджером ввода/вывода при загрузке драйвера в систему, после чего диспетчер вызывает процедуру инициализации драйвера DriverEntry и передает ей указатель на объект-драйвер. Эта процедура устанавливает стандартные и опциональные точки входа процедур драйвера, содержащиеся в структуре DRIVER_OBJECT, чтобы в дальнейшем диспетчер ввода/вывода мог направлять пакеты запроса ввода/вывода IRP соответствующей процедуре. В DriverEntry устанавливаются также точки входа для других процедур, например для Startlo, Unload. Во время инициализации также может происходить считывание информации в поля объекта-драйвера из базы данных реестра диспетчера конфигурации.
Пакеты IRP, направляемые стандартным процедурам, содержат, кроме всего прочего, указатель на объект - устройство, который является устройством назначения для конкретного запроса ввода/вывода.
Объект-драйвер является скрытым для кода пользовательского уровня, то есть только определенные компоненты уровня ядра (в том числе и диспетчер ввода/вывода) знают внутреннюю структуру этого типа объекта и могут получать доступ ко всем данным, содержащимся в объекте, напрямую.
Microsoft не гарантирует неизменность недокументированных полей любых своих структур в последующих версиях ОС. Однако фактически, некоторые недокументированные в DDK элементы различных системных структур документированы в другом пакете для разработчика - IPS Kit.
Объект-файл (файловый объект)
Объект-файл (файловый объект)
Код пользовательского режима может получить доступ к файлам на диске или всевозможным устройствам (физическим, логическим и виртуальным) только через описатели файловых объектов, обеспечиваемых менеджером ввода/вывода по запросу от пользовательской программы на открытие/создание файла или устройства. После открытия/создания виртуального файла, обозначающего любой источник или приемник ввода/вывода (работа с которым идет так, как если бы он был обычным файлом на диске), программы могут осуществлять ввод/вывод в этот виртуальный файл, манипулируя им посредством описателя.
Итак, файловый объект - это объект, видимый из режима пользователя, который представляет всевозможные открытые источники или приемники ввода/вывода: файл на диске или устройство (физическое, логическое, виртуальное). Физическим устройством может быть, например, последовательный порт, физический диск; логическим -логический диск; виртуальным - виртуальный сетевой адаптер, именованный канал, почтовый ящик.
Всякий раз, когда некоторый поток открывает файл, создается новый файловый объект с новым набором атрибутов. В любой момент времени сразу несколько файловых объектов могут быть ассоциированы с одним разделяемым виртуальным файлом, но каждый такой файловый объект имеет уникальный описатель, корректный только в контексте процесса, поток которого инициировал открытие файла. Возможны ситуации, когда два процесса имеют разные описатели, ссылающиеся на один и тот же файловый объект:
1. когда процесс дублирует описатель файлового объекта для другого процесса;
2. когда дочерний процесс наследует описатель от родительского.
Файловые объекты, как и другие объекты, имеют иерархические имена, охраняются объектной защитой, поддерживают синхронизацию и обрабатываются системными сервисами.
Объект Секция
Объект Секция
Диспетчер Памяти в NT экспортирует единственную структуру для контроля данных - Объект-Секцию. Подобно другим объектам, Объект-Секция может быть именованным, то есть имя будет видимо в пространстве имен Диспетчера Объектов.
Секция может быть использована драйвером для отображения участка памяти системного адресного пространства (в том числе невытесняемого) в пользовательское адресное пространство. В этом случае прикладная программа и драйвер получат в свое распоряжение совместно используемую область памяти, что может быть полезно, например, при необходимости передачи большого объема данных от драйвера к приложению.
Секция используется для описания всего, что может быть отображено в память. Например, для каждого отображаемого в память файла создается объект-Секция. При этом, сколько бы не было открытий такого файла, объект-Секция всегда одна. Поскольку все исполняемые файлы загружаются посредством механизма отображения памяти, единственность объекта-секции всегда гарантирует наличие только одной копии такого файла в памяти.
При создании секции указывается режим доступа (чтение/запись/исполнение). Этот режим доступа будет влиять на записи в таблице страниц, относящиеся к секции.
Функции работы с секциями:
ZwOpenSection();
ZwMapViewOfSection();
ZwUnmapViewOfSection().
Объект-устройство
Объект-устройство
Диспетчер ввода/вывода определяет тип объекта - объект-устройство, используемый для представления физического, логического или виртуального устройства, чей драйвер был загружен в систему. Формат объекта-устройство определяется частично документированной структурой данных DEVICE_OBJECT. Хотя объект-устройство может быть создан в любое время посредством вызова функции loCreateDeviceQ, обычно он создается внутри DriverEntry.
Объект-устройство описывает характеристики устройства, такие как требование по выравниванию буферов и местоположение очереди устройства, для хранения поступающих пакетов запросов ввода/вывода.
Процедура инициализации драйвера DriverEntry создает по одному объекту-устройству для каждого устройства, управляемого данным драйвером, посредством вызова процедур, предоставляемых менеджером ввода/вывода. Процедура инициализации драйвера должна создавать, по крайней мере, один объект-устройство, некоторые драйверы должны создавать более одного объекта-устройства в зависимости от уровня, на котором они располагаются в цепочке драйверов.
В каждом объекте-устройстве есть указатель на следующий объект-устройство (либо NULL) и указатель на объект-драйвер, управляющий Данным устройством. Благодаря этому диспетчер ввода/вывода может определить, процедуру какого драйвера он должен вызвать при получении запроса к устройству. Диспетчер ввода/вывода поддерживает также указатель на объект-устройство, созданный драйвером, в объекте-драйвере.
Большинство драйверов NT используют структуру данных DeviceExtension, указатель на которую хранится в объекте-устройстве, а ее размер и содержимое определяется при создании объекта-устройства во время инициализации драйвера. Внутренняя структура DeviceExtension определяется разработчиками драйверов и используется для хранения информации о состоянии устройства, некоторого контекста текущего запроса ввода/вывода, для хранения указателей на описатели различных объектов ядра, то есть для хранения любых данных, которые нужны драйверу. Диспетчер ввода/вывода выделяет память под DeviceExtension из резидентной системной области памяти.
Так как большинство процедур драйверов исполняются в контексте произвольного потока, то есть потока, оказавшегося текущим на момент вызова процедуры драйвера, то область DeviceExtension является одним из первых мест, где драйвер хранит необходимые ему данные.
Объектная модель
Объектная модель
В исполнительной системе объект (object) - это отдельный образец статически определенного типа объектов, существующий во время выполнения. Тип объектов, иногда называемый классом объектов, включает определенный системой тип данных, объектные сервисы, работающие с образцами этого типа, и набор атрибутов объекта.
Атрибут объекта - это поле данных внутри объекта, частично определяющее его состояние. Объектные сервисы - способы манипулирования объектами - обычно считывают или изменяют атрибуты объектов.
Windows NT использует объекты для унификации представления и управления системными ресурсами. Каждый системный ресурс, который могут совместно использовать несколько процессов, такой, как файл, память или физическое устройство, реализован как объект и обрабатывается объектными сервисами. Доступ ОС к ресурсам и работа с ними унифицированы. Создание, удаление и ссылка на объект осуществляется с использованием описателей (handle) объектов. Контроль использования ресурсов сводится к отслеживанию создания и использования объектов. Для всех объектов контроль доступа к ним осуществляется одинаково с помощью подсистемы защиты. Два процесса совместно используют объект тогда, когда каждый из них открыл его описатель. ОС может отслеживать количество описателей, открытых для данного объекта, чтобы определить, действительно ли они все еще используется, и может удалить объекты, которые более не используются.
Объекты - прерывания
Объекты - прерывания
Драйверам устройств необходим способ сообщения NT, что они хотят, чтобы исполнялась определенная функция, когда процессор получает прерывание, относящееся к их устройствам. Для этого драйверы устройств с помощью Диспетчера ввода/ вывода регистрируют функцию обработки прерывания (Interrupt Service Routine, ISR) посредством вызова функции loConnectlnterrupt. Параметры, передаваемые в loConnectlnterrupt описывают все свойства ISR драйвера, включая ее адрес, прерывание, к которому подключена ISR и то, могут ли другие устройства совместно использовать это же прерывание.
Функция loConnectlnterrupt инициализирует объект-прерывание (Interrupt Object), для того чтобы хранить информацию о прерывании и подключенной ISR. loConnectlnterrupt программирует также аппаратуру прерываний для того, чтобы указывать код, который loConnectlnterrupt поместила в объект-прерывание. Таким образом, когда CPU получит прерывание, управление немедленно перейдет к коду в объек-те-прерывание. Этот код вызовет вспомогательную функцию обслуживания прерывания, KilnterruptDispatch, которая повысит уровень IRQL процессора, вызовет соответствующую ISR, и понизит IRQL до предыдущего значения. KilnterruptDispatch также получает спин-блокировку, индивидуальную для прерывания, и удерживает ее, пока выполняется ISR (см. раздел «Механизмы синхронизации»). Спин-блокировка гарантирует, что ISR не будет одновременно исполняться более чем на одном процессоре, а это может привести к печальным последствиям.
В NT, ISR обычно не делает ничего, кроме чтения минимального количества информации из прерывающего устройства и подтверждения устройству того факта, что драйвер «увидел» прерывание. В других операционных системах ISR часто выполняют дополнительные обязанности, такие, как полная обработка прерывания путем чтения больших буферов данных, или записи больших буферов данных в устройство. Однако, одна из задач NT - минимизировать время, проводимое на повышенных уровнях IRQL, поэтому NT откладывает большую часть обслуживания прерывания до момента уменьшения уровня IRQL. Процедуры ISR запрашивают отложенный вызов процедур (Deferred Procedure Call, DPC) для информирования Диспетчера ВВОДА/ВЫВОДА о том, что у них имеется работа для исполнения на нижнем уровне IRQL. DPC - еще одна функция в драйвере, которую вызовет Диспетчер ввода/вывода после завершения ISR; DPC осуществляет почти все взаимодействие с устройством.
Обобщенная таблица механизмов синхронизации
Обобщенная таблица механизмов синхронизации
В таблице 9 представлены механизмы синхронизации и особенности использования каждого из них.
Обработка пакетов IRP в функции Startlo
Обработка пакетов IRP в функции Startlo
Как должна происходить обработка пакетов из системной очереди? Как уже говорилось, Startlo работает на уровне IRQL DISPATCHJLEVEL. Пока выполняется эта функция, ни один поток с более низким значением IRQL не может получить управление (если в системе один процессор). Следовательно, новые запросы ввода/ вывода от прикладных программ попасть в очередь не могут (потоки не выполняются). Если завершение очередного пакета ввода/вывода всегда происходит в функции Startlo, системная очередь всегда содержит не более одного пакета ввода/вывода. Если пакет ввода/вывода не может быть обработан в тот момент, когда он попал в функцию Startlo, функция просто должна завершиться, не завершая запрос ввода/ вывода и не вызывая IoStartNextPacket(). В этом случае устройство остается «занятым». Поле pDeviceObject->DeviceQueue.Busy все еще TRUE, а в поле pDevice-Object->CurrentIrp находится указатель на этот пакет IRP. Такой пакет может быть обработан, например, при поступлении прерывания от аппаратного устройства (или при возникновении другого ожидаемого события). Функция, которая завершит обработку такого пакета, обязана вызвать loStartNextPacket(), чтобы инициировать выборку очередного пакета из системной очереди. Заметим, что пока устройство остается «занятым», функция Startlo для обработки пакетов из системной очереди не может быть вызвана.
Несмотря на простоту использования системной очереди, имеется существенное ограничение. Оно состоит в том, что очередь одна на все типы запросов ввода/вывода (чтение, запись, управление устройством), В каждый конкретный момент обрабатывается только какой-то один пакет IRP.
Могут быть ситуации, когда такое ограничение неприемлемо. Классическим примером является драйвер полнодуплексного устройства, которое одновременно позволяет как отправлять, так и получать данные. В этом случае необходимо начать обработку следующего запроса чтения при завершении текущего запроса чтения, и следующего запроса записи при завершении текущего запроса записи. При этом важно понимать, что в этом случае одновременно (то есть в контекстах разных потоков) могут выполняться один запрос чтения и один запрос записи, Необходимы две очереди: одна - для запросов чтения, другая - для запросов записи.
Обработка запросов IRP стеком драйверов
Обработка запросов IRP стеком драйверов
Запрос ввода/вывода приходит в виде пакета IRP самому верхнему драйверу в стеке драйверов (драйверу верхнего уровня). При этом возможны следующие варианты обработки IRP:
1. Обработка IRP полностью в драйвере верхнего уровня.
2. После выполнения своей части обработки IRP драйвер отправляет первоначальный пакет IRP драйверу нижележащего уровня.
3. После выполнения своей части обработки IRP драйвер создае
т один или несколько новых пакетов IRP и отправляет их драйверу нижележащего уровня.
4. После выполнения своей части обработки IRP драйвер создает один или несколько новых пакетов IRP, ассоциированных с первоначальным пакетом IRP, и отправляет их драйверу нижележащего уровня.
Все варианты, кроме последнего, возможны при обработке пакета IRP драйвером любого уровня. Последний вариант - создание ассоциированных пакетов IRP - возможен только при обработке IRP драйвером верхнего уровня.
Самостоятельная обработка IRP драйвером. Если драйвер может завершить обработку пакетаIRP самостоятельно, он так и должен сделать. При этом обработка может быть завершена либо сразу при поступлении запроса ввода/вывода, либо после постановки запроса ввода/вывода в очередь.
Примером немедленного завершения обработки пакета IRP драйвером может служить случай обнаружения некорректного параметра в IRP.
Передача первоначального пакета IRP драйверу нижележащего уровня. Если драйвер не может самостоятельно обработать пакет IRP, он может передать его нижележащему драйверу. Для этого необходимо заполнить параметры в IRP в стеке размещения ввода/вывода, относящегося к нижележащему драйверу. Указатель на стек размещения ввода/вывода нижележащего драйвера возвращается функцией loGetNext IrpStackLocation(). После заполнения необходимых параметров IRP передается драйверу нижележащего уровня с помощью вызова функции IoCallDriver(). Прототип этой функции:
NTSTATUS loCallDriver (IN PDEVICE_OBJECT DeviceObject, IN OUT PIRP Irp) ;
Где:
DeviceObject - указатель на объект-устройство, принадлежащий драйверу нижележащего уровня в стеке драйверов, которому должен быть послан запрос;
IRP - указатель на IRP, который будет послан драйверу нижележащего уровня.
При вызове функции IoCallDriver() Диспетчер ввода/вывода напрямую вызывает соответствующую диспетчерскую функцию требуемого драйвера. Это означает, что loCallDriver() завершится только после завершения соответствующей диспетчерской функции.
Создание новых пакетов IRP для передачи драйверу нижележащего уровня. Вариантом передачиIRP драйверу нижележащего уровня является создание одного или нескольких новых пакетов IRP и передача этихIRP драйверу нижележащего уровня. Пакет IRP может быть создан драйвером различными способами, наиболее общим из которых является вызов функции IoAllocateIrp().
PIRP loAllocatelrp (IN CCHAR StackSize, IN BOOLEAN ChargeQuota);
Где: StackSize - Число Стеков размещения Ввода/вывода, требуемых в IRP;
ChargeQuota - Указывает, должна ли квота текущего процесса быть загружена.
Функция loAllocatelrp() возвращает частично инициализированный пакет IRP. По умолчанию, loAllocatelrp() предполагает, что вызванный драйвер не хочет иметь собственный Стек размещения Ввода/вывода. Драйвер, распределяющий IRP, может факультативно просить loAllocatelrp() создать IRP с достаточным количеством Стеков Размещения Ввода/вывода, которые он мог иметь один. В этом случае, драйвер должен вызвать IoSetNextIrpStackLocation() (макрокоманда в NTDDK.H), чтобы установить Стек Размещения. Драйвер может затем вызвать функцию loGetCurrentlrpStack Location().
Причина, по которой драйверу может потребоваться свой собственный Стек Размещения Ввода /вывода состоит в том, что драйвер может использовать стек для передачи информации к подпрограмме завершения. Подпрограмма Завершения (Completion Routine) обсуждается позже в этом разделе.
Как только IRP создан, драйвер может вызывать IoGetNextIrpStackLocation(), чтобы получить указатель на Стек размещения для драйвера нижележащего уровня в стеке драйверов. Затем драйвер заполняет параметры для Стека Ввода/вывода. Если для запроса требуется буфер данных, драйвер должен или установить MDL или SystemBuffer, как того требует используемый нижележащим драйвером способ передачи буферов в пакете IRP.
Общая архитектура Windows NT
Общая архитектура Windows NT
В этой главе рассматриваются ключевые архитектурные особенности и характеристики ОС Windows NT. Эти сведения необходимы для получения представления о назначении различных компонентов ОС, их взаимодействии друг с другом, а также для ознакомления с терминологией, используемой в данной книге. Кроме того, знание общей архитектуры ОС позволяет понять, какие возможности операционной системы могут задействовать средства защиты, расположенные на том или ином уровне архитектуры ОС.
Большое внимание в этой главе уделено модели драйвера, его структуре и характеристикам, а также взаимосвязи с другими драйверами и прикладными программами.
Очереди, управляемые драйвером
Очереди, управляемые драйвером
Вместо использования системной очереди, можно предусмотреть свой собственный механизм организации очереди. Это можно сделать двумя способами:
использовать для создания очереди функции управления очередью низкого уровня;
использовать функции управления очередью высокого уровня. Этот способ очень редко используется и является промежуточным между использованием системной очереди и функций управления очередью низкого уровня.
Используя очереди, управляемые драйвером, драйвер получает полный контроль над очередью. Например, драйвер может организовать одновременную обработку трех запросов записи и двух запросов чтения при условии, что в данный момент не выполняется запрос управления устройством.
Как и в случае использования системной очереди, при получении пакета IRP, который необходимо поставить в очередь, такой пакет необходимо пометить как отложенный с помощью вызова loMarklrpPending(). Затем используется любой способ помещения указателя на пакет IRP в очередь.
Ограничения, налагаемые на драйвер
Ограничения, налагаемые на драйвер
1. Драйвер режима ядра не может использовать API пользовательского уровня или стандартные библиотеки времени исполнения языка С. Можно использовать только функции ядра.
2. Драйвер не может осуществлять операции с числами с плавающей точкой. Попытка сделать это может вызвать аварийную остановку системы. Причина - в основе реализации архитектуры ММХ. Не вдаваясь в подробности можно сказать, что в этой архитектуре для обозначения регистров ММХ использованы те же обозначения, что и для использования регистров FPU. Переключение между использованием регистров MMX/FPU, производимое на пользовательском уровне, невидимо для драйвера.
3. Драйвер не может манипулировать физической памятью напрямую. Однако он может получить виртуальный адрес для любого физического адреса и манипулировать им.
4. Код драйвер не должен долгое время работать на повышенных уровнях IRQL. Другие ограничения можно посмотреть в [Developing Windows NT Device Driver,
chapter 5, Driver Limitation].
Последующие разделы будут посвящены описанию различных точек входа драйвера.
Ограничения, налагаемые на код с уровнем IRQL большим или равным DISPATCHJLEVEL
Ограничения, налагаемые на код с уровнем IRQL большим или равным DISPATCHJLEVEL
IRQL dispatchjevel имеет важное значение в Windows NT. Как уже говорилось выше, Диспетчер (планировщик) Windows NT получает запросы, чтобы выполнить
операцию перепланирования, на уровне IRQL dispatch_level. Этот факт имеет три важных следствия:
Любой поток с IRQL >= DISPATCH_LEVEL не подвержен механизму планирования.
Любой поток с IRQL >= DISPATCH_LEVEL не может использовать никакие функции ожидания Диспетчерских Объектов ядра с отличным от нуля временем ожидания.
Любой поток с IRQL >= DISPATCH_LEVEL не должен приводить к ошибкам отсутствия страниц памяти (что происходит при обращении к участку памяти, находящемуся йа выгруженной на диск странице памяти). Иными словами, на таких уровнях IRQL может быть использована только невыгружаемая память (nonpaged pool, организация памяти будет рассмотрена в следующем разделе).
Рассмотрим эти пункты более подробно.
Так как Диспетчер выполняется на уровне IRQL DISPATCH_LEVEL, любая подпрограмма, которая выполняется на IRQL dispatchjevel или выше, не подчиняется приоритетному прерыванию (выгрузке). Таким образом, когда квант времени потока истекает, если этот поток выполняется в настоящее время на IRQL dispatch_level или выше, он продолжит выполняться, пока не попытается понизить IRQL текущего процессора ниже dispatchjevel. Это должно быть очевидно, так как исполнение на некотором уровне IRQL блокирует распознавание других событий, запрошенных на этом же или более низком уровне IRQL.
Что может быть менее очевидно, так это то, что, когда код выполняется на уровне IRQL dispatch_level или выше, он не может ждать никакие диспетчерские объекты (Dispatcher Object - см. раздел «Механизмы синхронизации»), которые еще не переведены в сигнальное состояние (состояние «свободен»). Таким образом, например, код, выполняющийся на уровне IRQL dispatch_level или выше, не может ожидать установки объектов событие или мьютекс. Так происходит потому, что действие освобождения процессора (которое происходит, когда поток переходит в режим ожидания события) требует (по крайней мере, концептуально) запуска Диспетчера.
Однако, если подпрограмма выполняется на уровне dispatch_level или выше, прерывание уровня dispatchjevel (по которому запускается Диспетчер) будет маскировано и, следовательно, распознано не сразу. В результате происходит возврат обратно к коду, который вызвал операцию ожидания!
Еще менее очевидным может быть тот факт, что код, выполняющийся на уровне IRQL dispatch_level или выше не должен приводить к ошибкам отсутствия страниц (page faults). Это означает, что любой такой код сам должен быть невыгружаемым, и должен обращаться только к невыгружаемым структурам данных. В основном это объясняется тем, что код, выполняющийся на IRQL dispatch_level или выше не может ждать освобождения диспетчерского объекта. Таким образом, даже если бы страничный запрос был обработан, поток с ошибкой отсутствия страницы не мог бы быть приостановлен, пока необходимая страница читалась с диска.
Описание буфера данных
Описание буфера данных
Описатель для буфера данных инициатора запроса находится в фиксированной части IRP. Для осуществления операции ввода/вывода NT предусматривает три различных метода передачи буфера данных, принадлежащего инициатору запроса:
Прямой Ввод/вывод (Direct I/O). Буфер находится в виртуальном адресном пространстве инициатора запроса. Для передачи буфера драйверу Диспетчер ввода/вывода создает таблицу описания памяти (MDL), описывающую размещение буфера в физической памяти.
Буферизированный Ввод/вывод (Buffered I/O). Для передачи буфера драйверу Диспетчер ввода/вывода создает в невыгружаемой системной памяти копию первоначального буфера. Драйверу передается указатель на этот новый буфер. Выделенная память будет освобождена Диспетчером ввода/вывода при завершении запроса ввода/вывода.
«Никакой» Ввод/вывод (Neither I/O). В драйвер передается виртуальный адрес буфера инициатора запроса.
Коды функции Ввода/вывода и lOCTLs освещены более подробно ниже.
Определение конфигурации аппаратного устройства
Определение конфигурации аппаратного устройства
Довольно обширную тему, связанную с подготовкой драйвера к использованию аппаратного устройства мы пропустим. Интересующиеся могут обратиться к [Device Driver Development, chapter 13. Driver Entry].
Тем не менее, один часто используемый в драйверах физических устройств момент, а именно — использование реестра - необходимо упомянуть.
В соответствии с принятыми соглашениями драйверы хранят настроечные параметры в ключе реестра \HKLM\CurrentControlSet\Services\DrvName\Parameters или .. DrvName\DeviceA\Parameters.
Наиболее простой способ запроса содержимого реестра предоставляет функция RtlQueryRegistryValues(). Имя ключа реестра \HKLM\CurrentCont-rolSet\Services\ DrvName содержится во втором параметре функции DriverEntry.
Определение текущего уровня IRQL
Определение текущего уровня IRQL
Текущий уровень IRQL свой у каждого CPU. Код режима ядра может определить IRQL, в котором он выполняется, посредством вызова функции KeGetCurrentlrql (), прототип которой:
KIRQL KeGetCurrentlrql ();
KeGetCurrentlrql() возвращает IRQL текущего CPU.
Большинство подпрограмм драйвера устройства вызывается Диспетчером ввода/вывода на определенном архитектурой уровне IRQL. To есть разработчик драйвера знает уровень (или уровни) IRQL, на котором будет вызываться данная функция. Подпрограммы режима ядра могут изменять IRQL, на котором они выполняются, вызывая функции KeRaiselrql() и KeLowerlrql(), прототипы которых:
VOID KeRaiselrql (IN PKIRQL Newlrql, OUT PKIRQL Oldlrql);
Где: Newlrql - значение, до которого должен быть поднят уровень IRQL текущего процессора; Oldlrql - указатель на место, в которое будет помещен IRQL, на котором текущий процессор выполнялся перед тем, как был поднят к Newlrql.
VOID KeLowerlrql (IN KIRQL Newlrql);
Где: Newirql - значение, до которого должен быть понижен IRQL текущего процессора.
Так как уровни IRQL являются методом синхронизации, большинство подпрограмм режима ядра (в особенности драйверы устройств) никогда не должны понижать свой уровень IRQL ниже того, на котором они вызывались. Таким образом, драйверы могут вызывать KeRaiselrql (), чтобы поднять IRQL до более высокого уровня, и затем вызывать KeLowerlrql(), чтобы возвратиться обратно к первоначальному уровню IRQL, на котором они были вызваны (например, из Диспетчера ввода/вывода). Однако драйвер никогда не должен вызывать функцию KeLowerlrql(), чтобы понизить IRQL до уровня, меньшего, чем тот, на котором он был вызван. Такое поведение может привести к крайне непредсказуемой работе операционной системы, которая наверняка закончится полным отказом системы.
Организация памяти в защищенном режиме работы процессора
Организация памяти в защищенном режиме работы процессора
Ранее мы кратко рассмотрели работу процессоров серии 1386 и выше в защищенном режиме, использующем организацию памяти, при которой используются два механизма преобразования памяти:
сегментация;
разбиение на страницы.
ОС NT в различной мере использует оба этих механизма.
Как уже говорилось, в защищенном режиме может быть определено до 213 (8192) сегментов. Каждый сегмент может иметь размер до 4 Гб (232 байт). Таким образом, максимальный размер виртуального адресного пространства составляет 64 Тб.
Каждый сегмент описывается 8-байтной структурой данных - дескриптором сегмента. Дескрипторы находятся в специальной таблице дескрипторов (GDT, см. Рисунок 5). Для указания конкретного сегмента используется 16-битный селектор. Он является индексом внутри таблицы дескрипторов. Младшие 2 бита селектора определяют номер привилегированного режима (DPL - уровень привилегий дескриптора), который может воспользоваться данным селектором для доступа к дескриптору, третий бит определяет локальную/глобальную дескрипторную таблицу, (отсюда максимальное число селекторов 213).
ОС NT, хотя и использует селекторы, но использует их в минимальной степени. NT реализует плоскую 32-разрядную модель памяти с размером линейного адресного пространства 4 Гб (232 байт). Это сделано следующим образом:
Организация системного адресного пространства
Организация системного адресного пространства
Как уже отмечалось, системное адресное пространство сильно отличается от пользовательского:
Системное адресное пространство одинаково вне зависимости от текущего контекста памяти, то есть от содержимого пользовательского адресного пространства.
В системном адресном пространстве имеются диапазоны памяти как выгружаемые на диск, так и не выгружаемые.
На Рисунок 6 показана приблизительная организация системного адресного пространства для платформы х86.
Основные характеристики Windows NT
Основные характеристики Windows NT
ОС NT характеризуется поддержкой следующих механизмов:
1. модель модифицированного микроядра;
2. эмуляция нескольких ОС;
3. независимость от архитектуры процессора;
4. объектная модель;
5. многопоточность;
6. вытесняющая многозадачность;
7. виртуальная память с подкачкой страниц по требованию;
8. мультипроцессорная обработка;
9. интегрированная поддержка сети.
Отложенный вызов процедуры (Deferred Procedure Call, DPC)
Отложенный вызов процедуры (Deferred Procedure Call, DPC)
Вдобавок к использованию для работы Диспетчера (планировщика) NT, IRQL dispatch_level также используется для обработки Отложенных Вызовов Процедур (DPC). Вызовы DPC - обратные вызовы подпрограмм, которые будут выполнены на IRQL dispatchjevel. Вызовы DPC обычно запрашиваются с более высоких уровней IRQL, для осуществления расширенной, не критической по времени обработки.
Давайте рассмотрим пару примеров того, когда используются DPC. Драйверы устройств Windows NT выполняют очень небольшую обработку внутри своих подпрограмм обслуживания прерывания. Вместо этого, когда устройство прерывается (на уровне DIRQL) и его драйвер определяет, что требуется сложная обработка, драйвер запрашивает DPC. Запрос DPC приводит к обратному вызову определенной функции драйвера на уровне IRQL dispatch_level для выполнения оставшейся части требуемой обработки. Выполняя эту обработку на IRQL dispatch_level, драйвер проводит меньшее количество времени на уровне DIRQL, и, следовательно, уменьшает время задержки прерывания для всех других устройств в системе.
На Рисунок 15 изображена типовая последовательность событий.
Отмена IRP и очереди, управляемые драйвером
Отмена IRP и очереди, управляемые драйвером
VOID Cancel(IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp) { '
PIRP irpToCancel;
PDEVICE_EXT devExt;
KIRQL oldlrql;
// обнулить указатель на функцию отмены loSetCancelRoutine(Irp, NULL); // Освободить системную спин-блокировку // как можно быстрее
loReleaseCancelSpinLock(Irp->CancelIrql); devExt = DeviceObject->DeviceExtension; . // Захватить спин-блокировку доступа к очереди, // удалить IRP и освободить // спин-блокировку KeAcquireSpinLock(&devExt->QueueLock, soldlrql);
RemoveEntryList(&Irp->Tail.Overlay.ListEntry); KeReleaseSpinLock(&devExt->QueueLock, oldlrql) ;
// Отменить IRP
Irp->IoStatus Status = STATUS_CANCELLED; Irp->IoStatus.Information = 0; loCompleteRequest(Irp, IO_NO_INCREMENT);
Отмена IRP и Системная Очередь
Отмена IRP и Системная Очередь
Пример функции отмены IRP драйвера, использующего системную очередь, показан в следующем листинге. Необходимо отметить, что для удаления IRP из системной очереди используется функция KeRemoveEntryDeviceQueue() так, как это показано в листинге.
VOID Cancel(IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp) {
// Обрабатывается ли отменяемый запрос в данный момент?
if (Irp == DeviceOb]ect->Current!rp)
{
// Да. Освободить системную спин-блокировку и указать
// диспетчеру ввода/вывода начать обработку следующего
// пакета. Отмена IRP - в конце функции
loReleaseCancelSpinLock(Irp->CancelIrql);
loStartNextPacket(DeviceOb]ect, TRUE); }
else {
// Нет. Отменяемый IRP находится в очереди. // Удалить его из очереди
KeRemoveEntryDeviceQueue(SDeviceOb]ect->DeviceQueue, &Irp->Tail.Overlay.DeviceQueueEntry);
loReleaseCancelSpinLock(Irp->CancelIrql); }
// Отменить IRP
Irp->IoStatus.Status = STATUS_CANCELLED; Irp->IoStatus.Information = 0; loCompleteRequest(Irp, IO_NO_INCREMENT); return; }
Отмена запросов ввода/вывода
Отмена запросов ввода/вывода
Всякий раз, когда запрос ввода/вывода удерживается драйвером в течение продолжительного отрезка времени, драйвер должен быть готов к отмене данного запроса. В случае закрытия потока диспетчер ввода/вывода пытается отменить все запросы ввода/вывода, отправленные этим потоком и еще не завершенные. Пока все такие запросы ввода/вывода не будут завершены устройством, ему не придет запрос IRP_MJ_ CLOSE, и, следовательно, не освободится объект-файл, а впоследствии - и само устройство (драйвер никогда не получит запрос DriverUnload).
Для обеспечения отмены запроса ввода/вывода в пакете IRP, представляющем такой запрос, должен быть указан адрес диспетчерской точки входа драйвера, собственно отменяющей запрос. Для этого служит функция IoSetCancelRoutine().
PDRIVER_CANCEL loSetCancelRoutine(IN PIRP Irp,
PDRIVER_CANCEL CancelRoutine) ;
Где функция CancelRoutine() имеет такой же прототип, как и все диспетчерские функции.
VOID CancelRoutine(IN PDEVICE_OBJECT DeviceObject,
IN PIRP Irp) ;
Она вызывается на уровне IRQL DISPATCH_LEVEL в случайном контексте потока, однако перед ее вызовом происходит захват специальной системной спин-блокировки. До тех пор, пока системная спин-блокировка не будет освобождена, функция CancelRoutine() работает на уровне IRQL DISPATCH_LEVEL. Уровень IRQL, на который нужно перейти после освобождения блокировки указывается при вызове IoReleaseCancelSpinLock() (см. ниже). Принцип реализации функции следующий:
Если указанный пакет IRP не может быть отменен или не принадлежит драйверу, то надо освободить системную спин-блокировку и завершить работу функции.
В противном случае:
1. удалить пакет IRP из любых очередей, в которых он присутствует;
2. установить функцию отмены IRP в NULL с помощью IoSetCancelRoutine();
3. освободить системную спин-блокировку;
4. установить в IRP поле loStatus.Information равном 0, а поле loStatus.Status равном STATUS_CANCELLED;
5. завершить IRP с помощью loCompleteRequest().
Системная спин-блокировка отмены IRP освобождается с помощью loRelease CancelSpinLock():
VOID IoReleaseCancelSpinLock(IN KIRQL Irgl);
Где: Irql - уровень IRQL, на который система должна вернуться после освобождения спин-блокировки. Это значение хранится в IRP в поле Cancellrql.
Ожидание (захват) диспетчерских
Мьютексы ядра
Слово Мьютекс (mutex = Mutually Exclusive) означает взаимоисключение, то есть мьютекс обеспечивает нескольким потокам взаимоисключающий доступ к совместно используемому ресурсу.
Вначале отметим, что кроме мьютексов ядра, есть еще быстрые мьютексы, являющиеся объектами исполнительной системы и не являющиеся диспетчерскими объектами. Мьютексы ядра обычно называют просто мьютексами.
Мьютексы ядра - это диспетчерские объекты, эквиваленты спин-блокировок. Двумя важными отличиями мьютексов от спин-блокировок являются:
Захват мьютекса является уникальным в рамках конкретного контекста потока. Поток, в контексте которого произошел захват мьютекса, является его владельцем, и может впоследствии рекурсивно захватывать его. Драйвер, захвативший мьютекс в конкретном контексте потока, обязан освободить его в том же контексте потока, нарушение этого правила приведет к появлению «синего экрана».
Для мьютексов предусмотрен механизм исключения взаимоблокировок. Он заключается в том, что при инициализации мьютекса функцией KelnitializeMutex() указывается уровень (level) мьютекса. Если потоку требуется захватить несколько мьютексов одновременно, он должен делать это в порядке возрастания значения level.
Функции работы с мьютексами ядра:
1. VOID KeInitializeMutex (IN PKMUTEX Mutex, IN ULONG Level); Эта функция инициализирует мьютекс. Память под мьютекс уже должна быть выделена. После инициализации мьютекс находится в сигнальном состоянии.
2. LONG KeReleaseMutex (IN PKMUTEX Mutex, IN BOOLEAN Wait); Эта функция освобождает мьютекс, с указанием того, последует ли сразу после этого вызов функции ожидания мьютекса. Если параметр Wait равен TRUE, сразу за вызовом KeReleaseMutex() должен следовать вызов одной из функций ожидания KeWaitXxx(). В этом случае гарантируется, что пара функций - освобождение мьютекса и ожидание - будет выполнена как одна операция, без возможного в противном случае переключения контекста потока. Возвращаемым значением будет 0, если мьютекс был освобожден, то есть переведен из несигнального состояния в сигнальное. В противном случае возвращается ненулевое значение.
3. LONG KeReadStateMutex(IN PKMUTEX Mutex); Эта функция возвращает состояние мьютекса - сигнальное или несигнальное.
Передача данных от приложения к драйверу. Асинхронная обработка
Передача данных от приложения к драйверу. Асинхронная обработка
Код пользовательского уровня не может напрямую вызвать код режима ядра. Для этого существуют специальные прерывания. Одним из них является прерывание 2Е -вызов системного сервиса. Диспетчер ввода/вывода обрабатывает вызовы системных сервисов специальным образом (см. Рисунок 9). В своем обработчике системного сервиса он создает специальный запрос ввода/вывода IRP и передает его на обработку некоторому объекту-устройству, после чего работа обработчика может завершиться, но обработка IRP при этом может быть не закончена.