Nginx 启动流程还是比较清晰的,src/core/nginx.c 直接看 main 函数就可以。不过,模块化初始化的回调还是不直观,需要 gdb 帮助
配置 configure
不同于 Apache 模块可以根据 so 来定态加载,Nginx 必须在编绎时指定,其中 event、email、http、core 是必须加载的核心模块,其它或是自定义模块根据需要加载。学习时为了调试,打开 gdb 即可,使用最简单的单步 next, step 足够,调试 worker 时注意追踪父子进程 set follow-fork-mode parent|child
./configure --with-debug --without-http_rewrite_module --without-http_gzip_module
make && make install
粗略的阅读 configure 脚本,主要功能如下:
- uname -s, uname -r, uname -m 获取平台信息,例如我的虚拟机 : Linux 3.10.0-862.el7.x86_64 x86_64
- auto/os/conf 根据平台配置使用何种事件模型,比如 linux 使用 epoll,设置 cpu 绑定 sched_setaffinity, 是否是 64 位系统等等
- auto/modules 这是该脚本最核心的功能,生成 ngx_modules.c 文件,维护全局模块数组 ngx_modules, Nginx 初始化会依赖这个数组。并且数组必须是有顺序的
ngx_module_t *ngx_modules[] = {
&ngx_core_module,
&ngx_errlog_module,
&ngx_conf_module,
&ngx_events_module,
&ngx_event_core_module,
&ngx_epoll_module,
.......
&ngx_http_chunked_filter_module,
&ngx_http_range_header_filter_module,
&ngx_http_gzip_filter_module,
.......
&ngx_http_not_modified_filter_module,
NULL
};
可以看到前 6 个是核心模块,中间省略的是 http 相关的。后面会看到,http 初始化是倒序的,也就是说,一个 http request 会先经过 ngx_http_not_modified_filter_module, 再经过 ngx_http_gzip_filter_module 模块。开发时一定要注意顺序。
- 检测其它动态、静态库是否存在,生成 summary 汇总,如果失败给出原因。
启动流程
网上看到一哥们的《Nginx代码分析》图不错,借用,侵删。
1.解析命令。
ngx_get_options 获取命令行参数,比如大家常用的 -t 检测脚语法文件,-s reload|quit|stop 发送信息等等。如果 -h -v 或未识别的参数则打印 Usage 后退出
2.时间、正则、日志、ssl 初始化
3.初始化os相关
ngx_os_init 设置初始进程名称,pagesize, 获取cpu个数,打开的最大文件个数,生成随机种子,最重要的是执行平台相关的 ngx_os_specific_init 函数,生成全局变量 ngx_os_io
ngx_int_t
ngx_os_specific_init(ngx_log_t *log)
{
...........
ngx_os_io = ngx_linux_io;
return NGX_OK;
}
static ngx_os_io_t ngx_linux_io = {
ngx_unix_recv, //ngx_recv
ngx_readv_chain, //ngx_recv_chain ->recv_chain(相关指针的地方调用
ngx_udp_unix_recv, //ngx_udp_recv
ngx_unix_send, //ngx_send
#if (NGX_HAVE_SENDFILE)
ngx_linux_sendfile_chain, //ngx_send_chain
NGX_IO_SENDFILE //./configure配置了sendfile,编译的时候加上sendfile选项,,就会在ngx_linux_io把flag置为该值
#else
ngx_writev_chain, //ngx_send_chain
0
#endif
};
ngx_os_io_t 是一种多态的实现,根据不同平台,生成不同函数指针。后面会看到,从 socket 读数据时使用函数 ngx_unix_recv,发送 http body 时使用 ngx_writev_chain 等等
4.继承 sockets
Nginx 为了支持热升级,老的进程将己监听的端口文件描述符 fd 写到环境变量 “NGINX”,exec 新启动的进程获取。函数 ngx_add_inherited_sockets 将 sockets 信息写到 cycle->listening, 如果不做这一步会出什么问题呢?新启动的进程,读取配置文件,打开端口,bind 一个己经被打开的,肯定会报错。
- 生成配置结构体
Ngx_init_cycle 核心初始化函数,更新时间、时区、生成共享内存、解析配置文件、打开监听句柄等等。最重要的就是模块回调,通过 gdb 来追踪更容易理解
Nginx 启动要解析配置文件,每个模块都要读取解析,但是框架只关心 core 核心的就可以。那么在解析前,create_conf 生成相应的结构体来保存配置。默认为unset 即可。
for (i = 0; ngx_modules[i]; i++) {
if (ngx_modules[i]->type != NGX_CORE_MODULE) {
continue;
}
module = ngx_modules[i]->ctx;
// 只调用核心的 create_conf 接口
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;
}
}
static void *
ngx_core_module_create_conf(ngx_cycle_t *cycle)
{
........
ccf->daemon = NGX_CONF_UNSET;
ccf->master = NGX_CONF_UNSET;
ccf->timer_resolution = NGX_CONF_UNSET_MSEC;
ccf->worker_processes = NGX_CONF_UNSET;
ccf->debug_points = NGX_CONF_UNSET;
ccf->rlimit_nofile = NGX_CONF_UNSET;
ccf->rlimit_core = NGX_CONF_UNSET;
ccf->user = (ngx_uid_t) NGX_CONF_UNSET_UINT;
ccf->group = (ngx_gid_t) NGX_CONF_UNSET_UINT;
............
return ccf;
}
- 读取配置文件
ngx_conf_param, ngx_conf_parse 读取并生成配置文件块,Nginx 配置分为三类,parse_file、parse_block、parse_param,即文件,语句块,语句参数。ngx_conf_parse 会根据不同模块的 ngx_command_t set 方法来解析设置配置,举个例子,如果遇到 auth_basic 命令,那么就调用 ngx_http_set_complex_value_slot 来设置参数
static ngx_command_t ngx_http_auth_basic_commands[] = {
{ ngx_string("auth_basic"),
NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_HTTP_LMT_CONF
|NGX_CONF_TAKE1,
ngx_http_set_complex_value_slot,
NGX_HTTP_LOC_CONF_OFFSET,
offsetof(ngx_http_auth_basic_loc_conf_t, realm),
NULL },
ngx_null_command
};
然后,调用 init_conf 初始化 core 参数配置
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 的 ngx_core_module_init_conf 函数。
初始化共享内存
共享内存很重要,Nginx 用来做进程交互,数据的共享,比如 limit req, limit zone 模块打开监听的 socket 句柄
ngx_open_listening_sockets、ngx_configure_listening_sockets 用来 bind socket, 并设置额外的参数,比如 SO_REUSEPORT、SO_REUSEADDR、SO_RCVBUF、SO_SNDBUF、SO_KEEPALIVE、TCP_KEEPIDLE、TCP_FASTOPEN、TCP_DEFER_ACCEPT初始化模块 init_module
ngx_event_core_module 模块在此调用 init_module 回调 ngx_event_module_init 函数,开避共享内存用来防止惊群的 accept_mutex, 再初始化一些 counter 用来统计。
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);
}
}
}
- 周边设置
此时退出 ngx_init_cycle 函数,daemon 变成后台执行,并生成 pidfile, 最后根据角色进入不同逻辑,单机模式进入 ngx_single_process_cycle,否则走入正常的 ngx_master_process_cycle 初始化逻辑。
11.启动 worker
ngx_master_process_cycle 调用 ngx_start_worker_processes 启动子进程,启动 ngx_start_cache_manager_processes cache_loader、cache_manager 进程,然后进入 master 工作模式:热加载配置、拉取挂掉的子进程、binary热更新 等工作。
12.子进程登场
ngx_start_worker_processes 根据 worker 个数,fork 相应的进程,并设置 channel 用于父子进程通信。
- 初始化 worker
ngx_worker_process_cycle 调用 ngx_worker_process_init 进行初始化:环境变量、打开最大的文件个数、set sid gid、ngx_get_cpu_affinity绑定 cpu, 最重要的是调用模块的 init_process 回调
for (i = 0; ngx_modules[i]; i++) {
if (ngx_modules[i]->init_process) {
if (ngx_modules[i]->init_process(cycle) == NGX_ERROR) { //ngx_event_process_init等
/* fatal */
exit(2);
}
}
}
调用 ngx_events_module 事件模块的 ngx_event_process_init 方法,功能如下:
- ngx_event_timer_init 初始化红黑树定时器
- 执行 epoll module 的 ngx_epoll_init 方法
- 为 ngx_cycle 的 connections、read_events、write_events 提前分配空间
- 生成 ngx_cycle.free_connections 连接池
- 遍历 ngx_cycle.listenings 监听句柄,设置回调函数为 ngx_event_accept,最后将这个句柄 ngx_add_event 添加到 epoll 中。如果监听到 socket 有新连接,就会调用 ngx_event_accept 创建连接
- worker开始工作
此时 worker 进入死循环,正式进入工作模式,除了获得到 quit 等信号,阻塞在 函数 ngx_process_events_and_timers, 等待用户请求或是超时事件。
小结
这就是 Nginx 的完整启动流程,还有很多细节需要看代码才能清楚。梳理流程,可以深入理解 Nginx 是如何利用模块来组织功能,下一节争取走一遍 Nginx http 流程。