非阻塞异步(non-blocking asynchronous)是开发高性能应用程序的基础,下文详细描述了常用的这些异步模型。
IO模型
GNU/Linux I/O模型图略:
已知的IO模型有三种:阻塞同步(blocking synchronous) ,阻塞异步(blocking asynchronous),非阻塞同步(non-blocking synchronous) 和非阻塞异步(non-blocking asynchronous) 。
阻塞同步(blocking synchronous):必须等待操作的完成,否则只有等待。在此模型中,应用程序执行一个系统调用,会导致应用程序一直阻塞,直到系统调用执行完毕(成功或返回错误码)。例如:去ATM取钱,前面有人正在取钱,你只有等待此人完成后才能开始。
典型的read/write系统调用即是阻塞同步式IO,如图所示:
阻塞异步(blocking asynchronous):select(2)和poll(2)都是这种模型,select(2)调用本身会导致系统的阻塞,但是后续的处理是异步进行的,如图所示:
非阻塞同步 (non-blocking synchronous):同步操作,但是当资源不可用时,此调用将会立即返回,并得到通知指示资源不可用;否则立即开始。对应前面取钱例子,当你发现ATM前已经有人正在取钱,你立即放弃取钱,继续干以后的事情。在open(2)文件时加入O_NONBLOCK标志的read(2)系统调用的过程如下:
非阻塞异步 (non-blocking asynchronous):异步最重要的是调用方和被调用方的非阻塞运行。我要给你指派任务;好,如果操作立即完成,你会立即返回给我结果;否则就告诉我说你在执行中,完成了再通知我,我接着干自己的事情去吧,无须等待。对应前面例子,你发现前面有人在取钱,你立即委托他帮你取钱,然后立即做下一件事,被委托人取好将交给你。自然的必须提供一种机制,以期能在任务完成时让我得到通知。GNU/Linux的aio_read(2)系统调用的示意图:
阻塞式模型的问题
首先传统的poll()和select()方式在接受大量连接请求时会有以下问题:
0,kernel<->user space的内存拷贝代价。一但一个请求到来之后,无论其是否被处理都会有一次从kernel -> user space的内存拷贝;
1,应用程序需要扫描所有已经打开的文件描述符,根据kernel标记的状态来判断其是否就绪;因为系统内核已经知道了每个FD的状态,那么在app中的这个线性操作( O(N) )显然是一个重复劳动(显而易见的一个更好方式是kernel直接把就绪的FD告诉app);
2,同时在kernel内部对于select()的实现也不理想。首先必须在内存中保存在一个所有打开FD的列表;其次在一个连接到来时,kernel需要遍历这个列表找到空闲的FD。
这个问题的根源在于操作系统内核没有去记录每个应用程序关注的FD的状态和记录应用程序对应不同状态所要应用的操作(注册回掉函数)。
而epoll/kqueue/IOCP就针对以上3个问题采用了原理相同,但是细节略有差异的解决方法。
epoll kqueue IOCP
常规的select()操作是一个同步阻塞式的操作,如果要改变它的这些不足,需要将其改变为“非阻塞的同步”或“非阻塞的异步”。
非阻塞异步的好处是程序无需以相对较高的切换开销(如前文所说开销来自kernel <-> user 的切换,找到就绪FD的开销等)。
epoll
linux kernel 2.5开始引入到2.6内核已经非常成熟,包括Edge Triggered (ET)和Level Triggered (LT)两种工作方式。
LT(level triggered,水平触发)是默认方式,并且同时支持blocking和non-blocking 的IO操作。在这种做法中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的fd进行后续操作。如果你不作任何操作,内核还是会继续通知你的,所以,这种模式编程出错误可能性要小一点。
ET(edge-triggered,边缘触发)是高速工作方式,只支持no-blocking IO。在这种模式下,当文件描述符从未就绪变为就绪时,内核通过epoll告诉你。然后它会假设你知道文件描述符已经就绪,并且不会再为那个已经就绪的文件描述符发送更多的通知,等到下次有新的文件描述符就绪才会再次出发就绪通知。
kqueue:FreeBSD 4.x引入。
IOCP:windows 2000引入。
未完,继续。
写得不错,期待补充未完成的部分
这个模型跟UNP上面说的不太一样吧?