讲述之前,先描述下 {资源} 类型在内核中的结构:
- //每一个资源都是通过它来实现的。
- typedef struct _zend_rsrc_list_entry {
- void * ptr;
- int type;
- int refcount;
- }
- zend_rsrc_list_entry;
在真实世界中,我们经常需要操作一些不好用标量值表现的数据,比如某个文件的句柄,而对于 C 来说,它也仅仅是个指针而已。
- #include <stdio.h>
- int main(void)
- {
- FILE *fd;
- fd = fopen("/home/jdoe/.plan", "r");
- fclose(fd);
- return 0;
- }
C 语言中 stdio 的文件描述符 (file descriptor) 是与每个打开的文件相匹配的一个变量,它实际上十一个 FILE 类型的指针,它将在程序与硬件交互通讯时使用。我们可以使用 fopen 函数来打开一个文件获取句柄,之后只需把这个句柄传递给 feof()、fread()、fwrite()、fclose()之类的函数,便可以对这个文件进行后续操作了。既然这个数据在 C 语言中就无法直接用标量数据来表示,那我们如何对其进行封装才能保证用户在 PHP 语言中也能使用到它呢?这便是 PHP 中资源类型变量的作用了!所以它也是通过一个 zval 结构来进行封装的。
资源类型的实现并不复杂,它的值其实仅仅是一个整数,内核将根据这个整数值去一个类似资源池的地方寻找最终需要的数据。
资源类型的变量在实现中也是有类型区分的!为了区分不同类型的资源,比如一个是文件句柄,一个是 mysql 链接,我们需要为其赋予不同的分类名称。首先,我们需要先把这个分类添加到程序中去。这一步的操作可以在 MINIT 中来做:
- #define PHP_SAMPLE_DESCRIPTOR_RES_NAME "山寨文件描述符"
- static int le_sample_descriptor;
- ZEND_MINIT_FUNCTION(sample)
- {
- le_sample_descriptor = zend_register_list_destructors_ex(NULL, NULL, PHP_SAMPLE_DESCRIPTOR_RES_NAME,module_number);
- return SUCCESS;
- }
- //附加资料
- #define register_list_destructors(ld, pld) zend_register_list_destructors((void (*)(void *))ld, (void (*)(void *))pld, module_number);
- ZEND_API int zend_register_list_destructors(void (*ld)(void *), void (*pld)(void *), int module_number);
- ZEND_API int zend_register_list_destructors_ex(rsrc_dtor_func_t ld, rsrc_dtor_func_t pld, char *type_name, int module_number);
接下来,我们把定义好的 MINIT 阶段的函数添加到扩展的 module_entry 里去,只需要把原来的 "NULL, /* MINIT */" 一行替换掉即可:
- ZEND_MINIT(sample), /* MINIT */
ZEND_MINIT_FUNCTION() 宏用来帮助我们定义 MINIT 阶段的函数。看到 zend_register_list_destructors_ex() 函数,你肯定回想是不是也存在一个 zend_register_list_destructors() 函数呢?是的,确实有这么一个函数,它的参数中比前者少了资源类别的名称。那这两这的区别在哪呢?
- eaco $re_1;
- //resource(4) of type (山寨版File句柄)
- echo $re_2;
- //resource(4) of type (Unknown)
我们在上面向内核中注册了一种新的资源类型,下一步便可以创建这种类型的资源变量了。接下来让我们简单的重新实现一个 fopen 函数,现在叫 sample_open:
- PHP_FUNCTION(sample_fopen) {
- 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;
- }
- //将fp添加到资源池中去,并标记它为le_sample_descriptor类型的。
- ZEND_REGISTER_RESOURCE(return_value, fp, le_sample_descriptor);
- }
如果前面章节的知识你都看过的话,应该可以猜出最后一行代码是干啥的了。它创建了一个新的 le_sample_descriptor 类型的资源,此资源的值是 fp,另外它把这个资源加入到一个存储资源的 HashTable 中,并把此资源在其中对应的数字 Key 赋给 return_value。
资源并不局限与文件句柄,我们可以申请一块内存,并它指向它的指针来作为一种资源。所以资源可以对应任意类型的数据。
世间万物皆有喜有悲,有生有灭,到了我们探讨如何销毁资源的时候了。最简单的一种莫非于仿照 fclose 写一个 sample_close()函数,在它里面实现对某种 {资源:专指 PHP 的资源类型变量代表的值} 的释放。
但是,如果用户端的脚本通过 unset() 函数来释放某个资源类型的变量会如何呢?它们可不知道它的值最终对应一个 FILE * 指针啊,所以也无法使用 fclose() 函数来释放它,这个 FILE * 句柄很有可能会一直存在于内存中,直到 PHP 程序挂掉,由 OS 来回收。但在一个平常的 web 环境中,我们的服务器都会长时间运行的。
难道就没有解决方案了吗?当然不是,谜底就在那个 NULL 参数里,就是我们在上面为了生成新的资源类型,调用的 zend_register_list_destructors_ex() 函数的第一个参数和第二个参数。这两个参数都各自代表一个回调参数。第一个回调函数会在脚本中的相应类型的资源变量被释放掉的时候触发,比如作用域结束了,或者被 unset() 掉了。
第二个回调函数则是用在一个类似与长链接类型的资源上的,也就是这个资源创建后会一直存在于内存中,而不会在 request 结束后被释放掉。它将会在 Web 服务器进程终止时调用,相当与在 MSHUTDOWN 阶段被内核调用。
我们先来定义第一种回调函数。
- static void php_sample_descriptor_dtor(zend_rsrc_list_entry *rsrc TSRMLS_DC)
- {
- FILE *fp = (FILE*)rsrc->ptr;
- fclose(fp);
- }
然后用它替换掉 zend_register_list_destructors_ex() 函数的第一个参数 NULL:
- le_sample_descriptor = zend_register_list_destructors_ex(
- php_sample_descriptor_dtor,
- NULL,
- PHP_SAMPLE_DESCRIPTOR_RES_NAME,
- module_number);
现在,如果脚本中得到了一个上述类型的资源变量,当它被 unset 的时候,或者因为作用域执行完被内核释放掉的时候都会被内核调用底层的 php_sample_descriptor_dtor 来预处理它。这样一来,貌似我们根本就不需要 sample_close() 函数了!
- <?php
- $fp = sample_fopen("/home/jdoe/notes.txt", "r");
- unset($fp);
- ?>
unset($fp) 执行后,内核会自动的调用 php_sample_descriptor_dtor 函数来清理这个变量对应的一些数据。当然,事情绝对没有这么简单,让我们先记住这个疑问,继续往下看。
我们把资源变量比作书签,可如果仅有书签的话绝对没有任何作用啊!我们需要通过书签找到相应的页才行。对于资源变量,我们必须能够通过它找到相应的最终数据才行!
- ZEND_FUNCTION(sample_fwrite)
- {
- FILE *fp;
- 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();
- }
- /* Use the zval* to verify the resource type and
- * retrieve its pointer from the lookup table */
- ZEND_FETCH_RESOURCE(fp,FILE*,&file_resource,-1,PHP_SAMPLE_DESCRIPTOR_RES_NAME,le_sample_descriptor);
- /* Write the data, and
- * return the number of bytes which were
- * successfully written to the file */
- RETURN_LONG(fwrite(data, 1, data_len, fp));
- }
zend_parse_parameters() 函数中的 r 占位符代表着接收资源类型的变量,它的载体是一个 zval*。然后让我们看一下 ZEND_FETCH_RESOURCE() 宏函数。
- #define ZEND_FETCH_RESOURCE(rsrc, rsrc_type, passed_id,default_id, resource_type_name, resource_type)
- rsrc = (rsrc_type) zend_fetch_resource(passed_id TSRMLS_CC,default_id, resource_type_name, NULL,1, resource_type);
- ZEND_VERIFY_RESOURCE(rsrc);
- //在我们的例子中,它是这样的:
- fp = (FILE*) zend_fetch_resource(&file_descriptor TSRMLS_CC, -1,PHP_SAMPLE_DESCRIPTOR_RES_NAME, NULL,1, le_sample_descriptor);
- if (!fp)
- {
- RETURN_FALSE;
- }
zend_fetch_resource()是对 zend_hash_find()的一层封装,它使用一个数字 key 去一个保存各种 {资源} 的 HashTable 中寻找最终需要的数据,找到之后,我们用 ZEND_VERIFY_RESOURCE()宏函数校验一下这个数据。从上面的代码中我们可以看出,NULL、0 是绝对不能作为一种资源的。
上面的例子中,zend_fetch_resource() 函数首先获取 le_sample_descriptor 代表的资源类型,如果资源不存在或者接收的 zval 不是一个资源类型的变量,它便会返回 NULL,并抛出相应的错误信息。
最后的 ZEND_VERIFY_RESOURCE() 宏函数如果检测到错误,便会自动返回,是我们可以从错误检测中脱离出来,更加专注与程序的主逻辑。现在我们已经获取到了相应的 FILE * 了,下面就用 fwrite() 像其中写入点数据吧!
我们也可以通过另一种方法来获取我们最终想要的数据。
- ZEND_FUNCTION(sample_fwrite) {
- FILE * fp;
- zval * file_resource;
- char * data;
- int data_len,
- rsrc_type;
- if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "rs", &file_resource, &data, &data_len) == FAILURE) {
- RETURN_NULL();
- }
- fp = (FILE * ) zend_list_find(Z_RESVAL_P(file_resource), &rsrc_type);
- if (!fp || rsrc_type != le_sample_descriptor) {
- php_error_docref(NULL TSRMLS_CC, E_WARNING, "Invalid resource provided");
- RETURN_FALSE;
- }
- RETURN_LONG(fwrite(data, 1, data_len, fp));
- }
可以根据自己习惯来选择到底使用哪一种形式,不过推荐使用 ZEND_FETCH_RESOURCE() 宏函数。
在上面我们还有个疑问没有解决,就类似与我们上面实现的 unset($fp) 真的是万能的么?当然不是,看一下下面的代码:
- <?php
- $fp = sample_fopen("/home/jdoe/world_domination.log", "a");
- $evil_log = $fp;
- unset($fp);
- ?>
这次,$fp 和 $evil_log 共用一个 zval,虽然 $fp 被释放了,但是它的 zval 并不会被释放,因为 $evil_log 还在用着。也就是说,现在 $evil_log 代表的文件句柄仍然是可以写入的!所以为了避免这种错误,真的需要我们手动来 close it!sample_close() 函数是必须存在的!
- PHP_FUNCTION(sample_fclose) {
- FILE * fp;
- zval * file_resource;
- if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "r", &file_resource) == FAILURE) {
- RETURN_NULL();
- }
- /* While it's not necessary to actually fetch the
- * FILE* resource, performing the fetch provides
- * an opportunity to verify that we are closing
- * the correct resource type. */
- ZEND_FETCH_RESOURCE(fp, FILE * , &file_resource, -1, PHP_SAMPLE_DESCRIPTOR_RES_NAME, le_sample_descriptor);
- /* Force the resource into self-destruct mode */
- zend_hash_index_del( & EG(regular_list), Z_RESVAL_P(file_resource));
- RETURN_TRUE;
- }
这个删除操作也再次说明了资源数据是保存在 HashTable 中的。虽然我们可以通过 zend_hash_index_find() 或者 zend_hash_next_index_insert() 之类的函数操作这个储存资源的 HashTable,但这绝不是一个好主意,因为在后续的版本中,PHP 可能会修改有关这一部分的实现方式,到那时上述方法便不起作用了,所以为了更好的兼容性,请使用标准的宏函数或者 api 函数。
当我们在 EG(regular_list) 这个 HashTable 中删除数据的时候,回调用一个 dtor 函数,它根据资源变量的类别来调用相应的 dtor 函数实现,就是我们调用 zend_register_list_destructors_ex() 函数时的第一个参数。
来源: http://it.taocms.org/08/4628.htm