nginx源码阅读(七).nginx的模块化设计

来源:互联网 发布:写算法 编辑:程序博客网 时间:2024/05/29 12:53

前言

高度模块化是nginx的一个特点,在正式进入到具体的模块之前,有必要从整体把握各模块之间联系与nginx对模块的控制。在前面的分析中其实已经使用了模块中提供的方法,比如worker进程的工作循环中调用的ngx_process_events_and_timers()函数,用于处理事件。

模块之间的联系

官方提供了5个类型的模块:核心模块、配置模块、事件模块、http模块、mail模块。

  • 配置模块主要负责解析nginx.conf文件,是其他模块的基础,该类模块中只有一个ngx_conf_module模块;
  • 核心模块主要负责定义除配置模块之外的其他模块,该类模块中有6个核心模块。
    1. ngx_mail_module负责定义mail模块;
    2. ngx_http_module负责定义http模块;
    3. ngx_events_module负责定义事件模块;
    4. ngx_core_module则是nginx启动加载的第一个模块,它主要用来保存全局配置项。
    5. ngx_openssl_module只有当加载了之后,nginx才支持https请求
    6. ngx_errlog_module
  • 事件模块即负责事件的注册、分发处理、销毁等,该类模块中主要有这几个模块:
    1. ngx_event_core_module负责加载其他事件模块,是其他事件模块的基础
    2. ngx_epoll_module,该模块则是我们后面需要重点分析的模块,它负责事件的注册、集成、处理等
    3. 其他的模块比如ngx_kqueue_module这些我们后面基本不会涉及,就不解释了,毕竟是另外一种I/O多路复用机制,大致的思想是一样的
  • http模块和mail模块也无需太多解释,等到后面再介绍。

从中我们可以看出,核心模块中的部分模块与其他模块有一定的联系,比如核心模块中的ngx_event_module模块,它就作为事件模块的基础,会帮事件模块解析配置项以及存储事件模块中各模块的存储配置项的结构体指针。

这样的设计可以让核心模块尽量的简单,只做一些初始化还有退出的工作,而其他的事情交给相应类型的模块去细做即可。

除了核心模块与其他模块的联系之外,其实各类型模块中的子模块也有联系,同样也以事件模块为例子,在不同的系平台上,I/O多路复用机制可能也不同,比如linux2.6之后的epoll、FreeBSD的kqueue等,那么选择一种合适的机制就成了一个很重要的问题,事件模块中的ngx_event_core_module就负责这个工作;又比如http模块中,ngx_http_core_module也负责决定对于不同的请求该选用哪一个http模块来处理。

统一的通用接口

为了将每个模块简单的统一起来,nginx定义了ngx_module_t结构体作为每个模块的通用接口,同时考虑到灵活性的问题,ngx_module_t里面只涉及到了模块的初始化还有退出等操作,并且其中的ctx成员是一个void *指针,它可以作为任意一种具体类型的模块中的通用性接口,比如在事件模块中,又可以自己再实现一个通用性接口用于事件模块的统一。

还记得ngx_modules数组吗,它的类型就是ngx_module_t,即存储了所有模块的ngx_module_t接口,通过遍历该数组访问统一的接口,就可以做到初始化以及退出,并且通过ctx可以访问到具体类型模块的通用性接口(比如使用ctx调用核心模块中所有模块的create_conf方法)。

下面则是ngx_module_t的真面目:

typedef struct ngx_module_s       ngx_module_t;struct ngx_module_s {    //模块在该类模块中的序号    ngx_uint_t            ctx_index;    //所有模块在ngx_modules数组中的序号    ngx_uint_t            index;    ngx_uint_t            spare0;    ngx_uint_t            spare1;    ngx_uint_t            spare2;    ngx_uint_t            spare3;    //模块版本    ngx_uint_t            version;    //该成员一般指向同一类型模块下的通用性接口    //这样的设计使得模块之间层次分明    //并且支持多种不同类型模块拥有自己特点的接口    //这样的做法称为具体化ngx_module_t接口    void                 *ctx;    //该数组指定了模块处理配置项的方法    ngx_command_t        *commands;    //模块的类型    //比如配置模块的则是NGX_CONF_MODULE    //核心模块的则是NGX_CORE_MODULE    ngx_uint_t            type;    //master进程初始化时使用    //不过我这个版本并没有使用    ngx_int_t           (*init_master)(ngx_log_t *log);    //模块初始化时使用    ngx_int_t           (*init_module)(ngx_cycle_t *cycle);    //工作进程初始化时使用    //在nginx初始化的最后阶段会调用    ngx_int_t           (*init_process)(ngx_cycle_t *cycle);    //初始化/退出线程    //同样并没有使用    ngx_int_t           (*init_thread)(ngx_cycle_t *cycle);    void                (*exit_thread)(ngx_cycle_t *cycle);    //工作进程退出时调用    void                (*exit_process)(ngx_cycle_t *cycle);    //master进程退出时调用    void                (*exit_master)(ngx_cycle_t *cycle);    //预留成员,并没有使用    uintptr_t             spare_hook0;    uintptr_t             spare_hook1;    uintptr_t             spare_hook2;    uintptr_t             spare_hook3;    uintptr_t             spare_hook4;    uintptr_t             spare_hook5;    uintptr_t             spare_hook6;    uintptr_t             spare_hook7;};

为了加深对nginx中模块化的理解,我们再来回顾一下第二小节中分析的nginx的初始化。其实里面就用到了ngx_core_module模块,只是当时为了方便讲解以及不一来就以比较抽象的概念来描述初始化过程所以并没有提起。

这里可以以整体架构的角度来考虑nginx的做法,并且再复习一下前面的内容。

ngx_core_module

ngx_module_t只是整体框架提供给各模块的统一接口,但是每种类型的模块的需求并不一样,比如事件模块中,肯定都需要添加/删除事件等,而http模块中,则需要合并配置项等。因此,ctx成员的引入,使得不同类型之间的模块可以自己定义自己的接口(根据模块类型的不同具体化ngx_module_t),我们可以通过type的不同,使用ctx成员访问特定的接口,可能有点抽象,举个例子,比如核心模块,它的type成员为NGX_CORE_MODULE,我们可以遍历ngx_modules数组并只访问类型为NGX_CORE_MODULE的模块,然后通过ctx调用核心模块特有的create_conf以及init_conf方法。

这样设计的结果就是使得模块之前出现了多层次以及多类型化。配置模块很明显是所有模块的基础,而核心模块则是事件模块、http模块、mail模块的基础,事件模块又是http模块、mail模块的基础。

下面是ngx_init_cycle函数中涉及到核心模块的部分代码:

......//遍历所有模块for (i = 0; ngx_modules[i]; i++) {    //若非核心模块直接跳过    if (ngx_modules[i]->type != NGX_CORE_MODULE) {        continue;    }    //获取到核心模块具体化ngx_module_t之后的结构体指针    module = ngx_modules[i]->ctx;    //调用核心模块内通用性接口的create_conf方法    //用于创建存储配置项的数据结构    //并且在读取nginx.conf配置文件时,会根据ngx_command_t把解析出来的配置项存放在其中    if (module->create_conf) {        rv = module->create_conf(cycle);        if (rv == NULL) {            ngx_destroy_pool(pool);            return NULL;        }        //conf_ctx是一个四级指针,用于存放指向存储各模块配置项的结构体指针        cycle->conf_ctx[ngx_modules[i]->index] = rv;    }}......//调用核心模块中各模块的init_conffor (i = 0; ngx_modules[i]; i++) {    //非核心模块直接跳过    if (ngx_modules[i]->type != NGX_CORE_MODULE) {        continue;    }    module = ngx_modules[i]->ctx;    if (module->init_conf) {        /* 通过ctx访问核心模块中特有的接口         * 之前已经调用过了create_conf方法         * 因此这里将使用解析过的配置项来初始化核心模块         */        if (module->init_conf(cycle, cycle->conf_ctx[ngx_modules[i]->index])            == NGX_CONF_ERROR)        {            environ = senv;            ngx_destroy_cycle_pools(&conf);            return NULL;        }    }}

核心模块的ngx_module_tctx成员指向的具体化的结构体是ngx_core_module_t,定义如下:

typedef struct {  //核心模块的名称  ngx_str_t name;  /* create_conf和init_conf方法的功能上面已经说过,这里不做解释了 */  void *(*create_conf)(ngx_cycle_t *cycle);  char *(*init_conf)(ngx_cycle_t *cycle, void *conf);}

ngx_modules数组中各模块的顺序

综合以上所讲解的,可以轻而易举的看出各模块在ngx_modules的顺序是很重要的。比如ngx_event_module肯定需要在事件模块初始化前先初始化好,因为ngx_event_module的排在ngx_modules数组中的顺序肯定在事件模块中任何一个模块前面,而ngx_event_core_module模块又肯定在事件模块中的其他模块之前,毕竟它要选择使用哪一种I/O多路复用机制。

如何获取存储模块的配置项结构体指针

nginx的核心结构体ngx_cycle_t中有一个四级指针成员conf_ctx,通过它可以获取任意模块的配置项结构体指针。不过由于它是一个四级指针,可能第一次见理解起来不是那么顺利,这样去理解可能要好一些:首先conf_ctx是一个数组(一个),它存储的元素是指针类型(两个),每个指针又指向了另外的数组(三个),而每个数组又存储的是指针(四个)。这就是为什么conf_ctx是四级指针。

conf_ctx的第一级指针(数组),存储的其实是所有核心模块的配置结构体指针,以事件模块为例,通过该指针,就可以得到任意事件模块的配置项结构体指针(通过ngx_module_t中的ctx_index编号)(之ngx_event_module会将所有事件模块的配置项结构体指针形成一个数组)。

这样我们就可以传入想获取配置项结构体指针的模块名,然后通过ngx_module_t中的index还有ctx_index编号来获取配置项结构体指针。

小结

本小节中,主要从整体的角度来分析了模块之间的联系以及nginx对其的高度抽象。除了少量框架代码之外,其余的全都是模块。

有了对模块的整体认识,有助于我们对事件模块进行分析。