为 Unix 程序员准备的 Windows 异步 I/O 教程


在阅读之前,我认为你已经掌握了Unix系统上的非阻塞的Socket I/O。

同样的,在Windows系统上也能够找到select这个系统调用。但是,select 在文件描述上实现的是一个O(n)的算法,他并不像现在常用的实时多路复用的 epoll,这也让使 select在高并发服务器上没了用武之地。 接下来,我们将讲述的是Windows下的高并发服务器的设计.

除了epoll或者kqueue, Windows也有自己的多路复用I/O,叫做/O completion ports(IOCPs). IOCPs采用轮寻overlapped I/O方式。并且 IOCP 的消耗时间是一个常数 (REF?).

最根本的变化是,在Unix下,你一般要求内核等待一个文件描述符的可读性或writablity状态变化。使用overlapped I/O和IOCPs,我们将会等待异步方法调用的完成。举例来说,我们不是要等待一个socket的状态成为可写,然后用send(2)就可以了, 就像我们在 Unix 常做的一样, 使用overlapped I/O你应该使用 WSASend()发送消息,并且等待消息发送完全。

Unix非堵塞I/O不是完美的,在Unix系统中的原则抽象是将许多事情的处理统一看做对文件的操作(或者更准确的说是文件描述符)。write(2) read(2) close(2)在TCP协议下工作就像它们对常规文件的操作。同步操作工作在类似的不同文件描述符,但是一旦对性能的需求驱动你去使用O_NONBLOCK变量类型能起到完全不同的作用,甚至对于最基本的操作。尤其,常规系统文件不支持非阻塞操作。(令人不安的是没有人提及这个相当重要的事实)例如, 当它在做非阻塞读操作时,尽管安全的,一个人不可能在一个常规文件FD中轮询是否是可读的。常规文件总是可读的,并且read(2)调用总是有可能阻塞一个调用线程在一个未知的时间里。

POSIX 对于一些操作已经定义了 异步接口,但很多实现这些操作的 Unix,其情形并不明确。在 Linux 上,aio_* 例程使用 pthread,实现于 GNU libc 的用户态。io_submit(2) 没有 GNU libc 的封装,这被报告为非常缓慢,有可能阻塞。 Solaris 拥有真正的内核 AIO,但不清楚,与磁盘 I/O 相比,Socket I/O 的性能特性是什么。现代高性能 Unix socket 程序通过 I/O 复用器来使用非阻塞的文件描述符,而非 POSIX 的 AIO。异步访问磁盘的通常做法依然是通过使用定制的用户态线程池来完成,而非 POSIX 的AIO。

Windows 的 IOCP 不同时支持 socket 和 普通文件 I/O,后者可以极大的简化磁盘操作。 例如,ReadFileEx() 对两者都支持。第一个例子,我们先来看看 ReadFile() 是如何工作的。

typedef void* HANDLE;

BOOL ReadFile(HANDLE file,
              void* buffer,
              DWORD numberOfBytesToRead,
              DWORD* numberOfBytesRead,
              OVERLAPPED* overlapped);

这个函数执行读取时可以用同步或异步两种方式。同步操作是通过返回0和 WSAGetLastError()返回 WSA_IO_PENDING 来表示的。当 ReadFile() 异步运行时,用户提供的 OVERLAPPED* 是一个不完全操作的句柄。

typedef struct {
  unsigned long* Internal;
  unsigned long* InternalHigh;
  union {
    struct {
      WORD Offset;
      WORD OffsetHigh;
    };
    void* Pointer;
  };
  HANDLE hEvent;
} OVERLAPPED;

要调查这些函数的完成情况,可以使用IOCP,overlapped->hEvent, 和 GetQueuedCompletionStatus()。

简单的TCP连接例子

为了展示GetQueuedCompletionStatus()的使用,提出了一个本地连接到端口8000的例子

char* buffer[200];
WSABUF b = { buffer, 200 };
size_t bytes_recvd;
int r, total_events;
OVERLAPPED overlapped;
HANDLE port;

port = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, NULL, 0);
if (!port) {
  goto error;
}


r = WSARecv(socket, &b, 1, &bytes_recvd, NULL, &overlapped, NULL);

CreateIoCompletionPort(port, &overlapped.hEvent,

if (r == 0) {
  if (WSAGetLastError() == WSA_IO_PENDING) {
    /* Asynchronous */
    GetQueuedCompletionStatus()


    if (r == WAIT_TIMEOUT) {
      printf("Timeout\n");
    } else {
     
    }


  } else {
    /* Error */
    printf("Error %d\n", WSAGetLastError());
  }
} else {
  /* Synchronous */
  printf("read %ld bytes from socket\n", bytes_recvd);
}

先前工作

充分的跨越Unix和Window系统之上写代码是非常困难的, 这要求一个人去理解错综复杂的API和不同系统的非文档化细节。目前已有多个项目试图去提供一个抽象层,但是在作者的观念中,没有人能完全满意这种形式。

Marc Lehmann 的 libev 与 libeio. libev 是 UNIX I/O 多路复用的最小完美抽象。包括了一些有用的工具函数,如 ev_async,它用于异步通告,但主构件是 ev_io,用于通知用户文件描述符的状态。如前所述,一般通常不可能获得普通文件的状态变化 — 并且即使是 write(2) 和 read(2) 调用,也无法保证它们不阻塞。因此,libeio 被开发出来,用于在可管理的线程池中进行各种磁盘相关的系统调用。不幸的是,libev 做为目标的抽象层并不适合 IOCP — libev 的工作严重依赖于文件描述符,没有 socket 的概念。而且,Unix 的用户可能会将 libeio 用于文件 I/O,但移植到 Windows 上是不理想的。在 Windows 上,libev 当前使用的是 select()—每个线程不超过 64 个文件描述符。

libevent. 比 libev 更庞大,包含了 RPC,DNS,和 HTTP 代码。不支持文件 I/O。libev 在 Lehmann 评估 libevent 并加以拒绝之后开发了它 — 他的理由读起来很有趣。 关键性重写 在版本 2 完成,从而支持 Windows IOCP, 但大量的实例研究显示,它还没有正确的工作。

Boost ASIO. 它基本上满足你在 Windows 和 Unix 下使用 socket 的需要,即,Linux 的 epoll,Macintosh 的 kqueue,Windows 的 IOCP。它不支持文件 I/O。在笔者看来,对于一个并非十分复杂的问题,它过于庞大了(大约 300 个文件,大约 12000 个分号)。

命名管道

  • Window已有的命名管道,它或多或少的和AF_Unix domain sockets相同

  • AF_Unix

  • 存在于文件系统中的socket经常看起来像

    view source   print?
    1 /tmp/pipename
  • window的命名管道有一个路径,但是它们并不是文件系统的直接部分;而是

    view source   print?
    1 \\.\pipe\pipename

     

  • socket(AF_Unix, SOCK_STREAM, 0), bind(2), listen(2)

  • CreateNamedPipe() Use FILE_FLAG_OVERLAPPED, PIPE_TYPE_BYTE,PIPE_NOWAIT.

  • WriteFileEx()

  • recv(2), read(2)

  • ReadFileEx()

  • connect(2)

  • CreateNamedPipe()

  • accept(2)

  • ConnectNamedPipe()

例子:

  • Named Pipe Server Using Completion Routines

  • Named Pipe Server Using Overlapped I/O

常规文件

在Unix文件系统中文件不能使用非阻塞I/O。有一些操作系统有异步I/O,但是它不是标准的,至少在Linux系统需要GNU libc的pthreads。为了此应用设计在不同的Unix下是方便的, 必须管理一个线程池为分配文件的I/O系统调用。

在window系统中的较好情况是真正重叠的I/O是可用的,当读或者写一个数据流到文件中。

 

  • write(2)

  • Windows:WriteFileEx()

  • Solaris系统的事件完成端口实现了真正的内核级异步写操作aio_write(3RT)

  • read(2)

  • Windows:ReadFileEx()

  • Solaris系统的事件完成端口实现了真正的内核级异步读操作aio_read(3RT)

相关内容