高级IO模型

Posted mbf330

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了高级IO模型相关的知识,希望对你有一定的参考价值。

高级IO

一、高阶IO重要概念

1、同步通信 vs 异步通信

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

  • 所谓同步,就是在发出一个调用时,在没有得到结果之前,该调用就不返回. 但是一旦调用返回,就得到返回值了;换句话说,就是由调用者主动等待这个调用的结果;
  • 异步则是相反,调用在发出之后,这个调用就直接返回了,所以没有返回结果;换句话说,当一个异步过程调用发出后,调用者不会立刻得到结果;而是在调用发出后,被调用者通过状态、通知来通知调用者,或通过回调函数处理这个调用。

2、阻塞 vs 非阻塞

阻塞和非阻塞关注的是程序在等待调用结果(消息,返回值)时的状态

  • 阻塞调用是指调用结果返回之前,当前线程会被挂起. 调用线程只有在得到结果之后才会返回;
  • 非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程;

二、五种IO模型

1、阻塞IO

在内核将数据准备好之前, 系统调用会一直等待。所有的套接字,默认都是阻塞方式

阻塞IO是最常见的IO模型

2、非阻塞IO

如果内核还未将数据准备好,系统调用仍然会直接返回,并且返回EWOULDBLOCK错误码

非阻塞IO往往需要程序员循环的方式反复尝试读写文件描述符,这个过程称为轮询。这对CPU来说是较大的浪费,一般只有特定场景下才使用

3、信号驱动IO

内核将数据准备好的时候,使用SIGIO信号通知应用程序进行IO操作

4、多路转接IO

从流程图上看起来和阻塞IO类似;实际上最核心在于IO多路转接能够同时等待多个文件描述符的就绪状态

5、异步IO

由内核在数据拷贝完成时,通知应用程序(信号驱动是告诉应用程序何时可以开始拷贝数据)。

重点学习:多路转接IO

一、select模型

1、select函数

(1)函数功能
系统提供select函数来实现多路复用输入/输出模型;select系统调用用来让程序监视多个文件描述符的状态变化;程序会停在select这里等待,直到被监视的文件描述符有一个或多个发生了状态改变;

(2)函数原型

int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds,
			struct timeval *timeout);

(3)参数

  • 参数nfds是需要监视的最大的文件描述符值+1;
  • readfds、writefds、exceptfds分别对应于需要检测的可读文件描述符的集合,可写文件描述符的集合及异常文件描述符的集合;这个集合的结构就是一个整数数组, 更严格的说, 是一个 “位图”. 使用位图中对应的位来表示要监视的文件描述符;
    提供了一组操作fd_set的接口, 来比较方便的操作位图
void FD_CLR(int fd, fd_set *set); // 用来清除描述词组set中相关fd 的位
int FD_ISSET(int fd, fd_set *set); // 用来测试描述词组set中相关fd 的位是否为真
void FD_SET(int fd, fd_set *set); // 用来设置描述词组set中相关fd的位
void FD_ZERO(fd_set *set); // 用来清除描述词组set的全部位
  • 参数timeout为结构timeval,用来设置select()的等待时间;timeval结构用于描述一段时间长度,如果在这个时间内,需要监视的描述符没有事件发生则函数返回,返回值为0。
    参数timeout取值
    NULL:则表示select()没有timeout,select将一直被阻塞,直到某个文件描述符上发生了事件;
    0:仅检测描述符集合的状态,然后立即返回,并不等待外部事件的发生;
    特定的时间值:如果在指定的时间段里没有事件发生,select将超时返回。

(4)函数返回值
执行成功则返回文件描述词状态已改变的个数;如果返回0代表在描述词状态改变前已超过timeout时间,没有返回;当有错误发生时则返回-1,错误原因存于errno,此时参数readfds,writefds, exceptfds和timeout的值变成不可预测。
错误值可能为
EBADF:文件描述词为无效的或该文件已关闭;
EINTR:此调用被信号所中断;
EINVAL:参数n 为负值;
ENOMEM:核心内存不足。

2、socket就绪条件

(1)读就绪的条件

  • socket内核中, 接收缓冲区中的字节数, 大于等于低水位标记SO_RCVLOWAT. 此时可以无阻塞的读该文件描述符, 并且返回值大于0;
  • socket TCP通信中, 对端关闭连接, 此时对该socket读, 则返回0;监听socket上有新的连接请求;
  • socket上有未处理的错误;

(2)写就绪的条件

  • socket内核中, 发送缓冲区中的可用字节数(发送缓冲区的空闲位置大小), 大于等于低水位标记
    SO_SNDLOWAT, 此时可以无阻塞的写, 并且返回值大于0;
  • socket的写操作被关闭(close或者shutdown)。对一个写操作被关闭的socket进行写操作, 会触发SIGPIPE信号;
  • socket使用非阻塞connect连接成功或失败之后;
  • socket上有未读取的错误;

3、select的特点

  • 可监控的文件描述符个数取决与sizeof(fd_set)的值. 我这边服务器上sizeof(fd_set)=512,每bit表示一个文件描述符,则我服务器上支持的最大文件描述符是512*8=4096;
  • 将fd加入select监控集的同时,还要再使用一个数据结构array保存放到select监控集中的fd;一是用于再select 返回后,array作为源数据和fd_set进行FD_ISSET判断。二是select返回后会把以前加入的但并无事件发生的fd清空,则每次开始select前都要重新从array取得fd逐一加入(FD_ZERO最先),扫描array的同时取得fd最大值maxfd,用于select的第一个参数。

4、select缺点

  • 每次调用select, 都需要手动设置fd集合, 从接口使用角度来说也非常不便;
  • 每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大;
  • 每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时很大;
  • select支持的文件描述符数量太小。

二、poll 模型

1、poll函数

(1)函数功能
让程序监视多个文件描述符的状态变化;程序会停在poll这里等待,直到被监视的文件描述符有一个或多个发生了状态改变;
(2)函数原型

#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);

// pollfd结构
struct pollfd 
int fd; /*文件描述符 */
short events; /* 监听的事件集合 */
short revents; /* 返回的事件集合 */
;

(3)参数

  • fds是一个poll函数监听的结构列表;每一个元素中,包含了三部分内容:文件描述符,监听的事件集合,返回的事件集合;
  • nfds表示fds数组的长度;
  • timeout表示poll函数的超时时间, 单位是毫秒(ms);
    (4)返回值
  • 返回值小于0, 表示出错;
  • 返回值等于0, 表示poll函数等待超时;
  • 返回值大于0, 表示poll由于监听的文件描述符就绪而返回。

2、socket就绪条件

与select相同

(1)读就绪的条件

  • socket内核中, 接收缓冲区中的字节数, 大于等于低水位标记SO_RCVLOWAT. 此时可以无阻塞的读该文件描述符, 并且返回值大于0;
  • socket TCP通信中, 对端关闭连接, 此时对该socket读, 则返回0;监听socket上有新的连接请求;
  • socket上有未处理的错误;

(2)写就绪的条件

  • socket内核中, 发送缓冲区中的可用字节数(发送缓冲区的空闲位置大小), 大于等于低水位标记
    SO_SNDLOWAT, 此时可以无阻塞的写, 并且返回值大于0;
  • socket的写操作被关闭(close或者shutdown)。对一个写操作被关闭的socket进行写操作, 会触发SIGPIPE信号;
  • socket使用非阻塞connect连接成功或失败之后;
  • socket上有未读取的错误;

3、poll的优点&&缺点

(1)优点
不同与select使用三个位图来表示三个fdset的方式,poll使用一个pollfd的指针实现

  • pollfd结构包含了要监视的event和发生的event,不再使用select“参数-值”传递的方式. 接口使用比select更方便;
  • poll并没有最大数量限制 (但是数量过大后性能也是会下降);

(2)缺点
poll中监听的文件描述符数目增多时

  • 和select函数一样,poll返回后,需要轮询pollfd来获取就绪的描述符;
  • 每次调用poll都需要把大量的pollfd结构从用户态拷贝到内核中;
  • 同时连接的大量客户端在一时刻可能只有很少的处于就绪状态, 因此随着监视的描述符数量的增长, 其效率也会线性下降。

三、epoll模型

1、epoll函数

epoll有三个系统调用

(1)epoll_create
创建一个epoll的句柄。自从linux2.6.8之后,size参数是被忽略的;用完之后, 必须调用close()关闭。

int epoll_create(int size);

(2)epoll_ctl
epoll的事件注册函数

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

它不同于select()是在监听事件时告诉内核要监听什么类型的事件,而是在这里先注册要监听的事件类型.

  • 第一个参数epfd是epoll_create()的返回值(epoll的句柄);
  • 第二个参数op表示动作,用三个宏来表示;
    取值:
    EPOLL_CTL_ADD :注册新的fd到epfd中;
    EPOLL_CTL_MOD :修改已经注册的fd的监听事件;
    EPOLL_CTL_DEL :从epfd中删除一个fd;
  • 第三个参数fd是需要监听的fd;
  • 第四个参数event是告诉内核需要监听什么事;
    events可以是以下几个宏的集合:
    EPOLLIN : 表示对应的文件描述符可以读 (包括对端SOCKET正常关闭);
    EPOLLOUT : 表示对应的文件描述符可以写;
    EPOLLPRI : 表示对应的文件描述符有紧急的数据可读 (这里应该表示有带外数据到来);
    EPOLLERR : 表示对应的文件描述符发生错误;
    EPOLLHUP : 表示对应的文件描述符被挂断;
    EPOLLET : 将EPOLL设为边缘触发(Edge Triggered)模式, 这是相对于水平触发(Level Triggered)来说的;
    EPOLLONESHOT:只监听一次事件, 当监听完这次事件之后, 如果还需要继续监听这个socket的话, 需要再次把这个socket加入到EPOLL队列里。

(3)epoll_wait
收集在epoll监控的事件中已经发送的事件

int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
  • 参数events是分配好的epoll_event结构体数组;
  • epoll将会把发生的事件赋值到events数组中 (events不可以是空指针,内核只负责把数据复制到这个
    events数组中,不会去帮助我们在用户态中分配内存);
  • maxevents告之内核这个events有多大,这个 maxevents的值不能大于创建epoll_create()时的size;
  • 参数timeout是超时时间 (毫秒,0会立即返回,-1是永久阻塞);
  • 如果函数调用成功,返回对应I/O上已准备好的文件描述符数目,如返回0表示已超时, 返回小于0表示函
    数失败。

2、epoll的使用过程

  • 调用epoll_create创建一个epoll句柄;
  • 调用epoll_ctl, 将要监控的文件描述符进行注册;
  • 调用epoll_wait, 等待文件描述符就绪;

3、epoll的优点(和 select 的缺点对应)

  • 接口使用方便:虽然拆分成了三个函数, 但是反而使用起来更方便高效. 不需要每次循环都设置关注的文件描述符, 也做到了输入输出参数分离;
  • 数据拷贝轻量:只在合适的时候调用 EPOLL_CTL_ADD 将文件描述符结构拷贝到内核中, 这个操作并不频繁(而select/poll都是每次循环都要进行拷贝);
  • 事件回调机制:避免使用遍历, 而是使用回调函数的方式, 将就绪的文件描述符结构加入到就绪队列中,epoll_wait 返回直接访问就绪队列就知道哪些文件描述符就绪;这个操作时间复杂度O(1)。即使文件描述符数目很多,效率也不会受到影响;
  • 没有数量限制:文件描述符数目无上限。

4、epoll对文件描述符就绪事件的触发方式

水平触发&&边缘触发;epoll默认状态下就是LT(水平触发)工作模式

(1)水平触发Level Triggered 工作模式
满足条件就会一直触发 -> 比如你在打游戏,你的妈妈叫你去吃饭,你没去,她又会过来叫你

epoll的默认工作方式,select和poll也都是水平触发方式

  • 可读事件
    只要接收缓冲区当中的数据大于低水位标记(1字节),就会一直触发可读事件,直到接收缓冲区当中没有数据可读(接收缓冲区当中的数据低于低水位标记)

  • 可写事件
    只要发送缓冲区当中的空间大于低水位标记(1字节),就会一直触发可写事件,直到发送缓冲区当中没有空间可写(发送缓冲区当中的空间低于低水位标记)

(2)边缘触发Edge Triggered工作模式
满足条件后只会触发一次 -> 比如你在打游戏,你的妈妈叫你去吃饭,只会叫你一次

只有epoll才拥有

  • 设置
    设置文件描述符对应的事件结构的时候,只需要在事件结构当中的事件变量中按位或上EPOLLET即可

- 可读事件
只有当新的数据到来的时候,才会触发可读,否则通知一次之后,就不再通知了 -> 每次到来一个新的数据,只会通知一次,如果应用程序没有将接收缓冲区的数据读完(没有读完的数据留在缓冲区之中,下次触发就从这里开始),也不会再次通知,直到新的数据到来,才会触发可读事件,因此需要尽量将数据读完

  • 可写事件
    只有当发送缓冲区之中剩余空间从不可写变成可写的时候,才会触发一次可写事件就绪

对于ET模式而言,如果就绪事件产生,一定要把握好机会,对于可读事件,将数据读完,对于可写事件,将数据写完;ET模式结合了循环将数据进行读取和发送,不是频繁的进行通知,因此效率比较高。

5、epoll的使用场景

epoll的高性能,是有一定的特定场景的;如果场景选择的不适宜,epoll的性能可能适得其反

  • 对于多连接, 且多连接中只有一部分连接比较活跃时, 比较适合使用epoll;
    例如:典型的一个需要处理上万个客户端的服务器, 例如各种互联网APP的入口服务器, 这样的服务器就很适合epoll;
  • 如果只是系统内部, 服务器和服务器之间进行通信, 只有少数的几个连接, 这种情况下用epoll就并不合适. 具体要根据需求和场景特点来决定使用哪种IO模型。

以上是关于高级IO模型的主要内容,如果未能解决你的问题,请参考以下文章

高级IO模型

高级IO模型

#yyds干货盘点#高级IO模型之kqueue和epoll

详解--高级IO

Linux----高级IO(参考UNP)

Linux-高级IO