EPOLL(2014)实施情况

2020-10-13 19:52:34

在这一系列博客文章中,我将介绍Linux事件轮询(EPOLL)子系统的内部结构和实现细节。

请注意,我假设这些文章的读者熟悉EPOLAPI和用法。我将重点介绍EPOLL子系统的实际内核实现,而不是它的用法。如果您不熟悉EPOLL的用法或不太了解它的功能,我强烈建议您先阅读EPOLL的手册页,因为它将使理解实现变得容易得多。

在本系列文章中,我将解释基于Linux3.16.1内核源代码的EPOLL内部。

本文将引用来自Linux内核发行版的实际代码。这些代码是在GPLv2许可下发布的。但是,本文的其他部分是不允许再分发的,除非在分发之前征得了作者的同意。

EPOLL由Linux内核提供的几个系统调用组成,是执行I/O多路复用的有效方式。多亏了Linux的虚拟文件系统设计,任何可轮询的文件,或者更具体地说,实现Poll()文件操作的任何文件都可以与EPOLL一起使用。这包括套接字文件,这是当今的主要兴趣之一。

传统上,程序员使用选择或轮询作为执行I/O多路复用的方式。然而,SELECT和POLL都是很久以前设计的,当时网络服务只能处理数千个并发客户端。选择和投票的方式极其相似。事实上,它们都是在Linux内核存储库(fs/select.c)下的单个文件中实现的。他们的工作方式也很简单。应用程序生成它感兴趣的FD数组。然后,应用程序对内核进行系统调用。然后,内核将从用户空间复制FD数组,然后逐一检查其中的每一个,以查看文件的poll()操作是否有可用的事件。之后,SELECT将简单地生成一个位数组并将其复制回用户空间,在用户空间中,Poll将直接操作用户空间中的pollfd结构,而不会复制回用户空间。

正如我们所看到的,SELECT和POLL的实现意味着它们不能很好地扩展到大量的文件描述符,因为它们的时间复杂度都是O(N)。这并不是问题,直到过去十年使用互联网的人数激增。现在,服务器同时处理100000个tcp连接是很常见的。尽管可以使用SELECT或POLL处理那么多连接,但很可能会发生的是,轮询这些文件描述符将浪费宝贵的CPU时间。这将对服务器的可伸缩性和响应性产生重大影响。为了解决这个问题,我们需要一种能够以更智能的方式通知我们有关文件描述符事件的解决方案,而这正是EPOLL出现的目的。

在我们决定深入研究Linux内核源代码之前,让我们大体了解一下EPOLL是如何工作的。

EPOLL与传统I/O多路复用机制之间最大的区别在于,应用程序不需要每次都构建一个巨大的文件描述符数组并将其传递到内核中,而只需获取一个EPOLL实例并将文件描述符注册到该实例上即可。而且,EPOLL实例不是轮询整个文件描述符数组,而是监视已注册的文件描述符,并在被请求时向应用程序报告事件。当{已发生事件的文件描述符}/{监视的文件描述符总数}的比率相对较小时,此过程非常有效。由于EPOLL子系统的时间复杂度是恒定的,因此使用它可以很容易地处理数十万个开放的TCP连接。

EPOLL实例是EPOLL子系统的核心。在Linux下,我们通过调用EPOLL_CREATE(2)或EPOLL_CREATE1(2)来请求EPOLL实例。这两个函数都返回文件描述符。使用文件描述符作为对EPOLL实例的引用的原因是,这使得EPOLL实例也是可轮询的。这支持高级用法,例如使用EPOLL、SELECT甚至轮询来监视EPOLL实例。但是EPOLL实例的实际重要部分是在fs/eventpoll.c行180中定义的内核数据结构struct eventPoll。此数据结构维护EPOLL实例正常工作所需的几乎所有内容。可以在fs/eventpoll.c的第1767行找到分配struct eventpoll并返回文件描述符的代码,下面是它的片段:

Ep_alloc()只是分配足够的空间来保存内核堆中的struct eventpoll,并对其进行初始化。

/**创建设置事件轮询文件所需的所有项目。即,*文件结构和空闲文件描述符。*/fd=GET_UNUSED_FD_FLAGS(O_RDWR|(FLAGS&;O_CLOEXEC));

如果EPOLL_CREATE()能够获取文件描述符,它将尝试从系统获取匿名inode。注意,EPOLL_CREATE()将指向先前分配的struct eventpoll的指针保存到文件的PRIVATE_DATA字段中。因为操作EPOLL实例的任何系统调用都使用实例的文件描述符号引用它,所以这使得稍后重新获取struct eventpol变得非常容易和高效:

之后,EPOLL_CREATE将把匿名inode与文件描述符绑定,并将文件描述符返回给调用进程:

由于显而易见的原因,EPOLL必须以某种方式记住它被要求注意的文件描述符。EPOLL使用一种非常常用的内核数据结构-红黑树(RB-Tree),来跟踪某个EPOLL实例当前监视的所有文件描述符。RB-Tree的根是struct eventPoll的RBR成员,并在ep_alloc()函数中初始化。

对于EPOLL实例监视的每个文件描述符,RB-Tree中都会有一个相应的struct epitem引用它。Struct epitem还包含一些重要的数据结构,EPOLL实例将在此文件描述符的整个监视生命周期中使用这些数据结构。

当我们使用epoll_ctl(2)将文件描述符添加到EPOLL实例中时,内核首先使用ep_find()尝试定位与文件描述符对应的struct epitem,代码可以在fs/eventpoll.c行973找到。

由于RB-Tree是一个二叉搜索树,这意味着在我们可以将它们存储到RB-Tree中之前,我们必须为每个struct epitem都有一个可比较的键。对于EPOLL,RB-Tree条目的键是一个名为struct EPOLL_filefd的结构,该结构存储在struct epitem中。Struct EPOLL_filefd的定义非常简单,在fs/eventpoll.c第106行(添加了注释):

Struct epoll_filefd{struct file*file;//指向fd int fd对应的目标file结构的指针;//目标文件描述符号}__packed;

/*比较RB树键*/static inline int EP_CMP_FFD(struct EPOLL_filefd*p1,struct EPOLL_filefd*p2){return(p1->;file>;p2->;file?+1:(p1->;file<;p2->;file?-1:p1->;fd-p2->;fd));}。

Ep_cmp_ffd()首先比较结构文件的内存地址,地址较高的将被视为";较大";。

如果内存地址相同(这是可能的,因为多个文件描述符可能引用同一结构文件(例如,通过dup(),EP_cmp_ffd()将简单地将文件描述符号较高的文件视为";较大";。通过这样做,可以保证对于任何两个不等价的文件描述符,EP_cmp_ffd()都可以比较它们。对于两个相同的文件描述符,EP_CMP_FFD()将返回0。

一旦定义了比较函数,从RB-Tree中搜索文件描述符与在任何二叉搜索树中搜索节点没有什么不同。

假设我们试图将文件描述符添加到EPOLL实例中,一旦EP_find()返回,EPOLL_ctl()将确保EP_find()没有找到任何东西,否则它将返回并将errno设置为EEXIST。

之后,epoll_ctl()将调用ep_insert()将新的文件描述符添加到RB-Tree中,并设置一些接收事件通知所需的数据结构和回调。关于ep_insert()的更多细节将在下一篇文章中讨论。