Pourquoi une fonction infiniment récursive en PHP provoque-t-elle un segfault?

Une question hypothétique pour vous tous à mâcher...

J'ai récemment répondu à une autre question sur SO où un script PHP était segfaulting, et cela m'a rappelé quelque chose que je me suis toujours demandé, alors voyons si quelqu'un peut faire la lumière dessus.

Considérez ce qui suit:

<?php

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

  segfault();

?>

Évidemment, cette fonction (inutile) boucle infiniment. Et finalement, manquera de mémoire car chaque appel à la fonction s'exécute avant que le précédent ne soit terminé. Comme une sorte de fourche bombe sans la fourche.

Mais... finalement, sur les plates - formes POSIX, le script mourra avec SIGSEGV (il meurt également sur Windows, mais plus gracieusement-pour autant que mes compétences de débogage de bas niveau extrêmement limitées puissent le dire). Le nombre de boucles varie en fonction de la configuration du système (mémoire allouée à PHP, 32bit/64bit, etc etc) et du système d'exploitation, mais ma vraie question est - pourquoi cela arrive-t-il avec un segfault?

  • est-ce simplement comme ça que PHP gère les erreurs "hors mémoire"? Sûrement il doit y avoir une façon plus gracieuse de gérer cela?
  • Est-ce un bug dans le moteur Zend?
  • y a-t-il un moyen de contrôler ou de gérer plus gracieusement à partir d'un script PHP?
  • Existe-t-il un paramètre qui contrôle généralement le nombre maximum d'appels récursifs pouvant être effectués dans une fonction?
31
demandé sur DaveRandom 2011-09-07 03:43:29

3 réponses

Si vous utilisez XDebug, il existe une profondeur d'imbrication de fonction maximale contrôlée par un paramètre ini :

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

Produit l'erreur suivante:

Erreur fatale: niveau maximum d'imbrication de fonction de ' 100 ' atteint, abandon!

Ce IMHO est une bien meilleure alternative qu'un segfault, car il ne tue que le script actuel, pas tout le processus.

Il y a ce fil qui était sur la liste interne il y a quelques années (2006). Son les commentaires sont:

Jusqu'à présent personne n'avait proposé une solution pour la boucle problème satisferait à ces conditions:

  1. Pas de faux positifs (c'est-à-dire un bon code fonctionne toujours)
  2. Pas de ralentissement pour l'exécution
  3. Fonctionne avec n'importe quelle taille de pile

Ainsi, ce problème reste mal aimé.

Maintenant, #1 est littéralement impossible à résoudre en raison du problème d'arrêt . #2 est trivial si vous gardez un compteur de pile profondeur (puisque vous vérifiez simplement le niveau de pile incrémenté sur la poussée de pile).

Enfin, #3 est un problème beaucoup plus difficile à résoudre. Considérant que certains systèmes d'exploitation allouent de l'espace de pile de manière non contiguë, il ne sera pas possible de l'implémenter avec une précision de 100%, car il est impossible d'obtenir la taille ou l'utilisation de la pile (pour une plate-forme spécifique, cela peut être possible ou même facile, mais pas en général).

Au lieu de cela, PHP devrait prendre L'indice de XDebug et d'autres langages (Python, etc) et faire un niveau d'imbrication configurable (Python est défini sur 1000 par défaut)....

Soit cela, soit intercepter les erreurs d'allocation de mémoire sur la pile pour vérifier le segfault avant qu'il ne se produise et le convertir en RecursionLimitException afin que vous puissiez récupérer....

24
répondu ircmaxell 2011-09-07 14:02:50

Je pourrais me tromper totalement à ce sujet puisque mes tests étaient assez brefs. Il semble que Php ne manquera que s'il manque de mémoire (et tente vraisemblablement d'accéder à une adresse invalide). Si la limite de mémoire est définie et suffisamment faible, vous obtiendrez une erreur de mémoire au préalable. Sinon, le code seg défauts et est géré par le système d'exploitation.

Ne peut pas dire s'il s'agit d'un bug ou non, mais le script ne devrait probablement pas être autorisé à devenir incontrôlable comme ceci.

Voir le le script ci-dessous. Le comportement est pratiquement identique quelles que soient les options. Sans limite de mémoire, il ralentit également mon ordinateur sévèrement avant qu'il ne soit tué.

<?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;
}
?>
4
répondu Explosion Pills 2011-09-07 00:12:15

Ne sait rien sur L'implémentation de PHP, mais il n'est pas rare dans un runtime de laisser des pages non allouées en "haut" de la pile afin qu'un segfault se produise si la pile déborde. Habituellement, cela est géré dans l'exécution et soit la pile est étendue, soit une erreur plus élégante est signalée, mais il peut y avoir des implémentations (et des situations dans d'autres) où le segfault est simplement autorisé à augmenter (ou à s'échapper).

2
répondu Hot Licks 2011-09-07 00:19:22