php7扩展使用持久化hash
最近项目需要在PHP7的扩展里,维护一个全局的持久化zend_array,在多次请求之间可以共享使用。
在这里简单记录一下实现和原理。
首先是定义一个全局的 zend_array*:
zend_array *ormosia_domain_cache = NULL; 在扩展初始化回调里,分配并初始化一个 zend_array:
ormosia_domain_cache = (zend_array*)pemalloc(sizeof(*ormosia_domain_cache), 1);
zend_hash_init(ormosia_domain_cache, 0, NULL, persistant_zval_dtor, 1);
首先 zend_array自身的内存一定是 pemalloc(size, persistant=1)来创建的持久化内存,相当于malloc而不是emalloc,不会在请求结束后被释放。
之后,调用 zend_hash_init初始化这个array,需要注意的是value的dtor回调函数并不是 zval_ptr_dtor,而是我自己实现的 persistant_zval_dtor函数。
另外,最后一个参数persistant=1,这样 zend_array在内部分配哈希桶等内存时也会使用pemalloc分配持久化内存。
既然要持久化,除了 zend_array本身以外,保存在 zend_array里的zval也一定要持久化内存,包括key是持久化的 zend_string,value是持久化的任意类型zval。
这里就说说,为什么要自定义value的dtor函数,而不用zend API自带的 zval_ptr_dtor,这里截取了它的实现片段:
#define zval_ptr_dtor(zval_ptr) _zval_ptr_dtor((zval_ptr) ZEND_FILE_LINE_CC)
ZEND_API void _zval_ptr_dtor_wrapper(zval *zval_ptr)
{
i_zval_ptr_dtor(zval_ptr ZEND_FILE_LINE_CC);
}
static zend_always_inline void i_zval_ptr_dtor(zval *zval_ptr ZEND_FILE_LINE_DC)
{
if (Z_REFCOUNTED_P(zval_ptr)) {
if (!Z_DELREF_P(zval_ptr)) {
_zval_dtor_func(Z_COUNTED_P(zval_ptr) ZEND_FILE_LINE_RELAY_CC);
} else {
GC_ZVAL_CHECK_POSSIBLE_ROOT(zval_ptr);
}
}
}
ZEND_API void ZEND_FASTCALL _zval_dtor_func(zend_refcounted *p ZEND_FILE_LINE_DC)
{
switch (GC_TYPE(p)) {
case IS_STRING:
case IS_CONSTANT: {
zend_string *str = (zend_string*)p;
CHECK_ZVAL_STRING_REL(str);
zend_string_free(str);
break;
}
case IS_ARRAY: {
zend_array *arr = (zend_array*)p;
zend_array_destroy(arr);
break;
}
重点关注最后一个实现函数,当 zend_array里的某个value引用计数为0的时候将被调用。对于string类型来说, zend_string_free的内部实现其实判断了 zend_string是否为持久化内存:
static zend_always_inline void zend_string_free(zend_string *s)
{
if (!ZSTR_IS_INTERNED(s)) {
ZEND_ASSERT(GC_REFCOUNT(s) <= 1);
pefree(s, GC_FLAGS(s) & IS_STR_PERSISTENT);
}
}
可见 zend_string里的gc字段保存了 IS_STR_PERSISTANT标记,这是 zend_string_init时最后一个参数控制的,所以它通过pefree可以正确的根据内存类型进行相应的释放。
问题就出在array类型, zend_array_destroy内部释放哈希桶的内存使用的是efree而不是pefree:
ZEND_API void ZEND_FASTCALL zend_array_destroy(HashTable *ht)
{
...
efree(HT_GET_DATA_ADDR(ht));
free_ht:
FREE_HASHTABLE(ht);
}
不仅是array类型,其实reference类型也是写死了efree的:
case IS_REFERENCE: {
zend_reference *ref = (zend_reference*)p;
i_zval_ptr_dtor(&ref->val ZEND_FILE_LINE_RELAY_CC);
efree_size(ref, sizeof(zend_reference));
break;
}
所以说, zval_dtor_ptr并不能直接用于持久化 zend_array的value析构函数。
因为在我的业务场景中, zend_array保存的value只有string和array两种类型,并且嵌套的array也是保存的string或array类型,所以我的dtor函数只覆盖了所需的类型:
// 持久化哈希的析构函数
static void persistant_zval_dtor(zval *zval_ptr) {
if (Z_REFCOUNTED_P(zval_ptr)) {
if (!Z_DELREF_P(zval_ptr)) {
switch (Z_TYPE_P(zval_ptr)) {
case IS_STRING:
zend_string_free(zval_ptr->value.str);
break;
case IS_ARRAY:
zend_hash_destroy(zval_ptr->value.arr);
pefree(zval_ptr->value.arr, 1);
break;
default:
break;
}
} else {
// 回收循环引用, 这里不存在这种情况
// GC_ZVAL_CHECK_POSSIBLE_ROOT(zval_ptr);
}
}
}
这个函数基本参照了 zval_dtor_ptr,先减少1个引用计数,如果减少为0就进行资源释放,对于string直接调用对应的api,而对于array则调用另一个api叫做 zend_hash_destroy,它内部会区分内存的类型进行释放:
ZEND_API void ZEND_FASTCALL zend_hash_destroy(HashTable *ht)
{
...
} else if (EXPECTED(!(ht->u.flags & HASH_FLAG_INITIALIZED))) {
return;
}
pefree(HT_GET_DATA_ADDR(ht), ht->u.flags & HASH_FLAG_PERSISTENT);
}
和 zend_string原理类似,持久化的 zend_array会有所标记,从而控制pefree的释放行为。
zend_hash_destroy只会将桶内所有key和value进行dtor析构,然后释放哈希桶内存,并不会释放zend_array结构自身的内存,所以我接着调用了pefree释放它自身。
那么,代码中在else部分提到的”回收循环引用”是什么意思呢?为什么我注释掉了呢?
所谓”循环引用”,是指这样的一个例子:
我有一个 zend_array的zval1,我拥有唯一的引用计数=1。
接着,指定 key=”myself”,value就是zval1自身,将其zendhashupdate保存到zval1内,按照规矩我会为value增加1个引用计数,这样才算将value托付给了 zend_array,所以将导致zval1的引用计数为2。
某个时刻,我们不再想访问zval1,所以释放1个引用计数,结果还剩下1个计数,并没有触发 zend_hash_destroy的调用,这个zval1将永远没有机会被彻底释放。
究其原因,就是因为zval1保存了zval1,导致循环引用,GC垃圾回收无法生效。
上面这段C操作,对应到PHP里就是这样的代码:
<?php
$a = [];
$a[0] = $a;
unset($a);
难道这样的代码,PHP的GC就无能为力了吗?显然不是。else里的注释的代码,其实就是用来针对这种情况的,而这种情况只能出现在zval1的类型是array或者object的情况下,因为只有它们内部才能保存其他变量,从而导致出现循环引用。
至于else部分的代码是如何搞定循环引用的,你可以参考这篇博客: GC垃圾回收 。
原理并不算复杂,当我们的dtor函数发现减少1个引用计数后仍旧不为0的情况下,就会检测这是否是因为循环引用引起,所以进入检测函数 GC_ZVAL_CHECK_POSSIBLE_ROOT。
检测的大概原理是:在我们的例子中,既然剩余的1个引用计数是来自内部(子级)保存的自身,那么就深度遍历(因为孩子可能又循环引用了任意父级)它的孩子,将路过的zval的引用计数减1,如果在遍历的回溯路径上某个zval的引用计数减少为0,说明它的某个孩子引用了自己,现在可以释放它。
最后 在扩展退出前,记得释放一下持久化的zend_array:
zend_hash_destroy(ormosia_domain_cache);
zend_hash_destroy(ormosia_keys_cache);
pefree(ormosia_domain_cache, 1);
pefree(ormosia_keys_cache, 1);
- 《绝地求生》:霰弹枪正确使用方法
- 农民注意:农村使用自家耕地建房算违法吗?农民务必知道!
- 建行手机银行中信用卡预审批额度是怎么回事?如何使用?
- 家里有电磁炉的临泉人快看,千万别这样使用!
- 叙利亚媒体称土耳其在叙使用毒气弹,但国际社会恐难对土施压
- 17岁健身小伙连续使用类固醇6年,练的肌肉竟是为了讨女生欢心?
- 印度的银行禁止在加密货币购买中使用借记卡和信用卡
- 工行信用卡申请不下来,可以使用刷星的方法来帮助下卡
- 王者荣耀:六款皮肤值得兑换,大量欧皇使用抵用券抽中雅典娜皮肤
- 西班牙为区块链公司制定减税计划 菲律宾银行计划使用Visa区块链