zoukankan      html  css  js  c++  java
  • IO模型(主讲多路复用)

    一、基础概念说明:

    0)CPU的时间片轮转机制

    每个进程(线程)被分配一个时间段,称作它的时间片,即该进程允许运行的时间。

    如果在时间片结束时进程还在运行,则CPU使用权将被剥夺并分配给另一个进程。如果进程在时间片结束前阻塞或结束,则CPU当即进行切换。

    程序阻塞了它的进程之后,CPU 会立马跑别的进程。

    但是你想知道CPU 还会不会回来尝试跑这个进程,你需要知道工作队列和等待队列。

    0.1)工作队列与等待队列

    Linux 内核空间里会维持一个工作队列,因为时间片轮转机制,系统会在进程A、B、C等多个进程间切换着跑。

     假如现在进程 A 里跑的程序有一个对象执行了某个方法将当前进程阻塞了,内核会立刻将进程A从工作队列中移除,同时在该对象里创建等待队列,并新建一个引用指向进程A。如下图:

    从图中可以看到,进程A被排在了工作队列之外,不受系统调度了,这就是我们常说的被操作系统“挂起”。
    这也提现了阻塞和挂起的关系。阻塞是人为安排的,让你程序走到这里阻塞。而阻塞的实现方式是系统将进程挂起。

    当这个对象受到某种“刺激”(某事件触发)之后, 操作系统将该对象等待队列上的进程重新放回到工作队列上就绪,等待时间片轮转到该进程。

    1)socket套接字

    数据在client与server端的通信中的收发过程 

    客户端:char buf[] 应用程序缓冲区 — send/wirte(socket API)—> 内核缓冲区 —>网卡  ---->  网络  --->  网卡 —>内核缓冲区 —> recv / read —>应用程序缓冲区:服务器端

    客户端与服务器端都会通过socket生产一个套接字描述符fd;

    套接字描述符是用来标定系统为当前的进程划分的一块缓冲空间的

    2)进程切换

      为了控制进程的执行,内核必须有能力挂起正在CPU上运行的进程,并恢复以前挂起的某个进程的执行。这种行为被称为进程切换。

      进程切换要历经以下变化:

    • 保存处理机上下文,包括程序计数器和其他寄存器。

    • 更新PCB信息。

    • 把进程的PCB移入相应的队列,如就绪、在某事件阻塞等队列。

    • 选择另一个进程执行,并更新其PCB。

    • 更新内存管理的数据结构。

    • 恢复处理机上下文。

      总之,进程切换很耗费CPU资源;

    3)同步与异步

    同步和异步关注的是消息通知机制

    当一个同步调用发出后,调用者要一直等待返回消息(结果)通知后,才能进行后续的执行;   —— 主动等待

    当一个异步过程调用发出后,调用者不能立刻得到返回消息(结果)。实际处理这个调用的部件在完成后,通过状态、通知和回调来通知调用者。—— 被动通知

      比喻:

    去银行办理业务,可能会有两种方式:
    
    1、选择排队等候;
    2、另种选择取一个小纸条上面有我的号码,等到排到我这一号时由柜台的人通知我轮到我去办理业务了
    
    第一种:前者(排队等候)就是同步等待消息通知,也就是我要一直在等待银行办理业务情况;
    
    第二种:后者(等待别人通知)就是异步等待消息通知。在异步消息处理中,等待消息通知者(在这个例子中就是等待办理业务的人)往往注册一个回调机制,在所等待的事件被触发时由触发机制(在这里是柜台的人)通过某种机制(在这里是写在小纸条上的号码,喊号)找到等待该事件的人。
    去银行办业务

    4)阻塞与非阻塞

    阻塞和非阻塞描述的是程序在等待调用结果(消息,返回值)时的状态.
    阻塞调用是指调用结果返回之前,当前线程会被挂起。调用线程只有在得到结果之后才会返回。
    非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程

    5)文件描述符

    文件描述符的本质是一个索引值,指向一个进程打开的文件的记录表。

    linux中每个进程默认最多可以打开1024个文件,最多有1024个文件描述符

    文件描述符的特点:

      1、非负整数(默认 0~1023)

      2、从最小可用的数字来分配

      3、每个进程启动时默认打开0,1,2三个文件描述符

    需要被监控的文件描述符会被放到结构体fd_set集合中,本质是一个long数组,但以一位表示一个文件描述符。比如0~1023位,数组大小为1024/8个字节。

    6)标准IO

    标准IO又称缓存IO,大多文件系统的默认IO操作都是标准IO,在Linux的标准IO机制中,操作系统会将IO的数据缓冲在文件系统的页缓存中,即:

    数据先会被复制到内核的缓冲区中,然后才会从内核缓冲区复制到应用程序的地址空间。

    二、IO模型

    对于一次IO访问(以read举例),数据会先被拷贝到操作系统内核的缓冲区中,然后才会从内核的缓冲区拷贝到应用程序的地址空间

    所以说,当一个read操作发生时,它会经历两个阶段:

    • 第一阶段:等待数据准备 (Waiting for the data to be ready)。

    • 第二阶段:将数据从内核拷贝到进程中 (Copying the data from the kernel to the process)。

    对于socket流而言:

    1. 第一步:通常涉及等待网络上的数据分组到达,然后被复制到内核的某个缓冲区。

    2. 第二步:把数据从内核缓冲区复制到应用进程缓冲区。

    1、在UNIX/Linux下主要有以下网络I/O模型:

    • 阻塞IO(bloking IO) —— 最常用

    • 非阻塞IO(non-blocking IO) —— 可防止进程阻塞在IO操作上,需要轮询

    • 多路复用IO(multiplexing IO)—— 允许对多个IO进行控制

    • 信号驱动式IO(signal-driven IO)(不常用)—— 一种异步通信模型

    • 异步IO(asynchronous IO)

    前四种都属于同步IO模型。

    2、基本 Linux IO 模型的简单矩阵。如下图所示:

     常见的IO模型有阻塞、非阻塞、IO多路复用,异步

    • 阻塞I/O

    最为普遍的IO模式·,缺省情况下,套接字建立后所处的模式就是阻塞IO;之前很多读写函数在调用过程中会发生阻塞。

    ---读操作read、recv、recvfrom

    ---写操作write、send

    ---其他操作accept、connect

     eg:

    读阻塞:进程调用read函数从套接字上读取数据,当套接字的接收缓冲区没有数据可读,read函数会一直阻塞,直到有数据可读。缓冲区收到数据,内核就会唤醒该进程,通过read访问数据。

    写阻塞:当写入数据量大于要写入的缓冲区大小时,写操作将不会进行任何拷贝工作,发生阻塞。一旦发送缓冲区内有足够空间,内核将唤醒进程,将数据从用户缓冲区拷贝到发送数据缓冲区。 /**UDP不用等待确认,没有实际发送缓冲区,所以UDP协议中不存在发送缓冲区满的情况。在UDP套接字上执行的写操作永远不会阻塞。***/

    对于阻塞IO,以read为例,若内核缓冲区未完全接收到数据,以及内核缓冲区数据未将数据拷贝到用户缓冲区,这两个过程都会被阻塞。

    • 非阻塞I/O

    当把套接字设为非阻塞模式O_NONBLOCK,相当于进程请求的IO操作无法完成时,内核会返回一个错误而不是让进程休眠等待。

    当应用程序使用了非阻塞的套接字,需使用轮询的方式不断循环测试一个文件描述符是否有数据可读(polling)。

    应用程序不断轮询内核来检测IO操作是否就绪,是极其浪费CPU资源的。故此模式不普遍,而是在其他IO模型中使用非阻塞IO这一特性。

    流程描述:

    当用户进程发出read操作时,如果kernel中的数据还没有准备好,那么它并不会阻塞用户进程,而是立刻返回一个error。
    从用户进程角度讲,它发起一个read操作后,并不需要等待,而是马上就得到了一个结果。
    用户进程判断结果是一个error时,它就知道数据还没有准备好,于是它可以再次发送read操作。
    一旦kernel中的数据准备好了,并且又再次收到了用户进程的system call,那么它马上就将数据拷贝到了用户内存,然后返回

    当用户线程发起IO请求,通过系统调用,进入内核空间,
    若数据准备好了,就会执行IO操作;若数据未准备好,线程会立即返回一个错误,回到用户空间,过一段时间(线程可以去干其他的事),
    然后又进行IO操作,发起系统调用,以此循环往复检查内核数据,直到内核准备好数据,并拷贝到用户线程缓存,再进行数据处理。

    与阻塞IO不一样,"非阻塞将大的整片时间的阻塞分成N多的小的阻塞, 所以进程不断地有机会 '被' CPU光顾",而且用户线程是不断的主动询问kernel数据

     实现非阻塞模式   ——  fcntl()函数

    建立套接字后,内核默认将其设置为阻塞IO,可以使用fcntl()设置套接字标志为O_NONBLOCK实现非阻塞。

    • IO多路复用

    引入:

    当多个客户端与服务器通信,若服务器read,并阻塞于其中一个客户端socketfd1,当另一个客户的数据到达套接字socketfd2时,服务器不能去处理,仍然阻塞在read(socketfd1),怎么办?

    如何应用程序中同时处理多路输入输出流?

    若采用阻塞模式,将达不到预期目的;

    若采用非阻塞模式,对多个输入轮询,很浪费CPU时间

    若设置多进程并发处理,每个进程分别处理一条数据通路,将产生新的进程通信与同步问题,更加复杂。

    较好的办法就是IO多路复用

    基本思想为:

    使用单个线程,通过记录来跟踪每个IO流的状态,来同时管理多个IO流。

    —先构造一张有关描述符的表,然后调用一个函数循环查询多任务的完成状态。当这些文件描述符中的一个或多个已经准备好进行IO时,该函数才返回。

    —函数返回时,告诉进程那个描述符已经就绪,可以进行IO操作。

    该函数就是系统调用函数select、poll、epoll,

     流程描述:

    select,poll,epoll这个function会不断的轮询所负责的所有socket,当某个socket有数据到达了,就通知用户进程。

    当用户进程调用了select,那么整个进程会被block,而同时,kernel会“监视”所有select负责的socket,当任何一个socket中的数据准备好了,select就会返回

    这个时候用户线程再调用read操作,将数据从kernel拷贝到用户线程。

    对于非阻塞IO,在轮询时,会在用户空间与内核空间来回切换,而select函数是在内核中进行轮询,一旦有socket准备好,就会去用户空间通知用户线程
    对于阻塞IO,会执行一次系统调用read,但对于IO多路复用,会执行两次,read与select,IO多路复用的优势并不是对于单个连接能处理得更快,而是在于能处理更多的连接。

    IO多路复用使用场景:

    服务器需要同时处理多个处于监听状态或者多个连接状态的套接字。

    服务器需要同时处理多种网络协议的套接字。

    多路复用模型

    一)实现步骤

    1. 把关心的文件描述符加入到fd_set集合中
    2. 调用select / poll函数去监控集合fd_set中的文件描述符(阻塞等待集合中一个或多个文件描述符有数据)
    3. 当有数据时,退出select()阻塞
    4. 依次判断哪个文件描述符有数据
    5. 依次处理有数据的文件描述符的数据

    二)相关结构体与函数

      1、相关结构体

      fd_set  

    1 fd_set  rset;
    2 
    3 void FD_ZERO(fd_set *fdset);   //集合清零
    4 void FD_SET(int fd, fd_set *fdset);  //把fd加到fdset集合里
    5 void FD_CLR(int fd, fd_set *fdset);  //将fd从fdset里面清除
    6 void FD_ISSET(int fd, fd_set *fdset); //检查fdset中的fd是否可读写,>0即可读写

      2、相关函数

      2.1select

      基本原理

    select 函数监视的文件描述符分3类,分别是writefds、readfds、和exceptfds。调用后select函数会阻塞,直到有描述符就绪(有数据 可读、可写、或者有except),或者超时(timeout指定等待时间,如果立即返回设为null即可),函数返回。当select函数返回后,可以通过遍历fdset,来找到就绪的描述符。

     1 #include <sys/types.h>
     2 #include <sys/times.h>
     3 #include <sys/select.h>
     4 
     5 int select(int nfds, fd_set *readfds, fd_set *writefds,
     6                   fd_set *exceptfds, struct timeval *timeout);
     7 -----------------------------------参数------------------------------------
     8 //nfds--select监视的fd数,视进程中打开的文件数而定,一般设为你要监视各文件中的最大文件号加一(maxfd+1)
     9 //readfds--select监视的可读fd集合
    10 //writefds--select监视的可写fd集合
    11 //exceptfds--select监视的异常fd集合
    12 //timeout--本次select的超时结束时间
    返回值:准备就绪的描述符数,若超时则返回0,若出错则返回-1。 13 -----------------------------------备注------------------------------------- 14 一般:填读集合,对于写集合填NULL,异常集合(带外数据)填null 15 struct timeval { 16 long tv_sec; /* seconds */ 17 long tv_usec; /* microseconds,微秒us-10^6 */ 18 };

    注:select退出后,集合表示由数据的集合

    //注:select退出后,集合表示有数据的集合
           if(FD_SET(fd, &rset))  {....}
    //1、若是监听套接字上有数据,则有新客户端连接,则accept
    //2、若是已建立连接的套接字上有数据,则去读数据

    select模型----fd_set在select前后的变化

     注:select函数里的文件描述符fd_set集合的参数在select前后发送了变化

          前:表示关心的文件描述符集合‘(在监控后不一定都有数据)

       后:有数据的集合(例如,未超时返回的情况下)’

     select缺点:

    1. select最大的缺陷就是单个进程所打开的FD是有一定限制的,它由FD_SETSIZE设置,默认值是1024。
    2. 对socket进行扫描时是线性扫描,即采用轮询的方法,效率较低。
    3. 需要维护一个用来存放大量fd的数据结构,这样会使得用户空间和内核空间在传递该结构时复制开销大。

    2.2、poll

      基本原理

    poll本质上和select没有区别,它将用户传入的数组拷贝到内核空间,然后查询每个fd对应的设备状态,如果设备就绪则在设备等待队列中加入一项并继续遍历,如果遍历完所有fd后没有发现就绪设备,则挂起当前进程,直到设备就绪或者主动超时,被唤醒后它又要再次遍历fd。这个过程经历了多次无谓的遍历。

       优点

      基于链表存储,没有最大连接数限制

      缺点

    1、大量的fd的数组被整体复制于用户态和内核地址空间之间,而不管这样的复制是不是有意义。

    2、poll还有一个特点是“水平触发”,如果报告了fd后,没有被处理,那么下次poll时会再次报告该fd

      注意

    从上面看,select和poll都需要在返回后,通过遍历文件描述符来获取已经就绪的socket。
    事实上,同时连接的大量客户端在一时刻可能只有很少的处于就绪状态,
    因此随着监视的描述符数量的增长,其效率也会线性下降。

    2.3、epoll

    epoll是在2.6内核中提出的,是之前的select和poll的增强版本。相对于select和poll来说,epoll更加灵活,没有描述符限制。

    epoll使用一个文件描述符管理多个描述符,将用户关系的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的copy只需一次
      基本原理

    epoll支持水平触发边缘触发,最大的特点在于边缘触发,它只告诉进程哪些fd刚刚变为就绪态,并且只会通知一次。
    还有一个特点是,epoll使用“事件”的就绪通知方式,通过epoll_ctl注册fd,
    一旦该fd就绪,内核就会采用类似callback的回调机制来激活该fd,epoll_wait便可以收到通知。

      优点

    1、没有最大并发连接的限制,能打开的FD的上限远大于1024(1G的内存上能监听约10万个端口)。
    2、效率提升,不是轮询的方式,不会随着FD数目的增加效率下降。只有活跃可用的FD才会调用callback函数;
       即Epoll最大的优点就在于它只管你“活跃”的连接,而跟连接总数无关,
       因此在实际的网络环境中,Epoll的效率就会远远高于select和poll。
    3、内存拷贝,利用mmap()文件映射内存加速与内核空间的消息传递;即epoll使用mmap减少复制开销。

      epoll操作fd的两种模式:

    LT模式:当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序可以不立即处理该事件。下次调用epoll_wait时,会再次响应应用程序并通知此事件。

    ET模式:当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序必须立即处理该事件。如果不处理,下次调用epoll_wait时,不会再次响应应用程序并通知此事件。

    LT(level triggered)是缺省的工作方式,并且同时支持block和no-block socket。在这种做法中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的fd进行IO操作。如果你不作任何操作,内核还是会继续通知你的。
    LT模式
    ET(edge-triggered)是高速工作方式,只支持no-block socket。在这种模式下,当描述符从未就绪变为就绪时,内核通过epoll告诉你。然后它会假设你知道文件描述符已经就绪,并且不会再为那个文件描述符发送更多的就绪通知,直到你做了某些操作导致那个文件描述符不再为就绪状态了(比如,你在发送,接收或者接收请求,或者发送接收的数据少于一定量时导致了一个EWOULDBLOCK 错误)。但是请注意,如果一直不对这个fd作IO操作(从而导致它再次变成未就绪),内核不会发送更多的通知(only once)。
    
    ET模式在很大程度上减少了epoll事件被重复触发的次数,因此效率要比LT模式高。epoll工作在ET模式的时候,必须使用非阻塞套接口,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。
    ET模式

      select/poll 与 epoll的区别:

    —在select/poll中,进程只有在调用一定的方法后,内核才对所有监视的文件描述符进行扫描,
    —而epoll事先通过epoll_ctl()来注册一个文件描述符,一旦基于某个文件描述符就绪时,
       内核会采用类似callback的回调机制,迅速激活这个文件描述符,当进程调用epoll_wait()时便得到通知。
       (此处去掉了遍历文件描述符,而是通过监听回调的的机制。这正是epoll的魅力所在。)

      注意:

    如果没有大量的idle-connection或者dead-connection,epoll的效率并不会比select/poll高很多,但是当遇到大量的idle-connection,就会发现epoll的效率大大高于select/poll。

       epoll的使用:

    2.

    1.执行epoll_create时,创建了红黑树和就绪list链表。

    1  #include <sys/epoll.h>
    2  int epoll_create ( int size );
    3 //在epoll早期的实现中,对于监控文件描述符的组织并不是使用红黑树,而是hash表。这里的size实际上已经没有意义。

      

    2.执行epoll_ctl时,如果增加fdsocket),则检查在红黑树中是否存在,存在立即返回,不存在则添加到红黑树上,

               然后向内核注册回调函数,用于当中断事件来临时向准备就绪list链表中插入数据。

     1 #include <sys/epoll.h>
     2  int epoll_ctl ( int epfd, int op, int fd, struct epoll_event *event );
     3 
     4 /*
     5  函数说明:
     6      fd:要操作的文件描述符
     7      op:指定操作类型
     8 操作类型:
     9      EPOLL_CTL_ADD:往事件表中注册fd上的事件
    10      EPOLL_CTL_MOD:修改fd上的注册事件
    11      EPOLL_CTL_DEL:删除fd上的注册事件
    12      event:指定事件,它是epoll_event结构指针类型
    13  */
    14 
    15 //epoll_event定义:
    16  struct epoll_event
    17 {
    18      __unit32_t events;    // epoll事件
    19      epoll_data_t data;     // 用户数据 
    20  };
    21 //结构体说明:
    22      events:描述事件类型,和poll支持的事件类型基本相同(两个额外的事件:EPOLLET和EPOLLONESHOT,高效运作的关键)
    23      data成员:存储用户数据
    24 typedef union epoll_data
    25 {
    26     void* ptr;              //指定与fd相关的用户数据 
    27     int fd;                 //指定事件所从属的目标文件描述符 
    28     uint32_t u32;
    29     uint64_t u64;
    30 } epoll_data_t;

     

    3.执行epoll_wait时立刻返回准备就绪链表里的数据即可。

    1 #include <sys/epoll.h>
    2 int epoll_wait ( int epfd, struct epoll_event* events, int maxevents, int timeout );
    3 
    4 //函数说明:
    5      返回:成功时返回就绪的文件描述符的个数,失败时返回-1并设置errno
    6      timeout:指定epoll的超时时间,单位是毫秒。当timeout为-1是,epoll_wait调用将永远阻塞,直到某个时间发生。当timeout为0时,epoll_wait调用将立即返回。
    7      maxevents:指定最多监听多少个事件
    8      events:检测到事件,将所有就绪的事件从内核事件表中复制到它的第二个参数events指向的数组中。

    epoll详解文章:

    epoll源码实现分析[整理]

    彻底学会使用epoll

    Linux下的I/O复用与epoll详解

    2.4 select、poll、epoll区别

    1)支持一个进程所能打开的最大连接数

     2)FD剧增后带来的IO效率问题

     3)信息传递方式

     4、异步非阻塞IO

    相对于同步IO,异步IO不是顺序执行。
    用户线程进行aio_read系统调用之后,无论内核数据是否准备好,都会直接返回给用户线程,然后用户态线程可以去做别的事情。
    等到socket数据准备好了,内核直接复制数据给线程,然后从内核向线程发送通知。
    IO两个阶段,线程都是非阻塞的。
    (Linux提供了AIO库函数实现异步,但是用的很少。目前有很多开源的异步IO库,例如libevent、libev、libuv)

    模型

    参考:

     阻塞/非阻塞/同步/异步/概念辨析

    linux下的阻塞机制是如何实现的?(好文)

    聊聊Linux下的五种IO模型

    IO多路复用机制详解

    聊聊IO多路复用之select、poll、epoll详解 

  • 相关阅读:
    第108题:将有序数组转换成二叉搜索树
    第107题:二叉树的层次遍历II
    第106题:从中序与后序遍历序列构造二叉树
    java类读取properties文件
    WdatePicker.js开始日期和结束日期比较
    对两个整数变量的值进行互换
    Java基础知识总结
    jdk环境变量
    逻辑运算符有什么用?
    if和switch的应用
  • 原文地址:https://www.cnblogs.com/y4247464/p/12233282.html
Copyright © 2011-2022 走看看