0%

IO 入门

[TOC]

概述

在神作《UNIX 网络编程》里,总结归纳了 5 种 I/O 模型,包括同步和异步 I/O:

  • 阻塞 I/O (Blocking I/O)
  • 非阻塞 I/O (Nonblocking I/O)
  • I/O 多路复用 (I/O multiplexing)
  • 信号驱动 I/O (Signal driven I/O)
  • 异步 I/O (Asynchronous I/O)

DtXgAJ.md.png

阻塞IO (Blocking I/O)

阻塞型IO指的是进程的IO的系统调用(recv()),会导致这个系统阻塞在这个系统调用上,直到有数据到来且系统准备好了数据之后,系统才能继续处理数据,阻塞型IO一个线程只能处理一个请求。阻塞型IO为了能同时处理多个请求,一般通过多线程技术来实现。但是线程的成本其实很高,所以阻塞模型并不是很适用高并发的场景。

非阻塞IO(non-blocking IO)

Linux下,可以通过设置socket使其变为non-blocking。非阻塞就很简单了,就是服务器不断去忙轮询系统调用(反复调用recv(),因为non-blocking的IO如果数据没有准备好,会立即返回)。 可以看到服务器线程可以通过循环调用recv()接口,可以在单个线程内实现对所有连接的数据接收工作。但是上述模型绝不被推荐。因为,循环调用recv()将大幅度推高CPU 占用率;此外,在这个方案中recv()更多的是起到检测“操作是否完成”的作用,实际操作系统提供了更为高效的检测“操作是否完成“作用的接口,例如select()多路复用模式,可以一次检测多个连接是否活跃。

多路复用IO(IO multiplexing)(事件驱动 IO)

这种IO方式为事件驱动IO(event driven IO)。select或者epoll类型的IO,当你调用select()函数时,进程会被阻塞直到有数据准备完成。数据准备完成后,需要再调用recv()函数来从系统中接受数据到用户进程(注意这是两次系统调用,一次是阻塞,一次及时返回)。IO多路复用,虽然有一个阻塞的地方,但是多路复用IO依然支持海量并发请求,这是因为,select这个系统调用会让系统内核监视select负责的所有的socket,一旦有数据准备好,就停止这个阻塞,并返回ok,然后用户进程可以再次发起请求,拿取数据。IO多路复用是一种极其高效的IO模型,使用IO多路复用,在高并发的情况下,一个进程能够将CPU的使用发挥到极致(如果在用户进程中有阻塞的地方下面会说)。但是IO多路复用不能充分发挥多核cpu的优点,因为它只使用了一个cpu(不同于多线程+阻塞IO),所以,通常的做法是多进程+多路复用IO,例如nginx和tornado。

如果在用户进程中存在阻塞的地方,那么多路复用IO的效果将大打折扣,所以nginx的主要作用就是做转发。而tornado为自己作为client去请求其他服务器时,可以开启协程模式(异步请求框架!)。熟悉go的开发者应该知道,如果有多个IO读取,多个网络请求,那么,最好是开启多个协程,通过管道来实现数据回流,这能最大化避免阻塞带来的时间消耗。

在多路复用模型中,对于每一个socket,一般都设置成为non-blocking,整个用户的process其实是一直被block的。只不过process是被select这个函数block,而不是被socket IO给block。因此select()与非阻塞IO类似。

使用select搭建的服务流程,如果select()发现某句柄捕捉到了“可读事件”,服务器程序应及时做recv()操作,并根据接收到的数据准备好待发送数据,并将对应的句柄值加入writefds,准备下一次的“可写事件”的select()探测。同样,如果select()发现某句柄捕捉到“可写事件”,则程序应及时做send()操作,并准备好下一次的“可读事件”探测准备。

多路复用IO的缺点

缺点1:该模型将事件探测和事件响应夹杂在一起,一旦事件响应的执行体庞大,则对整个模型是灾难性的。真实案例,线上的tornado服务,和文件上传,下载的服务放在了一起,大量的用时极长的上传下载操作阻塞了所有的进程,最终导致整个服务死掉。

缺点2:select()接口并不是实现“事件驱动”的最好选择。因为当需要探测的句柄值较大时,select()接口本身需要消耗大量时间去轮询各个句柄。很多操作系统提供了更为高效的接口,如linux提供了epoll,BSD提供了kqueue,Solaris提供了/dev/poll。epoll和select相比好的地方在于,epoll会向每一个socket注册回调,一旦socket数据准备完成,就会通知epoll()函数,epoll函数再返回,好处就是轮训少了一点,但是阻塞还是在的。

信号驱动 I/O (Signal driven I/O)

信号驱动IO(signal-driven IO),使用信号机制,让内核在描述符就绪时发送SIGIO信号通知用户进程。整个过程是先通过sigaction系统调用安装一个信号处理函数。该系统调用将立即返回,用户进程继续工作,也就是说它没有被阻塞。当数据报准备好读取时,内核就为该进程产生一个SIGIO信号,我们随后可以在信号处理函数中调用recvfrom读取内核空间准备好的数据。特点:第一阶段(等待数据报到达期间)进程不被阻塞。

从上图中可以看出,信号驱动IO第一步非阻塞,通过sigaction系统调用达到目标。

异步 I/O (Asynchronous I/O)

用户进程发起read操作之后,立刻就可以开始去做其它的事。而另一方面,从kernel的角度,当它受到一个asynchronous read之后,首先它会立刻返回,所以不会对用户进程产生任何block。然后,kernel会等待数据准备完成,然后将数据拷贝到用户内存,当这一切都完成之后,kernel会给用户进程发送一个signal,告诉它read操作完成了。

IO总结

调用blocking IO会一直block住对应的进程直到操作完成,而non-blocking IO在kernel还在准备数据的情况下会立刻返回。

1
2
3
A synchronous I/O operation causes the requesting process to be blocked until that I/O operation completes;

An asynchronous I/O operation does not cause the requesting process to be blocked;

tornado

tornado 作为服务端,其编程模式是多进程+IO多路复用,这种编程模型的好处是能支持10k及以上的并发请求,缺点是一旦一个请求占用时间过长,那么整个系统的可能因为其他请求直接死掉,因为没有其他的线程(worker)来工作了。tornado作为请求的client(tornado也是一个http请求库),他可以实现异步请求。

关于tornado(python的一个web框架),很多文章都在说它是一个异步框架啦,巴拉巴拉,这句话其实不对(只对了一半的话能叫对吗?)官方的说法其实很明显了,tornado是一个非阻塞的web框架(作为服务端,non-blocking不等于异步哦),同时是一个异步请求库(作为客户端的库)。

1
Tornado is a Python web framework and asynchronous networking library, originally developed at FriendFeed. By using non-blocking network I/O, Tornado can scale to tens of thousands of open connections, making it ideal for long polling, WebSockets, and other applications that require a long-lived connection to each user.

tornado 实际上和nginx是一个编程模型,优先使用IO多路复用的IO模型(select, epoll),但是请注意,IO多路复用不是异步IO!IO多路复用的显著特点是这种IO模型可以让系统调用同时管理数以万计的请求,实现单进程也可以支持大量并发请求的特性!IO多路复用的模型很应该和Nonblocking IO做对比,实际上IO多路复用是另一种形式的Nonblocking IO。

参考

http://blog.chinaunix.net/uid-28458801-id-4464639.html

https://github.com/tornadoweb/tornado