【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() 系统调用:

  1. 内核创建进程 P 的完整副本
  2. 包括:代码、数据、堆栈、寄存器状态、文件描述符表
  3. 创建新进程 C(Child)
  4. 设置 C 的 PCB(进程控制块)
  5. 将 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_passlocationupstream 等等。

我们在这个过程一定会发现(通过查看调用栈,逐层往下,直至底层),Nginx 的设计一定是模块化的。


Nginx 处理多进程的惊群问题

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


那么,问题来了,一个访问请求发送到来,那么多个进程究竟哪个能够成功的接收到这个信息呢?难道是多进程一起去争抢吗(这其实就是惊群效应)?如果是那样,那程序的效率就太低了。

实际上,Nginx 在设计上是避免了惊群效应的。Nginx 采用 SO_REUSEPORT 策略,通过内核哈希映射实现多进程间的负载均衡,从而从根本上避免多进程间的惊群效应。(四元组的 hash 映射)

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

相关推荐
funnycoffee1231 天前
linux系统DNS修改命令
linux·运维·服务器·linux dns
袁小皮皮不皮1 天前
数据通信20-IPv6基础
运维·服务器·网络·网络协议·智能路由器
推理幻觉1 天前
ssh远程连接错误
运维·ssh
2401_858286111 天前
OS55.【Linux】理解信号量(不是信号)
linux·运维·服务器·计数器·信号量
SakitamaX1 天前
KEEPALIVED介绍与实验与介绍
运维·keepalived
楼田莉子1 天前
Linux学习:线程的同步与互斥
linux·运维·c++·学习
小草儿7991 天前
PG18备份恢复
linux·运维·服务器
Mikowoo0071 天前
KaLi系统基本使用
运维·服务器
Starry_hello world1 天前
Linux http代码
linux·运维·http
wuxi_joe1 天前
中国装备制造企业如何出海:以“配置管理”为核心构建全球竞争力
运维·人工智能·制造