49.多路转接epoll
select缺陷:支持的文件描述符数量太少
poll缺陷:poll随着用户fd的增多,效率会发生一定程度的降低epoll核心定位:基于多个fd等待的就绪事件通知机制 --- 等,同select,poll
按照man手册说法:是为了处理大批量句柄(资源的代表,例如sockfd)而做了改进的poll
认识epoll接口
int epoll_create(int size); // 创建一个epoll模型
- size:弃用,epoll_create() creates a new epoll(7) instance. Since Linux 2.6.8, the size argument is ignored, but must be greater than zero;see HISTORY.
- 返回值:epoll的文件描述符
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *_Nullable event); //用户告诉内核,你要帮我关心哪一个fd,上面的event事件
- epfd:epoll_create的返回值文件描述符
- op:操作,EPOLL_CTL_ADD,EPOLL_CTL_MOD,EPOLL_CTL_DEL(注:DEL只能移除合法的文件描述符)
- fd:需要关心的文件描述符
- event:输入型参数,events表示用户告诉内核需要关心的事件
events取值:
- 返回值:成功0返回,失败-1返回,错误码指明错误(同非阻塞调用)
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout); //内核通知用户:你让我关心的fd们,事件已经就绪(输出的就是已经就绪的,不需要轮询判断)
- epfd:epoll文件描述符
- events:输出型参数,用户定义的数组或申请好的一块内存
- maxevents:events数组的长度
- timeout:等待时间,同poll
- 返回值:同select和poll
特别注意:输出型参数struct epoll_event结构内uint32_t events是指的已经就绪的事件,和add时添加的有区别,因为可以添加WR,但只就绪了R,用的时候要判断。
这就引出另一个问题,不是说底层红黑树和就绪队列的节点是共享的,如果这个就绪队列中events事件会被修改,那么底层红黑树节点(我epoll_ctl时add进去的)不是跟着变了?
不会的,在epitem中,add的event和revents是分别存储的(rb_tree只关心event,就绪队列只关心revents)
epoll的原理
- epoll_create创建一个epoll模型(红黑树、就绪队列、回调队列),得到epfd
epoll_create -> 创建一个struct eventpoll(存放着红黑树、就绪队列)-> 对于每一个事件struct epitem(存放着其对应的 rb_node 和 就绪队列节点)-> 红黑树和就绪队列存的都是eptiem- epoll_ctl对于红黑树的增删改(用户拷贝到内核),本质:用户告诉内核,你要帮我关心哪个fd上的哪些事件
- epoll_wait将就绪队列中的数据由内核拷贝到用户,本质:内核告诉用户,你要我关心的哪些fd对应的事件已经就绪了
获取就绪事件时间复杂度:O(1)(只需要关心就绪队列即可)
检测是否有就绪事件:O(1)(红黑树每一个节点fd都有与之对应的struct epoll_entry,里面都注册了回调机制,将来事件就绪,直接回调“激活”rb_node到就绪队列中,暂时)
网络协议栈中,存在一种回调机制:底层,特定的fd上有数据就绪,自动进行回调:“激活”红黑树中的节点,链入到就绪队列中。
而所有添加到epoll中的事件都会与设备(网卡)驱动程序建立回调关系(每一个rb_node都有与之对应的回调关系,struct epoll_entry),当响应的事件发生时会调用这个回调方法(这个回调方法在内核中叫ep_poll_callback,它会将发生的事件添加到rdlist双链表中)
- 就绪队列,epoll本质:一个基于事件就绪的生产者消费者模型(epoll接口是线程安全的)
- 获取就绪事件,如果缓冲区大小不够了怎么办?--- 不影响,没拿完,默认给你保留着,下次拿 -> 细节:内核会严格按照0下标开始,依次拷贝保存就绪事件和fd -> 应用层处理就绪事件的时候,处理的全部是就绪的,不用轮询判断是否合法了
- epoll_ctl:1)在红黑树中插入节点 2)向底层回调注册回调方法
epoll内核结构
1)为什么epoll是和文件关联的?
stuct file中的void *private_data指针指向struct event_poll对象(类比TCP中,private_data指向struct socket)
2)rb_tree和就绪队列用的是同一个结构struct epitem
epitem里面的struct epoll_filefd,事件对应的文件描述符
3)回调机制原理
事件激活不需要遍历,每个红黑树节点都在驱动程序注册了回调机制,有对应红黑树节点的地址。
sys_epoll_ctl系统调用执行插入时,回调机制具体做什么 -> 将红黑树节点链入就绪队列
以读事件为例,当有数据到来,读事件就绪,回调机制的流程是什么?
暂留
echoserver --- epoll版本
https://gitee.com/its-quite-six/linux-remote/tree/master/25_9_30/1.EpollServer
Epoll的优点和缺点
epoll的优点:
- 接口使用方便
- 数据拷贝轻量:只在合适时调用EPOLL_CTL_ADD将文件描述符拷贝到内核中,这个操作不频繁(select/poll都是每次循环都要进行拷贝)
- 事件回调机制:避免使用遍历,而是精准使用回调函数,将就绪的文件描述符结构加入到就绪队列中(回调将红黑树节点链入到就绪队列中,时间复杂度O(1),不需要遍历,暂定)。epoll_wait返回直接访问就绪队列就知道哪些文件描述符就绪,时间复杂度O(1)(不会因为文件描述符增多,效率收到影响)
- 文件描述符数量无上限
注意:struct epoll_event*是我们在用户空间分配好的内存,势必还是需要将内核的数据拷贝到用户空间的内存中的。
LT和ET理解
水平触发:有事件就绪,但没有处理(没有来得及处理),EPOLL模型会一直通知我们!
边缘触发:以读为例,从无到有,从有到多,变化了才通知,如果没有处理完,也不通知了
举例:张三和李四是学校附近站点的快递员。他们平时的工作就是骑着三轮车去学生宿舍派发快递。
张三是个老好人,他去派发快递的时候,如果三轮车的快递没有派发完,他就会一直等一直打电话给学生叫他下来拿。如果派发完了,恰好李四这时让他帮忙派发,李四快递放到他车上了,他会继续重复等和打电话,让学生下来拿直到拿完。
李四的性格恰好相反,他去派发快递的时候,打电话告诉学生,对学生统一说,我只给你打这一次电话,你不来拿我就走了,不拿完也是你的事,我不管。如果学生没有拿完,李四准备走了,这时张三让他帮忙派发,张三快递放他车上了,他会再打一次电话,说新快递来了,但我还是只打只一次。
张三:LT模式,只要底层有报文,一直通知上层
李四:ET模式,底层数据从无到有,从有到多(只通知一次)-> 因为从网络中拿到数据,导致底层数据变化时,才会通知上层 -> 即便上层没读完,也不通知 -> 倒逼上层,必须收到通知,把本轮数据取完。
epoll的epoll_wait每次进行读取,底层就绪队列会被拿走,但是在下一次epoll_wait时,红黑树对应节点的回调队列里就绪事件依旧会触发,还是能“激活”对应节点(LT)
两种工作模式模拟了示波器的叫法。
ET+非阻塞理由
ET通知就绪,用户必须把缓冲区本轮数据全部读完 -> 循环读取 -> recv的时候,万一读完,用户并不清楚底层缓冲区已经被读完了,recv还会被调用(细节点:recv可以通过返回值来判断是否还有数据,返回值小于缓冲区大小,那么就表示本次读完。但是如果刚好,buff大小是本轮数据大小的约数,用阻塞调用读取完下一次读取会被阻塞)-> ET模式,必须把fd设置为非阻塞的工作模式(ET+fd)
细节:
1)LT模式需要设置为非阻塞吗?可以,但也没有必要,recv一次一定不会被阻塞
2)不考虑返回值,即阻塞判断返回值的方法(有bug),无脑循环EAGAIN(非阻塞)
ET高效的原因
关于ET的工作特性,LT也能实现 -> 但LT没有强制性,约束不了程序员 -> ET设置了,必须一次读完,不然代码就有bug
LT vs ET,高效:
1)ET通知效率更高:有效通知数量多
2)ET尽快读完所有的数据,可以给对方更新一个更大的win窗口,提高对方滑动窗口大小,提高网络发送的报文并发量(核心)
细节:
- 接受缓冲区有低水位线这种设计,不是说只要有数据读事件能就绪。
- 请求方可以在TCP报头设置标志位PSH,让接收方fd就绪。
- ET模式倒逼程序员的本质:为给对方在概率上提供一个更大的应答win,提高TCP传输的效率
epoll使用场景
epoll的高性能, 是有一定的特定场景的. 如果场景选择的不适宜, epoll的性能可能适得其反.
对于多连接, 且多连接中只有一部分连接比较活跃时, 比较适合使用epoll.
例如, 典型的一个需要处理上万个客户端的服务器, 例如各种互联往APP的入口服务器, 这样的服务器就很适合epoll.
如果只是系统内部, 服务器和服务器之间进行通信, 只有少数的几个连接, 这种情况下用epoll就并不合适. 具体要根据需求和场景特点来决定使用哪种IO模型.