[PHP-FPM] 一次请求过程的解析
一、概要
PHP-FPM启动后,master进程会陷入event_loop(0)
中来管理维持worker进程,而fork出的worker进程会回到主函数开始循环接收、处理请求。一次请求可以总结为 请求接收、请求处理、请求结束 三个阶段,下面就详细来讲一下。
运行环境: Mac 10.14.2 + PHP 7.3.7
二、请求接收阶段
- 对listen_socket加锁: 因为
accept()
会有惊群问题,在调用accept()
之前会对listen_socket加锁。惊群问题在Linux2.6版本中得到解决,内核在收到一个客户端连接时只会唤醒等待队列上的第一个进程。 - 获取client_socket: worker进程会调用
accept(listen_socket, (struct sockaddr *)&sa, &len)
从全连接队列中接受一个连接,如果队列中暂时没有则会一直阻塞着,这里的listen_socket
是在fcgi_listen()
中创建监听的。 - 判断client_socket是否被允许: 满足如下请求之一即可
- client_socket为unix_socket,表明客户端为本机
- 客户端地址在allowed_clients列表里,allowed_clients是通过listen.allowed_clients参数配置
- 等待client_socket上的可读事件发生: 在do-while循环中调用
poll()
来监听client_socket上的可读事件,这里的while条件是while (ret < 0 && errno == EINTR);
EINTR错误是当阻塞中的poll()
被捕获到的信号中断所产生的错误,所以可以重新执行poll
系统调用。 - 读取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运行过程
- 词法语法分析,生成AST: 这一步的目的是生成抽象语法树AST,AST是PHP7引入的概念,PHP7之前是在语法分析后就直接生成opcode了。在这过程中语法分析器yacc不断调用词法分析器re2c将PHP代码切割为token,然后yacc根据token组合匹配语法规则,最终生成AST。
- 解析AST,生成zend_op_array:这一步的目的是生成
zend_op_array
,zend_op_array
是编译后所有opline指令的集合,也包括编译期间生成的关键数据。对于ZendVM而言,zend_op_array
就是可执行数据。 - ZendVM执行zend_op_array:
zend_op_array
作为ZendVM编译器的输出,也是ZendVM执行器的输入。执行时,ZendVM执行器会调用opcode相应的handler完成指令的处理,其中handler是每条opcode对应的C语言编写的处理逻辑。
四、请求结束阶段
- 执行用户通过
register_shutdown_function()
注册的关闭函数 - 释放资源,清理符号表,销毁超全局变量,重置max_execution_time 等等
- 冲刷掉所有缓冲区
- 执行每个扩展的
PHP_RSHUTDOWN_FUNCTION
函数 - …...
经过以上的清理操作,worker进程就准备好接收处理下一个请求了。
本作品采用《CC 协议》,转载必须注明作者和本文链接