在閱讀下面的內(nèi)容之前,我假定已看到的人已經(jīng)對 PHP 7 基本的數(shù)據(jù)結(jié)構(gòu)都有大致的了解了玩祟,這是下面內(nèi)容閱讀的前提腹缩。
我們分為兩大塊:
首先實(shí)現(xiàn)一個(gè)自定義的文件打開、讀取空扎、寫入、關(guān)閉的文件操作擴(kuò)展润讥;
然后分析各個(gè)操作背后的實(shí)現(xiàn)原理转锈,其中某些部分的實(shí)現(xiàn)我會(huì)和PHP 5.3 使用資源包裹第三方擴(kuò)展源碼解讀對比分析。
0 通過原型生成擴(kuò)展骨架
首先進(jìn)入到源碼目錄的ext目錄中楚殿,添加一個(gè)文件操作的原型文件
[root@localhost?php-src-php-7.0.3]#?cd?ext/
[root@localhost?ext]#?vim?tipi_file.proto
編輯原型為
resource?file_open(string?filename,?string?mode)
string?file_read(resource?filehandle,?int?size)
bool?file_write(resource?filehandle,?string?buffer)
bool?file_close(resource?filehandle)
[root@localhost?ext]#?./ext_skel?--extname=tipi_file?--proto=./tipi_file.proto
這樣一個(gè)簡單的文件操作擴(kuò)展的代碼骨架就生成了撮慨。
完整代碼tipi_file.c(https://github.com/zhoumengkang/notes/blob/master/php-extension/php7.0/tipi_file/tipi_file.c),可以先有一個(gè)大致的了解脆粥,這樣后面閱讀時(shí)砌溺,思路可能會(huì)清晰很多。
1 擴(kuò)展的實(shí)現(xiàn)
1.1?注冊資源類型
1.1.1 注冊資源 API
ZEND_APIintzend_register_list_destructors_ex(rsrc_dtor_func_t?ld,?rsrc_dtor_func_t?pld,constchar*type_name,intmodule_number)
參數(shù)解釋
ld釋放該資源時(shí)調(diào)用的函數(shù)变隔。
pld釋放用于在不同請求中始終存在的永久資源的函數(shù)规伐。
type_name是一個(gè)具有描述性類型名稱的字符串。
module_number為引擎內(nèi)部使用匣缘,當(dāng)我們調(diào)用這個(gè)函數(shù)時(shí)猖闪,我們只需要傳遞一個(gè)已經(jīng)定義好的module_number變量。
該 API 返回一個(gè)資源類型 id肌厨,該id應(yīng)當(dāng)被作為全局變量保存在擴(kuò)展里培慌,以便在必要的時(shí)候傳遞給其他資源API。
1.1.2 添加資源釋放回調(diào)函數(shù)
staticvoidtipi_file_dtor(zend_resource?*rsrc?TSRMLS_DC){
FILE*fp?=?(FILE*)?rsrc->ptr;
fclose(fp);
}
我們發(fā)現(xiàn)該函數(shù)的參數(shù)類型是zend_resource柑爸。這是 PHP7 新增的數(shù)據(jù)結(jié)構(gòu)吵护,在 PHP 5 則是zend_rsrc_list_entry。細(xì)節(jié)的內(nèi)容表鳍,我們留在后面分析馅而。
1.1.3 在PHP_MINIT_FUNCTION中注冊
我們知道在 PHP 生命周期中,當(dāng) PHP 被裝載時(shí)进胯,PHP_MINIT_FUNCTION(模塊啟動(dòng)函數(shù))即被引擎調(diào)用用爪。這使得引擎做一些例如資源類型,注冊INI變量等的一次初始化胁镐。
那么我們需要在這里通過zend_register_list_destructors_ex在PHP_MINIT_FUNCTION來注冊資源類型偎血。
PHP_MINIT_FUNCTION(tipi_file)
{
/*?If?you?have?INI?entries,?uncomment?these?lines
REGISTER_INI_ENTRIES();
*/
le_tipi_file?=?zend_register_list_destructors_ex(tipi_file_dtor,?NULL,?TIPI_FILE_TYPE,?module_number);
returnSUCCESS;
}
其中TIPI_FILE_TYPE在前面已經(jīng)定義了诸衔,是該擴(kuò)展的別名(具體可以對比著代碼 tipi_file.c 查看(https://github.com/zhoumengkang/notes/blob/master/php-extension/php7.0/tipi_file/tipi_file.c))
1.2 注冊資源
1.2.1 注冊資源 API
在 PHP 7 中刪除了原來的ZEND_REGISTER_RESOURCE宏,直接使用zend_register_resource函數(shù)
ZEND_API?zend_resource*?zend_register_resource(void*rsrc_pointer,intrsrc_type)
參數(shù)解釋
rsrc_pointer資源數(shù)據(jù)指針
rsrc_type注冊資源類型時(shí)獲得的資源類型 id
1.2.2 在 file_open函數(shù)中實(shí)現(xiàn)資源的注冊
PHP_FUNCTION(file_open)
{
char*filename?=?NULL;
char*mode?=?NULL;
intargc?=?ZEND_NUM_ARGS();
size_tfilename_len;
size_tmode_len;
if(zend_parse_parameters(argc?TSRMLS_CC,"ss",?&filename,?&filename_len,?&mode,?&mode_len)?==?FAILURE)
return;
//?使用?VCWD?宏取代標(biāo)準(zhǔn)?C?文件操作函數(shù)
FILE*fp?=?VCWD_FOPEN(filename,?mode);
if(fp?==?NULL)?{
RETURN_FALSE;
}
RETURN_RES(zend_register_resource(fp,?le_tipi_file));
}
其中RETURN_RES宏的作用是將返回的zend_resource添加到zval中颇玷,然后將最后的zval作為返回值笨农。也就是說該函數(shù)的返回值為zval指針。RETURN_RES(zend_register_resource(fp, le_tipi_file))會(huì)將返回值的value.res設(shè)為fp帖渠,u1.type_info設(shè)為IS_RESOURCE_EX谒亦。大家可以根據(jù)源碼非常直觀的了解到,這里不粘貼代碼詳細(xì)說明了空郊。
1.3 使用資源
1.3.1 使用資源 API
ZEND_APIvoid*zend_fetch_resource(zend_resource?*res,constchar*resource_type_name,intresource_type)
在 PHP 7 中刪除了原有的ZEND_FETCH_RESOURCE宏份招,直接使用函數(shù)zend_fetch_resource,而且解析方式也變得簡單了很多狞甚,想比 PHP 5 要高效很多锁摔,后面我們再通過圖片分析對比。
參數(shù)含義
res資源指針
resource_type_name該類資源的字符串別名
resource_type該類資源的類型 id
1.3.2 解析資源的實(shí)現(xiàn)
當(dāng)我們要實(shí)現(xiàn)文件的讀取時(shí)哼审,最終還是需要使用原生的fread函數(shù)谐腰,所以這里需要通過zend_fetch_resource將zend_resource解析成為該資源包裹的原始的FILE *的指針。
PHP_FUNCTION(file_read)
{
intargc?=?ZEND_NUM_ARGS();
intfilehandle_id?=?-1;
zend_long?size;
zval?*filehandle?=?NULL;
FILE*fp?=?NULL;
char*result;
size_tbytes_read;
if(zend_parse_parameters(argc?TSRMLS_CC,"rl",?&filehandle,?&size)?==?FAILURE)
return;
if((fp?=?(FILE*)zend_fetch_resource(Z_RES_P(filehandle),?TIPI_FILE_TYPE,?le_tipi_file))?==?NULL)?{
RETURN_FALSE;
}
result?=?(char*)?emalloc(size+1);
bytes_read?=fread(result,?1,?size,?fp);
result[bytes_read]?='\0';
RETURN_STRING(result,?0);
}
這里需要說明涩盾,腳本自動(dòng)生成的擴(kuò)展代碼中還是使用ZEND_FETCH_RESOURCE十气, 是個(gè) BUG,因?yàn)樽詣?dòng)生成的腳本(ext/skeleton/create_stubs)還沒更新春霍。
與之類似的文件的寫入操作砸西,也很類似,這里就復(fù)制代碼了终畅,請查看完整的代碼 tipi_file.c(https://github.com/zhoumengkang/notes/blob/master/php-extension/php7.0/tipi_file/tipi_file.c)
1.4 資源的刪除
1.4.1 資源刪除 API
ZEND_APIintzend_list_close(zend_resource?*res)
傳入需要被刪除的資源即可籍胯。該 API 看似非常簡單,實(shí)際做了很多工作离福,后面原理分析細(xì)說杖狼。
1.4.2 資源刪除的實(shí)現(xiàn)
我們在函數(shù)file_close中需要調(diào)用資源刪除 API
PHP_FUNCTION(file_close)
{
intargc?=?ZEND_NUM_ARGS();
intfilehandle_id?=?-1;
zval?*filehandle?=?NULL;
if(zend_parse_parameters(argc?TSRMLS_CC,"r",?&filehandle)?==?FAILURE)
return;
zend_list_close(Z_RES_P(filehandle));
RETURN_TRUE;
}
1.5 編譯安裝以及測試
1.5.1 編譯安裝
通過上面的編碼,一個(gè)簡單的第三方的擴(kuò)展就實(shí)現(xiàn)了妖爷。查看完整版(https://github.com/zhoumengkang/notes/tree/master/php-extension/php7.0/tipi_file)
下面的一些命令配置請根據(jù)自己的環(huán)境而定(安裝的過程可以參考最基礎(chǔ)的擴(kuò)展開發(fā)教程(https://mengkang.net/660.html))
[root@localhost?tipi_file]#?php7ize
Configuringfor:
PHP?Api?Version:?????????20151012
Zend?Module?Api?No:??????20151012
Zend?Extension?Api?No:???320151012
[root@localhost?tipi_file]#?./configure?--with-php-config=/usr/local/php7/bin/php-config
...
[root@localhost?tipi_file]#?make
...
[root@localhost?tipi_file]#?make?install
...
1.5.2 測試
直接用 php 腳本測試蝶涩,就不一個(gè)功能一個(gè)功能寫測試樣例了,修改tipi_file.php文件絮识。
$fp?=?file_open("./CREDITS","r+");
var_dump($fp);
var_dump(file_read($fp,6));
var_dump(file_write($fp,"zhoumengakng"));
var_dump(file_close($fp));
然后通過命令行執(zhí)行
php7?-d"extension=tipi_file.so"tipi_file.php
2 源碼分析
2.1 注冊資源類型源碼
ZEND_API?int?zend_register_list_destructors_ex(rsrc_dtor_func_t?ld,?rsrc_dtor_func_t?pld,?const?char?*type_name,?int?module_number)
{
zend_rsrc_list_dtors_entry?*lde;
zval?zv;
lde?=?malloc(sizeof(zend_rsrc_list_dtors_entry));
lde->list_dtor_ex?=?ld;
lde->plist_dtor_ex?=?pld;
lde->module_number?=?module_number;
lde->resource_id?=?list_destructors.nNextFreeElement;
lde->type_name?=?type_name;
ZVAL_PTR(&zv,?lde);
if(zend_hash_next_index_insert(&list_destructors,?&zv)?==?NULL)?{
returnFAILURE;
}
returnlist_destructors.nNextFreeElement-1;
}
其中
ZVAL_PTR(&zv,?lde);
等價(jià)于
zv.value.ptr?=?(lde);
zv.u1.type_info?=?IS_PTR;
list_destructors是一個(gè)全局靜態(tài)HashTable绿聘,資源類型注冊時(shí),將一個(gè)zval結(jié)構(gòu)體變量zv存放入list_destructors的arData中次舌,而zv的value.ptr卻指向了zend_rsrc_list_dtors_entry *lde熄攘,lde中包含的該種資源釋放函數(shù)指針、持久資源的釋放函數(shù)指針彼念,資源類型名稱挪圾,該資源在 hashtable 中的索引依據(jù) (resource_id)等浅萧。
而這里的resource_id則是該函數(shù)的返回值,所以后面我們在解析該類型變量時(shí)哲思,都需要將resource_id帶上洼畅。
整個(gè)的注冊步驟可以總結(jié)為下圖:
2.2 資源的注冊
ZEND_API?zend_resource*?zend_register_resource(void*rsrc_pointer,intrsrc_type)
{
zval?*zv;
zv?=?zend_list_insert(rsrc_pointer,?rsrc_type);
returnZ_RES_P(zv);
}
該函數(shù)的功能則是將zend_list_insert返回的zval中的資源指針返回。Z_RES_P宏在Zend/zend_types.h中定義棚赔。
重點(diǎn)分析zend_list_insert
ZEND_API?zval?*zend_list_insert(void?*ptr,?inttype)
{
int?index;
zval?zv;
index?=?zend_hash_next_free_element(&EG(regular_list));
if(index?==?0)?{
index?=?1;
}
ZVAL_NEW_RES(&zv,?index,?ptr,type);
returnzend_hash_index_add_new(&EG(regular_list),?index,?&zv);
}
其中zend_hash_next_free_element宏帝簇,返回&EG(regular_list)表的nNextFreeElement,后面用來作為索引查詢的依據(jù)靠益。
而ZVAL_NEW_RES宏是 PHP 7 新增的一套東西丧肴,把一個(gè)資源裝載到zval里去,因?yàn)镻HP 7 中Bucket只能存zval了胧后。
#define?ZVAL_NEW_RES(z,?h,?p,?t)?do?{???????????????????????? \
zend_resource?*_res?=???????????????????????????????? \
(zend_resource?*)?emalloc(sizeof(zend_resource));???? \
zval?*__z;???????????????????????????????????????? \
GC_REFCOUNT(_res)?=?1;??????????????????????????????????? \
GC_TYPE_INFO(_res)?=?IS_RESOURCE;???????????????????? \
_res->handle?=?(h);??????????????????????????????????????? \
_res->type?=?(t);????????????????????????????????????? \
_res->ptr?=?(p);?????????????????????????????????????? \
__z?=?(z);??????????????????????????????????????????? \
Z_RES_P(__z)?=?_res;????????????????????????????????? \
Z_TYPE_INFO_P(__z)?=?IS_RESOURCE_EX;????????????????? \
}while(0)
代碼比較清晰闪湾,首先根據(jù)h,p,t新建了一個(gè)資源,然后一起存入了z這個(gè)zval的結(jié)構(gòu)體绩卤。(最后兩個(gè)宏前面剛剛討論過了)
最后就是zend_hash_index_add_new宏了,追蹤代碼發(fā)現(xiàn)其最后等價(jià)于調(diào)用的是
_zend_hash_index_add_or_update_i(&EG(regular_list),?index,?&zv,?HASH_ADD?|?HASH_ADD_NEW?ZEND_FILE_LINE_RELAY_CC)
關(guān)于HashTable的具體操作江醇,這里暫不做細(xì)致的分析濒憋,后面單獨(dú)再單獨(dú)說。
2.3 解析資源源碼分析
ZEND_APIvoid*zend_fetch_resource(zend_resource?*res,constchar*resource_type_name,intresource_type)
{
if(resource_type?==?res->type)?{
returnres->ptr;
}
if(resource_type_name)?{
constchar*space;
constchar*class_name?=?get_active_class_name(&space);
zend_error(E_WARNING,"%s%s%s():?supplied?resource?is?not?a?valid?%s?resource",?class_name,?space,?get_active_function_name(),?resource_type_name);
}
returnNULL;
}
在上面的例子中我們是這樣解析的
(FILE*)zend_fetch_resource(Z_RES_P(filehandle),?TIPI_FILE_TYPE,?le_tipi_file)
而現(xiàn)在 PHP7的解析則直接從zval里解析出zend_resource陶夜,如下圖所示:
2.4 刪除資源源碼分析
ZEND_APIintzend_list_close(zend_resource?*res)
{
if(GC_REFCOUNT(res)?<=?0)?{
returnzend_list_free(res);
}elseif(res->type?>=?0)?{
zend_resource_dtor(res);
}
returnSUCCESS;
}
與PHP5 不同的地方凛驮,這里不是每次都進(jìn)來將其引用計(jì)數(shù)減一操作,而是直接調(diào)用zend_resource_dtor函數(shù)条辟。
staticvoidzend_resource_dtor(zend_resource?*res)
{
zend_rsrc_list_dtors_entry?*ld;
zend_resource?r?=?*res;
res->type?=?-1;
res->ptr?=?NULL;
ld?=?zend_hash_index_find_ptr(&list_destructors,?r.type);
if(ld)?{
if(ld->list_dtor_ex)?{
ld->list_dtor_ex(&r);
}
}else{
zend_error(E_WARNING,"Unknown?list?entry?type?(%d)",?r.type);
}
}
如果引用計(jì)數(shù)已經(jīng)等于0或者小于0了黔夭,那么才從EG(regular_list)中刪除
ZEND_APIintzend_list_free(zend_resource?*res)
{
if(GC_REFCOUNT(res)?<=?0)?{
returnzend_hash_index_del(&EG(regular_list),?res->handle);
}else{
returnSUCCESS;
}
}
原理圖還是引用上面的注冊資源類型、并注冊資源的圖:
先從zend_resource逆向通過其type在list_destructors中索引層層關(guān)聯(lián)羽嫡,找到該類資源的釋放回調(diào)函數(shù)本姥,然后對該資源執(zhí)行釋放回調(diào)函數(shù)。
而后面的從EG(regular_list)中刪除杭棵,則是通過res->handler做為索引的依據(jù)婚惫。