Sommaire
Chaque fois que je commence un nouveau projet en C, je me pose toujours la même question : que faire en cas d’échec d’allocation de mémoire ?
C’est une question qu’on ne se pose pas dans la plupart des autres langages plus récents, où l’allocation de mémoire est généralement une opération cachée loin de la vue du programmeur. Mais en C, la question se pose chaque fois que l’on doit appeler malloc()
ou toute autre fonction allouant de la mémoire : que faire si malloc()
renvoie NULL
?
Gérer gracieusement l’erreur
La première possibilité est de tenter de « gérer » l’erreur. On teste le pointeur renvoyé par la fonction d’allocation et on adopte un comportement approprié en cas d’erreur (par exemple, arrêter le traitement en cours et revenir à la boucle principale du programme — ce que recommandent les GNU Coding Standards, pour les programmes interactifs).
Cette approche, idéale sur le papier et conforme aux « bonnes pratiques » (toujours tester la valeur de retour d’une fonction !), n’est malheureusement pas sans problèmes.
D’abord, elle complexifie considérablement le code, la moindre allocation de mémoire donnant lieu à un embranchement et à la nécessité de faire remonter l’erreur au code appelant jusqu’à un niveau capable de prendre la bonne décision (mais on pourrait en dire autant de la gestion des erreurs en général en C, ce n’est pas particulièrement propre aux erreurs d’allocation mémoire).
(Qui a dit « bah, il suffit de lancer une exception et de la rattraper au bon endroit ? » Veuillez faire sortir le programmeur Java, merci.)
Dans certains cas, il est possible de se rendre la tâche un peu plus facile en repensant le code. Par exemple, on peut tenter d’allouer d’un coup toute la mémoire nécessaire avant d’effectuer une opération et annuler proprement ladite opération si l’allocation échoue. Si l’allocation réussit, on n’a plus à se soucier du risque de manque de mémoire pendant toute la durée de l’opération. Cela implique toutefois de pouvoir calculer préalablement la quantité de mémoire nécessaire, ce qui n’est pas toujours possible. Par ailleurs, organiser le code autour de l’allocation de mémoire n’est pas forcément pertinent et risque de nuire à sa lisibilité.
Plus grave encore, le chemin pris en cas d’erreur n’est pratiquement jamais testé, le manque de mémoire étant une condition extrême que le développeur est très peu susceptible de rencontrer en pratique (encore moins sous GNU/Linux, où le comportement par défaut du système est de ne pas refuser la plupart des allocations quelque soit la mémoire réellement disponible, préférant compter sur l’OOM killer pour libérer de la mémoire en cas de besoin) et difficile à provoquer délibérément (à moins sans doute d’utiliser son propre allocateur, instrumentalisé pour simuler des erreurs d’allocation à la demande — beaucoup d’efforts pour tester une situation exceptionnelle). On risque donc de se retrouver avec une gestion du manque de mémoire que l’on pense « gracieuse »… et qui s’avère en réalité bancale, parce que truffée de bogues jamais mis en évidence.
Un argument parfois avancé pour justifier les efforts nécessaires à une gestion gracieuse des erreurs d’allocation est que le programme doit éviter de faire perdre à l’utilisateur son travail en cours. D’après moi, cet argument ne tient pas : si l’on tient à préserver à tout prix, à tout moment, le travail de l’utilisateur, attendre qu’une erreur survienne et tenter à ce moment‐là de sauver les meubles est une mauvaise stratégie. Enregistrer régulièrement le travail en cours (pas à la demande de l’utilisateur, mais en arrière‐plan) est une solution beaucoup plus robuste, qui permet de faire face non seulement à un soudain manque de mémoire, mais aussi à toutes sortes de situations pas forcément prévisibles ou évitables (une coupure de l’alimentation, par exemple).
Avorter en cas d’erreur
À l’opposé de la gestion gracieuse, il y a l’approche consistant à terminer immédiatement le programme à la moindre erreur d’allocation. L’idée étant que dans une situation où le système est à court de mémoire, le programme n’a de toute façon pas beaucoup de marge de manœuvre et qu’il vaut mieux tout arrêter, laisser l’utilisateur ou l’administrateur remédier au problème (en quittant Firefox, peut‐être ?) et relancer le programme après.
C’est ce que recommandent les GNU Coding Standards pour les programmes non interactifs :
If
malloc()
fails in a non‐interactive program, make that a fatal error.
Une façon particulièrement simple d’implémenter cette approche est… de ne rien faire, c’est‐à‐dire de ne pas vérifier le pointeur renvoyé par les fonctions d’allocation. On laissera simplement le programme faire un segfault tout seul lors du déréférencement d’un pointeur nul. J’appellerai ça la méthode « YOLO ».
Jens Gustedt suggère une petite variante qui consiste à appeler malloc()
ainsi :
memset(malloc(size), 0, 1);
On écrit ici immédiatement 0 au début du bloc alloué, de manière à provoquer tout de suite l’erreur de segmentation si jamais l’allocation a échoué.
Une autre façon classique d’implémenter l’avortement sur erreur est la méthode du « wrapper qui tue ». On enveloppe les fonctions d’allocation dans des wrappers qui se chargent de quitter proprement le programme en cas d’erreur (via exit(3)
, si l’on veut que les éventuelles fonctions de nettoyage mises en place par atexit(3)
soient appelées, ou plus violemment via abort(3)
). Les wrappers eux‐mêmes peuvent ensuite être appelés en mode « YOLO », sans vérifier le pointeur retourné — le simple fait que le wrapper retourne indique déjà que l’allocation s’est bien passée.
Voici le wrapper typique que j’utilise généralement dans mes programmes :
void *
xmalloc(size_t s)
{
void *p;
if ( ! (p = malloc(s)) && s )
err(EXIT_FAILURE, "Cannot allocate %lu bytes", s);
return p;
}
Ici, en cas d’erreur le wrapper affiche la quantité qu’il a tenté d’allouer (ça peut être utile à l’utilisateur pour évaluer la gravité de la situation), puis termine. La fonction err(3)
n’est pas standard, mais elle est assez répandue et peut trivialement être réécrite sur une plate‐forme qui ne la fournit pas.
On peut aussi imaginer des wrappers plus complexes, comme ceux utilisés par Git, qui en cas d’erreur tentent de libérer un peu de mémoire avant de retenter une allocation, après quoi seulement ils terminent le programme.
Le cas des bibliothèques
Selon les bibliothèques que vous utilisez dans votre programme, une décision a peut‐être déjà été prise pour vous. En effet, si certaines bibliothèques remontent les erreurs d’allocation au code appelant et vous laissent ainsi la possibilité de décider comment vous voulez réagir, d’autres ont déjà fait le choix d’avorter en cas d’erreur. Si ne serait‐ce qu’une seule des bibliothèques dont dépend votre programme a adopté cette approche, il ne sert plus à grand’chose de choisir la gestion gracieuse dans le reste de votre programme.
C’est ainsi, par exemple, que j’ai renoncé à gérer gracieusement les erreurs d’allocation dans Gfsecret, lorsque j’ai commencé à utiliser GIO pour accéder aux périphériques de stockage USB, parce que cette bibliothèque (comme, a priori, toutes les bibliothèques gravitant autour de GLib) termine abruptement le programme en cas d’erreur d’allocation.
Et vous ?
Les vieux barbus programmeurs C dans l’assistance sont invités à partager leur expérience et leur point de vue sur cette question.
Des commentaires sur les usages courants avec les autres langages sont aussi les bienvenus (par exemple, en C++ : new
ou new(std::nothrow)
?), mais on évitera si possible le « moi, j’utilise un vrai langage moderne qui ne m’oblige pas à m’occuper de ces menus détails » — sauf le vendredi.
# Mélange des deux
Posté par Renault (site web personnel) . Évalué à 10.
Personnellement, je n'utilise pas l'une ou l'autre méthode en permanence, je panache les deux dans un même programmes.
Si l'allocation est de petite taille, type doit stocker une chaîne de caractères de taille modeste genre moins de 1 Kio, l'approche de considérer par avance que l'allocation a réussi comme pertinente. Si cela n'est pas le cas, cela crashera.
Après tout, si ton programme a vraiment peu d'espace de disponible, les possibilités d'actions sont réduites. Donc crasher ou tenter de faire quelque chose ne donnera rien de bien pertinent.
Dans le cas où la taille est plus volumineuse potentiellement, genre le résultat d'un fichier en entrée, l'approche de gérer proprement l'erreur est meilleure. L'application peut fonctionner malgré cet échec, en utilisant un fichier de taille plus modeste en entrée par exemple. Il faut cependant indiquer à l'utilisateur l'erreur et comment y remédier si possible.
Après aussi, tout dépend du programme aussi. Si c'est un petit programme de test / prototype ou un produit industriel dans un domaine critique, cela peut imposer le style de fait à tout le programme.
[^] # Re: Mélange des deux
Posté par benja . Évalué à -1.
Vous recommandez d'accepter d'avoir des crash à n'importe quel moment et de gérer correctement la remonté des erreurs seulement pour certaines allocations. Si je comprends bien votre solution permet de garder les problèmes des deux approches ?
Étant donné que le journal pose la question, je suppose qu'il cherche une solution… De plus l'objet d'un prototype peut être, aussi, de valider la gestion des erreurs.
[^] # Re: Mélange des deux
Posté par barmic . Évalué à 5.
Plutôt une discussion qu'une solution. S'il existait une solution, on en parlerait pas :)
Tous les contenus que j'écris ici sont sous licence CC0 (j'abandonne autant que possible mes droits d'auteur sur mes écrits)
[^] # Commentaire supprimé
Posté par Anonyme . Évalué à 1.
Ce commentaire a été supprimé par l’équipe de modération.
[^] # Commentaire supprimé
Posté par Anonyme . Évalué à 2.
Ce commentaire a été supprimé par l’équipe de modération.
[^] # Re: Mélange des deux
Posté par pulkomandy (site web personnel, Mastodon) . Évalué à 1.
Uniquement sous Linux! Il y a des OS qui fonctionnent différemment.
Et encore, même sous Linux, c'est configurable, et il vaut mieux le configurer correctement, parce que sinon, on joue à la roulette russe avec l'OOM Killer.
[^] # Re: Mélange des deux
Posté par Ely . Évalué à 1.
Sur le sujet, je me pose une question depuis quelques mois. Ça fait maintenant un bout de temps que je n'utilise plus de partition swap.
Cependant, je remarque que dans les rares cas où j'ai une application avec fuite mémoire qui se met à prendre 100% de ma RAM, le programme ne crash pas et mon disque dur se met à plein régime.
Ça signifie que malgré tout, le kernel écrit de la RAM sur mon DD bien que je n'ai pas de swap ! Quelqu'un pourrait-il m'expliquer ce qu'il se passe ?
[^] # Re: Mélange des deux
Posté par jyes . Évalué à 5.
La mémoire non utilisée par les programmes en cours d’exécution est utilisée notamment pour le cache des accès disque. Si tu commences à saturer l’utilisation de ta mémoire, pour en libérer il faut effectuer les écritures sur le disque qui étaient dans le cache afin de réduire celui-ci. Du coup, même sans swap, il faut écrire sur le disque en cas de pointe de consommation de mémoire.
Exemple sur mon portable :
6976 Mo de RAM utilisés, mais en fait seuls 2311 restent si l’on enlève les cache et buffers qui peuvent être libérés en cas de besoin.
[^] # Re: Mélange des deux
Posté par ymorin . Évalué à 7.
Et ça va même au-delà : les pages
.text
(le code du programme) sont, dans la très grande majorité des cas, non-modifiées, donc ce qu'il y a en mémoire est exactement ce qu'il y a sur le disque. Le noyau va donc libérer ces pages lorsqu'il doit trouver de la place, puisqu'il sait pouvoir les récupérer depuis le disque. Ce qui fait que dès qu'un programme va être exécuté, ses pages ne sont peut-être plus en mémoire, et donc le noyau va les recharger depuis le disque, d'où activité du disque. Pire, pour recharger ces pages, il va peut-être devoir en libérer d'autres, d'un autre programme, qui lorsqu'il sera de exécuter à son tour, verra ses pages rechargées depuis le disque. Et ainsi de suite.Donc, même sans swap, une condition de
low memory
peut entraîner une forte activité du disque.Autant dire que les performances du système vont en souffrir grandement…
Hop,
Moi.
[^] # Re: Mélange des deux
Posté par Ely . Évalué à 1. Dernière modification le 04 novembre 2016 à 20:28.
Je suis bien au courant des caches du DD en RAM, ce que je ne comprends pas c'est comment mon programme peut continuer d'exister (et d'allouer de la RAM alors qu'elle est déjà pleine). Et quand je dis pleine, je veux dire que 100% est utilisée par des allocations mémoire, pas "semi-pleine" à cause des buffers DD.
edit : nvm, la réponse de ymorin à ton commentaire explique le phénomène.
[^] # Re: Mélange des deux
Posté par foobarbazz . Évalué à 1.
Regarde quel process écrit avec atop ou iotop.
Mon hypothèse, c'est qu'un gros programme genre firefox détecte qu'il n'y a plus de ram et se met à déplacer du cache sur le disque ou quelque chose comme ça.
[^] # Re: Mélange des deux
Posté par gouttegd . Évalué à 5.
Pas vraiment en fait, c’était plus un journal « exploratoire ». Présenter le problème et quelques-unes des solutions possibles, et voir ce que les autres programmeurs du coin ont à dire.
Au passage, une précision : comme le journal est construit (en gros ① on peut gérer gracieusement les erreurs d’allocation, ② mais c’est compliqué, ③ on peut aussi juste terminer en cas d’erreur vu que de toute façon avec un système à genoux on ne peut pas faire grand’chose), il donne peut-être l’impression que je recommande la terminaison immédiate sur erreur, ou que je considère ça comme « la bonne solution ».
Ce n’était pas mon intention. J’adopte généralement la méthode du « wrapper qui tue » dans mes programmes, parce qu’ils rentrent à fond dans la catégorie des programmes non-interactifs qu’on lance, font ce qu’ils ont à faire (ce qui typiquement prend un dixième de seconde), et terminent en rendant la main à l’utilisateur — le genre de programmes où il n’y guère d’intérêt à mon sens à essayer à tout prix de « gérer gracieusement » le manque de mémoire. Mais je ne veux surtout pas prétendre que c’est la bonne approche applicable à tous les cas de figure.
Au contraire, si je devais recommander quoi que ce soit, ce ne serait pas une méthode ou une autre, mais plutôt le fait de se poser quelques questions au début du développement (de quel type de programme s’agit-il ? quel devrait être, typiquement, le volume de données qu’il va manipuler ? est-ce un logiciel serveur supposé tourner indéfiniment, ou au contraire un programme à durée d’exécution limitée ? etc.) et de choisir la méthode de gestion du manque de mémoire en conséquence.
# undefined behaviour
Posté par louiz’ (site web personnel) . Évalué à 10.
Le problème c’est que ça va pas forcément crasher, ça peut faire des tas d’autres choses. Perso j’évite d’avoir des UB dans mon code, je préfère largement appeler abort() ou exit() dans un xmalloc.
[^] # Re: undefined behaviour
Posté par guppy . Évalué à 7.
La spécification de malloc (http://pubs.opengroup.org/onlinepubs/009695399/functions/malloc.html) indique :
Si malloc peut pas allouer, elle va donc renvoyer un pointeur nul. Je pensais qu'y accéder allait forcément entraîner un segfault, mais visiblement ce n'est pas le cas, tu as raison (voir http://stackoverflow.com/questions/12645647/what-happens-in-os-when-we-dereference-a-null-pointer-in-c). Apparemment ça a même permit des exploits fonctionnels pour récupérer les droits root.
[^] # Re: undefined behaviour
Posté par gouttegd . Évalué à 5.
J’avoue que c’est ce que je pensais aussi.
Merci pour le lien. J’avais déjà des réserves vis-à-vis de cette méthode, voilà qui finit de me convaincre que ce n’est pas une bonne idée.
Pour info, un argument que j’ai vu en faveur de cette méthode est que l’erreur de segmentation va provoquer la génération d’un core dump, qui peut être utile au programmeur pour une analyse post-mortem. Je ne suis pas vraiment convaincu, parce que je ne vois pas très bien ce qu’il y aurait de pertinent à analyser en cas de manque de mémoire (sauf éventuellement si c’est le programme lui-même qui est à l’origine de ce manque, à cause d’une consommation excessive ou d’une fuite). Au contraire, je pense que ça pourrait même compliquer l’analyse de tous les autres bugs, parce qu’alors à chaque segfault il faudrait se demander si ce n’est pas seulement dû à une allocation échouée plutôt qu’à un véritable bug.
[^] # Re: undefined behaviour
Posté par Cheuteumi . Évalué à 8. Dernière modification le 26 octobre 2016 à 22:06.
Mouais. Je voulais tester que ma config
cgroup
d’un conteneur LXC fonctionnait bien, et quoi de mieux qu’une boucle de malloc compilée avec-O0
?Ben sauf que non. J’avais beau faire des
malloc
de trouzillions de GB, jamais rien n’est arrivé.Alors du coup, je tente une approche avec
calloc
: initialisation de la mémoire. Et là magie :segfault
dès que le seuil maximal de mémoire allouable (via la configcgroup
) est atteinte.Alors je ne dev quasiment jamais en C, sauf pour besoin hyper spécifique en fait, donc j’ai peut-être posté pour ne rien dire…
[^] # Re: undefined behaviour
Posté par Aissen . Évalué à 5.
Tu as expérimenté l’overcommit et la lazy-allocation de Linux. Un peu de lecture:
http://stackoverflow.com/questions/864416/are-some-allocators-lazy
https://www.kernel.org/doc/Documentation/vm/overcommit-accounting
http://landley.net/writing/memory-faq.txt
[^] # Re: undefined behaviour
Posté par stiffux . Évalué à 1.
Merci pour ces liens qui sont très instructifs !
Je vais les lire et certainement les relire car à chaque fois je me repose les mêmes questions sur la gestion de la mémoire.
[^] # Re: undefined behaviour
Posté par pasBill pasGates . Évalué à 6.
Au contraire.
Sous Linux, si tu chopes un segfault en accédant a un NULL malloc sous Linux, tu as soit :
Sous Windows, si tu chopes un sefault en accédant à un NULL malloc :
Même situation, et aussi cela pourrait être un autre soft ayant bouffé la RAM pendant quelque secondes, et ton code n'est pas assez solide pour le gèrer
Bref, dans tous les cas, c'est super important de le savoir.
[^] # Re: undefined behaviour
Posté par benja . Évalué à 0. Dernière modification le 27 octobre 2016 à 02:43.
Tu peux nous la refaire ? C'est un peu dur à parser car tu parles de deux choses différentes à la fois: le comportement de malloc et du segfault.
IIMNM, sous linux:
malloc (en fait l'appel système brk) ne renvois jamais null, sauf si l'espace d'adresses est épuisé. malloc renvois bien null lorsque size=0 et probablement d'autres situations exceptionnelles mais il faut aller lire le code pour savoir. En pratique on peut considérer qu'il ne renvoit jamais null.
on se chope un segfault en déférençant null, sauf si on y a mappé quelque chose de manière explicite (je pense pas que l'allocateur de la glib s'amuse à faire ça dans notre dos, enfin peut-être avec les options de debug ?).
on se chope un OOM mais ça n'a aucune relation temporelle avec un malloc ou brk.Ça arrive au moment ou linux essaye d'honnorer une page-fault, soit après un dérérencement d'une adresse non encore accédée ou lors d'un swapout. Et la victime est limite tirée au sort. On dit que linux fait de l'overcommit: on peut allouer une quantité plus importante que l'espace de mémoire virtuelle (soit RAM+SWAP), il y a un problème uniquement lorsque la mémoire est utilisée… Ça j'ai entendu que windows ne le fait pas, ce qui peut être un avantage (pas de OOM…).
Care to elaborate? savoir quoi btw ? J'ai l'impression que sous linux on est foutu, on ne sait tout simplement rien faire du tout pour récupérer une situation de pénurie. Ce qui est un argument pour ceux qui ne veulent pas s'embêter avec ça… "c'est impossible à gérer alors à quoi bon s'embêter?" Un fois qu'on est au niveau zéro de la gestion, il n'est pas trop compliqué de porter sur un autre OS vu que le comportement attendu est "quelque chose plante" :P Tu peux carrément remplacer ton code par
while (malloc()==NULL) processTerminate (GetRandomPid())
pour être vraiment au niveau de linux ;-)[^] # Re: undefined behaviour
Posté par benja . Évalué à 1.
Il y a peut-ête le cas des limites dépassées (ulimit, cgroup), limites fixées par avance. Du coup ça sort du cardre de la discussion (ben oui, si on connaît d'avance la mémoire nécessaire, on n'a qu'à faire une allocation statique au début du programme…)
[^] # Re: undefined behaviour
Posté par benja . Évalué à 0. Dernière modification le 27 octobre 2016 à 03:01.
En fait non cf. message de Cheuteumi juste au dessus … :-/
[^] # Re: undefined behaviour
Posté par guppy . Évalué à 2.
Avec la config de son conteneur LXC, il a spécifié la RAM disponible pour tout son conteneur. Il doit y avoir plein d'autres trucs qui tournent, donc la mémoire dispo pour un soft particulier est non déterminable.
[^] # Re: undefined behaviour
Posté par benja . Évalué à 0.
=> Cette discussion est bel et bien dans une double voie sans issue et je ne peux que remercier tous ces lecteurs perspicaces qui ont masqué cette discussion perdue car trop "exploratoire". ;-)
[^] # Re: undefined behaviour
Posté par benja . Évalué à 1.
D'ailleurs contrairement à un sigsegv l'application ne sait rien faire, elle ne peut pas l'intercepter pour libérer de la mémoire. Je crois qu'Android à une solution particulière à ce problème, quelqu'un pour confirmer ? Il y aussi une idée d'implémenter des allocation faibles qui pourront être libérée automatiquement par le kernel (p.e. pour faire un cache applicatif), mais ça ne change rien au problème qu'une fois toutes ces weak-allocations libérée, on se retrouve dans le même cas de figure.
[^] # Re: undefined behaviour
Posté par pasBill pasGates . Évalué à 6.
ben en fait si. Tu peux marquer ton soft comme "prioritaire" histoire que OOM killer aille massacrer quelqu'un d'autre. Mais faut être admin.
[^] # Re: undefined behaviour
Posté par benja . Évalué à 0.
Dans ce cas tous les autres logiciels vont faire pareil.
[^] # Re: undefined behaviour
Posté par louiz’ (site web personnel) . Évalué à 2.
Ben… non, si t’es admin tu décides quel process est prioritaire et lequel ne l’est pas.
[^] # Re: undefined behaviour
Posté par benja . Évalué à 0. Dernière modification le 27 octobre 2016 à 21:17.
Hein ? Tu as la doc de soft A qui dit "veuillez donner ce privilège à cette application pour assurer son fonctionnement correct", tu as le soft B qui dit la même chose, tu fais quoi maintenant ? Variante : tu fais comment pour faire tourner deux instances du soft A ?
[^] # Re: undefined behaviour
Posté par Renault (site web personnel) . Évalué à 2.
L'OOM Killer est un score, l'admin système peut moduler ce score sur une échelle d'environ 20000 pour chaque processus de son système.
Bref, il y a largement de quoi gérer les cas critiques…
[^] # Re: undefined behaviour
Posté par pasBill pasGates . Évalué à 8.
tu utilises tout ton espace d'addresse sous Linux, et ton malloc se ramasse à cause de cela. Tu ne check pas le NULL, te choppes un segfault immédiatement à l'accès. Tu regardes le core dump, et tu vois que tu faisais des mappings de fichier et oubliais de les libèrer, ou que tu ne faisais pas gaffe qu'un des fichiers que tu essaies de mapper est énorme et bouffe tout l'espace…
sous Windows, ton malloc se ramasse car le processus d'à côté a décidé temporairement de tout allouer mais libère la plus grande partie 3 secondes après. Ton soft fait son malloc au milieu, se ramasse, tu regardes le crash dump et te rend compte que tu as oublié de gèrer le fait que malloc peut retourner NULL pour des raisons tout à fait valides sur une des plateformes que tu supportes.
[^] # Re: undefined behaviour
Posté par benja . Évalué à 1.
OK merci c'est plus clair.
[^] # Re: undefined behaviour
Posté par Guillaume Knispel . Évalué à 4.
Comment tu peux consommer trop d'espace pour un malloc avec des mapping de gros fichiers? Je veux dire, techniquement c'est possible très indirectement (par des ressources RAM consommées indirectement pour la gestion des dits mapping), et en combinant avec pleins d'autres allocations sur le système. Mais invoquer des mapping de fichier en soit et comme unique cause, c'est très curieux…
EDIT: à la réflexion tu parles surement de consommer tout l'espace virtuel en 32 bits? le 32 bits c'est tellement satan que j'y pense plus du tout apparemment :p
[^] # Re: undefined behaviour
Posté par pasBill pasGates . Évalué à 1.
La plupart des puces ARM sont 32bit donc bon, pas trop possible d'oublier :)
[^] # Re: undefined behaviour
Posté par Zenitram (site web personnel) . Évalué à 3.
En pratique tu peux avoir l’entrée avec une variable "pas de chance calcul foireux" qui va faire un malloc(0) ou un malloc (-1) ou une taille trop grosse pour la RAM disponible, et tu dis toi-même que ça renvoie null dans ce cas, donc c'est incohérent de dire "en pratique jamais". Tu parle d’exceptionnel pour des cas pas si exceptionnels, des fois avec des simples entrées on peut arriver à faire de grosses demande de RAM (perso un soft par exemple comme FFmpeg me jette car je lui demande de traiter de la 4K en lossless et il essaye d'allouer un peu trop de buffers pour ça, hop "out of memory" merci la gestion autre que "m'en fout").
Le seul cas où tu pourrais dire "jamais" est si "j'appelle malloc avec une constante dont je sais qu'elle est inférieure à la taille de mémoire maximale disponible pour un process, minoré de toute la mémoire que j'ai déjà consommé" si je comprend bien (et si c'est le cas, c'est vachement différent de "en pratique jamais"). Contre-argument?
Note que je ne dis pas qu'il faut toujours gérer car ça arriverai tout le temps (par exemple pour un prototype, un dev rapide, peu utilisé, ben ça coûte cher à gérer et pas forcément rentable), mais "en pratique jamais" me parait bien présomptueux.
[^] # Re: undefined behaviour
Posté par David Marec . Évalué à 1.
… ou par un
realloc
, qui prête plus le flanc à ce genre de boulettes.Ce qui fait que le
xmalloc
proposé par l'auteur du journal va retourner un pointeur null.Du coup, le wrapper proposé ne résout pas le problème.
[^] # Re: undefined behaviour
Posté par gouttegd . Évalué à 2. Dernière modification le 27 octobre 2016 à 16:36.
Le wrapper va en effet retourner
NULL
si la taille demandée est 0.Je pense que s’il y a une possibilité légitime pour que le paramètre passé à
xmalloc
soit 0 (par exemple parce que ce paramètre dépend, d’une manière ou d’une autre, d’une donnée extérieure au programme), c’est un cas à gérer explicitement. Le wrapper ne se substitue pas à la validation des entrées. Il ne me semble pas pertinent de traiter de la même façon une entrée incorrecte et une situation de manque de mémoire.Et s’il n’est pas légitime que le paramètre soit 0 (par exemple s’il ne l’est qu’à cause d’un « calcul foireux » en amont), alors c’est un bug, et le rôle du wrapper n’est pas de t’en prémunir. (A priori ce serait plutôt le rôle d’un
assert(size != 0)
.)[^] # Re: undefined behaviour
Posté par lasher . Évalué à 3.
Je suis globalement d'accord avec ce que tu racontes.
Cependant lorsque tu dis
C'est justement là où une gestion de type
xmalloc
est utile non ? Comme ça tu as au moins une idée d'où tu t'es planté, etc. D'ailleurs c'est peut-être là où une implémentation utilisant des macros peut s'avérer utile. Quelque chose du genre:Enfin, de façon générale, la norme dit que malloc peut renvoyer
NULL
, et donc si on veut faire du code portable, il ne faut pas s'appuyer sur ce que fait Linux seulement. Même si on ne veut être « que » portable POSIX, ce n'est pas raisonnable. Ça ne veut pas dire forcément se faire chier avec une gestion complexe des erreurs, mais dans le cas de prototypes/dév rapide que tu évoques, le code ci-dessus me semble amplement suffisant (et tu peux l'inclure dans un header de typeutil.h
et puis c'est marre), mais il reste utile à des fins de debug (note que je sais que tu n'as pas dit que c'était inutile, mais j'étends un peu ta réponse avec mon expérience :)).[^] # Re: undefined behaviour
Posté par gouttegd . Évalué à 5.
Ce n’est pas figé dans le marbre. Le comportement est configurable par l’administrateur du système, donc même dans le cas d’un programme qui n’ambitionne pas d’être portable sur un autre OS il vaut mieux ne pas compter sur le comportement par défaut.
Pour mémoire, les comportements possibles sont :
[^] # Re: undefined behaviour
Posté par benja . Évalué à 1.
Ah cool je ne savais pas. Effectivement ça a l'air d'être ça, documenté dans vm.txt, sysctl vm.overcommit_memory.
[^] # Re: undefined behaviour
Posté par Anthony Jaguenaud . Évalué à 3.
On peut aussi avoir le comportement « normal » (des autres OS) en faisant un
echo 2 >/proc/sys/vm/overcommit_memory
cf:man 5 proc
et chercher overcommit_memory.C’est une bizarrerie de Linux qui a failli me couter cher sur un projet embarqué avec une quantité limité de RAM, heureusement, j’avais fait un man malloc et j’avais lu ça. Désactivation au Boot pour pas avoir de surprise.
Je trouve logique qu’un logiciel comme
GIMP
,libreoffice
me dise je n’ai pas réussi à ouvrir le fichier par manque de RAM plutôt que d’avoir un logiciel aléatoire qui est tué quand j’ouvre un fichier. Même si dans la pratique, c’est rare.[^] # Re: undefined behaviour
Posté par Nicolas Boulay (site web personnel) . Évalué à 3.
L'overcommit part du principe que toute la mémoire allouée n'est pas utilisé. Ainsi l'OS n'alloue réellement la mémoire qu'au premier accès, cela ajoute aussi un lissage d'allocation dans le temps.
"La première sécurité est la liberté"
[^] # Re: undefined behaviour
Posté par Alek_Lyon . Évalué à 1.
Il m'est arrivé la même chose sur un projet embarqué.
Cependant je n'ai pas pu le désactiver: il n'y avait alors plus assez de RAM disponible.
L'explication :
Certaines pages mémoires sont partagées par de nombreux processus, mais peuvent être dupliquées à tout moment si l'un d'eux décide d'y écrire.
Le noyau ignore quelles pages vont devoir être dupliquées ; sans overcommit il réserve autant de pages que nécessaires pour le pire des cas, celui où tous les processus décident d'écrire dans ces pages initialement partagées.
Concrètement, si un programme utilisant 64Mo de RAM fait un fork(), sans overcommit, le noyau provisionnera une quantité équivalent, quant bien même le nouveau processus se contenterait de faire un printf("Hello world") puis un exit(), ce qui ne nécessite guère que quelques pages mémoire réellement nouvelles par rapport au processus parent.
Je ne suis pas certain à 100% de cette explication, la réalité est sûrement plus complexe, mais il est vraisemblable que désactiver l'overcommit conduise à réduire la quantité de RAM réellement disponible pour les programmes.
[^] # Re: undefined behaviour
Posté par Nicolas Boulay (site web personnel) . Évalué à 2.
En cas de fork, cela se passe exactement comme tu le décris. Il y a un mécanisme de COW sur les pages de data du nouveau processus.
"La première sécurité est la liberté"
[^] # Re: undefined behaviour
Posté par Guillaume Knispel . Évalué à 1.
D'ailleurs le modèle fork/exec pour spawner des nouveaux process est "moisi" entre autre à cause de ça. Enfin bon c'est moins grave maintenant que la RAM est assimilable à +oo même sur des ordis pas trop cher, et que l'overcommit de Linux a un comportement décent.
[^] # Re: undefined behaviour
Posté par Tonton Th (Mastodon) . Évalué à 7.
Tu peux expliquer un peu ce moisi, et que proposes-tu à la place ?
[^] # Re: undefined behaviour
Posté par lasher . Évalué à 3.
Sauf qu'en fait non,
malloc
appellebrk
pour les allocs mémoire de moins d'une page, mais il appellerammap
si ça dépasse la taille d'une page. Donc tout dépend du type d'allocation mémoire dont on cause.Oui et non. Il existe des softs qui se proposent de « surcharger » les allocations mémoire (par exemple un ancien collègue qui écrivait une bibliothèque de threads pour du calcul haute performance). Mon collègue utilisait les fonctions de « surcharge » de malloc pour rajouter des comportements spécifiques au moment de l'allocation mémoire. Dans ce cas on tombe sur des trucs rigolos, du genre tout un tas de logiciels GNU qui allouent presque tout l'espace mémoire adressable dans le processus car ils tablent sur l'allocation optimiste de Linux (en l'occurrence, c'est arrivé avec
sort(1)
). Du coup dans l'allocateur « normal » de Linux,sort
te trie correctement les chaînes de caractères sans segfault car seules les pages réellement utilisées sont allouées par le système. Mais avec l'allocateur de mon collègue, tout à coup on avait desabort()
et segfaults parce qu'il s'assurait que la mémoire était réellement allouée en initialisant au moins un octet pour chaque page théoriquement allouée1.Tout un tas de softs avec des objectifs de performance proposent leurs propres ajouts (si ce n'est leur propre allocateur) aux mécanismes de gestion de mémoire de l'OS (SGBD, environnement d'exécution multithreadés, etc.).
en partie pour garantir autant d'allocation de pages mémoires physiquement contiguës que possible. ↩
[^] # Re: undefined behaviour
Posté par barmic . Évalué à 3.
Au final, j'ai l'impression que c'est assez simple. Mis à part des cas précis où tu as des comportements lors des limites (grosse allocation pour ouvrir une entrée, gestion d'un cache, algorithme paramétrable,…). Tu n'a aucune espèce d'idée si ton malloc échoue ou pas et de quoi faire de bien pertinent (retenter perpétuellement n'a pas grand intérêt). Loguer si tu peux, dumper ton travail si c'est pertinent et s'arrêter et la seule solution à peu près générale, il me semble.
Tous les contenus que j'écris ici sont sous licence CC0 (j'abandonne autant que possible mes droits d'auteur sur mes écrits)
[^] # Re: undefined behaviour
Posté par lasher . Évalué à 3.
Ben disons que je pars du principe qu'il serait pas mal de faire une gestion un minimum correcte en cas de
malloc
qui échoue. Le minimum, c'est de planter « gracieusement ». Après, si tu es capable de faire une gestion plus fine, c'est cool. Dans tous les cas, supposer quemalloc
renvoie toujours un pointeur valide, c'est estimer que ton soft n'a pas besoin d'être maintenu au-delà de (1) Linux, et (2) une période de temps donnée (qu'est-ce qui nous dit que dans 5 ans, Linux ne va pas revenir sur sa politique optimiste actuelle, avec un nouvel algo top et plus précis?).Ben y'a certaines choses que tu peux faire : tu peux distinguer entre les ressources critiques et les autres. Si ton soft a absolument besoin de toutes les structures de données, alors tout est critique, et tu plantes. Sinon, peut-être que tu peux désactiver certaines fonctionnalités temporairement (à moins que tu aies réellement atteint les limites de la mémoire disponible, et dans ce cas c'est plus temporaire…). De façon générale, si tu en arrives à mettre le système à genoux niveau mémoire par processus, et surtout si on parle de systèmes 64 bits, je me dis que quelque chose ne va pas (et oui, l'exemple de Firefox en est un bon).
[^] # Re: undefined behaviour
Posté par gasche . Évalué à 5.
Pas besoin de provoquer un segfault pour obtenir un core dump, il suffit d'appeler
abort()
.# Langages modernes
Posté par guppy . Évalué à 10.
Je vois pas pourquoi tu imagines que les utilisateurs de "langages modernes" peuvent se passer de ce genre de considération. Que fera la jvm (par exemple) si elle exécute un algo qui a besoin d'allouer 1 To si la machine peut pas lui fournir ? C'est pas une question rhétorique d'ailleurs, j'en sais rien, mais ce que je sais ce qu'elle fait pas de magie, donc elle y arrivera pas.
Personnellement, dans mes softs (c++), je ne capture pas std::bac_alloc. Le prof qui m'a enseigné C++ disait à tort ou à raison que c'était inutile, parce si cette exception était déclenchée, on pouvait plus faire grand chose qui est un sens. Juste créer la std::string qui va contenir le message à destination de l'utilisateur risque fortement d'échouer par exemple (selon l'implémentation et l'espace restant par exemple) Quelques (trop ;p) années plus tard, je sais que cette affirmation doit être nuancée, mais dans mon cas précis (développement de softs dits "de gestion") elle reste vraie je trouve : les algos que j'écris ne doivent pas la déclencher, tout simplement parce qu'ils ne doivent utiliser que des quantités ridicules de mémoire. Imaginez je ne sais pas moi un crm. Si un std::bad_alloc est déclenché sur une machine moderne avec plusieurs Go de RAM, c'est qu'il y a un gros problème de conception. Faut pas chercher comment capturer et repartir, faut surtout chercher les fuites.
J'imagine que les avis vont être différents sur ce journal. Les types qui développent par exemple des outils de calcul dans la recherche vont peut-être avoir besoin d'allouer des Go, et captureront std::bad_alloc. Les types qui écrivent des applis de gestions (qui en général délèguent les opérations coûteuses à un SGBDR) ne s'en soucieront sûrement pas.
Ceci dit, je trouve ce journal très intéressant, car même si dans mes softs je ne m'en soucie pas (par pragmatisme), il y a toujours ce petit truc qui gratte. Je préférerai bien évidemment qu'ils marchent coûte que coûte.
Dernière chose, je vais quand même décrire une technique que j'utilise depuis quelques années pour résoudre une partie du problème : j'utilise le RAII au maximum. Une classe qui va allouer le fera dans son constructeur. Si ça échoue, un std::bad_alloc va donc être déclencher. Vu que j'utilise le RAII, je m'attends déjà à ce que le constructeur échoue et le code appelant capture déjà les exceptions, donc si jamais std::bad_alloc était déclenchée elle serait capturée par le niveau du dessus qui gère déjà l'échec de la construction. Je pense donc que même un std::bad_alloc serait géré proprement, même si c'est pas garanti (pas impossible que le code du catch alloue des trucs et du coup relance un std::bad_alloc).
A vrai dire et en conclusion, mes softs sont utilisés un peu partout en France depuis 10 ans par des centaines ou des milliers de gens, si des std::bad_alloc avaient été déclenchés, je serai déjà au courant et on m'aurait demandé de trouver une solution. Ce n'est pas le cas.
[^] # Re: Langages modernes
Posté par Antoine . Évalué à 6.
Je ne sais pas ce que fait la JVM, mais Python, par exemple, lèvera une belle exception MemoryError qui provoquera l'affichage de la pile d'appels et la terminaison du programme (sauf si le programme attrape l'exception au vol mais, dans le cas de MemoryError, cela ne me semble pas vraiment souhaitable).
Je ne pense pas qu'il s'agit de faire en sorte que les programmes « marchent coûte que coûte » (c'est un combat perdu d'avance) mais simplement que le retour d'erreur soit clair et informatif, plutôt que de risquer un plantage ou un comportement erroné, ce qui est possible en C.
[^] # Re: Langages modernes
Posté par benja . Évalué à -2.
Non ça n'est en pratique pas possible sous linux, désolé. Cf. commentaires plus haut.
[^] # Re: Langages modernes
Posté par Antoine . Évalué à 5.
Qu'est-ce qui n'est « pas possible sous Linux » ? Je suis désolé, sous Linux on peut très bien récupérer un NULL face à un malloc(). Exemple en Python (allocation d'un tableau Numpy de 32 Go alors que j'ai 8 Go de RAM et pas de swap) :
[^] # Re: Langages modernes
Posté par benja . Évalué à 0. Dernière modification le 27 octobre 2016 à 12:49.
Tu as juste prouvé que python ou numpy retourne une erreur, pas linux ni la libc (malloc). Si tu prennais la peine de lancer ton programme avec strace ou ltrace tu ne trouverais rien.
Je te le fais en ocaml si tu veux :
[^] # Re: Langages modernes
Posté par Frédéric Péters (site web personnel) . Évalué à 4.
Les 34359742464 mentionnés là sont les 32 * (1024 ** 3) demandés à numpy (et 4Ko d'overhead).
[^] # Re: Langages modernes
Posté par benja . Évalué à 3. Dernière modification le 27 octobre 2016 à 15:01.
Ok merci de m'avoir corrigé ! Alors c'est effectivement du au vm.overcommit_memory = 0 mentioné par gouttegd (i.e. une "grosse" allocation est rejettée).
Si on remplace le code par le suivant qui au lieu d'allouer 32 G en un coup alloue 32 fois 1G, il n'y a pas d'erreur. De même si on met le overcommit_memory à 1.
Donc "pratiquement", on n'a pas d'erreur sous linux…
(Aussi content de savoir que numpy ne fait pas de memset par défaut, c'eût été un peu idiot).
[^] # Re: Langages modernes
Posté par Antoine . Évalué à 2.
En effet. Je ne sais pas pourquoi c'est à 0 ici, mais je ne me souviens pas avoir changé le réglage, donc c'est peut-être la valeur par défaut sous Ubuntu 16.04 ?
Numpy fournit
np.empty
etnp.zeros
, le premier n'initialise pas le tableau, le deuxième l'initialise à zéro.[^] # Re: Langages modernes
Posté par Kalenx . Évalué à 2.
En fait, c'est encore mieux : np.zeros est un cas à part. Il "initialise" la mémoire à zéro, mais le kernel garde toujours un page remplie de zéros sur laquelle il peut faire du copy-on-write. Si bien que tu peux sans problème appeler np.zeros, sans overhead particulier par rapport à np.empty (sur Linux du moins).
On ne peut pas en dire autant de np.ones par contre…
[^] # Re: Langages modernes
Posté par arnaudus . Évalué à 8.
En mode "my life", c'est un peu ce qui m'arrive de faire, et je ne capture pas les exceptions. Évidemment, ça dépend du contexte (est-ce que c'est un soft largement distribué ou est-ce un code one-shot pour un projet en interne, etc), mais j'ai l'impression que le cas général dans la communauté scienifique, c'est qu'on cherche plus un comportement reproductible qu'une robustesse à toute épreuve. Si en fonction du contexte, ton soft utilise des bouts de code qui ne sont exécutés qu'en cas de bad_alloc, tu n'as aucune garantie que de refaire tourner ton calcul le lendemain après avoir pourri l'étudiant malotru qui osait avoir eu l'idée de faire un calcul sur le cluster en même temps que toi te donnera le même résultat.
En tout cas, à mon niveau et pour ce que je fais, le comportement souhaitable reste un abort() violent avec éventuellement un message d'erreur, que ce soit pour un problème d'allocation mémoire, d'accès disque en lecture/écriture, d'accès réseau, ou n'importe quel truc qui ne fonctionne pas exactement comme il le devrait. Tu rends la main, l'utilisateur résoud le problème, et relance le truc. D'ailleurs, de toutes manières, quand il y a des problèmes de mémoire et que tu fais partie des coupables, en général il y a plus de chances de se faire shooter par l'OOM killer que de lever toi-même une bad_alloc.
Ceci dit, je conçois tout à fait que ce n'est pas forcément non plus ce que tu souhaites si tu développes des logiciels utilisés par des milliers de chercheurs, ou sur des dispositifs embarqués, ou sur des appareils dont l'utilisation coûte des milliers d'euros à la minute, ou sur des trucs de calculs intensifs qui prennent 60 jours à tourner sur des clusters à 200 nœuds. C'est juste que pour beaucoup d'applications scientifiques, je pense qu'il est préférable d'éviter les comportements créatifs en cas de problème.
[^] # Re: Langages modernes
Posté par barmic . Évalué à 6.
La JVM c'est particulier :)
La JVM a 2 paramètres de configuration le Xms et le Xmx. Ce sont des paramètres de la JVM pas de ton programme (ton programme peut les voir, mais pas les modifier). Il définissent la taille minimale et maximale de mémoire que ta JVM va pouvoir allouer. au dessus toute allocation va échouer. Il va y avoir une erreur1
OutOfMemomryError
qui va être lancée. Avant que cela arrive, tu aura une exécution du full-gc qui aura tenté de libérer (et réorganiser) de la mémoire pour toi. Le message d'erreur généralement associé est assez intéressant :GC overhead limit exceeded
. Cela signifie que le full-gc s'exécute sans arriver à libérer significativement de mémoire (on passe trop de temps à exécuter le gc plutôt que ton programme).Il y a une autre partie la mémoire out of heap, mais c'est pas commun. Quand tu fais ce genre de choses tu gère toi-même l'allocation et la libération de la mémoire. Cette mémoire n'est pas limité par le Xmx.
Finalement, je trouve l'approche pas si éloignée2 de ce que certains font ici. Tu fais une distinction entre tes allocations « classiques » (→ de petites) qui ne doivent pas échouer et les allocations grandes (pour ne pas dire énorme) que tu va gérer manuellement.
Une erreur c'est comme une exception d'un type différent. En java, il y a les
Throwable
s qui se divisent enError
et enException
. ↩Bien sûr je ne dis pas que c'est identique. Ici il y a un gc, etc. ↩
Tous les contenus que j'écris ici sont sous licence CC0 (j'abandonne autant que possible mes droits d'auteur sur mes écrits)
# Approche hybride
Posté par pasBill pasGates . Évalué à 10. Dernière modification le 26 octobre 2016 à 21:48.
Perso c'est simple, je considère que ne pas gèrer un NULL retourné par malloc est une faute grave si malloc peut retourner NULL.
Pourquoi ? Parce que le comportement de malloc entre Linux et d'autres OS est diffèrent, et que si tu ne gères pas le cas, alors ton soft va exploser en vol sur d'autres plateformes.
Exemple tout simple : code qui est sensé être multiplateforme entre Windows et Linux, et le code fait une allocation de 256Kb.
256Kb c'est petit hein, c'est genre rien du tout.
Pourtant sur Windows, si ton soft tourne alors qu'un soft super vorace tourne en background, il pourrait tout à fait se retrouver dans la situation ou il n'y a effectivement pas 256Kb de mémoire dispo, car Windows ne tue pas les processus au hasard pour faire de la place. Ce pourrait être juste quelque secondes parce qu'un soft a décidé de faire une énorme allocation qu'il libère tout de suite après hein… et là malloc retournera NULL sous Windows, là ou Linux serait allé trouver un processus à tuer pour faire de la place.
Est-ce que laisser le soft exploser en vol parce qu'il n'y avait pas 256Kb de RAM dispo pendant qqe secondes est une bonne chose ? A mon avis non, c'est très mauvais.
Donc perso l'approche que j'utilises est la suivante :
J'ai un allocateur custom, qui ne retourne JAMAIS NULL pour les allocations plus petites qu'une certaine limite (genre 1Mb ou 4Mb, selon les besoins). Il se contente de ré-essayer avec une petite pause car le mode de pensée est que si le système n'a pas 1-4Mb de dispo, ce ne sera pas indéfiniment sinon le système entier est à plat.
Résultat, dans tout mon code, je n'ai des checks pour NULL que lorsque je sais que la taille allouée peut potentiellement être plus élevée. Dans les autres cas, mon allocateur prendra peut-être 30 secondes à retourner (mais c'est pas grave car si le système est à genoux niveau RAM, il ne ferait rien de toute facon), mais il ne retournera pas NULL.
L'avantage est que je n'ai pas besoin de me soucier de mettre des exception handlers pour la plupart de mes utilisations de STL, ou des NULL checks pour la plupart de mes allocations. Il n'y a qu'un très faible nombre de cas ou j'ai besoin de le faire, et l'approche permet au code de rester lisible et supporter plusieurs OS proprement.
[^] # Re: Approche hybride
Posté par Antoine . Évalué à 3.
Et que se passe-t-il si l'allocateur échoue toujours à allouer la taille demandée ? Le programme tourne en rond ? L'utilisateur doit lancer un débuggeur pour comprendre ce qui se passe ?
[^] # Re: Approche hybride
Posté par benja . Évalué à 5.
Pas possible vu qu'il n'y a plus d'allocation possible sur tout le système.
[^] # Re: Approche hybride
Posté par pasBill pasGates . Évalué à 8.
vu qu'aucun soft n'est capable d'allouer (rappel: je ne fais cela que pour des allocations plutôt petites, si elles échouent systématiquement sur le long terme, c'est que plus grand chose ne fonctionne sur le système), ben l'utilisateur ne fera rien car le système sera gelé de toute facon… ssh / bash / RDP ne pourra rien allouer pour permettre à l'utilisateur de se connecter ou lancer des commandes, etc… donc bref, ca ne changera rien.
[^] # Re: Approche hybride
Posté par Antoine . Évalué à -1.
Et donc, c'est un comportement souhaitable selon toi de geler le système de l'utilisateur au lieu de lui afficher un message d'erreur clair et précis ? L'utilisateur est censé faire quoi si sa machine gèle aléatoirement quand il utilise ton logiciel ?
Voyons:
1 - tu ne fournis aucune information à l'utilisateur permettant de savoir ce qui se passe
2 - tu gèles le système de l'utilisateur, empêchant toute autre action
3 - alors qu'au moins, si ton programme avait la décence de se terminer sur une erreur d'allocation, cela libérerait de la mémoire pour les autres programmes, et permettrait à l'utilisateur de sauvegarder ses données et / ou continuer son travail avec ces programmes
Dans le registre des comportements nuisibles, je dois dire que c'est assez fort.
[^] # Re: Approche hybride
Posté par CrEv (site web personnel) . Évalué à 9.
De ce que j'ai lu ce n'est pas du tout ce qui est écrit surtout.
Ce qu'il dit c'est que si l'OS ne peut pas te faire une petite alloc c'est que ton système est déjà gelé et que plus rien n'est dispo. Ce n'est pas son programme qui gèle c'est le système.
Et d'ailleurs comment allouer de la mémoire pour des strings, ui, etc s'il n'y en a plus ?
[^] # Re: Approche hybride
Posté par Antoine . Évalué à 2.
Un autre message donne la solution : on peut préallouer le message ou les structures nécessaires. Ainsi, en Python, un certain nombre d'instances de l'exception MemoryError sont préallouées, de façon à ce qu'on puisse lever l'exception sans partir en vrille.
Par ailleurs, je pense que « le système est déjà gelé » est une simplification abusive. Il n'y a pas un allocateur global, mais chaque processus a son allocateur qui peut conserver de la mémoire disponible (déjà allouée du point de vue du noyau, mais libre du point de vue de l'application faisant appel à malloc()). Cela arrive notamment si le tas est fragmenté. En fonction des heuristiques utilisées, un allocateur peut garder beaucoup de mémoire disponible qui ne sera pas allouable par d'autres processus.
Ainsi, ce n'est pas parce qu'un processus échoue à allouer même une petite zone mémoire que les autres processus ne sont pas en mesure de continuer à fonctionner.
[^] # Re: Approche hybride
Posté par CrEv (site web personnel) . Évalué à 2.
Ok. Donc l'idée si ça fail d'attendre un poil et de recommencer en espérant que quelqu'un (ou le système) ait libéré de la mémoire est plutôt bonne alors. Ça permet d'être tolérant face à un système qui ne donne pas la bonne (attendue) réponse immédiatement.
[^] # Re: Approche hybride
Posté par benja . Évalué à 0. Dernière modification le 27 octobre 2016 à 13:03.
Donc python met un handler sur sigsegv et lance une MemoryError. Il ne dit pas qu'il y a une pénurie de mémoire, ça pourrait être un bug dans le runtime ou dans une bilbiothèque. C'est comme ça qu'on peut faire en C aussi hein (vu que malloc ne retourne pas NULL sous linux, bis repetita), c'est juste très difficile à faire car on doit être certain que le path d'erreur ne fera pas d'allocation et, qui plus est, on ne sait pas à quel endroit le programme se trouve au moment où ça pète. (je me demance ce qui se passerait en python si tu faisais une alloc dans ton bloque catch…)
[^] # Re: Approche hybride
Posté par benja . Évalué à 2.
Précision: plus qu'être certain de ne pas faire d'allocation, il ne faut pas accéder à de la mémoire non précédemment accédée.
Python initialise la mémoire au moment de l'allocation (calloc()/memset) pour faire un fail-early, mais il ne règle pas le problème du path d'erreur. Est-il simplement possible d'écrire du code en ayant la certitude qu'il n'y ai pas d'allocations implicites ou "cachées" ?
[^] # Re: Approche hybride
Posté par Antoine . Évalué à 2.
Non, il se contente de récupérer NULL sur
malloc()
oummap()
. Un handler sur sigsegv serait trop délicat et peu portable.Tu peux effectivement demander à Python de récupérer un sigsegv, mais seulement pour afficher une pile d'appels très sommaire et terminer le processus : https://docs.python.org/3/library/faulthandler.html
Ben si :-) Par ailleurs Python ne tourne pas que sous Linux…
[^] # Re: Approche hybride
Posté par pasBill pasGates . Évalué à 3.
De nos jours la fragmentation, pour les petites allocations, c'est fini.
Les allocateurs utilisent un systeme de free list ou un bloc est alloué, coupé en morceaux de la taille demandée, et un morceau retourné.
Le moment ou le morceau est libéré, il est mis sur la free list pour cette taille là, et quand un autre morceau de cette taille est demandé, si la free list n'est pas vide, le morceau est retourné de là plutôt qu'aller faire une allocation qqe part.
Résultat, il n'y a plus de fragmentation lors d'allocations de petite taille (sauf dans cas totalement pathologiques qui n'existent pas vraiment en réalité), les blocs restent compacts. Le seul cas ou tu as encore un tel problème est quand tu fais une énorme allocation, et tu remarqueras que mon allocateur ne fait pas de loop en cas de grosse allocation.
[^] # Re: Approche hybride
Posté par guppy . Évalué à 8.
En fait l'approche de pasBill pasGates est plutôt valide dans un cas : un poste client (mono utilisateur donc) qui utilise une seule application à la fois et où il y a une myriade de processus systèmes qui tournent. Si un de ces processus (Cortana par exemple) a besoin d'un énorme espace mémoire quelques instants et qu'il reste plus rien pour les autres, c'est pas idiot qu'ils attendent un peu (mais c'est basé sur l'espoir que le gros demandeur a besoin de son allocation très peu de temps, ce qui n'est pas forcément vrai).
Au fond c'est peut-être ça la morale de l'histoire : ce problème n'a pas de solution générale. Certains choix seront meilleurs que d'autres uniquement dans un contexte précis.
[^] # Re: Approche hybride
Posté par pasBill pasGates . Évalué à 3.
Non, c'est basé sur une autre idée :
Et c'est tout.
Partant de là, on assume que les autres processus ne vont effectivement pas bouffer TOUTE la RAM de manière permanente, auquel cas le système serait gelé de manière permanente de toute facon, et ces softs seraient coupable du gel.
[^] # Re: Approche hybride
Posté par pasBill pasGates . Évalué à 3.
T'as pas compris. Ce n'est pas mon soft qui gèle le système, c'est le système qui est gelé, peu importe la présence de mon soft ou pas.
Mon soft essaie d'allouer disons 16Kb, et fais un loop avec attente si il n'y arrive pas.
Si il n'y a pas 16Kb de dispo sur la machine de manière permanent, inutile de te dire que, avec ou sans mon soft, rien ne fonctionne sur la machine.
[^] # Re: Approche hybride
Posté par guppy . Évalué à 3.
Ton approche est intéressante, mais après réflexion elle va n'être pertinente que dans le cas que tu décris : un soft qui fait éventuellement des grosses allocations pendant pas longtemps. Effectivement, les autres à côté peuvent patienter un peu jusqu'à la RAM soit libérée.
Mais imaginons un autre cas. Ta technique se répand, tous les softs s'y mettent. Sur un serveur, on a par un exemple un serveur web qui lance un processus par connexion. Chaque processus consomme pas grand chose, mais consomme régulièrement. Il y a beaucoup de connexion, on commence à toucher les limites en terme d'allocation mémoire :
- système "classique" qui segfaulte ou aborte : le processus qui va vouloir franchir la limite va claquer, ce qui va libérer un peu de RAM pour les autres qui vont continuer. Si d'autres connexions arrivent, le système va certes tuer quelques processus, mais il en fera toujours tourner à la limite de ses capacités.
- ton système : le processus qui va franchir la limite va partir en boucle infinie, les autres qui continuent d'allouer un peu vont aussi partir en boucle infinie. Un bon gros dead lock en somme.
Ceci dit, je suis parfaitement d'accord avec ça :
Mais en C je vois pas comment faire sans utiliser la méthode canonique qui consiste à vérifier le retour de malloc() à chaque appel et avoir architecturé le soft pour qu'il soit capable de gérer un NULL proprement.
[^] # Re: Approche hybride
Posté par pasBill pasGates . Évalué à 10. Dernière modification le 27 octobre 2016 à 20:17.
Tu as bien raison !
Maintenant, la réalité en pratique est que ce n'est pas le cas et ne le sera jamais.
Mais au cas oû je deviendrais soudainement un exemple à suivre par toute l'industrie, je me contenterais de rajouter un compteur incrémenté à chaque tentative d'allocation ratée, et remis à zero en cas de succes. Si le compteur atteint un certain nombre, le soft sort proprement avec un exit().
[^] # Re: Approche hybride
Posté par Nicolas Boulay (site web personnel) . Évalué à 1.
Je viens de tomber dans une faille spatio-temporelle. J'ai plussé un poste de PasBill PasGates.
"La première sécurité est la liberté"
[^] # Re: Approche hybride
Posté par guppy . Évalué à 1.
Ta solution initiale suppose que le manque de mémoire sera temporaire et que le gros consommateur n'est pas toi. Dans ce cadre elle est très intéressante.
Mais si tu lui rajoutes une limite temporelle, il va y avoir des cas où elle va aborter alors qu'il aurait suffit d'attendre un peu plus.
Donc pour la rendre résistante au dead lock tu l'affaiblis.
[^] # Re: Approche hybride
Posté par pasBill pasGates . Évalué à 1.
Oui. C'est un compromis mais uniquement pour permettre une solution générale. Tu peux certainement imaginer le délai comme étant un paramètre configurable par processus auquel cas tu as le meilleur des 2 mondes.
[^] # Re: Approche hybride
Posté par barmic . Évalué à 6.
Il y a une hypothèse importante : ça ne doit pas être toi qui a mis le système à plat.
Si tu viens d'allouer les 16Gio de ta machine que pour les traiter tu veux allouer 1Mio et que tu reste en carafe parce que personne ne libère de la mémoire (c'est toi qui a tout…) tu as juste réussi à geler totalement le système tout seul ^^
Tous les contenus que j'écris ici sont sous licence CC0 (j'abandonne autant que possible mes droits d'auteur sur mes écrits)
[^] # Re: Approche hybride
Posté par pasBill pasGates . Évalué à 3.
Tout à fait, mais j'utilises cet allocateur en sachant évidemment comment mon soft alloue et gère sa mémoire.
[^] # Re: Approche hybride
Posté par Tonton Th (Mastodon) . Évalué à 4.
Bah, il y a pire : je fais (un peu à la gruik, il faut l'avouer) du code de traitement d'image avec beaucoup de gestion tordue de mémoire, et la différence entre Linux et OpenBSD est flagrante :)
L'un te laisse dans le flou, et l'autre te larte avec vigueur. Ça apprend vite à faire gaffe.
# Exceptions
Posté par pulkomandy (site web personnel, Mastodon) . Évalué à 6.
J'ai vu (dans la libpng, par exemple), l'utilisation de setjmp/longjmp pour implémenter un truc qui ressemble à des exceptions. Ça permet de tenter de "rembobiner" l'état du programmer comme il était au début de l'opération qui a échoué. Mais bon, c'est pas terrible.
Quand un malloc échoue, la moindre des choses, c'est de logger un truc avant d'exploser en vol. Parce qu'un programme qui s'arrête d'un coup sans prévenir et sans aucun message, ça fait pas sérieux.
Mon point de vue en Java puisque la question a été posée: le langage Java prévoit une exception OutOfMemoryError. Le programme peut essayer de la catcher, mais s'il ne le fait pas, ça remonte tout en haut et ça quitte le thread en cours (et ça tue la VM avec si c'était le main thread).
Mon point de vue de codeur embarqué: là normalement le système est dimensionné comme il faut, si le malloc marche pas, c'est qu'il y a un problème de design. On met un message dans les logs, et on reboote tout le machin pour voir si ça marche mieux après (ou alors, en mode debug, on arrête tout et on préserve l'état pour que quelqu'un puisse venir investiguer).
[^] # Re: Exceptions
Posté par guppy . Évalué à 2.
Effectivement ça fait pas sérieux on est d'accord. Le truc c'est qu'il est difficile de garantir qu'on puisse logger par exemple. Si malloc a échoué, c'est qu'il y a moins d'espace disponible que ce qui a été demandé. Comment être certain que l'action qui va être utilisée pour avertir l'utilisateur va fonctionner ? Je pense qu'on peut pas. Si c'est une allocation d'1 Go qui a échoué, c'est peut-être parce qu'il reste 500 Mo disponible, avec ces 500 Mo on pourra signaler facilement effectivement. Mais c'est peut-être aussi parce qu'il reste 4 octets, et est-ce qu'on peut encore faire quelque chose avec ça ? De plus on pourra peut-être être certain que sur tel OS ça va fonctionner, mais comment être certain que ce soit portable ?
D'ailleurs plus j'y réfléchi plus je me dis que les langages "modernes" sont désavantagés au final. Oui une exception va être déclenchée. Mais si il reste 4 octets disponibles, il faut avoir des notions pointues sur le fonctionnement des allocations mémoires pour que le soft survive. Or ne pas avoir à se soucier de ces concepts est une des raisons d'être des langages managés.
[^] # Re: Exceptions
Posté par pulkomandy (site web personnel, Mastodon) . Évalué à 10.
Le mieux c'est de prévoir le coup dès le début.
Dans mon cas de soft embarqué, ça prend la forme d'un buffer statique de quelques centaines d'octets qui est disponible pour pouvoir écrire un message de log dedans, puis l'envoyer sur le port série (le tout pouvant donc se faire sans une seule allocation supplémentaire). Il me semble qu'on peut faire le même genre de chose dans un soft normal (préallouer tout ce qu'il y a besoin avant de faire quoi que ce soit d'autre).
Dans un langage plus haut niveau, c'est au runtime du langage de se débrouiller pour faire ce genre de choses. Avec éventuellement un endroit ou "brancher" du code utilisateur (en interceptant une exception, par exemple). La stratégie de pré-allocation des ressources nécessaires au traitement de l'erreur s'applique là aussi: l'exception elle-même doit être pré allouée, ainsi que tout ce dont aura besoin le code qui va la traiter.
En C, tu as au moins de la place sur la pile. Donc si malloc échoue, tu peux aussi faire ta gestion d'erreur à base de alloca() si elle a besoin de mémoire.
# Espace de sécurité
Posté par lolop (site web personnel) . Évalué à 6.
Si c'est pour finir une tache courte et éviter un crash, et si l'espace mémoire demandé n'est pas gigantesque, tu peux avoir alloué un espace "de sécurité" au démarrage, et si une allocation (plus petite que l'espace) échoue commencer à taper dans cet espace (le plus simple étant de le libérer puis de refaire la demande d'allocation).
Il faut ensuite positionner un indicateur pour faire sortir le plus rapidement et proprement possible du programme en indiquant le problème.
Votez les 30 juin et 7 juillet, en connaissance de cause. http://www.pointal.net/VotesDeputesRN
# abort != avorter
Posté par esdeem . Évalué à 2. Dernière modification le 27 octobre 2016 à 09:20.
Désolé de passer en mode chieur, surtout que le sujet m'intéresse, mais avorter a un sens précis en français, et certainement pas celui qui lui est prêté ici.
C'est un affreux anglicisme !
On lui préférera interrompre dans la majorité des cas (un complot peut aussi avorter) qui ne concernent pas une grossesse.
0. Assume good faith 1. Be kind to other people 2. Express yourself 4. Apply rule 0
[^] # Re: abort != avorter
Posté par CrEv (site web personnel) . Évalué à 4.
J'en ai pas vraiment l'impression : http://www.cnrtl.fr/definition/avorter
Par contre il est vrai que je ne sais pas trop à quel point on faisait déjà des anglicismes aux 18° et 19° siècles.
[^] # Re: abort != avorter
Posté par esdeem . Évalué à 5.
Oui, mais dans ce cas, le verbe est intransitif. Même Victor Hugo est d'accord avec moi ! ;)
La chose avorte d'elle même ; on ne l'avorte pas, on l'interrompt.
Ainsi, si on laisse une application planter, elle avorte. Mais si on gère le plantage, on l'interrompt.
0. Assume good faith 1. Be kind to other people 2. Express yourself 4. Apply rule 0
[^] # Re: abort != avorter
Posté par gouttegd . Évalué à 5.
Je ne crois pas avoir utilisé avorter comme un verte transitif.
J’ai utilisé « avorter en cas d’erreur » et « avortement sur erreur », sans complément : le programme est ici le sujet, c’est lui qui avorte.
Lorsque j’ai voulu considérer le programme comme l’objet plutôt que le sujet, j’ai utilisé « terminer le programme » ou « quitter le programme ».
Pardon aux drosophiles.
[^] # Re: abort != avorter
Posté par esdeem . Évalué à -1.
J'entends ton point de vue. Reste que la suite prouve que ce qui t'intéresse, c'est de bel et bien de maîtriser le plantage de l'application - donc de l'interrompre - et non pas de la laisser se vautrer lamentablement tout seule - et produire un truc mort.
Il demeure pas moins que avorter dans le sens de s’arrêter brutalement en informatique est un anglicisme.
Quant à avortement, même si son emploi au sens figuré et métaphorique est clairement attesté, son emploi n’est pas usuel dans ce sens, et est principalement restreint aux affaire ou à la biologie et de surcroît d'emploi vieilli ou littéraire.
On est d'accord. Toutes mes excuses à ces pauvres muscomorphes ! :)
0. Assume good faith 1. Be kind to other people 2. Express yourself 4. Apply rule 0
[^] # Re: abort != avorter
Posté par kantien . Évalué à 2.
C'est surtout un « affreux » latinisme au sens figuré ! Il y a aussi du latin dans la langue anglaise, et ce sens figuré se pratiquait déjà en France au XIIe siècle. ;-)
Mot qui vient du latin aborto : mettre à jour avant terme.
Sapere aude ! Aie le courage de te servir de ton propre entendement. Voilà la devise des Lumières.
[^] # Re: abort != avorter
Posté par esdeem . Évalué à 2. Dernière modification le 27 octobre 2016 à 12:22.
Comme expliqué plus haut, le verbe est intransitif, même en latin, origine que je ne conteste pas.
Aborto vient lui-même de aborior, verbe intransitif passif de sens moyen (pour soi, en rapport à soi), de ab-, qui indique l'origine, et orior, s'élever, soit mourir. Car en latin, quand on meurt, on s'élève.
Le verbe lui-même est reformé sur abortus (étant mort, ayant péri), le participe passé de aborior, et qui signifie assez littéralement produire quelque chose étant mort, soit avorter, puis donner naissance prématurément.
0. Assume good faith 1. Be kind to other people 2. Express yourself 4. Apply rule 0
[^] # Re: abort != avorter
Posté par kantien . Évalué à 2.
Certes mais, comme il t'a répondu, il l'utilise bien comme une verbe intransitif dans son journal. Du moins, on peut discuter ce dernier point. En dehors de son usage en titre de section, il écrit :
On peut soit considérer que les auteurs ont décidé d'avorter le programme (qui serait ici sous-entendu en fonction objet) ce qui en ferait bien un usage incorrect en tant que verbe transitif; soit, toujours dans une acception figurée du verbe, considérer que le programme représente le processus par lequel les auteurs veulent engendrer un résultat et ils décident d'avorter en cas d'erreur produisant ainsi quelque chose étant mort; ou encore on pourrait écrire, par métonymie, en substituant l'agent par l'instrument : le programme avorte en cas d'erreur.
Quoi qu'il en soit, je ne vois toujours pas en quoi un tel usage relève d'un affreux anglicisme (sic). ;-)
Aucun drosophile n'a été abusé sexuellement au cours de cet échange linguistique.
Sapere aude ! Aie le courage de te servir de ton propre entendement. Voilà la devise des Lumières.
[^] # Re: abort != avorter
Posté par esdeem . Évalué à 1.
On est d'accord, comme je l'ai mis plus haut, que les usages figurés sont parfaitement attestés en français, mais ne demeurent pas moins, d'usages secondaires, voire vieillis, ou alors les tournures utilisées sont différentes, typiquement faire avorter (en général un projet).
Ton argumentation est tout à fait censée et j'entends parfaitement ton point de vue, auquel je ne suis pas insensible.
Reste que la langue principale de l'informatique est l'anglais, langue dans laquelle le verbe abort est le mot exact pour décrire l'interruption, volontaire ou non, mais souvent brutale, d'un processus, d'une applications, etc. en informatique.
Or un tel usage est en français est particulier, bien qu'un usage figuré soit attesté, soit proche de sens et de matière quand on parle biologie soit par par analogie quand on parle d'une affaire.
En conséquence, j'en déduis que sans trop de difficultés que la proximité de la formulation anglaise est probablement plus qu'un vague cousinage avec l'usage qui est fait ici de avorter et avortement.
Cette proximité d'usage et la familiarité, supposée je l'admets, du rédacteur de ce journal avec la littérature informatique anglaise me font penser que glissement il y a eu de la langue anglaise vers le Français et qu'il s'agit par conséquent d'un anglicisme.
Quant au qualificatif d'affreux, je veux bien le retirer.
Et pour faire bonne figure, je confirme qu'aucun drosophile n'a été abusé sexuellement au cours de cet échange linguistique. ;)
0. Assume good faith 1. Be kind to other people 2. Express yourself 4. Apply rule 0
[^] # Re: abort != avorter
Posté par pulkomandy (site web personnel, Mastodon) . Évalué à 1.
L'anglais pour l'informatique est plein d'images et de métaphores plus ou moins douteuses. ça donne un peu un charme désuet, comme tu l'as dit. On s'en rend pas compte quand on voit des anglicismes utilisés partout, nous les Français, on a l'impression que ça claque et que c'est des mots modernes. Mais non. Alors pourquoi on ne s'amuserait pas à faire la même chose en français?
[^] # Re: abort != avorter
Posté par kantien . Évalué à 2.
Il est vrai que si le verbe anglais to abort et le verbe français avorter sont issus d'une racine commune, l'évolution de l'usage anglais en a fait un verbe qui peut être intransitif ou transitif. Là où en français, lorsque le sujet de la proposition est l'agent qui pratique l'avortement, on préférera utiliser des formulations comme « faire avorter qqch. », « arrêter qqch. » ou « interrompre qqch. ». Comme le montre ces quelques exemples issus de linguee :
Malgré cela, le projet ourdi en secret par les grammar nazi afin de conquérir le monde n'a pas encore avorté. :-P
Sapere aude ! Aie le courage de te servir de ton propre entendement. Voilà la devise des Lumières.
[^] # Re: abort != avorter
Posté par esdeem . Évalué à 2.
Ouf, nous voilà saufs ! ;)
0. Assume good faith 1. Be kind to other people 2. Express yourself 4. Apply rule 0
[^] # Re: abort != avorter
Posté par totof2000 . Évalué à 4.
Après le point Godwin, le point Capello ?
# 2016
Posté par kowalsky . Évalué à -10.
On est en 2016, la RAM ne coûte rien, il suffit d'en rajouter ! Et ne me parle pas d'embarquer, le moindre téléphone à pas chère dispose de 4Go de RAM !
=> [ ]
[^] # Re: 2016
Posté par totof2000 . Évalué à 5.
On est en 2016, la RAM s'ajoute de plus en plus difficilement (on la soude de plus en plus, sans possibilité d'ajout).
Le téléphone, c'est pas (plus) de l'embarqué (comme le raspberry pi d'ailleurs).
[^] # Re: 2016
Posté par Marotte ⛧ . Évalué à 5.
Pfff… 16GB ought to be enough for anyone.
[^] # Re: 2016
Posté par kowalsky . Évalué à 2.
Il se peut que peut être c'était de l'humour (pas drôle, certes).
[^] # Re: 2016
Posté par totof2000 . Évalué à 3.
Si je n'avais pas réagi, ça aurait été beaucoup moins drôle ;)
# xmalloc
Posté par David Demelier (site web personnel) . Évalué à 4. Dernière modification le 27 octobre 2016 à 11:12.
Pour ma part quand je faisais encore du C, j'écrivais souvent une fonction xmalloc qui faisait un exit() après avoir écrit un message d'erreur.
Évidemment, si
malloc
ne fonctionne pas, mon message d'erreur a peut-être un risque de ne pas être affiché non plus, car lui aussi a peut-être besoin d'allocation dans la fonctionprintf
. D'ailleurs j'avais vu une fonction dans GCC qui stockait une chaîne fixe et qui faisait appel àwrite(2)
directement pour éviter ce problème.Comme décrit dans plusieurs commentaires au dessus, cela dépend vraiment du contexte. Je code des applications assez basiques, donc ce n'est pas grave si elles se terminent parce qu'il n'y a plus de mémoire. Du coup je codais ceci :
git is great because linus did it, mercurial is better because he didn't
[^] # Re: xmalloc
Posté par MCMic (site web personnel) . Évalué à 4.
Quelle différence avec le xmalloc du journal du coup ?
[^] # Re: xmalloc
Posté par David Demelier (site web personnel) . Évalué à 1.
Ah oui, j'avais lu le journal une première fois et répondu le lendemain. J'aurais du dormir + du coup :D
git is great because linus did it, mercurial is better because he didn't
# foobar
Posté par needs . Évalué à 6. Dernière modification le 27 octobre 2016 à 12:52.
Je trouve que wrapper
malloc()
est déjà trop lourd, je préfère rester simple, surtout en C. Dans tout les programmes non interactif, j'exit()
après avoir utiliséperror()
:Notez qu'il est possible d'avoir un message d'erreur un peu plus personnalisé si vous le souhaitez :
Mais peu importe les circonstances, intentionnellement provoquer une segfault est une très mauvaise pratique, car il est alors impossible de faire la différence entre un crash à cause de
malloc()
et un bug réel, d'autant qu'un crash et qu'unexit()
ce n'est pas la même chose.Dans les programmes interactif ou les bibliothèques, j'échoue toujours gracieusement. A ce propos, voici une astuce assez connue à base de
goto
pour nettoyer les ressources allouées dans une même fonction (les messages d'erreur sont omis) :Mais voici le point ou je voulais en venir : le plus possible j'essaye d'utiliser l'allocation « sur la pile »* ou statique. Cela veut dire mettre une borne haute à mes conteneurs. Combiné à
sizeof()
c'est une façon de faire qui est très plaisante, et très rapide de surcroît. L'utilisation desnprintf()
est un vrai plaisir :Un exemple pas très recommandable mais qui montre que
sizeof()
est très pratique :Dans l'exemple qui suit, notez bien la ligne
buf[] = "yyyy-mm-ddThh-mm-ssZ"
. Les dates au format RFC 3339 ont une taille fixe, c'est un des nombreux avantages de ce standard. Voici un excellent article sur la gestion des dates et des temps en C.Même pour ce qui ne relève pas des chaînes de caractère j'essaye de trouver des bornes haute raisonnable :
Au final, je me retrouve très rarement à utiliser
malloc()
. Uniquement dans les cas ou la taille est potentiellement trop grande pour allouer « sur la pile »* ou statique. Et lorsque je ne connaît pas à l'avance la taille de mon buffer, alors j'utilise souventrealloc()
…*: Techniquement, le standard C n'a pas de notion de pile, lorsque l'on dit « variable allouée sur la pile », on fait référence aux variables avec une automatique storage duration. Enfin bon, ça ne change pas grand chose.
[^] # Re: foobar
Posté par David Marec . Évalué à 3. Dernière modification le 27 octobre 2016 à 15:31.
Je trouve surtout qu'il est dangereux parce que selon les extraits des pages de manuel publiées ici,
il peut sortir
NULL
en cas dexmalloc(0)
sous linux.De fait, il vaut mieux connaitre (un peu) l'implémentation du
malloc
que l'on utilise.Et lorsque l'on alloue pour le noyau c'est encore différent.
Quel différence avec err(3) ?(1)
Oui, l'un provoque un dump et pas l'autre. Si l'on veut un dump, on peut alors effectivement utiliser
perror(1)
puisabort(3)
.(1): au hasard, un des exemples de la page :
[^] # Re: foobar
Posté par needs . Évalué à 2.
Ce sont des extensions BSD¹, donc non POSIX et probablement pas très portable. Je préfère utiliser POSIX quand c'est possible.
Exactement, et je ne suis pas sur qu'un dump soit vraiment pertinent dans ce cas : une allocation qui échoue ce n'est pas un bug.
D'autant que : une segfault ou
abort()
provoque l’envoi du signal SIGSEGV (ou SIGABRT) et donc les fonctions enregistrées paratexit()
ne seront pas exécutées. Le code de retour du processus sera supérieur à 128 ce qui signifie que le processus c'est terminé anormalement, etc…¹: Page de manuel Linux: « Conforming to: These functions are nonstandard BSD extensions. ». La page de manuel de FreeBSD précise qu'il faut privilégier
strerror()
pour du code portable : « The err() and warn() families of functions are BSD extensions. As such they should not be used in truly portable code. Use strerror() or similar functions instead. ».# containers
Posté par Anonyme . Évalué à 8.
Un certain nombre de personnes semblent considérer que si erreur d'allocation il y a, alors c'est que de toute façon la machine est dans les choux.
C'est sans compter l'utilisation de plus en plus courante des containers et autres cgroups dans lesquels on peut définir des limites de mémoire alloué au programme que l'on y exécute.
De ce point de vue, il est probable que l'on rencontre de plus en plus souvent des circonstances dans lesquelles une allocation de mémoire pourrait être refusée sans que l'os soit en réalité à court de mémoire.
[^] # Re: containers
Posté par barmic . Évalué à 5.
Ça ne change rien ? Si tu épuise ta mémoire, que celle-ci soit celle d'une vm, d'un conteneur, d'une machine physique, d'un cluster ou autre, t'es toujours dans les choux.
Tous les contenus que j'écris ici sont sous licence CC0 (j'abandonne autant que possible mes droits d'auteur sur mes écrits)
[^] # Re: containers
Posté par Anonyme . Évalué à 4.
Justement, pas nécessairement. On pourrait souhaiter qu'un programme dont on a volontairement limité les ressources ne segfault pas parce qu'il n'a plus de mémoire mais soit en mesure de gérer ça de proprement. En attendant que de nouvelles ressources soient disponibles par exemple.
Une vm et un container n'ont pas la même finalité et n'ont pas besoin des mêmes ressources.
[^] # Re: containers
Posté par barmic . Évalué à 4.
C'est très rare les programmes qui peuvent entrevoir ce genre de choses (genre reparamétrer un algo parce qu'il n'ont pas réussi à allouer ce qu'il faut comme mémoire pour la premier config) et rendre ça souhaitable est amha encore plus rare. Tu veut limiter les ressources utilisées par ton soft → il te propose des configurations pour le faire. C'est bien plus sain pour la gueule du code que tu aura derrière.
Même la STL ne le fait pas que je sache (tu as un std::string dont tu consomme tout la capacité, il alloue 2 fois sa taille et va planter si ça marche pas, pas réessayer avec juste ce qu'il faut).
Il y a quelques cas où c'est « simple », un cache de données que tu garde plus où moins important. C'est trivial avec langages à GC qui ont la notion de pointeurs faibles, ailleurs par contre, c'est probablement plus simple de passer par de la configuration.
Tous les contenus que j'écris ici sont sous licence CC0 (j'abandonne autant que possible mes droits d'auteur sur mes écrits)
[^] # Re: containers
Posté par pasBill pasGates . Évalué à 1.
Non il ne va pas planter, il va lancer une exception. Ce n'est pas la même chose, et de loin.
[^] # Re: containers
Posté par barmic . Évalué à 3.
Bof par planter j'entendais échouer et pas réessayer. Mon propos n'est pas dans la remontée d'erreur mais dans ce qui est tenté avant de considérer que l'on est en erreur.
Tous les contenus que j'écris ici sont sous licence CC0 (j'abandonne autant que possible mes droits d'auteur sur mes écrits)
[^] # Re: containers
Posté par pasBill pasGates . Évalué à 2.
Oui mais tu remarqueras que la différence est très importante.
STL te permet de réessayer ou de sortir proprement. Il ne crashe pas sans te donner au moins une chance de gèrer la chose.
[^] # Re: containers
Posté par barmic . Évalué à 3.
Ok si tu veux, je vois pas bien le rapport avec ce que je disais, mais oui. Tant que tu as activer les exceptions.
Mon propos c'est que pour vraiment faire des programmes qui s'adaptent à la mémoire qu'ils ont de disponible est très complexe et qu'il vaut mieux ajouter des paramètres.
Je notais juste que les allocateurs de la STL permettent tout de même ce genre de choses. Le fais de gérer ça par exception n'a rien de particulier à la STL, ni de tes agréable à faire.
Tous les contenus que j'écris ici sont sous licence CC0 (j'abandonne autant que possible mes droits d'auteur sur mes écrits)
[^] # Re: containers
Posté par louiz’ (site web personnel) . Évalué à 1.
Tant que tu n’as pas désactivé les exceptions.
[^] # Re: containers
Posté par benoar . Évalué à 2.
Donc « terminer correctement » pour qu'on puisse le relancer « plus tard » quand il y aura plus de ressources ? Quelle différence avec faire que malloc() renvoie toujours un pointeur valide et que le kernel se débrouille pour toujours avoir de la mémoire disponible, quitte à swapper, etc ? Tu viens juste de remonter le problème plus haut, sur l'administrateur ou un autre composant de l'OS que ton kernel, sans le régler. Tu n'évoques pas de solution en tant que telle à ce problème, tu l'as juste déplacé, et ça ne fonctionne pas mieux car il n'existe pas de solution idéale encore aujourd'hui.
C'est pourquoi en général je trouve que ce genre de « gestion » des ressources est complètement inutile, et que le cloisonnement par VM ou container amène en général plus de problèmes qu'il n'en résout, en faisant croire qu'ils « gèrent » mieux les ressources (en fait, ils partitionnement plus durement, et globalement le multiplexage statistique ne peut pas avoir lieu). Bref, c'est nul.
[^] # Re: containers
Posté par Sufflope (site web personnel) . Évalué à 2.
Parce qu'avec le pointeur toujours valide le programme se dira "chouette j'ai ma mémoire je peux y aller" et au moment de l'utiliser paf ; alors que s'il attend il explosera pas.
Me semble bien que la swap est déjà comptée dans la mémoire totale, donc les cas de malloc != 0 mais j'ai pas la mémoire en fait, y a plus de swap non plus ; alors à moins de faire de la swap dynamique comme Windows ou OSX… (et évidemment, dans ce cas-là il se passera quoi quand y aura plus de disque ? Tu viens juste de remonter le problème plus haut)
[^] # Re: containers
Posté par pasBill pasGates . Évalué à 1.
Non, si tu épuises ta mémoire, tu dois au moins avoir la possibilité de décider si tu veux essayer de faire de la place et continuer plutôt que t'écraser.
# Pour moi: pré-allouer un buffer d'urgence (a l'ancienne)
Posté par Renaud . Évalué à 5. Dernière modification le 27 octobre 2016 à 19:26.
La réponse dépend vraiment de ce que fait ton programme. Dans mon cas, j'ai ma surcouche à malloc()/free()/realloc(), et lors de l'init de mon programme je reserve un bloc de 1Mo. Si un malloc() échoue, je tappe dans mon bloc de 1Mo et je met une variable globale à TRUE disant qu'il n'y a plus de mémoire. Comme mon programme fait une grosse boucle, je consulte souvent cette variable et si elle est activée je libère de la mémoire utilisée pour faire du cache. Si on échoue trop souvent, on loggue proprement l'erreur et on quitte.
Franchement, je ne trouve pas que wrapper malloc soit trop lourd. C'est pénible à court terme, c'est pratique de pouvoir factoriser cette fonction critique à long terme. De manière générale, je wrappe toutes les fonctions externes tant que possible, ça aide pour la portabilité.
Je ne recommande pas de faire un bzero() sur ton buffer alloué. Tu y perds énormément en performance si tu programme fait beaucoup d'allocations.
La plupart des bibliothèques (des bonnes en tout cas) te permettent de définir quelles fonctions utiliser pour malloc()/realloc()/free(), donc c'est globalement pas un problème (sauf pour la libc).
CECI ETANT DIT:
Comme tu le dis, sous Linux, un malloc() qui réussit n'indique pas forcément qu'il y a de la mémoire dispo. /proc/sys/vm/overcommit_memory peut t'aider mais risque de dégrader les perfs.
Les problèmes de fragmentation de mémoire font que si tu libères 200 blocs de 1ko, tu ne sais pas si ton allocation suivante de 200ko va fonctionner (probablement pas) ;
Si tu gères des énormes buffers, rien de tout ceci ne s'applique;
J'avais lu un papier récemment qui disait quand quand l'allocation de mémoire échoue, tu es fichu de toute façon. C'est pas forcément faux. Peut être que faire un abort() propre (avec message d'erreur) dans ton malloc() custom est une approche plus simple (je l'ai fait pendant longtemps).
(Ref: La première fois que j'ai vu cette histoire de buffer pré-alloué c'était dans 'Inside Macintosh' pour programmer sous système 7).
# En parlant de Git...
Posté par Joël . Évalué à 9.
Par curiosité et aussi parce que j'étais en train de développer un programme en C récemment, j'ai lu pas mal du code de Git récemment et effectivement, j'étais assez étonné de voir que Git ne s'embarrasse généralement pas de libérer la mémoire en cas d'erreur. En revanche pour la gestion des fichiers, j'ai trouvé que la gestion était assez intéressante. Par exemple, lorsque Git veut lire ou écrire l'index (
.git/index
):.git/index.lock
lockfile
(https://github.com/git/git/blob/master/lockfile.h) qui s'appuie sur l'APItempfile
(https://github.com/git/git/blob/master/tempfile.h)tempfile
ajoute chaque fichier verrou à une liste chainée et enregistre une fonction de callback au cas où le programme se termine (notamment avecatexit()
etsignal()
)..git/index.lock
=>.git/index
) donc il n'y a rien à faire. Autrement, les fichiers verrous temporaires sont effacés et l'état global est préservé.Un autre exemple est le
git clone
. Si vous avez déjà remarqué, stopper un git clone en plein milieu ne laisse pas de bordel derrière lui, le répertoire créé se retrouve vide. Au moment de faire un git clone, la commande clone enregistre elle aussi une fonction de callback pour nettoyer les fichiers en cas d'erreur ou d'interruption (voir https://github.com/git/git/blob/master/builtin/clone.c#L935).[^] # Re: En parlant de Git...
Posté par erdnaxeli (site web personnel) . Évalué à 3.
Intéressant cette technique. Leur fonction
atexit
me fait beaucoup penser àdefer
en go.Il existe deux catégories de gens : ceux qui divisent les gens en deux catégories et les autres.
[^] # Re: En parlant de Git...
Posté par erdnaxeli (site web personnel) . Évalué à 2.
Je me répond à moi-même : ce n'est pas "leur" fonction, ça fait partie de la libc, je ne connaissais pas.
Il existe deux catégories de gens : ceux qui divisent les gens en deux catégories et les autres.
# Toward more predictable and reliable out-of-memory handling
Posté par benoar . Évalué à 4.
Ces discussions m'ont fait penser à un article de Jonathan Corbet que j'ai lu récemment sur les avancées de la gestion de l'épuisement de la mémoire dans le noyau : « Toward more predictable and reliable out-of-memory handling » https://lwn.net/Articles/668126/
En suivant les liens, on tombe sur d'autres lectures intéressantes également.
# Un jour dans ma folie
Posté par oscar.stefanini . Évalué à 1.
Un jour dans ma folie j'ai tenter un truc. Je me suis dis "Hey, et si je listais toutes mes allocations mémoires dans un nombre, et que j'enregistrais ensuite les libérations de mémoires au fur et à mesure. Si jamais j'ai une erreur de mémoire, je pourrais appliquer des opérations bitwise pour savoir qu'elles allocations je dois libérer avant de revenir au point de départ."
Alors, deux choses
1. J'ai passé deux jours à mettre en place ce monstre
2. ça a fonctionné 10 minutes. Avant de me rendre compte que le nombre avait une limite, et qu'à partir de la 11 ème allocation, ça partait en cacahuète (normal parce que 2 puissance 11… blabla int, unsigned int)
3. J'étais totalement déçu parce que ça fonctionnait mal avec les boucles
4. j'ai arrêté de libérer la mémoire en cas d'erreur
5. On m'a toujours appris que exit() quittait sans libérer la mémoire. C'est faux
Pour conclure, je fais des if partout, c'est comme ça en C. En c++ avec la RAII (https://en.wikipedia.org/wiki/Resource_acquisition_is_initialization) on gère ça différemment… :)
[^] # Re: Un jour dans ma folie
Posté par lasher . Évalué à 3.
C'est surtout qu'on ne sait pas. Dans un OS moderne « poids lourd » (UNIX, Linux, Windows), oui, la mémoire est libérée. Dans des OS plus anciens (DOS), ou certains OS embarqués1 où c'est pas garanti du tout. Il y a aussi des OS bizarres, du genre de ceux qui sont utilisés dans certains supercalculateurs, où un cœur fait tourner un « OS lourd » (un Linux modifié la plupart du temps) et les autres cœurs font tourner un OS léger, qui délègue à l'OS lourd une partie de la gestion des ressources. Dans ces cas-là, je ne sais pas comment ça se passe niveau allocation/désallocation mémoire lorsqu'un programme quitte.
Après, dans un OS embarqué, t'as intérêt à avoir une très bonne raison pour faire de l'allocation dynamique de mémoire… ↩
[^] # Re: Un jour dans ma folie
Posté par claudex . Évalué à 3.
Dans les supercalculateurs, ça m'étonnerait qu'ils puissent se permettre d'avoir plein de mémoire allouée qui n'est plus utilisée si les développeurs ont oublié de gérer ce cas.
« Rappelez-vous toujours que si la Gestapo avait les moyens de vous faire parler, les politiciens ont, eux, les moyens de vous faire taire. » Coluche
# mtrace
Posté par LupusMic (site web personnel, Mastodon) . Évalué à 2.
J'ai aussi tenance à encapsuler l'allocation mémoire, pour tenter de gérer les cas d'erreur au mieux. Le soucis est que ceci rend compliqué l'usage de mtrace pour détecter les fuites mémoires (puisque toutes les allocations proviennent du même site d'appel à malloc. Alors oui, on peut commencer à envisage d'écrire des routines compatibles avec mtrace, mais ça devient compliqué.
Suivre le flux des commentaires
Note : les commentaires appartiennent à celles et ceux qui les ont postés. Nous n’en sommes pas responsables.