PHP作为开源脚本语言,具有C、Perl、Java等编程语言的特性,由于引入了中间解释层(Zend引擎),所以PHP是一门动态语言。
早期PHP是基于多进程模式,也就是单独的请求绑定唯一的进程,但在处理异步请求类的业务时则显得力不从心。随着发展,现在的PHP已经可以很好的支持多线程模型。
PHP依托Zend引擎、ZendExtensions扩展、SAPI隔绝层,实现了标准架构设计的基本要求。
- PHP使用“引擎+扩展”的模式,有效地降低了内部的耦合度。
- PHP使用中间层SAPI,有效的隔绝了Web服务器和PHP。
PHP体系结构
PHP四层架构由下向上,依次来看这四层的功能和作用:
1. Zend引擎
Zend使用纯C语言实现,是PHP的内核部分,它将PHP代码翻译为可执行的中间码opcode
,并实现了相应的处理方法。另外,Zend引擎实现了基本的数据结构、内存分配管理,并提供了相应的API供外部调用。Zend引擎是PHP的核心,所有的外围功能都围绕着它实现。
Zend引擎是PHP实现的核心,提供了语言实现上的基础设施,如PHP语法实现、脚本编译运行环境、扩展机制以及内存管理等。
Zend引擎最主要的特性是把PHP的边解释边执行方式改为预编译(Compile)再执行(Execute)方式,解释与执行的分离带给PHP革命性的变化,使得效率大幅提高,由于实现了功能分离,降低了模块之间的耦合度,可扩展性也大大增强。
PHP执行流程Zend引擎分为两部分:编译器和执行器
- 编译器负责将PHP代码编译为抽象语法树
AST
,然后进一步编译为可执行的中间代码opcode
,这个过程相当于GCC的工作,编译器是一个语言实现的基础。 - 执行器负责执行编译器输出的
opcode
,也就是执行PHP脚本中编写好的代码逻辑。opcode
是将PHP代码编译产生的Zend虚拟机可以识别的指令,PHP中所有语法实现都是由这些opcode
组成的。
PHP实现典型动态语言执行的过程
PHP源代码经过词法分析、语法解析等阶段后会被翻译成一条一条的指令(opcodes),然后Zend虚拟机顺序执行这些指令完成操作并实现功能。由于PHP本身是使用C语言实现的,因此最终调用的也都是C的函数,实际上是可以把PHP看作C开发的软件。
PHP执行的核心是翻译出一条条Opcode指令,Opcode作为PHP程序执行的最小单位,一个Opcode由两个参数、一个返回值和一个处理函数组成。PHP源代码最终被翻译成一组Opcode处理函数然后顺序执行。
PHP的生命周期- 词法分析
Scanning(Lexing)
:将PHP代码转换为语言片段Tokens
输入源代码,对构成源代码的字符串进行扫描和分解,将整个源码分解成一个个单词,如关键字、标识符、常量、运算符、标点符号、括号等。然后进行清洗源码,比如清楚空格、清除注释等。 - 语法解析
Parsing
:将Tokens
转换为简单而且有意义的表达式
在词法分析的基础上,根据语言的语法规则,将单词符号串分解成各类语法单位(语法范畴),如短语、子句、句子、程序段等。 - 编译
Compilation
:将表达式编译成Opcodes
- 语义分析与中间代码生成:对语法解析所识别出的各类语法范畴,分析其含义,并进行初步翻译,产生中间代码。
- 优化:对前段产生的中间代码进行加工变化,以便在最后阶段能产生出更高效的目标代码。
- 目标代码生成:将中间代码或经优化处理后的代码,变换成特定机器上的低级语言代码
- 执行
Execution
:顺序执行Opcodes
,每次一条,从而实现PHP脚本的功能。
Opcode
Opcode是PHP源代码经过Zend引擎编译后产生的中间代码,Opcode会交由Zend引擎去处理。如同C语言编译成汇编代码,然后再交由汇编编译处理一样。当然也是可以直接设置编译成二进制文件然后转为机器码,那么为什么不这么做呢?因为通过中间码环节可以将复杂的问题分步进行,并可以根据当前系统环境的不同来对opcode做进一步的优化,最后一步步地去解析和执行。另外,opcode作为中间语言可以帮助PHP源代码实现PHP源码不开源,可以使用APC截取生成opcode缓存文件,然后使用自己的PHP扩展加密程序对 opcode文件进行加密,最后在Zend引擎中对opcode进行解析前进行解密然后再执行。
Opcode CacheOpcode是一种PHP脚本编译后的中间语言,类似Java的Bytecode字节码或是.NET的MSL。现在有的PHP Cache缓存如APC、Opcache,可以使用PHP缓存Opcode。这样每次请求来临时,无需重复执行Lexing、Parsing、Compilation,而今大幅度提高PHP的执行速度。
Zend虚拟机
ZendVM(Virtual Machine)是一个虚拟的计算机,介于PHP应用于实际计算机之间。ZendVM的角色等价于Java中的JVM,都是抽象出来的虚拟计算机。与C/C++编译型语言不同的是,虚拟机上运行的指令并不是机器指令。虚拟机的一个突出的优点是跨平台,只需要按照不同平台编译出对应的解析器,就可以实现代码的跨平台执行。
PHP代码是由ZendVM解释执行的,它是PHP语言的核心实现,主要由2部分组成:编译器、解释器。编译器负责将PHP代码解释为执行器识别的指令,执行器负责执行编译器解释出的指令。
ZendVM体系结构从概念层上将ZendVM的实现进行抽象,可以将ZendVM体系结构划分为:解释层、执行引擎、中间数据层。
ZendVM操作流程
当一段PHP代码进入ZendVM时,它会执行两步操作:编译和执行。但这两步操作对于一个常规的执行过来说却是连续的,也就是说它并没有转变成和Java这种编译型语言一样,生成一个中间文件存放编译后的结果。如果每次执行这样的操作,对于PHP脚本的性能来说是一个极大的损失,虽然有类似于APC、eAccelerator等缓存解决方案,但本质上是没有变化的,并且不能将两个步骤分离,各自发展壮大。
- 解释层
解释层是ZendVM执行编译过程的位置,包括词法分析、语法解析、编译生成中间代码三个部分。词法分析就是将要执行的PHP源代码,去掉空格注释后切分为一个一个的标记,并且处理程序的层级机构。语法解析是将接收的标记序列,根据定义的语法规则来执行对应的动作,ZendVM现在使用的Bison巴克斯范式(BNF)来描述语法。编译生成中间代码是根据此法解析的结果对照Zend虚拟机制定的opcode生成中间代码。
- 中间数据层
当ZendVM执行PHP源代码时,它需要内存来存储许多东西,如中间代码、PHP自带函数列表、用户自定义函数列表、PHP自带的库、用户自定义的库、常量、程序创建的对象、传递给函数的方法的参数、返回值、局部变量以及一些运算的中间结果等。我们把这些存放数据的地方称之为中间数据层。
如果PHP以mod
扩展的方式依附于Apache2服务器运行,中间数据层的部分数据可能会被多个线程共享,如果PHP自带的函数列表等。如果只考虑单个进程的方式,当一个进程被创建的时候,它就会被加载PHP自带的各种函数列表、类列表、常量列表等。当解释层将PHP代码编译完成后,各种用户自定义的函数、类、常量都会添加到之前的列表中,只有这些函数在其自身的结构中某些字段的赋值是不一样的。
简单来说,当执行引擎生成中间代码时,会在ZendVM的栈中添加一个新的执行中间数据结构zend_execute_data
,它包括当前执行过程的活动列表的快照和一些局部变量等。
- 执行引擎
ZendVM的执行引擎是一个简单的实现,它只是依据中间代码序列EX(opline)
,一步一步调用对应的方法执行。在执行引擎中没有类似于PC寄存器一样的变量存放下一条指令,当ZendVM执行到某条指令时,当它所有的任务都执行完毕后,这条指令会自己调用下一条指令。也就时将序列的指针向前移动一个位置,从而执行下一条指令,并且在最后执行return
语句,如此反复。这在本质上是一个函数嵌套调用。
2. Extensions扩展
围绕着Zend引擎,Extensions扩展通过组件的方式提供各种基础服务,常用的内置函数、标准库等都是通过扩展来实现的。
Extensions扩展是PHP内核提供的一套用于扩充PHP的一种方式,PHP中很多操作的函数都是通过扩展提供的。通过扩展,可以使用C/C++实现更强大的功能和性能。
3. SAPI服务器应用编程接口
SAPI全称是Server Application Programming Interface
服务器应用编程接口,SAPI通过一系列钩子函数,使PHP可以和外围交互数据。这是PHP非常优雅和成功的一个设计,通过SAPI成功的将PHP本身和上层应用解耦隔离,PHP可以不用在考虑如何针对不同应用进行兼容,应用本身也可以针对自己的特点实现不同的处理方式。SAPI是在各个服务器抽象层之间遵守的相同的约定,是PHP内核提供给外部调用其服务的接口,简单来说外部系统可以通过SAPI来调用PHP提供的编译脚本并执行脚本的服务。
我们知道PHP是一个脚本解析器,提供脚本的解析与执行,可以在不同环境中应用PHP解析器,例如在Web环境中、在CLI命令行中或者是嵌入到其它应用中。面对这么多不同的环境,PHP提供了一个SAPI层以适配不同的应用环境。简单来说,SAPI是PHP架构最外层的一部分,主要负责PHP的初始化工作。
PHP是一个脚本解析器,提供脚本的解析与执行,其输入的是普通的文本,后由PHP解析器按照预先定义好的语法规则进行解析执行。PHP解析器可在不同环境中应用,例如CLI命令行下、Web环境中、嵌入其他应用中。为此,PHP提供了一个SAPI层以适配不同的应用环境,SAPI可视为PHP的宿主环境。它也是整个PHP框架最外层的一部分,负责PHP框架的初始化工作。如果SAPI是一个独立的应用程序,比如CLI、FPM,那么main()
函数也将定义在SAPI中。SAPI的代码位于PHP源码的sapi
目录下,经常用到的两个SAPI是CLI和FPM。
$ php -v
PHP 7.2.6 (cli) (built: Jan 17 2019 09:41:14) ( NTS )
Copyright (c) 1997-2018 The PHP Group
Zend Engine v3.2.0, Copyright (c) 1998-2018 Zend Technologies
$ cd /usr/local/php/include/php/sapi
$ tree
.
└── cli
└── cli.h
PHP SAPI
PHP提供了函数php_sapi_name
用来查看当前SAPI接口类型
string php_sapi_name(void)
PHP提供了常量PHP_SAPI
用来判断PHP的运行模式是命令行还是浏览器。
echo PHP_SAPI;
PHP提供了很多形式的SAPI接口,包括:apache、apache2filter、apache2handler、caudium、cgi 、cgi-fcgi、cli、cli-server、continuity、embed、isapi、litespeed、milter、nsapi、phttpd pi3web、roxen、thttpd、tux和webjames。
-
apache2handler
:以Apache作为Web服务器,采用mod_php
模式运行时的处理方式。
PHP作为Apache模块,Apache服务器在系统启动后,预先会生成多个进程副本驻留在内存中,一旦有请求出现,就立即使用这些空余的子进程进行处理,这样就不存在生成子进程而造成延迟。
Apache对于php的解析,就是通过众多Module中的php Module来完成的。这些服务器副本在处理完一次HTTP请求后并不会立即退出,而是停留在计算机中等待下次请求。对于客户端浏览器的请求反应也就会更快。
# 把PHP集成到Apache系统,以php的mod_php5 SAPI运行模式为例。
$ vim httpd.conf
LoadModule php5_module modules/mod_php5.so
AddType application/x-httpd-php .php
-
cgi
:通用网关接口(Common Gateway Interface, CGI)
CGI是Web服务器和PHP直接交互的一种方式,以及它的升级版FastCGI协议,它是异步Web服务器所唯一支持的方式。
关于CGI的知识,可以参见《CGI是什么》,其中有比较详细的介绍。 -
cli
:命令行调用的应用模式(Command Line Interface, CLI)
当在命令行终端使用PHP命令时,此时就是用的是CLI模式。
$ php server.php
$ php -f server.php
$ php -r "var_dump(PHP_SAPI)"
虽然通过Web服务器和命令行终端程序执行PHP脚本看起来是不一样的,但实际上它们的工作原理是一样的。命令行程序与Web程序类似,命令行参数传递给要执行的脚本,相当于通过Web的URL请求一个PHP页面,PHP脚本处理完成后返回响应结果,不同的是命令行响应的结果是显示在终端上。因此,PHP脚本执行开始时都是通过SAPI接口进行的。
PHP生命周期
无论是使用那种SAPI,在PHP执行脚本前后,都包含一系列的事件:Module的Init(MINIT)和Shutdown(MSHUTDOWN)、Request的Init(RINIT)和Shutdown(RSHUTDOWN)。
PHP实例生命周期第一阶段:PHP扩展模块初始化阶段(Module init,MINIT)
PHP模块初始化阶段可以初始化扩展的内部变量、分配资源并注册资源处理器,在整个PHP实例生命周期中,此过程只执行一次。
第二阶段: 请求初始化阶段(Request init,RINIT)
在模块初始化并激活后,会创建PHP运行环境,同时调用所有模块注册的RINIT函数去调用每个扩展的请求初始化函数,设置特定的环境变量、分配资源或执行其它任务如审核。
PHP_RINIT_FUNCTION(memcached) {
/* 执行一些关于请求的初始化 */
return SUCCESS;
}
第三阶段:执行PHP脚本文件
第四阶段:请求处理完成(Request Shutdown, RSHUTDOWN)
当请求处理完成后会调用PHP_RSHUTDOWN_FUNCTION
进行回收,这是每个扩展的请求关闭函数,执行最后的清理工作。Zend引擎执行清理过程、垃圾收集、对之前的请求期间用到的每个变量执行unset
。请求完成可能是执行到脚本完成,也可能是调用die()
或exit()
完成。
第五阶段:关闭PHP扩展模块(Module shutdown,MSHUTDOWN)
当PHP生命周期结束的时候,PHP_MSHUTDOWN_FUNCTION
对模块进行回收处理,这是每个扩展的模块关闭函数,用于关闭自己的内核子系统。
PHP_MSHUTDOWN_FUNCTION(memcached) {
/* 执行关于模块的销毁工作 */
UNREGISTER_INI_ENTRIES();
return SUCCESS;
}
MSHUTDOWN
PHP常见运行模式
-
CLI/CGI
单进程模式
CLI和CGI都属于单进程模式,PHP的生命周期在一次请求中完成,也就是说,每次执行PHP脚本,都会执行MINIT、MSHUTDOWN、RINIT、RSHUTDOWN事件。
PHP-CLI是PHP在命令行中运行的接口,区别于在Web服务器上运行的PHP环境,如PHP-CGI、ISAPI等。PHP的CLI Shell脚本适用于所有的PHP优势,在PHP-CLI模式下PHP彻底是属于多线程的,这个时候PHP属于Linux的一个守护进程。它的优点主要在于;
- PHP-CLI使用多进程,子进程结束后内核会负责回收资源。
- PHP-CLI使用多进程,子进程异常退出不会导致整个进程/线程退出,父进程还有机会重建流程。
- PHP-CLI时一个常驻主进程,只负责任务分发,这样逻辑更加清晰。
-
FastCGI
通用网关接口模式
FastCGI是一种特殊的CGI模式,使CGI的升级版本,是一种常驻(long-live)进程类型的CGI。运行后可以Fork创建多个进程,不用花费时间动态的Fork子进程,也不需要每次请求都调用MINIT/MSHUTDOWN。它可以一直执行着,只要激活后,不会每次都花费时间去Fork一次,这也使CGI最为人诟病的Fork-And-Execute模式。
FastCGI是一个可伸缩的、高速的在Web服务器和动态脚本语言间通信的接口,多数流行的Web服务器都支持FastCGI,包括Apache、Nginx、Lighttpd等。同时,FastCGI也被许多脚本语言所支持,如PHP。
FastCGI接口方式采用C/S结构,将Web服务器和脚本解析服务器分开,同时在脚本解析服务器上启动一个或多个脚本解析守护进程。当Web服务器每次遇到动态程序时,可将其直接交付给FastCGI进程来执行,然后将得到的结果返回给浏览器。这种方式可以让Web服务器专一地处理静态请求或将动态脚本服务器的结果返回给客户端,很大程度上提高了真个应用系统的性能。
FastCGI的工作流程
- Web服务器启动时载入FastCGI进程管理器,如IIS的ISAPI、Apache的Module...
- FastCGI进程管理器自身初始化,启动多个CGI解释器进程后等待来自Web服务器的连接。
- 当客户端请求到达Web服务器时,FastCGI进程管理器选择并连接一个CGI解释器。Web服务器将CGI环境变量和标准输入发送到FastCGI子进程中。
- FastCGI子进程完成处理后,将标准输出和错误信息从同一连接返回给Web服务器。当FastCGI子进程关闭连接时,请求便处理完毕。FastCGI子进程接着等待并处理来自FastCGI进程管理器的下一个连接。
最常用的Nginx+PHP-FPM就是使用的FastCGI模式,PHP通过PHP-FPM来管理和调度FastCGI的进程池。Nginx和PHP-FPM通过本地的TCP Socket和UNIX Socket进行通信。
FastCGI主要特点是将动态语言和Web服务器分离,使Nginx专一处理静态请求并向后转发动态请求,而PHP/PHP-FPM服务器专一解析PHP动态请求。
Nginx+PHP-FPMPHP-FPM进程管理器自身初始化后启动多个CGI解释器进程等待来自Nginx的请求,当客户端请求到达PHP-FPM,PHP-FPM会选择一个CGI进程进行执行,Nginx将CGI环境变量和标准输入发送到一个PHP-CGI子进程。PHP-CGI子进程处理完毕后,将标准输出和错误信息返回给Nginx,当PHP-CGI子进程关闭连接时,请求处理完成。PHP-CGI子进程等待下一个连接。
可以想象CGI的系统开销有多大,每个Web请求PHP都必须重新解析php.ini
、载入全部扩展并初始化全部数据结构。使用FastCGI,所有这些都只在进程启动时发生一次。另外对于数据库和Memcache的持续连接仍然可以工作。
-
Multiprocess
多进程模式
多进程模式可以将PHP内置到Web服务器中,PHP可以编译成Apache下的prefork MPM模式和APXS模式。当Apache启动后会Fork创建很多子进程,每个子进程都拥有自己独立的进程地址空间。
在一个子进程中,PHP的生命周期时调用MINIT启动后,执行多次请求(RINIT/RSHUTDOWN)。在Apache关闭或进程结束后,才会调用MSHUTDOWN进行回收阶段。
多进程模型中,每个子进程都是独立运行,没有代码和数据共享,因此一个子进程终止退出和重新生成,都不会影响其他子进程的稳定。
-
Multithreaded
多线程模式
Apache的Worker MPM采用了多线程模型,也就是在一个进程下创建多个线程,在同一进程地址空间执行。
-
Embeded
内嵌模式
Embed SAPI是一种特殊的SAPI,允许在C/C++语言中调用PHP提供的函数,这种SAPI和CGI模式一样。
4. Application 上层应用
Application也就是我们平时编写的PHP程序,它通过不同的SAPI方式得到各种应用模式,例如通过Web服务器实现Web应用,在命令行下以脚本方式运行等。
综上所述,如果把PHP当成一辆车,那么Zend就是车的发动机引擎,Extensions扩展则是车的轮子,而SAPI则可以看作是公路,车可以跑在不同的公路上。每一次PHP程序的执行,就是车跑在公路上的过程。
网友评论