服务器网络模型(1)--I/O模型与I/O复用

来源:互联网 发布:网站的源码如何查看 编辑:程序博客网 时间:2024/05/02 02:18

基本的I/O分为阻塞式和非阻塞式,而在非阻塞的情况下,又可进一步分为同步和异步,归类下来,分为三种:

l        阻塞式:以read()为例,它是停等式的读,在fd不可读时,会阻塞住线程继续等下去。一般高并发服务器比较少用。

l        同步非阻塞:它解决了阻塞式中停等的问题,在fd不可读时,会立即返回失败。一般结合一个统一的I/O复用机制(比如select、epoll)来对多fd做一个统一的等,而不消耗每一个线程的时间。

l        异步非阻塞:同步非阻塞解决了停等的问题,但是在实际读这一步,需要同步去读取数据,如果读的时间过长,同样会消耗工作线程大量时间。因此又出现了异步非阻塞的I/O方式。具体来说,异步需要在事件发生前向操作系统注册fd和回调函数,然后映射一段缓冲到操作系统,等待被回调即可。Linux采用了aio来支持这种读写,windows采用的是IOCP完成端口的概念。

我们不过多的讨论阻塞式I/O。同步非阻塞和异步非阻塞可以进一步抽象出两个常用的I/O事件模型,Reactor和Proactor:

l        Reactor:即反应式事件模型。反应式事件模型的读操作步骤如下:

1.        注册读事件

2.        事件分离器等待可读事件(分离器可以理解为select/epoll)

3.        事件到来,激活分离器,分离器调用事件处理器(事件的handle对象或者函数)

4.        事件处理器完成实际的读操作,处理读到的数据

抽象成生活中的例子——送快递:

1.        你的朋友发快递给你,写上你的地址(注册事件)

2.        快递公司分派快件

3.        快件到来,快递大叔通知你下楼拿快件(可读)

4.        你下楼拿快件然后上楼(读操作),拆快件(事件处理)

 

l        Proactor:即前摄式事件模型。它的读操作步骤如下:

1.        事件处理器发起异步操作,提供缓冲区和回调对象/函数

2.        事件分离器等待读操作完成事件(不一定需要epoll/select)

3.        分离器调用事件处理器

4.        处理已读到的数据

继续是送快递那个例子:

1.        你打电话给快递公司,说快递到了给你送到楼上

2.        快递公司分派快件

3.        快件到了,快递大叔非常友好的把快件给你送上了楼

4.        你直接拿到就可以拆了

 

从上面的送快递例子我们可以很简单的看出,Reactor仅需要你简单的等快递,快递到了,到楼下拿一下,整个过程,你只需要干一件事情,就是下楼收快递,但下楼比较耗时。Proactor需要我们有两个动作:打电话和收快递,但不需要消耗时间的下楼动作。

这里收快件的我们就相当于工作线程,Reactor模型直观易于理解,但需要处理一次I/O。Proactor增加了编程的复杂度,但给工作线程带来了更高的效率:Proactor可以在系统态将读写优化,利用I/O并行能力,提供一个高性能单线程模型。在windows上,由于没有epoll这样的机制,因此提供了IOCP来支持高并发, 由于操作系统做了较好的优化,windows较常采用Proactor的模型利用完成端口来实现服务器。在linux上,在2.6内核出现了aio接口,但aio实际效果并不理想,它的出现,主要是解决poll性能不佳的问题,但实际上经过测试,epoll的性能高于poll+aio,并且aio不能处理accept,因此linux主要还是以Reactor模型为主。

在不使用操作系统提供的异步I/O接口的情况下,还可以使用Reactor来模拟Proactor,差别是:使用异步接口可以利用系统提供的读写并行能力,而在模拟的情况下,这需要在用户态实现。具体的做法只需要这样:

1.        注册读事件(同时再提供一段缓冲区)

2.        事件分离器等待可读事件

3.        事件到来,激活分离器,分离器(立即读数据,写缓冲区)调用事件处理器

4.        事件处理器处理数据,删除事件(需要再用异步接口注册)

Boost::asio在linux上就采用了这样的方式:以epoll实现的Reactor来模拟Proactor,并且另外开了一个线程来完成读写调度。

实际的应用中,两者的性能差异并不是太大。因此大家不必纠结于模型的选择,更多的放在对模型的理解上或许更为重要。后面的讨论中,除非特殊的需求,我们将不明确的指明采用哪一种模型,都以分离器相称。