陈克,拥有十年行业经验,曾在浙江电信、阿里巴巴、华为、五霸同城担任开发工程师和架构师。 目前负责合力家的前端架构和运维工作。 博客地址:
图1:nginx启动及显存申请流程分析
任何程序都离不开启动和配置分析。 ngx的代码离不开ngx_cycle_s和ngx_pool_s这两个核心数据结构,所以在开始之前我们先来分析一下。
内存申请过程分为3步
如果请求的显存大于当前块的剩余空间,则直接在当前块中分配。
如果当前块空间不足php守护进程,则调用ngx_palloc_block分配新块,并将新块链接到d.next,然后分配数据。
如果请求的大小小于当前块的最大值,则直接调用ngx_palloc_large分配一个大块并链接到pool→large链表
内存分配过程图如下
(图片来自网络)
为了更好的理解上图,可以参考文末附录2中的几个数据结构:ngx_pool_s和ngx_cycle_s。
知道了这两个核心数据结构之后,我们就要进入main函数了。 main函数的执行流程如下
初始化各个模块的索引并估算ngx_max_module;
调用ngx_init_cycle进行初始化;
如果有信号,则进入ngx_signal_process进行处理;
调用ngx_init_signals初始化信号; 主要完成信号处理程序的注册;
如果没有继承的socket并且设置了守护进程标志,则调用ngx_daemon创建守护进程;
调用ngx_create_pidfile创建进程记录文件; (非NGX_PROCESS_MASTER = 1进程,不要创建此文件)
进入流程主循环;
在main函数执行过程中,有一个非常重要的函数ngx_init_cycle,这个阶段做了什么事情呢? 下面分析ngx_init_cycle,初始化过程:
更新时区和时间
创建内存池
将视频内存分配给循环指针
保存安装路径、配置文件、启动参数等。
初始化打开的文件句柄
初始化共享内存
初始化加入队列
保存主机名
调用每个NGX_CORE_MODULE的create_conf方法
解析配置文件
调用各个NGX_CORE_MODULE的init_conf方法
打开新文件句柄
创建共享内存
手柄攻丝插座
创建用于窃听的套接字
调用各个模块的init_module
图2:主流程工作原理及工作工程
以下流程均在ngx_master_process_cycle函数中进行启动进程:
暂时屏蔽ngx需要处理的所有信号
设置进程名称
启动工作进程
启动缓存管理进程
进入循环开始处理相关信号
主进程工作流程
设置worker进程退出的等待时间
挂断电话,等待新信号到来
更新时间
如果工作进程由于 SIGCHLD 信号而退出,则重新启动工作进程
master进程退出。如果所有worker进程退出并收到SIGTERM信号或SIGINT信号或SIGQUIT信号等,则master进程开始处理退出
处理 SIGTERM 信号
处理SIGQUIT信号并关闭套接字
处理 SIGHUP 信号
平滑升级,重启worker进程
升级不顺利,需要重新读取配置
进程重启 10 进程 SIGUSR1 信号重新打开所有文件 11 进程 SIGUSR2 信号热代码替换,执行新程序 12 进程 SIGWINCH 信号,不再处理任何请求
图3:worker进程的工作原理
首先执行 ngx_start_worker_processes 函数:
首先找到ngx_processes数组中的坑 if (ngx_processes[s].pid == -1) {break;}
流程相关结构初始化工作
创建管道(套接字对)
将管道设置为非阻塞模式
将管道设置为异步模式
设置异步I/O的所有者
如果执行exec,这个fd不会传递给exec创建的进程
fork 创建一个子进程。 创建成功后,子进程执行相关逻辑:proc(cycle, data)。
设置 ngx_processes[s] 相关属性
通知子进程新进程创建了 ngx_pass_open_channel(cycle, &ch);
接下来是ngx_worker_process_cycle工作进程逻辑
ngx_worker_process_init
初始化环境变量
设置进程优先级
设置文件句柄数量限制
设置 core_file 文件
用户组设置
CPU亲和性设置
设置工作目录
设置随机种子数
初始化窃听状态
调用各个模块的init_process方法进行初始化
关闭别人的fd[1],保留别人的fd[1],以便相互通信。 它自己的fd[1]接收来自主进程的消息。
收听频道阅读风暴
工艺模式
处理管道信号。 这个过程是由ngx_channel_handler完成的,这部分的具体实现在pipelinestorm中有解释。
线程模式
ngx_worker_thread_cycle是一个线程循环:不仅在无限循环中处理退出信号。 主要进行ngx_event_thread_process_posted的工作。 这个具体内容我们在讲风暴模型的时候会展开。
处理相关信号
Master和Worker之间的通信原理是:
Nginx事件机制简介
先看几个主要技能
n = 发送消息(s, &msg, 0);
表格顶部
表格底部
接下来分析storm模块的工作流程
ngx_event模块结构
ngx_events_module的数据结构如下:
ngx_module_t ngx_events_module = {
NGX_MODULE_V1,
&ngx_events_module_ctx, /* 模块上下文 */
ngx_events_commands, /* 模块指令 */
NGX_CORE_MODULE, /* 模块类型 */
, /* 初始化主机 */
, /* 初始化模块 */
, /* 初始化进程 */
, /* 初始化线程 */
, /* 退出线程 */
, /* 退出进程 */
, /* 退出主控 */
NGX_MODULE_V1_PADDING
};
ngx_event模块初始化
静态 ngx_command_t ngx_events_commands = {
ngx_string(“事件”) ,
NGX_MAIN_CONF|NGX_CONF_BLOCK|NGX_CONF_NOARGS ,
ngx_events_block, 0, 0,
},
ngx__命令
};
通过ngx_events_commands数组,我们可以知道事件模块初始化函数为ngx_events_block,该函数的工作内容如下:
创建模块上下文结构
为所有 NGX_EVENT_MODULE 模块调用 create_conf
解析事件配置
为所有 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) { /* 致命 */
退出(1);
我们先看一下ngx_core_event_module的结构:
ngx_module_t ngx_event_core_module = {
NGX_MODULE_V1,
&ngx_event_core_module_ctx, /* 模块上下文 */
ngx_event_core_commands, /* 模块指令 */
NGX_EVENT_MODULE, /* 模块类型 */
, /* 初始化主机 */
ngx_event_module_init, /* 初始化模块 */
ngx_event_process_init, /* 初始化进程 */
, /* 退出主控 */ 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) { /*致命 */
退出(2);
ngx_event_process_init 流程分为以下步骤:
设置 ngx_accept_mutex_held
初始化定时器
初始化真正的storm引擎(linux中的epoll)
初始化连接池
添加接受事件
ngx_process_events_and_timers 事件处理开始工作
工作流程如下:
ngx_trylock_accept_mutex只有在获得标志位后才注册接受事件。
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。 在此过程中,会执行几个初始化步骤:
并根据模块号存放到cycle→conf_ctx中。 这个过程主要是初始化配置数据结构。 以epoll模块为例:
该函数总共有以下几个流程:
结构 ngx_conf_s {
字符*名称;
ngx_array_t *args;
ngx_cycle_t *循环;
ngx_pool_t *池;
ngx_pool_t *temp_pool;
ngx_conf_file_t *conf_file;
ngx_log_t *日志;
无效*ctx;
ngx_uint_t 模块类型;
ngx_uint_t cmd_type;
ngx_conf_handler_pt 处理程序;
字符 *handler_conf;
};
rv = ngx_conf_parse(cf, ) ; 初始化http上下文后,继续内部解析逻辑。 只有这样才会调用ngx_conf_handler下半部分的逻辑:
此阶段会根据配置项的值初始化核心模块。 ngx的配置结构如下:
整体结构
serv_conf结构
loc_conf结构
附件1:Nginx主要数据结构
我们可以参考ngx_connection_s结构体,该结构体存储了ngx_connection_s中数组的指针:ngx_queue_t队列
6.ngx_hash_t
ngx的哈希表没有数组,如果找不到,就会继续向右寻找空闲的桶。 ngx_hash_init的整体初始化流程为:
预计需要的桶数
搜索所需的桶数
分配桶显存
初始化每个ngx_hash_elt_t
ngx对于显存有专门的扣除,假设哈希表不会占用太多的数据和空间,所以采用了这种方法。
附录2:内存分配的数据结构
ngx_pool_s是ngx的内存池php守护进程,每个工作线程都会持有一个,我们看一下它的结构:
结构 ngx_pool_s {
ngx_pool_data_t d ; // 数据块
最大尺寸; // 小显存的最大值
ngx_pool_t *当前; // 指向当前内存池
ngx_chain_t *链;
ngx_pool_large_t *大; // 分配大块显存,即显存请求超过max
ngx_pool_cleanup_t *清理; // 当某些内存池被挂载和释放时,资源同时释放
ngx_log_t *日志;
};
ngx_pool_data_t数据结构:
类型定义结构{
u_char *最后; // 当前数据块分配的结束位置
u_char *结束; // 数据块结束位置
ngx_pool_t *下一个; // 链接到下一个内存池
ngx_uint_t 失败; // 统计内存池无法满足分配请求的次数
} ngx_pool_data_t;
那么我们结合ngx_palloc方法来看看内存池的分配原理:
void * ngx_palloc (ngx_pool_t *pool, size_t 大小) {
u_char *m; ngx_pool_t *p;
如果(最大尺寸){
p = 池->当前;
做 {
m = ngx_align_ptr(p->d.last, NGX_ALIGNMENT) ;
if ((size_t) (p->d.end - m) >= size) {
p->d.last = m + 大小;
返回米;
p = p->d.下一个;
而(p);
返回 ngx_palloc_block(池, 大小) ;
返回 ngx_palloc_large(池, 大小) ;
每个工作进程维护一个 ngx_cycle_s:
结构 ngx_cycle_s {
无效****conf_ctx; // 配置上下文列表(包括所有模块)
ngx_pool_t *池; // 内存池
ngx_log_t *日志; // 日志
ngx_log_t new_log;
ngx_connection_t **文件; // 连接文件
ngx_connection_t *自由连接; // 空闲连接
ngx_uint_t free_connection_n ; // 空闲连接数
ngx_queue_t reusable_connections_queue ; // 再次使用连接队列
ngx_array_t 监听; // 监听列表
ngx_array_t 路径; // 路径链表
ngx_list_t open_files ; // 打开文件数组
ngx_list_t 共享内存; // 共享内存链表
ngx_uint_t 连接_n ; // 连接数
ngx_uint_tiles_n ; // 打开文件数
ngx_connection_t *连接; // 连接
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; //配置参数
ngx_str_t conf_prefix; //配置前缀
ngx_str_t 前缀; //字首
ngx_str_t 锁文件; //锁定文件
ngx_str_t 主机名; //主机名
};
附录3:Nginx内存管理和内存对齐
内存申请最终调用malloc函数,ngx_calloc调用ngx_alloc后使用memset填充0。 如果自己开发NGX模块,不要直接使用ngx_malloc/ngx_calloc,可以使用ngx_palloc,否则需要自己管理显存的释放。 ngx_http_create_request期间将创建请求级池:
池 = ngx_create_pool(cscf->request_pool_size, c->log) ;
如果(池==){
返回;
r = ngx_pcalloc(池, sizeof(ngx_http_request_t));
如果(r==){
ngx_destroy_pool(池);
r->池=池;
当 ngx_http_free_request 释放请求时,会调用 ngx_destroy_pool ( pool ) 释放连接。 内存对齐,首先在创建池时对齐:p = ngx_memalign(NGX_POOL_ALIGNMENT, size, log)。 ngx_memalign(根据指定的对齐方式返回大小的显存空间,其地址为对齐方式的整数倍,对齐方式为2的幂。)最后通过:posix_memalign或memalign来申请。
数据对齐(alignment)是指由硬件条件决定的数据的地址与内存块的大小之间的关系。 当变量的地址是其大小的倍数时,称为自然对齐。 例如,对于一个32位变量,如果它的地址是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字节的动态显存,该显存的地址是对齐的倍数。 对齐参数必须是 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二级缓存。