标签归档:深入理解PHP内核

PHP内核中用户函数、内部函数和中间代码的转换

昨天和一朋友在邮件中讨论这样一个问题:zend_internal_function,zend_function,zend_op_array这三种结构是可以相互转化的,这三者的转化是如何进行的呢? 以此文,总结。

在函数调用的执行代码中我们会看到这样一些强制转换:

EX(function_state).function = (zend_function *) op_array;

或者:

EG(active_op_array) = (zend_op_array *) EX(function_state).function;

这种不同结构间的强制转换是如何进行的呢?

首先我们来看zend_function的结构,在Zend/zend_compile.h文件中,其定义如下:

typedef union _zend_function {
    zend_uchar type;    /* MUST be the first element of this struct! */

    struct {
        zend_uchar type;  /* never used */
        char *function_name;
        zend_class_entry *scope;
        zend_uint fn_flags;
        union _zend_function *prototype;
        zend_uint num_args;
        zend_uint required_num_args;
        zend_arg_info *arg_info;
        zend_bool pass_rest_by_reference;
        unsigned char return_reference;
    } common;

    zend_op_array op_array;
    zend_internal_function internal_function;
} zend_function;

这是一个联合体,我们来温习一下联合体的一些特性。 联合体的所有成员变量共享内存中的一块内存,在某个时刻只能有一个成员使用这块内存, 并且当使用某一个成员时,其仅能按照它的类型和内存大小修改对应的内存空间。 我们来看看一个例子:

#include <stdio.h>
#include <stdlib.h>

int main() {
    typedef  union _utype
    {
        int i;
        char ch[2];
    } utype; 

    utype a;

    a.i = 10;
    a.ch[0] = '1';
    a.ch[1] = '1';

    printf("a.i= %d a.ch=%s",a.i, a.ch);
    getchar();

    return (EXIT_SUCCESS);
}

程序输出:a.i= 12593 a.ch=11 当修改ch的值时,它会依据自己的规则覆盖i字段对应的内存空间。 ’1′对应的ASCII码值是49,二进制为00110001,当ch字段的两个元素都为’1′时,此时内存中存储的二进制为 00110001 00110001 转成十进制,其值为12593。

回过头来看zend_function的结构,它也是一个联合体,第一个字段为type, 在common中第一个字段也为type,并且其后面注释为/* Never used*/,此处的type字段的作用就是为第一个字段的type留下内存空间。并且不让其它字段干扰了第一个字段。 我们再看zend_op_array的结构:

struct _zend_op_array {
    /* Common elements */
    zend_uchar type;
    char *function_name;
    zend_class_entry *scope;
    zend_uint fn_flags;
    union _zend_function *prototype;
    zend_uint num_args;
    zend_uint required_num_args;
    zend_arg_info *arg_info;
    zend_bool pass_rest_by_reference;
    unsigned char return_reference;
    /* END of common elements */

    zend_bool done_pass_two;
    ....//  其它字段
}

这里的字段集和common的一样,于是在将zend_function转化成zend_op_array时并不会产生影响,这种转变是双向的。

再看zend_internal_function的结构:

typedef struct _zend_internal_function {
    /* Common elements */
    zend_uchar type;
    char * function_name;
    zend_class_entry *scope;
    zend_uint fn_flags;
    union _zend_function *prototype;
    zend_uint num_args;
    zend_uint required_num_args;
    zend_arg_info *arg_info;
    zend_bool pass_rest_by_reference;
    unsigned char return_reference;
    /* END of common elements */

    void (*handler)(INTERNAL_FUNCTION_PARAMETERS);
    struct _zend_module_entry *module;
} zend_internal_function;

同样存在公共元素,和common结构体一样,我们可以将zend_function结构强制转化成zend_internal_function结构,并且这种转变是双向的。

总的来说zend_internal_function,zend_function,zend_op_array这三种结构在一定程序上存在公共的元素, 于是这些元素以联合体的形式共享内存,并且在执行过程中对于一个函数,这三种结构对应的字段在值上都是一样的, 于是可以在一些结构间发生完美的强制类型转换。 可以转换的列表如下:

  • zend_function可以与zend_op_array互换
  • zend_function可以与zend_internal_function互换

但是一个zend_op_array结构转换成zend_function是不能再次转变成zend_internal_function结构的,反之亦然。

其实zend_function就是一个混合的数据结构,这种结构在一定程序上节省了内存空间。

er结婚了,深入理解PHP内核(TIPI)项目第一阶段发布了

我们的朋友,TIPI团队成员,博客哥,er同学在今天这个春光灿烂,春暖花开,春心荡漾,春情澎湃的大好日子里,兴高采烈的走入了婚姻的殿堂。 在这样一个让人激动不已,激情四射,的日子,TIPI团队决定发布 深入理解PHP内核 项目的第一阶段成果。

大概在半年前,我们在网上相聚,莫名的邂逅,有了我们这样的一个团队。我们有激情,有想法,有行动,也有了我们这个项目。 开始的艰难,没有时间的痛苦,坚持,从而有了今天的发布。一路走来,有辛苦,也有收获,至少记录了我们的青春,至少做了我们想做的!

深入理解PHP内核(TIPI)项目是一个开源的,分析PHP内核的系列文章项目。整个项目是基于PHP5.3版本的源码。 它包括PHP语言中我们常用的变量,函数,类,对象等的实现原理,也包括PHP的虚拟机,内存管理机制,线程安全,错误异常,文件流和PHP5.3新增加的垃圾收集机制,命名空间等。 除了PHP语言本身的特性外,还包括PHP扩展的相关信息。我们希望这个项目可以帮助更多的PHPer可以更加了解PHP语言本身,知其然知其所以然!

第一阶段,我们发布了前四章,从环境的搭建,源码的阅读方式到对于PHP源码的整体把握,再到对于变量和函数的详细解说。随着项目的进展,我们本身对于PHP内核的理解也加深了许多。 后续我们将以章为单位发布后续的章节。现在第5章正在撰写…

在线阅读入口>>>

TIPI团队序

博客哥三者,今聚首于网络一隅,共谋TIPI大计,与诸君共享技术之事: 向来穷PHP内核之事者或多,却鲜有分享之举。哥三者,常流连于中外博客也,若得一佳作,即欣喜若狂,本乐分享,及有学习总结之心,欲为PHP内核之事穷全身之力。

  • reeze,博客哥者,好苹果,好开源, 陶醉于Web开发及架构, 为Ruby之美所折服, 甚爱iOS及其开发, 好一切善美之事物.
  • er,博客哥者,稀饭Linux, Web, 2.0, Ajax, C, PHP, Javascript, CSS等。乃一以代码为乐之码农也。
  • phppan,博客哥者,好书,好PHP,亲于PHP,C,Ajax,程序架构等

是以三人之力行分享之事,转GIT,习markdown,论项目之计于深夜,何怕事之不成?务使PHP内核之事向众人知。 为此特示。

项目大事记

  • 2010/12/28 14:47 pan向reeze提议写一个PHP内核系列文章,一拍即合.
  • 2010/12/28 15:10 er同学加入.组织正式形成.
  • 2010/12/30 11:11 pan发出<<深入理解PHP内核>>第一份完整目录草稿.
  • 2010/12/31 21:14 举行第一次三方会谈,结合pan和reeze的目录草稿确定了正式目录. 标志着TIPI团队项目的正式确立.. (鼓掌).
  • 2011/01/01 05:08 reeze向github版本库提交了完整的项目, TIPI项目开始进入实施阶段
  • 2011/01/06 15:22 经过哥三激烈的讨论后做出艰难的决定,我们的项目域名正式确定为php-internal.com.(撒花无数).
  • 2011/02/14 23:32 在这个几人欢喜几人愁,充满花香的日子里, 哥三在深夜确定了TIPI项目的第一次整体发布流程,并且定稿了前三章的大纲以及确定了发布前的调整工作。
  • 2011/02/25 02:53 虽然我们还没有正式开始推广TIPI, 但已经有人开始关注TIPI了. 恭喜icodeuhttp://blog.icodeu.com同学成为我们第一位留下脚印的同学(看留言时间,也是个夜猫子啊.)
  • 2011/03/10 11:22 经过TIPI团队的慎重考虑, TIPI团队新增一员大将:honestqiao同学, 欢迎他的加入!

特别鸣谢

我们需要感谢我们家里的领导,没有有她们的支持,也就没有我们今天的发布,感谢她们的包容,感谢她们的照顾,感谢她们的理解和支持。谢谢!

TIPI0203-PHP脚本的执行

在前面的章节介绍了PHP的生命周期,PHP的SAPI,这些内容都是处于上层的,在这个下面是对于PHP本身的解析和执行。这一小节我们介绍PHP脚本的执行。

目前的编程语言可以分为两大类:

  • 第一类是像C/C++, .NET, Java之类的编译型语言, 它们的共性是: 运行之前必须对源代码进行编译,然后运行编译后的目标文件.
  • 第二类比如:PHP, Javascript, Ruby, Python这些解释型语言, 他们都无需经过编译即可”运行”. 虽然可以理解为直接运行, 但它们并不是真的直接就被能被机器理解, 机器只能理解机器语言,那这些语言是怎么被执行的呢, 一般这些语言都需要一个解释器, 由解释器来执行这些源码, 实际上这些语言还是会经过编译环节, 只不过它们一般会在运行的时候实时进行编译. 为了效率,并不是所有语言在每次执行的时候都会重新编译一遍, 比如PHP的各种opcode缓存扩展(如APC, xcache, eAccelerator等),比如Python会将编译的中间文件保存成pyc/pyo文件,避免每次运行重新进行编译所带来的性能损失.

PHP的脚本的执行也需要一个解释器, 比如命令行下的php程序,或者apache的mod_php模块等等. 前一节提到了PHP的SAPI接口, 下面就以PHP命令行程序为例解释PHP脚本是怎么被执行的. 例如如下的这段PHP脚本:

<?php
$str = "Hello, Tipi!\n";
echo $str;

假设上面的代码保存在名为hello.php的文件中, 用PHP命令行程序执行这个脚本:

$ php --help  # 显示php程序可以接受的参数
$ php ./hello.php

这段代码的输出显然是Hello, Tipi!, 那么在执行脚本的时候PHP/Zend都做了些什么呢? 这些语句是怎么样让php输出这段话的呢? 下面将一步一步的进行介绍.

程序的执行

  1. 如上例中, 传递给php程序需要执行的文件, php程序完成基本的准备工作后启动PHP及Zend引擎, 加载注册的扩展模块.
  2. 初始化完成后读取脚本文件,Zend引擎对脚本文件进行词法分析,语法分析. 然后编译成opcode 执行. 如果安装了apc之类的opcode缓存, 编译环节可能会被跳过而直接从缓存中读取opcode执行.

脚本的编译执行

PHP在读取到脚本文件后首先对代码进行词法分析, PHP的词法分析器是通过lex生成的, 词法规则文件在$PHP_SRC/Zend/zend_language_scanner.l, 这一阶段lex会会将源代码按照词法规则切分一个一个的标记(token). PHP中提供了一个函数token_get_all(), 该函数接收一个字符串参数, 返回一个按照词法规则切分好的数组. 例如将上面的php代码作为参数传递给这个函数:

<?php
$code =<<<PHP_CODE
<?php
$str = "Hello, Tipi\n";
echo $str;
PHP_CODE;

var_dump(token_get_all($code));

运行上面的脚本你将会看到一如下的输出

array (
  0 =>
  array (
    0 => 368,       // 脚本开始标记
    1 => '<?php     // 匹配到的字符串
',
    2 => 1,
  ),
  1 =>
  array (
    0 => 371,
    1 => ' ',
    2 => 2,
  ),
  2 => '=',
  3 =>
  array (
    0 => 371,
    1 => ' ',
    2 => 2,
  ),
  4 =>
  array (
    0 => 315,
    1 => '"Hello, Tipi
"',
    2 => 2,
  ),
  5 => ';',
  6 =>
  array (
    0 => 371,
    1 => '
',
    2 => 3,
  ),
  7 =>
  array (
    0 => 316,
    1 => 'echo',
    2 => 4,
  ),
  8 =>
  array (
    0 => 371,
    1 => ' ',
    2 => 4,
  ),
  9 => ';',

这也是Zend引擎词法分析做的事情,将代码切分为一个个的标记,然后使用语法分析器(PHP使用yacc生成语法分析器, 规则见$PHP_SRC/Zend/zend_language_parser.y), yacc根据规则进行相应的处理, 如果代码找不到匹配的规则,也就是语法错误时Zend引擎会停止,并输出错误信息. 比如缺少括号,或者不符合语法规则的情况都会在这个环节检查. 在匹配到相应的语法规则后,Zend引擎还会进行编译, 将代码编译为opcode, 完成后,Zend引擎会执行这些opcode, 在执行opcode的过程中还有可能会继续重复进行编译-执行, 例如执行eval,include/require等语句, 因为这些语句还会包含或者执行其他文件或者字符串中的脚本.

例如上例中的echo语句会编译为一条ZEND_ECHO指令, 执行过程中,该指令由C函数zend_print_variable(zval* z)执行,将传递进来的字符串打印出来. 为了方便理解, 本例中省去了一些细节,例如opcode指令和处理函数之间的映射关系等. 后面的章节将会详细介绍.

如果想直接查看生成的Opcode,可以使用php的vld扩展查看。扩展下载地址: http://pecl.php.net/package/vld。Win下需要自己编译生成dll文件。

作者:TIPI团队