PHP7扩展持久化zend_array共享使用的实现和原理

2018-03-0906:15:27后端程序开发Comments2,751 views字数 4505阅读模式

项目需要在PHP7的扩展里,维护一个全局的持久化zend_array,在多次请求之间可以共享使用。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/bc/1368.html

在这里简单记录一下实现和原理。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/bc/1368.html

首先是定义一个全局的 zend_array*文章源自菜鸟学院-https://www.cainiaoxueyuan.com/bc/1368.html

zend_array *ormosia_domain_cache = NULL; 在扩展初始化回调里,分配并初始化一个 zend_array文章源自菜鸟学院-https://www.cainiaoxueyuan.com/bc/1368.html

  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,不会在请求结束后被释放。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/bc/1368.html

之后,调用 zend_hash_init初始化这个array,需要注意的是value的dtor回调函数并不是 zval_ptr_dtor,而是我自己实现的 persistant_zval_dtor函数。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/bc/1368.html

另外,最后一个参数persistant=1,这样 zend_array在内部分配哈希桶等内存时也会使用pemalloc分配持久化内存。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/bc/1368.html

既然要持久化,除了 zend_array本身以外,保存在 zend_array里的zval也一定要持久化内存,包括key是持久化的 zend_string,value是持久化的任意类型zval。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/bc/1368.html

这里就说说,为什么要自定义value的dtor函数,而不用zend API自带的 zval_ptr_dtor,这里截取了它的实现片段:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/bc/1368.html

  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是否为持久化内存:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/bc/1368.html

  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可以正确的根据内存类型进行相应的释放。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/bc/1368.html

问题就出在array类型, zend_array_destroy内部释放哈希桶的内存使用的是efree而不是pefree:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/bc/1368.html

  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的:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/bc/1368.html

  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析构函数。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/bc/1368.html

因为在我的业务场景中, zend_array保存的value只有string和array两种类型,并且嵌套的array也是保存的string或array类型,所以我的dtor函数只覆盖了所需的类型:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/bc/1368.html

  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,它内部会区分内存的类型进行释放:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/bc/1368.html

  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的释放行为。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/bc/1368.html

zend_hash_destroy只会将桶内所有key和value进行dtor析构,然后释放哈希桶内存,并不会释放zend_array结构自身的内存,所以我接着调用了pefree释放它自身。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/bc/1368.html

那么,代码中在else部分提到的”回收循环引用”是什么意思呢?为什么我注释掉了呢?文章源自菜鸟学院-https://www.cainiaoxueyuan.com/bc/1368.html

所谓”循环引用”,是指这样的一个例子:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/bc/1368.html

我有一个 zend_array的zval1,我拥有唯一的引用计数=1。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/bc/1368.html

接着,指定 key=”myself”,value就是zval1自身,将其zendhashupdate保存到zval1内,按照规矩我会为value增加1个引用计数,这样才算将value托付给了 zend_array,所以将导致zval1的引用计数为2。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/bc/1368.html

某个时刻,我们不再想访问zval1,所以释放1个引用计数,结果还剩下1个计数,并没有触发 zend_hash_destroy的调用,这个zval1将永远没有机会被彻底释放。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/bc/1368.html

究其原因,就是因为zval1保存了zval1,导致循环引用,GC垃圾回收无法生效。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/bc/1368.html

上面这段C操作,对应到PHP里就是这样的代码:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/bc/1368.html

  1. <?php
  2. $a = [];
  3. $a[0] = $a;
  4. unset($a);

难道这样的代码,PHP的GC就无能为力了吗?显然不是。else里的注释的代码,其实就是用来针对这种情况的,而这种情况只能出现在zval1的类型是array或者object的情况下,因为只有它们内部才能保存其他变量,从而导致出现循环引用。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/bc/1368.html

至于else部分的代码是如何搞定循环引用的,你可以参考这篇博客: GC垃圾回收 。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/bc/1368.html

原理并不算复杂,当我们的dtor函数发现减少1个引用计数后仍旧不为0的情况下,就会检测这是否是因为循环引用引起,所以进入检测函数 GC_ZVAL_CHECK_POSSIBLE_ROOT文章源自菜鸟学院-https://www.cainiaoxueyuan.com/bc/1368.html

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

最后 在扩展退出前,记得释放一下持久化的zend_array:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/bc/1368.html

  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);
文章源自菜鸟学院-https://www.cainiaoxueyuan.com/bc/1368.html
  • 本站内容整理自互联网,仅提供信息存储空间服务,以方便学习之用。如对文章、图片、字体等版权有疑问,请在下方留言,管理员看到后,将第一时间进行处理。
  • 转载请务必保留本文链接:https://www.cainiaoxueyuan.com/bc/1368.html

Comment

匿名网友 填写信息

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定