[Previous] [Next]

Исполняемый код драйвера

Как и в первом варианте драйвера, исходный текст всех функций драйвера представлен файлом Driver.cpp.

Часто повторяющийся код по завершению обработки IRP пакетов собран в функции CompleteIrp.

В процедуре DriverEntry дополнительно (по сравнению с первым вариантом) выполняется регистрация функции StartIo, которая будет использоваться при старте процесса обработки IRP пакетов, откладываемых в системную очередь обработчиком IOCTL запросов с IOCTL кодом IOCTL_SEND_TO_PORT.

Функция CreateDevice по-прежнему выполняет создание объекта устройства, регистрацию символьной ссылки и подключение драйвера к прерыванию. Однако теперь иначе выполняется регистрация DPC процедуры: при помощи вызова IoInitializeDpcRequest (таблица 8.14) с передачей ссылки на объект устройства создается DPC объект в "недрах" системы. И тот код, который пожелает запланировать вызов функции DpcForIsr (регистрируемой таким образом), должен выполнить вызов IoRequestDpc. Еще раз следует отметить, что в WDM драйвере реального PnP устройства эти операции следовало бы выполнять в процедуре AddDevice и обработчике IR_MJ_PNP + IRP_MN_START_DEVICE, так как загрузка драйвера является только частью старта настоящего PnP устройства.

Процедура DriverUnload (она отвечает за завершающие операции при выгрузке драйвера) более не занимается DPC объектами (считается, что драйвер теперь не имеет прямого доступа ни к одному такому объекту). Однако в обязанности этой функции теперь входит выполнение вызова ObDereferenceObject для уменьшения числа ссылок на используемый объект события, чтобы разрешить операционной системе его удаление, если эта операция будет сочтена целесообразной.

Обработчики DispatchCreate (при получении клиентом дескриптора для доступа к драйверу) и DispatchClose (при закрытии дескриптора, полученного в DispatchCreate) не претерпели изменений. Обработчики DispatchWrite и DispatchRead теперь в драйвере отсутствуют, а обработка запросов от клиента на чтение и запись данных в параллельный порт переложена на плечи функции DeviceControlRoutine (обработчик IRP пакетов, поступающих вследствие клиентских вызовов от Win32 функции DeviceIoControl).

Функционально работу модифицированного драйвера можно описать следующим образом.

Драйвер получает IOCTL запрос IOCTL_SEND_TO_PORT от клиента (тестирующего приложения), откладывает запрос в системную очередь (см. описание механизма System Queuing в 6 главе) и завершает обработку IRP пакета, помечая его как завершенный в состоянии STATUS_PENDING, указывая на всякий случай процедуру CancelRoutine драйвера. Эта процедура получит управление, если клиент решит отменить запрос. Заметим, что такая ситуация возникает, когда тестирующее приложение запускается в отсутствие заглушки CheckIt в параллельном порту. Пакет не может быть завершен, и приложение простаивает в ожидании окончания синхронного вызова DeviceIoControl — снятие приложения приводит к вызову CancelRoutine.

При извлечении IRP пакета из очереди Диспетчер ввода/вывода вызывает зарегистрированную в DriverEntry функцию StartIo, которая проверяет условия своего вызова, записывает в pIrpStack->Parameters.DeviceIoControl.Type3InputBuffer ноль и планирует работу DPC функции DpcForIsr вызовом IoRequestDpc. Заметим, что это проходит вполне корректно (несмотря на то, что IoRequestDpc необходимо вызывать на уровнях IRQL не ниже DISPATCH_LEVEL), поскольку сама функция StartIo работает на IRQL равном DISPATCH_LEVEL.

Работа DpcForIsr при первом вызове сводится к выводу первого байта (точнее половины байта) в порт и запуску первого прерывания, после чего срабатывает цепная реакция Isr-DpcForIsr-перенос/прерывание-Isr до полного переноса всей порции данных, поступивших по IOCTL запросу IOCTL_SEND_TO_PORT от клиента (тестирующего приложения). При выполнении переноса и при условии, что предварительно клиент запросил использование объекта события, процедура DpcForIsr выполняет установку объекта события в сигнальное состояние и таким образом сигнализирует клиенту, что перенос завершен. Тут следует обратить внимание, что, при условии регистрации объекта события, когда клиент решил отменить свой запрос на запись в порт и управление получает функция CancelRoutine, то ее код должен что-то сделать с объектом события. В нашем случае он переводится в сигнальное состояние, поскольку прекращение жизни IRP пакета — это тоже (хотя и своеобразное) завершение работы над ним.

//=========================================================================
// Файл driver.cpp (Вариант 2)
// Драйвер обслуживания заглушки CheckIt (параллельный порт 378h)
// By SVP, 21 June 2004
//=========================================================================
#include "driver.h"
// Предварительные объявления функций
NTSTATUS CreateDevice   ( IN PDRIVER_OBJECT pDriverObject,
                          IN ULONG          portBase,
                          IN ULONG          Irq  );
NTSTATUS DispatchCreate ( IN PDEVICE_OBJECT pDevObj, IN PIRP pIrp );
NTSTATUS DispatchClose  ( IN PDEVICE_OBJECT pDevObj, IN PIRP pIrp );
VOID     DriverUnload   ( IN PDRIVER_OBJECT pDriverObject );
NTSTATUS DeviceControlRoutine ( IN PDEVICE_OBJECT pDevObj, IN PIRP pIrp );
VOID     StartIo        ( IN PDEVICE_OBJECT pDevObj,IN PIRP pIrp );
BOOLEAN  Isr            ( IN PKINTERRUPT pInterruptObject, IN PVOID pServiceContext );
VOID     DpcForIsr      ( IN PKDPC pDpc,
                          IN PVOID DeferredContext,
                          IN PVOID pArg1, IN PVOID pArg2 );
//=========================================================================
NTSTATUS CompleteIrp( PIRP pIrp, NTSTATUS status, ULONG size)
{
	pIrp->IoStatus.Status = status;
	pIrp->IoStatus.Information = size;
	IoCompleteRequest( pIrp, IO_NO_INCREMENT );
return status;
}
//=========================================================================
// Функция:     DriverEntry
// Назначение:  Инициализирует драйвер, подключает объект устройства для
//              получения прерываний.
// Аргументы:   pDriverObject - поступает от Диспетчера ввода/вывода
//              pRegistryPath - указатель на Юникод-строку,
//              обозначающую раздел Системного Реестра, созданный
//              для данного драйвера.
// Возвращаемое значение:
//              NTSTATUS - в случае нормального завершения STATUS_SUCCESS
//                         или код ошибки STATUS_Xxx
//
extern "C" NTSTATUS DriverEntry (  IN PDRIVER_OBJECT  pDriverObject,
                                   IN PUNICODE_STRING pRegistryPath )
{
	NTSTATUS status;
	#if DBG==1
		DbgPrint("LPTPORT: in DriverEntry, RegistryPath is:\n     %ws. \n",
		                   pRegistryPath->Buffer);
	#endif
	// Регистрируем рабочие процедуры драйвера:
	pDriverObject->DriverUnload = DriverUnload;
	pDriverObject->MajorFunction[IRP_MJ_CREATE] = DispatchCreate;
	pDriverObject->MajorFunction[IRP_MJ_CLOSE]  = DispatchClose;
	pDriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] =
	                                             DeviceControlRoutine;
	pDriverObject->DriverStartIo = StartIo;
	// Работа по созданию объекта устройства, подключению
	// ресурсов, прерывания, созданию символьной ссылки:
	status = CreateDevice(pDriverObject, 0x378, 0x7);
return status;
}
//=========================================================================
// Функция:     CreateDevice
// Назначение:  Создание устройства с точки зрения системы
// Аргументы:   pDriverObject - поступает от Диспетчера ввода/вывода
//              portBase - адрес базового регистра параллельного порта (378h)
//              Irq - прерывание (в терминах шины ISA) для обслуживания порта
// Возвращаемое значение:
//              NTSTATUS - в случае нормального завершения STATUS_SUCCESS
//                         или код ошибки STATUS_Xxx
//
NTSTATUS CreateDevice ( IN PDRIVER_OBJECT   pDriverObject,
                        IN ULONG            portBase,
                        IN ULONG            Irq   )
{
	NTSTATUS status;
	PDEVICE_OBJECT pDevObj;
	PDEVICE_EXTENSION pDevExt;
	// Создаем внутреннее имя устройства
	UNICODE_STRING devName;
	RtlInitUnicodeString( &devName, L"\\Device\\LPTPORT" );
	// Создаем объект устройства
	status= IoCreateDevice( pDriverObject,
	            sizeof(DEVICE_EXTENSION),
	            &devName,
	            FILE_DEVICE_UNKNOWN,
	            0,
	            FALSE, // TRUE - for exclusive access
	            &pDevObj
	            );
	if (!NT_SUCCESS(status))  return status;

	// Указывать, что будем использовать метод буферизации
	// BUFFERED_IO при отсутствии обработчиков Read/Write
	// уже не актуально, поэтому можем закоментировать:
	// pDevObj->Flags |= DO_BUFFERED_IO;
	// Заполняем данными структуру Device Extension
	pDevExt = (PDEVICE_EXTENSION)pDevObj->DeviceExtension;
	pDevExt->pDevice        = pDevObj;   // сохраняем - это пригодится
	pDevExt->ustrDeviceName = devName;
	pDevExt->Irq            = Irq;
	pDevExt->portBase       = (PUCHAR)portBase;
	pDevExt->pIntObj        = NULL;
	pDevExt->xferCount      = 0; // сейчас нет полученных данных
	pDevExt->pIntObj        = NULL;
	pDevExt->pEvent         = NULL;
	pDevExt->hEvent         = NULL;
	//================================================
	// Теперь не инициализируем объект DPC,
	// а закрепляем за объектом устройства (а значит и за обслуживаемым
	// им прерыванием) единственную DpcForIsr функцию:
	IoInitializeDpcRequest( pDevObj, DpcForIsr );
	//================================================
	// На всякий случай блокируем поступление прерываний:
	WriteControlRegister ( pDevExt, CR_DEFAULT );
	//================================================
	// Создаем и подключаем объект прерываний:
	KIRQL     kIrql;
	KAFFINITY kAffinity;
	ULONG kVector =
		HalGetInterruptVector(Isa, 0, pDevExt->Irq, pDevExt->Irq,
		                      &kIrql,    &kAffinity);
	// Замечание. Для Isa шины второй параметр (номер шины) обычно
	// равен 0, а третий и четвертый параметры равны.
	#if DBG==1
		DbgPrint( "LPTPORT: Interrupt %d converted to kIrql = %d, "
		"kAffinity = %d, kVector = %X(hex)\n",
		pDevExt->Irq, kIrql, kAffinity, kVector);
	#endif

	status =
  	 IoConnectInterrupt (
		&pDevExt->pIntObj, // Здесь будет создан Interrupt Object
		Isr,       // Наша функция ISR
		pDevExt,   // Этот указатель ISR функция будет
		           // получать при вызове (контекстный указатель)
		NULL,      // Не будем использовать spin-блокировку для
		           // безопасного доступа к совместно используемым
		           // данным
		kVector,   // транслированное значение прерывания
		kIrql,	   // DIRQL
		kIrql,     // DIRQL
		Latched,   // Прерывание по перепаду
		TRUE,      // Совместно используемое (Shared) прерывание
		kAffinity, // Поцессоров в мультипроцессорной системе
		FALSE );   // Не сохранять значения регистров сопроцессора
	if (!NT_SUCCESS(status))
	{
		// В случае неудачи удаляем объект устройства
		IoDeleteDevice( pDevObj );
		return status;
	}	
	#if DBG==1
		DbgPrint("LPTPORT: Interrupt successfully connected.\n");
	#endif
	//================================================
	// Создаем символьную ссылку:
	UNICODE_STRING symLinkName;
	// Сформировать символьное имя:
	//#define	SYM_LINK_NAME	L"\\??\\LPTPORT0"
	//                              ^^ проходит только в NT
	// Для того, чтобы работало в Windows 98 & XP :
	#define SYM_LINK_NAME L"\\DosDevices\\LPTPORT0"
	RtlInitUnicodeString( &symLinkName, SYM_LINK_NAME );

	// Создать символьную ссылку:
	status = IoCreateSymbolicLink( &symLinkName, &devName );
	if (!NT_SUCCESS(status))
	{	// При неудаче - отключаемся от прерывания и
		// удаляем объект устройства:
		IoDisconnectInterrupt( pDevExt->pIntObj );
		IoDeleteDevice( pDevObj );
		return status;
	}
	pDevExt->ustrSymLinkName = symLinkName;
	#if DBG==1
		DbgPrint("LPTPORT: Symbolic Link is created: %ws. \n",
		                   pDevExt->ustrSymLinkName.Buffer);
	#endif
return STATUS_SUCCESS;
}
//=========================================================================
// Функция:     DriverUnload
// Назначение:  Останавливает и удаляет объекты устройств, отключает
//              прерывания, подготавливает драйвер к выгрузке.
// Аргументы:   pDriverObject - поступает от Диспетчера ввода/вывода
// Возвращаемое значение: нет
//
VOID DriverUnload ( IN PDRIVER_OBJECT pDriverObject )
{
	#if DBG==1
		DbgPrint("LPTPORT: in DriverUnload now\n");
	#endif
	PDEVICE_OBJECT pNextObj = pDriverObject->DeviceObject;
	// Проход по всем устройствам, контролируемым драйвером
	for( ; pNextObj!=NULL; )
	{
		PDEVICE_EXTENSION pDevExt = (PDEVICE_EXTENSION)pNextObj->DeviceExtension;
		// Удаляем объект прерываний:
		if (pDevExt->pIntObj)
		{
			// На всякий случай блокируем поступление прерываний
			WriteControlRegister( pDevExt, CR_DEFAULT);
			IoDisconnectInterrupt( pDevExt->pIntObj );
		}
		// Удаляем символьную ссылку:
		IoDeleteSymbolicLink(&pDevExt->ustrSymLinkName);
		#if DBG==1
			DbgPrint("LPTPORT: SymLink %ws deleted\n",
			                   pDevExt->ustrSymLinkName.Buffer);
		#endif
		// Сообщаем о прекращении использования объекта события
		if(pDevExt->pEvent!=NULL)
		{
		    ObDereferenceObject(pDevExt->pEvent);
		    #if DBG==1
		    	DbgPrint("LPTPORT: Event object dereferenced.\n");
		    #endif
		}
		// Сохраняем ссылку на следующее устройство и удаляем
		// текущий объект устройства:
		pNextObj = pNextObj->NextDevice;
		IoDeleteDevice( pDevExt->pDevice );
	}
	// Замечание. Поскольку мы использовали ресурс (параллельный порт)
	// объявленные не нами, то освобождение этого ресурса можно опустить.
}
//=========================================================================
// Функция:    DispatchCreate
// Назначение: Обрабатывает запрос по поводу Win32 вызова CreateFile
// Аргументы:  pDevObj - поступает от Диспетчера ввода/вывода
//             pIrp - поступает от Диспетчера ввода/вывода
// Возвращаемое значение: STATUS_SUCCESS
//
NTSTATUS DispatchCreate ( IN PDEVICE_OBJECT pDevObj, IN PIRP pIrp )
{
	#if DBG==1
		DbgPrint("LPTPORT: in DispatchCreate now\n");
	#endif
	CompleteIrp( pIrp, STATUS_SUCCESS, 0 ); // ничего не передано
return STATUS_SUCCESS;
}
//=========================================================================
// Функция:    DispatchClose
// Назначение: Обрабатывает запрос по поводу Win32 вызова CloseHandle
// Аргументы:  pDevObj - поступает от Диспетчера ввода/вывода
//             pIrp - поступает от Диспетчера ввода/вывода
// Возвращаемое значение: STATUS_SUCCESS
//
NTSTATUS DispatchClose ( IN PDEVICE_OBJECT pDevObj, IN PIRP pIrp )
{
	PDEVICE_EXTENSION  pDevExt =
	         (PDEVICE_EXTENSION) pDevObj->DeviceExtension;
	#if DBG==1
		DbgPrint("LPTPORT: in DispatchClose now\n");
	#endif
	// Сообщаем о прекращении использования объекта события
	if(pDevExt->pEvent!=NULL)
	{
	    // Удаляем объект события, поскольку клиент заканчивает
	    // работу с драйвером и не сможет больше отдать команду
	    // о прекращении работы с событием:
	    #if DBG==1
	    	DbgPrint("LPTPORT: DispatchClose, attempt to close handle %04X.\n",
			                   pDevExt->hEvent );
	    #endif
	    NTSTATUS sts = ZwClose(pDevExt->hEvent);
	    pDevExt->pEvent = NULL;
	    pDevExt->hEvent = NULL;
	    #if DBG==1
	    	DbgPrint("LPTPORT: DispatchClose, event handle closed (status=%0X).\n",
			                   sts );
	    // sts == 0 <==> sts == STATUS_SUCCESS
	    #endif
	}
	CompleteIrp( pIrp, STATUS_SUCCESS, 0 ); // ничего не передано
return STATUS_SUCCESS;
}
//=========================================================================
// Функция:    CancelRoutine
// Назначение: Вызывается в случае отказа клиента от запроса на обработку
//             IRP пакета, который был отложен (для работы со StartIo)
// Аргументы:  pDevObj - поступает от Диспетчера ввода/вывода
//             pIrp    - поступает от Диспетчера ввода/вывода
// Возвращаемое значение: нет
//
VOID CancelRoutine ( IN PDEVICE_OBJECT pDevObj, IN PIRP pIrp )
{
	PDEVICE_EXTENSION pDevExt =
			(PDEVICE_EXTENSION) pDevObj->DeviceExtension;
	#if DBG==1
		KIRQL  currentIrql = KeGetCurrentIrql();
		DbgPrint("LPTPORT: in CancelRoutine now, Irql=%d, IRP Canceled.\n",
		                   currentIrql );
	#endif
	if(pIrp == pDevObj->CurrentIrp)
	{
		IoReleaseCancelSpinLock(pIrp->CancelIrql);
		CompleteIrp( pIrp, STATUS_CANCELLED, 0 );
		//^^^^^^^^^ Что бы ни утверждалось в DDK, с этим
		// работает лучше! (Иначе - "глючит" Monitor)
	}
	else // Пакет - не текущий, удаляем его из системной очереди,
	{    // если он там есть:
		KeRemoveEntryDeviceQueue(
			&pDevObj->DeviceQueue,
			&pIrp->Tail.Overlay.DeviceQueueEntry);
		IoReleaseCancelSpinLock(pIrp->CancelIrql);
		CompleteIrp( pIrp, STATUS_CANCELLED, 0 );
	}
	// Во всяком случае, обработка данного пакета завершена
	if(pDevExt->pEvent!=NULL)
	{
	   KeSetEvent(pDevExt->pEvent, IO_NO_INCREMENT , FALSE );
	}
	// Не повредит...
	IoStartNextPacket( pDevObj, TRUE );
}
//=================================================================================
// Функция:    StartIo
// Назначение: Вызывается для обработки отложенных IRP пакетов.
// Аргументы:  pDevObj - поступает от Диспетчера ввода/вывода
//             pIrp    - поступает от Диспетчера ввода/вывода
// Возвращаемое значение: нет
//
VOID StartIo( IN PDEVICE_OBJECT pDevObj, IN PIRP pIrp)
{
	#if DBG==1
		KIRQL currentIrql = KeGetCurrentIrql();
		DbgPrint("LPTPORT: We are now in StartIo , currentIrql=%d\n", currentIrql );
	#endif
	PDEVICE_EXTENSION   pDevExt = (PDEVICE_EXTENSION)pDevObj->DeviceExtension;
	PIO_STACK_LOCATION  pIrpStack = IoGetCurrentIrpStackLocation( pIrp );
	PUCHAR userBuffer;
	ULONG  xferSize;		
	switch( pIrpStack->MajorFunction )
	{
	   case IRP_MJ_DEVICE_CONTROL:
	   {
		ULONG controlCode =
			pIrpStack->Parameters.DeviceIoControl.IoControlCode;
		if( controlCode != IOCTL_SEND_TO_PORT )
		{  // Сюда мы не должны попадать:
		   #if DBG==1
		   	DbgPrint("LPTPORT: StartIo: bad IOCTL.");
		   #endif
		   CompleteIrp( pIrp, STATUS_NOT_SUPPORTED, 0 );
		   IoStartNextPacket( pDevObj, FALSE );
		   break;
		}
		// Очищаем место, где будем хранить номер следующего байта в
		// клиентском буфере, который следует передать:
		pIrpStack->Parameters.DeviceIoControl.Type3InputBuffer=(PVOID)0;
		//=============================================================
		// Можно было сделать как и в варианте 1 для запуска переноса:
		// KeSynchronizeExecution( pDevExt->pIntObj,
		//                         DoNextTransfer,
		//                         pDevExt );
		//
		// Но пойдем усложненным путем: внесем изменения в DpcForIsr
		// и DoNextTransfer, затем запланируем вызов DpcForIsr.
		// Замечание. Функцию IoRequestDpc для планирования DpcForIsr
		// нужно вызывать с уровня IRQL не ниже DISPATCH_LEVEL,
		// а StartIo работает как раз на этом уровне IRQL:
		#if DBG==1
			DbgPrint("LPTPORT: StartIo: IoRequestDpc will be called now.");
		#endif
		IoRequestDpc( pDevObj, NULL, NULL );
		//=============================================================
	   	break;
	   }
	   default:
	   {    // Сюда мы не должны попадать:
		#if DBG==1
			DbgPrint("LPTPORT: StartIo: bad IOCTL (in 'switch-default').");
		#endif
		CompleteIrp( pIrp, STATUS_NOT_SUPPORTED, 0 );
		IoStartNextPacket( pDevObj, FALSE );
		break;
	   }
	}
}

Как было сказано, работа с процедурой StartIo и системной очередью незавершенных IRP пакетов без особых усилий со стороны разработчика драйвера дает сериализацию (последовательную обработку) пакетов, отражающих поступившие запросы от клиентов драйвера. Если драйвер зарегистрировал процедуру StartIo и затем в обработчиках запросов от клиента ставит IRP пакеты в очередь на обработку соответствующим образом (вызовом IoStartPacket и т.д., что выполняет обработчик DeviceControlRoutine в отношении пакетов с IOCTL кодом, равным IOCTL_SEND_TO_PORT), то Диспетчер ввода/вывода в каждый момент времени будет "выдавать" драйверу (точнее, объекту устройства) не более чем по одному пакету. Эти порции так и будут попадать в процедуру StartIo в связке "объект устройства"-"IRP пакет". Не нужно организовывать особых "синхронизационных" мероприятий по правильному и безопасному совместному использованию устройства. Процедура StartIo должна только запустить процесс (последовательность событий, а не программный!), в результате которого когда-то завершится обработка IRP пакета (успешно или нет, не имеет принципиального значения). Это может быть запуск устройства (прерывание, извещающее о готовности устройства, может вызвать к работе ISR процедуру и целую цепочку событий) или тривиальное программирование последующего запуска отложенной процедуры, как в данном случае. До момента объявления IRP пакета "завершенным" Диспетчер ввода/вывода не вызовет StartIo снова, несмотря на то, что сама процедура StartIo могла уже давно завершить свою работу.

Однако если устройство занято, то появляется желание и далее эксплуатировать механизм системной очереди незавершенных запросов (System Queuing), и поставить запрос снова в эту же очередь, в надежде на то, что очень скоро устройство будет все-таки готово воспринять запрос. В тех случаях, когда в этом действительно имеется необходимость и драйвер собственными силами ведет очередь незавершенных пакетов, ничто не мешает это сделать. Однако в системной очереди это не проходит — повторный вызов IoStartPacket с только что извлеченным из системной очереди IRP пакетом надежно "заклинивает" системную очередь. В этом имеется своя логика — если устройство не смогло обработать запрос сейчас, то зачем его снова вставлять в очередь IRP пакетов с такими же параметрами? Они все равно не смогут быть обработаны. Лучшее решение состоит в том, чтобы вернуть управление с соответствующим кодом завершения клиенту, который сам решит, как лучше поступить.

Однако, может возразить внимательный читатель, у драйвера одна процедура StartIo на все типы запросов. И в том случае, если ко всем будет применяться "прогон" через системную очередь, то вполне возможно, что одни запросы действительно не смогут быть обработаны в данный момент (это может установить, например, сама функция StartIo, "пообщавшись" с устройством), в то время как другие запросы могут быть обработаны вполне успешно. Вот если бы можно было выделить из очереди пакеты второго типа, оставляя напоследок пакеты первого типа! Возможно также, что к моменту завершения обработки пакетов более "удачного" типа, устройство станет доступно и для обработки первых, ранее бесперспективных. В данном случае процедуре StartIo (или другим процедурам, получившим управление по ее воле) придется пожертвовать текущим IRP пакетом, который следует завершить с кодом ошибки. Но далее процедура StartIo при помощи вызова IoStartNextPacketByKey может определить приоритет на извлечение из очереди IRP пакетов по определенному ключу и воплотить таким образом "экономичное" представление разработчика драйвера о работе с IRP пакетами и их сериализацией.

В коде данного драйвера часто встречаются фрагменты, когда указатель на текущий IRP пакет извлекается из поля CurrentIrp в структуре объекта устройства. Возникает вопрос: а всегда ли так можно поступать? Ведь это достаточно простой и удобный способ получения ссылки на IRP пакет в любом месте драйвера, которым так хочется пользоваться! Ответ: к сожалению, далеко не всегда и не везде. Диспетчер ввода/вывода устанавливает в поле CurrentIrp в структуре объекта устройства значение указателя на IRP пакет только для текущего пакета, который участвует в работе через системную очередь (System Queuing). Этот IRP пакет сейчас один из всех, которые возможно были занесены в эту очередь данным драйвером или, иными словами, данным объектом устройства, поступил на обработку в функцию StartIo и до окончания его обработки (с каким либо кодом завершения в одной из функций драйвера) другого адреса в поле CurrentIrp не появится. Соответственно, если разработчик так написал драйвер, что адрес такого IRP пакета где-то "оседает" (задерживается, например, в параллельно работающем системном потоке данного драйвера), то это может быть источником краха системы, поскольку обращение к области хранения полностью завершенных IRP пакетов совершенно некорректно.
//=========================================================================
// Процедура обслуживания прерывания:
//
BOOLEAN Isr (IN PKINTERRUPT pInterruptObject, IN PVOID pServiceContext )
{
	PDEVICE_EXTENSION pDevExt = (PDEVICE_EXTENSION) pServiceContext;
	PDEVICE_OBJECT    pDevObj = pDevExt->pDevice;
	#if DBG==1
		KIRQL         currentIrql = KeGetCurrentIrql();
		DbgPrint("LPTPORT: In Isr procedure, ISR_Irql=%d\n", currentIrql);
	#endif
	//===============================================================
	// Строго говоря,  следовало бы проверить, имело ли место
	// прерывание и наше ли это прерывание:
	// UCHAR status = ReadStatusRegister( pDevExt );
	// if( status & 0x04 ) return FALSE; // прерывания не было
	// Однако в силу упомянутых накладок с использованием бита SR.2,
	// не проверяем это вовсе, делая допущение, что если Isr получила
	// управление, то прерывание - наше.
	//===============================================================
	// Общей практикой является блокирование поступления прерываний
	// в этом месте:

	// WriteControlRegister( pDevExt, CR_DEFAULT);

	// Однако, мы не будем этого делать, чтобы не испортить данные,
	// находящиеся сейчас в Status Register. Полагаем, что кроме
	// нашего драйвера такие прерывания никто не генерирует, а драйвер
	// защищен тем, что Write запросы отвергаются до полного переноса
	// данных.
	//================================================================
	// Планируем вызов DPC процедуры для обработки прерывания позднее
	//
	IoRequestDpc( pDevObj,  // <- указание на объект устройства здесь
			        // однозначно определяет, какая процедура
				// DPC (здесь - DpcForIsr) будет вызвана
		      NULL,     // <- Arg1 in DpcForIsr
		      NULL );   // <- Arg2 in DpcForIsr

return TRUE;    // нормальное завершение обработки прерывания
}
//=========================================================================
// Код, который посылает в порт данные, вызывающие (при наличии
// CheckIt заглушки) сигнал прерывания:
//
VOID ForceInterrupt( PDEVICE_EXTENSION pDevExt, UCHAR bits )
{
	// Генерируем сигнал прерывания
	WriteControlRegister( pDevExt, bits | CR_INT_ENB | CR_DEFAULT );
	KeStallExecutionProcessor(50); // Удерживаем состояние 50 мкс
	// Импульс ACK#
	WriteControlRegister(
		pDevExt,
		bits | CR_INT_ENB | CR_NOT_RST | CR_DEFAULT  );
	KeStallExecutionProcessor(50); // Удерживаем состояние 50 мкс
	// Удерживая информационные биты, снимаем импульс ACK#
	WriteControlRegister( pDevExt, bits | CR_INT_ENB | CR_DEFAULT );
}
//=========================================================================
// Функция DoNextTransfer безопасно (от вмешательства кода прерывания, ISR
// функции) записывает данные в параллельный порт:
//
BOOLEAN DoNextTransfer ( IN PVOID pContext )
{

	PDEVICE_EXTENSION pDevExt  = (PDEVICE_EXTENSION)pContext;
	UCHAR             nextByte = 0x0F & (pDevExt->byteToBeOutToPort);

	#if DBG==1
		DbgPrint("LPTPORT: DoNextTransfer:\n");
		DbgPrint("LPTPORT: Sending 0x%02X to port %X\n",
		                   nextByte, pDevExt->portBase);
	#endif
	// Отправка полубайта данных.
	//= 1 ===============================================================
	// Заглушка CheckIt работает не самым простым образом.
	// Бит 0 отсылаемого полубайта нужно отправить как бит 0
	// в Data Register
	WriteDataRegister ( pDevExt, nextByte & 0x01);
	//
	// Это бит будет считан как бит 3 из Status Register

	//= 2 ===============================================================
	// Биты 1-3 отсылаемого полубайта нужно отправить как
	// биты 0, 1 и 3 в Control Register
	UCHAR bits = (nextByte & 0x8) + ((nextByte & 0x6)>> 1);
	// Таким образом бит 2 всегда равен 0

	bits ^= 0x3; // Инвертируем биты (0 & 1) перед
	         // записью в Control Register
	// Эти биты будут считаны в Status Register как биты 4,5 и 7
	#if DBG==1
		DbgPrint("LPTPORT:         generating next interrupt...\n");
	#endif
	// Собственно отправляем данные вместе с генерацией
	// сигнала прерывания:
	ForceInterrupt( pDevExt, bits );
return TRUE;
}
//=========================================================================
// Функция ReadDataSafely выполняет чтение данных из устройства
// без опасения быть прерванной кодом ISR функции:
//
BOOLEAN ReadDataSafely ( IN PVOID pContext )
{
	PDEVICE_EXTENSION pDevExt = (PDEVICE_EXTENSION)pContext;
	UCHAR             status  = ReadStatusRegister( pDevExt );

	// Преобазуем полученные через Status register биты
	// в понятную форму
	UCHAR readByte =
		((status & 0x8)<< 1) | ((status & 0x30)<< 1) | (status & 0x80);
	readByte >>= 4;
	#if DBG==1
		KIRQL currentIrql = KeGetCurrentIrql();
		DbgPrint("LPTPORT: ReadDataSafely, currentIrql=%d ReadStatus = %02X"
		         " ReadByte = %02X\n",     currentIrql,    status,    readByte );
		DbgPrint( "LPTPORT: \n");
	#endif
	if(pDevExt->xferCount < MAX_BUFFER_SIZE)
	{
	   // Помещаем принимаемые во внутренний буфер драйвера,
	   // только если его размер не исчерпан:
	   pDevExt->deviceInBuffer[pDevExt->xferCount++] = readByte;
	}
return TRUE;
}
//=========================================================================
// Функция TransferToUserSafely выполняет перенос данных из внутреннего
// буфера драйвера без опасения быть прерванной кодом ISR функции:
//
BOOLEAN TransferToUserSafely ( IN PVOID pContext )
{
	PDEVICE_EXTENSION pDevExt  = (PDEVICE_EXTENSION)pContext;
	ULONG             xferReq  = pDevExt->xferSize;
	PUCHAR            inBuffer = pDevExt->deviceInBuffer;

	#if DBG==1
		KIRQL currentIrql = KeGetCurrentIrql();
		DbgPrint("LPTPORT: TransferToUserSafely, currentIrql=%d\n",currentIrql );
		DbgPrint("         requested %d bytes, while ready %d bytes.\n",
		         xferReq,  pDevExt->xferCount);
	#endif

	if(  pDevExt->xferCount< 1  ||  xferReq< 1  )
	{	// Нет никаких полученных данных или нулевой запрос
		pDevExt->xferSize = 0;
		return FALSE;
	}
	if( xferReq > MAX_BUFFER_SIZE ) xferReq = MAX_BUFFER_SIZE;
	// Передаем не более данных, чем все, полученные из LPT порта,
	// оказавшиеся во внутреннем буфере драйвера:
	if( xferReq > pDevExt->xferCount ) xferReq = pDevExt->xferCount;

	// Собственно перенос запрошенных данных в буфер клиента:
	RtlCopyMemory( pDevExt->pUserBuffer, inBuffer, xferReq );

	if( xferReq < pDevExt->xferCount)
	{   // Перемещаем оставшиеся данные к началу буфера:
	    ULONG i=0,j=xferReq;
	    for(; j<pDevExt->xferCount; ) inBuffer[i++]=inBuffer[j++];
	}
	pDevExt->xferCount -= xferReq;
	pDevExt->xferSize   = xferReq;
	#if DBG==1
		DbgPrint("         Transferred %d, the rest %d bytes.\n",
		         xferReq, pDevExt->xferCount);
	#endif
return TRUE;
}
//=========================================================================
// Функция:    DpcForIsr
// Назначение: Данная функция начинает работу по "заказу" ISR функции
//             и выполняет ввод/вывод путем безопасного  вызова функций
//             ReadDataSafely и DoNextTransfer, то есть реализует
//             низкоуровневый ввод/вывод
// Аргументы:  Указатель на текущий DPC объект (не используется)
//             pDeferredContext - контекстный указатель - так
//             передается указатель на структуру объекта устройства
// Возвращаемое значение: нет
//
VOID
DpcForIsr( IN PKDPC pDpc,              // не используется
           IN PVOID pDeferredContext,  // Внимание! Теперь здесь
                                       // находится указатель pDevObj !
           IN PVOID pArg1,             // не используется
           IN PVOID pArg2              // не используется
           )
{
	PDEVICE_OBJECT     pDevObj   = (PDEVICE_OBJECT)pDeferredContext;
	PDEVICE_EXTENSION  pDevExt   =
				(PDEVICE_EXTENSION)pDevObj->DeviceExtension;
	PIRP               pIrp      = pDevObj->CurrentIrp;
	PIO_STACK_LOCATION pIrpStack = IoGetCurrentIrpStackLocation(pIrp);
	ULONG 	total =
		pIrpStack->Parameters.DeviceIoControl.InputBufferLength,
	        currentByteNumToWriteToPort =
		(ULONG)pIrpStack->Parameters.DeviceIoControl.Type3InputBuffer;

	#if DBG==1
		KIRQL currentIrql = KeGetCurrentIrql();
		DbgPrint("LPTPORT: We are now in DpcForIsr, currentIrql=%d xferCount = %d\n",
		                   currentIrql, pDevExt->xferCount );
	#endif

	if( currentByteNumToWriteToPort > 0 ) // <- т.е. уже были передачи
	{   // Безопасно читаем следующий байт из устройства:	
	    KeSynchronizeExecution( pDevExt->pIntObj,
		              ReadDataSafely,
		              pDevExt  );
	}
	if ( currentByteNumToWriteToPort < total )
	{  // Если остались данные, то записываем следующий байт	
	   // данных в порт и запускаем прерывание:
	   PUCHAR userBuffer = (PUCHAR)pIrp->AssociatedIrp.SystemBuffer;
	   pDevExt->byteToBeOutToPort =
				 userBuffer[currentByteNumToWriteToPort];
	   #if DBG==1
	   	DbgPrint("LPTPORT: currentByteNo = %d, byteToBeOutToPort=%02X(hex)\n",
		                   currentByteNumToWriteToPort, pDevExt->byteToBeOutToPort );
	   #endif
	   // Заранее корректируем номер следующего байта для записи в порт
	   // и сохраняем его там же:
	   pIrpStack->Parameters.DeviceIoControl.Type3InputBuffer =
				   (PVOID)( currentByteNumToWriteToPort+1 );
	   KeSynchronizeExecution( pDevExt->pIntObj,
				   DoNextTransfer,
				   pDevExt );
	}
	else
	{
	   #if DBG==1
	   	DbgPrint("LPTPORT: We are now in DpcForIsr, all data transmitted.\n");
	   #endif
	   if(pDevExt->pEvent!=NULL)
	   {
		// Устанавливаем объект события в сигнальное состояние,
		// что является сигналом клиету о полностью завершенном
		// переносе данных в параллельный порт
		KeSetEvent(pDevExt->pEvent, IO_NO_INCREMENT , FALSE );
		//^^^^^^^^^^^^^^^ Замечание А.
	   }
	   // Завершаем обработку текущего IRP пакета.
	   // Поскольку это была обработка пакета от Win32 вызова
	   // DeviceIoControl, который передавал данные в драйвер,
	   // а назад ничего не ожидал,
	   // то указываем, что число байт, возвращаемых клиенту
	   // (третий аргумент вызова CompleteIrp), равно 0:
	   PIRP pIrp = pDevObj->CurrentIrp;
	   CompleteIrp( pIrp, STATUS_SUCCESS, 0 );

	   // Сообщаем Диспетчеру ввода/вывода, что готовы обработать
	   // следующий пакет:
	   IoStartNextPacket( pDevObj, FALSE );
	}
return;
}
//=========================================================================
// Функция:     DeviceControlRoutine
// Назначение:  Обрабатывает запросы от Win32 вызова DeviceIoControl.
// Аргументы:   pDevObj - поступает от Диспетчера ввода/вывода,
//              pIrp - поступает от Диспетчера ввода/вывода.
// Возвращаемое значение:
//              NTSTATUS - в случае нормального завершения STATUS_SUCCESS
//                         или код ошибки STATUS_Xxx
//
NTSTATUS DeviceControlRoutine( IN PDEVICE_OBJECT pDevObj, IN PIRP pIrp )
{
	NTSTATUS           status = STATUS_SUCCESS;
	PIO_STACK_LOCATION pIrpStack=IoGetCurrentIrpStackLocation(pIrp);
	PDEVICE_EXTENSION  pDevExt =
			   (PDEVICE_EXTENSION)pDevObj->DeviceExtension;
	//-------------------------------
	#if DBG==1
		KIRQL currentIrql = KeGetCurrentIrql();
		DbgPrint("LPTPORT: We are now in DeviceControlRoutine, currentIrql=%d\n",
		                   currentIrql );
	#endif
	// Диспетчеризация по IOCTL кодам:
	switch( pIrpStack->Parameters.DeviceIoControl.IoControlCode )
	{
	case IOCTL_SEND_TO_PORT:
	{
	   // Размер данных, поступивших от клиента драйвера
	   // (см. таблицу 8.9):
	   ULONG xferSize =
		pIrpStack->Parameters.DeviceIoControl.InputBufferLength;
	   if( xferSize < 1 )
	   {    // Нет данных для отправки в порт:
		#if DBG==1
			DbgPrint("LPTPORT: DeviceControlRoutine: IOCTL_SEND_TO_PORT,\n",
			         "         no bytes to transfer.\n");
		#endif
		// Завершение без переноса данных клиенту:
	 	return CompleteIrp( pIrp, STATUS_SUCCESS, 0 );
	   }
	   // if( xferSize > MAX_BUFFER_SIZE ) ? Но размер входящих данных
	   // уже не актуален, поскольку будет использоваться буфер,
	   // поступивший от клиента (точнее, от Диспетчера ввода/вывода).
	   //
	   // Теперь мы ничего не копируем, а просто инициируем процесс
	   // программируемого вывода, откладывая пакет в очередь
	   // необработанных IRP пакетов (не забываем сообщить адрес
	   // процедуры CancelRoutine, которая должна получить управление,
	   // если клиент решит отозвать свой запрос):
	   #if DBG==1
	   	DbgPrint("LPTPORT: DeviceControlRoutine: IOCTL_SEND_TO_PORT,\n"
	   	         "         xfer size is %d Irp is pending.\n", xferSize );
	   #endif
	   IoMarkIrpPending( pIrp );
	   IoStartPacket( pDevObj, pIrp, 0, CancelRoutine);
	   return STATUS_PENDING;
	}
	case IOCTL_SEND_TO_USER:
	{	
	   // Размер данных, ожидаемых пользователем
	   pDevExt->xferSize =
		pIrpStack->Parameters.DeviceIoControl.OutputBufferLength;
	   if( pDevExt->xferSize> 0 )
	   {
		// Согласно таблице 8.9, адрес клиентского буфера при
		// методе METHOD_BUFFERED в IOCTL запросе находится там
		// же, что и при обработке обычного запроса Read (см.
		// первый вариант драйвера, функцию DispatchRead):
		pDevExt->pUserBuffer =
				(PUCHAR)pIrp->AssociatedIrp.SystemBuffer;
		// Пытаемся безопасно перенести данные:
		KeSynchronizeExecution( pDevExt->pIntObj,
		                        TransferToUserSafely,
		                        pDevExt    );
	   }
	   // Завершаем обработку IRP пакета:
	   #if DBG==1
	   	DbgPrint("LPTPORT: DeviceControlRoutine: IOCTL_SEND_TO_USER,\n"
	   	         "         %d bytes transferred to user.\n", pDevExt->xferSize);
	   #endif
	   return CompleteIrp( pIrp, STATUS_SUCCESS, pDevExt->xferSize);
	}
	case IOCTL_TAKE_EVENT:
	{
	   // Размер данных, поступивших от клиента драйвера
	   ULONG xferFromDriverSize =
		 pIrpStack->Parameters.DeviceIoControl.OutputBufferLength;
	   if( xferFromDriverSize < sizeof(HANDLE))
	   {
		#if DBG==1
			DbgPrint("LPTPORT: DeviceControlRoutine: IOCTL_TAKE_EVENT,\n         "
			         "event info can not be transferred due to 0 buffer size.\n");
		#endif
		status = STATUS_INVALID_PARAMETER;
		// Завершение без переноса данных клиенту:
	 	return CompleteIrp( pIrp, status, 0 ); // 0 - Нет переноса
	   }
	   if(pDevExt->pEvent==NULL) // Объект события еще не был создан
	   {                         // для данного клиента.
		// Создаем объект события типа
		// SynchronizationEvent, с автоматическим переходом в
		// несигнальное состояние:
		#define  EVENT_NAME L"\\BaseNamedObjects\\LPTPORT_EVENT"
		UNICODE_STRING eventName;
		RtlInitUnicodeString( &eventName, EVENT_NAME );

		HANDLE  hEvent;  // Объект события - без имени:
		PKEVENT pEvent =
		        IoCreateSynchronizationEvent( &eventName, &hEvent );
		if(pEvent==NULL)
		{
			// Объект события не был создан
			#if DBG==1
				DbgPrint("LPTPORT: DeviceControlRoutine: IOCTL_TAKE_EVENT,\n"
				         "         error - event wasn't created.\n");
			#endif
			// Завершение без переноса данных клиенту:
			return CompleteIrp( pIrp, STATUS_UNSUCCESSFUL, 0 );
		}
		#if DBG==1
			DbgPrint("LPTPORT: DeviceControlRoutine: IOCTL_TAKE_EVENT,\n"
			         "      event named %ws successfully created.\n",
			         eventName.Buffer);
		#endif
		pDevExt->pEvent = pEvent;
		pDevExt->hEvent = hEvent;
		// Предустанавливаем объект события в несигнальное состояние:
		KeClearEvent(pDevExt->pEvent);
	   }
	   // Cообщаем об объекте события клиенту - передаем дескриптор
	   RtlCopyMemory( pIrp->AssociatedIrp.SystemBuffer,
	                  &pDevExt->hEvent,
	                  sizeof(HANDLE) );
	   #if DBG==1
	   	DbgPrint( "LPTPORT: DeviceControlRoutine: IOCTL_TAKE_EVENT,\n"
		          "         event handle = %04X(hex) is sent to user.\n",
		          pDevExt->hEvent);
	   #endif
	   return CompleteIrp( pIrp, STATUS_SUCCESS, sizeof(HANDLE) );
	}
	case IOCTL_CLOSE_EVENT:
	{
	   if(pDevExt->pEvent!=NULL) // объект события был создан
	   {
		NTSTATUS sts = ZwClose(pDevExt->hEvent);
		#if DBG==1
			if(sts==STATUS_SUCCESS)
		    	DbgPrint("LPTPORT: DeviceControlRoutine: IOCTL_CLOSE_EVENT,\n"
		    	         "         event handle closed with STATUS_SUCCESS.\n");
			DbgPrint("LPTPORT: DeviceControlRoutine: IOCTL_CLOSE_EVENT,\n"
			         "         event (handle %04Xhex) closing status = %d.\n",
			         pDevExt->hEvent, sts );
		#endif
		// Во всяком случае, эти событием пользоваться не будем:
		pDevExt->pEvent = NULL;
		pDevExt->hEvent = NULL;
	   }
	   return CompleteIrp( pIrp, STATUS_SUCCESS, 0 );
	}
	default:
	{
		#if DBG==1
			DbgPrint("LPTPORT: DeviceControlRoutine: bad IOCTL code.");
		#endif
		// Завершение без переноса данных клиенту:
		status = STATUS_INVALID_DEVICE_REQUEST;
		CompleteIrp( pIrp, status, 0 );
	}
	} // <- конец оператора "switch"
return status;
}
//=================================================================================

Перед тем как перейти к анализу тестирующего приложения и результатов тестирования, детальнее рассмотрим код обработчика IOTCL запросов, то есть функции DeviceControlRoutine данного драйвера.

Драйвер обрабатывает четыре IOCTL кода, которые может задать клиент при своем обращении к драйверу (из приложения пользовательского режима это делается через Win32 вызов DeviceIoControl):

Если речь идет о взаимодействии приложения пользовательского режима с драйвером, то, как правило, событие создается именно в приложении пользовательского режима. Операционная система по Win32 вызову CreateEvent создает событие как объект режима ядра, возвращая приложению открытый дескриптор. Этот дескриптор передается в драйвер (через IOCTL запрос), а драйвер при помощи вызова ObReferenceObjectByHandle получает ссылку на созданный объект события по указателю на существующий объект (ссылка на объект является в режиме ядра более привычным способом обращения с событием, мьютексом и т.д.). Описание подобного способа совместного использования события в режиме ядра и пользовательском режиме несложно отыскать в литературе или в интернете, например, на Интернет сайте codeguru.com. Особенностью же данного драйвера является то, что событие изначально создается в драйвере (в режиме ядра), и лишь после этого соответствующий ему дескриптор передается клиенту. Для этого используется практически не применяемый разработчиками вызов IoCreateSynchronizationEvent (не применяемый по причине плохого описания в документации пакета DDK всех имеющихся на сегодня версий). Между тем этот удобный вызов, вся "особая" специфика использования которого состоит в правильном задании имени создаваемого объекта (это же относится и к вызову режима ядра IoCreateNotificationEvent), возвращает не только указатель на создаваемый объект, но и его дескриптор одновременно.

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

Данный драйвер (и первый, и второй варианты) в том виде как они приведены, не позволяют одновременно получить доступ к нему из нескольких приложений, поскольку объект устройства создается для эксклюзивного доступа. По этой причине Win32 вызов CreateFile, который должен получить дескриптор доступа к драйверу, завершается с ошибкой 5 (отказано в доступе). Для того чтобы исправить ситуацию необходимо предпоследний параметр вызова IoCreateDevice в драйверной функции CreateDevice задать равным FALSE (вместо TRUE, как указано изначально).

Еще один вопрос-опасение, который может возникнуть у внимательного читателя: как обстоят дела с доступом ко входным буферам с данными и для данных в смысле корректности уровня IRQL, на которых к ним обращается драйвер? Например, копирование в пользовательский буфер полученных данных по IOCTL запросу IOCTL_SEND_TO_USER производится функцией TransferToUserSafely, защищенной KeSynchronizeExecution, то есть работающей на уровне IRQL, равном IRQL процедуры обслуживания прерывания Isr. Почему не происходит сбоя, поскольку достоверно известно, что пользовательские области находятся в странично организованной памяти?

Ответ состоит в том, что в обоих драйверах используется буферизованный метод передачи данных при обращениях клиента к драйверу:

В обоих случаях указание на буферизацию приводит к тому, что между передачами клиент/драйвер Диспетчер ввода/вывода вставляет копирование в/из буферных областей, которые специально выделяются в нестранично организованной памяти, допускающей обращение к ней из кода, работающего на повышенных уровнях IRQL.