epoll一种高效的多路复用技术

191 阅读6分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第11天,点击查看活动详情 >> 本文根据罗培羽的知乎系列文章内容进行整理:

  1. zhuanlan.zhihu.com/p/63179839
  2. zhuanlan.zhihu.com/p/64138532
  3. zhuanlan.zhihu.com/p/64746509

epoll作为linux下高性能网络服务器的必备技术至关重要,nginx、redis、skynet和大部分游戏服务器都使用到这一多路复用技术

1.硬件-网卡接收数据

网卡会把接收到的数据写入内存。通过硬件传输,网卡接收的数据存放到内存中。操作系统就可以去读取它们。

2.cpu角度-数据接收

当网卡把数据写入到内存后,网卡向cpu发出一个中断信号,操作系统便能得知有新数据到来,再通过网卡中断程序去处理数据。

3. 操作系统进程调度角度-进程阻塞为什么不占用CPU资源

阻塞是进程调度的关键一环,指的是进程在等待某事件(如接收到网络数据)发生之前的等待状态,recv、select和epoll都是阻塞方法。

  • 工作队列

当程序运行行到recv阻塞函数时,会一直等待,直到接收到数据才会向下执行。

  • 等待队列

此时该进程A就会从工作队列移到该socket的等待队列中。不影响工作队列的进程调度,所以进程A被阻塞,不会往下执行代码,也不会占用CPU资源。

  • 唤醒进程

当socket接收到数据后,操作系统将该socket等待队列上的进程重新放回工作队列,进程继续执行

4. 内核接收网络数据全过程

image-20220617175228532

其一,操作系统如何知道网络数据对应于哪个socket?

因为一个socket对应着一个端口号,而网络数据包中包含了ip和端口的信息,内核可以通过端口号找到对应的socket。当然,为了提高处理速度,操作系统会维护端口号到socket的索引结构,以快速读取。

其二,如何同时监视多个socket的数据?

多路复用的重中之重

5. 同时监视多个socket的简单方法

epoll的要义是高效的监视多个socket

预先传入一个socket列表,如果列表中的socket都没有数据,挂起进程,直到有一个socket收到数据,唤醒进程。这种方法很直接,也是select的设计思想。

select方法会对每一个socket都记录进程A,然后任意一个socket结束,将进程A从所有socket里面删除,加入到工作队列中。当进程A被唤醒之后,遍历一遍socket列表得到就绪socket。

  • 两次遍历所有的socket,导致开销大,规定select最大的监视数量1024
  • 进程被唤醒还要再遍历一次

6. epoll的设计思路

epoll在select和poll的基础进行了改进和增强。主要表现在

  1. 将维护等待队列和阻塞进程两个步骤分开。用epoll_ctl维护等待队列,再调用epoll_wait阻塞进程
  2. 同时内核维护了一个就绪列表,获知哪些socket收到数据。当进程被唤醒后,只要获取rdlist的内容,就能够知道哪些socket收到数据。

7. epoll的原理和流程

创建epoll对象

当某个进程调用epoll_create方法时,内核会创建一个eventpoll对象

维护监视列表

用epoll_ctl添加或删除所要监听的socket,终端程序操作eventpoll对象,而不是直接操作进程

接收数据

中断程序给eventpoll的就绪列表添加socket引用

阻塞和唤醒进程

8. epoll的实现细节

nignx 如何快速从kernel中找到等待处理的事件?Epoll

  • 链表:仅仅存储活跃链接
  • 红黑树:添加事件

适合做大并发连接的事件,随着句柄树的增加,性能变化不大

image.png 就绪列表的数据结构

就绪列表引用着就绪的socket,所以它应能够快速的插入数据。

程序可能随时调用epoll_ctl添加监视socket,也可能随时删除。当删除时,若该socket已经存放在就绪列表中,它也应该被移除。

所以就绪列表应是一种能够快速插入和删除的数据结构。双向链表就是这样一种数据结构,epoll使用双向链表来实现就绪队列(对应上图的rdllist)。

索引结构

既然epoll将“维护监视队列”和“进程阻塞”分离,也意味着需要有个数据结构来保存监视的socket。至少要方便的添加和移除,还要便于搜索,以避免重复添加。红黑树是一种自平衡二叉查找树,搜索、插入和删除时间复杂度都是O(log(N)),效率较好。epoll使用了红黑树作为索引结构(对应上图的rbr)。

9.结论

epoll在select和poll(poll和select基本一样,有少量改进)的基础引入了eventpoll作为中间层,使用了先进的数据结构,是一种高效的多路复用技术。

img

10.评论

首先epoll初始化用epoll_create创建一个event_poll对象,这个对象有就绪列表,红黑树,等待列表。就绪列表存放就绪的socket,红黑树存放所有正在监听的socket引用,等待列表放正在等待的进程。

每次accept到一个新连接,调用中断在文件系统中创建fd,这个fd里有接收缓存区,发送缓存区,等待列表,(ps:如果已有这个fd,则直接将这个socket加入这个fd的等待列表中,这里我理解为fd对应的端口,计算机有65535个端口,所以epoll最多支持65535,而一个端口可能会有许多的连接,因为socket是IP:端口,IP不同,端口一样也是不同的socket,但是对应的端口fd是一个),同时在中断系统里注册一个监听回调函数(这个回调函数到底在哪里,没搞明白)。一旦某个socket发生了读写操作,中断程序会调用这个socket的回调函数,将这个socket的引用加入到event_poll的就绪队列中,在while程序里,一直都会有epoll_wait(A进程),一旦A进程会一直轮询就绪队列,一旦就绪队列非空,A进程获得其中的socket数据进入系统运行队列,由等待列表的下一个进程继续使用epoll_wait来轮询。

等待列表中的进程,我的理解是系统每次有空闲进程,就将其放入等待列表中阻塞,epoll_wait有一个time_out参数,在这个等待时间time里,如果就绪队列有socket需要处理,就调用阻塞的进程运行,如果一直为空,当计时器到了,进程变为非阻塞继续去干活。

面试题整理

面试题:select/poll/epoll的相关面试题

[select、poll、epoll之间的区别(搜狗面试)]