在前面大致預覽了常用變量的結構之后,我們今天來仔細的剖析一下字符串的具體實現。
一、字符串的結構
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內核剖析》