PHP運行原理

PHP簡介:PHP是一種適用於web開發的動態腳本語言(網頁快捷開發),是用純C語言實現的。我們可以認爲PHP就是一個用C語言實現包含大量組件的軟件框架。更狹義一點可以認爲是一個功能強大的UI框架。

PHP的設計理念及特點

  • 多進程模型:PHP採用多進程模型,不同請求之間互不干涉,保證了一個請求掛掉不會對其它請求和服務造成影響。當然,PHP目前已支持多線程模型;
  • 弱類型語言:PHP是一門弱類型語言,變量的類型在定義時是不需要給定的,會在運行中根據變量的值發生隱式或是顯式的類型轉換,這種機制的靈活性在web開發中非常方便、高效;
  • PHP通過引擎(zend)+組件(extension)的模式來降低內部耦合;
  • 中間層(sapi)隔絕web server 與 PHP;
  • 語法簡單靈活,沒有太多的規範,當容易導致代碼風格的混亂。

PHP的四層架構(如下圖)PHP核心架構圖

  • Zend引擎:也可以稱之爲Zend軟件虛擬機,用於翻譯執行PHP代碼。Zend整體用純C實現,是PHP的內核部分,它將PHP代碼翻譯(詞法、語法解析等一系列編譯過程)爲可執行opcode代碼,並實現相應的處理方法、實現了基本的數據結構(主要的數據結構是hashtable)、內存分配及管理、提供了相應的api方法供外部調用,是一切的核心,所有的外圍功能均圍繞Zend實現。

    • Opcode:是一種PHP腳本編譯後的中間語言,是PHP最終真正執行的代碼,類似於java中的字節碼(ByteCode)執行一段PHP代碼會有如下四個步驟:
      在這裏插入圖片描述
      • 將PHP代碼轉換爲語言片段(Tokens);
      • 將Tokens轉換成簡單而有意義的表達式;
      • 將表達式編譯成Opocde;
      • 順次執行Opcodes,每次一條,從而實現PHP腳本的功能。
    • 由上可知,PHP實現了一個典型的動態語言執行過程:拿到一段代碼後,經過詞法解析、語法解析等階段後,源程序會被翻譯成一個個指令 (opcode),然後Zend虛擬機順次執行這些指令完成操作。另外,PHP本身是用C實現的,因此最終調用的也都是C的函數,實際上,我們可以把PHP看做是一個C開發的軟件。PHP的執行的核心是翻譯出來的一條一條指令,即opcode。
    • 補充:現在有的Cache比如APC,可以使得PHP緩存住Opcodes,這樣,每次有請求來臨的時候,就不需要重複執行前面3步,從而能大幅的提高PHP的執行速度。
  • Extensions:圍繞着Zend引擎,extensions通過組件式的方式提供各種基礎服務,我們常見的各種內置函數(如array 系列)、標準庫等都是通過extension來實現,用戶也可以根據需要實現自己的extension以達到功能擴展、性能優化等目的(如PHP中間層、富文本解析就是extension的典型應用)。

  • Sapi:Sapi全稱是Server Application Programming Interface,也就是服務端應用編程接口,Sapi通過一系列鉤子函數,使得PHP可以和外圍交互數據,這是PHP非常優越和成功的一個設計,通過 sapi成功的將PHP本身和上層應用解耦隔離,PHP可以不再考慮如何針對不同應用進行兼容,而應用本身也可以針對自己的特點實現不同的處理方式。

    • Sapi通過一系列的接口,使得外部應用可以和PHP交換數據並可以根據不同應用的特性實現特定的處理方法,我們常見的一些sapi有:

      • apache2handler:這是以apache作爲webserver,採用mod_PHP模式運行時候的處理方式也是現在應用最廣泛的一種。
      • cgi:這是webserver和PHP直接的另一種交互方式,也就是大名鼎鼎的fastcgi協議,在最近幾年fastcgi+PHP得到越來越多的應用,也是異步webserver所唯一支持的方式。
      • cli:命令行調用的應用模式
  • 上層應用:這就是我們平時編寫的PHP腳本程序,通過不同的sapi方式得到各種各樣的應用模式,如通過webserver(比較常用的是Apache或Nginx)實現web應用、在命令行下以腳本方式運行等等。

核心數據結構HashTable

  • HashTable在PHP中是數據存儲的核心。各種常量、變量、函數、類以及對象都是通過HashTable來組織的。在實現HashTable時結合了雙向鏈表和數組兩種數據結構的優點,提供了非常高效的查詢與存儲機制。
  • 在Zend HashTable包括兩個主要的數據結構,其一是Bucket(桶)結構,另一個是HashTable結構。Bucket結構是用於保存數據的容器,而 HashTable結構則提供了對所有這些Bucket(或桶列)進行管理的機制。每個元素(Bucket)都有一個唯一的鍵名(Key),根據鍵名可以在HashTable中確定一個唯一的數據元素。
  • Bucket結構:
typedef struct bucket {
ulong h;       /* Used for numeric indexing */
uint nKeyLength;     /* key 長度 */
void *pData;      /* 指向Bucket中保存的數據的指針 */
void *pDataPtr;     /* 指針數據 */
struct bucket *pListNext;   /* 指向HashTable桶列中下一個元素 */
struct bucket *pListLast;    /* 指向HashTable桶列中前一個元素 */
struct bucket *pNext;    /* 指向具有同一個hash值的桶列的後一個元素 */
struct bucket *pLast;    /* 指向具有同一個hash值的桶列的前一個元素 */
char arKey[1];      /* 必須是最後一個成員,key名稱*/
} Bucket;
  • 補充:鍵名有兩種表示方式。第一種方式使用字符串arKey作爲鍵名,該字符串的長度爲nKeyLength。注意到在上面的數據結構中arKey雖然只是一個長度爲1的字符數組,但它並不意味着key只能是一個字符。實際上Bucket是一個可變長的結構體,由於arKey是 Bucket的最後一個成員變量,通過arKey與nKeyLength結合可確定一個長度爲nKeyLength的key。這是C語言編程中的一個比較常用的技巧。另一種鍵名的表示方式是索引方式,這時nKeyLength總是0,長整型字段h就表示該數據元素的鍵名。簡單的來說,即如果 nKeyLength=0,則鍵名爲h;否則鍵名爲arKey, 鍵名的長度爲nKeyLength。當nKeyLength > 0時,並不表示這時的h值就沒有意義。事實上,此時它保存的是arKey對應的hash值。不管hash函數怎麼設計,衝突都是不可避免的,也就是說不同 的arKey可能有相同的hash值。具有相同hash值的Bucket保存在HashTable的arBuckets數組(參考下面的解釋)的同一個索引對應的桶列中。這個桶列是一個雙向鏈表,其前向元素,後向元素分別用pLast, pNext來表示。新插入的Bucket放在該桶列的最前面。在Bucket中,實際的數據是保存在pData指針指向的內存塊中,通常這個內存塊是系統另外分配的。但有一種情況例外,就是當Bucket保存的數據是一個指針時,HashTable將不會另外請求系統分配空間來保存這個指針,而是直接將該指針保存到pDataPtr中,然後再將pData指向 本結構成員的地址。這樣可以提高效率,減少內存碎片。由此我們可以看到PHP HashTable設計的精妙之處。如果Bucket中的數據不是一個指針,pDataPtr爲NULL。HashTable中所有的Bucket通過pListNext, pListLast構成了一個雙向鏈表。最新插入的Bucket放在這個雙向鏈表的最後。
  • 注意:在一般情況下,Bucket並不能提供它所存儲的數據大小的信息。所以在PHP的實現中,Bucket中保存的數據必須具有管理自身大小的能力。
  • HashTable結構:
typedef struct _hashtable {
uint nTableSize;
uint nTableMask;
uint nNumOfElements;
ulong nNextFreeElement;
Bucket *pInternalPointer;
Bucket *pListHead;
Bucket *pListTail;
Bucket **arBuckets;
dtor_func_t pDestructor;
zend_bool persistent;
unsigned char nApplyCount;
zend_bool bApplyProtection;
 
#if ZEND_DEBUG
int inconsistent;
#endif
} HashTable;
  • 在HashTable結構中,nTableSize指定了HashTable的大小,同時它限定了HashTable中能保存Bucket的最大數量,此數越大,系統爲HashTable分配的內存就越多。爲了提高計算效率,系統自動會將nTableSize調整到最小一個不小於nTableSize的2 的整數次方。也就是說,如果在初始化HashTable時指定一個nTableSize不是2的整數次方,系統將會自動調整nTableSize的值。即:nTableSize = 2ceil(log(nTableSize, 2)) 或 nTableSize = pow(ceil(log(nTableSize,2)))。如果在初始化HashTable的時候指定nTableSize = 11,HashTable初始化程序會自動將nTableSize增大到16。
  • arBuckets是HashTable的關鍵,HashTable初始化程序會自動申請一塊內存,並將其地址賦值給arBuckets,該內存大小正好能容納nTableSize個指針。我們可以將arBuckets看作一個大小爲nTableSize的數組,每個數組元素都是一個指針,用於指向實際存放數據的Bucket。當然剛開始時每個指針均爲NULL。
  • nTableMask的值永遠是nTableSize – 1,引入這個字段的主要目的是爲了提高計算效率,是爲了快速計算Bucket鍵名在arBuckets數組中的索引。
  • nNumberOfElements記錄了HashTable當前保存的數據元素的個數。當nNumberOfElement大於nTableSize時,HashTable將自動擴展爲原來的兩倍大小。
  • nNextFreeElement記錄HashTable中下一個可用於插入數據元素的arBuckets的索引。
  • pListHead、 pListTail則分別表示Bucket雙向鏈表的第一個和最後一個元素,這些數據元素通常是根據插入的順序排列的。也可以通過各種排序函數對其進行重 新排列。pInternalPointer則用於在遍歷HashTable時記錄當前遍歷的位置,它是一個指針,指向當前遍歷到的Bucket,初始值是 pListHead。
  • pDestructor是一個函數指針,在HashTable增加、修改、刪除Bucket時自動調用,用於處理相關數據的清理工作。
  • persistent標誌位指出了Bucket內存分配的方式。如果persisient爲TRUE,則使用操作系統本身的內存分配函數爲Bucket分配內存,否則使用PHP的內存分配函數。具體請參考PHP的內存管理。
  • nApplyCount與bApplyProtection結合提供了一個防止在遍歷HashTable時進入遞歸循環時的一種機制。
  • inconsistent成員用於調試目的,只在PHP編譯成調試版本時有效。表示HashTable的狀態,狀態有四種:
狀態值 				含義
HT_IS_DESTROYING 	正在刪除所有的內容,包括arBuckets本身
HT_IS_DESTROYED 	已刪除,包括arBuckets本身
HT_CLEANING 		正在清除所有的arBuckets指向的內容,但不包括arBuckets本身
HT_OK 				正常狀態,各種數據完全一致
  • 鍵名結構:
typedef struct _zend_hash_key {
char *arKey;      /* hash元素key名稱 */
uint nKeyLength;     /* hash 元素key長度 */
ulong h;       /* key計算出的hash值或直接指定的數值下標 */
} zend_hash_key;
  • 現在來看zend_hash_key結構就比較容易理解了。它通過arKey, nKeyLength, h三個字段唯一確定了HashTable中的一個元素。

結語:以上便是自己總結的一些原理知識,其中參考了一位大佬寫的文章,非常不錯,推薦一下Zend HashTable詳解,文章中對HashTable的實現也做了描述,值得一看。

發佈了61 篇原創文章 · 獲贊 67 · 訪問量 5萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章