Journal Gestion des erreurs d’allocation mémoire en C

Posté par  . Licence CC By‑SA.
Étiquettes :
54
26
oct.
2016

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  (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  . É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 ?

      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.

      É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  . Évalué à 5.

        Étant donné que le journal pose la question, je suppose qu'il cherche une solution…

        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  . Évalué à 1.

          Ce commentaire a été supprimé par l’équipe de modération.

          • [^] # Commentaire supprimé

            Posté par  . Évalué à 2.

            Ce commentaire a été supprimé par l’équipe de modération.

          • [^] # Re: Mélange des deux

            Posté par  (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  . É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  . É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 :

              # freem -m
                           total       used       free     shared    buffers     cached
              Mem:          7861       6976        884        335        200       4464
              -/+ buffers/cache:       2311       5549
              Swap:         7627          0       7627
              

              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  . Évalué à 7.

                il faut écrire sur le disque en cas de pointe de consommation de mémoire.

                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  . É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  . É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  . Évalué à 5.

        Étant donné que le journal pose la question, je suppose qu'il cherche une solution…

        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  (site web personnel) . Évalué à 10.

    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 segfaulter tout seul lors du déréférencement d’un pointeur nul. J’appellerai ça la méthode « YOLO ».

    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  . Évalué à 7.

      La spécification de malloc (http://pubs.opengroup.org/onlinepubs/009695399/functions/malloc.html) indique :

      Upon successful completion with size not equal to 0, malloc() shall return a pointer to the allocated space. If size is 0, either a null pointer or a unique pointer that can be successfully passed to free() shall be returned. Otherwise, it shall return a null pointer.

      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  . Évalué à 5.

        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

        J’avoue que c’est ce que je pensais aussi.

        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)

        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  . É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 config cgroup) 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  . Évalué à 6.

          Au contraire.

          Sous Linux, si tu chopes un segfault en accédant a un NULL malloc sous Linux, tu as soit :

          • bouffé tout ton espace d'addresse, et c'est clairement un problème à corriger
          • TON soft bouffe trop de RAM (car Linux utilise OOM killer et ne te fera pas un segfault pour une petite alloc ici ou la)

          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  . Évalué à 0. Dernière modification le 27 octobre 2016 à 02:43.

            Sous Linux, si tu chopes un segfault en accédant a un NULL malloc sous Linux, tu as soit :

            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…).

            Bref, dans tous les cas, c'est super important de le savoir.

            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  . Évalué à 1.

              malloc (en fait l'appel système brk) ne renvois jamais null

              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  . Évalué à 0. Dernière modification le 27 octobre 2016 à 03:01.

                Il y a peut-ête le cas des limites dépassées (ulimit, cgroup)

                En fait non cf. message de Cheuteumi juste au dessus … :-/

              • [^] # Re: undefined behaviour

                Posté par  . Évalué à 2.

                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…)

                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  . Évalué à 0.

                  • S'il peut déterminer la quantité de mémoire nécessaire pour tout son conteneur, il doit forcément déterminer la quantité de mémoire nécessaire à son programme (ainsi qu'à tout les autres programmes du conteneur)…
                  • Deuxiemement admettons qu'il n'en soit pas capable, ça ne change rien au comportement de malloc.

                  => 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  . Évalué à 1.

              on se chope un OOM

              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  . É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  . Évalué à 0.

                  Dans ce cas tous les autres logiciels vont faire pareil.

                  • [^] # Re: undefined behaviour

                    Posté par  (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  . É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  (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  . É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  . Évalué à 1.

                OK merci c'est plus clair.

              • [^] # Re: undefined behaviour

                Posté par  . É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  (site web personnel) . Évalué à 3.

              En pratique on peut considérer qu'il ne renvoit jamais null.

              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  . Évalué à 1.

                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 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.

                if ( ! (p = malloc(s)) && s )
                   err(...)

                Du coup, le wrapper proposé ne résout pas le problème.

                • [^] # Re: undefined behaviour

                  Posté par  . Évalué à 2. Dernière modification le 27 octobre 2016 à 16:36.

                  Du coup, le wrapper proposé ne résout pas le problème.

                  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  . Évalué à 3.

                Je suis globalement d'accord avec ce que tu racontes.

                Cependant lorsque tu dis

                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

                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:

                static inline void 
                fatal(const char *err) {
                        perror(err);
                        exit(errno);
                }
                
                static inline void*
                xmalloc(size_t size, const char* filename, const int lineno)
                {
                        void* p = malloc(size);
                        if (!p) {
                                fprintf(stderr, "%s, line %d:\t", filename, lineno);
                                fatal("malloc");
                        }
                        return p;
                }
                
                #define XMALLOC(size) xmalloc((size), __FILE__, __LINE__)

                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 type util.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  . Évalué à 5.

              sous linux malloc (en fait l'appel système brk) ne renvois jamais null

              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 :

              • ne jamais retourner NULL (overcommit) ;
              • retourner NULL dès lors que le processus consomme déjà une certaine quantité de mémoire (pas d’overcommit du tout), la limite par défaut étant fixée à la taille du swap + la moitié de la mémoire physique ;
              • ne pas retourner NULL sur la plupart des allocations (quitte à overcommitter s’il le faut), mais refuser quand même les allocations jugées « démesurées » — c’est le mode par défaut.
              • [^] # Re: undefined behaviour

                Posté par  . É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  . É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  (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  . É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  (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  . É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  (Mastodon) . Évalué à 7.

                        D'ailleurs le modèle fork/exec pour spawner des nouveaux process est "moisi" entre autre à cause de ça.

                        Tu peux expliquer un peu ce moisi, et que proposes-tu à la place ?

            • [^] # Re: undefined behaviour

              Posté par  . Évalué à 3.

              malloc (en fait l'appel système brk)

              Sauf qu'en fait non, malloc appelle brk pour les allocs mémoire de moins d'une page, mais il appellera mmap si ça dépasse la taille d'une page. Donc tout dépend du type d'allocation mémoire dont on cause.

              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.

              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 des abort() 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.).


              1. en partie pour garantir autant d'allocation de pages mémoires physiquement contiguës que possible.  

              • [^] # Re: undefined behaviour

                Posté par  . Évalué à 3.

                Sauf qu'en fait non, malloc appelle brk pour les allocs mémoire de moins d'une page, mais il appellera mmap si ça dépasse la taille d'une page. Donc tout dépend du type d'allocation mémoire dont on cause.

                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  . É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 que malloc 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?).

                  Tu n'a aucune espèce d'idée si ton malloc échoue ou pas et de quoi faire de bien pertinent

                  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  . Évalué à 5.

          Pas besoin de provoquer un segfault pour obtenir un core dump, il suffit d'appeler abort().

  • # Langages modernes

    Posté par  . É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  . Évalué à 6.

      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.

      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).

      il y a toujours ce petit truc qui gratte. Je préférerai bien évidemment qu'ils marchent coûte que coûte.

      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  . Évalué à -2.

        Non ça n'est en pratique pas possible sous linux, désolé. Cf. commentaires plus haut.

        • [^] # Re: Langages modernes

          Posté par  . É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) :

          >>> import numpy as np
          >>> m = 32 * (1024 ** 3)
          >>> a = np.empty(m, dtype='int8')
          Traceback (most recent call last):
            File "<ipython-input-4-c19c3513c3d4>", line 1, in <module>
              a = np.empty(m, dtype='int8')
          MemoryError
          
          >>> !cat /etc/os-release
          NAME="Ubuntu"
          VERSION="16.04.1 LTS (Xenial Xerus)"
          ID=ubuntu
          ID_LIKE=debian
          PRETTY_NAME="Ubuntu 16.04.1 LTS"
          VERSION_ID="16.04"
          HOME_URL="http://www.ubuntu.com/"
          SUPPORT_URL="http://help.ubuntu.com/"
          BUG_REPORT_URL="http://bugs.launchpad.net/ubuntu/"
          UBUNTU_CODENAME=xenial
          
          • [^] # Re: Langages modernes

            Posté par  . É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 :

            exception OutOfMemory
            let alloc n = if n > Sys.max_string_length then raise OutOfMemory else Bytes.make n '\000'
            • [^] # Re: Langages modernes

              Posté par  (site web personnel) . Évalué à 4.

              mmap(NULL, 34359742464, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = -1 ENOMEM (Cannot allocate memory)
              brk(0x801d02000)                        = 0x1ce7000
              [...]
              

              Les 34359742464 mentionnés là sont les 32 * (1024 ** 3) demandés à numpy (et 4Ko d'overhead).

              • [^] # Re: Langages modernes

                Posté par  . É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.

                import numpy as np
                L = [ np.empty(1024**3, dtype='int8') for i in range(32)]

                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  . Évalué à 2.

                  Alors c'est effectivement du au vm.overcommit_memory = 0

                  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 ?

                  Aussi content de savoir que numpy ne fait pas de memset par défaut, c'eût été un peu idiot

                  Numpy fournit np.empty et np.zeros, le premier n'initialise pas le tableau, le deuxième l'initialise à zéro.

                  • [^] # Re: Langages modernes

                    Posté par  . É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  . Évalué à 8.

      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.

      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  . Évalué à 6.

      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 ?

      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 erreur1OutOfMemomryError 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.


      1. Une erreur c'est comme une exception d'un type différent. En java, il y a les Throwables qui se divisent en Error et en Exception

      2. 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  . É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  . Évalué à 3.

      Dans les autres cas, mon allocateur prendra peut-être 30 secondes à retourner

      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  . Évalué à 5.

        L'utilisateur doit lancer un débuggeur pour comprendre ce qui se passe ?

        Pas possible vu qu'il n'y a plus d'allocation possible sur tout le système.

      • [^] # Re: Approche hybride

        Posté par  . É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  . É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  (site web personnel) . Évalué à 9.

            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 ?

            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  . Évalué à 2.

              Et d'ailleurs comment allouer de la mémoire pour des strings, ui, etc s'il n'y en a plus ?

              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  (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  . É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  . Évalué à 2.

                  c'est juste très difficile à faire car on doit être certain que le path d'erreur ne fera pas d'allocation

                  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  . Évalué à 2.

                  Donc python met un handler sur sigsegv et lance une MemoryError.

                  Non, il se contente de récupérer NULL sur malloc() ou mmap(). 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

                  vu que malloc ne retourne pas NULL sous linux

                  Ben si :-) Par ailleurs Python ne tourne pas que sous Linux…

              • [^] # Re: Approche hybride

                Posté par  . Évalué à 3.

                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.

                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  . É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  . Évalué à 3.

              Non, c'est basé sur une autre idée :

              • Mon soft n'est pas un gros bouffeur de RAM, et ne ré-essaie que pour les petites allocations.

              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  . É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  . É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 :

      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.

      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  . Évalué à 10. Dernière modification le 27 octobre 2016 à 20:17.

        Mais imaginons un autre cas. Ta technique se répand, tous les softs s'y mettent.

        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  (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  . É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  . É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  . Évalué à 6.

      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.

      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  . É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  (Mastodon) . Évalué à 4.

      Exemple tout simple : code qui est sensé être multiplateforme entre Windows et Linux,

      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  (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  . Évalué à 2.

      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.

      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  (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  (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  . É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  (site web personnel) . Évalué à 4.

      C'est un affreux anglicisme !

      J'en ai pas vraiment l'impression : http://www.cnrtl.fr/definition/avorter

      B [En parlant d'un organe, d'une plante, d'un fruit, etc.] Ne pas arriver à son plein développement. Les fruits du verger avortent
      7. … des accidents de saison, des sécheresses excessives ayant fait avorter les récoltes, le gouvernement n'a fait pour l'impôt ni délai ni grace : … Volney, Les Ruines,1791, p. 94.
      C
      1. [En parlant d'une pers. qui entreprend qqc.] S'interrompre prématurément; échouer.
      8. Son père avait fini par avoir une maison et un champ; mais lui, le fils, n'avait rien eu de plus pressé que de perdre dans une fausse spéculation ce champ et cette maison. Il ne lui était rien resté. Il avait de la science et de l'esprit, mais il avortait. Tout lui manquait, tout le trompait; ce qu'il échafaudait croulait sur lui. S'il fendait du bois, il se coupait un doigt. S'il avait une maîtresse, il découvrait bientôt qu'il avait aussi un ami. À tout moment quelque misère lui advenait; de là sa jovialité. Hugo, Les Misérables,t. 1, 1862, p. 781.
      2. [En parlant de tout ce qui est susceptible de connaître une évolution, un développement, un résultat (action, maladie, sentiment, etc.)] Ne pas atteindre le terme attendu de son évolution. Une entreprise qui avorte :

      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  . É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  . Évalué à 5.

          Oui, mais dans ce cas, le verbe est intransitif.

          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  . Évalué à -1.

            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.

            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.

            Pardon aux drosophiles.

            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  . Évalué à 2.

      C'est un affreux anglicisme !

      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. ;-)

      fin xiie s. fig. « ne pas réussir, échouer »

      Étymologie sur le CNRTL

      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  . É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  . É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 :

          d’autres ont déjà fait le choix d’avorter en cas d’erreur

          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  . Évalué à 1.

            Quoi qu'il en soit, je ne vois toujours pas en quoi un tel usage relève d'un affreux anglicisme (sic). ;-)

            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  (site web personnel, Mastodon) . Évalué à 1.

              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.

              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  . Évalué à 2.

              les tournures utilisées sont différentes, typiquement faire avorter (en général un projet). […]

              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. […]

              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.

              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 :

              If something is wrong, abort the take off. Si quelque chose ne va pas, arrêtez le décollage.
              Foolish haste or laziness could abort a great hope-with consequences that we do not imagine. Une hâte imbécile ou de l'indolence pourraient faire avorter un grand espoir, avec des conséquences que nous ne pouvons pas imaginer.
              […] have preprogrammed safety checks that will signal problems by means of error messages and will abort the testing procedure. […] de sécurité préprogrammés signaleront les problèmes au moyen de messages d'erreur et provoqueront l'interruption du test.

              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  . Évalué à 2.

                Malgré cela, le projet ourdi en secret par les grammar nazi afin de conquérir le monde n'a pas encore avorté. :-P

                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  . Évalué à 4.

              Après le point Godwin, le point Capello ?

  • # 2016

    Posté par  . Évalué à -10.

    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 ?

    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  . Évalué à 5.

      On est en 2016, la RAM ne coûte rien,

      On est en 2016, la RAM s'ajoute de plus en plus difficilement (on la soude de plus en plus, sans possibilité d'ajout).

      Et ne me parle pas d'embarquer, le moindre téléphone à pas chère dispose de 4Go de RAM !

      Le téléphone, c'est pas (plus) de l'embarqué (comme le raspberry pi d'ailleurs).

      • [^] # Re: 2016

        Posté par  . Évalué à 5.

        Pfff… 16GB ought to be enough for anyone.

      • [^] # Re: 2016

        Posté par  . Évalué à 2.

        Il se peut que peut être c'était de l'humour (pas drôle, certes).

        • [^] # Re: 2016

          Posté par  . Évalué à 3.

          Si je n'avais pas réagi, ça aurait été beaucoup moins drôle ;)

  • # xmalloc

    Posté par  (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 fonction printf. 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 :

    void *xmalloc(size_t size)
    {
        void *ptr = malloc(size);
    
        if (ptr == NULL)
            err(1, "malloc");
    
        return ptr;
    }

    git is great because linus did it, mercurial is better because he didn't

    • [^] # Re: xmalloc

      Posté par  (site web personnel) . Évalué à 4.

      Quelle différence avec le xmalloc du journal du coup ?

      • [^] # Re: xmalloc

        Posté par  (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  . É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():

        if (!(foo = malloc(size))) {
            perror("malloc(foo)");
            exit(EXIT_FAILURE);
        }

    Notez qu'il est possible d'avoir un message d'erreur un peu plus personnalisé si vous le souhaitez :

        if (!(foo = malloc(size))) {
            fprintf(stderr, "malloc(%zu): %s\n", size, strerror(errno));
            exit(EXIT_FAILURE);
        }

    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'un exit() 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) :

    int fum(size_t size)
    {
        void *foo, *bar, *baz;
    
        if (!(foo = malloc(size)))
            goto err_foo;
    
        if (!(bar = malloc(size)))
            goto err_bar;
    
        if (!(baz = malloc(size)))
            goto err_baz;
    
        /* ... */
    
        return 1;
    
    err_baz:
        free(bar);
    err_bar:
        free(foo);
    err_foo:
        return 0;
    }

    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 de snprintf() est un vrai plaisir :

    struct player {
        char name[32];
        char clan[32];
    };
    
    static void set_default_player_name(
        struct player *player, const char *firstname, const char *lastname)
    {
        snprintf(player->name, sizeof(player->name), "%s.%s", firstname, lastname);
    }

    Un exemple pas très recommandable mais qui montre que sizeof() est très pratique :

    #define IPV6_STRSIZE sizeof("xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx")
    

    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.

    /* RFC 3339 */
    static void print_zulu_time(struct tm *tm)
    {
        char buf[] = "yyyy-mm-ddThh-mm-ssZ";
    
        if (strftime(buf, sizeof(buf), "%Y-%m-%dT%H:%M:%SZ", tm))
            puts(buf);
    }

    Même pour ce qui ne relève pas des chaînes de caractère j'essaye de trouver des bornes haute raisonnable :

    #define MAX_PLAYERS 32
    #define MAX_PACKET_DATA 1400
    

    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 souvent realloc()


    *: 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  . Évalué à 3. Dernière modification le 27 octobre 2016 à 15:31.

      Je trouve que wrapper malloc() est déjà trop lourd

      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 de xmalloc(0) sous linux.

      • sous FreeBSD, jemalloc(3) retourne un pointeur dans ce cas là.
          Even a request for zero bytes (i.e., malloc(0)) returns a pointer 
          to something of the minimum allocatable size.
      

      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.

      Dans tout les programmes non interactif, j'exit() après avoir utilisé perror():

      Quel différence avec err(3) ?(1)

      d'autant qu'un crash et qu'un exit() ce n'est pas la même chose

      Oui, l'un provoque un dump et pas l'autre. Si l'on veut un dump, on peut alors effectivement utiliser perror(1) puis abort(3).


      (1): au hasard, un des exemples de la page :

       if ((p = malloc(size)) == NULL)
                         err(EX_OSERR, NULL);
      • [^] # Re: foobar

        Posté par  . Évalué à 2.

        Quel différence avec err(3) ?

        Ce sont des extensions BSD¹, donc non POSIX et probablement pas très portable. Je préfère utiliser POSIX quand c'est possible.

        Oui, l'un provoque un dump et pas l'autre. Si l'on veut un dump, on peut alors effectivement utiliser perror(1) puis abort(3).

        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 par atexit() 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  . É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  . Évalué à 5.

      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.

      Ç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  . É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  . É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  . Évalué à 1.

            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).

            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  . É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  . É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  . É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  (site web personnel) . Évalué à 1.

                    Ok si tu veux, je vois pas bien le rapport avec ce que je disais, mais oui. Tant que tu as activer les exceptions.

                    Tant que tu n’as pas désactivé les exceptions.

        • [^] # Re: containers

          Posté par  . Évalué à 2.

          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.

          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  (site web personnel) . É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

            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.

            le kernel se débrouille pour toujours avoir de la mémoire disponible, quitte à swapper

            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  . É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  . É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  . É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):

    • Il verrouille le fichier en créant un autre fichier nommé .git/index.lock
    • Ce nouveau fichier est géré par l'API lockfile (https://github.com/git/git/blob/master/lockfile.h) qui s'appuie sur l'API tempfile (https://github.com/git/git/blob/master/tempfile.h)
    • (Notez que si ce fichier verrou existe déjà, il y a deux possibilités : 1/ une autre instance de git est déjà en train d'être exécutée et dans ce cas, on sort ; 2/ l'état global est corrompu, le fichier verrou existant devrait être effacé manuellement)
    • L'API tempfile ajoute chaque fichier verrou à une liste chainée et enregistre une fonction de callback au cas où le programme se termine (notamment avec atexit() et signal()).
    • Cette fonction de callback va effacer tous les fichiers verrous de la liste chainée (s'ils existent) au moment où le processus se termine
    • Dans le cas où le processus se termine correctement, les fichiers verrous auront tous été renommés (.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  (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  (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  . É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  . É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  . Évalué à 3.

      On m'a toujours appris que exit() quittait sans libérer la mémoire. C'est faux

      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.


      1. 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  . Évalué à 3.

        Dans ces cas-là, je ne sais pas comment ça se passe niveau allocation/désallocation mémoire lorsqu'un programme quitte.

        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  (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.