Gestion de la mémoireDans le dialogue d’impression, choisissez « Enregistrer au format PDF » comme destination.
Retour

Systèmes d'exploitation · C4 Mémoire · Chapitre 1 · 10 h

Gestion de la mémoire

Allocation contiguë et fragmentation, pagination et table des pages, segmentation ; mémoire virtuelle et pagination à la demande, défauts de page, FIFO, LRU, optimal et anomalie de Belady.

Un programme compilé contient des adresses. Si ces adresses étaient celles de la mémoire physique, deux programmes ne pourraient pas coexister sans se piétiner, il faudrait recompiler un programme pour le charger ailleurs, et rien n'empêcherait l'un de lire les données de l'autre.

Le système résout les trois problèmes d'un coup en interposant une traduction : le programme manipule des adresses logiques, le matériel les convertit en adresses physiques. C'est le sujet de ce chapitre, et c'est aussi son point difficile — la conversion d'une adresse logique en adresse physique se travaille par des exercices systématiques, pas par la lecture d'un schéma. Prévoyez d'en faire beaucoup.

Allocation contiguë et fragmentation

La première idée est de donner à chaque processus un bloc de mémoire d'un seul tenant. Le matériel n'a alors besoin que de deux registres, chargés au changement de contexte : un registre de base, ajouté à chaque adresse logique, et un registre de limite, qui provoque une exception si l'adresse la dépasse. Relocation et protection en deux additions.

Le problème arrive avec les départs. Les processus se terminent en laissant des trous ; les trous ne sont pas au bon endroit. On peut avoir 500 Mio libres au total et être incapable de loger un processus de 100 Mio parce que le plus grand trou en fait 80. C'est la fragmentation externe.

Trois stratégies de placement se disputent le choix du trou, et leur classement est contre-intuitif. Le premier ajustement prend le premier trou assez grand : rapide, et en pratique le meilleur. Le meilleur ajustement prend le plus petit trou suffisant : plus lent, et il produit une poussière de trous minuscules inutilisables. Le pire ajustement prend le plus grand : il laisse des restes exploitables mais détruit vite les grands trous.

On peut compacter — déplacer les processus pour réunir les trous — mais c'est coûteux et impossible si le programme contient des adresses physiques déjà calculées. La vraie solution est ailleurs : cesser d'exiger que la mémoire d'un processus soit d'un seul tenant.

La pagination

On découpe la mémoire logique en pages et la mémoire physique en cadres, tous de la même taille — typiquement 4 Kio. Une page quelconque peut aller dans un cadre quelconque, et une table des pages, propre à chaque processus, note où chacune se trouve.

La fragmentation externe disparaît : tout trou est un cadre, donc utilisable. Il reste une fragmentation interne, la partie inutilisée de la dernière page — en moyenne une demi-page par processus, soit 2 Kio, un prix dérisoire.

Le découpage de l'adresse

C'est ici qu'il faut ralentir. Une adresse logique n'est pas un couple à calculer : c'est un simple nombre, que le matériel lit en deux morceaux.

Adresse logique sur 32 bits, pages de 4 Kio (2¹² octets)  31                                   12 11                    0┌───────────────────────────────────────┬───────────────────────┐│        numéro de page   (20 bits)     │  déplacement (12 bits)│└───────────────────────────────────────┴───────────────────────┘

La taille de page fixe le découpage, et rien d'autre : 4Kio=2124\,\text{Kio} = 2^{12}, donc 12 bits de déplacement, et les 20 bits restants numérotent 2202^{20} pages.

La traduction se fait en trois gestes :

1. numéro de page  = adresse ÷ taille_page        (les bits de poids fort)2. déplacement     = adresse mod taille_page      (les bits de poids faible)3. adresse physique = table[numéro] × taille_page + déplacement

Le point que tout le monde manque au premier essai : le déplacement n'est jamais traduit. Il traverse la conversion inchangé, parce qu'une page et un cadre ont la même taille — le kk-ième octet d'une page est le kk-ième octet de son cadre. Seul le numéro change.

Déroulons un exemple complet, avec des pages de 4 Kio et une table qui dit que la page 2 est dans le cadre 7 :

adresse logique   90009000 ÷ 4096 = 2   reste 808                  ↳ page 2, déplacement 808 table[2] = cadre 7 adresse physique  7 × 4096 + 808 = 29 480

Un contrôle qui évite bien des erreurs : le déplacement doit toujours être strictement inférieur à la taille de page. S'il ne l'est pas, la division a été faite avec la mauvaise taille.

Le coût, et la TLB

La table des pages est en mémoire. Chaque accès du programme en demande donc deux : un pour lire la table, un pour l'octet voulu. La mémoire est deux fois plus lente, ce qui est inacceptable.

D'où le tampon de traduction (TLB), petit cache totalement associatif — au sens du chapitre 7 d'architecture — qui garde les traductions récentes. Grâce à la localité, quelques dizaines d'entrées suffisent à obtenir plus de 98 % de succès : la quasi-totalité des accès se traduisent sans toucher la table.

Le TLB est vidé à chaque changement de contexte, puisque les traductions appartiennent au processus sortant. C'est un coût caché du chapitre 4, qui s'ajoute à la sauvegarde des registres — et l'une des raisons pour lesquelles un quantum trop court est ruineux.

Enfin, la table elle-même est un problème : 2202^{20} entrées par processus, à 4 octets, font 4 Mio de table pour un programme qui n'utilise peut-être que quelques pages. On la découpe donc en plusieurs niveaux, ce qui permet de ne matérialiser que les branches réellement employées.

Quiz · 1 question

Pages de 1 Kio, la table indique que la page 3 est dans le cadre 5. Quelle est l'adresse physique correspondant à l'adresse logique 3500 ?

  • 5548 : on remplace le numéro de page 3 par le cadre 5, soit 3500 + 2 × 1024par différence
  • 5548 : 3500 = page 3, déplacement 428 ; l'adresse physique vaut 5 × 1024 + 428par décomposition
  • 3500 : l'adresse ne change pas, seule la protection diffèreinchangée

Réponse : La méthode : 3500 ÷ 1024 = 3 reste 428, donc page 3 et déplacement 428. La table donne le cadre 5, et l'adresse physique vaut 5 × 1024 + 428 = 5548. Le déplacement 428 n'est PAS traduit — il traverse inchangé, parce que page et cadre ont la même taille, donc le 428ᵉ octet de la page est le 428ᵉ octet du cadre. Le contrôle à faire systématiquement : le déplacement doit être strictement inférieur à la taille de page, ici 428 < 1024. La première réponse donne le bon nombre par un raccourci (ajouter la différence de rang × taille) qui ne fonctionne que par coïncidence et s'effondre dès que les tailles diffèrent : décomposez toujours.

Segmentation

La pagination découpe selon une taille arbitraire, qui ne correspond à rien dans le programme. La segmentation découpe selon la structure logique : un segment pour le code, un pour les données, un pour la pile, un par bibliothèque partagée. Une adresse y est un couple (numéro de segment, déplacement), et chaque segment porte sa propre longueur et ses propres droits.

L'avantage est la protection fine et le partage naturel : marquer le segment de code en lecture seule, ou le rendre visible à plusieurs processus, devient trivial. Le défaut est que les segments sont de tailles différentes — donc la fragmentation externe revient.

D'où la combinaison retenue par les architectures réelles : segmenter puis paginer. Le programme voit des segments, chacun est découpé en pages, et les pages vont dans des cadres quelconques. On garde la protection du premier et l'absence de fragmentation du second.

Mémoire virtuelle

Dernier étage, et le plus rentable. Rien n'oblige à ce que toutes les pages d'un processus soient en mémoire. Chaque entrée de la table porte un bit de présence ; les pages absentes sont sur le disque.

Toucher une page absente déclenche un défaut de page, qui est une exception matérielle, pas une erreur du programme. Le système la traite : il choisit un cadre libre — ou en libère un —, lit la page depuis le disque, met à jour la table, et réexécute l'instruction fautive, qui réussit cette fois. Le processus n'a rien vu, sinon qu'il a été bloqué au sens du chapitre 3 pendant plusieurs millisecondes.

Trois bénéfices, considérables. Un programme peut être plus grand que la mémoire physique. Le chargement à la demande ne lit que ce qui sert réellement, donc le démarrage est immédiat. Et plusieurs processus partagent les cadres du même code en lecture seule — les trois éditeurs du chapitre 3 ne coûtent qu'une copie.

Le coût est brutal, et c'est le chiffre à retenir : un accès mémoire prend 60 ns, un défaut de page 10 ms sur un disque magnétique, soit cent mille fois plus. Un taux de défaut de un pour mille suffit à doubler le temps moyen d'accès. C'est pourquoi la mémoire virtuelle n'est viable que si les défauts sont rarissimes — ce que la localité du chapitre 7 d'architecture rend possible.

Quand elle ne l'est plus, le système s'écroule (thrashing). Trop de processus, pas assez de cadres : chacun passe son temps à provoquer des défauts, le disque sature, l'utilisation du processeur s'effondre. Le piège est que l'ordonnanceur, voyant le processeur inoccupé, admet davantage de processus — et aggrave exactement le problème. La parade est la notion d'ensemble de travail : l'ensemble des pages qu'un processus utilise réellement dans une fenêtre de temps récente. Tant que la somme des ensembles de travail tient en mémoire, tout va bien ; dès qu'elle déborde, il faut suspendre un processus entier plutôt que d'en admettre un de plus.

Choisir la victime

Reste la question la plus concrète : quelle page évincer ? La même qu'au chapitre 7 d'architecture pour le cache, mais avec un enjeu cent mille fois supérieur.

FIFO évince la plus anciennement chargée. Simple, et mauvais : l'ancienneté du chargement ne dit rien de l'utilité, et une page chargée tôt mais utilisée en permanence sera sacrifiée.

Optimal évince celle dont la prochaine utilisation est la plus lointaine. Il est démontrablement le meilleur, et irréalisable — il faudrait connaître l'avenir. Il sert d'étalon : un algorithme réel se juge à son écart avec lui.

LRU évince la moins récemment utilisée. C'est le pari sur la localité temporelle, et il approche l'optimal de près. Sa version exacte coûte cher — il faudrait horodater chaque accès —, donc on l'approche : le matériel maintient un simple bit de référence par page, mis à 1 à chaque accès, et l'algorithme dit de la seconde chance parcourt les pages en cercle, remet à 0 les bits qu'il trouve à 1, et évince la première page dont le bit était déjà à 0.

Enfin, une bizarrerie de FIFO qui achève de le disqualifier : l'anomalie de Belady. Sur certaines suites de références, augmenter le nombre de cadres augmente le nombre de défauts. La suite 1 2 3 4 1 2 5 1 2 3 4 5 produit 9 défauts sur trois cadres et 10 sur quatre. LRU et l'optimal n'ont pas ce défaut.

Animation · 12 étapes

Remplacement FIFO sur 3 cadres — suite 1 2 3 4 1 2 5 1 2 3 4 5

  1. Page 1 — défautLes trois cadres sont vides : le premier accès est nécessairement un défaut. On charge la page 1 dans le cadre 0, et le pointeur de victime avance.
  2. Page 2 — défautDeuxième défaut obligatoire. Ces défauts-là sont dits « obligatoires » : aucun algorithme, même clairvoyant, ne peut les éviter.
  3. Page 3 — défautLa mémoire est pleine. À partir d'ici, tout défaut coûte une éviction, et c'est là que la politique de remplacement commence à compter.
  4. Page 4 — défaut, la page 1 est évincéeFIFO sacrifie la plus ANCIENNEMENT CHARGÉE, sans regarder si elle vient d'être utilisée. Ici c'est la page 1, chargée en premier — et elle sera redemandée à l'accès suivant.
  5. Page 1 — défaut, la page 2 est évincéeLe défaut que LRU aurait évité : la page 1 venait d'être utilisée trois accès plus tôt. FIFO n'en sait rien, il ne mémorise que l'ordre d'arrivée.
  6. Page 2 — défaut, la page 3 est évincéeMême scénario. Les trois pages du début tournent sans jamais rester assez longtemps pour servir.
  7. Page 5 — défaut, la page 4 est évincéeSeptième défaut d'affilée. La suite de référence a été construite pour cela : elle demande systématiquement la page qu'on vient de jeter.
  8. Page 1 — succèsEnfin un succès : la page 1 est encore là. Le pointeur de victime ne bouge PAS — un succès ne charge rien, donc ne modifie pas l'ordre d'arrivée.
  9. Page 2 — succèsSecond succès consécutif. Ces deux accès ne coûtent rien : ni éviction, ni lecture disque, ni changement de contexte.
  10. Page 3 — défaut, la page 1 est évincéeLa page 1, tout juste utilisée, est évincée parce qu'elle est la plus anciennement chargée. C'est le défaut structurel de FIFO : il confond l'âge du chargement et l'inutilité.
  11. Page 4 — défaut, la page 2 est évincéeNeuvième et dernier défaut. Le bilan sera de 9 défauts pour 12 accès, soit un taux de 75 %.
  12. Page 5 — succèsTrois succès sur douze accès. Avec QUATRE cadres au lieu de trois, la même suite produit 10 défauts au lieu de 9 : c'est l'anomalie de Belady, et c'est ce qui disqualifie FIFO.

Quiz · 1 question

Un serveur ralentit brutalement : l'utilisation du processeur tombe à 10 % alors que le disque est saturé. L'administrateur lance davantage de tâches, puisque le processeur semble libre. Que se passe-t-il ?

  • La charge supplémentaire va occuper le processeur inutilisé et rétablir le débitajouter de la charge
  • C'est un écroulement : les processus se volent leurs cadres et passent leur temps en défauts de page. Ajouter des processus aggrave exactement le problème ; il faut au contraire en suspendreécroulement
  • Le disque est défectueux et doit être remplacépanne matérielle

Réponse : C'est l'écroulement, et le piège est que le symptôme — un processeur inoccupé — suggère exactement le contraire du remède. La somme des ensembles de travail dépasse la mémoire disponible : chaque processus évince les pages d'un autre, qui les redemande aussitôt. Tout le monde est bloqué en attente disque, donc le processeur chôme. Un ordonnanceur naïf, ou un administrateur, en conclut qu'il y a de la place et admet davantage de processus — ce qui réduit encore le nombre de cadres par processus et enfonce le système. La seule sortie est de réduire le degré de multiprogrammation : suspendre un processus ENTIER libère son ensemble de travail d'un coup et laisse les autres repasser sous le seuil.

À vous

L'exercice attaque le point qui coince de front. Première partie : la traduction d'adresses, avec une table donnée, sur une série de cas dont certains doivent produire une exception — déplacement hors bornes, page absente, page en lecture seule écrite.

Seconde partie : les trois algorithmes de remplacement sur la même suite de références, avec le comptage des défauts. Puis la vérification de l'anomalie de Belady, en faisant varier le nombre de cadres de 3 à 5 — et en constatant que la courbe de FIFO n'est pas monotone, alors que celle de LRU l'est.

Exercice de code

Traduisez des adresses logiques avec leurs exceptions, puis comparez FIFO, LRU et optimal.

Point de départ

// ── 1. Traduction d'adresses ──────────────────────────────────────────────
const TAILLE_PAGE = 1024;            // 1 Kio, donc 10 bits de déplacement

// Une entrée de table : le cadre, la présence, les droits.
const TABLE = [
  { cadre: 5, present: true,  ecriture: false },   // page 0 : code, lecture seule
  { cadre: 9, present: true,  ecriture: true  },   // page 1 : données
  { cadre: 2, present: false, ecriture: true  },   // page 2 : sur le disque
  { cadre: 7, present: true,  ecriture: true  },   // page 3
];

function traduire(adresseLogique, ecrit = false) {
  const page = 0;          // ← à écrire
  const deplacement = 0;   // ← à écrire

  if (page >= TABLE.length) return { erreur: "hors de l'espace d'adressage" };
  const e = TABLE[page];
  if (!e.present) return { erreur: "DÉFAUT DE PAGE sur la page " + page };
  if (ecrit && !e.ecriture) return { erreur: "violation : écriture en lecture seule" };

  return { page, deplacement, cadre: e.cadre, physique: 0 };   // ← à écrire
}

const CAS = [
  { a: 3500, ecrit: false },   // page 3, déplacement 428 -> 7 x 1024 + 428
  { a: 0,    ecrit: false },
  { a: 1023, ecrit: false },   // dernier octet de la page 0
  { a: 1024, ecrit: false },   // premier octet de la page 1
  { a: 500,  ecrit: true  },   // écriture en lecture seule
  { a: 2100, ecrit: false },   // page absente
  { a: 9000, ecrit: false },   // au-delà de la table
];

for (const c of CAS) {
  const r = traduire(c.a, c.ecrit);
  console.log(String(c.a).padStart(5) + (c.ecrit ? " (écriture)" : "           ") + " -> " +
    (r.erreur ?? "page " + r.page + ", dépl. " + String(r.deplacement).padStart(4) +
                 ", cadre " + r.cadre + "  =>  physique " + r.physique));
}

// ── 2. Remplacement de pages ──────────────────────────────────────────────
const SUITE = [1, 2, 3, 4, 1, 2, 5, 1, 2, 3, 4, 5];

function fifo(suite, cadres) {
  const memoire = [];
  let defauts = 0;
  for (const page of suite) {
    if (memoire.includes(page)) continue;
    defauts++;
    if (memoire.length === cadres) memoire.shift();   // le plus ancien CHARGÉ
    memoire.push(page);
  }
  return defauts;
}

function lru(suite, cadres) { return 0; }        // ← à écrire
function optimal(suite, cadres) { return 0; }    // ← à écrire

// ── À VOUS ────────────────────────────────────────────────────────────────
// 1. Complétez traduire() : numéro de page, déplacement, adresse physique.
// 2. Écrivez lru (évincer la moins récemment UTILISÉE) et optimal (évincer
//    celle dont la prochaine utilisation est la plus lointaine).
// 3. Faites varier le nombre de cadres de 3 à 5 et cherchez l'anomalie de
//    Belady : chez qui la courbe n'est-elle pas décroissante ?

for (const c of [3, 4, 5]) console.log(c + " cadres : FIFO " + fifo(SUITE, c) + " défauts");

Solution

const TAILLE_PAGE = 1024;

const TABLE = [
  { cadre: 5, present: true,  ecriture: false },
  { cadre: 9, present: true,  ecriture: true  },
  { cadre: 2, present: false, ecriture: true  },
  { cadre: 7, present: true,  ecriture: true  },
];

function traduire(adresseLogique, ecrit = false) {
  // Les bits de poids fort numérotent la page, ceux de poids faible donnent
  // le déplacement. Avec une taille de page en puissance de 2, division et
  // modulo sont un simple découpage de bits — c'est pourquoi la taille de
  // page EST une puissance de 2.
  const page = Math.floor(adresseLogique / TAILLE_PAGE);
  const deplacement = adresseLogique % TAILLE_PAGE;

  if (page >= TABLE.length) return { erreur: "hors de l'espace d'adressage" };
  const e = TABLE[page];
  if (!e.present) return { erreur: "DÉFAUT DE PAGE sur la page " + page };
  if (ecrit && !e.ecriture) return { erreur: "violation : écriture en lecture seule" };

  // Le déplacement traverse INCHANGÉ : page et cadre ont la même taille.
  return { page, deplacement, cadre: e.cadre, physique: e.cadre * TAILLE_PAGE + deplacement };
}

const CAS = [
  { a: 3500, ecrit: false }, { a: 0, ecrit: false }, { a: 1023, ecrit: false },
  { a: 1024, ecrit: false }, { a: 500, ecrit: true }, { a: 2100, ecrit: false },
  { a: 9000, ecrit: false },
];

for (const c of CAS) {
  const r = traduire(c.a, c.ecrit);
  console.log(String(c.a).padStart(5) + (c.ecrit ? " (écriture)" : "           ") + " -> " +
    (r.erreur ?? "page " + r.page + ", dépl. " + String(r.deplacement).padStart(4) +
                 ", cadre " + r.cadre + "  =>  physique " + r.physique));
}

const SUITE = [1, 2, 3, 4, 1, 2, 5, 1, 2, 3, 4, 5];

function fifo(suite, cadres) {
  const memoire = [];
  let defauts = 0;
  for (const page of suite) {
    if (memoire.includes(page)) continue;
    defauts++;
    if (memoire.length === cadres) memoire.shift();
    memoire.push(page);
  }
  return defauts;
}

function lru(suite, cadres) {
  const memoire = [];   // du moins récemment utilisé au plus récent
  let defauts = 0;
  for (const page of suite) {
    const i = memoire.indexOf(page);
    if (i !== -1) {
      // Un SUCCÈS met à jour l'ordre : c'est toute la différence avec FIFO,
      // qui ne regarde que l'instant du chargement.
      memoire.splice(i, 1);
      memoire.push(page);
      continue;
    }
    defauts++;
    if (memoire.length === cadres) memoire.shift();
    memoire.push(page);
  }
  return defauts;
}

function optimal(suite, cadres) {
  const memoire = [];
  let defauts = 0;
  for (let t = 0; t < suite.length; t++) {
    const page = suite[t];
    if (memoire.includes(page)) continue;
    defauts++;
    if (memoire.length === cadres) {
      // On regarde l'AVENIR : la victime est celle dont la prochaine
      // utilisation est la plus lointaine, ou qui ne reviendra jamais.
      let victime = 0, plusLoin = -1;
      for (let i = 0; i < memoire.length; i++) {
        let prochain = suite.indexOf(memoire[i], t + 1);
        if (prochain === -1) { victime = i; break; }
        if (prochain > plusLoin) { plusLoin = prochain; victime = i; }
      }
      memoire.splice(victime, 1);
    }
    memoire.push(page);
  }
  return defauts;
}

console.log("");
console.log("cadres |  FIFO |  LRU  | optimal");
for (const c of [3, 4, 5, 6]) {
  console.log(String(c).padStart(6) + " | " +
    String(fifo(SUITE, c)).padStart(5) + " | " +
    String(lru(SUITE, c)).padStart(5) + " | " +
    String(optimal(SUITE, c)).padStart(7));
}
// Regardez la colonne FIFO entre 3 et 4 cadres : 9 puis 10. Ajouter de la
// mémoire a AUGMENTÉ le nombre de défauts. C'est l'anomalie de Belady, et
// elle vient de ce que FIFO ne garantit pas l'inclusion : l'ensemble des
// pages présentes avec 4 cadres n'est pas un sur-ensemble de celui à 3.
// LRU et l'optimal ont cette propriété d'inclusion, donc leur courbe est
// toujours décroissante.

En travaux pratiques

Travaux pratiques 6 · 3 h

La mémoire qu'on croit avoir

Constater que l'adresse manipulée par un programme n'existe pas, mesurer le coût d'un défaut de page, et faire paginer la machine volontairement.

Avant de commencer

  • Le TP 7 d'Architecture : hiérarchie mémoire
  • gcc, et l'accès à /proc

Énoncé

  1. La même adresse deux foisAffichez l'adresse d'une variable globale. Lancez le programme dans deux terminaux en même temps et comparez. Que concluez-vous ?
  2. Voir la carteFaites afficher par votre programme le contenu de son propre fichier maps dans /proc. Identifiez le code, les données, le tas, la pile, et les bibliothèques.
  3. Allouer sans consommerAllouez un gigaoctet avec malloc sans y écrire, et regardez la mémoire réellement utilisée. Écrivez ensuite un octet toutes les 4096 positions et regardez de nouveau. Indice : Une allocation est une promesse ; la page n'existe qu'au premier accès.
  4. Compter les défauts de pageMesurez les défauts de page mineurs et majeurs de votre programme. Distinguez-les et dites lequel coûte cher.
  5. Faire paginer la machineAllouez et écrivez plus que la mémoire physique disponible, dans une machine virtuelle. Observez le système. Notez le moment exact où la réactivité s'effondre.
  6. Projeter un fichierLisez un gros fichier de deux façons : avec read dans un tampon, et avec mmap. Chronométrez et expliquez la différence.
  7. Le partage invisibleAprès un fork, faites afficher par le père et le fils l'adresse et la valeur d'une même variable. Modifiez-la dans le fils et réaffichez les deux.

C'est réussi quand

  • Deux exécutions simultanées affichent la même adresse pour une variable différente
  • Vous montrez qu'un gigaoctet alloué ne consomme presque rien
  • Vous savez distinguer un défaut mineur d'un majeur et donner leur coût respectif

Correction

La même adresse, deux mémoires
terminal 1 : &compteur = 0x55d3e8a04010, valeur 42
terminal 2 : &compteur = 0x55d3e8a04010, valeur 7

même adresse VIRTUELLE, deux adresses PHYSIQUES différentes

L'adresse manipulée par un programme n'est pas une adresse en mémoire : c'est un numéro traduit par l'unité de gestion mémoire, différemment pour chaque processus. C'est ce qui rend l'isolation possible — un processus ne peut pas nommer la mémoire d'un autre, même s'il le voulait. Toute la protection du TP 1 découle de ce mécanisme, et il est MATÉRIEL.

La carte du processus
cat /proc/self/maps

55d3e8a01000-55d3e8a02000 r-xp   /usr/bin/monprog     ← code, exécutable
55d3e8a03000-55d3e8a04000 r--p   /usr/bin/monprog     ← constantes
55d3e8a04000-55d3e8a05000 rw-p   /usr/bin/monprog     ← données globales
55d3e9c1f000-55d3e9c40000 rw-p   [heap]               ← le tas, malloc
7f2c4a000000-7f2c4a028000 r-xp   /lib/libc.so.6
7ffd3c5e0000-7ffd3c601000 rw-p   [stack]              ← la pile

Chaque zone a ses droits, et ils sont appliqués par le matériel. Le code est r-xp : lisible, exécutable, NON MODIFIABLE — c'est ce qui interdit le code auto-modifiant du TP 5 d'Architecture. Le tas et la pile sont rw-p : modifiables, NON exécutables — c'est la protection W^X, qui rend un débordement de tampon beaucoup moins facile à exploiter.

L'allocation paresseuse
malloc(1 Go)                → mémoire résidente : 1,3 Mo
puis 1 octet toutes les 4 Ko → mémoire résidente : 1,0 Go

/* malloc ne fait que réserver un intervalle d'adresses.
 La page physique est allouée au PREMIER ACCÈS,
 par un défaut de page. */

C'est la surréservation : le système promet plus qu'il ne possède, en pariant que tout le monde ne réclamera pas en même temps. Le pari est presque toujours gagnant — et quand il ne l'est pas, le tueur de processus en choisit un et le supprime. C'est pourquoi, sous Linux, un malloc réussi ne garantit PAS que la mémoire sera là.

Mineur, majeur
défaut MINEUR : la page existe en mémoire, il manque juste
              l'entrée dans la table → ~1 µs
défaut MAJEUR : la page doit être lue sur le DISQUE → ~5 ms

/usr/bin/time -v ./monprog
Minor page faults: 262 144
Major page faults: 3

Un facteur cinq mille. Un programme qui accumule les défauts mineurs va bien ; le même avec des défauts majeurs est en train de paginer, et aucune optimisation de code ne le sauvera. C'est la première grandeur à regarder devant un programme mystérieusement lent, avant même le profileur.

L'effondrement
mémoire libre décroissante, tout va bien… puis :

si  in   out          cpu
0  0     0      95 % utilisateur
0  0     0      93 %
128 4 096 12 288  8 % utilisateur, 91 % en attente d'E/S
                ← le système ne calcule plus, il pagine

l'effondrement n'est pas progressif : il est brutal

L'écroulement, ou trashing : le système passe plus de temps à déplacer des pages qu'à exécuter du code, et chaque déplacement en provoque d'autres. La courbe de performance ne décroît pas doucement, elle tombe d'une falaise — parce que le coût passe de 100 ns à 5 ms d'un seul coup. C'est le même seuil que la marche du TP 7 d'Architecture, un cran plus bas.

mmap, et la copie sur écriture
read  : 0,84 s   (copie noyau → tampon utilisateur)
mmap  : 0,31 s   (pas de copie : le fichier EST la mémoire)

après fork :
père : &x = 0x7ffd… valeur 42
fils : &x = 0x7ffd… valeur 42      ← même adresse, MÊME page physique
fils modifie x = 99
père : 42   |   fils : 99          ← la page a été DUPLIQUÉE à l'écriture

La copie sur écriture explique pourquoi fork est rapide même sur un processus d'un gigaoctet : rien n'est copié, les tables pointent vers les mêmes pages en lecture seule, et la duplication n'a lieu que sur les pages effectivement modifiées. C'est aussi ce qui rend le couple fork/exec du TP 3 peu coûteux — exec remplace tout avant que la moindre page n'ait été dupliquée.

Ce que la suite en fait

Le bloc V descend d'un cran. La mémoire virtuelle de ce chapitre suppose un disque où ranger les pages absentes ; le chapitre 7 explique comment ce disque est organisé, et le chapitre 8 comment on lui parle.

Le lien est plus étroit qu'il n'y paraît. Le fichier projeté en mémoire, que le chapitre 7 présentera, applique la mécanique de la pagination à un fichier ordinaire : on l'ouvre, on obtient une plage d'adresses, et les lectures disque se font toutes seules par défaut de page. Le système de fichiers et la mémoire virtuelle sont deux visages du même mécanisme.

À retenir

Flashcards · 6 cartes

Comment traduit-on une adresse logique en adresse physique en pagination ?
Trois gestes. 1) numéro de page = adresse ÷ taille_page (bits de poids fort). 2) déplacement = adresse mod taille_page (bits de poids faible). 3) adresse physique = table[numéro] × taille_page + déplacement. Le point clé : le DÉPLACEMENT N'EST JAMAIS TRADUIT — page et cadre ont la même taille, donc le k-ième octet de l'une est le k-ième de l'autre. Contrôle : le déplacement doit être strictement inférieur à la taille de page.
Quelle est la différence entre fragmentation externe et interne ?
L'EXTERNE est le mal de l'allocation contiguë : la mémoire libre existe mais en trous dispersés, donc on ne peut pas loger un processus alors que le total suffirait. L'INTERNE est le mal de la pagination : la dernière page d'un processus n'est pas remplie, ce qui gaspille en moyenne une demi-page par processus, soit 2 Kio — un prix dérisoire. La pagination échange la première contre la seconde, et c'est un très bon échange.
À quoi sert la TLB, et quel est son coût caché ?
Sans elle, chaque accès du programme en demanderait DEUX : un pour lire la table des pages en mémoire, un pour l'octet voulu. La TLB est un petit cache totalement associatif de traductions récentes ; grâce à la localité, quelques dizaines d'entrées donnent plus de 98 % de succès. Son coût caché : elle est vidée à chaque changement de contexte, puisque les traductions appartiennent au processus sortant — ce qui s'ajoute au prix d'un quantum trop court.
Qu'est-ce qu'un défaut de page, et pourquoi son coût est-il si structurant ?
Une exception matérielle levée quand le bit de présence d'une page est à 0. Le système libère un cadre, lit la page depuis le disque, met à jour la table et RÉEXÉCUTE l'instruction fautive. Ce n'est pas une erreur du programme. Le coût : 60 ns pour un accès mémoire contre 10 ms pour un défaut sur disque magnétique, soit cent mille fois plus — un taux de un pour mille suffit à doubler le temps d'accès moyen.
Qu'est-ce que l'écroulement, et pourquoi le remède est-il contre-intuitif ?
La somme des ensembles de travail dépasse la mémoire : les processus se volent leurs cadres et passent leur temps en défauts de page. Le disque sature, tout le monde est bloqué, et le PROCESSEUR CHÔME. Un ordonnanceur naïf en conclut qu'il y a de la place et admet plus de processus, ce qui aggrave tout. Le remède est l'inverse du symptôme : suspendre un processus entier pour libérer son ensemble de travail d'un coup.
Comparez FIFO, optimal et LRU, et qu'est-ce que l'anomalie de Belady ?
FIFO évince la plus anciennement chargée : simple et mauvais, l'ancienneté ne dit rien de l'utilité. OPTIMAL évince celle dont la prochaine utilisation est la plus lointaine : le meilleur possible, mais irréalisable — il sert d'étalon. LRU évince la moins récemment utilisée : pari sur la localité temporelle, proche de l'optimal, approché en pratique par le bit de référence et l'algorithme de la seconde chance. L'anomalie de Belady : avec FIFO, AUGMENTER le nombre de cadres peut augmenter le nombre de défauts (9 sur 3 cadres, 10 sur 4 pour la suite 1 2 3 4 1 2 5 1 2 3 4 5). LRU et l'optimal en sont exempts.