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。

2.4 select、poll、epoll区别

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

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

 3)信息传递方式

参考:

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

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

聊聊Linux下的五种IO模型

IO多路复用机制详解

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

猜你喜欢

转载自www.cnblogs.com/y4247464/p/12233282.html