[Previous] [Next]

Приоритеты выполнения программного кода

Поскольку разные процессорные архитектуры реализуют разные подходы к управлению аппаратурой при помощи прерываний, разделяемых по приоритетам, операционная система Windows NT использует идеализированную схему, которая удовлетворяет особенностям всех аппаратных платформ. Практическая сторона данной схемы состоит в использовании процедур слоя аппаратный абстракций HAL, которые и берут на себя специфические особенности аппаратуры, позволяя создавать платформенно-независимый драйверный код.

Основой этой схемы абстрактных приоритетов прерываний является IRQL (interrupt request level) — уровень запроса на прерывание. Уровень IRQL представляет собой число, определяющее приоритет. Программный код, выполняющийся на данном уровне IRQL, не может быть прерван программным кодом, имеющим равный или более низкий IRQL. В таблице 6.1 приводятся уровни IRQL, используемые в Windows 2000/XP/Server 2003. Именно так уровни IRQL видятся драйверу, независимо от того, на каком процессоре или в какой шинной архитектуре приходится драйверу работать. Важно также и то, что в любой конкретный момент времени каждая инструкция (оператор программного кода) выполняется на одном определенном уровне приоритета со специфическим значением IRQL. Уровень IRQL входит в состав контекста выполнения каждого потока, следовательно, в любой момент времени операционной системе достоверно известен его текущий уровень IRQL.

Таблица 6.1. Уровни IRQL

Генерируется Наименование Назначение
Аппаратным
обеспечением

HIGH_LEVEL

Проверка компьютера и шинные ошибки
  POWER_LEVEL Прерывание по сбою в энергоснабжении
  IPI_LEVEL Прерывания межпроцессорного взаимодействия для многопроцессорных систем
  CLOCK_LEVEL Интервальный таймер
  PROFILE_LEVEL Таймер профилирования
  DIRQL Платформенно-зависимое число уровней для прерываний устройств ввода/вывода
Программным
обеспечением
DISPATCH_LEVEL Планирование потоков и выполнение отложенных процедурных вызовов (DPC)
  APC_LEVEL Выполнение асинхронных процедурных вызовов (1)
  PASSIVE_LEVEL Уровень нормального исполнения потоков (0)

В приведенной схеме значения IQRL аппаратных прерываний лежат в интервале выше DISPATCH_LEVEL и ниже PROFILE_LEVEL. Эти уровни еще встречаются в литературе под названием 'device IRQL', DIRQL — уровни IRQL устройств. Уровни выше PASSIVE_LEVEL называются повышенными (elevated IRQLs). Программные потоки, работающие на повышенных уровнях, могут быть вытеснены только потоками с более высоким уровнем IRQL. Такой способ работы с потоками называется в литературе dispatching, диспетчеризация.

Потоки, работающие на уровне PASSIVE_LEVEL, попадают под управление планировщика заданий (sheduler). Приоритеты, которые различает планировщик заданий для потоков с уровнем PASSIVE_LEVEL, принимают значения от 0 до 32 (MAXIMUM_PRIORITY) и называются в ряде источников 'приоритетом планирования' (sheduler priority, 'приоритет планировщика').

Между потоками PASSIVE_LEVEL, имеющими приоритеты планирования Real-Time и Normal имеется существенное различие. Первые продолжают свою работу до тех пор, пока не появится поток с большим приоритетом, так что потоки низких приоритетов должны дожидаться, пока текущий поток RealTime не завершит работу естественным путем. Потоки с приоритетами Normal планируются по другим правилам. Для работы им выделяется определенный квант времени, после чего управление передается другим потокам такого же приоритета. Время от времени планировщик может повышать приоритет отложенного потока в пределах диапазона Normal, в результате чего все программные потоки среди потоков этой группы, даже имеющие самые низкие приоритеты, рано или поздно получают управление.

Таблица 6.2. Приоритеты планирования для потоков уровня PASSIVE_LEVEL IRQL

Приоритеты Наименование Назначение
RealTime
(Приоритеты реального
времени)
HIGH_PRIORITY (31)
:
Приоритеты системных программных потоков (программного кода режима ядра)
:
LOW_REALTIME_PRIORITY (16)
 
Normal
(Динамические приоритеты)
Normal maximum (15)
:
Приоритеты потоков пользовательских приложений
:
Normal Idle (1)
 
LOW_PRIORITY (0) Системный поток обнуления страничной памяти

Драйвер имеет возможность создавать системные программные потоки с приоритетами планирования, укладывающимися в диапазон RealTime и регулировать их приоритеты в этом диапазоне при помощи вызова KeSetPriorityThread (рекомендуемым DDK документацией значением для этой операции является LOW_REALTIME_PRIORITY, равное 16, см. заголовочные файлы wdm.h или ntddk.h). Подробнее вопросы работы с программными потоками будут рассмотрены в главе 10. Получить текущее значение приоритета планирования известного потока можно при помощи вызова KeQueryPriorityThread, в то время как получить текущее значение IRQL (при работе внутри самого потока режима ядра) можно при помощи вызова KeGetCurrentIrql, как это было сделано в коде драйвера Example, см. главу 3.

Значение приоритета системного потока сразу после его создания (без искусственного изменения) в Windows XP равно 8.

Что касается программных потоков пользовательского режима, то они могут иметь как приоритеты планирования из диапазона Normal, так и более низкие. Например, приоритет THREAD_BASE_PRIORITY_IDLE имеет численное значение — 15.

Обработка прерываний

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

По окончании работы сервисная процедура выполняет специальную инструкцию (процессорную команду), означающую полное завершение обработки прерывания. По этой инструкции из стека восстанавливается информация о предыдущем состоянии процессора (включая предыдущее значение IRQL) и управление возвращается прерванному программному коду.

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

Прерывания, вызванные программно

Нижние строки таблицы 6.1 описывают уровни IRQL, связанные с прерываниями, вызванными программно. Некоторые виды обработки прерываний инициируются программным кодом, работающим в режиме ядра, путем выполнения привилегированных инструкций (процессорных команд). Операционная система Windows NT 5 использует эти программно вызываемые прерывания для расширения своей схемы приоритетов, и это позволяет ей лучше выполнять планирование потоков. Введение этих IRQL позволяет разрешить противоречия между соревнующимися потоками путем принудительного повышения приоритета одного из них по сравнению с другими, например, при выполнении критических операций, связанных с конкретным устройством. В примере кода драйвера в главе 3 такая операция выполнена при помощи вызова KeRaiseIrlq.

Доступ к областям памяти пользовательских приложений

В момент, когда программный код приложений, выполняющийся в пользовательском режиме, делает запрос на ввод/вывод, то он передает адрес буферной области, которая содержит данные (или предназначена для их получения) и размещена в пользовательском адресном пространстве. Поскольку адреса пользовательского режима указывают в нижнюю часть (ниже 2 Гбайт для обычных версий ОС) страничных таблиц, драйвер должен рассматривать возможность того, что страничные таблицы могут (и будут) меняться до окончания обработки запроса. Это может произойти в случае, если рабочая ветвь кода драйвера выполняется в контексте прерывания или в контексте потока, выполняющегося в режиме ядра. Нижняя часть страничных таблиц меняется при каждом переключении процесса (process switch). Таким образом, далеко не весь программный код драйвера может рассчитывать на то, что все адреса пользовательского режима будут верны и применимы в течение всего времени обработки запроса.

Хуже того, пользовательский буфер может быть и вовсе перемещен из оперативной памяти на жесткий диск в системный swap-файл. Области памяти, выделенные пользовательским приложениям, всегда рассматриваются операционной системой в качестве кандидатов для сброса на диск, если системе не хватает ресурсов оперативной памяти для других процессов.

Общим правилом является то, что виртуальная память пользовательских приложений является страничной, а к такой памяти нельзя обращаться из программного кода, выполнение которого происходит на IRQL уровне DISPATCH_LEVEL или выше. Этому постулату следовать необходимо по той простой причине, что работа по возвращению страницы памяти из swap-файла (в случае, если она туда попала) в оперативную память выполняется службами, которым для завершения этой операции могут потребоваться устройства, работающие при DIRQL более низком, чем IRQL кода-инициатора обращения к этой "неудачной" странице виртуальной памяти.

Способы доступа к буферным областям

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

Первая стратегия состоит в том, чтобы поручить Диспетчеру ввода/вывода копирование пользовательской буферной области в специальную область оперативной памяти, которая не является страничной и зафиксирована в физической памяти. Драйвер использует копию буфера для работы с устройством и осуществления операций ввода/вывода. По завершении работы, Диспетчер ввода/вывода обычно производит копирование данных из системного буфера в пользовательский. При запросе на операцию записи (то есть при переносе данных в обслуживаемое устройство), пользовательская область копируется в область в системном адресном пространстве до того, как последняя будет представлена драйверу. При запросе на чтение (получение данных из обслуживаемого устройства) копирование системного буфера в пользовательский производится после того, как драйвер помечает запрос как завершенный. Стандартные запросы на чтение или запись не требуют выполнения двунаправленного копирования, хотя при обработке IOCTL запросов (выполненных в пользовательских приложениях при помощи функции DeviceIoControl) это может потребоваться.

Описанный выше метод носит название buffered I/O — буферизованного ввода/вывода. Он используется медленными устройствами, которые редко работают с большими объемами данных. Этот метод не сложен для реализации в логике драйвера, но требует дополнительных временных затрат на операции по копированию буферных областей.

Вторая стратегия позволяет избежать операций копирования путем предоставления драйверу прямого доступа к пользовательской буферной области в оперативной физической памяти. В начале выполнения операции Диспетчер ввода/вывода фиксирует всю область пользовательского буфера в памяти, что предотвращает перемещение этого блока в swap-файл и саму возможность возникновения ошибки отсутствующей страницы (page fault). Затем он создает список элементов страничной таблицы, которые отображаются на область памяти выше 2 Гбайт (на системную область), таким образом, устраняя повод для переключений контекста процесса. В сложившейся ситуации, когда память и элементы страничной таблицы зафиксированы на время обработки всего запроса ввода/вывода, драйверный код может без опаски работать с пользовательским буфером. Правда, свойства этого буфера существенно изменились: теперь эта область зафиксирована в памяти (фактически стала нестраничной), а оригинальный пользовательский адрес транслирован в другой адрес, пригодный для использования только в коде, работающем в режиме ядра.

Второй метод хорошо подходит для использования драйверами быстрых устройств, выполняющих перенос больших объемов данных. Этот метод известен как direct I/O — прямой ввод/вывод. Устройства, имеющие способность к операциям DMA, практически всегда используют этот механизм.