# 9.2 PHP中的資源類型
通常情況下,像{資源}這類復合類型的數據都會占用大量的硬件資源,比如內存、CPU以及網絡帶寬。對于使用頻率超級高的數據庫鏈接,我們可以獲取一個長鏈接,使其不會在腳本結束后自動銷毀,一旦創建便可以在各個請求中直接使用,從而減少每次創建它的消耗。Mysql的長鏈接在PHP內核中其實就是一種持久{資源}。
Memory Allocation
前面的章節里我們接觸了emalloc()之類的以e開頭的內存管理函數,通過它們申請的內存都會被內核自動的進行垃圾回收的操作。而對于一個持久{資源}來說,我們是絕對不希望它在腳本結束后被回收的。
假設我們需要在我們的{資源}中同時保存文件名和文件句柄兩個數據,現在我們就需要自己定義個結構了:
````c
typedef struct _php_sample_descriptor_data
{
char *filename;
FILE *fp;
}php_sample_descriptor_data;
````
當然,因為結構變了(之前是個FILE*),我們之前的代碼也需要跟著改動。這里還沒有涉及到持久{資源},僅僅是換了一種{資源}結構
````c
static void php_sample_descriptor_dtor(zend_rsrc_list_entry *rsrc TSRMLS_DC)
{
php_sample_descriptor_data *fdata = (php_sample_descriptor_data*)rsrc->ptr;
fclose(fdata->fp);
efree(fdata->filename);
efree(fdata);
}
PHP_FUNCTION(sample_fopen)
{
php_sample_descriptor_data *fdata;
FILE *fp;
char *filename, *mode;
int filename_len, mode_len;
if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "ss",&filename, &filename_len,&mode, &mode_len) == FAILURE)
{
RETURN_NULL();
}
if (!filename_len || !mode_len) {
php_error_docref(NULL TSRMLS_CC, E_WARNING,"Invalid filename or mode length");
RETURN_FALSE;
}
fp = fopen(filename, mode);
if (!fp)
{
php_error_docref(NULL TSRMLS_CC, E_WARNING,"Unable to open %s using mode %s",filename, mode);
RETURN_FALSE;
}
fdata = emalloc(sizeof(php_sample_descriptor_data));
fdata->fp = fp;
fdata->filename = estrndup(filename, filename_len);
ZEND_REGISTER_RESOURCE(return_value, fdata,le_sample_descriptor);
}
PHP_FUNCTION(sample_fwrite)
{
php_sample_descriptor_data *fdata;
zval *file_resource;
char *data;
int data_len;
if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "rs",&file_resource, &data, &data_len) == FAILURE )
{
RETURN_NULL();
}
ZEND_FETCH_RESOURCE(fdata, php_sample_descriptor_data*,&file_resource, -1,PHP_SAMPLE_DESCRIPTOR_RES_NAME, le_sample_descriptor);
RETURN_LONG(fwrite(data, 1, data_len, fdata->fp));
}
````
<div class="tip-common">我們這里沒有重寫sample_fclose()函數,你可以嘗試著自己實現它。</div>
現在編譯運行,所有代碼的結果都非常正確,我們還可以在內核中獲取每個{資源}對應的文件名稱了。
````c
PHP_FUNCTION(sample_fname)
{
php_sample_descriptor_data *fdata;
zval *file_resource;
if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "r",&file_resource) == FAILURE )
{
RETURN_NULL();
}
ZEND_FETCH_RESOURCE(fdata, php_sample_descriptor_data*,&file_resource, -1,PHP_SAMPLE_DESCRIPTOR_RES_NAME, le_sample_descriptor);
RETURN_STRING(fdata->filename, 1);
}
````
現在,Persistent Resources來了!
### Delayed Destruction
在前面我們刪除一個{資源}的時候,其實是去EG(regular_list)中將其刪掉,EG(regular_list)存儲著所有的只用在當前請求的{資源}。
持久{資源},存儲在另一個HashTable中:EG(persistent_list)。其與EG(regular_list)有個明顯的區別,那就是它每個值的索引都是字符串類型的,而且它的每個值也不會在每次請求結束后被釋放掉,只能我們手動通過zend_hash_del()來刪除,或者在進程結束后類似于MSHUTDOWN階段將EG(persistent_list)整體清除,最常見的情景便是操作系統關閉了Web Server。
EG(persistent_list)對其元素也有自己的dtor回調函數,和EG(regular_list)一樣,它將根據其值的類型去調用不同的回調函數,我們這一次注冊回調函數的時候,需要用到zend_register_list_destructors_ex()函數的第二個參數,第一個則被賦成NULL。
在底層的實現中,持久的和regular{資源}是分別在不同的地方存儲的,也分別擁有各自不同的釋放函數。但在我們為腳本提供的函數中,卻希望能夠封裝這種差異,從而使我們的用戶使用起來更加方便快捷。
````c
static int le_sample_descriptor_persist;
static void php_sample_descriptor_dtor_persistent(zend_rsrc_list_entry *rsrc TSRMLS_DC)
{
php_sample_descriptor_data *fdata = (php_sample_descriptor_data*)rsrc->ptr;
fclose(fdata->fp);
pefree(fdata->filename, 1);
pefree(fdata, 1);
}
PHP_MINIT_FUNCTION(sample)
{
le_sample_descriptor = zend_register_list_destructors_ex(php_sample_descriptor_dtor, NULL,PHP_SAMPLE_DESCRIPTOR_RES_NAME, module_number);
le_sample_descriptor_persist =zend_register_list_destructors_ex(NULL, php_sample_descriptor_dtor_persistent,PHP_SAMPLE_DESCRIPTOR_RES_NAME, module_number);
return SUCCESS;
}
````
我們并沒有為這兩種{資源}起不同的名字,以防使用戶產生疑惑。
現在我們的PHP擴展中引進了一種新的{資源},所以我們需要改寫一下上面的函數,<b>盡量使</b>用戶使用時感覺不到這種差異。
````c
//sample_fopen()
PHP_FUNCTION(sample_fopen)
{
php_sample_descriptor_data *fdata;
FILE *fp;
char *filename, *mode;
int filename_len, mode_len;
zend_bool persist = 0;
//類比一下mysql_connect函數的最后一個參數。
if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC,"ss|b",&filename, &filename_len, &mode, &mode_len,&persist) == FAILURE)
{
RETURN_NULL();
}
if (!filename_len || !mode_len)
{
php_error_docref(NULL TSRMLS_CC, E_WARNING,"Invalid filename or mode length");
RETURN_FALSE;
}
fp = fopen(filename, mode);
if (!fp)
{
php_error_docref(NULL TSRMLS_CC, E_WARNING,"Unable to open %s using mode %s",filename, mode);
RETURN_FALSE;
}
if (!persist)
{
fdata = emalloc(sizeof(php_sample_descriptor_data));
fdata->filename = estrndup(filename, filename_len);
fdata->fp = fp;
ZEND_REGISTER_RESOURCE(return_value, fdata,le_sample_descriptor);
}
else
{
list_entry le;
char *hash_key;
int hash_key_len;
fdata =pemalloc(sizeof(php_sample_descriptor_data),1);
fdata->filename = pemalloc(filename_len + 1, 1);
memcpy(fdata->filename, filename, filename_len + 1);
fdata->fp = fp;
//在EG(regular_list中存一份)
ZEND_REGISTER_RESOURCE(return_value, fdata,le_sample_descriptor_persist);
//在EG(persistent_list)中再存一份
le.type = le_sample_descriptor_persist;
le.ptr = fdata;
hash_key_len = spprintf(&hash_key, 0,"sample_descriptor:%s:%s", filename, mode);
zend_hash_update(&EG(persistent_list),hash_key, hash_key_len + 1,(void*)&le, sizeof(list_entry), NULL);
efree(hash_key);
}
}
````
在持久{資源}時,因為我們在EG(regular_list)中也保存了一份,所以腳本中我們資源類型的變量在實現中仍然是保存著一個resource ID,我們可以用它來進行之前章節所做的工作。
將其添加到EG(persistent_list)中時,我們進行的操作流程幾乎和ZEND_REGISTER_RESOURCE()宏函數一樣,唯一的不同便是索引由之前的數字類型換成了字符串類型。
當一個保存在EG(regular_list)中的持久{資源}被腳本釋放時,內核會在EG(regular_list)尋找它對應的dtor函數,但它找到的是NULL,因為我們在使用zend_register_list_destructors_ex()函數聲明這種資源類型時,第一個參數的值為NULL。所以此時這個{資源}不會被任何dtor函數調用,可以繼續存在于內存中,任腳本流逝,請求更迭。
當web server的進程執行完畢后,內核會掃描EG(persistent_list)的dtor,并調用我們已經定義好的釋放函數。在我們定義的釋放函數中,一定要記得使用pfree函數來釋放內存,而不是efree。
### Reuse
創建持久{資源}的目的是為了使用它,而不是讓它來浪費內存的,我們再次重寫一下sample_open()函數,這一次我們將檢測需要創建的資源是否已經在persistent_list中存在了。
````c
PHP_FUNCTION(sample_fopen)
{
php_sample_descriptor_data *fdata;
FILE *fp;
char *filename, *mode, *hash_key;
int filename_len, mode_len, hash_key_len;
zend_bool persist = 0;
list_entry *existing_file;
if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC,"ss|b",&filename, &filename_len, &mode, &mode_len,&persist) == FAILURE)
{
RETURN_NULL();
}
if (!filename_len || !mode_len)
{
php_error_docref(NULL TSRMLS_CC, E_WARNING,"Invalid filename or mode length");
RETURN_FALSE;
}
//看看是否已經存在,如果已經存在就直接使用,不再創建
hash_key_len = spprintf(&hash_key, 0,"sample_descriptor:%s:%s", filename, mode);
if (zend_hash_find(&EG(persistent_list), hash_key,hash_key_len + 1, (void **)&existing_file) == SUCCESS)
{
//存在一個,直接使用!
ZEND_REGISTER_RESOURCE(return_value,existing_file->ptr, le_sample_descriptor_persist);
efree(hash_key);
return;
}
fp = fopen(filename, mode);
if (!fp)
{
php_error_docref(NULL TSRMLS_CC, E_WARNING,"Unable to open %s using mode %s",filename, mode);
RETURN_FALSE;
}
if (!persist)
{
fdata = emalloc(sizeof(php_sample_descriptor_data));
fdata->filename = estrndup(filename, filename_len);
fdata->fp = fp;
ZEND_REGISTER_RESOURCE(return_value, fdata,le_sample_descriptor);
}
else
{
list_entry le;
fdata =pemalloc(sizeof(php_sample_descriptor_data),1);
fdata->filename = pemalloc(filename_len + 1, 1);
memcpy(fdata->filename, filename, filename_len + 1);
fdata->fp = fp;
ZEND_REGISTER_RESOURCE(return_value, fdata,le_sample_descriptor_persist);
/* Store a copy in the persistent_list */
le.type = le_sample_descriptor_persist;
le.ptr = fdata;
//hash_key在上面已經被創建了
zend_hash_update(&EG(persistent_list),hash_key, hash_key_len + 1,(void*)&le, sizeof(list_entry), NULL);
}
efree(hash_key);
}
````
因為所有的PHP擴展都共用同一個HashTable來保存持久{資源},所以我們在為{資源}的索引起名時,一定要唯一,同時必須簡單,方便我們在其它的函數中構造出來。
### Liveness Checking and Early Departure
一旦我們打開一個本地文件,便可以一直占有它的操作句柄,保證隨時可以打開它。但是對于一些存在于遠程計算機上的資源,比如mysql鏈接、http鏈接,雖然我們仍然握著與服務器的鏈接,但是這個鏈接在服務器端可能已經被關閉了,在本地我們就無法再用它來做一些有價值的工作了。
所以,當我們使用{資源},尤其是持久{資源}時,一定要保證獲取出來的{資源}仍然是有效的、可以使用的。如果它失效了,我們必須將其從persistent list中移除。下面就是一個檢測socket有效性的例子:
````c
if (zend_hash_find(&EG(persistent_list), hash_key,hash_key_len + 1, (void**)&socket) == SUCCESS)
{
if (php_sample_socket_is_alive(socket->ptr))
{
ZEND_REGISTER_RESOURCE(return_value,socket->ptr, le_sample_socket);
return;
}
zend_hash_del(&EG(persistent_list),hash_key, hash_key_len + 1);
}
````
如你所見,{資源}失效后,我們只要把它從HashTable中刪除就行了,這一步操作同樣會激活我們設置的回調函數。On completion of this code block, the function will be in the same state it would have been if no resource had been found in the persistent list.
### Agnostic Retrieval
現在我們已經可以創建資源類型并生成新的資源,還能將持久{資源}與平常{資源}使用的差異性封裝起來。但是如果用戶對一個持久{資源}調用sample_fwrite()時候并不會正常工作,先想一下內核是如何通過一個數字所以在regular_list中獲取最終資源的。
````c
ZEND_FETCH_RESOURCE(
fdata,
php_sample_descriptor_data*,
&file_resource,
-1,
PHP_SAMPLE_DESCRIPTOR_RES_NAME,
le_sample_descriptor
);
````
le_sample_descriptor可以保證你獲取到的資源確實是這種類型的,絕不會出現你想要一個文件句柄,卻返回給你一個mysql鏈接的情況。這種驗證是必須的,但有時你又想繞過這種驗證,因為我們放在persistenst_list中的{資源}是le_sample_descruotor_persist類型的,所以當我們把它復制到regular_list中時,它也是le_sample_descructor_persist的,所以如果我們想獲取它,貌似只有兩種方法,要么修改類型,要么再寫一個新的sample_write_persistent函數的實現。或者極端一些,在sample_write函數里進行復雜的判斷。但是如果sample_write()函數能同時接收它們兩種類型的{資源}多好啊....
事情沒有這么復雜,我們確實可以在sample_write()函數里獲取{資源}時候同時指定兩種類型。那就是使用ZEND_FETCH_RESOURCE2()宏函數,它與ZEND_FETCH_RESOURCE()宏函數的唯一區別就是它可以接收兩種類型參數。
````c
ZEND_FETCH_RESOURCE2(
fdata,
php_sample_descriptor_data*,
&file_resource,
-1,
PHP_SAMPLE_DESCRIPTOR_RES_NAME,
le_sample_descriptor,
le_sample_descriptor_persist
);
````
現在,只要resource ID對應的最終資源類型是persistent或者non-persistent的一種便可以正常通過驗證了。
什么,你想設置三種甚至更多的類型?!!那你只能直接使用zend_fetch_resource()函數了。
````c
//一種類型的
fp = (FILE*) zend_fetch_resource(
&file_descriptor TSRMLS_CC,
-1,
PHP_SAMPLE_DESCRIPTOR_RES_NAME,
NULL,
1,
le_sample_descriptor
);
ZEND_VERIFY_RESOURCE(fp);
````
想看看ZEND_FETCH_RESOURCE2()宏函數的實現么?
````c
//兩種類型的
fp = (FILE*) zend_fetch_resource(
&file_descriptor TSRMLS_CC,
-1,
PHP_SAMPLE_DESCRIPTOR_RES_NAME,
NULL,
2,
le_sample_descriptor,
le_sample_descriptor_persist
);
ZEND_VERIFY_RESOURCE(fp);
````
再給力一些,三種類型的:
````c
fp = (FILE*) zend_fetch_resource(
&file_descriptor TSRMLS_CC,
-1,
PHP_SAMPLE_DESCRIPTOR_RES_NAME,
NULL,
3,
le_sample_descriptor,
le_sample_descriptor_persist,
le_sample_othertype
);
ZEND_VERIFY_RESOURCE(fp);
````
話都說到這份上了,你肯定知道四種、五種、更多種類型的應該怎么調用了。
## links
* 9.1 [復合類型的數據——{資源}](<9.1.md>)
* 9.3 [{資源}自有的引用計數](<9.3.md>)
- about
- 開始閱讀
- 目錄
- 1 PHP的生命周期
- 1.讓我們從SAPI開始
- 2.PHP的啟動與終止
- 3.PHP的生命周期
- 4.線程安全
- 5.小結
- 2 PHP變量在內核中的實現
- 1. 變量的類型
- 2. 變量的值
- 3. 創建PHP變量
- 4. 變量的存儲方式
- 5. 變量的檢索
- 6. 類型轉換
- 7. 小結
- 3 內存管理
- 1. 內存管理
- 2. 引用計數
- 3. 總結
- 4 動手編譯PHP
- 1. 編譯前的準備
- 2. PHP編譯前的config配置
- 3. Unix/Linux平臺下的編譯
- 4. 在Win32平臺上編譯PHP
- 5. 小結
- 5 Your First Extension
- 1. 一個擴展的基本結構
- 2. 編譯我們的擴展
- 3. 靜態編譯
- 4. 編寫函數
- 5. 小結
- 6 函數返回值
- 1. 一個特殊的參數:return_value
- 2. 引用與函數的執行結果
- 3. 小結
- 7 函數的參數
- 1. zend_parse_parameters
- 2. Arg Info 與類型綁定
- 3. 小結
- 8 使用HashTable與{數組}
- 1. 數組(C中的)與鏈表
- 2. 操作HashTable的API
- 3. 在內核中操作PHP語言中數組
- 4. 小結
- 9 PHP中的資源類型
- 1. 復合類型的數據——{資源}
- 2. Persistent Resources
- 3. {資源}自有的引用計數
- 4. 小結
- 10 PHP中的面向對象(一)
- 1. zend_class_entry
- 2. 定義一個類
- 3. 定義一個接口
- 4. 類的繼承與接口的實現
- 5. 小結
- 11 PHP中的面向對象(二)
- 1. 生成對象的實例與調用方法
- 2. 讀寫對象的屬性
- 3. 小結
- 12 啟動與終止的那點事
- 2. 小結
- 1. 關于生命周期
- 2. MINFO與phpinfo
- 3. 常量
- 4. PHP擴展中的全局變量
- 5. PHP語言中的超級全局變量
- 6. 小結
- 13 INI設置
- 1. 聲明和訪問ini設置
- 2. 小結
- 2. 小結
- 14 流式訪問
- 1. 概覽
- 2. 打開流
- 3. 訪問流
- 4. 靜態資源操作
- 5. 小結
- 15 流的實現
- 1. php流的表象之下
- 2. 包裝器操作
- 3. 實現一個包裝器
- 4. 操縱
- 5. 檢查
- 6. 小結
- 16 有趣的流
- 1. 上下文
- 2. 過濾器
- 3. 小結
- 17 配置和鏈接
- 1. autoconf
- 2. 庫的查找
- 3. 強制模塊依賴
- 4. Windows方言
- 5. 小結
- 18 擴展生成
- 1. ext_skel
- 2. PECL_Gen
- 3. 小結
- 19 設置宿主環境
- 1. 嵌入式SAPI
- 2. 構建并編譯一個宿主應用
- 3. 通過嵌入包裝重新創建cli
- 4. 老技術新用
- 5. 小結
- 20 高級嵌入式
- 1. 回調到php中
- 2. 錯誤處理
- 3. 初始化php
- 4. 覆寫INI_SYSTEM和INI_PERDIR選項
- 5. 捕獲輸出
- 6. 同時擴展和嵌入
- 7. 小結
- 約定