为 Unix 程序员准备的 Windows 异步 I/O 教程 已翻译 100%

oschina 投递于 2014/04/01 07:52 (共 10 段, 翻译完成于 04-08)
阅读 5841
收藏 119
5
加载中

在阅读之前,我认为你已经掌握了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()发送消息,并且等待消息发送完全。

Mike-Tang
Mike-Tang
翻译于 2014/04/01 12:13
2

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

漠天
漠天
翻译于 2014/04/01 17:49
1

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);
yxrykds
yxrykds
翻译于 2014/04/04 18:39
1

这个函数执行读取时可以用同步或异步两种方式。同步操作是通过返回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()

赵亮-碧海情天
赵亮-碧海情天
翻译于 2014/04/01 13:32
1

简单的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和不同系统的非文档化细节。目前已有多个项目试图去提供一个抽象层,但是在作者的观念中,没有人能完全满意这种形式。

漠天
漠天
翻译于 2014/04/01 16:35
1

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 个文件描述符。

yxrykds
yxrykds
翻译于 2014/04/02 01:06
1

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 个分号)。

yxrykds
yxrykds
翻译于 2014/04/02 00:38
1

文件类型

几乎每一个你所熟悉socket操作都有一个异步的对应部分。下面的条目试着就Windows异步I/O系统调用与Unix的非阻塞配个对。

TCP Socket套接字

TCP Sockets是到目前为止最重要的保证正确性的流。服务器被期待去处理成千上万个这样同时发生的每一个线程。即使考虑避免类似Unix主义的文件描述符,在Windows下这种异步I/O也是可能的。(Windows有一个打开的文件描述符不能超过2048的硬限制——具体见 _setmaxstdio()。)

• send(2), write(2)
• Windows:  WSASend() WriteFileEx()
• recv(2), read(2)
• Windows: WSARecv() ReadFileEx()
• connect(2)
• Windows: ConnectEx()和非阻塞  connect()  在Unix下有不同的含义。适当的连接一个远程主机方式是调用connect(2),当它返回 EINPROGRESS 时,轮询文件描述符是否可写。这个时候使用

int error;
socklen_t len = sizeof(int);
getsockopt(fd, SOL_SOCKET, SO_ERROR, &error, &len);

一个值为0的 error 变量来告诉我们连接成功。(文档在Linux man page的 connect(2) 的EINPROGRESS错误码中。)
• accept(2)
• Windows: AcceptEx()
• sendfile(2)
• Windows: TransmitFile() 在Unix中对应的确切API是sendfile(2),然而它却没有被同意。每一种操作系统都有些许的不同。Allsendfile(2) 的实现(可能出了FreeBSD吧?)是甚至是在非阻塞的socket上实现的阻塞。Marc Lehmann已经写了 一个libeio可移植性版本库

Linux sendfile(2)  
FreeBSD sendfile(2)
Darwin sendfile(2)

• shutdown(2),优雅的关闭, 半双工连接
优雅的 Shutdown, Linger 选项组, 和Socket关闭 DisconnectEx()
• close(2)
closesocket()

• 下面的条目中在Windows异步和Unix非阻塞几乎是一样的。唯一的不同是Unix中的文件描述符是一个整型,在Windows中使用的是SOCKET类型。
sockaddr
bind()
getsockname()

溪边九节
溪边九节
翻译于 2014/04/07 20:59
1

命名管道

例子:

常规文件

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

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

漠天
漠天
翻译于 2014/04/01 16:55
1

控制台/TTY

它 (是不是常常?) 可能想一个TCP套接字那样轮询一个Unix TTY 文件描述器来获取可读和可写属性—这非常有益和美好. 在windows中这种境地是非常糟糕的,它不仅仅是一个完全不同的API,还要有读取和写入TTY的不重叠版本. 对可读属性进行轮询可以通过等待在另外一个线程中使用RegisterWaitForSingleObject()来完成.

  • read(2)

  • ReadConsole()ReadConsoleInput()不支持重叠的I/O,并且没有重叠的计数部分. 解决这个问题的一种策略是

    RegisterWaitForSingleObject(&tty_wait_handle, tty_handle,
      tty_want_poll, NULL, INFINITE, WT_EXECUTEINWAITTHREAD |
      WT_EXECUTEONLYONCE)

    其将会在一个不同的线程中执行 tty_want_poll() . 你可以使用这个东西去通知被调用的线程ReadConsoleInput() 将不会阻塞.

  • write(2)

  • WriteConsole()也是阻塞的,但这可能是可以被接受的.

  • tcsetattr(3)

  • SetConsoleMode()

什锦链接

小贴士

IOCP:

APC:

LeoXu
LeoXu
翻译于 2014/04/08 08:34
1
本文中的所有译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接。
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。
加载中

评论(14)

哆啦比猫
哆啦比猫

但是,为什么要为 unix 程序员准备 windows 的东西呢

ruki
ruki

引用来自“冰力”的评论

IOCP 是这几个性能最好、用起来最简单的了。
当然优秀的还有2000年FreeBSD的kqueue。
epoll 是最后弄出来的,参考了 kqueue ,但是用法却比较乱,而且目前来看性能最差。。

引用来自“Neo”的评论

IOCP不了解,不过epoll跟kqueue貌似就不是面向相同的用途的,kqueue能做epoll不能做的东西,而且kqueue的参数更多,更复杂。epoll这个接口很简洁,同时它的适用范围也比kqueue的小,当然也更容易掌握

引用来自“冰力”的评论

kqueue 网络 IO 和本地 IO 通吃。

引用来自“Neo”的评论

谢谢指点,我就依稀记得kqueue/epoll有不同而已,不记得在哪里看过了,记得当初看到的时候就是说kqueue的设计本初就是要一个系统调用搞定很多IO问题,所以就变复杂了。

就接口设计上讲 kqueue的更有效率 它能一次调用 批量设置多个fd 减少了内核间的调用切换次数

ruki
ruki

引用来自“vietor”的评论

iocp就是一个大坑。向句柄中写数据时无法设置超时,出现慢速情况内存就暴涨了。

我是通过一个定时器维护 然后cancelioex 来解决的 就是xp不支持这接口 只能win7以上使用

ruki
ruki

Windows 的 IOCP 不同时支持 socket 和 普通文件 I/O? 你确定 ?我socket file在一个iocp port上跑 用到现在了

李斯文
李斯文

Win下面很麻烦。 系统紧密性太强

Neo
Neo

引用来自“冰力”的评论

IOCP 是这几个性能最好、用起来最简单的了。
当然优秀的还有2000年FreeBSD的kqueue。
epoll 是最后弄出来的,参考了 kqueue ,但是用法却比较乱,而且目前来看性能最差。。

引用来自“Neo”的评论

IOCP不了解,不过epoll跟kqueue貌似就不是面向相同的用途的,kqueue能做epoll不能做的东西,而且kqueue的参数更多,更复杂。epoll这个接口很简洁,同时它的适用范围也比kqueue的小,当然也更容易掌握

引用来自“冰力”的评论

kqueue 网络 IO 和本地 IO 通吃。

谢谢指点,我就依稀记得kqueue/epoll有不同而已,不记得在哪里看过了,记得当初看到的时候就是说kqueue的设计本初就是要一个系统调用搞定很多IO问题,所以就变复杂了。

totmann
totmann

为什么翻译成什锦呢。。assorted基本就等于misc

冰力
冰力

引用来自“冰力”的评论

IOCP 是这几个性能最好、用起来最简单的了。
当然优秀的还有2000年FreeBSD的kqueue。
epoll 是最后弄出来的,参考了 kqueue ,但是用法却比较乱,而且目前来看性能最差。。

引用来自“小猫嘿嘿哈”的评论

有测试数据没

我们工作中基本都用 kqueue/epoll,你有兴趣自己做做测试,也可以找找资料。

冰力
冰力

引用来自“冰力”的评论

IOCP 是这几个性能最好、用起来最简单的了。
当然优秀的还有2000年FreeBSD的kqueue。
epoll 是最后弄出来的,参考了 kqueue ,但是用法却比较乱,而且目前来看性能最差。。

引用来自“Neo”的评论

IOCP不了解,不过epoll跟kqueue貌似就不是面向相同的用途的,kqueue能做epoll不能做的东西,而且kqueue的参数更多,更复杂。epoll这个接口很简洁,同时它的适用范围也比kqueue的小,当然也更容易掌握

kqueue 网络 IO 和本地 IO 通吃。

Neo
Neo

引用来自“冰力”的评论

IOCP 是这几个性能最好、用起来最简单的了。
当然优秀的还有2000年FreeBSD的kqueue。
epoll 是最后弄出来的,参考了 kqueue ,但是用法却比较乱,而且目前来看性能最差。。

IOCP不了解,不过epoll跟kqueue貌似就不是面向相同的用途的,kqueue能做epoll不能做的东西,而且kqueue的参数更多,更复杂。epoll这个接口很简洁,同时它的适用范围也比kqueue的小,当然也更容易掌握

返回顶部
顶部