PHP中$GLOBALS和global的区别,简单了解符号表、zval

前言

单位里有一套老代码,写了这么一个换库逻辑。

function conn() {
    global $conn;
    if ($conn) {
        unset($conn);
    }
    $conn = mysqli_connect...;
    return $conn;
}

这套代码之前的换库操作,都是使用的返回值的$conn,一直相安无事。直到有新同事接手,用了另一个封装使用global $conn的查询方法,就出了问题。

按照这个设计,理论上应当是在任意位置都可以使用唯一global $conn。那么为什么会出错呢,这就涉及到PHP中,global关键字,和$GLOBALS全局变量的区别陷阱了。

现象

$b = 2;
function aa()
{
    global $a;
    $a = 1;
    unset($a);
    unset($GLOBALS['b']);
}
aa();
echo $a, PHP_EOL;
echo $GLOBALS['a'], PHP_EOL;
var_dump($b);

输出:

1
1
Null

解释

在函数中,使用global关键字时,实际上是生成了一个局部变量,指向对应的全局变量,如果全局变量不存在,会先生成对应的全局变量,再指向它。

如果对这个局部变量使用unset,实际上只是将这个局部变量销毁,并不会影响到全局变量。

也即是说,global $a$a = &$GLOBALS['a'] 在使用和表现形式上是等价的。因底层实现有区别,推荐使用gloabal关键字,消耗较少。

但使用$GLOBALS超全局变量时,$GLOBALS['b'] 和 全局的 $b 是完全一致的,使用unset会被影响。

底层原理

简而言之,

  • $GLOBALS['a'],是全局$a本身。K存储在全局符号表中,K(a)->zval(a)。
  • 函数中global $a,是指向全局$a的局部符号。K存储在局部符号表中,K->zval(a)。
  • 函数中$_a = &$GLOBALS['a'],是指向全局$a的局部变量。K存储在局部变量表中,K->zval(_a)->zval(a)。

zval

每一个变量,最终指向的都是zval。

简单介绍

zval 是 PHP 的核心数据结构之一,用于表示 PHP 变量的内部实现。PHP 是一种弱类型语言,变量的类型可以动态变化,而 zval 则是实现这种动态类型的关键结构。它不仅存储变量的值,还包含了类型信息、引用计数等元数据。

struct _zval_struct {
	zend_value        value;			/* value */
	union {
		uint32_t type_info;
		struct {
			ZEND_ENDIAN_LOHI_3(
				uint8_t    type,			/* active type */
				uint8_t    type_flags,
				union {
					uint16_t  extra;        /* not further specified */
				} u)
		} v;
	} u1;
	union {
		uint32_t     next;                 /* hash collision chain */
		uint32_t     cache_slot;           /* cache slot (for RECV_INIT) */
		uint32_t     opline_num;           /* opline number (for FAST_CALL) */
		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     guard;                /* recursion and single property guard */
		uint32_t     constant_flags;       /* constant flags */
		uint32_t     extra;                /* not further specified */
	} u2;
};

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;

struct _zend_string {
	zend_refcounted_h gc;
	zend_ulong        h;                /* hash value */
	size_t            len;
	char              val[1];
};
...

zval 的基本结构

变量值 (value):存储变量的实际值,可以是整型、浮点型、字符串、数组、对象等。
变量类型 (type):表示当前变量的类型,如 IS_LONG (整型)、IS_STRING (字符串)、IS_ARRAY (数组) 等。
引用计数 (refcount):用来跟踪有多少个变量引用了该 zval,用于实现引用计数的垃圾回收机制。
引用标志 (is_ref):表示该 zval 是否是一个引用。如果变量被引用了(例如使用 &),这个标志会被设置。

zval 中的重要概念

写时复制(Copy-On-Write, COW)、引用计数和垃圾回收、动态类型转换。

符号表

在PHP中,变量的管理是通过符号表(Symbol Table)来实现的。符号表是一个哈希表,存储变量名和变量的值(实际上是一个 zval 结构)。每个作用域(如全局作用域、函数作用域)都有自己的符号表。

可以把符号表简单的理解为一个KV结构。变量名为K,zval为V。
在 PHP 脚本开始执行时,全局符号表就会被创建。也就是$GLOBALS。
局部符号表是在进入函数或方法时动态创建的。当函数调用结束时,局部符号表会被销毁。

哈希表

HashTable结构体,也叫zend_array,同样是_zend_value中的一种数据类型。

struct _zend_array {
	zend_refcounted_h gc;
	union {
		struct {
			ZEND_ENDIAN_LOHI_4(
				uint8_t    flags,
				uint8_t    _unused,
				uint8_t    nIteratorsCount,
				uint8_t    _unused2)
		} v;
		uint32_t flags;
	} u;
	uint32_t          nTableMask;
	union {
		uint32_t     *arHash;   /* hash table (allocated above this pointer) */
		Bucket       *arData;   /* array of hash buckets */
		zval         *arPacked; /* packed array of zvals */
	};
	uint32_t          nNumUsed;
	uint32_t          nNumOfElements;
	uint32_t          nTableSize;
	uint32_t          nInternalPointer;
	zend_long         nNextFreeElement;
	dtor_func_t       pDestructor;
};

题外话,符号表的生命周期,Swoole相关

PHP中,有一个叫EG的宏定义,用于存储PHP执行引擎的全局状态信息,符号表就存储在EG的结构体_zend_executor_globals中。

struct _zend_executor_globals {
	...
	zend_array symbol_table; /* main symbol table */
}

_zend_executor_globals 是 PHP 执行引擎的全局结构体,包含了所有与脚本执行相关的状态信息。该结构体被设计为线程局部存储(TLS),在多线程环境中,每个线程都有自己独立的 executor_globals 实例。

由于EG是线程共享的,所以Swoole协程中,对($GLOBALS、$_SESSION、$_SERVER)等的操作会互相污染,这也是为什么Swoole对全局变量,$_SESSION等操作要单独隔离处理的原因。

global $a 和 $a = &$GLOBALS['a'] 的区别

符号表绑定 vs. 引用赋值:

global $a; 是通过符号表来实现的,局部作用域中的 $a 直接指向全局符号表中的同名变量。这意味着局部变量 $a 是全局变量 $a 的一个别名。

$a = &$GLOBALS['a']; 是通过显式的引用赋值来实现的,局部变量 $a 被赋值为全局符号表中 $a 的引用。虽然效果是一样的,但这里涉及的是通过 $GLOBALS 数组的访问。

底层操作差异:

global $a; 只是修改了局部符号表的指针,使得局部符号表中的 $a 指向全局符号表中的变量。

$a = &$GLOBALS['a']; 则是通过引用赋值操作,直接修改局部变量的 zval 引用,使它与 $GLOBALS['a'] 指向同一个 zval。

执行效率:

global $a; 的执行稍微快一些,因为它仅仅修改符号表中的指针。

$a = &$GLOBALS['a']; 需要通过 $GLOBALS 数组进行一次查找操作,然后执行引用赋值,因此可能稍微慢一些。

posted @ 2024-09-19 16:00  cy_b  阅读(16)  评论(0编辑  收藏  举报