Windows Via C/C++ 读书笔记 7 异步IO, 完成端口模式

来源:互联网 发布:珠海博明视觉 知乎 编辑:程序博客网 时间:2024/04/29 02:58

Windows Via C/C++ 读书笔记 

1. 异步IO

异步IO简单来讲就是把读写IO操作调用后交给操作系统处理,调用线程可以继续干其它的事情。当操作系统完成IO操作后,通知调用线程。调用线程得到通知后,再处理。

后面会讲如何设置IO模式为异步模式,如何获取通知。

 

操作顺序未知

文件的异步IO操作会提交给操作系统的队列,驱动程序不会严格按提交顺序执行。比如有的操作离目前文件指针位置比较近,为了减少磁盘指针移动,可能会先执行这个操作。

 

1.1. 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;

异步IO操作,必须在CreateFile的时候指明FILE_FLAG_OVERLAPPED模式(CreateFile函数的参数)。

OVERLAPPED结构成员的作用,见注释。在IO操作函数调用的时候,把这个结构的地址传递进去即可,它是个IN-OUT参数(这个说法可能不对,windows要求用GetOverlappedResult函数取这个结构的返回值,而不是直接用,见后面章节)。

Msdn2005OVERLAPPED

typedef struct _OVERLAPPED {
  ULONG_PTR Internal;
  ULONG_PTR InternalHigh;
  union {
    struct {
      DWORD Offset;
      DWORD OffsetHigh;
    };
    PVOID Pointer;
  };
  HANDLE hEvent;

} OVERLAPPED,
*LPOVERLAPPED;

Msdn2005用有一段:

Internal

Reserved for operating system use. This member, which specifies a system-dependent status, is valid when the GetOverlappedResult function returns without setting the extended error information to ERROR_IO_PENDING.

要求用GetOverlappedResult函数来获取返回值。后面章节书中给了个此函数的实现,可以看代码。因为IO状态可能还处于未完成状态,因此是不能立刻获取结果的,必须wait IO操作结束。

1.2. 等待异步IO通知

1.2.1. File内核对象

文件内核对象会在IO操作提交后立即变为nonsignaled状态,在任何一个IO请求完成后变为signaled状态。因此可以通过wait File内核对象来得到操作完成通知。见例子。

HANDLE hFile = CreateFile(..., FILE_FLAG_OVERLAPPED, ...);

BYTE bBuffer[100];

OVERLAPPED o = { 0 };

o.Offset = 345;

 

BOOL bReadDone = ReadFile(hFile, bBuffer, 100, NULL, &o);

DWORD dwError = GetLastError();

 

if (!bReadDone && (dwError == ERROR_IO_PENDING)) {

   // The I/O is being performed asynchronously; wait for it to complete

   WaitForSingleObject(hFile, INFINITE);

   bReadDone = TRUE;

}

 

if (bReadDone) {

   // o.Internal contains the I/O error

   // o.InternalHigh contains the number of bytes transferred

   // bBuffer contains the read data

} else {

   // An error occurred; see dwError

}

 

1.2.2. Event事件内核对象

文件内核对象的特点是队列中的任何一个IO操作完成都会使内核对象变为signaled,如果需要不同的操作获取不同的通知,那就使用Event对象(OVERLAPPED中的hEvent参数)。

 

HANDLE hFile = CreateFile(..., FILE_FLAG_OVERLAPPED, ...);

 

BYTE bReadBuffer[10];

OVERLAPPED oRead = { 0 };

oRead.Offset = 0;

oRead.hEvent = CreateEvent(...);

ReadFile(hFile, bReadBuffer, 10, NULL, &oRead);

 

BYTE bWriteBuffer[10] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };

OVERLAPPED oWrite = { 0 };

oWrite.Offset = 10;

oWrite.hEvent = CreateEvent(...);

WriteFile(hFile, bWriteBuffer, _countof(bWriteBuffer), 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:   // Read completed

      break;

 

   case 1:   // Write completed

      break;

}

 

1.2.3. GetOverlappedResult

微软以前的文档没有描述OVERLAPPED结构的Internal InternalHigh。需要GetOverlappedResult来获取结果。

 

BOOL GetOverlappedResult(

   HANDLE      hFile,

   OVERLAPPED* pOverlapped,

   PDWORD      pdwNumBytes,

   BOOL        bWait);

现在微软文档已经有Internal InternalHigh,因此这个函数没有用了。(书中原话,但是我查msdn2005还是需要此函数)。书给了个实现,可以帮助理解这个函数作用。

BOOL GetOverlappedResult(

   HANDLE hFile,

   OVERLAPPED* po,

   PDWORD pdwNumBytes,

   BOOL bWait) {

 

 

   if (po->Internal == STATUS_PENDING) {

      DWORD dwWaitRet = WAIT_TIMEOUT;

      if (bWait) {

         // Wait for the I/O to complete

         dwWaitRet = WaitForSingleObject(

            (po->hEvent != NULL) ? po->hEvent : hFile, INFINITE);

      }

 

      if (dwWaitRet == WAIT_TIMEOUT) {

         // I/O not complete and we're not supposed to wait

         SetLastError(ERROR_IO_INCOMPLETE);

         return(FALSE);

      }

 

      if (dwWaitRet != WAIT_OBJECT_0) {

         // Error calling WaitForSingleObject

         return(FALSE);

      }

   }

 

   // I/O is complete; return number of bytes transferred

   *pdwNumBytes = po->InternalHigh;

 

   if (SUCCEEDED(po->Internal)) {

      return(TRUE);   // No I/O error

   }

 

   // Set last error to I/O error

   SetLastError(po->Internal);

   return(FALSE);

}

 

1.3. Alertable I/O

作者认为这是个不好的东西,大家避免使用。这里介绍下工作原理和缺点。

操作系统给线程分配一个(异步调用)APC队列。线程做IO操作的时候设置异步调用的callback函数。每当一个操作完成的时候,操作系统会往APC队列里面放一个entry。当线程处于Alertable状态的时候,线程会来执行APC队列中的调用。注意是Alertable状态,平时线程是非alertable的,windows提供6个函数使线程进入alertable状态。执行APC调用的线程和调用线程是同一个线程。

 

缺点:

Callback函数没有上下文信息,会用大量的全局变量与线程交换信息。文者在写DDE程序的时候必须使用callback函数,搞了很多全局变量。当然,因为callback函数的调用线程和设置callback的线程是一个线程,不用考虑访问冲突加锁的问题。

 

最大的缺点是负载平衡问题。因为callback线程和IO线程是同一个线程,IO线程可能是个很繁忙的主线程,而callback函数必须占用这个主线程,而不能用其它闲置的线程来执行,因此会影响程序性能。

 

1.4. IO完成端口Completion Ports

一个服务程序通常设计为2种类型:

串行模式:只有一个主线程,它等待客户端的请求,收到请求后处理,再回复。

并行模式:主线程只负责等待请求,有请求的时候新建一个线程去处理,主线程继续等待其它客户端的请求。新线程处理完请求后回复客户端,然后结束。(非常类似Unix网络编程的例子,accept新连接后就fork一个新的进程去处理,子进程完成下面的工作,主进程继续accept)。

 

由于现在的网络服务程序需要处理海量的连接请求,而操作系统不能给所有的请求都分配一个线程处理,因此上面这种模式不能解决这种问题。为此,windows提供了完成端口模式,Unix提供了epoll

 

1.4.1. 完成端口模式

完成端口的工作模式是这样: 首先要分 主线程 和 服务线程池。

主线程做IO操作调用,然后把IO工作交给操作系统,自己不必等待,继续提供服务,做下一个IO操作调用。当IO操作完成后,操作系统通知服务线程池中的一个线程来完成后面的工作。

因此主线程的工作流程是这样:

1. 创建一个完成端口。

2. 创建线程池(CreateThread多个线程),把完成端口作为参数传个每个线程。

3. 接受一个客户端请求,把IO句柄与完成端口关联。

4. 用异步IO 加 OVERLAPPED结构做IO操作。

5. 循环步骤3-4

 

服务线程池的工作:

1.调用GetQueuedCompletionStatus,等待IO完成通知,该操作会使线程处于wait状态。

2.当有IO操作完成时,操作系统会唤醒该线程。线程通过上面的GetQueuedCompletionStatus函数返回结果获取IO操作完成情况(传送字节,错误代码等等)。

3.执行服务代码。

4.重复1-3

可以看出来,监听客户端请求的工作都在主线程中,提供服务的线程在线程池中。因此服务线程的数目是一定的,由操作系统维护一个请求队列,逐个调用线程池中的线程为它们提供服务。解决了上节并行模式为每个客户端创建一个线程带来的问题。因为线程池的数目是根据处理器数目优化的,因此效率会非常高,既不会让有的CPU空闲,也不会因为创建太多线程,使CPU浪费在线程上下文切换上面。

1.4.2. 创建和关联完成端口

CreateIoCompletionPort函数有2个作用:

1.如果hFile参数为空,表示创建一个完成端口。

2.如果hFile不为空,表示把一个device和一个完成端口关联。

 

HANDLE CreateIoCompletionPort(

   HANDLE    hFile,

   HANDLE    hExistingCompletionPort,

   ULONG_PTR CompletionKey,

   DWORD     dwNumberOfConcurrentThreads);

 

 

完成端口模式

 

1.4.3. IO完成队列(IO Completion Queue

这个队列为FIFO队列,记录了当前已经完成的IO请求,操作系统遍历这个队列,把IO操作完成信息通知给一个等待中的线程处理。

每个entry的信息有:dwBytesTransferred(传输字节数) ,dwComplecationKey(创建完成端口的参数key) pOverlappedOverlapped结构指针) dwError(错误代码)。

进入队列:

当一个IO操作完成时,操作系统检查是否关联了完成端口,如果有就往IO完成队列里面加入一个entry

出队列:

操作系统检查"等待线程队列"(下一节),如果有等待的线程,唤醒线程,并把参数传递进去。删除该entry

1.4.4. 等待线程队列(Wating Thread Queue-WTQ

这个队列记录了等待完成端口的线程(线程ID),当IO完成队列不为空时,唤醒队列中的一个线程处理。

 

进入队列:

线程调用GetQueuedCompletionStatus注册到这个队列,并进入wait状态。

出队列:

唤醒线程后,出队列。

 

1.4.5. 运行线程表(Released Thread List-RTL

如果线程被唤醒,那么线程ID添加到RTL中。

1.4.6. 暂停线程表(Paused Thread List-PTL

如果线程自己调用wait之类的函数进入等待,那么线程离开RTL,进入PTL

1.4.7. 线程池管理和调度

3个线程队列(表)的作用就是方便线程池的管理。还记得前面讲过,使用IO完成端口,是要指定最大可唤醒的线程数。即,操作系统检查IO完成队列,然后唤醒不超过这个最大数目的线程来执行。比如线程池有4个线程,IO完成端口设置最大唤醒数是2,有2CPU。这个配置在平时是工作非常好的,因为总有2个线程在工作。但是假设某个线程有个地方会调用wait函数把自己暂停下来,那么IO完成端口再想唤醒2个线程的时候,发现前面还有一个线程正在执行中,它就只能唤醒一个线程,这造成CPU的浪费。因此windows设计了更智能的解决办法,把运行中的线程放在RTL,暂停的线程放在PTL。那么既能保证运行时刻最大只有2个线程(有利于CPU,减少线程切换),又能保证有足够的线程提供服务。当然,如果有暂停的线程,那么唤醒2个线程后是有可能同时存在3个线程运行,这种情况是没有关系的。