🌚

PHP资源的实现和操作

Posted at — May 05, 2015
#PHP #源码

存储结构

资源变量也是一个zval结构,zval->type == IS_RESOURCE,zval->value->lval存储一个整数,此整数为资源数据在存储资源的哈希表中的索引。

资源数据的结构为:

1
2
3
4
5
6
typedef struct _zend_rsrc_list_entry
{
    void *ptr;
    int type;
    int refcount;
}zend_rsrc_list_entry;

常规资源与持久资源

有两个存储资源数据的哈希表。EG(regular_list)存储常规资源,EG(persistent_list)存储持久资源。

常规资源对应的变量在作用域结束后会被内核回收,对应的资源数据也会被销毁。持久资源可以保持并被多次请求使用。持久资源的自动析构发生在PHP进程退出时。

实现

重新实现基本的文件句柄和相关操作。

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
// 资源名称
#define PHP_DONIE_RES_NAME_FILE "Donie's File Descriptor"

// 资源类型
static int le_donie_file_descriptor;
static int le_donie_file_descriptor_persist;

// 资源析构函数
static void php_donie_file_descriptor_dtor(zend_rsrc_list_entry *rsrc TSRMLS_CC)
{
	FILE *fp = (FILE*)rsrc->ptr;
	fclose(fp);
}

// 在扩展的MINIT方法里创建资源类型
PHP_MINIT_FUNCTION(donie)
{
	/* create a new resource type */
	le_donie_file_descriptor = zend_register_list_destructors_ex(
		php_donie_file_descriptor_dtor, NULL, PHP_DONIE_RES_NAME_FILE, module_number
	);

	/* create a persistent resource type */
	le_donie_file_descriptor_persist = zend_register_list_destructors_ex(
		NULL, php_donie_file_descriptor_dtor, PHP_DONIE_RES_NAME_FILE, module_number
	);

	return SUCCESS;
}

// 文件打开操作
PHP_FUNCTION(donie_fopen)
{
	FILE *fp;
	char *filename, *mode;
	int filename_len, mode_len;
	zend_bool persist = 0;
	char *hash_key;
	int hash_key_len;
	list_entry *persist_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 file name or mode.");
		RETURN_FALSE;
	}

	/* reuse persistent resource if exists */
	hash_key_len = spprintf(&hash_key, 0, "php_donie_file_descriptor:%s-%s", filename, mode);
	if (zend_hash_find(&EG(persistent_list), hash_key, hash_key_len+1, (void **)&persist_file) == SUCCESS)
	{
		ZEND_REGISTER_RESOURCE(return_value, persist_file->ptr, le_donie_file_descriptor_persist);
		efree(hash_key);
		return;
	}

	fp = fopen(filename, mode);
	if (!fp)
	{
		php_error_docref(NULL TSRMLS_CC, E_WARNING, "Failed opening %s with mode %s.", filename, mode);
		RETURN_FALSE;
	}

	/* this is the key point for registering resources */
	if (persist)
	{
		ZEND_REGISTER_RESOURCE(return_value, fp, le_donie_file_descriptor_persist);
		list_entry le;
		le.type = le_donie_file_descriptor_persist;
		le.ptr = fp;
		zend_hash_update(&EG(persistent_list), hash_key, hash_key_len+1, (void*)&le, sizeof(list_entry), NULL);
	}
	else
	{
		ZEND_REGISTER_RESOURCE(return_value, fp, le_donie_file_descriptor);
	}
	efree(hash_key);
}

// 文件写操作
PHP_FUNCTION(donie_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();
	}

	ZEND_FETCH_RESOURCE2(fp, FILE*, &file_resource, -1, PHP_DONIE_RES_NAME_FILE, le_donie_file_descriptor, le_donie_file_descriptor_persist);
	RETURN_LONG(fwrite(data, 1, data_len, fp));
}

// 文件关闭操作
PHP_FUNCTION(donie_fclose)
{
	FILE *fp;
	zval *file_resource;

	if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "r", &file_resource) == FAILURE)
	{
		RETURN_NULL();
	}

	ZEND_FETCH_RESOURCE2(fp, FILE*, &file_resource, -1, PHP_DONIE_RES_NAME_FILE, le_donie_file_descriptor, le_donie_file_descriptor_persist);
	zend_hash_index_del(&EG(regular_list), Z_RESVAL_P(file_resource));
	RETURN_TRUE;
}

创建新资源类型

zend_register_list_destructors_ex()创建新资源类型,并注册该资源类型的析构函数、资源名称。第一个参数是常规资源的析构函数,第二个是持久资源的析构函数,此处创建的是常规资源类型,故第二个参数不指定。

注册资源

宏函数ZEND_REGISTER_RESOURCE()注册新生成的资源到EG(regular_list),并保存资源的索引到zval->value->lval中。

双重引用计数

资源变量zval中存在一个引用计数,资源数据zend_rsrc_list_entry中也存在一个。前者遵循与其它变量一致的计数原则,后者取决于资源数据被几个资源变量zval引用。

例如对于以下场景:

1
2
3
4
5
<?php
$a = donie_fopen('/tmp/donie.txt', 'r');
$b = $a;
$c = &$a;
?>

a赋值给b时,zval的引用计数加一。a的引用赋值给c时,发生zval的拆分,b获得新的zval,引用计数是1,a和c共用一个zval,引用计数是2。此时,资源数据的引用计数加一。

获取资源

ZEND_FETCH_RESOURCE()根据资源变量zval取出资源数据的ptr并验证资源类型。ZEND_FETCH_RESOURCE2()可以同时指定两个资源类型,任一类型匹配成功都可以。

销毁资源

根据上述二重计数原则,只有当资源数据的引用计数为0时,资源的析构函数才会被调用,而销毁资源变量不一定能销毁资源,所以需要手工强制销毁资源。

zend_hash_index_del()从EG(regular_list)中删除资源时,该资源类型注册的dtor会被自动调用,从而析构资源。

持久资源

存储

EG(persistent_list)是个用字符串索引的哈希表。需要自行定义键的命名规则,做到全局唯一。

创建持久资源类型

zend_register_list_destructors_ex()注册资源类型时,将析构函数指定为第二个参数,第一个参数为NULL。析构持久资源时,会自动调用该函数。

注册持久资源

EG(persistent_list)中的资源数据并不被直接使用,对资源的操作仍然使用EG(regular_list)。故在注册持久资源时,两个哈希表中都需要保存一份。

往EG(persistent_list)中存资源数据:

1
2
3
4
5
6
7
char *hash_key;
int hash_key_len;
zend_rsrc_list_entry le;
le.type = le_donie_file_descriptor_persist;
le.ptr = fp;
hash_key_len = spprintf(&hash_key, 0, "php_donie_file_descriptor:%s-%s", filename, mode);
zend_hash_update(&EG(persistent_list), hash_key, hash_key_len+1, (void*)&le, sizeof(list_entry), NULL);

获取持久资源

对持久资源的常规操作和操作常规资源一样,仍使用EG(regular_list),因为变量zval中存储的是EG(regular_list)中的索引。所以需要先在EG(persistent_list)中查询,若资源存在,先注册到EG(regular_list)中,再进行后续操作。

手动析构持久资源

用zend_hash_del()从EG(persistent_list)中删除资源数据即可自动触发析构函数。