广告:宝塔Linux面板高效运维的服务器管理软件 点击【 https://www.bt.cn/p/uNLv1L 】立即购买
本篇文章给大家介绍《分析PHP底层内核源码之变量 (三)》。有一定的参考价值,有需要的朋友可以参考一下,希望对大家有所帮助。
相关文章推荐:《解析PHP底层内核源码之变量 (一)》《分析PHP底层内核源码之变量 (二) zend_string》
上文通读了zend_string的 结构体 的源码。
struct _zend_string {zend_refcounted_h gc; //占用8个字节 用于gc的计数和字符串类型的记录zend_ulong h; // 占用8个字节 用于记录 字符串的哈希值size_t len; //占用8个字节 字符串的长度char val[1]; //占用1个字节 字符串的值存储位置};登录后复制
其中 len 变量 使得 zend_string 具备了 二进制安全 的特性
gc 也就是zend_refcounted_h 结构体的加持 可以实现 写时复制 (写时拷贝 copy-on-write) 的功能
typedef struct _zend_refcounted_h {uint32_t refcount;//引用数union {uint32_t type_info; //字符串所属的变量类别} u;} zend_refcounted_h;登录后复制
copy-on-write 技术在redis 和linux内核里广泛应用
比如 Redis需要创建当前服务器进程的子进程,而大多数操作系统都采用写时复制(copy-on-write)来优化子进程的使用效率,所以在子进程存在期间,服务器会提高负载因子的阈值,从而避免在子进程存在期间进行哈希表扩展操作,避免不必要的内存写入操作,最大限度地节约内存。
PHP 7也采用了写时复制从而在进行赋值操作时比较节省内存,当字符串在赋值时并不直接拷贝一份数据,而是把zend_string结构体里的 _zend_refcounted_h中的 refcount 做+1 运算,字符串销毁时再把zend_string结构体里的 _zend_refcounted_h中的 refcount 做-1 运算。
如果您看过 陈雷大佬写的 《PHP底层源码设计与实现》 一书 可以会发现 稍微不一样 因为 我的版本是PHP7.4 书中版本 与我本地安装的不同 ,猜测可能是为了统一进行内存管理。
zend_string结构体里面的gc.u.flags字段,gc.u.flags总共有8位,每个类别占一位,可以重复打标签,理论上最多打8种标签。目前PHP 7源码主要涉及以下几种:1)对于临时的普通字符串,flags字段被标识为0。2)对于内部字符串,用于存储PHP代码中的字面量、标识符等,flags字段被标识成IS_STR_PERSISTENT |IS_STR_INTERNED。3)对于PHP已知字符串,flags字段会被标识成IS_STR_PERSISTENT|IS_STR_INTERNED|IS_STR_PERMANENT。
--------摘自 《PHP底层源码设计与实现》
在 PHP7.4源码底层会给 变量进行分类 方便内存的管理 其依赖于 zend_zval结构体里的u1.v.type_flags字段
struct _zval_struct { 197 zend_value value; //变量 198 union { 199 struct { 200 ZEND_ENDIAN_LOHI_3( 201 zend_uchar type, //变量类型 202 zend_uchar type_flags,//可以用于变量的分类 203 union { 204 uint16_t extra; /* not further specified */ 205 } u) 206 } v; 207 uint32_t type_info;//变量类型 208 } u1; 209 u2; 222 };登录后复制
在555行有如下代码
/* zval.u1.v.type_flags */#define IS_TYPE_REFCOUNTED(1<<0) //REFCOUNTED 可以计数的#define IS_TYPE_COLLECTABLE(1<<1) // TYPE_COLLECTABLE可收集的#if 1/* This optimized version assumes that we have a single "type_flag" *//* IS_TYPE_COLLECTABLE may be used only with IS_TYPE_REFCOUNTED *//*优化后的版本假设我们有一个单一的"type_flag" *//* IS_TYPE_COLLECTABLE只能与IS_TYPE_REFCOUNTED一起使用*/# define Z_TYPE_INFO_REFCOUNTED(t)(((t) & Z_TYPE_FLAGS_MASK) != 0)#else# define Z_TYPE_INFO_REFCOUNTED(t)(((t) & (IS_TYPE_REFCOUNTED << Z_TYPE_FLAGS_SHIFT)) != 0)#endif登录后复制
所以PHP7.4版本中 zval.u1.v.type_flags 只有两种类型 0或者 1 同时我也看了下最新的PHP8版本代码 也是如此
为了更好的深入了解源码 也将 前面两节内容穿起来 我们安装gdb 来调试下PHP
GDB(GNU symbolic debugger)简单地说就是一个调试工具。它是一个受通用公共许可证即GPL保护的自由软件。像所有的调试器一样,GDB可以让你调试一个程序,包括让程序在你希望的地方停下,此时你可以查看变量、寄存器、内存及堆栈。更进一步你可以修改变量及内存值。GDB是一个功能很强大的调试器,它可以调试多种语言。在此我们仅涉及 C 和 C++ 的调试,而不包括其它语言。还有一点要说明的是,GDB是一个调试器,而不像 VC 是一个集成环境。你可以使用一些前端工具如XXGDB、DDD等。他们都有图形化界面,因此使用更方便,但它们仅是GDB的一层外壳。因此,你仍应熟悉GDB命令。事实上,当你使用这些图形化界面时间较长时,你才会发现熟悉GDB命令的重要性。
-----摘自oschina
[root@a3d3f47671d9 /]# php -vPHP 7.4.15 (cli) (built: Feb 21 2021 09:07:07) ( NTS )Copyright (c) The PHP GroupZend Engine v3.4.0, Copyright (c) Zend Technologies[root@a3d3f47671d9 /]# gbv bash: gbv: command not found[root@a3d3f47671d9 /]# gdbbash: gdb: command not found[root@a3d3f47671d9 /]# yum install gdb登录后复制
.........
新建一个 PHP 文件
[root@a3d3f47671d9 cui]# vim php7-4-test-zval.php php7-4-test-zval.php Buffers <?php $a="abcdefg"; echo $a; $b=88; echo $b; $c = $a; echo $c; echo $a; $c ="abc"; echo $c; echo $a;登录后复制
用 gdb 运行 PHP
[root@a3d3f47671d9 cui]# gdb phpGNU gdb (GDB) Red Hat Enterprise Linux 8.2-12.el8Copyright (C) 2018 Free Software Foundation, Inc.License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>This is free software: you are free to change and redistribute it.There is NO WARRANTY, to the extent permitted by law.Type "show copying" and "show warranty" for details.This GDB was configured as "x86_64-redhat-linux-gnu".Type "show configuration" for configuration details.For bug reporting instructions, please see:<http://www.gnu.org/software/gdb/bugs/>.Find the GDB manual and other documentation resources online at: <http://www.gnu.org/software/gdb/documentation/>.For help, type "help".Type "apropos word" to search for commands related to "word"...Reading symbols from php...done.(gdb) b ZEND_ECHO_SPEC_CV_HANDLER # b 命令意思是打断点Breakpoint 1 at 0x6dfe80: file /cui/php-7.4.15/Zend/zend_vm_execute.h, line 36987.(gdb) r php7-4-test-zval.phpStarting program: /usr/local/bin/php php7-4-test-zval.phpwarning: Error disabling address space randomization: Operation not permittedMissing separate debuginfos, use: yum debuginfo-install glibc-2.28-127.el8.x86_64warning: Loadable section ".note.gnu.property" outside of ELF segmentswarning: Loadable section ".note.gnu.property" outside of ELF segmentswarning: Loadable section ".note.gnu.property" outside of ELF segments[Thread debugging using libthread_db enabled]Using host libthread_db library "/lib64/libthread_db.so.1".warning: Loadable section ".note.gnu.property" outside of ELF segmentswarning: Loadable section ".note.gnu.property" outside of ELF segmentsBreakpoint 1, ZEND_ECHO_SPEC_CV_HANDLER () at /cui/php-7.4.15/Zend/zend_vm_execute.h:3698736987SAVE_OPLINE();Missing separate debuginfos, use: yum debuginfo-install libxcrypt-4.1.1-4.el8.x86_64 libxml2-2.9.7-8.el8.x86_64 sqlite-libs-3.26.0-11.el8.x86_64 xz-libs-5.2.4-3.el8.x86_64 zlib-1.2.11-16.el8_2.x86_64登录后复制
可以看到 我的报错了 因为我是在docker里跑的 centos镜像 查了一些资料解决方法如下
编辑 /etc/yum.repos.d/CentOS-Debuginfo.repo 文件
修改enable=1
然后 yum install yum-utils
然后 dnf install glibc-langpack-en
yum debuginfo-install libxcrypt-4.1.1-4.el8.x86_64 libxml2-2.9.7-8.el8.x86_64 sqlite-libs-3.26.0-11.el8.x86_64 xz-libs-5.2.4-3.el8.x86_64 zlib-1.2.11-16.el8_2.x86_64
yum debuginfo-install glibc-2.28-127.el8.x86_64
让我们再次运行一下 gdb
[root@a3d3f47671d9 cui]# vim php7-4-test-zval.php[root@a3d3f47671d9 cui]# gdb phpGNU gdb (GDB) Red Hat Enterprise Linux 8.2-12.el8Copyright (C) 2018 Free Software Foundation, Inc.License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>This is free software: you are free to change and redistribute it.There is NO WARRANTY, to the extent permitted by law.Type "show copying" and "show warranty" for details.This GDB was configured as "x86_64-redhat-linux-gnu".Type "show configuration" for configuration details.For bug reporting instructions, please see:<http://www.gnu.org/software/gdb/bugs/>.Find the GDB manual and other documentation resources online at: <http://www.gnu.org/software/gdb/documentation/>.For help, type "help".Type "apropos word" to search for commands related to "word"...Reading symbols from php...done.(gdb)登录后复制
在gdb模式 命令b 可以设置断点 你可以理解为PHP的 xdebug
还记得我们的 php7-4-test-zval.php 文件内容吗
<?php $a="abcdefg"; echo $a; $b=88; echo $b; $c = $a; echo $c; echo $a; $c ="abc"; echo $c; echo $a;登录后复制
这个echo 语言结构 是为了我们调试使用 这里是个小技巧
(ps 我这里说的语言结构 可没说echo是函数 有一道面试题 php 中 echo()和var_dump()的主要区别?)
这个echo 其实是为了我们设置 断点ZEND_ECHO_SPEC_CV_HANDLER
ZEND_ECHO_SPEC_CV_HANDLER其实是个宏 以后在词法解析 语法分析 execute时候会详细展开讲解 如图
我们设置这个断点的意义是为了让程序在拼接echo 的时候暂停代码 以便我们分析
(gdb) b ZEND_ECHO_SPEC_CV_HANDLERBreakpoint 1 at 0x6dfe80: file /cui/php-7.4.15/Zend/zend_vm_execute.h, line 36987.登录后复制
在gdb中 使用 r 运行文件
typedef struct _zend_refcounted_h {uint32_t refcount;//引用数union {uint32_t type_info; //字符串所属的变量类别} u;} zend_refcounted_h;0登录后复制
在gdb中 用 n 可以执行下一步操作
typedef struct _zend_refcounted_h {uint32_t refcount;//引用数union {uint32_t type_info; //字符串所属的变量类别} u;} zend_refcounted_h;1登录后复制
这里我们暂且忽略继续往下走
ZEND_ECHO_SPEC_CV_HANDLER的完整代码如下(我贴出来只是想告诉你代码里有这行代码 让你知道为什么往下走,你现阶段不需要理解代码,慢慢来 )
typedef struct _zend_refcounted_h {uint32_t refcount;//引用数union {uint32_t type_info; //字符串所属的变量类别} u;} zend_refcounted_h;2登录后复制
这里到了关键位置 变量z出现了
gdb中 用p 查看变量
typedef struct _zend_refcounted_h {uint32_t refcount;//引用数union {uint32_t type_info; //字符串所属的变量类别} u;} zend_refcounted_h;3登录后复制
这是一个 zval 结构体的指针地址
typedef struct _zend_refcounted_h {uint32_t refcount;//引用数union {uint32_t type_info; //字符串所属的变量类别} u;} zend_refcounted_h;4登录后复制
看到这里应该很熟悉了 这就是源码里的 结构体 格式
再次复习下 zval
typedef struct _zend_refcounted_h {uint32_t refcount;//引用数union {uint32_t type_info; //字符串所属的变量类别} u;} zend_refcounted_h;5登录后复制
gdb中变量$2 中 u1.v.type=6 我们拿出第二节的 类型定义源码部分对比下
typedef struct _zend_refcounted_h {uint32_t refcount;//引用数union {uint32_t type_info; //字符串所属的变量类别} u;} zend_refcounted_h;6登录后复制
再看下 zval种 value 对应的 zend_value联合体中的代码
typedef struct _zend_refcounted_h {uint32_t refcount;//引用数union {uint32_t type_info; //字符串所属的变量类别} u;} zend_refcounted_h;7登录后复制
还记得联合体的特性吗 ? 所有值公用一个内存空间
上面的gdb中变量$2 的v.type=6 所以 在value中 值被str占用了 同时str 前面有个*
*星号 在C语言里代表指针 指向另外一个值的地址 所以指向 zend_string结构体
关于C语言指针您可以参考 菜鸟学院-指针
所以 接下来我们可以通过获取value中的str来获取 查看值
typedef struct _zend_refcounted_h {uint32_t refcount;//引用数union {uint32_t type_info; //字符串所属的变量类别} u;} zend_refcounted_h;8登录后复制
对比下 zend_string 源码
typedef struct _zend_refcounted_h {uint32_t refcount;//引用数union {uint32_t type_info; //字符串所属的变量类别} u;} zend_refcounted_h;9登录后复制
* 你可能有疑问 val为啥 是val=“a” 我们不是定义$a="abcdefg"; 吗 ? 还记得柔性数组吗?:)
接下来继续往下走
gdb中 用c 来执行到下一个断点处
(gdb) cContinuing.Breakpoint 1, ZEND_ECHO_SPEC_CV_HANDLER () at /cui/php-7.4.15/Zend/zend_vm_execute.h:3698736987SAVE_OPLINE();typedef struct _zend_refcounted_h {uint32_t refcount;//引用数union {uint32_t type_info; //字符串所属的变量类别} u;} zend_refcounted_h;1(gdb) n441return pz->u1.v.type;(gdb) n36997zend_string *str = zval_get_string_func(z);(gdb) p *z$6 = { value = {lval = 88, dval = 4.3477776834029696e-322, counted = 0x58, str = 0x58, arr = 0x58, obj = 0x58, res = 0x58, ref = 0x58, ast = 0x58, zv = 0x58, ptr = 0x58, ce = 0x58, func = 0x58, ww = {w1 = 88, w2 = 0}}, u1 = {v = {type = 4 '\004', type_flags = 0 '\000', u = {extra = 0}}, type_info = 4}, u2 = {next = 0, cache_slot = 0, opline_num = 0, lineno = 0, num_args = 0, fe_pos = 0, fe_iter_idx = 0, access_flags = 0, property_guard = 0, constant_flags = 0, extra = 0}}登录后复制
u1.v.type=4 对应的是IS_LONG 代表整型 所以 在value中 值被lval占用了
可以看到值就是88 (lval不是指针 无需再跟进去查看了)
至此 我们用gdb 结合之前所看的核心源码 亲自实战了 PHP的zval
下一节我们继续 进行写时复制 的gdb跟踪
看完此文 希望你务必也用gdb调试下 深度体会zval的巧妙之处
感谢陈雷前辈的《PHP7源码底层设计与实现》
▏本文经原作者PHP崔雪峰同意,发布在9543建站博客,原文地址:https://zhuanlan.zhihu.com/p/353173325
以上就是分析PHP底层内核源码之变量 (三)的详细内容,更多请关注9543建站博客其它相关文章!
发表评论