LARGE_INTEGER liCurrentPosition = { 0 };
SetFilePointerEx(hfile, liCurrentPosition, &liCurrentPosition,
FILE_CURRENT);设定文件的结尾
通常在关闭文件时,系统会处理设定文件的结尾;不过,有时候您可能会想要强迫一个文件变得更小或更大,在这些场合中,可以呼叫:
BOOL SetEndOfFile(HANDLE hfile);
这个SetEndOfFile函数会把文件的大小截短或延长,并转变成文件物件之文件指标所指示的大小。例如,您想要迫使一个文件变成1024位元组的长度,可用这个方式来使用SetEndOfFile:
HANDLE hfile = CreateFile(...);
LARGE_INTEGER liDistanceToMove;
liDistanceToMove.QuadPart = 1024;
SetFilePointerEx(hfile, liDistanceToMove, NULL, FILE_BEGIN);
SetEndOfFile(hfile);
CloseHandle(hfile);
执行后使用Windows Explorer来检查文件所显示的属性,文件恰好是1024位元组的长度。
执行同步设备I/O
本节会讨论执行同步设备I/O的Windows函数。请记住,设备可以是一个文件、mailslot、管道(pipe)、套接字(Socket)等等。无论使用了那个设备,I/O皆会被相同的函数执行。
最简单且最常用来从设备读取和写入的函数即是ReadFile及WriteFile:
BOOL ReadFile(
HANDLE hfile,
PVOID pvBuffer,
DWORD nNumBytesToRead,
PDWORD pdwNumBytes,
OVERLAPPED* pOverlapped);
BOOL WriteFile(
HANDLE hfile,
CONST VOID *pvBuffer,
DWORD nNumBytesToWrite,
PDWORD pdwNumBytes,
OVERLAPPED* pOverlapped);
hfile参数表示您所要存取设备的handle。当设备被开启后,您不需指定FILE_FLAG_OVERLAPPED标记,否则系统将认为您想要对该设备执行非同步I/O的动作。pvBuffer参数说明了哪一些设备的资料应该被缓冲器读取,或是内含应被写入设备之资料的缓冲器。nNumBytesToRead和nNumBytesToWrite参数分别指出ReadFile和WriteFile要从设备中读取多少个位元组,以及要写入多少位元组到设备上。
pdwNumByte参数会指出DWORD的位址,以使函数填满一些被顺利地传送至设备上与来自该设备的位元组。最后一个pOverlapped参数在执行同步I/O时应该为NULL。当我们讨论非同步I/O时,您将会更详细地察看这个参数的内容。
假如已经成功地执行,则ReadFile及WriteFile两者皆会传回TRUE。顺便一提,ReadFile可以只被拥有GENERIC_READ标记之已开启设备呼叫。同样地,WriteFile则可以只被具有GENERIC_WRITE标记之已开启设备呼叫。
注满资料到设备中
记得我们所看过的CreateFile函数,它可以传递相当多的标记以改变系统之快取文件资料的方式。有一些其他的设备,例如序列埠、mailslots及管道,也会执行快取资料动作。假如您想要迫使系统将快取资料写入设备时,可以呼叫FlushFileBuffers:
BOOL FlushFileBuffers(HANDLE hfile);
FlushFileBuffers函数会迫使所有与hfile参数指定设备有关的缓冲资料被写入。为了执行这个工作,必须开启拥有GENERIC_WRITE标记的设备。假如函数执行成功,则会传回TRUE。
基本的非同步设备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请求,您可以使用在〈执行同步设备I/O〉一节中所学到的ReadFile和WriteFile函数。为了方便起见,我再次列出该函数的原型:
BOOL ReadFile(
HANDLE hfile,
PVOID pvBuffer,
DWORD nNumBytesToRead,
PDWORD pdwNumBytes,
OVERLAPPED* pOverlapped);
BOOL WriteFile(
HANDLE hfile,
CONST VOID *pvBuffer,
DWORD nNumBytesToWrite,
PDWORD pdwNumBytes,
OVERLAPPED* pOverlapped);
当这些函数的其中之一被呼叫时,该函数会检查由hfile参数指定的设备是否具有FILE_FLAG_OVERLAPPED标记且已被开启。假如标记被指定,则函数会执行非同步设备I/O。顺便一提,当您为了非同步I/O而呼叫任何一个函数时,您可以(且通常会)在pdwNumBytes参数中传递NULL。
毕竟,您预期这些函数会在I/O请求完成前传回,所以在此时检查那些已被传送的位元组数量是无意义的。
OVERLAPPED结构
在执行非同步的设备I/O时,您必须经由pOverlapped参数传递位址到已被初始化的OVERLAPPED结构中。「overlapped」这个字在这里意味着执行I/O请求的时间与您的线程用来执行其他任务的时间重叠。以下, , 就是OVERLAPPED结构的样子:
typedef struct _OVERLAPPED {
DWORD Internal; // [out] 错误码
DWORD InternalHigh; // [out] 转换的位元组数量
DWORD Offset; // [in] 低32位元之文件位移量
DWORD OffsetHigh; // , [in] , , 高32位元之文件位移量
HANDLE hEvent; // [in] 事件的handle或资料
} OVERLAPPED, *LPOVERLAPPED;这个结构包含了五个成员。其中叁个成员—Offset、OffsetHigh及hEvent—必须在呼叫ReadFile或WriteFile前即初始化;其他两个成员—Internal及InternalHigh—当I/O操作完成时由设备驱动程序设定及检查。以下有这些成员变数之更详细说明:
- Offset 及 OffsetHigh 当某个文件被存取时,这些成员会指出文件中您想要开始进行I/O操作之64位元位移量。回想每个核心物件中有一个与它相关联的文件指标。当发布了一个同步的I/O请求时,系统会知道由文件指标所指出的位置开始存取文件。在操作完成后,系统便会自动地更新文件指标以便下一个操作能取得最后一个停止操作的位址。
当执行非同步I/O时,这个文件指标会被系统忽视。您可以想像假如程序代码中被放置了两个非同步的呼叫到ReadFile(对相同的文件核心物件)时会发生什么事。在这种情况下,系统不会知道第二个对ReadFile的呼叫要从哪里开始读取。您或许不想在第一个呼叫ReadFile所使用之相同位置开始读取文件。您可能想要接在第一次呼叫ReadFile时所读取的最后一个位元组后开始第二次读取文件。为了避免对相同物件之数个非同步呼叫产生混淆,所有的非同步I/O请求必须在OVERLAPPED结构中指定开始的文件位移量。
请注意,位移量和OffsetHigh成员不会被nonfile设备所忽视-即您必须初始化这两个成员,使其为0,否则I/O请求会失败而且GetLastError会传回ERROR_INVALID_PARAMETER。
- hEvent 这个成员被四个方法的其中一个用来接收I/O完成通知。当您使用可警告的I/O通知方法时,这个成员可被用来达到您的目的。我知道有许多开发人员将C++ 物件的位址储存在hEvent。(这个成员将在〈用信号通知事件核心物件〉一节中有更多的讨论)。
- Internal 这个成员持有处理I/O的错误程序代码。一旦您发布非同步的I/O请求,设备驱动程序会把Interna置放为到STATUS_PENDING,以指出没有因为未启动的操作而发生错误。事实上,定义在WinBase.h中的巨集指令HasOverlappedIoCompleted可让您检查一个非同步的I/O操作是否已经完成。假如这个请求仍旧未被决定,则传回FALSE;若这个I/O请求已经完成,则传回TRUE。以下为一个巨集指令的定义:
#define HasOverlappedIoCompleted(pOverlapped) \
((pOverlapped)->Internal != STATUS_PENDING) - InternalHigh 当一个非同步的I/O请求完成时,这个成员会持有已传送的位元组数量。
起初设计OVERLAPPED结构时,Microsoft即决定不提供Internal及InternalHigh成员的文件(解释它们的名称)。随着时间过去,Microsoft了解到加入这些成员的资讯对开发人员是有帮助的,所以将它们写成文件。然而,因为作业系统之原始程序代码常常会参考它们,且Microsoft又不想修改程序代码,所以没有改变成员的名称。
说明
当一个非同步的I/O请求完成时,您将会接收到OVERLAPPED结构被初始化时使用的位址之请求。使用OVERLAPPED结构四处传递相关的资讯是非常有用的-例如,您想要储存OVERLAPPED结构内用来初始I/O请求的handle。OVERLAPPED结构不为正在储存的内容提供设备handle成员或其他可能有帮助的成员,但您可以很容易地解决这个问题。
笔者常建立从OVERLAPPED结构衍生的C++ 类别。这个C++ 类别可以持有任何额外的资讯。当我的应用程序接收到OVERLAPPED结构的位址时,我只不过把位址扔给C++ 类别的指标。现在我可以使用OVERLAPPED成员及应用程序所需要的额外相关资讯。在本章结尾的FileCopy范例应用程序示范了这个技巧。有关相关细节,请参阅FileCopy范例应用程序中的CIOReq。
非同步设备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以表示失败,或非零的值表示成功。然而,ReadFile及WriteFile函数却表现得有点不同。举个例子可能有助于说明。
当试图去要求一个非同步的I/O请求时,设备驱动程序可能宁愿以同步的方式处理请求。假如您正在读取文件且系统正在检查您所要的资料是否已经在系统的快取中,这是可能发生的。如果资料是可得到的,您的I/O请求不会被设备驱动程序放置到伫列中;反之,系统会从快取中复制资料到您的缓冲器且完成I/O操作。
假如以同步的方式执行被请求的I/O,则ReadFile及WriteFile会回传一个非零值。如果以非同步的方式执行所请求的I/O,或是当呼叫ReadFile或WriteFile时发生错误,则回传FALSE。一旦回传FALSE,就必须呼叫GetLastError来明确地判定发生了什么事。假如GetLastError传回ERROR_IO_PENDING,则表示I/O请求已被成功地储存在伫列中且将在稍后完成。
假如GetLastError传回除了ERROR_IO_PENDING以外的值,则该I/O请求无法被放置到设备驱动程序伫列中。以下是当I/O请求无法被放置到设备驱动程序伫列时,从GetLastError传回之最常见的错误程序代码:
- ERROR_INVALID_USER_BUFFER或ERROR_NOT_ENOUGH_ MEMORY 每个设备驱动程序皆会维护一个未完成之I/O请求的固定大小清单(在无页码的集区中)。假如这个清单满了,系统便无法将您的请求放置到伫列中,如此一来,ReadFile及WriteFile会传回FALSE且GetLastError会报告这两个错误程序代码的其中一个(取决于驱动程序)。
- ERROR_NOT_ENOUGH_QUOTA 某些设备会要求您锁定分页资料缓冲器的储存,因此当I/O未完成时,资料无法从RAM交换。当您使用FILE_FLAG_NO_BUFFERING标记时,这个锁定分页的储存规定对文件I/O而言是正确的。然而,系统也限制了单一行程能够锁定分页的储存数量。假如ReadFile及WriteFile不能对您的缓冲器做锁定分页的储存动作,则函数会传回FALSE,且GetLastError会报告ERROR_NOT_ENOUGH_QUOTA。您可以经由呼叫SetProcessWorkingSetSize来增加一个行程的配额。
您该如何处理这些错误?在根本上,这些错误会因为一些重要的I/O请求还未完成而发生,您必须完成某些悬而未决的I/O请求,然后再重新发布对ReadFile及WriteFile的呼叫。
第叁个您应意识到的议题是在I/O请求完成前,务必不能移动或毁坏该用来发布非同步I/O请求的资料缓冲器及OVERLAPPED结构。当I/O请求位于设备驱动程序的伫列时,资料缓冲器及OVERLAPPED结构的位址会被传递给驱动程序。请注意,只有位址而非实际的区块被传递。这个结果应该是非常明显的:内存的复制是非常昂贵且浪费很多CPU时间的。
当设备驱动程序准备好去处理伫列的请求时,它会转换pvBuffer位址所参考的资料,并存取文件的位移量成员及指向OVERLAPPED结构内之其他成员的pOverlapped参数。设备驱动程序对应到I/O错误程序代码更新之Internal成员及转移位元组的数量以更新InternalHigh成员。
说明
这些缓冲器不能被移动或毁坏,直到I/O请求完成或者内存内容被更改为止。并且,您必须为每个I/O请求分派及初始化一个唯一的OVERLAPPED结构。
前面所提内容非常的重要,而且它是开发人员在实作一个非同步设备I/O结构时,最常见的一个错误。以下是个什么都不做的范例:
VOID ReadData(HANDLE hfile) {
OVERLAPPED o = { 0 };
BYTE b[100];
ReadFile(hfile, b, 100, NULL, &o);
}这个程序代码看起来相当地无害,而且完美地呼叫了ReadFile。唯一的问题是函数会在要求非同步I/O请求后返回。从函数返回时,从线程的堆叠中实际地释放了缓冲器之OVERLAPPED及结构,但是设备驱动程序并没有察觉到ReadData的返回。设备驱动程序仍旧有两个指向线程堆叠的内存位址。当I/O完成时,不管那时在内存被占用的地点发生了什么情形,设备驱动程序皆会去修改线程堆叠上的内存。因为内存非同步地发生改变,所以这个错误特别难找。有时设备驱动程序可能会同步地执行I/O,在此个案里您将不会看到这个错误。有时在函数返回后,I/O可能会正确的完成,或者可能在它完成超过一个小时后;但是谁会知道堆叠从什么时候开始被使用呢?
取消伫列中的设备I/O请求
有时您可能会想要在设备驱动程序处理存在伫列中的设备I/O请求前取消它。Windows提供了一些方法:
- 您可以呼叫CancelIo,以取消一个为了特定handle而呼叫之线程的I/O请求。
BOOL CancelIo(HANDLE hfile);
- 不管是哪个线程之请求,您皆可以经由关闭设备本身的handle来取消所有伫列中之I/O请求。
- 当一个线程停止时,系统会自动地取消线程所发布的所有I/O请求。
如您所看到的,没有一个方法可以取消单一且特定的I/O请求。
说明
取消I/O请求完成会产生一个ERROR_OPERATION_ABORTED的错误程序代码。
接收完成的I/O请求通知
此时,您已知道如何要求一个非同步设备I/O请求,但还没讨论到设备驱动程序在I/O请求完成后会如何通知您。
为了接收I/O完成通知,Windows提供了四个不同的方法(表2-9中有简短的叙述),本章涵盖了所有的方法。这些方法依复杂性的高低列出,从最容易理解及实作(通知设备核心物件)到最难理解及实作(I/O完成端口)。
| 表2-9 接收I/O完成通知的方法 |
| 技巧 | 摘要 |
|---|---|
| 通知设备核心物件 | 在单一设备中执行多重的同步I/O请求是没有用的。允许一个线程发布一个I/O请求并由另一个线程来处理它。 |
| 通知事件核心物件 | 允许单一设备之多重的同步I/O请求。允许某个线程发布一个I/O请求而由另一个线程来处理。 |
| 使用可警告的I/O | 允许单一设备之多重的同步I/O请求。发布I/O请求的线程也必须处理它。 |
| 使用I/O完成连接埠通讯埠 | 允许单一设备之多重的同步I/O请求。允许某个线程发布一个I/O请求而由另一个线程来处理。这个技巧的可伸缩性很高而且有最大的弹性。 |
就像本章一开始所陈述的,I/O完成端口是四个接收I/O完成通知方法中最容易且有效的。经由学会这四个方法,您将会得知Microsoft将I/O完成端口加到Windows的原因以及I/O通讯埠解决因其他方法而存在的所有问题之方法。
通知设备核心物件
一旦线程发布了非同步的I/O请求,它仍旧会执行其他有用的工作,到了最后,线程会需要与I/O操作同时完成。换言之,除非将从设备传入来的资料完全载入缓冲器中,否则您将在线程的程序代码中碰撞到某个线程无法继续执行的位置。
在Windows中,设备核心物件能被当成线程同步化物件来使用,所以该物件可以是通知或非通知状态。在伫列I/O请求之前,ReadFile及WriteFile函数会将设备核心物件设定为非通知状态。当设备驱动程序完成请求时,驱动程序则将设备核心物件设定为通知状态。一个线程可以决定某个I/O请求是否经由呼叫WaitForSingleObject或WaitForMultipleObjects完成。以下有个简单的例子:
HANDLE hfile = CreateFile(..., FILE_FLAG_OVERLAPPED, ...);
BYTE bBuffer[100];
OVERLAPPED o = { 0 };
o.Offset = 345;
BOOL fReadDone = ReadFile(hfile, bBuffer, 100, NULL, &o);
DWORD dwError = GetLastError();
if (!fReadDone && (dwError == ERROR_IO_PENDING)) {
// I/O被非同步地执行;等待它完成
WaitForSingleObject(hfile, INFINITE);
fReadDone = TRUE;
}
if (fReadDone) {
// o.Internal包含I/O错误
// o.InternalHigh包含转移位元组的数量
// bBuffer包含读取的资料
} else {
// 发生某个错误:请查看dwError
}
这个程序代码发布了一个非同步I/O请求,然后立即等待请求的结束,最后使得非同步I/O的目的失败!显然地,您大概从未实际地编写过类似的程序代码,但这个程序代码的确说明了即将在这里叙述的重要观念:
- 经由使用FILE_FLAG_OVERLAPPED标记,设备必须为非同步I/O而开启。
- OVERLAPPED结构必须初始化它的Offset、OffsetHigh及hEvent成员。在这个程序代码范例中,除了Offset设定为345以便从文件的第346位元组开始读取资料外,其他的设定为0。
- ReadFile的传回值储存在fReadDone,以指出I/O请求是否被同步地执行。
- 假如I/O请求没有被同步地执行,则要检查是否有发生错误或者I/O被非同步地执行。将GetLastError的结果与ERROR_IO_PENDING做比较会给我们这些资讯。
- 为了等待资料,我呼叫WaitForSingleObject传递设备核心物件的handle。在核心物件变成通知状态前呼叫这个函数以使线程暂停。当它完成了I/O时,设备驱动程序会通知这个物件。在WaitForSingleObject返回后,I/O会完成并且将fReadDone设定为TRUE。
- 在读取完成后,您可以检查bBuffer中的资料、OVERLAPPED结构之Internal成员中的错误程序代码以及OVERLAPPED结构之InternalHigh成员中的转移位元组数量。
- 假如发生了真实的错误,dwError所包含的错误程序代码会带来更多资讯。
通知事件核心物件
刚才叙述之接收I/O完成通知方法是非常简单且明确的,但其结果并非完全有帮助,因为它没有把多重I/O请求处理的很好。举例来说,假设您试图于同一时间在单一的文件上实作多重之非同步操作。假定您想要同时从文件中读取10个位元组并且写入10个位元组至文件中。它的程序代码可能看起来像这个样子:
HANDLE hfile = CreateFile(..., FILE_FLAG_OVERLAPPED, ...);
BYTE bBuffer[10];
OVERLAPPED oRead = { 0 };
oRead.Offset = 0;
ReadFile(hfile, bBuffer, 10, NULL, &oRead);
OVERLAPPED oWrite = { 0 };
oWrite.Offset = 10;
WriteFile(hfile, "Jeff", 5, NULL, &oWrite);
. . . WaitForSingleObject(hfile, INFINITE);
// 我们不知道完成了什么:读取?写入?或两者都是?
您不能经由等待设备变成通知状态而使线程同时产生,因为操作一完成,物件就会变成通知的状态。假如您呼叫WaitForSingleObject并且传递设备handle给它,因为在读取、写入操作的完成或者两者皆已完成的情形下,您将没有把握函数是否会被返回。显然地,那里需要一个更好的方式去执行多重、同步之非同步I/O请求,以使您不会遇到这个困境-幸运的是,那里的确有。
OVERLAPPED结构的最后一个成员—hEvent,它识别某个事件之核心物件。您必须经由呼叫CreateEvent来建立这个事件物件。当一个非同步I/O请求完成时,设备驱动程序会检查OVERLAPPED结构的hEvent成员是否为NULL。假如hEvent不是NULL,则设备会经由呼叫SetEvent来通知这个事件。驱动程序也将设备物件设定为通知状态就如同之前所做的。然而,假如您使用事件去决定一个设备操作什么时候完成,则您不应等待设备物件变成通知状态-即以等待事件来取代。
假如您想要同时执行多重之非同步I/O请求,您必须为每个请求建立个别的事件物件,在每个请求的OVERLAPPED结构中初始化hEvent成员然后呼叫ReadFile或WriteFile。当您到达程序代码中必须与I/O请求完成同时发生的位置时,简单地呼叫WaitForMultipleObjects并传入与每个未完成I/O请求的OVERLAPPED结构有关之事件handles。由于这个方案,您可以容易且可*地同时执行多重的非同步设备I/O操作以及使用相同的设备物件。以下的程序代码说明了这个方法:
HANDLE hfile = CreateFile(..., FILE_FLAG_OVERLAPPED, ...);
BYTE bBuffer[10];
OVERLAPPED oRead = { 0 };
oRead.Offset = 0;
oRead.hEvent = CreateEvent(...);
ReadFile(hfile, bBuffer, 10, NULL, &oRead);
OVERLAPPED oWrite = { 0 };
oWrite.Offset = 10;
oWrite.hEvent = CreateEvent(...);
WriteFile(hfile, "Jeff", 5, NULL, &oWrite);
. . . HANDLE h[2];
h[0] = oRead.hEvent;
h[1] = oWrite.hEvent;
DWORD dw = WaitForMultipleObjects(2, h, FALSE, INFINITE);
switch (dw - WAIT__OBJECT_0) {
case 0: // 读取完成
break;
case 1: // 写入完成
break;
}
这个程序代码有点不自然且不完全是您在真实的应用程序中所要做的,但它阐明了我的论点。一个典型的真实应用程序会有一个等待I/O请求完成的回圈。当每个请求完成,线程会执行被要求的任务、伫列另一个非同步I/O请求以及循环回圈,以及等待更多的I/O请求完成。
GetOverlappedResult
回想当初Microsoft不为OVERLAPPED结构的Internal及InternalHigh成员提供文件,意味着它必须提供另一个能让您知道在I/O行程期间有多少位元组已被转移以及获得I/O错误程序代码的方式。为了使您可得到这个资讯,Microsoft建立了这个GetOverlappedResult函数:
BOOL GetOverlappedResult(
HANDLE hfile,
OVERLAPPED* pOverlapped,
PDWORD pdwNumBytes,
BOOL fWait);
如今Microsoft为Internal及InternalHigh成员提供了文件,因此GetOverlappedResult函数即变得不是很有帮助。然而,当我第一次学习非同步I/O时,决定将这个函数做逆向工程以使我头脑里的观念变得稳固。以下的程序代码显示出内部如何实作GetOverlappedResult之方法:
BOOL GetOverlappedResult( HANDLE hfile,
OVERLAPPED* po,
PDWORD pdwNumBytes,
BOOL fWait) {
if (po->Internal == STATUS_PENDING) {
DWORD dwWaitRet = WAIT_TIMEOUT;
if (fWait) {
// 等待I/O完成
dwWaitRet = WaitForSingleObject(
(po->hEvent != NULL) ? po->hEvent : hfile, INFINITE);
}
if (dwWaitRet == WAIT_TIMEOUT) {
// I/O未完成而且我们不可以等待
SetLastError(ERROR_IO_INCOMPLETE);
return(FALSE);
}
if (dwWaitRet != WAIT_OBJECT_0) {
// 呼叫WaitForSingleObject错误
return(FALSE);
}
}
// I/O完成:传回转移位元组的数量
*pdwNumBytes = po->InternalHigh;
if (SUCCEEDED(po->Internal)) {
return(TRUE); // No I/O error
}
// 将最后的错误设定为I/O错误
SetLastError(po->Internal);
return(FALSE);
}
可警告的I/O
第叁个可以用来接收I/O完成通知的方法是呼叫可警告的I/O。首先,对想要创造高效能、可伸缩性应用程序的开发人员来说,Microsoft侦查可警告的I/O作为绝对适当的机制。但是当开发人员开始使用可警告的I/O后,他们会很快地了解到它并没有实践这个承诺。
我对可警告的I/O已经了解得很彻底,而且会是第一个跟您说可警告的I/O糟透了的人,您应该避免使用它。然而,为了使可警告的I/O能够工作,我发现Microsoft在操作系统中加进了一些非常有帮助且有价值的基础建设。当您阅读这节时,请集中精神于适当的基础建设上,并且不要陷入I/O的泥沼。
无论线程何时被建立,系统也会建立一个与线程有关的伫列。这个伫列被称为非同步程序呼叫(Asynchronous Procedure Call, APC)伫列。当发布一个I/O请求时,您可以命令设备驱动程序附加一个入口到呼叫线程的APC伫列中。为了使完成的I/O通知被伫列到线程的APC伫列,可以呼叫ReadFileEx及WriteFileEx函数:
BOOL ReadFileEx(
HANDLE hfile,
PVOID pvBuffer,
DWORD nNumBytesToRead,
OVERLAPPED* pOverlapped,
LPOVERLAPPED_COMPLETION_ROUTINE pfnCompletionRoutine);
BOOL WriteFileEx(
HANDLE hfile,
CONST VOID *pvBuffer,
DWORD nNumBytesToWrite,
OVERLAPPED* pOverlapped,
LPOVERLAPPED_COMPLETION_ROUTINE pfnCompletionRoutine);
就像ReadFile及WriteFile,ReadFileEx及WriteFileEx发布I/O请求到某个设备驱动程序后,函数立即返回。ReadFileEx及WriteFileEx有与ReadFile和WriteFile函数相同的参数,其中有两个例外。第一,*Ex函数没有传递指标给填满转移位元组数量的DWORD;这个资讯只能经由回呼函数而被撷取。第二,*Ex函数需要您传递回呼函数的位址,称为完成例行程序。这个例行程序必须有以下的原型:
VOID WINAPI CompletionRoutine(
DWORD dwError,
DWORD dwNumBytes,
OVERLAPPED* po);
当您与ReadFileEx及WriteFileEx一起发布非同步的I/O请求时,函数会传递这个函数的位址到设备驱动程序。当设备驱动程序完成I/O请求后,它会在发布线程的APC伫列里附加一个入口(entry)。这个入口包含了用来初始化I/O请求之完成例行程序的位址以及OVERLAPPED结构的位址。
说明
顺便一提,当某个可警告的I/O被完成时,设备驱动程序将不试图通知事件物件。事实上,设备一点也没有参考到OVERLAPPED结构的hEvent成员。因此,如果您喜欢的话,可以为您自己的目的而使用hEvent成员。
当线程为可警告的状态时,系统会检查它的APC伫列以及伫列中的每个入口,系统呼叫完成函数并传递I/O错误程序代码、转移位元组的数量以及OVERLAPPED结构的位址给它。请注意,错误程序代码及转移位元组的数量也可以在OVERLAPPED结构的Internal及InternalHigh成员中找到。(如前所述,Microsoft最初并不想为此提供文件,所以将它们当作参数传递给函数)。
我们不久将回到这里完成例行程序函数。现在让我们先看看系统如何处理非同步的I/O请求。以下的程序代码会伫列叁个不同的非同步操作:
hfile = CreateFile(..., FILE_FLAG_OVERLAPPED, ...);
ReadFileEx(hfile, ...); // 执行第一个ReadFileEx
WriteFileEx(hfile, ...); // 执行第一个WriteFileEx
ReadFileEx(hfile, ...); // 执行第二个ReadFileEx
SomeFunc();
假如呼叫SomeFunc占用了一些执行时间,系统会在SomeFunc返回之前完成这叁个操作。当线程正在执行SomeFunc函数时,设备驱动程序会附加完成的I/O入口到线程的APC伫列中。APC伫列可能看起来有点像这个:
第一个WriteFileEx完成
第二个ReadFileEx完成
第一个ReadFileEx完成
APC伫列由系统内部维持。您亦可从清单中注意到系统可以使用任何顺序来执行伫列中的I/O请求,所以最后发布的I/O请求可能会最先完成,反之亦然。在线程之APC伫列中的每个入口包含收回函数的位址以及被传递到这个函数的值。
当I/O请求完成时,它们只简单地被伫列到线程的APC伫列中-即收回例行程序不会立即被呼叫,因为线程可能忙于作其他事情而不能被中断。为了处理线程之APC伫列中的入口,线程必须把它自己加进可警告的状态。这意味着线程已经达到执行中的位置,在那里它可以处理中断。Windows提供五个可以将线程加进可警告状态的函数: