ГЛABA 3  Системные механизмы

ГЛABA 3  Системные механизмы

B Microsoft Windows существует несколько базовых механизмов, которыми пользуются компоненты режима ядра: исполнительная система (executive), ядро и драйверы устройств. B этой главе описываются следующие системные механизмы (а также способы их использования):

диспетчеризация ловушек (trap dispatching), в том числе прерываний, DPC (deferred procedure call), APC (asynchronous procedure call), исключений и системных сервисов;

диспетчер объектов исполнительной системы;

синхронизация, в том числе спин-блокировки, объекты диспетчера ядра (kernel dispatcher objects) и реализация механизмов ожидания;

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

различные механизмы вроде поддержки глобальных флагов Windows;

LPC (local procedure call);

Kernel Event Tracing;

Wow64.

Диспетчеризация ловушек

Прерывания и исключения — такие ситуации в операционной системе, в которых нормальный поток выполнения кода процессором прерывается. Эти ситуации обнаруживаются как программным, так и аппаратным обеспечением. Термин ловушка (trap) относится к механизму, благодаря которому при прерывании или исключении процессор перехватывает контроль над выполняемым потоком и передает управление определенной части операционной системы. B Windows процессор передает управление обработчику ловушек (trap handler) — функции, специфичной для конкретного прерывания или исключения. Рис. 3–1 иллюстрирует некоторые ситуации, в которых активизируются обработчики ловушек.

Ядро различает прерывания и исключения: прерывание (interrupt) является асинхронным событием (т. е. оно может произойти в любой момент независимо от текущих команд, выполняемых процессором). Прерывания в основном генерируются устройствами ввода-вывода и таймерами. Их можно включать и отключать. Исключение (exception), напротив, представляет собой синхронное событие, являющееся результатом выполнения конкретной команды. Повторный запуск программы в аналогичных условиях с теми же данными позволит воспроизвести исключение. Примерами исключений могут служить нарушения доступа (ошибки защиты памяти), выполнение некоторых команд отладчика, а также попытки деления на нуль. Ядро также считает исключениями вызовы системных сервисов (хотя с точки зрения технической реализации это системные ловушки).

Прерывания и исключения можно генерировать как программно, так и аппаратно. Например, исключение «bus error» (ошибка шины) возникает из-за аппаратной ошибки, а причиной исключения «divide-by-zero» (деление на нуль) является ошибка в программе. Аналогичным образом прерывания могут генерироваться устройствами ввода-вывода или самим ядром (такие программные прерывания, как, например, APC или DPC).

При аппаратном прерывании или исключении процессор записывает статусную информацию в стек ядра для прерванного потока, чтобы впоследствии можно было вернуться к исходной точке в потоке управления и продолжить выполнение команд так, будто ничего не произошло. Если поток выполнялся в пользовательском режиме, Windows переключается на стек режима ядра для потока. Затем создает в стеке ядра прерванного потока фрейм ловушки (trap frame), в котором сохраняет информацию о состоянии потока. Фрейм ловушки является подмножеством полного контекста потока (см. главу 6), и вы можете просмотреть его определение, введя в отладчике ядра команду dt nt!_ktrap_frame. Программное прерывание ядро обслуживает либо при обработке аппаратного прерывания, либо синхронно — при вызове потоком функции ядра, относящейся к данному программному прерыванию.

B большинстве случаев ядро устанавливает функции, выполняющие общую обработку ловушек до и после передачи управления другим функциям, которые ставят ловушки. Например, когда устройство генерирует прерывание, обработчик ловушек аппаратных прерываний (принадлежащий ядру) передает управление процедуре обслуживания прерывания (interrupt service routine, ISR), предоставленной драйвером соответствующего устройства. Если прерывание возникло в результате вызова системного сервиса, обработчик ловушек общесистемных сервисов передает управление функции указанного системного сервиса в исполнительной системе. Ядро также устанавливает обработчики для ловушек, которые оно не ожидает или не обрабатывает. Эти обработчики, как правило, выполняют системную функцию KeBugCheckEx. Она останавливает компьютер, если ядро обнаруживает в работе системы отклонения, способные привести к повреждению данных (подробнее об этом см. главу 14). Диспетчеризация прерываний, исключений и системных сервисов детальнее описывается в следующих разделах.

Диспетчеризация прерываний

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

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

Для обработки аппаратных прерываний ядро устанавливает обработчики ловушек прерываний, которые передают управление внешней процедуре (ISR), обрабатывающей прерывание, или внутренней процедуре ядра, реагирующей на прерывание. Драйверы устройств предоставляют ISR для обслуживания прерываний от своих устройств, а ядро — внутренние процедуры для обработки других типов прерываний.

Далее мы рассмотрим, как процессор уведомляется об аппаратных прерываниях, какие типы прерываний поддерживаются ядром и как драйверы устройств взаимодействуют с ядром (в процессе обработки прерываний). Кроме того, мы поговорим о распознавании ядром программных прерываний и об объектах, используемых для реализации таких прерываний.

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

Ha аппаратных платформах, поддерживаемых Windows, прерывания, связанные с внешним вводом-выводом, поступают по одной из линий контроллера прерываний. Контроллер в свою очередь связан с процессором единственной линией, по которой и уведомляет о прерывании. Как только процессор прерывается, он требует от контроллера запрос прерывания (interrupt request, IRQ). Контроллер транслирует IRQ в номер прерывания, используемый как индекс в структуре, называемой таблицей диспетчеризации прерываний (interrupt dispatch table, IDT), и передает управление соответствующей процедуре. При загрузке Windows заносит в IDT указатели на процедуры ядра, обрабатывающие каждое прерывание и исключение.

ЭКСПЕРИМЕНТ: просмотр IDT

Просмотреть содержимое IDT, включая сведения об обработчиках ловушек, которые Windows назначила прерываниям, можно с помощью команды !idt отладчика ядра. Команда !idt без флагов показывает векторы, которые сопоставлены с адресами в модулях, отличных от Ntoskrnl.exe.

Ниже показано, что выводит команда !idt.

B системе, задействованной в этом эксперименте, номер прерывания 0x3C — с ISR драйвера клавиатуры (I8042prt.sys), а прерывание 0x3B совместно используется несколькими устройствами, в том числе видеоадаптером, шиной PCMCIA, портами USB и IEEE 1394, а также сетевым адаптером.

Windows увязывает аппаратные IRQ с номерами прерываний в IDT. Эта таблица используется системой и при конфигурировании обработчиков ловушек для исключений. Так, номер исключения для ошибки страницы на x86 и x64 (это исключение возникает, когда поток пытается получить доступ к отсутствующей или не определенной в виртуальной памяти странице) равен 0xe. Следовательно, запись 0xe в IDT указывает на системный обработчик ошибок страниц. Хотя архитектуры, поддерживаемые Windows, допус-каютдо 256 элементов в IDT, число IRQ на конкретной машине определяется архитектурой используемого в ней контроллера прерываний.

У каждого процессора имеется своя IDT, так что разные процессоры могут при необходимости выполнять разные ISR. Например, в многопроцессорной системе каждый процессор получает прерывания системного таймера, но обновление значения системного таймера в результате обработки этого прерывания осуществляется только одним процессором. Однако все процессоры используют это прерывание для измерения кванта времени, выделенного потоку, и для инициации новой процедуры планирования по истечении этого кванта. Аналогичным образом в некоторых конфигурациях может понадобиться, чтобы определенные аппаратные прерывания обрабатывал конкретный процессор.

Контроллеры прерываний на платформе x86

B большинстве систем x86 применяется либо программируемый контроллер прерываний (Programmable Interrupt Controller, PIC) i8259A, либо его разновидность, усовершенствованный программируемый контроллер прерываний (Advanced Programmable Interrupt Controller, APIC) i82489. Новые компьютеры, как правило, оснащаются APIC Стандарт PIC был разработан для оригинальных IBM PC PIC работает только в однопроцессорных системах и имеет 15 линий прерываний. APIC и SAPIC (о нем чуть позже) способен работать в многопроцессорных системах и предлагает 256 линий прерываний. Intel совместно с другими компаниями создали спецификацию Multiprocessor (MP) Specification, стандарт для многопроцессорных систем x86, основанный на использовании APIC Для совместимости с однопроцессорными операционными системами и загрузочным кодом, запускающим многопроцессорную систему в однопроцессорном режиме, APIC поддерживает PIC-совместимый режим с 15 линиями прерываний и передачей прерываний лишь главному процессору. Архитектура APIC показана на рис. 3–2. Ha самом деле APIC состоит из нескольких компонентов: APIC ввода-вывода, принимающего прерывания от устройств, локальных APIC, принимающих прерывания от APIC ввода-вывода по выделенной шине и прерывающих pa-ботутого процессора, с которым они связаны, а также 18259А-совместимого контроллера прерываний, транслирующего входные сигналы APIC в соответствующие PIC-эквиваленты. APIC ввода-вывода отвечает за реализацию алгоритмов перенаправления прерываний, и операционная система выбирает нужный ей алгоритм (в Windows выбор возлагается на HAL). Эти алгоритмы равномерно распределяют между процессорами нагрузку, связанную с обработкой прерываний от устройств, и в максимальной мере используют все преимущества локальности, направляя такие прерывания процессору, который только что обрабатывал прерывания аналогичного типа.

Контроллеры прерываний на платформе x64

Поскольку архитектура x64 совместима с операционными системами для x86, системы на базе x64 должны предоставлять те же контроллеры прерываний, что и на базе x86. Однако х64-версии Windows не будут работать на системах без APIC (т. е. они не поддерживают PIC).

Контроллеры прерываний на платформе IA64

B архитектуре IA64 используется контроллер прерываний Streamlined Advanced Programmable Interrupt Controller (SAPIC) — результат эволюционного развития APIC Главное различие между архитектурами APIC и SAPIC в том, что APIC ввода-вывода в APIC-системе направляет прерывания локальным APIC по выделенной шине APIC, тогда как в системе SAPIC прерывания передаются по шине ввода-вывода и системы (I/O and system bus) для большего быстродействия. Еще одно различие — перенаправление прерываний и балансировка нагрузки в APIC-системе обрабатывается самой шиной APIC, а в SAPIC-системе, где нет выделенной шины APIC, требуется, чтобы соответствующая поддержка была запрограммирована в микрокоде (прошивке). Ho, даже если эта поддержка имеется в микрокоде, Windows не использует ее — вместо этого она статически назначает прерывания процессорам по принципу карусели.

ЭКСПЕРИМЕНТ: просмотр конфигурации PIC и APIC

Конфигурацию PIC в однопроцессорной системе и APIC в многопроцессорной системе можно просмотреть с помощью команд !pic или !apic отладчика ядра. (Для этого эксперимента LiveKd не годится, так как она не может напрямую обращаться к оборудованию.) Ниже показан образец вывода команды !pic в однопроцессорной системе (учтите, что команда !pic не работает в системе, использующей APIC HAL).

Ha следующем листинге приводится выходная информация команды !apic в системе, использующей MPS HAL. Префикс «0:» в командной строке отладчика говорит о том, что текущие команды выполняются на процессоре 0, поэтому данный листинг относится к APIC ввода-вывода процессора 0.

Теперь взгляните на образец вывода команды !ioapic, показывающей конфигурацию APIC ввода-вывода:

Уровни запросов программных прерываний

Хотя контроллеры прерываний различают уровни приоритетов прерываний, Windows использует свою схему приоритетов прерываний, известную под названием уровни запросов прерываний (interrupt request levels, IRQL). Внутри ядра IRQL представляются в виде номеров 0-31 в системах x86 и 0-15 в системах x64 и IA64, причем больший номер соответствует прерыванию с более высоким приоритетом. Ядро определяет стандартный набор IRQL для программных прерываний, a HAL увязывает IRQL с номерами аппаратных прерываний. IRQL, определенные для архитектуры x86, показаны на рис. 3–3, а аналогичные сведения для архитектур x64 и IA64 — на рис. 3–4.

ПРИМЕЧАНИЕ Уровень SYNCH_LEVEL, используемый многопроцессорными версиями ядра для защиты доступа к индивидуальным для каждого процессора блокам PRCB (processor control blocks), не показан на этих схемах, так как его значение варьируется в разных версиях Windows. Описание SYNCH_LEVEL и его возможных значений см. в главе 6.

Рис. 3–4. Уровни запросов прерываний (IRQL) в системах x64 и IA64

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

Уровни приоритетов IRQL имеют совершенно иной смысл, чем приоритеты в схеме планирования потоков (см. главу 6). Приоритет в этой схеме является атрибутом потока, тогда как IRQL — атрибутом источника прерывания, например клавиатуры или мыши. Кроме того, IRQL каждого процессора меняется во время выполнения команд операционной системы.

Значение IRQL определяет, какие прерывания может получать данный процессор. IRQL также используется для синхронизации доступа к структурам данных режима ядра (о синхронизации мы поговорим позже). При выполнении поток режима ядра повышает или понижает IRQL процессора либо напрямую (вызовом соответственно KeRaiseIrql или KeLowerIrqL), либо — что бывает гораздо чаще — опосредованно (через функции, которые обращаются к синхронизирующим объектам ядра). Как показано на рис. 3–5, прерывания от источника с IRQL, превышающим текущий уровень, прерывают работу процессора, а прерывания от источников, IRQL которых меньше или равен текущему уровню, маскируются до тех пор, пока выполняемый поток не понизит IRQL.

Поскольку доступ к PIC — операция довольно медленная, в HAL, использующих PIC, реализован механизм оптимизации «отложенный IRQL» (lazy IRQL), который избегает обращений к PIC Когда IRQL повышается, HAL — вместо того чтобы изменять маску прерывания — просто отмечает новый IRQL. Если вслед за этим возникает прерывание с более низким приоритетом, HAL устанавливает маску прерывания в соответствии с первым и откладывает обработку прерывания с более низким приоритетом до понижения IRQL. Таким образом, если при повышенном IRQL не возникнет прерываний с более низким приоритетом, HAL не потребуется обращаться к PIC.

Поток режима ядра повышает и понижает IRQL процессора, на котором он выполняется, в зависимости от того, что именно делает этот поток. Например, обработчик ловушки (или сам процессор) при прерывании повышает IRQL процессора до IRQL источника прерывания. B результате все прерывания с более низким или равным IRQL маскируются (только на этом процессоре), что не дает прерыванию с таким же или более низким IRQL помешать процессору обработать текущее прерывание. Замаскированные прерывания либо обрабатываются другим процессором, либо откладываются до понижения IRQL. Поэтому все системные компоненты, в том числе ядро и драйверы устройств, пытаются удерживать IRQL на уровне passive («пассивный»), иногда называемом низким уровнем. Если бы IRQL долго оставался неоправданно высоким, драйверы устройств не смогли бы оперативно реагировать на аппаратные прерывания.

ПРИМЕЧАНИЕ Прерывания APC_LEVEL являются исключением из правила, которое гласит, что повышение IRQL блокирует прерывания такого же уровня и ниже. Если поток повышает IRQL до уровня APC_ LEVEL, а затем отключается от процессора из-за появления прерывания DISPATCH_LEVEL, то система может доставить ему прерывание APC_LEVEL, как только он вновь получит процессорное время. Таким образом, APC_LEVEL можно считать IRQL, локальным для потока.

ЭКСПЕРИМЕНТ: определяем IRQL

Если вы работаете с отладчиком ядра в Windows Server 2003, то можете определить IRQL процессора командой !irql:

kd›!irql

Debugger saved IRQL for processor 0x0 — 0 (L0W_LEVEL)

Заметьте, что в структуре данных, называемой PCR (processor control region), и ее расширении — PRCB (processor control block) имеется поле с именем Irql. Эти структуры содержат информацию о состоянии каждого процессора в системе, в том числе текущий IRQL, указатель на аппаратную IDT, сведения о текущем потоке и потоке, который будет выполняться следующим. Ядро и HAL используют эту информацию для выполнения операций, специфичных для данной машины и ее архитектуры. Отдельные части структур PCR и PRCB открыто определены в заголовочном файле Ntddk.h (в Windows DDK). Загляните в него, чтобы получить представление об этих структурах.

Для просмотра содержимого PCR воспользуемся командой !pcr отладчика ядра.

K сожалению, Windows не поддерживает поле Irql на платформах, не использующих отложенные IRQL, поэтому в большинстве систем это поле всегда содержит 0.

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

Каждый уровень прерывания имеет определенное назначение. Так, ядро генерирует межпроцессорное прерывание (interprocessor interrupt, IPI), чтобы потребовать выполнения какой-либо операции от другого процессора, например, при диспетчеризации некоего потока или обновлении кэша ассоциативного буфера трансляции [translation look-aside buffer (TLB) cache]. Системный таймер через регулярные промежутки генерирует прерывания, на которые ядро реагирует обновлением системного времени, и это используется для измерения продолжительности выполнения потока. Если аппаратная платформа поддерживает два таймера, то для измерения производительности ядро добавляет еще один уровень прерываний от таймера. HAL поддерживает несколько уровней запросов прерываний для устройств, управляемых прерываниями; конкретное число таких уровней зависит от процессора и конфигурации системы. Ядро использует программные прерывания для инициации планирования потоков и асинхронного вмешательства в выполнение потока.

Увязка прерываний с IRQL

Уровни IRQL и запросы прерываний (IRQ) — вещи разные. Концепция IRQL в архитектурах, на которых работает Windows, не реализована аппаратно. Тогда возникает вопрос как Windows определяет, какой IRQL следует присвоить прерыванию? Ответ нужно искать в HAL. B Windows за определение устройств на конкретной шине (PCI, USB и т. д.) и назначение им прерываний отвечают драйверы устройств особого типа — драйверы шин. Драйвер шины сообщает эту информацию диспетчеру Plug and Play, и тот, учитывая приемлемые для других устройств прерывания, принимает решение о конкретных прерываниях, выделяемых каждому устройству. Далее он вызывает HAL-функцию HalpGetSystemInterruptVector, которая увязывает прерывания со значениями IRQL.

Этот алгоритм неодинаков в различных версиях HAL. B однопроцессорных х86-системах HAL выполняет прямую трансляцию: IRQL данного вектора прерывания вычисляется путем вычитания значения вектора из 27. Таким образом, если устройство использует 5-й вектор прерывания, его ISR выполняется при IRQL, равном 22. B многопроцессорной х86-системе преобразования более сложны. APIC поддерживает более 200 векторов прерываний, поэтому при трансляции «один в один» имеющихся IRQL окажется недостаточно. Многопроцессорная версия HAL присваивает IRQL векторам прерываний, циклически перебирая значения из диапазона IRQL устройств (device IRQL, DIRQL). B итоге на многопроцессорной х86-системе не так-то просто предсказать или выяснить IRQL, назначаемый IRQ. Наконец, в x64- и IА64-системах HAL вычисляет IRQL для IRQ путем деления вектора прерывания, назначенного данному IRQ, на 16.

Предопределенные IRQL

Давайте повнимательнее приглядимся к предопределенным IRQL, начиная с самого верхнего уровня схемы, представленной на рис. 3–5.

Уровень «high» (высокий) используется ядром, только если оно останавливает систему в функции KeBugCheckEx и маскирует все прерывания.

Уровень «power fail» (отказ электропитания) был заложен еще в самый первый проект Microsoft Windows NT Он определяет поведение системы при отказе электропитания, но никогда не применялся.

Уровень «interprocessor interrupt» (межпроцессорное прерывание) используется для того, чтобы запрашивать от другого процессора выполнение какой-либо операции, например, при постановке в очередь прерывания DISPATCH_EVEL для планирования конкретного потока к выполнению, при обновлении кэша TLB, завершении работы или крахе системы.

Уровень «clock» (часы) используется для системных часов, с помощью которых ядро отслеживает время суток, измеряет и распределяет процессорное время между потоками.

Уровень «profile» (профиль) используется системным таймером реального времени, если активизирован механизм профилирования ядра (kernel profiling), т. е. измерения его производительности. Когда он активен, обработчик ловушки профилирования регистрирует адрес команды, выполнявшейся на момент прерывания. Co временем создается таблица адресов, которую можно извлечь и проанализировать с помощью соответствующих утилит. Вы можете скачать утилиту Kernrate, позволяющую просматривать статистику, полученную при использовании механизма профилирования ядра. Подробнее об этой утилите см. описание эксперимента с Kernrate.

Уровень «device» (устройство) применяется для задания приоритетов прерываний от устройств (о принципах увязки аппаратных прерываний с IRQL см. предыдущий раздел).

Прерывания уровней «DPC/dispatch» и «APC» являются программными; они генерируются ядром и драйверами устройств (о DPC и APC будет рассказано позже).

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

ЭКСПЕРИМЕНТ: применение утилиты Kernrate

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

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

1. Откройте окно командной строки.

2. Введите cd c: program fileskrviewkernrates.

3. Введите dir. (Вы увидите образы kernrate для каждой платформы.)

4. Запустите образ, который подходит для вашей платформы (без аргументов или ключей). Например, Kernrate_i386_XP.exe — это образ для Windows XP на платформе x86.

5. Пока Kernrate выполняется, поделайте что-нибудь в системе. Скажем, запустите Windows Media Player и проиграйте музыку, запустите игру, интенсивно работающую с графикой, или перечислите содержимое каталога на удаленном сетевом ресурсе.

6. Нажмите Ctrl+C, чтобы остановить Kernrate. Это заставит Kernrate вывести статистику за прошедший период.

Ниже приведена часть вывода Kernrate, когда выполнялся Windows Media Player, воспроизводивший дорожку с компакт-диска.

Сводные данные показывают, что система провела 11,7 % времени в режиме ядра, 30,2 % в пользовательском режиме, 58,1 % в простое, 6,4 % на уровне DPC и 1,3 % на уровне прерываний (interrupt level). Модуль, чаще всего требовавший к себе внимания, был GV3.SYS, драйвер процессора для Pentium M (семейства Geyserville). Он используется для сбора информации о производительности, поэтому и оказался на первом месте. Модуль, занявший второе место, — Smwdm.sys, драйвер звуковой платы на компьютере, где проводился тест. Это вполне объяснимо, учитывая, что основную нагрузку в системе создавал Windows Media Player, посылавший звуковой ввод-вывод этому драйверу.

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

C: Program FilesKrViewKernrates›Kernrate_i386_XP.exe — z ntoskrnl — z win32k

B данном случае самым «прожорливым» был модуль Win32k.sys, драйвер системы, отвечающей за работу с окнами. Второй в списке — видеодрайвер. И действительно, основная нагрузка в системе была связана с рисованием окна на экране. B детальном выводе для Win32k.sys видно, что наиболее активна была его функция EngPaint, основная GDI-функция для рисования на экране.

Ha код, выполняемый на уровне «DPC/dispatch» и выше, накладывается важное ограничение: он не может ждать освобождения объекта, если такое ожидание заставило бы планировщик подключить к процессору другой поток (а это недопустимая операция, так как планировщик синхронизирует свои структуры данных на уровне «DPC/dispatch» и, следовательно, не может быть активизирован для выполнения перераспределения процессорного времени). Другое ограничение заключается в том, что при уровне IRQL «DPC/ dispatch» или выше доступна только неподкачиваемая память. Ha самом деле второе ограничение является следствием первого, так как обращение к отсутствующей в оперативной памяти странице вызывает ошибку страницы. Тогда диспетчер памяти должен был бы инициировать операцию дискового ввода-вывода, после чего ждать, когда драйвер файловой системы загрузит эту страницу с диска. Это в свою очередь вынудило бы планировщик переключить контекст (возможно, на поток простоя, если нет ни одного пользовательского потока, ждущего выполнения). B результате было бы нарушено правило, запрещающее вызов планировщика в таких ситуациях (поскольку при чтении с диска IRQL все еще остается на уровне «DPC/dispatch» или выше). При нарушении любого из этих двух ограничений происходит крах системы с кодом завершения IRQL_NOT_LESS_OR_EQUAL (подробнее о кодах завершения при крахе системы см. главу 4). Кстати, нарушение этих ограничений является довольно распространенной ошибкой в драйверах устройств. Локализовать причину ошибок такого типа помогает утилита Driver Verifier, о которой будет подробно рассказано в разделе «Утилита Driver Verifier» главы 7.

Объекты «прерывание» (interrupt objects)

Ядро предоставляет переносимый (портируемый) механизм — объект прерывания, позволяющий драйверам устройств регистрировать ISR для своих устройств. Этот объект содержит всю информацию, необходимую ядру для назначения конкретного уровня прерывания для ISR устройства, включая адрес ISR, IRQL устройства и запись в IDT ядра, с которой должна быть сопоставлена ISR. При инициализации в объект прерывания из шаблона обработки прерываний, KiIn-terruptTemplate, копируется несколько ассемблерных инструкций — код диспетчеризации. Этот код выполняется при возникновении прерывания.

Код, хранящийся в объекте прерывания, вызывает реальный диспетчер прерываний, которым обычно является процедура ядра KiInterruptDispatch или KiChainedDispatch, и передает ему указатель на объект прерывания. KiInterruptDispatch обслуживает векторы прерываний, для которых зарегистрирован только один объект прерывания, a KiChainedDispatch — векторы, разделяемые несколькими объектами прерываний. B объекте прерывания содержится информация, необходимая процедуре KiChainedDispatch для поиска и корректного вызова ISR драйвера. Объект прерывания также хранит значение IRQL, сопоставленное с данным прерыванием, так что KiDispatchInterrupt или KiChainedDispatch может перед вызовом ISR повысить IRQL до нужного уровня и вернуть его к исходному после завершения ISR. Этот двух-этапный процесс необходим из-за того, что при начальной диспетчеризации нельзя передать указатель (или какой-либо иной аргумент) объекту прерывания, так как она выполняется не программно, а аппаратно. B многопроцессорных системах ядро создает и инициализирует объект прерывания для каждого процессора, позволяя их локальным APlC принимать конкретные прерывания. Ha рис. 3–6 показана типичная схема обслуживания прерываний, сопоставленных с объектами прерываний.

ЭКСПЕРИМЕНТ: изучение внутреннего устройства прерываний

C помощью отладчика ядра вы можете просмотреть детальные сведения об объекте прерывания, в том числе его IRQL, адрес ISR и собственный код диспетчеризации прерывания (custom interrupt dispatching code). Во-первых, введите команду !idt и найдите запись со ссылкой на I8042KeyboardInterruptService — процедуру ISR для устройства «PS2-клавиатура»:

31: 8a39dc3c i8042prt!I8042KeyboardInterruptService (KINTERRUPT 8a39dc00)

Для просмотра содержимого объекта, сопоставленного с прерыванием, введите dt nt!_kinterrupt, указав адрес, следующий за KINTERRUPT:

B этом примере IRQL, назначенный Windows прерыванию, — 0x1a (или 26 в десятичной системе). Поскольку вывод получен на однопроцессорной х86-системе, IRQ равно 1 (IRQL в таких системах вычисляются путем вычитания IRQ из 27). Это можно проверить, открыв Device Manager (Диспетчер устройств) [на вкладке Hardware (Оборудование) в окне свойств системы)], найдя PS/2-клавиатуру и посмотрев назначенные ей ресурсы, как показано на следующей иллюстрации.

B многопроцессорных х86-системах IRQ назначается в основном случайным образом, а в x64- или IА64-системе вы увидите, что IRQ — это номер вектора прерываний [0x31 (49 в десятичной системе)], деленный на 16.

Адрес ISR для объекта прерывания хранится в поле ServiceRoutine (его и показывает !idt в своем выводе), а код прерывания, срабатывающий при появлении этого прерывания, — в массиве DispatchCode в конце объекта прерывания. Этот код программируется так, чтобы создавать фрейм ловушки в стеке и потом вызывать функцию, хранящуюся в поле DispatchAddress (в нашем примере это KilnterruptDispatcb}, с передачей ей указателя на объект прерывания.

Windows и обработка данных в реальном времени

K средам, предназначенным для работы в реальном времени, предъявляются либо жесткие, либо очень жесткие требования к максимальному времени реакции. Реакция системы реального времени, к которой предъявляются очень жесткие требования (например, системы управления атомной электростанцией), должна быть исключительно быстрой, иначе неизбежны катастрофы, опасные не только для оборудования, но и для жизни людей. Менее ответственные системы реального времени (например, система экономичного расхода топлива автомобиля) могут в какой-то мере отклоняться от этих требований, но их соблюдение все же желательно. B системах реального времени устройствами ввода служат датчики, а устройствами вывода — управляющие устройства. Проектировщик компьютерных систем реального времени должен знать величину максимально допустимого запаздывания между моментом генерации прерывания устройством ввода и ответом управляющего устройства, контролируемого драйвером. Анализ самых неблагоприятных вариантов должен учитывать как запаздывание операционной системы, так и запаздывание драйверов и приложений.

Поскольку проконтролировать расстановку приоритетов IRQ устройств операционной системой Windows нельзя, а пользовательские приложения выполняются лишь при IRQL уровня «passive», Windows не всегда подходит для обработки данных в реальном времени. Максимальное запаздывание определяется в конечном счете устройствами и драйверами системы, а не самой Windows. Этот фактор становится проблемой при использовании готового оборудования, имеющегося в продаже. Проектировщик может столкнуться с трудностями при определении максимального времени выполнения ISR или DPC драйвера готового устройства. Даже после тестирования он не сможет гарантировать, что запаздывание ни при каких обстоятельствах не превысит заданного предела. Более того, суммарное запаздывание системных DPC и ISR, как правило, существенно превосходит значения, приемлемые для чувствительных к задержкам систем.

Хотя ко многим типам встраиваемых систем (например, к принтерам и автомобильным компьютерам) предъявляются требования, как к системам реального времени, Windows XP Embedded не обладает соответствующими характеристиками. Это просто версия Windows XP, которая создана с использованием технологии, лицензированной Microsoft у компании VenturCom, и способна работать в системах с ограниченными ресурсами. Так, для устройства без сетевых функций исключается вся функциональность Windows XP, связанная с поддержкой работы в сетях, включая средства управления сетью, драйверы стека протокола и сетевого адаптера.

Тем не менее третьи фирмы поставляют версии ядра реального времени для Windows. Их подход заключается в том, что они встраивают ядро реального времени в собственный HAL и выполняют Windows как задачу в операционной системе реального времени. Windows, выполняемая в таком виде, служит в качестве пользовательского интерфейса системы и имеет меньший приоритет по сравнению с задачами, ответственными за управление нужным устройством. Пример расширения ядра Windows реального времени можно увидеть на Web-сайте VenturCom www.venturcom.com.

Сопоставление ISR с конкретным уровнем прерывания называется подключением объекта прерывания, а разрыв связи между ISR и записью в IDT — отключением. Эти операции, выполняемые функциями ядра IoCon-nectInterrupt и IoDisconnectInterrupt, позволяют драйверу устройства «включать» ISR после своей загрузки и «отключать» ISR, если он не загружен.

Применение объекта прерывания для регистрации ISR позволяет драйверам устройств избегать прямого взаимодействия с контроллером прерываний (разным на разных процессорных архитектурах) и исключает необходимость детального знания IDT. Это дает возможность создавать переносимые драйверы устройств, поскольку благодаря такой функциональности ядра программировать драйверы устройств на ассемблере и учитывать в них особенности конкретных процессоров больше не нужно.

Использование объекта прерывания дает и другие преимущества: ядро может синхронизировать выполнение ISR с другими частями драйвера устройства, которые могут разделять данные с ISR. (Подробнее о том, как драйверы устройств реагируют на прерывания, см. главу 9.)

Более того, объекты прерывания позволяют ядру легко вызывать более одной ISR для любого уровня прерывания. Если несколько драйверов создают объекты прерывания и сопоставляют их с одной записью в IDT, то при прерывании на определенной линии диспетчер вызывает каждую из этих процедур (ISR). Такая функциональность позволяет ядру поддерживать конфигурации в виде цепочек, когда несколько устройств совместно используют одну линию прерывания. Когда одна из ISR объявляет диспетчеру о захвате прерывания, происходит разрыв цепочки. Если несколько устройств, разделяющих одну линию прерывания, одновременно запрашивают обслуживание, то устройства, не получившие подтверждения от своих ISR, будут вновь генерировать прерывания, как только диспетчер понизит IRQL. Связывание устройств в цепочку разрешается, только если все драйверы устройств, стремящиеся использовать одно и то же прерывание, сообщат ядру о своей способности разделять данное прерывание. Если они не в состоянии совместно использовать это прерывание, диспетчер Plug and Play переназначит IRQ с учетом запросов каждого устройства. Если разделяемым является вектор прерываний, объект прерывания вызывает KiChainedDispatch, которая поочередно обращается к ISR каждого зарегистрированного объекта прерывания, пока один из них не сообщит, что прерывание вызвано им, или пока все они не будут выполнены. B одном из предыдущих примеров вывода !idt вектор 0x3b был подключен к нескольким объектам прерываний, связанным в цепочку (chained interrupt objects).

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

Хотя большинство прерываний генерируется аппаратно, ядро Windows тоже может генерировать прерывания — только они являются программными. Этот вид прерываний служит для решения многих задач, в том числе:

инициации диспетчеризации потоков;

обработки прерываний, не критичных по времени;

обработки событий таймеров;

асинхронного выполнения какой-либо процедуры в контексте конкретного потока;

поддержки асинхронного ввода-вывода. Эти задачи подробно рассматриваются ниже.

Прерывания DPC или диспетчеризации

Когда дальнейшее выполнение потока невозможно, например из-за его завершения или перехода в ждущее состояние, ядро напрямую обращается к диспетчеру, чтобы вызвать немедленное переключение контекста. Однако иногда ядро обнаруживает, что перераспределение процессорного времени (rescheduling) должно произойти при выполнении глубоко вложенных уровней кода. B этой ситуации ядро запрашивает диспетчеризацию, но саму операцию откладывает до выполнения текущих действий. Такую задержку удобно организовать с помощью программного прерывания DPC (deferred procedure call).

При необходимости синхронизации доступа к разделяемым структурам ядра последнее всегда повышает IRQL процессора до уровня «DPC/dispatch» или выше. При этом дополнительные программные прерывания и диспетчеризация потоков запрещаются. Обнаружив необходимость в диспетчеризации, ядро генерирует прерывание уровня «DPC/dispatch». Ho поскольку IRQL уже находится на этом уровне или выше, процессор откладывает обработку этого прерывания. Когда ядро завершает свои операции, оно определяет, что должно последовать снижение IRQL ниже уровня "DPC/dispatch", и проверяет, не ожидают ли выполнения отложенные прерывания диспетчеризации. Если да, IRQL понижается до уровня «DPC/dispatch», и эти отложенные прерывания обрабатываются. Активизация диспетчера потоков через программное прерывание — способ отложить диспетчеризацию до подходящего момента. Однако Windows использует программные прерывания для отложенного выполнения и других операций.

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

DPC позволяют операционной системе генерировать прерывания и выполнять системные функции в режиме ядра. Ядро использует DPC для обработки прерываний по таймеру (и освобождения потоков, ждущих на таймерах), а также для перераспределения процессорного времени по истечении кванта времени, отведенного текущему потоку. Драйверы устройств используют DPC для выполнения запросов ввода-вывода. Для своевременного обслуживания аппаратных прерываний Windows — во взаимодействии с драйверами устройств — пытается удерживать текущий IRQL ниже IRQL устройств. Один из способов достижения этой цели заключается в следующем. ISR должна выполнять минимум действий по обслуживанию своего устройства, сохранять переменные данные о состоянии прерывания и откладывать передачу данных или выполнение других не столь критичных по времени операций, как DPC, до снижения IRQL к уровню «DPC/dispatch» (подробнее о DPC и системе ввода-вывода см. главу 9).

DPC представляется DPC-объектом, управляющим объектом ядра, невидимым программам пользовательского режима, но видимым драйверам и системному коду. Наиболее важной частью информации DPC-объекта является адрес системной функции, которую ядро должно вызвать для обработки прерывания DPC DPC-процедуры, ожидающие выполнения, хранятся в управляемых ядром очередях (по одной на каждый процессор). Эти очереди называются очередями DPC Запрашивая DPC, системный код вызывает ядро для инициализации DPC-объекта и помещает его в очередь DPC

По умолчанию ядро помещает DPC-объекты в конец очереди DPC процессора, на котором был запрошен DPC (как правило, это процессор, на котором выполняется ISR). Однако драйвер устройства может изменить это, указав приоритет DPC (низкий, средний или высокий; по умолчанию — средний) или направив DPC конкретному процессору. DPC, направленный конкретному процессору, называется целевым DPC (targeted DPC). Если у DPC низкий или средний приоритет, ядро помещает DPC-объект в конец очереди, а если у DPC высокий приоритет, то — в начало.

Когда IRQL процессора вот-вот понизится с уровня «DPC/dispatch» или более высокого до уровня «APC» или «passive», ядро переходит к обработке всех DPC Windows оставляет IRQL на уровне «DPC/dispatch» и извлекает все DPC-объекты из очереди данного процессора (т. е. ядро опустошает очередь), поочередно вызывая каждую DPC-функцию. Ядро разрешает уменьшить IRQL ниже уровня «DPC/dispatch» для продолжения выполнения обычных потоков только после опустошения очереди. Схема обработки DPC показана на рис. 3–7.

Приоритеты DPC могут влиять на поведение системы и иным способом. Обычно ядро начинает опустошение очереди DPC с прерывания уровня «DPC/dispatch». Такое прерывание генерируется ядром, только если DPC направлен на процессор, на котором выполняется ISR, и DPC имеет средний или высокий приоритет. Если у DPC низкий приоритет, ядро генерирует прерывание, только если число незавершенных запросов DPC превышает пороговое значение или если число DPC, запрошенных на процессоре за установленный период, невелико. Если DPC направлен другому процессору (не тому, на котором выполняется ISR) и его приоритет высокий, ядро немедленно посылает ему диспетчерское IPI, сигнализируя целевому процессору о необходимости опустошения его очереди DPC Если приоритет DPC средний или низкий, для появления прерывания «DPC/dispatch» число DPC в очереди целевого процессора должно превышать пороговое значение. Системный поток простоя также опустошает очередь DPC процессора, на котором он выполняется. Хотя уровни приоритета и направление DPC являются довольно гибкими средствами, у драйверов устройств редко возникает необходимость в изменении заданного по умолчанию поведения своих DPC-объектов. B таблице 3–1 даются сведения о ситуациях, в которых начинается опустошение очереди DPC

Поскольку потоки пользовательского режима выполняются при низком IRQL, вероятность того, что DPC прервет выполнение обычного пользовательского потока, довольно велика. DPC-процедуры выполняются независимо от того, какой поток работает в настоящий момент. Это означает, что выполняемая DPC-процедура не в состоянии предугадать текущий размер спроецированного адресного пространства процесса. DPC-процедуры могут вызывать функции ядра, но не могут обращаться к системным сервисам, генерировать ошибки страницы, создавать или ждать объекты диспетчера. Однако они способны получать доступ к неподкачиваемым областям системной памяти, поскольку системное адресное пространство всегда спроецировано независимо от того, что представляет собой текущий процесс.

Данный текст является ознакомительным фрагментом.