内核艺术系列:绚丽整个php的zend(1)

让我们的故事从一条最简单的php代码开始:

$obj = 6;

这条代码估计大家再熟悉不过了,不就是写一个变量再给它赋值为6么,然而,你看到的只是echo出来的表象,我既然要说zend引擎的设计,那就得越过php这件华丽的外衣,直视最底层的内核,看看它背着我们到底干了什么。

php其实就是一个c程序,所以zend也是完全用c来进行编写,首先来看看整个php的设计架构吧:

这里写图片描述

这张图可谓是php的经典了,首先上层的应用接口以及跟webserver交互的接口我就不展开说了,来来zend层,首先zend有一个扩展层,如果有需要自己编写php的扩展功能就可以在这里调用zend引擎提供的api进行开发,不过内存泄露在这个时候经常来的悄无声息,主要来看zend引擎是怎么处理上面的代码的。

我们知道,在c程序中要想执行php的代码,首先得把它翻译过来,这个过程就是zend的词法分析,语法分析,翻译的结果是什么呢,这一点跟其他的一些执行引擎基本类似,当然是一条条的操作指令,然后再用一个执行队列包装起来进行顺序的执行,不过zend中,使用一个有趣的结构体_zend_op 来完成指令的封装,而所有的指令结构都放在一个数组中,对于内核的执行者来说,只保留数组的指针就可以了,我们来看看zend内核源代码中它的实现:

struct _zend_op {
    opcode_handler_t handler; // 执行该opcode时调用的处理函数
    znode result;
    znode op1;
    znode op2;
    ulong extended_value;
    uint lineno;
    zend_uchar opcode;  // opcode代码
};

首先看第一个字段handle,写过win32的都应该知道这玩意不就是个句柄么,事实也确实是这样的,它其实是个执行指令到底要干什么的C函数的函数指针,何以见得?我们得从内核执行opcode的入口函数看看到底是怎么执行的:

ZEND_API void zend_execute(zend_op_array *op_array, zval *return_value);

就是它了,它的参数就是上面所说的执行队列的指针,再加上一个数据返回值的指针,函数中比较关键的一句就是:

EX(prev_execute_data) = EG(current_execute_data);
    i_init_execute_data(execute_data, op_array, return_value);
    zend_execute_ex(execute_data);
    zend_vm_stack_free_call_frame(execute_data);

在对execute_data 也就是我们指令结构进行初始化之后,调用了
zend_execute_ex函数,这个函数又有

#if defined(ZEND_VM_FP_GLOBAL_REG) && defined(ZEND_VM_IP_GLOBAL_REG)
((opcode_handler_t)OPLINE->handler)(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU);
if (UNEXPECTED(!OPLINE)) {
#else
if (UNEXPECTED((ret = ((opcode_handler_t)OPLINE->handler)(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU)) != 0)) {
#endif

可以看到handle直接像个函数一样被调用了,也就是这个执行泵最终还是得找到每一个操作对应的函数进行调用,zend中把所有可能出现的函数都放在一张表里面,其实就是一个巨大的数组,大概有3800多个元素,这么多元素,怎么找啊,这时候另外一个字段就派上用场了,opcode为我们服务,这个家伙在我们的语法分析结束后,即编译后这个结构生成时就已经确定,内核会给我们建立并初始化一个opcode的节点,malloc一块内存(当然已经封装在宏中),比如上面的语句,这个值就为ASSIGN,即为分配一个变量的操作,内核中同样也机智地为所有可能出现的操作码进行了一系列的宏定义,比如上面这个可以得到值为38,接下来zend采取了哈希映射的手段,通过一个公式进行映射,这一波映射直接计算出实际操作函数在函数表中的索引,这样尽管数组非常庞大,时间复杂度还是O(1),然后handle就可以指向函数了,函数的参数呢?op1,op2该出现了,两个就是每一次的操作的操作数,比如这一次的操作数有两个,一个为cv型的,即变量$obj,一个为const类型,为int6,有了参数再进行函数调用就完事具备了。

所以函数干了什么呢,我们的变量在内核中怎么存在的?看下V8js引擎,所有的js对象都以handle<>类型存在于v8运行环境中,我们说这些脚本语言的内存管理是高级的,自动智能的,怎么做到呢?首先我们的指针必须是智能的,这样gc才能准确的判断垃圾回收,所以一切的对象都要穿上引擎发给他们的制服,有了这个才知道你是归我管的,我也能更好的控制你,gc也能更好认出你,在你没有用处的时候把你消灭掉,zend引擎跟v8一样,也有自己的外套,这就是zval结构,先来看看zval的定义:

typedef struct _zval_struct zval;
...
struct _zval_struct {
    /* Variable information */
    zvalue_value value;     /* value */
    zend_uint refcount__gc;
    zend_uchar type;    /* active type */
    zend_uchar is_ref__gc;
};

第一个字段就是变量真正的值了,我们知道php的解释型的动态语言,不执行你都不知道$obj到底是什么类型的变量,这种不确定性我们在编译时无法打破,知道最后zval生成时才确定,也就意味zval作为一个容器,需要接受任何一种可能类型的变量,怎么解决呢,看下它的源代码就清楚了:

typedef union _zvalue_value {
    long lval;                  /* long value */
    double dval;                /* double value */
    struct {
        char *val;
        int len;
    } str;
    HashTable *ht;              /* hash table value */
    zend_object_value obj;
} zvalue_value;

联合体就解决了,有点巧妙咯,至于下面的字段,用过智能指针的基本很熟悉,就是引用计数跟是否被引用,这个值在初始化时是为0的,然而上面我给$obj赋值6,首先内核得malloc申请一块内存给zval,然后初始化is_ref__gc为0,并且引用计数为1,obj指向了这个zval嘛,zval实际的东西在哪,6呢,这里又要涉及到zend里面一个高效的数据结构–哈希表,zend所有的符号表都是以哈希表的形式存在的,包括内存管理,像jvm一样,要完成很好的内存管理,得有一个控制的heap,还有空闲内存链表,这里用哈希表来完成小块内存的管理。大块内存的更加复杂,内存的结构如下:

这里写图片描述

在bucket中,两个索引为一个双向链表单元,一个存放头指针,一个存放尾指针,双向链表的好处就是为了删除方便(我觉得这是双向链表在这唯一的价值),也正是有了这种结构,我们php才能各种关联数组用的飞起,字符串都拿来当做key了,现在我们知道我们变量已经分配内存,并且可以通过哈希表进行唯一的映射,其实在这之前,还干了一件很重要的事情,在符号表中注册我们的变量,不然帅不过一秒,变量在当前的scope中就不能被访问了,加入符号表其实在哈希表中加入一个元素就好,至此,我们这个分配的操作基本就干了这些事情了,这就是内核背着我们干了什么。

说到符号表,我们知道php可以有scope这个概念,类似于js的scope,比如函数域,类的定义,这就导致符号的隔离问题,比如当前函数scope中不能访问外部的变量,但外面的变量根据上面的原则也是进行内存分配然后进行符号表的注册,为什么我们访问不到呢,这个的实现就要一个特定的指针,永远指向当前活跃的这个符号表,比如进入一个函数的域,内核新建一个符号表,里面所有局部变量的注册都是在这个内建的符号表,但离开这个scope,这个表就被指针抛弃了,指针指向别的活跃表,或许是另外一个函数,是一个类定义,而可怜的备胎可能已经进入gc的回收缓存区了,具体的我们哈市看看excute函数,在代码中有一个叫做_zend_execute_data 变量,这个就是实现scope切换函数调用的关键,是本次调用的信息体,里面含有一个当前scope建立的符号表指针,opcode的指针,还有父级scope即caller的_zend_execute_data 的指针,便于调用栈的pop,

#ifdef ZEND_VM_FP_GLOBAL_REG
            execute_data = orig_execute_data;
# ifdef ZEND_VM_IP_GLOBAL_REG
            opline = orig_opline;
# endif
            return;
#else
            if (EXPECTED(ret > 0)) {
                execute_data = EG(current_execute_data);
            } else {
# ifdef ZEND_VM_IP_GLOBAL_REG
                opline = orig_opline;
orig_execute_data 

就是原来的数据,上面这段代码就是调用stack一个pop的过程,zend就是这样实现了全局变量与局部变量, 一个巧妙的指针就解决了问题
关于zend引擎其他巧妙的地方,比如内存管理,gc的工作,引用的cow机制(对引用指向的内容进行写是会导致zval的copy,cow机制)函数类等的实现,后面的文章会说到,ok,本次的分享就到这里啦~

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值