Windows服务器端编程-第二章 设备IO与线程间通信-11-模拟已完成的I/O请求

模拟已完成的I/O请求

I/O完成端口并不是只能用于设备I/O。本章也是关于线程间通讯技术的章节,而完成端口内核对象是帮助实现这点的权威机制。在“警告式I/O”小节,提及了QueueUserAPC函数,该函数允许线程向另一个线程发送一个APC项。I/O完成端口也有一个模拟函数,PostQueuedCompletionStatus:

BOOL PostQueuedCompletionStatus(
   HANDLE      hCompPort,
   DWORD       dwNumBytes,
   ULONG_PTR   CompKey,
   OVERLAPPED* pOverlapped);

该函数将一个I/O已完成的通知添加到一个I/O完成端口队列中。第一个参数,hCompPort,指出目标完成端口,新的项将添加该完成端口的队列中。其余的参数,dwNumBytes, CompKey, pOverlapped,成为线程调用GetQueuedCompletionStatus时得到值,GetQueuedCompletionStatus返回TRUE,表示成功执行了一个I/O请求。

PostQueuedCompletionStatus函数极其有用,它给出了一个在线程池间进行通信的方法。当用户用户终止服务应用程序时,应该让所有的线程干净利落的退出。但如果线程正在等待完成端口,又没有I/O请求到来,线程不会被唤醒。通过为线程池内的每个线程调用PostQueuedCompletionStatus,每个线程都会被唤醒。线程检查GetQueuedCompletionStatus返回的值,知道程序正在结束,于是能够进行清除工作然后正常退出。

在以下例子中使用线程退出技巧时必须注意。该例子能够工作是因为线程池内的线程正在结束,并且没有再次调用GetQueuedCompletionStatus。而如果想通知线程池内的每个线程其他东西并再次循环调用GetQueuedCompletionStatus,就会产生问题,因为线程是按LIFO顺序被唤醒的。因此,必须在应用程序中加些额外的线程同步,以确保线程池内的每个线程能够有机会接收到模拟的I/O项。缺少这些额外的线程同步,线程可能会收到同一个通知好几次。

FileCopy示例应用程序

FileCopy示例应用程序(“02 FileCopy.Exe”),在本章末尾的清单2-1中,演示了I/O完成端口的用法。源代码和资源文件在随书CD的02-FileCopy目录中。程序简单的将一个指定的文件拷贝成新的名为FileCopy.cpy文件。当执行FileCopy时,会出现以下面的对话框:

表2-2,FileCoyp示例应用程序的对话框。
非常抱歉,图片欠奉。
点击“Pathname”按钮选择要拷贝的文件,显示文件名称和尺寸都会刷新。在点击“Copy”按钮时,程序调用FileCopy函数,完成所有的艰苦工作。现在集中精力讨论FileCopy函数。

在进行拷贝时,FileCopy打开源文件并取得其大小。我想让文件拷贝尽可能执行如飞一般,因为使用FILE_FLAG_NO_BUFFERING标志来打开文件。该标志允许我直接访问文件,跳过系统缓冲区“帮助”文件访问时所附加的那些过头内存拷贝操作。当然,直接访问文件意味要做更多的工作:必须使用磁盘卷的扇区尺寸的倍数为偏移来访问文件,读和写数据都必须以这个扇区尺寸的倍数来进行。我选择以BUFFSIZE(64KB)为块大小来传输文件数据,可保证该值为扇区尺寸的倍数。这也是我为什么将源文件尺寸取整为BUFFSIZE倍数的原因。要注意到源文件是以FILE_FLAG_OVERLAPPED标志打开的,因此对文件的I/O请求是异步的。

目标文件以相似的方法打开:都指定了FILE_FLAG_NO_BUFFERING和FILE_FLAG_OVERLAPPED标志。在创建目标文件时也将源文件的句柄作为CreateFile的hFile模板参数传入,使得目标文件和源文件有一致属性。


注意


一旦两个文件都被打开,调用SetFilePointerEx和SetEndOfFile会立即将目标文件的尺寸设置成最大值。调整目标文件的尺寸现在成为极其重要的工作,因为NTFS维护了一个类似水位的标记,用于指示写文件时的最高点。如果读取时超过了此标记,系统就会返回一堆0。如果写入时超过了此标记,文件从旧的水位标记点到最后写偏移处的数据会先以0填充, 然后再将数据写入文件,而文件的水位标志会被更新。该行为满足了C2安全级别中关于不保留原来数据的要求。在NTFS分区中写入文件结尾部分,会导致水位标记被修改,NTFS必须以同步方式处理I/O请求,即使该I/O请求原来是异步的。如果FileCopy函数没有设置目标文件的尺寸,所有的重叠I/O请求都不会以异步方式执行。

现在文件已经打开并准备进行处理了,FileCopy创建了一个I/O完成端口。为了更方便的使用I/O完成端口,我创建了一个小的C++类,CIOCP。该类对I/O完成端口函数进行很简单的包装。该类定义在IOCP.H中。将在附录B“类库”中讨论。FileCopy通过创建名为iocp的CIOP类的实例来创建I/O完成端口。

通过调用CIOCP的AssociateDevice成员方法将源文件和目标文件关联到完成端口。在和完成端口相关联后,每个设备被分配一个完成键值。当一个针对源文件的I/O请求完成时,该完成键值是CK_READ,表示有一个读操作已经完成了。类似地,当一个针对目标文件的I/O请求完成后,完成键值为CK_WRITE,表示一个写操作已经完成了。

现在准备初始化一组I/O请求(OVERLAPPED结构)和它们的内存缓冲。FileCopy函数在任一时间都保持4个(MAX_PENDING_IO_REQS)未完成的I/O请求。对于你自己的应用程序,可能需要允许I/O请求数量按需要动态增长或减少。在FileCopy程序中,CIOReq类封装了单个的I/O请求。正如你所见,该类从OVERLAPPED结构继承,并增加了一些额外的上下文信息。FileCopy分配一个CIOReq对象数组并调用AllocBuffer方法将BUFFSIZE大小的数据缓冲区与每个I/O请求对象关联起来。数据缓冲区使用VirtualAlloc函数分配。使用VirtualAlloc保证了内存块从偶数地址的分配粒度边界开始,这就满足了FILE_FLAG_NO_BUFFERING标志的标志:缓冲区地址必须是偶数且能够被卷扇区尺寸整除。

要产生对源文件的初始读请求,我用了一点小技巧:投递4个CK_WRITE的I/O完成通知给完成端口。当主循环开始时,线程在端口上等待并立即被唤醒,认为一个写操作已经完成了。这导致线程产生一个对源文件的读请求,这时才真正开始文件拷贝。

主循环在没有未完成I/O请求时终止。只要有未完成的I/O请求,循环内部就会调用CIOCP的GetStatus方法(该方法内部调用GetQueuedCompletionStatus)在完成端口上等待。GetQueuedCompletionStatus方法将线程转入睡眠,直到完成端口完成一个I/O请求。当GetQueuedCompletionStatus返回时,也返回了完成键值CompKey,应对CompKey进行检查,如果CompKey值为CK_READ,表示对源文件的一个I/O请求完成了。然后就调用CIOReq的写方法来产生对目标文件的写I/O请求。如果CompKey值为CK_WRITE,表示对目标文件的一个I/O请求完成了。只要读取操作没有超出源文件的尾部,就调用CIOReq的读方法继续从源文件中读数据。

当没有未完成I/O请求时,循环结束并进行关闭源文件和目标文件句柄的清理工作。在FileCopy返回之前,还有更多的工作要做:修改目标文件的尺寸,使之和源文件一致。要做到这点,则必须在不设置FILE_FLAG_NO_BUFFERING标志的情况重新打开目标文件。因为如果不使用这个标志,文件操作就没有必要在扇区边界进行处理。这就允许我将目标文件的尺寸收缩到与源文件一样大小。

你可能感兴趣的:(c++)