[ PHP 内核与扩展开发系列] PHP 中的资源类型:持久资源

栏目: PHP · 发布时间: 7年前

内容简介:[ PHP 内核与扩展开发系列] PHP 中的资源类型:持久资源

通常情况下,像资源这类复合类型的数据都会占用大量的硬件资源,比如内存、CPU以及网络带宽。对于使用频率超级高的数据库连接,我们可以获取一个长连接,使其不会在脚本结束后自动销毁,一旦创建便可以在各个请求中直接使用,从而减少每次创建它的消耗。MySQL 的长连接在 PHP 内核中其实就是一种持久资源。

内存分配

前面的章节里我们接触了 emalloc() 之类的以 e 开头的内存管理函数,通过它们申请的内存都会被内核自动的进行垃圾回收的操作。而对于一个持久资源来说,我们是绝对不希望它在脚本结束后被回收的。

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

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

当然,因为结构变了,我们之前的代码也需要跟着改动。这里还没有涉及到持久资源,仅仅是换了一种资源结构

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);
}

ZEND_FUNCTION(academy_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, academy_sample_descriptor);
}

ZEND_FUNCTION(academy_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, academy_sample_descriptor);
    RETURN_LONG(fwrite(data, 1, data_len, fdata->fp));
}

接下来我们来重写 academy_sample_fclose() 函数:

ZEND_FUNCTION(academy_sample_fclose)
{
    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, academy_sample_descriptor);
    zend_hash_index_del(&EG(regular_list), Z_RESVAL_P(file_resource));
    RETURN_TRUE;
}

我们还可以在内核中获取每个资源对应的文件名称了:

ZEND_FUNCTION(academy_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, academy_sample_descriptor); 
    RETURN_STRING(fdata->filename, 1); 
}

现在编译运行,所有代码的结果都非常正确:

<?php
$fp = academy_sample_fopen("/tmp/test", "a");
academy_sample_fwrite($fp, "laravel academy");
var_dump(academy_sample_fname($fp));
academy_sample_fclose($fp);
var_dump(file_get_contents("/tmp/test"));

运行结果如下:

[ PHP 内核与扩展开发系列] PHP 中的资源类型:持久资源

现在,是时候引入持久资源了!

延迟析构

在前面我们删除一个资源的时候,其实是去 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

在底层的实现中,持久的和正常的资源是分别在不同的地方存储的,也分别拥有各自不同的释放函数。但在我们为脚本提供的函数中,却希望能够封装这种差异,从而使我们的用户使用起来更加方便快捷:

static int academy_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);
}

ZEND_MINIT_FUNCTION(academy_sample_resource)
{
    academy_sample_descriptor = zend_register_list_destructors_ex(php_sample_descriptor_dtor, NULL, PHP_SAMPLE_DESCRIPTOR_RES_NAME, module_number);
    academy_sample_descriptor_persist = zend_register_list_destructors_ex(NULL, php_sample_descriptor_dtor_persistent, PHP_SAMPLE_DESCRIPTOR_RES_NAME, module_number);
    return SUCCESS;
}

我们并没有为这两种资源起不同的名字,以防使用户产生疑惑。现在我们的 PHP 扩展中引进了一种新的资源,所以我们需要改写一下上面的函数,尽量使用户使用时感觉不到这种差异。

//academy_sample_fopen()
PHP_FUNCTION(academy_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, academy_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, academy_sample_descriptor_persist);

        //在EG(persistent_list)中再存一份
        le.type = academy_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*)≤, sizeof(list_entry), NULL);
        efree(hash_key);
    }
}

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

资源 复用

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

PHP_FUNCTION(academy_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, academy_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, academy_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, academy_sample_descriptor_persist);

        /* 在 persistent_list 中存一份*/
        le.type = academy_sample_descriptor_persist;
        le.ptr = fdata;

        //hash_key在上面已经被创建了
        zend_hash_update(&EG(persistent_list), hash_key, hash_key_len + 1,(void*)≤, sizeof(list_entry), NULL);
    }
    efree(hash_key);
}

因为所有的 PHP 扩展都共用同一个 HashTable 来保存持久资源,所以我们在为资源的索引起名时,一定要唯一,同时必须简单,方便我们在其它的函数中构造出来。

有效性检测

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

所以,当我们使用资源,尤其是持久资源时,一定要保证获取出来的资源仍然是有效的、可以使用的。如果它失效了,我们必须将其从 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, academy_sample_socket);
        return;
    }
    zend_hash_del(&EG(persistent_list), hash_key, hash_key_len + 1);
}

如你所见,资源失效后,我们只要把它从 HashTable 中删除就行了,这一步操作同样会激活我们设置的回调函数。

获取更多资源类型

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

ZEND_FETCH_RESOURCE(
    fdata,
    php_sample_descriptor_data*,
    &file_resource,
    -1,
    PHP_SAMPLE_DESCRIPTOR_RES_NAME,
    academy_sample_descriptor
);

academy_sample_descriptor 可以保证你获取到的资源确实是这种类型的,绝不会出现你想要一个文件句柄,却返回给你一个 MySQL 连接的情况。这种验证是必须的,但有时你又想绕过这种验证,因为我们放在 persistenst_list 中的资源是 academy_sample_descruotor_persist 类型的,所以当我们把它复制到 regular_list 中时,它也是 academy_sample_descructor_persist 类型的,所以如果我们想获取它,貌似只有两种方法,要么修改类型,要么再写一个新的 sample_write_persistent 函数的实现。或者极端一些,在 academy_sample_write 函数里进行复杂的判断。但是如果 academy_sample_write() 函数能同时接收它们两种类型的资源多好啊!

事情没有这么复杂,我们确实可以在 academy_sample_write() 函数里获取资源时同时指定两种类型。那就是使用 ZEND_FETCH_RESOURCE2() 宏函数,它与 ZEND_FETCH_RESOURCE() 宏函数的唯一区别就是它可以接收两种类型参数:

ZEND_FETCH_RESOURCE2(
    fdata,
    php_sample_descriptor_data*,
    &file_resource,
    -1,
    PHP_SAMPLE_DESCRIPTOR_RES_NAME,
    academy_sample_descriptor,
    academy_sample_descriptor_persist
);

现在,只要资源 ID 对应的最终资源类型是 persistent 或者 non-persistent 中的一种便可以正常通过验证了。

什么,你想设置三种甚至更多的类型?那你只能直接使用 zend_fetch_resource() 函数了:

// 一种类型的
fp = (FILE*) zend_fetch_resource(
    &file_descriptor TSRMLS_CC, 
        -1,
        PHP_SAMPLE_DESCRIPTOR_RES_NAME, 
        NULL,
        1, 
        academy_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, 
        academy_sample_descriptor, 
        academy_sample_descriptor_persist
);
ZEND_VERIFY_RESOURCE(fp);

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

fp = (FILE*) zend_fetch_resource(
    &file_descriptor TSRMLS_CC, 
    -1,
    PHP_SAMPLE_DESCRIPTOR_RES_NAME, 
    NULL,
    3, 
    academy_sample_descriptor, 
    academy_sample_descriptor_persist,
    academy_sample_othertype
);
ZEND_VERIFY_RESOURCE(fp);

话都说到这份上了,你肯定知道四种、五种、更多种类型的应该怎么调用了。


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

结构化计算机组成

结构化计算机组成

Andrew S.Tanenbaum / 刘卫东 / 机械工业出版社 / 2001-10-1 / 46.00

AndrewcS.Tanenbaum获得过美国麻省理工学院的理学学士学位和加利福尼亚大学伯克利分校的哲学博士学位,目前是荷兰阿姆斯特丹Vrije大学计算机科学系的教授,并领导着一个计算机系统的研究小组.同时,他还是一所计算与图像处理学院的院长,这是由几所大学合作成立的研究生院.尽管社会工作很多,但他并没有中断学术研究. 多年来,他在编译技术.操作系统.网络及局域分布式系统方面进行了大量的一起来看看 《结构化计算机组成》 这本书的介绍吧!

RGB转16进制工具
RGB转16进制工具

RGB HEX 互转工具

MD5 加密
MD5 加密

MD5 加密工具

UNIX 时间戳转换
UNIX 时间戳转换

UNIX 时间戳转换