1.基础知识本章简要介绍了Zend引擎的一些内部机制,它与Extensions密切相关,也可以帮助我们编写更高效的PHP代码。1.1 PHP变量的存储1.1.1 zval结构Zend使用zval结构存储PHP变量的值,如下所示:复制代码如下: typedef union _ zvalue _ value { long lval;/*长值*/double dval;/*双精度值*/struct { char * val;int len} strHashTable * ht/*哈希表值*/Zend _ object _ value obj;} zvalue _ valuestruct _zval_struct { /*变量信息*/zvalue _ value;/* value */Zend _ uint ref count;zend_uchar类型;/*活动类型*/Zend _ uchar is _ ref;};typedef struct _ zval _ struct zvalSpan id='more-597'/spanZend根据类型值决定访问哪个值成员。以下值可用:IS_NULLN/Is _ long对应于value.lval IS_DOUBLE对应于value.dval IS_STRING对应于value.str IS_ARRAY对应于value.ht IS_OBJECT对应于value.obj IS_BOOL对应于value.lval. IS_RESOURCE对应于value.lval root。根据这个表,我们可以发现两个有趣的地方:第一,PHP的数组实际上是一个HashTable,这解释了为什么PHP可以支持关联数组;其次,Resource是长值,通常存储指针、内部数组的索引或者只有创建者知道的东西,可以看作是句柄1.1.1引用计数。引用计数广泛应用于垃圾收集、内存池和字符串,Zend实现了典型的引用计数。很多PHP变量可以通过引用计数机制共享同一个zval,而zval中剩下的两个成员就是_ ref和refcount来支持这种共享。显然,refcount用于计数。当参考值增加或减少时,该值也会相应地增加或减少。一旦降至零,Zend将回收zval。is_ref呢?1 . 1 . 2 ZP中的zval状态,有——引用和非引用两种变量,通过引用计数的方式存储在Zend中。对于非引用变量,要求变量之间互不相关。修改变量时,不能影响其他变量。这种冲突可以通过采用写时复制机制来解决。——当试图写入一个变量时,如果Zend发现该变量指向的zval被多个变量共享,它会为其复制一个refcount为1的zval,并减少原始zval的refcount。这个过程被称为“zval分离”。但是对于引用变量,要求与非引用变量相反,引用赋值的变量必须绑定。修改一个变量会修改所有绑定变量。因此,有必要指出zval的当前状态,以分别处理这两种情况。这是is_ref的用途,它指示指向此zval的所有变量是否都是通过引用分配的——,要么是所有引用,要么是无引用。此时修改另一个变量,Zend只有在发现其zval is_ref为0时才会执行写时复制,即不是引用。1.1.3 zval状态切换当zval上的所有赋值操作都是引用或非引用时,一个is_ref就足够了。然而,世界永远不会如此美好。PHP不能这样限制用户。当我们混合引用和非引用赋值时,我们必须特别处理它。一、看下面的PHP代码:-p $ a=1;$ b=$ a;$ c=$ b;$ d=$ c;//在一堆引用赋值中插入一个非引用——整个过程如下:这段代码的前三句将A、B、C指向一个zval,其is _ ref=1,ref count=3;第四句是非引用赋值,通常只需要增加引用数即可。但是,由于目标zval属于引用变量,因此简单地增加引用计数显然是错误的。Zend的解决方案是为d单独生成一个zval的副本,整个过程如下:。
1.1.1参数传递PHP函数参数传递与变量赋值相同,非引用传递相当于非引用赋值,引用传递相当于引用赋值,也可能导致zval状态切换。这个后面会提到。1.2 HashTable结构HashTable是Zend引擎中最重要、应用最广泛的数据结构,用来存储几乎所有内容。1.1.1数据结构HashTable数据结构定义如下:复制代码如下: typedef struct bucket { ulong h;//存储哈希uint nKeyLengthvoid * pData//指向value,是用户数据void *pDataPtr的副本;struct bucket * pListNext//pListNext和pListLast构成一个结构桶* pListLast//整个HashTable的双链表结构bucket * pNext//用pNext和pLast组成一个struct bucket *pLast对应一个hash//双链表char ArKey[1];//key } Bucket;typedef struct _ hashtable { uint nTableSize;uint问题询问;uint nNumOfElementsulong nNextFreeElementBucket * pInternalPointer/*用于元素遍历*/Bucket * plithead;Bucket * pListTail水桶* *阿不思;//哈希数组dtor _ func _ t pDestructor//HashTable指定在初始化期间,销毁Bucket时调用zend _ bool persistent//是否使用c的内存分配例程无符号char nApplyCountzend _ bool bApplyProtection#如果ZEND_DEBUG int不一致;#endif }哈希表;一般来说,Zend的HashTable是一个链表哈希,也是针对线性遍历优化的,如下所示:。
哈希表包含两个数据结构,一个链表哈希和一个双向链表。前者用于快速键值查询,后者便于线性遍历和排序。存储桶存在于两种数据结构中。关于这种数据结构的一些解释:为什么在L链表哈希中使用双链表?链表的通用哈希只需要通过按键操作,只有一个链表就足够了。然而,Zend有时需要从链表的散列中删除给定的Bucket,这可以通过使用双链表非常有效地实现。这个可折叠的面具是干什么用的?该值用于将哈希值转换为数组下标。初始化HashTable时,Zend首先为arBuckets数组分配nTableSize的内存,nTableSize不小于用户指定大小的最小2 n,即二进制的10*。Ntablemask=Ntablemask1,即二进制01*。此时,Ntablemask正好落在[0,Ntablemask1]中,Zend使用它作为索引来访问arBuckets数组。pdata ptr是做什么的?通常,当用户插入键值对时,Zend会复制值并将pData指向值的副本。复制操作需要调用Zend内部例程emalloc来分配内存,这是一个非常耗时的操作,并且会消耗一块大于值的内存(多余的内存用于存储cookie),如果值很小,就会造成很大的浪费。考虑到HashTable主要用于存储指针值,Zend引入了pDataPtr。当值和指针一样长时,Zend直接复制到pDataPtr中,并将pData指向pDataPtr。这避免了emalloc操作,也有利于提高Cache命中率。为什么arKey只有1码?为什么不用指针来管理键呢?ArKey是一个存储按键的数组,但是它的大小只有1,不足以放下按键。在HashTable的初始化函数中可以找到以下代码:1p=(bucket *)pemmalloc(sizeof(bucket)-1 key length,ht-persistent);可以看出,Zend已经分配了足够的内存来放下自己和一个水桶的钥匙。L的上半部分是Bucket,下半部分是Key,arKey“恰好”是Bucket的最后一个元素,所以arKey可以用来访问key。这种技术在内存管理例程中最为常见。分配内存时,它实际分配的内存大于指定的大小。额外的上部通常被称为cookie,它存储这个内存的信息,如块大小、上一个指针、下一个指针等。百度的Transmit程序使用了这种方法。不使用指针来管理键的目的是减少一次电子分配操作并提高缓存命中率。另一个必要的原因是,大多数情况下密钥是固定的,整个Bucket不会因为密钥变长而重新分配。同时,它还解释了为什么值没有作为——的数组分配,因为值是可变的。1.2.2 PHP数组关于HashTable还有一个问题没有回答,那就是nNextFreeElement是做什么的?与普通哈希值不同,Zend的HashTable允许用户直接指定哈希值,而忽略甚至不指定key(此时nKeyLength为0)。同时,HashTable还支持追加操作,用户只需要提供值,而不需要指定hash值。此时,Zend使用nNextFreeElement作为哈希,然后递增nNextFreeElement。哈希表的行为似乎很奇怪,因为它将无法通过键访问值,而且它根本不是哈希。理解问题的关键在于,PHP数组是由HashTable实现的——关联数组,它使用正常的k-v映射向HashTable添加元素,其关键是用户指定的字符串。非关联数组直接使用数组下标作为哈希值,没有关键字;在数组中使用关联和非关联时,或者使用array_push操作时,需要使用nNextFreeElement。再来看value,PHP数组的值直接使用了zval的一般结构,pData指向zval*,会像上一节描述的那样直接存储在pDataPtr中。由于直接使用zval,数组的元素可以是任何PHP类型。
数组的遍历操作,即foreach、each等。是通过HashTable的双链表实现的,pInternalPointer将当前位置记录为光标。1.2.3变量符号表除了数组之外,HashTable还用来存储很多其他数据,比如PHP函数、变量符号、加载的模块、类成员等。一个变量符号表相当于一个关联数组,它的键是变量名(很明显使用长的变量名不是一个好主意),值是zval*。任何时候,PHP代码都可以看到两个变量符号表——symbol_table和active_symbol_table——。前者用于存储全局变量,称为全局符号表。后者是指向当前活动的变量符号表的指针,变量符号表通常是全局符号表。但是,每次输入一个PHP函数(在这种情况下,是用户使用PHP代码创建的函数),Zend都会在函数的本地部分创建一个变量符号表,并将active_symbol_table指向本地符号表。Zend总是使用active_symbol_table来访问变量,从而实现局部变量的范围控制。但是,如果标记为全局的变量在函数中被本地访问,Zend将进行特殊处理。——在active_symbol_table中创建对symbol_table中同名变量的引用,如果symbol_table中没有同名变量,将首先创建。1.3内存和文件程序拥有的资源一般包括内存和文件。对于普通程序,这些资源是面向过程的。当这个过程完成后,操作系统或者C库会自动回收那些我们没有明确释放的资源。但是,PHP程序有其特殊性。它基于页面。当一个页面运行时,它还会申请内存或文件等资源。但是,页面运行后,操作系统或C库可能不知道它需要回收资源。例如,我们将php作为一个模块编译到apache中,并在prefork或worker模式下运行apache。在这种情况下,apache进程或线程被重用,php页面分配的内存将永远留在内存中,直到它离开内核。针对这个问题,Zend提供了一套内存分配API,与C语言中对应的函数相同,只是这些函数从Zend自己的内存池中分配内存,可以实现基于页面的自动回收。在我们的模块中,为页面分配的内存应该使用这些API,而不是C例程,否则Zend会在页面结束时尝试释放我们的内存,结果通常是崩溃。e malloc()efree()e str dup()e calloc()e realloc()此外,Zend还以VCWD_xxx的形式提供了一组宏来替代操作系统的c库和相应的文件API。这些宏可以支持PHP的虚拟工作目录,应该始终在模块代码中使用。宏的具体定义见PHP源代码“TSRM/tsrm_virtual_cwd.h”。你可能会注意到那些宏都没有提供close操作,因为close的对象是一个打开的资源,不涉及文件路径,所以可以直接使用C或者操作系统例程;类似地,读/写等操作是直接使用C或操作系统的例程。