一、 基礎知識
本章簡單介紹一些Zend引擎的內部機制,這些知識和Extensions密切相關,同時也可以幫助我們寫出更有效率的PHP程式碼。
1.1 PHP變數的儲存
1.1.1 zval結構
Zend使用zval結構來儲存PHP變數的值,此結構如下所示:
_化的複製碼long lval; /* long value */ double dval; /* double value */ struct {
char *val;
int len;
} str;
HashTable *ht /* hash table /* h
} zvalue_value;
struct _zvalue_struct {
/* Variable information */
zvalue_value value; /* value */
zend_uint refcounts );
typedef struct _zval_struct zval ;
Zend根據type值決定存取value的哪個成員,可用值如下:
IS_NULLN/A
IS_LONG對應value.lval
〜 ISSTRvalue。 IS_ARRAY對應value.ht
IS_OBJECT對應value.obj
IS_BOOL對應value.lval.
IS_RESOURCE對應碼為什麼PHP能夠支援關聯數組了;其次,Resource就是一個long值,它裡面存放的通常是個指針、一個內部數組的index或者其它什麼只有創建者自己才知道的東西,可以將其視為一個handle
1.1 .1 引用計數
引用計數在垃圾收集、記憶體池以及字串等地方應用廣泛,Zend就實現了典型的引用計數。多個PHP變數可以透過引用計數機制來共享同一份zval,zval中剩餘的兩個成員is_ref和refcount就用來支援這種共享。
很明顯,refcount用於計數,當增減引用時,這個值也相應的遞增和遞減,一旦減到零,Zend就會回收該zval。
那麼is_ref呢?
1.1.2 zval狀態
在PHP中,變數有兩種-引用和非引用的,它們在Zend中都是採用引用計數的方式儲存的。對於非引用型變量,要求變量間互不相干,修改一個變量時,不能影響到其他變量,採用Copy-On-Write機制即可解決這種衝突——當試圖寫入一個變量時,Zend若發現此變數所指向的zval被多個變數共享,則為其複製一份refcount為1的zval,並遞減原zval的refcount,這個過程稱為「zval分離」。然而,對於引用型變量,其要求和非引用型相反,引用賦值的變量間必須是捆綁的,修改一個變量就修改了所有捆綁變量。
可見,有必要指出當前zval的狀態,以分別應對這兩種情況,is_ref就是這個目的,它指出了當前所有指向該zval的變量是否是採用引用賦值的——要么全是引用,要么全不是。此時再修改一個變量,只有當發現其zval的is_ref為0,即非引用時,Zend才會執行Copy-On-Write。
1.1.3 zval狀態切換
當在一個zval上進行的所有賦值操作都是引用或者都是非引用時,一個is_ref就足夠應付了。然而,世界總是不會那麼美好,PHP無法對使用者進行這種限制,當我們混合使用引用和非引用賦值時,就必須進行特別處理了。
情況I、看如下PHP代碼:
全過程如下所示:
這段程式碼的前三句將把a、b和c指向一個zval,其is_ref=1, refcount=3;第四句是個非引用賦值,通常情況下只需要增加引用計數即可,然而目標zval屬於引用變量,單純的增加引用計數顯然是錯誤的, Zend的解決辦法是為d單獨生成一份zval副本。
全過程如下所示:
1.1.1 參數傳遞
PHP函數參數的傳遞和變數賦值是一樣的,非引用傳遞相當於非引用賦值,引用傳遞相當於非引用賦值,並且也有可能會賦值導致執行zval狀態切換。這在後面也會提到。
1.2 HashTable結構
HashTable是Zend引擎中最重要、使用最廣泛的資料結構,它被用來儲存幾乎所有的東西。
1.1.1 資料結構
HashTable資料結構定義如下:
複製程式碼
程式碼如下:
typedef struct bucket {
ulong h; // 存放hash
uint nKeyLength;
void *pData; // 指向value,是使用者資料的副本
void *pDataPtr; struct bastuck*pListNext
struct bucket *pListLast; // 整個HashTable的雙鍊錶
struct bucket *pNext; // pNext和pLast用來組成某個hash對應
struct bucket *pLast; // 的雙鍊錶
char arKey[1]; / / key
} Bucket;
typedef struct _hashtable {
uint nTableSize;
uint nTableMask;
uint nNumOfElements;
ead;
Bucket *pListTail;
Bucket **arBuckets; // hash陣列
dtor_func_t pDestructor; // HashTable初始化時指定,銷毀Bucket時呼叫
zend_bool persistent; // 是否採用C的記憶體分配程式
unsigned char nAppeool; if ZEND_DEBUG
int inconsistent;
#endif
} HashTable;
總的來說,Zend的HashTable是一種鍊錶散列,同時也為線性遍歷進行了最佳化,圖示如下:
HashTable中包含兩種資料結構,一個鍊錶散列和一個雙向鍊錶,前者用於進行快速鍵-值查詢,後者方便線性遍歷和排序,一個Bucket同時存在於這兩個資料結構中。
關於此資料結構的幾點解釋:
l 鍊錶散列中為什麼使用雙向鍊錶?
一般的鍊錶散列只需要按key進行操作,只需要單鍊錶就夠了。但是,Zend有時需要從鍊錶散列中刪除給定的Bucket,使用雙鍊錶可以非常有效率的實作。
l nTableMask是做什麼的?
這個值用於hash值到arBuckets數組下標的轉換。當初始化一個HashTable,Zend首先為arBuckets數組分配nTableSize大小的內存,nTableSize取不小於用戶指定大小的最小的2^n,即二進制的10*。 nTableMask = nTableSize – 1,即二進位的01*,此時h & nTableMask就恰好落在 [0, nTableSize – 1] 裡,Zend就以其為index來存取arBuckets數組。
l pDataPtr是做什麼的?
通常情況下,當使用者插入一個鍵值對時,Zend會將value複製一份,並將pData指向value副本。複製操作需要呼叫Zend內部例程emalloc來分配內存,這是個非常耗時的操作,並且會消耗比value大的一塊內存(多出的內存用於存放cookie),如果value很小的話,將會造成較大的浪費。考慮到HashTable多用於存放指標值,於是Zend引入pDataPtr,當value小到和指標一樣長時,Zend就直接將其複製到pDataPtr裡,並且將pData指向pDataPtr。這就避免了emalloc操作,同時也有利於提高Cache命中率。
arKey大小為什麼只有1?為什麼不使用指針管理key?
arKey是存放key的數組,但其大小卻只有1,並不足以放下key。在HashTable的初始化函數裡可以找到如下程式碼:
1p = (Bucket *) pemalloc(sizeof(Bucket) - 1 + nKeyLength, ht->persistent);
可見,Zend為一個足夠Bucketkey的內存,
l 上半部是Bucket,下半部是key,而arKey「剛好」是Bucket的最後一個元素,於是就可以使用arKey來存取key了。這種手法在記憶體管理例程中最為常見,當分配記憶體時,實際上是分配了比指定大小要大的內存,多出的上半部通常被稱為cookie,它儲存了這塊記憶體的信息,例如塊大小、上一塊指標、下一塊指標等,baidu的Transmit程式就使用了這種方法。
不用指針管理key,是為了減少一次emalloc操作,同時也可以提高Cache命中率。另一個必要的理由是,key絕大部分情況下是固定不變的,不會因為key變長了而導致重新分配整個Bucket。這同時也解釋了為什麼不把value也一起作為數組分配了--因為value是可變的。
1.2.2 PHP數組
關於HashTable還有一個疑問沒有回答,就是nNextFreeElement是幹什麼的?
不同於一般的散列,Zend的HashTable允許用戶直接指定hash值,而忽略key,甚至可以不指定key(此時,nKeyLength為0)。同時,HashTable也支援append操作,使用者連hash值也不用指定,只需要提供value,此時,Zend就用nNextFreeElement作為hash,之後將nNextFreeElement遞增。
HashTable的這種行為看起來很奇怪,因為這將無法按key訪問value,已經完全不是個散列了。理解問題的關鍵在於,PHP數組就是使用HashTable實現的-關聯數組使用正常的k-v映射將元素加入HashTable,其key為用戶指定的字串;非關聯數組則直接使用數組下標作為hash值,不存在key;而當在一個數組中混合使用關聯和非關聯時,或者使用array_push操作時,就需要用nNextFreeElement了。
再來看value,PHP陣列的value直接使用了zval這個通用結構,pData指向的是zval*,依照上一節的介紹,這個zval*將直接儲存在pDataPtr裡。由於直接使用了zval,數組的元素可以是任意PHP類型。
數組的遍歷操作,即foreach、each等,是透過HashTable的雙向鍊錶來進行的,pInternalPointer作為遊標記錄了目前位置。
1.2.3 變數符號表
除了數組,HashTable也被用來儲存許多其他數據,例如,PHP函數、變數符號、載入的模組、類別成員等。
一個變數符號表就相當於一個關聯數組,其key是變數名稱(可見,使用很長的變數名稱並不是個好主意),value是zval*。
在任一時刻PHP程式碼都可以看見兩個變數符號表-symbol_table和active_symbol_table-前者用於儲存全域變量,稱為全域符號表;後者是個指針,指向目前活動的變數符號表,通常情況下就是全域符號表。但是,當每次進入一個PHP函數時(此處指的是使用者使用PHP程式碼建立的函數),Zend都會建立函數局部的變數符號表,並將active_symbol_table指向局部符號表。 Zend總是使用active_symbol_table來存取變量,這樣就實現了局部變量的作用域控制。
但如果在函數局部訪問標記為global的變量,Zend會進行特殊處理——在active_symbol_table中創建symbol_table中同名變量的引用,如果symbol_table中沒有同名變量則會先創建。
1.3 內存和文件
程序擁有的資源一般包括內存和文件,對於通常的程序,這些資源是面向進程的,當進程結束後,操作系統或C庫會自動回收那些我們沒有明確釋放的資源。
但是,PHP程式有其特殊性,它是基於頁面的,一個頁面運行時同樣也會申請內存或文件這樣的資源,然而當頁面運行結束後,操作系統或C庫也許不會知道需要進行資源回收。例如,我們將php當作模組編譯到apache裡,並且以prefork或worker模式執行apache。這種情況下apache進程或執行緒是重複使用的,php頁面分配的記憶體將永駐記憶體直到出core。
為了解決這種問題,Zend提供了一套內存分配API,它們的作用和C中相應函數一樣,不同的是這些函數從Zend自己的內存池中分配內存,並且它們可以實現基於頁面的自動回收。在我們的模組中,為頁面分配的記憶體應該使用這些API,而不是C例程,否則Zend會在頁面結束時嘗試efree掉我們的內存,其結果通常就是crush。
emalloc()
efree()
estrdup()
estrndup()
ecalloc() 🎀用於替代C庫和作業系統對應的檔案API ,這些巨集能夠支援PHP的虛擬工作目錄,在模組程式碼中應該總是使用它們。巨集的具體定義請參考PHP原始碼”TSRM/tsrm_virtual_cwd.h”。可能你會注意到,所有那些巨集中並沒有提供close操作,這是因為close的物件是已開啟的資源,不涉及到檔案路徑,因此可以直接使用C或作業系統例程;同理,read/ write之類的操作也是直接使用C或作業系統的例程。
以上就介紹了機械設計製造及其自動化專業介紹 PHP核心介紹及擴展開發指南—基礎知識,包括了機械設計製造及其自動化專業介紹方面的內容,希望對PHP教程有興趣的朋友有所幫助。