编者按:高可用架构分享及传播在架构领域具有典型意义的文章,本文由陈科在高可用架构群分享。转载请注明来自高可用架构公众号「ArchNotes」。
导读:很多工程师及架构师都希望了解及掌握高性能服务器开发,阅读优秀源代码是一种有效的方式,nginx 是业界知名的高性能 Web 服务器实现,如何有效的阅读及理解 nginx?本文用图解的方式帮助大家来更好的阅读及理解 nginx 关键环节的实现。
陈科,十年行业从业经验,曾在浙江电信、阿里巴巴、华为、五八同城任开发工程及架构师等职,目前负责河狸家后端架构和运维。博客地址:http://www.dumpcache.com/wiki/doku.php
图一:nginx 启动及内存申请过程分析
任何程序都离不开启动和配置解析。ngx 的代码离不开 ngx_cycle_s 和 ngx_pool_s 这两个核心数据结构,所以我们在启动之前先来分析下。
内存申请过程分为 3 步
-
假如申请的内存小于当前块剩余的空间,则直接在当前块中分配。
-
假如当前块空间不足,则调用 ngx_palloc_block 分配一个新块然后把新块链接到 d.next 中,然后分配数据。
-
假如申请的大小大于当前块的最大值,则直接调用 ngx_palloc_large 分配一个大块,并且链接到 pool→large 链表中
内存分配过程图解如下
(图片来自网络)
为了更好理解上面的图,可以参看文末附 2 的几个数据结构:ngx_pool_s 及 ngx_cycle_s。
知道了这两个核心数据结构之后,我们正式进入 main 函数,main 函数执行过程如下
-
调用 ngx_get_options() 解析命令参数;
-
调用 ngx_time_init() 初始化并更新时间,如全局变量ngx_cached_time;
-
调用 ngx_log_init() 初始化日志,如初始化全局变量 ngx_prefix,打开日志文件 ngx_log_file.fd;
-
清零全局变量 ngx_cycle,并为 ngx_cycle.pool 创建大小为 1024B 的内存池;
-
调用 ngx_save_argv() 保存命令行参数至全局变量 ngx_os_argv、ngx_argc、ngx_argv 中;
-
调用 ngx_process_options() 初始化 ngx_cycle 的 prefix, conf_prefix, conf_file, conf_param 等字段;
-
调用 ngx_os_init() 初始化系统相关变量,如内存页面大小 ngx_pagesize , ngx_cacheline_size , 最大连接数 ngx_max_sockets 等;
-
调用 ngx_crc32_table_init() 初始化 CRC 表 ( 后续的 CRC 校验通过查表进行,效率高 );
-
调用 ngx_add_inherited_sockets() 继承 sockets:
-
解析环境变量 NGINX_VAR = "NGINX" 中的 sockets,并保存至 ngx_cycle.listening 数组;
-
设置 ngx_inherited = 1;
-
调用 ngx_set_inherited_sockets() 逐一对 ngx_cycle.listening 数组中的 sockets 进行设置;
-
初始化每个 module 的 index,并计算 ngx_max_module;
-
调用 ngx_init_cycle() 进行初始化;
-
该初始化主要对 ngx_cycle 结构进行;
-
若有信号,则进入 ngx_signal_process() 处理;
-
调用 ngx_init_signals() 初始化信号;主要完成信号处理程序的注册;
-
若无继承 sockets,且设置了守护进程标识,则调用 ngx_daemon() 创建守护进程;
-
调用 ngx_create_pidfile() 创建进程记录文件;( 非 NGX_PROCESS_MASTER = 1 进程,不创建该文件 )
-
进入进程主循环;
-
若为 NGX_PROCESS_SINGLE=1模式,则调用 ngx_single_process_cycle() 进入进程循环;
-
否则为 master-worker 模式,调用 ngx_master_process_cycle() 进入进程循环;
在 main 函数执行过程中,有一个非常重要的函数 ngx_init_cycle,这个阶段做了什么呢?下面分析 ngx_init_cycle,初始化过程:
-
更新 timezone 和 time
-
创建内存池
-
给 cycle 指针分配内存
-
保存安装路径,配置文件,启动参数等
-
初始化打开文件句柄
-
初始化共享内存
-
初始化连接队列
-
保存 hostname
-
调用各 NGX_CORE_MODULE 的 create_conf 方法
-
解析配置文件
-
调用各NGX_CORE_MODULE的init_conf方法
-
打开新的文件句柄
-
创建共享内存
-
处理监听socket
-
创建socket进行监听
-
调用各模块的init_module
图二:master 进程工作原理及工作工程
以下过程都在ngx_master_process_cycle 函数中进行,启动过程:
-
暂时阻塞所有 ngx 需要处理的信号
-
设置进程名称
-
启动工作进程
-
启动cache管理进程
-
进入循环开始处理相关信号
master 进程工作过程
-
设置 work 进程退出等待时间
-
挂起,等待新的信号来临
-
更新时间
-
如果有 worker 进程因为 SIGCHLD 信号退出了,则重启 worker 进程
-
master 进程退出。如果所有 worker 进程都退出了,并且收到 SIGTERM 信号或 SIGINT 信号或 SIGQUIT 信号等,master 进程开始处理退出
-
处理SIGTERM信号
-
处理SIGQUIT信号,并且关闭socket
-
处理SIGHUP信号
-
平滑升级,重启worker进程
-
不是平滑升级,需要重新读取配置
-
处理重启 10处理SIGUSR1信号 重新打开所有文件 11处理SIGUSR2信号 热代码替换,执行新的程序 12处理SIGWINCH信号,不再处理任何请求
图三:worker 进程工作原理
启动通过执行 ngx_start_worker_processes 函数:
-
先在 ngx_processes 数组中找坑位if (ngx_processes[s].pid == -1) {break;}
-
进程相关结构初始化工作
-
创建管道 ( socketpair )
-
设置管道为非阻塞模式
-
设置管道为异步模式
-
设置异步 I/O 的所有者
-
如果 exec 执行的时候本 fd 不传递给 exec 创建的进程
-
fork 创建子进程。创建成功后,子进程执行相关逻辑:proc(cycle, data)。
-
设置 ngx_processes[s] 相关属性
-
通知子进程新进程创建完毕 ngx_pass_open_channel(cycle, &ch);
接下来是 ngx_worker_process_cycle worker 进程逻辑
-
ngx_worker_process_init
-
初始化环境变量
-
设置进程优先级
-
设置文件句柄数量限制
-
设置 core_file 文件
-
用户组设置
-
cpu 亲和度设置
-
设定工作目录
-
设置随机种子数
-
初始化监听状态
-
调用各模块的init_process方法进行初始化
-
关闭别人的fd[1],保留别人的fd[1]用于互相通信。自己的fd[1]接收master进程的消息。
-
监听channel读事件
-
进程模式
-
处理管道信号。这个过程由 ngx_channel_handler 完成,这部分具体实现在管道事件中讲解。
-
线程模式
-
ngx_worker_thread_cycle 是一个线程的循环:死循环中除了处理退出信号。主要进行ngx_event_thread_process_posted工作,这块具体内容在后面讲事件模型的时候再展开。
-
处理相关信号
master 和 worker 通信原理为:
Nginx 事件机制介绍
先看几个主要方法
-
ngx_add_channel_event 主要是把事件注册到事件池中,并且添加事件 handler,具体要结合后面的事件机制来展开。
-
ngx_write_channel 主要是将数据写入到 pipe 中:
n = sendmsg(s, &msg, 0);
Top of Form
Bottom of Form
-
ngx_read_channel 从 pipe 中读取数据:n = recvmsg(s, &msg, 0);
接下来分析事件模块工作流程
ngx_event模块结构
ngx_events_module 的数据结构如下:
ngx_module_t ngx_events_module = {
NGX_MODULE_V1,
&ngx_events_module_ctx, /* module context */
ngx_events_commands, /* module directives */
NGX_CORE_MODULE, /* module type */
NULL, /* init master */
NULL, /* init module */
NULL, /* init process */
NULL, /* init thread */
NULL, /* exit thread */
NULL, /* exit process */
NULL, /* exit master */
NGX_MODULE_V1_PADDING
};
ngx_event 模块初始化
static ngx_command_t ngx_events_commands[] = {
{
ngx_string("events") ,
NGX_MAIN_CONF|NGX_CONF_BLOCK|NGX_CONF_NOARGS ,
ngx_events_block, 0, 0, NULL
},
ngx_null_command
};
通过 ngx_events_commands 数组可以知道,event 模块初始化函数为 ngx_events_block,该函数工作内容如下:
-
创建模块 context 结构
-
调用所有 NGX_EVENT_MODULE 模块的 create_conf
-
解析 event 配置
-
调用所有 NGX_EVENT_MODULE 模块的 init_conf
ngx_core_event模块初始化
ngx_core_event_module 是在 ngx_cycle_init 的时候初始化的:
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_core_event_module 的结构:
ngx_module_t ngx_event_core_module = {
NGX_MODULE_V1,
&ngx_event_core_module_ctx, /* module context */
ngx_event_core_commands, /* module directives */
NGX_EVENT_MODULE, /* module type */
NULL, /* init master */
ngx_event_module_init, /* init module */
ngx_event_process_init, /* init process */
NULL, /* init thread */
NULL, /* exit thread */
NULL, /* exit process */
NULL, /* exit master */ NGX_MODULE_V1_PADDING
};
ngx_event_module_init 实现了初始化过程,该过程分以下几个步骤:
-
连接数校验
-
初始化互斥锁
事件进程初始化
在工作线程初始化的时候,将会调用 ngx_event_process_init:
for (i = 0; ngx_modules[i]; i++) {
if (ngx_modules[i]->init_process) {
if (ngx_modules[i]->init_process(cycle) == NGX_ERROR) { /*fatal */
exit(2);
}
}
}
ngx_event_process_init 该过程分以下几步:
-
设置 ngx_accept_mutex_held
-
初始化定时器
-
初始化真正的事件引擎(linux 中为 epoll)
-
初始化连接池
-
添加 accept 事件
ngx_process_events_and_timers 事件处理开始工作
工作流程如下:
-
ngx_trylock_accept_mutex 当获取到标志位后才进行 accept 事件注册。
-
ngx_process_events 处理事件
-
释放 accept_mutex 锁
-
处理定时器事件
-
ngx_event_process_posted 处理 posted 队列的事件
ngx 定时器实现
ngx 的定时器利用了红黑树的实现
ngx 惊群处理
accept_mutex 解决了惊群问题,虽然linux的新内核已经解决了这个问题,但是ngx 是为了兼容。
整体原理图:
Nginx 配置解析
再补充一下配置解析,Nginx 配置解析最大的亮点是用一个三级指针和 ctx 关联了起来,然后每个模块关注各自的配置专注解析和初始化就行了。
配置文件解析
ngx 在 main 函数执行的时候会调用 ngx_init_cycle,在这个过程中,会进行初始化的几个步骤:
-
create_conf 针对 core_module 类型的模块,将会调用 create_conf 方法:
并且把根据模块号存入了 cycle→conf_ctx 中。这个过程主要是进行配置数据结构的初始化。以epoll模块为例:
-
ngx_conf_parse 解析配置文件
这个函数一共有以下几个过程:
-
ngx_conf_read_token 这个过程主要进行配置配置的解析工作,解析完成的一个配置结构为:
struct ngx_conf_s {
char *name;
ngx_array_t *args;
ngx_cycle_t *cycle;
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;
ngx_conf_handler_pt handler;
char *handler_conf;
};
-
ngx_conf_handler 进行配置的处理
-
cmd→set,以 ngx_http 模块为例
rv = ngx_conf_parse(cf, NULL) ; 在初始化完 http 的上下文之后,继续进行内部的解析逻辑。这样就会调用到 ngx_conf_handler 的下面部分逻辑:
-
init_conf阶段
core 模块将会按照配置项的值在这个阶段进行初始化。ngx 的配置架构如下:
整体架构
serv_conf 结构
loc_conf 结构
附1:Nginx 主要数据结构
我们可以参考 ngx_connection_s 结构体,在 ngx_connection_s 中保存了链表的指针:ngx_queue_t queue
6 . ngx_hash_t
ngx 的 hash 表没有链表,如果找不到则往右继续查找空闲的 bucket。总的初始化 ngx_hash_init 流程即为:
-
预估需要的桶数量
-
搜索需要的桶数量
-
分配桶内存
-
初始化每一个 ngx_hash_elt_t
ngx 对内存非常扣,假设了 hash 表不会占用太多的数据和空间,所以采用了这样的方式。
附2:内存分配的数据结构
ngx_pool_s 是 ngx 的内存池,每个工作线程都会持有一个,我们来看它的结构:
struct ngx_pool_s {
ngx_pool_data_t d ; // 数据块
size_t max ; // 小块内存的最大值
ngx_pool_t *current ; // 指向当前内存池
ngx_chain_t *chain ;
ngx_pool_large_t *large; // 分配大块内存用,即超过max的内存请求
ngx_pool_cleanup_t *cleanup ; // 挂载一些内存池释放的时候,同时释放的资源
ngx_log_t *log;
} ;
ngx_pool_data_t 数据结构:
typedef struct {
u_char *last ; // 当前数据块分配结束位置
u_char *end ; // 数据块结束位置
ngx_pool_t *next ; // 链接到下一个内存池
ngx_uint_t failed ; // 统计该内存池不能满足分配请求的次数
} ngx_pool_data_t ;
然后我们结合 ngx_palloc 方法来看一下内存池的分配原理:
void * ngx_palloc (ngx_pool_t *pool, size_t size) {
u_char *m; ngx_pool_t *p ;
if (size <= pool->max) {
p = pool->current ;
do {
m = ngx_align_ptr(p->d.last, NGX_ALIGNMENT) ;
if ((size_t) (p->d.end - m) >= size) {
p->d.last = m + size ;
return m ;
}
p = p->d.next ;
} while (p) ;
return ngx_palloc_block(pool, size) ;
}
return ngx_palloc_large(pool, size) ;
}
ngx_cycle_s 每个工作进程都会维护一个:
struct ngx_cycle_s {
void ****conf_ctx ; // 配置上下文数组(含所有模块)
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_array_t listening ; // 监听数组
ngx_array_t pathes ; // 路径数组
ngx_list_t open_files ; // 打开文件链表
ngx_list_t shared_memory ; // 共享内存链表
ngx_uint_t connection_n ; // 连接个数
ngx_uint_t iles_n ; // 打开文件个数
ngx_connection_t *connections ; // 连接
ngx_event_t *read_events ; // 读事件
ngx_event_t *write_events ; // 写事件
ngx_cycle_t *old_cycle; //old 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; //主机名
};
附3:Nginx 内存管理 & 内存对齐
内存的申请最终调用的是 malloc 函数,ngx_calloc 则在调用 ngx_alloc 后,使用 memset 来填 0。假如自己开发NGX模块,不要直接使用 ngx_malloc/ngx_calloc,可以使用 ngx_palloc 否则还需要自己管理内存的释放。在 ngx_http_create_request 的时候会创建 request 级别的 pool:
pool = ngx_create_pool(cscf->request_pool_size, c->log) ;
if (pool == NULL) {
return NULL;
}
r = ngx_pcalloc(pool, sizeof(ngx_http_request_t));
if (r == NULL) {
ngx_destroy_pool(pool) ;
return NULL ;
}
r->pool = pool ;
在 ngx_http_free_request 释放 request 的时候会调用 ngx_destroy_pool ( pool ) 释放连接。内存对齐,首先在创建 pool 的时候对齐:p = ngx_memalign(NGX_POOL_ALIGNMENT, size, log) 。ngx_memalign(返回基于一个指定 alignment 的大小为 size 的内存空间,且其地址为 alignment 的整数倍,alignment 为 2 的幂。)最终通过:posix_memalign 或 memalign 来申请。
数 据的对齐 ( alignment ) 是指数据的地址和由硬件条件决定的内存块大小之间的关系。一个变量的地址是它大小的倍数的时候,这就叫做自然对齐 ( naturally aligned )。例如,对于一个 32bit 的变量,如果它的地址是 4 的倍数,-- 就是说,如果地址的低两位是 0,那么这就是自然对齐了。所以,如果一个类型的大小是 2n 个字节,那么它的地址中,至少低 n 位是 0。对齐的规则是由硬件引起 的。一些体系的计算机在数据对齐这方面有着很严格的要求。在一些系统上,一个不对齐的数据的载入可能会引起进程的陷入。在另外一些系统,对不对齐的数据的 访问是安全的,但却会引起性能的下降。在编写可移植的代码的时候,对齐的问题是必须避免的,所有的类型都该自然对齐。
预对齐内存的分配在大多数情况下,编译器和 C 库透明地帮你处理对齐问题。POSIX 标明了通过 malloc( ), calloc( ), 和 realloc( ) 返回的地址对于任何的C类型来说都是对齐的。在 Linux 中,这些函数返回的地址在 32 位系统是以 8 字节为边界对齐,在 64 位系统是以 16 字节为边界对齐 的。有时候,对于更大的边界,例如页面,程序员需要动态的对齐。虽然动机是多种多样的,但最常见的是直接块 I/O 的缓存的对齐或者其它的软件对硬件的交 互,因此,POSIX 1003.1d 提供一个叫做 posix_memalign( ) 的函数。
调用 posix_memalign( ) 成功时会返回 size 字节的动态内存,并且这块内存的地址是 alignment 的倍数。参数 alignment 必须是 2 的幂,还是 void 指针的大小的倍数。返回的内存块的地址放在了 memptr 里面,函数返回值是 0.
指针对齐:#define ngx_align_ptr(p, a) (u_char *) (((uintptr_t) (p) + ((uintptr_t) a - 1)) & ~((uintptr_t) a - 1))
例如:计算宏 ngx_align (1, 64) = 64,只要输入d < 64,则结果总是 64,如果输入 d = 65,则结果为 128,以此类推。
进行内存池管理的时候,对于小于64字节的内存,给分配64字节,使之总是cpu二级缓存读写行的大小倍数,从而有利cpu二级缓存取速度和效率。
由于公众号文章篇幅关系,以上就是陈科分享的 nginx 源码分析前半部分,关注本公众号可收到后半部分内容。