Epoll 要点总结 | Epoll LT ET 区别 | Nginx epoll 原理 listend 用 LT

Epoll 要点总结 | Epoll LT ET 区别 | Nginx epoll 原理 listend 用 LT,第1张

Epoll 要点总结 | Epoll LT ET 区别 | Nginx epoll 原理 listend 用 LT

最近学 muduo 和 nginx 写网络库,总结一下 epoll 上遇到的一些问题和学习的笔记,主要是对 LT、ET 和平滑升级里的一些点理解一下。可以看作是上一次根据2.6 源码写的意识流(大白话)垃圾笔记(EPOLL 原理分析线索 SELECT POLL 原理_我说我谁呢 --CSDN博客)的翻新。

socket 驱动和系统调用

数据缓冲区,每当 listen 和 bind 了之后,内核就要接收对应 socket 地址(src_ip:src_port::dst_ip:dst_port四元组)的数据包,意味着不能直接发送 RST 或者静默丢弃(一般 UNIX 对没有 bind 没有 listen 的地址,或者对端提前 close 都可能发送 RST)。从而存在一个缓冲区,如果是 TCP,这个会是一个带流量控制和拥塞控制的滑动窗口,如果是 UDP,这个会是一个块缓冲区。一般 TCP listen 的来说,不用分配空间,可以等收到了甚至等到握手结束后再根据 meta 建立窗口缓冲区。listen 的缓冲区,内核会存在一个栈表存储入站请求,等待用户应用程序调用 accept 取走请求。数据的复制,每当一个应当接受的数据包到达,内核的驱动中断程序(或者 NAPI 的 polling 程序)会进行 ring buffer 的复制,可能有 DMA,这里有两次复制,一次从硬件到 ring,一次解包从 ring 到滑动窗口/块缓冲区(这里会触发红黑树/哈希表 知识点,快速查找 ip:port)。每当应用层调用 accept、read 、recv 等慢系统调用时,

如果缓冲区没有可用的数据(非就绪态),就要睡觉。睡觉过程中如果程序收到信号,会直接返回,设置 errno 为  EINTR。如果缓冲区有可用的数据,将会把缓冲区的数据复制到如果启用了 NonBLOCK 方式而且没有可用数据,就会返回 -1,errno 为 Resource temporarily unavailable (EAGAIN、EWOULDBLOCK)poll 接口,linux 的 VFS 通过 file 结构体中的一个 file_operator 结构体成员实现自定义各种 *** 作的函数指针。这个东西类似虚函数表,如果驱动(网卡供应商的硬件驱动、kernel 的 TCP 协议栈都是这里“驱动”的范围,或者说是 VFS 的底层更准确)不支持,就是 NULL 了。其中的 op 包括 fseek 的指针、aio(淘汰了)、ioctl、mmap、fsync、readv、writev 等。这些都是特定 file 下层驱动需要实现的,比如对于磁盘文件和 socket 文件,实现当然是不一样的。当然,最重要的(针对本文来说)是 poll 接口,这个是用来查询该 file 是否有数据的,实际对于 一个 socket file 来说,这个东西是 TCP 协议栈实现的,内核应当存储了一个巨大的表(红黑树或者 hashmap)可以直接根据 ip:port 查询,然后设置对应的 metadata。硬件中断后的回调,同样是 file 结构体,里面有一个 private_data 的内存块指针,这里不同的驱动会不同的方式访问。对于可能阻塞的文件,需要解决 wait queue 的问题,Linux 通过这里实现 wake up 的功能,总之,数据包到达之后,kernel 拆包找到特定的 file 之后,通过 private_data 里面的内容进行回调,epoll、select 这些函数绑定的时候,会在 private_data 里面注册一个回调函数,驱动将会触发他们。对于 select 和 poll 来说,做的事情很简单就是唤醒程序,之后在 kernel side 醒来,做一些标记之类的事情,就能返回到用户了。对于 epoll 来说,这个回调函数会把当前 fd 的某个结构体连上一个 double linked list 里面,之后就能直接返回成功的给。epoll 的数据结构优化,epoll 的红黑树是用来查询用户关心的 fd (epitem)用的(epoll_ctl 的 *** 作都走红黑树)。对于要返回给用户的链表(rdlist),他的钩子是在 file 结构体里面的,上面说的双链表就是这个东西:

#ifdef CONFIG_EPOLL

    

    struct list_head    f_ep_links;

    spinlock_t        f_ep_lock;

#endif 

没有 mmap,网上有些博客有毒,说 epoll 用了 mmap。其实一直没有用。实际直到 5.6 的 io_uring 才引入了 mmap 的优化。

旧 api - select 和 poll

select 是有动静触发返回,然后用户要遍历检查整个 set (一种 bitmap 数据结构)。线性复杂度。简单的实现是,循环pselect 增加了避免信号缺失的处理(记住一般 handler 不能有 states,所以一般用全局变量做 flag 之后再在 eventloop 里面检查这个 flag),可以睡前检查信号。poll 改进数据结构和超时精度。由于 set 的数据结构不好用,以及之前的 ms 超时精度太小,poll 提供了支持 ns 超时的链表结构体,方便,但是实际还是线性复杂度。2.6 源码

static int do_poll(unsigned int nfds,  struct poll_list *list,

            struct poll_wqueues *wait, long timeout)

{
    int count = 0;
    poll_table* pt = &wait->pt;
    if (!timeout)
        pt = NULL;
    for (;;) {
        struct poll_list *walk;
        walk = list;
        while(walk != NULL) {
            do_pollfd( walk->len, walk->entries, &pt, &count);
            walk = walk->next;
        }
        // count代表有没有就绪事件,timeout 说明没有设置超时或者已经超时,signal_pending代表有信号需要处理
        if (count || !timeout || signal_pending(current))
            break;
        // 挂起进程,timeout后被唤醒
        timeout = schedule_timeout(timeout);
    }
    return count;
}

需要注意实际 poll 和 select 会有两次 O(n) *** 作,一次是 kernel side 的轮询,一次是用户态需要看结构体里面的 revent (a bunch of AND operations),这个对大量空闲连接的来说,是花时间的的,所以实际还可能被 yield 出去。

ET 和 LT 的区别

针对事件的设置,注意 ET 和 LT 是可以针对每个 fd 设置的。linux 2.6 代码(裁剪了一部分内容,直接去 github 看完整源代码)

	static int ep_send_events(struct eventpoll *ep,
				  struct epoll_event __user *events, int maxevents)
	{
	
		
		list_for_each_entry_safe(epi, tmp, &txlist, rdllink) {
			struct wakeup_source *ws;
			__poll_t revents;
	
			if (res >= maxevents)
				break;
	
			list_del_init(&epi->rdllink);
	
			
			revents = ep_item_poll(epi, &pt, 1);
			if (!revents)
				continue;
	
			events = epoll_put_uevent(revents, epi->event.data, events);
			if (!events) {
				list_add(&epi->rdllink, &txlist);
				ep_pm_stay_awake(epi);
				if (!res)
					res = -EFAULT;
				break;
			}
			res++;
			if (epi->event.events & EPOLLONESHOT)
				epi->event.events &= EP_PRIVATE_BITS;
			else if (!(epi->event.events & EPOLLET)) {
				
				list_add_tail(&epi->rdllink, &ep->rdllist);
				ep_pm_stay_awake(epi);
			}
		}
		ep_done_scan(ep, &txlist);
		mutex_unlock(&ep->mtx);
	
		return res;
	}
来自  

对于 LT 来说,就是他要返回用户态,他还是保留在 kernel side 的链表中的,从而下一次还会再检查一次,如果还是没有 ready 的话,下一次就会把他删除掉了。

一些其他知识

epoll_wait 惊群,如果采用 oneshot 就能保证只唤醒一个。epoll 再 fork 之前的话,fork 会复制同一个 epoll fd,所以理论上是安全的。如果是多个 epoll fd 来的话,还是可能惊群,解决方案是用锁,nginx 旧用了锁。linux 无法覆盖监听端口,重复绑定/listen 会引发 bind error。SO_RESUEPORT 是内核 3.9 引入的内核空间里进行负载均衡的方案。

nginx 的 master-worker 模型

master 平时都处于睡觉状态。所有修改状态机的行为都通过信号机制和全局 flag 来实现。

Nginx 的 epoll 选择

listenfd 用 LTconnectfd 用 ET

为什么 ET 可能会丢失连接(程序员的锅)

循环,如果应用程序没有正确的处理 epoll wait 醒来后的处理(accept、read、write 都要套 while),就会丢失连接。立即返回,理论上 epoll_wait 睡觉之前会检查一下有没有能立即返回的。但是 et 可能本身是高电平的话,不会返回(5.6 后某个 patch 之后可能更改了逻辑:The edge-triggered misunderstanding [LWN.net])

edge 并不是指 "有人写入了更多的数据"。edge 的意思是 "以前没有数据,现在有数据了"。

而一个 level triggered 事件 也不是 指 "有人写了更多的数据"。它只是表示 "这里有数据"。

请注意,edge 和 level 都没有提到 "更多的数据" 这个信息。其中一个是指从 "没有数据"->"有数据 "的这个变化,而另一个只是表示 "有数据"。

---- Linus Torvalds

要理解这个,就要理解 socket 的回调挂上double linked 列表是怎么做的,我们之前的笔记里说了,实际是 file 的 private data 里面被驱动 reinterpret_cast 为 struct socket,然后会有一个管理 wait queue 的结构体,回调函数里面做的事情很重要。下面这个函数就是回调函数

         ep_ptable_queue_proc(Linux 2.9)

        来自

仔细查看源码就能理解,回调挂上 double linked list 的时候并没有什么电平检查,就直接连上链表了(当然,本身tcp栈是已经检查过了是不是这个进程绑定的了)。所以实际上,后续如果有了新的数据,还是能保证挂上来的。丢失的问题主要在于一开始的情况,以及后续没有新数据来的情况。上面 LWN 讲 Android bug 的时候也是主要是 pipe 一开始没处理好的问题。

平滑升级和回滚的方案

首先思想上要允许中间状态(新 worker 未启动完成的时候)一部分连接进入旧的,直到我们关掉旧 worker 的 listen d。对于同一个 nginx 版本的,只是更新服务器配置(nginx 只是一个 web 服务器+负载均衡,本身不是网络库),我们开新的 worker,然后关掉旧 worker 的 listen fd (epoll_ctl) 就行了。更新 nginx 版本的要麻烦一点。思想上允许中间状态存在。所以此时可以并行存在新 worker 和旧 worker 保证不间断服务,直到新的设置好了之后,再关掉旧的就好了,就是这么简单,要实现这个,必须处理好各种全局结构的访问,比如 nginx 的 pid 文件。为了安全,实际运行的时候,并不会马上关闭旧的 master+workers,而是让运维手动控制,从而保证能够 revive,这时发送信号则主要是控制 worker 恢复 accept。

Nginx listenfd 为什么要用 LT

nginx 的 worker 都是 master fork 出来的。nginx 更新配置文件是用旧 master fork 升级的,更新二进制则是停掉 accept,然后用新启动 master。我的总结是,一个是为了方便统一编写平滑升级的模块,以及方便控制最大并发连接数。首先,理解一下 nginx 进行 configure file 更新的方法,是通过 HUP 信号来实现的,一种实现方法是,设置一个 flag,说 worker 不能再 accept 新的连接了,必须先更新一下配置文件,然后设置准备新的 worker 了,这样要求 listen fd 的内容要 backlog。say 负载均衡的应用,我们在某个 worker 里面,此时收到了更新配置文件的请求,worker 需要暂停 accept。对于 ET 来说,没有办法跳出 epoll_wait 的循环,我们必须把处理 reload 的代码放到 accept 的 loop 内部(目前是一层 epoll_wait 一层做 accept 的)。具体其实用 ET 也能实现,但是会复杂一点。而且 Nginx 本身也要兼容 select 和 poll 的(具体的说,在适配 IOCP 之前,windows 的版本一直用的是 select)。还有一个好处是,对于 listen fd 来说,使用 LT 可以控制同时并发数,比如,我们可以限制一次 accept N 个连接,等 worker 都处理完了,我们再 accept。用 ET 的话,当然也能实现,但是要更加复杂。

epoll 和 poll/select 的选择

没有什么事情的话其实当然是选 epoll 的。但是!!epoll 每次修改一个 fd 的关注都要进行一次 context switch !(epoll_ctl 是 system call,修改的数据结构是内核的红黑树,而且一次只能修改一个 fd)。while select/poll 只需要在 user space 修改好数据结构,然后 switch 过去复制一遍。

LT 和 ET 的一些问题(面试题)

对于 EPOLL_OUT 可写事件,LT 会不断的触发。解决方案当然就是直接移除他,等需要些了再监听。实际还是要维护一个表。或者说,处理完了(生成数据),需要写的时候就非阻塞写一下,如果 EWOULDBLOCK 了的话,就注册 epoll 进去睡觉,写完了又丢掉(epoll_ctl)就行了。这个会引发很多的 context switch。(腾讯、快手)

我又一篇经典有头无尾文章。有时候我在想,为什么有的人写的东西搜出来这么难懂,或者各种跳步就点点东西,他自己看的吗?他自己能看明白吗?还有那些抄来抄去的东西,又有什么意义。慢慢好像感觉没有资格这样想。因为自己试着学一遍某个知识点,最后又会发现,感觉好像会了,但是要写出来,又花时间又要重新表达一些东西,实际别人可能精品表达已经够好了,东拼西凑一个笔记是不是更方便复习和快速学习呢。做笔记费劲,用拙略的话再说一遍更加离谱,想起以前学东西都是直接忠于原书,如果忘记了细节,那就应该完整的回味他,如果需要概要,是不是一个目录、思维导图就够了呢?而不是去写一些这缺一块那缺一块的不完备的离奇总结(都不能成为综述的质量)。而且要写的完备的自己也能复习看懂也不是那么简单的更何况让别人看懂,但是又感觉自己需要复习使用。我之前写的笔记都是各种知识点的杂糅怪,标题概括不了内容,而且常常是标题本来研究的东西有头无尾一笔带过,而旁支末节又钻牛角尖。之后我感觉再做没有意义的笔记是没有意义的(==),润。

欢迎分享,转载请注明来源:内存溢出

原文地址: https://outofmemory.cn/zaji/5714835.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-12-18
下一篇 2022-12-18

发表评论

登录后才能评论

评论列表(0条)

保存