网络IO模型有5种:阻塞IO、非阻塞IO、IO多路复用、信号驱动IO、异步IO
本文仅对阻塞IO、非阻塞IO、IO多路复用进行讲解
在网络通信中,对数据进行收发在发送端和接收端的相应设备上会进行如下顺序的传递:
发送端应用的发送缓冲区->发送端操作系统的内核缓冲区->发送端网卡->网线->接收端网卡->接收端操作系统的内核缓冲区->接收端应用的接收缓冲区
对数据进行写入时,应用程序调用write/sendto等相关系统调用将数据发送到接收端的接收缓冲区;在读取数据时,应用程序调用read/recvfrom等相关系统调用将数据从接收缓冲区搬运到用户区。
以数据的读取为例,在调用read等系统调用时,会经历以下两个阶段:
(1)等待数据到达内核接收缓冲区
(2)将数据从接收缓冲区搬运到用户区
阻塞式IO在进行数据读取时,如果内核中没有数据(发送端可能还没有发送数据或者发送的数据还没有达到),此时内核就开始等待数据,同时用户进程也进入阻塞状态,整个进程就会被挂起等待,不能做其他的事情。当有数据到达内核时,内核等待结束,将数据从内核拷贝到用户区,用户进程结束阻塞,从挂起状态转为运行状态。
所以,阻塞式IO在进行数据读取时,上述两个阶段都会阻塞(内核等待数据,用户进程阻塞)。
在Linux中,默认所有的socket都是阻塞式的。阻塞式接口是指当进行系统调用时,如果数据没有准备好,该应用进程就会被挂起,系统调用不会返回,直到有数据达到或者调用出错时,系统调用才会返回,进程才会结束阻塞状态。
但是在网络编程中,一般服务器需要处理多客户端,如果是像这种单进程服务器。与一个客户端连接建立之后,服务器就会使用read等系统调用对客户端进行数据读取来处理请求。当该客户端没有发送数据或者发送的数据还没有达到时,服务器就会进入阻塞状态,此时整个服务器进程就会挂起。当其他客户端连接请求达到时,服务器由于处于挂起状态,什么也不能做,所以也不能对其他客户端进行处理。因此,这种单进程的阻塞式IO的服务器只能处理一个客户端的情况,这样的服务器没有任何的实用性。
一种解决办法是,利用多进程或多线程来处理多客户端的情形。如果在对一个客户端进行读取时导致一个进程被挂起,可以创建其他的进程来处理其他客户端的请求。但是多进程和多线程的创建需要浪费一定的资源,当有过多的连接没有数据往来的时候,会造成浪费,同时也可能造成频繁的进程上下文切换。所以多线程做法一般适用于中小型应用场景。
非阻塞式IO在使用recvfrom等系统调用进行数据读取时,如果内核中没有数据到达,此时内核会进行等待。但是与阻塞式IO不同的是,此时的用户进程并不会被阻塞,不会被挂起,而是出错返回。
但是之后内核将数据准备好之后,由于该系统调用已经返回,所以进程无法得到数据已经准备好并且无法将数据由内核拷贝到用户区。所以,此时还需要使用系统调用进行数据的拷贝。因为不知道内核什么时候将数据准备好,所以就需要不断的使用系统调用来询问内核有没有将数据准备好,一旦准备好就进行数据的拷贝。
非阻塞式IO中,一般需要循环的对文件描述符进行读写,不断的询问数据有没有准备好。这个过程就称为轮询。因此,在非阻塞IO中,内核在等待数据,用户进程在轮询访问数据有无准备好。
在该模型中,进程大部分的工作都是在轮询访问,如果大多数连接都没有数据到达,那么大多数轮询都是空轮旋,并没有发挥实际有效的作用,所以这样做实际是对CPU资源的一种浪费。一般在特定场景下才会使用该模型。
既然非阻塞IO中,进程会产生很多空轮询,那能不能直接操作系统告诉我们那个连接的数据准备好了,我们直接去轮询这些连接不就好了?IO多路复用就是做这一件事情
IO多路复用下,进程首先使用select/epoll等系统调用等待多个连接。select等系统调用可以设置阻塞和非阻塞(或者阻塞的时间)。如果是阻塞方式,若等待的所有连接的数据均未达到,此时进程会阻塞在select处。当至少有一个连接就绪条件满足时,该系统调用就会返回,然后应用进程再调用read等分别对就绪的连接进行数据的拷贝。如果是非阻塞方式或者设置了阻塞的时间,当没有调用select时或者在规定时间内没有连接满足就绪条件,此时select会出错返回-1,此时为了对数据进行操作,所以必须循环的调用select来判断有无连接满足就绪条件。
当等待的多个连接中至少有一个满足就绪条件时,select返回数据准备好的连接。此时应用进程调用read(此时的套接字是阻塞的)等对数据进行拷贝。此时,read一定不会阻塞。因为内核中一定有数据到达。
所以,通过上述的说明,对于单进程的服务器可以通过IO多路复用的方式来处理多客户端的情况。此时便可避免多线程阻塞IO带来的多进程或多线程的创建造成的资源方面的问题,同时可以处理多客户端。但是两种方式各有优缺点,比如在连接比较少的时候,内核这种处理显得太笨重。
数据从内核拷贝到应用进程其实还是阻塞的状态
阻塞IO需要一直等待内核数据准备好,进程处于阻塞状态;非阻塞IO一直在询问内核数据是否准备好,进程处于运行态,一直占用着CPU
数据从内核拷贝到应用进程其实还是阻塞的状态
当有多个连接的时候,非阻塞IO会不断地询问每个连接的数据是否准备好,如果准备好则调用read系统调用进行读取,但是我们上面说过,如果连接一直没有数据到,将会有许多无用的轮旋;IO多路复用则把这项工作交给操作系统,有操作系统通过与IO设备绑定的回调直接把有数据到来的连接返回给应用进程,应用进程可以直接一个个读取而无需再像非阻塞IO那样去询问没有数据的连接。
在网络刚刚诞生的时代,服务器还没有这么大的负载,用单线程阻塞IO的时候搓搓有余。
随着并发量越来越大,单线程无法支撑,人们便利用多核并发处理能力,每个连接创建一个线程进行处理,也就是多线程阻塞IO。
但是慢慢发现,创建过多线程造成很大地资源浪费,并且频繁地上下文切换也带来不菲地系统开销,因此出现了非阻塞IO(单线程多线程根据具体业务要求),线程每次询问操作系统是否准备好数据时不再向阻塞IO那样,要把自己挂起,而是每次询问,操作系统直接返回结果,有数据就返回数据,没有数据就返回异常,但同时造成大量地空沦陷,不是所有的连接都有数据到来,进程也会一直占用CPU。
因此,人们后来又发明了IO多路复用,将“轮旋”操作交给操作系统完成,操作系统通过与IO设备的回调等优化,直接返回数据准备好的连接给应用进程。典型的实现有windows的select和linux的epoll。window的select虽然也是一个个轮询每一个连接,但是每次轮询都是在内核空间下进行,不存在用户态和内核态之间的切换,相比非阻塞IO每次询问都是一次昂贵的系统调用。而在linux系统上实现的epoll则进一步优化(通过红黑树),使得可以在O(1)的时间复杂度内返回数据准备好的连接,而不需要轮询。
再后来出现的信号驱动IO和异步IO也是为了进一步解决应用场景而出现的技术,本文不再展开,有兴趣的读者可以自行查阅,一般的业务场景最多就用到IO多路复用技术。技术没有好坏,仅仅只是为了适应应用场景而被创造出来,选用什么技术也要看具体的业务场景。
计算机网络------五种IO模型
浅谈5种IO模型——阻塞式IO、非阻塞式IO、信号驱动IO、多路复用IO及异步IO
图解 | 原来这就是 IO 多路复用
四图,读懂 BIO、NIO、AIO、多路复用 IO 的区别