PHP内核探索之变量Zval
我们已经知道php变量在内核中其实是通过zval结构来实现的,也初步了解如何设置一个zval结构的类型和值。
在code的时候,很希望在内核中创建的zval可以让用户在PHP语言里以变量的形式使用,为了实现这个功能, 我们首先要创建一个zval。最容易想到的办法便是创建一个zval指针, 然后申请一块内存并让指针指向它。如果你脑海里浮现出了malloc(sizeof(zval))的影子, 那么请你立即刹车,不要用malloc来做这件事情,内核给我们提供了相应的宏来处理这件事, 理由和以前一样:为了代码漂亮并保持版本升级时的兼容性。 这个宏的是:MAKE_STD_ZVAL(pzv)。这个宏会用内核的方式来申请一块内存并将其地址付给pzv, 并初始化它的refcount和is_ref两个属性,更棒的是,它不但会自动的处理内存不足问题, 还会在内存中选个最优的位置来申请。
作为数据的容器,我们常常需要跟变量打交道,不管这个变量是数字、数组、字符串、对象还是其他,因而可以说变量是构成语言的不可或缺的基础。本文是PHP内核探索之变量的第一篇,主要介绍zval的基本知识,包括如下几个方面的内容:
- Zval的基本结构
- 查看zval的方法:debug_zval_dump和xdebug
- Zval的原理,COW等
由于写作仓促,难免会有错误,欢迎指出。
一、Zval的基本结构
Zval是PHP中最重要的数据结构之一(另一个比较重要的数据结构是hash table),它包含了PHP中的变量值和类型的相关信息。它是一个struct,基本结构为:
<span>struct</span><span> _zval_struct { zvalue_value value; </span><span>/*</span><span> value </span><span>*/</span><span> zend_uint refcount__gc; </span><span>/*</span><span> variable ref count </span><span>*/</span><span> zend_uchar type; </span><span>/*</span><span> active type </span><span>*/</span><span> zend_uchar is_ref__gc; </span><span>/*</span><span> if it is a ref variable </span><span>*/</span><span> }; typedef </span><span>struct</span> _zval_struct zval;
其中:
1. zval_value value
变量的实际值,具体来说是一个zvalue_value的联合体(union):
<span>typedef union _zvalue_value { </span><span>long</span> lval; <span>/*</span><span> long value </span><span>*/</span> <span>double</span> dval; <span>/*</span><span> double value </span><span>*/</span> <span>struct</span> { <span>/*</span><span> string </span><span>*/</span> <span>char</span> *<span>val; </span><span>int</span><span> len; } str; HashTable </span>*ht; <span>/*</span><span> hash table value,used for array </span><span>*/</span><span> zend_object_value obj; </span><span>/*</span><span> object </span><span>*/</span><span> } zvalue_value;</span>
2. zend_uint refcount__gc
该值实际上是一个计数器,用来保存有多少变量(或者符号,symbols,所有的符号都存在符号表(symble table)中, 不同的作用域使用不同的符号表,关于这一点,我们之后会论述)指向该zval。在变量生成时,其refcount=1,典型的赋值操作如$a = $b会令zval的refcount加1,而unset操作会相应的减1。在PHP5.3之前,使用引用计数的机制来实现GC,如果一个zval的refcount较少到0,那么Zend引擎会认为没有任何变量指向该zval,因此会释放该zval所占的内存空间。但,事情有时并不会那么简单。后面我们会看到,单纯的引用计数机制无法GC掉循环引用的zval,即使指向该zval的变量已经被unset,从而导致了内存泄露(Memory Leak)。
3. zend_uchar type
该字段用于表明变量的实际类型。在开始学习PHP的时候,我们已经知道,PHP中的变量包括四种标量类型(bool,int,float,string),两种复合类型(array, object)和两种特殊的类型(resource 和NULL)。在zend内部,这些类型对应于下面的宏(代码位置 phpsrc/Zend/zend.h):
<span>#define</span> IS_NULL 0 <span>#define</span> IS_LONG 1 <span>#define</span> IS_DOUBLE 2 <span>#define</span> IS_BOOL 3 <span>#define</span> IS_ARRAY 4 <span>#define</span> IS_OBJECT 5 <span>#define</span> IS_STRING 6 <span>#define</span> IS_RESOURCE 7 <span>#define</span> IS_CONSTANT 8 <span>#define</span> IS_CONSTANT_ARRAY 9 <span>#define</span> IS_CALLABLE 10
4. is_ref__gc
这个字段用于标记变量是否是引用变量。对于普通的变量,该值为0,而对于引用型的变量,该值为1。这个变量会影响zval的共享、分离等。关于这点,我们之后会有论述。
正如名字所示,ref_count__gc和is_ref__gc是PHP的GC机制所需的很重要的两个字段,这两个字段的值,可以通过xdebug等调试工具查看。
二、xdebug的安装配置
Xdebug是一个开源的PHP 性能分析和debug工具。虽然对于一般的程序调试,var_dump,echo,print,debug_backtrace等常见的调试工具已经基本够用,但对于一些复杂的调试和性能测试,xdebug绝对是一个很好的帮手(其他的如Xhprof等工具也很优秀)。
本文的基本环境:
安装Xdebug的基本过程为(实际上是源码编译一个扩展):
1. 下载源码包.
下载地址为:http://www.xdebug.org/docs/install
本文中下载的版本为:Xdebug-2.6.tar.gz
2. 解压
<span>tar</span> xvzf xdebug-<span>2.6</span>.<span>tar</span>.gz
3. 在xdebug的目录执行phpize
4. ./configure 配置
5. Make&& make install
这会生成xdebug.so扩展文件(zend_extension),位置在xdebug/modules
6. 在php.ini中加载xdebug扩展
zend_extension=your-xdebug-path/xdebug.so
7. 添加xdebug的配置
xdebug.profiler_enable =<span> on xdebug.default_enable </span>=<span> on xdebug.trace_output_dir</span>="/tmp/xdebug"<span> xdebug.trace_output_name </span>=<span> trace.%c.%p xdebug.profiler_output_dir</span>="/tmp/xdebug"<span> xdebug.profiler_output_name</span>="cachegrind.out.%s"
这里不再详细介绍各个配置项的含义,详细的请看:http://www.xdebug.org/docs/all
现在,PHP中,应该已经有了Xdebug的扩展信息(php –m,也可以phpinfo()):
在Xdebug中,可以通过xdebug_debug_zval打印Zval的信息:
<?php $a = array( 'test' ); $a[] = &$a; xdebug_debug_zval( 'a' );
3. Zval的更多原理
(注,本部分主要参考:http://derickrethans.nl/collecting-garbage-phps-take-on-variables.html, 作者Derick Rethans是一位优秀的PHP内核专家,在全世界做过多次报告,都有相关的pdf下载,这里(http://derickrethans.nl/talks.html )有作者每次演讲的记录,很多都值得我们深入去学习研究)
前面我们已经说过,PHP使用Zval这种结构来保存变量,这里我们将继续追踪zval的更多细节。
1. 创建变量时,会创建一个zval.
$str = "test zval"; xdebug_debug_zval('str');
输出结果:
str: (refcount=1, is_ref=0)='test zval'
当使用$str="test zval";来创建变量时,会在当前作用域的符号表中插入新的符号(str),由于该变量是一个普通的变量,因此会生成一个refcount=1且is_ref=0的zval容器。也就是说,实际上是这样的:
2. 变量赋值给另外一个变量时,会增加zval的refcount值。
$str = "test zval"; $str2 = $str; xdebug_debug_zval('str'); xdebug_debug_zval('str2');
输出结果:
str: (refcount=2, is_ref=0)=<span>'test zval' str2: (refcount</span>=2, is_ref=0)='test zval'
同时我们看到,str和是str2这两个symbol的zval结构是一样的。这里其实是PHP所做的一个优化,由于str和str2都是普通变量,因而它们指向了同一个zval,而没有为str2开辟单独的zval。这么做,可以在一定程度上节省内存。这时的str,str2与zval的对应关系是这样的:
3. 使用unset时,对减少相应zval的refcount值
$str = "test zval"; $str3 = $str2 = $str; xdebug_debug_zval('str'); unset($str2,$str3) xdebug_debug_zval('str');
结果为:
str: (refcount=3, is_ref=0)=<span>'test zval' str: (refcount</span>=1, is_ref=0)='test zval'
由于unset($str2,$str3)会将str2和str3从符号表中删除,因此,在unset之后,只有str指向该zval,如下图所示:
现在如果执行unset($str),则由于zval的refcount会减少到0,该zval会从内存中清理。这当然是最理想的情况。
但是事情并不总是那么乐观。
4. 数组变量与普通变量生成的zval非常类似,单也有很大不同
与标量这些普通变量不同,数组和对象这类复合型的变量在生成zval时,会为每个item项生成一个zval容器。例如:
$ar = array( 'id' => 38, 'name' => 'shine' ); <span>xdebug_debug_zval('ar');</span>
打印出zval的结构是:
ar: (refcount=1, is_ref=0)=<span>array ( 'id' </span>=> (refcount=1, is_ref=0)=38,<span> 'name' </span>=> (refcount=1, is_ref=0)=<span>'shine' )</span>
如下图所示:
可以看出,变量$ar生成的过程中,共生成了3个zval容器(红色部分标注)。对于每个zval而言,refcount的增减规则与普通变量的相同。例如,我们在数组中添加另外一个元素,并把$ar['name']的值赋给它:
$ar = array( 'id' => 38, 'name' => 'shine' ); $ar['test'] = $ar['name']; xdebug_debug_zval('ar');
则打印出的zval为:
ar: (refcount=1, is_ref=0)=<span>array ( 'id' </span>=> (refcount=1, is_ref=0)=38,<span> 'name' </span>=> (refcount=2, is_ref=0)='shine',<span> 'test' </span>=> (refcount=2, is_ref=0)=<span>'shine' )</span>
如同普通变量一样,这时候,name和test这两个symbol指向同一个zval:
同样的,从数组中移除元素时,会从符号表中删除相应的符号,同时减少对应zval的refcount值。同样,如果zval的refcount值减少到0,那么就会从内存中删除该zval:
$ar = array( 'id' => 38, 'name' => 'shine' ); $ar['test'] = $ar['name']; unset($ar['test'],$ar['name']); xdebug_debug_zval('ar');
输出结果为:
ar: (refcount=1, is_ref=0)=array ('id' => (refcount=1, is_ref=0)=38)
5. 引用的出现,会令zval的规则变得复杂
在加入引用之后,情况会变的稍微复杂一点。例如,在数组中添加对本身的引用:
$a = $array('one'); $a[] = &$a; xdebug_debug_zval('a');
输出的结果:
a: (refcount=2, is_ref=1)=<span>array ( </span>0 => (refcount=1, is_ref=0)='one', 1 => (refcount=2, is_ref=1)=<span>... )</span>
上述输出中,…表示指向原始数组,因而这是一个循环的引用。如下图所示:
现在,我们对$a执行unset操作,这会在symbol table中删除相应的symbol,同时,zval的refcount减1(之前为2),也就是说,现在的zval应该是这样的结构:
(refcount=1, is_ref=1)=<span>array ( </span>0 => (refcount=1, is_ref=0)='one', 1 => (refcount=1, is_ref=1)=<span>... )</span>
也就是下图所示的结构:
这时,不幸的事情发生了!
Unset之后,虽然没有变量指向该zval,但是该zval却不能被GC(指PHP5.3之前的单纯引用计数机制的GC)清理掉,因为zval的refcount均大于0。这样,这些zval实际上会一直存在内存中,直到请求结束(参考SAPI的生命周期)。在此之前,这些zval占据的内存不能被使用,便白白浪费了,换句话说,无法释放的内存导致了内存泄露。
如果这种内存泄露仅仅发生了一次或者少数几次,倒也还好,但如果是成千上万次的内存泄露,便是很大的问题了。尤其在长时间运行的脚本中(例如守护程序,一直在后台执行不会中断),由于无法回收内存,最终会导致系统“再无内存可用”。
6. zval分离(Copy on write和change on write)
前面我们已经介绍过,在变量赋值的过程中例如$b = $a,为了节省空间,并不会为$a和$b都开辟单独的zval,而是使用共享zval的形式:
那么问题来了:如果其中一个变量发生变化时,如何处理zval的共享问题?
对于这样的代码:
$a = "a simple test"; $b = $a; echo "before write:".PHP_EOL; xdebug_debug_zval('a'); xdebug_debug_zval('b'); $b = "thss"; echo "after write:".PHP_EOL; xdebug_debug_zval('a'); xdebug_debug_zval('b');
打印的结果是:
<span>before write: a: (refcount</span>=2, is_ref=0)=<span>'a simple test' b: (refcount</span>=2, is_ref=0)=<span>'a simple test' after write: a: (refcount</span>=1, is_ref=0)=<span>'a simple test' b: (refcount</span>=1, is_ref=0)='thss'
起初,符号表中a和b指向了同一个zval(这么做的原因是节省内存),而后$b发生了变化,Zend会检查b指向的zval的refcount是否为1,如果是1,那么说明只有一个符号指向该zval,则直接更改zval。否则,说明这是一个共享的zval,需要将该zval分离出去,以保证单独变化互不影响,这种机制叫做COW –Copy on write。在很多场景下,COW都是一种比较高效的策略。
那么对于引用变量呢?
$a = 'test'; $b = &$a; echo "before change:".PHP_EOL; xdebug_debug_zval('a'); xdebug_debug_zval('b'); $b = 12; echo "after change:".PHP_EOL; xdebug_debug_zval('a'); xdebug_debug_zval('b'); unset($b); echo "after unset:".PHP_EOL; xdebug_debug_zval('a'); xdebug_debug_zval('b');
输出的结果为:
<span>before change: a: (refcount</span>=2, is_ref=1)=<span>'test' b: (refcount</span>=2, is_ref=1)=<span>'test' after change: a: (refcount</span>=2, is_ref=1)=12<span> b: (refcount</span>=2, is_ref=1)=12<span> after unset: a: (refcount</span>=1, is_ref=0)=12
可以看出,在改变了$b的值之后,Zend会检查zval的is_ref检查是否是引用变量,如果是引用变量,则直接更改即可,否则,需要执行刚刚提到的zval分离。由于$a 和 $b是引用变量,因而更改共享的zval实际上也间接更改了$a的值。而在unset($b)之后,变量$b从符号表中删除了。
这里也说明一个问题,unset并不是清除zval,而只是从符号表中删除相应的symbol。这样一来,之前很多的关于引用的疑问也可以理解了(下一节我们将深入探索PHP的引用)。
以上就介绍了PHP内核探索之变量Zval,包括了变量Zval方面的内容,希望对PHP教程有兴趣的朋友有所帮助。

热AI工具

Undresser.AI Undress
人工智能驱动的应用程序,用于创建逼真的裸体照片

AI Clothes Remover
用于从照片中去除衣服的在线人工智能工具。

Undress AI Tool
免费脱衣服图片

Clothoff.io
AI脱衣机

AI Hentai Generator
免费生成ai无尽的。

热门文章

热工具

记事本++7.3.1
好用且免费的代码编辑器

SublimeText3汉化版
中文版,非常好用

禅工作室 13.0.1
功能强大的PHP集成开发环境

Dreamweaver CS6
视觉化网页开发工具

SublimeText3 Mac版
神级代码编辑软件(SublimeText3)

热门话题

“你的组织要求你更改PIN消息”将显示在登录屏幕上。当在使用基于组织的帐户设置的电脑上达到PIN过期限制时,就会发生这种情况,在该电脑上,他们可以控制个人设备。但是,如果您使用个人帐户设置了Windows,则理想情况下不应显示错误消息。虽然情况并非总是如此。大多数遇到错误的用户使用个人帐户报告。为什么我的组织要求我在Windows11上更改我的PIN?可能是您的帐户与组织相关联,您的主要方法应该是验证这一点。联系域管理员会有所帮助!此外,配置错误的本地策略设置或不正确的注册表项也可能导致错误。即

Windows11将清新优雅的设计带到了最前沿;现代界面允许您个性化和更改最精细的细节,例如窗口边框。在本指南中,我们将讨论分步说明,以帮助您在Windows操作系统中创建反映您的风格的环境。如何更改窗口边框设置?按+打开“设置”应用。WindowsI转到个性化,然后单击颜色设置。颜色更改窗口边框设置窗口11“宽度=”643“高度=”500“>找到在标题栏和窗口边框上显示强调色选项,然后切换它旁边的开关。若要在“开始”菜单和任务栏上显示主题色,请打开“在开始”菜单和任务栏上显示主题

默认情况下,Windows11上的标题栏颜色取决于您选择的深色/浅色主题。但是,您可以将其更改为所需的任何颜色。在本指南中,我们将讨论三种方法的分步说明,以更改它并个性化您的桌面体验,使其具有视觉吸引力。是否可以更改活动和非活动窗口的标题栏颜色?是的,您可以使用“设置”应用更改活动窗口的标题栏颜色,也可以使用注册表编辑器更改非活动窗口的标题栏颜色。若要了解这些步骤,请转到下一部分。如何在Windows11中更改标题栏的颜色?1.使用“设置”应用按+打开设置窗口。WindowsI前往“个性化”,然

您是否在Windows安装程序页面上看到“出现问题”以及“OOBELANGUAGE”语句?Windows的安装有时会因此类错误而停止。OOBE表示开箱即用的体验。正如错误提示所表示的那样,这是与OOBE语言选择相关的问题。没有什么可担心的,你可以通过OOBE屏幕本身的漂亮注册表编辑来解决这个问题。快速修复–1.单击OOBE应用底部的“重试”按钮。这将继续进行该过程,而不会再打嗝。2.使用电源按钮强制关闭系统。系统重新启动后,OOBE应继续。3.断开系统与互联网的连接。在脱机模式下完成OOBE的所

任务栏缩略图可能很有趣,但它们也可能分散注意力或烦人。考虑到您将鼠标悬停在该区域的频率,您可能无意中关闭了重要窗口几次。另一个缺点是它使用更多的系统资源,因此,如果您一直在寻找一种提高资源效率的方法,我们将向您展示如何禁用它。不过,如果您的硬件规格可以处理它并且您喜欢预览版,则可以启用它。如何在Windows11中启用任务栏缩略图预览?1.使用“设置”应用点击键并单击设置。Windows单击系统,然后选择关于。点击高级系统设置。导航到“高级”选项卡,然后选择“性能”下的“设置”。在“视觉效果”选

在Windows11上的显示缩放方面,我们都有不同的偏好。有些人喜欢大图标,有些人喜欢小图标。但是,我们都同意拥有正确的缩放比例很重要。字体缩放不良或图像过度缩放可能是工作时真正的生产力杀手,因此您需要知道如何对其进行自定义以充分利用系统功能。自定义缩放的优点:对于难以阅读屏幕上的文本的人来说,这是一个有用的功能。它可以帮助您一次在屏幕上查看更多内容。您可以创建仅适用于某些监视器和应用程序的自定义扩展配置文件。可以帮助提高低端硬件的性能。它使您可以更好地控制屏幕上的内容。如何在Windows11

屏幕亮度是使用现代计算设备不可或缺的一部分,尤其是当您长时间注视屏幕时。它可以帮助您减轻眼睛疲劳,提高易读性,并轻松有效地查看内容。但是,根据您的设置,有时很难管理亮度,尤其是在具有新UI更改的Windows11上。如果您在调整亮度时遇到问题,以下是在Windows11上管理亮度的所有方法。如何在Windows11上更改亮度[10种方式解释]单显示器用户可以使用以下方法在Windows11上调整亮度。这包括使用单个显示器的台式机系统以及笔记本电脑。让我们开始吧。方法1:使用操作中心操作中心是访问

Windows上的激活过程有时会突然转向显示包含此错误代码0xc004f069的错误消息。虽然激活过程已经联机,但一些运行WindowsServer的旧系统可能会遇到此问题。通过这些初步检查,如果这些检查不能帮助您激活系统,请跳转到主要解决方案以解决问题。解决方法–关闭错误消息和激活窗口。然后,重新启动计算机。再次从头开始重试Windows激活过程。修复1–从终端激活从cmd终端激活WindowsServerEdition系统。阶段–1检查Windows服务器版本您必须检查您使用的是哪种类型的W
