• 技术文章 >后端开发 >PHP7

    分析PHP底层内核源码之变量 (二) zend_string

    藏色散人藏色散人2021-06-10 14:30:51转载156
    本篇文章给大家介绍《分析PHP底层内核源码之变量 (二) zend_string》。有一定的参考价值,有需要的朋友可以参考一下,希望对大家有所帮助。

    相关文章推荐:《解析PHP底层内核源码之变量 (一)》《分析PHP底层内核源码之变量 (三)

    在变量(一)中 我们主要通读了_zval_struct 来深入了解 PHP7以上版本的 变量实现和内存占用

    struct _zval_struct {
    	zend_value        value;
    	 u1;
    	 u2;
    };

    其中 zend_value 结构体的核心代码如下

    typedef union _zend_value {
    	zend_long         lval;          //整型
            double            dval;          //浮点型
            zend_refcounted  *counted;     //获取不同类型结构的gc头部的指针
            zend_string      *str;        //string字符串 的指针
            zend_array       *arr;        //数组指针
            zend_object      *obj;        //object 对象指针
            zend_resource    *res;         ///资源类型指针
            zend_reference   *ref;       //引用类型指针   比如你通过&$c  定义的
            zend_ast_ref     *ast;     // ast 指针  线程安全 相关的 内核使用的  
            zval             *zv;   // 指向另外一个zval的指针  内核使用的
            void             *ptr;   //指针  ,通用类型  内核使用的
            zend_class_entry *ce;    //类 ,内核使用的
            zend_function    *func;   // 函数 ,内核使用的
            struct {
             uint32_t w1;//自己定义的。 无符号的32位整数
             uint32_t w2;//同上
             } ww;
     } zend_value;

    可以看出常用的 zend_value包含 上面几种 会不会有个疑问 怎么没有布尔型呢?

    其实这里这里的 zend_value 只是负责存储 内容 同样你也会发现 也没有null类型

    再次回去打开 zend_types.h

    [root@2890cf458ee2 Zend]# vim zend_types.h
    /* regular data types */
    #define IS_UNDEF					0
    #define IS_NULL						1
    #define IS_FALSE					2
    #define IS_TRUE						3
    #define IS_LONG						4
    #define IS_DOUBLE					5
    #define IS_STRING					6
    #define IS_ARRAY					7
    #define IS_OBJECT					8
    #define IS_RESOURCE					9
    #define IS_REFERENCE				10
    
    /* constant expressions */
    #define IS_CONSTANT_AST				11
    
    /* internal types */
    #define IS_INDIRECT             	13
    #define IS_PTR						14
    #define IS_ALIAS_PTR				15
    #define _IS_ERROR					15
    
    /* fake types used only for type hinting (Z_TYPE(zv) can not use them) */
    #define _IS_BOOL					16
    #define IS_CALLABLE					17
    #define IS_ITERABLE					18
    #define IS_VOID						19
    #define _IS_NUMBER					20

    可以看到 在代码里 定义了 20种类型 其中前11种 是常用类型 后面的类型包含ast和 internal 等 不常用 后面到内存管理 会依次展开 ast和 internal的使用

    言归正传 在PHP中 管理字符串会使用zend_string。每次 PHP 需要使用字符串时,都会使用zend_string结构, PHP没有用原生c语言的 char 而是封装了个结构体

    [root@2890cf458ee2 Zend]# vim zend_types.h
      82 typedef struct _zend_object_handlers zend_object_handlers;
      83 typedef struct _zend_class_entry     zend_class_entry;
      84 typedef union  _zend_function        zend_function;
      85 typedef struct _zend_execute_data    zend_execute_data;
      86
      87 typedef struct _zval_struct     zval;
      88
      89 typedef struct _zend_refcounted zend_refcounted;
      90 typedef struct _zend_string     zend_string;
      91 typedef struct _zend_array      zend_array;
      92 typedef struct _zend_object     zend_object;
      93 typedef struct _zend_resource   zend_resource;
      94 typedef struct _zend_reference  zend_reference;
      95 typedef struct _zend_ast_ref    zend_ast_ref;
      96 typedef struct _zend_ast        zend_ast;

    在第90行看到 zend_string实际上是_zend_string的别名

    别名是c语言特有的一种 形式

    继续跟到第235行 看到了 _zend_string是一个结构体

    struct _zend_string {
    	zend_refcounted_h gc;
    	zend_ulong        h;                /* hash value */
    	size_t            len;
    	char              val[1];
    };

    这个结构体包含 4个部分

    其中 有gc (这显然又是一个自定义类型 ) h(也是一个自定义类型) len (整型) val[1](字符串类型,但是这个名字怎么怪怪的)。

    我们继续跟gc 这个类型

    typedef struct _zend_refcounted_h {
    	uint32_t         refcount;			/* reference counter 32-bit */
    	union {
    		uint32_t type_info;
    	} u;
    } zend_refcounted_h;

    可以看到 zend_refcounted_h 是 _zend_refcounted_h结构体的别名

    这个结构体 包括 一个 32位纯数字的 refcount 和一个联合体u 联合体u里面包括一个 type_info zend_refcounted_h 占用8字节 ,refount英文翻译成中文是引用的意思 显然 这个 zend_refcounted_h是为了引用计数和字符串类别存储用的。

    引用计数存放在refcount字段、字符串所属的变量类别则存储在type字段。zend_string结构体中因为加入了gc字段,使得其和数组、对象一样可被多个zval引用 这非常巧妙了。

    [root@2890cf458ee2 Zend]# vim zend_types.h
    [root@2890cf458ee2 Zend]# php -v
    PHP 7.4.15 (cli) (built: Feb 22 2021 08:46:50) ( NTS )
    Copyright (c) The PHP Group
    Zend Engine v3.4.0, Copyright (c) Zend Technologies
    ****************************************
    我的版本为 7.4.15 你如果看过其他大佬做的源码文章会发现跟我这个版本的_zend_refcounted_h
    结构体有所不同 ,比如 陈雷大佬的书中 的_zend_refcounted_h结构体会包含一个联合体 
    联合体里面又有用于垃圾回收颜色用的 gc_info 等 
    
    
    *************************************

    个人认为是因为 zend_zval 的u1 已经包含了 type_flags type 等字段 所以在PHP7.4版本里zend_refcounted_h 就弃用了这些值

    在 zend_string结构体 第二个值 h 指向了zend_ulong

    通过追踪代码 发现 zendulong 在 zend_long.h 中

    a7f116f0227ca1b050cd77d122b3fa0.png

    h是typedef uint64_t zend_ulong类型的一个变量,保存字符串对应的哈希值,其后续会用在数组里面。他占用8个字节

    我们把 zend_string 加上注释

    struct _zend_string {
    	zend_refcounted_h gc; //占用8个字节 用于gc的计数和字符串类型的记录
    	zend_ulong        h;        // 占用8个字节 用于记录 字符串的哈希值
    	size_t            len;       //占用8个字节    字符串的长度
    	char              val[1];   //占用1个字节    字符串的值存储位置
    };

    len和val[1]用于标识字符串,c语言中字符串的表示形式可以以\0结尾,通过遍历得到字符串长度,但是其非二进制安全,如字符串中本身就包含\0,那么该字符串\0后面的字符串会被截断,这里len用于保存字符串的长度, val是一个柔性数组。实现的字符串是二进制安全的。

    关于\0 可以看以下 c语言代码

    main(){
     char a[] = "aa\0";
     char b[] = "aa\0aaaaaaaaaaaaaaaaaa";
        
        printf(strlen(a));
        printf(strlen(b));
     }

    运行结果为 2 2

    也就是说C语言认为a和b这两个字符串是相等的,而且ab的长度为都为2

    但是在PHP中因为有了zend_string的存在 可以做到二进制安全

    例如,字符串 “foo” 在zend_string中存储为 “foo\0”,且它的长度为3。另外,字符串 “foo\0bar” 将存储为 “foo\0bar\0”,且其长度为7。

    至于什么是柔性数组 参考goole搜的介绍

    1、什么是柔性数组?
    柔性数组既数组大小待定的数组, C语言中结构体的最后一个元素可以是大小未知的数组,也就是所谓的0长度,
    所以我们可以用结构体来创建柔性数组。
    2、柔性数组有什么用途 ?
    它的主要用途是为了满足需要变长度的结构体,为了解决使用数组时内存的冗余和数组的越界问题。
    3、用法 :在一个结构体的最后 ,申明一个长度为空的数组,就可以使得这个结构体是可变长的。
    对于编译器来说,此时长度为0的数组并不占用空间,因为数组名
    本身不占空间,它只是一个偏移量, 数组名这个符号本身代 表了一个不可修改的地址常量 
    (注意:数组名永远都不会是指针! ),但对于这个数组的大小,我们
    可以进行动态分配,对于编译器而言,数组名仅仅是一个符号,
    它不会占用任何空间,它在结构体中,只是代表了一个偏移量,代表一个不可修改的地址常量!
    对于柔性数组的这个特点,很容易构造出变成结构体,如缓冲区,数据包等等

    用柔性数组的好处很明显,读写字符串值时可以省一次内存读写

    那为什么不用val[0] 或者var[] 而是var[1] 呢 因为 为了兼容c99的标准 c99里不允许变长数组的定义,但是支持var[1] 你可以理解为 为了兼容不同版本的c编译器即可。

    len字段是记录 字符串的长度 跟上面的柔性数组一配合就知道 字符串的真实长度了 读取的数据长度以自身结构体len值为准。同时这也是典型的空间换时间算法 也节省了还要去计算字符串的长度的消耗。

    所以 zend_string 结构体整体占用 25个字节 但是因为内存对齐 所以占用32个字节

    以上你已经掌握了 字符串 结构体的 基础知识

    在PHP中 封装了很多 操作字符串的基础宏 一般在 zend_string.h 中

    下面这行代码 php是怎么实现的?

    其实整个过程是

    3991c06d19282253d469cc6812a8681.png

    (先不要考虑 词法分析 语法分析 AST 等过程)

    <?php  
    $str = 'PHP';  
    printf("字符串内容为".$str);  
    printf("字符串长度为".strlen($str));
    ?>

    其实对应的 ‘伪代码’如下

    zend_string *s;
    zend_string_init(s,"PHP", strlen("PHP"), 0) 
    // 其中 zend_string_init 为初始化一个普通字符串 s
    // 存储字符串到s 到变量 zval a 中 
    ZVAL_STR(&a, s);
    
    php_printf("子字符串内容为", Z_STRVAL(a));
    php_printf("字符串长度为", Z_STRLEN(a));
    zend_string_release(a);

    zend_string_init()函数(实际上是宏)计算完整的char *字符串和它的长度。最后一个参数的类型为 int 值为 0 或 1。如果传0,则通过 Zend 内存管理使用请求绑定的堆分配。这种分配在当前请求结束后时销毁。如果不销毁,内存就会泄漏。如果传1,则要求了所谓的“持久”分配,将使用传统的 C语言的malloc()调用。

    说人话就是zend_string_init函数把一个普通字符串初始化成zend_string

    在zend_string.h 中 第152行 可以找到

     //上述我们传进来  zend_string_init("PHP", 3, 0);
    static zend_always_inline zend_string *zend_string_init(const char *str, size_t len, int persistent)
    { 
           //分配内存及初始化 初始化内存的值
    	zend_string *ret = zend_string_alloc(len, persistent);
           //拷贝 str 到 zend_string 中的val中 
    	memcpy(ZSTR_VAL(ret), str, len);
          //把字符串末尾加上\0 毕竟要依赖c语言 所以最最底层要按照人家规则走
    	ZSTR_VAL(ret)[len] = '\0';
    	return ret;
    }

    zend_string_init 第一步 又调用了 zend_string_alloc 然后进行 memcpy 执行ZSTR_VAL

    最后返回一个 字符串变量

    下面是zend_string_alloc的代码

    static zend_always_inline zend_string *zend_string_alloc(size_t len, int persistent)
    {
    zend_string *ret = (zend_string *)pemalloc(ZEND_MM_ALIGNED_SIZE(_ZSTR_STRUCT_SIZE(len)), persistent);
    GC_SET_REFCOUNT(ret, 1);
    GC_TYPE_INFO(ret) = IS_STRING | ((persistent ? IS_STR_PERSISTENT : 0) << GC_FLAGS_SHIFT);
    ZSTR_H(ret) = 0;
    ZSTR_LEN(ret) = len;
    return ret;
    }

    这个宏代码主要是申请一块连续的内存,内存的大小的计算公式为:实际申请大小= 结构体的大小(24) + 字符串的长度(len)+1,实际申请大小是按照8字节对齐的,不一定等于实际计算的结果。 len = string.len + new_str_len + string_struct_len + 1

    这个+1就是为了追加 \0 使用的

    并且还做了初始化 zend_string 工作

    //这是个宏  设置 zend_string 中的 h值    还记得h值是干嘛的吗?
            ZSTRH(ret) = 0;  
    //这是个宏 设置 zend_string 中的len的值  
    	ZSTR_LEN(ret) = len;

    然后进行memcpy 函数

    C 库函数 中的memcpy()
    void *memcpy(void *str1, const void *str2, size_t n)
    参数
    str1 -- 指向用于存储复制内容的目标数组,类型强制转换为 void* 指针。
    str2 -- 指向要复制的数据源,类型强制转换为 void* 指针。
    n -- 要被复制的字节数。
    返回值
    该函数返回一个指向目标存储区 str1 的指针

    memcpy主要用于拷贝数据 里面包含了一个宏 ZSTR_VAL

    这个宏是设置zend_string的val中数据

    通过阅读源码我们可以发现
    以ZSTR_***(s)开头的每个宏都会作用到 zend_string。
    ZSTR_VAL()   访问字符数组 
    ZSTR_LEN()  访问长度信息 
    ZSTR_HASH() 访问哈希值
    …
    以 Z_STR**(z) 开头的宏都会作用于到 zval 中的 zend_string 。
    Z_STRVAL() 
    Z_STRLEN()
    Z_STRHASH()
    …

    这样就开辟了一个字符串 值为 "PHP"

    下一步又是一个宏 zend_string_release

    static zend_always_inline void zend_string_release(zend_string *s)
    {
    if (!ZSTR_IS_INTERNED(s)) {
    if (GC_DELREF(s) == 0) {
    pefree(s, GC_FLAGS(s) & IS_STR_PERSISTENT);
    }
    }
    }

    显然是用于释放内存的

    关于zend_string 的宏 可以参考以下注释 (慢慢会依次展开讲解)

    05b9fd10711b29bf2b04707f5660e28.png

    接下来的小节我们将继续 分析zend_string 的写时赋值 和 内存管理 以及字符串的各种操作的实现。所以你务必吸收上面的内容 并且打开源码进行查看

    感谢陈雷前辈的《PHP7源码底层设计与实现》

    ▏本文经原作者PHP崔雪峰同意,发布在php中文网,原文地址:https://zhuanlan.zhihu.com/p/352830733

    以上就是分析PHP底层内核源码之变量 (二) zend_string的详细内容,更多请关注php中文网其它相关文章!

    声明:本文转载于:PHP崔雪峰,如有侵犯,请联系admin@php.cn删除
    专题推荐:PHP底层 zend_string
    上一篇:解析PHP底层内核源码之变量 (一) 下一篇:分析PHP底层内核源码之变量 (三)
    第16期线上培训班

    相关文章推荐

    • PHP7底层做了哪些优化• 详解PHP底层运行机制与工作原理• 从PHP底层源码视角分析PHP 7数组的实现• 解析PHP底层内核源码之变量 (一)

    全部评论我要评论

  • 取消发布评论发送
  • 1/1

    PHP中文网