您现在的位置是:烟消火灭网 > 综合
nginx源码解析
烟消火灭网2026-10-17 07:48:57【综合】9人已围观
简介nginx 的 master 进程启动后,第一件事是解析配置文件。`ngx_init_cycle` 这个函数在 `src/core/ngx_cycle.c` 里,差不多一千行,干的活却极其杂:读配置、
nginx 的码解 master 进程启动后,第一件事是码解解析配置文件。`ngx_init_cycle` 这个函数在 `src/core/ngx_cycle.c` 里,码解差不多一千行,码解干的码解活却极其杂:读配置、建监听套接字、码解初始化共享内存、码解打开日志文件,码解全塞在一起。码解我第一次读的码解时候觉得这函数太臃肿,后来才明白——它本质上就是码解一次“把配置变成运行时数据结构”的过程,没有更干净的码解办法。
配置文件解析走的码解是递归下降。`ngx_conf_parse` 逐字符扫描,码解遇到 token 就交给对应模块的码解指令处理函数。每个模块注册自己的 `ngx_command_t` 数组,里面写清楚指令名、参数个数、回调函数。比如 `worker_processes` 这条指令,定义在 `ngx_core_commands` 里,回调是 `ngx_set_worker_processes`。这个设计的好处是模块完全自治,核心不需要知道任何模块的配置长什么样。
但有个细节容易被忽略:nginx 的配置解析分两阶段。第一阶段只处理核心配置和确定模块加载顺序,第二阶段才真正调用各模块的 `create_conf` 和 `init_conf`。为什么?因为有些配置项的值依赖其他模块的配置结果。比如 `worker_connections` 的上限受 `worker_rlimit_nofile` 影响。这种依赖关系逼着 nginx 把配置分成“结构创建”和“值填充”两步走。
再说事件模块。nginx 支持 epoll、kqueue、select 等多种 IO 多路复用机制,靠的是 `ngx_event_actions_t` 这个函数指针表。编译时通过宏判断操作系统,运行时直接调对应的实现。以 epoll 为例,`ngx_epoll_init` 创建 epoll 实例,`ngx_epoll_add_event` 把 fd 注册进去。每次事件触发后,`ngx_epoll_process_events` 遍历就绪数组,把事件分发给对应的 handler。
这里有个设计我觉得挺妙:nginx 把读事件和写事件分开管理。`ngx_event_t` 结构体里同时有 `read` 和 `write` 两个 handler 指针。一个连接可能同时注册了读写事件,但 epoll 返回时只关心哪个 fd 就绪,具体是读还是写由事件结构体自己判断。这样避免了每次事件触发都去查 fd 对应的连接对象。
内存管理是另一个值得看的地方。nginx 用内存池(`ngx_pool_t`)而不是直接 malloc。每个连接创建时分配一个 pool,连接关闭时整个 pool 销毁。小内存块从 pool 的 `d` 链表上切,大块单独分配并挂在 `large` 链表上。这种设计在请求处理这种生命周期明确的场景下非常高效,因为不需要逐个 free,也不会产生碎片。
但内存池有个坑:如果某个请求处理过程中分配了大量内存,pool 不会自动回收,直到连接关闭。所以 nginx 在 HTTP 请求处理里又搞了个 `ngx_http_request_pool`,请求结束时清理。两层 pool 嵌套,读代码时容易搞混哪个 pool 管哪段生命周期。
真正让 nginx 扛住高并发的,不是某个单点技术,而是整套协作方式。master 进程不碰网络 IO,只负责管理 worker。worker 进程各自独立 accept 新连接,靠的是 `ngx_accept_mutex` 这把锁来避免惊群。每个 worker 在一个事件循环里处理成千上万个连接,没有线程切换开销,也没有锁竞争——除了那个 accept 锁。
读 nginx 源码最累的部分是各种回调。一个 HTTP 请求从读到写,中间经过十几个状态机切换,每个状态都可能注册新的回调。`ngx_http_process_request_line` 读完请求行后跳到 `ngx_http_process_request_headers`,读完头部又跳到 `ngx_http_handler`,然后进入 phase 引擎。第一次跟下来会觉得绕,但理解了状态机模型之后,反而觉得这种写法比同步阻塞清晰得多。
很赞哦!(71)







