Подтвердить что ты не робот

Почему бесконечно рекурсивная функция в PHP вызывает segfault?

Гипотетический вопрос для вас всех, чтобы пожевать...

Недавно я ответил на другой вопрос о SO, где PHP script был segfault, и это напомнило мне то, что я всегда задавал себе вопрос, поэтому давайте посмотрим, сможет ли кто-нибудь пролить свет на него.

Рассмотрим следующее:

<?php

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

  segfault();

?>

Очевидно, что эта (бесполезная) функция бесконечно петли. И, в конце концов, закончится память, потому что каждый вызов функции выполняется до того, как предыдущий закончен. Похоже на разветвленную бомбу без разветвления.

Но... в конечном итоге на платформах POSIX script умрет вместе с SIGSEGV (он также умирает в Windows, но более изящно - насколько это может сказать мои крайне ограниченные навыки отладки низкого уровня). Количество циклов варьируется в зависимости от конфигурации системы (память, выделенная для PHP, 32 бит /64 бит и т.д.) И ОС, но мой реальный вопрос: почему это происходит с segfault?

  • Это просто, как PHP обрабатывает ошибки "вне памяти"? Разумеется, должен быть более грациозный способ справиться с этим?
  • Это ошибка в движке Zend?
  • Есть ли способ, которым это можно контролировать или обрабатывать более изящно из PHP script?
  • Есть ли какой-либо параметр, который обычно контролирует максимальное количество рекурсивных вызовов, которые могут выполняться в функции?
4b9b3361

Ответ 1

Если вы используете XDebug, существует максимальная глубина вложенности функции, которая контролируется установкой ini:

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

Выдает следующую ошибку:

Неустранимая ошибка: максимальный уровень вложенности функции "100" достигнут, прерывается!

Это ИМХО - гораздо лучшая альтернатива, чем segfault, поскольку она только убивает текущий script, а не весь процесс.

Существует этот поток, который был включен в список внутренних дел несколько лет назад (2006). Его комментарии:

До сих пор никто не предлагал решение проблемы бесконечного цикла, будет удовлетворять следующим условиям:

  • Нет ложных срабатываний (т.е. хороший код всегда работает)
  • Нет замедления для выполнения
  • Работает с любым размером стека

Таким образом, эта проблема остается нераскрытой.

Теперь # 1 практически невозможно решить из-за проблемы с остановкой . # 2 тривиально, если вы храните счетчик глубины стека (поскольку вы просто проверяете уровень инкрементного стека на push стека).

Наконец, # 3 Решается гораздо более сложная задача. Учитывая, что некоторые операционные системы будут распределять пространство стека непересекающимся образом, реализовать его со 100% -ной точностью не представляется возможным, поскольку невозможно обеспечить портативный размер стека или его использование (для конкретной платформы это может быть возможно или даже легко, но не вообще).

Вместо этого PHP должен взять подсказку с XDebug и других языков (Python и т.д.) и сделать настраиваемый уровень вложенности (Python установлен на 1000 по умолчанию)....

Либо это, либо ошибки распределения памяти ловушки в стеке для проверки segfault перед тем, как это произойдет, и преобразуйте это в RecursionLimitException, чтобы вы могли восстановить....

Ответ 2

Я мог бы полностью ошибиться в этом, так как мое тестирование было довольно кратким. Похоже, что Php будет только отпадать, если закончится нехватка памяти (и, по-видимому, пытается получить доступ к недопустимому адресу). Если ограничение памяти установлено и достаточно низкое, вы получите ошибку из памяти заранее. В противном случае код seg неисправен и обрабатывается ОС.

Нельзя сказать, является ли это ошибкой или нет, но script, вероятно, не должно быть разрешено выйти из-под контроля, как это.

См. ниже script. Поведение практически идентично независимо от вариантов. Без ограничения памяти он также сильно замедляет мой компьютер до его уничтожения.

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

Ответ 3

Не знаю ничего о реализации PHP, но не редкость в языковой среде, чтобы оставить страницы нераспределенными в "верхней части" стека, чтобы segfault возникнет, если переполнение стека. Обычно это обрабатывается внутри среды выполнения, и либо стек расширяется, либо сообщается о более элегантной ошибке, но могут быть реализации (и ситуации в других), где segfault просто позволяет подняться (или ускользнуть).