nginx 源码(5)事件驱动架构

接上文。
这一节来看看nginx是如何封装epoll构筑整个事件驱动模块的,主要参看了《nginx从入门到精通》 http://tengine.taobao.org/book/index.html 、《nginx模块开发与架构解析》和官方的一些资源http://wiki.nginx.org/Resources。
先不去看源码,想想假如我们用epoll来实现服务器会怎么做。

  • 创建server socket
  • 用fcntl设置nonblocking标志
  • 用setsockopt设置socket其他一些标志位
  • 绑定、监听
  • 用epoll_create创建epoll描述符,相当于初始化epoll框架本身
  • 设置epoll想要监听的事件,这里是为了accept新的socket,所以一定是读事件,另外这里要考虑使用ET还是LT触发
  • 使用epoll_ctl把server socket加入epoll框架
  • epoll_wait开始等待事件,简单的服务器就直接阻塞在这里,直到有事件返回
  • 新连接到达,读事件被触发,新建client socket连接,设置nonblocking和setsockopt,设置想要监听的事件,这里有读也有写,但不是连接,都是指在已建立的client socket连接上进行的读写,加入epoll_ctl
  • 等待事件被触发,循环处理事件,通常因为这里是个循环,所以新连接到达和已有连接上新数据到达在这里通过判断返回的描述符来区分
不知道其他人怎么想,我在上面的描述中已经重点区分了新连接的建立和已有连接上的新数据到达两个事件。网上流行的几乎所有例子都是在一个循环里,直接if else处理这些事件,但我一直觉得很别扭,直到看了nginx源码才发现这样真的不太好。虽然一般连接建立,就预示新数据要到达,而且新连接本质上就是新数据到达,但从概念上这的确是两个不同的东西,同一个连接,新连接的数据只会有一次,而后可以有很多次的数据读写,这些都和连接无关,不在一个逻辑概念层次上。
先不说这个了,上面只是一家之言。nginx的事件模块本身就是一个模块,而nginx设计的核心就是配置和模块,因为对源码还不够熟悉,所以目前还没有开始涉及这一块,但此处有必要简单地描述nginx的模块架构。在nginx里,模块的定义在ngx_conf_file.h文件中。
struct ngx_module_s { ngx_uint_tctx_index; ngx_uint_tindex; void*ctx; ngx_command_t*commands; ngx_uint_ttype; ngx_int_t(*init_module)(ngx_cycle_t *cycle); ngx_int_t(*init_process)(ngx_cycle_t *cycle); };

看上面的结构体,在前面的文章中,已经在系统启动过程中打印过其中的ctx_index、index、type和commands中的name属性了。ngx_command_s也定义在ngx_conf_file.h中文件内。
struct ngx_command_s { ngx_str_tname; inttype; char*(*set)(ngx_conf_t *cf, ngx_command_t *cmd, void *conf); intconf; intoffset; void*post; };

对C语言不太熟悉的人看到这两个结构体可能没什么感觉,而且上C语言课的时候也没有人这么说过,但还是有一些资料上会提到,这样的结构体本质上就是一个类,而不仅仅是平常印象里用来存储数据的单元,结构体远比存储单元所做的事情要多,因为这里不仅可以定义指向其他结构体的指针,比如ngx_command_s,还可以定义函数指针。其实这就相当于C++或Java中的类,只是没有属性限定符。理清这节,再来看这两个结构体就比较容易理解了,两个不同的类型里各自定义了一些属性,和可以执行的方法。
ngx_module_s中除了index和type外,有个ngx_command_s类型的熟悉,定义的是这个模块的指令(command翻译成命令,但很多文献上又用directive来说明,不知道当初nginx作者为什么不直接用directive来命名),而指令的实际意义就是nginx.conf文件中以行为单位的配置项(区别于以{}为单元的配置项)。这一点就给了每个模块在配置上很大的自由,因为每个模块可以有自己的配置指令。再看另外两个方法,init_module和init_process,很显然这是两个初始化的方法,前者在init_cycle中调用,主要分配空间,后者在process_cycle中调用,主要对该模块做各种设置。
ngx_command_s中的就不细说了,总之是与存取和解析指令相关的。本文重点关注事件模块。提到模块的架构,是因为这里涉及到了事件模块的初始化。当nginx启动时,对所有模块进行初始化的时候,事件模块也在其中。
由于ngx_module_s是一个类,这个“类“就得“实例化“,在nginx这个“实例化“就在ngx_event.c文件中。
ngx_module_tngx_events_module = { NGX_MODULE, &ngx_events_module_ctx,/* module context */ ngx_events_commands,/* module directives */ NGX_CORE_MODULE,/* module type */ NULL,/* init module */ NULL/* init process */ };

严格和非严格地说,这里都算不上实例化,因为它形式上就是赋值,但因为在c语言的系统里,它这样定义就是一个全局变量,而在此,它的值基本不可能会变,所以就当作是实例化,但又因为,如果把C语言的结构体以类的形式来看,它的属性就不如方法那么重要了,而此处的两个方法都是NULL,也就是空的方法,用C++类比就是方法未定义,相当于抽象类或接口,所以也就谈不上“实例化“了。
无法实例化,它就只能当一个接口存储一些标志,而实际完成工作的有另外一个实例,在ngx_event.c文件中:
ngx_module_tngx_event_core_module = { NGX_MODULE, &ngx_event_core_module_ctx,/* module context */ ngx_event_core_commands,/* module directives */ NGX_EVENT_MODULE,/* module type */ ngx_event_module_init,/* init module */ ngx_event_process_init/* init process */ };

从名字看,它是事件核心模块,而且定义了初始化方法,当事件模块初始化的时候,就会调用这两个方法,在其中完成相应的设置操作。
在上面的两个结构体里都有一个ctx是前面没有提到的,这本身是个空指针类型,两处都赋上了相应的值,前者是:
static ngx_core_module_tngx_events_module_ctx = { ngx_string("events"), NULL, NULL };

后者是:
ngx_event_module_tngx_event_core_module_ctx = { &event_core_name, ngx_event_create_conf,/* create configuration */ ngx_event_init_conf,/* init configuration */{ NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL } };

【nginx 源码(5)事件驱动架构】两个变量都在ngx_event.c文件中,前者是static类型只在当前文件范围内有效,后者是全局变量且是ngx_event_module_t类型,定义在ngx_event.h文件中:
typedef struct { ngx_str_t*name; void*(*create_conf)(ngx_cycle_t *cycle); char*(*init_conf)(ngx_cycle_t *cycle, void *conf); ngx_event_actions_tactions; } ngx_event_module_t;

其中有两个方法,和一个结构体ngx_event_actions_t,而ngx_event_actions_t结构体也在ngx_event.h中,完全就是一个方法的接口:
typedef struct { ngx_int_t(*add)(ngx_event_t *ev, int event, u_int flags); ngx_int_t(*del)(ngx_event_t *ev, int event, u_int flags); ngx_int_t(*enable)(ngx_event_t *ev, int event, u_int flags); ngx_int_t(*disable)(ngx_event_t *ev, int event, u_int flags); ngx_int_t(*add_conn)(ngx_connection_t *c); ngx_int_t(*del_conn)(ngx_connection_t *c, u_int flags); ngx_int_t(*process_changes)(ngx_cycle_t *cycle, ngx_uint_t try); ngx_int_t(*process_events)(ngx_cycle_t *cycle); ngx_int_t(*init)(ngx_cycle_t *cycle); void(*done)(ngx_cycle_t *cycle); } ngx_event_actions_t;

这些方法都很容易理解,都是对于事件操作的函数。至此,nginx事件模型的所有结构体都齐全了,但还没有考虑epoll,因为nginx要考虑的跨平台移植性,所以对于不同的平台还有相应的模块来完成实际的功能,也就是说,前面定义的结构体相当与是一个模块,加上管理功能,管理具体的事件(做事情的)模块,在这里是epoll,所以在epoll模块里也会有一套完整的定义。从形式上看,epoll模块定义了前面结构体中为空的那些函数指针的值,设置了相应的函数,而前面已有的则反设为空,这非常类似类的继承,父类定义了一些实现了的方法和虚函数,子类继承已有的方法和实现自己的函数。
至此,结构方面的内容介绍完了,下面看流程。系统启动时事件模块遵循整理启动流程,同时,其自身流程如下
  • 首先在系统启动的整体架构下,process_cycle方法中,各模块初始化
  • 事件模块调用module_init方法,分配内存
  • 调用process_init方法,根据ecf->use这个值选择具体的事件(做事的)模块
  • 还是在process_init方法中,分配和设置新连接的各种标志,并设置accept事件的处理方法为accept;
rev->event_handler = &ngx_event_accept;

  • 此时系统整体进入for循环,开始process_events,而这个函数的实际执行者就是epoll模块中的ngx_epoll_process_events方法
  • 在ngx_epoll_process_events函数内,epoll_wait,等待连接
  • 连接到达,判断事件或立即执行ngx_event_accept或延迟事件
  • 在ngx_event_accept方法中,该方法在ngx_event_accept.c文件中,获得并添加连接ngx_epoll_add_connection,设置监听连接的读写事件,
  • 调用ngx_http_init_connection方法,设置读写事件的处理函数ngx_http_init_request,最后调用方法ngx_http_process_request_line。
rev->event_handler = ngx_http_init_request;

上面基本把nginx的事件处理串起来了,但还有两点:
  • accpet的handler函数ngx_http_init_connection被nginx_http_block调用,而nginx_http_block是如何与event模块关联起来的?
  • 如何自己封装epoll模块,类似与nginx?
to be continued…

    推荐阅读