为啥 PHP 中的无限递归函数会导致段错误?

Posted

技术标签:

【中文标题】为啥 PHP 中的无限递归函数会导致段错误?【英文标题】:Why does an infinitely recursive function in PHP cause a segfault?为什么 PHP 中的无限递归函数会导致段错误? 【发布时间】:2011-11-11 17:55:12 【问题描述】:

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

我最近回答了关于 SO 的另一个问题,其中 php 脚本出现了段错误,这让我想起了我一直想知道的事情,所以让我们看看是否有人能对此有所了解。

考虑以下几点:

<?php

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

  segfault();

?>

显然,这个(无用的)函数无限循环。最终,将耗尽内存,因为对函数的每次调用都会在前一次调用完成之前执行。有点像没有分叉的分叉炸弹。

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

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

【问题讨论】:

According to PHP,这是预期行为。 @NullUserException 这很有趣,我确实搜索了 PHP 错误,但没有发现......他们说它是 known recursion limit 似乎很奇怪,但没有说明限制该限制,或提供任何方式来控制它。正如那个 bug 的报告者所说,如果你编写有 bug 的代码,这只会导致问题,但如果知道边界在哪里就好了。 我希望所有被炸毁的函数都将自己重命名为 segfault - 这肯定会在办公室节省一些漫长的夜晚! @Lawrence Cherone 在某些情况下,代码不打算用完堆栈但确实(比如说一个完美的递归算法,它遇到了一个退化的情况;你知道一个正常的“漏洞”)。 PHP 只是有一个不可接受的“解决方案”,IMOHO。 (Ruby、Perl 和 Python —— 3 个动态竞争者施加了更理智但有些武断的限制。) @Lawrence 将段错误称为“非常好的错误代码”有点过分,嗯? 【参考方案1】:

如果您使用 XDebug,则有一个最大函数嵌套深度,由 ini setting 控制:

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

产生以下错误:

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

这个恕我直言是一个比段错误更好的选择,因为它只杀死当前脚本,而不是整个过程。

几年前(2006 年)内部列表中有this thread。他的 cmets 是:

到目前为止,没有人提出解决无限循环问题的方法 将满足这些条件:

    没有误报(即好的代码总是有效的) 执行速度不会减慢 适用于任何堆栈大小

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

现在,由于halting problem,#1 几乎不可能解决。如果您保留堆栈深度计数器,#2 是微不足道的(因为您只是检查堆栈推送时增加的堆栈级别)。

最后,#3 是一个更难解决的问题。考虑到某些操作系统会以非连续方式分配堆栈空间,因此不可能以 100% 的准确度实现,因为不可能便携地获取堆栈大小或使用情况(对于特定平台,它可能是可能的或甚至很容易,但不是一般的)。

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

要么这样,要么在堆栈上捕获内存分配错误以在段错误发生之前检查它并将其转换为RecursionLimitException,以便您可以恢复......

【讨论】:

捕获 SIGSEGV 并抛出异常? 为什么我之前在寻找分段错误的原因时没有找到这个帖子。我花了几个小时在登台服务器上调试这个问题。【参考方案2】:

我可能完全错了,因为我的测试相当简短。似乎 Php 只有在内存不足时才会出现段错误(并且可能会尝试访问无效地址)。如果设置了内存限制并且足够低,您将事先收到内存不足错误。否则,代码段出错并由操作系统处理。

不能说这是否是一个错误,但可能不应该让脚本像这样失控。

请参阅下面的脚本。无论选项如何,行为实际上都是相同的。如果没有内存限制,它还会在我的计算机被杀死之前严重降低它的速度。

<?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;

?>

【讨论】:

那里做了一些很好的实验,很好地说明了问题和结果。今天早上进一步谷歌搜索后,我发现this(谷歌缓存,因为该站点已关闭)这表明您可以捕获和处理段错误 - 虽然a)我怀疑它在我们正在处理的内存不足的情况下会起作用和 b) 我没有安装 PCNTL 扩展的机器来测试它。【参考方案3】:

对 PHP 实现一无所知,但在语言运行时将未分配的页面留在堆栈的“顶部”并不少见,这样如果堆栈溢出就会发生段错误。通常这是在运行时内部处理的,要么扩展堆栈,要么报告更优雅的错误,但可能存在仅允许段错误上升(或逃逸)的实现(和其他情况)。

【讨论】:

我有点理解这背后的原因,但它确实使 PHP 脚本更难调试 - 我不知道段错误是由我的脚本还是 Zend 引擎引起的。收到一条有意义的错误消息会很好,但我接受实际上对此无能为力。 我同意我通常不关心让这类例外情况浮出水面。但我也理解可能会导致这种选择的情况——堆栈溢出是语言运行时中最难处理的事情之一。

以上是关于为啥 PHP 中的无限递归函数会导致段错误?的主要内容,如果未能解决你的问题,请参考以下文章

为啥增加递归深度会导致堆栈溢出错误?

python-15

为啥在循环开始时调用 requestAnimationFrame 不会导致无限递归?

PHP利用递归函数实现无限级分类的方法_php技巧 - PHP

PHP二叉树递归遍历无限循环问题

PHP递归函数