在前面大致預覽了常用變數的結構之後,我們今天來仔細的剖析一下字元串的具體實現。 一、字元串的結構 zend_refcounted_h對應的結構體: 下麵我們來瞭解一下具體每個成員的作用: gc:就是_zend_refcounted_h結構體,主要作用是引用計數以及標記變數的類別。 h:字元串的哈希值 ...
在前面大致預覽了常用變數的結構之後,我們今天來仔細的剖析一下字元串的具體實現。
一、字元串的結構
struct _zend_string {
zend_refcounted_h gc; /* 字元串類別及引用計數 */
zend_ulong h; /* 字元串的哈希值 */
size_t len; /* 字元串的長度 */
char val[1]; /* 柔性數組,字元串存儲位置 */
};
zend_refcounted_h對應的結構體:
typedef struct _zend_refcounted_h {
uint32_t refcount; /* 引用計數 */
union {
struct {
ZEND_ENDIAN_LOHI_3(
zend_uchar type,
zend_uchar flags, /* 字元串的類型 */
uint16_t gc_info /* 垃圾回收信息 */
)
} v;
uint32_t type_info;
} u;
} zend_refcounted_h;
下麵我們來瞭解一下具體每個成員的作用:
- gc:就是_zend_refcounted_h結構體,主要作用是引用計數以及標記變數的類別。
- h:字元串的哈希值,在字元串被用來當數組的key時才初始化,這樣如果同一個字元串被多次用來做key,就不會重覆計算了。
- val:這裡的char[1]並不意味著只存儲1位,char[1]被稱為柔性數組,下麵來瞭解一下PHP在字元串記憶體分配時做了什麼。
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);
......
}
巨集替換後:
static zend_always_inline zend_string *zend_string_alloc(size_t len, int persistent)
{
zend_string *ret = (zend_string *)pemalloc(ZEND_MM_ALIGNED_SIZE(XtOffsetOf(zend_string, val) + len + 1), persistent);
......
}
示例中的代碼XtOffsetOf(zend_string, val)
表示計算出zend_string結構體的大小,而len就是要分配字元串的長度,最後的+1
是留給結束字元\0
的。也就是說,分配記憶體時不僅僅分配結構體大小的記憶體,還要顧及到長度不可控的val,這樣不僅柔性的分配了記憶體,還使它與其他成員存儲在同一塊連續的空間中,在分配、釋放記憶體時可以把struct統一處理。
- len:字元串的長度,避免重覆計算浪費時間,典型的空間換時間做法。
二、字元串的二進位安全
學習過C語言的應該知道,字元串中除了最後一個字元外不允許含有\0
,否則會被認為是字元串的結束字元,這就導致了C語言的字元串有很多的限制,比如不存儲圖片、文件等二進位數據。但是PHP就沒有這樣的限制,它的字元串可以存儲二進位數據,並不會出現任何報錯,而PHP的這種能力就叫做字元串的二進位安全。
C語言代碼如下:
main() {
char a[] = "aaa\0b"; /* 含有\0的字元串 */
printf("%d\n", strlen(a)); /* 長度為3,\0後的b被忽略 */
}
PHP代碼:
<?php
$a = "aaa\0b";
echo strlen($a); //輸出5
?>
但是PHP不是C語言寫的嗎?為什麼PHP不會報錯?我們再來回顧一下zend_string結構體,還記得成員變數len嗎?它是實現二進位安全的關鍵,我們不需要像C一樣通過\0
來判定字元串是否被讀取完成,而是通過長度len來判斷,這樣就保證了字元串的二進位安全。
三、zend_string API
在瞭解了zend_string結構之後,我們來瞭解一下用來操作zend_string的函數集合。
函數 | 作用 |
---|---|
zend_interned_strings_init | 初始化內部字元串存儲哈希表,並把PHP的關鍵字等字元串信息寫進去 |
zend_new_interned_string | 把一個zend_string寫入CG(interned_strings)哈希表中 |
zend_interned_strings_snapshot | 將CG(interned_strings)哈希表中的字元串標記為永久字元串,這裡標記的只有PHP關鍵字、內部函數名、內部方法名等 |
zend_interned_strings_restore | 銷毀CG(interned_strings)哈希表中類型為非永久字元串的值,在php_request_shutdown階段釋放 |
zend_interned_strings_dtor | 銷毀整個CG(interned_strings)哈希表,在php_module_shutdown階段釋放 |
zend_string_hash_val | 得到字元串的哈希值,沒有則實時計算 |
zend_string_forget_hash_val | 將字元串的哈希值置為0 |
zend_string_refcount | 讀取字元串的引用計數 |
zend_string_addref | 引用計數+1 |
zend_string_delref | 引用計數-1 |
zend_string_alloc | 分配記憶體及初始化字元串的值 |
zend_string_init | 初始化字元串併在最後追加\0 |
zend_string_cop | 使用引用計數方式複製字元串 |
zend_string_dup | 直接複製一個字元串 |
zend_string_extend | 擴容到len,保留原來的值 |
zend_string_truncate | 截斷到len,保留開頭到len的值 |
zend_string_free | 釋放字元串記憶體 |
zend_string_release | GC引用遞減,直到為0時釋放記憶體 |
zend_string_equals | 普通判等 |
zend_string_equals_ci | 基於二進位安全,兩個zend_string類型字元串判等 |
zend_string_equals_literal_ci | 基於二進位安全,zend_string類型和char*字元串判等 |
zend_inline_hash_func | 計算字元串的哈希值 |
zend_intern_known_strings | 往zend_intern_known_strings全局數組寫入str |
下麵挑幾個函數來介紹一下。
3.1、zend_string_init函數
zend_string_init函數主要負責把一個普通的字元串轉化為zend_string結構體。
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);
memcpy(ZSTR_VAL(ret), str, len);
ZSTR_VAL(ret)[len] = '\0';
return ret;
}
- 申請一塊連續的記憶體,這個在上文中已經提到,申請的記憶體大小是zend_string結構體大小+字元串長度+1。
- 指針偏移到val位置,開始字元串拷貝。
- 在zend_string.val結尾追加
\0
。
3.2、zend_string_extend函數
該函數主要用於對字元串的擴容,註意這裡擴容不會改變原來保存的值,只是把長度擴大到len。
static zend_always_inline zend_string *zend_string_extend(zend_string *s, size_t len, int persistent)
{
zend_string *ret;
ZEND_ASSERT(len >= ZSTR_LEN(s));
if (!ZSTR_IS_INTERNED(s)) {
if (EXPECTED(GC_REFCOUNT(s) == 1)) {
ret = (zend_string *)perealloc(s, ZEND_MM_ALIGNED_SIZE(_ZSTR_STRUCT_SIZE(len)), persistent);
ZSTR_LEN(ret) = len;
zend_string_forget_hash_val(ret);
return ret;
} else {
GC_REFCOUNT(s)--;
}
}
ret = zend_string_alloc(len, persistent);
memcpy(ZSTR_VAL(ret), ZSTR_VAL(s), ZSTR_LEN(s) + 1);
return ret;
}
- 如果不是內部字元串並且引用計數為1時,直接調用perealloc分配記憶體。
- 如果字元串的引用計數大於1或者是內部字元串時,就不能在原來的基礎上擴容了,先通過zend_string_alloc申請一塊新記憶體,讓後將舊內容拷貝到新記憶體中。
3.3、zend_string_equals_ci函數
主要基於二進位安全對兩個字元串進行判等,我們來看下PHP是怎麼比較兩個字元串的。
#define zend_string_equals_ci(s1, s2) \
(ZSTR_LEN(s1) == ZSTR_LEN(s2) && !zend_binary_strcasecmp(ZSTR_VAL(s1), ZSTR_LEN(s1), ZSTR_VAL(s2), ZSTR_LEN(s2)))
- 先比較兩個字元串的長度是否相等,註意這裡是通過zend_string中的len來比較的。
- zend_binary_strcasecmp函數在長度比較完成後,進行逐個字元進行比較。先遍歷整個字元串數組,取出每個字元,轉換為ASC碼進行判等,如果不等則返回差值。迴圈完了還沒發現差異的話就返回兩者的長度差,如果長度相等就返回0。感覺這裡做的有點多餘,參數傳進來之前就已經做了長度判等了。
ZEND_API int ZEND_FASTCALL zend_binary_strcasecmp(const char *s1, size_t len1, const char *s2, size_t len2) /* {{{ */
{
size_t len;
int c1, c2;
if (s1 == s2) {
return 0;
}
len = MIN(len1, len2);
while (len--) {
c1 = zend_tolower_ascii(*(unsigned char *)s1++);
c2 = zend_tolower_ascii(*(unsigned char *)s2++);
if (c1 != c2) {
return c1 - c2;
}
}
return (int)(len1 - len2);
}
感興趣的同學可以到源碼中查看。
四、參考文獻
- 《PHP7底層設計與源碼實現》
- 《PHP7內核剖析》