Skip to content

Latest commit

 

History

History
350 lines (303 loc) · 14.9 KB

9.2.md

File metadata and controls

350 lines (303 loc) · 14.9 KB

9.2 PHP中的资源类型

通常情况下,像{资源}这类复合类型的数据都会占用大量的硬件资源,比如内存、CPU以及网络带宽。对于使用频率超级高的数据库链接,我们可以获取一个长链接,使其不会在脚本结束后自动销毁,一旦创建便可以在各个请求中直接使用,从而减少每次创建它的消耗。Mysql的长链接在PHP内核中其实就是一种Persistent Resources。 Memory Allocation 前面的章节里我们接触了emalloc()之类的以e开头的内存管理函数,通过它们申请的内存都会在被内核自动的进行垃圾回收的操作。而对于一个Persistent Resources来说,我们是绝对不希望它在脚本结束后被回收的。

假设我们需要在我们的{资源}中同时保存文件名和文件句柄两个数据,现在我们就需要自己定义个结构了:

typedef struct _php_sample_descriptor_data
{
    char *filename;
    FILE *fp;
}php_sample_descriptor_data;

当然,因为结构变了(之前是个FILE*),我们之前的代码也需要跟着改动。这里还没有涉及到Persistent Resources,仅仅是换了一种{资源}结构

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));
}
我们这里没有重写sample_fclose()函数,你可以尝试着自己实现它。
现在编译运行,所以的代码的结果都非常正确,我们还可以在内核中获取每个{资源}对应的文件名称了。 ````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)存储着所有的只用在当前请求的{资源}。

Persistent resources,存储在另一个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。
在底层的实现中,persistent和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扩展中引进了一种新的{资源},所以我们需要改写一下我们上面的函数,尽量使用户使用时感觉不到这种差异。

//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(data->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);
    }
}

在Persistent Resources时,因为我们在EG(regular_list)中也保存了一份,所以脚本中我们资源类型的变量在实现中仍然是保存着一个resource ID,我们可以用它来进行之前章节所做的工作。 将其添加到EG(persistent_list)中时,我们进行的操作流程几乎和ZEND_REGISTER_RESOURCE()宏函数一样,唯一的不同便是索引由之前的数字类型换成了字符串类型。 当一个保存在EG(regular_list)中的Persistent Resources被脚本释放时,内核会在EG(regular_list)寻找它对应的dtor函数,但它找到的是NULL,因为我们在使用zend_register_list_destructors_ex()函数声明这种资源类型时,第一个参数的值为NULL。所以此时这个{资源}不会被任何dtor函数调用,可以继续存在于内存中,任脚本流逝,请求更迭。 当web server的进程执行完毕后,内核会扫描EG(persistent_list)的dtor,并调用我们已经定义好的释放函数。在我们定义的释放函数中,一定要记得使用pfree函数来释放内存,而不是efree。

Reuse

创建Persistent Resources的目的是为了使用它,而不是让它来浪费内存的,我们再次重写一下sample_open()函数,这一次我们将检测需要创建的资源是否已经在persistent_list中存在了。

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(data->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来保存Persistent Resources,所以我们在为{资源}的索引起名时,一定要唯一,同时必须简单,方面我们在其它的函数中构造出来。

Liveness Checking and Early Departure

一旦我们打开一个本地文件,便可以一直占有它的操作句柄,保证随时可以打开它。但是对于一些存在于远程计算机上的资源,比如mysql链接、http链接,虽然我们仍然握着与服务器的链接,但是这个链接在服务器端可能已经被关闭了,在本地我们就无法再用它来做一些有价值的工作了。

所以,当我们使用{资源},尤其是Persistent Resources时,一定要保证获取出来的{资源}仍然是有效的、可以使用的。如果它失效了,我们必须将其从persistent list中移除。先面就是一个检测socket有效性的例子:

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

现在我们已经可以创建资源类型并生成新的资源,还能将Persistent Resources与平常{资源}使用的差异性封装起来。但是如果用户对一个Persistent Resources调用sample_fwrite()时候并不会正常工作,先想一下内核是如何通过一个数字所以在regular_list中获取最终资源的。

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()宏函数的唯一区别就是它可以接收两种类型参数。

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

//一种类型的
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()宏函数的实现么?

//两种类型的
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);

再给力一些,三种类型的:

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

LastModified

  • $Id$