nginx源码阅读分析(•̀⌄•́)
—— By Jihan
本人主要使用的nginx源码是nginx-1.0.15,以降低阅读难度,毕竟需要了解的是大体框架和原理。最新的版本,代码量太大,会增加阅读理解难度,没有必要。而本文的阅读方法,也按照基本使用->简单示例->示例在源码中如何工作->相关联版块实现逻辑->结构化整理nginx逻辑框架的方法来写文档。其核心思路围绕:什么功能->功能如何实现来进行分析,反复循环改过程以理解整个nginx源码。最后会提出一个实际问题,通过阅读源码后来提出相应的解决方案。
有必要可以增加一个如何编写nginx的module的文章,单开篇,就不在这里写了。
序
首先你必须要有一定的C功底,linux功底,以及初步了解过nginx最好也初步使用过,本文对于这几项会几乎略过,当然会给出一个基本使用示例。
由于本人也是才工作不久的新人,初次阅读源码,菜是自然的,有问题的地方,欢迎讨论。
其他:本文的客户端(浏览器)在windows上,使用Chrome;服务器在linux(centos 7.6)上,其他平台的编译执行请自行处理。
nginx应用示例
我们第一步很简单,先把它用起来。这里我们给出两个nginx的经典应用场景,1. 作为web服务器,即在服务器放一个html文件,然后浏览器通过nginx访问该html的网页。2. 作为反向代理,即在服务器A上安装一个nginx做反向代理到服务器B,客户端通过服务器A访问服务器B资源。我们先配置场景1
在此之前,先干下面的事情:
- 下载nginx源码
- 编译
- 修改配置文件
- 执行nginx,并测试场景1成功
源码下载编译
更加详细的编译和配置可以参考:最全Nginx 配置文件详解及安装
下载:nginx-1.0.15
编译:
1 | wget http://nginx.org/download/nginx-1.0.15.tar.gz |
不出意外就成功了,如果有错误,自行解决。相应的可执行程序在objs/nginx
配置场景
环境准备
我们安装的时候,把可执行文件install
到了一个新目录:nginx_learning
不出意外,结构如下:
1 | # tree |
修改配置文件
上述完成后,我们进行场景1和2的配置文件修改,得到nginx.conf
如下:
1 | #user nobody; |
配置文件的root
配置根据个人情况修改。
注意:www
目录位置,注意权限问题,详情可查看日志logs/error.log
。权限问题处理方式可参考:Nginx 403 forbidden for all files,翻不了墙的同学,自行百度
构建资源
然后我们在www
目录下新建一个index.html
文件,作为场景1的服务端资源:
index.html:
1 | <h1> Hello nginx </h1> |
nohub python3 -m http.server --bind 127.0.0.1 8000 &
1 | 可以本地curl一下以验证启动效果: |
1 | 最后我们启动nginx: |
nginx_learning/nginx -p /<your_path>/nginx_learning/
1 | #### 测试效果 |
ngx_int_t i;
ngx_log_t *log; //猜测用于日志记录
ngx_cycle_t *cycle, init_cycle; //猜测是个贯穿整个流程重要的数据结构,里面包含的东西很多,暂时不管
ngx_core_conf_t *ccf;//猜测配置相关内容,细节再说
1 | 可以大致看一看,猜测一下它们作用。大致看一下各个数据结构类型。我这里好奇它的[命名习惯](#nginx中的命名习惯),因此初步进行梳理。`ngx_cycle_t`和`ngx_core_conf_t`结构有些多,后面梳理(你也可以提前看看[nginx中数据结构](#nginx中数据结构))。 |
2. `ngx_conf_handler`用于将命令行参数载入扩展module。TODO细节
4. `for (i = 0; ngx_modules[i]; i++)` 赋值相应的配置变量给扩展的module
5. `ngx_create_pathes`创建文件夹路径
6. `part = &cycle->open_files.part; file = part->elts; for (i = 0; /* void */ ; i++) {` 打开要用到的文件。
7. `part = &cycle->shared_memory.part; shm_zone = part->elts;`创建共享内存
8. `if (old_cycle->listening.nelts)`赋值相关监听所需值
9. `ngx_open_listening_sockets`启动监听
10. 释放一些不用的内存,打开的文件,以及socket
11. `failed`出错处理,配置回滚。
-
ngx_signal_process
(可先跳过) -
ngx_init_signals
注册信号量的回调函数,信号量和回调函数的关联关系在signals
全局量中。控制相关标记位来间接控制master进程中的动作 -
ngx_os_status
打印系统状态 -
获取配置:
1
ccf = (ngx_core_conf_t *) ngx_get_conf(cycle->conf_ctx, ngx_core_module);
-
标识是否是master进程:
1
2
3if (ccf->master && ngx_process == NGX_PROCESS_SINGLE) {
ngx_process = NGX_PROCESS_MASTER;
} -
ngx_daemon
fork进程,具体操作后续继续阅读 -
ngx_create_pidfile
创建pidfile -
创建worker进程或者master进程
1
2
3
4
5
6if (ngx_process == NGX_PROCESS_SINGLE) {
ngx_single_process_cycle(cycle);
} else {
ngx_master_process_cycle(cycle);
}
结构流程:
从上面的函数大概分析,我们基本能够梳理出整个main函数的逻辑:
flowchart TD A(初始化errnum) B(解析命令行传入参数) C1(时间格式,SSL,内存池初始化) C2(获取系统的一些信息) C3(其他一些不影响的初始化) D1(读取解析额外配置) D2(读取解析配置文件) D3(赋值配置到module) D4(创建文件夹,打开需要使用的文件) D5(创建共享内存) D6(启动监听) D7(释放多余内存,文件,状态) D8(错误处理和配置回滚) E{标识是否为master} F1(创建子进程) F2(创建主进程) subgraph C [其他初始化] C1 --> C2 C2 --> C3 end subgraph D [初始化cycle] D1 --> D2 D2 --> D3 D3 --> D4 D4 --OK--> D5 D5 --OK--> D6 D6 --OK--> D7 D7 --> D8 D6 --failed-->D8 D5 --failed-->D8 D4 --failed-->D8 end A --> B B --> C C --> D D -->E E --否--> F1 E --是--> F2
从上述流程图,基本可以了解到主函数所做的工作。接下来就开始梳理master进程和worker进程分别做了什么工作。
master进程
- 注册许多的信号量,并设置成阻塞,延后处理。sigprocmask函数讲解
- 设置进程标记
ngx_start_worker_processes
控制子进程启动(这里的fork干嘛用的?)for ngx_get_cpu_affinity
循环子进程个数ngx_spawn_process
创建和子进程通信的套接字。ngx_worker_process_cycle
回调函数,具体作用是解析收到的消息,放到状态机里处理。消息从何而来?未知,主要是fork函数作用?
ngx_pass_open_channel
往套接字里发送消息,消息转发给子进程。
ngx_start_cache_manager_processes
, 其抽象作用和ngx_start_worker_processes
差不多,都是发送消息给子进程,只是功能作用上是进行cache的管理启动。主要调用的也是下面两个函数:ngx_spawn_process
内容是:‘cache manager process’ngx_pass_open_channel
- 主进程的主要
for ( ;; )
- 是否进行delay
- 使用
setitimer
控制进程延时
- 使用
sigsuspend
恢复之前的sigprocmask阻塞信号的处理- 根据接收到的信号(用户或系统发送),修改标记位来控制相应的子进程,给子进程发送相应的信号,或者套接字消息(类似上面的ngx_start_worker_processes的方式)。
这里我们会疑惑,哪里修改的标记位?哪里接受处理的信号。全局搜索一下可以找到:设置接收信号的回调函数在main
中的ngx_init_signals
函数中完成,根据对应的信号量调用ngx_signal_handler
函数来修改相应标记位。
- 是否进行delay
worker进程
- 设置环境变量,是nginx中的全局变量
environ
- 执行
ngx_modules
中的init_process
函数,也就是利用nginx中模块注册机制。 - 主
for ( ;; )
循环。-
ngx_process_events_and_timers
非常核心的函数,包含了事件处理,和延时处理。nginx
中所有事务都是围绕这个函数中事件处理来完成的。也就是利用了状态机的机制,每次事件的触发,执行触发的相应事件,就是在该函数中完成。- timer初始化,如果ngx开了线程处理方式,则timer初始化和处理有所差异。
ngx_process_events
核心函数。define为ngx_event_actions.process_events
,ngx_event_actions
是一个全局变量,这个全局变量可以注册不同的事件触发器模块(只能注册一个事件触发器,注册方式和普通模块注册一致),比如你使用epoll
做事件触发器,那么就使用ngx_epool_module
中的事件触发器的process_events
函数。而所有的事件都在ngx_event.h
中有定义:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17typedef struct {
ngx_int_t (*add)(ngx_event_t *ev, ngx_int_t event, ngx_uint_t flags);
ngx_int_t (*del)(ngx_event_t *ev, ngx_int_t event, ngx_uint_t flags);
ngx_int_t (*enable)(ngx_event_t *ev, ngx_int_t event, ngx_uint_t flags);
ngx_int_t (*disable)(ngx_event_t *ev, ngx_int_t event, ngx_uint_t flags);
ngx_int_t (*add_conn)(ngx_connection_t *c);
ngx_int_t (*del_conn)(ngx_connection_t *c, ngx_uint_t flags);
ngx_int_t (*process_changes)(ngx_cycle_t *cycle, ngx_uint_t nowait);
ngx_int_t (*process_events)(ngx_cycle_t *cycle, ngx_msec_t timer,
ngx_uint_t flags);
ngx_int_t (*init)(ngx_cycle_t *cycle, ngx_msec_t timer);
void (*done)(ngx_cycle_t *cycle);
} ngx_event_actions_t;这里肯定有一些列的事件轮转机制,需要绘图列出TODO
3. 执行相应延时。
4.posted_events
相应的事件还不清楚干啥用的TODO -
处理接收到的系统信号或者主进程信号:
ngx_terminate
,ngx_quit
,ngx_reconfigure
或ngx_reopen
。这里面的所代表的功能都很好理解,细节需要的时候再看。
-
nginx架构
从上面的代码分析,我们基本上可以得到大致的结论:
- nginx有多个进程,分别为master进程和work进程(这个从nginx的实际运行情况也可以看到)。
- nginx通过环境变量和配置文件来进行管理。
- master的工作是对work进程进行管理和控制,work进程进行实际的工作。
因此我们能得到以下的结构图(图片来源):
场景1的执行流程
当nginx启动后,master进程就一直在主for循环里等待接收信号量,来进行work和自身的控制。而work进程则在主for循环中由ngx_process_events_and_timers
函数来处理事件,或处理接收到的信号量。
那么场景1的触发流程入口,肯定在ngx_process_events_and_timers
函数中的process_events
函数触发,而这个函数是个全局函数指针,通过注册生效。而注册的地方我们在编译完成后可以发现一个obj/ngx_modules.c
的文件,改文件通过configure
生成,通过你选择的编译选择项来生成模块注册文件。那么我们先研究一下nginx的模块注册是如何工作的。
nginx模块注册机制
注册入口
ngx_modules.c
用于控制nginx模块注册,模块注册的统一入口。根据编译时的configure
参数生成。其文件结构如下:
1 |
|
其核心注册机制就是通过配置文件,选择生成全局变量ngx_modules
, nginx主体程序读取该全局变量的值来执行相应模块。
模块入口
通过注册机制可知,主程序与模块直接的交互只能通过ngx_module_t
这个结构,也就是说ngx_module_t
是模块提供的接口。ngx_module_t
结构如下(先撇一眼,在后续分析):
1 | typedef struct ngx_module_s ngx_module_t; |
我们现在已经知道如何进行模块注册,主程序和模块之间的交互接口。对于主程序来说,它并不知道模块具体功能是什么,只知道ngx_module_t
这个结构,和ngx_modules
全局量。那么现在我们就需要分析,ngx_modules
在什么时候使用(模块实际生效位置)以及ngx_module_t
里每个变量功能(模块生效干了啥)。即模块生效位置和功能。
模块生效位置
我们跟着启动顺序进行梳理,寻找ngx_modules
使用的位置。注册模块分为两类,一是核心模块(NGX_CORE_MODULE),二是配置模块(NGX_CONF_MODULE)。
-
main函数,进行
ngx_module_t.index
初始化:1
2
3
4ngx_max_module = 0;
for (i = 0; ngx_modules[i]; i++) {
ngx_modules[i]->index = ngx_max_module++;
} -
在
ngx_init_cycle
中执行注册的核心模块配置初始化(module->create_conf
)。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17for (i = 0; ngx_modules[i]; i++) {
if (ngx_modules[i]->type != NGX_CORE_MODULE) {
continue;
}
//`ngx_module_t.ctx`强转`ngx_core_module_t`
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;
}
} -
在配置文件解析中,读取注册的配置模块,并用于处理配置文件参数(具体配置方法后面细化)。
1
2
3
4
5if (ngx_modules[i]->type != NGX_CONF_MODULE
&& ngx_modules[i]->type != cf->module_type)
{
continue;
} -
注册的核心模块初始化,类似上述第二点:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17for (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;
}
}
} -
执行注册模块各自的
init_module
1
2
3
4
5
6
7
8for (i = 0; ngx_modules[i]; i++) {
if (ngx_modules[i]->init_module) {
if (ngx_modules[i]->init_module(cycle) != NGX_OK) {
/* fatal */
exit(1);
}
}
} -
work里
ngx_single_process_cycle
中会执行模块的init_process
和exit_process
:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15for (i = 0; ngx_modules[i]; i++) {
if (ngx_modules[i]->init_process) {
if (ngx_modules[i]->init_process(cycle) == NGX_ERROR) {
/* fatal */
exit(2);
}
}
}
if (ngx_terminate || ngx_quit) {
for (i = 0; ngx_modules[i]; i++) {
if (ngx_modules[i]->exit_process) {
ngx_modules[i]->exit_process(cycle);
}
}
} -
master的
ngx_master_process_cycle
函数的子函数,ngx_master_process_exit
会调用到对应的函数,1
2
3
4
5for (i = 0; ngx_modules[i]; i++) {
if (ngx_modules[i]->exit_master) {
ngx_modules[i]->exit_master(cycle);
}
}
上面的1-5几乎全是模块配置初始化相关的内容,init_process
和exit_process
是work进程中调用的模块初始化和退出。而init_master
和exit_master
则是在master进程中进行初始化和退出的。但这里还是有一个问题,模块真正处理请求的函数时如何和work程序挂钩的?
nginx事件处理流程
这里要弄清楚事件处理流程,有两种方式:
-
从必调的init_process
入手,阅读一个注册模块的init_process
函数,看实现了什么。- 首先从一个熟悉的
ngx_http_module
入手,看init_process
做了什么。找到定义的ngx_http_module
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20static ngx_core_module_t ngx_http_module_ctx = {
ngx_string("http"),
NULL,
NULL
};
ngx_module_t ngx_http_module = {
NGX_MODULE_V1,
&ngx_http_module_ctx, /* module context */
ngx_http_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
};一看,麻了麻了,这啥也没有啊。
init_process
函数为空,ngx_http_module_ctx
中的create_conf
和init_conf
也为空。
然后一连看了多个http的模块,发现init_process
都为空。看来这种方法不行了。 - 首先从一个熟悉的
-
从
work
进程入手,看谁注册了ngx_event_actions
时间处理函数,然后阅读该模块的实现机制。- 在linux下,默认使用的是
epoll
, 对应注册的ngx_event_actions
如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19ngx_event_module_t ngx_poll_module_ctx = {
&poll_name,
NULL, /* create configuration */
ngx_poll_init_conf, /* init configuration */
{
ngx_poll_add_event, /* add an event */
ngx_poll_del_event, /* delete an event */
ngx_poll_add_event, /* enable an event */
ngx_poll_del_event, /* disable an event */
NULL, /* add an connection */
NULL, /* delete an connection */
NULL, /* process the changes */
ngx_poll_process_events, /* process the events */
ngx_poll_init, /* init the events */
ngx_poll_done /* done the events */
}
};work进程中调用的事件函数
(void) ngx_process_events(cycle, timer, flags);
对应着epoll
中的ngx_poll_process_events
函数。
那么我们可以得到初步的结论:- nginx的模块依赖是一层层递进的:
- 核心模块
- 事件处理模块
- 业务处理模块
ngx_module_t
负责模块注册时的初始化。比如各种配置相关处理,执行业务前的初始化等。ngx_event_module_t
则是负责进行业务注册,也就是当事件触发后,我应当执行哪些业务。而业务注册则是通过ngx_event_actions_t.add*
函数实现。
- 在linux下,默认使用的是
结合上述两点,可以初步得到模块注册和事件处理的流程:
-
在
ngx_event_core_module
注册模块(编译时控制),生成ngx_modules.c
文件,包含全局变量ngx_modules
-
nginx启动时执行相关初始化,执行
ngx_modules
中init_conf
等。 -
启动nginx的work进程,执行
ngx_modules
中init_process
。其中包含ngx_event_core_module.ngx_event_process_init
-
ngx_event_process_init
又以同样的原理,进行event事件的注册
:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19for (m = 0; ngx_modules[m]; m++) {
if (ngx_modules[m]->type != NGX_EVENT_MODULE) {
continue;
}
if (ngx_modules[m]->ctx_index != ecf->use) {
continue;
}
//ngx_event_module_t *module;
module = ngx_modules[m]->ctx;
if (module->actions.init(cycle, ngx_timer_resolution) != NGX_OK) {
/* fatal */
exit(2);
}
break;
} -
ngx_event_process_init
注册的事件中就有ngx_epoll_module
模块(ngx_modules.c),可以说ngx_epoll_module
是真正处理事件的地方。ngx_event_process_init
中调用的module->actions.init
在这里本质就是ngx_epoll_init
-
ngx_epoll_init
中进行了ngx_event_actions
事件的注册:1
ngx_event_actions = ngx_epoll_module_ctx.actions;
-
nginx的work进程处理事件实际调用的是
ngx_event_actions.process_events
,那么就等同于ngx_epoll_module
中的ngx_epoll_process_events
函数。 -
ngx_epoll_process_events
中调用epoll_wait
来接收事件,接收到的事件通过event_list[i].data.ptr
用户自定义指针来进行处理。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20//只列出核心步骤,省略其他代码
//使用epoll的异步io来获取事件
events = epoll_wait(ep, event_list, (int) nevents, timer);
for (i = 0; i < events; i++) {
//读取触发的事件,触发的事件是之前通过epoll_ctl注册到ep中的。
revents = event_list[i].events;
//用户自定义的结构体指针,这里是nginx中的ngx_connection_s结构
c = event_list[i].data.ptr;
//读取数据
rev = c->read;
if ((revents & EPOLLIN) && rev->active) {
rev->handler(rev);
}
//写入数据
wev = c->write;
if ((revents & EPOLLOUT) && wev->active) {
wev->handler(wev);
}
} -
epoll事件处理通过调用
event_list[i].data.ptr
指针来指向具体实现进行处理。而事件的注册,则由ngx_event_actions.add/add_conn
来调用对应的注册事件epoll_ctl
来实现。ngx_event_actions.add/add_conn
相当于一个抽象接口,实现注册的一方,只需要根据add
接口的定义ngx_int_t (*add)(ngx_event_t *ev, ngx_int_t event, ngx_uint_t flags);
实现注册即可。而调用的函数也只需要调用对应的ngx_event_actions.add
,传递对应的参数,不用关系内部实现。#define ngx_add_event ngx_event_actions.add
被进一步封装成ngx_handle_read_event, ngx_handle_write_event
等,由业务模块来进行调用注册。- 业务模块调用
ngx_handle_write_event
等事件注册时机通常在ngx_module_s
结构下的ngx_command_t
模块,比如ngx_http_module
模块的定义如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26static ngx_command_t ngx_http_commands[] = {
{ ngx_string("http"),
NGX_MAIN_CONF|NGX_CONF_BLOCK|NGX_CONF_NOARGS,
ngx_http_block, //改函数中进行了事件注册的调用。
0,
0,
NULL },
ngx_null_command
};
ngx_module_t ngx_http_module = {
NGX_MODULE_V1,
&ngx_http_module_ctx, /* module context */
ngx_http_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_command_t
结构,都会在ngx_conf_handler
函数中进行处理:
1
2
3cmd = ngx_modules[i]->commands;
//...
rv = cmd->set(cf, cmd, conf);
nginx注册机制结构图
根据Nginx的注册机制,我们把结构流程图分为了三个部分:
- 编译时控制注册哪些模块
- 运行时模块注册执行
- 运行时事件处理函数注册执行
前文已提过,nginx的模块注册控制是根据configure
配置来控制是否编译某些文件,以及生成对应的ngx_module.c
文件来控制注册列表。而执行的时候,则根据ngx_module.c
文件中的模块列表的全局变量值ngx_modules
来执行对应模块实现的接口。
TODO 对应的commands没有体现
nginx通过以上机制对事件处理模块进行注册,实例化事件处理接口ngx_event_actions
(这个变量只有一个,也就是说nginx同时只能有一个事件处理模块)。
nginx通过事件处理统一接口ngx_event_actions.add
,来进行事件处理函数的注册,同样删除事件也有相应的接口,而事件注册的具体方式,则通过上述注册的事件处理模块来实现(如epoll
,一些epoll示例)。
TODO 该图还需要修改和细化
一个事件触发流程
-
nginx启动后会监听配置的端口,监听端口后才会进行fork子进程,此时所有nginx都会监听对应的端口。使用算法让其中一个子进程获得fd
- 为了方式
惊群
效应,通过获取互斥锁的方式保证只有一个子进程获取到fd。参考
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39//ngx_process_events_and_timers
if (ngx_use_accept_mutex) {
if (ngx_accept_disabled > 0) {
ngx_accept_disabled--;
} else {
//在此函数中尝试进行加锁,争抢锁。
if (ngx_trylock_accept_mutex(cycle) == NGX_ERROR) {
return;
}
//抢到锁,使用post机制进行通知
if (ngx_accept_mutex_held) {
flags |= NGX_POST_EVENTS;
} else {
//没有抢到,推迟进行锁竞争
if (timer == NGX_TIMER_INFINITE
|| timer > ngx_accept_mutex_delay)
{
timer = ngx_accept_mutex_delay;
}
}
}
}
//尝试加锁,成功即可进行fd获取,失败则过一段时间再竞争。
ngx_trylock_accept_mutex(ngx_cycle_t *cycle)
{
if (ngx_shmtx_trylock(&ngx_accept_mutex)) {
ngx_log_debug0(NGX_LOG_DEBUG_EVENT, cycle->log, 0,
"accept mutex locked");
...
}
ngx_log_debug1(NGX_LOG_DEBUG_EVENT, cycle->log, 0,
"accept mutex lock failed: %ui", ngx_accept_mutex_held);
...
} - 为了方式
-
加锁后,事件来临之前,获取锁work进程一直阻塞在
ngx_epoll_module.c:ngx_epoll_process_events->epoll_wait
,而没有获取锁的设置timer,进入下一轮循环。 -
触发
ngx_event_accept
函数,该函数在ngx_event_process_init
时期被注册到了epoll
中。- 获取链接相关信息, 使用
accept
获取返回新的socket
。
图片来源 - 调用注册的
ls->handler
函数,这里对应的是ngx_http_init_connection
,该函数是在ngx_http_commands
里的ngx_http_block
函数中被调用的。ngx_http_init_connection
初始化了一个timer(TODO干啥的?),然后使用ngx_handle_read_event
注册了一个读事件,事件rev->handler = ngx_http_init_request
- 获取链接相关信息, 使用
-
触发步骤2注册的
epoll
读事件,对应调用rev->handler
函数,也就是ngx_http_init_request
函数 -
ngx_http_init_request
函数处理完所有的http请求的数据处理。
事件轮转
nginx
的master
进程在接收到请求后,将获取到的fd
根据算法分配给某一个子进程,子进程触发epoll
监听事件,获取执行accept
,并根据类型增加epoll
的读/写事件epoll
触发读/写事件,进行读写操作。由于nginx
的epoll
是ET
模式, 如果第一次读取数据没有读完,会继续增加对应的epoll
读事件。(TODO)- 继续触发后续事件。
timer机制
Nginx的定时事件的实现
nginx的master进程使用的是系统信号来进行死循环的延时,关键函数setitimer(ITIMER_REAL, &itv, NULL)
.
在worker进程中,则是使用自定义的timer,来判断超时,以及调用超时函数。nginx中的timer是通过红黑树实现的。
-
timer的初始化
1
2
3
4
5
6
7
8
9
10//ngx_event_process_init函数中调用
ngx_int_t
ngx_event_timer_init(ngx_log_t *log)
{
ngx_rbtree_init(&ngx_event_timer_rbtree, &ngx_event_timer_sentinel,
ngx_rbtree_insert_timer_value);
...
return NGX_OK;
} -
整体流程,和timer的读取处理:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20//主for循环
for ( ;; ) {
ngx_log_debug0(NGX_LOG_DEBUG_EVENT, cycle->log, 0, "worker cycle");
//主要处理事件和event
ngx_process_events_and_timers(cycle);
//收到退出信号,terminate或者quit
if (ngx_terminate || ngx_quit) {
for (i = 0; ngx_modules[i]; i++) {
if (ngx_modules[i]->exit_process) {
ngx_modules[i]->exit_process(cycle);
}
}
//内部会调用exit
ngx_master_process_exit(cycle);
}
...
}ngx_process_events_and_timers:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24//在红黑树中找到对应的最新超时。
if (ngx_timer_resolution) {
timer = NGX_TIMER_INFINITE;
flags = 0;
} else {
timer = ngx_event_find_timer();
flags = NGX_UPDATE_TIME;
}
//在evnet的wait前更新ngx_current_msec
delta = ngx_current_msec;
/*事件处理核心工作函数,将timer传递给epoll_wait中,作为超时时间。
如果该timer从epoll_wait中超时,那么证明这个timer到期了,
可以执行后面的ngx_event_expire_timers */
(void) ngx_process_events(cycle, timer, flags);
delta = ngx_current_msec - delta;
//只有超过了1ms,才会执行timer中的超时。
if (delta) {
ngx_event_expire_timers();
} -
timer的添加
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17//本质上就是向红黑树中插入节点
static ngx_inline void
ngx_event_add_timer(ngx_event_t *ev, ngx_msec_t timer)
{
ngx_msec_t key;
ngx_msec_int_t diff;
...
ngx_mutex_lock(ngx_event_timer_mutex);
ngx_rbtree_insert(&ngx_event_timer_rbtree, &ev->timer);
ngx_mutex_unlock(ngx_event_timer_mutex);
ev->timer_set = 1;
}
nginx中的事件机制
-
nginx通过
accept_mutex
锁来解决惊群问题,意味着同一时间一个请求只有一个进程接收到 -
当进程接收到数据后,事件模块产生事件,并添加到事件队列中。比如:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20//epoll触发事件
events = epoll_wait(ep, event_list, (int) nevents, timer);
...
//这里将读写事件放到ngx_posted_accept_events或者ngx_posted_events事件中
//在ngx_process_events_and_timers函数中的ngx_event_process_posted中进行处理
//猜测这个锁是在多线程的模式下才加上的
ngx_mutex_lock(ngx_posted_events_mutex);
for (i = 0; i < events; i++) {
c = event_list[i].data.ptr;
rev = c->read;
...
if (flags & NGX_POST_EVENTS) {
queue = (ngx_event_t **) (rev->accept ?
&ngx_posted_accept_events : &ngx_posted_events);
ngx_locked_post_event(rev, queue);
}
} -
事件放到
ngx_posted_accept_events
队列或ngx_posted_events
队列后,由ngx_process_events_and_timers
函数进行处理:1
2
3
4
5
6
7
8
9
10
11
12
13
14//获取防止惊群的锁
if (ngx_trylock_accept_mutex(cycle) == NGX_ERROR) {
return;
}
//处理事件接收的事件
if (ngx_posted_accept_events) {
ngx_event_process_posted(cycle, &ngx_posted_accept_events);
}
//释放锁,防止阻塞其他进程
if (ngx_accept_mutex_held) {
ngx_shmtx_unlock(&ngx_accept_mutex);
}
//处理其他事件
ngx_event_process_posted(cycle, &ngx_posted_events);
防止惊群问题,nginx在后续采用了新的方式Socket ReusePort
来处理。同时对应的就是linux系统中的SO_REUSEPORT参数。粗浅理解可参考,其他一些参考
NEXT:
使用gdb+debug日志更加快捷和方便。
- nginx的web缓存机制(相同请求缓存返回),NGINX为单进程模型,不存在互斥问题,直接到cache中找即可,只是数据写入如果要进行落盘的时候,是异步操作。
- 编写一个基于
epoll
的状态机。epoll状态机只能基于fd轮转,而触发只能通过内核软中断触发。epoll是用于io复用层面,意味着必须有io操作。业务层面的状态转换并没有io操作,并不适合这个。 - 更新一些图和说明
nginx中的命名习惯
- 几乎所有的通用变量都使用ngx_xxx进行了重定义,比如
intptr_t->ngx_int_t
。至于intptr_t
数据结构的解析,可参考 - nginx中,_s结尾的都代表是定义的结构体,_t都代表在声明变量时需要使用的类型,比如:
1
2
3
4
5
6
7typedef struct ngx_log_s ngx_log_t;
struct ngx_log_s { //_s用于定义结构
ngx_uint_t log_level;
ngx_open_file_t *file;
...
};
ngx_log_t *log; //_t用于声明变量
nginx中数据结构
nginx的数据共享
分析验证
重新编译debug版本,来验证我们的猜想,或者使用gdb跟踪也是可以的。gdb跟踪更方便快捷,但是在多进程分析的时候,debug模式往往更方便。
整体流程图
其他
nginx中的一些骚操作
内存节约
unsigned recycled:1;
使用了位域的方法来减少内存的使用。unsigned
==unsigned int
参考
Nginx开发从入门到精通
通俗易懂的Nginx工作原理
epoll 原理是如何实现的
可以列出相关的看过的书籍,或者问题参考链接网页一类的。
备注:
可以进行单独开帖:
- nginx中的一些骚操作
- nginx中数据结构
- nginx的数据共享
- nginx的模块机制
- 都可以在编译后的objs/ngx_modules.c查看哪些被注册。
- 模块分为几大类:
- 事件模块,构建不同的事件触发框架,类似
epoll
,poll
。这类模块不实现具体功能,只是配置不同的的事件触发器。 - 功能模块,如http模块…
- 事件模块,构建不同的事件触发框架,类似
- 模块注册方式:
- 有configure来控制文件拷贝,进而控制全局变量
ngx_modules
和ngx_event_actions
,以此达到模块注册效果。 - configure内容解析??TODO
- 有configure来控制文件拷贝,进而控制全局变量
- nginx中的events以及进程间通信