Сидякин И.М.
MS Windows. Элементы архитектуры и системное программирование. (курс лекций)
MS Windows. Элементы архитектуры и системное программирование.(Курс лекций) ИУ-3.МГТУ. 2000 Автор: Сидякин И.М. Email:
[email protected] Содержание Программная архитектура микропроцессоров семейства Intel x86. Краткое описание. ..................................................................................................................................5 Основные программные характеристики МП 8086. ...........................................................5 Основные программные характеристики МП 80286. .........................................................6 Основные программные характеристики МП 80386. .........................................................7 Адресация в защищённом режиме МП8086 .......................................................................7 Сегменты и схема вычисления виртуального адреса. ......................................................7 Страничная адресация. ........................................................................................................9 Защита памяти. ...................................................................................................................10 Расширение памяти за счёт дискового пространства. ....................................................11 Поддержка многозадачного исполнения кода. Плоская модель памяти. ......................12 Обзор архитектуры операционных систем Windows 95 и Windows NT. ....................13 Обзор архитектуры операционной системы MS Windows 95..........................................13 Виртуальные Машины. .......................................................................................................13 Системные службы. ............................................................................................................13 32-х разрядные приложения Windows (Win32). ................................................................14 16-ти разрядные приложения Windows (Win16). ..............................................................16 Менеджер Конфигурации (Configuration Manager). ..........................................................16 Менеджер виртуальных машин. (Virtual Machine Manager).............................................17 Менеджер Файловой системы (Installable File System Manager). ...................................18 Драйверы устройств............................................................................................................18 Обзор архитектуры MS Windows NT..................................................................................19 Hardware Abstraction Level (HAL). ......................................................................................19 Ядро (Kernel)........................................................................................................................19 Менеджер Объектов (Object Manager). .............................................................................20 Менеджер Процессов (Process Manager)..........................................................................20 Менеджер виртуальной памяти (Virtual Memory Manager) ..............................................20 Модуль Вызова Локальных Процедур (Local Procedure Call Facility). ............................21 Менеджер ввода-вывода (I/O Manager). ...........................................................................21 Монитор защиты (Security reference monitor)....................................................................22 Подсистема Win32...............................................................................................................22 Управление памятью в операционных системах Windows 95 и NT. .........................24 Основные задачи управления памятью. ...........................................................................24 Страничная организация памяти. ......................................................................................25 Структура виртуального адресного пространства Windows 95. .....................................25 Типы памяти. .......................................................................................................................26 Ассоциативный кэш буфер страничного преобразования. .............................................27 Общая память......................................................................................................................27 Технология копирования при записи.................................................................................29 Переключение контекстов памяти. ....................................................................................29 Управление физической памятью в Windows NT. Рабочие наборы страниц (Working Sets). .....................................................................................................................................30 База данных страниц (Page Frame Database). .................................................................30 Детали организации общей памяти в Windows NT. .........................................................32 Функции для управления памятью. ...................................................................................33 Функции управления виртуальной памятью. ....................................................................33 LPVOID VirtualAlloc(...................................................................................................33 BOOL VirtualFree( .........................................................................................................33 Куча (Heap). .........................................................................................................................34 Файлы отображаемые в память (Memory Mapped Files). ................................................35 Функции для организации отображения файлов в память. ............................................36 Создание Отображения файла (File Mapping)..................................................................36 Создание окна (view) в файле отображаемом в память. ................................................38 Деинициализация MMF.......................................................................................................38 Принудительная запись на диск содержимого окон MMF. ..............................................39 Объекты и ссылки................................................................................................................40 Основные операции с объектами. .....................................................................................40 Интерфейс объектов...........................................................................................................40 Типы объектов. ....................................................................................................................41 2
Объекты User.......................................................................................................................41 Объекты GDI. .......................................................................................................................41 Объекты Kernel....................................................................................................................41 Имена объектов Kernel. ......................................................................................................43 Наследование и дублирование ссылок на объекты Kernel............................................43 Многозадачность Windows. ................................................................................................45 Процессы и потоки. .............................................................................................................45 Совместная и вытесняющая мультизадачность. .............................................................46 Приоритеты потоков............................................................................................................46 Классы приоритета..............................................................................................................47 Базовый приоритет потока. ................................................................................................48 Переключение потоков. ......................................................................................................48 Блокировка потоков.............................................................................................................49 Динамическое повышение приоритета (Priority boosts)...................................................49 Инверсия приоритета (Priority inversion). ..........................................................................49 Системы с несколькими процессорами.............................................................................50 Создание потоков................................................................................................................51 Завершение работы потоков..............................................................................................53 Создание потоков в Visual C++ с использованием библиотеки классов Microsoft Foundation Classes (MFC). ..................................................................................................53 Создание потоков в Delphi с помощью класса TThread...................................................53 Псевдопотоки (Fibers). ........................................................................................................54 Локальные переменные потоков. ......................................................................................54 Синхронизация потоков. .....................................................................................................55 Объекты синхронизации.....................................................................................................56 Индикаторы событий (Events). ...........................................................................................56 Взаимоисключения (Mutexеs). ...........................................................................................58 Семафоры (Semaphores)....................................................................................................59 Ждущие таймеры (Waitable timers)....................................................................................61 Критические секции (Critical sections)................................................................................61 Функции ожидания...............................................................................................................62 Потоки и графический интерфейс. ....................................................................................65 Тупиковые ситуации (Deadlocks). ......................................................................................67 Синхронный и асинхронный Ввод/вывод..........................................................................68 Создание дочерних процессов. Запуск приложений. ......................................................70 Динамические библиотеки..................................................................................................73 Динамическая компоновка. ................................................................................................74 Импорт функций. .................................................................................................................74 Экспорт функций. ................................................................................................................76 Прямая компоновка (Explicit loading). ................................................................................77 Косвенная компоновка (Implicit loading).............................................................................78 Процедура создания динамической библиотки. ..............................................................80 Имена функций C и C++. ....................................................................................................81 Точка входа DLL. .................................................................................................................82 Использование Локальной Области Потока (Thread Local Storage) в динамических библиотеках. ........................................................................................................................82 Реестр Windows.....................................................................................................................85 Структура реестра Windows. ..............................................................................................86 Корневые ключи (Root Keys). .............................................................................................87 Функции API для работы с реестром.................................................................................88 Пример работы с реестром на Delphi................................................................................90 Файлы реестра. ...................................................................................................................91 Оболочка Windows (Shell). .................................................................................................92 Связь документов и приложений. ......................................................................................92 Запуск приложений в процессе загрузки операционной системы. .................................94 Размещение иконки в поле статуса Панели задач. .........................................................94 Приложения Панели Управления (Control Panel Applets)................................................97 Основы проектирования Виртуальных Устройств Windows 95 ...............................102 Виртуальные Машины и Менеджер Виртуальных Машин (VMM). ...............................103 Стандартные виртуальные устройства...........................................................................104 Программная архитектура виртуальных устройств. ......................................................105 Сегменты виртуальных устройств. ..................................................................................105 Блок описания виртуального устройства. .......................................................................106 Процедура обработки команд (Device control procedure). .............................................107 Системные сообщения. ....................................................................................................108 3
Системные сообщения (примеры)...................................................................................108 Интерфейсы виртуальных устройств. .............................................................................109 Интерфейс с приложениями Win32. ................................................................................109 Интерфейс приложений Win32. Пользовательский уровень. .......................................109 Интерфейс приложений Win32. Системный уровень. ...................................................111 Интерфейсы приложений Win16 и MS-DOS. ..................................................................113 Сервисы VxD (Services). ...................................................................................................115 Базовая структура исходного текста VxD. ......................................................................116 Динамически загружаемые виртуальные устройства....................................................116 Разработка VxD на языке Си............................................................................................117 Вызов сервисов виртуального устройства на языке Си. ...............................................118 Реализация функций вызываемых извне (callback functions) на языке Си..................119 Пример 1. Обработка аппаратных прерываний. ............................................................120 Пример 2. Управление контроллером ПДП. ...................................................................122 Пример 3. Асинхронные сообщения................................................................................125 Сервисы Shell. ...................................................................................................................125 Асинхронный ввод/вывод. ................................................................................................125 Основы проектирования драйверов WDM для Windows 98/2K.................................129 Средства проектирования драйверов WDM...................................................................130 Использование оболочки Microsoft Visual C++ для создания драйверов. ...................131 Регистрация драйверов WDM..........................................................................................131 Организация обмена данными. Пакеты запросов ввода-вывода.................................133 Точка входа драйвера WDM.............................................................................................134 Устройства. Расширения и cимволические ссылки. ......................................................135 Процедура создания объекта устройства.......................................................................137 Типы объектов устройств. ................................................................................................139 Интерфейсы устройств. ....................................................................................................140 Обработка пакетов запросов ввода-вывода (IRP). ........................................................143 Синхронная обработка IRP. .............................................................................................144 Асинхронная обработка IRP.............................................................................................145 Передача IRP объекту устройства расположенного следующим в стеке....................146 Отмена асинхронно исполняющихся запросов. .............................................................150 Основные типы IRP...........................................................................................................152 Формат кода команды DEVICE_CONTROL.....................................................................153 Методы передачи данных. ...............................................................................................154 Метод буферизации (BUFFERED)...................................................................................155 Прямой метод передачи данных (DIRECT). ...................................................................156 Простой метод передачи данных (NEITHER). ................................................................156 Уровни приоритетов..........................................................................................................157 Поддержка механизма Plug and Play в драйверах WDM...............................................157 Пакеты запросов PnP и стек объектов устройств. .........................................................160 Управление аппаратными ресурсами. Обработка IRP_MN_START_DЕVICE.............161
4
Программная описание.
архитектура микропроцессоров семейства
Intel x86.
Краткое
Автор: Сидякин И.М. Московский Государственный Технический Университет им. Н.Э. Баумана. Кафедра ИУ-3, (11.1998) Email:
[email protected] В этой главе даётся представление об основных элементах программной архитектуры микропроцессоров семейства x86. Системное программное обеспечение работает в тесном взаимодействии с аппаратурой. Основные принципы, на которых строится современная операционная система, такие как мультизадачность, управление памятью, защита данных, являются развитием свойств аппаратуры. Знание основ работы аппаратуры необходимо разработчику программного обеспечения. Информация, которая приводится в этой главе, послужит основой для последующего изложения принципов работы системного программного обеспечения. Основные программные характеристики МП 8086. Первое, и наиболее простое устройство из серии процессоров х86, которое мы рассмотрим это МП 8086. Старые операционные системы, такие как, например, DOS, разрабатывались в основном в расчёте на этот микропроцессор. Основные особенности программной архитектуры МП 8086 приведены в таблице.
Регистры данных. Индексные регистры и регистры указателей. Сегментные регистры. Разрядность шины данных Разрядность шины адреса Адресуемый диапазон памяти
16(8/8) бит: AX (AH/AL), BX(BH/BL), CX(CH/CL), DX(DH/DL) 16 бит: SI, DI , BP, SP, IP 16 бит: CS, DS, SS, ES 16 бит. 20 бит. 1 МБ.
Для доступа к памяти это устройство использует так называемую сегментную схему адресации. Физический адрес вычисляется по двум заданным 16-ти разрядным значениям: СМЕЩЕНИЕ и СЕГМЕНТ. Эти значения МП извлекает из кода команды. Они могут быть определены косвенно или прямо в аргументах команды. Значение сегмента всегда извлекается из сегментного регистра. Имя сегментного регистра определяется кодом команды. В случае если имя регистра не определено в исходном тексте команды, используется ряд соглашений (см. ниже примеры). СМЕЩЕНИЕ может располагаться в одном из регистров данных, указателей или индексных регистров (пример.1), прямо прописываться в аргументах команды (пример.2), или вычислятся (пример. 3). Ex.1 Ex.2
Mov Mov
[SI], AX ES:[555h], AX
Ex.3
Mov
[BP-2], AX
; сегмент DS, смещение в SI ; сегмент ES, смещение равно 555 ;(hexadecimal) ; сегмент SS, смещение равно ;содержимому BP минус 2.
МП вычисляет физический адрес (PHA) по следующей простой формуле: PHA = (СЕГМЕНТ * 16) + СМЕЩЕНИЕ; Содержимое 16-ти разрядного сегментного регистра умножается на 16 ( (или, что тоже самое, сдвигается влево на 4 разряда), затем к полученному 20 разрядному значению прибавляется 16-ти разрядное смещение.
5
МП 8086 имеет 20 разрядную шину адреса, что ограничивает адресуемое пространство 20 одним мегабайтом (2 -1 максимальное значение адреса). Рассмотренная схема адресации позволяет вычислить любой адрес в пределах 1 МБ. Архитектура МП 8086 имеет ряд особенностей. Среди прочих следует отметить: отсутствие аппаратной поддержки защиты памяти. Это означает, что любая пользовательская или системная программа имеет доступ ко всей физически адресуемой памяти без ограничений. Часто это служит причиной сбоев. Относительно небольшой диапазон адресного пространства. Современное программное обеспечение требует для работы, как правило, гораздо больше одного мегабайта памяти. Перечисленные выше характеристики МП 8086 определяют сферу применения этого устройства, например, в качестве микроконтроллера в измерительной системе или системе управления. Но приложения такого типа здесь обсуждаться не будут. Основные программные характеристики МП 80286. В MП 80268 реализован новый защищённый режим работы. Этот МП может работать в двух режимах. В защищённом режиме доступны новые свойства, к которым относится отличное от схемы СЕГМЕНТ:СМЕЩЕНИЕ управление памятью, включая и защиту памяти. В то же время МП 80286 может работать как МП 8086. Этот режим называется реальным. В таблице приведены основные характеристики микропроцессора 80286.
Регистры Шина данных Шина адреса Адресуемый диапазон памяти Защита памяти
Реальный режим Защищённый режим Те же 16 разрядные регистры МП 8086 16 бит 24 бита до 1MБ + 64КБ - 16 байт до 16 МБ Не возможна
Возможна
Он имеет тот же, что и МП 8086 набор 16-ти разрядных регистров и 16-ти разрядную шину данных. Но шина адреса расширена до 24 разрядов, что позволяет адресоваться 24 к 2 (= 16МБ) внешней памяти. К сожалению, эту возможность нельзя использовать в реальном режиме потому, что изложенная выше схема вычисления адреса работает в пределах немного превышающих 1МБ. Шина адреса МП 8086 имеет 20 линий, что 20 ограничивает максимальный физический адрес значением 2 -1. МП 80286 имеет дополнительные линии шина адреса, что позволяет, хотя и незначительно, увеличить адресное пространство доступное программам работающим в реальном режиме. Определим максимальное значение адреса, которое может быть получено схемой сегментной адресации принятой в МП 8086 и реальных режимах следующих моделей 16 семейства. Подставив максимальные (2 = 0xFFFF) значения сегмента и смещения в формулу для вычисления физического адреса получим PHA = (СЕГМЕНТ * 16) + СМЕЩЕНИЕ = 0xFFFF*16 + 0xFFFF = 0xFFFF0 + 0xFFFF = 0x10FFEF. «1 мегабайт плюс 64 килобайта минус 16 байт». Таким образом, используя дополнительно 21-ю линию шины адреса, можно расширить адресное пространство процессора в реальном режиме примерно на на 64 килобайта. Приведённые расчеты определяют предел схемы вычисления адреса в реальном режиме. Эти дополнительные 64К – 16 байт называются верхней областью памяти (High Memory Area /HMA/) и используются, как правило, операционной системой (DOS) для размещения драйверов. HMA используется в реальном режиме всех микропроцессоров семейства, начиная с МП 80286. В защищённом режиме применяется другой метод адресации, который позволяет адресоваться к памяти в полном диапазоне 0 – 16МБ. Этот метод получил развитие в последующих моделях МП x86. Адресное пространство было увеличено до четырёх гигабайт. Более подробно схема работы процессора в защищённом режиме описана в следующем параграфе, который посвящён МП80386. 6
Основные программные характеристики МП 80386. Этот микропроцессор является базовой моделью, на основе которой были разработаны последующие МП компании Intel включая МП 80486 и Pentium. Несмотря на значительные усовершенствования, отличающие последние модели микропроцессоров, существуют основные принципы архитектуры, которые их объединяют. Архитектура МП80386 построена на классических концепциях, которые операционные системы используют для управления памятью и процессом исполнения программ. Основные программные характеристики процессора приводятся в таблице:
Режимы работы Регистры общего назначения Сегментные регистры Шина данных Шина адреса Адресуемый диапазон памяти Основные свойства защищённого режима:
Реальный, Защищённый, Режим виртуального MП8086 (V86) 32-х разрядные. (доступны) 16-ти разрядные (используются по разному в различных режимах) 32 бита 32 бита До 4-х ГБ в защищённом режиме Защита памяти, Мультизадачность, Управление виртуальной памятью.
Это устройство может работать в трёх различных режимах. Реальный режим. Регистры общего назначения процессора расширены до 32-х бит. Например регистр 16-ти разрядный регистр Aх составляет младшее слово 32-х разрядного регистра Eax. 32-х разрядные регистры могут использоваться в реальном режиме в логических и арифметических операциях. Однако для формирования смещения адреса в реальном режиме используются только младшие 16 разрядов этих регистров. Сегментные регистры этого МП так же 16-ти разрядные.
Режим виртуального МП8086 (V86). В этом режиме процессор эмулирует работу МП8086. Одновременно может быть запущено несколько виртуальных машин V86 которые разделяют процессорное время.
Защищённый режим. Этот режим работы процессора является основным. В защищённом режиме адресуемая память расширяется до 4ГБ. Появляется новые мощные средства управления памятью и псевдопараллельным исполнением программ. Управление памятью эта одна из наиболее важных проблем, которые операционная система решает в тесном взаимодействии с аппаратурой.
Адресация в защищённом режиме МП8086 В защищённом режиме для управления памятью микропроцессор использует дополнительные «внешние» данные. Эти данные располагаются в памяти. Поэтому процессоры Intel начиная с 808386, после включения питания запускаются в реальном, а не в защищённом режиме. Системное программное обеспечение перед переключением МП в защищённый режим размещает в памяти и инициализирует все необходимые структуры данных. Ссылки на эти структуры помещаются в специальные служебные регистры МП. После этих подготовительных операций МП переключается в защищённый режим. Сегменты и схема вычисления виртуального адреса. С «точки зрения» процессора память это набор блоков, которые называются сегментами. Следует отметить, что в реальном режиме СЕГМЕНТом называется параметр в формуле вычисления адреса. В защищённом режиме термин сегмент просто имеет другой смысл. Сегменты могут размещаться в любом месте 32-х разрядного адресного пространства процессора и могут иметь размеры до 4ГБ. Диапазоны адресов сегментов могут перекрываться. Характеристики каждого сегмента описываются в структуре, которая называется дескриптор сегмента. Дескриптор содержит начальный адрес сегмента, размер (limit), информацию о типе, уровне привилегий и других атрибутах сегмента. Упрощённый формат дескриптора приведён на рисунке 1. Следует отметить, что дескрипторы используются не только для описания сегментов содержащих код, данные или стеки программ. Некоторые системные структуры, как например, локальные дескрипторные таблицы и шлюзы так же 7
описываются дескрипторами. Форматы дескрипторов различных типов могут отличаться. 31 Base 31..24
G x 0
20 x
16 Limit 19..16
Base 15..0 Base Limit G Type DPL P
8 P DPL
Type
0 Base 23..16
Limit 15..0
32-х битовый базовый адрес сегмента. Размер сегмента в байтах или страницах (зависит от значения бита G). Гранулярность. Если G = 0 Limit указан в байтах, иначе в страницах (Размер страницы 4096 байт). тип дескриптора. Уровень привилегий дескриптора. бит присутствия.
Рисунок 1. Формат дескриптора. Дескрипторы размешаются во внешней памяти в нескольких системных таблицах. Операционная система должна создать и проинициализировать некоторые из этих таблиц перед переключением МП в защищённый режим. ОС, по крайней мере, должна создать Глобальную дескрипторную таблицу (GDT) с одним или несколькими дескрипторами сегментов содержащих системный код, данные и стек. Кроме этого следует создать Дескрипторную таблицу прерываний (IDT) которая содержит дескрипторы обработчиков прерываний. Во время работы МП в защищённом режиме дескрипторы могут модифицироваться удаляться и добавляться. Обычно операционная система создаёт одну таблицу GDT и одну таблицу IDT. Дополнительно система может создать одну или несколько Локальных дескрипторных таблиц (LDT). Каждая такая таблица выделяется для дескрипторов сегментов отдельной пользовательской программы. Такой способ используется для защиты программ друг от друга. Адреса и размеры таблиц записываются в служебные регистры микропроцессора. В регистр GDTR заносится адрес GDT, в регистр IDTR адрес IDT и в регистр LDTR адрес текущей LDT. Содержимое регистра LDTR изменяется каждый раз, когда операционная система выделяет время для исполнения очередной программы. Сегментные регистры процессора используются для ссылки на дескриптор в одной из дескрипторных таблиц. Содержимое сегментного регистра называется селектором. Селектор содержит номер дескриптора в таблице и флаг, по которому определяется в какой из таблиц LDT или GDT расположен дескриптор. Кроме этого в селекторе указываются атрибуты защиты. 13 старших разрядов занимает номер дескриптора. 13 Следовательно, каждая таблица может содержать до 2 = 8K дескрипторов. Бит 2 (считая с нуля) определяет таблицу, и два младших бита используются в механизме защиты памяти (Рисунок 2). 15 Index Index TI RPL
2 TI
0 RPL
номер дескриптора в таблице. выбор таблицы. Если TI = 0 используется GDT иначе LDT. запрашиваемый уровень привилегий.
Рисунок 2. Формат селектора. При обращении к сегменту памяти процессор дополнительно должен считать содержимое дескриптора этого сегмента. Такой способ обмена данными с памятью может существенно снизить производительность работы системы. Однако в действительности при обращении к памяти чтение дескриптора не производится. Микропроцессор имеет так называемые теневые сегментные регистры, в которых сохраняются дескрипторы сегментов. Для каждого сегментного регистра имеется теневой регистр. Теневой регистр содержит дескриптор, на который указывает селектор, записанный в сегментном регистре. Дескриптор записывается в теневой регистр, при изменении содержимого сегментного регистра. Эта технология особенно эффективна при работе с так называемой плоской моделью памяти. В такой модели используется единственный сегмент, перекрывающий всё адресное пространство. Так как вся память доступна через этот единственный сегмент, во время работы нет необходимости изменять значение сегментных регистров. 8
Рассмотрим процедуру вычисления адреса в защищённом режиме на примере исполнения инструкции чтения памяти MOV AL, [EDI]. Эта инструкция считывает в регистр AL байт из ячейки памяти, адрес которой вычисляется по схеме изображённой на рисунке 3.
ПАМЯТЬ Считать это в AL
Mov AL, [EDI] GDT (или LDT)
Дескриптор
+
GDTR ( или LDTR) База и размер таблицы Регистр DS. Index
Процессор
Теневой регистр DS Base Limit
Др. атрибуты
EDI +
СМЕЩЕНИЕ
ВИРТУАЛЬНЫЙ АДРЕС Рисунок 3. Схема вычисления виртуального адреса в защищённом режиме. Страничная адресация. Физический адрес, это значение, которое выставляется на шину адреса процессора. Схема представленная на рисунке 3 используется для вычисления так называемого виртуального адреса. Процессор имеет встроенный механизм преобразования виртуального адреса в физический. Значения этих адресов могут не совпадать. Этот механизм называется трансляция страниц. Трансляция страниц разрешается программно установкой бита PG(31) в служебном регистре процессора CR0. Если трансляция страниц запрещена, физический адрес всегда равен виртуальному. Трансляция страниц позволяет изменить 20 старших бит виртуального адреса перед выдачей его на шину. Таблица перекодировки виртуальных адресов в физические задаётся программно. Это важное свойство аппаратуры используется операционной системой для решения ряда важнейших задач. Управление физической памятью может быть скрыто от пользовательских программ. Каждая пользовательская программа работает с линейным непрерывным диапазоном виртуальных адресов, в то время как физическую основу этого диапазона формирует операционная система. Физически данные могут быть разделены на фрагменты, которые располагаться в любом порядке в оперативной памяти или на диске. Эта гибкость в размещении данных позволяет операционной системе оптимизировать работу с памятью. Для использования механизма трансляции станиц операционная система должна разместить в памяти специальные таблицы перекодировки адресов: каталог таблиц страниц (Page Directory (PD)) и таблицы страниц (Page Tables(PT)). Форматы записей каталога и таблиц страниц приведены на рисунке 4. Схема трансляции адресов приведена на рисунке 5. Начальные адреса таблиц выровнены по границе 4К. Размер одой таблицы не может больше 4К. Следовательно, каждая таблица может содержать до 1024 32-х разрядных записей. Двухуровневая схема 9
трансляции адресов (см. рисунок 5) позволяет охватить 1024 (записи в PD)*1024(записи в PT)*4096(размер страницы) = 4GB адресов. 31 Физический адрес страницы (или таблицы станиц для каталога)
11 xx00
D
A
00
U/S
R/W
Физический адрес измеряется в страницах, т.о. для адресации к любой станице в пределах 4GB достаточно 20 бит. A бит доступа. МПУ устанавливает этот бит перед записью или чтением из страницы. D МПУ устанавливает этот бит (только в записях таблиц страниц) перед записью в страницу. U/S пользователь/супервизор. Определяет уровень привелегий страницы. Используется для защиты страниц. R/W чтение/запись. Ограничивает доступ к странице чтением. P бит присутствия. Указывает на то, что страница присутствует в физической памяти. Рисунок 4. Формат записи таблицы страниц/каталога страниц (PTE/PDE).
ВИРТУАЛЬНЫЙ АДРЕС 31 номер таблицы в каталоге
+
21 номер страницы в таблице
11 смещение внутри 4к страницы.
0
Регистр CR3 физический адрес каталога
каталог
Таблица страниц +
адрес страницы
+
адрес таблицы страниц
Страница (4096 байт)
ФИЗИЧЕСКИЙ АДРЕС
Рисунок 5. Трансляция виртуального адреса в физический. Адресное пространство процессора разбивается на блоки размером 4096 байт. Эти блоки называются страницами. Процессор может отобразить любую страницу из виртуального диапазона адресов в физический. Операционная система управляет трансляцией страниц, модифицируя записи каталога и таблиц страниц. С помощью этого механизма решаются следующие основные задачи управления памятью. Защита памяти. МП 80386 обеспечивает защиту памяти на уровне сегментов и на уровне страниц. Это разные методы защиты. Защита на уровне сегментов основана на использовании битов поля DPL дескриптора сегмента. Значение этого поля определяет уровень приоритета 10
0 P
сегмента: 0 – самый высокий уровень (сегмент «наиболее защищён»), 1, 2 и 3. Эти уровни так же называются кольцами защиты. Основное правило защиты заключается в том, что код расположенный в сегменте с более низким уровнем приоритета не может получить доступ к данным (или коду), расположенным в сегменте с более высоким приоритетом. Пользовательские программы размещаются в сегментах с DLP > 0. Системные структуры, включая дескрипторные таблицы и таблицы страниц, располагаются в сегментах с DLP = 0. Защищённый код операционной системы так же располагается в сегментах с наивысшим уровнем привилегий. Этот код может модифицировать системные структуры и содержать привилегированные инструкции МП, которые используются для изменения содержимого служебных регистров. Пользовательское программное обеспечение может получить доступ к защищённым данным и коду, только под контролем операционной системы. Защита на уровне сегментов использует четыре уровня приоритетов. В механизме защиты памяти на уровне страниц применяется только два уровня. Для защиты страниц используются биты U/S и R/W записей таблиц страниц. Если бит U/S сброшен, страница относится к классу supervisor. Такая страница доступна только из 0-го кольца защиты. Если значение бита R/W равно 0, страница из любого кольца доступна только для чтения. В 32-х разрядных ОС Windows применяется двухуровневая схема защиты. Пользовательские приложения Win32 размещаются в сегментах (DPL = 3) покрывающих всё адресное пространство процессора. Однако, страницы в которых размещаются системные данные и код защищены от пользовательских приложений (U/S = 0). К этим страницам может обращаться только системное ПО размещённое в сегментах с DPL = 3. Сегменты с уровнями привилегий DPL = 1 и DPL = 2 не используются. Расширение памяти за счёт дискового пространства. Оперативная память вычислительной системы, как правило, занимает незначительную часть адресного пространства. Однако современное программное обеспечение, например, системы управления базами данных или обработки изображений требует для нормальной работы значительных объемов памяти. В мультизадачной операционной системе память расходуется на все параллельно работающие приложения. Объем памяти может быть существенно увеличен с помощью механизма трансляции страниц. В оперативной памяти можно располагать только необходимые в данный момент страницы. Остальные страницы могут быть временно сохранены на жёстком диске. Эта выгрузка страниц на диск производится операционной системой, в случае если ресурсы оперативной памяти исчерпаны. Бит присутствия (P) в записях таблиц станиц (PTE) используется для организации загрузки страницы с диска в оперативную память по запросу. Если страница находится в физической памяти бит P установлен в единицу и запись PTE содержит адрес страницы в памяти. Если страница находится на диске, бит P сброшен, и запись PTE содержит ссылку на месторасположения страницы на диске, вместо её адреса . При попытке обращения к не присутствующей в оперативной памяти странице процессор вырабатывает исключение (page fault exception). Исключение приводит к передаче управления по заранее определённому операционной системой адресу. Вызывается программа обработчик исключения. Обработчик загружает страницу в оперативную память по какому либо физическому адресу и корректирует запись в таблице страниц. После этого обработчик возвращает управление и операция, которая привела к исключению, повторяется. Операционная система также отвечает за выгрузку страниц на диск для освобождения памяти. При этом каждый раз выбирается страница, обращение к которой производилось наиболее давно. Схема загрузки страниц по запросу приводится на рисунке 6.
11
Данные из страницы Физическая память Страница
Таблица страниц запись PTE
Страница присутствует в памяти
Аппаратура управления виртуальной памятью
ЧТЕНИЕ ПАМЯТИ
Аппаратное исключение. (страница не присутствует в памяти) Системное ПО загружает страницу в память и модифицирует запись PTE.
Рисунок 6. Обработка исключения при обращении к памяти. Поддержка многозадачного исполнения кода. Плоская модель памяти. Процесс трансляции адресов управляется программно. Операционная система может модифицировать запись таблицы страниц, для изменения физического адреса одной страницы,, модифицировать запись каталога таблиц страниц изменив сразу же адреса 1024 страниц или полностью изменить структуру перекодировки адресов записав новое значение в регистр CR3. Корректировка таблиц используется для поддержки псевдопараллельного исполнения программ в операционных системах MS Windows. Код и данные 32-х разрядных пользовательских приложений Windows размещается в сегментах, размер которых равен 4ГБ. Эти сегменты перекрывают друг друга и покрывают всё адресное пространство процессора. Реально приложения могут использовать только часть страниц этих сегментов. В Windows 95, например, приложению выделяется область виртуальных адресов в диапазоне от 4МВ до 2ГБ. Каждое приложение имеет своё собственное виртуальное пространство. Это означает, что одинаковые виртуальные адреса двух разных приложений проецируются в разные физические. В виртуальном адресном пространстве приложения Win32 как бы наложены друг на друга. Однако для каждого приложения операционная система создаёт отдельную структуру таблиц страниц. Приложения Windows 95 исполняются псевдопараллельно. Операционная система выделяет каждому приложению небольшие интервалы процессорного времени. Когда время очередного интервала истекает, система передаёт управление очередному приложению. В этот момент происходит модификация структуры таблиц страниц.
12
Обзор архитектуры операционных систем Windows 95 и Windows NT. Автор: Сидякин И.М. Московский Государственный Технический Университет им. Н.Э. Баумана. Кафедра ИУ-3, (11.1998) Email:
[email protected] Обзор архитектуры операционной системы MS Windows 95. Общая структура операционной системы Windows 95 (Чикаго) приведена на рисунке 1. На этой схеме компоненты операционной системы разделены на две группы: пользовательские компоненты (user mode components) и компоненты ядра (kernel mode components). Компоненты ядра ОС защищены от пользовательских, так как они размещаются в памяти в нулевом кольце защиты а пользовательские в третьем кольце защиты. Системная виртуальная машина
Компоненты третьего кольца защиты RING-3
Компоненты нулевого кольца защиты RING-0
Приложения Win 32
Приложения Win 16
MS DOS MS DOS virtual Виртуальная virtual machine машина machine MS DOS
Системные службы: 16 & 32 бит. KERNEL, USER, GDI
Модуль управления виртуальными машинами VMM
Модуль управления файловой системой IFSM
Модуль управления конфигурацией. (Configuration Manager.)
Драйверы устройств Рисунок 1. Windows 95. Виртуальные Машины. По определению, которое приводится в документации MS Win 95 Resource Kit Виртуальная Машина, это объект, размещаемый в памяти, который с точки зрения программных приложений, выглядит как отдельный компьютер, с полным набором ресурсов необходимых физическому компьютеру для запуска и обслуживания программных приложений. Windows 95 создаёт одну Системную виртуальную машину для всех приложений Windows и системных служб и отдельные виртуальные машины для обслуживания каждого сеанса MS-DOS. Это свойство поддерживается на уровне аппаратуры (МП 80386) которая позволяет запускать несколько виртуальных машин МП 8086. Системная ВМ работает в защищённом режиме МП 80386, а виртуальные машины V86 в режиме виртуального МП 8086. Системные службы. Системные службы, это часть системного ПО, расположенная в третьем кольце защиты. Системные службы обеспечивают программный интерфейс операционной системы и пользовательских приложений и выполняют ряд служебных функций.. Системные службы разделяются на три основных модуля, которые показаны на рисунке 2. В Чикаго каждый модуль реализован в виде пары динамических библиотек 16-ти и 32-х разрядной.
13
GDI графические функции, печать. User интерфейс с устройствами ввода например мышь и клавиатура, пользовательский интерфейс (окна, меню), таймер, звуковые карты и коммуникационные порты.
Kernel управление виртуальной памятью, переключение задач, файловый ввод-вывод, обработка исключений и т.д.
USER16 - USER32, GDI16-GDI32, KERNEL16-KERNEL32 Рисунок 2. Системные службы. 16-ти разрядный код используется для обеспечения совместимости с приложениями Win16. Но 32-х разрядные приложения так же используют 16-ти разрядный системный код. Таким образом, нельзя утверждать, что Чикаго полностью 32-х разрядная операционная система. Системные службы содержат значительный набор функций, которые доступны пользовательским приложениям. Этот набор функций называется API (Application Programming Interface) - Программный интерфейс приложения. Важно отметить, что системные динамические библиотеки тесно взаимодействуют с компонентами нулевого кольца. 32-х разрядные приложения Windows (Win32). Windows может управлять одновременно одним или несколькими 32-х разрядными приложениями. Каждое приложение рассматривается операционной системой как отдельный процесс. (Часто, хотя и не вполне оправданно термины приложение и процесс используются как синонимы) Процесс может исполняться внутри одной виртуальной машины. Каждый процесс Win32 может работать с виртуальным адресным пространством размером 4ГБ. Каждому процессу выделяется частное адресное пространство для хранения его кода и данных размером до 2ГБ. Приложения Win32 не могут «видеть» друг друга в памяти, так как в этом частном диапазоне, для разных приложений (процессов), система обеспечивает различное отображение логических адресов в физические (рисунок 3). Оставшаяся половина адресов используется операционной системой. Виртуальные адреса в этом диапазоне частично защищены от пользовательских программ (компонентов третьего кольца защиты). Эти адреса в основном глобальные, т.е. отображаются в физические одинаково для всех процессов.
14
Виртуальная память
Физическая память
Процесс 1.
Системная память (2GB) Память выделенная процессу (2GB) Прцесс 2.
Системная память (2GB) Память выделенная процессу (2GB)
Трансляция страниц
Свободная страница 1 Система, страница 4 Процесс 2, страница 3 Процесс 1, страница 2 Процесс 2, страница 4 Система, страница 1 Процесс 1, страница 1 Свободная страница 2 Процесс 1, страница 4 Система, страница 5 Система, страница 3 Процесс 2, страница 1 Свободная страница 5 Процесс 2, страница 2 Процесс 2, страница 5 Свободная страница 4 Процесс 1, страница 3 Свободная страница 3 Система, страница 2
Рисунок 3. Отображение виртуальной памяти в физическую. Каждое приложение Win32 имеет свою собственную очередь сообщений (рисунок 4). Это означает, что обработка сообщений в одном из приложений Win 32 запущенных в системе не оказывает влияния на работу других приложений. Эта схема обработки сообщений называется Асинхронный ввод (Desynchronized Input). Системные сообщения и сообщения от приложений
Входная очередь сообщений (сообщения от аппаратных устройств)
очередь сообщений приложения Win 32
очередь сообщений приложения Win 32
Приложение Win32
Приложение Win32
Рисунок 4. Каждое Win32 приложение имеет свою очередь сообщений (Десинхронизированный ввод).
15
16-ти разрядные приложения Windows (Win16). 16-ти разрядный код в Windows 95 поддерживается для совместимости с программным обеспечением разработанным для версий Windows 3.x. Кроме того, как было отмечено выше, часть системных служб ОС реализована в виде 16-разрядных динамических библиотек. 16-ти разрядный код и данные размещаются в общей (shared) области памяти адресного пространства. Это означает, что приложения Win16 не могут быть скрыты друг от друга в памяти как 32-х разрядные приложения. Они размещаются в верхних двух гигабайтах виртуального адресного пространства. Код и данные приложения Win16 теоретически доступны другим приложениям Win16 и Win32. Разумеется, приложения Win 16 не используют плоскую модель памяти с сегментами размером 4 гигабайта. Вместо этого каждому приложению Win16 выделяются несколько небольших сегментов (размером до 64K) для размещения кода стека и данных. Эти сегменты, как уже было отмечено, располагаются в верхних 2ГБ виртуального адресного пространства (точнее между вторым и третьим гигабайтом). Все приложения Win16 используют единственную очередь сообщений. (Рисунок 5). В этой схеме, во время, когда одно из приложений Win16 обрабатывает сообщение, извлечённое из общей очереди, другие приложения находятся в режиме ожидания. Этот метод называется синхронным вводом (Synchronized Input). Системные сообщения и сообщения от приложений
Входная очередь сообщений (сообщения от аппаратных устройств
Общая очередь сообщений Win16
БЛОКИРОВАНО Приложение Win16
БЛОКИРОВАНО
Приложение Win16
Приложение Win16
Рисунок 5. Все приложения Win16 совместно используют одну очередь сообщений. (Синхронизированный ввод) Менеджер Конфигурации (Configuration Manager). Менеджер конфигурации, это модуль, в задачу которого входит поддержка процедуры автоматического определения аппаратной конфигурации системы (Plug and Play). Модуль управляет процессом обнаружения аппаратных устройств и выделения им необходимых для работы системных ресурсов. Менеджер конфигурации взаимодействует с драйверами устройств, нумераторами (enumerator) и арбитраторами (arbitrator). Специальные системные драйверы, так называемые нумераторы, используются для обнаружения аппаратных устройств и определения системных ресурсов которые требуются каждому устройству для функционирования. С помощью нумераторов создаётся древовидная структура данных (Hardware Tree) в которой описывается всё обнаруженное оборудование. Каждое устройство в этой структуре представлено в этом дереве отдельным листом, который располагается в одной из ветвей. Ветви представляют отдельные шины, к которым могут быть подключены устройства (шина ISA, шина PCI, шина SCSI, шина USB, BIOS, MONITOR). Шина имеет 16
более широкое понятие. Так, например, BIOS в данном контексте считается шиной, к которой подключены стандартные системные устройства (контроллер прерываний и т.п.). Каждый системный ресурс (память, адресное пространство портов ввода вывода, аппаратные прерывания, и каналы ПДП) распределяется между устройствами соответствующими системными драйверами - арбитраторами.
Менеджер конфигурации
Enumerator Нумераторы
Enumerator Арбитраторы
Драйверы Enumerator устройств
Рисунок 6. Модуль управления конфигурацией (Configuration Manager). Менеджер виртуальных машин. (Virtual Machine Manager). Менеджер виртуальных машин (VMM) это основной компонент ядра (нулевого кольца) операционной системы. В его задачу входит создание и обслуживание виртуальных машин. Напомню, что все приложения Win32 и Win16 запускаются внутри одной системной виртуальной машины, плюс каждый сеанс MS-DOS запускается в отдельной виртуальной машине МП 8086. VMM выполняет следующие три основные задачи: распределение процессорного времени между исполняемыми программными модулями. управление памятью поддержка приложений MS-DOS. Мультизадачность одно из основных свойств операционной системы. Windows 95 поддерживает две схемы организации мультизадачности. Совместная мультизадачность (Cooperative Multitasking), применяется для обеспечения совместимости с приложениями, разработанными для Windows 3.x. Эта модель используется приложениями Win16, и предполагает, что переключение задач зависит от проверки общей очереди сообщений. Если какое либо приложение перестаёт выполнять эту операцию (например, в результате зависания) остальные 16-ти разрядные программы не смогут получить управление. Этот метод, следовательно, легко приводит к сбою всей системы при возникновении ошибки в одном из исполняющихся приложений. Важной особенностью данного способа является, то, что можно быть вполне уверенным в том, что операционная система не передаст управление другой задаче во время обработки очередного сообщения, но только после окончания обработки. Приложения Win32 используют другой метод, который называется Вытесняющая мультизадачность (Preemptive Multitasking). Этот метод не зависит от обработки сообщений. VMM принимает решение о переключении задач по «внутренним соображениям». Этот процесс конечно можно программно контролировать. Для этого операционная система предоставляет определённый набор средств, которые мы рассмотрим позднее. В вытесняющей мультизадачности вводится термин поток (Thread). Поток определяется как цепочка инструкций МП расположенная в адресном пространстве одного из запущенных процессов. Операционная система, загружая приложение Win32, создаёт новый процесс. Процессу выделяются сегменты размером 4GB и, как обсуждалось выше, частное адресное пространство в пределах младших двух гигабайт. В эти адреса помещается помимо прочего код приложения. Вместе с процессом система создаёт новый поток - первый поток процесса (primary thread) который начинается с первой инструкции программы. Для каждого приложения Win32 автоматически создаётся один процесс, и один поток, но в дальнейшем несколько потоков могут быть созданы в дополнение к первому. Операционная система предоставляет несколько API функций для создания и управления потоками. Поток это элемент в алгоритме распределения процессорного времени. Система периодически выделяет небольшие интервалы процессорного времени для исполнения каждого активного потока. При работе с памятью Чикаго использует так называемую схему загрузки/выгрузки страниц по требованию (Demand paging). Страницы кода и данных приложений и системных модулей могут временно размещаться вне физической памяти - на жёстком 17
диске. Они загружаются в физическую память по требованию, в момент когда к ним производится обращение (чтение или запись). Этот механизм управления виртуальной памятью поддерживается аппаратно. VMM так же обеспечивает интерфейс с ядром системы для программного обеспечения MS-DOS которое использует системные ресурсы. Менеджер Файловой системы (Installable File System Manager). Этот компонент операционной системы контролирует доступ к аппаратуре и системным программным модулям обслуживающим файловую систему. Программное обеспечение входящее в IFS полностью 32-х разрядное. IFS поддерживает файловую систему FAT32 (VFAT), файловую систему CD-ROM (VCDFS) и отвечает за перенаправление данных в сеть. Драйверы устройств. Windows 95 поддерживает различные типы драйверов т.е. программных модулей непосредственно работающих с аппаратурой. Некоторые типы унаследованы из предыдущих версий Windows и DOS. Для некоторых стандартных классов оборудования операционная система включает, так называемые универсальные драйверы (Universal drivers), которые реализуют набор основных и наиболее общих функций по работе с аппаратурой. Примером универсального драйвера является драйвер принтера UNIDRV. В дополнение к этому драйверу для каждой конкретной модели принтера разрабатывается и подключается к универсальному драйверу минидрайвер (Mini-driver) который содержит относительно небольшое количество кода и выполняет функции специфические для данной модели. (рисунок 7). Драйверы такого типа реализуются в виде динамических библиотек и следовательно принадлежат компонентам третьего кольца защиты операционной системы. Другие компоненты ОС Универсальный драйвер Мини-драйвер
Мини-драйвер
Устройство
Устройство
Рисунок 7. Универсальные драйверы и мини-драйверы. Драйверы, расположенные в нулевом кольце защиты называются виртуальными устройствами (Virtual Devices) или сокращённо VxD. Виртуальные устройства могут работать с аппаратурой за некоторым исключением напрямую без посредничества операционной системы. VxD могут использовать функции предоставляемые менеджером виртуальных машин и другими виртуальными устройствами. Этот набор функций отличается от API, который используют пользовательские программы (VxD не может вызывать функции API). В состав операционной системы входит ряд виртуальных устройств, которые управляют стандартной аппаратурой. Например, VPICD.VXD обслуживает программируемый контроллер прерываний, VDMAD.VXD управляет контроллером ПДП. Виртуальное устройство может быть не связано с какой либо аппаратурой. Такие VxD разрабатываются в случае, когда требуется использовать преимущества программного обеспечения расположенного в нулевом кольце защиты. Виртуальные устройства имеют интерфейс с другими VxD, приложениями Win32, Win16 и MS-DOS.
18
Приложения Win32/Win16/MS-DOS Кольцо-3
Кольцо-0 Менеджер виртуальных машин (VMM)
VPICD.VXD
Программируем ый контроллер прерываний
VDMAD.VXD
MyDevice.VXD
Контроллер ПДП
нестандартное устройство
Рисунок 8. Виртуальные устройства (Virtual devices). Обзор архитектуры MS Windows NT. В отличие от Windows 95, операционная система Windows NT разрабатывалась изначально как 32-х разрядная ОС. Однако 16-ти разрядные приложения так же поддерживаются. Windows NT имеет модульную структуру которая придаёт гибкость этой операционной системе. Windows NT может работать на нескольких платформах CISC (complex instruction set computing/80386/) и RISC (reduced instruction set computing/MIPS R4000 и Digital Alpha AXP/). Это свойство называется переносимостью (Portability). Windows NT может работать в системе с одним микропроцессором или в системе с симметричной многопроцессорной архитектурой (до 32-х МП). Это свойство называется Scalability. Модульная структура операционной системы приводится на рисунке 1. Приложения Подсистема Win 32 (subsystem)
Подсистемы OS/2 & POSIX
Кольцо-3 Кольцо-0 I/O Manager
Object Manager
Executive services Security Process Local Reference Manager Procedure Monitor Call Facility Kernel Hardware Abstraction Level (HAL)
Virtual Memory Manager
Аппаратура Рисунок 1. Windows NT. Hardware Abstraction Level (HAL). Модуль HAL скрывает конкретную реализацию аппаратуры от остального программного обеспечения. HAL работает как фильтр обеспечивающий единый программный интерфейс аппаратных устройств. Для каждой платформы разработана своя версия этого модуля. Остальные компоненты операционной системы не требуют модификации при смене платформы. Ядро (Kernel) Kernel размещается в нулевом кольце защиты и всегда в физической памяти. Данные и код модуля не могут быть временно выгружены на диск. Kernel управляет 19
переключением потоков в одно- и многопроцессорной системе. Kernel управляет рядом системных объектов которые используются для организации работы в многозадачном режиме. Эти объекты делятся на два типа: Объекты диспетчеризации, которые используются для синхронизации потоков. К ним относятся объекты синхронизации работы потоков: индикаторы событий, мьютексы, семафоры и таймеры, а также сами объекты потоков. Объекты управления, которые используются для управления работой модуля Kernel. К ним относятся: асинхронные вызовы процедур, прерывания и процессы. Менеджер Объектов (Object Manager). В документации Win32 SDK объект определяется как «внутренняя структура, которая описывает системный ресурс, например файл, поток, или графическое изображение». Программное обеспечение получает доступ к объекту через ссылку на объект (handle). Ссылка это 32-х разрядное число, которое является прямым указателем на объект (виртуальным адресом объекта) или содержит информацию косвенно указывающую на объект. Все ссылки на объекты в Windows NT создаются с помощью Менеджера Объектов. Менеджер Процессов (Process Manager). Процесс это исполняющееся приложение, которому выделяется частное виртуальное адресное пространство. Процесс содержит код, данные и набор ресурсов операционной системы, к которым относятся, например, файлы и объекты синхронизации. Процесс содержит один или несколько потоков. Каждому потоку выделяются интервалы процессорного времени. Поток может включать любой фрагмент кода приложения, в том числе принадлежащий другому потоку. Все потоки одного процесса используют совместно адресное пространство, глобальные переменные и ресурсы операционной системы, выделенные процессу. Менеджер процессов управляет потоками и процессами. В его задачу входит создание и удаление процессов. Он так же предоставляет набор функций по созданию и управлению потоками. Менеджер виртуальной памяти (Virtual Memory Manager) VMM (не путать с Менеджером Виртуальных Машин в Windows 95) реализует функции управления виртуальной памятью. В его задачу входит обеспечение преобразования виртуальных адресов в физические, предоставление виртуального адресного пространства остальному программному обеспечению. VMM работает непосредственно с таблицами страниц. VMM использует упомянутый ранее метод загрузки/выгрузки страниц по запросу (Demand paging). Часть страниц виртуальной памяти хранится на диске и загружается по запросу в физическую память. VMM скрывает этот процесс от остального программного обеспечения. Концепция виртуального адресного пространства приведена на рисунке 2.
Виртуальная память Физ. память (станица присутствует) Неиспользуемая страница Диск (страница не присутствует) Физ. память (станица присутствует) Диск (страница не присутствует)
Диск
... Физическая память
Рисунок 2. Виртуальная память. VMM создаёт для каждого процесса отдельные таблицы страниц, так чтобы одни и те же виртуальные адреса в частном адресном пространстве разных процессов 20
отображались в разные физические адреса. Поэтому процессы становятся невидимыми друг для друга. Это проиллюстрировано на рисунке 3./W95/. Модуль Вызова Локальных Процедур (Local Procedure Call Facility). LPC используется для обеспечения интерфейса приложений и компонентов системы. В задачу этого модуля входит трансляция вызовов API функций из пользовательского приложения в систему. Когда приложение вызывает системную функцию этот вызов преобразуется в сообщение которое пересылается модулю LPC. Операционная система декодирует это сообщение, производит необходимые вызовы системных функций для того, чтобы выполнить требуемую операцию и наконец возвращает результат приложению. Такой механизм, запрещающий вызов системных функций напрямую повышает надёжность системы. Менеджер ввода-вывода (I/O Manager). I/O Manager контролирует все операции ввода-вывода операционной системы. Он обеспечивает единый интерфейс передачи данных между драйверами. Обработка ввода-вывода в операционной системе разделяется на несколько логических уровней. Программное обеспечение на каждом уровне не зависит от деталей реализации ПО на других уровнях. Например, только драйвер самого низкого уровня работает напрямую с аппаратурой и учитывает её особенности. Многоуровневая структура драйверов приводится на рисунке 3.
Менеджер ввода/вывода Менеджер кэш памяти Файловая система Сетевые драйверы Драйверы аппаратуры
Рисунок 3. Многоуровневая архитектура драйверов NT. Это структура позволяет легко заменить драйвер на любом уровне не внося изменения на других уровнях (например, в случае смены файловой системы или замены жёсткого диска). Драйверы обмениваются данными, используя специальную структуру Пакет запроса ввода-вывода (I/O Request packets/IRP/). В задачу Менеджера ввода-вывода входит доставка пакетов адресатам. Менеджер как видно из рисунка 3 обслуживает следующие основные компоненты, размещённые на разных уровнях: Менеджер кэш памяти (Cache manager) который обеспечивает кэширование всей системы ввода-вывода. Работа Менеджера кэш памяти основана на использовании функций модуля VMM. Кэш менеджер управляет операцией опережающего чтения, подгружая в физическую память страницы памяти, обращение к которым наиболее вероятно в последующие моменты времени. Этот модуль так же поддерживает операцию отложенной записи (lazy write) когда страница физической памяти, в которую записываются данные, сохраняется на диске в файле подкачки не сразу после её модификации, а в последствии, во время, когда процессор будет менее загружен. Драйверы файловой системы, поддерживающие различные типы файловых систем: FAT, HPFS и NTFS. Драйверы аппаратных устройств, которые имеют единый унифицированный интерфейс для обмена данными. Драйверы реализуются как 32-х разрядные программные модули нулевого кольца. Сетевые драйверы.
21
Монитор защиты (Security reference monitor) Последний компонент используется для защиты ресурсов операционной системы. Он отвечает за проверку прав доступа к системным объектам, проверку привилегий пользователей и т.п.. Подсистема Win32. Подсистема Win32 это основной компонент пользовательского уровня операционной системы. Он содержит набор API функций необходимых для работы приложений Win32, а также реализует операции управления клавиатурой мышью и выводом на экран для всех других подсистем. Подсистема Win 32 это отдельный процесс, который получает вызовы от пользовательских приложений через Модуль Вызова Локальных процедур (LPC). Подсистема работает как сервер, а пользовательские приложения выступают в роли клиентов. Такой механизм позволяет защитить системный код третьего кольца от несанкционированного доступа со стороны пользовательских программ. Эта защита основана на том, что память, в которой располагается подсистема не глобальна. Это частная память процесса подсистемы, и следовательно она недоступна из пользовательских приложений Win32. Вызовы функций подсистемы транслируются через ядро (kernel) расположенное в нулевом кольце защиты. Ядро обеспечивает интерфейс между клиентами и сервером, переключая контексты памяти. Когда клиент вызывает функцию API, активным становится контекст памяти потока сервера. Когда сервер выполняет функцию, система снова делает текущим контекст потока клиента и возвращает ему результат и управление. В Windows NT 3.51 подсистема Win32 включает системный код 3-его кольца для управления окнами (user32.dll) и для управления графикой (gdi32.dll) вместе с графическими драйверами (Рисунок 4). Подсистема Win32. Консольный ввод-вывод Графический интерфейс Управление пользовательским интерфейсом
Графические драйверы
User mode Kernel mode Executive services Kernel HAL Рисунок 4. Подсистема Win32 в NT 3.51. В Windows NT 4.0 структура подсистемы другая. Управление окнами графический интерфейс и графические драйверы перенесены в нулевое кольцо и являются частью ядра системы. Передача управления между системным (0-кольцо) и пользовательским (3-кольцо) уровнями - это длительная по времени процедура. Поэтому при разработке NT 4.0 преследовалась цель сократить число таких операций. Архитектура NT 3.51 предусматривает следующую последовательность переходов между пользовательским и системным кодом при вызове функции подсистемы (user или gdi): пользовательский (клиент) - системный (ядро) - пользовательский (сервер) - системный (ядро) - пользовательский (клиент). В Windows NT 4.0 эта цепочка короче: пользовательский (клиент) - системный(ядро) - пользовательский (клиент). 22
Подсистема Win32. Консольный ввод-вывод Некоторые служебные функции
User mode Executive Executive services
Kernel mode
Графический интерфейс Управление пользовательским интерфейсом.
Графические драйверы
Kernel HAL Рисунок 5. Реализация функций USER и GDI в NT 4.0.
23
Управление памятью в операционных системах Windows 95 и NT. Автор: Сидякин И.М. Московский Государственный Технический Университет им. Н.Э. Баумана. Кафедра ИУ-3, (11.1998) Email:
[email protected] Основные задачи управления памятью. Память один из наиболее важных ресурсов вычислительной системы. Производительность работы программного обеспечения в значительной мере зависит от эффективного управления памятью. В 32-х разрядных операционных системах Windows управление памятью основывается на концепции виртуальной памяти, которая поддерживается аппаратно. В этом разделе мы перейдём к более детальному рассмотрению вопроса. Память представлена двумя различными «слоями»: физическая память, с которой работает аппаратура и специальные программные модули, и виртуальная память доступная остальному программному обеспечению. Виртуальное адресное пространство открыто программному обеспечению, в то время как реальное физическое расположение данных и кода скрыто от пользовательских программ и большей части системных компонентов. Операционная система использует такое представление памяти для решения ряда важнейших задач: Предоставление линейного адресного пространства каждому процессу в системе. Программное обеспечение, как правило, работает с непрерывным пространством адресов, но данные, на которые указывают эти адреса, могут быть размещены в произвольном порядке в физической памяти. Этот порядок определяется таблицами страниц, которые создаются и корректируются операционной системой. Расширение памяти за счёт использования дискового пространства для хранения содержимого виртуальной памяти. Операционная система хранит на диске данные, не требующиеся в данный момент, тем самым, освобождая физическую память для более необходимой информации. Аппаратура поддерживает механизм исключений, который позволяет программно перехватить обращение к данным не присутствующим в физической памяти и загрузить эти данные с диска. Такая операция производится скрытно от программы, которая читает или записывает данные в виртуальную память. Выделение частного адресного пространства (контекста памяти) каждому процессу в мультизадачной среде. Все процессы в системе используют одинаковый диапазон виртуальных адресов. Но каждый процесс имеет свой собственный набор таблиц страниц. Это позволяет отображать одинаковые виртуальные адреса в различные физические адреса для различных процессов. По умолчанию (т.е. без дополнительных действий со стороны программы) процессу не известны физические адреса, в которых располагается его код и данные и конечно процесс не имеет доступа к физическому месту расположения кода и данных других процессов. Процесс просто не может указать виртуальный адрес который будет отображён в физическую память, занимаемую другим процессом. Точнее, эта операция может быть выполнена только под контролем операционной системы. Быстрое переключение контекстов памяти при переключении задач. Windows мультизадачная операционная система. Основным понятием мультизадачности является поток. Поток это цепочка инструкций МП - код, расположенный в адресном пространстве какого либо процесса. Один процесс приложения может содержать один или более потоков. ОС делит процессорное время на небольшие интервалы, которые называются квантами и периодически выделяет эти кванты для исполнения каждого потока. Переключение потоков происходит, в частности при истечении времени кванта или в случае если поток завершился до этого момента. Если следующий поток в очереди и текущий принадлежат разным процессам, система должна переключить контекст памяти, т.е. изменить текущие таблицы страниц. Это переключение контекста легко осуществляется коррекцией таблиц страниц или изменением указателя на каталог таблиц страниц в регистре процессора CR3. Организация общей памяти доступной нескольким или всем процессам. По умолчанию каждое приложение (процесс) не имеет доступа к физической памяти, в которой располагаются данные других процессов. Но в ряде случаев возникает необходимость в организации физически общей памяти доступной нескольким процессам. Для этого операционная система должна модифицировать записи в 24
таблицах страниц каждого такого процесса так, чтобы они указывали на одну и ту же область физической памяти. Защита областей памяти от доступа со стороны пользовательских приложений и запрещение модификации содержимого памяти. 32-х разрядные операционные системы Windows используют плоскую модель памяти (размер сегментов 4ГБ) при работе с приложениями Win32. Теоретически эти приложения могут использовать адреса в диапазоне от 0 до 0xFFFFFFFF. Но значительная часть этого адресного пространства зарезервирована операционной системой для собственных нужд. Эта системная память может быть легко защищена от доступа приложений расположенных в третьем кольце защиты. Кроме того, данные в нулевом или третьем кольце защиты могут быть доступны только для чтения. Это используется, например, для секций, в которых размещается код, который не следует модифицировать.
Страничная организация памяти. Все эти полезные свойства основаны на технике трансляции страниц реализованной аппаратно в процессоре Intel 80386. Всё виртуальное адресное пространство разделяется на блоки одинаковой длины. Эти блоки называются страницами. Размер страницы равен 4K (4096 байт). Физический адрес страницы определяется соответствующей записью в таблице страниц. Таблицы страниц создаются операционной системой. Таблицы страниц составляют двухуровневую древовидную структуру. В корне этой структуры располагается каталог таблиц страниц. Каталог это таблица, записи которой указывают на таблицы страниц. В свою очередь записи таблиц страниц содержат ссылки на страницы. Структура приведена на рисунке 1.
Каталог таблиц страниц
...
Таблица страниц
...
Страница (4K) Страница (4K)
...
... Таблица страниц
...
Страница (4K) Страница (4K)
Рисунок 1. Структура дерева для трансляции адресов страниц. Запись таблицы страниц (page table entry /PTE/)может содержать физический адрес страницы, если страница присутствует в памяти, или указывать на место на диске, в котором хранится содержимое страницы. В дополнение к этой информации записи каталога и таблиц страниц содержат сведения, использующиеся для защиты страниц. Страница защищена от доступа из приложений третьего кольца защиты, если бит User/Supervisor в записи сброшен в ноль. К такой странице могут обращаться только модули, расположенные в нулевом кольце защиты. Страница доступна только для чтения, если бит R/W записи установлен в единицу. Структура виртуального адресного пространства Windows 95. На рисунке 2 показана структура адресного пространства Windows 95. Система выделяет примерно 2ГБ виртуальных адресов процессу. Это частная область памяти процесса и по умолчанию каждый процесс в системе имеет различные записи таблиц страниц для описания этой области памяти. Остальное адресное пространство общее. Верхние 2ГБ зарезервированы использования операционной системой. В верхнем гигабайте располагаются системные структуры и модули, такие как таблицы страниц, виртуальные устройства и другое системное программное обеспечение нулевого кольца. Эта область памяти защищена от доступа из пользовательских приложений. Адреса в диапазоне от 0x80000000 до 0xBFFFFFFF (2ГБ-3ГБ) используются системным 25
ПО третьего кольца. Здесь расположены системные динамические библиотеки (USER, KERNEL, GDI). Здесь так же размещаются структуры файлов отображаемых в память (Memory mapped files), которые используются для организации общей памяти процессов. Часть этого адресного пространства занято «верхней» частью 16-ти битовой кучи(heap). «Нижняя» часть кучи располагается по адресам ниже 4МБ. Это делается для совместимости с 16-ти разрядными приложениями, которые используют кучу такого типа для динамического размещения памяти. Нижний мегабайт адресного пространства зарезервирован для поддержки программ MS-DOS. Следует отметить, что весь 16-ти разрядный код размещается в общей памяти между 2-м и 3-м гигабайтом. 16-ти разрядные приложения Windows не имеют частных виртуальных адресов. Это означает, что приложения Win16 могут «видеть» друг друга в памяти. Конечно, если известны значения селекторов сегментов, в которых они располагаются. 4GB
Таблицы страниц и др. системные структуры. Виртуальные устройства (VxD).
3GB
Системные DLL. Memory Mapped Files.
общая память
общая память
Верхняя часть глобальной кучи Win16. 2GB
Частная область процесса.
частная память
4MB
Нижняя часть глобальной кучи Win16. 0
общая память
MS-DOS.
Рисунок 2. Адресное пространство Windows 95. Типы памяти. Как уже было отмечено, для управления виртуальной памятью система использует дополнительные структуры данных: каталоги таблиц страниц и таблицы страниц, которые так же размещаются в памяти. Подсчитаем максимальный размер памяти, который может быть занят этими структурами. Полный диапазон адресов равен 4ГБ или 1024*1024*4096 - 1 мегабайт страниц. Таблица страниц может содержать 1024 записи. Таким образом, для описания 1МБ страниц требуется 1024 таблицы страниц плюс один каталог таблиц страниц размером 4К. Заметим, что каждая таблица страниц (так же как и каталог таблиц страниц) занимает 4 килобайта. Для описания всех страниц необходимо зарезервировать 4МБ (1024*4096) памяти под таблицы страниц и дополнительно 4096 байт под каталог таблиц страниц. Конечно, это может показаться слишком высокой ценой, если к тому же принять во внимание что каждый процесс может иметь свои собственные таблицы страниц для страниц частной области адресного пространства. Однако нет необходимости размещать и инициализировать все таблицы страниц. Вместо этого таблицы создаются, только если в этом есть необходимость. На практике, большинство приложений Windows использует гораздо меньший объем памяти, чем максимально допустимые 2ГБ. Каждая страница виртуального адресного пространства с точки зрения операционной системы может 26
находиться в одном из следующих состояниях: доступна(available), зарезервирована(reserved), используется и присутствует в физической памяти (committed and present), используется и не присутствует в физической памяти (committed and not present). В состоянии available страница, только теоретически может быть доступна, однако работать с ней невозможно. Такая страница не имеет записи в какой либо таблице страниц. Если страница зарезервирована, её содержимое не существует ни в физической памяти, ни на диске, однако система резервирует этот участок памяти для будущего использования. Если страница находится в состоянии committed and present, её содержимое находится в физической памяти и в одной из таблиц страниц имеется запись соответствующая этой странице. Состояние committed and not present отличается от предыдущего тем, что содержимое страницы располагается на диске. Страница в состоянии сommitted and present может дополнительно иметь статус locked. Содержимое такой страницы не может быть выгружено на диск. Т.е. гарантируется что такая страница всегда будет находится в физической памяти. Попытка доступа к страницам находящимся в состоянии reserved, available или commited and not present приводит к генерации исключительной ситуации (0x0E - page fault). Windows API содержит ряд функций для управления виртуальной памятью и в частности для резервирования памяти и переключения памяти в состояние commited. Только после этой операции память становится доступной, так как для неё создаются записи в таблице страниц. Таблицы страниц так же размещаются в памяти. Каждая таблица занимает одну страницу. После размещения и инициализации таблицы страниц, страница в которой она располагается, может находится в одном из состояний committed. Если эта страница не присутствует в физической памяти, в момент обращения к ней аппаратура вырабатывает исключительную ситуацию с кодом 0х0E. Таким образом, процедура чтения/записи в виртуальную память может включать обработку двух исключений подряд, первое возникает при доступе к неприсутствующей в памяти таблице страниц, а второе при доступе к неприсутствующей в памяти странице. Ассоциативный кэш буфер страничного преобразования. Обработка каждого исключения занимает относительно небольшое время, но всё же снижает производительность системы. Эта проблема пока не обсуждалась, поэтому перейдём к её рассмотрению. Каждое обращение к памяти требует двух дополнительных операций чтения – каталога таблиц страниц и таблицы страниц. Процессор аппаратно поддерживает операцию кэширования адресов страниц. Пары физических и соответствующих им виртуальных адресов страниц сохраняются в ассоциативном буфере страничного преобразования (TLB). TLB может содержать до 32-х записей, которые содежат физические адреса последних обращений к памяти. TLB позволяет избежать с довольно высокой степенью вероятности, дополнительного чтения записей таблиц страниц при обращении к страницам памяти. При каждом обращении к памяти, по какому либо виртуальному адресу аппаратура проверяет TLB, и в случае если в этом буфере уже содержится запись с физическим адресом страницы берёт его из буфера. Эта ситуация называется попаданием в кэш (cache hit). В этом случае пропадает необходимость в чтении каталога и таблиц страниц. Однако, если в буфере отсутствует нужная запись происходит чтение PTE из таблиц и новая запись создаётся и помещается в буфер. Если буфер TLB уже заполнен, процессор удаляет запись из начала буфера, перед тем как поместить в буфер новую запись. В наихудшем случае каждое очередное обращение к памяти не попадает в кэш, и он становится бесполезным. Однако такая ситуация маловероятна. В многозадачной операционной системе использование буфера TLB имеет одну важную особенность. При переключении контекста памяти (т.е. при изменении записей таблиц страниц) кэш по прежнему содержит записи соответствующие предыдущему контексту. Поэтому одновременно с переключением контекста памяти операционная система должна сбросить кэш буфер - удалить все имеющиеся в нём записи. Эта операция происходит автоматически при перезаписи содержимого регистра CR3. Общая память. Около 2ГБ виртуального адресного пространства выделяется для организации частной памяти процессов. Эта технология используется для защиты процессов друг от друга. Однако в ряде случаев возникает необходимость в организации областей физической памяти, которые доступны более чем из одного процесса. Каждый процесс имеет свой собственный набор таблиц страниц. Очевидно, для того чтобы разные процессы могли обращаться к одной и той же физической области памяти, достаточно изменить записи таблиц страниц этих процессов так,, чтобы они отображали логические адреса в 27
одинаковые физические. Это простое решение, однако, не лишено недостатков. Состояние страницы памяти динамически изменяется. Страница может располагаться в физической памяти или на диске, например в файле подкачки. При изменении состояния страницы (загрузки в память или выгрузки на диск), система должна модифицировать записи PTE каждого процесса который совместно с другими процессами использует эту страницу. Эта сложная операция приводит в целом к снижению производительности работы системы. Windows NT использует метод позволяющий избежать сканирования таблиц страниц процессов при загрузке общей страницы в физическую память. Однако выгрузка страницы из физической памяти производится описанным выше способом. Каждый процесс Win NT имеет свою собственную структуру таблиц страниц и корневой каталог таблиц страниц. Для общих страниц система дополнительно размещает структуры данных, которые называются прототипами таблиц страниц (prototype page table entries /PPTE). Прототипы таблиц страниц размещаются в глобальной памяти в старших адресах виртуального адресного пространства. Запись таблицы страниц процесса, которая назначается для организации общей памяти, первоначально указывает не на общую страницу памяти, а на структуру PPTE. В свою очередь PPTE содержит указатель на страницу. Система размещает прототипы таблиц страниц динамически по запросу. Детали этого механизма будут рассмотрены ниже.
Процесс 1
Процесс 2
Каталог таблиц
Запись таблицы страниц
Станица
Страница
Каталог табиц
Прототип записи таблицы страниц
Общая страница
Запись таблицы страниц
Страница
Страница
Рисунок 3. Организация общей памяти в Windows NT. Программно общую память можно организовать несколькими различными методами. В конце этой главы подробно рассматривается метод, основанный на использовании Файлов отображаемых в память (Memory mapped files). В ряде случаев можно ограничиться более простым с точки зрения программной реализации способом. Он менее универсален и применяется для организации общих данных для всех загруженных экземпляров одного приложения или динамической библиотеки. Общие данные должны размещаться в секции исполняемого файла с атрибутом SHARED. Такая секция объявляется в MSVC следующим образом: #pragma data_seg(«MYSHARE») ,где «MYSHARE» выбранное произвольно имя секции Все переменные, размещаемые в секции MYSHARE, объявляются директивой __declspec(allocate(«MYSHARE»)). Например: __declspec(allocate(«MYSHARE»)) int sharedvar; Эта директива указывает компилятору, что переменная должна быть помещена в секцию MYSHARE. В командной строке компоновщика следует указать атрибуты этой секции. 28
LINK < ... > /SECTION:MYSHARE, RWS Параметр RWS указывает, что секция доступна для чтения и записи и располагается в памяти общей для всех экземпляров исполняемого файла (read-write-shareable). Другой способ организации общей памяти заключается в её размещении в адресах выше 2ГБ. Эта память глобальна для всех процессов. Драйверы нулевого кольца могут открыть пользовательским приложениям доступ к этой памяти. Общая память используется в основном для организации обмена данными между процессами. Кроме этого общая память позволяет избавиться от дублирования в памяти данных и кода программ. По умолчанию код приложений размещается в общей памяти. Это означает, что два или более загруженных экземпляров одного приложения совместно используют одну копию кода. Для оптимизации совместного использования памяти экземплярами программ в Windows NT используется технология копирования при записи (Copy on Write). Технология копирования при записи. Совместное использование кода приложения несколькими его экземплярами приводит к экономии памяти, но вместе с тем вызывает проблемы при отладке приложения. Отладчик размещает точки останова и модифицирует код приложения. Одновременно могут выполняться несколько экземпляров приложения. Если один из них работает под управлением отладчика это не должно отражаться на остальных. Следовательно, в данном случае необходимо иметь в памяти, по крайней мере, две копии кода приложения: одну для экземпляра приложения, который отлаживается и другую для всех остальных. Операционная система Windows NT использует для решения этой проблемы технологию копирования при записи. Эта технология используется и для кода и для данных приложения. Основная идея состоит в том, что система создаёт копии только тех страниц, которые модифицируются в процессе работы приложения. Остальные страницы всё равно кода или данных остаются общими для всех экземпляров приложения и в единственном числе. Первоначально этому условию удовлетворяют все страницы приложения. Кроме этого все страницы имеют атрибут доступа только для чтения. Попытка записи в такую страницу приводит к исключительной ситуации. Системный обработчик исключения снимает атрибут страницы «только чтение» создаёт копию страницы и отображает её (корректирует PTE) в адресное пространство процесса, который произвёл операцию записи. Процесс в данном случае это один из запущенных экземпляров приложения. При загрузке очередного одном экземпляра приложения для него создаются отдельные копии страниц, которые на данный момент были модифицированы ранее запущенными экземплярами. Таким образом, в Windows NT страницы дублируются по запросу, что существенно экономит память. Windows 95 не поддерживает это механизм, однако, имеет специальные средства для поддержки процедуры отладки программ. Windows API включают функции WriteProcessMemory и ReadProcessMemory которые используются отладчиками для чтения и модификации памяти отлаживаемых процессов. Эти функции не позволяют работать с адресами выше 2ГБ. Поэтому для трассировки кода системных динамических библиотек Windows 95 размещённого выше 2ГБ необходимо использовать системные отладчики, такие как SoftIce, которые не полагаются в своей работе на эти функции. Переключение контекстов памяти. Основными элементами мультизадачной структуры Windows являются процессы и потоки. Каждому процессу соответствует отдельный контекст памяти, включающий частные и общие страницы. Каждый процесс должен содержать, по крайней мере, один поток, который определяет последовательность исполнения инструкций программы. Операционная система переключает контексты памяти всякий раз при передаче управления потоку, процесс которого отличен от текущего. Контексты памяти организованы по разному в различных операционных системах. В Windows NT каждый процесс имеет собственную структуру таблиц страниц и собственный каталог таблиц страниц. Регистр CR3 микропроцессора 80386 содержит указатель на каталог таблиц страниц текущего процесса. Таким образом, для того чтобы переключить контекст достаточно записать в регистр CR3 указатель на каталог таблиц очередного процесса. При этом также автоматически очищается ассоциативный буфер страничного преобразования. В Windows 95 каталог таблиц страниц совместно используется всеми 29
процессами. Эта операционная система при переключении контекста выполняет более сложную процедуру модификации записей таблиц страниц. Управление физической памятью в Windows NT. Рабочие наборы страниц (Working Sets). В Windows NT с каждым процессом связана системная структура данных, которая называется рабочим набором страниц. Рабочий набор страниц это связный список, содержащий ссылки на все принадлежащие процессу страницы расположенные в физической памяти. Эта структура динамически изменяется в процессе работы. Операционная система может дополнять список новыми страницами или удалять страницы из списка (отнимать их у процесса) в зависимости от текущего размера рабочего набора страниц и текущего объёма свободной физической памяти. Менеджер Памяти добавляет страницы к списку при их загрузке в физическую память. Загрузка, как правило, происходит при попытке обращения к странице не присутствующей в физической памяти. NT использует метод кэширования страниц (clustered demand paging) для оптимизации механизма загрузки страниц по запросу (demand paging). Если какой либо процесс обращается к странице не присутствующей в физической памяти, имеется высокая вероятность того, что следующие обращения к памяти будут происходить в окрестности этой страницы. Менеджер памяти NT поэтому загружает вместе со страницей, к которой было произведено обращение ещё несколько страниц расположенных вокруг неё. Эти страницы составляют кластер, размер которого зависит от типа содержимого страниц (код или данные) и от объёма свободной физической памяти. В кластер может входить от 1 до 8 страниц. Если физическая память полностью занята, Менеджер Памяти должен выгрузить одну из страниц из памяти в файл подкачки. Для определения такой страницы используется алгоритм Least Recently Used (LRU). Алгоритм находит страницу, которая использовалась «наиболее давно». Считается, что вероятность обращения к странице памяти тем ниже, чем дольше к ней не обращаются. Алгоритм использует свойство аппаратуры помечать страницы, к которым производится обращение. Когда какая либо программа читает или записывает данные в страницу, микропроцессор устанавливает в единицу бит доступа A (Accessed) в записи таблицы страниц этой страницы. Менеджер памяти сканирует таблицы страниц. Если бит A очередной записи установлен, Менеджер сбрасывает этот бит в ноль и продолжает сканирование. При обнаружении записи PTE со сброшенным битом A процесс поиска останавливается и страница, на которую указывает запись становиться кандидатом на выгрузку из памяти. Если при первом проходе бит A всех записей был установлен, что само по себе маловероятно, сканирование таблиц повторяется. Так как биты A были сброшены во время первого прохода, вероятность успешного завершения поиска на втором проходе практически равна единице. Однако, принятие решения о том какая именно страница должна быть выгружена из памяти делается не только на основе изложенного алгоритма. Операционная система NT руководствуется правилами так называемых локальной и глобальной политики удаления страниц из физической памяти. Когда какой либо процесс обращается к не присутствующей в памяти странице система может освободить память для загрузки этой страницы удалив наиболее «старую» страницу из рабочего набора страниц этого процесса или из рабочего набора другого процесса или просто выделить этой странице свободную физическую память. В первом случае применяется локальная политика, а в двух последних глобальная. Решение о том какая политика должна использоваться в каждом конкретном случае зависит от текущего размера рабочего набора страниц процесса и объёма свободной физической памяти. Система задаёт минимальный и максимальный размеры рабочих наборов страниц. Если текущий размер превышает максимальный, и свободные страницы отсутствуют, используется локальная политика. В системе в фоновом режиме работает специальный модуль Менеджер баланса наборов страниц (Balance Set Manager). Он периодически проверят размеры Рабочих наборов страниц процессов, и удаляет из них «лишние» страницы. Основная задача этого модуля поддерживать баланс использования памяти между различными процессами. Размер рабочего набора страниц процесса может увеличиваться, по мере того как процесс обращается к неприсутствующим в физической памяти страницам. С другой стороны его размер уменьшается Менеджером баланса или в результате применения глобальной политики по мере роста рабочих наборов других процессов, которые более активно работают с памятью. База данных страниц (Page Frame Database). В операционной системе Windows NT атрибуты страниц физической памяти заносятся в системную таблицу, которая называется База данных страниц (PFD). Каждая запись в 30
этой таблице соответствует одной из физических страниц. Первая запись первой странице вторая запись второй странице и т.д.. (См. рисунок 7).
Процесс 1 Рабочий набор страниц
...
Процесс 2 Рабочий набор страниц
...
ОБЩАЯ
Valid Standby Modified Modified nw Free Zeroed In Transition Bad Modified nw In Transition Standby Free Modified Valid Zeroed Standby Valid Free Valid Valid Free ... База данных физических страниц
Таблица страниц Процесса 1
...
Прототип записи таблицы страниц (PPTE) Таблица страниц Процесса 2
...
Рисунок 7. База Данных Физических Страниц (Page Frame Database) Каждая запись в PFD содержит информацию о текущем состоянии физической страницы памяти. Если страница принадлежит рабочему набору какого либо процесса, она считается Активной (Active или Valid). Если Менеджер Памяти удаляет страницу из рабочего набора, страница переходит в одно из следующих состояний: - Standby. Состояние ожидания. Если содержимое страницы в физической памяти соответствует содержимому её копии на диске в файле подкачки. Каждая страница в физической памяти имеет зарезервированное место на диске в файле подкачки, куда она помещается при выгрузке из памяти. Система должна следить за тем, чтобы содержимое страницы в физической памяти и на диске было идентичным. Система в фоновом режиме периодически сбрасывает на диск содержимое модифицированных в памяти страниц. - Modified. Страница модифицирована. Если процесс изменил содержимое страницы, и эти изменения не были сохранены на диске, пока страница была активной. Такая страница не может быть использована до тех пор, пока Менеджер Памяти не сохранит её содержимое на диске. Менеджер Памяти периодически сканирует базу данных страниц. При этом страницы Modified сохраняются на диске. - Modified no-write. Страница модифицирована, но запись запрещена. В ряде случаев модифицированные страницы могут быть записаны на диск, только по завершении каких либо операций. Например, если страница расположена в области таблицы размещения файлов (FAT), система делает протокольную запись, перед тем как изменить на диске содержимое этой структуры. Эта протокольная запись может быть использована для восстановления системы, если запись в FAT выполнена некорректно. Пока такая страница находится в состоянии Modified no-write, Менеджер памяти не записывает её содержимое на диск. По завершении необходимых операций система разрешает запись страницы, переключая её в состояние Modified. - Free. Свободная. Страница становится свободной, в случае если она более не используется процессом. Это происходит, когда процесс завершается или освобождает динамически выделенную память. Специальный системный фоновый поток используется для защиты данных процессов. Этот поток заполняет нулями все свободные страницы. Смысл защиты заключаются в 31
том, что содержимое страницы извлечённой из рабочего набора страниц процесса стирается, перед тем как эта страница включается в рабочий набор другого процесса. Этот фоновый поток получает управление, в случае если загрузка процессора очень мала (Этот поток имеет самый низкий нулевой приоритет). После заполнения нулями страница переходит в состояние Zeroеd - чистая страница. Во время процедуры загрузки страницы в память или выгрузки на диск она находится в состоянии In Transition - в процессе передачи. Последнее из возможных состояний Bad - сбойная страница. Менеджер Памяти присваивает странице атрибут Bad в случае аппаратного сбоя при обращении к ней. Такие страницы в дальнейшем не используются. Вместе с информацией о состоянии записи PFD содержат указатели на следующую запись таблицы, имеющую то же состояние. Например, запись страницы Standby указывает на следующую запись Standby в базе данных. Страницы, находящиеся в одинаковых состояниях объединены в связные списки. Это упрощает работу Менеджера Памяти. Исключение составляют активные страницы и страницы находящиеся в процессе передачи. Когда требуется найти свободную страницу памяти Менеджер Памяти начинает поиск со списка Zeroed, если этот список пуст поиск продолжается в списке Free и наконец в списке Standby. Номер найденной свободной страницы добавляется к связному списку рабочего набора страниц процесса, а страница переходит в состояние Valid. Детали организации общей памяти в Windows NT. Запись PFD активной страницы также содержит указатель на запись таблицы страниц, которая в свою очередь содержит «аппаратные» атрибуты и ссылку на физическое месторасположение страницы. В частном адресном пространстве процессов одной физической странице и, следовательно, одной записи в базе данных физических страниц соответствует одна запись PTE в таблицах страниц. Но в случае, если одна и та же физическая страница совместно используется несколькими процессами, для неё должны быть созданы отдельные записи PTE в таблицах страниц каждого процесса (рисунок 7). Однако, запись в PFD не может одновременно указывать на несколько записей PTE. В этом случае она указывает на структуру Прототипа Записи Таблицы Страниц (PPTE). Структура PPTE размещается в памяти и инициализируется операционной системой. Она используется для организации общей памяти. PPTE содержит ссылку на страницу - физический адрес или смещение в файле подкачки. Записи PTE процессов, которые совместно используют страницу, могут содержать в зависимости от ситуации адрес структуры PPTE или собственно адрес страницы. Поясним это на примере: Пусть страница совместно используется двумя процессами. Каждый процесс имеет в одной из своих таблиц страниц запись PTE этой страницы. Когда страница не присутствует в памяти, в записях PTE она помечена как не присутствующая (notpresent). При этом записи PTE содержат адреса структуры PPTE. PPTE в свою очередь содержит ссылку на месторасположение страницы в файле подкачки. Обращение одного из процессов (далее Процесс1) к странице приводит к исключительной ситуации (page fault). Обработчик исключения находит в PTE страницы адрес PPTE. Далее по ссылке в PPTE находит и загружает из файла страницу. Ссылка на страницу в PPTE, а так же адрес PPTE в записи PTE Процесса 1 заменяются на физический адрес загруженной страницы. Атрибут not-present в записи PTE процесса сбрасывается. После этой операции Процесс1 может свободно обращаться к странице. Когда второй процесс (Процесс 2) пытается обратиться к странице, процессор снова вырабатывает исключение. Это происходит потому, что запись PTE этого процесса по прежнему имеет атрибут not-present и указывает на PPTE. Система обращается к PPTE и определяет, что страница уже загружена в физическую память. В этом случае система просто переписывает в PTE Процесса 2 физический адрес страницы из PPTE. Таким образом, при загрузке страницы в память отпадает необходимость в сканировании и коррекции записей таблиц страниц всех процессов. Однако при выгрузке страницы все структуры должны быть приведены в исходное состояние. В записи PTE процессов заносится атрибут not-present и адрес PPTE. А в структуру PPTE помещается ссылка на место страницы в файле подкачки.
32
Функции для управления памятью. Операционная система предлагает несколько способов работы с памятью. Функции управления памятью разделяются на три основные группы: функции управления виртуальной памятью, функции управления кучей (heap) и функции для организации отображения файлов в память (memory mapped files). Приведём краткое описание каждой группы функций: Функции управления виртуальной памятью. Эти функции используются для резервирования и управления состоянием страниц виртуальной памяти. Функции, как правило, используются для работы с большими блоками памяти, выровненными по границе страниц. Страница виртуальной памяти может быть свободна (free), зарезервирована (reserved) и используема (committed). Свободные страницы потенциально доступны, однако не используются в данный момент и не зарезервированы для использования в будущем. Такие страницы могут быть преобразованы в зарезервированные или используемые. Зарезервированные страницы не занимают физического пространства на диске или в памяти. Резервирование диапазона виртуальных адресов означает лишь только то, что этот диапазон не будет участвовать в последующих операциях резервирования памяти. Содержимое страниц используемой памяти хранится в физической памяти и на диске в файле подкачки. Такие страницы могут быть защёлкнуты (locked). Если страница защелкнута, её содержимое всегда находится в физической памяти. Для резервирования и/или перевода в используемое состояние страниц памяти используется функция VirtuallAlloc. LPVOID VirtualAlloc( LPVOID lpAddress, DWORD dwSize, DWORD flAllocationType, DWORD flProtect );
// // // //
базовый адрес области памяти размер области памяти в байтах тип операции атрибуты защиты памяти
Параметр flAllocationType задаёт тип операции - резервирование памяти и/или перевод в используемое состояние. Параметр flProtect определяет атрибуты защиты области памяти. LpAddress - начальный адрес - всегда выравнивается по границе 64K для резервируемой памяти и по границе 4k для используемой памяти. Размер всегда округляется вверх до границы страницы. Пример использования функции: /* Резервируем область размером 10 МБ */ lpBase = VirtualAlloc (NULL, 10485760, MEM_RESERVE, PAGE_NOACCESS); /* Переводим в ипользумое состояние 3-ю страницу этой области. */ lpPage3 = VirtualAlloc (lpBase + (2 * 4096), 4096, MEM_COMMIT, PAGE_READWRITE); Функция VirtualFree используется для освобождения зарезервированной или используемой памяти. BOOL VirtualFree( LPVOID lpAddress, // базовый адрес области памяти DWORD dwSize, // размер области памяти в байтах DWORD dwFreeType // тип операции ); Функция может перевести память из используемого состояния в зарезервированное или свободное. Тип операции задаётся четвёртым параметром dwFreeType. Примеры: /* Переводим из используемого в зарезервированное страницу ранее размещённого диапазона. */ VirtualFree (lpBase + (2 * 4096),
состояние
3-ю
33
4096, MEM_DECOMMIT, PAGE_NOACCESS); /* Освобождаем весь диапазон размером 10 МБ. */ VirtualFree (lpBase, 10485760, MEM_RELEASE, PAGE_NOACCESS); Атрибуты защиты заданные функцией VirtualAlloc можно изменить с помощью функции VirtualProtect. BOOL VirtualProtect( LPVOID lpAddress, DWORD dwSize, DWORD flNewProtect, PDWORD lpflOldProtect
// базовый адрес области памяти // размер области памяти в байтах // атрибуты защиты памяти // адрес для возврата старых атрибутов защиты
); Праметр flNewProtect может содержать флаги: PAGE_READONLY, PAGE_READWRITE, PAGE_EXECUTE_READ и др. Например: /* Разрешаем чтение и запись в страницу */ VirtualProtect (lpStack + 4096, 4096, PAGE_READWRITE, lpdwOldProt); Используемые страницы могут быть защёлкнуты в памяти. Защёлкнутая страница всегда находится в физической памяти и не может быть выгружена на диск в файл подкачки. Защёлкиваются обычно код и данные, принимающие участие в критичных ко времени исполнения процедурах. Функция VirtualLock защёлкивает страницы памяти, а функция VirtalUnlock отменяет защёлкивание. BOOL VirtualLock( LPVOID lpAddress, // базовый адрес области памяти DWORD dwSize // размер области памяти в байтах ); Защелкивание памяти следует использовать только в случае необходимости. Это приводит к снижению производительности работы системы. Куча (Heap). Куча это надстройка над функциями управления виртуальной памятью, которая позволяет более экономно использовать виртуальные адреса. Функции кучи допускают работу с блоками данных, размеры которых не кратны размеру страницы. Приложения, которые используют эти функции, могут не учитывать специфику внутреннего страничного управления памятью. Они получают динамически выделенные блоки памяти точно заданного в байтах размера. В Windows 3.x имеются кучи двух типов: одна глобальная куча и локальные кучи для каждого приложения. Глобальная куча Windows 3.х доступна для всех приложений и в частности используется для организации обмена данными между приложениями Win16. Функции для работы с кучей в этой операционной системе разделяются на функции для работы с глобальной и локальной кучей. Функции отличаются префиксами - соответственно Local и Global. В 32-х разрядных OC Windows каждый процесс имеет свою собственную кучу. Функции с префиксами Local и Global в приложениях Win32 работают одинаково. Для работы с кучей имеется несколько функций, включая : GlobalAlloc, - размещает в куче блок памяти заданного размера; GlobalFree, - освобождает блок памяти GlobalRealloc - изменяет размер блока памяти. 34
GlobalLock, and GobalUnLock - защёлкивает и отменяет защёлкивание блока памяти. Отметим, что имеется такой же набор функций с префиксом Local. Большинство функций кучи используют ссылки на блоки памяти, а не прямые виртуальные адреса. Например, функция GlobalAlloc возвращает ссылку на блок памяти. Адрес этого блока можно получить с помощью функции GlobalLock (или LocalLock). Файлы отображаемые в память (Memory Mapped Files). Отображение содержимого файлов в виртуальную память полезное и часто используемое свойство операционной системы. Доступ к файлам в этом случае ничем не отличается от работы с обычной памятью. Например, прочитать 4 байта из файла можно инструкцией: Mov Eax, [Edi] Идея отображения файлов в память заключается в том, что содержимое файла или фрагмента файла проецируется в заданный диапазон виртуальных адресов. Файл делится на страницы размером 4K. Операционная система создаёт записи таблиц страниц для этого диапазона. Первоначально страницы файла помечены как неприсутствующие в памяти (not present). Запись PTE каждой такой страницы содержит ссылку на файл, в котором расположена страница, и смещение страницы от начала файла. Обращение странице вызывает исключительную ситуацию (page fault) и системный обработчик исключения загружает страницу в физическую память. Этот механизм используется операционной системой для организации виртуальной памяти на диске в файле (файлах) подкачки. Однако его можно применить к любому файлу. Длина файла может быть слишком велика, чтобы его можно было целиком отобразить в память. Поэтому допускается отображать в память отдельные фрагменты, файла которые называются окна (views). В окно может входить фрагмент файла или весь файл. Операционная система отображает в память не только файл подкачки. Эта же технология применяется при загрузке исполняемых модулей: приложений и динамических библиотек. При загрузке приложения секции файла приложения содержащие код отображаются в память. Формат исполняемых файлов Windows (portable executable file format, сокращённо PE) сконструирован таким образом, что содержимое файла исполняемого модуля почти не отличается от его образа в памяти после загрузки. В общем, можно сказать, что операционная система использует файл подкачки для хранения страниц содержащих данные и файлы приложений для хранения страниц содержащих код (Рисунок 4).
Виртуалная память Процесса 1 Станицы кода
Диск
Файл программы 1
Станицы данных Системный файл подкачки Виртуалная память Процесса 2 Станицы данных
Файл программы 2
Станицы кода
35
Рисунок 4. Системный файл подкачки и файлы программ. Файлы отображаемые в память могут быть легко использованы для организации общей памяти. Любой процесс может открыть файл, зная его имя. Для организации обмена данными через файл приложения отображают какой либо участок этого файла в адресные пространства своих процессов. Этим файлом может быть, например системный файл подкачки (Рисунок 5). Такая же технология используется для организации совместного использования кода несколькими экземплярами приложения.
Виртуальная память процесса 1
ДИСК
Страница кода Общие данные Страница данных
Файл программы 1
Системный файл подкачки Виртуальная память процесса 1 Страница данных Общие данные Страница кода
Файл программы 2
Рисунок 5. Организация общей памяти. Функции для организации отображения файлов в память. К этим функциям относятся: CreateFileMapping, OpenFileMapping, MapViewOf File, MapViewOfFileEx, UnMapViewOfFile, FlushViewOfFile и CloseHandle. Создание Отображения файла (File Mapping) Перед отображением файла в память его следует открыть или создать. Эта операция пропускается, если будет использоваться файл подкачки. Файл открывается или создаётся функцией API CreateFile: hFile = CreateFile(FileName, GENERIC_READ | GENERIC_WRITE, FILE_SHARE_WRITE, NULL, CREATE_ALLWAYS, FILE_ATTRIBUTE_TEMPORARY, NULL); Если операция завершается без ошибки функция возвращает ссылку на открытый файл. Эта ссылка используется в дальнейшем для доступа к открытому файлу. Атрибуты GENERIC_READ и _WRITE разрешают чтение и запись в файл. Атрибут FILE_SHARE_WRITE разрешает запись в файл из других процессов, которые совместно используют этот файл. Флаг CREATE_ALLWAYS указывает на то, что файл должен быть создан заново. Затем следует создать файл отображаемый в память (MMF). MMF создаётся на основе открытого файла или внутри файла подкачки. В последнем случае операционная система резервирует в файле подкачки место для MMF. Для создания MMF используется функция CreateFileMapping. 36
CreateFileMapping(HandleOfDiskFile, SecurityAttributes, Protection, MaxSizeHigh, MaxSizeLow, ObjectName); Эта функция создаёт объект MMF и возвращает ссылку на него. Функция имеет следующие параметры: HandleOfDiskFile - ссылка на открытый файл или –1, если используется системный файл подкачки. SecurityAttributes - атрибуты защиты. Protection - флаги: PAGE_READONLY - только чтение. PAGE_READWRITE - чтение и запись PAGE_WRITECOPY - разрешено применение технологии копирования при записи (Только NT). MaxSizeHigh старшее двойное слово размера отображаемой области памяти. MaxSizeLow младшее двойное слово размера отображаемой области памяти. Размер это 64-х битовое значение. ObjectName строка, содержащая имя объекта MMF. Если имя объекта задано, оно доступно любому процессу в системе. Если один из процессов создаёт именованный объект MMF, другой процесс может получить доступ к объекту по его имени. Имя используется, например, когда файл отображаемый в память применяется для организации общей памяти. Пример вызова функции CreateFileMapping: hMMFile = CreateFileMapping(hFile, NULL, PAGE_READ_WRITE, 0, 0x01400000, NULL); Если объект MMF совместно используется несколькими процессами ,один из них назначает объекту уникальное имя и создаёт его, а другие могут получить ссылку на объект с помощью функции OpenFileMapping. OpenFileMapping(DesiredAccess, IheritanceFlag, ObjectName); Параметр DesiredAccess по смыслу соответствует параметру Protection функции CreateFileMapping и может принимать значения: FILE_MAP_READ, FILE_MAP_WRITE и FILE_MAP_COPY. Параметр InheritanceFlag может принимать значения TRUE или FALSE. Он определяет, может ли ссылка быть наследована. По умолчанию разные процессы не могут использовать одну и ту же ссылку (одно и то же численное значение) на объект MMF потому, что ссылка это виртуальный адрес объекта. (Подробнее см. следующую главу.) Каждый процесс имеет своё собственное адресное пространство. Одни и те же виртуальные адреса в разных процессах отображаются в разные физические адреса. Исключение из этого правила можно сделать, когда один процесс создаётся из другого. В этом случае создаваемый процесс называется дочерним процессом, а создающий родительским процессом. Дочерний процесс может наследовать некоторые ссылки на объекты созданные в родительском процессе и использовать их для доступа к объектам. Когда в процессе создаётся ссылка на объект, следует специально указать что эта ссылка может быть наследована дочерними процессами. Функции, которые используются для создания объектов, например CreateFileMapping, позволяют указать возможность наследования ссылки в структуре SecurityAttributes. Указатель на эту структуру передаётся в параметрах функций. Следует отметить, что ссылка на существующий объект MMF может быть так же получена с помощью функции CreateFileMapping. Объект идентифицируется по уникальному имени. Если объект с заданным именем уже существует, функция не создаёт новый экземпляр объекта. Функция просто возвращает ссылку на существующий объект. При этом атрибуты объекта заданные в параметрах функции игнорируются. Для того чтобы выяснить был ли создан новый объект или открыта ссылка на существующий объект сразу после вызова функции CreateFileMapping следует вызвать функцию GetLastError. Если эта функция возвращает ошибку ERROR_ALREADY_EXISTS объект уже существовал до вызова функции CreateFileMapping. 37
Создание окна (view) в файле отображаемом в память. После создания объекта MMF следует отобразить нужный фрагмент файла в виртуальные адреса процесса. Эта операция производится с помощью функций MapViewOfFile или MapViewOfFileEx. Функция MapViewOfFileEx позволяет прямо указать базовый виртуальный адрес фрагмента файла. MapViewOfFile(MMFObjectHandle, DesiredAccess, FileOffsetHigh, FileOffsetLow, NumberOfBytestoMap); MapViewOfFileEx(MMFObjectHandle, DesiredAccess, FileOffsetHigh, FileOffsetLow, NumberObBytestoMap, BaseVirtualAddress); MMFObjectHandle – ссылка на объект MMF, которую возвращает функция CreateFileMapping. DesiredAccess определяет права доступа к памяти. Например «только чтение».FileOffsetHigh и FileOffsetLow соответственно старшее и младшее двойное слово 64-х битового значения смещения фрагмента относительно начала файла. NumberOfBytestoMap 32-х разрядное значение, определяющее размер окна. Если размер и смещение равны нулю, создаётся окно, содержащее весь файл: lpViewPtr = MapViewOfFile (hMMFile, FILE_MAP_WRITE, 0, 0, 0); Функция возвращает начальный (базовый) виртуальный адрес окна или NULL в случае ошибки. Теперь с файлом можно работать так же как с блоком памяти. Несколько процессов могут совместно использовать один и тот же объект MMF. Каждый процесс может разместить несколько окон в одном и том же файле. Окна могут иметь различные размеры и могут перекрываться. Примеры размещения окон приведены на рисунке 6.
Виртуальная память процесса 1
Файл отображаемый в память (MMF)
Окно 1 Окно 2
Виртуальная память процесса 2 Окно 1 Окно 2 Окно 3 Рисунок 6. Примеры окон (Views). Деинициализация MMF. Каждое окно должно быть удалено, после того как работа с ним завершается. Для удаления окон MMF используется функция UnmapViewOfFile. UnmapViewOfFile (lpViewPtr);
38
В параметрах функции следует указать виртуальный адрес окна, который возвращает функция MapViewOfFile (Ex). Ссылка на объект MMF удаляется вызовом функции CloseHandle. CloseHandle(hMMFile); Если объект MMF создан не в файле подкачки, следует закрыть файл, на основе которого был создан этот объект. CloseHandle(hFile); Принудительная запись на диск содержимого окон MMF. Страницы памяти, в которых размещается содержимое окна, загружаются в физическую память по запросу. Т.е. при обращении к этим страницам. Если страница загружена в физическую память, существует две копии её содержимого – в памяти и на диске. Изменение содержимого страницы в памяти и сохранение изменений на диске разделены во времени. Модифицированная страница сохраняется в файле в «удобное» с точки зрения операционной системы время. Этот процесс скрыт от приложений. Однако в случае необходимости можно принудительно записать содержимое страниц окна MMF на диск. Для этого используется функция FlushViewOfFile. FlushViewOfFile(StartingAddress, Size) Первый параметр задаёт виртуальный адрес внутри окна MMF, а второй размер блока, который должен быть сохранён на диске.
39
Объекты и ссылки. Автор: Сидякин И.М. Московский Государственный Технический Университет им. Н.Э. Баумана. Кафедра ИУ-3, (11.1998) Email:
[email protected] Понятия объектов и ссылок определяются в документации Windows Platform SDK следующим образом: Объект это структура данных, которая описывает системный ресурс, например файл, поток, или графическое изображение. Приложение не имеет прямого доступа к объекту или системному ресурсу, который описывает объект. Вместо этого, приложение должно получить ссылку на объект (handle), которую оно может использовать для доступа к системному ресурсу. Простой пример объекта это файл. Файл имеет набор атрибутов которые доступны приложениям. Например, имя, путь, размер, права доступа (открыт только чтения, только для записи), текущая позиция в файле указателя чтения и записи и т.п. Все эти параметры (они могут быть статичными или динамически изменяемыми в процессе работы) собраны в единой структуре - объекте. Структура имеет поля которые содержат текущие значения каждого параметра. Этот упрощённый пример иллюстрирует основную идею объектно-ориентированной структуры операционной системы. Система использует объекты для управления своими ресурсами. Например, система может запретить операции чтения в файл, установив в соответствующем поле объекта файла атрибут «только чтение». Как правило, пользовательские программы не имеют возможности модифицировать поля объекта напрямую. Вместо этого, система экспортирует набор функций API, предназначенных для работы с объектами различных типов. Эти функции работают с сылками на объекты. Ссылка это 32-х битовое число. Для некоторых типов объектов ссылка это просто виртуальный адрес объекта. Для других типов ссылка может быть индексом в системной таблице объектов данного типа. В любом случае по ссылке можно получить доступ к объекту. Ссылка идентифицирует объект в системе в целом или в только виртуальном адресном пространстве одного из процессов. Основные операции с объектами. Объекты предназначены для описания различных системных ресурсов. Каждый ресурс имеет свой собственный формат объекта, но некоторые операции могут применяться ко всем типам объектов. Объект независимо от его типа имеет стандартный заголовок. Этот заголовок позволяет использовать единый механизм управления объектами, который включает следующий набор операций:
Создание объекта Проверка прав доступа к объекту Создание ссылок на объект Ограничение использования объекта (например, ограничение максимального числа ссылок на объект) Дублирование ссылок на объект Закрытие ссылок и уничтожение объекта
Каждый объект включает поля необходимые для выполнения перечисленных операций. Например, атрибуты защиты и имя объекта. Интерфейс объектов. Операционная система поддерживает единый интерфейс работы с объектами для приложений. Существует набор функций, которые могут быть использованы для выполнения стандартных операций над объектами. К этим функциям относятся:
Создание объекта Получение ссылки на объект Получение параметров объекта Установка параметров объекта Закрытие ссылки на объект Уничтожение объекта 40
К некоторым объектам могут применяться не все вышеперечисленные функции. Типы объектов. Все объекты разделяются на три типа: объекты user, объекты GDI и объекты kernel. Объекты User используются для управления оконным интерфейсом Windows. К ним относятся иконки, курсоры, окна, меню. Объекты GDI (графического интерфейса) используются для поддержки графических операций. Примерами этих объектов являются, графические контексты, кисти, палитры, растровые изображения и др. Объекты Kernel описывают разнообразные системные ресурсы использующиеся для управления памятью, синхронизации потоков, организации обмена данными между процессами и т.п. Примеры объектов этого типа: процессы, потоки, файлы, объекты синхронизации. Объекты User. Система может создать только одну ссылку на каждый объект User. Однако эту ссылку может использовать любой процесс. Объекты, относящиеся к этому типу доступны из любого процесса в системе. Для объектов User ссылка на объект это индекс в глобальной системной таблице, а не виртуальный адрес объекта. Записи таблицы в свою очередь содержат адреса объектов. Ниже приводится пример работы с объектом такого типа. В примере один процесс получает доступ к окну созданному в другом процессе. Практический смысл примера заключается в том, что бы запретить загрузку более чем одного экземпляра приложения Win32. Загрузка первого экземпляра приложения должна проходить нормально. Однако попытка загрузки второго экземпляра при работающем первом должна пресекаться. Приложение каждый раз в процессе загрузки (до создания главного окна) должно проверять наличие в системе объекта окна, заголовок которого совпадает с заголовком какого либо (например, главного) окна приложения. Если окно с таким заголовком уже существует в системе, экземпляр приложения завершает работу. Если окна с таким заголовком нет, (первый) экземпляр приложения продолжает работу. Определить наличие в системе окна с заданным заголовком можно с помощью функции FindWindow. Эта функция возвращает ссылку на объект окна, если он существует или NULL, если окно не найдено. HWND hWnd; hWnd = FindWindow(WindowName, WindowClassName); Если FindWindow возвращает не нулевое значение, один экземпляр приложения уже загружен. В этом случае второй экземпляр приложения должен завершить свою работу. Перед завершением работы он может активизировать окно первого экземпляра: if (hWnd != NULL) { //экземпляр не первый //активизируем окно первого экземпляра по полученной //ссылке SetForegroundWindow(hWnd); //завершаем работу } else //продолжаем работу, так как это первый экземпляр. Объекты GDI. Объекты GDI не могут совместно использоваться разными процессами. Каждый процесс должен иметь свою собственную копию объекта такого типа. Однако, как и для объектов User, система может создать только одну ссылку на объект GDI. Эта ссылка используется процессом внутри которого объект был создан. Примером функции создающей объект GDI является функция CreateBitmap. Объекты Kernel. Объекты kernel могут быть созданы в одном из процессов. При этом доступ к ним можно организовать из нескольких процессов. Ссылки на объекты kernel это виртуальные адреса объектов, поэтому любая ссылка имеет смысл только в контексте одного процесса. Если несколько процессов работают с одним и тем же объектом, 41
каждый из них должен получить свою собственную ссылку. Объекту Kernel может быть присвоено уникальное имя. Любой процесс может получить на объект ссылку по его имени. Ссылки на один и тот же объект могут иметь разные права доступа. Например, файл может быть открыт только на чтение или на запись. Один процесс может получить несколько ссылок на объект с разными правами доступа. На рисунке 1 приводится пример операции создания объекта индикатора события (Event) принадлежащего к типу объектов kernel.
Виртуальное адресное пространство Handle
Приложение
Объект Event
3.CreateEvent возвращает handle на объект Event.
1.Выполнить CreateEvent 2. CreateEvent создаёт в памяти объект Event Рисунок 1. CreateEvent. Процесс может создать объект или получить одну или несколько ссылок на существующий объект, указав имя этого объекта. Получить ссылку на существующий объект Event можно с помощью функции OpenEvent. (Рисунок 2). 3.CreateEvent возвращает handle объекта Event. Handle 1 Handle 2
Приложение
Виртуальное адресное пространство Объект Event
5.OpenEvent создаёт второй handle объекта Event
1.Вызов CreateEvent 4.Вызов OpenEvent
2. CreateEvent создаёт в памяти объект Event
Рисунок 2. CreateEvent/OpenEvent HANDLE OpenEvent( DWORD dwDesiredAccess, BOOL bInheritHandle, LPCTSTR lpName );
// права доступа // разрешение наследования ссылки // указатель на строку с именем объекта
Пары функций Create/Open имеются практически для всех объектов типа Kernel. По завершении работы с объектом ссылка на него должна быть закрыта. Ссылки всех объектов kernel закрываются функцией CloseHandle. Единственным параметром этой функции является ссылка на объект. Объект удаляется, после того, как будут закрыты все ссылки на него.
42
Имена объектов Kernel. Как уже было отмечено, объект типа kernel может иметь уникальное имя. Именование объектов используется для идентификации объекта в системе. Если несколько процессов совместно используют объект, они должны получить ссылки на этот объект. Процесс может получить ссылку на объект по имени объекта. Имя назначается объекту при его создании. Ссылку на объект можно получить с помощью функций Open или Create. Выбор зависит от логики программы. Если только один из процессов имеет право создать объект, он должен использовать функцию Create. Другие процессы должны вызывать функцию Open. Если это не имеет значения, все процессы могут применять функцию Creаte. При этом первый процесс, вызвавший эту функцию, создаст объект, а остальные получат на него ссылку. Примеры: HANDLE hEvent; 1.Создание объекта без имени. hEvent = CreateEvent(NULL, FALSE, FALSE, NULL); 2.Создание объекта с именем или получение ссылки на объект, если он существует. hEvent = CreateEvent(NULL, FALSE, FALSE, «MyEventObject»); В этом случае можно дополнительно проверить существовал ли объект до вызова функции: if (GetLastError() == ERROR_ALREADY_EXISTS) { //объект с именем «MyEventObject» уже существовал } 3.Попытка получения ссылки на объект. hEvent = OpenEvent(EVENT_ALL_ACCESS, FALSE, «MyEventObject); В переменную hEvent возвращается NULL если объект не существует. После окончания работы со ссылкой она должна быть закрыта: CloseHandle(hEvent); Наследование и дублирование ссылок на объекты Kernel. Ссылка на объект kernel это виртуальный адрес объекта. Каждый процесс имеет свой собственный контекст памяти, поэтому ссылка не может совместно использоваться разными процессами. Разные процессы могут получить ссылку на объект по его имени, однако существуют дополнительные способы передачи ссылки на объект из процесса в процесс. Один из этих способов называется наследование. Рассмотрим случай, когда один процесс создаётся из другого. Создаваемый процесс называется дочерним (child process), а создающий родительским процессом (parent process). Дочерний процесс можно создать с помощью функции CreateProcess. Дочерний процесс при соблюдении нескольких условий может использовать (наследовать) ссылки родительского процесса. Разрешить наследование ссылки в дочерних процессах необходимо при её создании. Интерфейсы функции Create различных объектов, как правило, содержат указатель на структуру SECURITY_ATTRIBUTES. Эта структура содержит информацию о правах доступа к объекту, в том числе и атрибут разрешающий наследование ссылки. Например для создания объекта синхронизации Event и получения ссылки на него используется функция CreateEvent. HANDLE CreateEvent( 43
LPSECURITY_ATTRIBUTES атрибуты
lpEventAttributes,
//
указатель
на
// защиты BOOL bManualReset, BOOL bInitialState, LPCTSTR lpName
// указатель на строку с именем объекта ); Первый параметр функции это указатель на структуру SECURITY_ATTRIBUTES. Структура имеет следующий формат: typedef struct _SECURITY_ATTRIBUTES { DWORD nLength; LPVOID lpSecurityDescriptor; BOOL bInheritHandle; } SECURITY_ATTRIBUTES; Структура размещается и инициализируется в вызывающей программе. Если полю bInheritHandle этой структуры присвоить значение TRUE ссылка которую вернёт функция CreateEvent в дальнейшем может быть наследована дочерними процессами. Кроме этого, при создании дочернего процесса следует разрешить наследование всех ссылок созданных в родительском процессе с атрибутом SECURITY_ATTRIBUTES. bInheritHandle = TRUE. Интерфейс функции CreateProcess содержит параметр bInheritHandles . Если он равен TRUE, созданный дочерний процесс может наследовать ссылки родительского процесса. Второй способ передачи ссылок из процесса в процесс называется дублирование. Процесс может создать копию своей ссылки на объект для другого процесса. При дублировании ссылки, необходимо указать для какого процесса она дублируется. Процесс с точки зрения операционной системы это так же объект типа kernel. Процесс, который дублирует свою ссылку на какой либо объект, должен указать ссылку на объект процесса, для которого производится дублирование. Процесс в системе можно идентифицировать не только по ссылке на его объект, но и по так называемому идентификатору процесса. В отличие от ссылки, идентификатор это глобальное значение однозначно определяющее процесс в любом контексте памяти. Ссылку и идентификатор процесса можно получить при его создании функцией CreateProcess. Функция OpenProcess возвращает ссылку на объект процесса по его идентификатору. Дублирование ссылок производится с помощью функции DuplicateHandle. BOOL DuplicateHandle( HANDLE hSourceProcessHandle, // ссылка на процесс в котором находится ссылка HANDLE hSourceHandle, // ссылка HANDLE hTargetProcessHandle, // ссылка на процесс для которого дублируется ссылка LPHANDLE lpTargetHandle, // указатель на дубликат ссылки DWORD dwDesiredAccess, // права доступа к дубликату ссылки BOOL bInheritHandle, // флаг наследования DWORD dwOptions //дополнительные флаги );
44
Многозадачность Windows. Автор: Сидякин И.М. Московский Государственный Технический Университет им. Н.Э. Баумана. Кафедра ИУ-3, (11.1998) Email:
[email protected] Процессы и потоки. Одним из основных понятий, которые вводятся при рассмотрении механизма организации мультизадачного режима исполнения программ в 32-х разрядных операционных системах MS Windows, является поток (thread). Поток это последовательность инструкций микропроцессора. Каждая работающая программа содержит как минимум один поток. Одновременно в системе может существовать несколько потоков, которые исполняются в отведённые им интервалы времени. Система выделяет эти интервалы времени каждому потоку периодически. Иначе говоря, система распределяет процессорное время между потоками. Продолжительность этих интервалов относительно небольшая (несколько десятков миллисекунд), что создаёт иллюзию параллельного исполнения потоков. Операция смены текущего исполняемого потока называется переключением потоков. Операционная система при поддержке аппаратуры сохраняет и восстанавливает параметры потоков при их переключении. Параметры каждого потока хранятся в специальной системной структуре, которая называется контекстом потока. Контекст потока в частности содержит значения регистров МП и стек потока на момент переключения потока. Для каждого приложения Win32 ОС создаёт отдельный процесс. Контекст процесса включает виртуальное адресное пространство приложения и ряд других системных ресурсов, которые используются потоками. Один или несколько потоков могут быть организованы внутри одного процесса. Такие потоки совместно используют память и другие ресурсы выделенные процессу. Переключение потоков, которые принадлежат различным процессам является более сложной процедурой чем переключение потоков, которые принадлежат одному процессу. Когда происходит переключение потоков принадлежащих разным процессам система должна изменить контекст процесса и выполнить ряд других дополнительных операций. Процессорное время ...
...
...
Поток 1
Поток 1
Поток 2
Поток 2
Поток 3
Процесс 1
Процесс 2
Рисунок 1. Общая концепция Процессов/Потоков. (Process/Thread) Каждый процесс должен содержать, по крайней мере, один поток который называется первым потоком (primary thread). Этот поток создаётся при создании процесса. Другие 45
потоки процесса могут быть созданы впоследствии из любого существующего потока процесса с помощью функций API. Совместная и вытесняющая мультизадачность. ОС Windows 3.x использует механизм, который называется совместная мультизадачность. 32-х разрядные ОС Windows поддерживают этот механизм, только для 16-ти разрядных приложений. Приложения Win32 используют механизм вытесняющей мультизадачности. В Windows 3.x отсутствуют понятия потока и процесса. Вместо них используется термин задача (task). Для каждого приложения Win16 создаётся отдельная задача, которая объединяет атрибуты процесса и потока. Приложение Win16 может иметь только одну последовательность инструкций - один поток. Таким образом, количество потоков в ОС Windows 3.x равно количеству запущенных программ или, что тоже самое, количеству запущенных задач. Другим, возможно более существенным недостатком совместной мультизадачности является то, что каждая задача самостоятельно определяет длительность интервала времени, в течение которого она исполняется. Переключение задач происходит после того, как задача завершает цикл обработки сообщений. Таким образом, временной интервал исполнения каждой 16-ти разрядной задачи, зависит от времени обработки сообщения. Любое приложение Windows 3.x может остановить процесс переключения задач, если его обработчик сообщений не возвращает управление. Это известная проблема 16-ти разрядных ОС Windows. Если одно из приложений зависает в результате ошибки, вместе с ним перестаёт функционировать вся система, потому, что другие приложения не получают процессорное время. Вытесняющая мультизадачность лишена этих недостатков. Переключение потоков производится операционной системой по «внутренним соображениям» вне зависимости от процесса обработки сообщений. Каждое приложение Win32 (процесс) может содержать один или несколько потоков. Это свойство позволяет более эффективно решать самые различные задачи. Например, с помощью известной программы Internet Explorer вы можете одновременно копировать несколько файлов по протоколу ftp и рассматривать страницы Internet. В данном случае операции записи файлов и просмотра страниц реализуются как отдельные потоки, принадлежащие процессу приложения Internet Explorer. Как правило, приложения используют первый поток для организации пользовательского интерфейса, а дополнительно созданные потоки для выполнения фоновых операций, например длительных операций копирования файлов, ожидания ввода данных и т.п. При разработке программных систем использующих свойства мультизадачности, более эффективным считается размещение отдельных потоков внутри одного процесса(программы), чем создание нескольких процессов (программ), которые содержат по одному потоку. На это есть по крайней мере три причины: Система быстрее выполняет переключение потоков, если они принадлежат одному процессу. В этом случае не требуется переключение контекста потока (коррекция таблиц страниц и другие операции) Все потоки принадлежащие одному процессу могут совместно использовать адресное пространство процесса и следовательно могут обращаться к глобальным переменным программы, что решает проблему обмена информацией между потоками. Все потоки, принадлежащие одному процессу могут совместно использовать системные ресурсы доступные процессу - файлы, объекты синхронизации и др. При всех достоинствах механизма мультизадачного исполнения программ, не следует, однако им злоупотреблять. Каждый поток и процесс использует ограниченные системные ресурсы: память, занимаемую структурами контекстов и время исполнения. Если, например, интервал времени исполнения потока равен 20мс и общее количество потоков в системе равно 40, то каждый поток будет исполняться 20мс и ожидать своёй очереди 20*39 = 780мс. Приоритеты потоков. Каждый поток в системе имеет приоритет. Это означает, что процессорное время распределяется между потоками не равномерно, в соответствии с приоритетами потоков. В системе всегда исполняется поток, который имеет наивысший приоритет. Потоки, имеющие более низкий приоритет, не могут получить управление до тех пор, пока более приоритетная операция не будет завершена или блокирована. Такой механизм в частности позволяет выполнять операции критичные ко времени исполнения. Приоритет потока определяется комбинацией двух значений: 46
Класс приоритета (priority class), который приписывается процессу, которому принадлежит поток Уровень приоритета (priority level), который назначается потоку в пределах класса приоритета процесса
Процесс (Process) Поток (Tread) Базовый приоритет = Класс приоритета процесса (Process Priority Class) + Уровень приоритета потока (Thread Priority Level)
Рисунок 2. Базовый приоритет. Комбинация этих значений называется базовым приоритетом потока ( base priority). Это число в диапазоне от 0 до 31. 0 - низший, 31 - высший приоритет. Следует отметить, что разделение базового приоритета на класс и уровень имеет место только в интерфейсах API функций, которые используются для управления потоками и процессами. Системное ПО нулевого кольца защиты работает непосредственно с базовым приоритетом. Классы приоритета. Существует шесть различных классов приоритета: IDLE_PRIORITY_CLASS BELOW_NORMAL_PRIORITY_CLASS NORMAL_PRIORITY_CLASS ABOVE_NORMAL_PRIORITY_CLASS HIGH_PRIORITY_CLASS REALTIME_PRIORITY_CLASS IDLE_PRIORITY_CLASS низший класс приоритета. Этот класс назначается процессам, которые исполняются в фоновом режиме и выполняют какие либо операции периодически и относительно редко. Пример такого процесса - хранитель экрана. Все процессы по умолчанию создаются с классом приоритета NORMAL_PRIORITY_CLASS. Класс приоритета может быть изменён при создании процесса или впоследствии. Не рекомендуется надолго увеличивать класс приоритета до HIGH_PRIORITY_CLASS так как это сокращает время выделяемое остальным процессам в системе. Кратковременное повышение приоритета используется, как правило, для исполнения операций критичных ко времени исполнения. Класс REALTIME_PRIORITY_CLASS используется в редких случаях, когда исполняемая операция столь критична ко времени, что на время её выполнения следует блокировать ряд системных потоков, включая потоки отвечающие за ввод с клавиатуры и мыши. Для создания процесса используется функция API CreateProcess. Класс приоритета задаётся параметром этой функции dwCreationFlags. Windows API включает дополнительно две функции GetPriorityClass и SetPriorityClass которые используются для определения и динамического изменения класса приоритета процесса. При вызове этих функций следует указать ссылку (handle) на процесс. Эту ссылку возвращает функция CreateProcess. BOOL SetPriorityClass( HANDLE hProcess, // ссылка на процесс DWORD dwPriorityClass // значение класса приоритета ); Уровни приоритета.
47
Уровень приоритета определяет приоритет потока в пределах заданных классом приоритета процесса. Уровень приоритета может принимать одно из следующих значений: THREAD_PRIORITY_IDLE THREAD_PRIORITY_LOWEST THREAD_PRIORITY_BELOW_NORMAL THREAD_PRIORITY_NORMAL THREAD_PRIORITY_ABOVE_NORMAL THREAD_PRIORITY_HIGHEST THREAD_PRIORITY_TIME_CRITICAL Все потоки создаются с уровнем приоритета THREAD_PRIORITY_NORMAL. Уровень приоритета может быть впоследствии изменён с помощью функции SetThreadProirity. Эта функция требует в качестве параметра ссылку (handle) на поток. Эту ссылку возвращает функция создания потока CreateThread. BOOL SetThreadPriority( HANDLE hThread, // ссылка на поток int nPriority // уровень приоритета потока ); Уровень приоритета потока можно получить с помощью функции GetThreadPriority. Базовый приоритет потока. Базовый приоритет потока может изменяться от 0 до 31. Базовый приоритет вычисляется как сумма класса приоритета и уровня приоритета. Комбинация THREAD_PRIORITY_IDLE и IDLE_PRIORITY_CLASS даёт базовый приоритет равный 1. Наивысший базовый приоритет (31) получается комбинированием класса REALTIME_PRIORITY_CLASS и уровня THREAD_PRIORITY_TIME_CRITICAL. Переключение потоков. Система принимает решение о переключении потоков на основании приоритета потока и состояния потока. Интервал времени выделяется потоку с наивысшим приоритетом. Исключением из этого правила является ситуация, когда поток, имеющий наивысший приоритет находится в блокированном состоянии (suspended state). Поток может быть блокирован по различным причинам. Например, поток ожидает результатов работы другого потока и логически не может продолжить исполнение, пока требуемые данные не будут подготовлены. Другой пример - ситуация, когда несколько потоков работают с одним глобальным ресурсом, например файлом и по логике работы, пока один из потоков не выполнит всех необходимых операций по работе с этим ресурсом, все остальные потоки должны ждать своей очереди. Система производит переключение потоков в случае если:
Интервал времени выделенный потоку истёк. Создан или разблокирован поток с более высоким приоритетом. Текущий исполняемый поток завершился или блокирован.
Системный диспетчер потоков scheduler использует специальные очереди для хранения контекстов потоков. Таких очередей тридцать две, по одной для каждого базового уровня приоритета. В случае, если интервал времени, выделенный потоку завершился (1), система помещает его контекст в конец очереди его приоритета. Затем сканирует очереди контекстов с убыванием приоритета. В процессор загружается контекст потока из начала первой непустой очереди. Это может быть и только что исполнявшийся поток. Контексты блокированных потоков не заносятся в очереди. В случае если в системе появляется поток, приоритет которого выше приоритета потока исполняемого в данный момент (2), текущий поток немедленно завершается и его контекст помещается в очередь соответствующую его приоритету. Контекст нового более приоритетного потока минуя очередь загружается в процессор и начинается исполнение этого потока. 48
1. Сохранить контекст потока закончившего
исполнение. 2. Поместить поток закончивший исполнение в
конец очереди потоков с этим приоритетом. 3.
Найти очередь с наивысшим приоритетом содержащую готовые к исполнению потоки.
4.
Удалить поток из начала очереди, загрузить его контекст, и начать его исполнение.
Рисунок 3. Алгоритм переключения задач (Scheduler algorithm). Блокировка потоков. Поток может находиться в состоянии готовности или в блокированном состоянии (suspended). Каждый блокированный поток удаляется из цикла переключения потоков. Поток может быть блокирован с момента создания, если он создаётся с атрибутом CREATE_SUSPENDED, или впоследствии с помощью функции SuspendThread. Поток блокируется также в случае ожидания ввода или объекта синхронизации. Объекты синхронизации используются, как следует из названия, для синхронизации исполнения потоков. Объект синхронизации это флаг, который может находиться в одном из двух состояний. Поток может ожидать переключения состояния объекта в блокированном состоянии. Разумеется, должен существовать другой поток, который способен изменить состояние объекта синхронизации. Контексты блокированных потоков исключаются из очередей диспетчера потоков и поэтому не используют процессорное время. Когда такой поток выходит из блокированного состояния, диспетчер потоков сравнивает его приоритет с приоритетом текущего исполняемого потока (текущим приоритетом). Если приоритет потока выше текущего, он передаётся на исполнение, иначе контекст потока записывается в очередь соответствующую его приоритету. Динамическое повышение приоритета (Priority boosts). Приоритет потока не является статическим параметром. Он может быть временно изменён операционной системой. Система динамически увеличивает приоритеты потоков базовые приоритеты которых меньше или равны 15 (16 это самый низкий базовый приоритет потоков принадлежащих процессам с классом приоритета REALTIME_PRIORITY_CLASS). Такое повышение приоритета производится в случае:
Когда приложение которому принадлежит поток помещается на передний план. Когда поток получает ввод (от клавиатуры, таймера или мыши) Когда выполняется условие ожидания объекта синхронизации.
После повышения система снижает приоритет потока на единицу каждый раз когда поток получает очередной интервал времени для исполнения. Это происходит до тех пор пока приоритет не станет равным своему первоначальному значению. В Windows NT имеется возможность разрешать/запрещать повышения приоритета потока отдельного потока или всех потоков заданного процесса. Для этого используются функции SetThreadPriorityBoost и SetProcessPriorityBoost. Инверсия приоритета (Priority inversion). Процессорное время получают только потоки с наивысшим приоритетом. Однако безусловное соблюдение этого правила может привести к нарушению функционирования программного обеспечения. На рисунке 4 приводится пример такой ситуации. 49
Поток 1 Высокий Приоритет
Поток 2 захватил всё процессорное время так как его приоритет выше приоритета Потока 3
Поток 1 ждёт пока поток 3 освободит общий ресурс.
Поток 2 Средний Приоритет Общий ресурс Поток 3 Низкий Приоритет
Поток 3 занимает общий ресурс и не может освободить его так как не получает процессорного времени
Рисунок 4. Инверсии приоритета (Priority inversion). Общим ресурсом может быть, например область памяти или файл. Операционная система имеет средства позволяющие потокам синхронизировать доступ к общему ресурсу. Необходимость в синхронизации возникает, например, в случае если несколько потоков не могут одновременно работать с общим ресурсом. Одна из схем синхронизации следующая: для ресурса создаётся флаг занятости (объект синхронизации) поток, прежде чем начать работу с ресурсом проверяет флаг занятости ресурса если флаг сброшен (ресурс свободен) поток устанавливает флаг занятости ресурса, работает с ресурсом и затем сбрасывает флаг занятости. если флаг установлен (ресурс занят, с ним работает другой поток) поток переходит в блокированное состояние до тех пор пока флаг не будет сброшен(пока ресурс не освободится), а затем захватывает ресурс. В приведённом примере Поток 3, который имеет низший приоритет, захватывает общий ресурс. Поток 1 должен ожидать пока ресурс не освободится. Но Поток 3 не имеет возможности сбросить флаг занятости ресурса потому что, система выделяет всё процессорное время Потоку 2 , который имеет более высокий приоритет, чем Поток 3. Таким образом, несмотря на то, что в системе имеется поток с более высоким приоритетом (Поток 1) в первую очередь исполняется Поток 2. Эта проблема решается разными методами в операционных системах Windows 95 и Windows NT. Диспетчер потоков NT периодически повышает приоритеты неблокированных потоков. Поток при этом выбирается случайно. В данном примере достаточно увеличить приоритет Потока 3 до приоритета Потока 2 или выше. Тогда Поток 3 получает возможность завершить работу с общим ресурсом и освободить его. Как только это произойдёт Поток 1 продолжит исполняться. Такое повышение приоритета, конечно, носит временный характер. ОС Windows 95 имеет механизм определения ситуаций, когда поток с высшим приоритетом зависит от потока имеющего более низкий приоритет. В этом случае система временно увеличивает приоритет низкоприоритетного пока до приоритета высокоприоритетного потока. Системы с несколькими процессорами. Windows NT может работать в вычислительной системе с симметричной многопроцессорной архитектурой. В такой системе потоки могут параллельно выполнятся на нескольких процессорах. Общая идеология переключения задач в таких системах имеет рассмотренную выше основу, однако, включает ряд особенностей. В Windows NT поток имеет дополнительный атрибут маску процессоров (affinity). Маска определяет подмножество микропроцессоров, на которых может исполняться поток. Этот атрибут задаётся функцией SetThreadAffinity. DWORD SetThreadAffinityMask ( HANDLE hThread, // ссылка на поток DWORD dwThreadAffinityMask // маска affinity 50
); Маска может быть задана сразу для всех потоков принадлежащих одному процессу. Для этого используется функция SetProcessAffinityMask. Кроме маски для потока можно программно задать так называемый идеальный процессор (ideal processor). Идеальный процессор это процессор на котором предпочтительно исполнение потока. Если в момент переключения потока свободно несколько процессоров, включая идеальный, поток будет исполняться на идеальном процессоре. Идеальный процессор назначается функцией SetThreadIdealProcessor. DWORD SetThreadIdealProcessor( HANDLE hThread, // ссылка на поток DWORD dwIdealProcessor // номер идеального процессора ); Вышеприведённые операции допустимы только в Windows NT. Windows 95 рассчитана на работу в системе с одним процессором. Создание потоков. При запуске приложения Win32 операционная система автоматически создаёт новый процесс и первый поток процесса. Все остальные потоки могут быть созданы из существующих потоков. Второй поток может быть создан из первого, третий из первого или второго и т.п. Нет никаких ограничений того, из какой точки программы создаётся новый поток. Windows API включает две основные функции для создания потоков: CreateThread и CreateRemoteThread. Первая функция создаёт поток в контексте процесса из которого она вызвана, а вторая может создать поток в любом заданном процессе (эта функция используется отладчиками). Рассмотрим интерфейс функции CreateThead. HANDLE CreateThread( LPSECURITY_ATTRIBUTES lpThreadAttributes, // указатель на атрибуты // защиты DWORD dwStackSize, // начальный размер стека потока LPTHREAD_START_ROUTINE lpStartAddress, // указатель на функцию //потока LPVOID lpParameter, // значение параметра функции потока DWORD dwCreationFlags, // флаги создания потока LPDWORD lpThreadId // указатель на возвращаемый идентификатор потока ); Функция возвращает ссылку на поток или NULL в случае ошибки. Первый параметр это указатель на структуру содержащую атрибуты защиты. Поток это объект типа kernel и как любой объект такого типа может иметь атрибуты защиты. Атрибуты защиты объекта потока в частности определяют, может ли ссылка на поток быть наследована дочерними процессами (см. ниже). Этот параметр необязателен и может быть равен NULL. dwStackSize определяет начальный размер стека потока в байтах. Если этот параметр равен нулю, по умолчанию создаётся стек размером 1MБ. Большая часть стека перед началом работы потока размещается в зарезервированной памяти. По мере заполнения стека система может увеличивать его размер и переводить память стека из зарезервированного в используемое (committed) состояние. LpStartAddress виртуальный адрес инструкции с которой начинается поток. Как правило, поток оформляется в виде функции которая имеет приведённый ниже интерфейс: DWORD WINAPI ThreadProc( LPVOID lpParameter // параметр ); где lpParameter значение, которое передаётся функции потока при запуске. Это значение может быть задано при вызове функции CreateThread. Смысл его определяется разработчиком. Например, это может быть указатель на глобальную 51
структуру данных для передачи информации между создаваемым и создающим потоком. Функция ThreadProc создаётся разработчиком, однако одна должна иметь указанный интерфейс. Имя этой функции, конечно, не имеет значения. Структура мультипоточного приложения достаточно проста. Каждый поток реализуется в виде функции. Функция запускается, как отдельный поток с помощью CreateThread. Одна и та же функция может быть запущена в нескольких потоках. DwCreationFlags может быть равен 0 или CREATE_SUSPENDED. В последнем случае поток создаётся в блокированном состоянии. Иногда возникает необходимость разделить во времени процедуры создания запуска потока. Для разблокирования потока следует вызвать функцию ResumeThread, которой требуется указать ссылку на объект потока. Кроме ссылки на объект потока CreateThread возвращает уникальный идентификатор потока в системе lpThreadId. Напомню, что объект потока принадлежит к объектам ядра (kernel), и ссылка на объект имеет смысл только в контексте процесса, к которому принадлежит поток. В отличие от ссылки идентификатор это глобальный ресурс. Объект поток может быть доступен из других процессов по его идентификатору. Некоторые API функции при работе с потоками используют идентификаторы вместо ссылок. Ниже приводится пример создания потока: //функция дополнительного потока DWORD WINAPI MyThreadFunction( LPVOID lpParam ) { //DebugMsg: Поток создан //здесь размещается код потока //DebugMsg: Поток завершается return 0; } //первый поток приложения VOID main( VOID ) { DWORD dwThreadId, dwThrdParam = 1; HANDLE hThread; //создать дополнительный поток hThread = CreateThread( NULL, // атрибуты защиты по умолчанию 0, // размер стека потока по умолчанию (1MB) MyThreadFunction, // адрес функции потока &dwThrdParam, // аргумент функции потока 0, // флаги = 0; запустить после создания &dwThreadId); // адрес для идентификатора // Проверка результата вызова функции. if (hThread == NULL) { // ошибка CloseHandle( hThread ); } В этом примере поток создаётся и немедленно запускается. Поток автоматически удаляется когда функция MyThreadFunction завершается. Функция CreateRemoteThread имеет дополнительный параметр hProcess - ссылку на процесс в котором создаётся поток.
52
Завершение работы потоков. Исполнение потока завершается, когда функция потока возврашает управление. Это нормальный и рекомендуемый способ завершения работы потока. Однако в ряде случаев применяются другие методы. Поток может быть завешён функциями ExitThread, TerminateThread или функциями ExitProcess, TerminateProcess. Последняя пара функций закрывает поток вместе с процессом, которому он принадлежит. Функции ExitXXX более «правильные». TerminateThread и TerminateProcess используются только в критических обстоятельствах, например при завершении потоков и процессов в случае ошибки, после которой нормальное функционирование программы невозможно. Когда поток завершается ор возвращает код выхода. Этот код может быть указан в параметрах команды return функции потока или в параметрах функций Exit/Terminate. Код выхода может быть получен с помощью функции GetExitCodeThread. Создание потоков в Visual C++ с использованием библиотеки классов Microsoft Foundation Classes (MFC). Библиотека Microsoft Visual C имеет ряд функций которые упрощают операции создания и управления потоками. В концепции MFC потоки разделяются на два типа: рабочие потоки (worker threads) и потоки имеющие пользовательский интерфейс (user-interface threads). Рабочие потоки реализуются в виде обычных функций, как было рассмотрено выше. Рабочие потоки предназначены для выполнения различных задач в фоновом режиме. Потоки, имеющие пользовательский интерфейс связаны с одним или несколькими окнами Windows. Примером такого потока является первый поток процесса (primary thread). В этом потоке работает оконный интерфейс программы и цикл обработки сообщений. Для создания потоков MFC предоставляет класс CThreadWnd и функцию AfxBeginThread. Эта функция имеет две версии интерфейса для создания потоков двух типов. Ниже рассматривается пример создания рабочего потока. Функция потока MyThreadProc имеет такой же вид как и в предыдущем примере. CWinThread * hThread; hThread = AfxBeginThread( (AFX_THREADPROC)MyThreadProc, pParam, nPriority, nStackSize, dwCreateFlags, lpSecurityAttrs); Параметры функции AfxBeginThread фактически те же самые что использовались при создании потока с помошью функции CreateThread. Это очевидно, так как AfxBeginThread вызывает функцию API CreateThread. Создание потоков в Delphi с помощью класса TThread. Borland Delphi в полном соответствии со своей объектно ориентированной моделью, предлагает класс TThread который так же как класс CThreadWnd в VC++ является надстройкой над функциями Windows API. Для создания потока (в том случае если вы не желаете использовать функции API) необходимо создать класс потомок TThread. Приведённый ниже пример иллюстрирует процедуру создания потока. type MyThread = class(TThread) private { Private declarations } protected {описание функции потока } procedure Execute; override; public Constructor Create(Parameter1 : LongInt; Parameter2 : Integer); end; Constructor MyThread.Create(Parameter1 : LongInt; Parameter2 : Integer); begin 53
{здесь производится инициализация полей объекта и затем вызывается конструктор предка } inherited Create(FALSE); end; {функция потока } procedure MyThread.Execute; begin { Здесь размещается код функции потока } end; Метод Execute класса MyThread, это тоже самое что функция MyThreadFunction в предыдущем примере. Поток, который создаёт этот объект, может передать через параметры конструктора любые данные. Конструктор класса TThread имеет только один параметр, который определяет флаг создания потока. Если этот параметр равен TRUE, поток создаётся в блокированном состоянии: constructor Create(CreateSuspended: Boolean); Поток может быть создан и запущен следующим образом: MyThread.Create(Par1, Par2); Псевдопотоки (Fibers). Система переключает обычные потоки автоматически. Однако существует особый класс потоков, которые переключаются программно. Эти потоки называются Fibers. Каждый поток может быть преобразован в файбер. Для этого используется функция ConvertThreadtoFiber. Эта функция имеет один необязательный параметр двойное слово, которое может быть передано файберу в момент при его создании. Функция возвращает указатель на файбер, который используется для управления им. После такого преобразования поток исключается из процедуры диспетчеризации, которую выполняет система. Для того чтобы файбер получил процессорное время необходимо вызвать функцию SwitchtoFiber. Файбер так же может быть создан с помощью функции CreateFiber. Однако эта функция может быть вызвана только из другого файбера. Таким образом, для того, чтобы создать несколько файберов необходимо сначала преобразовать существующий поток в файбер, а затем создать из него или созданных в последствии файберов другие. Файбер удаляется функцией DeleteFiber, которая использует в качестве входного параметра указатель на файбер полученный при его создании. Локальные переменные потоков. Потоки расположенные внутри одного процесса совместно используют его адресное пространство и следовательно имеют доступ к глобальным переменным приложения. Они работают с одним контекстом памяти. Например, операция myvar = 4; при условии, что myvar объявлена как глобальная переменная, записывает число 4 по одному и тому же физическому адресу в независимости от того, из какого потока приложения она вызывается. Иногда, однако, возникает необходимость размещения частных копий переменных для каждого потока. Если переменная myvar объявлена как локальная переменная потока, операция myvar = 4 приводит к записи числа в разные физические ячейки памяти в том случае, если она производится из разных потоков. Локальные переменные потоков объявляются в Visuаl C++ директивой __declspec(thread). Например: __declspec( thread ) int myvar = 1; Заметим, что этот способ не работает, если локальная переменная объявлена в динамической библиотеке, которая загружается программно функцией LoadLibrary. Описание динамических библиотек будет приведено позднее. Локальные переменные потоков также задаются с помощью функций API Tls. Этот способ будет описан при рассмотрении динамических библиотек. Компилятор размещает локальные переменные потоков в специальную секцию файла приложения или библиотеки. Эта секция имеет название .tls. Система переключает контекст памяти для этой секции каждый раз при переключении потоков, вне 54
зависимости от того принадлежат ли текущий и следующий поток одному или разным процессам. Система обеспечивает разные физические адреса для каждого потока в диапазоне виртуальных адресов секции .tls. Синхронизация потоков. Организация доступа к общим ресурсам и управление различными зависимостями между потоками является одной из наиболее важных проблем в режиме мультизадачного исполнения программ. Операционная система должна иметь средства синхронизации исполнения потоков. Потоки могут использовать совместно глобальные переменные приложения. Например, один из потоков, имеет право считывать значение глобальной переменной, только после того как другой поток выполнит операцию инициализации этой переменной. Такая операция может включать несколько инструкций МП. В этом случае доступ к такому общему ресурсу из других потоков должен быть запрещён до тех пор, пока данные не будут подготовлены. Если какая либо операция разделяется между несколькими потоками необходимо обеспечить механизм ожидания в ситуациях, когда одни потоки ждут промежуточных результатов работы других потоков. Потоки внутри одного процесса могут использовать глобальные переменные для организации флагов готовности. Например, один поток устанавливает такой флаг для индикации события, которого ждут другие потоки. Потоки, исполнение которых зависит от значения этого флага, периодически проверяют его состояние. При изменении состояния флага потоки выполняют предусмотренные действия. Этот метод обмена информацией между потоками называется поллинг (polling). //глобальная переменная BOOL ReadyFlag; Поток 1. ReadyFlag = False; //операция в процессе исполнения ReadyFlag = True; //операция завершена Поток 2 //Ожидание результатов операции while (ReadyFlag == FALSE); Этот метод прост, однако имеет, по крайней мере, два существенных недостатка:
Поток 2 занимает процессорное время не делая ничего, кроме проверки флага готовности. Этот метод просто реализуется в случае обмена информацией между потоками внутри одного процесса. Для того, чтобы потоки в разных процессах имели доступ к переменной ReadyFlag её необходимо поместить в общую память. (Например поместить её файл отображаемый в память MMF). Это требует дополнительных программных затрат.
Первый недостаток, конечно более важен. Поллинг может существенно снизить производительность в мультизадачной операционной системе, так как приводит к неэффективному использованию процессорного времени. К счастью имеется другой способ решения подобных задач. Механизм синхронизации потоков Windows использует два основных понятия Объекты синхронизации и Функции ожидания. Объекты синхронизации выполняют роль логического флага RadyFlag в приведённом выше примере. Объект синхронизации может находиться в одном из двух состояний: Signaled (как TRUE для ReadyFlag) и Nonsignaled (как FALSE). Однако объекты синхронизации обладают более широкими возможностями, чем глобальные переменные. Они могу быть доступны из разных процессов. Они так же имеют ряд полезных свойств, которые используются в специальных случаях. Поэтому существует несколько типов объектов синхронизации, каждый из которых используется для решения определённых задач. Объекты синхронизации совместно с функциями ожидания замещают опрецию поллинга на более приемлемый метод проверки и ожидания событий. Функция ожидания в этом методе играет роль цикла while. Однако процессорное время при ожидании 55
переключения состояния объекта синхронизации не тратится. Функция ожидания блокирует поток, из которого она вызвана до тех пор, пока условие ожидания не будет выполнено. Это условие выполняется, когда объект синхронизации переключается в состояние Signaled. Объекты синхронизации. Операционная система поддерживает ряд объектов предназначенных исключительно для синхронизации работы потоков. К ним относятся: события (еvents), мьютексы(Mutexes), семафоры (semaphores) и таймеры (waitable timers). Дополнительно некоторые другие объекты могут принимать состояния Signalled и NonSignalled. Например, объекты потоков и процессов. Объекты синхронизации относятся к классу объектов ядра ОС (kernel objects). Они имеют ряд общих атрибутов, таких как, уникальное имя и атрибуты защиты. Индикаторы событий (Events). Эти объекты используется для информирования потоков о возникновении какого либо события. Event используется в ситуациях подобных описанной выше в примере поллинга. Когда, например, один поток завершает операцию, которую ожидают другие, он может установить объект Event в состояние Signalled. Event создаётся или открывается функцией CreateEvent. Эта функция возвращает ссылку на объект. Event может иметь имя. Если имя объекта указано в параметре функции CreateEvent система пытается найти существующий объект с таким именем. В случае если объект уже существует, CreateEvent возвращает ссылку на объект, иначе она создаёт новый объект и так же возвращает ссылку на него. Назначение имени объекта имеет смысл, если требуется обеспечить доступ к нему из разных процессов или если в одном процессе требуется две различных ссылки, с разными правами доступа (права определяются в структуре атрибутов защиты при создании ссылки). Все задачи внутри одного процесса могут использовать для обращения к объекту одну ссылку. Рассмотрим подробнее интерфейс функции CreateEvent: HANDLE CreateEvent( LPSECURITY_ATTRIBUTES lpEventAttributes, // указатель на атрибуты // защиты BOOL bManualReset, // флаг программного сброса события BOOL bInitialState, // флаг начального состояния объекта LPCTSTR lpName // указатель на имя объекта ); bManualReset логический флаг который определяет метод сброса объекта в состояние NonSignaled. Программно или автоматически. Предварительно следует пояснить, как объект может изменить своё состояние. Установка в состояние signaled выполняется просто. Для этой цели можно использовать функции SetEvent и PulseEvent. В параметрах этих функций указывается ссылка на объект, полученная при его создании или открытии. Обе эти функции устанавливают объект Event в состояние signaled. Сброс более сложная операция, которая зависит от метода использования объекта. Во первых объект может быть переключён в состояние nonsignaled функцией ResetEven. Эта функция так же требует ссылку на объект. Во вторых, event может быть сброшен ,когда функция ожидания этого объекта возвращает управление. Функция ожидания работает следующим образом. Если объект находится в состоянии non-signaled, функция ждёт. Функция возвращает управление сразу после того, как объект переключается в состояние signaled. Когда поток вызывает функцию ожидания объекта Event он блокируется до тех пор пока какой либо другой поток не вызовет функцию SetEvent или PulseEvent для этого объекта. В двух случаях при возврате управления функцией ожидания объект будет переключён обратно в состояние non-signaled: если параметр bManualReset при создании объекта функцией CreateEvent был равен FALSE или если объект был переключён в состояние signaled функцией PulseEvent.
56
Поток 1
Поток 2
hEvent = CreateEvent(..имя)
hEvent = CreateEvent(..имя)
WaitForSingleObject(hEvent) Выполнение приостановлено в ожидании Event = signaled CloseHandle(hEvent)
Объект Event
SetEvent(hEvent) установка объекта в состояние signaled
CloseHandle(hEvent)
Рисунок 5. Управление объектом Event. Рассмотрим практический пример использования объекта Event. В одной из предыдущих глав приводился метод предотвращения загрузки второй копии приложения. Эта задача решается просто для приложений Win16, так как каждая копия такого приложения размещается в различных виртуальных адресах. Однако для приложений Win32 это не так. При запуске каждой копии приложения Win32 система создаёт отдельный процесс, при этом виртуальные адресные пространства процессов наложены друг на друга. Для определения того, что одна копия приложения Win32 уже загружена, можно использовать функцию FindWindow. Эта функция позволяет получить ссылку на какое либо (кроме дочерних) окно приложения по заголовку окна, т.е. позволяет проверить существует ли в системе окно с заданным заголовком. Таким образом, приложение при запуске перед созданием своего главного окна может проверить наличие окна с таким именем в системе и по результатам проверки продолжить или завершить исполнение. Однако это не вполне надёжная проверка. Вопервых, поиск по строке заголовка содержит определённый риск совпадения заголовков окон разных программ. Во вторых, между загрузкой приложения и созданием его окна проходит время, за которое может быть запущена вторая копия. Наиболее корректный способ решения этой задачи основан на использовании объекта синхронизации (например, события) и дополнительно функций FindWindow и SetWindowToForeground. HANDLE hEvent; HWND hWnd; hEvent = CreateEvent(NULL, FALSE, FALSE, «ApplicationEventName»); if (GetLastError() == ERROR_ALLREADY_EXISTS) { hWnd = FindWindow(NULL, «MyWindowName»); if (hWnd != NULL) SetWindowToForeground(hWnd); CloseHandle(hEvent); //завершить приложение ... } else //продолжить исполнение ... //конец программы CloseHandle(hEvent); Этот код следует разместить в самом начале программы (CloseHandle в самом конце) Только первая копия приложения создаст объект event. Вторая копия просто получит ссылку на существующий объект. Приложение может проверить действительное положение вещей с помощью функции API GetLastErrorFunction и завершить исполнение, в случае если объект уже был создан. Следует отметить так же и то, что полученная ссылка на объект должна быть обязательно закрыта при завершении работы копии приложения.
57
Взаимоисключения (Mutexеs). Mutex это аббревиатура термина mutual exclusion (взаимоисключение). Этот объект используется для организации защиты общих ресурсов от одновременного доступа из нескольких потоков. Мьютексу всегда ставится в соответствие (логически) какой либо ресурс, с которым одновременно имеет право работать только один поток. Мьютекс, который имеет имя может быть доступен из процессов, которым это имя известно и которые обладают достаточными правами доступа к объекту. Mutex использует концепцию владельца ресурса (owner). Ресурс, связанный с объектом может иметь одновременно только одного владельца. Объект имеет стандартную пару функций Create/Open.
Поток 1
Поток 2
hMutex = CreateMutex(..имя)
hMutext = CreateMutex(..имя)
WaitForSingleObject(hMutex) Выполнение приостановлено в ожидании Mutex = signaled
Объект Mutex
WaitForSingleObject(hMutex) Выполнение приостановлено в ожидании Mutex = signaled
ReleaseMutex(hMutex)
ReleaseMutex(hMutex)
CloseHandle(hMutex)
CloseHandle(hMutex)
Рисунок 6. Управление объектом Mutextes. Эти функции кроме операций создания и получения ссылки на объект могут использоваться для захвата ресурса. Замечу снова, что ресурс связан с мьютексом лишь логически. Ресурс в данном случае это нечто, доступное из нескольких потоков, при условии, что пока один поток является владельцем ресурса, другие потоки не имеют права с ним работать. Потоки могут захватывать ресурс и ожидать своей очереди с помощью функций ожидания. Далее в тексте термины владелец ресурса и владелец мьютекса, а так же захват мьютекса и захвата ресурса эквивалентны. Сценарий достаточно простой. Поток создаёт или открывает Мьютекс. При этом поток может сразу запросить статус владельца мьютекса. Однако если мьютекс уже имеет владельца, запрос игнорируется. После создания объекта для его захвата может быть использована одна из функций ожидания. Если мьютекс уже захвачен, функция ожидания блокирует исполнение потока до тех пор, пока текущий владелец не освободит объект. После этого функция захватывает мьютекс и возвращает управление. Если одновременно несколько потоков пытаются захватить мьютекс, они выстраиваются в очередь. После того как владелец мьютекса завершает работу с ресурсом, он обязан освободить мьютекс, для того чтобы его могли захватить другие потоки. Эта операция выполняется функцией ReleaseMutex. Ниже приводятся интерфейсы функций, которые используются для создания и управления мьютексом: HANDLE CreateMutex( LPSECURITY_ATTRIBUTES lpMutexAttributes, // указатель на атрибуты // защиты объекта BOOL bInitialOwner, // флаг захвата мьютекса при создании LPCTSTR lpName // указатель на сторку с именем объекта ); HANDLE OpenMutex( DWORD dwDesiredAccess, // флаги прав доступа BOOL bInheritHandle, // флаг разрешения наследования объекта 58
LPCTSTR lpName //указатель на сторку с именем объекта ); Функции возвращают ссылку на объект. bInitialOwner равен TRUE, если при создании объекта сразу же происходит его захват. Следует отметить, что если объект уже создан и захвачен, функция не ждёт пока он будет освобождён и немедленно возвращает управление. BOOL ReleaseMutex( HANDLE hMutex // ссылка на объект мьютекс ); Пример использования мьютекса: //Код потока HANDLE hMutex; int res; hMutex = CreateMutex(NULL, FALSE,
«MyMutexName»);
//запрашивается захват мьютекса. Если он уже имеет владельца, ждём 1 //секунду. if(WaitForSingleObject(hMutex, 1000) == WAIT_OBJECT_0) { //мьютекс захвачен //в этом месте поток является владельцем мьютекса //освобождение мьютекса ReleaseMutex(hMutex); //в этом месте владельцем мьютекса может стать другой поток } else // время ожидания (1 сек.) истекло или произошла какая либо ошибка. Семафоры (Semaphores). По принципу действия объект семафор похож на мьютекс. Однако с его помощью можно организовать одновременный доступ к ресурсу нескольких потоков. Семафор позволяет ограничить число таких потоков. Семафор имеет дополнительный атрибут – счётчик владельцев ресурса. Если значение счётчика равно нулю объект находится в состоянии non-signaled. Если значение счетчика больше нуля объект находится в состоянии signaled. При создании объекта счётчик устанавливается в заданное значение. Каждый раз, когда какой либо поток вызывает функцию ожидания, счётчик уменьшается на единицу. До тех пор пока значение счётчика выше нуля объект находится в состоянии signaled. Следовательно, функции ожидания немедленно возвращают управление и исполнение потоков продолжается. Как только значение счётчика уменьшится до нуля, объект переключается в состояние non-signaled и последующие вызовы функций ожидания будут блокировать вызывающие их потоки. Счётчик увеличивается на единицу каждый раз при вызове функции ReleaseSemaphore. Объект семафор удобно использовать для контроля доступа к ресурсу, который может быть обновременно доступен некоторому ограниченному числу пользователей.
59
Объект Semaphore Поток 1 hSemaphore = CreateSemaphore (N, имя); WaitForSingleObject (hSemaohore);
Счётчик = N Счётчик = N-1 Счётчик = N-2
ReleaseSemaphore (hSemaphore); CloseHandle (hSemaphore);
Поток 1 hSemaphore = CreateSemaphore (N, имя);
WaitForSingleObject (hSemaohore);
Счётчик = N-1 Счётчик = N
ReleaseSemaphore (hSemaphore); CloseHandle (hSemaphore);
Рисунок 7. Управление объектом Semaphore. Функции для работы с объектом семафор: HANDLE CreateSemaphore( LPSECURITY_ATTRIBUTES lpSemaphoreAttributes,//указатель на атрибуты //защиты LONG lInitialCount, //начальное значение счётчика LONG lMaximumCount, //максимально допустимое значение счётчика LPCTSTR lpName // указатель на сторку с именем объекта ); HANDLE OpenSemaphore( DWORD dwDesiredAccess, //флаги прав доступа BOOL bInheritHandle, // флаг разрешения наследования объекта LPCTSTR lpName // указатель на сторку с именем объекта ); BOOL ReleaseSemaphore( HANDLE hSemaphore, // сылка на объект LONG lReleaseCount, // значение добавляемое к счётчику LPLONG lpPreviousCount // адрес по которому возвращается предыдущее //значение счётчика );
60
Ждущие таймеры (Waitable timers). Объект таймер преключается в состояние signaled когда истекает заданный период времени. Имеется три различных типа таймеров. Программный Таймер (Manual-reset timer) сбрасывается в состояние non-signaled функцией SetWaitableTimer которая устанавливает временной интервал срабатывания таймера. Таймер Синхронизации (Synchronization timer) остаётся в состоянии signaled до тех пор, пока все функции ожидания применённые к нему не возвратят управление. Периодический таймер (Periodic timer) перезапускается каждый раз по истечении заданного временного интервала. Периодический таймер может быть таймером синхронизации или программным таймером. Объект таймера может быть создан или открыт соответственно функциями CreateWaitableTimer или OpenWaitableTimer. Ниже приводятся основные функции предназначенные для работы с ждущими таймерами. HANDLE CreateWaitableTimer( LPSECURITY_ATTRIBUTES lpTimerAttributes, // указатель на атрибуты // защиты BOOL bManualReset, //флаг программного сброса LPCTSTR lpTimerName //указатель на строку с именем объекта ); Пареметр bManualReset определяет тип таймера программный или таймер синхронизации. Интерфейс функции OpenWaitableTimer похож на интерфейс функций Open других объектов синхронизации. BOOL SetWaitableTimer( HANDLE hTimer, // ссылка на объект const LARGE_INTEGER *pDueTime, // время задержки LONG lPeriod, // период таймера PTIMERAPCROUTINE pfnCompletionRoutine, // процедура таймера LPVOID lpArgToCompletionRoutine, // параметр процедуры таймера BOOL fResume // флаг состояния таймера ); Функция SetWaitableTimer используется для активизации таймера и настройки его параметров. pDueTime указатель не переменную, которая задаёт интервал времени после которого таймер изменяет своё состояние. Время задаётся в единицах равных 100нс. Если этот параметр имеет отрицательное значение время считается относительно времени вызова функции, иначе время считается как “абсолютное” число 100 нс интервалов прошедшее с 1 января 1601 года. Это 64-х битовое целое значение типа FILETIME. lPeriod определяет период таймера. Таймер активизируется периодически до тех пор пока не будет вызвана функция CancelWaitableTimer. Если этот параметр равен нулю таймер не периодический. pfnCompletionRoutine необязательный параметр –адрес процедуры которая вызывается когда объект переходит в состояние signaled. Значение LpArgToCompletionRoutine передаётся этой процедуре при вызове. Флаг fResume используется для перевода вычислительной системы в состояние экономного потребления энергии (power-save mode) до тех пор пока таймер не будет переключён в состояние signaled. Это свойство может не поддерживаться системой. Критические секции (Critical sections). Критическая секция (КС) по принципу действия похожа на объект мьютекс. Разница между ними заключается в том, что мьютексы доступны потокам, которые принадлежат разным процессам, в то время как критические секции могут использоваться только внутри одного процесса. Критические секции используются для контроля совместного доступа к ресурсам общим для потоков одного процесса. Для работы с критическими 61
секциями вместо ссылок используются переменные типа CRITICAL_SECTION. Перед использованием критической секции её неодходимо проинициализировать с помощью функции InitializeCriticalSection. VOID InitializeCriticalSection( LPCRITICAL_SECTION lpCriticalSection // адрес объекта КС ); После инициализации поток может захватить КС функцией EnterCriticalSection и освободить её функцией LeaveCriticalSection. Эти функции имеют единственный параметр указатель на переменную CRITICAL_SECTION. Если секция уже захвачена другим потоком, функция EnterCriticalSection блокирует исполнение потока. Критические секции не используют функции ожидания применимые к другим объектам синхронизации. Функции ожидания. Основное назначение и принципы работы функций ожидания уже были проиллюстрированы рядом примеров. Они используются совместно с объектами синхронизации для организации обмена сообщениями между процессами и совместного доступа к ресурсам. Функция ожидания проверяет состояние объекта синхронизации и блокирует вызывающий поток до тех пор пока объект не переключается в состояние signaled. После этого функция возвращает упрвление и исполнение потока продолжается. Основное достоинство этого механизма состоит в том, что блокированные потоки исключаются из очередей диспетчера потоков и не используют процессорное время. Рассмотрим некоторые имеющиеся в Windows API функции ожидания.. Наиболее проста в использовании функция WaitForSingleObject: DWORD WaitForSingleObject( HANDLE hHandle, //ссылка на объект синхронизации DWORD dwMilliseconds //время ожидания в миллисекундах ); Первый параметр это ссылка на ранее созданный объект синхронизации. Второй параметр задаёт временной интервал ожидания изменения состояния объекта. Если объект находится в состоянии non-signaled, функция блокирует поток и по истечении указанного временного интервала возвращает ошибку. Если параметр dwMilliseconds равен нулю, функция просто проверяет состояние объекта и немедленно возвращает управление. Если задана константа INFINITE ( = -1) функция блокирует поток до тех пор пока объект не будет установлен в состояние signaled. Функция возвращает значения, которые используются для определения причины её завершения. Функция может вернуть одну из следующих констант: WAIT_FAILED – системная ошибка. Детальную информацию можно получить, вызвав функцию GetLastError. WAIT_ABANDONED - эта ошибка может возникнуть при работе с мьютексом, в случае если какой либо другой поток захватил мьютекс и завершился, не освободив его. Если функция вернула такое значение, поток становится новым владельцем мьютекса. WAIT_OBJECT_0 – условие ожидания выполнено. Объект был установлен в состояние signaled. WAIT_TIMEOUT – указанный временной интервал ожидания истёк, но объект по прежнему находится в состоянии non-signaled. Ниже приводится пример использования функции: HANDLE hEvent; DWORD WaitResult; //создать или открыть объект event hEvent = CreateEvent(NULL, FALSE, FALSE, «MyEventName»); ... 62
//ждём и перрываем ожидание в случае, если объект будет находится в //состоянии nonsignaled более 1й секунды WaitResult = WaitForSingleObject(hEvent, 1000); switch(WaitResult) { case WAIT_OBJECT_0: //объект установлен в состояние signaled. Ok break; case WAIT_TIMEOUT: //время ожидания истекло break; default: //ошибка ErrorCode = GetLastError(); } Иногда более полезно использовать функцию ожидания WaitForMultipleObjects. Эта функция позволяет наблюдать сразу за несколькими объектами. DWORD WaitForMultipleObjects( DWORD nCount, // количество записей в массиве ссылок объектов LPHANDLE pHandles, // указатель на массив ссылок объектов BOOL fWaitAll, // ждать всех или одного DWORD dwMilliseconds, // время ожидания в миллисекундах ); Перед вызовом этой функции все ссылки объектов синхронизации должны быть занесены в специально подготовленный массив. nCount определяет число записей в этом массиве. Функция (это определяется значением параметра fWaitAll), может ожидать изменение состояния одного или всех объектов указанных в массиве. В последнем случае функция не возвращает управление до тех пор, пока все объекты не изменят своё состояние на signaled. Функция может вернуть одно из следующих значений: WAIT_OBJECT_0 + n, где n это номер (считая от нуля) ссылки объекта который переключился в состояние signaled в массиве ссылок. Если одновременно несколько объектов находятся в состоянии signaled, возвращается наименьший номер. WAIT_ABANDONED + n, то-же для неосвобождённых мьютексов. WAIT_TIME_OUT и WAIT_FAILED имеют тот же смысл что и для функции WaitForSingleObject. Пример: HANDLE hEventArray[2]; DWORD WaitResult; //создать или открыть объекты event hEventArray[0] = CreateEvent(NULL, FALSE, FALSE, «MyEventName1»); hEventArray[1] = CreateEvent(NULL, FALSE, FALSE, «MyEventName2»); ... //ждём и прерываем ожидание в случае если оба объекта будут находится //в состоянии nonsignaled более 1й секунды WaitResult = WaitForMultipleObjects(2, hEventArray, FALSE, 1000); switch(WaitResult) { case WAIT_OBJECT_0: //event1 был установлен в состояние signaled. //возможно event2 был так же установлен в состояние signaled break; case WAIT_OBJECT_0 + 1: //event2 был установлен в состояние signaled. break; case WAIT_TIMEOUT: 63
//время ожидания истекло break; default: //ошибка ErrorCode = GetLastError(); } Мы рассмотрели две функции ожидания общего назначения. Windows API включает так же несколько функций ожидания которые используются в особых случаях. Перейдём к рассмотрению примера использования механизма синхронизации исполнения потоков. Приложение создаёт и запускает фоновый поток, который ожидает поступления данных и производит их обработку. Первый поток приложения управляет пользовательским интерфейсом программы. В его задачу может, например, входить отображение обработанных данных. Поток ввода производит низкоуровневые операции ввода данных и передаёт их по мере поступления фоновому потоку. Поток ввода и Фоновый поток совместно используют объект синхронизации InputEvent. С помощью этого объекта Поток ввода информирует Фоновый поток о наличии новых данных. CancelEvent используется совместно Первым потоком и фоновым потоком. Он используется для передачи Фоновому потоку команды прекращения работы. Эту команду формирует Первый поток, при завершении работы приложения. Потоки могут устанавливать объекты синхронизации в состояние signaled функцией SetEvent. Предполагается, что объекты автоматически сбрасываются в состояние nonsignaled при возврате из функций ожидания.
Основной поток (PrimaryThread). CreateThread(BackGroundThread) SetEvent(CancelEvent); Завершение работы потока BackGround из потока PrimaryThread
Объект Event CancelEvent
Фоновый поток (BackGroundThread) WaitForMultipleObjects(CancelEvent, InputEvent)
Объект Event InputEvent Данные готовы
Поток ввода (InputThread) SetEvent(InputEvent); Рисунок 8. Пример синхронизации потоков. Функция WaitForMultipleObject вызывается с параметром fWaitAll равным FALSE. Это означает, что функция возвращает управление, в случае если хотя бы один из объектов InputEvent или CancelEvent установился в состояние signaled. Массив ссылок, подготовленный для функции WaitForMultipleObjects, содержит две ссылки на объекты CancelEvent и InputEvent. Операция завершения работы фонового потока проверяется в первую очередь, так как ссылка на объект CancelEvent расположена первой в массиве. Функция вызывается так: 64
hEventArray[0] = CancelEvent; hEventArray[1] = InputEvent; while (TRUE) { //бесконечный цикл WaitResult = WaitForMultipleObjects(2, hEventArray, FALSE, INFINITE); if (WaitResult == WAIT_OBJECT_0) { //команда завершения работы (из первого потока) return 0; //завершаем Фоновый поток } else if (WaitResult == WAIT_OBJECT_0 + 1) { //получены новые данные от Потока ввода ... //обработка данных } } Приложение завершает исполнение фонового потока вызовом SetEvent(BackGroundEvent). Когда Поток ввода подготавливает новые данные, он сообщает об этом фоновому потоку вызовом SetEvent(InputEvent); Потоки и графический интерфейс. В приведённом выше примере подразумевается, что Фоновый поток должен сообщать Первому потоку приложения о наличии новых обработанных данных. Первый поток управляет пользовательским интерфейсом программы. Он может выполнять операцию отображения результатов обработки по мере их поступления. В примере организована одностороння связь Первого и фонового потока. Первый поток, используя объект CancelEvent, может передать команду завершения работы фоновому потоку. Дополнительно для обмена информацией между потоками допускается использовать глобальные переменные программы. Первый поток, например, может считывать данные, подготовленные фоновым потоком и помещённые им в глобальные переменные. Но первый поток должен получать сообщения всякий раз, когда появляются новые данные. В данном случае применение механизма, который используют Фоновый поток и Поток ввода вызывает затруднения. Это связано с тем, что первый поток отвечает за пользовательский интерфейс и не может ожидать в блокированном состоянии ввода данных от фонового потока. Можно решить эту задачу введением глобальной переменной - флага который Фоновый поток устанавливает при появлении новых данных а первый поток периодически проверяет (например, в цикле обработки сообщений). Это известный метод поллинга с недостатками, которые обсуждались выше. Более эффективный способ заключается в том, чтобы посылать сообщения какому либо окну приложения из фоновой задачи всякий раз когда появляются новые данные. Фоновая задача может использовать для этой цели функцию PostMessage. Эта функция помещает сообщение в очередь сообщений приложения и немедленно возвращает управление фоновому потоку, который продолжает исполнение. Позднее в интервал времени выделенный Первому потоку сообщение будет извлечено из очереди и передано для обработки указанному окну. PostMessage возвращает FALSE, если сообщение не может быть доставлено получателю. При вызове PostMessage следует указать ссылку на окно, которому отправляется сообщение. Первый поток может, например, сохранить эту ссылку в глобальной переменной или передать её фоновому потоку при его создании через параметр функции потока lpParameter (см. CreateThread).
65
Основной поток (Primary Thread). Окно nWnd
Обработчик сообщения
Очередь сообщений приложения
Фоновый поток WaitForMultipleObjects(CancelEvent, InputEvent) PostMessage(MY_MESSAGE_ID) Объект Event InputEvent Данные готовы
Поток ввода SetEvent(InputEvent); Рисунок 9. Связь фонового потока с основным через очередь сообщений. Следует отметить, что если скорость поступления данных выше чем скорость их отображения (или другой обработки) в Первом потоке очередь сообщений переполняется. В этом случае операционная система увеличивает размер очереди сообщений приложения. Этот процесс ограничен только ресурсами памяти вычислительной системы и в конце концов приводит к зависанию. Для того, чтобы это предотвратить, можно использовать флаг разрешения посылки сообщений: Вводится глобальная логическая переменная MsgEnable. Первоначально ей присваивается значение TRUE. Перед отправкой очередного сообщения фоновый поток проверяет этот флаг. Если значение флага равно TRUE оно инвертируется и затем посылается сообщение, иначе сообщение не посылается. Когда Первый поток получает сообщение он обрабатывает его и затем снова устанавливает флаг MsgEnable в TRUE разрешая фоновому потоку отправку очередного сообщения. Ниже приводится пример использования такой техники: Global data: BOOL MsgEnable = TRUE; //флаг разрешения передачи сообщений BYTE InputBlock[INPUT_BLOCK_SIZE]; //данные передаваемые первому //потоку HWND hWnd; // ссылка на окно приложения В фононовом потоке:
если WaitForMultipleObjects возвращает WAIT_OBJECT_0 + 1 читаем данные обрабатываем их и помещаем результат в глобальный массив InputBlock.
if (MsgEnable) { MsgEnable = FALSE; if (PostMessage(hWnd, MY_MESSAGE_ID, 0, 0) == FALSE ) MsgEnable = TRUE; //если сообщение не может быть отправлено //разрешаем посылку сообщений 66
} В первом потоке:
Выбирать окно приёмник сообщений от фонового потока. (Например, главное окно приложения) Создать функцию обработки пользовательского сообщения с кодом MY_MESASGE_ID. Запомнить ссылку на окно в глобальной переменной hWnd. Запустить фоновый поток функцией CreateThread. Каждый раз при получении сообщения с кодом MY_MESSAGE_ID считываем данные из массива InputBlock и выполняем необходимую обработку (например, отображение на экране). В конце обработчика необходимо установить флаг MsgEnable в TRUE для разрешения посылки следующих сообщений. В такой схеме без буферизации, конечно возможны потери данных. Они возникают в случае когда Первый поток не успевает обработать сообщение до прихода следующей порции данных от фонового потока.
Следует отметить, что фоновый поток должен присвоить флагу MsgEnable значение FALSE перед вызовом функции PostMessage. причина проста. Система может переключить потоки в любой момент времени. Если, например, имеется следующий код в фоновом потоке: if (PostMessage(hWnd, MY_MESSAGE_ID, 0, 0) != FALSE ) { MsgEnable = FALSE; } преключение на превый поток может произойти между этими двумя строками прогаммы. В этом случае PostMessage помещает сообщение в очередь Первый поток получает управление. (переключение потоков) Сообщение извлекается из очереди, передаётся окну и обрабатывается. В конце обработчика флагу MsgEnable присваивается значение TRUE. Фоновый поток получает управление. (переключение потоков) MsgEnable = FALSE; Таким образом флаг останется равным FALSE и посылка сообщений будет окончательно запрещена. Тупиковые ситуации (Deadlocks). Этот термин определяет одну общую проблему многозадачных систем. Тупиковые ситуации возникают в лучаях когда несколько потоков совместно используют общие ресурсы. Потоки захватывают ресурсы с помощью объектов синхронизации мьютексов, семафоров или критических секций. простейшее условие которое приводит к тупиковой ситуации показано на рисунке 10.
Поток 1
Поток 2
Захват ресурса A
Захват ресурса B
Ожидание ресурса B
Ожидание ресурса A
Рисунок 10. Пример тупиковой ситуации (Deadlock).
67
Синхронный и асинхронный Ввод/вывод. Windows API содержит ряд функций, которые предназначены для организации вводавывода. Такие функции используются, например, для записи и чтения файлов. Однако понятие ввода-вывода имеет более широкое значение. Оно включает операции обращения к различным устройствам ввода-вывода и ресурсам ОС. Функции вводавывода API используются для работы с последовательным портом, передачи данных по сети и доступа к драйверам аппаратуры. Операции ввода-вывода это способ связи приложений и драйверов устройств, которые могут передавать и принимать данные. Запись в файл подразумевает передачу команды записи и записываемых данных драйверу файловой системы, который выполняет эту операцию. Интерфейс чтения и записи данных одинаков для всех драйверов устройств ввода-вывода. При записи данных в последовательный порт задаются те же параметры (код команды записи и данные) что и при записи в файл. Эта информация будет передана драйверу последовательного порта. Высокоуровневый интерфейс передачи данных одинаков для различных устройств ввода-вывода. К основным функциям ввода-вывода относятся: ReadFile - чтение данных из файла, или какого либо устройства ввода-вывода и WriteFile -запись данных в файл или устройство ввода-вывода. Операции ввода-вывода деляться на два типа: синхронные и асинхронные ( используется также термин оverlapped). Синхронный вызов функции ввода-вывода возвращает управление после окончания операции. Асинхронный вызов возвращает управление немедленно. При этом операция ввода-вывода продолжает исполняться в фоновом режиме. Процесс исполнения операции скрыт от вызывающей программы, однако, она получает сообщение об окончании операции. Такой метод позволяет приложению использовать время исполнения асинхронной операции для выполнения какой либо другой работы. Поток вызывающий асинхронную операцию может перейти в блокированное состояние и не использовать процессорное время до тех пор, пока операция не будет выполнена. Асинхронный ввод-вывод используется при обращении к некоторым устройствам и для передачи данных по сети. Асинхронные вызовы применяются для организации обмена информацией между пользовательскими приложениями и драйверами устройств. Асинхронная операция должна быть поддержана со стороны драйвера и вызывающего приложения. Интерфейс драйверов устройств будет рассмотрен в соответствующей главе. Ниже приводится информация о том, как асинхронные операции ввода-вывода реализуются в пользовательских приложениях. Не все устройства ввода-вывода поддерживают асинхронные операции чтения и записи. Такой механизм ввода-вывода, как правило, применяется для работы с «медленными» устройствами. Примером такого устройства может служить последовательный порт с относительно низкой скоростью передачи данных. Исключение составляют операции чтения записи файлов, которые всегда выполняются синхронно. Доступ к файлу по сравнению ,например, с обращением к физической памяти можно так же считать медленной операцией, однако файловая система использует ряд методов для повышения быстродействия таких операций. Для чтения и записи файлов система использует кэширование данных. Операция записи в файл фактически приводит к записи данных в кэш и немедленному возврату из функции WriteFile. Система использует механизм поздней записи (lazy writing). Специальный системный поток переносит данные из кэш памяти на диск уже после того, как функция WriteFile вернула управление. Для операций чтения используется техника опережающего чтения (read-ahead). Система может считывать больше данных, чем требуется. При чтении файла в память попадают данные, которые вероятно потребуются при следующей операции чтения. Таким образом, большинство операций чтения и записи в файл фактически сводятся к быстрому копированию областей памяти. BOOL ReadFile( HANDLE hFile, // ссылка на открытый файл или увв LPVOID lpBuffer, // указатель на буфер для приёма данных DWORD nNumberOfBytesToRead, // размер буфера данный в байтах LPDWORD lpNumberOfBytesRead, // указатель на число принятых байтов LPOVERLAPPED lpOverlapped // структура для организации асинхронного //чтения ); 68
HFile это ссылка объект файла или устройства ввода-вывода. lpBuffer указатель на буфер для приёма данных, а nNumberOfBytesToRead размер этого буфера. Функция возвращает по адресу lpNumberOfBytesRead действительное число байтов, которое было передано в lpBuffer. lpOverlapped указатель на структуру, которая используется для организации асинхронного чтения. Если этот параметр равен нулю, операция выполняется синхронно. Асинхронные операции как и синхронные могут быть выполнены ещё до возврата из функции. Это происходит в случае, если драйвер устройства уже имеет данные готовые для передачи. В таком случае функция ReadFile возвращает TRUE. Иначе операция переходит в состояние запроса (pending) и передаётся на исполнение в фоновом режиме. При этом ReadFile возвращает FALSE. Функция может вернуть FALSE так же в случае ошибки. Если ReadFile возвращает FALSE, следует определить причину вызвав функцию GetLastError. Если GetLastError возвращает константу ERROR_IO_PENDING запрошенная операция чтения выполняется в фоновом режиме. Все эти проверки выполняются только в том случае, если операция задана как асинхронная (поле lpOverlapped указывает на правильно инициализированную структуру типа OVERLAPPED). Функция WriteFile имеет схожий интерфейс. Различие состоит в направлении передачи данных. BOOL WriteFile( HANDLE hFile, // ссылка на объект файла или увв LPCVOID lpBuffer, // указатель на буфер данных DWORD nNumberOfBytesToWrite, // размер буфера в байтах LPDWORD lpNumberOfBytesWritten, // указатель на число записанных //байтов LPOVERLAPPED lpOverlapped // структура для организации асинхронного //чтения ); lpOverlapped указывает на структуру OVERLAPPED, которая приводится ниже: typedef struct _OVERLAPPED { DWORD Internal; DWORD InternalHigh; DWORD Offset; DWORD OffsetHigh; HANDLE hEvent; } OVERLAPPED; Поля Offset и OffsetHigh соответственно младшее и старшее двойное слово 64-х битового смещения в файле для операций чтения и записи. Эти значения не используются для обмена данными с устройствами ввода-вывода. Поля Internal зарезервированы для использования системой. Наиболее важный параметр для нас это hEvent. Это ссылка на объект синхронизации Event который должен быть предварительно создан приложением. Приложение передаёт ссылку на объект синхронизации драйверу через структуру OVERLAPPED при вызове функции ReadFile или WriteFile. Функция возвращает управление немедленно. Драйвер сохраняет ссылку на объект до конца операции и использует объект для индикации её завершения. Приложение может проверить состояние объекта с помощью одной из функций ожидания и таким образом определить момент когда операция будет в действительности завершена. Обычно для этой цели используется функция GetOverlappedResult. Эта функция ожидания предназначена специально для организации асинхронного ввода-вывода. BOOL GetOverlappedResult( HANDLE hFile, // ссылка на объект увв LPOVERLAPPED lpOverlapped, // адрес структуры overlapped LPDWORD lpNumberOfBytesTransferred, // указатель на число переданых //байт BOOL bWait // флаг режима ожидания ); Эта функция не позволяет указать время ожидания. Вместо этого праметр bWait определяет два возможных режима работы: bWait = FALSE -> время ожидания = 0, 69
bWait = TRUE -> время ожидания = INFINITE. LpNumberOfBytesTransferred указывает на переменную, в которую возвращается действительное число переданных байт. Функция может быть использована как при записи, так и при чтении данных. Устройство, которое поддерживает асинхронные операции должно быть открыто с флагом FILE_FLAG_OVERLAPPED. Это указывается в параметре dwFlagAttributes функции CreateFile. Функция CreateFile используется для открытия и получения ссылки файл или усторйство ввода-вывода. Следующий пример иллюстрирует чтение данных из COM порта. OVERLAPPED Ovl; HANDLE hEvent; HANDLE hFile; //открыть com1 hCom = CreateFile( "COM1", GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, FILE_FLAG_OVERLAPPED, NULL); //создать объект event hEvent = CreateEvent(NULL, FALSE, FALSE, NULL); // инициализация структуры overlapped Ovl.Offset = 0; Ovl.OffsetHigh = 0; Ovl.hEvent = hEvent; bResult = ReadFile(hCom, &inBuffer, nBytesToRead, &nBytesRead, &Ovl) ; //функция возвращает FALSE если операция в состоянии запроса //или в случае ошибки if (!bResult) { switch (dwError = GetLastError()) { case ERROR_IO_PENDING: // асинхронная операция выполняется // делаем что либо GoDoSomethingElse() ; // проверка результатов асинхронного чтения bResult = GetOverlappedResult(hFile, &gOverlapped, &nBytesRead, TRUE); // в случае ошибки ... if (!bResult) { // обрабатываем код ошибки } else { // данные приняты асинхронно } break; default: //другая ошибка } } else //данные приняты синхронно Создание дочерних процессов. Запуск приложений. Для запуска приложения необходимо создать новый процесс. Существует несколько способов запуска приложений. Все способы в конечном итоге сводятся к вызову функции CreateProcess. Однако в простейших случаях, когда не требуется использовать широкие возможности этой функции можно использовать функцию WinExec. UINT WinExec( LPCSTR lpCmdLine, // указатель на командную строку UINT uCmdShow // стиль главного окна приложения ); В параметрах этой функции указывается путь и имя файла приложения вместе с командной строкой, а также стиль отображения uCmdShow главного окна приложения. 70
Функция WinExec для создания процесса вызывает CreateProcess с заданным по умолчанию набором параметров. Интерфейс функции CreateProcess приводится ниже. BOOL CreateProcess(LPCTSTR lpszImageName, LPCTSTR lpszCommandLine, LPSECURITY_ATTRIBUTES lpsaProcess, LPSECURITY_ATTRIBUTES lpsaThread, BOOL fInheritHandles, DWORD fdwCreate, LPVOID lpvEnvironment, LPTSTR lpszCurDir, LPSTARTUPINFO lpsiStartInfo, LPPROCESS_INFORMATION lppiProcInfo) Эта функция создает процесс приложения имя файла которого задаётся первым параметром lpszImageName. Если строка содержит только имя файла без пути к нему CreateProcess последовательно пытается найти файл с указанным именем в текущем каталоге, в каталоге Windows\System directory затам в каталоге Windows и наконец в каталогах указанных в блоке переменных среды окружения. LpszCommandLine задаёт командную строку которая передаётся как аргумент при вызове функции WinMain процесса. Процесс может получить ссылку на эту строку с помощью функции GetCommandLine. Третий и четвертый параметры указывают на структуры с атрибутами защиты объектов соответственно процесса и первого потока процесса. Объекты процессов и потоков принадлежат к типу объектов ядра (kernel) и имеют, как и другие объекты этого типа, атрибуты защиты SECURITY_ATTRIBUTES. (Подробнее см. предыдущую главу). Атрибуты защиты процесса и его первого потока задаются отдельно. FInheritHandles логический флаг, который разрешает наследование создаваемым процессом ссылок, которые доступны создающему процессу. DwCreationFlags содержит набор флагов, которые задают состояние процесса и первого потока после его создания (например, CREATE_SUSPENDED) а так же класс приоритета процесса. LpEnvironment указатель на блок переменных среды окружения, который содержит строки среды окружения процесса. Если этот параметр равен нулю, создаваемый процесс получает блок переменных среды окружения создающего процесса. LpszCurDir задаёт рабочий каталог процесса. Если этот параметр равен нулю, используется текущий каталог. LpsiStatrInfo указатель на следующую структуру: typedef struct _STARTUPINFO { DWORD cb; LPSTR lpReserved; LPSTR lpDesktop; LPSTR lpTitle; DWORD dwX; DWORD dwY; DWORD dwXSize; DWORD dwYSize; DWORD dwXCountChars; DWORD dwYCountChars; DWORD dwFillAttribute; DWORD dwFlags; WORD wShowWindow; WORD cbReserved2; LPBYTE lpReserved2; } STARTUPINFO, *LPSTARTUPINFO; Некоторые поля этой структуры используются только для процессов графических приложений. Некоторые применяются только для процессов консольных приложений. Пары dwX - dwY и dwXSize - dwYSize определяют начальную позицию на экране и размер главного окна приложения. wShowWindow (этот параметр так же используется функцией WinExec) задаёт вид и состояние главного окна приложения. Для этого 71
параметра имеется несколько предопределённых констант. Например, SW_HIDE, SW_MINIMIZE, SW_SHOW SW_MAXIMIZED. dwFlags комбинация констант, которые определяют какие из полей структуры содержат заданные значения. Например, если необходимо задать позицию главного окна приложения эта позиция заносится в поля dwX и dwY, и дополнительно к полю dwFlag добавляется константа STARTF_USERPOSITION. Функция CreateProcess копирует структуру PROCESS_INFORMATION в буфер на который указывает параметр lppiProcInfo. typedef struct _PROCESS_INFORMATION { HANDLE hProcess; HANDLE hThread; DWORD dwProcessId; DWORD dwThreadId; } PROCESS_INFORMATION; Эта структура содержит пары ссылок и идентификаторов процесса и первого потока процесса. Ссылки процесса и потока могут быть закрыты до завершения работы созданного процесса. Лучше всего сделать это сразу после вызова функции CreateProcess. В этом случае объекты процесса и потока не будут уничтожены. Однако ссылки могут быть использованы для доступа к созданному процессу. Например, создающий процесс может по ссылке определить, когда созданный процесс завершил исполнение. Объект процесс не предназначен непосредственно для решения задач синхронизации исполнения потоков, но всё же имеет атрибут состояние. Он может находиться в состоянии signaled или non-signaled. Во время работы процесса его объект находится в состоянии non-signaled и непосредственно перед завершением процесса переходит в состояние signaled. Состояние объекта можно проверить с помощью любой функции ожидания.
72
Динамические библиотеки. Автор: Сидякин И.М. Московский Государственный Технический Университет им. Н.Э. Баумана. Кафедра ИУ-3, (11.1998) Email:
[email protected] Динамические библиотеки (DLL) содержат код, данные или ресурсы которые могут быть подключены к приложениям во время исполнения. Такие библиотеки размещаются в файлах с расширением .dll. DLL обладают рядом преимуществ по сравнению со статическими библиотеками. Использование DLL приводит к экономии дискового пространства, так как несколько приложений могут одновременно использовать одну и ту же динамическую библиотеку. Другим преимуществом является то, что при внесении изменений в DLL не требуется перекомпиляция приложений, которые используют библиотеку. Файлы 32-х разрядных приложений Windows (.exe) и динамических библиотек (.dll) имеют одинаковый формат. С системной точки зрения приложения отличаются от динамических библиотек в нижеследующем::
Система создаёт новый процесс при загрузке приложения (EXE). Динамическая библиотека загружается в адресное пространство процесса, который использует функции DLL. Приложение поддерживает обработку сообщений. Каждое 32-х разрядное приложение имеет свою собственную очередь сообщений и код реализующий цикл обработки сообщений. DLL предоставляет только набор функций, которые могут быть вызваны из приложения или другой библиотеки. Обычно с приложением связаны одно или несколько окон. DLL может содержать или не содержать элементы пользовательского интерфейса.
Как уже было отмечено, система не создаёт процесс при загрузке DLL, вместо этого код и данные DLL отображаются в адресное пространство процесса, который использует библиотеку. Эта операция производится для каждого процесса, который обращается к функциям DLL.
Физическая память и диск Виртуальное адресное пространство Приложения 1
Код приложения
Приложение 1
Приложение 2
Код DLL общие данные частные данные
Виртуальное адресное пространство Приложения 2
Код приложения
Код DLL DLL общие данные частные данные 1 частные данные 2
общие данные частные данные
Рисунок 1. Отображение кода и данных DLL в адресное пространство процесса. 73
На рисунке 1 показано, как код и данные DLL отображаются в адресное пространство двух процессов, которые используют эту динамическую библиотеку. Данные, размещаемые в DLL, могут быть общими или частными. Виртуальные адреса общих переменных отображаются в одинаковые физические адреса для всех процессов использующих DLL. Виртуальные адреса частных данных (per process, instance data) отображаются в разные физические адреса для разных процессов. Динамическая компоновка. Динамическая компоновка это метод позволяющий приложению получить адрес внешней функции на этапе исполнения. В данном случае внешняя функция это функция которая расположена в каком либо другом исполняемом модуле (EXE или DLL). Так как код и данные DLL загружаются в адресное пространство процесса приложения, любая экспортируемая DLL функция может быть вызвана из приложения. Основная проблема состоит в том, что адрес функции зависит от того, в какое место виртуального адресного пространства процесса загружается DLL. Другими словами, этот адрес может быть определён только после того, как динамическая библиотека будет загружена. Для более детального знакомства с вопросом рассмотрим некоторые особенности формата 32-х битовых исполняемых файлов Windows. Этот формат носит название Windows Portable Executable File Format (сокращённо PE формат). Это формат 32разрядных файлов EXE, DLL и OBJ. Следует отметить, что операционная система использует технологию отображения файлов в память(Memory Mapped Files) для загрузки кода и данных исполняемого модуля в память. Эта технология позволяет упростить доступ к коду, данным и служебным таблицам, расположенным в файле. В действительности структура и содержание файла исполняемого модуля почти не меняется при его загрузке в память. Файл разделяется на несколько секций, которые содержат код приложения, данные (общие и частные отдельно), ресурсы и служебную информацию. При загрузке приложения система создаёт в памяти образы отдельных секций файла. После этого к содержимому секций файла можно обращаться командами, предназначенными для работы с памятью (mov и т.п). T.е. работа с файлом в дальнейшем ничем не отличается от работы с памятью. После создания образа файла система должна выполнить несколько дополнительных операций, включая инициализацию указателей на функции расположенные во внешних модулях динамических библиотеках. Исполняемый файл может содержать одну или две секции, использующиеся для динамической компоновки. Эти секции: .idata содержащая информацию необходимую для импортирования функций из внешних модулей и .edata которая используется для обратной операции и описывает функции экспортируемые модулем. Эти секции так же называются Таблица Импорта (import table) и Таблица Экспорта (export table). Секции размещаются в файле во время компиляции. С помощью Таблицы Импорта система определяет, какие внешние модули, и какие функции в этих модулях используются приложением. Затем, используя информацию Таблиц Экспорта в указанных модулях, система получает адреса требуемых функций и записывает эти адреса в специально отведённые поля Таблицы Импорта. Импорт функций. Код приложения размешается в exe файле в секции с именем .text. Вызов внешней процедуры из этого кода происходит не прямо а косвенно. Компилятор подставляет в инструкцию Call адрес команды Jmp Dword Ptr [XXXXXXXX] Эта команда так же расположена в секции .text модуля. В свою очередь XXXXXXXX это указатель, расположенный в секции .idata (в Таблице Импорта). Заметим, что указанные вставки делает компилятор. Значение указателя это адрес внешней функции. Этот адрес заносится в Таблицу Импорта при запуске приложения и загрузке динамической библиотеки, которая содержит внешнюю функцию. Компилятор заменяет прямой вызов внешней функции вызовом по адресу инструкции Jmp и размещает служебную таблицу адресов которые подставляются в параметры инструкций Jmp. Каждая внешняя функция имеет поле для записи адреса в секции .idata и свой собственный Jmp в секции .text. При загрузке динамической библиотеки система инициализирует Таблицу Импорта реальными значениями адресов функций DLL. (Рисунок 2) 74
Приложение
АдресY
DLL
АдресZ .idata
АдресX
Jmp Dword Ptr [АдресY]
внешняя функция
.text
Call АдресX (вызов внешней функции)
Рисунок 2. Вызов функции из другого модуля. Секция idata содержит несколько таблиц. В первой таблице записаны структуры типа IMAGE_IMPORT_DESCRIPTOR. Каждая запись в таблице соответствует одной из динамических библиотек подключённых к приложению. Структура IMAGE_IMPORT_DESCRIPTOR включает следующие поля: Name - адрес ASCIIZ строки содержащей имя импортируемой DLL. Два поля структуры, Characteristics и FirstThunk, содержат адреса двух массивов указателей. Массив на который указывает поле Characteristic называется HintName, а массив на который указывает поле FirstThunk называется Таблица Импортируемых Адресов (Import Address Table IAT). Первоначально оба массива содержат указатели на структуры типа IMAGE_IMPORT_BY_NAME. Каждая структура описывает одну из внешних функций. typedef struct _IMAGE_IMPORT_BY_NAME { WORD Hint; BYTE Name[1]; } IMAGE_IMPORT_BY_NAME; Hint или индекс это уникальный номер присвоенный функции. Этот номер может быть назначен программно при проектировании модуля экспортирующего функцию. Name это ASCIIZ строка содержащая имя функции. Перед инициализацией адреса каждой структуры IMAGE_IMPORT_BY_NAME имеются в двух таблицах. Один в массиве HintName а другой в Таблице Импортируемых Адресов. Система при загрузке приложения через IAT получает ссылки на имя и индекс функции. По этим параметрам загрузчик может определить адрес функции. Загрузчик заменяет в таблице IAT указатели на структуры IMAGE_IMPORT_BY_NAME адресами функций. Таким образом, в файле исполняемого модуля каждая запись в таблице IAT указывает на одну из структур IMAGE_IMPORT_BY_NAME, но после загрузки модуля записи этой таблицы прямо указывают на внешние функции. Другая таблица HintName не модифицируется в процессе загрузки модуля и по прежнему указывает на структуры IMAGE_IMPORT_BY_NAME. Команды Jmp о которых говорилось выше выполняют передачу управления по адресам записанным в Таблице Импортируемых Адресов (IAT). В команде Jmp Dword Ptr [XXXXXXXX] XXXXXXXX это адрес записанный в соответствующем элементе таблицы IAT.
75
Image Import Descriptor (IID) Characteristics Имя импорт. DLL FirstThunk Следующий IID
Массив HintName
Структуры Image Import by Name Индекс функции Имя функции Индекс функции Имя функции Индекс функции Имя функции Индекс функции Имя функции
Имя DLL (ASCIIZ)
Import Address Table
Эта таблица перезаписывае тся загрузчиком
Рисунок 3. Таблица импортируемых функций (Import Table). Такая технология позволяет настроить вызовы внешней функции из разных мест программы просто записав адрес функции в Таблицу Импортируемых Адресов. Экспорт функций. Исполняемый модуль, который экспортирует функции, должен иметь таблицу экспорта. Эта таблица используется загрузчиком для определения адреса экспортируемой функции по её имени или индексу. Таблица экспорта размешается в секции файла .edata. Эта секция размещается в файле исполняемого модуля (DLL) компилятором. Следует заметить, что exe файл тоже может содержать секцию .edata. Эта секция содержит три параллельных массива с именами, индексами и адресами экспортируемых функций, а так же таблицу (каталог) которая содержит ссылки на перечисленные массивы. Формат записи каталога описывается структурой IMAGE_EXPORT_DIRECTORY. Некоторые поля этой структуры приведены ниже: Name - указатель на ASCIIZ строку содержащую имя DLL. Base - начальный индекс экспортируемых функций. Каждой функции может быть присвоен уникальный индекс. Индексы функций располагаются в массиве AddressOfNameOrdinals. Действительный индекс функции, по которому она может быть найдена, вычисляется как сумма поля Base и значения записи в массиве индексов, соответствующего этой функции. Поля NumberOfFunctions и NumberOfNames определяют общее количество функций экспортируемых модулем. Значения этих полей идентичны. AddressOfFunctions указатель на массив адресов функций. AddressOfNames указатель на массив указателей на строки. Строки содержат имена экспортируемых функций. AddressOfNameOrdinals указатель на массив индексов функций. Это массив слов (WORD). Информация о функциях размещается в трёх отдельных массивах. Для того чтобы получить имя, индекс и адрес функции требуется просмотреть все три массива. Данные относящиеся к одной функции естественно сохраняются в элементах массивов под одинаковыми номерами. Структура Таблицы Экспорта приведена на рисунке 4.
76
IMAGE_EXPORT_DIRECTORY Другие поля . . . NumberOfFunctions NumberOfNames AddressOfFunctions AddressOfNames AddressOfNameOrdinals
3 параллельных массива Таблица адресов функций Адрес1 Адрес2 Адрес3 Таблица имён функций MyFunc1
MyFunc2
MyFunc2
Таблица индексов функций 1 2 3
Рисунок 4. Таблица экспортируемых функций (Export Table). Динамическая компоновка (загрузка библиотеки и подстановка адресов функций) может быть выполнена прямо (implicitly) или косвенно(explicitly). Косвенная компоновка производится системой при загрузке приложения, так как это было описано выше. Windows предоставляет дополнительно набор функций которые позволяют загрузить и выгрузить динамическую библиотеку а так же получить адрес требуемой функции в загруженной библиотеке. Это прямая компоновка. Прямая компоновка (Explicit loading). Динамическая библиотека может быть загружена функцией LoadLibrary. HINSTANCE LoadLibrary( LPCTSTR lpLibFileName );
// имя файла динамической библиотеки
Эта функция возвращает ссылку (instance) на DLL. Для 32-х битовых DLL instance это базовый виртуальный адрес области памяти, в которую система загрузила DLL. Это значение используется как параметр в других API функциях работающих с DLL. Функция FreeLibrary используется для выгрузки DLL. Функция имеет единственный параметр - instance возвращаемый предыдущей функцией. Заметим, что подобно другим объектам Windows DLL загружается в память (создаётся) только при первом вызове LoadLibrary. Последующие вызовы этой функции из одного процесса возвращают то же значение instance, что и первый вызов, и увеличивают счётчик загрузки DLL. Напротив, каждый вызов FreeLibrary уменьшает этот счётчик, и когда он станет равным нулю, система выгружает DLL. После загрузки библиотеки следует определить адрес нужной библиотечной функции. Для этого используется функция API GetProcAddress. FARPROC GetProcAddress( HMODULE hModule, // ссылка на DLL (instance) LPCSTR lpProcName // имя функции ); FARPROC это стандартный тип указателя на функцию. hModule параметр instance возвращённый функцией LoadLibrary и lpProcName указатель на имя функции. Адрес функции может быть так же получен по её индексу. В этом случае lpProcName интерпретируется не как указатель на строку, а как значение типа DWORD, старшее слово которого равно нулю а младшее содержит индекс функции. GetProcAddress интерпретирует lpProcName как индекс функции если старшее слово равно нулю. Следующие примеры на языках Pascal (Delphi) и C иллюстрируют приёмы работы с функцией GetProcAddress: Пример 1. Pascal. 77
Var {указатель на импортируемую функцию} MyDLLFunction : function (A, B : Integer) : Integer; {DLL handle} HANDLE : hDLL; xA, xB xC : Ineger; ... {загрузить dll} hDLL := LoadLibrary(‘DLLName’); {получить адрес функции с именем MyFunction из dll} @MyDLLFunction := GetProcAddress(hDLL, ‘MyFunction’); {вызвать функцию} xC := MyDLLFunction(xA, xB); {выгрузить dll} FreeLibrary(hDLL); Пример 2. C: typedef
UINT (CALLBACK* LPMYFUNCTION)(int,
int);
HINSTANCE hDLL; // ссылка на DLL LPMYFUNCTION MyDLLFunction; // указатель на функцию int xA, xB, xC; //загрузить dll hDLL = LoadLibrary(«DLLName»); //получить адрес функции с именем MyFunction из dll MyDLLFunction = (LPMYFUNCTION)GetProcAddress(hDLL, "MyFunction"); //вызвать функцию xC = MyFunction(xA, xB); //выгрузить dll FreeLibrary(hDLL); LoadLibrary и GetProcAddress возвращают 0 в случае ошибки. Косвенная компоновка (Implicit loading). Как правило, для подключения динамических библиотек к приложению используется косвенная компоновка. Этот тип компоновки основан на описанной выше технологии использующей таблицы импорта и экспорта. Библиотека автоматически подключается к приложению во время загрузки последнего. Для организации косвенной компоновки требуется внести ряд дополнений в исходный текст приложения и динамической библиотеки. Языки программирования C и Pascal имеют специальные директивы компилятора для размещения и инициализации таблиц импорта и экспорта. Delphi. В Delphi для объявления импортируемых функций используется директива external. Функции можно импортировать по имени, по имени по умолчанию и по индексу. Во всех случаях функция должна быть объявлена с использованием директивы external. Импорт по имени: Function MyDLLFunction(A, B : Integer) : Integer; external «DLLName» name «MyFunction»; Имя функции в динамической библиотеке «MyFunction» но в исходном тексте приложения используется имя «MyDLLFunction» Импорт по имени по умолчанию: Function MyFunction(A, B : Integer) : Integer; external «DLLName»; Имена функции в DLL и в тексте приложения одинаковы. 78
Импорт по индексу: Function MyFunction(A, B : Integer) : Integer; external «DLLName» index 10; Индекс функции в DLL равен 10. В тексте приложения используется имя «MyFunction». Как правило при разработке DLL создаётся отдельный модуль (unit) с прототипами функций экспортируемых библиотекой (как в приведённом выше примере). Этот модуль с помощью директивы Uses подключается к приложению. {Интерфейсный модуль DLL MyDLL.pas} Unit MyDLL; interface function MyFunction(A, B : Integer) : Integer; implementation function MyFunction(A, B : Integer) : Integer; external «MyDLL»; end. {основной модуль приложения } Uses MyDLL; ... C := MyFunction(10, 20); В отличие от C, Delphi не использует каких либо дополнительных файлов для подключения библиотеки к приложению в процессе компиляции. Рассмотрим теперь пример для Visual C. Visual C. C требует создания .lib файла для динамической библиотеки. Компилятор автоматически создаёт при компиляции DLL.Этот файл содержит информацию необходимую для создания таблицы импорта в файле приложения, которое будет использовать DLL. Вместе с файлом lib разработчик DLL должен предоставить .h файл с описанием прототипов экспортируемых функций. Эти прототипы объявляются с директивой __declspec(dllimport). Например: __declspec(dllimport) __stdcall int
MyFunction(int A, int B);
Вызов функции из приложения выглядит, например, так: C = MyFunction(A, B); В этом примере функция объявлена с использованием директивы __stdcall. Для таких функций компилятор использует правила вызова языка Паскаль. Разница в правилах вызова C и stdcall заключается в том, в какой последовательности параметры функции помещаются в стек перед вызовом. Для stdcall первый параметр помещается первым в стек, а для C последний параметр помещается первым. Например, вызов функции MyFunction по правилам C выглядит следующим образом: Push B Push A Call MyFunction , а если MyFunction объявлена с директивой __stdcall: Push A Push B Call MyFunction Как правило, для функций экспортируемых DLL используется правила вызова stdcall, так как DLL может быть использована программами написанными на разных языках. 79
Следует заметить, что загрузчик должен обнаружить динамические библиотеки, подключённые к приложению при его загрузке. Загрузчик последовательно ищет динамические библиотеки в текущем каталоге, в каталоге Window\System в каталоге \Windows и затем в каталогах указанных в переменных среды окружения программы (environment block). Процедура создания динамической библиотки. Динамические библиотеки используются пользовательскими и системными программами. Функции API Windows размещаются в системных динамических библиотеках. Часто возникает необходимость в создании собственных DLL. Ниже приводятся правила написания динамических библиотек на Delphi и Visual. Delphi. Проект dll создаётся из пункта меню File->New. Delphi создаёт основной файл проекта с директивой Library в первой строке. Эта директива указывает компилятору на то, что исполняемый файл динамическая библиотека в не приложение. Library MyDLL; {MyDLL имя DLL} Каждая экспортируемая функция должна быть объявлена с директивой export. Function MyFunction(A, B : Integer) : Integer; StdCall; Export; begin ... end; Дополнительно в исходном тексте Dll необходимо разместить список всех экспортируемых функций. Список задаётся директивой exports. Exports MyFunction; MyFunction2 MyFunction3 {...} End;
index 10; name ‘MyFunction100’
В этом списке можно дополнительно указать индекс функции или изменить имя, под которым она будет экспортирована. Как уже было отмечено выше, вместе с DLL следует поставлять интерфейсный модуль (*.PAS) содержащий прототипы функций. Visual C. С помощью Visual C 5.0 можно создавать динамические библиотеки двух типов. DLL которые используют библиотеку Microsoft Foundation Class (MFC) и DLL которые ёё не используют (non-MFC DLLs). Обычная, не-MFC DLL, создаётся в пункте меню File->New->Project->Win32 Dynamic-link Library. Существует два способа объявления экспортируемых функций. Первый (рекомендуемый) способ поддерживается в версиях VC, начиная с 5.0. Экспортируемая функция объявляется директивой __declspec(dllexport): __declspec(dllexport) __stdcall int MyFunction(int A, int B ); Объявление функции обычно помещается в отдельный файл заголовок (header file) библиотеки. Этот файл может применяться при компиляции библиотеки и при компиляции приложений которые используют эту библиотеку. Однако для библиотеки эти функции экспортируемые, а для приложений импортируемые. Импортируемые функции объявляются директивой __declspec(dllimport) а экспортируемые директивой __declspec(dllexport). Для того чтобы использовать в обоих случаях один и тот же заголовочный файл применяют директивы условной компиляции и вносят изменения в исходный текст библиотеки: Исходный текст DLL: 80
#define _MYDLLNAME_ Заголовочный файл: #if !defined(_MYDLLNAME_) #define MYDLLAPI __declspec(dllimport) #else #define MYDLLAPI __declspec(dllexport) #endif //объявления функций ... int MYDLLAPI MyFunction(int A, int B); В приложениях символ _MYDLLNAME_ не определяется, поэтому для них все функции объявлены с директивой __declspec(dllimport) ,а для библиотеки с директивой __declspec(dllexport). Экспортируемые функции могут быть так же указаны в .DEF файле проекта DLL. Это другой метод. .DEF файл должен содержать оператор LIBRARY, который определяет имя библиотеки и оператор EXPORT содержащий список экспортируемых функций. Ниже приводится пример .DEF файла: LIBRARY MYDLL EXPORTS MyFunction @1 MyFunction1 @2 MyFunction2 @3 MyFunction3 @4 В операторе EXPORT также задаются индексы функций. В обоих методах компилятор создаёт .lib файл для динамической библиотеки. Этот lib файл вместе с заголовочным файлом необходим для создания Visual C приложений которые используют динамическую библиотеку. Имена функций C и C++. Компилятор помещает в файлы lib и dll имена функций отличающиеся от указанных в тексте программы. Компилятор использует специальные соглашения - правила преобразования имён функций. Имена функций в C и в C++ преобразуются по разным правилам. Одинаковые имена функций в текстах программ написанных на С и на C++ будут разными в исполняемых модулях. Это не имеет значения если скажем библиотека написана на С++ и приложения которые её используют также написаны на С++. Проблема возникает в случае если написанную на С++ библиотеку требуется подключить к приложению написанному на языке С. Одним из способов решения является прямое указание правила преобразования имени функции в тексте программы. Следующий пример иллюстрирует способ объявления правила преобразования имени С для функции в файле C++. #ifdef __cplusplus //если это текст c++ extern «C» { #endif int MYDLLAPI MyFunction(int A, int B); #ifdef __cplusplus //если это текст c++ } #endif Символ __cplusplus объявляется автоматически для файлов c++. В приведённом примере имя функции будет преобразовано по правилам С в независимости от типа файла исходного текста.
81
Точка входа DLL. Каждая динамическая библиотека имеет точку входа. Точка входа это функция с определённым именем, которая вызывается операционной системой в нескольких специальных случаях. По умолчанию имя этой функции DllMain. Имя можно изменить с помощью директивы командной строки компоновщика /ENTRY. Ниже приводится шаблон функции DllMain: BOOL APIENTRY DllMain(HANDLE hModule, DWORD ul_reason_for_call, LPVOID lpReserved) { switch( ul_reason_for_call ) { case DLL_PROCESS_ATTACH: ... case DLL_THREAD_ATTACH: ... case DLL_THREAD_DETACH: ... case DLL_PROCESS_DETACH: ... } return TRUE; } Система при вызове функции передаёт следующие параметры: hModule ссылка на DLL. В действительности для 32-ч разрядных библиотек этот параметр содержит базовый (начальный) адрес по которому загружена библиотека в виртуальном адресном пространстве процесса.Двойное слово ul_reason_for_call содержит код события которое было причиной вызова функции. Последний параметр зарезервирован и не используется. Используя эту функцию DLL может перехватить четыре различных события коды которых указываются в параметре ul_reson_for_call. DLL_PROCESS_ATTACH новый процесс загружает динамическую библиотеку автоматически или функцией LoadLibrary. DLL_PROCESS_DETACH процесс который использовал динамическую библиотеку завершается или выгружает её функцией FreeLibrary. DLL_THREAD_ATTACH в одном из процессов использующих DLL создан новый поток. DLL_THREAD_DETACH в одном из процессов использующих DLL завершился поток. Следует отметить, что если процесс или поток завершаются функциями соответственно TerminateProcess и TerminateThread динамическая библиотека не получает сообщений DLL_PROCESS_DETACH и DLL_TREAD_DETACH. Так же важно учесть, что общее количество сообщений DLL_TERAD_DATACH может быть больше чем количество сообщений DLL_TREAD_ATTACH так как некоторые потоки процесса могут быть созданы в перед загрузкой динамической библиотеки. Это происходит в частности если библиотека загружается функцией LoadLibrary после того как первый поток процесса уже создан. Использование Локальной Области Потока (Thread Local Storage) в динамических библиотеках. По умолчанию данные процесса являются общими для всех потоков этого процесса. Однако в ряде случаев необходимо организация частных областей памяти для каждого потока приложения так чтобы одни и те же виртуальные адреса в указанном диапазоне отображались в разные физические адреса для разных потоков одного процесса. Эта задача может быть решена в Visual C с помощью директивы __declspec(thread) (см. выше). Однако этот метод имеет недостаток. Он не работает в динамической библиотеке, если она загружена функцией LoadLibrary. Он может быть применён только в случае, если DLL загружается автоматически. Есть другой более универсальный способ. Он основан на использовании Локальной Области Потока (TLS). Windows API включает четыре функции предназначенные для размещения и управления данными в TLS. Эти данные являются частными для каждого потока приложения. DWORD TlsAlloc(VOID) LPVOID TlsGetValue(DWORD dwTlsIndex) BOOL TlsSetValue(DWORD dwTlsIndex, DWORD lpvTlsValue) 82
BOOL TlsFree(DWORD dwTlsIndex); TLS организована в виде так называемых слотов которые используются для ссылок на данные размещённые в TLS отдельно для каждого потока. Каждый процесс может использовать до 64-х слотов. Функция TlsAlloc резервирует слот и возвращает его индекс. Этот индекс используется потоками процесса для ссылки на 32-х разрядный параметр. Для каждого потока значение этого параметра уникально. Как правило, параметр это указатель на частные данные потока (см. пример). Поток сохраняет значение параметра в TLS с помощью функции TlsSetValue и может получить это значение вызвав функцию TlsGetValue. Использующийся слот должен быть освобождён перед завершением работы приложения. Для этой операции используется функция TlsFree.
Поток 1 Индекс TLS 1 2 3 свободно ... 64 свободно
TlsIndex = TlsAlloc();
Поток 2
Поток 3
Рисунок 5. Организация частной памяти для потоков. Thread Local Storage(TLS). Процедуры инициализации и деинициализации TLS, как правило, помещают в функцию DllMain библиотеки. static DWORD dwTlsIndex; BOOL WINAPI DllMain (HINSTANCE hInstance, DWORD dwReason, LPVOID lpvRsv) { switch(dwReason) { case DLL_PROCESS_ATTACH: //резервируем слот TLS dwTlsIndex = TlsAlloc(); //продолжаем инициализацию параметра для первого потока. case DLL_THREAD_ATTACH: //размещаем память под данные потока и сохраняем указатель //на них в TLS TlsSetValue(dwTlsIndex, malloc (SIZE_OF_PER_THREAD_DATA)); break; case DLL_TREAD_DETACH: //освобождаем память выделенную под данные потока. Функция //TlsGetValue возвращает указатель на эти данные. free(TlsGetValue(dwTlsIndex)); break; case DLL_PROCESS_DETACH: free (TlsGetValue (TlsIndex)); //освобождаем слот TLS TlsFree(dwTlsIndex) ; break; } return TRUE; } 83
Каждый поток может получить указатель на свои данные, вызвав функцию TlsGetValue.
84
Реестр Windows. Автор: Сидякин И.М. Московский Государственный Технический Университет им. Н.Э. Баумана. Кафедра ИУ-3, (11.1998) Email:
[email protected] Реестр это централизованная база данных операционной системы, в которой сохраняется информация о конфигурации системы, аппаратных и программных настройках. Реестр заменяет файлы config.sys autoexec.bat и файлы .INI которые использовались для этих целей в версиях Windows 3.x. Однако для обеспечения совместимости с программным обеспечением, разработанным для 16-ти разрядных операционных систем, Windows 95 по-прежнему поддерживает использование этих файлов. Реестр в отличие от текстовых INI файлов разработан как двоичная база данных. Для просмотра и модификации реестра используют программу Редактор Реестра (REGEDIT.EXE) или набор API функций Windows. Реестр используют различные программные компоненты операционной системы. Общая структура взаимодействия системных модулей с реестром приведена на рисунке 1.
Инициализация (Setup)
Средства администриров ания
Аппаратная конфигурация
Поиск устройств
Версия и конфигурация
Конфигурация пользователей
Реестр Windows Использование ресурсов и конфигурация
Системное ядро Windows Управление загрузкой
Драйверы аппаратуры Рисунок 1. Использование Реестра. Новые записи вносятся в реестр, при установке нового оборудования и программного обеспечения. Большое количество настроек в реестре можно изменить через Панель Управления. Все 32-х разрядные операционные системы Windows используют реестр, однако, структуры их реестров несколько отличаются друг от друга. Далее в основном будет рассматриваться структура реестра ОС Windows 95.
85
Структура реестра Windows. Наиболее просто изучить структуру реестра с помощью программы RegEdit.Exe. Эта программа отображает содержимое реестра в виде древовидной структуры.
Рисунок 2. Структура Реестра. Левое окно содержит структуру подкаталогов реестра. Эти подкаталоги называются ключами (keys). Каждый ключ в реестре может содержать список значений (values). Каждое значение имеет имя и данные. Например, на рисунке 2 ключ HKEY_LOCAL_MACHINE\System\Control содержит два значения. Первое с именем Default данные, которого не определены (value not set). Второе, с именем Current User, которое равно строке «Joe». Кроме значений ключ может содержать подключи (subkeys). Например, System это подключ ключа HKEY_LOCAL_MACHINE. Структура реестра напоминает структуру каталогов файловой системы с каталогами (ключами) и файлами (значениями). Каждое значение реестра имеет тип. Например: REG_BINARYдвоичные данные, REG_DWORD - двойное слово, REG_SZ - asciiz строка. Эти типы используются наиболее часто.
Рисунок 3. Редактирование Реестра. RegEdit.exe. На рисунке 3 показано как помощью программы regedit в реестр добавляются новые ключи и значения.
86
Корневые ключи (Root Keys). В реестре Windows имеется шесть так называемых корневых ключей. Два основных ключа HKEY_LOCAL_MACHINE и HKEY_USERS распределяют информацию хранящуюся в реестре на два раздела: информация о компьютере и информация о пользователе. HKEY_CLASSES_ROOT Записи этого ключа используются для сохранения настроек приложений использующих технологии DDE и OLE. В частности в подключе CLSID регистрируются компоненты COM. Здесь также хранятся записи связывающие типы данных (типы файлов) с приложениями которые их обрабатывают (см. главу «Оболочка Windows»). Этот ключ отображается в ключ HKEY_LOCAL_MACHINE\SOFTWARE\Classes. Это означает, что одну и ту же информацию можно получить через тот или другой ключ. В реестре такой двойной способ доступа к одним и тем же данным применяется для удобства. HKEY_CURRENT_USER Этот ключ содержит информацию о настройках текущего пользователя. Ключ отображается в один из подключей ключа HKEY_USERS который содержит настройки всех зарегистрированных пользователей. HKEY_LOCAL_MACHINE Записи ключа описывают физическое состояние компьютера, включая данные о системной памяти, установленной аппаратуре и программном обеспечении. Этот ключ содержит подключи в которых хранится информация о конфигурации, в том числе самонастраивающихся устройств (Plug and Play), сетевых настроек, настроек программного обеспечения, и другая системная информация. Назначение некоторых подключей приводится ниже. Подключ \Enum содержит древовидную структуру с информацией о всём установленном оборудовании. Каждая ветвь дерева соответствует одной из шин на которых размещаются устройства. Например, отдельные ветви дерева имеются для устройств SCSI (подключ SCSI), устройств PCI (подключ PCI), самонастраивающихся устройств ISA (подключ ISAPNP), сетевых адаптеров (подключ Network) и системных устройств (подключ Root и подключ BIOS). К системным устройствам настройки которых хранятся в подключе Root относятся например материнская плата, мышь, последовательные порты. К шине BIOS относятся контроллеры прерываний и ПДП, системный таймер и т.п. Из этого перечисления можно заключить, что шина это логическое понятие не всегда соответствующее обычной интерпретации этого слова. В частности BIOS и Root так же считаются отдельными шинами. Подключ \Software содержит настройки установленного программного обеспечения. Здесь приложения хранят свои конфигурационные параметры. При установке приложения программа инсталлятор (например, созданная с помощью InstallShield) создаёт в реестре подключи следующего формата: HKEY_LOCAL_MACHINE\Software\НазваниеКомпании\НазваниеПрограммногоПродукта\ Версия HKEY_LOCAL_MACHINE\Software\CompanyName\ProductName\Version Операционная система также хранит ряд своих настроек в ключе, имеющем такой формат: HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion. Подключ \System\CurrentControlSet содержит информацию о системных службах и драйверах, включая параметры их загрузки и выгрузки. HKEY_USERS
87
Содержит настройки всех зарегистрированных пользователей, включая раскладку клавиатуры настройки сети и Панели Управления. HKEY_CURRENT_CONFIG Этот ключ отображается в подключ HKEY_LOCAL_MACHINE\Config. Здесь хранятся настройки не входящие в раздел информации о пользователе. Например, настройки текущего выбранного принтера и настройки дисплейного адаптера. HKEY_DYN_DATA Этот ключ имеется только в реестрах Windows 95 и Windows 98. Часть реестра, которая хранится в этом ключе, загружается в память. Подключи и значения этого ключа используются для организации обмена данными между пользовательскими приложениями и драйверами, расположенными в нулевом кольце. Важным свойством реестра является то, что он доступен из нулевого и третьего кольца защиты. Драйверы в частности используют ключ HKEY_DYN_DATA для передачи своих текущих параметров пользовательским приложениям. Подключ \ConfigManager\Enum содержит информацию о текущем состоянии аппаратных устройств зарегистрированных в подключе \Enum корневого ключа HKEY_LOCAL_MACHINE. HKEY_PERFORMANCE_DATA Этот ключ используется в реестре NT для доступа к текущему состоянию оборудования. В реестрах операционных систем Windows 95/98 этот ключ отсутствует. На самом деле реестр Windows NT не имеет физического пространства, в котором хранится информация этого ключа. Вместо этого операция чтения значения или подключа транслируется системой в вызов соответствующего программного модуля (драйвера) который возвращает запрашиваемый параметр. Функции API для работы с реестром. 32-х разрядные ОС Windows экспортируют для пользовательских приложений набор функций предназначенных для добавления удаления и модификации записей реестра. Аналогичные функции, правда имеющие несколько отличающийся интерфейс доступны для драйверов нулевого кольца. Работа с реестром начинается с открытия или создания ключа. Эта операция выполняется функцией RegCreateKeyEx или RegOpenKeyEx. LONG RegCreateKeyEx( HKEY hKey, // LPCTSTR lpSubKey, // DWORD Reserved, // LPTSTR lpClass, // DWORD dwOptions, // REGSAM samDesired, // LPSECURITY_ATTRIBUTES
ссылка на корневой ключ адрес строки с именем подключа зарезервировано адрес строки с именем класса ключа флаги права доступа lpSecurityAttributes, // адрес структуры прав доступа PHKEY phkResult, // адрес возвращаемой ссылки на ключ LPDWORD lpdwDisposition // как был открыт ключ (адрес) ); Эта функция создаёт новый ключ или открывает ключ, если он уже существует. hKey это ссылка на ранее открытый ключ, расположенный по иерархии дерева реестра выше создаваемого (или открываемого) ключа. hKey может быть ссылкой на один из корневых ключей. Корневой ключ не нужно предварительно открывать. В этом случае hKey может принимать одно из следующих значений: HKEY_CLASSES_ROOT HKEY_CURRENT_CONFIG HKEY_CURRENT_USER HKEY_LOCAL_MACHINE HKEY_USERS Windows NT: HKEY_PERFORMANCE_DATA Windows 95 и Windows 98: HKEY_DYN_DATA 88
lpSubKey это указатель на строку оканчивающуюся нулём (asciiz строку) которая содержит имя подключа. Имя должно начинаться с символа ‘\’.Например ‘\Software\MyCompany\MyApplication’. lpClass - указатель на asciiz строку с именем класса ключа (типа ключа). Например ‘REG_SZ’. dwOptions по умолчанию равен REG_OPTION_NON_VOLATILE. В этом случае данные записанные под этим ключом сохранятся после перезагрузки компьютера. Windows 95 не поддерживает других вариантов. SamDesired определяет права доступа к ключу. Например, KEY_ALL_ACCESS - полный доступ или KEY_EXECUTE - разрешено только чтение. Параметр LpSecurityAttributes в Windows 95 не используется и должен быть равен NULL. Функция возвращает по указателю phkResult ссылку на созданый или открытый ключ, а по указателю lpdwDisposition константу REG_CREATED_NEW_KEY, если ключ был создан или константу REG_OPENED_EXISTING_KEY, если был открыт уже существующий ключ. Пример создания ключа: Result = RegCreateKeyEx( HKEY_CLASSES_ROOT, «\MyKey», 0, "REG_SZ", REG_OPTION_NON_VOLATILE, KEY_ALL_ACCESS, 0, &hKey, &dwDisposition); Функция CloseKey закрывает ключ. Эту функцию необходимо вызвать после окончания работы с ключом и значениями внутри ключа. LONG RegCloseKey( HKEY hKey // ссылка на ключ ); Операционная система кэширует операции записи в реестр. Это означает что данные, записанные в реестр, сохраняются на диск не во время операции записи, а впоследствии в «удобное» с точки зрения системы время. Для того чтобы заставить систему сохранить данные на диске минуя кэш, следует использовать функцию RegFlushKey. Функция RegDeleteKey предназначена для удаления ключей. LONG RegDeleteKey( HKEY hKey, // ссылка на открытый корневой ключ LPCTSTR lpSubKey // адрес строки с именем подключа ); Для удаления ключа не нужно его открывать. В Windows NT ключ может быть удалён только в том случае, если он не содержит подключей. Функция RegSetValueEx используется для записи значений в ключе. В параметрах этой функции следует указать ссылку на ране открытый ключ. LONG RegSetValueEx( HKEY hKey, значение LPCTSTR lpValueName, DWORD Reserved, DWORD dwType, CONST BYTE *lpData, DWORD cbData );
// ссылка на ключ, в который записывается // // // // //
указатель на строку с именем значения зарезервировано флаг типа значения адрес данных значения размер данных значения
Например: Result = RegSetValueEx (hKey, «MyValue», 0, REG_SZ, (BYTE *)lpData, dwDataLen); 89
С помощью функции RegQueryValue можно прочитать значение, размещённое в заданном ключе. LONG RegQueryValueEx( HKEY hKey, // LPTSTR lpValueName, // LPDWORD lpReserved, // LPDWORD lpType, // LPBYTE lpData, // LPDWORD lpcbData // );
ссылка на ключ в котором размещено значение указатель на строку с именем значения зарезервировано адрес буфера для типа значения адрес буфера для данных значения адрес буфера для размера данных значения
Например: Result = RegQueryValueEx(hKey, «MyValue», 0, «REG_SZ», (BYTE *)lpData, dwDataLen); Windows API также содержит функции RegEnumKey и RegEnumValue которые позволяют организовать сканирование по ветвям структуры реестра и спискам значений. Пример работы с реестром на Delphi. В Delphi для работы с реестром можно использовать функции API, однако, удобнее воспользоваться надстройкой над этими функциями реализованной в виде класса TRegistry.
Uses Registry, RegStr;
Var Reg : TRegistry; DisabeVxD : Boolean;
Reg := TRegistry.Create; Reg.RootKey :=HKEY_LOCAL_MACHINE; if Reg.OpenKey(REGSTR_PATH_CLASS +'\LTMI\HOST', True) then begin if DisableVxD then begin if Reg.ValueExists('StaticVxD') then Reg.DeleteValue('StaticVxD'); end else if Not Reg.ValueExists('StaticVxD') then Reg.WriteString('StaticVxD', 'tmihost.vxd'); Reg.CloseKey; end; Reg.Destroy; REGSTR_PATH_CLASS это константа определённая в модуле RegStr 'System\CurrentControlSet\Services\Class’. В примере в ключе с полным именем HKEY_LOCAL_MACHINE\System\CurrentControlSet\Services\Class\LTMI\HOST создаётся строковое значение ‘StaticVxD’. Этому значению присваивается строка ‘tmihost.vxd’. Значение создаётся в случае если DisableVxD равно TRUE и удаляется, если DisableVxD равно FALSE. HKEY_LOCAL_MACHINE |System |CurrentControlSet |Services |Class |LTMI |HOST StaticVxD = ‘tmihost.vxd’ 90
Файлы реестра. Реестр Windows 95 хранится в двух файлах. Раздел с информацией о пользователе хранится в файле USER.DAT, а раздел с информацией о компьютере в файле SYSTEM.DAT. По умолчанию оба файла размещаются в каталоге Windows\System. При загрузке опреационной системы создаются копии этих файлов - USER.DA0 и SYSTEM.DA0. Если работа Windows завершается аварийно содержимое файлов USER и SYSTEM может быть повреждено. В этом случае система пытается автоматически восстановить эти файлы из копий. Однако в ряде случаев автоматическое восстановление реестра невозможно (Если копии также повреждены). Реестр можно сохранить и восстановить (если он был прежде сохранён) вручную с помощью программы RegEdit. Эта программа может быть запущена из командной строки в случае если оболочка Windows не загружается из за повреждений в реестре . Содержимое реестра полностью или частично можно экспортировать в текстовый файл и импортировать из текстового файла. В ряде случаев для внесения записей в реестр применяется технология импорта записей из текстового файла. Для этого можно использовать редактор реестра RegEdit или функции API: RegLoadKey, RegReplaceKey и RegRestoreKey. Ключи реестра вместе с содержимым сохраняются в текстовом файле функцией RegSaveKey. Ниже приводится пример такого текстового файла: REGEDIT4 [HKEY_LOCAL_MACHINE\System\CurrentControlSet\Services\SAMPDRV] "ImagePath"="\\SystemRoot\\system32\\drivers\\sampdrv.sys" "Type"=dword:00000001 "Start"=dword:00000001 "Group"="Base" "ErrorControl"=dword:00000001 Этот файл с расширением .reg содержит информацию необходимую для регистрации драйвера WDM sampdrv.sys в реестре Windows 98. Строка в квадратных скобках определяет имя ключа, а четыре следующие строки задают четыре значения которые должны быть добавлены в ключ. Детальное исследование реестра это комплексная задача, которая заслуживает изложения в отдельной книге. В последующих главах посвященных Оболочке Windows и проектированию драйверов будут рассмотрены некоторые полезные ключи и значения, хранящиеся в реестре.
91
Оболочка Windows (Shell). Автор: Сидякин И.М. Московский Государственный Технический Университет им. Н.Э. Баумана. Кафедра ИУ-3, (11.1998) Email:
[email protected] Оболочка Windows это модуль операционной системы, который отвечает за интерфейс с пользователем. Оболочка представляет «видимую» часть операционной системы. Оболочка включает набор компонентов, которые позволяют оператору перемещаться по системе каталогов, запускать программы, производить различные настройки и т.п. Оболочка допускает программное управление её компонентами. Оболочка Windows расширяема. Это означает, что к Оболочке могут быть легко добавлены новые компоненты. Так называемые Расширения Оболочки (Shell extensions) разрабатываются по правилам объектов (COM). В этой главе вопрос проектирования расширений оболочки с использованием COM технологий не рассматривается. Далее будут приведены несколько полезных примеров использования свойств Оболочки в обход интерфейсов COM. Оболочка тесно взаимодействует с реестром Windows. В реестре хранится большое число настроек компонентов Оболочки. Манипуляции с некоторыми ключами реестра позволяют легко решить ряд часто встречающихся в программировании задач. Связь документов и приложений. Тип файла традиционно определяется его расширением. Расширения файлов регистрируются в реестре. Каждому расширению можно так же через реестр поставить в соответствие набор свойств. Например, можно связать файл данных с иконкой, которая будет отображаться вместе с именем файла в окне Проводника. Можно также связать расширение файла данных с приложением, которое работает с файлами этого типа. Для организации такой связи следует вначале зарегистрировать расширение файла данных. В корневом ключе HKEY_CLASSES_ROOT создаётся подключ с именем расширения файла данных. Например ,для файла с расширением .MFE (my files extension) создаётся ключ: HKEY_CLASSES_ROOT\.MFE Сразу после создания ключа он содержит единственное неопределённое значение с именем (Default) Это значение надо проинициализировать строкой содержащей имя типа документа. Например: (Default) = MyFiles Имя типа является одновременно именем нового подключа который должен быть создан в том же корневом ключе HKEY_CLASSES_ROOT. HKEY_CLASSES_ROOT\MyFiles В этом ключе будут храниться все свойства документа нового типа. В значении (Default) этого ключа указывается строка, которая будет отображаться в Проводнике вместе с именем файла документа в колонке Type. Следует отметить, что допускается ссылка на один и тот же ключ типа документа из нескольких ключей расширений.
92
Рисунок 1. Окно Проводника. Для того чтобы связать тип документа с иконкой следует добавить в ключ типа документа подключ (не значение) с именем DefaultIcon. HKEY_CLASSES_ROOT\MyFiles\DefaultIcon Значение (Default) этого подключа должно содержать строку с именем файла, в котором находится иконка и (если из файла берётся не первая по списку иконка) её индекс. (Default) = C:\Program Files\MyAppDir\MyApp.exe ,1 В этом примере иконка располагается в файле MyApp.exe и её индекс равен 1. Оболочка может автоматически создавать новые файлы документов типы, которых зарегистрированы в реестре. Эта операция производится, например, через меню File>New Проводника. Для того чтобы это работало необходимо добавить в ключ расширения файла подключ с именем ShellNew. В нашем случае: HKEY_CLASSES_ROOT\.MFE\ShellNew В этот подключ можно записать следующие значения: (В начале указано имя значения затем его данные) NullFile
«»
Оболочка создаёт пустой файл с заданным расширением. FileName
«Path»
Оболочка создаёт новый файл и копирует в него содержимое файла с именем ‘Path’. Data
Двоичные данные
Оболочка создаёт новый файл и копирует в него двоичные данные значения Data. Command Командная строка Оболочка выполняет указанную командную строку. Этот способ используется, в случае если какое либо приложение самостоятельно создаёт свои файлы данных. Записи реестра могут использоваться для добавления специфических для документа операций. Проводник имеет стандартную систему пунктов меню содержащую список операций над файлами данных. Эти операции перечислены в меню File и в всплывающем меню. Этот список может быть расширен или изменён. Вначале следует создать подключ с именем Shell в ключе типа документа: 93
HKEY_CLASSES_ROOT\MyFiles\Shell Затем создать в этом подключе дополнительные подключи для каждого из пунктов меню. Например: HKEY_CLASSES_ROOT\MyFiles\Shell\Edit HKEY_CLASSES_ROOT\MyFiles\Shell\Open Значения (Default) задают строки отображаемые в пунктах меню Проводника. Например «&Edit» (символ & определяет горячую клавишу). Далее необходимо связать пункт меню с действием. Для этого в каждом подключе соответствующем пункту меню следует создать подключ с именем Command. Значение (Default) подключа Сommand должно содержать командную строку которая будет выполнятся при выборе пункта меню. Например: HKEY_CLASSES_ROOT\ MyFiles\Shell\Edit\Command (Default) = C:\Program Files\MyAppDir\MyApp.Exe %1 %1 указывает на то, имя файла данных будет передано приложению MyApp.exe при запуске через командную строку. Запуск приложений в процессе загрузки операционной системы. Иногда возникает необходимость в запуске приложений (например, антивирусов) при загрузке ОС. Эта задача так же легко решается с помощью реестра. В реестре имеется два ключа: HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\RunOnce и HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Run Ключ RunOnce используется для однократного запуска приложений. Записи в этом ключе удаляются после загрузки системы. Приложения, которые зарегистрированы в ключе Run будут запускаться всякий раз при загрузке ОС. Например, для однократного запуска приложения MyApp.exe следует добавить в ключ RunOnce следующее значение: MyAppId = «C:\Program Files\MyAppDir\MyApp.exe» Имя значения MyAppId должно быть уникально в этом ключе. Размещение иконки в поле статуса Панели задач. Поле статуса содержит иконки связанные с приложениями. Обычно в поле статуса располагаются часы и переключатель раскладки клавиатуры. MS Windows предоставляет программный интерфейс для размещения, удаления и связи этих иконок и соответствующих им программ. Приложение, связанное с иконкой в поле статуса, должно выполняться всё время пока эта связь существует. Оно может отображаться на экране, например, после двойного щелчка по иконке, а при закрытии окна приложения, вместо завершения снова становиться невидимым.
Рисунок 2. Панель задач. Для работы с полем статуса Панели задач используется функция Shell_NotifyIcon. С помощью этой функции приложение может выполнить три операции: добавить иконку в поле статуса, удалить иконку, изменить параметры иконки. Обратная связь иконки с приложением реализуется с помощью обычного механизма сообщений. Сообщения от мыши в области иконки транслируются приложению. Поле wPаram сообщения содержит идентификатор иконки а поле lParam код сообщения мыши. Код сообщения, которое получает окно приложения, и идентификатор иконки задаются программистом при добавлении иконки в поле статуса. 94
Функция Shell_NotifyIcon. WINSHELLAPI BOOL WINAPI Shell_NotifyIcon( DWORD dwMessage, // код операции PNOTIFYICONDATA pnid // указатель на структуру NOTIFYICONDATA ); Параметр dwMessage может принимать одно из трёх значений: NIM_ADD - добавить иконку NIM_DELETE - удалить иконку NIM_MODIFY - изменить параметры иконки pnid - указатель на структуру, которая должна быть размещена и инициализирована перед вызовом функции. Функция возвращает FALSE в случае ошибки. Структура NOTIFYICONDATA. typedef struct _NOTIFYICONDATA { DWORD cbSize; HWND hWnd; UINT uID; UINT uFlags; UINT uCallbackMessage; HICON hIcon; char szTip[64]; } NOTIFYICONDATA, *PNOTIFYICONDATA; Поля структуры cbSize Размер структуры NOTIFYICONDATA. hWnd Указатель окна которому будут транслироваться сообщения от иконки. uID Произвольный идентификатор иконки. uFlags Флаги указывающие какие из полей структуры заданы. NIF_ICON Поле hIcon инициализировано. NIF_MESSAGEПоле uCallbackMessage инициализировано. NIF_TIP Поле szTip инициализировано. uCallbackMessage Код сообщения от иконки. Задаётся, как и идентификатор иконки программистом. Сообщение с таким кодом транслируется окну hWnd приложения, как реакция на событие мыши в поле иконки. hIcon Указатель на рисунок иконки. szTip Всплывающий над иконкой текст. Пример. Ниже приведены фрагменты программы написанной на Microsoft Visual C 5.0. В данном примере CTaskBar это класс главного окна приложения. К классу добавлены три метода: AddTaskBarIcon, который размещает иконку в поле статуса, DelTaskBarIcon, который удаляет иконку и обработчик сообщения WM_TASKBAR (= WM_USER+1) OnTaskBarNotify. Пример также показывает, как установить обработчик нестандартного сообщения (WM_TASKBAR). 95
//определения #define TB_ID 100 //идентификатор иконки #define WM_TASKBAR WM_USER+1 //код сообщения от иконки ... //описание класса окна приложения class CTaskBar : public CDialog { ... //прототип процедуры добавления иконки в поле статуса void DelTaskBarIcon(); //прототип процедуры удаления иконки из поля статуса void AddTaskBarIcon(); ... //прототип обработчика сообщения в описании класса СTaskBar afx_msg LONG OnTaskBarNotify(UINT, LONG); ... } //конец описания класса окна ... //добавление обработчика сообщения в карту сообщений окна BEGIN_MESSAGE_MAP(CTaskBar, CDialog) ... ON_MESSAGE(WM_TASKBAR, OnTaskBarNotify) ... END_MESSAGE_MAP() ... //Процедура размещения иконки в поле статуса. void CTaskBar::AddTaskBarIcon() { BOOL res; NOTIFYICONDATA tnid; LPSTR lpszTip = "Тест."; //иконка приложения HICON hicon = AfxGetApp()->LoadIcon(IDR_MAINFRAME); tnid.cbSize = sizeof(NOTIFYICONDATA); //указатель (handle) окна хранится в переменной m_hWnd класса окна. tnid.hWnd = m_hWnd; //указатель окна tnid.uID = TB_ID; //идентификатор иконки tnid.uFlags = NIF_MESSAGE | NIF_ICON | NIF_TIP; tnid.uCallbackMessage = WM_TASKBAR; tnid.hIcon = hicon; if (lpszTip) lstrcpyn(tnid.szTip, lpszTip, sizeof(tnid.szTip)); else tnid.szTip[0] = '\0'; //если всплывающая надпись не нужна res = Shell_NotifyIcon(NIM_ADD, &tnid); if (hicon) DestroyIcon(hicon); return; } ... //Процедура удаления иконки из поля статуса void CTaskBar::DelTaskBarIcon() { BOOL res; NOTIFYICONDATA tnid; tnid.cbSize = sizeof(NOTIFYICONDATA); //указатель (handle) окна хранится в переменной m_hWnd класса окна. tnid.hWnd = m_hWnd ; tnid.uID = TB_ID; res = Shell_NotifyIcon(NIM_DELETE, &tnid); return; } ... //Обработчик сообщений от иконки LONG CTaskBar::OnTaskBarNotify(UINT wParam, LONG lParam) { //wParam содержит идентификатор иконки 96
//lParam содержит код сообщения мыши //в этом примере обрабатывается только код WM_LBUTTONDOWN if((wParam == TB_ID) && ((UINT)lParam == WM_LBUTTONDOWN)) { MessageBox("В области иконки нажата левая кнопка мыши.", "Сообщение.", MB_OK); } return 1; } ... Delphi.
Функция Shell_NotifyIcon, константы, тип TNotifyIconData и указатель на него PNotifyIconData объявлены в модуле ShellApi. (Delphi 3.0)
Для обработки нестандартного сообщения следует объявить обработчик сообщения при описании класса окна используя ключевое слово message.
const WM_TASKBAR = WM_USER + 1; TB_ID = 100; type TTaskBarForm = Class(TForm) ... Procedure OnTaskBarNotify(Var Msg : TMessage); message WM_TASKBAR; ... End; Обработчик сообщения может выглядеть, например, так: Procedure TTaskBarForm.OnTaskBarNotify(Var Msg : TMessage); begin if (Msg.wParam = TB_ID) and (ShortInt(Msg.lParam) = WM_LBUTTONDOWN) then MessageBox(nil, "В области иконки нажата левая кнопка мыши.", "Сообщение.", mbOk); end;
Указатель окна (handle) хранится в поле Handle класса окна. tnid.hWnd := Self.Handle;
Приложения Панели Управления (Control Panel Applets). Приложения Панели Управления это динамические библиотеки доступные через Окно Панели Управления Windows.
97
Рисунок 3. Панель управления. Эти библиотеки используются для организации пользовательского интерфейса по управлению различными системными компонентами. Приложения Панели Управления обычно содержат одно или несколько диалоговых окон, которые отображаются по двойному щелчку мыши в области иконки приложения в Панели Управления. Для больших программных проектов, состоящих из нескольких модулей, полезно создавать дополнительные к имеющимся в системе Приложения Панели Управления. Эти приложения могут выполнять функции настройки и отображения состояния отдельных модулей проекта. В частности такое приложение может использоваться для изменения настроек драйвера аппаратуры. При этом несколько приложений используют драйвер, но все его глобальные настройки выполняются через Панель Управления. Создание и регистрация Приложения Панели Управления несложная процедура. Windows поддерживает специальный программный интерфейс Приложений Панели Управления. Как уже было отмечено, Приложение реализуется в виде динамической библиотеки. DLL должна экспортировать функцию с именем CplApplet. Это точка входа Приложения. Система вызывает эту функцию в нескольких случаях. Например, при загрузке Приложения или когда пользователь щёлкает по иконке приложения в окне Панели Управления. Эту функцию, конечно, может вызвать не только Оболочка Windows, но и любое приложение. Ниже приводится интерфейс этой функции: LONG APIENTRY CPlApplet( HWND hwndCPl, UINT uMsg , LONG lParam1, LONG lParam2 ); Функция имеет четыре параметра. HwndCPl - ссылка на окно Панели Управления в случае если функция вызвана Оболочкой. uMsg - код сообщения. lParam1 и lParam2 32-х разрядные параметры значения которых зависят от кода сообщения. Оболочка или какое либо пользовательское приложение может вызвать функцию со следующими кодами сообщений: CPL_INIT посылается Приложению Панели управления при загрузке. В обработчике этого сообщения, как правило, производятся различные процедур по инициализации. Если при инициализации возникли ошибки, функция должна вернуть 0. В этом случае Приложение не будет загружено.
98
Если инициализация проведена успешно, оболочка посылает сообщение CPL_GET_COUNT. Это запрос количества диалоговых окон, которое поддерживает приложение. Учитываются только те окна которые могут быть непосредственно вызваны из окна Панели Управления. Функция возвращает количество таких окон. Для каждого указанного диалогового окна в Панели Управления будет отображаться отдельная иконка. Затем Оболочка посылает по паре сообщений CPL_INQUIRE и CPL_NEWINQUIRE для каждого из диалоговых окон. В параметре lParam1 передаётся порядковый номер диалогового окна. Приложение должно заполнить структуры CPLINFO или NEWCPLINFO указатели, на которые передаются в параметре lParam2. Буферы для этих структур размещаются вызывающей программой. Эти структуры описывают параметры каждого диалогового окна Приложения. Приложение может обрабатывать сообщения CPL_INQUIRE или CPL_NEWINQUIRE по выбору, однако новые версии оболочки ( начиная с Internet Explorer 4.0) корректно отображают иконки в Окне Панели Управления только в случае если Приложение передаёт параметры диалоговых окон в структуре NEWCPLINFO. Ниже приводится формат структур: typedef struct tag CPLINFO { int idIcon; int idName; int idInfo; LONG lData; } CPLINFO; typedef struct tag NEWCPLINFO { DWORD dwSize; DWORD dwFlags; DWORD dwHelpContext; LONG lData; HICON hIcon; TCHAR szName[32]; TCHAR szInfo[64]; TCHAR szHelpFile[128]; } NEWCPLINFO; dwSize - размер структуры определяется как sizeof(NEWCPLINFO). Поля dwFlags, dwHelpContext и zsHelpFile не используются. lData заданное пользователем значение которое впоследствии передается Приложению при вызовах. hIcon ссылка на иконку диалогового окна. Эта иконка будет отображаться в окне Панели Управления. szName и szInfo asciiz строки содержащие имена диалогового окна. szName отображается под иконкой. szInfo отображается когда иконка выбрана. Следует отметить, что структура CPLINFO содержит идентификаторы ресурсов idIcon, idName, idInfo вместо ссылки на иконку и самих строк, как это реализовано в структуре NEWCPLINFO. Каждый раз при выборе иконки диалогового окна в Панели Управления Приложение получает сообщение CPL_DBLCLK. Параметр lParam1 содержит порядковый номер диалогового окна а параметр lParam2 значение, заданное параметром lData. Как правило, Приложение по этому сообщению отображает диалоговое окно. Когда Панель Управления закрывается Приложения получают сообщения CPL_STOP по одному для каждого диалогового окна. lParam1 содержит порядковый номер окна и lParam2 значение lData. После серии сообщений CPL_STOP непосредственно перед выгрузкой Оболочка посылает Приложению заключительное сообщение CPL_EXIT. Для регистрации Приложения Панели Управления достаточно изменить расширение исполняемого файла Приложения на .cpl и записать его в каталог Windows\System. Панель Управления перед отображением своего окна сканирует этот каталог в поисках cpl файлов. 99
Пример на Си с использованием структуры CPLINFO: HUNSTANCE hinst; LONG CALLBACK CPlApplet(HWND hWnd, UINT uMsg, LPARAM lParam1, LPARAM lParam2) { int i = (int) lParam1; LPCPLINFO lpCPlInfo; switch (uMsg) { case CPL_INIT: hinst = GetModuleHandle(«MyCPL.cpl»); return TRUE; case CPL_GETCOUNT: return NUM_APPLETS; //количество диалоговых окон break; case CPL_INQUIRE: //инициализация массива DialоgArray не показана lpCPlInfo = (LPCPLINFO) lParam2; lpCPlInfo->lData = 0; lpCPlInfo->idIcon = DialogArray[i].IconID; lpCPlInfo->idName = DialogArray[i].NameStringID; lpCPlInfo->idInfo = DialogArray[i].DescStringID; break; case CPL_DBLCLK: DialogBox(hinst, MAKEINTRESOURCE( DialogArray[i].DlgTemplateID), hWnd, DialogArray[i].DlgFunction); break; case CPL_STOP: break; case CPL_EXIT: break; default: break; } return 0; } Пример на Паскале с использованием структуры NEWCPLINFO и одним диалоговым окном: Uses CPL; Function CPlApplet(hwndCPl : THandle; uMsg : DWORD; lParam1 : LongInt; lParam2 : LongInt) : LongInt; StdCall; Begin Case uMSg of CPL_INIT: Begin MyCPLApplet := 1; {Ok} End; CPL_GETCOUNT: begin MyCPLApplet := 1; {одно окно} end; CPL_NEWINQUIRE: begin with pNEWCPLInfo(lParam2)^ do begin dwSize := SizeOf(TNewCPLInfo); dwFlags:= 0; dwHelpContext:= 0; lData := 0; 100
hIcon := LoadIcon(hInstance, 'MAINICON'); szName := 'Пример'#00; szInfo := 'Пример Приложения Панели Управления'#00; szHelpFile:= ''; end; MyCPLApplet := 0; end; CPL_DBLCLK: begin ViewDialogBox; {эта функция отображает диалоговое окно } {или так: MessageDlg(‘А вот и я’, mtInformation, [mbOk], 0); } MyCPLApplet := 0; end; Else MyCPLApplet := 0; End; End; ... exports CPlApplet; ...
101
Основы проектирования Виртуальных Устройств Windows 95 Автор: Сидякин И.М. Московский Государственный Технический Университет им. Н.Э. Баумана. Кафедра ИУ-3, (01.1999) Email:
[email protected] Виртуальные устройства относятся к компонентам нулевого кольца операционной системы. VxD могут напрямую взаимодействовать с аппаратными устройствами. Они в частности выполняют обработку аппаратных прерываний и поддерживают низкоуровневые операции ввода-вывода. VxD предназначены не только для управления аппаратурой. Они так же используются для организации интерфейса пользовательского программного обеспечения с системным ПО нулевого кольца и приложениями MS-DOS которые исполняются в виртуальных машинах V86. Виртуальные устройства имеют интерфейс с Менеджером виртуальных машин, другими VxD, 16-ти и 32-х разрядными приложениями Windows и приложениями MSDOS. VxD используют различные преимущества программного обеспечения работающего в нулевом кольце защиты операционной системы Windows включая защиту памяти от доступа со стороны пользовательских приложений, свободный доступ к памяти, портам ввода-вывода и другим системным ресурсам, быструю обработку аппаратных прерываний, общее адресное пространство. Разработка VxD как правило необходима в приложениях сбора и обработки данных которые исполняются в реальном времени. Пример программной архитектуры такой системы приводится на рисунке 1.
Приложение Win32
Приложение MS_DOS
Приложение Win16
Кольцо 3 Кольцо 0
управление
данные
Виртуальное устройство (VxD)
VMM32.VxD
VPICD.VxD
VDMAD.VxD
Аппаратура
Устройство сбора данных
Программируем ый контроллер прерываний
Контроллер прямого доступа к памяти
Рисунок 1. Система сбора данных. Аппаратное устройство сбора данных использует механизм ПДП для копирования входного потока данных в память и выставляет аппаратное прерывание для индикации готовности данных. Системные виртуальные устройства VPICD и VDMAD предоставляют функции необходимые для инициализации контроллера ПДП и контроллера прерываний, подстановки процедуры обработчика IRQ, и запуска цикла ПДП. Интерфейс менеджера виртуальных машин (VMM) содержит различные системные функции управления памятью, синхронизации, работы с реестром, 102
таймером и т.д. VxD может прямо обращаться к памяти и портам ввода вывода аппаратного устройства для чтения данных и информации о состоянии, а так же для записи команд. С другой стороны виртуальное устройство взаимодействует с пользовательскими приложениями Win32, Win16 и MS-DOS. VxD получает команды от приложений и возвращает данные полученные от аппаратуры. Приложения третьего кольца в такой системе обеспечивают пользовательский интерфейс. Приложения Windows имеют мощный графический интерфейс для отображения данных и управления аппаратурой в режиме реального времени. Иногда дополнительно к виртуальному устройству создаётся динамическая библиотека, которая экспортирует набор API функций необходимых для работы с устройством. Приложения обращаются к устройству через интерфейс динамической библиотеки (Рисунок 2).
Win 32 application Win 32 application Приложение Win32
Win 32 application Win 32 application Приложение Win16
32-битовая DLL
16-битовая DLL
Кольцо 3 Кольцо 0
Виртуальное устройство (VxD) Рисунок 2. DLL обеспечивает интерфейс VxD и пользовательских приложений. Windows поддерживает стандартные интерфейсы для передачи данных между приложениями и виртуальными устройствами. Назначение DLL состоит в том, чтобы скрыть от приложения тонкости системных вызовов VxD и заменить их набором API функций. Например, передача данных и команд из приложения Win32 в VxD производится с помощью функции DeviceIoControl. Динамическая библиотека экспортирует функции с понятными именами, например InitializeDevice, StartDmaTransfer. Эти функции выполняют необходимые подготовительные операции и вызывают DeviceIoControl. Приложение, которое работает с устройством, не опускается до специфики организации передачи данных между нулевым и третьим кольцом защиты операционной системы. Виртуальные Машины и Менеджер Виртуальных Машин (VMM). VMM это ядро операционной системы. Он отвечает за управление виртуальными машинами. Каждая виртуальная машина это отдельная задача, которая исполняется в вычислительной системе. Виртуальная машина содержит код и данные приложений. С точки зрения приложения виртуальная машина это отдельный компьютер со всеми ресурсами необходимыми для работы приложения. Системная виртуальная машина содержит все 32-х и 16-ти разрядные приложения Windows. Дополнительно создаваемые виртуальные машины V86 предназначены для исполнения программ MSDOS.
103
Системная Виртуальная Машина Win 32 Win 32 application Приложение application Win32
Приложение Win16 Приложение Win16 Приложение Win16
Virtual 86 Виртуаль Virtual 86 Machine ная Machine машина V86
Модуль управления вирт. машинами VMM Рисунок 3. Виртуальные машины. VMM это 32-х разрядный модуль ОС который работает в плоской модели памяти. VMM размещается в сегментах размером 4ГБ. Селекторы этих сегментов располагаются в Глобальной дескрипторной таблице (GDT). Верхний гигабайт виртуальной памяти защищён от пользовательских программ. Кроме того, это общая память, которая одинаково отображается в физические адреса для всех процессов. В этой памяти размещается код и данные VMM и виртуальных устройств. Основные функции VMM следующие:
Управление виртуальной памятью.
Windows 95 использует механизм загрузки страниц по запросу (demand-paging). Часть кода и данных программ размещается на диске и загружается в память непосредственно при обращении. Процесс загрузки страниц скрыт от приложений и выполняется менеджером виртуальных машин. VMM создаёт для каждого процесса частное виртуальное адресное пространство размером примерно 2ГБ и управляет операцией трансляции виртуальных адресов в физические.
Переключение потоков
VMM содержит диспетчер потоков (scheduler) - системный модуль, который отвечает за разделение процессорного времени между потоками. Windows 95 использует механизм вытесняющей мультизадачности в котором переключение потоков происходит не зависимо от пользовательских программ. Диспетчер определяет момент переключения потоков, основываясь на приоритетах и текущем состоянии потоков.
Поддержка интерфейса с программами MS-DOS.
Каждое приложение MS-DOS запускается в отдельной виртуальной машине V86. VMM обеспечивает программам MS-DOS доступ к ресурсам ядра операционной системы. Например, VMM транслирует запросы аппаратных прерываний в виртуальные машины V86 и управляет отображением виртуальных адресов в диапазоне первого мегабайта памяти, который используется MS-DOS. VMM также отвечает за организацию интерфейса виртуальных устройств с приложениями Windows и DOS. Стандартные виртуальные устройства. VMM работает во взаимодействии с различными виртуальными устройствами, которые выполняют функции управления отдельными аппаратными устройствами и программными ресурсами вычислительной системы. Например, упомянутый выше VPICD.VxD это драйвер программируемого контроллера прерываний. Он предоставляет системный интерфейс для управления этим аппаратным устройством, включая функции инициализации контроллера и установки процедур обработчиков аппаратных прерываний. Некоторые стандартные VxD не связаны с какой либо аппаратурой. Они предназначены для управления программными ресурсами операционной системы. Например, виртуальное устройство Shell используется (помимо прочего) для организации интерфейса между виртуальными устройствами и пользовательскими приложениями Windows. В таблице приводятся вместе с кратким описанием некоторые стандартные виртуальные устройства. 104
Виртуальный контроллер ПДП Виртуальный контроллер прерываний Виртуальный менеджер режима V86.
VDMAD VPICD
контроллер ПДП. контроллер прерываний.
V86MMGR
Загрузчик VxD
VXDLDR
Виртуальный сопроцессор
VMCPD
Виртуальное устройство Shell
SHELL
VWin 32
VWIN32
Виртуальный таймер
VTD
размещение памяти для машин V86 и организация вызовов из защизённого режима функций машины V86. Функции загрузки и выгрузки динамически загружаемых VxD. Обеспечивает совместный доступ к сопроцессору из нескольких задач. Используется для связи VxD и пользовательских приложений, Содержит функции посылки сообщений окнам приложений, отображения сообщений на экране и др. Набор функций для поддержки асинхронного и синхронного обмена данных между VxD и приложениями Win32 Системный таймер
Программная архитектура виртуальных устройств. Для проектирования виртуальных устройств Windows 95 используется язык ассемблера и частично Cи. Ассемблерная часть может быть небольшим стандартным текстом, при этом основная часть проекта VxD пишется на языке Си. Правила и особенности проектирования VxD на языке Си в среде Microsoft Visual C будут рассмотрены позднее. Первые примеры текстов приводятся на языке ассемблер. Для компиляции программ следует использовать MASM. Кроме собственно компилятора и компоновщика для создания VxD требуются дополнительные средства: заголовочные файлы и библиотеки входящие в состав Microsoft Device Driver Kit (Windows 95 DDK). В состав DDK входят заголовочные файлы для ассемблера (.inc) и Си (.h). Эти файлы содержат заголовки функций, объявления констант, макроопределений и типов, необходимых для компиляции VxD. Следует отметить, что текст виртуального устройства содержит много макроопределений, что делает его несколько не похожим на обычную ассемблерную программу. Можно сказать, что VxD частично пишется на ассемблере, и частично на некотором «системном» языке, содержащем операторы, построенные на основе макроопределений. Сегменты виртуальных устройств. Код и данные VxD могут располагаться в нескольких секциях, которые называются сегментами. К этим сегментам относятся: Сегмент кода защищённого режима. Наличие этого сегмента обязательно. В этом сегменте размещается код. В сегменте располагается одна или несколько так называемых точек входа. Точки входа это функции, которые обеспечивают интерфейс виртуального устройства с внешним миром. Операционная система имеет возможность определить адреса этих функций и вызывать их. Начало сегмента в тексте программы объявляется директивой VXD_CODE_SEG, конец директивой VXD_CODE_ENDS. Сегмент данных защищённого режима. Сегмент содержит глобальные данные виртуального устройства. Сегмент объявляется директивами VXD_DATA_SEG и VXD_DATA_ENDS. Сегмент кода инициализации защищённого режима. Сегмент уничтожается сразу после загрузки виртуального устройства. Код, размещаемый в этом сегменте доступен только в процессе загрузки VxD. В сегменте размещается код инициализации устройства, который не требуется после его загрузки. Сегмент объявляется директивами VXD_ICODE_SEG и VXD_ICODE_ENDS. Сегмент данных инициализации защищённого режима. Сегмент используется для размещения данных которые требуются только при загрузке для инициализации. После 105
загрузки сегмент уничтожается. Сегмент объявляется директивами VXD_IDATA_SEG и VXD_IDATA_ENDS. Защёлкнутый сегмент кода защищённого режима. Код этого сегмента всегда располагается в оперативной памяти. В сегменте размещаются обработчики аппаратных прерываний и другой код критичный ко времени исполнения. Сегмент начинается директивой VXD_LOCKED_CODE_SEG и заканчивается директивой VXD_LOCKED_CODE_ENDS. Защелкнутый сегмент данных защищённого режима. Сегмент используется для хранения данных критичных ко времени доступа. Данные этого сегмента всегда располагаются в оперативной памяти и не могут быть выгружены на диск. Сегмент объявляется директивами VXD_LOCKED_DATA_SEG и VXD_LOCKED_DATA_ENDS. Сегмент инициализации реального режима. Сегмент содержит код реального режима и данные. В сегменте должна находиться процедура, которая вызывается при загрузке виртуального устройства, ещё до переключения системы в защищённый режим. Этот сегмент используется в статических виртуальных устройствах, которые загружаются при запуске операционной системы. Начало и конец сегмента объявляются директивами VXD_REAL_INIT_SEG и VXD_REAL_INIT_ENDS. Блок описания виртуального устройства. Каждое VxD имеет блок описания устройства - device description block (DDB). DDB это структура данных которая содержит важные сведения об устройстве: имя, идентификатор, адреса процедур точек входа и т.п. Тип этой структуры объявлен в файле vmm.inc(h). Некоторые поля этой структуры должны быть заданы в тексте виртуального устройства. Для этой операции используется макроопределение Declare_Virtual_Device. Declare_Virtual_Device , ,, , , \ , \ < Обработчик API V86>,
\
Имя задаёт имя VxD. Имя, как правило совпадает с именем файла. Старшая и младшая версии определяют номер версии VxD. Например, если старшая версия равна 1 а младшая равна 0, полная версия определяется как 1.0. Процедура обработки команд (Device control procedure) адрес системной точки входа виртуального устройства. Система вызывает эту процедуру для передачи устройству команд. Интерфейс этой функции описывается ниже. Идентификатор устройства (Device ID) уникальный идентификатор виртуального устройства. Если виртуальное устройство статическое (загружается при запуске операционной системы) или если оно предоставляет свои функции другим VxD или приложениям MS-DOS и Win16, наличие уникального идентификационного номера обязательно. Этот номер выделяется компанией Microsoft по запросу. Нумерация предназначена для предотвращения конфликтов при обращении к устройствам. Если идентификатор не требуется, он может быть задан константой UNDEFINED_DEVICE_ID. Эта константа объявлена в файле vmm.inc(h). Порядок загрузки определяет, в какой последовательности система загружает драйвер при запуске. Это двойное слово, которое указывается, в случае если виртуальное устройство должно быть загружено до или после каких либо других виртуальных устройств. Например, если устройство использует контроллер ПДП и инициализирует при загрузке какие либо внутренние структуры, связанные с этим контроллером, его порядок загрузки должен быть численно больше аналогичного параметра виртуального устройства VDMAD. Устройства численно меньшим значением этого параметра загружаются в первую очередь. VMM имеет порядок загрузки равный 0, следовательно он загружается первым при запуске операционной системы. В следующей таблице приводятся порядки загрузки некоторых виртуальных устройств. Менеджер виртуальных машин
VMM
00000000H 106
Программируемый прерываний Системный таймер Загрузчик VxD Нумератор Клавиатура Контроллер ПДП Shell
контроллер
VPICD
0C000000H
VTD VXDLDR ENUMERATOR VKD VDMAD SHELL
14000000H 16000000H 16800000H 38000000H 90000000H B0000000H
Виртуальное устройство Shell загружается в последнюю очередь. Если порядок загрузки устройства не важен, он может быть пропущен в тексте директивы Declare_Virual_Device. По умолчанию этот параметр равен UNDEFINED_INIT_ORDER = 80000000H. Эта константа объявлена в файле vmm.inc(h). Порядок загрузки имеет смысл только для статически загружаемых виртуальных устройств. Обработчик API V86 адрес точки входа для приложений V86. Эта процедура вызывается системой при обращении к виртуальному устройству из программ MS-DOS. Обработчик API защищённого режима адрес точки входа для 16-ти разрядных приложений Windows. Эта процедура вызывается системой при обращении к виртуальному устройству из приложений Win16. Пример объявления блока описания виртуального устройства: MYVXD_ID EQU 7FEDH MYVXD_INIT_ORDER EQU 78000000H Declare_Virtual_Device MYVXD, 1, 0, ControlDispatch, \ MYVXD_ID, MYVXD_INIT_ORDER, \ V86Handler, PMHandler В этом примере заданы все аргументы макроопределения. Обратная косая черта «\» используется для переноса макроопределения на следующую строку. Если некоторые свойства VxD не используются или задаются по умолчанию, они пропускаются в макроопределении. Declare_Virtual_Device MYVXD, 1, 0, OFFSET32 ControlDispatch, , , , В этом примере блок устройства объявляется с UNDEFINED_DEVICE_ID и UNDEFINED_INIT_ORDER. Кроме того устройство не имеет обработчиков API функций для программ Win16 и MS-DOS. Процедура обработки команд (Device control procedure). Эта процедура является системной точкой входа виртуального устройства. Система взаимодействует с VxD, вызывая процедуру обработки команд. Процедура должна обязательно присутствовать в тексте виртуального устройства и её адрес должен быть указан в блоке описания. Система вызывает процедуру для передачи устройству сообщений о некоторых системных событиях. Процедура обработки команд имеет стандартный интерфейс. Этот интерфейс позволяет передать устройству код сообщения вместе с дополнительными параметрами и получить результаты обработки сообщения. Система всегда предаёт код сообщения в регистре Eax и ссылку на виртуальную машину, из которой происходит вызов процедуры в регистре Ebx. Обработчик сообщения, как правило, возвращает результат обработки, устанавливая флаг переноса (инструкция stc) в случае ошибки и сбрасывая его (инструкция clc) в случае нормального завершения. Как правило, процедура обработки команд состоит из оператора switch, в котором проверяется код сообщение и делаются вызовы процедур обработчиков сообщений. В файле vmm.inc(h) объявлены макроопределения Begin_Control_Dispatch, End_Control_Dispatch и Control_Dispatch, которые могут быть использованы для этой операции: Begin_Control_Dispatch VSAMPLED Control_Dispatch Sys_Critical_Init, VSAMPLED_Crit_Init Control_Dispatch Device_Init, VSAMPLED_Device_Init 107
Control_Dispatch Sys_Critical_Exit, VSAMPLED_Crit_Exit End_Control_Dispatch VSAMPLED Begin_Control_Dispatch и End_Control_Dispatch имеют единственный аргумент - имя VxD. Первое макроопределение задаёт начало «оператора switch» а второе его окончание. Control_Dispatch используется для связи кода сообщения и процедуры его обработки. В приведённом примере процедура обработки команд вызывает процедуру VSAMPLED_Crit_Init по сообщению с кодом Sys_Critical_Init. Константа Sys_Critical_Init как и все другие коды сообщений объявлена в заголовочном файле DDK vmm.inc(h). Этот фрагмент кода также передаёт на обработку сообщения Device_Init и Sys_Critical_Exit. Первый аргумент макроопределения Control_Dispatch задаёт код сообщения, а второй адрес процедуры обработчика сообщения. Без использования макроопределений код выглядел бы следующим образом: cmp Eax, Sys_Critical_Init jne case_1 call VSAMPLED_Crit_Init jmp case_end case_1: cmp Eax, Device_Init jne case_2 call VSAMPLED_Device_Init jmp case_end case_2: cmp Eax, Sys_Critical_Exit jne case_end call Sys_Critical_Exit case_end: ret Системные сообщения. Как уже было отмечено, операционная система использует процедуру обработки команд для передачи устройству различных системных сообщений. Этот метод напоминает способ организации доступа к динамическим библиотекам. В данном случае процедура обработки команд играет роль функции DllMain. Но в отличие от DLL, которая получает только четыре системных сообщения через функцию DLLMain виртуальное устройство может получать и обрабатывать гораздо больше системных сообщений. Некоторые наиболее часто используемые системные сообщения приводятся в следующей таблице: Create_VM Destroy_VM Sys_Critical_Init Device_Init
Init_Complete System_Exit Sys_Critical_Exit Reboot_Processor
Сообщение посылается при создании новой виртуальной машины Сообщение посылается при закрытии виртуальной машины. Первое сообщение, которое получает VxD при загрузке системы. Прерывания запрещены, т.о. VxD должно обработать это сообщение как можно быстрее. Второе сообщение, которое получает VxD при загрузке системы. Прерывания разрешены. VxD может установить обработчики прерываний и выполнить другие операции по инициализации. Третье (последнее) сообщение которое VxD получает при загрузке системы. Сообщение указывает, что инициализация системы завершена. Первое сообщение, которое VxD получает при завершении работы системы. Прерывания разрешены. Последнее сообщение, которое посылается виртуальному устройству при завершении работы системы. Прерывания запрещены. Сообщение о том, что пользователь выполнил команду перезапуск компьютера.
Системные сообщения (примеры) Здесь приведены лишь несколько примеров системных сообщений. Полное описание приводится в документации DDK. Следует отметить, что обработка всех возможных 108
сообщений необязательна. Устройство должно содержать только обработчики необходимых ему сообщений. Интерфейсы виртуальных устройств. Каждое виртуальное устройство может взаимодействовать с другими виртуальными устройствами, приложениями Win32, Win16 и MS-DOS. Макроопределение Declare_Virtual_Device позволяет задать отдельные точки входа для приложений, однако интерфейс между виртуальными устройствами организован по другой схеме с использованием так называемых сервисных функций (services). На рисунке показаны различные типы интерфейсов VxD.
Приложения Win32 Процедура Device control
Приложения Win16 Обработчик PM API
Виртуальное Устройство
Приложения MS-DOS
Обработчик V86 API
Сервисы
Другие VxD
Рисунок 4. Интерфейсы VxD. Интерфейс с приложениями Win32. Приложение Win32 может обращаться к виртуальному устройству. Обратная операция, когда инициатором обмена является VxD более сложна и будет рассмотрена позднее. Приложения Win32 могут вызывать API функции устройства через процедуру обработки команд VxD. Для организации обмена данными с VxD приложение Win32 использует несколько функций API. Интерфейс приложений Win32. Пользовательский уровень. Для обращения к виртуальному устройству приложение должно знать имя VxD и набор кодов команд, которые поддерживает виртуальное устройство. Приложение не может вызвать напрямую какую либо функцию VxD. Вместо этого все обращения к VxD осуществляются через процедуру обработки команд. Этой процедуре при вызове передаётся код команды, которую требуется выполнить и набор параметров соответствующий этой команде. Перед началом работы с виртуальным устройством приложение должно получить на него ссылку (открыть виртуальное устройство). Ссылку на VxD возвращает функция CreateFile. Операция получения ссылки на VxD в этом смысле похожа на операцию открытия файла. Для того чтобы открыть виртуальное устройство функция CreateFile вызывается с определённым набором аргументов: HANDLE hVxD; hVxD = CreateFile(«\\\\.\\MYVXD.VXD», 0, 0, 0, 0, FILE_FLAG_DELETE_ON_CLOSE, 0); где MYVXD.VXD это имя файла виртуального устройства. Специальный формат строки с именем устройства включает комбинацию символов \\.\. Это означает что функция CreateFile используется для получения ссылки на VxD а не для открытия файла MYVXD.VXD. Число символов «\» в строке удваивается из за особенностей синтаксиса языка Си. В Cи символ «\» в строке используется для вставки специальных символов, поэтому для того чтобы вставить в строку один такой символ его необходимо прописать дважды. Вариант открытия VxD на языке Паскаль (Delphi) приводится ниже: hVxD := CreateFile(«\\.\MYVXD.VXD», 0, 0, Nil, 0 109
FILE_FLAG_DELETE_ON_CLOSE, 0); Если ссылка которую возвращает функция CreateFile не равна нулю операция прошла успешно. Эта ссылка используется в дальнейшем для обращения к виртуальному устройству. Для передачи команд виртуальному устройству используется функция API DeviceIoControl. BOOL DeviceIoControl( HANDLE hDevice, //ссылка на устройство DWORD dwIoControlCode, //код команды LPVOID lpInBuffer, //указатель на буфер входных данных команды DWORD nInBufferSize, //размер буфера входных данных LPVOID lpOutBuffer, //указатель на выходной буфер данных команды DWORD nOutBufferSize, //размер выходного буфера LPDWORD lpBytesReturned, //указатель на число байтов заисанных в //выходной буфер драйвером LPOVERLAPPED lpOverlapped //указатель на структуру для организации //асинхронного обмена ); hDevice это ссылка на устройство которую возвращает функция CreateFile. dwIoControlCode задаёт код команды или другими словами номер функции виртуального устройства которую вызывает приложение. Виртуальное устройство получает код команды вместе с указателями на входной и выходной буфер данных, и вызывает обработчик команды с кодом указанным в параметре dwIoControlCode. Коды команд задаются разработчиком виртуального устройства. Буферы входных и выходных данных размещаются приложением. Наличие и содержимое этих буферов зависит от кода команды. Если например приложение только передаёт данные виртуальному устройству параметр lpOutBuffer должен быть равен Null и nOutBufferSize равен нулю. Функция возвращает количество байтов переданных приложению в выходном буфере (в случае если он используется) по указателю LpBytesReturned. Это значение может отличаться от заказанного в параметре nOutBufferSize. LpOverlapped указывает на структуру типа OVERLAPPED. Эта структура используется только в асинхронных операциях ввода-вывода. Если DeviceIoControl вызывается синхронно, этот параметр должен быть равен NULL. Различие между синхронными и асинхронными вызовами заключается в том, что в первом случае функция возвращает управление только после окончания операции, а в случае асинхронного обмена функция возвращает управление немедленно, при этом операция продолжает исполняться в фоновом режиме. Приложение может проверить результат операции с помощью функций ожидания. Функция DeviceIoControl возвращает TRUE, если операция успешно завершена. Пример синхронного вызова функции DeviceIoControl: #define IOCTL_ID_1
100
BOOL Res; DWORD dwInParam; DWORD cbReturned; char cBufOut[20]; Res = DeviceIoControl(hVxD, IOCTL_ID_1, &dwInParam, sizeof(dwInParam), cBufOut, 20, &cbReturned, NULL); В этом примере виртуальному устройству передаётся команда с кодом IOCTL_ID_1 = 100 и одновременно 32-х битовый параметр команды, значение которого записано в переменную dwInParam. VxD выполняет команду и возвращает результат в массиве cBufOut вместе с количеством байт записанных в массив в переменной cbReturned. Ссылка на VxD должна быть закрыта по окончании работы с виртуальным устройством. Для этого используется стандартная функция Windows API CloseHandle. CloseHandle(hVxD);
110
Интерфейс приложений Win32. Системный уровень. Операционная система транслирует вызов API функции DeviceIoControlCall в вызов функции обработки команд виртуального устройства с кодом сообщения W32_DeviceIoControl. Это сообщение должно быть обработано вместе с системными сообщениями по стандартной схеме: Control_Dispatch W32_DeviceIoControl, MYVXD_DeviceIoControl Обработчик сообщения MYVXD_DeviceIoControl должен размещаться в сегменте кода или фиксированном сегменте кода защищённого режима виртуального устройства. При вызове обработчика ему передаётся следующий набор параметров: Ecx - код команды. VxD как правило содержит набор функций которые могут быть вызваны из приложений Win32 (через процедуру обработки команд). Каждая такая функция соответствует одному из кодов команд. Код команды задаётся вызывающей программой и обрабатывается виртуальным устройством. Приложение не может вызывать функции VxD по имени, как это делается в случае обращения к динамическим библиотекам. Вместо этого приложение указывает код команды (номер функции) который передаётся в регистре Ecx.. Ebx содержит указатель на блок описания устройства. Edx содержит ссылку на виртуальное устройство. Esi содержит указатель на структуру DIOCParams. Эта структура используется передачи данных в обе стороны между виртуальным устройством и вызывающим приложением. Описание этой структуры приводится ниже: DIOCParams STRUC Internal1 DD ? VMHandle DD ? Internal2 DD ? dwIoControlCode DD ? lpvInBuffer DD ? cbInBuffer DD ? lpvOutBuffer DD ? cbOutBuffer DD ? lpcbBytesReturned DD ? lpoOverlapped DD ? hDevice DD ? tagProcess DD ? DIOCParams ENDS К наиболее важным полям этой структуры относятся: VMHandle - ссылка на виртуальную машину из которой был произведён вызов(ссылка на системную виртуальную машину). DwIoControlCode код команды который дублируется в регистре Ecx. LpvInBufer указатель на входной буфер данных переданный из приложения Win32. CbInBuffer размер входного буфера в байтах. LpvOutBuffer указатель на выходной буфер в котором виртуальное устройство возвращает данные приложению Win32. CbOutBuffer размер выходного буфера в байтах. LpcbBytesReturned указатель на двойное слово в которое виртуальное устройство должно записать действительный размер данных которые VxD возвращает приложению в выходном буфере. LpoOverlapped указатель на структуру OVERLAPPED, которая используется для организации асинхронного обмена. Правила использования этой структуры будут обсуждаться позднее. Обработчик команд DeviceIoControl работает по принципу процедуры обработки сообщений. В его задачу входит диспетчеризация команд.
111
Процедура Device control switch Номер сообщения ...
case W32_DeviceIoControl ...
Обработчик IOCTL_ID_1
Обработчики других сообщений
DeviceIoControl handler switch Управляющий Код ... case IOCTL_ID_N
Обработчик IOCTL_ID_N
...
Рисунок 5. Диспетчеризация Управляющих кодов. Обработчик DeviceIoControl должен перед возвратом управления записать код ошибки в регистр Eax. Если операция завершена успешно, регистр Eax должен содержать нулевое значение. Пример процедуры DeviceIoControl: cmp Ecx, IOCTL_GET_VERSION jne case_1 call GetVersion jmp case_end_ok case_1: cmp Ecx, IOCTL_DO_ANYTHING_ELSE xor Eax, Eax jne case_not_supported call DoAnythingElse xor Eax, Eax jmp case_end_ok case_not_supported: mov Eax, 1 ret case_end_ok: Xor Eax, Eax ret Этот фрагмент обрабатывает две команды или, другими словами позволяет вызвать приложений Win32 две функции виртуального устройства GetVersion и DoAnythingElse. Для того чтобы вызвать функцию GetVersion приложение должно указать код команды IOCTL_GET_VERSION в параметре dwIoControlCode функции DeviceIoControl. Номер версии возвращается в переменной, на которую указывает поле lpvOutBuffer структуры DIOCParams (указатель на структуру находится в регистре Esi). Функция GetVersion может выглядеть так: IOCTL_GET_VERSION
EQU 200h
BeginProc GetVersion Mov Edi , [Esi].DIOCParams.lpvOutBuffer Mov Dword Ptr [Edi], 10h Mov Edi, [Esi].DIOCParams.lpcbBytesReturned Mov Dword Ptr[Edi], 4 Ret EndProc GetVersion 112
Заметим, что начало, и окончание процедуры задаётся по правилам макроассемблера MASM директивами BeginProc и EndProc. Приведём в завершение код приложения вызывающий функцию GetVersion: #define IOCTL_GET_VERSION 0x200 DWORD dwVersion; DWORD cbRet; DeviceIoControl(hVxD, IOCTL_GET_VERSION, NULL, 0, &dwVersion, 4, &cbRet, NULL); VxD возвращает номер версии (0x10) в переменной dwVersion и число записанных байт (4) в переменной cbRet. Интерфейсы приложений Win16 и MS-DOS. Эти интерфейсы используются для обеспечения связи приложений Win16 и MS-DOS с виртуальными устройствами. Интерфейс Win16 исторически называется интерфейсом защищённого режима, так как в операционных системах Windows 3.x под определение программам защищённого режима подпадали только 16-ти разрядные приложения Windows. Приложения MS-DOS и Win16 должны использовать для доступа к виртуальным устройствам программное прерывание 2Fh. Функция 1684h возвращает адрес точки входа виртуального устройства. Приложения Win16 получают с помощью этой функции адрес обработчика API защищённого режима, а приложения MS-DOS адрес обработчика API V86. Адреса этих точек входа должны быть указаны в макроопределении Declare_Virtual_Device виртуального устройства. В приведённом ниже примере показана процедура получения адреса точки входа виртуального устройства. Эта процедура одинакова для программ Win16 и MS-DOS. Различие заключается в формате адреса точки входа, который возвращает функция прерывания. mov mov int mov or mov mov Jz
ax, 1684h bx, MYVXD_ID 2Fh ax, es ax, di Word Ptr API, di Word Ptr API+2, ax error
Функция возвращает адрес точки входа в паре регистров Es:Di в формате СЕЛЕКТОР:СМЕЩЕНИЕ для приложений Win16 или в формате СЕГМЕНТ:СМЕЩЕНИЕ для приложений MS-DOS. Нулевой адрес возвращается в случае ошибки. MYVXD_ID это уникальный идентификатор виртуального устройства заданный в макроопределении Declare_Virtual_Device. В этом примере адрес сохраняется в переменной API типа dword. Точка входа вызывается как обычная дальняя функция: mov ax, GET_VERSION call Dword Ptr [API] mov Version, bx Параметры, которые приложение передаёт и получает от виртуального устройства, должны размещаться в регистрах. В примере перед вызовом функции приложение записывает в регистр ax константу GET_VERSION. Эта константа передаётся обработчику API (V86 или защищённого режима). Обработчик по значению регистра ax определяет, какую из функций устройства вызывает приложение. В данном случае по соглашению это функция GetVersion. Функция возвращает номер версии в регистре bx. Однако вызов точки входа и передача значений через регистры между приложением и виртуальным устройством происходит не на прямую. Система должна транслировать вызовы из виртуальных машин V86 и 16-ти разрядных приложений в 32-х битовую оболочку, в которой работает VxD. При этом содержимое регистров приложения на момент вызова обработчика API VxD сохраняется в специальной структуре регистров клиента Client_Reg_Struc. Перед возвратом управления вызывающему приложению система восстанавливает значения регистров из этой структуры. Виртуальное устройство использует структуру регистров клиента для чтения и изменения значений регистров вызывающего приложения. Как правило один из регистров ( например ax) используется для передачи кода команды (номера функции) обработчику API 113
виртуального устройства. Код команды применяется для той же цели что и в интерфейсе с приложениями Win32. Точка входа представляет собой диспетчер, в задачу которого входит вызов обработчиков конкретных команд. Указатель на структуру Client_Reg_Struct передаётся виртуальному устройству в регистре ebp. В ряде случаев обработчик API защищённого режима и обработчик API V86 это одна и та же процедура. Разные обработчики для программ Win16 и MS-DOS приходится писать только в некоторых специфических ситуациях. Declare_Virtual_Device MYVXD, 1, 0, ControlDispatch, \ MYVXD_ID, MYVXD_INIT_ORDER, \ PM_and_V86_EntryPoint, PM_and_V86_EntryPoint VXD_CODE_SEG BeginProc PM_and_V86_EntryPoint cmp [ebp.Client_Ax], GET_VERSION mov [ebp.Client_Bx], 10h and [ebp.Client_Flags], NOT CF_Mask jmp case_end ret case_1: ... case_not_supported: or [ebp.Client_Flags], CF_Mask ret EndProc PM_and_V86_EntryPoint
//сбросить флаг переноса
//установить флаг переноса
VXD_CODE_ENDS Этот фрагмент обрабатывает команду GET_VERSION, возвращая в регистре bx номер версии (10h) и сбрасывая флаг переноса, или устанавливает флаг переноса если команда не поддерживается виртуальным устройством. Существует другой способ организации интерфейса приложений Win 16 и MS_DOS. В этой технологии так же используется прерывание 2Fh. VxD устанавливает свой собственный обработчик функции прерывания 2F. Приложения MS-DOS и Win16 вызывают функции не через точку входа и через программное прерывание 2Fh. Прерывание 2F может быть перехвачено виртуальным устройством в обработчике системного сообщения Device_Init: BeginProc DeviceInitHandler Mov Eax, 2Fh Mov Esi, OFFSET32 V86_Interface_Handler VMMCall Hook_V86_Int_Chain EndProc DeviceInitHandler Функция Менеджера виртуальных машин Hook_V86_Int_Chain предназначена для установки обработчика программного прерывания. Регистр Eax перед вызовом функции должен содержать номер прерывания а регистр Esi 32-х разрядный виртуальный адрес процедуры обработчика прерывания. Ниже приводится пример обработчика прерывания 2F для использования в приложениях MS-DOS: GET_MYVXD_VERSION
EQU 0E2h
BeginProc V86_Interface_Handler mov Ax, [Ebp.Client_ax] cmp Ah, GET_MYVXD_VERSION jnz case_not_supported mov [ebp.Client_bx], 10h clc ret case_not_supported: stc ret EndProc V86_Interface_Handler 114
Приложение MS-DOS может получить номер версии виртуального устройства MYVXD, как показано в следующем примере: mov Int jc mov
ax, 0E2h 2Fh error Version, bx
GET_MYVXD_VERSION номер функции прерывания 2F которая обрабатывается виртуальным устройством MYVXD. При использовании этого способа доступа к виртуальному устройству отсутствует необходимость в указании обработчика API V86 в макроопределении Declare_Virtual_Device. Сервисы VxD (Services). Для организации взаимодействия виртуальных устройств друг с другом используются так называемые сервисы. Сервисы это функции, которые экспортируются виртуальным устройством. Каждое виртуальное устройство может вызывать сервисы других VxD и Менеджера виртуальных машин(VMM). Функция Hook_V86_Int_Chain которая использовалась ранее для установки обработчика программного прерывания это один из сервисов VMM. В тексте виртуального устройства сервис как и обычная процедура объявляется директивами BeginProc и EndProc. Дополнительно к этому указываются директивы Service или AsyncService. Например: BeginProc MYVXD_GetVersion, Service mov ax, 10h clc ret EndProc MYVXD_GetVersion С помощью директивы AsyncService указывается, что сервис может быть вызван асинхронно, например из обработчика аппаратного прерывания. Асинхронные сервисы должны быть реентерабельны и не должны вызывать синхронные сервисы. Процедуры сервисов должны располагаться в одном из сегментов кода защищённого режима VxD. Все сервисы описываются в таблице сервисов VxD (Service Table). Эта таблица создаётся с помощью макроопределений Begin_Service_Table, End_Service_Table и xxxx_Service. где xxxx имя VxD указанноё в блоке описания устройства (Declare_Virtual_Device). Например: Begin_Service_Table MYVXD MYVXD_Service GetVersion MYVXD_Service Service1 MYVXD_Service Service2, VXD_ICODE End_Service_Table MYVXD Для каждого сервиса создаётся отдельная запись в таблице. Первый параметр макроопределения _Service задаёт имя сервиса. По умолчанию, считается что сервис располагается в сегменте VXD_CODE_SEG. Если в действительности процедура сервиса находится в другом сегменте это должно быть указано в аргументах макроопределения (например, VXD_ICODE). Описание таблицы сервисов должно присутствовать в тексте VxD которое экспортирует сервисы и в текстах виртуальных устройств, которые импортируют сервисы этого VxD. Кроме того, текст VxD, которое экспортирует сервисы, должен содержать следующую строку Create_xxxx_Service_Table EQU 1 перед описанием таблицы сервисов. В данном случае Create_MYVXD_Service_Table EQU 1 Как правило вместе с VxD поставляется .inc файл с описанием таблицы сервисов которые экспортирует устройство. Этот файл включается в проекты VxD, которые используют его сервисы: include MYVXD.INC Для вызова сервиса вместо инструкции call используется макроопределение VxDCall: VxDCall MYVXD_GetVersion 115
mov
Version, Eax
Для вызова например: VMMCall
сервиса
VMM следует
использовать
макроопределение
VMMCall,
Hook_V86_Int_Chain
Сервис GetVersion виртуального устройства играет особую роль. Этот сервис должен возвращать в регистре eax ненулевое значение номера версии VxD. С помощью этого сервиса можно определить факт загрузки VxD. Если вызывается несуществующий сервис VxD или вызывается сервис VxD, которое не загружено, система отображает «голубой экран» с сообщением об ошибке. Для сервисов с именами xxx_GetVersion, где xxx имя VxD делается исключение. Например, если имя VxD MYVXD его сервис, возвращающий версию должен иметь имя MYVXD_GetVersion. Если VMM не в состоянии обнаружить VxD с именем MYVXD или виртуальное устройство MYVXD не имеет такого сервиса, система не отображает сообщение об ошибке и возвращает в регистре eax ноль. Этот механизм позволяет вызывающим устройствам произвести проверку наличия VxD перед вызовом других его сервисов. Базовая структура исходного текста VxD. Шаблон исходного текста, который может быть использован как основа для разработки виртуальных устройств приводится в приложении 1. Динамически загружаемые виртуальные устройства. В Windows 3.x предоставляется возможность загрузки виртуальных устройств только в процессе загрузки операционной системы (Статически загружаемые или статические VxD) . Windows 95 имеет дополнительно механизм загрузки и выгрузки VxDs во время работы ОС (Динамически загружаемые или динамические VxD). Приложения Win32 могут обращаться к статически и динамически загружаемым VxD. Функция CreateFile загружает VxD или создаёт ссылку на VxD если виртуальное устройство загружено при запуске системы или предыдущим вызовом функции CreateFile. Если CreateFile вызывается для открытия динамического устройства повторно, система не создаёт несколько копий VxD в памяти, но вместо этого инкрементирует внутренний счётчик загрузки. Функция CloseHandle уменьшает этот счётчик на единицу. Когда счётчик достигнет нулевого значения, VxD выгружается из памяти. Динамически загружаемое VxD разрабатывается в том случае если нет необходимости в использовании функций виртуального устройства на протяжении всей работы системы. Такое VxD временно захватывает системные ресурсы и освобождает их когда работа с устройством завершается. Механизм доступа к функциям динамически и статически загружаемых устройств из приложений Win32 одинаков. Для этой цели используется функция DeviceIoControl. Исходный текст динамически загружаемого VxD практически не отличается от текста статически загружаемого VxD. Принципиальная разница между этими двумя типами устройств заключается том, что динамически загружаемые VxD могут получать и обрабатывать некоторые дополнительные системные сообщения и не получают некоторые сообщения предназначенные только для статических VxD. Действительно, если VxD загружается после загрузки операционной системы оно не может получить например сообщение Sys_Critical_Init. Вместо этого при динамической загрузке система посылает устройству сообщение Sys_Dynamic_Device_Init а при выгрузке сообщение Sys_Dynamic_Device_Exit. Эти сообщения не посылаются статически загружаемым виртуальным устройствам. Эти два сообщения, как и все другие, должны быть переадресованы обработчикам в процедуре обработки команд. Например: BeginProc MYVXD_ControlProc Begin_Control_Dispatch MYVXD Control_Dispatch Sys_Dynamic_Device_Init, Control_Dispatch Sys_Dynamic_Device_Exit, Control_Dispatch W32_DeviceIoControl, End_Control_Dispatch MYVXD clc ret
MYVXD_DeviceInit MYVXD_DeviceExit MYVXD_DeviceIoControl
EndProc MYVXD_ControlProc 116
Дополнительно система посылает динамически загружаемому устройству сообщение W32_DeviceIoControl с кодом команды DIOC_OPEN при загрузке VxD или создании новой ссылки на VxD и с кодом DIOC_CLOSE_HANDLE при выгрузке VxD или уничтожении ссылки на него (CloseHandle). Эти коды должны быть обработаны в данном случае в процедуре MYVXD_DeviceIoControl. Обработчик кода DIOC_OPEN возвращает ноль в регистре Eax, сообщая системе, что устройство поддерживает интерфейс с приложениями Win32. BeginProc MYVXD_DeviceIoControl cmp ecx, DIOC_OPEN jne case_1 mov eax, 0 ret case_1: ... ret EndProc MYVXD_DeviceIoControl Обработчик MYVXD_DeviceInit должен сбросить флаг переноса и записать ноль в регистр Eax в случае успешной инициализации устройства. В противном случае устройство не будет загружено. BeginProc MYVXD_DeviceInit ;any initialization code xor eax, eax clc ret EndProc MYVXD_DeviceInit Кроме этого в тексте динамического виртуального устройства должна быть объявлена константа xxxx_Dynamic (где xxxx имя VxD): MYVXD_DYNAMIC
EQU 1
В .DEF файле проекта VxD следует указать, что устройство динамически загружаемое: VXD MYVXD DYNAMIC Содержимое .DEF файла будет рассмотрено более подробно позднее. Шаблон исходного текста который может быть использован как основа для разработки динамически загружаемых виртуальных устройств приводится в приложении 2. Разработка VxD на языке Си. Виртуальные устройства могут быть частично написаны на языках ассемблер и Си. Следует отметить, что для разработки драйверов для NT использование ассемблера вовсе необязательно. Однако проект VxD должен содержать, по крайней мере, небольшой фрагмент ассемблерного текста. Этот фрагмент может быть откомпилирован в объектный модуль один раз, и в дальнейшем, если вы пишете на языке Си возвращаться к нему нет необходимости. Как правило, этот ассемблерный текст содержит инициализацию блока описания устройства, и процедуру обработки команд: .386p .xlist include vmm.inc .list MYVXD_DYNAMIC EQU 1 MYVXD_DEVICE_ID EQU 19ABH DECLARE_VIRTUAL_DEVICE MYVXD, 1, 0, ASYNCW32_Control, \ MYVXD_DEVICE_ID, UNDEFINED_INIT_ORDER VxD_LOCKED_CODE_SEG BeginProc MYVXD_Control Control_Dispatch SYS_DYNAMIC_DEVICE_INIT, MYVXD_Dynamic_Init, sCall 117
Control_Dispatch SYS_DYNAMIC_DEVICE_EXIT, MYVXD_Dynamic_Exit, sCall Control_Dispatch W32_DEVICEIOCONTROL, MYVXD_DeviceIOControl, \ sCall, <ecx, ebx, edx, esi> clc ret EndProc MYVXD_Control VxD_LOCKED_CODE_ENDS END В этом примере обработчик MYVXD_DeviceIoControl объявляется с 4 параметрами типа dword, , которые размещаются на стеке перед его вызовом. Эта функция вместе с другими указанными в диспетчере написана на Си и размещается в другом текстовом файле. Директивы sCall указывают на то, что функции имеют стандартную конвенцию вызова: первый параметр помещается в стек первым. Функции обработчики сообщений написанные на Си: DWORD _stdcall MYVXD_Dynamic_Init(void) { return(VXD_SUCCESS); } DWORD _stdcall MYVXD_Dynamic_Exit(void) { return(VXD_SUCCESS); } По соглашению принятому в Си функции возвращают результат типа dword в регистре eax. DWORD _stdcall MYVXD_DeviceIOControl(DWORD dwService, DWORD dwDDB, DWORD hDevice, LPDIOC lpDIOCParams) { DWORD dwRetVal = 0; if ( dwService == DIOC_OPEN ) { dwRetVal = 0; } else if ( dwService == DIOC_CLOSEHANDLE ) { dwRetVal = 0; } else if ( dwService == IOCTL_GET_VERSION ) { *(DWORD *)lpDIOCParams->lpvOutBuffer = 0x10; *(DWORD *)lpDIOCParams->lpcbBytesReturned = 4; dwRetVal = 0; } else dwRetVal = ERROR_NOT_SUPPORTED; } return dwRetVal; } Вызов сервисов виртуального устройства на языке Си. Большинство сервисов VMM и виртуальных устройств для передачи параметров используют регистры. Для языков высокого уровня включая Си это представляет определённую трудность, так как параметры функций передаются через стек. Интерфейсные файлы Си (.h) Windows DDK содержат объявления функций которые называются рапперы (wrappers). Раппер это функция Си которая вызывает какой либо определённый сервис. Раппер получает входные параметры по правилам Си через стек, переписывает их в регистры и вызывает сервис. Когда сервис возвращает управление, раппер перемещает выходные данные из регистров в стек и заканчивается как обычная функция Си. Программа использует рапперы вместо того, чтобы делать ассемблерные вставки на ассемблере для вызова сервисов. К сожалению рапперы имеются далеко не для всех сервисов. Однако они могут быть легко созданы. Следующий пример содержит текст раппера для вызова сервиса VWIN32_DIOCCompletionRoutine. Этот сервис устанавливает указанный объект синхронизации в состояние signaled. Отметим, что рапперы используются исключительно в целях удобства написания программы. void VXDINLINE 118
VWIN32_DIOCCompletionRoutine(DWORD KernelEvent) { __asm mov Ebx, [KernelEvent] __asm VxDCall (VWIN32_DIOCCompletionRoutine) return; } VxDCall () и VMMCall() это макросы объявленные в интерфейсном файле DDK vmm.h. Они используются соответственно для вызова сервисов VxD и VMM. Реализация функций вызываемых извне (callback functions) на языке Си. Функции Callback это особый часто использующийся при проектировании виртуальных устройств класс функций. Эти функции виртуального устройства вызываются системой в случае возникновения определённых событий. Например, к этому классу относится процедура обработки команд VxD. Другой пример это обработчик аппаратного прерывания. Система передаёт параметры при вызове таких функций через регистры. Функции языка Си имеют так называемые пролог и эпилог - фрагменты кода, которые автоматически вставляются компилятором в начало и конец функции. Этот код может изменить значения регистров заданные системой при вызове функции и возвращаемые функцией по завершении её работы. К счастью при использовании Microsoft Visual C 5.0 эта проблема может быть легко решена. Эта версия компилятора имеет директиву __declspec(naked) которая может быть использована при объявлении функции. Если функция объявлена с такой директивой, компилятор не вставляет в тело функции фрагменты пролога и эпилога. В этом случае функция содержит только код прямо заданный в тексте функции. Например, текст: DWORD Myfunction (ulong a); { ulong b = 0; a = 1; ... return 0; } без директивы __declspec(naked) будет откомпилирован в следующую последовательность инструкций: push mov push mov mov ... mov pop mov ret
ebp ebp, esp ecx Dword Ptr [ebp-4], 0 Dword Ptr [ebp+8], 1 esp, ebp ebp eax, 0 4
Локальные переменные и параметры функции по соглашению принятому в Си адресуются через регистр ebp. Содержимое стека показано на следующем рисунке:
Stack ebp-8 ebp-4 ebp = esp ebp+4
a eip ebp b
push a call push ebp push b
Из этого примера видно, что компилятор вставляет дополнительный код в начало и конец функции. Код этот необходим для того, чтобы обеспечить доступ к локальным 119
переменным и параметрам функции. Эти вставки запрещаются директивой __declspec(naked). Например следующий сервис VMM используется для организации таймаута. Set_Global_Time_Out(CallBackFunction, Milliseconds, RefData); При вызове сервиса указывается длительность таймаута, адрес функции, которая будет вызвана по завершению таймаута и значение, которое будет передано этой функции в качестве параметра. Когда время таймаута истекает, система вызывает функцию, адрес которой указан в аргументе CallBackFunction сервиса и передаёт ей через регистры: ecx - длительность таймаута в миллисекундах edx - заданное пользователем значение RefData. и некоторые другие параметры. Функция CallbackFunction объявляется с директивой __declspec(naked) для того чтобы параметры переданные ей через регистр не были испорчены кодом пролога: void __declspec(naked) TimeOutCallBack(void) { __asm ... ... __asm ret } Примеры задач решаемых с помощью виртуальных устройств. Пример 1. Обработка аппаратных прерываний. Сервисы необходимые для работы с аппаратными прерываниями предоставляются виртуальным программируемым контроллером прерываний VPICD.VxD. Таблица сервисов этого виртуального устройства содержится в файле VPICD.inc, который должен быть включён в проект VxD. include VPICD.inc Для установки обработчика аппаратного прерывания используется сервис VPICD_Virtualize_IRQ. Пере вызовом сервиса Virtualize_IRQ необходимо разместить и инициализировать поля структуры описателя линии прерывания VPICD_IRQ_Descriptor. Эта структура так же объявлена в файле vpicd.inc. В полях структуры указывается номер аппаратного прерывания, указатель на процедуру обработчик прерывания и некоторые дополнительные параметры. Формат структуры приводится ниже: VPICD_IRQ_Descriptor STRUC VID_IRQ_Number dw VID_Options dw VID_Hw_Int_Proc dd VID_Virt_Int_Proc dd VID_EOI_Proc dd VID_Mask_Change_Proc dd VID_IRET_Proc dd VID_IRET_Time_Out dd VID_Hw_Int_Ref dd VPICD_IRQ_Descriptor ENDS
? 0 ? 0 0 0 0 500 ?
VID_IRQ_number это номер аппаратного прерывания. В поле VID_Options указываются свойства обработчика прерывания. Например, флаг VPICD_OPT_CAN_SHARE указывает на то что в системе могут быть несколько обработчиков этого прерывания, которые вызываются последовательно при возникновении прерывания. Если флаг не установлен, виртуальное устройство использует линию прерывания монопольно. VID_Hw_Int_Proc задаёт адрес процедуры обработчика аппаратного прерывания. Этот параметр как и номер прерывания обязателен. Остальные поля можно не использовать. В этом случае их значения приравнивают нулю. Эти поля позволяют установить обработчики определённых событий связанных с контроллером прерываний. Например, поле VID_Mask_Change_Proc задаёт адрес процедуры, 120
которая вызывается при изменении содержимого регистра масок прерываний контроллера. Пример инициализации структуры для установки обработчика IntHandler аппаратного прерывания 11: V_IRQ_Dsc VPICD_IRQ_Descriptor \ Вызов сервиса Virtualize_IRQ: mov VxDcall jc mov
edi, OFFSET32 V_IRQ_Dsc VPICD_Virtualize_IRQ errorhandler [IRQHand], eax
В регистр Edi помещается адрес структуры VPICD_IRQ_Descriptor. Сервис устанавливает флаг переноса в случае ошибки и сбрасывает его в случае успешной установки обработчика. Сервис возвращает ссылку на виртуализированное прерывание в регистре eax. Эта ссылка используется в дальнейшем для управления линией прерывания: деинсталляции обработчика, маскирования линии прерывания и т.п. Например, линия прерывания маскируется сервисом VPICD_Physically_Mask и размаскируется сервисом VPICD_Physically_UnMask. mov eax, IRQHandle VxDcall VPICD_Physically_Mask mov eax, IRQHandle VxDcall VPICD_Physically_Unmask Эти сервисы не возвращают каких либо параметров. Для деинсталляции обработчика используется сервис: mov eax, IRQHandle VxDcall VPICD_Force_Default_Behavior Обработчик аппаратного прерывания на ассемблере, как и любая другая процедура объявляется директивами BeginProc и EndProc и заканчивается инструкцией ret. Дополнительно в директиве BeginProc может быть указан параметр High_Freq (High frequency). Обработчик должен располагаться в фиксированном сегменте кода защищённого режима. Обработчик аппаратного прерывания должен разрешить дальнейшую генерацию прерываний контроллером прерываний (Сбросить КП). Эта стандартная процедура в программах MS-DOS выполняется инструкцией out 20, 20 (для первого контроллера). VPICD имеет сервис VPICD_Phys_EOI который используется для сброса контроллера. mov eax, IRQHandle VxDcall VPICD_Phys_EOI Этот сервис должен быть вызван из обработчика аппаратного прерывания для сброса контроллера прерываний. Пример обработки аппаратного прерывания: VXD_LOCKED_DATA_SEG V_IRQ_Dsc VPICD_IRQ_Descriptor \ IRQHandler DD 0 VXD_LOCKED_DATA_ENDS VXD_LOCKED_CODE_SEG BeginProc SetInterrupt mov edi, OFFSET32 V_IRQ_Dsc VxDCall VPICD_Virtualize_IRQ 121
mov [IRQHandler], eax EndProc SetInterrupt BeginProc ResetInterrupt mov eax, [IRQHandler] VxDCall VPICD_Force_Default_Behaviour EndProc ResetInterrupt BeginProc IntHandler, High_Freq ;any code mov eax, IRQHandler VxDCall VPICD_Phys_EOI ret EndProc IntHandler, High_Freq VXD_LOCKED_CODE_ENDS Пример 2. Управление контроллером ПДП. Сервисы для управления контроллером ПДП объявлены в файле vdmad.inc. VDMAD это стандартное виртуальное устройство предназначенное для виртуализации и управления каналами ПДП. Сервис VDMAD_Virtualize_Channel используется для захвата и инициализации канала ПДП. mov eax, Channel mov esi, OFFSET32 CallbackProc VxDcall VDMAD_Virtualize_Channel jc ErrorHandler mov [hChannel], eax Перед вызовом сервиса регистр Eax должен содержать номер канала ПДП, Esi - адрес callback процедуры. Эта процедура вызывается при смене физического состояния канала ПДП. Для того чтобы это отследить VDMAD перехватывает обращения к портам контроллера. Этот параметр необязателен и может равняться нулю. Сервис возвращает ссылку на виртуализированный канал ПДП в регистре Eax. Сервис сообщает об ошибке, устанавливая флаг переноса. Сервис VDMAD_Unvirtualize_Channel используется для освобождения захваченного канала ПДП. mov eax, hChannel VxDcall VDMAD_Unvirtualize_Channel jc error Контроллер ПДП это аппаратное устройство, которое работает с физическими адресами. Данные для передачи с помощью ПДП должны размещаться в линейном физическом адресном пространстве. Если размер буфера данных больше 4КБ он размещается в нескольких страницах. Эти страницы должны располагаться в физической памяти друг за другом. Физическая память буфера ПДП должна быть непрерывна. VMM предоставляет сервис _Page_Allocate который может используется для резервирования страниц виртуальной памяти. Этот сервис так же может быть использован для размещения непрерывной памяти для буфера ПДП. VMMCall _PageAllocate, test eax, eax ;возвращает 0 в случае ошибки jz error mov [Address], eax ; виртуальный адрес блока памяти При размещении буфера для канала системного контроллера ПДП дополнительно необходимо руководствоваться следующими соображениями. В машинах AT буфер ПДП должен размещаться в нижних 16-ти мегабайтах физической памяти и не пересекать адреса, делящиеся без остатка на 64K и 128K. В примере, который приводится ниже, размещается буфер ПДП размером в три страницы (12K). Тип памяти буфера PG_SYS указывает на то, что буфер должен располагаться в системной памяти 122
(в нулевом кольце защиты). Параметр AlignMask задаёт выравнивание начала буфера по границе 16К. Этот параметр равен 011b. С таким выравниванием блок размером 12K не может пересекать адреса, делящиеся без остатка на 64K или 128K. Параметр MinPhys определяет минимальный номер страницы памяти, в которой системе разрешается разместить буфер. Этот параметр равен нулю. MaxPhys задаёт максимальный номер страницы в которой может размещаться буфер. В машинах AT этот параметр должен быть равен FFFh так как это максимальный номер страницы памяти, которая расположена ниже 16МБ: 1). Размер страницы равен 4K, 2). FFF+1 = 4096 = 4K, 3). 4K*4K = 16MB. При размещении памяти указываются три флага: PageUseAlign - Начальный адрес буфера должен быть выровнен по границе указанной в параметре AlignMask. PageContig - буфер размером 12K должен быть непрерывным в физичесой памяти. PageFixed - буфер фиксирован в памяти (всегда присутствует в физической памяти и не может быть выгружен на диск). VMMcall _PageAllocate > Сервис возвращает виртуальный адрес начала буфера в регистре eax и физический начальный адрес буфера в регистре ebx. Нулевое значение регистра eax указывает на ошибку. Для освобождения памяти используется сервис _PageFree. mov eax, [Address] VMMCall _PageFree <eax, 0> ;virtual address and flags or eax, eax ; nonzero if freed, zero if error jz failed После размещения буфер должен быть связан с виртуализированным каналом ПДП. Эта операция выполняется сервисом VDMAD_Set_Region_Info. mov eax, hChannel mov bl, Buffer mov bh, LockStatus mov esi, OFFSET32 Region mov ecx, RegionSize mov edx, OFFSET32 PhysAddress VxDcall VDMAD_Set_Region_Info hChannel это ссылка на канал ПДП которую возвращает сервис VDMAD_Virtualize_Channel. Buffer - идентификатор канала ПДП, который назначается пользователем и может равняться нулю. LockStatus определяет статус буфера фиксированный (не ноль) или нет (ноль). В данном случае буфер фиксирован в памяти, так как при его размещении был указан флаг PageFixed. Region это виртуальный а PhysAddress физический адрес начала буфера. Эти значения возвращаются при размещении буфера сервисом _PageAllocate. RegionSize размер буфера в байтах. В данном случае этот параметр равен 12K. Сервис не возвращает каких либо значений. Следующий шаг заключается в подготовке контроллера ПДП к передачи данных установке режима ПДП. Для этого используется сервис VDMAD_Set_Phys_State. mov eax, mov ebx, mov dl, mov dh, VxDcall
hChannel VMHandle Mode Ext_Mode VDMAD_Set_Phys_State
VM handle это ссылка на виртуальную машину (Ссылка на системную ВМ может быть получена с помощью сервиса VMMCall Get_Sys_VM_Handle ,который возвращает ссылку в регистре Ebx). 123
Mode и Ext_Mode задают режим работы контроллера ПДП 8237. Например для установки режима 16-ти разрядной записи в single mode следует занести в регистр dl комбинацию констант DMA_single_mode + DMA_type_write ( = 0x44), а в регистр dh константу _16_bit_xfer (=0x40). Заметим, что эта функция не предназначена для работы с контроллерами ПДП PS/2 и EISA. Для инициализации таких контроллеров имеются другие сервисы виртуального устройства VDMAD. Последняя операция заключается в размаскировании канала ПДП. mov edx, hChannel mov ebx, VMHandle VxDcall VDMAD_Phys_Unmask_Channel Функция не возвращает значений. Канал ПДП может быть снова замаскирован: mov eax, [hChannel] VxDCall VDMAD_Phys_Mask_Channel Каждая последующая операция ПДП должна сопровождаться указанием буфера ПДП ( VDMAD_Set_Region_Info) и установкой режима работы канала (VDMAD_Set_Phys_State). Обычно аппаратура, которая применяет для передачи данных механизм ПДП, использует аппаратное прерывания для того, чтобы сообщить программному обеспечению о завершении операции ПДП. В таком случае виртуальное устройство должно виртуализировать линию аппаратного прерывания и канал ПДП, разместить в памяти буфер ПДП и запустить операцию ПДП. Операция ПДП может быть инициирована, например пользовательским приложением, которое получает данные от устройства. В этом случае приложение должно разместить буфер для приёма данных. Обработчик аппаратного прерывания который вызывается по завершении операции ПДП копирует данные из системного буфера ПДП (размещённого по правилам изложенным выше /_PageAllocate/) в буфер подготовленный приложением и сообщает приложению о поступлении новых данных. Системный буфер удовлетворяет всем условиям работы с контроллером ПДП, в то время как буфер приложения может быть не непрерывным, пересекать запрещённые адреса, и располагаться в любом месте адресного пространства. Структура такой системы сбора данных приведена на рисунке:
Приложение Win 32.
Пользовательский буфер данных
1.Call GetData DeviceIoControl(IOCTL_GET_DATA, ...);
6.Сообщение Данные готовы 5.Копирование данных из системного буфера в пользовательский
Виртуальное устройство Системный непрерывный буфер ПДП
3.Копирование данных в системный буфер 2.Пуск чтения ПДП 4.Данные готовы. Прерывание.
Устройство сбора данных Рисунок 6. Чтение данных с использованием ПДП. Пример проекта.
124
Пример 3. Асинхронные сообщения. Виртуальное устройство получает асинхронные сообщения от аппаратуры через механизм прерываний. Передача сообщений от виртуальных устройств пользовательским программам включает ряд специальных программных приёмов. Операционные системы Windows 95/98/NT предлагают стандартное решение этой задачи основанное на использовании асинхронных операций ввода-вывода. Это единственный метод, который позволяет асинхронно предавать информацию от драйверов приложениям Win32 в Windows NT. Однако в операционных системах Windows 95/98 имеется альтернативный способ асинхронной передачи сообщений от драйверов пользовательским приложениям. VxD Shell предоставляет сервисы с помощью которых виртуальные устройства могут выступать в качестве инициаторов обмена данными с приложениями. Сервисы Shell. Сервисы виртуального устройства Shell объявлены в файле shell.inc, который входит в состав Windows 95/98 DDK. Сервис _SHELL_PostMessage используется для передачи сообщения Windows из VxD оконной функции заданного окна пользовательского приложения. Сообщения пересылается через очередь сообщений приложения. Этот сервис аналогичен функции API PostMessage, которую используют пользовательские приложения. Сервис немедленно возвращает управление, не дожидаясь, пока сообщение будет помещено в очередь, извлечено из очереди, доставлено адресату и обработано. Сервис позволяет установить callback функцию, (pfnCallback) которая вызывается системой после того, как сообщение будет помещено в очередь сообщений приложения. Если параметр pfnCallback при вызове сервиса равен NULL, функция не вызывается. VxDcall _SHELL_PostMessage, or eax, eax jz not_posted Сервис возвращает ноль в регистре eax если сообщение не может быть отправлено. Параметр HWnd задаёт ссылку (handle) на окно которому отправляется сообщение. Эта ссылку приложение должно заранее передать виртуальному устройству. UMsg это код сообщения - как правило константа в диапазоне который выделен пользовательским сообщениям (WM_USER + n). wParam и lParam параметры сообщения смысл которых определяется разработчиком. DwRefData это необязательный параметр значение которого будет передано callback функции при вызове. Callback функция должна иметь следующий интерфейс: cCall [pfnCallback], dwRc - флаг ошибки, который не равен нулю если сообщение отправлено, и равен нулю в случае ошибки. Виртуальное устройство может также использовать сервис _SHELL_BroadcastSystemMessage для отправки широковещательных сообщений заданному набору окон. VxD Shell экспортирует несколько сервисов для работы с 16-ти разрядными динамическими библиотеками. Сервис _SHELL_CallDll например используется для вызова экспортируемых функций 16-ти разрядных DLL. Этот механизм нельзя использовать для доступа к функциям 32-х разрядных динамических библиотек. Асинхронный ввод/вывод. Асинхронный (используется так же термин overlapped) ввод/вывод используется для организации передачи асинхронных сообщений от драйверов расположенных в системной области пользовательским приложениям Win32. 32-х разрядные приложения Windows могут обращаться к виртуальным устройствам с помощью API функции DeviceIoContol. Последний параметр в интерфейсе этой функции указывает на структуру типа OVERLAPPED. Напомню, что в случае синхронного вызова функции (когда она возвращает управление только после того, как заданная операция будет выполнена) структура не используется и этот параметр равен NULL. Структура OVERLAPPED должна содержать ссылку на объект синхронизации Event. Структура 125
передаётся виртуальному устройству вместе с кодом команды и буферами данных. Система рассматривает операцию как асинхронную в случае если: При открытии VxD (создании ссылки на VxD) был указан флаг FILE_FLAG_OVERLAPPED. HANDLE hVxD; hVxD = CreateFile(«\\\\.\\MYVXD.VXD», 0, 0, 0, 0, FILE_FLAG_DELETE_ON_CLOSE | FILE_FLAG_OVERLAPPED, 0);
Параметр lpOverlapped при вызове функции DeviceIoControl не равен NULL.
OVERLAPPED ovr; //create event ovr.hEvent = CreateEvent(NULL, FALSE, FALSE, NULL); Res = DeviceIoControl(hVxD, IOCTL_ID_1, &dwInParam, sizeof(dwInParam), cBufOut, 20, &cbReturned, &Ovr); Если виртуальное устройство немедленно выполняет операцию заданную кодом команды, асинхронный вызов превращается в синхронный. VxD заполняет выходной буфер данными и возвращает ноль в регистре Eax. В этом случае DeviceIoControl возвращает TRUE. Если операция не может быть выполнена сразу, обработчик команды VxD перед тем как вернуть управление должен выполнить следующую процедуру:
Защёлкнуть в физической памяти структуру OVERLAPPED, входной и выходной буферы. Указатели на эти параметры передаются в структуре DIOCParams. Память должна быть защёлкнута с флагом PAGEMAPGLOBAL. При этом память отображается в глобальное адресное пространство и становится доступной из любого контекста памяти. Переключение потоков (включая переключение контекстов памяти) производится системой независимо. При этом указатели на буферы и структуру OVERLAPPED должны быть достоверны всегда, пока в них есть необходимость, так как драйвер может обратиться к ним в любой момент времени и при любом контексте. Минимальный элемент памяти который может быть зафиксирован это страница(=4096 байт). Нужное количество страниц защёлкивается сервисом VMM _LinPageLock (и освобождается сервисом _LinPageUnlock). Запустить заданную операцию. Например ,запустить операцию чтения ПДП. Вернуть управление с -1 в регистре Eax. В этом случае функция DeviceIoControl вернёт False и вызванная затем функция GetLastError вернёт константу ERROR_IO_PENDING.
Когда операция будет закончена (например, операция чтения ПДП) драйвер должен выполнить следующую последовательность действий:
Переписать полученные данные в защёлкнутый выходной буфер. Установить объект event переданный драйверу в состояние signaled. Пользовательская программа помещает ссылку на объект в поле hEvent структуры OVERLAPPED. Система транслируя вызов функции DeviceIoControl перед тем как передать управление VxD помещает ссылку на объект event в поле O_Internal этой структуры. Виртуальное устройство должно брать ссылку на объект из поля O_Internal а не из поля h_Event. Эти ссылки имеют разные форматы. Пользовательские программы не могут работать с ссылками на объекты которые использует системное ПО. VxD получает ссылку в системном формате и именно с таким форматом ссылок работают сервисы управления объектами синхронизации. В данном случае для установки объекта event в состояние signaled следует использовать сервис VWIN32_DIOCCompletionRoutine. Единственным параметром при вызове является значение поля O_Internal структуры OVERLAPPED - системная ссылка на объект event. Этот параметр передаётся через регистр ebx.
3.Освободить защёлкнутую память буферов данных и структуры OVERLAPPED (_LinPageUnlock). 126
Пользовательское приложение может инициировать асинхронную операцию, вызвав API функцию DeviceIoControl. Затем так как операция выполняется асинхронно приложение может произвести какие либо действия и перейти в режим ожидания окончания операции. Для этого используется одна из функций ожидания. Поток приложения, который вызвал функцию ожидания исключается из цикла переключения потоков и не занимает процессорное время до тех пор пока виртуальное устройство не установит объект синхронизации event в состояние signaled. Обычно в асинхронных операциях ввода/вывода используется функция ожидания GetOverlappedResult. Со стороны приложений интерфейс асинхронного ввода/вывода стандартизирован и одинаков для всех 32-х разрядных ОС Windows. Реализации интерфейса на системном уровне для ОС Windows95/N отличаются. Нижеследующий текст показывает пример обработки асинхронной операции в виртуальном устройстве. LPOVERLAPPED sioLpo; PVOID sioOutBuf; DWORD sioOutBufSize; DWORD _stdcall MYVXD_DeviceIOControl(DWORD dwService, DWORD dwDDB, DWORD hDevice, LPDIOC lpDIOCParams) { switch (dwService) { case IOCTL_GET_DATA: //защёлкиваем структуру overlapped и сохраняем адрес в sioLpo sioLpo = (LPOVERLAPPED)lpDIOCParams->lpoOverlapped; sioLpo = (LPOVERLAPPED)PageLock((DWORD)sioLpo, sizeof(OVERLAPPED)); //защёлкиваем выходной буфер sioOutBufSize = lpDIOCParams->cbOutBuffer; sioOutBuf = lpDIOCParams->lpvOutBuffer; sioOutBuf = (PVOID)PageLock((DWORD)sioOutBuf, sioOutBufSize); //запускаем асинхронную операцию StartDMATransfer(); //возвращаем ERROR_IO_PENDING return -1; case ... }} //эта процедура отображает страницы содержащие заданную область //памяти lpMem - lpMem+cbSize в диапазон глобальных адресов, //защелкивает их и возвращает адрес защёлкнутого буфера DWORD _stdcall PageLock(DWORD lpMem, DWORD cbSize) { DWORD LinPageNum, LinOffset, nPages; LinOffset = lpMem & 0xfff; // смещение от начала страницы LinPageNum = lpMem >> 12; // номер первой страницы nPages = ((lpMem + cbSize) >> 12) - LinPageNum + 1; return (_LinPageLock(LinPageNum, nPages, PAGEMAPGLOBAL) + LinOffset); } //освобождает защёлкнутые страницы содержащие заданную область //памяти lpMem - lpMem+cbSize //lpMem это адрес который вернула функция PageLock void _stdcall PageUnlock(DWORD lpMem, DWORD cbSize) { DWORD LinPageNum, nPages; LinPageNum = lpMem >> 12; nPages = ((lpMem + cbSize) >> 12) - LinPageNum + 1; _LinPageUnlock(LinPageNum, nPages, PAGEMAPGLOBAL); 127
} //аппаратура вырабатывает прерывание по завершении операции ПДП //в обработчике данные копируются в выходной буфер и устанавливается //специальная callback функция которая будет вызвана системой после //того как завершится обработчик прерывания. Такой метод используется //потому, что сервис VWIN32_DIOCCompletionRoutine не может быть //вызван из обработчика прерывания. Подробнее см. документацию DDK. //обработчик прерывания void __declspec(naked) IntHandler(void) { ... //копируем данные из буфера ПДП в выходной буфер memcpy(sioOutBuf, dmaBuf, sioOutBufSize); ... //устанавливаем callback функцию (один из вариантов). __asm mov ebx, SYS_VM_HANDLE //ссылка на системную BM __asm mov esi, OFFSET EventCallback __VMMcall Schedule_VM_Event ... } //callback функция void __declspec(naked) EventCallback(void) { ... __asm mov edi, sioLpo __asm mov ebx, [edi].OVERLAPPED.O_Internal __asm VxDCall (VWIN32_DIOCCompletionRoutine) PageUnlock((DWORD)sioOutBuf, sioOutBufSize); PageUnlock((DWORD)sioLpo, sizeof(OVERLAPPED)); ... } Асинхронный ввод/вывод может быть использован для организации асинхронных «вызовов» приложений Win32 из нулевого кольца. Приложение должно инициировать асинхронную операцию и передать драйверу ссылку на объект синхронизации и указатель на буфер обмена данными. В дальнейшем приложение может, например, в специально созданном для этого потоке ожидать изменения состояния объекта. Драйвер для того, чтобы передать приложению асинхронное сообщение заполняет буфер информацией и переключает объект синхронизации в состояние signaled. Приложение обрабатывает полученное от драйвера сообщение и снова инициирует асинхронную операцию.
Приложение Win32
Поток ожидающий ввод. WaitForSingleObject();
Создаёт объект event 1. Передача ссылки на объект event
Объект Event
2. Установка объекта в состояние signaled
Виртуальное устройство Рисунок 7. Перехват асинхронных событий. 128
Основы проектирования драйверов WDM для Windows 98/2K. Автор: Сидякин И.М. Московский Государственный Технический Университет им. Н.Э. Баумана. Кафедра ИУ-3, (02.2000) Email:
[email protected] В операционных системах MS Windows 98 и Windows 2K, используется новая модель драйверов нулевого кольца. Эта модель получила название Модель Драйверов Windows (Windows Driver Model), сокращённо WDM. Драйверы WDM являются развитием структуры драйверов kernel, которая используется в операционных системах Windows NT. Модель WDM включает несколько новых концепций, которые позволяют использовать более гибкий подход при проектировании ПО для управления аппаратными ресурсами вычислительной системы. Драйверы WDM более универсальны. Они обладают свойством переносимости. Один и тот же драйвер может работать на различных аппаратных платформах. Базовые структуры драйверов kernel и WDM очень похожи. Это позволяет во многих случаях без особых проблем переделать драйвер kernel в драйвер WDM. Как правило, переделка заключается в замене функций системного программного интерфейса (SPI) которые использует драйвер. Функции, специфические для какой либо платформы удаляются или заменяются на универсальные. Можно сделать вывод, что спецификация WDM является подмножеством спецификации драйверов kernel. В тоже время модель WDM имеет ряд существенных дополнений, например в области организации интерфейса драйверов и поддержки технологии автоматической идентификации устройств Plug and Play. Если эти дополнения не используются в явном виде, исходный текст драйвера WDM может ничем не отличаться от текста драйвера kernel. В Модели Драйверов Windows широко используется сочетание мини-драйверов (minidriver) и так называемых драйверов классов (class driver). Для некоторых классов устройств, например, устройств подключаемых к шине USB или шине IEEE 1394 разработаны драйверы классов. Драйвер класса выполняет операции общие для всех устройств данного класса. Каждое отдельное устройство, входящее в класс обслуживается мини-драйвером, который разрабатывается производителем устройства. Мини-драйвер реализует функции специфические для устройства. Процедура разработки мини-драйвера упрощается за счёт того, что ряд функций по управлению устройством переносится в драйвер класса. Мини-драйверы устройств относящихся к различным классам пишутся согласно спецификациям, разработанным для этих классов. В этой главе не рассматриваются особенности проектирования мини-драйверов для существующих классов устройств. Здесь будет изложена общая структура драйвера WDM устройства, которое не относится к какому либо конкретному классу.
129
Средства проектирования драйверов WDM. Драйверы WDM пишутся, как правило, на языке Си. Автор допускает возможность разработки драйверов на других языках, однако сам не проводил подобных экспериментов. Поэтому далее будут обсуждаться приёмы программирования драйверов исключительно на языке Си. По той же причине для создания исполняемых модулей драйверов примеров следует использовать компилятор Си и компоновщик, входящие в состав Microsoft Visual C++ 5.0 (6.0). Драйверы можно компилировать из командной строки или из оболочки MSVC. Далее будут рассмотрены оба способа. Дополнительно следует установить пакет проектирования драйверов, в зависимости от операционной системы, Windows 98 DKK или Windows 2000 DDK. Эти пакеты включают набор заголовочных файлов и библиотек, необходимых для создания драйверов WDM. DDK содержат так же документацию и примеры. В настоящее время (1.2000) пакеты DDK как для Windows 98, так и для Windows 2000 можно бесплатно переписать с web узла компании Microsoft. Однако эти копии запрещено использовать в коммерческих целях. DDK следует устанавливать после установки MSVC. Программа инсталлятор DDK добавляет в меню «Пуск->Программы» подменю «Windows 98 DDK» (или «Windows 2000 DDK») . Это подменю содержит три пункта: «Checked Build Environment», «Free Build Environment» и «DDK Documentation». Последний пункт предназначен для вызова справки по DDK, а первые два используются для организации компиляции драйверов из командной строки в сессии MS-DOS. В сессии «Checked» драйверы компилируются с отладочной информацией. В сессии «Free» драйверы компилируются без отладочной информации. Через пункты «Checked» и «Free» запускается командный файл setenv.bat, который расположен в каталоге \bin. Этот файл устанавливает переменные среды окружения необходимые для компиляции драйверов. В частности пути к заголовочным файлам, и библиотекам DDK. Кроме этого настраиваются переменные среды окружения Visuаl С++. В параметрах командного файла указан корневой каталог DDK и способ компиляции драйвера, соответственно checked или free. Полная строка команды пункта «Checked Build Environment» Windows 98 DDK выглядит так: C:\WIN98\COMMAND.COM /e:4096 /k c:\98ddk\bin\setenv c:\98ddk checked Копия процессора COMMAND.COM с ключом /e:4096 вызывается для того, чтобы увеличить до 4К установленный по умолчанию размер блока переменных среды окружения. Драйвер WDM создаётся с помощью утилиты Build. Buld вызывается из командной строки. Эта утилита последовательно запускает компилятор для всех исходных файлов проекта, и затем компоновщик. Ошибки компиляции помещаются в текстовый файл Build.err. Предупреждения помещаются в текстовый файл Build.wrn. Если процесс создания драйвера завершается без ошибок, в сессии «Checked» по умолчанию исполняемые модули драйверов (с расширением sys) помещаются в каталог \lib\i386\checked, а в сессии «Free» в каталог \lib\i386\free. Проект драйвера, как правило, размещается в отдельном каталоге. Этот каталог может содержать подкаталоги, в которых размещаются исходные тексты модулей проекта, заголовочные файлы, ресурсы и промежуточные файлы компиляции. Если отдельные модули проекта размещены в подкаталогах, в корневом каталоге проекта создаётся тестовый файл без расширения с именем Dirs. В этом файле указываются все подкаталоги проекта. Пример файла Dirs: DIRS
=
mainsrc headers addons
\ \
Символ «\» используется для переноса строки. Утилита Build использует файл Dirs для поиска исходных компонентов проекта. Кроме этого в каталоге проекта, который содержит файлы исходных текстов располагается командный файлы для утилиты Build. Это текстовый файл (так же без расширения) с именем Sources. Sources содержит список файлов исходных текстов проекта и некоторую другую информацию. 130
Пример файла Sources: TARGETNAME=TESTDRV TARGETTYPE=DRIVER TARGETPATH=$(BASEDIR)\lib INCLUDES=$(BASEDIR)\inc C_DEFINES=-DDRIVER SOURCES= drvshell.c vector.c testdrv.c testdrv.rc debugwdm.c
\ \ \ \ \
Директива TARGETNAME задаёт имя драйвера. TARGETTYPE определяет тип создаваемого файла. Для драйверов указывается тип DRIVER. Директива INCLUDES задаёт пути к заголовочным файлам. Директива C_DEFINES задаёт дополнительные директивы командной строки компилятора Си. В разделе SOURCES указываются все исходные файлы проекта. Файл может содержать ряд дополнительных директив, полное описание которых приводится в файле Sources.tpl DDK. Использование оболочки Microsoft Visual C++ для создания драйверов. Для создания проекта MSVC драйвера WDM можно воспользоваться протокольным файлом, который утилита Build создаёт в процессе компиляции драйвера. Этот файл с именем Build.log содержит настройки компилятора и компоновщика, которые использовались при создании исполняемого модуля драйвера. Например, строка запуска компилятора Си в протокольном файле может быть такой: cl -nologo -Ii386\ -I. -Ic:\98ddk\inc -Ic:\98ddk\inc\win98 Ic:\98ddk\inc\win98 -Ic:\98ddk\inc\win98 -D_X86_=1 -Di386=1 DSTD_CALL -DCONDITION_HANDLING=1 -DNT_UP=1 -DNT_INST=0 -DWIN32=100 D_NT1X_=100 -DWINNT=1 -D_WIN32_WINNT=0x0400 DWIN32_LEAN_AND_MEAN=1 -DDBG=1 -DDEVL=1 -DFPO=0 -DNDEBUG -D_DLL=1 -DDRIVER /c /Zel /Zp8 /Gy -cbstring /W3 /Gz /QIfdiv- /QIf /Gi/Gm- /GX- /GR- /GF -Z7 /Od /Oi /OyFIc:\98ddk\inc\win98\warning.h .\wldh000.c wldh000.c Для того, чтобы создать проект MSVC, сначала следует запустить компиляцию драйвера из командной строки с помощью утилиты Build. После этого надо загрузить оболочку MSVC и создать новый проект «Win32 application» или «Win32 dynamic link library». Какой тип проекта выбрать не имеет значения, так как настройки проекта будут изменены. Далее следует вызвать диалоговое окно настроек проекта. Настройки компилятора в закладке C/C++ заменяются на настройки из строки запуска компилятора cl.ехе в Log файле. Если проект включает несколько С файлов в протокольном файле содержатся строки запуска компилятора для каждого файла. Все эти настройки одинаковы, поэтому следует взять их из первой строки. Настройки компилятора ресурсов в закладке Resources заменяются на настройки из строки запуска компилятора ресурсов rc.ехе в Log файле. Настройки компоновщика в закладке Link так же замещаются настройками компоновщика link.ехе из Log файла. Далее необходимо аккуратно удалить из настроек компиляторов и компоновщика все записи содержащие имена исходных файлов. Например, в приведённой выше строке компилятора сl.ехе следует удалить записи ‘.\wldh000.c’ и wldh000. После этого окно настроек следует закрыть и в закладке FileView с помощью всплывающего меню добавить к проекту все исходные файлы. Не забудьте добавить файл ресурсов. Voila! Теперь проект можно компилировать (F7). Настройки следует сохранить на будущее для использования в других проектах. Можно пойти дальше и разработать Wizard для генерации шаблона проекта драйвера. Регистрация драйверов WDM. Для того чтобы операционная система обнаружила драйвер в реестр необходимо внести учётную запись этого драйвера. Учётные записи драйверов kernel и WDM 131
похожи. Поэтому с некоторыми оговорками нижеизложенная процедура установки применима так же к драйверам Windows NT. Драйвер WDM следует поместить в каталог <WINDOWS>\SYSTEM32\DRIVERS. Затем в реестре в ключ HKEY_LOCAL_MACHINE\System\CurrentControlSet\Services необходимо добавить подключ с именем драйвера. Имя драйвера задаёт разработчик. Например: HKEY_LOCAL_MACHINE\System\CurrentControlSet\Services\MYDRV В подключе создаются несколько значений, которые содержат информацию о драйвере. Строковое значение ImagePath задаёт путь к файлу драйвера. Например: ImagePath = ‘\SystemRoot\system32\drivers\mydrv.sys’ Двойное слово Type задаёт тип драйвера. Для драйверов WDM это значение равно 1. Двойное слово Start определяет вариант запуска драйвера. Возможны следующие значения: 0
1
2 3
Драйвер запускается загрузчиком ОС на начальном этапе загрузки системы. Этот тип запуска применяется редко, в тех случаях когда есть необходимость в том, чтобы драйвер стартовал в самом начале загрузки операционной системы. Драйвер запускается во время процедуры инициализации ОС (старта операционной системы). Для драйверов, которые должны запускаться автоматически при загрузке операционной системы, как правило, используется этот вариант. Драйвер запускается вместе с ОС после загрузки графического интерфейса, но перед операцией log in. Драйвер не запускается во время загрузки ОС. Он может быть загружен впоследствии. Например, из командной строки командой net start . В Windows 98 драйвер WDM должны запускаться в процессе загрузки операционной системы.
Двойное слово ErrorControl определяет варианты действия операционной системы в случае ошибки инициализации драйвера. Допускаются следующие значения: 0 1 2 3
Ошибка игнорируется, сообщение об ошибке помещается в протокольный файл загрузки (log файл). Система выдаёт пользователю текстовое сообщение об ошибке. Если идёт загрузка последней успешной конфигурации загрузка продолжается, иначе загрузка прерывается и производится загрузка последней успешной конфигурации. Если идёт загрузка последней успешной конфигурации загрузка прекращается с ошибкой, иначе загрузка прерывается и производится загрузка последней успешной конфигурации.
Строковое значение Group задаёт имя группы, к которой относится драйвер. Например, Base, SCSI, Port. Имя группы можно использовать, в случае если необходимо загружать группы драйверов в заданной последовательности. Для определения последовательности загрузки драйверов используется несколько дополнительных значений реестра. Строковое значение DependOnGroup задаёт зависимость драйвера, от какой либо группы. Например, в случае если драйвер может быть загружен только после загрузки группы драйверов с именем ‘MYGROUP1’, подключ драйвера должен содержать значение DependOnGroup = ‘MYGROUP1’. Строковое значение DependOnService определяет зависимость драйвера от другого драйвера. Например: DependOnService = ‘MYDRV2’ Полное описание значений реестра можно найти в документации Windows NT DDK во второй главе Programmer’s Guide. 132
Все необходимые регистрационные записи удобно разместить в текстовом файле с расширением reg. Содержимое этого файла вносится в реестр, программой RegEdit. Пример такого файла приводится ниже: REGEDIT4 [HKEY_LOCAL_MACHINE\System\CurrentControlSet\Services\MYDRV] "ImagePath"="\\SystemRoot\\system32\\drivers\\mydrv.sys" "Type"=dword:00000001 "Start"=dword:00000001 "Group"="Base" Организация обмена данными. Пакеты запросов ввода-вывода. Прежде чем перейти непосредственно к вопросу проектирования драйверов WDM, рассмотрим несколько важных концепций общих для драйверов kernel и WDM. Операционная система применяет единый механизм передачи команд и данных между драйверами. Драйверы kernel и WDM в отличие от виртуальных устройств Windows 95 пользуются унифицированным интерфейсом для организации связи друг с другом и с 32-х разрядными приложениями. Процессом передачи информации управляет Менеджер ввода-вывода (I/O Мanager). Менеджер ввода-вывода формирует так называемые Пакеты запросов ввода-вывода (I/O Request Packets), сокращенно IRP. Пакет запроса содержит служебную информацию, которая используется в процедуре доставки пакета получателю и возврата результата обработки Пакета вызывающей программе. Кроме этого в IRP содержатся собственно данные для получателя. Драйвер содержит процедуры, предназначенные для обработки Пакетов запросов различного типа. Операционная система для передачи пакета драйверу вызывает один из обработчиков и передаёт ему в параметрах указатель на IRP. Адреса обработчиков IRP записываются в специальную служебную таблицу доступную операционной системе. Адреса обработчиков указываются программистом при проектировании драйвера. Пакет запроса может быть передан по цепочке драйверов. Т.е. один IRP может последовательно обрабатываться несколькими драйверами. Такой механизм используется для многоступенчатой развязки низкоуровневого и высокоуровневого программного обеспечения. Эта технология используется, например, для организации файлового ввода-вывода(Рисунок 1).
Менеджер ввода/вывода Менеджер кэш памяти Файловая система Сетевые драйверы Драйверы аппаратуры
Рисунок 1. Многоуровневая структура драйверов. Для того, чтобы прочитать блок данных из файла приложение вызывает функцию API ReadFile. Менеджер ввода-вывода формирует Пакет запроса и передаёт его расположенному первым в цепочке Менеджеру кэш памяти. Если указанный фрагмент файла находится в кэш памяти, он передаётся приложению. Иначе IRP передаётся на более низкий уровень драйверу файловой системы (или сетевому драйверу, если файл расположен на удалённой машине). Драйвер файловой системы по таблице размещения файлов может определить какие именно сектора диска содержат нужную информацию. Эти сведения он помещает в Пакет запроса и передаёт его драйверу физического носителя. Например, драйверу жёсткого диска. Драйвер диска выполняет операции низкоуровневого чтения секторов и помещает результат чтения в IRP. Далее заполненный данными IRP возвращается вызывающей программе. Такая структура 133
имеет важные преимущества. Замена одного из звеньев цепи не влияет на работу остальных. В случае смены носителя достаточно установить новый драйвер аппаратуры. При этом драйвер файловой системы остаётся прежним. При смене файловой системы (например, с VFAT на NTFS) нет необходимости заменять драйвер непосредственно работающий с носителем. Кроме этого, в цепочку может быть добавлен какой либо другой модуль. Например, драйвер шифровки данных. Пакет запроса ввода-вывода включает элементы необходимые для поддержки многоуровневой обработки IRP. Вместе с IRP Менеджер ввода-вывода создаёт массив структур типа IO_STACK_LOCATION. Эти структуры используются для передачи параметров драйверам, включённым в цепочку обработки IRP. Каждый драйвер в цепочке имеет свою собственную структуру IO_STACK_LOCATION и следовательно свой собственный набор параметров. Параметры содержаться в поле Parameters структуры IO_STACK_LOCATION. Это поле имеет тип объединения (UNION) нескольких структур. Кроме параметров IO_STACK_LOCATION содержит информацию о типе запроса. Формат параметров, т.е. какая из структур входящих в объединение Parameters должна использоваться, определяется типом запроса. Например, запрос чтения данных имеет тип IRP_MJ_READ. Параметры этого типа запроса описываются структурой Read: struct { ULONG Length; ULONG Key; LARGE_INTEGER ByteOffset; } Read; Длина буфера данных в запросе чтения может быть получена следующим образом: ReadBufLength = CurStackLocation->Parameters.Read.Length; ,где CurStackLocation это указатель на структуру IO_STACK_LOCATION драйвера. Перед тем, как передать IRP обработчику драйвера Менеджер ввода-вывода записывает в него ссылку на структуру IO_STACK_LOCATION этого драйвера. Для получения адреса IO_STACK_LOCATION следует использовать функцию IoGetCurrentIrpStackLocation. PIO_STACK_LOCATION CurStackLocation = IoGetCurrentIrpStackLocation(Irp); ,где Irp это указатель на структуру IRP, который передаётся обработчику при вызове через параметры. Точка входа драйвера WDM. Драйвер WDM, так же как динамическая библиотека имеет точку входа. Точка входа драйвера это функция, которая вызывается операционной системой при загрузке драйвера. В точке входа размещаются процедуры инициализация драйвера. Точка входа драйвера традиционно называется DriverEntry. Это имя прямо указывается в командной строке компоновщика: link
/entry:DriverEntry@8
Функция вызывается по соглашению __stdcall. Первый параметр помещается в стек первым. Функция имеет следующий интерфейс: NTSTATUS DriverEntry(IN PDRIVER_OBJECT DriverObject, IN PUNICODE_STRING RegistryPath) Параметр DriverObject указывает на объект драйвера. Объект драйвера это структура данных, которая содержит служебную информацию о драйвере. Объект драйвера создаётся и частично инициализируется операционной системой перед вызовом точки входа драйвера. Объект драйвера должен содержать в частности адреса обработчиков Пакетов запросов ввода-вывода, адресуемых драйверу. Второй параметр RegistryPath указывает на строку в формате UNICODE содержащую ключ реестра, в котором расположены учётные записи драйвера: \HKEY_LOCAL_MACHINE\System\CurrentControlSet\Services\ 134
Функция возвращает код ошибки инициализации. В случае если инициализация драйвера закончилась успешно, функция должна вернуть константу STATUS_SUCCESS. В функции DriverEntry необходимо проинициализировать таблицы адресов обработчиков IRP. Таблица адресов размещается в объекте драйвера. Ссылка на таблицу даётся в поле MajorFunction структуры объекта драйвера DRIVER_OBJECT. Указатель эту на структуру содержится в параметре DriverObject. В процессе работы драйверу передаются Пакеты запросов различных типов. Адреса обработчиков IRP каждого типа размещаются в таблице в заданном порядке. В заголовочном файле DDK wdm.h? определены константы IRP_nnn для различных типов IRP. Эти константы задают номера обработчиков в таблице объекта драйвера. Например, константа IRP_MJ_DEVICE_CONTROL задаёт номер обработчика пакетов IRP использующихся для организации передачи драйверу команд из 32-х разрядных приложений Windows. Для того, чтобы записать в таблицу адрес обработчика IRP такого типа следует поместить в тело функции DeviceEntry следующую строку: DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL]
=
IoControlHandler; ,где IoControlHandler это имя функции обработчика. Позднее будут рассмотрены наиболее часто использующиеся типы IRP и правила написания функций обработчиков. Кроме обработчиков IRP и точки входа DriverEntry, в драйвере могут быть реализованы некоторые другие функции вызываемые извне. Например, функция DriverUnload, которая вызывается при выгрузке драйвера. Если эта функция реализована, её адрес так же заносится в процессе инициализации в структуру DriverObject. DriverObject->DriverUnload = UnloadHandler; Итак, минимальная функциональная нагрузка, которая лежит на функции DriverEntry, заключается в подстановке адресов различных обработчиков. Следует отметить, что адреса обработчиков устанавливаются динамически в процессе загрузки драйвера, а не при его создании. Исключение составляет точка входа DeviceEntry. В простейшем случае точка входа драйвера может выглядеть, например, так: NTSTATUS DriverEntry(IN PDRIVER_OBJECT DriverObject, IN PUNICODE_STRING RegistryPath) { DriverObject->DriverExtension.AddDevice = AddDeviceHandler; DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL]= IoControlHandler; DriverObject->MajorFunction[IRP_MJ_CREATE] = CreateHandler; DriverObject->MajorFunction[IRP_MJ_CLOSE] = CloseHandler; DriverObject->DriverUnload = UnloadHandler; return STATUS_SUCCESS; } Устройства. Расширения и cимволические ссылки. Драйвер это исполняемый модуль, который содержит код и глобальные данные необходимые для управления каким либо аппаратным или программным ресурсом операционной системы. Объект драйвера DRIVER_OBJECT содержит различные параметры модуля драйвера. В этом объекте, как было показано выше, определяются адреса обработчиков системных вызовов. Однако объект драйвера не предназначен для описания самого ресурса. Ресурс описывается объектом другого типа. В данном случае под ресурсом понимается физическое или виртуальное устройство, для которого написан драйвер. Это может быть, например, сетевой адаптер или файловая система. Для управления устройством необходимо создать объект устройства. Объект устройства описывается структурой DEVICE_OBJECT. Понятие, связанное с этой 135
структурой данных называется устройством. Взаимодействие с устройством происходит через объект устройства, а не через объект драйвера. Драйвер может обслуживать несколько устройств. В этом случае для каждого устройства создаётся отдельный объект DEVICE_OBJECT. Устройства могут совместно использовать код и глобальные переменные драйвера. Каждое устройство может иметь символическую ссылку (Symbolic Link). Символическая ссылка это имя, по которому можно получить доступ к устройству. При создании объекта устройства, как правило, в памяти динамически размещается буфер для хранения данных устройства. Этот буфер называется расширение (Extension). Устройство использует расширение для хранения своих внутренних данных. Каждое устройство может хранить свои данные в собственном расширении. Такая технология позволяет избавиться от зависимостей между устройствами, созданными внутри одного драйвера. Ситуации, в которой один и тот же драйвер обслуживает несколько устройств встречаются достаточно часто. Например, в системе может быть установлено несколько одинаковых сетевых контроллеров. Все эти контроллеры обсуживаются одним драйвером. Только одна копия исполняемого модуля драйвера загружается в память. Однако для каждого контроллера создаётся отдельное устройство (объект DEVICE_OBJECT). Устройствам назначается уникальные символические ссылки. Например ‘NETCARD1’, ‘NETCARD2’ и т.д. С помощью этих ссылок внешнее программное обеспечение может указать, к какому именно контроллеру производится обращение. Кроме этого для каждого устройства создаётся расширение. В расширении устройства могут храниться текущие настройки одного из контроллеров и другая частная информация об устройстве. При этом все устройства используют один и тот же код обработчиков Пакетов запросов реализованных в драйвере. Возникает вопрос - как обработчик драйвера определяет контроллер, которому предназначен Пакет запроса? Операционная система по символической ссылке может определить адрес объекта устройства которому направляется Пакет запроса. Этот адрес вместе с указателем на IRP передаётся обработчику. Структура DEVICE_OBJECT содержит поле DeviceExtension. Это поле указывает на блок расширения устройства, т.е. на внутренние данные устройства. Таким образом для всех устройств, обработчики IPR могут быть абсолютно одинаковыми. Устройства отличаются друг от друга только данными. Это справедливо, конечно лишь в том случае, если драйвер обсуживает несколько одинаковых устройств. В драйверах kernel Windows NT объекты устройств, как правило, создаются непосредственно при запуске драйвера в процедуре DriverEntry. В этом случае разработчик должен включить в процедуру DriverEntry операции поиска физических устройств, которыми управляет драйвер и создания объекта для каждого обнаруженного устройства. В спецификации WDM предусмотрена возможность автоматизации процесса создания объектов устройств, выполненных по технологии Plug and Play. Менеджер конфигурации при обнаружении физического устройства вызывает обработчик драйвера устройства, адрес которого указан поле AddDevice расширения объекта драйвера. Объект драйвера, как и объект устройства, имеет расширение. Объект драйвера и его расширение создаются автоматически операционной системой при загрузке драйвера перед вызовом точки входа DriverEntry. Адрес обработчика AddDevice задаётся вместе с другими в процедуре DriverEntry. DriverObject->DriverExtension.AddDevice
= AddDeviceHandler;
Обработчик AddDevice имеет следующий интерфейс: NTSTATUS AddDeviceHandler(IN PDRIVER_OBJECT DriverObject, IN PDEVICE_OBJECT pdo); В первом параметре передаётся указатель на объект драйвера DRIVER_OBJECT. Второй параметр содержит указатель на так называемый Физический Объект Устройства (Physical Device Object). Системные драйверы, обслуживающие шины, к которым подключаются устройства, самостоятельно создают для каждого обнаруженного устройства Физический Объект. С помощью этого объекта драйвер шины может контролировать работу устройства. Этот дополнительный объект создаётся драйвером шины для собственных целей. Таким образом, для устройств, выполненных по технологии Plug and Play, процедура поиска выполняется операционной системой, а объекты обнаруженных устройств создаются в обработчике AddDevice драйвера. В драйверах WDM можно так же 136
использовать традиционный метод создания объектов устройств в процедуре DriverEntry. Процедура создания объекта устройства. Объект устройства создаётся с помощью функции IoCreateDevice. NTSTATUS ntStatus; UNICODE_STING deviceNameUnicodeString; PDEVICE_OBJECT DeviceObject; ntStatus = IoCreateDevice(DriverObject, sizeof(DEVICE_EXTENSION), &deviceNameUnicodeString, FILE_DEVICE_UNKNOWN, 0, FALSE, &DeviceObject); Первый параметр функции это указатель на объект драйвера устройства. Указатель PDRIVER_OBJECT передаётся в параметрах точки входа (или в параметрах обработчика AddDevice). Далее в параметрах указывается размер в байтах блока расширения. Переменная deviceNameUnicodeString содержит указатель на имя устройства. Имя устройства определяется разработчиком. Имя и символическая ссылка это разные понятия. Имя может использоваться для идентификации объекта устройства в системе. Символическая ссылка используется для организации доступа к устройству, например из 32-х разрядных приложений Windows. Имя задаётся строкой в формате UNICODE. Строка UNICODE имеет заголовок, в котором задаётся текущая длина строки, максимально возможная длина строки и указатель на буфер строки. Под каждый символ в буфере строки резервируется слово (16-бит). Для работы с такими строками используются несколько специальных функций. Кроме строк UNICODE используются так же строки в формате ANSI. Эти строки имеют такой же заголовок, однако, в буфере строки под каждый символ резервируется один байт. Для инициализации строки UNICODE с именем устройства используют следующую последовательность операций: ANSI_STRING devName; NTSTATUS ntStatus; UNICODE_STRING deviceNameUnicodeString; CHAR DeviceNameBuffer[NAME_MAX]; // инициализация буфера строки strcpy(DeviceNameBuffer, "\\Device\\MYDEVICE"); // инициализация ANSI строки с именем устройства RtlInitAnsiString(&devName, DeviceNameBuffer); // преобразование ANSI строки в строку UNICODE ntStatus = RtlAnsiStringToUnicodeString(&deviceNameUnicodeString, &devName, TRUE); Переменную deviceNameUnicodeString теперь можно подставлять в параметры функции IoCreateDevice. В этом примере устройству присваивается имя ‘MYDEVICE’. Четвёртый и пятый параметры функции IoCreateDevice задают соответственно тип и дополнительные характеристики устройства. Возможные значения этих параметров приводятся в документации DDK. Шестой параметр равен TRUE, в случае если одновременное обращение к устройству из нескольких потоков запрещено. Наконец, в последнем параметре передаётся адрес переменной, в которую функция возвращает указатель на созданный объект устройства. Процедура создания устройства может включать две дополнительных операции: создание символической ссылки и расширения. В приведённом примере функция IoCreateDevice создаёт блок расширения устройства, размер которого равен размеру структуры DEVICE_EXTENSION. Структура DEVICE_EXTENSION декларируется разработчиком в тексте драйвера. Структура перекрывает весь блок расширения и используется для удобства работы с его содержимым. Например, устройство, использующее линию аппаратного прерывания и канал ПДП может хранить настройки в расширении следующего вида: typedef struct _DEVICE_EXTENSION { DWORD IrqNumber; // номер линии прерывания DWORD DmaLine; // номер канала ПДП CHAR LinkName[NAME_MAX]; 137
PDEVICE_OBJECT } DEVICE_EXTENSION,
PhysDeviceObject; *PDEVICE_EXTENSION;
Дополнительно в расширении, как правило, для удобства сохраняются указатели на физический объект устройства и символическая ссылка. Символическая ссылка используется для организации внешнего интерфейса устройства. Приложения Win32 должны обращаться к устройству по его символической ссылке, а не по имени. Для того, чтобы получить доступ к устройству приложения Win32 используют функцию API CreateFile (см. так же главу 9). В первом параметре этой функции указывается символическая ссылка на устройство. Символическая ссылка доступная из приложений Win32 создаётся функцией IoCreateUnprotectedSymbolicLink. ntStatus = IoCreateUnprotectedSymbolicLink(&linkNameUnicodeString, &deviceNameUnicodeString); В первом параметре функции указывается имя символической ссылки, а во втором имя устройства. Символическая ссылка, так же как имя устройства, задаётся строкой в формате UNICODE. Ниже приводится пример создания объекта устройства с именем ‘MYDEVICE» и символической ссылки с таким же именем. #define DeviceName
«MYDEVICE»
NTSTATUS ASL_CreateDeviceObject(IN PDRIVER_OBJECT DriverObject, IN OUT PDEVICE_OBJECT *DeviceObject, IN PCHAR DeviceName) { ANSI_STRING devName; ANSI_STRING linkName; NTSTATUS ntStatus = STATUS_SUCCESS; UNICODE_STRING deviceNameUnicodeString; UNICODE_STRING linkNameUnicodeString; PDEVICE_EXTENSION deviceExtension; CHAR DeviceNameBuffer[NAME_MAX]; CHAR DeviceLinkBuffer[NAME_MAX]; // инициализация переменных с именами устройства и символической ссылки strcpy(DeviceNameBuffer, "\\Device\\"); strcpy(DeviceLinkBuffer, "\\DosDevices\\"); // окончательное формирование переменных с именами устройства и ссылки strcat(DeviceNameBuffer, DeviceName); strcat(DeviceLinkBuffer, DeviceName); // инициализация ANSI строки с именами устройства и ссылки RtlInitAnsiString(&devName, DeviceNameBuffer); RtlInitAnsiString(&linkName, DeviceLinkBuffer); // преобразование строк ANSI в строки UNICODE ntStatus = RtlAnsiStringToUnicodeString(&deviceNameUnicodeString, &devName, TRUE); ntStatus = RtlAnsiStringToUnicodeString(&linkNameUnicodeString, &linkName, TRUE); // создание объекта устройства ntStatus = IoCreateDevice(DriverObject, sizeof(DEVICE_EXTENSION), &deviceNameUnicodeString, FILE_DEVICE_UNKNOWN, 0, FALSE, DeviceObject); if (NT_SUCCESS(ntStatus)) { // создание символической ссылки ntStatus = IoCreateUnprotectedSymbolicLink(&linkNameUnicodeString, &deviceNameUnicodeString); 138
// получение указателя на Device Extension deviceExtension= (PDEVICE_EXTENSION)(*DeviceObject)->DeviceExtension; // обнуление Device Extension RtlZeroMemory(deviceExtension, sizeof(DEVICE_EXTENSION)); //запись в расширение символической ссылки strcpy(deviceExtension->LinkName, DeviceLinkBuffer); //и указателя на устройство deviceExtension->PhysDeviceObject = *DeviceObject; } // освобождение памяти от Unicode строк RtlFreeUnicodeString(&deviceNameUnicodeString); RtlFreeUnicodeString(&linkNameUnicodeString); return ntStatus; } Объект устройства динамически размещается в памяти. Если устройство удаляется из системы, объект устройства должен быть, так же удалён. Символическая ссылка на объект так же должна быть удалена. Расширение объекта удаляется автоматически при удалении объекта DEVICE_OBJECT. Ниже приводится пример удаления объекта устройства и символической ссылки. NTSTATUS RemoveDevice(IN PDEVICE_OBJECT pDeviceObject) { NTSTATUS ntStatus = STATUS_SUCCESS; UNICODE_STRING DeviceLinkUnicodeString; ANSI_STRING DeviceLinkAnsiString; PDEVICE_EXTENSION DeviceExtension = pDeviceObject->DeviceExtension; //преобразование строки с именем символической ссылки в строку //UNICODE RtlInitAnsiString(&DeviceLinkAnsiString, DeviceExtension->LinkName); ntStatus = RtlAnsiStringToUnicodeString( &DeviceLinkUnicodeString, &DeviceLinkAnsiString, TRUE); //удаление символической ссылки IoDeleteSymbolicLink(&DeviceLinkUnicodeString); //удаление устройства IoDeleteDevice(pDeviceObject); RtlFreeUnicodeString(&DeviceLinkUnicodeString); return ntStatus; } Типы объектов устройств. Спецификация WDM разделяет объекты устройств на несколько типов. (В Windows NT такого разделения нет). Для управления одним физическим устройством может быть создано несколько объектов. Все они описываются структурой DEVICE_OBJECT, однако имеют различное функциональное назначение. Физический объект устройства (Physical Device Object) создаётся драйвером шины, к которой подключается устройство. Например, системный драйвер, обслуживающий шину PCI после обнаружения устройства на шине создаёт свой собственный физический объект устройства, который он в дальнейшем использует для обслуживания устройства. Существует набор операций по управлению устройством, которые не входят в компетенцию драйвера устройства. Эти операции драйвер шины выполняет через физический объект устройства. В свою очередь драйвер устройства создаёт и использует для работы с устройством функциональный объект устройства (Functional Device Object). Процедура создания именно такого объекта была описана выше. Приложения и другие системные модули получают доступ к устройству через функциональный объект. Кроме этих двух объектов с устройством могут быть связаны один или несколько фильтров (Filter Object). Фильтры используются для расширения функциональных возможностей драйверов. Фильтры вставляются в цепочку обработки пакетов запросов, которые направляются устройству. Каждый объект в цепочке может выполнять свою часть процедуры обработки IRP. Например, в цепь объектов 139
обрабатывающих пакет запроса передачи данных может быть добавлен фильтр шифрующий эти данные. Объекты, относящиеся к одному физическому устройству, выстраиваются в определённом порядке в цепочке обработки пакетов запросов. (Рисунок 2). Эта цепь объектов устройств называется так же стеком. Устройство, расположенное на вершине стека, первым получает пакет запроса. Оно может выполнить свою часть обработки IRP передать его нижерасположенному в стеке устройству.
Объект Фильтр (создаётся драйвером этого фильтра)
Функциональный объект устройства (создаётся драйвером устройства)
Объект Фильтр (создаётся драйвером этого фильтра)
Физический объект устройства (создаётся драйвером шины)
Рисунок 2. Последовательность обработки IRP различными объектами устройства. Создание объектов фильтров необязательно. В простейшем случае, если устройство не относится ни к одному из классов устройств, для которых имеются драйверы шины, для управления устройством достаточно создать только функциональный объект. Так как система не в состоянии обнаружить такое устройство и, следовательно, вызвать обработчик AddDevice, функциональный объект устройства следует создавать в точке входа DriverEntry. Драйвер WDM такого устройства может практически не отличаться от kernel драйвера Windows NT. Интерфейсы устройств. Приложения и системные компоненты получают доступ к устройству по его символической ссылке. В Windows NT это единственный возможный способ. Он имеет ряд недостатков. В частности отсутствует гарантия того, что символическая ссылка будет уникальной. Кроме этого появляются проблемы при изменении версии драйвера. Новая версия, как правило, имеет дополнительные функциональные возможности. Набор функций и следовательно интерфейс устройства зависит не только от символической ссылки, но и от номера версии. Введение этого дополнительного параметра усложняет структуру драйвера и программ, которые с ним работают. В спецификации WDM эти проблемы решаются с помощью введения понятия интерфейса устройства. Интерфейс открывает доступ к определённому набору функций реализованных в драйвере. Драйвер WDM может одновременно поддерживать несколько различных интерфейсов. Каждый интерфейс идентифицируется в системе уникальным «в пространстве и времени» кодом GUID (Global User Identifier). Этот код фактически используется как символическая ссылка, с помощью которой приложения и другие системные компоненты могут получить доступ к конкретному набору функций. Коды GUID задаются при проектировании драйвера. Код может быть получен, например, с помощью программы UUIDGEN, которая входит в состав Windows SDK. Код формируется на основе системного времени и IP адреса, поэтому он уникален. Код GUID декларируется в заголовочном файле проекта драйвера. Этот файл может быть использован в дальнейшем приложениями, которые 140
работают с драйвером. Для объявления кода используется макроопределение DEFINE_GUID . Например: #define INITGUID DEFINE_GUID(MYDEVICE_GUID, 0x673b9cc0, 0x9a, 0x8c, 0x47, 0x70, 0x31, 0x18);
0x320d,
0x11d3, 0x93, 0xeb,
Далее в тексте программы для указания кода GUID используется имя MYDEVICE_GUID. Если перед DEFINE_GUID записать #define INITGUID под структуру GUID автоматически резервируется память. Перед использованием интерфейс устройства должен быть зарегистрирован. Как правило, интерфейс устройства регистрируется сразу после создания объекта устройства DEVICE_OBJECT в процедуре AddDevice. NTSTATUS AddDevice(IN PDRIVER_OBJECT DriverObject, IN PDEVICE_OBJECT pdo) { NTSTATUS status; PDEVICE_OBJECT DeviceObject; PDEVICE_EXTENSION DeviceExtension; status = IoCreateDevice(..., &DeviceObject); dext = DeviceObject->DeviceExtension; if (!NT_SUCCESS(status)) return status; status = IoRegisterDeviceInterface(pdo, &MYDEVICE_GUID, NULL, &dext->InterfaceName); if (!NT_SUCCESS(status)) { IoDeleteDevice(DeviceObject); return status;} IoSetDeviceInterfaceState(DeviceExtension->InterfaceName, TRUE); ... return status; } Интерфейс регистрируется с помощью функции IoRegisterDeviceInterface. Первый параметр функции это указатель на физический объект устройства (не функциональный!). Во втором параметре задаётся код GUID интерфейса. Третий параметр, указатель на строку UNICODE, используется для организации пространства имён внутри интерфейса. Эта технология используется некоторыми драйверами шин, однако она не документирована, и не рекомендуется её использовать. Последний параметр это адрес строки UNICODE, в которую функция возвращает символическую ссылку на интерфейс. Память под заголовок строки UNICODE размещает вызывающая программа. В приведённом примере заголовок строки располагается в расширении функционального объекта DeviceObject. ... PUNICODE_STRING InterfaceName; ... Буфер для хранения содержимого строки размещается автоматически при вызове функции IoRegisterDEviceInterface. Вызывающая программа должна освободить эту память после того, как необходимость в ссылке отпадает. Используя эту ссылку, драйвер может управлять состоянием интерфейса. Использование зарегистрированного интерфейса можно разрешить или запретить в процессе работы с помощью функции IoSetDeviceInterfaceState. В параметрах функции указывается адрес символической ссылки интерфейса и флаг разрешения(TRUE)/запрещения работы интерфейса. Драйвер может экспортировать несколько различных интерфейсов, в то же время несколько устройств созданных в одном или разных драйверах могут поддерживать один интерфейс. Например, если в системе установлено несколько одинаковых устройств, драйвер создаёт отдельные объекты и регистрирует интерфейс с одним и тем же кодом GUID для всех устройств. Следовательно, для доступа к устройству кода GUID интерфейса недостаточно. Необходимо выбрать одно из устройств которое поддерживает заданный кодом GUID интерфейс. Драйвер регистрирует интерфейс с помощью функции IoRegisterDeviceInterface. Эта функция, как было показано выше, 141
генерирует символическую ссылку на устройство - InterfaceName. Для каждого устройства поддерживающего данный интерфейс создаётся уникальная символическая ссылка. Таким образом, устройство идентифицируется в два этапа, сначала по коду GUID интерфейса, а затем по символической ссылке созданной внутри этого интерфейса. Пользовательское приложение получает доступ к устройству через его интерфейс и символическую ссылку InterfaceName. Для этого необходимо:
Получить список устройств, которые экспортируют заданный интерфейс.
HANDLE hDevList = SetupDiGetClassDevs(&MYDEVICE_GUID, NULL, NULL, DIGCF_PRESENT | DIGCF_INTERFACEDEVICE); В параметрах функции указывается код GUID интерфейса. Функция возвращает ссылку на список устройств (device information set), которые поддерживают интерфейс. По окончании работы со списком он должен быть удалён. SetupDiDestroyDeviceInfoList(hDevList);
Получить информацию о интерфейсе каждого устройства в списке.
SP_INTERFACE_DEVICE_DATA InterfaceData; for (i = 0; ; i++) { SP_INTERFACE_DEVICE InterfaceData; InterfaceData.cbSize = sizeof(InterfaceData); if (!SetupDiEnumDeviceInterfaces(hDevList, NULL, &MYDEVICE_GUID, i, &InterfaceData)) { if (GetLastError() == ERROR_NO_MORE_ITEMS) break; //выход (пройден весь список) } //доступ к i-ому устройству. (пп. 3 и 4) } Функция SetupDiEnumDeviceInterfaces вызывается в цикле и возвращает в структуре InterfaceData информацию об интерфейсе i-ого устройства в списке hDevList.
Получить расширенную информацию о интерфейсе для каждого (i-ого) устройства в списке.
SetupDiGetInterfaceDeviceDetail(hDevList, &InterfaceData, NULL, 0, &size, NULL); PSP_INTERFACE_DEVICE_DETAIL_DATA idd = (PSP_INTERFACE_DEVICE_DETAIL_DATA)malloc(size); idd->cbSize = sizeof(SP_INTERFACE_DEVICE_DETAIL_DATA); SetupDiGetInterfaceDeviceDetail(hDevList, &InterfaceData, idd, size, NULL, NULL); char linkname[MAX_PATH]; strncpy(linkname, idd->DevicePath, sizeof(linkname)); free((PVOID)idd); Для каждого из обнаруженных устройств вызывается функция SetupDiGetInterfaceDeviceDetail. Эта функция возвращает в структуре idd расширенную информацию о интерфейсе устройства. В первом параметре функции указывается ссылка на список устройств, во втором указатель на информацию об интерфейсе полученную с помощью функции SetupDiEnumDeviceInterface. Третий параметр указывает на буфер в который записывается структура с расширенной информацией о интерфейсе устройства. В четвёртом параметре указывается размер этой структуры. В пятом возвращается её действительный размер. Функцию приходится вызывать дважды, так как размер данных которые функция возвращает по указателю idd заранее 142
неизвестен. Первый вызов функции используется для определения размера возвращаемой структуры (&cbsize). Структура SP_INTERFACE_DEVICE_DETAIL_DATA содержит поле cbSize, которое следует проинициализировать перед вызовом функции. Поле cbSize используется для указания версии структуры SP_INTERFACE_DEVICE_DETAIL_DATA. Его значение определяется константой sizeof(SP_INTERFACE_DEVICE_DETAIL_DATA), а не реальным размером структуры который возвращается функцией в переменную size.
Открыть устройство с помощью функции CreateFile.
В поле DevicePath структуры SP_INTERFACE_DEVICE_DETAIL_DATA содержится символическая ссылка устройства (InterfaceName). Эта ссылка создаётся автоматически когда драйвер регистрирует интерфейс устройства вызовом функции IoRegisterDeviceInterface. Приложение использует символическую ссылку для открытия устройства. HANDLE hDevice = CreateFile(linkname, GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL); После того как устройство открыто ссылка (handle) hDevice указывается в операциях записи (WriteFile), чтения (ReadFile) и передачи команд (DeviceIoControl) адресованных этому устройству. По завершении работы с устройством ссылка hDevice должна быть закрыта: CloseHandle(hDevice). К устройству можно получить доступ через интерфейс, или через символическую ссылку, типа «\\DosDevices\\MYDEVICE», как это принято в драйверах kernel Windows NT. В последнем случае символическая ссылка создаётся, например, функцией CreatеUnprotectedSymbolicLink. Это два различных метода, и вопрос о том какой из них использовать в драйвере WDM решается разработчиком. Обработка пакетов запросов ввода-вывода (IRP). Пакеты запросов IRP передаются устройству через вызовы обработчиков, адреса которых записываются в структуру DEVICE_OBJECT при загрузке драйвера в процедуре DriverEntry. В драйвере может быть реализовано несколько обработчиков различных пакетов или групп пакетов запросов. Один обработчик может обслуживать несколько различных типов пакетов запросов. В этом случае обработчик выполняет функции диспетчеризации пактов запросов внутри драйвера. Интерфейс обработчика IRP устройства содержит два параметра: указатель на объект устройства и указатель на IRP. По указателю на объект устройства обработчик может получить доступ к блоку расширения устройства. В структуре IRP содержится служебная информация, в том числе ссылка на предназначенную драйверу структуру параметров IO_STACK_LOCATION и данные. Тип IRP определяется двумя значениями из структуры IO_STACK_LOCATION, которые называются старшим кодом функции MajorFunction и младшим кодом функции MinorFunction. Для некоторых типов пакетов IRP декодируется только старший код функции. Значение старшего кода функции совпадает с номером в таблице адресов обработчиков расположенной в объекте драйвера: DriverObject>MajorFunction. Если в этой таблице для каждого кода MajorFunction задан отдельный обработчик диспетчеризация выполняется автоматически. Допускается также указывать адрес одного и того же обработчика в нескольких записях этой таблицы. Например: DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = IoControlHandler; DriverObject->MajorFunction[IRP_MJ_INTERNAL_DEVICE_CONTROL] = IoControlHandler; В этом случае обработчик IoControlHandler должен обеспечивать диспетчеризацию IRP двух типов. Обработчик может получить старший код функции через указатель на структуру IO_STACK_LOCATION, который возвращает функция IoGetCurrentIrpStackLocation: PIO_STACK_LOCATION
irpStack;
irpStack = IoGetCurrentIrpStackLocation(Irp); 143
MjFunc = irpStack->MajorFunction; MnFunc = irpStack->MinorFunction; Переданная через параметры обработчика переменная Irp указывает на структуру пакета запроса. Обработчик производит диспетчеризацию вызова в зависимости от значений MjFunc и MnFunc. IRP содержит структуру IoStatus в которой возвращается результат обработки IRP. В поле Irp-> IoStatus.Status обработчик, перед возвратом управления должен поместить код ошибки. В случае если Irp обработано без ошибок в поле Status записывается константа STATUS_SUCCESS. Обработка асинхронных сообщений это особый случай. Если устройство не может завершить операцию синхронно, т.е. до возврата управления вызывающей программе в поле Status записывается константа STATUS_PENDING. При этом IRP сохраняется до тех пор пока асинхронная операция не будет закончена. Содержимое поля Irp-> IoStatus.Information зависит от типа Пакета запроса. В пакетах запросов которые используются для передачи (чтения/записи) данных в поле Information следует указывать число реально переданных байтов. После обработки пакета запроса следует вызвать функцию IoCompleteRequest которая информирует систему о завершении обработки IRP. Следует отметить, что если Irp передаётся по цепочке драйверов, эту функцию вызывает драйвер расположенный последним в цепочке. Драйверы, через которые проходит Irp, могут передать его следующим в очереди c помощью функции IoCallDriver. Каждый драйвер может установить свою собственную callback функцию IoCompletionRoutine. Эти функции вызываются, когда последний в цепочке драйвер заканчивает обработку Irp и вызывает функцию IoCompleteRequest. В случае если Пакет запроса выполняется асинхронно, вместо функции IoCompleteRequest следует вызвать функцию IoMarkIrpPending. При этом после того, как драйвер возвращает управление, IRP не уничтожается. Впоследствии когда операция будет выполнена, например, в результате обработки аппаратного прерывания, драйвер, для того чтобы указать что обработка IRP завершена, должен вызвать функцию IoCompleteRequest. Обработчик IRP может заканчиваться следующими строками: Irp-> IoStatus.Status = ntStatus; Irp-> IoStatus.Information = sizeof(DataBuffer); if (ntStatus == STATUS_PENDING) IoMarkIrpPending(Irp); else IoCompleteRequest(Irp, IO_NO_INCREMENT); Функция IoCompleteRequest, кроме указателя на IRP содержит так же параметр, задающий значение, на которое следует увеличить базовый приоритет потока инициировавшего передачу IRP. Базовый приоритет потока увеличивается временно (см. главу 5). Если этот параметр равен IO_NO_INCRENENT приоритет потока не изменяется. Суммируя вышесказанное, можно выделить два различных варианта обработки IRP синхронный и асинхронный. Кроме этого IRP может быть передано для обработки объекту расположенному следующим в стеке. Рассмотрим эти процедуры более подробно. Синхронная обработка IRP. Драйвер устройства полностью завершает обработку пакета запроса и возвращает результат вызывающей программе. В конце обработчика необходимо занести результат операции в блок статуса Irp и указать системе, что обработка Irp завершена. Irp->IoStatus.Status = ntStatus; Irp->IoStatus.Information = Information; IoCompleteRequest(Irp, IO_NO_INCREMENT); return ntStatus;
144
Асинхронная обработка IRP. Драйверы WDM используют специальную технологию конвееризации асинхронной обработки пакетов запросов. Если пакет запроса не может быть обработан синхронно, он передаётся на конвеер обработки пакетов запросов. Для этого используется функция IoStartPacket. Для использования этой технологии в драйвере должна быть реализована функция DriverStartIo. Адрес этой функции заносится в структуру объекта драйвера устройства, как правило в процедуре DriverEntry. DriverObject->DriverStartIo = StartIoHandler;
Если устройство занято обработкой предыдущих пакетов запросов функция IoStartPacket помещает IRP в стандартную очередь пакет запросов. Если устройство свободно сразу же вызывается обработчик DriverStartIo. Собственно обработка пакета запроса начинается в функции DriverStartIo. void
StartIoHandler(IN_PDEVICE_OBJECT DeviceObject, IN PIRP Irp)
Перед вызовом IoStartPacket обработчик IRP должен вызвать функцию IoMarkIrpPending. После вызова функции IoStartPacket обработчик завершается, возвращая код STATUS_PENDING, который информирует вызывающую программу о том, что исполнение запроса отложено. IoMarkIrpPending(Irp); IoStartPacket(DeviceObject, Irp, NULL, OnCancel); return STATUS_PENDING; В параметрах функции IoStartPacket передаются указатели на объект устройства DeviceObject, пакет запроса Irp и необязательный указатель на процедуру OnCancel которая будет вызвана в случае если обработка Irp будет отменена до её завершения. Отменить обработку Irp может программа, которая изначально инициировала запрос. Третий параметр функции задаёт место в очереди, в которое помешается пакет запроса. Если этот параметр равен NULL, пакет запроса помещается в конец очереди. По завершении обработки IRP драйвер должен вызвать функцию IoStartNextPacket. Эта функция извлекает следующий в очереди пакет запроса и вызывает функцию DriverStartIo для его обработки. Если в очереди нет запросов, функция просто возвращает управление. Как правило, в функции DriverStartIo производится инициализация аппаратуры и запуск асинхронной операции. Типичным, является сценарий, в котором аппаратное устройство запускается в функции DriverStartIo, выполняет заданную операцию и сообщает о её окончании выставляя аппаратное прерывание. Драйвер должен установить обработчик, который используется для перехвата сообщения об окончании операции. Асинхронная обработка IRP с привлечением механизма прерываний имеет важную особенность. Обработчик прерывания запускается на повышенном уровне приоритета IRQL. На этом уровне запрещен вызов некоторых важных системных функций, в частности функции IoStartNextPacket и функции IoCompleteRequest, с помощью которой драйвер должен сообщить Менеджеру ввода-вывода о завершении обработки IRP. Поэтому драйвер использует специальный механизм отложенных вызовов процедур (Deferred Procedure Call, сокращённо DPC). Процедура DPC автоматически получает управление после того, как уровень приоритета будет снижен. Это происходит уже после того, как обработчик прерывания завершится. Подробнее эта технология будет рассмотрена в дальнейшем. Сейчас для нас важным является то, что аппаратура информирует драйвер о завершении асинхронной операции, используя механизм прерываний. Обработчик прерывания формирует отложенный вызов процедуры. Наконец в процедуре DPC обработка IRP завершается и запускается обработка очередного пакета запроса (рисунок 3). IoStartNextPacket(DeviceObject, FALSE); Irp->IoStatus.Status = ntStatus; Irp->IoStatus.Information = Information; 145
IoCompleteRequest(Irp, IO_NO_INCREMENT);
1 Обработчик IRP 2 3 Менеджер ввода-вывода
MarkIrpPending() IoStartPacket return STATUS_PENDING DriverStartIo //запуск аппаратуры
4 Обработчик прерывания (ISR)
5 6 7
//запрос вызова отложенной //процедуры IoRequestDpc Отложенная процедура (DPC) IoStartNextPacket IoCompleteRequest
Рисунок 3. Последовательность выполнения асинхронной операции. Следует отметить, что использование стандартной очереди пакетов запросов и функции DriverStartIo для асинхронной обработки IRP необязательно. Можно создавать и использовать собственные очереди пакетов запросов. Необходимость в этом возникает в случае если устройство должно одновременно поддерживать несколько независимых очередей IRP. Для создания дополнительных очередей и управления ими используются функции KeInitializeDeviceQueue, KeInsertDeviceQueue, KeRemoveDeviceQueue и др. В целом использование механизма конвееризации пакетов запросов для обработки асинхронных сообщений не обязательно. Если драйвер проектируется в расчёте на то, что он получает очередной пакет запроса, только после того как выполнен предыдущий или все запросы обрабатываются синхронно применение очередей не требуется. Передача IRP объекту устройства расположенного следующим в стеке. Стек объектов создаётся на этапе загрузки и инициализации драйверов устройства. Наиболее типичным является случай, в котором стек устройств содержит физический объект созданный драйвером шины и функциональный объект созданный драйвером устройства. Функциональный объект расположен первым в стеке. В ряде случаев пакет запроса переданный функциональному объекту устройства должен быть передан для обработки физическому объекту. Физический объект устройства создаётся первым. Драйвер устройства получает адрес физического объекта и как правило сохраняет этот адрес в расширении функционального объекта. Эта процедура более подробно описывается в параграфе «Пакеты запросов PnP и стек объектов устройств». Для вызова обработчика IRP ниже расположенного в стеке объеста используется функция IoCallDriver. NTSTATUS status = IoCallDriver(dext->NextDevice, Irp); В параметрах функции указывается адрес объекта, которому передаётся IRP и указатель на структуру IRP.
146
При создании пакета запроса учитывается количество объектов расположенных в стеке. Для каждого объекта создаётся отдельный блок IO_STACK_LOCATION. (рисунок 4)
IRP
Функциональный объект устройства
Физический объект устройства
IO_STACK_LOCATION (1)
IO_STACK_LOCATION (2)
Рисунок 4 Объекты и элементы стека. В структуре Irp содержится указатель CurrentLocation на текущий элемент IO_STACK_LOCATION. Инициатор вызова Irp инициализирует этот указатель, так чтобы он указывал на первый элемент в массиве структур IO_STACK_LOCATION. При передаче IRP вниз по стеку устройств указатель автоматически смещается на следующие элементы массива. Указатель CurrentLocation не документирован. Вместо него для определения адреса текущего элемента, следует использовать функцию IoGetCurrentIrpStackLocation. Выше расположенный объект устройства должен инициализировать структуру IO_STACK_LOCATION следующего устройства, перед тем как передать ему управление. Как правило текущий элемент копируется в следующий. Для этого используется функция IoCopyCurrentIrpStackLocationToNext. Объекты устройств в стеке могут совместно использовать один и тот же элемент IO_STACK_LOCATION. Для этого вместо копирования элемента, необходимо вызвать функцию IoSkipCurrentIrpStackLocation. Эта функция сдвигает указатель в массиве элементов на одну позицию назад. При вызове обработчика ниже расположенного в стеке устройства 3указатель автоматически сдвигается на одну позицию вперёд и снова указывает на элемент массива выше расположенного объекта. Какой из этих двух способов следует использовать, зависит от решаемой задачи. Однако следует отметить, что применение функции IoCopyCurrentIrpStackLocationToNext имеет дополнительное преимущество. Пакет запроса передаётся вниз по стеку до тех пор, пока его обработку не закончит устройство, расположенное на дне стека. Это устройство вызывает функцию IoCompleteRequest. Устройство, которое передаёт пакет запроса следующему объекту, может продолжить обработку IRP после того, как её закончат устройства расположенные ниже в стеке. Для того чтобы объект устройства мог получить управление после того, как обработка Irp на более низком уровне завершена, в структуре IO_STACK_LOCATION предусмотрено поле CompletionRoutine. Перед вызовом функции IoCallDriver объект устройства может занести в это поле адрес процедуры, которая будет вызвана, после того как нижерасположенный драйвер закончит обработку IRP и вызовет функцию IoCompleteRequest. При вызове функции IoCompleteRequest Менеджер ввода-вывода последовательно сканирует элементы массива IO_STACK_LOCATION начиная с самого нижнего. Если поле CompletionRoutine очередного элемента не равно NULL Менеджер передаёт управление по указанному в поле адресу. Функция CompletionRoutine имеет следующий интерфейс: NTSTATUS OnRequestComplete(IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp, IN PVOID Context); Функции при вызове предаются указатели на объект устройства и Irp, а так же определяемая пользователем переменная Context. 147
Для того чтобы установить функцию CompletionRoutine следует использовать функцию IoSetCompletionRoutine. Каждый объект в стеке может установить собственную функцию CompletionRoutine, однако если два драйвера совместно используют один элемент массива IO_STACK_LOCATION (драйвер расположенный выше в стеке вызывает функцию IoSkipCurrentIrpStackLocation), только один из объектов может установить функцию CompletionRoutine. В случае простого копирования элементов массива, может возникнуть ситуация, когда поля СompletionRoutine двух элементов массива содержат один и тот же адрес функции установленной выше расположенным драйвером. В этом случае функция будет вызвана дважды. Использование функции IoCopyCurrentIrpStackLocationToNext позволяет избежать этой ошибки, так как она копирует все поля структуры IO_STACK_LOCATION за исключением поля CompletionRoutine. В приведённом ниже примере обработчик выполняет функции простого ретранслятора IRP следующему объекту в стеке без какой либо обработки. PDEVICE_EXTENSION dext = DeviceObject->DeviceExtension; IoSkipCurrentStackLocation(Irp); return IoCallDriver(dext->NextDevice, Irp); В следующем примере устанавливается функция CompletionRoutine. IoCopyCurrentIrpStackLocationToNext(Irp); IoSetCompletionRoutine(Irp,(PIO_COMPLETION_ROUTINE)OnRequestComplete, &Context, TRUE, TRUE, TRUE); return IoCallDriver(dext->NextDevice, Irp); Последний расположенный в стеке объект устройства может выполнить операцию синхронно, т.е. до возврата управления обработчику объекта расположенного над ним. При этом он возвращает код отличный от STATUS_PENDING. Например, в случае успешного завершения операции возвращается код STATUS_SUCCESS. Если операция должна быть выполнена асинхронно, обработчик объекта устройства только инициирует исполнение операции. Например, IRP помещается в стандартную очередь пакетов запросов для последующей передачи процедуре DriverStartIo. Обработчик вызывает функцию IoMarkIrpPending, запускает асинхронную операцию и немедленно возвращает управление с кодом STATUS_PENDING обработчику вышерасположенного объекта устройства. Функция IoMarkIrpPending устанавливает в текущем элементе IO_STACK_LOCATION флаг SL_PENDING_RETURNED. Этот флаг указывает, что обработка IRP производится асинхронно. Флаг в дальнейшем используется в процедурах завершающих обработку IRP. Код возврата STATUS_PENDING последовательно передаётся обратно, вверх по стеку до программы, которая инициировала запрос. Когда вызывающая программа получает управление, пакет запроса может быть ещё не выполнен аппаратурой. Вызывающая программа получив код возврата STATUS_PENDING, как правило, переходит в режим ожидания завершения асинхронной операции. Например, пользовательское приложение может применить следующую стратегию ожидания завершения операции: OVERLAPPED ovr = {0,0,0,0,0}; char buffer[1024]; ULONG cbReaden; HANDLE hDevice = CreateFile(DeviceSLinkName, ...); ... ovr.hEvent = CreateEvent(0, TRUE, 0, NULL); if (!ReadFile(hDevice, buffer, 1024, &cbReaden, &ovr) { if (GetLastError() == ERROR_IO_PENDING) //ждём завершения асинхронной операции WaitForSingleObject(ovr.hEvent, INFINITE); ... else { //ошибка ... return} } else { //операция выполнена синхронно}
148
Вызывающая программа передаёт в данном случае в параметрах функции ReadFile ссылку на объект синхронизации ovr.hEvent. Системная ссылка на этот объект помещается в поле UserEvent структуры IRP. Если операция выполняется асинхронно, ReadFile возвращает значение FALSE, а последующий вызов GetLastError возвращает код ERROR_IO_PENDING. Объект устройства, выполняющий асинхронную операцию, сообщает о завершении операции, устанавливая объект синхронизации ovr.hEvent (UserEvent) в состояние signaled. После этого функция WaitForSingleObject возвращает управление, и исполнение вызывающей программы продолжается.. Функция IoCompleteRequest используется для завершения как синхронных, так и асинхронных операций. Функцию вызывает устройство, расположенное на дне стека. Если операция выполняется асинхронно, IoCompleteRequest вызывается по завершении операции, например из процедуры DPC. Функция последовательно сканирует элементы IO_STACK_LOCATION IRP, начиная с самого нижнего, и вызывает установленные функции CompletionRoutine. Кроме этого функция устанавливает в состояние signaled объект UserEvent, если операция выполнялась асинхронно. Функция принимает решения о том асинхронно или синхронно выполнялась операция по значению поля PendingReturned структуры IRP. Если значение этого поля равно TRUE (операция исполнялась асинхронно) функция устанавливает в состояние signaled объект UserEvent. Если PendingReturned = FALSE (операция исполнялась синхронно) функция не изменяет состояние объекта UserEvent. Установка значения поля PendingReturned так же происходит в функции IoCompleteRequest. Стратегия, которая для этого используется, обусловлена многоуровневой структурой объектов устройств. Как уже было отмечено функция последовательно, начиная с самого нижнего, просматривает элементы массива IO_STACK_LOCATION. Если флаг SL_PENDING_RETURNED в текущем элементе установлен полю PendingReturned присваивается значение TRUE. Если флаг сброшен полю PendingReturned присваивается значение FALSE. Далее если в элементе не установлена функция CompletionRoutine в выше расположенном элементе стека флаг SL_PENDING_RETURNED устанавливается в том случае, если текущее значение PendingReturned = TRUE. Если текущее значение PendingReturned = FALSE значение флага SL_PENDING_RETURNED в вышерасположенном элементе стека не изменяется. Таким образом, если устройство расположенное на дне стека установило в своём элементе IO_STACK_LOCATION флаг асинхронной операции SL_PENDING_RETURNED (IoMarkIrpPending) и при этом ни один драйвер в цепочке не установил свою функцию CompletionRoutine, флаг SL_PENDING_RETURNED и вместе с ним значение TRUE поля PendingReturned последовательно всплывает по стеку до самого верхнего элемента. По окончании операции сканирования стека значение поля PendingReturned равно TRUE. На основании этого функция IoCompleteRequest принимает решение о том, что операция производилась асинхронно и устанавливает объект синхронизации UserEvent в состояние signaled. Однако, если в каком либо элементе стека установлена функция CompletionRoutine значение флага SL_PENDING_RETURNED автоматически не переносится в следующий элемент стека. Вместо этого просто вызывается функция CompletionRoutine. Программист должен самостоятельно выполнить операцию переноса флага в верхний элемент IO_STACK_LOCATION, иначе «всплытие» флага по стеку прервётся и результирующее значение PendingReturned может быть равно FALSE, несмотря на то, что объект устройства расположенный на дне стека выполнял операцию асинхронно и указал на это установив флаг SL_PENDING_RETURNED в своём элементе IO_STACK_LOCATION. Это происходит по той причине, что объекты устройств расположенные выше в стеке передают IPR вниз по стеку, не вызывая функции IoMarkIrpPending, т.е. не устанавливая в своих элементах IO_STACK_LOCATION флага SL_PENDING_RETURNED. Эти объекты при передаче IRP не могут определить, синхронно или асинхронно будет обрабатываться этот запрос в объектах расположенных ниже в стеке. В случае если флаг асинхронной операции будет потерян при сканировании стека функция IoСompleteRequest не установит объект синхронизации UserEvent в состояние signaled и вызывающая программа зависнет на вызове WaitForSingleObject(ovr.hEvent ...); Для того, чтобы избежать этой ситуации в функции CompleteionRoutine поместить следующий фрагмент кода: 149
NTSTATUS OnRequestComplete(PDEVICE_OBJECT DeviceObject, PIRP Irp, PVOID Context) { if(Irp->PendingReturned) IoMarkIrpPending(Irp); ... } Этот код устанавливает флаг SL_PENDING_RETURNED в текущем элементе IO_STACK_LOCATION, если значение поля PendingReturned, полученное в результате сканирования ниже расположенных элементов IO_STACK_LOCATION равно TRUE. Если функция CompletionRoutine возвращает код STATUS_MORE_PROCESSING_REQUIRED, вставка указанного фрагмента кода необязательна. Это особый случай. Следует ещё раз отметить, что установленные в обработчиках объектов устройств функции CompletionRoutine (типа OnRequestComplete) вызываются из функции IoCompleteRequest. Если какая либо из функций CompletionRoutine возвращает код STATUS_MORE_PROCESSING_REQUIRED функция IoCompleteRequest прекращает сканирование элементов стека и завершается. Объект устройства, который вызвал эту функцию должен выполнить дополнительную обработку IRP. Другими словами устройство расположенное в середине стека возвращая этот код сообщает устройству на дне стека, что обработка IRP не может быть завершена. Драйвер расположенный на дне стека в этом случае должен завершить обработку IRP и снова вызвать функцию IoCompleteRequest. Отмена асинхронно исполняющихся запросов. Асинхронная обработка запроса может быть отменена, до её завершения. Эту операцию может выполнить программа, которая инициировала запрос или Менеджер ввода-вывода, например в случае если поток (thread) вызывающей программы завершает исполнение в то время, как в очереди драйвера ещё остаются необработанные пакеты запросов переданные из этого потока. Пользовательское приложение, для отмены обработки пакетов запросов вызывает функцию CancelIo. Драйверы используют функцию IoCancelIrp. К сожалению, инициатор пакета запроса не может самостоятельно выполнить всю процедуру отмены обработки пакета запроса. Простое освобождение памяти выделенной под структуру IRP может привести к ошибке. IRP передаётся по стеку объектов устройств. Объект устройства, который выполняет асинхронную обработку пакета запроса, использует свои собственные указатели на структуру IRP. Например, IRP может находится в стандартной очереди объекта устройства, ожидая передачи на конвеер DriverStartIo. Инициатор передачи пакета запроса не обладает достаточной информацией для того, чтобы корректно удалить пакет запроса. Часть процедуры удаления IRP должен выполнить драйвер, который обрабатывает IRP. Для того чтобы драйвер устройства мог перехватить команду отмены обработки запроса и выполнить свою часть операции отмены, в структуре IRP предусмотрено поле CancelRoutine. Перед тем как начать асинхронную обработку пакета запроса драйвер записывает в это поле адрес процедуры, которая будет вызвана в случае отмены обработки IRP. void CancelHandler (PDEVICE_OBJECT DeviceObject, PIRP Irp); Структура Irp также содержит поле Cancel, которое используется, как флаг отмены обработки Irp. Функция IoCancelIrp устанавливает флаг Cancel и вызывает CancelRountine если её адрес не равен NULL. Если драйвер использует стандартный конвеер обработки IRP, адрес CancelRoutine, указывается в последнем параметре функции IoStartPacket. IoMarkIrpPending(Irp); IoStartPacket(fdo, Irp, NULL, CancelHandler); Если стандартный конвеер не используется, CancelRoutine можно установить функцией IoSetCancelRoutine. IoSetCancelRoutine(Irp, CancelHandler);
150
Операция отмены запроса должна быть синхронизирована с операциями обработки IRP, которые выполняет драйвер. Это обусловлено тем, что MS Windows многозадачная операционная система. Функция IoCancelIrp может, например, исполнятся на одном из процессоров, в то время как на другом процессоре выполняется операция извлечения IRP из стандартной очереди пакетов запросов для последующей передачи IRP обработчику DriverStartIo. Одновременное выполнение этих операций приводит к ошибке. Поэтому на время исполнения таких операций IRP следует защитить от доступа из других потоков. Для этой цели используются системные структуры spin lock. Spin lock работает подобно критической секции. Одновременно только один поток может захватить ресурс с которым ассоциирован spin lock. В данном случае этим ресурсом является IRP. Поток захватывает ресурс, выполняет необходимые операции и затем освобождает его. Если другой поток пытается захватить уже занятый ресурс, его исполнение блокируется до тех пор, пока текущий владелец ресурса не освободит его. В операциях связанных с процедурой отмены обработки IRP пакет запроса захватывается функцией IoAcquireCancelSpinLock и освобождается функцией IoReleaseCancelSpinLock. Если для обработки IRP используется стандартный конвеер, большинство операций синхронизации выполняется автоматически функциями IoStartPacket, IoStartNextPacket и IoCancelIrp. Однако часть работы возлагается на разработчика драйвера. Рассмотрим распространённый случай, когда CancelRoutine устанавливается только на время, пока IRP находится в стандартной очереди пакетов запросов. В ниже приведённом примере, когда Irp передаётся для исполнения обработчику DriverStartIo в поле CancelRoutine заносится значение NULL, поэтому запрос может быть отменён только пока он находится в очереди. void StartIoHandler(PDEVICE_OBJECT DeviceObject, PIRP Irp) { KIRQL oldirql; IoAcquireCancelSpinLock(&oldirql); if (Irp != DeviceObject->CurrentIrp || Irp->Cancel) { IoReleaseCancelSpinLock(oldirql); return; } else { IoSetCancelRoutine(Irp, NULL); IoReleaseCancelSpinLock(oldirql); } ... } Операции проверки состояния Irp и сброса CancelRoutine защищены структурой spin lock, поэтому они не могут быть прерваны вызовом IoCancelIrp. Функция IoCancelIrp перед тем как передать управление обработчику CancelRoutine захватывает Irp вызывая функцию IoAcquireCancelSpinLock. В функции CancelRoutine необходимо освободить ресурс вызвав функцию IoReleaseCancelSpinLock. void CancelHandler(PDEVICE_OBJECT DeviceObject, PIRP Irp) { if (DeviceObject->CurrentIrp == Irp) { IoReleaseCancelSpinLock(Irp->CancelIrql); IoStartNextPacket(DeviceObject, TRUE); } else { KeRemoveEntryDeviceQueue(&DeviceObject->DeviceQueue, &Irp->Tail.Overlay.DeviceQueueEntry); IoReleaseCancelSpinLock(Irp->CancelIrql); } Irp->IoStatus.Status = STATUS_CANCELLED; IoCompleteRequest (Irp, IO_NO_INCREMENT); }
151
Если пакет запроса находится в очереди, он из неё извлекается (KeRemoveEntryDeviceQueue). Функция CancelHandler завершает обработку Irp с кодом STATUS_CANCELLED. Основные типы IRP. IRP_MJ_CREATE. Этот пакет запроса посылается устройству, когда пользовательское приложение или другой драйвер запрашивает ссылку (handle) на устройство. Для того чтобы получить доступ к устройству драйверы должны использовать функцию ZwCreateFile, а пользовательские приложения функцию CreateFile. В параметре FileName этой функции указывается символическая ссылка устройства. Например: HANDLE hDevice; hDevice = CreateFile("\\\\.\\MYDEVICE", 0,0,0,CREATE_NEW, FILE_FLAG_DELETE_ON_CLOSE, 0); В процессе исполнения функции CreateFile система формирует пакет запроса IRP_MJ_CREATE и передаёт его обработчику драйвера устройства. Функция CreateFile (ZwCreateFile) создаёт специальный объект файла (file оbject) который в дальнейшем используется для организации обмена данными между приложением и устройством. Функция возвращает ссылку на объект файла в переменную hDevice. Эта ссылка в дальнейшем используется для организации обмена данными с устройством. Указатель на объект файла передаётся драйверу в поле FileObject структуры IO_STACK_LOCATION. Этот указатель присутствует во всех пакетах запросов связанных со ссылкой hDevice (В том числе и в пакете запроса IPR_MJ_CREATE). По этому указателю устройство может различать источники запросов. Несколько приложений могут одновременно получить несколько ссылок на устройство. Для каждой из ссылок создаётся отдельный объект файла. Приложение, обращаясь к устройству, указывает ссылку по которой происходит обращение. Например, для передачи данных драйверу может использоваться функция WriteFile(hDevice, ...) в первом параметре которой указывается ссылка на устройство. WriteFile формирует IRP и передаёт его обработчику драйвера устройства. При этом в поле FileObject структуры IO_STACK_LOCATION заносится указатель на объект файла ранее созданный для ссылки hDevice. IRP_MJ_CLOSE. Устройство получает IRP такого типа, в случае если пользовательское приложение или другой драйвер закрывает ссылку на устройство. Пользовательское приложение закрывает ссылку на устройство с помощью функции CloseHandle. CloseHandle(hDevice); Драйверы используют для этой цели функцию ZwClose. При уничтожении ссылки на устройство удаляется так же связанный с ней объект файла. IRP_MJ_CLEANUP. Этот пакет запроса устройство получает непосредственно перед пакетом запроса IRP_MJ_CLOSE. В обработчике этого IRP устройство должно отменить асинхронное исполнение всех пакетов запросов, которые относятся к объекту файла указанному в структуре IO_STACK_LOCATION. Т.е. перед тем как ссылка на устройство будет закрыта, следует удалить все необработанные пакеты запросов переданные по этой ссылке. Если в драйвере применяется технология буферизации пакетов запросов (например, используется стандартный конвеер обработки Irp) в обработчике IRP_MJ_CLEANUP, как правило, выполняется процедура просмотра очереди запросов. При этом из очереди извлекаются пакеты запросов, в которых указатель FileObject совпадает с указателем FileObject переданным в пакете запроса IRP_MJ_CLEANUP. Здесь же в обработчике эти пакеты запросов завершаются с кодом STATUS_CANCELLED. DequeuedIrp->IoStatus.Status = STATUS_CANCELLED; IoCompleteRequest(DequeuedIrp, IO_NO_INCREMENT); Процедура извлечения Irp из очереди должна быть защищена spin lock’ом.
152
IRP_MJ_READ и IRP_MJ_WRITE. Это запросы чтения и записи данных. Инициатором таких запросов может быть пользовательское приложение. Например, вызов функции ReadFile приводит к формированию и пересылке драйверу устройства запроса IRP_MJ_READ. ReadFile(hDevice, &DataBuffer, DataSize, &cbRet, NULL); IRP_MJ_DEVICE_CONTROL и IRP_MJ_INTERNAL_DEVICE_CONTROL. Эти IRP используется для передачи команд устройству соответственно от пользовательских приложений и других устройств . Можно сказать, что запросы чтения и записи данных являются частными случаями этих типов запросов. С помощью этих IRP можно передать блок данных драйверу и возвратить блок данных вызывающей программе. IRP DEVICE_CONTROL и INTERNAL_DEVICE_CONTROL имеют дополнительный параметр IoControlCode, определяющий код команды. На пользовательском уровне для передачи команд устройству используется функция DeviceIoControl. В параметрах функции задаётся код команды, адреса и размеры входного и выходного буфера. DeviceIoControl(hDevice, CTRL_ID1, &InputBuffer, sizeof(InputBuffer), &OutputBuffer, sizeof(OutputBuffer), &cbRet, NULL); Драйверы могут посылать пакеты запросов с помощью функции IoCallDriver. Коды команд и содержимое буферов данных соответствующее каждой из команд определяется разработчиком. Процедура обработки IRP должна включать диспетчер кодов команд. NTSTATUS IoControlHandler(IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp) { NTSTATUS ntStatus = STATUS_SUCCESS; PIO_STACK_LOCATION irpStack; ULONG ioControlCode; // инициализация полей в структуре Irp для возвращаемых значений Irp->IoStatus.Status = STATUS_SUCCESS; Irp->IoStatus.Information = 0; // указатель на структуру StackLocation в Irp, где находятся // коды функций и код команды irpStack = IoGetCurrentIrpStackLocation(Irp); ioControlCode = irpStack->Parameters.DeviceIoControl.IoControlCode; switch (ioControlCode) { case CTRL_ID1: ... break; case CTRL_ID2: ... break; default: ntStatus = STATUS_INVALID_PARAMETER; break; } Irp->IoStatus.Status = ntStatus; IoCompleteRequest(Irp, IO_NO_INCREMENT); return ntStatus; } Формат кода команды DEVICE_CONTROL. Универсальный ( во обе стороны) обмен данными с устройством организуется с помощью пакетов запросов IRP_MJ_DEVICE_CONTROL или IRP_MJ_INTERNAL_DEVICE_CONTROL. Пакеты этих типов могут передавать различные команды и сопутствующие им параметры. Код команды помещается в структуру IO_STACK_LOCATION указатель на которую можно получить по указателю на IRP. Код команды это 32-х битовое значение, которое задаётся разработчиком. Код кроме номера команды, который так же называется номером функции, должен содержать дополнительную служебную информацию. Формат кода команды приводится на рисунке 5. 153
31 16 DeviceType
15 14 Access
13
2 Function
1
0
Method
Рисунок 5. Формат кода команды. В поле DeviceType указывается тип устройства. Тип устройства задаётся при его создании функцией IoCreateDevice. Поле типа устройства содержит 16 разрядов. Значения от 0 до 32767 зарезервированы. Некоторые основные типы устройств описываются константами FILE_DEVICE_xxx, значения которых лежат в этом диапазоне. Например, FILE_DEVICE_CDROM, FILE_DEVICE_MOUSE и т.п. Разработчики могут использовать значения от 32768 до 65535 для введения дополнительных типов. Поле Access определяет допустимое направление передачи данных. Это поле может содержать один из флагов: FILE_READ_ACCESS - чтение. FILE_WRITE_ACCESS - запись. FILE_ANY_ACCESS - чтение или запись. В поле Function указывается уникальный номер команды (функции). По этому номеру обработчик IRP производит диспетчеризацию команды. Номера функций в диапазоне от 0 до 2047 зарезервированы для использования операционной системой. Разработчики могут использовать оставшиеся значения от 2048 до 4095. Поле Method определяет метод передачи данных. Подробнее различные методы передачи данных устройству будут рассмотрены в следующем параграфе. Для того чтобы задать код команды можно использовать макроопределение CTL_CODE. Это макроопределение содержится в заголовочном файле DDK wdm.h #define CTL_CODE( DeviceType, Function, Method, Access ) ( \ ((DeviceType) SystemBuffer. Обработчик IRP драйвера устройства должен записать возвращаемые приложению данные в системный буфер. Следует отметить, что в системном буфере входные и выходные данные пересекаются. Обработчик IRP должен считать из буфера входные данные, прежде чем записывать в него выходные. Размеры входных и выходных буферов заносятся в блок параметров структуры IO_STACK_LOCATION. Блок параметров имеет разный формат для разных типов IRP. Ниже приводятся варианты определения размеров выходного буфера в операциях различных типов: Операция чтения: BufferSize = irpStack->Parameters.Read.Length; Операция записи: BufferSize = irpStack->Parameters.Write.Length; Операция передачи команды: - выходной буфер: ОutputBufferSize = irpStack->Parameters.DeviceIoControl.OutputBufferLength; - входной буфер: InputBufferSize = irpStack->Parameters.DeviceIoControl.InputBufferLength; Прямой метод передачи данных (DIRECT). В операциях чтения и записи пользовательский буфер защёлкивается в памяти. Менеджер ввода вывода создаёт структуру, которая называется Список описателей памяти (Memory Descriptor List, сокращённо MDL). Адрес этой структуры помещается в поле MdlAddress IRP. Структура MDL содержит список номеров физических страниц памяти, в которых располагается буфер. Для того чтобы получить системный виртуальный адрес буфера обработчик IRP должен использовать функцию MmGetSystemAddressForMdl. Размер буфера определяется с помощью функции MmGetMdlByteCount. BufferAddr = MmGetSystemAddressForMdl(Irp->MdlAddress); BufferSize = MmGetMdlByteCount(Irp->MdlAddress); Поля SystemBuffer и UserBuffer в операциях прямого чтения и записи не используются. При передаче команды, в случае если код команды содержит флаги METHOD_IN_DIRECT или METHOD_OUT_DIRECT, Менеджер ввода-вывода создаёт системный буфер защёлкивает его в памяти, копирует содержимое входного пользовательского буфера в системный буфер и записывает адрес системного буфера в поле SystemBuffer. Выходной пользовательский буфер защёлкивается в памяти. Для этого буфера создаётся структура MDL, указатель на которую заносится в поле MdlAddress. Таким образом, адресация к входным данным производится через системный буфер, а выходные данные доступны напрямую. Простой метод передачи данных (NEITHER). В этом случае в структуре IRP передаются только указатели на пользовательские буферы. В операциях чтения и записи указатель содержится в поле UserBuffer. В операции передачи команды, адрес выходного буфера так же заносится в поле UserBuffer. Адрес входного буфера содержится в блоке параметров структуры IO_STACK_LOCATION. InBuffer = Parameters.DeviceIoControl.Type3InputBuffer;
156
Если пользовательские буферы располагаются в частном адресном пространстве процесса, их адреса имеют смысл, только в контексте этого процесса. Во время исполнения драйвером устройства процедуры обработки IRP, контекст памяти может измениться. Поэтому использование адресов пользовательских буферов допускается только в некоторых частных случаях. Уровни приоритетов. Отдельные фрагменты кода драйвера могут иметь различный приоритет исполнения. Система поддерживает несколько уровней приоритетов, которые называются уровнями запросов прерываний (Interrupt Request Levels, сокращённо IRQLs). Обработчики пакетов запросов и другие процедуры драйвера, асинхронно вызываемые системой, имеют жёстко определённые приоритеты. Самый низкий приоритет имеет код расположенный на уровне PASSIVE_LEVEL. На этом уровне обрабатывается большинство пакетов запросов, например, пакеты запросов чтения, записи и команд. Для всех стандартных обработчиков IRP в документации DDK указывается уровень приоритета, с которым они вызываются. Уровень приоритета обработчиков аппаратных прерываний определяется номером прерывания. Эти уровни приоритета поддерживаются аппаратно контроллером прерываний. Уровень приоритета ограничивает набор функций системного программного интерфейса, которые могут быть вызваны из различных фрагментов кода драйвера. Некоторые функции разрешено вызывать на уровне приоритета не выше или не ниже заданного. Некоторые функции допускается вызывать, только на определённом уровне приоритета или в заданном диапазоне уровней приоритетов. Эта информация приводится в документации DDK в описании каждой функции. Уровень приоритета кода можно изменить в процессе исполнения. Для повышения уровня приоритета используется функции KeRaiseIrql, а для возврата к исходному уровню функция KeLowerIrql. Эта пара функций используется для временного повышения IRQL, например, для выполнения процедур, критичных ко времени исполнения. KIRQL OldIrql; //уровень приоритета PASSIVE_LEVEL KeRaiseIrql(DISPATCH_LEVEL, &OldIrql); //уровень приоритета DISPATCH_LEVEL KeLowerIrql(DISPATCH_LEVEL, &OldIrql); //уровень приоритета PASSIVE_LEVEL Текущий уровень приоритета возвращает функция KeGetCurrentIrql. Поддержка механизма Plug and Play в драйверах WDM. В предыдущих параграфах уже обсуждались некоторые элементы архитектуры драйвера WDM предназначенные для упрощения обслуживания устройств разработанных по технологии Plug and Play. Рассмотрим этот важный вопрос более детально. Аппаратные устройства размещаются на нескольких системных шинах. Каждая шина имеет свой собственный драйвер шины который выполняет операции общие для всех устройств подключённых к шине. В том числе драйвер шины отвечает за обнаружение устройств на шине. Кроме этого каждое устройство так же имеет свой собственный драйвер который выполняет специфические для данного устройства функции. Схема подключения аппаратуры имеет древовидную структуру фрагмент которой приводится на рисунке 6.
157
Контроллер PCI
Контроллер USB
Камера
Джойстик
Контроллер SCSI
Контроллер ISA
Диск 1
Звуковая карта
Рисунок 6. Пример схемы подключения аппаратных устройств. Параллельно древовидной структуре аппаратных устройств строится структура объектов устройств. Драйверы шины создают физические объекты, для системного обслуживания каждого устройства. Драйверы устройств создают функциональные объекты, через которые производится управление устройствами (рисунок 7).
Функциональный объект устройства
Физический объект устройства
Драйвер устройства
Драйвер шины
Рисунок 7. Схема подключения объектов устройств Дополнительно к этим типам объектов могут быть созданы объекты фильтров, которые участвуют в процессе обработки запросов ввода-вывода направляемых устройству. Все объекты относящиеся к одному устройству выстраиваются в цепочку при обработке IRP (рисунок 2). Физический объект располагается на самом нижнем уровне. Он получает Irp в последнюю очередь. Объекты фильтров могут располагаются на разных уровнях в зависимости от их назначения. Далее речь пойдет о том, как элементы технологии Plug and Play поддерживаются в драйверах устройств. Операциями автоматической идентификации устройств управляет Менеджер PnP. Процедура создания функционального объекта устройства переносится в обработчик AddDevice. Этот обработчик вызывается Менеджером PnP при обнаружении аппаратного устройства. Менеджер PnP получает информацию о том, какой именно драйвер обслуживает устройство из реестра. Собственно процедура поиска аппаратного устройства подключённого к шине производится драйвером шины. Драйвер шины создаёт физический объект для обнаруженного устройства. Указатель на этот объект передаётся в параметрах функции AddDevice драйверу устройства. В обработчике AddDevice драйвер устройства создаёт функциональный объект и помещает его на вершину так называемого стека объектов устройств. До этой операции в стеке верхним элементом является физический объект устройства созданный драйвером шины к которой это устройство подключено. В общем случае шина к которой подключается устройство может в свою очередь подключаться к другой шине. Таким образом в стеке может располагаться несколько физических объектов. Однако в простейшем случае стек состоит всего из двух объектов - физического и функционального. Причём функциональный объект располагается на вершине стека. Пакет запроса ввода-вывода переданный функциональному объекту устройства может 158
быть после обработки в драйвере устройства передан физическому объекту драйвера шины. Каждый драйвер в стеке выполняет свою часть обработки пакета запроса. Устройство может завершить обработку запроса или передать запрос устройству расположенному ниже в стеке. Функциональный объект устройства выполненного по технологии Plug and Play должен обрабатывать пакеты запросов ввода вывода IRP_MJ_PNP (MajorFunction). Обработчик пакетов PNP устанавливается в точке входа драйвера DriverEntry. DriverObject->MajorFunction[IRP_MJ_PNP] = PnpHandler; Обработчик имеет стандартный интерфейс: NTSTATUS PnpHandler(IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp); через который передаётся указатель на функциональный объект устройства и указатель на структуру IRP. Пакеты запросов PNP формирует и передаёт драйверу устройства Менеджер PnP. Существует несколько типов запросов PNP. Тип запроса PNP определяется младшим кодом функции MinorFunction. Пакет запроса IRP_MN_START_DEVICE передаётся драйверу при начальной установке или изменении аппаратной конфигурации устройства. В пакете запроса передаются сведения о конфигурации. Например, номер прерывания или диапазон адресов памяти выделенный устройству. Как правило, используется следующий сценарий: Драйвер шины обнаруживает устройство, считывает его аппаратные установки и создаёт физический объект устройства. Вызывается функция AddDevice драйвера устройства, в которой создаётся функциональный объект устройства. По установкам аппаратуры определяются ресурсы которые требуются устройству для работы . К ним относятся диапазоны памяти и портов ввода-вывода устройства, а также информация о том, использует ли устройство линии прерываний и каналы ПДП. Эти сведения передаются специальным драйверам арбитраторам, которые выделяют устройству требуемые ресурсы. Настройки, которые возвращаются арбитраторами используются для инициализации аппаратуры. Менеджер конфигурации формирует пакет запроса IRP_MJ_PNP:IRP_MN_START_DEVICE. В пакет запроса заносится информация о аппаратных настройках. Этот пакет запроса передаётся драйверу устройства. Драйвер устройства использует информацию, переданную в пакете запроса для подключения к аппаратным ресурсам. Например, драйвер устанавливает обработчик прерывания или захватывает канал ПДП. Менеджер PnP передаёт драйверу пакет запроса IRP_MN_STOP_DEVICE, в случае если требуется перенастройка устройства. В обработчике этого Irp освобождаются ресурсы захваченные в обработчике IRP_MN_START_DEVICE. Например, снимается обработчик прерывания. Драйвер получает пакет запроса IRP_MN_REMOVE_DEVICE когда устройство физически удаляется из системы. В обработчике освобождаются все занятые ресурсы и удаляется функциональный объект устройства. Перед передачей пакетов запросов IRP_MN_STOP_DEVICE и IRP_MN_REMOVE_DEVICE Менеджер PnP посылает драйверу запросы о возможности остановки/удаления устройства, соответственно IRP_MN_QUERY_STOP_DEVICE и IRP_MN_QUERY_REMOVE_DEVICE. Если обработчики этих запросов возвращают Status = STATUS_SUCCESS процедура остановки/удаления продолжается, иначе Менеджер PnP прекращает операцию и посылает драйверу сообщения IRP_CANCEL_STOP_DEVICE или IPR_CANCEL_REMOVE_DEVICE. Схема механизма PnP и основные типы запросов PnP приведены на рисунке 8.
159
Устройство удалено
Устройство физически установлено
IRP_MN_REMOVE _DEVICE
IRP_MN_REMOVE _DEVICE
Устройство готово к удалению
Устройство обнаружено драйвером шины
IRP_MN_QUERY_ REMOVE_DEVICE
Драйвер устройства загружен Менеджером конфигурации
IRP_MN_CANCE L_REMOVE_DE VICE
IRP_MN_START_D EVICE
IRP_MN_STOP_DE VICE
Устройство готово к остановке
IRP_MN_QUERY_ STOP_DEVICE
IRP_MN_CANCE L_STOP_DEVICE
Устройство работает
Менеджер PnP вызывает DriverEntry драйвера устройства
Драйвер устройства выполнил операции инициализации
Устройство остановлено
Менеджер PnP вызывает AddDevice
Менеджер PnP выделяет устройству системные ресурсы и посылает IRP_MN_START_DEVICE
Создан функциональный объект устройства
Рисунок 6. Схема механизма PnP. Пакеты запросов PnP и стек объектов устройств. Менеджер PnP посылает пакеты запросов PnP функциональному объекту устройства, который реализован в драйвере устройства. Обработчик драйвера устройства должен передать эти пакеты запросов физическому объекту устройства расположенному ниже в стеке объектов. Указатель на ниже расположенный объект устройства, как правило, сохраняется в расширении функционального объекта устройства в процедуре AddDevice. Указатель на физический объект передаётся в параметрах этой функции. Функциональный объект помешается на вершину стека объектов устройств. Для этого используется функция IoAttachDeviceToDeviceStack. Функция помещает объект на вершину стека и возвращает указатель на объект который был на вершине стека до вызова. Как правило им является физический объект устройства (pdo == IoAttachDeviceToDeviceStack(DeviceObject, pdo)); NTSTATUS AddDevice(IN PDRIVER_OBJECT DriverObject, IN PDEVICE_OBJECT pdo) { NTSTATUS status; PDEVICE_OBJECT DeviceObject; PDEVICE_EXTENSION DeviceExtension; status = IoCreateDevice(..., &DeviceObject); dext = DeviceObject->DeviceExtension; //сохраняем в расширении указатель на объект устройства расположенный //ниже в стеке dext->NextDevice = IoAttachDeviceToDeviceStack(DeviceObject, pdo); ... return status; } 160
Менеджер PnP передаёт пакеты запросов PnP объекту устройства, который расположен на вершине стека объектов устройств. Однако первым пакет запроса должен обработать объект, расположенный на дне стека. Объект устройства, если он не последний в стеке, перед обработкой пакета должен передать его нижерасположенному объекту, дождаться результатов обработки IRP нижерасположенными объектами, и только после этого приступить к собственной обработке IRP. Таким образом, в начале обработчика IRP функционального объекта, который расположен выше физического и следовательно первым получает пакет запроса, необходимо разместить следующий фрагмент кода: PEDVICE_EXTENSION dext = DeviceObject->DeviceExtension; IoCopyCurrentIrpStackLocationToNext(Irp); IoSetCompletionRoutine(Irp, (PIO_COMPLETION_ROUTINE)OnRequestComplete, (PVOID)hEvent, TRUE, TRUE, TRUE); NTSTATUS status = IoCallDriver(dext->NextDevice, Irp); KEVENT hEvent; KeInitializeEvent(&hEvent, NotoficationEvent, FALSE); if (status == STATUS_PENDING) { KeWaitForSingleObject(&hEvent, Executive, KernelMode, FALSE, NULL); status = Irp->IoStatus.Status; } if (status == STATUS_SUCCESS) { IO_STACK_LOCATION ioStack = GetCurrentIrpStackLocation(Irp); //обработка Irp в нижерасположенных объектах успешно завершена. //продолжить обработку. } Функция IoCallDriver вызывает обработчик пакета запроса объекта расположенного ниже в стеке. Этот обработчик может сразу вернуть результат или начать асинхронную операцию. В последнем случае функция возвращает STATUS_PENDING и вызывающая программа (обработчик функционального объекта) переходит в режим ожидания окончания операции (KeWaitForSingleObject). Перед вызовом функции IoCallDriver создаётся объект синхронизации (KeInitializeEvent) и устанавливается процедура завершения операции OnRequestComplete. Эта процедура будет вызвана автоматически, когда нижерасположенный объект устройства завершит асинхронную операцию и вызовет функцию IoCompleteRequest. OnRequestComplete просто устанавливает объект синхронизации hEvent в состояние Signalled. NTSTATUS OnRequestComplete(IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp, IN PKEVENT hEvent) { KeSetEvent(hEvent, 0, FALSE); return STATUS_MORE_PROCESSING_REQUIRED; } Как только hEvent будет установлен в состояние signalled, вызывающая программа продолжит работу. Она может получить результат обработки Irp нижерасположенными объектами из блока статуса Irp->IoStatus.Status и начать собственную обработку Irp. Управление аппаратными ресурсами. Обработка IRP_MN_START_DЕVICE В пакете запроса IRP_MN_START_DEVICE настройки аппаратуры передаются драйверу устройства в виде списка типа CM_RESOURCE_LIST. Указатель на список содержится в блоке параметров структуры IO_STACK_LOCATION List = StackLocation->Parameters.StartDevice.AllocatedResourcesTranslated. Этот список содержит транслированные параметры настроек. Настройки аппаратуры могут зависеть от особенностей подключения аппаратных устройств к системе. Например, диапазоны адресов устройства на шине к которой подключается устройство, могут отличаться от диапазона адресов выделенных устройству на шине адреса процессора. Программный доступ к устройству осуществляется через системную шину адреса, поэтому специфические шинные адреса должны быть перетранслированы в системные. В 161
пакете запроса передаются оба списка. Указатель на список исходных настроек аппаратуры содержится в поле блока параметров StackLocation->Parameters.StartDevice.AllocatedResources. Структура CM_RESOURCE_LIST описывает заголовок массива переменной длинны который содержит полные (full) описатели ресурсов выделенных устройству. В заголовке указывается количество полных описателей - Count. typedef struct _CM_RESOURCE_LIST { ULONG Count; CM_FULL_RESOURCE_DESCRIPTOR List[1]; } CM_RESOURCE_LIST, *PCM_RESOURCE_LIST; В массиве располагаются CM_FULL_RESOURCE_DESCRIPTOR;
Count
полных
описателей
типа
typedef struct _CM_FULL_RESOURCE_DESCRIPTOR { INTERFACE_TYPE InterfaceType; ULONG BusNumber; CM_PARTIAL_RESOURCE_LIST PartialResourceList; } CM_FULL_RESOURCE_DESCRIPTOR, *PCM_FULL_RESOURCE_DESCRIPTOR; В полных описателях содержится массив переменной длинны в котором располагаются частичные (partial) описатели ресурсов. Заголовок этого массива описывается следующей структурой: typedef struct _CM_PARTIAL_RESOURCE_LIST { USHORT Version; USHORT Revision; ULONG Count; CM_PARTIAL_RESOURCE_DESCRIPTOR PartialDescriptors[1]; } CM_PARTIAL_RESOURCE_LIST, *PCM_PARTIAL_RESOURCE_LIST; В поле Count структуры указано количество частичных описателей ресурсов. Частичный описатель ресурса описывается структурой CM_PARTIAL_RESOURCE_DESCRIPTOR. Эта структура имеет стандартный заголовок, в поле Type которого указывается тип ресурса. Интерпретация поля Flags в заголовке зависит от типа ресурса. Остальная часть структуры описывается объединением (union) u. Объединение включает несколько структур, применение которых так же зависит от типа ресурса. typedef struct _CM_PARTIAL_RESOURCE_DESCRIPTOR { UCHAR Type; UCHAR ShareDisposition; USHORT Flags; union { ... } u; } CM_PARTIAL_RESOURCE_DESCRIPTOR, *PCM_PARTIAL_RESOURCE_DESCRIPTOR; Ниже рассматриваются типы ресурсов и описывающие их структуры, входящие в объединение u:
Диапазон адресов памяти (Type = CmResourceTypeMemory):
struct { //8-байтовый базовый физический адрес диапазона. PHYSICAL_ADDRESS Start; ULONG Length; //размер диапазона } Memory; Программное обеспечение использует для доступа к памяти виртуальные адреса, поэтому заданный в структуре диапазон физических адресов должен быть отображён в виртуальное адресное пространство. Для этой цели используется функция MmMapIoSpace. 162
В параметрах функции указывается базовый физический адрес диапазона памяти и размер диапазона. Функция возвращает виртуальный базовый адрес диапазона памяти. PHYSICAL_ADDRESS PhysAddr = ...u.Memory.Start; ULONG Length = ...u.Memory.Length; PVOID VirtualAddr = MmMapIoSpace(PhysAddr, Length, MmNonCached); При выгрузке драйвера выделенное виртуальное адресное пространство следует освободить: MmUnmapIoSpace(VirtualAddr, Length);
Диапазон портов ввода-вывода(Type = CmResourceTypePort):
struct { PHYSICAL_ADDRESS Start; // 8-ми байтовый базовый адрес группы портов ULONG Length; //количество портов } Port; Если поле Flags заголовка описателя ресурса этого типа содержит значение CM_RESOURCE_PORT_MEMORY указанный диапазон портов отображается в память. В этом случае для обращение к портам следует создать диапазон виртуальных адресов, так же как это делается для памяти.
Линия аппаратного прерывания (Type = СmResourceTypeInterrupt):
struct { ULONG Level; ULONG Vector; ULONG Affinity; } Interrupt; Параметр Level задаёт уровень приоритета, на котором будет исполнятся код обработчика прерываний. Подробнее эта тема обсуждается в параграфе «Уровни приоритетов». В поле Vector передаётся номер вектора прерывания. Значение этого параметра составляется из номера IRQ и кода шины, к которой подключено устройство. Поле Affinity содержит маску, определяющую набор процессоров, которые имеют право обрабатывать прерывание. Для каждого процессора в маске выделяется один бит, начиная с младшего. Если бит установлен, обработка прерываний разрешена. Для однопроцессорной системы значение этого поля равно 1. Дополнительно для этого типа ресурса в поле Flags указывается тип прерывания CM_RESOURCE_INTERRUPT_LEVEL_SENSITIVE - по уровню, или CM_RESOURCE_INTERRUPT_LATCHED по фронту. Драйвер устанавливает обработчик прерывания с помощью функции IoConnectInterrupt. PKINTERRUPT InterruptObject; PDEVICE_EXTENSION de = DeviceObject->DeviceExtension; ULONG vector = ...u.Interrupt.Vector; ULONG level = ...u.Interrupt.Level; KINTERRUPT_MODE mode = (...Flags == CM_RESOURCE_INTERRUPT_LATCHED) ? Latched : LevelSensitive; KAFFINITY affinity = ...u.Interrupt.Affinity; NTSTATUS Status = IoConnectInterrupt(&InterruptObject, (PKSERVICE_ROUTINE)IrqHandler, (PVOID)de, NULL, vector, level, level, mode, FALSE, affinity, FALSE); Функция инициализирует объект прерывания и возвращает указатель на него в переменную, адрес которой передаётся в первом параметре. Объект прерывания в дальнейшем используется для управления линией прерывания. Во втором параметре 163
функции передаётся указатель на процедуру обработчик прерывания. Эта процедура имеет следующий интерфейс: BOOLEAN IrqHandler( IN PKINTERRUPT Interrupt, IN PVOID ServiceContext ); Через параметры обработчику передаётся указатель на объект прерывания (Interrupt) и значение (ServiceContext) которое указывается разработчиком в третьем параметре функции IoConnectInterrupt. Таким способом можно, например, передать обработчику указатель на расширение устройства (PVOID)de. Для снятия обработчика прерывания используется функция IoDisconnectInterrupt. IoDisconnectInterrupt(InterruptObject); Обработчик аппаратного прерывания вызывается на повышенном уровне приоритета IRQL. Некоторые функции, например IoCompleteRequest не могут быть вызваны на повышенном IRQL, т.е. непосредственно из обработчика IrqHandler. Необходимость вызова этой функции возникает, например, в случае если прерывание применяется как индикатор завершения асинхронной операции. Для решения проблемы следует использовать механизм отложенных вызовов процедур Deferred Procedure Call (DPC). Из обработчика прерывания можно заказать отложенный вызов заданной процедуры DPC. Процедура будет вызвана, после того как текущий уровень приоритета в системе снизится до значения DISPATCH_LEVEL. Это произойдёт после завершения обработчика прерывания. Таким образом, можно сказать, что обработчик перывания состоит из как бы из двух фрагментов кода. Первый выполняется на повышенном уровне IRQL - это собственно процедура обработчика прерывания. Следом за ним на уровне DISPATCH_LEVEL выполняется второй - отложенная процедура DPC. Запросы вызовов отложенных процедур помещаются в очередь в обработчике прерывания. Когда текущий IRQL снизится до уровня DISPATCH_LEVEL запросы последовательно извлекаются из очереди и выполняются. Программист может использовать стандартную очередь запросов вызовов DPC или создавать свои собственные очереди. Для инициализации стандартной очереди используется функция IoInitializeDpcRequest. IoInitializeDpcRequest(DeviceObject, DpcRoutine); В первом параметре функции передаётся указатель на объект устройства, а во втором адрес отложенной процедуры DPC. Функция IoInitializeDpcRequest вызывается как правило в процессе инициализации устройства вместе с установкой обработчика прерывания. Например в обработчике IRP_MN_START_DEVICE. Ниже приводится интерфейс процедуры DPC. void DpcRoutine(IN PKDPC Dpc,IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp, IN PVOID Context); В параметрах процедуры передаётся указатель на объект DPC, указатель на объект устройства, указатель на Irp и данные определяемые разработчиком (Context). Обработчик прерывания запрашивает вызов отложенной процедуры с помощью функции IoRequestDpc. IoRequestDpc (DeviceObject, Irp, Context); В параметрах функции указывается адрес объекта устройства, адрес Irp и определяемые пользователем данные которые будут переданы процедуре DPC через параметр Context. Ниже приводится пример процедуры установки обработчика прерывания и процедуры DPC, а так же их фрагменты. //Установка ... PDEVICE_EXTENSION de = DeviceObject->DeviceExtension; 164
NTSTATUS Status = IoConnectInterrupt(&InterruptObject, (PKSERVICE_ROUTINE)IrqHandler, (PVOID)de, ...); ... IoInitializeDpcRequest(DeviceObject, DpcRoutine); ... //обработчик прерывания BOOLEAN IrqHandler ( IN PKINTERRUPT InterruptObject, IN PVOID ServiceContext) { PDEVICE_EXTENSION de = (PDEVICE_EXTENSION) ServiceContext; PDEVICE_OBJECT DeviceObject = de->DeviceObject; PIRP Irp = DeviceObject->CurrentIrp; ... IoRequestDpc (DeviceObject, Irp, NULL); return TRUE; } //Отложенная процедура DPC void DpcRoutine(IN PKDPC Dpc,IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp, IN PVOID Context) { ... IoStartNextPacket(DeviceObject, FALSE); IoCompleteRequest(Irp, IO_NO_INCREMENT); }
Канал или порт ПДП (Type = CmResourceTypeDma)
struct { ULONG Channel; ULONG Port; ULONG Reserved1; } Dma; В структуре для системного контроллера ПДП указывается номер канала (Channel). Для контроллера подключённого к шине MCA указывается номер порта (Port). Для инициализации канала ПДП используется функция IoGetDmaAdapter. DEVICE_DESCRIPTON decsDma; ULONG MaxMapReg; PDMA_ADAPTER DmaObject1 = IoGetDmaAdapter(DeviceObject, &descDma, &MaxMapReg); Функция возвращает указатель на структуру данных, которая в дальнейшем используется для управления каналом ПДП. В параметрах функции передаётся указатель на физический объект устройства DeviceObject, указатель на структуру описатель устройства decsDma и указатель на переменную (MaxMapReg) в которую возвращается максимальное количество так называемых регистров отображения (map registers), которые драйвер может использовать в операциях ПДП. Регистры отображения используются для отображения системного буфера ПДП в виртуальное адресное пространство. Структура descDma инициализируется перед вызовом функции IoGetDmaAdapter. Эта структура содержит разнообразную информацию о канале ПДП: разрядность, тип шины, к которой подключён контроллер, номер канала и т.п. Ниже приводится пример иницализации структуры DEVICE_DESCRIPTON для 5-го канала системного контроллера ПДП. #define DMA_CHANNEL 5 165
#define DMA_BUF_SIZE 1024 DEVICE_DESCRIPTION descDma; RtlZeroMemory(&descDma1, sizeof(&descDma1)); descDma.Version = 0; descDma.Master = FALSE; descDma.ScatterGather = FALSE; descDma.AutoInitialize = FALSE; descDma.Dma32BitAddresses = FALSE; descDma.DmaChannel = DMA_CHANNEL; descDma.InterfaceType = Isa; descDma.DmaWidth = Width16Bits; descDma.DmaSpeed = Compatible; descDma.MaximumLength = DMA_BUF_SIZE; descDma.DmaPort = 0; Структура DMA_ADAPTER содержит поле DmaOperations. Это поле (типа DMA_OPERATIONS) указывает на таблицу адресов функций, которые используются для управления операциями ПДП. В эту таблицу, например, входят адреса функций использующихся для размещения буфера ПДП (AllocateCommonBuffer), захвата канала ПДП (AllocateAdapterChannel), чтения счетчика (ReadDmaCounter) и т.п. Для большинства функций в заголовочном файле wdm.h имеются макроопределения. Например: #define HalReadDmaCounter(DmaAdapter) \ ((*(DmaAdapter)->DmaOperations->ReadDmaCounter)(DmaAdapter))
166