乐趣区

PHPFPM系列一次请求的过程

一、概要

PHP-FPM 启动后,master 进程会陷入 event_loop(0) 中来管理维持 worker 进程,而 fork 出的 worker 进程会回到主函数开始循环接收、处理请求。一次请求可以总结为 请求接收、请求处理、请求结束 三个阶段,下面就详细来讲一下。

运行环境:Mac 10.14.2 + PHP 7.3.7

二、请求接收阶段

  1. 对 listen_socket 加锁:因为 accept() 会有惊群问题,在调用 accept() 之前会对 listen_socket 加锁。惊群问题在 Linux2.6 版本中得到解决,内核在收到一个客户端连接时只会唤醒等待队列上的第一个进程。
  2. 获取 client_socket:worker 进程会调用 accept(listen_socket, (struct sockaddr *)&sa, &len) 从全连接队列中接受一个连接,如果队列中暂时没有则会一直阻塞着,这里的 listen_socket 是在 fcgi_listen() 中创建监听的。
  3. 判断 client_socket 是否被允许:满足如下请求之一即可

    1. client_socket 为 unix_socket,表明客户端为本机
    2. 客户端地址在 allowed_clients 列表里,allowed_clients 是通过 listen.allowed_clients 参数配置
  4. 等待 client_socket 上的可读事件发生:在 do-while 循环中调用 poll() 来监听 client_socket 上的可读事件,这里的 while 条件是 while (ret < 0 && errno == EINTR); EINTR 错误是当阻塞中的poll() 被捕获到的信号中断所产生的错误,所以可以重新执行 poll 系统调用。
  5. 读取 client_socket 中的数据:这里是对 FastCGI 协议的一个实现,Nginx 会按照 FastCGI 协议的消息格式发送数据,worker 进程再按照协议多次 read() 数据并解析,消息传递大致如下。关于 PHP 如何实现 FastCGI 协议可以看下这篇文章。

三、请求处理阶段

初始化

在上一阶段读取到请求数据后,worker 进程接着会初始化输出相关的堆栈、初始化编译阶段用到的 compiler_globals(CG 宏)、执行阶段用到的 executor_globals(EG 宏)、执行每个扩展的 PHP_RINIT_FUNCTION 函数 等等。

ZendVM

讲到请求处理阶段就不得不提 ZendVM,大家都知道 PHP 是解释型语言,ZendVM 就是 PHP 的解释器,负责 PHP 的解析、执行。计算机理解不了 PHP 代码,但是 ZendVM 可以,对 PHP 而言,ZendVM 就像是真正的“计算机“,这台“计算机“可以识别的指令就是自己事先定义好的 opcode。在运行时,PHP 会被编译为一系列 opcode 指令,ZendVM 会逐个调用 opcode 对应的机器指令,最终完成 PHP 代码的运行。

ZendVM 运行过程

  1. 词法语法分析,生成 AST:这一步的目的是生成抽象语法树 AST,AST 是 PHP7 引入的概念,PHP7 之前是在语法分析后就直接生成 opcode 了。在这过程中语法分析器 yacc 不断调用词法分析器 re2c 将 PHP 代码切割为 token,然后 yacc 根据 token 组合匹配语法规则,最终生成 AST。
  2. 解析 AST,生成 zend_op_array:这一步的目的是生成 zend_op_array,zend_op_array 是编译后所有 opline 指令的集合,也包括编译期间生成的关键数据。对于 ZendVM 而言,zend_op_array 就是可执行数据。
  3. ZendVM 执行 zend_op_array:zend_op_array 作为 ZendVM 编译器的输出,也是 ZendVM 执行器的输入。执行时,ZendVM 执行器会调用 opcode 相应的 handler 完成指令的处理,其中 handler 是每条 opcode 对应的 C 语言编写的处理逻辑。

四、请求结束阶段

  1. 执行用户通过 register_shutdown_function() 注册的关闭函数
  2. 释放资源,清理符号表,销毁超全局变量,重置 max_execution_time 等等
  3. 冲刷掉所有缓冲区
  4. 执行每个扩展的 PHP_RSHUTDOWN_FUNCTION 函数
  5. ……

经过以上的清理操作,worker 进程就准备好接收处理下一个请求了。

退出移动版