Mémoire Grégory Mounié Édition 2020-2021

La page est créée Adrien Marin
 
CONTINUER À LIRE
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