高级IO——多路转接

本文介绍了五种IO模型,包括阻塞、非阻塞、信号驱动、IO多路转接和异步IO,重点讲解了select、poll和epoll的原理及其在减少等待时间和资源消耗方面的优势,以及如何解决epoll在多线程环境中的惊群效应问题。
摘要由CSDN通过智能技术生成

高级IO——五种IO模型

首先我们之前在基础IO部分就学过IO的过程分等待过程和读写过程!

比如我们的scanf除了从键盘缓冲区读取数据所花的时间,主要的时间花费放在了等你输入的过程!

所以我们如果想提高我们的IO效率,我们除了要缩减读取数据,主要的是减少花费在等你输入的时间!可是我们肯定不可能决定用户的输入速度,所以我们就只能让这个等待时间我们的程序去做其他事情,这样就可以减少等待IO花费的时间了!

由此我们就可以引出我们的5种IO模型了!(我们用钓鱼做例子)

阻塞IO: 在内核将数据准备好之前, 系统调用会一直等待. 所有的套接字, 默认都是阻塞方式
老老实实抓着一根杆子钓鱼,鱼来了就拉钩,否则就一直等!
非阻塞IO: 如果内核还未将数据准备好, 系统调用仍然会直接返回, 并且返回EWOULDBLOCK错误码.
把杆放在池塘里面就走了,过一段时间回来一次看一看,有鱼就掉,没鱼就离开去干其他事情。
信号驱动IO: 内核将数据准备好的时候, 使用SIGIO信号通知应用程序进行IO操作
放个信号器,来鱼了信号器报警,听到信号以后就去拉杆!
IO多路转接: 实际上最核心在于IO多路转接能够同时等待多个文件 描述符的就绪状态.
一次性放很多鱼竿,然后不停的检测所有鱼竿的情况,那个鱼竿有鱼就去处理那一根鱼竿!
 
异步IO: 由内核在数据拷贝完成时, 通知应用程序(而信号驱动是告诉应用程序何时可以开始拷贝数据).
直接让别人去钓鱼,等别人调完鱼,直接去取鱼!
前面四种IO我们称之为同步IO,我们先不看异步IO,那种钓鱼的效率最高?
当然是IO多路转接?为什么?因为杆多!!!鱼竿多自然钓鱼就多,因为不同鱼竿的等待时间重叠了!!!本身就是提高效率!
可是我们一般选择多是多路复用,而不是异步IO。为什么呢?因为首先异步IO看起来是让别人钓鱼,但是在程序中我们就要创建线程或子进程去做事,本身就要耗费较大资源,而且一旦设计多线程,就可能会出现很多问题,大幅度提高程序的复杂性!所以我们一般更青睐于多路转接的方法!
接下来我们来学习系统专门为多路复用设计的接口!
首先我们来介绍select
在此之前由于我们的fd都默认是阻塞式IO,所以我们先要将想要的fd设置为非阻塞式IO。
这个时候我们就要先用fcntl去设置相应的fd呢!
复制一个现有的描述符( cmd=F_DUPFD .
获得 / 设置文件描述符标记 (cmd=F_GETFD F_SETFD).
获得 / 设置文件状态标记 (cmd=F_GETFL F_SETFL).
获得 / 设置异步 I/O 所有权 (cmd=F_GETOWN F_SETOWN).
获得 / 设置记录锁 (cmd=F_GETLK,F_SETLK F_SETLKW).
比如下面的程序就是先获取一个程序的旧的标志位,然后我们再设置其为非阻塞!
接下来我们就可以学习select呢
返回值:n>0的时候就代表有多少个fd已经就绪,n==0就是没有就绪但是倒计时已经到了,n<0就是出错了!
timeval是一个结构体,如下图,分别是秒和微秒!
并且timeval还是个输入输出形参数!
也即比如我们设置的是5,0也即5秒返回一次,如果过了两秒就有就绪了,那么timeval这个时候就会使3,0.
然后我们再来了解最重要的是fd_set是什么?
其实就是一张位图,readfds里面响应的位置如果被设置为1,则关心相应事件的读,writefds则关心写!
当传入的时候我们就是告诉内核那个fd我们要关心,返回的时候,传出的就是那个fd已经可以读取了!不会产生等待了!
我们在这里先讲一下select的一些限制,既然fd_set是结构体,那么其中等待的fd有没有上限呢?是多少呢?
不同的系统可能跑出来的是不一样的,但是大差不差!
并且我们每一次都要重新给select传表,我们还要用一个辅助数组提前记录哪些fd我们要监听,也是一个开销!
所以我们可以总结一下select的优缺点
优点:主要是已经实现了多路转接了!
缺点:
1.fd有上限!
2.输入输出型参数多,需要不停的数据拷贝和遍历修改,会导致效率低下!
3.而且还要辅助数组记录我们要关心的fd
下面是一个基于select的多路转接的单词翻译器的实现!
然后我们在学习epoll之前,先看一下poll!
第一个参数就是一个struct pollfd的数组
第二个参数就是数组中元素的个数
第三个参数与select一致
至于struct pollfd的结构主要包含关心的fd,还有两个短整形变量!
如果想要添加指令就只用将短整数&上对应的宏就可以了!!!
其中events是传入给poll的,revents是poll要写入的,我们进行读取就可以!
poll相对于select首先克服了数量有限的问题,并且减少了遍历,并且不用我们维护辅助数组了!
也更容易编写!!!
但是我们仍然需要去不停遍历整个数组,那么当数量增多以后,主要矛盾就从等待变成了遍历了!
如果学会了select,那poll难度应该不大,就不再过多赘述!而epoll则在此基础之上又大幅度提高效率,所以下面我们来学习epoll!!!
epoll 的事件注册函数 .
它不同于 select() 是在监听事件时告诉内核要监听什么类型的事件 , 而是在这里先注册要监听的事件类型 .
第一个参数是 epoll_create() 的返回值 (epoll 的句柄 ).
第二个参数表示动作,用三个宏来表示 .
第三个参数是需要监听的 fd.
第四个参数是告诉内核需要监听什么事 .
第二个参数的取值 :
EPOLL_CTL_ADD :注册新的 fd epfd 中;
EPOLL_CTL_MOD :修改已经注册的 fd 的监听事件;
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 监控的事件中已经发送的事件 .
参数 events 是分配好的 epoll_event 结构体数组 .
epoll 将会把发生的事件赋值到 events 数组中 (events 不可以是空指针,内核只负责把数据复制到这个 events数组中,不会去帮助我们在用户态中分配内存 ).
maxevents 告之内核这个 events 有多大,这个 maxevents 的值不能大于创建 epoll_create() 时的 size.
参数 timeout 是超时时间 ( 毫秒, 0 会立即返回, -1 是永久阻塞 ).
如果函数调用成功,返回对应 I/O 上已准备好的文件描述符数目,如返回 0 表示已超时 , 返回小于 0 表示函 数失败
学了epoll的基本使用以后,我们来学习一下epoll的原理!
当某一进程调用 epoll_create 方法时, Linux 内核会创建一个 eventpoll 结构体,这个结构体中有两个成 员与epoll 的使用方式密切相关 .
主要是一个红黑树和一个双链表的队列!
每个被检测的fd都会被放入红黑树,一旦事件就绪,就会调用回调函数,让操作系统从红黑树中找到相关的rbn成员然后读取其信息,然后在等待队列中添加,这样就我们进行读取事件的时候就是在等待队列中读取事件了,并且在这个过程中事件主动回调,时间复杂度为o(1),比我们之前的select和poll效率拥有了质的提升!!!
总结一下 , epoll 的使用过程就是三部曲 :
调用 epoll_create 创建一个 epoll 句柄 ;
调用 epoll_ctl, 将要监控的文件描述符进行注册 ;
调用 epoll_wait, 等待文件描述符就绪 ;
epoll 的优点 ( select 的缺点对应 )
接口使用方便 : 虽然拆分成了三个函数 , 但是反而使用起来更方便高效 . 不需要每次循环都设置关注的文 件描述符, 也做到了输入输出参数分离开
数据拷贝轻量 : 只在合适的时候调用 EPOLL_CTL_ADD 将文件描述符结构拷贝到内核中 , 这个操作并不频 繁( select/poll 都是每次循环都要进行拷贝 )
事件回调机制 : 避免使用遍历 , 而是使用回调函数的方式 , 将就绪的文件描述符结构加入到就绪队列中 , epoll_wait 返回直接访问就绪队列就知道哪些文件描述符就绪 . 这个操作时间复杂度 O(1). 即使文件描述 符数目很多, 效率也不会受到影响 . 没有数量限制: 文件描述符数目无上限
我们再来讲一下epoll的工作模式!
epoll2种工作方式-水平触发(LT)和边缘触发(ET)
假如有这样一个例子 :
我们已经把一个 tcp socket 添加到 epoll 描述符
这个时候 socket 的另一端被写入了 2KB 的数据
调用 epoll_wait ,并且它会返回 . 说明它已经准备好读取操作
然后调用 read, 只读取了 1KB 的数据
继续调用 epoll_wait......
水平触发 Level Triggered 工作模式
epoll 默认状态下就是 LT 工作模式 .
epoll 检测到 socket 上事件就绪的时候 , 可以不立刻进行处理 . 或者只处理一部分 .
如上面的例子 , 由于只读了 1K 数据 , 缓冲区中还剩 1K 数据 , 在第二次调用 epoll_wait , epoll_wait
仍然会立刻返回并通知 socket 读事件就绪 .
直到缓冲区上所有的数据都被处理完 , epoll_wait 才不会立刻返回 .
支持阻塞读写和非阻塞读写
边缘触发 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.
对比 LT ET
LT epoll 的默认行为 . 使用 ET 能够减少 epoll 触发的次数 . 但是代价就是强逼着程序猿一次响应就绪过程中就把 所有的数据都处理完.
相当于一个文件描述符就绪之后 , 不会反复被提示就绪 , 看起来就比 LT 更高效一些 . 但是在 LT 情况下如果也能做到
每次就绪的文件描述符都立刻处理, 不让这个就绪被重复提示的话 , 其实性能也是一样的 .
另一方面 , ET 的代码复杂程度更高了 .
理解 ET 模式和非阻塞文件描述符
使用 ET 模式的 epoll, 需要将文件描述设置为非阻塞 . 这个不是接口上的要求 , 而是 " 工程实践 " 上的要求 .
假设这样的场景 : 服务器接受到一个 10k 的请求 , 会向客户端返回一个应答数据 . 如果客户端收不到应答 , 不会发送第 二个10k 请求 .
如果服务端写的代码是阻塞式的read, 并且一次只 read 1k 数据的话 (read 不能保证一次就把所有的数据都读出来 , 参考 man 手册的说明 , 可能被信号打断 ), 剩下的 9k 数据就会待在缓冲区中
此时由于 epoll ET 模式 , 并不会认为文件描述符读就绪 . epoll_wait 就不会再次返回 . 剩下的 9k 数据会一直在缓 冲区中. 直到下一次客户端再给服务器写数据 . epoll_wait 才能返回 但是问题来了.
服务器只读到 1k 个数据 , 10k 读完才会给客户端返回响应数据 .
客户端要读到服务器的响应 , 才会发送下一个请求 客户端发送了下一个请求, epoll_wait 才会返回 , 才能去读缓冲区中剩余的数据
所以 , 为了解决上述问题 ( 阻塞 read 不一定能一下把完整的请求读完 ), 于是就可以使用非阻塞轮训的方式来读缓冲区 , 保证一定能把完整的请求都读出来.
而如果是 LT 没这个问题 . 只要缓冲区中的数据没读完 , 就能够让 epoll_wait 返回文件描述符读就绪 .
也即如果设置为非阻塞的时候,我们反复读取,一旦读完就会出错返回,如果阻塞模式,我们就会阻塞在读的地方,这显然是我们不能接受的,所以我们要把读设为非阻塞模式!!!
epoll 的使用场景
epoll 的高性能 , 是有一定的特定场景的 . 如果场景选择的不适宜 , epoll 的性能可能适得其反 .
对于多连接 , 且多连接中只有一部分连接比较活跃时 , 比较适合使用 epoll.
例如 , 典型的一个需要处理上万个客户端的服务器 , 例如各种互联网 APP 的入口服务器 , 这样的服务器就很适合 epoll.
如果只是系统内部 , 服务器和服务器之间进行通信 , 只有少数的几个连接 , 这种情况下用 epoll 就并不合适 . 具体要根 据需求和场景特点来决定使用哪种IO模型

1.epoll惊群效应产生的原因
在Linux下使用epoll编写过socket的服务端程序,在多线程环境下可能会遇到epoll的惊群效应。那么什么是惊群效应呢。其产生的原因是什么呢?
在多线程或者多进程环境下,有些人为了提高程序的稳定性,往往会让多个线程或者多个进程同时在epoll_wait监听的socket描述符。当一个新的链接请求进来时,操作系统不知道选派那个线程或者进程处理此事件,则干脆将其中几个线程或者进程给唤醒,而实际上只有其中一个进程或者线程能够成功处理accept事件,其他线程都将失败,且errno错误码为EAGAIN。这种现象称为惊群效应,结果是肯定的,惊群效应肯定会带来资源的消耗和性能的影响。
那么如何解决这个问题。

2.惊群问题的解决方法
多线程环境下解决惊群解决方法
这种情况,不建议让多个线程同时在epoll_wait监听的socket,而是让其中一个线程epoll_wait监听的socket,当有新的链接请求进来之后,由epoll_wait的线程调用accept,建立新的连接,然后交给其他工作线程处理后续的数据读写请求,这样就可以避免了由于多线程环境下的epoll_wait惊群效应问题。

最后再把基于epoll的多路转接翻译服务器的代码贴在这里,这篇文章就到底为止了!

感谢观看!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值