PHP7内核剖析 学习笔记 第一章 PHP基础架构

1.1 简介

PHP是一种非常流行的高级脚本语言,尤其适合Web开发,快速、灵活、实用是PHP最重要的特点。PHP自1995年由Lerdorf创建以来,在全球得到了非常广泛的应用。

PHP在1995年早期以Personal Home Page Tools(PHP Tools)开始对外发表第一个版本,Lerdorf写了一些介绍此程序的文档,并且发布了PHP1.0。在这早期的版本中,提供了访客留言本、访客计数器等简单的功能,之后越来越多的网站开始使用PHP,并且强烈要求增加一些特性,在新成员加入开发行列后,Rasmus Lerdorf在1995年6月8日将PHP公开发布,希望可以通过社群来加速程序开发与寻找错误。这个版本被命名为PHP2,已经有了今日PHP的一些雏形,类似Perl的变量命名方式(Perl也用$声明标量变量,标量变量可以存储单一的值,如整数、浮点数、字符串,此外Perl中还有用@声明的数组变量和%声明的散列变量)、表单处理功能、嵌入到HTML中执行的能力。程序语法上也类似Perl,有较多的限制,但更简单。

PHP/FI加入了对MySQL的支持,从此建立了PHP在动态网页开发上的地位。到了1996年底,有15000个网站使用了PHP。

在1997年,任职于Technion IIT公司的两个以色列设计师Zeev Suraski和Andi Gutmans重写了PHP的解析器,成为PHP3的基础,而PHP也在这时改称为PHP:Hypertext Preprocessor,1998年6月正式发布PHP3。Zeev Suraski和Andi Gutmans在PHP3发布后开始改写PHP的核心,这个在1999年发布的解析器称为Zend Engine,他们在以色列的Ramat Gan成立了Zend Technologies来管理PHP的开发。

在2000年5月22日,以Zend Engine 1.0为基础的PHP4正式发布。2004年7月13日发布了PHP5,PHP5使用了第二代的Zend Engine,包含了许多新特色:完全实现面向对象,引入PDO(PHP Data Object,数据库访问抽象层,统一各种数据库的访问接口),以及许多性能方面的改进。目前PHP5.x仍然是应用非常广泛的一个版本。

PHP特点:

1.开源免费:PHP社群有大量活跃的开发者贡献代码。

2.快捷:程序开发快,运行快,技术本身学习快,实用性强。

3.效率高:PHP消耗相当少的系统资源,自动gc机制。

4.类库资源:有大量可用类库供开发者使用。

5.扩展性:允许用户使用C/C++扩展PHP。

6.跨平台:可以在UNIX、Windows、Mac OS等系统上使用PHP。

1.2 安装及调试

本书使用的PHP版本为7.0.12,下载地址为http://php.net/distributions/php-7.0.12.tar.gz,下载后使用以下命令进行编译、安装:

--enable-debug参数为开启debug模式,方便我们进行调试。关于调试自然少不了gdb了,PHP内核的实现虽然比较复杂,但是阶段划分比较鲜明,可以通过gdb在各个阶段设置断点,然后进行相应调试。学习内核时,可以使用Cli模式,因为它是单线程的,方便调试,这并不影响我们对内核的学习。同时,想要弄清楚PHP内核,自然少不了阅读PHP源码,本书后面的内容将会非常频繁地列举源码。本书主要目的是引导大家自己去阅读源码、探索PHP的实现,而不希望只简单地通过书中的描述来了解PHP,所以希望大家在阅读本书时,准备好一份源码以便随时查看和调试。

1.3 PHP7的变化

PHP7与PHP5版本相比变化很大,尤其是在Zend引擎方面。为提升性能,PHP7对Zend进行了深度优化,使得PHP的运行速度大大提高,比PHP5.0~5.6快了近5倍,同时还降低了PHP对系统资源的占用。下面介绍PHP7比较大的几个变化:

1.抽象语法树

在PHP之前的版本中,PHP代码在语法解析阶段直接生成了ZendVM指令,也就是在zend_language_parser.y中直接生成opline指令(operation line,每行代表一个操作,它是Zend引擎内部用于表示PHP代码的低级操作指令),这使得编译器与执行器耦合在一起。编译生成的指令供执行引擎使用,该指令是在语法解析时直接生成的,假如要把执行引擎换成别的,就需要修改语法解析规则;或者如果PHP的语法解析规则变了,但对应的执行指令没有变化,那也需要修改语法解析规则。

PHP7中增加了抽象语法树,首先是将PHP代码解析生成抽象语法树,然后将抽象语法树编译为ZendVM指令。抽象语法树的加入使得PHP的编译器与执行器很好地隔离开,编译器不需要关心指令的生成规则,然后执行器根据自己的规则将抽象语法树编译为对应的指令,执行器同样不需要关心该指令的语法规则是什么样子的。

2.Native TLS

开发过PHP5.x版本扩展的读者对TSRM_CC、TSRM_DC这两个宏一定不会陌生,它们是用于线程安全的。PHP提供了一个线程安全资源管理器,将全局资源进行了线程隔离,不同线程间互不干扰(线程本地存储)。

使用全局资源需要先获取本线程的资源池,这个过程比较占用时间,因此,PHP5.x通过参数传递的方式将本线程的资源池传递给其他函数,避免重复查找。这种实现方式使得几乎所有函数都需要加上接收资源池的参数,也就是TSRM_DC宏(该宏的作用是在函数声明中加上与线程局部存储相关的参数,该宏通常放在函数的参数列表的最后)所加的参数,然后调用其他函数时再把这个参数传下去,不仅容易遗漏,而且这种方式极不优雅。

PHP7中使用Native TLS(线程局部存储)来保存线程的资源池,就是通过__thread标识一个全局变量,这样这个全局变量就是线程独享的了,不同线程的修改不会相互影响。

3.指定函数参数、返回值类型

PHP7中可以指定函数参数及返回值的类型,例如:

php 复制代码
function foo(string $name): array {
    return [];
}

这个函数的参数必须为字符串,返回值必须是数组,否则会报error错误。

4.zval结构的变化

zval是PHP变量的内部结构,也是PHP内核中应用最为普遍的一个结构。在PHP5.x中,zval的结构如下:

c 复制代码
struct _zval_struct {
    /* Variable information */
    zvalue_value value;    /* value */
    zend_uint refcount__gc;
    zend_uchar type;    /* active type */
    zend_uchar is_ref__gc;
};

type为类型,is_ref__gc标识该变量是否为引用,value为变量的具体值,它是一个union,用来适配不同的变量类型:

c 复制代码
typedef union _zval_value {
    long val;    /* long value */
    double dval;    /* double value */
    struct {
        char *val;
        int len;
    } str;
    HashTable *ht;    /* hash table value */
    zend_object_value obj;
    zend_ast *ast;
} zvalue_value;

zval中的refcount__gc成员用来记录变量的引用计数。引用计数是PHP实现变量自动回收的基础,它记录了一个变量有多少个地方在使用。PHP5.x中引用计数是在zval(指_zval_struct)中而不是具体的value(指zvalue_value)中,这样一来,导致变量复制时需要复制两个结构(复制变量时,同时需要这两个结构中的内容),zval、zval_value始终绑定在一起。PHP7将引用计数转移到了具体的value中,这样更合理。因为zval只是变量的载体,可以简单地认为是变量名,而value才是真正的值,这个改变使得PHP变量之间的复制、传递更加简洁易懂。此外,zval结构的大小也从24byte减少到了16byte,这是PHP7降低系统资源占用的一个优化。

5.异常处理

PHP5.x中很多操作会直接抛出error错误,PHP7中将多数错误改为了异常抛出,这样就能通过try catch捕捉到,例如:

php 复制代码
try {
    test();
} catch (Throwable $e) {
    echo $e->getMessage()
}

脚本中调用了一个不存在的函数,PHP5.x中报"PHP Fatal error: Call to undefined function test()",而PHP7中可通过Throwable异常类型进行捕获。新的异常处理方式使得错误处理更加可控。

6.HashTable的变化

HashTable,即哈希表,也被称为散列表,它是PHP中强大的array()类型的内部实现结构,也是内核中使用非常频繁的一个结构,函数符号表、类符号表、常量符号表等都是通过HashTable实现的。

PHP7中的HashTable结构的大小从72byte减小到了56byte,同时,数组元素Bucket结构也从72byte减小到了32byte。

7.执行器

execute_data、opline采用寄存器变量存储,执行器的调度函数为execute_ex(),这个函数负责执行PHP代码编译生成的ZendVM指令。在执行期间会频繁地用到execute_data、opline两个变量,在PHP5.x中,这两个变量是由execute_ex()通过参数传递给各指令handler的,在PHP7中不再采用传参的方式,而是将execute_data、opline通过寄存器进行存储,避免了传参导致的频繁出入栈操作,同时,寄存器相比内存的访问速度更快。这个优化使得PHP的性能有了5%左右的提升。

8.新的参数解析方式

PHP5.2通过zend_parse_parameters()解析函数的参数,PHP7提供了另一种方式,同时保留了原来的方式,但新的解析方式速度更快。

除了上面介绍的这些变化,PHP7还有很多优化和新特性,不再一一列举。

1.4 PHP的构成

PHP的源码下有几个主要目录:SAPI、main、Zend、ext。其中SAPI是PHP的应用接口层;main为PHP的主要代码,主要是输入输出、Web通信、PHP框架的初始化操作(如fastcgi协议的解析、扩展的加载、PHP配置的解析工作)等,它位于ZendVM的上一层;Zend目录是PHP解析器的主要实现,即ZendVM,它是PHP的核心事项,PHP代码的解释、执行就是由Zend完成的;ext是PHP的扩展目录;TSRM是线程安全相关的实现。

PHP各组成部分之间的关系如图1-1所示:

1.SAPI

PHP是一个脚本解析器,提供脚本的解析与执行,它的输入是普通的文本,然后由PHP解析器按照预定的语法规则进行解析执行。我们可以在不同环境中应用这个解析器,如命令行下、Web环境中、嵌入其他应用中使用。为此,PHP提供了一个SAPI层以适配不同的应用环境,SAPI可认为是PHP的宿主环境。SAPI也是整个PHP框架的最外层的一部分,它主要负责PHP框架的初始化工作。如果SAPI是一个独立的应用,比如Cli、Fpm,那么main函数也将定义在SAPI中。SAPI的代码位于PHP源码的/sapi目录下,经常用到的两个SAPI是Cli、Fpm。

2.ZendVM

ZendVM是一个虚拟的计算机,它介于PHP应用与实际计算机中间,我们编写的PHP代码就是被它解释执行的。ZendVM是PHP语言的核心实现,它主要由两部分组成:编译器、执行器。其中编译器负责将PHP代码解释为执行器可识别的指令,执行器负责执行编译器解释出的指令。ZendVM的角色等价于Java中的JVM,它们都是抽象出来的计算机,与C/C++这类编译型语言不同,虚拟机上运行的指令并不是机器指令。虚拟机的一个突出优点是跨平台,只需要按照不同平台编译出对应的解析器就可以实现代码的跨平台执行。

3.Extension

扩展是PHP内核提供的一套用于扩充PHP功能的一种方式,PHP社区中有丰富的扩展可供使用,这些扩展为PHP提供了大量实用的功能,PHP中很多操作的函数都是通过扩展提供的。通过扩展,我们可以使用C/C++实现更强大的功能和更高的性能,这也使得PHP与C/C++非常相近,甚至可以在C/C++应用中把PHP嵌入作为第三库使用。扩展分为PHP扩展、Zend扩展,PHP扩展比较常见,而Zend扩展主要应用于ZendVM,它可以做的东西更多,我们所熟知的Opcache就是Zend扩展。

1.5 生命周期

PHP的整个生命周期被划分为以下几个阶段:模块初始化阶段(module startup)、请求初始化阶段(request startup)、执行脚本阶段(execute script)、请求关闭阶段(request shutdown)、模块关闭阶段(module shutdown)。根据不同SAPI的实现,各阶段的执行情况会有差异,比如命令行模式下,每次执行一个脚本都会完整地经历这些阶段,而FastCgi模式下则在启动时执行一次模块初始化,然后各个请求只经历请求初始化、执行请求脚本、请求关闭几个阶段,在SAPI关闭时经历模块关闭阶段。各阶段执行的顺序与对应的处理函数如图1-2所示:

1.模块初始化阶段

这个阶段主要进行PHP框架、Zend引擎的初始化操作。该阶段的入口函数为php_module_startup(),如图1-3所示:

这个阶段一般只在SAPI启动时执行一次,对于Fpm而言,就是在Fpm的master进程启动时执行的。

该阶段的几个主要处理:

(1)激活SAPI:sapi_activate(),初始化请求信息SG(request_info)(SG表示Server Globals,它是表示请求的一个底层数据结构,其中包含HTTP请求方法(GET或POST)、请求的URI等)、设置读取POST请求的handler等,在module startup阶段处理完成后将调用sapi_deactivate()。

(2)启动PHP输出:php_output_startup()。

(3)初始化垃圾回收器:gc_globals_ctor(),分配zend_gc_globals内存。

(4)启动Zend引擎:zend_startup(),主要操作包括:

①启动内存池start_memory_manager();

②设置一些util函数句柄(如zend_error_cb、zend_printf、zend_write等);

③设置Zend虚拟机编译、执行器的函数句柄zend_compile_file、zend_execute_ex,以及垃圾回收的函数句柄gc_collect_cycles;

④分配函数符号表(CG(function_table))、类符号表(CG(class_table))、常量符号表(EG(zend_constants))等,如果是多线程,还会分配编译器、执行器的全局变量;

⑤注册Zend核心扩展:zend_startup_builtin_functions(),这个扩展是内核提供的,该过程将注册Zend核心扩展提供的函数,如strlen、define、func_get_args、class_exists等;

⑥注册Zend定义的标准常量:zend_register_standard_constants(),如:E_ERROR、E_WARNING、E_ALL、TRUE、FALSE等。

⑦注册$GLOBALS超全局变量的获取handler;

⑧分配php.ini配置的存储符号表:EG(ini_directives)。

(5)注册PHP定义的常量:PHP_VERSION、PHP_ZTS、PHP_SAPI等。

(6)解析php.ini:解析完成后所有的php.ini配置保存在configuration_hash哈希表中。

(7)映射PHP、Zend核心的php.ini配置:根据解析出的php.ini,获取对应的配置值,将最终的配置插入EG(ini_directives)哈希表。

(8)注册用于获取$_GET$_POST$_SERVER$_ENV$_REQUEST$_FILES变量的handler。

(9)注册静态编译的扩展:php_register_internal_extensions_func()。

(10)注册动态加载的扩展:php_ini_register_extensions()将php.ini中配置的扩展加载到PHP中。

(11)回调各扩展定义的module startup钩子函数,即通过PHP_MINIT_FUNCTION()定义的函数。

(12)注册php.ini中禁用的函数和类:disable_functions、disable_classes。

2.请求初始化阶段

该阶段是在请求处理前每一个请求都会经历的一个阶段,对于Fpm而言,是在worker进程accept一个请求且读取、解析完请求数据后的一个阶段。该阶段的处理函数为php_request_startup(),如图1-4所示:

主要的处理有以下几个:

(1)激活输出:php_output_activate()。

(2)激活Zend引擎:zend_activate(),主要操作如下:

①重置垃圾回收器:gc_reset();

②初始化编译器:init_compiler();

③初始化执行器:init_executor(),将EG(function_table)、EG(class_table)分别指向CG(function_table)、CG(class_table),所以在PHP的编译、执行期间,EG(function_table)与CG(function_table)、EG(class_table)与CG(class_table)是同一个值;另外还会初始化全局变量符号表EG(symbol_table)、include过的文件符号表EG(included_files);

④初始化词法扫描器:startup_scanner()。

(3)激活SAPI:sapi_activate()。

(4)回调各扩展定义的request startup钩子函数:zend_activate_modules()。

3.执行脚本阶段

该阶段包括PHP代码的编译、执行两个核心阶段,这也是Zend引擎最重要的功能。在编译阶段,PHP脚本将经历从PHP源代码到抽象语法树再到opline指令的转化过程,最终生成的opline指令就是Zend引擎可识别的执行指令,这些指令接着被执行器执行,这就是PHP代码解释执行的过程,本书介绍的大部分内容都是关于这两个阶段的。这个接口的入口函数为php_execute_script(),如图1-5所示:

4.请求关闭阶段

在PHP脚本解释执行完成后将进入请求关闭阶段,这个阶段将flush输出内容、发送HTTP应答header头、清理全局变量、关闭编译器、关闭执行器等。该阶段还会回调各扩展的request shutdown钩子函数。该阶段是请求初始化阶段的相反操作,与请求初始化时的处理一一对应,如图1-6所示:

5.模块关闭阶段

该阶段在SAPI关闭时执行,与模块初始化阶段对应,这个阶段主要进行资源的清理、PHP各模块的关闭操作,同时,将回调各扩展的module shutdown钩子函数。具体的处理函数为php_module_shutdown(),如图1-7所示:

相关推荐
BingoGo6 小时前
当你的 PHP 应用的 API 没有限流时会发生什么?
后端·php
JaguarJack6 小时前
当你的 PHP 应用的 API 没有限流时会发生什么?
后端·php·服务端
BingoGo1 天前
OpenSwoole 26.2.0 发布:支持 PHP 8.5、io_uring 后端及协程调试改进
后端·php
JaguarJack1 天前
OpenSwoole 26.2.0 发布:支持 PHP 8.5、io_uring 后端及协程调试改进
后端·php·服务端
JaguarJack2 天前
推荐 PHP 属性(Attributes) 简洁读取 API 扩展包
后端·php·服务端
BingoGo2 天前
推荐 PHP 属性(Attributes) 简洁读取 API 扩展包
php
JaguarJack3 天前
告别 Laravel 缓慢的 Blade!Livewire Blaze 来了,为你的 Laravel 性能提速
后端·php·laravel
郑州光合科技余经理4 天前
代码展示:PHP搭建海外版外卖系统源码解析
java·开发语言·前端·后端·系统架构·uni-app·php
西岸行者4 天前
学习笔记:SKILLS 能帮助更好的vibe coding
笔记·学习
starlaky4 天前
Django入门笔记
笔记·django