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 数组进行一次查找操作,然后执行引用赋值,因此可能稍微慢一些。