这个工作一直拖延了很久,终于有时间拿出来写写。
本文主要是根据“TIPI深入理解PHP内核”一书进行阅读和分析
一、概述
可以利用辅助的工具进行php代码阅读
例如vim或者sublimetext,重量级的可以用eclipse或者phpstorm这样的工具
本人用的是phpstorm进行代码阅读,CTRL+鼠标点击可以找到变量的定义位置,十分之方便
PG
|
|
PG
用于定义或获取全局变量ZTS
是线程安全的标记
在main/php_globalsh
中定义了宏PG
和结构体_php_core_globals
,用于存放一些常用到的全局参数
举例如下:
SG
我们来看一下SG
的定义
如同PG
一样,ZTS
是线程安全的标志SG
主要是用来获取SAPI的所需要用到的全局变量的
CG
CG
定义在Zend/zend_globals_macros.h
文件中
我们一起来看看CG
的定义相关代码:
可见CG
是用于存取compiler需要用到的一些全局变量的
EG
|
|
EG
用于存取执行器需要用到的全局变量(executor_globals)
EX
|
关于PHPAPI
在源码中我们经常能见到PHPAPI
这样的前缀,__attribute__ ((packed))
的作用就是告诉编译器取消结构在编译过程中的优化对齐,按照实际占用字节数进行对齐,是GCC特有的语法。这个功能是跟操作系统没关系,跟编译器有关,gcc编译器不是紧凑模式的,我在windows下,用vc的编译器也不是紧凑的,用tc的编译器就是紧凑的
他的定义如下:
TSRM
在PHP源码中随处到可以看到TSRM这个标记
TSRM
线程安全资源管理器(Thread Safe Resource Manager),这是个尝尝被忽视,并很少被人说起的“层”(layer), 它在PHP源码的TSRM目录下。一般的情况下,这个层只会在被指明需要的时候才会被启用(比如,Apache2+worker MPM,一个基于线程的MPM),对于Win32下的Apache来说,是基于多线程的,所以这个层在Win32下总是被启用的。ZTS
Zend线程安全(Zend Thread Safety),当TSRM被启用的时候,就会定义这个名为ZTS的宏。tsrm_ls
TSRM存储器(TSRM Local Storage),这个是在扩展和Zend中真正被实际使用的指代TSRM存储的变量名。
相关的定义如下
注意上面的逗号
相关原理介绍可以看一看PHP大牛鸟哥的这篇文章:TSRM到底是什么?
简单来说TSRM就是用来保证线程安全的,在编写代码的时候要记得加上
二、代码生成以及执行
PHP执行的生命周期和ZEND引擎
PHP的单进程生命周期
步骤如下:
- 启动
- 初始化若干全局变量
- 初始化若干常量
- 初始化Zend引擎和核心组件
- 解析php.ini
- 全局操作函数的初始化
- 初始化静态构建的模块和共享模块(MINIT)
- 禁用函数和类
- ACTIVATION
- 激活Zend引擎
- 激活SAPI
- 环境初始化
- 模块请求初始化
- 运行
- DEACTIVATION
- 结束
- flush
- 关闭Zend引擎
多进程的生命周期
多线程的生命周期
关于Zend引擎
Zend引擎是PHP实现的核心,提供了语言实现上的基础设施。例如:PHP的语法实现,脚本的编译运行环境, 扩展机制以及内存管理等,当然这里的PHP指的是官方的PHP实现(除了官方的实现, 目前比较知名的有facebook的hiphop实现,不过到目前为止,PHP还没有一个标准的语言规范), 而PHP则提供了请求处理和其他Web服务器的接口(SAPI)。
PHP的SAPI
SAPI提供了请求处理和其他Web Server的接口
PHP SAPI简单示意图
对应源码文件在/main/SAPI.h
整个SAPI类似于一个面向对象中的模板方法模式的应用。 SAPI.c和SAPI.h文件所包含的一些函数就是模板方法模式中的抽象模板, 各个服务器对于sapi_module的定义及相关实现则是一个个具体的模板。
_sapi_module_struct
结构体的定义
还有存放全局变量的结构体:
在apache中SAPI的定义
PHP脚本的执行
PHP是一边运行一边解析的脚本型语言,在解析的过程中生成了OP码,减少性能的损耗
在PHP的执行过程中,通过cli方式或者CGI方式传递给php程序需要执行的文件, php程序完成基本的准备工作后启动PHP及Zend引擎, 加载注册的扩展模块。
初始化完成后读取脚本文件,Zend引擎对脚本文件进行词法分析,语法分析。然后编译成opcode执行。 如果安装了apc之类的opcode缓存, 编译环节可能会被跳过而直接从缓存中读取opcode执行。
PHP在读取到脚本文件后首先对代码进行词法分析,PHP的词法分析器是通过lex生成的, 词法规则文件在$PHP_SRC/Zend/zend_language_scanner.l, 这一阶段lex会会将源代码按照词法规则切分一个一个的标记(token)。PHP中提供了一个函数token_get_all(), 该函数接收一个字符串参数, 返回一个按照词法规则切分好的数组。 例如将上面的php代码作为参数传递给这个函数:
举个例子:
运行上述代码,即代码被按照标准分词
PHP脚本编译为opcode保存在op_array中,其内部存储的结构如下:
如上面的注释,opcodes保存在这里,在执行的时候由下面的execute函数执行:
三、变量以及数据类型
从类型的维度来看,编程语言可以分为三大类:
- 静态类型语言,比如:C/Java等,在静态语言类型中,类型的检查是在编译期(compile-time)确定的, 也就是说在运行时变量的类型是不会发生变化的。
- 动态语言类型,比如:PHP,python等各种脚本语言,这类语言中的类型是在运行时确定的, 那么也就是说类型通常可以在运行时发生变化
- 无类型语言,比如:汇编语言,汇编语言操作的是底层存储,他们对类型毫无感知。
变量相关结构都在Zend/zend.h
中定义
变量结构
zval的结构如下:
其中refcount__gc
是用来计算变量被引用的数量,is_ref__gc
记录变量是否被引用
这两个值都是用来辅助gc即内存回收机制的,当refcount为0的时候则回收内存
其中zvalue_value value
用于存放变量的值,结构如下:
union是共用体声明,即共享一块内存,取最大长度的值作为整个结构的大小。详细介绍可以看这里:共用声明和共用一变量定义
上述HashTable
就是PHP的array的实现
object的结构如下:
存在的问题
PHP5的zval定义是随着Zend Engine 2诞生的, 随着时间的推移, 当时设计的局限性也越来越明显:
首先这个结构体的大小是(在64位系统)24个字节, 我们仔细看这个zval.value
联合体, 其中zend_object_value
是最大的长板, 它导致整个value需要16个字节, 这个应该是很容易可以优化掉的, 比如把它挪出来, 用个指针代替,因为毕竟IS_OBJECT也不是最最常用的类型.
第二, 这个结构体的每一个字段都有明确的含义定义, 没有预留任何的自定义字段, 导致在PHP5时代做很多的优化的时候, 需要存储一些和zval相关的信息的时候, 不得不采用其他结构体映射, 或者外部包装后打补丁的方式来扩充zval
第三, PHP的zval大部分都是按值传递, 写时拷贝的值, 但是有俩个例外, 就是对象和资源, 他们永远都是按引用传递, 这样就造成一个问题, 对象和资源在除了zval中的引用计数以外, 还需要一个全局的引用计数, 这样才能保证内存可以回收. 所以在PHP5的时代, 以对象为例, 它有俩套引用计数, 一个是zval中的, 另外一个是obj自身的计数
第四, 我们知道PHP中, 大量的计算都是面向字符串的, 然而因为引用计数是作用在zval的, 那么就会导致如果要拷贝一个字符串类型的zval, 我们别无他法只能复制这个字符串. 当我们把一个zval的字符串作为key添加到一个数组里的时候, 我们别无他法只能复制这个字符串. 虽然在PHP5.4的时候, 我们引入了INTERNED STRING, 但是还是不能根本解决这个问题.
以上zval的结构是在php5中的定义,php7中发生了变化并且进行了优化
以下是php7中的zval结构
虽然看起来变得好大, 但其实仔细看, 全部都是联合体, 这个新的zval在64位环境下,现在只需要16个字节(2个指针size), 它主要分为俩个部分, value和扩充字段, 而扩充字段又分为u1和u2俩个部分, 其中u1是type info, u2是各种辅助字段.
所有的复杂类型的定义, 开始的时候都是zend_refcounted_h
结构, 这个结构里除了引用计数以外, 还有GC相关的结构. 从而在做GC回收的时候, GC不需要关心具体类型是什么, 所有的它都可以当做zend_refcounted*
结构来处理.
HashTable
PHP强大的数组就是利用HashTable实现的
关于HashTable的内容在我的博客之前也有提及,可以去瞧一瞧 PHP内核中的HashTable
常量
常量的定义放在Zend/zend_constants.h
中
定义结构十分之简单define()
内置函数的实现过程如下:
上面的代码已经对对象和类常量做了简化处理, 其实现上是一个将传递的参数传递给新建的zend_constant结构,并将这个结构体注册到常量列表中的过程。 关于大小写敏感,函数的第三个参数表示是否大小不敏感,默认为false(大小写敏感)。 这个参数最后会赋值给zend_constant结构体的flags字段。
预定义变量
在某个局部函数中使用类似于$GLOBALS变量这样的预定义变量, 如果在此函数中有改变的它们的值的话,这些变量在其它局部函数调用时会发现也会同步变化。 为什么呢?是否是这些变量存放在一个集中存储的地方? 从PHP中间代码的执行来看,这些变量是存储在一个集中的地方:EG(symbol_table)。
在模块初始化时,$GLOBALS在zend_startup函数中通过调用zend_register_auto_global将GLOBALS注册为预定义变量。 $_GET、$_POST等在php_startup_auto_globals函数中通过zend_register_auto_global将_GET、_POST等注册为预定义变量。
在通过$获取变量时,PHP内核都会通过这些变量名区分是否为全局变量(ZEND_FETCH_GLOBAL
), 其调用的判断函数为zend_is_auto_global
,这个过程是在生成中间代码过程中实现的。 如果是ZEND_FETCH_GLOBAL
或ZEND_FETCH_GLOBAL_LOCK
(global语句后的效果), 则在获取获取变量表时(zend_get_target_symbol_table), 直接返回EG(symbol_table)。则这些变量的所有操作都会在全局变量表进行。
类型提示实现
变量作用域
对于全局变量,Zend引擎有一个_zend_executor_globals
结构(EG),该结构中的symbol_table就是全局符号表, 其中保存了在顶层作用域中的变量。同样,函数或者对象的方法在被调用时会创建active_symbol_table来保存局部变量。 当程序在顶层中使用某个变量时,Zend Engine就会在symbol_table中进行遍历, 同理,如果程序运行于某个函数中,Zend引擎会遍历查询与其对应的active_symbol_table, 而每个函数的active_symbol_table
是相对独立的,由此而实现的作用域的独立。
|
|
函数中的局部变量就存储在_zend_execute_data
的symbol_table
中,在执行当前函数的op_array时, 全局zend_executor_globals
中的*active_symbol_table
会指向当前_zend_execute_data
中的*symbol_table
。 因为每个函数调用开始时都会重新初始化EG(active_symbol_table)为NULL, 在这个函数的所有opcode的执行过程中这个全局变量会一直存在,并且所有的局部变量修改都是在它上面操作完成的,如前面的赋值操作等。 而此时,其他函数中的symbol_table会存放在栈中,将当前函数执行完并返回时,程序会将之前保存的zend_execute_data恢复, 从而其他函数中的变量也就不会被找到,局部变量的作用域就是以这种方式来实现的。
类型转换
可以参照文件ext/standard/type.c
,里面包含了类型转换需要用到的函数
PHP的标准扩展中提供了两个有用的方法settype()以及gettype()方法,前者可以动态的改变变量的数据类型, gettype()方法则是返回变量的数据类型
四、函数的实现
PHP函数分为以下几种:
- 用户定义的函数
- 内部函数:如我们常见的count、strpos、implode等函数,这些都是标准函数,它们都是由标准扩展提供的; 如我们经常用到的isset、empty、eval等函数,这些结构被称之为语言结构。 还有一些函数需要和特定的PHP扩展模块一起编译并开启,否则无法使用。也就是有些扩展是可选的。
- 匿名函数:Closure
变量函数:
12$func = 'print_r';$func('i am print_r function.');zend_function可以与zend_op_array互换
- zend_function可以与zend_internal_function互换
但是一个zend_op_array结构转换成zend_function是不能再次转变成zend_internal_function结构的,反之亦然。
其实zend_function就是一个混合的数据结构,这种结构在一定程序上节省了内存空间。
函数的结构体包含一些公共的元素,即Common elements,所以它们之间可以比较方便地实现转换
函数定义
词法分析->语法分析->生成中间代码(zend_op)->执行中间代码
执行代码的过程在文件Zend/zend_vm_execute.h
其中zend_op
的定义如下:
函数的参数
函数的参数存放在zend_arg_info
中,其定义放在文件Zend/zend.compile.h
中
|
|
函数的返回值
在PHP中,函数都有返回值,分两种情况,使用return语句明确的返回和没有return语句返回NULL。
函数结束时需要调用zend_do_return
:
可见生成的中间代码为ZEND_RETURN
ZEND_RETURN中间代码会执行 ZEND_RETURN_SPEC_CONST_HANDLER
, ZEND_RETURN_SPEC_TMP_HANDLER
或ZEND_RETURN_SPEC_TMP_HANDLER
。 这三个函数的执行流程基本类似,包括对一些错误的处理。
这里我们看看ZEND_RETURN_SPEC_CONST_HANDLER
是如何执行的:
在没有声明返回值时:
zend引擎“自动”返回一个NULL
函数的调用与执行
函数的调用
Zend在调用执行PHP代码之前先要把代码转换为opcode,然后再进行执行。
我们先来看看一个PHP实例以及其生成的opcode
|
|
可以看到,上部主要集中在调用函数foo上面,PHP对函数名统一采用strtolower
操作,所以对大小写是不敏感的
Zend Engine会在function_table
中根据函数名,若找不到,则抛出错误提示;若找到该名,则返回函数zend_function的结构指针,然后通过function.type
的值来判断函数是内部函数还是用户定义的函数,调用zend_execute_internal(zend_internal_function.handler)
或者直接 调用zend_execute
来执行这个函数包含的zend_op_array
函数的执行
内部函数的执行与用户函数不同。用户函数是php语句一条条“翻译”成op_line组成的一个op_array,而内部函数则是用C来实现的,因为执行环境也是C环境, 所以可以直接调用
看看这个例子:
其对应的opcode
先将EG下的This,scope等暂时缓存起来(这些在后面会都恢复到此时缓存的数据)。在此之后,对于用户自定义的函数, 程序会依据zend_execute
是否等于execute
并且是否为异常来判断是返回,还是直接执行函数定义的op_array:
|
|
而在Zend/zend.c文件的zend_startup函数中,已将zend_execute赋值为:
调用每个opcode的处理函数。而execute_data在execute函数开始时就已经给其分配了空间,这就是这个函数的执行环境。
匿名函数
PHP匿名函数的实现主要有:create_function()函数的使用、__invoke、闭包
create_function
先介绍一下create_function()
create_function()
可以创建一个匿名函数
看看官方手册上关于create_function的介绍:
string create_function ( string $args , string $code )
create_function — Create an anonymous (lambda-style) function
该函数主要就是用于创建匿名函数用的
我们来一起看看一个例子
从上面例子中可以看到,创建一个匿名函数其实他是“有名”的,但是为什么这里会提示找不到函数呢
我们一起来看看debug_zval_dump
的结果
可见匿名函数的长度是9而实际上lambda_1
的长度只有8
|
|
可以见到函数在名字的前面多加了一个'\0'
的空字符,并且利用count
来进行函数名的编号
所以我们可以通过在函数名前加一个“空字符”来调用匿名函数:
这种创建”匿名函数”的方式有一些缺点:
- 函数的定义是通过字符串动态eval的, 这就无法进行基本的语法检查;
- 这类函数和普通函数没有本质区别, 无法实现闭包的效果.
真正的匿名函数
__invoke
如果定义了__invoke()
魔术方法的话那么在对象被当作函数调用时则会被调用
这个和C++中的重载有点类似
匿名函数的实现
其实匿名函数也只是一个普通的类而已
闭包的实现
看看一段PHP闭包代码的执行过程吧:
再看看VLD生成的结果
上面根据情况去掉了一些无关的输出, 从上到下, 第1开始将100赋值给!0也就是变量$i, 随后执行ZEND_DECLARE_LAMBDA_FUNCTION
, 那我们去相关的opcode执行函数中看看这里是怎么执行的, 这个opcode的处理函数位于Zend/zend_vm_execute.h
中:
看看创建闭包的函数,在Zend/zend_closures.c
中:
类和面向对象
类的结构和实现
类的结构
|
|