Home > Backend Development > PHP7 > body text

Analyze variables of PHP underlying kernel source code (2) zend_string

藏色散人
Release: 2023-02-18 06:16:02
forward
1830 people have browsed it

本篇文章给大家介绍《分析PHP底层内核源码之变量 (二) zend_string》。有一定的参考价值,有需要的朋友可以参考一下,希望对大家有所帮助。

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

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

struct _zval_struct {
	zend_value        value;
	 u1;
	 u2;
};
Copy after login

其中 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;
Copy after login

可以看出常用的 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
Copy after login

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

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

[root@2890cf458ee2 Zend]# vim zend_types.h
Copy after login
  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;
Copy after login

在第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];
};
Copy after login

这个结构体包含 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;
Copy after login

可以看到 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 等 


*************************************
Copy after login

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

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

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

Analyze variables of PHP underlying kernel source code (2) zend_string

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个字节    字符串的值存储位置
};
Copy after login

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));
 }
Copy after login

运行结果为 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的数组并不占用空间,因为数组名
本身不占空间,它只是一个偏移量, 数组名这个符号本身代 表了一个不可修改的地址常量 
(注意:数组名永远都不会是指针! ),但对于这个数组的大小,我们
可以进行动态分配,对于编译器而言,数组名仅仅是一个符号,
它不会占用任何空间,它在结构体中,只是代表了一个偏移量,代表一个不可修改的地址常量!
对于柔性数组的这个特点,很容易构造出变成结构体,如缓冲区,数据包等等
Copy after login

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

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

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

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

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

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

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

其实整个过程是

Analyze variables of PHP underlying kernel source code (2) zend_string

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

<?php  
$str = &#39;PHP&#39;;  
printf("字符串内容为".$str);  
printf("字符串长度为".strlen($str));
?>
Copy after login

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

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);
Copy after login

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] = &#39;\0&#39;;
	return ret;
}
Copy after login

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;
}
Copy after login

这个宏代码主要是申请一块连续的内存,内存的大小的计算公式为:实际申请大小= 结构体的大小(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;
Copy after login

然后进行memcpy 函数

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

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()
…
Copy after login

这样就开辟了一个字符串 值为 "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);
}
}
}
Copy after login

显然是用于释放内存的

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

Analyze variables of PHP underlying kernel source code (2) zend_string

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

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

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

The above is the detailed content of Analyze variables of PHP underlying kernel source code (2) zend_string. For more information, please follow other related articles on the PHP Chinese website!

Related labels:
source:zhihu.com
Statement of this Website
The content of this article is voluntarily contributed by netizens, and the copyright belongs to the original author. This site does not assume corresponding legal responsibility. If you find any content suspected of plagiarism or infringement, please contact admin@php.cn
Popular Tutorials
More>
Latest Downloads
More>
Web Effects
Website Source Code
Website Materials
Front End Template