IO模型介绍五种常用的IO模型:阻塞IOnonblockingIOIO多路复用signaldrivenIOasynchronousIO先说一下IO发生涉及的对象和步骤:对于一个网络IO(这里我们以read为例),它会涉及到两个系统对象:一个是调用这个IO的进程(或线程),一个是系统内核(kernel)。当一个读操作发生时,会经历两个阶段:waiting数据准备,比如accept(),recv()等待数据(Waitingforthedatatobeready)将数据从内核拷贝到进程中,比如accept()接收到请求,recv()接收到连接发送的数据后需要复制到内核,然后从内核复制数据到进程用户空间(Copyingthedatafromthekerneltotheprocess)对于socket流,数据的流转经历了两个阶段:第一步通常是等待网络上的数据包到达,然后复制到内核中的缓冲区中。第二步将数据从内核缓冲区复制到应用程序进程缓冲区。记住这两点很重要,因为这些IOModel的区别在于两个阶段有不同的情况。阻塞I/O(blockingIO)在Linux中,所有套接字默认都是阻塞的。一个典型的读操作流程大概是这样的:当用户进程调用recvfrom系统调用时,内核启动第一个IO第一阶段:准备数据(对于网络IO来说,很多时候一开始数据还没有到达。例如,一个完整的UDP包还没有收到,这时候内核要等待足够的数据到达)。这个过程需要等待,也就是说数据复制到操作系统内核的缓冲区需要一个过程。在用户进程端,整个进程都会被阻塞(当然是进程自己选择阻塞)。当内核等到数据准备好后,会将内核中的数据拷贝到用户内存中,然后内核返回结果,用户进程释放block状态,重新开始运行。所以,阻塞IO的特点就是在IO执行的两个阶段都被阻塞。linux下的非阻塞I/O(nonblockingIO),可以通过设置socket使其成为非阻塞的。当对非阻塞套接字执行读操作时,流程是这样的:当用户进程发出读操作时,如果内核中的数据还没有准备好,则不会阻塞用户进程,而是立即返回错误.从用户进程的角度来看,它发起读操作后,不需要等待,而是立即得到结果。当用户进程判断结果为错误时,就知道数据没有准备好,可以再次发送读操作。一旦内核中的数据准备好,再次收到用户进程的系统调用,就立即将数据拷贝到用户内存中,然后返回。所以,非阻塞IO的特点就是用户进程需要不断主动询问内核数据是否就绪。值得注意的是,此时的非阻塞IO只应用于等待数据。当真正的数据到来执行recvfrom时,还是同步阻塞IO。从图中kernel向user拷贝数据可以看出,I/OMultiplexing(IO多路复用)IO多路复用就是我们所说的select、poll、epoll。在某些地方,这种IO方式也被称为事件驱动IO。select/epoll的优点是单个进程可以同时处理多个网络连接的IO。它的基本原理是select、poll、epoll函数会不断轮询它负责的所有socket,当某个socket有数据到达时通知用户进程。该图实际上与阻塞IO的图并没有什么不同,事实上,它更糟。因为这里需要用到2个系统调用(select和recvfrom),而阻塞IO只调用1个系统调用(recvfrom)。但是,使用select的好处是它可以同时处理多个连接。因此,如果处理的连接数不是很高,使用select/epoll的webserver不一定比使用多线程+阻塞IO的webserver性能好,延迟可能会更大。select/epoll的优势不在于处理单个连接速度快,而在于可以处理更多的连接。)在IO多路复用模型中,在实践中,对于每个socket,一般都设置为非阻塞,因为只有设置为非阻塞,单个线程/进程才不会被阻塞(或锁定),可以继续处理其他插座。如上图所示,实际上整个用户进程一直处于阻塞状态。只是进程是由functionblock选择的,而不是socketIO给block的。当用户进程调用select时,整个进程会被阻塞,同时所有传入的连接socket都会被添加到select监控列表中,内核会“监控”所有负责select的socket,然后select(poll、epoll等)函数会不断轮询所有负责的socket。这些套接字是非阻塞的,存在于select监控列表中。Select通过一定的监听机制来检查某个socket是否有数据。准备就绪后,选择将返回。这时用户进程调用read操作将数据从内核拷贝到用户进程。点评:I/O多路复用的特点是一个进程可以通过一种机制同时等待多个文件描述符,其中任意一个文件描述符(套接字描述符)进入read-ready状态,select()函数可以返回。所以,IO多路复用在本质上是不具备并发功能的,因为任何时候仍然只有一个进程或线程在工作。之所以能提高效率,是因为selectepoll把传入的sockets放到了它们的'monitoring'列表里面,当任何一个socket有可读可写的数据时,都会立即处理。如果selectepoll同时检测到很多socket,只要有动静就会回到进程处理。当然,也可以使用多线程/多进程模式。一个连接开启一个进程/线程进行处理,这样消耗的内存和进程切换页面会消耗更多的系统资源。因此,我们可以结合IO多路复用和多进程/多线程来实现高性能并发。IO多路复用负责提高接收socket通知的效率。收到请求后,交给进程池/线程池处理逻辑。AsynchronousI/O(异步IO)linux下的异步IO其实很少用到。我们先来看看它的流程:用户进程发起读操作后,就可以马上开始做其他事情了。另一方面,从内核的角度来看,当它接收到一个异步读取时,它会先立即返回,因此它不会为用户进程产生任何块。然后,内核会等待数据准备完成,然后将数据拷贝到用户内存中。当这一切完成后,内核会向用户进程发送一个信号,告诉它读操作已经完成。阻塞IO、非阻塞IO和同步IO、异步IO和接触的区别BlockingIOVSnon-blockingIO:概念:阻塞和非阻塞关注的是程序在等待调用结果(消息、返回)时的状态价值)。阻塞调用是指在返回调用结果之前,当前线程将被挂起。调用线程只有在得到结果后才会返回。非阻塞调用是指调用不会阻塞当前线程,直到不能立即得到结果。例:你打电话给书店老板问有没有《分布式系统》这本书。如果你以阻塞的方式调用它,你会自己“挂掉”,直到得到这本书是否可用的结果。如果是非阻塞调用,你不管老板有没有告诉你,你先去玩,当然你要偶尔过几分钟看看老板有没有返回结果。这里阻塞和非阻塞与是同步还是异步无关。这与老板如何回答你的结果无关。分析:阻塞IO会阻塞相应的进程,直到操作完成,而非阻塞IO会在内核还在准备数据时立即返回。SynchronousIOVSAsynchronousIO:概念:同步和异步同步和异步关注的是消息通信机制(同步通信/异步通信)所谓同步就是当一个调用发出后,直到得到结果后调用才会返回.但是一旦调用返回,你就会得到返回值。也就是说,由调用者主动等待本次调用的结果。异步则相反。发出调用后,调用直接返回,所以没有返回结果。换句话说,当发出异步过程调用时,调用者不会立即得到结果。相反,在调用发出后,被调用者通过状态和通知通知调用者,或者通过回调函数处理调用。Node.js等典型的异步编程模型举了一个通俗的例子:你打电话给书店老板,问他有没有这本书《分布式系统》。如果是同步通讯机制,书店老板会说,等一下,“我查一下”,然后开始检查检查,等待检查(可能是5秒,也可能是一天)告诉你结果(返回结果)。至于异步通信机制,书店老板直接跟你说我查,查完我给你打电话,然后直接挂断(不返回结果)。然后查一下,他就会主动给你打电话。这里老板用“回电”来回电。分析:在解释同步IO和异步IO的区别之前,需要先给出两者的定义。Stevens给出的定义(实际上是POSIX的定义)是这样的:同步I/O操作导致请求进程阻塞,直到该I/O操作完成;异步I/O操作不会导致请求进程被阻塞;两者的区别在于同步IO在做“IO操作”时会阻塞进程。按照这个定义,前面提到的阻塞IO、非阻塞IO、IO多路复用都是同步IO。可能有人会说,非阻塞IO是不阻塞的。这里有一个很“狡猾”的地方。定义中所指的“IO操作”是指真正的IO操作,即示例中的系统调用recvfrom。非阻塞IO在执行recvfrom系统调用时,如果内核数据还没有准备好,此时进程不会被阻塞。但是,当内核中的数据准备好后,recvfrom会将数据从内核中复制到用户内存中。这个时候进程就被阻塞了。在此期间,进程被阻塞。异步IO则不同。当进程发起IO操作时,直接返回并忽略,直到内核发送信号告诉进程IO完成。在这整个过程中,进程完全没有被阻塞。IO模型形象举例最后再举几个不恰当的例子来说明四种IO模型:有四个人A、B、C、D钓鱼:A用的是最老的鱼竿,所以,你得守望,待鱼上钩后,再拉竿。B的鱼竿有一个功能,可以显示有没有鱼上钩,于是,B和旁边的MM聊了聊,然后查看有没有鱼上钩。然后他迅速拉动杆子;C用的和B一样的鱼竿,不过他想到了一个好办法,就是同时放好几根鱼竿,然后站在一边。一旦有显示说上钩了,就会移动相应的鱼竿。拉起;D是个有钱人,所以他就雇了一个人帮他钓鱼,那人钓到鱼后,就给D发了一条短信。select/Poll/Epoll轮询机制select、poll、epoll本质上都是同步I/O,因为它们都需要在读写事件就绪后负责读写,也就是说读写过程是阻塞的Select/Poll/Epoll都是IO多路复用的实现。上面说过,使用IO多路复用会将socket设置为非阻塞,然后放入Select/Poll/Epoll各自的监听列表中。那么,它们对socket数据到达的监听机制究竟是怎样的呢?效率呢?我们应该用什么方法来实现IO多路复用呢?下面列出它们各自的实现方式、效率和优缺点:(1)select和poll实现需要不断轮询所有fd集合,直到设备就绪,期间sleep和wake-up可能交替进行。其实epoll还需要调用epoll_wait来不断轮询就绪链表。期间sleep和wakeup可能会交替多次,但是当设备就绪时,调用回调函数,将就绪的fd放入就绪链表,在epoll_wait进程中唤醒到sleep。虽然sleep和alternate,select和poll在“唤醒”的时候都要遍历整个fd集合,而epoll在“唤醒”的时候只需要判断就绪链表是否为空,这样就节省了大量的CPU时间。这就是回调机制带来的性能提升。(2)select,poll每次调用都需要将fd集合从用户态复制到内核态一次,并将current挂到设备等待队列一次,而epoll只需要复制一次,并将current挂到等待队列.只挂一次(epoll_wait开头,注意这里的等待队列不是设备等待队列,而是epoll内部定义的等待队列)。这也可以节省很多开销。
