【譯】PHP 內核 — 字符串管理

  • (Strings management: zend_string 譯文)

字符串管理:zend_string

任何程序都須要管理字符串。在這裏,咱們將詳細介紹一個適合 PHP 須要的定製解決方案:zend_string。每次 PHP 須要處理字符串時,都會使用 zend_string 結構體。這個結構體是 C 語言的 char * 類型包裝後的結果。php

它增長了內存管理功能,這樣的話,同一個字符串能夠在多個地方共享,而不須要複製它。另外,有些字符串是 「interned」 的,它們被持久化的分配內存並由內存管理器進行特殊管理的,這樣能夠實如今多個請求之間進行復用。那些字符串在後續會被 Zend 內存管理器持久化分配。html

結構體和訪問宏

這裏簡單地展現 zend_string 結構體:git

struct _zend_string {
        zend_refcounted_h gc;
        zend_ulong        h;
        size_t            len;
        char              val[1];
};

如你看到的,這個結構體嵌入了一個 zend_refcounted_h 頭。這樣作是爲了內存管理和引用計數。因爲字符串極可能用做哈希表探測的鍵,因此它將哈希值嵌入爲 h 成員。這是一個無符號的 zend_ulong。這個數字只在 zend_string 須要計算哈希時使用,特別是跟 HashTables: zend_array 一塊兒使用時;這是很是有可能的。github

正如你知道的那樣,字符串知道它本身的長度是 len 字段值,經過它能夠支持「二進制字符串」。二進制字符串是一個字符串中包含了一個或多個 NUL 字符(\0)。當它傳遞給 libc 函數時,這些字符串會被截斷,或者它們的長度不會以正確的方式計算。那麼,在 zend_string 中,字符串的長度是已知的。請注意,長度的計算是計算 ASCII 字符,不包含結束時的 NUL,但包含中間的若干 NUL。例如,字符串 「foo」 在 zend_string 中存儲爲 「foo\0」,其長度爲 3。此外,字符串 「foo\0bar」 將存儲爲 「foo\0bar\0」,長度是 7。api

最後,字符存儲到 char[1] 字段中。它不是 char *,而是 char[1]。爲何呢?這是一種內存優化,叫作 「C struct hack」(你能夠去搜索一下這個術語)。基本上,它容許 zend 引擎爲 zend_string 結構體類型以及要存儲的字符分配內存空間時,只需用一個 C 指針。這樣能夠優化內存訪問,由於此處分配的內存是一個連續的塊,而非在內存中分配兩個塊(一個用於 zend_string *,一個用於 char *)。數組

必定要記住這個 struct hack,由於 C 中 zend_string 結構體的內存佈局看起來和 C 中 char 是很類似的,在使用 C 調試器調試(調試字符串)時能夠感受到/看到是這樣。這種 hack 可讓你經過對應的 API 操做徹底地管理 zend_string 結構體。安全

使用 zend_string API

簡單的使用場景

就像使用 Zvals 同樣,你不須要手動操做 zend_string 內部的字段,而是使用宏來實現這一點。還存在一些宏來觸發字符串上的操做。這些不是函數,而是宏,都被定義在 Zend/zend_string.h 頭文件中:php7

zend_string *str;

str = zend_string_init("foo", strlen("foo"), 0);
php_printf("This is my string: %s\n", ZSTR_VAL(str));
php_printf("It is %zd char long\n", ZSTR_LEN(str));

zend_string_release(str);

上面的簡單示例展現了基本的字符串管理。函數 zend_string_init()(它其實是一個宏,可是咱們能夠傳遞詳細的參數)應該返回給你一個相似於 char * 的 C 字符串類型,,以及它的長度。最後一個參數類型是 int —— 值應該是 0 或者 1。若是傳遞 0,則要求引擎使用 Zend 內存管理器使用「請求綁定」的堆分配方式分配內存。這種分配將在當前請求結束時銷燬。若是你不銷燬,在調試構建時,引擎將會告訴你:你剛剛建立的內存泄露了。若是你使用 1,那麼你將會使用咱們所說的「持久化」分配,即引擎將使用傳統的 C 的 malloc() 調用,而且不會跟蹤內存的分配。函數

  • 若是你須要知道更多關於內存管理的信息,能夠閱讀詳細章節

而後,我米呢顯示字符串。咱們使用 ZSTR_VAL() 宏訪問字符數組。ZSTR_LEN() 容許訪問字符串長度信息。zend_string 相關的宏都是以 ZSTR_**() 開頭的宏,注意這跟 Z_STR**() 宏可不同!工具

  • 長度使用 size_t 類型存儲。所以,要顯示它,可使用 printf() 配合 「%zd」 格式。若是不這樣的話,可能會致使應用程序崩潰或者產生安全問題。有關 printf() 格式化打印的詳細介紹,能夠訪問此連接

最後,咱們使用 zend_string_release() 來釋放字符串。這個版本是強制性的。這是關於內存管理的部分。「釋放」是一個簡單的操做:減小字符串的引用計數,若是它是零,API 將爲你釋放字符串的內存。若是忘記釋放字符串,極可能形成內存泄露。

  • 你必須始終考慮用 C 進行內存管理。若是你進行了內存分配 —— 不管是直接使用了 malloc(),仍是使用一個 API 進行分配 —— 你都必須在必定的時候使用 free()。若是作不到這一點,就會形成內存泄露,這樣的程序就是一個不能被安全地使用的糟糕程序。

使用 hash

若是須要訪問字符串的哈希值,請使用 ZSTR_H()。可是,當你建立 zend_string 時,不會自動計算好對應的哈希值。當該字符串跟 HashTable API 一塊兒使用時,哈希值纔會計算。若是你想強制計算哈希值,請使用 ZSTR_HASH() 或者 zend_string_hash_val()。一旦哈希值被計算出來,它就會保存起來,而且不再會計算了。若是由於某種緣由,你須要從新計算 —— 例如你改變了字符串的值,這時候可使用 zend_string_forget_hash_val() 從新計算哈希值:

zend_string *str;

str = zend_string_init("foo", strlen("foo"), 0);
php_printf("This is my string: %s\n", ZSTR_VAL(str));
php_printf("It is %zd char long\n", ZSTR_LEN(str));

zend_string_hash_val(str);
php_printf("The string hash is %lu\n", ZSTR_H(str));

zend_string_forget_hash_val(str);
php_printf("The string hash is now cleared back to 0!");

zend_string_release(str);

字符串拷貝和內存管理

zend_string API 有一個很是好的特性是,它容許經過一小部分的簡單聲明來「擁有」一個字符串。而後引擎將不會在內存中拷貝字符串,而是簡單的增長引用計數(zend_refcounted_h 的一部分)。這容許在代碼的不少地方共享一塊內存。

這樣每當咱們談到「拷貝」一個 zend_string 時,實際上可能咱們不會複製內存中的任何內容。若是須要 —— 確實能夠這樣作 —— 接下來咱們談談字符串的拷貝。開始:

zend_string *foo, *bar, *bar2, *baz;

foo = zend_string_init("foo", strlen("foo"), 0); /* creates the "foo" string in foo */
bar = zend_string_init("bar", strlen("bar"), 0); /* creates the "bar" string in bar */

/* 建立 bar2 並將 bar 中的 "bar" 字符串 共享給 bar2
並且增長 "bar" 字符串的引用計數爲 2*/
bar2 = zend_string_copy(bar);

php_printf("We just copied two strings\n");
php_printf("See : bar content : %s, bar2 content : %s\n", ZSTR_VAL(bar), ZSTR_VAL(bar2));

/* 在內存中複製 "bar" 字符串,建立了 baz 變量並使它是一個獨立的 "bar" 字符串 */
baz = zend_string_dup(bar, 0);

php_printf("We just duplicated 'bar' in 'baz'\n");
php_printf("Now we are free to change 'baz' without fearing to change 'bar'\n");

/* 更改第二個 "bar" 字符串的最後一個字符,變成 "baz" */
ZSTR_VAL(baz)[ZSTR_LEN(baz) - 1] = 'z';

/* 當字符串值被改變,丟棄舊值的 hash(若是已經計算了),這樣須要從新計算它的 hash */
zend_string_forget_hash_val(baz);

php_printf("'baz' content is now %s\n", ZSTR_VAL(baz));

zend_string_release(foo);  /* destroys (frees) the "foo" string */
zend_string_release(bar);  /* decrements the refcount of the "bar" string to one */
zend_string_release(bar2); /* destroys (frees) the "bar" string both in bar and bar2 vars */
zend_string_release(baz);  /* destroys (frees) the "baz" string */

咱們從分配 「foo」 和 「bar」 開始。而後咱們建立了 bar2 字符串做爲 bar 的副本。在這裏,咱們都須要記住:barbar2 指向相同的內存區域中的 C 字符串,更改一個也將會更改另外一個。這是 zend_string_copy() 的行爲:它只是增長了所擁有的 C 字符串的引用計數。

若是咱們想分離開這個字符串 —— 也就是說咱們想在內存中有兩個獨立的字符串副本 —— 咱們須要使用 zend_string_dup() 拷貝。而後咱們將 bar2 變量字符串複製到 baz 變量中。如今,baz 變量嵌入了它本身的字符串副本,而且能夠在不影響 bar2 的狀況下更改它。這就是咱們要作的:咱們把 ‘bar’ 中的最後一個 ‘r’ 換成 ‘z’。而後咱們顯示它,並釋放全部字符串的內存。

注意,咱們忘記了哈希值(若是以前計算過,就不須要考慮這個細節)。這是一個值得記住的好習慣。如以前所述,若是 zend_string 用做 HashTable 的一部分,則可使用 hash。這是開發中很是常見的操做,更改字符串值也須要從新計算哈希值。忘記這樣一個細節將會致使錯誤,可能就會須要花一些時間來跟蹤排查了。

字符串的操做

zend_string API 還容許其餘的操做,好比擴展或縮減字符串、更改字符串大小寫或者比較字符串。目前尚未能用的鏈接操做,可是這也很是容易實現:

zend_string *FOO, *bar, *foobar, *foo_lc;

FOO = zend_string_init("FOO", strlen("FOO"), 0);
bar = zend_string_init("bar", strlen("bar"), 0);

/* 和 C 字符串字面量比較 zend_string */
if (!zend_string_equals_literal(FOO, "foobar")) {
    foobar = zend_string_copy(FOO);

    /* realloc() 將 C 字符串分配到一個更大的 buffer 中 */
    foobar = zend_string_extend(foobar, strlen("foobar"), 0);

    /* 在從新分配了足夠大的內存後鏈接 "bar" 和 "FOO"  */
    memcpy(ZSTR_VAL(foobar) + ZSTR_LEN(FOO), ZSTR_VAL(bar), ZSTR_LEN(bar));
}

php_printf("This is my new string: %s\n", ZSTR_VAL(foobar));

/* 比較兩個 zend_string  */
if (!zend_string_equals(FOO, foobar)) {
    /* 複製一個字符串並將其轉爲小寫 */
    foo_lc = zend_string_tolower(foo);
}

php_printf("This is FOO in lower-case: %s\n", ZSTR_VAL(foo_lc));

/* 釋放內存 */
zend_string_release(FOO);
zend_string_release(bar);
zend_string_release(foobar);
zend_string_release(foo_lc);

經過 zval 訪問 zend_string

如今你已經知道了如何管理和操做 zend_string,讓咱們看看他們與 zval 容器之間的交互。

  • 你須要熟悉 zval,若是不熟悉,請閱讀 Zvals 專用章節。

使用宏你將能夠將 zend_string 存儲到 zval 中,或從 zval 某種讀取 zend_string

zval myval;
zend_string *hello, *world;

zend_string_init(hello, "hello", strlen("hello"), 0);

/* 將字符串存入 zval */
ZVAL_STR(&myval, hello);

/* 從 zval 中的 zend_string 讀取 C 字符串 */
php_printf("The string is %s", Z_STRVAL(myval));

zend_string_init(world, "world", strlen("world"), 0);

/* 更改 myval 中的 zend_string:使用另外一個 zend_string 替換它  */
Z_STR(myval) = world;

/* ... */

你必須記住,以 ZSTR_***(s) 開頭的宏都是做用於 zend_string 的。

  • ZSTR_VAL()
  • ZSTR_LEN()
  • ZSTR_HASH()

全部以 Z_STR**(z) 開頭的宏都是做用於嵌入 zval 中的 zend_string

  • Z_STRVAL()
  • Z_STRLEN()
  • Z_STRHASH()

還有一些其餘你可能用不上的宏。

PHP 的歷史和 C 中的典型的字符串

簡單介紹一下經典的 C 字符串。在 C 語言中,字符串是字符數組(char foo[])或者字符指針(char *)。它們的長度是不肯定的,這就是它們會被 NUL 結束的緣由(知道字符串的開頭和結尾,就能知道它們的長度)。

在 PHP 7 以前,zend_string 結構體根本不存在。以前的傳統是將 char * / int 成對使用。你可能還能找到一些 PHP 源碼中還在使用 char * / int 而非 zend_string。你還能夠找到 API 工具來將 zend_stringchar * / int 相互轉換。

只要有可能:就使用 zend_string。有些罕見的地方不使用 zend_string 是由於那個地方無所謂使用 zend_string,可是 PHP 源碼中,你會發現不少地方都是對 zend_string 的引用。

內部的 zend_string

這裏簡單介紹一下 interned strings。在擴展開發中不多須要用到這樣的概念。Interned 字符串也時常與 OPCache 擴展交互。

Interned 字符串是去重複的字符串。當與 OPCache 一塊兒使用時,它們還會從一個請求重複使用到另外一個請求中。

假設你想建立字符串 「foo」。你要作的只是簡單地建立一個新的字符串 「foo」:

zend_string *foo;
foo = zend_string_init("foo", strlen("foo"), 0);

/* ... */

但問題來了:在你須要使用它以前,那個字符串不是已經被建立了嗎?當你須要一個字符串時,你代碼在 PHP 生命週期中的某個時刻執行,這意味着在你以前的一些代碼可能須要徹底同樣的字符串(也就是示例中的 「foo」)。

Interned 字符串是會要求引擎探測已經存儲了的 interned 字符串,並重用已經分配的指針(若是它能找到你的字符串)。若是找不到:建立一個新的字符串並將其標識爲 「interned」 字符串。這樣它可用於 PHP 源碼之外的場景(其餘擴展,引擎自己,等等)

這裏有一個示例:

zend_string *foo;
foo = zend_string_init("foo", strlen("foo"), 0);

foo = zend_new_interned_string(foo);

php_printf("This string is interned : %s", ZSTR_VAL(foo));

zend_string_release(foo);

在上面的代碼中,咱們建立了一個新的很是經典的 zend_string。而後咱們將建立的 zend_string 傳遞給 zend_new_interned_string()。這個函數在引擎的字符串緩衝區中查找相同的字符串片斷(這裏是 「foo」)。若是找到了它(意味着以前已經建立過這樣的字符串),它就會釋放你建立的字符串(多是釋放吧),並將其替換爲來自 interned 字符緩衝區的字符串。若是它沒有找到:就將 「foo」 添加到 interned 字符串緩衝區中,以便以後使用或者 PHP 的其它地方使用。

你必須注意一下這裏的內存分配。Interned 字符串老是將 refcount 設爲一,由於它們不須要被再次引用計數,由於它們將與 Interned 字符串緩衝區共享,所以它們不能被銷燬。

示例:

zend_string *foo, *foo2;

foo  = zend_string_init("foo", strlen("foo"), 0);
foo2 = zend_string_copy(foo); /* increments refcount of foo */

 /* refcount falls back to 1, even if the string is now
  * used at three different places */
foo = zend_new_interned_string(foo);

/* 當 foo 是 interned 字符串,這裏什麼也不會作 */
zend_string_release(foo);

/* 當 foo2 是 interned 字符串,這裏什麼也不會作 */
zend_string_release(foo2);

/* 進程結束後,PHP 將清楚 interned 字符串緩衝區,而且所以會對 "foo" 字符串進行 free()  */

接下來是關於垃圾回收的部分。

當字符串是 interned 時,它的 GC 標誌將被修改成 IS_STR_INTERNED 標識,無論它們使用的是什麼內存分配類(永久的仍是基於請求的)。當你要複製或者釋放字符串時,將探測這個標誌位。若是字符串是 interned 類型的,引擎將不會增長這個字符串的引用計數。但若是你釋放這個字符串,它不會減小引用計數也不會釋放。它什麼也不作。在進程的生命週期結束時,它纔會銷燬整個 interned 字符串緩衝區,也就釋放了你的 interned 字符串。

若是 OPCache 被觸發,這個過程實際上要更加複雜一些。OPCache 擴展改變了使用 interned 字符串的方式。沒有 OPCache 時,若是你在請求的過程當中建立了一個 interned zend_string,該字符串將在當前請求的結束時被清除,而且不會在下一個請求中重用。可是,若是你使用了 OPCache,則會將 interned 字符串存儲在共享內存塊中,並在共享同一個「池」的每一個 PHP 進程之間,會共享此內存塊。此外,在多個請求之間能夠複用 interned 字符串。

使用 Interned 字符串會節省內存,由於同一個字符串在內存中的存儲次數不會超過一次。可是,它可能會浪費一些 CPU 時間,由於它常常須要查找存儲了的 interned 字符串,即便這個過程已經優化不少了。做爲一個擴展設計者,如下是一些全局規則:

  • 若是使用了 OPCache(應該使用),若是須要建立請求綁定的只讀字符串:那就使用 interned 字符串吧。
  • 若是你知道本身須要一個 interned 字符串(一種衆所周知的 PHP 字符串,好比 「php」 或 「str_replace」),那就是使用 interned 字符串。
  • 若是字符串不是隻讀的,而且可能/應該在建立後會更改,則不要使用 interned 字符串。
  • 若是是未來不太可能複用的字符串,就不要使用 interned 字符串。

Interned 字符串詳情能夠參考Zend/zend_string.c

相關文章
相關標籤/搜索