PHP7內(nèi)核(八):深入理解字符串的實現(xiàn)
在前面大致預覽了常用變量的結(jié)構(gòu)之后,我們今天來仔細的剖析一下字符串的具體實現(xiàn)。
一、字符串的結(jié)構(gòu)
struct _zend_string {
zend_refcounted_h gc; /* 字符串類別及引用計數(shù) */
zend_ulong h; /* 字符串的哈希值 */
size_t len; /* 字符串的長度 */
char val[1]; /* 柔性數(shù)組,字符串存儲位置 */
};
zend_refcounted_h對應的結(jié)構(gòu)體:
typedef struct _zend_refcounted_h {
uint32_t refcount; /* 引用計數(shù) */
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結(jié)構(gòu)體,主要作用是引用計數(shù)以及標記變量的類別。
- h:字符串的哈希值,在字符串被用來當數(shù)組的key時才初始化,這樣如果同一個字符串被多次用來做key,就不會重復計算了。
- val:這里的char[1]并不意味著只存儲1位,char[1]被稱為柔性數(shù)組,下面來了解一下PHP在字符串內(nèi)存分配時做了什么。
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結(jié)構(gòu)體的大小,而len就是要分配字符串的長度,最后的+1是留給結(jié)束字符\0的。也就是說,分配內(nèi)存時不僅僅分配結(jié)構(gòu)體大小的內(nèi)存,還要顧及到長度不可控的val,這樣不僅柔性的分配了內(nèi)存,還使它與其他成員存儲在同一塊連續(xù)的空間中,在分配、釋放內(nèi)存時可以把struct統(tǒng)一處理。
- len:字符串的長度,避免重復計算浪費時間,典型的空間換時間做法。
二、字符串的二進制安全
學習過C語言的應該知道,字符串中除了最后一個字符外不允許含有\0,否則會被認為是字符串的結(jié)束字符,這就導致了C語言的字符串有很多的限制,比如不存儲圖片、文件等二進制數(shù)據(jù)。但是PHP就沒有這樣的限制,它的字符串可以存儲二進制數(shù)據(jù),并不會出現(xiàn)任何報錯,而PHP的這種能力就叫做字符串的二進制安全。
C語言代碼如下:
main() {
char a[] = "aaa\0b"; /* 含有\(zhòng)0的字符串 */
printf("%d\n", strlen(a)); /* 長度為3,\0后的b被忽略 */
}
PHP代碼:
<?php
$a = "aaa\0b";
echo strlen($a); //輸出5
?>
但是PHP不是C語言寫的嗎?為什么PHP不會報錯?我們再來回顧一下zend_string結(jié)構(gòu)體,還記得成員變量len嗎?它是實現(xiàn)二進制安全的關(guān)鍵,我們不需要像C一樣通過\0來判定字符串是否被讀取完成,而是通過長度len來判斷,這樣就保證了字符串的二進制安全。
三、zend_string API
在了解了zend_string結(jié)構(gòu)之后,我們來了解一下用來操作zend_string的函數(shù)集合。
| 函數(shù) | 作用 |
|---|---|
| zend_interned_strings_init | 初始化內(nèi)部字符串存儲哈希表,并把PHP的關(guān)鍵字等字符串信息寫進去 |
| zend_new_interned_string | 把一個zend_string寫入CG(interned_strings)哈希表中 |
| zend_interned_strings_snapshot | 將CG(interned_strings)哈希表中的字符串標記為永久字符串,這里標記的只有PHP關(guān)鍵字、內(nèi)部函數(shù)名、內(nèi)部方法名等 |
| 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 | 讀取字符串的引用計數(shù) |
| zend_string_addref | 引用計數(shù)+1 |
| zend_string_delref | 引用計數(shù)-1 |
| zend_string_alloc | 分配內(nèi)存及初始化字符串的值 |
| zend_string_init | 初始化字符串并在最后追加\0 |
| zend_string_cop | 使用引用計數(shù)方式復制字符串 |
| zend_string_dup | 直接復制一個字符串 |
| zend_string_extend | 擴容到len,保留原來的值 |
| zend_string_truncate | 截斷到len,保留開頭到len的值 |
| zend_string_free | 釋放字符串內(nèi)存 |
| zend_string_release | GC引用遞減,直到為0時釋放內(nèi)存 |
| 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全局數(shù)組寫入str |
下面挑幾個函數(shù)來介紹一下。
3.1、zend_string_init函數(shù)
zend_string_init函數(shù)主要負責把一個普通的字符串轉(zhuǎn)化為zend_string結(jié)構(gòu)體。
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;
}
- 申請一塊連續(xù)的內(nèi)存,這個在上文中已經(jīng)提到,申請的內(nèi)存大小是zend_string結(jié)構(gòu)體大小+字符串長度+1。
- 指針偏移到val位置,開始字符串拷貝。
- 在zend_string.val結(jié)尾追加
\0。
3.2、zend_string_extend函數(shù)
該函數(shù)主要用于對字符串的擴容,注意這里擴容不會改變原來保存的值,只是把長度擴大到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;
}
- 如果不是內(nèi)部字符串并且引用計數(shù)為1時,直接調(diào)用perealloc分配內(nèi)存。
- 如果字符串的引用計數(shù)大于1或者是內(nèi)部字符串時,就不能在原來的基礎(chǔ)上擴容了,先通過zend_string_alloc申請一塊新內(nèi)存,讓后將舊內(nèi)容拷貝到新內(nèi)存中。
3.3、zend_string_equals_ci函數(shù)
主要基于二進制安全對兩個字符串進行判等,我們來看下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函數(shù)在長度比較完成后,進行逐個字符進行比較。先遍歷整個字符串數(shù)組,取出每個字符,轉(zhuǎn)換為ASC碼進行判等,如果不等則返回差值。循環(huán)完了還沒發(fā)現(xiàn)差異的話就返回兩者的長度差,如果長度相等就返回0。感覺這里做的有點多余,參數(shù)傳進來之前就已經(jīng)做了長度判等了。
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底層設(shè)計與源碼實現(xiàn)》
- 《PHP7內(nèi)核剖析》

浙公網(wǎng)安備 33010602011771號