资源被过早回收

9
我是一位有用的助手,可以为您翻译文本。

我使用 SWIG 创建了一个PHP扩展,并且一切都运行正常,但是当链接方法调用时,我观察到一些奇怪的垃圾回收行为。例如,这个可以工作:

$results = $response->results();
$row = $results->get(0)->iterator()->next();
printf('%s %s' . "\n", $row->getString(0), $row->getString(1));

但是这个程序段出现了段错误:
$row = $response->results()->get(0)->iterator()->next();
printf('%s %s' . "\n", $row->getString(0), $row->getString(1));

唯一的区别是第一个创建了$results,而第二个则将调用链接在一起。
SWIG只向PHP暴露函数,并生成用于与其交互的PHP代理类。这些代理类基本上持有一个资源,该资源与每个暴露的函数一起传递,以及那些函数通常会使用的其他参数。认为这些代理类可能是问题所在,我重写了代码,绕过它们,改为直接使用暴露的函数。与之前一样,这也有效:
$results = InvocationResponse_results($response->_cPtr);
$row = TableIterator_next(Table_iterator(Tables_get($results, 0)));
printf('%s %s' . "\n", Row_getString($row, 0), Row_getString($row, 1));

再次出现段错误:

$row = TableIterator_next(Table_iterator(Tables_get(InvocationResponse_results($response->_cPtr), 0)));
printf('%s %s' . "\n", Row_getString($row, 0), Row_getString($row, 1));

再次强调,唯一的区别在于第一个创建了$results,而第二个将调用链接在一起。

此时,我花了一些时间在gdb/valgrind中进行调试,并确定当链接调用在一起时,InvocationResponse_results返回的析构函数被过早地调用。为了观察,我在公开的C++函数及其析构函数的顶部插入了std::cout语句。这是没有链接的输出:

InvocationResponse_results()
Tables_get()
Table_iterator()
TableIterator_next()
__wrap_delete_TableIterator
Row_getString()
Row_getString()
Hola Mundo
---
__wrap_delete_InvocationResponse
__wrap_delete_Row
__wrap_delete_Tables

我在脚本末尾打印了---,以便区分脚本执行期间发生的事情和之后发生的事情。 Hola Mundo来自printf。其余部分来自C ++。正如您所看到的,所有内容都按预期顺序调用。析构函数仅在脚本执行后调用,尽管TableIterator析构函数比我预期的要早。然而,这并没有引起任何问题,可能与此无关。现在将其与链接输出进行比较:

InvocationResponse_results()
Tables_get()
__wrap_delete_Tables
Table_iterator()
TableIterator_next()
__wrap_delete_TableIterator
Row_getString()
Segmentation fault (core dumped)

如果不将InvocationResponse_results的返回值保存到$results中,它会很快被垃圾回收,甚至在执行调用链(在Tables_getTable_iterator之间)之前就被回收,这很快会导致问题,并最终导致段错误。

我还使用xdebug_debug_zval()在各种地方检查引用计数,但没有发现任何异常。以下是在不链接的情况下对$results$row输出的结果:

results: (refcount=1, is_ref=0)=resource(18) of type (_p_std__vectorT_voltdb__Table_t)
row: (refcount=1, is_ref=0)=resource(21) of type (_p_voltdb__Row)

使用链接的 $row

row: (refcount=1, is_ref=0)=resource(21) of type (_p_voltdb__Row)

我已经花了几天时间在这上面,现在我几乎没有什么想法了,所以任何关于如何解决这个问题的见解都将不胜感激。


@Artefacto 我在 __wrap_delete_Tables 被调用时窥视了一下 _zend_list_delete,在两种情况下(无段错误和段错误),它都被垃圾回收了,因为它的引用计数 (--le->refcount) 是 -1。 - Ed Mazur
可能在两种情况下,相关的 zval 的引用计数被以不同的方式操纵。为其 refcount 字段(在 5.3+ 中为 refcount__gc)设置数据断点。 - Artefacto
1
最后,请确保您正在使用调试版本,并且在使用valgrind时关闭zend内存管理器会很有用。 - Artefacto
一种可能的方法是在资源的refcount上设置写入数据断点(与zval的refcount不同)。当然,如果bug恰好是由于创建了该资源的新zval而没有增加资源refcount(缺少对zval_copy_ctor或直接使用_zend_list_addref的调用),那么断点将无法捕获它。最好的方法是在原始zval的值上设置读取断点,希望在创建zval的浅拷贝时读取它。 - Artefacto
即使这样也可能不起作用。例如,您可以创建一个资源,存储其ID,创建一个带有该ID的资源zval,然后创建另一个带有存储的ID的资源zval,而无需从第一个复制。 - Artefacto
显示剩余3条评论
1个回答

1

实际上是一个类似于调试问题段错误的问题的一部分。(就像Artefacto所说的那样)


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接