本文转载自微信公众号《潜行》,作者cscw。转载本文请联系SneakUp公众号。前言NetworkI/O可以理解为网络上的数据流。通常我们会根据socket和远端建立一个TCP或者UDP通道,然后进行读写。对于单个socket,可以使用一个线程进行高效处理;但是,如果有10K或更多的socket连接,我们如何实现高性能处理呢?基本概念介绍了网络I/O的读写过程。/O模型多路复用I/O深入理解一波Reactor模型Proacotr模型基本概念进程(线程)切换介绍所有系统都具有进程调度能力,可以暂停一个当前正在运行的进程,恢复之前暂停的进程被启动进程阻塞的进程(线程)有时会等待其他事件的执行完成,如等待锁、请求I/O读写;进程在等待过程中会被系统自动阻塞,进程不会占用CPU文件描述符在Linux中,文件描述符是一个抽象概念,用来表示对文件的引用,是一个非负整数.当程序打开现有文件或创建新文件时,内核会向进程返回一个文件描述符。linux信号处理Linux进程在运行过程中可以接受来自系统或进程的信号值,然后根据信号值运行相应的捕获函数;signal相当于硬件中断的软件模拟在零拷贝机制章节中已经介绍了用户空间、内核空间和buffer,这里省略网络IO的读写过程。导致上下文切换,用户进程阻塞(R1)等待网络数据流到达,并从网卡拷贝到内核;(R2)然后从内核缓冲区复制到用户进程缓冲区。此时进程切换恢复,对获取到的数据进行处理。这里我们给socket读操作的第一阶段起一个别名R1,第二阶段就叫R2。阻塞等待(1)数据从用户进程缓冲区复制到内核缓冲区。数据拷贝完成。这时进程切换恢复了linux的五种网络IO模型。阻塞I/O(阻塞IO)ssize_trecvfrom(intsockfd,void*buf,size_tlen,unsignedintflags,structsockaddr*from,socket_t*fromlen);最基本的I/O模型就是阻塞I/O模型,也是最简单的模型。所有操作都按顺序执行。在阻塞IO模型中,用户空间的应用程序执行系统调用(recvform),导致应用程序阻塞,直到内核缓冲区中的数据准备好,数据从内核复制到用户。过程。最后进程被系统唤醒处理数据。在R1和R2这两个连续的阶段,整个进程都被非阻塞I/O(非阻塞IO)阻塞。非阻塞IO也是同步IO的一种。它基于轮询机制实现,套接字以非阻塞方式打开。也就是说,I/O操作不会立即完成,而是I/O操作会返回一个错误码(EWOULDBLOCK),表示操作没有完成。轮询检查内核数据,如果数据没有准备好,返回EWOULDBLOCK。该进程继续启动recvfrom调用。当然也可以暂停做其他事情,直到内核数据准备好,然后将数据拷贝到用户空间,然后进程拿到非错误码数据,再进行数据处理。需要注意的是,在复制数据的整个过程中,进程始终处于阻塞状态。进程阻塞在R2阶段。虽然在R1阶段没有阻塞,但是需要不断轮询多路复用的I/O(IO多路复用)。一般后端服务会有大量的socket连接,如果能一次查询多个socket的读写状态,如果有一个就绪,再处理,效率会高很多。这就是“I/O多路复用”。多路复用是指多个套接字,多路复用是指多路复用同一个进程。Linux提供了select、poll和epoll等多路复用I/O实现。select、poll、epoll的方法与阻塞IO不同。select不会等到所有的socket数据都到达了才处理,而是在部分socket数据准备好后,再恢复用户进程去处理。你怎么知道一些数据在内核中准备好了?答:交给系统吧。该进程也在R1和R2阶段被阻塞;但是在R1阶段有一个技巧。在多进程多线程编程的环境下,我们是不是只能分配一个进程(线程)来阻塞对select的调用,其他线程就可以解放出来了?Signal-drivenI/O(SIGIO)需要提供一个信号捕获函数,并将其与socket套接字关联起来;发起sigaction调用后,进程就可以腾出来处理其他事情了。当数据在内核准备好后,进程会收到一个SIGIO信号,然后中断运行信号捕获函数,调用recvfrom将数据从内核读取到用户空间,然后处理数据到用户空间进程在R1阶段不会阻塞,但R2仍会阻塞等待异步IO(POSIX的aio_series函数)。与同步IO相比,异步IO是在用户进程发起异步读(aio_read)系统调用后,无论内核缓冲区数据是否就绪,当前进程都不会被阻塞;aio_read系统调用返回后,进程可以处理其他逻辑套接字数据。当内核就绪后,系统直接将数据从内核拷贝到用户空间,然后用信号通知用户进程R1和R2,在两个阶段,进程是非阻塞多路复用IO理解一波选择intselect(intnfds,fd_set*readfds,fd_set*writefds,fd_set*exceptfds,structtimeval*timeout);1)使用copy_from_user将fd_set从用户空间拷贝到内核空间2)注册回调函数__pollwait3)遍历所有fds,调用相应的poll方法(对于socket,这个poll方法为sock_poll,sock_poll会根据调用tcp_poll,udp_poll或datagram_poll情况)4)以tcp_poll为例,它的核心实现是__pollwait,也就是上面注册的回调函数5)__pollwait的主要工作是把current(当前进程)挂在设备的等待队列中。不同的设备有不同的等待队列。对于tcp_poll来说,等待队列是sk->sk_sleep(注意,进程挂在等待队列不代表进程已经休眠)。设备接收到消息(网络设备)或填写文件数据(磁盘设备)后,会唤醒队列上等待休眠进程的设备,然后唤醒当前。6)poll方法返回的时候,会返回一个描述读写操作是否就绪的mask掩码,并根据这个mask掩码给fd_set赋值7)如果所有的fd都已经遍历过,并且有一个可读可读的可写掩码掩码尚未返回,schedule_timeout将被调用。调用select(即current)的进程进入sleep8)当设备驱动有自己的资源可读可写时,就会唤醒休眠在waitingqueue上的进程。如果经过一定的超时时间(timeout指定)没有人醒来,调用select的进程会被再次唤醒获取CPU,然后再次遍历fd判断是否有就绪的fd9)从内核空间copyfd_set到userspaceselect的缺点是每次调用select时,都需要将fd集合从用户态复制到内核态。当fd很多的时候这个开销会很大。同时,每次调用select时,都需要遍历内核中传入的所有fds。这个开销在fd中很多select支持的文件描述符数量太少,默认是1024epoll_create(intsize);intepoll_ctl(intepfd,intop,intfd,structepoll_event*event);intepoll_wait(intepfd,structepoll_event*事件,intmaxevents,inttimeout);调用epoll_create,会在kernelcache中建立一个红黑树来存放epoll_ctl以后发送的sockets,同时建立一个rdllist双向链表来存放就绪事件。调用epoll_wait时,只看rdllist双向链表的数据。epoll_ctl向epoll对象添加、修改、删除事件时,是在rbr红黑树中进行操作的。很快添加到epoll的事件会和设备(如网卡)建立回调关系,当设备上有相应的事件发生时会调用回调方法,并将该事件双重添加到rdllist中链表;这个回调方法在内核中叫做ep_poll_callback。两种触发模式epoll有EPOLLLT和EPOLLET两种触发模式,LT是默认模式,ET是“高速”模式(只支持no-blocksocket)在LT(leveltrigger)模式下,只要文件描述符还在有数据要读,每次epoll_wait都会触发它的读事件ET(edgetrigger)模式,当检测到I/O事件时,会通过epoll_wait调用获取带有事件通知的文件描述符。对于文件描述符,如果是可读的,必须读取文件描述符直到为空(或者返回EWOULDBLOCK),否则接下来的epoll_wait不会触发事件epoll相对于select的优点解决select的三个缺点对于第一个缺点:epoll的解决方案在epoll_ctl函数中。每次有新的事件注册到epoll句柄(在epoll_ctl中指定EPOLL_CTL_ADD),所有的fds都会被复制到内核中,而不是在epoll_wait期间重复复制。epoll保证每个fd在整个过程中只会被复制一次(epoll_wait不需要复制)对于第二个缺点:epoll为每个fd指定了一个回调函数,当设备就绪时,唤醒waiting上的waitersqueue,它会调用这个回调函数,这个回调函数会将就绪的fd加入到一个就绪列表中。epoll_wait的工作其实就是检查这个就绪链表中是否有就绪的fd(不需要遍历)对于第三个缺点:epoll没有这个限制,它支持的fd上限是最大的文件数可以打开,这个数字一般比2048大很多。比如在1GB内存的机器上,大概是10万。一般来说,这个数字与系统内存有很大关系。epoll的高性能epoll使用红黑树存储需要监听的文件描述符事件,epoll_ctl增删改查快速。epoll不需要遍历就可以得到就绪fd,可以直接返回就绪链表。linux2.6之后,使用mmap技术,不再需要从内核copy数据到用户空间。epoll的零拷贝IO模型同步异步问题概念定义同步I/O操作:导致请求进程阻塞,直到I/O操作完成异步I/O操作:不导致请求进程阻塞,异步只处理I/O操作完成后的通知,不主动读写数据,系统内核完成数据读写阻塞,非阻塞:进程/线程要访问的数据是否是ready,进程/线程是否需要等待异步IO。该概念需要非阻塞I/O调用。前面介绍过,I/O操作分为两个阶段:R1等待数据准备好。R2将数据从内核复制到进程。epoll虽然在2.6内核之后采用了mmap机制,使得在R2阶段不需要copy,但是在R1阶段还是阻塞了。因此,归类为同步IOReactor模型的Reactor的中心思想是将所有需要处理的I/O事件注册到一个中央I/O多路复用器上,而主线程/进程阻塞在多路复用器上;一旦有I/O事件到达或就绪,多路复用器返回,将预先注册的相应I/O事件分发给相应的处理器。相关概念介绍:Event:是状态;例如:readready事件是指最重要的是我们可以从内核中读取数据的状态事件分隔符:一般情况下,等待事件都会交给epoll和select;并且事件的到来是随机的、异步的,所以需要循环调用epoll,并相应封装在框架中模块是事件分隔符(简单理解为epoll封装)事件处理器:事件发生后,需要通过一个进程或线程。这个处理程序就是事件处理程序,一般和事件分隔符不同。Reactor的一般流程1)应用程序在事件分隔符中注册读写就绪事件和读写就绪事件处理器2)事件分隔符等待读写就绪事件发生3)读写就绪事件发生,激活事件分离器,分离器调用读写就绪事件处理器4)事件处理器先从内核读取数据到用户空间,然后对数据进行处理单线程+Reactor多线程+Reactor多线程+多个ReactorProactor模型大体流程1)应用在事件分离器Completion事件和读取完成事件处理器中注册读取,向系统发送异步读取请求2)事件分离器等待读取事件完成3)在splitter的等待过程中,系统使用并行内核线程执行实际的读操作,并将数据复制到进程缓冲区,最后通知事件separator读取完成。4)事件分离器监听读完成事件,激活读完成事件的处理器。5)读取完成事件处理器直接处理用户进程缓冲区中的数据Proactor和Reactor的区别在于Proactor是基于异步I/O的概念,而Reactor一般是基于多路复用I/O的概念。Proactor不需要将数据从内核复制到用户空间。此步骤由系统完成。文中有错的地方欢迎指正文章讲Linux的五种IO模型[1]网络IO模型[2]网络IO[3]五种网络IO模型[4]epoll原理详解及epollreactor模型[5]参考资料[1]说说Linux的五种IO模型:https://www.jianshu.com/p/486b0965c296[2]网络io模型:https://www.jianshu.com/p/a95bcb116765[3]网络IO:https://www.cnblogs.com/hesper/p/11547263.html[4]五种网络IO模型:https://www.cnblogs.com/findumars/p/6361627.html[5】epoll原理及epollreactor模型详解:https://blog.csdn.net/daaikuaichuan/article/details/83862311
