Введение в обработку прерываний
Активизация и обслуживание 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, SystemArgument1 и 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.