phpfpm在接收到fastcgi协议后或者直接通过php执行脚本,zend engine内部处理过程
1. ZendEngine
- Zend引擎:充当PHP的核心和大脑
- Zend引擎:是由Technion - Israel Institute of Technology(以色列)的两名学生开发的虚拟机。
- Zend引擎由三个主要组件组成:
- Lexical analyzer or the Lexer(词法分析) - 负责对脚本进行标记
- Parser(解析器) - 解析令牌并生成操作码或opnums
- Executor(执行程序) - 执行操作码。
- Zend引擎包含以下内部组件:
- ZFMI或Zend功能模块接口 - 不同模块之间的通信通道
- 操作码缓存 - 缓存操作码,以便在需要时可以重复使用(Opcache模块支持)
(Web服务器以及PHP底层执行涉及模块)
- PHP提供了请求处理和其他Web服务器的接口(SAPI)
- Zend引擎是PHP实现的核心,提供了语言实现上的基础设施(词法、语法解析、扩展机制、内存管理
- ZendAPI,目前PHP的实现和Zend引擎之间的关系非常紧密,甚至有些过于紧密了,PHP的扩展大都都采用ZendAPI耦合一起
参考:
- http://www.laruence.com/2008/07/16/225.html
- http://www.laruence.com/2008/06/18/221.html
- http://www.php-internals.com/book/?p=chapt02/02-01-php-life-cycle-and-zend-engine
2. SAPI
SAPI(Server Application Programming Interface)指的是PHP具体应用的编程接口,PHP脚本要执行有很多种方式,通过Web服务器,或者直接在命令行下,也可以嵌入在其他程序中,脚本执行的开始都是以SAPI接口实现开始的。
整个SAPI类似于模板模式,SAPI.c
和SAPI.h
文件所包含的一些函数就是模板方法模式中的抽象模板;
- 初始化全局、常量、ZendEngine核心、ini解析、
- 模块初始化阶段(MINIT)
- 模块激活阶段(RINIT)
- 请求到达,处理开始
- PHP初始化执行脚本的基本环境(包括保存PHP运行过程中变量名称和值内容的符号表, 以及当前所有的函数以及类等信息的符号表)
- 调用所有模块的RINIT函数,完成请求
- 模块关停节点(RSHUTDOWN)
- 进入结束阶段(MSHUTDOWN)
2.1. 图示SAPI生命周期
(多进程SAPI)
(多线程SAPI)
2.2. Apache运行php
Apache以mod_php5
模块的形式集成PHP,PHP模块的作用是接收Apache传递过来的PHP文件请求,并处理这些请求, 然后将处理后的结果返回给Apache。
- Apache启动之前,配置好PHP模块
- 启动阶段,获取系统资源,整个过程处于一个单进程单线程的环境中(包含
httpd.conf
解析、mod_php
模块加载、系统资源初始化-日志、内存、共享段等) - 运行阶段,Apache主要工作是处理用户的服务请求,基于安全,使用普通用户处理,处理过程包括连接、处理、断开3个大的阶段,细分为:
- 读请求:Post-Read-Request、URI转义、Header 解析
- 认证授权:Access Control、Authentication、Authorization
- 内容:MIME Type Checking、FixUp、
- 响应:Response
- 清理:Logging、CleanUp
- Apache处理请求对象为
request_rec
,逻辑web服务器/虚拟机对象为server_rec
,管理连接对象为conn_rec
3. FastCGI运行php
- CGI为通用网关缩写,CGI描述了客户端和这个程序之间传输数据的一种标准,可以用于执行客服端发送给服务端应用程序的请求数据,
- FastCGI是Web服务器和处理程序直接的协议,与语言无关,CGI的改进(常驻型CGI,解决重复加载的问题),CGI接受FastCGI进程管理器的调度
FastCGI 是与语言无关的、可伸缩架构的 CGI 开放扩展,将 CGI 解释器进程保持在内存中,以此获得较高的性能。 CGI 程序反复加载是 CGI 性能低下的主要原因,如果 CGI 程序保持在内存中并接受 FastCGI 进程管理器调度, 则可以提供良好的性能、伸缩性、Fail-Over 特性等。
FastCGI进程管理器自身初始化,启动多个CGI解释器进程,并等待来自 Web Server 的连接。Web服务器与FastCGI进程管理器进行 Socket 通信,通过FastCGI协议发送CGI环境变量和标准输入数据给CGI解释器进程。CGI解释器进程完成处理后将标准输出和错误信息从同一连接返回 Web Server。CGI 解释器进程接着等待并处理来自Web Server的下一个连接
通常的Nginx+PhpFPM就是通过Web服务器将数据基于fastCGI协议封装,通过Socket与FastCGI子进程(由FastCGI主进程管理器启动)交互,进行php的脚本执行。
PHP的CGI实现了FastCGI协议,通过绑定TCP或UDP协议的服务,接收来至Web服务器的请求,随后进行PHP的生命周期。
(web服务器与fastcgi进程管理器)
参考:
3.1. php-src/main/fastcgi.h
FastCGI协议:
- 是FastCGI二进制协议,有统一结构的消息头
- FastCGI请求时序:
FCGI_BEGIN_REQUEST
、FCGI_PARAMS
、FCGI_STDIN
、FCGI_STDOUT
、FCGI_STDERR
、FCGI_END_REQUEST
- FastCGI协议头,包含协议版本、类型、请求ID、请求长度等信息
- 协议请求状态:正常处理/过载拒绝/限流拒绝/未知拒绝
|
|
4. TCP为例,PHP中CGI的生命周期
- 调用socket函数创建一个TCP用的流式套接字;
- 调用bind函数将服务器的本地地址与前面创建的套接字绑定;
- 调用listen函数将新创建的套接字作为监听,等待客户端发起的连接,当客户端有多个连接连接到这个套接字时,可能需要排队处理;
- 服务器进程调用accept函数进入阻塞状态,直到有客户进程调用connect函数而建立起一个连接;
- 当与客户端创建连接后,服务器调用read_stream函数读取客户的请求;
- 处理完数据后,服务器调用write函数向客户端发送应答。
(TCP上处理Web服务器的请求事务的时序)
4.1. php-src/sapi/cgi/cgi_main.c
sapi中的cgi程序中的cgi_main.c
的main函数中,会执行下述socket监听代码(通过调用php-src/main/fastcgi.c
中的fcgi_listen
函数实现)
|
|
- init_request_info函数,初始化fastcgi请求
- php_request_startup函数,处理fastcgi请求
- fcgi_finish_request函数,接收fastcgi请求
- fcgi_destroy_request,请求销毁
- php_module_shutdown,模块关闭
- sapi_shutdown,sapi关闭
4.2. php-src/main/fastcgi.c
- fcgi_listen函数
|
|
- fcgi_accept_request函数,接收一个fastcgi请求处理:
|
|
- fcgi_read_request函数,读取fastcgi相关信息:
|
|
4.3. php-src/main/main.c
CGI通过接收fastcgi请求,进行相关初始化后,若为PHP_MODE_STANDARD
,会调用php_execute_script(zend_file_handle *primary_file)
函数
|
|
5. PHP脚本执行
PHP的SAPI,SAPI处于PHP整个架构较上层,而真正脚本的执行主要由Zend引擎来完成,编程语言分为编译或者解释性语言,php属于解释性语言,需要通过解释器将php源码,编译成指定的opcode码执行,如果装了opcode缓存扩展(如APC, xcache, eAccelerator等),该编译环境可以省略,直接进入opcode码执行阶段,避免每次运行重新进行编译所带来的性能损失。
分析和执行的过程需要经过:
- 读取到脚本文件后首先对代码进行词法分析(lex),将源代码(
*.php
文件内容)按照词法规则切分一个一个的标记(token),ps:切好的内容可以通过PHP中提供函数token_get_all()查看 - 使用语法分析器(PHP使用bison生成语法分析器),将代码编译为opcode
- Zend引擎会执行这些opcode,在执行opcode的过程中还有可能会继续重复进行编译-执行(比如执行eval,include/require等语句)
如果想直接查看生成的Opcode,可以使用php的vld扩展查看。
5.1. php-src/Zend/zend.c
再到zend_execute_scripts
函数,就转到Zend引擎内部执行
|
|
6. Opcode - 字节码
ZendEngine做到事情:
- Scanning(Lexing) ,将PHP代码转换为语言片段(Tokens)
- Parsing, 将Tokens转换成简单而有意义的表达式
- Compilation, 将表达式编译成Opocdes
- Execution, 顺次执行Opcodes,每次一条,从而实现PHP脚本的功能。
(PHP脚本执行过程)
opcode是计算机指令中的一部分,用于指定要执行的操作, 指令的格式和规范由处理器的指令规范指定。
除了指令本身以外通常还有指令所需要的操作数,可能有的指令不需要显式的操作数。 这些操作数可能是寄存器中的值,堆栈中的值,某块内存的值或者IO端口中的值等等。
通常opcode还有另一种称谓: 字节码(byte codes)。 例如Java虚拟机(JVM),.NET的通用中间语言(CIL: Common Intermeditate Language)等等。
PHP是构建在Zend虚拟机(Zend VM)之上的,PHP的opcode就是Zend虚拟机中的指令。
6.1. opcode操作指令
和CPU的机器指令类似,有一个标示指令的opcode字段,以及这个opcode所操作的操作数,PHP不像汇编那么底层, 在脚本实际执行的时候可能还需要其他更多的信息,extended_value字段就保存了这类信息, 其中的result域则是保存该指令执行完成后的结果。
每条opcode都有一个opcode_handler_t的函数指针字段,用于执行该opcode。
PHP有三种方式来进行opcode的处理:CALL,SWITCH和GOTO,PHP默认使用CALL的方式,也就是函数调用的方式, 由于opcode执行是每个PHP程序频繁需要进行的操作,可以使用SWITCH或者GOTO的方式来分发, 通常GOTO的效率相对会高一些,不过效率是否提高依赖于不同的CPU。
|
|
7. PhpFPM Model分析
7.1. main.c
FPM启动函数在fpm_main.c中
|
|
7.2. fpm_worker_pool.h
fpm进程池,进程池监听一个端口,进程池之间使用链表关联,有监听的套接字、uid、gid、日志事件等
|
|
7.3. fpm_children.h
进程池中的fpm子进程,有pid、自己归属pool等,fpm事件,双向链表
|
|
7.4. phpfpm 池
8. SO_REUSEADDR 和 SO_REUSEPORT
可以通过
man 7 socket
查看到这两个参数的含义,另外可以参考man tcp
,tcp_tw_reuse
的含义
tcp_tw_reuse
:是否允许重用TIME_WAIT套接字,以便在从协议的角度来看是安全的情况下进行新的连接(不推荐更改)
8.1. SO_REUSEADDR
bind
的套接字是否可以重用本地地址
解决的问题:
- 解决
EADDRINUSE
的问题 - 解决TCP服务异常中断,如果监听的ip:port存在
主动断开
,在服务端会形成一个TIME_WAIT
状态,默认会持续2MSL,若不启用地址重用,则服务无法在同一个ip:port
上启用
|
|
8.2. SO_REUSEPORT
常规情况一个监听的套接字ip:port,多进程或多线程无法在同一ip:port套接字上进程listen监听,通过SO_REUSEPORT
可以做到在多进程或多线程服务器下,每个进程或线程有一个独立的套接字监听socket,独立的listen和accept,但绑定相同的ip:port,提高程序的并发能力。
SO_REUSEPORT是linux kernel 3.9 引入的,若是多进程套接字同端口监听,需要所有监听的进程都要设置(线程可以做到资源共享),另外基于安全考虑,进程的有效用户ID需要保证一致。
解决的问题:
- 通过每个进程或线程维护自己的套接字,可以避免惊群效应(多个线程从同一套接字竞争接收请求)
- 通过内核层面的负载均衡,可以做到每个进程或线程都是有自己独立的套接字,接收均衡的连接请求()
|
|
8.3. 惊群问题
惊群问题是计算机科学中,当许多进程等待一个事件,事件发生后这些进程被唤醒,但只有一个进程能获得CPU执行权,其他进程又得被阻塞,这造成了严重的系统上下文切换代价。
解决问题方式,基于IO多路复用技术:
- 不把所有进程都唤醒,仅wakeup一个进程/线程
- 尽量避免上下文的开销(绑定CPU处理机,CPU亲缘性)
参考:
8.4. IO多路复用技术
可以参考
man 7 epoll
、man 2 poll
复用技术最开始在电信和计算机网络中,主要分享稀缺资源,有对应的MUX和DEMUX/DMX。IDMUX还可实现数据分解成多个流在不同信道上传输,并重建它们。
通信领域,有涉及通信复用的技术:空分复用、频分复用、时分复用、码分复用,以及偏振分割多路复用和轨道角动量多路复用
在计算中,I/O multiplexing
,也可以指代通过单一循环事件(event loop
)处理多I/O event
,比如poll和select
poll,是在一个文件描述符上等待一个IO事件的发生,在文件符过多时候(通常通过数组存放被监控的描述符),系统调度的开销,对系统性能损耗较大
epoll,是一个内核调用的IO事件通知机制,相比poll机制,在对大量文件描述符监控下的IO事件,epoll可以做到O(1)的时间效率,而poll需要O(n)。epoll支持edge-triggered
(ET)和level-triggered
(LT)两种触发方式
参考:
9. 后续
暂时先写这么多,还有很多细节后续需要完善,后续有时间再把这个坑填上。
9.1. 补充:php7的zval调整
- php5中的zval包含值、类型、引用计数器等,opcache jit时候效率低,转而优化该zval值
- php5中的zval问题:
- zval是php engine2使用,zval.value联合体中zend_object_value可以优化掉(基于引用)
- zval未保留预定变量,在一些特点情况下导致需要额外能力来扩展zval
- zval除对象和资源外,都是按值传递,导致
ref_count
统计还需要全局引用计数器 - 写时分离,带来可能的性能问题
- zval含引用计数器,copy字符串类型也是需要值拷贝
- MAKE_STD_ZVAL/ALLOC_ZVAL内存申请分配带来性能问题
- php7中的zval:
- 全部改为联合体(value、u1-type、u2-辅助扩展部分)
- zval变成了一个值指针,它要么保存着原始值, 要么保存着指向一个保存原始值的指针
- zval中的value可以保存下的值,直接赋值,不进行引用计数,省掉一次指针解引用
php7性能提升,主要是基于对内存的优化:持续不断的降低内存占用,提高缓存友好性,降低执行的指令数3个方面带来优化效果
参考: