Nginx 源码阅读笔记5 初始化 cycle

    xiaoxiao2021-04-11  35

    距离上一篇过了有一个多星期,虽然没有写总结笔记,但是一直有在看代码,目前看完了 http 的发送,感兴趣的只剩下 subrequest 和 upstream 部分了,不得不说啊,这个异步的代码,看起来真的是难受,还有就是前几天发布了1.12.0版本,而我在看得是1.10.3版本,虽然没什么大问题,但是强迫症告诉我要看新的,于是搞了很久的diff和patch,由于代码中含注释太多,打起补丁来太麻烦了,所以放弃了之前的注释,需要的时候再对照吧 接下来进入正题吧 ,cycle 存在于 nginx 的整个运行周期,是十分重要的部分,首先看一下结构体的定义,省略了部分认为不重要的内容

    struct ngx_cycle_s { void ****conf_ctx; // 各模块配置 ngx_pool_t *pool; // 内存池 ngx_log_t *log; // 日志文件 ngx_log_t new_log; // 配置文件中指定的 error_log ngx_uint_t log_use_stderr; // 使用 stderr ngx_connection_t **files; // 由于 epoll 不使用这个 所以忽略 ngx_connection_t *free_connections; // 空闲连接数组 ngx_uint_t free_connection_n; // 空闲连接数组大小 ngx_module_t **modules; // 所有模块 ngx_uint_t modules_n; // 模块数量 ngx_uint_t modules_used; // 模块已开始使用 用于判断是否可以动态加载 ngx_queue_t reusable_connections_queue; // 可重用连接队列 ngx_uint_t reusable_connections_n; // 可重用连接数量 ngx_array_t listening; // 监听对象数组 ngx_array_t paths; // 需要操作的路径数组 ... ngx_list_t open_files; // 打开的所有文件 例如 log 文件 ngx_list_t shared_memory; // 共享内存 ngx_uint_t connection_n; // 连接数组大小 ngx_uint_t files_n; // 由于 epoll 不使用这个 所以忽略 ngx_connection_t *connections; // 连接数组 ngx_event_t *read_events; // 连接数组对应的读事件数组 ngx_event_t *write_events; // 连接数组对应的写事件数组 ngx_cycle_t *old_cycle; // 旧的 cycle ngx_str_t conf_file; // 配置文件绝对路径 ngx_str_t conf_param; // 额外配置参数 ngx_str_t conf_prefix; // 配置文件路径前缀 ngx_str_t prefix; // 路径前缀 ngx_str_t lock_file; // 实现文件锁的文件路径 ngx_str_t hostname; // 主机名 };

    然后来看一下初始化函数的原型

    ngx_cycle_t *ngx_init_cycle(ngx_cycle_t *old_cycle);

    首先需要明确一点,除了main函数会调用这个函数(参数为初始化时使用的 cycle),进程接收到信号需要 reload 时也会调用这个函数(参数为当前使用的 cycle) 函数一开始先初始化时间,这里还是老套路,将秒数设置为 0 以强制更新时间

    ngx_timezone_update(); /* force localtime update with a new timezone */ tp = ngx_timeofday(); tp->sec = 0; ngx_time_update();

    接下来重用old_cycle的部分成员,根据old_cycle来为各个数组或链表分配空间

    cycle->pool = pool; cycle->log = log; cycle->old_cycle = old_cycle; cycle->conf_prefix.len = old_cycle->conf_prefix.len; cycle->conf_prefix.data = ngx_pstrdup(pool, &old_cycle->conf_prefix); if (cycle->conf_prefix.data == NULL) { ... } cycle->prefix.len = old_cycle->prefix.len; cycle->prefix.data = ngx_pstrdup(pool, &old_cycle->prefix); if (cycle->prefix.data == NULL) { ... } cycle->conf_file.len = old_cycle->conf_file.len; cycle->conf_file.data = ngx_pnalloc(pool, old_cycle->conf_file.len + 1); if (cycle->conf_file.data == NULL) { ... } ngx_cpystrn(cycle->conf_file.data, old_cycle->conf_file.data, old_cycle->conf_file.len + 1); cycle->conf_param.len = old_cycle->conf_param.len; cycle->conf_param.data = ngx_pstrdup(pool, &old_cycle->conf_param); if (cycle->conf_param.data == NULL) { ... } n = old_cycle->paths.nelts ? old_cycle->paths.nelts : 10; if (ngx_array_init(&cycle->paths, pool, n, sizeof(ngx_path_t *)) != NGX_OK) { ... } ngx_memzero(cycle->paths.elts, n * sizeof(ngx_path_t *)); ... if (old_cycle->open_files.part.nelts) { n = old_cycle->open_files.part.nelts; for (part = old_cycle->open_files.part.next; part; part = part->next) { n += part->nelts; } } else { n = 20; } if (ngx_list_init(&cycle->open_files, pool, n, sizeof(ngx_open_file_t)) != NGX_OK) { ... } if (old_cycle->shared_memory.part.nelts) { n = old_cycle->shared_memory.part.nelts; for (part = old_cycle->shared_memory.part.next; part; part = part->next) { n += part->nelts; } } else { n = 1; } if (ngx_list_init(&cycle->shared_memory, pool, n, sizeof(ngx_shm_zone_t)) != NGX_OK) { ... } n = old_cycle->listening.nelts ? old_cycle->listening.nelts : 10; if (ngx_array_init(&cycle->listening, pool, n, sizeof(ngx_listening_t)) != NGX_OK) { ... } ngx_memzero(cycle->listening.elts, n * sizeof(ngx_listening_t)); ngx_queue_init(&cycle->reusable_connections_queue);

    根据模块数量ngx_max_module为conf_ctx分配空间,模块数量是在ngx_preinit_modules函数中设置的,这个函数在main函数中被调用

    cycle->conf_ctx = ngx_pcalloc(pool, ngx_max_module * sizeof(void *)); if (cycle->conf_ctx == NULL) { ... }

    获取主机名,如果太长则截断即可

    if (gethostname(hostname, NGX_MAXHOSTNAMELEN) == -1) { ... } /* on Linux gethostname() silently truncates name that does not fit */ hostname[NGX_MAXHOSTNAMELEN - 1] = '\0'; cycle->hostname.len = ngx_strlen(hostname); cycle->hostname.data = ngx_pnalloc(pool, cycle->hostname.len); if (cycle->hostname.data == NULL) { ... } ngx_strlow(cycle->hostname.data, (u_char *) hostname, cycle->hostname.len);

    初始化modules和modules_n成员,并为核心模块NGX_CORE_MODULE调用create_conf函数,创建存放各核心模块配置的结构体,并存放到conf_ctx的相应位置

    if (ngx_cycle_modules(cycle) != NGX_OK) { ... } for (i = 0; cycle->modules[i]; i++) { if (cycle->modules[i]->type != NGX_CORE_MODULE) { continue; } module = cycle->modules[i]->ctx; if (module->create_conf) { rv = module->create_conf(cycle); if (rv == NULL) { ... } cycle->conf_ctx[cycle->modules[i]->index] = rv; } }

    这里ngx_cycle_modules函数简单地将由configure生成的ngx_modules数组拷贝过来 接下来准备解析配置文件,先简单看下解析配置用到的结构体

    struct ngx_conf_s { char *name; ngx_array_t *args; // 配置项对应的参数数组 ngx_pool_t *pool; ngx_pool_t *temp_pool; ngx_conf_file_t *conf_file; // 对应的配置文件 ngx_log_t *log; void *ctx; // 解析配置项时对应的上下文环境 ngx_uint_t module_type; // 需要解析的模块类型 ngx_uint_t cmd_type; // 需要解析的命令类型 ... };

    这里需要注意的是ctx这个成员,比如在解析配置文件的核心模块配置时,与cycle->conf_ctx指向相同位置,但是解析http块的main级配置、srv级配置或loc级配置时值会被设置为各自的上下文环境结构体 还有就是args存放了配置项的命令,例如若解析到listen 80则第一个元素为listen,第二个元素为80

    ngx_memzero(&conf, sizeof(ngx_conf_t)); /* STUB: init array ? */ conf.args = ngx_array_create(pool, 10, sizeof(ngx_str_t)); if (conf.args == NULL) { ... } conf.temp_pool = ngx_create_pool(NGX_CYCLE_POOL_SIZE, log); if (conf.temp_pool == NULL) { ... } conf.ctx = cycle->conf_ctx; conf.cycle = cycle; conf.pool = pool; conf.log = log; conf.module_type = NGX_CORE_MODULE; conf.cmd_type = NGX_MAIN_CONF;

    上面的代码设置了conf变量,并指定了解析的类型为核心模块,命令类型为NGX_MAIN_CONF 接下来首先解析的是conf_param,也就是-g后面所带的参数,然后才是配置文件

    if (ngx_conf_param(&conf) != NGX_CONF_OK) { ... } if (ngx_conf_parse(&conf, &cycle->conf_file) != NGX_CONF_OK) { ... }

    ngx_conf_parse函数中会打开cycle->conf_file指定的配置文件,简单来说这个函数就是逐个token地读入,若遇到特殊情况(例如{与;表示一个配置项的结束),则进行各种判断,根据情况调用相应的命令处理函数,其中有一段代码个人认为有必要理解一下

    if (cmd->type & NGX_DIRECT_CONF) { conf = ((void **) cf->ctx)[cf->cycle->modules[i]->index]; } else if (cmd->type & NGX_MAIN_CONF) { conf = &(((void **) cf->ctx)[cf->cycle->modules[i]->index]); } else if (cf->ctx) { confp = *(void **) ((char *) cf->ctx + cmd->conf); if (confp) { conf = confp[cf->cycle->modules[i]->ctx_index]; } } rv = cmd->set(cf, cmd, conf);

    首先需要区分NGX_DIRECT_CONF和NGX_MAIN_CONF,两者都被核心模块使用,至于不同点比较权威的解释可以看这里,但是简单来说就是指定NGX_DIRECT_CONF的命令,其模块会自动创建配置结构体,再来看下简化的ctx_conf结构图 可以看到,对于无需上下文的核心模块,只需要二级指针即可,这也就是代码中的第一种情况,四级指针ctx_conf被转型为二级指针,而第二种情况对应的就是需要上下文的核心模块,例如图中的ngx_http_module,这里传过去的是二级指针的地址,目的是在其中为这个二级指针分配空间(因为这个模块没有设置create_conf函数,所以不会自动创建配置结构体),也就是在解析http块时,首先会使用传入的指针地址,解引用并分配ngx_http_conf_ctx_t结构体 最后一种情况对应着非核心模块,需要注意之前说过conf->ctx的值,例如在解析http块遇到server配置块时,conf->ctx会被设置为先前分配的ngx_http_conf_ctx_t结构体以作为上下文环境

    typedef struct { void **main_conf; void **srv_conf; void **loc_conf; } ngx_http_conf_ctx_t;

    所以这里confp就是在计算是哪一级配置,例如配置项中指定了NGX_HTTP_SRV_CONF_OFFSET,则confp就会指向结构体中的srv_conf,代码的最后调用了set,也就是命令处理函数 最后还要说明下,依旧以解析http块为例子,在解析时如果遇到server配置块,会将原来的conf保存,除了之前说的ctx,还会修改module_type、cmd_type等参数,然后再次调用ngx_conf_parse递归地进行解析,也就是说,如果ngx_cycle_init中的ngx_conf_parse成功返回,也就代表成功解析了整个配置文件 在解析完配置后调用核心模块的init_conf函数,需要记住这个时间点,以后分析核心模块的这个函数时头脑会比较清晰

    for (i = 0; cycle->modules[i]; i++) { if (cycle->modules[i]->type != NGX_CORE_MODULE) { continue; } module = cycle->modules[i]->ctx; if (module->init_conf) { if (module->init_conf(cycle, cycle->conf_ctx[cycle->modules[i]->index]) == NGX_CONF_ERROR) { ... } } }

    判断进程类型是否为NGX_PROCESS_SIGNALLER,也就是用来发送信号的进程,是的话到这里就可以返回了,至于为什么发送信号也要等到解析完配置文件,我觉得可能是为了知道pid文件的路径吧 接下来是根据解析得到的路径创建pid文件,如果是main函数中调用的ngx_init_cycle则不用创建,因为在变成守护进程时涉及fork操作,会改变进程 id,所以需要留到main函数中创建,然而如果是 reload 的情况则需要在这里创建

    ccf = (ngx_core_conf_t *) ngx_get_conf(cycle->conf_ctx, ngx_core_module); if (ngx_test_config) { ... } else if (!ngx_is_init_cycle(old_cycle)) { /* * we do not create the pid file in the first ngx_init_cycle() call * because we need to write the demonized process pid */ 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) { /* new pid file name */ if (ngx_create_pidfile(&ccf->pid, log) != NGX_OK) { goto failed; } ngx_delete_pidfile(old_cycle); } }

    代码中利用ngx_is_init_cycle判断是否为初次调用,其实这是个宏定义,也就是判断old_cycle->conf_ctx是否为空 接下来测试lock_file是否可用,在利用文件锁实现互斥锁时需要它,然后是创建cycle->paths中指定的路径,打开默认的日志文件

    if (ngx_test_lockfile(cycle->lock_file.data, log) != NGX_OK) { goto failed; } if (ngx_create_paths(cycle, ccf->user) != NGX_OK) { goto failed; } if (ngx_log_open_default(cycle) != NGX_OK) { goto failed; }

    ngx_log_open_default首先会判断是否在配置文件中指定了error_log,有则什么都不做,若没有则将默认的日志文件加入open_files,这里说的打开并不是调用open打开文件,而是单纯地加入open_files数组,接下来统一打开

    part = &cycle->open_files.part; file = part->elts; for (i = 0; /* void */ ; i++) { if (i >= part->nelts) { if (part->next == NULL) { break; } part = part->next; file = part->elts; i = 0; } if (file[i].name.len == 0) { continue; } file[i].fd = ngx_open_file(file[i].name.data, NGX_FILE_APPEND, NGX_FILE_CREATE_OR_OPEN, NGX_FILE_DEFAULT_ACCESS); if (file[i].fd == NGX_INVALID_FILE) { goto failed; } #if !(NGX_WIN32) if (fcntl(file[i].fd, F_SETFD, FD_CLOEXEC) == -1) { goto failed; } #endif }

    上面这段程序就是真正意义上的打开文件,如果不是WIN32系统,还需要设置CLOEXEC标志位,需要注意的是,这里包括了error_log指定的或默认的日志文件,也就是说,打开新的日志文件后,就可以替换掉在main函数中打开的初始日志文件了

    cycle->log = &cycle->new_log; pool->log = &cycle->new_log;

    然后是初始化共享内存和处理listening数组,其中都重用了符合条件的旧对象,这里要看懂共享内存可能需要了解 ngnix 共享内存的slab机制,由于代码过长这里就不贴出来了 在设置完listening数组后,需要打开并配置相应的 socket

    if (ngx_open_listening_sockets(cycle) != NGX_OK) { goto failed; } if (!ngx_test_config) { ngx_configure_listening_sockets(cycle); }

    根据ngx_use_stderr决定是否调用重定向函数,之前在main中提到过的,初始运行时ngx_use_stderr为 1 所以不会调用,然后在main中被设置为了 0,所以之后每次 reload 时都会调用,作用之前说过,至于为什么要这样,我也不清楚

    if (!ngx_use_stderr) { (void) ngx_log_redirect_stderr(cycle); }

    调用各个模块的init_module函数

    if (ngx_init_modules(cycle) != NGX_OK) { /* fatal */ exit(1); }

    最后是销毁掉old_cycle中无效的资源,例如共享内存、监听描述符和打开的文件,当然还有临时内存池 清理完之后,如果是 master 模式或初次初始化则提前返回

    if (ngx_process == NGX_PROCESS_MASTER || ngx_is_init_cycle(old_cycle)) { ngx_destroy_pool(old_cycle->pool); cycle->old_cycle = NULL; return cycle; }

    如果是 reload 且为单进程模式,则需要注意,old_cycle中部分连接可能还在使用中,不能马上清理,需要将其加入到定时器中延迟清理,master 模式不存在这个问题是因为连接是在 worker 进程中使用的

    if (!ngx_cleaner_event.timer_set) { ngx_add_timer(&ngx_cleaner_event, 30000); ngx_cleaner_event.timer_set = 1; }

    其中ngx_cleaner_event为ngx_event_t类型,其handler为ngx_clean_old_cycles 剩下的为fail标签的部分,里面是一些收拾烂摊子的工作,这里就不详细分析了

    转载请注明原文地址: https://ju.6miu.com/read-666481.html

    最新回复(0)