前言

网络I/O,能够了解为网络上的数据流。通常咱们会基于socket与远端建设一条TCP或者UDP通道,而后进行读写。单个socket时,应用一个线程即可高效解决;然而如果是10K个socket连贯,或者更多,咱们如何做到高性能解决?

  • 基本概念介绍
  • 网络I/O的读写过程
  • linux下的五种网络I/O模型
  • 多路复用I/O深刻了解一波
  • Reactor模型
  • Proacotr模型

关注公众号,一起交换 :潜行前行

github地址,感激star

基本概念介绍

  • 过程(线程)切换

    *    所有零碎都有调度过程的能力,它能够挂起一个以后正在运行的过程,并复原之前挂起的过程
  • 过程(线程)的阻塞

    *    运行中的过程,有时会期待其余事件的执行实现,比方期待锁,申请I/O的读写;过程在期待过程会被零碎主动执行阻塞,此时过程不占用CPU
  • 文件描述符

    *    在Linux,文件描述符是一个用于表述指向文件援用的抽象化概念,它是一个非负整数。当程序关上一个现有文件或者创立一个新文件时,内核向过程返回一个文件描述符
  • linux信号处理

    * Linux过程运行中能够承受来自零碎或者过程的信号值,而后依据信号值去运行相应捕获函数;信号相当于是硬件中断的软件模仿

在零拷贝机制篇章已介绍过 用户空间和内核空间缓冲区,这里就省略了

网络IO的读写过程

  • 当在用户空间发动对socket套接字的读操作时,会导致上下文切换,用户过程阻塞(R1)期待网络数据流到来,从网卡复制到内核;(R2)而后从内核缓冲区向用户过程缓冲区复制。此时过程切换复原,解决拿到的数据
  • 这里咱们给socket读操作的第一阶段起个别名R1,第二阶段称为R2
  • 当在用户空间发动对socket的send操作时,导致上下文切换,用户过程阻塞期待(1)数据从用户过程缓冲区复制到内核缓冲区。数据copy实现,此时过程切换复原

linux五种网络IO模型

阻塞式I/O (blocking IO)

ssize_t recvfrom(int sockfd,void *buf,size_t len,unsigned int flags, struct sockaddr *from,socket_t *fromlen);

  • 最根底的I/O模型就是阻塞I/O模型,也是最简略的模型。所有的操作都是程序执行的
  • 阻塞IO模型中,用户空间的应用程序执行一个零碎调用(recvform),会导致应用程序被阻塞,直到内核缓冲区的数据筹备好,并且将数据从内核复制到用户过程。最初过程才被零碎唤醒解决数据
  • 在R1、R2间断两个阶段,整个过程都被阻塞

非阻塞式I/O (nonblocking IO)

  • 非阻塞IO也是一种同步IO。它是基于轮询(polling)机制实现,在这种模型中,套接字是以非阻塞的模式关上的。就是说I/O操作不会立刻实现,然而I/O操作会返回一个错误代码(EWOULDBLOCK),提醒操作未实现
  • 轮询查看内核数据,如果数据未筹备好,则返回EWOULDBLOCK。过程再持续发动recvfrom调用,当然你能够暂停去做其余事
  • 直到内核数据筹备好,再拷贝数据到用户空间,而后过程拿到非错误码数据,接着进行数据处理。须要留神,拷贝数据整个过程,过程依然是属于阻塞的状态
  • 过程在R2阶段阻塞,尽管在R1阶段没有被阻塞,然而须要一直轮询

多路复用I/O (IO multiplexing)

  • 个别后端服务都会存在大量的socket连贯,如果一次能查问多个套接字的读写状态,若有任意一个筹备好,那就去解决它,效率会高很多。这就是“I/O多路复用”,多路是指多个socket套接字,复用是指复用同一个过程
  • linux提供了select、poll、epoll等多路复用I/O的实现形式
  • select或poll、epoll是阻塞调用
  • 与阻塞IO不同,select不会等到socket数据全副达到再解决,而是有了一部分socket数据筹备好就会复原用户过程来解决。怎么晓得有一部分数据在内核筹备好了呢?答案:交给了零碎零碎解决吧
  • 过程在R1、R2阶段也是阻塞;不过在R1阶段有个技巧,在多过程、多线程编程的环境下,咱们能够只调配一个过程(线程)去阻塞调用select,其余线程不就能够解放了吗

信号驱动式I/O (SIGIO)

  • 须要提供一个信号捕获函数,并和socket套接字关联;发动sigaction调用之后过程就能解放去解决其余事
  • 当数据在内核筹备好后,过程会收到一个SIGIO信号,继而中断去运行信号捕获函数,调用recvfrom把数据从内核读取到用户空间,再解决数据
  • 能够看出用户过程是不会阻塞在R1阶段,但R2还是会阻塞期待

异步IO (POSIX的aio_系列函数)

  • 绝对同步IO,异步IO在用户过程发动异步读(aio_read)零碎调用之后,无论内核缓冲区数据是否筹备好,都不会阻塞以后过程;在aio_read零碎调用返回后过程就能够解决其余逻辑
  • socket数据在内核就绪时,零碎间接把数据从内核复制到用户空间,而后再应用信号告诉用户过程
  • R1、R2两阶段时过程都是非阻塞的

多路复用IO深刻了解一波

select

int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
  • 1)应用copy_from_user从用户空间拷贝fd_set到内核空间
  • 2)注册回调函数__pollwait
  • 3)遍历所有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(留神把过程挂到期待队列中并不代表过程曾经睡眠了)。在设施收到一条音讯(网络设备)或填写完文件数据(磁盘设施)后,会唤醒设施期待队列上睡眠的过程,这时current便被唤醒了
  • 6)poll办法返回时会返回一个形容读写操作是否就绪的mask掩码,依据这个mask掩码给fd_set赋值
  • 7)如果遍历完所有的fd,还没有返回一个可读写的mask掩码,则会调用schedule_timeout是调用select的过程(也就是current)进入睡眠
  • 8) 当设施驱动产生本身资源可读写后,会唤醒其期待队列上睡眠的过程。如果超过肯定的超时工夫(timeout指定),还是没人唤醒,则调用select的过程会从新被唤醒取得CPU,进而从新遍历fd,判断有没有就绪的fd
  • 9)把fd_set从内核空间拷贝到用户空间

select的毛病

  • 每次调用select,都须要把fd汇合从用户态拷贝到内核态,这个开销在fd很多时会很大
  • 同时每次调用select都须要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大
  • select反对的文件描述符数量太小了,默认是1024

epoll

int epoll_create(int size);  int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);  int epoll_wait(int epfd, struct epoll_event *events,int maxevents, int timeout); 
  • 调用epoll_create,会在内核cache里建个红黑树用于存储当前epoll_ctl传来的socket,同时也会再建设一个rdllist双向链表用于存储准备就绪的事件。当epoll_wait调用时,仅查看这个rdllist双向链表数据即可
  • epoll_ctl在向epoll对象中增加、批改、删除事件时,是在rbr红黑树中操作的,十分快
  • 增加到epoll中的事件会与设施(如网卡)建设回调关系,设施上相应事件的产生时会调用回调办法,把事件加进rdllist双向链表中;这个回调办法在内核中叫做ep_poll_callback

epoll的两种触发模式

  • epoll有EPOLLLT和EPOLLET两种触发模式,LT是默认的模式,ET是“高速”模式(只反对no-block socket)

    *    LT(程度触发)模式下,只有这个文件描述符还有数据可读,**每次epoll_wait都会触发它的读事件***    ET(边缘触发)模式下,检测到有I/O事件时,通过 epoll_wait 调用会失去有事件告诉的文件描述符,对于文件描述符,如可读,则必须将该文件描述符始终读到空(或者返回EWOULDBLOCK),**否则下次的epoll_wait不会触发该事件**

epoll相比select的长处

  • 解决select三个毛病

    *    **对于第一个毛病**:epoll的解决方案在epoll_ctl函数中。每次注册新的事件到epoll句柄中时(在epoll_ctl中指定EPOLL_CTL_ADD),会把所有的fd拷贝进内核,而不是在epoll_wait的时候反复拷贝。epoll保障了每个fd在整个过程中只会拷贝一次(epoll_wait不须要复制)*    **对于第二个毛病**:epoll为每个fd指定一个回调函数,当设施就绪,唤醒期待队列上的期待者时,就会调用这个回调函数,而这个回调函数会把就绪的fd退出一个就绪链表。epoll_wait的工作实际上就是在这个就绪链表中查看有没有就绪的fd(不须要遍历)*    **对于第三个毛病**:epoll没有这个限度,它所反对的FD下限是最大能够关上文件的数目,这个数字个别远大于2048,举个例子,在1GB内存的机器上大概是10万左右,一般来说这个数目和零碎内存关系很大
  • epoll的高性能

    *    epoll应用了红黑树来保留须要监听的文件描述符事件,epoll_ctl增删改操作疾速*    epoll不须要遍历就能获取就绪fd,间接返回就绪链表即可*    linux2.6 之后应用了mmap技术,数据不在须要从内核复制到用户空间,零拷贝

对于epoll的IO模型是同步异步的疑难

  • 概念定义

    *  同步I/O操作:导致申请过程阻塞,直到I/O操作实现*  异步I/O操作:不导致申请过程阻塞,异步只用解决I/O操作实现后的告诉,并不被动读写数据,由零碎内核实现数据的读写*  阻塞,非阻塞:过程/线程要拜访的数据是否就绪,过程/线程是否须要期待
  • 异步IO的概念是要求无阻塞I/O调用。后面有介绍到I/O操作分两阶段:R1期待数据筹备好。R2从内核到过程拷贝数据。尽管epoll在2.6内核之后采纳mmap机制,使得其在R2阶段不须要复制,然而它在R1还是阻塞的。因而归类到同步IO

Reactor模型

Reactor的中心思想是将所有要解决的I/O事件注册到一个核心I/O多路复用器上,同时主线程/过程阻塞在多路复用器上;一旦有I/O事件到来或是准备就绪,多路复用器返回,并将当时注册的相应I/O事件散发到对应的处理器中

相干概念介绍:

  • 事件:就是状态;比方:读就绪事件指的是咱们能够从内核读取数据的状态
  • 事件分离器:个别会把事件的期待产生交给epoll、select;而事件的到来是随机,异步的,所以须要循环调用epoll,在框架里对应封装起来的模块就是事件分离器(简略了解为对epoll封装)
  • 事件处理器:事件产生后须要过程或线程去解决,这个解决者就是事件处理器,个别和事件分离器是不同的线程

    Reactor的个别流程

  • 1)应用程序在事件分离器注册读写就绪事件读写就绪事件处理器
  • 2)事件分离器期待读写就绪事件产生
  • 3)读写就绪事件产生,激活事件分离器,分离器调用读写就绪事件处理器
  • 4)事件处理器先从内核把数据读取到用户空间,而后再解决数据

单线程 + Reactor

多线程 + Reactor

多线程 + 多个Reactor

Proactor模型的个别流程

  • 1)应用程序在事件分离器注册读实现事件读实现事件处理器,并向零碎收回异步读申请
  • 2)事件分离器期待读事件的实现
  • 3)在分离器期待过程中,零碎利用并行的内核线程执行理论的读操作,并将数据复制过程缓冲区,最初告诉事件分离器读实现到来
  • 4)事件分离器监听到读实现事件,激活读实现事件的处理器
  • 5)读实现事件处理器间接解决用户过程缓冲区中的数据

    Proactor和Reactor的区别

  • Proactor是基于异步I/O的概念,而Reactor个别则是基于多路复用I/O的概念
  • Proactor不须要把数据从内核复制到用户空间,这步由零碎实现

欢送指注释中谬误

参考文章

  • 聊聊Linux 五种IO模型
  • 网络io模型
  • 网络IO
  • 5种网络IO模型
  • epoll原理详解及epoll反应堆模型