前言网络I/O可以理解为网络上的数据流。通常我们会根据socket和远端建立一个TCP或者UDP通道,然后进行读写。对于单个socket,可以使用一个线程进行高效处理;但是,如果有10K或更多的socket连接,我们如何实现高性能处理呢?基本概念介绍网络I/O的读写过程linux下五种网络I/O模型MultiplexedI/O深入理解一波Reactor模型Proacotr模型关注公众号,一起交流:潜行到github地址,感谢star介绍了进程(线程)切换的基本概念*所有系统都有进程调度的能力,它可以挂起一个当前正在运行的进程,恢复之前挂起的进程(线程)的阻塞*正在运行的进程,有时会等待其他事件的执行完成,比如等待锁,请求I/O读写;进程在等待过程中会被系统自动阻塞,此时进程不占用CPU文件描述符*在Linux中,文件描述符是一个用来表达指向文件引用的抽象概念,是一个非负整数。当程序打开已有文件或创建新文件时,内核返回一个文件描述符给进程linuxsignalprocessing*Linux进程在运行过程中可以接受来自系统或进程的信号值,然后根据运行相应的捕获函数到信号值;该信号相当于硬件中断的软件模拟。在零拷贝机制章节中,已经介绍了用户空间、内核空间和缓冲区。这里省略网络IO读写过程。当在用户空间发起socket套接字的读操作时,会引起上下文切换,用户进程阻塞(R1)等待网络数据流的到来,并从网卡复制到内核;(R2)然后从内核缓冲区复制到用户进程缓冲区。此时进程切换恢复,对获取到的数据进行处理。这里我们给socket读操作的第一阶段起一个别名R1,第二阶段就叫R2。阻塞等待(1)数据从用户进程缓冲区复制到内核缓冲区。数据拷贝完成。这时进程切换恢复了linux的五种网络IO模型。阻塞I/O(blockingIO)ssize_trecvfrom(intsockfd,void*buf,size_tlen,unsignedintflags,structsockaddr*from,socket_t*fromlen);最基本的I/O模型是阻塞I/O模型,也是最简单的模型。所有操作都按顺序执行。在阻塞IO模型中,用户空间的应用程序执行一个系统调用(recvform),这会导致应用程序被阻塞,直到内核缓冲区中的数据准备好,数据从内核复制到用户。过程。最后进程被系统唤醒处理数据。在R1和R2这两个连续的阶段,整个过程都是阻塞的。Non-blockingI/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(POSIXaio_系列函数)相对于同步IO,异步IO在用户进程发起异步读(aio_read)系统调用后不会阻塞当前进程,无论内核缓冲区数据是否就绪;在aio_read系统调用返回后,进程可以处理其他逻辑套接字数据。当内核就绪后,系统直接将数据从内核拷贝到用户空间,然后用信号通知用户进程。R1和R2这两个阶段,过程是非阻塞多路复用IO深入理解一波selectintselect(intnfds,fd_set*readfds,fd_set*writefds,fd_set*exceptfds,structtimeval*timeout);1)使用copy_from_user将fd_set从用户空间拷贝到内核空间2)注册回调函数__pollwait3)遍历所有fd,调用其对应的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(注意进程Hang在等待队列中不代表进程已经休眠)。设备接收到消息(网络设备)或填写文件数据(磁盘设备)后,会唤醒队列上等待休眠进程的设备,然后唤醒当前。6)poll方法返回的时候,会返回一个描述读写操作是否就绪的mask掩码,并根据这个mask掩码给fd_set赋值7)如果所有的fd都已经遍历过,并且有一个可读可读的可写掩码掩码尚未返回,schedule_timeout将被调用。调用select(即current)的进程进入sleep8)当设备驱动有自己的资源可读可写时,就会唤醒休眠在waitingqueue上的进程。如果经过一定的超时时间(timeout指定)没有人醒来,调用select的进程会被再次唤醒获取CPU,然后再次遍历fd判断是否有就绪的fd9)将fd_set从内核空间复制到userspaceselect的缺点是每次调用select时,都需要将fd集合从用户态复制到内核态。当fd很多的时候这个开销会很大。同时,每次调用select时,都需要遍历内核中传入的所有fds。这个开销在fd中很多select支持的文件描述符数量太少,默认是1024epollintepoll_create(intsize);intepoll_ctl(intepfd,intop,intfd,structepoll_event*event);intepoll_wait(intepfd,structepoll_event*events,intmaxevents,inttimeout);调用epoll_create会在内核缓存中建立一个红黑树来存放epoll_ctl以后发送的socket,同时也会建立一个rdllist双向链表来存放就绪事件。调用epoll_wait时,只看rdllist双向链表的数据。epoll_ctl向epoll对象添加、修改、删除事件时,是在rbr红黑树中进行操作的。很快添加到epoll的事件会和设备相关(比如网卡)建立回调关系,当设备上相应的事件发生时,会调用回调方法,将事件添加到rdllist中双向链表;这个回调方法在内核中叫做ep_poll_callback。epoll的两种触发模式有两种触发模式:EPOLLLT和EPOLLET,LT是默认模式,ET是“高速”模式(只支持no-blocksocket)*在LT(leveltrigger)模式下,只要文件描述符还有数据要读,**每次Read事件都会触发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指定了一个回调函数,当设备就绪时,唤醒waitingqueue等待的时候会调用这个回调函数,这个回调函数会把就绪的fd加入到一个就绪列表中。epoll_wait的工作其实就是检查这个就绪链表中是否有就绪的fd(不需要遍历)***针对第三个缺点**:epoll没有这个限制,它支持的fd上限是可以打开的最大文件数,这个数一般比2048大很多,比如在1GB内存的机器上大约是10万。一般来说,这个数字与系统内存有很大关系。epoll的高性能*epoll使用红黑树保存Monitored文件描述符事件,epoll_ctl增删改查快速*epoll无需遍历即可获取就绪fd,可直接返回就绪链表*linux2.6后,使用了mmap技术,不再需要从内核拷贝数据到用户空间,零拷贝epoll的IO模型是一个同步异步问题概念定义*同步I/O操作:导致请求进程阻塞,直到I/O操作完成*异步I/O操作:不会导致请求进程阻塞,异步只处理I/OO操作完成后的通知,不主动读写数据,系统内核完成数据的读写*阻塞、非阻塞:进程/线程要访问的数据是否准备好,进程/线程是否需要等待异步IO的概念是一个要求非阻塞I/O调用。前面介绍过,I/O操作分为两个阶段:R1等待数据准备好。R2将数据从内核复制到进程。epoll虽然在2.6内核之后采用了mmap机制,使得在R2阶段不需要copy,但是在R1阶段还是阻塞了。因此,归类为同步IOReactor模型的Reactor的中心思想是将所有需要处理的I/O事件注册到一个中央I/O多路复用器上,而主线程/进程阻塞在多路复用器上;一旦有I/O事件到达或就绪,多路复用器返回,将预先注册的相应I/O事件分发给相应的处理器。最重要的是我们可以从内核中读取数据的状态事件分隔符:一般情况下,等待事件都会交给epoll和select;而事件的到来是随机的、异步的,所以需要循环调用epoll,封装在框架中模块是事件分隔符(简单理解为epoll封装)事件处理程序:事件发生后,需要进行处理通过进程或线程。这个处理程序就是事件处理程序,一般和事件分隔符不同。Reactor的一般流程1)应用程序在事件分隔符中注册读写就绪事件和读写就绪事件处理器2)事件分隔符等待读写就绪事件发生3)读写就绪事件发生,激活事件分离器,分离器调用读写就绪事件处理器4)事件处理器先从内核读取数据到用户空间,然后对数据进行处理单线程+Reactor多线程+Reactor多线程+多个ReactorProactor模型大体流程1)应用在事件分隔符Completion事件和读取完成事件处理器中注册读取,并向系统发送异步读取请求2)事件分隔符等待读取事件完成3)分离器等待过程中,系统使用并行内核线程执行实际的读取操作,并将数据复制到进程缓冲区,最后通知事件分离器读取完成的到来4)事件分离器监听到读完成事件并激活读完成事件的处理器5)读完成事件处理器直接处理用户进程缓冲区中的数据Proactor和Reactor不同的是Proactor是基于异步I/O的概念,而Reactor一般基于多路复用I/O的概念。Proactor不需要将数据从内核复制到用户空间。此步骤由系统完成。文中错误的引用欢迎指正文章讲Linux的五种IO模型,网络IO模型,网络IO5网络IO模型,epoll原理详解和epollreactor模型
