php7扩展使用持久化hash

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

  • php7扩展使用持久化hash