[Previous] [Next]

Обслуживание прерываний

В операционной системе Windows NT 5 все прерывания изначально обрабатываются ядром. Это сделано для облегчения переносимости операционной системы на разные аппаратные платформы. Ядро обеспечивает диспетчеризацию прерываний между драйверами путем создания и последующего подключения объектов прерывания по вызову IoConnectInterrupt (см. таблицу 8.10). Этот вызов получает адрес процедуры для обслуживания прерываний (ISR, Interrupt Service Routine) драйвера, и таким образом операционная система связывает указанное аппаратное прерывание с определенным драйвером и принадлежащей ему ISR функцией.

Вызов IoConnectInterrupt возвращает указатель на объект прерывания (через первый аргумент). Возвращенный указатель должен быть сохранен в структуре расширения объекта устройства, так как он понадобится в дальнейшей работе, в частности, при отключении от источника прерываний.

Когда операционная система получает сигнал прерывания от устройства, она использует свой список объектов прерывания для локализации ISR процедуры, в ведении которой находится обслуживание данного события. Она "пробегает" по всем объектам прерывания, подключенным к DIRQL этого прерывания, и вызывает ISR процедуры до тех пор, пока одна из них не заявит о своих правах на него.

Диспетчер прерываний режима ядра вызывает ISR процедуру на уровне синхронизации SynchronizeIrql, указанном в вызове IoConnectInterrupt. Обычно, это один из DIRQL уровней. Кроме того, диспетчер прерываний получает владение над объектом спин-блокировки pSpinLock и удерживает ее во время выполнения ISR процедуры, что предохраняет от выполнения ISR процедуры на других процессорах.

При выполнении на столь высоком уровне IRQL существует некоторые вещи, которые процедура ISR не может себе позволить. В дополнении к обычным предостережениям избегать манипуляций со страничной памятью, ISR процедура не должна пытаться получать или освобождать какие-либо системные ресурсы, даже нестраничную память. Если разработчик предполагает сделать системный вызов из ISR процедуры, следует обязательно обратить внимание на уровень, на котором тот может выполняться. Вполне вероятно, что такие системные вызовы придется перепоручить DPC процедуре, запуск которой вполне может запланировать данная функция обслуживания прерываний.

Таблица 8.10. Прототип функции IoConnectInterrupt

NTSTATUS IoConnectInterrupt IRQL == PASSIVE_LEVEL
Параметры Регистрирует процедуру обслуживания прерывания, предоставляемую драйвером, и "подключает" ее к источнику прерываний
OUT PKINTERRUPT
*pInterruptObject
Адрес указателя, в котором будет возращен указатель на объект прерывания
IN PKSERVICE_ROUTINE
ServiceRoutine
Процедура (функция) драйвера, которая теперь будет обслуживать прерывание
IN PVOID pServiceContext Аргумент, передаваемый в процедуру ISR, обычно рекомендуется приводить здесь указатель на структуру расширения объекта устройства
IN PKSPIN_LOCK pSpinLock Инициализированный объект спин-блокировки
IN ULONG Vector Транслированное значение вектора прерывания
IN KIRQL Irql Значение DIRQL для данного устройства
IN KIRQL SynchronizeIrql Обычно равно значению Irql
IN KINTERRUPT_MODE InterruptMode Аппаратный тип прерывания. Одно из значений:
• LevelSensitive
• Latched
IN BOOLEAN isSharableVector Если TRUE — данный вектор прерывания является совместно используемым (разделяемым)
IN KAFFINITY ProcessorEnableMask Установить набор процессоров, которые могут получать сигналы прерывания
IN BOOLEAN doFloatingSave Если TRUE — сохранять состояние регистров сопроцессора (FPU). Обычно используется FALSE
Возвращаемое значение

• STATUS_SUCCESS
• STATUS_INVALID_PARAMETER
• STATUS_UNSUFFUCIENT_RESOURCES

Таблица 8.11. Прототип функции драйвера для обслуживания прерываний

BOOLEAN ISR IRQL == DIRQL
Параметры Процедура драйвера, предоставляемая им для обслуживания прерывания
IN PKINTERRUPT *pInterruptObject Объект прерывания, "генерирующий" прерывания
IN VOID pServiceContext Контекстный аргумент, указанный при регистрации в IoConnectInterrupt
Возвращаемое значение

• TRUE — прерывание было обслужено ISR
• FALSE — прерывание не обслуживается

Параметр InterruptMode вызова IoConnectInterrupt интерпретируется операционной системой следующим образом. Когда драйверы подключили свои ISR процедуры к прерыванию, считая его LevelSensitive, операционная система вызывает все подключенные таким образом ISR функции до тех пор, пока одна из них не возвратит TRUE. В противном случае, то есть если указано значение Latched для параметра InterruptMode, то операционная система вызывает все из подключенных таким образом ISR процедур, и эти вызовы повторяются до тех пор, пока все ISR процедуры не возвратят значение FALSE.

В рамках общего подхода Windows любой программный код должен минимизировать свое пребывание на высоких приоритетах. Всегда следует оптимизировать код ISR процедуры для достижения максимальной скорости его выполнения. Действия, которые нельзя отнести к абсолютно необходимым именно в ISR процедуре, следует вынести в процедуру отложенного вызова (DPC). Особенно важно, чтобы ISR процедура сразу же определилась, будет ли она обрабатывать поступившее прерывание. Возможно, многие ISR процедуры ожидают этого прерывания, а малозначительный код данной ISR процедуры блокирует их работу.

Использование объектов прерываний дело достаточно хлопотное. Во-первых, если ISR процедура обслуживает более чем одно прерывание или драйвер имеет более чем одну ISR процедуру, должна использоваться спин-блокировка для того, чтобы не возникло недоразумений в использовании контекстного аргумента pServiceContext процедур(ы) ISR.

Во-вторых, в случае, если процедура ISR управляет более чем одним прерыванием (вектором прерывания), следует позаботиться о том, чтобы значение, указанное в качестве SynchronizeIrql было наибольшим значение DIRQL из всех обслуживаемых прерываний.

Наконец, процедура ISR драйвера должна быть готова к работе с момента выполнения вызова IoConnectInterrupt. Даже в том случае, если выполнены еще не все действия по инициализации драйвера.

В общих чертах, процедура обслуживания прерываний должна придерживаться следующего плана:

  1. Определить, относится ли поступившее прерывание к данному драйверу. Если не относится — немедленно возвратить FALSE.
  2. Выполнить все операции над устройством, необходимые для того, чтобы подтвердить устройству получение прерывания.
  3. Определить, существует ли необходимость в передаче данных и дополнительных действиях, которые могут быть выполнены на низких уровнях IRQL. Если такая работа имеется, то следует запланировать вызов DPC процедуры (предоставляемой драйвером), то есть поставить в очередь DPC-запрос вызовом IoRequestDpc.
  4. Возвратить значение TRUE.

Таблица 8.12. Прототип вызова IoRequestDpc

VOID IoRequestDpc IRQL == DIRQL
Параметры Помещает DPC вызов в очередь
IN PDEVICE_OBJECT pDevObject Объект устройства, для которого зарегистрирована DPC процедура
IN PIRP pIrp Указатель на интересующий IRP пакет
IN VOID pServiceContext Контекстный аргумент
Возвращаемое значение void

Если сравнить прототип ISR процедуры и прототип вызова IoRequestDpc, который делается из ISR процедуры для планирования последующего вызова DPC процедуры (для завершения работы над прерыванием), то становится очевидной проблема.

Обычно рабочие процедуры драйвера получают указатель на объект устройства и указатель на адресованный этому объекту IRP пакет через заголовок при вызове. Но прототип ISR процедур этого не предусматривает. Как же тогда сделать из нее вызов IoRequestDpc, чтобы запланировать DPC процедуру?

Данное затруднение решается, если при регистрации ISR процедуры вызовом IoConnectInterrupt в качестве контекстного аргумента pServiceContext ввести указатель на структуру расширения структуры (извините за неблагозвучные повторы) данного объекта устройства, то есть указатель на DEVICE_EXTENSION. При условии заблаговременного сохранения там указателя на объект устройства (например, как это было сделано в DriverEntry примера главы 3) процедура ISR драйвера не будет испытывать затруднения с тем, откуда ей взять данный указатель.

Остается вопрос, что такое pIrp и где его найти?

Возможны два варианта. Во-первых, для обработки прерывания действительно требуется текущий обрабатываемый драйвером IRP пакет, и тогда его можно найти, как pDeviceObject->CurrentIrp, но только для пакета из системной очереди, SystemQueuing. Во-вторых, IRP пакет может и не потребоваться (по логике прерывания и работы драйвера), тогда можно просто указать NULL. Более того, во многих случаях IRP пакет указать невозможно или нежелательно. Поскольку Диспетчер ввода/вывода не анализирует этот параметр, то в нем можно передавать нужные (по логике работы) данные так же, как и в указателе pServiceContext. В результате, ISR процедура может выглядеть следующим образом:

BOOLEAN OnInterrupt ( PKINTERRUPT pInterruptObject,
                      PVOID pServiceContext )
{
	PMYDEVICE_EXTENSION pDevExt=(PMYDEVICE_EXTENSION) pServiceContext;


// Считываем нужный регистр нашего устройства чтобы
// определить, действительно ли оно посылало сигнал прерывания
ULONG intrStatus =
	READ_PORT_ULONG((PULONG) (pDevExt->InterruptStateReg));
if( intrStatus!=. . . ) return FALSE; // Это чужое прерывание

// Некоторые действия, например, изменение состояния
// регистров устройства, чтобы оно знало: о нем помнят.
. . .
// Планируем вызов DPC процедуры, например, без IRP пакета:
IoRequestDpc( pDevExt->DeviceObject, NULL, pDevExt);

return TRUE; // Прерывание обработано
} 

Детальное рассмотрение вопросов обработки прерываний и применения DPC процедур выполнено в главе 11, "Обработка аппаратных прерываний".

Процедуры отложенного вызова обслуживания прерываний DpcForIsr

Вызов процедуры DpcForIsr может быть запланирован из собственно ISR процедуры вызовом IoRequestDpc. Прототип этой функции драйвера описан в таблице 8.13.

Таблица 8.13. Прототип функции для процедуры DpcForIsr

VOID DpcForIsr IRQL == DISPATCH_LEVEL
Параметры Завершает обработку прерывания, начатую в ISR процедуре драйвера
IN PKDPC pDpc DPC-объект
IN PDEVICE_OBJECT pDevObj Указатель на объект устройства, для которого зарегистрирована данная DPC процедура
IN PIRP pIrp Интересующий пакет IRP
IN VOID pContext Контекстный указатель, переданный вызовом IoRequestDpc
Возвращаемое значение void

При анализе прототипа вызова IoRequestDpc и примера кода перед таблицей 8.13, возникает правомерный вопрос: вызов какой функции планирует ISR процедура, выполняя вызов IoRequestDpc? Ведь DpcForIsr функция не фигурирует ни при регистрации ISR процедуры вызовом IoConnectInterrupt, ни в тексте OnInterrupt.

Ответ состоит в том, что DpcForIsr регистрируется для конкретного объекта устройства вызовом IoInitializeDpcRequest (см. таблицу 8.14). Если обратить внимание на структуру DEVICE_OBJECT, описанную в заголовочных файлах wdm.h и ntddk.h (см. пакет DDK), то несложно заметить поле 'KDPC Dpc', предназначенное как раз для хранения DPC объекта. Соответственно, выполняя вызов IoRequestDpc c указателем на объект устройства в качестве первого аргумента, ISR процедура драйвера однозначно определяет, какая именно DpcForIsr функция будет вызвана позже — хранящаяся в объекте устройства.

Таблица 8.14. Прототип вызова IoInitializeDpcRequest

VOID IoInitializeDpcRequest IRQL == PASSIVE_LEVEL
Параметры Регистрирует DpcForIsr процедуру для данного объекта устройства, создает и инициализирует DPC объект
IN PDEVICE_OBJECT pDevObj Указатель на объект устройства, для которого регистрируется данная DPC процедура
IN VOID (*DpcForIsr) Адрес DpcForIsr процедуры, которая должна иметь прототип, описанный в таблице 8.13
Возвращаемое значение void

Регистрацию DpcForIsr процедуры (и ее связывание с объектом устройства) для PnP драйверов рекомендуется делать в процедуре AddDevice драйвера.

Первый параметр pDpc в описании прототипа процедуры DpcForIsr (таблица 8.13) не должен пугать разработчика. Если используется вызов IoInitializeDpcRequest, то именно он создает данный объект, а в вызов DpcForIsr поступит указатель на уже готовый DPC объект.

Выполнения кода процедуры DpcForIsr

В ответ на выполненный ISR процедурой вызов IoRequestDpc, принадлежащая данному драйверу процедура DpcForIsr добавляется в очередь DPC объектов (DPC dispatch queue). Когда значение процессорного IRQL падает ниже DISPATCH_LEVEL, диспетчер процедур обработки отложенных вызовов выполняет вызов DpcForIsr данного драйвера. Процедура DpcForIsr выполняется на уровне DISPATCH_LEVEL, что означает: она не должна работать с адресами страничной памяти.

Диспетчер ввода/вывода игнорирует множественные вызовы IoRequestDpc для данного объекта устройства до начала выполнения DpcForIsr. Попытки повторно поместить объект DPC в очередь отклоняются. Это является нормальным отношением ко всем DPC объектам. В случае если логика работы драйвера такова, что он может выдавать перекрывающиеся DPC запросы для одного и того же устройства, и их следует учитывать, то такой драйвер должен реализовать собственную очередь DPC запросов (объектов).

Перечень обязанностей типовой процедуры DpcForIsr может включать:

VOID DpcForIsr( PKDPC Dpc,
                PDEVICE_OBJECT pDevObject,
                PIRP junk,
                PDEVICE EXTENSION pDevExtension)
{
	// Неким образом получаем текущий IRP (например, из очереди
	// queueReadWrite, которую поддерживает сам драйвер):
	PIRP pIrp = GetCurrentIrp(&pDevExtension->queueReadWrite);

	// Инициируем поступление IRP из внутренней очереди в
	// в процедуру StartIO():
	TodoStartNextPacket(&pDevExtension->dqReadWrite, pDevObject)

	// Даем возможность отработать процедурам завершения всех
	// вышестоящий драйверов, если они есть:
	IoCompleteRequest(pIrp, IO_NO_INCREMENT);
} 

Отключение от источника прерываний

В случае, если драйвер должен обладать возможностью быть выгружаемым, то имеется насущная необходимость его отключения от источника прерываний. При этом он удаляется из внутреннего списка кода ядра, где он обозначен как обработчик прерываний. Разумеется, это необходимо выполнить до того, как драйвер будет удален из оперативной памяти. В противном случае код ядра операционной системы в ответ на прерывание, сгенерированной устройством, осуществит вызов процедуры по адресу нестраничном пуле, где раньше "обитала" процедура ISR. Это неминуемо приведет к краху системы.

Отключение от источника прерываний является двухступенчатой процедурой. Во-первых, следует использовать KeSynchronizeExecution и процедуру SynchCritSection для того, чтобы обеспечить такое состояние устройства, когда он не будет производить генерацию сигналов на прерывание. Во-вторых, следует произвести удаление ISR процедуры из системного списка обработчиков прерываний путем осуществления вызова IoDisconnectInterrupt (с передачей этому вызову в качестве аргумента указателя на полученный ранее объект прерывания для данного устройства).

Таблица 8.15. Прототип вызова IoDisconnectInterrupt

VOID IoDisconnectInterrupt IRQL == PASSIVE_LEVEL
Параметры Регистрирует DpcForIsr процедуру для данного объекта устройства
IN PKINTERRUPT pInterruptObject Указатель на объект прерывания, полученный ранее в результате вызова IoConnectInterrupt
Возвращаемое значение void

Нерассмотренным остается один весьма деликатный момент. При регистрации ISR процедуры для обслуживания конкретного прерывания используется вызов IoConnectInterrupt, подробно описанный в таблице 8.10. Наиболее важным и трудным в обращении является параметр Vector, представляющий транслированное прерывание, к которому и производится подключение регистрируемой ISR процедуры. Данная процедура имеет свою специфику для каждого типа не-WDM драйверов (в зависимости от того, к шине какого типа подключено устройство, обслуживаемое драйвером, например, ISA или PCI). Однако для WDM драйверов эта процедура универсальна. В данном случае, когда PnP Менеджер делает запрос с кодом IRP_MJ_PNP и субкодом IRP_MN_START_DEVICE, то в каждом таком запросе он передает и список присвоенных драйверу ресурсов.

Драйвер WDM, например, из пакета разработки устройств PCI шины фирмы PLX Technology, выходит из положения следующим образом.

Прежде всего, данный драйвер, получив IRP пакет с указанными признаками (IRP_MJ_PNP, субкодом IRP_MN_START_DEVICE), дает возможность сначала обработать его нижележащим драйверам, перехватывая его на обратном пути. После этого он производит анализ полученных ресурсов, в частности, подключает собственную функцию обработки прерываний PlxOnInterrupt к прерыванию, как оно было предопределено PnP Менеджером, передавшим выделенные ресурсы в ячейке стека пакета IRP.

int i, count;
BOOLEAN interruptPresented = FALSE;
PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation( pIrp );
PCM_PARTIAL_RESOURCE_LIST pRawResource=
	&stack->Parameters.StartDevice.AllocatedResources->
		List[0].PartialResourceList;
count = pRawResource->Count;
for (i = 0; i < count; i++, pRawResource++)
{
	//Просмотр всех выделенных драйверу ресурсов
	switch (pRawResource ->Type)
	{
	case CmResourceTypeInterrupt:     // Прерывание
		interruptPresented = TRUE;
		IrqL = (KIRQL) Resource->u.Interrupt.Level;
		vector = Resource->u.Interrupt.Vector;
		affinity = Resource->u.Interrupt.Affinity;
		if (ResourceRaw->Flags == CM_RESOURCE_INTERRUPT_LATCHED)
			mode = Latched;
		else
			mode = LevelSensitive;
		break;
		. . .
	}
}
if (interruptPresented)
{
	// Здесь следует запретить прерывания от ведомого устройства
	. . .
	// Создание объекта прерывания и подключение к нему
	status = IoConnectInterrupt(
								&pDevExtension->pInterruptObject,
								PlxOnInterrupt,
								pDevExtension,
								NULL,
								vector,
								IrqL,
								IrqL,
								mode,
								TRUE,
								affinity,
								FALSE
								);
	if ( !NT_SUCCESS(status) )
	{ // обработка ошибки
	}
	else
	{ // разрешить прерывания от устройства
	}
} 

Здесь pDevExtension указывает на структуру расширения объекта устройства (соответственно, эта структура должна предусмотреть в своем составе место для хранения указателя на создаваемый объект прерывания, здесь pInterruptObject), a PlxOnInterrupt передает адрес предоставляемой данным драйвером ISR процедуры, созданной в соответствии с прототипом, описанным в таблице 8.11.