捕获内存泄漏
让我们尝试传递带有某种意外类型值的数组:
$ php -r 'var_dump(test_scale([null]));'
Warning: test_scale(): unexpected argument type in Command line code on line 1
NULL
[Wed Jan 22 13:56:11 2020] Script: 'Standard input code'
/home/dmitry/tmp/php-src/Zend/zend_hash.c(256) : Freeing 0x00007f8189c57840 (56 bytes), script=Standard
input code
=== Total 1 memory leaks detected ===
我们看到了预期的警告和 NULL 结果,但随后我们又从 PHP 内部内存调试器中看到了一些关于泄露内存的调试信息。请注意,这些信息只有在 PHP 的 DEBUG 版本中才有,这也是我建议在开发过程中使用 DEBUG 版本的原因之一。上述信息表明,Zend/zend_hash.c 第 256 行分配的 56 字节内存被泄露。这是 _zend_new_array() 的主体,我们可能已经猜到了它是从哪里调用的,因为我们只调用了一次。不过,在现实生活中,我们无法确定调用位置,如果能获得泄漏分配的回溯信息就更好了。
在 Linux 上,我们可以使用 valgrind。这是一个很好的工具,可以捕捉内存泄露和其他不正确的内存访问问题(如使用后释放和越界)。Valgrind 会模拟程序中的系统内存管理器(malloc、free 和相关函数),并捕捉不一致的地方。
展望未来,PHP 使用自己的内存管理器,我们应该使用 USE_ZEND_ALLOC 环境变量切换到系统内存管理器。此外,禁用扩展卸载也是有意义的。
$ USE_ZEND_ALLOC=0 ZEND_DONT_UNLOAD_MODULES=1 valgrind --leak-check=full \
php -r 'var_dump(test_scale([null]));'
...
==19882== 56 bytes in 1 blocks are definitely lost in loss record 19 of 27
==19882== at 0x483880B: malloc (vg_replace_malloc.c:309)
==19882== by 0x997CC5: __zend_malloc (zend_alloc.c:2975)
==19882== by 0x996C30: _malloc_custom (zend_alloc.c:2416)
==19882== by 0x996D6E: _emalloc (zend_alloc.c:2535)
==19882== by 0x9E13BE: _zend_new_array (zend_hash.c:256)
==19882== by 0x4849AE0: do_scale (test.c:66)
==19882== by 0x4849F69: zif_test_scale (test.c:100)
==19882== by 0xA3CE1B: ZEND_DO_ICALL_SPEC_RETVAL_USED_HANDLER (zend_vm_execute.h:1313)
==19882== by 0xA9D0E8: execute_ex (zend_vm_execute.h:53564)
==19882== by 0xAA11A0: zend_execute (zend_vm_execute.h:57664)
==19882== by 0x9B7D0B: zend_eval_stringl (zend_execute_API.c:1082)
==19882== by 0x9B7EBF: zend_eval_stringl_ex (zend_execute_API.c:1123)
...
现在我们可以确定:内存泄漏的源头是 do_scale() 中调用了 zend_new_array() 函数。为了解决这个问题,我们应该在失败的情况下销毁数组。
} else if (Z_TYPE_P(x) == IS_ARRAY) {
zend_array *ret = zend_new_array(zend_array_count(Z_ARR_P(x)));
zend_ulong idx;
zend_string *key;
zval *val, tmp;
ZEND_HASH_FOREACH_KEY_VAL(Z_ARR_P(x), idx, key, val) {
if (do_scale(&tmp, val, factor) != SUCCESS) {
zend_array_destroy(ret);
return FAILURE;
}
if (key) {
zend_hash_add(ret, key, &tmp);
} else {
zend_hash_index_add(ret, idx, &tmp);
}
} ZEND_HASH_FOREACH_END();
RETVAL_ARR(ret);
} else {
别忘了进行测试。
Valgrind 比 PHP 内部内存调试器要聪明得多,如果您的扩展中包含 *.phpt
回归测试,您可以在 Valgrind 下运行所有这些测试。
$ make test TESTS="-m"