为什么在PHP中无限递归的函数会导致segfault?

32

一个供大家思考的假设性问题...

最近我在stackoverflow上回答了另一个问题,其中一个PHP脚本出现了segfault,这让我想起了一些我一直在想的东西,所以让我们看看是否有人能够给予任何启示。

考虑以下情况:

<?php

  function segfault ($i = 1) {
    echo "$i\n";
    segfault($i + 1);
  }

  segfault();

?>

显然,这个(无用的)函数会无限循环。最终,由于每次调用该函数都在前一个完成之前执行,因此内存将耗尽。有点像一个没有分叉但会出现fork炸弹的东西。

但是... 最终,在POSIX平台上,脚本将以SIGSEGV死亡(它也会在Windows上死亡,但更为优雅-就我极其有限的低级调试技能而言)。循环次数因系统配置(分配给PHP的内存、32位/64位等等)和操作系统而异,但我的真正问题是-为什么会出现段错误?

  • 这是否仅是PHP处理“内存不足”错误的方法?肯定有一种更为优雅的处理方式吧?
  • 这是Zend引擎中的错误吗?
  • 是否有任何方式可以在PHP脚本内部更加优雅地控制或处理这个问题?
  • 是否有任何设置通常控制在函数中可以进行的递归调用的最大数量?

7
根据 PHP(https://bugs.php.net/bug.php?id=43187)的说明,这是预期行为。 - NullUserException
3
很有趣,我搜索了PHP的错误报告,但没有找到这个问题...他们说这是一个“已知的递归限制”,但没有提示该限制的约束条件或提供任何控制方式,这似乎很奇怪。正如那个错误报告者所说,只有在编写有错误的代码时才有可能出现问题,但知道边界将会更好。 - DaveRandom
1
我希望所有崩溃的函数都能重命名为segfault - 这肯定可以节省一些在办公室度过的漫长夜晚! - corsiKa
2
@Lawrence Cherone,有时代码并不打算超出堆栈而实际上确实超出了(比如一个本来很好的递归算法碰到了一个退化情况;你知道这是一个正常的“bug”)。在我看来,PHP对此的“解决方案”是不可接受的。(Ruby、Perl和Python这三个动态竞争对手都施加了更合理但有些武断的限制。) - user166390
4
把"segfault"称作是"perfectly good error code"有些过头了,嗯? - NullUserException
显示剩余4条评论
3个回答

24

如果您使用 XDebug,那么有一个由一个ini设置控制的最大函数嵌套深度:

$foo = function() use (&$foo) { 
    $foo();
};
$foo();

产生以下错误:

致命错误:达到'100'的最大函数嵌套级别,正在中止!

在我看来,这比段错误要好得多,因为它只会终止当前脚本,而不是整个进程。

几年前(2006年)在内部列表上有这个线程。他的评论是:

到目前为止,没有人为无限循环问题提出了满足以下条件的解决方案:

  1. 没有错误判断(即良好的代码始终有效)
  2. 执行速度不慢
  3. 适用于任何堆栈大小

因此,这个问题仍未解决。

现在,#1由于停机问题而几乎不可能解决。如果您保持堆栈深度计数器,那么#2就非常简单(因为您只需检查堆栈推送时增加的堆栈级别即可)。

最后,#3是一个更难的问题要解决。考虑到一些操作系统将以非连续的方式分配堆栈空间,因此不可能以100%的准确性实现它,因为在可移植的情况下无法获得堆栈大小或使用情况(对于特定平台来说可能是可能甚至很容易,但不通用)。

相反,PHP应该从XDebug和其他语言(Python等)中获取提示,并设置可配置的嵌套级别(Python默认设置为1000)....

要么,捕获堆栈上的内存分配错误以检查段错误是否发生并将其转换为RecursionLimitException ,以便您可以进行恢复...


捕获SIGSEGV并抛出异常? - Demi
我为什么之前没有看到这篇帖子,当时我在寻找分段错误的原因时。我在一个暂存服务器上花了几个小时来调试这个问题。 - Julius Š.

4
我对此可能完全错误,因为我的测试相当简短。似乎只有当Php运行内存不足(并且可能尝试访问无效地址)时才会导致seg fault。如果内存限制被设置并且足够低,您将首先收到内存不足的错误。否则,代码seg fault并由操作系统处理。
不能确定这是否是错误,但是脚本应该不能像这样失控。
请参见下面的脚本。行为几乎与选项无关。没有内存限制,它也会在被杀死之前严重减慢我的计算机。
<?php
$opts = getopt('ilrv');
$type = null;
//iterative
if (isset($opts['i'])) {
   $type = 'i';
}
//recursive
else if (isset($opts['r'])) {
   $type = 'r';
}
if (isset($opts['i']) && isset($opts['r'])) {
}

if (isset($opts['l'])) {
   ini_set('memory_limit', '64M');
}

define('VERBOSE', isset($opts['v']));

function print_memory_usage() {
   if (VERBOSE) {
      echo memory_get_usage() . "\n";
   }
}

switch ($type) {
   case 'r':
      function segf() {
         print_memory_usage();
         segf();
      }
      segf();
   break;
   case 'i':
      $a = array();
      for ($x = 0; $x >= 0; $x++) {
         print_memory_usage();
         $a[] = $x;
      }
   break;
   default:
      die("Usage: " . __FILE__ . " <-i-or--r> [-l]\n");
   break;
}
?>

这里有一些不错的实验,很好地说明了问题和结果。今天早上经过进一步的谷歌搜索,我发现了这个(因为该网站已经关闭,所以使用了谷歌缓存),它建议您可以捕获和处理分段错误 - 尽管a)我怀疑它是否适用于我们正在处理的内存不足情况,b)我没有安装PCNTL扩展的机器来测试它。 - DaveRandom

2

虽然我对PHP实现不太了解,但在语言运行时中,将页面留空在栈的“顶部”以防止堆栈溢出是很常见的。通常这在运行时内部处理,要么扩展堆栈,要么报告更优雅的错误,但也可能有一些实现(或其他情况)允许Segfault简单地升级(或逃避)。


我有点理解这背后的原因,但这确实使得 PHP 脚本更难调试 - 我无法知道段错误是由我的脚本还是 Zend 引擎引起的。能够得到有意义的错误消息会很好,但我认为实际上没有什么可以做的。 - DaveRandom
我同意通常不喜欢让这种异常出现。但我也理解有些情况下不得不这样做的原因——堆栈溢出是语言运行时最难处理的问题之一。 - Hot Licks

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