标签 - php7内核剖析

php7内核剖析    2019-09-16 14:49:18    17    0    0

3.4.6 类的自动加载

在实际使用中,通常会把一个类定义在一个文件中,然后使用时include加载进来,这样就带来一个问题:在每个文件的头部都需要包含一个长长的include列表,而且当文件名称修改时也需要把每个引用的地方都改一遍,另外前面我们也介绍过,原则上父类需要在子类定义之前定义,当存在大量类时很难得到保证,因此PHP提供了一种类的自动加载机制,当使用未被定义的类时自动调用类加载器将类加载进来,方便类的同一管理。

在内核实现上类的自动加载实际就是定义了一个钩子函数,实例化类时如果在EG(class_table)中没有找到对应的类则会调用这个钩子函数,调用完以后再重新查找一次。这个钩子函数保存在EG(autoload_func)中。

PHP中提供了两种方式实现自动加载:__autoload()spl_autoload_register()

(1)__autoload():

这种方式比较简单,用户自定义一个__autoload()函数即可,参数是类名,当实例化一个类是如果没有找到这个类则会查找用户是否定义了__autoload()函数,如果定义了则调用此函数,比如:

//文件1:my_class.php
<?php
class my_class {
    public $id = 123;
}

//文件2:b.php
<?php
function __autoload($class_name){
    //do something...
    include $class_name . '.php';
}

$obj = new my_class();
var_dump($obj);

(2)spl_autoload_register():

相比__autoload()只能定义一个加载器,spl_autoload_register()提供了更加灵活的注册方式,可以支持任意数量的加载器,比如第三方库加载规则不可能保持一致,这样就可以通过此函数注册自己的加载器了,在实现上spl创建了一个队列来保存用户注册的加载器,然后定义了一个spl_autoload函数到EG(autoload_func),当找不到类时内核回调spl_autoload,这个函数再依次调用用户注册的加载器,没调用一个重新检查下查找的类是否在EG(class_table)中已经注册,仍找不到的话继续调用下一

php7内核剖析    2019-09-16 14:48:40    40    0    0

3.4.5 魔术方法

PHP在类的成员方法中预留了一些特殊的方法,它们会在一些特殊的时机被调用(比如创建对象之初、访问成员属性时...),这类方法称为:魔术方法,包括:construct()、destruct()、call()、callStatic()、get()、set()、isset()、unset()、sleep()、wakeup()、toString()、invoke()、 set_state()、 clone() 和 __debugInfo(),关于这些方法的用法这里不作说明,不清楚的可以翻下官方文档。

魔术方法实际是PHP提供的一些特殊操作时的钩子函数,与普通成员方法无异,它们只是与一些操作的口头约定,并没有什么字段标识它们,比如我们定义了一个函数:my_function(),我们希望在这个函数处理对象时首先调用其成员方法my_magic(),那么my_magic()也可以认为是一个魔术方法。

魔术方法与普通成员方法一样保存在zend_class_entry.function_table中,另外针对一些内核常用到的成员方法在zend_class_entry中还有一些单独的指针指向具体的成员方法:

struct _zend_class_entry {
    ...
    union _zend_function *constructor;
    union _zend_function *destructor;
    union _zend_function *clone;
    union _zend_function *__get;
    union _zend_function *__set;
    union _zend_function *__unset;
    union _zend_function *__isset;
    union _zend_function *__call;
    union _zend_function *__callstatic;
    union _zend_function *__tostring;
    union _zend_function *__debugInfo;
    ...
}

在编译成员方法时如果发现与这些魔术方法名称一致,则除了插入zend_class_entry.funct

php7内核剖析    2019-09-16 14:47:46    40    0    0

3.4.4 动态属性

前面介绍的成员属性都是在类中明确的定义过的,这些属性在实例化时会被拷贝到对象空间中去,PHP中除了显示的在类中定义成员属性外,还可以动态的创建非静态成员属性,这种属性不需要在类中明确定义,可以直接通过:$obj->property_name=xxx$this->property_name = xxx为对象设置一个属性,这种属性称之为动态属性,举个例子:

class my_class {
    public $id = 123;

    public function test($name, $value){
        $this->$name = $value;
    }
}

$obj = new my_class;
$obj->test("prop_1", array(1,2,3));
//或者直接:
//$obj->prop_1 = array(1,2,3);

print_r($obj);

test()方法中直接操作了没有定义的成员属性,上面的例子将输出:

my_class Object
(
    [id] => 123
    [prop_1] => Array
        (
             [0] => 1
             [1] => 2
             [2] => 3
        )
)

前面类、对象两节曾介绍,非静态成员属性值在实例化时保存到了对象中,属性的操作按照编译时按顺序编好的序号操作,各对象对其非静态成员属性的操作互不干扰,那么动态属性是在运行时创建的,它是如何存储的呢?

与普通非静态属性不同,动态创建的属性保存在zend_object->properties哈希表中,查找的时候首先按照普通属性在zend_class_entry.properties_info找,没有找到再去zend_object->properties继续查找。动态属性的创建过程(即:修改属性的操作):

//zend_object->handlers->write_property:
ZEND_API void zend_std_write_property(zval *object, zval *member, zval *value, void **cache_slot)
{
    ...
    
php7内核剖析    2019-09-16 14:47:11    18    0    0

3.4.3 继承

继承是面向对象编程技术的一块基石,它允许创建分等级层次的类,它允许子类继承父类所有公有或受保护的特征和行为,使得子类对象具有父类的实例域和方法,或子类从父类继承方法,使得子类具有父类相同的行为。

继承对于功能的设计和抽象是非常有用的,而且对于类似的对象增加新功能就无须重新再写这些公用的功能。

PHP中通过extends关键词继承一个父类,一个类只允许继承一个父类,但是可以多级继承。

class 父类 {
}

class 子类 extends 父类 {
}

前面的介绍我们已经知道,类中保存着成员属性、方法、常量等,父类与子类之间通过zend_class_entry.parent建立关联,如下图所示。

问题来了:每个类都有自己独立的常量、成员属性、成员方法,那么继承类父子之间的这些信息是如何进行关联的呢?接下来我们将带着这个疑问再重新分析一下类的编译过程中是如何处理继承关系的。

3.4.1.5一节详细介绍了类的编译过程,这里再简单回顾下:首先为类分配一个zendclassentry结构,如果没有继承类则生成一条类声明的opcode(ZENDDECLARECLASS),有继承类则生成两条opcode(ZENDFETCHCLASS、ZENDDECLAREINHERITED_CLASS),然后再继续编译常量、成员属性、成员方法注册到zend_class_entry中,最后编译完成后调用zend_do_early_binding()进行 父子类关联 以及 注册到EG(class_table)符号表

如果父类在子类之前定义的,那么父子类之间的关联就是在zend_do_early_binding()中完成的,这里不考虑子类在父类前定义的情况,实际两者没有本质差别,区别在于在哪一个阶段执行。有继承类的情况在zend_do_early_binding()中首先是查找父类,然后调用do_bind_inherited_class()处理,最后将ZEND_FETCH_CLASSZEND_DECLARE_INHERITED_CLASS两条opcode删除,这些过程前面已经介绍过了,下面我们重点看下do_bind_inherited_class()的处理过程。

ZEND_API zend_class_entry *do_bind_inherited_class(
    const zen
php7内核剖析    2019-09-16 14:46:30    11    0    0

3.4.2 对象

对象是类的实例,PHP中要创建一个类的实例,必须使用 new 关键字。类应在被实例化之前定义(某些情况下则必须这样,比如3.4.1最后那几个例子)。

3.4.2.1 对象的数据结构

对象的数据结构非常简单:

typedef struct _zend_object     zend_object;

struct _zend_object {
    zend_refcounted_h gc; //引用计数
    uint32_t          handle;
    zend_class_entry *ce; //所属类
    const zend_object_handlers *handlers; //对象操作处理函数
    HashTable        *properties;
    zval              properties_table[1]; //普通属性值数组
};

几个主要的成员:

(1)handle: 一次request期间对象的编号,每个对象都有一个唯一的编号,与创建先后顺序有关,主要在垃圾回收时用,下面会详细说明。

(2)ce: 所属类的zend_class_entry。

(3)handlers: 这个保存的对象相关操作的一些函数指针,比如成员属性的读写、成员方法的获取、对象的销毁/克隆等等,这些操作接口都有默认的函数。

struct _zend_object_handlers {
    int                                     offset;
    zend_object_free_obj_t                  free_obj; //释放对象
    zend_object_dtor_obj_t                  dtor_obj; //销毁对象
    zend_object_clone_obj_t                 clone_obj;//复制对象

    zend_object_read_property_t             read_property; //读取成员属性
    zend_object_write_property_t            write_property;//修改成员属性
  
php7内核剖析    2019-09-16 14:44:52    7    0    0

3.4.1 类

类是现实世界或思维世界中的实体在计算机中的反映,它将某些具有关联关系的数据以及这些数据上的操作封装在一起。在面向对象中类是对象的抽象,对象是类的具体实例。

在PHP中类编译阶段的产物,而对象是运行时产生的,它们归属于不同阶段。

PHP中我们这样定义一个类:

class 类名 {
    常量;
    成员属性;
    成员方法;
}

一个类可以包含有属于自己的常量、变量(称为“属性”)以及函数(称为“方法”),本节将围绕这三部分具体弄清楚以下几个问题:

  • a.类的存储及索引
  • b.成员属性的存储结构
  • c.成员方法的存储结构
  • d.成员方法的调用过程及与普通function调用的差别

3.4.1.1 类的结构及存储

首先我们看下类的数据结构:

struct _zend_class_entry {
    char type;          //类的类型:内部类ZEND_INTERNAL_CLASS(1)、用户自定义类ZEND_USER_CLASS(2)
    zend_string *name;  //类名,PHP类不区分大小写,统一为小写
    struct _zend_class_entry *parent; //父类
    int refcount;
    uint32_t ce_flags;  //类掩码,如普通类、抽象类、接口,除了这还有别的含义,暂未弄清

    int default_properties_count;        //普通属性数,包括public、private
    int default_static_members_count;    //静态属性数,static
    zval *default_properties_table;      //普通属性值数组
    zval *default_static_members_table;  //静态属性值数组
    zval *static_members_table;
    HashTable function_table;  //成员方法哈希表
    HashTable properties_info; //成员属性基本信息哈希表,key为成员名,value为zend_property_info
    HashTable constants_table; 
php7内核剖析    2019-09-16 14:43:30    12    0    0

3.3.4 全局execute_data和opline

Zend执行器在opcode的执行过程中,会频繁的用到execute_data和opline两个变量,execute_data为zend_execute_data结构,opline为当前执行的指令。普通的处理方式在执行每条opcode指令的handler时,会把execute_data地址作为参数传给handler使用,使用时先从当前栈上获取execute_data地址,然后再从堆上获取变量的数据,这种方式下Zend执行器展开后是下面这样:

ZEND_API void execute_ex(zend_execute_data *ex)
{
    zend_execute_data *execute_data = ex;

    while (1) {
        int ret;

        if (UNEXPECTED((ret = ((opcode_handler_t)execute_data->opline->handler)(execute_data)) != 0)) {
            if (EXPECTED(ret > 0)) {
                execute_data = EG(current_execute_data);
            } else {
                return;
            }
        }
    }
}

执行器实际是一个大循环,从第一条opcode开始执行,execute_data->opline指向当前执行的指令,执行完以后指向下一条指令,opline类似eip(或rip)寄存器的作用。通过这个循环,ZendVM完成opcode指令的执行。opcode执行完后以后指向下一条指令的操作是在当前handler中完成,也就是说每条执行执行完以后会主动更新opline,这里会有下面几个不同的动作:

#define ZEND_VM_CONTINUE()     return  0
#define ZEND_VM_ENTER()        return  1
#define ZEND_VM_LEAVE()        return  2
#define ZEND_VM_RETURN()      
php7内核剖析    2019-09-16 14:42:46    41    0    0

3.3 Zend引擎执行过程

Zend引擎主要包含两个核心部分:编译、执行:

zend_vm

前面分析了Zend的编译过程以及PHP用户函数的实现,接下来分析下Zend引擎的执行过程。

3.3.1 数据结构

执行流程中有几个重要的数据结构,先看下这几个结构。

3.3.1.1 opcode

opcode是将PHP代码编译产生的Zend虚拟机可识别的指令,php7共有173个opcode,定义在zend_vm_opcodes.h中,PHP中的所有语法实现都是由这些opcode组成的。

struct _zend_op {
    const void *handler; //对应执行的C语言function,即每条opcode都有一个C function处理
    znode_op op1;   //操作数1
    znode_op op2;   //操作数2
    znode_op result; //返回值
    uint32_t extended_value; 
    uint32_t lineno; 
    zend_uchar opcode;  //opcode指令
    zend_uchar op1_type; //操作数1类型
    zend_uchar op2_type; //操作数2类型
    zend_uchar result_type; //返回值类型
};

3.3.1.2 zend_op_array

zend_op_array是Zend引擎执行阶段的输入,整个执行阶段的操作都是围绕着这个结构,关于其具体结构前面我们已经讲过了。

zend_op_array

这里再重复说下zend_op_array几个核心组成部分:

  • opcode指令:即PHP代码具体对应的处理动作,与二进制程序中的代码段对应
  • 字面量存储:PHP代码中定义的一些变量初始值、调用的函数名称、类名称、常量名称等等称之为字面量,这些值用于执行时初始化变量、函数调用等等
  • 变量分配情况:与字面量类似,这里指的是当前opcodes定义了多少变量、临时变量,每个变量都有一个对应的编号,执行初始化按照总的数目一次性分配zval,使用时也完全按照编号索引,而不是根据变量名索引

3.3.1.3 zend_executor_globals

zend_executor_globals executor_globals是PHP整个生命周期中最主要的一个结构,是一个全

php7内核剖析    2019-09-16 14:40:56    9    0    0

3.1.2 抽象语法树编译流程

上一小节我们简单介绍了从PHP代码解析为抽象语法树的过程,这一节我们再介绍下从 抽象语法树->Opcodes 的过程。

语法解析过程的产物保存于CG(AST),接着zend引擎会把AST进一步编译为 zend_op_array ,它是编译阶段最终的产物,也是执行阶段的输入,后面我们介绍的东西基本都是围绕zendoparray展开的,AST解析过程确定了当前脚本定义了哪些变量,并为这些变量 __顺序编号 ,这些值在使用时都是按照这个编号获取的,另外也将变量的初始化值、调用的函数/类/常量名称等值(称之为字面量)保存到zend_op_array.literals中,这些字面量也有一个唯一的编号,所以执行的过程实际就是根据各指令调用不同的C函数,然后根据变量、字面量、临时变量的编号对这些值进行处理加工。

我们首先看下zend_op_array的结构,明确几个关键信息,然后再看下ast编译为zend_op_array的过程。

3.1.2.1 zend_op_array数据结构

PHP主脚本会生成一个zend_op_array,每个function也会编译为独立的zend_op_array,所以从二进制程序的角度看zend_op_array包含着当前作用域下的所有堆栈信息,函数调用实际就是不同zend_op_array间的切换。

zend_compile

struct _zend_op_array {
    //common是普通函数或类成员方法对应的opcodes快速访问时使用的字段,后面分析PHP函数实现的时候会详细讲
    ...

    uint32_t *refcount;

    uint32_t this_var;

    uint32_t last;
    //opcode指令数组
    zend_op *opcodes;

    //PHP代码里定义的变量数:op_type为IS_CV的变量,不含IS_TMP_VAR、IS_VAR的
    //编译前此值为0,然后发现一个新变量这个值就加1
    int last_var;
    //临时变量数:op_type为IS_TMP_VAR、IS_VAR的变量
    uint32_t T;
    //PHP变量名数组
    zend_string **vars; //这个数组在ast编译期间配合
php7内核剖析    2019-09-16 14:38:25    14    0    0

3.1 PHP代码的编译

PHP是解析型高级语言,事实上从Zend内核的角度来看PHP就是一个普通的C程序,它有main函数,我们写的PHP代码是这个程序的输入,然后经过内核的处理输出结果,内核将PHP代码"翻译"为C程序可识别的过程就是PHP的编译。

那么这个"翻译"过程具体都有哪些操作呢?

C程序在编译时将一行行代码编译为机器码,每一个操作都认为是一条机器指令,这些指令写入到编译后的二进制程序中,执行的时候将二进制程序load进相应的内存区域(常量区、数据区、代码区)、分配运行栈,然后从代码区起始位置开始执行,这是C程序编译、执行的简单过程。

同样,PHP的编译与普通的C程序类似,只是PHP代码没有编译成机器码,而是解析成了若干条opcode数组,每条opcode就是C里面普通的struct,含义对应C程序的机器指令,执行的过程就是引擎依次执行opcode,比如我们在PHP里定义一个变量:$a = 123;,最终到内核里执行就是malloc一块内存,然后把值写进去。

所以PHP的解析过程任务就是将PHP代码转化为opcode数组,代码里的所有信息都保存在opcode中,然后将opcode数组交给zend引擎执行,opcode就是内核具体执行的命令,比如赋值、加减操作、函数调用等,每一条opcode都对应一个处理handle,这些handler是提前定义好的C函数。

从PHP代码到opcode是怎么实现的?最容易想到的方式就是正则匹配,当然过程没有这么简单。PHP编译过程包括词法分析、语法分析,使用re2c、bison完成,旧的PHP版本直接生成了opcode,PHP7新增了抽象语法树(AST),在语法分析阶段生成AST,然后再生成opcode数组。

zend_compile2

PHP编译阶段的基本过程如下图:

zend_compile_process

后面两个小节将看下 PHP代码->AST->Opcodes 的具体编译过程。

3/4