Отложенный вызов процедуры (Deferred Procedure Call, DPC)

Вдобавок к использованию для работы Диспетчера (планировщика) NT, IRQL dispatch_level также используется для обработки Отложенных Вызовов Процедур (DPC). Вызовы DPC - обратные вызовы подпрограмм, которые будут выполнены на IRQL dispatchjevel. Вызовы DPC обычно запрашиваются с более высоких уровней IRQL, для осуществления расширенной, не критической по времени обработки.
Давайте рассмотрим пару примеров того, когда используются DPC. Драйверы устройств Windows NT выполняют очень небольшую обработку внутри своих подпрограмм обслуживания прерывания. Вместо этого, когда устройство прерывается (на уровне DIRQL) и его драйвер определяет, что требуется сложная обработка, драйвер запрашивает DPC. Запрос DPC приводит к обратному вызову определенной функции драйвера на уровне IRQL dispatch_level для выполнения оставшейся части требуемой обработки. Выполняя эту обработку на IRQL dispatch_level, драйвер проводит меньшее количество времени на уровне DIRQL, и, следовательно, уменьшает время задержки прерывания для всех других устройств в системе.
На рис. 15 изображена типовая последовательность событий.

Рис. 15

Вначале ISR запрашивает DPC и NT помещает объект DPC в очередь целевого процессора. В зависимости от приоритета DPC и длины очереди DPC, NT генерирует программное прерывание DPC сразу же или спустя некоторое время. Когда процессор очищает очередь DPC, объект DPC покидает очередь и управление передается в его функцию DPC, завершающую обработку прерывания путем чтения данных из устройства или записи данных в устройство, сгенерировавшего прерывание.
Другое распространенное использование DPC - подпрограммы таймера. Драйвер может запросить выполнение конкретной функции для уведомления об истечении определенного периода времени (это делается путем использования функции KeSetTimer()). Программа обработки прерывания часов следит за прохождением времени, и, по истечении определенного периода времени, запрашивает DPC для подпрограммы, определенной драйвером. Использование DPC для таймерного уведомления позволяет программе обработки прерывания часов возвращаться быстро, но все же приводить к вызову указанной процедуры без чрезмерной задержки.

DPC-объекты

Вызов DPC описывается Объектом DPC. Определение Объекта DPC (KDPC) произведено в ntddk.h и показано на рис. 16.

Рис. 16. Объект DPC

Объект DPC может быть выделен драйвером из любого невыгружаемого пространства (типа невыгружаемого пула). Объекты DPC инициализируются, используя функцию KelnitializeDpc(), прототип которой:

VOID KelnitializeDpc (IN PKDPC Dpc,
IN PKDEFERRED^ROUTINE DeferredRoutine,
IN PVOID DeferredContext);

Где:
Dpc - Указатель на DPC объект, который надо инициализировать; DeferredRoutine - указатель на функцию, по которому должен быть сделан отложенный вызов на уровне IRQL DISPATCH_LEVEL. Прототип функции DeferredRoutine следующий:

VOID (*PKDEFERRED_ROUTINE)(
IN PKDPC Dpc,
IN PVOID DeferredContext,
IN PVOID SystemArgumentI,
IN PVOID SystemArgument2 );

Где:
DeferredContext - значение для передачи к DeferredRoutine в качестве параметра, вместе с указателем на объект DPC и двумя дополнительными параметрами.
Запрос на выполнение конкретной подпрограммы DPC делается путем помещения объекта DPC, описывающего эту подпрограмму DPC, в Очередь DPC заданного CPU, и последующим (обычно) запросом программного прерывания уровня IRQL
dispatch_level. Имеется по одной Очереди DPC на процессор. CPU, к которому объект DPC поставлен в очередь, является обычно текущим процессором, на котором выдан запрос (на прерывание). Как выбирается процессор для конкретного DPC, обсуждается позже, в разделе "Характеристики Объекта DPC". Объект DPC ставится в очередь с помощью функцию KelnsertQueueDpc(), прототип которой:

VOID KelnsertQueueDpc (IN PKDPC Dpc,
IN PVOID SystemArgument1,
IN PVOID SystemArgument2);

Где:
Dpc - Указывает на объект DPC, который нужно поставить в очередь;
SystemArgumentl, SystemArgument2 - произвольные значения, которые нужно передать функции DeferredRoutme как 3 и 4 параметры соответственно, наряду с указателем на объект DPC и параметром DeferredContext, определенным при инициализации Объекта DPC.

Активизация и обслуживание DPC

Происхождение программного прерывания уровня Dispatch_level распознается тогда, когда это прерывание становится наивысшим по уровню IRQL событием, ожидающем обработки на этом процессоре. Таким образом, после вызова функции KelnsertQueueDpc(), обычно в следующий раз, когда процессор готов возвратиться на уровень IRQL ниже dispatch_level, вместо этого он вернется на IRQL dispatch_level и попытается обработать содержимое Очереди DPC.
Как отмечено ранее в этой главе, IRQL DISPATCHJLEVEL используется как для диспетчеризации ,так и для обработки Очереди DPC. В NT 4.0, когда обработано прерывание уровня DISPATCH_LEVEL, сначала обслуживается вся очередь DPC, и затем вызывается Диспетчер для планирования выполнения следующего потока. Это разумно, потому что обработка, сделанная подпрограммой DPC, могла изменить состояние базы данных планирования потоков, например, делая работоспособным ожидающий до того поток.
Очередь DPC обслуживается Микроядром. Каждый раз, когда обслуживается Очередь DPC, обрабатываются все элементы Очереди DPC для текущего процессора. По одному за раз, Микроядро удаляет Объект DPC из начала очереди и вызывает DeferredRoutine, указанную в объекте. Микроядро передает в качестве параметров для функции DeferredRoutine указатель на Объект DPC, содержимое полей DeferredContext, SystemArgumentl и SystemArgument2 Объекта DPC.
Поскольку Очередь DPC обслуживается на IRQL dispatch_level, подпрограммы DPC вызываются на IRQL dispatch_level. Поскольку Очередь DPC обслуживается всякий раз, когда IRQL dispatch_level является самым высокоприоритетным IRQL для обслуживания (например, сразу после того, как отработала программа обработки прерывания и перед возвращением к прерванному потоку пользователя), функции DPC работают в контексте произвольного потока (arbitrary thread context). Под контекстом произвольного потока мы подразумеваем, что DPC выполняется в процессе и потоке, которые могут вообще не иметь никакого отношения к запросу, который обрабатывает DPC. (Контекст выполнения описан более подробно в разделе "Многоуровневая Модель Драйверов".)
Подпрограмма DPC завершает обработку и возвращается. По возвращении из подпрограммы DPC, Микроядро пытается выбрать другой Объект DPC из Очереди DPC и обрабатывать его. Когда очередь DPC пуста, обработка DPC заканчивается. Микроядро переходит к вызову Диспетчера (планировщика).

Многочисленные обращения к DPC

Каждый DPC описан конкретным Объектом DPC. В результате всякий раз, когда вызывается функция KelnsertQueueDpc() и выясняется, что переданный ей Объект DPC уже находится в той же самой Очереди DPC, функция KelnsertQueueDpcQ просто возвращается (не выполняя никаких действий). Таким образом, всякий раз, когда Объект DPC уже находится в Очереди DPC, любые последующие попытки постановки в очередь того же самого Объекта DPC, осуществляемые до удаления Объекта DPC из очереди, игнорируются. Это имеет смысл, так как Объект DPC может физически быть включен только в одну Очередь DPC одновременно.
Может возникнуть очевидный вопрос: Что произойдет, когда сделан запрос постановки Объекта DPC в очередь, но система уже выполняет подпрограмму DPC, указанную этим Объектом DPC (на этом же или другом процессоре)? Ответ на этот вопрос может быть найден при внимательном чтении предыдущего раздела. Когда Микроядро обслуживает Очередь DPC, оно удаляет Объект DPC из головы очереди, и только потом вызывает подпрограмму DPC, указанную Объектом DPC. Таким образом, когда подпрограмма DPC вызвана, Объект DPC уже удален из Очереди DPC процессора. Поэтому, когда сделан запрос на постановку Объекта DPG в очередь и система находится внутри подпрограммы DPC, заданной в этом Объекте DPC, DPC ставится в очередь как обычно.

DPC на многопроцессорных системах

Вопреки тому, что утверждалось в некоторых других источниках, и, как должно быть очевидно из предшествующего обсуждения, одна и та же подпрограмма DPC может выполняться на нескольких процессорах одновременно. Нет абсолютно никакого блокирования со стороны Микроядра, чтобы предотвратить это.
Рассмотрим случай драйвера устройства, который в одно и то же время имеет несколько запросов, ожидающих обработки. Устройство драйвера прерывается на Процессоре 0, выполняется программа обработки прерывания драйвера и запрашивает DPC для завершения обработки прерывания. Это стандартный путь, которому следуют драйверы в Windows NT. Когда завершается программа обработки прерывания, и система готова возвратиться к прерванному потоку пользователя, уровень IRQL процессора О понижается от DIRQL, на котором выполнялась ISR, до IRQL dispatch_level. В результате, Микроядро обслуживает Очередь DPC, удаляя Объект DPC драйвера и вызывая указанную в нем подпрограмму DPC. На Процессоре 0 теперь выполняется подпрограмма DPC драйвера.
Сразу после вызова подпрограммы DPC драйвера, устройство генерирует прерывание еще раз. Однако на этот раз, по причинам, известным только аппаратуре, прерывание обслуживается на Процессоре 1. Снова, программа обработки прерывания драйвера запрашивает DPC. И, снова, когда программа обработки прерывания закончится, система (Процессор 1) готова возвратиться к прерванному потоку пользователя. При этом IRQL процессора 1 понижается до уровня IRQL dispatch_level, и Микроядро обслуживает Очередь DPC. Делая так (и по-прежнему выполняясь на Процессоре 1), микроядро удаляет Объект DPC драйвера, и вызывает подпрограмму DPC драйвера. Подпрограмма DPC драйвера теперь выполняется на Процессоре 1. Предполагая, что подпрограмма DPC драйвера еще не завершила выполнение на Процессоре 0, заметим, что та же самая подпрограмма DPC теперь выполняется параллельно на обоих процессорах.
Этот пример подчеркивает важность использования в драйверах надлежащего набора механизмов многопроцессорной синхронизации. В особенности, в функции DPC должны использоваться спин-блокировки для сериализации доступа к любым структурам данных, к которым нужно обратиться как к единому целому, при условии, что конструкция драйвера такая, что одновременно может произойти несколько вызовов DPC.

Характеристики Объекта DPC

Объекты DPC имеют две характеристики, которые влияют на путь, которым они обрабатываются. Этими характеристиками являются поля Importance и Number.

Важность DPC (DPC Importance)

Каждый Объект DPC имеет важность, которая хранится в поле Importance Объекта DPC. Значения для этого поля перечислены в ntddk.h под именами Highlmportance, Mediumlmportance, и Lowlmportance. Это значение DPC Объекта влияет на место в Очереди DPC, куда помещается Объект DPC при постановке в очередь, а также то, будет ли иметь место прерывание уровня IRQL dispatch_level при постановке Объекта DPC в очередь. Функция KelnitializeDpc() инициализирует Объекты DPC с важностью Mediumlmportance. Значение важности объекта DPC может быть установлено, используя функцию KeSetlmportanceDpc(), прототип которой:

VOID KeSetlmportanceDpc (IN PKDPC Dpc,
В KDPCIMPORTANCE Importance);

Где:
Dpc - Указатель на объект DPC, в котором должно быть установлено поле Importance;
Importance - значение важности для установки в Объекте DPC.
Объекты DPC с Mediumlmportance или Lowlmportance помещаются в конец Очереди DPC. Объекты DPC с Highlmportance ставятся в начало Очереди DPC.
Важность Объектов DPC также влияет на то, будет ли при помещении Объекта DPC в очередь сгенерировано программное прерывание уровня dispatch_level. Когда Объект DPC с Highlmportance или Mediumlmportance ставится в очередь текущего процессора, всегда генерируется прерывание dispatchjevel. Прерывание dispatch_level генерируется для Lowlmportance DPC или для тех DPC, которые предназначены для отличного от текущего процессора, согласно сложному (и недокументированному) алгоритму планирования.
В таблице 11 перечислены ситуации, инициирующие освобождение очереди объектов DPC.
Большинству драйверов устройства никогда не понадобится устанавливать важность своих Объектов DPC. В редких случаях, когда задержка между запросом DPC и выполнением DPC чрезмерна, и разработчик драйвера не в состоянии решить устранить эту задержку другим способом, Вы можете попытаться установить DPC Объекта в Highlmportance. Однако обычно драйверы устройств в Windows NT не изменяют свое значение DPC со значения по умолчанию Mediumlmportance.

Таблица 11. Ситуации, инициирующие очистку очереди DPC

Приоритет DPC DPC выполняются на том же процессоре, что и ISR DPC выполняются на другом процессоре
Низкий Размер очереди DPC превышает максимум, частота появления запросов DPC меньше минимальной, или система простаивает Размер очереди DPC превышает максимум или система простаивает (выполняется поток idle)
Средний Всегда Размер очереди DPC превышает максимум или система простаивает (выполняется поток idle)
Высокий Всегда Всегда

Целевой процессор для DPC (DPC Target Processor)

В дополнение к важности, каждый DPC Объект имеет целевой процессор (target processor). Это значение хранится в поле Number Объекта DPC. Целевой процессор показывает, ограничено ли выполнение DPC заданным процессором в системе, и, если да, то каким процессором. По умолчанию, Reinitialize Dpc() не определяет целевой процессор. Следовательно, по умолчанию, процедуры DPC будут работать на процессоре, на котором они запрошены (то есть, DPC будет вызван на процессоре, на котором была вызвана подпрограмма KelnsertQueueDpc()).
DPC может быть ограничено выполнением на указанном процессоре, используя функцию KeSetTargetProcessorDpc(), прототип которой:

VOID KeSetTargetProcessorDpc(IN PKDPC Dpc,
IN CCHAR Number);

Где:
Dpc - Указывает на объект DPC, для которого должен быть установлен целевой процессор;
Number - отсчитываемый от нуля номер процессора, на котором должен быть выполнен DPC.
Подобно важности DPC, целевой процессор DPC почти никогда не устанавливается драйвером устройства. Заданное по умолчанию значение, которое служит для выполнения DPC на текущем процессоре, почти всегда желательно.
Когда для Объекта DPC установлен конкретный целевой процессор, такой Объект DPC будет всегда ставиться в Очередь DPC указанного процессора. Таким образом, например, даже когда KelnsertQueueDpc() вызывается на процессоре 0, Объект DPC с установленным в качестве целевого процессора Процессором 1 будет вставлен в Очередь DPC Процессора 1.

DpcForlsr

Как уже было сказано ранее в этой главе, наиболее часто DPC используются для завершения Программы Обработки Прерывания (ISR). Для того, чтобы упростить драйверам устройств запросы DPC для завершения ISR из их функций ISR, Диспетчер ввода/вывода определяет специальный DPC, который может использоваться для этой цели. Этот DPC называется DpcForlsr.
Диспетчер ввода/вывода вставляет Объект DPC в каждый Объект Устройство, который он создает. Этот внедренный Объект DPC инициализируется драйвером устройства, обычно при первой загрузке драйвера, посредством вызова функции IoInitializeDpcRequest().
IoInitializeDpcRequest() принимает на входе указатель на Объект Устройство, в который внедрен Объект DPC, указатель на функцию драйвера для вызова, и значение контекста для передачи этой функции. IoInitializeDpcRequest(), в свою очередь, вызывает KelnitializeDpc(), чтобы инициализировать внедренный Объект DPC, передавая указатель на функцию драйвера как параметр DeferredRoutine, и значение контекста как параметр DeferredContext.
Чтобы запросить DPC из ISR, драйвер просто вызывает loRequestDpc(), передавая указатель на Объект Устройство. IoRequestDpc(), в свою очередь, вызывает KelnsertQueueDpc() для Объекта DPC, внедренного в Объект-Устройство.
Поскольку все драйверы устройства имеют Объекты-Устройства, и все драйверы, которые используют прерывания, также используют DPC, использование механизма DpcForlsr Диспетчера ввода/вывода очень удобно. Фактически, большинство драйверов устройств в Windows NT никогда напрямую не вызывают функции KelnitializeDpc() или KelnsertQueueDpc(), а вместо этого вызывают loInitializeDpcRequest() и IoRequestDpc().