跟廠長學PHP7內核(八):深入理解字元串的實現

来源:https://www.cnblogs.com/enochzzg/archive/2018/09/21/9688248.html
-Advertisement-
Play Games

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

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

您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 我想很多程式員應該記得 GitHub 上有一個 Awesome - XXX 系列的資源整理。awesome-ios 就是 vsouza 發起維護的 iOS 資源列表,內容包括:框架、組件、測試、Apple Store、SDK、XCode、網站、書籍等。Swift 語言寫成的項目會被標記為 ★ ,Ap ...
  • 前言:項目中經常要用到Maven,從來也沒有配置過,直到當人問到Maven是乾什麼的,是怎麼管理項目的?一頭霧水,所以寫了這篇博客,首先附上百度百科的詞條: Maven項目對象模型(POM),可以通過一小段描述信息來管理項目的構建,報告和文檔的軟體項目管理工具。 一、Maven的下載環境變數配置 下 ...
  • 題意 題目鏈接 Sol 如果給出的樹是鏈的話顯然就是LIS 不是鏈的時候直接當鏈做,每個節點維護一個multiset表示計算LIS過程中的單調棧 啟髮式合併即可 時間複雜度:$O(nlog^2n)$ ...
  • 明白生產環境中的jvm參數 寫代碼的時候,程式寫完了,發到線上去運行,跑一段時間後,程式變慢了,cpu負載高了……一堆問題出來了,所以瞭解一下生產環境的機器上的jvm配置是有必要的。比如說: JDK版本是多少?採用何種垃圾回收器? 程式啟動的時候預設分配堆記憶體空間是多少?隨著程式的運行,程式最多能使 ...
  • 每個程式員、或者說每個工作者都應該有自己的職業規劃,如果你不是富二代,不是官二代,也沒有職業規劃,希望你可以思考一下自己的將來。今天給大家分享的是一篇來自阿裡Java架構師對普通程式員的職業建議,希望對你有啟發。 普通程式員,三年成為年薪70w架構師,只因做到了這些 ...
  • 1.分析 上傳文件的過程:客服端選擇一個文件後,寫入到伺服器端,伺服器端使用一個目錄來存儲該文件--底層IO流操作 2.jsp文件上的表單設計 表單傳輸格式用multipart/form-data,要上傳的文件input標簽name屬性最好用同樣的首碼或者尾碼好獲取 3後臺Servlet處理 1.S ...
  • 題意 題目鏈接 題意:給出一張無向圖,每次詢問兩點之間的最短路,滿足$m - n <= 20$ $n, m, q \leqslant 10^5$ Sol 非常好的一道題。 首先建出一個dfs樹。 因為邊數-點數非常少,所以我們可以對於某些非樹邊特殊考慮。 具體做法是:對於非樹邊連接的兩個點,暴力求出 ...
  • 分散式架構有以下幾點普適性的共性需求: 1. 提供集群的集中化的配置管理功能,可以不重啟就讓新的配置參數生效,類似與配置中心 2. 簡單可靠的集群節點動態發現機制,便於動態發現服務,動態擴展節點 3. 簡單可靠的leader選舉機制 4. 提供分散式鎖 zookeeper的數據結構整體上可以看作一顆 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...