2. 浅析 ZVAL
15

2.1 走近zval

2.1.1 什么是zval

zval是Zend value的简称,它是PHP的基本存储单元。PHP程序执行过程中产生的每一个变量,在底层都与一个zval相对应。

PHP7和PHP5的zval结构有很大不同,这里我们为了避免认知上的混淆,不讨论PHP5的情况。如果有兴趣了解PHP5之后Zend API的变化,可以自行查阅资料(比如这篇wiki)。

2.1.2 zval的基本结构

PHP是动态类型的语言,那它是如何用一个zval来表示所有类型的变量呢?我们首先看一下zend_types.h中zval的定义。

struct _zval_struct {
    zend_value        value;            /* value */
    union {
        struct {
            ZEND_ENDIAN_LOHI_4(
                zend_uchar    type,         /* active type */
                zend_uchar    type_flags,
                zend_uchar    const_flags,
                zend_uchar    reserved)      /* call info for EX(This) */
        } v;
        uint32_t type_info;
    } u1;
    union {
        uint32_t     next;                 /* hash collision chain */
        uint32_t     cache_slot;           /* literal cache slot */
        uint32_t     lineno;               /* line number (for ast nodes) */
        uint32_t     num_args;             /* arguments number for EX(This) */
        uint32_t     fe_pos;               /* foreach position */
        uint32_t     fe_iter_idx;          /* foreach iterator index */
        uint32_t     access_flags;         /* class constant access flags */
        uint32_t     property_guard;       /* single property guard */
        uint32_t     extra;                /* not further specified */
    } u2;
};

我们主要关心的是valueu1这两个成员,下面我们分别对它们进行说明。

2.2 zval的值

2.2.1 zend_value

一个zval结构体的value成员存储了这个zval的值,它的结构如下:

typedef union _zend_value {
    zend_long         lval;             /* long value */
    double            dval;             /* double value */
    zend_refcounted  *counted;
    zend_string      *str;
    zend_array       *arr;
    zend_object      *obj;
    zend_resource    *res;
    zend_reference   *ref;
    zend_ast_ref     *ast;
    zval             *zv;
    void             *ptr;
    zend_class_entry *ce;
    zend_function    *func;
    struct {
        uint32_t w1;
        uint32_t w2;
    } ww;
} zend_value;

正如我们所料,zval的值存储在一个union中。不论它的类型如何,它都位于同一块地址空间。当值为整型或浮点型时,它是直接地存储在zval中的。否则,它属于GC类型,zval只存储指向它的指针。我们在2.3.3中会详细介绍GC类型的特性。

还有一些特殊的情况,我们这里不讨论。

2.2.2 操作zval的值

我们可以使用zend_types.h中提供的宏来便捷地访问zval的值。它们的格式为Z_***Z_***_P,前者接受zval作为参数,而后者接受zval*。例如:

#define Z_LVAL(zval)                 (zval).value.lval
#define Z_LVAL_P(zval_p)             Z_LVAL(*(zval_p))

当然,在我们对值进行读写操作前,必须要确定它的类型。否则,比如我们将一个double型数据当作一个指向zend_string的指针进行操作,那我们的扩展往往会segfault,或者发生一些意料之外的事情。

2.3 zval的类型

2.3.1 获取和设置类型

如果想要正确地操作一个zval,我们必须正确地设置和获取它的类型。它存储在zvalu1成员中。

union {
    struct {
        ZEND_ENDIAN_LOHI_4(
            zend_uchar    type,         /* active type */
            zend_uchar    type_flags,
            zend_uchar    const_flags,
            zend_uchar    reserved)      /* call info for EX(This) */
    } v;
    uint32_t type_info;
} u1;

其中,type为数据类型。

/* 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

通过宏Z_TYPE()Z_TYPE_P()可以便捷的访问zval的数据类型,例如:

zval* foo = some_function();
if (Z_TYPE_P(foo) == IS_STRING) {
    // ...
}

type_flags为类型所具有的特性的标识。

/* zval.u1.v.type_flags */
#define IS_TYPE_CONSTANT             (1<<0)
#define IS_TYPE_IMMUTABLE            (1<<1)
#define IS_TYPE_REFCOUNTED           (1<<2)
#define IS_TYPE_COLLECTABLE          (1<<3)
#define IS_TYPE_COPYABLE             (1<<4)

当我们需要获取一个zval的类型时,我们只需要关心它的类型就可以了,所以Z_TYPE()宏就可以满足需要。然而,当我们需要设置类型时,我们不应该只设置type,也应该设置type_flags

我们不需要显式地设置type_flags,由于u1是一个union,我们只需要直接修改u1.type_info就可以改变u1.v中的每一个成员。ZEND_ENDIAN_LOHI_4()宏确保了大端模式和小端模式下不同的字节序不会影响vtype_info的值的对应关系。在大端模式下,这四个成员的位置应颠倒。

以下是设置type_info时所用的宏。

/* extended types */
#define IS_INTERNED_STRING_EX        IS_STRING

#define IS_STRING_EX                 (IS_STRING         | ((                   IS_TYPE_REFCOUNTED |                       IS_TYPE_COPYABLE) << Z_TYPE_FLAGS_SHIFT))
#define IS_ARRAY_EX                  (IS_ARRAY          | ((                   IS_TYPE_REFCOUNTED | IS_TYPE_COLLECTABLE | IS_TYPE_COPYABLE) << Z_TYPE_FLAGS_SHIFT))
#define IS_OBJECT_EX                 (IS_OBJECT         | ((                   IS_TYPE_REFCOUNTED | IS_TYPE_COLLECTABLE                   ) << Z_TYPE_FLAGS_SHIFT))
#define IS_RESOURCE_EX               (IS_RESOURCE       | ((                   IS_TYPE_REFCOUNTED                                         ) << Z_TYPE_FLAGS_SHIFT))
#define IS_REFERENCE_EX              (IS_REFERENCE      | ((                   IS_TYPE_REFCOUNTED                                         ) << Z_TYPE_FLAGS_SHIFT))

#define IS_CONSTANT_EX               (IS_CONSTANT       | ((IS_TYPE_CONSTANT | IS_TYPE_REFCOUNTED |                       IS_TYPE_COPYABLE) << Z_TYPE_FLAGS_SHIFT))
#define IS_CONSTANT_AST_EX           (IS_CONSTANT_AST   | ((IS_TYPE_CONSTANT | IS_TYPE_REFCOUNTED |                       IS_TYPE_COPYABLE) << Z_TYPE_FLAGS_SHIFT))

Z_TYPE_INFO()Z_TYPE_INFO_P()可以方便我们设置type_info。例如,在宏ZVAL_ARR()中:

#define ZVAL_ARR(z, a) do {                       \
         zval *__z = (z);                         \
         Z_ARR_P(__z) = (a);                      \
         Z_TYPE_INFO_P(__z) = IS_ARRAY_EX;        \
    } while (0)

这个宏先将一个zend_array*赋值给zval.value.arr,然后将IS_ARRAY_EX赋值给zval.u1.type_info。这样,我们就成功地将一个任意类型的zval修改为一个数组。

2.3.2 伪类型

有些类型,它们不是基本类型,而是多种基本类型的组合。它们的意义仅限于传参和返回值的类型判断,以及对基本类型在特定情况下的限制。这样的类型我们称为伪类型。例如,我们在传参表中可能会使用伪类型:

ZEND_BEGIN_ARG_INFO(foo_arginfo, 0)
    ZEND_ARG_TYPE_INFO(0, use_bar, _IS_BOOL, 0)
ZEND_END_ARG_INFO();

最常见的伪类型是bool。在zval中,用IS_TRUEIS_FALSE两个基本类型分别对应truefalse。这样,我们就可以只通过类型判断真假,而不需要访问其值。

callable也是一个常见的伪类型。它的实际类型可以是zend_objectClosure、实现了__invoke()魔术方法的类实例),zend_string(函数名),甚至是zend_array(类实例和方法名、类名和静态方法名)。判断一个zval是否为一个callable,需要调用函数zend_is_callable()。有些初学者曾经试着通过Z_TYPE(zval) == IS_CALLABLE来进行判断,当然这是完全错误的。

PHP7.1引入了一个新的伪类型iterable,它可以是一个数组或是任何一个实现了Traversable接口的对象。在PHP扩展中,我们可以调用函数zend_is_iterable()进行判断。

2.3.3 GC类型

GC(garbage collector)即为垃圾收集器。PHP的垃圾回收机制基于引用记数。在PHP中,每一个GC类型的数据都有一个对应的头部,存储它的引用数量和其他信息。

typedef struct _zend_refcounted_h {
    uint32_t         refcount;           /* reference counter 32-bit */
    union {
        struct {
            ZEND_ENDIAN_LOHI_3(
                zend_uchar    type,
                zend_uchar    flags,     /* used for strings & objects */
                uint16_t      gc_info)   /* keeps GC root number (or 0) and color */
        } v;
        uint32_t type_info;
    } u;
} zend_refcounted_h;

PHP垃圾收集器还实现了Mark-and-Sweep算法来处理循环引用的问题。有兴趣可以阅读这篇文章

我们需要在堆上初始化GC类型的数据,如:

zend_array* arr = (zend_array*)emalloc(sizeof(zend_array));

而不是zend_array arr;。其中,宏emalloc()ecalloc()efree()等宏的使用方式和对应的C标准库中的malloc()等函数的作用是相同的,区别在于前者在ZendMM(Zend Memory Manager)中留下了相关记录,当本次请求结束后,ZendMM就会释放掉堆上所有尚未被释放掉的内存空间。这使得有些时候内存泄漏是可以容忍的。更深入地了解ZendMM,请阅读这篇文章

注意,GC类型的数据所在的内存空间会在特定的条件下被垃圾收集器自动释放,显式调用efree()是错误的。

常见的五种GC类型是zend_stringzend_arrayzend_objectzend_resourcezend_reference,每一种我都会在之后单独开一篇文章来详细讲,尤其是字符串、数组和对象。

为了方便理解,下面我用zend_string举一个简单的例子:

$a = 'foo'; // 'foo': refcount = 1
$b = $a;    // 'foo': refcount = 2
$b = 'bar'; // 'foo': refcount = 1; 'bar': refcount = 1;

可以看到,当两个zval存放的是同一个zend_string时,字符串并没有复制,而是引用计数+1,两个zval.value.str是指向同一块内存空间的指针。而当我们在PHP中修改字符串的值的时候,并不是直接修改zend_string(那样$a$b的值都改变了),而是先判断它的引用数量。如果大于1,则创建一个新的zend_string,将欲修改的zval指向它,然后将原先的zend_string的引用数量减1。这就是copy-on-write机制。我们在开发PHP扩展的过程中应该牢记这一点。当对一个用户传递进来的参数变量指向的GC类型数据进行修改操作之前,一定要三思。

还有一点一定要注意,将一个zval指向一个GC类型数据并不会增加它的引用数量,如果它曾经指向其他GC类型数据,也不会使其引用数量减少。与此同时,将其作为参数传递给某些Zend API可能会导致其引用数量的变化。所以,在PHP扩展开发的过程中,一定要对引用计数机制充分警惕,否则segfault和内存泄漏随处都可能发生。宏GC_REFCOUNT()用于获取一个GC类型数据的引用数量。在PHP7.3之前,我们可以直接修改它,像++GC_REFCOUNT()。但是自从PHP7.3,我们需要宏GC_ADDREF()GC_DELREF()才能修改。

2.4 其他

2.4.1 创建和销毁zval

大多数情况下,在我们的PHP扩展中,尽量避免为新的zval分配内存,而是通过现有的zval的指针进行操作(如读取参数变量和写入返回变量等)。一个PHP扩展的业务逻辑往往是和PHP不相关的,因此zval不重要,重要的是它所包含的类型和值。当然有的时候你必须创建新的zval,在这种情况下,我们应该在栈上而不是堆上为它分配内存,除非你有充足的理由一定要这样做。

所谓”销毁zval“指的不是销毁它本身,而是销毁它的值所指向的对象。宏zval_ptr_dtor()zval_dtor()用于这一工作。其中,后者不能用于销毁可能会导致循环引用的GC类型数据,即具有IS_TYPE_COLLECTABLE标识位的zend_arrayzend_object。当zval存储的数据并不属于GC类型,则什么都不会发生。这里,”销毁“是将其引用数量-1,如果为0,再进行真正的销毁操作(调用对象的析构函数、释放内存等)。

2.4.2 拷贝zval

有时我们需要将一个zval的值拷贝到另一个zval。宏ZVAL_COPY()ZVAL_COPY_VALUE()可以方便地做到这一点。其中,前者不仅将源zval的值和类型拷贝至目标zval,同时,若值为GC类型,则将引用数量+1。

还有一个宏ZVAL_DUP(),当源zval的类型的type_flags中含有IS_TYPE_COPYABLE标识位时(即类型为zend_arrayzend_string),它将会对其进行硬拷贝,而不是增加引用数量。否则,ZVAL_DUP()ZVAL_COPY()的行为完全相同。

2.4.3 调试PHP扩展

使用调试工具GDB和Valgrind可以帮助我们找出并修正我们编写的PHP扩展中的错误。

例如,我们需要调试一处内存泄漏。首先在Valgrind中执行一个可以复现该问题的PHP脚本,Valgrind就可以输出那块内存空间当初被分配时的调用栈,从而找到是哪个变量没有被正常销毁。然后,我们使用GDB,在涉及那个变量的操作的代码块设下breakpoint,等到那个变量被初始化后,对其引用数量设下watchpoint。这样,我们就可以实时跟踪它的引用数量的变化,以及是哪个操作导致了它的变化。最终,解决这一问题。

当然,实际的调试过程往往比较复杂,而且错误的来源不容易定位,这就需要我们在长期的开发和调试过程中积累经验,才能敏锐地发现问题。之后我会在一篇文章内详细讨论调试PHP扩展的细节和技巧。

2.5 下期预告

这篇文章主要讲了zval的结构和PHP扩展开发中对它的常用操作。下一次,我将为大家讲如何在PHP扩展中定义函数供PHP调用,并处理参数传递和返回值。

敬请期待。

Living on the bleeding edge

本帖由 Summer 于 7个月前 加精
《L02 从零构建论坛系统》
以构建论坛项目 LaraBBS 为线索,展开对 Laravel 框架的全面学习。应用程序架构思路贴近 Laravel 框架的设计哲学。
《L05 电商实战》
从零开发一个电商项目,功能包括电商后台、商品 & SKU 管理、购物车、订单管理、支付宝支付、微信支付、订单退款流程、优惠券等
讨论数量: 5

比如整形变量,在64位系统中占用多少字节的内存。

7个月前
CismonX

@xuanjiang1985 zend_long.h中定义,在64位系统zend_longint64_t,所以占8个字节。然而,由于内存对齐机制,即使在32位系统上,zend_value都会占用8个字节(见它的ww成员)。再加上zval的两个各占4个字节的成员u1u2,一个zval永远占16个字节,与32位或64位系统无关。

7个月前

再请教个内存问题:64位系统下整形占8字节,比如-1的二进制64位,1的二进制1位。那么1真的要占8字节吗,岂不浪费内存?

7个月前
motecshine

挺好的 再分享个网站
http://blog.jpauli.tech/

6个月前

变量的值真正的存储,是在u1里边value下的lval

6个月前

  • 请注意单词拼写,以及中英文排版,参考此页
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`, 更多语法请见这里 Markdown 语法
  • 支持表情,使用方法请见 Emoji 自动补全来咯,可用的 Emoji 请见 :metal: :point_right: Emoji 列表 :star: :sparkles:
  • 上传图片, 支持拖拽和剪切板黏贴上传, 格式限制 - jpg, png, gif
  • 发布框支持本地存储功能,会在内容变更时保存,「提交」按钮点击时清空
  请勿发布不友善或者负能量的内容。与人为善,比聪明更重要!