【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 机制被代理到不同服务器上面,这是另一种意义的 "负载均衡"。

相关推荐
怣502 小时前
Linux创意命令组合:让终端变得有趣又高效
linux·运维·服务器
啟明起鸣2 小时前
【Nginx 网关开发】上手 Nginx,简简单单启动一个静态 html 页面
运维·c语言·前端·nginx·html
MACKEI2 小时前
服务器流式传输接口问题排查与解决方案
python·nginx·流式
Tinyundg2 小时前
Linux系统分区
linux·运维·服务器
要做一个小太阳2 小时前
华为Atlas 900 A3 SuperPoD 超节点网络架构
运维·服务器·网络·华为·架构
江畔何人初2 小时前
service发现
linux·运维·云原生
life码农3 小时前
Linux系统清空文件内容的几种方法
linux·运维·chrome
zbguolei3 小时前
虚拟机安装Ubuntu后无法登录
linux·运维·ubuntu
UP_Continue3 小时前
Linux--基础IO
linux·运维·服务器