epoll惊群效应介绍
epoll通过一颗红黑树来组织所有被epoll_ctl加入到epoll监听列表中的fd,每一个监听的fd在epoll中用epoll item来标识。
每一个文件描述符都有一个等待队列,队列存放epoll entry,epoll entry中设置某个epoll对该文件描述符关心的事件,如果文件描述符上的事件ready了,可以通过epoll注册的回调函数通知到epoll。
epoll需要有一个等待队列给task在epoll_wait时来睡眠等待。epoll还需要有一个ready_list来组织已经ready的就绪fd,以便能够高效通知给进程task,而不需要再去遍历所有的fd。当事件发生后,唤醒文件描述符睡眠队列的epoll entry,调用其回调。
epoll的惊群现象就发生在ep_send_events<向用户态上报就绪事件过程>中:
在epoll LT模式下,如果事件来了,只要还没有处理,epoll都会通知你。epoll_wait在取到事件的时候并不会马上调用accept,此时还在子函数ep_poll中,因为此时事件并没有被处理掉,系统会继续唤醒别的进程来准备处理这个事件,存在多个进程被唤醒处理一个事件,这就是epoll中的惊群效应。
“惊群效应”的保证条件有两个:
-
在执行ep_send_events_proc<通知用户>的过程中,如果判断当前是LT模式,则将读取到的epi重新放回到epoll的ready list中。
-
只要epoll的ready list不为空,就唤醒睡眠在epoll的task队列。
假设有多个进程共用一个epoll,此时来了一个client连接
-
进程a的epoll_wait首先被ep_poll_callback唤醒,执行到2时,如果事件还没有被处理,则唤醒进程b;
-
进程b在执行ep_scan_ready_list的时候,事件还没有被处理,则唤醒进程c;
-
这个过程一直循环到之前唤醒的某个进程将事件取出,然后c进程从ep_item_poll中取不到事件,也就不会再将epi加回到“就绪链表”了,该事件的LT触发结束了。
解决方法
解决该问题的方法比较简单,使不同进程的epoll_wait互斥调用即可,具体如下:
-
使用边缘触发EPOLLET模式:边缘触发模式下,只有当文件描述符状态发生变化时才会被唤醒,可以有效避免惊群效应。
-
使用epoll的EPOLLONESHOT事件:EPOLLONESHOT事件可以保证每个文件描述符只被唤醒一次,需要重新注册到epoll中才能再次被触发。
-
使用EPOLLEXCLUSIVE:使用EPOLLEXCLUSIVE可以保证只有一个进程或线程被唤醒并处理就绪事件,其他等待此事件就绪的进程或线程不会被唤醒,从而避免了惊群效应。(Note:EPOLLEXCLUSIVE只能在ET模式下使用,且对性能具有一定损耗,谨慎使用)