ПРИМЕЧАНИЕ DWORD-параметр реестра HKLM\System\CurrentControlSet\Session Manager\I/O System\LargIrpStackLocations (если он определен) указывает, сколько блоков стека содержится в IRP, которые хранятся в ассоциативном списке больших IRP
Ha рис. 9-9 показан пример запроса ввода-вывода, демонстрирующий взаимосвязи между IRP и объектами «файл», «устройство» и «драйвер». Данный пример относится к запросу ввода-вывода, который адресован одноуровневому драйверу, но большинство операций ввода-вывода гораздо сложнее, так как в их выполнении участвует не один, а несколько многоуровневых драйверов. (Этот случай мы рассмотрим позже.)

Блок стека IRP
IRP состоит из двух частей: фиксированного заголовка (часто называемого телом IRP) и одного или нескольких блоков стека. Фиксированная часть содержит такую информацию, как тип и размер запроса, указатель на буфер в случае буферизованного ввода-вывода, данные о состоянии, изменяющиеся по мере обработки запроса, а также сведения о том, является запрос синхронным или асинхронным. Блок стека IRP (IRP stack location) содержит номер функции (состоящий из основного и дополнительного номеров), параметры, специфичные для функции, и указатель на объект «файл» вызывающего потока. Основной номер функции(major function code) идентифицирует принадлежащую драйверу процедуру диспетчеризации, которую диспетчер ввода-вывода вызывает при передаче IRP драйверу. Необязательный дополнительный номер функции(minor function code) иногда используется как модификатор основного номера. B командах управления электропитанием и Plug and Play всегда указывается дополнительный номер функции.B большинстве драйверов процедуры диспетчеризации определены только для подмножества основных функций, т. е. функций, предназначенных для создания/открытия, записи, чтения, управления вводом-выводом на устройстве, управления электропитанием, операций Plug and Play, System (для WMI-команд) и закрытия. Драйверы файловой системы определяют функции для всех (или почти всех) точек входа. Диспетчер ввода-вывода записывает в точки входа, не заполненные драйверами, указатели на свою функцию IopInvalidDeviceRequest.Эта функция возвращает вызывающему потоку код ошибки, который уведомляет о попытке обращения к функции, не поддерживаемой данным устройством.
ЭКСПЕРИМЕНТ: исследуем процедуры диспетчеризации, принадлежащие драйверу
Вы можете получить список всех функций, определенных драйвером для своих процедур диспетчеризации. Для этого введите команду !drvobjотладчика ядра и после имени (или адреса) объекта «драйвер» укажите значение 7. Следующий вывод показывает, что драйверы поддерживают 28 типов IRR



ЭКСПЕРИМЕНТ: просмотр незавершенных IRP потока
Команда !threadвыводит любые IRP, сопоставленные с потоком. Запустите отладчик ядра в работающей системе и найдите процесс диспетчера управления сервисами (Services.exe) в выводе, сгенерированном командой !process\



Управление буфером IRP
Когда приложение или драйвер устройства неявно создает IRP с помощью системного сервиса NtReadFile, NtWriteFile или NtDeviceIoControlFile(этим сервисам соответствуют Windows-функции ReadFile, WriteFile и DeviceIoControt),диспетчер ввода-вывода определяет, должен ли он участвовать в управлении буферами ввода и вывода вызывающего потока. Диспетчер ввода-вывода поддерживает три вида управления буферами.(o) Буферизованный ввод-вывод (buffered I/O)Диспетчер ввода-вывода выделяет в пуле неподкачиваемой памяти буфер, равный по размеру буферу вызывающего потока. Создавая IRP при операциях записи, диспетчер ввода-вывода копирует данные из буфера вызывающего потока в выделенный буфер. Завершая обработку IRP при операциях чтения, диспетчер ввода-вывода копирует данные из выделенного буфера в пользовательский буфер и освобождает выделенный буфер.
(o) Прямой ввод-вывод (direct I/O)Создавая IRP, диспетчер ввода-вывода блокирует пользовательский буфер в памяти (делает его неподкачиваемым). Закончив работу с IRP, диспетчер ввода-вывода разблокирует буфер. Диспетчер хранит описание этой памяти в форме MDL (memory descriptor list). MDL указывает объем физической памяти, занятой буфером (подробнее о MDL см. Windows DDK). Устройствам, использующим DMA (прямой доступ к памяти), требуется лишь физическое описание буфера, поэтому таким устройствам достаточно MDL. (Устройства, поддерживающие DMA, передают данные в память компьютера напрямую, не используя процессор.) Ho, если драйверу нужен доступ к содержимому буфера, он может спроецировать его на системное адресное пространство.
(o) Ввод-вывод без управления (neither I/O)Диспетчер ввода-вывода не участвует в управлении буферами. Ответственность за управление ими возлагается на драйвер устройства.
При любом типе управления буферами диспетчер ввода-вывода помещает в IRP ссылки на буферы ввода и вывода. Тип управления буферами, реализуемого диспетчером ввода-вывода, зависит от типа управления, запрошенного драйвером для операций конкретного типа. Драйвер регистрирует нужный ему тип управления буферами для операций чтения и записи в объекте «устройство», который представляет устройство. Операции управления вводом-выводом на устройстве (выполняемые NtDeviceIoControlFile)задаются определенными в драйвере управляющими кодами ввода-вывода. Управляющий код включает тип управления буферами со стороны диспетчера ввода-вывода при обработке IRP с данным кодом.
Когда вызывающие потоки передают запросы размером менее одной страницы (4 Кб на х86-процессорах), драйверы, как правило, используют буферизованный ввод-вывод, а для запросов большего размера – прямой. Буфер примерно равен размеру страницы, и операция копирования с применением буферизованного ввода-вывода приводит практически к тем же издержкам, что и прямой ввод-вывод, требующий блокирования памяти. Драйверы файловой системы обычно используют третий тип управления, так как при копировании данных из кэша файловой системы в буфер вызывающего потока это позволяет избавиться от издержек, связанных с управлением буферами. Ho большинство драйверов не использует этот вид управления из-за того, что указатель на буфер вызывающего потока действителен лишь на то время, пока выполняется этот поток. Если драйверу нужно передать данные с устройства (или на устройство) при выполнении DPC-процедуры или ISR, он должен позаботиться о доступности данных вызывающего потока из контекста любого процесса, а значит, у буфера должен быть системный виртуальный адрес.
Драйверы, использующие ввод-вывод без управления для доступа к буферам, которые могут быть расположены в пользовательском пространстве, должны проверять, что адреса буфера действительны и не ссылаются на память режима ядра. Если они этого не делают, появляется вероятность краха системы или уязвимости в системе защиты, так как приложения получают доступ к памяти режима ядра или возможность внедрения своего кода в ядро. Функции ProbeForReadи ProbeForWrite,которые ядро предоставляет драйверам, проверяют, полностью ли умещается буфер в пользовательской части адресного пространства. Чтобы избежать краха из-за ссылки на недопустимый адрес, драйверы могут обращаться к буферам пользовательского режима из кода обработки исключений (блоков try/except), который перехватывает любые попытки доступа по неправильным адресам и транслирует их в коды ошибок для передачи приложению.
Запрос ввода-вывода к одноуровневому драйверу
B этом разделе вы увидите, как обрабатывается запрос синхронного ввода-вывода к одноуровневому драйверу режима ядра. Такая обработка проходит в семь этапов.1. Запрос на ввод-вывод передается через DLL подсистемы.
2. DLL подсистемы вызывает сервис NtWriteFileдиспетчера ввода-вывода.
3. Диспетчер ввода-вывода создает IRP, описывающий запрос, и посылает его драйверу (в данном случае – драйверу устройства), вызывая свою функцию IoCallDriver.
4. Драйвер передает данные из IRP на устройство и инициирует операцию ввода-вывода.
5. Драйвер уведомляет о завершении ввода-вывода, генерируя прерывание.
6. Когда устройство завершает операцию и вызывает прерывание, драйвер устройства обслуживает прерывание.
7. Драйвер вызывает функцию IoCompleteRequestдиспетчера ввода-вывода, чтобы уведомить его о завершении обработки IRP, и диспетчер ввода-вывода завершает данный запрос на ввод-вывод.
Эти семь этапов показаны на рис. 9-10.

Обслуживание прерывания
Завершая передачу данных, устройство генерирует прерывание, после чего в дело вступают ядро Windows, диспетчер ввода-вывода и драйвер устройства. Ha рис. 9-11 показана первая фаза этого процесса. (Механизм диспетчеризации прерываний, включая DPC, описывается в главе 3. Мы кратко повторяем этот материал, потому что DPC играют ключевую роль в обработке ввода-вывода.)
Преимущество выполнения большей части обработки прерываний от устройств через DPC в том, что это разрешает любые блокируемые прерывания с приоритетами от «Device IRQL» до «DPC/dispatch» – пока не началась обработка DPC, имеющего более низкий приоритет. A за счет этого удается более оперативно (чем это могло бы быть в ином случае) обслуживать прерывания среднего приоритета. Вторая фаза ввода-вывода (обработка DPC) показана на рис. 9-12.
Завершение обработки запроса на ввод-вывод
После того как DPC-процедура драйвера выполнена, до завершения запроса на ввод-вывод остается проделать кое-какую оставшуюся работу. Третья стадия обработки ввода-вывода называется завершением ввода-вывода(I/O completion) и начинается с вызова драйвером функции IoСотрleteRequestдля уведомления диспетчера ввода-вывода о том, что обработка запроса, указанного в IRP (и принадлежащих ему блоках стека), закончена. Действия, выполняемые на этом этапе, различны для разных операций ввода-вывода. Например, все сервисы ввода-вывода записывают результат операции в блок статуса ввода-вывода (I/O status block) – структуру данных, предоставляемую вызывающим потоком. Некоторые сервисы, выполняющие буферизованный ввод-вывод, требуют возврата данных вызывающему потоку через подсистему ввода-вывода.B любом случае подсистема ввода-вывода должна копировать отдельные данные из системной памяти в виртуальное адресное пространство процесса, которому принадлежит вызывающий поток. Если IRP выполняется синхронно, это адресное пространство является текущим и доступно напрямую, но если IRP обрабатывается асинхронно, диспетчер ввода-вывода должен отложить завершение IRP до тех пор, пока у него не появится возможность обращаться к нужному адресному пространству. Чтобы получить доступ к виртуальному адресному пространству процесса, которому принадлежит вызывающий поток, диспетчер ввода-вывода должен передавать данные «в контексте вызывающего потока», т. е. при выполнении этого потока (иначе говоря, процесс этого потока должен быть текущим, а его адресное пространство – активным на процессоре). Эту задачу диспетчер ввода-вывода решает, ставя в очередь данному потоку APC режима ядра (рис. 9-13).
Как уже говорилось в главе 3, APC выполняется только в контексте определенного потока, a DPC – в контексте любого потока. Это означает, что DPC не затрагивает адресное пространство процесса пользовательского режима. Вспомните также, что приоритет программного прерывания у DPC выше, чем у APC



Синхронизация
Драйверы должны синхронизировать свое обращение к глобальным данным и регистрам устройств в силу двух причин.(o)Выполнение драйвера может быть прервано из-за вытеснения потоками с более высоким приоритетом, по истечении выделенного кванта процессорного времени, а также из-за генерации прерывания.
(o)B многопроцессорных системах Windows может выполнять код драйвера сразу на нескольких процессорах.
Без синхронизации данные могут быть повреждены. Например, код драйвера устройства выполняется при IRQL уровня «passive». Какая-то программа инициирует операцию ввода-вывода, в результате чего возникает аппаратное прерывание. Оно прерывает выполнение кода драйвера и активизирует его ISR. Если в этот момент драйвер изменял какие-либо данные, которые модифицирует и ISR (например, регистры устройства, память из кучи или статические данные), они могут быть повреждены после выполнения ISR. Эту проблему демонстрирует рис. 9-15.

Ядро Windows предоставляет специальную синхронизирующую процедуру KeSynchronizeExecution,которую драйверы устройств должны вызывать при доступе к данным, разделяемым с ISR. Эта процедура не допускает выполнения ISR, пока драйвер обращается к общим данным. B однопроцессорных системах перед обновлением общих структур данных она повышает IRQL до уровня, сопоставленного с ISR. Ho в многопроцессорных системах эта методика не гарантирует полной блокировки, так как код драйвера может выполняться на двух и более процессорах одновременно. Поэтому в многопроцессорных системах применяется другой механизм – спин-блокировка (см. раздел «Синхронизация ядра» главы 3). Драйвер также может использовать KeAcquireInterruptSpinLockдля прямого доступа к спин-блокировке объекта прерывания, хотя вариант синхронизации с ISR через KeSynchronizeExecutionобычно работает быстрее.
Теперь вы понимаете, что не только ISR требуют особого внимания: любые данные, используемые драйвером устройства, могут быть объектом доступа со стороны другой части того же драйвера, выполняемой на другом процессоре. Так что синхронизация доступа к любым глобальным или разделяемым данным (и обращений к самому физическому устройству) критически важна для кода драйвера устройства. Если ISR тоже обращается к этим данным, драйвер устройства должен вызывать KeSynchronizeExecution\в ином случае драйвер устройства может использовать стандартные спин-блокировки ядра.
Запрос ввода-вывода к многоуровневому драйверу
B предыдущем разделе мы рассмотрели обработку запроса на ввод-вывод, адресованного простому устройству, которое управляется единственным драйвером устройства. Обработка ввода-вывода для устройств, имеющих дело с файлами, или запросов к другим многоуровневым драйверам во многом аналогична. Конечно, основное отличие в том, что появляется один или несколько дополнительных уровней обработки.Прохождение запроса на асинхронный ввод-вывод через многоуровневые драйверы показано на рис. 9-l6. Данный пример относится к диску, управляемому файловой системой.
И вновь диспетчер ввода-вывода получает запрос, создает IRP для его представления, но на этот раз передает пакет драйверу файловой системы. C этого момента драйвер файловой системы в основном и управляет операцией ввода-вывода. B зависимости от типа запроса файловая система посылает драйверу диска тот же IRP или генерирует дополнительные IRP и передает их этому драйверу по отдельности.
ЭКСПЕРИМЕНТ: просмотр стека устройства
Команда !devstackотладчика ядра показывает стек устройства, содержащий многоуровневые объекты «устройство», сопоставленные с указанным объектом «устройство». B данном примере выводится стек устройства для объекта «устройство» \device\keyboardclass0, который принадлежит драйверу класса клавиатур:

Файловая система скорее всего будет повторно использовать IRP, если полученный запрос можно преобразовать в единый запрос к устройству. Например, если приложение выдаст запрос на чтение первых 512 байтов из файла на дискете, файловая система FAT просто вызовет драйвер диска, попросив его считать один сектор с того места на дискете, где начинается нужный файл.
Для поддержки использования несколькими драйверами IRP содержит набор блоков стека (не путать со стеком потока). Эти блоки данных – по одному на каждый вызываемый драйвер – хранят информацию, необходимую каждому драйверу для обработки своей части запроса (например, номер функции, параметры, сведения о контексте драйвера). Как показано на рис. 9-l6, по мере передачи IRP от одного драйвера другому заполняются дополнительные блоки стека. IRP можно считать аналогом стека в отношении добавления и удаления данных. Ho IRP не сопоставляется ни с каким процессом, и его размер фиксирован. B самом начале операции ввода-вывода диспетчер ввода-вывода выделяет память для IRP в одном из ассоциативных списков IRP или в пуле неподкачиваемой памяти.

ЭКСПЕРИМЕНТ: исследуем IRP
B этом эксперименте вы найдете незавершенные IRP в системе и определите тип IRP, устройство, которому он адресован, драйвер, управляющий этим устройством, поток, выдавший IRP, и процесс, к которому относится данный поток.
B любой момент в системе есть хотя бы несколько незавершенных IRR Это вызвано тем, что существует много устройств, которым приложения могут посылать IRP, а драйвер обрабатывает запрос только при возникновении определенного события, скажем, при появлении данных. Один из примеров – чтение с сетевого устройства. Увидеть незавершенные IRP в системе позволяет команда !irpfindотладчика ядра:


Следующий шаг – выяснить, какому объекту «устройство» адресован IRP Для этого выполните команду !devobj,указав адрес объекта «устройство», взятый из активного блока стека:


Детальные сведения о потоке и процессе, выдавшем этот IRP, можно просмотреть командами !threadи /process:


Рис. 9-17. Завершение обработки запроса на ввод-вывод к многоуровневым драйверам



ПРИМЕЧАНИЕ Все драйверы, управляющие дисковыми файловыми системами в Windows, являются частью как минимум трехуровневого стека драйверов: драйвер файловой системы находится на верхнем уровне, диспетчер томов – на среднем, а драйвер диска – на нижнем. Кроме того, между этими драйверами может размещаться любое число драйверов фильтров. Для ясности в предыдущем примере были показаны лишь драйверы файловой системы и диска. Подробнее об управлении внешней памятью см. главу 10.