Вызов отложенной обработки не должен выполнять блокирующие действия, например блокирующий ввод-вывод.
Вызов отложенной обработки напоминает процедуру обработки прерывания, поскольку также должен выполняться быстро и эффективно. Для минимизации нагрузки на систему при планировании вызовов отложенной обработки Windows NT перед передачей управления DPC сохраняет минимальную информацию о состоянии. После завершения DPC восстановление состояния также занимает мало времени, так как при передаче управления сохранялся минимум информации. В результате DPC может выполняться в контексте произвольного процесса. Например, если программа Excel выполняется в виде процесса и запускает процедуру ввода-вывода, вызов отложенной обработки (если он потребуется) может запускаться в контексте процессов Word или PowerPoint (а не обязательно в контексте процесса Excel).
Каждый процесс имеет собственную очередь вызовов отложенной обработки. Таким образом, многопроцессорный компьютер с четырьмя центральными процессорами будет иметь четыре отдельных очереди DPC. Вызов отложенной обработки может иметь высокий, средний и низкий приоритет; по умолчанию присваивается средний приоритет. Драйвер может изменить значение приоритета. Вызов отложенной обработки
с высоким приоритетом размещается в начало очереди, a DPC с низким и средним приоритетом – в конец очереди.
Обычно вызов отложенной обработки выполняется на том же процессоре, что и процедура обслуживания прерывания, что можно изменить с помощью драйвера.
Если драйвер уже поместил DPC в очередь, следующий запрос на размещение DPC в очереди просто игнорируется. При выполнении DPC выясняется, существует ли несколько рабочих элементов, например, при многократной обработке прерываний, когда каждое прерывание требует наличия отдельного рабочего элемента.
Вызов отложенной обработки может быть размещен в очереди другого процессора, если очередь DPC текущего процессора превышает определенное значение. Ядро Windows NT периодически пытается выполнить вызов отложенной обработки, генерируя программные прерывания.
Вызов отложенной обработки не может быть выгружен на диск.
1.5.3 Асинхронный вызов процедуры
Асинхронный вызов процедуры (asynchronous procedure call – АРС) немного похож на вызов отложенной обработки, но существуют и заметные различия. Как и вызов отложенной обработки, АРС выполняется на уровне привилегий, превышающем уровень привилегий обычного кода. В отличие от вызова отложенной обработки, выполняемого в контексте произвольного процесса, асинхронный вызов процедуры всегда выполняется в контексте определенного процесса. Таким образом, асинхронный вызов процедуры требует больших затрат, чем вызов отложенной обработки, так как приходится сохранять и восстанавливать большее количество параметров. Читателю, знакомому с операционными системами UNIX, асинхронные вызовы процедур напомнят процедуры обработки сигналов UNIX.
Существует два типа АРС: вызов в режиме ядра и вызов в пользовательском режиме. Асинхронный вызов процедуры в режиме ядра связан с драйвером или другим кодом режима ядра и обычно используется для передачи данных, например для копирования данных из буфера ядра в пользовательский буфер. Помните, что пользовательский буфер должен быть доступен в контексте процесса, который владеет буфером.
Код пользовательского режима тоже может использовать асинхронный вызов процедур. Для этого необходим прикладной интерфейс программирования QueueUserAPC, который рассматривается в документации к набору Platform SDK. Асинхронные вызовы процедур в пользовательском режиме предоставляются только тогда, когда поток получает предупреждение, например при блокировании в результате вызова функций WaitForSingleObject или WaitForMultipleObject. Подробная информация об этих функциях доступна в документации Platform SDK. Достаточно сказать, что эти функции позволяют организовать синхронизацию потоков.
Асинхронный вызов процедуры может быть блокирующим, например для выполнения специального ввода-вывода. Вызовы помещаются в очередь, соответствующую потоку, т.е. существует несколько очередей асинхронных вызовов процедур.
1.6 Драйверы и буферы ввода-вывода
В этом разделе рассматриваются буферы ввода-вывода, которые уже упоминались ранее в главе. Драйверы используют буферы для осуществления ввода-вывода и управления им (IOCTL). Для этого драйверы посредством соответствующего объекта указывают предпочтительный способ ввода-вывода. Существует три поддерживаемых драйверами Windows NT типа ввода- вывода: буферизированный, прямой и небуферизированный. Эти методы рассматриваются в данном разделе.
1.6.1 Буферизированный ввод-вывод
Буферизированный ввод-вывод обычно используется для передачи меньших объемов данных, так как в процессе ввода-вывода участвуют операции копирования данных. После того как приложение отправило запрос на ввод- вывод, диспетчер ввода-вывода проверяет соответствие прав доступа приложения к предоставленному буферу ввода-вывода (соответствие прав доступа предполагает, что приложение имеет права на чтение и запись, а также имеет доступ к буферу необходимого размера, указанного в запросе на ввод- вывод). Диспетчер ввода-вывода выделяет буфер в невыгружаемой памяти и в случае запроса на запись копирует данные из буфера приложения в выделенный буфер. Этот буфер передается драйверу.
Драйвер не обязан заботиться о контексте потока, так как буфер находится в невыгружаемой памяти и действителен в контексте любого процесса или потока. Драйвер выполняет необходимую операцию ввода-вывода. Для операции чтения драйвер копирует данные в полученный буфер. Кроме того, драйвер может «предположить», что в контексте виртуального адресного пространства буфер будет непрерывным, но, как и в случае других буферов Windows NT, непрерывность буфера в виртуальном адресном пространстве не гарантирует непрерывности в физическом адресном пространстве.
На этом этапе драйвер завершает обработку IRP, и ответственность за копирование данных из невыгружаемого буфера в буфер приложения возлагается на диспетчер ввода-вывода. Эта операция копирования должна выполняться в контексте процесса, который отправил запрос на ввод-вывод. Кроме того, диспетчер ввода-вывода должен освобождать буфер, выделенный в невыгружаемой области памяти.
1.6.2 Прямой ввод-вывод
Прямой ввод-вывод (Direct I/O) несколько более запутан, чем буферизированный, однако намного эффективнее при выполнении операций ввода- вывода для больших массивов данных. Диспетчер ввода-вывода выполняет базовые проверки, например проверяет разрешения приложения на доступ к буферу посредством области ввода-вывода желательного объема. Буфер в памяти, независимый от процесса, описывается для драйвера средствами структуры данных, которая называется список дескрипторов памяти (memory descriptor list – MDL). Адрес буфера используется в качестве общесистемного виртуального адресного пространства.
Операционная система Windows NT предоставляет процедуры драйверов, которые позволяют получать доступ к различным полям списка дескрипторов памяти. Создателям драйверов рекомендуется использовать список дескрипторов памяти в качестве целостного элемента. Дополнительная информация по использованию процедур работы со списками дескрипторов памяти приводится в документации к инструментарию создания драйверов (DDK) Windows NT. Процедуры, описанные в DDK, предоставляют следующие возможности:
• блокирование и разблокирование буфера приложения;
• связывание заблокированного буфера с виртуальным адресом, который доступен из контекста любого потока;
• сбор информации, необходимой для выполнения операции ввода-вывода с прямым доступом к памяти в буфер или из него, который в действительности представляет собой последовательность потенциально несоседних физических страниц памяти.
Прямой ввод-вывод чаще всего используется в драйверах ввода-вывода, например драйверах управления дисками и приводами на магнитной ленте.
1.6.3 Небуферизированный ввод-вывод
Этот тип ввода-вывода позволяет избавиться от генерирования дополнительных данных, что свойственно для буферизированного ввода-вывода (операций копирования данных и выделение/освобождение буфера) и прямого ввода-вывода (создания и уничтожения списка дескрипторов памяти), но за это приходится расплачиваться ограниченностью применения данного типа ввода-вывода. При небуферизированном вводе-выводе драйверу непосредственно передается адрес буфера запросившего приложения. Внимательный читатель быстро догадается, что, поскольку виртуальный адрес имеет смысл только в контексте определенного процесса или потока, драйвер должен вызываться в контексте запросившего приложения. Более того, драйвер должен выполнить операцию в этом же контексте (т.е. драйвер не может поместить запрос в очереди для последующего выполнения в произвольном контексте).