CancelIo),диспетчер ввода-вывода выполняет процедуру отмены, связанную с IRP (если таковая есть). Процедура отмены отвечает за выполнение любых действий, необходимых для освобождения всех ресурсов, выделенных при обработке IRP, а также за завершение IRP со статусом отмены.
(o) Процедура выгрузкиЭта процедура освобождает все системные ресурсы, задействованные драйвером, после чего диспетчер ввода-вывода может удалить их из памяти. При выполнении процедуры выгрузки обычно освобождаются ресурсы, выделенные процедурой инициализации. Драйвер может загружаться и выгружаться во время работы системы.
(o) Процедура уведомления о завершении работы системыЭта процедура позволяет драйверу проводить очистку при завершении работы системы.
(o) Процедуры регистрации ошибокПри возникновении неожиданных ошибок (например, когда на диске появляется поврежденный блок), процедуры регистрации ошибок, принадлежащие драйверу, уведомляют о них диспетчер ввода-вывода. Последний записывает эту информацию в файл журнала ошибок.
ПРИМЕЧАНИЕ Большинство драйверов устройств написано на С. Применение языка ассемблера крайне не рекомендуется из-за его сложности и из-за того, что он затрудняет перенос драйвера между аппаратными архитектурами вроде x86, х64 и IА64.
(o)Объект «драйвер», представляющий отдельный драйвер в системе. Именно от этого объекта диспетчер ввода-вывода получает адрес процедуры диспетчеризации (точки входа) драйвера.
(o)Объект «устройство», представляющий физическое или логическое устройство в системе и описывающий его характеристики, например границы выравнивания буферов и адреса очередей для приема IRP, поступающих на это устройство.
Диспетчер ввода-вывода создает объект «драйвер» при загрузке в систему соответствующего драйвера и вызывает его инициализирующую процедуру (например, DriverEntry),которая записывает в атрибуты объекта точки входа этого драйвера.
После загрузки драйвер может создавать объекты «устройство» для представления устройств или даже для формирования интерфейса драйвера (вызовом IoCreateDeviceили IoCreateDeviceSecure).Однако большинство PnP-драйверов создают объекты «устройство» с помощью своих процедур добавления устройств, когда диспетчер PnP информирует их о присутствии управляемого ими устройства. C другой стороны, драйверы, не отвечающие спецификации Plug and Play, создают объекты «устройство» при вызове диспетчером ввода-вывода их инициализирующих процедур. Диспетчер ввода-вывода выгружает драйвер после удаления его последнего объекта «устройство», когда ссылок на устройство больше нет.
Создавая объект «устройство», драйвер может присвоить ему имя. Тогда этот объект помещается в пространство имен диспетчера объектов. Драйвер может определить имя этого объекта явно или позволить диспетчеру ввода-вывода сгенерировать его автоматически (о пространстве имен диспетчера объектов см. главу 3). По соглашению объекты «устройство» помещаются в каталог \Device пространства имен, недоступный приложениям через Windows API.
ПРИМЕЧАНИЕ Некоторые драйверы размещают объекты «устройство» в каталогах, отличных от \Device. Так, диспетчер томов Logical Disk Manager создает объекты «устройство», представляющие разделы жесткого диска, в каталоге \Device\HarddiskDmVolumes (подробнее на эту тему см. главу 10).
Чтобы сделать объект «устройство» доступным для приложений, драйвер должен создать в каталоге \Global?? (или в каталоге \?? в Windows 2000) символьную ссылку на имя этого объекта в каталоге \Device. Драйверы, не поддерживающие Plug and Play, и драйверы файловой системы обычно создают символьную ссылку с общеизвестным именем (скажем, \Device\Hardware2). Поскольку общеизвестные имена не срабатывают в средах с динамически меняющимся составом оборудования, PnP-драйверы предоставляют один или несколько интерфейсов через функцию IoRegisterDeviceInterface,передавая ей GUID, определяющий тип предоставляемой функциональности. GUID являются 128-битными числами, которые можно генерировать с помощью утилиты Guidgen, входящей в состав DDK и Platform SDK. Диапазон чисел, который может быть представлен 128 битами, гарантирует, что каждый GUID, созданный этой утилитой, всегда будет глобально уникальным.
IoRegisterDeviceInterfaceопределяет символьную ссылку, сопоставляемую с экземпляром объекта «устройство». Однако, прежде чем диспетчер ввода-вывода действительно создаст ссылку, драйвер должен вызвать функцию IoSetDeviceInterfaceState,чтобы разрешить использование интерфейса этого устройства. Обычно драйвер делает это, когда диспетчер PnP посылает ему команду start-deviceдля запуска устройства.
Приложение, которому нужно открыть объект «устройство», представленный GUID-идентификатором, может вызывать PnP-функции настройки, например SetupDiEnumDeviceInterfacesдля перечисления интерфейсов, доступных по конкретному GUID, и получения имен символьных ссылок, с помощью которых может быть открыт объект «устройство». Чтобы получить дополнительную информацию (например, автоматически сгенерированное имя устройства), приложение вызывает функцию SetupDiGetDeviceInterface-Detailдля всех устройств, перечисленных SetupDiEnumDeviceInterfaces.Получив от SetupDiGetDeviceInterfaceDetailимя устройства, приложение обращается к Windows-функции CreateFile,чтобы открыть устройство и получить его описатель.
ЭКСПЕРИМЕНТ: просмотр каталога \Device
Для просмотра имен устройств в каталоге \Device пространства имен диспетчера объектов можно использовать утилиту Winobj (wwwsysin ternals.com )или команду !objectотладчика ядра. Ниже показан пример символьной ссыпки, созданной диспетчером ввода-вывода и указывающей на объект «устройство» с автоматически сгенерированным именем.
Команда !objectотладчика ядра для каталога \Device выводит следующую информацию.
При выполнении команды !objectс указанием объекта-каталога диспетчера объектов отладчик ядра записывает дамп содержимого каталога в соответствии с его внутренней организацией в диспетчере объектов. Для ускорения поиска каталог хранит объекты в хэш-таблице, основанной на хэше имен объектов, поэтому команда !objectперечисляет объекты так, как они хранятся в каждой корзине (bucket) хэш-таблицы каталога.
Как видно на рис. 9-6 ,объект «устройство» ссыпается на свой объект «драйвер», благодаря чему диспетчер ввода-вывода знает, из какого драйвера нужно вызвать процедуру при получении запроса ввода-вывода. C помощью объекта «устройство» он находит объект «драйвер», который представляет драйвер, обслуживающий устройство. После этого он обращается к объекту «драйвер», используя номер функции из исходного запроса; каждый номер функции соответствует точке входа драйвера (номера функций на рис. 9-6 подробнее описываются в разделе «Блок стека IRP» далее в этой главе).
C объектом «драйвер» нередко сопоставляется несколько объектов «устройство». Список объектов «устройство» представляет физические и логические устройства, управляемые драйвером. Так, для каждого раздела жесткого диска имеется отдельный объект «устройство» с информацией, специфичной для данного раздела. Ho для обращения ко всем разделам используется один и тот же драйвер жесткого диска. При выгрузке драйвера из системы диспетчер ввода-вывода с помощью очереди объектов «устройство» определяет устройства, на которые повлияет удаление драйвера.
ЭКСПЕРИМЕНТ: исследуем объекты «драйвер» и «устройство»
Эти объекты можно исследовать с помощью команд !drvobjи !devobjотладчика ядра. Следующий пример относится к объекту «драйвер» для драйверов класса «клавиатура». У этого объекта имеется единственный объект «устройство».
Заметьте, что команда !devobjзаодно сообщает адреса и имена любых объектов «устройство», поверх которых размещен просматриваемый вами объект ( строка AttachedTo ),а также объектов «устройство», размещенных над указанным объектом ( строка AttachedDevice ).
Использование объектов для регистрации информации о драйверах означает, что диспетчеру ввода-вывода не нужно знать никаких деталей реализации драйверов. Он просто находит драйвер по указателю, тем самым позволяя легко загружать новые драйверы и обеспечивая их переносимость. Кроме того, представление устройств и драйверов разными объектами упрощает подсистеме ввода-вывода закрепление драйверов за дополнительными устройствами, которые появляются при изменении конфигурации системы.
Объекты «файл» – представление ресурсов в памяти, которое обеспечивает чтение и запись данных в эти ресурсы. B таблице 9-1 перечислены некоторые атрибуты объектов «файл». Описание и размеры полей см. в определении структуры FILE_OBJECT в Ntddk.h.
ЭКСПЕРИМЕНТ: просмотр структуры данных объекта «файл»
Вы можете просмотреть содержимое этой структуры с помощью команды dtотладчика ядра:
Когда поток открывает файл или простое устройство, диспетчер ввода-вывода возвращает описатель объекта «файл». Рис. 9-7 иллюстрирует, что происходит при открытии файла.
B этом примере С-программа (1) вызывает из стандартной библиотеки функцию fopen, которая в свою очередь вызывает Windows-функцию CreateFile(2). Далее DLL подсистемы Windows (в данном случае – Kernel32.dll) вызывает функцию NtCreateFileиз Ntdll.dll (3). Эта функция в Ntdll.dll содержит соответствующие команды, вызывающие переход в режим ядра в диспетчер системных сервисов, который и обращается к настоящей функции NtCreateFile(4) из Ntoskrnl.exe (о диспетчеризации системных сервисов см. главу 3).
ПРИМЕЧАНИЕ Объекты «файл» представляют открытые экземпляры файлов, а не сами файлы. B отличие от UNIX-систем, где используются vnode, в Windows представление файла не определено – системные драйверы Windows определяют свои представления.
Как и другие объекты исполнительной системы, файлы защищаются дескрипторами защиты, которые содержат список управления доступом (ACL). Чтобы выяснить, позволяет ли ACL файла получить процессу доступ того типа, который был запрошен его потоком, диспетчер ввода-вывода обращается к системе защиты. Если да, диспетчер объектов (5,6) разрешает доступ и сопоставляет предоставленные права доступа с описателем файла, возвращаемым потоку. Если этому или другому потоку того же процесса понадобятся дополнительные операции с файлом, не указанные в исходном запросе, ему придется открыть новый описатель, по которому тоже будет проведена проверка прав доступа (подробнее о защите объектов см. главу 8).
ЭКСПЕРИМЕНТ: просмотр описателей устройств
У любого процесса, открывшего описатель какого-либо устройства, в таблице описателей появляется объект «файл», соответствующий открытому экземпляру. Для просмотра таких описателей вполне годится Process Explorer: выберите процесс и пометьте Show Lower Pane в меню View и Handles в подменю Lower Pane View меню View. Отсортируйте по столбцу Туре и прокрутите содержимое до того места, где показываются описатели, представляющие объекты «файл»; они помечаются как «File».
B данном примере у процесса Csrss имеются открытые описатели объектов «файл», которые представляют открытые экземпляры устройств и получают автоматически генерируемые имена, а также описатели объектов «файл», принадлежащих драйверу службы терминалов. Чтобы просмотреть конкретный объект «файл» в отладчике ядра, сначала определите адрес этого объекта. Следующая команда выводит сведения о выделенном на иллюстрации описателе (0xB8), который принадлежит процессу Csrss.exe с идентификатором 2332 (0x91c):
Поскольку это объект «файл», вы можете получить информацию о нем командой !fileobj:
Поскольку объект «файл» – представление разделяемого ресурса в памяти, а не сам ресурс, он отличается от остальных объектов исполнительной системы. Объект «файл» содержит лишь данные, уникальные для описателя объекта, тогда как собственно файл – совместно используемые данные или текст. Всякий раз, когда поток открывает описатель файла, создается новый объект «файл» с новым набором атрибутов, специфичным для этого описателя. Например, атрибут «текущее смещение в байтах» определяет место в файле, где будут записаны или считаны следующие данные по текущему описателю. У каждого описателя одного и того же файла свое значение атрибута «текущее смещение в байтах». Кроме того, объект «файл» уникален для процесса; исключение составляют случаи, когда процесс дублирует описатель файла для передачи другому процессу (через Windows-функцию DuplicateHandle)или когда дочерний процесс наследует описатель файла от родительского. Только в этих двух случаях процессы располагают отдельными описателями, которые ссылаются на один и тот же объект «файл».
Описатель файла уникален для процесса, но определяемый им физический ресурс – нет. Поэтому, как и при доступе к любому другому общему ресурсу, потоки должны синхронизировать свое обращение к совместно используемым файлам, каталогам и устройствам. Если, например, поток что-то записывает в файл, то при открытии описателя файла он должен указать монопольный доступ для записи, чтобы другие потоки не могли записывать в этот файл одновременно с первым потоком. B качестве альтернативы поток может заблокировать на время записи часть файла с помощью Windows-функции LockFile.
Имя открываемого файла включает имя объекта «устройство», который представляет устройство, содержащее файл. Например, \Device\FloppyO\Myfile.dat ссылается на файл Myfile.dat на дискете в дисководе А. Подстрока «\Device\FloppyO» является внутренним именем объекта «устройство», представляющего данный дисковод. При открытии файла Myfile.dat диспетчер ввода-вывода создает объект «файл», сохраняет в нем указатель на объект «устройство» FloppyO и возвращает описатель файла вызывающему потоку. Впоследствии, когда вызывающий поток воспользуется описателем файла, диспетчер ввода-вывода сможет обращаться непосредственно к объекту FloppyO. Учтите, что внутренние имена устройств неприменимы в Window's-приложениях. Для них имена устройств должны находиться в специальном каталоге пространства имен диспетчера объектов – \?? (в Windows 2000) или \Global?? (в Windows XP и Windows Server 2003). B этом каталоге содержатся символьные ссылки на внутренние имена устройств. За создание ссылок в этом каталоге отвечают драйверы устройств. Вы можете просмотреть и даже изменить эти ссылки программным способом, через Windows-функции QueryDosDeviceи DefineDosDevice.
ЭКСПЕРИМЕНТ: просмотр сопоставлений Windows-имен устройств внутренним именам устройств
Утилита Winobj ( wwwsysinternak.com )позволяет исследовать символьные ссылки, определяющие пространство Windows-имен устройств. Запустите Winobj и выберите каталог \?? в Windows 2000 или \Global?? в Windows XP либо Windows Server 2003.
Обратите внимание на символьные ссылки справа. Попробуйте дважды щелкнуть устройство С: – вы должны увидеть нечто вроде того, что показано ниже.
С: представляет собой символьную ссылку на внутреннее устройство с именем \Device\HarddiskVolumel, или на первый том первого жесткого диска в системе. Элемент COMl, показанный Winobj, – символьная ссылка на \Device\SerialO и т. д. Попробуйте создать собственные ссылки командой substв командной строке.
Асинхронный ввод-выводпозволяет приложению выдать запрос на ввод-вывод и продолжить выполнение, не дожидаясь передачи данных устройством. Этот тип ввода-вывода увеличивает эффективность работы приложения, позволяя заниматься другими задачами, пока выполняется операция ввода-вывода. Для использования асинхронного ввода-вывода вы должны указать при вызове CreateFileфлаг FILE_FLAG_OVERLAPPED. Конечно, инициировав операцию асинхронного ввода-вывода, поток должен соблюдать осторожность и не обращаться к запрошенным данным до их получения от устройства. Следовательно, поток должен синхронизировать свое выполнение с завершением обработки запроса на ввод-вывод, отслеживая описатель синхронизирующего объекта (которым может быть событие, порт завершения ввода-вывода или сам объект «файл»), который по окончании ввода-вывода перейдет в свободное состояние.
Независимо от типа запроса операции ввода-вывода, инициированные драйвером в интересах приложения, выполняются асинхронно, т. е. после выдачи запроса драйвер устройства возвращает управление подсистеме ввода-вывода. A когда она вернет управление приложению, зависит от типа запроса. Схема управления при инициации операции чтения показана на рис. 9-8. Заметьте, что ожидание зависит от состояния флага перекрытия в объекте «файл» и реализуется функцией NtReadFileв режиме ядра.
Вы можете проверить статус незавершенной операции асинхронного ввода-вывода вызовом Windows-функции HasOverlappedIoCompleted.При использовании портов завершения ввода-вывода с той же целью можно вызывать GetQueuedCompletionStatus.
ЭКСПЕРИМЕНТ: просмотр процедур быстрого ввода-вывода, зарегистрированных драйвером
Список процедур быстрого ввода-вывода, зарегистрированных драйвером в своем объекте «драйвер», выводит команда !drvobjотладчика ядра. Ho такие процедуры обычно имеют смысл только для драйверов файловой системы. Ниже показан список процедур быстрого ввода-вывода для объекта драйвера файловой системы NTFS.
Как показывает вывод, NTFS зарегистрировала свою процедуру Ntfs FastIoCheckIfPossibleкак элемент FastIoCheckIfPossibleсписка процедур быстрого ввода-вывода. По имени этого элемента можно догадаться, что диспетчер ввода-вывода вызывает эту функцию перед выдачей запроса на быстрый ввод-вывод и в ответ драйвер сообщает, возможны ли операции быстрого ввода-вывода применительно к данному файлу.
Ввод-вывод в проецируемые файлы доступен в пользовательском режиме через Windows-функции CreateFileMappingи MapViewOfFile.B самой операционной системе такой ввод-вывод используется при выполнении важных операций – при кэшировании файлов и активизации образов (загрузке и запуске исполняемых программ). Другим потребителем этого типа ввода-вывода является диспетчер кэша. Файловые системы обращаются к диспетчеру кэша для проецирования файлов данных на виртуальную память, что ускоряет работу программ, интенсивно использующих ввод-вывод. По мере использования файла диспетчер памяти подгружает в память страницы, к которым производится обращение. Если большинство систем кэширования выделяет для кэширования фиксированную область памяти, то кэш Windows расширяется или сокращается в зависимости от объема свободной памяти. Такое изменение размера кэша возможно благодаря тому, что диспетчер кэша опирается на соответствующую поддержку со стороны диспетчера памяти (см. главу 7). Используя преимущества подсистемы подкачки страниц диспетчера памяти, диспетчер кэша избегает дублирования работы, уже проделанной диспетчером памяти. (Внутреннее устройство диспетчера кэша рассматривается в главе 11.)
IRP look-aside list) хранит IRP с одним блоком стека (об этих блоках – чуть позже), а ассоциативный список больших IRP (large-IRP look-aside list) – IRP с несколькими блоками стека. По умолчанию в IRP второго списка содержится 8 блоков стека, но раз в минуту система варьирует это число на основании того, сколько блоков было запрошено. Если для IRP требуется больше блоков стека, чем имеется в ассоциативном списке больших IRP, диспетчер ввода-вывода выделяет память под IRP из пула неподкачиваемой памяти. После создания и инициализации IRP диспетчер ввода-вывода сохраняет в IRP указатель на объект «файл» вызывающего потока.
(o) Процедура выгрузкиЭта процедура освобождает все системные ресурсы, задействованные драйвером, после чего диспетчер ввода-вывода может удалить их из памяти. При выполнении процедуры выгрузки обычно освобождаются ресурсы, выделенные процедурой инициализации. Драйвер может загружаться и выгружаться во время работы системы.
(o) Процедура уведомления о завершении работы системыЭта процедура позволяет драйверу проводить очистку при завершении работы системы.
(o) Процедуры регистрации ошибокПри возникновении неожиданных ошибок (например, когда на диске появляется поврежденный блок), процедуры регистрации ошибок, принадлежащие драйверу, уведомляют о них диспетчер ввода-вывода. Последний записывает эту информацию в файл журнала ошибок.
ПРИМЕЧАНИЕ Большинство драйверов устройств написано на С. Применение языка ассемблера крайне не рекомендуется из-за его сложности и из-за того, что он затрудняет перенос драйвера между аппаратными архитектурами вроде x86, х64 и IА64.
Объекты «драйвер» и «устройство»
Когда поток открывает описатель объекта «файл» (этот процесс описывается в разделе «Обработка ввода-вывода» далее в этой главе), диспетчер ввода-вывода, исходя из имени этого объекта, должен определить, к какому драйверу (или драйверам) нужно обратиться для обработки запроса. Более того, диспетчер ввода-вывода должен знать, где найти эту информацию, когда в следующий раз поток вновь воспользуется тем же описателем файла. Для этого предназначены следующие объекты.(o)Объект «драйвер», представляющий отдельный драйвер в системе. Именно от этого объекта диспетчер ввода-вывода получает адрес процедуры диспетчеризации (точки входа) драйвера.
(o)Объект «устройство», представляющий физическое или логическое устройство в системе и описывающий его характеристики, например границы выравнивания буферов и адреса очередей для приема IRP, поступающих на это устройство.
Диспетчер ввода-вывода создает объект «драйвер» при загрузке в систему соответствующего драйвера и вызывает его инициализирующую процедуру (например, DriverEntry),которая записывает в атрибуты объекта точки входа этого драйвера.
После загрузки драйвер может создавать объекты «устройство» для представления устройств или даже для формирования интерфейса драйвера (вызовом IoCreateDeviceили IoCreateDeviceSecure).Однако большинство PnP-драйверов создают объекты «устройство» с помощью своих процедур добавления устройств, когда диспетчер PnP информирует их о присутствии управляемого ими устройства. C другой стороны, драйверы, не отвечающие спецификации Plug and Play, создают объекты «устройство» при вызове диспетчером ввода-вывода их инициализирующих процедур. Диспетчер ввода-вывода выгружает драйвер после удаления его последнего объекта «устройство», когда ссылок на устройство больше нет.
Создавая объект «устройство», драйвер может присвоить ему имя. Тогда этот объект помещается в пространство имен диспетчера объектов. Драйвер может определить имя этого объекта явно или позволить диспетчеру ввода-вывода сгенерировать его автоматически (о пространстве имен диспетчера объектов см. главу 3). По соглашению объекты «устройство» помещаются в каталог \Device пространства имен, недоступный приложениям через Windows API.
ПРИМЕЧАНИЕ Некоторые драйверы размещают объекты «устройство» в каталогах, отличных от \Device. Так, диспетчер томов Logical Disk Manager создает объекты «устройство», представляющие разделы жесткого диска, в каталоге \Device\HarddiskDmVolumes (подробнее на эту тему см. главу 10).
Чтобы сделать объект «устройство» доступным для приложений, драйвер должен создать в каталоге \Global?? (или в каталоге \?? в Windows 2000) символьную ссылку на имя этого объекта в каталоге \Device. Драйверы, не поддерживающие Plug and Play, и драйверы файловой системы обычно создают символьную ссылку с общеизвестным именем (скажем, \Device\Hardware2). Поскольку общеизвестные имена не срабатывают в средах с динамически меняющимся составом оборудования, PnP-драйверы предоставляют один или несколько интерфейсов через функцию IoRegisterDeviceInterface,передавая ей GUID, определяющий тип предоставляемой функциональности. GUID являются 128-битными числами, которые можно генерировать с помощью утилиты Guidgen, входящей в состав DDK и Platform SDK. Диапазон чисел, который может быть представлен 128 битами, гарантирует, что каждый GUID, созданный этой утилитой, всегда будет глобально уникальным.
IoRegisterDeviceInterfaceопределяет символьную ссылку, сопоставляемую с экземпляром объекта «устройство». Однако, прежде чем диспетчер ввода-вывода действительно создаст ссылку, драйвер должен вызвать функцию IoSetDeviceInterfaceState,чтобы разрешить использование интерфейса этого устройства. Обычно драйвер делает это, когда диспетчер PnP посылает ему команду start-deviceдля запуска устройства.
Приложение, которому нужно открыть объект «устройство», представленный GUID-идентификатором, может вызывать PnP-функции настройки, например SetupDiEnumDeviceInterfacesдля перечисления интерфейсов, доступных по конкретному GUID, и получения имен символьных ссылок, с помощью которых может быть открыт объект «устройство». Чтобы получить дополнительную информацию (например, автоматически сгенерированное имя устройства), приложение вызывает функцию SetupDiGetDeviceInterface-Detailдля всех устройств, перечисленных SetupDiEnumDeviceInterfaces.Получив от SetupDiGetDeviceInterfaceDetailимя устройства, приложение обращается к Windows-функции CreateFile,чтобы открыть устройство и получить его описатель.
ЭКСПЕРИМЕНТ: просмотр каталога \Device
Для просмотра имен устройств в каталоге \Device пространства имен диспетчера объектов можно использовать утилиту Winobj (wwwsysin ternals.com )или команду !objectотладчика ядра. Ниже показан пример символьной ссыпки, созданной диспетчером ввода-вывода и указывающей на объект «устройство» с автоматически сгенерированным именем.
Команда !objectотладчика ядра для каталога \Device выводит следующую информацию.
При выполнении команды !objectс указанием объекта-каталога диспетчера объектов отладчик ядра записывает дамп содержимого каталога в соответствии с его внутренней организацией в диспетчере объектов. Для ускорения поиска каталог хранит объекты в хэш-таблице, основанной на хэше имен объектов, поэтому команда !objectперечисляет объекты так, как они хранятся в каждой корзине (bucket) хэш-таблицы каталога.
Как видно на рис. 9-6 ,объект «устройство» ссыпается на свой объект «драйвер», благодаря чему диспетчер ввода-вывода знает, из какого драйвера нужно вызвать процедуру при получении запроса ввода-вывода. C помощью объекта «устройство» он находит объект «драйвер», который представляет драйвер, обслуживающий устройство. После этого он обращается к объекту «драйвер», используя номер функции из исходного запроса; каждый номер функции соответствует точке входа драйвера (номера функций на рис. 9-6 подробнее описываются в разделе «Блок стека IRP» далее в этой главе).
C объектом «драйвер» нередко сопоставляется несколько объектов «устройство». Список объектов «устройство» представляет физические и логические устройства, управляемые драйвером. Так, для каждого раздела жесткого диска имеется отдельный объект «устройство» с информацией, специфичной для данного раздела. Ho для обращения ко всем разделам используется один и тот же драйвер жесткого диска. При выгрузке драйвера из системы диспетчер ввода-вывода с помощью очереди объектов «устройство» определяет устройства, на которые повлияет удаление драйвера.
ЭКСПЕРИМЕНТ: исследуем объекты «драйвер» и «устройство»
Эти объекты можно исследовать с помощью команд !drvobjи !devobjотладчика ядра. Следующий пример относится к объекту «драйвер» для драйверов класса «клавиатура». У этого объекта имеется единственный объект «устройство».
Заметьте, что команда !devobjзаодно сообщает адреса и имена любых объектов «устройство», поверх которых размещен просматриваемый вами объект ( строка AttachedTo ),а также объектов «устройство», размещенных над указанным объектом ( строка AttachedDevice ).
Использование объектов для регистрации информации о драйверах означает, что диспетчеру ввода-вывода не нужно знать никаких деталей реализации драйверов. Он просто находит драйвер по указателю, тем самым позволяя легко загружать новые драйверы и обеспечивая их переносимость. Кроме того, представление устройств и драйверов разными объектами упрощает подсистеме ввода-вывода закрепление драйверов за дополнительными устройствами, которые появляются при изменении конфигурации системы.
Открытие устройств
Объекты «файл» являются структурами режима ядра, которые точно соответствуют определению объектов в Windows: это системные ресурсы, доступные для совместного использования двум или нескольким процессам, у них могут быть имена, их безопасность обеспечивается моделью защиты объектов, и они поддерживают синхронизацию. Хотя большинство разделяемых ресурсов в Windows базируется в памяти, основная часть ресурсов, с которыми имеет дело подсистема ввода-вывода, размещается на физических устройствах или представляет их. Несмотря на эту разницу, операции над совместно используемыми ресурсами подсистемы ввода-вывода осуществляются как над объектами.Объекты «файл» – представление ресурсов в памяти, которое обеспечивает чтение и запись данных в эти ресурсы. B таблице 9-1 перечислены некоторые атрибуты объектов «файл». Описание и размеры полей см. в определении структуры FILE_OBJECT в Ntddk.h.
ЭКСПЕРИМЕНТ: просмотр структуры данных объекта «файл»
Вы можете просмотреть содержимое этой структуры с помощью команды dtотладчика ядра:
Когда поток открывает файл или простое устройство, диспетчер ввода-вывода возвращает описатель объекта «файл». Рис. 9-7 иллюстрирует, что происходит при открытии файла.
B этом примере С-программа (1) вызывает из стандартной библиотеки функцию fopen, которая в свою очередь вызывает Windows-функцию CreateFile(2). Далее DLL подсистемы Windows (в данном случае – Kernel32.dll) вызывает функцию NtCreateFileиз Ntdll.dll (3). Эта функция в Ntdll.dll содержит соответствующие команды, вызывающие переход в режим ядра в диспетчер системных сервисов, который и обращается к настоящей функции NtCreateFile(4) из Ntoskrnl.exe (о диспетчеризации системных сервисов см. главу 3).
ПРИМЕЧАНИЕ Объекты «файл» представляют открытые экземпляры файлов, а не сами файлы. B отличие от UNIX-систем, где используются vnode, в Windows представление файла не определено – системные драйверы Windows определяют свои представления.
Как и другие объекты исполнительной системы, файлы защищаются дескрипторами защиты, которые содержат список управления доступом (ACL). Чтобы выяснить, позволяет ли ACL файла получить процессу доступ того типа, который был запрошен его потоком, диспетчер ввода-вывода обращается к системе защиты. Если да, диспетчер объектов (5,6) разрешает доступ и сопоставляет предоставленные права доступа с описателем файла, возвращаемым потоку. Если этому или другому потоку того же процесса понадобятся дополнительные операции с файлом, не указанные в исходном запросе, ему придется открыть новый описатель, по которому тоже будет проведена проверка прав доступа (подробнее о защите объектов см. главу 8).
ЭКСПЕРИМЕНТ: просмотр описателей устройств
У любого процесса, открывшего описатель какого-либо устройства, в таблице описателей появляется объект «файл», соответствующий открытому экземпляру. Для просмотра таких описателей вполне годится Process Explorer: выберите процесс и пометьте Show Lower Pane в меню View и Handles в подменю Lower Pane View меню View. Отсортируйте по столбцу Туре и прокрутите содержимое до того места, где показываются описатели, представляющие объекты «файл»; они помечаются как «File».
B данном примере у процесса Csrss имеются открытые описатели объектов «файл», которые представляют открытые экземпляры устройств и получают автоматически генерируемые имена, а также описатели объектов «файл», принадлежащих драйверу службы терминалов. Чтобы просмотреть конкретный объект «файл» в отладчике ядра, сначала определите адрес этого объекта. Следующая команда выводит сведения о выделенном на иллюстрации описателе (0xB8), который принадлежит процессу Csrss.exe с идентификатором 2332 (0x91c):
Поскольку это объект «файл», вы можете получить информацию о нем командой !fileobj:
Поскольку объект «файл» – представление разделяемого ресурса в памяти, а не сам ресурс, он отличается от остальных объектов исполнительной системы. Объект «файл» содержит лишь данные, уникальные для описателя объекта, тогда как собственно файл – совместно используемые данные или текст. Всякий раз, когда поток открывает описатель файла, создается новый объект «файл» с новым набором атрибутов, специфичным для этого описателя. Например, атрибут «текущее смещение в байтах» определяет место в файле, где будут записаны или считаны следующие данные по текущему описателю. У каждого описателя одного и того же файла свое значение атрибута «текущее смещение в байтах». Кроме того, объект «файл» уникален для процесса; исключение составляют случаи, когда процесс дублирует описатель файла для передачи другому процессу (через Windows-функцию DuplicateHandle)или когда дочерний процесс наследует описатель файла от родительского. Только в этих двух случаях процессы располагают отдельными описателями, которые ссылаются на один и тот же объект «файл».
Описатель файла уникален для процесса, но определяемый им физический ресурс – нет. Поэтому, как и при доступе к любому другому общему ресурсу, потоки должны синхронизировать свое обращение к совместно используемым файлам, каталогам и устройствам. Если, например, поток что-то записывает в файл, то при открытии описателя файла он должен указать монопольный доступ для записи, чтобы другие потоки не могли записывать в этот файл одновременно с первым потоком. B качестве альтернативы поток может заблокировать на время записи часть файла с помощью Windows-функции LockFile.
Имя открываемого файла включает имя объекта «устройство», который представляет устройство, содержащее файл. Например, \Device\FloppyO\Myfile.dat ссылается на файл Myfile.dat на дискете в дисководе А. Подстрока «\Device\FloppyO» является внутренним именем объекта «устройство», представляющего данный дисковод. При открытии файла Myfile.dat диспетчер ввода-вывода создает объект «файл», сохраняет в нем указатель на объект «устройство» FloppyO и возвращает описатель файла вызывающему потоку. Впоследствии, когда вызывающий поток воспользуется описателем файла, диспетчер ввода-вывода сможет обращаться непосредственно к объекту FloppyO. Учтите, что внутренние имена устройств неприменимы в Window's-приложениях. Для них имена устройств должны находиться в специальном каталоге пространства имен диспетчера объектов – \?? (в Windows 2000) или \Global?? (в Windows XP и Windows Server 2003). B этом каталоге содержатся символьные ссылки на внутренние имена устройств. За создание ссылок в этом каталоге отвечают драйверы устройств. Вы можете просмотреть и даже изменить эти ссылки программным способом, через Windows-функции QueryDosDeviceи DefineDosDevice.
ЭКСПЕРИМЕНТ: просмотр сопоставлений Windows-имен устройств внутренним именам устройств
Утилита Winobj ( wwwsysinternak.com )позволяет исследовать символьные ссылки, определяющие пространство Windows-имен устройств. Запустите Winobj и выберите каталог \?? в Windows 2000 или \Global?? в Windows XP либо Windows Server 2003.
Обратите внимание на символьные ссылки справа. Попробуйте дважды щелкнуть устройство С: – вы должны увидеть нечто вроде того, что показано ниже.
С: представляет собой символьную ссылку на внутреннее устройство с именем \Device\HarddiskVolumel, или на первый том первого жесткого диска в системе. Элемент COMl, показанный Winobj, – символьная ссылка на \Device\SerialO и т. д. Попробуйте создать собственные ссылки командой substв командной строке.
Обработка ввода-вывода
Теперь, когда мы рассмотрели структуру и типы драйверов, а также поддерживающие их структуры данных, давайте обсудим то, как запросы ввода-вывода проходят по системе. Обработка запросов ввода-вывода включает несколько предсказуемых этапов. Наборы операций, выполняемых на этих этапах, варьируются в зависимости от того, какой драйвер управляет устройством, которому адресован запрос, – одно- или многоуровневый. Ho обработка зависит и от того, какой ввод-вывод запрошен – синхронный или асинхронный. Так что для начала мы рассмотрим эти два типа ввода-вывода и уже после этого перейдем к другим темам.Типы ввода-вывода
Приложения могут выдавать запросы ввода-вывода различных типов, например синхронного, асинхронного, проецируемого (данные с устройства проецируются на адресное пространство приложения для доступа через виртуальную память приложения, а не API-функции ввода-вывода), а также ввода-вывода, при котором данные передаются между устройством и непрерывными буферами приложения за один запрос. Более того, диспетчер ввода-вывода позволяет драйверам реализовать специфический интерфейс ввода-вывода, что нередко обеспечивает сокращение числа IRP, необходимых для обработки ввода-вывода. B этом разделе мы рассмотрим все типы ввода-вывода.Синхронный и асинхронный ввод-вывод
Большинство операций ввода-вывода приложений являются синхронными,т. е. приложение ждет, когда устройство выполнит передачу данных и вернет код статуса по завершении операции ввода-вывода. После этого программа продолжает работу и немедленно использует полученные данные. B таком простейшем варианте Windows-функции ReadFileи WriteFileвыполняются синхронно. Перед возвратом управления они должны завершить операцию ввода-вывода.Асинхронный ввод-выводпозволяет приложению выдать запрос на ввод-вывод и продолжить выполнение, не дожидаясь передачи данных устройством. Этот тип ввода-вывода увеличивает эффективность работы приложения, позволяя заниматься другими задачами, пока выполняется операция ввода-вывода. Для использования асинхронного ввода-вывода вы должны указать при вызове CreateFileфлаг FILE_FLAG_OVERLAPPED. Конечно, инициировав операцию асинхронного ввода-вывода, поток должен соблюдать осторожность и не обращаться к запрошенным данным до их получения от устройства. Следовательно, поток должен синхронизировать свое выполнение с завершением обработки запроса на ввод-вывод, отслеживая описатель синхронизирующего объекта (которым может быть событие, порт завершения ввода-вывода или сам объект «файл»), который по окончании ввода-вывода перейдет в свободное состояние.
Независимо от типа запроса операции ввода-вывода, инициированные драйвером в интересах приложения, выполняются асинхронно, т. е. после выдачи запроса драйвер устройства возвращает управление подсистеме ввода-вывода. A когда она вернет управление приложению, зависит от типа запроса. Схема управления при инициации операции чтения показана на рис. 9-8. Заметьте, что ожидание зависит от состояния флага перекрытия в объекте «файл» и реализуется функцией NtReadFileв режиме ядра.
Вы можете проверить статус незавершенной операции асинхронного ввода-вывода вызовом Windows-функции HasOverlappedIoCompleted.При использовании портов завершения ввода-вывода с той же целью можно вызывать GetQueuedCompletionStatus.
Быстрый ввод-вывод
Быстрый ввод-вывод (fast I/O) – специальный механизм, который позволяет подсистеме ввода-вывода напрямую, не генерируя IRP, обращаться к драйверу файловой системы или диспетчеру кэша (быстрый ввод-вывод описывается в главах 11 и 12). Драйвер регистрирует свои точки входа для быстрого ввода-вывода, записывая их адреса в структуру, на которую ссылается указатель PFASTIODISPATCH его объекта «драйвер».ЭКСПЕРИМЕНТ: просмотр процедур быстрого ввода-вывода, зарегистрированных драйвером
Список процедур быстрого ввода-вывода, зарегистрированных драйвером в своем объекте «драйвер», выводит команда !drvobjотладчика ядра. Ho такие процедуры обычно имеют смысл только для драйверов файловой системы. Ниже показан список процедур быстрого ввода-вывода для объекта драйвера файловой системы NTFS.
Как показывает вывод, NTFS зарегистрировала свою процедуру Ntfs FastIoCheckIfPossibleкак элемент FastIoCheckIfPossibleсписка процедур быстрого ввода-вывода. По имени этого элемента можно догадаться, что диспетчер ввода-вывода вызывает эту функцию перед выдачей запроса на быстрый ввод-вывод и в ответ драйвер сообщает, возможны ли операции быстрого ввода-вывода применительно к данному файлу.
Ввод-вывод в проецируемые файлы и кэширование файлов
Ввод-вывод в проецируемые файлы(mapped file I/O) – важная функция подсистемы ввода-вывода, поддерживаемая ею совместно с диспетчером памяти (о проецируемых файлах см. главу 7). Термин «ввод-вывод в проецируемые файлы» относится к возможности интерпретировать файл на диске как часть виртуальной памяти процесса. Программа может обращаться к такому файлу как к большому массиву, не прибегая к буферизации или дисковому вводу-выводу. При доступе программы к памяти диспетчер памяти использует свой механизм подкачки для загрузки нужной страницы из дискового файла. Если программа изменяет какие-то данные в своем виртуальном адресном пространстве, диспетчер памяти записывает эти данные обратно в дисковый файл в ходе обычной операции подкачки страниц.Ввод-вывод в проецируемые файлы доступен в пользовательском режиме через Windows-функции CreateFileMappingи MapViewOfFile.B самой операционной системе такой ввод-вывод используется при выполнении важных операций – при кэшировании файлов и активизации образов (загрузке и запуске исполняемых программ). Другим потребителем этого типа ввода-вывода является диспетчер кэша. Файловые системы обращаются к диспетчеру кэша для проецирования файлов данных на виртуальную память, что ускоряет работу программ, интенсивно использующих ввод-вывод. По мере использования файла диспетчер памяти подгружает в память страницы, к которым производится обращение. Если большинство систем кэширования выделяет для кэширования фиксированную область памяти, то кэш Windows расширяется или сокращается в зависимости от объема свободной памяти. Такое изменение размера кэша возможно благодаря тому, что диспетчер кэша опирается на соответствующую поддержку со стороны диспетчера памяти (см. главу 7). Используя преимущества подсистемы подкачки страниц диспетчера памяти, диспетчер кэша избегает дублирования работы, уже проделанной диспетчером памяти. (Внутреннее устройство диспетчера кэша рассматривается в главе 11.)
Ввод-вывод по механизму «scatter/gather»
Windows также поддерживает особый вид высокопроизводительного ввода-вывода с использованием механизма «scatter/gather»; он доступен через Windows-функции ReadFileScatterи WriteFileGather.Эти функции позволяют приложению в рамках одной операции считывать или записывать данные из нескольких буферов в виртуальной памяти в непрерывную область дискового файла, а не выдавать отдельный запрос ввода-вывода для каждого буфера. Чтобы задействовать такой ввод-вывод, вы должны открыть файл для некэшируемого асинхронного (перекрывающегося) ввода-вывода и выровнять пользовательские буферы по границам страниц. Более того, если ввод-вывод направлен на устройство массовой памяти, то передаваемые данные нужно выровнять по границам секторов устройства, а их объем должен быть кратен размеру сектора.Пакеты запроса ввода-вывода
Пакет запроса ввода-вывода(I/O request packet, IRP) хранит информацию, нужную для обработки запроса на ввод-вывод. Когда поток вызывает сервис ввода-вывода, диспетчер ввода-вывода создает IRP для представления операции в процессе ее выполнения подсистемой ввода-вывода. По возможности диспетчер ввода-вывода выделяет память под IRP в одном из двух ассоциативных списков IRP, индивидуальных для каждого процессора и хранящихся в пуле неподкачиваемой памяти. Ассоциативный список малых IRP (small-IRP look-aside list) хранит IRP с одним блоком стека (об этих блоках – чуть позже), а ассоциативный список больших IRP (large-IRP look-aside list) – IRP с несколькими блоками стека. По умолчанию в IRP второго списка содержится 8 блоков стека, но раз в минуту система варьирует это число на основании того, сколько блоков было запрошено. Если для IRP требуется больше блоков стека, чем имеется в ассоциативном списке больших IRP, диспетчер ввода-вывода выделяет память под IRP из пула неподкачиваемой памяти. После создания и инициализации IRP диспетчер ввода-вывода сохраняет в IRP указатель на объект «файл» вызывающего потока.