文章目录
- 前言
- [Nginx 是多进程模型](#Nginx 是多进程模型)
- [Nginx 的模块化设计](#Nginx 的模块化设计)
- [Nginx 处理多进程的惊群问题](#Nginx 处理多进程的惊群问题)
推荐一个零声教育学习教程,个人觉得老师讲得不错,分享给大家:[Linux,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK等技术内容,点击立即学习: https://github.com/0voice 链接。
前言
我在上一篇文章里面介绍了 Nginx 是如何简单地启动的,【Nginx 网关开发】上手 Nginx,简简单单启动一个静态 html 页面。这篇文章,我将会以 conf 文件为线索,去查询 Nginx 的源码,期望能捕捉到一些运行的细节。
老规矩,先给出一段关于 Nginx 的八股文介绍。

Nginx(发音为engine-x)是一个异步事件驱动架构的高性能HTTP及反向代理服务器,同时提供IMAP/POP3代理服务。其核心架构采用多进程模型(主进程+工作进程)与非阻塞 I/O 事件处理机制,通 epoll/kqueue/IOCP等系统调用实现高并发连接处理能力。
Nginx 是多进程模型
可执行程序与进程是一对多的关系,一个可执行程序能同时被实例化为多个进程,与八股文那一句 "其核心架构采用多进程模型(主进程+工作进程)" 产生对应,我们就可以得出一个结论,"主进程" 与 "工作进程" 都是同一个程序的实例。那此时,我们就想问了,为什么同一个程序主进程和工作进程工作的内容会不一样?
Nginx 主进程:进程监控、配置验证,只在信号处理、fork() 时短暂使用 CPU
Nginx 工作进程:具体的服务器反向代理工作
bash
worker_processes 2;
events {
worker_connections 1024;
}
http {
upstream backend {
server 192.168.152.128:9002 weight=1;
server 192.168.152.128:9003 weight=2;
}
server {
listen 9000;
location / {
proxy_pass http://backend;
}
}
server {
listen 9001;
location / {
proxy_pass http://backend;
}
}
server {
listen 9002;
location / {
root html/html9002/;
}
}
server {
listen 9003;
location / {
root html/html9003/;
}
}
}
这是来自我上一篇文章的 nginx 配置文件,我们注意到 worker_processes 其实就是多进程的配置关键字,以此为线索,我们在 vscode 里面打开 nginx 的源码,使用搜索功能完成多进程机制的探索拆解。

先搜索 worker_processes ,注意,我搜的是字符串,因为程序读取配置文件内容是字符串的

于是找到了 nginx.c 里面的 static ngx_command_t ngx_core_commands[] 命令数组,发现这个关键字有一个专门的回调函数 ngx_set_worker_processes,还有一个奇奇怪怪的结构体
c
{ ngx_string("worker_processes"),
NGX_MAIN_CONF|NGX_DIRECT_CONF|NGX_CONF_TAKE1,
ngx_set_worker_processes,
0,
0,
NULL },
这个 ngx_command_t 到底是什么东西呢?
c
// src/core/ngx_conf_file.h
struct ngx_command_s {
ngx_str_t name; // 指令名称(如 "worker_processes")
ngx_uint_t type; // 指令类型(位掩码)
char *(*set)(ngx_conf_t *cf, ngx_command_t *cmd, void *conf); // 处理函数
ngx_uint_t conf; // 标识配置应该存储在哪个模块的配置结构体中
ngx_uint_t offset; // 指定配置项在结构体中的字节偏移量
void *post; // 配置解析后的验证、转换或初始化
};
typedef struct ngx_command_s ngx_command_t;
NGX_MAIN_CONF|NGX_DIRECT_CONF|NGX_CONF_TAKE1的含义:
NGX_MAIN_CONF:指令只能用在配置文件的最外层(不在任何块内)
NGX_DIRECT_CONF:指令是简单的"参数+分号"形式,没有花括号块
NGX_CONF_TAKE1:指令需要且只需要1个参数
上面那个指令 worker_processes 的作用流程
- 1、分词:读取配置文件里面的 "
worker_processes 2;" 并分解
2、查找:在指令表ngx_core_commands中找到worker_processes的定义
3、验证type:
------------上下文:必须在主配置区
------------参数数量:必须有1个参数
------------指令类型:必须是直接配置
4、定位:由于字段conf=0,找到ngx_core_conf_t结构体作为存储位置
5、处理:调用自定义的ngx_set_worker_processes()函数
------------不作后续处理
------------存储到ccf->worker_processes
6、完成:返回NGX_CONF_OK,继续解析下一行
这个指令只是做设置的功能。
以此类推,我们按 "堆栈" 展开搜索,从 "worker_processes" 到变量 ngx_core_commands 再到变量ngx_core_module,于是我们去到了一个关键点,我们要找到一个合适的方向展开搜查

我们逐个去挑,按照变量函数的具体语义加以判断,发现按照函数 ngx_master_process_cycle(ngx_cycle_t *cycle) 这个栈往下走(一看就知道这个函数跟 "主进程有关"),我们发现他在主函数里面被调用 int ngx_cdecl main(int argc, char *const *argv),而且主函数里面有以下这两个函数

至此,关于 nginx 多进程的函数-变量栈都找完了,我们现在就要分析这些函数是怎么导致程序的多进程实例运行。
我们针对这个 ngx_master_process_cycle 函数(多进程运行才使用这个函数),探查里面的结构

发现其里面还使用了 ngx_start_worker_processes 函数,里面确确实实传入了进程的数量 ccf->worker_processes 作为参数。 ngx_start_worker_processes 里面调用了 ngx_spawn_process 函数

于是,我们发现了 Nginx 其实是使用了 fork 进行子进程分叉

关于 fork,调用前:只有一个进程,我们称它为 P(Parent)
fork() 系统调用:
- 内核创建进程 P 的完整副本
- 包括:代码、数据、堆栈、寄存器状态、文件描述符表
- 创建新进程 C(Child)
- 设置 C 的 PCB(进程控制块)
- 将 C 加入就绪队列
返回时:
- 在父进程 P 中:返回子进程 C 的 PID(>0)
- 在子进程 C 中:返回 0
故而,我们可以得出一个重要结论,nginx 的主进程与工作子进程是属于同一个可执行程序,工作内容之所以不同就是进程分叉,利用进程号 pid 差异来选择性执行任务,导致功能分化(就像高中生物教的干细胞特异化一样)

Nginx 主进程的作用
- 控制与执行分离:架构清晰,各司其职
无缝热部署:配置更新、二进制升级无需停机
高可用保障:Worker崩溃自动重启,服务不中断
特权隔离:Worker以非特权用户运行,提升安全性
资源管理:统一管理共享资源(内存、端口)
监控中心:收集统计信息,提供管理接口
父进程与子进程的关系
有一个机制叫做 "写时复制"(Copy-On-Write, COW)
- 当运行
fork()函数,生成一个子进程时,子进程按 COW 机制共享父进程的资源
1、操作系统无法预知哪些页会被修改,所以子进程共享的内容先全部标记为 COW
2、按需复制:只有实际被修改的页才会触发复制
3、永久的只读页(如代码):永远共享,引用计数可能很高
4、从未被写的 COW 页:在进程整个生命周期保持共享
5、一旦某个进程写入某页:该页对该进程变为私有,但其他进程仍然引用原页
父进程和子进程的关系
- 资源继承关系:
子进程继承父进程的代码段、数据段、堆栈、环境变量
继承打开的文件描述符(如监听的 socket)
继承信号处理器设置
回到配置文件
回到配置文件,我的配置文件里面写了 4 个服务器,那对于多进程来说又是怎么一回事呢?
答案就是:每个进程都开启相同数量的服务器,分别监视同一批的端口

bash
worker_processes 2;
events {
worker_connections 1024;
}
http {
upstream backend {
server 192.168.152.128:9002 weight=1;
server 192.168.152.128:9003 weight=2;
}
server {
listen 9000;
location / {
proxy_pass http://backend;
}
}
server {
listen 9001;
location / {
proxy_pass http://backend;
}
}
server {
listen 9002;
location / {
root html/html9002/;
}
}
server {
listen 9003;
location / {
root html/html9003/;
}
}
}
Nginx 的模块化设计
Nginx 的源码挖掘流程如上所述,我们还可以去挖掘其他关键字的意涵,诸如 proxy_pass、location 和 upstream 等等。
我们在这个过程一定会发现(通过查看调用栈,逐层往下,直至底层),Nginx 的设计一定是模块化的。

Nginx 处理多进程的惊群问题
前面说到过,Nginx 是多进程模型,所有工作进程本质上都是一样的,都监听着同样的端口,为同一批服务器、前端页面做反向代理服务。

那么,问题来了,一个访问请求发送到来,那么多个进程究竟哪个能够成功的接收到这个信息呢?难道是多进程一起去争抢吗(这其实就是惊群效应)?如果是那样,那程序的效率就太低了。
实际上,Nginx 在设计上是避免了惊群效应的。Nginx 采用 SO_REUSEPORT 策略,通过内核哈希映射实现多进程间的负载均衡,从而从根本上避免多进程间的惊群效应。(四元组的 hash 映射)

有一点需要注意的是,同一个连接通过 upstream 机制被代理到不同服务器上面,这是另一种意义的 "负载均衡"。