nginx源码分析(1)——启动过程

1. 重要的数据结构

        1. ngx_module_t
        nginx中所有模块的类型都是ngx_module_t类型的,定义了模块的一些属性。nginx是完全模块化的,所有的组件都是模块,从而实现了nginx的高度松耦合。同时,我们在进行nginx模块开发时,也离不开这个数据结构。
struct ngx_module_s {
    /**
     * 在具体类型模块(http、event等)的全局配置结构数组的下标。以http module模块为例,
     * nginx把所有的http module的config信息存放在ngx_http_conf_ctx_t类型的变量中,
     * 这个变量只有3个属性,分别是所有http module的main、srv、loc的config信息的数组。
     * 如果该模块是http module,则ctx_index是该模块的config信息(main、srv、loc)
     * 在ngx_http_conf_ctx_t中的下标。
     */
    ngx_uint_t            ctx_index;

    /**
     * nginx把所有模块(ngx_module_t)存放到ngx_modules数组中,这个数组在nginx源码路
     * 径的objs/ngx_modules.c中,是在运行configure脚本后生成的。index属性就是该模块
     * 在ngx_modules数组中的下标。同时nginx把所有的core module的配置结构存放到ngx_cycle的
     * conf_ctx数组中,index也是该模块的配置结构在ngx_cycle->conf_ctx数组中的下标。
     */
    ngx_uint_t            index;

    ……

    /**
     * 模块的上下文属性,同一类型的模块的属性是相同的,比如core module的ctx是ngx_core_module_t类型。
     * 而http module的ctx是ngx_http_moduel_t类型,event module的ctx是ngx_event_module_t类型等等。
     * 相应类型的模块由分开处理的,比如所有的http module由ngx_http_module解析处理,而所有的event module
     * 由ngx_events_module解析处理。
     */
    void                 *ctx;

    /**
     * 该模块支持的指令的数组,最后以一个空指令结尾。ngx_commond_t的分析见下文。
     */
    ngx_command_t        *commands;

    /**
     * 模块的类型,nginx所有的模块类型:
     * 		NGX_CORE_MODULE
     * 		NGX_CONF_MODULE
     * 		NGX_HTTP_MODULE
     * 		NGX_EVENT_MODULE
     * 		NGX_MAIL_MODULE
     * 这些不同的类型也指定了不同的ctx。
     */
    ngx_uint_t            type;

    /* 接下来都是一些回调函数,在nginx初始化过程的特定时间点调用 */
    ngx_int_t           (*init_master)(ngx_log_t *log);

    /* 初始化完所有模块后调用,在ngx_int_cycle函数(ngx_cycle.c)中 */
    ngx_int_t           (*init_module)(ngx_cycle_t *cycle);

    /* 初始化完worker进程后调用,在ngx_worker_process_init函数(ngx_process_cycle.c)中 */
    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);

    void                (*exit_master)(ngx_cycle_t *cycle);
    ……
};
        下面的表格是type与ctx的对应关系:
模块类型(type) 上下文属性类型(ctx)
NGX_CORE_MODULE
ngx_core_module_t(ngx_conf_file.h)
NGX_CONF_MODULE
NULL
NGX_HTTP_MODULE
ngx_http_module_t(http/ngx_http_config.h)
NGX_EVENT_MODULE
ngx_event_module_t(event/ngx_event.h)
NGX_MAIL_MODULE
ngx_mail_module_t(mail/ngx_mail.h)

        2. ngx_commond_t
        ngx_commond_t描述的是模块的配置指令,也就是出现在配置文件的指令。nginx模块支持多个配置指令,所以是以ngx_commond_t数组形式存储的。这个结构在配置文件解析和模块的配置结构信息初始化时会用到。
struct ngx_command_s {
    /**
     * 指令名,与配置文件中一致
     */
    ngx_str_t             name;

    /**
     * 指令的类型,以及参数的个数。这个属性有两个作用:
     * 	1. 实现只解析某个类型的指令,比如当前这个指令是event module类型的,而正在解析的是
     * 	   http module,所以会跳过所有不是http module类型的指令。
     * 	2. 实现指令参数个数的校验。
     */
    ngx_uint_t            type;

    /*
     * 回调函数,在解析配置文件时,遇到这个指令时调用。
     * cf: 包括配置参数信息cf->args(ngx_array_t类型),以及指令对应的模块上下文cf->ctx
     * 		在解析不同模块的指令时,这个上下文信息不同。比如在解析core module时,cf->ctx
     * 		是ngx_cycle->conf_ctx也就是所有core module的配置结构数组,而在解析http module
     * 		时cf->ctx是ngx_http_conf_ctx_t类型的,其中包含所有http module的main、srv、loc
     * 		的配置结构数组。
     * cmd: 指令对应的ngx_command_t结构。
     * conf:指令对应的模块的配置信息。
     */
    char               *(*set)(ngx_conf_t *cf, ngx_command_t *cmd, void *conf);

    /**
     * 对http module有效,http module的配置结构信息(main、srv、loc)都存放在ngx_http_conf_ctx_t
     * 中对应的数组,conf属性指示这个指令的配置结构是main、srv还是loc。
     */
    ngx_uint_t            conf;

    /**
     * 指令对应属性在模块配置结构中的偏移量。
     */
    ngx_uint_t            offset;
    
    /**
     * 一般是函数指针,在set回调函数中调用。
     */
    void                 *post;
};
        
        3. ngx_cycle_t
        ngx_cycle_t是nginx中最重要的数据结构,包含了全局的配置信息、所有监听的套接字、连接池、读写事件等。ngx_cycle_t相当于nginx的一个生命周期,从nginx启动后直到向nginx发送stop或者reload信号。nginx中有一个全局变量ngx_cycle指向当前的cycle。
struct ngx_cycle_s {
    /**
     * 所有core module的配置结构信息的数组,大小是ngx_max_module。配置结构信息由
     * ngx_core_module_t的create_config回调函数生成,并有init_config回调函数初始化。
     * module的index属性指示了该模块的配置结构信息在conf_ctx数组中的下标。
     */
    void                  ****conf_ctx;

    /**
     * 随ngx_cycle_t同生命周期的内存池,是生命周期最长的,除非nginx关闭或重启。内存块大小
     * 由NGX_CYCLE_POOL_SIZE定义,是16KB。
     */
    ngx_pool_t               *pool;

    ngx_log_t                *log;						// 日志
    ngx_log_t                 new_log;

    ngx_connection_t        **files;						// 链接文件
    ngx_connection_t         *free_connections;				        // 空闲链接数组
    ngx_uint_t                free_connection_n;			        // 空闲链接个数

    ngx_queue_t               reusable_connections_queue;	// 重用链接队列

    /**
     * 监听套接字的数组,类型是ngx_listening_t,ngx_create_listening函数会向这个数组
     * 添加新的监听句柄。
     */
    ngx_array_t               listening;
    ngx_array_t               pathes;						// 路径数组,ngx_path_t

    /**
     * 所有打开的文件描述符的链表,ngx_open_file_t类型。ngx_conf_file.c中的
     * 		ngx_conf_open_file(ngx_cycyle_t *cycle, ngx_str_t *name)
     * 可以返回名字为name的打开的文件描述符,如果对应的描述符不存在,则打开并存入open_files中。
     * 比如:logs/error.log的文件描述符就在其中。
     */
    ngx_list_t                open_files;

    /**
     * 共享内存的链表,ngx_shm_zone_t类型。nginx会把需要进程共享的数据放在共享内存中。
     * 比如,accept锁就放在这里。worker进程只有获取这个锁之后,才能accept到新的连接。
     * 防止惊群。
     */
    ngx_list_t                shared_memory;

    ngx_uint_t                connection_n;					// 链接个数
    ngx_uint_t                files_n;						// 打开文件个数

    /**
     * 连接池,nginx把连接池组织成综合数组和链表特性的一种数据结构,后文会详细介绍。
     */
    ngx_connection_t         *connections;
    ngx_event_t              *read_events;					// 读事件
    ngx_event_t              *write_events;					// 写事件

    ngx_cycle_t              *old_cycle;

    ngx_str_t                 conf_file;					// 配置文件名
    ngx_str_t                 conf_param;					// 由命令行-g提供配置参数
    ngx_str_t                 conf_prefix;					// 配置前缀
    ngx_str_t                 prefix;						// nginx所在路径
    ngx_str_t                 lock_file;
    ngx_str_t                 hostname;						// 主机名
};


2. nginx启动过程

        nginx启动显然是由main函数驱动的,在core/nginx.c文件中,整个启动过程主要是初始化ngx_cycle_t类型的全局变量ngx_cycle。下面分析按照nginx的主要执行流程讲述的,首先从main函数开始。

        (1) main函数

    if (ngx_strerror_init() != NGX_OK) {
        return 1;
    }
        初始化错误提示列表,以errno为下标,元素就是对应的错误提示信息。

    if (ngx_get_options(argc, argv) != NGX_OK) {
        return 1;
    }
        获取命令行参数,比如-t、-c、g等。这里没有使用linux的getopt,主要是为了跨平台。

    ngx_time_init();

#if (NGX_PCRE)
    ngx_regex_init();
#endif

    ngx_pid = ngx_getpid();

    // 初始化log
    log = ngx_log_init(ngx_prefix);
    if (log == NULL) {
        return 1;
    }
        上面几行代码主要完成时间、正则表达式和log的初始化。

    /*
     * init_cycle->log is required for signal handlers and
     * ngx_process_options()
     */

    ngx_memzero(&init_cycle, sizeof(ngx_cycle_t));
    init_cycle.log = log;
    ngx_cycle = &init_cycle;

    // cycle的内存池以1024大小的内存块
    init_cycle.pool = ngx_create_pool(1024, log);
    if (init_cycle.pool == NULL) {
        return 1;
    }

    if (ngx_save_argv(&init_cycle, argc, argv) != NGX_OK) {
        return 1;
    }
        初始化cycle结构,并创建内存块大小为1024的内存池,ngx_save_argv会将命令行参数保存到ngx_os_argv,ngx_argc和ngx_argv,以便于之后master进程完成热代码替换。

    if (ngx_process_options(&init_cycle) != NGX_OK) {
        return 1;
    }
        处理配置文件相关信息,初始化cycle的conf_prefix(配置文件所在路径的前缀)、prefix(nginx可执行文件所在路径)、conf_file(配置文件名)和conf_param(通过命令行-g选项指定的全局配置信息)。

    if (ngx_os_init(log) != NGX_OK) {
        return 1;
    }
        处理操作系统的相关信息,比如页大小ngx_pagesize等。

    if (ngx_crc32_table_init() != NGX_OK) {
        return 1;
    }
        初始化循环冗余检验表。

    if (ngx_add_inherited_sockets(&init_cycle) != NGX_OK) {
        return 1;
    }
        通过环境变量NGINX完成socket的继承,这些继承来的socket会放到init_cycyle的listening数组中。

    ngx_max_module = 0;
    for (i = 0; ngx_modules[i]; i++) {
        ngx_modules[i]->index = ngx_max_module++;
    }
        这段代码是对所有nginx的模块计数,同时更新模块的index属性。ngx_max_module就是所有模块的总数。关于index的解释见上文对ngx_module_t的解释。

    cycle = ngx_init_cycle(&init_cycle);
        这行代码是nginx启动过程中最重要的一个步骤,用于初始化ngx_cycle。下面就进入ngx_init_cycle看看具体cycle的初始化过程。ngx_init_cycle代码有800多行,这里只讲解大概的流程,细节不在展开。

        (2) ngx_init_cycle函数

        在ngx_cycle_t结构中包含各种ngx_list_t、ngx_array_t等类型的属性,ngx_init_cycle的前面200多行代码都是对这些数据结构进行初始化。主要有
                1. 更新时区和时间。
                2. 创建内存池。
                3. 根据old_cycle初始化cycle中的conf_file、conf_prefix、prefix和conf_param。
                4. 初始化pathes(ngx_array_t)。
                5. 初始化openfiles(ngx_list_t)。
                6. 初始化shared_memory(ngx_list_t)。
                7. 初始化listening(ngx_array_t)。
                8. 初始化conf_ctx(void ****)数组,大小是ngx_max_module,用于存储所有core module的配置结构信息。
                9. 调用系统调用gethostname获取主机名,初始化hostname。
        介绍完ngx_init_cycle前面的大部分初始化工作后,下面看一下比较重要的初始化步骤。

    for (i = 0; ngx_modules[i]; i++) {
        if (ngx_modules[i]->type != NGX_CORE_MODULE) {
            continue;
        }

        module = ngx_modules[i]->ctx;

        if (module->create_conf) {
            rv = module->create_conf(cycle);
            if (rv == NULL) {
                ngx_destroy_pool(pool);
                return NULL;
            }
            cycle->conf_ctx[ngx_modules[i]->index] = rv;
        }
    }
        这段代码的逻辑是调用所有core module的create_conf回调函数创建该core module的配置信息结构,并且更新cycle->conf_ctx数组。nginx的core module主要有:
                ngx_core_module(core/nginx.c)
                ngx_http_module(http/ngx_http.c)
                ngx_events_module(event/ngx_event.c)
                ngx_errlog_module(core/ngx_log.c)
                ngx_mail_module(mail/ngx_mail.c)
                ngx_openssl_module(event/ngx_event_openssl.c)
                ngx_google_perftools_module(misc/ngx_google_perftools_module.c)
        只有ngx_core_module和ngx_google_perftools_module两个模块有定义create_conf,而ngx_google_perftools_module仅用于性能测试,所以真正使用时只有ngx_core_module有create_conf回调函数。这个会调用函数会创建ngx_core_conf_t结构,用于存储整个配置文件main scope范围内的信息,比如worker_processes,worker_cpu_affinity等。

    ngx_memzero(&conf, sizeof(ngx_conf_t));
    /* STUB: init array ? */
    conf.args = ngx_array_create(pool, 10, sizeof(ngx_str_t));	// args是指令后带的参数的数组
    if (conf.args == NULL) {
        ngx_destroy_pool(pool);
        return NULL;
    }

    conf.temp_pool = ngx_create_pool(NGX_CYCLE_POOL_SIZE, log);
    if (conf.temp_pool == NULL) {
        ngx_destroy_pool(pool);
        return NULL;
    }

    /* 解析core module对应的指令,此时conf的上下文是cycle->conf_ctx,就是所有core module的config信息 */
    conf.ctx = cycle->conf_ctx;
    conf.cycle = cycle;
    conf.pool = pool;
    conf.log = log;
    conf.module_type = NGX_CORE_MODULE;		// conf.module_type指示将要解析这个类型模块的指令。
    conf.cmd_type = NGX_MAIN_CONF;		// conf.cmd_type指示将要解析的指令的类型。
        这段代码是为解析配置文件做准备。初始化ngx_conf_t,用于解析配置文件并保存解析出来的信息。args是配置文件中指令的信息的数组,args[0]是指令名,args[1] - args[n]是指令的参数,参数个数需要根据ngx_commond_t的type属性做校验。ngx_conf_t中的module_type和cmd_type用于控制解析什么类型的指令,module_type表示只解析该类型模块包含的指令,cmd_type表示将要解析的指令的类型,也就是说只有符合module_type和cmd_type的指令才会被解析。比如module_type取NGX_HTTP_MODULE,而cmd_type取NGX_HTTP_SRV_CONF,那么在一次配置文件解析中,只会对http module的server块的指令进行解析。

    if (ngx_conf_param(&conf) != NGX_CONF_OK) {
        environ = senv;
        ngx_destroy_cycle_pools(&conf);
        return NULL;
    }
        对通过nginx -g xxx 设置的全局配置指令初始化、解析。

    if (ngx_conf_parse(&conf, &cycle->conf_file) != NGX_CONF_OK) {
        environ = senv;
        ngx_destroy_cycle_pools(&conf);
        return NULL;
    }
        解析配置文件。配置文件的解析类似一棵树的遍历,nginx中的指令分为块指令和普通指令,每个块指令对应一棵子树,比如http块和event块。由这些块指令负责调用ngx_conf_parse函数解析块内部的指令。配置文件的具体分析会另开一片文章,这里忽略这些细节。在ngx_conf_parse函数返回后,整个配置文件解析完毕,所有模块的指令已经初始化,也就意味着所有模块基本上都初始化完,实际上ngx_conf_parse函数后面隐藏了大量的信息,包括http模块的初始化和事件模块的初始化。关于http的初始化我们后面再详细描述,这里接着讲述ngx_init_cycle。

    for (i = 0; ngx_modules[i]; i++) {
        if (ngx_modules[i]->type != NGX_CORE_MODULE) {
            continue;
        }

        module = ngx_modules[i]->ctx;

        if (module->init_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;
            }
        }
    }
        初始化所有core module模块的config结构调用ngx_core_module_t的init_conf 。在所有core module中,只有ngx_core_module有init_conf回调,用于对ngx_core_conf_t中没有配置的字段设置默认值。

    if (ngx_test_config) {
        if (ngx_create_pidfile(&ccf->pid, log) != NGX_OK) {
            goto failed;
        }
    } else if (!ngx_is_init_cycle(old_cycle)) {
        old_ccf = (ngx_core_conf_t *) ngx_get_conf(old_cycle->conf_ctx,
                                                   ngx_core_module);
        if (ccf->pid.len != old_ccf->pid.len
            || ngx_strcmp(ccf->pid.data, old_ccf->pid.data) != 0)
        {
            if (ngx_create_pidfile(&ccf->pid, log) != NGX_OK) {
                goto failed;
            }
            ngx_delete_pidfile(old_cycle);
        }
    }
        这段代码很简单就是创建nginx的pid文件。随后的很长一段代码用于创建所有的文件路径、打开文件描述符以及创建共享内存,这里就不贴代码了。接下来的一段代码是用于处理监听socket的,如果监听地址相同的话,则把新、旧cycle的监听socket合并。

    if (ngx_open_listening_sockets(cycle) != NGX_OK) {
        goto failed;
    }
         ngx_open_listening_sockets函数的功能就同它名字一样,用于打开所有的监听socket,具体过程和用socket编程时是一样的,调用socket创建套接字 -> 调用setsockopt设置成可重用socket -> 设置成非阻塞socket -> 调用bind绑定要监听的socket地址 -> 调用listen转化成监听socket。

    if (!ngx_test_config) {
        ngx_configure_listening_sockets(cycle);
    }
        根据cycle配置所有的监听socket,包括设置监听socket的接收缓冲区大小、发送缓冲区大小以及accept filter等。

    for (i = 0; ngx_modules[i]; i++) {
        if (ngx_modules[i]->init_module) {
            if (ngx_modules[i]->init_module(cycle) != NGX_OK) {
                /* fatal */
                exit(1);
            }
        }
    }
        调用所有模块的init_module回调函数,进行模块的初始化动作。ngx_init_cycle最后部分代码主要就是释放多余的资源,包括关闭共享内存、监听socket已经打开的文件等,然后ngx_init_cycle正常返回,接下来继续看main函数。

        (3) main函数

    if (ngx_init_signals(cycle->log) != NGX_OK) {
        return 1;
    }
        注册信号,这些信号是定义在os/unix/ngx_process.c中的signals数组,元素的类型是ngx_signal_t,这个结构定义了信号的值、名字已经信号处理函数。

    if (!ngx_inherited && ccf->daemon) {
        if (ngx_daemon(cycle->log) != NGX_OK) {
            return 1;
        }

        ngx_daemonized = 1;
    }
        这段代码很明显是实现守护进程的。

    if (ngx_create_pidfile(&ccf->pid, cycle->log) != NGX_OK) {
        return 1;
    }
        接下来是创建nginx的pid文件。main函数的最后是关于进程模型的初始化的。

    if (ngx_process == NGX_PROCESS_SINGLE) {
        ngx_single_process_cycle(cycle);

    } else {
        ngx_master_process_cycle(cycle);
    }
        可以看出,这里分为单进程和多进程模式,大部分情况我们都采用多进程模式。这部分内容会在另一篇专门讲述nginx进程模型的文章中再详细介绍。介绍完整个nginx的启动过程,可以总结出启动过程就是一堆的初始化工作,其中最重要的是配置文件解析的过程,在一个ngx_conf_parse函数背后隐藏了大量的秘密,下一篇源码分析就会介绍http模块的启动流程,这样大家就可以了解一个具体模块是怎么初始化的的。




 

你可能感兴趣的:(Web,Server,源码之路,Nginx)