第9章 事件模块

在上文中提到,Nginx是一个事件驱动架构的Web服务器,本章将全面探讨Nginx的事件驱动机制是如何工作的。ngx_event_t事件和ngx_connection_t连接是处理TCP连接的基础数据结构,在对它们有了基本了解后,在9.4节将首先探讨核心模块ngx_events_module,它定义了一种新的模块类型——事件模块,而在9.5节将开始说明第1个事件模块ngx_event_core_module,它的职责更多地体现在如何管理当前正在使用的事件驱动模式。例如,在Nginx启动时决定到底是基于select还是epoll来监控网络事件。

epoll是目前Linux操作系统上最强大的事件管理机制,本书描述的场景都是使用epoll来驱动事件的处理,在9.6节中,首先会深入到Linux内核,研究epoll的实现原理和使用方法,以此明确epoll的高并发是怎么来的,以及怎样使用epoll才能发挥它的最大性能。接着,就到了ngx_epoll_module模块“亮相”的时候了,这里可以看到一个实际的事件驱动模块是如何实现声明过的事件抽象接口的(见9.1节),同时这个模块也是高效使用epoll的较好的例子。例如,在使用epoll时,非常容易遇到过期事件的处理问题,Nginx就使用了一个巧妙的、成本低廉的方法完美地解决了这个问题,稍后读者将会看到这个小技巧。

Nginx的定时器事件是由第7章中谈到的红黑树实现的,它也由epoll等事件模块触发,在9.7节中,读者将看到Nginx如何实现独立的定时器功能。

在9.8节中,我们开始综合性地介绍事件处理框架,这里将会使用到9.1节~9.7节中的所有知识。这一节将说明核心的ngx_process_events_and_timers方法处理网络事件、定时器事件、post事件的完整流程,同时读者会看到Nginx是如何解决多个worker子进程监听同一端口引起的“惊群”现象的,以及如何均衡多个worker子进程上处理的连接数。

毫无疑问,Linux内核提供的文件异步I/O是不同于glibc库实现的多线程伪异步I/O的,它充分利用了在Linux内核中CPU与I/O设备独立工作的特性,使得进程在提交文件异步I/O操作后可以占用CPU做其他工作。在9.9节中,将会讨论这种高效读取磁盘的机制,在简单说明它的使用方式后,读者还可以看到文件异步I/O是如何集成到ngx_epoll_module模块中与epoll一起工作的。

9.1 事件处理框架概述

事件处理框架所要解决的问题是如何收集、管理、分发事件。这里所说的事件,主要以网络事件和定时器事件为主,而网络事件中又以TCP网络事件为主(Nginx毕竟是个Web服务器),本章所述的事件处理框架都将围绕这两种事件进行。

定时器事件将在9.7节中阐述,因为它的实现简单而且独立,同时它基于网络事件的触发实现,并不涉及操作系统内核。这里先来了解一下Nginx是如何收集、管理TCP网络事件的。由于网络事件与网卡中断处理程序、内核提供的系统调用密切相关,所以网络事件的驱动既取决于不同的操作系统平台,在同一个操作系统中也受制于不同的操作系统内核版本。这样的话,Nginx支持多少种操作系统(包括支持哪些版本),就必须提供多少个事件驱动机制,因为基本上每个操作系统提供的事件驱动机制(通常事件驱动机制还有个名字,叫做I/O多路复用)都是不同的。例如,Linux内核2.6之前的版本或者大部分类UNIX操作系统都可以使用poll(ngx_poll_module模块实现)或者select(ngx_select_module模块实现),而Linux内核2.6之后的版本可以使用epoll(ngx_epoll_module模块实现),FreeBSD上可以使用kqueue(ngx_kqueue_module模块实现),Solaris 10上可以使用eventport(ngx_eventport_module模块实现)等。

如此一来,事件处理框架需要在不同的操作系统内核中选择一种事件驱动机制支持网络事件的处理(Nginx的高可移植性亦来源于此)。Nginx是如何做到这一点的呢?

首先,它定义了一个核心模块ngx_events_module,这样在Nginx启动时会调用ngx_init_cycle方法解析配置项,一旦在nginx.conf配置文件中找到ngx_events_module感兴趣的"events{}"配置项,ngx_events_module模块就开始工作了。在图9-3中,ngx_events_module模块定义了事件类型的模块,它的全部工作就是为所有的事件模块解析"events{}"中的配置项,同时管理这些事件模块存储配置项的结构体。

其次,Nginx定义了一个非常重要的事件模块ngx_event_core_module,这个模块会决定使用哪种事件驱动机制,以及如何管理事件。在9.5节中,将会详细讨论ngx_event_core_module模块在启动过程中的工作,而在9.8节中,则会在事件框架的正常运行中再次看到ngx_event_core_module模块的“身影”。

最后,Nginx定义了一系列(目前为9个)运行在不同操作系统、不同内核版本上的事件驱动模块,包括:ngx_epoll_module、ngx_kqueue_module、ngx_poll_module、ngx_select_module、ngx_devpoll_module、ngx_eventport_module、ngx_aio_module、ngx_rtsig_module和基于Windows的ngx_select_module模块。在ngx_event_core_module模块的初始化过程中,将会从以上9个模块中选取1个作为Nginx进程的事件驱动模块。

下面开始介绍事件驱动模块接口的相关知识。

事件模块是一种新的模块类型,ngx_module_t表示Nginx模块的基本接口,而针对于每一种不同类型的模块,都有一个结构体来描述这一类模块的通用接口,这个接口保存在ngx_module_t结构体的ctx成员中。例如,核心模块的通用接口是ngx_core_module_t结构体,而事件模块的通用接口则是ngx_event_module_t结构体(参见图8-1),具体如下所示。


typedef struct{

//事件模块的名称

ngx_str_t*name;

//create_conf和init_conf方法的调用可参见图9-3

//在解析配置项前,这个回调方法用于创建存储配置项参数的结构体

voidcreate_conf)(ngx_cycle_t*cycle);

/在解析配置项完成后,init_conf方法会被调用,用以综合处理当前事件模块感兴趣的全部配置项/

charinit_conf)(ngx_cycle_tcycle,voidconf);

//对于事件驱动机制,每个事件模块需要实现的10个抽象方法

ngx_event_actions_t actions;

}ngx_event_module_t;


ngx_event_module_t中的actions成员是定义事件驱动模块的核心方法,下面重点看一下actions中的这10个抽象方法,代码如下。


typedef struct{

/添加事件方法,它将负责把1个感兴趣的事件添加到操作系统提供的事件驱动机制(如epoll、kqueue等)中,这样,在事件发生后,将可以在调用下面的process_events时获取这个事件/

ngx_int_t(add)(ngx_event_tev,ngx_int_t event,ngx_uint_t flags);

/删除事件方法,它将把1个已经存在于事件驱动机制中的事件移除,这样以后即使这个事件发生,调用process_events方法时也无法再获取这个事件/

ngx_int_t(del)(ngx_event_tev,ngx_int_t event,ngx_uint_t flags);

/启用1个事件,目前事件框架不会调用这个方法,大部分事件驱动模块对于该方法的实现都是与上面的add方法完全一致的/

ngx_int_t(enable)(ngx_event_tev,ngx_int_t event,ngx_uint_t flags);

/禁用1个事件,目前事件框架不会调用这个方法,大部分事件驱动模块对于该方法的实现都是与上面的del方法完全一致的/

ngx_int_t(disable)(ngx_event_tev,ngx_int_t event,ngx_uint_t flags);

/向事件驱动机制中添加一个新的连接,这意味着连接上的读写事件都添加到事件驱动机制中了/

ngx_int_t(add_conn)(ngx_connection_tc);

//从事件驱动机制中移除一个连接的读写事件

ngx_int_t(del_conn)(ngx_connection_tc,ngx_uint_t flags);

/仅在多线程环境下会被调用。目前,Nginx在产品环境下还不会以多线程方式运行,因此这里不做讨论/

ngx_int_t(process_changes)(ngx_cycle_tcycle,ngx_uint_t nowait);

/在正常的工作循环中,将通过调用process_events方法来处理事件。这个方法仅在第8章中提到的ngx_process_events_and_timers方法中调用,它是处理、分发事件的核心/

ngx_int_t(process_events)(ngx_cycle_tcycle,ngx_msec_t timer,ngx_uint_t flags);

//初始化事件驱动模块的方法

ngx_int_t(init)(ngx_cycle_tcycle,ngx_msec_t timer);

//退出事件驱动模块前调用的方法

void(done)(ngx_cycle_tcycle);

}ngx_event_actions_t;


ngx_event_core_module和9个事件驱动模块都必须在ngx_module_t结构体的ctx成员中实现ngx_event_module_t接口。读者将在9.5节中看到ngx_event_core_module模块是如何实现该接口的,对于具体的事件驱动模块,这里只讨论ngx_epoll_module事件驱动模块,在9.6节中,我们才会介绍ngx_epoll_module是如何实现该接口的。