标签 - php7内核剖析

php7内核剖析    2019-09-16 15:10:34    25    0    0

4.5 include/require

在实际应用中,我们不可能把所有的代码写到一个文件中,而是会按照一定的标准进行文件划分,include与require的功能就是将其他文件包含进来并且执行,比如在面向对象中通常会把一个类定义在单独文件中,使用时再include进来,类似其他语言中包的概念。

include与require没有本质上的区别,唯一的不同在于错误级别,当文件无法被正常加载时include会抛出warning警告,而require则会抛出error错误,本节下面的内容将以include说明。

在分析include的实现过程之前,首先要明确include的基本用法及特点:

  • 被包含的文件将继承include所在行具有的全部变量范围,比如调用文件前面定义了一些变量,那么这些变量就能够在被包含的文件中使用,反之,被包含文件中定义的变量也将从include调用处开始可以被被调用文件所使用。
  • 被包含文件中定义的函数、类在include执行之后将可以被随处使用,即具有全局作用域。
  • include是在运行时加载文件并执行的,而不是在编译时。

这几个特性可以理解为include就是把其它文件的内容拷贝到了调用文件中,类似C语言中的宏(当然执行的时候并不是这样),举个例子来说明:

//a.php
$var_1 = "hi";
$var_2 = array(1,2,3);

include 'b.php';

var_dump($var_2);
var_dump($var_3);

//b.php
$var_2 = array();
$var_3 = 9;

执行php a.php结果显示$var_2值被修改为array()了,而include文件中新定义的$var_3也可以在调用文件中使用。

接下来我们就以这个例子详细介绍下include具体是如何实现的。

zend_compile_process

前面我们曾介绍过Zend引擎的编译、执行两个阶段(见上图),整个过程的输入是一个文件,然后经过PHP代码->AST->Opcodes->execute一系列过程完成整个处理,编译过程的输入是一个文件,输出是zend_op_array,输出接着成为执行过程的输入,而include的处理实际就是这个过程,执行include时把被包含的文件像主脚本一样编译然后执行,接着在回到调用处继续执行。

include的编译过程非常简单,只编译为一条opcode:

php7内核剖析    2019-09-16 15:08:54    16    0    0

附录1:break/continue按标签中断语法实现

1.1 背景

首先看下目前PHP中break/continue多层循环的情况:

//loop1
while(...){
    //loop2
    for(...){
        //loop3
        foreach(...){
            ...
            break 2;
        }

    }
    //loop2 end
    ...

}

break 2表示要中断往上数两层也就是loop2这层循环,break 2之后将从loop2 end开始继续执行。PHP的break、continue只能根据数值中断对应的循环,当嵌套循环比较多的时候这种方式维护起来就变得很不方便,需要一层层的去数要中断的循环。

了解Go语言的读者应该知道在Go中可以按照标签中断,举个例子来看:

//test.go
func main() {
loop1:
    for i := 0; i < 2; i++ {
        fmt.Println("loop1")

        for j := 0; j < 5; j++ {
            fmt.Println("  loop2")
            if j == 2 {
                break loop1
            }
        }
    }
}

go run test.go将输出:

loop1
  loop2
  loop2
  loop2

break loop1这种语法在PHP中是不支持的,接下来我们就对PHP进行改造,让PHP实现同样的功能。

1.2 实现

想让PHP支持类似Go语言那样的语法首先需要明确PHP中循环及中断语句的实现,关于这两部分内容前面《PHP基础语法实现》一章已经详细介绍过了,这里再简单概括下实现的关键点:

  • 不管是哪种循环结构,其编译时都生成了一个zend_brk_cont_element结构,此结构记录着这个循环break、continue要跳转的位置,以及嵌套的父层循环
  • break/continue编译时分为两个步骤:首先初步编译为临时opcode,此opcode记录着break/continue所在循环层以及要中断的层级(即:break n,默认
php7内核剖析    2019-09-16 15:07:59    24    0    0

8.1 概述

什么是命名空间?从广义上来说,命名空间是一种封装事物的方法。在很多地方都可以见到这种抽象概念。例如,在操作系统中目录用来将相关文件分组,对于目录中的文件来说,它就扮演了命名空间的角色。具体举个例子,文件 foo.txt 可以同时在目录/home/greg 和 /home/other 中存在,但在同一个目录中不能存在两个 foo.txt 文件。另外,在目录 /home/greg 外访问 foo.txt 文件时,我们必须将目录名以及目录分隔符放在文件名之前得到 /home/greg/foo.txt。这个原理应用到程序设计领域就是命名空间的概念。(引用自php.net)

命名空间主要用来解决两类问题:

  • 用户编写的代码与PHP内部的或第三方的类、函数、常量、接口名字冲突
  • 为很长的标识符名称创建一个别名的名称,提高源代码的可读性

PHP命名空间提供了一种将相关的类、函数、常量和接口组合到一起的途径,不同命名空间的类、函数、常量、接口相互隔离不会冲突,注意:PHP命名空间只能隔离类、函数、常量和接口,不包括全局变量。

接下来的两节将介绍下PHP命名空间的内部实现,主要从命名空间的定义及使用两个方面分析。

8.2 命名空间的定义

8.2.1 定义语法

命名空间通过关键字namespace 来声明,如果一个文件中包含命名空间,它必须在其它所有代码之前声明命名空间,除了declare关键字以外,也就是说除declare之外任何代码都不能在namespace之前声明。另外,命名空间并没有文件限制,可以在多个文件中声明同一个命名空间,也可以在同一文件中声明多个命名空间。

namespace com\aa;

const MY_CONST = 1234;
function my_func(){ /* ... */ }
class my_class { /* ... */ }

另外也可以通过{}将类、函数、常量封装在一个命名空间下:

namespace com\aa{
    const MY_CONST = 1234;
    function my_func(){ /* ... */ }
    class my_class { /* ... */ }
}

但是同一个文件中这两种定义方式不能混用,下面这样的定义将是非法的:

namespace com\aa{
    /* ... */
}

name
php7内核剖析    2019-09-16 15:06:57    37    0    0

7.8 常量

常量的具体实现前面章节已经介绍过,这里不再重复。PHP提供了很多用于常量注册的宏,可以在扩展的PHP_MINIT_FUNCTION()中定义:

//注册NULL常量
#define REGISTER_NULL_CONSTANT(name, flags)  \
    zend_register_null_constant((name), sizeof(name)-1, (flags), module_number)

//注册bool常量
#define REGISTER_BOOL_CONSTANT(name, bval, flags)  \
    zend_register_bool_constant((name), sizeof(name)-1, (bval), (flags), module_number)

//注册整形常量
#define REGISTER_LONG_CONSTANT(name, lval, flags)  \
    zend_register_long_constant((name), sizeof(name)-1, (lval), (flags), module_number)

//注册浮点型常量
#define REGISTER_DOUBLE_CONSTANT(name, dval, flags)  \
    zend_register_double_constant((name), sizeof(name)-1, (dval), (flags), module_number)

//注册字符串常量,str类型为char*
#define REGISTER_STRING_CONSTANT(name, str, flags)  \
    zend_register_string_constant((name), sizeof(name)-1, (str), (flags), module_number)

//注册字符串常量,截取指定长度,str类型为char*
#define REGISTER_STRINGL_CONSTANT(name, str, len, flags)  \
    zend_register_stringl_constant((name), sizeof(name)-1, (str), (le
php7内核剖析    2019-09-16 15:06:33    46    0    0

7.7 zval的操作

扩展中经常会用到各种类型的zval,PHP提供了很多宏用于不同类型zval的操作,尽管我们也可以自己操作zval,但这并不是一个好习惯,因为zval有很多其它用途的标识,如果自己去管理这些值将是非常繁琐的一件事,所以我们应该使用PHP提供的这些宏来操作用到的zval。

7.7.1 新生成各类型zval

PHP7将变量的引用计数转移到了具体的value上,所以zval更多的是作为统一的传输格式,很多情况下只是临时性使用,比如函数调用时的传参,最终需要的数据是zval携带的zend_value,函数从zval取得zend_value后就不再关心zval了,这种就可以直接在栈上分配zval。分配完zval后需要将其设置为我们需要的类型以及设置其zend_value,PHP中定义的ZVAL_XXX()系列宏就是用来干这个的,这些宏第一个参数z均为要设置的zval的指针,后面为要设置的zend_value。

  • ZVAL_UNDEF(z): 表示zval被销毁
  • ZVAL_NULL(z): 设置为NULL
  • ZVAL_FALSE(z): 设置为false
  • ZVAL_TRUE(z): 设置为true
  • ZVAL_BOOL(z, b): 设置为布尔型,b为IS_TRUE、IS_FALSE,与上面两个等价
  • ZVAL_LONG(z, l): 设置为整形,l类型为zend_long,如:zval z; ZVAL_LONG(&z, 88);
  • ZVAL_DOUBLE(z, d): 设置为浮点型,d类型为double
  • ZVAL_STR(z, s): 设置字符串,将z的value设置为s,s类型为zend_string*,不会增加s的refcount,支持interned strings
  • ZVAL_NEW_STR(z, s): 同ZVAL_STR(z, s),s为普通字符串,不支持interned strings
  • ZVAL_STR_COPY(z, s): 将s拷贝到z的value,s类型为zend_string*,同ZVAL_STR(z, s),这里会增加s的refcount
  • ZVAL_ARR(z, a): 设置为数组,a类型为zend_array*
  • ZVAL_NEW_ARR(z): 新分配一个数组,主动分配一个zend_array
  • ZVAL_NEW_PERSISTENT_ARR(z): 创建持久化数组,通过m
php7内核剖析    2019-09-16 15:06:04    11    0    0

7.6 函数

7.6.1 内部函数注册

通过扩展可以将C语言实现的函数提供给PHP脚本使用,如同大量PHP内置函数一样,这些函数统称为内部函数(internal function),与PHP脚本中定义的用户函数不同,它们无需经历用户函数的编译过程,同时执行时也不像用户函数那样每一个指令都调用一次C语言编写的handler函数,因此,内部函数的执行效率更高。除了性能上的优势,内部函数还可以拥有更高的控制权限,可发挥的作用也更大,能够完成很多用户函数无法实现的功能。

前面介绍PHP函数的编译时曾经详细介绍过PHP函数的实现,函数通过zend_function来表示,这是一个联合体,用户函数使用zend_function.op_array,内部函数使用zend_function.internal_function,两者具有相同的头部用来记录函数的基本信息。不管是用户函数还是内部函数,其最终都被注册到EG(function_table)中,函数被调用时根据函数名称向这个符号表中查找。从内部函数的注册、使用过程可以看出,其定义实际非常简单,我们只需要定义一个zend_internal_function结构,然后注册到EG(function_table)中即可,接下来再重新看下内部函数的结构:

typedef struct _zend_internal_function {
    /* Common elements */
    zend_uchar type;
    zend_uchar arg_flags[3]; /* bitset of arg_info.pass_by_reference */
    uint32_t fn_flags;
    zend_string* function_name;
    zend_class_entry *scope;
    zend_function *prototype;
    uint32_t num_args;
    uint32_t required_num_args;
    zend_internal_arg_info *arg_info;
    /* END of common elements */

    void (*handler)(INTERNAL_FUNCTION_PARAMETERS); //
php7内核剖析    2019-09-16 15:05:36    18    0    0

7.5 运行时配置

7.5.1 全局变量(资源)

使用C语言开发程序时经常会使用全局变量进行数据存储,这就涉及前面已经介绍过的一个问题:线程安全,PHP设计了TSRM(即:线程安全资源管理器)用于解决这个问题,内核中频繁使用到的EG、CG等都是根据是否开启ZTS封装的宏,同样的,在扩展中也需要必须按照TSRM的规范定义全局变量,除非你的扩展不支持多线程的环境。

PHP为扩展的全局变量提供了一种存储方式:每个扩展将自己所有的全局变量统一定义在一个结构体中,然后将这个结构体注册到TSRM中,这样扩展就可以像使用EG、CG那样访问这个结构体。

这个结构体的定义通过ZEND_BEGIN_MODULE_GLOBALS(extension_name)ZEND_END_MODULE_GLOBALS(extension_name)两个宏完成,这两个宏必须成对出现,中间定义扩展需要的全局变量即可。

ZEND_BEGIN_MODULE_GLOBALS(mytest)
    zend_long    open_cache;
    HashTable    class_table;
ZEND_END_MODULE_GLOBALS(mytest)

展开后实际就是个普通的struct:

typedef struct _zend_mytest_globals {
    zend_long   open_cache;
    HashTable   class_table;
}zend_mytest_globals;

接着创建一个此结构体的全局变量,这时候就会涉及ZTS了,如果未开启线程安全直接创建普通的全局变量即可,如果开启线程安全了则需要向TSRM注册,得到一个唯一的资源id,这个操作也由专门的宏来完成:ZEND_DECLARE_MODULE_GLOBALS(extension_name),展开后:

//ZTS:此时只是定义资源id,并没有向TSRM注册
ts_rsrc_id mytest_globals_id;

//非ZTS
zend_mytest_globals mytest_globals;

最后需要定义一个像EG、CG那样的宏用于访问扩展的全局资源结构体,这一步将使用ZEND_MODULE_GLOBALS_ACCESSOR()宏完成:

#define MYTEST_G(v) ZEND_MODULE
php7内核剖析    2019-09-16 15:05:03    55    0    0

7.4 钩子函数

PHP为扩展提供了5个钩子函数,PHP执行到不同阶段时回调各个扩展定义的钩子函数,扩展可以通过这些钩子函数介入到PHP生命周期的不同阶段中去,这些钩子函数的定义非常简单,PHP提供了对应的宏,定义完成后只需要设置zend_module_entry对应的函数指针即可。

前面已经介绍过PHP生命周期的几个阶段,这几个钩子函数执行的先后顺序:module startup -> request startup -> 编译、执行 -> request shutdown -> post deactivate -> module shutdown。

7.4.1 module_startup_func

这个函数在PHP模块初始化阶段执行,通常情况下,此过程只会在SAPI启动后执行一次。这个阶段可以进行内部类的注册,如果你的扩展提供了类就可以在此函数中完成注册;除了类还可以在此函数中注册扩展定义的常量;另外,扩展可以在此阶段覆盖PHP编译、执行的两个函数指针:zend_compile_file、zend_execute_ex,从而可以接管PHP的编译、执行,opcache的实现原理就是替换了zend_compile_file,从而使得PHP编译时调用的是opcache自己定义的编译函数,对编译后的结果进行缓存。

此钩子函数通过PHP_MINIT_FUNCTION()ZEND_MINIT_FUNCTION()宏完成定义:

PHP_MINIT_FUNCTION(extension_name)
{
    ...
}

展开后:

zm_startup_extension_name(int type, int module_number)
{
    ...
}

最后通过PHP_MINIT()ZEND_MINIT()宏将zend_module_entry的module_startup_func设置为上面定义的函数。

#define PHP_MINIT          ZEND_MODULE_STARTUP_N
#define ZEND_MINIT          ZEND_MODULE_STARTUP_N

#define ZEND_MODULE_STARTUP_N(module)       zm_startup_##module

7.4.2 request_startup_func

此函

php7内核剖析    2019-09-16 15:04:35    10    0    0

7.3 扩展的构成及编译

7.3.1 扩展的构成

扩展首先需要创建一个zend_module_entry结构,这个变量必须是全局变量,且变量名必须是:扩展名称_module_entry,内核通过这个结构得到这个扩展都提供了哪些功能,换句话说,一个扩展可以只包含一个zend_module_entry结构,相当于定义了一个什么功能都没有的扩展。

//zend_modules.h
struct _zend_module_entry {
    unsigned short size; //sizeof(zend_module_entry)
    unsigned int zend_api; //ZEND_MODULE_API_NO
    unsigned char zend_debug; //是否开启debug
    unsigned char zts; //是否开启线程安全
    const struct _zend_ini_entry *ini_entry;
    const struct _zend_module_dep *deps;
    const char *name; //扩展名称,不能重复
    const struct _zend_function_entry *functions; //扩展提供的内部函数列表
    int (*module_startup_func)(INIT_FUNC_ARGS); //扩展初始化回调函数,PHP_MINIT_FUNCTION或ZEND_MINIT_FUNCTION定义的函数
    int (*module_shutdown_func)(SHUTDOWN_FUNC_ARGS); //扩展关闭时回调函数
    int (*request_startup_func)(INIT_FUNC_ARGS); //请求开始前回调函数
    int (*request_shutdown_func)(SHUTDOWN_FUNC_ARGS); //请求结束时回调函数
    void (*info_func)(ZEND_MODULE_INFO_FUNC_ARGS); //php_info展示的扩展信息处理函数
    const char *version; //版本
    ...
    unsigned char 
php7内核剖析    2019-09-16 15:03:38    9    0    0

7.2 扩展的实现原理

PHP中扩展通过zend_module_entry这个结构来表示,此结构定义了扩展的全部信息:扩展名、扩展版本、扩展提供的函数列表以及PHP四个执行阶段的hook函数等,每一个扩展都需要定义一个此结构的变量,而且这个变量的名称格式必须是:{module_name}_module_entry,内核正是通过这个结构获取到扩展提供的功能的。

扩展可以在编译PHP时一起编译(静态编译),也可以单独编译为动态库,动态库需要加入到php.ini配置中去,然后在php_module_startup()阶段把这些动态库加载到PHP中:

int php_module_startup(sapi_module_struct *sf, zend_module_entry *additional_modules, uint num_additional_modules)
{
    ...
    //根据php.ini注册扩展
    php_ini_register_extensions();
    zend_startup_modules();

    zend_startup_extensions();
    ...
}

动态库就是在php_ini_register_extensions()这个函数中完成的注册:

//main/php_ini.c
void php_ini_register_extensions(void)
{
    //注册zend扩展
    zend_llist_apply(&extension_lists.engine, php_load_zend_extension_cb);
    //注册php扩展
    zend_llist_apply(&extension_lists.functions, php_load_php_extension_cb);

    zend_llist_destroy(&extension_lists.engine);
    zend_llist_destroy(&extension_lists.functions);
}

extension_lists是一个链表,保存着根据php.ini中定义的extension=xxx.so取到的全部扩展名称,其中engine是zend扩展,functions为ph

1/4