一、概要
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进程就准备好接收处理下一个请求了。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。