可是我们一般选择多是多路复用,而不是异步IO。为什么呢?因为首先异步IO看起来是让别人钓鱼,但是在程序中我们就要创建线程或子进程去做事,本身就要耗费较大资源,而且一旦设计多线程,就可能会出现很多问题,大幅度提高程序的复杂性!所以我们一般更青睐于多路转接的方法!
复制一个现有的描述符(
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的工作模式!
epoll有2种工作方式-水平触发(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的多路转接翻译服务器的代码贴在这里,这篇文章就到底为止了!
感谢观看!