version on-line : les programmes se trouvent à la fin de l'article.
Le C perd la mémoire
La maladie d'Alzimer selon Kernigan et Richie
La mémoire des ordinateurs a toujours été assez complexe.
Toujours les concepteurs nous trouvent des excuses pour ne pas
avoir un système linéaire. Que ce soit les banks sur Amstrad,
ou les pages sur PC. Seul l'Amiga avait et a toujours un système
que l'on qualifiera de simple.
Mais dans notre cas, nous sommes sur PC : il va falloir faire
avec. Et vous avez de la chance car le PC a une mémoire
particulièrement bordélique. Aujourd'hui nous allons commencer
par programmer la mémoire conventionnelle en C.
Allouons enfants de la patrie...
Comme vous êtes des gens biens et que vous ne
voulez pas avoir un message plantant le système à la sortie de
votre programme, vous allez allouer de la mémoire. Car avoir un
tableau d'entiers de 65000 éléments est un peu lourd. Vous
risquez un débordement lors de la compilation (et ca c'est le
pavé dans la mare qui fait déborder la vase). De plus votre
tableau va prendre de la mémoire même si vous ne vous en servez
pas : avouez que c'est plutôt gênant.
Donc, vous allez quémander de la mémoire au DOS, en passant par
l'instruction FARMALLOC(long Nb). Notre premier programme fait
a bien, en désallouant la mémoire par la suite (Exemple1).
C'est NULL
J'ai oublié ma mémoire
Tout le monde me dira que ce programme marche. Mais imaginons
que votre programme est hypercomplexe, et biensûr vous ne vous
rappelez plus si vous avez déjà libéré la mémoire utilisée
par votre tableau. Or, si vous lancez le programme, vous vous
apercevrez qu'une fois la mémoire libérée, le pointeur n'a pas
bougé : il pointe vers une zone mémoire qui ne nous appartient
plus. Rajoutons donc une instruction pour la réinitialiser.
Bon, cette fois, tout est dans l'ordre. Tout ? Hmmm... Oui et
non, n'oublions pas le titre de cet article : "le C perd la
mémoire".
Alors ? Eh bien imaginons que vous voulez reprogrammer un Win'95
en lui otant tous ses bugs. Certes y'a du boulot, mais vous aurez
sûrement besoin d'allouer de la mémoire, et notamment plusieurs
blocs, dynamiquement. Comme vous être plutot désorganisé, vous
ne savez plus dans quel ordre vous avez alloué vos blocs. Vous
obtenez donc le programme suivant.
"Ca ne fait rien" entends-je au fond de l'assemblée
aujourd'hui réunie ! Tout à fait vrai : le C s'en fiche dans
quel ordre vous libérez la mémoire. Il est aussi vrai qu'avec
la plupart des compilateurs, vous pouvez "oublier" de
libérer la
mémoire ou de fermer des fichiers : le C le fera pour vous en
quittant votre programme.
Mais vous vous exposez à de gros déboirs. Si vous libérez une
zone qui n'est pas la dernière allouée elle ne sera pas
vraiment libérée car elle n'est pas accessible. Le C note dans
un coin qu'elle est libérée et la libérera automatiquement
quand elle
deviendra accessible. Pourquoi ?
"Faites face à votre pile..."
(Ou un autre jeu de mot débile dans le même genre)
Pour comprendre ce phénomènene, plongeons nous au coeur du
fonctionnement de FARMALLOC et FARFREE.
Si vous compilez un des programmes fournis et lancez le
TurboDebuger, vous vous apercevrez que FARMALLOC ne fait pas que
demander de la mémoire au DOS. En fait, il va au travers d'une
procèdure à la fois simple et utile, une sorte de gestion de
pile : LIFO (Last In, First Out).
Comme dans une pile de livres, si vous ne voulez pas que tout
s'écroule, vous n'avez accès qu'à celui au-dessus de la pile.
Imaginez maintenant que chaque livre représente une zone de
mémoire. Si vous libérez le livre du dessus, c'est cool, vous
le jetez, et celui d'en dessous devient celui du dessus. Ok ?
Mais là on a se complique, c'est lorsque vous voulez
atteindre le livre tout en bas de la pile, donc le premier livre
qui a constitué la pile. C'est un peu comme jouer au mikado avec
des Séquoias, c'est complexe.
Pour éviter d'avoir à tout désempiler (ce qui reviendrai à
faire un Hanoi, pour ceux qui connaissent), vous
"marquez" simplement que celui d'en dessous est
"virtuellement" libéré. Quand tout ceux au-dessus
seront partis, on dégagera
physiquement ce livre devenu accessible.
Seulement, ce système, qui reste pratiquement transparent
tant pour le programmeur que l'utilisateur a un gros défaut :
c'est qu'entre une "mémoire libre" et une
"mémoire libérée" il y a une grosse différence. La
mémoire d'en-dessous est
libérée ("Marquée libre") mais n'est pas libre en
réalité.
Résultat ? Imaginez que vous avez 400 Ko de mémoire libre. Vous
allouez un bloc A de 399 Ko, puis un bloc B de 1Ko.
Vous libérez alors le bloc A. Théoriquement, vous disposez de
399 Ko de libre. Mais essayez d'allouer ne serait-ce qu'1 Ko :
PAF ! Eh oui, tant que le bloc B existe (qui est au-dessus du
bloc A) le bloc B ne sera pas réellement libre.
La folie du LIFO
L'inventeur de la pile (Mr Volta) mangeait de la
viande bovine britannique.
Les solutions sont alors variées. On pourrait reprogrammer
entièrement la gestion de la mémoire en remplaant la pile
LIFO par une liste chaînée (ou double chaînée) qui nous
permettrait de libérer n'importe quelle zone n'importe quand.
Mais bon. Nous aborderont les listes chainées dans quelques
numéros, dans les cours de C++.
A priori, si le programme veut libérer une zone se trouvant au
milieu de la pile, c'est qu'il a été mal programmé. Pensez
donc lorsque vous programmez à l'ordre d'allocation. La durée
de vie de la mémoire dynamique doit être proportionnelle à sa
profondeur dans la pile.
Mais dans les grands programmes, difficile de s'y retrouver. Le
programme suivant permet de détecter
qu'une mémoire n'a pas vraiment été libéré. On aurait
également pu faire plus compliqué, en vérifiant la mémoire
qu'on libère est au-dessus de la pile.
Dans un prochain numéro, on verra comment parcourir toute la
pile pour améliorer encore sa gestion.
Merci, de votre attention, bon courage à tous. N'hésitez pas à
nous écrire pour tous commentaires, corrections, idées...
#include <alloc.H> // Permet d'utiliser la mmoire en C #include <stdio.H> void main(void) { void far *Buffer; // Pointeur sur la zone alloue Buffer=farmalloc(16000); // Allocation de 16000 octets printf("%p %lu\n",Buffer,farcoreleft()); // Affichage du pointeur // et de la mmoire restante farfree(Buffer); // Libration de la mmoire printf("%p %lu\n",Buffer,farcoreleft()); // Affichage du pointeur // et de la mmoire restante } //
#include <alloc.H> // Permet d'utiliser la mmoire en C #include <stdio.H> void main(void) { void far *Buffer; // Pointeur sur la zone alloue Buffer=farmalloc(16000); // Allocation de 16000 octets printf("%p %lu\n",Buffer,farcoreleft()); // Affichage du pointeur // et de la mmoire restante farfree(Buffer); Buffer = NULL; // Libration de la mmoire printf("%p %lu\n",Buffer,farcoreleft()); // Affichage du pointeur // et de la mmoire restante } //
#include <alloc.H> #include <stdio.H> void main(void) { void far *Buffer1,*Buffer2,*Buffer3; Buffer1=farmalloc(16000); Buffer2=farmalloc(10000); Buffer3=farmalloc(16000); farfree(Buffer2); Buffer2 = NULL; printf("%p %lu\n",Buffer2,farcoreleft()); farfree(Buffer3); Buffer3 = NULL; printf("%p %lu\n",Buffer3,farcoreleft()); farfree(Buffer1); Buffer1 = NULL; printf("%p %lu\n",Buffer1,farcoreleft()); } //
#include <alloc.H> #include <stdio.H> #include <process.H> void far *Personnal_farfree(void far *zone) { long MemRest; MemRest=farcoreleft(); farfree(zone); if (MemRest==farcoreleft()) // La mmoire dispo n'a pas change : Erreur { printf("Memory LIFO error\n");exit(1); } return NULL; } void main(void) { void far *Buffer1,*Buffer2; Buffer1=farmalloc(16000); Buffer2=farmalloc(10000); Buffer1 = Personnal_farfree(Buffer1); Buffer2 = Personnal_farfree(Buffer2); }//