代码技巧

我们先来探讨一下什么样的代码才是优秀的代码? 稳定可靠、可维护且简洁、高效、简短、共享性、可测试性、可移植性...

php7扩展使用持久化hash

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

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

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

zend_array *ormosia_domain_cache = NULL; 在扩展初始化回调里,分配并初始化一个 zend_array

  1. ormosia_domain_cache = (zend_array*)pemalloc(sizeof(*ormosia_domain_cache), 1);

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

  1. #define zval_ptr_dtor(zval_ptr) _zval_ptr_dtor((zval_ptr) ZEND_FILE_LINE_CC)

  2. ZEND_API void _zval_ptr_dtor_wrapper(zval *zval_ptr)

  3. {

  4. i_zval_ptr_dtor(zval_ptr ZEND_FILE_LINE_CC);

  5. }

  6. static zend_always_inline void i_zval_ptr_dtor(zval *zval_ptr ZEND_FILE_LINE_DC)

  7. {

  8. if (Z_REFCOUNTED_P(zval_ptr)) {

  9. if (!Z_DELREF_P(zval_ptr)) {

  10. _zval_dtor_func(Z_COUNTED_P(zval_ptr) ZEND_FILE_LINE_RELAY_CC);

  11. } else {

  12. GC_ZVAL_CHECK_POSSIBLE_ROOT(zval_ptr);

  13. }

  14. }

  15. }

  16. ZEND_API void ZEND_FASTCALL _zval_dtor_func(zend_refcounted *p ZEND_FILE_LINE_DC)

  17. {

  18.        switch (GC_TYPE(p)) {

  19.                case IS_STRING:

  20.                case IS_CONSTANT: {

  21.                                zend_string *str = (zend_string*)p;

  22.                                CHECK_ZVAL_STRING_REL(str);

  23.                                zend_string_free(str);

  24.                                break;

  25.                        }

  26.                case IS_ARRAY: {

  27.                                zend_array *arr = (zend_array*)p;

  28.                                zend_array_destroy(arr);

  29.                                break;

  30.                        }

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

  1. static zend_always_inline void zend_string_free(zend_string *s)

  2. {

  3.        if (!ZSTR_IS_INTERNED(s)) {

  4.                ZEND_ASSERT(GC_REFCOUNT(s) <= 1);

  5.                pefree(s, GC_FLAGS(s) & IS_STR_PERSISTENT);

  6.        }

  7. }

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

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

  1. ZEND_API void ZEND_FASTCALL zend_array_destroy(HashTable *ht)

  2. {

  3.   ...

  4.        efree(HT_GET_DATA_ADDR(ht));

  5. free_ht:

  6.        FREE_HASHTABLE(ht);

  7. }

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

  1.                case IS_REFERENCE: {

  2.                                zend_reference *ref = (zend_reference*)p;

  3.                                i_zval_ptr_dtor(&ref->val ZEND_FILE_LINE_RELAY_CC);

  4.                                efree_size(ref, sizeof(zend_reference));

  5.                                break;

  6.                        }

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

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

  1. // 持久化哈希的析构函数

  2. static void persistant_zval_dtor(zval *zval_ptr) {

  3.    if (Z_REFCOUNTED_P(zval_ptr)) {

  4.        if (!Z_DELREF_P(zval_ptr)) {

  5.            switch (Z_TYPE_P(zval_ptr)) {

  6.            case IS_STRING:

  7.                zend_string_free(zval_ptr->value.str);

  8.                break;

  9.            case IS_ARRAY:

  10.                zend_hash_destroy(zval_ptr->value.arr);

  11.                pefree(zval_ptr->value.arr, 1);

  12.                break;

  13.            default:

  14.                break;

  15.            }

  16.        } else {

  17.            // 回收循环引用, 这里不存在这种情况

  18.            // GC_ZVAL_CHECK_POSSIBLE_ROOT(zval_ptr);

  19.        }

  20.    }

  21. }

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

  1. ZEND_API void ZEND_FASTCALL zend_hash_destroy(HashTable *ht)

  2. {

  3.   ...

  4.        } else if (EXPECTED(!(ht->u.flags & HASH_FLAG_INITIALIZED))) {

  5.                return;

  6.        }

  7.        pefree(HT_GET_DATA_ADDR(ht), ht->u.flags & HASH_FLAG_PERSISTENT);

  8. }

和 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里就是这样的代码:

  1. <?php

  2. $a = [];

  3. $a[0] = $a;

  4. 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:

  1.    zend_hash_destroy(ormosia_domain_cache);

  2.    zend_hash_destroy(ormosia_keys_cache);

  3.    pefree(ormosia_domain_cache, 1);

  4.    pefree(ormosia_keys_cache, 1);


阅读更多
想对作者说点什么? 我来说一句

php_sqlsrv_7_ts.dll等文件下载

2017年09月06日 734KB 下载

php7及以上版本 swoole扩展

2018年06月20日 834KB 下载

没有更多推荐了,返回首页

加入CSDN,享受更精准的内容推荐,与500万程序员共同成长!