您的位置:首页 > 编程语言

Windows服务器端编程-第二章 设备IO与线程间通信-2-异步设备I/O操作基础

2007-07-31 10:51 357 查看
[第一节]打开和关闭设备


[第二节]使用文件设备


[第三节]进行设备I/O操作


[第四节]异步设备I/O操作基础

相比较计算机的其他操作而言,设备I/O操作是最慢和最不可预知的操作之一。CPU处理起数学运算和屏幕绘制事件来要比从文件或网络中读取及写入数据要快得多,而使用异步设备I/O能够更好的使用资源,因此可以创建更高效的应用程序。

设想线程准备向设备提交异步I/O请求。该I/O请求被传递给设备驱动,设备驱动负责执行实际的I/O操作。在设备驱动等待设备做出反应之前,线程并不会被一直挂起直到I/O请求完成,而是继续运行,进行其他有用的工作。

在这个场景中,设备驱动完成I/O请求队列的处理,并且必须通知应用程序数据已经被发送,数据被接收,或者产生了一个错误。在下一节[接收I/O请求已完成的通知]将会了解到设备驱动是如何通过I/O完成端口来通知应用程序的。现在,让我们着眼于如何产生异步I/O请求队列。产生异步I/O请求队列是设计高性能,可伸缩应用程序的精髓,本章剩余部分都将围绕这一个主题。

要异步访问一个设备,首先要调用CreateFile来打开它,并指定dwFlagsAndAttrs参数为FILE_FLAG_OVERLAPPED。该标志通知系统你需要异步访问该设备。针对设备驱动产生I/O请求队列,可以使用ReadFile和WriteFile函数,它们已经在[进行同步设备I/O中提及。方便起见,下面再次给出它们原型:(略)

当这两个函数中的任一个被调用时,函数会检查由hFile参数标识的设备是否使用FILE_FLAG_OVERLAPPED标志来打开。如果该标志被指定,函数将执行异步设备I/O操作。顺便说一下,当使用上面两个函数中的任一个进行异步I/O时,可以(并且通常)将pdwNumBytes参数设为NULL。必竟,你期望这些函数在I/O请求被处理完成之前能够返回,因此,此时检查传输的字节数是没有意义的(传输还没有开始或完成)。

l OVERLAPPED的结构
当进行异步设备I/O时,必须通过pOverlapped参数传递一个已经初始化过的OVERLAPPED结构的地址。在这里,"overlapped"意味着执行I/O请求操作的时间与线程执行其他任务的时间重叠。OVERLAPPED结构看起来像下面:


typedef struct _OVERLAPPED {
DWORD Internal; // [out] Error code
DWORD InternalHigh; // [out] Number of bytes transferred
DWORD Offset; // [in] Low 32-bit file offset
DWORD OffsetHigh; // [in] High 32-bit file offset
HANDLE hEvent; // [in] Event handle or data
} OVERLAPPED, *LPOVERLAPPED;


结构包含5个成员。其中的三个:Offset, OffsetHigh,以及hEvent,必须在调用ReadFile或WriteFile之前被初始化。其余两个:Internal和InternalHigh,由设备驱动设置,并可以在I/O操作完成后进行检查。以下是关于这些成员变量的更详细描述:
l Offset和OffsetHigh
当正在访问一个文件时,这两个成员指出I/O操作开始的64位偏移。回想一下,每个文件内核对象都有一个文件指针与其关联。当发出异步I/O请求时,系统会从这个文件指针所指定的位置开始访问文件。完成操作后,系统自动更新文件指针,这样下一个操作将从上一个操作结束的地方开始。进行异步I/O操作时,文件指针被系统忽略。想像一下,如果代码中包含两个对于ReadFile的异步调用(针对同一上文件内核对象),将会发生什么情况?在这种情况下,系统将不知道第二个ReadFile该从哪里开始读取。或许你可能想从不同于第一个ReadFile的位置开始读取。也可能你想要第二个ReadFile从紧接着第一个ReadFile的最后一个字节开始。为了避免对同一对象的多重异步调用的冲突,所有的异步I/O请求都必须在OVERLAPPED结构指定文件的起始偏移。


注意,非文件设备并不会忽略Offset和OffsetHigh参数,它们必须被初始化为0,否则I/O请求将失败,并且GetLastError将返回ERROR_INVALID_PARAMETER。


l hEvent
四个接收I/O完成通知的方法将会使用这个成员。在使用警告式(alertable)I/O通知方法时,该成员可用于你自己的意图。我知道许多开发员使用hEvent来保存某个C++对象的地址。(这个成员将在[触发事件内核对象]一节讨论。)

l Internal
该成员用于保存已处理的I/O请求的错误代码。一旦你发出一个异步I/O请求,设备驱动会设置Internal为STATUS_PENDING,表示现在没有错误发生,因为实际的操作此时还没有开始。事实上,使用在WindBase.H中定义的HasOverlappedIoCompleted宏,可以检测出异步I/O操作是否已经完成。如果I/O请求被阻塞,该宏将返回FALSE;如果已经完成,会返回TRUE。下面是该宏的定义:


#define HasOverlappedIoCompleted(pOverlapped) /
((pOverlapped)->Internal != STATUS_PENDING)



l InternalHigh
当异步I/O请求完成时,该成员指出有多少字节被传输。在开始定义OVERLAPPED结构时,微软决定不将Internal和InternalHigh公储文档(通过名字可以看出)。随着时间的推移,微软意识到这两个成员所包含的信息对开发人员很有用,所以将其文档化。但是,微软并没有修改它们的名字,因为操作系统的源代码中频繁的引用到它们,而微软又不想修改代码。


注意

当异步I/O请求完成时,将会接收到OVERLAPPED结构的地址指针,该结构就是在初始化请求时使用的那一个。拥有围绕OVERLAPPED结构所传递的上下文信息通常是很有用的,例如:在OVERLAPPED结构中存储用于初始化I/O请求的设备句柄。OVERLAPPED结构没有提供存储设备句柄的成员变量或其他用来存储上下文的本质上非常有用的成员,不过这个问题很容易解决。

我创建一个继承自OVERLAPPED结构的C++类。该类能够包含任何我想要的附加信息。当我的应用程序接收到OVERLAPPED结构的地址指针时,只需简单的将该指针映射(CAST)为该类即可。这样我的应用程序就可以访问OVERLAPPED的成员变量和需要的任何附加上下文信息。本章结尾的FileCopy示例应用程序使用了这一技巧,请查看该示例中的CIOReq类以了解更多细节。


l 异步设备I/O的告诫

你必须意识到进行异步I/O的几个要点。首先,设备驱动没有必要以FIFO的方式处理I/O请求队列。例如,某一线程如果执行以下代码,设备驱动很可能会先写入文件,然后从中读取:


OVERLAPPED o1 = { 0 };
OVERLAPPED o2 = { 0 };
BYTE bBuffer[100];
ReadFile (hfile, bBuffer, 100, NULL, &o1);
WriteFile(hfile, bBuffer, 100, NULL, &o2);


典型的情况是,如果能够有助于提高性能,设备驱动可能会以乱序执行I/O请求。例如,为了减少磁头移动和寻道时间,文件系统驱动可能扫描I/O请求队列,查找靠近硬盘物理位置的请求。

第二条,应该以正确的方法进行错误检查。大多数的Windows函数返回FALSE来指出错误,以非0值来表示成功。但是,ReadFile和WriteFile函数的行为有点不同,示例程序可能会有助于理解。

在试图产生异步I/O请求队列时,设备驱动可能会选择以同步的方法来处理请求。这发生于从文件中读取并且系统检查需要的数据是否已经存在于系统缓存的时候。如果在系统缓存中有数据,不会产生针对设备驱动的I/O请求队列驱动;相反,系统将数据从系统缓存中拷贝到你的本地缓存,完成I/O操作。

ReadFile和WriteFile在进行同步I/O操作时会返回非零值。如果所请求的I/O是以异步方式执行的,或者在调用ReadFile或WriteFile过程中发生错误,返回FALSE。如果返回FALSE,必须调用GetLastError进行检查。如果GetLastError返回ERROR_IO_PENDING,表明I/O请求被成功排队,将在稍后完成。如果GetLastError返回ERROR_IO_PENDING以外的其他值,表明该I/O请求不能排队给设备驱动。以下是I/O请求不能排队给设备驱动时GetLastError通常会返回的值:

l ERROR_INVALID_USER_BUFFER或ERROR_NOT_ENOUGH_MEMORY
每个设备驱动在非分页内存池中维护一个固定尺寸的列表,保存每个I/O请求。如果列表已满,系统将不能对请求进行排队,ReadFile和WriteFile会返回FALSE,GetLastError会返回以上两个值之一(依赖于驱动)。

l ERROR_NOT_ENOUGH_QUOTA
特定的设备会要求对本地缓存的存储器进行页面锁定,防止本地缓存被对换出内存。对存储器的页面锁定在使用FILE_FLAG_NO_BUFFERING标志进行文件I/O时一定会发生。但是,系统限制了单一进程能够进行页面锁定的存储器数量。如果ReadFile和WriteFile无法对本地缓存的存储器进行页面锁定,将返回FALSE,GetLastError会返回ERROR_NOT_ENOUGH_QUOTA。可以调用SetProcessWorkingSetSize来增加进程的可用配额。

应该怎样处理这些错误?基本上,当一定数量的独立I/O请求仍没有完成时,会产生这些错误。因此需要允许一些I/O请求被阻塞,然后在稍候重新调用ReadFile和WriteFile来产生请求。

第三个要点是本地数据缓冲以及用于产生异步I/O请求的OVERLAPPED结构不能被移动或销毁,直到I/O操作完成。当将I/O请求排队到设备驱动时,本地缓冲和OVERLAPPED结构的地址也被传递给驱动。注意,只有地址被传递,而不是数据区块。原因很明显:内存拷贝的代价昂贵,会消耗大量CPU时间。

当设备驱动准备开始处理排队的请求时,它通过pvBuffer传送本地数据缓冲的地址引用,通过由pOverlapped指针所指向OVERLAPPED结构中的表示文件偏移的成员变量以及其他成员来访问文件。特别地,设备驱动将I/O错误代码填充到Internal成员中,将传输的字节数填充到InternalHigh成员中。


注意
本地缓冲绝对不能被移动或销毁,除非I/O请求已经被处理完毕,内存也不能被破坏。另外,必须为每一个I/O请求创建一个OVERLAPPED结构并初始化之。


上面的注意非常重要,也是开发人员在实现异步设备I/O架构时最常见的BUG之一。以下是一个会产生这样BUG

的例子:


VOID ReadData(HANDLE hfile) {
OVERLAPPED o = { 0 };
BYTE b[100];
ReadFile(hfile, b, 100, NULL, &o);
}


这段代码看来无害,并且正确调用了ReadFile。仅有的问题该函数在对异步I/O请求进行排队后返回。从函数中返回会导致本地缓冲和OVERLAPPED结构从堆栈中被释放,而设备驱动并不会意识到ReadData已经返回了。

设备驱动仍然拥有指定该线程缓冲的两个内存指针。当I/O操作完成,设备驱动将修改线程堆栈中的内存,此时该内存区域中的内容会被破坏。由于修改内存的操作是异步的,所以该BUG将难于发现。有时设备驱动会同步执行I/O操作,这样BUG就不会被发现。有时I/O操作可能在函数返回后完成,有可能超过1小时之后,那时有谁知道这段堆栈正在被谁使用?

l 取消设备队列中I/O请求

有时可能需要在已经加入队列的I/O请求被设备驱动处理之前的取消它。Windows提供了有限的几个方法来这样做:可以调用CancelIo,传入指定的句柄,来取消调用线程已经排队的I/O请求。


BOOL CancelIo(HANDLE hfile);


关闭设备句柄可以取消所有已加入到队列中的I/O请求,无论该队列属于哪个线程。当一个线程终止,系统自动取消所有该线程产生的I/O请求。

可以看出,没有办法取消单一的,特定的I/O请求。


注意

被取消的I/O请求会以错误码ERROR_OPERATION_ABORTED完成。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐