php7扩展使用持久化hash

最近项目需要在PHP7的扩展里,维护一个全局的持久化zend_array,在多次请求之间可以共享使用。

在这里简单记录一下实现和原理。


首先是定义一个全局的zend_array*:

在扩展初始化回调里,分配并初始化一个zend_array:

首先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,这里截取了它的实现片段:

重点关注最后一个实现函数,当zend_array里的某个value引用计数为0的时候将被调用。对于string类型来说,zend_string_free的内部实现其实判断了zend_string是否为持久化内存:

可见zend_string里的gc字段保存了IS_STR_PERSISTANT标记,这是zend_string_init时最后一个参数控制的,所以它通过pefree可以正确的根据内存类型进行相应的释放。

问题就出在array类型,zend_array_destroy内部释放哈希桶的内存使用的是efree而不是pefree:

不仅是array类型,其实reference类型也是写死了efree的:

所以说,zval_dtor_ptr并不能直接用于持久化zend_array的value析构函数。

因为在我的业务场景中,zend_array保存的value只有string和array两种类型,并且嵌套的array也是保存的string或array类型,所以我的dtor函数只覆盖了所需的类型:

这个函数基本参照了zval_dtor_ptr,先减少1个引用计数,如果减少为0就进行资源释放,对于string直接调用对应的api,而对于array则调用另一个api叫做zend_hash_destroy,它内部会区分内存的类型进行释放:

和zend_string原理类似,持久化的zend_array会有所标记,从而控制pefree的释放行为。

zend_hash_destroy只会将桶内所有key和value进行dtor析构,然后释放哈希桶内存,并不会释放zend_array结构自身的内存,所以我接着调用了pefree释放它自身。


那么,代码中在else部分提到的”回收循环引用”是什么意思呢?为什么我注释掉了呢?

所谓”循环引用”,是指这样的一个例子:

我有一个zend_array的zval1,我拥有唯一的引用计数=1。

接着,指定key=”myself”,value就是zval1自身,将其zend_hash_update保存到zval1内,按照规矩我会为value增加1个引用计数,这样才算将value托付给了zend_array,所以将导致zval1的引用计数为2。

某个时刻,我们不再想访问zval1,所以释放1个引用计数,结果还剩下1个计数,并没有触发zend_hash_destroy的调用,这个zval1将永远没有机会被彻底释放。

究其原因,就是因为zval1保存了zval1,导致循环引用,GC垃圾回收无法生效。

上面这段C操作,对应到PHP里就是这样的代码:

难道这样的代码,PHP的GC就无能为力了吗?显然不是。else里的注释的代码,其实就是用来针对这种情况的,而这种情况只能出现在zval1的类型是array或者object的情况下,因为只有它们内部才能保存其他变量,从而导致出现循环引用。

至于else部分的代码是如何搞定循环引用的,你可以参考这篇博客:GC垃圾回收

原理并不算复杂,当我们的dtor函数发现减少1个引用计数后仍旧不为0的情况下,就会检测这是否是因为循环引用引起,所以进入检测函数GC_ZVAL_CHECK_POSSIBLE_ROOT。

检测的大概原理是:在我们的例子中,既然剩余的1个引用计数是来自内部(子级)保存的自身,那么就深度遍历(因为孩子可能又循环引用了任意父级)它的孩子,将路过的zval的引用计数减1,如果在遍历的回溯路径上某个zval的引用计数减少为0,说明它的某个孩子引用了自己,现在可以释放它。

最后

在扩展退出前,记得释放一下持久化的zend_array:

 

 

 

发表评论

电子邮件地址不会被公开。