知乎问题:
Web服务器nginx使用ET模式的epoll。我想问,它相对LT模式epoll有哪些优势呢?另外一篇帖子(epoll的边沿触发模式(ET)真的比水平触发模式(LT)快吗?(当然LT模式也使用非阻塞IO,重点是要求ET模式下的代码不能造成饥饿))说ET不一定比LT快,那么为什么要使用ET模式呢?
1. 源码
这个问题,其实回到了 epoll 的 LT 与 ET 模式的区别。
看过 epoll 内核源码的朋友可能都会很惊讶,LT 和 ET 的区别核心就几行代码,主要看 Linux 内核的 eventpoll.c 文件这几行源码(github)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/* Linux 5.0.1 - fs/eventpoll.c */
static __poll_t ep_send_events_proc(struct eventpoll *ep,
struct list_head *head, void *priv) {
...
/* 遍历处理 txlist(原 ep->rdllist 数据)就绪队列结点,
* 获取事件拷贝到用户空间。*/
list_for_each_entry_safe(epi, tmp, head, rdllink) {
if (esed->res >= esed->maxevents)
break;
...
/* 先从就绪队列(头部)删除 epi。*/
list_del_init(&epi->rdllink);
/* 获取 epi 对应 fd 的就绪事件。 */
revents = ep_item_poll(epi, &pt, 1);
if (!revents)
/* 如果没有就绪事件,说明就绪事件已经处理完了,就返回。
* (这时候,epi 已经从就绪队列中删除了。) */
continue;
...
/* 主要看这一行哈~~~~ */
else if (!(epi->event.events & EPOLLET)) {
/* lt 模式,前面删除掉了的就绪事件节点,重新追加到就绪队列尾部。*/
list_add_tail(&epi->rdllink, &ep->rdllist);
...
}
}
...
}
2. nginx
其实为啥 epoll 会设计 LT 和 ET 两种工作模式呢?我大胆猜测一下它的设计意图:事件处理的紧急程度。
举个 🌰:你有急事打电话找人,如果对方一直不接,那你只有一直打,直到他接电话为止,这就是 lt 模式;如果不急,电话打过去对方不接,那就等有空再打,这就是 et 模式。
我们接下去分析一下 nginx,使用 strace 命令,可以轻松获得 nginx 的 epoll 相关系统调用。通过 strace 日志你会发现,除了 listen socket 默认是使用 LT 模式的,其它 accept 出来的 client fd,基本都是使用 ET 模式的。
从上面”设计意图“理解,显然 listen socket 要比 accept 出来的 socket 事件紧急度要高啊!因为你不把 listen socket 的链接 accept 出来,哪里来数据处理啊;其次 listen socket 的完全队列长度是有限制的,如果不快点将链接数据捞出来,队列可能就会溢出了,所以 listen socket 采用 LT 模式,当触发可读事件后,内核就一直通知应用层快点调用 accept 将链接取出来。而 accept 出来的 socket 相对来说,优先级就没那么高了,所以设置为 ET 模式,当然将它设置为 LT 模式好像也没什么问题,真的是这样么?
3. 优先权
在高并发系统里,LT 模式与 ET 模式其实有一个比较容易忽视的差别:新事件处理的”优先“程度。
ET 模式会使得新的其它 client fd 的就绪事件能快速被处理。(在高并发系统里,有海量事件,每个事件都希望自己快点被处理啊~~~)
可以看下图 epoll_wait 的工作时序,假如 epoll_wait 每次最大从内核取一个事件。
如果是 LT 模式,(就绪队列上的节点)epi 节点刚开始在内核被删除,然后数据从内核空间拷贝到用户空间后,内核马上将这个被删除的节点重新追加回就绪队列,这个速度很快,所以后面来的其它的 client fd 的就绪事件很大几率会排在已经处理过的事件后面。
而 ET 模式呢,数据从内核拷贝到用户空间后,内核不会重新将就绪事件节点添加回就绪队列,当事件在用户空间操作完后,用户空间根据需要重新将这个事件通过 epoll_ctl 添加回就绪队列–如果事件还没有完全处理完毕。(又或者这个节点因为有新的数据到来,重新触发了就绪事件而被添加回就绪队列)。从节点被删除到重新添加这个环节,这中间的过程是比较“漫长”的,所以新来的其它事件节点能排在旧的节点前面,能快速处理。
分析到这里,可能大概理解 nginx 使用 LT 和 ET 模式的场景了。
这个道理有点像排队打饭,一个队列上,有些同学要打包两份饭,如果每次只能打包一份,lt 模式就是,这些同学打包了一份之后,马上重新回去排队,再打一份。et 模式是,这些同学先打包一份,然后拿回去吃掉了,再回来排队,在高峰期显然整个排队的效率和结果不一样。