nginx源码阅读

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
在此之前,先干下面的事情:

  1. 下载nginx源码
  2. 编译
  3. 修改配置文件
  4. 执行nginx,并测试场景1成功

源码下载编译

更加详细的编译和配置可以参考:最全Nginx 配置文件详解及安装
下载:nginx-1.0.15
编译:

1
2
3
4
5
wget http://nginx.org/download/nginx-1.0.15.tar.gz
tar -xzvf nginx-1.0.15.tar.gz
cd nginx-1.0.15/
./configure --with-debug --prefix=/<your_path>/nginx_learning/
make && make install

不出意外就成功了,如果有错误,自行解决。相应的可执行程序在objs/nginx

配置场景

环境准备

我们安装的时候,把可执行文件install到了一个新目录:nginx_learning
不出意外,结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# tree
.
|-- conf
| |-- fastcgi.conf
| |-- fastcgi.conf.default
| |-- fastcgi_params
| |-- fastcgi_params.default
| |-- koi-utf
| |-- koi-win
| |-- mime.types
| |-- mime.types.default
| |-- nginx.conf
| |-- nginx.conf.default
| |-- scgi_params
| |-- scgi_params.default
| |-- uwsgi_params
| |-- uwsgi_params.default
| `-- win-utf
|-- html
| |-- 50x.html
| `-- index.html
|-- logs
`-- sbin
`-- nginx

修改配置文件

上述完成后,我们进行场景1和2的配置文件修改,得到nginx.conf如下:

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
#user  nobody;
worker_processes 1;

error_log logs/error.log;
#error_log logs/error.log notice;
#error_log logs/error.log info;

#pid logs/nginx.pid;


events {
worker_connections 1024;
}


http {
include mime.types;
default_type application/octet-stream;

log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';

access_log logs/access.log main;

sendfile on;
#tcp_nopush on;

#keepalive_timeout 0;
keepalive_timeout 65;

#gzip on;

server {
listen 8080;
server_name localhost;

#charset koi8-r;

#access_log logs/host.access.log main;

location / {
root /data/www;
index index.html index.htm;
}

#error_page 404 /404.html;

# redirect server error pages to the static page /50x.html
#
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}

# proxy the PHP scripts to Apache listening on 127.0.0.1:80
#
location /proxy {
proxy_pass http://127.0.0.1:8000/;
}
}
}

配置文件的root配置根据个人情况修改。
注意:www目录位置,注意权限问题,详情可查看日志logs/error.log。权限问题处理方式可参考:Nginx 403 forbidden for all files,翻不了墙的同学,自行百度

构建资源

然后我们在www目录下新建一个index.html文件,作为场景1的服务端资源:
index.html:

1
2
3
4
<h1> Hello nginx </h1>
```

对于场景2,我们需要构建反向代理的后端服务,可以自己启动一个http的文件服务器。比如:

nohub python3 -m http.server --bind 127.0.0.1 8000 &

1
可以本地curl一下以验证启动效果:

curl http://127.0.0.1:8000

1
最后我们启动nginx:

nginx_learning/nginx -p /<your_path>/nginx_learning/

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#### 测试效果
我们可以看下场景1的展示效果:
![场景1](./nginx源码阅读/场景1效果.png)
我这里使用的是我自己的一个桩。访问效果如下:
![场景2](./nginx源码阅读/场景2效果.png)


## 场景1源码分析
我们上面已经实现了场景1和场景2的展示,那么现在开始分析场景1的流程。
### 源码分析
按照惯例,我们可以从main函数开始,先分析程序启动,再一步一步的分析场景1是如何实现的。分析过程中,遇到变量结构太复杂可以先记录下来,跳过,等实际应用的时候再回头分析。记住,这时候我们的主要目的是梳理出整个执行逻辑,而不是细节,不必在对流程不重要的细节上花费太多时间,以阻碍主流程梳理进度。
#### main函数
**mian函数局部变量定义:**
找到main函数,一开始就是四个数据结构:
ngx_int_t         i;
ngx_log_t        *log; //猜测用于日志记录
ngx_cycle_t      *cycle, init_cycle; //猜测是个贯穿整个流程重要的数据结构,里面包含的东西很多,暂时不管
ngx_core_conf_t  *ccf;//猜测配置相关内容,细节再说
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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
可以大致看一看,猜测一下它们作用。大致看一下各个数据结构类型。我这里好奇它的[命名习惯](#nginx中的命名习惯),因此初步进行梳理。`ngx_cycle_t`和`ngx_core_conf_t`结构有些多,后面梳理(你也可以提前看看[nginx中数据结构](#nginx中数据结构))。
**初始化和参数分析:**
1. `ngx_strerror_init`初始化了一个自己的errnum 列表。关于error number,有这样一个解释:[参考](https://blog.csdn.net/baishuwei/article/details/2535484)
>在没有解释这些错误定义之前,先让我们看看为什么要知道这些常用error number? 对于写C程序的人来说,errno并不是一个陌生的变量,无论你是在用户态还是在内核态。简短来说,errno是用来保存错误信息,这些信息反应了相应错误原因。因此,一个小小errno就可以连接user space programmer 与 kernel space programmers,可见其重要性。但是,我们的programmers确又常常忽略这些,他们往往只看中正确与错误,而不是去访问强大的errno获取更多的信息。对于普通的程序,为此可能仅仅是"没关系,大不了可以重启"。但是,对于一些重要的任务,重启可能意味着灾难的发生,尤其是在重要的领域。为此,这里我想给大家列出常见的errno,提醒大家在处理设备时,"check errno when your routine failed!".

和[手册](https://man7.org/linux/man-pages/man3/errno.3.html)中的描述差距不大:
>The <errno.h> header file defines the integer variable errno,
which is set by system calls and some library functions in the
event of an error to indicate what went wrong.
2. `ngx_get_options`参数分析,nginx主要依赖配置文件,因此命令行参数很简单。
3. `ngx_time_init`初始化nginx中时间记录格式。
4. `ngx_regex_init`(可先跳过)
5. `ngx_log_init`(可先跳过)
6. `ngx_ssl_init`(可先跳过)
7. `ngx_create_pool`知道是初始化内存池的就行。
8. `ngx_save_argv`(可先跳过)
9. `ngx_process_options`(可先跳过)
10. `ngx_os_init`获取一些系统相关信息
11. `ngx_crc32_table_init`(可先跳过)
12. `ngx_add_inherited_sockets`(可先跳过)看起来像获取NGINX系统变量,然后干些事情,和主流程似乎不影响。毕竟有`inherited == NULL`这种判断
13.
14. `ngx_init_cycle`这个玩意儿,里面包含了很多东西,粗看一眼,牵涉到系统socket监听,配置文件分析,和主流程是紧密相关,不能跳过了。看 一下其流程
1. 分配一些内存池,更新一些时间戳,以及一些链表。
2. `ngx_conf_param` 读取配置文件外的全局内容,实际工作方式调用`ngx_conf_parse`
3. `ngx_conf_parse(&conf, &cycle->conf_file)` 读取配置文件内容
1. `if (filename)` 判断是打开配置文件以及获取基本信息。
2. `for ( ;; )`中的`ngx_conf_read_token`进行每一个token的分析。这里1个`;`所包含的为一个token
1. 当找到完整的有用的单词,就会标记`found = 1`,然后进行下面的赋值处理:
```c
if (found) {
//每当找到一个可用配置,比如‘worker_processes 1;’
//就会先后将‘worker_processes’和‘1’放到cf->args中,
//至于这个ngx_array_t结构如何,待后续分析。TODO
word = ngx_array_push(cf->args);
if (word == NULL) {
return NGX_ERROR;
}

//分配相应内存给数据
word->data = ngx_pnalloc(cf->pool, b->pos - start + 1);
if (word->data == NULL) {
return NGX_ERROR;
}

//拷贝找到的值
for (dst = word->data, src = start, len = 0;
src < b->pos - 1;
len++){
....
}
if (ch == ';') {
return NGX_OK;
}
        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`出错处理,配置回滚。
  1. ngx_signal_process(可先跳过)

  2. ngx_init_signals注册信号量的回调函数,信号量和回调函数的关联关系在signals全局量中。控制相关标记位来间接控制master进程中的动作

  3. ngx_os_status打印系统状态

  4. 获取配置:

    1
    ccf = (ngx_core_conf_t *) ngx_get_conf(cycle->conf_ctx, ngx_core_module);
  5. 标识是否是master进程:

    1
    2
    3
    if (ccf->master && ngx_process == NGX_PROCESS_SINGLE) {
    ngx_process = NGX_PROCESS_MASTER;
    }
  6. ngx_daemonfork进程,具体操作后续继续阅读

  7. ngx_create_pidfile创建pidfile

  8. 创建worker进程或者master进程

    1
    2
    3
    4
    5
    6
    if (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进程

  1. 注册许多的信号量,并设置成阻塞,延后处理。sigprocmask函数讲解
  2. 设置进程标记
  3. ngx_start_worker_processes控制子进程启动(这里的fork干嘛用的?)
    1. for ngx_get_cpu_affinity循环子进程个数
    2. ngx_spawn_process创建和子进程通信的套接字。
      1. ngx_worker_process_cycle回调函数,具体作用是解析收到的消息,放到状态机里处理。消息从何而来?未知,主要是fork函数作用?
    3. ngx_pass_open_channel往套接字里发送消息,消息转发给子进程。
  4. ngx_start_cache_manager_processes, 其抽象作用和ngx_start_worker_processes差不多,都是发送消息给子进程,只是功能作用上是进行cache的管理启动。主要调用的也是下面两个函数:
    1. ngx_spawn_process 内容是:‘cache manager process’
    2. ngx_pass_open_channel
  5. 主进程的主要for ( ;; )
    1. 是否进行delay
      1. 使用setitimer控制进程延时
    2. sigsuspend恢复之前的sigprocmask阻塞信号的处理
    3. 根据接收到的信号(用户或系统发送),修改标记位来控制相应的子进程,给子进程发送相应的信号,或者套接字消息(类似上面的ngx_start_worker_processes的方式)。
      这里我们会疑惑,哪里修改的标记位?哪里接受处理的信号。全局搜索一下可以找到:设置接收信号的回调函数在main中的ngx_init_signals函数中完成,根据对应的信号量调用ngx_signal_handler函数来修改相应标记位。

worker进程

  1. 设置环境变量,是nginx中的全局变量environ
  2. 执行ngx_modules中的init_process函数,也就是利用nginx中模块注册机制。
  3. for ( ;; )循环。
    1. ngx_process_events_and_timers非常核心的函数,包含了事件处理,和延时处理。nginx中所有事务都是围绕这个函数中事件处理来完成的。也就是利用了状态机的机制,每次事件的触发,执行触发的相应事件,就是在该函数中完成。

      1. timer初始化,如果ngx开了线程处理方式,则timer初始化和处理有所差异。
      2. 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
      17
      typedef 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

    2. 处理接收到的系统信号或者主进程信号:ngx_terminate,ngx_quit,ngx_reconfigurengx_reopen。这里面的所代表的功能都很好理解,细节需要的时候再看。

nginx架构

从上面的代码分析,我们基本上可以得到大致的结论:

  1. nginx有多个进程,分别为master进程和work进程(这个从nginx的实际运行情况也可以看到)。
  2. nginx通过环境变量和配置文件来进行管理。
  3. 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <ngx_config.h>
#include <ngx_core.h>

extern ngx_module_t ngx_core_module;
extern ngx_module_t ngx_errlog_module;
extern ngx_module_t ngx_conf_module;
extern ngx_module_t ngx_events_module;
extern ngx_module_t ngx_event_core_module;
extern ngx_module_t ngx_epoll_module;
extern ngx_module_t ngx_http_module;
...
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_module,
...
NULL
};

其核心注册机制就是通过配置文件,选择生成全局变量ngx_modules, nginx主体程序读取该全局变量的值来执行相应模块。

模块入口

通过注册机制可知,主程序与模块直接的交互只能通过ngx_module_t这个结构,也就是说ngx_module_t是模块提供的接口。ngx_module_t结构如下(先撇一眼,在后续分析):

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
typedef struct ngx_module_s      ngx_module_t;
struct ngx_module_s {
ngx_uint_t ctx_index;
ngx_uint_t index;

ngx_uint_t spare0;
ngx_uint_t spare1;
ngx_uint_t spare2;
ngx_uint_t spare3;

ngx_uint_t version;

void *ctx;
ngx_command_t *commands;
ngx_uint_t type;

ngx_int_t (*init_master)(ngx_log_t *log);

ngx_int_t (*init_module)(ngx_cycle_t *cycle);

ngx_int_t (*init_process)(ngx_cycle_t *cycle);
ngx_int_t (*init_thread)(ngx_cycle_t *cycle);
void (*exit_thread)(ngx_cycle_t *cycle);
void (*exit_process)(ngx_cycle_t *cycle);

void (*exit_master)(ngx_cycle_t *cycle);

uintptr_t spare_hook0;
uintptr_t spare_hook1;
uintptr_t spare_hook2;
uintptr_t spare_hook3;
uintptr_t spare_hook4;
uintptr_t spare_hook5;
uintptr_t spare_hook6;
uintptr_t spare_hook7;
};

我们现在已经知道如何进行模块注册,主程序和模块之间的交互接口。对于主程序来说,它并不知道模块具体功能是什么,只知道ngx_module_t这个结构,和ngx_modules全局量。那么现在我们就需要分析,ngx_modules在什么时候使用(模块实际生效位置)以及ngx_module_t里每个变量功能(模块生效干了啥)。即模块生效位置和功能。

模块生效位置

我们跟着启动顺序进行梳理,寻找ngx_modules使用的位置。注册模块分为两类,一是核心模块(NGX_CORE_MODULE),二是配置模块(NGX_CONF_MODULE)。

  1. main函数,进行ngx_module_t.index初始化:

    1
    2
    3
    4
    ngx_max_module = 0;
    for (i = 0; ngx_modules[i]; i++) {
    ngx_modules[i]->index = ngx_max_module++;
    }
  2. ngx_init_cycle中执行注册的核心模块配置初始化(module->create_conf)。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
       for (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;
    }
    }
  3. 在配置文件解析中,读取注册的配置模块,并用于处理配置文件参数(具体配置方法后面细化)。

    1
    2
    3
    4
    5
    if (ngx_modules[i]->type != NGX_CONF_MODULE
    && ngx_modules[i]->type != cf->module_type)
    {
    continue;
    }
  4. 注册的核心模块初始化,类似上述第二点:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    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;
    }
    }
    }
  5. 执行注册模块各自的init_module

    1
    2
    3
    4
    5
    6
    7
    8
    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);
    }
    }
    }
  6. work里ngx_single_process_cycle中会执行模块的init_processexit_process:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    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);
    }
    }
    }
    if (ngx_terminate || ngx_quit) {
    for (i = 0; ngx_modules[i]; i++) {
    if (ngx_modules[i]->exit_process) {
    ngx_modules[i]->exit_process(cycle);
    }
    }
    }
  7. master的ngx_master_process_cycle函数的子函数,ngx_master_process_exit会调用到对应的函数,

    1
    2
    3
    4
    5
    for (i = 0; ngx_modules[i]; i++) {
    if (ngx_modules[i]->exit_master) {
    ngx_modules[i]->exit_master(cycle);
    }
    }

上面的1-5几乎全是模块配置初始化相关的内容,init_processexit_process是work进程中调用的模块初始化和退出。而init_masterexit_master则是在master进程中进行初始化和退出的。但这里还是有一个问题,模块真正处理请求的函数时如何和work程序挂钩的?

nginx事件处理流程

这里要弄清楚事件处理流程,有两种方式:

  1. 从必调的init_process入手,阅读一个注册模块的init_process函数,看实现了什么。

    1. 首先从一个熟悉的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
    20
    static 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_confinit_conf也为空。
    然后一连看了多个http的模块,发现init_process都为空。看来这种方法不行了。

  2. work进程入手,看谁注册了ngx_event_actions时间处理函数,然后阅读该模块的实现机制。

    1. 在linux下,默认使用的是epoll, 对应注册的ngx_event_actions如下:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    ngx_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函数。
    那么我们可以得到初步的结论:

    1. nginx的模块依赖是一层层递进的:
      1. 核心模块
      2. 事件处理模块
      3. 业务处理模块
    2. ngx_module_t负责模块注册时的初始化。比如各种配置相关处理,执行业务前的初始化等。
    3. ngx_event_module_t则是负责进行业务注册,也就是当事件触发后,我应当执行哪些业务。而业务注册则是通过ngx_event_actions_t.add*函数实现。

结合上述两点,可以初步得到模块注册和事件处理的流程:

  1. ngx_event_core_module注册模块(编译时控制),生成ngx_modules.c文件,包含全局变量ngx_modules

  2. nginx启动时执行相关初始化,执行ngx_modulesinit_conf等。

  3. 启动nginx的work进程,执行ngx_modulesinit_process。其中包含ngx_event_core_module.ngx_event_process_init

  4. ngx_event_process_init又以同样的原理,进行event事件的注册

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    for (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;
    }
  5. ngx_event_process_init注册的事件中就有ngx_epoll_module模块(ngx_modules.c),可以说ngx_epoll_module是真正处理事件的地方。ngx_event_process_init中调用的module->actions.init在这里本质就是ngx_epoll_init

  6. ngx_epoll_init中进行了ngx_event_actions事件的注册:

    1
    ngx_event_actions = ngx_epoll_module_ctx.actions;
  7. nginx的work进程处理事件实际调用的是ngx_event_actions.process_events,那么就等同于ngx_epoll_module中的ngx_epoll_process_events函数。

  8. 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);
    }
    }
  9. epoll事件处理通过调用event_list[i].data.ptr指针来指向具体实现进行处理。而事件的注册,则由ngx_event_actions.add/add_conn来调用对应的注册事件epoll_ctl来实现。

    1. 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,传递对应的参数,不用关系内部实现。
    2. #define ngx_add_event ngx_event_actions.add被进一步封装成ngx_handle_read_event, ngx_handle_write_event等,由业务模块来进行调用注册。
    3. 业务模块调用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
    26
    static 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
    };
    1. 而每个模块的ngx_command_t结构,都会在ngx_conf_handler函数中进行处理:
    1
    2
    3
    cmd = ngx_modules[i]->commands;
    //...
    rv = cmd->set(cf, cmd, conf);

nginx注册机制结构图

根据Nginx的注册机制,我们把结构流程图分为了三个部分:

  1. 编译时控制注册哪些模块
  2. 运行时模块注册执行
  3. 运行时事件处理函数注册执行

前文已提过,nginx的模块注册控制是根据configure配置来控制是否编译某些文件,以及生成对应的ngx_module.c文件来控制注册列表。而执行的时候,则根据ngx_module.c文件中的模块列表的全局变量值ngx_modules来执行对应模块实现的接口。
TODO 对应的commands没有体现
nginx模块注册机制

nginx通过以上机制对事件处理模块进行注册,实例化事件处理接口ngx_event_actions(这个变量只有一个,也就是说nginx同时只能有一个事件处理模块)。
nginx通过事件处理统一接口ngx_event_actions.add,来进行事件处理函数的注册,同样删除事件也有相应的接口,而事件注册的具体方式,则通过上述注册的事件处理模块来实现(如epoll,一些epoll示例)。
TODO 该图还需要修改和细化
nginx事件注册机制

一个事件触发流程

  1. nginx启动后会监听配置的端口,监听端口后才会进行fork子进程,此时所有nginx都会监听对应的端口。使用算法让其中一个子进程获得fd

    1. 为了方式惊群效应,通过获取互斥锁的方式保证只有一个子进程获取到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);
    ...
    }
  2. 加锁后,事件来临之前,获取锁work进程一直阻塞在ngx_epoll_module.c:ngx_epoll_process_events->epoll_wait,而没有获取锁的设置timer,进入下一轮循环。

  3. 触发ngx_event_accept函数,该函数在ngx_event_process_init时期被注册到了epoll中。

    1. 获取链接相关信息, 使用accept获取返回新的socket
      accept原理图片来源
    2. 调用注册的ls->handler函数,这里对应的是ngx_http_init_connection,该函数是在ngx_http_commands里的ngx_http_block函数中被调用的。
      1. ngx_http_init_connection初始化了一个timer(TODO干啥的?),然后使用ngx_handle_read_event注册了一个读事件,事件rev->handler = ngx_http_init_request
  4. 触发步骤2注册的epoll读事件,对应调用rev->handler函数,也就是ngx_http_init_request函数

  5. ngx_http_init_request函数处理完所有的http请求的数据处理。

事件轮转

  1. nginxmaster进程在接收到请求后,将获取到的fd根据算法分配给某一个子进程,子进程触发epoll监听事件,获取执行accept,并根据类型增加epoll的读/写事件
  2. epoll触发读/写事件,进行读写操作。由于nginxepollET模式, 如果第一次读取数据没有读完,会继续增加对应的epoll读事件。(TODO)
  3. 继续触发后续事件。

timer机制

Nginx的定时事件的实现
nginx的master进程使用的是系统信号来进行死循环的延时,关键函数setitimer(ITIMER_REAL, &itv, NULL).
在worker进程中,则是使用自定义的timer,来判断超时,以及调用超时函数。nginx中的timer是通过红黑树实现的。

  1. 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;
    }
  2. 整体流程,和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();
    }
  3. 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中的事件机制

参考

  1. nginx通过accept_mutex锁来解决惊群问题,意味着同一时间一个请求只有一个进程接收到

  2. 当进程接收到数据后,事件模块产生事件,并添加到事件队列中。比如:

    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);
    }
    }
  3. 事件放到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日志更加快捷和方便。

  1. nginx的web缓存机制(相同请求缓存返回),NGINX为单进程模型,不存在互斥问题,直接到cache中找即可,只是数据写入如果要进行落盘的时候,是异步操作。
  2. 编写一个基于epoll的状态机。epoll状态机只能基于fd轮转,而触发只能通过内核软中断触发。epoll是用于io复用层面,意味着必须有io操作。业务层面的状态转换并没有io操作,并不适合这个。
  3. 更新一些图和说明

nginx中的命名习惯

  1. 几乎所有的通用变量都使用ngx_xxx进行了重定义,比如intptr_t->ngx_int_t。至于intptr_t数据结构的解析,可参考
  2. nginx中,_s结尾的都代表是定义的结构体,_t都代表在声明变量时需要使用的类型,比如:
    1
    2
    3
    4
    5
    6
    7
    typedef 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中的一些骚操作

内存节约

  1. unsigned recycled:1;
    使用了位域的方法来减少内存的使用。unsigned == unsigned int

参考

Nginx开发从入门到精通
通俗易懂的Nginx工作原理
epoll 原理是如何实现的

可以列出相关的看过的书籍,或者问题参考链接网页一类的。

备注:
可以进行单独开帖:

  1. nginx中的一些骚操作
  2. nginx中数据结构
  3. nginx的数据共享
  4. nginx的模块机制
    1. 都可以在编译后的objs/ngx_modules.c查看哪些被注册。
    2. 模块分为几大类:
      1. 事件模块,构建不同的事件触发框架,类似epoll, poll。这类模块不实现具体功能,只是配置不同的的事件触发器。
      2. 功能模块,如http模块…
    3. 模块注册方式:
      1. 有configure来控制文件拷贝,进而控制全局变量ngx_modulesngx_event_actions,以此达到模块注册效果。
      2. configure内容解析??TODO
  5. nginx中的events以及进程间通信
-------------本文结束感谢您的阅读-------------