Mémoire Grégory Mounié Édition 2020-2021
←
→
Transcription du contenu de la page
Si votre navigateur ne rend pas la page correctement, lisez s'il vous plaît le contenu de la page ci-dessous
Mémoire Grégory Mounié Édition 2020-2021
Outline Introduction Observation de la mémoire Allocation par zones, coté kernel Allocateurs en mémoire virtuelle et outils coté utilisateur Garbage collection en C Garbage collection en Java Garbage Collection en Go Allocation Mémoire en Rust Partage du processeur et de la mémoire Memoire virtuelle 1
Introduction
Contexte Système multi-processus, utilisant et se partageant une mémoire bornée, en partie occupée par l’OS. Objectif • permettre aux processus de s’exécuter • utilisation efficace de la mémoire de l’UC (et de la RAM) • bonnes performances (temps de réponse) pour les utilisateurs Conséquences de l’objectif lié au matériel Depuis l’ENIAC, la seule entité capable de servir les instructions et les données assez vite au processeur est la RAM. Il va falloir l’utiliser efficacement. 2
Quelques contraintes • La somme des tailles des processus est supérieure à la taille de la mémoire physique • la taille de la partie utile d’un processus à un instant donné (le working set) est plus petite que la mémoire disponible • avant exécution, le code d’un processus est stocké dans un fichier sur le disque. Il va falloir le copier dans la RAM. 3
Observation de la mémoire
Outils d’observations Analyse coté OS, globale, à l’extérieur des processus free memoire disponible ps, top, htop capable d’afficher info sur la mémoire smem mémoire de chaque processus, incluant la proportion de mémoire partagée (PSS) proc[pid]/map proc[pid]/pagemap mapping mémoire Analyse de l’utilisaton mémoire à l’intérieur d’un processus • profiler/débogueur: valgrind (cachegrind (cache), massif (heap usage), exp-sgcheck (overrun of stack and global array), exp-dhat (heap block usage)) • discussion avec le Gc 4
Allocation par zones, coté kernel
Les allocateurs mémoire classiques Mémoire virtuelle (Plus tard dans ce cours) Utilisation pour allouer de la mémoire à un processus (gestion du tas): common, malloc, gc, pile Mémoire physique Utilisation pour charger des processus et les exécuter. Usage interne pour le noyau: structure de données (table des processus); tampons pour les IO et les communications. Fonctionnement par découpage en zones • zones de tailles quelconques • zones de taille fixe • zones de tailles prédéfinies 5
Allocateur physique: représentation de l’espace libre • Utilisation d’une liste chaînée des zones libres • dans chaque zone: • taille de la zone • adresse de la zone libre suivante 6
Allocateur physique: Libération d’une zone • Fusion des zones libres contiguës pour éviter la fragmentation • recherche des zones libres voisines • plus efficace si la liste libre est classée pas adresses • Conséquence sur l’allocation : • first fit • deux listes différentes 7
Allocateur physique: le problème de la fragmentation externe Sa cause: les allocations succéssives créent des zones libres trop petites pour être utilisables par une grosse allocation. Solutions • Périodiquement réorganiser l’ensemble de la mémoire • nécéssite que les processus modifiés soient suspendus, surtout en cas d’écriture mémoire pendant la migration • réserver (UEFI, kernel) des pans entier de mémoire à un usage précis (GPU embarqué utilisant la RAM) 8
Cas des zones de taille prédéfinie • liste libre par taille • allocation et libération simple 9
Problèmes • le choix des tailles • que faire quand on n’a plus de blocs d’une taille donnée mais des blocs de taille supérieure • Solution: technique de subdivision 10
Allocateur physique: la méthode du compagnon (buddy) • buddy system: fractionnement à l’allocation, fusion à la libération • uniquement des puissances de 2 [file: ///usr/src/linux-source-4.20/mm/page_alloc.c] 11
libération par ramasse-miettes • allocations explicites • pas de libérations : la mémoire est grande, on verra bien • en cas d’épuisement de la mémoire libre: • détermination des zones libres et occupées • création de la liste des zones libre 12
Autre allocateur physique: SLAB/SLUB (SLOB/SLQB) Allouer les objets de taille fixe SLAB une file par type d’objet/cpu/processeurs où l’on garde les objets libres à leur libération. Zone contigüe en mémoire. Pas de besoin de lock sur les structures. Une freelist par frame qui est un champ de bit. Lorsqu’un SLAB est plein, il suffit d’en allouer un autre. SLUB: idem mais sans multiples files SLQB: file + partial 13
Allocateurs en mémoire virtuelle et outils coté utilisateur
Allocateurs de la glibc malloc [tiré de malloc/malloc.c de la glibc] The main properties of the algorithms are: • For large (>= 512 bytes) requests, it is a pure best-fit allocator, with ties normally decided via FIFO (i.e. least recently used). • For small (= 128KB by default), it relies on system memory mapping facilities, if supported. Autrefois, utilisait les appels systèmes brk ou sbrk Pour agrandir le program break et allouer de la mémoire au tas 14
Garbage collection en C
utilisation de gc 1 #include 2 #include 3 // #define GC_DEBUG // GC_MALLOC et pas GC_malloc 4 #include 5 int main() { 6 GC_INIT(); /* Optional on Linux/X86 */ 7 for (int i = 0; i < 10000000; ++i) { 8 int **p = (int **) GC_MALLOC(sizeof(int *)); 9 int *q = (int *) GC_MALLOC_ATOMIC(sizeof(int)); 10 assert(*p == 0); 11 *p = (int *) GC_REALLOC(q, 2 * sizeof(int)); 12 if (i % 100000 == 0) 13 printf("Heap size = %ld\n", ,→ GC_get_heap_size()); 15
Difficulté d’implantation d’un GC en C • Où sont les roots (les variables pointeurs) ? • Une allocation n’est pas toujours pointer uniquement à son début (exemple: votre allocateur mémoire) Un ramasse-miette conservateur • Ce qui ressemble à une adresse de pointeur est traité comme un pointeur. • ⇒ Risque de ne pas libérer certains objets qu’il faudrait libérer. • Les objects ne sont jamais déplacés. 16
API de libgc 1 #include 2 struct elem { struct elem *next; }; 3 int main() { 4 // appel de base 5 struct elem *a = GC_MALLOC(sizeof(struct elem)); 6 // pas de pointeurs internes 7 int *b = GC_MALLOC_ATOMIC(100* sizeof(int)); 8 // ne jamais libérer 9 int *c = GC_UNCOLLECTABLE(10); 10 // enregistrer un destructeur 11 // GC_REGISTER_FINALIZER(...); 12 // lent pour les petits objects 13 GC_FREE(b); 14 } 17
Algorithme de libgc: Mark-and-sweep De temps en temps (suivant la pression mémoire): 1. marquer tous les objets directement référencer par les roots (variables pointeurs) 2. Marquer, en boucle, tout objet qui est référencé par un objet qui vient d’être référencé. 3. identifier les objects non marqués et les mettre dans la liste des objects libres à réutiliser pour d’autres allocations. Les objets ne sont jamais déplacés ! 18
Garbage collection en Java
Bases du GC • Les objet sont stockés dans le Tas (heap) qui est géré par la JVM • Toutes les références racines (root) sont dans la pile de chaque thread (stack) (ou dans les variables thread-local, les variables static ou JNI) • Si on déplace un objet, il faut mettre à jour toutes les références sur cet objet. On peut introduire une table de référence intermédiaire, ce qui coûte plus cher pour chaque accès mais permet de ne mettre à jour que la table des objets. • S’ils ne sont pas référencés, ils sont elligibles pour l’effacement 19
Le Tas selon Java les objets ont souvent une durée de vie très courte D’où l’idée de les regrouper par génération: la plupart des objets n’allant jamais jusqu’à la deuxième génération. Le tas est fractionné en plusieurs zones 1. Young generation: 1.1 Eden space: le début de chaque instance 1.2 S0 Survivor Space: les instances anciennes vont de eden à S0 1.3 S1 Survivor Space: les instances encore plus anciennes 2. Old generation 2.1 tenured: instance venant de S1 3. Permanent 20
Les cycles du GC de Java Il y a deux types de cycles de GC: minor GC qui scanne Eden, SO S1: • les objets encore référencés passent de Eden à S0, de S0 à S1, de S1 à tenure (après un seuil, expliqué après) • les objets non référencés sont marqués pour éviction, tout de suite ou plus tard suivant le GC utilisé major GC • scanne Tenured La fragmentation due à la libération peut-être compactée à la volée ou plus tard 21
Lancement des cycles: quand eden est plein, on lance un minor GC sur Young Generation • eden est vidé (référencés passent dans S0, les autres sont effacés) • S0 est vidé (référencés passent dans S1): eden et S0 sont donc vides à chaque minor GC. • les zones utilisées par S0 et S1 sont inversés • Au bout d’un certain seuil d’aller-retour (8 ?) les objets de S1 deviennent Tenured.: de temps en temps il y a un major GC sur la zone Tenured de la Old Generation 22
Les références • Les instances non atteignables par un thread, les cycles de références non atteignables • Il y a quatre types de références: • Strong (pas de garbage collection) • Soft (possible mais en dernière option) • Weak et Phantom (Elligible pour le GC) 23
Les différents GC de Java En fait, Java a quatre type de GC 1. serial: bloque tous les threads et 1 thread fait le GC, mark-compact en début de zone 2. parallel (defaut): bloque tous les threads applicatifs avant que plusieurs threads GC sur Young, 1 seul thread sur Old Generation, il compacte 3. CMS: comme parallel, mais nettoye les instances marquées: coûte plus cher en CPU. Il ne compacte pas par défaut, juste en Stop The World situation 4. G1: pour les gros tas, sépare le tas en region et les traite en parallèles. Compacte aussi la mémoire, mais en most garbage first (compactage incrémental) 24
Débogage de performance Comme les GC en Java, c’est complexe mais crucial pour les performances, il existe des outils de monitoring et de mesure de perf des applications (visualvm) 25
Garbage Collection en Go
GC de Dijkstra, Lamport Plutôt que d’avoir un arrêt complet de l’environnement lors du GC, il est possible de le faire en parallèle à l’exécution. Le temps consacré à l’un (le mutator qui calcule) ou à l’autre (le collector) peut être réglé en fonction de la pression mémoire (C’est le cas dans Go). 26
Description de l’algorithme de Dijkstra Lamport • une liste de roots • un nœud spécial pour les nœuds libres, qui sert à stocker les nœuds libres • tous les noeuds sont marqués en blancs • Le collector va marquer en noir tous les noeuds accessibles depuis les roots • Le mutator va shadé en gris (blanc vers gris, gris et noir inchangé), les nouveaux noeuds alloués • deux étapes dans le collector. 27
GC Dijkstra: étape 1/2 Marqué tous les noeuds en les parcourant dans l’ordre !: • shadé les racines (donc elles deviennent grises au pire) • puis parcourir chaque noeuds (modulo) jusqu’au nombre de noeuds total: • atomic(récupérer sa couleur) • s’il est gris • atomic(shadé ses successeurs et le rendre noir) • reprendre la suite pour le nombre de noeud total • sinon, passer au suivant modulo M, et un noeud de moins à traiter 28
GC Dijkstra: étape 2/2 récupération de noeuds dans la liste Pour chaque noeud, dans l’ordre de la numérotation • atomic(récupérer sa couleur) • si blanc, atomic(le mettre dans la liste libre) • si noir, atomic(le rendre blanc) il faut aussi faire attention lors de l’allocation, coté mutator: • Dijktra montre comment faire en enchainant des petites opérations atomiques, plutôt qu’une seule grosse. 29
GC de Go • Avant Go 1.5 parallel stop-the-world (STW) GC, après en parallèle, chaque fois que le tas double de taille. • Depuis Go 1.5 utilise le mark-and-sweep de Dijkstra • 2 bits par mots: scalar vs pointer; et s’il y a des pointeurs dans l’objet; + 1 bit de debug • execution (allocation) et GC en parallèle 30
Allocation Mémoire en Rust
Rust Idée de base: faire un langage pour remplacer C: donc pas de GC ! • Notation très fonctionnel • Il faut tout expliquer au compilateur ! • match (switch case à la OCaml avec garde) sur tous les cas ! • retour des fonctions avec Option, Result) • Il faut tout tester (valeurs de retours) • Notion de borrow checker 31
Borrow checker L’idée est qu’il y a un seul propriétaire pour chaque variable. On peut emprunter quelque chose, mais alors le propriétaire ne peut pas le changer ! Le compilo limite au maximum les emprunts. 1 let mut a = 12; 2 let b = &a; // borrow of `a` occurs here 3 println!("a= {:?}", a); 4 println!("b= {:?}", b); // auto deref de b 5 a= 11; // assignment to borrowed `a` 6 println!("a= {:?}", a); 7 println!("b= {:?}", b); // borrow later used here 32
Usage omniprésent des itérateurs Pas d’évaluation des itérateurs tant que ce n’est pas explicitement demandé. 1 let u: Vec = (1..=10).map(|x| x * x) 2 .take(5) 3 .collect(); // [1, 4, 9, 16, 25] 4 println!("{:?}", u); 33
Rust allocation dans le tas Il est possible d’allouer dans le tas et d’avoir plusieurs propriétaires, à condition de bien les compter ! 1 // alloue un entier dans le Tas, free avec ,→ destruction de la Box 2 let five = Box::::new(5); 3 // 3 allocations d'un vecteur de taille variable ,→ dans le tas 4 let v: Vec = Vec::new(); 5 let v2 = vec![42; 3]; // macro + 42 42 42 6 let v3 = vec![1, 2, 3]; 7 v3.push(12).pop(); 8 // plusieurs proprios possibles car comptés ! ,→ monothread 9 let partagefive = Rc::new(5); 34
Partage du processeur et de la mémoire
Éxécution d’un programme: implication sur la mémoire Le code est les données utilisées à un instant donné par le CPU sont lues en RAM Conséquence Pour utiliser efficacement le processeur, il faut donc que l’ensemble des code et données utiles soient dans la mémoire au moment où le processeur en a besoin Histoire du partage de la machine Les exemples simplistes suivants ont existé. Ils sont là pour montrer pourquoi ils étaient insuffisants et illuster pourquoi nous utilisons maintenant des mécanismes de paginations. 35
Schéma simpliste: la monoprogrammation • un seul processus en mémoire à un instant donné • chargement à la création du processus • mémoire libérée à la fin du processus Inconvénients • UC oisives pendant les chargements et les entrées-sorties • mémoire mal utilisée 36
Utilisation des temps morts dus aux ES lentes Pour utiliser les temps morts, il faut avoir quelque chose à faire Il faut plusieurs processus prêts à calculer ⇒ plusieurs processus en mémoire ⇒ il faut donc de la place Que faire des processus bloqués ? • les stocker temporairement sur disque pour les recharger ultérieurement • On réquisitionne la mémoire centrale 37
La mono-programmation avec va-et-vient (swapping) un processus en mémoire à un instant donné: le processus élu • chargement au passage d’éligible à élu • vidage à la sortie de l’état élu Inconvénients • UC oisive pendant les chargements et vidage • mémoire mal utilisé 38
Idée de la multiprogrammation • la mémoire est partagée en zone • chaque zone contient un processus • l’UC est alloué à un processus chargé et éligible 39
Multiprogrammation sans réquisition • Un processus est chargé une seule fois • reste en mémoire jusqu’à sa terminaison • l’UC peut être active pendant le chargement d’un nouveau processus 40
Multiprogrammation avec réquisition Un processus dont l’exécution a commencé peut être vidé sur disque • entrée sortie longue • exécution d’un processus prioritaire Peut-on recharger un processus vidé dans n’importe quelle zone ? En général il est attaché à une zone à cause des adresses absolues. Il est possible d’avoir des adresses relatives (segmentation), mais on va faire mieux (pagination). Il faut assurer l’isolation de ces zones ? 41
Memoire virtuelle
Mémoire virtuelle Objectif Rendre un processus indépendant de la zone physique où il s’exécute • Conventions de programmation • mécanisme câblé de traduction dynamique d’adresses • unité de gestion de la mémoire (MMU) 42
Doit-on charger en mémoire la totalité d’un processus ? • En général c’est inutile: tests d’erreur n’arrivant pas, fonctions non utilisées, etc. • comment déterminer les portions utiles à un instant donné ? • comment charger dynamiquement les portions qui deviennent indispensables ? Les mémoires virtuelles modernes elles permettent de changer ou de vider des sous-ensembles de programmes: les pages 43
Position des pages: Implantation statique • Un processus est chargé une fois pour toutes • il réside en mémoire jusqu’à sa terminaison • choix: • mono-multi programmation • comment définir et gérées les différentes zone ? 44
Position des pages: réimplantation dynamique • Où charger ? Existe-t-il des emplacements disponibles ou faut-il libérer la place ? • Comment charger ? en totalité ou en morceau (par page) • Quand charger ? • statiquement avant exécution • dynamiquement au fur et à mesure des besoins La pagination sera l’objet du prochain cours 45
Vous pouvez aussi lire