系列开篇就说过,Nginx 的所有功能都是模块提供的。前面的文章你看到了 HTTP 模块、Event 模块、Upstream 模块在各自领域的工作方式,现在是时候把视角拉回到模块系统本身,,看看 Nginx 的模块到底长什么样,静态模块怎么初始化,动态模块又是如何通过 dlopen 加载进来的。
这篇文章会深入 ngx_module_t 的每一个字段,拆解 ngx_modules.c 的生成逻辑、ngx_count_modules() 的索引分配、ngx_load_module() 的动态加载路径,以及模块 commands 数组如何驱动配置文件解析器。
ngx_module_t:模块的完整形态#
每个 Nginx 模块本质就是一个 ngx_module_t 结构体变量。定义在 src/core/ngx_module.h:
| |
看起来字段很多,但核心就三块:元数据(ctx_index/index/name/version/signature/type)、配置接口(ctx/commands)、生命周期回调(7 个 init/exit 函数)。
NGX_MODULE_V1 宏为前 6 个字段提供默认值,,index 和 ctx_index 初始化为 NGX_MODULE_UNSET_INDEX(即 (ngx_uint_t)-1),version 设为 nginx_version,signature 设为 NGX_MODULE_SIGNATURE。后面 7 个回调函数 NULL,8 个 spare_hook 0,由 NGX_MODULE_V1_PADDING 补齐。看实际模块的声明就简洁多了:
| |
模块类型:五大分类#
type 字段决定了模块归属的子系统。每个类型的值是用 ASCII 字符拼出来的整数,,方便调试时从内存里直接读出类型名:
| |
- NGX_CORE_MODULE:核心模块,如
ngx_core_module、ngx_http_module、ngx_events_module。它们不处理具体业务,而是负责创建、初始化子系统的配置上下文。 - NGX_EVENT_MODULE:事件处理模块,如
ngx_epoll_module、ngx_kqueue_module。它们实现事件驱动机制。 - NGX_HTTP_MODULE:HTTP 处理模块,数量最多(200+),涵盖 proxy、fastcgi、headers、gzip 等。
- NGX_MAIL_MODULE / NGX_STREAM_MODULE:邮件代理和四层 TCP/UDP 代理模块。
ctx 指针的具体类型随 type 变化,,NGX_CORE_MODULE 时是 ngx_core_module_t(包含 create_conf 和 init_conf 函数指针),NGX_HTTP_MODULE 时是 ngx_http_module_t(包含 8 个钩子:create_main_conf、create_srv_conf、create_loc_conf 等)。
ngx_modules:全局模块数组#
所有静态链接的模块在编译时被 auto/modules 脚本生成到 ngx_modules.c:
| |
数组以 NULL 结尾。这个数组在 ngx_preinit_modules() 中完成初始化:
| |
关键设计:ngx_max_module = 静态模块数 + 128。NGX_MAX_DYNAMIC_MODULES 这个硬上限(128)意味着一个 Nginx 进程最多额外加载 128 个动态 .so 模块。
每个 cycle 初始化时,ngx_cycle_modules() 从 ngx_modules 拷贝到 cycle->modules:
| |
为什么拷贝而不是直接用全局数组?因为动态加载的模块需插入到 cycle->modules 中,不能修改只读的全局 ngx_modules[]。
两个索引:index 与 ctx_index#
这是很多初读 Nginx 源码的人容易混淆的地方:
index:模块在全局cycle->modules[]数组中的位置。在ngx_preinit_modules()中被赋值为数组下标 i。对动态模块来说,由ngx_add_module()调用ngx_module_index()分配。这个索引用于访问cycle->conf_ctx[index]获取模块的配置结构体指针。ctx_index:模块在同一类型模块内的序号。比如所有类型为NGX_HTTP_MODULE的模块,ctx_index 分别是 0、1、2……由ngx_count_modules()分配。这个索引用于 HTTP 模块的 phase handlers 数组查找。
ngx_count_modules() 的逻辑值得一看:
| |
这里有个精妙的设计:cycle->modules_used = 1。一旦模块被计数标记为 used,后续的 load_module 指令就会被拒绝(ngx_load_module() 中首先检查 cf->cycle->modules_used)。确保在配置解析期间动态加载的模块必须在任何 ngx_count_modules() 调用之前完成。
ngx_init_modules():模块初始化#
所有模块的 init_module 回调在配置解析完成后统一调用:
| |
这个调用序列发生在 ngx_init_cycle() 内部,在配置解析完成、监听 socket 创建之后。init_module 回调通常用于初始化与配置相关的全局状态,,比如 ngx_http_module 的 init_module 会初始化 HTTP 的 phase handlers。init_process 则在每个 worker 进程 fork 后调用,用于初始化进程级资源(如连接池、定时器)。
init_master 在主进程中调用,exit_master / exit_process 则对应退出清理。整个模块生命周期环环相扣。
动态模块加载:ngx_load_module()#
从 Nginx 1.9.11 开始支持动态模块。配置写法很简单:
| |
背后是 ngx_load_module() 函数(src/core/nginx.c):
| |
流程三步走:
dlopen:使用
RTLD_NOW | RTLD_GLOBAL标志加载 .so。RTLD_NOW表示立即解析所有未定义符号,有链接错误马上报出来而不是推到运行时。RTLD_GLOBAL是关键,,它把 .so 中的符号提升到全局符号表,后续加载的其它 .so 也能看到。但这也是双刃剑(见下文符号冲突)。dlsym 提取模块符号:从 .so 中查找
ngx_modules、ngx_module_names、ngx_module_order三个全局符号。注意这里的ngx_modules是 .so 内部定义的数组,不是主程序的静态数组。ngx_add_module 逐个注册:对 .so 导出的每个模块,检查版本和签名兼容性,检查是否重名,分配全局 index 和 ctx_index,插入
cycle->modules数组。
ngx_dlopen.c 对 dlerror() 做了简单的封装:
| |
版本与签名检查#
ngx_add_module() 最关键的前两步检查:
| |
版本检查确保模块是为相同主版本编译的。签名检查则细粒度得多,,NGX_MODULE_SIGNATURE 是一个在编译时通过宏拼接的字符串,包含 35 个标志位:指针大小(4/8)、原子类型大小、time_t 大小、kqueue/epoll/inet6/linux 特性的启用状态、PCRE/SSL/GZIP 支持等。任意差异都会导致签名不匹配。这避免了把一个在 --with-pcre 下编译的模块加载到没有 PCRE 的主程序中。
符号冲突风险与隔离#
dlopen 使用了 RTLD_GLOBAL,这意味着 .so 中定义的所有全局符号都会进入主程序的动态符号表。如果两个 .so 定义了同名函数(比如都有 ngx_http_module_ctx),后加载的那个会覆盖前面的。这在实际生产中是真实风险。
Nginx 的缓解策略很有限:
ngx_add_module()会检查模块名是否重复,但这是针对module->name字符串,不是 C 符号。- 模块作者应尽量使用
static限定内部符号,导出符号最好加上模块特定的前缀。 ngx_module_order允许配置模块加载顺序,间接缓解部分依赖问题。
本质上 Nginx 并没有实现真正的符号隔离,,这是 C 语言动态链接的固有局限。如果你需要完全的模块隔离,可以考虑 RTLD_LOCAL 配合手动导出,但 Nginx 选择了 RTLD_GLOBAL 以简化模块间符号共享。
commands 数组:配置驱动的引擎#
模块通过 commands 数组告诉配置解析器自己能处理哪些指令。ngx_command_t 的结构:
| |
配置解析器在读取 nginx.conf 时,每当遇到一个指令名,就在所有模块的 commands 数组中线性查找。找到后调用 set 函数指针,传入当前 ngx_conf_t 上下文和待处理的配置结构体。
ngx_core_module 的 ngx_core_commands 包含指令如 daemon、worker_processes、pid、load_module 等。加载动态模块的 load_module 本身就是一条指令,由 ngx_core_commands 配置并指向 ngx_load_module。
这种设计让 Nginx 的配置解析极度模块化,,新增一个功能只需要写一个新的 ngx_module_t 变量,填充 commands 数组和对应的 set 函数,配置解析器完全不需要修改。
系列总结#
至此,这个 Nginx 源码解析系列全部 12 篇完成了。回顾整个系列的脉络:
整体架构总览,,代码树结构、核心类型系统、模块体系、启动流程、整体数据流。所有后续分析的基础全景图。
进程模型与生命周期,,Master-Worker 进程模型、
ngx_master_process_cycle/ngx_worker_process_cycle、信号处理、平滑升级(USR2 + WINCH)和热加载。事件驱动核心,—
ngx_process_events_and_timers()主循环、epoll 封装、定时器红黑树、事件驱动模型如何支撑百万并发。内存管理,,
ngx_pool_t内存池(ngx_create_pool/ngx_palloc/ngx_destroy_pool)、大内存块管理、小内存块的 pool 分配链和释放机制。配置解析系统,,
ngx_conf_parse()的词法/语法分析、指令分派、配置上下文栈、Nginx 指令三层作用域(main/srv/loc)。HTTP 模块与请求处理,—11 个 HTTP 处理阶段、phase handlers 的注册与调用顺序、request 结构体生命管理周期。
Upstream 与负载均衡,,upstream 的 server 选择、健康检查、
ngx_http_upstream_init的上下游数据流、失败重试策略、peer.get()接口抽象。连接管理,,
ngx_connection_t的设计、预分配连接池、cycle->files[]的 fd->connection 映射、客户端连接和 upstream 连接的连接生命周期。事件模块与定时器,,
ngx_event_actions_t接口层(add/del/enable/disable)、ngx_event_timer.c的红黑树实现、定时器事件分发、ngx_usec时间缓存。共享内存与锁,,slab allocator 的分区与 page 管理、
ngx_shmtx_t自旋锁实现、accept 互斥锁与惊群解决。配置解析进阶与变量系统,,变量索引、
ngx_http_variable_t的 get/set 回调、变量哈希表的构建、SSI 变量、$arg_XXX动态变量实现。模块系统与动态加载,—
ngx_module_t完整结构、ngx_modules全局数组、ngx_count_modules()和ngx_init_modules()的初始流程、dlopen/dlsym动态加载、版本签名检查、符号冲突风险。
从第一篇文章俯瞰全景,到后面每篇文章深入一个子系统的实现细节,再到最后一篇收束回模块系统这个架构核心,—读完这 12 篇,你应该能清晰地回答 Nginx 最本质的问题:一个 18 万行 C 代码的 Web 服务器,是如何在极简的抽象下实现极致性能与极致灵活性的。
参考#
- Nginx 1.24.x 源码:
src/core/ngx_module.h/src/core/ngx_module.c/src/core/nginx.c/src/os/unix/ngx_dlopen.c - Nginx 官方开发指南: https://nginx.org/en/docs/dev/development_guide.html
src/core/ngx_conf_file.h, ngx_command_t 定义src/event/ngx_event.h, NGX_EVENT_MODULEsrc/http/ngx_http_config.h, NGX_HTTP_MODULEsrc/stream/ngx_stream.h, NGX_STREAM_MODULEsrc/mail/ngx_mail.h, NGX_MAIL_MODULE

