本章回 答了如下几个问题:
◆ 什么是Overlapped I/O?为什么需要Overlapped I/O?如何让数据传输支持Overlapped I/O?
◆ 数据传输结束后,Win32提供了哪些方式对用户进行通告,以便进行适当的善后?
◆ 影响线程优先级的因素有哪些?如何获取或设置进程线程优先级?优先级的改变容易带来哪些问题?又该如何应对?
◆ 什么是被激发的文件句柄?什么是被激发的事件?什么是异步进程调用(APCs)?这些方式各是如何实现Overlapped I/O的?各有何优缺点?
◆ 使用Overlapped I/O的初衷是使“受制于I/O的程序”中获得高效率。但是否是各种情况下Overlapped I/O都能提高系统效率吗?
◆ 什么是I/O completion port?这个机制是怎么工作的?有什么优点?为什么说此方式能够很好地支持scalable(可升级的)系统?而对于工作于此模式下的文件,从提高系统 效率考虑,怎样才能避免无谓的completion packet通告呢?
什么是Overlapped I/O?
Overlapped I/O,常被设计为多线程处理,以便在一个“受制于I/O的程序”中获得高效率。
利用Win32所谓的overlapped I/O特性,可以让I/O操作并行处理,并且当一个I/O完成时,程序会收到一个通告。有些系统把这个特性称为非阻塞I/O,或异步I/O。
overlapped I/O是Win32的一项技术,你可以要求操作系统为你传送数据,并且传送完毕后通知你。这项技术使你的程序在I/O进行中仍然能够继续处理事务。事实 上,操作系统内部正是以线程来完成overlapped I/O。这样,你可以获得线程的所有利益,而不需付出痛苦代价。
在Windows 95环境下,Overlapped I/O使用有些限制。它不支持磁盘或光盘中的文件操作。
Win32文件操作函数
HANDLE CreateFile(
LPCTSTR lpFileName, // pointer to name of the file
DWORD dwDesiredAccess, // access (read-write) mode
DWORD dwShareMode, // share mode
LPSECURITY_ATTRIBUTES lpSecurityAttributes,// pointer to security attributes
DWORD dwCreationDisposition, // how to create
DWORD dwFlagsAndAttributes, // file attributes
HANDLE hTemplateFile // handle to file with attributes to copy
);
BOOL ReadFile(
HANDLE hFile, // handle of file to read
LPVOID lpBuffer, // pointer to buffer that receives data
DWORD nNumberOfBytesToRead, // number of bytes to read
LPDWORD lpNumberOfBytesRead, // pointer to number of bytes read
LPOVERLAPPED lpOverlapped // pointer to structure for data
);
BOOL WriteFile(
HANDLE hFile, // handle to file to write to
LPCVOID lpBuffer, // pointer to data to write to file
DWORD nNumberOfBytesToWrite, // number of bytes to write
LPDWORD lpNumberOfBytesWritten, // pointer to number of bytes written
LPOVERLAPPED lpOverlapped // pointer to structure for overlapped I/O
);
BOOL CloseHandle(
HANDLE hObject // handle to object to close
);
一些说明:
CreateFile()可处理的对象包括:files、pipes、mailslots、communication resource、disk device(Windows NT only)、consoles、directories(open only)。
如果要使用异步I/O,CreateFile()时设置dwFlagsAndAttributes含FILE_FLAG_OVERLAPPED 标 志,通知操作系统对此文件的访问采取异步I/O模式。
由于异步I/O模式下,可以在同一时间读(或写)文件的许多部分,所以没有“目前的文件位置”这一概念。每次读写都必须包含其文件位置。
OVERLAPPED 结构
typedef struct _OVERLAPPED {
DWORD Internal;
DWORD InternalHigh;
DWORD Offset;
DWORD OffsetHigh;
HANDLE hEvent;
} OVERLAPPED ;
说明:
Internal
通常它被保留。然而,当GetOverlappedResult()返回FALSE,GetLastError()返回ERROR_IO_PENDING 时,这个栏位将内含一个视系统而定的状态。
InternalHigh
通常它被保留。然而,当GetOverlappedResult()返回TRUE时,这个栏位将内含“被传输的数据长度”。
Offset
定义文件开始读(或写)的开始位置,以字节为单位。该偏离位置从文件头开始起算。如果目标设备不支持文件位置(比如管道),此栏忽略。
OffsetHigh
64位的文件偏离量,较高的32位。如果目标设备不支持文件位置(比如管道),此栏忽略。
hEvent
一个手动事件。当overlapped I/O完成后即被激发。ReadFileEx()和WriteFileEx()忽略这个栏位,彼时它可能用来传输一个用户自定义的指针。
注意: OVERLAPPED 结 构的生命周期应超越ReadFile()和WriteFile()函数。
被激发的File Handles
注意,此方式在Windows95/98下不被支持。详看MSDN中关于GetOverlappedResult()函数的说明。
在Windows NT下,最简单的overlapped I/O类型,是使用它自己的文件句柄作为同步机制。大致流程如下:
-
调用CreateFile(),设置dwFlagsAndAttributes含FILE_FLAG_ OVERLAPPED 标志,通知操作系统对此文件的访问采取异 步I/O模式;
-
建立一个 OVERLAPPED 结构,其中包含I/O请求所需参数;
-
调用ReadFile()或WriteFile(),最后一个参数为 OVERLAPPED 结构为参数。此时Win32会在后台处理 你的请求;
-
使用wait…()函数,并将该文件的文件句柄作为参数,等待操作结束。文件句柄是核心对象,一旦操作完成即被激发;
-
使用GetOverlappedResult()函数,查询操作结果。此时你获得的结果和调用ReadFile()、WriteFile()而没指定 overlapped I/O所传回的东西一样。
-
文件使用完毕,CloseHandle()。
BOOL GetOverlappedResult(
HANDLE hFile, // handle to file, pipe, or comm device
LPOVERLAPPED lpOverlapped, // pointer to overlapped structure
LPDWORD lpNumberOfBytesTransferred, // pointer to actual bytes count
BOOL bWait // wait flag
);
说明:如果bWait设置为FALSE,如果overlapped I/O还没完成,函数返回FALSE,调用GetLastError()会返回ERROR_IO_INCOMPLETE。所以,调用 GetOverlappedResult()时如果bWait设置为FALSE,可即时查询overlapped I/O的状态。
举例:
hFile = CreateFile( szPath,
GENERIC_READ,
FILE_SHARE_READ|FILE_SHARE_WRITE,
NULL,
OPEN_EXISTING,
FILE_FLAG_OVERLAPPED ,
NULL);
if (hFile == INVALID_HANDLE_VALUE)
{
return -1;
}
// Initialize the OVERLAPPED structure
memset(&overlap, 0, sizeof(overlap));
overlap.Offset = 1500;
// Request the data
rc = ReadFile( hFile,buf,READ_SIZE,&numread,&overlap);
if (rc)
{
// The data was read successfully
}
else
{
if (GetLastError() == ERROR_IO_PENDING)
{
VERIFY(WAIT_OBJECT_0 == WaitForSingleObject(hFile, INFINITE));
rc = GetOverlappedResult(hFile, &overlap,&numread,FALSE);
}
else
{
// Something went wrong
}
}
CloseHandle(hFile);
说明:
-
虽然你要求一个 overlapped 操 作,但如果数据已被放入cache中,或操作系统认为它可以快速地取得那份你所要求的数据,那么文件操作会在ReadFile()返回前完成。此时 ReadFile()返回TRUE。
-
如果你要求一个文件操作为 overlapped 操作,而操作系统把这个“操作请求”放到队列中等待执行,那么ReadFile()或WriteFile() 都会传回FALSE以示失败。但函数返回FALSE有多种原因,可调用GetLastError()调查原因,如果是ERROR_IO_PENDING, 那说明“操作请求”被放入队列等待执行;其它值,那可能真正代表一个错误了。
-
OVERLAPPED 结 构中包含了“这个操作从哪里开始的信息”,可以处理64位的偏移量。
-
WaitForSingleObject(hFile, INFINITE); rc = GetOverlappedResult(hFile, &overlap,&numread,FALSE);其实可合并为一句:rc = GetOverlappedResult(hFile, &overlap,&numread,TRUE);
被激发的事件对象
使用文件句柄作为激发机制,有一个明显的限制:如果多个线程对同一个文件进行操作,由于只有一个相同的handle,对于每个可能进行的overlapped 操 作都调用GetOverlappedResult()查看操作是否完成,这将不是一个很有效率的做法——因为很多的时候并不是自己所期待的操作完成了。另 外,Windows95/98下不可以使用文件句柄作为激发机制。
OVERLAPPED 结 构中的最后一个栏位是一个事件句柄。如果使用文件句柄作为激发对象,可将该位设置为NULL。如果该位被设定为一个事件对象时,系统核心会在overlapped 操 作完成后,自动设置此事件为激发。
由于每个overlapped 操 作都有它独一无二的OVERLAPPED 结 构,每个结构都有它独一无二的事件对象,用以代表该操作。
注意:OVERLAPPED 结 构中的事件必须是手动事件。否则,如果事件为自动事件,由于系统核心可能会在你有机会等待该事件前就激发它,而自动事件的激发状态是不能保留的,于是事件 遗失,这将导致你的等待永远无法返回。
使用手动事件配搭overlapped I/O,就可以对同一个文件发出多个读取操作和多个写入操作,每个操作都有自己的事件对象。然后调用wait…()函数等待其中之一或全部完成。
异步过程调用(Asynchronous Procedure Calls,APCs)
使用overlapped I/O配搭手动事件,会产生两个问题:
-
使用WaitForMultipleObjects()最多只能等待MAXIMUM_WAIT_OBJECTS个对象。对于Win32,此值目前为64。 如果等待的对象超过64,就会出问题。
-
必须不断地根据“哪一个事件被激发”而计算如何反应。
异步过程调用可解决此问题。此时只要使用“Ex”版的ReadFile()和WriteFile()。这两函数可额外指定一个参数,定义一个 callback函数。当一个overlapped I/O完成时,系统调用该callback函数。这个callback函数被称为I/O completion routine,因为,系统是在一个特定的overlapped I/O完成后调用它的。
但是,需要注意的是,一个特定的overlapped I/O完成后,Windows并不会贸然中断你的程序,然后调用你所提供的callback函数。那显然可能会带来新的问题。只有线程说“好,现在是一个 安全时机”时系统才会调用你的callback函数。以Windows的说法,只有线程处于所谓的“alertabe”状态,回调函数才会被执行,否则对 I/O completion routine的调用会被暂时搁置下来。因此,当一个线程终于处于“alertabe”状态时,可能有一堆储备的APCs等待被处理。
如果线程因为以下5个函数而处于等待状态,而其“alertabe”标志被设置为TRUE,则该现程就是处于“alertabe”状态:
DWORD SleepEx(
DWORD dwMilliseconds, // time-out interval in milliseconds
BOOL bAlertable // early completion flag
);
DWORD WaitForSingleObjectEx(
HANDLE hHandle, // handle to object to wait for
DWORD dwMilliseconds, // time-out interval, in milliseconds
BOOL bAlertable // return to execute I/O completion routine if TRUE
);
DWORD WaitForMultipleObjectsEx(
DWORD nCount, // number of handles in handle array
CONST HANDLE *lpHandles, // points to the object-handle array
BOOL fWaitAll, // wait flag
DWORD dwMilliseconds, // time-out interval in milliseconds
BOOL bAlertable // alertable wait flag
)
DWORD MsgWaitForMultipleObjectsEx(
DWORD nCount, // number of handles in handle array
LPHANDLE pHandles, // pointer to an object-handle array
DWORD dwMilliseconds, // time-out interval in milliseconds
DWORD dwWakeMask, // type of input events to wait for
DWORD dwFlags // wait flags
);
DWORD SignalObjectAndWait(
HANDLE hObjectToSignal, // handle to object to signal
HANDLE hObjectToWaitOn, // handle to object to wait for
DWORD dwMilliseconds, // time-out interval in milliseconds
BOOL bAlertable // alertable flag
);
只有使用上面这些函数进行等待ReadFileEx()或WriteFileEx()操作,回调函数才会被执行。
回调函数解释
VOID CALLBACK FileIOCompletionRoutine(
DWORD dwErrorCode, // completion code
DWORD dwNumberOfBytesTransfered, // number of bytes transferred
LPOVERLAPPED lpOverlapped // pointer to structure with I/O information
);
dwErrorCode:0表示操作完成,ERROR_HANDLE_EOF表示操作到文件尾
dwNumberOfBytesTransfered:真正被传输的数据字节数
lpOverlapped:指向OVERLAPPED 结构,由开启overlapped I/O操作的函数提供。由于APCs时OVERLAPPED 结构中hEvent栏位不需要用来放置一个事件句柄,此栏程序员可自由运用,比如用作回调函数的入 口参数。
对文件进行Overlapped I/O的缺点
在Windows NT测试时发现,Windows NT似乎是以“I/O请求”的大小来决定是否进行overlapped I/O。如果数据量较小,系统实际上总是采取非overlapped 方式进行文件读取;数据量略大些,采取Overlapped I/O其实比单纯调用ReadFile()并无优越性,相反地,效率降低。如果文件数据量比较大,Overlapped I/O才能凸显其优越性。