Dois-je me préoccuper de détecter les erreurs de mémoire MOO (mémoire insuffisante) dans mon code C?

J’ai consacré un grand nombre de lignes de code C aux étiquettes de nettoyage / conditionnelles pour l’allocation de mémoire défaillante (indiqué par la famille alloc retournant la NULL ). On m’a appris qu’il s’agissait d’une bonne pratique, car en cas de défaillance de la mémoire, un statut d’erreur approprié pouvait être signalé et l’appelant pouvait potentiellement effectuer un “nettoyage de mémoire en douceur”, puis réessayer. J’ai maintenant des doutes sur cette philosophie que j’espère éclaircir.

Je suppose qu’il est possible qu’un appelant puisse libérer l’espace tampon excessif ou supprimer des objects relationnels de ses données, mais j’aperçois que l’appelant a rarement la capacité (ou le niveau d’abstraction approprié) de le faire. De plus, le retour rapide de la fonction appelée sans effets secondaires est souvent non sortingvial.

Je viens également de découvrir le tueur Linux OOM, qui semble rendre ces efforts totalement inutiles sur ma plate-forme de développement principale.

Par défaut, Linux applique une stratégie d’allocation de mémoire optimiste. Cela signifie que lorsque malloc () renvoie non-NULL, rien ne garantit que la mémoire est réellement disponible. C’est un très mauvais bug. S’il s’avère que le système manque de mémoire, un ou plusieurs processus seront tués par l’infâme tueur de MOO.

Je suppose qu’il existe probablement d’autres plateformes qui suivent le même principe. Existe-t-il quelque chose de pragmatique qui justifie la vérification des conditions de MOO?

Des conditions de mémoire insuffisante peuvent survenir même sur des ordinateurs modernes disposant de beaucoup de mémoire, si l’utilisateur ou l’administrateur système limite (voir ulimit) l’espace mémoire d’un processus ou si le système d’exploitation prend en charge les limites d’allocation de mémoire par utilisateur. Dans les cas pathologiques, la fragmentation rend cela assez probable, même.

Cependant, étant donné que l’utilisation de la mémoire allouée dynamicment est répandue dans les programmes modernes, il devient très difficile de gérer les erreurs de mémoire insuffisante pour de bonnes raisons. Les erreurs de vérification et de traitement de ce type devraient être effectuées partout, avec un coût élevé en complexité.

Je trouve qu’il est préférable de concevoir le programme de sorte qu’il puisse se bloquer à tout moment. Par exemple, assurez-vous que les données créées par l’utilisateur sont sauvegardées à tout moment sur le disque, même si l’utilisateur ne les enregistre pas explicitement. (Voir vi -r, par exemple.) De cette façon, vous pouvez créer une fonction pour allouer de la mémoire qui termine le programme en cas d’erreur. Étant donné que votre application est conçue pour gérer les pannes à tout moment, vous pouvez vous planter. L’utilisateur sera surpris, mais ne perdra pas (beaucoup) de travail.

La fonction d’allocation qui ne manque jamais peut ressembler à ceci (code non testé, non compilé, à des fins de démonstration uniquement):

 /* Callback function so application can do some emergency saving if it wants to. */ static void (*safe_malloc_callback)(int error_number, size_t requested); void safe_malloc_set_callback(void (*callback)(int, size_t)) { safe_malloc_callback = callback; } void *safe_malloc(size_t n) { void *p; if (n == 0) n = 1; /* malloc(0) is not well defined. */ p = malloc(n); if (p == NULL) { if (safe_malloc_callback) safe_malloc_callback(errno, n); exit(EXIT_FAILURE); } return p; } 

L’article de Valerie Aurora, le logiciel Crash-only, pourrait être éclairant.

Regardez l’autre côté de la question: si vous mémorisez un malloc, il échoue et que vous ne le détectez pas au malloc, quand le détecterez-vous?

Évidemment, lorsque vous essayez de déréférencer le pointeur.

Comment allez-vous le détecter? En obtenant une Bus error ou quelque chose de similaire, quelque part après le malloc, vous devrez localiser avec un vidage de mémoire et le débogueur.

D’autre part, vous pouvez écrire

  #define OOM 42 /* just some number */ /* ... */ if((ptr=malloc(size))==NULL){ /* a well-behaved fprintf should NOT malloc, so it can be used * in this sort of context */ fprintf(stderr,"OOM at %s: %s\n", __FILE__, __LINE__); exit(OOM); } 

et obtenez “OOM à parser.c: 447”.

Vous choisissez.

Mettre à jour

Bonne question sur le retour gracieux. La difficulté à assurer un retour gracieux réside dans le fait qu’en général, vous ne pouvez vraiment pas définir de paradigme ou de modèle pour ce faire, en particulier en C, qui est après tout un langage d’assemblage sophistiqué. Dans un environnement récupéré, vous pouvez forcer un GC. dans une langue avec des exceptions, vous pouvez lancer une exception et décompresser des choses. En C, vous devez le faire vous-même et vous devez donc décider des efforts que vous souhaitez y consacrer.

Dans la plupart des programmes, une terminaison anormale est ce que vous pouvez faire de mieux. Dans ce schéma, vous recevez (espérons-le) un message utile sur stderr – bien sûr, il pourrait également s’agir d’un enregistreur ou quelque chose du genre – et d’une valeur connue en tant que code de retour.

Les programmes de haute fiabilité avec des temps de récupération courts vous poussent dans quelque chose comme des blocs de récupération , dans lesquels vous écrivez du code qui tente de remettre un système dans un état de survie. Ce sont géniaux, mais compliqués; le document que j’ai lié en parle en détail.

Au milieu, vous pouvez imaginer un système de gestion de la mémoire plus complexe, par exemple gérer votre propre pool de mémoire dynamic. Après tout, si quelqu’un d’autre peut écrire malloc, vous pouvez le faire aussi.

Mais il n’ya tout simplement pas de tendance générale (dont je suis au courant, de toute façon) à un nettoyage suffisant pour pouvoir revenir de manière fiable et laisser le programme environnant se poursuivre.

Quelle que soit la plate-forme (sauf peut-être les systèmes embarqués), il est judicieux de vérifier la NULL , puis de quitter sans effectuer aucun nettoyage (ni beaucoup) à la main.

Le manque de mémoire n’est pas une simple erreur. C’est une catastrophe sur les systèmes actuels.

Le livre The Practice of Programming (Brian W. Kernighan et Rob Pike, 1999) définit des fonctions comme emalloc() qui se emalloc() avec un message d’erreur s’il ne rest plus de mémoire.

Cela dépend de ce que vous écrivez. Est-ce une bibliothèque polyvalente? Dans ce cas, vous souhaitez remédier au manque de mémoire le plus gracieusement possible, en particulier s’il est raisonnable de penser qu’il sera utilisé sur des systèmes el-cheapo ou des périphériques intégrés.

Considérez ceci: un programmeur utilise votre bibliothèque. Il y a un bogue (variable non initialisée peut-être) dans son programme qui transmet un argument idiot à votre code, qui tente par conséquent d’allouer un seul bloc de mémoire de 3,6 Go. De toute évidence, malloc() renvoie NULL. Aurait-il plutôt une erreur de segmentation inexpliquée générée quelque part dans le code de la bibliothèque, ou une valeur de retour pour indiquer l’erreur?

Pour éviter toute vérification d’erreur sur tout votre code, une approche consiste à allouer une quantité de mémoire raisonnable au début et à la sous-allouer si nécessaire.

En ce qui concerne le tueur de MOO Linux, j’ai entendu dire que ce comportement est maintenant désactivé par défaut sur les principales dissortingbutions. Même s’il est activé, ne vous méprenez pas: malloc() peut renvoyer NULL, et le sera certainement si la mémoire totale utilisée par votre programme dépasse 4GiB (sur un système 32 bits). En d’autres termes, même si malloc() ne vous sécurise pas un peu de RAM / swap, il réservera une partie de votre espace d’adressage.

Je suggère une expérience – écrivez un petit programme qui continue à allouer de la mémoire sans la libérer, puis affiche un petit message (fixe) en cas d’échec de l’allocation. Quels effets constatez-vous sur votre système lorsque vous exécutez ce programme? Le message est-il déjà imprimé?

Si le système se comporte normalement et rest réactif jusqu’au moment où l’erreur est affichée, alors je dirais que oui, cela vaut la peine de vérifier. OTOH, si le système devient lent, ne répond plus et est éventuellement inutilisable avant que le message ne soit affiché (si cela se produit), alors je dirais que non, cela ne vaut pas la peine d’être vérifié.

Important: avant d’exécuter ce test, enregistrez tout le travail important. Ne l’exécutez pas sur un serveur de production.

En ce qui concerne le comportement de MOO sous Linux, c’est vraiment souhaitable et c’est ainsi que fonctionnent la plupart des systèmes d’exploitation. Il est important de réaliser que lorsque vous malloc () une partie de la mémoire, vous ne la récupérez PAS directement à partir du système d’exploitation, vous la récupérez à partir de la bibliothèque d’exécution C. Cela aura typiquement demandé au système d’exploitation une grosse quantité de mémoire à l’avance (ou à la première demande) qu’il gère ensuite via l’interface malloc / free. Comme de nombreux programmes n’utilisent jamais la mémoire dynamic, il serait indésirable que le système d’exploitation remette de la “vraie” mémoire au runtime C – à la place, il laisse un certain nombre de VM, qui seront réellement validés lors de vos appels Malloc.

Avec les ordinateurs actuels et la quantité de RAM généralement installée, la recherche de toute erreur d’allocation de mémoire est probablement trop détaillée. Comme vous l’avez vu, il est souvent difficile, voire impossible, de prendre une décision rationnelle sur ce qu’il faut désallouer. Au fur et à mesure que votre processus alloue de plus en plus de mémoire, le système d’exploitation réduira d’autant la quantité de mémoire disponible pour les mémoires tampon de disque. Lorsque cela tombe en dessous d’un certain seuil, le système d’exploitation commencera à paginer de la mémoire sur le disque. (Ceci est une simplification, car il existe de nombreux facteurs dans la gestion de la mémoire.)

Une fois que le système d’exploitation commence à paginer de la mémoire, le système tout entier devient de plus en plus lent et il faudra probablement encore un bon bout de temps avant que votre application ne voie réellement la valeur NULL de malloc (le cas échéant).

Avec la quantité de mémoire disponible sur les systèmes actuels, une erreur «insuffisante de mémoire» signifie probablement qu’un bogue dans votre code a tenté d’allouer une quantité de mémoire arbitraire. Dans ce cas, aucune tentative de libération et de nouvelle tentative de la part de votre processus ne résoudra le problème.

Vous devez peser ce qui est meilleur ou pire pour vous: mettez tout le travail en oeuvre pour vérifier la présence de MOO ou faites échouer votre programme à des moments inattendus

Les processus sont généralement exécutés avec une limite de ressources (voir ulimit (3)) sur la taille de la stack, mais pas sur la taille du tas. malloc (3) gérera l’augmentation de la mémoire de sa zone de tas page par page à partir du système d’exploitation, et le système d’exploitation s’arrangera pour que cette page soit allouée physiquement et corresponde à votre segment de mémoire pour votre processus. S’il n’y a plus de RAM dans votre ordinateur, la plupart des systèmes d’exploitation ont une sorte de partition swap sur disque. Lorsque votre système commence à avoir besoin d’utiliser le swap, les choses se ralentissent progressivement. Si un processus conduit à cela, il peut facilement être identifié avec un utilitaire tel que ps (1).

À moins que votre code ne soit exécuté avec une limite de ressources ou sur un système avec une mémoire de petite taille et sans échange, je pense que l’on peut programmer en supposant que malloc (3) réussisse. Si vous n’êtes pas certain, créez simplement une enveloppe factice qui pourra éventuellement faire la vérification et simplement quitter. Une valeur de retour d’état d’erreur n’a pas de sens, car votre programme nécessite la mémoire qu’il a déjà allouée. Si votre malloc (3) échoue et que vous ne vérifiez pas la valeur NULL, votre processus mourra de toute façon s’il commence à accéder au pointeur (NULL) qu’il a reçu.

Les problèmes avec malloc (3) ne proviennent généralement pas de mémoire insuffisante, mais d’une erreur logique dans votre programme entraînant des appels incorrects à malloc et à free. Ce problème habituel ne sera pas détecté en vérifiant le succès de malloc.

Bien. Tout dépend de la situation.

Tout d’abord. Si vous avez détecté une mémoire insuffisante pour vos besoins, que ferez-vous? L’utilisation la plus commune est:

 if (ptr == NULL) { fprintf(log /* stderr or anything */, "Cannot allocate memory"); exit(2); } 

Bien. Même s’il n’utilise pas malloc, il peut allouer les tampons. En outre, tant pis s’il s’agit d’une application graphique – il est peu probable que votre utilisateur la repère. Si votre utilisateur est suffisamment intelligent pour exécuter une application depuis la console pour vérifier les erreurs, il verra probablement que quelque chose a dévoré toute sa mémoire. D’accord. Alors peut être afficher un dialog? Mais l’affichage de dialog peut consumr des ressources – et c’est généralement le cas.

Deuxièmement, pourquoi avez-vous besoin des informations sur le MOO? Cela se produit dans deux cas:

  1. Un autre logiciel est un buggy. Vous ne pouvez rien faire avec
  2. Votre programme est buggy. Dans ce cas, il est peu probable que l’utilisateur avertisse l’utilisateur de quelque manière que ce soit (sans mentionner que 99% des utilisateurs ne lisent pas les messages et diront que le logiciel est tombé en panne sans plus de détails). Si ce n’est pas le cas, l’utilisateur est susceptible de le repérer de toute façon (surveiller le système ou utiliser un logiciel plus spécialisé).
  3. Pour libérer des caches, etc. Vous devez vérifier dans le système, mais sachez que cela ne fonctionnera probablement pas. Vous ne pouvez gérer que sbrk / mmap / etc. appels et sous Linux, vous obtiendrez de toute façon MOO

Oui, je le crois, si vous suivez toujours la pratique. Cela peut ne pas être pratique pour un programme volumineux écrit en C en raison du degré de travail manuel que cela peut nécessiter, mais dans un langage plus moderne, la majeure partie de ce travail est effectuée pour vous, car une insuffisance de mémoire entraîne une exception renvoyée.

Le fait que le programme n’entre pas dans un état non défini en raison du manque de mémoire, entraînant un dépassement de la mémoire tampon (évite évidemment la possibilité d’un état non défini en raison d’une sortie prématurée de la fonction, bien qu’il une classe de bug différente). Cela fait, votre programme peut toujours gérer la condition d’erreur ou, en cas d’échec critique, décider de quitter de manière gracieuse.

Vérifier les conditions du MOO et prendre les mesures appropriées peut s’avérer difficile si vous déformez le logiciel. La fiabilité du logiciel que vous souhaitez acquérir dépend de la fiabilité du logiciel.

Par exemple, l’hyperviseur VirtualBox détectera les erreurs de mémoire insuffisante et mettra en pause la machine virtuelle, ce qui permettra à l’utilisateur de fermer certaines applications pour libérer de la mémoire. J’ai observé un tel comportement sous Windows. En fait, presque tous les appels dans VirtualBox ont un indicateur de succès comme valeur de retour et vous pouvez simplement renvoyer VERR_NO_MEMORY pour indiquer que l’allocation de mémoire a échoué. Cela introduit des vérifications supplémentaires, mais dans ce cas, cela en vaut la peine.