文章目录

一、同步阻塞(blocking)

Unix/Linux上的五种I/O模型总结_应用程序


应用程序调用read,在内核的TCP接收缓冲区有数据之前,应用程序是一直阻塞的。内核的TCP接收缓冲区有数据时,应用程序把内核缓冲区的数据自行拷贝到用户空间,拷贝完成,read才返回

效率不高,编程简单

二、同步非阻塞(non-blocking)

Unix/Linux上的五种I/O模型总结_tcp/ip_02


应用进程调用read之前,用setsockopt这个方法把sockfd设置成non-blocking,在数据未就绪状态下,它是不断的返回

非阻塞情况下,我们一般这样判断

  • ​size==-1​​,表示错误,是系统的内部错误,需要查看错误码errno,可能要close(sockfd)
  • ​size==0 && errno==EAGAIN​​,表示正常的非阻塞返回,sockfd上没有数据,但是连接正常
  • ​size==0 && 无errno​​,表示网络对端关闭了连接,对端close(sockfd)
  • ​size>0​​,表示sockfd上有数据

如果数据到达内核空间TCP接收缓冲区,应用程序会去缓冲区拷贝数据到用户空间,花费的还是应用程序的时间,所以这一过程是同步的

三、IO复用(IO multiplexing)

Unix/Linux上的五种I/O模型总结_应用程序_03


select/poll/epoll都是可以设置timeout超时时间的

  • 如果不设置, 相当于工作在阻塞模式下,事件未发生时,调用I/O复用接口的进程线程是被阻塞的
  • 设置超时时间,相当于工作在非阻塞模式下,同样去检测返回值是不是EAGAIN,是错误返回还是正常非阻塞的返回

数据准备好以后,返回可读的fd以及发生事件的event,接下来查看event,对event调用accept或者read

上图中发生的是已建立连接的读写事件,发起系统调用read,应用程序自己从内核tcp接收缓冲区拷贝到用户空间,还是要耗费应用程序的时间,故这也是一个同步的过程

如果说一个线程只能处理一个socket,那就是每个客户端,服务器就得分配一个线程进行服务。在某些平台,客户端的数量是巨大的,服务器不可能开启这么多线程。有了I/O复用,服务器就不用分配那么多服务线程了,因为一个线程调用I/O复用接口后,可以监听很多的套接字

应用程序调用I/O复用接口后,会根据I/O复用接口返回的fd以及event调用相应的处理事件的接口,比如accept或者read,好处是在一个线程调用一个I/O复用接口后,可以监听很多的套接字(高并发),当多个套接字有数据可读时,I/O复用会给应用程序返回可读或者可写的socket列表,然后应用程序根据I/O复用返回的fd和event进行相应的读写操作

四、信号驱动(signal-driven)

Unix/Linux上的五种I/O模型总结_复用_04


上图中,数据准备阶段是异步的,数据读写阶段是同步的

数据准备阶段:应用程序向内核注册了通知信号,不需要进程像在非阻塞IO模式下不断轮询,或是在阻塞模式下一直等待。当数据到达应用程序指定fd对应的tcp的接收缓冲区后,内核会通知应用程序,你感兴趣的数据到了。在收到这个通知前,应用程序可以处理其他的逻辑,所以这是一个异步的过程

数据读写阶段:当数据到来,应用程序收到内核通知后,应用程序自己去tcp的接收缓冲区取数据,数据取完,才能继续向下执行,这是一个同步的过程

五、异步非阻塞(asynchronous)

异步非阻塞模型:数据准备阶段和数据读写阶段都异步的

Unix/Linux上的五种I/O模型总结_复用_05


当我们应用程序调用异步I/O接口的时候,我们就把sockfd,buff,SIGIO(通知方式,也可以通过回调,我们在这里用的是信号)通过异步I/O接口都塞给了操作系统

在内核通知应用程序前,应用程序做自己的任何事情都可以。当操作系统通过SIGIO通知应用层的时候,buff中就已经有数据了,应用程序不用自己去TCP缓冲区搬数据,不用像I/O同步一样一直阻塞等待或者非阻塞空转

一般来说,我们采用异步模型,就是希望别人(内核或其他程序)能帮我们当前的应用程序做好一切的准备工作,包括接收数据,拷贝数据等,这就可以释放我们当前应用程序,不用去等待数据到来

我们若采用异步阻塞,这就违背了我们的初衷,所以一般不采用异步阻塞模型。而Node.js就是典型的异步非阻塞网络IO模型,效率最高,编程最复杂,出问题最难排查

陈硕:在处理IO时,阻塞和非阻塞都是同步IO,只有使用了特殊的API才是异步IO

六、设计良好的网络服务器

在这个多核时代,服务端网络编程如何选择线程模型呢?

libev作者的观点:one loop per thread is usually a good model,这样多线程服务端编程的问题就转换为如何设计一个高效且易于使用的event loop,然后每个线程run一个event loop就行了(当然线程间的同步、互斥少不了,还有其它的耗时事件需要起另外的线程来做)

一般不会使用I/O复用 + blocking

假设我们使用了I/O复用 + 阻塞socket,当有数据到达阻塞sockfd对应的tcp缓冲区,应用程序读取这个sockfd,读完数据的时候,当前线程就阻塞在该sockfd了,由于I/O复用是单线程处理多个socket,阻塞在上一个sockfd了,就无法再处理下一个有事件发生的sockfd了,也就无法再次进入epoll_wait监听socket了。

一般使用I/O复用 + non-blocking

如果使用的I/O复用 + 非阻塞socket,应用程序读取某个sockfd,读完数据的时候,当前线程不会阻塞在该sockfd,会正常返回,接着处理下一个事件发生的sockfd了,然后进入epoll_wait继续监听socket

不会单独使用non-blocking

因为数据还没有准备好,应用程序还是不断轮询查看socket是否有数据到来,这会浪费CPU资源,导致服务器效率低下

所以当我们提到 non-blocking 的时候,实际上指的是 non-blocking + IO-multiplexing,单用其中任何一个都不能很好的实现功能,一般我们会使用non-blocking + IO-multiplexing + Thread Pool,比如muduo、netty都是典型的这种架构

Unix/Linux上的五种I/O模型总结_复用_06


nginx使用了多进程监听新用户的连接,不像non-blocking + IO-multiplexing + Thread Pool一般只有一个IO线程监听新用户的连接,其他工作线程处理已连接用户的读写事件