重叠I/O之可等待的重叠I/O【系列一】

时间:2022-06-12 19:29:22

一 什么是异步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在大多数情况下可以大幅度提升程序的新能,当然一些个别情况就不一定使用啦。

 

二 异步I/O的分类

  在Windows下一共有四种异步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请求中带有一个回调函数,即完成例程。当I/O请求完成时,如果线程处于可提醒状态,则系统会通知该线程调用它的异步队列中的回调函数来处理完成的I/O。
  • I/O完成端口。I/O完成端口允许我们向一个设备同时发出多个I/O请求。它允许一个线程发出I/O请求而另外一个线程对结果进行处理,这个是是和完成例程不同之处。这个技术具有很好的伸缩性和灵活性,算是异步I/O中最好的一种方式。I/O完成端口里面有一套线程池的调度策略,所以免去了我们去维护多线程的烦恼。

 

三  相关数据结构和函数

  下面我们来看看重叠I/O中的第一个方法----重叠I/O(等待)。先从了解重叠结构开始:

1  重叠结构 OVERLAPPED

  “overlapped”是执行I/O请求的时间与其他线程执行其他任务的时间是重叠的,OVERLAPPED的结构如下:

   

typedef struct _OVERLAPPED {
ULONG_PTR Internal;
//系统保留参数,初始化时不用设置,返回值时保存已处理的I/O请求的错误码
ULONG_PTR InternalHigh; //系统保留参数,初始化时不用设置,返回值时保存已传输的字节数
union {
struct {
DWORD Offset;
//文件偏移量的低位
DWORD OffsetHigh; //文件偏移量的高位
};

PVOID Pointer;
};

HANDLE hEvent;
//事件句柄,必须是手动事件
} OVERLAPPED, *LPOVERLAPPED;

  参数说明:

  Offset 和OffsetHigh

  这两个成员构成一个64位的偏移量。如果当前操作的是文件设备,则表示当前文件I/O操作应该从哪里开始。当执行异步I/O时,系统会忽略与文件关联的内核文件指针,而是用OVERLAPPED中指定的起始偏移量,这样可以避免对同一个对象进行异步调用的时候出现混淆。对于非文件设备,则需要将这两个参数设置为0,否则I/O请求会失败,返回错误ERROR_INVALID_PARAMETER。

  hEvent

  不同的重叠I/O操作方法中,hEvent的设置不同。在可等待的重叠I/0操作中,如果等待的是操作的文件句柄,则不需要hEvent不需要设置;而如果通过OVERLAPPED结构中的hEvent来等待的,或者是I/O完成端口方法,则需要将此参数设置为一个人工事件,注意不能使自动事件。如果是I/O完成例程的方法,则hEvent一般都用来传递I/O操作的信息。

  Internal

  用来保存已经处理的I/O请求的错误码。当我们发出异步I/O请求后,设备驱动程序会将internal设置为STATUS_PENDING,表示没有错误,操作尚未开始。

  InternalHigh

  当异步I/O请求完成时,用来保存已经传输的字节数。

异步I/O的注意事项:

  1 在异步I/O请求完成之前,一定不恩能够移动或是销毁在发出I/O请求时所使用的数据缓存和OVERLAPPED机构,否则会使内存遭到破坏。因为我们传给I/O设备的是这两个数据的地址,而I/O设备并不知道我们已经销毁了它们。

  2 在执行异步I/O的时候,设备不必以先入先出的方式处理队列中的I/O请求。

  3 以异步I/O执行的时候,文件的读写等相关的操作返回为FALSE不一定表示失败。必须调用GetLastError,如果GetLastError返回的是ERRO_IO_PENDING,表示I/O请求已经成功的加入到I/O队列中,会在晚些时候完成。

 

2 获取重叠I/O状态

  当我们使用ReadFile或WriteFile向文件发送读写请求后,函数会立刻返回。大多数情况下,返回时I/O操作都未完成,所以返回的FALSE,自然我们也不知道传输的字节数了。所以我们需要另外一种方法来获取文件I/O完成时传输的字节数,这个函数便是GetOverlappedResult:

  

BOOL
WINAPI
GetOverlappedResult(
__in HANDLE hFile,
//I/O文件句柄
__in LPOVERLAPPED lpOverlapped, //重叠结构
__out LPDWORD lpNumberOfBytesTransferred, //I/O完成时传输的字节数
__in BOOL bWait //Ture:一直等待,Flase:立刻返回
);

 

 参数hFile和lpOverlapped组合在一起可以唯一的表示一个I/O操作。当bWait设置为True,则函数会一直等待此I/O操作完成才会返回,此时lpNumberOfBytesTransfferred中即是传输的字节数。如果bWait设置为False,函数会立刻返回,如果返回会False且GetLastError()为ERRO_IO_PENDING,表示I/O尚未完成,需要轮询检查I/O是否完成。

 

3 取消队列中的重叠I/O请求

  如果想要取消一个已经加入到I/O队列但是尚未处理的I/O请求,则可以调用CancelIoEx取消。

BOOL
WINAPI
CancelIoEx(
__in HANDLE hFile,
__in_opt LPOVERLAPPED lpOverlapped
);

  CancelIoEx不仅可以取消掉本线程发出的相关文件的I/O请求,还可以将调用线程外的其他线程发出的待处理的I/O请求取消掉。这个函数会将hFile设备待处理的I/O请求中所有与lpOverlapped参数相关联的请求都取消掉。但是由于每个待处理的I/O请求都有一个其特定的OVERLAPPED结构,所以如果lpOverlpaped非空,则CancelIoEx每次只能取消一个请求。而lpOverlpaped为NULL的话,会取消掉hFileI/O请求队列中的所有I/O请求。

 

四 例子:使用可等待的重叠I/O进行文件复制操作

  这个示例程序使用事件来实现重叠I/O的等待。这个程序实现从输入文件中异步读数据,然后异步的写数据到输出文件中。程序采用多缓冲区的方法进行文件的转换,假设输入和输出各采用N个缓冲区,则N个输入缓冲和N个输出缓冲需要N个输入重叠结构和N个输出重叠结构与其对应。初始时,N个输入缓冲分别发出重叠的读操作,然后程序使用WaitForMultipleObjects来等待单一的I/O操作完成事件,表示一个读或写操作完成。当一个读操作完成时,则对缓冲区进行复制,然后发起一个写操作请求。当写完成时,就可以进行下一个读操作请求了。