• Aucun résultat trouvé

Utilisation de malloc( )

Dans le document Programmation système en C sous (Page 166-169)

Pour allouer une nouvelle zone de mémoire, on utilise généralement la fonction malloc(

), dont le prototype est déclaré dans <stdlib.h> ainsi : void * malloc (size_t taille);

L'argument transmis correspond à la taille, en octets, de la zone mémoire désirée. Le type size_t étant non signé, il n'y a pas de risque de transmettre une valeur négative. Si on demande une taille valant zéro, la version Gnu de malloc( ) renvoie un pointeur NULL.

Sinon, le système nous accorde une zone de la taille voulue et renvoie un pointeur sur cette zone. Si la mémoire disponible ne permet pas de faire l'allocation, malloc( ) renvoie un pointeur NULL. Il est fortement recommandé de tester le retour de toutes les demandes d'allocation. Le code demandant d'allouer une nouvelle structure de type ma_struct_t serait donc :

ma_struct_t * ma_struct;

if ((ma_struct = (ma_struct_t *) malloc (sizeof(ma_struct_t))) == NULL) { fprintf (stderr, "Pas assez de mémoire pour la structure voulue n");

exit (1);

}

Remarquons au passage que nous convertissons explicitement le pointeur renvoyé par malloc( ) — de type void * — en un pointeur sur notre type de donnée, permettant ainsi d'éviter des avertissements du compilateur.

Le problème qui se pose souvent au programmeur est de savoir quoi faire en cas d'échec d'allocation mémoire. En effet, il est possible que la mémoire du système se libère, lors de la terminaison d'un processus gourmand, et que l'allocation réussisse si on la tente de nouveau quelques instants plus tard, mais on peut estimer aussi que l'état de surcharge du système est tel qu'il est probablement inutilisable, et qu'il vaut mieux terminer l'application au plus vite pour redonner la main à l'utilisateur, qui devra éliminer les

Malheureusement, dans certains cas extrêmes, le fait qu'une allocation mémoire ait réussi ne signifie pas que le processus puisse effectivement utiliser la mémoire qu'il croit disponible. Pour comprendre ce problème, il est nécessaire de s'intéresser au mécanisme détaillé de la gestion de la mémoire virtuelle sous Linux.

Un processus dispose d'un espace d'adressage linéaire immense, s'étendant jusqu'à 3 Go.

Cet espace est découpé en segments ayant des rôles bien particuliers. Le processus peut connaître les limites de ses segments en utilisant des variables externes remplies par le chargeur de programmes du noyau :

Le segment nommé text contient le code exécutable du processus ; il s'étend jusqu'à l'adresse contenue dans la variable _etext. Le début de ce segment varie selon le format de fichier exécutable. Dans le segment de code se trouvent également les routines des bibliothèques partagées utilisées par le processus.

• Le segment des données initialisées au chargement du processus et des données locales statiques des fonctions est nommé data. Il s'étend de l'adresse contenue dans la variable _etext jusqu'à celle contenue dans _data.

• Le segment des données non initialisées et des données allouées dynamiquement est nommé bss. Il s'étend de l'adresse contenue dans _data à celle contenue dans _end.

À l'autre bout de l'espace d'adressage se trouvent d'autres données comme les variables d'environnement et la pile du processus. Ces éléments ne nous concernent pas directement ici.

Lorsqu'on appelle la routine malloc( ), de la bibliothèque C, celle-ci invoque l'appel-système brk( ), qui est déclaré par le prototype suivant :

int brk (void * pointeur_end) ;

Cet appel-système permet de positionner la variable _end, modifiant ainsi la taille du segment bss. Il existe également une fonction de bibliothèque nommée sbrk( ) et déclarée ainsi :

environnement Figure 13.1

Espace

d'adressage d'un processus

pile variables automatiques

tas

variables globales non initialisées variables statiques variables globales initialisées

code exécutable text

data bss

0 stack

env

_etext _data _end 3 Go

Celle-ci augmente ou diminue la taille du segment bss de l'incrément fourni en argument.

En réalité, malloc( )gère elle-même un ensemble de blocs mémoire qu'elle garde à disposition du processus, et ne fait appel à sbrk( ) qu'occasionnellement. Lorsque l'incrément est négatif, sbrk( ) sert à libérer de la mémoire. Le problème — si on peut dire — est que l'appel sbrk( ) n'échoue que très rarement. En effet, les cas d'erreur sont les suivants :

• Le processus essaye de libérer de la mémoire appartenant au segment de code text.

• Le processus essaye de dépasser sa limite RLIMIT_DATA fixée par la routine setrlimit( ) que nous avons vue dans le chapitre concernant l'exécution des programmes.

• La nouvelle zone de données va déborder sur une zone de projection mémoire telle que nous en verrons dans le prochain chapitre.

• L'allocation demandée excède la taille de la mémoire virtuelle globale du système (moins les valeurs minimales des buffers et du cache, ainsi qu'une marge de sécurité de 2 %).

Le problème principal se pose avec le dernier point. En effet, lorsque le noyau a augmenté la taille du segment de données d'un processus, il n'a pas pour autant réservé de la place effective dans la mémoire du système. Le principe de la mémoire virtuelle fait que c'est unique-ment au moment où le processus tente d'écrire dans la zone nouvellement allouée que le système déclenche une faute d'accès et lui attribue une page mémoire réelle. Il est donc possible de réclamer beaucoup plus de mémoire que le système ne peut en fournir, sans pour autant que les allocations échouent.

Si la machine dispose par exemple de 128 Mo de mémoire virtuelle, une demande d'allocation de 128 Mo en une fois échouera. Par contre, 128 demandes (ou plus) d'un Mo chacune seront acceptées tant qu'on n'aura pas essayé d'écrire réellement dans les zones allouées.

À titre d'exemple, ma machine actuelle contient 128 Mo de mémoire vive et 128 Mo de swap. Même sans prendre en compte le fait que le système fait tourner X-Window, Kde, un lecteur de CD audio, la suite bureautique sur laquelle je rédige ce texte et plusieurs Xterms, je ne peux vraiment pas compter disposer dans une application de plus de 256 Mo de mémoire. Et pour-tant le code suivant fonctionne sans erreur :

exemple_malloc_1.c #include <stdio.h>

#include <stdlib.h>

#define NB_BLOCS 257 #define TAILLE (1024*1024) int

main (void) {

int i;

char * bloc [NB_BLOCS];

for (i = 0; i < NB_BLOCS; i ++) {

if ((bloc [i] = (char *) malloc (TAILLE)) == NULL) {

fprintf (stderr, "Alloués : %d blocs de %d Ko \n", i, TAILLE / 1024);

return (0);

}

En voici l'exécution :

$ ./exemple_malloc_1

Alloués : 257 blocs de 1024 Ko

$

257 Mo alloués dans une mémoire virtuelle qui n'en comporte que 256, espace de swap compris !

Si nous essayons d'utiliser les zones allouées, par contre, le comportement est différent.

Nous remplissons avec des zéros la mémoire renvoyée au fur et à mesure.

exemple_malloc_2.

#include <stdio.h>

#include <stdlib.h>

#define NB_BLOCS 257 #define TAILLE (1024*1024) int

main (void) {

int i;

char * bloc [NB_BLOCS];

for (i = 0; i < NB_BLOCS; i++) {

if ((bloc [i] = (char *) malloc (TAILLE)) == NULL) { fprintf (stderr, "Échec pour i = %d\n", i);

break;

}

fprintf (stderr, "Remplissage de %d\n", i);

memset (bloc [i], 0, TAILLE);

}

fprintf (stderr, "Alloués : %d blocs \n", i);

return (0);

}

Nous lançons le programme avec l'utilitaire nice, afin d'essayer de ne pas trop bloquer le reste du système (il faut quand même éviter de lancer ce processus sur une machine ouverte à plusieurs utilisateurs, le ralentissement du système dû à l'usage intensif du périphérique de swap est sensible).

$ nice ./exemple_malloc_2 Remplissage de 0

Remplissage de 1 Remplissage de 2 Remplissage de 3 Remplissage de 4 Remplissage de 5 [...]

Remplissage de 198 Remplissage de 199 Remplissage de 200 Remplissage de 201 Remplissage de 202 Échec pour i = 203 Alloués : 203 blocs

$

Cette fois-ci, nous voyons que le programme échoue effectivement lorsqu'il n'y a plus de mémoire utilisable. Le problème c'est donc le risque qu'une allocation réussisse, alors qu'elle conduira par la suite à des dégradations sensibles des performances du système lorsqu'on tentera d'utiliser les zones allouées. Pour limiter au maximum cette éventualité, on essayera toujours de réclamer uniquement la mémoire dont on a réellement besoin au moment voulu, et on utilisera systématiquement les pages mémoire allouées le plus vite possible.

On pourrait être tenté d'utiliser par principe calloc( ) à la place de malloc( ) , car cette fonction effectue l'initialisation à zéro de toute la zone allouée. Pourtant, cela ne marcherait pas non plus dans certains cas. On notera tout d'abord que calloc( ) est une simple fonction de bibliothèque, et qu'il y a toujours un risque de voir un processus concurrent s'allouer une énorme zone de mémoire et la remplir aussitôt entre le moment où calloc( ) a fait appel à sbrk( ) et le moment où il initialise la nouvelle mémoire. Par ailleurs, pour des raisons d'efficacité, calloc( ) ne fait pas nécessairement des écritures en mémoire mais peut utiliser — surtout pour de grosses allocations — la fonction mmap(

) , que nous verrons dans le prochain chapitre, pour obtenir une zone remplie de zéros en projetant le périphérique /dey/zero.

La seule méthode vraiment efficace pour s'assurer la disponibilité des zones allouées est donc de toujours écrire rapidement dans les nouvelles données et d'éviter d'appeler successivement malloc( ) plusieurs fois de suite sans avoir rempli la mémoire fournie entre-temps.

On pourrait légitimement se demander si l'utilisation directe de sbrk( ) ne serait pas plus simple que celle de malloc( ). En fait, la présentation que nous avons faite du rôle de malloc( ) — fonction de bibliothèque — vis-à-vis de l'appel-système brk( ) est largement simplificatrice. En fait, malloc( ) assure des fonctionnalités bien plus complexes que le simple agrandisse-ment de la zone de données pour renvoyer un pointeur sur la mémoire allouée :

• Alignement : malloc( ) garantit que la mémoire fournie sera correctement positionnée afin de pouvoir y stocker n'importe quel type de donnée. Cela signifie que le processeur pourra manipuler directement les types entiers ou réels qu'on placera dans la mémoire allouée. Sur la plupart des machines, l'alignement des données est réalisé tous les 8 octets (taille d'un double ou d'un long long int sur les x86). Sur les architectures 64 bits, l'alignement est fixé tous les 16 octets.

• Configuration : malloc( ) offre de nombreuses possibilités de configuration de l'algorithme d'allocation, notamment en ce qui concerne le seuil où on passe d'une allocation avec sbrk( ) à une projection avec mmap( ). De plus, malloc( ) permet à l'utilisateur de fournir ses propres points d'appel qui seront invoqués dans la routine.

Cela permet d'inclure notre code de débogage personnalisé au coeur même des fonctions de bibliothèque.

• Optimisation : pour éviter le supplément de travail dû à l'appel-système sbrk( ), la fonction malloc( ) réclame des blocs plus importants que nécessaire, afin de pouvoir en fournir directement une partie lors des invocations ultérieures.

De plus, malloc( ) utilise souvent l'appel-système mmap( ) pour obtenir de gros blocs de données indépendants, faciles à restituer au système lors de leur libération. Le fonctionnement de mmap( ) n'a rien à voir avec brk( ).

Enfin, ajoutons que malloc( ) doit fonctionner correctement dans le cadre d'un processus déployant de multiples threads, en évitant les conflits d'accès simultanés à la limite de la zone de données. Pour toutes ces raisons, on voit que l'implémentation de la fonction malloc( ) est loin d'être triviale, et que les personnalisations éventuelles devront de préférence être apportées en utilisant les points d'entrée fournis par la bibliothèque GlibC plutôt qu'en tentant de réécrire une version bricolée de cette fonction.

Insistons sur un dernier point, avant de passer aux autres routines d'allocation mémoire, qui concerne l'utilisation de malloc( ) avec les chaînes de caractères. La bibliothèque C terminant toujours ses chaînes de caractères par un caractère nul, il est nécessaire d'allouer un octet de plus pour la nouvelle chaîne que la longueur désirée. Voici un exemple de fonction renvoyant une copie fraîchement attribuée de la chaîne passée en argument. Il sera du ressort de la fonction appelante de libérer la mémoire occupée par la copie lorsqu'elle n'en aura plus besoin :

char *

alloue_et_copie_chaine (char * ancienne) {

char * nouvelle = NULL;

if (ancienne != NULL) {

nouvelle = (char *) malloc (strlen (ancienne) + 1);

if (nouvelle != NULL)

strcpy (nouvelle, ancienne);

}

return (nouvelle);

}

Dans le document Programmation système en C sous (Page 166-169)