Discussion sur les fuites possibles de mémoire

En général le programmeur peut obtenir de la mémoire pour son programme de plusieurs manières :
  1. statiquement, quand la taille requise est connue à la compilation
  2. dynamiquement "implicitement", en déclarant des variables locales aux fonctions, qui seront donc allouées sur la pile
  3. dynamiquement "explicitement", sur le 'tas' (heap en anglais), à l'aide de fonctions de bibliothèque comme malloc() en C, ou bien à l'aide de l'opérateur new en C++.
Dans la suite, on ne parle que de la variante 3 ci-dessus. Cette variante est offerte au programmeur à l'aide d'un système soujacent de gestion (G) (des listes avec les zones occuppées et leur tailles, etc.) Donc le programmeur doit faire particuliè attention dans des langages comme C et C++ à bien libérer la mémoire ainsi obtenue, sous peine d'avoir ce qu'on appelle des "fuites de m émoire" (memory leak en anglais). C'est quoi une fuite de mémoire ? C'est une situation où une certaine zone de mémoire Z est "oubliée" -- elle devient inaccessible pour un usage ultérieur car Par conséquent, G pense que P a encore besoin de Z, alors que P sait qu'il ne s'en sert plus, mais ne le dit pas à G. Si cette situation arrive de manière répétée (par exemple dans une boucle) suffisamment de fois, ou bien si Z est très grande dès le début, au bout d'un certain temps on fini par épuiser toute la mémoire disponible pour un processus et celui-ci est alors terminé par le système, pour excès de gourmandise. Ce qui suit est un petit exemple (comme il ne faut pas faire) que vous pouvez faire tourner pour obtenir cela (ça durera un peu) :
#include <stdio.h>
#include <stdlib.h>

int main() {
    int numberOfIter  = 5000;
    int numberOfTabs  = 4000;
    int eachTabLength = 4000; 
    int kIter;
    for(kIter = 0; kIter < numberOfIter; kIter++) {
        // alloc 'em
        char **tab = (char**)malloc(numberOfTabs * sizeof(char*));
        int kTab;
        for(kTab = 0; kTab < numberOfTabs; kTab++) {
            tab[kTab] = (char *)malloc(eachTabLength * sizeof(char));
        }
        // free 'em all
        free(tab); // or at least u think so
        fprintf(stderr,".");
    }
    printf("Good bye.\n");
    return(0);
}
La manière correcte de procéder est bien entendu celle-ci:
#include <stdio.h>
#include <stdlib.h>

int main() {
    int numberOfIter  = 5000;
    int numberOfTabs  = 4000;
    int eachTabLength = 4000; 
    int kIter;
    for(kIter = 0; kIter < numberOfIter; kIter++) {
        // alloc 'em
        char **tab = (char**)malloc(numberOfTabs * sizeof(char*));
        int kTab;
        for(kTab = 0; kTab < numberOfTabs; kTab++) {
            tab[kTab] = (char *)malloc(eachTabLength * sizeof(char));
        }
        // free 'em all
        for(kTab = 0; kTab < numberOfTabs; kTab++) {
            free(tab[kTab]);
        }
        free(tab);
        fprintf(stderr,".");
    }
    printf("Good bye.\n");
    return(0);
}

Comment faire pour ne pas en arriver là?

On doit faire trés attention -- un peu comme avec les paranthèses -- tout ce qu'on ouvre on doit aussi fermer. (La situation se complique un peu lorsqu'on a à faire à de gros programmes, ou bien si on alloue à un endroit du code et on libère à un autre endroit, bien éloigné
C'est pour cela qu'en C++ par exemple on fait très attention au code des constructeurs et destructeurs des classes, et à la notion de "possession"-"responsabilité -- quel objet est donc celui qui est censé prendre soin de la libération de la mémoire parmi tous ceux qui manipulent des pointeurs vers une zone Z.
Il y a enfin également des outils d'analyse des programmes, dont par exemple valgrind. Prenons une variante "rapide" des programmes ci-dessus :
#include <stdio.h>
#include <stdlib.h>

int main() {
    int numberOfIter  = 5;
    int numberOfTabs  = 4;
    int eachTabLength = 4; 
    int kIter;
    for(kIter = 0; kIter < numberOfIter; kIter++) {
        // alloc 'em
        char **tab = (char**)malloc(numberOfTabs * sizeof(char*));
        int kTab;
        for(kTab = 0; kTab < numberOfTabs; kTab++) {
            tab[kTab] = (char *)malloc(eachTabLength * sizeof(char));
            int kElem;
            for(kElem = 0; kElem < eachTabLength-1; kElem++) {
                tab[kTab][kElem] = 'A' + kTab + kElem;
            }
            tab[kTab][eachTabLength-1] = '\0';
        }
        // use 'em
        for(kTab = 0; kTab < numberOfTabs; kTab++) {
            printf("%s ",tab[kTab]);
        }
        // free 'em all
        free(tab); // or at least u think so
        printf("Free to go\n");
    }
    printf("Good bye.\n");
    return(0);
}
Une fois compilé, mettons sous le nom memLeak.run (donc ceci est bien l'eécutable obtenu avec e.g. gcc), on peut l'exécuter -- il aura l'air de bien fonctionner, sans aucun problème apparent. Néanmoins, on ne libère que tab-même, donc ce programme comporte bien des fuites de mémoire. En le lançant avec valgrind ainsi
   valgrind memLeak.run
on pourra avoir un bilan de son comportement et des problèmes détectés, dont notamment la fuite de mémoire :
==18462== Memcheck, a memory error detector
==18462== Copyright (C) 2002-2009, and GNU GPL'd, by Julian Seward et al.
==18462== Using Valgrind-3.5.0 and LibVEX; rerun with -h for copyright info
==18462== Command: memLeak.run
==18462== 
ABC BCD CDE DEF Free to go
ABC BCD CDE DEF Free to go
ABC BCD CDE DEF Free to go
ABC BCD CDE DEF Free to go
ABC BCD CDE DEF Free to go
Good bye.
==18462== 
==18462== HEAP SUMMARY:
==18462==     in use at exit: 80 bytes in 20 blocks
==18462==   total heap usage: 25 allocs, 5 frees, 240 bytes allocated
==18462== 
==18462== LEAK SUMMARY:
==18462==    definitely lost: 80 bytes in 20 blocks
==18462==    indirectly lost: 0 bytes in 0 blocks
==18462==      possibly lost: 0 bytes in 0 blocks
==18462==    still reachable: 0 bytes in 0 blocks
==18462==         suppressed: 0 bytes in 0 blocks
==18462== Rerun with --leak-check=full to see details of leaked memory
==18462== 
==18462== For counts of detected and suppressed errors, rerun with: -v
==18462== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 2 from 2)
Cet outil dispose de nombreuses options, et peut s'avérer très utile pour aiguiller une telle "investigation". Vous pouvez rajouter la boucle qui manque pour libèrer les zones mémoire pointées par chaque élément de tab, corrigeant ainsi ce programme, et lancer ensuite valgrind dessus. Vous devez voir comme cette fois-là, valgrind semble content.

Que se passe-t-il quand on rajoute execve() dans tout cela?

L'appel système execve() remplace le code en cours d'exécution du processus P appelant avec le code d'un exécutable X donné en argument (à execve()) et démarre son exécution, utilisant également les arguments supplémentaires (donnés à execve()) pour remplir l'argv du main() de ce nouvel exécutable X.
En faisant cette opération (complexe, mais fondamentale pour Unix), le noyau réinitialise la plupart des attributs du processus P, pour "nettoyer" le tout, en faisant donc la place propre pour que X "démarre bien". Entre autres, un appel brk(0) est automagiquement fait "en tout début" de X, et ceci a pour effet de "nettoyer" aussi le tas, donc tout ce qui aurait été alloué dessus (avec malloc(), new, etc.).
Donc a priori "on pourra faire ce qu'on veut" avant l'execve(), puisque après celui-ci, tout est à nouveau propre. En fait, pas tout à fait, car Autrement, si "on prend soin", on peut donc en effet nous baser là-dessus pour que l'execve() et ce qui suit automagiquement "nettoient le tout". (Et parfois on n'a pas le choix). En occurence, si avant l'execve() on fait seulement "quelques" préparatifs, alors "tout va bien".
C'est donc ce qui se passe dans l'exercice 6 du TP Processus. De surcroît, ici on n'a pas le choix car on a bien besoin de passer la commande X et ses arguments à execve(), mais, comme on l'a dit, "tout va bien".

Analyse de l'exercice 6 du TP Processus

Le but du jeu ici est d'utiliser strace -f pour voir l'enchaînement des appels système et les changements successifs des zones mémoire du processus durant son exécution, regardant dans /proc/LePIDduProcFils/maps au fur et à mesure qu'on l'exécute. C'est pour cela que la solution de cet exercice est parsemée de sleep() (et de plus, ceux-ci sont bien visibles avec strace en tant qu'appels système à nanosleep()).
Enfin, pour une analyse "pure" avec strace on se prend un X trè simple et "gentil" à execve()er. Écrivez donc ce tout petit programme m.c de trois lignes
int main() {
    sleep(30);
    return(0);
}
et compilez-le en m.run. Ainsi, ceci sera notre X (sans arguments).
Regardons en détail le code de la première variante de la solution de l'exercice 6 du TP Processus (donc celui appelé varexo_06.cxx). Notamment, le premier appel à new engendrera à un appel système à brk(unGrandNombreEnHexa) (visible bien entendu avec strace -f). Une fois cet appel exécuté, dans /proc/LePIDduProcFils/maps on pourra alors voir effectivement apparaître une ligne avec le mot heap commençant exactement à l'adresse donné en retour de l'appel système à brk(unGrandNombreEnHexa).
La suite de ces explications est à lire deux fois: une fois vous ne faites que la lire, pour comprendre. Ensuite, vous pouvez refaire ces manipulations pas à pas, et donc en relisant le texte qui suit.
  1. On ouvre deux terminaux. Dans un terminal on lance
  2.    strace -f exo_06.run 'm.run' 2>&1 tee log.txt
    
    et on commence à voir des choses du genre
    [pid 19630] write(2, "My pid is ", 10My pid is )  = 10
    [pid 19630] write(2, "19630", 519630)        = 5
    [pid 19630] write(2, "\n", 1
    )           = 1
    [pid 19630] rt_sigprocmask(SIG_BLOCK, [CHLD], [], 8) = 0
    [pid 19630] rt_sigaction(SIGCHLD, NULL, {SIG_DFL, [], 0}, 8) = 0
    [pid 19630] rt_sigprocmask(SIG_SETMASK, [], NULL, 8) = 0
    [pid 19630] nanosleep({1, 0}, 0x7ffff2801c20) = 0
    [pid 19630] rt_sigprocmask(SIG_BLOCK, [CHLD], [], 8) = 0
    [pid 19630] rt_sigaction(SIGCHLD, NULL, {SIG_DFL, [], 0}, 8) = 0
    [pid 19630] rt_sigprocmask(SIG_SETMASK, [], NULL, 8) = 0
    [pid 19630] nanosleep({2, 0}, 0x7ffff2801c20) = 0
    
  3. Alors on récupère ainsi le pid (19630) et "vite-vite" on va dans l'autre terminal et on fait
  4.    less /proc/19630/maps
    
    et on voit plein de lignes de ce fichier-là avec les zones mémoires dont le noyau a muni notre processus
    00400000-00403000 r-xp 00000000 08:07 14344203                           /home/....
    7fc087321000-7fc087477000 r-xp 00000000 08:06 156058                     /lib64/libc-2.11.2.so
    ...
    7fffec018000-7fffec039000 rw-p 00000000 00:00 0                          [stack]
    ...
    
    Par contre, on ne voit pas de ligne avec le mot heap (pas encore).
  5. Quittons alors le less (avec la touche q) dans cet autre terminal (gardons le terminal ouvert).
  6. Revenons à l'autre terminal et continuons à regarder le déroulement :
  7. ...
    [pid 19630] nanosleep({3, 0}, 0x7ffff2801c20) = 0
    [pid 19630] rt_sigprocmask(SIG_BLOCK, [CHLD], [], 8) = 0
    [pid 19630] rt_sigaction(SIGCHLD, NULL, {SIG_DFL, [], 0}, 8) = 0
    [pid 19630] rt_sigprocmask(SIG_SETMASK, [], NULL, 8) = 0
    [pid 19630] nanosleep({4, 0}, 0x7ffff2801c20) = 0
    [pid 19630] rt_sigprocmask(SIG_BLOCK, [CHLD], [], 8) = 0
    [pid 19630] rt_sigaction(SIGCHLD, NULL, {SIG_DFL, [], 0}, 8) = 0
    [pid 19630] rt_sigprocmask(SIG_SETMASK, [], NULL, 8) = 0
    [pid 19630] nanosleep({1, 0}, 0x7ffff2801c20) = 0
    [pid 19630] brk(0)                      = 0x605000
    [pid 19630] brk(0x626000)               = 0x626000
    [pid 19630] rt_sigprocmask(SIG_BLOCK, [CHLD], [], 8) = 0
    [pid 19630] rt_sigaction(SIGCHLD, NULL, {SIG_DFL, [], 0}, 8) = 0
    [pid 19630] rt_sigprocmask(SIG_SETMASK, [], NULL, 8) = 0
    [pid 19630] nanosleep({1, 0}, 0x7ffff2801c20) = 0
    
  8. Nous venons de voir le brk(unGrandNombreEnHexa) en question.
  9. Regardons vite notre /proc/19630/maps à nouveau (juste touche Flêche-en-haut et ensuite touche Entrée (si vous utilisez bash), pour rappeler la dernière commande dans l'autre terminal). Cette fois on voit bien une nouvelle ligne
  10. 00605000-00626000 rw-p 00000000 00:00 0                                  [heap]
    
    qui est apparue suite à l'exécution du brk(0x626000). (Si on ne l'aperçoit pas tout de suite "à l'oeil nu", on peut la chercher en tapant /heap (le / annonce à less qu'on souhaite qu'il nous cherche une chaîne de caractères dans le fichier en cours d'examination)).
  11. Quittons à nouveau le less et revenons à l'autre terminal avec l'exécution.
  12. On y voit par la suite apparaître d'autres nanosleep(), etc., et enfin l'execve() de notre m.run, suivi d'un nouveau brk(0), et après plein d'autres appels, on voit enfin un nanosleep(30), qui est celui du programme m.c.
  13. [pid 19630] execve("/home/.../m.run", ...) = 0
    [pid 19630] brk(0)                      = 0x602000
    [pid 19630] mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7eff6fc8a0
    ...
    [pid 19630] rt_sigprocmask(SIG_SETMASK, [], NULL, 8) = 0
    [pid 19630] nanosleep({30, 0}, 0x7ffff3e98f50) = 0
    
  14. Une dernière fois on va alors vite dans l'autre terminal et on regarde à nouveau notre /proc/19630/maps, et là on voit un paysage bien différent. Les premières lignes maintenant parlent de m.run (et non plus d'exo_06.run), et il n'y a plus de ligne avec heap.
Vous pouvez remplacer les less par des cat et même sauvegarder dans des fichiers leur résultats. Ainsi vous aurez le déroulement complet, ensemble avec log.txt sauvegardé avec le tee comme suggéré plus haut et vous allez ensuite pouvoir réexaminer le déroulement pour mieux comprendre le tout.