TP 07 - Gestion de la mémoire

Gestion de la mémoire

Sle système de gestion de la mémoire d'un système d'exploitation est utilisé par tous les autres sous-systèmes: planificateur, E / S, système de fichiers, gestion des processus, mise en réseau. La mémoire est une ressource importante, c'est pourquoi des algorithmes efficaces pour son utilisation et sa gestion sont nécessaires.

Le rôle du sous-système de gestion de la mémoire est de:

  • garder une trace des zones de mémoire physique (occupée ou libre)
  • pour fournir aux processus ou autres sous-systèmes un accès à la mémoire
  • mapper les pages de mémoire virtuelle d'un processus (pages) sur les pages physiques (frames).

Le noyau du système d'exploitation propose un ensemble d'interfaces (appels système) qui permettent l'allocation / partage de mémoire, le mappage de régions de mémoire virtuelle sur des fichiers, le partage de zones de mémoire.

Malheureusement, le niveau limité de compréhension de ces interfaces et des actions qui se produisent derrière elles conduit à un certain nombre de problèmes qui sont couramment rencontrés dans les applications logicielles: fuites de mémoire, accès invalides, écrasement, débordement de tampon, corruption de zone mémoire.

Il est donc essentiel de connaître le contexte dans lequel le sous-système de gestion de la mémoire fonctionne et de comprendre l'interface fournie au programmeur par le système d'exploitation.

L'espace d'adressage d'un processus

L'espace d'adressage d'un processus

L'espace d'adressage d'un processus ou, mieux encore, l'espace d'adressage virtuel d'un processus est la zone de mémoire virtuelle qu'un processus peut utiliser. Chaque processus a son propre espace d'adressage. Même dans les situations où deux processus partagent une zone mémoire, l'espace virtuel est distinct, mais il est mappé sur la même zone mémoire physique.

La figure suivante montre un espace d'adressage typique pour un processus. Dans les systèmes d'exploitation modernes, dans l'espace virtuel de chaque processus, la mémoire du noyau est mappée, elle peut être mappée au début ou à la fin de l'espace d'adressage. Ensuite, nous ferons uniquement référence à l'espace d'adressage dans l'espace utilisateur pour un processus.

Les 4 zones importantes de l'espace d'adressage du processus sont la zone de données, la zone de code, la pile et le tas. Comme le montre la figure, la pile et le tas sont les zones qui peuvent croître. En fait, ces deux domaines sont dynamiques et n'ont de sens que dans le contexte d'un processus. De l'autre côté, les données dans la zone de données et la zone de code sont décrites dans l'exécutable.

Parce que Python est un langage interprété, toutes les variables sont affectées sur le tas. Pour pouvoir analyser la mémoire allouée, nous devons utiliser un langage compilé. Par conséquent, dans ce laboratoire, nous utiliserons des exemples en langage C.

Zone de code

Le segment de code (également appelé segment de texte) représente les instructions en langage machine du programme. Le registre du pointeur d'instruction (IP) fera référence aux adresses dans la zone de code. Lisez l'instruction indiquée par l'IP, décodez-la et interprétez-la, puis incrémentez le compteur de programme et passez à l'instruction suivante. La zone de code est généralement une zone en lecture seule afin que le processus ne puisse pas modifier ses instructions en utilisant le mauvais pointeur. La zone de code est partagée entre tous les processus exécutant le même programme. Ainsi, une seule copie du code est mappée à l'espace d'adressage virtuel de tous les processus.

Zones de données

Les zones de données contiennent les variables globales définies dans un programme et les variables en lecture seule. Il existe plusieurs sous-types de zones de données selon le type de données.

.data

La zone .data contient les variables globales et les variables statiques initialized aux valeurs nulles d'un programme. Par exemple:

static int a = 3;
char b = 'a';

.bss

La zone .bss contient les variables globales et statiques initialized d'un programme. Avant l'exécution du code, ce segment est initialisé avec 0. Par exemple:

static int a;
char b;

En général, ces variables ne seront pas préallouées dans l'exécutable, mais lors de la création du processus. L'allocation de la zone .bss se fait sur des pages mises à zéro.

.rodata

La zone .rodata contient des informations qui ne peuvent être lues que et non modifiées. Voici les literals stockés:

"Hello, World!"
"En Taro Adun!"

et contenu. Toutes les variables globales déclarées avec le mot-clé const seront placées dans les données . Les variables locales déclarées comme “const” seront placées sur la pile, donc dans une zone mémoire non marquée comme en lecture seule et peuvent être modifiées par un pointeur sur elles. Un cas particulier est représenté par les variables locales constantes déclarées avec le mot-clé statique qui seront mises dans le .data :

const int a;           /* în .rodata */
const char ptr[];      /* în .rodata */
 
void myfunc(void)
{
   int x;              /* pe stivă */
   const int y;        /* pe stivă */
 
   static const int z; /* în .rodata */
 
   static int p = 8;   /* în .data */
   static int q;       /* în .bss */
   ...

Pile

La pile est une région dynamique au sein d'un processus, gérée automatiquement par le compilateur.

La pile est utilisée pour stocker des “cadres de pile”. Un nouveau “cadre de pile” sera créé pour chaque appel de fonction.

Un “cadre de pile” contient:

  • variables locales
  • arguments de fonction
  • adresse de retour

Sur la grande majorité des architectures modernes, la pile croît (des grandes adresses aux petites adresses) et le tas augmente. La pile augmente à chaque appel de fonction et diminue à chaque retour de la fonction.

La figure ci-dessous montre une vue conceptuelle sur la pile lors de l'appel d'une fonction.

Tas (Heap)

Le tas est la zone de mémoire dédiée à l'allocation dynamique de mémoire. Le segment de mémoire est utilisé pour allouer des régions de mémoire dont la taille est déterminée lors de l'exécution.

Comme la pile, le tas est une région dynamique qui change sa taille. Contrairement à la pile, cependant, le tas n'est pas géré par le compilateur. Il est du devoir du programmeur de savoir combien de mémoire allouer et de se rappeler combien allouer et quand allouer. Les problèmes courants dans la plupart des programmes sont la perte de références aux zones allouées (fuites de mémoire) ou la référence de zones non allouées ou insuffisamment allouées (accès invalides).

Dans des langages comme Java, Lisp, etc. là où il n'y a pas de “liberté de pointeur”, l'effacement de l'espace alloué se fait automatiquement via un garbage collector. Sur ces systèmes, le problème de la perte de références est évité, mais le problème de référence de zones non allouées reste actif.

Allocation / affectation de mémoire

L'allocation de mémoire est effectuée statiquement par le compilateur ou dynamiquement pendant l'exécution. L'allocation statique est effectuée dans des segments de données pour les variables globales ou pour les littéraux.

Lors de l'exécution, les variables sont allouées sur la pile ou dans le tas. L'allocation de pile est automatiquement effectuée par le compilateur pour les variables locales d'une fonction (à l'exception des variables locales préfixées par l'identifiant statique ).

L'allocation dynamique se fait en tas. L'allocation dynamique se produit quand on ne sait pas, au moment de la compilation, combien de mémoire sera nécessaire pour une variable, une structure, un vecteur. Si, au moment de la compilation, on sait combien d'espace une variable occupera, son allocation statique est recommandée, afin d'éviter les erreurs qui se produisent fréquemment dans le contexte de l'allocation dynamique.

Afin de fragmenter le moins possible l'espace d'adressage du processus, à la suite d'allocations et de désallocations de zones de différentes tailles, l'allocateur de mémoire organisera le segment de données alloué dynamiquement en tas , d'où le nom du segment.

Débordement de mémoire signifie libérer de la zone de mémoire précédemment allouée (elle est marquée comme libre).

Si le verrouillage d'une zone mémoire est omis, il restera alloué pendant toute la durée d'exécution du processus. Chaque fois qu'une zone mémoire n'est plus nécessaire, elle doit être compensée pour l'efficacité d'utilisation de l'espace mémoire.

Il n'est pas nécessaire de traiter différents domaines avant un appel exit ou avant la fin du programme car ils sont automatiquement libérés par le système d'exploitation.

Dans la plupart des cas, des problèmes surviennent si vous essayez de colocaliser deux fois la même zone de mémoire. En effet, les données de gestion interne des zones allouées sont corrompues. Habituellement, le noyau signale le problème avec une interruption synchrone.

Allocation de mémoire sous Linux

Sous Linux, l'allocation de mémoire pour les processus utilisateur se fait via les fonctions de bibliothèque malloc, calloc et realloc, et sa collocation à travers la fonction free. Ces fonctions représentent les appels de bibliothèque et résolvent, dans la mesure du possible, l'allocation de mémoire et les demandes d'allocation dans l'espace utilisateur.

Implémentation de la fonction malloc

Implémentation de la fonction malloc


Implémentation de la fonction malloc dépend du système d'exploitation.

Il existe des implémentations qui conservent des tables qui spécifient les zones de mémoire allouées au tas. S'il y a des zones libres sur le tas, appelez malloc cil nécessite une zone de mémoire qui peut être encadrée dans une zone libre du tas sera satisfaite immédiatement, marquant la zone dans la table comme étant allouée et renvoyant au programme appelant un pointeur vers celle-ci.

Si, au lieu de cela, une zone est demandée qui ne rentre dans aucune zone sans segment de mémoire, malloc va tenter d'étendre le tas par appel système brk ou mmap.

Il existe des implémentations qui, pour chaque zone de mémoire requise avec malloc ajoute un en-tête qui contient des informations utiles - taille d'une zone, pointeur vers la zone suivante, que la zone ait été effacée ou non.

Plus de détails sur malloc et comment organiser le tas ici et ici.


void *malloc(size_t size);
void *calloc(size_t nmemb, size_t size);
void *realloc(void *ptr, size_t size);
void free(void *ptr);

Toujours relâcher (free) mémoire allouée. La mémoire allouée au processus est automatiquement libérée à la fin du processus, mais, par exemple, dans le cas d'un processus serveur qui s'exécute depuis longtemps et ne libère pas la mémoire allouée, il finira par occuper toute la mémoire disponible dans le système, ce qui aura des conséquences néfastes.

Avertissement! Ne libérez pas deux fois la même zone de mémoire, car cela entraînerait la corruption des tables malloc qui aura à nouveau des conséquences néfastes. Depuis la fonction free renvoie immédiatement s'il reçoit en paramètre un pointeur NULL , il est recommandé qu'après un appel free, le pointeur est réinitialisé sur NULL .

Voici quelques exemples d'utilisation de l'allocation de mémoire malloc:

int n = atoi(argv[1]);
char *str;
 
/* généralement malloc reçoit l'argument taille comme:
   num_elements * size_of_element */
str = malloc((n + 1) * sizeof(char));
if (NULL == str) {
perror("malloc");
exit(EXIT_FAILURE);
}
 
[...]
 
free(str);
str = NULL;
/* Création d'un tableau de références aux arguments reçus par un programme */
char **argv_no_exec;
 
/* allouer de l'espace pour le tableau */
argv_no_exec = malloc((argc - 1) * sizeof(char*));
if (NULL == argv_no_exec) {
perror("malloc");
exit(EXIT_FAILURE);
}
 
/* définir des références aux arguments du programme */
for (i = 1; i < argc; i++)
argv_no_exec[i-1] = argv[i];
 
[...]
 
free(argv_no_exec);
argv_no_exec = NULL;

L'appel realloc est utilisé pour changer l'espace mémoire alloué par un appel malloc ou calloc:

int *p;
 
p = malloc(n * sizeof(int));
if (NULL == p) {
perror("malloc");
exit(EXIT_FAILURE);
}
 
[...]
 
p = realloc(p, (n + extra) * sizeof(int));
 
[...]
 
free(p);
p = NULL;

L'appel calloc est utilisé pour allouer des zones de mémoire dont le contenu est nul (plein de valeurs nulles). Contrairement à malloc, l'appel recevra deux arguments: le nombre d'éléments et la taille d'un élément.

list_t *list_v; /* list_t could be any C type ( except void ) */
 
list_v = calloc(n, sizeof(list_t));
if (NULL == list_v) {
perror("calloc");
exit(EXIT_FAILURE);
}
 
[...]
 
free(list_v);
list_v = NULL;

Attention Selon la norme C, il est redondant (et considéré comme une mauvaise pratique) de faire cast à la valeur retournée par malloc.

int *p = (int *)malloc(10 * sizeof(int));

malloc renvoie void * qui en C est automatiquement converti en le type souhaité. De plus, si le cast est fait et que l'en-tête stdlib.h requis pour la fonction malloc n'est pas inclus, aucune erreur ne sera générée! Sur certaines architectures, ce cas peut conduire à un comportement indéfini. Contrairement à C, en C ++, vous avez besoin d'un cast. La discussion est élaborée ici .

Vous trouverez plus d'informations sur les fonctions d'allocation dans le manuel de bibliothèque standard C et dans la page de manuel man malloc.

Travailler avec la mémoire - Problèmes

Travailler avec le tas est l'une des principales causes de l'émergence de problèmes de programmation. Travailler avec des pointeurs, la nécessité d'utiliser les appels système / bibliothèque pour l'allocation / concession, peut conduire à un certain nombre de problèmes qui affectent le fonctionnement du programme (souvent fatal).

Les problèmes les plus fréquemment rencontrés lors de l'utilisation de la mémoire sont les suivants:

  • accès mémoire invalide - qui implique l'accès à des zones qui n'ont pas été allouées ou qui ont été libérées.
  • fuites de mémoire - situations où la référence à une zone précédemment allouée est perdue. Cette zone restera occupée jusqu'à la fin du procès.

Les problèmes et les utilitaires qui peuvent être utilisés pour les combattre seront présentés ci-dessous.

Accès invalide

En règle générale, l'accès à une zone de mémoire non valide entraîne une erreur de page et l'achèvement du processus (sous Unix, cela signifie l'envoi du signal SIGSEGV → affichage du message Erreur de segmentation ). Toutefois, si l'erreur apparaît à une adresse non valide, mais sur une page valide, le matériel et le système d'exploitation ne pourront pas signaler l'action comme non valide. En effet, l'allocation de mémoire se fait au niveau de la page. Par exemple, il peut y avoir des situations où seule la moitié de la page est utilisée. Bien que l'autre moitié contienne des adresses non valides, le système d'exploitation ne pourra pas détecter un accès non valide à cette zone.

Ces accès peuvent entraîner une corruption de tas et une perte de cohérence de la mémoire allouée. Comme nous le verrons ci-dessous, il existe des utilitaires qui aident à détecter ces situations.

Un type spécial d'accès non valide est buffer overflow. Ce type d'attaque implique de référencer des régions valides dans l'espace d'adressage du processus au moyen d'une variable qui ne devrait pas pouvoir référencer ces adresses. En règle générale, une attaque par dépassement de tampon entraîne l'exécution de code non sécurisé. La protection contre les attaques par débordement de tampon est obtenue en vérifiant les limites d'un tampon / vecteur à la compilation ou au moment de l'exécution.

Fuites de mémoire

Une fuite de mémoire apparaît dans deux situations:

  • un programme omet d'effacer une zone mémoire
  • un programme perd la référence à une zone mémoire allouée et, par conséquent, ne peut pas la libérer

Les fuites de mémoire ont pour effet de réduire la quantité de mémoire existante dans le système. On peut atteindre, dans des situations extrêmes, la consommation de toute la mémoire du système et l'impossibilité de fonctionnement de ses différentes applications.

Comme pour le problème d'accès à la mémoire invalide, Utilitaire Valgrind il est très utile pour détecter les fuites de mémoire d'un programme.

Accord double

Le nom “accord double” offre une bonne intuition sur la cause: libérer deux fois le même espace mémoire. La double désallocation peut avoir des effets négatifs car elle affecte les structures internes utilisées pour gérer la mémoire occupée.

Dans les dernières versions de la bibliothèque C standard, les cas de double désallocation sont automatiquement détectés. Prenez l'exemple ci-dessous:

dubla_dealocare.c
#include <stdlib.h>
 
int main(void)
{
char *p;
 
p = malloc(10);
free(p);
free(p);
 
return 0;
}

L'exécution de l'exécutable obtenu à partir du programme ci-dessus entraîne l'affichage d'un message de libération double glibc spécifique d'une région de mémoire et l'arrêt du programme:

 so@spook$ make
 cc -Wall -g    dfree.c   -o dfree
 so@spook$ ./dfree 
  *** glibc detected *** ./dfree: double free or corruption (fasttop): 0x0000000000601010 ***
 ======= Backtrace: =========
 /lib/libc.so.6[0x2b675fdd502a]
 /lib/libc.so.6(cfree+0x8c)[0x2b675fdd8bbc]
 ./dfree[0x400510]
 /lib/libc.so.6(__libc_start_main+0xf4)[0x2b675fd7f1c4]
 ./dfree[0x400459]

Des situations de double transaction sont également détectées par Valgrind.

Autres utilitaires pour résoudre les problèmes de travail avec la mémoire

Les utilitaires présentés ci-dessus ne sont pas les seuls utilisés pour détecter les problèmes dans travailler avec la mémoire. Les autres utilitaires sont:

Mémoire virtuelle

Le mécanisme de mémoire virtuelle est utilisé par le cœur du système d'exploitation pour implémenter une politique de gestion de mémoire efficace. Ainsi, bien que les applications utilisent actuellement la mémoire virtuelle, elles ne le font pas explicitement. Cependant, dans certains cas, les applications utilisent explicitement la mémoire virtuelle.

Le système d'exploitation fournit des primitives pour mapper des fichiers, de la mémoire ou des périphériques à l'espace d'adressage d'un processus.

  • Le mappage de fichiers en mémoire est utilisé dans certains systèmes d'exploitation pour implémenter des mécanismes de mémoire partagée. De plus, ce mécanisme permet d'implémenter la pagination à la demande et des bibliothèques partagées.
  • Le mappage de la mémoire dans l'espace d'adressage est utile lorsqu'un processus souhaite allouer une grande quantité de mémoire.
  • Le mappage d'appareil est utile lorsqu'un processus souhaite utiliser directement la mémoire d'un appareil (comme une carte vidéo).

Concepts théoriques

La taille de l'espace d'adressage virtuel d'un processus dépend de la taille des registres du processeur. Ainsi, sur un système 32 bits, un processus pourra accéder à 2 ^ 32 = 4 Go d'espace mémoire (en revanche, sur un système 64 bits, il accédera théoriquement à 2 ^ 64 B). L'espace mémoire du processus est divisé en un espace réservé aux adresses de noyau virtuel - cet espace est commun à tous les processus - et l'espace d'adressage virtuel (propre) du processus. La plupart du temps, le partage entre les deux est de 3/1 (3 Go d'espace utilisateur contre 1 Go d'espace noyau).

La mémoire physique (RAM) est répartie entre les processus actuellement actifs et le système d'exploitation. Ainsi, selon la quantité de mémoire dont nous disposons sur la machine physique, il est possible d'épuiser toutes les ressources et de ne pas pouvoir démarrer un nouveau processus. Pour éviter ce scénario, le mécanisme de mémoire virtuelle a été introduit. De cette façon, même si l'espace virtuel (composé du segment de texte, de la date, du tas, de la pile) d'un processus est plus grand que la mémoire physique disponible sur le système, le processus pourra s'exécuter en mémoire en ne chargeant que les pages dont il a besoin dans temps d'exécution (paging à la demande).

L'espace d'adressage virtuel est divisé en pages virtuelles (page). Le correspondant de la mémoire physique est la page physique (frame). La taille d'une page virtuelle est égale à celle d'une page physique. La taille est donnée par le matériel (dans la plupart des cas, une page a 4 Ko sur un système 32 bits ou 64 bits).

Tant qu'un processus en cours d'exécution n'accède qu'aux pages résidant en mémoire, il s'exécute comme s'il avait tout l'espace mappé dans la mémoire physique. Lorsqu'un processus souhaite accéder à une certaine page virtuelle, qui n'est pas mappée en mémoire, une erreur de page sera générée, et après cette page manquante, la page virtuelle sera mappée sur une page physique. Deux processus différents ont un espace virtuel différent, mais certaines pages virtuelles de ces processus peuvent être mappées sur la même page physique. Ainsi, deux processus différents peuvent partager la même page physique, mais ne partagent pas de pages virtuelles.

malloc

Comme nous l'avons trouvé dans la section Allocation de mémoire sous Linux, malloc alloue de la mémoire sur le tas, donc dans l'espace virtuel du processus.

L'allocation de mémoire virtuelle se fait au niveau de la page, de sorte que 'malloc' alloue réellement le plus petit nombre de pages virtuelles qui composent l'espace mémoire requis. Soit le code suivant:

char *p = malloc(4150);
DIE(p == NULL, "malloc failed");

Étant donné qu'une page virtuelle a 4KB = 4096 octets, alors l'appel malloc allouera 4096 octets + 54 octets = 4KB + 54 octets, espace qui n'est pas contenu dans une seule page virtuelle, de sorte que 2 pages seront allouées virtuel. Au moment de l'affectation avec “malloc” il n'y aura pas de pages allouées (tout le temps) et physiques; ils ne seront alloués que lors de l'accès aux données de la zone de mémoire allouée avec 'malloc'. Par exemple, lors de l'accès à un élément à partir de p, une erreur de page sera générée et la page virtuelle contenant cet élément sera mappée sur une page physique.

Généralement, sur le petit appel malloc (lorsque l'appel système brk est rappelé), la bibliothèque C standard analyse les pages allouées, génère des erreurs de page et lorsque l'appel revient, les pages physiques seront déjà allouées. On peut dire que pour les petites dimensions, l'appel 'malloc', vu depuis l'application (en dehors de la bibliothèque standard C), alloue également des pages physiques et des pages virtuelles.

De plus, l'allocation réelle des pages virtuelles et physiques se produit au moment de l'appel système brk . Il alloue un espace plus grand, et les futurs appels malloc utiliseront cet espace. De cette façon, les appels “malloc” suivants seront efficaces: ils ne feront pas d'appels système, ils ne feront pas d'allocation efficace d'espace virtuel ou physique, ils ne généreront pas de défauts de page .

L'appel malloc est plus efficace que l'appel calloc car il ne parcourt pas l'espace alloué pour le remplir de zéros. Cela signifie que malloc renverra la zone allouée avec les informations qui s'y trouvent; Dans certaines situations, cela peut constituer un risque pour la sécurité - si les données y sont privées.

Mappage de fichiers

Après avoir mappé un fichier à l'espace d'adressage du processus, l'accès à ce fichier peut se faire de la même manière que l'accès aux données à partir d'un vecteur. L'efficacité de la méthode vient du fait que la zone mémoire est gérée de la même manière que la mémoire virtuelle, soumise aux règles d'évacuation du disque lorsque la mémoire devient insuffisante (de cette façon on peut travailler avec des mappages qui dépassent la taille effective de la mémoire physique). De plus, la partie I / O est réalisée par le noyau, le programmeur écrivant du code qui ne récupère / stocke que les valeurs de / dans la région mappée. Ainsi, il n'est plus appelé read , write , lseek - ce qui simplifie souvent l'écriture de code.

Tous les descripteurs de fichiers ne peuvent pas être mappés en mémoire. Les sockets, les tuyaux, les périphériques qui n'autorisent que l'accès séquentiel (par exemple, le périphérique char) sont incompatibles avec les concepts de mappage. Il existe des cas où les fichiers normaux ne peuvent pas être mappés (par exemple, s'ils n'étaient pas ouverts pour plus de lisibilité; pour plus d'informations: man mmap ).

mmap

Prototype de fonction mmap qui permet le mappage d'un fichier à l'espace d'adressage du processus est le suivant:

void *mmap(void *start, size_t length, int prot, int flags, int fd, off_t offset);

La fonction retournera en cas d'erreur MAP_FAILED . Si le mappage a réussi, il renverra un pointeur vers une zone de mémoire dans l'espace d'adressage du processus, la zone où le fichier décrit par le descripteur fd a été mappé, en commençant par l'offset. L'utilisation du paramètre start vous permet de proposer une zone mémoire spécifique à mapper. L'utilisation de la valeur NULL pour le paramètre start indique l'absence de préférence concernant la zone dans laquelle l'allocation sera effectuée. L'adresse spécifiée par le paramètre start doit être multiple de la taille d'une page . Si le système d'exploitation ne peut pas mapper le fichier à l'adresse requise, il le mappera à un emplacement proche et multiple de la taille d'une page. L'adresse la plus appropriée est également retournée.

Le paramètre prot spécifie le type d'accès souhaité:

  • PROT_READ (lire)
  • PROT_WRITE (écrire)
  • PROT_EXEC (exécution)
  • PROT_NONE .

Lorsque la zone est utilisée autrement que ce qui est indiqué, un signal SIGSEGV est généré.

Le paramètre flags vous permet de définir le type de mappage que vous souhaitez; il peut prendre les valeurs suivantes (combinées par OR en bits; il doit y avoir au moins MAP_PRIVATE ou MAP_SHARED ):

  • MAP_PRIVATE - Utilisez une politique de copie sur écriture . La zone contiendra initialement une copie du fichier, mais les écritures ne sont pas effectuées dans le fichier. Les modifications ne seront pas visibles dans les autres processus , s'il y a plusieurs processus qui ont fait “mmap” sur la même zone du même fichier.
  • MAP_SHARED - Les entrées sont mises à jour immédiatement dans tous les mappages existants. De cette façon, tous les processus qui ont effectué des mappages verront les modifications. En effet, les mappages “MAP_SHARED” sont effectués sur les pages physiques du cache de pages et les appels r / w utilisent les pages physiques du cache de pages pour réduire le nombre de lectures / écritures à partir du disque. Les mises à jour du disque auront lieu ultérieurement, sans précision.
  • MAP_FIXED - Si l'affectation à l'adresse spécifiée par start ne peut pas être effectuée, l'appel échouera.
  • MAP_LOCKED - Cette page sera bloquée de cette manière mlock.
  • MAP_ANONYMOUS - Mappe la RAM (les arguments fd et offset sont ignorés).

  Il est à noter que l'utilisation de MAP_SHARED permet le partage de mémoire entre des processus qui ne sont pas liés. Dans ce cas, le contenu du fichier devient le contenu initial de la mémoire partagée et toutes les modifications apportées par les processus dans cette zone sont ensuite copiées dans le fichier, garantissant ainsi la persistance dans le système de fichiers.

msync

La fonctionnalité est disponible pour déclencher explicitement la synchronisation des fichiers avec le mappage de la mémoire msync:

int msync(void *start, size_t length, int flags);

drapeaux peut être:

  • MS_SYNC - Les données seront écrites dans le fichier et attendront jusqu'à ce qu'elles soient terminées.
  • MS_ASYNC - La séquence de sauvegarde est lancée, mais son achèvement n'est pas prévu.
  • MS_INVALIDATE - Le mappage de la zone à partir d'autres processus est invalidé, de sorte que les processus seront mis à jour avec les nouvelles données entrées.

L'appel msync est utile pour écrire des pages modifiées du cache de pages sur le disque, afin d'éviter de perdre des modifications en cas de plantage du système.

Allouer de la mémoire dans l'espace d'adressage du processus

Sous UNIX, traditionnellement, pour l'allocation de mémoire dynamique , l'appel système est utilisé brk. Cet appel augmente ou diminue la zone de segment de mémoire associée au processus. Avec la fourniture d'appels du système de gestion de la mémoire virtuelle aux applications (mmap), il était possible pour les processus d'allouer de la mémoire à l'aide de ces nouveaux appels système. Fondamentalement, les processus peuvent mapper la mémoire à l'espace d'adressage, pas aux fichiers.

Les processus peuvent nécessiter l'allocation d'une zone mémoire à partir d'une adresse spécifique dans l'espace adresse, même avec une certaine stratégie d'accès (lecture, écriture ou exécution). Sous UNIX, cela se fait également via la fonction mmap. Pour cela, le paramètre flags doit contenir le flag MAP_ANONYMOUS .

Appareils de cartographie

Il est même possible pour les applications de mapper un périphérique d'entrée-sortie dans l'espace d'adressage du processus. Ceci est utile, par exemple, pour les cartes vidéo: une application peut mapper la mémoire physique de la carte vidéo à l'espace d'adressage. Sous UNIX, les périphériques étant représentés par des fichiers, il suffit pour cela d'ouvrir le fichier associé au périphérique et de l'utiliser dans un appel mmap .

Tous les appareils ne peuvent pas être mappés en mémoire, mais lorsqu'ils sont mappés, l'importance de ce mappage dépend strictement de l'appareil.

Un autre exemple d'un périphérique qui peut être mappé est la mémoire elle-même. Sous Linux, vous pouvez utiliser le fichier / dev / zero pour effectuer le mappage de la mémoire, comme si vous utilisiez l'indicateur MAP_ANONYMOUS .

Suppression d'une zone de l'espace d'adressage

Si vous souhaitez effacer une zone de l'espace d'adressage du processus, vous pouvez utiliser la fonction munmap:

 int munmap(void *start, size_t length);

start est l'adresse de la première page à délimiter (doit être multiple de la taille d'une page ). Si la longueur n'est pas une taille qui représente un nombre entier de pages, elle sera arrondie plus haut. La zone peut contenir des pièces qui ont déjà été supprimées. Vous pouvez ainsi effacer plusieurs zones en même temps.

Redimensionner une zone mappée

La fonction peut être utilisée pour effectuer des opérations de redimensionnement de la zone de mappage mremap:

void *mremap(void *old_address, size_t old_size, size_t new_size, unsigned long flags);

La zone décrite par old_address et old_size doit appartenir à un seul mappage. Une seule option est disponible pour les indicateurs : MREMAP_MAYMOVE qui montre qu'il est correct d'obtenir le nouveau mappage pour effectuer un nouveau mappage dans une autre zone de mémoire (l'ancienne zone étant floue).

Modifier la protection d'une zone cartographiée

Parfois, il est nécessaire de changer la façon (droits d'accès) dans laquelle une zone a été cartographiée. Pour cela, vous pouvez utiliser la fonction mprotect:

int mprotect(const void *addr, size_t len, int prot);

La fonction reçoit comme paramètres la plage d'adresses [ addr , addr + len - 1] et de nouveaux droits d'accès ( PROT_READ , PROT_WRITE , PROT_EXEC ,PROT_NONE). Comme dans munmap, addr doit être multiple de la taille de la page . La fonction modifiera la protection de toutes les pages contenant au moins un octet dans la plage spécifiée.

Exemple

int fd = open("fisier", O_RDWR);
void *p = mmap(NULL, 2*getpagesize(), PROT_NONE, MAP_SHARED, fd, 0);
// *(char*)p = 'a'; // segv fault
mprotect(p, 2*getpagesize(), PROT_WRITE);
*(char*)p = 'a';
munmap(p, 2*getpagesize());

L'appel getpagesize renverra la taille d'une page en octets.

Exercices

Exercice 1 - Zones de stockage variables

Entrez dans le répertoire 1 compteur et analysez la fonction inc , qui renvoie un nombre entier représentant le nombre d'appels jusqu'à présent. Compilez le programme en exécutant la commande make et inspectez où la mémoire de la variable cnt a été allouée.

Utilisez les commandes suivantes:

make
objdump -t counter | grep cnt

Exercice 2 - L'espace d'adressage d'un processus

Entrez dans le répertoire 2-adr_space et ouvrez la source adr_space.c . Dans un autre terminal, compilez et exécutez le programme. Notez les zones de mémoire dans l'exécutable où les variables sont enregistrées, à l'aide de la commande:

objdump -t adr_space | grep var

Notez que certaines variables apparaissent dans la table des symboles (variables locales globales et statiques - comme indiqué par les drapeaux g (global) et l (local) à côté d'elles; man objdump pour plus d'informations) et d'autres non. Les variables qui n'apparaissent pas dans le tableau sont sur la pile.

Afficher le contenu de la zone .data à l'aide de l'utilitaire readelf

Astuce : Vous devez afficher le vidage hexadécimal de la section .data de l'exécutable adr_space . Voir la page de manuel 'readelf' pour le paramètre correct.

N'oubliez pas d'ajouter le nom du fichier exécutable en tant que paramètre de la commande 'readelf'.

Exercice 3 - Allocation et utilisation de la mémoire

Entrez dans le répertoire 3-alloc et ouvrez la source alloc.c . Dans le même temps, décompressez l'une des lignes commençant par TODO x .

Une seule des 3 lignes doit être mise hors service avant de compiler le programme.

Dans un autre terminal, compilez le programme. Notez les zones de mémoire de l'exécutable dans lesquelles les variables et leur taille sont enregistrées, puis affichez la taille de l'exécutable à l'aide des commandes:

make
objdump -t alloc | grep var
ls -lh alloc

Exercice 4 - Enquêter sur les mappages à l'aide de pmap

Entrez dans le répertoire 4-intro et compilez la source intro.c . Exécutez le programme intro :

./intro

Dans une autre console, utilisez la commande pmap.:

 watch -d pmap $(pidof intro)

pour suivre les modifications apportées à la mémoire de processus.

Dans la première console, utilisez ENTER pour continuer le programme. Dans la deuxième console, suivez les modifications qui se produisent après les différents types de mappage dans le code.

Analysez les mappages effectués par le processus init à l'aide de la commande:

sudo pmap 1

Vous pouvez voir que pour les bibliothèques partagées (par exemple, libc ), trois zones sont mappées: la zone de code (lecture-exécution), la zone .rodata (lecture seule) et la zone .data (lecture-écriture).

Exercice 5 - Écrire dans un fichier - écrire ou mmap

Entrez dans le répertoire 6-compare et inspectez les sources write.c et mmap.c , puis compilez. Obtenez le runtime des deux programmes en utilisant la commande 'time':

time ./write; time ./mmap

Notez que la variante “mmap” est plus rapide que la ”” variante d'écriture. Nous utiliserons strace pour voir quels appels système sont effectués pour exécuter chaque programme:

strace -c ./write
strace -c ./mmap

A partir de la sortie ”” strace ””, nous remarquons que le programme ”” write ”” fait de nombreux (100000) appels ”” write ”” et à cause de cela est plus lent que le programme “mmap”.

Ensuite, nous examinerons les deux modes de mappage de fichiers: MAP_SHARED et MAP_PRIVATE . Notez que le fichier test_mmap (créé par le programme mmap avec MAP_SHARED ) contient 100000 lignes:

cat test_mmap | wc -l

Dans le programme mmap.c , changez l'indicateur de création de mémoire partagée de MAP_SHARED en MAP_PRIVATE , compilez et réexécutez:

./mmap
cat test_mmap | wc -l

Les modifications apportées à une zone de mémoire mappée avec MAP_PRIVATE ne seront pas visibles par les autres processus et n'atteindront pas le fichier mappé sur le disque.

Exercice 6 - Défauts de page

Entrez dans le répertoire '7-faults' et parcourez le contenu du fichier fork-faults.c.

Nous utiliserons l'utilitaire pidstat ( pidstat) à partir du package sysstat pour surveiller les défauts de page causés par un essai.

Si vous rencontrez des problèmes lors de l'installation du package sysstat , téléchargez-le depuis ici et installez-le en utilisant la commande dpkg.

student@spook:~$ wget http://ro.archive.ubuntu.com/ubuntu/pool/main/s/sysstat/sysstat_11.2.0-1_i386.deb
student@spook:~$ sudo dpkg -i sysstat_11.2.0-1_i386.deb 

Exécutez le programme 'fork-faults'. Dans une autre console, exécutez la commande

pidstat -r -T ALL -p $(pidof fork-faults) 5

pour suivre les défauts de page. La commande ci-dessus vous montre un message toutes les 5 secondes; nous nous intéressons aux valeurs minflt-nr .

À son tour, appuyez sur la touche ENTRÉE de la console sur laquelle vous avez exécuté le programme fork et voyez la sortie de la commande pidstat. Suivez l'évolution du nombre de défauts de page pour les deux essais: parent et enfant. Les défauts de page qui se produisent dans le cas d'une copie sur écriture dans le processus enfant seront visibles plus tard dans le processus parent (une fois que le processus enfant aura terminé son exécution).

Le package sysstat contient également l'utilitaire jump où vous pouvez collecter et faire des rapports sur l'activité du système. Pour activer l'enregistrement des données, l'indicateur ENABLED dans / etc / default / sysstat doit être défini. Avec l'utilitaire “jump”, vous pouvez surveiller des informations telles que le chargement du processeur, l'utilisation de la mémoire et des pages, les opérations d'E / S, l'activité du processus. Détails que vous pouvez trouver auprès de sar.

sde/laboratoare/07_python.txt · Last modified: 2020/04/01 13:44 by iuliana.marin
CC Attribution-Share Alike 3.0 Unported
www.chimeric.de Valid CSS Driven by DokuWiki do yourself a favour and use a real browser - get firefox!! Recent changes RSS feed Valid XHTML 1.0