【Linux高级IO】多路转接之epoll
多路复用之epoll
- 一,认识epoll
- 二,epoll的相关接口
- 1. epoll_create
- 2. epoll_ctl
- 3. epoll_wait
- 三,epoll的原理
- 四,epoll的两种工作模式(ET和LT)
- 1. 两种工作模式
- 2. 对比ET和LT
- 五,总结
在了解到select和poll的缺点后,我们现在来看看epoll。epoll虽然是改进后的poll,但是在底层已经完全不一样了。
一,认识epoll
epoll有三个系统调用,但是在使用上还是非常的方便,下面来直接看看epoll的接口
二,epoll的相关接口
1. epoll_create
使用 epoll 时先调用这个,用于创建一个 epoll模型
,返回值是个 文件描述符
,至于原理我们原理部分解释
int epoll_create(int size);
这里要注意一下:
- 自从linux2.6.8之后,size参数是被忽略的.
- 用完之后, 必须调用
close()
关闭
2. epoll_ctl
再创建完 epoll 句柄后,就要来进行事件注册了。这个接口是用户告诉内核,关心哪个 fd 的哪个 events事件,op 参数表示对 fd 的操作,epfd 是 epoll_create 的返回值。
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
对于第二个参数的取值:
- EPOLL_CTL_ADD :注册新的fd到epfd中;
- EPOLL_CTL_MOD :修改已经注册的fd的监听事件;
- EPOLL_CTL_DEL :从epfd中删除一个fd;
第三个参数:
epoll特有的数据类型:
typedef union epoll_data {void *ptr;int fd;uint32_t u32;uint64_t u64;
} epoll_data_t;struct epoll_event {uint32_t events; /* Epoll events */epoll_data_t data; /* User data variable */
};
这个 epoll_event 结构中有两个,一个是events表示要关心的哪个事件,另一个
poll_data_t data 是一个联合体,里面可以存放文件描述符等,只不过是提供给用户用的。
events是下面几个宏的集合:
- EPOLLIN : 表示对应的文件描述符可以读 (包括对端SOCKET正常关闭);
- EPOLLOUT : 表示对应的文件描述符可以写;
- EPOLLPRI : 表示对应的文件描述符有紧急的数据可读 (这里应该表示有带外数据到来);
- EPOLLERR : 表示对应的文件描述符发生错误;
- EPOLLHUP : 表示对应的文件描述符被挂断;
- EPOLLET : 将EPOLL设为边缘触发(Edge Triggered)模式, 这是相对于水平触发(Level Triggered)来说的.
- EPOLLONESHOT:只监听一次事件, 当监听完这次事件之后, 如果还需要继续监听这个socket的话, 需要再次把这个socket加入到EPOLL队列里
3. epoll_wait
epoll这里用户到内核,内核到用户是两个独立的接口,所以这个接口是内核告诉用户,你要我关心的这些fd的哪些事件就绪了
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
- 我们在使用时需要定义一个 struct epoll_event结构的数组,然后传入这个接口。epoll将会把发生的事件赋值到events数组中 (events不可以是空指针,内核只负责把数据复制到这个events数组中,不会去帮助我们在用户态中分配内存)
- maxevents 是告诉内核这个 events 有多大,这个 maxevents 的值不能大于创建 epoll_create时 的 size
- 参数timeout是超时时间 (毫秒,0会立即返回,-1是永久阻塞)
- 返回值和poll一样
三,epoll的原理
知道了epoll的一些接口后,我们来看看epoll的原理
首先,OS是知道底层网卡上是否有数据的,当网卡有数据时,会向OS发送
硬件中断
,而select和poll不知道,因为遍历的是 fd 对应的 struct file 中是否有数据
当epoll_create时,内核中会创建一个红黑树
,红黑树中每一个节点都表示用户要求关心的fd上的事件。同时还会维护一个就绪队列
,每一个节点表示内核告诉用户,要求关心的哪些fd上的事件已经就绪。
也就是说epoll_create时,内核中会创建一个
epoll模型
,创建一个红黑树和就绪队列
同时,在网卡驱动层,会有一个回调机制,用来直接检测网卡的数据 :
- 当网卡有数据时,确认是否关心
- 在就绪队列中,形成新节点,表示哪个 fd 上的哪个事件已经就绪
其次,应用层面上 epoll_ctl 就是在向底层的红黑树通过 op 参数进行增删改,和 select 和 poll 自己维护的数组类似,只不过 epoll 是在内核中维护不用上层自己维护。
而 epoll_wait 就是向就绪队列中拿取已经就绪的节点
- epoll_wait检测底层是否有数据,时间复杂度为O(1)
- epoll_wait获取所有就绪事件的时间复杂度是O(N),必然的,因为要遍历就绪队列
细节1:
如果epoll_wait是传入的数组满了怎么办? 满了就会返回,同时就绪队列会保存就绪的事件节点,只要队列有节点,就绪事件就会一直产生,epoll_wait继续取就可以
细节2:
epoll_wait的返回值表示 所有的返回事件会严格按照顺序放在events中,返回值表示就绪个数
那么关于epoll_create的返回值?其实就是文件描述符!
epoll模型 也就是一个 数据结构 ,里面指向了红黑树和就绪队列和注册的回调,叫eventpoll
- 那么一个进程可以用epoll_create,其他进程也可以,所以OS对这些eventpoll要进行
先描述再组织
- 另一方面,linux下一切皆文件,当进程创建 epoll模型 时,和文件操作一样,该进程的文件描述符表中会分配文件描述符给 epoll ,其 struct file结构体 中就会指向 eventpoll 。这样就和其他文件一样对 epoll 进行了管理
总结一下, epoll的使用过程就是:
- 调用epoll_create创建一个epoll句柄;
- 调用epoll_ctl, 将要监控的文件描述符进行注册;
- 调用epoll_wait, 等待文件描述符就绪
四,epoll的两种工作模式(ET和LT)
1. 两种工作模式
epoll的工作模式就是 epoll 给用户提供的事件就绪通知的策略
epoll有2种工作模式:
- 水平触发(LT)
- 边缘触发(ET)
epoll在默认下是LT模式
LT模式(默认):就是在epoll时,底层事件就绪后,epoll就会一直通知上层 ,
ET模式:底层有数据只通知一次,直到下次数据发生变化(收到新数据)再通知
2. 对比ET和LT
ET模式比更高效,为什么?
- 因为在ET通知策略中,没有无效的通知,LT可能会重复通知
- ET只通知一次,就会倒逼上层把本轮的数据取完,这样就会导致tcp底层给发送方通告一个更大的窗口,使发送方滑动窗口更大,那么从概率上提高双方通信效率
如何保证ET模式下,把fd缓冲区数据全部取完?
循环读取,直到读完,但是读完后不想阻塞,就要采用非阻塞读取
所以ET模式下必须以非阻塞状态进行IO
假设有下面一个场景:服务器接受到一个10k的请求, 会向客户端返回一个应答数据. 如果客户端收不到应答, 不会发送第二个10k请求
如果服务端写的代码是阻塞式的read, 并且一次只 read 1k 数据的话(read不能保证一次就把所有的数据都读出来,参考 man 手册的说明, 可能被信号打断), 剩下的9k数据就会待在缓冲区中
此时由于 epoll 是ET模式, 并不会认为文件描述符读就绪. epoll_wait 就不会再次返回. 剩下的 9k 数据会一直在缓冲区中. 直到下一次客户端再给服务器写数据. epoll_wait 才能返回。
但是服务端只有在读取完10K后才会给客户端响应,客户端收不到响应,也就不会再次发送请求,那么服务端也不会继续读取。
所以, 为了解决上述问题(阻塞read不一定能一下把完整的请求读完), 于是就可以使用非阻塞轮训的方式来读缓冲区,保证一定能把完整的请求都读出来。
如果是LT没这个问题. 只要缓冲区中的数据没读完, 就能够让 epoll_wait 返回文件描述符读就绪。
五,总结
学习完了多路转接epoll,我们就可以做到让一个进程或者线程去同时处理多个IO,那么在对于服务器高性能的要求场景下使用epoll就是个很好的选择。