PHP7內核(八):深入理解字符串的實現


在前面大致預覽了常用變量的結構之后,我們今天來仔細的剖析一下字符串的具體實現。

一、字符串的結構

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;

image

下面我們來了解一下具體每個成員的作用:

  • 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內核剖析》


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM