PHP底層核心原始碼之變數

wwwhj518vip16228719999發表於2021-07-19

struct _zval_struct {
zend_value value;
u1;
u2;
};
其中 zend_value 結構體的核心程式碼如下

typedef union _zend_value {
zend_long lval; //整型
double dval; //浮點型
zend_refcounted counted; //獲取不同型別結構的gc頭部的指標
zend_string *str; //string字串 的指標
zend_array *arr; //陣列指標
zend_object *obj; //object 物件指標
zend_resource *res; ///資源型別指標
zend_reference *ref; //引用型別指標 比如你通過&$c 定義的
zend_ast_ref *ast; // ast 指標 執行緒安全 相關的 核心使用的
zval *zv; // 指向另外一個zval的指標 核心使用的
void *ptr; //指標 ,通用型別 核心使用的
zend_class_entry *ce; //類 ,核心使用的
zend_function *func; // 函式 ,核心使用的
struct {
uint32_t w1;//自己定義的。無符號的32位整數
uint32_t w2;//同上
} ww;
} zend_value;
可以看出常用的 zend_value包含 上面幾種 會不會有個疑問 怎麼沒有布林型呢?
其實這裡這裡的 zend_value 只是負責儲存 內容 同樣你也會發現 也沒有null型別
再次回去開啟 zend_types.h
[root@2890cf458ee2 Zend]# vim zend_types.h
/
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

/* constant expressions */

#define IS_CONSTANT_AST 11

/* internal types */

#define IS_INDIRECT 13

#define IS_PTR 14

#define IS_ALIAS_PTR 15

#define _IS_ERROR 15

/* fake types used only for type hinting (Z_TYPE(zv) can not use them) */

#define _IS_BOOL 16

#define IS_CALLABLE 17

#define IS_ITERABLE 18

#define IS_VOID 19

#define _IS_NUMBER 20
可以看到 在程式碼裡 定義了 20種型別 其中前11種 是常用型別 後面的型別包含ast和 internal 等 不常用 後面到記憶體管理 會依次展開 ast和 internal的使用
言歸正傳 在PHP中 管理字串會使用zend_string。每次 PHP 需要使用字串時,都會使用zend_string結構, PHP沒有用原生c語言的 char 而是封裝了個結構體
[root@2890cf458ee2 Zend]# vim zend_types.h
82 typedef struct _zend_object_handlers zend_object_handlers;
83 typedef struct _zend_class_entry zend_class_entry;
84 typedef union _zend_function zend_function;
85 typedef struct _zend_execute_data zend_execute_data;
86
87 typedef struct _zval_struct zval;
88
89 typedef struct _zend_refcounted zend_refcounted;
90 typedef struct _zend_string zend_string;
91 typedef struct _zend_array zend_array;
92 typedef struct _zend_object zend_object;
93 typedef struct _zend_resource zend_resource;
94 typedef struct _zend_reference zend_reference;
95 typedef struct _zend_ast_ref zend_ast_ref;
96 typedef struct _zend_ast zend_ast;
在第90行看到 zend_string實際上是_zend_string的別名
別名是c語言特有的一種 形式
繼續跟到第235行 看到了 _zend_string是一個結構體
struct _zend_string {
zend_refcounted_h gc;
zend_ulong h; /* hash value /
size_t len;
char val[1];
};
這個結構體包含 4個部分
其中 有gc (這顯然又是一個自定義型別 ) h(也是一個自定義型別) len (整型) val1
我們繼續跟gc 這個型別
typedef struct _zend_refcounted_h {
uint32_t refcount; /
reference counter 32-bit */
union {
uint32_t type_info;
} u;
} zend_refcounted_h;
可以看到 zend_refcounted_h 是 _zend_refcounted_h結構體的別名
這個結構體 包括 一個 32位純數字的 refcount 和一個聯合體u 聯合體u裡面包括一個 type_info zend_refcounted_h 佔用8位元組 ,refount英文翻譯成中文是引用的意思 顯然 這個 zend_refcounted_h是為了引用計數和字串類別儲存用的。
引用計數存放在refcount欄位、字串所屬的變數類別則儲存在type欄位。zend_string結構體中因為加入了gc欄位,使得其和陣列、物件一樣可被多個zval引用 這非常巧妙了。
[root@2890cf458ee2 Zend]# vim zend_types.h
[root@2890cf458ee2 Zend]# php -v
PHP 7.4.15 (cli) (built: Feb 22 2021 08:46:50) ( NTS )
Copyright (c) The PHP Group
Zend Engine v3.4.0, Copyright (c) Zend Technologies


我的版本為 7.4.15 你如果看過其他大佬做的原始碼文章會發現跟我這個版本的_zend_refcounted_h
結構體有所不同 ,比如 陳雷大佬的書中 的_zend_refcounted_h結構體會包含一個聯合體
聯合體裡面又有用於垃圾回收顏色用的 gc_info 等


個人認為是因為 zend_zval 的u1 已經包含了 type_flags type 等欄位 所以在PHP7.4版本里zend_refcounted_h 就棄用了這些值
在 zend_string結構體 第二個值 h 指向了zend_ulong
通過追蹤程式碼 發現 zendulong 在 zend_long.h 中
圖片

h是typedef uint64_t zend_ulong型別的一個變數,儲存字串對應的雜湊值,其後續會用在陣列裡面。他佔用8個位元組

我們把 zend_string 加上註釋

struct _zend_string {
zend_refcounted_h gc; //佔用8個位元組 用於gc的計數和字串型別的記錄
zend_ulong h; // 佔用8個位元組 用於記錄 字串的雜湊值
size_t len; //佔用8個位元組 字串的長度
char val[1]; //佔用1個位元組 字串的值儲存位置
};
len和val[1]用於標識字串,c語言中字串的表示形式可以以\0結尾,通過遍歷得到字串長度,但是其非二進位制安全,如字串中本身就包含\0,那麼該字串\0後面的字串會被截斷,這裡len用於儲存字串的長度, val是一個柔性陣列。實現的字串是二進位制安全的。

關於\0 可以看以下 c語言程式碼

main(){
char a[] = “aa\0”;
char b[] = “aa\0aaaaaaaaaaaaaaaaaa”;

printf(strlen(a));
printf(strlen(b));

}
執行結果為 2 2

也就是說C語言認為a和b這兩個字串是相等的,而且ab的長度為都為2

但是在PHP中因為有了zend_string的存在 可以做到二進位制安全

例如,字串 “foo” 在zend_string中儲存為 “foo\0”,且它的長度為3。另外,字串 “foo\0bar” 將儲存為 “foo\0bar\0”,且其長度為7。

至於什麼是柔性陣列 參考goole搜的介紹

1、什麼是柔性陣列?
柔性陣列既陣列大小待定的陣列, C語言中結構體的最後一個元素可以是大小未知的陣列,也就是所謂的0長度,
所以我們可以用結構體來建立柔性陣列。
2、柔性陣列有什麼用途 ?
它的主要用途是為了滿足需要變長度的結構體,為了解決使用陣列時記憶體的冗餘和陣列的越界問題。
3、用法 :在一個結構體的最後 ,申明一個長度為空的陣列,就可以使得這個結構體是可變長的。
對於編譯器來說,此時長度為0的陣列並不佔用空間,因為陣列名
本身不佔空間,它只是一個偏移量, 陣列名這個符號本身代 表了一個不可修改的地址常量
(注意:陣列名永遠都不會是指標!),但對於這個陣列的大小,我們
可以進行動態分配,對於編譯器而言,陣列名僅僅是一個符號,
它不會佔用任何空間,它在結構體中,只是代表了一個偏移量,代表一個不可修改的地址常量!
對於柔性陣列的這個特點,很容易構造出變成結構體,如緩衝區,資料包等等
用柔性陣列的好處很明顯,讀寫字串值時可以省一次記憶體讀寫

那為什麼不用val[0] 或者var[] 而是var[1] 呢 因為 為了相容c99的標準 c99裡不允許變長陣列的定義,但是支援var[1] 你可以理解為 為了相容不同版本的c編譯器即可。

len欄位是記錄 字串的長度 跟上面的柔性陣列一配合就知道 字串的真實長度了 讀取的資料長度以自身結構體len值為準。同時這也是典型的空間換時間演算法 也節省了還要去計算字串的長度的消耗。

所以 zend_string 結構體整體佔用 25個位元組 但是因為記憶體對齊 所以佔用32個位元組

以上你已經掌握了 字串 結構體的 基礎知識

在PHP中 封裝了很多 操作字串的基礎巨集 一般在 zend_string.h 中

下面這行程式碼 php是怎麼實現的?

其實整個過程是

圖片

(先不要考慮 詞法分析 語法分析 AST 等過程)
<?php $str = 'PHP'; printf("字串內容為".$str); printf("字串長度為".strlen($str)); ?>
其實對應的 ‘虛擬碼’如下
zend_string *s;
zend_string_init(s,”PHP”, strlen(“PHP”), 0)
// 其中 zend_string_init 為初始化一個普通字串 s
// 儲存字串到s 到變數 zval a 中
ZVAL_STR(&a, s);

php_printf(“子字串內容為”, Z_STRVAL(a));
php_printf(“字串長度為”, Z_STRLEN(a));
zend_string_release(a);
zend_string_init()函式(實際上是巨集)計算完整的char *字串和它的長度。最後一個引數的型別為 int 值為 0 或 1。如果傳0,則通過 Zend 記憶體管理使用請求繫結的堆分配。這種分配在當前請求結束後時銷燬。如果不銷燬,記憶體就會洩漏。如果傳1,則要求了所謂的“持久”分配,將使用傳統的 C語言的malloc()呼叫。

說人話就是zend_string_init函式把一個普通字串初始化成zend_string

在zend_string.h 中 第152行 可以找到

//上述我們傳進來 zend_string_init(“PHP”, 3, 0);
static zend_always_inline zend_string *zend_string_init(const char *str, size_t len, int persistent)
{
//分配記憶體及初始化 初始化記憶體的值
zend_string *ret = zend_string_alloc(len, persistent);
//拷貝 str 到 zend_string 中的val中
memcpy(ZSTR_VAL(ret), str, len);
//把字串末尾加上\0 畢竟要依賴c語言 所以最最底層要按照人家規則走
ZSTR_VAL(ret)[len] = ‘\0’;
return ret;
}
zend_string_init 第一步 又呼叫了 zend_string_alloc 然後進行 memcpy 執行ZSTR_VAL

最後返回一個 字串變數

下面是zend_string_alloc的程式碼

static zend_always_inline zend_string *zend_string_alloc(size_t len, int persistent)
{
zend_string *ret = (zend_string *)pemalloc(ZEND_MM_ALIGNED_SIZE(_ZSTR_STRUCT_SIZE(len)), persistent);
GC_SET_REFCOUNT(ret, 1);
GC_TYPE_INFO(ret) = IS_STRING | ((persistent ? IS_STR_PERSISTENT : 0) << GC_FLAGS_SHIFT);
ZSTR_H(ret) = 0;
ZSTR_LEN(ret) = len;
return ret;
}
這個巨集程式碼主要是申請一塊連續的記憶體,記憶體的大小的計算公式為:實際申請大小= 結構體的大小(24) + 字串的長度(len)+1,實際申請大小是按照8位元組對齊的,不一定等於實際計算的結果。len = string.len + new_str_len + string_struct_len + 1

這個+1就是為了追加 \0 使用的

並且還做了初始化 zend_string 工作

//這是個巨集 設定 zend_string 中的 h值 還記得h值是幹嘛的嗎?
ZSTRH(ret) = 0;
//這是個巨集 設定 zend_string 中的len的值
ZSTR_LEN(ret) = len;
然後進行memcpy 函式
C 庫函式 中的memcpy()
void memcpy(void *str1, const void *str2, size_t n)
引數
str1 – 指向用於儲存複製內容的目標陣列,型別強制轉換為 void
指標。
str2 – 指向要複製的資料來源,型別強制轉換為 void* 指標。
n – 要被複制的位元組數。
返回值
該函式返回一個指向目標儲存區 str1 的指標
memcpy主要用於拷貝資料 裡面包含了一個巨集 ZSTR_VAL

這個巨集是設定zend_string的val中資料

通過閱讀原始碼我們可以發現
以ZSTR_*(s)開頭的每個巨集都會作用到 zend_string。
ZSTR_VAL() 訪問字元陣列
ZSTR_LEN() 訪問長度資訊
ZSTR_HASH() 訪問雜湊值

以 Z_STR
(z) 開頭的巨集都會作用於到 zval 中的 zend_string 。
Z_STRVAL()
Z_STRLEN()
Z_STRHASH()

這樣就開闢了一個字串 值為 “PHP”

下一步又是一個巨集 zend_string_release

static zend_always_inline void zend_string_release(zend_string *s)
{
if (!ZSTR_IS_INTERNED(s)) {
if (GC_DELREF(s) == 0) {
pefree(s, GC_FLAGS(s) & IS_STR_PERSISTENT);
}
}
}
顯然是用於釋放記憶體的
關於zend_string 的巨集 可以參考以下注釋 (慢慢會依次展開講解)
圖片
接下來的小節我們將繼續 分析zend_string 的寫時賦值 和 記憶體管理 以及字串的各種操作的實現。

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章