Windows核心编程学习笔记(21)--同步设备I/O与异步设备I/O2

Drecik学习经验分享

转载请注明出处:http://blog.csdn.net/drecik__/article/details/8186961


1. 异步设备I/O基础

异步设备I/O是指,当一个线程向设备发送一个异步I/O请求时,这个I/O被传送给设备驱动程序,后者负责完成实际的I/O惭怍。当驱动程序在等待设备响应的时候,应用程序的线程并没有因为要等待I/O请求完成而被挂起,线程会继续运行并执行其他有用的任务。

通俗点来说,如果是异步调用处理设备I/O的函数,该函数会立即返回,不用等待。

使用异步I/O的操作方式:

  1. 在CreateFile创建设备对象的时候在dwFlagsAndAttributes中指定FILE_FLAG_OVERLAPPED标志
  2. 在ReadFile和WriteFile中指定最后一个LPOVERLAPPED 参数

1. 介绍OVERLAPPED结构

typedef struct _OVERLAPPED {
	ULONG_PTR Internal;				// 返回出错误码;
	ULONG_PTR InternalHigh;			// 返回传输的字节;
	union {
		struct {
			DWORD Offset;			// 指定文件访问中应该从哪里开始访问文件数据;
			DWORD OffsetHigh;
		} DUMMYSTRUCTNAME;
		PVOID Pointer;
	} DUMMYUNIONNAME;

	HANDLE  hEvent;					// 用来通知请求完成;
} OVERLAPPED, *LPOVERLAPPED;
其中第一个参数Internal保存已处理的I/O请求的错误码,一旦发送异步I/O请求,设备驱动程序会立即将该参数设备STATUS_PENDING,表示没有错误,应用程序可以使用HasOverlappedIoCompleted宏来检查一个异步I/O操作是否已经完成。

2. 异步I/O的注意事项

我们需要注意以下几点:

1. 设备驱动程序不必以先入先出的方式处理队列中的I/O请求,即不一定是先调用I/O请求的就一定先完成

2. 检查错误的方式,使用异步调用ReadFile或WriteFile都会立即返回,返回为FALSE,GetLastError检查是ERROR_IO_PENDING说明I/O操作正在进行并且没发生错误,如果GetLastError返回其他值说明发送错误调用失败

3. 在异步操作请求完成之前,一定不能移动或销毁I/O请求使用的数据缓存和OVERLAPPED结构,每个I/O请求都必须分配和初始化一个OVERLAPPED结构

3. 取消队列中的设备I/O请求

我们肯呢个想要在设备驱动程序对一个已经在加入队列的设备I/O请求进行处理之前取消:

1. 调用CancelIo来取消给定句柄所标示的线程天极道队列中的所有I/O请求(除非该句柄具有与之相关联的I/O完成端口)

2. 关闭设备句柄来取消已经添加到队列中的所有I/O请求,不管它们是由哪个线程创建

3. 线程终止时候,系统会自动取消该线程发出的所有I/O请求,关联I/O完成端口的请求是例外

4. 使用CancelIoEx取消某个设备的某个异步I/O请求,有两个参数,第一个为设备句柄,第二个为指定的OVERLAPPED对象,用来指定取消哪个异步I/O请求,如果第二个参数为NULL,则取消该设备所有的I/O请求

2. 接收I/O请求完成通知

Windows提供4中不同方法来接收I/O请求已经完成的通知

1. 触发设备内核对象

当一个线程触发一个设备异步I/O请求时,设备内核对象就会设为未触发状态,当设备驱动程序完成了请求之后,会将设备内核对象设为触发状态。

所以线程可以通过WaitForSingleObject或WaitForMultipleObjects。

这种方法使用简单,但是缺点是每个内核对象每次只能处理一个异步I/O请求,否则的话不知道是哪个请求完成。

2. 触发事件内核对象

发送异步I/O请求,传入的OVERLAPPED结构最后一个成员hEvent用来标识一个事件内核对象,我们必须通过CreateEvent来创建你该对象。

当一个异步I/O请求完成的时候,设备驱动程序会检查hEvent成员是否为NULL,不为NULL则触发该事件,所以我们可以等待每个异步I/O请求的hEvent事件是否触发来判断是否完成请求。

多个异步设备I/O请求必须为每个请求创建不同的事件对象。

我们可以使用GetOverlappedResult来获得异步I/O的结果:

BOOL
GetOverlappedResult(
	HANDLE hFile,						// 设备句柄;
	LPOVERLAPPED lpOverlapped,			// 异步I/O请求时传入的OVERLAPPED结构体;
	LPDWORD lpNumberOfBytesTransferred,	// 传输的字节;
	 BOOL bWait							// 是否等待请求完成;
	);

我们也可以自己等待hEvent成员触发,然后自己检查OVERLAPPED的Internal来判断错误码,InternalHigh判断传输的字节。

为了略微提高性能,我们可以使用函数SetFileCompletionNotificationModes设置不触发第一种情况下所说的设备内核对象:

BOOL
SetFileCompletionNotificationModes(
	HANDLE FileHandle,	// 设备句柄;
	UCHAR Flags			// 标志,传入FILE_SKIP_SET_EVENT_ON_HANDLE;
	);

 

3. 可提醒I/O

首先需要知道的是,可提醒I/O设计的非常糟糕,所以应该避免使用!

每个线程创建的时候都会创建一个异步过程调用队列,当发出一个I/O请求的时候,我们可以告诉设备驱动程序在调用线程的APC队列中添加一项。

为了能够实现我们的调用ReadFileEx和WriteFileEx函数来完成异步I/O请求,该两个函数和原先函数相比,少了返回已经传输的字节数,多了最后一个LPOVERLAPPED_COMPLETION_ROUTINE参数,该参数是一个回调函数,函数形式如下:

VOID
(WINAPI *LPOVERLAPPED_COMPLETION_ROUTINE)(
	DWORD dwErrorCode,					// 错误码;
	DWORD dwNumberOfBytesTransfered,	// 实际传输的字节;
	LPOVERLAPPED lpOverlapped			// 异步请求时候的OVERLAPPED结构体;
	);

使用ReadFileEx和WriteFIleEx发出的请求会将回调函数地址传给设备驱动程序,当请求完成的时候,会向发出请求的线程的APC队列中添加一项。

顺便提下,可提醒I/O完成时不会触发OVERLAPPED的hEvent成员,所以可以占为己用。

当线程处于可提醒状态,系统会检查它的APC队列,对队列中的每一项,都会调用完成函数并传入参数。

当I/O请求完成时候,系统将他们会添加到APC队列中——并不会立即调用。线程为了对APC队列的项进行处理,必须将自己置为可提醒状态。Windows提供了6个函数可将线程置为可提醒状态:SleepEx,WaitForSingleObjectEx,WaitForMultipleObjectsEx,SingalObjectAndWait,GetQueuedCompletionStatusEx,MsgWaitForMultipleObjectsEx。前5个参数最后一个参数是个BOOL值,表示是否将线程置为可提醒状态,最后一个函数的最后一个参数必须使用标志MWMO_ALERTABLE来让线程进入可提醒状态。

当使用这些函数调用等待函数并设置可提醒状态时,如果APC队列有项,则他们并不会被挂起。

可提醒I/O的劣势:

  1. 可提醒必须创建一个回调函数,是代码变得复杂,因为我们不得不把换掉函数用到的信息放在全局变量中。
  2. 线程问题,发出I/O请求的线程必须同时对完成通知进行处理,伸缩性不是很好。

可提醒I/O的优点:

Windows提供函数QueueUserAPC手动向将一项添加到APC队列中:

DWORD
QueueUserAPC(
	PAPCFUNC pfnAPC,	// 回调函数;
	HANDLE hThread,		// 线程句柄;
	ULONG_PTR dwData	// 传入回调函数的参数;
	);

回调函数指针原型:

VOID
(NTAPI *PAPCFUNC)(
	ULONG_PTR Parameter	// QueueUserAPC的第3个参数;
	);

hThread标识的线程可以在另一个进程的地址空间中,如果是那样那么pfnAPC也必须在同一个进程的地址空间中。

QueueUserAPC可以用来强制让线程退出等待状态:

如果某一个线程正在等待,并且该线程是可提醒的,我们可以向它APC队列添加项来使它根据等待函数返回的内容来判断是否退出,处理APC队列的等待函数返回的是WAIT_IO_COMPLETION

 

4. I/O完成端口

完成端口是到目前为止伸缩性最好的处理I/O请求的方法,他可以让用户创建等待请求线程,然后在另外一个线程完成请求,适合多个并发操作的请求,例如套接字请求。

这里指介绍完成端口的使用方法:

1. 了解CreateIoCompletionPort函数:

HANDLE		// 返回创建的句柄;
CreateIoCompletionPort(
	HANDLE FileHandle,				// 设备句柄;
	HANDLE ExistingCompletionPort,	// 当前存在的完成端口句柄,用于与设备句柄关联;
	ULONG_PTR CompletionKey,		// 关键字,通常是一个结构体指针,标识I/O的请求,该指针会在结果中返回;
	DWORD NumberOfConcurrentThreads	// 允许并发线程数目,通常为0表示等于CPU数目;
	);
使用该函数最常用的是,先创建一个完成端口,然后与设备进行关联:
// 创建完成端口;
HANDLE hComplete = CreateIoCompletionPort( INVALID_HANDLE_VALUE, NULL, 0, 0 );

// 与设备句柄关联;
CreateIoCompletionPort( hFile, hComplete, dwKey, 0 );
也可以在创建的时候直接关联:
// 创建完成端口的时候直接关联;
HANDLE hComplete = CreateIoCompletionPort( hFile, NULL, dwKey, 0 );
如果我们的设备已经与完成端口关联,但是我们发出一个的请求,使它在完成时候不添加到I/O完成端口队列中,则我们需要设置发送请求的OVERLAPPED的hEvent成员:
// 创建OVERLAPPED对象,并让hEvent与按位1或起来;
OVERLAPPED ol = {0};
ol.hEvent = CreateEvent( NULL, TRUE, FALSE, NULL );
ol.hEvent = (HANDLE)((DWORD_PTR)ol.hEvent | 1 );

ReadFile(..., &ol);

// 关闭事件的时候不要忘了将低位清除;
CloseHandle( (HANDLE)((DWORD_PTR)ol.hEvent & ~1) );
另外提一个,如果关联了完成端口的设备,在发送同步请求的时候,请求完成Windows也会将结果放到完成队列中,为了略微提高性能,可能使用上面提到的SetFileCompletionNotificationModes函数,第一个参数传入设备句柄,第二个参数传入FILE_SKIP_COMPLETION_PORT_ON_SUCCESS告诉Windows不要将以同步方式完成的请求放到完成端口中。

2. 获得I/O请求的结果

通常处理I/O请求结果是在另外一个独立的工作线程,且该线程的数量最好是处理器数量的2倍左右。

完成端口将完成的I/O请求添加到I/O完成队列中,工作线程可以使用函数GetQueuedCompletionStatus来从完成端口获取到完成的I/O请求:

BOOL
GetQueuedCompletionStatus(
	HANDLE CompletionPort,				// 完成端口句柄;
	LPDWORD lpNumberOfBytesTransferred,	// 实际传送的字节数;
	PULONG_PTR lpCompletionKey,			// 设备句柄与完成端口关联的时候传入的关键字;
	LPOVERLAPPED *lpOverlapped,			// 发送I/O请求时候的OVERLAPPED结构体指针;
	DWORD dwMilliseconds				// 等待时间,可以为INFINITE;
	);
也可以使用函数GetQueuedCompletionStatusEx来同时获取多个I/O请求结果:
BOOL
GetQueuedCompletionStatusEx(
	HANDLE CompletionPort,						// 完成端口句柄;
	LPOVERLAPPED_ENTRY lpCompletionPortEntries,	// OVERLAPPED_ENTRY数组指针;
	ULONG ulCount,								// 上面数组的数量;
	PULONG ulNumEntriesRemoved,		// 实际获得的OVERLAPPED_ENTRY数组元素个数;
	DWORD dwMilliseconds,			// 等待时间;
	BOOL fAlertable					// 是否进入可提醒状态;
	);

// OVERLAPPED_ENTRY结构体;
typedef struct _OVERLAPPED_ENTRY {
	ULONG_PTR lpCompletionKey;		// 关键字;
	LPOVERLAPPED lpOverlapped;		// 返回发送I/O请求时候的OVERLAPPED结构体的指针;
	ULONG_PTR Internal;				// 没有明确定义,不应该使用;
	DWORD dwNumberOfBytesTransferred;	// 实际传输的字节数;
} OVERLAPPED_ENTRY, *LPOVERLAPPED_ENTRY;


3. 为了方便理解,我拿了以前一本Socket书上的基于完成端口的实现代码,可以去下载看看: http://download.csdn.net/detail/drecik__/4776851

你可能感兴趣的:(Windows核心编程学习笔记(21)--同步设备I/O与异步设备I/O2)