php5的變量實(shí)現(xiàn)
php通過(guò)一個(gè)zval結(jié)構(gòu)體來(lái)實(shí)現(xiàn)變量,對(duì)于全局變量,php維護(hù)一個(gè)全局的hashtable,通過(guò)某種散列關(guān)系將變量名和對(duì)應(yīng)的zval指針保存起來(lái)瞒御,這個(gè)hashtable稱(chēng)為symbol_table。對(duì)于函數(shù)神郊,會(huì)建立一個(gè)局部的hashtable存儲(chǔ)局部變量肴裙。php中的非對(duì)象/資源變量在傳參時(shí)傳遞的是值,但實(shí)際上也是傳遞的指針涌乳,將局部hashtable中相應(yīng)的變量映射到全局hashtable中的相應(yīng)指針中蜻懦,這是為了減少內(nèi)存和時(shí)間開(kāi)銷(xiāo)采用了寫(xiě)時(shí)拷貝機(jī)制。
寫(xiě)時(shí)拷貝(copy on write):在需要復(fù)制一個(gè)資源時(shí)夕晓,并不總是直接復(fù)制值宛乃,而是首先選擇復(fù)制指針,當(dāng)兩個(gè)指向同一資源的兩個(gè)變量中的一個(gè)需要發(fā)生改變時(shí)再進(jìn)行值復(fù)制蒸辆。在很多時(shí)候征炼,資源并不會(huì)被改變,采用COW機(jī)制可以減少很多不必要的時(shí)間和空間開(kāi)銷(xiāo)吁朦。Linux中的fork函數(shù)就采用的這種機(jī)制柒室。
與之對(duì)應(yīng)的機(jī)制稱(chēng)為寫(xiě)時(shí)改變(change on write):當(dāng)值發(fā)生改變時(shí)渡贾,直接改變值如$b=&$a逗宜。
那么php如何分辨一個(gè)zval是copy on write/change on write。如果一個(gè)zval是copy on write的空骚,如b纺讲,那么這個(gè)zval的refcount=2。refcount>2表示當(dāng)前zval被多于1個(gè)的變量引用囤屹,那么某一個(gè)變量對(duì)改zval值進(jìn)行修改時(shí)熬甚,需要首先將該zval拷貝一份,并將拷貝的副本分配給需要修改的變量再進(jìn)行修改肋坚,這個(gè)過(guò)程稱(chēng)為分離乡括。如果一個(gè)zval是change on write的肃廓,如$a=&$b,那么php會(huì)將該zval的is_ref__gc置1標(biāo)志該zval成為一個(gè)引用诲泌,對(duì)該zval指進(jìn)行修改時(shí)不會(huì)進(jìn)行分離盲赊。那么一個(gè)變量即存在cpoy又存在change怎么處理呢,如果對(duì)一個(gè)copy的zval進(jìn)行change on write處理時(shí)敷扫,判斷到refcount>1哀蘑,那么會(huì)首先對(duì)該zval進(jìn)行分離再進(jìn)行change on write處理。如果對(duì)一個(gè)change的zval進(jìn)行copy on write處理葵第,那么直接拷貝當(dāng)前zval绘迁。可以看下面的代碼
//php5.6
<?php
$a = '1';
$b = $a;//copy on write
debug_zval_dump($a);//輸出string(1) "1" refcount(3)卒密,有三個(gè)變量指向當(dāng)前zval缀台,因?yàn)閷?xiě)時(shí)拷貝機(jī)制,$a哮奇,$b和傳進(jìn)debug_zval_dump()的參數(shù)都是指向了當(dāng)前zval
$c = $a;
debug_zval_dump($a);//輸出string(1) "1" refcount(4)
$d = &$a;
debug_zval_dump($a);//輸出string(1) "1" refcount(1)将硝,對(duì)一個(gè)copy變量$a進(jìn)行change on write處理,首先進(jìn)行分離屏镊。那么$a和$d已經(jīng)是一對(duì)change on write的變量了依疼,此時(shí)再傳入debug_zval_dump()函數(shù),函數(shù)傳參時(shí)也是值傳遞即copy on write而芥,對(duì)一個(gè)change的zval進(jìn)行copy on write處理律罢,會(huì)直接拷貝zval,因此傳入的參數(shù)是一個(gè)獨(dú)立的變量所有refcount=1棍丐。
php5中的zval結(jié)構(gòu)體_zval_struct實(shí)現(xiàn)
struct _zval_struct {
union {
long lval;
double dval;
struct {
char *val;//字節(jié)型指針8字節(jié)
int len;//整型4字節(jié)
} str;
HashTable *ht;
zend_object_value obj;
zend_ast *ast;
} value;//value變量通過(guò)一個(gè)聯(lián)合體儲(chǔ)存變量的值或指針
zend_uint refcount__gc;//計(jì)數(shù)误辑,指向當(dāng)前值的變量數(shù),在debug_zval_dump()函數(shù)中輸出的refcount
zend_uchar type;//變量類(lèi)型標(biāo)志位
zend_uchar is_ref__gc;//引用標(biāo)志位歌逢,用于標(biāo)志當(dāng)前值是否是引用如$b=&$a
};
在這個(gè)_zval_struct結(jié)構(gòu)體中巾钉,最主要的部分是一個(gè)value聯(lián)合體,這個(gè)聯(lián)合體儲(chǔ)存了變量的值或指針秘案,變量的類(lèi)型由zend_uchar類(lèi)型的type確定砰苍。在value聯(lián)合體中最大的部分是str結(jié)構(gòu)體占用12字節(jié)內(nèi)存,在內(nèi)存對(duì)齊的情況下阱高,value共占用16字節(jié)內(nèi)存赚导。剩下的部分包括一個(gè)4個(gè)字節(jié)的zend_uint類(lèi)型的refcount__gc和兩個(gè)單字節(jié)zend_uchar類(lèi)型的type和is_ref__gc,在內(nèi)存對(duì)齊的情況下zval結(jié)構(gòu)體共占用24字節(jié)內(nèi)存赤惊,并且沒(méi)有預(yù)留下拓展的空間吼旧。在5.3以后的版本中,為了解決數(shù)組和字符串變量的循環(huán)引用問(wèn)題未舟,使用了一個(gè)新的zval_gc_info結(jié)構(gòu)體來(lái)擴(kuò)充zval圈暗,這個(gè)結(jié)構(gòu)體占8字節(jié)掂为。這樣在php5中,一個(gè)變量會(huì)至少占用32字節(jié)內(nèi)存员串。并且由于zval結(jié)構(gòu)和cow機(jī)制造成很大的空間和時(shí)間開(kāi)銷(xiāo)菩掏,如以下代碼:
//php5.6
$a = '1';
$b = &$a;
print_r($a);
這里傳入print_r()函數(shù)的參數(shù)本可以利用cow機(jī)制避免不必要的開(kāi)銷(xiāo),但是由于$a在第二行和$b進(jìn)行了change on write綁定昵济,使$a成為了一個(gè)引用智绸,那么在函數(shù)傳遞參數(shù)時(shí),由于字符串是通過(guò)值傳遞即copy on write的访忿,那么對(duì)一個(gè)change的zval進(jìn)行copy on write時(shí)必須要進(jìn)行分離瞧栗,即使函數(shù)內(nèi)并沒(méi)有對(duì)該變量進(jìn)行修改。如果這個(gè)函數(shù)在一個(gè)循環(huán)里重復(fù)運(yùn)行幾千幾萬(wàn)次海铆,就會(huì)造成很大的無(wú)效開(kāi)銷(xiāo)迹恐。
php7中的變量實(shí)現(xiàn)
struct _zval_struct {
union {
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; //refenerce
zend_ast_ref *ast; //abstract syntax tree
zval *zv;
void *ptr;
zend_class_entry *ce;
zend_function *func;
struct {
uint32_t w1;
uint32_t w2;
} ww;
} value;
union {
struct {
ZEND_ENDIAN_LOHI_4(
zend_uchar type, //類(lèi)型標(biāo)志如字符串,整型等
zend_uchar type_flags, //變量類(lèi)型標(biāo)志位如需要引用計(jì)數(shù)卧斟,可被復(fù)制等
zend_uchar const_flags, //常量標(biāo)志位
zend_uchar reserved //保留字段
} v;
uint32_t type_info;//u1是一個(gè)聯(lián)合體殴边,所以實(shí)際上type_info的值就是上面ZEND_ENDIAN_LOHI_4結(jié)構(gòu)體的值,通過(guò)type_info可以簡(jiǎn)化賦值珍语。
} u1;
union {
uint32_t var_flags;
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 */
} u2;
};
php7的zval通過(guò)一個(gè)value聯(lián)合體保存變量锤岸,一個(gè)u1聯(lián)合體記錄變量類(lèi)型,u2聯(lián)合體為各種輔助變量板乙。value8字節(jié)是偷,u1和u2分別為4字節(jié),共16字節(jié)募逞,相比php5節(jié)省了一半的內(nèi)存蛋铆。
value中可以保存一個(gè)指針或者一個(gè)long/double。在php7中引用獨(dú)立出來(lái)成為一種類(lèi)型的變量放接。u1中的type類(lèi)型定義如下
/* regular data types */
#define IS_UNDEF 0 //標(biāo)記為未定義刺啦,如unset后的變量
#define IS_NULL 1
#define IS_FALSE 2 //對(duì)于布爾類(lèi)型,直接在type中儲(chǔ)存了指IS_FALSE/IS_TRUE
#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
/* constant expressions */
#define IS_CONSTANT 11
#define IS_CONSTANT_AST 12
/* fake types */
#define _IS_BOOL 13
#define IS_CALLABLE 14
#define IS_ITERABLE 19
#define IS_VOID 18
/* internal types */
#define IS_INDIRECT 15
#define IS_PTR 17
#define IS_ERROR 20
通過(guò)type值取出相應(yīng)的value值纠脾。如果是IS_LONG/IS_DOUBLE則直接取出value值作為zval值玛瘸,若是IS_FLASE/IS_TRUE則直接把type作為zval值,否則獲取相應(yīng)指針獲取值乳乌。php7中的long和double不再使用COW機(jī)制捧韵。
u1中的type_flags用于標(biāo)記變量的一些屬性市咆,8位的type_flags中每一位都可以表示一個(gè)標(biāo)志汉操,最多可以同時(shí)有8個(gè)標(biāo)志如下
//zval,作用于zval也就是zval.u1.v.type_flags
IS_TYPE_CONSTANT //是常量類(lèi)型
IS_TYPE_IMMUTABLE //不可變的類(lèi)型蒙兰, 比如存在共享內(nèi)存的數(shù)組
IS_TYPE_REFCOUNTED //需要引用計(jì)數(shù)的類(lèi)型
IS_TYPE_COLLECTABLE //可能包含循環(huán)引用的類(lèi)型(IS_ARRAY, IS_OBJECT)
IS_TYPE_COPYABLE //可被復(fù)制的類(lèi)型磷瘤, 如對(duì)象和資源就不是
IS_TYPE_SYMBOLTABLE //zval保存的是全局符號(hào)表
//以下作用與具體變量的gc.u.v.flags
//string
IS_STR_PERSISTENT //是malloc分配內(nèi)存的字符串
IS_STR_INTERNED //INTERNED STRING
IS_STR_PERMANENT //不可變的字符串芒篷, 用作哨兵作用
IS_STR_CONSTANT //代表常量的字符串
IS_STR_CONSTANT_UNQUALIFIED //帶有可能命名空間的常量字符串
//array
IS_ARRAY_IMMUTABLE //同IS_TYPE_IMMUTABLE
//object
IS_OBJ_APPLY_COUNT //遞歸保護(hù)
IS_OBJ_DESTRUCTOR_CALLED //析構(gòu)函數(shù)已經(jīng)調(diào)用
IS_OBJ_FREE_CALLED //清理函數(shù)已經(jīng)調(diào)用
IS_OBJ_USE_GUARDS //魔術(shù)方法遞歸保護(hù)
IS_OBJ_HAS_GUARDS //是否有魔術(shù)方法遞歸保護(hù)標(biāo)志
zend_refcounted_h,在需要進(jìn)行引用計(jì)數(shù)的數(shù)據(jù)類(lèi)型結(jié)構(gòu)體中都包含這個(gè)結(jié)構(gòu)體采缚,gc通過(guò)這個(gè)結(jié)構(gòu)體來(lái)實(shí)現(xiàn)垃圾回收针炉,gc可以不關(guān)心數(shù)據(jù)的類(lèi)型。
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;
字符串
struct _zend_string{
zend_refcounted_h gc; //一個(gè)gc結(jié)構(gòu)體和媳,儲(chǔ)存了gc相關(guān)的信息漠其,使gc進(jìn)行垃圾回收時(shí)可以不關(guān)心數(shù)據(jù)類(lèi)型
zend_ulong h; //字符串哈希值留美,避免數(shù)組訪問(wèn)時(shí)重復(fù)計(jì)算哈希值,當(dāng)字符串被當(dāng)作數(shù)組索引時(shí)才計(jì)算該值
sizt_t len;
char val[1]; //柔性數(shù)組
}
對(duì)于一個(gè)字符串結(jié)構(gòu)體镰烧,我們可以直接用一個(gè)char指針來(lái)儲(chǔ)存字符串,但是這樣在訪問(wèn)字符串時(shí)需要兩次訪問(wèn)內(nèi)存楞陷,一次是讀地址怔鳖,一次是讀數(shù)據(jù)。并且在釋放資源時(shí)也要先釋放字符串指針再釋放結(jié)構(gòu)體固蛾。在C99標(biāo)準(zhǔn)中加入了一種柔性數(shù)組的結(jié)構(gòu)结执,可以使php中的字符串操作更高效
那么如何改進(jìn)呢?很容易想到艾凯,我們將字符串值和結(jié)構(gòu)體存儲(chǔ)在一片連續(xù)的內(nèi)存空間就可以了献幔。這樣的話(huà),訪問(wèn)字符串與釋放字符串的內(nèi)存空間趾诗,均僅需1次內(nèi)存訪問(wèn)斜姥。
鑒于這種代碼結(jié)構(gòu)所產(chǎn)生的重要作用,C99甚至把它收入了標(biāo)準(zhǔn)中沧竟。C99使用不完整類(lèi)型實(shí)現(xiàn)柔性數(shù)組成員铸敏,在C99 中,結(jié)構(gòu)中的最后一個(gè)元素允許是未知大小的數(shù)組悟泵,這就叫做柔性數(shù)組(flexible array)成員(也叫伸縮性數(shù)組成員)杈笔,但結(jié)構(gòu)中的柔性數(shù)組成員前面必須至少一個(gè)其他成員。柔性數(shù)組成員允許結(jié)構(gòu)中包含一個(gè)大小可變的數(shù)組糕非。柔性數(shù)組成員只作為一個(gè)符號(hào)地址存在蒙具,而且必須是結(jié)構(gòu)體的最后一個(gè)成員,sizeof 返回的這種結(jié)構(gòu)大小不包括柔性數(shù)組的內(nèi)存朽肥。柔性數(shù)組成員不僅可以用于字符數(shù)組禁筏,還可以是元素為其它類(lèi)型的數(shù)組。包含柔性數(shù)組成員的結(jié)構(gòu)用malloc ()函數(shù)進(jìn)行內(nèi)存的動(dòng)態(tài)分配衡招,并且分配的內(nèi)存應(yīng)該大于結(jié)構(gòu)的大小篱昔,以適應(yīng)柔性數(shù)組的預(yù)期大小。
- 第一個(gè)問(wèn)題:為什么要存長(zhǎng)度len?不存長(zhǎng)度州刽,直接和C語(yǔ)言一樣通過(guò)字符串的'\0'來(lái)判斷字符串結(jié)束不行嗎空执?不行。這里有一個(gè)二進(jìn)制安全的問(wèn)題穗椅。
- 二進(jìn)制安全:寫(xiě)入的數(shù)據(jù)和讀出來(lái)的數(shù)據(jù)完全相同辨绊,就是二進(jìn)制安全的。
- 假設(shè)你寫(xiě)入了一個(gè)字符串的內(nèi)容為:hello\0world匹表,按照C語(yǔ)言的讀取字符串的方法就會(huì)判定\0是字符串結(jié)束的標(biāo)志门坷,讀出來(lái)就是hello,這樣讀出來(lái)的數(shù)據(jù)就和寫(xiě)入的數(shù)據(jù)不一致袍镀,就是非二進(jìn)制安全的拜鹤。
- 如果存了長(zhǎng)度,就不會(huì)管你是否有\(zhòng)0流椒,從頭開(kāi)始讀字符串敏簿,一直讀len長(zhǎng)度為止即可。
- 第二個(gè)問(wèn)題:最后一個(gè)字段改成char val[0]可以嗎宣虾?可以惯裕。寫(xiě)成char val[1]是出于可移植性的考慮。有些編譯器不支持[0]數(shù)組绣硝,可將其改成[]或[1]均可蜻势。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(char * args[]){
typedef struct s {
int a;
char b[0];
}s;
s * a = (s*)(malloc(sizeof(s)+4*sizeof(char)));
a->a = 4;
strcpy(a->b, "he");
}
(gdb) b main
Breakpoint 1 at 0x1149: file string.c, line 5.
(gdb) run
Starting program: /home/whye/Desktop/php/phpsrc/str
Breakpoint 1, main (args=0x555555555060 <_start>) at string.c:5
5 int main(char * args[]){
(gdb)
(gdb) n
10 s * a = (s*)(malloc(sizeof(s)+4*sizeof(char)));
(gdb)
11 a->a = 4;
(gdb)
12 strcpy(a->b, "he");
(gdb)
14 }
(gdb) p a
$1 = (s *) 0x5555555592a0
(gdb) p *a
$2 = {a = 4, b = 0x5555555592a4 "he"}
(gdb) p sizeof(s)
$3 = 4
(gdb) p sizeof(a)
$4 = 8
(gdb) p a+1
$5 = (s *) 0x5555555592a4
(gdb) p (char*)(a+1)
$6 = 0x5555555592a4 "he"
引用實(shí)現(xiàn)
struct _zend_reference {
zend_refcounted_h gc;
zval val;
}
在php7中,引用也成為了一種數(shù)據(jù)類(lèi)型鹉胖,對(duì)于以下代碼
<?php
$a = 'hello';
$b = $a;
$c = &$a;
上面的_zend_reference和_zend_string實(shí)例中g(shù)c計(jì)數(shù)均為2握玛。
參考
深入理解PHP原理之變量(Variables inside PHP)
深入理解PHP原理之變量分離/引用(Variables Separation)
PHP7源碼學(xué)習(xí) 2019-03-13 PHP字符串筆記