Wicky's Blog

深入理解PHP(一)

从毕业开始使用PHP也有接近三年时间,对PHP的理解还只是停留在很表层的使用阶段,而没有深入去了解它的内部运行及处理机制。所以决定开始每周花一点时间去深入理解PHP的内核,当做是一些自己的学习笔记和总结。

运行模式及生命周期

PHP运行模式

  • CGI(通用网关接口 / Common Gateway Interface)
  • FastCGI(常驻型CGI / Long-Live CGI)
  • CLI(命令行运行 / Command Line Interface)
  • Web模块模式(Apache等Web服务器运行的模式)

CGI: 本质上是以socket编程实现一个TCP/UDP协议的服务器,启动时创建TCP/UDP协议的服务器socket监听,并接受相关请求进行处理,并在此基础上添加模块的初始化、sapi初始化、模块关闭、sapi关闭等就构成了整个CGI的生命周期。

CGI全称是“通用网关接口”(Common Gateway Interface),它可以让一个客户端从网页浏览器向执行在Web服务器上的程序请求数据。

CGI描述了客户端和这个程序之间传输数据的一种标准。

CGI的一个目的是要独立于任何语言的,所以CGI可以用任何一种语言编写,只要这种语言具有标准输入、输出和环境变量。如php,perl,tcl等。

存在的问题:

  • 每当有用户请求,会创建CGI的子进程,然后处理完请求才会结束子进程。当
  • 当用户请求数量非常大事,会大量占用系统资源,比如内存、CPU等,造成性能低下。
  • 有多少客户端请求就会创建多少个CGI子进程,子进程反复加载是造成性能低下的主要原因。

webserver收到浏览器index.php的请求以后,会启动相应的CGI程序,也就是PHP解释器。接下来PHP解释器会解析php.ini文件,加载模块,初始化执行环境然后处理,按照CGI规定执行完成后返回结果,结束进程,webserver再返回浏览器。

browser->webserver-CGI/FastCGI->PHP->close CGI->webserver->browser

FastCGI:是CGI的升级版本,像一个常驻(long live)的CGI,它可以一直在执行,激活以后不需要每次都花时间去fork进程。

工作原理:

  • webserver启动时载入FastCGI管理器(PHP-FPM)。
  • FastCGI进程管理器自身初始化,启动多个CGI解释器进程等待webserver连接。
  • 客户端请求到达webserver时,FastCGI选择一个CGI解释器进程并连接。
  • FastCGI子进程处理完后将标准输出和错误信息由同一连接返回Webserver,当关闭FastCGI子进程连接时,请求宣告完成。FastCGI等待下一个连接。
  • 使用FastCGI,只需要解析一次php.ini文件,初始化数据结构也只需要一次,不需要像CGI那样每次都经历这个过程。一个好处持续数据库连接可以工作。

优点:

  • 从稳定性看,FastCGI是以独立的进程池来运行CGI,单个进程死掉,系统可以丢弃并重新分配进程来处理。
  • 从安全性看,FastCGI支持分布式运算,和宿主Server可以独立,FastCGI Down不会将Server弄垮。
  • 从性能上看,FastCGI将动态逻辑处理从Server中分离开,I/O处理留给Server处理。

不足:

因为需要开启多个进程,FastCGI需要更多的服务器内存,PHP-CGI进程解释器需要消耗7-25M内存,若开启500个进程则是很大的内存数。

Tips:

如果php-fpm的单个子进程占用内存一直居高不下,如图fpm需要看php-fpm的配置中max_request是否打开,不开启会导致fpm进程一直不释放内存,可设置为max_request=300-500,请求次数达到这个值后php-fpm进程进行重启,保存内存不增长。如果内存不释放,导致占用内存增加不够用,从而导致php-fpm不可用造成nginx开始报502。如果是间歇性502一般是php-fpm进程重启造成的。如果参数设置的太小,导致无进程可用也会报502。进程占用内存计算方法(内存/20M)。

CLI

PHP-CLI是PHP Command Line Interface的简称,就是PHP在命令行运行的接口,区别于在Web服务器上运行的PHP环境(PHP-CGI,ISAPI等)。PHP的CLI Shell脚本适用于所有的PHP优势,使创建要么支持脚本或系统甚至与GUI应用程序的服务端,在Windows和Linux下都是支持PHP-CLI模式的,如php xx.php来执行。

开始和结束

首先看一段代码:

1
2
3
4
5
<?php
echo "hello world";
$a = 1+1;
echo $a;
?>

通常我们在Apache或Nginx这类web服务器来测试PHP脚本,或者在CGI的命令行模式下执行,脚本执行完后,Web服务器应答,浏览器显示应答信息,或者在命令行标准输出上显示内容。以CGI执行为例,会执行下面的几个步骤:

  • Scanning(Lexing) ,将PHP代码转换为语言片段(Tokens)
  • Parsing, 将Tokens转换成简单而有意义的表达式
  • Compilation, 将表达式编译成Opocdes
  • Execution, 顺次执行Opcodes,每次一条,从而实现PHP脚本的功能。

脚本执行的开始都是以SAPI接口实现开始的,只是不同的SAPI接口会实现他们特定的工作,如Apache的mod_php SAPI实现需要初始化从Apache获取的一些信息,将输出内容返回给Apache,其他的SAPI也是类似。

PHP开始执行要经过两个主要的阶段:处理请求之前的开始阶段和请求之后的阶段。开始阶段主要有两个过程:第一个是模块初始化阶段(MINIT),在整个生命周期之内只进行一次。第二个过程是模块激活阶段(RINIT),发生在请求阶段,每次请求之前都会进行模块激活。例如PHP注册一些扩展模块,则在MINIT阶段会回调所有模块的MINIT函数。

请求到达之后PHP初始化执行脚本的基本环境,包括保存运行过程中变量名称、值内容符号表,所有的函数及类等信息的符号表。然后PHP会调用所有RINIT的函数,各模块也会执行一些相关操作。

请求处理完成后,执行到脚本末尾或是调用die()/exit()函数,PHP都将进入结束阶段。也分为两个环节,一个请求结束后停用模块(RSHOUTDOWN,对应RINIT),一个在SAPI生命周期结束时关闭模块(MSHUTDOWN, 对应MINIT)。

单进程SAPI生命周期

sapi

启动

在调用每个模块的模块初始化前,会有一个初始化的过程,包括以下:

  • 初始化若干个变量
  • 初始化若干常量
  • 初始化Zend引擎和核心组件
  • 解析php.ini
  • 全局操作函数的初始化
  • 初始化静态构造的模块和共享模块(MINIT)
  • 禁用函数和类

ACTIVATION

调用php_request_startup做请求初始化操作,还有其他的操作:

  • 激活Zend引擎

    • gc_reset函数来重置垃圾收集机制
    • init_complier初始化编译器
    • init_executor初始化中间代码的执行过程
  • 激活SAPI

  • 环境初始化

    • $_POST、$_GET、$_COOKIE、$_SERVER、$_ENV、$_FILES等
  • 模块请求初始化

运行

php_execute_script函数包含了运行PHP脚本的全部过程

DEACTIVITION

关闭请求的过程是若干个关闭操作的集合,存在于php_request_shutdown函数。

结束

  • flush
  • 关闭zend引擎

多进程SAPI生命周期

以Apache为例,Apache启动后会fork多个子进程,进程间内存独立,每个子进程都会经过开始和结束环节,所以在整个生命周期中可能会有多个请求。如下图所示:
multi

多线程的SAPI生命周期

整个进程的生命周期内会并行的重复着 请求开始-请求关闭环节,如下图:
thread

Zend引擎

Zend引擎是PHP实现的核心,提供了语言实现上的基础设施。如:语法实现、脚本的编译运行环境、扩展机制、内存管理。很多PHP扩展也是使用的ZendAPI。

参考资料