9.6 epoll事件驱动模块

本章9.1节~9.5节都在探讨Nginx是如何设计事件驱动框架、如何管理不同的事件驱动模块的,但本节中将以epoll为例,讨论Linux操作系统内核是如何实现epoll事件驱动机制的,在简单了解它的用法后,会进一步说明ngx_epoll_module模块是如何基于epoll实现Nginx的事件驱动的。这样读者就会对Nginx完整的事件驱动设计方法有全面的了解,同时可以弄清楚Nginx在几十万并发连接下是如何做到高效利用服务器资源的。

9.6.1 epoll的原理和用法

设想一个场景:有100万用户同时与一个进程保持着TCP连接,而每一时刻只有几十个或几百个TCP连接是活跃的(接收到TCP包),也就是说,在每一时刻,进程只需要处理这100万连接中的一小部分连接。那么,如何才能高效地处理这种场景呢?进程是否在每次询问操作系统收集有事件发生的TCP连接时,把这100万个连接告诉操作系统,然后由操作系统找出其中有事件发生的几百个连接呢?实际上,在Linux内核2.4版本以前,那时的select或者poll事件驱动方式就是这样做的。

这里有个非常明显的问题,即在某一时刻,进程收集有事件的连接时,其实这100万连接中的大部分都是没有事件发生的。因此,如果每次收集事件时,都把这100万连接的套接字传给操作系统(这首先就是用户态内存到内核态内存的大量复制),而由操作系统内核寻找这些连接上有没有未处理的事件,将会是巨大的资源浪费,然而select和poll就是这样做的,因此它们最多只能处理几千个并发连接。而epoll不这样做,它在Linux内核中申请了一个简易的文件系统,把原先的一个select或者poll调用分成了3个部分:调用epoll_create建立1个epoll对象(在epoll文件系统中给这个句柄分配资源)、调用epoll_ctl向epoll对象中添加这100万个连接的套接字、调用epoll_wait收集发生事件的连接。这样,只需要在进程启动时建立1个epoll对象,并在需要的时候向它添加或删除连接就可以了,因此,在实际收集事件时,epoll_wait的效率就会非常高,因为调用epoll_wait时并没有向它传递这100万个连接,内核也不需要去遍历全部的连接。

那么,Linux内核将如何实现以上的想法呢?下面以Linux内核2.6.35版本为例,简单说明一下epoll是如何高效处理事件的。图9-5展示了epoll的内部主要数据结构是如何安排的。

9.6 epoll事件驱动模块 - 图1

图 9-5 epoll原理示意图

当某一个进程调用epoll_create方法时,Linux内核会创建一个eventpoll结构体,这个结构体中有两个成员与epoll的使用方式密切相关,如下所示。


struct eventpoll{

……

/红黑树的根节点,这棵树中存储着所有添加到epoll中的事件,也就是这个epoll监控的事件/

struct rb_root rbr;

//双向链表rdllist保存着将要通过epoll_wait返回给用户的、满足条件的事件

struct list_head rdllist;

……

};


每一个epoll对象都有一个独立的eventpoll结构体,这个结构体会在内核空间中创造独立的内存,用于存储使用epoll_ctl方法向epoll对象中添加进来的事件。这些事件都会挂到rbr红黑树中,这样,重复添加的事件就可以通过红黑树而高效地识别出来(epoll_ctl方法会很快)。Linux内核中的这棵红黑树与第7章中介绍的Nginx红黑树是非常相似的,可以参照ngx_rbtree_t容器进行理解。

所有添加到epoll中的事件都会与设备(如网卡)驱动程序建立回调关系,也就是说,相应的事件发生时会调用这里的回调方法。这个回调方法在内核中叫做ep_poll_callback,它会把这样的事件放到上面的rdllist双向链表中。这个内核中的双向链表与ngx_queue_t容器几乎是完全相同的(Nginx代码与Linux内核代码很相似),我们可以参照着理解。在epoll中,对于每一个事件都会建立一个epitem结构体,如下所示。


struct epitem{

……

//红黑树节点,与第7章中的ngx_rbtree_node_t红黑树节点相似

struct rb_node rbn;

//双向链表节点,与第7章中的ngx_queue_t双向链表节点相似

struct list_head rdllink;

//事件句柄等信息

struct epoll_filefd ffd;

//指向其所属的eventpoll对象

struct eventpoll*ep;

//期待的事件类型

struct epoll_event event;

……

};


这里包含每一个事件对应着的信息。

当调用epoll_wait检查是否有发生事件的连接时,只是检查eventpoll对象中的rdllist双向链表是否有epitem元素而已,如果rdllist链表不为空,则把这里的事件复制到用户态内存中,同时将事件数量返回给用户。因此,epoll_wait的效率非常高。epoll_ctl在向epoll对象中添加、修改、删除事件时,从rbr红黑树中查找事件也非常快,也就是说,epoll是非常高效的,它可以轻易地处理百万级别的并发连接。