Nginx 是业界之名的高性能服务器,采用模块化实现,这里简单看下其实现方式。
模块
Nginx 的基础架构是模块化的,从 官方文档 中可以发现,很大一部分是关于模块的介绍,可执行文件对应的模块可以通过 nginx -V
查看,也就是编译选项。
nginx 模块要负责三种角色:1) handler,接收请求并产生输出;2) filters,处理 hander 产生的输出;3) load-balancer,负载均衡,选择一个后端 server 发送请求。
在执行 configure
命令时,会根据配置选项动态生成 objs/ngx_modules.c
文件,有效的模块会包含在 ngx_modules[]
中。模块包括了核心模块和可选模块,前者是必需的,不会出现在源码编译的配置项中,如 ngx_core_module
模块;而后者是可以在编译的时候进行配置的,只有添加了编译选项之后,才会出现在 ngx_modules[]
中。
相关结构体
其中与模块相关的数据结构包括。
- ngx_module_t
代表模块本身,由 nginx 统一定义的结构,其指针通过编译的配置项被放入ngx_modules[]
中。 - ngx_(module name)_conf_t
用来表示模块的配置内容,由各个模块进行定义,其中部分成员可以通过配置文件进行配置。 - ngx_(module name)_module_t
各个模块的通用回调函数,由各个模块定义,根据各个模块的特性在不同阶段调用相关函数。 - ngx_command_t
对应配置文件中的指令,由 nginx 统一定义。
nginx 的模块分为 4 个大类型的模块,包括事件模块、HTTP 模块、邮件代理相关的 mail 模块。
新建模块
开发一个 HTTP 模块的步骤如下,当打开 http://localhost/hello_world
时,显示 hello world, foobar!!!
。
1. 修改配置文件
默认是 /etc/nginx/nginx.conf
,在 location / { ... }
中添加如下内容。
... ...
location /hello_world {
hello_world;
}
... ...
2. 添加编译
编写 auto/modules
文件,为了让 nginx 在 configure
过程能找到编写的模块。
$ cat auto/modules
... ...
if [ $HTTP_ACCESS = YES ]; then
HTTP_MODULES="$HTTP_MODULES $HTTP_ACCESS_MODULE"
HTTP_SRCS="$HTTP_SRCS $HTTP_ACCESS_SRCS"
fi
# 上面是原有的, 这里才是加上的
HTTP_MODULES="$HTTP_MODULES ngx_http_test_module"
HTTP_SRCS="$HTTP_SRCS src/http/modules/ngx_http_test_module.c"
... ...
3. 编写源码
添加如下 src/http/module/ngx_http_test_module.c
文件。
#include <ngx_core.h>
#include <ngx_http.h>
#include <ngx_config.h>
static char *set(ngx_conf_t *, ngx_command_t *, void *);
static ngx_int_t handler(ngx_http_request_t *);
static ngx_command_t test_commands[] = {
{
ngx_string("hello_world"),
NGX_HTTP_LOC_CONF | NGX_CONF_NOARGS,
set,
NGX_HTTP_LOC_CONF_OFFSET,
0,
NULL
},
ngx_null_command
};
static ngx_http_module_t test_ctx = {
NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL
};
ngx_module_t ngx_http_test_module = {
NGX_MODULE_V1,
&test_ctx,
test_commands,
NGX_HTTP_MODULE,
NULL, NULL, NULL, NULL, NULL, NULL, NULL,
NGX_MODULE_V1_PADDING
};
static char *set(ngx_conf_t *cf, ngx_command_t *cmd, void *conf) {
ngx_http_core_loc_conf_t *corecf;
corecf = ngx_http_conf_get_module_loc_conf(cf, ngx_http_core_module);
corecf->handler = handler;
return NGX_CONF_OK;
};
static ngx_int_t handler(ngx_http_request_t *req) {
u_char html[1024] = "<h1>hello world, foorbar!!!</h1>";
req->headers_out.status = 200;
int len = sizeof(html) - 1;
req->headers_out.content_length_n = len;
ngx_str_set(&req->headers_out.content_type, "text/html");
ngx_http_send_header(req);
ngx_buf_t *b;
b = ngx_pcalloc(req->pool, sizeof(ngx_buf_t));
ngx_chain_t out;
out.buf = b;
out.next = NULL;
b->pos = html;
b->last = html + len;
b->memory = 1;
b->last_buf = 1;
return ngx_http_output_filter(req, &out);
}
然后可以直接通过 ./configure && make
编译。
4. 三方模块
也可以通过如下方式添加。
$ cat /path/to/module1/source/config
ngx_addon_name=ngx_http_test_module
HTTP_MODULES="$HTTP_MODULES ngx_http_test_module"
NGX_ADDON_SRCS="$NGX_ADDON_SRCS $ngx_addon_dir/ngx_http_test_module.c"
CORE_LIBS="$CORE_LIBS -lfoo"
$ cat /path/to/module1/source/ngx_http_test_module.c
... ...
$ ./configure --add-module=/path/to/module1/source # 编译
$ make
源码解析
main() @ src/core/nginx.c
|-ngx_debug_init()
|-ngx_strerror_init() // 初始化错误提示列表,以errno为下标,元素就是对应的错误提示信息
|-ngx_get_options() // 解析命令参数
|-ngx_time_init() // 初始化并更新时间,如全局变量ngx_cached_time
|-ngx_regex_init() // 正则表达式的初始化
|-ngx_log_init() // 初始化日志
|-ngx_create_pool() // 创建以1024大小的cycle内存块
|-ngx_save_argv() // 保存命令行参数,方便后面进行热代码替换
|-ngx_process_options() // 处理配置文件相关信息,并初始化cycle
|-ngx_os_init() // 处理操作系统相关初始化
|-ngx_crc32_table_init() // 初始化循环冗余检验表
|-ngx_add_inherited_sockets() // 通过环境变量NGINX完成socket的继承
|-... ... // +++包括了对ngx_modules[]的index初始化
|-ngx_init_cycle() // 主要的初始化过程
| |-gethostname() // 获取设置hostname
| |-... ... // 调用core-modules的create_conf()函数,并准备解析配置文件的数据结构
| |-ngx_conf_param() // -g参数的解析
| |-ngx_conf_parse() // 解析配置文件
| | |-ngx_conf_handler()
| |-... ... // 调用core-modules的init_conf()函数
| |-ngx_create_pidfile() // 创建PID文件
| |-... ... // 创建文件路径、打开文件描述符以及创建共享内存
| |-ngx_open_listening_sockets() // 打开所有的监听socket
| | |-ngx_socket() // 创建socket套接字
| | |-setsockopt() // 设置可重用
| | |-ngx_nonblocking() // 设置为非阻塞
| | |-bind() // 绑定要监听的socket地址
| | |-listen() // 监听socket
| |-... ... // 调用所有模块的init_module()函数
|-ngx_init_signals() // 设置信号处理函数
|
|-ngx_single_process_cycle() // 如果是单进程
|-ngx_master_process_cycle() // 以master方式运行多进程,主要解析该流程
|-ngx_start_worker_processes()
| |-ngx_spawn_process() // 创建子进程ngx_worker_process_cycle()
| |
| |-ngx_worker_process_cycle() // worker的主要处理流程,子进程处理流程
| |-ngx_worker_process_init() // 初始化
| | |-... ... // 设置执行环境、优先级等参数
| | |-ngx_modules[i]->init_process() // 调用所有模块的初始化回调函数
| | |-... ... // 关闭不使用的socket
| |-ngx_setproctitle() // 设置进程名称worker process,然后接下来是死循环
| |
| |-... ... // 通过ngx_exiting判断,如果进程退出,关闭所有连接
| |-ngx_process_events_and_timers() // 处理事件和计时,处理惊群现象
| | |-... ... // 在此会通过ngx_use_accept_mutex防止惊群
| | |-ngx_process_events() // 实际调用ngx_event_actions.process_events()
| | |-ngx_event_process_posted()
| |-... ... // 处理终止、退出、重新打开命令
|
|-ngx_start_cache_manager_processes()
事件机制
在配置文件中可以通过如下方式选择不同的事件机制。
events {
use epoll; # kqueue, rtsig, epoll, select, poll
}
在 Linux 中默认会采用 epoll 实现 Nginx 的事件驱动,在 src/event/modules/ngx_epoll_module.c
中实现;其中 ngx_epoll_commands[]
表明该模块对那些配置项感兴趣。
初始化通过 ngx_epoll_init()
实现。
ngx_epoll_init() // +++初始化
|-ngx_event_get_conf() // 获取解析所得的配置项的值
|-epoll_create() // 创建epoll对象,多数Linux中的参数无效
|-ngx_epoll_aio_init() // 异步IO
|-ngx_alloc() // 创建event_list数组,用于进行epoll_wait调用时传递内核对象
参考
一篇介绍 Nginx 模块的文章,非常不过的入门文章,可以参考 Emiller’s Guide To Nginx Module Development,也可以查看 本地文档 。
github nginx-systemtap-toolkit 通过 systemtap 监控 nginx 的一系列工具。