忆杰的博客

忆杰的博客

StartIo例程


凡是和多线程有关的东西, 一般就涉及非常恼火的线程同步问题, 而这个问题还不是那么直观, 不像其他一般的bug比较好调试, 有时候这个多线程的问题, 在一般情况下又不会有问题, 但是特殊请求下程序又会崩溃. 这种bug想要重现都有点问题.

 

前面说了内核下面几种同步的方法, 那些是对线程进行同步的, 但是很明显IRP有很多时候也是需要同步处理的, 这个有个专门的名词, 就是串行化.串行化能够保证各个并行的IRP能够按照顺序执行, 典型情况下, 对某个设备的操作的, 假如有很多个线程同时去操作这个设备, 那么必须将这些操作排队, 然后一一进行处理, 如果不做串行化操作就会变得混乱.
 

当一个新的Irp请求来临的时候首先检查设备是否处于"忙"状态, 如果处于忙状态, 那么将Irp插入一个队列中, 等待设备变为闲的时候从队列中取出一个IRP进行处理, 这个就是串行化了, 也就是StartIo. Windows内核中有专门支持这种操作的一系列结构和例程, 但是Windows提供的只有一个队列, 如果需要多个队列, 比如读一个队列写一个队列, 那么就要自己实现队列了. 这里先看看Windows搞的这个队列有多舒服.
 

另外必须要说的一点StartIo例程运行在DISPATCH_LEVEL级别上, 所以不会被线程调度打断, 但是在DISPATCH_LEVEL级别上也有很多限制, 要记住这一点. StartIo其他方面就类似于普通写的分发函数了, 只是没有返回值..分发函数如果想把当前Irp串行化, 就调用IoStartPacket函数, 就可以将Irp插入队列, 而且IoStartPacket函数还可以指定一个取消例程. IoStartPacket会判断设备处于忙或者空闲的状态, 如果处于空闲状态, 则提升当前IRQL到DISPATCH_LEVEL级别, 并进入StartIo例程, 串行处理Irp请求. 如果设备处于忙的状态, 则将Irp插入队列后返回.在StartIo例程结束前, 应该调用IoStartNextPacket函数, 其作用是从队列中取下一个Irp进行处理, 这样就链起来了.在调用StartIo前, 操作系统会将设备的Device->CurrentIrp设置为当前Irp. 这意味着这个Irp正准备由StartIo例程处理.
 

当处理StartIo例程的时候, 最复杂的莫过于对取消例程的处理, 准确使用Cancel自旋锁是关键, 在StartIo开始处, 应首先获得自旋锁, 然后判断当前的Irp是否是DeviceObject->CurrentIrp. 如果是说明这个Irp正在或者即将被StartIo处理, StartIo应该立即释放自旋锁, 什么也不做. 立刻退出. 不过我到现在也没有搞明白取消例程那点代码要那样子.我倒!
 

下面这个代码就是关于上面这段长篇大论的具体实现了. 我跑了几次发现这个StartIo和我想象中的处理方式不一样啊,首先将一个线程的发送的Irp一次处理完, 然后在处理另外一个线程的, 但是处理另外一个线程的时候我发现是一个一个慢慢处理, 但是前面那个线程的是一次不停的全部处理, 我倒. 到现在也没有看出来是什么原因.
 

凡是和多线程有关的东西, 一般就涉及非常恼火的线程同步问题, 而这个问题还不是那么直观, 不像其他一般的bug比较好调试, 有时候这个多线程的问题, 在一般情况下又不会有问题, 但是特殊请求下程序又会崩溃. 这种bug想要重现都有点问题.

 

前面说了内核下面几种同步的方法, 那些是对线程进行同步的, 但是很明显IRP有很多时候也是需要同步处理的, 这个有个专门的名词, 就是串行化.串行化能够保证各个并行的IRP能够按照顺序执行, 典型情况下, 对某个设备的操作的, 假如有很多个线程同时去操作这个设备, 那么必须将这些操作排队, 然后一一进行处理, 如果不做串行化操作就会变得混乱.
 

当一个新的Irp请求来临的时候首先检查设备是否处于"忙"状态, 如果处于忙状态, 那么将Irp插入一个队列中, 等待设备变为闲的时候从队列中取出一个IRP进行处理, 这个就是串行化了, 也就是StartIo. Windows内核中有专门支持这种操作的一系列结构和例程, 但是Windows提供的只有一个队列, 如果需要多个队列, 比如读一个队列写一个队列, 那么就要自己实现队列了. 这里先看看Windows搞的这个队列有多舒服.
 

另外必须要说的一点StartIo例程运行在DISPATCH_LEVEL级别上, 所以不会被线程调度打断, 但是在DISPATCH_LEVEL级别上也有很多限制, 要记住这一点. StartIo其他方面就类似于普通写的分发函数了, 只是没有返回值..分发函数如果想把当前Irp串行化, 就调用IoStartPacket函数, 就可以将Irp插入队列, 而且IoStartPacket函数还可以指定一个取消例程. IoStartPacket会判断设备处于忙或者空闲的状态, 如果处于空闲状态, 则提升当前IRQL到DISPATCH_LEVEL级别, 并进入StartIo例程, 串行处理Irp请求. 如果设备处于忙的状态, 则将Irp插入队列后返回.在StartIo例程结束前, 应该调用IoStartNextPacket函数, 其作用是从队列中取下一个Irp进行处理, 这样就链起来了.在调用StartIo前, 操作系统会将设备的Device->CurrentIrp设置为当前Irp. 这意味着这个Irp正准备由StartIo例程处理.
 

当处理StartIo例程的时候, 最复杂的莫过于对取消例程的处理, 准确使用Cancel自旋锁是关键, 在StartIo开始处, 应首先获得自旋锁, 然后判断当前的Irp是否是DeviceObject->CurrentIrp. 如果是说明这个Irp正在或者即将被StartIo处理, StartIo应该立即释放自旋锁, 什么也不做. 立刻退出. 不过我到现在也没有搞明白取消例程那点代码要那样子.我倒!
 

下面这个代码就是关于上面这段长篇大论的具体实现了. 我跑了几次发现这个StartIo和我想象中的处理方式不一样啊,首先将一个线程的发送的Irp一次处理完, 然后在处理另外一个线程的, 但是处理另外一个线程的时候我发现是一个一个慢慢处理, 但是前面那个线程的是一次不停的全部处理, 我倒. 到现在也没有看出来是什么原因.

 

这边是用户态的代码:

/*	Windows 内核下StartIo例程试验 3环代码
	编译方法参见makefile. TAB = 8
*/
#include <stdio.h>
#include <windows.h>

#pragma comment( linker, "/Entry:Jmain" )
#pragma comment( linker, "/subsystem:console" )

#define DEVICE_NAME "\\\\.\\SysLinkStartIo"
//===========================================================================
//线程过程, 向设备发送15次, 写入请求
//===========================================================================
DWORD __stdcall ThreadProc( PVOID pContext ) {
	UCHAR ucBuf[10];
	ULONG i, j;
	DWORD dwByteWrite;
	BOOL bRet;
	OVERLAPPED StOverlapped[15] = {0};
	HANDLE hEvent[15] = {0};

	__try {
		for( i = 0; i < sizeof( StOverlapped ) / sizeof( StOverlapped[0] ); i++ ) {

			StOverlapped[i].hEvent = CreateEvent( NULL, FALSE, TRUE, NULL );
			if ( !StOverlapped[i].hEvent ) {
				printf( "创建同步事件失败!\n" );
				return -1;
			}
		}

		for( i = 0; i < sizeof( StOverlapped ) / sizeof( StOverlapped[0] ); i++ ) {
			hEvent[i] = StOverlapped[i].hEvent;
		}

		Sleep( 1000 );
		for( i = 0; i < sizeof( StOverlapped ) / sizeof( StOverlapped[0] ); i++ ) {

			RtlFillMemory( ucBuf, sizeof( ucBuf ), i + 'a' );

			bRet = WriteFile( *( PHANDLE )pContext, ucBuf, sizeof( ucBuf ),
					  &dwByteWrite, &StOverlapped[i]  );

			if ( !bRet && GetLastError() != ERROR_IO_PENDING ) {
				printf( "写入设备失败!\n" );
				return -1;
			} else {
				for( j = 0; j < sizeof( ucBuf ); j++ ) {
					printf( "%c\t", ucBuf[j] );
				}

				printf( "\n" );
			}
		}

		//这个复杂度加了一些中间有可能还会有取消IRP的出现
		//CancelIo(*(PHANDLE)pContext);
		WaitForMultipleObjects( sizeof( StOverlapped ) / sizeof( StOverlapped[0] ),
					&hEvent[0], TRUE, INFINITE );

		printf( "设备处理完毕!\n" );

	} __finally {
		for( i = 0; i < sizeof( hEvent ) / sizeof( hEvent[0] ); i++ ) {
			if ( StOverlapped[i].hEvent ) {
				CloseHandle( StOverlapped[i].hEvent );
			}
		}
	}

	return 0;
}
//===========================================================================
int Jmain( ) {
	HANDLE hDevice = NULL;
	HANDLE hThead[2] = {0};
	DWORD dwTemp;

	do {
		hDevice = CreateFile( DEVICE_NAME, GENERIC_READ | GENERIC_WRITE, 0, NULL,
			OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED, NULL );

		if ( hDevice == INVALID_HANDLE_VALUE ) {
			printf( "打开设备失败\n" );
			break;
		}

		hThead[0] = CreateThread( NULL, 0, &ThreadProc, &hDevice, 0, &dwTemp );

		if ( !hThead[0] ) {
			printf( "创建线程1失败!\n" );
			break;
		} else {
			printf( "创建线程1成功, 线程1已经开始运行!\n" );
		}

		hThead[1] = CreateThread( NULL, 0, &ThreadProc, &hDevice, 0, &dwTemp );

		if ( !hThead[1] ) {
			printf( "创建线程2失败!\n" );
			break;
		} else {
			printf( "创建线程2成功, 线程2已经开始运行!\n" );
		}

		printf( "主线程开始等待两个线程返回\n" );

		WaitForMultipleObjects( 2, hThead, TRUE, INFINITE );
		printf( "两个线程都已经返回!\n" );

	} while ( FALSE );

//---------------------------------------------------------------------------
	if ( hDevice ) {
		CloseHandle( hDevice );
	}
	if ( hThead[0] ) {
		CloseHandle( hThead[0] );
	}
	if ( hThead[1] ) {
		CloseHandle( hThead[1] );
	}

	system( "pause" );

	return 0;
}

 

这边是内核态的:
 

/*	Windows 内核下StartIo例程试验 0环代码
	编译方法参见makefile. TAB = 8
*/

#include <ntddk.h>
#define DEVICE_NAME	L"\\Device\\StartIo"
#define SYS_LINK_NAME	L"\\??\\SysLinkStartIo"

typedef struct tagDeviceExt {
	PDEVICE_OBJECT pDeviceObj;
	UNICODE_STRING USzDeviceName;
	UNICODE_STRING USzSysLinkName;
} DEVICE_EXT, *PDEVICE_EXT;

//===========================================================================
//驱动卸载例程
//===========================================================================
#pragma code_seg( "PAGE" )
VOID DriverUnLoad ( PDRIVER_OBJECT pDriverObj ) {
	PDEVICE_EXT pDeviceExt = NULL;
	PDEVICE_OBJECT pNextDeviceObj = NULL;

	pNextDeviceObj = pDriverObj->DeviceObject;

	while ( pNextDeviceObj != NULL ) {
		pDeviceExt = pNextDeviceObj->DeviceExtension;

		IoDeleteDevice( pDeviceExt->pDeviceObj );
		IoDeleteSymbolicLink( &pDeviceExt->USzSysLinkName );

		KdPrint( ( "删除%wZ设备成功!\n", &pDeviceExt->USzDeviceName ) );

		pNextDeviceObj = pNextDeviceObj->NextDevice;
	}
}
//===========================================================================
//所有不关心的IRP处理例程
//===========================================================================
NTSTATUS DispatchRoutine( PDEVICE_OBJECT pDeviceObj, PIRP pIrp ) {

	pIrp->IoStatus.Information = 0;
	pIrp->IoStatus.Status = STATUS_SUCCESS;

	IoCompleteRequest( pIrp, IO_NO_INCREMENT );

	return STATUS_SUCCESS;
}
//===========================================================================
//取消IRP例程(这里为什么要降低IRQL没有搞明白)
//===========================================================================
VOID OnCancelIrp( PDEVICE_OBJECT pDeviceObj, PIRP pIrp ) {
	KIRQL OldIrql;

//	_asm int 3;
	//当前IRP正在被StartIo处理,
	if ( pIrp == pDeviceObj->CurrentIrp ) {

		//这里为何要获取原来的IRQL呢?
		OldIrql = pIrp->CancelIrql;

		//释放自旋锁, 这个倒是明白的. 必须要释放
		IoReleaseCancelSpinLock( pIrp->CancelIrql );

		//继续下一个IRP(这里是TRUE表示可以取消该IRP )
		IoStartNextPacket( pDeviceObj, TRUE );

		//降低回原来的特权级
		KeLowerIrql( OldIrql );

		KdPrint(( "当前IRP正在被取消!\n" ));
//---------------------------------------------------------------------------
	} else {
		//从设备队列中将该IRP抽取出来(放弃该IRP)
		KeRemoveEntryDeviceQueue( &pDeviceObj->DeviceQueue,
					  &pIrp->Tail.Overlay.DeviceQueueEntry );

		//释放自旋锁, 这个倒是明白的. 必须要释放
		IoReleaseCancelSpinLock( pIrp->CancelIrql );

		KdPrint(( "当前IRP没有被StartIo处理, 将被取消!\n" ));
	}
//---------------------------------------------------------------------------
	//将该IRP改为取消
	pIrp->IoStatus.Information = 0;
	pIrp->IoStatus.Status = STATUS_CANCELLED;
	IoCompleteRequest( pIrp, IO_NO_INCREMENT );
}
//===========================================================================
//写入请求
//===========================================================================
NTSTATUS DispatchWrite( PDEVICE_OBJECT pDeviceObj, PIRP pIrp ) {

//	_asm int 3;
	PAGED_CODE();

	//将IRP设置为挂起
	IoMarkIrpPending( pIrp );

	//将IRP插入系统的队列(这里要设置取消例程)
	IoStartPacket( pDeviceObj, pIrp, 0, OnCancelIrp );

	KdPrint( ( "写入请求处理, 挂起, 串行化, 返回Pending状态!\n" ) );

	//返回pending状态
	return STATUS_PENDING;
}

//===========================================================================
//StartIo例程, 串行化 IRQL = DISPATCH_LEVEL
//===========================================================================
#pragma code_seg( )
VOID StartIoRoutine( PDEVICE_OBJECT pDeviceObj, PIRP pIrp ) {
	KIRQL OldIrql;
	KEVENT Event;
	LARGE_INTEGER liTimeOut;
	PIO_STACK_LOCATION Stack = NULL;
	ULONG ulWriteLen, i;

//	_asm int 3;
	PAGED_CODE_LOCKED();

	//获取自旋锁
	IoAcquireCancelSpinLock( &OldIrql );

	//如果当前StartIo有正在处理的IRP. 或者正在被取消
	if ( pIrp != pDeviceObj->CurrentIrp || pIrp->Cancel ) {

		IoReleaseCancelSpinLock( OldIrql );
		KdPrint( ( "离开StartIo例程, 有正在被处理的IRP" ) );
		return;
//---------------------------------------------------------------------------
	} else {
		//这里准备开始处理IRP里, 所以这里我们取消掉原来
		//设置的取消例程
		IoSetCancelRoutine( pIrp, NULL );

		IoReleaseCancelSpinLock( OldIrql );
	}
//---------------------------------------------------------------------------
	//这里真正开始处理IRP了
//---------------------------------------------------------------------------
	KeInitializeEvent( &Event, NotificationEvent, FALSE );

	Stack = IoGetCurrentIrpStackLocation(pIrp);
	ulWriteLen = Stack->Parameters.Write.Length;

	for( i = 0; i < ulWriteLen; i++ ) {
		KdPrint(( "%c\t", *((UCHAR*)pIrp->AssociatedIrp.SystemBuffer + i) ));
	}
	KdPrint(( "\n" ));

	//3秒
	liTimeOut.QuadPart = -3 * 1000 * 1000 * 10;
	//这里模拟这个IRP处理需要3秒钟
	KeWaitForSingleObject( &Event, Executive, KernelMode, FALSE, &liTimeOut );

	pIrp->IoStatus.Information = 0;
	pIrp->IoStatus.Status = STATUS_SUCCESS;
	IoCompleteRequest( pIrp, IO_NO_INCREMENT );

	KdPrint( ( "离开StartIoRoutine例程, 处理完毕!\n" ) );

	//在队列中获取下一个IRP, 再次进入StartIo
	IoStartNextPacket( pDeviceObj, TRUE );
}
//===========================================================================
//驱动入口
//===========================================================================
#pragma code_seg( "INIT" )
NTSTATUS DriverEntry( PDRIVER_OBJECT pDriverObj, PUNICODE_STRING pUSzRegPath ) {
	ULONG i;
	NTSTATUS Status;
	PDEVICE_EXT pDeviceExt = NULL;
	PDEVICE_OBJECT pDeviceObj = NULL;
	UNICODE_STRING USzDeviceName = RTL_CONSTANT_STRING( DEVICE_NAME );
	UNICODE_STRING USzSysLinkName = RTL_CONSTANT_STRING( SYS_LINK_NAME );

	Status = IoCreateDevice( pDriverObj, sizeof( DEVICE_EXT ), &USzDeviceName,
				 FILE_DEVICE_UNKNOWN, 0, TRUE, &pDeviceObj );
	if ( !NT_SUCCESS( Status ) ) {
		KdPrint( ( "创建设备失败!\n" ) );
	}

	Status = IoCreateSymbolicLink( &USzSysLinkName, &USzDeviceName );
	if ( !NT_SUCCESS( Status ) ) {
		KdPrint( ( "创建符号链接失败!\n" ) );
	}

	//设置设备属性和设备扩展
	pDeviceObj->Flags |= DO_BUFFERED_IO;
	pDeviceExt = pDeviceObj->DeviceExtension;
	pDeviceExt->pDeviceObj = pDeviceObj;
	pDeviceExt->USzDeviceName = USzDeviceName;
	pDeviceExt->USzSysLinkName = USzSysLinkName;

	//设置分发函数例程, 特别设置IRP_MJ_WRITE, 和StartIo例程
	for( i = 0; i < IRP_MJ_MAXIMUM_FUNCTION; i++ ) {
		pDriverObj->MajorFunction[i] = &DispatchRoutine;
	}
	pDriverObj->MajorFunction[IRP_MJ_WRITE] = &DispatchWrite;
	pDriverObj->DriverStartIo = &StartIoRoutine;
	pDriverObj->DriverUnload = &DriverUnLoad;

	return Status;

}

 

 

 

 

 

 

发表评论

您必须登录 才能进行评论。