资讯详情

八十二、PHP内核探索:资源resource类型 ☞ 资源类型的实现并不复杂

在描述之前,首先描述{资源}类型内核中的结构:

 ///通过它实现每一个资源。  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)它实际上是11个与每个打开的文件相匹配的变量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函数来清理这个变量对应的数据。当然,事情绝对没那么简单。让我们先记住这个问题,继续往下看。

Decoding Resources

我们把资源变量比作书签,但如果只有书签,它绝对没有效果!我们需要通过书签找到相应的页面。对于资源变量,我们必须能够找到相应的最终数据!

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 ofbytes 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()宏函数。

Forcing Destruction

在上面我们还有个疑问没有解决,就类似与我们上面实现的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()函数时的第一个参数。

标签: eaco电容900v600uf

锐单商城拥有海量元器件数据手册IC替代型号,打造 电子元器件IC百科大全!

锐单商城 - 一站式电子元器件采购平台