内容简介:[ 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"));
运行结果如下:

现在,是时候引入持久资源了!
延迟析构
在前面我们删除一个资源的时候,其实是去 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);
话都说到这份上了,你肯定知道四种、五种、更多种类型的应该怎么调用了。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:- hadoop-yarn、内核资源隔离
- [ PHP 内核与扩展开发系列] PHP 中的资源类型:复合数据类型 —— 资源
- 内核必须懂(六): 使用kgdb调试内核
- 如何扩展AngularJS资源($资源)的构造函数?
- Linux内核如何替换内核函数并调用原始函数
- Linux内核工程师是怎么步入内核殿堂的?
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Paradigms of Artificial Intelligence Programming
Peter Norvig / Morgan Kaufmann / 1991-10-01 / USD 77.95
Paradigms of AI Programming is the first text to teach advanced Common Lisp techniques in the context of building major AI systems. By reconstructing authentic, complex AI programs using state-of-the-......一起来看看 《Paradigms of Artificial Intelligence Programming》 这本书的介绍吧!
XML、JSON 在线转换
在线XML、JSON转换工具
正则表达式在线测试
正则表达式在线测试