epoll
epoll和select一样也是用于实现IO多路转接,但epoll的效率要比select高的多
epoll相较于select或poll的优势:
1.select 和poll 主要基于线性表(可以理解为数组)来线性管理待检测 集合, 而epoll 则利用红黑树来管理待检测的集合, 这样epoll对待检测集合进行增删改查的效率就会更高
2.select和poll 在内核中是通过轮询检测的方式挨个来查看是否有文件描述符处于就绪状态, 这样在文件描述符比较多的情况下, 查询效率就会比较低, 而epoll是通过回调来实现检测(当有文件描述符读缓冲区有数据到来), 就会触发回调事件, 通知epoll 自己已经处于就绪状态,epoll就不用轮询的检测每个文件描述符, 效率高, 即便有大量的文件描述符需要检测, epoll的效率也不会下降
3.select和epoll 需要将待检测的集合频繁的在用户区和内核区进行拷贝, 会产生额外的开销, 效率也低, 而epoll中用户区和内核使用的是共享内存, 省去了不必要的拷贝,减少了系统开销
4.select 检测的文件描述符是有上限的, 而epoll所检测的文件描述符是没有上限的
// epoll 一共就3个函数, 来实现不同的操作 #include<sys/epoll.h> // 创建函数,创建一个epoll实例(创建一颗红黑树), 返回epoll的文件描述符 int epoll_create(int size); // 操作函数,主要是管理和操作epoll树上的文件描述符(增删改操作) int epoll_ctl(int epfd,int op,int fd,struct epoll_event* event); // 检测函数,检测epoll树中是否有就绪的文件描述符 int epoll_wait(int epfd,struct epoll_event* events,int maxevents,int timeout);
具体分析这3个函数
int epoll_create(int size);
epoll_create() 的作用:
就是创建红黑树模型的实例, 用于管理待检测文件描述符的集合
参数:
size: 大于0 即可, 随意设置
返回值:
-1: 创建失败
>0: 创建成功, 返回epoll的文件描述符epfd, 通过epfd就可以管理和访问这颗epoll实例
epoll_ctl()的作用:
对epoll树上的节点进行 增, 删, 改的操作
// 联合体, 多个变量共用同一块内存 typedef union epoll_data { void *ptr; int fd; // 通常情况下使用这个成员, 和epoll_ctl的第三个参数相同即可 uint32_t u32; uint64_t u64; } epoll_data_t; struct epoll_event { uint32_t events; /* Epoll events */ epoll_data_t data; /* User data variable */ }; int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
参数解释:
epfd: epoll_create() 的返回值, 通过epfd 可以访问epoll
op: 对应的增删改操作
EPOLL_CTL_ADD : 增 往epoll树上添加节点
EPOLL_CTL_MOD : 改 修改epoll树上的节点
EPOLL_CTL_DEL : 删 删除epoll树上指定的节点
fd : 要操作的文件描述符
event_event* event 传入参数
参数1 events : 委托epoll检测的事件类型
EPOLLIN: 读事件, 就是看要检测的fd 的读缓冲区是否有数据
EPOLLOUT: 写事件 就是看要检测的fd的写缓冲区是否有数据
EPOLLERR: 异常事件 就是看要检测的fd是否发生异常
参数2 epoll_data_t data:
一般就设置要检测的fd的值即可
当fd准备就绪时, 就是通过这个传入的fd来找多对应的文件描述符(在调用epoll_wait()时, 这个值就会被弹出)
函数返回值:
成功 0
失败 -1
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
参数解释
-
epfd: epoll树对应的文件描述符
-
events: 传出参数, 传出处于就绪状态的文件描述符, 这个参数就是通过epoll_ctl函数中的第4个参数传入的
-
maxevents 修饰第二个参数, epoll_event 结构体数组的最大容量(提前创建好结构体数组), 这个参数表示最多可以存储多个文件描述符的信息, 如果epoll树上的文件描述符很多, events一次没办法全部传出, 那么在下一次循环时, epoll_wait函数返回时, 会将剩下的信息传出
-
超时机制
-
0 不阻塞
-
-1 阻塞
-
>0 阻塞的秒数, 超时退出阻塞
-
函数返回值
-1 失败
>0 成功
=0 表示超时退出阻塞, 没有满足条件的文件描述符
epoll工作流程
-
创建监听的套接字
-
绑定IP和端口
-
设置监听
-
调用epoll_create 创建epoll实例
-
调用epoll_ctl 将监听的套接字加入到epoll树中, 同时指定要执行的操作
-
调用epoll_wait() 开始检测epoll树上的文件描述符(循环检测)
-
如果有满足条件的文件描述符, epoll_wait() 就会解除阻塞,并将满足条件的文件描述符的放入到epoll_wait的传出参数(也就是结构体数组)中, 这个结构体数组中存储的就是包含准备就绪的文件描述符的结构体
-
通过遍历传出的结构体数组, 取出对应的文件描述符
-
判断文件描述符是属于监听的还是通信的
-
如果是监听的文件描述符, 就调用accept函数创建新的通信的文件描述符, 并将这个新的文件描述符加入到epoll树上
-
如果的通信的文件描述符, 就进行通信的操作, 如果客户端与服务器断开连接, 就将对应的文件描述符从epoll树上移除掉
epoll的工作模式
两种工作模式 水平模式和边沿模式
其实就是内核通知的两种规则
水平模式: 内核缺省(默认)的工作模式, 支持阻塞和非阻塞模式
以读缓冲区为例, 只要内核检测到文件描述符的读缓冲区有数据, 读事件就会被触发,直到读缓冲区的数据全部被读完,epoll_wait 就会解除阻塞, 将对应的文件描述符传出,
比如 某个文件描述符的读缓冲区的数据比较多, 用户一次没有读完, 那么下一次调用epoll_wait 检测的时候, 内核还是会检测到, 读事件还是会被触发,并将这个文件描述符传出, 让用户继续读取, 直到读缓冲区的数据全部被读完,。 才停止触发读事件, epoll才停止通知
读事件: 当文件描述符的读缓冲区有数据, 读事件就会被触发, 内核就会通过epoll来通知使用者, epoll_wait 就会解除阻塞
写事件: 当文件描述符的写缓冲区可写, 写事件就会被触发, 内核就会通过epoll来通知使用者, epoll_wait 就会解除阻塞
边沿模式: 是高速的工作模式
当文件描述符从未就绪转变为就绪时, 内核会通过epoll通知使用者来处理, 然后内核就默认 使用者已经知道这个文件描述符处于就绪状态, 所有, 等下次再次检测到这个文件描述符还处于就绪状态时, 内核就不会再通知使用者来处理了, 只有当对该文件描述符的缓冲区进行处理, 使得该文件描述符从就绪态转为非就绪态, 当这个未就绪的文件描述符再次变为就绪态时, 内核才会再次通知(简单来说,就是当内核要检测的文件描述符的状态发生变化时, 内核才会通知), 边沿模式在很大程度上减少了内核通知(读写事件触发)的次数, 效率要比边沿模式高
读事件:
当读缓冲区有新的数据进入, 那么就会触发读事件, 没有新的数据进入, 就不会触发读事件
1.当缓冲区有新的数据进入, 就会触发读时间, epoll_wait 解除阻塞
2.当缓冲区的数据没有读完, 且没有新的数据进入, 那么就不会再次触发读事件, 只通知一次
写事件: 当写缓冲区可写, 写事件只会触发一次 当缓冲区被写满后, 再次变为可写, 写事件才会再次被触发
简单来说: epoll在边沿模式下,epoll_wait检测到文件描述符有新事件到来时, 才会通知, 如果不是新的事件, 就不通知
这样通知的次数就比水平模式少, 效率就会比较高
// 设置边沿模式 struct epoll_event ev; ev.events=EPOLLIN|EPOLLET; // 设置边沿模式
设置非阻塞
在边沿模式下, 就必须要设置文件描述符为非阻塞模式
因为边沿模式下, 在新数据到来时,只会通知一次, 所有 这就要求, 用户保证要在得到通知后,要把缓冲区的数据全部读完, 这就需要循环进行read操作, 但是,当最后读完缓冲区的数据后, 没有数据可读时, read就会默认陷入阻塞, 这就使得服务器的整个执行流程无法继续进行下去, 所以必须将文件描述符设置为非阻塞, 这样当循环读数据, 将数据读完后, 就会检测到缓冲区数据已读完, 就不会陷入阻塞
// 将套接字设置为非阻塞 int flag=fcntl(cfd,F_GETFL); flag|=O_NONBLOCK; fcntl(cfd,F_SETFL,flag);
当设置为非阻塞后, 循环进行read操作,就不怕被阻塞了, 当read读取到空的读缓冲区时, read函数会直接返回-1, 同时将erron指定为EAGAIN,用户只需对erron进行判断, 就可以知道read是否已经读完。