高级IO

目录

一、五种IO模型

二、高级IO的重要概念

2.1同步通信&异步通信

2.2阻塞&非阻塞

2.3非阻塞IO

三、IO多路转接之select

3.1初识select

3.2select函数原型

3.3理解select的执行过程

3.4 socket就绪条件

3.4select的特点

3.5select的缺点

3.6select使用示例: 检测标准输入输出 

3.7使用 select 实现字典服务器

四、IO多路转接之poll

4.1poll函数接口

4.2socket就绪条件

4.3poll的优点

4.4poll的缺点

4.5poll示例: 使用poll监控标准输入

五、I/O多路转接之epoll

5.1epoll初识

5.2epoll的相关系统调用

5.3epoll的工作原理

5.4epoll的优点(和select对应)

5.5epoll的工作方式

5.5.1水平触发Level Triggered 工作模式

5.5.2边缘触发Edge Triggered工作模式

5.6对比LT和ET

5.7理解ET模式和非阻塞文件描述符

5.8epoll示例: epoll服务器(LT模式)

5.9epoll示例: epoll服务器(ET模式)


一、五种IO模型

  1. 阻塞IO: 在内核将数据准备好之前,系统调用会一直等待,所有的套接字,默认都是阻塞方式
  2. 非阻塞IO: 如果内核还未将数据准备好,系统调用仍然会直接返回,并且返回EWOULDBLOCK错误码,需要轮询,对CPU的消耗比较大
  3. 信号驱动IO: 内核将数据准备好的时候,使用SIGIO信号通知应用程序进行IO操作
  4. IO多路转接: 虽然从流程图上看起来和阻塞IO类似,实际上最核心在于IO多路转接能够同时等待多个文件描述符的就绪状态
  5. 异步IO: 由内核在数据拷贝完成时,通知应用程序(而信号驱动是告诉应用程序何时可以开始拷贝数据)
  6. 任何IO过程中,都包含两个步骤,第一是等待,第二是拷贝,而且在实际的应用场景中,等待消耗的时间往往都远远高于拷贝的时间,让IO更高效,最核心的办法就是让等待的时间尽量少

二、高级IO的重要概念

2.1同步通信&异步通信

  • 同步通信:发出一个调用时,没有得到结果就一直等待不返回,只有得到结果了调用才会有返回,才有返回值,也就是说是调用者主动等待的一个结果
  • 异步通信:调用在发出之后,这个调用就直接返回了,所以没有返回结果,换句话说,当一个异步过程调用发出后,调用者不会立刻得到结果,而是在调用发出后,被调用者通过状态、通知来通知调用者,或通过回调函数处理这个调用
  • 同步与互斥:同步是指在多线程或者多进程的场景下,各个进程或者线程按照一定的工作次序等待某种资源的就绪,比如访问临界资源
  • 要注意区分同步通信和同步互斥之间的关系

2.2阻塞&非阻塞

  • 阻塞调用:一个线程在调用返回之前,线程会被挂起,只有调用返回了线程才会返回
  • 非阻塞调用:在调用结果未返回之前,不会将调用线程阻塞挂起

2.3非阻塞IO

  • 一个文件描述符,默认都是阻塞IO
#include <unistd.h>
#include <fcntl.h>
int fcntl(int fd, int cmd, ... /* arg */ );
  • 传入的cmd的值不同,后面追加的参数也不相同
  • fcntl函数有5种功能:
  1. 复制一个现有的描述符(cmd=F_DUPFD)
  2. 获得/设置文件描述符标记(cmd=F_GETFD或F_SETFD) 
  3. 获得/设置文件状态标记(cmd=F_GETFL或F_SETFL)
  4. 获得/设置异步I/O所有权(cmd=F_GETOWN或F_SETOWN)
  5. 获得/设置记录锁(cmd=F_GETLK,F_SETLK或F_SETLKW)

三、IO多路转接之select

3.1初识select

select系统调用是用来让我们的程序监视多个文件描述符的状态变化的,程序会停在select这里等待,直到被监视的文件描述符有一个或多个发生了状态改变

3.2select函数原型

select的函数原型如下: #include <sys/select.h>

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

参数解释:

  • 参数nfds是需要监视的最大的文件描述符值+1
  • rdset,wrset,exset分别对应于需要检测的可读文件描述符的集合,可写文件描述符的集合及异常文件描 述符的集合
  • 参数timeout为结构timeval,用来设置select()的等待时间

参数timeout取值:

  1. NULL:则表示select()没有timeout,select将一直被阻塞,直到某个文件描述符上发生了事件
  2. 0:仅检测描述符集合的状态,然后立即返回,并不等待外部事件的发生
  3. 特定的时间值:如果在指定的时间段里没有事件发生,select将超时返回

关于fd_set的结构:

 

  • 其实这个结构就是一个整数数组,更严格的说, 是一个 "位图",使用位图中对应的位来表示要监视的文件描述符,提供了一组操作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的全部位

关于timevalt结构:

  • timeval结构用于描述一段时间长度,如果在这个时间内,需要监视的描述符没有事件发生则函数返回,返回值为 0

函数返回值:

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

 常见的程序片段:

fs_set readset;
FD_SET(fd,&readset);
select(fd+1,&readset,NULL,NULL,NULL);
if(FD_ISSET(fd,readset)){……}

3.3理解select的执行过程

  1. 执行fd_set set
  2. FD_ZERO(&set),则set用位表示是0000,0000
  3. 若fd=5,执行FD_SET(fd,&set),后set变为0001,0000(第5位置为1)
  4. 若再加入fd=2,fd=1,则set变为0001,0011
  5. 执行 select(6,&set,0,0,0)阻塞等待
  6. 若fd=1,fd=2上都发生可读事件,则select返回,此时set变为 0000,0011
  7. 注意:没有事件发生的fd=5被清空 

3.4 socket就绪条件

读就绪:

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

写就绪:

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

异常就绪:

  • socket上收到带外数据,关于带外数据,和TCP紧急模式相关(回忆TCP协议头中, 有一个紧急指针的字段)

3.4select的特点

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

3.5select的缺点

  1. 支持的文件描述符取决于fd_set的大小,有上限
  2. 每次都要手动设置fd_set,接口使用不方便
  3. 需要额外的一个数组来维护等待监控的fd
  4. 每次调用select的时候,需要把fd集合从用户态拷贝到内核态,需要一定的开销
  5. 每次调用select都需要在内核遍历所有的fd,用户也需要遍历额外的数组来找到最大的fd,这也都需要一定的开销

3.6select使用示例: 检测标准输入输出 

复习select函数的使用 · 9c13ecc · 阿赭/复习 - Gitee.com

3.7使用 select 实现字典服务器

select实现服务器 · f22d860 · 阿赭/复习 - Gitee.com

四、IO多路转接之poll

4.1poll函数接口

#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
// pollfd结构
struct pollfd 
{
   int   fd;         /* file descriptor */
   short events;     /* requested events */
   short revents;    /* returned events */
 };
  • fds是一个poll函数监听的结构列表
  • 每一个元素中,包含了三部分内容:文件描述符,监听的事件集合,返回的事件集合
  • nfds表示fds数组的长度
  • timeout表示poll函数的超时时间,单位是毫秒(ms) 
  • events和revents的取值

  • 返回结果:返回值小于0,表示出错;返回值等于0,表示poll函数等待超时;返回值大于0,表示poll由于监听的文件描述符就绪而返回

4.2socket就绪条件

同select

4.3poll的优点

  • 使用一个pollfd的指针实现,不同与select使用三个位图来表示三个集合
  • pollfd结构包含了要监视的event和发生的event,不再使用select“参数-值”传递的方式,接口使用比 select更方便
  • poll并没有最大数量限制 (但是数量过大后性能也是会下降)

4.4poll的缺点

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

4.5poll示例: 使用poll监控标准输入

复习poll函数 · bb9f1bd · 阿赭/复习 - Gitee.com

五、I/O多路转接之epoll

5.1epoll初识

按照man手册的说法: 是为处理大批量句柄而作了改进的poll,它是在2.5.44内核中被引进的,它几乎具备了之前所说的一切优点,被公认为下性能最好的多路I/O就绪通知方法

5.2epoll的相关系统调用

epoll_create

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

epoll_ctl

  • int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
  • epoll的事件注册函数,它不同于select()是在监听事件时告诉内核要监听什么类型的事件,而是在这里先注册要监听的事件类型
  • 第一个参数是epoll_create()的返回值(epoll的句柄)
  • 第二个参数表示动作,用三个宏来表示
  • 第三个参数是需要监听的fd
  • 第四个参数是告诉内核需要监听什么事
  • 第二个参数的取值:
  1. EPOLL_CTL_ADD :注册新的fd到epfd中
  2. EPOLL_CTL_MOD :修改已经注册的fd的监听事件
  3. EPOLL_CTL_DEL :从epfd中删除一个fd
  • struct epoll_event结构如下:
  • events可以是以下几个宏的集合:

EPOLLIN : 表示对应的文件描述符可以读 (包括对端SOCKET正常关闭)

EPOLLOUT : 表示对应的文件描述符可以写

EPOLLPRI : 表示对应的文件描述符有紧急的数据可读 (这里应该表示有带外数据到来); EPOLLERR : 表示对应的文件描述符发生错误

EPOLLHUP : 表示对应的文件描述符被挂断

EPOLLET : 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的

EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里.

epoll_wait

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

5.3epoll的工作原理

  • 当某一进程调用epoll_create方法时,Linux内核会创建一个eventpoll结构体,这个结构体中有两个成员与epoll的使用方式密切相关
struct eventpoll{ 
 .... 
 /*红黑树的根节点,这颗树中存储着所有添加到epoll中的需要监控的事件*/ 
 struct rb_root rbr; 
 /*双链表中则存放着将要通过epoll_wait返回给用户的满足条件的事件*/ 
 struct list_head rdlist; 
 .... 
};
  •  每一个epoll对象都有一个独立的eventpoll结构体,用于存放通过epoll_ctl方法向epoll对象中添加进来的事件
  • 这些事件都会挂载在红黑树中,如此,重复添加的事件就可以通过红黑树而高效的识别出来(红黑树的插入时间效率是lgn,其中n为树的高度)
  • 而所有添加到epoll中的事件都会与设备(网卡)驱动程序建立回调关系,也就是说,当响应的事件发生时会调用这个回调方法
  • 这个回调方法在内核中叫ep_poll_callback,它会将发生的事件添加到rdlist双链表中
  • 在epoll中,对于每一个事件,都会建立一个epitem结构体
struct epitem{ 
 struct rb_node rbn;//红黑树节点 
 struct list_head rdllink;//双向链表节点 
 struct epoll_filefd ffd; //事件句柄信息 
 struct eventpoll *ep; //指向其所属的eventpoll对象 
 struct epoll_event event; //期待发生的事件类型 
}
  • 当调用epoll_wait检查是否有事件发生时,只需要检查eventpoll对象中的rdlist双链表中是否有epitem元素即可
  • 如果rdlist不为空,则把发生的事件复制到用户态,同时将事件数量返回给用户,这个操作的时间复杂度是O(1)
  • 总结一下, epoll的使用过程就是三部曲:
  1. 调用epoll_create创建一个epoll句柄
  2. 调用epoll_ctl,将要监控的文件描述符进行注册
  3. 调用epoll_wait,等待文件描述符就绪

5.4epoll的优点(和select对应)

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

5.5epoll的工作方式

5.5.1水平触发Level Triggered 工作模式

  • epoll默认状态下就是LT工作模式
  • 当epoll检测到socket上事件就绪的时候,可以不立刻进行处理,或者只处理一部分
  • 如例子,由于只读了1K数据,缓冲区中还剩1K数据,在第二次调用 epoll_wait 时,epoll_wait 仍然会立刻返回并通知socket读事件就绪,直到缓冲区上所有的数据都被处理完,epoll_wait 才不会立刻返回
  • 支持阻塞读写和非阻塞读写

5.5.2边缘触发Edge Triggered工作模式

  • 如果我们在第1步将socket添加到epoll描述符的时候使用了EPOLLET标志, epoll进入ET工作模式

  • 当epoll检测到socket上事件就绪时,必须立刻处理
  • 例子,虽然只读了1K的数据,缓冲区还剩1K的数据,在第二次调用 epoll_wait 的时候epoll_wait 不会再返回了
  • 也就是说,ET模式下,文件描述符上的事件就绪后,只有一次处理机会
  • ET的性能比LT性能更高( epoll_wait 返回的次数少了很多),Nginx默认采用ET模式使用epoll
  • 只支持非阻塞的读写,select和poll其实也是工作在LT模式下,epoll既可以支持LT, 也可以支持ET

5.6对比LT和ET

LT是 epoll 的默认行为,使用 ET 能够减少 epoll 触发的次数,但是代价就是强逼着程序猿一次响应就绪过程中就把所有的数据都处理完,相当于一个文件描述符就绪之后,不会反复被提示就绪,看起来就比 LT 更高效一些,但是在 LT 情况下如果也能做到每次就绪的文件描述符都立刻处理,不让这个就绪被重复提示的话,其实性能也是一样的,另一方面,ET 的代码复杂程度更高了

5.7理解ET模式和非阻塞文件描述符

  • 假设这样的场景: 服务器接受到一个10k的请求,会向客户端返回一个应答数据,如果客户端收不到应答,不会发送第二个10k请求
  • 如果服务端写的代码是阻塞式的read,并且一次只 read 1k 数据的话(read不能保证一次就把所有的数据都读出来,(参考 man 手册的说明, 可能被信号打断),剩下的9k数据就会待在缓冲区中
  • 此时由于 epoll 是ET模式,并不会认为文件描述符读就绪,epoll_wait 就不会再次返回,剩下的 9k 数据会一直在缓冲区中,直到下一次客户端再给服务器写数据 epoll_wait 才能返回
  • 服务器只读到1k个数据,要10k读完才会给客户端返回响应数据客户端要读到服务器的响应,才会发送下一个请求客户端发送了下一个请求, epoll_wait 才会返回,才能去缓冲区读取数据
  • 所以,为了解决上述问题必须倒逼程序员一次性把数据全部读取走。ET模式下的读写需要一直读或写直到出错(对于读,当读到的实际字节数小于请求字节数时就可以停止),而如果你的文件描述符如果不是非阻塞的,那这个一直读或一直写势必会在最后一次阻塞,这样就不能在阻塞在epoll_wait上了,造成其他文件描述符的任务饿死
  • 惊群效应?
  • 在多线程或者多进程环境下,有人为了提高程序的稳定性,会让多个线程或者进程同时去监听一个socket文件描述符,但是当有新的连接请求进来的时候,操作系统不知道该选择哪个线程或者进程来处理,或者同时唤醒多个进程或者线程,但实际上只有一个进程或者线程能够处理该socket,这必然会导致资源消耗问题和性能的降低,这就是epoll的惊群效应
  • 多线程环境下如何解决?让其中一个线程epoll_wait监听的socket,当有新的链接请求进来之后,由epoll_wait的线程调用accept,建立新的连接,然后交给其他工作线程处理后续的数据读写请求,这样就可以避免了由于多线程环境下的epoll_wait惊群效应问题
  • 多进程环境下如何解决?
  1. lighttpd的解决思路是无视惊群效应,仍然采用master/workers模式,每个子进程仍然管自己在监听的socket上调用epoll_wait,当有新的链接请求发生时,操作系统仍然只是唤醒其中部分的子进程来处理该事件,仍然只有一个子进程能够成功处理此事件,那么其他被惊醒的子进程捕获EAGAIN错误,并无视。
  2. nginx的解决思路:在同一时刻,永远都只有一个子进程在监听的socket上epoll_wait,其做法是,创建一个全局的pthread_mutex_t,在子进程进行epoll_wait前,则先获取锁

5.8epoll示例: epoll服务器(LT模式)

 复习epoll服务器(LT模式) · a4307a0 · 阿赭/复习 - Gitee.com

5.9epoll示例: epoll服务器(ET模式)

epoll服务器(ET模式) · 85779c9 · 阿赭/复习 - Gitee.com

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值