Wikilivres
frwikibooks
https://fr.wikibooks.org/wiki/Accueil
MediaWiki 1.47.0-wmf.24
first-letter
Média
Spécial
Discussion
Utilisateur
Discussion utilisateur
Wikilivres
Discussion Wikilivres
Fichier
Discussion fichier
MediaWiki
Discussion MediaWiki
Modèle
Discussion modèle
Aide
Discussion aide
Catégorie
Discussion catégorie
Transwiki
Discussion Transwiki
Wikijunior
Discussion Wikijunior
TimedText
TimedText talk
Module
Discussion module
Event
Event talk
Neurosciences/L'éveil, le sommeil et la vigilance
0
65747
775324
733881
2026-10-16T17:09:01Z
Mewtow
31375
/* Les stades du sommeil */
775324
wikitext
text/x-wiki
On pourrait croire que le chapitre sur les rythmes circadiens vous a enseigné tout ce qu'il y a à savoir sur le sommeil, mais ce n'est pas le cas. S'il est vrai que le sommeil a une régularité similaire à celle du rythme circadien, l'horloge circadienne n'est pas la seule responsable de l'envie de dormir.
Les chercheurs ont remarqué que l'envie de sommeil n'avait parfois aucun rapport avec le rythme de cette horloge. Par exemple, le manque de sommeil augmente l'envie de dormir alors qu'il n'a pas d'effet sur l’horloge biologique. On peut l'observer en regardant non pas le sommeil, mais les autres paramètres qui évoluent avec une période de 24 heures (la température, par exemple). Ainsi, il est possible de désynchroniser l'envie de dormir du rythme circadien, suite à des privations de sommeil : le sujet aura envie de dormir à des horaires sans lien avec son rythme circadien.
Il y a donc quelque chose qui est capable d'induire le sommeil, sans que cela soit lié aux rythmes biologiques. Cette autre chose, les scientifiques lui ont donné les noms d'« homéostasie du sommeil » ou encore de « pression de sommeil ». Ce terme ne fait que formuler une évidence : plus on passe de temps éveillé, plus la fatigue se fait sentir et plus on a envie de dormir. Comme quoi, il fallait bien un chapitre complet sur le sommeil !
==La durée du sommeil==
Présent chez de nombreuses espèces animales, le sommeil est certainement apparu pour permettre aux animaux de conserver leur énergie. De nombreux animaux peu évolués peuvent tomber dans un état de torpeur, où ils restent immobiles durant plus ou moins longtemps, tandis que d'autres sont capables de dormir profondément. La durée de sommeil varie beaucoup chez les animaux, allant d'une absence quasi-totale à plus de 10 heures.
En moyenne, on estime qu'un être humain normal a besoin d'environ 8 à 9 heures de sommeil pour se sentir pleinement reposé. Cependant, cette moyenne cache une forte variabilité, vraisemblablement pour des raisons génétiques : les gros dormeurs doivent dormir 8 à 10 heures par nuit pour se sentir en forme, alors que les petits dormeurs se contentent de 3 à 4 heures.
De plus, l'âge rentre aussi en compte dans les besoins de sommeil, la durée idéale de sommeil diminuant progressivement jusqu’à la fin de l'adolescence. Un nouveau-né a tendance à dormir plus de 18 heures par jour, un enfant de 3 ans 11 à 13 heures, les adolescents ont besoin de 9 à 10 heures par jour et les adultes entre 7 et 8 heures. Contrairement à l'idée reçue, les personnes âgées dorment autant que les jeunes adultes, mais ont un sommeil plus fragmenté : elles dorment moins la nuit et se rattrapent le jour en faisant la sieste.
===La privation de sommeil===
Quand on manque de sommeil, tout se passe comme si le sujet en manque de sommeil avait une dette de sommeil à rattraper. Cela se remarque surtout par une diminution des performances du sujet : ses réactions sont plus lentes, il fait plus d'erreurs, sa concentration diminue, ses performances intellectuelles déclinent, etc.
Quelqu'un qui a mal dormit durant la nuit aura tendance à plus dormir que le lendemain pour rattraper sa dette de sommeil. Cependant, une nuit blanche est souvent rattrapée par quelques heures de sommeil supplémentaires. Plus que la durée du sommeil, c'est surtout la '''qualité du sommeil''', l'intensité du sommeil, qui permet de récupérer. Cette intensité dépend de la dette de sommeil accumulée, le sommeil étant plus réparateur après une restriction de sommeil. Et cette intensité du sommeil a une trace dans le cerveau, comme nous allons le voir dans la section suivante.
===Le ''Familial natural short sleep''===
Il existe des personnes qui ont besoin de très peu de sommeil, à peine 4 à 6 heures, pour se sentir reposées. Les personnes touchées n'expriment pas de perte de performances malgré cette courte durée de sommeil. Il s'agit d'une particularité génétique appelée ''Familial natural short sleep'', qui est très peu fréquente dans la population. Le ''Familial natural short sleep'' est un trait présent dès la naissance et dure toute la vie. De plus, il est associé à d'autres particularité : meilleure mémoire, sensibilité à la douleur diminuée, personnalité plus extravertie et optimiste.
N'allez pas croire que vous l'avez, beaucoup de monde pense avoir besoin de peu de sommeil et n'en ressent pas les conséquences cognitives, alors qu'ils sont en réalité en privation de sommeil et ont des performances dégradées. En réalité, cette particularité génétique est très rare. Les chercheurs n'ont identifié avec certitude qu'une cinquantaine de familles dans le monde avec cette particularité, et ces familles sont celles sur lesquelles la recherche génétique s'est concentrée. Mais quelques estimations disent que le chiffre réel est proche des 1% de la population. Les gènes concernés ne sont pas connus avec certitude, quelques candidats ont été rejetés (DEC2, ADRB1, autres). Il se pourrait que des gènes liés à l'orexine soient impliqués, mais sans certitude.
==Les stades du sommeil==
Le cerveau est très loin de rester inactif lors du sommeil. Même durant le sommeil, le cerveau a une activité électrique relativement soutenue. Pour rappel, nous avions vu dans le chapitre sur l'activité électrique du cerveau que l'activité électrique cérébrale se mesure avec un instrument composé de plusieurs électrodes placées à la surface du crâne : l''''électroencéphalographe''', ou EEG.
L'EEG permet de mesurer des rythmes électriques récurrents, appelés des ''rythmes cérébraux'', ou encore ''ondes cérébrales''. Ils sont classés arbitrairement dans 4 à 6 bandes de fréquences nommées alpha, bêta, gamma, delta, et thêta.
* Delta : Inférieur à 4 hertz
* Thêta : 4 à 7 hertz
* Alpha : 8 à 15 hertz
* Bêta : 16 à 31 hertz
* Gamma : Plus de 32 hertz
Les scientifiques ont remarqué que chaque bande de fréquence correspond à des niveaux d'éveil et de sommeil différents. L'activité électrique du cerveau varie entre le sommeil et l'éveil, et on peut observer des variations par rapport à la normale dans certains cas pathologiques. Si on observe l'EEG de quelqu'un qui dort, on remarque que le sommeil est organisé en '''phases de sommeil''', identifiables par l'apparition de certaines ondes cérébrales.
Pour simplifier, le sommeil est organisé en cycles, qui se répètent plusieurs fois durant une nuit. Un cycle fait environ 90 minutes, mais avec de fortes variation individuelles. Concrètement, un cycle va de 30 minutes à 3 heures, mais la moyenne est proche de 1h30. Un cycle contient une phase de ''sommeil lent'' et de ''sommeil paradoxal''. Le '''sommeil lent''' est une phase où les ondes cérébrales sont synchronisées, de basse fréquence et de forte amplitude. Tous les neurones émettent des potentiels d'action en même temps. Le '''sommeil paradoxal''' a une activité électrique proche de l'éveil : ondes de fréquence élevée, de faible amplitude, totalement désynchronisées.
[[File:Hypnogramme.svg|vignette|upright=2.0|Hypnogramme]]
{| class="wikitable"
|+ Stades du sommeil
|-
! scope="col"| Stade du sommeil|| Ondes cérébrales prédominantes
|-
| Eveil || Ondes Gamma, Bêta
|-
| Endormissement || Ondes Alpha
|-
| Sommeil lent léger || Ondes Thêta
|-
| Sommeil lent profond || Ondes Delta
|-
| Sommeil paradoxal|| Ondes Gamma, Bêta
|}
===Endormissement===
Lors de l'endormissement, les ondes alpha apparaissent par bouffées successives. Le sujet peut être réveillé facilement. Lors de cette phase, le sujet peut être pris d'hallucinations dites hypnagogiques, ainsi que de sursauts musculaires nommées myoclonies d’endormissement. Dans la majorité des cas, cela est tout à fait normal et n'a rien de pathologique (l'auteur en a déjà fait expérience, sans avoir de problème de santé particulier).
===Sommeil à ondes lentes===
Lors du sommeil à ondes lentes, on observe une diminution de la fréquence des ondes cérébrales et une augmentation de leur amplitude. Le dormeur devient de plus en plus difficile à réveiller. Si on réveille quelqu'un lors du sommeil à ondes lentes, il se sentira fatigué. Le dormeur peut rêver lors du sommeil lent, les rêves étant relativement logiques et peu chargés émotionnellement. Les chercheurs ont trouvé la marque de l'intensité du sommeil dans les ondes du sommeil lent, qui sont plus amples et plus lentes suite à une privation de sommeil. On peut découper le sommeil à ondes lentes en deux phases : le sommeil léger et le sommeil profond.
Le '''sommeil léger''' dure environ une demi-heure chez un sujet d'âge moyen, mais peut être plus long chez les personnes âgées. Dans ce stade, le tonus musculaire commence à diminuer progressivement dans ce stade, de même que les mouvements oculaires se font plus lents et de moindre intensité. La respiration et le rythme cardiaques deviennent plus réguliers et ralentissent un tout petit peu. Le dormeur peut encore être réveillé facilement dans ce stade du sommeil. Du bruit, ou de la lumière peuvent réveiller un dormeur dans ce stade. De plus, le dormeur ne se sentira pas trop fatigué après le réveil, même si il aura une performance légèrement amoindrie. Quelques micro-réveils, inférieures à la dizaine de secondes, peuvent se produire spontanément dans ce stade. Le dormeur peut se souvenir de ces phases de réveil, qui sont parfois sources d'hallucinations hypnagogiques. Il donne un EEG avec des ondes Thêta, des bouffées d'ondes de haute fréquence (12-14 Hertz) appelées '''fuseaux du sommeil''' qui proviennent de la boucle thalamico-corticale et des ondes de haute amplitude qui apparaissent en réaction à des bruits extérieurs ('''complexes K''').
[[File:Stage2sleep.svg|centre|vignette|upright=2.0|Sommeil léger.]]
Lors du '''sommeil profond''', les ondes Delta s'installent. Les muscles ne sont pas paralysés et le dormeur peut encore bouger. C'est d'ailleurs lors de cette phase que les somnambules sortent de leur lit ou que les terreurs nocturnes surviennent. De manière générale, une bonne partie de ce que les médecins appellent parasomnies survient dans ce stade du sommeil. Si on réveille quelqu'un en sommeil profond, celui-ci se sentira fatigué et aura des performances fortement dégradées. La durée de cette baisse de performance est d'environ une demi-heure, une heure maximum. Les rêves sont courants en sommeil profond, comparé au sommeil léger. Ces rêves sont souvent assez logiques, cohérents, bien organisés et assez peu chargés émotionnellement.
Ce sommeil profond aurait comme rôle principal de réparer le corps et de lui permettre de reposer. À l'appui de cette affirmation, on peut dire que l'hormone de croissance est surtout produite lors du sommeil profond : cette hormone de croissance sert non seulement de faire grandir les enfants, mais aide aussi les cellules et tissus à se réparer. La consommation énergétique du cerveau est aussi beaucoup diminuée lors du sommeil profond, comparé à l'éveil. De plus, une nuit reposante est associée à des ondes lentes de grande amplitude lors du sommeil profond. Ce stade du sommeil aurait aussi un rôle à jouer dans l'apprentissage et la mémorisation. Plus précisément, il aurait une influence positive sur la consolidation des souvenirs et des connaissances acquises dans la journée. Dans le détail, les ondes lentes du sommeil profond permettraient le transfert des informations apprises de l'hippocampe vers le néocortex.
===Sommeil paradoxal===
Immédiatement après les 80 minutes de sommeil à onde lentes, le '''sommeil paradoxal''' s'installe. Si on l'appelle sommeil paradoxal, c'est parce que l'EEG est très proche de l'éveil, avec quelques différences. L'hippocampe est dominé par des ondes thêta, ce qui n'est pas le cas lors de la veille. De plus, les régions motrices du cerveau sont inactives, le dormeur étant complètement paralysé. Seuls les yeux du dormeur peuvent encore bouger, ils effectuent beaucoup de mouvements rapides sous l'effet d'ondes cérébrales produites par la moelle épinière : les ondes ponto-geniculo-occipitales. Le rythme cardiaque et la respiration augmentent, de même que la pression artérielle. Les rêves sont courants lors du sommeil paradoxal, nettement plus que dans les autres phases du sommeil. De plus, les rêves du sommeil paradoxal ont tendance à être assez illogiques, incohérents, riches en émotions. Leur contenu est souvent incompréhensible, vague, bizarre. Enfin, le pénis entre en érection, ce qui est très utile pour savoir si un cas d'impuissance a une origine physique ou psychique.
==L'anatomie du sommeil et de l'éveil==
On l'a vu, la chimie du sommeil donne quelques indications sur les processus à l’œuvre dans le cerveau lors de l'endormissement ou de l'éveil. Mais les molécules précitées n'ont pas un effet global sur le cerveau : leur effet semble localisé à un petit ensemble de structures cérébrales bien précises, qui seraient autant de zones cérébrales dédiées à l'endormissement ou aux différents stades du sommeil. Certaines zones du cerveau ne sont actives qu'à l'état d'éveil, d'autres ne l'étant que lors du sommeil paradoxal ou du sommeil lent. L'identification de ces zones a permis de déterminer l'existence de trois systèmes : un qui maintient le sujet à l'état d'éveil, un autre actif lors du sommeil lent et un autre actif uniquement lors du sommeil paradoxal. Ces systèmes sont tous composés d'un ensemble d'aires cérébrales distinctes, qui ont chacun leur rôle à jouer. Ces systèmes interagissent entre eux, notamment pour s'inhiber mutuellement. Par exemple, le système d'éveil inhibe les systèmes du sommeil lent et paradoxal. Ces deux systèmes s'inhibent eux-mêmes mutuellement.
[[File:Centres de l'éveil et du sommeil.png|centre|vignette|upright=2.5|Centres de l'éveil et du sommeil]]
===L'éveil===
On a vu que l'éveil se traduit par une activité électrique soutenue dans le cortex, riche en ondes cérébrales rapides et désynchronisée. Cette activité s'atténue et devient de plus en plus rythmique et lente lors du sommeil. Le centre de l'éveil a pour but de maintenir cette activité dans le cortex. Le centre de l'éveil a longtemps été pensé comme une structure unique. Mais on sait aujourd'hui que celui-ci regroupe des structures cérébrales distantes, qui ne sont pas déterminantes à elles seules. La lésion de ces structures peut entraîner de sérieuses pertes de vigilance, qui peuvent aller jusqu’au coma profond. Ces trois aires suivantes sont souvent regroupées dans ce qu'on appelle le '''réseau exécutif de l’éveil'''.
{|
|[[File:Système d'éveil - cerveau.png|vignette|upright=2.5|Système d'éveil - cerveau]]
|[[File:NeuralCorrelatesOfConsciousness2.jpg|vignette|upright=2.0|Réseau exécutif de l’éveil et formation réticulée.]]
|}
====Le réseau exécutif de l'éveil====
Les premières aires cérébrales identifiées comme faisant partie du centre de l'éveil le furent en 1918, lors de l'épidémie de grippe espagnole. Durant cette période, le neurologue Constantin von Economo remarqua que certains de ses patients tombaient dans le coma suite à diverses séquelles neurologiques, tandis que d’autres étaient atteints de sévères insomnies, allant jusqu’à une agrypnie quasi-totale. Il se mit en tête d'étudier quelles étaient les aires cérébrales lésées pouvant expliquer ce résultat. Les patients léthargiques ou comateux avaient des lésions à l''''hypothalamus postérieur'''. Celui-ci envoie des axones dans l'ensemble du cortex, innervant celui-ci de façon diffuse. Plus précisément, on peut distinguer une contribution distincte du noyau inféro-mammilaire et du noyau latéral. Les neurones du premier sont des neurones à orexine, tandis que les neurones du premier relâchent de l'histamine dans les structures cérébrales voisines, ce qui est cohérent avec l'implication de ce neurotransmetteur dans l'état d'éveil. Il est apparu que son action est en partie une action contre-inhibitrice. En effet, l'hypothalamus antérieur se connecte à des neurones GABAergiques, à l'action inhibitrice. Les neurones de l'hypothalamus inhiberaient ces neurones GABAergiques dispersés dans le cortex : cette inhibition de l'inhibition GABAergiques se traduit naturellement par une activation du cortex.
On peut aussi citer les noyaux du '''télencéphale basal''', qui émettent de l'acétylcholine et du glutamate, le principal d'entre eux étant le noyau basal de Meynert. Leur action est directement excitatrice sur le cortex.
[[File:Thalamus, boucles thalamico-corticales et sommeil.png|vignette|Thalamus, boucles thalamico-corticales et sommeil.]]
Il est rapidement apparu que le '''thalamus''' a aussi un rôle à jouer dans l'état de vigilance et dans le cycle veille-sommeil. En effet, le thalamus régule l'activité cérébrale dans le cortex. Lors de la veille, ces boucles sont le lieu d'une activité cérébrale désynchronisée. Mais lors du sommeil lent, une activité rythmique prend place et il est aujourd'hui évident que ce comportement est dirigé par des neurones réticulaires, eux-mêmes commandés par des formations du tronc cérébral. Pour détailler le rôle du thalamus lors du sommeil, nous allons nous référer au schéma ci-contre. Rappelons que les noyaux du thalamus sont presque tous des relais sensoriels, qui transmettent les informations sensorielles au cortex cérébral. Ces noyaux de relai sensoriels sont chapeautés par un noyau thalamique réticulaire, qui inhibe les noyaux relai. D'autres noyaux sont des relais corticaux, à savoir qu'ils reçoivent des axones du cortex et lui en renvoient. ces noyaux forment avec le cortex des circuits fermés, où le cortex se connecte au thalamus, qui lui-même innerve le cortex. Ces circuits fermés sont appelés des boucles thalamico-corticales. Lors de l'éveil, les noyaux de relai sensoriels sont actifs et transmettent les sensations au cortex. Mais lors du sommeil, le noyau réticulaire s'active et éteint les noyaux de relai sensoriel. Le cerveau est alors isolé du monde extérieur et le cortex fonctionne en circuit fermé. Seules les boucles thalamico-corticales fonctionnent et leur activité est gouvernée par le noyau réticulaire. Or, le noyau réticulaire thalamique a une activité rythmique, semblable à celle des ondes lentes du sommeil lent.
====La formation réticulée : le système activateur ascendant====
Mais le réseau exécutif de l'éveil n'est pas le seul à avoir une influence sur l'état de veille. Les observations et les autopsies sur des patients comateux montrent que tous ont des lésions de la formation réticulée, suggérant que celle-ci est une des aires de commande du thalamus. On peut voir le réseau exécutif de l’éveil postérieur comme un relai de la formation réticulée. Durant longtemps, les savants crurent que cette structure correspondait au centre de l'éveil, d'où le nom qui lui a été donné de '''système réticulé activateur ascendant'''. Ce système est innervé par la plupart des afférences sensorielles, mais aussi par les structures corticales. On voyait autrefois ce système comme une sorte de centre qui recevait les sensations et les intégrait avant d'activer le cerveau en conséquence. Plus le cerveau était soumis à des stimulations sensorielles fortes (douleur, vision lumineuse, autre), plus l'activation cérébrale et l'éveil étaient forts. L'éveil était alors vu comme la conséquence d'une stimulation sensorielle suffisamment forte, tandis que le sommeil se manifestait quand les stimulations sensorielles devenaient insuffisantes. On sait aujourd'hui que cette vision est au moins partielle, trop simplifiée pour être exacte.
Le système réticulaire ascendant comprend un grand nombre de noyaux appartenant à la formation réticulée, les plus connus étant le locus coeruleus et les noyaux du raphé. À ceux-ci, il faut ajouter des noyaux cholinergiques, qui activent les neurones thalamiques. On peut aussi citer les noyaux réticulés bulbaires magnocellulaires, qui émettent de l'acétylcholine et du glutamate, deux neurotransmetteurs excitateurs. Ces derniers innervent aussi bien le thalamus que l'hypothalamus postérieur. Et enfin, citons les noyaux du noyau parabrachial, qui innervent directement le télencéphale basal. Le système réticulaire ascendant active l'ensemble du néocortex par l'intermédiaire de deux voies : une voie réticulo-thalamico-corticale qui passe par le thalamus, et une voie reticulo-hypothalamico-corticale qui fait relai dans l'hypothalamus.
===Le sommeil lent===
Lors de l'endormissement, les boucles thalamico-corticales deviennent insensibles aux stimulations extérieures. Leur activité électrique se résume à une oscillation rythmique, périodique, spontanée. Cela se produit quand le système de veille se désactive, cesse d'inhiber les neurones pacemaker du thalamus. De plus, l'activité électrique globale du cortex doit diminuer, ce qui implique une inhibition du locus coeruleus, du noyau du raphé, et des autres composants du système de veille.
Le système responsable de cette inhibition est localisé dans l'hypothalamus antérieur, et plus précisément dans l''''aire pré-optique'''. La preuve principale étant que toute lésion de cette aire entraîne une insomnie presque totale. Cette aire est sensible à l'adénosine et on peut la voir comme une sorte de capteur qui mesurerait la pression de sommeil, afin de déclencher le sommeil quand celle-ci atteint un niveau trop élevé. Cette aire pré-optique contient des neurones inhibiteurs GABAergiques, qui innerveraient l'ensemble du système d'éveil. Ainsi, quand la pression de sommeil est trop forte, ces neurones émettent suffisamment de GABA pour éteindre le système de veille. Cependant, l'aire pré-optique est inhibé par le réseau de l'éveil, ces deux systèmes s'inhibant mutuellement. L'aire pré-optique est notamment sensible à la sérotonine, l'adrénaline et l'acétylcholine, qui l'inhibent. Seuls les neurones à hypocrétine/orexine de l'hypothalamus n'influencent pas l'aire pré-optique. Cela suggère que ceux-ci stabiliseraient l'état d'éveil. D'ailleurs, la narcolepsie est en grande partie causée par une destruction de ces neurones à orexine.
Cependant, l'aire pré-optique n'est pas le seul mécanisme d'entrée en sommeil lent. Une destruction totale de l'aire pré-optique entraîne certes une diminution du temps passé en sommeil lent, ainsi que des difficultés d'endormissement, mais elle ne supprime pas le sommeil lent. D'autres systèmes du sommeil lent doivent donc exister. À l'heure actuelle, les chercheurs suspectent l'implication des neurones GABAergiques de la zone parafaciale du tronc cérébral. L'activation de ces neurones semble induire un état proche, si ce n'est identique, du sommeil lent. Ils agiraient par inhibition de la transmission glutamatinergique du noyau parabrachial.
===Le sommeil paradoxal===
Le sommeil paradoxal est commandé par un ensemble de noyaux spécialisés qui interagissent entre eux. Certains de ces noyaux, ainsi que les aires cérébrales du cortex, sont aussi actifs lors de l'éveil. C'est notamment le cas des noyaux cholinergiques, qui s'activent lors du sommeil paradoxal. La chimie du sommeil paradoxal est donc un intermédiaire entre le sommeil lent et l'éveil. La production d'acétylcholine est proche de celle de l'éveil, tandis que la production de monoamines et d'orexine est nulle.
Mais d'autres noyaux sont spécifiquement actifs lors du sommeil paradoxal. Leurs neurones sont appelés des '''neurones SP-on'''. Ceux-ci sont responsables de ce qui se passe lors du sommeil paradoxal. Ces noyaux SP-on contiennent les neurones glutaminergique du noyau sublatérodorsal, la partie ventrolatérale de la substance grise périacqueducale, le noyau reticularis pontis oralis (un noyau sensible à l'acétylcholine) et enfin le noyau paragigantocellulaire latéral. L'inhibition motrice du sommeil paradoxal est causée par l'activation du noyau sublatérodorsal. Ainsi, des lésions de ce noyau suppriment l'atonie musculaire lors du sommeil paradoxal. Ceux-ci émettent des afférences glutaminergiques vers des noyaux GABAergiques/glycinergique du myélencéphalequi innervent la moelle épinière. Les ondes géniculopontiques sont générés dans le tegmentum du mésencéphale.
Certaines structures du cerveau, n'appartenant pas à ce système, sont totalement désactivées lors du sommeil paradoxal. C'est notamment le cas du locus coeruleus et du noyau du raphé. Les neurones de ces aires sont donc appelés des '''neurones SP-off''', pour bien signaler qu'il s"éteignent lors du sommeil paradoxal. De manière générale, le cerveau ne sécrète presque pas de monoamine lors du sommeil paradoxal. L'activation des systèmes mono-aminergiques pendant le sommeil paradoxal interrompt d'ailleurs celui-ci. C'est ainsi que les anti-dépresseurs, qui favorisent la production de mono-amines, peuvent diminuer ou complètement abolir le sommeil paradoxal. Les inhibiteurs sélectifs de la sérotonine vont ainsi diminuer la durée de sommeil paradoxal, tandis que les tricycliques et les inhibiteurs de la monoamine oxydase vont parfois totalement l'abolir.
==Les troubles du sommeil==
Le sommeil peut facilement être troublé, que ce soit par des maladies, de l'anxiété, des médicaments, ou bien d'autres raisons. Le chapitre précédent a notamment abordé les troubles du sommeil liés aux troubles du rythme circadien. Mais ce sont loin d'être les seuls, comme tout insomniaque le sait déjà ! Mais leur classification est clairement assez difficile, plusieurs classifications existant à l'heure actuelle. De nos jours, les trois classifications qui font référence sont celles du DSM (le manuel de diagnostic des troubles mentaux), l'ICD (International Classification of Disease de l'OMS) et celle de l'ICSD (International Classification of Sleep Disorder). Mais ces trois classifications sont assez similaires, ce qui fait que les maladies du sommeil sont assez bien identifiées. Dans les grandes lignes, les troubles du sommeil se classent en quelques grands syndromes : les troubles du rythme circadien, l'insomnie, les hypersomnies, les parasomnies, les mouvements anormaux liés au sommeil et les apnées du sommeil. Il arrive aussi certaines maladies modifient la durée du sommeil, certaines pouvant faire disparaître totalement le sommeil : on parle alors d'agrypnie.
===L'insomnie===
L''''insomnie''' est juste une réduction de la quantité de sommeil, là où les hypersomnies correspondent à l'inverse, à savoir un excès de sommeil. Le syndrome le plus courant est clairement l'insomnie, qui frappe bien plus de monde que l'hypersomnie. La plupart des insomnies sont temporaires, liées à de l'anxiété ou une origine psychologique bénigne quelconque. Ces insomnies passent le plus souvent en quelques semaines ou quelques jours au mieux. Plus rares sont les insomnies organiques, dont la cause est totalement autre que psychologique. Elles peuvent provenir de la consommation d’existants ou de médicaments aux effets stimulants, mais aussi de troubles neurologiques ou psychiatriques un peu plus sérieux. Par exemple, les patients dépressifs ont souvent des insomnies (ou des hypersomnies, selon le cas) : ils ont notamment tendance à se réveiller plus tôt le matin et à se lever plus fréquemment la nuit. On observe aussi des insomnies assez sévères chez les bipolaires en phase maniaque : leurs besoins en sommeil diminuent aux alentours de 2 ou 3 heures de sommeil.
===Les hypersomnies===
Les '''hypersomnies''' sont plus rares, quoiqu'elles soient le second trouble du sommeil en termes de fréquence. La plupart ont des origines assez classiques : une réaction face un manque de sommeil, le résultat de problèmes psychologiques, l'arrivée de l'hiver, etc. Rarement, l'hypersomnie est liée à des maladies psychiatriques. Par exemple, la dépression peut entraîner des hypersomnies assez importantes chez certains patients. L'abus d'hypnotiques est aussi une raison assez évidente. Mais certaines maladies peuvent entraîner des hypersomnies assez sévères, assez idiosyncratiques. Ces maladies sont connues sous le nom de narcolepsie et de syndrome de Klein-Levin.
La '''narcolepsie''' est une maladie caractérisée par des endormissements soudains, en pleine journée. Ceux-ci sont caractéristiques de la narcolepsie, mais elle peut être couplée à des symptômes assez accessoires, comme une fatigue diurne et des insomnies. Les hallucinations lors de l'endormissement ou du réveil sont aussi assez fréquentes, bien que parfois absentes chez certaines narcoleptiques. Beaucoup de narcoleptiques sont aussi atteints de catalepsie, un syndrome caractérisé par une décontraction totale des muscles sans perte de conscience. Un patient cataleptique peut ainsi s'effondrer ou chuter soudainement, suite à une décontraction de tous ses muscles, bien que la catalepsie puisse se limiter à quelques muscles bien précis. Cela arrive suite à une émotion, ou spontanément. Chose étonnante, les endormissements soudains ne font pas passer le malade en sommeil lent, mais le font passer directement en sommeil paradoxal. La cause la plus probable de la narcolepsie serait un dysfonctionnement impliquant l'orexine, soit que les neurones à orexine dysfonctionnent, soit qu'une déficience en orexine se fasse jour dans le cerveau.
Certaines hypersomnies sont dites récurrentes, à savoir qu'elles surviennent "régulièrement". Le syndrome de ce type le plus connu est le '''syndrome de Klein-Levin''', bien qu'il soit particulièrement rare. Ce syndrome se manifeste par poussées d'hypersomnies, avec rémission complète entre les poussées. Lors des poussées, le patient dort beaucoup plus que la normale, mais ce n'est pas le seul symptôme décelable, des symptômes cognitifs ou psychiatriques sont aussi fréquents. Niveau psychiatrique, le sujet est irritable, fatigué, apathique, parfois dépressif. Plus grave, des manifestations psychiatriques plus importantes peuvent se produire : hallucinations, délire, hypersexualité, confusion mentale, délirium, variations d'humeur, etc. Des troubles cognitifs sont très fréquents : difficultés de concentration, troubles de la mémoire, et autres.
===Les troubles du comportement en sommeil paradoxal===
Les troubles du sommeil précédent dégradaient la durée du sommeil, mais quelques troubles ne rentrent pas dans ce cadre. Les '''troubles du comportement en sommeil paradoxal''', que nous abrégerons en TCSP, sont de ceux-là. Il s'agit de rêves agités ou cauchemardesques où le sujet se met à bouger, parfois au point d'avoir un comportement violent ou impulsif. Ce trouble ressemble quelque peu au somnambulisme, si ce n'est pour une différence de taille : si le somnambulisme est un trouble du sommeil lent, les phases de pseudo-réveil du TCSP ont lieu assez souvent 90 minutes après l'endormissement, lors du sommeil paradoxal. Ce trouble a une origine assez intéressante : il se traduit par une abolition de la paralysie lors du sommeil paradoxal. Des muscles qui devraient perdre tout leur tonus et ne pas se contracter lors du sommeil paradoxal vont en fait fonctionner comme lors de l'éveil.
Le TCSP peut avoir plusieurs causes, comme l'usage d'antidépresseurs. Ceux-ci ont tendance à réduire le sommeil paradoxal et à dégrader celui-ci. Il n'est donc pas étonnant que ces médicaments déclenchent de tels troubles du sommeil paradoxal. Mais la plupart du temps, ce trouble n'a pas de cause bien définie. Le TCSP est plus fréquent chez les sujets âgés, notamment chez ceux atteints de maladies neurodégénératives. Chose importante, plus de 30% des patients âgés atteints de TCSP déclarent des maladies neurodégénératives dans les années ou décennies qui suivent l'apparition du TCSP. Plus précisément, ils déclarent des synucléopathies, tel la maladie de Parkinson, la paralysie supra-nucléaire progressive, ou la démence à corps de Lewy. Le TCSP est inhabituel dans les tauopathies et amyloses.
<noinclude>
{{NavChapitre | book=Neurosciences
| prev=Les rythmes circadiens
| prevText=Les rythmes circadiens
| next=L'état de conscience
| nextText=L'état de conscience
}}{{autoCat}}
</noinclude>
8i8k8hi8waw1zpdv9qbstlh0kqu48og
775325
775324
2026-10-16T17:09:32Z
Mewtow
31375
/* Les stades du sommeil */
775325
wikitext
text/x-wiki
On pourrait croire que le chapitre sur les rythmes circadiens vous a enseigné tout ce qu'il y a à savoir sur le sommeil, mais ce n'est pas le cas. S'il est vrai que le sommeil a une régularité similaire à celle du rythme circadien, l'horloge circadienne n'est pas la seule responsable de l'envie de dormir.
Les chercheurs ont remarqué que l'envie de sommeil n'avait parfois aucun rapport avec le rythme de cette horloge. Par exemple, le manque de sommeil augmente l'envie de dormir alors qu'il n'a pas d'effet sur l’horloge biologique. On peut l'observer en regardant non pas le sommeil, mais les autres paramètres qui évoluent avec une période de 24 heures (la température, par exemple). Ainsi, il est possible de désynchroniser l'envie de dormir du rythme circadien, suite à des privations de sommeil : le sujet aura envie de dormir à des horaires sans lien avec son rythme circadien.
Il y a donc quelque chose qui est capable d'induire le sommeil, sans que cela soit lié aux rythmes biologiques. Cette autre chose, les scientifiques lui ont donné les noms d'« homéostasie du sommeil » ou encore de « pression de sommeil ». Ce terme ne fait que formuler une évidence : plus on passe de temps éveillé, plus la fatigue se fait sentir et plus on a envie de dormir. Comme quoi, il fallait bien un chapitre complet sur le sommeil !
==La durée du sommeil==
Présent chez de nombreuses espèces animales, le sommeil est certainement apparu pour permettre aux animaux de conserver leur énergie. De nombreux animaux peu évolués peuvent tomber dans un état de torpeur, où ils restent immobiles durant plus ou moins longtemps, tandis que d'autres sont capables de dormir profondément. La durée de sommeil varie beaucoup chez les animaux, allant d'une absence quasi-totale à plus de 10 heures.
En moyenne, on estime qu'un être humain normal a besoin d'environ 8 à 9 heures de sommeil pour se sentir pleinement reposé. Cependant, cette moyenne cache une forte variabilité, vraisemblablement pour des raisons génétiques : les gros dormeurs doivent dormir 8 à 10 heures par nuit pour se sentir en forme, alors que les petits dormeurs se contentent de 3 à 4 heures.
De plus, l'âge rentre aussi en compte dans les besoins de sommeil, la durée idéale de sommeil diminuant progressivement jusqu’à la fin de l'adolescence. Un nouveau-né a tendance à dormir plus de 18 heures par jour, un enfant de 3 ans 11 à 13 heures, les adolescents ont besoin de 9 à 10 heures par jour et les adultes entre 7 et 8 heures. Contrairement à l'idée reçue, les personnes âgées dorment autant que les jeunes adultes, mais ont un sommeil plus fragmenté : elles dorment moins la nuit et se rattrapent le jour en faisant la sieste.
===La privation de sommeil===
Quand on manque de sommeil, tout se passe comme si le sujet en manque de sommeil avait une dette de sommeil à rattraper. Cela se remarque surtout par une diminution des performances du sujet : ses réactions sont plus lentes, il fait plus d'erreurs, sa concentration diminue, ses performances intellectuelles déclinent, etc.
Quelqu'un qui a mal dormit durant la nuit aura tendance à plus dormir que le lendemain pour rattraper sa dette de sommeil. Cependant, une nuit blanche est souvent rattrapée par quelques heures de sommeil supplémentaires. Plus que la durée du sommeil, c'est surtout la '''qualité du sommeil''', l'intensité du sommeil, qui permet de récupérer. Cette intensité dépend de la dette de sommeil accumulée, le sommeil étant plus réparateur après une restriction de sommeil. Et cette intensité du sommeil a une trace dans le cerveau, comme nous allons le voir dans la section suivante.
===Le ''Familial natural short sleep''===
Il existe des personnes qui ont besoin de très peu de sommeil, à peine 4 à 6 heures, pour se sentir reposées. Les personnes touchées n'expriment pas de perte de performances malgré cette courte durée de sommeil. Il s'agit d'une particularité génétique appelée ''Familial natural short sleep'', qui est très peu fréquente dans la population. Le ''Familial natural short sleep'' est un trait présent dès la naissance et dure toute la vie. De plus, il est associé à d'autres particularité : meilleure mémoire, sensibilité à la douleur diminuée, personnalité plus extravertie et optimiste.
N'allez pas croire que vous l'avez, beaucoup de monde pense avoir besoin de peu de sommeil et n'en ressent pas les conséquences cognitives, alors qu'ils sont en réalité en privation de sommeil et ont des performances dégradées. En réalité, cette particularité génétique est très rare. Les chercheurs n'ont identifié avec certitude qu'une cinquantaine de familles dans le monde avec cette particularité, et ces familles sont celles sur lesquelles la recherche génétique s'est concentrée. Mais quelques estimations disent que le chiffre réel est proche des 1% de la population. Les gènes concernés ne sont pas connus avec certitude, quelques candidats ont été rejetés (DEC2, ADRB1, autres). Il se pourrait que des gènes liés à l'orexine soient impliqués, mais sans certitude.
==Les stades du sommeil==
Le cerveau est très loin de rester inactif lors du sommeil. Même durant le sommeil, le cerveau a une activité électrique relativement soutenue. Pour rappel, nous avions vu dans le chapitre sur l'activité électrique du cerveau que l'activité électrique cérébrale se mesure avec un instrument composé de plusieurs électrodes placées à la surface du crâne : l''''électroencéphalographe''', ou EEG.
L'EEG permet de mesurer des rythmes électriques récurrents, appelés des ''rythmes cérébraux'', ou encore ''ondes cérébrales''. Ils sont classés arbitrairement dans 4 à 6 bandes de fréquences nommées alpha, bêta, gamma, delta, et thêta.
* Delta : Inférieur à 4 hertz
* Thêta : 4 à 7 hertz
* Alpha : 8 à 15 hertz
* Bêta : 16 à 31 hertz
* Gamma : Plus de 32 hertz
Les scientifiques ont remarqué que chaque bande de fréquence correspond à des niveaux d'éveil et de sommeil différents. L'activité électrique du cerveau varie entre le sommeil et l'éveil, et on peut observer des variations par rapport à la normale dans certains cas pathologiques. Si on observe l'EEG de quelqu'un qui dort, on remarque que le sommeil est organisé en '''phases de sommeil''', identifiables par l'apparition de certaines ondes cérébrales.
Lors de l'endormissement, les ondes alpha apparaissent par bouffées successives. Le sujet peut être réveillé facilement. Lors de cette phase, le sujet peut être pris d'hallucinations dites hypnagogiques, ainsi que de sursauts musculaires nommées myoclonies d’endormissement. Dans la majorité des cas, cela est tout à fait normal et n'a rien de pathologique (l'auteur en a déjà fait expérience, sans avoir de problème de santé particulier).
Une fois le sujet endormi, son sommeil est organisé en cycles, qui se répètent plusieurs fois durant une nuit. Un cycle fait environ 90 minutes, mais avec de fortes variation individuelles. Concrètement, un cycle va de 30 minutes à 3 heures, mais la moyenne est proche de 1h30. Un cycle contient une phase de ''sommeil lent'' et de ''sommeil paradoxal''. Le '''sommeil lent''' est une phase où les ondes cérébrales sont synchronisées, de basse fréquence et de forte amplitude. Tous les neurones émettent des potentiels d'action en même temps. Le '''sommeil paradoxal''' a une activité électrique proche de l'éveil : ondes de fréquence élevée, de faible amplitude, totalement désynchronisées.
[[File:Hypnogramme.svg|vignette|upright=2.0|Hypnogramme]]
{| class="wikitable"
|+ Stades du sommeil
|-
! scope="col"| Stade du sommeil|| Ondes cérébrales prédominantes
|-
| Eveil || Ondes Gamma, Bêta
|-
| Endormissement || Ondes Alpha
|-
| Sommeil lent léger || Ondes Thêta
|-
| Sommeil lent profond || Ondes Delta
|-
| Sommeil paradoxal|| Ondes Gamma, Bêta
|}
===Sommeil à ondes lentes===
Lors du sommeil à ondes lentes, on observe une diminution de la fréquence des ondes cérébrales et une augmentation de leur amplitude. Le dormeur devient de plus en plus difficile à réveiller. Si on réveille quelqu'un lors du sommeil à ondes lentes, il se sentira fatigué. Le dormeur peut rêver lors du sommeil lent, les rêves étant relativement logiques et peu chargés émotionnellement. Les chercheurs ont trouvé la marque de l'intensité du sommeil dans les ondes du sommeil lent, qui sont plus amples et plus lentes suite à une privation de sommeil. On peut découper le sommeil à ondes lentes en deux phases : le sommeil léger et le sommeil profond.
Le '''sommeil léger''' dure environ une demi-heure chez un sujet d'âge moyen, mais peut être plus long chez les personnes âgées. Dans ce stade, le tonus musculaire commence à diminuer progressivement dans ce stade, de même que les mouvements oculaires se font plus lents et de moindre intensité. La respiration et le rythme cardiaques deviennent plus réguliers et ralentissent un tout petit peu. Le dormeur peut encore être réveillé facilement dans ce stade du sommeil. Du bruit, ou de la lumière peuvent réveiller un dormeur dans ce stade. De plus, le dormeur ne se sentira pas trop fatigué après le réveil, même si il aura une performance légèrement amoindrie. Quelques micro-réveils, inférieures à la dizaine de secondes, peuvent se produire spontanément dans ce stade. Le dormeur peut se souvenir de ces phases de réveil, qui sont parfois sources d'hallucinations hypnagogiques. Il donne un EEG avec des ondes Thêta, des bouffées d'ondes de haute fréquence (12-14 Hertz) appelées '''fuseaux du sommeil''' qui proviennent de la boucle thalamico-corticale et des ondes de haute amplitude qui apparaissent en réaction à des bruits extérieurs ('''complexes K''').
[[File:Stage2sleep.svg|centre|vignette|upright=2.0|Sommeil léger.]]
Lors du '''sommeil profond''', les ondes Delta s'installent. Les muscles ne sont pas paralysés et le dormeur peut encore bouger. C'est d'ailleurs lors de cette phase que les somnambules sortent de leur lit ou que les terreurs nocturnes surviennent. De manière générale, une bonne partie de ce que les médecins appellent parasomnies survient dans ce stade du sommeil. Si on réveille quelqu'un en sommeil profond, celui-ci se sentira fatigué et aura des performances fortement dégradées. La durée de cette baisse de performance est d'environ une demi-heure, une heure maximum. Les rêves sont courants en sommeil profond, comparé au sommeil léger. Ces rêves sont souvent assez logiques, cohérents, bien organisés et assez peu chargés émotionnellement.
Ce sommeil profond aurait comme rôle principal de réparer le corps et de lui permettre de reposer. À l'appui de cette affirmation, on peut dire que l'hormone de croissance est surtout produite lors du sommeil profond : cette hormone de croissance sert non seulement de faire grandir les enfants, mais aide aussi les cellules et tissus à se réparer. La consommation énergétique du cerveau est aussi beaucoup diminuée lors du sommeil profond, comparé à l'éveil. De plus, une nuit reposante est associée à des ondes lentes de grande amplitude lors du sommeil profond. Ce stade du sommeil aurait aussi un rôle à jouer dans l'apprentissage et la mémorisation. Plus précisément, il aurait une influence positive sur la consolidation des souvenirs et des connaissances acquises dans la journée. Dans le détail, les ondes lentes du sommeil profond permettraient le transfert des informations apprises de l'hippocampe vers le néocortex.
===Sommeil paradoxal===
Immédiatement après les 80 minutes de sommeil à onde lentes, le '''sommeil paradoxal''' s'installe. Si on l'appelle sommeil paradoxal, c'est parce que l'EEG est très proche de l'éveil, avec quelques différences. L'hippocampe est dominé par des ondes thêta, ce qui n'est pas le cas lors de la veille. De plus, les régions motrices du cerveau sont inactives, le dormeur étant complètement paralysé. Seuls les yeux du dormeur peuvent encore bouger, ils effectuent beaucoup de mouvements rapides sous l'effet d'ondes cérébrales produites par la moelle épinière : les ondes ponto-geniculo-occipitales. Le rythme cardiaque et la respiration augmentent, de même que la pression artérielle. Les rêves sont courants lors du sommeil paradoxal, nettement plus que dans les autres phases du sommeil. De plus, les rêves du sommeil paradoxal ont tendance à être assez illogiques, incohérents, riches en émotions. Leur contenu est souvent incompréhensible, vague, bizarre. Enfin, le pénis entre en érection, ce qui est très utile pour savoir si un cas d'impuissance a une origine physique ou psychique.
==L'anatomie du sommeil et de l'éveil==
On l'a vu, la chimie du sommeil donne quelques indications sur les processus à l’œuvre dans le cerveau lors de l'endormissement ou de l'éveil. Mais les molécules précitées n'ont pas un effet global sur le cerveau : leur effet semble localisé à un petit ensemble de structures cérébrales bien précises, qui seraient autant de zones cérébrales dédiées à l'endormissement ou aux différents stades du sommeil. Certaines zones du cerveau ne sont actives qu'à l'état d'éveil, d'autres ne l'étant que lors du sommeil paradoxal ou du sommeil lent. L'identification de ces zones a permis de déterminer l'existence de trois systèmes : un qui maintient le sujet à l'état d'éveil, un autre actif lors du sommeil lent et un autre actif uniquement lors du sommeil paradoxal. Ces systèmes sont tous composés d'un ensemble d'aires cérébrales distinctes, qui ont chacun leur rôle à jouer. Ces systèmes interagissent entre eux, notamment pour s'inhiber mutuellement. Par exemple, le système d'éveil inhibe les systèmes du sommeil lent et paradoxal. Ces deux systèmes s'inhibent eux-mêmes mutuellement.
[[File:Centres de l'éveil et du sommeil.png|centre|vignette|upright=2.5|Centres de l'éveil et du sommeil]]
===L'éveil===
On a vu que l'éveil se traduit par une activité électrique soutenue dans le cortex, riche en ondes cérébrales rapides et désynchronisée. Cette activité s'atténue et devient de plus en plus rythmique et lente lors du sommeil. Le centre de l'éveil a pour but de maintenir cette activité dans le cortex. Le centre de l'éveil a longtemps été pensé comme une structure unique. Mais on sait aujourd'hui que celui-ci regroupe des structures cérébrales distantes, qui ne sont pas déterminantes à elles seules. La lésion de ces structures peut entraîner de sérieuses pertes de vigilance, qui peuvent aller jusqu’au coma profond. Ces trois aires suivantes sont souvent regroupées dans ce qu'on appelle le '''réseau exécutif de l’éveil'''.
{|
|[[File:Système d'éveil - cerveau.png|vignette|upright=2.5|Système d'éveil - cerveau]]
|[[File:NeuralCorrelatesOfConsciousness2.jpg|vignette|upright=2.0|Réseau exécutif de l’éveil et formation réticulée.]]
|}
====Le réseau exécutif de l'éveil====
Les premières aires cérébrales identifiées comme faisant partie du centre de l'éveil le furent en 1918, lors de l'épidémie de grippe espagnole. Durant cette période, le neurologue Constantin von Economo remarqua que certains de ses patients tombaient dans le coma suite à diverses séquelles neurologiques, tandis que d’autres étaient atteints de sévères insomnies, allant jusqu’à une agrypnie quasi-totale. Il se mit en tête d'étudier quelles étaient les aires cérébrales lésées pouvant expliquer ce résultat. Les patients léthargiques ou comateux avaient des lésions à l''''hypothalamus postérieur'''. Celui-ci envoie des axones dans l'ensemble du cortex, innervant celui-ci de façon diffuse. Plus précisément, on peut distinguer une contribution distincte du noyau inféro-mammilaire et du noyau latéral. Les neurones du premier sont des neurones à orexine, tandis que les neurones du premier relâchent de l'histamine dans les structures cérébrales voisines, ce qui est cohérent avec l'implication de ce neurotransmetteur dans l'état d'éveil. Il est apparu que son action est en partie une action contre-inhibitrice. En effet, l'hypothalamus antérieur se connecte à des neurones GABAergiques, à l'action inhibitrice. Les neurones de l'hypothalamus inhiberaient ces neurones GABAergiques dispersés dans le cortex : cette inhibition de l'inhibition GABAergiques se traduit naturellement par une activation du cortex.
On peut aussi citer les noyaux du '''télencéphale basal''', qui émettent de l'acétylcholine et du glutamate, le principal d'entre eux étant le noyau basal de Meynert. Leur action est directement excitatrice sur le cortex.
[[File:Thalamus, boucles thalamico-corticales et sommeil.png|vignette|Thalamus, boucles thalamico-corticales et sommeil.]]
Il est rapidement apparu que le '''thalamus''' a aussi un rôle à jouer dans l'état de vigilance et dans le cycle veille-sommeil. En effet, le thalamus régule l'activité cérébrale dans le cortex. Lors de la veille, ces boucles sont le lieu d'une activité cérébrale désynchronisée. Mais lors du sommeil lent, une activité rythmique prend place et il est aujourd'hui évident que ce comportement est dirigé par des neurones réticulaires, eux-mêmes commandés par des formations du tronc cérébral. Pour détailler le rôle du thalamus lors du sommeil, nous allons nous référer au schéma ci-contre. Rappelons que les noyaux du thalamus sont presque tous des relais sensoriels, qui transmettent les informations sensorielles au cortex cérébral. Ces noyaux de relai sensoriels sont chapeautés par un noyau thalamique réticulaire, qui inhibe les noyaux relai. D'autres noyaux sont des relais corticaux, à savoir qu'ils reçoivent des axones du cortex et lui en renvoient. ces noyaux forment avec le cortex des circuits fermés, où le cortex se connecte au thalamus, qui lui-même innerve le cortex. Ces circuits fermés sont appelés des boucles thalamico-corticales. Lors de l'éveil, les noyaux de relai sensoriels sont actifs et transmettent les sensations au cortex. Mais lors du sommeil, le noyau réticulaire s'active et éteint les noyaux de relai sensoriel. Le cerveau est alors isolé du monde extérieur et le cortex fonctionne en circuit fermé. Seules les boucles thalamico-corticales fonctionnent et leur activité est gouvernée par le noyau réticulaire. Or, le noyau réticulaire thalamique a une activité rythmique, semblable à celle des ondes lentes du sommeil lent.
====La formation réticulée : le système activateur ascendant====
Mais le réseau exécutif de l'éveil n'est pas le seul à avoir une influence sur l'état de veille. Les observations et les autopsies sur des patients comateux montrent que tous ont des lésions de la formation réticulée, suggérant que celle-ci est une des aires de commande du thalamus. On peut voir le réseau exécutif de l’éveil postérieur comme un relai de la formation réticulée. Durant longtemps, les savants crurent que cette structure correspondait au centre de l'éveil, d'où le nom qui lui a été donné de '''système réticulé activateur ascendant'''. Ce système est innervé par la plupart des afférences sensorielles, mais aussi par les structures corticales. On voyait autrefois ce système comme une sorte de centre qui recevait les sensations et les intégrait avant d'activer le cerveau en conséquence. Plus le cerveau était soumis à des stimulations sensorielles fortes (douleur, vision lumineuse, autre), plus l'activation cérébrale et l'éveil étaient forts. L'éveil était alors vu comme la conséquence d'une stimulation sensorielle suffisamment forte, tandis que le sommeil se manifestait quand les stimulations sensorielles devenaient insuffisantes. On sait aujourd'hui que cette vision est au moins partielle, trop simplifiée pour être exacte.
Le système réticulaire ascendant comprend un grand nombre de noyaux appartenant à la formation réticulée, les plus connus étant le locus coeruleus et les noyaux du raphé. À ceux-ci, il faut ajouter des noyaux cholinergiques, qui activent les neurones thalamiques. On peut aussi citer les noyaux réticulés bulbaires magnocellulaires, qui émettent de l'acétylcholine et du glutamate, deux neurotransmetteurs excitateurs. Ces derniers innervent aussi bien le thalamus que l'hypothalamus postérieur. Et enfin, citons les noyaux du noyau parabrachial, qui innervent directement le télencéphale basal. Le système réticulaire ascendant active l'ensemble du néocortex par l'intermédiaire de deux voies : une voie réticulo-thalamico-corticale qui passe par le thalamus, et une voie reticulo-hypothalamico-corticale qui fait relai dans l'hypothalamus.
===Le sommeil lent===
Lors de l'endormissement, les boucles thalamico-corticales deviennent insensibles aux stimulations extérieures. Leur activité électrique se résume à une oscillation rythmique, périodique, spontanée. Cela se produit quand le système de veille se désactive, cesse d'inhiber les neurones pacemaker du thalamus. De plus, l'activité électrique globale du cortex doit diminuer, ce qui implique une inhibition du locus coeruleus, du noyau du raphé, et des autres composants du système de veille.
Le système responsable de cette inhibition est localisé dans l'hypothalamus antérieur, et plus précisément dans l''''aire pré-optique'''. La preuve principale étant que toute lésion de cette aire entraîne une insomnie presque totale. Cette aire est sensible à l'adénosine et on peut la voir comme une sorte de capteur qui mesurerait la pression de sommeil, afin de déclencher le sommeil quand celle-ci atteint un niveau trop élevé. Cette aire pré-optique contient des neurones inhibiteurs GABAergiques, qui innerveraient l'ensemble du système d'éveil. Ainsi, quand la pression de sommeil est trop forte, ces neurones émettent suffisamment de GABA pour éteindre le système de veille. Cependant, l'aire pré-optique est inhibé par le réseau de l'éveil, ces deux systèmes s'inhibant mutuellement. L'aire pré-optique est notamment sensible à la sérotonine, l'adrénaline et l'acétylcholine, qui l'inhibent. Seuls les neurones à hypocrétine/orexine de l'hypothalamus n'influencent pas l'aire pré-optique. Cela suggère que ceux-ci stabiliseraient l'état d'éveil. D'ailleurs, la narcolepsie est en grande partie causée par une destruction de ces neurones à orexine.
Cependant, l'aire pré-optique n'est pas le seul mécanisme d'entrée en sommeil lent. Une destruction totale de l'aire pré-optique entraîne certes une diminution du temps passé en sommeil lent, ainsi que des difficultés d'endormissement, mais elle ne supprime pas le sommeil lent. D'autres systèmes du sommeil lent doivent donc exister. À l'heure actuelle, les chercheurs suspectent l'implication des neurones GABAergiques de la zone parafaciale du tronc cérébral. L'activation de ces neurones semble induire un état proche, si ce n'est identique, du sommeil lent. Ils agiraient par inhibition de la transmission glutamatinergique du noyau parabrachial.
===Le sommeil paradoxal===
Le sommeil paradoxal est commandé par un ensemble de noyaux spécialisés qui interagissent entre eux. Certains de ces noyaux, ainsi que les aires cérébrales du cortex, sont aussi actifs lors de l'éveil. C'est notamment le cas des noyaux cholinergiques, qui s'activent lors du sommeil paradoxal. La chimie du sommeil paradoxal est donc un intermédiaire entre le sommeil lent et l'éveil. La production d'acétylcholine est proche de celle de l'éveil, tandis que la production de monoamines et d'orexine est nulle.
Mais d'autres noyaux sont spécifiquement actifs lors du sommeil paradoxal. Leurs neurones sont appelés des '''neurones SP-on'''. Ceux-ci sont responsables de ce qui se passe lors du sommeil paradoxal. Ces noyaux SP-on contiennent les neurones glutaminergique du noyau sublatérodorsal, la partie ventrolatérale de la substance grise périacqueducale, le noyau reticularis pontis oralis (un noyau sensible à l'acétylcholine) et enfin le noyau paragigantocellulaire latéral. L'inhibition motrice du sommeil paradoxal est causée par l'activation du noyau sublatérodorsal. Ainsi, des lésions de ce noyau suppriment l'atonie musculaire lors du sommeil paradoxal. Ceux-ci émettent des afférences glutaminergiques vers des noyaux GABAergiques/glycinergique du myélencéphalequi innervent la moelle épinière. Les ondes géniculopontiques sont générés dans le tegmentum du mésencéphale.
Certaines structures du cerveau, n'appartenant pas à ce système, sont totalement désactivées lors du sommeil paradoxal. C'est notamment le cas du locus coeruleus et du noyau du raphé. Les neurones de ces aires sont donc appelés des '''neurones SP-off''', pour bien signaler qu'il s"éteignent lors du sommeil paradoxal. De manière générale, le cerveau ne sécrète presque pas de monoamine lors du sommeil paradoxal. L'activation des systèmes mono-aminergiques pendant le sommeil paradoxal interrompt d'ailleurs celui-ci. C'est ainsi que les anti-dépresseurs, qui favorisent la production de mono-amines, peuvent diminuer ou complètement abolir le sommeil paradoxal. Les inhibiteurs sélectifs de la sérotonine vont ainsi diminuer la durée de sommeil paradoxal, tandis que les tricycliques et les inhibiteurs de la monoamine oxydase vont parfois totalement l'abolir.
==Les troubles du sommeil==
Le sommeil peut facilement être troublé, que ce soit par des maladies, de l'anxiété, des médicaments, ou bien d'autres raisons. Le chapitre précédent a notamment abordé les troubles du sommeil liés aux troubles du rythme circadien. Mais ce sont loin d'être les seuls, comme tout insomniaque le sait déjà ! Mais leur classification est clairement assez difficile, plusieurs classifications existant à l'heure actuelle. De nos jours, les trois classifications qui font référence sont celles du DSM (le manuel de diagnostic des troubles mentaux), l'ICD (International Classification of Disease de l'OMS) et celle de l'ICSD (International Classification of Sleep Disorder). Mais ces trois classifications sont assez similaires, ce qui fait que les maladies du sommeil sont assez bien identifiées. Dans les grandes lignes, les troubles du sommeil se classent en quelques grands syndromes : les troubles du rythme circadien, l'insomnie, les hypersomnies, les parasomnies, les mouvements anormaux liés au sommeil et les apnées du sommeil. Il arrive aussi certaines maladies modifient la durée du sommeil, certaines pouvant faire disparaître totalement le sommeil : on parle alors d'agrypnie.
===L'insomnie===
L''''insomnie''' est juste une réduction de la quantité de sommeil, là où les hypersomnies correspondent à l'inverse, à savoir un excès de sommeil. Le syndrome le plus courant est clairement l'insomnie, qui frappe bien plus de monde que l'hypersomnie. La plupart des insomnies sont temporaires, liées à de l'anxiété ou une origine psychologique bénigne quelconque. Ces insomnies passent le plus souvent en quelques semaines ou quelques jours au mieux. Plus rares sont les insomnies organiques, dont la cause est totalement autre que psychologique. Elles peuvent provenir de la consommation d’existants ou de médicaments aux effets stimulants, mais aussi de troubles neurologiques ou psychiatriques un peu plus sérieux. Par exemple, les patients dépressifs ont souvent des insomnies (ou des hypersomnies, selon le cas) : ils ont notamment tendance à se réveiller plus tôt le matin et à se lever plus fréquemment la nuit. On observe aussi des insomnies assez sévères chez les bipolaires en phase maniaque : leurs besoins en sommeil diminuent aux alentours de 2 ou 3 heures de sommeil.
===Les hypersomnies===
Les '''hypersomnies''' sont plus rares, quoiqu'elles soient le second trouble du sommeil en termes de fréquence. La plupart ont des origines assez classiques : une réaction face un manque de sommeil, le résultat de problèmes psychologiques, l'arrivée de l'hiver, etc. Rarement, l'hypersomnie est liée à des maladies psychiatriques. Par exemple, la dépression peut entraîner des hypersomnies assez importantes chez certains patients. L'abus d'hypnotiques est aussi une raison assez évidente. Mais certaines maladies peuvent entraîner des hypersomnies assez sévères, assez idiosyncratiques. Ces maladies sont connues sous le nom de narcolepsie et de syndrome de Klein-Levin.
La '''narcolepsie''' est une maladie caractérisée par des endormissements soudains, en pleine journée. Ceux-ci sont caractéristiques de la narcolepsie, mais elle peut être couplée à des symptômes assez accessoires, comme une fatigue diurne et des insomnies. Les hallucinations lors de l'endormissement ou du réveil sont aussi assez fréquentes, bien que parfois absentes chez certaines narcoleptiques. Beaucoup de narcoleptiques sont aussi atteints de catalepsie, un syndrome caractérisé par une décontraction totale des muscles sans perte de conscience. Un patient cataleptique peut ainsi s'effondrer ou chuter soudainement, suite à une décontraction de tous ses muscles, bien que la catalepsie puisse se limiter à quelques muscles bien précis. Cela arrive suite à une émotion, ou spontanément. Chose étonnante, les endormissements soudains ne font pas passer le malade en sommeil lent, mais le font passer directement en sommeil paradoxal. La cause la plus probable de la narcolepsie serait un dysfonctionnement impliquant l'orexine, soit que les neurones à orexine dysfonctionnent, soit qu'une déficience en orexine se fasse jour dans le cerveau.
Certaines hypersomnies sont dites récurrentes, à savoir qu'elles surviennent "régulièrement". Le syndrome de ce type le plus connu est le '''syndrome de Klein-Levin''', bien qu'il soit particulièrement rare. Ce syndrome se manifeste par poussées d'hypersomnies, avec rémission complète entre les poussées. Lors des poussées, le patient dort beaucoup plus que la normale, mais ce n'est pas le seul symptôme décelable, des symptômes cognitifs ou psychiatriques sont aussi fréquents. Niveau psychiatrique, le sujet est irritable, fatigué, apathique, parfois dépressif. Plus grave, des manifestations psychiatriques plus importantes peuvent se produire : hallucinations, délire, hypersexualité, confusion mentale, délirium, variations d'humeur, etc. Des troubles cognitifs sont très fréquents : difficultés de concentration, troubles de la mémoire, et autres.
===Les troubles du comportement en sommeil paradoxal===
Les troubles du sommeil précédent dégradaient la durée du sommeil, mais quelques troubles ne rentrent pas dans ce cadre. Les '''troubles du comportement en sommeil paradoxal''', que nous abrégerons en TCSP, sont de ceux-là. Il s'agit de rêves agités ou cauchemardesques où le sujet se met à bouger, parfois au point d'avoir un comportement violent ou impulsif. Ce trouble ressemble quelque peu au somnambulisme, si ce n'est pour une différence de taille : si le somnambulisme est un trouble du sommeil lent, les phases de pseudo-réveil du TCSP ont lieu assez souvent 90 minutes après l'endormissement, lors du sommeil paradoxal. Ce trouble a une origine assez intéressante : il se traduit par une abolition de la paralysie lors du sommeil paradoxal. Des muscles qui devraient perdre tout leur tonus et ne pas se contracter lors du sommeil paradoxal vont en fait fonctionner comme lors de l'éveil.
Le TCSP peut avoir plusieurs causes, comme l'usage d'antidépresseurs. Ceux-ci ont tendance à réduire le sommeil paradoxal et à dégrader celui-ci. Il n'est donc pas étonnant que ces médicaments déclenchent de tels troubles du sommeil paradoxal. Mais la plupart du temps, ce trouble n'a pas de cause bien définie. Le TCSP est plus fréquent chez les sujets âgés, notamment chez ceux atteints de maladies neurodégénératives. Chose importante, plus de 30% des patients âgés atteints de TCSP déclarent des maladies neurodégénératives dans les années ou décennies qui suivent l'apparition du TCSP. Plus précisément, ils déclarent des synucléopathies, tel la maladie de Parkinson, la paralysie supra-nucléaire progressive, ou la démence à corps de Lewy. Le TCSP est inhabituel dans les tauopathies et amyloses.
<noinclude>
{{NavChapitre | book=Neurosciences
| prev=Les rythmes circadiens
| prevText=Les rythmes circadiens
| next=L'état de conscience
| nextText=L'état de conscience
}}{{autoCat}}
</noinclude>
iw44gzvr16qtgpopp73az0r0in2yboc
775326
775325
2026-10-16T17:22:28Z
Mewtow
31375
/* Les stades du sommeil */
775326
wikitext
text/x-wiki
On pourrait croire que le chapitre sur les rythmes circadiens vous a enseigné tout ce qu'il y a à savoir sur le sommeil, mais ce n'est pas le cas. S'il est vrai que le sommeil a une régularité similaire à celle du rythme circadien, l'horloge circadienne n'est pas la seule responsable de l'envie de dormir.
Les chercheurs ont remarqué que l'envie de sommeil n'avait parfois aucun rapport avec le rythme de cette horloge. Par exemple, le manque de sommeil augmente l'envie de dormir alors qu'il n'a pas d'effet sur l’horloge biologique. On peut l'observer en regardant non pas le sommeil, mais les autres paramètres qui évoluent avec une période de 24 heures (la température, par exemple). Ainsi, il est possible de désynchroniser l'envie de dormir du rythme circadien, suite à des privations de sommeil : le sujet aura envie de dormir à des horaires sans lien avec son rythme circadien.
Il y a donc quelque chose qui est capable d'induire le sommeil, sans que cela soit lié aux rythmes biologiques. Cette autre chose, les scientifiques lui ont donné les noms d'« homéostasie du sommeil » ou encore de « pression de sommeil ». Ce terme ne fait que formuler une évidence : plus on passe de temps éveillé, plus la fatigue se fait sentir et plus on a envie de dormir. Comme quoi, il fallait bien un chapitre complet sur le sommeil !
==La durée du sommeil==
Présent chez de nombreuses espèces animales, le sommeil est certainement apparu pour permettre aux animaux de conserver leur énergie. De nombreux animaux peu évolués peuvent tomber dans un état de torpeur, où ils restent immobiles durant plus ou moins longtemps, tandis que d'autres sont capables de dormir profondément. La durée de sommeil varie beaucoup chez les animaux, allant d'une absence quasi-totale à plus de 10 heures.
En moyenne, on estime qu'un être humain normal a besoin d'environ 8 à 9 heures de sommeil pour se sentir pleinement reposé. Cependant, cette moyenne cache une forte variabilité, vraisemblablement pour des raisons génétiques : les gros dormeurs doivent dormir 8 à 10 heures par nuit pour se sentir en forme, alors que les petits dormeurs se contentent de 3 à 4 heures.
De plus, l'âge rentre aussi en compte dans les besoins de sommeil, la durée idéale de sommeil diminuant progressivement jusqu’à la fin de l'adolescence. Un nouveau-né a tendance à dormir plus de 18 heures par jour, un enfant de 3 ans 11 à 13 heures, les adolescents ont besoin de 9 à 10 heures par jour et les adultes entre 7 et 8 heures. Contrairement à l'idée reçue, les personnes âgées dorment autant que les jeunes adultes, mais ont un sommeil plus fragmenté : elles dorment moins la nuit et se rattrapent le jour en faisant la sieste.
===La privation de sommeil===
Quand on manque de sommeil, tout se passe comme si le sujet en manque de sommeil avait une dette de sommeil à rattraper. Cela se remarque surtout par une diminution des performances du sujet : ses réactions sont plus lentes, il fait plus d'erreurs, sa concentration diminue, ses performances intellectuelles déclinent, etc.
Quelqu'un qui a mal dormit durant la nuit aura tendance à plus dormir que le lendemain pour rattraper sa dette de sommeil. Cependant, une nuit blanche est souvent rattrapée par quelques heures de sommeil supplémentaires. Plus que la durée du sommeil, c'est surtout la '''qualité du sommeil''', l'intensité du sommeil, qui permet de récupérer. Cette intensité dépend de la dette de sommeil accumulée, le sommeil étant plus réparateur après une restriction de sommeil. Et cette intensité du sommeil a une trace dans le cerveau, comme nous allons le voir dans la section suivante.
===Le ''Familial natural short sleep''===
Il existe des personnes qui ont besoin de très peu de sommeil, à peine 4 à 6 heures, pour se sentir reposées. Les personnes touchées n'expriment pas de perte de performances malgré cette courte durée de sommeil. Il s'agit d'une particularité génétique appelée ''Familial natural short sleep'', qui est très peu fréquente dans la population. Le ''Familial natural short sleep'' est un trait présent dès la naissance et dure toute la vie. De plus, il est associé à d'autres particularité : meilleure mémoire, sensibilité à la douleur diminuée, personnalité plus extravertie et optimiste.
N'allez pas croire que vous l'avez, beaucoup de monde pense avoir besoin de peu de sommeil et n'en ressent pas les conséquences cognitives, alors qu'ils sont en réalité en privation de sommeil et ont des performances dégradées. En réalité, cette particularité génétique est très rare. Les chercheurs n'ont identifié avec certitude qu'une cinquantaine de familles dans le monde avec cette particularité, et ces familles sont celles sur lesquelles la recherche génétique s'est concentrée. Mais quelques estimations disent que le chiffre réel est proche des 1% de la population. Les gènes concernés ne sont pas connus avec certitude, quelques candidats ont été rejetés (DEC2, ADRB1, autres). Il se pourrait que des gènes liés à l'orexine soient impliqués, mais sans certitude.
==Les stades du sommeil==
Le cerveau est très loin de rester inactif lors du sommeil. Même durant le sommeil, le cerveau a une activité électrique relativement soutenue. Pour rappel, nous avions vu dans le chapitre sur l'activité électrique du cerveau que l'activité électrique cérébrale se mesure avec un instrument composé de plusieurs électrodes placées à la surface du crâne : l''''électroencéphalographe''', ou EEG.
L'EEG permet de mesurer des rythmes électriques récurrents, appelés des ''rythmes cérébraux'', ou encore ''ondes cérébrales''. Ils sont classés arbitrairement dans 4 à 6 bandes de fréquences nommées alpha, bêta, gamma, delta, et thêta.
* Delta : Inférieur à 4 hertz
* Thêta : 4 à 7 hertz
* Alpha : 8 à 15 hertz
* Bêta : 16 à 31 hertz
* Gamma : Plus de 32 hertz
Les scientifiques ont remarqué que chaque bande de fréquence correspond à des niveaux d'éveil et de sommeil différents. L'activité électrique du cerveau varie entre le sommeil et l'éveil, et on peut observer des variations par rapport à la normale dans certains cas pathologiques. Si on observe l'EEG de quelqu'un qui dort, on remarque que le sommeil est organisé en '''phases de sommeil''', identifiables par l'apparition de certaines ondes cérébrales.
Lors de l'endormissement, les ondes alpha apparaissent par bouffées successives. Le sujet peut être réveillé facilement. Lors de cette phase, le sujet peut être pris d'hallucinations dites hypnagogiques, ainsi que de sursauts musculaires nommées myoclonies d’endormissement. Dans la majorité des cas, cela est tout à fait normal et n'a rien de pathologique (l'auteur en a déjà fait expérience, sans avoir de problème de santé particulier).
Une fois le sujet endormi, son sommeil est organisé en cycles, qui se répètent plusieurs fois durant une nuit. Un cycle fait environ 90 minutes, mais avec de fortes variation individuelles. Concrètement, un cycle va de 30 minutes à 3 heures, mais la moyenne est proche de 1h30. Un cycle contient une phase de ''sommeil lent'' et de ''sommeil paradoxal''. Le '''sommeil à ondes lentes''' est une phase où les ondes cérébrales sont synchronisées, de basse fréquence et de forte amplitude. Tous les neurones émettent des potentiels d'action en même temps. Le '''sommeil paradoxal''' a une activité électrique proche de l'éveil : ondes de fréquence élevée, de faible amplitude, totalement désynchronisées.
[[File:Hypnogramme.svg|vignette|upright=2.0|Hypnogramme]]
{| class="wikitable"
|+ Stades du sommeil
|-
! scope="col"| Stade du sommeil|| Ondes cérébrales prédominantes
|-
| Eveil || Ondes Gamma, Bêta
|-
| Endormissement || Ondes Alpha
|-
| Sommeil lent léger || Ondes Thêta
|-
| Sommeil lent profond || Ondes Delta
|-
| Sommeil paradoxal|| Ondes Gamma, Bêta
|}
===Le sommeil à ondes lentes===
Lors du sommeil à ondes lentes, on observe une diminution de la fréquence des ondes cérébrales et une augmentation de leur amplitude. Le dormeur devient de plus en plus difficile à réveiller. Si on réveille quelqu'un lors du sommeil à ondes lentes, il se sentira fatigué. Le dormeur peut "rêver" lors du sommeil lent, mais les rêves en question sont souvent sans composants visuelle, ils sont relativement logiques et peu chargés émotionnellement. Disons que la plupart du temps, le sujet "pense" plus qu'il rêve.
Le sommeil à ondes lentes est lui aussi subdivisé en deux phases : le sommeil léger et le sommeil profond. La distinction n'est pas très claire, il y a parfois plusieurs subdivisions supplémentaires, mais les distinctions ne sont pas très claires, surtout basées sur des critères liés à la fréquence des ondes cérébrales.
Le '''sommeil léger''' dure environ une demi-heure chez un sujet d'âge moyen, mais peut être plus long chez les personnes âgées. Dans ce stade, le tonus musculaire diminue progressivement, les mouvements oculaires se font plus lents et de moindre intensité, la respiration et le rythme cardiaques deviennent plus réguliers et ralentissent un tout petit peu.
Le dormeur peut encore être réveillé facilement dans ce stade du sommeil. Du bruit, ou de la lumière peuvent réveiller un dormeur dans ce stade. D'où le fait qu'il soit appelé le sommeil ''léger''. De plus, le dormeur ne se sentira pas trop fatigué après le réveil, même si il aura une performance légèrement amoindrie. Quelques micro-réveils, inférieures à la dizaine de secondes, peuvent se produire spontanément dans ce stade. Le dormeur peut se souvenir de ces phases de réveil, qui sont parfois sources d'hallucinations hypnagogiques.
Sur l'EEG, on peut observer des bouffées d'ondes de haute fréquence (12-14 Hertz) appelées '''fuseaux du sommeil''' qui proviennent de la boucle thalamico-corticale et des ondes de haute amplitude qui apparaissent en réaction à des bruits extérieurs ('''complexes K''').
[[File:Stage2sleep.svg|centre|vignette|upright=2.0|Sommeil léger.]]
Lors du '''sommeil profond''', les ondes Delta s'installent. Le dormeur peut encore bouger. C'est d'ailleurs lors de cette phase que les somnambules sortent de leur lit ou que les terreurs nocturnes surviennent. De manière générale, une bonne partie de ce que les médecins appellent parasomnies survient dans ce stade du sommeil. Au-delà des parasomnies, le sommeil profond est lui aussi bien nommé. Si on réveille quelqu'un en sommeil profond, celui-ci se sentira fatigué et aura des performances fortement dégradées. La durée de cette baisse de performance est d'environ une demi-heure, une heure maximum.
Le sommeil profond aurait comme rôle principal de réparer le corps et de lui permettre de reposer. À l'appui de cette affirmation, on peut dire que l'hormone de croissance est surtout produite lors du sommeil profond. Cette hormone fait grandir les enfants, mais aide aussi les cellules et tissus à se réparer. La consommation énergétique du cerveau est aussi beaucoup diminuée lors du sommeil profond, comparé à l'éveil. Une nuit reposante est associée à des ondes lentes de grande amplitude lors du sommeil profond.
Ce stade du sommeil aurait aussi un rôle à jouer dans l'apprentissage et la mémorisation. Plus précisément, il aurait une influence positive sur la consolidation des souvenirs et des connaissances acquises dans la journée. Dans le détail, les ondes lentes du sommeil profond permettraient le transfert des informations apprises de l'hippocampe vers le néocortex.
===Le sommeil paradoxal (''Rapid Eye Movement Sleep'')===
Après le sommeil à onde lentes, le '''sommeil paradoxal''' s'installe. Si on l'appelle sommeil paradoxal, c'est parce que l'EEG est très proche de l'éveil. Les rêves sont courants lors du sommeil paradoxal, nettement plus que dans les autres phases du sommeil. De plus, les rêves du sommeil paradoxal ont tendance à être assez illogiques, incohérents, riches en émotions. Leur contenu est souvent incompréhensible, vague, bizarre.
De plus, les régions motrices du cerveau sont inactives, mais le dormeur est complètement paralysé. Seuls les yeux du dormeur peuvent encore bouger, ils effectuent beaucoup de mouvements rapides sous l'effet d'ondes cérébrales produites par la moelle épinière : les ondes ponto-geniculo-occipitales. Le rythme cardiaque et la respiration augmentent, de même que la pression artérielle. Enfin, le pénis entre en érection, ce qui est très utile pour savoir si un cas d'impuissance a une origine physique ou psychique.
Il faut noter que la paralysie observée lors du sommeil paradoxal est présente chez les mammifères, mais pas chez les oiseaux (règle générale avec de nombreuses exceptions). Ils peuvent dormir debout, avec la tête relevée, sans que le sommeil paradoxal ne leur coupe les jambes. Ils donnent ainsi l'impression d'être alertes alors qu'ils dorment, ce qui peut tromper un prédateur. Leur sommeil paradoxal est de plus nettement plus court que chez les mammifères : quelques dizaines de secondes chez certaines espèces, contre plusieurs dizaines de minutes chez les mammifères ! Et ils sont aussi beaucoup plus fréquents. En somme, ils ont des cycles de sommeils plus irréguliers, plus fréquents, plus courts.
==L'anatomie du sommeil et de l'éveil==
On l'a vu, la chimie du sommeil donne quelques indications sur les processus à l’œuvre dans le cerveau lors de l'endormissement ou de l'éveil. Mais les molécules précitées n'ont pas un effet global sur le cerveau : leur effet semble localisé à un petit ensemble de structures cérébrales bien précises, qui seraient autant de zones cérébrales dédiées à l'endormissement ou aux différents stades du sommeil. Certaines zones du cerveau ne sont actives qu'à l'état d'éveil, d'autres ne l'étant que lors du sommeil paradoxal ou du sommeil lent. L'identification de ces zones a permis de déterminer l'existence de trois systèmes : un qui maintient le sujet à l'état d'éveil, un autre actif lors du sommeil lent et un autre actif uniquement lors du sommeil paradoxal. Ces systèmes sont tous composés d'un ensemble d'aires cérébrales distinctes, qui ont chacun leur rôle à jouer. Ces systèmes interagissent entre eux, notamment pour s'inhiber mutuellement. Par exemple, le système d'éveil inhibe les systèmes du sommeil lent et paradoxal. Ces deux systèmes s'inhibent eux-mêmes mutuellement.
[[File:Centres de l'éveil et du sommeil.png|centre|vignette|upright=2.5|Centres de l'éveil et du sommeil]]
===L'éveil===
On a vu que l'éveil se traduit par une activité électrique soutenue dans le cortex, riche en ondes cérébrales rapides et désynchronisée. Cette activité s'atténue et devient de plus en plus rythmique et lente lors du sommeil. Le centre de l'éveil a pour but de maintenir cette activité dans le cortex. Le centre de l'éveil a longtemps été pensé comme une structure unique. Mais on sait aujourd'hui que celui-ci regroupe des structures cérébrales distantes, qui ne sont pas déterminantes à elles seules. La lésion de ces structures peut entraîner de sérieuses pertes de vigilance, qui peuvent aller jusqu’au coma profond. Ces trois aires suivantes sont souvent regroupées dans ce qu'on appelle le '''réseau exécutif de l’éveil'''.
{|
|[[File:Système d'éveil - cerveau.png|vignette|upright=2.5|Système d'éveil - cerveau]]
|[[File:NeuralCorrelatesOfConsciousness2.jpg|vignette|upright=2.0|Réseau exécutif de l’éveil et formation réticulée.]]
|}
====Le réseau exécutif de l'éveil====
Les premières aires cérébrales identifiées comme faisant partie du centre de l'éveil le furent en 1918, lors de l'épidémie de grippe espagnole. Durant cette période, le neurologue Constantin von Economo remarqua que certains de ses patients tombaient dans le coma suite à diverses séquelles neurologiques, tandis que d’autres étaient atteints de sévères insomnies, allant jusqu’à une agrypnie quasi-totale. Il se mit en tête d'étudier quelles étaient les aires cérébrales lésées pouvant expliquer ce résultat. Les patients léthargiques ou comateux avaient des lésions à l''''hypothalamus postérieur'''. Celui-ci envoie des axones dans l'ensemble du cortex, innervant celui-ci de façon diffuse. Plus précisément, on peut distinguer une contribution distincte du noyau inféro-mammilaire et du noyau latéral. Les neurones du premier sont des neurones à orexine, tandis que les neurones du premier relâchent de l'histamine dans les structures cérébrales voisines, ce qui est cohérent avec l'implication de ce neurotransmetteur dans l'état d'éveil. Il est apparu que son action est en partie une action contre-inhibitrice. En effet, l'hypothalamus antérieur se connecte à des neurones GABAergiques, à l'action inhibitrice. Les neurones de l'hypothalamus inhiberaient ces neurones GABAergiques dispersés dans le cortex : cette inhibition de l'inhibition GABAergiques se traduit naturellement par une activation du cortex.
On peut aussi citer les noyaux du '''télencéphale basal''', qui émettent de l'acétylcholine et du glutamate, le principal d'entre eux étant le noyau basal de Meynert. Leur action est directement excitatrice sur le cortex.
[[File:Thalamus, boucles thalamico-corticales et sommeil.png|vignette|Thalamus, boucles thalamico-corticales et sommeil.]]
Il est rapidement apparu que le '''thalamus''' a aussi un rôle à jouer dans l'état de vigilance et dans le cycle veille-sommeil. En effet, le thalamus régule l'activité cérébrale dans le cortex. Lors de la veille, ces boucles sont le lieu d'une activité cérébrale désynchronisée. Mais lors du sommeil lent, une activité rythmique prend place et il est aujourd'hui évident que ce comportement est dirigé par des neurones réticulaires, eux-mêmes commandés par des formations du tronc cérébral. Pour détailler le rôle du thalamus lors du sommeil, nous allons nous référer au schéma ci-contre. Rappelons que les noyaux du thalamus sont presque tous des relais sensoriels, qui transmettent les informations sensorielles au cortex cérébral. Ces noyaux de relai sensoriels sont chapeautés par un noyau thalamique réticulaire, qui inhibe les noyaux relai. D'autres noyaux sont des relais corticaux, à savoir qu'ils reçoivent des axones du cortex et lui en renvoient. ces noyaux forment avec le cortex des circuits fermés, où le cortex se connecte au thalamus, qui lui-même innerve le cortex. Ces circuits fermés sont appelés des boucles thalamico-corticales. Lors de l'éveil, les noyaux de relai sensoriels sont actifs et transmettent les sensations au cortex. Mais lors du sommeil, le noyau réticulaire s'active et éteint les noyaux de relai sensoriel. Le cerveau est alors isolé du monde extérieur et le cortex fonctionne en circuit fermé. Seules les boucles thalamico-corticales fonctionnent et leur activité est gouvernée par le noyau réticulaire. Or, le noyau réticulaire thalamique a une activité rythmique, semblable à celle des ondes lentes du sommeil lent.
====La formation réticulée : le système activateur ascendant====
Mais le réseau exécutif de l'éveil n'est pas le seul à avoir une influence sur l'état de veille. Les observations et les autopsies sur des patients comateux montrent que tous ont des lésions de la formation réticulée, suggérant que celle-ci est une des aires de commande du thalamus. On peut voir le réseau exécutif de l’éveil postérieur comme un relai de la formation réticulée. Durant longtemps, les savants crurent que cette structure correspondait au centre de l'éveil, d'où le nom qui lui a été donné de '''système réticulé activateur ascendant'''. Ce système est innervé par la plupart des afférences sensorielles, mais aussi par les structures corticales. On voyait autrefois ce système comme une sorte de centre qui recevait les sensations et les intégrait avant d'activer le cerveau en conséquence. Plus le cerveau était soumis à des stimulations sensorielles fortes (douleur, vision lumineuse, autre), plus l'activation cérébrale et l'éveil étaient forts. L'éveil était alors vu comme la conséquence d'une stimulation sensorielle suffisamment forte, tandis que le sommeil se manifestait quand les stimulations sensorielles devenaient insuffisantes. On sait aujourd'hui que cette vision est au moins partielle, trop simplifiée pour être exacte.
Le système réticulaire ascendant comprend un grand nombre de noyaux appartenant à la formation réticulée, les plus connus étant le locus coeruleus et les noyaux du raphé. À ceux-ci, il faut ajouter des noyaux cholinergiques, qui activent les neurones thalamiques. On peut aussi citer les noyaux réticulés bulbaires magnocellulaires, qui émettent de l'acétylcholine et du glutamate, deux neurotransmetteurs excitateurs. Ces derniers innervent aussi bien le thalamus que l'hypothalamus postérieur. Et enfin, citons les noyaux du noyau parabrachial, qui innervent directement le télencéphale basal. Le système réticulaire ascendant active l'ensemble du néocortex par l'intermédiaire de deux voies : une voie réticulo-thalamico-corticale qui passe par le thalamus, et une voie reticulo-hypothalamico-corticale qui fait relai dans l'hypothalamus.
===Le sommeil lent===
Lors de l'endormissement, les boucles thalamico-corticales deviennent insensibles aux stimulations extérieures. Leur activité électrique se résume à une oscillation rythmique, périodique, spontanée. Cela se produit quand le système de veille se désactive, cesse d'inhiber les neurones pacemaker du thalamus. De plus, l'activité électrique globale du cortex doit diminuer, ce qui implique une inhibition du locus coeruleus, du noyau du raphé, et des autres composants du système de veille.
Le système responsable de cette inhibition est localisé dans l'hypothalamus antérieur, et plus précisément dans l''''aire pré-optique'''. La preuve principale étant que toute lésion de cette aire entraîne une insomnie presque totale. Cette aire est sensible à l'adénosine et on peut la voir comme une sorte de capteur qui mesurerait la pression de sommeil, afin de déclencher le sommeil quand celle-ci atteint un niveau trop élevé. Cette aire pré-optique contient des neurones inhibiteurs GABAergiques, qui innerveraient l'ensemble du système d'éveil. Ainsi, quand la pression de sommeil est trop forte, ces neurones émettent suffisamment de GABA pour éteindre le système de veille. Cependant, l'aire pré-optique est inhibé par le réseau de l'éveil, ces deux systèmes s'inhibant mutuellement. L'aire pré-optique est notamment sensible à la sérotonine, l'adrénaline et l'acétylcholine, qui l'inhibent. Seuls les neurones à hypocrétine/orexine de l'hypothalamus n'influencent pas l'aire pré-optique. Cela suggère que ceux-ci stabiliseraient l'état d'éveil. D'ailleurs, la narcolepsie est en grande partie causée par une destruction de ces neurones à orexine.
Cependant, l'aire pré-optique n'est pas le seul mécanisme d'entrée en sommeil lent. Une destruction totale de l'aire pré-optique entraîne certes une diminution du temps passé en sommeil lent, ainsi que des difficultés d'endormissement, mais elle ne supprime pas le sommeil lent. D'autres systèmes du sommeil lent doivent donc exister. À l'heure actuelle, les chercheurs suspectent l'implication des neurones GABAergiques de la zone parafaciale du tronc cérébral. L'activation de ces neurones semble induire un état proche, si ce n'est identique, du sommeil lent. Ils agiraient par inhibition de la transmission glutamatinergique du noyau parabrachial.
===Le sommeil paradoxal===
Le sommeil paradoxal est commandé par un ensemble de noyaux spécialisés qui interagissent entre eux. Certains de ces noyaux, ainsi que les aires cérébrales du cortex, sont aussi actifs lors de l'éveil. C'est notamment le cas des noyaux cholinergiques, qui s'activent lors du sommeil paradoxal. La chimie du sommeil paradoxal est donc un intermédiaire entre le sommeil lent et l'éveil. La production d'acétylcholine est proche de celle de l'éveil, tandis que la production de monoamines et d'orexine est nulle.
Mais d'autres noyaux sont spécifiquement actifs lors du sommeil paradoxal. Leurs neurones sont appelés des '''neurones SP-on'''. Ceux-ci sont responsables de ce qui se passe lors du sommeil paradoxal. Ces noyaux SP-on contiennent les neurones glutaminergique du noyau sublatérodorsal, la partie ventrolatérale de la substance grise périacqueducale, le noyau reticularis pontis oralis (un noyau sensible à l'acétylcholine) et enfin le noyau paragigantocellulaire latéral. L'inhibition motrice du sommeil paradoxal est causée par l'activation du noyau sublatérodorsal. Ainsi, des lésions de ce noyau suppriment l'atonie musculaire lors du sommeil paradoxal. Ceux-ci émettent des afférences glutaminergiques vers des noyaux GABAergiques/glycinergique du myélencéphalequi innervent la moelle épinière. Les ondes géniculopontiques sont générés dans le tegmentum du mésencéphale.
Certaines structures du cerveau, n'appartenant pas à ce système, sont totalement désactivées lors du sommeil paradoxal. C'est notamment le cas du locus coeruleus et du noyau du raphé. Les neurones de ces aires sont donc appelés des '''neurones SP-off''', pour bien signaler qu'il s"éteignent lors du sommeil paradoxal. De manière générale, le cerveau ne sécrète presque pas de monoamine lors du sommeil paradoxal. L'activation des systèmes mono-aminergiques pendant le sommeil paradoxal interrompt d'ailleurs celui-ci. C'est ainsi que les anti-dépresseurs, qui favorisent la production de mono-amines, peuvent diminuer ou complètement abolir le sommeil paradoxal. Les inhibiteurs sélectifs de la sérotonine vont ainsi diminuer la durée de sommeil paradoxal, tandis que les tricycliques et les inhibiteurs de la monoamine oxydase vont parfois totalement l'abolir.
==Les troubles du sommeil==
Le sommeil peut facilement être troublé, que ce soit par des maladies, de l'anxiété, des médicaments, ou bien d'autres raisons. Le chapitre précédent a notamment abordé les troubles du sommeil liés aux troubles du rythme circadien. Mais ce sont loin d'être les seuls, comme tout insomniaque le sait déjà ! Mais leur classification est clairement assez difficile, plusieurs classifications existant à l'heure actuelle. De nos jours, les trois classifications qui font référence sont celles du DSM (le manuel de diagnostic des troubles mentaux), l'ICD (International Classification of Disease de l'OMS) et celle de l'ICSD (International Classification of Sleep Disorder). Mais ces trois classifications sont assez similaires, ce qui fait que les maladies du sommeil sont assez bien identifiées. Dans les grandes lignes, les troubles du sommeil se classent en quelques grands syndromes : les troubles du rythme circadien, l'insomnie, les hypersomnies, les parasomnies, les mouvements anormaux liés au sommeil et les apnées du sommeil. Il arrive aussi certaines maladies modifient la durée du sommeil, certaines pouvant faire disparaître totalement le sommeil : on parle alors d'agrypnie.
===L'insomnie===
L''''insomnie''' est juste une réduction de la quantité de sommeil, là où les hypersomnies correspondent à l'inverse, à savoir un excès de sommeil. Le syndrome le plus courant est clairement l'insomnie, qui frappe bien plus de monde que l'hypersomnie. La plupart des insomnies sont temporaires, liées à de l'anxiété ou une origine psychologique bénigne quelconque. Ces insomnies passent le plus souvent en quelques semaines ou quelques jours au mieux. Plus rares sont les insomnies organiques, dont la cause est totalement autre que psychologique. Elles peuvent provenir de la consommation d’existants ou de médicaments aux effets stimulants, mais aussi de troubles neurologiques ou psychiatriques un peu plus sérieux. Par exemple, les patients dépressifs ont souvent des insomnies (ou des hypersomnies, selon le cas) : ils ont notamment tendance à se réveiller plus tôt le matin et à se lever plus fréquemment la nuit. On observe aussi des insomnies assez sévères chez les bipolaires en phase maniaque : leurs besoins en sommeil diminuent aux alentours de 2 ou 3 heures de sommeil.
===Les hypersomnies===
Les '''hypersomnies''' sont plus rares, quoiqu'elles soient le second trouble du sommeil en termes de fréquence. La plupart ont des origines assez classiques : une réaction face un manque de sommeil, le résultat de problèmes psychologiques, l'arrivée de l'hiver, etc. Rarement, l'hypersomnie est liée à des maladies psychiatriques. Par exemple, la dépression peut entraîner des hypersomnies assez importantes chez certains patients. L'abus d'hypnotiques est aussi une raison assez évidente. Mais certaines maladies peuvent entraîner des hypersomnies assez sévères, assez idiosyncratiques. Ces maladies sont connues sous le nom de narcolepsie et de syndrome de Klein-Levin.
La '''narcolepsie''' est une maladie caractérisée par des endormissements soudains, en pleine journée. Ceux-ci sont caractéristiques de la narcolepsie, mais elle peut être couplée à des symptômes assez accessoires, comme une fatigue diurne et des insomnies. Les hallucinations lors de l'endormissement ou du réveil sont aussi assez fréquentes, bien que parfois absentes chez certaines narcoleptiques. Beaucoup de narcoleptiques sont aussi atteints de catalepsie, un syndrome caractérisé par une décontraction totale des muscles sans perte de conscience. Un patient cataleptique peut ainsi s'effondrer ou chuter soudainement, suite à une décontraction de tous ses muscles, bien que la catalepsie puisse se limiter à quelques muscles bien précis. Cela arrive suite à une émotion, ou spontanément. Chose étonnante, les endormissements soudains ne font pas passer le malade en sommeil lent, mais le font passer directement en sommeil paradoxal. La cause la plus probable de la narcolepsie serait un dysfonctionnement impliquant l'orexine, soit que les neurones à orexine dysfonctionnent, soit qu'une déficience en orexine se fasse jour dans le cerveau.
Certaines hypersomnies sont dites récurrentes, à savoir qu'elles surviennent "régulièrement". Le syndrome de ce type le plus connu est le '''syndrome de Klein-Levin''', bien qu'il soit particulièrement rare. Ce syndrome se manifeste par poussées d'hypersomnies, avec rémission complète entre les poussées. Lors des poussées, le patient dort beaucoup plus que la normale, mais ce n'est pas le seul symptôme décelable, des symptômes cognitifs ou psychiatriques sont aussi fréquents. Niveau psychiatrique, le sujet est irritable, fatigué, apathique, parfois dépressif. Plus grave, des manifestations psychiatriques plus importantes peuvent se produire : hallucinations, délire, hypersexualité, confusion mentale, délirium, variations d'humeur, etc. Des troubles cognitifs sont très fréquents : difficultés de concentration, troubles de la mémoire, et autres.
===Les troubles du comportement en sommeil paradoxal===
Les troubles du sommeil précédent dégradaient la durée du sommeil, mais quelques troubles ne rentrent pas dans ce cadre. Les '''troubles du comportement en sommeil paradoxal''', que nous abrégerons en TCSP, sont de ceux-là. Il s'agit de rêves agités ou cauchemardesques où le sujet se met à bouger, parfois au point d'avoir un comportement violent ou impulsif. Ce trouble ressemble quelque peu au somnambulisme, si ce n'est pour une différence de taille : si le somnambulisme est un trouble du sommeil lent, les phases de pseudo-réveil du TCSP ont lieu assez souvent 90 minutes après l'endormissement, lors du sommeil paradoxal. Ce trouble a une origine assez intéressante : il se traduit par une abolition de la paralysie lors du sommeil paradoxal. Des muscles qui devraient perdre tout leur tonus et ne pas se contracter lors du sommeil paradoxal vont en fait fonctionner comme lors de l'éveil.
Le TCSP peut avoir plusieurs causes, comme l'usage d'antidépresseurs. Ceux-ci ont tendance à réduire le sommeil paradoxal et à dégrader celui-ci. Il n'est donc pas étonnant que ces médicaments déclenchent de tels troubles du sommeil paradoxal. Mais la plupart du temps, ce trouble n'a pas de cause bien définie. Le TCSP est plus fréquent chez les sujets âgés, notamment chez ceux atteints de maladies neurodégénératives. Chose importante, plus de 30% des patients âgés atteints de TCSP déclarent des maladies neurodégénératives dans les années ou décennies qui suivent l'apparition du TCSP. Plus précisément, ils déclarent des synucléopathies, tel la maladie de Parkinson, la paralysie supra-nucléaire progressive, ou la démence à corps de Lewy. Le TCSP est inhabituel dans les tauopathies et amyloses.
<noinclude>
{{NavChapitre | book=Neurosciences
| prev=Les rythmes circadiens
| prevText=Les rythmes circadiens
| next=L'état de conscience
| nextText=L'état de conscience
}}{{autoCat}}
</noinclude>
4s815cvf9brhtzmhpvft6x90ikg7hp1
775327
775326
2026-10-16T17:32:00Z
Mewtow
31375
/* L'anatomie du sommeil et de l'éveil */
775327
wikitext
text/x-wiki
On pourrait croire que le chapitre sur les rythmes circadiens vous a enseigné tout ce qu'il y a à savoir sur le sommeil, mais ce n'est pas le cas. S'il est vrai que le sommeil a une régularité similaire à celle du rythme circadien, l'horloge circadienne n'est pas la seule responsable de l'envie de dormir.
Les chercheurs ont remarqué que l'envie de sommeil n'avait parfois aucun rapport avec le rythme de cette horloge. Par exemple, le manque de sommeil augmente l'envie de dormir alors qu'il n'a pas d'effet sur l’horloge biologique. On peut l'observer en regardant non pas le sommeil, mais les autres paramètres qui évoluent avec une période de 24 heures (la température, par exemple). Ainsi, il est possible de désynchroniser l'envie de dormir du rythme circadien, suite à des privations de sommeil : le sujet aura envie de dormir à des horaires sans lien avec son rythme circadien.
Il y a donc quelque chose qui est capable d'induire le sommeil, sans que cela soit lié aux rythmes biologiques. Cette autre chose, les scientifiques lui ont donné les noms d'« homéostasie du sommeil » ou encore de « pression de sommeil ». Ce terme ne fait que formuler une évidence : plus on passe de temps éveillé, plus la fatigue se fait sentir et plus on a envie de dormir. Comme quoi, il fallait bien un chapitre complet sur le sommeil !
==La durée du sommeil==
Présent chez de nombreuses espèces animales, le sommeil est certainement apparu pour permettre aux animaux de conserver leur énergie. De nombreux animaux peu évolués peuvent tomber dans un état de torpeur, où ils restent immobiles durant plus ou moins longtemps, tandis que d'autres sont capables de dormir profondément. La durée de sommeil varie beaucoup chez les animaux, allant d'une absence quasi-totale à plus de 10 heures.
En moyenne, on estime qu'un être humain normal a besoin d'environ 8 à 9 heures de sommeil pour se sentir pleinement reposé. Cependant, cette moyenne cache une forte variabilité, vraisemblablement pour des raisons génétiques : les gros dormeurs doivent dormir 8 à 10 heures par nuit pour se sentir en forme, alors que les petits dormeurs se contentent de 3 à 4 heures.
De plus, l'âge rentre aussi en compte dans les besoins de sommeil, la durée idéale de sommeil diminuant progressivement jusqu’à la fin de l'adolescence. Un nouveau-né a tendance à dormir plus de 18 heures par jour, un enfant de 3 ans 11 à 13 heures, les adolescents ont besoin de 9 à 10 heures par jour et les adultes entre 7 et 8 heures. Contrairement à l'idée reçue, les personnes âgées dorment autant que les jeunes adultes, mais ont un sommeil plus fragmenté : elles dorment moins la nuit et se rattrapent le jour en faisant la sieste.
===La privation de sommeil===
Quand on manque de sommeil, tout se passe comme si le sujet en manque de sommeil avait une dette de sommeil à rattraper. Cela se remarque surtout par une diminution des performances du sujet : ses réactions sont plus lentes, il fait plus d'erreurs, sa concentration diminue, ses performances intellectuelles déclinent, etc.
Quelqu'un qui a mal dormit durant la nuit aura tendance à plus dormir que le lendemain pour rattraper sa dette de sommeil. Cependant, une nuit blanche est souvent rattrapée par quelques heures de sommeil supplémentaires. Plus que la durée du sommeil, c'est surtout la '''qualité du sommeil''', l'intensité du sommeil, qui permet de récupérer. Cette intensité dépend de la dette de sommeil accumulée, le sommeil étant plus réparateur après une restriction de sommeil. Et cette intensité du sommeil a une trace dans le cerveau, comme nous allons le voir dans la section suivante.
===Le ''Familial natural short sleep''===
Il existe des personnes qui ont besoin de très peu de sommeil, à peine 4 à 6 heures, pour se sentir reposées. Les personnes touchées n'expriment pas de perte de performances malgré cette courte durée de sommeil. Il s'agit d'une particularité génétique appelée ''Familial natural short sleep'', qui est très peu fréquente dans la population. Le ''Familial natural short sleep'' est un trait présent dès la naissance et dure toute la vie. De plus, il est associé à d'autres particularité : meilleure mémoire, sensibilité à la douleur diminuée, personnalité plus extravertie et optimiste.
N'allez pas croire que vous l'avez, beaucoup de monde pense avoir besoin de peu de sommeil et n'en ressent pas les conséquences cognitives, alors qu'ils sont en réalité en privation de sommeil et ont des performances dégradées. En réalité, cette particularité génétique est très rare. Les chercheurs n'ont identifié avec certitude qu'une cinquantaine de familles dans le monde avec cette particularité, et ces familles sont celles sur lesquelles la recherche génétique s'est concentrée. Mais quelques estimations disent que le chiffre réel est proche des 1% de la population. Les gènes concernés ne sont pas connus avec certitude, quelques candidats ont été rejetés (DEC2, ADRB1, autres). Il se pourrait que des gènes liés à l'orexine soient impliqués, mais sans certitude.
==Les stades du sommeil==
Le cerveau est très loin de rester inactif lors du sommeil. Même durant le sommeil, le cerveau a une activité électrique relativement soutenue. Pour rappel, nous avions vu dans le chapitre sur l'activité électrique du cerveau que l'activité électrique cérébrale se mesure avec un instrument composé de plusieurs électrodes placées à la surface du crâne : l''''électroencéphalographe''', ou EEG.
L'EEG permet de mesurer des rythmes électriques récurrents, appelés des ''rythmes cérébraux'', ou encore ''ondes cérébrales''. Ils sont classés arbitrairement dans 4 à 6 bandes de fréquences nommées alpha, bêta, gamma, delta, et thêta.
* Delta : Inférieur à 4 hertz
* Thêta : 4 à 7 hertz
* Alpha : 8 à 15 hertz
* Bêta : 16 à 31 hertz
* Gamma : Plus de 32 hertz
Les scientifiques ont remarqué que chaque bande de fréquence correspond à des niveaux d'éveil et de sommeil différents. L'activité électrique du cerveau varie entre le sommeil et l'éveil, et on peut observer des variations par rapport à la normale dans certains cas pathologiques. Si on observe l'EEG de quelqu'un qui dort, on remarque que le sommeil est organisé en '''phases de sommeil''', identifiables par l'apparition de certaines ondes cérébrales.
Lors de l'endormissement, les ondes alpha apparaissent par bouffées successives. Le sujet peut être réveillé facilement. Lors de cette phase, le sujet peut être pris d'hallucinations dites hypnagogiques, ainsi que de sursauts musculaires nommées myoclonies d’endormissement. Dans la majorité des cas, cela est tout à fait normal et n'a rien de pathologique (l'auteur en a déjà fait expérience, sans avoir de problème de santé particulier).
Une fois le sujet endormi, son sommeil est organisé en cycles, qui se répètent plusieurs fois durant une nuit. Un cycle fait environ 90 minutes, mais avec de fortes variation individuelles. Concrètement, un cycle va de 30 minutes à 3 heures, mais la moyenne est proche de 1h30. Un cycle contient une phase de ''sommeil lent'' et de ''sommeil paradoxal''. Le '''sommeil à ondes lentes''' est une phase où les ondes cérébrales sont synchronisées, de basse fréquence et de forte amplitude. Tous les neurones émettent des potentiels d'action en même temps. Le '''sommeil paradoxal''' a une activité électrique proche de l'éveil : ondes de fréquence élevée, de faible amplitude, totalement désynchronisées.
[[File:Hypnogramme.svg|vignette|upright=2.0|Hypnogramme]]
{| class="wikitable"
|+ Stades du sommeil
|-
! scope="col"| Stade du sommeil|| Ondes cérébrales prédominantes
|-
| Eveil || Ondes Gamma, Bêta
|-
| Endormissement || Ondes Alpha
|-
| Sommeil lent léger || Ondes Thêta
|-
| Sommeil lent profond || Ondes Delta
|-
| Sommeil paradoxal|| Ondes Gamma, Bêta
|}
===Le sommeil à ondes lentes===
Lors du sommeil à ondes lentes, on observe une diminution de la fréquence des ondes cérébrales et une augmentation de leur amplitude. Le dormeur devient de plus en plus difficile à réveiller. Si on réveille quelqu'un lors du sommeil à ondes lentes, il se sentira fatigué. Le dormeur peut "rêver" lors du sommeil lent, mais les rêves en question sont souvent sans composants visuelle, ils sont relativement logiques et peu chargés émotionnellement. Disons que la plupart du temps, le sujet "pense" plus qu'il rêve.
Le sommeil à ondes lentes est lui aussi subdivisé en deux phases : le sommeil léger et le sommeil profond. La distinction n'est pas très claire, il y a parfois plusieurs subdivisions supplémentaires, mais les distinctions ne sont pas très claires, surtout basées sur des critères liés à la fréquence des ondes cérébrales.
Le '''sommeil léger''' dure environ une demi-heure chez un sujet d'âge moyen, mais peut être plus long chez les personnes âgées. Dans ce stade, le tonus musculaire diminue progressivement, les mouvements oculaires se font plus lents et de moindre intensité, la respiration et le rythme cardiaques deviennent plus réguliers et ralentissent un tout petit peu.
Le dormeur peut encore être réveillé facilement dans ce stade du sommeil. Du bruit, ou de la lumière peuvent réveiller un dormeur dans ce stade. D'où le fait qu'il soit appelé le sommeil ''léger''. De plus, le dormeur ne se sentira pas trop fatigué après le réveil, même si il aura une performance légèrement amoindrie. Quelques micro-réveils, inférieures à la dizaine de secondes, peuvent se produire spontanément dans ce stade. Le dormeur peut se souvenir de ces phases de réveil, qui sont parfois sources d'hallucinations hypnagogiques.
Sur l'EEG, on peut observer des bouffées d'ondes de haute fréquence (12-14 Hertz) appelées '''fuseaux du sommeil''' qui proviennent de la boucle thalamico-corticale et des ondes de haute amplitude qui apparaissent en réaction à des bruits extérieurs ('''complexes K''').
[[File:Stage2sleep.svg|centre|vignette|upright=2.0|Sommeil léger.]]
Lors du '''sommeil profond''', les ondes Delta s'installent. Le dormeur peut encore bouger. C'est d'ailleurs lors de cette phase que les somnambules sortent de leur lit ou que les terreurs nocturnes surviennent. De manière générale, une bonne partie de ce que les médecins appellent parasomnies survient dans ce stade du sommeil. Au-delà des parasomnies, le sommeil profond est lui aussi bien nommé. Si on réveille quelqu'un en sommeil profond, celui-ci se sentira fatigué et aura des performances fortement dégradées. La durée de cette baisse de performance est d'environ une demi-heure, une heure maximum.
Le sommeil profond aurait comme rôle principal de réparer le corps et de lui permettre de reposer. À l'appui de cette affirmation, on peut dire que l'hormone de croissance est surtout produite lors du sommeil profond. Cette hormone fait grandir les enfants, mais aide aussi les cellules et tissus à se réparer. La consommation énergétique du cerveau est aussi beaucoup diminuée lors du sommeil profond, comparé à l'éveil. Une nuit reposante est associée à des ondes lentes de grande amplitude lors du sommeil profond.
Ce stade du sommeil aurait aussi un rôle à jouer dans l'apprentissage et la mémorisation. Plus précisément, il aurait une influence positive sur la consolidation des souvenirs et des connaissances acquises dans la journée. Dans le détail, les ondes lentes du sommeil profond permettraient le transfert des informations apprises de l'hippocampe vers le néocortex.
===Le sommeil paradoxal (''Rapid Eye Movement Sleep'')===
Après le sommeil à onde lentes, le '''sommeil paradoxal''' s'installe. Si on l'appelle sommeil paradoxal, c'est parce que l'EEG est très proche de l'éveil. Les rêves sont courants lors du sommeil paradoxal, nettement plus que dans les autres phases du sommeil. De plus, les rêves du sommeil paradoxal ont tendance à être assez illogiques, incohérents, riches en émotions. Leur contenu est souvent incompréhensible, vague, bizarre.
De plus, les régions motrices du cerveau sont inactives, mais le dormeur est complètement paralysé. Seuls les yeux du dormeur peuvent encore bouger, ils effectuent beaucoup de mouvements rapides sous l'effet d'ondes cérébrales produites par la moelle épinière : les ondes ponto-geniculo-occipitales. Le rythme cardiaque et la respiration augmentent, de même que la pression artérielle. Enfin, le pénis entre en érection, ce qui est très utile pour savoir si un cas d'impuissance a une origine physique ou psychique.
Il faut noter que la paralysie observée lors du sommeil paradoxal est présente chez les mammifères, mais pas chez les oiseaux (règle générale avec de nombreuses exceptions). Ils peuvent dormir debout, avec la tête relevée, sans que le sommeil paradoxal ne leur coupe les jambes. Ils donnent ainsi l'impression d'être alertes alors qu'ils dorment, ce qui peut tromper un prédateur. Leur sommeil paradoxal est de plus nettement plus court que chez les mammifères : quelques dizaines de secondes chez certaines espèces, contre plusieurs dizaines de minutes chez les mammifères ! Et ils sont aussi beaucoup plus fréquents. En somme, ils ont des cycles de sommeils plus irréguliers, plus fréquents, plus courts.
==L'anatomie du sommeil et de l'éveil==
Certaines zones du cerveau ne sont actives qu'à l'état d'éveil, d'autres ne l'étant que lors du sommeil paradoxal ou du sommeil lent. L'identification de ces zones a permis de déterminer l'existence de trois systèmes : un qui maintient le sujet à l'état d'éveil, un autre actif uniquement lors du sommeil lent et un autre actif uniquement lors du sommeil paradoxal. Ces systèmes sont tous composés d'un ensemble d'aires cérébrales distinctes, qui interagissent entre elles, notamment pour s'inhiber mutuellement. Par exemple, le système d'éveil inhibe les systèmes du sommeil lent et paradoxal, et ces deux systèmes s'inhibent eux-mêmes mutuellement.
[[File:Centres de l'éveil et du sommeil.png|centre|vignette|upright=2.5|Centres de l'éveil et du sommeil]]
===L'éveil===
L'éveil se traduit par une activité électrique soutenue dans le cortex, riche en ondes cérébrales rapides et désynchronisée. Le centre de l'éveil a pour but de maintenir cette activité dans le cortex. Le centre de l'éveil a longtemps été pensé comme une structure unique. Mais on sait aujourd'hui que celui-ci regroupe des structures cérébrales distantes, regroupées dans ce qu'on appelle le '''réseau exécutif de l’éveil'''. La lésion de ces structures peut entraîner de sérieuses pertes de vigilance, qui peuvent aller jusqu’au coma profond.
{|
|[[File:Système d'éveil - cerveau.png|vignette|upright=2.5|Système d'éveil - cerveau]]
|[[File:NeuralCorrelatesOfConsciousness2.jpg|vignette|upright=2.0|Réseau exécutif de l’éveil et formation réticulée.]]
|}
====Le réseau exécutif de l'éveil====
Les premières aires cérébrales identifiées comme faisant partie du centre de l'éveil le furent en 1918, lors de l'épidémie de grippe espagnole. Durant cette période, le neurologue Constantin von Economo remarqua que certains de ses patients tombaient dans le coma suite à diverses séquelles neurologiques, tandis que d’autres étaient atteints de sévères insomnies. Il étudia quelles étaient les aires cérébrales lésées pouvant expliquer ce résultat.
Les patients léthargiques ou comateux avaient des lésions à l''''hypothalamus postérieur'''. Celui-ci envoie des axones dans l'ensemble du cortex, innervant celui-ci de façon diffuse. Plus précisément, on peut distinguer une contribution distincte du ''noyau inféro-mammilaire'' et du ''noyau latéral''. Les neurones du premier sont des neurones à orexine, tandis que les neurones du premier relâchent de l'histamine dans les structures cérébrales voisines, ce qui est cohérent avec l'implication de ces neurotransmetteurs dans l'état d'éveil.
Il est apparu que son action est en partie une action contre-inhibitrice. En effet, l'hypothalamus antérieur se connecte à des neurones GABAergiques, à l'action inhibitrice. Les neurones de l'hypothalamus inhiberaient ces neurones GABAergiques dispersés dans le cortex : cette inhibition de l'inhibition GABAergiques se traduit naturellement par une activation du cortex.
On peut aussi citer les noyaux du '''télencéphale basal''', qui émettent de l'acétylcholine et du glutamate, le principal d'entre eux étant le noyau basal de Meynert. Leur action est directement excitatrice sur le cortex.
Le '''thalamus''' a un rôle dans l'état de vigilance et dans le cycle veille-sommeil. Et plus précisément, les boucles thalamo-corticales. Lors de la veille, les boucles thalamo-corticales sont le lieu d'une activité cérébrale désynchronisée. Mais lors du sommeil lent, une activité rythmique prend place et il est aujourd'hui évident que ce comportement est dirigé par des neurones réticulaires, eux-mêmes commandés par des formations du tronc cérébral. Pour détailler le rôle du thalamus lors du sommeil, nous allons nous référer au schéma ci-dessous.
[[File:Thalamus, boucles thalamico-corticales et sommeil.png|centre|vignette|upright=2|Thalamus, boucles thalamico-corticales et sommeil.]]
Rappelons que les noyaux du thalamus sont presque tous des relais sensoriels, qui transmettent les informations sensorielles au cortex cérébral. Ces noyaux de relai sensoriels sont chapeautés par un noyau thalamique réticulaire, qui inhibe les noyaux relai. D'autres noyaux sont des relais corticaux, à savoir qu'ils reçoivent des axones du cortex et lui en renvoient. ces noyaux forment avec le cortex des circuits fermés, où le cortex se connecte au thalamus, qui lui-même innerve le cortex. Ces circuits fermés sont appelés des boucles thalamico-corticales. Lors de l'éveil, les noyaux de relai sensoriels sont actifs et transmettent les sensations au cortex. Mais lors du sommeil, le noyau réticulaire s'active et éteint les noyaux de relai sensoriel. Le cerveau est alors isolé du monde extérieur et le cortex fonctionne en circuit fermé. Seules les boucles thalamico-corticales fonctionnent et leur activité est gouvernée par le noyau réticulaire. Or, le noyau réticulaire thalamique a une activité rythmique, semblable à celle des ondes lentes du sommeil lent.
====La formation réticulée : le système activateur ascendant====
Mais le réseau exécutif de l'éveil n'est pas le seul à avoir une influence sur l'état de veille. Les observations et les autopsies sur des patients comateux montrent que tous ont des lésions de la formation réticulée, suggérant que celle-ci est une des aires de commande du thalamus. On peut voir le réseau exécutif de l’éveil postérieur comme un relai de la formation réticulée. Durant longtemps, les savants crurent que cette structure correspondait au centre de l'éveil, d'où le nom qui lui a été donné de '''système réticulé activateur ascendant'''.
Ce système est innervé par la plupart des afférences sensorielles, mais aussi par les structures corticales. On voyait autrefois ce système comme une sorte de centre qui recevait les sensations et les intégrait avant d'activer le cerveau en conséquence. Plus le cerveau était soumis à des stimulations sensorielles fortes (douleur, vision lumineuse, autre), plus l'activation cérébrale et l'éveil étaient forts. L'éveil était alors vu comme la conséquence d'une stimulation sensorielle suffisamment forte, tandis que le sommeil se manifestait quand les stimulations sensorielles devenaient insuffisantes. On sait aujourd'hui que cette vision est au moins partielle, trop simplifiée pour être exacte.
Le système réticulaire ascendant comprend un grand nombre de noyaux appartenant à la formation réticulée, les plus connus étant le locus coeruleus et les noyaux du raphé. À ceux-ci, il faut ajouter des noyaux cholinergiques, qui activent les neurones thalamiques. On peut aussi citer les noyaux réticulés bulbaires magnocellulaires, qui émettent de l'acétylcholine et du glutamate, deux neurotransmetteurs excitateurs. Ces derniers innervent aussi bien le thalamus que l'hypothalamus postérieur. Et enfin, citons les noyaux du noyau parabrachial, qui innervent directement le télencéphale basal. Le système réticulaire ascendant active l'ensemble du néocortex par l'intermédiaire de deux voies : une voie réticulo-thalamico-corticale qui passe par le thalamus, et une voie reticulo-hypothalamico-corticale qui fait relai dans l'hypothalamus.
===Le sommeil lent===
Lors de l'endormissement, les boucles thalamico-corticales deviennent insensibles aux stimulations extérieures. Leur activité électrique se résume à une oscillation rythmique, périodique, spontanée. Cela se produit quand le système de veille se désactive, cesse d'inhiber les neurones pacemaker du thalamus. De plus, l'activité électrique globale du cortex doit diminuer, ce qui implique une inhibition du locus coeruleus, du noyau du raphé, et des autres composants du système de veille.
Le système responsable de cette inhibition est localisé dans l'hypothalamus antérieur, et plus précisément dans l''''aire pré-optique'''. La preuve principale étant que toute lésion de cette aire entraîne une insomnie presque totale. Cette aire est sensible à l'adénosine et on peut la voir comme une sorte de capteur qui mesurerait la pression de sommeil, afin de déclencher le sommeil quand celle-ci atteint un niveau trop élevé. Cette aire pré-optique contient des neurones inhibiteurs GABAergiques, qui innerveraient l'ensemble du système d'éveil. Ainsi, quand la pression de sommeil est trop forte, ces neurones émettent suffisamment de GABA pour éteindre le système de veille. Cependant, l'aire pré-optique est inhibé par le réseau de l'éveil, ces deux systèmes s'inhibant mutuellement. L'aire pré-optique est notamment sensible à la sérotonine, l'adrénaline et l'acétylcholine, qui l'inhibent. Seuls les neurones à hypocrétine/orexine de l'hypothalamus n'influencent pas l'aire pré-optique. Cela suggère que ceux-ci stabiliseraient l'état d'éveil. D'ailleurs, la narcolepsie est en grande partie causée par une destruction de ces neurones à orexine.
Cependant, l'aire pré-optique n'est pas le seul mécanisme d'entrée en sommeil lent. Une destruction totale de l'aire pré-optique entraîne certes une diminution du temps passé en sommeil lent, ainsi que des difficultés d'endormissement, mais elle ne supprime pas le sommeil lent. D'autres systèmes du sommeil lent doivent donc exister. À l'heure actuelle, les chercheurs suspectent l'implication des neurones GABAergiques de la zone parafaciale du tronc cérébral. L'activation de ces neurones semble induire un état proche, si ce n'est identique, du sommeil lent. Ils agiraient par inhibition de la transmission glutamatinergique du noyau parabrachial.
===Le sommeil paradoxal===
Le sommeil paradoxal est commandé par un ensemble de noyaux spécialisés qui interagissent entre eux. Certains de ces noyaux, ainsi que les aires cérébrales du cortex, sont aussi actifs lors de l'éveil. C'est notamment le cas des noyaux cholinergiques, qui s'activent lors du sommeil paradoxal. La chimie du sommeil paradoxal est donc un intermédiaire entre le sommeil lent et l'éveil. La production d'acétylcholine est proche de celle de l'éveil, tandis que la production de monoamines et d'orexine est nulle.
Mais d'autres noyaux sont spécifiquement actifs lors du sommeil paradoxal. Leurs neurones sont appelés des '''neurones SP-on'''. Ceux-ci sont responsables de ce qui se passe lors du sommeil paradoxal. Ces noyaux SP-on contiennent les neurones glutaminergique du noyau sublatérodorsal, la partie ventrolatérale de la substance grise périacqueducale, le noyau reticularis pontis oralis (un noyau sensible à l'acétylcholine) et enfin le noyau paragigantocellulaire latéral. L'inhibition motrice du sommeil paradoxal est causée par l'activation du noyau sublatérodorsal. Ainsi, des lésions de ce noyau suppriment l'atonie musculaire lors du sommeil paradoxal. Ceux-ci émettent des afférences glutaminergiques vers des noyaux GABAergiques/glycinergique du myélencéphalequi innervent la moelle épinière. Les ondes géniculopontiques sont générés dans le tegmentum du mésencéphale.
Certaines structures du cerveau, n'appartenant pas à ce système, sont totalement désactivées lors du sommeil paradoxal. C'est notamment le cas du locus coeruleus et du noyau du raphé. Les neurones de ces aires sont donc appelés des '''neurones SP-off''', pour bien signaler qu'il s"éteignent lors du sommeil paradoxal. De manière générale, le cerveau ne sécrète presque pas de monoamine lors du sommeil paradoxal. L'activation des systèmes mono-aminergiques pendant le sommeil paradoxal interrompt d'ailleurs celui-ci. C'est ainsi que les anti-dépresseurs, qui favorisent la production de mono-amines, peuvent diminuer ou complètement abolir le sommeil paradoxal. Les inhibiteurs sélectifs de la sérotonine vont ainsi diminuer la durée de sommeil paradoxal, tandis que les tricycliques et les inhibiteurs de la monoamine oxydase vont parfois totalement l'abolir.
==Les troubles du sommeil==
Le sommeil peut facilement être troublé, que ce soit par des maladies, de l'anxiété, des médicaments, ou bien d'autres raisons. Le chapitre précédent a notamment abordé les troubles du sommeil liés aux troubles du rythme circadien. Mais ce sont loin d'être les seuls, comme tout insomniaque le sait déjà ! Mais leur classification est clairement assez difficile, plusieurs classifications existant à l'heure actuelle. De nos jours, les trois classifications qui font référence sont celles du DSM (le manuel de diagnostic des troubles mentaux), l'ICD (International Classification of Disease de l'OMS) et celle de l'ICSD (International Classification of Sleep Disorder). Mais ces trois classifications sont assez similaires, ce qui fait que les maladies du sommeil sont assez bien identifiées. Dans les grandes lignes, les troubles du sommeil se classent en quelques grands syndromes : les troubles du rythme circadien, l'insomnie, les hypersomnies, les parasomnies, les mouvements anormaux liés au sommeil et les apnées du sommeil. Il arrive aussi certaines maladies modifient la durée du sommeil, certaines pouvant faire disparaître totalement le sommeil : on parle alors d'agrypnie.
===L'insomnie===
L''''insomnie''' est juste une réduction de la quantité de sommeil, là où les hypersomnies correspondent à l'inverse, à savoir un excès de sommeil. Le syndrome le plus courant est clairement l'insomnie, qui frappe bien plus de monde que l'hypersomnie. La plupart des insomnies sont temporaires, liées à de l'anxiété ou une origine psychologique bénigne quelconque. Ces insomnies passent le plus souvent en quelques semaines ou quelques jours au mieux. Plus rares sont les insomnies organiques, dont la cause est totalement autre que psychologique. Elles peuvent provenir de la consommation d’existants ou de médicaments aux effets stimulants, mais aussi de troubles neurologiques ou psychiatriques un peu plus sérieux. Par exemple, les patients dépressifs ont souvent des insomnies (ou des hypersomnies, selon le cas) : ils ont notamment tendance à se réveiller plus tôt le matin et à se lever plus fréquemment la nuit. On observe aussi des insomnies assez sévères chez les bipolaires en phase maniaque : leurs besoins en sommeil diminuent aux alentours de 2 ou 3 heures de sommeil.
===Les hypersomnies===
Les '''hypersomnies''' sont plus rares, quoiqu'elles soient le second trouble du sommeil en termes de fréquence. La plupart ont des origines assez classiques : une réaction face un manque de sommeil, le résultat de problèmes psychologiques, l'arrivée de l'hiver, etc. Rarement, l'hypersomnie est liée à des maladies psychiatriques. Par exemple, la dépression peut entraîner des hypersomnies assez importantes chez certains patients. L'abus d'hypnotiques est aussi une raison assez évidente. Mais certaines maladies peuvent entraîner des hypersomnies assez sévères, assez idiosyncratiques. Ces maladies sont connues sous le nom de narcolepsie et de syndrome de Klein-Levin.
La '''narcolepsie''' est une maladie caractérisée par des endormissements soudains, en pleine journée. Ceux-ci sont caractéristiques de la narcolepsie, mais elle peut être couplée à des symptômes assez accessoires, comme une fatigue diurne et des insomnies. Les hallucinations lors de l'endormissement ou du réveil sont aussi assez fréquentes, bien que parfois absentes chez certaines narcoleptiques. Beaucoup de narcoleptiques sont aussi atteints de catalepsie, un syndrome caractérisé par une décontraction totale des muscles sans perte de conscience. Un patient cataleptique peut ainsi s'effondrer ou chuter soudainement, suite à une décontraction de tous ses muscles, bien que la catalepsie puisse se limiter à quelques muscles bien précis. Cela arrive suite à une émotion, ou spontanément. Chose étonnante, les endormissements soudains ne font pas passer le malade en sommeil lent, mais le font passer directement en sommeil paradoxal. La cause la plus probable de la narcolepsie serait un dysfonctionnement impliquant l'orexine, soit que les neurones à orexine dysfonctionnent, soit qu'une déficience en orexine se fasse jour dans le cerveau.
Certaines hypersomnies sont dites récurrentes, à savoir qu'elles surviennent "régulièrement". Le syndrome de ce type le plus connu est le '''syndrome de Klein-Levin''', bien qu'il soit particulièrement rare. Ce syndrome se manifeste par poussées d'hypersomnies, avec rémission complète entre les poussées. Lors des poussées, le patient dort beaucoup plus que la normale, mais ce n'est pas le seul symptôme décelable, des symptômes cognitifs ou psychiatriques sont aussi fréquents. Niveau psychiatrique, le sujet est irritable, fatigué, apathique, parfois dépressif. Plus grave, des manifestations psychiatriques plus importantes peuvent se produire : hallucinations, délire, hypersexualité, confusion mentale, délirium, variations d'humeur, etc. Des troubles cognitifs sont très fréquents : difficultés de concentration, troubles de la mémoire, et autres.
===Les troubles du comportement en sommeil paradoxal===
Les troubles du sommeil précédent dégradaient la durée du sommeil, mais quelques troubles ne rentrent pas dans ce cadre. Les '''troubles du comportement en sommeil paradoxal''', que nous abrégerons en TCSP, sont de ceux-là. Il s'agit de rêves agités ou cauchemardesques où le sujet se met à bouger, parfois au point d'avoir un comportement violent ou impulsif. Ce trouble ressemble quelque peu au somnambulisme, si ce n'est pour une différence de taille : si le somnambulisme est un trouble du sommeil lent, les phases de pseudo-réveil du TCSP ont lieu assez souvent 90 minutes après l'endormissement, lors du sommeil paradoxal. Ce trouble a une origine assez intéressante : il se traduit par une abolition de la paralysie lors du sommeil paradoxal. Des muscles qui devraient perdre tout leur tonus et ne pas se contracter lors du sommeil paradoxal vont en fait fonctionner comme lors de l'éveil.
Le TCSP peut avoir plusieurs causes, comme l'usage d'antidépresseurs. Ceux-ci ont tendance à réduire le sommeil paradoxal et à dégrader celui-ci. Il n'est donc pas étonnant que ces médicaments déclenchent de tels troubles du sommeil paradoxal. Mais la plupart du temps, ce trouble n'a pas de cause bien définie. Le TCSP est plus fréquent chez les sujets âgés, notamment chez ceux atteints de maladies neurodégénératives. Chose importante, plus de 30% des patients âgés atteints de TCSP déclarent des maladies neurodégénératives dans les années ou décennies qui suivent l'apparition du TCSP. Plus précisément, ils déclarent des synucléopathies, tel la maladie de Parkinson, la paralysie supra-nucléaire progressive, ou la démence à corps de Lewy. Le TCSP est inhabituel dans les tauopathies et amyloses.
<noinclude>
{{NavChapitre | book=Neurosciences
| prev=Les rythmes circadiens
| prevText=Les rythmes circadiens
| next=L'état de conscience
| nextText=L'état de conscience
}}{{autoCat}}
</noinclude>
psn19t9hzpawqu4e0t2wh6zuca6bldu
775328
775327
2026-10-16T17:32:37Z
Mewtow
31375
/* L'éveil */
775328
wikitext
text/x-wiki
On pourrait croire que le chapitre sur les rythmes circadiens vous a enseigné tout ce qu'il y a à savoir sur le sommeil, mais ce n'est pas le cas. S'il est vrai que le sommeil a une régularité similaire à celle du rythme circadien, l'horloge circadienne n'est pas la seule responsable de l'envie de dormir.
Les chercheurs ont remarqué que l'envie de sommeil n'avait parfois aucun rapport avec le rythme de cette horloge. Par exemple, le manque de sommeil augmente l'envie de dormir alors qu'il n'a pas d'effet sur l’horloge biologique. On peut l'observer en regardant non pas le sommeil, mais les autres paramètres qui évoluent avec une période de 24 heures (la température, par exemple). Ainsi, il est possible de désynchroniser l'envie de dormir du rythme circadien, suite à des privations de sommeil : le sujet aura envie de dormir à des horaires sans lien avec son rythme circadien.
Il y a donc quelque chose qui est capable d'induire le sommeil, sans que cela soit lié aux rythmes biologiques. Cette autre chose, les scientifiques lui ont donné les noms d'« homéostasie du sommeil » ou encore de « pression de sommeil ». Ce terme ne fait que formuler une évidence : plus on passe de temps éveillé, plus la fatigue se fait sentir et plus on a envie de dormir. Comme quoi, il fallait bien un chapitre complet sur le sommeil !
==La durée du sommeil==
Présent chez de nombreuses espèces animales, le sommeil est certainement apparu pour permettre aux animaux de conserver leur énergie. De nombreux animaux peu évolués peuvent tomber dans un état de torpeur, où ils restent immobiles durant plus ou moins longtemps, tandis que d'autres sont capables de dormir profondément. La durée de sommeil varie beaucoup chez les animaux, allant d'une absence quasi-totale à plus de 10 heures.
En moyenne, on estime qu'un être humain normal a besoin d'environ 8 à 9 heures de sommeil pour se sentir pleinement reposé. Cependant, cette moyenne cache une forte variabilité, vraisemblablement pour des raisons génétiques : les gros dormeurs doivent dormir 8 à 10 heures par nuit pour se sentir en forme, alors que les petits dormeurs se contentent de 3 à 4 heures.
De plus, l'âge rentre aussi en compte dans les besoins de sommeil, la durée idéale de sommeil diminuant progressivement jusqu’à la fin de l'adolescence. Un nouveau-né a tendance à dormir plus de 18 heures par jour, un enfant de 3 ans 11 à 13 heures, les adolescents ont besoin de 9 à 10 heures par jour et les adultes entre 7 et 8 heures. Contrairement à l'idée reçue, les personnes âgées dorment autant que les jeunes adultes, mais ont un sommeil plus fragmenté : elles dorment moins la nuit et se rattrapent le jour en faisant la sieste.
===La privation de sommeil===
Quand on manque de sommeil, tout se passe comme si le sujet en manque de sommeil avait une dette de sommeil à rattraper. Cela se remarque surtout par une diminution des performances du sujet : ses réactions sont plus lentes, il fait plus d'erreurs, sa concentration diminue, ses performances intellectuelles déclinent, etc.
Quelqu'un qui a mal dormit durant la nuit aura tendance à plus dormir que le lendemain pour rattraper sa dette de sommeil. Cependant, une nuit blanche est souvent rattrapée par quelques heures de sommeil supplémentaires. Plus que la durée du sommeil, c'est surtout la '''qualité du sommeil''', l'intensité du sommeil, qui permet de récupérer. Cette intensité dépend de la dette de sommeil accumulée, le sommeil étant plus réparateur après une restriction de sommeil. Et cette intensité du sommeil a une trace dans le cerveau, comme nous allons le voir dans la section suivante.
===Le ''Familial natural short sleep''===
Il existe des personnes qui ont besoin de très peu de sommeil, à peine 4 à 6 heures, pour se sentir reposées. Les personnes touchées n'expriment pas de perte de performances malgré cette courte durée de sommeil. Il s'agit d'une particularité génétique appelée ''Familial natural short sleep'', qui est très peu fréquente dans la population. Le ''Familial natural short sleep'' est un trait présent dès la naissance et dure toute la vie. De plus, il est associé à d'autres particularité : meilleure mémoire, sensibilité à la douleur diminuée, personnalité plus extravertie et optimiste.
N'allez pas croire que vous l'avez, beaucoup de monde pense avoir besoin de peu de sommeil et n'en ressent pas les conséquences cognitives, alors qu'ils sont en réalité en privation de sommeil et ont des performances dégradées. En réalité, cette particularité génétique est très rare. Les chercheurs n'ont identifié avec certitude qu'une cinquantaine de familles dans le monde avec cette particularité, et ces familles sont celles sur lesquelles la recherche génétique s'est concentrée. Mais quelques estimations disent que le chiffre réel est proche des 1% de la population. Les gènes concernés ne sont pas connus avec certitude, quelques candidats ont été rejetés (DEC2, ADRB1, autres). Il se pourrait que des gènes liés à l'orexine soient impliqués, mais sans certitude.
==Les stades du sommeil==
Le cerveau est très loin de rester inactif lors du sommeil. Même durant le sommeil, le cerveau a une activité électrique relativement soutenue. Pour rappel, nous avions vu dans le chapitre sur l'activité électrique du cerveau que l'activité électrique cérébrale se mesure avec un instrument composé de plusieurs électrodes placées à la surface du crâne : l''''électroencéphalographe''', ou EEG.
L'EEG permet de mesurer des rythmes électriques récurrents, appelés des ''rythmes cérébraux'', ou encore ''ondes cérébrales''. Ils sont classés arbitrairement dans 4 à 6 bandes de fréquences nommées alpha, bêta, gamma, delta, et thêta.
* Delta : Inférieur à 4 hertz
* Thêta : 4 à 7 hertz
* Alpha : 8 à 15 hertz
* Bêta : 16 à 31 hertz
* Gamma : Plus de 32 hertz
Les scientifiques ont remarqué que chaque bande de fréquence correspond à des niveaux d'éveil et de sommeil différents. L'activité électrique du cerveau varie entre le sommeil et l'éveil, et on peut observer des variations par rapport à la normale dans certains cas pathologiques. Si on observe l'EEG de quelqu'un qui dort, on remarque que le sommeil est organisé en '''phases de sommeil''', identifiables par l'apparition de certaines ondes cérébrales.
Lors de l'endormissement, les ondes alpha apparaissent par bouffées successives. Le sujet peut être réveillé facilement. Lors de cette phase, le sujet peut être pris d'hallucinations dites hypnagogiques, ainsi que de sursauts musculaires nommées myoclonies d’endormissement. Dans la majorité des cas, cela est tout à fait normal et n'a rien de pathologique (l'auteur en a déjà fait expérience, sans avoir de problème de santé particulier).
Une fois le sujet endormi, son sommeil est organisé en cycles, qui se répètent plusieurs fois durant une nuit. Un cycle fait environ 90 minutes, mais avec de fortes variation individuelles. Concrètement, un cycle va de 30 minutes à 3 heures, mais la moyenne est proche de 1h30. Un cycle contient une phase de ''sommeil lent'' et de ''sommeil paradoxal''. Le '''sommeil à ondes lentes''' est une phase où les ondes cérébrales sont synchronisées, de basse fréquence et de forte amplitude. Tous les neurones émettent des potentiels d'action en même temps. Le '''sommeil paradoxal''' a une activité électrique proche de l'éveil : ondes de fréquence élevée, de faible amplitude, totalement désynchronisées.
[[File:Hypnogramme.svg|vignette|upright=2.0|Hypnogramme]]
{| class="wikitable"
|+ Stades du sommeil
|-
! scope="col"| Stade du sommeil|| Ondes cérébrales prédominantes
|-
| Eveil || Ondes Gamma, Bêta
|-
| Endormissement || Ondes Alpha
|-
| Sommeil lent léger || Ondes Thêta
|-
| Sommeil lent profond || Ondes Delta
|-
| Sommeil paradoxal|| Ondes Gamma, Bêta
|}
===Le sommeil à ondes lentes===
Lors du sommeil à ondes lentes, on observe une diminution de la fréquence des ondes cérébrales et une augmentation de leur amplitude. Le dormeur devient de plus en plus difficile à réveiller. Si on réveille quelqu'un lors du sommeil à ondes lentes, il se sentira fatigué. Le dormeur peut "rêver" lors du sommeil lent, mais les rêves en question sont souvent sans composants visuelle, ils sont relativement logiques et peu chargés émotionnellement. Disons que la plupart du temps, le sujet "pense" plus qu'il rêve.
Le sommeil à ondes lentes est lui aussi subdivisé en deux phases : le sommeil léger et le sommeil profond. La distinction n'est pas très claire, il y a parfois plusieurs subdivisions supplémentaires, mais les distinctions ne sont pas très claires, surtout basées sur des critères liés à la fréquence des ondes cérébrales.
Le '''sommeil léger''' dure environ une demi-heure chez un sujet d'âge moyen, mais peut être plus long chez les personnes âgées. Dans ce stade, le tonus musculaire diminue progressivement, les mouvements oculaires se font plus lents et de moindre intensité, la respiration et le rythme cardiaques deviennent plus réguliers et ralentissent un tout petit peu.
Le dormeur peut encore être réveillé facilement dans ce stade du sommeil. Du bruit, ou de la lumière peuvent réveiller un dormeur dans ce stade. D'où le fait qu'il soit appelé le sommeil ''léger''. De plus, le dormeur ne se sentira pas trop fatigué après le réveil, même si il aura une performance légèrement amoindrie. Quelques micro-réveils, inférieures à la dizaine de secondes, peuvent se produire spontanément dans ce stade. Le dormeur peut se souvenir de ces phases de réveil, qui sont parfois sources d'hallucinations hypnagogiques.
Sur l'EEG, on peut observer des bouffées d'ondes de haute fréquence (12-14 Hertz) appelées '''fuseaux du sommeil''' qui proviennent de la boucle thalamico-corticale et des ondes de haute amplitude qui apparaissent en réaction à des bruits extérieurs ('''complexes K''').
[[File:Stage2sleep.svg|centre|vignette|upright=2.0|Sommeil léger.]]
Lors du '''sommeil profond''', les ondes Delta s'installent. Le dormeur peut encore bouger. C'est d'ailleurs lors de cette phase que les somnambules sortent de leur lit ou que les terreurs nocturnes surviennent. De manière générale, une bonne partie de ce que les médecins appellent parasomnies survient dans ce stade du sommeil. Au-delà des parasomnies, le sommeil profond est lui aussi bien nommé. Si on réveille quelqu'un en sommeil profond, celui-ci se sentira fatigué et aura des performances fortement dégradées. La durée de cette baisse de performance est d'environ une demi-heure, une heure maximum.
Le sommeil profond aurait comme rôle principal de réparer le corps et de lui permettre de reposer. À l'appui de cette affirmation, on peut dire que l'hormone de croissance est surtout produite lors du sommeil profond. Cette hormone fait grandir les enfants, mais aide aussi les cellules et tissus à se réparer. La consommation énergétique du cerveau est aussi beaucoup diminuée lors du sommeil profond, comparé à l'éveil. Une nuit reposante est associée à des ondes lentes de grande amplitude lors du sommeil profond.
Ce stade du sommeil aurait aussi un rôle à jouer dans l'apprentissage et la mémorisation. Plus précisément, il aurait une influence positive sur la consolidation des souvenirs et des connaissances acquises dans la journée. Dans le détail, les ondes lentes du sommeil profond permettraient le transfert des informations apprises de l'hippocampe vers le néocortex.
===Le sommeil paradoxal (''Rapid Eye Movement Sleep'')===
Après le sommeil à onde lentes, le '''sommeil paradoxal''' s'installe. Si on l'appelle sommeil paradoxal, c'est parce que l'EEG est très proche de l'éveil. Les rêves sont courants lors du sommeil paradoxal, nettement plus que dans les autres phases du sommeil. De plus, les rêves du sommeil paradoxal ont tendance à être assez illogiques, incohérents, riches en émotions. Leur contenu est souvent incompréhensible, vague, bizarre.
De plus, les régions motrices du cerveau sont inactives, mais le dormeur est complètement paralysé. Seuls les yeux du dormeur peuvent encore bouger, ils effectuent beaucoup de mouvements rapides sous l'effet d'ondes cérébrales produites par la moelle épinière : les ondes ponto-geniculo-occipitales. Le rythme cardiaque et la respiration augmentent, de même que la pression artérielle. Enfin, le pénis entre en érection, ce qui est très utile pour savoir si un cas d'impuissance a une origine physique ou psychique.
Il faut noter que la paralysie observée lors du sommeil paradoxal est présente chez les mammifères, mais pas chez les oiseaux (règle générale avec de nombreuses exceptions). Ils peuvent dormir debout, avec la tête relevée, sans que le sommeil paradoxal ne leur coupe les jambes. Ils donnent ainsi l'impression d'être alertes alors qu'ils dorment, ce qui peut tromper un prédateur. Leur sommeil paradoxal est de plus nettement plus court que chez les mammifères : quelques dizaines de secondes chez certaines espèces, contre plusieurs dizaines de minutes chez les mammifères ! Et ils sont aussi beaucoup plus fréquents. En somme, ils ont des cycles de sommeils plus irréguliers, plus fréquents, plus courts.
==L'anatomie du sommeil et de l'éveil==
Certaines zones du cerveau ne sont actives qu'à l'état d'éveil, d'autres ne l'étant que lors du sommeil paradoxal ou du sommeil lent. L'identification de ces zones a permis de déterminer l'existence de trois systèmes : un qui maintient le sujet à l'état d'éveil, un autre actif uniquement lors du sommeil lent et un autre actif uniquement lors du sommeil paradoxal. Ces systèmes sont tous composés d'un ensemble d'aires cérébrales distinctes, qui interagissent entre elles, notamment pour s'inhiber mutuellement. Par exemple, le système d'éveil inhibe les systèmes du sommeil lent et paradoxal, et ces deux systèmes s'inhibent eux-mêmes mutuellement.
[[File:Centres de l'éveil et du sommeil.png|centre|vignette|upright=2.5|Centres de l'éveil et du sommeil]]
===Le réseau exécutif de l'éveil et le système activateur ascendant====
L'éveil se traduit par une activité électrique soutenue dans le cortex, riche en ondes cérébrales rapides et désynchronisée. Le centre de l'éveil a pour but de maintenir cette activité dans le cortex. Le centre de l'éveil a longtemps été pensé comme une structure unique. Mais on sait aujourd'hui que celui-ci regroupe des structures cérébrales distantes, regroupées dans ce qu'on appelle le '''réseau exécutif de l’éveil'''. La lésion de ces structures peut entraîner de sérieuses pertes de vigilance, qui peuvent aller jusqu’au coma profond.
{|
|[[File:Système d'éveil - cerveau.png|vignette|upright=2.5|Système d'éveil - cerveau]]
|[[File:NeuralCorrelatesOfConsciousness2.jpg|vignette|upright=2.0|Réseau exécutif de l’éveil et formation réticulée.]]
|}
Les premières aires cérébrales identifiées comme faisant partie du centre de l'éveil le furent en 1918, lors de l'épidémie de grippe espagnole. Durant cette période, le neurologue Constantin von Economo remarqua que certains de ses patients tombaient dans le coma suite à diverses séquelles neurologiques, tandis que d’autres étaient atteints de sévères insomnies. Il étudia quelles étaient les aires cérébrales lésées pouvant expliquer ce résultat.
Les patients léthargiques ou comateux avaient des lésions à l''''hypothalamus postérieur'''. Celui-ci envoie des axones dans l'ensemble du cortex, innervant celui-ci de façon diffuse. Plus précisément, on peut distinguer une contribution distincte du ''noyau inféro-mammilaire'' et du ''noyau latéral''. Les neurones du premier sont des neurones à orexine, tandis que les neurones du premier relâchent de l'histamine dans les structures cérébrales voisines, ce qui est cohérent avec l'implication de ces neurotransmetteurs dans l'état d'éveil.
Il est apparu que son action est en partie une action contre-inhibitrice. En effet, l'hypothalamus antérieur se connecte à des neurones GABAergiques, à l'action inhibitrice. Les neurones de l'hypothalamus inhiberaient ces neurones GABAergiques dispersés dans le cortex : cette inhibition de l'inhibition GABAergiques se traduit naturellement par une activation du cortex.
On peut aussi citer les noyaux du '''télencéphale basal''', qui émettent de l'acétylcholine et du glutamate, le principal d'entre eux étant le noyau basal de Meynert. Leur action est directement excitatrice sur le cortex.
Le '''thalamus''' a un rôle dans l'état de vigilance et dans le cycle veille-sommeil. Et plus précisément, les boucles thalamo-corticales. Lors de la veille, les boucles thalamo-corticales sont le lieu d'une activité cérébrale désynchronisée. Mais lors du sommeil lent, une activité rythmique prend place et il est aujourd'hui évident que ce comportement est dirigé par des neurones réticulaires, eux-mêmes commandés par des formations du tronc cérébral. Pour détailler le rôle du thalamus lors du sommeil, nous allons nous référer au schéma ci-dessous.
[[File:Thalamus, boucles thalamico-corticales et sommeil.png|centre|vignette|upright=2|Thalamus, boucles thalamico-corticales et sommeil.]]
Rappelons que les noyaux du thalamus sont presque tous des relais sensoriels, qui transmettent les informations sensorielles au cortex cérébral. Ces noyaux de relai sensoriels sont chapeautés par un noyau thalamique réticulaire, qui inhibe les noyaux relai. D'autres noyaux sont des relais corticaux, à savoir qu'ils reçoivent des axones du cortex et lui en renvoient. ces noyaux forment avec le cortex des circuits fermés, où le cortex se connecte au thalamus, qui lui-même innerve le cortex. Ces circuits fermés sont appelés des boucles thalamico-corticales. Lors de l'éveil, les noyaux de relai sensoriels sont actifs et transmettent les sensations au cortex. Mais lors du sommeil, le noyau réticulaire s'active et éteint les noyaux de relai sensoriel. Le cerveau est alors isolé du monde extérieur et le cortex fonctionne en circuit fermé. Seules les boucles thalamico-corticales fonctionnent et leur activité est gouvernée par le noyau réticulaire. Or, le noyau réticulaire thalamique a une activité rythmique, semblable à celle des ondes lentes du sommeil lent.
Mais le réseau exécutif de l'éveil n'est pas le seul à avoir une influence sur l'état de veille. Les observations et les autopsies sur des patients comateux montrent que tous ont des lésions de la formation réticulée, suggérant que celle-ci est une des aires de commande du thalamus. On peut voir le réseau exécutif de l’éveil postérieur comme un relai de la formation réticulée. Durant longtemps, les savants crurent que cette structure correspondait au centre de l'éveil, d'où le nom qui lui a été donné de '''système réticulé activateur ascendant'''.
Ce système est innervé par la plupart des afférences sensorielles, mais aussi par les structures corticales. On voyait autrefois ce système comme une sorte de centre qui recevait les sensations et les intégrait avant d'activer le cerveau en conséquence. Plus le cerveau était soumis à des stimulations sensorielles fortes (douleur, vision lumineuse, autre), plus l'activation cérébrale et l'éveil étaient forts. L'éveil était alors vu comme la conséquence d'une stimulation sensorielle suffisamment forte, tandis que le sommeil se manifestait quand les stimulations sensorielles devenaient insuffisantes. On sait aujourd'hui que cette vision est au moins partielle, trop simplifiée pour être exacte.
Le système réticulaire ascendant comprend un grand nombre de noyaux appartenant à la formation réticulée, les plus connus étant le locus coeruleus et les noyaux du raphé. À ceux-ci, il faut ajouter des noyaux cholinergiques, qui activent les neurones thalamiques. On peut aussi citer les noyaux réticulés bulbaires magnocellulaires, qui émettent de l'acétylcholine et du glutamate, deux neurotransmetteurs excitateurs. Ces derniers innervent aussi bien le thalamus que l'hypothalamus postérieur. Et enfin, citons les noyaux du noyau parabrachial, qui innervent directement le télencéphale basal. Le système réticulaire ascendant active l'ensemble du néocortex par l'intermédiaire de deux voies : une voie réticulo-thalamico-corticale qui passe par le thalamus, et une voie reticulo-hypothalamico-corticale qui fait relai dans l'hypothalamus.
===Le sommeil lent===
Lors de l'endormissement, les boucles thalamico-corticales deviennent insensibles aux stimulations extérieures. Leur activité électrique se résume à une oscillation rythmique, périodique, spontanée. Cela se produit quand le système de veille se désactive, cesse d'inhiber les neurones pacemaker du thalamus. De plus, l'activité électrique globale du cortex doit diminuer, ce qui implique une inhibition du locus coeruleus, du noyau du raphé, et des autres composants du système de veille.
Le système responsable de cette inhibition est localisé dans l'hypothalamus antérieur, et plus précisément dans l''''aire pré-optique'''. La preuve principale étant que toute lésion de cette aire entraîne une insomnie presque totale. Cette aire est sensible à l'adénosine et on peut la voir comme une sorte de capteur qui mesurerait la pression de sommeil, afin de déclencher le sommeil quand celle-ci atteint un niveau trop élevé. Cette aire pré-optique contient des neurones inhibiteurs GABAergiques, qui innerveraient l'ensemble du système d'éveil. Ainsi, quand la pression de sommeil est trop forte, ces neurones émettent suffisamment de GABA pour éteindre le système de veille. Cependant, l'aire pré-optique est inhibé par le réseau de l'éveil, ces deux systèmes s'inhibant mutuellement. L'aire pré-optique est notamment sensible à la sérotonine, l'adrénaline et l'acétylcholine, qui l'inhibent. Seuls les neurones à hypocrétine/orexine de l'hypothalamus n'influencent pas l'aire pré-optique. Cela suggère que ceux-ci stabiliseraient l'état d'éveil. D'ailleurs, la narcolepsie est en grande partie causée par une destruction de ces neurones à orexine.
Cependant, l'aire pré-optique n'est pas le seul mécanisme d'entrée en sommeil lent. Une destruction totale de l'aire pré-optique entraîne certes une diminution du temps passé en sommeil lent, ainsi que des difficultés d'endormissement, mais elle ne supprime pas le sommeil lent. D'autres systèmes du sommeil lent doivent donc exister. À l'heure actuelle, les chercheurs suspectent l'implication des neurones GABAergiques de la zone parafaciale du tronc cérébral. L'activation de ces neurones semble induire un état proche, si ce n'est identique, du sommeil lent. Ils agiraient par inhibition de la transmission glutamatinergique du noyau parabrachial.
===Le sommeil paradoxal===
Le sommeil paradoxal est commandé par un ensemble de noyaux spécialisés qui interagissent entre eux. Certains de ces noyaux, ainsi que les aires cérébrales du cortex, sont aussi actifs lors de l'éveil. C'est notamment le cas des noyaux cholinergiques, qui s'activent lors du sommeil paradoxal. La chimie du sommeil paradoxal est donc un intermédiaire entre le sommeil lent et l'éveil. La production d'acétylcholine est proche de celle de l'éveil, tandis que la production de monoamines et d'orexine est nulle.
Mais d'autres noyaux sont spécifiquement actifs lors du sommeil paradoxal. Leurs neurones sont appelés des '''neurones SP-on'''. Ceux-ci sont responsables de ce qui se passe lors du sommeil paradoxal. Ces noyaux SP-on contiennent les neurones glutaminergique du noyau sublatérodorsal, la partie ventrolatérale de la substance grise périacqueducale, le noyau reticularis pontis oralis (un noyau sensible à l'acétylcholine) et enfin le noyau paragigantocellulaire latéral. L'inhibition motrice du sommeil paradoxal est causée par l'activation du noyau sublatérodorsal. Ainsi, des lésions de ce noyau suppriment l'atonie musculaire lors du sommeil paradoxal. Ceux-ci émettent des afférences glutaminergiques vers des noyaux GABAergiques/glycinergique du myélencéphalequi innervent la moelle épinière. Les ondes géniculopontiques sont générés dans le tegmentum du mésencéphale.
Certaines structures du cerveau, n'appartenant pas à ce système, sont totalement désactivées lors du sommeil paradoxal. C'est notamment le cas du locus coeruleus et du noyau du raphé. Les neurones de ces aires sont donc appelés des '''neurones SP-off''', pour bien signaler qu'il s"éteignent lors du sommeil paradoxal. De manière générale, le cerveau ne sécrète presque pas de monoamine lors du sommeil paradoxal. L'activation des systèmes mono-aminergiques pendant le sommeil paradoxal interrompt d'ailleurs celui-ci. C'est ainsi que les anti-dépresseurs, qui favorisent la production de mono-amines, peuvent diminuer ou complètement abolir le sommeil paradoxal. Les inhibiteurs sélectifs de la sérotonine vont ainsi diminuer la durée de sommeil paradoxal, tandis que les tricycliques et les inhibiteurs de la monoamine oxydase vont parfois totalement l'abolir.
==Les troubles du sommeil==
Le sommeil peut facilement être troublé, que ce soit par des maladies, de l'anxiété, des médicaments, ou bien d'autres raisons. Le chapitre précédent a notamment abordé les troubles du sommeil liés aux troubles du rythme circadien. Mais ce sont loin d'être les seuls, comme tout insomniaque le sait déjà ! Mais leur classification est clairement assez difficile, plusieurs classifications existant à l'heure actuelle. De nos jours, les trois classifications qui font référence sont celles du DSM (le manuel de diagnostic des troubles mentaux), l'ICD (International Classification of Disease de l'OMS) et celle de l'ICSD (International Classification of Sleep Disorder). Mais ces trois classifications sont assez similaires, ce qui fait que les maladies du sommeil sont assez bien identifiées. Dans les grandes lignes, les troubles du sommeil se classent en quelques grands syndromes : les troubles du rythme circadien, l'insomnie, les hypersomnies, les parasomnies, les mouvements anormaux liés au sommeil et les apnées du sommeil. Il arrive aussi certaines maladies modifient la durée du sommeil, certaines pouvant faire disparaître totalement le sommeil : on parle alors d'agrypnie.
===L'insomnie===
L''''insomnie''' est juste une réduction de la quantité de sommeil, là où les hypersomnies correspondent à l'inverse, à savoir un excès de sommeil. Le syndrome le plus courant est clairement l'insomnie, qui frappe bien plus de monde que l'hypersomnie. La plupart des insomnies sont temporaires, liées à de l'anxiété ou une origine psychologique bénigne quelconque. Ces insomnies passent le plus souvent en quelques semaines ou quelques jours au mieux. Plus rares sont les insomnies organiques, dont la cause est totalement autre que psychologique. Elles peuvent provenir de la consommation d’existants ou de médicaments aux effets stimulants, mais aussi de troubles neurologiques ou psychiatriques un peu plus sérieux. Par exemple, les patients dépressifs ont souvent des insomnies (ou des hypersomnies, selon le cas) : ils ont notamment tendance à se réveiller plus tôt le matin et à se lever plus fréquemment la nuit. On observe aussi des insomnies assez sévères chez les bipolaires en phase maniaque : leurs besoins en sommeil diminuent aux alentours de 2 ou 3 heures de sommeil.
===Les hypersomnies===
Les '''hypersomnies''' sont plus rares, quoiqu'elles soient le second trouble du sommeil en termes de fréquence. La plupart ont des origines assez classiques : une réaction face un manque de sommeil, le résultat de problèmes psychologiques, l'arrivée de l'hiver, etc. Rarement, l'hypersomnie est liée à des maladies psychiatriques. Par exemple, la dépression peut entraîner des hypersomnies assez importantes chez certains patients. L'abus d'hypnotiques est aussi une raison assez évidente. Mais certaines maladies peuvent entraîner des hypersomnies assez sévères, assez idiosyncratiques. Ces maladies sont connues sous le nom de narcolepsie et de syndrome de Klein-Levin.
La '''narcolepsie''' est une maladie caractérisée par des endormissements soudains, en pleine journée. Ceux-ci sont caractéristiques de la narcolepsie, mais elle peut être couplée à des symptômes assez accessoires, comme une fatigue diurne et des insomnies. Les hallucinations lors de l'endormissement ou du réveil sont aussi assez fréquentes, bien que parfois absentes chez certaines narcoleptiques. Beaucoup de narcoleptiques sont aussi atteints de catalepsie, un syndrome caractérisé par une décontraction totale des muscles sans perte de conscience. Un patient cataleptique peut ainsi s'effondrer ou chuter soudainement, suite à une décontraction de tous ses muscles, bien que la catalepsie puisse se limiter à quelques muscles bien précis. Cela arrive suite à une émotion, ou spontanément. Chose étonnante, les endormissements soudains ne font pas passer le malade en sommeil lent, mais le font passer directement en sommeil paradoxal. La cause la plus probable de la narcolepsie serait un dysfonctionnement impliquant l'orexine, soit que les neurones à orexine dysfonctionnent, soit qu'une déficience en orexine se fasse jour dans le cerveau.
Certaines hypersomnies sont dites récurrentes, à savoir qu'elles surviennent "régulièrement". Le syndrome de ce type le plus connu est le '''syndrome de Klein-Levin''', bien qu'il soit particulièrement rare. Ce syndrome se manifeste par poussées d'hypersomnies, avec rémission complète entre les poussées. Lors des poussées, le patient dort beaucoup plus que la normale, mais ce n'est pas le seul symptôme décelable, des symptômes cognitifs ou psychiatriques sont aussi fréquents. Niveau psychiatrique, le sujet est irritable, fatigué, apathique, parfois dépressif. Plus grave, des manifestations psychiatriques plus importantes peuvent se produire : hallucinations, délire, hypersexualité, confusion mentale, délirium, variations d'humeur, etc. Des troubles cognitifs sont très fréquents : difficultés de concentration, troubles de la mémoire, et autres.
===Les troubles du comportement en sommeil paradoxal===
Les troubles du sommeil précédent dégradaient la durée du sommeil, mais quelques troubles ne rentrent pas dans ce cadre. Les '''troubles du comportement en sommeil paradoxal''', que nous abrégerons en TCSP, sont de ceux-là. Il s'agit de rêves agités ou cauchemardesques où le sujet se met à bouger, parfois au point d'avoir un comportement violent ou impulsif. Ce trouble ressemble quelque peu au somnambulisme, si ce n'est pour une différence de taille : si le somnambulisme est un trouble du sommeil lent, les phases de pseudo-réveil du TCSP ont lieu assez souvent 90 minutes après l'endormissement, lors du sommeil paradoxal. Ce trouble a une origine assez intéressante : il se traduit par une abolition de la paralysie lors du sommeil paradoxal. Des muscles qui devraient perdre tout leur tonus et ne pas se contracter lors du sommeil paradoxal vont en fait fonctionner comme lors de l'éveil.
Le TCSP peut avoir plusieurs causes, comme l'usage d'antidépresseurs. Ceux-ci ont tendance à réduire le sommeil paradoxal et à dégrader celui-ci. Il n'est donc pas étonnant que ces médicaments déclenchent de tels troubles du sommeil paradoxal. Mais la plupart du temps, ce trouble n'a pas de cause bien définie. Le TCSP est plus fréquent chez les sujets âgés, notamment chez ceux atteints de maladies neurodégénératives. Chose importante, plus de 30% des patients âgés atteints de TCSP déclarent des maladies neurodégénératives dans les années ou décennies qui suivent l'apparition du TCSP. Plus précisément, ils déclarent des synucléopathies, tel la maladie de Parkinson, la paralysie supra-nucléaire progressive, ou la démence à corps de Lewy. Le TCSP est inhabituel dans les tauopathies et amyloses.
<noinclude>
{{NavChapitre | book=Neurosciences
| prev=Les rythmes circadiens
| prevText=Les rythmes circadiens
| next=L'état de conscience
| nextText=L'état de conscience
}}{{autoCat}}
</noinclude>
p6gwdfuy2mbnbs7hpxolgm73kmucbll
775329
775328
2026-10-16T18:26:15Z
Mewtow
31375
/* Le réseau exécutif de l'éveil et le système activateur ascendant= */
775329
wikitext
text/x-wiki
On pourrait croire que le chapitre sur les rythmes circadiens vous a enseigné tout ce qu'il y a à savoir sur le sommeil, mais ce n'est pas le cas. S'il est vrai que le sommeil a une régularité similaire à celle du rythme circadien, l'horloge circadienne n'est pas la seule responsable de l'envie de dormir.
Les chercheurs ont remarqué que l'envie de sommeil n'avait parfois aucun rapport avec le rythme de cette horloge. Par exemple, le manque de sommeil augmente l'envie de dormir alors qu'il n'a pas d'effet sur l’horloge biologique. On peut l'observer en regardant non pas le sommeil, mais les autres paramètres qui évoluent avec une période de 24 heures (la température, par exemple). Ainsi, il est possible de désynchroniser l'envie de dormir du rythme circadien, suite à des privations de sommeil : le sujet aura envie de dormir à des horaires sans lien avec son rythme circadien.
Il y a donc quelque chose qui est capable d'induire le sommeil, sans que cela soit lié aux rythmes biologiques. Cette autre chose, les scientifiques lui ont donné les noms d'« homéostasie du sommeil » ou encore de « pression de sommeil ». Ce terme ne fait que formuler une évidence : plus on passe de temps éveillé, plus la fatigue se fait sentir et plus on a envie de dormir. Comme quoi, il fallait bien un chapitre complet sur le sommeil !
==La durée du sommeil==
Présent chez de nombreuses espèces animales, le sommeil est certainement apparu pour permettre aux animaux de conserver leur énergie. De nombreux animaux peu évolués peuvent tomber dans un état de torpeur, où ils restent immobiles durant plus ou moins longtemps, tandis que d'autres sont capables de dormir profondément. La durée de sommeil varie beaucoup chez les animaux, allant d'une absence quasi-totale à plus de 10 heures.
En moyenne, on estime qu'un être humain normal a besoin d'environ 8 à 9 heures de sommeil pour se sentir pleinement reposé. Cependant, cette moyenne cache une forte variabilité, vraisemblablement pour des raisons génétiques : les gros dormeurs doivent dormir 8 à 10 heures par nuit pour se sentir en forme, alors que les petits dormeurs se contentent de 3 à 4 heures.
De plus, l'âge rentre aussi en compte dans les besoins de sommeil, la durée idéale de sommeil diminuant progressivement jusqu’à la fin de l'adolescence. Un nouveau-né a tendance à dormir plus de 18 heures par jour, un enfant de 3 ans 11 à 13 heures, les adolescents ont besoin de 9 à 10 heures par jour et les adultes entre 7 et 8 heures. Contrairement à l'idée reçue, les personnes âgées dorment autant que les jeunes adultes, mais ont un sommeil plus fragmenté : elles dorment moins la nuit et se rattrapent le jour en faisant la sieste.
===La privation de sommeil===
Quand on manque de sommeil, tout se passe comme si le sujet en manque de sommeil avait une dette de sommeil à rattraper. Cela se remarque surtout par une diminution des performances du sujet : ses réactions sont plus lentes, il fait plus d'erreurs, sa concentration diminue, ses performances intellectuelles déclinent, etc.
Quelqu'un qui a mal dormit durant la nuit aura tendance à plus dormir que le lendemain pour rattraper sa dette de sommeil. Cependant, une nuit blanche est souvent rattrapée par quelques heures de sommeil supplémentaires. Plus que la durée du sommeil, c'est surtout la '''qualité du sommeil''', l'intensité du sommeil, qui permet de récupérer. Cette intensité dépend de la dette de sommeil accumulée, le sommeil étant plus réparateur après une restriction de sommeil. Et cette intensité du sommeil a une trace dans le cerveau, comme nous allons le voir dans la section suivante.
===Le ''Familial natural short sleep''===
Il existe des personnes qui ont besoin de très peu de sommeil, à peine 4 à 6 heures, pour se sentir reposées. Les personnes touchées n'expriment pas de perte de performances malgré cette courte durée de sommeil. Il s'agit d'une particularité génétique appelée ''Familial natural short sleep'', qui est très peu fréquente dans la population. Le ''Familial natural short sleep'' est un trait présent dès la naissance et dure toute la vie. De plus, il est associé à d'autres particularité : meilleure mémoire, sensibilité à la douleur diminuée, personnalité plus extravertie et optimiste.
N'allez pas croire que vous l'avez, beaucoup de monde pense avoir besoin de peu de sommeil et n'en ressent pas les conséquences cognitives, alors qu'ils sont en réalité en privation de sommeil et ont des performances dégradées. En réalité, cette particularité génétique est très rare. Les chercheurs n'ont identifié avec certitude qu'une cinquantaine de familles dans le monde avec cette particularité, et ces familles sont celles sur lesquelles la recherche génétique s'est concentrée. Mais quelques estimations disent que le chiffre réel est proche des 1% de la population. Les gènes concernés ne sont pas connus avec certitude, quelques candidats ont été rejetés (DEC2, ADRB1, autres). Il se pourrait que des gènes liés à l'orexine soient impliqués, mais sans certitude.
==Les stades du sommeil==
Le cerveau est très loin de rester inactif lors du sommeil. Même durant le sommeil, le cerveau a une activité électrique relativement soutenue. Pour rappel, nous avions vu dans le chapitre sur l'activité électrique du cerveau que l'activité électrique cérébrale se mesure avec un instrument composé de plusieurs électrodes placées à la surface du crâne : l''''électroencéphalographe''', ou EEG.
L'EEG permet de mesurer des rythmes électriques récurrents, appelés des ''rythmes cérébraux'', ou encore ''ondes cérébrales''. Ils sont classés arbitrairement dans 4 à 6 bandes de fréquences nommées alpha, bêta, gamma, delta, et thêta.
* Delta : Inférieur à 4 hertz
* Thêta : 4 à 7 hertz
* Alpha : 8 à 15 hertz
* Bêta : 16 à 31 hertz
* Gamma : Plus de 32 hertz
Les scientifiques ont remarqué que chaque bande de fréquence correspond à des niveaux d'éveil et de sommeil différents. L'activité électrique du cerveau varie entre le sommeil et l'éveil, et on peut observer des variations par rapport à la normale dans certains cas pathologiques. Si on observe l'EEG de quelqu'un qui dort, on remarque que le sommeil est organisé en '''phases de sommeil''', identifiables par l'apparition de certaines ondes cérébrales.
Lors de l'endormissement, les ondes alpha apparaissent par bouffées successives. Le sujet peut être réveillé facilement. Lors de cette phase, le sujet peut être pris d'hallucinations dites hypnagogiques, ainsi que de sursauts musculaires nommées myoclonies d’endormissement. Dans la majorité des cas, cela est tout à fait normal et n'a rien de pathologique (l'auteur en a déjà fait expérience, sans avoir de problème de santé particulier).
Une fois le sujet endormi, son sommeil est organisé en cycles, qui se répètent plusieurs fois durant une nuit. Un cycle fait environ 90 minutes, mais avec de fortes variation individuelles. Concrètement, un cycle va de 30 minutes à 3 heures, mais la moyenne est proche de 1h30. Un cycle contient une phase de ''sommeil lent'' et de ''sommeil paradoxal''. Le '''sommeil à ondes lentes''' est une phase où les ondes cérébrales sont synchronisées, de basse fréquence et de forte amplitude. Tous les neurones émettent des potentiels d'action en même temps. Le '''sommeil paradoxal''' a une activité électrique proche de l'éveil : ondes de fréquence élevée, de faible amplitude, totalement désynchronisées.
[[File:Hypnogramme.svg|vignette|upright=2.0|Hypnogramme]]
{| class="wikitable"
|+ Stades du sommeil
|-
! scope="col"| Stade du sommeil|| Ondes cérébrales prédominantes
|-
| Eveil || Ondes Gamma, Bêta
|-
| Endormissement || Ondes Alpha
|-
| Sommeil lent léger || Ondes Thêta
|-
| Sommeil lent profond || Ondes Delta
|-
| Sommeil paradoxal|| Ondes Gamma, Bêta
|}
===Le sommeil à ondes lentes===
Lors du sommeil à ondes lentes, on observe une diminution de la fréquence des ondes cérébrales et une augmentation de leur amplitude. Le dormeur devient de plus en plus difficile à réveiller. Si on réveille quelqu'un lors du sommeil à ondes lentes, il se sentira fatigué. Le dormeur peut "rêver" lors du sommeil lent, mais les rêves en question sont souvent sans composants visuelle, ils sont relativement logiques et peu chargés émotionnellement. Disons que la plupart du temps, le sujet "pense" plus qu'il rêve.
Le sommeil à ondes lentes est lui aussi subdivisé en deux phases : le sommeil léger et le sommeil profond. La distinction n'est pas très claire, il y a parfois plusieurs subdivisions supplémentaires, mais les distinctions ne sont pas très claires, surtout basées sur des critères liés à la fréquence des ondes cérébrales.
Le '''sommeil léger''' dure environ une demi-heure chez un sujet d'âge moyen, mais peut être plus long chez les personnes âgées. Dans ce stade, le tonus musculaire diminue progressivement, les mouvements oculaires se font plus lents et de moindre intensité, la respiration et le rythme cardiaques deviennent plus réguliers et ralentissent un tout petit peu.
Le dormeur peut encore être réveillé facilement dans ce stade du sommeil. Du bruit, ou de la lumière peuvent réveiller un dormeur dans ce stade. D'où le fait qu'il soit appelé le sommeil ''léger''. De plus, le dormeur ne se sentira pas trop fatigué après le réveil, même si il aura une performance légèrement amoindrie. Quelques micro-réveils, inférieures à la dizaine de secondes, peuvent se produire spontanément dans ce stade. Le dormeur peut se souvenir de ces phases de réveil, qui sont parfois sources d'hallucinations hypnagogiques.
Sur l'EEG, on peut observer des bouffées d'ondes de haute fréquence (12-14 Hertz) appelées '''fuseaux du sommeil''' qui proviennent de la boucle thalamico-corticale et des ondes de haute amplitude qui apparaissent en réaction à des bruits extérieurs ('''complexes K''').
[[File:Stage2sleep.svg|centre|vignette|upright=2.0|Sommeil léger.]]
Lors du '''sommeil profond''', les ondes Delta s'installent. Le dormeur peut encore bouger. C'est d'ailleurs lors de cette phase que les somnambules sortent de leur lit ou que les terreurs nocturnes surviennent. De manière générale, une bonne partie de ce que les médecins appellent parasomnies survient dans ce stade du sommeil. Au-delà des parasomnies, le sommeil profond est lui aussi bien nommé. Si on réveille quelqu'un en sommeil profond, celui-ci se sentira fatigué et aura des performances fortement dégradées. La durée de cette baisse de performance est d'environ une demi-heure, une heure maximum.
Le sommeil profond aurait comme rôle principal de réparer le corps et de lui permettre de reposer. À l'appui de cette affirmation, on peut dire que l'hormone de croissance est surtout produite lors du sommeil profond. Cette hormone fait grandir les enfants, mais aide aussi les cellules et tissus à se réparer. La consommation énergétique du cerveau est aussi beaucoup diminuée lors du sommeil profond, comparé à l'éveil. Une nuit reposante est associée à des ondes lentes de grande amplitude lors du sommeil profond.
Ce stade du sommeil aurait aussi un rôle à jouer dans l'apprentissage et la mémorisation. Plus précisément, il aurait une influence positive sur la consolidation des souvenirs et des connaissances acquises dans la journée. Dans le détail, les ondes lentes du sommeil profond permettraient le transfert des informations apprises de l'hippocampe vers le néocortex.
===Le sommeil paradoxal (''Rapid Eye Movement Sleep'')===
Après le sommeil à onde lentes, le '''sommeil paradoxal''' s'installe. Si on l'appelle sommeil paradoxal, c'est parce que l'EEG est très proche de l'éveil. Les rêves sont courants lors du sommeil paradoxal, nettement plus que dans les autres phases du sommeil. De plus, les rêves du sommeil paradoxal ont tendance à être assez illogiques, incohérents, riches en émotions. Leur contenu est souvent incompréhensible, vague, bizarre.
De plus, les régions motrices du cerveau sont inactives, mais le dormeur est complètement paralysé. Seuls les yeux du dormeur peuvent encore bouger, ils effectuent beaucoup de mouvements rapides sous l'effet d'ondes cérébrales produites par la moelle épinière : les ondes ponto-geniculo-occipitales. Le rythme cardiaque et la respiration augmentent, de même que la pression artérielle. Enfin, le pénis entre en érection, ce qui est très utile pour savoir si un cas d'impuissance a une origine physique ou psychique.
Il faut noter que la paralysie observée lors du sommeil paradoxal est présente chez les mammifères, mais pas chez les oiseaux (règle générale avec de nombreuses exceptions). Ils peuvent dormir debout, avec la tête relevée, sans que le sommeil paradoxal ne leur coupe les jambes. Ils donnent ainsi l'impression d'être alertes alors qu'ils dorment, ce qui peut tromper un prédateur. Leur sommeil paradoxal est de plus nettement plus court que chez les mammifères : quelques dizaines de secondes chez certaines espèces, contre plusieurs dizaines de minutes chez les mammifères ! Et ils sont aussi beaucoup plus fréquents. En somme, ils ont des cycles de sommeils plus irréguliers, plus fréquents, plus courts.
==L'anatomie du sommeil et de l'éveil==
Certaines zones du cerveau ne sont actives qu'à l'état d'éveil, d'autres ne l'étant que lors du sommeil paradoxal ou du sommeil lent. L'identification de ces zones a permis de déterminer l'existence de trois systèmes : un qui maintient le sujet à l'état d'éveil, un autre actif uniquement lors du sommeil lent et un autre actif uniquement lors du sommeil paradoxal. Ces systèmes sont tous composés d'un ensemble d'aires cérébrales distinctes, qui interagissent entre elles, notamment pour s'inhiber mutuellement. Par exemple, le système d'éveil inhibe les systèmes du sommeil lent et paradoxal, et ces deux systèmes s'inhibent eux-mêmes mutuellement.
[[File:Centres de l'éveil et du sommeil.png|centre|vignette|upright=2.5|Centres de l'éveil et du sommeil]]
===Le réseau exécutif de l'éveil et le système activateur ascendant===
L'éveil se traduit par une activité électrique soutenue dans le cortex, riche en ondes cérébrales rapides et désynchronisée. Le centre de l'éveil a pour but de maintenir cette activité dans le cortex. Le centre de l'éveil a longtemps été pensé comme une structure unique. Mais on sait aujourd'hui que celui-ci regroupe des structures cérébrales distantes, regroupées dans ce qu'on appelle le '''réseau exécutif de l’éveil'''. La lésion de ces structures peut entraîner de sérieuses pertes de vigilance, qui peuvent aller jusqu’au coma profond.
{|
|[[File:Système d'éveil - cerveau.png|vignette|upright=2.5|Système d'éveil - cerveau]]
|[[File:NeuralCorrelatesOfConsciousness2.jpg|vignette|upright=2.0|Réseau exécutif de l’éveil et formation réticulée.]]
|}
Les premières aires cérébrales identifiées comme faisant partie du centre de l'éveil le furent en 1918, lors de l'épidémie de grippe espagnole. Durant cette période, le neurologue Constantin von Economo remarqua que certains de ses patients tombaient dans le coma suite à diverses séquelles neurologiques, tandis que d’autres étaient atteints de sévères insomnies. Il étudia quelles étaient les aires cérébrales lésées pouvant expliquer ce résultat.
Les patients léthargiques ou comateux avaient des lésions à l''''hypothalamus postérieur'''. Celui-ci envoie des axones dans l'ensemble du cortex, innervant celui-ci de façon diffuse. Plus précisément, on peut distinguer une contribution distincte du ''noyau inféro-mammilaire'' et du ''noyau latéral''. Les neurones du premier sont des neurones à orexine, tandis que les neurones du premier relâchent de l'histamine dans les structures cérébrales voisines, ce qui est cohérent avec l'implication de ces neurotransmetteurs dans l'état d'éveil.
Il est apparu que son action est en partie une action contre-inhibitrice. En effet, l'hypothalamus antérieur se connecte à des neurones GABAergiques, à l'action inhibitrice. Les neurones de l'hypothalamus inhiberaient ces neurones GABAergiques dispersés dans le cortex : cette inhibition de l'inhibition GABAergiques se traduit naturellement par une activation du cortex.
On peut aussi citer les noyaux du '''télencéphale basal''', qui émettent de l'acétylcholine et du glutamate, le principal d'entre eux étant le noyau basal de Meynert. Leur action est directement excitatrice sur le cortex.
Le '''thalamus''' a un rôle dans l'état de vigilance et dans le cycle veille-sommeil. Et plus précisément, les boucles thalamo-corticales. Lors de la veille, les boucles thalamo-corticales sont le lieu d'une activité cérébrale désynchronisée. Mais lors du sommeil lent, une activité rythmique prend place et il est aujourd'hui évident que ce comportement est dirigé par des neurones réticulaires, eux-mêmes commandés par des formations du tronc cérébral. Pour détailler le rôle du thalamus lors du sommeil, nous allons nous référer au schéma ci-dessous.
[[File:Thalamus, boucles thalamico-corticales et sommeil.png|centre|vignette|upright=2|Thalamus, boucles thalamico-corticales et sommeil.]]
Rappelons que les noyaux du thalamus sont presque tous des relais sensoriels, qui transmettent les informations sensorielles au cortex cérébral. Ces noyaux de relai sensoriels sont chapeautés par un noyau thalamique réticulaire, qui inhibe les noyaux relai. D'autres noyaux sont des relais corticaux, à savoir qu'ils reçoivent des axones du cortex et lui en renvoient. ces noyaux forment avec le cortex des circuits fermés, où le cortex se connecte au thalamus, qui lui-même innerve le cortex. Ces circuits fermés sont appelés des boucles thalamico-corticales. Lors de l'éveil, les noyaux de relai sensoriels sont actifs et transmettent les sensations au cortex. Mais lors du sommeil, le noyau réticulaire s'active et éteint les noyaux de relai sensoriel. Le cerveau est alors isolé du monde extérieur et le cortex fonctionne en circuit fermé. Seules les boucles thalamico-corticales fonctionnent et leur activité est gouvernée par le noyau réticulaire. Or, le noyau réticulaire thalamique a une activité rythmique, semblable à celle des ondes lentes du sommeil lent.
Mais le réseau exécutif de l'éveil n'est pas le seul à avoir une influence sur l'état de veille. Les observations et les autopsies sur des patients comateux montrent que tous ont des lésions de la formation réticulée, suggérant que celle-ci est une des aires de commande du thalamus. On peut voir le réseau exécutif de l’éveil postérieur comme un relai de la formation réticulée. Durant longtemps, les savants crurent que cette structure correspondait au centre de l'éveil, d'où le nom qui lui a été donné de '''système réticulé activateur ascendant'''.
Ce système est innervé par la plupart des afférences sensorielles, mais aussi par les structures corticales. On voyait autrefois ce système comme une sorte de centre qui recevait les sensations et les intégrait avant d'activer le cerveau en conséquence. Plus le cerveau était soumis à des stimulations sensorielles fortes (douleur, vision lumineuse, autre), plus l'activation cérébrale et l'éveil étaient forts. L'éveil était alors vu comme la conséquence d'une stimulation sensorielle suffisamment forte, tandis que le sommeil se manifestait quand les stimulations sensorielles devenaient insuffisantes. On sait aujourd'hui que cette vision est au moins partielle, trop simplifiée pour être exacte.
Le système réticulaire ascendant comprend un grand nombre de noyaux appartenant à la formation réticulée, les plus connus étant le locus coeruleus et les noyaux du raphé. À ceux-ci, il faut ajouter des noyaux cholinergiques, qui activent les neurones thalamiques. On peut aussi citer les noyaux réticulés bulbaires magnocellulaires, qui émettent de l'acétylcholine et du glutamate, deux neurotransmetteurs excitateurs. Ces derniers innervent aussi bien le thalamus que l'hypothalamus postérieur. Et enfin, citons les noyaux du noyau parabrachial, qui innervent directement le télencéphale basal. Le système réticulaire ascendant active l'ensemble du néocortex par l'intermédiaire de deux voies : une voie réticulo-thalamico-corticale qui passe par le thalamus, et une voie reticulo-hypothalamico-corticale qui fait relai dans l'hypothalamus.
===Le sommeil lent===
Lors de l'endormissement, les boucles thalamico-corticales deviennent insensibles aux stimulations extérieures. Leur activité électrique se résume à une oscillation rythmique, périodique, spontanée. Cela se produit quand le système de veille se désactive, cesse d'inhiber les neurones pacemaker du thalamus. De plus, l'activité électrique globale du cortex doit diminuer, ce qui implique une inhibition du locus coeruleus, du noyau du raphé, et des autres composants du système de veille.
Le système responsable de cette inhibition est localisé dans l'hypothalamus antérieur, et plus précisément dans l''''aire pré-optique'''. La preuve principale étant que toute lésion de cette aire entraîne une insomnie presque totale. Cette aire est sensible à l'adénosine et on peut la voir comme une sorte de capteur qui mesurerait la pression de sommeil, afin de déclencher le sommeil quand celle-ci atteint un niveau trop élevé. Cette aire pré-optique contient des neurones inhibiteurs GABAergiques, qui innerveraient l'ensemble du système d'éveil. Ainsi, quand la pression de sommeil est trop forte, ces neurones émettent suffisamment de GABA pour éteindre le système de veille. Cependant, l'aire pré-optique est inhibé par le réseau de l'éveil, ces deux systèmes s'inhibant mutuellement. L'aire pré-optique est notamment sensible à la sérotonine, l'adrénaline et l'acétylcholine, qui l'inhibent. Seuls les neurones à hypocrétine/orexine de l'hypothalamus n'influencent pas l'aire pré-optique. Cela suggère que ceux-ci stabiliseraient l'état d'éveil. D'ailleurs, la narcolepsie est en grande partie causée par une destruction de ces neurones à orexine.
Cependant, l'aire pré-optique n'est pas le seul mécanisme d'entrée en sommeil lent. Une destruction totale de l'aire pré-optique entraîne certes une diminution du temps passé en sommeil lent, ainsi que des difficultés d'endormissement, mais elle ne supprime pas le sommeil lent. D'autres systèmes du sommeil lent doivent donc exister. À l'heure actuelle, les chercheurs suspectent l'implication des neurones GABAergiques de la zone parafaciale du tronc cérébral. L'activation de ces neurones semble induire un état proche, si ce n'est identique, du sommeil lent. Ils agiraient par inhibition de la transmission glutamatinergique du noyau parabrachial.
===Le sommeil paradoxal===
Le sommeil paradoxal est commandé par un ensemble de noyaux spécialisés qui interagissent entre eux. Certains de ces noyaux, ainsi que les aires cérébrales du cortex, sont aussi actifs lors de l'éveil. C'est notamment le cas des noyaux cholinergiques, qui s'activent lors du sommeil paradoxal. La chimie du sommeil paradoxal est donc un intermédiaire entre le sommeil lent et l'éveil. La production d'acétylcholine est proche de celle de l'éveil, tandis que la production de monoamines et d'orexine est nulle.
Mais d'autres noyaux sont spécifiquement actifs lors du sommeil paradoxal. Leurs neurones sont appelés des '''neurones SP-on'''. Ceux-ci sont responsables de ce qui se passe lors du sommeil paradoxal. Ces noyaux SP-on contiennent les neurones glutaminergique du noyau sublatérodorsal, la partie ventrolatérale de la substance grise périacqueducale, le noyau reticularis pontis oralis (un noyau sensible à l'acétylcholine) et enfin le noyau paragigantocellulaire latéral. L'inhibition motrice du sommeil paradoxal est causée par l'activation du noyau sublatérodorsal. Ainsi, des lésions de ce noyau suppriment l'atonie musculaire lors du sommeil paradoxal. Ceux-ci émettent des afférences glutaminergiques vers des noyaux GABAergiques/glycinergique du myélencéphalequi innervent la moelle épinière. Les ondes géniculopontiques sont générés dans le tegmentum du mésencéphale.
Certaines structures du cerveau, n'appartenant pas à ce système, sont totalement désactivées lors du sommeil paradoxal. C'est notamment le cas du locus coeruleus et du noyau du raphé. Les neurones de ces aires sont donc appelés des '''neurones SP-off''', pour bien signaler qu'il s"éteignent lors du sommeil paradoxal. De manière générale, le cerveau ne sécrète presque pas de monoamine lors du sommeil paradoxal. L'activation des systèmes mono-aminergiques pendant le sommeil paradoxal interrompt d'ailleurs celui-ci. C'est ainsi que les anti-dépresseurs, qui favorisent la production de mono-amines, peuvent diminuer ou complètement abolir le sommeil paradoxal. Les inhibiteurs sélectifs de la sérotonine vont ainsi diminuer la durée de sommeil paradoxal, tandis que les tricycliques et les inhibiteurs de la monoamine oxydase vont parfois totalement l'abolir.
==Les troubles du sommeil==
Le sommeil peut facilement être troublé, que ce soit par des maladies, de l'anxiété, des médicaments, ou bien d'autres raisons. Le chapitre précédent a notamment abordé les troubles du sommeil liés aux troubles du rythme circadien. Mais ce sont loin d'être les seuls, comme tout insomniaque le sait déjà ! Mais leur classification est clairement assez difficile, plusieurs classifications existant à l'heure actuelle. De nos jours, les trois classifications qui font référence sont celles du DSM (le manuel de diagnostic des troubles mentaux), l'ICD (International Classification of Disease de l'OMS) et celle de l'ICSD (International Classification of Sleep Disorder). Mais ces trois classifications sont assez similaires, ce qui fait que les maladies du sommeil sont assez bien identifiées. Dans les grandes lignes, les troubles du sommeil se classent en quelques grands syndromes : les troubles du rythme circadien, l'insomnie, les hypersomnies, les parasomnies, les mouvements anormaux liés au sommeil et les apnées du sommeil. Il arrive aussi certaines maladies modifient la durée du sommeil, certaines pouvant faire disparaître totalement le sommeil : on parle alors d'agrypnie.
===L'insomnie===
L''''insomnie''' est juste une réduction de la quantité de sommeil, là où les hypersomnies correspondent à l'inverse, à savoir un excès de sommeil. Le syndrome le plus courant est clairement l'insomnie, qui frappe bien plus de monde que l'hypersomnie. La plupart des insomnies sont temporaires, liées à de l'anxiété ou une origine psychologique bénigne quelconque. Ces insomnies passent le plus souvent en quelques semaines ou quelques jours au mieux. Plus rares sont les insomnies organiques, dont la cause est totalement autre que psychologique. Elles peuvent provenir de la consommation d’existants ou de médicaments aux effets stimulants, mais aussi de troubles neurologiques ou psychiatriques un peu plus sérieux. Par exemple, les patients dépressifs ont souvent des insomnies (ou des hypersomnies, selon le cas) : ils ont notamment tendance à se réveiller plus tôt le matin et à se lever plus fréquemment la nuit. On observe aussi des insomnies assez sévères chez les bipolaires en phase maniaque : leurs besoins en sommeil diminuent aux alentours de 2 ou 3 heures de sommeil.
===Les hypersomnies===
Les '''hypersomnies''' sont plus rares, quoiqu'elles soient le second trouble du sommeil en termes de fréquence. La plupart ont des origines assez classiques : une réaction face un manque de sommeil, le résultat de problèmes psychologiques, l'arrivée de l'hiver, etc. Rarement, l'hypersomnie est liée à des maladies psychiatriques. Par exemple, la dépression peut entraîner des hypersomnies assez importantes chez certains patients. L'abus d'hypnotiques est aussi une raison assez évidente. Mais certaines maladies peuvent entraîner des hypersomnies assez sévères, assez idiosyncratiques. Ces maladies sont connues sous le nom de narcolepsie et de syndrome de Klein-Levin.
La '''narcolepsie''' est une maladie caractérisée par des endormissements soudains, en pleine journée. Ceux-ci sont caractéristiques de la narcolepsie, mais elle peut être couplée à des symptômes assez accessoires, comme une fatigue diurne et des insomnies. Les hallucinations lors de l'endormissement ou du réveil sont aussi assez fréquentes, bien que parfois absentes chez certaines narcoleptiques. Beaucoup de narcoleptiques sont aussi atteints de catalepsie, un syndrome caractérisé par une décontraction totale des muscles sans perte de conscience. Un patient cataleptique peut ainsi s'effondrer ou chuter soudainement, suite à une décontraction de tous ses muscles, bien que la catalepsie puisse se limiter à quelques muscles bien précis. Cela arrive suite à une émotion, ou spontanément. Chose étonnante, les endormissements soudains ne font pas passer le malade en sommeil lent, mais le font passer directement en sommeil paradoxal. La cause la plus probable de la narcolepsie serait un dysfonctionnement impliquant l'orexine, soit que les neurones à orexine dysfonctionnent, soit qu'une déficience en orexine se fasse jour dans le cerveau.
Certaines hypersomnies sont dites récurrentes, à savoir qu'elles surviennent "régulièrement". Le syndrome de ce type le plus connu est le '''syndrome de Klein-Levin''', bien qu'il soit particulièrement rare. Ce syndrome se manifeste par poussées d'hypersomnies, avec rémission complète entre les poussées. Lors des poussées, le patient dort beaucoup plus que la normale, mais ce n'est pas le seul symptôme décelable, des symptômes cognitifs ou psychiatriques sont aussi fréquents. Niveau psychiatrique, le sujet est irritable, fatigué, apathique, parfois dépressif. Plus grave, des manifestations psychiatriques plus importantes peuvent se produire : hallucinations, délire, hypersexualité, confusion mentale, délirium, variations d'humeur, etc. Des troubles cognitifs sont très fréquents : difficultés de concentration, troubles de la mémoire, et autres.
===Les troubles du comportement en sommeil paradoxal===
Les troubles du sommeil précédent dégradaient la durée du sommeil, mais quelques troubles ne rentrent pas dans ce cadre. Les '''troubles du comportement en sommeil paradoxal''', que nous abrégerons en TCSP, sont de ceux-là. Il s'agit de rêves agités ou cauchemardesques où le sujet se met à bouger, parfois au point d'avoir un comportement violent ou impulsif. Ce trouble ressemble quelque peu au somnambulisme, si ce n'est pour une différence de taille : si le somnambulisme est un trouble du sommeil lent, les phases de pseudo-réveil du TCSP ont lieu assez souvent 90 minutes après l'endormissement, lors du sommeil paradoxal. Ce trouble a une origine assez intéressante : il se traduit par une abolition de la paralysie lors du sommeil paradoxal. Des muscles qui devraient perdre tout leur tonus et ne pas se contracter lors du sommeil paradoxal vont en fait fonctionner comme lors de l'éveil.
Le TCSP peut avoir plusieurs causes, comme l'usage d'antidépresseurs. Ceux-ci ont tendance à réduire le sommeil paradoxal et à dégrader celui-ci. Il n'est donc pas étonnant que ces médicaments déclenchent de tels troubles du sommeil paradoxal. Mais la plupart du temps, ce trouble n'a pas de cause bien définie. Le TCSP est plus fréquent chez les sujets âgés, notamment chez ceux atteints de maladies neurodégénératives. Chose importante, plus de 30% des patients âgés atteints de TCSP déclarent des maladies neurodégénératives dans les années ou décennies qui suivent l'apparition du TCSP. Plus précisément, ils déclarent des synucléopathies, tel la maladie de Parkinson, la paralysie supra-nucléaire progressive, ou la démence à corps de Lewy. Le TCSP est inhabituel dans les tauopathies et amyloses.
<noinclude>
{{NavChapitre | book=Neurosciences
| prev=Les rythmes circadiens
| prevText=Les rythmes circadiens
| next=L'état de conscience
| nextText=L'état de conscience
}}{{autoCat}}
</noinclude>
1gfi4oumlp2yzbd2l1rwlj6mhz6tufo
775330
775329
2026-10-16T18:33:27Z
Mewtow
31375
/* L'anatomie du sommeil et de l'éveil */
775330
wikitext
text/x-wiki
On pourrait croire que le chapitre sur les rythmes circadiens vous a enseigné tout ce qu'il y a à savoir sur le sommeil, mais ce n'est pas le cas. S'il est vrai que le sommeil a une régularité similaire à celle du rythme circadien, l'horloge circadienne n'est pas la seule responsable de l'envie de dormir.
Les chercheurs ont remarqué que l'envie de sommeil n'avait parfois aucun rapport avec le rythme de cette horloge. Par exemple, le manque de sommeil augmente l'envie de dormir alors qu'il n'a pas d'effet sur l’horloge biologique. On peut l'observer en regardant non pas le sommeil, mais les autres paramètres qui évoluent avec une période de 24 heures (la température, par exemple). Ainsi, il est possible de désynchroniser l'envie de dormir du rythme circadien, suite à des privations de sommeil : le sujet aura envie de dormir à des horaires sans lien avec son rythme circadien.
Il y a donc quelque chose qui est capable d'induire le sommeil, sans que cela soit lié aux rythmes biologiques. Cette autre chose, les scientifiques lui ont donné les noms d'« homéostasie du sommeil » ou encore de « pression de sommeil ». Ce terme ne fait que formuler une évidence : plus on passe de temps éveillé, plus la fatigue se fait sentir et plus on a envie de dormir. Comme quoi, il fallait bien un chapitre complet sur le sommeil !
==La durée du sommeil==
Présent chez de nombreuses espèces animales, le sommeil est certainement apparu pour permettre aux animaux de conserver leur énergie. De nombreux animaux peu évolués peuvent tomber dans un état de torpeur, où ils restent immobiles durant plus ou moins longtemps, tandis que d'autres sont capables de dormir profondément. La durée de sommeil varie beaucoup chez les animaux, allant d'une absence quasi-totale à plus de 10 heures.
En moyenne, on estime qu'un être humain normal a besoin d'environ 8 à 9 heures de sommeil pour se sentir pleinement reposé. Cependant, cette moyenne cache une forte variabilité, vraisemblablement pour des raisons génétiques : les gros dormeurs doivent dormir 8 à 10 heures par nuit pour se sentir en forme, alors que les petits dormeurs se contentent de 3 à 4 heures.
De plus, l'âge rentre aussi en compte dans les besoins de sommeil, la durée idéale de sommeil diminuant progressivement jusqu’à la fin de l'adolescence. Un nouveau-né a tendance à dormir plus de 18 heures par jour, un enfant de 3 ans 11 à 13 heures, les adolescents ont besoin de 9 à 10 heures par jour et les adultes entre 7 et 8 heures. Contrairement à l'idée reçue, les personnes âgées dorment autant que les jeunes adultes, mais ont un sommeil plus fragmenté : elles dorment moins la nuit et se rattrapent le jour en faisant la sieste.
===La privation de sommeil===
Quand on manque de sommeil, tout se passe comme si le sujet en manque de sommeil avait une dette de sommeil à rattraper. Cela se remarque surtout par une diminution des performances du sujet : ses réactions sont plus lentes, il fait plus d'erreurs, sa concentration diminue, ses performances intellectuelles déclinent, etc.
Quelqu'un qui a mal dormit durant la nuit aura tendance à plus dormir que le lendemain pour rattraper sa dette de sommeil. Cependant, une nuit blanche est souvent rattrapée par quelques heures de sommeil supplémentaires. Plus que la durée du sommeil, c'est surtout la '''qualité du sommeil''', l'intensité du sommeil, qui permet de récupérer. Cette intensité dépend de la dette de sommeil accumulée, le sommeil étant plus réparateur après une restriction de sommeil. Et cette intensité du sommeil a une trace dans le cerveau, comme nous allons le voir dans la section suivante.
===Le ''Familial natural short sleep''===
Il existe des personnes qui ont besoin de très peu de sommeil, à peine 4 à 6 heures, pour se sentir reposées. Les personnes touchées n'expriment pas de perte de performances malgré cette courte durée de sommeil. Il s'agit d'une particularité génétique appelée ''Familial natural short sleep'', qui est très peu fréquente dans la population. Le ''Familial natural short sleep'' est un trait présent dès la naissance et dure toute la vie. De plus, il est associé à d'autres particularité : meilleure mémoire, sensibilité à la douleur diminuée, personnalité plus extravertie et optimiste.
N'allez pas croire que vous l'avez, beaucoup de monde pense avoir besoin de peu de sommeil et n'en ressent pas les conséquences cognitives, alors qu'ils sont en réalité en privation de sommeil et ont des performances dégradées. En réalité, cette particularité génétique est très rare. Les chercheurs n'ont identifié avec certitude qu'une cinquantaine de familles dans le monde avec cette particularité, et ces familles sont celles sur lesquelles la recherche génétique s'est concentrée. Mais quelques estimations disent que le chiffre réel est proche des 1% de la population. Les gènes concernés ne sont pas connus avec certitude, quelques candidats ont été rejetés (DEC2, ADRB1, autres). Il se pourrait que des gènes liés à l'orexine soient impliqués, mais sans certitude.
==Les stades du sommeil==
Le cerveau est très loin de rester inactif lors du sommeil. Même durant le sommeil, le cerveau a une activité électrique relativement soutenue. Pour rappel, nous avions vu dans le chapitre sur l'activité électrique du cerveau que l'activité électrique cérébrale se mesure avec un instrument composé de plusieurs électrodes placées à la surface du crâne : l''''électroencéphalographe''', ou EEG.
L'EEG permet de mesurer des rythmes électriques récurrents, appelés des ''rythmes cérébraux'', ou encore ''ondes cérébrales''. Ils sont classés arbitrairement dans 4 à 6 bandes de fréquences nommées alpha, bêta, gamma, delta, et thêta.
* Delta : Inférieur à 4 hertz
* Thêta : 4 à 7 hertz
* Alpha : 8 à 15 hertz
* Bêta : 16 à 31 hertz
* Gamma : Plus de 32 hertz
Les scientifiques ont remarqué que chaque bande de fréquence correspond à des niveaux d'éveil et de sommeil différents. L'activité électrique du cerveau varie entre le sommeil et l'éveil, et on peut observer des variations par rapport à la normale dans certains cas pathologiques. Si on observe l'EEG de quelqu'un qui dort, on remarque que le sommeil est organisé en '''phases de sommeil''', identifiables par l'apparition de certaines ondes cérébrales.
Lors de l'endormissement, les ondes alpha apparaissent par bouffées successives. Le sujet peut être réveillé facilement. Lors de cette phase, le sujet peut être pris d'hallucinations dites hypnagogiques, ainsi que de sursauts musculaires nommées myoclonies d’endormissement. Dans la majorité des cas, cela est tout à fait normal et n'a rien de pathologique (l'auteur en a déjà fait expérience, sans avoir de problème de santé particulier).
Une fois le sujet endormi, son sommeil est organisé en cycles, qui se répètent plusieurs fois durant une nuit. Un cycle fait environ 90 minutes, mais avec de fortes variation individuelles. Concrètement, un cycle va de 30 minutes à 3 heures, mais la moyenne est proche de 1h30. Un cycle contient une phase de ''sommeil lent'' et de ''sommeil paradoxal''. Le '''sommeil à ondes lentes''' est une phase où les ondes cérébrales sont synchronisées, de basse fréquence et de forte amplitude. Tous les neurones émettent des potentiels d'action en même temps. Le '''sommeil paradoxal''' a une activité électrique proche de l'éveil : ondes de fréquence élevée, de faible amplitude, totalement désynchronisées.
[[File:Hypnogramme.svg|vignette|upright=2.0|Hypnogramme]]
{| class="wikitable"
|+ Stades du sommeil
|-
! scope="col"| Stade du sommeil|| Ondes cérébrales prédominantes
|-
| Eveil || Ondes Gamma, Bêta
|-
| Endormissement || Ondes Alpha
|-
| Sommeil lent léger || Ondes Thêta
|-
| Sommeil lent profond || Ondes Delta
|-
| Sommeil paradoxal|| Ondes Gamma, Bêta
|}
===Le sommeil à ondes lentes===
Lors du sommeil à ondes lentes, on observe une diminution de la fréquence des ondes cérébrales et une augmentation de leur amplitude. Le dormeur devient de plus en plus difficile à réveiller. Si on réveille quelqu'un lors du sommeil à ondes lentes, il se sentira fatigué. Le dormeur peut "rêver" lors du sommeil lent, mais les rêves en question sont souvent sans composants visuelle, ils sont relativement logiques et peu chargés émotionnellement. Disons que la plupart du temps, le sujet "pense" plus qu'il rêve.
Le sommeil à ondes lentes est lui aussi subdivisé en deux phases : le sommeil léger et le sommeil profond. La distinction n'est pas très claire, il y a parfois plusieurs subdivisions supplémentaires, mais les distinctions ne sont pas très claires, surtout basées sur des critères liés à la fréquence des ondes cérébrales.
Le '''sommeil léger''' dure environ une demi-heure chez un sujet d'âge moyen, mais peut être plus long chez les personnes âgées. Dans ce stade, le tonus musculaire diminue progressivement, les mouvements oculaires se font plus lents et de moindre intensité, la respiration et le rythme cardiaques deviennent plus réguliers et ralentissent un tout petit peu.
Le dormeur peut encore être réveillé facilement dans ce stade du sommeil. Du bruit, ou de la lumière peuvent réveiller un dormeur dans ce stade. D'où le fait qu'il soit appelé le sommeil ''léger''. De plus, le dormeur ne se sentira pas trop fatigué après le réveil, même si il aura une performance légèrement amoindrie. Quelques micro-réveils, inférieures à la dizaine de secondes, peuvent se produire spontanément dans ce stade. Le dormeur peut se souvenir de ces phases de réveil, qui sont parfois sources d'hallucinations hypnagogiques.
Sur l'EEG, on peut observer des bouffées d'ondes de haute fréquence (12-14 Hertz) appelées '''fuseaux du sommeil''' qui proviennent de la boucle thalamico-corticale et des ondes de haute amplitude qui apparaissent en réaction à des bruits extérieurs ('''complexes K''').
[[File:Stage2sleep.svg|centre|vignette|upright=2.0|Sommeil léger.]]
Lors du '''sommeil profond''', les ondes Delta s'installent. Le dormeur peut encore bouger. C'est d'ailleurs lors de cette phase que les somnambules sortent de leur lit ou que les terreurs nocturnes surviennent. De manière générale, une bonne partie de ce que les médecins appellent parasomnies survient dans ce stade du sommeil. Au-delà des parasomnies, le sommeil profond est lui aussi bien nommé. Si on réveille quelqu'un en sommeil profond, celui-ci se sentira fatigué et aura des performances fortement dégradées. La durée de cette baisse de performance est d'environ une demi-heure, une heure maximum.
Le sommeil profond aurait comme rôle principal de réparer le corps et de lui permettre de reposer. À l'appui de cette affirmation, on peut dire que l'hormone de croissance est surtout produite lors du sommeil profond. Cette hormone fait grandir les enfants, mais aide aussi les cellules et tissus à se réparer. La consommation énergétique du cerveau est aussi beaucoup diminuée lors du sommeil profond, comparé à l'éveil. Une nuit reposante est associée à des ondes lentes de grande amplitude lors du sommeil profond.
Ce stade du sommeil aurait aussi un rôle à jouer dans l'apprentissage et la mémorisation. Plus précisément, il aurait une influence positive sur la consolidation des souvenirs et des connaissances acquises dans la journée. Dans le détail, les ondes lentes du sommeil profond permettraient le transfert des informations apprises de l'hippocampe vers le néocortex.
===Le sommeil paradoxal (''Rapid Eye Movement Sleep'')===
Après le sommeil à onde lentes, le '''sommeil paradoxal''' s'installe. Si on l'appelle sommeil paradoxal, c'est parce que l'EEG est très proche de l'éveil. Les rêves sont courants lors du sommeil paradoxal, nettement plus que dans les autres phases du sommeil. De plus, les rêves du sommeil paradoxal ont tendance à être assez illogiques, incohérents, riches en émotions. Leur contenu est souvent incompréhensible, vague, bizarre.
De plus, les régions motrices du cerveau sont inactives, mais le dormeur est complètement paralysé. Seuls les yeux du dormeur peuvent encore bouger, ils effectuent beaucoup de mouvements rapides sous l'effet d'ondes cérébrales produites par la moelle épinière : les ondes ponto-geniculo-occipitales. Le rythme cardiaque et la respiration augmentent, de même que la pression artérielle. Enfin, le pénis entre en érection, ce qui est très utile pour savoir si un cas d'impuissance a une origine physique ou psychique.
Il faut noter que la paralysie observée lors du sommeil paradoxal est présente chez les mammifères, mais pas chez les oiseaux (règle générale avec de nombreuses exceptions). Ils peuvent dormir debout, avec la tête relevée, sans que le sommeil paradoxal ne leur coupe les jambes. Ils donnent ainsi l'impression d'être alertes alors qu'ils dorment, ce qui peut tromper un prédateur. Leur sommeil paradoxal est de plus nettement plus court que chez les mammifères : quelques dizaines de secondes chez certaines espèces, contre plusieurs dizaines de minutes chez les mammifères ! Et ils sont aussi beaucoup plus fréquents. En somme, ils ont des cycles de sommeils plus irréguliers, plus fréquents, plus courts.
==L'anatomie du sommeil et de l'éveil==
Certaines zones du cerveau ne sont actives qu'à l'état d'éveil, d'autres ne l'étant que lors du sommeil paradoxal ou du sommeil lent. L'identification de ces zones a permis de déterminer l'existence de trois systèmes : un qui maintient le sujet à l'état d'éveil, un autre actif uniquement lors du sommeil lent et un autre actif uniquement lors du sommeil paradoxal. Ces systèmes sont tous composés d'un ensemble d'aires cérébrales distinctes, qui interagissent entre elles, notamment pour s'inhiber mutuellement. Par exemple, le système d'éveil inhibe les systèmes du sommeil lent et paradoxal, et ces deux systèmes s'inhibent eux-mêmes mutuellement.
[[File:Centres de l'éveil et du sommeil.png|centre|vignette|upright=2.5|Centres de l'éveil et du sommeil]]
Mais avant de détailler ces circuits neuronaux, prenons une vision globale et parlons des hémisphères cérébraux. Les oiseaux, les reptiles et de nombreuses autres espèces animales dorment un hémisphère cérébral à la fois. Pendant qu'un hémisphère dort, l'autre est en plein état de veille. Chez les oiseaux, les deux hémisphères peuvent être en sommeil, mais d'une manière asymétrique, un hémisphère étant plus endormit que l'autre. Le fait d'avoir un sommeil asymétrique entre hémisphères pourrait venir de l’absence du corps calleux chez les oiseaux et les reptiles.
Et c'est très différent de ce qu'on observe avec les mammifères. Chez les mammifères, les deux hémisphères cérébraux dorment en même temps. A l'exception des mammifères marins, comme les dauphins et les baleines. Il faut dire que dormir sous l'eau est assez compliqué : l'animal devient vulnérable aux prédateurs. Au passage, les dauphins et baleines ont des corps calleux très petits, assez peu développés si j'ose dire.
===Le réseau exécutif de l'éveil et le système activateur ascendant===
L'éveil se traduit par une activité électrique soutenue dans le cortex, riche en ondes cérébrales rapides et désynchronisée. Le centre de l'éveil a pour but de maintenir cette activité dans le cortex. Le centre de l'éveil a longtemps été pensé comme une structure unique. Mais on sait aujourd'hui que celui-ci regroupe des structures cérébrales distantes, regroupées dans ce qu'on appelle le '''réseau exécutif de l’éveil'''. La lésion de ces structures peut entraîner de sérieuses pertes de vigilance, qui peuvent aller jusqu’au coma profond.
{|
|[[File:Système d'éveil - cerveau.png|vignette|upright=2.5|Système d'éveil - cerveau]]
|[[File:NeuralCorrelatesOfConsciousness2.jpg|vignette|upright=2.0|Réseau exécutif de l’éveil et formation réticulée.]]
|}
Les premières aires cérébrales identifiées comme faisant partie du centre de l'éveil le furent en 1918, lors de l'épidémie de grippe espagnole. Durant cette période, le neurologue Constantin von Economo remarqua que certains de ses patients tombaient dans le coma suite à diverses séquelles neurologiques, tandis que d’autres étaient atteints de sévères insomnies. Il étudia quelles étaient les aires cérébrales lésées pouvant expliquer ce résultat.
Les patients léthargiques ou comateux avaient des lésions à l''''hypothalamus postérieur'''. Celui-ci envoie des axones dans l'ensemble du cortex, innervant celui-ci de façon diffuse. Plus précisément, on peut distinguer une contribution distincte du ''noyau inféro-mammilaire'' et du ''noyau latéral''. Les neurones du premier sont des neurones à orexine, tandis que les neurones du premier relâchent de l'histamine dans les structures cérébrales voisines, ce qui est cohérent avec l'implication de ces neurotransmetteurs dans l'état d'éveil.
Il est apparu que son action est en partie une action contre-inhibitrice. En effet, l'hypothalamus antérieur se connecte à des neurones GABAergiques, à l'action inhibitrice. Les neurones de l'hypothalamus inhiberaient ces neurones GABAergiques dispersés dans le cortex : cette inhibition de l'inhibition GABAergiques se traduit naturellement par une activation du cortex.
On peut aussi citer les noyaux du '''télencéphale basal''', qui émettent de l'acétylcholine et du glutamate, le principal d'entre eux étant le noyau basal de Meynert. Leur action est directement excitatrice sur le cortex.
Le '''thalamus''' a un rôle dans l'état de vigilance et dans le cycle veille-sommeil. Et plus précisément, les boucles thalamo-corticales. Lors de la veille, les boucles thalamo-corticales sont le lieu d'une activité cérébrale désynchronisée. Mais lors du sommeil lent, une activité rythmique prend place et il est aujourd'hui évident que ce comportement est dirigé par des neurones réticulaires, eux-mêmes commandés par des formations du tronc cérébral. Pour détailler le rôle du thalamus lors du sommeil, nous allons nous référer au schéma ci-dessous.
[[File:Thalamus, boucles thalamico-corticales et sommeil.png|centre|vignette|upright=2|Thalamus, boucles thalamico-corticales et sommeil.]]
Rappelons que les noyaux du thalamus sont presque tous des relais sensoriels, qui transmettent les informations sensorielles au cortex cérébral. Ces noyaux de relai sensoriels sont chapeautés par un noyau thalamique réticulaire, qui inhibe les noyaux relai. D'autres noyaux sont des relais corticaux, à savoir qu'ils reçoivent des axones du cortex et lui en renvoient. ces noyaux forment avec le cortex des circuits fermés, où le cortex se connecte au thalamus, qui lui-même innerve le cortex. Ces circuits fermés sont appelés des boucles thalamico-corticales. Lors de l'éveil, les noyaux de relai sensoriels sont actifs et transmettent les sensations au cortex. Mais lors du sommeil, le noyau réticulaire s'active et éteint les noyaux de relai sensoriel. Le cerveau est alors isolé du monde extérieur et le cortex fonctionne en circuit fermé. Seules les boucles thalamico-corticales fonctionnent et leur activité est gouvernée par le noyau réticulaire. Or, le noyau réticulaire thalamique a une activité rythmique, semblable à celle des ondes lentes du sommeil lent.
Mais le réseau exécutif de l'éveil n'est pas le seul à avoir une influence sur l'état de veille. Les observations et les autopsies sur des patients comateux montrent que tous ont des lésions de la formation réticulée, suggérant que celle-ci est une des aires de commande du thalamus. On peut voir le réseau exécutif de l’éveil postérieur comme un relai de la formation réticulée. Durant longtemps, les savants crurent que cette structure correspondait au centre de l'éveil, d'où le nom qui lui a été donné de '''système réticulé activateur ascendant'''.
Ce système est innervé par la plupart des afférences sensorielles, mais aussi par les structures corticales. On voyait autrefois ce système comme une sorte de centre qui recevait les sensations et les intégrait avant d'activer le cerveau en conséquence. Plus le cerveau était soumis à des stimulations sensorielles fortes (douleur, vision lumineuse, autre), plus l'activation cérébrale et l'éveil étaient forts. L'éveil était alors vu comme la conséquence d'une stimulation sensorielle suffisamment forte, tandis que le sommeil se manifestait quand les stimulations sensorielles devenaient insuffisantes. On sait aujourd'hui que cette vision est au moins partielle, trop simplifiée pour être exacte.
Le système réticulaire ascendant comprend un grand nombre de noyaux appartenant à la formation réticulée, les plus connus étant le locus coeruleus et les noyaux du raphé. À ceux-ci, il faut ajouter des noyaux cholinergiques, qui activent les neurones thalamiques. On peut aussi citer les noyaux réticulés bulbaires magnocellulaires, qui émettent de l'acétylcholine et du glutamate, deux neurotransmetteurs excitateurs. Ces derniers innervent aussi bien le thalamus que l'hypothalamus postérieur. Et enfin, citons les noyaux du noyau parabrachial, qui innervent directement le télencéphale basal. Le système réticulaire ascendant active l'ensemble du néocortex par l'intermédiaire de deux voies : une voie réticulo-thalamico-corticale qui passe par le thalamus, et une voie reticulo-hypothalamico-corticale qui fait relai dans l'hypothalamus.
===Le sommeil lent===
Lors de l'endormissement, les boucles thalamico-corticales deviennent insensibles aux stimulations extérieures. Leur activité électrique se résume à une oscillation rythmique, périodique, spontanée. Cela se produit quand le système de veille se désactive, cesse d'inhiber les neurones pacemaker du thalamus. De plus, l'activité électrique globale du cortex doit diminuer, ce qui implique une inhibition du locus coeruleus, du noyau du raphé, et des autres composants du système de veille.
Le système responsable de cette inhibition est localisé dans l'hypothalamus antérieur, et plus précisément dans l''''aire pré-optique'''. La preuve principale étant que toute lésion de cette aire entraîne une insomnie presque totale. Cette aire est sensible à l'adénosine et on peut la voir comme une sorte de capteur qui mesurerait la pression de sommeil, afin de déclencher le sommeil quand celle-ci atteint un niveau trop élevé. Cette aire pré-optique contient des neurones inhibiteurs GABAergiques, qui innerveraient l'ensemble du système d'éveil. Ainsi, quand la pression de sommeil est trop forte, ces neurones émettent suffisamment de GABA pour éteindre le système de veille. Cependant, l'aire pré-optique est inhibé par le réseau de l'éveil, ces deux systèmes s'inhibant mutuellement. L'aire pré-optique est notamment sensible à la sérotonine, l'adrénaline et l'acétylcholine, qui l'inhibent. Seuls les neurones à hypocrétine/orexine de l'hypothalamus n'influencent pas l'aire pré-optique. Cela suggère que ceux-ci stabiliseraient l'état d'éveil. D'ailleurs, la narcolepsie est en grande partie causée par une destruction de ces neurones à orexine.
Cependant, l'aire pré-optique n'est pas le seul mécanisme d'entrée en sommeil lent. Une destruction totale de l'aire pré-optique entraîne certes une diminution du temps passé en sommeil lent, ainsi que des difficultés d'endormissement, mais elle ne supprime pas le sommeil lent. D'autres systèmes du sommeil lent doivent donc exister. À l'heure actuelle, les chercheurs suspectent l'implication des neurones GABAergiques de la zone parafaciale du tronc cérébral. L'activation de ces neurones semble induire un état proche, si ce n'est identique, du sommeil lent. Ils agiraient par inhibition de la transmission glutamatinergique du noyau parabrachial.
===Le sommeil paradoxal===
Le sommeil paradoxal est commandé par un ensemble de noyaux spécialisés qui interagissent entre eux. Certains de ces noyaux, ainsi que les aires cérébrales du cortex, sont aussi actifs lors de l'éveil. C'est notamment le cas des noyaux cholinergiques, qui s'activent lors du sommeil paradoxal. La chimie du sommeil paradoxal est donc un intermédiaire entre le sommeil lent et l'éveil. La production d'acétylcholine est proche de celle de l'éveil, tandis que la production de monoamines et d'orexine est nulle.
Mais d'autres noyaux sont spécifiquement actifs lors du sommeil paradoxal. Leurs neurones sont appelés des '''neurones SP-on'''. Ceux-ci sont responsables de ce qui se passe lors du sommeil paradoxal. Ces noyaux SP-on contiennent les neurones glutaminergique du noyau sublatérodorsal, la partie ventrolatérale de la substance grise périacqueducale, le noyau reticularis pontis oralis (un noyau sensible à l'acétylcholine) et enfin le noyau paragigantocellulaire latéral. L'inhibition motrice du sommeil paradoxal est causée par l'activation du noyau sublatérodorsal. Ainsi, des lésions de ce noyau suppriment l'atonie musculaire lors du sommeil paradoxal. Ceux-ci émettent des afférences glutaminergiques vers des noyaux GABAergiques/glycinergique du myélencéphalequi innervent la moelle épinière. Les ondes géniculopontiques sont générés dans le tegmentum du mésencéphale.
Certaines structures du cerveau, n'appartenant pas à ce système, sont totalement désactivées lors du sommeil paradoxal. C'est notamment le cas du locus coeruleus et du noyau du raphé. Les neurones de ces aires sont donc appelés des '''neurones SP-off''', pour bien signaler qu'il s"éteignent lors du sommeil paradoxal. De manière générale, le cerveau ne sécrète presque pas de monoamine lors du sommeil paradoxal. L'activation des systèmes mono-aminergiques pendant le sommeil paradoxal interrompt d'ailleurs celui-ci. C'est ainsi que les anti-dépresseurs, qui favorisent la production de mono-amines, peuvent diminuer ou complètement abolir le sommeil paradoxal. Les inhibiteurs sélectifs de la sérotonine vont ainsi diminuer la durée de sommeil paradoxal, tandis que les tricycliques et les inhibiteurs de la monoamine oxydase vont parfois totalement l'abolir.
==Les troubles du sommeil==
Le sommeil peut facilement être troublé, que ce soit par des maladies, de l'anxiété, des médicaments, ou bien d'autres raisons. Le chapitre précédent a notamment abordé les troubles du sommeil liés aux troubles du rythme circadien. Mais ce sont loin d'être les seuls, comme tout insomniaque le sait déjà ! Mais leur classification est clairement assez difficile, plusieurs classifications existant à l'heure actuelle. De nos jours, les trois classifications qui font référence sont celles du DSM (le manuel de diagnostic des troubles mentaux), l'ICD (International Classification of Disease de l'OMS) et celle de l'ICSD (International Classification of Sleep Disorder). Mais ces trois classifications sont assez similaires, ce qui fait que les maladies du sommeil sont assez bien identifiées. Dans les grandes lignes, les troubles du sommeil se classent en quelques grands syndromes : les troubles du rythme circadien, l'insomnie, les hypersomnies, les parasomnies, les mouvements anormaux liés au sommeil et les apnées du sommeil. Il arrive aussi certaines maladies modifient la durée du sommeil, certaines pouvant faire disparaître totalement le sommeil : on parle alors d'agrypnie.
===L'insomnie===
L''''insomnie''' est juste une réduction de la quantité de sommeil, là où les hypersomnies correspondent à l'inverse, à savoir un excès de sommeil. Le syndrome le plus courant est clairement l'insomnie, qui frappe bien plus de monde que l'hypersomnie. La plupart des insomnies sont temporaires, liées à de l'anxiété ou une origine psychologique bénigne quelconque. Ces insomnies passent le plus souvent en quelques semaines ou quelques jours au mieux. Plus rares sont les insomnies organiques, dont la cause est totalement autre que psychologique. Elles peuvent provenir de la consommation d’existants ou de médicaments aux effets stimulants, mais aussi de troubles neurologiques ou psychiatriques un peu plus sérieux. Par exemple, les patients dépressifs ont souvent des insomnies (ou des hypersomnies, selon le cas) : ils ont notamment tendance à se réveiller plus tôt le matin et à se lever plus fréquemment la nuit. On observe aussi des insomnies assez sévères chez les bipolaires en phase maniaque : leurs besoins en sommeil diminuent aux alentours de 2 ou 3 heures de sommeil.
===Les hypersomnies===
Les '''hypersomnies''' sont plus rares, quoiqu'elles soient le second trouble du sommeil en termes de fréquence. La plupart ont des origines assez classiques : une réaction face un manque de sommeil, le résultat de problèmes psychologiques, l'arrivée de l'hiver, etc. Rarement, l'hypersomnie est liée à des maladies psychiatriques. Par exemple, la dépression peut entraîner des hypersomnies assez importantes chez certains patients. L'abus d'hypnotiques est aussi une raison assez évidente. Mais certaines maladies peuvent entraîner des hypersomnies assez sévères, assez idiosyncratiques. Ces maladies sont connues sous le nom de narcolepsie et de syndrome de Klein-Levin.
La '''narcolepsie''' est une maladie caractérisée par des endormissements soudains, en pleine journée. Ceux-ci sont caractéristiques de la narcolepsie, mais elle peut être couplée à des symptômes assez accessoires, comme une fatigue diurne et des insomnies. Les hallucinations lors de l'endormissement ou du réveil sont aussi assez fréquentes, bien que parfois absentes chez certaines narcoleptiques. Beaucoup de narcoleptiques sont aussi atteints de catalepsie, un syndrome caractérisé par une décontraction totale des muscles sans perte de conscience. Un patient cataleptique peut ainsi s'effondrer ou chuter soudainement, suite à une décontraction de tous ses muscles, bien que la catalepsie puisse se limiter à quelques muscles bien précis. Cela arrive suite à une émotion, ou spontanément. Chose étonnante, les endormissements soudains ne font pas passer le malade en sommeil lent, mais le font passer directement en sommeil paradoxal. La cause la plus probable de la narcolepsie serait un dysfonctionnement impliquant l'orexine, soit que les neurones à orexine dysfonctionnent, soit qu'une déficience en orexine se fasse jour dans le cerveau.
Certaines hypersomnies sont dites récurrentes, à savoir qu'elles surviennent "régulièrement". Le syndrome de ce type le plus connu est le '''syndrome de Klein-Levin''', bien qu'il soit particulièrement rare. Ce syndrome se manifeste par poussées d'hypersomnies, avec rémission complète entre les poussées. Lors des poussées, le patient dort beaucoup plus que la normale, mais ce n'est pas le seul symptôme décelable, des symptômes cognitifs ou psychiatriques sont aussi fréquents. Niveau psychiatrique, le sujet est irritable, fatigué, apathique, parfois dépressif. Plus grave, des manifestations psychiatriques plus importantes peuvent se produire : hallucinations, délire, hypersexualité, confusion mentale, délirium, variations d'humeur, etc. Des troubles cognitifs sont très fréquents : difficultés de concentration, troubles de la mémoire, et autres.
===Les troubles du comportement en sommeil paradoxal===
Les troubles du sommeil précédent dégradaient la durée du sommeil, mais quelques troubles ne rentrent pas dans ce cadre. Les '''troubles du comportement en sommeil paradoxal''', que nous abrégerons en TCSP, sont de ceux-là. Il s'agit de rêves agités ou cauchemardesques où le sujet se met à bouger, parfois au point d'avoir un comportement violent ou impulsif. Ce trouble ressemble quelque peu au somnambulisme, si ce n'est pour une différence de taille : si le somnambulisme est un trouble du sommeil lent, les phases de pseudo-réveil du TCSP ont lieu assez souvent 90 minutes après l'endormissement, lors du sommeil paradoxal. Ce trouble a une origine assez intéressante : il se traduit par une abolition de la paralysie lors du sommeil paradoxal. Des muscles qui devraient perdre tout leur tonus et ne pas se contracter lors du sommeil paradoxal vont en fait fonctionner comme lors de l'éveil.
Le TCSP peut avoir plusieurs causes, comme l'usage d'antidépresseurs. Ceux-ci ont tendance à réduire le sommeil paradoxal et à dégrader celui-ci. Il n'est donc pas étonnant que ces médicaments déclenchent de tels troubles du sommeil paradoxal. Mais la plupart du temps, ce trouble n'a pas de cause bien définie. Le TCSP est plus fréquent chez les sujets âgés, notamment chez ceux atteints de maladies neurodégénératives. Chose importante, plus de 30% des patients âgés atteints de TCSP déclarent des maladies neurodégénératives dans les années ou décennies qui suivent l'apparition du TCSP. Plus précisément, ils déclarent des synucléopathies, tel la maladie de Parkinson, la paralysie supra-nucléaire progressive, ou la démence à corps de Lewy. Le TCSP est inhabituel dans les tauopathies et amyloses.
<noinclude>
{{NavChapitre | book=Neurosciences
| prev=Les rythmes circadiens
| prevText=Les rythmes circadiens
| next=L'état de conscience
| nextText=L'état de conscience
}}{{autoCat}}
</noinclude>
t02vlyielc24oz7iaidm2mvenlw6v25
775331
775330
2026-10-16T18:53:46Z
Mewtow
31375
/* Les troubles du sommeil */
775331
wikitext
text/x-wiki
On pourrait croire que le chapitre sur les rythmes circadiens vous a enseigné tout ce qu'il y a à savoir sur le sommeil, mais ce n'est pas le cas. S'il est vrai que le sommeil a une régularité similaire à celle du rythme circadien, l'horloge circadienne n'est pas la seule responsable de l'envie de dormir.
Les chercheurs ont remarqué que l'envie de sommeil n'avait parfois aucun rapport avec le rythme de cette horloge. Par exemple, le manque de sommeil augmente l'envie de dormir alors qu'il n'a pas d'effet sur l’horloge biologique. On peut l'observer en regardant non pas le sommeil, mais les autres paramètres qui évoluent avec une période de 24 heures (la température, par exemple). Ainsi, il est possible de désynchroniser l'envie de dormir du rythme circadien, suite à des privations de sommeil : le sujet aura envie de dormir à des horaires sans lien avec son rythme circadien.
Il y a donc quelque chose qui est capable d'induire le sommeil, sans que cela soit lié aux rythmes biologiques. Cette autre chose, les scientifiques lui ont donné les noms d'« homéostasie du sommeil » ou encore de « pression de sommeil ». Ce terme ne fait que formuler une évidence : plus on passe de temps éveillé, plus la fatigue se fait sentir et plus on a envie de dormir. Comme quoi, il fallait bien un chapitre complet sur le sommeil !
==La durée du sommeil==
Présent chez de nombreuses espèces animales, le sommeil est certainement apparu pour permettre aux animaux de conserver leur énergie. De nombreux animaux peu évolués peuvent tomber dans un état de torpeur, où ils restent immobiles durant plus ou moins longtemps, tandis que d'autres sont capables de dormir profondément. La durée de sommeil varie beaucoup chez les animaux, allant d'une absence quasi-totale à plus de 10 heures.
En moyenne, on estime qu'un être humain normal a besoin d'environ 8 à 9 heures de sommeil pour se sentir pleinement reposé. Cependant, cette moyenne cache une forte variabilité, vraisemblablement pour des raisons génétiques : les gros dormeurs doivent dormir 8 à 10 heures par nuit pour se sentir en forme, alors que les petits dormeurs se contentent de 3 à 4 heures.
De plus, l'âge rentre aussi en compte dans les besoins de sommeil, la durée idéale de sommeil diminuant progressivement jusqu’à la fin de l'adolescence. Un nouveau-né a tendance à dormir plus de 18 heures par jour, un enfant de 3 ans 11 à 13 heures, les adolescents ont besoin de 9 à 10 heures par jour et les adultes entre 7 et 8 heures. Contrairement à l'idée reçue, les personnes âgées dorment autant que les jeunes adultes, mais ont un sommeil plus fragmenté : elles dorment moins la nuit et se rattrapent le jour en faisant la sieste.
===La privation de sommeil===
Quand on manque de sommeil, tout se passe comme si le sujet en manque de sommeil avait une dette de sommeil à rattraper. Cela se remarque surtout par une diminution des performances du sujet : ses réactions sont plus lentes, il fait plus d'erreurs, sa concentration diminue, ses performances intellectuelles déclinent, etc.
Quelqu'un qui a mal dormit durant la nuit aura tendance à plus dormir que le lendemain pour rattraper sa dette de sommeil. Cependant, une nuit blanche est souvent rattrapée par quelques heures de sommeil supplémentaires. Plus que la durée du sommeil, c'est surtout la '''qualité du sommeil''', l'intensité du sommeil, qui permet de récupérer. Cette intensité dépend de la dette de sommeil accumulée, le sommeil étant plus réparateur après une restriction de sommeil. Et cette intensité du sommeil a une trace dans le cerveau, comme nous allons le voir dans la section suivante.
===Le ''Familial natural short sleep''===
Il existe des personnes qui ont besoin de très peu de sommeil, à peine 4 à 6 heures, pour se sentir reposées. Les personnes touchées n'expriment pas de perte de performances malgré cette courte durée de sommeil. Il s'agit d'une particularité génétique appelée ''Familial natural short sleep'', qui est très peu fréquente dans la population. Le ''Familial natural short sleep'' est un trait présent dès la naissance et dure toute la vie. De plus, il est associé à d'autres particularité : meilleure mémoire, sensibilité à la douleur diminuée, personnalité plus extravertie et optimiste.
N'allez pas croire que vous l'avez, beaucoup de monde pense avoir besoin de peu de sommeil et n'en ressent pas les conséquences cognitives, alors qu'ils sont en réalité en privation de sommeil et ont des performances dégradées. En réalité, cette particularité génétique est très rare. Les chercheurs n'ont identifié avec certitude qu'une cinquantaine de familles dans le monde avec cette particularité, et ces familles sont celles sur lesquelles la recherche génétique s'est concentrée. Mais quelques estimations disent que le chiffre réel est proche des 1% de la population. Les gènes concernés ne sont pas connus avec certitude, quelques candidats ont été rejetés (DEC2, ADRB1, autres). Il se pourrait que des gènes liés à l'orexine soient impliqués, mais sans certitude.
==Les stades du sommeil==
Le cerveau est très loin de rester inactif lors du sommeil. Même durant le sommeil, le cerveau a une activité électrique relativement soutenue. Pour rappel, nous avions vu dans le chapitre sur l'activité électrique du cerveau que l'activité électrique cérébrale se mesure avec un instrument composé de plusieurs électrodes placées à la surface du crâne : l''''électroencéphalographe''', ou EEG.
L'EEG permet de mesurer des rythmes électriques récurrents, appelés des ''rythmes cérébraux'', ou encore ''ondes cérébrales''. Ils sont classés arbitrairement dans 4 à 6 bandes de fréquences nommées alpha, bêta, gamma, delta, et thêta.
* Delta : Inférieur à 4 hertz
* Thêta : 4 à 7 hertz
* Alpha : 8 à 15 hertz
* Bêta : 16 à 31 hertz
* Gamma : Plus de 32 hertz
Les scientifiques ont remarqué que chaque bande de fréquence correspond à des niveaux d'éveil et de sommeil différents. L'activité électrique du cerveau varie entre le sommeil et l'éveil, et on peut observer des variations par rapport à la normale dans certains cas pathologiques. Si on observe l'EEG de quelqu'un qui dort, on remarque que le sommeil est organisé en '''phases de sommeil''', identifiables par l'apparition de certaines ondes cérébrales.
Lors de l'endormissement, les ondes alpha apparaissent par bouffées successives. Le sujet peut être réveillé facilement. Lors de cette phase, le sujet peut être pris d'hallucinations dites hypnagogiques, ainsi que de sursauts musculaires nommées myoclonies d’endormissement. Dans la majorité des cas, cela est tout à fait normal et n'a rien de pathologique (l'auteur en a déjà fait expérience, sans avoir de problème de santé particulier).
Une fois le sujet endormi, son sommeil est organisé en cycles, qui se répètent plusieurs fois durant une nuit. Un cycle fait environ 90 minutes, mais avec de fortes variation individuelles. Concrètement, un cycle va de 30 minutes à 3 heures, mais la moyenne est proche de 1h30. Un cycle contient une phase de ''sommeil lent'' et de ''sommeil paradoxal''. Le '''sommeil à ondes lentes''' est une phase où les ondes cérébrales sont synchronisées, de basse fréquence et de forte amplitude. Tous les neurones émettent des potentiels d'action en même temps. Le '''sommeil paradoxal''' a une activité électrique proche de l'éveil : ondes de fréquence élevée, de faible amplitude, totalement désynchronisées.
[[File:Hypnogramme.svg|vignette|upright=2.0|Hypnogramme]]
{| class="wikitable"
|+ Stades du sommeil
|-
! scope="col"| Stade du sommeil|| Ondes cérébrales prédominantes
|-
| Eveil || Ondes Gamma, Bêta
|-
| Endormissement || Ondes Alpha
|-
| Sommeil lent léger || Ondes Thêta
|-
| Sommeil lent profond || Ondes Delta
|-
| Sommeil paradoxal|| Ondes Gamma, Bêta
|}
===Le sommeil à ondes lentes===
Lors du sommeil à ondes lentes, on observe une diminution de la fréquence des ondes cérébrales et une augmentation de leur amplitude. Le dormeur devient de plus en plus difficile à réveiller. Si on réveille quelqu'un lors du sommeil à ondes lentes, il se sentira fatigué. Le dormeur peut "rêver" lors du sommeil lent, mais les rêves en question sont souvent sans composants visuelle, ils sont relativement logiques et peu chargés émotionnellement. Disons que la plupart du temps, le sujet "pense" plus qu'il rêve.
Le sommeil à ondes lentes est lui aussi subdivisé en deux phases : le sommeil léger et le sommeil profond. La distinction n'est pas très claire, il y a parfois plusieurs subdivisions supplémentaires, mais les distinctions ne sont pas très claires, surtout basées sur des critères liés à la fréquence des ondes cérébrales.
Le '''sommeil léger''' dure environ une demi-heure chez un sujet d'âge moyen, mais peut être plus long chez les personnes âgées. Dans ce stade, le tonus musculaire diminue progressivement, les mouvements oculaires se font plus lents et de moindre intensité, la respiration et le rythme cardiaques deviennent plus réguliers et ralentissent un tout petit peu.
Le dormeur peut encore être réveillé facilement dans ce stade du sommeil. Du bruit, ou de la lumière peuvent réveiller un dormeur dans ce stade. D'où le fait qu'il soit appelé le sommeil ''léger''. De plus, le dormeur ne se sentira pas trop fatigué après le réveil, même si il aura une performance légèrement amoindrie. Quelques micro-réveils, inférieures à la dizaine de secondes, peuvent se produire spontanément dans ce stade. Le dormeur peut se souvenir de ces phases de réveil, qui sont parfois sources d'hallucinations hypnagogiques.
Sur l'EEG, on peut observer des bouffées d'ondes de haute fréquence (12-14 Hertz) appelées '''fuseaux du sommeil''' qui proviennent de la boucle thalamico-corticale et des ondes de haute amplitude qui apparaissent en réaction à des bruits extérieurs ('''complexes K''').
[[File:Stage2sleep.svg|centre|vignette|upright=2.0|Sommeil léger.]]
Lors du '''sommeil profond''', les ondes Delta s'installent. Le dormeur peut encore bouger. C'est d'ailleurs lors de cette phase que les somnambules sortent de leur lit ou que les terreurs nocturnes surviennent. De manière générale, une bonne partie de ce que les médecins appellent parasomnies survient dans ce stade du sommeil. Au-delà des parasomnies, le sommeil profond est lui aussi bien nommé. Si on réveille quelqu'un en sommeil profond, celui-ci se sentira fatigué et aura des performances fortement dégradées. La durée de cette baisse de performance est d'environ une demi-heure, une heure maximum.
Le sommeil profond aurait comme rôle principal de réparer le corps et de lui permettre de reposer. À l'appui de cette affirmation, on peut dire que l'hormone de croissance est surtout produite lors du sommeil profond. Cette hormone fait grandir les enfants, mais aide aussi les cellules et tissus à se réparer. La consommation énergétique du cerveau est aussi beaucoup diminuée lors du sommeil profond, comparé à l'éveil. Une nuit reposante est associée à des ondes lentes de grande amplitude lors du sommeil profond.
Ce stade du sommeil aurait aussi un rôle à jouer dans l'apprentissage et la mémorisation. Plus précisément, il aurait une influence positive sur la consolidation des souvenirs et des connaissances acquises dans la journée. Dans le détail, les ondes lentes du sommeil profond permettraient le transfert des informations apprises de l'hippocampe vers le néocortex.
===Le sommeil paradoxal (''Rapid Eye Movement Sleep'')===
Après le sommeil à onde lentes, le '''sommeil paradoxal''' s'installe. Si on l'appelle sommeil paradoxal, c'est parce que l'EEG est très proche de l'éveil. Les rêves sont courants lors du sommeil paradoxal, nettement plus que dans les autres phases du sommeil. De plus, les rêves du sommeil paradoxal ont tendance à être assez illogiques, incohérents, riches en émotions. Leur contenu est souvent incompréhensible, vague, bizarre.
De plus, les régions motrices du cerveau sont inactives, mais le dormeur est complètement paralysé. Seuls les yeux du dormeur peuvent encore bouger, ils effectuent beaucoup de mouvements rapides sous l'effet d'ondes cérébrales produites par la moelle épinière : les ondes ponto-geniculo-occipitales. Le rythme cardiaque et la respiration augmentent, de même que la pression artérielle. Enfin, le pénis entre en érection, ce qui est très utile pour savoir si un cas d'impuissance a une origine physique ou psychique.
Il faut noter que la paralysie observée lors du sommeil paradoxal est présente chez les mammifères, mais pas chez les oiseaux (règle générale avec de nombreuses exceptions). Ils peuvent dormir debout, avec la tête relevée, sans que le sommeil paradoxal ne leur coupe les jambes. Ils donnent ainsi l'impression d'être alertes alors qu'ils dorment, ce qui peut tromper un prédateur. Leur sommeil paradoxal est de plus nettement plus court que chez les mammifères : quelques dizaines de secondes chez certaines espèces, contre plusieurs dizaines de minutes chez les mammifères ! Et ils sont aussi beaucoup plus fréquents. En somme, ils ont des cycles de sommeils plus irréguliers, plus fréquents, plus courts.
==L'anatomie du sommeil et de l'éveil==
Certaines zones du cerveau ne sont actives qu'à l'état d'éveil, d'autres ne l'étant que lors du sommeil paradoxal ou du sommeil lent. L'identification de ces zones a permis de déterminer l'existence de trois systèmes : un qui maintient le sujet à l'état d'éveil, un autre actif uniquement lors du sommeil lent et un autre actif uniquement lors du sommeil paradoxal. Ces systèmes sont tous composés d'un ensemble d'aires cérébrales distinctes, qui interagissent entre elles, notamment pour s'inhiber mutuellement. Par exemple, le système d'éveil inhibe les systèmes du sommeil lent et paradoxal, et ces deux systèmes s'inhibent eux-mêmes mutuellement.
[[File:Centres de l'éveil et du sommeil.png|centre|vignette|upright=2.5|Centres de l'éveil et du sommeil]]
Mais avant de détailler ces circuits neuronaux, prenons une vision globale et parlons des hémisphères cérébraux. Les oiseaux, les reptiles et de nombreuses autres espèces animales dorment un hémisphère cérébral à la fois. Pendant qu'un hémisphère dort, l'autre est en plein état de veille. Chez les oiseaux, les deux hémisphères peuvent être en sommeil, mais d'une manière asymétrique, un hémisphère étant plus endormit que l'autre. Le fait d'avoir un sommeil asymétrique entre hémisphères pourrait venir de l’absence du corps calleux chez les oiseaux et les reptiles.
Et c'est très différent de ce qu'on observe avec les mammifères. Chez les mammifères, les deux hémisphères cérébraux dorment en même temps. A l'exception des mammifères marins, comme les dauphins et les baleines. Il faut dire que dormir sous l'eau est assez compliqué : l'animal devient vulnérable aux prédateurs. Au passage, les dauphins et baleines ont des corps calleux très petits, assez peu développés si j'ose dire.
===Le réseau exécutif de l'éveil et le système activateur ascendant===
L'éveil se traduit par une activité électrique soutenue dans le cortex, riche en ondes cérébrales rapides et désynchronisée. Le centre de l'éveil a pour but de maintenir cette activité dans le cortex. Le centre de l'éveil a longtemps été pensé comme une structure unique. Mais on sait aujourd'hui que celui-ci regroupe des structures cérébrales distantes, regroupées dans ce qu'on appelle le '''réseau exécutif de l’éveil'''. La lésion de ces structures peut entraîner de sérieuses pertes de vigilance, qui peuvent aller jusqu’au coma profond.
{|
|[[File:Système d'éveil - cerveau.png|vignette|upright=2.5|Système d'éveil - cerveau]]
|[[File:NeuralCorrelatesOfConsciousness2.jpg|vignette|upright=2.0|Réseau exécutif de l’éveil et formation réticulée.]]
|}
Les premières aires cérébrales identifiées comme faisant partie du centre de l'éveil le furent en 1918, lors de l'épidémie de grippe espagnole. Durant cette période, le neurologue Constantin von Economo remarqua que certains de ses patients tombaient dans le coma suite à diverses séquelles neurologiques, tandis que d’autres étaient atteints de sévères insomnies. Il étudia quelles étaient les aires cérébrales lésées pouvant expliquer ce résultat.
Les patients léthargiques ou comateux avaient des lésions à l''''hypothalamus postérieur'''. Celui-ci envoie des axones dans l'ensemble du cortex, innervant celui-ci de façon diffuse. Plus précisément, on peut distinguer une contribution distincte du ''noyau inféro-mammilaire'' et du ''noyau latéral''. Les neurones du premier sont des neurones à orexine, tandis que les neurones du premier relâchent de l'histamine dans les structures cérébrales voisines, ce qui est cohérent avec l'implication de ces neurotransmetteurs dans l'état d'éveil.
Il est apparu que son action est en partie une action contre-inhibitrice. En effet, l'hypothalamus antérieur se connecte à des neurones GABAergiques, à l'action inhibitrice. Les neurones de l'hypothalamus inhiberaient ces neurones GABAergiques dispersés dans le cortex : cette inhibition de l'inhibition GABAergiques se traduit naturellement par une activation du cortex.
On peut aussi citer les noyaux du '''télencéphale basal''', qui émettent de l'acétylcholine et du glutamate, le principal d'entre eux étant le noyau basal de Meynert. Leur action est directement excitatrice sur le cortex.
Le '''thalamus''' a un rôle dans l'état de vigilance et dans le cycle veille-sommeil. Et plus précisément, les boucles thalamo-corticales. Lors de la veille, les boucles thalamo-corticales sont le lieu d'une activité cérébrale désynchronisée. Mais lors du sommeil lent, une activité rythmique prend place et il est aujourd'hui évident que ce comportement est dirigé par des neurones réticulaires, eux-mêmes commandés par des formations du tronc cérébral. Pour détailler le rôle du thalamus lors du sommeil, nous allons nous référer au schéma ci-dessous.
[[File:Thalamus, boucles thalamico-corticales et sommeil.png|centre|vignette|upright=2|Thalamus, boucles thalamico-corticales et sommeil.]]
Rappelons que les noyaux du thalamus sont presque tous des relais sensoriels, qui transmettent les informations sensorielles au cortex cérébral. Ces noyaux de relai sensoriels sont chapeautés par un noyau thalamique réticulaire, qui inhibe les noyaux relai. D'autres noyaux sont des relais corticaux, à savoir qu'ils reçoivent des axones du cortex et lui en renvoient. ces noyaux forment avec le cortex des circuits fermés, où le cortex se connecte au thalamus, qui lui-même innerve le cortex. Ces circuits fermés sont appelés des boucles thalamico-corticales. Lors de l'éveil, les noyaux de relai sensoriels sont actifs et transmettent les sensations au cortex. Mais lors du sommeil, le noyau réticulaire s'active et éteint les noyaux de relai sensoriel. Le cerveau est alors isolé du monde extérieur et le cortex fonctionne en circuit fermé. Seules les boucles thalamico-corticales fonctionnent et leur activité est gouvernée par le noyau réticulaire. Or, le noyau réticulaire thalamique a une activité rythmique, semblable à celle des ondes lentes du sommeil lent.
Mais le réseau exécutif de l'éveil n'est pas le seul à avoir une influence sur l'état de veille. Les observations et les autopsies sur des patients comateux montrent que tous ont des lésions de la formation réticulée, suggérant que celle-ci est une des aires de commande du thalamus. On peut voir le réseau exécutif de l’éveil postérieur comme un relai de la formation réticulée. Durant longtemps, les savants crurent que cette structure correspondait au centre de l'éveil, d'où le nom qui lui a été donné de '''système réticulé activateur ascendant'''.
Ce système est innervé par la plupart des afférences sensorielles, mais aussi par les structures corticales. On voyait autrefois ce système comme une sorte de centre qui recevait les sensations et les intégrait avant d'activer le cerveau en conséquence. Plus le cerveau était soumis à des stimulations sensorielles fortes (douleur, vision lumineuse, autre), plus l'activation cérébrale et l'éveil étaient forts. L'éveil était alors vu comme la conséquence d'une stimulation sensorielle suffisamment forte, tandis que le sommeil se manifestait quand les stimulations sensorielles devenaient insuffisantes. On sait aujourd'hui que cette vision est au moins partielle, trop simplifiée pour être exacte.
Le système réticulaire ascendant comprend un grand nombre de noyaux appartenant à la formation réticulée, les plus connus étant le locus coeruleus et les noyaux du raphé. À ceux-ci, il faut ajouter des noyaux cholinergiques, qui activent les neurones thalamiques. On peut aussi citer les noyaux réticulés bulbaires magnocellulaires, qui émettent de l'acétylcholine et du glutamate, deux neurotransmetteurs excitateurs. Ces derniers innervent aussi bien le thalamus que l'hypothalamus postérieur. Et enfin, citons les noyaux du noyau parabrachial, qui innervent directement le télencéphale basal. Le système réticulaire ascendant active l'ensemble du néocortex par l'intermédiaire de deux voies : une voie réticulo-thalamico-corticale qui passe par le thalamus, et une voie reticulo-hypothalamico-corticale qui fait relai dans l'hypothalamus.
===Le sommeil lent===
Lors de l'endormissement, les boucles thalamico-corticales deviennent insensibles aux stimulations extérieures. Leur activité électrique se résume à une oscillation rythmique, périodique, spontanée. Cela se produit quand le système de veille se désactive, cesse d'inhiber les neurones pacemaker du thalamus. De plus, l'activité électrique globale du cortex doit diminuer, ce qui implique une inhibition du locus coeruleus, du noyau du raphé, et des autres composants du système de veille.
Le système responsable de cette inhibition est localisé dans l'hypothalamus antérieur, et plus précisément dans l''''aire pré-optique'''. La preuve principale étant que toute lésion de cette aire entraîne une insomnie presque totale. Cette aire est sensible à l'adénosine et on peut la voir comme une sorte de capteur qui mesurerait la pression de sommeil, afin de déclencher le sommeil quand celle-ci atteint un niveau trop élevé. Cette aire pré-optique contient des neurones inhibiteurs GABAergiques, qui innerveraient l'ensemble du système d'éveil. Ainsi, quand la pression de sommeil est trop forte, ces neurones émettent suffisamment de GABA pour éteindre le système de veille. Cependant, l'aire pré-optique est inhibé par le réseau de l'éveil, ces deux systèmes s'inhibant mutuellement. L'aire pré-optique est notamment sensible à la sérotonine, l'adrénaline et l'acétylcholine, qui l'inhibent. Seuls les neurones à hypocrétine/orexine de l'hypothalamus n'influencent pas l'aire pré-optique. Cela suggère que ceux-ci stabiliseraient l'état d'éveil. D'ailleurs, la narcolepsie est en grande partie causée par une destruction de ces neurones à orexine.
Cependant, l'aire pré-optique n'est pas le seul mécanisme d'entrée en sommeil lent. Une destruction totale de l'aire pré-optique entraîne certes une diminution du temps passé en sommeil lent, ainsi que des difficultés d'endormissement, mais elle ne supprime pas le sommeil lent. D'autres systèmes du sommeil lent doivent donc exister. À l'heure actuelle, les chercheurs suspectent l'implication des neurones GABAergiques de la zone parafaciale du tronc cérébral. L'activation de ces neurones semble induire un état proche, si ce n'est identique, du sommeil lent. Ils agiraient par inhibition de la transmission glutamatinergique du noyau parabrachial.
===Le sommeil paradoxal===
Le sommeil paradoxal est commandé par un ensemble de noyaux spécialisés qui interagissent entre eux. Certains de ces noyaux, ainsi que les aires cérébrales du cortex, sont aussi actifs lors de l'éveil. C'est notamment le cas des noyaux cholinergiques, qui s'activent lors du sommeil paradoxal. La chimie du sommeil paradoxal est donc un intermédiaire entre le sommeil lent et l'éveil. La production d'acétylcholine est proche de celle de l'éveil, tandis que la production de monoamines et d'orexine est nulle.
Mais d'autres noyaux sont spécifiquement actifs lors du sommeil paradoxal. Leurs neurones sont appelés des '''neurones SP-on'''. Ceux-ci sont responsables de ce qui se passe lors du sommeil paradoxal. Ces noyaux SP-on contiennent les neurones glutaminergique du noyau sublatérodorsal, la partie ventrolatérale de la substance grise périacqueducale, le noyau reticularis pontis oralis (un noyau sensible à l'acétylcholine) et enfin le noyau paragigantocellulaire latéral. L'inhibition motrice du sommeil paradoxal est causée par l'activation du noyau sublatérodorsal. Ainsi, des lésions de ce noyau suppriment l'atonie musculaire lors du sommeil paradoxal. Ceux-ci émettent des afférences glutaminergiques vers des noyaux GABAergiques/glycinergique du myélencéphalequi innervent la moelle épinière. Les ondes géniculopontiques sont générés dans le tegmentum du mésencéphale.
Certaines structures du cerveau, n'appartenant pas à ce système, sont totalement désactivées lors du sommeil paradoxal. C'est notamment le cas du locus coeruleus et du noyau du raphé. Les neurones de ces aires sont donc appelés des '''neurones SP-off''', pour bien signaler qu'il s"éteignent lors du sommeil paradoxal. De manière générale, le cerveau ne sécrète presque pas de monoamine lors du sommeil paradoxal. L'activation des systèmes mono-aminergiques pendant le sommeil paradoxal interrompt d'ailleurs celui-ci. C'est ainsi que les anti-dépresseurs, qui favorisent la production de mono-amines, peuvent diminuer ou complètement abolir le sommeil paradoxal. Les inhibiteurs sélectifs de la sérotonine vont ainsi diminuer la durée de sommeil paradoxal, tandis que les tricycliques et les inhibiteurs de la monoamine oxydase vont parfois totalement l'abolir.
==Les troubles du sommeil==
Le sommeil peut facilement être troublé, que ce soit par des maladies, de l'anxiété, des médicaments, ou bien d'autres raisons. Le chapitre précédent a notamment abordé les troubles du sommeil liés aux troubles du rythme circadien. Mais ce sont loin d'être les seuls, comme tout insomniaque le sait déjà ! Leur classification est clairement assez difficile, plusieurs classifications existant à l'heure actuelle.
De nos jours, les trois classifications qui font référence sont celles du DSM (le manuel de diagnostic des troubles mentaux), l'ICD (''International Classification of Disease'' de l'OMS) et celle de l'ICSD (''International Classification of Sleep Disorder''). Mais ces trois classifications sont assez similaires, ce qui fait que les maladies du sommeil sont assez bien identifiées. Dans les grandes lignes, les troubles du sommeil se classent en quelques grands syndromes : les troubles du rythme circadien, l'insomnie, les hypersomnies, les parasomnies, les mouvements anormaux liés au sommeil et les apnées du sommeil.
===Les insomnies et hypersomnies===
Les maladies les plus simples à aborder sont celles qui modifient la durée du sommeil. L'insomnie est une réduction de la durée du sommeil, l'hypersomnie est au contraire un sommeil qui dure trop longtemps. L'insomnie et l'hypersomnie se manifestent de manières très diverses, avec des causes et caractéristiques très différentes d'un cas à l'autre. Il n'y a ainsi rien de commun entre une insomnie liée au stress et une insomnie liée à une maladie à prion, par exemple. Dans le cas extrême où l'insomnie fait disparaître totalement le sommeil, on parle d'agrypnie.
L''''insomnie''' frappe bien plus de monde que l'hypersomnie. La plupart des insomnies sont temporaires, liées à de l'anxiété ou une origine psychologique bénigne. Elles passent le plus souvent en quelques semaines, quelques jours au mieux. Plus rares sont les insomnies organiques, dont la cause n'est pas psychologique. Elles peuvent provenir de la consommation d’excitants ou de médicaments, mais aussi de troubles neurologiques ou psychiatriques un peu plus sérieux. Par exemple, les patients dépressifs ont souvent des insomnies ou des hypersomnies, selon le cas. Ils ont notamment tendance à se réveiller plus tôt le matin et à se lever plus fréquemment la nuit. On observe aussi des insomnies assez sévères chez les bipolaires en phase maniaque, leurs besoins en sommeil diminuant aux alentours de 2 ou 3 heures de sommeil.
Les '''hypersomnies''' sont plus rares, quoiqu'elles soient le second trouble du sommeil en termes de fréquence. La plupart ont des origines assez classiques : une réaction face un manque de sommeil, le résultat de problèmes psychologiques, l'arrivée de l'hiver, etc. Rarement, l'hypersomnie est liée à des maladies psychiatriques. Par exemple, la dépression peut entraîner des hypersomnies assez importantes chez certains patients. L'abus d'hypnotiques est aussi une raison assez évidente. Mais il y a deux sous-types d'hypersomnies qui ont une origine purement neurologique : la narcolepsie et le syndrome de Klein-Levin.
La '''narcolepsie''' est une maladie caractérisée par des endormissements soudains, en pleine journée.Ils sont souvent accompagnés d'une fatigue diurne et d'insomnies. Les hallucinations lors de l'endormissement ou du réveil sont aussi assez fréquentes, bien que parfois absentes chez certaines narcoleptiques. Chose étonnante, les endormissements soudains ne font pas passer le malade en sommeil lent, mais le font passer directement en sommeil paradoxal. La cause la plus probable de la narcolepsie serait un dysfonctionnement impliquant l'orexine, soit que les neurones à orexine dysfonctionnent, soit qu'une déficience en orexine se fasse jour dans le cerveau.
Il existe deux sous-types de narcolepsies, sobrement nommé type 1 et type 2. Ils se distinguent sur un symptôme : la '''catalepsie''', à savoir une décontraction totale des muscles sans perte de conscience. Un patient cataleptique peut s'effondrer ou chuter soudainement, parce que tous ses muscles se décontractent d'un seul coup. De tels crises de catalepsie se déclenchent bien suite à une émotion, ou spontanément. Il arrive rarement que la catalepsie limite à quelques muscles bien précis.
La narcolepsie de type 1 est de la narcolepsie avec catalepsie, alors que le type 2 est sans. La cause du type 1 est connue : c'est la disparation des neurones à orexine dans le cerveau. La cause exacte qui tue ces neurones n'est pas connue, une origine auto-immune est suspectée. Pour la narcolepsie de type 2, un déficit en orexine est suspecté, mais la cause est totalement inconnue.
Le '''syndrome de Klein-Levin''' est particulièrement rare. Il se manifeste par poussées d'hypersomnies, avec rémission complète entre les poussées. Lors des poussées, le patient dort beaucoup plus que la normale, mais ce n'est pas le seul symptôme décelable, des symptômes cognitifs ou psychiatriques sont aussi fréquents. Niveau psychiatrique, le sujet est irritable, fatigué, apathique, parfois dépressif. Plus grave, des manifestations psychiatriques plus importantes peuvent se produire : hallucinations, délire, hypersexualité, confusion mentale, délirium, variations d'humeur, etc. Des troubles cognitifs sont très fréquents : difficultés de concentration, troubles de la mémoire, et autres.
===Les troubles du comportement en sommeil paradoxal===
Les troubles du sommeil précédent dégradaient la durée du sommeil, mais quelques troubles ne rentrent pas dans ce cadre. Les '''troubles du comportement en sommeil paradoxal''', que nous abrégerons en TCSP, sont de ceux-là. Il s'agit de rêves agités ou cauchemardesques où le sujet se met à bouger, parfois au point d'avoir un comportement violent ou impulsif. Ce trouble ressemble quelque peu au somnambulisme, si ce n'est pour une différence de taille : si le somnambulisme est un trouble du sommeil lent, les phases de pseudo-réveil du TCSP ont lieu assez souvent 90 minutes après l'endormissement, lors du sommeil paradoxal. Ce trouble a une origine assez intéressante : il se traduit par une abolition de la paralysie lors du sommeil paradoxal. Des muscles qui devraient perdre tout leur tonus et ne pas se contracter lors du sommeil paradoxal vont en fait fonctionner comme lors de l'éveil.
Le TCSP peut avoir plusieurs causes, comme l'usage d'antidépresseurs. Ceux-ci ont tendance à réduire le sommeil paradoxal et à dégrader celui-ci. Il n'est donc pas étonnant que ces médicaments déclenchent de tels troubles du sommeil paradoxal. Mais la plupart du temps, ce trouble n'a pas de cause bien définie. Le TCSP est plus fréquent chez les sujets âgés, notamment chez ceux atteints de maladies neurodégénératives. Chose importante, plus de 30% des patients âgés atteints de TCSP déclarent des maladies neurodégénératives dans les années ou décennies qui suivent l'apparition du TCSP. Plus précisément, ils déclarent des synucléopathies, tel la maladie de Parkinson, la paralysie supra-nucléaire progressive, ou la démence à corps de Lewy. Le TCSP est inhabituel dans les tauopathies et amyloses.
<noinclude>
{{NavChapitre | book=Neurosciences
| prev=Les rythmes circadiens
| prevText=Les rythmes circadiens
| next=L'état de conscience
| nextText=L'état de conscience
}}{{autoCat}}
</noinclude>
1cjmvdm7zqxwi9kyrhgpy7vb9ueg0ec
Fonctionnement d'un ordinateur/Langage machine et assembleur
0
65776
775332
775091
2026-10-16T20:29:40Z
Mewtow
31375
/* Les instructions sur les entiers BCD */
775332
wikitext
text/x-wiki
Ce chapitre va aborder le langage machine, à savoir un standard qui définit les instructions du processeur, le nombre de registres, etc. Dans ce chapitre, on considérera que le processeur est une boîte noire au fonctionnement interne inconnu.
Nous verrons le fonctionnement interne d'un processeur dans quelques chapitres. Les concepts que nous allons aborder ne sont rien d'autre que les bases nécessaires pour apprendre l'assembleur.
Nous allons surtout parler des '''instructions du processeur'''. Pour rappel, un processeur est conçu pour effectuer un nombre limité d'opérations bien précises, comme des calculs arithmétiques, des échanges de données avec la mémoire, etc. Ces opérations sont appelées des instructions. Les plus intuitives sont les instructions arithmétiques, comme l'addition, la soustraction, la multiplication, la division. Mais il y a aussi des instructions d'accès mémoire, qui échangent des données entre la mémoire RAM et le processeur, et d'autres instructions bien moins intuitives. Pour simplifier, on peut classer les instructions en quatre grands types :
* les échanges de données entre mémoires ;
* les calculs et autres opérations arithmétiques ;
* les instructions de comparaison ;
* les instructions de branchement.
À côté de ceux-ci, on peut trouver d'autres types d'instructions plus exotiques, pour gérer du texte, pour modifier la consommation en électricité de l'ordinateur, pour chiffrer ou de déchiffrer des données de taille fixe, générer des nombres aléatoires, etc. Mais elles sont plus rares.
Les instructions d'un processeur dépendent fortement du processeur utilisé. Le '''jeu d'instructions''' d'un processeur définit la liste des instructions supportées, ainsi que la manière dont elles sont encodées en mémoire. Le jeu d'instruction des PC actuels est le x86, un jeu d'instructions particulièrement ancien, apparu en 1978. Les macintoshs utilisent un jeu d'instruction différent : le PowerPC. Et il existe d'autres jeux d'instructions qui sont très utilisées dans le monde de l’informatique embarquée ou mobile, comme les architectures ARM, MIPS et SPARC.
Pour simplifier fortement, les jeux d'instructions se classent en deux types : les jeux d'instruction CISC ('' Complex Instruction Set Computer'') et RISC (''Reduced Instruction Set Computer''). La différence entre les deux est que les premiers gérent un grand nombre d'instructions, dont certaines sont très complexes, alors que les second préférent utiliser un petit nombre d'instructions simples. Mais nous détaillerons cela dans un chapitre dédié aux jeux d'instructions.
Dans ce chapitre, nous allons voir un aperçu assez large des instructions qui existent. Nous n'allons pas nous limiter aux instructions les plus importantes, mais nous allons aussi parler d'instructions assez exotiques, soit présentes uniquement sur d'anciens ordinateurs, soit très rares, soit franchement tordues. Et certaines de ces instructions exotiques sont assez impressionnantes et risquent de vous surprendre !
==Les instructions entières==
Les '''instructions arithmétiques''' sont les plus courantes et comprennent au minimum l'addition, la soustraction, la multiplication, plus rarement la division. Précisons qu'une opération mathématique ne se fait pas de la même manière suivant la représentation utilisée pour coder ces nombres : on ne manipule pas de la même façon des nombres entiers binaires, des entiers codés en BCD et des nombres flottants. Aussi, dans ce qui va suivre, nous allons séparer les opérations entières, les opérations flottantes et les opérations en BCD.
Les processeurs modernes utilisent des nombres encodés soit en binaire normal, soit en complément à deux. L'avantage est que les opérations arithmétiques se font de la même manière dans les deux encodages. Il y a quelques différences, mais elles sont suffisamment mineures, pas besoin d'avoir une instruction dédiée pour les nombres signés. Les anciens processeurs utilisaient eux des entiers signés en signe-magnitude ou en complément à 1, ou d'autres représentations. Et avec ces représentations, les calculs ne sont pas exactement les mêmes pour les nombres signés et non-signés. Ils avaient donc des instructions séparées pour l'addition signée et l'addition non-signée, idem pour la soustraction, la multiplication, etc.
De nos jours, les ordinateurs utilisent des entiers de taille fixe, qui tiennent dans un registre. J'ai dit qui tiennent dans un registre, car la taille des opérandes dépend de l'instruction. Un processeur pouvait supporter des entiers de taille différentes. Par exemple, un processeur 32 bits peut faire des calculs sur des opérandes 32 bits, 16 bits, voire 8 bits. Il dispose d'instructions de calculs pour chaque taille : certaines en 32 bits, d'autres en 16, d'autres en 8. Mais la taille d'une opérande ne dépasse pas la taille d'un registre entier.
===La ou les instructions d'addition entière===
Un processeur implémente au minimum une opération d'addition, même s'il existe de rares exemples de processeurs qui s'en passent. Mieux : certains processeurs implémentent plusieurs instructions d'addition, qui se distinguent par de subtiles différences.
La première est évidemment la '''taille des opérandes'''. Un processeur 32 bits peut par exemple implémenter trois additions différentes : une addition 32 bits qui additionne deux opérandes 32 bits : une addition 16 bits qui additionne deux doublets (16 bits), et une addition 8 bits qui additionne deux octets. Précisons cependant que c'est une possibilité et que ca n'a rien d'automatique. De nombreux processeurs ne fournissent que des additions sur des opérandes 32 bits, et rien d'autre,les autres taille d'opérandes ne sont pas supportées. Mais on peut toujours émuler des opérations sur 8 ou 16 bits avec des opérations de masquage.
La seconde différence est la '''gestion des débordements d'entier'''. Pour gérer les débordements d'entier, certains processeurs disposent d'un '''registre de débordement''', de 1 bit, qui indique si une instruction arithmétique a généré un débordement d'entier ou non. Il est mis à 1 en cas de débordement, mais reste à 0 sinon. Il est mis à jour à chaque addition/multiplication/soustraction.
Pour rappel, les débordements d'entiers sont différents selon que l'addition est signée ou non, et il faut en tenir compte au niveau du jeu d'instruction. La solution la plus simple est l'existence de deux instructions dédiées : une pour les additions de nombres signés et une autre pour les nombres non-signés. Le circuit additionneur est le même pour ces deux instruction, mais le calcul du bit de débordement est différent. D'autres processeurs s'en sortent autrement, en ayant deux bits de débordement : un si le résultat est signé, l'autre pour les résultats non-signés. Mais nous reparlerons de cela plus tard.
Une autre différence, présente sur les vieux processeurs, tient dans la '''gestion de la retenue''. À l'époque, les processeurs ne pouvaient gérer que des nombres de 4 à 8 bits, guère plus. Pourtant, la plupart des applications logicielles demandait d'utiliser des entiers de 16 bits. Les opérations comme l'addition ou la soustraction étaient alors réalisées en plusieurs fois. Par exemple, on additionnait les 8 bits de poids faible, puis les 8 bits de poids fort. Dans un cas pareil, il fallait aussi gérer la retenue dans l'addition des 8 bits de poids fort.
Pour cela, la retenue en question était mémorisée dans un registre de 1 bit dédié, semblable au registre de débordement, appelé le '''bit de retenue'''. De plus, les processeurs incorporaient une instruction d'addition qui additionnait les deux opérandes avec la retenue en question, instruction souvent appelée ADDC (''ADD with Carry''). Niveau circuits, cela ne changeait pas grand chose, tous les additionneurs ayant une entrée pour la retenue (qui est utilisée pour implémenter la soustraction, rappelez-vous). L'implémentation de l'instruction ADDC était très simple, n'utilisait presque pas de circuits, et rendait de fiers services aux programmeurs de l'époque. Nous parlons là des vieux processeurs 4 et 8, mais certains processeurs 16, 32, voire 64 bits, sont capables d'effectuer ce genre d'opération, même si ils sont rares.
: Il existe parfois des instructions pour mettre le bit de retenue à 0 ou à 1. C'est le cas sur l'architecture x86. Mais son utilité est marginale, car toutes les instructions arithmétiques modifient le bit de retenue.
La même chose a lieu pour la soustraction, qui demande qu'une "retenue" soit propagée d'une colonne à l'autre (bien que la "retenue" soit utilisée différemment : elle n'est pas additionnée, mais soustraite du résultat de la colonne). Pour cela, les processeurs implémentent l'opération SUBC (''Substract with Carry''). Et pour mettre en œuvre cette opération, ils ont deux options. La première est d'ajouter un bit pour la "retenue" de la soustraction. Pour une soustraction qui calcule a-b, elle vaut 0 si a>=b et 1 si a<b. Elle est utilisée telle qu'elle par le circuit qui effectue la soustraction, généralement un soustracteur. L'autre option est d'utiliser le but de retenue de l'addition, sans rien modifier. Pour comprendre pourquoi, rappelons que la soustraction est implémentée en complément à deux par le calcul suivant :
: <math>a - b = a + \overline{b} + 1</math>
Il s'agit d'une addition, qui a donc une retenue lors d'un débordement. On peut alors réutiliser le bit de retenue de l'addition pour la soustraction, en modifiant quelque peu la soustraction à effectuer. Pour comprendre comment faire la modification, précisons tout d'abord que les règles du complément à deux nous disent qu'on a un débordement quand a>=b. Passons maintenant à l'étude d'un exemple théorique : le cas où on veut soustraire deux nombres de 16 bits avec deux opérations SUBC qui travaillent sur 8 bits. Appelons les deux opérandes A et B, <math>a_h</math> et <math>b_h</math> les 8 bits de poids forts de ces opérandes, et <math>a_l</math> et <math>b_l</math> leurs 8 bits de poids faible. Voyons ce qui se passe suivant que la retenue soit de 0 ou 1.
* Si la retenue de l'addition est de 1, on a <math>a_l \geq b_l</math>, ce qui veut dire que la seconde soustraction doit calculer <math>a_h + \overline{b_h} + 1</math>.
* À l'inverse, si la retenue de l'addition est de 0, on a <math>a_l \leq b_l</math>. Cela veut dire que si on posait la soustraction, alors une retenue devrait être propagée à la colonne suivante, ce qui veut dire que la seconde soustraction doit calculer <math>a_h + \overline{b_h} + 1 - \text{retenue de 1}</math>.
Récapitulons dans un tableau.
{|class="wikitable"
|-
! Retenue
! Opération à effectuer pour la seconde soustraction
|-
! 0
| <math>a_h + \overline{b_h} + 0</math>
|-
! 1
| <math>a_h + \overline{b_h} + 1</math>
|}
On voit que ce qu'il faut ajouter à <math>a_h + \overline{b_h}</math> est égal à la retenue. L'opération SUBC fait donc le calcul suivant :
: <math>a - b = a + \overline{b} + \text{retenue}</math>
La plupart des processeurs 8 et 16 bits avaient deux instructions de soustraction séparées : une sans gestion de la retenue, une avec. Mais certains processeurs comme le 6502 n'avaient qu'une seule instruction de soustraction : celle qui tient compte de la retenue ! Il n'y avait pas de soustraction normale. Pour éviter tout problème, les programmeurs devaient faire attention. Ils disposaient d'une instruction mettre à 0 ou à 1 le bit de retenue pour éviter tout problème, qu'ils utilisaient avant toute soustraction simple.
===Les instructions de division entière===
Il faut savoir que la division est une opération très lourde pour le processeur : une instruction de division dédiée met au mieux une dizaine de cycles d'horloge pour fournir un résultat, parfois plus de 50 à 70 cycles d’horloge. Et elle est facilement 10 à 50 fois plus lente qu'une opération de multiplication. De plus, son implémentation matérielle utilise beaucoup de portes logiques/transistors. Son cout globalement le même que pour un circuit multiplieur, parfois un peu plus. Mais on a de la chance : c'est aussi une opération assez rare, peu utilisée.
De plus, les divisions les plus courantes sont des divisions par une constante : un programme devant manipuler des nombres décimaux aura tendance à effectuer des divisions par 10, un programme manipulant des durées pourra faire des divisions par 60 (gestion des minutes/secondes) ou 24 (gestion des heures). Or, diverses astuces permettent de les remplacer par des suites d'instructions plus simples, qui donnent le même résultat (l'usage de décalages, la multiplication par un entier réciproque, etc). Et la suite d'instruction équivalent est beaucoup plus rapide qu'une implémentation matérielle, car on profite qu'une opérande est constante. En-dehors de ce cas, l'usage des divisions est assez rare.
Sachant cela, certains processeurs ne possèdent pas d'instruction de division, car les gains de performance seraient modérés et le coût en transistors élevé. Les divisions sont alors émulées en logiciel, par une suite de soustractions ou tout autre code équivalent. D'autres processeurs implémentent toutefois la division dans une instruction machine, avec un circuit dédié, mais c'est signe que le coût en transistors n'est pas un problème pour le concepteur de la puce.
===Les instructions de multiplication entière===
Contrairement à la division, l'opération de multiplication entière est très courante, presque tous les processeurs modernes en ont une. Les multiplications sont plus fréquentes dans le code des programmes, sans compter que c'est une opération pas trop lourde à implémenter en circuit. Il en existe parfois plusieurs versions sur un même processeur. Ces différentes versions servent à résoudre un problème quant à la taille du résultat d'une multiplication.
Le résultat d'une multiplication est très large et prend deux registres. Par exemple, une multiplication de deux opérandes 64 bits donne un résultat sur 128 bits, ce qui ne tient pas dans un registre, mais en prend deux. Et en général, la multiplication de deux opérandes de <math>n</math> bits donne un résultat de <math>2 \times n</math>, soit deux fois plus de bits. Le résultat complet ne tient pas dans un registre, il en prend exactement deux. Pour résoudre ce problème, il existe plusieurs solutions.
Dans sa version la plus simple, l'instruction de multiplication se contente de garder les <math>n</math> bits de poids faible. Tous les processeurs qui supportent la multiplication ont une '''instruction MUL''' qui ne calcule que les bits de poids faible du résultat, dont le résultat est enregistré dans un seul registre. L'instruction peut générer des débordements d'entiers, si le résultat déborde dans les bits de poids fort.
Pour obtenir le résultat complet, le reste du processeur. Beaucoup de processeurs supportent juste l'instruction MUL, et ne permettent pas de calculer les bits de poids fort du résultat. Mais d'autres le permettent, avec des instructions spécialisées. Le processeur ajoute des instructions de multiplication, séparées de l'instruction MUL, qui permettent d'obtenir le résultat complet, directement ou indirectement. Le problème à résoudre est que le résultat prend deux registres. Globalement, il y a deux grandes solutions.
La première rajoute une '''instruction MULH''', pour calculer les bits de poids fort du résultat. Le processeur a donc deux instructions de multiplication : l'instruction MUL qui calcule les bits de poids faible du résultat, l'instruction MULH qui calcule les bits de poids fort. D'autres processeurs disposent d'une instruction de multiplication configurable, qui permet de choisir quels bits conserver : poids fort ou poids faible. Le calcul du résultat complet est donc fait en deux instructions, chacune n'écrivant son résultat que dans un seul registre.
La seconde rajoute une '''instruction MULX''' ou ''FULL MUL'', qui calcule les tout le résultat en une seule opération, et qui mémorisent le résultat dans deux registres. C'est plus rare, mais possible. Le défaut est que l'instruction doit préciser les deux registres dans lesquels enregistrer le résultat. Elle doit préciser le registre pour les bits de poids faible, un autre pour les bits de poids fort. Et préciser deux registres de résultat, pus deux registre d'opérande, c'est compliqué : les quatre registres prennent trop de place dans l'instruction, comme on le verra dans le chapitre sur l'encodage des instructions.
==Les instructions sur les entiers BCD==
Parlons maintenant des '''instructions BCD''', qui agissent sur des opérandes codées en BCD. Devenues très rares avec la disparition du BCD dans l'informatique grand-public, elles étaient quasiment essentielles sur les tous premiers processeurs 8 bits. De nos jours, elles subsistent encore sur les ''mainframes'' IBM, avec le jeu d'instruction z/architecture.
===Les ordinateurs décimaux===
Au tout début de l'informatique, il a existé des processeurs qui travaillaient uniquement en BCD, appelés des '''processeurs décimaux'''. Ils avaient uniquement des instructions BCD, pas d'instruction sur des opérandes codées en binaire. Ils étaient assez courants entre les années 60 et 70, même s'ils ne représentent pas la majorité des architectures de l'époque. Il y avait une vraie séparation entre processeurs décimaux et processeurs binaires, sans intermédiaires notables.
Les tous premiers ordinateurs décimaux pouvaient manipuler des entiers BCD de taille arbitraire. Ils stockaient leurs nombres dans des chaines de caractères ou des tableaux encodés en BCD. Le processeur faisait les calculs chiffre par chiffre, caractère BCD par caractère BCD. Les ordinateur décimaux qui ont abandonné ce fonctionnement. À la place, ils ont intégré des registres capables de mémoriser des entiers BCD de taille fixe.
Le grand avantage des processeurs décimaux est leur très bonne performance dans les tâches de bureautique, de comptabilité, et autres. Les processeurs de l'époque recevaient des entiers codés en BCD de la part des entrées-sorties, et devaient les traiter. Les processeurs binaires devaient faire des conversion BCD-binaire pour communiquer avec les entrées-sorties, mais pas les processeurs décimaux. Le gain en performance pouvait être substantiel dans certaines applications.
===Les instructions BCD des processeurs binaires===
Par la suite, dans les années 80, l'augmentation des performances a favorisé les processeurs binaires. Faire des conversions binaires-BCD n'était plus un problème, les conversions étaient suffisamment rapides. Par contre, faire les calculs en BCD était plus lent que faire les calculs en binaire, ce qui fait que le compromis technologique de l'époque favorisaient le binaire. Mais pour obtenir le meilleur des deux mondes, les processeurs binaires ont ajouté un support du BCD. C'était l'époque des architectures 8 bits, puis 16 bits.
Intuitivement, on se dit que, les processeurs de l'époque des 8 bits avaient leurs instructions de calcul en double : une instruction pour les calculs entier et une autre pour les calculs BCD. Mais d'autres processeurs faisaient autrement. À l'époque des processeurs 8-16 bits, certains processeurs ne géraient pas d'instructions BCD proprement dit, mais géraient quand même les calculs en BCD grâce à des instructions spéciales. Concrètement, les programmeurs utilisaient une instruction d'addition ou de soustraction binaire, mais il était possible de corriger ce résultat pour obtenir le bon résultat codé en BCD. Les processeurs de l'époque disposaient d'une instruction pour faire la correction, souvent appelée ''Decimal Adjust After Addition''.
Avant d'aller plus loin, nous devons préciser que les processeurs 8 bits de l'époque pouvaient gérer deux formats de BCD. Le premier est le '''format ''Packed''''', où deux chiffres sont mémorisé dans un octet. Le premier nibble (4bits) mémorise un chiffre BCD, le second nibble mémorise le second. Il s'agit d'un format qui remplit au maximum les bits de l'octet avec des données utiles. L'autre format, appelé '''''Unpacked''''', ne mémorise qu'un seul chiffre BCD dans un octet. Le chiffre est quasi- tout le temps dans le nibble de poids faible. Il se trouve que la manière de corriger le résultat d'une opération n'est pas la même suivant que le nombre est codé en BCD ''packed'' et ''unpacked''.
Pour commencer, étudions le cas des nombres BCD ''unpacked'', plus simples à comprendre. Lors d'une addition, il se peut que le résultat ne soit pas représentable en BCD ''unpacked''. Par exemple, si je fais le calcul 5 + 8, le résultat sera 13 : il tient dans un nibble en décimal, mais pas en BCD ! Cette situation survient quand le résultat d'une addition de deux entiers BCD ''unpacked'' dépasse 9, la limite de débordement en BCD ''unpacked''. Nous avons vu dans le chapitre sur les circuits d'addition que la correction est alors toute simple : si le résultat dépasse 9, on ajoute 6. De plus, on doit prévenir que le résultat a débordé et ne tient pas sur un nibble en BCD. Pour cela, on ajoute un bit spécialisé, appelé le '''''half carry flag''''', indicateur de demi-retenue en français, qui joue le même rôle que le bit de débordement, mais pour le BCD ''unpacked''. Sur les processeurs x86, tout cela est réalisé par une instruction appelée ''ASCII adjust after addition''.
: Pour la soustraction, il faut faire la même chose, sauf qu'il faut soustraire 6, pas l'ajouter. Et il y a aussi une instruction pour cela : ''ASCII adjust after substraction''.
Pour les nombres BCD ''packed'', la procédure est globalement la même, sauf qu'il faut traiter les deux ''nibbles'' : on ajoute 6 si le nibble de poids faible dépasse 9, puis on fait la même chose avec le ''nibble'' de poids fort. Le bit ''half carry flag'' n'est pas mis à jour et est tout simplement ignoré. Notons qu'on modifie d'abord le ''nibble'' de poids faible, avant de traiter celui de poids fort. En effet, si on corrige celui de poids faible, la retenue obtenue en ajoutant 6 se propage au nibble suivant, ce qui fait que ce dernier peut voir sa valeur augmenter. Une fois cela fait, le bit de retenue est mis à 1. La procédure est différente, vu qu'il faut traiter deux ''nibles'' au lieu d'un, et que le bit ''half-carry'' est ignoré au profit du bit de retenue normal. D'où l'existence d'une instruction séparée pour l'addition des nombres BCD ''packed'', appelée ''Décimal adjust after addition'' sur les processeurs x86.
Les processeurs modernes ne supportent plus le BCD, car il est très peu utilisé. Les processeurs ARM et MIPS ont toujours fait sans instructions BCD. Les processeurs x86 des PC désactivent les instructions BCD en mode 64 bits, mais conservent des opérations BCD en mode 32 bits, pour des raisons de compatibilité. Le support du codage BCD est surtout quelque chose qu'on trouve sur les anciens processeurs. Seuls les ''mainframes'' IBM gardent un support des entiers en BCD, avec le jeu d'instruction z/architecture.
==Les instructions flottantes==
Passons maintenant au support des nombres flottants. Avant les années 60, les processeurs n'était pas encore capables de faire de calculs avec des nombres flottants. Les calculs flottants étaient émulés soit par une bibliothèque logicielle, soit par le système d'exploitation. Pour améliorer les performances, les concepteurs de processeurs ont conçus des '''coprocesseurs arithmétiques''', des processeurs secondaires spécialisés dans les calculs flottants, qui complémentaient le processeur principal, et avaient leur propre emplacement sur la carte mère. Par la suite, les concepteurs de processeurs ont incorporé des instructions de calculs sur des nombres flottants, rendant le coprocesseur inutile.
Pour supporter les nombres flottants, il y a deux écoles. La première utilise des instructions flottantes séparées des instructions entières. La seconde utilise des instructions d'addition/soustraction/multiplication généralistes, capables de traiter indifféremment entiers et flottants, voire les encodages BCD.
===Les architectures taguées===
Le second cas correspond à certaines machines assez anciennes, datant des années 50 à 70. Ces processeurs n'avaient qu'une seule instruction d'addition, qui pouvait traiter indifféremment flottants, nombres entiers codés en BCD, en complément à deux, etc. Pour cela, les opérandes étaient associés à un type qui précisait s'il s'agissait d'un opérande flottant, entier, BCD, d'un pointeur, ou autre. Le traitement effectué par une instruction dépendait des ''tag'' associés aux opérandes. Par exemple, si les deux opérandes avaient un ''tag'' "entier", l'instruction faisait un calcul entier. Si les deux opérandes avaient un ''tag'' "flottant", l'instruction faisait un calcul flottant.
Le type de l'opérande était encodé avec un ''tag'', une sorte de numéro codé en binaire. Le ''tag'' était stocké en mémoire à côté de l'opérande, un mot mémoire contenait la donnée et le ''tag'' concaténés l'un à la suite de l'autre. Le défaut est que la mémoire devait être conçue pour gérer le ''tag'', de même que le processeur. Des processeurs de ce type s'appellent des ''architectures à tags'', ou '''tagged architectures''', en référence au ''tag'' ajouté dans la mémoire.
Mais les processeurs modernes n'utilisent pas cette technique, du fait de son cout en mémoire. À la place, le processeur dispose d'une instruction par type à manipuler : une instruction de multiplication pour les flottants, une autre pour les entiers codés en complément à deux, etc.
===Les processeurs avec des instructions flottantes dédiées===
Les processeurs modernes disposent d''''instructions de calculs flottants''', qui font des calculs avec des nombres flottants. En général, les instructions flottantes les plus courantes sont l'addition, la soustraction, la multiplication et la division. D'autres opérations plus complexes peuvent être supportées, comme les fonctions trigonométriques, les logarithmes, l'exponentielle, la fonction puissance, etc. Mais c'est déjà un peu plus rare, la majorité des processeurs anciens se contentent des quatres opérations de base.
: Nous palerons aussi d'''instructions flottantes'' pour désigner les ''instructions de calcul flottant''. Mais les deux termes ne sont pas strictement équivalent, car les ''instructions flottantes'' regroupent toutes les instructions flottantes, pas que les calculs flottants. La différence entre les deux deviendra plus claire quand on arrivera à la section sur les instructions d'accès mémoire.
Le standard IEEE 754 standardise quelques instructions flottantes : les quatre opérations arithmétiques de base, les comparaisons et la racine carrée. Certains processeurs vont plus loin et implémentent aussi d'autres instructions sur les flottants qui ne sont pas supportées par la norme IEEE 754. Par exemple, certaines fonctions mathématiques telles que sinus, cosinus, tangente, arctangente et d'autres. Le seul problème, c'est que ces instructions peuvent mener à des erreurs de calcul incompatibles avec la norme IEEE 754. Heureusement, les compilateurs peuvent mitiger ces désagréments.
Il faut néanmoins préciser que le support de la norme IEEE 754 n'est pas une obligation : certains processeurs ne la supportent que partiellement. Par exemple, certains processeurs ne supportent que les flottants simple précision, mais pas les double précision (ou inversement). Exemple : les premiers processeurs Intel ne géraient que les flottants double précision étendue. D'autres processeurs ne gèrent pas les ''underflow'', ''overflow'', les NaN, les infinis, ou même les flottants dénormaux. Précisons que même lorsque la gestion des dénormaux est implémentée en hardware (comme c'est le cas sur certains processeurs AMD), celle-ci reste malgré tout très lente.
Si les exceptions ne sont pas supportées, le processeur peut être configuré de façon à soit ignorer les exceptions en fournissant un résultat, soit déclencher une exception matérielle (à ne pas confondre avec les exceptions de la norme). Le choix du mode d'arrondi ou la gestion des exceptions flottantes se fait en configurant un registre dédié, intégré dans le processeur.
Plus rarement, il arrive que certains processeurs gèrent des formats de flottants spéciaux qui ne font pas partie de la norme IEEE 754. Par exemple, certains processeurs utilisent des formats de flottants différents de la norme IEEE 754, avec par exemple des flottants codés sur 8 ou 16 bits, qui sont très utiles pour les applications qui se contentent très bien d'un résultat approché.
===Les registres flottants===
En général, les processeurs utilisent des registres séparés pour les nombres flottants. Ils ont d'un côté des '''registres généraux''' pour les nombres entiers et les adresses mémoire, de l'autre des '''registres flottants''' pour les nombres flottants. Les instructions de calcul flottants utilisent ces ''registres flottants'' uniquement, elles ne peuvent pas lire/écrire dans les registres généraux, sauf pour quelques exceptions. La séparation entre registres flottants et entiers est la source de quelques subtilités, qu'on abordera dans la section sur les instructions d'accès mémoire.
Il existe cependant quelques exceptions, comme MIPS R3000. Il utilise des registres généraux qui mémorisent aussi bien entiers, adresses et flottants. Il a 32 registres généraux, de 32 bits chacun. Un registre général peut mémoriser un entier 32 bits, une adresse 32 bits, ou un flottant simple précision (32 bits). Mais il gérait aussi les flottants double précision. Pour cela, il utilisait une paire de registres généraux pour mémoriser un seul flottant double précision (64 bits). Le banc de registre était alors organisé comme 16 registres de 64 bits, qui regroupait un registre pair avec le registre impair (de numéro immédiatement suivant).
==Les instructions logiques==
À côté des instructions de calcul, on trouve des '''instructions logiques''' qui travaillent sur des bits ou des groupes de bits. Les opérations bit à bit ont déjà été vues dans les premiers chapitres, ceux sur l'électronique. Pour rappel, les plus courantes sont :
* La négation, ou encore le NON bit à bit, inverse tous les bits d'un nombre : les 1 deviennent des 0 et les 0 deviennent des 1.
* Le ET bit à bit, qui agit sur deux nombres : il va prendre les bits qui sont à la même place et va effectuer un ET (l’opération effectuée par la porte logique ET). Exemple : 1100·1010=1000.
* Les instructions similaires avec le OU ou le XOR.
Mais d'autres instructions sont intéressantes à étudier.
===Les instructions de décalage et de rotation===
Les '''instructions de décalage''' décalent tous les bits d'un nombre vers la gauche ou la droite. Pour rappel, il existe plusieurs types de décalages : les rotations, les décalages logiques, et les décalages arithmétiques. Nous avions déjà vu ces trois opérations dans le chapitre sur les circuits de décalage et de rotation, ce qui fait que nous allons simplement faire un rappel dans le tableau suivant. Pour résumer, voici la différence entre les trois opérations :
{|class="wikitable"
|-
!
! Schéma
! Gestion des bits sortants
! Remplissage des vides laissés par le décalage
|-
! Rotation
|[[File:Rotation de bits.png|Rotations de bits.]]
| Réinjectés dans les vides laissés par le décalage
| Remplis par les bits sortants
|-
! Décalage logique
|[[File:Décalage logique.png|Décalage logique.]]
| rowspan="2" | Les bits sortants sont oubliés
| Remplis par des zéros
|-
|-
! Décalage arithmétique
|[[File:Décalage arithmétique.png|Décalage arithmétique.]]
| Remplis par :
* le bit de signe pour les bits de poids fort ;
* des zéros pour les bits de poids faible .
|}
Pour rappel, les décalages de n rangs sont équivalents à une multiplication/division par 2^n. Un décalage à gauche est une multiplication par 2^n, alors qu'un décalage à droite est une division par 2^n. Les décalages logiques effectuent la multiplication/division pour un nombre non-signé (positif), alors que les décalages arithmétiques sont utilisés pour les nombres signés. Précisons cependant que pour ce qui est des décalages à droite, les décalages logiques et arithmétiques n'arrondissent pas le résultat de la même manière. Les décalages logiques à droite arrondissent vers zéro, alors que les décalages arithmétiques arrondissent vers la valeur inférieure (vers moins l'infini). De plus, les décalages à gauche entraînent des débordements d'entiers qui ne se gèrent pas de la même manière entre décalage logique et décalage arithmétique.
La plupart des processeurs stockent le bit sortant dans un registre, généralement le registre qui stocke le bit de retenue des additions qui est réutilisé pour cette optique. Ils possèdent aussi une variante des instructions de décalage où le bit de retenue est utilisé pour remplir le bit libéré au lieu de mettre un zéro. Cela permet d'enchaîner les décalages sur un nombre de bits plus grand que celui supporté par les instructions en procédant par morceaux. Par exemple, pour un processeur supportant les décalages sur 8 et 16 bits, il peut enchaîner deux décalages de 16 bits pour décaler un nombre de 32 bits ; ou bien un décalage de 8 bits et un autre de 16 bits pour décaler un nombre de 24 bits.
[[File:Shift Arithmetic Right.svg|centre|vignette|Décalage arithmétique à droite, où le bit sortant est mémorisé dans le bit de retenue (CF pour ''Carry Flag'').]]
Ils existe une variante des instructions de rotation où le bit de retenue est utilisé. Un bon exemple est celui des instructions LRCY (''Left Rotate Through Carry'') et RRCY (''Right Rotate Through Carry''). La première est un décalage/rotation à gauche, l'autre est un décalage/rotation à droite. Les deux décalent un registre de 16 bits, mais on pourrait imaginer la même chose avec des registres de taille différente. Lors du décalage, le bit sortant est mémorisé dans le registre de retenue, alors que le bit de retenue précédent est lui envoyé dans le vide laissé par le décalage. Tout se passe comme s'il s'agissait d'une rotation, sauf que le nombre décalé/rotaté est composé du registre concaténé au bit de retenue, le bit de retenue étant le bit de poids fort pour un décalage à gauche, de poids faible pour un décalage à droite.
====Le cas particuliers des processeurs ARM====
Sur les processeurs ARM, il n'y a pas d'instruction de décalage à proprement parler, ni de rotations. Par contre, les décalages sont fusionnés avec la plupart des opérations arithmétiques ! Toute opération arithmétique prend deux registres d'opérandes et un registre de destination, ainsi qu'un décalage facultatif. Le décalage est appliqué sur la seconde opérande automatiquement, sans avoir à faire une instruction séparée. À l'intérieur de l'instruction, deux bits précisent quel type de décalage effectuer (logique à droite, arithmétique à droite, à gauche, rotation à droite). Le nombre de rangs dont il faut décaler est soit encodé directement dans l'instruction sur 5-6 bits, soit précisé dans un registre.
Toutes les instructions ne permettent pas de faire un décalage automatique. Seules les opérations simples peuvent le faire, à savoir les additions, soustractions, copies d'un registre à un autre, les opérations logiques, et quelques autres. Et cela se marie très bien avec le fait qu'on applique le décalage sur la seconde opérande fait qu'il se marie très bien avec les calculs d'adresse. En effet, quand un programmeur manipule une structure de données, notamment un tableau, il arrive souvent qu'il fasse des calculs d'adresse cachés. La majeure partie de ces calculs d'adresse sont de la forme : adresse de base + (indice * taille de la donnée). Vu que les données ont très souvent une taille qui est une puissance de deux, le second terme se calcule en décalant l'indice, le calcul total demande une simple addition avec une opérande décalée, ce qui prend une seule instruction sur les processeur ARM.
===Les instructions de manipulation de bits===
D'autres opérations effectuent des calculs non pas bit à bit (on traite en parallèle les bits sur la même colonne), mais qui manipulent les bits à l'intérieur d'un nombre. les plus simples d'entre elles comptent sur les bits.
Une instruction très commune de ce type est l'instruction '''''population count''''', qui compte le nombre de bits d'un nombre qui sont à 1. Par exemple, pour le nombre 0100 1100 (sur 8 bits), la ''population count'' est de 3. Il s'agit d'une instruction utile dans les codes correcteurs d'erreur, très utilisés pour tout et n'importe quoi (trames réseau, sommes de contrôle des secteurs des disques dur, et bien d'autres). De plus, elle permet de calculer facilement le bit de parité d'un nombre, ce qui est utile pour les codes de détection d'erreur. En soi, l'instruction est facultative et l'implémenter est un choix qui n'est pas trivial. Mais cette instruction est très simple à implémenter en circuits, sans compter que son implémentation utilise assez peu de transistors. Le circuit de calcul est ridiculement simple, utilise peu de transistors.
Les processeurs gèrent aussi assez souvent des instructions pour compter les zéros ou les uns après le bit de poids fort/faible. Pour rappel, voici les quatre possibilités :
* '''''Count Trailing Zeros''''' donne le nombre de zéros situés à droite du 1 de poids faible.
* '''''Count Leading Zeros''''' donne le nombre de zéros situés à gauche du 1 de poids fort.
* '''''Count Trailing Ones''''' donnent le nombre de 1 situés à gauche du 0 de poids fort.
* '''''Count Leading Ones''''' donne le nombre de 1 situés à droite du 0 de poids faible.
[[File:Opérations Find First Set ; Find First Zero ; Find Highest Set (le logarithme binaire) ; Find Highest Zero ; Count Leading Zeros ; Count Trailing Zeros ; Count Leading Ones et Count Trailing Ones.png|centre|upright=2.0|vignette|Opérations Find First Set ; Find First Zero ; Find Highest Set (le logarithme binaire) ; Find Highest Zero ; Count Leading Zeros ; Count Trailing Zeros ; Count Leading Ones et Count Trailing Ones.]]
Comme vous le voyez sur le schéma du dessus, ces quatre opérations de comptage sont liées à quatre autres opérations. Ces dernières donnent la position du 0 ou du 1 de poids faible/fort :
* '''''Find First Set''''', donne la position du 1 de poids faible.
* '''''Find highest set''''' donne la position du 1 de poids fort.
* '''''Find First Zero''''' donne la position du 0 de poids faible.
* '''''Find Highest Zero''''' donne la position du 0 de poids fort.
Il est rare que des processeurs s’implémentent toutes ces opérations. En effet, le résultat de certaines opérations se calcule à partir des autres. Pour donner un exemple, les processeurs x86 modernes incorporent une extension, appelée '''''Bit manipulation instructions sets (BMI sets)''''', qui ajoute quelques instructions de ce type. Pour le moment, seules les instructions ''Count Trailing Zeros'' et ''Count Leading Zeros'' sont supportées.
Et il existe bien d'autres instructions de ce type. On peut citer, par exemple, l'instruction BLSI, qui ne garde que le 1 de poids faible et met à zéro tous les autres bits. L'instruction BLSR quant à elle, met à 0 ce 1 de poids faible. Et il y en a bien d'autres, qui impliquent le 1 de poids fort, le 0 de poids faible, le 1 de poids faible, etc.
===Les instructions d'extension de registres===
Certains processeurs sont capables de gérer des données de taille diverses. Ils peuvent par exemple gérer des données codées sur 16 bits ou sur 32 bits comme c'est le cas sur certains processeurs anciens. Comme autre exemple, les processeurs x86 modernes peuvent gérer des données de 8, 16 et 32 bits. Pour cela, ils disposent généralement de registres de taille différentes, certains font 8 bits, d'autres 16, d'autres 32, etc. Dans ce cas, il est courant que l'on ait à faire des conversions vers un nombre de taille plus grande. Par exemple, convertir un nombre de 8 bits en un nombre de 16 bits, ou un nombre de 8 bits en 32 bits. Les processeurs de ce type incorporent des instructions pour ce faire, qui s'appellent les '''instructions d'extension de nombres'''.
La seule difficulté de ces instructions tient à la manière dont on remplit les bits de poids forts. Par exemple, si l'on passe de 8 bits à 16 bits, les 8 bits de poids forts sont inconnus. Pareil si on passe de 16 bits à 32 bits : quelle doit être la valeur des 16 bits de poids fort ? Intuitivement, on se dit qu'il suffit de les remplir par des zéros. Faire ainsi marche très bien, mais à condition que le nombre soit un nombre positif ou nul. Mais dans le cas d'un nombre négatif, cela ne marche pas (le résultat n'est pas bon). Pour les nombres codés en complément à deux, il faut remplir les bits manquants par le bit de signe, le bit de poids fort. On a donc deux choix : soit on remplit les bits manquants par des 0, ou par le bit de poids fort. Globalement, les deux choix possibles correspondent à deux instructions : une pour les nombres non-signés et une autre pour les nombres codés en complément à deux. On parle respectivement d''''instruction de ''zero extension''''' et '''instruction d'extension de signe'''.
Formellement, les instructions d'extension de nombre sont des copies entre registres : le contenu d'un registre est copié dans un autre plus grand. L'extension de signe est couplée avec cette copie, les deux étant effectués en une instruction. Du moins, c'est le cas sur la plupart des processeurs. On pourrait imaginer séparer les deux en deux instructions séparées, une instruction MOV (qui copie un registre dans un autre, comme on le verra plus bas) et une instruction d'extension de signe/zéro. Dans ce cas, l'instruction d'extension de signe/zéro prend un registre de grande taille et étend la donnée dans ce registre. Par exemple, pour une extension de signe de 16 à 32 bits, l'instruction prendrait un registre de 32 bits, ne considérerait que les 16 bits de poids faible et effectuerait l'extension de signe/zéro à partir de ces derniers. En soi, l'extension avec des zéros est un simple masque, réalisable avec des opérations bit à bit et cette instruction n'a pas besoin d'être implémentée. Par contre, les instructions d'extension de signe sont elles très utiles.
De plus, une instruction d'extension de signe séparée a l'avantage d'être utilisable même sur des architecture avec des registres de taille identique. Mais quel intérêt, me direz-vous ? Il faut savoir que certaines langages de programmation permettent de travailler sur des entiers dont on peut préciser la taille. Un même programme peut ainsi manipuler des entiers de 16 bits et de 32 bits, ou des entiers de 8, d'autres de 16, d'autres de 32, et d'autres de 64. L'intérêt n'est as évident, mais un bon exemple est celui de la rétrocompatibilité entre programmes. Par exemple, un programme 64 bits qui utilise une librairie 32 bits, ou un programme codé en 64 bits qui émule une console 16 bits. Ou encore, la communication entre un programme codé en 16 bits avec un capteur qui mesure des données de 8 bits. Bref, les possibilités sont nombreuses. Imaginons que tout se passe dans des registres de 32 bits. Le processeur peut incorporer des instructions de calcul sur 16 bits, en plus des instructions 32 bits. Dans ce cas, l'extension de signe sert à faire des conversions entre entiers de taille différentes.
===Les instructions de permutation d'octets===
Les instructions de ''byte swap'', aussi appelée '''instructions de permutation d'octets''', échangent de place les octets d'un nombre. Leur implémentation varie grandement selon la taille des entiers. Le cas le plus simple est celui des instructions qui travaillent sur des entiers de 16 bits, soit deux octets. Il n'y a alors qu'une seule solution pour échanger les octets : l'octet de poids fort devient l'octet de poids faible et réciproquement. Les deux octets échangent leur place.
Pour les nombres de 32 bits, soit 4 octets, il y a plusieurs possibilités. La première inverse l'ordre des octets dans le nombre : on échange l'octet de poids faible et de poids fort, mais on échange aussi les deux octets restant entre eux. Une autre solution découpe l'entier en deux morceaux de 16 bits : l'un avec les deux octets de poids fort, l'autre avec les deux octets de poids faible. Les octets sont inversés dans ces blocs de 16 bitzs, mais on n'effectue pas d'autres échanges. On peut aussi échanger les deux morceaux de 16 bits, mais sans changer l'ordre des octets dans les blocs.
[[File:Instruction de permutation d'octets.png|centre|vignette|upright=2|Instruction de permutation d'octets]]
L'utilité de ces instructions n'est pas évidente au premier abord, mais elle sert beaucoup dans les opérations de conversion de données. Tout cela devrait devenir plus clair dans le chapitre sur le boutisme.
==Les instructions d'accès mémoire==
Les instructions d’accès mémoire permettent de copier ou d'échanger des données entre le processeur et la RAM. On peut ainsi copier le contenu d'un registre en mémoire, charger une donnée de la RAM dans un registre, initialiser un registre à une valeur bien précise, etc. Il en existe plusieurs, les plus connues étant les suivantes : LOAD, STORE et MOV. D'autres processeurs utilisent une instruction d'accès mémoire généraliste, plus complexe.
===Les instructions d'accès à la RAM : LOAD et STORE===
Les instructions LOAD et STORE sont deux instructions qui permettent d'échanger des données entre la mémoire RAM et les registres. Elles copient le contenu d'un registre dans la mémoire, ou au contraire une portion de mémoire RAM dans un registre.
* L''''instruction LOAD''' est une instruction de lecture : elle copie le contenu d'un ou plusieurs mots mémoire consécutifs dans un registre. Le contenu du registre est remplacé par le contenu des mots mémoire de la mémoire RAM.
* L''''instruction STORE''' fait l'inverse : elle copie le contenu d'un registre dans un ou plusieurs mots mémoire consécutifs en mémoire RAM.
{|class="wikitable"
|[[File:Instruction LOAD.png|300px|Instruction LOAD.]]
|[[File:Instruction STORE.png|300px|Instruction STORE.]]
|}
En théorie, une variante de l'instruction STORE peut enregistrer une constante en mémoire RAM. Il est en théorie possible pour une instruction STORE modifiée de stocker la valeur 9 dans l'adresse X, par exemple. La constante est fournie par l'instruction, elle n'est pas stockée dans un registre, mais carrément intégrée à l'instruction. Cela sera plus clair quand nous verrons les modes d'adressage, notamment le mode d'adressage immédiat.
Plus haut dans le chapitre, j'ai dit qu'un processeur pouvait supporter des entiers de taille différentes. Par exemple, un processeur 32 bits peut faire des calculs sur des opérandes 32 bits, 16 bits, voire 8 bits. Il dispose d'instructions de calculs dédiées, certaines en 32 bits, d'autres en 16, d'autres en 8. Et bien il en est de même pour les instructions LOAD et STORE, qui ont chacune une variante pour les entiers 8, 16, 32, 64 bits.
De telles instructions ne font pas que lire ou écrire des données. Pour comprendre pourquoi, prenons un processeur 64 bits. Il dispose d'un bus de 64 bits, pour lire ou écrire des mots mémoire de 64 bits chacun. Pour des lectures de 8, 16 ou 32 bits, un mot mémoire entier est lu, puis seuls les octets adéquats sont conservés. Par exemple, pour une lecture 32 bits, un mot mémoire de 64 bits est lu, puis une opération de masquage (ET bit à bit) met à zéro le 32 bits inutiles. Ce sont souvent les 32 bits de poids faible ou de poids fort qui sont mis à zéro. Il en est de mêma vec les lectures 16 bits, sauf que les 48 bits inutiles sont alors mis à zéro, idem avec les lectures 8 bits.
Pour les écritures, les choses sont encore plus compliquées. Imaginez que je veuille modifier 32 bits. Les 32 bits à modifier font partie d'un mot mémoire de 64 bits, qui contient les 32 bits utiles, et 32 bits supplémentaires. l'écriture ne doit pas effacer les 32 bits supplémentaires, seulement les 32 bits utiles. Pour cela, le processeur n'a pas le : il doit lire un mot mémoire de 64 bits, modifier le 32 bits utiles, puis réécrire le tout en mémoire RAM. L'écriture demande de faire une lecture, une modification, puis une écriture. Le mot mémoire de 64 bits est copié dans un registre, la modification se fait dans ce registre, vient une opération de masquage complexe, puis l'écriture est effectuée.
===Les instructions de transfert entre registres : MOV et XCHG===
L''''instruction MOV''' copie le contenu d'un registre dans un autre sans passer par la mémoire. C'est donc un échange de données entre registres, qui n'implique en rien la mémoire RAM, mais MOV est quand même considérée comme une instruction d'accès mémoire. Les données sont copiées d'un registre source vers un registre de destination. Le contenu du registre source est conservé, alors que le contenu du registre de destination est écrasé (il est remplacé par la valeur copiée). Cette instruction est utile pour gérer les registres, notamment sur les architectures avec peu de registres et/ou sur les architectures avec des registres spécialisés.
[[File:Instruction MOV.png|centre|vignette|upright=1.5|Instruction MOV.]]
Quelques rares architectures ne disposent pas d'instruction MOV, qui n'est formellement pas nécessaire, même si bien utile. Et au pire, si elles ont besoin de faire un MOV, elles émulent l'instruction MOV à partir d'autres instructions. L'idée utilise des opérations idempotentes, à savoir que le résultat de l'opération est égal à son opérande. Une possibilité est de faire un ET/OU entre un registre et lui-même : le résultat est égal au contenu du registre opérande. Une autre façon de faire additionne un registre avec zéro, et mémorise le résultat dans le registre de destination. L'addition peut être remplacée par un ET bit à bit, voire un OU bit à bit.
Quelques processeurs assez rares ont des instructions pour échanger le contenu de deux registres. Par exemple, on peut citer l''''instruction XCHG''' sur les processeurs x86 des PC anciens et actuels. Elle permet d'échanger le contenu de deux registres, quel qu'ils soient, il n'y a pas de restrictions sur le registre source et sur le registre de destination. Mais d'autres processeurs ont des restrictions sur les registres source et destination. Par exemple, le processeur Z80 a des instructions d'échanges assez restrictives, comme on le verra dans quelques chapitres.
Sur les processeurs ARM, l'instruction MOV fait partie des instructions qui permettent d'effectuer un décalage sur la seconde opérande. Il s'agit en réalité de décalages/rotations décalées, qui sont fusionnées avec une instruction MOV. L'instruction MOV des processeurs ARM fait soit un MOV normal, soit un décalage.
===Les instructions pour initialiser un registre===
L'instruction MOV permet de copier un registre dans un autre. Il existe aussi des instructions qui copient une valeur constante dans un registre. Par exemple, il est possible d'initialiser un registre à zéro, à 5, à 6, à 256, etc. Il existe deux méthodes pour cela. La première utilise des '''instructions SET''' séparées, qui initialisent le registre sélectionné à la valeur voulue. La seconde solution réutilise l'''instruction MOV'', pour qu'elle copie une constante dans le registre. L'instruction MOV peut alors fonctionner suivant deux modes : un "mode registre" où elle copie un registre dans un autre, un "mode immédiat" où elle copie une constante dans le registre destination. En pratique, c'est presque toujours un '''MOV immédiat''' qui est utilisé. La raison du pourquoi deviendra plus claire quand on verra les modes d'adressage.
[[File:Instruction MOV immédiate.png|centre|vignette|upright=1.5|Instruction MOV immédiate]]
La constante est intégrée directement dans l'instruction. Pour rappel, une instruction est encodée par une suite de bits en mémoire RAM. Elle prend en général 32 bits, c'est la norme sur les jeux d'instruction RISC (simples, peu d'instruction) actuels. Sur ces 32 bits, quand les 16 premiers bits indiquent que c'est un MOV utilisant une constante, les 16 restants sont utilisés pour encoder la constante. Le résultat est qu'il est facile d'initialiser un registre avec une valeur comprise entre 0 et 65 535. Mais au-delà, il faut ruser.
Pour charger une constante de 32/64 bits sur ces processeurs, il faut s'y prendre en plusieurs fois. Et les processeurs ARM fournissent de quoi simplifier le processeur. Ils disposent pour cela deux instructions MOVZ et MOVK. L'instruction MOVZ charge les 16 premiers bits et met à zéro les 48 bits de poids fort. Puis, 2 à 4 instructions MOVK remplissent le reste. MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). Par exemple, pour charger une constante 32 bits, on charge les 16 premiers bits avec MOVZ, puis les 16 bits de poids fort avec MOVK. Avec une constante codée sur 48 bits, il faudrait rajouter une troisième instruction MOVK pour remplir le troisième doublet.
===L'instruction mémoire généraliste : une alternative à LOAD et STORE===
Sur certains processeurs, il n'y a pas d'instruction LOAD ou STORE, ni même MOV . À la place, on trouve une '''instruction d'accès mémoire généraliste''', qui fusionne les trois. Elle est capable de faire une lecture, une écriture, ou une copie entre registres, et parfois une copie d'une adresse mémoire vers une autre. Les trois premières opérations sont presque toujours supportées, mais la copie d'une adresse mémoire vers une autre est beaucoup plus rare.
Sur les processeurs x86, l'instruction généraliste s'appelle l'instruction MOV. Elle gère la lecture en RAM, l'écriture en RAM, la copie d'un registre vers un autre, l'écriture d'une constante dans un registre ou une adresse. Par contre, elle ne gère pas la copie d'une adresse mémoire vers une autre ; pour un transfert de mémoire à mémoire, il faut utiliser les instructions MOVS* ('''''Mov'''e '''S'''tring'' ; MOVSB : par octets, MOVSW : par mots de 16-bits, ...).
===Les instructions d'accès mémoire complexes===
Nous venons de voir les instructions LOAD, STORE et les transferts entre registres. Elles sont présentes sur tous les processeurs, modernes comme anciens. Mais quelques processeurs gèrent des instructions mémoire plus complexes. Elles ont tendance à effectuer plusieurs accès mémoire simultanés.
Les '''instructions de copie mémoire''' copient une donnée d'une adresse vers une autre. Les copies en mémoire sont des opérations très fréquentes, il est très fréquent qu'un programme copie un bloc de mémoire dans un autre et beaucoup de programmeurs ont déjà été confronté à un tel cas. Aussi, les processeurs ajoutent des instructions multi-accès pour accélérer ces copies, ce qui fait un bon compromis entre performance et simplicité d'implémentation.
D'autres instructions d'accès mémoire plus complexes existent. Pour en donner un exemple, citons les instructions de transferts par bloc sur les premiers processeurs ARM. Ces instructions permettent de copier plusieurs registres en mémoire RAM, en une seule instruction. Sur l'ARM1, il y a 16 registres en tout. Les instructions de transfert de bloc peuvent sélectionner n'importe quel sous-ensemble de ces 16 registres, pour les copier en mémoire : on peut sélectionner tous les registres, une partie des registres (5 registres sur 16, ou 7, ou 8, ...), voire aucun registre.
D'autres instructions mémoires effectuent des opérations à l'utilité moins évidente à ce stade du cours. Sur certains processeurs, on trouve notamment des instructions pour vider la mémoire cache de son contenu, pour la réinitialiser. L'utilité ne vous est pas évidente, mais cela peut servir dans certains scénarios, notamment sur les architectures avec plusieurs processeurs pour synchroniser ces derniers. Cela sert aussi pour le système d'exploitation, qui doit remettre à zéro certains caches (comme la TLB qu'on verra dans le chapitre sur la mémoire virtuelle) quand on exécute plusieurs programmes en même temps.
===Les instructions mémoire flottantes===
Plus haut, j'ai parlé du fait que certains processeurs avaient des registres flottants séparés des registres généraux. Mais encore faut-il pouvoir les remplir, ces registres flottants. Et cela implique de charger des opérandes depuis la mémoire RAM à un moment où un autre. Pour lire ou écrire ces registres, le processeur utilise des instructions mémoire séparées, distinctes des instructions LOAD et STORE vues plus haut. Pour faire la différence, ces instructions sont appelées '''FLOAD''' et '''FSTORE'''.
Elles sont séparées des LOAD/STORE normaux pour plusieurs raisons. La première est évidemment que les registres utilisés ne sont pas les mêmes : registres "généraux" pour LOAD/STORE, registres flottants pour FLOAD et FSTORE. La seconde raison est que les nombres flottants et entiers n'ont pas forcément la même taille. Et cela impacte à la fois la taille des registres, mais aussi les transferts mémoire.
Un nombre flottant fait 32 ou 64 bits, suivant qu'on parle d'un flottant simple ou double précision. Sur un processeur 32 bits, les flottants simple précision ont la même taille qu'un entier, les flottant double précision sont deux fois plus gros. Mais sur un processeur 64 bits, c'est l'inverse : les flottants double précision font la même taille qu'un entier, les simples précision sont deux fois plus petits. Et ne parlons pas du cas de certaines architectures 16 bits qui supportent quand même les nombres flottants ! Elles sont très rares, mais elles existent. Pour le moment, concentrons-nous sur les processeurs 32 et 64 bits.
En général, les registres flottants font 64 bits (parfois 80, mais je passe cela sous silence), afin de mémoriser des flottants double précision. Et ce, même sur les processeurs 32 bits. La gestion des nombres flottants est en effet apparue sur les architectures 32 bits et elles supportaient les calculs en double précision dès le départ. Et cette différence entre 32 et 64 bits a posé quelques problèmes. Les registres flottants n'avaient pas la même taille que les registres généraux : 32 pour les registres généraux, 64 pour les registres flottants. Mais ce n'était pas un énorme problème.
Le vrai problème est que les lectures/écritures flottantes faisaient des transferts de 64 bits entre les registres flottants et la mémoire RAM. Or, le bus de données de ces processeurs faisait 32 bits, vu que c'était des architectures 32 bits. Lire/écrire un flottant demandait donc de faire deux accès mémoire consécutifs. Par exemple, lire un flottant depuis la RAM demandait de lire deux morceaux de 32 bits, le flottant était chargé en deux fois. Cette différence fait qu'il est préférable d'utiliser des instructions FSTORE/FLOAD séparées des instructions LOAD/STORE. Leur comportement n'est pas le même : LOAD/STORE lisent un seul mot mémoire, FLOAD/FSTORE en lisent deux pour la double précision.
Un autre détail est qu'il existe des instructions FLOAD et FSTORE spécialisées pour les flottants simple précision. Par exemple, une instruction FLOAD en simple précision ne lit que 32 bits en mémoire, puis le flottant est convertit en double précision 64 bits, avant d'être enregistré dans les registres. Pour l'écriture, c'est l'inverse : un flottant double précision est convertit en simple précision 32 bits, avant d'être écrit en mémoire. Le comportement est alors différent : FLOAD/FSTORE ne lisent/écrivent qu'un seul mot mémoire, de 32 bits, mais doivent faire une conversion entre simple et double précision.
Il faut noter que c'est l'inverse sur les processeurs 64 bits. Là, un flottant double précision peut être lu/écrit en un seul accès mémoire, vu que le bus de données fait 64 bits. Par contre, les flottants simples précision sont gérés un peu autrement. Concrètement, l'instruction FLOAD lit un mot de 64 bits, mais n'en garde que la moitié adéquate. L'instruction d'écriture fait elle plus de traail : elle doit lire un mot de 64 bits, en modifier la moitié, et réécrire le tout en mémoire RAM.
Vu qu'il y a des registres flottants séparés, il y aussi des instructions de copie de registre séparées pour les registres flottants. L'instruction XCHG a son équivalent flottant '''FXCHG''', l'instruction MOV a une '''instruction FMOV''' séparées, etc.
Un autre moyen de remplir un registre flottant est d'obtenir sa valeur depuis un registre général. Attention, je ne parle pas de copier un registre général dans un flottant, ou inversement. Je parle de convertir une opérande entière en un nombre flottant. Et il y a des instructions dédiées pour cela. Par exemple, il y a une instruction prend un registre entier, convertit sa valeur en flottant 64 bits, et écrit le résultat dans un registre flottant. Idem avec la conversion inverse, de flottant vers entier, qui a sa propre instruction dédiée. Et ces deux instructions ont deux variantes, suivant que le flottant soit simple ou double précision. De telles '''instructions de conversion entier-flottant''' ne sont pas à proprement parler des instructions de copie entre registre, ce sont plus un intermédiaire entre copie entre registres et opération de calcul.
==Les instructions de contrôle (branchements et tests)==
Un processeur serait sacrément inflexible s'il ne faisait qu'exécuter des instructions dans l'ordre. Certains processeurs ne savent pas faire autre chose, comme le Harvard Mark I, et il est difficile, voire impossible, de coder certains programmes sur de tels ordinateurs. Mais rassurez-vous : il existe de quoi permettre au processeur de faire des choses plus évoluées. Pour rendre notre ordinateur "plus intelligent", on peut par exemple souhaiter que celui-ci n'exécute une suite d'instructions que si une certaine condition est remplie. Ou faire mieux : on peut demander à notre ordinateur de répéter une suite d'instructions tant qu'une condition bien définie est respectée. Diverses '''structures de contrôle''' de ce type ont donc étés inventées.
Voici les plus utilisées et les plus courantes : ce sont celles qui reviennent de façon récurrente dans un grand nombre de langages de programmation actuels. Concevoir un programme (dans certains langages de programmation), c'est simplement créer une suite d'instructions, et utiliser ces fameuses structures de contrôle pour l'organiser. D'ailleurs, ceux qui savent déjà programmer auront reconnu ces fameuses structures de contrôle. On peut bien sur en inventer d’autres, en spécialisant certaines structures de contrôle à des cas un peu plus particuliers ou en combinant plusieurs de ces structures de contrôles de base, mais cela dépasse le cadre de ce cours : on ne va pas vous apprendre à programmer.
{|class="wikitable"
|-
! Nom de la structure de contrôle
! Description
|-
! SI...ALORS
| Exécute une suite d'instructions si une condition est respectée
|-
! SI...ALORS...SINON
| Exécute une suite d'instructions si une condition est respectée ou exécute une autre suite d'instructions si elle ne l'est pas.
|-
! Boucle WHILE...DO
| Répète une suite d'instructions tant qu'une condition est respectée.
|-
! Boucle DO...WHILE aussi appelée REPEAT UNTIL
| Répète une suite d'instructions tant qu'une condition est respectée. La différence, c'est que la boucle DO...WHILE exécute au moins une fois cette suite d'instructions.
|-
! Boucle FOR
| Répète un nombre fixé de fois une suite d'instructions.
|}
Les '''conditions''' à respecter pour qu'une structure de contrôle fasse son office sont généralement très simples.
Elles se calculent le plus souvent en comparant deux opérandes (des adresses, ou des nombres entiers ou à virgule flottante).
Elles correspondent le plus souvent aux comparaisons suivantes :
* A == B (est-ce que A est égal à B ?) ;
* A != B (est-ce que A est différent de B ?) ;
* A > B (est-ce que A est supérieur à B ?) ;
* A < B (est-ce que A est inférieur à B ?) ;
* A >= B (est-ce que A est supérieur ou égal à B ?) ;
* A <= B (est-ce que A est inférieur ou égal à B ?).
Il est également possible de tester si le bit d'un registre ou octet mémoire est à 1 ou 0.
Notamment le registre des indicateurs est testé après une instruction arithmétique (est-ce que le calcul a généré une retenue, ...).
Pour implémenter ces structures de contrôle, on a besoin d'une instruction qui saute en avant ou en arrière dans le programme, suivant le résultat d'une condition. Par exemple, un SI...ALORS zappera une suite d'instruction si une condition n'est pas respectée, ce qui demande de sauter après cette suite d’instruction cas échéant. Répéter une suite d'instruction demande juste de revenir en arrière et de redémarrer l’exécution du programme au début de la suite d'instruction. Nous verrons comment sont implémentées les structures de contrôle plus bas, mais toujours est-il que cela implique de faire des sauts dans le programme.
Faire un saut en avant ou en arrière dans le programme est assez simple : il suffit de modifier la valeur stockée dans le ''program counter'', ce qui permet de sauter directement à une instruction et de poursuivre l'exécution à partir de celle-ci. Et un tel saut est réalisé par des instructions spécialisées. Dans ce qui va suivre, nous allons appeler '''instructions de branchement''' les instructions qui sautent à un autre endroit du programme. Ce n'est pas la terminologie la plus adaptée, mais elle conviendra pour les explications.
L'implémentation des structures de contrôle demande donc de calculer une condition, puis de faire un saut. Mais il faut savoir que l'implémentation demande parfois de faire un saut, sans avoir à tester de condition. Dans ce cas, l'instruction qui fait un saut sans faire de test de condition est elle aussi une instruction de branchement. Cela nous amène à faire la différence entre un branchement conditionnel et non-conditionnel. La différence entre les deux est simple. Une instruction de '''branchement conditionnel''' effectue deux opérations : un test qui vérifie si la condition adéquate est respectée, et un saut dans le programme aussi appelé branchement. Une instruction de '''branchement inconditionnelle''' ne teste pas de condition et ne fait qu'un saut dans le programme.
===Les structures de contrôle===
Le IF permet d’exécuter une suite d'instructions si et seulement si une certaine condition est remplie.
[[File:Codage d'un if.. en assembleur.jpg|centre|vignette|upright=1.5|Codage d'un SI...ALORS en assembleur.]]
Le IF...ELSE sert à effectuer une suite d'instructions différente selon que la condition est respectée ou non : c'est un SI…ALORS contenant un second cas. Une boucle consiste à répéter une suite d'instructions machine tant qu'une condition est valide (ou fausse).
[[File:Codage d'un IF.ELSE en assembleur.png|centre|vignette|upright=1.5|Codage d'un SI...ALORS..SINON en assembleur.]]
Les boucles sont une variante du IF dont le branchement renvoie le processeur sur une instruction précédente. Commençons par la boucle DO…WHILE : la suite d'instructions est exécutée au moins une fois, et est répétée tant qu'une certaine condition est vérifiée. Pour cela, la suite d'instructions à exécuter est placée avant les instructions de test et de branchement, le branchement permettant de répéter la suite d'instructions si la condition est remplie. Si jamais la condition testée est fausse, on passe tout simplement à la suite du programme.
[[File:DO...WHILE.jpg|centre|vignette|upright=1.5|DO...WHILE.]]
Une boucle WHILE…DO est identique à une boucle DO…WHILE à un détail près : la suite d'instructions de la boucle n'a pas forcément besoin d'être exécutée au moins une fois. On peut donc adapter une boucle DO…WHILE pour en faire une boucle WHILE…DO : il suffit de tester si la boucle doit être exécutée au moins une fois avec un IF, et exécuter une boucle DO…WHILE équivalente si c'est le cas.
[[File:WHILE...DO.png|centre|vignette|upright=2|WHILE...DO.]]
===Les branchements conditionnels et leur implémentation ===
Il existe de nombreuses manières de mettre en œuvre les branchements conditionnels et tous les processeurs ne font pas de la même manière. Sur la plupart des processeurs, les branchements conditionnels sont séparés en deux instructions : une instruction de test qui vérifie si la condition voulue est respectée, et une instruction de saut conditionnelle. D'autres processeurs effectuent le test et le saut en une seule instruction machine.
[[File:Implémentations possibles des branchements conditionnels.png|centre|vignette|upright=3|Implémentations possibles des branchements conditionnels]]
: Plus surprenant, sur quelques rares processeurs, le ''program counter'' est un registre qui peut être modifié comme tous les autres. Cela permet de remplacer les branchements par une simple écriture dans le ''program counter'', avec une instruction MOV. Un bon exemple est le processeur ARM1, un des tout premiers processeur ARM. Cette dernière solution n'est presque jamais utilisée, mais elle reste surprenante !
Dans les faits, la solution la plus simple est clairement d'implémenter le tout avec une seule instruction. Mais beaucoup de processeurs anciens utilisent la première méthode, celle qui sépare le branchement conditionnel en deux instructions. Le branchement prend le résultat de l'instruction de test et décide s'il faut passer à l'instruction suivante ou sauter à une autre adresse. Il faut donc mémoriser le résultat de l'instruction de test dans un registre spécialisé, afin qu'il soit disponible pour l'instruction de branchement. L'usage d'un registre intermédiaire pour mémoriser le résultat de l'instruction de test demande d'ajouter un registre au processeur. De plus, le résultat de l'instruction de test varie grandement suivant le processeur, suivant la manière dont on répartit les responsabilités entre test et branchements.
Il existe, dans les grandes lignes, deux techniques pour séparer test et branchement conditionnel. La première impose une séparation stricte entre calcul de la condition et saut : l'instruction de test calcule la condition, le branchement fait ou non le saut dans le programme suivant le résultat de la condition. On a alors une instruction de test proprement dit, qui vérifie si une condition est valide et fournit un résultat sur 1 bit. Nous appellerons ces dernières des comparaisons, car de telles instruction effectuent réellement une comparaison.
La seconde méthode procède autrement, avec un calcul de la condition qui est réalisé en partie par l'instruction de test, en partie par le branchement. Cela peut paraitre surprenant, mais il y a de bonnes raisons à cette séparation peu intuitive. La raison est que l'instruction de test est une soustraction déguisée, qui fournit un résultat de plusieurs bits, qui est ensuite utilisé pour calculer la condition voulue par le branchement. l'instruction de test ne fait pas une comparaison proprement dit, mais leur résultat permet de déterminer le résultat d'une comparaison avec quelques manipulations simples.
====Les instructions de test proprement dit====
Les instructions de test, au sens le plus pur, effectuent une comparaison pour vérifier si deux nombres sont égaux, différents, lequel est supérieur, inférieur, etc. En clair, elles implémentent directement les comparaisons vues précédemment. Au total, on s'attend à ce que les 6 comparaisons précédentes soient implémentées avec 6 instructions de test différentes : une pour l'égalité, une pour la différence, une autre pour la supériorité stricte, etc.
Mais certaines de ces comparaisons sont en deux versions : une qui compare des entiers non-signés, et une autre pour les entiers signés. La raison est que comparer deux nombres entiers ne se fait pas de la même manière selon que les opérandes soient signées ou non. Nous avions vu cela dans le chapitre sur les comparateurs, mais un petit rappel ne fait pas de mal. Pour comparer deux entiers signés, il faut tenir compte de leurs signes, et le circuit utilisé n'est pas le même. Cela a des conséquences au niveau des instructions du processeur, ce qui impose d'avoir des opérations séparées pour les entiers signés et non-signés.
Dans les faits, les processeurs actuels utilisent le complément à deux pour les entiers signés, ce qui fait que les comparaisons d'égalité ou de différence A == B et A != B ne sont présentes qu'en un seul exemplaire. En complément à deux, l'égalité se détermine avec la même règle que pour les entiers non-signés : deux nombres sont égaux s'ils ont la même représentation binaire, ils sont différents sinon. Ce ne serait pas le cas avec les entiers en signe-magnitude ou en complément à un, du fait de la présence de deux zéros : un zéro positif et un zéro négatif. Les circuits de comparaison d'égalité et de différence seraient alors légèrement différents pour les entiers signés ou non. Au total, en complément à deux, on trouve donc 10 comparaisons usuelles, vu que les comparaisons de supériorité/infériorité sont en double.
Le résultat d'une comparaison est un bit, qui dit si la condition testée est vraie ou fausse. Dans la majorité des cas, ce bit vaut 1 si la comparaison est vérifiée, et 0 sinon. Une fois que l'instruction a fait son travail, il reste à stocker son résultat quelque part. Pour cela, le processeur utilise un ou plusieurs '''registres à prédicats''', des registres de 1 bit qui peuvent stocker n'importe quel résultat de comparaison. Une comparaison peut enregistrer son résultat dans n'importe quel registre à prédicats : elle a juste à préciser lequel avec son nom de registre.
Les registres à prédicats sont utiles pour accumuler les résultats de plusieurs comparaisons et les combiner par la suite. Par exemple, cela permet d'émuler une instruction qui teste si A >= B à partir de deux instructions qui calculent respectivement A > B et A == B. Pour cela, certains processeurs incorporent des instructions pour faire des opérations logiques sur les registres à prédicats. Ces opérations permettent de faire un ET, OU, XOR entre deux registres à prédicats et de stocker le résultat dans un registre à prédicat quelconque.
D'autres instructions permettent de lire le résultat d'un registre à prédicat, de calculer une condition, de combiner son résultat avec la valeur lue et d'altérer le registre à prédicat sélectionné. Par exemple, sur l'architecture IA-64, il existe une instruction cmp.eq.or, qui calcule une condition, lit un registre à prédicat fait un OU logique entre le registre lu et le résultat de la condition, et enregistre le tout dans un autre registre à prédicat. De telles instructions facilitent grandement le codage de certaines fonctions, qui demandent que plusieurs conditions soient vérifiées pour exécuter un morceau de code.
====Les instructions de test qui sont des soustractions déguisées====
Le second type d'instruction de test ne calcule pas ces conditions directement, mais elle fournit un résultat de quelques bits qui permet de les calculer avec quelques manipulations simples. Sur ces processeurs, il n'y a qu'une seule instruction de comparaison, qui est une soustraction déguisée. Le résultat de la soustraction n'est pas sauvegardé dans un registre et est simplement perdu. C'est le cas sur certains processeurs ARM ou sur les processeurs x86. Par exemple, un processeur x86 possède une instruction CMP qui n'est qu'une soustraction déguisée dans un opcode différent.
Le résultat de cette soustraction déguisée est un résultat portant sur 4 bits, qui donne des informations sur le résultat de la soustraction. Le premier bit, appelé bit ''null'', indique si le résultat est nul ou non. Le second bit indique le signe du résultat, s'il est positif ou négatif. Enfin, deux autres bits précisent si la soustraction a donné lieu à un débordement d'entier. Il y a deux bits, car on vérifie deux types de débordement : un débordement non-signé (une retenue sortante de l'additionneur), et le débordement signé (débordement en complément à deux).
Pour mémoriser le résultat d'une soustraction déguisée, le processeur incorpore un '''registre d'état'''. Le registre d'état stocke des bits qui ont chacun une signification prédéterminée lors de la conception du processeur et il sert à beaucoup de choses. Dans le cas qui nous intéresse, le registre d'état mémorise les résultats de l'instruction de test : les deux bit de débordement, le bit qui précise que le résultat d'une instruction vaut zéro, le bit de retenue pour le bit de signe.
La condition en elle-même est réalisée par le branchement. L'instruction de branchement fait donc deux choses : calculer la condition à partir du registre d'état, et effectuer le saut si la condition est valide. Le calcul des conditions se fait à partir des 4 bits de résultat.
* Le bit ''null'' permet de savoir si les deux opérandes sont égales ou non : si le résultat d'une soustraction est nul, cela implique que les deux opérandes sont égales.
* Le bit de signe permet de déterminer si le première opérande est supérieur ou inférieure à la seconde : le résultat de la soustraction est positif si A >= B, négatif sinon.
* Les bits de débordements signé/non-signé permettent de faire la différence entre infériorité stricte ou non.
[[File:Branchements et tests avec un registre d'état.png|centre|vignette|upright=1.5|Branchements et tests avec un registre d'état]]
La conséquence est qu'il y a autant d'instructions de branchements que de conditions possibles. Aussi, on a une instruction de test, mais environ une dizaine d'instructions de branchements. C'est l'inverse de ce qu'on a avec des instructions de test proprement dites, où on a autant d'instructions de test que de conditions, mais un seul branchement. Un bon exemple est celui des processeurs x86. Le registre d'état des CPU x86 contient 5 bits appelés OF, SF, ZF, CF et PF : ZF indique que le résultat de la soustraction vaut 0, SF indique son signe, CF est le bit de retenue et de débordement non-signé, OF le bit de débordement signé, et PF le bit qui donne la parité du résultat. Il existe plusieurs branchements, certains testant un seul bit du registre d'état, et d'autres une combinaison de plusieurs bits.
{|class="wikitable"
|-
! Instruction de branchement
! Bit du registre d'état testé
! Condition testée si on compare deux nombres A et B avec une instruction de test
|-
! JS (Jump if Sign)
| SF = 1
| Le résultat est négatif
|-
! JNS (Jump if not Sign)
| SF = 0
| Le résultat est positif
|-
! JO (Jump if Overflow)
| OF = 1
| Le calcul arithmétique précédent a généré un débordement signé
|-
! JNO (Jump if Not Overflow)
| OF = 0
| Le calcul arithmétique précédent n'a pas généré de débordement signé
|-
! JNE (Jump if Not equal)
| ZF = 1
| Les deux nombres A et B sont égaux
|-
! JE (Jump if Equal)
| ZF = 0
| Les deux nombres A et B sont différents
|-
! JB (Jump if below)
| CF = 1
| A < B, avec A et B non-signés
|-
! JAE (Jump if Above or Equal)
| CF = 0
| A >= B, avec A et B non-signés
|-
! (JBE) Jump if below or equal
| CF OU ZF = 1
| A >= B si A et B sont non-signés
|-
! JA (Jump if above)
| CF ET ZF = 0
| A > B si A et B sont non-signés
|-
! JL (Jump if less)
| SF != OF
| si A < B, si A et B sont signés
|-
! JGE (Jump if Greater or Equal)
| SF = OF
| si A >= B, si A et B sont signés
|-
! JLE (Jump if less or equal)
| (SF != OF) OU ZF = 1
| si A <= B, si A et B sont signés
|-
! JGE (Jump if Greater)
| (SF = OF) ET (NOT ZF) = 1
| si A > B, si A et B sont signés
|}
===Les instructions à prédicat===
Les '''instructions à prédicat''' sont des instructions qui ne font quelque chose que si une condition est respectée, et se comportent comme un NOP (une instruction qui ne fait rien) sinon. Elles lisent le résultat d'une comparaison, dans le registre d'état ou un registre à prédicat, et s’exécutent ou non suivant sa valeur.
En général, les instructions à prédicats sont des instructions en plus des instructions normales, pas à prédicat. L'instruction à prédicat la plus représentative, présente sur de nombreux processeurs, est l'instruction CMOV, qui copie un registre dans un autre si une condition est remplie. Elle permet de faire certaines opérations assez simples, comme calculer la valeur absolue d'un nombre, le maximum de deux nombres, etc.
Pour gérer les instructions à prédicat, les instructions de test s'adaptent sur plusieurs points. Le premier est que les instructions de test préfèrent utiliser des registres à prédicats, au lieu d'un registre d'état. Une des raisons à cela est la suivante : avec des registres à prédicats, on peut ajouter des instructions pour faire un ET/OU/XOR entre registres à prédicats, ce qui permet de tester des combinaisons de conditions sans faire appel au moindre branchement. Et ces conditions complexes ne sont pas rares, ce qui rend l'usage de registres à prédicats très utiles avec les instructions à prédicats.
Une autre adaptation est que les instructions de test tendent à fournir deux résultats : le résultat de la condition, et le résultat de la condition inverse. Ils sont fournis dans deux registres à prédicats distincts. Faire ainsi facilite grandement l'implémentation des structures de contrôle SI...ALORS simples. Pour rappel, elles ont deux morceaux de code : un qui s’exécute si une condition est remplie, l'autre si la condition inverse est remplie. Avec ce système, les instructions du premier et du second morceau de code testerons deux bits distincts.
L'usage d'instructions à prédicat séparées des autres instructions porte le nom de '''prédication partielle'''. C'est de loin l'implémentation la plus courante. Mais sur certains processeurs, assez rares, toutes les instructions sont des instructions à prédicat : on parle de '''prédication totale'''. Les anciens processeurs ARM 32 bits étaient dans ce cas, par exemple.
Cela peut paraitre étrange, vu que certaines instructions ne sont pas dans une structure de contrôle et doivent toujours s’exécuter, peu importe les conditions testées avant. Mais rassurez-vous, sur les processeurs à prédication totale, il y a toujours un moyen pour spécifier que certaines instructions doivent toujours s’exécuter, de manière inconditionnelle. Par exemple, sur les processeurs d'architecture HP IA-64, un des registre à prédicat, le tout premier, contient la valeur 1 et ne peut pas être modifié. Si on veut une instruction inconditionnelle, il suffit qu'elle précise que le registre à prédicat à lire est ce registre.
: Les processeurs avec un registre d'état n'ont généralement que de la prédication partielle, le plus souvent limitée à une seule instruction CMOV. Alors que qui dit prédication totale dit systématiquement registres à prédicats.
Les processeurs ARM disposent d'une prédication totale, du moins pour le jeu d'instruction ARM 32 bits. Mais il supportent une second jeu d’instruction optionnel, le ''thumb'', qui est plus compact. Au lieu d'encoder ses instructions sur 32-64 bits, ses instructions font seulement 16 bits. Et pour compacter ce jeu d'instruction, la prédication totale a été supprimée. À la place, ''thumb'' incorpore une '''instruction IT''', qui active l’exécution conditionnelle des instructions suivantes.
L'instruction teste une condition et calcule un prédicat, qui est ensuite utilisé pour décider de l'exécution des instructions suivantes. L'exécution conditionnelle marche jusqu'à 4 instructions consécutives. Le nombre exact est configurable et va de 1 à 4 instructions consécutives après l'instruction IT. La condition est configurable pour les 1 à 4 instructions : chaque instruction peut s'exécuter si la condition est vraie, ou au contraire fausse. Un masque de 4 bits décide de quelle condition utiliser pour chaque instruction. Il y a un bit par instruction, qui vaut 1 si la condition doit être vérifiée, 0 si on veut la condition inverse.
===L'instruction SKIP===
L''''instruction SKIP''' permet de zapper l'instruction suivante si une condition testée est fausse. Il en existe deux versions. La première est fusionnée avec l'instruction de test. La condition est calculée par l'instruction SKIP, qui décide s'il faut sauter l'instruction suivante. Dans un autre cas, l'instruction SKIP est précédée d'une instruction de test, récupère son résultat dans un registre de prédicat ou dans le registre d'état, et effectue le saut. On peut la voir comme une forme particulière de branchement conditionnel, qui brancherait deux instructions plus loin.
Son utilité n'est pas évidente, mais on peut classer ses utilisations en trois catégories. La première permet d'émuler une instruction de branchement conditionnel en combinant une instruction SKIP avec un branchement inconditionnel. Le branchement inconditionnel est skippé si la condition est remplie/fausse, mais éxecuté dans le cas contraire : c'est le comportement d'un branchement conditionnel. Une autre utilisation permet d'émuler une instruction à prédicat en faisant précéder une instruction normale par une instruction SKIP. Enfin, elles servent à implémenter des structures de controle IF à condition que le code du IF se résume à une vulgaire instruction. C'est assez rare, mais certains calculs comme la valeur absolue ou le calcul du minimum peuvent se calculer ainsi.
[[File:Skip instruction.png|centre|vignette|upright=3|Skip instruction]]
Vous pourriez imaginer des versions améliorées de l'instruction, qui permettent de zapper plusieurs instructions, mais de telles instructions ne sont autres que des instructions de branchement conditionnelles spécifiques, comme nous le verrons dans le chapitre sur les modes d'adressage.
===Les instructions de comparaison flottantes===
Plus haut, nous avons vu que les instructions flottantes sont séparées des instructions entières. Non seulement, les instructions de calcul flottantes sont séparées, mais les instructions de lecture/écriture le sont aussi. Et il en est de même pour les comparaisons. Il y a donc des opérations de '''comparaison flottante''', et même des '''branchements flottants'''. Il est possible d'utiliser une instruction de test/branchement unique pour les entiers et flottants, mais c'est la solution minoritaire. La solution la plus fréquent utilise des instructions de test et de branchement séparées pour entiers et flottants.
Il y a plusieurs raisons à cela, et on retrouve le fait que les registres flottants sont séparés. De plus, comparer deux flottants n'est pas la même chose que comparer deux entiers. Il faut utiliser une unité de calcul dédiée pour comparer deux flottants, qui est différente d'un comparateur entier. Un comparateur entier est généralement un circuit soustracteur camouflé, qui est souvent fusionné avec l'ALU entière. Le comparateur flottant est lui plus complexe : il teste les bits de signe, doit tester des cas spéciaux, les NaNs, les infinis, les dénormaux, etc. Il a souvent un circuit dédié dans la FPU.
Enfin, il y a une dernière raison, beaucoup plus fondamentale, bien que reliée à la précédente. Le registre d'état n'est pas le même les opérations entières et les opérations flottantes. Le '''registre d'état flottant''' est foncièrement différent du registre d'état entier, il n'a pas les mêmes bits. Par exemple, il a un bit qui indique qu'un résultat est un flottant dénormal. Il n'a de sens que pour le registre d'état flottant, n'a pas d'équivalent dans le registre d'état entier, et il faut des instructions de test dédiées pour le tester.
==Les méta-instructions REPEAT et EXECUTE==
Les '''méta-instructions''' agissent sur l’exécution d'autres instructions. Il en existe deux : REPEAT et EXECUTE. Elles servent techniquement à manipuler le flot de contrôle d'un programme, et ont quelques ressemblances avec les branchements ou les boucles. Elles sont extrêmement rares et on ne les trouve que sur des anciens processeurs (généralement micro-codés), ou sur certaines processeurs spécialisés dans le traitement de signal appelés des DSP, qui feront l'objet d'un chapitre à eux seuls.
===L’instruction REPEAT et le ''Zero-overhead looping''===
La première est la '''méta-instruction REPEAT''', qui permet de simplifier l'implémentation des boucles. Dans sa version la plus simple, elle fait en sorte que l'instruction qui la suit soit répétée plusieurs fois. Le nombre de répétition est soit un nombre fixe, toujours le même, soit l'instruction se répète tant qu'une condition n'est pas remplie. Les deux situations ne se rencontrent pas sur les mêmes processeurs. Par exemple, l'UNIVAC 1103 possède une instruction REPEAT très simple, qui répète l'instruction cible un nombre fixe de fois, mais ne gère pas les conditions.
Le GE-600/Honeywell 6000 series incorpore trois instructions REPEAT de ce type. La première répète l'instruction suivante soit un nombre fixe de fois, soit quand une condition est remplie. La seconde répète les deux instructions suivantes, l'une après l'autre. Enfin, la dernière répète l'instruction suivante jusqu'à : soit qu'une condition soit remplie, soit qu'une adresse bien précise soit atteinte. Elle servait sans doute à implémenter le parcours d'une liste chainée pour trouver un item bien précis.
Une version améliorée de l'instruction REPEAT permet de répéter un bloc de plusieurs instructions, ce qui permet d'implémenter une boucle en une seule instruction. Elles sont fréquentes sur certains processeurs de traitement de signal, qui doivent exécuter en permanence de petites boucles. Typiquement, elles permettent d'implémenter des boucles FOR dont le nombre d’exécution est fixe. La répétition de la boucle est contrôlée par un registre de boucle, qui mémorise le nombre de répétitions, et qui est décrémenté à chaque itération, sans avoir à utiliser d'instruction d'incrémentation/décrémentation.
Une variante précise deux adresses, qui délimitent les instructions de la boucle : une adresse pour le début de la boucle, une adresse pour la fin. L'implémentation hardware est alors assez simple : quand le ''program counter'' atteint l'adresse de fin, il est réinitialisé à l'adresse de début. La gestion du ''program counter'' se fait alors sans utiliser d'instructions de branchement. les deux registres sont initialisés avec une isntruction LOPP, spécialement dédiée pour cela.
Les deux techniques précédentes permettent d'implémenter des boucles sans utiliser de branchements, ou presque. Elles rendent l'exécution des boucles tellement rapides qu'elles portent le nom de '''''Zero-overhead looping'''''.
: Les instructions REP mentionnées plus haut, ne sont PAS des exemples d'instruction REPEAT. Le préfixe REP des processeurs x86 est une instruction qui se répète elle-même. Mais l'instruction REPEAT force la répétition de l'instruction suivante ! Elle agit sur une autre instruction, d'où son caractère de méta-instruction.
===L'instruction EXECUTE===
La '''méta-instruction EXECUTE''' fournit l'adresse d'une instruction cible, qui est exécutée en lieu et place de l'instruction EXECUTE. Quand le processeur exécute l'instruction EXECUTE, il prend l'adresse cible, charge l'instruction dans l'adresse cible, et l’exécute à la place de l'instruction EXECUTE. Il est possible de comprendre l'instruction EXECUTE comme un branchement spécial. Tout se passe comme si l'instruction EXECUTE effectuait un branchement vers l'instruction cible, mais que le processeur ne poursuivait pas l’exécution à la suite de l’instruction cible, et revenait à la suite de l'instruction cible.
Il y a souvent des contraintes sur l'instruction cible, qui est pointée par l'adresse cible. Généralement, il n'est pas possible que l'instruction cible soit elle-même une autre instruction EXECUTE. De même, beaucoup de processeurs interdisent que l’instruction cible soit un branchement. Mais d'autres processeurs n'ont pas ces contraintes et autorisent à utiliser des branchements pour l'instruction cible.
L'utilité d'une telle instruction est tout sauf évidente. Elle servait pourtant à beaucoup de choses sur les processeurs où elle était implémentée. Précisons qu'elle était surtout présente sur des vieux ordinateurs, dans les années 50-60, et quelques processeurs des années 70. Elle a disparu dès les années 80 et n'est présentée que par souci d'exhaustivité et intérêt historique pour les curieux. Cette précision permet de comprendre que cette instruction servait pour résoudre des problèmes qui sont actuellement résolus autrement.
L'adresse cible peut être soit intégrée dans l'instruction, soit dans un registre. Dans le second cas, l'adresse peut être incrémentée ou modifiée afin que l'instruction EXECUTE change de cible à chaque exécution. Tout se passe comme si le registre contenant l'adresse cible servait à émuler un ''program counter''. En mettant une instruction EXECUTE dans une boucle et en complétant le tout avec du code annexe, on peut exécuter du code extérieur sans modifier le véritable ''program counter''. L'utilité est de faciliter l'implémentation des logiciels debuggers, des émulateurs, ou d'autres programmes dans le même genre.
Les autres utilisations sont assez nombreuses, mais aussi difficiles à expliquer pour qui n'a pas de connaissances poussées en programmation. La première utilité est l'implémentation de certaines fonctionnalités des langages de programmation évolués, à savoir le ''late binding'', les fonctions virtuelles, et quelques autres. Elle facilite aussi l'implémentation des compilateurs à la volée, en permettant d’exécuter du code produit à volée sans faire tiquer les mécanismes de protection mémoire implémentés dans le processeur. Elle permet d'émuler du code automodifiant pour les logiciels qui gagnaient autrefois à l'utiliser. Et il y en a d'autres.
==Les instructions de calcul d'adresse==
Les instructions mémoire sont souvent associées à des instructions arithmétiques particulières : les '''instructions de calcul d'adresse'''. Elles permettent de faire des calculs sur des adresses mémoire. Et pour comprendre à quoi elles servent, nous allons devoir expliquer une fonctionnalité des langages de programmation modernes. Nous allons parler d'une structure de données connue sous le nom de tableaux, et nous allons aussi expliquer l'arithmétique des pointeurs. Commençons.
===Les tableaux : des données consécutives en RAM===
Les '''structures de données''' sont des regroupements de données plus simples, généralement des regroupements de nombres entiers, flottants, ou de caractères. Les données simples en question sont supportées naturellement par le processeur. les structures de données permettent d'en regrouper un paquet dans une seule structure que le programmeur peut manipuler à sa guise. Il en existe beaucoup : les tableaux, les listes chainées, les arbres, les graphes, les piles, les files, et bien d'autres. Et nous allons nous concentrer sur les tableaux.
Les '''tableaux''' regroupent plusieurs données de même type, qui sont placées les unes à la suite des autres en mémoire. Par exemple, on peut placer 105 entiers les uns à la suite des autres en mémoire, placer un millier de nombres flottants simple précision les uns à la suite des autres, etc. Un exemple est le stockage du texte : on peut stocker du texte en mémoire RAM en utilisant un tableau de caractères, chacun codé sur un octet/''byte''. Un texte de 154 256 mots correspond alors à un tableau de 154 256 octets, chacun codant un caractère du texte encodé.
Toute donnée dans un tableau se voit attribuer un '''indice''', un nombre entier qui indique la position de la donnée dans le tableau. Attention : les indices commencent à zéro, et non à 1, ce qui fait que la première donnée du tableau porte l'indice 0 ! L'indice dit si on veut la première donnée (indice 0), la deuxième (indice 1), la troisième (indice 2), etc. Dans ce qui suit, nous supposons que cet indice est dans un registre.
[[File:Tableau à une dimension.png|centre|vignette|upright=2|Tableau]]
Le tableau commence à une adresse appelée l''''adresse de base''', et nous supposerons qu'elle est mémorisée dans un registre. Localiser un entier/flottant/caractère dans le tableau demande de faire des calculs avec l'adresse de base et l'indice. Intuitivement, on se dit qu'il suffit d'additionner le pointeur avec l'indice. Mais ce serait oublier qu'il faut tenir compte de la taille de la donnée.
Par exemple, prenons un tableau d'entier, chacun codé sur 32 bits (4 octets). Le tableau commence à l'adresse A, le second entier est à l'adresse A + 4, le troisième entier est à l'adresse A + 8, le quatrième à l'adresse A + 12, etc. En regardant bien, on remarque que l'adresse final se calcule en multipliant l'indice par 4, avant d'ajouter l'adresse de base.
{|class="wikitable"
|-
! Adresses !! Tableau
|-
| '''A''' || rowspan="4" | Premier entier
|-
| A + 1
|-
| A + 2
|-
| A + 3
|-
| '''A + 4''' || rowspan="4" | Second entier
|-
| A + 5
|-
| A + 6
|-
| A + 7
|-
| '''A + 8''' || rowspan="4" | Troisième entier
|-
| A + 9
|-
| A + 10
|-
| A + 11
|-
| '''A + 12''' || rowspan="4" | Quatrième entier
|-
| A + 13
|-
| A + 14
|-
| A + 15
|-
| ... || ...
|-
| '''A + indice * 4''' || rowspan="4" | Énième entier
|-
| A + indice * 4 + 1
|-
| A + indice * 4 + 2
|-
| A + indice * 4 + 3
|}
Et cela se généralise pour des données qui ne font pas 4 octets, mais aussi 1 octet, 2 octets, 8 octets, etc. Il suffit alors de remplacer 4 par la taille de la donnée en octets. Le calcul de l'adresse d'une donnée dans le tableau se fait en multipliant l'indice par la taille de la donnée, puis en additionnant le pointeur. De nombreux modes d'adressage permettent de faire ce calcul directement, comme nous allons le voir.
: <math>\text{Adresse finale} = \text{Adresse de base} + \text{Indice} \times \text{Taille de la donnée}</math>
La multiplication est ce qui permet de tenir compte de la taille des données. L'indice seul ne donne pas la position de la donnée dans le tableau, il doit être '''mis à l'échelle''' pour cela, et c'est exactement ce que fait la multiplication par la taille de la donnée.
====Le calcul base + indice décalé====
Pour résumer, les calculs d'adresse localisent une donnée à partir d'une adresse de base, qui indique où commence le tableau en mémoire. Elles permettent de localiser une donnée qui est 5 octets avant l'adresse de base, ou 16 mots mémoires après. Pour cela, ils additionnent un ''indice'' à l'adresse de base, en tenant compte de la taille de la donnée. L'indice doit être mis à l'échelle, et c'est là que les subtilités commencent.
En pratique, les calculs d'adresse partent du principe que le tableau contient soit des nombres entiers, soit des nombres flottants, soit des caractères. Et ces données prennent soit un octet, soit un doublet, soit un quadruplet, soit 64 bits, éventuellement 128 bits, mais guère plus. L'indice est donc exprimé en octets, doublet (16 bits), quadruplets (32 bits), voire plus. Par exemple, un indice de 5 octets n'est pas un décalage de 5 doublets.
Toujours est-il que les données supportées ont une taille qui est une puissance de deux : 1 octet, 2 octets pour les doublets, 4 octets pour les quadruplets, etc. La mise à l'échelle de l'indice est donc très simple : l'indice doit donc être décalé par un certain nombre de rangs avant l'addition. Pas besoin de faire de multiplication par la taille de la donnée, le calcul d'adresse fait juste un décalage avant une addition. C'est un calcul du type :
: <math>\text{Adresse de base} + \text{Indice} << n</math>
Les processeurs ARM permettent de faire cela avec la seule instruction d'addition. Leur instruction ADD ne fait pas qu'additionner deux registres, elle peut aussi faire un décalage sur la seconde opérande, si elle est configurée correctement. L'instruction prend trois opérandes : un registre pour la base, un registre pour l'indice, et un décalage (combien de rangs il faut décaler).
Les processeurs RISC-V ont trois instructions : ''sh1add'', ''sh2add'', et ''sh3add''. Elles font une addition, tout en décalant l'indice de 1 rang pour ''sh1add'', 2 rangs pour ''sh2add'' et 3 rangs pour ''sh3add''. Les décalages supportés correspondent à des multiplications par 2, 4 et 8, ce qui couvre toutes les tailles d'entiers possibles sur ces processeurs 64 bits (doublet, quadruplet et 64 bits). Les décalages plus grands que 3 rangs ne sont pas supportés tels quels, mais les cas les plus courants sont supportés.
Les instructions de ce type sont souvent détournées pour faire des calculs arithmétiques purs. Par exemple, les instructions LEA qui additionnent une adresse de base avec un indice décalé sont utilisés pour simplifier des calculs du style ''A + B * 4'', ''A + B * 8'', etc. Imaginez par exemple que je veuille multiplier un opérande par 3. Je peux utiliser une opération de multiplication, mais celle-ci sera un petit peu lente. Ou je peux utiliser l'instruction de calcul d'adresse pour faire le calcul A + A << 1. L'instruction de calcul d'adresse est généralement plus rapide, de quelques cycles d'horloge. C'est déjà ça d'économisé.
====Le calcul base + indice décalé + décalage====
Un calcul d'adresse plus compliqué permet d'ajouter une constante à l'indice. Par exemple, imaginez que je calcule l'adresse d'un nombre entier, avec un calcul du type ''base + indice << n''. Seulement, je l'intéresse uniquement à l'octet de poids faible de cet entier, quelqu'un soit la raison. L'octet a sa propre adresse, et je souhaite la calculer. Pour cela, au calcul ''base + indice << n'', il faut rajouter un décalage de 1, pour sélectionner le second octet. Le calcul d'adresse est donc un calcul de la forme :
: <math>\text{Adresse de base} + \text{Indice} << n + \text{Décalage} </math>
L'adresse de base et l'indice sont dans deux registres, mais le décalage est une constante. Il faut donc faire un décalage, suivi d'une addition à trois opérandes. En soi, le circuit pour n'est pas compliqué. Nous avons vu qu'un additionneur à trois opérandes est à peine plus compliqué qu'un additionneur normal, à deux opérandes. Avec un additionneur ''carry save'', le cout en circuit est ridicule. Mais encore faut-il pouvoir l'exploiter, et une instruction de calcul d'adresse dédiée est parfaite pour ça.
Les processeurs x86 supportent une instruction capable de faire ce calcul : l''''instruction LEA'''. Le Motorola 68000 a la même instruction. Les processeurs z-architecture d'IBM ont une instruction LOAD ADRESS, qui fait une addition à trois opérandes, mais qui ne fait pas le décalage. Mais c'est un défaut mineur, car le décalage de l'indice peut souvent être omis (c'est souvent un entier incrémenté de X dans une boucle, passons).
Là encore, ces instructions peuvent être détournées pour faire des additions à trois opérandes. La seule subtilité est que l'un des opérandes doit être une constante, alors que les deux autres sont dans des registres. Le cas est cependant plus rare. Autant il est rare d'avoir deux additions successives dans un programme, autant les contraintes sur les opérandes posent problèmes. Il est rare de devoir additionner deux registres, pour ajouter une constante.
===L'adressage relatif et le code indépendant de la position===
[[File:Program memory layout.svg|vignette|Organisation de la mémoire occupée par un programme.]]
Maintenant, parlons de tout autre chose. Quand vous démarrez un logiciel, n'importe lequel, celui-ci est copié depuis le SSD, dans la mémoire RAM. Peu importe comment se fait cette copie, ce qui compte est le résultat : le logiciel est copié dans une portion de la mémoire RAM. On va supposer que ce bloc de mémoire RAM est fait d'un seul tenant, c'est une suite d'adresses consécutives. Le bloc de mémoire est généralement découpé en trois portions :
* une qui mémorise le '''code du programme''', à savoir ses instructions ;
* une qui mémorise des '''données statiques''', qui sont toujours au même endroit ;
* et une autre pour des '''données variables'''.
Les données statiques sont des données dont le programme a besoin tout au long de son exécution. Il leur réserve une place attitrée dans la mémoire RAM, qui est toujours la même. Les données statiques ont une adresse qui ne change jamais, qui est connue lors de l'exécution.
A l'opposé, les données dynamiques n'ont pas de place attitrées. Le programme peut les créer ou les détruire à la demande. Il s'en débarrasse quand il n'en a plus besoin, et la mémoire qu'elles occupaient peut être réutilisée pour autre chose. Inversement, quand il a besoin de faire de la place pour de nouvelles données, il réserve de la mémoire pour leur attribuer. Les systèmes d'exploitation modernes supportent des techniques d'allocation mémoire avancée pour ces données dynamiques. Ils séparent les données dynamiques dans la ''pile'' et le ''tas'' (''stack, heap''), et un chapitre sera dédié entièrement à la pile d'appel.
Toujours est-il que les données statiques sont censées avoir une place réservée, on est censé connaitre leur adresse à l'avance. Sauf qu'un détail vient changer la donne. Quand on lance un programme, le programme n'est jamais copié au même endroit, il n'a pas de place réservée en mémoire RAM. Les systèmes d'exploitation modernes ne le permettent pas. Et c'est un problème pour gérer les donnée statiques. Si elles n'ont plus une adresse fixe, comment déterminer leur adresse ?
Les systèmes d'exploitation modernes gèrent la situation de plusieurs manières, mais les techniques dites de "mémoire virtuelle" font tout le travail. Nous les détaillerons dans un chapitré dédié, il n'est pas encore le temps d'en parler. Mais que fait sans ses techniques ? Ou mieux : que faire quand ces techniques ne marchent pas, commet dans certaines situations impliquant des bibliothèques dynamiques partagées et autres joyeusetés ?
Une solution est alors de localiser les données statiques par rapport à quelque chose dont on connait l'adresse. Par exemple, on peut dire que la donnée statique voulue est située 254 octets après ce point de référence. Ou encore, qu'une autre donnée statique est située 97621 octets avant ce point de référence. L'idée est de prendre l'adresse d'un point de référence, et d'additionner un ''décalage''. Il s'agit de l''''adressage relatif'''.
Il existe des instructions de calcul d'adresse qui utilisent l'adressage relatif. Elle font une addition entre un décalage constant et l'adresse du point de référence. Vous vous dites que l'instruction d'addition devrait suffir, mais en même temps, vous vous demandez quel est ce point de référence ? La réponse à cette seconde question donne un indice pour la première.
La réponse est que le point de référence n'est ni plus ni moins que l'adresse de l'instruction en cours, l'adresse de l’instruction de calcul d'adresse. Cette adresse est mémorisée dans le ''Program Counter'', ce qui fait qu'elle est connue à l'exécution. On peut placer le programme n'importe où en mémoire, cela ne changera rien. le code et les données statiques sont dans le même bloc de mémoire, le bloc est placé n'importe où en mémoire RAM, mais les distance entre l'instruction et la donnée statique ne changent pas. Le ''Program Counter'' peut changer, mais c'est pris en compte lors du chargement du programme : le programme est démarré par le système d'exploitation en faisant un branchement vers sa première adresse, qui est connue de l'OS parce que c'est lui qui a placé le programme en mémoire RAM.
L''''instruction de calcul d'adresse relatif''' fait donc une addition entre le ''Program Counter'' et un décalage constant. Le résultat est enregistré dans un registre, afin de pouvoir être utilisé par une instruction LOAD ou STORE. Une telle instruction existe car les additions classiques n'utilisent que les registres dits généraux, qui mémorisent des données. Elles n'ont pas accès au ''Program Counter'' et ne peuvent ni lire, ni écrire dedans. Sauf sur certains processeurs ARM 32 bits, qui permettaient d'adresser directement le ''Program Counter'' en lecture et écriture, mais c'est une exception. Et nous la verrons plus tard dans ce cours, dans le chapitre sur les registres.
Sur les processeurs ARM 64 bits, l'instruction de calcul d'adresse relatif est l''''instruction ADR'''. L'instruction ADR encode le décalage sur 21 bits, dont un bit de signe, ce qui permet d'adresser 1 mébioctet avant et 1 mébioctet après l'instruction de calcul. Aller au-delà demande d'ajouter une seconde addition, à la suite de l'instruction ADR. ARM fournit bien des additions pour ajouter une constante à un registre. Cependant, les constantes font 12 bits, pas plus.
Pour résoudre le problème, ARM fournit l''''instruction ADRP'''. Grâce à elle, on peut générer un décalage de 32 bits, en combinant ADRP et une addition. L'idée est que ADRP s'occupe les 21 bits de poids fort, et une seconde addition s'occupe des 12 bits de poids faible. ADRP utilise un décalage de 21 bits, comme ADR, sauf qu'il est décalé de 12 rangs vers la gauche avant d'être additionné. L'opérande de l'addition est donc un nombre de 32 bits, dont les 12 bits de poids faibles valent zéro. L'addition qui suit ADRP remplit les 12 bits restants avec la valeur adéquate.
==Les instructions d'arrêt et de mise en veille==
Les processeurs gèrent souvent des '''instructions de mise en veille''' le processeur. Une fois mis en veille, le processeur ne peut être réveillé que si un signal de sortie de veille survient. Les signaux de sortie de veille sont envoyés par les périphériques, les entrées-sorties, le ''chipset'' de la carte mère, etc. Ils sont rarement mis totalement en veille, ce qui leur permet de générer ce signal de sortie si nécessaire. Le processeur attend alors qu'un périphérique lui envoie un signal de sortie de veille, mais ne fait rien en attendant. De telles instructions ne sont exécutables qu'en mode noyau, par le système d'exploitation.
: Le signal de sortie de veille est techniquement une interruption matérielle, comme on le verra plus tard.
Il existe aussi des '''instructions d'arrêt''', qui éteignent le processeur complétement. Le processeur peut redémarrer, mais pas reprendre là où il en était. Pour redémarrer, il faut qu'il reçoive un signal RESET, envoyé sur une entrée du processeur. Lorsque l'entrée RESET du processeur est mise à 1, le processeur redémarre. Typiquement, l'alimentation du processeur est suspendue.
===Les exemples sur les processeurs les plus connus===
Sur les processeurs ARM, les instructions WFI et WFE sont utiles pour les systèmes embarqués, pour mettre le processeur en mode basse consommation. Lorsqu'une instruction de ce type est exécutée, le processeur continue d'être alimenté, mais il ne tient pas compte du signal d'horloge. Le signal d'horloge est clock gaté dès l'entrée dans le CPU. La différence entre les deux instructions est que WFI s'exécute inconditionnellement, alors que WFE ne s’exécute que un registre spécifique, le registre d'évènement, est à 0.
Sur les processeurs Motorola 6502 et ses dérivés, l'instruction WAI est une instruction de mise en veille, alors que l'instruction STP éteint l'ordinateur. La différence est qu'avec l'instruction STP, le processeur ne réagit qu'au signal RESET, qui fait redémarrer le processeur, mais ne réagit pas aux signaux de sortie de veille. L'exécution du processeur ne reprend alors pas là où le processeur s'est arrêté, le ''program counter'' est réinitialisé.
Sur les CPU x86 avant le 486, l'instruction HLT ne mettait pas le processeur en mode basse consommation. Aucune technique n'était prévue pour réduire la consommation du processeur pendant la veille. Le 8086 se contentait de se déconnecter du bus et n'incrémentait pas son ''program counter''. Au passage, l'implémentation exacte au niveau des circuits de l'instruction HLT sur le 8086 est détaillée dans cet article de blog :
* [https://www.righto.com/2023/01/reverse-engineering-intel-8086.html Reverse-engineering the Intel 8086 processor's HALT circuits]
C'est avec le 486 que la mise en veille s'est accompagnée d'une extinction partielle du processeur. Le processeur passait en mode basse consommation, ce qui permettait des économies d'énergie de 5 à 10%. Cependant, les premiers CPU sortis d'usine avaient un bug : le CPU stoppé via l'instruction HLT ne pouvait pas redémarrer de manière logicielle, il fallait lui envoyer le signal de RESET directement via le matériel.
{|class="wikitable"
|-
! x86
| HLT (HALT)
|-
! ARM
|
* WFI (Wait For Interrupt)
* WFE (Wait For Event).
|-
! Motorola 6502 et plus
|
* WAI (WAit for Interrupt)
* STP (SToP)
|}
===Les instructions ''Halt And Catch Fire''===
Le bug de l’instruction HTL, sur le 486, n'est pas spécifique aux processeurs 486, de nombreux CPU ont des instructions non-documentées qui bloquent l'ordinateur, si exécutées. Le processeur ne peut alors pas sortir de la mise en veille et soit s'arrête, soit doit être RESET. Les instructions ne sont souvent pas voulues par les concepteurs et sont souvent des bugs. De telles instructions illégales devraient lever une exception matérielle "instruction non reconnue", mais ne le font pas en raison d'un bug quelconque. Elles sont appelées, non sans humour, des '''instructions ''Halt And Catch Fire''''' (stoppe et prend feu).
Un exemple classique est celle du bug du Pentium F00F. Le responsable est une instruction non-documentée, encodée en binaire comme suit F0 0F C7 C8. Il s'agit d'une instruction dont l'encodage est invalide. Elle correspond à une instruction cmpxchg8b qui agit sur une valeur 64 bits obtenue en concaténant les deux registres (EAX et EDX). Elle compare cette valeur 64 bits avec une donnée en mémoire RAM. Mais avec l'encodage F0 0F C7 C8, l'adresse mémoire est remplacée par un nom de registre, du fait de l'encodage des modes d'adressage sur ce CPU. En théorie, le processeur devrait lever une exception matérielle ''opcode invalide''. Seulement, l'instruction est associée à un préfixe LOCK, qu'on ne détaillera pas ici, qui empêche la levée de l'exception.
Elle agissait comme une instruction ''Halt And Catch Fire'' classique. Un problème est que cette instruction n'était pas une instruction en mode noyau, mais une instruction accessible en espace utilisateur. N'importe quel programme invalide, voire un programme malicieux, pouvait bloquer l'ordinateur. La situation a été prise an sérieux, les systèmes d'exploitation fournissant des mitigations logicielles. Intel corrigea le problème, dans une seconde fournée sortie d'usine, avec la version B2 du 486.
==Résumé==
Pour résumer, les instructions les plus courantes sont les suivantes :
{|class="wikitable"
|-
!Instruction
!Utilité
|-
!Instructions arithmétiques
|Elles font des calculs sur des nombres. Les plus communes sont l'addition, la soustraction, la multiplication, la division, le modulo. Pour les instructions flottantes, la racine carrée est souvent supportée, parfois avec les opérations trigonométriques, les logarithmes et l'exponentielle.
|-
!Instructions logiques
|Elles travaillent sur des bits ou des groupes de bits. Les plus communes sont ;
* Les opérations bit à bit, à savoir le ET logique, le OU logique, le OU exclusif (XOR) et le NON logique.
* Les instructions de décalage à droite et à gauche, qui vont décaler tous les bits d'un nombre d'un cran vers la gauche ou la droite. Les bits qui sortent du nombre sont considérés comme perdus.
* Les instructions de rotation, qui font la même chose que les instructions de décalage, à la différence près que les bits qui "sortent d'un côté du nombre" après le décalage rentrent de l'autre.
|-
!Instructions de test et de contrôle (branchements)
|Elles permettent de contrôler la façon dont notre programme s’exécute sur notre ordinateur. Elles permettent notamment de choisir la prochaine instruction à exécuter, histoire de répéter des suites d'instructions, de ne pas exécuter des blocs d'instructions dans certains cas, et bien d'autres choses.
|-
!Instructions d’accès mémoire
|Elles permettent d'échanger des données entre le processeur et la mémoire, ou encore permettent de gérer la mémoire et son adressage. Les deux les plus courantes sont les suivantes :
* LOAD : charge une donnée dans un registre ;
* STORE : copie le contenu d'un registre dans une adresse mémoire.
|}
En plus de ces instructions, beaucoup de processeurs ajoutent d'autres instructions. La plupart sont utilisées par le système d'exploitation pour configurer certaines fonctionnalités importante : la protection mémoire, la mémoire virtuelle, les modes de compatibilité du processeur, la gestion de l'alimentation, l'arrêt ou la mise en veille, etc.
On peut aussi trouver des instructions spécialisées dans les calculs cryptographiques : certaines instructions permettent de chiffrer ou de déchiffrer des données de taille fixe. De même, certains processeurs ont une instruction permettant de générer des nombres aléatoires. Et on peut trouver bien d'autres exemples...
Mais d'autres sont franchement exotiques. Par exemple, certains processeurs sont capables d'effectuer des instructions sur du texte directement. Pour stocker du texte, la technique la plus simple utilise une suite de lettres, stockées les unes à la suite des autres dans la mémoire, dans l'ordre dans lesquelles elles sont placées dans le texte. Quelques ordinateurs disposent d'instructions pour traiter ces suites de lettres. D'ailleurs, n'importe quel PC x86 actuel dispose de telles instructions, bien qu'elles ne soient pas utilisées car paradoxalement trop lente comparé aux solutions logicielles ! Cela peut paraître surprenant, mais il y a une explication assez simple qui sera compréhensible dans quelques chapitres (les instructions en question sont microcodées).
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Les mémoires FIFO et LIFO
| prevText=Les mémoires FIFO et LIFO
| next=Les registres du processeur
| nextText=Les registres du processeur
}}
</noinclude>
o5lnq9kf3jnwvlzc5nciz7ziftt0b9a
Fonctionnement d'un ordinateur/L'abstraction mémoire et la mémoire virtuelle
0
65813
775333
775247
2026-10-16T21:02:24Z
Mewtow
31375
/* Un cas particulier : la z/architecture d'IBM */
775333
wikitext
text/x-wiki
Pour introduire ce chapitre, nous devons faire un rappel sur le concept d{{'}}'''espace d'adressage'''. Pour rappel, un espace d'adressage correspond à l'ensemble des adresses utilisables par le processeur. Par exemple, si je prends un processeur 16 bits, il peut adresser en tout 2^16 = 65536 adresses, l'ensemble de ces adresses forme son espace d'adressage. Intuitivement, on s'attend à ce qu'il y ait correspondance avec les adresses de la mémoire RAM. J'entends par là que l'adresse 1209 de l'espace d'adressage correspond à l'adresse 1209 en mémoire RAM. C'est là une hypothèse parfaitement raisonnable et on voit mal comment ce pourrait ne pas être le cas.
Mais les processeurs modernes utilisent des techniques d{{'}}'''abstraction mémoire''' qui font que ce n'est pas le cas. Avec ces techniques, l'adresse 1209 de l'espace d'adressage correspond en réalité à l'adresse 9999 en mémoire RAM, voire n'est pas en RAM. L'abstraction mémoire fait que l'espace d'adressage regroupe des adresses fictives, qui doivent être traduites en adresses mémoires réelles pour être utilisées. Les adresses de l'espace d'adressage portent le nom d{{'}}'''adresses logiques''', alors que les adresses de la mémoire RAM sont appelées '''adresses physiques'''.
L'intérêt de l'abstraction matérielle n'est pas évident. Aussi, avant de parler de comment l'abstraction mémoire fonctionne, nous allons voir à quoi elle sert. Nous allons voir qu'elle a plusieurs utilisations différentes, qui sont absolument nécessaires sur tous les ordinateurs personnels modernes. Tous les processeurs modernes la prennent en charge, les systèmes d'exploitation coopérent avec le processeur pour l'utiliser au mieux.
==L'abstraction mémoire permet plusieurs fonctionnalités complémentaires==
La fonctionnalité la plus connue est la mémoire virtuelle, dont vous avez peut-être déjà entendu parler, peut-être que le nom vous dit quelque chose. Si ce n'est pas le cas, nous la détailleront dans la suite. Elle permet concrétement à un programme d'utiliser plus de mémoire qu'il n'y a de RAM installé dans un ordinateur, en utilisant le disque dur comme solution de secours. Mais d'autres fonctionnalités moins évidentes sont permises par l'abstraction mémoire. Et la première que nous allons voir est l'abstraction des processus.
: En général, une adresse logique correspond à une seule adresse physique. Mais beaucoup de fonctionnalités avancées ne respectent pas cette règle.
===L'abstraction matérielle des processus===
Les systèmes d'exploitation modernes sont dits multi-tâche, à savoir qu'ils sont capables d'exécuter plusieurs logiciels en même temps. Et ce même si un seul processeur est présent dans l'ordinateur : les logiciels sont alors exécutés à tour de rôle. Toutefois, cela amène un paquet de problèmes qu'il faut résoudre au mieux. Par exemple, les programmes exécutés doivent se partager la mémoire RAM, ce qui ne vient pas sans problèmes. Le problème principal est que les programmes ne doivent pas lire ou écrire dans les données d'un autre, sans quoi on se retrouverait rapidement avec des problèmes. Il faut donc introduire des mécanismes d{{'}}'''isolement des processus''', pour isoler les programmes les uns des autres.
Un de ces mécanismes est l{{'}}'''abstraction matérielle des processus''', une technique qui fait que chaque programme a son propre espace d'adressage. Chaque programme a l'impression d'avoir accès à tout l'espace d'adressage, de l'adresse 0 à l'adresse maximale gérée par le processeur. Évidemment, il s'agit d'une illusion maintenue justement grâce à la traduction d'adresse. Les espaces d'adressage contiennent des adresses logiques, les adresses de la RAM sont des adresses physiques, la nécessité de l'abstraction mémoire est évidente.
Implémenter l'abstraction mémoire peut se faire de plusieurs manières. Mais dans tous les cas, il faut que la correspondance adresse logique - physique change d'un programme à l'autre. Ce qui est normal, vu que les deux processus sont placés à des endroits différents en RAM physique. La conséquence est qu'avec l'abstraction mémoire, une adresse logique correspond à plusieurs adresses physiques. Une même adresse logique dans deux processus différents correspond à deux adresses phsiques différentes, une par processus. Une adresse logique dans un processus correspondra à l'adresse physique X, la même adresse dans un autre processus correspondra à l'adresse Y.
Les adresses physiques qui partagent la même adresse logique sont alors appelées des '''adresses homonymes'''. Le choix de la bonne adresse étant réalisé par un mécanisme matériel et dépend du programme en cours. Le mécanisme pour choisir la bonne adresse dépend du processeur, mais il y en a deux grands types :
* La première consiste à utiliser l'identifiant de processus CPU, vu au chapitre précédent. C'est, pour rappel, un numéro attribué à chaque processus par le processeur. L'identifiant du processus en cours d'exécution est mémorisé dans un registre du processeur. La traduction d'adresse utilise cet identifiant, en plus de l'adresse logique, pour déterminer l'adresse physique.
* La seconde solution mémorise les correspondances adresses logiques-physique dans des tables en mémoire RAM, qui sont différentes pour chaque programme. Les tables sont accédées à chaque accès mémoire, afin de déterminer l'adresse physique.
===Le partage de la mémoire===
L'isolation des processus est très importante sur les systèmes d'exploitation modernes. Cependant, il existe quelques situations où elle doit être contournée ou du moins mise en pause. Les situations sont multiples : gestion de bibliothèques partagées, communication entre processus, usage de ''threads'', etc. Elles impliquent toutes un '''partage de mémoire''', à savoir qu'une portion de mémoire RAM est partagée entre plusieurs programmes. Le partage de mémoire est une sorte de brèche de l'isolation des processus, mais qui est autorisée car elle est utile.
Un cas intéressant est celui des '''bibliothèques partagées'''. Les bibliothèques sont des collections de fonctions regroupées ensemble, dans une seule unité de code. Un programme qui utilise une bibliothèque peut appeler n’importe quelle fonction présente dans la bibliothèque. La bibliothèque peut être simplement inclue dans le programme lui-même, on parle alors de bibliothèques statiques. De telles bibliothèques fonctionnent très bien, mais avec un petit défaut pour les bibliothèques très utilisées : plusieurs programmes qui utilisent la même bibliothèque vont chacun l'inclure dans leur code, ce qui fera doublon.
Pour éviter cela, les OS modernes gèrent des bibliothèques partagées, à savoir qu'un seul exemplaire de la bibliothèque est partagé entre plusieurs programmes. Chaque programme peut exécuter une fonction de la bibliothèque quand il le souhaite, en effectuant un branchement adéquat. Mais cela implique que la bibliothèque soit présente dans l'espace d'adressage du programme en question. Une bibliothèque est donc présente dans plusieurs espaces d'adressage, alors qu'il n'y en a qu'un seul exemplaire en mémoire RAM.
[[File:Ogg vorbis libs and application dia.svg|centre|vignette|upright=2|Exemple de bibliothèques, avec Ogg vorbis.]]
D'autres situations demandent de partager de la mémoire entre deux programmes. Par exemple, les systèmes d'exploitation modernes gèrent nativement des systèmes de '''communication inter-processus''', très utilisés par les programmes modernes pour échanger des données. Et la plupart demandant de partager un bout de mémoire entre processus, même si c'est seulement temporairement. Typiquement, deux processus partagent un intervalle d'adresse où l'un écrit les données à l'autre, l'autre lisant les données envoyées.
Une dernière utilisation de la mémoire partagée est l{{'}}'''accès direct au noyau'''. Sur les systèmes d'exploitations moderne, dans l'espace d'adressage de chaque programme, les adresses hautes sont remplies avec une partie du noyau ! Évidemment, ces adresses sont accessibles uniquement en lecture, pas en écriture. Pas question de modifier le noyau de l'OS ! De plus, il s'agit d'une portion du noyau dont on sait que la consultation ne pose pas de problèmes de sécurité.
Le programme peut lire des données dans cette portion du noyau, mais aussi exécuter les fonctions du noyau qui sont dedans. L'idée est d'éviter des appels systèmes trop fréquents. Au lieu d'effectuer un véritable appel système, avec une interruption logicielle, le programme peut exécuter des appels systèmes simplifiés, de simples appels de fonctions couplés avec un changement de niveau de privilège (passage en espace noyau nécessaire).
[[File:AMD64-canonical--48-bit.png|vignette|Répartition des adresses entre noyau (jaune/orange) et programme (verte), sur les systèmes x86-64 bits, avec des adresses physiques de 48 bits.]]
L'espace d'adressage est donc séparé en deux portions : l'OS d'un côté, le programme de l'autre. La répartition des adresses entre noyau et programme varie suivant l'OS ou le processeur utilisé. Sur les PC x86 32 bits, Linux attribuait 3 gigas pour les programmes et 1 giga pour le noyau, Windows attribuait 2 gigas à chacun. Sur les systèmes x86 64 bits, l'espace d'adressage d'un programme est coupé en trois, comme illustré ci-contre : une partie basse de 2^48 octets, une partie haute de même taille, et un bloc d'adresses invalides entre les deux. Les adresses basses sont utilisées pour le programme, les adresses hautes pour le noyau, il n'y a rien entre les deux.
Avec le partage de mémoire, plusieurs adresses logiques correspondent à la même adresse physique. Tel processus verra la zone de mémoire partagée à l'adresse X, l'autre la verra à l'adresse Y. Mais il s'agira de la même portion de mémoire physique, avec une seule adresse physique. En clair, lorsque deux processus partagent une même zone de mémoire, la zone sera mappées à des adresses logiques différentes. Les adresses logiques sont alors appelées des '''adresses synonymes''', terme qui trahit le fait qu'elles correspondent à la même adresse physique.
===La mémoire virtuelle===
Toutes les adresses ne sont pas forcément occupées par de la mémoire RAM, s'il n'y a pas assez de RAM installée. Par exemple, un processeur 32 bits peut adresser 4 gibioctets de RAM, même si seulement 3 gibioctets sont installés dans l'ordinateur. L'espace d'adressage contient donc 1 gigas d'adresses inutilisées, et il faut éviter ce surplus d'adresses pose problème.
Sans mémoire virtuelle, seule la mémoire réellement installée est utilisable. Si un programme utilise trop de mémoire, il est censé se rendre compte qu'il n'a pas accès à tout l'espace d'adressage. Quand il demandera au système d'exploitation de lui réserver de la mémoire, le système d'exploitation le préviendra qu'il n'y a plus de mémoire libre. Par exemple, si un programme tente d'utiliser 4 gibioctets sur un ordinateur avec 3 gibioctets de mémoire, il ne pourra pas. Pareil s'il veut utiliser 2 gibioctets de mémoire sur un ordinateur avec 4 gibioctets, mais dont 3 gibioctets sont déjà utilisés par d'autres programmes. Dans les deux cas, l'illusion tombe à plat.
Les techniques de '''mémoire virtuelle''' font que l'espace d'adressage est utilisable au complet, même s'il n'y a pas assez de mémoire installée dans l'ordinateur ou que d'autres programmes utilisent de la RAM. Par exemple, sur un processeur 32 bits, le programme aura accès à 4 gibioctets de RAM, même si d'autres programmes utilisent la RAM, même s'il n'y a que 2 gibioctets de RAM d'installés dans l'ordinateur.
Pour cela, on utilise une partie des mémoires de masse (disques durs) d'un ordinateur en remplacement de la mémoire physique manquante. Le système d'exploitation crée sur le disque dur un fichier, appelé le ''swapfile'' ou '''fichier de ''swap''''', qui est utilisé comme mémoire RAM supplémentaire. Il mémorise le surplus de données et de programmes qui ne peut pas être mis en mémoire RAM.
[[File:Vm1.png|centre|vignette|upright=2.0|Mémoire virtuelle et fichier de Swap.]]
Une technique naïve de mémoire virtuelle serait la suivante. Avant de l'aborder, précisons qu'il s'agit d'une technique abordée à but pédagogique, mais qui n'est implémentée nulle part tellement elle est lente et inefficace. Un espace d'adressage de 4 gigas ne contient que 3 gigas de RAM, ce qui fait 1 giga d'adresses inutilisées. Les accès mémoire aux 3 gigas de RAM se font normalement, mais l'accès aux adresses inutilisées lève une exception matérielle "Memory Unavailable". La routine d'interruption de cette exception accède alors au ''swapfile'' et récupère les données associées à cette adresse. La mémoire virtuelle est alors émulée par le système d'exploitation.
Le défaut de cette méthode est que l'accès au giga manquant est toujours très lent, parce qu'il se fait depuis le disque dur. D'autres techniques de mémoire virtuelle logicielle font beaucoup mieux, mais nous allons les passer sous silence, vu qu'on peut faire mieux, avec l'aide du matériel.
L'idée est de charger les données dont le programme a besoin dans la RAM, et de déplacer les autres sur le disque dur. Par exemple, imaginons la situation suivante : un programme a besoin de 4 gigas de mémoire, mais ne dispose que de 2 gigas de mémoire installée. On peut imaginer découper l'espace d'adressage en 2 blocs de 2 gigas, qui sont chargés à la demande. Si le programme accède aux adresses basses, on charge les 2 gigas d'adresse basse en RAM. S'il accède aux adresses hautes, on charge les 2 gigas d'adresse haute dans la RAM après avoir copié les adresses basses sur le ''swapfile''.
On perd du temps dans les copies de données entre RAM et ''swapfile'', mais on gagne en performance vu que tous les accès mémoire se font en RAM. Du fait de la localité temporelle, le programme utilise les données chargées depuis le swapfile durant un bon moment avant de passer au bloc suivant. La RAM est alors utilisée comme une sorte de cache alors que les données sont placées dans une mémoire fictive représentée par l'espace d'adressage et qui correspond au disque dur.
Mais avec cette technique, la correspondance entre adresses du programme et adresses de la RAM change au cours du temps. Les adresses de la RAM correspondent d'abord aux adresses basses, puis aux adresses hautes, et ainsi de suite. On a donc besoin d'abstraction mémoire. Les correspondances entre adresse logique et physique peuvent varier avec le temps, ce qui permet de déplacer des données de la RAM vers le disque dur ou inversement. Une adresse logique peut correspondre à une adresse physique, ou bien à une donnée swappée sur le disque dur. C'est l'unité de traduction d'adresse qui se charge de faire la différence. Si une correspondance entre adresse logique et physique est trouvée, elle l'utilise pour traduire les adresses. Si aucune correspondance n'est trouvée, alors elle laisse la main au système d'exploitation pour charger la donnée en RAM. Une fois la donnée chargée en RAM, les correspondances entre adresse logique et physiques sont modifiées de manière à ce que l'adresse logique pointe vers la donnée chargée.
===L'extension d'adressage===
Une autre fonctionnalité rendue possible par l'abstraction mémoire est l{{'}}'''extension d'adressage'''. Elle permet d'utiliser plus de mémoire que l'espace d'adressage ne le permet. Par exemple, utiliser 7 gigas de RAM sur un processeur 32 bits, dont l'espace d'adressage ne gère que 4 gigas. L'extension d'adresse est l'exact inverse de la mémoire virtuelle. La mémoire virtuelle sert quand on a moins de mémoire que d'adresses, l'extension d'adresse sert quand on a plus de mémoire que d'adresses.
Il y a quelques chapitres, nous avions vu que c'est possible via la commutation de banques. Mais l'abstraction mémoire est une méthode alternative. Que ce soit avec la commutation de banques ou avec l'abstraction mémoire, les adresses envoyées à la mémoire doivent être plus longues que les adresses gérées par le processeur. La différence est que l'abstraction mémoire étend les adresses d'une manière différente.
Une implémentation possible de l'extension d'adressage fait usage de l'abstraction matérielle des processus. Chaque processus a son propre espace d'adressage, mais ceux-ci sont placés à des endroits différents dans la mémoire physique. Par exemple, sur un ordinateur avec 16 gigas de RAM, mais un espace d'adressage de 2 gigas, on peut remplir la RAM en lançant 8 processus différents et chaque processus aura accès à un bloc de 2 gigas de RAM, pas plus, il ne peut pas dépasser cette limite. Ainsi, chaque processus est limité par son espace d'adressage, mais on remplit la mémoire avec plusieurs processus, ce qui compense. Il s'agit là de l'implémentation la plus simple, qui a en plus l'avantage d'avoir la meilleure compatibilité logicielle. De simples changements dans le système d'exploitation suffisent à l'implémenter.
[[File:Extension de l'espace d'adressage.png|centre|vignette|upright=1.5|Extension de l'espace d'adressage]]
Un autre implémentation donne plusieurs espaces d'adressage différents à chaque processus, et a donc accès à autant de mémoire que permis par la somme de ces espaces d'adressage. Par exemple, sur un ordinateur avec 16 gigas de RAM et un espace d'adressage de 4 gigas, un programme peut utiliser toute la RAM en utilisant 4 espaces d'adressage distincts. On passe d'un espace d'adressage à l'autre en changeant la correspondance adresse logique-physique. L'inconvénient est que la compatibilité logicielle est assez mauvaise. Modifier l'OS ne suffit pas, les programmeurs doivent impérativement concevoir leurs programmes pour qu'ils utilisent explicitement plusieurs espaces d'adressage.
Les deux implémentations font usage des adresses logiques homonymes, mais à l'intérieur d'un même processus. Pour rappel, cela veut dire qu'une adresse logique correspond à des adresses physiques différentes. Rien d'étonnant vu qu'on utilise plusieurs espaces d'adressage, comme pour l'abstraction des processus, sauf que cette fois-ci, on a plusieurs espaces d'adressage par processus. Prenons l'exemple où on a 8 gigas de RAM sur un processeur 32 bits, dont l'espace d'adressage ne gère que 4 gigas. L'idée est qu'une adresse correspondra à une adresse dans les premiers 4 gigas, ou dans les seconds 4 gigas. L'adresse logique X correspondra d'abord à une adresse physique dans les premiers 4 gigas, puis à une adresse physique dans les seconds 4 gigas.
===La protection mémoire===
La '''protection mémoire''' regroupe des techniques très différentes les unes des autres, qui visent à améliorer la sécurité des programmes et des systèmes d'exploitation. Elles visent à empêcher de lire, d'écrire ou d'exécuter certaines portions de mémoire. Sans elle, les programmes peuvent techniquement lire ou écrire les données des autres, ce qui causent des situations non-prévues par le programmeur, avec des conséquences qui vont d'un joli plantage à des failles de sécurité dangereuses.
La première technique de protection mémoire est l{{'}}'''isolation des processus''', qu'on a vue plus haut. Elle garantit que chaque programme n'a accès qu'à certaines portions dédiées de la mémoire et rend le reste de la mémoire inaccessible en lecture et en écriture. Le système d'exploitation attribue à chaque programme une ou plusieurs portions de mémoire rien que pour lui, auquel aucun autre programme ne peut accéder. Un tel programme, isolé des autres, s'appelle un '''processus''', d'où le nom de cet objectif. Toute tentative d'accès à une partie de la mémoire non autorisée déclenche une exception matérielle (rappelez-vous le chapitre sur les interruptions) qui est traitée par une routine du système d'exploitation. Généralement, le programme fautif est sauvagement arrêté et un message d'erreur est affiché à l'écran.
La '''protection de l'espace exécutable''' empêche d’exécuter quoique ce soit provenant de certaines zones de la mémoire. En effet, certaines portions de la mémoire sont censées contenir uniquement des données, sans aucun programme ou code exécutable. Cependant, des virus informatiques peuvent se cacher dedans et d’exécuter depuis celles-ci. Ou encore, des failles de sécurités peuvent permettre à un attaquant d'injecter du code exécutable malicieux dans des données, ce qui peut lui permettre de lire les données manipulées par un programme, prendre le contrôle de la machine, injecter des virus, ou autre. Pour éviter cela, le système d'exploitation peut marquer certaines zones mémoire comme n'étant pas exécutable. Toute tentative d’exécuter du code localisé dans ces zones entraîne la levée d'une exception ou d'une erreur et le système d'exploitation réagit en conséquence. Là encore, le processeur doit détecter les exécutions non autorisées.
D'autres méthodes de protection mémoire visent à limiter des actions dangereuses. Pour cela, le processeur et l'OS gèrent des '''droits d'accès''', qui interdisent certaines actions pour des programmes non-autorisés. Lorsqu'on exécute une opération interdite, le système d’exploitation et/ou le processeur réagissent en conséquence. La première technique de ce genre n'est autre que la séparation entre espace noyau et utilisateur, vue dans le chapitre sur les interruptions. Mais il y en a d'autres, comme nous le verrons dans ce chapitre.
==La MMU==
La traduction des adresses logiques en adresses physiques se fait par un circuit spécialisé, appelé la '''''Memory Management Unit''''' (MMU). De nos jours, elle est intégrée dans le processeur, à la suite de l'unité mémoire. Mais il a existé des processeurs avec une MMU externe, soudée sur la carte mère.
La traduction des adresses logiques en adresses physiques demande d'accéder à des données sont en mémoire RAM, qui sont gérés par le système d'exploitation. Aussi, les processeurs modernes incorporent des mémoires caches appelées des '''''Translation Lookaside Buffers''''', ou encore TLB. Nous nous pouvons pas parler des TLB pour le moment, car nous n'avons pas encore abordé le chapitre sur les mémoires caches, mais un chapitre entier sera dédié aux TLB d'ici peu.
[[File:MMU principle updated.png|centre|vignette|upright=2|MMU.]]
===Les MMU intégrées au processeur===
D'ordinaire, la MMU est intégrée au processeur. Et elle peut l'être de deux manières. La première en fait un circuit séparé, relié au bus d'adresse. La seconde fusionne la MMU avec l'unité de calcul d'adresse. La première solution est surtout utilisée avec une technique d'abstraction mémoire appelée la pagination, alors que l'autre l'est avec une autre méthode appelée la segmentation. La raison est que la traduction d'adresse avec la segmentation est assez simple : elle demande d'additionner le contenu d'un registre avec l'adresse logique, ce qui est le genre de calcul qu'une unité de calcul d'adresse sait déjà faire. La fusion est donc assez évidente.
Pour donner un exemple, l'Intel 8086 fusionnait l'unité de calcul d'adresse et la MMU. Précisément, il utilisait un même additionneur pour incrémenter le ''program counter'' et effectuer des calculs d'adresse liés à la segmentation. Il aurait été logique d'ajouter les pointeurs de pile avec, mais ce n'était pas possible. La raison est que le pointeur de pile ne peut pas être envoyé directement sur le bus d'adresse, vu qu'il doit passer par une phase de traduction en adresse physique liée à la segmentation.
[[File:80186 arch.png|centre|vignette|upright=2|Intel 8086, microarchitecture.]]
===Les MMU séparées du processeur, sur la carte mère===
Avant d'être intégrées au processeur, les MMU étaient des circuits séparés, placés sur la carte mère, entre le processeur et la mémoire. Elle étaient connectées en plein milieu du bus d'adresse. A savoir que le bus d'adresse entrait dans la MMU et en ressortait. Le processeur envoyait des adresses à la MMU, qui renvoyait une adresse physique. La MMU était facultative, à savoir que le système pouvait fonctionner sans MMU d'installée, mais l'abstraction mémoire n’était alors pas disponible.
Par exemple, les processeurs Motorola 68000 et 68010 pouvaient être combinés avec une MMU de type Motorola 68451. Elle supportait des versions simplifiées de la segmentation et de la pagination. Au minimum, elle ajoutait un support de la protection mémoire contre certains accès non-autorisés. La gestion de la mémoire virtuelle proprement dit n'était possible que si le processeur utilisé était un Motorola 68010, en raison de la manière dont le 68000 gérait ses accès mémoire. La MMU 68451 gérait un espace d'adressage de 16 mébioctets, découpé en maximum 32 pages/segments. On pouvait dépasser cette limite de 32 segments/pages en combinant plusieurs 68451.
Le Motorola 68851 était une MMU qui était prévue pour fonctionner de paire avec le Motorola 68020. Elle gérait la pagination pour un espace d'adressage de 32 bits.
Les processeurs suivants, les 68030, 68040, et 68060, avaient une MMU interne au processeur.
==La relocation matérielle==
Pour rappel, les systèmes d'exploitation moderne permettent de lancer plusieurs programmes en même temps et les laissent se partager la mémoire. Dans le cas le plus simple, qui n'est pas celui des OS modernes, le système d'exploitation découpe la mémoire en blocs d'adresses contiguës qui sont appelés des '''segments''', ou encore des ''partitions mémoire''. Les segments correspondent à un bloc de mémoire RAM. C'est-à-dire qu'un segment de 259 mébioctets sera un segment continu de 259 mébioctets dans la mémoire physique comme dans la mémoire logique. Dans ce qui suit, un segment contient un programme en cours d'exécution, comme illustré ci-dessous.
[[File:CPT Memory Addressable.svg|centre|vignette|upright=2|Espace d'adressage segmenté.]]
Le système d'exploitation mémorise la position de chaque segment en mémoire, ainsi que d'autres informations annexes. Le tout est regroupé dans la '''table de segment''', un tableau dont chaque case est attribuée à un programme/segment. La table des segments est un tableau numéroté, chaque segment ayant un numéro qui précise sa position dans le tableau. Chaque case, chaque entrée, contient un '''descripteur de segment''' qui regroupe plusieurs informations sur le segment : son adresse de base, sa taille, diverses informations.
===La relocation avec la relocation matérielle : le registre de base===
Un segment peut être placé n'importe où en RAM physique et sa position en RAM change à chaque exécution. Le programme est chargé à une adresse, celle du début du segment, qui change à chaque chargement du programme. Et toutes les adresses utilisées par le programme doivent être corrigées lors du chargement du programme, généralement par l'OS. Cette correction s'appelle la '''relocation''', et elle consiste à ajouter l'adresse de début du segment à chaque adresse manipulée par le programme.
[[File:Relocation assistée par matériel.png|centre|vignette|upright=2.5|Relocation.]]
La relocation matérielle fait que la relocation est faite par le processeur, pas par l'OS. La relocation est intégrée dans le processeur par l'intégration d'un registre : le '''registre de base''', aussi appelé '''registre de relocation'''. Il mémorise l'adresse à laquelle commence le segment, la première adresse du programme. Pour effectuer la relocation, le processeur ajoute automatiquement l'adresse de base à chaque accès mémoire, en allant la chercher dans le registre de relocation.
[[File:Registre de base de segment.png|centre|vignette|upright=2|Registre de base de segment.]]
Le processeur s'occupe de la relocation des segments et le programme compilé n'en voit rien. Pour le dire autrement, les programmes manipulent des adresses logiques, qui sont traduites par le processeur en adresses physiques. La traduction se fait en ajoutant le contenu du registre de relocation à l'adresse logique. De plus, cette méthode fait que chaque programme a son propre espace d'adressage.
[[File:CPU created logical address presentation.png|centre|vignette|upright=2|Traduction d'adresse avec la relocation matérielle.]]
Le système d'exploitation mémorise les adresses de base pour chaque programme, dans la table des segments. Le registre de base est mis à jour automatiquement lors de chaque changement de segment. Pour cela, le registre de base est accessible via certaines instructions, accessibles en espace noyau, plus rarement en espace utilisateur. Le registre de segment est censé être adressé implicitement, vu qu'il est unique. Si ce n'est pas le cas, il est possible d'écrire dans ce registre de segment, qui est alors adressable.
===La protection mémoire avec la relocation matérielle : le registre limite===
Sans restrictions supplémentaires, la taille maximale d'un segment est égale à la taille complète de l'espace d'adressage. Sur les processeurs 32 bits, un segment a une taille maximale de 2^32 octets, soit 4 gibioctets. Mais il est possible de limiter la taille du segment à 2 gibioctets, 1 gibioctet, 64 Kibioctets, ou toute autre taille. La limite est définie lors de la création du segment, mais elle peut cependant évoluer au cours de l'exécution du programme, grâce à l'allocation mémoire.
Le processeur vérifie à chaque accès mémoire que celui-ci se fait bien dans le segment, qu'il ne déborde pas en-dehors. C'est possible qu'une adresse calculée sorte du segment, à la suite d'un bug ou d'une erreur de programmation, voire pire. Et le processeur doit éviter de tels '''débordements de segments'''.
A chaque accès mémoire, le processeur compare l'adresse accédée et vérifie qu'elle est bien dans le segment. Pour cela, il y a deux solutions. La première part du principe que le segment est placé en mémoire entre l'adresse de base et l'adresse limite. Il suffit de mémoriser l'adresse limite, l'adresse physique à ne pas dépasser. Une autre solution mémorise la taille du segment. La table des segments doit donc mémoriser, en plus de l'adresse de base : soit l'adresse maximale du segment, soit la taille du segment. D'autres informations peuvent être ajoutées, comme on le verra plus tard, mais cela complexifie la table des segments.
De plus, le processeur se voit ajouter un '''registre limite''', qui mémorise soit la taille du segment, soit l'adresse limite. Les deux registres, base et limite, sont utilisés pour vérifier si un programme qui lit/écrit de la mémoire en-dehors de son segment attitré : au-delà pour le registre limite, en-deça pour le registre de base. Le processeur vérifie pour chaque accès mémoire ne déborde pas au-delà du segment qui lui est allouée, ce qui n'arrive que si l'adresse d'accès dépasse la valeur du registre limite. Pour les accès en-dessous du segment, il suffit de vérifier si l'addition de relocation déborde, tout débordement signifiant erreur de protection mémoire.
[[File:Registre limite.png|centre|vignette|upright=2|Registre limite]]
Utiliser la taille du segment a de nombreux avantages. L'un d'entre eux se manifeste quand on déplace un segment en mémoire RAM. Le descripteur doit alors être mis à jour, et c'est plus facile quand on utilise la taille du segment. Si on utilise l'adresse limite, il faut mettre à jour à la fois l'adresse de base et l'adresse limite, dans le descripteur. En utilisant la taille, seule l'adresse de base doit être modifiée, vu que le segment n'a pas changé de taille. Un autre avantage est lié aux performances, mais nous devons faire un détour pour le comprendre.
La taille du segment est équivalent à l'adresse logique maximale possible. Par exemple, si un segment fait 256 octets, les adresses logiques possibles vont de 0 à 255, 256 est donc à la fois la taille du segment et l'adresse logique à partir de laquelle on déborde du segment. Et cela marche si on remplace 256 par n'importe quelle valeur : vu que le segment commence à l'adresse 0, sa taille en octets indique l'adresse de dépassement. Interpréter la taille du segment comme une adresse logique fait que les tests avec le registre limite sont plus performants, voyons pourquoi.
En utilisant l'adresse physique limite, on doit faire la relocation, puis comparer l'adresse calculée avec l'adresse limite. Le calcul d'adresse doit se faire avant la vérification. En utilisant la taille, on doit comparer l'adresse logique avec la taille du segment. On peut alors faire le test de débordement avant ou pendant la relocation. Les deux peuvent être faits en parallèle, dans deux circuits distincts, ce qui améliore un peu le temps d'un accès mémoire. Quelques processeurs en ont profité, mais on verra cela dans la section sur la segmentation.
[[File:Comparaison entre adresse limite physique et logique.png|centre|vignette|upright=2|Comparaison entre adresse limite physique et logique]]
Les registres de base et limite sont altérés uniquement par le système d'exploitation et ne sont accessibles qu'en espace noyau. Lorsque le système d'exploitation charge un programme, ou reprend son exécution, il charge les adresses de début/fin du segment dans ces registres. D'ailleurs, ces deux registres doivent être sauvegardés et restaurés lors de chaque interruption. Par contre, et c'est assez évident, ils ne le sont pas lors d'un appel de fonction. Cela fait une différence de plus entre interruption et appels de fonctions.
: Il faut noter que le registre limite et le registre de base sont parfois fusionnés en un seul registre, qui contient un descripteur de segment tout entier.
Pour information, la relocation matérielle avec un registre limite a été implémentée sur plusieurs processeurs assez anciens, notamment sur les anciens supercalculateurs de marque CDC. Un exemple est le fameux CDC 6600, qui implémentait cette technique.
===La mémoire virtuelle avec la relocation matérielle===
Il est possible d'implémenter la mémoire virtuelle avec la relocation matérielle. Pour cela, il faut swapper des segments entiers sur le disque dur. Les segments sont placés en mémoire RAM et leur taille évolue au fur et à mesure que les programmes demandent du rab de mémoire RAM. Lorsque la mémoire est pleine, ou qu'un programme demande plus de mémoire que disponible, des segments entiers sont sauvegardés dans le ''swapfile'', pour faire de la place.
Faire ainsi de demande juste de mémoriser si un segment est en mémoire RAM ou non, ainsi que la position des segments swappés dans le ''swapfile''. Pour cela, il faut modifier la table des segments, afin d'ajouter un '''bit de swap''' qui précise si le segment en question est swappé ou non. Lorsque le système d'exploitation veut swapper un segment, il le copie dans le ''swapfile'' et met ce bit à 1. Lorsque l'OS recharge ce segment en RAM, il remet ce bit à 0. La gestion de la position des segments dans le ''swapfile'' est le fait d'une structure de données séparée de la table des segments.
L'OS exécute chaque programme l'un après l'autre, à tour de rôle. Lorsque le tour d'un programme arrive, il consulte la table des segments pour récupérer les adresses de base et limite, mais il vérifie aussi le bit de swap. Si le bit de swap est à 0, alors l'OS se contente de charger les adresses de base et limite dans les registres adéquats. Mais sinon, il démarre une routine d'interruption qui charge le segment voulu en RAM, depuis le ''swapfile''. C'est seulement une fois le segment chargé que l'on connait son adresse de base/limite et que le chargement des registres de relocation peut se faire.
Un défaut évident de cette méthode est que l'on swappe des programmes entiers, qui sont généralement assez imposants. Les segments font généralement plusieurs centaines de mébioctets, pour ne pas dire plusieurs gibioctets, à l'époque actuelle. Ils étaient plus petits dans l'ancien temps, mais la mémoire était alors plus lente. Toujours est-il que la copie sur le disque dur des segments est donc longue, lente, et pas vraiment compatible avec le fait que les programmes s'exécutent à tour de rôle. Et ca explique pourquoi la relocation matérielle n'est presque jamais utilisée avec de la mémoire virtuelle.
===L'extension d'adressage avec la relocation matérielle===
Passons maintenant à la dernière fonctionnalité implémentable avec la traduction d'adresse : l'extension d'adressage. Elle permet d'utiliser plus de mémoire que ne le permet l'espace d'adressage. Par exemple, utiliser plus de 64 kibioctets de mémoire sur un processeur 16 bits. Pour cela, les adresses envoyées à la mémoire doivent être plus longues que les adresses gérées par le processeur.
L'extension des adresses se fait assez simplement avec la relocation matérielle : il suffit que le registre de base soit plus long. Prenons l'exemple d'un processeur aux adresses de 16 bits, mais qui est reliée à un bus d'adresse de 24 bits. L'espace d'adressage fait juste 64 kibioctets, mais le bus d'adresse gère 16 mébioctets de RAM. On peut utiliser les 16 mébioctets de RAM à une condition : que le registre de base fasse 24 bits, pas 16.
Un défaut de cette approche est qu'un programme ne peut pas utiliser plus de mémoire que ce que permet l'espace d'adressage. Mais par contre, on peut placer chaque programme dans des portions différentes de mémoire. Imaginons par exemple que l'on ait un processeur 16 bits, mais un bus d'adresse de 20 bits. Il est alors possible de découper la mémoire en 16 blocs de 64 kibioctets, chacun attribué à un segment/programme, qu'on sélectionne avec les 4 bits de poids fort de l'adresse. Il suffit de faire démarrer les segments au bon endroit en RAM, et cela demande juste que le registre de base le permette. C'est une sorte d'émulation de la commutation de banques.
==La segmentation en mode réel des processeurs x86==
Avant de passer à la suite, nous allons voir la technique de segmentation de l'Intel 8086, un des tout premiers processeurs 16 bits. Il s'agissait d'une forme très simple de segmentation, sans aucune forme de protection mémoire, ni même de mémoire virtuelle, ce qui le place à part des autres formes de segmentation. Il s'agit d'une amélioration de la relocation matérielle, qui avait pour but de permettre d'utiliser plus de 64 kibioctets de mémoire, ce qui était la limite maximale sur les processeurs 16 bits de l'époque.
Par la suite, la segmentation s'améliora et ajouta un support complet de la mémoire virtuelle et de la protection mémoire. L'ancienne forme de segmentation fut alors appelé le '''mode réel''', et la nouvelle forme de segmentation fut appelée le '''mode protégé'''. Le mode protégé rajoute la protection mémoire, en ajoutant des registres limite et une gestion des droits d'accès aux segments, absents en mode réel. De plus, il ajoute un support de la mémoire virtuelle grâce à l'utilisation d'une des segments digne de ce nom, table qui est absente en mode réel ! Pour le moment, voyons le mode réel.
===Les segments en mode réel===
[[File:Typical computer data memory arrangement.png|vignette|upright=0.5|Typical computer data memory arrangement]]
La segmentation en mode réel sépare la pile, le tas, le code machine et les données constantes dans quatre segments distincts.
* Le segment '''''text''''', qui contient le code machine du programme, de taille fixe.
* Le segment '''''data''''' contient des données de taille fixe qui occupent de la mémoire de façon permanente, des constantes, des variables globales, etc.
* Le segment pour la '''pile''', de taille variable.
* le reste est appelé le '''tas''', de taille variable.
Un point important est que sur ces processeurs, il n'y a pas de table des segments proprement dit. Chaque programme gére de lui-même les adresses de base des segments qu'il manipule. Il n'est en rien aidé par une table des segments gérée par le système d'exploitation.
===Les registres de segments en mode réel===
Chaque segment subit la relocation indépendamment des autres. Pour cela, le processeur intégre plusieurs registres de base, un par segment. Notons que cette solution ne marche que si le nombre de segments par programme est limité, à une dizaine de segments tout au plus. Les processeurs x86 utilisaient cette méthode, et n'associaient que 4 à 6 registres de segments par programme.
Les processeurs 8086 et le 286 avaient quatre registres de segment : un pour le code, un autre pour les données, et un pour la pile, le quatrième étant un registre facultatif laissé à l'appréciation du programmeur. Ils sont nommés CS (''code segment''), DS (''data segment''), SS (''Stack segment''), et ES (''Extra segment''). Le 386 rajouta deux registres, les registres FS et GS, qui sont utilisés pour les segments de données. Les processeurs post-386 ont donc 6 registres de segment.
Les registres CS et SS sont adressés implicitement, en fonction de l'instruction exécutée. Les instructions de la pile manipulent le segment associé à la pile, le chargement des instructions se fait dans le segment de code, les instructions arithmétiques et logiques vont chercher leurs opérandes sur le tas, etc. Et donc, toutes les instructions sont chargées depuis le segment pointé par CS, les instructions de gestion de la pile (PUSH et POP) utilisent le segment pointé par SS.
Les segments DS et ES sont, eux aussi, adressés implicitement. Pour cela, les instructions LOAD/STORE sont dupliquées : il y a une instruction LOAD pour le segment DS, une autre pour le segment ES. D'autres instructions lisent leurs opérandes dans un segment par défaut, mais on peut changer ce choix par défaut en précisant le segment voulu. Un exemple est celui de l'instruction CMPSB, qui compare deux octets/bytes : le premier est chargé depuis le segment DS, le second depuis le segment ES.
Un autre exemple est celui de l'instruction MOV avec un opérande en mémoire. Elle lit l'opérande en mémoire depuis le segment DS par défaut. Il est possible de préciser le segment de destination si celui-ci n'est pas DS. Par exemple, l'instruction MOV [A], AX écrit le contenu du registre AX dans l'adresse A du segment DS. Par contre, l'instruction MOV ES:[A], copie le contenu du registre AX das l'adresse A, mais dans le segment ES.
===La traduction d'adresse en mode réel===
La segmentation en mode réel a pour seul but de permettre à un programme de dépasser la limite des 64 KB autorisée par les adresses de 16 bits. L'idée est que chaque segment a droit à son propre espace de 64 KB. On a ainsi 64 Kb pour le code machine, 64 KB pour la pile, 64 KB pour un segment de données, etc. Les registres de segment mémorisaient la base du segment, les adresses calculées par l'ALU étant des ''offsets''. Ce sont tous des registres de 16 bits, mais ils ne mémorisent pas des adresses physiques de 16 bits, comme nous allons le voir.
[[File:Table des segments dans un banc de registres.png|centre|vignette|upright=2|Table des segments dans un banc de registres.]]
L'Intel 8086 utilisait des adresses de 20 bits, ce qui permet d'adresser 1 mébioctet de RAM. Vous pouvez vous demander comment on peut obtenir des adresses de 20 bits alors que les registres de segments font tous 16 bits ? Cela tient à la manière dont sont calculées les adresses physiques. Le registre de segment n'est pas additionné tel quel avec le décalage : à la place, le registre de segment est décalé de 4 rangs vers la gauche. Le décalage de 4 rangs vers la gauche fait que chaque segment a une adresse qui est multiple de 16. Le fait que le décalage soit de 16 bits fait que les segments ont une taille de 64 kibioctets.
{|class="wikitable"
|-
| <code> </code><code style="background:#DED">0000 0110 1110 1111</code><code>0000</code>
| Registre de segment -
| 16 bits, décalé de 4 bits vers la gauche
|-
| <code>+ </code><code style="background:#DDF">0001 0010 0011 0100</code>
| Décalage/''Offset''
| 16 bits
|-
| colspan="3" |
|-
| <code> </code><code style="background:#FDF">0000 1000 0001 0010 0100</code>
| Adresse finale
| 20 bits
|}
Vous aurez peut-être remarqué que le calcul peut déborder, dépasser 20 bits. Mais nous reviendrons là-dessus plus bas. L'essentiel est que la MMU pour la segmentation en mode réel se résume à quelques registres et des additionneurs/soustracteurs.
Un exemple est l'Intel 8086, un des tout premier processeur Intel. Le processeur était découpé en deux portions : l'interface mémoire et le reste du processeur. L'interface mémoire est appelée la '''''Bus Interface Unit''''', et le reste du processeur est appelé l{{'}}'''''Execution Unit'''''. L'interface mémoire contenait les registres de segment, au nombre de 4, ainsi qu'un additionneur utilisé pour traduire les adresses logiques en adresses physiques. Elle contenait aussi une file d'attente où étaient préchargées les instructions.
Sur le 8086, la MMU est fusionnée avec les circuits de gestion du ''program counter''. Les registres de segment sont regroupés avec le ''program counter'' dans un même banc de registres, et un additionneur unique est utilisé à la fois à incrémenter le ''program counter'' et pour gérer la segmentation. L'additionneur est donc mutualisé entre segmentation et ''program counter''. En somme, il n'y a pas vraiment de MMU dédiée, mais un super-circuit en charge du Fetch et de la mémoire virtuelle, ainsi que du préchargement des instructions. Nous en reparlerons au chapitre suivant.
[[File:80186 arch.png|centre|vignette|upright=2.5|Architecture du 8086, du 80186 et de ses variantes.]]
La MMU du 286 était fusionnée avec l'unité de calcul d'adresse. Elle contient les registres de segments, un comparateur pour détecter les accès hors-segment, et plusieurs additionneurs. Il y a un additionneur pour les calculs d'adresse proprement dit, suivi d'un additionneur pour la relocation.
[[File:Intel i80286 arch.svg|centre|vignette|upright=3|Intel i80286 arch]]
===La segmentation en mode réel accepte plusieurs segments de code/données===
Les programmes peuvent parfaitement répartir leur code machine dans plusieurs segments de code. La limite de 64 KB par segment est en effet assez limitante, et il n'était pas rare qu'un programme stocke son code dans deux ou trois segments. Il en est de même avec les données, qui peuvent être réparties dans deux ou trois segments séparés. La seule exception est la pile : elle est forcément dans un segment unique et ne peut pas dépasser 64 KB.
Pour gérer plusieurs segments de code/donnée, il faut changer de segment à la volée suivant les besoins, en modifiant les registres de segment. Il s'agit de la technique de '''commutation de segment'''. Pour cela, tous les registres de segment, à l'exception de CS, peuvent être altérés par une instruction d'accès mémoire, soit avec une instruction MOV, soit en y copiant le sommet de la pile avec une instruction de dépilage POP. L'absence de sécurité fait que la gestion de ces registres est le fait du programmeur, qui doit redoubler de prudence pour ne pas faire n'importe quoi.
Pour le code machine, le répartir dans plusieurs segments posait des problèmes au niveau des branchements. Si la plupart des branchements sautaient vers une instruction dans le même segment, quelques rares branchements sautaient vers du code machine dans un autre segment. Intel avait prévu le coup et disposait de deux instructions de branchement différentes pour ces deux situations : les '''''near jumps''''' et les '''''far jumps'''''. Les premiers sont des branchements normaux, qui précisent juste l'adresse à laquelle brancher, qui correspond à la position de la fonction dans le segment. Les seconds branchent vers une instruction dans un autre segment, et doivent préciser deux choses : l'adresse de base du segment de destination, et la position de la destination dans le segment. Le branchement met à jour le registre CS avec l'adresse de base, avant de faire le branchement. Ces derniers étaient plus lents, car on n'avait pas à changer de segment et mettre à jour l'état du processeur.
Il y avait la même pour l'instruction d'appel de fonction, avec deux versions de cette instruction. La première version, le '''''near call''''' est un appel de fonction normal, la fonction appelée est dans le segment en cours. Avec la seconde version, le '''''far call''''', la fonction appelée est dans un segment différent. L'instruction a là aussi besoin de deux opérandes : l'adresse de base du segment de destination, et la position de la fonction dans le segment. Un ''far call'' met à jour le registre CS avec l'adresse de base, ce qui fait que les ''far call'' sont plus lents que les ''near call''. Il existe aussi la même chose, pour les instructions de retour de fonction, avec une instruction de retour de fonction normale et une instruction de retour qui renvoie vers un autre segment, qui sont respectivement appelées '''''near return''''' et '''''far return'''''. Là encore, il faut préciser l'adresse du segment de destination dans le second cas.
La même chose est possible pour les segments de données. Sauf que cette fois-ci, ce sont les pointeurs qui sont modifiés. pour rappel, les pointeurs sont, en programmation, des variables qui contiennent des adresses. Lors de la compilation, ces pointeurs sont placés soit dans un registre, soit dans les instructions (adressage absolu), ou autres. Ici, il existe deux types de pointeurs, appelés '''''near pointer''''' et '''''far pointer'''''. Vous l'avez deviné, les premiers sont utilisés pour localiser les données dans le segment en cours d'utilisation, alors que les seconds pointent vers une donnée dans un autre segment. Là encore, la différence est que le premier se contente de donner la position dans le segment, alors que les seconds rajoutent l'adresse de base du segment. Les premiers font 16 bits, alors que les seconds en font 32 : 16 bits pour l'adresse de base et 16 pour l{{'}}''offset''.
===L'occupation de l'espace d'adressage par les segments===
Nous venons de voir qu'un programme pouvait utiliser plus de 4-6 segments, avec la commutation de segment. Mais d'autres programmes faisaient l'inverse, à savoir qu'ils se débrouillaient avec seulement 1 ou 2 segments. Suivant le nombre de segments utilisés, la configuration des registres n'était pas la même. Les configurations possibles sont appelées des ''modèle mémoire'', et il y en a en tout 6. En voici la liste :
{| class="wikitable"
|-
! Modèle mémoire !! Configuration des segments !! Configuration des registres || Pointeurs utilisés || Branchements utilisés
|-
| Tiny* || Segment unique pour tout le programme || CS=DS=SS || ''near'' uniquement || ''near'' uniquement
|-
| Small || Segment de donnée séparé du segment de code, pile dans le segment de données || DS=SS || ''near'' uniquement || ''near'' uniquement
|-
| Medium || Plusieurs segments de code unique, un seul segment de données || CS, DS et SS sont différents || ''near'' et ''far'' || ''near'' uniquement
|-
| Compact || Segment de code unique, plusieurs segments de données || CS, DS et SS sont différents || ''near'' uniquement || ''near'' et ''far''
|-
| Large || Plusieurs segments de code, plusieurs segments de données || CS, DS et SS sont différents || ''near'' et ''far'' || ''near'' et ''far''
|}
Un programme est censé utiliser maximum 4-6 segments de 64 KB, ce qui permet d'adresser maximum 64 * 6 = 384 KB de RAM, soit bien moins que le mébioctet de mémoire théoriquement adressable. Mais ce défaut est en réalité contourné par la commutation de segment, qui permettait d'adresser la totalité de la RAM si besoin. Une second manière de contourner cette limite est que plusieurs processus peuvent s'exécuter sur un seul processeur, si l'OS le permet. Ce n'était pas le cas à l'époque du DOS, qui était un OS mono-programmé, mais c'était en théorie possible. La limite est de 6 segments par programme/processus, en exécuter plusieurs permet d'utiliser toute la mémoire disponible rapidement.
[[File:Overlapping realmode segments.svg|vignette|Segments qui se recouvrent en mode réel.]]
Vous remarquerez qu'avec des registres de segments de 16 bits, on peut gérer 65536 segments différents, chacun de 64 KB. Et 65 536 segments de 64 kibioctets, ça ne rentre pas dans le mébioctet de mémoire permis avec des adresses de 20 bits. La raison est que plusieurs couples segment+''offset'' pointent vers la même adresse. En tout, chaque adresse peut être adressée par 4096 couples segment+''offset'' différents.
L'avantage de cette méthode est que des segments peuvent se recouvrir, à savoir que la fin de l'un se situe dans le début de l'autre, comme illustré ci-contre. Cela permet en théorie de partager de la mémoire entre deux processus. Mais la technique est tout sauf pratique et est donc peu utilisée. Elle demande de placer minutieusement les segments en RAM, et les données à partager dans les segments. En pratique, les programmeurs et OS utilisent des segments qui ne se recouvrent pas et sont disjoints en RAM.
Le nombre maximal de segments disjoints se calcule en prenant la taille de la RAM, qu'on divise par la taille d'un segment. Le calcul donne : 1024 kibioctets / 64 kibioctets = 16 segments disjoints. Un autre calcul prend le nombre de segments divisé par le nombre d'adresses aliasées, ce qui donne 65536 / 4096 = 16. Seulement 16 segments, c'est peu. En comptant les segments utilisés par l'OS et ceux utilisés par le programme, la limite est vite atteinte si le programme utilise la commutation de segment.
===Le mode réel sur les 286 et plus : la ligne d'adresse A20===
Pour résumer, le registre de segment contient des adresses de 20 bits, dont les 4 bits de poids faible sont à 0. Et il se voit ajouter un ''offset'' de 16 bits. Intéressons-nous un peu à l'adresse maximale que l'on peut calculer avec ce système. Nous allons l'appeler l{{'}}'''adresse maximale de segmentation'''. Elle vaut :
{|class="wikitable"
|-
| <code> </code><code style="background:#DED">1111 1111 1111 1111</code><code>0000</code>
| Registre de segment -
| 16 bits, décalé de 4 bits vers la gauche
|-
| <code>+ </code><code style="background:#DDF">1111 1111 1111 1111</code>
| Décalage/''Offset''
| 16 bits
|-
| colspan="3" |
|-
| <code> </code><code style="background:#FDF">1 0000 1111 1111 1110 1111</code>
| Adresse finale
| 20 bits
|}
Le résultat n'est pas l'adresse maximale codée sur 20 bits, car l'addition déborde. Elle donne un résultat qui dépasse l'adresse maximale permis par les 20 bits, il y a un 21ème bit en plus. De plus, les 20 bits de poids faible ont une valeur bien précise. Ils donnent la différence entre l'adresse maximale permise sur 20 bit, et l'adresse maximale de segmentation. Les bits 1111 1111 1110 1111 traduits en binaire donnent 65 519; auxquels il faut ajouter l'adresse 1 0000 0000 0000 0000. En tout, cela fait 65 520 octets adressables en trop. En clair : on dépasse la limite du mébioctet de 65 520 octets. Le résultat est alors très différent selon que l'on parle des processeurs avant le 286 ou après.
Avant le 286, le bus d'adresse faisait exactement 20 bits. Les adresses calculées ne pouvaient pas dépasser 20 bits. L'addition générait donc un débordement d'entier, géré en arithmétique modulaire. En clair, les bits de poids fort au-delà du vingtième sont perdus. Le calcul de l'adresse débordait et retournait au début de la mémoire, sur les 65 520 premiers octets de la mémoire RAM.
[[File:IBM PC Memory areas.svg|vignette|IBM PC Memory Map, la ''High memory area'' est en jaune.]]
Le 80286 en mode réel gère des adresses de base de 24 bits, soit 4 bits de plus que le 8086. Le résultat est qu'il n'y a pas de débordement. Les bits de poids fort sont conservés, même au-delà du 20ème. En clair, la segmentation permettait de réellement adresser 65 530 octets au-delà de la limite de 1 mébioctet. La portion de mémoire adressable était appelé la '''''High memory area''''', qu'on va abrévier en HMA.
{| class="wikitable"
|+ Espace d'adressage du 286
|-
! Adresses en héxadécimal !! Zone de mémoire
|-
| 10 FFF0 à FF FFFF || Mémoire étendue, au-delà du premier mébioctet
|-
| 10 0000 à 10 FFEF || ''High Memory Area''
|-
| 0 à 0F FFFF || Mémoire adressable en mode réel
|}
En conséquence, les applications peuvent utiliser plus d'un mébioctet de RAM, mais au prix d'une rétrocompatibilité imparfaite. Quelques programmes DOS ne marchaient pus à cause de ça. D'autres fonctionnaient convenablement et pouvaient adresser les 65 520 octets en plus.
Pour résoudre ce problème, les carte mères ajoutaient un petit circuit relié au 21ème bit d'adresse, nommé A20 (pas d'erreur, les fils du bus d'adresse sont numérotés à partir de 0). Le circuit en question pouvait mettre à zéro le fil d'adresse, ou au contraire le laisser tranquille. En le forçant à 0, le calcul des adresses déborde comme dans le mode réel des 8086. Mais s'il ne le fait pas, la ''high memory area'' est adressable. Le circuit était une simple porte ET, qui combinait le 21ème bit d'adresse avec un '''signal de commande A20''' provenant d'ailleurs.
Le signal de commande A20 était géré par le contrôleur de clavier, qui était soudé à la carte mère. Le contrôleur en question ne gérait pas que le clavier, il pouvait aussi RESET le processeur, alors gérer le signal de commande A20 n'était pas si problématique. Quitte à avoir un microcontrôleur sur la carte mère, autant s'en servir au maximum... La gestion du bus d'adresse étaitdonc gérable au clavier. D'autres carte mères faisaient autrement et préféraient ajouter un interrupteur, pour activer ou non la mise à 0 du 21ème bit d'adresse.
: Il faut noter que le signal de commande A20 était mis à 1 en mode protégé, afin que le 21ème bit d'adresse soit activé.
Le 386 ajouta deux registres de segment, les registres FS et GS, ainsi que le '''mode ''virtual 8086'''''. Ce dernier permet d’exécuter des programmes en mode réel alors que le système d'exploitation s'exécute en mode protégé. C'est une technique de virtualisation matérielle qui permet d'émuler un 8086 sur un 386. L'avantage est que la compatibilité avec les programmes anciens écrits pour le 8086 est conservée, tout en profitant de la protection mémoire. Tous les processeurs x86 qui ont suivi supportent ce mode virtuel 8086.
==La segmentation avec une table des segments==
La '''segmentation avec une table des segments''' est apparue sur des processeurs assez anciens, le tout premier étant le Burrough 5000. Elle a ensuite été utilisée sur les processeurs x86 des PCs, à partir du 286 d'Intel. Elle est aujourd'hui abandonnée sur les jeux d'instruction x86. Tout comme la segmentation en mode réel, la segmentation attribue plusieurs segments par programmes ! Et cela a des répercutions sur la manière dont la traduction d'adresse est effectuée.
===Pourquoi plusieurs segments par programme ?===
L'utilité d'avoir plusieurs segments par programme n'est pas évidente, mais elle le devient quand on se plonge dans le passé. Dans le passé, les programmeurs devaient faire avec une quantité de mémoire limitée et il n'était pas rare que certains programmes utilisent plus de mémoire que disponible sur la machine. Mais les programmeurs concevaient leurs programmes en fonction.
[[File:Overlay Programming.svg|vignette|upright=1|Overlay Programming]]
L'idée était d'implémenter un système de mémoire virtuelle, mais émulé en logiciel, appelé l{{'}}'''''overlaying'''''. Le programme était découpé en plusieurs morceaux, appelés des ''overlays''. Les ''overlays'' les plus importants étaient en permanence en RAM, mais les autres étaient faisaient un va-et-vient entre RAM et disque dur. Ils étaient chargés en RAM lors de leur utilisation, puis sauvegardés sur le disque dur quand ils étaient inutilisés. Le va-et-vient des ''overlays'' entre RAM et disque dur était réalisé en logiciel, par le programme lui-même. Le matériel n'intervenait pas, comme c'est le cas avec la mémoire virtuelle.
Avec la segmentation, un programme peut utiliser la technique des ''overlays'', mais avec l'aide du matériel. Il suffit de mettre chaque ''overlay'' dans son propre segment, et laisser la segmentation faire. Les segments sont swappés en tout ou rien : on doit swapper tout un segment en entier. L'intérêt est que la gestion du ''swapping'' est grandement facilitée, vu que c'est le système d'exploitation qui s'occupe de swapper les segments sur le disque dur ou de charger des segments en RAM. Pas besoin pour le programmeur de coder quoique ce soit. Par contre, cela demande l'intervention du programmeur, qui doit découper le programme en segments/''overlays'' de lui-même. Sans cela, la segmentation n'est pas très utile.
L{{'}}''overlaying'' est une forme de '''segmentation à granularité grossière''', à savoir que le programme est découpé en segments de grande taille. L'usage classique est d'avoir un segment pour la pile, un autre pour le code exécutable, un autre pour le reste. Éventuellement, on peut découper les trois segments précédents en deux ou trois segments, rarement au-delà. Les segments sont alors peu nombreux, guère plus d'une dizaine par programme. D'où le terme de ''granularité grossière''.
La '''segmentation à granularité fine''' pousse le concept encore plus loin. Avec elle, il y a idéalement un segment par entité manipulée par le programme, un segment pour chaque structure de donnée et/ou chaque objet. Par exemple, un tableau aura son propre segment, ce qui est idéal pour détecter les accès hors tableau. Pour les listes chainées, chaque élément de la liste aura son propre segment. Et ainsi de suite, chaque variable agrégée (non-primitive), chaque structure de donnée, chaque objet, chaque instance d'une classe, a son propre segment. Diverses fonctionnalités supplémentaires peuvent être ajoutées, ce qui transforme le processeur en véritable processeur orienté objet, mais passons ces détails pour le moment.
Vu que les segments correspondent à des objets manipulés par le programme, on peut deviner que leur nombre évolue au cours du temps. En effet, les programmes modernes peuvent demander au système d'exploitation du rab de mémoire pour allouer une nouvelle structure de données. Avec la segmentation à granularité fine, cela demande d'allouer un nouveau segment à chaque nouvelle allocation mémoire, à chaque création d'une nouvelle structure de données ou d'un objet. De plus, les programmes peuvent libérer de la mémoire, en supprimant les structures de données ou objets dont ils n'ont plus besoin. Avec la segmentation à granularité fine, cela revient à détruire le segment alloué pour ces objets/structures de données. Le nombre de segments est donc dynamique, il change au cours de l'exécution du programme.
===Les tables de segments avec la segmentation===
La présence de plusieurs segments par programme a un impact sur la table des segments. Avec la relocation matérielle, elle conte nait un segment par programme. Chaque entrée, chaque ligne de la table des segment, mémorisait l'adresse de base, l'adresse limite, un bit de présence pour la mémoire virtuelle et des autorisations liées à la protection mémoire. Avec la segmentation, les choses sont plus compliquées, car il y a plusieurs segments par programme. Les entrées ne sont pas modifiées, mais elles sont organisées différemment.
Avec cette forme de segmentation, la table des segments doit respecter plusieurs contraintes. Premièrement, il y a plusieurs segments par programmes. Deuxièmement, le nombre de segments est variable : certains programmes se contenteront d'un seul segment, d'autres de dizaine, d'autres plusieurs centaines, etc. Il y a typiquement deux manières de faire : soit utiliser une table des segments uniques, utiliser une table des segment par programme.
Il est possible d'utiliser une table des segment unique qui mémorise tous les segments de tous les processus, système d'exploitation inclut. On parle alors de '''table des segment globale'''. Mais cette solution n'est pas utilisée avec la segmentation proprement dite. Elle est utilisée sur les architectures à capacité qu'on détaillera vers la fin du chapitre, dans une section dédiée. À la place, la segmentation utilise une table de segment par processus/programme, chacun ayant une '''table des segment locale'''.
Dans les faits, les choses sont plus compliquées. Le système d'exploitation doit savoir où se trouvent les tables de segment locale pour chaque programme. Pour cela, il a besoin d'utiliser une table de segment globale, dont chaque entrée pointe non pas vers un segment, mais vers une table de segment locale. Lorsque l'OS effectue une commutation de contexte, il lit la table des segment globale, pour récupérer un pointeur vers celle-ci. Ce pointeur est alors chargé dans un registre du processeur, qui mémorise l'adresse de la table locale, ce qui sert lors des accès mémoire.
Une telle organisation fait que les segments d'un processus/programme sont invisibles pour les autres, il y a une certaine forme de sécurité. Un programme ne connait que sa table de segments locale, il n'a pas accès directement à la table des segments globales. Tout accès mémoire se passera à travers la table de segment locale, il ne sait pas où se trouvent les autres tables de segment locales.
Les processeurs x86 sont dans ce cas : ils utilisent une table de segment globale couplée à autant de table des segments qu'il y a de processus en cours d'exécution. La table des segments globale s'appelle la '''''Global Descriptor Table''''' et elle peut contenir 8192 segments maximum, ce qui permet le support de 8192 processus différents. Les tables de segments locales sont appelées les '''''Local Descriptor Table''''' et elles font aussi 8192 segments maximum, ce qui fait 8192 segments par programme maximum. Il faut noter que la table de segment globale peut mémoriser des pointeurs vers les routines d'interruption, certaines données partagées (le tampon mémoire pour le clavier) et quelques autres choses, qui n'ont pas leur place dans les tables de segment locales.
===La relocation avec la segmentation===
La table des segments locale mémorise les adresses de base et limite de chaque segment, ainsi que d'autres méta-données. Les informations pour un segment sont regroupés dans un '''descripteur de segment''', qui est codé sur plusieurs octets, et qui regroupe : adresse de base, adresse limite, bit de présence en RAM, méta-données de protection mémoire.
La table des segments est un tableau dans lequel les descripteurs de segment sont placés les uns à la suite des autres en mémoire RAM. La table des segments est donc un tableau de segment. Les segments d'un programme sont numérotés, le nombre s'appelant un '''indice de segment''', appelé '''sélecteur de segment''' dans la terminologie Intel. L'indice de segment n'est autre que l'indice du segment dans ce tableau.
[[File:Global Descriptor table.png|centre|vignette|upright=2|Table des segments locale.]]
Il n'y a pas de registre de segment proprement dit, qui mémoriserait l'adresse de base. À la place, les segments sont adressés de manière indirecte. À la place, les registres de segment mémorisent des sélecteurs de segment. Ils sont utilisés pour lire l'adresse de base/limite dans la table de segment en mémoire RAM. Pour cela, un registre mémorise l'adresse de la table de segment locale, sa position en mémoire RAM.
Toute lecture ou écriture se fait en deux temps, en deux accès mémoire, consécutifs. Premièrement, le numéro de segment est utilisé pour adresser la table des segment. La lecture récupère alors un pointeur vers ce segment. Deuxièmement, ce pointeur est utilisé pour faire la lecture ou écriture. Plus précisément, la première lecture récupère un descripteur de segment qui contient l'adresse de base, le pointeur voulu, mais aussi l'adresse limite et d'autres informations.
[[File:Segmentation avec table des segments.png|centre|vignette|upright=2|Segmentation avec table des segments]]
L'accès à la table des segments se fait automatiquement à chaque accès mémoire. La conséquence est que chaque accès mémoire demande d'en faire deux : un pour lire la table des segments, l'autre pour l'accès lui-même. Il s'agit en quelque sorte d'une forme d'adressage indirect mémoire.
Un point important est que si le premier accès ne fait qu'une simple lecture dans un tableau, le second accès implique des calculs d'adresse. En effet, le premier accès récupère l'adresse de base du segment, mais le second accès sélectionne une donnée dans le segment, ce qui demande de calculer son adresse. L'adresse finale se déduit en combinant l'adresse de base avec un décalage (''offset'') qui donne la position de la donnée dans ce segment. L'indice de segment est utilisé pour récupérer l'adresse de base du segment. Une fois cette adresse de base connue, on lui additionne le décalage pour obtenir l'adresse finale.
[[File:Table des segments.png|centre|vignette|upright=2|Traduction d'adresse avec une table des segments.]]
Pour effectuer automatiquement l'accès à la table des segments, le processeur doit contenir un registre supplémentaire, qui contient l'adresse de la table de segment, afin de la localiser en mémoire RAM. Nous appellerons ce registre le '''pointeur de table'''. Le pointeur de table est combiné avec l'indice de segment pour adresser le descripteur de segment adéquat.
[[File:Segment 2.svg|centre|vignette|upright=2|Traduction d'adresse avec une table des segments, ici appelée table globale des de"scripteurs (terminologie des processeurs Intel x86).]]
Un point important est que la table des segments n'est pas accessible pour le programme en cours d'exécution. Il ne peut pas lire le contenu de la table des segments, et encore moins la modifier. L'accès se fait seulement de manière indirecte, en faisant usage des indices de segments, mais c'est un adressage indirect. Seul le système d'exploitation peut lire ou écrire la table des segments directement.
Plus haut, j'ai dit que tout accès mémoire impliquait deux accès mémoire : un pour charger le descripteur de segment, un autre pour la lecture/écriture proprement dite. Cependant, cela aurait un impact bien trop grand sur les performances. Dans les faits, les processeurs avec segmentations intégraient un '''cache de descripteurs de segments''', pour limiter la casse. Quand un descripteur de segment est lu depuis la RAM, il est copié dans ce cache. Les accès ultérieurs accédent au descripteur dans le cache, pas besoin de passer par la RAM. L'intel 386 avait un cache de ce type.
===La protection mémoire : les accès hors-segments===
Comme avec la relocation matérielle, le processeur détecte les débordements de segment. Pour cela, il compare l'adresse logique accédée avec l'adresse limite, ou compare la taille limite avec le décalage. De nombreux processeurs, comme l'Intel 386, préféraient utiliser la taille du segment, pour une question d'optimisation. En effet, si on compare l'adresse finale avec l'adresse limite, on doit faire la relocation avant de comparer l'adresse relocatée. Mais en utilisant la taille, ce n'est pas le cas : on peut faire la comparaison avant, pendant ou après la relocation.
Un détail à prendre en compte est la taille de la donnée accédée. Sans cela, la comparaison serait très simple : on vérifie si ''décalage <= taille du segment'', ou on compare des adresses de la même manière. Mais imaginez qu'on accède à une donnée de 4 octets : il se peut que l'adresse de ces 4 octets rentre dans le segment, mais que quelques octets débordent. Par exemple, les deux premiers octets sont dans le segment, mais pas les deux suivants. La vraie comparaison est alors : ''décalage + 4 octets <= taille du segment''.
Mais il est possible de faire le calcul autrement, et quelques processeurs comme l'Intel 386 ne s'en sont pas privé. Il calculait la différence ''taille du segment - décalage'', et vérifiait le résultat. Le processeur gérait des données de 1, 2 et 4 octets, ce qui fait que le résultat devait être entre 0 et 3. Le processeur prenait le résultat de la soustraction, et vérifiait alors que les 30 bits de poids fort valaient bien 0. Il vérifiait aussi que les deux bits de poids faible avaient la bonne valeur.
[[File:Vm7.svg|centre|vignette|upright=2|Traduction d'adresse avec vérification des accès hors-segment.]]
Une nouveauté fait son apparition avec la segmentation : la '''gestion des droits d'accès'''. Par exemple, il est possible d'interdire d'exécuter le contenu d'un segment, ce qui fournit une protection contre certaines failles de sécurité ou certains virus. Lorsqu'on exécute une opération interdite, le processeur lève une exception matérielle, à charge du système d'exploitation de gérer la situation.
Pour cela, chaque segment se voit attribuer un certain nombre d'autorisations d'accès qui indiquent si l'on peut lire ou écrire dedans, si celui-ci contient un programme exécutable, etc. Les autorisations pour chaque segment sont placées dans le descripteur de segment. Elles se résument généralement à quelques bits, qui indiquent si le segment est accesible en lecture/écriture ou exécutable. Le tout est souvent concaténé dans un ou deux '''octets de droits d'accès'''.
L'implémentation de la protection mémoire dépend du CPU considéré. Les CPU microcodés peuvent en théorie utiliser le microcode. Lorsqu'une instruction mémoire s'exécute, le microcode effectue trois étapes : lire le descripteur de segment, faire les tests de protection mémoire, exécuter la lecture/écriture ou lever une exception. Létape de test est réalisée avec un ou plusieurs micro-branchements. Par exemple, une écriture va tester le bit R/W du descripteur, qui indique si on peut écrire dans le segment, en utilisant un micro-branchement. Le micro-branchement enverra vers une routine du microcode en cas d'erreur.
Les tests de protection mémoire demandent cependant de tester beaucoup de conditions différentes. Par exemple, le CPU Intel 386 testait moins d'une dizaine de conditions pour certaines instructions. Il est cependant possible de faire plusieurs comparaisons en parallèle en rusant un peu. Il suffit de mémoriser les octets de droits d'accès dans un registre interne, de masquer les bits non-pertinents, et de faire une comparaison avec une constante adéquate, qui encode la valeur que doivent avoir ces bits.
Une solution alternative utiliser un circuit combinatoire pour faire les tests de protection mémoire. Les tests sont alors faits en parallèles, plutôt qu'un par un par des micro-branchements. Par contre, le cout en matériel est assez important. Il faut ajouter ce circuit combinatoire, ce qui demande pas mal de circuits.
===La mémoire virtuelle avec la segmentation===
La mémoire virtuelle est une fonctionnalité souvent implémentée sur les processeurs qui gèrent la segmentation, alors que les processeurs avec relocation matérielle s'en passaient. Il faut dire que l'implémentation de la mémoire virtuelle est beaucoup plus simple avec la segmentation, comparé à la relocation matérielle. Le remplacement des registres de base par des sélecteurs de segment facilite grandement l'implémentation.
Le problème de la mémoire virtuelle est que les segments peuvent être swappés sur le disque dur n'importe quand, sans que le programme soit prévu. Le swapping est réalisé par une interruption de l'OS, qui peut interrompre le programme n'importe quand. Et si un segment est swappé, le registre de base correspondant devient invalide, il point sur une adresse en RAM où le segment était, mais n'est plus. De plus, les segments peuvent être déplacés en mémoire, là encore n'importe quand et d'une manière invisible par le programme, ce qui fait que les registres de base adéquats doivent être modifiés.
Si le programme entier est swappé d'un coup, comme avec la relocation matérielle simple, cela ne pose pas de problèmes. Mais dès qu'on utilise plusieurs registres de base par programme, les choses deviennent soudainement plus compliquées. Le problème est qu'il n'y a pas de mécanismes pour choisir et invalider le registre de base adéquat quand un segment est déplacé/swappé. En théorie, on pourrait imaginer des systèmes qui résolvent le problème au niveau de l'OS, mais tous ont des problèmes qui font que l'implémentation est compliquée ou que les performances sont ridicules.
L'usage d'une table des segments accédée à chaque accès résout complètement le problème. La table des segments est accédée à chaque accès mémoire, elle sait si le segment est swappé ou non, chaque accès vérifie si le segment est en mémoire et quelle est son adresse de base. On peut changer le segment de place n'importe quand, le prochain accès récupérera des informations à jour dans la table des segments.
L'implémentation de la mémoire virtuelle avec la segmentation est simple : il suffit d'ajouter un bit dans les descripteurs de segments, qui indique si le segment est swappé ou non. Tout le reste, la gestion de ce bit, du swap, et tout ce qui est nécessaire, est délégué au système d'exploitation. Lors de chaque accès mémoire, le processeur vérifie ce bit avant de faire la traduction d'adresse, et déclenche une exception matérielle si le bit indique que le segment est swappé. L'exception matérielle est gérée par l'OS.
===Le partage de segments===
Il est possible de partager un segment entre plusieurs applications. Cela peut servir pour partager des données entre deux programmes : un segment de données partagées est alors partagé entre deux programmes. Partager un segment de code est utile pour les bibliothèques partagées : la bibliothèque est placée dans un segment dédié, qui est partagé entre les programmes qui l'utilisent. Partager un segment de code est aussi utile quand plusieurs instances d'une même application sont lancés simultanément : le code n'ayant pas de raison de changer, celui-ci est partagé entre toutes les instances. Mais ce n'est là qu'un exemple.
La première solution pour cela est de configurer les tables de segment convenablement. Le même segment peut avoir des droits d'accès différents selon les processus. Les adresses de base/limite sont identiques, mais les tables des segments ont alors des droits d'accès différents. Mais cette méthode de partage des segments a plusieurs défauts.
Premièrement, les sélecteurs de segments ne sont pas les mêmes d'un processus à l'autre, pour un même segment. Le segment partagé peut correspondre au segment numéro 80 dans le premier processus, au segment numéro 1092 dans le second processus. Rien n'impose que les sélecteurs de segment soient les mêmes d'un processus à l'autre, pour un segment identique.
Deuxièmement, les adresses limite et de base sont dupliquées dans plusieurs tables de segments. En soi, cette redondance est un souci mineur. Mais une autre conséquence est une question de sécurité : que se passe-t-il si jamais un processus a une table des segments corrompue ? Il se peut que pour un segment identique, deux processus n'aient pas la même adresse limite, ce qui peut causer des failles de sécurité. Un processus peut alors subir un débordement de tampon, ou tout autre forme d'attaque.
[[File:Vm9.png|centre|vignette|upright=2|Illustration du partage d'un segment entre deux applications.]]
Une seconde solution, complémentaire, utilise une table de segment globale, qui mémorise des segments partagés ou accessibles par tous les processus. Les défauts de la méthode précédente disparaissent avec cette technique : un segment est identifié par un sélecteur unique pour tous les processus, il n'y a pas de duplication des descripteurs de segment. Par contre, elle a plusieurs défauts.
Le défaut principal est que cette table des segments est accessible par tous les processus, impossible de ne partager ses segments qu'avec certains pas avec les autres. Un autre défaut est que les droits d'accès à un segment partagé sont identiques pour tous les processus. Impossible d'avoir un segment partagé accessible en lecture seule pour un processus, mais accessible en écriture pour un autre. Il est possible de corriger ces défauts, mais nous en parlerons dans la section sur les architectures à capacité.
===L'extension d'adresse avec la segmentation===
L'extension d'adresse est possible avec la segmentation, de la même manière qu'avec la relocation matérielle. Il suffit juste que les adresses de base soient aussi grandes que le bus d'adresse. Mais il y a une différence avec la relocation matérielle : un même programme peut utiliser plus de mémoire qu'il n'y en a dans l'espace d'adressage. La raison est simple : un segment peut prendre tout l'espace d'adressage, et il y a plusieurs segments par programme.
Pour donner un exemple, prenons un processeur 16 bits, qui peut adresser 64 kibioctets, associé à une mémoire de 4 mébioctets. Il est possible de placer le code machine dans les premiers 64k de la mémoire, la pile du programme dans les 64k suivants, le tas dans les 64k encore après, et ainsi de suite. Le programme dépasse donc les 64k de mémoire de l'espace d'adressage. Ce genre de chose est impossible avec la relocation, où un programme est limité par l'espace d'adressage.
===Le mode protégé des processeurs x86===
L'Intel 80286, aussi appelé 286, ajouta un mode de segmentation séparé du mode réel, qui ajoute une protection mémoire à la segmentation, ce qui lui vaut le nom de '''mode protégé'''. Dans ce mode, les registres de segment ne contiennent pas des adresses de base, mais des sélecteurs de segments qui sont utilisés pour l'accès à la table des segments en mémoire RAM.
Le 286 bootait en mode réel, puis le système d'exploitation devait faire quelques manipulations pour passer en mode protégé. Le 286 était pensé pour être rétrocompatible au maximum avec le 80186. Mais les différences entre le 286 et le 8086 étaient majeures, au point que les applications devaient être réécrites intégralement pour profiter du mode protégé. Un mode de compatibilité permettait cependant aux applications destinées au 8086 de fonctionner, avec même de meilleures performances. Aussi, le mode protégé resta inutilisé sur la plupart des applications exécutées sur le 286.
Vint ensuite le processeur 80386, renommé en 386 quelques années plus tard. Sur ce processeur, les modes réel et protégé sont conservés tel quel, à une différence près : toutes les adresses passent à 32 bits, qu'il s'agisse des adresses de base, limite ou des ''offsets''. Le processeur peut donc adresser un grand nombre de segments : 2^32, soit plus de 4 milliards. Les segments grandissent aussi et passent de 64 KB maximum à 4 gibioctets maximum. Mais surtout : le 386 ajouta le support de la pagination en plus de la segmentation. Ces modifications ont été conservées sur les processeurs 32 bits ultérieurs.
Les processeurs x86 gèrent deux types de tables des segments : une table locale pour chaque processus, et une table globale partagée entre tous les processus. Il ne peut y avoir qu'une table locale d'active, vu que le processeur ne peut exécuter qu'un seul processus en même temps. Chaque table locale définit 8192 segments, pareil pour la table globale. La table globale est utilisée pour les segments du noyau et la mémoire partagée entre processus. Un défaut est qu'un segment partagé par la table globale est visible par tous les processus, avec les mêmes droits d'accès. Ce qui fait que cette méthode était peu utilisée en pratique. La table globale mémorise aussi des pointeurs vers les tables locales, avec un descripteur de segment par table locale.
Sur les processeurs x86 32 bits, un descripteur de segment est organisé comme suit, pour les architectures 32 bits. On y trouve l'adresse de base et la taille limite, ainsi que de nombreux bits de contrôle.
Le premier groupe de bits de contrôle est l'octet en bleu à droite. Il contient :
* le bit P qui indique que l'entrée contient un descripteur valide, qu'elle n'est pas vide ;
* deux bits DPL qui indiquent le niveau de privilège du segment (noyau, utilisateur, les deux intermédiaires spécifiques au x86) ;
* un bit S qui précise si le segment est de type système (utiles pour l'OS) ou un segment de code/données.
* un champ Type qui contient les bits suivants :
** un bit E qui indique si le segment contient du code exécutable ou non ;
** le bit RW qui indique s'il est en lecture seule ou non ;;
** Un bit A qui indique que le segment a récemment été accédé, information utile pour l'OS;
** un bit DC assez spécifiques.
En haut à gauche, en bleu, on trouve deux bits :
* Le bit G indique comment interpréter la taille contenue dans le descripteur : 0 si la taille est exprimée en octets, 1 si la taille est un nombre de pages de 4 kibioctets. Ce bit précise si on utilise la segmentation seule, ou combinée avec la pagination.
* Le bit DB précise si l'on utilise des segments en mode de compatibilité 16 bits ou des segments 32 bits.
[[File:SegmentDescriptor.svg|centre|vignette|upright=3|Segment Descriptor]]
Les indices de segment sont appelés des sélecteurs de segment. Ils ont une taille de 16 bits, mais 3 bits sont utilisés pour encoder des méta-données. Le numéro de segment est donc codé sur 13 bits, ce qui permettait de gérer maximum 8192 segments par table de segment (locale ou globale). Les 16 bits sont organisés comme suit :
* 13 bits pour le numéro du segment dans la table des segments, l'indice de segment proprement dit ;
* un bit qui précise s'il faut accéder à la table des segments globale ou locale ;
* deux bits qui indiquent le niveau de privilège de l'accès au segment (les 4 niveaux de protection, dont l'espace noyau et utilisateur).
[[File:SegmentSelector.svg|centre|vignette|upright=1.5|Sélecteur de segment 16 bit.]]
En tout, l'indice permet de gérer 8192 segments pour la table locale et 8192 segments de la table globale.
====La MMU du 386/486 : cache de segment, protection mémoire====
La MMU du 386 et celle du 486 étaient assez similaires. Elles étaient plus complexes que celle du 186 et du 286. Elle contenait un additionneur pour les calculs d'adresse et un comparateur pour tester si l'accès mémoire déborde d'un segment. Le test de débordement se faisait en parallèle du calcul de l'adresse finale, comme sur le 286.
L'additionneur était un additionneur trois-opérandes, qui additionnait l'adresse à lire/écrire, l'adresse de base du segment, et un décalage intégré dans l'instruction. En clair, l'unité de segmentation n'était pas qu'une MMU, elle prenait en charge une partie du calcul d'adresse. L'avantage est que cela permettait de gérer les modes d'adressage "base + décalage" et "base + indice + décalage" très facilement, en utilisant un minimum de circuits. Une conséquence de cette organisation était que l'usage d'un décalage était gratuit. Par contre, dès qu'on utilisait l'adressage "Base + Indice", avec ou sans décalage, l'instruction prenait un cycle de plus à s'exécuter, parce qu'il fallait faire l'addition "Base + Indice" dans l'ALU entière.
Le CPU 386 était le premier à implémenter la protection mémoire avec des segments. Pour cela, il intégrait une '''''Protection Test Unit''''', séparée du microcode, qu'on va abrévier en PTU. Précisément, il s'agissait d'un PLA (''Programmable Logic Array''), une sorte d'intermédiaire entre circuit logique fait sur mesure et mémoire ROM, qu'on a déjà abordé dans le chapitre sur les mémoires ROM. Mais cette unité ne faisait pas tout, le microcode était aussi impliqué. La PTU sera détaillée dans la section suivante.
Pour améliorer les performances, le 386 et le 486 intégraient un '''cache de descripteurs de segment''', aussi appelé le cache de descripteurs. Lorsqu'un descripteur état chargé pour la première fois, il était copié dans le cache de descripteurs de segment. Les accès mémoire ultérieur lisaient le descripteur de segment depuis ce cache, pas depuis la table des segments en RAM. Le cache de descripteurs gère aussi bien les segments en mode réel qu'en mode protégé. Récupérer l'adresse de base depuis cache se fait un peu différemment en mode réel et protégé, mais le cache gère cela tout seul. Idem pour récupérer la taille/adresse limite.
[[File:Microarchitecture du 386, avec focus sur la segmentation.png|centre|vignette|upright=2|Microarchitecture du 386 et du 486, avec focus sur la segmentation.]]
En mode réel, la taille des segment est censée être limitée à 64 kibioctets. Mais le processeur ne vérifiait pas si cette limite était dépassée. À la place, il utilisait la limite précisée dans le cache de segments. Pire que ça : le cache de segment n'était pas réinitialisé quand on passe du mode réel au mode protégé, et réciproquement. Et cette propriété a été à l'origine de l''''''unreal mode'''''. Il s'agissait d'un mode réel amélioré, capable d'utiliser des segments de 4 gibioctets et des adresses de 32 bits.
Passer en mode ''unreal'' pouvait se faire de deux manières. Il était possible d'altérer le cache de descripteur en utilisant l'instruction non-documentée LOADALL. Elle permettait de charger les descripteurs de segments dans le cache de descripteurs, avec une taille arbitraire. Une autre solution, beaucoup plus complexe sur le 386, demandait d'entrer en mode protégé pour configurer des segments de grande taille, de charger leurs descripteurs dans le cache de descripteur, puis de revenir en mode réel. En mode réel, les descripteurs dans le cache étaient encore disponibles et on pouvait les lire dans le cache.
====L'implémentation de la protection mémoire sur le 386====
La protection mémoire teste la valeur des bits P, S, X, E, R/W. Elle teste aussi les niveaux de privilège, avec deux bits DPL et CPL. En tout, le processeur pouvait tester 148 conditions différentes en parallèle dans la PTU. Cependant, les niveaux de privilèges étaient pré-traités par le microcode. Le microcode vérifiait aussi s'il y avait une erreur en terme d’anneau mémoire, avec par "exemple un segment en mode noyau accédé alors que le CPU est en espace utilisateur. Il fournissait alors un résultat sur deux bits, qui indiquait s'il y avait une erreur ou non, que la PTU utilisait.
Mais toutes les conditions n'étaient pas pertinentes à un instant t. Par exemple, il est pertinent de vérifier si le bit R/W était cohérent si l'instruction à exécuter est une écriture. Mais il n'y a pas besoin de tester le bit E qui indique qu'un segment est exécutable ou non, pour une lecture. En tout, le processeur pouvait se retrouver dans 33 situations possibles, chacune demandant de tester un sous-ensemble des 148 conditions. Pour préciser quel sous-ensembles tester, la PTU recevait un code opération, généré par le microcode.
Pour faire les tests de protection mémoire, le microcode avait une micro-opération nommée ''protection test operation'', qui envoyait les droits d'accès à la PTU. Lors de l'exécution d'une ''protection test operation'', le PLA recevait un descripteur de segment, lu depuis la mémoire RAM, ainsi qu'un code opération provenant du microcode.
{|class="wikitable"
|+ Entrée de la ''Protection Test Unit''
|-
! 15 - 14 !! 13 - 12 !! 11 !! 10 !! 9 !! 8 !! 7 !! 6 !! 5-0
|-
| P1 , P2 || || P || S || X || E || R/W || A || Code opération
|-
| Niveaux de privilèges cohérents/erreur || || Segment présent en mémoire ou swappé || S || X || Segment exécutable ou non || Segment accesible en lecture/écriture || Segment récemment accédé || Code opération
|}
Il fournissait en sortie un bit qui indiquait si une erreur de protection mémoire avait eu lieu ou non. Il fournissait aussi une adresse de 12 bits, utilisée seulement en cas d'erruer. Elle pointait dans le microcode, sur un code levant une exception en cas d'erreur. Enfin, la PTU fournissait 4 bits pouvant être testés par un branchement dans le microcode. L'un d'entre eux demandait de tester s'il y a un accès hors-limite, les autres étaient assez peu reliés à la protection mémoire.
Un détail est que le chargement du descripteur de segment est réalisé par une fonction dans le microcode. Elle est appliquée pour toutes les instructions ou situations qui demandent de faire un accès mémoire. Et les tests de protection mémoire sont réalisés dans cette fonction, pas après elle. Vu qu'il s'agit d'une fonction exécutée quelque soit l'instruction, le microcode doit transférer le code opération à cette fonction. Le microcode est pour cela associé à un registre interne, dans lequel le code opération est mémorisé, avant d'appeler la fonction. Le microcode a une micro-opération PTSAV (''Protection Save'') pour mémoriser le code opération dans ce registre. Dans la fonction qui charge le descripteur, une micro-opération PTOVRR (''Protection Override'') lit le code opération dans ce registre, et lance les tests nécessaires.
Il faut noter que le PLA était certes plus rapide que de tester les conditions une par une, mais il était assez lent. La PTU mettait environ 3 cycles d'horloges pour rendre son résultat. Le microcode en profitait alors pour exécuter des micro-opérations durant ces 3 cycles d'attente. Par exemple, le microcode pouvait en profiter pour lire l'adresse de base dans le descripteur, si elle n'a pas été chargée avant (les descripteur était chargé en deux fois). Il fallait cependant que les trois micro-opérations soient valides, peu importe qu'il y ait une erreur de protection mémoire ou non. Ou du moins, elles produisaient un résultat qui n'est pas utilisé en cas d'erreur. Si ce n'était pas possible, le microcode ajoutait des NOP pendant ce temps d'attente de 3 cycles.
Le bit A du descripteur de segment indique que le segment a récemment été accédé. Il est mis à jour après les tests de protection mémoire, quand ceux-ci indiquent que l'accès mémoire est autorisé. Le bit A est mis à 1 si la PTU l'autorise. Pour cela, la PTU utilise un des 4 bits de sortie mentionnés plus haut : l'un d'entre eux indique que le bit A doit être mis à 1. La mise à jour est ensuite réalisée par le microcode, qui utilise trois micro-opérations pour le mettre à jour.
====Le ''Hardware task switching'' des CPU x86====
Les systèmes d’exploitation modernes peuvent lancer plusieurs logiciels en même temps. Les logiciels sont alors exécutés à tour de rôle. Passer d'un programme à un autre est ce qui s'appelle une commutation de contexte. Lors d'une commutation de contexte, l'état du processeur est sauvegardé, afin que le programme stoppé puisse reprendre là où il était. Il arrivera un moment où le programme stoppé redémarrera et il doit reprendre dans l'état exact où il s'est arrêté. Deuxièmement, le programme à qui c'est le tour restaure son état. Cela lui permet de revenir là où il était avant d'être stoppé. Il y a donc une sauvegarde et une restauration des registres.
Divers processeurs incorporent des optimisations matérielles pour rendre la commutation de contexte plus rapide. Ils peuvent sauvegarder et restaurer les registres du processeur automatiquement lors d'une interruption de commutation de contexte. Les registres sont sauvegardés dans des structures de données en mémoire RAM, appelées des '''contextes matériels'''. Sur les processeurs x86, il s'agit de la technique d{{'}}''Hardware Task Switching''. Fait intéressant, le ''Hardware Task Switching'' se base beaucoup sur les segments mémoires.
Avec ''Hardware Task Switching'', chaque contexte matériel est mémorisé dans son propre segment mémoire, séparé des autres. Les segments pour les contextes matériels sont appelés des '''''Task State Segment''''' (TSS). Un TSS mémorise tous les registres généraux, le registre d'état, les pointeurs de pile, le ''program counter'' et quelques registres de contrôle du processeur. Par contre, les registres flottants ne sont pas sauvegardés, de même que certaines registres dit SIMD que nous n'avons pas encore abordé. Et c'est un défaut qui fait que le ''Hardware Task Switching'' n'est plus utilisé.
Le programme en cours d'exécution connait l'adresse du TSS qui lui est attribué, car elle est mémorisée dans un registre appelé le '''''Task Register'''''. En plus de pointer sur le TSS, ce registre contient aussi les adresses de base et limite du segment en cours. Pour être plus précis, le ''Task Register'' ne mémorise pas vraiment l'adresse du TSS. À la place, elle mémorise le numéro du segment, le numéro du TSS. Le numéro est codé sur 16 bits, ce qui explique que 65 536 segments sont adressables. Les instructions LDR et STR permettent de lire/écrire ce numéro de segment dans le ''Task Register''.
Le démarrage d'un programme a lieu automatiquement dans plusieurs circonstances. La première est une instruction de branchement CALL ou JMP adéquate. Le branchement fournit non pas une adresse à laquelle brancher, mais un numéro de segment qui pointe vers un TSS. Cela permet à une routine du système d'exploitation de restaurer les registres et de démarrer le programme en une seule instruction de branchement. Une seconde circonstance est une interruption matérielle ou une exception, mais nous la mettons de côté. Le ''Task Register'' est alors initialisé avec le numéro de segment fournit. S'en suit la procédure suivante :
* Le ''Task Register'' est utilisé pour adresser la table des segments, pour récupérer un pointeur vers le TSS associé.
* Le pointeur est utilisé pour une seconde lecture, qui adresse le TSS directement. Celle-ci restaure les registres du processeur.
En clair, on va lire le ''TSS descriptor'' dans la GDT, puis on l'utilise pour restaurer les registres du processeur.
[[File:Hardware Task Switching x86.png|centre|vignette|upright=2|Hardware Task Switching x86]]
===La segmentation sur les processeurs Burrough B5000 et plus===
Le Burrough B5000 est un très vieil ordinateur, commercialisé à partir de l'année 1961. Ses successeurs reprennent globalement la même architecture. C'était une machine à pile, doublé d'une architecture taguée, choses très rare de nos jours. Mais ce qui va nous intéresser dans ce chapitre est que ce processeur incorporait la segmentation, avec cependant une différence de taille : un programme avait accès à un grand nombre de segments. La limite était de 1024 segments par programme ! Il va de soi que des segments plus petits favorise l'implémentation de la mémoire virtuelle, mais complexifie la relocation et le reste, comme nous allons le voir.
Le processeur gère deux types de segments : les segments de données et de procédure/fonction. Les premiers mémorisent un bloc de données, dont le contenu est laissé à l'appréciation du programmeur. Les seconds sont des segments qui contiennent chacun une procédure, une fonction. L'usage des segments est donc différent de ce qu'on a sur les processeurs x86, qui n'avaient qu'un segment unique pour l'intégralité du code machine. Un seul segment de code machine x86 est découpé en un grand nombre de segments de code sur les processeurs Burrough.
La table des segments contenait 1024 entrées de 48 bits chacune. Fait intéressant, chaque entrée de la table des segments pouvait mémoriser non seulement un descripteur de segment, mais aussi une valeur flottante ou d'autres types de données ! Parler de table des segments est donc quelque peu trompeur, car cette table ne gère pas que des segments, mais aussi des données. La documentation appelaiat cette table la '''''Program Reference Table''''', ou PRT.
La raison de ce choix quelque peu bizarre est que les instructions ne gèrent pas d'adresses proprement dit. Tous les accès mémoire à des données en-dehors de la pile passent par la segmentation, ils précisent tous un indice de segment et un ''offset''. Pour éviter d'allouer un segment pour chaque donnée, les concepteurs du processeur ont décidé qu'une entrée pouvait contenir directement la donnée entière à lire/écrire.
La PRT supporte trois types de segments/descripteurs : les descripteurs de données, les descripteurs de programme et les descripteurs d'entrées-sorties. Les premiers décrivent des segments de données. Les seconds sont associés aux segments de procédure/fonction et sont utilisés pour les appels de fonction (qui passent, eux aussi, par la segmentation). Le dernier type de descripteurs sert pour les appels systèmes et les communications avec l'OS ou les périphériques.
Chaque entrée de la PRT contient un ''tag'', une suite de bit qui indique le type de l'entrée : est-ce qu'elle contient un descripteur de segment, une donnée, autre. Les descripteurs contiennent aussi un ''bit de présence'' qui indique si le segment a été swappé ou non. Car oui, les segments pouvaient être swappés sur ce processeur, ce qui n'est pas étonnant vu que les segments sont plus petits sur cette architecture. Le descripteur contient aussi l'adresse de base du segment ainsi que sa taille, et diverses informations pour le retrouver sur le disque dur s'il est swappé.
: L'adresse mémorisée ne faisait que 15 bits, ce qui permettait d'adresse 32 kibi-mots, soit 192 kibioctets de mémoire. Diverses techniques d'extension d'adressage étaient disponibles pour contourner cette limitation. Outre l'usage de l{{'}}''overlay'', le processeur et l'OS géraient aussi des identifiants d'espace d'adressage et en fournissaient plusieurs par processus. Les processeurs Borrough suivants utilisaient des adresses plus grandes, de 20 bits, ce qui tempérait le problème.
[[File:B6700Word.jpg|centre|vignette|upright=2|Structure d'un mot mémoire sur le B6700.]]
==Les architectures à capacités==
Les architectures à capacité utilisent la segmentation à granularité fine, mais ajoutent des mécanismes de protection mémoire assez particuliers, qui font que les architectures à capacité se démarquent du reste. Les architectures de ce type sont très rares et sont des processeurs assez anciens. Le premier d'entre eux était le Plessey System 250, qui date de 1969. Il fu suivi par le CAP computer, vendu entre les années 70 et 77. En 1978, le System/38 d'IBM a eu un petit succès commercial. En 1980, la Flex machine a aussi été vendue, mais à très peu d'examplaires, comme les autres architectures à capacité. Et enfin, en 1981, l'architecture à capacité la plus connue, l'Intel iAPX 432 a été commercialisée. Depuis, la seule architecture de ce type est en cours de développement. Il s'agit de l'architecture CHERI, dont la mise en projet date de 2014.
===Le partage de la mémoire sur les architectures à capacités===
Le partage de segment est grandement modifié sur les architectures à capacité. Avec la segmentation normale, il y a une table de segment par processus. Les conséquences sont assez nombreuses, mais la principale est que partager un segment entre plusieurs processus est compliqué. Les défauts ont été évoqués plus haut. Les sélecteurs de segments ne sont pas les mêmes d'un processus à l'autre, pour un même segment. De plus, les adresses limite et de base sont dupliquées dans plusieurs tables de segments, et cela peut causer des problèmes de sécurité si une table des segments est modifiée et pas l'autre. Et il y a d'autres problèmes, tout aussi importants.
[[File:Partage des segments avec la segmentation.png|centre|vignette|upright=1.5|Partage des segments avec la segmentation]]
À l'opposé, les architectures à capacité utilisent une table des segments unique pour tous les processus. La table des segments unique sera appelée dans de ce qui suit la '''table des segments globale''', ou encore la table globale. En conséquence, les adresses de base et limite ne sont présentes qu'en un seul exemplaire par segment, au lieu d'être dupliquées dans autant de processus que nécessaire. De plus, cela garantit que l'indice de segment est le même quel que soit le processus qui l'utilise.
Un défaut de cette approche est au niveau des droits d'accès. Avec la segmentation normale, les droits d'accès pour un segment sont censés changer d'un processus à l'autre. Par exemple, tel processus a accès en lecture seule au segment, l'autre seulement en écriture, etc. Mais ici, avec une table des segments uniques, cela ne marche plus : incorporer les droits d'accès dans la table des segments ferait que tous les processus auraient les mêmes droits d'accès au segment. Et il faut trouver une solution.
===Les capacités sont des pointeurs protégés===
Pour éviter cela, les droits d'accès sont combinés avec les sélecteurs de segments. Les sélecteurs des segments sont remplacés par des '''capacités''', des pointeurs particuliers formés en concaténant l'indice de segment avec les droits d'accès à ce segment. Si un programme veut accéder à une adresse, il fournit une capacité de la forme "sélecteur:droits d'accès", et un décalage qui indique la position de l'adresse dans le segment.
Il est impossible d'accéder à un segment sans avoir la capacité associée, c'est là une sécurité importante. Un accès mémoire demande que l'on ait la capacité pour sélectionner le bon segment, mais aussi que les droits d'accès en permettent l'accès demandé. Par contre, les capacités peuvent être passées d'un programme à un autre sans problème, les deux programmes pourront accéder à un segment tant qu'ils disposent de la capacité associée.
[[File:Comparaison entre capacités et adresses segmentées.png|centre|vignette|upright=2.5|Comparaison entre capacités et adresses segmentées]]
Mais cette solution a deux problèmes très liés. Au niveau des sélecteurs de segment, le problème est que les sélecteur ont une portée globale. Avant, l'indice de segment était interne à un programme, un sélecteur ne permettait pas d'accéder au segment d'un autre programme. Sur les architectures à capacité, les sélecteurs ont une portée globale. Si un programme arrive à forger un sélecteur qui pointe vers un segment d'un autre programme, il peut théoriquement y accéder, à condition que les droits d'accès le permettent. Et c'est là qu'intervient le second problème : les droits d'accès ne sont plus protégés par l'espace noyau. Les droits d'accès étaient dans la table de segment, accessible uniquement en espace noyau, ce qui empêchait un processus de les modifier. Avec une capacité, il faut ajouter des mécanismes de protection qui empêchent un programme de modifier les droits d'accès à un segment et de générer un indice de segment non-prévu.
La première sécurité est qu'un programme ne peut pas créer une capacité, seul le système d'exploitation le peut. Les capacités sont forgées lors de l'allocation mémoire, ce qui est du ressort de l'OS. Pour rappel, un programme qui veut du rab de mémoire RAM peut demander au système d'exploitation de lui allouer de la mémoire supplémentaire. Le système d'exploitation renvoie alors un pointeurs qui pointe vers un nouveau segment. Le pointeur est une capacité. Il doit être impossible de forger une capacité, en-dehors d'une demande d'allocation mémoire effectuée par l'OS. Typiquement, la forge d'une capacité se fait avec des instructions du processeur, que seul l'OS peut éxecuter (pensez à une instruction qui n'est accessible qu'en espace noyau).
La seconde protection est que les capacités ne peuvent pas être modifiées sans raison valable, que ce soit pour l'indice de segment ou les droits d'accès. L'indice de segment ne peut pas être modifié, quelqu'en soit la raison. Pour les droits d'accès, la situation est plus compliquée. Il est possible de modifier ses droits d'accès, mais sous conditions. Réduire les droits d'accès d'une capacité est possible, que ce soit en espace noyau ou utilisateur, pas l'OS ou un programme utilisateur, avec une instruction dédiée. Mais augmenter les droits d'accès, seul l'OS peut le faire avec une instruction précise, souvent exécutable seulement en espace noyau.
Les capacités peuvent être copiées, et même transférées d'un processus à un autre. Les capacités peuvent être détruites, ce qui permet de libérer la mémoire utilisée par un segment. La copie d'une capacité est contrôlée par l'OS et ne peut se faire que sous conditions. La destruction d'une capacité est par contre possible par tous les processus. La destruction ne signifie pas que le segment est effacé, il est possible que d'autres processus utilisent encore des copies de la capacité, et donc le segment associé. On verra quand la mémoire est libérée plus bas.
Protéger les capacités demande plusieurs conditions. Premièrement, le processeur doit faire la distinction entre une capacité et une donnée. Deuxièmement, les capacités ne peuvent être modifiées que par des instructions spécifiques, dont l'exécution est protégée, réservée au noyau. En clair, il doit y avoir une séparation matérielle des capacités, qui sont placées dans des registres séparés. Pour cela, deux solutions sont possibles : soit les capacités remplacent les adresses et sont dispersées en mémoire, soit elles sont regroupées dans un segment protégé.
====La liste des capacités====
Avec la première solution, on regroupe les capacités dans un segment protégé. Chaque programme a accès à un certain nombre de segments et à autant de capacités. Les capacités d'un programme sont souvent regroupées dans une '''liste de capacités''', appelée la '''''C-list'''''. Elle est généralement placée en mémoire RAM. Elle est ce qu'il reste de la table des segments du processus, sauf que cette table ne contient pas les adresses du segment, qui sont dans la table globale. Tout se passe comme si la table des segments de chaque processus est donc scindée en deux : la table globale partagée entre tous les processus contient les informations sur les limites des segments, la ''C-list'' mémorise les droits d'accès et les sélecteurs pour identifier chaque segment. C'est un niveau d'indirection supplémentaire par rapport à la segmentation usuelle.
[[File:Architectures à capacité.png|centre|vignette|upright=2|Architectures à capacité]]
La liste de capacité est lisible par le programme, qui peut copier librement les capacités dans les registres. Par contre, la liste des capacités est protégée en écriture. Pour le programme, il est impossible de modifier les capacités dedans, impossible d'en rajouter, d'en forger, d'en retirer. De même, il ne peut pas accéder aux segments des autres programmes : il n'a pas les capacités pour adresser ces segments.
Pour protéger la ''C-list'' en écriture, la solution la plus utilisée consiste à placer la ''C-list'' dans un segment dédié. Le processeur gère donc plusieurs types de segments : les segments de capacité pour les ''C-list'', les autres types segments pour le reste. Un défaut de cette approche est que les adresses/capacités sont séparées des données. Or, les programmeurs mixent souvent adresses et données, notamment quand ils doivent manipuler des structures de données comme des listes chainées, des arbres, des graphes, etc.
L'usage d'une ''C-list'' permet de se passer de la séparation entre espace noyau et utilisateur ! Les segments de capacité sont eux-mêmes adressés par leur propre capacité, avec une capacité par segment de capacité. Le programme a accès à la liste de capacité, comme l'OS, mais leurs droits d'accès ne sont pas les mêmes. Le programme a une capacité vers la ''C-list'' qui n'autorise pas l'écriture, l'OS a une autre capacité qui accepte l'écriture. Les programmes ne pourront pas forger les capacités permettant de modifier les segments de capacité. Une méthode alternative est de ne permettre l'accès aux segments de capacité qu'en espace noyau, mais elle est redondante avec la méthode précédente et moins puissante.
====Les capacités dispersées, les architectures taguées====
Une solution alternative laisse les capacités dispersées en mémoire. Les capacités remplacent les adresses/pointeurs, et elles se trouvent aux mêmes endroits : sur la pile, dans le tas. Comme c'est le cas dans les programmes modernes, chaque allocation mémoire renvoie une capacité, que le programme gére comme il veut. Il peut les mettre dans des structures de données, les placer sur la pile, dans des variables en mémoire, etc. Mais il faut alors distinguer si un mot mémoire contient une capacité ou une autre donnée, les deux ne devant pas être mixés.
Pour cela, chaque mot mémoire se voit attribuer un certain bit qui indique s'il s'agit d'un pointeur/capacité ou d'autre chose. Mais cela demande un support matériel, ce qui fait que le processeur devient ce qu'on appelle une ''architecture à tags'', ou ''tagged architectures''. Ici, elles indiquent si le mot mémoire contient une adresse:capacité ou une donnée.
[[File:Architectures à capacité sans liste de capacité.png|centre|vignette|upright=2|Architectures à capacité sans liste de capacité]]
L'inconvénient est le cout en matériel de cette solution. Il faut ajouter un bit à chaque case mémoire, le processeur doit vérifier les tags avant chaque opération d'accès mémoire, etc. De plus, tous les mots mémoire ont la même taille, ce qui force les capacités à avoir la même taille qu'un entier. Ce qui est compliqué.
===Les registres de capacité===
Les architectures à capacité disposent de registres spécialisés pour les capacités, séparés pour les entiers. La raison principale est une question de sécurité, mais aussi une solution pragmatique au fait que capacités et entiers n'ont pas la même taille. Les registres dédiés aux capacités ne mémorisent pas toujours des capacités proprement dites. À la place, ils mémorisent des descripteurs de segment, qui contiennent l'adresse de base, limite et les droits d'accès. Ils sont utilisés pour la relocation des accès mémoire ultérieurs. Ils sont en réalité identiques aux registres de relocation, voire aux registres de segments. Leur utilité est d'accélérer la relocation, entre autres.
Les processeurs à capacité ne gèrent pas d'adresses proprement dit, comme pour la segmentation avec plusieurs registres de relocation. Les accès mémoire doivent préciser deux choses : à quel segment on veut accéder, à quelle position dans le segment se trouve la donnée accédée. La première information se trouve dans le mal nommé "registre de capacité", la seconde information est fournie par l'instruction d'accès mémoire soit dans un registre (Base+Index), soit en adressage base+''offset''.
Les registres de capacités sont accessibles à travers des instructions spécialisées. Le processeur ajoute des instructions LOAD/STORE pour les échanges entre table des segments et registres de capacité. Ces instructions sont disponibles en espace utilisateur, pas seulement en espace noyau. Lors du chargement d'une capacité dans ces registres, le processeur vérifie que la capacité chargée est valide, et que les droits d'accès sont corrects. Puis, il accède à la table des segments, récupère les adresses de base et limite, et les mémorise dans le registre de capacité. Les droits d'accès et d'autres méta-données sont aussi mémorisées dans le registre de capacité. En somme, l'instruction de chargement prend une capacité et charge un descripteur de segment dans le registre.
Avec ce genre de mécanismes, il devient difficile d’exécuter certains types d'attaques, ce qui est un gage de sureté de fonctionnement indéniable. Du moins, c'est la théorie, car tout repose sur l'intégrité des listes de capacité. Si on peut modifier celles-ci, alors il devient facile de pouvoir accéder à des objets auxquels on n’aurait pas eu droit.
===Le recyclage de mémoire matériel===
Les architectures à capacité séparent les adresses/capacités des nombres entiers. Et cela facilite grandement l'implémentation de la ''garbage collection'', ou '''recyclage de la mémoire''', à savoir un ensemble de techniques logicielles qui visent à libérer la mémoire inutilisée.
Rappelons que les programmes peuvent demander à l'OS un rab de mémoire pour y placer quelque chose, généralement une structure de donnée ou un objet. Mais il arrive un moment où cet objet n'est plus utilisé par le programme. Il peut alors demander à l'OS de libérer la portion de mémoire réservée. Sur les architectures à capacité, cela revient à libérer un segment, devenu inutile. La mémoire utilisée par ce segment est alors considérée comme libre, et peut être utilisée pour autre chose. Mais il arrive que les programmes ne libèrent pas le segment en question. Soit parce que le programmeur a mal codé son programme, soit parce que le compilateur n'a pas fait du bon travail ou pour d'autres raisons.
Pour éviter cela, les langages de programmation actuels incorporent des '''''garbage collectors''''', des morceaux de code qui scannent la mémoire et détectent les segments inutiles. Pour cela, ils doivent identifier les adresses manipulées par le programme. Si une adresse pointe vers un objet, alors celui-ci est accessible, il sera potentiellement utilisé dans le futur. Mais si aucune adresse ne pointe vers l'objet, alors il est inaccessible et ne sera plus jamais utilisé dans le futur. On peut libérer les objets inaccessibles.
Identifier les adresses est cependant très compliqué sur les architectures normales. Sur les processeurs modernes, les ''garbage collectors'' scannent la pile à la recherche des adresses, et considèrent tout mot mémoire comme une adresse potentielle. Mais les architectures à capacité rendent le recyclage de la mémoire très facile. Un segment est accessible si le programme dispose d'une capacité qui pointe vers ce segment, rien de plus. Et les capacités sont facilement identifiables : soit elles sont dans la liste des capacités, soit on peut les identifier à partir de leur ''tag''.
Le recyclage de mémoire était parfois implémenté directement en matériel. En soi, son implémentation est assez simple, et peu être réalisé dans le microcode d'un processeur. Une autre solution consiste à utiliser un second processeur, spécialement dédié au recyclage de mémoire, qui exécute un programme spécialement codé pour. Le programme en question est placé dans une mémoire ROM, reliée directement à ce second processeur.
===L'intel iAPX 432===
Voyons maintenat une architecture à capacité assez connue : l'Intel iAPX 432. Oui, vous avez bien lu : Intel a bel et bien réalisé un processeur orienté objet dans sa jeunesse. La conception du processeur Intel iAPX 432 commença en 1975, afin de créer un successeur digne de ce nom aux processeurs 8008 et 8080.
La conception du processeur Intel iAPX 432 commença en 1975, afin de créer un successeur digne de ce nom aux processeurs 8008 et 8080. Ce processeur s'est très faiblement vendu en raison de ses performances assez désastreuses et de défauts techniques certains. Par exemple, ce processeur était une machine à pile à une époque où celles-ci étaient tombées en désuétude, il ne pouvait pas effectuer directement de calculs avec des constantes entières autres que 0 et 1, ses instructions avaient un alignement bizarre (elles étaient bit-alignées). Il avait été conçu pour maximiser la compatibilité avec le langage ADA, un langage assez peu utilisé, sans compter que le compilateur pour ce processeur était mauvais.
====Les segments prédéfinis de l'Intel iAPX 432====
L'Intel iAPX432 gère plusieurs types de segments. Rien d'étonnant à cela, les Burrough géraient eux aussi plusieurs types de segments, à savoir des segments de programmes, des segments de données, et des segments d'I/O. C'est la même chose sur l'Intel iAPX 432, mais en bien pire !
Les segments de données sont des segments génériques, dans lequels on peut mettre ce qu'on veut, suivant les besoins du programmeur. Ils sont tous découpés en deux parties de tailles égales : une partie contenant les données de l'objet et une partie pour les capacités. Les capacités d'un segment pointent vers d'autres segments, ce qui permet de créer des structures de données assez complexes. La ligne de démarcation peut être placée n'importe où dans le segment, les deux portions ne sont pas de taille identique, elles ont des tailles qui varient de segment en segment. Il est même possible de réserver le segment entier à des données sans y mettre de capacités, ou inversement. Les capacités et données sont adressées à partir de la ligne de démarcation, qui sert d'adresse de base du segment. Suivant l'instruction utilisée, le processeur accède à la bonne portion du segment.
Le processeur supporte aussi d'autres segments pré-définis, qui sont surtout utilisés par le système d'exploitation :
* Des segments d'instructions, qui contiennent du code exécutable, typiquement un programme ou des fonctions, parfois des ''threads''.
* Des segments de processus, qui mémorisent des processus entiers. Ces segments contiennent des capacités qui pointent vers d'autres segments, notamment un ou plusieurs segments de code, et des segments de données.
* Des segments de domaine, pour les modules ou bibliothèques dynamiques.
* Des segments de contexte, utilisés pour mémoriser l'état d'un processus, utilisés par l'OS pour faire de la commutation de contexte.
* Des segments de message, utilisés pour la communication entre processus par l'intermédiaire de messages.
* Et bien d'autres encores.
Sur l'Intel iAPX 432, chaque processus est considéré comme un objet à part entière, qui a son propre segment de processus. De même, l'état du processeur (le programme qu'il est en train d’exécuter, son état, etc.) est stocké en mémoire dans un segment de contexte. Il en est de même pour chaque fonction présente en mémoire : elle était encapsulée dans un segment, sur lequel seules quelques manipulations étaient possibles (l’exécuter, notamment). Et ne parlons pas des appels de fonctions qui stockaient l'état de l'appelé directement dans un objet spécial. Bref, de nombreux objets système sont prédéfinis par le processeur : les objets stockant des fonctions, les objets stockant des processus, etc.
L'Intel 432 possédait dans ses circuits un ''garbage collector'' matériel. Pour faciliter son fonctionnement, certains bits de l'objet permettaient de savoir si l'objet en question pouvait être supprimé ou non.
====Le support de la segmentation sur l'Intel iAPX 432====
La table des segments est une table hiérarchique, à deux niveaux. Le premier niveau est une ''Object Table Directory'', qui réside toujours en mémoire RAM. Elle contient des descripteurs qui pointent vers des tables secondaires, appelées des ''Object Table''. Il y a plusieurs ''Object Table'', typiquement une par processus. Plusieurs processus peuvent partager la même ''Object Table''. Les ''Object Table'' peuvent être swappées, mais pas l{{'}}''Object Table Directory''.
Une capacité tient compte de l'organisation hiérarchique de la table des segments. Elle contient un indice qui précise quelle ''Object Table'' utiliser, et l'indice du segment dans cette ''Object Table''. Le premier indice adresse l{{'}}''Object Table Directory'' et récupère un descripteur de segment qui pointe sur la bonne ''Object Table''. Le second indice est alors utilisé pour lire l'adresse de base adéquate dans cette ''Object Table''. La capacité contient aussi des droits d'accès en lecture, écriture, suppression et copie. Il y a aussi un champ pour le type, qu'on verra plus bas. Au fait : les capacités étaient appelées des ''Access Descriptors'' dans la documentation officielle.
Une capacité fait 32 bits, avec un octet utilisé pour les droits d'accès, laissant 24 bits pour adresser les segments. Le processeur gérait jusqu'à 2^24 segments/objets différents, pouvant mesurer jusqu'à 64 kibioctets chacun, ce qui fait 2^40 adresses différentes, soit 1024 gibioctets. Les 24 bits pour adresser les segments sont partagés moitié-moitié pour l'adressage des tables, ce qui fait 4096 ''Object Table'' différentes dans l{{'}}''Object Table Directory'', et chaque ''Object Table'' contient 4096 segments.
====Le jeu d'instruction de l'Intel iAPX 432====
L'Intel iAPX 432 est une machine à pile. Le jeu d'instruction de l'Intel iAPX 432 gère pas moins de 230 instructions différentes. Il gére deux types d'instructions : les instructions normales, et celles qui manipulent des segments/objets. Les premières permettent de manipuler des nombres entiers, des caractères, des chaînes de caractères, des tableaux, etc.
Les secondes sont spécialement dédiées à la manipulation des capacités. Il y a une instruction pour copier une capacité, une autre pour invalider une capacité, une autre pour augmenter ses droits d'accès (instruction sécurisée, exécutable seulement sous certaines conditions), une autre pour restreindre ses droits d'accès. deux autres instructions créent un segment et renvoient la capacité associée, la première créant un segment typé, l'autre non.
le processeur gérait aussi des instructions spécialement dédiées à la programmation système et idéales pour programmer des systèmes d'exploitation. De nombreuses instructions permettaient ainsi de commuter des processus, faire des transferts de messages entre processus, etc. Environ 40 % du micro-code était ainsi spécialement dédié à ces instructions spéciales.
Les instructions sont de longueur variable et peuvent prendre n'importe quelle taille comprise entre 10 et 300 bits, sans vraiment de restriction de taille. Les bits d'une instruction sont regroupés en 4 grands blocs, 4 champs, qui ont chacun une signification particulière.
* Le premier est l'opcode de l'instruction.
* Le champ référence, doit être interprété différemment suivant la donnée à manipuler. Si cette donnée est un entier, un caractère ou un flottant, ce champ indique l'emplacement de la donnée en mémoire. Alors que si l'instruction manipule un objet, ce champ spécifie la capacité de l'objet en question. Ce champ est assez complexe et il est sacrément bien organisé.
* Le champ format, n'utilise que 4 bits et a pour but de préciser si les données à manipuler sont en mémoire ou sur la pile.
* Le champ classe permet de dire combien de données différentes l'instruction va devoir manipuler, et quelles seront leurs tailles.
[[File:Encodage des instructions de l'Intel iAPX-432.png|centre|vignette|upright=2|Encodage des instructions de l'Intel iAPX-432.]]
====Le support de l'orienté objet sur l'Intel iAPX 432====
L'Intel 432 permet de définir des objets, qui correspondent aux classes des langages orientés objets. L'Intel 432 permet, à partir de fonctions définies par le programmeur, de créer des '''''domain objects''''', qui correspondent à une classe. Un ''domain object'' est un segment de capacité, dont les capacités pointent vers des fonctions ou un/plusieurs objets. Les fonctions et les objets sont chacun placés dans un segment. Une partie des fonctions/objets sont publics, ce qui signifie qu'ils sont accessibles en lecture par l'extérieur. Les autres sont privées, inaccessibles aussi bien en lecture qu'en écriture.
L'exécution d'une fonction demande que le branchement fournisse deux choses : une capacité vers le ''domain object'', et la position de la fonction à exécuter dans le segment. La position permet de localiser la capacité de la fonction à exécuter. En clair, on accède au ''domain object'' d'abord, pour récupérer la capacité qui pointe vers la fonction à exécuter.
Il est aussi possible pour le programmeur de définir de nouveaux types non supportés par le processeur, en faisant appel au système d'exploitation de l'ordinateur. Au niveau du processeur, chaque objet est typé au niveau de son object descriptor : celui-ci contient des informations qui permettent de déterminer le type de l'objet. Chaque type se voit attribuer un domain object qui contient toutes les fonctions capables de manipuler les objets de ce type et que l'on appelle le type manager. Lorsque l'on veut manipuler un objet d'un certain type, il suffit d'accéder à une capacité spéciale (le TCO) qui pointera dans ce type manager et qui précisera quel est l'objet à manipuler (en sélectionnant la bonne entrée dans la liste de capacité). Le type d'un objet prédéfini par le processeur est ainsi spécifié par une suite de 8 bits, tandis que le type d'un objet défini par le programmeur est défini par la capacité spéciale pointant vers son type manager.
===Conclusion===
Pour ceux qui veulent en savoir plus, je conseille la lecture de ce livre, disponible gratuitement sur internet (merci à l'auteur pour cette mise à disposition) :
* [https://homes.cs.washington.edu/~levy/capabook/ Capability-Based Computer Systems].
Voici un document qui décrit le fonctionnement de l'Intel iAPX432 :
* [https://homes.cs.washington.edu/~levy/capabook/Chapter9.pdf The Intel iAPX 432 ]
==La pagination==
Avec la pagination, la mémoire est découpée en blocs de taille fixe, appelés des '''pages mémoires'''. La taille des pages varie suivant le processeur et le système d'exploitation et tourne souvent autour de 4 kibioctets. Mais elles sont de taille fixe : on ne peut pas en changer la taille. C'est la différence avec les segments, qui sont de taille variable. Le contenu d'une page en mémoire fictive est rigoureusement le même que le contenu de la page correspondante en mémoire physique.
L'espace d'adressage est découpé en '''pages logiques''', alors que la mémoire physique est découpée en '''pages physique''' de même taille. Les pages logiques correspondent soit à une page physique, soit à une page swappée sur le disque dur. Quand une page logique est associée à une page physique, les deux ont le même contenu, mais pas les mêmes adresses. Les pages logiques sont numérotées, en partant de 0, afin de pouvoir les identifier/sélectionner. Même chose pour les pages physiques, qui sont elles aussi numérotées en partant de 0.
[[File:Principe de la pagination.png|centre|vignette|upright=2|Principe de la pagination.]]
Pour information, le tout premier processeur avec un système de mémoire virtuelle était le super-ordinateur Atlas. Il utilisait la pagination, et non la segmentation. Mais il fallu du temps avant que la méthode de la pagination prenne son essor dans les processeurs commerciaux x86.
Un point important est que la pagination implique une coopération entre OS et hardware, les deux étant fortement mélés. Une partie des informations de cette section auraient tout autant leur place dans le wikilivre sur les systèmes d'exploitation, mais il est plus simple d'en parler ici.
===La mémoire virtuelle : le ''swapping'' et le remplacement des pages mémoires===
Le système d'exploitation mémorise des informations sur toutes les pages existantes dans une '''table des pages'''. C'est un tableau où chaque ligne est associée à une page logique. Une ligne contient un bit ''Valid'' qui indique si la page logique associée est swappée sur le disque dur ou non, et la position de la page physique correspondante en mémoire RAM. Elle peut aussi contenir des bits pour la protection mémoire, et bien d'autres. Les lignes sont aussi appelées des ''entrées de la table des pages''
[[File:Gestionnaire de mémoire virtuelle - Pagination et swapping.png|centre|vignette|upright=2|Table des pages.]]
De plus, le système d'exploitation conserve une '''liste des pages vides'''. Le nom est assez clair : c'est une liste de toutes les pages de la mémoire physique qui sont inutilisées, qui ne sont allouées à aucun processus. Ces pages sont de la mémoire libre, utilisable à volonté. La liste des pages vides est mise à jour à chaque fois qu'un programme réserve de la mémoire, des pages sont alors prises dans cette liste et sont allouées au programme demandeur.
====Les défauts de page====
Lorsque l'on veut traduire l'adresse logique d'une page mémoire, le processeur vérifie le bit ''Valid'' et l'adresse physique. Si le bit ''Valid'' est à 1 et que l'adresse physique est présente, la traduction d'adresse s'effectue normalement. Mais si ce n'est pas le cas, l'entrée de la table des pages ne contient pas de quoi faire la traduction d'adresse. Soit parce que la page est swappée sur le disque dur et qu'il faut la copier en RAM, soit parce que les droits d'accès ne le permettent pas, soit parce que la page n'a pas encore été allouée, etc. On fait alors face à un '''défaut de page'''. Un défaut de page a lieu quand la MMU ne peut pas associer l'adresse logique à une adresse physique, quelque qu'en soit la raison.
Il existe deux types de défauts de page : mineurs et majeurs. Un '''défaut de page majeur''' a lieu quand on veut accéder à une page déplacée sur le disque dur. Un défaut de page majeur lève une exception matérielle dont la routine rapatriera la page en mémoire RAM. S'il y a de la place en mémoire RAM, il suffit d'allouer une page vide et d'y copier la page chargée depuis le disque dur. Mais si ce n'est par le cas, on va devoir faire de la place en RAM en déplaçant une page mémoire de la RAM vers le disque dur. Dans tous les cas, c'est le système d'exploitation qui s'occupe du chargement de la page, le processeur n'est pas impliqué. Une fois la page chargée, la table des pages est mise à jour et la traduction d'adresse peut recommencer. Si je dis recommencer, c'est car l'accès mémoire initial est rejoué à l'identique, sauf que la traduction d'adresse réussit cette fois-ci.
Un '''défaut de page mineur''' a lieu dans des circonstances pas très intuitives : la page est en mémoire physique, mais l'adresse physique de la page n'est pas accessible. Par exemple, il est possible que des sécurités empêchent de faire la traduction d'adresse, pour des raisons de protection mémoire. Une autre raison est la gestion des adresses synonymes, qui surviennent quand on utilise des libraires partagées entre programmes, de la communication inter-processus, des optimisations de type ''copy-on-write'', etc. Enfin, une dernière raison est que la page a été allouée à un programme par le système d'exploitation, mais qu'il n'a pas encore attribué sa position en mémoire. Pour comprendre comment c'est possible, parlons rapidement de l'allocation paresseuse.
Imaginons qu'un programme fasse une demande d'allocation mémoire et se voit donc attribuer une ou plusieurs pages logiques. L'OS peut alors réagir de deux manières différentes. La première est d'attribuer une page physique immédiatement, en même temps que la page logique. En faisant ainsi, on ne peut pas avoir de défaut mineur, sauf en cas de problème de protection mémoire. Cette solution est simple, on l'appelle l{{'}}'''allocation immédiate'''. Une autre solution consiste à attribuer une page logique, mais l'allocation de la page physique se fait plus tard. Elle a lieu la première fois que le programme tente d'écrire/lire dans la page physique. Un défaut mineur a lieu, et c'est lui qui force l'OS à attribuer une page physique pour la page logique demandée. On parle alors d{{'}}'''allocation paresseuse'''. L'avantage est que l'on gagne en performance si des pages logiques sont allouées mais utilisées, ce qui peut arriver.
Une optimisation permise par l'existence des défauts mineurs est le '''''copy-on-write'''''. Le but est d'optimiser la copie d'une page logique dans une autre. L'idée est que la copie est retardée quand elle est vraiment nécessaire, à savoir quand on écrit dans la copie. Tant que l'on ne modifie pas la copie, les deux pages logiques, originelle et copiée, pointent vers la même page physique. A quoi bon avoir deux copies avec le même contenu ? Par contre, la page physique est marquée en lecture seule. La moindre écriture déclenche une erreur de protection mémoire, et un défaut mineur. Celui-ci est géré par l'OS, qui effectue alors la copie dans une nouvelle page physique.
Je viens de dire que le système d'exploitation gère les défauts de page majeurs/mineurs. Un défaut de page déclenche une exception matérielle, qui passe la main au système d'exploitation. Le système d'exploitation doit alors déterminer ce qui a levé l'exception, notamment identifier si c'est un défaut de page mineur ou majeur. Pour cela, le processeur a un ou plusieurs '''registres de statut''' qui indique l'état du processeur, qui sont utiles pour gérer les défauts de page. Ils indiquent quelle est l'adresse fautive, si l'accès était une lecture ou écriture, si l'accès a eu lieu en espace noyau ou utilisateur (les espaces mémoire ne sont pas les mêmes), etc. Les registres en question varient grandement d'une architecture de processeur à l'autre, aussi on ne peut pas dire grand chose de plus sur le sujet. Le reste est de toute façon à voir dans un cours sur les systèmes d'exploitation.
====Le remplacement des pages====
Les pages virtuelles font référence soit à une page en mémoire physique, soit à une page sur le disque dur. Mais l'on ne peut pas lire une page directement depuis le disque dur. Les pages sur le disque dur doivent être chargées en RAM, avant d'être utilisables. Ce n'est possible que si on a une page mémoire vide, libre. Si ce n'est pas le cas, on doit faire de la place en swappant une page sur le disque dur. Les pages font ainsi une sorte de va et vient entre le fichier d'échange et la RAM, suivant les besoins. Tout cela est effectué par une routine d'interruption du système d'exploitation, le processeur n'ayant pas vraiment de rôle là-dedans.
Supposons que l'on veuille faire de la place en RAM pour une nouvelle page. Dans une implémentation naïve, on trouve une page à évincer de la mémoire, qui est copiée dans le ''swapfile''. Toutes les pages évincées sont alors copiées sur le disque dur, à chaque remplacement. Néanmoins, cette implémentation naïve peut cependant être améliorée si on tient compte d'un point important : si la page a été modifiée depuis le dernier accès. Si le programme/processeur a écrit dans la page, alors celle-ci a été modifiée et doit être sauvegardée sur le ''swapfile'' si elle est évincée. Par contre, si ce n'est pas le cas, la page est soit initialisée, soit déjà présente à l'identique dans le ''swapfile''.
Mais cette optimisation demande de savoir si une écriture a eu lieu dans la page. Pour cela, on ajoute un '''''dirty bit''''' à chaque entrée de la table des pages, juste à côté du bit ''Valid''. Il indique si une écriture a eu lieu dans la page depuis qu'elle a été chargée en RAM. Ce bit est mis à jour par le processeur, automatiquement, lors d'une écriture. Par contre, il est remis à zéro par le système d'exploitation, quand la page est chargée en RAM. Si le programme se voit allouer de la mémoire, il reçoit une page vide, et ce bit est initialisé à 0. Il est mis à 1 si la mémoire est utilisée. Quand la page est ensuite swappée sur le disque dur, ce bit est remis à 0 après la sauvegarde.
Sur la majorité des systèmes d'exploitation, il est possible d'interdire le déplacement de certaines pages sur le disque dur. Ces pages restent alors en mémoire RAM durant un temps plus ou moins long, parfois en permanence. Cette possibilité simplifie la vie des programmeurs qui conçoivent des systèmes d'exploitation : essayez d'exécuter l'interruption pour les défauts de page alors que la page contenant le code de l'interruption est placée sur le disque dur ! Là encore, cela demande d'ajouter un bit dans chaque entrée de la table des pages, qui indique si la page est swappable ou non. Le bit en question s'appelle souvent le '''bit ''swappable'''''.
====Les algorithmes de remplacement des pages pris en charge par l'OS====
Le choix de la page doit être fait avec le plus grand soin et il existe différents algorithmes qui permettent de décider quelle page supprimer de la RAM. Leur but est de swapper des pages qui ne seront pas accédées dans le futur, pour éviter d'avoir à faire triop de va-et-vient entre RAM et ''swapfile''. Les données qui sont censées être accédées dans le futur doivent rester en RAM et ne pas être swappées, autant que possible. Les algorithmes les plus simples pour le choix de page à évincer sont les suivants.
Le plus simple est un algorithme aléatoire : on choisit la page au hasard. Mine de rien, cet algorithme est très simple à implémenter et très rapide à exécuter. Il ne demande pas de modifier la table des pages, ni même d'accéder à celle-ci pour faire son choix. Ses performances sont surprenamment correctes, bien que largement en-dessous de tous les autres algorithmes.
L'algorithme FIFO supprime la donnée qui a été chargée dans la mémoire avant toutes les autres. Cet algorithme fonctionne bien quand un programme manipule des tableaux de grande taille, mais fonctionne assez mal dans le cas général.
L'algorithme LRU supprime la donnée qui été lue ou écrite pour la dernière fois avant toutes les autres. C'est théoriquement le plus efficace dans la majorité des situations. Malheureusement, son implémentation est assez complexe et les OS doivent modifier la table des pages pour l'implémenter.
L'algorithme le plus utilisé de nos jours est l{{'}}'''algorithme NRU''' (''Not Recently Used''), une simplification drastique du LRU. Il fait la différence entre les pages accédées il y a longtemps et celles accédées récemment, d'une manière très binaire. Les deux types de page sont appelés respectivement les '''pages froides''' et les '''pages chaudes'''. L'OS swappe en priorité les pages froides et ne swappe de page chaude que si aucune page froide n'est présente. L'algorithme est simple : il choisit la page à évincer au hasard parmi une page froide. Si aucune page froide n'est présente, alors il swappe au hasard une page chaude.
Pour implémenter l'algorithme NRU, l'OS mémorise, dans chaque entrée de la table des pages, si la page associée est froide ou chaude. Pour cela, il met à 0 ou 1 un bit dédié : le '''bit ''Accessed'''''. La différence avec le bit ''dirty'' est que le bit ''dirty'' est mis à jour uniquement lors des écritures, alors que le bit ''Accessed'' l'est aussi lors d'une lecture. Uen lecture met à 1 le bit ''Accessed'', mais ne touche pas au bit ''dirty''. Les écritures mettent les deux bits à 1.
Implémenter l'algorithme NRU demande juste de mettre à jour le bit ''Accessed'' de chaque entrée de la table des pages. Et sur les architectures modernes, le processeur s'en charge automatiquement. A chaque accès mémoire, que ce soit en lecture ou en écriture, le processeur met à 1 ce bit. Par contre, le système d'exploitation le met à 0 à intervalles réguliers. En conséquence, quand un remplacement de page doit avoir lieu, les pages chaudes ont de bonnes chances d'avoir le bit ''Accessed'' à 1, alors que les pages froides l'ont à 0. Ce n'est pas certain, et on peut se trouver dans des cas où ce n'est pas le cas. Par exemple, si un remplacement a lieu juste après la remise à zéro des bits ''Accessed''. Le choix de la page à remplacer est donc imparfait, mais fonctionne bien en pratique.
Tous les algorithmes précédents ont chacun deux variantes : une locale, et une globale. Avec la version locale, la page qui va être rapatriée sur le disque dur est une page réservée au programme qui est la cause du page miss. Avec la version globale, le système d'exploitation va choisir la page à virer parmi toutes les pages présentes en mémoire vive.
===La protection mémoire avec la pagination===
Avec la pagination, chaque page a des '''droits d'accès''' précis, qui permettent d'autoriser ou interdire les accès en lecture, écriture, exécution, etc. La table des pages mémorise les autorisations pour chaque page, sous la forme d'une suite de bits où chaque bit autorise/interdit une opération bien précise. En pratique, les tables de pages modernes disposent de trois bits : un qui autorise/interdit les accès en lecture, un qui autorise/interdit les accès en écriture, un qui autorise/interdit l'éxecution du contenu de la page.
Le format exact de la suite de bits a cependant changé dans le temps sur les processeurs x86 modernes. Par exemple, avant le passage au 64 bits, les CPU et OS ne pouvaient pas marquer une page mémoire comme non-exécutable. C'est seulement avec le passage au 64 bits qu'a été ajouté un bit pour interdire l'exécution de code depuis une page. Ce bit, nommé '''bit NX''', est à 0 si la page n'est pas exécutable et à 1 sinon. Le processeur vérifie à chaque chargement d'instruction si le bit NX de page lue est à 1. Sinon, il lève une exception matérielle et laisse la main à l'OS.
Une amélioration de cette protection est la technique dite du '''''Write XOR Execute''''', abréviée WxX. Elle consiste à interdire les pages d'être à la fois accessibles en écriture et exécutables. Il est possible de changer les autorisations en cours de route, ceci dit.
Les premiers IBM 360 disposaient d'un mécanisme de protection mémoire totalement différent, sans registres limite/base. Ce mécanisme de protection attribue à chaque programme une '''clé de protection''', qui consiste en un nombre unique de 4 bits (chaque programme a donc une clé différente de ses collègues). La mémoire est fragmentée en blocs de même taille, de 2 kibioctets. Le processeur mémorise, pour chacun de ses blocs, la clé de protection du programme qui a réservé ce bloc. À chaque accès mémoire, le processeur compare la clé de protection du programme en cours d’exécution et celle du bloc de mémoire de destination. Si les deux clés sont différentes, alors un programme a effectué un accès hors des clous et il se fait sauvagement arrêter.
===La traduction d'adresse avec la pagination===
Comme dit plus haut, les pages sont numérotées, de 0 à une valeur maximale, afin de les identifier. Le numéro en question est appelé le '''numéro de page'''. Il est utilisé pour dire au processeur : je veux lire une donnée dans la page numéro 20, la page numéro 90, etc. Une fois qu'on a le numéro de page, on doit alors préciser la position de la donnée dans la page, appelé le '''décalage''', ou encore l{{'}}''offset''.
Le numéro de page et le décalage se déduisent à partir de l'adresse, en divisant l'adresse par la taille de la page. Le quotient obtenu donne le numéro de la page, alors que le reste est le décalage. Les processeurs actuels utilisent tous des pages dont la taille est une puissance de deux, ce qui fait que ce calcul est fortement simplifié. Sous cette condition, le numéro de page correspond aux bits de poids fort de l'adresse, alors que le décalage est dans les bits de poids faible.
Le numéro de page existe en deux versions : un numéro de page physique qui identifie une page en mémoire physique, et un numéro de page logique qui identifie une page dans la mémoire virtuelle. Traduire l'adresse logique en adresse physique demande de remplacer le numéro de la page logique en un numéro de page physique.
[[File:Phycical address.JPG|centre|vignette|upright=2|Traduction d'adresse avec la pagination.]]
====Les tables des pages simples====
Dans le cas le plus simple, il n'y a qu'une seule table des pages, qui est adressée par les numéros de page logique. La table des pages est un vulgaire tableau d'adresses physiques, placées les unes à la suite des autres. Avec cette méthode, la table des pages a autant d'entrée qu'il y a de pages logiques en mémoire virtuelle. Accéder à la mémoire nécessite donc d’accéder d'abord à la table des pages en mémoire, de calculer l'adresse de l'entrée voulue, et d’y accéder.
[[File:Table des pages.png|centre|vignette|upright=2|Table des pages.]]
La table des pages est souvent stockée dans la mémoire RAM, son adresse est connue du processeur, mémorisée dans un registre spécialisé du processeur. Le processeur effectue automatiquement le calcul d'adresse à partir de l'adresse de base et du numéro de page logique.
[[File:Address translation (32-bit).png|centre|vignette|upright=2|Address translation (32-bit)]]
====Les tables des pages inversées====
Sur certains systèmes, notamment sur les architectures 64 bits ou plus, le nombre de pages est très important. Sur les ordinateurs x86 récents, les adresses sont en pratique de 48 bits, les bits de poids fort étant ignorés en pratique, ce qui fait en tout 68 719 476 736 pages. Chaque entrée de la table des pages fait au minimum 48 bits, mais fait plus en pratique : partons sur 64 bits par entrée, soit 8 octets. Cela fait 549 755 813 888 octets pour la table des pages, soit plusieurs centaines de gibioctets ! Une table des pages normale serait tout simplement impraticable.
Pour résoudre ce problème, on a inventé les '''tables des pages inversées'''. L'idée derrière celles-ci est l'inverse de la méthode précédente. La méthode précédente stocke, pour chaque page logique, son numéro de page physique. Les tables des pages inversées font l'inverse : elles stockent, pour chaque numéro de page physique, la page logique qui correspond. Avec cette méthode table des pages contient ainsi autant d'entrées qu'il y a de pages physiques. Elle est donc plus petite qu'avant, vu que la mémoire physique est plus petite que la mémoire virtuelle.
Quand le processeur veut convertir une adresse virtuelle en adresse physique, la MMU recherche le numéro de page de l'adresse virtuelle dans la table des pages. Le numéro de l'entrée à laquelle se trouve ce morceau d'adresse virtuelle est le morceau de l'adresse physique. Pour faciliter le processus de recherche dans la page, la table des pages inversée est ce que l'on appelle une table de hachage. C'est cette solution qui est utilisée sur les processeurs Power PC.
[[File:Table des pages inversée.jpg|centre|vignette|upright=2|Table des pages inversée.]]
====Les tables des pages multiples par espace d'adressage====
Dans les deux cas précédents, il y a une table des pages unique. Cependant, les concepteurs de processeurs et de systèmes d'exploitation ont remarqué que les adresses les plus hautes et/ou les plus basses sont les plus utilisées, alors que les adresses situées au milieu de l'espace d'adressage sont peu utilisées en raison du fonctionnement de la pile et du tas. Il y a donc une partie de la table des pages qui ne sert à rien et est utilisé pour des adresses inutilisées. C'est une source d'économie d'autant plus importante que les tables des pages sont de plus en plus grosses.
Pour profiter de cette observation, les concepteurs d'OS ont décidé de découper l'espace d'adressage en plusieurs sous-espaces d'adressage de taille identique : certains localisés dans les adresses basses, d'autres au milieu, d'autres tout en haut, etc. Et vu que l'espace d'adressage est scindé en plusieurs parties, la table des pages l'est aussi, elle est découpée en plusieurs sous-tables. Si un sous-espace d'adressage n'est pas utilisé, il n'y a pas besoin d'utiliser de la mémoire pour stocker la table des pages associée. On ne stocke que les tables des pages pour les espaces d'adressage utilisés, ceux qui contiennent au moins une donnée.
L'utilisation de plusieurs tables des pages ne fonctionne que si le système d'exploitation connaît l'adresse de chaque table des pages (celle de la première entrée). Pour cela, le système d'exploitation utilise une super-table des pages, qui stocke les adresses de début des sous-tables de chaque sous-espace. En clair, la table des pages est organisé en deux niveaux, la super-table étant le premier niveau et les sous-tables étant le second niveau.
L'adresse est structurée de manière à tirer profit de cette organisation. Les bits de poids fort de l'adresse sélectionnent quelle table de second niveau utiliser, les bits du milieu de l'adresse sélectionne la page dans la table de second niveau et le reste est interprété comme un ''offset''. Un accès à la table des pages se fait comme suit. Les bits de poids fort de l'adresse sont envoyés à la table de premier niveau, et sont utilisés pour récupérer l'adresse de la table de second niveau adéquate. Les bits au milieu de l'adresse sont envoyés à la table de second niveau, pour récupérer le numéro de page physique. Le tout est combiné avec l{{'}}''offset'' pour obtenir l'adresse physique finale.
[[File:Table des pages hiérarchique.png|centre|vignette|upright=2|Table des pages hiérarchique.]]
On peut aussi aller plus loin et découper la table des pages de manière hiérarchique, chaque sous-espace d'adressage étant lui aussi découpé en sous-espaces d'adressages. On a alors une table de premier niveau, plusieurs tables de second niveau, encore plus de tables de troisième niveau, et ainsi de suite. Cela peut aller jusqu'à 5 niveaux sur les processeurs x86 64 bits modernes. On parle alors de '''tables des pages emboitées'''. Dans ce cours, la table des pages désigne l'ensemble des différents niveaux de cette organisation, toutes les tables inclus. Seules les tables du dernier niveau mémorisent des numéros de page physiques, les autres tables mémorisant des pointeurs, des adresses vers le début des tables de niveau inférieur. Un exemple sera donné plus bas, dans la section suivante.
====L'exemple des processeurs x86====
Pour rendre les explications précédentes plus concrètes, nous allons prendre l'exemple des processeur x86 anciens, de type 32 bits. Les processeurs de ce type utilisaient deux types de tables des pages : une table des page unique et une table des page hiérarchique. Les deux étaient utilisées dans cas séparés. La table des page unique était utilisée pour les pages larges et encore seulement en l'absence de la technologie ''physical adress extension'', dont on parlera plus bas. Les autres cas utilisaient une table des page hiérarchique, à deux niveaux, trois niveaux, voire plus.
Une table des pages unique était utilisée pour les pages larges (de 2 mébioctets et plus). Pour les pages de 4 mébioctets, il y avait une unique table des pages, adressée par les 10 bits de poids fort de l'adresse, les bits restants servant comme ''offset''. La table des pages contenait 1024 entrées de 4 octets chacune, ce qui fait en tout 4 kibioctet pour la table des pages. La table des page était alignée en mémoire sur un bloc de 4 kibioctet (sa taille).
[[File:X86 Paging 4M.svg|centre|vignette|upright=2|X86 Paging 4M]]
Pour les pages de 4 kibioctets, les processeurs x86-32 bits utilisaient une table des page hiérarchique à deux niveaux. Les 10 bits de poids fort l'adresse adressaient la table des page maitre, appelée le directoire des pages (''page directory''), les 10 bits précédents servaient de numéro de page logique, et les 12 bits restants servaient à indiquer la position de l'octet dans la table des pages. Les entrées de chaque table des pages, mineure ou majeure, faisaient 32 bits, soit 4 octets. Vous remarquerez que la table des page majeure a la même taille que la table des page unique obtenue avec des pages larges (de 4 mébioctets).
[[File:X86 Paging 4K.svg|centre|vignette|upright=2|X86 Paging 4K]]
La technique du '''''physical adress extension''''' (PAE), utilisée depuis le Pentium Pro, permettait aux processeurs x86 32 bits d'adresser plus de 4 gibioctets de mémoire, en utilisant des adresses physiques de 64 bits. Les adresses virtuelles de 32 bits étaient traduites en adresses physiques de 64 bits grâce à une table des pages adaptée. Cette technologie permettait d'adresser plus de 4 gibioctets de mémoire au total, mais avec quelques limitations. Notamment, chaque programme ne pouvait utiliser que 4 gibioctets de mémoire RAM pour lui seul. Mais en lançant plusieurs programmes, on pouvait dépasser les 4 gibioctets au total. Pour cela, les entrées de la table des pages passaient à 64 bits au lieu de 32 auparavant.
La table des pages gardait 2 niveaux pour les pages larges en PAE.
[[File:X86 Paging PAE 2M.svg|centre|vignette|upright=2|X86 Paging PAE 2M]]
Par contre, pour les pages de 4 kibioctets en PAE, elle était modifiée de manière à ajouter un niveau de hiérarchie, passant de deux niveaux à trois.
[[File:X86 Paging PAE 4K.svg|centre|vignette|upright=2|X86 Paging PAE 4K]]
En 64 bits, la table des pages est une table des page hiérarchique avec 5 niveaux. Seuls les 48 bits de poids faible des adresses sont utilisés, les 16 restants étant ignorés.
[[File:X86 Paging 64bit.svg|centre|vignette|upright=2|X86 Paging 64bit]]
===La TLB et les caches associés===
En théorie, la table des pages est censée être accédée à chaque accès mémoire. Mais pour éviter d'avoir à lire la table des pages en mémoire RAM à chaque accès mémoire, les concepteurs de processeurs ont décidé d'implanter un cache dédié, le '''''translation lookaside buffer''''', ou TLB. Le TLB stocke au minimum de quoi faire la traduction entre adresse virtuelle et adresse physique, à savoir une correspondance entre numéro de page logique et numéro de page physique. Pour faire plus général, il stocke des entrées de la table des pages.
[[File:MMU principle updated.png|centre|vignette|upright=2.0|MMU avec une TLB.]]
Les accès à la table des pages sont gérés de deux façons : soit le processeur gère tout seul la situation, soit il délègue cette tâche au système d’exploitation. Sur les processeurs anciens, le système d'exploitation gère le parcours de la table des pages. Mais cette solution logicielle n'a pas de bonnes performances. D'autres processeurs gèrent eux-mêmes le défaut d'accès à la TLB et vont chercher d'eux-mêmes les informations nécessaires dans la table des pages. Ils disposent de circuits, les '''''page table walkers''''' (PTW), qui s'occupent eux-mêmes du défaut.
Les ''page table walkers'' contiennent des registres qui leur permettent de faire leur travail. Le plus important est celui qui mémorise la position de la table des pages en mémoire RAM, dont nous avons parlé plus haut. Les PTW ont besoin, pour faire leur travail, de mémoriser l'adresse physique de la table des pages, ou du moins l'adresse de la table des pages de niveau 1 pour des tables des pages hiérarchiques. Mais d'autres registres existent. Toutes les informations nécessaires pour gérer les défauts de TLB sont stockées dans des registres spécialisés appelés des '''tampons de PTW''' (PTW buffers).
===L'abstraction matérielle des processus===
[[File:Memoire virtuelle.svg|vignette|Mémoire virtuelle]]
Il est possible d'implémenter l'abstraction matérielle des processus avec la pagination. En clair, chaque programme lancé sur l'ordinateur dispose de son propre espace d'adressage, ce qui fait que la même adresse logique ne pointera pas sur la même adresse physique dans deux programmes différents. Pour cela, il y a plusieurs méthodes.
La première solution n'utilise qu'une seule table des pages. Pour cela, chaque entrée indique à quel processus elle est associée. Pour cela, chaque entrée contient un '''identifiant de processus''', un numéro qui précise pour quel processus, pour quel espace d'adressage, la correspondance est valide. La page des tables peut aussi contenir des entrées qui sont valides pour tous les processus en même temps. Pour cela, l'entrée contient un '''bit ''global''''', qui précise que cette correspondance est valide pour tous les processus, l'identifiant de processus est ignoré. Un défaut est que le partage d'une page entre plusieurs processus est impossible : soit on partage une page avec tous les processus, soit on l'alloue avec un seul processus.
L'intérêt de partager des pages entre tous les processus n'est pas évident, mais il le devient quand on se rappelle que le noyau de l'OS est mappé dans le haut de l'espace d'adressage. Et peu importe l'espace d'adressage, le noyau est toujours mappé de manière identique, les mêmes adresses logiques adressant la même adresse mémoire. En conséquence, les correspondances adresse physique-logique sont les mêmes pour le noyau, peu importe l'espace d'adressage. Le bit global accélère rapidement la traduction d'adresse pour l'accès au noyau.
Une solution alternative, plus simple, utilise une table des pages par processus lancé sur l'ordinateur, une table des pages unique par espace d'adressage. À chaque changement de processus, le registre qui mémorise la position de la table des pages est modifié pour pointer sur la bonne. C'est le système d'exploitation qui se charge de cette mise à jour.
Avec cette méthode, il est possible de partager une ou plusieurs pages entre plusieurs processus, en configurant les tables des pages convenablement. Les pages partagées sont mappées dans l'espace d'adressage de plusieurs processus, mais pas forcément au même endroit, pas forcément dans les mêmes adresses logiques. On peut placer la page partagée à l'adresse logique 0x0FFF pour un processus, à l'adresse logique 0xFF00 pour un autre processus, etc. Par contre, les entrées de la table des pages pour ces adresses pointent vers la même adresse physique.
[[File:Vm5.png|centre|vignette|upright=2|Tables des pages de plusieurs processus.]]
===La taille des pages===
La taille des pages varie suivant le processeur et le système d'exploitation et tourne souvent autour de 4 kibioctets. Les processeurs actuels gèrent plusieurs tailles différentes pour les pages : 4 kibioctets par défaut, 2 mébioctets, voire 1 à 4 gibioctets pour les pages les plus larges. Les pages de 4 kibioctets sont les pages par défaut, les autres tailles de page sont appelées des ''pages larges''. La taille optimale pour les pages dépend de nombreux paramètres et il n'y a pas de taille qui convienne à tout le monde. Certaines applications gagnent à utiliser des pages larges, d'autres vont au contraire perdre drastiquement en performance en les utilisant.
Le désavantage principal des pages larges est qu'elles favorisent la fragmentation mémoire. Si un programme veut réserver une portion de mémoire, pour une structure de donnée quelconque, il doit réserver une portion dont la taille est multiple de la taille d'une page. Par exemple, un programme ayant besoin de 110 kibioctets allouera 28 pages de 4 kibioctets, soit 120 kibioctets : 2 kibioctets seront perdus. Par contre, avec des pages larges de 2 mébioctets, on aura une perte de 2048 - 110 = 1938 kibioctets. En somme, des morceaux de mémoire seront perdus, car les pages sont trop grandes pour les données qu'on veut y mettre. Le résultat est que le programme qui utilise les pages larges utilisent plus de mémoire et ce d'autant plus qu'il utilise des données de petite taille. Un autre désavantage est qu'elles se marient mal avec certaines techniques d'optimisations de type ''copy-on-write''.
Mais l'avantage est que la traduction des adresses est plus performante. Une taille des pages plus élevée signifie moins de pages, donc des tables des pages plus petites. Et des pages des tables plus petites n'ont pas besoin de beaucoup de niveaux de hiérarchie, voire peuvent se limiter à des tables des pages simples, ce qui rend la traduction d'adresse plus simple et plus rapide. De plus, les programmes ont une certaine localité spatiale, qui font qu'ils accèdent souvent à des données proches. La traduction d'adresse peut alors profiter de systèmes de mise en cache dont nous parlerons dans le prochain chapitre, et ces systèmes de cache marchent nettement mieux avec des pages larges.
Il faut noter que la taille des pages est presque toujours une puissance de deux. Cela a de nombreux avantages, mais n'est pas une nécessité. Par exemple, le tout premier processeur avec de la pagination, le super-ordinateur Atlas, avait des pages de 3 kibioctets. L'avantage principal est que la traduction de l'adresse physique en adresse logique est trivial avec une puissance de deux. Cela garantit que l'on peut diviser l'adresse en un numéro de page et un ''offset'' : la traduction demande juste de remplacer les bits de poids forts par le numéro de page voulu. Sans cela, la traduction d'adresse implique des divisions et des multiplications, qui sont des opérations assez couteuses.
===Les entrées de la table des pages===
Avant de poursuivre, faisons un rapide rappel sur les entrées de la table des pages. Nous venons de voir que la table des pages contient de nombreuses informations : un bit ''valid'' pour la mémoire virtuelle, des bits ''dirty'' et ''accessed'' utilisés par l'OS, des bits de protection mémoire, un bit ''global'' et un potentiellement un identifiant de processus, etc. Étudions rapidement le format de la table des pages sur un processeur x86 32 bits.
* Elle contient d'abord le numéro de page physique.
* Les bits AVL sont inutilisés et peuvent être configurés à loisir par l'OS.
* Le bit G est le bit ''global''.
* Le bit PS vaut 0 pour une page de 4 kibioctets, mais est mis à 1 pour une page de 4 mébioctets dans le cas où le processus utilise des pages larges.
* Le bit D est le bit ''dirty''.
* Le bit A est le bit ''accessed''.
* Le bit PCD indique que la page ne peut pas être cachée, dans le sens où le processeur ne peut copier son contenu dans le cache et doit toujours lire ou écrire cette page directement dans la RAM.
* Le bit PWT indique que les écritures doivent mettre à jour le cache et la page en RAM (dans le chapitre sur le cache, on verra qu'il force le cache à se comporter comme un cache ''write-through'' pour cette page).
* Le bit U/S précise si la page est accessible en mode noyau ou utilisateur.
* Le bit R/W indique si la page est accessible en écriture, toutes les pages sont par défaut accessibles en lecture.
* Le bit P est le bit ''valid''.
[[File:PDE.png|centre|vignette|upright=2.5|Table des pages des processeurs Intel 32 bits.]]
==Comparaison des différentes techniques d'abstraction mémoire==
Pour résumer, l'abstraction mémoire permet de gérer : la relocation, la protection mémoire, l'isolation des processus, la mémoire virtuelle, l'extension de l'espace d'adressage, le partage de mémoire, etc. Elles sont souvent implémentées en même temps. Ce qui fait qu'elles sont souvent confondues, alors que ce sont des concepts sont différents. Ces liens sont résumés dans le tableau ci-dessous.
{|class="wikitable"
|-
!
! colspan="5" | Avec abstraction mémoire
! rowspan="2" | Sans abstraction mémoire
|-
!
! Relocation matérielle
! Segmentation en mode réel (x86)
! Segmentation, général
! Architectures à capacités
! Pagination
|-
! Abstraction matérielle des processus
| colspan="4" | Oui, relocation matérielle
| Oui, liée à la traduction d'adresse
| Emulée via relocation logicielle au démarrage du programme.
|-
! Mémoire virtuelle
| colspan="2" | Non, sauf émulation logicielle
| colspan="3" | Oui, gérée par le processeur et l'OS
| Non, sauf émulation logicielle
|-
! Extension de l'espace d'adressage
| colspan="2" | Oui : registre de base élargi
| colspan="2" | Oui : adresse de base élargie dans la table des segments
| ''Physical Adress Extension'' des processeurs 32 bits
| Commutation de banques
|-
! Protection mémoire
| Registre limite
| Aucune
| colspan="2" | Registre limite, droits d'accès aux segments
| Gestion des droits d'accès aux pages
| Possible, méthodes variées
|-
! Partage de mémoire
| colspan="2" | Non
| colspan="2" | Segment partagés
| Pages partagées
| Possible, méthodes variées
|}
===Les différents types de segmentation===
La segmentation regroupe plusieurs techniques franchement différentes, qui auraient gagné à être nommées différemment. La principale différence est l'usage de registres de relocation versus des registres de sélecteurs de segments. L'usage de registres de relocation est le fait de la relocation matérielle, mais aussi de la segmentation en mode réel des CPU x86. Par contre, l'usage de sélecteurs de segments est le fait des autres formes de segmentation, architectures à capacité inclues.
La différence entre les deux est le nombre de segments. L'usage de registres de relocation fait que le CPU ne gère qu'un petit nombre de segments de grande taille. La mémoire virtuelle est donc rarement implémentée vu que swapper des segments de grande taille est trop long, l'impact sur les performances est trop important. Sans compter que l'usage de registres de base se marie très mal avec la mémoire virtuelle. Vu qu'un segment peut être swappé ou déplacée n'importe quand, il faut invalider les registres de base au moment du swap/déplacement, ce qui n'est pas chose aisée. Aucun processeur ne gère cela, les méthodes pour n'existent tout simplement pas. L'usage de registres de base implique que la mémoire virtuelle est absente.
La protection mémoire est aussi plus limitée avec l'usage de registres de relocation. Elle se limite à des registres limite, mais la gestion des droits d'accès est limitée. En théorie, la segmentation en mode réel pourrait implémenter une version limitée de protection mémoire, avec une protection de l'espace exécutable. Mais ca n'a jamais été fait en pratique sur les processeurs x86.
Le partage de la mémoire est aussi difficile sur les architectures avec des registres de base. L'absence de table des segments fait que le partage d'un segment est basiquement impossible sans utiliser des méthodes complétement tordues, qui ne sont jamais implémentées en pratique.
===Segmentation versus pagination===
Par rapport à la pagination, la segmentation a des avantages et des inconvénients. Tous sont liés aux propriétés des segments et pages : les segments sont de grande taille et de taille variable, les pages sont petites et de taille fixe.
L'avantage principal de la segmentation est sa rapidité. Le fait que les segments sont de grande taille fait qu'on a pas besoin d'équivalent aux tables des pages inversée ou multiple, juste d'une table des segments toute simple. De plus, les échanges entre table des pages/segments et registres sont plus rares avec la segmentation. Par exemple, si un programme utilise un segment de 2 gigas, tous les accès dans le segment se feront avec une seule consultation de la table des segments. Alors qu'avec la pagination, il faudra une consultation de la table des pages chaque bloc de 4 kibioctet, au minimum.
Mais les désavantages sont nombreux. Le système d'exploitation doit agencer les segments en RAM, et c'est une tâche complexe. Le fait que les segments puisse changer de taille rend le tout encore plus complexe. Par exemple, si on colle les segments les uns à la suite des autres, changer la taille d'un segment demande de réorganiser tous les segments en RAM, ce qui demande énormément de copies RAM-RAM. Une autre possibilité est de laisser assez d'espace entre les segments, mais cet espace est alors gâché, dans le sens où on ne peut pas y placer un nouveau segment.
Swapper un segment est aussi très long, vu que les segments sont de grande taille, alors que swapper une page est très rapide.
==Un cas particulier : la z/architecture d'IBM==
IBM a sa propre gamme de ''mainframes'', qui utilisent des processeurs fait maison par IBM lui-même. Les CPU IBM ont leur propre jeu d'instruction CISC, appelé '''''z/architecture'''''. Et ce jeu d'instruction a des possibilités en termes d'adressage qui sont extrêmement intéressantes, en plus d'être assez inédites.
La ''z/architecture'' gère des pages de 4 kibioctets, en plus de pages larges introduites dans les dernières versions. L traduction d’adresse se fait cependant en passant des regroupements de 4 pages virtuelles, appelés des ''segments'', qui n'ont rien à voir avec les segments au sens de la segmentation. Pour éviter les confusions, je vais les appeler des ''super-pages''. Les super-pages sont des groupes de 4 pages virtuelles, ce qui veut dire que les 4 pages peuvent être très éloignées en mémoire physique. Les super-pages sont concrètement des intermédiaires utilisés pour la traduction d'adresse. La traduction d'adresse démarre à une table des super-pages, qui point vers une table des pages à plusieurs niveaux (5 maximum).
Mais ce n'est pas ce qui va nous intéresser dans ce qui suit. À la place, nous allons nous concentrer sur une fonctionnalité : les programmes ont accès à plusieurs espaces d'adressage simultanément. La ''z/architecture'' supporte 65 536 espaces d'adressage simultanés. Par contre, un programme ne peut en utiliser que 2048 en même temps. De plus, ils ne sont accessibles que quand le processeur est dans un mode de fonctionnement bien précis. Un processeur ''z/architecture'' peut en effet fonctionner dans deux modes, qu'on va appeler "standard" et "étendu".
L'existence de ces deux modes s'explique par le fait que les ''mainframes'' IBM ont évolués dans le temps. Les premiers ''mainframes'' n'avaient même pas de support basique de la mémoire virtuelle, le System/360 s'en passait totalement. C'est sur le System/370 que la pagination a été ajoutée. Le System/370 supportait deux espaces d'adressage virtuel par programme, là où les implémentations classiques n'en utilisent qu'un par programme. L'ESA/370 a ajouté la possibilité d'utiliser 16 espaces d'adressage par programme, mais l'a fait d'une manière à garder la compatibilité avec le System/370. Le processeur a donc deux modes : un avec deux espaces d'adressage apr programme, un autre pouvant monter jusqu’à 16.
élargit pour support deux modes d'adressages par programmes puis 16.
===Le mode standard de la ''z/architecture''===
En mode standard, un programme exécuté sur la ''z/architecture'' utilise deux espaces d'adressages : l''''espace d'adressage primaire''' où se situent le programme exécuté et les données importantes, un '''espace d'adressage secondaire''' utilisé pour des tâches/données annexes.
: Un troisième espace d'adressage l''''espace d'adressage ''home''''', est réservé au système d'exploitation. Il a pour particularité de ne pas avoir d'ASN. Il n'est concrètement accessible qu'en mode superviseur, donc par le système d'exploitation. C'est en quelque sorte un espace d'adressage pour le noyau, séparé des espaces d'adressage utilisateur. Nous n'en parlerons pas dans ce qui suit.
Mais comment le processeur pour utiliser ces espaces d'adressages ? Par exemple, s'il veut faire une lecture, comment précise-t-il l'espace d'adressage courant ? La réponse est que le processeur peut être dans deux états : soit primaire, soit secondaire. Dans le premier état, toutes les lectures/écritures se font dans l'espace d'adressage primaire. Dans le second, elles se font toutes dans l'espace d'adressage secondaire. Il peut passer de l'un à l'autre en utilisant l'instruction ''Set Address Space Control''. Le processeur dispose aussi d'instructions ''MOVE TO PRIMARY'' et ''MOVE TO SECONDARY'' pour échanger des données dans les deux espaces d'adressage.
L'espace d'adressage primaire a une table des pages dédiée, séparée de celle de l'espace d'adressage secondaire. Ou plutôt devrais-je dire une table des super-pages. La table des pages est une table des pages emboitée. Il y a peu à détailler dessus, si ce n'est pour cette histoire de segments/super-pages. L'adresse de la table des super-page est mémorisée dans le registre de contrôle CR1 pour l'espace d'adressage primaire, CR7 pour l'espace d'adressage secondaire. L'un ou l'autre est utilisé, selon que le processeur est en mode "primaire" ou "secondaire".
===Les registres d'accès de la ''z/architecture''===
Un programme peut donc passer de l'espace d'adressage primaire à secondaire (ou inversement) en utilisant l’instruction ''SET ADDRESS SPACE CONTROL''. Mais il peut en profiter pour passer dans un mode où il a accès à 2048 espaces d'adressage différents, en même temps. Le processeur n'a pas besoin d'utiliser d'instructions particulières pour copier des données d'un espace d'adressage à un autre, un simple MOV suffit. Le mode en question est appelé le '''mode ''access register'''''.
Le mode ''access register'' donne accès aux '''registres d'accès''', qui lui donnent son nom. Les registres d'accès mémorisent des identifiants qui permettent de sélectionner un espace d'adressage parmi 2048. Il y a en tout 16 registres d'accès, chacun étant appairé avec un registre général. Par exemple, le registre général numéro 5 est appairé au registre d'accès de même numéro. En mode ''access register'', toute lecture/écriture lit l'adresse dans un registre général et le registre d'accès associé indique dans quel espace d'adressage se trouve cette adresse. Par exemple, une lecture utilise l'adresse dans le registre général numéro 7, l'espace d'adressage associé sera dans le registre d'accès numéro 7.
Les registres d'accès peuvent être modifiés par des instructions classiques, en espace utilisateur. Elles sont nommées COPY ACCESS, EXTRACT ACCESS, LOAD ACCESS MULTIPLE, LOAD ADDRESS EXTENDED, SET ACCESS et STORE ACCESS MULTIPLE. Elles permettent de copier un registre d'accès dans un autre, de charger le contenu d'un registre d'accès depuis la mémoire RAM, d'enregistrer les registres d'accès dans la mémoire RAM, de copier une constante dans un registre d'accès. Mais quel est le contenu de ces registres ?
Une implémentation simple numéroterait les espaces d'adressage et mettrait les numéros dans les registres d'accès. Un espace d'adressage serait associé à une table des super-pages, reste à mémoriser la correspondance ''numéro d'espace d'adressage - adresse de la table des super-pages''. Pour cela, l'idéal est de mémoriser ces correspondances dans un tableau en mémoire RAM. Le numéro d'espace d'adressage est alors un indice, qui pointe sur l'entrée adéquate dans cette table. Il devrait y avoir un tableau par programme, qui serait protégé en écriture et seulement accessible par le système d'exploitation, comme le serait une table des pages. Mais ce n'est pas exactement ce qui est fait.
===L'utilisation des registres d'accès===
La différence est qu'il n'y a pas une table de correspondance par programme, mais un système à deux tableaux imbriqués. Le premier tableau possède 65 536 entrées, une par espace d'adressage supporté par le processeur. C'est donc une table partagée par tous les programmes. Par contre, la seconde table est bien unique pour chaque programme, chaque programme a la sienne. Faire ainsi permet de simplifier le partage de la mémoire. La table partagée est l''''''Access Space Table Entry''''' (ASTE), alors que la seconde table est la '''liste privée''' (''access list''), sous-entendu "privée pour un programme".
La liste privée mémorise une liste des espaces d'adressage auquel le programme peut accéder, auxquels le système d'exploitation lui a accordé l'accès. Pour chaque espace d'adressage, elle renvoie vers l'entrée de l'ASTE associée. En plus de mémoriser l'adresse de cette entrée, elle mémorise les droits d'accès que le programme a sur l'espace d'adressage associé. Par exemple, si deux programmes partagent un espace d'adressage, l'un pourra y avoir accès en lecture seule, l'autre pourra avoir un accès total. Pour cela, ils ont des droits différents dans la liste privée, mais l'entrée de l'ASTE sera la même.
[[File:Tradiction d'adresse sur la zarchitecture d'IBM.png|centre|vignette|upright=2|Traduction d'adresse sur la zarchitecture d'IBM.]]
Il y a donc une vérification des droits d'accès, suivi d'un accès à une table des super-pages globale. Si un programme veut accéder à un espace d'adressage qui lui est interdit, l'accès mémoire stoppera dans la liste privée. Le processeur utilisera l'identifiant d'espace d'adressage, mais la liste privée n'aura pas d'entrée associée. Elle lèvera alors une exception matérielle et le système d'exploitation gérera la situation, vraisemblablement en tuant le programme fautif. Mais si l'accès est autorisé, alors le programme accède à la table des super-pages et l'accès mémoire se poursuit.
Je dis une liste d'accès privée, mais il y en a deux en réalité. Mais la distinction exacte demanderait de parler de ''threads'' et de processus. Pour ceux qui sont familiers avec ces concepts, chaque ''thread'' a sa propre liste privée, avec en plus une liste par processus, partagée entre tous les ''threads''. Deux listes de 1024 entrées maximum, cela fait 2048 entrées au total. Les deux listes portent les noms de ''Dispatchable Unit Access List'' et de ''PASN-AL (Primary Address Space Access List''. Inutile de retenir ces deux termes, cependant.
Un registre d'accès mémorise un '''''Access List Entry Token''''' ou ALET. L'ALET contient l'indice d'une entrée dans la liste privée. Elle contient aussi un bit qui indique quelle liste privée utiliser, entre la ''Dispatchable Unit Access List'' et la ''PASN-AL (Primary Address Space Access List''. Précisons cependant que deux valeurs font exceptions : 0 pointe vers l'espace d'adressage principal, 1 vers l'espace d'adressage secondaire. Les autres valeurs sont configurables.
{|class="wikitable"
|+ Registre d'accès
|-
! 0000000 !! L (choix de la liste privée !! ASLEN !! ALEN
|-
| 0000000 || Choix de la liste privée || Identifiant de protection || Indice dans la liste privée
|}
Lors d'une lecture/écriture, l'ALETs est utilisé pour sélectionner l'espace d'adressage. Les valeurs 0 et 1 sont traitées telles quelles, l'espace d'adressage est déterminé automatiquement par le processeur. Pour les autres valeurs, récupérer l'adresse mémoire demande de consulter la liste d'accès privée. L'ALET est envoyé à la liste d'accès privée, et cette dernière fournit l'adresse de l'entrée de l'ASTE à consulter. Le processeur connait l'adresse de la liste privée, car son adresse mémoire est mémorisée dans les registres de contrôle numéro 2 et 5.
: Précisons que le registre d'accès numéro 0 contient obligatoirement la valeur 0, ce qui fait qu'il pointe vers l'espace d'adressage primaire.
[[File:Traduction d'adresse - trois espaces d'adressage, zarchitecture d'IBM.png|centre|vignette|upright=2|Traduction d'adresse - trois espaces d'adressage, zarchitecture d'IBM.]]
Une subtilité est que le système d'exploitation peut à tout moment modifier le contenu de la liste privée. Il se peut qu'un ALET qui correspondait à un espace d'adressage se mette à pointer vers un autre. Pour détecter cette situation, le processeur attribue un identifiant, à chaque espace d'adressage. L'identifiant est mémorisé dans les entrées de la liste privée, de l'ASTE et dans les registres d'accès (dans les bits de poids fort de l'ALET). L'identifiant est modifié dans l'ALET à chaque fois que le système d'exploitation modifie l'entrée. Si un programme fait un accès avec un registre d'accès, mais que l'identifiant ne correspond pas avec celui dans l'ALET/liste privée, c'est signe que l'espace d'adressage a été swappé, une exception matérielle est levée.
Un tel fonctionnement vous aura sans doute fait penser à une architecture à capacité, mais la z/architecture n'en est pas une. Malgré des points de ressemblance assez importants, il y a des différences fondamentales avec les architectures à capacité.
* Le z/architecture ne supporte pas la segmentation, qui est nécessaire pour les architectures à capacité. Sur la z/architecture, ce sont des espaces d'adressage qui sont sélectionnés, pas des segments.
* Les registres d'accès ne sont pas des registres de capacité, car on peut les modifier avec des instructions en espace utilisateur. En comparaison, modifier les registres de capacité ne peut se faire qu'en espace noyau, sous l'égide du système d'exploitation.
* Les droits d'accès sont dans une table séparée des pointeurs, pas concaténés à ceux-ci. La gestion des droits d'accès est gérée par les tables des pages.
* Les identifiants ALET ne peuvent pas être passés à un autre programme. Deux ALETs identiques correspondent à deux espaces d'adressages différents dans deux programmes différents. L'identifiant peut être identique, il pointera vers l'entrée de même numéro dans les deux listes privées, mais l'adresse dans l'entrée peut être différente.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=L'espace d'adressage du processeur
| prevText=L'espace d'adressage du processeur
| next=Les méthodes de synchronisation entre processeur et périphériques
| nextText=Les méthodes de synchronisation entre processeur et périphériques
}}
</noinclude>
khqme0chb2hz89ahxne52ue753tnxlz
775334
775333
2026-10-16T21:02:33Z
Mewtow
31375
/* Un cas particulier : la z/architecture d'IBM */
775334
wikitext
text/x-wiki
Pour introduire ce chapitre, nous devons faire un rappel sur le concept d{{'}}'''espace d'adressage'''. Pour rappel, un espace d'adressage correspond à l'ensemble des adresses utilisables par le processeur. Par exemple, si je prends un processeur 16 bits, il peut adresser en tout 2^16 = 65536 adresses, l'ensemble de ces adresses forme son espace d'adressage. Intuitivement, on s'attend à ce qu'il y ait correspondance avec les adresses de la mémoire RAM. J'entends par là que l'adresse 1209 de l'espace d'adressage correspond à l'adresse 1209 en mémoire RAM. C'est là une hypothèse parfaitement raisonnable et on voit mal comment ce pourrait ne pas être le cas.
Mais les processeurs modernes utilisent des techniques d{{'}}'''abstraction mémoire''' qui font que ce n'est pas le cas. Avec ces techniques, l'adresse 1209 de l'espace d'adressage correspond en réalité à l'adresse 9999 en mémoire RAM, voire n'est pas en RAM. L'abstraction mémoire fait que l'espace d'adressage regroupe des adresses fictives, qui doivent être traduites en adresses mémoires réelles pour être utilisées. Les adresses de l'espace d'adressage portent le nom d{{'}}'''adresses logiques''', alors que les adresses de la mémoire RAM sont appelées '''adresses physiques'''.
L'intérêt de l'abstraction matérielle n'est pas évident. Aussi, avant de parler de comment l'abstraction mémoire fonctionne, nous allons voir à quoi elle sert. Nous allons voir qu'elle a plusieurs utilisations différentes, qui sont absolument nécessaires sur tous les ordinateurs personnels modernes. Tous les processeurs modernes la prennent en charge, les systèmes d'exploitation coopérent avec le processeur pour l'utiliser au mieux.
==L'abstraction mémoire permet plusieurs fonctionnalités complémentaires==
La fonctionnalité la plus connue est la mémoire virtuelle, dont vous avez peut-être déjà entendu parler, peut-être que le nom vous dit quelque chose. Si ce n'est pas le cas, nous la détailleront dans la suite. Elle permet concrétement à un programme d'utiliser plus de mémoire qu'il n'y a de RAM installé dans un ordinateur, en utilisant le disque dur comme solution de secours. Mais d'autres fonctionnalités moins évidentes sont permises par l'abstraction mémoire. Et la première que nous allons voir est l'abstraction des processus.
: En général, une adresse logique correspond à une seule adresse physique. Mais beaucoup de fonctionnalités avancées ne respectent pas cette règle.
===L'abstraction matérielle des processus===
Les systèmes d'exploitation modernes sont dits multi-tâche, à savoir qu'ils sont capables d'exécuter plusieurs logiciels en même temps. Et ce même si un seul processeur est présent dans l'ordinateur : les logiciels sont alors exécutés à tour de rôle. Toutefois, cela amène un paquet de problèmes qu'il faut résoudre au mieux. Par exemple, les programmes exécutés doivent se partager la mémoire RAM, ce qui ne vient pas sans problèmes. Le problème principal est que les programmes ne doivent pas lire ou écrire dans les données d'un autre, sans quoi on se retrouverait rapidement avec des problèmes. Il faut donc introduire des mécanismes d{{'}}'''isolement des processus''', pour isoler les programmes les uns des autres.
Un de ces mécanismes est l{{'}}'''abstraction matérielle des processus''', une technique qui fait que chaque programme a son propre espace d'adressage. Chaque programme a l'impression d'avoir accès à tout l'espace d'adressage, de l'adresse 0 à l'adresse maximale gérée par le processeur. Évidemment, il s'agit d'une illusion maintenue justement grâce à la traduction d'adresse. Les espaces d'adressage contiennent des adresses logiques, les adresses de la RAM sont des adresses physiques, la nécessité de l'abstraction mémoire est évidente.
Implémenter l'abstraction mémoire peut se faire de plusieurs manières. Mais dans tous les cas, il faut que la correspondance adresse logique - physique change d'un programme à l'autre. Ce qui est normal, vu que les deux processus sont placés à des endroits différents en RAM physique. La conséquence est qu'avec l'abstraction mémoire, une adresse logique correspond à plusieurs adresses physiques. Une même adresse logique dans deux processus différents correspond à deux adresses phsiques différentes, une par processus. Une adresse logique dans un processus correspondra à l'adresse physique X, la même adresse dans un autre processus correspondra à l'adresse Y.
Les adresses physiques qui partagent la même adresse logique sont alors appelées des '''adresses homonymes'''. Le choix de la bonne adresse étant réalisé par un mécanisme matériel et dépend du programme en cours. Le mécanisme pour choisir la bonne adresse dépend du processeur, mais il y en a deux grands types :
* La première consiste à utiliser l'identifiant de processus CPU, vu au chapitre précédent. C'est, pour rappel, un numéro attribué à chaque processus par le processeur. L'identifiant du processus en cours d'exécution est mémorisé dans un registre du processeur. La traduction d'adresse utilise cet identifiant, en plus de l'adresse logique, pour déterminer l'adresse physique.
* La seconde solution mémorise les correspondances adresses logiques-physique dans des tables en mémoire RAM, qui sont différentes pour chaque programme. Les tables sont accédées à chaque accès mémoire, afin de déterminer l'adresse physique.
===Le partage de la mémoire===
L'isolation des processus est très importante sur les systèmes d'exploitation modernes. Cependant, il existe quelques situations où elle doit être contournée ou du moins mise en pause. Les situations sont multiples : gestion de bibliothèques partagées, communication entre processus, usage de ''threads'', etc. Elles impliquent toutes un '''partage de mémoire''', à savoir qu'une portion de mémoire RAM est partagée entre plusieurs programmes. Le partage de mémoire est une sorte de brèche de l'isolation des processus, mais qui est autorisée car elle est utile.
Un cas intéressant est celui des '''bibliothèques partagées'''. Les bibliothèques sont des collections de fonctions regroupées ensemble, dans une seule unité de code. Un programme qui utilise une bibliothèque peut appeler n’importe quelle fonction présente dans la bibliothèque. La bibliothèque peut être simplement inclue dans le programme lui-même, on parle alors de bibliothèques statiques. De telles bibliothèques fonctionnent très bien, mais avec un petit défaut pour les bibliothèques très utilisées : plusieurs programmes qui utilisent la même bibliothèque vont chacun l'inclure dans leur code, ce qui fera doublon.
Pour éviter cela, les OS modernes gèrent des bibliothèques partagées, à savoir qu'un seul exemplaire de la bibliothèque est partagé entre plusieurs programmes. Chaque programme peut exécuter une fonction de la bibliothèque quand il le souhaite, en effectuant un branchement adéquat. Mais cela implique que la bibliothèque soit présente dans l'espace d'adressage du programme en question. Une bibliothèque est donc présente dans plusieurs espaces d'adressage, alors qu'il n'y en a qu'un seul exemplaire en mémoire RAM.
[[File:Ogg vorbis libs and application dia.svg|centre|vignette|upright=2|Exemple de bibliothèques, avec Ogg vorbis.]]
D'autres situations demandent de partager de la mémoire entre deux programmes. Par exemple, les systèmes d'exploitation modernes gèrent nativement des systèmes de '''communication inter-processus''', très utilisés par les programmes modernes pour échanger des données. Et la plupart demandant de partager un bout de mémoire entre processus, même si c'est seulement temporairement. Typiquement, deux processus partagent un intervalle d'adresse où l'un écrit les données à l'autre, l'autre lisant les données envoyées.
Une dernière utilisation de la mémoire partagée est l{{'}}'''accès direct au noyau'''. Sur les systèmes d'exploitations moderne, dans l'espace d'adressage de chaque programme, les adresses hautes sont remplies avec une partie du noyau ! Évidemment, ces adresses sont accessibles uniquement en lecture, pas en écriture. Pas question de modifier le noyau de l'OS ! De plus, il s'agit d'une portion du noyau dont on sait que la consultation ne pose pas de problèmes de sécurité.
Le programme peut lire des données dans cette portion du noyau, mais aussi exécuter les fonctions du noyau qui sont dedans. L'idée est d'éviter des appels systèmes trop fréquents. Au lieu d'effectuer un véritable appel système, avec une interruption logicielle, le programme peut exécuter des appels systèmes simplifiés, de simples appels de fonctions couplés avec un changement de niveau de privilège (passage en espace noyau nécessaire).
[[File:AMD64-canonical--48-bit.png|vignette|Répartition des adresses entre noyau (jaune/orange) et programme (verte), sur les systèmes x86-64 bits, avec des adresses physiques de 48 bits.]]
L'espace d'adressage est donc séparé en deux portions : l'OS d'un côté, le programme de l'autre. La répartition des adresses entre noyau et programme varie suivant l'OS ou le processeur utilisé. Sur les PC x86 32 bits, Linux attribuait 3 gigas pour les programmes et 1 giga pour le noyau, Windows attribuait 2 gigas à chacun. Sur les systèmes x86 64 bits, l'espace d'adressage d'un programme est coupé en trois, comme illustré ci-contre : une partie basse de 2^48 octets, une partie haute de même taille, et un bloc d'adresses invalides entre les deux. Les adresses basses sont utilisées pour le programme, les adresses hautes pour le noyau, il n'y a rien entre les deux.
Avec le partage de mémoire, plusieurs adresses logiques correspondent à la même adresse physique. Tel processus verra la zone de mémoire partagée à l'adresse X, l'autre la verra à l'adresse Y. Mais il s'agira de la même portion de mémoire physique, avec une seule adresse physique. En clair, lorsque deux processus partagent une même zone de mémoire, la zone sera mappées à des adresses logiques différentes. Les adresses logiques sont alors appelées des '''adresses synonymes''', terme qui trahit le fait qu'elles correspondent à la même adresse physique.
===La mémoire virtuelle===
Toutes les adresses ne sont pas forcément occupées par de la mémoire RAM, s'il n'y a pas assez de RAM installée. Par exemple, un processeur 32 bits peut adresser 4 gibioctets de RAM, même si seulement 3 gibioctets sont installés dans l'ordinateur. L'espace d'adressage contient donc 1 gigas d'adresses inutilisées, et il faut éviter ce surplus d'adresses pose problème.
Sans mémoire virtuelle, seule la mémoire réellement installée est utilisable. Si un programme utilise trop de mémoire, il est censé se rendre compte qu'il n'a pas accès à tout l'espace d'adressage. Quand il demandera au système d'exploitation de lui réserver de la mémoire, le système d'exploitation le préviendra qu'il n'y a plus de mémoire libre. Par exemple, si un programme tente d'utiliser 4 gibioctets sur un ordinateur avec 3 gibioctets de mémoire, il ne pourra pas. Pareil s'il veut utiliser 2 gibioctets de mémoire sur un ordinateur avec 4 gibioctets, mais dont 3 gibioctets sont déjà utilisés par d'autres programmes. Dans les deux cas, l'illusion tombe à plat.
Les techniques de '''mémoire virtuelle''' font que l'espace d'adressage est utilisable au complet, même s'il n'y a pas assez de mémoire installée dans l'ordinateur ou que d'autres programmes utilisent de la RAM. Par exemple, sur un processeur 32 bits, le programme aura accès à 4 gibioctets de RAM, même si d'autres programmes utilisent la RAM, même s'il n'y a que 2 gibioctets de RAM d'installés dans l'ordinateur.
Pour cela, on utilise une partie des mémoires de masse (disques durs) d'un ordinateur en remplacement de la mémoire physique manquante. Le système d'exploitation crée sur le disque dur un fichier, appelé le ''swapfile'' ou '''fichier de ''swap''''', qui est utilisé comme mémoire RAM supplémentaire. Il mémorise le surplus de données et de programmes qui ne peut pas être mis en mémoire RAM.
[[File:Vm1.png|centre|vignette|upright=2.0|Mémoire virtuelle et fichier de Swap.]]
Une technique naïve de mémoire virtuelle serait la suivante. Avant de l'aborder, précisons qu'il s'agit d'une technique abordée à but pédagogique, mais qui n'est implémentée nulle part tellement elle est lente et inefficace. Un espace d'adressage de 4 gigas ne contient que 3 gigas de RAM, ce qui fait 1 giga d'adresses inutilisées. Les accès mémoire aux 3 gigas de RAM se font normalement, mais l'accès aux adresses inutilisées lève une exception matérielle "Memory Unavailable". La routine d'interruption de cette exception accède alors au ''swapfile'' et récupère les données associées à cette adresse. La mémoire virtuelle est alors émulée par le système d'exploitation.
Le défaut de cette méthode est que l'accès au giga manquant est toujours très lent, parce qu'il se fait depuis le disque dur. D'autres techniques de mémoire virtuelle logicielle font beaucoup mieux, mais nous allons les passer sous silence, vu qu'on peut faire mieux, avec l'aide du matériel.
L'idée est de charger les données dont le programme a besoin dans la RAM, et de déplacer les autres sur le disque dur. Par exemple, imaginons la situation suivante : un programme a besoin de 4 gigas de mémoire, mais ne dispose que de 2 gigas de mémoire installée. On peut imaginer découper l'espace d'adressage en 2 blocs de 2 gigas, qui sont chargés à la demande. Si le programme accède aux adresses basses, on charge les 2 gigas d'adresse basse en RAM. S'il accède aux adresses hautes, on charge les 2 gigas d'adresse haute dans la RAM après avoir copié les adresses basses sur le ''swapfile''.
On perd du temps dans les copies de données entre RAM et ''swapfile'', mais on gagne en performance vu que tous les accès mémoire se font en RAM. Du fait de la localité temporelle, le programme utilise les données chargées depuis le swapfile durant un bon moment avant de passer au bloc suivant. La RAM est alors utilisée comme une sorte de cache alors que les données sont placées dans une mémoire fictive représentée par l'espace d'adressage et qui correspond au disque dur.
Mais avec cette technique, la correspondance entre adresses du programme et adresses de la RAM change au cours du temps. Les adresses de la RAM correspondent d'abord aux adresses basses, puis aux adresses hautes, et ainsi de suite. On a donc besoin d'abstraction mémoire. Les correspondances entre adresse logique et physique peuvent varier avec le temps, ce qui permet de déplacer des données de la RAM vers le disque dur ou inversement. Une adresse logique peut correspondre à une adresse physique, ou bien à une donnée swappée sur le disque dur. C'est l'unité de traduction d'adresse qui se charge de faire la différence. Si une correspondance entre adresse logique et physique est trouvée, elle l'utilise pour traduire les adresses. Si aucune correspondance n'est trouvée, alors elle laisse la main au système d'exploitation pour charger la donnée en RAM. Une fois la donnée chargée en RAM, les correspondances entre adresse logique et physiques sont modifiées de manière à ce que l'adresse logique pointe vers la donnée chargée.
===L'extension d'adressage===
Une autre fonctionnalité rendue possible par l'abstraction mémoire est l{{'}}'''extension d'adressage'''. Elle permet d'utiliser plus de mémoire que l'espace d'adressage ne le permet. Par exemple, utiliser 7 gigas de RAM sur un processeur 32 bits, dont l'espace d'adressage ne gère que 4 gigas. L'extension d'adresse est l'exact inverse de la mémoire virtuelle. La mémoire virtuelle sert quand on a moins de mémoire que d'adresses, l'extension d'adresse sert quand on a plus de mémoire que d'adresses.
Il y a quelques chapitres, nous avions vu que c'est possible via la commutation de banques. Mais l'abstraction mémoire est une méthode alternative. Que ce soit avec la commutation de banques ou avec l'abstraction mémoire, les adresses envoyées à la mémoire doivent être plus longues que les adresses gérées par le processeur. La différence est que l'abstraction mémoire étend les adresses d'une manière différente.
Une implémentation possible de l'extension d'adressage fait usage de l'abstraction matérielle des processus. Chaque processus a son propre espace d'adressage, mais ceux-ci sont placés à des endroits différents dans la mémoire physique. Par exemple, sur un ordinateur avec 16 gigas de RAM, mais un espace d'adressage de 2 gigas, on peut remplir la RAM en lançant 8 processus différents et chaque processus aura accès à un bloc de 2 gigas de RAM, pas plus, il ne peut pas dépasser cette limite. Ainsi, chaque processus est limité par son espace d'adressage, mais on remplit la mémoire avec plusieurs processus, ce qui compense. Il s'agit là de l'implémentation la plus simple, qui a en plus l'avantage d'avoir la meilleure compatibilité logicielle. De simples changements dans le système d'exploitation suffisent à l'implémenter.
[[File:Extension de l'espace d'adressage.png|centre|vignette|upright=1.5|Extension de l'espace d'adressage]]
Un autre implémentation donne plusieurs espaces d'adressage différents à chaque processus, et a donc accès à autant de mémoire que permis par la somme de ces espaces d'adressage. Par exemple, sur un ordinateur avec 16 gigas de RAM et un espace d'adressage de 4 gigas, un programme peut utiliser toute la RAM en utilisant 4 espaces d'adressage distincts. On passe d'un espace d'adressage à l'autre en changeant la correspondance adresse logique-physique. L'inconvénient est que la compatibilité logicielle est assez mauvaise. Modifier l'OS ne suffit pas, les programmeurs doivent impérativement concevoir leurs programmes pour qu'ils utilisent explicitement plusieurs espaces d'adressage.
Les deux implémentations font usage des adresses logiques homonymes, mais à l'intérieur d'un même processus. Pour rappel, cela veut dire qu'une adresse logique correspond à des adresses physiques différentes. Rien d'étonnant vu qu'on utilise plusieurs espaces d'adressage, comme pour l'abstraction des processus, sauf que cette fois-ci, on a plusieurs espaces d'adressage par processus. Prenons l'exemple où on a 8 gigas de RAM sur un processeur 32 bits, dont l'espace d'adressage ne gère que 4 gigas. L'idée est qu'une adresse correspondra à une adresse dans les premiers 4 gigas, ou dans les seconds 4 gigas. L'adresse logique X correspondra d'abord à une adresse physique dans les premiers 4 gigas, puis à une adresse physique dans les seconds 4 gigas.
===La protection mémoire===
La '''protection mémoire''' regroupe des techniques très différentes les unes des autres, qui visent à améliorer la sécurité des programmes et des systèmes d'exploitation. Elles visent à empêcher de lire, d'écrire ou d'exécuter certaines portions de mémoire. Sans elle, les programmes peuvent techniquement lire ou écrire les données des autres, ce qui causent des situations non-prévues par le programmeur, avec des conséquences qui vont d'un joli plantage à des failles de sécurité dangereuses.
La première technique de protection mémoire est l{{'}}'''isolation des processus''', qu'on a vue plus haut. Elle garantit que chaque programme n'a accès qu'à certaines portions dédiées de la mémoire et rend le reste de la mémoire inaccessible en lecture et en écriture. Le système d'exploitation attribue à chaque programme une ou plusieurs portions de mémoire rien que pour lui, auquel aucun autre programme ne peut accéder. Un tel programme, isolé des autres, s'appelle un '''processus''', d'où le nom de cet objectif. Toute tentative d'accès à une partie de la mémoire non autorisée déclenche une exception matérielle (rappelez-vous le chapitre sur les interruptions) qui est traitée par une routine du système d'exploitation. Généralement, le programme fautif est sauvagement arrêté et un message d'erreur est affiché à l'écran.
La '''protection de l'espace exécutable''' empêche d’exécuter quoique ce soit provenant de certaines zones de la mémoire. En effet, certaines portions de la mémoire sont censées contenir uniquement des données, sans aucun programme ou code exécutable. Cependant, des virus informatiques peuvent se cacher dedans et d’exécuter depuis celles-ci. Ou encore, des failles de sécurités peuvent permettre à un attaquant d'injecter du code exécutable malicieux dans des données, ce qui peut lui permettre de lire les données manipulées par un programme, prendre le contrôle de la machine, injecter des virus, ou autre. Pour éviter cela, le système d'exploitation peut marquer certaines zones mémoire comme n'étant pas exécutable. Toute tentative d’exécuter du code localisé dans ces zones entraîne la levée d'une exception ou d'une erreur et le système d'exploitation réagit en conséquence. Là encore, le processeur doit détecter les exécutions non autorisées.
D'autres méthodes de protection mémoire visent à limiter des actions dangereuses. Pour cela, le processeur et l'OS gèrent des '''droits d'accès''', qui interdisent certaines actions pour des programmes non-autorisés. Lorsqu'on exécute une opération interdite, le système d’exploitation et/ou le processeur réagissent en conséquence. La première technique de ce genre n'est autre que la séparation entre espace noyau et utilisateur, vue dans le chapitre sur les interruptions. Mais il y en a d'autres, comme nous le verrons dans ce chapitre.
==La MMU==
La traduction des adresses logiques en adresses physiques se fait par un circuit spécialisé, appelé la '''''Memory Management Unit''''' (MMU). De nos jours, elle est intégrée dans le processeur, à la suite de l'unité mémoire. Mais il a existé des processeurs avec une MMU externe, soudée sur la carte mère.
La traduction des adresses logiques en adresses physiques demande d'accéder à des données sont en mémoire RAM, qui sont gérés par le système d'exploitation. Aussi, les processeurs modernes incorporent des mémoires caches appelées des '''''Translation Lookaside Buffers''''', ou encore TLB. Nous nous pouvons pas parler des TLB pour le moment, car nous n'avons pas encore abordé le chapitre sur les mémoires caches, mais un chapitre entier sera dédié aux TLB d'ici peu.
[[File:MMU principle updated.png|centre|vignette|upright=2|MMU.]]
===Les MMU intégrées au processeur===
D'ordinaire, la MMU est intégrée au processeur. Et elle peut l'être de deux manières. La première en fait un circuit séparé, relié au bus d'adresse. La seconde fusionne la MMU avec l'unité de calcul d'adresse. La première solution est surtout utilisée avec une technique d'abstraction mémoire appelée la pagination, alors que l'autre l'est avec une autre méthode appelée la segmentation. La raison est que la traduction d'adresse avec la segmentation est assez simple : elle demande d'additionner le contenu d'un registre avec l'adresse logique, ce qui est le genre de calcul qu'une unité de calcul d'adresse sait déjà faire. La fusion est donc assez évidente.
Pour donner un exemple, l'Intel 8086 fusionnait l'unité de calcul d'adresse et la MMU. Précisément, il utilisait un même additionneur pour incrémenter le ''program counter'' et effectuer des calculs d'adresse liés à la segmentation. Il aurait été logique d'ajouter les pointeurs de pile avec, mais ce n'était pas possible. La raison est que le pointeur de pile ne peut pas être envoyé directement sur le bus d'adresse, vu qu'il doit passer par une phase de traduction en adresse physique liée à la segmentation.
[[File:80186 arch.png|centre|vignette|upright=2|Intel 8086, microarchitecture.]]
===Les MMU séparées du processeur, sur la carte mère===
Avant d'être intégrées au processeur, les MMU étaient des circuits séparés, placés sur la carte mère, entre le processeur et la mémoire. Elle étaient connectées en plein milieu du bus d'adresse. A savoir que le bus d'adresse entrait dans la MMU et en ressortait. Le processeur envoyait des adresses à la MMU, qui renvoyait une adresse physique. La MMU était facultative, à savoir que le système pouvait fonctionner sans MMU d'installée, mais l'abstraction mémoire n’était alors pas disponible.
Par exemple, les processeurs Motorola 68000 et 68010 pouvaient être combinés avec une MMU de type Motorola 68451. Elle supportait des versions simplifiées de la segmentation et de la pagination. Au minimum, elle ajoutait un support de la protection mémoire contre certains accès non-autorisés. La gestion de la mémoire virtuelle proprement dit n'était possible que si le processeur utilisé était un Motorola 68010, en raison de la manière dont le 68000 gérait ses accès mémoire. La MMU 68451 gérait un espace d'adressage de 16 mébioctets, découpé en maximum 32 pages/segments. On pouvait dépasser cette limite de 32 segments/pages en combinant plusieurs 68451.
Le Motorola 68851 était une MMU qui était prévue pour fonctionner de paire avec le Motorola 68020. Elle gérait la pagination pour un espace d'adressage de 32 bits.
Les processeurs suivants, les 68030, 68040, et 68060, avaient une MMU interne au processeur.
==La relocation matérielle==
Pour rappel, les systèmes d'exploitation moderne permettent de lancer plusieurs programmes en même temps et les laissent se partager la mémoire. Dans le cas le plus simple, qui n'est pas celui des OS modernes, le système d'exploitation découpe la mémoire en blocs d'adresses contiguës qui sont appelés des '''segments''', ou encore des ''partitions mémoire''. Les segments correspondent à un bloc de mémoire RAM. C'est-à-dire qu'un segment de 259 mébioctets sera un segment continu de 259 mébioctets dans la mémoire physique comme dans la mémoire logique. Dans ce qui suit, un segment contient un programme en cours d'exécution, comme illustré ci-dessous.
[[File:CPT Memory Addressable.svg|centre|vignette|upright=2|Espace d'adressage segmenté.]]
Le système d'exploitation mémorise la position de chaque segment en mémoire, ainsi que d'autres informations annexes. Le tout est regroupé dans la '''table de segment''', un tableau dont chaque case est attribuée à un programme/segment. La table des segments est un tableau numéroté, chaque segment ayant un numéro qui précise sa position dans le tableau. Chaque case, chaque entrée, contient un '''descripteur de segment''' qui regroupe plusieurs informations sur le segment : son adresse de base, sa taille, diverses informations.
===La relocation avec la relocation matérielle : le registre de base===
Un segment peut être placé n'importe où en RAM physique et sa position en RAM change à chaque exécution. Le programme est chargé à une adresse, celle du début du segment, qui change à chaque chargement du programme. Et toutes les adresses utilisées par le programme doivent être corrigées lors du chargement du programme, généralement par l'OS. Cette correction s'appelle la '''relocation''', et elle consiste à ajouter l'adresse de début du segment à chaque adresse manipulée par le programme.
[[File:Relocation assistée par matériel.png|centre|vignette|upright=2.5|Relocation.]]
La relocation matérielle fait que la relocation est faite par le processeur, pas par l'OS. La relocation est intégrée dans le processeur par l'intégration d'un registre : le '''registre de base''', aussi appelé '''registre de relocation'''. Il mémorise l'adresse à laquelle commence le segment, la première adresse du programme. Pour effectuer la relocation, le processeur ajoute automatiquement l'adresse de base à chaque accès mémoire, en allant la chercher dans le registre de relocation.
[[File:Registre de base de segment.png|centre|vignette|upright=2|Registre de base de segment.]]
Le processeur s'occupe de la relocation des segments et le programme compilé n'en voit rien. Pour le dire autrement, les programmes manipulent des adresses logiques, qui sont traduites par le processeur en adresses physiques. La traduction se fait en ajoutant le contenu du registre de relocation à l'adresse logique. De plus, cette méthode fait que chaque programme a son propre espace d'adressage.
[[File:CPU created logical address presentation.png|centre|vignette|upright=2|Traduction d'adresse avec la relocation matérielle.]]
Le système d'exploitation mémorise les adresses de base pour chaque programme, dans la table des segments. Le registre de base est mis à jour automatiquement lors de chaque changement de segment. Pour cela, le registre de base est accessible via certaines instructions, accessibles en espace noyau, plus rarement en espace utilisateur. Le registre de segment est censé être adressé implicitement, vu qu'il est unique. Si ce n'est pas le cas, il est possible d'écrire dans ce registre de segment, qui est alors adressable.
===La protection mémoire avec la relocation matérielle : le registre limite===
Sans restrictions supplémentaires, la taille maximale d'un segment est égale à la taille complète de l'espace d'adressage. Sur les processeurs 32 bits, un segment a une taille maximale de 2^32 octets, soit 4 gibioctets. Mais il est possible de limiter la taille du segment à 2 gibioctets, 1 gibioctet, 64 Kibioctets, ou toute autre taille. La limite est définie lors de la création du segment, mais elle peut cependant évoluer au cours de l'exécution du programme, grâce à l'allocation mémoire.
Le processeur vérifie à chaque accès mémoire que celui-ci se fait bien dans le segment, qu'il ne déborde pas en-dehors. C'est possible qu'une adresse calculée sorte du segment, à la suite d'un bug ou d'une erreur de programmation, voire pire. Et le processeur doit éviter de tels '''débordements de segments'''.
A chaque accès mémoire, le processeur compare l'adresse accédée et vérifie qu'elle est bien dans le segment. Pour cela, il y a deux solutions. La première part du principe que le segment est placé en mémoire entre l'adresse de base et l'adresse limite. Il suffit de mémoriser l'adresse limite, l'adresse physique à ne pas dépasser. Une autre solution mémorise la taille du segment. La table des segments doit donc mémoriser, en plus de l'adresse de base : soit l'adresse maximale du segment, soit la taille du segment. D'autres informations peuvent être ajoutées, comme on le verra plus tard, mais cela complexifie la table des segments.
De plus, le processeur se voit ajouter un '''registre limite''', qui mémorise soit la taille du segment, soit l'adresse limite. Les deux registres, base et limite, sont utilisés pour vérifier si un programme qui lit/écrit de la mémoire en-dehors de son segment attitré : au-delà pour le registre limite, en-deça pour le registre de base. Le processeur vérifie pour chaque accès mémoire ne déborde pas au-delà du segment qui lui est allouée, ce qui n'arrive que si l'adresse d'accès dépasse la valeur du registre limite. Pour les accès en-dessous du segment, il suffit de vérifier si l'addition de relocation déborde, tout débordement signifiant erreur de protection mémoire.
[[File:Registre limite.png|centre|vignette|upright=2|Registre limite]]
Utiliser la taille du segment a de nombreux avantages. L'un d'entre eux se manifeste quand on déplace un segment en mémoire RAM. Le descripteur doit alors être mis à jour, et c'est plus facile quand on utilise la taille du segment. Si on utilise l'adresse limite, il faut mettre à jour à la fois l'adresse de base et l'adresse limite, dans le descripteur. En utilisant la taille, seule l'adresse de base doit être modifiée, vu que le segment n'a pas changé de taille. Un autre avantage est lié aux performances, mais nous devons faire un détour pour le comprendre.
La taille du segment est équivalent à l'adresse logique maximale possible. Par exemple, si un segment fait 256 octets, les adresses logiques possibles vont de 0 à 255, 256 est donc à la fois la taille du segment et l'adresse logique à partir de laquelle on déborde du segment. Et cela marche si on remplace 256 par n'importe quelle valeur : vu que le segment commence à l'adresse 0, sa taille en octets indique l'adresse de dépassement. Interpréter la taille du segment comme une adresse logique fait que les tests avec le registre limite sont plus performants, voyons pourquoi.
En utilisant l'adresse physique limite, on doit faire la relocation, puis comparer l'adresse calculée avec l'adresse limite. Le calcul d'adresse doit se faire avant la vérification. En utilisant la taille, on doit comparer l'adresse logique avec la taille du segment. On peut alors faire le test de débordement avant ou pendant la relocation. Les deux peuvent être faits en parallèle, dans deux circuits distincts, ce qui améliore un peu le temps d'un accès mémoire. Quelques processeurs en ont profité, mais on verra cela dans la section sur la segmentation.
[[File:Comparaison entre adresse limite physique et logique.png|centre|vignette|upright=2|Comparaison entre adresse limite physique et logique]]
Les registres de base et limite sont altérés uniquement par le système d'exploitation et ne sont accessibles qu'en espace noyau. Lorsque le système d'exploitation charge un programme, ou reprend son exécution, il charge les adresses de début/fin du segment dans ces registres. D'ailleurs, ces deux registres doivent être sauvegardés et restaurés lors de chaque interruption. Par contre, et c'est assez évident, ils ne le sont pas lors d'un appel de fonction. Cela fait une différence de plus entre interruption et appels de fonctions.
: Il faut noter que le registre limite et le registre de base sont parfois fusionnés en un seul registre, qui contient un descripteur de segment tout entier.
Pour information, la relocation matérielle avec un registre limite a été implémentée sur plusieurs processeurs assez anciens, notamment sur les anciens supercalculateurs de marque CDC. Un exemple est le fameux CDC 6600, qui implémentait cette technique.
===La mémoire virtuelle avec la relocation matérielle===
Il est possible d'implémenter la mémoire virtuelle avec la relocation matérielle. Pour cela, il faut swapper des segments entiers sur le disque dur. Les segments sont placés en mémoire RAM et leur taille évolue au fur et à mesure que les programmes demandent du rab de mémoire RAM. Lorsque la mémoire est pleine, ou qu'un programme demande plus de mémoire que disponible, des segments entiers sont sauvegardés dans le ''swapfile'', pour faire de la place.
Faire ainsi de demande juste de mémoriser si un segment est en mémoire RAM ou non, ainsi que la position des segments swappés dans le ''swapfile''. Pour cela, il faut modifier la table des segments, afin d'ajouter un '''bit de swap''' qui précise si le segment en question est swappé ou non. Lorsque le système d'exploitation veut swapper un segment, il le copie dans le ''swapfile'' et met ce bit à 1. Lorsque l'OS recharge ce segment en RAM, il remet ce bit à 0. La gestion de la position des segments dans le ''swapfile'' est le fait d'une structure de données séparée de la table des segments.
L'OS exécute chaque programme l'un après l'autre, à tour de rôle. Lorsque le tour d'un programme arrive, il consulte la table des segments pour récupérer les adresses de base et limite, mais il vérifie aussi le bit de swap. Si le bit de swap est à 0, alors l'OS se contente de charger les adresses de base et limite dans les registres adéquats. Mais sinon, il démarre une routine d'interruption qui charge le segment voulu en RAM, depuis le ''swapfile''. C'est seulement une fois le segment chargé que l'on connait son adresse de base/limite et que le chargement des registres de relocation peut se faire.
Un défaut évident de cette méthode est que l'on swappe des programmes entiers, qui sont généralement assez imposants. Les segments font généralement plusieurs centaines de mébioctets, pour ne pas dire plusieurs gibioctets, à l'époque actuelle. Ils étaient plus petits dans l'ancien temps, mais la mémoire était alors plus lente. Toujours est-il que la copie sur le disque dur des segments est donc longue, lente, et pas vraiment compatible avec le fait que les programmes s'exécutent à tour de rôle. Et ca explique pourquoi la relocation matérielle n'est presque jamais utilisée avec de la mémoire virtuelle.
===L'extension d'adressage avec la relocation matérielle===
Passons maintenant à la dernière fonctionnalité implémentable avec la traduction d'adresse : l'extension d'adressage. Elle permet d'utiliser plus de mémoire que ne le permet l'espace d'adressage. Par exemple, utiliser plus de 64 kibioctets de mémoire sur un processeur 16 bits. Pour cela, les adresses envoyées à la mémoire doivent être plus longues que les adresses gérées par le processeur.
L'extension des adresses se fait assez simplement avec la relocation matérielle : il suffit que le registre de base soit plus long. Prenons l'exemple d'un processeur aux adresses de 16 bits, mais qui est reliée à un bus d'adresse de 24 bits. L'espace d'adressage fait juste 64 kibioctets, mais le bus d'adresse gère 16 mébioctets de RAM. On peut utiliser les 16 mébioctets de RAM à une condition : que le registre de base fasse 24 bits, pas 16.
Un défaut de cette approche est qu'un programme ne peut pas utiliser plus de mémoire que ce que permet l'espace d'adressage. Mais par contre, on peut placer chaque programme dans des portions différentes de mémoire. Imaginons par exemple que l'on ait un processeur 16 bits, mais un bus d'adresse de 20 bits. Il est alors possible de découper la mémoire en 16 blocs de 64 kibioctets, chacun attribué à un segment/programme, qu'on sélectionne avec les 4 bits de poids fort de l'adresse. Il suffit de faire démarrer les segments au bon endroit en RAM, et cela demande juste que le registre de base le permette. C'est une sorte d'émulation de la commutation de banques.
==La segmentation en mode réel des processeurs x86==
Avant de passer à la suite, nous allons voir la technique de segmentation de l'Intel 8086, un des tout premiers processeurs 16 bits. Il s'agissait d'une forme très simple de segmentation, sans aucune forme de protection mémoire, ni même de mémoire virtuelle, ce qui le place à part des autres formes de segmentation. Il s'agit d'une amélioration de la relocation matérielle, qui avait pour but de permettre d'utiliser plus de 64 kibioctets de mémoire, ce qui était la limite maximale sur les processeurs 16 bits de l'époque.
Par la suite, la segmentation s'améliora et ajouta un support complet de la mémoire virtuelle et de la protection mémoire. L'ancienne forme de segmentation fut alors appelé le '''mode réel''', et la nouvelle forme de segmentation fut appelée le '''mode protégé'''. Le mode protégé rajoute la protection mémoire, en ajoutant des registres limite et une gestion des droits d'accès aux segments, absents en mode réel. De plus, il ajoute un support de la mémoire virtuelle grâce à l'utilisation d'une des segments digne de ce nom, table qui est absente en mode réel ! Pour le moment, voyons le mode réel.
===Les segments en mode réel===
[[File:Typical computer data memory arrangement.png|vignette|upright=0.5|Typical computer data memory arrangement]]
La segmentation en mode réel sépare la pile, le tas, le code machine et les données constantes dans quatre segments distincts.
* Le segment '''''text''''', qui contient le code machine du programme, de taille fixe.
* Le segment '''''data''''' contient des données de taille fixe qui occupent de la mémoire de façon permanente, des constantes, des variables globales, etc.
* Le segment pour la '''pile''', de taille variable.
* le reste est appelé le '''tas''', de taille variable.
Un point important est que sur ces processeurs, il n'y a pas de table des segments proprement dit. Chaque programme gére de lui-même les adresses de base des segments qu'il manipule. Il n'est en rien aidé par une table des segments gérée par le système d'exploitation.
===Les registres de segments en mode réel===
Chaque segment subit la relocation indépendamment des autres. Pour cela, le processeur intégre plusieurs registres de base, un par segment. Notons que cette solution ne marche que si le nombre de segments par programme est limité, à une dizaine de segments tout au plus. Les processeurs x86 utilisaient cette méthode, et n'associaient que 4 à 6 registres de segments par programme.
Les processeurs 8086 et le 286 avaient quatre registres de segment : un pour le code, un autre pour les données, et un pour la pile, le quatrième étant un registre facultatif laissé à l'appréciation du programmeur. Ils sont nommés CS (''code segment''), DS (''data segment''), SS (''Stack segment''), et ES (''Extra segment''). Le 386 rajouta deux registres, les registres FS et GS, qui sont utilisés pour les segments de données. Les processeurs post-386 ont donc 6 registres de segment.
Les registres CS et SS sont adressés implicitement, en fonction de l'instruction exécutée. Les instructions de la pile manipulent le segment associé à la pile, le chargement des instructions se fait dans le segment de code, les instructions arithmétiques et logiques vont chercher leurs opérandes sur le tas, etc. Et donc, toutes les instructions sont chargées depuis le segment pointé par CS, les instructions de gestion de la pile (PUSH et POP) utilisent le segment pointé par SS.
Les segments DS et ES sont, eux aussi, adressés implicitement. Pour cela, les instructions LOAD/STORE sont dupliquées : il y a une instruction LOAD pour le segment DS, une autre pour le segment ES. D'autres instructions lisent leurs opérandes dans un segment par défaut, mais on peut changer ce choix par défaut en précisant le segment voulu. Un exemple est celui de l'instruction CMPSB, qui compare deux octets/bytes : le premier est chargé depuis le segment DS, le second depuis le segment ES.
Un autre exemple est celui de l'instruction MOV avec un opérande en mémoire. Elle lit l'opérande en mémoire depuis le segment DS par défaut. Il est possible de préciser le segment de destination si celui-ci n'est pas DS. Par exemple, l'instruction MOV [A], AX écrit le contenu du registre AX dans l'adresse A du segment DS. Par contre, l'instruction MOV ES:[A], copie le contenu du registre AX das l'adresse A, mais dans le segment ES.
===La traduction d'adresse en mode réel===
La segmentation en mode réel a pour seul but de permettre à un programme de dépasser la limite des 64 KB autorisée par les adresses de 16 bits. L'idée est que chaque segment a droit à son propre espace de 64 KB. On a ainsi 64 Kb pour le code machine, 64 KB pour la pile, 64 KB pour un segment de données, etc. Les registres de segment mémorisaient la base du segment, les adresses calculées par l'ALU étant des ''offsets''. Ce sont tous des registres de 16 bits, mais ils ne mémorisent pas des adresses physiques de 16 bits, comme nous allons le voir.
[[File:Table des segments dans un banc de registres.png|centre|vignette|upright=2|Table des segments dans un banc de registres.]]
L'Intel 8086 utilisait des adresses de 20 bits, ce qui permet d'adresser 1 mébioctet de RAM. Vous pouvez vous demander comment on peut obtenir des adresses de 20 bits alors que les registres de segments font tous 16 bits ? Cela tient à la manière dont sont calculées les adresses physiques. Le registre de segment n'est pas additionné tel quel avec le décalage : à la place, le registre de segment est décalé de 4 rangs vers la gauche. Le décalage de 4 rangs vers la gauche fait que chaque segment a une adresse qui est multiple de 16. Le fait que le décalage soit de 16 bits fait que les segments ont une taille de 64 kibioctets.
{|class="wikitable"
|-
| <code> </code><code style="background:#DED">0000 0110 1110 1111</code><code>0000</code>
| Registre de segment -
| 16 bits, décalé de 4 bits vers la gauche
|-
| <code>+ </code><code style="background:#DDF">0001 0010 0011 0100</code>
| Décalage/''Offset''
| 16 bits
|-
| colspan="3" |
|-
| <code> </code><code style="background:#FDF">0000 1000 0001 0010 0100</code>
| Adresse finale
| 20 bits
|}
Vous aurez peut-être remarqué que le calcul peut déborder, dépasser 20 bits. Mais nous reviendrons là-dessus plus bas. L'essentiel est que la MMU pour la segmentation en mode réel se résume à quelques registres et des additionneurs/soustracteurs.
Un exemple est l'Intel 8086, un des tout premier processeur Intel. Le processeur était découpé en deux portions : l'interface mémoire et le reste du processeur. L'interface mémoire est appelée la '''''Bus Interface Unit''''', et le reste du processeur est appelé l{{'}}'''''Execution Unit'''''. L'interface mémoire contenait les registres de segment, au nombre de 4, ainsi qu'un additionneur utilisé pour traduire les adresses logiques en adresses physiques. Elle contenait aussi une file d'attente où étaient préchargées les instructions.
Sur le 8086, la MMU est fusionnée avec les circuits de gestion du ''program counter''. Les registres de segment sont regroupés avec le ''program counter'' dans un même banc de registres, et un additionneur unique est utilisé à la fois à incrémenter le ''program counter'' et pour gérer la segmentation. L'additionneur est donc mutualisé entre segmentation et ''program counter''. En somme, il n'y a pas vraiment de MMU dédiée, mais un super-circuit en charge du Fetch et de la mémoire virtuelle, ainsi que du préchargement des instructions. Nous en reparlerons au chapitre suivant.
[[File:80186 arch.png|centre|vignette|upright=2.5|Architecture du 8086, du 80186 et de ses variantes.]]
La MMU du 286 était fusionnée avec l'unité de calcul d'adresse. Elle contient les registres de segments, un comparateur pour détecter les accès hors-segment, et plusieurs additionneurs. Il y a un additionneur pour les calculs d'adresse proprement dit, suivi d'un additionneur pour la relocation.
[[File:Intel i80286 arch.svg|centre|vignette|upright=3|Intel i80286 arch]]
===La segmentation en mode réel accepte plusieurs segments de code/données===
Les programmes peuvent parfaitement répartir leur code machine dans plusieurs segments de code. La limite de 64 KB par segment est en effet assez limitante, et il n'était pas rare qu'un programme stocke son code dans deux ou trois segments. Il en est de même avec les données, qui peuvent être réparties dans deux ou trois segments séparés. La seule exception est la pile : elle est forcément dans un segment unique et ne peut pas dépasser 64 KB.
Pour gérer plusieurs segments de code/donnée, il faut changer de segment à la volée suivant les besoins, en modifiant les registres de segment. Il s'agit de la technique de '''commutation de segment'''. Pour cela, tous les registres de segment, à l'exception de CS, peuvent être altérés par une instruction d'accès mémoire, soit avec une instruction MOV, soit en y copiant le sommet de la pile avec une instruction de dépilage POP. L'absence de sécurité fait que la gestion de ces registres est le fait du programmeur, qui doit redoubler de prudence pour ne pas faire n'importe quoi.
Pour le code machine, le répartir dans plusieurs segments posait des problèmes au niveau des branchements. Si la plupart des branchements sautaient vers une instruction dans le même segment, quelques rares branchements sautaient vers du code machine dans un autre segment. Intel avait prévu le coup et disposait de deux instructions de branchement différentes pour ces deux situations : les '''''near jumps''''' et les '''''far jumps'''''. Les premiers sont des branchements normaux, qui précisent juste l'adresse à laquelle brancher, qui correspond à la position de la fonction dans le segment. Les seconds branchent vers une instruction dans un autre segment, et doivent préciser deux choses : l'adresse de base du segment de destination, et la position de la destination dans le segment. Le branchement met à jour le registre CS avec l'adresse de base, avant de faire le branchement. Ces derniers étaient plus lents, car on n'avait pas à changer de segment et mettre à jour l'état du processeur.
Il y avait la même pour l'instruction d'appel de fonction, avec deux versions de cette instruction. La première version, le '''''near call''''' est un appel de fonction normal, la fonction appelée est dans le segment en cours. Avec la seconde version, le '''''far call''''', la fonction appelée est dans un segment différent. L'instruction a là aussi besoin de deux opérandes : l'adresse de base du segment de destination, et la position de la fonction dans le segment. Un ''far call'' met à jour le registre CS avec l'adresse de base, ce qui fait que les ''far call'' sont plus lents que les ''near call''. Il existe aussi la même chose, pour les instructions de retour de fonction, avec une instruction de retour de fonction normale et une instruction de retour qui renvoie vers un autre segment, qui sont respectivement appelées '''''near return''''' et '''''far return'''''. Là encore, il faut préciser l'adresse du segment de destination dans le second cas.
La même chose est possible pour les segments de données. Sauf que cette fois-ci, ce sont les pointeurs qui sont modifiés. pour rappel, les pointeurs sont, en programmation, des variables qui contiennent des adresses. Lors de la compilation, ces pointeurs sont placés soit dans un registre, soit dans les instructions (adressage absolu), ou autres. Ici, il existe deux types de pointeurs, appelés '''''near pointer''''' et '''''far pointer'''''. Vous l'avez deviné, les premiers sont utilisés pour localiser les données dans le segment en cours d'utilisation, alors que les seconds pointent vers une donnée dans un autre segment. Là encore, la différence est que le premier se contente de donner la position dans le segment, alors que les seconds rajoutent l'adresse de base du segment. Les premiers font 16 bits, alors que les seconds en font 32 : 16 bits pour l'adresse de base et 16 pour l{{'}}''offset''.
===L'occupation de l'espace d'adressage par les segments===
Nous venons de voir qu'un programme pouvait utiliser plus de 4-6 segments, avec la commutation de segment. Mais d'autres programmes faisaient l'inverse, à savoir qu'ils se débrouillaient avec seulement 1 ou 2 segments. Suivant le nombre de segments utilisés, la configuration des registres n'était pas la même. Les configurations possibles sont appelées des ''modèle mémoire'', et il y en a en tout 6. En voici la liste :
{| class="wikitable"
|-
! Modèle mémoire !! Configuration des segments !! Configuration des registres || Pointeurs utilisés || Branchements utilisés
|-
| Tiny* || Segment unique pour tout le programme || CS=DS=SS || ''near'' uniquement || ''near'' uniquement
|-
| Small || Segment de donnée séparé du segment de code, pile dans le segment de données || DS=SS || ''near'' uniquement || ''near'' uniquement
|-
| Medium || Plusieurs segments de code unique, un seul segment de données || CS, DS et SS sont différents || ''near'' et ''far'' || ''near'' uniquement
|-
| Compact || Segment de code unique, plusieurs segments de données || CS, DS et SS sont différents || ''near'' uniquement || ''near'' et ''far''
|-
| Large || Plusieurs segments de code, plusieurs segments de données || CS, DS et SS sont différents || ''near'' et ''far'' || ''near'' et ''far''
|}
Un programme est censé utiliser maximum 4-6 segments de 64 KB, ce qui permet d'adresser maximum 64 * 6 = 384 KB de RAM, soit bien moins que le mébioctet de mémoire théoriquement adressable. Mais ce défaut est en réalité contourné par la commutation de segment, qui permettait d'adresser la totalité de la RAM si besoin. Une second manière de contourner cette limite est que plusieurs processus peuvent s'exécuter sur un seul processeur, si l'OS le permet. Ce n'était pas le cas à l'époque du DOS, qui était un OS mono-programmé, mais c'était en théorie possible. La limite est de 6 segments par programme/processus, en exécuter plusieurs permet d'utiliser toute la mémoire disponible rapidement.
[[File:Overlapping realmode segments.svg|vignette|Segments qui se recouvrent en mode réel.]]
Vous remarquerez qu'avec des registres de segments de 16 bits, on peut gérer 65536 segments différents, chacun de 64 KB. Et 65 536 segments de 64 kibioctets, ça ne rentre pas dans le mébioctet de mémoire permis avec des adresses de 20 bits. La raison est que plusieurs couples segment+''offset'' pointent vers la même adresse. En tout, chaque adresse peut être adressée par 4096 couples segment+''offset'' différents.
L'avantage de cette méthode est que des segments peuvent se recouvrir, à savoir que la fin de l'un se situe dans le début de l'autre, comme illustré ci-contre. Cela permet en théorie de partager de la mémoire entre deux processus. Mais la technique est tout sauf pratique et est donc peu utilisée. Elle demande de placer minutieusement les segments en RAM, et les données à partager dans les segments. En pratique, les programmeurs et OS utilisent des segments qui ne se recouvrent pas et sont disjoints en RAM.
Le nombre maximal de segments disjoints se calcule en prenant la taille de la RAM, qu'on divise par la taille d'un segment. Le calcul donne : 1024 kibioctets / 64 kibioctets = 16 segments disjoints. Un autre calcul prend le nombre de segments divisé par le nombre d'adresses aliasées, ce qui donne 65536 / 4096 = 16. Seulement 16 segments, c'est peu. En comptant les segments utilisés par l'OS et ceux utilisés par le programme, la limite est vite atteinte si le programme utilise la commutation de segment.
===Le mode réel sur les 286 et plus : la ligne d'adresse A20===
Pour résumer, le registre de segment contient des adresses de 20 bits, dont les 4 bits de poids faible sont à 0. Et il se voit ajouter un ''offset'' de 16 bits. Intéressons-nous un peu à l'adresse maximale que l'on peut calculer avec ce système. Nous allons l'appeler l{{'}}'''adresse maximale de segmentation'''. Elle vaut :
{|class="wikitable"
|-
| <code> </code><code style="background:#DED">1111 1111 1111 1111</code><code>0000</code>
| Registre de segment -
| 16 bits, décalé de 4 bits vers la gauche
|-
| <code>+ </code><code style="background:#DDF">1111 1111 1111 1111</code>
| Décalage/''Offset''
| 16 bits
|-
| colspan="3" |
|-
| <code> </code><code style="background:#FDF">1 0000 1111 1111 1110 1111</code>
| Adresse finale
| 20 bits
|}
Le résultat n'est pas l'adresse maximale codée sur 20 bits, car l'addition déborde. Elle donne un résultat qui dépasse l'adresse maximale permis par les 20 bits, il y a un 21ème bit en plus. De plus, les 20 bits de poids faible ont une valeur bien précise. Ils donnent la différence entre l'adresse maximale permise sur 20 bit, et l'adresse maximale de segmentation. Les bits 1111 1111 1110 1111 traduits en binaire donnent 65 519; auxquels il faut ajouter l'adresse 1 0000 0000 0000 0000. En tout, cela fait 65 520 octets adressables en trop. En clair : on dépasse la limite du mébioctet de 65 520 octets. Le résultat est alors très différent selon que l'on parle des processeurs avant le 286 ou après.
Avant le 286, le bus d'adresse faisait exactement 20 bits. Les adresses calculées ne pouvaient pas dépasser 20 bits. L'addition générait donc un débordement d'entier, géré en arithmétique modulaire. En clair, les bits de poids fort au-delà du vingtième sont perdus. Le calcul de l'adresse débordait et retournait au début de la mémoire, sur les 65 520 premiers octets de la mémoire RAM.
[[File:IBM PC Memory areas.svg|vignette|IBM PC Memory Map, la ''High memory area'' est en jaune.]]
Le 80286 en mode réel gère des adresses de base de 24 bits, soit 4 bits de plus que le 8086. Le résultat est qu'il n'y a pas de débordement. Les bits de poids fort sont conservés, même au-delà du 20ème. En clair, la segmentation permettait de réellement adresser 65 530 octets au-delà de la limite de 1 mébioctet. La portion de mémoire adressable était appelé la '''''High memory area''''', qu'on va abrévier en HMA.
{| class="wikitable"
|+ Espace d'adressage du 286
|-
! Adresses en héxadécimal !! Zone de mémoire
|-
| 10 FFF0 à FF FFFF || Mémoire étendue, au-delà du premier mébioctet
|-
| 10 0000 à 10 FFEF || ''High Memory Area''
|-
| 0 à 0F FFFF || Mémoire adressable en mode réel
|}
En conséquence, les applications peuvent utiliser plus d'un mébioctet de RAM, mais au prix d'une rétrocompatibilité imparfaite. Quelques programmes DOS ne marchaient pus à cause de ça. D'autres fonctionnaient convenablement et pouvaient adresser les 65 520 octets en plus.
Pour résoudre ce problème, les carte mères ajoutaient un petit circuit relié au 21ème bit d'adresse, nommé A20 (pas d'erreur, les fils du bus d'adresse sont numérotés à partir de 0). Le circuit en question pouvait mettre à zéro le fil d'adresse, ou au contraire le laisser tranquille. En le forçant à 0, le calcul des adresses déborde comme dans le mode réel des 8086. Mais s'il ne le fait pas, la ''high memory area'' est adressable. Le circuit était une simple porte ET, qui combinait le 21ème bit d'adresse avec un '''signal de commande A20''' provenant d'ailleurs.
Le signal de commande A20 était géré par le contrôleur de clavier, qui était soudé à la carte mère. Le contrôleur en question ne gérait pas que le clavier, il pouvait aussi RESET le processeur, alors gérer le signal de commande A20 n'était pas si problématique. Quitte à avoir un microcontrôleur sur la carte mère, autant s'en servir au maximum... La gestion du bus d'adresse étaitdonc gérable au clavier. D'autres carte mères faisaient autrement et préféraient ajouter un interrupteur, pour activer ou non la mise à 0 du 21ème bit d'adresse.
: Il faut noter que le signal de commande A20 était mis à 1 en mode protégé, afin que le 21ème bit d'adresse soit activé.
Le 386 ajouta deux registres de segment, les registres FS et GS, ainsi que le '''mode ''virtual 8086'''''. Ce dernier permet d’exécuter des programmes en mode réel alors que le système d'exploitation s'exécute en mode protégé. C'est une technique de virtualisation matérielle qui permet d'émuler un 8086 sur un 386. L'avantage est que la compatibilité avec les programmes anciens écrits pour le 8086 est conservée, tout en profitant de la protection mémoire. Tous les processeurs x86 qui ont suivi supportent ce mode virtuel 8086.
==La segmentation avec une table des segments==
La '''segmentation avec une table des segments''' est apparue sur des processeurs assez anciens, le tout premier étant le Burrough 5000. Elle a ensuite été utilisée sur les processeurs x86 des PCs, à partir du 286 d'Intel. Elle est aujourd'hui abandonnée sur les jeux d'instruction x86. Tout comme la segmentation en mode réel, la segmentation attribue plusieurs segments par programmes ! Et cela a des répercutions sur la manière dont la traduction d'adresse est effectuée.
===Pourquoi plusieurs segments par programme ?===
L'utilité d'avoir plusieurs segments par programme n'est pas évidente, mais elle le devient quand on se plonge dans le passé. Dans le passé, les programmeurs devaient faire avec une quantité de mémoire limitée et il n'était pas rare que certains programmes utilisent plus de mémoire que disponible sur la machine. Mais les programmeurs concevaient leurs programmes en fonction.
[[File:Overlay Programming.svg|vignette|upright=1|Overlay Programming]]
L'idée était d'implémenter un système de mémoire virtuelle, mais émulé en logiciel, appelé l{{'}}'''''overlaying'''''. Le programme était découpé en plusieurs morceaux, appelés des ''overlays''. Les ''overlays'' les plus importants étaient en permanence en RAM, mais les autres étaient faisaient un va-et-vient entre RAM et disque dur. Ils étaient chargés en RAM lors de leur utilisation, puis sauvegardés sur le disque dur quand ils étaient inutilisés. Le va-et-vient des ''overlays'' entre RAM et disque dur était réalisé en logiciel, par le programme lui-même. Le matériel n'intervenait pas, comme c'est le cas avec la mémoire virtuelle.
Avec la segmentation, un programme peut utiliser la technique des ''overlays'', mais avec l'aide du matériel. Il suffit de mettre chaque ''overlay'' dans son propre segment, et laisser la segmentation faire. Les segments sont swappés en tout ou rien : on doit swapper tout un segment en entier. L'intérêt est que la gestion du ''swapping'' est grandement facilitée, vu que c'est le système d'exploitation qui s'occupe de swapper les segments sur le disque dur ou de charger des segments en RAM. Pas besoin pour le programmeur de coder quoique ce soit. Par contre, cela demande l'intervention du programmeur, qui doit découper le programme en segments/''overlays'' de lui-même. Sans cela, la segmentation n'est pas très utile.
L{{'}}''overlaying'' est une forme de '''segmentation à granularité grossière''', à savoir que le programme est découpé en segments de grande taille. L'usage classique est d'avoir un segment pour la pile, un autre pour le code exécutable, un autre pour le reste. Éventuellement, on peut découper les trois segments précédents en deux ou trois segments, rarement au-delà. Les segments sont alors peu nombreux, guère plus d'une dizaine par programme. D'où le terme de ''granularité grossière''.
La '''segmentation à granularité fine''' pousse le concept encore plus loin. Avec elle, il y a idéalement un segment par entité manipulée par le programme, un segment pour chaque structure de donnée et/ou chaque objet. Par exemple, un tableau aura son propre segment, ce qui est idéal pour détecter les accès hors tableau. Pour les listes chainées, chaque élément de la liste aura son propre segment. Et ainsi de suite, chaque variable agrégée (non-primitive), chaque structure de donnée, chaque objet, chaque instance d'une classe, a son propre segment. Diverses fonctionnalités supplémentaires peuvent être ajoutées, ce qui transforme le processeur en véritable processeur orienté objet, mais passons ces détails pour le moment.
Vu que les segments correspondent à des objets manipulés par le programme, on peut deviner que leur nombre évolue au cours du temps. En effet, les programmes modernes peuvent demander au système d'exploitation du rab de mémoire pour allouer une nouvelle structure de données. Avec la segmentation à granularité fine, cela demande d'allouer un nouveau segment à chaque nouvelle allocation mémoire, à chaque création d'une nouvelle structure de données ou d'un objet. De plus, les programmes peuvent libérer de la mémoire, en supprimant les structures de données ou objets dont ils n'ont plus besoin. Avec la segmentation à granularité fine, cela revient à détruire le segment alloué pour ces objets/structures de données. Le nombre de segments est donc dynamique, il change au cours de l'exécution du programme.
===Les tables de segments avec la segmentation===
La présence de plusieurs segments par programme a un impact sur la table des segments. Avec la relocation matérielle, elle conte nait un segment par programme. Chaque entrée, chaque ligne de la table des segment, mémorisait l'adresse de base, l'adresse limite, un bit de présence pour la mémoire virtuelle et des autorisations liées à la protection mémoire. Avec la segmentation, les choses sont plus compliquées, car il y a plusieurs segments par programme. Les entrées ne sont pas modifiées, mais elles sont organisées différemment.
Avec cette forme de segmentation, la table des segments doit respecter plusieurs contraintes. Premièrement, il y a plusieurs segments par programmes. Deuxièmement, le nombre de segments est variable : certains programmes se contenteront d'un seul segment, d'autres de dizaine, d'autres plusieurs centaines, etc. Il y a typiquement deux manières de faire : soit utiliser une table des segments uniques, utiliser une table des segment par programme.
Il est possible d'utiliser une table des segment unique qui mémorise tous les segments de tous les processus, système d'exploitation inclut. On parle alors de '''table des segment globale'''. Mais cette solution n'est pas utilisée avec la segmentation proprement dite. Elle est utilisée sur les architectures à capacité qu'on détaillera vers la fin du chapitre, dans une section dédiée. À la place, la segmentation utilise une table de segment par processus/programme, chacun ayant une '''table des segment locale'''.
Dans les faits, les choses sont plus compliquées. Le système d'exploitation doit savoir où se trouvent les tables de segment locale pour chaque programme. Pour cela, il a besoin d'utiliser une table de segment globale, dont chaque entrée pointe non pas vers un segment, mais vers une table de segment locale. Lorsque l'OS effectue une commutation de contexte, il lit la table des segment globale, pour récupérer un pointeur vers celle-ci. Ce pointeur est alors chargé dans un registre du processeur, qui mémorise l'adresse de la table locale, ce qui sert lors des accès mémoire.
Une telle organisation fait que les segments d'un processus/programme sont invisibles pour les autres, il y a une certaine forme de sécurité. Un programme ne connait que sa table de segments locale, il n'a pas accès directement à la table des segments globales. Tout accès mémoire se passera à travers la table de segment locale, il ne sait pas où se trouvent les autres tables de segment locales.
Les processeurs x86 sont dans ce cas : ils utilisent une table de segment globale couplée à autant de table des segments qu'il y a de processus en cours d'exécution. La table des segments globale s'appelle la '''''Global Descriptor Table''''' et elle peut contenir 8192 segments maximum, ce qui permet le support de 8192 processus différents. Les tables de segments locales sont appelées les '''''Local Descriptor Table''''' et elles font aussi 8192 segments maximum, ce qui fait 8192 segments par programme maximum. Il faut noter que la table de segment globale peut mémoriser des pointeurs vers les routines d'interruption, certaines données partagées (le tampon mémoire pour le clavier) et quelques autres choses, qui n'ont pas leur place dans les tables de segment locales.
===La relocation avec la segmentation===
La table des segments locale mémorise les adresses de base et limite de chaque segment, ainsi que d'autres méta-données. Les informations pour un segment sont regroupés dans un '''descripteur de segment''', qui est codé sur plusieurs octets, et qui regroupe : adresse de base, adresse limite, bit de présence en RAM, méta-données de protection mémoire.
La table des segments est un tableau dans lequel les descripteurs de segment sont placés les uns à la suite des autres en mémoire RAM. La table des segments est donc un tableau de segment. Les segments d'un programme sont numérotés, le nombre s'appelant un '''indice de segment''', appelé '''sélecteur de segment''' dans la terminologie Intel. L'indice de segment n'est autre que l'indice du segment dans ce tableau.
[[File:Global Descriptor table.png|centre|vignette|upright=2|Table des segments locale.]]
Il n'y a pas de registre de segment proprement dit, qui mémoriserait l'adresse de base. À la place, les segments sont adressés de manière indirecte. À la place, les registres de segment mémorisent des sélecteurs de segment. Ils sont utilisés pour lire l'adresse de base/limite dans la table de segment en mémoire RAM. Pour cela, un registre mémorise l'adresse de la table de segment locale, sa position en mémoire RAM.
Toute lecture ou écriture se fait en deux temps, en deux accès mémoire, consécutifs. Premièrement, le numéro de segment est utilisé pour adresser la table des segment. La lecture récupère alors un pointeur vers ce segment. Deuxièmement, ce pointeur est utilisé pour faire la lecture ou écriture. Plus précisément, la première lecture récupère un descripteur de segment qui contient l'adresse de base, le pointeur voulu, mais aussi l'adresse limite et d'autres informations.
[[File:Segmentation avec table des segments.png|centre|vignette|upright=2|Segmentation avec table des segments]]
L'accès à la table des segments se fait automatiquement à chaque accès mémoire. La conséquence est que chaque accès mémoire demande d'en faire deux : un pour lire la table des segments, l'autre pour l'accès lui-même. Il s'agit en quelque sorte d'une forme d'adressage indirect mémoire.
Un point important est que si le premier accès ne fait qu'une simple lecture dans un tableau, le second accès implique des calculs d'adresse. En effet, le premier accès récupère l'adresse de base du segment, mais le second accès sélectionne une donnée dans le segment, ce qui demande de calculer son adresse. L'adresse finale se déduit en combinant l'adresse de base avec un décalage (''offset'') qui donne la position de la donnée dans ce segment. L'indice de segment est utilisé pour récupérer l'adresse de base du segment. Une fois cette adresse de base connue, on lui additionne le décalage pour obtenir l'adresse finale.
[[File:Table des segments.png|centre|vignette|upright=2|Traduction d'adresse avec une table des segments.]]
Pour effectuer automatiquement l'accès à la table des segments, le processeur doit contenir un registre supplémentaire, qui contient l'adresse de la table de segment, afin de la localiser en mémoire RAM. Nous appellerons ce registre le '''pointeur de table'''. Le pointeur de table est combiné avec l'indice de segment pour adresser le descripteur de segment adéquat.
[[File:Segment 2.svg|centre|vignette|upright=2|Traduction d'adresse avec une table des segments, ici appelée table globale des de"scripteurs (terminologie des processeurs Intel x86).]]
Un point important est que la table des segments n'est pas accessible pour le programme en cours d'exécution. Il ne peut pas lire le contenu de la table des segments, et encore moins la modifier. L'accès se fait seulement de manière indirecte, en faisant usage des indices de segments, mais c'est un adressage indirect. Seul le système d'exploitation peut lire ou écrire la table des segments directement.
Plus haut, j'ai dit que tout accès mémoire impliquait deux accès mémoire : un pour charger le descripteur de segment, un autre pour la lecture/écriture proprement dite. Cependant, cela aurait un impact bien trop grand sur les performances. Dans les faits, les processeurs avec segmentations intégraient un '''cache de descripteurs de segments''', pour limiter la casse. Quand un descripteur de segment est lu depuis la RAM, il est copié dans ce cache. Les accès ultérieurs accédent au descripteur dans le cache, pas besoin de passer par la RAM. L'intel 386 avait un cache de ce type.
===La protection mémoire : les accès hors-segments===
Comme avec la relocation matérielle, le processeur détecte les débordements de segment. Pour cela, il compare l'adresse logique accédée avec l'adresse limite, ou compare la taille limite avec le décalage. De nombreux processeurs, comme l'Intel 386, préféraient utiliser la taille du segment, pour une question d'optimisation. En effet, si on compare l'adresse finale avec l'adresse limite, on doit faire la relocation avant de comparer l'adresse relocatée. Mais en utilisant la taille, ce n'est pas le cas : on peut faire la comparaison avant, pendant ou après la relocation.
Un détail à prendre en compte est la taille de la donnée accédée. Sans cela, la comparaison serait très simple : on vérifie si ''décalage <= taille du segment'', ou on compare des adresses de la même manière. Mais imaginez qu'on accède à une donnée de 4 octets : il se peut que l'adresse de ces 4 octets rentre dans le segment, mais que quelques octets débordent. Par exemple, les deux premiers octets sont dans le segment, mais pas les deux suivants. La vraie comparaison est alors : ''décalage + 4 octets <= taille du segment''.
Mais il est possible de faire le calcul autrement, et quelques processeurs comme l'Intel 386 ne s'en sont pas privé. Il calculait la différence ''taille du segment - décalage'', et vérifiait le résultat. Le processeur gérait des données de 1, 2 et 4 octets, ce qui fait que le résultat devait être entre 0 et 3. Le processeur prenait le résultat de la soustraction, et vérifiait alors que les 30 bits de poids fort valaient bien 0. Il vérifiait aussi que les deux bits de poids faible avaient la bonne valeur.
[[File:Vm7.svg|centre|vignette|upright=2|Traduction d'adresse avec vérification des accès hors-segment.]]
Une nouveauté fait son apparition avec la segmentation : la '''gestion des droits d'accès'''. Par exemple, il est possible d'interdire d'exécuter le contenu d'un segment, ce qui fournit une protection contre certaines failles de sécurité ou certains virus. Lorsqu'on exécute une opération interdite, le processeur lève une exception matérielle, à charge du système d'exploitation de gérer la situation.
Pour cela, chaque segment se voit attribuer un certain nombre d'autorisations d'accès qui indiquent si l'on peut lire ou écrire dedans, si celui-ci contient un programme exécutable, etc. Les autorisations pour chaque segment sont placées dans le descripteur de segment. Elles se résument généralement à quelques bits, qui indiquent si le segment est accesible en lecture/écriture ou exécutable. Le tout est souvent concaténé dans un ou deux '''octets de droits d'accès'''.
L'implémentation de la protection mémoire dépend du CPU considéré. Les CPU microcodés peuvent en théorie utiliser le microcode. Lorsqu'une instruction mémoire s'exécute, le microcode effectue trois étapes : lire le descripteur de segment, faire les tests de protection mémoire, exécuter la lecture/écriture ou lever une exception. Létape de test est réalisée avec un ou plusieurs micro-branchements. Par exemple, une écriture va tester le bit R/W du descripteur, qui indique si on peut écrire dans le segment, en utilisant un micro-branchement. Le micro-branchement enverra vers une routine du microcode en cas d'erreur.
Les tests de protection mémoire demandent cependant de tester beaucoup de conditions différentes. Par exemple, le CPU Intel 386 testait moins d'une dizaine de conditions pour certaines instructions. Il est cependant possible de faire plusieurs comparaisons en parallèle en rusant un peu. Il suffit de mémoriser les octets de droits d'accès dans un registre interne, de masquer les bits non-pertinents, et de faire une comparaison avec une constante adéquate, qui encode la valeur que doivent avoir ces bits.
Une solution alternative utiliser un circuit combinatoire pour faire les tests de protection mémoire. Les tests sont alors faits en parallèles, plutôt qu'un par un par des micro-branchements. Par contre, le cout en matériel est assez important. Il faut ajouter ce circuit combinatoire, ce qui demande pas mal de circuits.
===La mémoire virtuelle avec la segmentation===
La mémoire virtuelle est une fonctionnalité souvent implémentée sur les processeurs qui gèrent la segmentation, alors que les processeurs avec relocation matérielle s'en passaient. Il faut dire que l'implémentation de la mémoire virtuelle est beaucoup plus simple avec la segmentation, comparé à la relocation matérielle. Le remplacement des registres de base par des sélecteurs de segment facilite grandement l'implémentation.
Le problème de la mémoire virtuelle est que les segments peuvent être swappés sur le disque dur n'importe quand, sans que le programme soit prévu. Le swapping est réalisé par une interruption de l'OS, qui peut interrompre le programme n'importe quand. Et si un segment est swappé, le registre de base correspondant devient invalide, il point sur une adresse en RAM où le segment était, mais n'est plus. De plus, les segments peuvent être déplacés en mémoire, là encore n'importe quand et d'une manière invisible par le programme, ce qui fait que les registres de base adéquats doivent être modifiés.
Si le programme entier est swappé d'un coup, comme avec la relocation matérielle simple, cela ne pose pas de problèmes. Mais dès qu'on utilise plusieurs registres de base par programme, les choses deviennent soudainement plus compliquées. Le problème est qu'il n'y a pas de mécanismes pour choisir et invalider le registre de base adéquat quand un segment est déplacé/swappé. En théorie, on pourrait imaginer des systèmes qui résolvent le problème au niveau de l'OS, mais tous ont des problèmes qui font que l'implémentation est compliquée ou que les performances sont ridicules.
L'usage d'une table des segments accédée à chaque accès résout complètement le problème. La table des segments est accédée à chaque accès mémoire, elle sait si le segment est swappé ou non, chaque accès vérifie si le segment est en mémoire et quelle est son adresse de base. On peut changer le segment de place n'importe quand, le prochain accès récupérera des informations à jour dans la table des segments.
L'implémentation de la mémoire virtuelle avec la segmentation est simple : il suffit d'ajouter un bit dans les descripteurs de segments, qui indique si le segment est swappé ou non. Tout le reste, la gestion de ce bit, du swap, et tout ce qui est nécessaire, est délégué au système d'exploitation. Lors de chaque accès mémoire, le processeur vérifie ce bit avant de faire la traduction d'adresse, et déclenche une exception matérielle si le bit indique que le segment est swappé. L'exception matérielle est gérée par l'OS.
===Le partage de segments===
Il est possible de partager un segment entre plusieurs applications. Cela peut servir pour partager des données entre deux programmes : un segment de données partagées est alors partagé entre deux programmes. Partager un segment de code est utile pour les bibliothèques partagées : la bibliothèque est placée dans un segment dédié, qui est partagé entre les programmes qui l'utilisent. Partager un segment de code est aussi utile quand plusieurs instances d'une même application sont lancés simultanément : le code n'ayant pas de raison de changer, celui-ci est partagé entre toutes les instances. Mais ce n'est là qu'un exemple.
La première solution pour cela est de configurer les tables de segment convenablement. Le même segment peut avoir des droits d'accès différents selon les processus. Les adresses de base/limite sont identiques, mais les tables des segments ont alors des droits d'accès différents. Mais cette méthode de partage des segments a plusieurs défauts.
Premièrement, les sélecteurs de segments ne sont pas les mêmes d'un processus à l'autre, pour un même segment. Le segment partagé peut correspondre au segment numéro 80 dans le premier processus, au segment numéro 1092 dans le second processus. Rien n'impose que les sélecteurs de segment soient les mêmes d'un processus à l'autre, pour un segment identique.
Deuxièmement, les adresses limite et de base sont dupliquées dans plusieurs tables de segments. En soi, cette redondance est un souci mineur. Mais une autre conséquence est une question de sécurité : que se passe-t-il si jamais un processus a une table des segments corrompue ? Il se peut que pour un segment identique, deux processus n'aient pas la même adresse limite, ce qui peut causer des failles de sécurité. Un processus peut alors subir un débordement de tampon, ou tout autre forme d'attaque.
[[File:Vm9.png|centre|vignette|upright=2|Illustration du partage d'un segment entre deux applications.]]
Une seconde solution, complémentaire, utilise une table de segment globale, qui mémorise des segments partagés ou accessibles par tous les processus. Les défauts de la méthode précédente disparaissent avec cette technique : un segment est identifié par un sélecteur unique pour tous les processus, il n'y a pas de duplication des descripteurs de segment. Par contre, elle a plusieurs défauts.
Le défaut principal est que cette table des segments est accessible par tous les processus, impossible de ne partager ses segments qu'avec certains pas avec les autres. Un autre défaut est que les droits d'accès à un segment partagé sont identiques pour tous les processus. Impossible d'avoir un segment partagé accessible en lecture seule pour un processus, mais accessible en écriture pour un autre. Il est possible de corriger ces défauts, mais nous en parlerons dans la section sur les architectures à capacité.
===L'extension d'adresse avec la segmentation===
L'extension d'adresse est possible avec la segmentation, de la même manière qu'avec la relocation matérielle. Il suffit juste que les adresses de base soient aussi grandes que le bus d'adresse. Mais il y a une différence avec la relocation matérielle : un même programme peut utiliser plus de mémoire qu'il n'y en a dans l'espace d'adressage. La raison est simple : un segment peut prendre tout l'espace d'adressage, et il y a plusieurs segments par programme.
Pour donner un exemple, prenons un processeur 16 bits, qui peut adresser 64 kibioctets, associé à une mémoire de 4 mébioctets. Il est possible de placer le code machine dans les premiers 64k de la mémoire, la pile du programme dans les 64k suivants, le tas dans les 64k encore après, et ainsi de suite. Le programme dépasse donc les 64k de mémoire de l'espace d'adressage. Ce genre de chose est impossible avec la relocation, où un programme est limité par l'espace d'adressage.
===Le mode protégé des processeurs x86===
L'Intel 80286, aussi appelé 286, ajouta un mode de segmentation séparé du mode réel, qui ajoute une protection mémoire à la segmentation, ce qui lui vaut le nom de '''mode protégé'''. Dans ce mode, les registres de segment ne contiennent pas des adresses de base, mais des sélecteurs de segments qui sont utilisés pour l'accès à la table des segments en mémoire RAM.
Le 286 bootait en mode réel, puis le système d'exploitation devait faire quelques manipulations pour passer en mode protégé. Le 286 était pensé pour être rétrocompatible au maximum avec le 80186. Mais les différences entre le 286 et le 8086 étaient majeures, au point que les applications devaient être réécrites intégralement pour profiter du mode protégé. Un mode de compatibilité permettait cependant aux applications destinées au 8086 de fonctionner, avec même de meilleures performances. Aussi, le mode protégé resta inutilisé sur la plupart des applications exécutées sur le 286.
Vint ensuite le processeur 80386, renommé en 386 quelques années plus tard. Sur ce processeur, les modes réel et protégé sont conservés tel quel, à une différence près : toutes les adresses passent à 32 bits, qu'il s'agisse des adresses de base, limite ou des ''offsets''. Le processeur peut donc adresser un grand nombre de segments : 2^32, soit plus de 4 milliards. Les segments grandissent aussi et passent de 64 KB maximum à 4 gibioctets maximum. Mais surtout : le 386 ajouta le support de la pagination en plus de la segmentation. Ces modifications ont été conservées sur les processeurs 32 bits ultérieurs.
Les processeurs x86 gèrent deux types de tables des segments : une table locale pour chaque processus, et une table globale partagée entre tous les processus. Il ne peut y avoir qu'une table locale d'active, vu que le processeur ne peut exécuter qu'un seul processus en même temps. Chaque table locale définit 8192 segments, pareil pour la table globale. La table globale est utilisée pour les segments du noyau et la mémoire partagée entre processus. Un défaut est qu'un segment partagé par la table globale est visible par tous les processus, avec les mêmes droits d'accès. Ce qui fait que cette méthode était peu utilisée en pratique. La table globale mémorise aussi des pointeurs vers les tables locales, avec un descripteur de segment par table locale.
Sur les processeurs x86 32 bits, un descripteur de segment est organisé comme suit, pour les architectures 32 bits. On y trouve l'adresse de base et la taille limite, ainsi que de nombreux bits de contrôle.
Le premier groupe de bits de contrôle est l'octet en bleu à droite. Il contient :
* le bit P qui indique que l'entrée contient un descripteur valide, qu'elle n'est pas vide ;
* deux bits DPL qui indiquent le niveau de privilège du segment (noyau, utilisateur, les deux intermédiaires spécifiques au x86) ;
* un bit S qui précise si le segment est de type système (utiles pour l'OS) ou un segment de code/données.
* un champ Type qui contient les bits suivants :
** un bit E qui indique si le segment contient du code exécutable ou non ;
** le bit RW qui indique s'il est en lecture seule ou non ;;
** Un bit A qui indique que le segment a récemment été accédé, information utile pour l'OS;
** un bit DC assez spécifiques.
En haut à gauche, en bleu, on trouve deux bits :
* Le bit G indique comment interpréter la taille contenue dans le descripteur : 0 si la taille est exprimée en octets, 1 si la taille est un nombre de pages de 4 kibioctets. Ce bit précise si on utilise la segmentation seule, ou combinée avec la pagination.
* Le bit DB précise si l'on utilise des segments en mode de compatibilité 16 bits ou des segments 32 bits.
[[File:SegmentDescriptor.svg|centre|vignette|upright=3|Segment Descriptor]]
Les indices de segment sont appelés des sélecteurs de segment. Ils ont une taille de 16 bits, mais 3 bits sont utilisés pour encoder des méta-données. Le numéro de segment est donc codé sur 13 bits, ce qui permettait de gérer maximum 8192 segments par table de segment (locale ou globale). Les 16 bits sont organisés comme suit :
* 13 bits pour le numéro du segment dans la table des segments, l'indice de segment proprement dit ;
* un bit qui précise s'il faut accéder à la table des segments globale ou locale ;
* deux bits qui indiquent le niveau de privilège de l'accès au segment (les 4 niveaux de protection, dont l'espace noyau et utilisateur).
[[File:SegmentSelector.svg|centre|vignette|upright=1.5|Sélecteur de segment 16 bit.]]
En tout, l'indice permet de gérer 8192 segments pour la table locale et 8192 segments de la table globale.
====La MMU du 386/486 : cache de segment, protection mémoire====
La MMU du 386 et celle du 486 étaient assez similaires. Elles étaient plus complexes que celle du 186 et du 286. Elle contenait un additionneur pour les calculs d'adresse et un comparateur pour tester si l'accès mémoire déborde d'un segment. Le test de débordement se faisait en parallèle du calcul de l'adresse finale, comme sur le 286.
L'additionneur était un additionneur trois-opérandes, qui additionnait l'adresse à lire/écrire, l'adresse de base du segment, et un décalage intégré dans l'instruction. En clair, l'unité de segmentation n'était pas qu'une MMU, elle prenait en charge une partie du calcul d'adresse. L'avantage est que cela permettait de gérer les modes d'adressage "base + décalage" et "base + indice + décalage" très facilement, en utilisant un minimum de circuits. Une conséquence de cette organisation était que l'usage d'un décalage était gratuit. Par contre, dès qu'on utilisait l'adressage "Base + Indice", avec ou sans décalage, l'instruction prenait un cycle de plus à s'exécuter, parce qu'il fallait faire l'addition "Base + Indice" dans l'ALU entière.
Le CPU 386 était le premier à implémenter la protection mémoire avec des segments. Pour cela, il intégrait une '''''Protection Test Unit''''', séparée du microcode, qu'on va abrévier en PTU. Précisément, il s'agissait d'un PLA (''Programmable Logic Array''), une sorte d'intermédiaire entre circuit logique fait sur mesure et mémoire ROM, qu'on a déjà abordé dans le chapitre sur les mémoires ROM. Mais cette unité ne faisait pas tout, le microcode était aussi impliqué. La PTU sera détaillée dans la section suivante.
Pour améliorer les performances, le 386 et le 486 intégraient un '''cache de descripteurs de segment''', aussi appelé le cache de descripteurs. Lorsqu'un descripteur état chargé pour la première fois, il était copié dans le cache de descripteurs de segment. Les accès mémoire ultérieur lisaient le descripteur de segment depuis ce cache, pas depuis la table des segments en RAM. Le cache de descripteurs gère aussi bien les segments en mode réel qu'en mode protégé. Récupérer l'adresse de base depuis cache se fait un peu différemment en mode réel et protégé, mais le cache gère cela tout seul. Idem pour récupérer la taille/adresse limite.
[[File:Microarchitecture du 386, avec focus sur la segmentation.png|centre|vignette|upright=2|Microarchitecture du 386 et du 486, avec focus sur la segmentation.]]
En mode réel, la taille des segment est censée être limitée à 64 kibioctets. Mais le processeur ne vérifiait pas si cette limite était dépassée. À la place, il utilisait la limite précisée dans le cache de segments. Pire que ça : le cache de segment n'était pas réinitialisé quand on passe du mode réel au mode protégé, et réciproquement. Et cette propriété a été à l'origine de l''''''unreal mode'''''. Il s'agissait d'un mode réel amélioré, capable d'utiliser des segments de 4 gibioctets et des adresses de 32 bits.
Passer en mode ''unreal'' pouvait se faire de deux manières. Il était possible d'altérer le cache de descripteur en utilisant l'instruction non-documentée LOADALL. Elle permettait de charger les descripteurs de segments dans le cache de descripteurs, avec une taille arbitraire. Une autre solution, beaucoup plus complexe sur le 386, demandait d'entrer en mode protégé pour configurer des segments de grande taille, de charger leurs descripteurs dans le cache de descripteur, puis de revenir en mode réel. En mode réel, les descripteurs dans le cache étaient encore disponibles et on pouvait les lire dans le cache.
====L'implémentation de la protection mémoire sur le 386====
La protection mémoire teste la valeur des bits P, S, X, E, R/W. Elle teste aussi les niveaux de privilège, avec deux bits DPL et CPL. En tout, le processeur pouvait tester 148 conditions différentes en parallèle dans la PTU. Cependant, les niveaux de privilèges étaient pré-traités par le microcode. Le microcode vérifiait aussi s'il y avait une erreur en terme d’anneau mémoire, avec par "exemple un segment en mode noyau accédé alors que le CPU est en espace utilisateur. Il fournissait alors un résultat sur deux bits, qui indiquait s'il y avait une erreur ou non, que la PTU utilisait.
Mais toutes les conditions n'étaient pas pertinentes à un instant t. Par exemple, il est pertinent de vérifier si le bit R/W était cohérent si l'instruction à exécuter est une écriture. Mais il n'y a pas besoin de tester le bit E qui indique qu'un segment est exécutable ou non, pour une lecture. En tout, le processeur pouvait se retrouver dans 33 situations possibles, chacune demandant de tester un sous-ensemble des 148 conditions. Pour préciser quel sous-ensembles tester, la PTU recevait un code opération, généré par le microcode.
Pour faire les tests de protection mémoire, le microcode avait une micro-opération nommée ''protection test operation'', qui envoyait les droits d'accès à la PTU. Lors de l'exécution d'une ''protection test operation'', le PLA recevait un descripteur de segment, lu depuis la mémoire RAM, ainsi qu'un code opération provenant du microcode.
{|class="wikitable"
|+ Entrée de la ''Protection Test Unit''
|-
! 15 - 14 !! 13 - 12 !! 11 !! 10 !! 9 !! 8 !! 7 !! 6 !! 5-0
|-
| P1 , P2 || || P || S || X || E || R/W || A || Code opération
|-
| Niveaux de privilèges cohérents/erreur || || Segment présent en mémoire ou swappé || S || X || Segment exécutable ou non || Segment accesible en lecture/écriture || Segment récemment accédé || Code opération
|}
Il fournissait en sortie un bit qui indiquait si une erreur de protection mémoire avait eu lieu ou non. Il fournissait aussi une adresse de 12 bits, utilisée seulement en cas d'erruer. Elle pointait dans le microcode, sur un code levant une exception en cas d'erreur. Enfin, la PTU fournissait 4 bits pouvant être testés par un branchement dans le microcode. L'un d'entre eux demandait de tester s'il y a un accès hors-limite, les autres étaient assez peu reliés à la protection mémoire.
Un détail est que le chargement du descripteur de segment est réalisé par une fonction dans le microcode. Elle est appliquée pour toutes les instructions ou situations qui demandent de faire un accès mémoire. Et les tests de protection mémoire sont réalisés dans cette fonction, pas après elle. Vu qu'il s'agit d'une fonction exécutée quelque soit l'instruction, le microcode doit transférer le code opération à cette fonction. Le microcode est pour cela associé à un registre interne, dans lequel le code opération est mémorisé, avant d'appeler la fonction. Le microcode a une micro-opération PTSAV (''Protection Save'') pour mémoriser le code opération dans ce registre. Dans la fonction qui charge le descripteur, une micro-opération PTOVRR (''Protection Override'') lit le code opération dans ce registre, et lance les tests nécessaires.
Il faut noter que le PLA était certes plus rapide que de tester les conditions une par une, mais il était assez lent. La PTU mettait environ 3 cycles d'horloges pour rendre son résultat. Le microcode en profitait alors pour exécuter des micro-opérations durant ces 3 cycles d'attente. Par exemple, le microcode pouvait en profiter pour lire l'adresse de base dans le descripteur, si elle n'a pas été chargée avant (les descripteur était chargé en deux fois). Il fallait cependant que les trois micro-opérations soient valides, peu importe qu'il y ait une erreur de protection mémoire ou non. Ou du moins, elles produisaient un résultat qui n'est pas utilisé en cas d'erreur. Si ce n'était pas possible, le microcode ajoutait des NOP pendant ce temps d'attente de 3 cycles.
Le bit A du descripteur de segment indique que le segment a récemment été accédé. Il est mis à jour après les tests de protection mémoire, quand ceux-ci indiquent que l'accès mémoire est autorisé. Le bit A est mis à 1 si la PTU l'autorise. Pour cela, la PTU utilise un des 4 bits de sortie mentionnés plus haut : l'un d'entre eux indique que le bit A doit être mis à 1. La mise à jour est ensuite réalisée par le microcode, qui utilise trois micro-opérations pour le mettre à jour.
====Le ''Hardware task switching'' des CPU x86====
Les systèmes d’exploitation modernes peuvent lancer plusieurs logiciels en même temps. Les logiciels sont alors exécutés à tour de rôle. Passer d'un programme à un autre est ce qui s'appelle une commutation de contexte. Lors d'une commutation de contexte, l'état du processeur est sauvegardé, afin que le programme stoppé puisse reprendre là où il était. Il arrivera un moment où le programme stoppé redémarrera et il doit reprendre dans l'état exact où il s'est arrêté. Deuxièmement, le programme à qui c'est le tour restaure son état. Cela lui permet de revenir là où il était avant d'être stoppé. Il y a donc une sauvegarde et une restauration des registres.
Divers processeurs incorporent des optimisations matérielles pour rendre la commutation de contexte plus rapide. Ils peuvent sauvegarder et restaurer les registres du processeur automatiquement lors d'une interruption de commutation de contexte. Les registres sont sauvegardés dans des structures de données en mémoire RAM, appelées des '''contextes matériels'''. Sur les processeurs x86, il s'agit de la technique d{{'}}''Hardware Task Switching''. Fait intéressant, le ''Hardware Task Switching'' se base beaucoup sur les segments mémoires.
Avec ''Hardware Task Switching'', chaque contexte matériel est mémorisé dans son propre segment mémoire, séparé des autres. Les segments pour les contextes matériels sont appelés des '''''Task State Segment''''' (TSS). Un TSS mémorise tous les registres généraux, le registre d'état, les pointeurs de pile, le ''program counter'' et quelques registres de contrôle du processeur. Par contre, les registres flottants ne sont pas sauvegardés, de même que certaines registres dit SIMD que nous n'avons pas encore abordé. Et c'est un défaut qui fait que le ''Hardware Task Switching'' n'est plus utilisé.
Le programme en cours d'exécution connait l'adresse du TSS qui lui est attribué, car elle est mémorisée dans un registre appelé le '''''Task Register'''''. En plus de pointer sur le TSS, ce registre contient aussi les adresses de base et limite du segment en cours. Pour être plus précis, le ''Task Register'' ne mémorise pas vraiment l'adresse du TSS. À la place, elle mémorise le numéro du segment, le numéro du TSS. Le numéro est codé sur 16 bits, ce qui explique que 65 536 segments sont adressables. Les instructions LDR et STR permettent de lire/écrire ce numéro de segment dans le ''Task Register''.
Le démarrage d'un programme a lieu automatiquement dans plusieurs circonstances. La première est une instruction de branchement CALL ou JMP adéquate. Le branchement fournit non pas une adresse à laquelle brancher, mais un numéro de segment qui pointe vers un TSS. Cela permet à une routine du système d'exploitation de restaurer les registres et de démarrer le programme en une seule instruction de branchement. Une seconde circonstance est une interruption matérielle ou une exception, mais nous la mettons de côté. Le ''Task Register'' est alors initialisé avec le numéro de segment fournit. S'en suit la procédure suivante :
* Le ''Task Register'' est utilisé pour adresser la table des segments, pour récupérer un pointeur vers le TSS associé.
* Le pointeur est utilisé pour une seconde lecture, qui adresse le TSS directement. Celle-ci restaure les registres du processeur.
En clair, on va lire le ''TSS descriptor'' dans la GDT, puis on l'utilise pour restaurer les registres du processeur.
[[File:Hardware Task Switching x86.png|centre|vignette|upright=2|Hardware Task Switching x86]]
===La segmentation sur les processeurs Burrough B5000 et plus===
Le Burrough B5000 est un très vieil ordinateur, commercialisé à partir de l'année 1961. Ses successeurs reprennent globalement la même architecture. C'était une machine à pile, doublé d'une architecture taguée, choses très rare de nos jours. Mais ce qui va nous intéresser dans ce chapitre est que ce processeur incorporait la segmentation, avec cependant une différence de taille : un programme avait accès à un grand nombre de segments. La limite était de 1024 segments par programme ! Il va de soi que des segments plus petits favorise l'implémentation de la mémoire virtuelle, mais complexifie la relocation et le reste, comme nous allons le voir.
Le processeur gère deux types de segments : les segments de données et de procédure/fonction. Les premiers mémorisent un bloc de données, dont le contenu est laissé à l'appréciation du programmeur. Les seconds sont des segments qui contiennent chacun une procédure, une fonction. L'usage des segments est donc différent de ce qu'on a sur les processeurs x86, qui n'avaient qu'un segment unique pour l'intégralité du code machine. Un seul segment de code machine x86 est découpé en un grand nombre de segments de code sur les processeurs Burrough.
La table des segments contenait 1024 entrées de 48 bits chacune. Fait intéressant, chaque entrée de la table des segments pouvait mémoriser non seulement un descripteur de segment, mais aussi une valeur flottante ou d'autres types de données ! Parler de table des segments est donc quelque peu trompeur, car cette table ne gère pas que des segments, mais aussi des données. La documentation appelaiat cette table la '''''Program Reference Table''''', ou PRT.
La raison de ce choix quelque peu bizarre est que les instructions ne gèrent pas d'adresses proprement dit. Tous les accès mémoire à des données en-dehors de la pile passent par la segmentation, ils précisent tous un indice de segment et un ''offset''. Pour éviter d'allouer un segment pour chaque donnée, les concepteurs du processeur ont décidé qu'une entrée pouvait contenir directement la donnée entière à lire/écrire.
La PRT supporte trois types de segments/descripteurs : les descripteurs de données, les descripteurs de programme et les descripteurs d'entrées-sorties. Les premiers décrivent des segments de données. Les seconds sont associés aux segments de procédure/fonction et sont utilisés pour les appels de fonction (qui passent, eux aussi, par la segmentation). Le dernier type de descripteurs sert pour les appels systèmes et les communications avec l'OS ou les périphériques.
Chaque entrée de la PRT contient un ''tag'', une suite de bit qui indique le type de l'entrée : est-ce qu'elle contient un descripteur de segment, une donnée, autre. Les descripteurs contiennent aussi un ''bit de présence'' qui indique si le segment a été swappé ou non. Car oui, les segments pouvaient être swappés sur ce processeur, ce qui n'est pas étonnant vu que les segments sont plus petits sur cette architecture. Le descripteur contient aussi l'adresse de base du segment ainsi que sa taille, et diverses informations pour le retrouver sur le disque dur s'il est swappé.
: L'adresse mémorisée ne faisait que 15 bits, ce qui permettait d'adresse 32 kibi-mots, soit 192 kibioctets de mémoire. Diverses techniques d'extension d'adressage étaient disponibles pour contourner cette limitation. Outre l'usage de l{{'}}''overlay'', le processeur et l'OS géraient aussi des identifiants d'espace d'adressage et en fournissaient plusieurs par processus. Les processeurs Borrough suivants utilisaient des adresses plus grandes, de 20 bits, ce qui tempérait le problème.
[[File:B6700Word.jpg|centre|vignette|upright=2|Structure d'un mot mémoire sur le B6700.]]
==Les architectures à capacités==
Les architectures à capacité utilisent la segmentation à granularité fine, mais ajoutent des mécanismes de protection mémoire assez particuliers, qui font que les architectures à capacité se démarquent du reste. Les architectures de ce type sont très rares et sont des processeurs assez anciens. Le premier d'entre eux était le Plessey System 250, qui date de 1969. Il fu suivi par le CAP computer, vendu entre les années 70 et 77. En 1978, le System/38 d'IBM a eu un petit succès commercial. En 1980, la Flex machine a aussi été vendue, mais à très peu d'examplaires, comme les autres architectures à capacité. Et enfin, en 1981, l'architecture à capacité la plus connue, l'Intel iAPX 432 a été commercialisée. Depuis, la seule architecture de ce type est en cours de développement. Il s'agit de l'architecture CHERI, dont la mise en projet date de 2014.
===Le partage de la mémoire sur les architectures à capacités===
Le partage de segment est grandement modifié sur les architectures à capacité. Avec la segmentation normale, il y a une table de segment par processus. Les conséquences sont assez nombreuses, mais la principale est que partager un segment entre plusieurs processus est compliqué. Les défauts ont été évoqués plus haut. Les sélecteurs de segments ne sont pas les mêmes d'un processus à l'autre, pour un même segment. De plus, les adresses limite et de base sont dupliquées dans plusieurs tables de segments, et cela peut causer des problèmes de sécurité si une table des segments est modifiée et pas l'autre. Et il y a d'autres problèmes, tout aussi importants.
[[File:Partage des segments avec la segmentation.png|centre|vignette|upright=1.5|Partage des segments avec la segmentation]]
À l'opposé, les architectures à capacité utilisent une table des segments unique pour tous les processus. La table des segments unique sera appelée dans de ce qui suit la '''table des segments globale''', ou encore la table globale. En conséquence, les adresses de base et limite ne sont présentes qu'en un seul exemplaire par segment, au lieu d'être dupliquées dans autant de processus que nécessaire. De plus, cela garantit que l'indice de segment est le même quel que soit le processus qui l'utilise.
Un défaut de cette approche est au niveau des droits d'accès. Avec la segmentation normale, les droits d'accès pour un segment sont censés changer d'un processus à l'autre. Par exemple, tel processus a accès en lecture seule au segment, l'autre seulement en écriture, etc. Mais ici, avec une table des segments uniques, cela ne marche plus : incorporer les droits d'accès dans la table des segments ferait que tous les processus auraient les mêmes droits d'accès au segment. Et il faut trouver une solution.
===Les capacités sont des pointeurs protégés===
Pour éviter cela, les droits d'accès sont combinés avec les sélecteurs de segments. Les sélecteurs des segments sont remplacés par des '''capacités''', des pointeurs particuliers formés en concaténant l'indice de segment avec les droits d'accès à ce segment. Si un programme veut accéder à une adresse, il fournit une capacité de la forme "sélecteur:droits d'accès", et un décalage qui indique la position de l'adresse dans le segment.
Il est impossible d'accéder à un segment sans avoir la capacité associée, c'est là une sécurité importante. Un accès mémoire demande que l'on ait la capacité pour sélectionner le bon segment, mais aussi que les droits d'accès en permettent l'accès demandé. Par contre, les capacités peuvent être passées d'un programme à un autre sans problème, les deux programmes pourront accéder à un segment tant qu'ils disposent de la capacité associée.
[[File:Comparaison entre capacités et adresses segmentées.png|centre|vignette|upright=2.5|Comparaison entre capacités et adresses segmentées]]
Mais cette solution a deux problèmes très liés. Au niveau des sélecteurs de segment, le problème est que les sélecteur ont une portée globale. Avant, l'indice de segment était interne à un programme, un sélecteur ne permettait pas d'accéder au segment d'un autre programme. Sur les architectures à capacité, les sélecteurs ont une portée globale. Si un programme arrive à forger un sélecteur qui pointe vers un segment d'un autre programme, il peut théoriquement y accéder, à condition que les droits d'accès le permettent. Et c'est là qu'intervient le second problème : les droits d'accès ne sont plus protégés par l'espace noyau. Les droits d'accès étaient dans la table de segment, accessible uniquement en espace noyau, ce qui empêchait un processus de les modifier. Avec une capacité, il faut ajouter des mécanismes de protection qui empêchent un programme de modifier les droits d'accès à un segment et de générer un indice de segment non-prévu.
La première sécurité est qu'un programme ne peut pas créer une capacité, seul le système d'exploitation le peut. Les capacités sont forgées lors de l'allocation mémoire, ce qui est du ressort de l'OS. Pour rappel, un programme qui veut du rab de mémoire RAM peut demander au système d'exploitation de lui allouer de la mémoire supplémentaire. Le système d'exploitation renvoie alors un pointeurs qui pointe vers un nouveau segment. Le pointeur est une capacité. Il doit être impossible de forger une capacité, en-dehors d'une demande d'allocation mémoire effectuée par l'OS. Typiquement, la forge d'une capacité se fait avec des instructions du processeur, que seul l'OS peut éxecuter (pensez à une instruction qui n'est accessible qu'en espace noyau).
La seconde protection est que les capacités ne peuvent pas être modifiées sans raison valable, que ce soit pour l'indice de segment ou les droits d'accès. L'indice de segment ne peut pas être modifié, quelqu'en soit la raison. Pour les droits d'accès, la situation est plus compliquée. Il est possible de modifier ses droits d'accès, mais sous conditions. Réduire les droits d'accès d'une capacité est possible, que ce soit en espace noyau ou utilisateur, pas l'OS ou un programme utilisateur, avec une instruction dédiée. Mais augmenter les droits d'accès, seul l'OS peut le faire avec une instruction précise, souvent exécutable seulement en espace noyau.
Les capacités peuvent être copiées, et même transférées d'un processus à un autre. Les capacités peuvent être détruites, ce qui permet de libérer la mémoire utilisée par un segment. La copie d'une capacité est contrôlée par l'OS et ne peut se faire que sous conditions. La destruction d'une capacité est par contre possible par tous les processus. La destruction ne signifie pas que le segment est effacé, il est possible que d'autres processus utilisent encore des copies de la capacité, et donc le segment associé. On verra quand la mémoire est libérée plus bas.
Protéger les capacités demande plusieurs conditions. Premièrement, le processeur doit faire la distinction entre une capacité et une donnée. Deuxièmement, les capacités ne peuvent être modifiées que par des instructions spécifiques, dont l'exécution est protégée, réservée au noyau. En clair, il doit y avoir une séparation matérielle des capacités, qui sont placées dans des registres séparés. Pour cela, deux solutions sont possibles : soit les capacités remplacent les adresses et sont dispersées en mémoire, soit elles sont regroupées dans un segment protégé.
====La liste des capacités====
Avec la première solution, on regroupe les capacités dans un segment protégé. Chaque programme a accès à un certain nombre de segments et à autant de capacités. Les capacités d'un programme sont souvent regroupées dans une '''liste de capacités''', appelée la '''''C-list'''''. Elle est généralement placée en mémoire RAM. Elle est ce qu'il reste de la table des segments du processus, sauf que cette table ne contient pas les adresses du segment, qui sont dans la table globale. Tout se passe comme si la table des segments de chaque processus est donc scindée en deux : la table globale partagée entre tous les processus contient les informations sur les limites des segments, la ''C-list'' mémorise les droits d'accès et les sélecteurs pour identifier chaque segment. C'est un niveau d'indirection supplémentaire par rapport à la segmentation usuelle.
[[File:Architectures à capacité.png|centre|vignette|upright=2|Architectures à capacité]]
La liste de capacité est lisible par le programme, qui peut copier librement les capacités dans les registres. Par contre, la liste des capacités est protégée en écriture. Pour le programme, il est impossible de modifier les capacités dedans, impossible d'en rajouter, d'en forger, d'en retirer. De même, il ne peut pas accéder aux segments des autres programmes : il n'a pas les capacités pour adresser ces segments.
Pour protéger la ''C-list'' en écriture, la solution la plus utilisée consiste à placer la ''C-list'' dans un segment dédié. Le processeur gère donc plusieurs types de segments : les segments de capacité pour les ''C-list'', les autres types segments pour le reste. Un défaut de cette approche est que les adresses/capacités sont séparées des données. Or, les programmeurs mixent souvent adresses et données, notamment quand ils doivent manipuler des structures de données comme des listes chainées, des arbres, des graphes, etc.
L'usage d'une ''C-list'' permet de se passer de la séparation entre espace noyau et utilisateur ! Les segments de capacité sont eux-mêmes adressés par leur propre capacité, avec une capacité par segment de capacité. Le programme a accès à la liste de capacité, comme l'OS, mais leurs droits d'accès ne sont pas les mêmes. Le programme a une capacité vers la ''C-list'' qui n'autorise pas l'écriture, l'OS a une autre capacité qui accepte l'écriture. Les programmes ne pourront pas forger les capacités permettant de modifier les segments de capacité. Une méthode alternative est de ne permettre l'accès aux segments de capacité qu'en espace noyau, mais elle est redondante avec la méthode précédente et moins puissante.
====Les capacités dispersées, les architectures taguées====
Une solution alternative laisse les capacités dispersées en mémoire. Les capacités remplacent les adresses/pointeurs, et elles se trouvent aux mêmes endroits : sur la pile, dans le tas. Comme c'est le cas dans les programmes modernes, chaque allocation mémoire renvoie une capacité, que le programme gére comme il veut. Il peut les mettre dans des structures de données, les placer sur la pile, dans des variables en mémoire, etc. Mais il faut alors distinguer si un mot mémoire contient une capacité ou une autre donnée, les deux ne devant pas être mixés.
Pour cela, chaque mot mémoire se voit attribuer un certain bit qui indique s'il s'agit d'un pointeur/capacité ou d'autre chose. Mais cela demande un support matériel, ce qui fait que le processeur devient ce qu'on appelle une ''architecture à tags'', ou ''tagged architectures''. Ici, elles indiquent si le mot mémoire contient une adresse:capacité ou une donnée.
[[File:Architectures à capacité sans liste de capacité.png|centre|vignette|upright=2|Architectures à capacité sans liste de capacité]]
L'inconvénient est le cout en matériel de cette solution. Il faut ajouter un bit à chaque case mémoire, le processeur doit vérifier les tags avant chaque opération d'accès mémoire, etc. De plus, tous les mots mémoire ont la même taille, ce qui force les capacités à avoir la même taille qu'un entier. Ce qui est compliqué.
===Les registres de capacité===
Les architectures à capacité disposent de registres spécialisés pour les capacités, séparés pour les entiers. La raison principale est une question de sécurité, mais aussi une solution pragmatique au fait que capacités et entiers n'ont pas la même taille. Les registres dédiés aux capacités ne mémorisent pas toujours des capacités proprement dites. À la place, ils mémorisent des descripteurs de segment, qui contiennent l'adresse de base, limite et les droits d'accès. Ils sont utilisés pour la relocation des accès mémoire ultérieurs. Ils sont en réalité identiques aux registres de relocation, voire aux registres de segments. Leur utilité est d'accélérer la relocation, entre autres.
Les processeurs à capacité ne gèrent pas d'adresses proprement dit, comme pour la segmentation avec plusieurs registres de relocation. Les accès mémoire doivent préciser deux choses : à quel segment on veut accéder, à quelle position dans le segment se trouve la donnée accédée. La première information se trouve dans le mal nommé "registre de capacité", la seconde information est fournie par l'instruction d'accès mémoire soit dans un registre (Base+Index), soit en adressage base+''offset''.
Les registres de capacités sont accessibles à travers des instructions spécialisées. Le processeur ajoute des instructions LOAD/STORE pour les échanges entre table des segments et registres de capacité. Ces instructions sont disponibles en espace utilisateur, pas seulement en espace noyau. Lors du chargement d'une capacité dans ces registres, le processeur vérifie que la capacité chargée est valide, et que les droits d'accès sont corrects. Puis, il accède à la table des segments, récupère les adresses de base et limite, et les mémorise dans le registre de capacité. Les droits d'accès et d'autres méta-données sont aussi mémorisées dans le registre de capacité. En somme, l'instruction de chargement prend une capacité et charge un descripteur de segment dans le registre.
Avec ce genre de mécanismes, il devient difficile d’exécuter certains types d'attaques, ce qui est un gage de sureté de fonctionnement indéniable. Du moins, c'est la théorie, car tout repose sur l'intégrité des listes de capacité. Si on peut modifier celles-ci, alors il devient facile de pouvoir accéder à des objets auxquels on n’aurait pas eu droit.
===Le recyclage de mémoire matériel===
Les architectures à capacité séparent les adresses/capacités des nombres entiers. Et cela facilite grandement l'implémentation de la ''garbage collection'', ou '''recyclage de la mémoire''', à savoir un ensemble de techniques logicielles qui visent à libérer la mémoire inutilisée.
Rappelons que les programmes peuvent demander à l'OS un rab de mémoire pour y placer quelque chose, généralement une structure de donnée ou un objet. Mais il arrive un moment où cet objet n'est plus utilisé par le programme. Il peut alors demander à l'OS de libérer la portion de mémoire réservée. Sur les architectures à capacité, cela revient à libérer un segment, devenu inutile. La mémoire utilisée par ce segment est alors considérée comme libre, et peut être utilisée pour autre chose. Mais il arrive que les programmes ne libèrent pas le segment en question. Soit parce que le programmeur a mal codé son programme, soit parce que le compilateur n'a pas fait du bon travail ou pour d'autres raisons.
Pour éviter cela, les langages de programmation actuels incorporent des '''''garbage collectors''''', des morceaux de code qui scannent la mémoire et détectent les segments inutiles. Pour cela, ils doivent identifier les adresses manipulées par le programme. Si une adresse pointe vers un objet, alors celui-ci est accessible, il sera potentiellement utilisé dans le futur. Mais si aucune adresse ne pointe vers l'objet, alors il est inaccessible et ne sera plus jamais utilisé dans le futur. On peut libérer les objets inaccessibles.
Identifier les adresses est cependant très compliqué sur les architectures normales. Sur les processeurs modernes, les ''garbage collectors'' scannent la pile à la recherche des adresses, et considèrent tout mot mémoire comme une adresse potentielle. Mais les architectures à capacité rendent le recyclage de la mémoire très facile. Un segment est accessible si le programme dispose d'une capacité qui pointe vers ce segment, rien de plus. Et les capacités sont facilement identifiables : soit elles sont dans la liste des capacités, soit on peut les identifier à partir de leur ''tag''.
Le recyclage de mémoire était parfois implémenté directement en matériel. En soi, son implémentation est assez simple, et peu être réalisé dans le microcode d'un processeur. Une autre solution consiste à utiliser un second processeur, spécialement dédié au recyclage de mémoire, qui exécute un programme spécialement codé pour. Le programme en question est placé dans une mémoire ROM, reliée directement à ce second processeur.
===L'intel iAPX 432===
Voyons maintenat une architecture à capacité assez connue : l'Intel iAPX 432. Oui, vous avez bien lu : Intel a bel et bien réalisé un processeur orienté objet dans sa jeunesse. La conception du processeur Intel iAPX 432 commença en 1975, afin de créer un successeur digne de ce nom aux processeurs 8008 et 8080.
La conception du processeur Intel iAPX 432 commença en 1975, afin de créer un successeur digne de ce nom aux processeurs 8008 et 8080. Ce processeur s'est très faiblement vendu en raison de ses performances assez désastreuses et de défauts techniques certains. Par exemple, ce processeur était une machine à pile à une époque où celles-ci étaient tombées en désuétude, il ne pouvait pas effectuer directement de calculs avec des constantes entières autres que 0 et 1, ses instructions avaient un alignement bizarre (elles étaient bit-alignées). Il avait été conçu pour maximiser la compatibilité avec le langage ADA, un langage assez peu utilisé, sans compter que le compilateur pour ce processeur était mauvais.
====Les segments prédéfinis de l'Intel iAPX 432====
L'Intel iAPX432 gère plusieurs types de segments. Rien d'étonnant à cela, les Burrough géraient eux aussi plusieurs types de segments, à savoir des segments de programmes, des segments de données, et des segments d'I/O. C'est la même chose sur l'Intel iAPX 432, mais en bien pire !
Les segments de données sont des segments génériques, dans lequels on peut mettre ce qu'on veut, suivant les besoins du programmeur. Ils sont tous découpés en deux parties de tailles égales : une partie contenant les données de l'objet et une partie pour les capacités. Les capacités d'un segment pointent vers d'autres segments, ce qui permet de créer des structures de données assez complexes. La ligne de démarcation peut être placée n'importe où dans le segment, les deux portions ne sont pas de taille identique, elles ont des tailles qui varient de segment en segment. Il est même possible de réserver le segment entier à des données sans y mettre de capacités, ou inversement. Les capacités et données sont adressées à partir de la ligne de démarcation, qui sert d'adresse de base du segment. Suivant l'instruction utilisée, le processeur accède à la bonne portion du segment.
Le processeur supporte aussi d'autres segments pré-définis, qui sont surtout utilisés par le système d'exploitation :
* Des segments d'instructions, qui contiennent du code exécutable, typiquement un programme ou des fonctions, parfois des ''threads''.
* Des segments de processus, qui mémorisent des processus entiers. Ces segments contiennent des capacités qui pointent vers d'autres segments, notamment un ou plusieurs segments de code, et des segments de données.
* Des segments de domaine, pour les modules ou bibliothèques dynamiques.
* Des segments de contexte, utilisés pour mémoriser l'état d'un processus, utilisés par l'OS pour faire de la commutation de contexte.
* Des segments de message, utilisés pour la communication entre processus par l'intermédiaire de messages.
* Et bien d'autres encores.
Sur l'Intel iAPX 432, chaque processus est considéré comme un objet à part entière, qui a son propre segment de processus. De même, l'état du processeur (le programme qu'il est en train d’exécuter, son état, etc.) est stocké en mémoire dans un segment de contexte. Il en est de même pour chaque fonction présente en mémoire : elle était encapsulée dans un segment, sur lequel seules quelques manipulations étaient possibles (l’exécuter, notamment). Et ne parlons pas des appels de fonctions qui stockaient l'état de l'appelé directement dans un objet spécial. Bref, de nombreux objets système sont prédéfinis par le processeur : les objets stockant des fonctions, les objets stockant des processus, etc.
L'Intel 432 possédait dans ses circuits un ''garbage collector'' matériel. Pour faciliter son fonctionnement, certains bits de l'objet permettaient de savoir si l'objet en question pouvait être supprimé ou non.
====Le support de la segmentation sur l'Intel iAPX 432====
La table des segments est une table hiérarchique, à deux niveaux. Le premier niveau est une ''Object Table Directory'', qui réside toujours en mémoire RAM. Elle contient des descripteurs qui pointent vers des tables secondaires, appelées des ''Object Table''. Il y a plusieurs ''Object Table'', typiquement une par processus. Plusieurs processus peuvent partager la même ''Object Table''. Les ''Object Table'' peuvent être swappées, mais pas l{{'}}''Object Table Directory''.
Une capacité tient compte de l'organisation hiérarchique de la table des segments. Elle contient un indice qui précise quelle ''Object Table'' utiliser, et l'indice du segment dans cette ''Object Table''. Le premier indice adresse l{{'}}''Object Table Directory'' et récupère un descripteur de segment qui pointe sur la bonne ''Object Table''. Le second indice est alors utilisé pour lire l'adresse de base adéquate dans cette ''Object Table''. La capacité contient aussi des droits d'accès en lecture, écriture, suppression et copie. Il y a aussi un champ pour le type, qu'on verra plus bas. Au fait : les capacités étaient appelées des ''Access Descriptors'' dans la documentation officielle.
Une capacité fait 32 bits, avec un octet utilisé pour les droits d'accès, laissant 24 bits pour adresser les segments. Le processeur gérait jusqu'à 2^24 segments/objets différents, pouvant mesurer jusqu'à 64 kibioctets chacun, ce qui fait 2^40 adresses différentes, soit 1024 gibioctets. Les 24 bits pour adresser les segments sont partagés moitié-moitié pour l'adressage des tables, ce qui fait 4096 ''Object Table'' différentes dans l{{'}}''Object Table Directory'', et chaque ''Object Table'' contient 4096 segments.
====Le jeu d'instruction de l'Intel iAPX 432====
L'Intel iAPX 432 est une machine à pile. Le jeu d'instruction de l'Intel iAPX 432 gère pas moins de 230 instructions différentes. Il gére deux types d'instructions : les instructions normales, et celles qui manipulent des segments/objets. Les premières permettent de manipuler des nombres entiers, des caractères, des chaînes de caractères, des tableaux, etc.
Les secondes sont spécialement dédiées à la manipulation des capacités. Il y a une instruction pour copier une capacité, une autre pour invalider une capacité, une autre pour augmenter ses droits d'accès (instruction sécurisée, exécutable seulement sous certaines conditions), une autre pour restreindre ses droits d'accès. deux autres instructions créent un segment et renvoient la capacité associée, la première créant un segment typé, l'autre non.
le processeur gérait aussi des instructions spécialement dédiées à la programmation système et idéales pour programmer des systèmes d'exploitation. De nombreuses instructions permettaient ainsi de commuter des processus, faire des transferts de messages entre processus, etc. Environ 40 % du micro-code était ainsi spécialement dédié à ces instructions spéciales.
Les instructions sont de longueur variable et peuvent prendre n'importe quelle taille comprise entre 10 et 300 bits, sans vraiment de restriction de taille. Les bits d'une instruction sont regroupés en 4 grands blocs, 4 champs, qui ont chacun une signification particulière.
* Le premier est l'opcode de l'instruction.
* Le champ référence, doit être interprété différemment suivant la donnée à manipuler. Si cette donnée est un entier, un caractère ou un flottant, ce champ indique l'emplacement de la donnée en mémoire. Alors que si l'instruction manipule un objet, ce champ spécifie la capacité de l'objet en question. Ce champ est assez complexe et il est sacrément bien organisé.
* Le champ format, n'utilise que 4 bits et a pour but de préciser si les données à manipuler sont en mémoire ou sur la pile.
* Le champ classe permet de dire combien de données différentes l'instruction va devoir manipuler, et quelles seront leurs tailles.
[[File:Encodage des instructions de l'Intel iAPX-432.png|centre|vignette|upright=2|Encodage des instructions de l'Intel iAPX-432.]]
====Le support de l'orienté objet sur l'Intel iAPX 432====
L'Intel 432 permet de définir des objets, qui correspondent aux classes des langages orientés objets. L'Intel 432 permet, à partir de fonctions définies par le programmeur, de créer des '''''domain objects''''', qui correspondent à une classe. Un ''domain object'' est un segment de capacité, dont les capacités pointent vers des fonctions ou un/plusieurs objets. Les fonctions et les objets sont chacun placés dans un segment. Une partie des fonctions/objets sont publics, ce qui signifie qu'ils sont accessibles en lecture par l'extérieur. Les autres sont privées, inaccessibles aussi bien en lecture qu'en écriture.
L'exécution d'une fonction demande que le branchement fournisse deux choses : une capacité vers le ''domain object'', et la position de la fonction à exécuter dans le segment. La position permet de localiser la capacité de la fonction à exécuter. En clair, on accède au ''domain object'' d'abord, pour récupérer la capacité qui pointe vers la fonction à exécuter.
Il est aussi possible pour le programmeur de définir de nouveaux types non supportés par le processeur, en faisant appel au système d'exploitation de l'ordinateur. Au niveau du processeur, chaque objet est typé au niveau de son object descriptor : celui-ci contient des informations qui permettent de déterminer le type de l'objet. Chaque type se voit attribuer un domain object qui contient toutes les fonctions capables de manipuler les objets de ce type et que l'on appelle le type manager. Lorsque l'on veut manipuler un objet d'un certain type, il suffit d'accéder à une capacité spéciale (le TCO) qui pointera dans ce type manager et qui précisera quel est l'objet à manipuler (en sélectionnant la bonne entrée dans la liste de capacité). Le type d'un objet prédéfini par le processeur est ainsi spécifié par une suite de 8 bits, tandis que le type d'un objet défini par le programmeur est défini par la capacité spéciale pointant vers son type manager.
===Conclusion===
Pour ceux qui veulent en savoir plus, je conseille la lecture de ce livre, disponible gratuitement sur internet (merci à l'auteur pour cette mise à disposition) :
* [https://homes.cs.washington.edu/~levy/capabook/ Capability-Based Computer Systems].
Voici un document qui décrit le fonctionnement de l'Intel iAPX432 :
* [https://homes.cs.washington.edu/~levy/capabook/Chapter9.pdf The Intel iAPX 432 ]
==La pagination==
Avec la pagination, la mémoire est découpée en blocs de taille fixe, appelés des '''pages mémoires'''. La taille des pages varie suivant le processeur et le système d'exploitation et tourne souvent autour de 4 kibioctets. Mais elles sont de taille fixe : on ne peut pas en changer la taille. C'est la différence avec les segments, qui sont de taille variable. Le contenu d'une page en mémoire fictive est rigoureusement le même que le contenu de la page correspondante en mémoire physique.
L'espace d'adressage est découpé en '''pages logiques''', alors que la mémoire physique est découpée en '''pages physique''' de même taille. Les pages logiques correspondent soit à une page physique, soit à une page swappée sur le disque dur. Quand une page logique est associée à une page physique, les deux ont le même contenu, mais pas les mêmes adresses. Les pages logiques sont numérotées, en partant de 0, afin de pouvoir les identifier/sélectionner. Même chose pour les pages physiques, qui sont elles aussi numérotées en partant de 0.
[[File:Principe de la pagination.png|centre|vignette|upright=2|Principe de la pagination.]]
Pour information, le tout premier processeur avec un système de mémoire virtuelle était le super-ordinateur Atlas. Il utilisait la pagination, et non la segmentation. Mais il fallu du temps avant que la méthode de la pagination prenne son essor dans les processeurs commerciaux x86.
Un point important est que la pagination implique une coopération entre OS et hardware, les deux étant fortement mélés. Une partie des informations de cette section auraient tout autant leur place dans le wikilivre sur les systèmes d'exploitation, mais il est plus simple d'en parler ici.
===La mémoire virtuelle : le ''swapping'' et le remplacement des pages mémoires===
Le système d'exploitation mémorise des informations sur toutes les pages existantes dans une '''table des pages'''. C'est un tableau où chaque ligne est associée à une page logique. Une ligne contient un bit ''Valid'' qui indique si la page logique associée est swappée sur le disque dur ou non, et la position de la page physique correspondante en mémoire RAM. Elle peut aussi contenir des bits pour la protection mémoire, et bien d'autres. Les lignes sont aussi appelées des ''entrées de la table des pages''
[[File:Gestionnaire de mémoire virtuelle - Pagination et swapping.png|centre|vignette|upright=2|Table des pages.]]
De plus, le système d'exploitation conserve une '''liste des pages vides'''. Le nom est assez clair : c'est une liste de toutes les pages de la mémoire physique qui sont inutilisées, qui ne sont allouées à aucun processus. Ces pages sont de la mémoire libre, utilisable à volonté. La liste des pages vides est mise à jour à chaque fois qu'un programme réserve de la mémoire, des pages sont alors prises dans cette liste et sont allouées au programme demandeur.
====Les défauts de page====
Lorsque l'on veut traduire l'adresse logique d'une page mémoire, le processeur vérifie le bit ''Valid'' et l'adresse physique. Si le bit ''Valid'' est à 1 et que l'adresse physique est présente, la traduction d'adresse s'effectue normalement. Mais si ce n'est pas le cas, l'entrée de la table des pages ne contient pas de quoi faire la traduction d'adresse. Soit parce que la page est swappée sur le disque dur et qu'il faut la copier en RAM, soit parce que les droits d'accès ne le permettent pas, soit parce que la page n'a pas encore été allouée, etc. On fait alors face à un '''défaut de page'''. Un défaut de page a lieu quand la MMU ne peut pas associer l'adresse logique à une adresse physique, quelque qu'en soit la raison.
Il existe deux types de défauts de page : mineurs et majeurs. Un '''défaut de page majeur''' a lieu quand on veut accéder à une page déplacée sur le disque dur. Un défaut de page majeur lève une exception matérielle dont la routine rapatriera la page en mémoire RAM. S'il y a de la place en mémoire RAM, il suffit d'allouer une page vide et d'y copier la page chargée depuis le disque dur. Mais si ce n'est par le cas, on va devoir faire de la place en RAM en déplaçant une page mémoire de la RAM vers le disque dur. Dans tous les cas, c'est le système d'exploitation qui s'occupe du chargement de la page, le processeur n'est pas impliqué. Une fois la page chargée, la table des pages est mise à jour et la traduction d'adresse peut recommencer. Si je dis recommencer, c'est car l'accès mémoire initial est rejoué à l'identique, sauf que la traduction d'adresse réussit cette fois-ci.
Un '''défaut de page mineur''' a lieu dans des circonstances pas très intuitives : la page est en mémoire physique, mais l'adresse physique de la page n'est pas accessible. Par exemple, il est possible que des sécurités empêchent de faire la traduction d'adresse, pour des raisons de protection mémoire. Une autre raison est la gestion des adresses synonymes, qui surviennent quand on utilise des libraires partagées entre programmes, de la communication inter-processus, des optimisations de type ''copy-on-write'', etc. Enfin, une dernière raison est que la page a été allouée à un programme par le système d'exploitation, mais qu'il n'a pas encore attribué sa position en mémoire. Pour comprendre comment c'est possible, parlons rapidement de l'allocation paresseuse.
Imaginons qu'un programme fasse une demande d'allocation mémoire et se voit donc attribuer une ou plusieurs pages logiques. L'OS peut alors réagir de deux manières différentes. La première est d'attribuer une page physique immédiatement, en même temps que la page logique. En faisant ainsi, on ne peut pas avoir de défaut mineur, sauf en cas de problème de protection mémoire. Cette solution est simple, on l'appelle l{{'}}'''allocation immédiate'''. Une autre solution consiste à attribuer une page logique, mais l'allocation de la page physique se fait plus tard. Elle a lieu la première fois que le programme tente d'écrire/lire dans la page physique. Un défaut mineur a lieu, et c'est lui qui force l'OS à attribuer une page physique pour la page logique demandée. On parle alors d{{'}}'''allocation paresseuse'''. L'avantage est que l'on gagne en performance si des pages logiques sont allouées mais utilisées, ce qui peut arriver.
Une optimisation permise par l'existence des défauts mineurs est le '''''copy-on-write'''''. Le but est d'optimiser la copie d'une page logique dans une autre. L'idée est que la copie est retardée quand elle est vraiment nécessaire, à savoir quand on écrit dans la copie. Tant que l'on ne modifie pas la copie, les deux pages logiques, originelle et copiée, pointent vers la même page physique. A quoi bon avoir deux copies avec le même contenu ? Par contre, la page physique est marquée en lecture seule. La moindre écriture déclenche une erreur de protection mémoire, et un défaut mineur. Celui-ci est géré par l'OS, qui effectue alors la copie dans une nouvelle page physique.
Je viens de dire que le système d'exploitation gère les défauts de page majeurs/mineurs. Un défaut de page déclenche une exception matérielle, qui passe la main au système d'exploitation. Le système d'exploitation doit alors déterminer ce qui a levé l'exception, notamment identifier si c'est un défaut de page mineur ou majeur. Pour cela, le processeur a un ou plusieurs '''registres de statut''' qui indique l'état du processeur, qui sont utiles pour gérer les défauts de page. Ils indiquent quelle est l'adresse fautive, si l'accès était une lecture ou écriture, si l'accès a eu lieu en espace noyau ou utilisateur (les espaces mémoire ne sont pas les mêmes), etc. Les registres en question varient grandement d'une architecture de processeur à l'autre, aussi on ne peut pas dire grand chose de plus sur le sujet. Le reste est de toute façon à voir dans un cours sur les systèmes d'exploitation.
====Le remplacement des pages====
Les pages virtuelles font référence soit à une page en mémoire physique, soit à une page sur le disque dur. Mais l'on ne peut pas lire une page directement depuis le disque dur. Les pages sur le disque dur doivent être chargées en RAM, avant d'être utilisables. Ce n'est possible que si on a une page mémoire vide, libre. Si ce n'est pas le cas, on doit faire de la place en swappant une page sur le disque dur. Les pages font ainsi une sorte de va et vient entre le fichier d'échange et la RAM, suivant les besoins. Tout cela est effectué par une routine d'interruption du système d'exploitation, le processeur n'ayant pas vraiment de rôle là-dedans.
Supposons que l'on veuille faire de la place en RAM pour une nouvelle page. Dans une implémentation naïve, on trouve une page à évincer de la mémoire, qui est copiée dans le ''swapfile''. Toutes les pages évincées sont alors copiées sur le disque dur, à chaque remplacement. Néanmoins, cette implémentation naïve peut cependant être améliorée si on tient compte d'un point important : si la page a été modifiée depuis le dernier accès. Si le programme/processeur a écrit dans la page, alors celle-ci a été modifiée et doit être sauvegardée sur le ''swapfile'' si elle est évincée. Par contre, si ce n'est pas le cas, la page est soit initialisée, soit déjà présente à l'identique dans le ''swapfile''.
Mais cette optimisation demande de savoir si une écriture a eu lieu dans la page. Pour cela, on ajoute un '''''dirty bit''''' à chaque entrée de la table des pages, juste à côté du bit ''Valid''. Il indique si une écriture a eu lieu dans la page depuis qu'elle a été chargée en RAM. Ce bit est mis à jour par le processeur, automatiquement, lors d'une écriture. Par contre, il est remis à zéro par le système d'exploitation, quand la page est chargée en RAM. Si le programme se voit allouer de la mémoire, il reçoit une page vide, et ce bit est initialisé à 0. Il est mis à 1 si la mémoire est utilisée. Quand la page est ensuite swappée sur le disque dur, ce bit est remis à 0 après la sauvegarde.
Sur la majorité des systèmes d'exploitation, il est possible d'interdire le déplacement de certaines pages sur le disque dur. Ces pages restent alors en mémoire RAM durant un temps plus ou moins long, parfois en permanence. Cette possibilité simplifie la vie des programmeurs qui conçoivent des systèmes d'exploitation : essayez d'exécuter l'interruption pour les défauts de page alors que la page contenant le code de l'interruption est placée sur le disque dur ! Là encore, cela demande d'ajouter un bit dans chaque entrée de la table des pages, qui indique si la page est swappable ou non. Le bit en question s'appelle souvent le '''bit ''swappable'''''.
====Les algorithmes de remplacement des pages pris en charge par l'OS====
Le choix de la page doit être fait avec le plus grand soin et il existe différents algorithmes qui permettent de décider quelle page supprimer de la RAM. Leur but est de swapper des pages qui ne seront pas accédées dans le futur, pour éviter d'avoir à faire triop de va-et-vient entre RAM et ''swapfile''. Les données qui sont censées être accédées dans le futur doivent rester en RAM et ne pas être swappées, autant que possible. Les algorithmes les plus simples pour le choix de page à évincer sont les suivants.
Le plus simple est un algorithme aléatoire : on choisit la page au hasard. Mine de rien, cet algorithme est très simple à implémenter et très rapide à exécuter. Il ne demande pas de modifier la table des pages, ni même d'accéder à celle-ci pour faire son choix. Ses performances sont surprenamment correctes, bien que largement en-dessous de tous les autres algorithmes.
L'algorithme FIFO supprime la donnée qui a été chargée dans la mémoire avant toutes les autres. Cet algorithme fonctionne bien quand un programme manipule des tableaux de grande taille, mais fonctionne assez mal dans le cas général.
L'algorithme LRU supprime la donnée qui été lue ou écrite pour la dernière fois avant toutes les autres. C'est théoriquement le plus efficace dans la majorité des situations. Malheureusement, son implémentation est assez complexe et les OS doivent modifier la table des pages pour l'implémenter.
L'algorithme le plus utilisé de nos jours est l{{'}}'''algorithme NRU''' (''Not Recently Used''), une simplification drastique du LRU. Il fait la différence entre les pages accédées il y a longtemps et celles accédées récemment, d'une manière très binaire. Les deux types de page sont appelés respectivement les '''pages froides''' et les '''pages chaudes'''. L'OS swappe en priorité les pages froides et ne swappe de page chaude que si aucune page froide n'est présente. L'algorithme est simple : il choisit la page à évincer au hasard parmi une page froide. Si aucune page froide n'est présente, alors il swappe au hasard une page chaude.
Pour implémenter l'algorithme NRU, l'OS mémorise, dans chaque entrée de la table des pages, si la page associée est froide ou chaude. Pour cela, il met à 0 ou 1 un bit dédié : le '''bit ''Accessed'''''. La différence avec le bit ''dirty'' est que le bit ''dirty'' est mis à jour uniquement lors des écritures, alors que le bit ''Accessed'' l'est aussi lors d'une lecture. Uen lecture met à 1 le bit ''Accessed'', mais ne touche pas au bit ''dirty''. Les écritures mettent les deux bits à 1.
Implémenter l'algorithme NRU demande juste de mettre à jour le bit ''Accessed'' de chaque entrée de la table des pages. Et sur les architectures modernes, le processeur s'en charge automatiquement. A chaque accès mémoire, que ce soit en lecture ou en écriture, le processeur met à 1 ce bit. Par contre, le système d'exploitation le met à 0 à intervalles réguliers. En conséquence, quand un remplacement de page doit avoir lieu, les pages chaudes ont de bonnes chances d'avoir le bit ''Accessed'' à 1, alors que les pages froides l'ont à 0. Ce n'est pas certain, et on peut se trouver dans des cas où ce n'est pas le cas. Par exemple, si un remplacement a lieu juste après la remise à zéro des bits ''Accessed''. Le choix de la page à remplacer est donc imparfait, mais fonctionne bien en pratique.
Tous les algorithmes précédents ont chacun deux variantes : une locale, et une globale. Avec la version locale, la page qui va être rapatriée sur le disque dur est une page réservée au programme qui est la cause du page miss. Avec la version globale, le système d'exploitation va choisir la page à virer parmi toutes les pages présentes en mémoire vive.
===La protection mémoire avec la pagination===
Avec la pagination, chaque page a des '''droits d'accès''' précis, qui permettent d'autoriser ou interdire les accès en lecture, écriture, exécution, etc. La table des pages mémorise les autorisations pour chaque page, sous la forme d'une suite de bits où chaque bit autorise/interdit une opération bien précise. En pratique, les tables de pages modernes disposent de trois bits : un qui autorise/interdit les accès en lecture, un qui autorise/interdit les accès en écriture, un qui autorise/interdit l'éxecution du contenu de la page.
Le format exact de la suite de bits a cependant changé dans le temps sur les processeurs x86 modernes. Par exemple, avant le passage au 64 bits, les CPU et OS ne pouvaient pas marquer une page mémoire comme non-exécutable. C'est seulement avec le passage au 64 bits qu'a été ajouté un bit pour interdire l'exécution de code depuis une page. Ce bit, nommé '''bit NX''', est à 0 si la page n'est pas exécutable et à 1 sinon. Le processeur vérifie à chaque chargement d'instruction si le bit NX de page lue est à 1. Sinon, il lève une exception matérielle et laisse la main à l'OS.
Une amélioration de cette protection est la technique dite du '''''Write XOR Execute''''', abréviée WxX. Elle consiste à interdire les pages d'être à la fois accessibles en écriture et exécutables. Il est possible de changer les autorisations en cours de route, ceci dit.
Les premiers IBM 360 disposaient d'un mécanisme de protection mémoire totalement différent, sans registres limite/base. Ce mécanisme de protection attribue à chaque programme une '''clé de protection''', qui consiste en un nombre unique de 4 bits (chaque programme a donc une clé différente de ses collègues). La mémoire est fragmentée en blocs de même taille, de 2 kibioctets. Le processeur mémorise, pour chacun de ses blocs, la clé de protection du programme qui a réservé ce bloc. À chaque accès mémoire, le processeur compare la clé de protection du programme en cours d’exécution et celle du bloc de mémoire de destination. Si les deux clés sont différentes, alors un programme a effectué un accès hors des clous et il se fait sauvagement arrêter.
===La traduction d'adresse avec la pagination===
Comme dit plus haut, les pages sont numérotées, de 0 à une valeur maximale, afin de les identifier. Le numéro en question est appelé le '''numéro de page'''. Il est utilisé pour dire au processeur : je veux lire une donnée dans la page numéro 20, la page numéro 90, etc. Une fois qu'on a le numéro de page, on doit alors préciser la position de la donnée dans la page, appelé le '''décalage''', ou encore l{{'}}''offset''.
Le numéro de page et le décalage se déduisent à partir de l'adresse, en divisant l'adresse par la taille de la page. Le quotient obtenu donne le numéro de la page, alors que le reste est le décalage. Les processeurs actuels utilisent tous des pages dont la taille est une puissance de deux, ce qui fait que ce calcul est fortement simplifié. Sous cette condition, le numéro de page correspond aux bits de poids fort de l'adresse, alors que le décalage est dans les bits de poids faible.
Le numéro de page existe en deux versions : un numéro de page physique qui identifie une page en mémoire physique, et un numéro de page logique qui identifie une page dans la mémoire virtuelle. Traduire l'adresse logique en adresse physique demande de remplacer le numéro de la page logique en un numéro de page physique.
[[File:Phycical address.JPG|centre|vignette|upright=2|Traduction d'adresse avec la pagination.]]
====Les tables des pages simples====
Dans le cas le plus simple, il n'y a qu'une seule table des pages, qui est adressée par les numéros de page logique. La table des pages est un vulgaire tableau d'adresses physiques, placées les unes à la suite des autres. Avec cette méthode, la table des pages a autant d'entrée qu'il y a de pages logiques en mémoire virtuelle. Accéder à la mémoire nécessite donc d’accéder d'abord à la table des pages en mémoire, de calculer l'adresse de l'entrée voulue, et d’y accéder.
[[File:Table des pages.png|centre|vignette|upright=2|Table des pages.]]
La table des pages est souvent stockée dans la mémoire RAM, son adresse est connue du processeur, mémorisée dans un registre spécialisé du processeur. Le processeur effectue automatiquement le calcul d'adresse à partir de l'adresse de base et du numéro de page logique.
[[File:Address translation (32-bit).png|centre|vignette|upright=2|Address translation (32-bit)]]
====Les tables des pages inversées====
Sur certains systèmes, notamment sur les architectures 64 bits ou plus, le nombre de pages est très important. Sur les ordinateurs x86 récents, les adresses sont en pratique de 48 bits, les bits de poids fort étant ignorés en pratique, ce qui fait en tout 68 719 476 736 pages. Chaque entrée de la table des pages fait au minimum 48 bits, mais fait plus en pratique : partons sur 64 bits par entrée, soit 8 octets. Cela fait 549 755 813 888 octets pour la table des pages, soit plusieurs centaines de gibioctets ! Une table des pages normale serait tout simplement impraticable.
Pour résoudre ce problème, on a inventé les '''tables des pages inversées'''. L'idée derrière celles-ci est l'inverse de la méthode précédente. La méthode précédente stocke, pour chaque page logique, son numéro de page physique. Les tables des pages inversées font l'inverse : elles stockent, pour chaque numéro de page physique, la page logique qui correspond. Avec cette méthode table des pages contient ainsi autant d'entrées qu'il y a de pages physiques. Elle est donc plus petite qu'avant, vu que la mémoire physique est plus petite que la mémoire virtuelle.
Quand le processeur veut convertir une adresse virtuelle en adresse physique, la MMU recherche le numéro de page de l'adresse virtuelle dans la table des pages. Le numéro de l'entrée à laquelle se trouve ce morceau d'adresse virtuelle est le morceau de l'adresse physique. Pour faciliter le processus de recherche dans la page, la table des pages inversée est ce que l'on appelle une table de hachage. C'est cette solution qui est utilisée sur les processeurs Power PC.
[[File:Table des pages inversée.jpg|centre|vignette|upright=2|Table des pages inversée.]]
====Les tables des pages multiples par espace d'adressage====
Dans les deux cas précédents, il y a une table des pages unique. Cependant, les concepteurs de processeurs et de systèmes d'exploitation ont remarqué que les adresses les plus hautes et/ou les plus basses sont les plus utilisées, alors que les adresses situées au milieu de l'espace d'adressage sont peu utilisées en raison du fonctionnement de la pile et du tas. Il y a donc une partie de la table des pages qui ne sert à rien et est utilisé pour des adresses inutilisées. C'est une source d'économie d'autant plus importante que les tables des pages sont de plus en plus grosses.
Pour profiter de cette observation, les concepteurs d'OS ont décidé de découper l'espace d'adressage en plusieurs sous-espaces d'adressage de taille identique : certains localisés dans les adresses basses, d'autres au milieu, d'autres tout en haut, etc. Et vu que l'espace d'adressage est scindé en plusieurs parties, la table des pages l'est aussi, elle est découpée en plusieurs sous-tables. Si un sous-espace d'adressage n'est pas utilisé, il n'y a pas besoin d'utiliser de la mémoire pour stocker la table des pages associée. On ne stocke que les tables des pages pour les espaces d'adressage utilisés, ceux qui contiennent au moins une donnée.
L'utilisation de plusieurs tables des pages ne fonctionne que si le système d'exploitation connaît l'adresse de chaque table des pages (celle de la première entrée). Pour cela, le système d'exploitation utilise une super-table des pages, qui stocke les adresses de début des sous-tables de chaque sous-espace. En clair, la table des pages est organisé en deux niveaux, la super-table étant le premier niveau et les sous-tables étant le second niveau.
L'adresse est structurée de manière à tirer profit de cette organisation. Les bits de poids fort de l'adresse sélectionnent quelle table de second niveau utiliser, les bits du milieu de l'adresse sélectionne la page dans la table de second niveau et le reste est interprété comme un ''offset''. Un accès à la table des pages se fait comme suit. Les bits de poids fort de l'adresse sont envoyés à la table de premier niveau, et sont utilisés pour récupérer l'adresse de la table de second niveau adéquate. Les bits au milieu de l'adresse sont envoyés à la table de second niveau, pour récupérer le numéro de page physique. Le tout est combiné avec l{{'}}''offset'' pour obtenir l'adresse physique finale.
[[File:Table des pages hiérarchique.png|centre|vignette|upright=2|Table des pages hiérarchique.]]
On peut aussi aller plus loin et découper la table des pages de manière hiérarchique, chaque sous-espace d'adressage étant lui aussi découpé en sous-espaces d'adressages. On a alors une table de premier niveau, plusieurs tables de second niveau, encore plus de tables de troisième niveau, et ainsi de suite. Cela peut aller jusqu'à 5 niveaux sur les processeurs x86 64 bits modernes. On parle alors de '''tables des pages emboitées'''. Dans ce cours, la table des pages désigne l'ensemble des différents niveaux de cette organisation, toutes les tables inclus. Seules les tables du dernier niveau mémorisent des numéros de page physiques, les autres tables mémorisant des pointeurs, des adresses vers le début des tables de niveau inférieur. Un exemple sera donné plus bas, dans la section suivante.
====L'exemple des processeurs x86====
Pour rendre les explications précédentes plus concrètes, nous allons prendre l'exemple des processeur x86 anciens, de type 32 bits. Les processeurs de ce type utilisaient deux types de tables des pages : une table des page unique et une table des page hiérarchique. Les deux étaient utilisées dans cas séparés. La table des page unique était utilisée pour les pages larges et encore seulement en l'absence de la technologie ''physical adress extension'', dont on parlera plus bas. Les autres cas utilisaient une table des page hiérarchique, à deux niveaux, trois niveaux, voire plus.
Une table des pages unique était utilisée pour les pages larges (de 2 mébioctets et plus). Pour les pages de 4 mébioctets, il y avait une unique table des pages, adressée par les 10 bits de poids fort de l'adresse, les bits restants servant comme ''offset''. La table des pages contenait 1024 entrées de 4 octets chacune, ce qui fait en tout 4 kibioctet pour la table des pages. La table des page était alignée en mémoire sur un bloc de 4 kibioctet (sa taille).
[[File:X86 Paging 4M.svg|centre|vignette|upright=2|X86 Paging 4M]]
Pour les pages de 4 kibioctets, les processeurs x86-32 bits utilisaient une table des page hiérarchique à deux niveaux. Les 10 bits de poids fort l'adresse adressaient la table des page maitre, appelée le directoire des pages (''page directory''), les 10 bits précédents servaient de numéro de page logique, et les 12 bits restants servaient à indiquer la position de l'octet dans la table des pages. Les entrées de chaque table des pages, mineure ou majeure, faisaient 32 bits, soit 4 octets. Vous remarquerez que la table des page majeure a la même taille que la table des page unique obtenue avec des pages larges (de 4 mébioctets).
[[File:X86 Paging 4K.svg|centre|vignette|upright=2|X86 Paging 4K]]
La technique du '''''physical adress extension''''' (PAE), utilisée depuis le Pentium Pro, permettait aux processeurs x86 32 bits d'adresser plus de 4 gibioctets de mémoire, en utilisant des adresses physiques de 64 bits. Les adresses virtuelles de 32 bits étaient traduites en adresses physiques de 64 bits grâce à une table des pages adaptée. Cette technologie permettait d'adresser plus de 4 gibioctets de mémoire au total, mais avec quelques limitations. Notamment, chaque programme ne pouvait utiliser que 4 gibioctets de mémoire RAM pour lui seul. Mais en lançant plusieurs programmes, on pouvait dépasser les 4 gibioctets au total. Pour cela, les entrées de la table des pages passaient à 64 bits au lieu de 32 auparavant.
La table des pages gardait 2 niveaux pour les pages larges en PAE.
[[File:X86 Paging PAE 2M.svg|centre|vignette|upright=2|X86 Paging PAE 2M]]
Par contre, pour les pages de 4 kibioctets en PAE, elle était modifiée de manière à ajouter un niveau de hiérarchie, passant de deux niveaux à trois.
[[File:X86 Paging PAE 4K.svg|centre|vignette|upright=2|X86 Paging PAE 4K]]
En 64 bits, la table des pages est une table des page hiérarchique avec 5 niveaux. Seuls les 48 bits de poids faible des adresses sont utilisés, les 16 restants étant ignorés.
[[File:X86 Paging 64bit.svg|centre|vignette|upright=2|X86 Paging 64bit]]
===La TLB et les caches associés===
En théorie, la table des pages est censée être accédée à chaque accès mémoire. Mais pour éviter d'avoir à lire la table des pages en mémoire RAM à chaque accès mémoire, les concepteurs de processeurs ont décidé d'implanter un cache dédié, le '''''translation lookaside buffer''''', ou TLB. Le TLB stocke au minimum de quoi faire la traduction entre adresse virtuelle et adresse physique, à savoir une correspondance entre numéro de page logique et numéro de page physique. Pour faire plus général, il stocke des entrées de la table des pages.
[[File:MMU principle updated.png|centre|vignette|upright=2.0|MMU avec une TLB.]]
Les accès à la table des pages sont gérés de deux façons : soit le processeur gère tout seul la situation, soit il délègue cette tâche au système d’exploitation. Sur les processeurs anciens, le système d'exploitation gère le parcours de la table des pages. Mais cette solution logicielle n'a pas de bonnes performances. D'autres processeurs gèrent eux-mêmes le défaut d'accès à la TLB et vont chercher d'eux-mêmes les informations nécessaires dans la table des pages. Ils disposent de circuits, les '''''page table walkers''''' (PTW), qui s'occupent eux-mêmes du défaut.
Les ''page table walkers'' contiennent des registres qui leur permettent de faire leur travail. Le plus important est celui qui mémorise la position de la table des pages en mémoire RAM, dont nous avons parlé plus haut. Les PTW ont besoin, pour faire leur travail, de mémoriser l'adresse physique de la table des pages, ou du moins l'adresse de la table des pages de niveau 1 pour des tables des pages hiérarchiques. Mais d'autres registres existent. Toutes les informations nécessaires pour gérer les défauts de TLB sont stockées dans des registres spécialisés appelés des '''tampons de PTW''' (PTW buffers).
===L'abstraction matérielle des processus===
[[File:Memoire virtuelle.svg|vignette|Mémoire virtuelle]]
Il est possible d'implémenter l'abstraction matérielle des processus avec la pagination. En clair, chaque programme lancé sur l'ordinateur dispose de son propre espace d'adressage, ce qui fait que la même adresse logique ne pointera pas sur la même adresse physique dans deux programmes différents. Pour cela, il y a plusieurs méthodes.
La première solution n'utilise qu'une seule table des pages. Pour cela, chaque entrée indique à quel processus elle est associée. Pour cela, chaque entrée contient un '''identifiant de processus''', un numéro qui précise pour quel processus, pour quel espace d'adressage, la correspondance est valide. La page des tables peut aussi contenir des entrées qui sont valides pour tous les processus en même temps. Pour cela, l'entrée contient un '''bit ''global''''', qui précise que cette correspondance est valide pour tous les processus, l'identifiant de processus est ignoré. Un défaut est que le partage d'une page entre plusieurs processus est impossible : soit on partage une page avec tous les processus, soit on l'alloue avec un seul processus.
L'intérêt de partager des pages entre tous les processus n'est pas évident, mais il le devient quand on se rappelle que le noyau de l'OS est mappé dans le haut de l'espace d'adressage. Et peu importe l'espace d'adressage, le noyau est toujours mappé de manière identique, les mêmes adresses logiques adressant la même adresse mémoire. En conséquence, les correspondances adresse physique-logique sont les mêmes pour le noyau, peu importe l'espace d'adressage. Le bit global accélère rapidement la traduction d'adresse pour l'accès au noyau.
Une solution alternative, plus simple, utilise une table des pages par processus lancé sur l'ordinateur, une table des pages unique par espace d'adressage. À chaque changement de processus, le registre qui mémorise la position de la table des pages est modifié pour pointer sur la bonne. C'est le système d'exploitation qui se charge de cette mise à jour.
Avec cette méthode, il est possible de partager une ou plusieurs pages entre plusieurs processus, en configurant les tables des pages convenablement. Les pages partagées sont mappées dans l'espace d'adressage de plusieurs processus, mais pas forcément au même endroit, pas forcément dans les mêmes adresses logiques. On peut placer la page partagée à l'adresse logique 0x0FFF pour un processus, à l'adresse logique 0xFF00 pour un autre processus, etc. Par contre, les entrées de la table des pages pour ces adresses pointent vers la même adresse physique.
[[File:Vm5.png|centre|vignette|upright=2|Tables des pages de plusieurs processus.]]
===La taille des pages===
La taille des pages varie suivant le processeur et le système d'exploitation et tourne souvent autour de 4 kibioctets. Les processeurs actuels gèrent plusieurs tailles différentes pour les pages : 4 kibioctets par défaut, 2 mébioctets, voire 1 à 4 gibioctets pour les pages les plus larges. Les pages de 4 kibioctets sont les pages par défaut, les autres tailles de page sont appelées des ''pages larges''. La taille optimale pour les pages dépend de nombreux paramètres et il n'y a pas de taille qui convienne à tout le monde. Certaines applications gagnent à utiliser des pages larges, d'autres vont au contraire perdre drastiquement en performance en les utilisant.
Le désavantage principal des pages larges est qu'elles favorisent la fragmentation mémoire. Si un programme veut réserver une portion de mémoire, pour une structure de donnée quelconque, il doit réserver une portion dont la taille est multiple de la taille d'une page. Par exemple, un programme ayant besoin de 110 kibioctets allouera 28 pages de 4 kibioctets, soit 120 kibioctets : 2 kibioctets seront perdus. Par contre, avec des pages larges de 2 mébioctets, on aura une perte de 2048 - 110 = 1938 kibioctets. En somme, des morceaux de mémoire seront perdus, car les pages sont trop grandes pour les données qu'on veut y mettre. Le résultat est que le programme qui utilise les pages larges utilisent plus de mémoire et ce d'autant plus qu'il utilise des données de petite taille. Un autre désavantage est qu'elles se marient mal avec certaines techniques d'optimisations de type ''copy-on-write''.
Mais l'avantage est que la traduction des adresses est plus performante. Une taille des pages plus élevée signifie moins de pages, donc des tables des pages plus petites. Et des pages des tables plus petites n'ont pas besoin de beaucoup de niveaux de hiérarchie, voire peuvent se limiter à des tables des pages simples, ce qui rend la traduction d'adresse plus simple et plus rapide. De plus, les programmes ont une certaine localité spatiale, qui font qu'ils accèdent souvent à des données proches. La traduction d'adresse peut alors profiter de systèmes de mise en cache dont nous parlerons dans le prochain chapitre, et ces systèmes de cache marchent nettement mieux avec des pages larges.
Il faut noter que la taille des pages est presque toujours une puissance de deux. Cela a de nombreux avantages, mais n'est pas une nécessité. Par exemple, le tout premier processeur avec de la pagination, le super-ordinateur Atlas, avait des pages de 3 kibioctets. L'avantage principal est que la traduction de l'adresse physique en adresse logique est trivial avec une puissance de deux. Cela garantit que l'on peut diviser l'adresse en un numéro de page et un ''offset'' : la traduction demande juste de remplacer les bits de poids forts par le numéro de page voulu. Sans cela, la traduction d'adresse implique des divisions et des multiplications, qui sont des opérations assez couteuses.
===Les entrées de la table des pages===
Avant de poursuivre, faisons un rapide rappel sur les entrées de la table des pages. Nous venons de voir que la table des pages contient de nombreuses informations : un bit ''valid'' pour la mémoire virtuelle, des bits ''dirty'' et ''accessed'' utilisés par l'OS, des bits de protection mémoire, un bit ''global'' et un potentiellement un identifiant de processus, etc. Étudions rapidement le format de la table des pages sur un processeur x86 32 bits.
* Elle contient d'abord le numéro de page physique.
* Les bits AVL sont inutilisés et peuvent être configurés à loisir par l'OS.
* Le bit G est le bit ''global''.
* Le bit PS vaut 0 pour une page de 4 kibioctets, mais est mis à 1 pour une page de 4 mébioctets dans le cas où le processus utilise des pages larges.
* Le bit D est le bit ''dirty''.
* Le bit A est le bit ''accessed''.
* Le bit PCD indique que la page ne peut pas être cachée, dans le sens où le processeur ne peut copier son contenu dans le cache et doit toujours lire ou écrire cette page directement dans la RAM.
* Le bit PWT indique que les écritures doivent mettre à jour le cache et la page en RAM (dans le chapitre sur le cache, on verra qu'il force le cache à se comporter comme un cache ''write-through'' pour cette page).
* Le bit U/S précise si la page est accessible en mode noyau ou utilisateur.
* Le bit R/W indique si la page est accessible en écriture, toutes les pages sont par défaut accessibles en lecture.
* Le bit P est le bit ''valid''.
[[File:PDE.png|centre|vignette|upright=2.5|Table des pages des processeurs Intel 32 bits.]]
==Comparaison des différentes techniques d'abstraction mémoire==
Pour résumer, l'abstraction mémoire permet de gérer : la relocation, la protection mémoire, l'isolation des processus, la mémoire virtuelle, l'extension de l'espace d'adressage, le partage de mémoire, etc. Elles sont souvent implémentées en même temps. Ce qui fait qu'elles sont souvent confondues, alors que ce sont des concepts sont différents. Ces liens sont résumés dans le tableau ci-dessous.
{|class="wikitable"
|-
!
! colspan="5" | Avec abstraction mémoire
! rowspan="2" | Sans abstraction mémoire
|-
!
! Relocation matérielle
! Segmentation en mode réel (x86)
! Segmentation, général
! Architectures à capacités
! Pagination
|-
! Abstraction matérielle des processus
| colspan="4" | Oui, relocation matérielle
| Oui, liée à la traduction d'adresse
| Emulée via relocation logicielle au démarrage du programme.
|-
! Mémoire virtuelle
| colspan="2" | Non, sauf émulation logicielle
| colspan="3" | Oui, gérée par le processeur et l'OS
| Non, sauf émulation logicielle
|-
! Extension de l'espace d'adressage
| colspan="2" | Oui : registre de base élargi
| colspan="2" | Oui : adresse de base élargie dans la table des segments
| ''Physical Adress Extension'' des processeurs 32 bits
| Commutation de banques
|-
! Protection mémoire
| Registre limite
| Aucune
| colspan="2" | Registre limite, droits d'accès aux segments
| Gestion des droits d'accès aux pages
| Possible, méthodes variées
|-
! Partage de mémoire
| colspan="2" | Non
| colspan="2" | Segment partagés
| Pages partagées
| Possible, méthodes variées
|}
===Les différents types de segmentation===
La segmentation regroupe plusieurs techniques franchement différentes, qui auraient gagné à être nommées différemment. La principale différence est l'usage de registres de relocation versus des registres de sélecteurs de segments. L'usage de registres de relocation est le fait de la relocation matérielle, mais aussi de la segmentation en mode réel des CPU x86. Par contre, l'usage de sélecteurs de segments est le fait des autres formes de segmentation, architectures à capacité inclues.
La différence entre les deux est le nombre de segments. L'usage de registres de relocation fait que le CPU ne gère qu'un petit nombre de segments de grande taille. La mémoire virtuelle est donc rarement implémentée vu que swapper des segments de grande taille est trop long, l'impact sur les performances est trop important. Sans compter que l'usage de registres de base se marie très mal avec la mémoire virtuelle. Vu qu'un segment peut être swappé ou déplacée n'importe quand, il faut invalider les registres de base au moment du swap/déplacement, ce qui n'est pas chose aisée. Aucun processeur ne gère cela, les méthodes pour n'existent tout simplement pas. L'usage de registres de base implique que la mémoire virtuelle est absente.
La protection mémoire est aussi plus limitée avec l'usage de registres de relocation. Elle se limite à des registres limite, mais la gestion des droits d'accès est limitée. En théorie, la segmentation en mode réel pourrait implémenter une version limitée de protection mémoire, avec une protection de l'espace exécutable. Mais ca n'a jamais été fait en pratique sur les processeurs x86.
Le partage de la mémoire est aussi difficile sur les architectures avec des registres de base. L'absence de table des segments fait que le partage d'un segment est basiquement impossible sans utiliser des méthodes complétement tordues, qui ne sont jamais implémentées en pratique.
===Segmentation versus pagination===
Par rapport à la pagination, la segmentation a des avantages et des inconvénients. Tous sont liés aux propriétés des segments et pages : les segments sont de grande taille et de taille variable, les pages sont petites et de taille fixe.
L'avantage principal de la segmentation est sa rapidité. Le fait que les segments sont de grande taille fait qu'on a pas besoin d'équivalent aux tables des pages inversée ou multiple, juste d'une table des segments toute simple. De plus, les échanges entre table des pages/segments et registres sont plus rares avec la segmentation. Par exemple, si un programme utilise un segment de 2 gigas, tous les accès dans le segment se feront avec une seule consultation de la table des segments. Alors qu'avec la pagination, il faudra une consultation de la table des pages chaque bloc de 4 kibioctet, au minimum.
Mais les désavantages sont nombreux. Le système d'exploitation doit agencer les segments en RAM, et c'est une tâche complexe. Le fait que les segments puisse changer de taille rend le tout encore plus complexe. Par exemple, si on colle les segments les uns à la suite des autres, changer la taille d'un segment demande de réorganiser tous les segments en RAM, ce qui demande énormément de copies RAM-RAM. Une autre possibilité est de laisser assez d'espace entre les segments, mais cet espace est alors gâché, dans le sens où on ne peut pas y placer un nouveau segment.
Swapper un segment est aussi très long, vu que les segments sont de grande taille, alors que swapper une page est très rapide.
==Un cas particulier : la z/architecture d'IBM==
IBM a sa propre gamme de ''mainframes'', qui utilisent des processeurs fait maison par IBM lui-même. Les CPU IBM ont leur propre jeu d'instruction CISC, appelé '''''z/architecture'''''. Et ce jeu d'instruction a des possibilités en termes d'adressage qui sont extrêmement intéressantes, en plus d'être assez inédites.
La ''z/architecture'' gère des pages de 4 kibioctets, en plus de pages larges introduites dans les dernières versions. L traduction d’adresse se fait cependant en passant des regroupements de 4 pages virtuelles, appelés des ''segments'', qui n'ont rien à voir avec les segments au sens de la segmentation. Pour éviter les confusions, je vais les appeler des ''super-pages''. Les super-pages sont des groupes de 4 pages virtuelles, ce qui veut dire que les 4 pages peuvent être très éloignées en mémoire physique. Les super-pages sont concrètement des intermédiaires utilisés pour la traduction d'adresse. La traduction d'adresse démarre à une table des super-pages, qui point vers une table des pages à plusieurs niveaux (5 maximum).
Mais ce n'est pas ce qui va nous intéresser dans ce qui suit. À la place, nous allons nous concentrer sur une fonctionnalité : les programmes ont accès à plusieurs espaces d'adressage simultanément. La ''z/architecture'' supporte 65 536 espaces d'adressage simultanés. Par contre, un programme ne peut en utiliser que 2048 en même temps. De plus, ils ne sont accessibles que quand le processeur est dans un mode de fonctionnement bien précis. Un processeur ''z/architecture'' peut en effet fonctionner dans deux modes, qu'on va appeler "standard" et "étendu".
L'existence de ces deux modes s'explique par le fait que les ''mainframes'' IBM ont évolués dans le temps. Les premiers ''mainframes'' n'avaient même pas de support basique de la mémoire virtuelle, le System/360 s'en passait totalement. C'est sur le System/370 que la pagination a été ajoutée. Le System/370 supportait deux espaces d'adressage virtuel par programme, là où les implémentations classiques n'en utilisent qu'un par programme. L'ESA/370 a ajouté la possibilité d'utiliser 16 espaces d'adressage par programme, mais l'a fait d'une manière à garder la compatibilité avec le System/370. Le processeur a donc deux modes : un avec deux espaces d'adressage par programme, un autre pouvant monter jusqu’à 16.
===Le mode standard de la ''z/architecture''===
En mode standard, un programme exécuté sur la ''z/architecture'' utilise deux espaces d'adressages : l''''espace d'adressage primaire''' où se situent le programme exécuté et les données importantes, un '''espace d'adressage secondaire''' utilisé pour des tâches/données annexes.
: Un troisième espace d'adressage l''''espace d'adressage ''home''''', est réservé au système d'exploitation. Il a pour particularité de ne pas avoir d'ASN. Il n'est concrètement accessible qu'en mode superviseur, donc par le système d'exploitation. C'est en quelque sorte un espace d'adressage pour le noyau, séparé des espaces d'adressage utilisateur. Nous n'en parlerons pas dans ce qui suit.
Mais comment le processeur pour utiliser ces espaces d'adressages ? Par exemple, s'il veut faire une lecture, comment précise-t-il l'espace d'adressage courant ? La réponse est que le processeur peut être dans deux états : soit primaire, soit secondaire. Dans le premier état, toutes les lectures/écritures se font dans l'espace d'adressage primaire. Dans le second, elles se font toutes dans l'espace d'adressage secondaire. Il peut passer de l'un à l'autre en utilisant l'instruction ''Set Address Space Control''. Le processeur dispose aussi d'instructions ''MOVE TO PRIMARY'' et ''MOVE TO SECONDARY'' pour échanger des données dans les deux espaces d'adressage.
L'espace d'adressage primaire a une table des pages dédiée, séparée de celle de l'espace d'adressage secondaire. Ou plutôt devrais-je dire une table des super-pages. La table des pages est une table des pages emboitée. Il y a peu à détailler dessus, si ce n'est pour cette histoire de segments/super-pages. L'adresse de la table des super-page est mémorisée dans le registre de contrôle CR1 pour l'espace d'adressage primaire, CR7 pour l'espace d'adressage secondaire. L'un ou l'autre est utilisé, selon que le processeur est en mode "primaire" ou "secondaire".
===Les registres d'accès de la ''z/architecture''===
Un programme peut donc passer de l'espace d'adressage primaire à secondaire (ou inversement) en utilisant l’instruction ''SET ADDRESS SPACE CONTROL''. Mais il peut en profiter pour passer dans un mode où il a accès à 2048 espaces d'adressage différents, en même temps. Le processeur n'a pas besoin d'utiliser d'instructions particulières pour copier des données d'un espace d'adressage à un autre, un simple MOV suffit. Le mode en question est appelé le '''mode ''access register'''''.
Le mode ''access register'' donne accès aux '''registres d'accès''', qui lui donnent son nom. Les registres d'accès mémorisent des identifiants qui permettent de sélectionner un espace d'adressage parmi 2048. Il y a en tout 16 registres d'accès, chacun étant appairé avec un registre général. Par exemple, le registre général numéro 5 est appairé au registre d'accès de même numéro. En mode ''access register'', toute lecture/écriture lit l'adresse dans un registre général et le registre d'accès associé indique dans quel espace d'adressage se trouve cette adresse. Par exemple, une lecture utilise l'adresse dans le registre général numéro 7, l'espace d'adressage associé sera dans le registre d'accès numéro 7.
Les registres d'accès peuvent être modifiés par des instructions classiques, en espace utilisateur. Elles sont nommées COPY ACCESS, EXTRACT ACCESS, LOAD ACCESS MULTIPLE, LOAD ADDRESS EXTENDED, SET ACCESS et STORE ACCESS MULTIPLE. Elles permettent de copier un registre d'accès dans un autre, de charger le contenu d'un registre d'accès depuis la mémoire RAM, d'enregistrer les registres d'accès dans la mémoire RAM, de copier une constante dans un registre d'accès. Mais quel est le contenu de ces registres ?
Une implémentation simple numéroterait les espaces d'adressage et mettrait les numéros dans les registres d'accès. Un espace d'adressage serait associé à une table des super-pages, reste à mémoriser la correspondance ''numéro d'espace d'adressage - adresse de la table des super-pages''. Pour cela, l'idéal est de mémoriser ces correspondances dans un tableau en mémoire RAM. Le numéro d'espace d'adressage est alors un indice, qui pointe sur l'entrée adéquate dans cette table. Il devrait y avoir un tableau par programme, qui serait protégé en écriture et seulement accessible par le système d'exploitation, comme le serait une table des pages. Mais ce n'est pas exactement ce qui est fait.
===L'utilisation des registres d'accès===
La différence est qu'il n'y a pas une table de correspondance par programme, mais un système à deux tableaux imbriqués. Le premier tableau possède 65 536 entrées, une par espace d'adressage supporté par le processeur. C'est donc une table partagée par tous les programmes. Par contre, la seconde table est bien unique pour chaque programme, chaque programme a la sienne. Faire ainsi permet de simplifier le partage de la mémoire. La table partagée est l''''''Access Space Table Entry''''' (ASTE), alors que la seconde table est la '''liste privée''' (''access list''), sous-entendu "privée pour un programme".
La liste privée mémorise une liste des espaces d'adressage auquel le programme peut accéder, auxquels le système d'exploitation lui a accordé l'accès. Pour chaque espace d'adressage, elle renvoie vers l'entrée de l'ASTE associée. En plus de mémoriser l'adresse de cette entrée, elle mémorise les droits d'accès que le programme a sur l'espace d'adressage associé. Par exemple, si deux programmes partagent un espace d'adressage, l'un pourra y avoir accès en lecture seule, l'autre pourra avoir un accès total. Pour cela, ils ont des droits différents dans la liste privée, mais l'entrée de l'ASTE sera la même.
[[File:Tradiction d'adresse sur la zarchitecture d'IBM.png|centre|vignette|upright=2|Traduction d'adresse sur la zarchitecture d'IBM.]]
Il y a donc une vérification des droits d'accès, suivi d'un accès à une table des super-pages globale. Si un programme veut accéder à un espace d'adressage qui lui est interdit, l'accès mémoire stoppera dans la liste privée. Le processeur utilisera l'identifiant d'espace d'adressage, mais la liste privée n'aura pas d'entrée associée. Elle lèvera alors une exception matérielle et le système d'exploitation gérera la situation, vraisemblablement en tuant le programme fautif. Mais si l'accès est autorisé, alors le programme accède à la table des super-pages et l'accès mémoire se poursuit.
Je dis une liste d'accès privée, mais il y en a deux en réalité. Mais la distinction exacte demanderait de parler de ''threads'' et de processus. Pour ceux qui sont familiers avec ces concepts, chaque ''thread'' a sa propre liste privée, avec en plus une liste par processus, partagée entre tous les ''threads''. Deux listes de 1024 entrées maximum, cela fait 2048 entrées au total. Les deux listes portent les noms de ''Dispatchable Unit Access List'' et de ''PASN-AL (Primary Address Space Access List''. Inutile de retenir ces deux termes, cependant.
Un registre d'accès mémorise un '''''Access List Entry Token''''' ou ALET. L'ALET contient l'indice d'une entrée dans la liste privée. Elle contient aussi un bit qui indique quelle liste privée utiliser, entre la ''Dispatchable Unit Access List'' et la ''PASN-AL (Primary Address Space Access List''. Précisons cependant que deux valeurs font exceptions : 0 pointe vers l'espace d'adressage principal, 1 vers l'espace d'adressage secondaire. Les autres valeurs sont configurables.
{|class="wikitable"
|+ Registre d'accès
|-
! 0000000 !! L (choix de la liste privée !! ASLEN !! ALEN
|-
| 0000000 || Choix de la liste privée || Identifiant de protection || Indice dans la liste privée
|}
Lors d'une lecture/écriture, l'ALETs est utilisé pour sélectionner l'espace d'adressage. Les valeurs 0 et 1 sont traitées telles quelles, l'espace d'adressage est déterminé automatiquement par le processeur. Pour les autres valeurs, récupérer l'adresse mémoire demande de consulter la liste d'accès privée. L'ALET est envoyé à la liste d'accès privée, et cette dernière fournit l'adresse de l'entrée de l'ASTE à consulter. Le processeur connait l'adresse de la liste privée, car son adresse mémoire est mémorisée dans les registres de contrôle numéro 2 et 5.
: Précisons que le registre d'accès numéro 0 contient obligatoirement la valeur 0, ce qui fait qu'il pointe vers l'espace d'adressage primaire.
[[File:Traduction d'adresse - trois espaces d'adressage, zarchitecture d'IBM.png|centre|vignette|upright=2|Traduction d'adresse - trois espaces d'adressage, zarchitecture d'IBM.]]
Une subtilité est que le système d'exploitation peut à tout moment modifier le contenu de la liste privée. Il se peut qu'un ALET qui correspondait à un espace d'adressage se mette à pointer vers un autre. Pour détecter cette situation, le processeur attribue un identifiant, à chaque espace d'adressage. L'identifiant est mémorisé dans les entrées de la liste privée, de l'ASTE et dans les registres d'accès (dans les bits de poids fort de l'ALET). L'identifiant est modifié dans l'ALET à chaque fois que le système d'exploitation modifie l'entrée. Si un programme fait un accès avec un registre d'accès, mais que l'identifiant ne correspond pas avec celui dans l'ALET/liste privée, c'est signe que l'espace d'adressage a été swappé, une exception matérielle est levée.
Un tel fonctionnement vous aura sans doute fait penser à une architecture à capacité, mais la z/architecture n'en est pas une. Malgré des points de ressemblance assez importants, il y a des différences fondamentales avec les architectures à capacité.
* Le z/architecture ne supporte pas la segmentation, qui est nécessaire pour les architectures à capacité. Sur la z/architecture, ce sont des espaces d'adressage qui sont sélectionnés, pas des segments.
* Les registres d'accès ne sont pas des registres de capacité, car on peut les modifier avec des instructions en espace utilisateur. En comparaison, modifier les registres de capacité ne peut se faire qu'en espace noyau, sous l'égide du système d'exploitation.
* Les droits d'accès sont dans une table séparée des pointeurs, pas concaténés à ceux-ci. La gestion des droits d'accès est gérée par les tables des pages.
* Les identifiants ALET ne peuvent pas être passés à un autre programme. Deux ALETs identiques correspondent à deux espaces d'adressages différents dans deux programmes différents. L'identifiant peut être identique, il pointera vers l'entrée de même numéro dans les deux listes privées, mais l'adresse dans l'entrée peut être différente.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=L'espace d'adressage du processeur
| prevText=L'espace d'adressage du processeur
| next=Les méthodes de synchronisation entre processeur et périphériques
| nextText=Les méthodes de synchronisation entre processeur et périphériques
}}
</noinclude>
i8nfu51o75noeoh4ia96jlnq2v2od44
775335
775334
2026-10-16T21:02:56Z
Mewtow
31375
/* Un cas particulier : la z/architecture d'IBM */
775335
wikitext
text/x-wiki
Pour introduire ce chapitre, nous devons faire un rappel sur le concept d{{'}}'''espace d'adressage'''. Pour rappel, un espace d'adressage correspond à l'ensemble des adresses utilisables par le processeur. Par exemple, si je prends un processeur 16 bits, il peut adresser en tout 2^16 = 65536 adresses, l'ensemble de ces adresses forme son espace d'adressage. Intuitivement, on s'attend à ce qu'il y ait correspondance avec les adresses de la mémoire RAM. J'entends par là que l'adresse 1209 de l'espace d'adressage correspond à l'adresse 1209 en mémoire RAM. C'est là une hypothèse parfaitement raisonnable et on voit mal comment ce pourrait ne pas être le cas.
Mais les processeurs modernes utilisent des techniques d{{'}}'''abstraction mémoire''' qui font que ce n'est pas le cas. Avec ces techniques, l'adresse 1209 de l'espace d'adressage correspond en réalité à l'adresse 9999 en mémoire RAM, voire n'est pas en RAM. L'abstraction mémoire fait que l'espace d'adressage regroupe des adresses fictives, qui doivent être traduites en adresses mémoires réelles pour être utilisées. Les adresses de l'espace d'adressage portent le nom d{{'}}'''adresses logiques''', alors que les adresses de la mémoire RAM sont appelées '''adresses physiques'''.
L'intérêt de l'abstraction matérielle n'est pas évident. Aussi, avant de parler de comment l'abstraction mémoire fonctionne, nous allons voir à quoi elle sert. Nous allons voir qu'elle a plusieurs utilisations différentes, qui sont absolument nécessaires sur tous les ordinateurs personnels modernes. Tous les processeurs modernes la prennent en charge, les systèmes d'exploitation coopérent avec le processeur pour l'utiliser au mieux.
==L'abstraction mémoire permet plusieurs fonctionnalités complémentaires==
La fonctionnalité la plus connue est la mémoire virtuelle, dont vous avez peut-être déjà entendu parler, peut-être que le nom vous dit quelque chose. Si ce n'est pas le cas, nous la détailleront dans la suite. Elle permet concrétement à un programme d'utiliser plus de mémoire qu'il n'y a de RAM installé dans un ordinateur, en utilisant le disque dur comme solution de secours. Mais d'autres fonctionnalités moins évidentes sont permises par l'abstraction mémoire. Et la première que nous allons voir est l'abstraction des processus.
: En général, une adresse logique correspond à une seule adresse physique. Mais beaucoup de fonctionnalités avancées ne respectent pas cette règle.
===L'abstraction matérielle des processus===
Les systèmes d'exploitation modernes sont dits multi-tâche, à savoir qu'ils sont capables d'exécuter plusieurs logiciels en même temps. Et ce même si un seul processeur est présent dans l'ordinateur : les logiciels sont alors exécutés à tour de rôle. Toutefois, cela amène un paquet de problèmes qu'il faut résoudre au mieux. Par exemple, les programmes exécutés doivent se partager la mémoire RAM, ce qui ne vient pas sans problèmes. Le problème principal est que les programmes ne doivent pas lire ou écrire dans les données d'un autre, sans quoi on se retrouverait rapidement avec des problèmes. Il faut donc introduire des mécanismes d{{'}}'''isolement des processus''', pour isoler les programmes les uns des autres.
Un de ces mécanismes est l{{'}}'''abstraction matérielle des processus''', une technique qui fait que chaque programme a son propre espace d'adressage. Chaque programme a l'impression d'avoir accès à tout l'espace d'adressage, de l'adresse 0 à l'adresse maximale gérée par le processeur. Évidemment, il s'agit d'une illusion maintenue justement grâce à la traduction d'adresse. Les espaces d'adressage contiennent des adresses logiques, les adresses de la RAM sont des adresses physiques, la nécessité de l'abstraction mémoire est évidente.
Implémenter l'abstraction mémoire peut se faire de plusieurs manières. Mais dans tous les cas, il faut que la correspondance adresse logique - physique change d'un programme à l'autre. Ce qui est normal, vu que les deux processus sont placés à des endroits différents en RAM physique. La conséquence est qu'avec l'abstraction mémoire, une adresse logique correspond à plusieurs adresses physiques. Une même adresse logique dans deux processus différents correspond à deux adresses phsiques différentes, une par processus. Une adresse logique dans un processus correspondra à l'adresse physique X, la même adresse dans un autre processus correspondra à l'adresse Y.
Les adresses physiques qui partagent la même adresse logique sont alors appelées des '''adresses homonymes'''. Le choix de la bonne adresse étant réalisé par un mécanisme matériel et dépend du programme en cours. Le mécanisme pour choisir la bonne adresse dépend du processeur, mais il y en a deux grands types :
* La première consiste à utiliser l'identifiant de processus CPU, vu au chapitre précédent. C'est, pour rappel, un numéro attribué à chaque processus par le processeur. L'identifiant du processus en cours d'exécution est mémorisé dans un registre du processeur. La traduction d'adresse utilise cet identifiant, en plus de l'adresse logique, pour déterminer l'adresse physique.
* La seconde solution mémorise les correspondances adresses logiques-physique dans des tables en mémoire RAM, qui sont différentes pour chaque programme. Les tables sont accédées à chaque accès mémoire, afin de déterminer l'adresse physique.
===Le partage de la mémoire===
L'isolation des processus est très importante sur les systèmes d'exploitation modernes. Cependant, il existe quelques situations où elle doit être contournée ou du moins mise en pause. Les situations sont multiples : gestion de bibliothèques partagées, communication entre processus, usage de ''threads'', etc. Elles impliquent toutes un '''partage de mémoire''', à savoir qu'une portion de mémoire RAM est partagée entre plusieurs programmes. Le partage de mémoire est une sorte de brèche de l'isolation des processus, mais qui est autorisée car elle est utile.
Un cas intéressant est celui des '''bibliothèques partagées'''. Les bibliothèques sont des collections de fonctions regroupées ensemble, dans une seule unité de code. Un programme qui utilise une bibliothèque peut appeler n’importe quelle fonction présente dans la bibliothèque. La bibliothèque peut être simplement inclue dans le programme lui-même, on parle alors de bibliothèques statiques. De telles bibliothèques fonctionnent très bien, mais avec un petit défaut pour les bibliothèques très utilisées : plusieurs programmes qui utilisent la même bibliothèque vont chacun l'inclure dans leur code, ce qui fera doublon.
Pour éviter cela, les OS modernes gèrent des bibliothèques partagées, à savoir qu'un seul exemplaire de la bibliothèque est partagé entre plusieurs programmes. Chaque programme peut exécuter une fonction de la bibliothèque quand il le souhaite, en effectuant un branchement adéquat. Mais cela implique que la bibliothèque soit présente dans l'espace d'adressage du programme en question. Une bibliothèque est donc présente dans plusieurs espaces d'adressage, alors qu'il n'y en a qu'un seul exemplaire en mémoire RAM.
[[File:Ogg vorbis libs and application dia.svg|centre|vignette|upright=2|Exemple de bibliothèques, avec Ogg vorbis.]]
D'autres situations demandent de partager de la mémoire entre deux programmes. Par exemple, les systèmes d'exploitation modernes gèrent nativement des systèmes de '''communication inter-processus''', très utilisés par les programmes modernes pour échanger des données. Et la plupart demandant de partager un bout de mémoire entre processus, même si c'est seulement temporairement. Typiquement, deux processus partagent un intervalle d'adresse où l'un écrit les données à l'autre, l'autre lisant les données envoyées.
Une dernière utilisation de la mémoire partagée est l{{'}}'''accès direct au noyau'''. Sur les systèmes d'exploitations moderne, dans l'espace d'adressage de chaque programme, les adresses hautes sont remplies avec une partie du noyau ! Évidemment, ces adresses sont accessibles uniquement en lecture, pas en écriture. Pas question de modifier le noyau de l'OS ! De plus, il s'agit d'une portion du noyau dont on sait que la consultation ne pose pas de problèmes de sécurité.
Le programme peut lire des données dans cette portion du noyau, mais aussi exécuter les fonctions du noyau qui sont dedans. L'idée est d'éviter des appels systèmes trop fréquents. Au lieu d'effectuer un véritable appel système, avec une interruption logicielle, le programme peut exécuter des appels systèmes simplifiés, de simples appels de fonctions couplés avec un changement de niveau de privilège (passage en espace noyau nécessaire).
[[File:AMD64-canonical--48-bit.png|vignette|Répartition des adresses entre noyau (jaune/orange) et programme (verte), sur les systèmes x86-64 bits, avec des adresses physiques de 48 bits.]]
L'espace d'adressage est donc séparé en deux portions : l'OS d'un côté, le programme de l'autre. La répartition des adresses entre noyau et programme varie suivant l'OS ou le processeur utilisé. Sur les PC x86 32 bits, Linux attribuait 3 gigas pour les programmes et 1 giga pour le noyau, Windows attribuait 2 gigas à chacun. Sur les systèmes x86 64 bits, l'espace d'adressage d'un programme est coupé en trois, comme illustré ci-contre : une partie basse de 2^48 octets, une partie haute de même taille, et un bloc d'adresses invalides entre les deux. Les adresses basses sont utilisées pour le programme, les adresses hautes pour le noyau, il n'y a rien entre les deux.
Avec le partage de mémoire, plusieurs adresses logiques correspondent à la même adresse physique. Tel processus verra la zone de mémoire partagée à l'adresse X, l'autre la verra à l'adresse Y. Mais il s'agira de la même portion de mémoire physique, avec une seule adresse physique. En clair, lorsque deux processus partagent une même zone de mémoire, la zone sera mappées à des adresses logiques différentes. Les adresses logiques sont alors appelées des '''adresses synonymes''', terme qui trahit le fait qu'elles correspondent à la même adresse physique.
===La mémoire virtuelle===
Toutes les adresses ne sont pas forcément occupées par de la mémoire RAM, s'il n'y a pas assez de RAM installée. Par exemple, un processeur 32 bits peut adresser 4 gibioctets de RAM, même si seulement 3 gibioctets sont installés dans l'ordinateur. L'espace d'adressage contient donc 1 gigas d'adresses inutilisées, et il faut éviter ce surplus d'adresses pose problème.
Sans mémoire virtuelle, seule la mémoire réellement installée est utilisable. Si un programme utilise trop de mémoire, il est censé se rendre compte qu'il n'a pas accès à tout l'espace d'adressage. Quand il demandera au système d'exploitation de lui réserver de la mémoire, le système d'exploitation le préviendra qu'il n'y a plus de mémoire libre. Par exemple, si un programme tente d'utiliser 4 gibioctets sur un ordinateur avec 3 gibioctets de mémoire, il ne pourra pas. Pareil s'il veut utiliser 2 gibioctets de mémoire sur un ordinateur avec 4 gibioctets, mais dont 3 gibioctets sont déjà utilisés par d'autres programmes. Dans les deux cas, l'illusion tombe à plat.
Les techniques de '''mémoire virtuelle''' font que l'espace d'adressage est utilisable au complet, même s'il n'y a pas assez de mémoire installée dans l'ordinateur ou que d'autres programmes utilisent de la RAM. Par exemple, sur un processeur 32 bits, le programme aura accès à 4 gibioctets de RAM, même si d'autres programmes utilisent la RAM, même s'il n'y a que 2 gibioctets de RAM d'installés dans l'ordinateur.
Pour cela, on utilise une partie des mémoires de masse (disques durs) d'un ordinateur en remplacement de la mémoire physique manquante. Le système d'exploitation crée sur le disque dur un fichier, appelé le ''swapfile'' ou '''fichier de ''swap''''', qui est utilisé comme mémoire RAM supplémentaire. Il mémorise le surplus de données et de programmes qui ne peut pas être mis en mémoire RAM.
[[File:Vm1.png|centre|vignette|upright=2.0|Mémoire virtuelle et fichier de Swap.]]
Une technique naïve de mémoire virtuelle serait la suivante. Avant de l'aborder, précisons qu'il s'agit d'une technique abordée à but pédagogique, mais qui n'est implémentée nulle part tellement elle est lente et inefficace. Un espace d'adressage de 4 gigas ne contient que 3 gigas de RAM, ce qui fait 1 giga d'adresses inutilisées. Les accès mémoire aux 3 gigas de RAM se font normalement, mais l'accès aux adresses inutilisées lève une exception matérielle "Memory Unavailable". La routine d'interruption de cette exception accède alors au ''swapfile'' et récupère les données associées à cette adresse. La mémoire virtuelle est alors émulée par le système d'exploitation.
Le défaut de cette méthode est que l'accès au giga manquant est toujours très lent, parce qu'il se fait depuis le disque dur. D'autres techniques de mémoire virtuelle logicielle font beaucoup mieux, mais nous allons les passer sous silence, vu qu'on peut faire mieux, avec l'aide du matériel.
L'idée est de charger les données dont le programme a besoin dans la RAM, et de déplacer les autres sur le disque dur. Par exemple, imaginons la situation suivante : un programme a besoin de 4 gigas de mémoire, mais ne dispose que de 2 gigas de mémoire installée. On peut imaginer découper l'espace d'adressage en 2 blocs de 2 gigas, qui sont chargés à la demande. Si le programme accède aux adresses basses, on charge les 2 gigas d'adresse basse en RAM. S'il accède aux adresses hautes, on charge les 2 gigas d'adresse haute dans la RAM après avoir copié les adresses basses sur le ''swapfile''.
On perd du temps dans les copies de données entre RAM et ''swapfile'', mais on gagne en performance vu que tous les accès mémoire se font en RAM. Du fait de la localité temporelle, le programme utilise les données chargées depuis le swapfile durant un bon moment avant de passer au bloc suivant. La RAM est alors utilisée comme une sorte de cache alors que les données sont placées dans une mémoire fictive représentée par l'espace d'adressage et qui correspond au disque dur.
Mais avec cette technique, la correspondance entre adresses du programme et adresses de la RAM change au cours du temps. Les adresses de la RAM correspondent d'abord aux adresses basses, puis aux adresses hautes, et ainsi de suite. On a donc besoin d'abstraction mémoire. Les correspondances entre adresse logique et physique peuvent varier avec le temps, ce qui permet de déplacer des données de la RAM vers le disque dur ou inversement. Une adresse logique peut correspondre à une adresse physique, ou bien à une donnée swappée sur le disque dur. C'est l'unité de traduction d'adresse qui se charge de faire la différence. Si une correspondance entre adresse logique et physique est trouvée, elle l'utilise pour traduire les adresses. Si aucune correspondance n'est trouvée, alors elle laisse la main au système d'exploitation pour charger la donnée en RAM. Une fois la donnée chargée en RAM, les correspondances entre adresse logique et physiques sont modifiées de manière à ce que l'adresse logique pointe vers la donnée chargée.
===L'extension d'adressage===
Une autre fonctionnalité rendue possible par l'abstraction mémoire est l{{'}}'''extension d'adressage'''. Elle permet d'utiliser plus de mémoire que l'espace d'adressage ne le permet. Par exemple, utiliser 7 gigas de RAM sur un processeur 32 bits, dont l'espace d'adressage ne gère que 4 gigas. L'extension d'adresse est l'exact inverse de la mémoire virtuelle. La mémoire virtuelle sert quand on a moins de mémoire que d'adresses, l'extension d'adresse sert quand on a plus de mémoire que d'adresses.
Il y a quelques chapitres, nous avions vu que c'est possible via la commutation de banques. Mais l'abstraction mémoire est une méthode alternative. Que ce soit avec la commutation de banques ou avec l'abstraction mémoire, les adresses envoyées à la mémoire doivent être plus longues que les adresses gérées par le processeur. La différence est que l'abstraction mémoire étend les adresses d'une manière différente.
Une implémentation possible de l'extension d'adressage fait usage de l'abstraction matérielle des processus. Chaque processus a son propre espace d'adressage, mais ceux-ci sont placés à des endroits différents dans la mémoire physique. Par exemple, sur un ordinateur avec 16 gigas de RAM, mais un espace d'adressage de 2 gigas, on peut remplir la RAM en lançant 8 processus différents et chaque processus aura accès à un bloc de 2 gigas de RAM, pas plus, il ne peut pas dépasser cette limite. Ainsi, chaque processus est limité par son espace d'adressage, mais on remplit la mémoire avec plusieurs processus, ce qui compense. Il s'agit là de l'implémentation la plus simple, qui a en plus l'avantage d'avoir la meilleure compatibilité logicielle. De simples changements dans le système d'exploitation suffisent à l'implémenter.
[[File:Extension de l'espace d'adressage.png|centre|vignette|upright=1.5|Extension de l'espace d'adressage]]
Un autre implémentation donne plusieurs espaces d'adressage différents à chaque processus, et a donc accès à autant de mémoire que permis par la somme de ces espaces d'adressage. Par exemple, sur un ordinateur avec 16 gigas de RAM et un espace d'adressage de 4 gigas, un programme peut utiliser toute la RAM en utilisant 4 espaces d'adressage distincts. On passe d'un espace d'adressage à l'autre en changeant la correspondance adresse logique-physique. L'inconvénient est que la compatibilité logicielle est assez mauvaise. Modifier l'OS ne suffit pas, les programmeurs doivent impérativement concevoir leurs programmes pour qu'ils utilisent explicitement plusieurs espaces d'adressage.
Les deux implémentations font usage des adresses logiques homonymes, mais à l'intérieur d'un même processus. Pour rappel, cela veut dire qu'une adresse logique correspond à des adresses physiques différentes. Rien d'étonnant vu qu'on utilise plusieurs espaces d'adressage, comme pour l'abstraction des processus, sauf que cette fois-ci, on a plusieurs espaces d'adressage par processus. Prenons l'exemple où on a 8 gigas de RAM sur un processeur 32 bits, dont l'espace d'adressage ne gère que 4 gigas. L'idée est qu'une adresse correspondra à une adresse dans les premiers 4 gigas, ou dans les seconds 4 gigas. L'adresse logique X correspondra d'abord à une adresse physique dans les premiers 4 gigas, puis à une adresse physique dans les seconds 4 gigas.
===La protection mémoire===
La '''protection mémoire''' regroupe des techniques très différentes les unes des autres, qui visent à améliorer la sécurité des programmes et des systèmes d'exploitation. Elles visent à empêcher de lire, d'écrire ou d'exécuter certaines portions de mémoire. Sans elle, les programmes peuvent techniquement lire ou écrire les données des autres, ce qui causent des situations non-prévues par le programmeur, avec des conséquences qui vont d'un joli plantage à des failles de sécurité dangereuses.
La première technique de protection mémoire est l{{'}}'''isolation des processus''', qu'on a vue plus haut. Elle garantit que chaque programme n'a accès qu'à certaines portions dédiées de la mémoire et rend le reste de la mémoire inaccessible en lecture et en écriture. Le système d'exploitation attribue à chaque programme une ou plusieurs portions de mémoire rien que pour lui, auquel aucun autre programme ne peut accéder. Un tel programme, isolé des autres, s'appelle un '''processus''', d'où le nom de cet objectif. Toute tentative d'accès à une partie de la mémoire non autorisée déclenche une exception matérielle (rappelez-vous le chapitre sur les interruptions) qui est traitée par une routine du système d'exploitation. Généralement, le programme fautif est sauvagement arrêté et un message d'erreur est affiché à l'écran.
La '''protection de l'espace exécutable''' empêche d’exécuter quoique ce soit provenant de certaines zones de la mémoire. En effet, certaines portions de la mémoire sont censées contenir uniquement des données, sans aucun programme ou code exécutable. Cependant, des virus informatiques peuvent se cacher dedans et d’exécuter depuis celles-ci. Ou encore, des failles de sécurités peuvent permettre à un attaquant d'injecter du code exécutable malicieux dans des données, ce qui peut lui permettre de lire les données manipulées par un programme, prendre le contrôle de la machine, injecter des virus, ou autre. Pour éviter cela, le système d'exploitation peut marquer certaines zones mémoire comme n'étant pas exécutable. Toute tentative d’exécuter du code localisé dans ces zones entraîne la levée d'une exception ou d'une erreur et le système d'exploitation réagit en conséquence. Là encore, le processeur doit détecter les exécutions non autorisées.
D'autres méthodes de protection mémoire visent à limiter des actions dangereuses. Pour cela, le processeur et l'OS gèrent des '''droits d'accès''', qui interdisent certaines actions pour des programmes non-autorisés. Lorsqu'on exécute une opération interdite, le système d’exploitation et/ou le processeur réagissent en conséquence. La première technique de ce genre n'est autre que la séparation entre espace noyau et utilisateur, vue dans le chapitre sur les interruptions. Mais il y en a d'autres, comme nous le verrons dans ce chapitre.
==La MMU==
La traduction des adresses logiques en adresses physiques se fait par un circuit spécialisé, appelé la '''''Memory Management Unit''''' (MMU). De nos jours, elle est intégrée dans le processeur, à la suite de l'unité mémoire. Mais il a existé des processeurs avec une MMU externe, soudée sur la carte mère.
La traduction des adresses logiques en adresses physiques demande d'accéder à des données sont en mémoire RAM, qui sont gérés par le système d'exploitation. Aussi, les processeurs modernes incorporent des mémoires caches appelées des '''''Translation Lookaside Buffers''''', ou encore TLB. Nous nous pouvons pas parler des TLB pour le moment, car nous n'avons pas encore abordé le chapitre sur les mémoires caches, mais un chapitre entier sera dédié aux TLB d'ici peu.
[[File:MMU principle updated.png|centre|vignette|upright=2|MMU.]]
===Les MMU intégrées au processeur===
D'ordinaire, la MMU est intégrée au processeur. Et elle peut l'être de deux manières. La première en fait un circuit séparé, relié au bus d'adresse. La seconde fusionne la MMU avec l'unité de calcul d'adresse. La première solution est surtout utilisée avec une technique d'abstraction mémoire appelée la pagination, alors que l'autre l'est avec une autre méthode appelée la segmentation. La raison est que la traduction d'adresse avec la segmentation est assez simple : elle demande d'additionner le contenu d'un registre avec l'adresse logique, ce qui est le genre de calcul qu'une unité de calcul d'adresse sait déjà faire. La fusion est donc assez évidente.
Pour donner un exemple, l'Intel 8086 fusionnait l'unité de calcul d'adresse et la MMU. Précisément, il utilisait un même additionneur pour incrémenter le ''program counter'' et effectuer des calculs d'adresse liés à la segmentation. Il aurait été logique d'ajouter les pointeurs de pile avec, mais ce n'était pas possible. La raison est que le pointeur de pile ne peut pas être envoyé directement sur le bus d'adresse, vu qu'il doit passer par une phase de traduction en adresse physique liée à la segmentation.
[[File:80186 arch.png|centre|vignette|upright=2|Intel 8086, microarchitecture.]]
===Les MMU séparées du processeur, sur la carte mère===
Avant d'être intégrées au processeur, les MMU étaient des circuits séparés, placés sur la carte mère, entre le processeur et la mémoire. Elle étaient connectées en plein milieu du bus d'adresse. A savoir que le bus d'adresse entrait dans la MMU et en ressortait. Le processeur envoyait des adresses à la MMU, qui renvoyait une adresse physique. La MMU était facultative, à savoir que le système pouvait fonctionner sans MMU d'installée, mais l'abstraction mémoire n’était alors pas disponible.
Par exemple, les processeurs Motorola 68000 et 68010 pouvaient être combinés avec une MMU de type Motorola 68451. Elle supportait des versions simplifiées de la segmentation et de la pagination. Au minimum, elle ajoutait un support de la protection mémoire contre certains accès non-autorisés. La gestion de la mémoire virtuelle proprement dit n'était possible que si le processeur utilisé était un Motorola 68010, en raison de la manière dont le 68000 gérait ses accès mémoire. La MMU 68451 gérait un espace d'adressage de 16 mébioctets, découpé en maximum 32 pages/segments. On pouvait dépasser cette limite de 32 segments/pages en combinant plusieurs 68451.
Le Motorola 68851 était une MMU qui était prévue pour fonctionner de paire avec le Motorola 68020. Elle gérait la pagination pour un espace d'adressage de 32 bits.
Les processeurs suivants, les 68030, 68040, et 68060, avaient une MMU interne au processeur.
==La relocation matérielle==
Pour rappel, les systèmes d'exploitation moderne permettent de lancer plusieurs programmes en même temps et les laissent se partager la mémoire. Dans le cas le plus simple, qui n'est pas celui des OS modernes, le système d'exploitation découpe la mémoire en blocs d'adresses contiguës qui sont appelés des '''segments''', ou encore des ''partitions mémoire''. Les segments correspondent à un bloc de mémoire RAM. C'est-à-dire qu'un segment de 259 mébioctets sera un segment continu de 259 mébioctets dans la mémoire physique comme dans la mémoire logique. Dans ce qui suit, un segment contient un programme en cours d'exécution, comme illustré ci-dessous.
[[File:CPT Memory Addressable.svg|centre|vignette|upright=2|Espace d'adressage segmenté.]]
Le système d'exploitation mémorise la position de chaque segment en mémoire, ainsi que d'autres informations annexes. Le tout est regroupé dans la '''table de segment''', un tableau dont chaque case est attribuée à un programme/segment. La table des segments est un tableau numéroté, chaque segment ayant un numéro qui précise sa position dans le tableau. Chaque case, chaque entrée, contient un '''descripteur de segment''' qui regroupe plusieurs informations sur le segment : son adresse de base, sa taille, diverses informations.
===La relocation avec la relocation matérielle : le registre de base===
Un segment peut être placé n'importe où en RAM physique et sa position en RAM change à chaque exécution. Le programme est chargé à une adresse, celle du début du segment, qui change à chaque chargement du programme. Et toutes les adresses utilisées par le programme doivent être corrigées lors du chargement du programme, généralement par l'OS. Cette correction s'appelle la '''relocation''', et elle consiste à ajouter l'adresse de début du segment à chaque adresse manipulée par le programme.
[[File:Relocation assistée par matériel.png|centre|vignette|upright=2.5|Relocation.]]
La relocation matérielle fait que la relocation est faite par le processeur, pas par l'OS. La relocation est intégrée dans le processeur par l'intégration d'un registre : le '''registre de base''', aussi appelé '''registre de relocation'''. Il mémorise l'adresse à laquelle commence le segment, la première adresse du programme. Pour effectuer la relocation, le processeur ajoute automatiquement l'adresse de base à chaque accès mémoire, en allant la chercher dans le registre de relocation.
[[File:Registre de base de segment.png|centre|vignette|upright=2|Registre de base de segment.]]
Le processeur s'occupe de la relocation des segments et le programme compilé n'en voit rien. Pour le dire autrement, les programmes manipulent des adresses logiques, qui sont traduites par le processeur en adresses physiques. La traduction se fait en ajoutant le contenu du registre de relocation à l'adresse logique. De plus, cette méthode fait que chaque programme a son propre espace d'adressage.
[[File:CPU created logical address presentation.png|centre|vignette|upright=2|Traduction d'adresse avec la relocation matérielle.]]
Le système d'exploitation mémorise les adresses de base pour chaque programme, dans la table des segments. Le registre de base est mis à jour automatiquement lors de chaque changement de segment. Pour cela, le registre de base est accessible via certaines instructions, accessibles en espace noyau, plus rarement en espace utilisateur. Le registre de segment est censé être adressé implicitement, vu qu'il est unique. Si ce n'est pas le cas, il est possible d'écrire dans ce registre de segment, qui est alors adressable.
===La protection mémoire avec la relocation matérielle : le registre limite===
Sans restrictions supplémentaires, la taille maximale d'un segment est égale à la taille complète de l'espace d'adressage. Sur les processeurs 32 bits, un segment a une taille maximale de 2^32 octets, soit 4 gibioctets. Mais il est possible de limiter la taille du segment à 2 gibioctets, 1 gibioctet, 64 Kibioctets, ou toute autre taille. La limite est définie lors de la création du segment, mais elle peut cependant évoluer au cours de l'exécution du programme, grâce à l'allocation mémoire.
Le processeur vérifie à chaque accès mémoire que celui-ci se fait bien dans le segment, qu'il ne déborde pas en-dehors. C'est possible qu'une adresse calculée sorte du segment, à la suite d'un bug ou d'une erreur de programmation, voire pire. Et le processeur doit éviter de tels '''débordements de segments'''.
A chaque accès mémoire, le processeur compare l'adresse accédée et vérifie qu'elle est bien dans le segment. Pour cela, il y a deux solutions. La première part du principe que le segment est placé en mémoire entre l'adresse de base et l'adresse limite. Il suffit de mémoriser l'adresse limite, l'adresse physique à ne pas dépasser. Une autre solution mémorise la taille du segment. La table des segments doit donc mémoriser, en plus de l'adresse de base : soit l'adresse maximale du segment, soit la taille du segment. D'autres informations peuvent être ajoutées, comme on le verra plus tard, mais cela complexifie la table des segments.
De plus, le processeur se voit ajouter un '''registre limite''', qui mémorise soit la taille du segment, soit l'adresse limite. Les deux registres, base et limite, sont utilisés pour vérifier si un programme qui lit/écrit de la mémoire en-dehors de son segment attitré : au-delà pour le registre limite, en-deça pour le registre de base. Le processeur vérifie pour chaque accès mémoire ne déborde pas au-delà du segment qui lui est allouée, ce qui n'arrive que si l'adresse d'accès dépasse la valeur du registre limite. Pour les accès en-dessous du segment, il suffit de vérifier si l'addition de relocation déborde, tout débordement signifiant erreur de protection mémoire.
[[File:Registre limite.png|centre|vignette|upright=2|Registre limite]]
Utiliser la taille du segment a de nombreux avantages. L'un d'entre eux se manifeste quand on déplace un segment en mémoire RAM. Le descripteur doit alors être mis à jour, et c'est plus facile quand on utilise la taille du segment. Si on utilise l'adresse limite, il faut mettre à jour à la fois l'adresse de base et l'adresse limite, dans le descripteur. En utilisant la taille, seule l'adresse de base doit être modifiée, vu que le segment n'a pas changé de taille. Un autre avantage est lié aux performances, mais nous devons faire un détour pour le comprendre.
La taille du segment est équivalent à l'adresse logique maximale possible. Par exemple, si un segment fait 256 octets, les adresses logiques possibles vont de 0 à 255, 256 est donc à la fois la taille du segment et l'adresse logique à partir de laquelle on déborde du segment. Et cela marche si on remplace 256 par n'importe quelle valeur : vu que le segment commence à l'adresse 0, sa taille en octets indique l'adresse de dépassement. Interpréter la taille du segment comme une adresse logique fait que les tests avec le registre limite sont plus performants, voyons pourquoi.
En utilisant l'adresse physique limite, on doit faire la relocation, puis comparer l'adresse calculée avec l'adresse limite. Le calcul d'adresse doit se faire avant la vérification. En utilisant la taille, on doit comparer l'adresse logique avec la taille du segment. On peut alors faire le test de débordement avant ou pendant la relocation. Les deux peuvent être faits en parallèle, dans deux circuits distincts, ce qui améliore un peu le temps d'un accès mémoire. Quelques processeurs en ont profité, mais on verra cela dans la section sur la segmentation.
[[File:Comparaison entre adresse limite physique et logique.png|centre|vignette|upright=2|Comparaison entre adresse limite physique et logique]]
Les registres de base et limite sont altérés uniquement par le système d'exploitation et ne sont accessibles qu'en espace noyau. Lorsque le système d'exploitation charge un programme, ou reprend son exécution, il charge les adresses de début/fin du segment dans ces registres. D'ailleurs, ces deux registres doivent être sauvegardés et restaurés lors de chaque interruption. Par contre, et c'est assez évident, ils ne le sont pas lors d'un appel de fonction. Cela fait une différence de plus entre interruption et appels de fonctions.
: Il faut noter que le registre limite et le registre de base sont parfois fusionnés en un seul registre, qui contient un descripteur de segment tout entier.
Pour information, la relocation matérielle avec un registre limite a été implémentée sur plusieurs processeurs assez anciens, notamment sur les anciens supercalculateurs de marque CDC. Un exemple est le fameux CDC 6600, qui implémentait cette technique.
===La mémoire virtuelle avec la relocation matérielle===
Il est possible d'implémenter la mémoire virtuelle avec la relocation matérielle. Pour cela, il faut swapper des segments entiers sur le disque dur. Les segments sont placés en mémoire RAM et leur taille évolue au fur et à mesure que les programmes demandent du rab de mémoire RAM. Lorsque la mémoire est pleine, ou qu'un programme demande plus de mémoire que disponible, des segments entiers sont sauvegardés dans le ''swapfile'', pour faire de la place.
Faire ainsi de demande juste de mémoriser si un segment est en mémoire RAM ou non, ainsi que la position des segments swappés dans le ''swapfile''. Pour cela, il faut modifier la table des segments, afin d'ajouter un '''bit de swap''' qui précise si le segment en question est swappé ou non. Lorsque le système d'exploitation veut swapper un segment, il le copie dans le ''swapfile'' et met ce bit à 1. Lorsque l'OS recharge ce segment en RAM, il remet ce bit à 0. La gestion de la position des segments dans le ''swapfile'' est le fait d'une structure de données séparée de la table des segments.
L'OS exécute chaque programme l'un après l'autre, à tour de rôle. Lorsque le tour d'un programme arrive, il consulte la table des segments pour récupérer les adresses de base et limite, mais il vérifie aussi le bit de swap. Si le bit de swap est à 0, alors l'OS se contente de charger les adresses de base et limite dans les registres adéquats. Mais sinon, il démarre une routine d'interruption qui charge le segment voulu en RAM, depuis le ''swapfile''. C'est seulement une fois le segment chargé que l'on connait son adresse de base/limite et que le chargement des registres de relocation peut se faire.
Un défaut évident de cette méthode est que l'on swappe des programmes entiers, qui sont généralement assez imposants. Les segments font généralement plusieurs centaines de mébioctets, pour ne pas dire plusieurs gibioctets, à l'époque actuelle. Ils étaient plus petits dans l'ancien temps, mais la mémoire était alors plus lente. Toujours est-il que la copie sur le disque dur des segments est donc longue, lente, et pas vraiment compatible avec le fait que les programmes s'exécutent à tour de rôle. Et ca explique pourquoi la relocation matérielle n'est presque jamais utilisée avec de la mémoire virtuelle.
===L'extension d'adressage avec la relocation matérielle===
Passons maintenant à la dernière fonctionnalité implémentable avec la traduction d'adresse : l'extension d'adressage. Elle permet d'utiliser plus de mémoire que ne le permet l'espace d'adressage. Par exemple, utiliser plus de 64 kibioctets de mémoire sur un processeur 16 bits. Pour cela, les adresses envoyées à la mémoire doivent être plus longues que les adresses gérées par le processeur.
L'extension des adresses se fait assez simplement avec la relocation matérielle : il suffit que le registre de base soit plus long. Prenons l'exemple d'un processeur aux adresses de 16 bits, mais qui est reliée à un bus d'adresse de 24 bits. L'espace d'adressage fait juste 64 kibioctets, mais le bus d'adresse gère 16 mébioctets de RAM. On peut utiliser les 16 mébioctets de RAM à une condition : que le registre de base fasse 24 bits, pas 16.
Un défaut de cette approche est qu'un programme ne peut pas utiliser plus de mémoire que ce que permet l'espace d'adressage. Mais par contre, on peut placer chaque programme dans des portions différentes de mémoire. Imaginons par exemple que l'on ait un processeur 16 bits, mais un bus d'adresse de 20 bits. Il est alors possible de découper la mémoire en 16 blocs de 64 kibioctets, chacun attribué à un segment/programme, qu'on sélectionne avec les 4 bits de poids fort de l'adresse. Il suffit de faire démarrer les segments au bon endroit en RAM, et cela demande juste que le registre de base le permette. C'est une sorte d'émulation de la commutation de banques.
==La segmentation en mode réel des processeurs x86==
Avant de passer à la suite, nous allons voir la technique de segmentation de l'Intel 8086, un des tout premiers processeurs 16 bits. Il s'agissait d'une forme très simple de segmentation, sans aucune forme de protection mémoire, ni même de mémoire virtuelle, ce qui le place à part des autres formes de segmentation. Il s'agit d'une amélioration de la relocation matérielle, qui avait pour but de permettre d'utiliser plus de 64 kibioctets de mémoire, ce qui était la limite maximale sur les processeurs 16 bits de l'époque.
Par la suite, la segmentation s'améliora et ajouta un support complet de la mémoire virtuelle et de la protection mémoire. L'ancienne forme de segmentation fut alors appelé le '''mode réel''', et la nouvelle forme de segmentation fut appelée le '''mode protégé'''. Le mode protégé rajoute la protection mémoire, en ajoutant des registres limite et une gestion des droits d'accès aux segments, absents en mode réel. De plus, il ajoute un support de la mémoire virtuelle grâce à l'utilisation d'une des segments digne de ce nom, table qui est absente en mode réel ! Pour le moment, voyons le mode réel.
===Les segments en mode réel===
[[File:Typical computer data memory arrangement.png|vignette|upright=0.5|Typical computer data memory arrangement]]
La segmentation en mode réel sépare la pile, le tas, le code machine et les données constantes dans quatre segments distincts.
* Le segment '''''text''''', qui contient le code machine du programme, de taille fixe.
* Le segment '''''data''''' contient des données de taille fixe qui occupent de la mémoire de façon permanente, des constantes, des variables globales, etc.
* Le segment pour la '''pile''', de taille variable.
* le reste est appelé le '''tas''', de taille variable.
Un point important est que sur ces processeurs, il n'y a pas de table des segments proprement dit. Chaque programme gére de lui-même les adresses de base des segments qu'il manipule. Il n'est en rien aidé par une table des segments gérée par le système d'exploitation.
===Les registres de segments en mode réel===
Chaque segment subit la relocation indépendamment des autres. Pour cela, le processeur intégre plusieurs registres de base, un par segment. Notons que cette solution ne marche que si le nombre de segments par programme est limité, à une dizaine de segments tout au plus. Les processeurs x86 utilisaient cette méthode, et n'associaient que 4 à 6 registres de segments par programme.
Les processeurs 8086 et le 286 avaient quatre registres de segment : un pour le code, un autre pour les données, et un pour la pile, le quatrième étant un registre facultatif laissé à l'appréciation du programmeur. Ils sont nommés CS (''code segment''), DS (''data segment''), SS (''Stack segment''), et ES (''Extra segment''). Le 386 rajouta deux registres, les registres FS et GS, qui sont utilisés pour les segments de données. Les processeurs post-386 ont donc 6 registres de segment.
Les registres CS et SS sont adressés implicitement, en fonction de l'instruction exécutée. Les instructions de la pile manipulent le segment associé à la pile, le chargement des instructions se fait dans le segment de code, les instructions arithmétiques et logiques vont chercher leurs opérandes sur le tas, etc. Et donc, toutes les instructions sont chargées depuis le segment pointé par CS, les instructions de gestion de la pile (PUSH et POP) utilisent le segment pointé par SS.
Les segments DS et ES sont, eux aussi, adressés implicitement. Pour cela, les instructions LOAD/STORE sont dupliquées : il y a une instruction LOAD pour le segment DS, une autre pour le segment ES. D'autres instructions lisent leurs opérandes dans un segment par défaut, mais on peut changer ce choix par défaut en précisant le segment voulu. Un exemple est celui de l'instruction CMPSB, qui compare deux octets/bytes : le premier est chargé depuis le segment DS, le second depuis le segment ES.
Un autre exemple est celui de l'instruction MOV avec un opérande en mémoire. Elle lit l'opérande en mémoire depuis le segment DS par défaut. Il est possible de préciser le segment de destination si celui-ci n'est pas DS. Par exemple, l'instruction MOV [A], AX écrit le contenu du registre AX dans l'adresse A du segment DS. Par contre, l'instruction MOV ES:[A], copie le contenu du registre AX das l'adresse A, mais dans le segment ES.
===La traduction d'adresse en mode réel===
La segmentation en mode réel a pour seul but de permettre à un programme de dépasser la limite des 64 KB autorisée par les adresses de 16 bits. L'idée est que chaque segment a droit à son propre espace de 64 KB. On a ainsi 64 Kb pour le code machine, 64 KB pour la pile, 64 KB pour un segment de données, etc. Les registres de segment mémorisaient la base du segment, les adresses calculées par l'ALU étant des ''offsets''. Ce sont tous des registres de 16 bits, mais ils ne mémorisent pas des adresses physiques de 16 bits, comme nous allons le voir.
[[File:Table des segments dans un banc de registres.png|centre|vignette|upright=2|Table des segments dans un banc de registres.]]
L'Intel 8086 utilisait des adresses de 20 bits, ce qui permet d'adresser 1 mébioctet de RAM. Vous pouvez vous demander comment on peut obtenir des adresses de 20 bits alors que les registres de segments font tous 16 bits ? Cela tient à la manière dont sont calculées les adresses physiques. Le registre de segment n'est pas additionné tel quel avec le décalage : à la place, le registre de segment est décalé de 4 rangs vers la gauche. Le décalage de 4 rangs vers la gauche fait que chaque segment a une adresse qui est multiple de 16. Le fait que le décalage soit de 16 bits fait que les segments ont une taille de 64 kibioctets.
{|class="wikitable"
|-
| <code> </code><code style="background:#DED">0000 0110 1110 1111</code><code>0000</code>
| Registre de segment -
| 16 bits, décalé de 4 bits vers la gauche
|-
| <code>+ </code><code style="background:#DDF">0001 0010 0011 0100</code>
| Décalage/''Offset''
| 16 bits
|-
| colspan="3" |
|-
| <code> </code><code style="background:#FDF">0000 1000 0001 0010 0100</code>
| Adresse finale
| 20 bits
|}
Vous aurez peut-être remarqué que le calcul peut déborder, dépasser 20 bits. Mais nous reviendrons là-dessus plus bas. L'essentiel est que la MMU pour la segmentation en mode réel se résume à quelques registres et des additionneurs/soustracteurs.
Un exemple est l'Intel 8086, un des tout premier processeur Intel. Le processeur était découpé en deux portions : l'interface mémoire et le reste du processeur. L'interface mémoire est appelée la '''''Bus Interface Unit''''', et le reste du processeur est appelé l{{'}}'''''Execution Unit'''''. L'interface mémoire contenait les registres de segment, au nombre de 4, ainsi qu'un additionneur utilisé pour traduire les adresses logiques en adresses physiques. Elle contenait aussi une file d'attente où étaient préchargées les instructions.
Sur le 8086, la MMU est fusionnée avec les circuits de gestion du ''program counter''. Les registres de segment sont regroupés avec le ''program counter'' dans un même banc de registres, et un additionneur unique est utilisé à la fois à incrémenter le ''program counter'' et pour gérer la segmentation. L'additionneur est donc mutualisé entre segmentation et ''program counter''. En somme, il n'y a pas vraiment de MMU dédiée, mais un super-circuit en charge du Fetch et de la mémoire virtuelle, ainsi que du préchargement des instructions. Nous en reparlerons au chapitre suivant.
[[File:80186 arch.png|centre|vignette|upright=2.5|Architecture du 8086, du 80186 et de ses variantes.]]
La MMU du 286 était fusionnée avec l'unité de calcul d'adresse. Elle contient les registres de segments, un comparateur pour détecter les accès hors-segment, et plusieurs additionneurs. Il y a un additionneur pour les calculs d'adresse proprement dit, suivi d'un additionneur pour la relocation.
[[File:Intel i80286 arch.svg|centre|vignette|upright=3|Intel i80286 arch]]
===La segmentation en mode réel accepte plusieurs segments de code/données===
Les programmes peuvent parfaitement répartir leur code machine dans plusieurs segments de code. La limite de 64 KB par segment est en effet assez limitante, et il n'était pas rare qu'un programme stocke son code dans deux ou trois segments. Il en est de même avec les données, qui peuvent être réparties dans deux ou trois segments séparés. La seule exception est la pile : elle est forcément dans un segment unique et ne peut pas dépasser 64 KB.
Pour gérer plusieurs segments de code/donnée, il faut changer de segment à la volée suivant les besoins, en modifiant les registres de segment. Il s'agit de la technique de '''commutation de segment'''. Pour cela, tous les registres de segment, à l'exception de CS, peuvent être altérés par une instruction d'accès mémoire, soit avec une instruction MOV, soit en y copiant le sommet de la pile avec une instruction de dépilage POP. L'absence de sécurité fait que la gestion de ces registres est le fait du programmeur, qui doit redoubler de prudence pour ne pas faire n'importe quoi.
Pour le code machine, le répartir dans plusieurs segments posait des problèmes au niveau des branchements. Si la plupart des branchements sautaient vers une instruction dans le même segment, quelques rares branchements sautaient vers du code machine dans un autre segment. Intel avait prévu le coup et disposait de deux instructions de branchement différentes pour ces deux situations : les '''''near jumps''''' et les '''''far jumps'''''. Les premiers sont des branchements normaux, qui précisent juste l'adresse à laquelle brancher, qui correspond à la position de la fonction dans le segment. Les seconds branchent vers une instruction dans un autre segment, et doivent préciser deux choses : l'adresse de base du segment de destination, et la position de la destination dans le segment. Le branchement met à jour le registre CS avec l'adresse de base, avant de faire le branchement. Ces derniers étaient plus lents, car on n'avait pas à changer de segment et mettre à jour l'état du processeur.
Il y avait la même pour l'instruction d'appel de fonction, avec deux versions de cette instruction. La première version, le '''''near call''''' est un appel de fonction normal, la fonction appelée est dans le segment en cours. Avec la seconde version, le '''''far call''''', la fonction appelée est dans un segment différent. L'instruction a là aussi besoin de deux opérandes : l'adresse de base du segment de destination, et la position de la fonction dans le segment. Un ''far call'' met à jour le registre CS avec l'adresse de base, ce qui fait que les ''far call'' sont plus lents que les ''near call''. Il existe aussi la même chose, pour les instructions de retour de fonction, avec une instruction de retour de fonction normale et une instruction de retour qui renvoie vers un autre segment, qui sont respectivement appelées '''''near return''''' et '''''far return'''''. Là encore, il faut préciser l'adresse du segment de destination dans le second cas.
La même chose est possible pour les segments de données. Sauf que cette fois-ci, ce sont les pointeurs qui sont modifiés. pour rappel, les pointeurs sont, en programmation, des variables qui contiennent des adresses. Lors de la compilation, ces pointeurs sont placés soit dans un registre, soit dans les instructions (adressage absolu), ou autres. Ici, il existe deux types de pointeurs, appelés '''''near pointer''''' et '''''far pointer'''''. Vous l'avez deviné, les premiers sont utilisés pour localiser les données dans le segment en cours d'utilisation, alors que les seconds pointent vers une donnée dans un autre segment. Là encore, la différence est que le premier se contente de donner la position dans le segment, alors que les seconds rajoutent l'adresse de base du segment. Les premiers font 16 bits, alors que les seconds en font 32 : 16 bits pour l'adresse de base et 16 pour l{{'}}''offset''.
===L'occupation de l'espace d'adressage par les segments===
Nous venons de voir qu'un programme pouvait utiliser plus de 4-6 segments, avec la commutation de segment. Mais d'autres programmes faisaient l'inverse, à savoir qu'ils se débrouillaient avec seulement 1 ou 2 segments. Suivant le nombre de segments utilisés, la configuration des registres n'était pas la même. Les configurations possibles sont appelées des ''modèle mémoire'', et il y en a en tout 6. En voici la liste :
{| class="wikitable"
|-
! Modèle mémoire !! Configuration des segments !! Configuration des registres || Pointeurs utilisés || Branchements utilisés
|-
| Tiny* || Segment unique pour tout le programme || CS=DS=SS || ''near'' uniquement || ''near'' uniquement
|-
| Small || Segment de donnée séparé du segment de code, pile dans le segment de données || DS=SS || ''near'' uniquement || ''near'' uniquement
|-
| Medium || Plusieurs segments de code unique, un seul segment de données || CS, DS et SS sont différents || ''near'' et ''far'' || ''near'' uniquement
|-
| Compact || Segment de code unique, plusieurs segments de données || CS, DS et SS sont différents || ''near'' uniquement || ''near'' et ''far''
|-
| Large || Plusieurs segments de code, plusieurs segments de données || CS, DS et SS sont différents || ''near'' et ''far'' || ''near'' et ''far''
|}
Un programme est censé utiliser maximum 4-6 segments de 64 KB, ce qui permet d'adresser maximum 64 * 6 = 384 KB de RAM, soit bien moins que le mébioctet de mémoire théoriquement adressable. Mais ce défaut est en réalité contourné par la commutation de segment, qui permettait d'adresser la totalité de la RAM si besoin. Une second manière de contourner cette limite est que plusieurs processus peuvent s'exécuter sur un seul processeur, si l'OS le permet. Ce n'était pas le cas à l'époque du DOS, qui était un OS mono-programmé, mais c'était en théorie possible. La limite est de 6 segments par programme/processus, en exécuter plusieurs permet d'utiliser toute la mémoire disponible rapidement.
[[File:Overlapping realmode segments.svg|vignette|Segments qui se recouvrent en mode réel.]]
Vous remarquerez qu'avec des registres de segments de 16 bits, on peut gérer 65536 segments différents, chacun de 64 KB. Et 65 536 segments de 64 kibioctets, ça ne rentre pas dans le mébioctet de mémoire permis avec des adresses de 20 bits. La raison est que plusieurs couples segment+''offset'' pointent vers la même adresse. En tout, chaque adresse peut être adressée par 4096 couples segment+''offset'' différents.
L'avantage de cette méthode est que des segments peuvent se recouvrir, à savoir que la fin de l'un se situe dans le début de l'autre, comme illustré ci-contre. Cela permet en théorie de partager de la mémoire entre deux processus. Mais la technique est tout sauf pratique et est donc peu utilisée. Elle demande de placer minutieusement les segments en RAM, et les données à partager dans les segments. En pratique, les programmeurs et OS utilisent des segments qui ne se recouvrent pas et sont disjoints en RAM.
Le nombre maximal de segments disjoints se calcule en prenant la taille de la RAM, qu'on divise par la taille d'un segment. Le calcul donne : 1024 kibioctets / 64 kibioctets = 16 segments disjoints. Un autre calcul prend le nombre de segments divisé par le nombre d'adresses aliasées, ce qui donne 65536 / 4096 = 16. Seulement 16 segments, c'est peu. En comptant les segments utilisés par l'OS et ceux utilisés par le programme, la limite est vite atteinte si le programme utilise la commutation de segment.
===Le mode réel sur les 286 et plus : la ligne d'adresse A20===
Pour résumer, le registre de segment contient des adresses de 20 bits, dont les 4 bits de poids faible sont à 0. Et il se voit ajouter un ''offset'' de 16 bits. Intéressons-nous un peu à l'adresse maximale que l'on peut calculer avec ce système. Nous allons l'appeler l{{'}}'''adresse maximale de segmentation'''. Elle vaut :
{|class="wikitable"
|-
| <code> </code><code style="background:#DED">1111 1111 1111 1111</code><code>0000</code>
| Registre de segment -
| 16 bits, décalé de 4 bits vers la gauche
|-
| <code>+ </code><code style="background:#DDF">1111 1111 1111 1111</code>
| Décalage/''Offset''
| 16 bits
|-
| colspan="3" |
|-
| <code> </code><code style="background:#FDF">1 0000 1111 1111 1110 1111</code>
| Adresse finale
| 20 bits
|}
Le résultat n'est pas l'adresse maximale codée sur 20 bits, car l'addition déborde. Elle donne un résultat qui dépasse l'adresse maximale permis par les 20 bits, il y a un 21ème bit en plus. De plus, les 20 bits de poids faible ont une valeur bien précise. Ils donnent la différence entre l'adresse maximale permise sur 20 bit, et l'adresse maximale de segmentation. Les bits 1111 1111 1110 1111 traduits en binaire donnent 65 519; auxquels il faut ajouter l'adresse 1 0000 0000 0000 0000. En tout, cela fait 65 520 octets adressables en trop. En clair : on dépasse la limite du mébioctet de 65 520 octets. Le résultat est alors très différent selon que l'on parle des processeurs avant le 286 ou après.
Avant le 286, le bus d'adresse faisait exactement 20 bits. Les adresses calculées ne pouvaient pas dépasser 20 bits. L'addition générait donc un débordement d'entier, géré en arithmétique modulaire. En clair, les bits de poids fort au-delà du vingtième sont perdus. Le calcul de l'adresse débordait et retournait au début de la mémoire, sur les 65 520 premiers octets de la mémoire RAM.
[[File:IBM PC Memory areas.svg|vignette|IBM PC Memory Map, la ''High memory area'' est en jaune.]]
Le 80286 en mode réel gère des adresses de base de 24 bits, soit 4 bits de plus que le 8086. Le résultat est qu'il n'y a pas de débordement. Les bits de poids fort sont conservés, même au-delà du 20ème. En clair, la segmentation permettait de réellement adresser 65 530 octets au-delà de la limite de 1 mébioctet. La portion de mémoire adressable était appelé la '''''High memory area''''', qu'on va abrévier en HMA.
{| class="wikitable"
|+ Espace d'adressage du 286
|-
! Adresses en héxadécimal !! Zone de mémoire
|-
| 10 FFF0 à FF FFFF || Mémoire étendue, au-delà du premier mébioctet
|-
| 10 0000 à 10 FFEF || ''High Memory Area''
|-
| 0 à 0F FFFF || Mémoire adressable en mode réel
|}
En conséquence, les applications peuvent utiliser plus d'un mébioctet de RAM, mais au prix d'une rétrocompatibilité imparfaite. Quelques programmes DOS ne marchaient pus à cause de ça. D'autres fonctionnaient convenablement et pouvaient adresser les 65 520 octets en plus.
Pour résoudre ce problème, les carte mères ajoutaient un petit circuit relié au 21ème bit d'adresse, nommé A20 (pas d'erreur, les fils du bus d'adresse sont numérotés à partir de 0). Le circuit en question pouvait mettre à zéro le fil d'adresse, ou au contraire le laisser tranquille. En le forçant à 0, le calcul des adresses déborde comme dans le mode réel des 8086. Mais s'il ne le fait pas, la ''high memory area'' est adressable. Le circuit était une simple porte ET, qui combinait le 21ème bit d'adresse avec un '''signal de commande A20''' provenant d'ailleurs.
Le signal de commande A20 était géré par le contrôleur de clavier, qui était soudé à la carte mère. Le contrôleur en question ne gérait pas que le clavier, il pouvait aussi RESET le processeur, alors gérer le signal de commande A20 n'était pas si problématique. Quitte à avoir un microcontrôleur sur la carte mère, autant s'en servir au maximum... La gestion du bus d'adresse étaitdonc gérable au clavier. D'autres carte mères faisaient autrement et préféraient ajouter un interrupteur, pour activer ou non la mise à 0 du 21ème bit d'adresse.
: Il faut noter que le signal de commande A20 était mis à 1 en mode protégé, afin que le 21ème bit d'adresse soit activé.
Le 386 ajouta deux registres de segment, les registres FS et GS, ainsi que le '''mode ''virtual 8086'''''. Ce dernier permet d’exécuter des programmes en mode réel alors que le système d'exploitation s'exécute en mode protégé. C'est une technique de virtualisation matérielle qui permet d'émuler un 8086 sur un 386. L'avantage est que la compatibilité avec les programmes anciens écrits pour le 8086 est conservée, tout en profitant de la protection mémoire. Tous les processeurs x86 qui ont suivi supportent ce mode virtuel 8086.
==La segmentation avec une table des segments==
La '''segmentation avec une table des segments''' est apparue sur des processeurs assez anciens, le tout premier étant le Burrough 5000. Elle a ensuite été utilisée sur les processeurs x86 des PCs, à partir du 286 d'Intel. Elle est aujourd'hui abandonnée sur les jeux d'instruction x86. Tout comme la segmentation en mode réel, la segmentation attribue plusieurs segments par programmes ! Et cela a des répercutions sur la manière dont la traduction d'adresse est effectuée.
===Pourquoi plusieurs segments par programme ?===
L'utilité d'avoir plusieurs segments par programme n'est pas évidente, mais elle le devient quand on se plonge dans le passé. Dans le passé, les programmeurs devaient faire avec une quantité de mémoire limitée et il n'était pas rare que certains programmes utilisent plus de mémoire que disponible sur la machine. Mais les programmeurs concevaient leurs programmes en fonction.
[[File:Overlay Programming.svg|vignette|upright=1|Overlay Programming]]
L'idée était d'implémenter un système de mémoire virtuelle, mais émulé en logiciel, appelé l{{'}}'''''overlaying'''''. Le programme était découpé en plusieurs morceaux, appelés des ''overlays''. Les ''overlays'' les plus importants étaient en permanence en RAM, mais les autres étaient faisaient un va-et-vient entre RAM et disque dur. Ils étaient chargés en RAM lors de leur utilisation, puis sauvegardés sur le disque dur quand ils étaient inutilisés. Le va-et-vient des ''overlays'' entre RAM et disque dur était réalisé en logiciel, par le programme lui-même. Le matériel n'intervenait pas, comme c'est le cas avec la mémoire virtuelle.
Avec la segmentation, un programme peut utiliser la technique des ''overlays'', mais avec l'aide du matériel. Il suffit de mettre chaque ''overlay'' dans son propre segment, et laisser la segmentation faire. Les segments sont swappés en tout ou rien : on doit swapper tout un segment en entier. L'intérêt est que la gestion du ''swapping'' est grandement facilitée, vu que c'est le système d'exploitation qui s'occupe de swapper les segments sur le disque dur ou de charger des segments en RAM. Pas besoin pour le programmeur de coder quoique ce soit. Par contre, cela demande l'intervention du programmeur, qui doit découper le programme en segments/''overlays'' de lui-même. Sans cela, la segmentation n'est pas très utile.
L{{'}}''overlaying'' est une forme de '''segmentation à granularité grossière''', à savoir que le programme est découpé en segments de grande taille. L'usage classique est d'avoir un segment pour la pile, un autre pour le code exécutable, un autre pour le reste. Éventuellement, on peut découper les trois segments précédents en deux ou trois segments, rarement au-delà. Les segments sont alors peu nombreux, guère plus d'une dizaine par programme. D'où le terme de ''granularité grossière''.
La '''segmentation à granularité fine''' pousse le concept encore plus loin. Avec elle, il y a idéalement un segment par entité manipulée par le programme, un segment pour chaque structure de donnée et/ou chaque objet. Par exemple, un tableau aura son propre segment, ce qui est idéal pour détecter les accès hors tableau. Pour les listes chainées, chaque élément de la liste aura son propre segment. Et ainsi de suite, chaque variable agrégée (non-primitive), chaque structure de donnée, chaque objet, chaque instance d'une classe, a son propre segment. Diverses fonctionnalités supplémentaires peuvent être ajoutées, ce qui transforme le processeur en véritable processeur orienté objet, mais passons ces détails pour le moment.
Vu que les segments correspondent à des objets manipulés par le programme, on peut deviner que leur nombre évolue au cours du temps. En effet, les programmes modernes peuvent demander au système d'exploitation du rab de mémoire pour allouer une nouvelle structure de données. Avec la segmentation à granularité fine, cela demande d'allouer un nouveau segment à chaque nouvelle allocation mémoire, à chaque création d'une nouvelle structure de données ou d'un objet. De plus, les programmes peuvent libérer de la mémoire, en supprimant les structures de données ou objets dont ils n'ont plus besoin. Avec la segmentation à granularité fine, cela revient à détruire le segment alloué pour ces objets/structures de données. Le nombre de segments est donc dynamique, il change au cours de l'exécution du programme.
===Les tables de segments avec la segmentation===
La présence de plusieurs segments par programme a un impact sur la table des segments. Avec la relocation matérielle, elle conte nait un segment par programme. Chaque entrée, chaque ligne de la table des segment, mémorisait l'adresse de base, l'adresse limite, un bit de présence pour la mémoire virtuelle et des autorisations liées à la protection mémoire. Avec la segmentation, les choses sont plus compliquées, car il y a plusieurs segments par programme. Les entrées ne sont pas modifiées, mais elles sont organisées différemment.
Avec cette forme de segmentation, la table des segments doit respecter plusieurs contraintes. Premièrement, il y a plusieurs segments par programmes. Deuxièmement, le nombre de segments est variable : certains programmes se contenteront d'un seul segment, d'autres de dizaine, d'autres plusieurs centaines, etc. Il y a typiquement deux manières de faire : soit utiliser une table des segments uniques, utiliser une table des segment par programme.
Il est possible d'utiliser une table des segment unique qui mémorise tous les segments de tous les processus, système d'exploitation inclut. On parle alors de '''table des segment globale'''. Mais cette solution n'est pas utilisée avec la segmentation proprement dite. Elle est utilisée sur les architectures à capacité qu'on détaillera vers la fin du chapitre, dans une section dédiée. À la place, la segmentation utilise une table de segment par processus/programme, chacun ayant une '''table des segment locale'''.
Dans les faits, les choses sont plus compliquées. Le système d'exploitation doit savoir où se trouvent les tables de segment locale pour chaque programme. Pour cela, il a besoin d'utiliser une table de segment globale, dont chaque entrée pointe non pas vers un segment, mais vers une table de segment locale. Lorsque l'OS effectue une commutation de contexte, il lit la table des segment globale, pour récupérer un pointeur vers celle-ci. Ce pointeur est alors chargé dans un registre du processeur, qui mémorise l'adresse de la table locale, ce qui sert lors des accès mémoire.
Une telle organisation fait que les segments d'un processus/programme sont invisibles pour les autres, il y a une certaine forme de sécurité. Un programme ne connait que sa table de segments locale, il n'a pas accès directement à la table des segments globales. Tout accès mémoire se passera à travers la table de segment locale, il ne sait pas où se trouvent les autres tables de segment locales.
Les processeurs x86 sont dans ce cas : ils utilisent une table de segment globale couplée à autant de table des segments qu'il y a de processus en cours d'exécution. La table des segments globale s'appelle la '''''Global Descriptor Table''''' et elle peut contenir 8192 segments maximum, ce qui permet le support de 8192 processus différents. Les tables de segments locales sont appelées les '''''Local Descriptor Table''''' et elles font aussi 8192 segments maximum, ce qui fait 8192 segments par programme maximum. Il faut noter que la table de segment globale peut mémoriser des pointeurs vers les routines d'interruption, certaines données partagées (le tampon mémoire pour le clavier) et quelques autres choses, qui n'ont pas leur place dans les tables de segment locales.
===La relocation avec la segmentation===
La table des segments locale mémorise les adresses de base et limite de chaque segment, ainsi que d'autres méta-données. Les informations pour un segment sont regroupés dans un '''descripteur de segment''', qui est codé sur plusieurs octets, et qui regroupe : adresse de base, adresse limite, bit de présence en RAM, méta-données de protection mémoire.
La table des segments est un tableau dans lequel les descripteurs de segment sont placés les uns à la suite des autres en mémoire RAM. La table des segments est donc un tableau de segment. Les segments d'un programme sont numérotés, le nombre s'appelant un '''indice de segment''', appelé '''sélecteur de segment''' dans la terminologie Intel. L'indice de segment n'est autre que l'indice du segment dans ce tableau.
[[File:Global Descriptor table.png|centre|vignette|upright=2|Table des segments locale.]]
Il n'y a pas de registre de segment proprement dit, qui mémoriserait l'adresse de base. À la place, les segments sont adressés de manière indirecte. À la place, les registres de segment mémorisent des sélecteurs de segment. Ils sont utilisés pour lire l'adresse de base/limite dans la table de segment en mémoire RAM. Pour cela, un registre mémorise l'adresse de la table de segment locale, sa position en mémoire RAM.
Toute lecture ou écriture se fait en deux temps, en deux accès mémoire, consécutifs. Premièrement, le numéro de segment est utilisé pour adresser la table des segment. La lecture récupère alors un pointeur vers ce segment. Deuxièmement, ce pointeur est utilisé pour faire la lecture ou écriture. Plus précisément, la première lecture récupère un descripteur de segment qui contient l'adresse de base, le pointeur voulu, mais aussi l'adresse limite et d'autres informations.
[[File:Segmentation avec table des segments.png|centre|vignette|upright=2|Segmentation avec table des segments]]
L'accès à la table des segments se fait automatiquement à chaque accès mémoire. La conséquence est que chaque accès mémoire demande d'en faire deux : un pour lire la table des segments, l'autre pour l'accès lui-même. Il s'agit en quelque sorte d'une forme d'adressage indirect mémoire.
Un point important est que si le premier accès ne fait qu'une simple lecture dans un tableau, le second accès implique des calculs d'adresse. En effet, le premier accès récupère l'adresse de base du segment, mais le second accès sélectionne une donnée dans le segment, ce qui demande de calculer son adresse. L'adresse finale se déduit en combinant l'adresse de base avec un décalage (''offset'') qui donne la position de la donnée dans ce segment. L'indice de segment est utilisé pour récupérer l'adresse de base du segment. Une fois cette adresse de base connue, on lui additionne le décalage pour obtenir l'adresse finale.
[[File:Table des segments.png|centre|vignette|upright=2|Traduction d'adresse avec une table des segments.]]
Pour effectuer automatiquement l'accès à la table des segments, le processeur doit contenir un registre supplémentaire, qui contient l'adresse de la table de segment, afin de la localiser en mémoire RAM. Nous appellerons ce registre le '''pointeur de table'''. Le pointeur de table est combiné avec l'indice de segment pour adresser le descripteur de segment adéquat.
[[File:Segment 2.svg|centre|vignette|upright=2|Traduction d'adresse avec une table des segments, ici appelée table globale des de"scripteurs (terminologie des processeurs Intel x86).]]
Un point important est que la table des segments n'est pas accessible pour le programme en cours d'exécution. Il ne peut pas lire le contenu de la table des segments, et encore moins la modifier. L'accès se fait seulement de manière indirecte, en faisant usage des indices de segments, mais c'est un adressage indirect. Seul le système d'exploitation peut lire ou écrire la table des segments directement.
Plus haut, j'ai dit que tout accès mémoire impliquait deux accès mémoire : un pour charger le descripteur de segment, un autre pour la lecture/écriture proprement dite. Cependant, cela aurait un impact bien trop grand sur les performances. Dans les faits, les processeurs avec segmentations intégraient un '''cache de descripteurs de segments''', pour limiter la casse. Quand un descripteur de segment est lu depuis la RAM, il est copié dans ce cache. Les accès ultérieurs accédent au descripteur dans le cache, pas besoin de passer par la RAM. L'intel 386 avait un cache de ce type.
===La protection mémoire : les accès hors-segments===
Comme avec la relocation matérielle, le processeur détecte les débordements de segment. Pour cela, il compare l'adresse logique accédée avec l'adresse limite, ou compare la taille limite avec le décalage. De nombreux processeurs, comme l'Intel 386, préféraient utiliser la taille du segment, pour une question d'optimisation. En effet, si on compare l'adresse finale avec l'adresse limite, on doit faire la relocation avant de comparer l'adresse relocatée. Mais en utilisant la taille, ce n'est pas le cas : on peut faire la comparaison avant, pendant ou après la relocation.
Un détail à prendre en compte est la taille de la donnée accédée. Sans cela, la comparaison serait très simple : on vérifie si ''décalage <= taille du segment'', ou on compare des adresses de la même manière. Mais imaginez qu'on accède à une donnée de 4 octets : il se peut que l'adresse de ces 4 octets rentre dans le segment, mais que quelques octets débordent. Par exemple, les deux premiers octets sont dans le segment, mais pas les deux suivants. La vraie comparaison est alors : ''décalage + 4 octets <= taille du segment''.
Mais il est possible de faire le calcul autrement, et quelques processeurs comme l'Intel 386 ne s'en sont pas privé. Il calculait la différence ''taille du segment - décalage'', et vérifiait le résultat. Le processeur gérait des données de 1, 2 et 4 octets, ce qui fait que le résultat devait être entre 0 et 3. Le processeur prenait le résultat de la soustraction, et vérifiait alors que les 30 bits de poids fort valaient bien 0. Il vérifiait aussi que les deux bits de poids faible avaient la bonne valeur.
[[File:Vm7.svg|centre|vignette|upright=2|Traduction d'adresse avec vérification des accès hors-segment.]]
Une nouveauté fait son apparition avec la segmentation : la '''gestion des droits d'accès'''. Par exemple, il est possible d'interdire d'exécuter le contenu d'un segment, ce qui fournit une protection contre certaines failles de sécurité ou certains virus. Lorsqu'on exécute une opération interdite, le processeur lève une exception matérielle, à charge du système d'exploitation de gérer la situation.
Pour cela, chaque segment se voit attribuer un certain nombre d'autorisations d'accès qui indiquent si l'on peut lire ou écrire dedans, si celui-ci contient un programme exécutable, etc. Les autorisations pour chaque segment sont placées dans le descripteur de segment. Elles se résument généralement à quelques bits, qui indiquent si le segment est accesible en lecture/écriture ou exécutable. Le tout est souvent concaténé dans un ou deux '''octets de droits d'accès'''.
L'implémentation de la protection mémoire dépend du CPU considéré. Les CPU microcodés peuvent en théorie utiliser le microcode. Lorsqu'une instruction mémoire s'exécute, le microcode effectue trois étapes : lire le descripteur de segment, faire les tests de protection mémoire, exécuter la lecture/écriture ou lever une exception. Létape de test est réalisée avec un ou plusieurs micro-branchements. Par exemple, une écriture va tester le bit R/W du descripteur, qui indique si on peut écrire dans le segment, en utilisant un micro-branchement. Le micro-branchement enverra vers une routine du microcode en cas d'erreur.
Les tests de protection mémoire demandent cependant de tester beaucoup de conditions différentes. Par exemple, le CPU Intel 386 testait moins d'une dizaine de conditions pour certaines instructions. Il est cependant possible de faire plusieurs comparaisons en parallèle en rusant un peu. Il suffit de mémoriser les octets de droits d'accès dans un registre interne, de masquer les bits non-pertinents, et de faire une comparaison avec une constante adéquate, qui encode la valeur que doivent avoir ces bits.
Une solution alternative utiliser un circuit combinatoire pour faire les tests de protection mémoire. Les tests sont alors faits en parallèles, plutôt qu'un par un par des micro-branchements. Par contre, le cout en matériel est assez important. Il faut ajouter ce circuit combinatoire, ce qui demande pas mal de circuits.
===La mémoire virtuelle avec la segmentation===
La mémoire virtuelle est une fonctionnalité souvent implémentée sur les processeurs qui gèrent la segmentation, alors que les processeurs avec relocation matérielle s'en passaient. Il faut dire que l'implémentation de la mémoire virtuelle est beaucoup plus simple avec la segmentation, comparé à la relocation matérielle. Le remplacement des registres de base par des sélecteurs de segment facilite grandement l'implémentation.
Le problème de la mémoire virtuelle est que les segments peuvent être swappés sur le disque dur n'importe quand, sans que le programme soit prévu. Le swapping est réalisé par une interruption de l'OS, qui peut interrompre le programme n'importe quand. Et si un segment est swappé, le registre de base correspondant devient invalide, il point sur une adresse en RAM où le segment était, mais n'est plus. De plus, les segments peuvent être déplacés en mémoire, là encore n'importe quand et d'une manière invisible par le programme, ce qui fait que les registres de base adéquats doivent être modifiés.
Si le programme entier est swappé d'un coup, comme avec la relocation matérielle simple, cela ne pose pas de problèmes. Mais dès qu'on utilise plusieurs registres de base par programme, les choses deviennent soudainement plus compliquées. Le problème est qu'il n'y a pas de mécanismes pour choisir et invalider le registre de base adéquat quand un segment est déplacé/swappé. En théorie, on pourrait imaginer des systèmes qui résolvent le problème au niveau de l'OS, mais tous ont des problèmes qui font que l'implémentation est compliquée ou que les performances sont ridicules.
L'usage d'une table des segments accédée à chaque accès résout complètement le problème. La table des segments est accédée à chaque accès mémoire, elle sait si le segment est swappé ou non, chaque accès vérifie si le segment est en mémoire et quelle est son adresse de base. On peut changer le segment de place n'importe quand, le prochain accès récupérera des informations à jour dans la table des segments.
L'implémentation de la mémoire virtuelle avec la segmentation est simple : il suffit d'ajouter un bit dans les descripteurs de segments, qui indique si le segment est swappé ou non. Tout le reste, la gestion de ce bit, du swap, et tout ce qui est nécessaire, est délégué au système d'exploitation. Lors de chaque accès mémoire, le processeur vérifie ce bit avant de faire la traduction d'adresse, et déclenche une exception matérielle si le bit indique que le segment est swappé. L'exception matérielle est gérée par l'OS.
===Le partage de segments===
Il est possible de partager un segment entre plusieurs applications. Cela peut servir pour partager des données entre deux programmes : un segment de données partagées est alors partagé entre deux programmes. Partager un segment de code est utile pour les bibliothèques partagées : la bibliothèque est placée dans un segment dédié, qui est partagé entre les programmes qui l'utilisent. Partager un segment de code est aussi utile quand plusieurs instances d'une même application sont lancés simultanément : le code n'ayant pas de raison de changer, celui-ci est partagé entre toutes les instances. Mais ce n'est là qu'un exemple.
La première solution pour cela est de configurer les tables de segment convenablement. Le même segment peut avoir des droits d'accès différents selon les processus. Les adresses de base/limite sont identiques, mais les tables des segments ont alors des droits d'accès différents. Mais cette méthode de partage des segments a plusieurs défauts.
Premièrement, les sélecteurs de segments ne sont pas les mêmes d'un processus à l'autre, pour un même segment. Le segment partagé peut correspondre au segment numéro 80 dans le premier processus, au segment numéro 1092 dans le second processus. Rien n'impose que les sélecteurs de segment soient les mêmes d'un processus à l'autre, pour un segment identique.
Deuxièmement, les adresses limite et de base sont dupliquées dans plusieurs tables de segments. En soi, cette redondance est un souci mineur. Mais une autre conséquence est une question de sécurité : que se passe-t-il si jamais un processus a une table des segments corrompue ? Il se peut que pour un segment identique, deux processus n'aient pas la même adresse limite, ce qui peut causer des failles de sécurité. Un processus peut alors subir un débordement de tampon, ou tout autre forme d'attaque.
[[File:Vm9.png|centre|vignette|upright=2|Illustration du partage d'un segment entre deux applications.]]
Une seconde solution, complémentaire, utilise une table de segment globale, qui mémorise des segments partagés ou accessibles par tous les processus. Les défauts de la méthode précédente disparaissent avec cette technique : un segment est identifié par un sélecteur unique pour tous les processus, il n'y a pas de duplication des descripteurs de segment. Par contre, elle a plusieurs défauts.
Le défaut principal est que cette table des segments est accessible par tous les processus, impossible de ne partager ses segments qu'avec certains pas avec les autres. Un autre défaut est que les droits d'accès à un segment partagé sont identiques pour tous les processus. Impossible d'avoir un segment partagé accessible en lecture seule pour un processus, mais accessible en écriture pour un autre. Il est possible de corriger ces défauts, mais nous en parlerons dans la section sur les architectures à capacité.
===L'extension d'adresse avec la segmentation===
L'extension d'adresse est possible avec la segmentation, de la même manière qu'avec la relocation matérielle. Il suffit juste que les adresses de base soient aussi grandes que le bus d'adresse. Mais il y a une différence avec la relocation matérielle : un même programme peut utiliser plus de mémoire qu'il n'y en a dans l'espace d'adressage. La raison est simple : un segment peut prendre tout l'espace d'adressage, et il y a plusieurs segments par programme.
Pour donner un exemple, prenons un processeur 16 bits, qui peut adresser 64 kibioctets, associé à une mémoire de 4 mébioctets. Il est possible de placer le code machine dans les premiers 64k de la mémoire, la pile du programme dans les 64k suivants, le tas dans les 64k encore après, et ainsi de suite. Le programme dépasse donc les 64k de mémoire de l'espace d'adressage. Ce genre de chose est impossible avec la relocation, où un programme est limité par l'espace d'adressage.
===Le mode protégé des processeurs x86===
L'Intel 80286, aussi appelé 286, ajouta un mode de segmentation séparé du mode réel, qui ajoute une protection mémoire à la segmentation, ce qui lui vaut le nom de '''mode protégé'''. Dans ce mode, les registres de segment ne contiennent pas des adresses de base, mais des sélecteurs de segments qui sont utilisés pour l'accès à la table des segments en mémoire RAM.
Le 286 bootait en mode réel, puis le système d'exploitation devait faire quelques manipulations pour passer en mode protégé. Le 286 était pensé pour être rétrocompatible au maximum avec le 80186. Mais les différences entre le 286 et le 8086 étaient majeures, au point que les applications devaient être réécrites intégralement pour profiter du mode protégé. Un mode de compatibilité permettait cependant aux applications destinées au 8086 de fonctionner, avec même de meilleures performances. Aussi, le mode protégé resta inutilisé sur la plupart des applications exécutées sur le 286.
Vint ensuite le processeur 80386, renommé en 386 quelques années plus tard. Sur ce processeur, les modes réel et protégé sont conservés tel quel, à une différence près : toutes les adresses passent à 32 bits, qu'il s'agisse des adresses de base, limite ou des ''offsets''. Le processeur peut donc adresser un grand nombre de segments : 2^32, soit plus de 4 milliards. Les segments grandissent aussi et passent de 64 KB maximum à 4 gibioctets maximum. Mais surtout : le 386 ajouta le support de la pagination en plus de la segmentation. Ces modifications ont été conservées sur les processeurs 32 bits ultérieurs.
Les processeurs x86 gèrent deux types de tables des segments : une table locale pour chaque processus, et une table globale partagée entre tous les processus. Il ne peut y avoir qu'une table locale d'active, vu que le processeur ne peut exécuter qu'un seul processus en même temps. Chaque table locale définit 8192 segments, pareil pour la table globale. La table globale est utilisée pour les segments du noyau et la mémoire partagée entre processus. Un défaut est qu'un segment partagé par la table globale est visible par tous les processus, avec les mêmes droits d'accès. Ce qui fait que cette méthode était peu utilisée en pratique. La table globale mémorise aussi des pointeurs vers les tables locales, avec un descripteur de segment par table locale.
Sur les processeurs x86 32 bits, un descripteur de segment est organisé comme suit, pour les architectures 32 bits. On y trouve l'adresse de base et la taille limite, ainsi que de nombreux bits de contrôle.
Le premier groupe de bits de contrôle est l'octet en bleu à droite. Il contient :
* le bit P qui indique que l'entrée contient un descripteur valide, qu'elle n'est pas vide ;
* deux bits DPL qui indiquent le niveau de privilège du segment (noyau, utilisateur, les deux intermédiaires spécifiques au x86) ;
* un bit S qui précise si le segment est de type système (utiles pour l'OS) ou un segment de code/données.
* un champ Type qui contient les bits suivants :
** un bit E qui indique si le segment contient du code exécutable ou non ;
** le bit RW qui indique s'il est en lecture seule ou non ;;
** Un bit A qui indique que le segment a récemment été accédé, information utile pour l'OS;
** un bit DC assez spécifiques.
En haut à gauche, en bleu, on trouve deux bits :
* Le bit G indique comment interpréter la taille contenue dans le descripteur : 0 si la taille est exprimée en octets, 1 si la taille est un nombre de pages de 4 kibioctets. Ce bit précise si on utilise la segmentation seule, ou combinée avec la pagination.
* Le bit DB précise si l'on utilise des segments en mode de compatibilité 16 bits ou des segments 32 bits.
[[File:SegmentDescriptor.svg|centre|vignette|upright=3|Segment Descriptor]]
Les indices de segment sont appelés des sélecteurs de segment. Ils ont une taille de 16 bits, mais 3 bits sont utilisés pour encoder des méta-données. Le numéro de segment est donc codé sur 13 bits, ce qui permettait de gérer maximum 8192 segments par table de segment (locale ou globale). Les 16 bits sont organisés comme suit :
* 13 bits pour le numéro du segment dans la table des segments, l'indice de segment proprement dit ;
* un bit qui précise s'il faut accéder à la table des segments globale ou locale ;
* deux bits qui indiquent le niveau de privilège de l'accès au segment (les 4 niveaux de protection, dont l'espace noyau et utilisateur).
[[File:SegmentSelector.svg|centre|vignette|upright=1.5|Sélecteur de segment 16 bit.]]
En tout, l'indice permet de gérer 8192 segments pour la table locale et 8192 segments de la table globale.
====La MMU du 386/486 : cache de segment, protection mémoire====
La MMU du 386 et celle du 486 étaient assez similaires. Elles étaient plus complexes que celle du 186 et du 286. Elle contenait un additionneur pour les calculs d'adresse et un comparateur pour tester si l'accès mémoire déborde d'un segment. Le test de débordement se faisait en parallèle du calcul de l'adresse finale, comme sur le 286.
L'additionneur était un additionneur trois-opérandes, qui additionnait l'adresse à lire/écrire, l'adresse de base du segment, et un décalage intégré dans l'instruction. En clair, l'unité de segmentation n'était pas qu'une MMU, elle prenait en charge une partie du calcul d'adresse. L'avantage est que cela permettait de gérer les modes d'adressage "base + décalage" et "base + indice + décalage" très facilement, en utilisant un minimum de circuits. Une conséquence de cette organisation était que l'usage d'un décalage était gratuit. Par contre, dès qu'on utilisait l'adressage "Base + Indice", avec ou sans décalage, l'instruction prenait un cycle de plus à s'exécuter, parce qu'il fallait faire l'addition "Base + Indice" dans l'ALU entière.
Le CPU 386 était le premier à implémenter la protection mémoire avec des segments. Pour cela, il intégrait une '''''Protection Test Unit''''', séparée du microcode, qu'on va abrévier en PTU. Précisément, il s'agissait d'un PLA (''Programmable Logic Array''), une sorte d'intermédiaire entre circuit logique fait sur mesure et mémoire ROM, qu'on a déjà abordé dans le chapitre sur les mémoires ROM. Mais cette unité ne faisait pas tout, le microcode était aussi impliqué. La PTU sera détaillée dans la section suivante.
Pour améliorer les performances, le 386 et le 486 intégraient un '''cache de descripteurs de segment''', aussi appelé le cache de descripteurs. Lorsqu'un descripteur état chargé pour la première fois, il était copié dans le cache de descripteurs de segment. Les accès mémoire ultérieur lisaient le descripteur de segment depuis ce cache, pas depuis la table des segments en RAM. Le cache de descripteurs gère aussi bien les segments en mode réel qu'en mode protégé. Récupérer l'adresse de base depuis cache se fait un peu différemment en mode réel et protégé, mais le cache gère cela tout seul. Idem pour récupérer la taille/adresse limite.
[[File:Microarchitecture du 386, avec focus sur la segmentation.png|centre|vignette|upright=2|Microarchitecture du 386 et du 486, avec focus sur la segmentation.]]
En mode réel, la taille des segment est censée être limitée à 64 kibioctets. Mais le processeur ne vérifiait pas si cette limite était dépassée. À la place, il utilisait la limite précisée dans le cache de segments. Pire que ça : le cache de segment n'était pas réinitialisé quand on passe du mode réel au mode protégé, et réciproquement. Et cette propriété a été à l'origine de l''''''unreal mode'''''. Il s'agissait d'un mode réel amélioré, capable d'utiliser des segments de 4 gibioctets et des adresses de 32 bits.
Passer en mode ''unreal'' pouvait se faire de deux manières. Il était possible d'altérer le cache de descripteur en utilisant l'instruction non-documentée LOADALL. Elle permettait de charger les descripteurs de segments dans le cache de descripteurs, avec une taille arbitraire. Une autre solution, beaucoup plus complexe sur le 386, demandait d'entrer en mode protégé pour configurer des segments de grande taille, de charger leurs descripteurs dans le cache de descripteur, puis de revenir en mode réel. En mode réel, les descripteurs dans le cache étaient encore disponibles et on pouvait les lire dans le cache.
====L'implémentation de la protection mémoire sur le 386====
La protection mémoire teste la valeur des bits P, S, X, E, R/W. Elle teste aussi les niveaux de privilège, avec deux bits DPL et CPL. En tout, le processeur pouvait tester 148 conditions différentes en parallèle dans la PTU. Cependant, les niveaux de privilèges étaient pré-traités par le microcode. Le microcode vérifiait aussi s'il y avait une erreur en terme d’anneau mémoire, avec par "exemple un segment en mode noyau accédé alors que le CPU est en espace utilisateur. Il fournissait alors un résultat sur deux bits, qui indiquait s'il y avait une erreur ou non, que la PTU utilisait.
Mais toutes les conditions n'étaient pas pertinentes à un instant t. Par exemple, il est pertinent de vérifier si le bit R/W était cohérent si l'instruction à exécuter est une écriture. Mais il n'y a pas besoin de tester le bit E qui indique qu'un segment est exécutable ou non, pour une lecture. En tout, le processeur pouvait se retrouver dans 33 situations possibles, chacune demandant de tester un sous-ensemble des 148 conditions. Pour préciser quel sous-ensembles tester, la PTU recevait un code opération, généré par le microcode.
Pour faire les tests de protection mémoire, le microcode avait une micro-opération nommée ''protection test operation'', qui envoyait les droits d'accès à la PTU. Lors de l'exécution d'une ''protection test operation'', le PLA recevait un descripteur de segment, lu depuis la mémoire RAM, ainsi qu'un code opération provenant du microcode.
{|class="wikitable"
|+ Entrée de la ''Protection Test Unit''
|-
! 15 - 14 !! 13 - 12 !! 11 !! 10 !! 9 !! 8 !! 7 !! 6 !! 5-0
|-
| P1 , P2 || || P || S || X || E || R/W || A || Code opération
|-
| Niveaux de privilèges cohérents/erreur || || Segment présent en mémoire ou swappé || S || X || Segment exécutable ou non || Segment accesible en lecture/écriture || Segment récemment accédé || Code opération
|}
Il fournissait en sortie un bit qui indiquait si une erreur de protection mémoire avait eu lieu ou non. Il fournissait aussi une adresse de 12 bits, utilisée seulement en cas d'erruer. Elle pointait dans le microcode, sur un code levant une exception en cas d'erreur. Enfin, la PTU fournissait 4 bits pouvant être testés par un branchement dans le microcode. L'un d'entre eux demandait de tester s'il y a un accès hors-limite, les autres étaient assez peu reliés à la protection mémoire.
Un détail est que le chargement du descripteur de segment est réalisé par une fonction dans le microcode. Elle est appliquée pour toutes les instructions ou situations qui demandent de faire un accès mémoire. Et les tests de protection mémoire sont réalisés dans cette fonction, pas après elle. Vu qu'il s'agit d'une fonction exécutée quelque soit l'instruction, le microcode doit transférer le code opération à cette fonction. Le microcode est pour cela associé à un registre interne, dans lequel le code opération est mémorisé, avant d'appeler la fonction. Le microcode a une micro-opération PTSAV (''Protection Save'') pour mémoriser le code opération dans ce registre. Dans la fonction qui charge le descripteur, une micro-opération PTOVRR (''Protection Override'') lit le code opération dans ce registre, et lance les tests nécessaires.
Il faut noter que le PLA était certes plus rapide que de tester les conditions une par une, mais il était assez lent. La PTU mettait environ 3 cycles d'horloges pour rendre son résultat. Le microcode en profitait alors pour exécuter des micro-opérations durant ces 3 cycles d'attente. Par exemple, le microcode pouvait en profiter pour lire l'adresse de base dans le descripteur, si elle n'a pas été chargée avant (les descripteur était chargé en deux fois). Il fallait cependant que les trois micro-opérations soient valides, peu importe qu'il y ait une erreur de protection mémoire ou non. Ou du moins, elles produisaient un résultat qui n'est pas utilisé en cas d'erreur. Si ce n'était pas possible, le microcode ajoutait des NOP pendant ce temps d'attente de 3 cycles.
Le bit A du descripteur de segment indique que le segment a récemment été accédé. Il est mis à jour après les tests de protection mémoire, quand ceux-ci indiquent que l'accès mémoire est autorisé. Le bit A est mis à 1 si la PTU l'autorise. Pour cela, la PTU utilise un des 4 bits de sortie mentionnés plus haut : l'un d'entre eux indique que le bit A doit être mis à 1. La mise à jour est ensuite réalisée par le microcode, qui utilise trois micro-opérations pour le mettre à jour.
====Le ''Hardware task switching'' des CPU x86====
Les systèmes d’exploitation modernes peuvent lancer plusieurs logiciels en même temps. Les logiciels sont alors exécutés à tour de rôle. Passer d'un programme à un autre est ce qui s'appelle une commutation de contexte. Lors d'une commutation de contexte, l'état du processeur est sauvegardé, afin que le programme stoppé puisse reprendre là où il était. Il arrivera un moment où le programme stoppé redémarrera et il doit reprendre dans l'état exact où il s'est arrêté. Deuxièmement, le programme à qui c'est le tour restaure son état. Cela lui permet de revenir là où il était avant d'être stoppé. Il y a donc une sauvegarde et une restauration des registres.
Divers processeurs incorporent des optimisations matérielles pour rendre la commutation de contexte plus rapide. Ils peuvent sauvegarder et restaurer les registres du processeur automatiquement lors d'une interruption de commutation de contexte. Les registres sont sauvegardés dans des structures de données en mémoire RAM, appelées des '''contextes matériels'''. Sur les processeurs x86, il s'agit de la technique d{{'}}''Hardware Task Switching''. Fait intéressant, le ''Hardware Task Switching'' se base beaucoup sur les segments mémoires.
Avec ''Hardware Task Switching'', chaque contexte matériel est mémorisé dans son propre segment mémoire, séparé des autres. Les segments pour les contextes matériels sont appelés des '''''Task State Segment''''' (TSS). Un TSS mémorise tous les registres généraux, le registre d'état, les pointeurs de pile, le ''program counter'' et quelques registres de contrôle du processeur. Par contre, les registres flottants ne sont pas sauvegardés, de même que certaines registres dit SIMD que nous n'avons pas encore abordé. Et c'est un défaut qui fait que le ''Hardware Task Switching'' n'est plus utilisé.
Le programme en cours d'exécution connait l'adresse du TSS qui lui est attribué, car elle est mémorisée dans un registre appelé le '''''Task Register'''''. En plus de pointer sur le TSS, ce registre contient aussi les adresses de base et limite du segment en cours. Pour être plus précis, le ''Task Register'' ne mémorise pas vraiment l'adresse du TSS. À la place, elle mémorise le numéro du segment, le numéro du TSS. Le numéro est codé sur 16 bits, ce qui explique que 65 536 segments sont adressables. Les instructions LDR et STR permettent de lire/écrire ce numéro de segment dans le ''Task Register''.
Le démarrage d'un programme a lieu automatiquement dans plusieurs circonstances. La première est une instruction de branchement CALL ou JMP adéquate. Le branchement fournit non pas une adresse à laquelle brancher, mais un numéro de segment qui pointe vers un TSS. Cela permet à une routine du système d'exploitation de restaurer les registres et de démarrer le programme en une seule instruction de branchement. Une seconde circonstance est une interruption matérielle ou une exception, mais nous la mettons de côté. Le ''Task Register'' est alors initialisé avec le numéro de segment fournit. S'en suit la procédure suivante :
* Le ''Task Register'' est utilisé pour adresser la table des segments, pour récupérer un pointeur vers le TSS associé.
* Le pointeur est utilisé pour une seconde lecture, qui adresse le TSS directement. Celle-ci restaure les registres du processeur.
En clair, on va lire le ''TSS descriptor'' dans la GDT, puis on l'utilise pour restaurer les registres du processeur.
[[File:Hardware Task Switching x86.png|centre|vignette|upright=2|Hardware Task Switching x86]]
===La segmentation sur les processeurs Burrough B5000 et plus===
Le Burrough B5000 est un très vieil ordinateur, commercialisé à partir de l'année 1961. Ses successeurs reprennent globalement la même architecture. C'était une machine à pile, doublé d'une architecture taguée, choses très rare de nos jours. Mais ce qui va nous intéresser dans ce chapitre est que ce processeur incorporait la segmentation, avec cependant une différence de taille : un programme avait accès à un grand nombre de segments. La limite était de 1024 segments par programme ! Il va de soi que des segments plus petits favorise l'implémentation de la mémoire virtuelle, mais complexifie la relocation et le reste, comme nous allons le voir.
Le processeur gère deux types de segments : les segments de données et de procédure/fonction. Les premiers mémorisent un bloc de données, dont le contenu est laissé à l'appréciation du programmeur. Les seconds sont des segments qui contiennent chacun une procédure, une fonction. L'usage des segments est donc différent de ce qu'on a sur les processeurs x86, qui n'avaient qu'un segment unique pour l'intégralité du code machine. Un seul segment de code machine x86 est découpé en un grand nombre de segments de code sur les processeurs Burrough.
La table des segments contenait 1024 entrées de 48 bits chacune. Fait intéressant, chaque entrée de la table des segments pouvait mémoriser non seulement un descripteur de segment, mais aussi une valeur flottante ou d'autres types de données ! Parler de table des segments est donc quelque peu trompeur, car cette table ne gère pas que des segments, mais aussi des données. La documentation appelaiat cette table la '''''Program Reference Table''''', ou PRT.
La raison de ce choix quelque peu bizarre est que les instructions ne gèrent pas d'adresses proprement dit. Tous les accès mémoire à des données en-dehors de la pile passent par la segmentation, ils précisent tous un indice de segment et un ''offset''. Pour éviter d'allouer un segment pour chaque donnée, les concepteurs du processeur ont décidé qu'une entrée pouvait contenir directement la donnée entière à lire/écrire.
La PRT supporte trois types de segments/descripteurs : les descripteurs de données, les descripteurs de programme et les descripteurs d'entrées-sorties. Les premiers décrivent des segments de données. Les seconds sont associés aux segments de procédure/fonction et sont utilisés pour les appels de fonction (qui passent, eux aussi, par la segmentation). Le dernier type de descripteurs sert pour les appels systèmes et les communications avec l'OS ou les périphériques.
Chaque entrée de la PRT contient un ''tag'', une suite de bit qui indique le type de l'entrée : est-ce qu'elle contient un descripteur de segment, une donnée, autre. Les descripteurs contiennent aussi un ''bit de présence'' qui indique si le segment a été swappé ou non. Car oui, les segments pouvaient être swappés sur ce processeur, ce qui n'est pas étonnant vu que les segments sont plus petits sur cette architecture. Le descripteur contient aussi l'adresse de base du segment ainsi que sa taille, et diverses informations pour le retrouver sur le disque dur s'il est swappé.
: L'adresse mémorisée ne faisait que 15 bits, ce qui permettait d'adresse 32 kibi-mots, soit 192 kibioctets de mémoire. Diverses techniques d'extension d'adressage étaient disponibles pour contourner cette limitation. Outre l'usage de l{{'}}''overlay'', le processeur et l'OS géraient aussi des identifiants d'espace d'adressage et en fournissaient plusieurs par processus. Les processeurs Borrough suivants utilisaient des adresses plus grandes, de 20 bits, ce qui tempérait le problème.
[[File:B6700Word.jpg|centre|vignette|upright=2|Structure d'un mot mémoire sur le B6700.]]
==Les architectures à capacités==
Les architectures à capacité utilisent la segmentation à granularité fine, mais ajoutent des mécanismes de protection mémoire assez particuliers, qui font que les architectures à capacité se démarquent du reste. Les architectures de ce type sont très rares et sont des processeurs assez anciens. Le premier d'entre eux était le Plessey System 250, qui date de 1969. Il fu suivi par le CAP computer, vendu entre les années 70 et 77. En 1978, le System/38 d'IBM a eu un petit succès commercial. En 1980, la Flex machine a aussi été vendue, mais à très peu d'examplaires, comme les autres architectures à capacité. Et enfin, en 1981, l'architecture à capacité la plus connue, l'Intel iAPX 432 a été commercialisée. Depuis, la seule architecture de ce type est en cours de développement. Il s'agit de l'architecture CHERI, dont la mise en projet date de 2014.
===Le partage de la mémoire sur les architectures à capacités===
Le partage de segment est grandement modifié sur les architectures à capacité. Avec la segmentation normale, il y a une table de segment par processus. Les conséquences sont assez nombreuses, mais la principale est que partager un segment entre plusieurs processus est compliqué. Les défauts ont été évoqués plus haut. Les sélecteurs de segments ne sont pas les mêmes d'un processus à l'autre, pour un même segment. De plus, les adresses limite et de base sont dupliquées dans plusieurs tables de segments, et cela peut causer des problèmes de sécurité si une table des segments est modifiée et pas l'autre. Et il y a d'autres problèmes, tout aussi importants.
[[File:Partage des segments avec la segmentation.png|centre|vignette|upright=1.5|Partage des segments avec la segmentation]]
À l'opposé, les architectures à capacité utilisent une table des segments unique pour tous les processus. La table des segments unique sera appelée dans de ce qui suit la '''table des segments globale''', ou encore la table globale. En conséquence, les adresses de base et limite ne sont présentes qu'en un seul exemplaire par segment, au lieu d'être dupliquées dans autant de processus que nécessaire. De plus, cela garantit que l'indice de segment est le même quel que soit le processus qui l'utilise.
Un défaut de cette approche est au niveau des droits d'accès. Avec la segmentation normale, les droits d'accès pour un segment sont censés changer d'un processus à l'autre. Par exemple, tel processus a accès en lecture seule au segment, l'autre seulement en écriture, etc. Mais ici, avec une table des segments uniques, cela ne marche plus : incorporer les droits d'accès dans la table des segments ferait que tous les processus auraient les mêmes droits d'accès au segment. Et il faut trouver une solution.
===Les capacités sont des pointeurs protégés===
Pour éviter cela, les droits d'accès sont combinés avec les sélecteurs de segments. Les sélecteurs des segments sont remplacés par des '''capacités''', des pointeurs particuliers formés en concaténant l'indice de segment avec les droits d'accès à ce segment. Si un programme veut accéder à une adresse, il fournit une capacité de la forme "sélecteur:droits d'accès", et un décalage qui indique la position de l'adresse dans le segment.
Il est impossible d'accéder à un segment sans avoir la capacité associée, c'est là une sécurité importante. Un accès mémoire demande que l'on ait la capacité pour sélectionner le bon segment, mais aussi que les droits d'accès en permettent l'accès demandé. Par contre, les capacités peuvent être passées d'un programme à un autre sans problème, les deux programmes pourront accéder à un segment tant qu'ils disposent de la capacité associée.
[[File:Comparaison entre capacités et adresses segmentées.png|centre|vignette|upright=2.5|Comparaison entre capacités et adresses segmentées]]
Mais cette solution a deux problèmes très liés. Au niveau des sélecteurs de segment, le problème est que les sélecteur ont une portée globale. Avant, l'indice de segment était interne à un programme, un sélecteur ne permettait pas d'accéder au segment d'un autre programme. Sur les architectures à capacité, les sélecteurs ont une portée globale. Si un programme arrive à forger un sélecteur qui pointe vers un segment d'un autre programme, il peut théoriquement y accéder, à condition que les droits d'accès le permettent. Et c'est là qu'intervient le second problème : les droits d'accès ne sont plus protégés par l'espace noyau. Les droits d'accès étaient dans la table de segment, accessible uniquement en espace noyau, ce qui empêchait un processus de les modifier. Avec une capacité, il faut ajouter des mécanismes de protection qui empêchent un programme de modifier les droits d'accès à un segment et de générer un indice de segment non-prévu.
La première sécurité est qu'un programme ne peut pas créer une capacité, seul le système d'exploitation le peut. Les capacités sont forgées lors de l'allocation mémoire, ce qui est du ressort de l'OS. Pour rappel, un programme qui veut du rab de mémoire RAM peut demander au système d'exploitation de lui allouer de la mémoire supplémentaire. Le système d'exploitation renvoie alors un pointeurs qui pointe vers un nouveau segment. Le pointeur est une capacité. Il doit être impossible de forger une capacité, en-dehors d'une demande d'allocation mémoire effectuée par l'OS. Typiquement, la forge d'une capacité se fait avec des instructions du processeur, que seul l'OS peut éxecuter (pensez à une instruction qui n'est accessible qu'en espace noyau).
La seconde protection est que les capacités ne peuvent pas être modifiées sans raison valable, que ce soit pour l'indice de segment ou les droits d'accès. L'indice de segment ne peut pas être modifié, quelqu'en soit la raison. Pour les droits d'accès, la situation est plus compliquée. Il est possible de modifier ses droits d'accès, mais sous conditions. Réduire les droits d'accès d'une capacité est possible, que ce soit en espace noyau ou utilisateur, pas l'OS ou un programme utilisateur, avec une instruction dédiée. Mais augmenter les droits d'accès, seul l'OS peut le faire avec une instruction précise, souvent exécutable seulement en espace noyau.
Les capacités peuvent être copiées, et même transférées d'un processus à un autre. Les capacités peuvent être détruites, ce qui permet de libérer la mémoire utilisée par un segment. La copie d'une capacité est contrôlée par l'OS et ne peut se faire que sous conditions. La destruction d'une capacité est par contre possible par tous les processus. La destruction ne signifie pas que le segment est effacé, il est possible que d'autres processus utilisent encore des copies de la capacité, et donc le segment associé. On verra quand la mémoire est libérée plus bas.
Protéger les capacités demande plusieurs conditions. Premièrement, le processeur doit faire la distinction entre une capacité et une donnée. Deuxièmement, les capacités ne peuvent être modifiées que par des instructions spécifiques, dont l'exécution est protégée, réservée au noyau. En clair, il doit y avoir une séparation matérielle des capacités, qui sont placées dans des registres séparés. Pour cela, deux solutions sont possibles : soit les capacités remplacent les adresses et sont dispersées en mémoire, soit elles sont regroupées dans un segment protégé.
====La liste des capacités====
Avec la première solution, on regroupe les capacités dans un segment protégé. Chaque programme a accès à un certain nombre de segments et à autant de capacités. Les capacités d'un programme sont souvent regroupées dans une '''liste de capacités''', appelée la '''''C-list'''''. Elle est généralement placée en mémoire RAM. Elle est ce qu'il reste de la table des segments du processus, sauf que cette table ne contient pas les adresses du segment, qui sont dans la table globale. Tout se passe comme si la table des segments de chaque processus est donc scindée en deux : la table globale partagée entre tous les processus contient les informations sur les limites des segments, la ''C-list'' mémorise les droits d'accès et les sélecteurs pour identifier chaque segment. C'est un niveau d'indirection supplémentaire par rapport à la segmentation usuelle.
[[File:Architectures à capacité.png|centre|vignette|upright=2|Architectures à capacité]]
La liste de capacité est lisible par le programme, qui peut copier librement les capacités dans les registres. Par contre, la liste des capacités est protégée en écriture. Pour le programme, il est impossible de modifier les capacités dedans, impossible d'en rajouter, d'en forger, d'en retirer. De même, il ne peut pas accéder aux segments des autres programmes : il n'a pas les capacités pour adresser ces segments.
Pour protéger la ''C-list'' en écriture, la solution la plus utilisée consiste à placer la ''C-list'' dans un segment dédié. Le processeur gère donc plusieurs types de segments : les segments de capacité pour les ''C-list'', les autres types segments pour le reste. Un défaut de cette approche est que les adresses/capacités sont séparées des données. Or, les programmeurs mixent souvent adresses et données, notamment quand ils doivent manipuler des structures de données comme des listes chainées, des arbres, des graphes, etc.
L'usage d'une ''C-list'' permet de se passer de la séparation entre espace noyau et utilisateur ! Les segments de capacité sont eux-mêmes adressés par leur propre capacité, avec une capacité par segment de capacité. Le programme a accès à la liste de capacité, comme l'OS, mais leurs droits d'accès ne sont pas les mêmes. Le programme a une capacité vers la ''C-list'' qui n'autorise pas l'écriture, l'OS a une autre capacité qui accepte l'écriture. Les programmes ne pourront pas forger les capacités permettant de modifier les segments de capacité. Une méthode alternative est de ne permettre l'accès aux segments de capacité qu'en espace noyau, mais elle est redondante avec la méthode précédente et moins puissante.
====Les capacités dispersées, les architectures taguées====
Une solution alternative laisse les capacités dispersées en mémoire. Les capacités remplacent les adresses/pointeurs, et elles se trouvent aux mêmes endroits : sur la pile, dans le tas. Comme c'est le cas dans les programmes modernes, chaque allocation mémoire renvoie une capacité, que le programme gére comme il veut. Il peut les mettre dans des structures de données, les placer sur la pile, dans des variables en mémoire, etc. Mais il faut alors distinguer si un mot mémoire contient une capacité ou une autre donnée, les deux ne devant pas être mixés.
Pour cela, chaque mot mémoire se voit attribuer un certain bit qui indique s'il s'agit d'un pointeur/capacité ou d'autre chose. Mais cela demande un support matériel, ce qui fait que le processeur devient ce qu'on appelle une ''architecture à tags'', ou ''tagged architectures''. Ici, elles indiquent si le mot mémoire contient une adresse:capacité ou une donnée.
[[File:Architectures à capacité sans liste de capacité.png|centre|vignette|upright=2|Architectures à capacité sans liste de capacité]]
L'inconvénient est le cout en matériel de cette solution. Il faut ajouter un bit à chaque case mémoire, le processeur doit vérifier les tags avant chaque opération d'accès mémoire, etc. De plus, tous les mots mémoire ont la même taille, ce qui force les capacités à avoir la même taille qu'un entier. Ce qui est compliqué.
===Les registres de capacité===
Les architectures à capacité disposent de registres spécialisés pour les capacités, séparés pour les entiers. La raison principale est une question de sécurité, mais aussi une solution pragmatique au fait que capacités et entiers n'ont pas la même taille. Les registres dédiés aux capacités ne mémorisent pas toujours des capacités proprement dites. À la place, ils mémorisent des descripteurs de segment, qui contiennent l'adresse de base, limite et les droits d'accès. Ils sont utilisés pour la relocation des accès mémoire ultérieurs. Ils sont en réalité identiques aux registres de relocation, voire aux registres de segments. Leur utilité est d'accélérer la relocation, entre autres.
Les processeurs à capacité ne gèrent pas d'adresses proprement dit, comme pour la segmentation avec plusieurs registres de relocation. Les accès mémoire doivent préciser deux choses : à quel segment on veut accéder, à quelle position dans le segment se trouve la donnée accédée. La première information se trouve dans le mal nommé "registre de capacité", la seconde information est fournie par l'instruction d'accès mémoire soit dans un registre (Base+Index), soit en adressage base+''offset''.
Les registres de capacités sont accessibles à travers des instructions spécialisées. Le processeur ajoute des instructions LOAD/STORE pour les échanges entre table des segments et registres de capacité. Ces instructions sont disponibles en espace utilisateur, pas seulement en espace noyau. Lors du chargement d'une capacité dans ces registres, le processeur vérifie que la capacité chargée est valide, et que les droits d'accès sont corrects. Puis, il accède à la table des segments, récupère les adresses de base et limite, et les mémorise dans le registre de capacité. Les droits d'accès et d'autres méta-données sont aussi mémorisées dans le registre de capacité. En somme, l'instruction de chargement prend une capacité et charge un descripteur de segment dans le registre.
Avec ce genre de mécanismes, il devient difficile d’exécuter certains types d'attaques, ce qui est un gage de sureté de fonctionnement indéniable. Du moins, c'est la théorie, car tout repose sur l'intégrité des listes de capacité. Si on peut modifier celles-ci, alors il devient facile de pouvoir accéder à des objets auxquels on n’aurait pas eu droit.
===Le recyclage de mémoire matériel===
Les architectures à capacité séparent les adresses/capacités des nombres entiers. Et cela facilite grandement l'implémentation de la ''garbage collection'', ou '''recyclage de la mémoire''', à savoir un ensemble de techniques logicielles qui visent à libérer la mémoire inutilisée.
Rappelons que les programmes peuvent demander à l'OS un rab de mémoire pour y placer quelque chose, généralement une structure de donnée ou un objet. Mais il arrive un moment où cet objet n'est plus utilisé par le programme. Il peut alors demander à l'OS de libérer la portion de mémoire réservée. Sur les architectures à capacité, cela revient à libérer un segment, devenu inutile. La mémoire utilisée par ce segment est alors considérée comme libre, et peut être utilisée pour autre chose. Mais il arrive que les programmes ne libèrent pas le segment en question. Soit parce que le programmeur a mal codé son programme, soit parce que le compilateur n'a pas fait du bon travail ou pour d'autres raisons.
Pour éviter cela, les langages de programmation actuels incorporent des '''''garbage collectors''''', des morceaux de code qui scannent la mémoire et détectent les segments inutiles. Pour cela, ils doivent identifier les adresses manipulées par le programme. Si une adresse pointe vers un objet, alors celui-ci est accessible, il sera potentiellement utilisé dans le futur. Mais si aucune adresse ne pointe vers l'objet, alors il est inaccessible et ne sera plus jamais utilisé dans le futur. On peut libérer les objets inaccessibles.
Identifier les adresses est cependant très compliqué sur les architectures normales. Sur les processeurs modernes, les ''garbage collectors'' scannent la pile à la recherche des adresses, et considèrent tout mot mémoire comme une adresse potentielle. Mais les architectures à capacité rendent le recyclage de la mémoire très facile. Un segment est accessible si le programme dispose d'une capacité qui pointe vers ce segment, rien de plus. Et les capacités sont facilement identifiables : soit elles sont dans la liste des capacités, soit on peut les identifier à partir de leur ''tag''.
Le recyclage de mémoire était parfois implémenté directement en matériel. En soi, son implémentation est assez simple, et peu être réalisé dans le microcode d'un processeur. Une autre solution consiste à utiliser un second processeur, spécialement dédié au recyclage de mémoire, qui exécute un programme spécialement codé pour. Le programme en question est placé dans une mémoire ROM, reliée directement à ce second processeur.
===L'intel iAPX 432===
Voyons maintenat une architecture à capacité assez connue : l'Intel iAPX 432. Oui, vous avez bien lu : Intel a bel et bien réalisé un processeur orienté objet dans sa jeunesse. La conception du processeur Intel iAPX 432 commença en 1975, afin de créer un successeur digne de ce nom aux processeurs 8008 et 8080.
La conception du processeur Intel iAPX 432 commença en 1975, afin de créer un successeur digne de ce nom aux processeurs 8008 et 8080. Ce processeur s'est très faiblement vendu en raison de ses performances assez désastreuses et de défauts techniques certains. Par exemple, ce processeur était une machine à pile à une époque où celles-ci étaient tombées en désuétude, il ne pouvait pas effectuer directement de calculs avec des constantes entières autres que 0 et 1, ses instructions avaient un alignement bizarre (elles étaient bit-alignées). Il avait été conçu pour maximiser la compatibilité avec le langage ADA, un langage assez peu utilisé, sans compter que le compilateur pour ce processeur était mauvais.
====Les segments prédéfinis de l'Intel iAPX 432====
L'Intel iAPX432 gère plusieurs types de segments. Rien d'étonnant à cela, les Burrough géraient eux aussi plusieurs types de segments, à savoir des segments de programmes, des segments de données, et des segments d'I/O. C'est la même chose sur l'Intel iAPX 432, mais en bien pire !
Les segments de données sont des segments génériques, dans lequels on peut mettre ce qu'on veut, suivant les besoins du programmeur. Ils sont tous découpés en deux parties de tailles égales : une partie contenant les données de l'objet et une partie pour les capacités. Les capacités d'un segment pointent vers d'autres segments, ce qui permet de créer des structures de données assez complexes. La ligne de démarcation peut être placée n'importe où dans le segment, les deux portions ne sont pas de taille identique, elles ont des tailles qui varient de segment en segment. Il est même possible de réserver le segment entier à des données sans y mettre de capacités, ou inversement. Les capacités et données sont adressées à partir de la ligne de démarcation, qui sert d'adresse de base du segment. Suivant l'instruction utilisée, le processeur accède à la bonne portion du segment.
Le processeur supporte aussi d'autres segments pré-définis, qui sont surtout utilisés par le système d'exploitation :
* Des segments d'instructions, qui contiennent du code exécutable, typiquement un programme ou des fonctions, parfois des ''threads''.
* Des segments de processus, qui mémorisent des processus entiers. Ces segments contiennent des capacités qui pointent vers d'autres segments, notamment un ou plusieurs segments de code, et des segments de données.
* Des segments de domaine, pour les modules ou bibliothèques dynamiques.
* Des segments de contexte, utilisés pour mémoriser l'état d'un processus, utilisés par l'OS pour faire de la commutation de contexte.
* Des segments de message, utilisés pour la communication entre processus par l'intermédiaire de messages.
* Et bien d'autres encores.
Sur l'Intel iAPX 432, chaque processus est considéré comme un objet à part entière, qui a son propre segment de processus. De même, l'état du processeur (le programme qu'il est en train d’exécuter, son état, etc.) est stocké en mémoire dans un segment de contexte. Il en est de même pour chaque fonction présente en mémoire : elle était encapsulée dans un segment, sur lequel seules quelques manipulations étaient possibles (l’exécuter, notamment). Et ne parlons pas des appels de fonctions qui stockaient l'état de l'appelé directement dans un objet spécial. Bref, de nombreux objets système sont prédéfinis par le processeur : les objets stockant des fonctions, les objets stockant des processus, etc.
L'Intel 432 possédait dans ses circuits un ''garbage collector'' matériel. Pour faciliter son fonctionnement, certains bits de l'objet permettaient de savoir si l'objet en question pouvait être supprimé ou non.
====Le support de la segmentation sur l'Intel iAPX 432====
La table des segments est une table hiérarchique, à deux niveaux. Le premier niveau est une ''Object Table Directory'', qui réside toujours en mémoire RAM. Elle contient des descripteurs qui pointent vers des tables secondaires, appelées des ''Object Table''. Il y a plusieurs ''Object Table'', typiquement une par processus. Plusieurs processus peuvent partager la même ''Object Table''. Les ''Object Table'' peuvent être swappées, mais pas l{{'}}''Object Table Directory''.
Une capacité tient compte de l'organisation hiérarchique de la table des segments. Elle contient un indice qui précise quelle ''Object Table'' utiliser, et l'indice du segment dans cette ''Object Table''. Le premier indice adresse l{{'}}''Object Table Directory'' et récupère un descripteur de segment qui pointe sur la bonne ''Object Table''. Le second indice est alors utilisé pour lire l'adresse de base adéquate dans cette ''Object Table''. La capacité contient aussi des droits d'accès en lecture, écriture, suppression et copie. Il y a aussi un champ pour le type, qu'on verra plus bas. Au fait : les capacités étaient appelées des ''Access Descriptors'' dans la documentation officielle.
Une capacité fait 32 bits, avec un octet utilisé pour les droits d'accès, laissant 24 bits pour adresser les segments. Le processeur gérait jusqu'à 2^24 segments/objets différents, pouvant mesurer jusqu'à 64 kibioctets chacun, ce qui fait 2^40 adresses différentes, soit 1024 gibioctets. Les 24 bits pour adresser les segments sont partagés moitié-moitié pour l'adressage des tables, ce qui fait 4096 ''Object Table'' différentes dans l{{'}}''Object Table Directory'', et chaque ''Object Table'' contient 4096 segments.
====Le jeu d'instruction de l'Intel iAPX 432====
L'Intel iAPX 432 est une machine à pile. Le jeu d'instruction de l'Intel iAPX 432 gère pas moins de 230 instructions différentes. Il gére deux types d'instructions : les instructions normales, et celles qui manipulent des segments/objets. Les premières permettent de manipuler des nombres entiers, des caractères, des chaînes de caractères, des tableaux, etc.
Les secondes sont spécialement dédiées à la manipulation des capacités. Il y a une instruction pour copier une capacité, une autre pour invalider une capacité, une autre pour augmenter ses droits d'accès (instruction sécurisée, exécutable seulement sous certaines conditions), une autre pour restreindre ses droits d'accès. deux autres instructions créent un segment et renvoient la capacité associée, la première créant un segment typé, l'autre non.
le processeur gérait aussi des instructions spécialement dédiées à la programmation système et idéales pour programmer des systèmes d'exploitation. De nombreuses instructions permettaient ainsi de commuter des processus, faire des transferts de messages entre processus, etc. Environ 40 % du micro-code était ainsi spécialement dédié à ces instructions spéciales.
Les instructions sont de longueur variable et peuvent prendre n'importe quelle taille comprise entre 10 et 300 bits, sans vraiment de restriction de taille. Les bits d'une instruction sont regroupés en 4 grands blocs, 4 champs, qui ont chacun une signification particulière.
* Le premier est l'opcode de l'instruction.
* Le champ référence, doit être interprété différemment suivant la donnée à manipuler. Si cette donnée est un entier, un caractère ou un flottant, ce champ indique l'emplacement de la donnée en mémoire. Alors que si l'instruction manipule un objet, ce champ spécifie la capacité de l'objet en question. Ce champ est assez complexe et il est sacrément bien organisé.
* Le champ format, n'utilise que 4 bits et a pour but de préciser si les données à manipuler sont en mémoire ou sur la pile.
* Le champ classe permet de dire combien de données différentes l'instruction va devoir manipuler, et quelles seront leurs tailles.
[[File:Encodage des instructions de l'Intel iAPX-432.png|centre|vignette|upright=2|Encodage des instructions de l'Intel iAPX-432.]]
====Le support de l'orienté objet sur l'Intel iAPX 432====
L'Intel 432 permet de définir des objets, qui correspondent aux classes des langages orientés objets. L'Intel 432 permet, à partir de fonctions définies par le programmeur, de créer des '''''domain objects''''', qui correspondent à une classe. Un ''domain object'' est un segment de capacité, dont les capacités pointent vers des fonctions ou un/plusieurs objets. Les fonctions et les objets sont chacun placés dans un segment. Une partie des fonctions/objets sont publics, ce qui signifie qu'ils sont accessibles en lecture par l'extérieur. Les autres sont privées, inaccessibles aussi bien en lecture qu'en écriture.
L'exécution d'une fonction demande que le branchement fournisse deux choses : une capacité vers le ''domain object'', et la position de la fonction à exécuter dans le segment. La position permet de localiser la capacité de la fonction à exécuter. En clair, on accède au ''domain object'' d'abord, pour récupérer la capacité qui pointe vers la fonction à exécuter.
Il est aussi possible pour le programmeur de définir de nouveaux types non supportés par le processeur, en faisant appel au système d'exploitation de l'ordinateur. Au niveau du processeur, chaque objet est typé au niveau de son object descriptor : celui-ci contient des informations qui permettent de déterminer le type de l'objet. Chaque type se voit attribuer un domain object qui contient toutes les fonctions capables de manipuler les objets de ce type et que l'on appelle le type manager. Lorsque l'on veut manipuler un objet d'un certain type, il suffit d'accéder à une capacité spéciale (le TCO) qui pointera dans ce type manager et qui précisera quel est l'objet à manipuler (en sélectionnant la bonne entrée dans la liste de capacité). Le type d'un objet prédéfini par le processeur est ainsi spécifié par une suite de 8 bits, tandis que le type d'un objet défini par le programmeur est défini par la capacité spéciale pointant vers son type manager.
===Conclusion===
Pour ceux qui veulent en savoir plus, je conseille la lecture de ce livre, disponible gratuitement sur internet (merci à l'auteur pour cette mise à disposition) :
* [https://homes.cs.washington.edu/~levy/capabook/ Capability-Based Computer Systems].
Voici un document qui décrit le fonctionnement de l'Intel iAPX432 :
* [https://homes.cs.washington.edu/~levy/capabook/Chapter9.pdf The Intel iAPX 432 ]
==La pagination==
Avec la pagination, la mémoire est découpée en blocs de taille fixe, appelés des '''pages mémoires'''. La taille des pages varie suivant le processeur et le système d'exploitation et tourne souvent autour de 4 kibioctets. Mais elles sont de taille fixe : on ne peut pas en changer la taille. C'est la différence avec les segments, qui sont de taille variable. Le contenu d'une page en mémoire fictive est rigoureusement le même que le contenu de la page correspondante en mémoire physique.
L'espace d'adressage est découpé en '''pages logiques''', alors que la mémoire physique est découpée en '''pages physique''' de même taille. Les pages logiques correspondent soit à une page physique, soit à une page swappée sur le disque dur. Quand une page logique est associée à une page physique, les deux ont le même contenu, mais pas les mêmes adresses. Les pages logiques sont numérotées, en partant de 0, afin de pouvoir les identifier/sélectionner. Même chose pour les pages physiques, qui sont elles aussi numérotées en partant de 0.
[[File:Principe de la pagination.png|centre|vignette|upright=2|Principe de la pagination.]]
Pour information, le tout premier processeur avec un système de mémoire virtuelle était le super-ordinateur Atlas. Il utilisait la pagination, et non la segmentation. Mais il fallu du temps avant que la méthode de la pagination prenne son essor dans les processeurs commerciaux x86.
Un point important est que la pagination implique une coopération entre OS et hardware, les deux étant fortement mélés. Une partie des informations de cette section auraient tout autant leur place dans le wikilivre sur les systèmes d'exploitation, mais il est plus simple d'en parler ici.
===La mémoire virtuelle : le ''swapping'' et le remplacement des pages mémoires===
Le système d'exploitation mémorise des informations sur toutes les pages existantes dans une '''table des pages'''. C'est un tableau où chaque ligne est associée à une page logique. Une ligne contient un bit ''Valid'' qui indique si la page logique associée est swappée sur le disque dur ou non, et la position de la page physique correspondante en mémoire RAM. Elle peut aussi contenir des bits pour la protection mémoire, et bien d'autres. Les lignes sont aussi appelées des ''entrées de la table des pages''
[[File:Gestionnaire de mémoire virtuelle - Pagination et swapping.png|centre|vignette|upright=2|Table des pages.]]
De plus, le système d'exploitation conserve une '''liste des pages vides'''. Le nom est assez clair : c'est une liste de toutes les pages de la mémoire physique qui sont inutilisées, qui ne sont allouées à aucun processus. Ces pages sont de la mémoire libre, utilisable à volonté. La liste des pages vides est mise à jour à chaque fois qu'un programme réserve de la mémoire, des pages sont alors prises dans cette liste et sont allouées au programme demandeur.
====Les défauts de page====
Lorsque l'on veut traduire l'adresse logique d'une page mémoire, le processeur vérifie le bit ''Valid'' et l'adresse physique. Si le bit ''Valid'' est à 1 et que l'adresse physique est présente, la traduction d'adresse s'effectue normalement. Mais si ce n'est pas le cas, l'entrée de la table des pages ne contient pas de quoi faire la traduction d'adresse. Soit parce que la page est swappée sur le disque dur et qu'il faut la copier en RAM, soit parce que les droits d'accès ne le permettent pas, soit parce que la page n'a pas encore été allouée, etc. On fait alors face à un '''défaut de page'''. Un défaut de page a lieu quand la MMU ne peut pas associer l'adresse logique à une adresse physique, quelque qu'en soit la raison.
Il existe deux types de défauts de page : mineurs et majeurs. Un '''défaut de page majeur''' a lieu quand on veut accéder à une page déplacée sur le disque dur. Un défaut de page majeur lève une exception matérielle dont la routine rapatriera la page en mémoire RAM. S'il y a de la place en mémoire RAM, il suffit d'allouer une page vide et d'y copier la page chargée depuis le disque dur. Mais si ce n'est par le cas, on va devoir faire de la place en RAM en déplaçant une page mémoire de la RAM vers le disque dur. Dans tous les cas, c'est le système d'exploitation qui s'occupe du chargement de la page, le processeur n'est pas impliqué. Une fois la page chargée, la table des pages est mise à jour et la traduction d'adresse peut recommencer. Si je dis recommencer, c'est car l'accès mémoire initial est rejoué à l'identique, sauf que la traduction d'adresse réussit cette fois-ci.
Un '''défaut de page mineur''' a lieu dans des circonstances pas très intuitives : la page est en mémoire physique, mais l'adresse physique de la page n'est pas accessible. Par exemple, il est possible que des sécurités empêchent de faire la traduction d'adresse, pour des raisons de protection mémoire. Une autre raison est la gestion des adresses synonymes, qui surviennent quand on utilise des libraires partagées entre programmes, de la communication inter-processus, des optimisations de type ''copy-on-write'', etc. Enfin, une dernière raison est que la page a été allouée à un programme par le système d'exploitation, mais qu'il n'a pas encore attribué sa position en mémoire. Pour comprendre comment c'est possible, parlons rapidement de l'allocation paresseuse.
Imaginons qu'un programme fasse une demande d'allocation mémoire et se voit donc attribuer une ou plusieurs pages logiques. L'OS peut alors réagir de deux manières différentes. La première est d'attribuer une page physique immédiatement, en même temps que la page logique. En faisant ainsi, on ne peut pas avoir de défaut mineur, sauf en cas de problème de protection mémoire. Cette solution est simple, on l'appelle l{{'}}'''allocation immédiate'''. Une autre solution consiste à attribuer une page logique, mais l'allocation de la page physique se fait plus tard. Elle a lieu la première fois que le programme tente d'écrire/lire dans la page physique. Un défaut mineur a lieu, et c'est lui qui force l'OS à attribuer une page physique pour la page logique demandée. On parle alors d{{'}}'''allocation paresseuse'''. L'avantage est que l'on gagne en performance si des pages logiques sont allouées mais utilisées, ce qui peut arriver.
Une optimisation permise par l'existence des défauts mineurs est le '''''copy-on-write'''''. Le but est d'optimiser la copie d'une page logique dans une autre. L'idée est que la copie est retardée quand elle est vraiment nécessaire, à savoir quand on écrit dans la copie. Tant que l'on ne modifie pas la copie, les deux pages logiques, originelle et copiée, pointent vers la même page physique. A quoi bon avoir deux copies avec le même contenu ? Par contre, la page physique est marquée en lecture seule. La moindre écriture déclenche une erreur de protection mémoire, et un défaut mineur. Celui-ci est géré par l'OS, qui effectue alors la copie dans une nouvelle page physique.
Je viens de dire que le système d'exploitation gère les défauts de page majeurs/mineurs. Un défaut de page déclenche une exception matérielle, qui passe la main au système d'exploitation. Le système d'exploitation doit alors déterminer ce qui a levé l'exception, notamment identifier si c'est un défaut de page mineur ou majeur. Pour cela, le processeur a un ou plusieurs '''registres de statut''' qui indique l'état du processeur, qui sont utiles pour gérer les défauts de page. Ils indiquent quelle est l'adresse fautive, si l'accès était une lecture ou écriture, si l'accès a eu lieu en espace noyau ou utilisateur (les espaces mémoire ne sont pas les mêmes), etc. Les registres en question varient grandement d'une architecture de processeur à l'autre, aussi on ne peut pas dire grand chose de plus sur le sujet. Le reste est de toute façon à voir dans un cours sur les systèmes d'exploitation.
====Le remplacement des pages====
Les pages virtuelles font référence soit à une page en mémoire physique, soit à une page sur le disque dur. Mais l'on ne peut pas lire une page directement depuis le disque dur. Les pages sur le disque dur doivent être chargées en RAM, avant d'être utilisables. Ce n'est possible que si on a une page mémoire vide, libre. Si ce n'est pas le cas, on doit faire de la place en swappant une page sur le disque dur. Les pages font ainsi une sorte de va et vient entre le fichier d'échange et la RAM, suivant les besoins. Tout cela est effectué par une routine d'interruption du système d'exploitation, le processeur n'ayant pas vraiment de rôle là-dedans.
Supposons que l'on veuille faire de la place en RAM pour une nouvelle page. Dans une implémentation naïve, on trouve une page à évincer de la mémoire, qui est copiée dans le ''swapfile''. Toutes les pages évincées sont alors copiées sur le disque dur, à chaque remplacement. Néanmoins, cette implémentation naïve peut cependant être améliorée si on tient compte d'un point important : si la page a été modifiée depuis le dernier accès. Si le programme/processeur a écrit dans la page, alors celle-ci a été modifiée et doit être sauvegardée sur le ''swapfile'' si elle est évincée. Par contre, si ce n'est pas le cas, la page est soit initialisée, soit déjà présente à l'identique dans le ''swapfile''.
Mais cette optimisation demande de savoir si une écriture a eu lieu dans la page. Pour cela, on ajoute un '''''dirty bit''''' à chaque entrée de la table des pages, juste à côté du bit ''Valid''. Il indique si une écriture a eu lieu dans la page depuis qu'elle a été chargée en RAM. Ce bit est mis à jour par le processeur, automatiquement, lors d'une écriture. Par contre, il est remis à zéro par le système d'exploitation, quand la page est chargée en RAM. Si le programme se voit allouer de la mémoire, il reçoit une page vide, et ce bit est initialisé à 0. Il est mis à 1 si la mémoire est utilisée. Quand la page est ensuite swappée sur le disque dur, ce bit est remis à 0 après la sauvegarde.
Sur la majorité des systèmes d'exploitation, il est possible d'interdire le déplacement de certaines pages sur le disque dur. Ces pages restent alors en mémoire RAM durant un temps plus ou moins long, parfois en permanence. Cette possibilité simplifie la vie des programmeurs qui conçoivent des systèmes d'exploitation : essayez d'exécuter l'interruption pour les défauts de page alors que la page contenant le code de l'interruption est placée sur le disque dur ! Là encore, cela demande d'ajouter un bit dans chaque entrée de la table des pages, qui indique si la page est swappable ou non. Le bit en question s'appelle souvent le '''bit ''swappable'''''.
====Les algorithmes de remplacement des pages pris en charge par l'OS====
Le choix de la page doit être fait avec le plus grand soin et il existe différents algorithmes qui permettent de décider quelle page supprimer de la RAM. Leur but est de swapper des pages qui ne seront pas accédées dans le futur, pour éviter d'avoir à faire triop de va-et-vient entre RAM et ''swapfile''. Les données qui sont censées être accédées dans le futur doivent rester en RAM et ne pas être swappées, autant que possible. Les algorithmes les plus simples pour le choix de page à évincer sont les suivants.
Le plus simple est un algorithme aléatoire : on choisit la page au hasard. Mine de rien, cet algorithme est très simple à implémenter et très rapide à exécuter. Il ne demande pas de modifier la table des pages, ni même d'accéder à celle-ci pour faire son choix. Ses performances sont surprenamment correctes, bien que largement en-dessous de tous les autres algorithmes.
L'algorithme FIFO supprime la donnée qui a été chargée dans la mémoire avant toutes les autres. Cet algorithme fonctionne bien quand un programme manipule des tableaux de grande taille, mais fonctionne assez mal dans le cas général.
L'algorithme LRU supprime la donnée qui été lue ou écrite pour la dernière fois avant toutes les autres. C'est théoriquement le plus efficace dans la majorité des situations. Malheureusement, son implémentation est assez complexe et les OS doivent modifier la table des pages pour l'implémenter.
L'algorithme le plus utilisé de nos jours est l{{'}}'''algorithme NRU''' (''Not Recently Used''), une simplification drastique du LRU. Il fait la différence entre les pages accédées il y a longtemps et celles accédées récemment, d'une manière très binaire. Les deux types de page sont appelés respectivement les '''pages froides''' et les '''pages chaudes'''. L'OS swappe en priorité les pages froides et ne swappe de page chaude que si aucune page froide n'est présente. L'algorithme est simple : il choisit la page à évincer au hasard parmi une page froide. Si aucune page froide n'est présente, alors il swappe au hasard une page chaude.
Pour implémenter l'algorithme NRU, l'OS mémorise, dans chaque entrée de la table des pages, si la page associée est froide ou chaude. Pour cela, il met à 0 ou 1 un bit dédié : le '''bit ''Accessed'''''. La différence avec le bit ''dirty'' est que le bit ''dirty'' est mis à jour uniquement lors des écritures, alors que le bit ''Accessed'' l'est aussi lors d'une lecture. Uen lecture met à 1 le bit ''Accessed'', mais ne touche pas au bit ''dirty''. Les écritures mettent les deux bits à 1.
Implémenter l'algorithme NRU demande juste de mettre à jour le bit ''Accessed'' de chaque entrée de la table des pages. Et sur les architectures modernes, le processeur s'en charge automatiquement. A chaque accès mémoire, que ce soit en lecture ou en écriture, le processeur met à 1 ce bit. Par contre, le système d'exploitation le met à 0 à intervalles réguliers. En conséquence, quand un remplacement de page doit avoir lieu, les pages chaudes ont de bonnes chances d'avoir le bit ''Accessed'' à 1, alors que les pages froides l'ont à 0. Ce n'est pas certain, et on peut se trouver dans des cas où ce n'est pas le cas. Par exemple, si un remplacement a lieu juste après la remise à zéro des bits ''Accessed''. Le choix de la page à remplacer est donc imparfait, mais fonctionne bien en pratique.
Tous les algorithmes précédents ont chacun deux variantes : une locale, et une globale. Avec la version locale, la page qui va être rapatriée sur le disque dur est une page réservée au programme qui est la cause du page miss. Avec la version globale, le système d'exploitation va choisir la page à virer parmi toutes les pages présentes en mémoire vive.
===La protection mémoire avec la pagination===
Avec la pagination, chaque page a des '''droits d'accès''' précis, qui permettent d'autoriser ou interdire les accès en lecture, écriture, exécution, etc. La table des pages mémorise les autorisations pour chaque page, sous la forme d'une suite de bits où chaque bit autorise/interdit une opération bien précise. En pratique, les tables de pages modernes disposent de trois bits : un qui autorise/interdit les accès en lecture, un qui autorise/interdit les accès en écriture, un qui autorise/interdit l'éxecution du contenu de la page.
Le format exact de la suite de bits a cependant changé dans le temps sur les processeurs x86 modernes. Par exemple, avant le passage au 64 bits, les CPU et OS ne pouvaient pas marquer une page mémoire comme non-exécutable. C'est seulement avec le passage au 64 bits qu'a été ajouté un bit pour interdire l'exécution de code depuis une page. Ce bit, nommé '''bit NX''', est à 0 si la page n'est pas exécutable et à 1 sinon. Le processeur vérifie à chaque chargement d'instruction si le bit NX de page lue est à 1. Sinon, il lève une exception matérielle et laisse la main à l'OS.
Une amélioration de cette protection est la technique dite du '''''Write XOR Execute''''', abréviée WxX. Elle consiste à interdire les pages d'être à la fois accessibles en écriture et exécutables. Il est possible de changer les autorisations en cours de route, ceci dit.
Les premiers IBM 360 disposaient d'un mécanisme de protection mémoire totalement différent, sans registres limite/base. Ce mécanisme de protection attribue à chaque programme une '''clé de protection''', qui consiste en un nombre unique de 4 bits (chaque programme a donc une clé différente de ses collègues). La mémoire est fragmentée en blocs de même taille, de 2 kibioctets. Le processeur mémorise, pour chacun de ses blocs, la clé de protection du programme qui a réservé ce bloc. À chaque accès mémoire, le processeur compare la clé de protection du programme en cours d’exécution et celle du bloc de mémoire de destination. Si les deux clés sont différentes, alors un programme a effectué un accès hors des clous et il se fait sauvagement arrêter.
===La traduction d'adresse avec la pagination===
Comme dit plus haut, les pages sont numérotées, de 0 à une valeur maximale, afin de les identifier. Le numéro en question est appelé le '''numéro de page'''. Il est utilisé pour dire au processeur : je veux lire une donnée dans la page numéro 20, la page numéro 90, etc. Une fois qu'on a le numéro de page, on doit alors préciser la position de la donnée dans la page, appelé le '''décalage''', ou encore l{{'}}''offset''.
Le numéro de page et le décalage se déduisent à partir de l'adresse, en divisant l'adresse par la taille de la page. Le quotient obtenu donne le numéro de la page, alors que le reste est le décalage. Les processeurs actuels utilisent tous des pages dont la taille est une puissance de deux, ce qui fait que ce calcul est fortement simplifié. Sous cette condition, le numéro de page correspond aux bits de poids fort de l'adresse, alors que le décalage est dans les bits de poids faible.
Le numéro de page existe en deux versions : un numéro de page physique qui identifie une page en mémoire physique, et un numéro de page logique qui identifie une page dans la mémoire virtuelle. Traduire l'adresse logique en adresse physique demande de remplacer le numéro de la page logique en un numéro de page physique.
[[File:Phycical address.JPG|centre|vignette|upright=2|Traduction d'adresse avec la pagination.]]
====Les tables des pages simples====
Dans le cas le plus simple, il n'y a qu'une seule table des pages, qui est adressée par les numéros de page logique. La table des pages est un vulgaire tableau d'adresses physiques, placées les unes à la suite des autres. Avec cette méthode, la table des pages a autant d'entrée qu'il y a de pages logiques en mémoire virtuelle. Accéder à la mémoire nécessite donc d’accéder d'abord à la table des pages en mémoire, de calculer l'adresse de l'entrée voulue, et d’y accéder.
[[File:Table des pages.png|centre|vignette|upright=2|Table des pages.]]
La table des pages est souvent stockée dans la mémoire RAM, son adresse est connue du processeur, mémorisée dans un registre spécialisé du processeur. Le processeur effectue automatiquement le calcul d'adresse à partir de l'adresse de base et du numéro de page logique.
[[File:Address translation (32-bit).png|centre|vignette|upright=2|Address translation (32-bit)]]
====Les tables des pages inversées====
Sur certains systèmes, notamment sur les architectures 64 bits ou plus, le nombre de pages est très important. Sur les ordinateurs x86 récents, les adresses sont en pratique de 48 bits, les bits de poids fort étant ignorés en pratique, ce qui fait en tout 68 719 476 736 pages. Chaque entrée de la table des pages fait au minimum 48 bits, mais fait plus en pratique : partons sur 64 bits par entrée, soit 8 octets. Cela fait 549 755 813 888 octets pour la table des pages, soit plusieurs centaines de gibioctets ! Une table des pages normale serait tout simplement impraticable.
Pour résoudre ce problème, on a inventé les '''tables des pages inversées'''. L'idée derrière celles-ci est l'inverse de la méthode précédente. La méthode précédente stocke, pour chaque page logique, son numéro de page physique. Les tables des pages inversées font l'inverse : elles stockent, pour chaque numéro de page physique, la page logique qui correspond. Avec cette méthode table des pages contient ainsi autant d'entrées qu'il y a de pages physiques. Elle est donc plus petite qu'avant, vu que la mémoire physique est plus petite que la mémoire virtuelle.
Quand le processeur veut convertir une adresse virtuelle en adresse physique, la MMU recherche le numéro de page de l'adresse virtuelle dans la table des pages. Le numéro de l'entrée à laquelle se trouve ce morceau d'adresse virtuelle est le morceau de l'adresse physique. Pour faciliter le processus de recherche dans la page, la table des pages inversée est ce que l'on appelle une table de hachage. C'est cette solution qui est utilisée sur les processeurs Power PC.
[[File:Table des pages inversée.jpg|centre|vignette|upright=2|Table des pages inversée.]]
====Les tables des pages multiples par espace d'adressage====
Dans les deux cas précédents, il y a une table des pages unique. Cependant, les concepteurs de processeurs et de systèmes d'exploitation ont remarqué que les adresses les plus hautes et/ou les plus basses sont les plus utilisées, alors que les adresses situées au milieu de l'espace d'adressage sont peu utilisées en raison du fonctionnement de la pile et du tas. Il y a donc une partie de la table des pages qui ne sert à rien et est utilisé pour des adresses inutilisées. C'est une source d'économie d'autant plus importante que les tables des pages sont de plus en plus grosses.
Pour profiter de cette observation, les concepteurs d'OS ont décidé de découper l'espace d'adressage en plusieurs sous-espaces d'adressage de taille identique : certains localisés dans les adresses basses, d'autres au milieu, d'autres tout en haut, etc. Et vu que l'espace d'adressage est scindé en plusieurs parties, la table des pages l'est aussi, elle est découpée en plusieurs sous-tables. Si un sous-espace d'adressage n'est pas utilisé, il n'y a pas besoin d'utiliser de la mémoire pour stocker la table des pages associée. On ne stocke que les tables des pages pour les espaces d'adressage utilisés, ceux qui contiennent au moins une donnée.
L'utilisation de plusieurs tables des pages ne fonctionne que si le système d'exploitation connaît l'adresse de chaque table des pages (celle de la première entrée). Pour cela, le système d'exploitation utilise une super-table des pages, qui stocke les adresses de début des sous-tables de chaque sous-espace. En clair, la table des pages est organisé en deux niveaux, la super-table étant le premier niveau et les sous-tables étant le second niveau.
L'adresse est structurée de manière à tirer profit de cette organisation. Les bits de poids fort de l'adresse sélectionnent quelle table de second niveau utiliser, les bits du milieu de l'adresse sélectionne la page dans la table de second niveau et le reste est interprété comme un ''offset''. Un accès à la table des pages se fait comme suit. Les bits de poids fort de l'adresse sont envoyés à la table de premier niveau, et sont utilisés pour récupérer l'adresse de la table de second niveau adéquate. Les bits au milieu de l'adresse sont envoyés à la table de second niveau, pour récupérer le numéro de page physique. Le tout est combiné avec l{{'}}''offset'' pour obtenir l'adresse physique finale.
[[File:Table des pages hiérarchique.png|centre|vignette|upright=2|Table des pages hiérarchique.]]
On peut aussi aller plus loin et découper la table des pages de manière hiérarchique, chaque sous-espace d'adressage étant lui aussi découpé en sous-espaces d'adressages. On a alors une table de premier niveau, plusieurs tables de second niveau, encore plus de tables de troisième niveau, et ainsi de suite. Cela peut aller jusqu'à 5 niveaux sur les processeurs x86 64 bits modernes. On parle alors de '''tables des pages emboitées'''. Dans ce cours, la table des pages désigne l'ensemble des différents niveaux de cette organisation, toutes les tables inclus. Seules les tables du dernier niveau mémorisent des numéros de page physiques, les autres tables mémorisant des pointeurs, des adresses vers le début des tables de niveau inférieur. Un exemple sera donné plus bas, dans la section suivante.
====L'exemple des processeurs x86====
Pour rendre les explications précédentes plus concrètes, nous allons prendre l'exemple des processeur x86 anciens, de type 32 bits. Les processeurs de ce type utilisaient deux types de tables des pages : une table des page unique et une table des page hiérarchique. Les deux étaient utilisées dans cas séparés. La table des page unique était utilisée pour les pages larges et encore seulement en l'absence de la technologie ''physical adress extension'', dont on parlera plus bas. Les autres cas utilisaient une table des page hiérarchique, à deux niveaux, trois niveaux, voire plus.
Une table des pages unique était utilisée pour les pages larges (de 2 mébioctets et plus). Pour les pages de 4 mébioctets, il y avait une unique table des pages, adressée par les 10 bits de poids fort de l'adresse, les bits restants servant comme ''offset''. La table des pages contenait 1024 entrées de 4 octets chacune, ce qui fait en tout 4 kibioctet pour la table des pages. La table des page était alignée en mémoire sur un bloc de 4 kibioctet (sa taille).
[[File:X86 Paging 4M.svg|centre|vignette|upright=2|X86 Paging 4M]]
Pour les pages de 4 kibioctets, les processeurs x86-32 bits utilisaient une table des page hiérarchique à deux niveaux. Les 10 bits de poids fort l'adresse adressaient la table des page maitre, appelée le directoire des pages (''page directory''), les 10 bits précédents servaient de numéro de page logique, et les 12 bits restants servaient à indiquer la position de l'octet dans la table des pages. Les entrées de chaque table des pages, mineure ou majeure, faisaient 32 bits, soit 4 octets. Vous remarquerez que la table des page majeure a la même taille que la table des page unique obtenue avec des pages larges (de 4 mébioctets).
[[File:X86 Paging 4K.svg|centre|vignette|upright=2|X86 Paging 4K]]
La technique du '''''physical adress extension''''' (PAE), utilisée depuis le Pentium Pro, permettait aux processeurs x86 32 bits d'adresser plus de 4 gibioctets de mémoire, en utilisant des adresses physiques de 64 bits. Les adresses virtuelles de 32 bits étaient traduites en adresses physiques de 64 bits grâce à une table des pages adaptée. Cette technologie permettait d'adresser plus de 4 gibioctets de mémoire au total, mais avec quelques limitations. Notamment, chaque programme ne pouvait utiliser que 4 gibioctets de mémoire RAM pour lui seul. Mais en lançant plusieurs programmes, on pouvait dépasser les 4 gibioctets au total. Pour cela, les entrées de la table des pages passaient à 64 bits au lieu de 32 auparavant.
La table des pages gardait 2 niveaux pour les pages larges en PAE.
[[File:X86 Paging PAE 2M.svg|centre|vignette|upright=2|X86 Paging PAE 2M]]
Par contre, pour les pages de 4 kibioctets en PAE, elle était modifiée de manière à ajouter un niveau de hiérarchie, passant de deux niveaux à trois.
[[File:X86 Paging PAE 4K.svg|centre|vignette|upright=2|X86 Paging PAE 4K]]
En 64 bits, la table des pages est une table des page hiérarchique avec 5 niveaux. Seuls les 48 bits de poids faible des adresses sont utilisés, les 16 restants étant ignorés.
[[File:X86 Paging 64bit.svg|centre|vignette|upright=2|X86 Paging 64bit]]
===La TLB et les caches associés===
En théorie, la table des pages est censée être accédée à chaque accès mémoire. Mais pour éviter d'avoir à lire la table des pages en mémoire RAM à chaque accès mémoire, les concepteurs de processeurs ont décidé d'implanter un cache dédié, le '''''translation lookaside buffer''''', ou TLB. Le TLB stocke au minimum de quoi faire la traduction entre adresse virtuelle et adresse physique, à savoir une correspondance entre numéro de page logique et numéro de page physique. Pour faire plus général, il stocke des entrées de la table des pages.
[[File:MMU principle updated.png|centre|vignette|upright=2.0|MMU avec une TLB.]]
Les accès à la table des pages sont gérés de deux façons : soit le processeur gère tout seul la situation, soit il délègue cette tâche au système d’exploitation. Sur les processeurs anciens, le système d'exploitation gère le parcours de la table des pages. Mais cette solution logicielle n'a pas de bonnes performances. D'autres processeurs gèrent eux-mêmes le défaut d'accès à la TLB et vont chercher d'eux-mêmes les informations nécessaires dans la table des pages. Ils disposent de circuits, les '''''page table walkers''''' (PTW), qui s'occupent eux-mêmes du défaut.
Les ''page table walkers'' contiennent des registres qui leur permettent de faire leur travail. Le plus important est celui qui mémorise la position de la table des pages en mémoire RAM, dont nous avons parlé plus haut. Les PTW ont besoin, pour faire leur travail, de mémoriser l'adresse physique de la table des pages, ou du moins l'adresse de la table des pages de niveau 1 pour des tables des pages hiérarchiques. Mais d'autres registres existent. Toutes les informations nécessaires pour gérer les défauts de TLB sont stockées dans des registres spécialisés appelés des '''tampons de PTW''' (PTW buffers).
===L'abstraction matérielle des processus===
[[File:Memoire virtuelle.svg|vignette|Mémoire virtuelle]]
Il est possible d'implémenter l'abstraction matérielle des processus avec la pagination. En clair, chaque programme lancé sur l'ordinateur dispose de son propre espace d'adressage, ce qui fait que la même adresse logique ne pointera pas sur la même adresse physique dans deux programmes différents. Pour cela, il y a plusieurs méthodes.
La première solution n'utilise qu'une seule table des pages. Pour cela, chaque entrée indique à quel processus elle est associée. Pour cela, chaque entrée contient un '''identifiant de processus''', un numéro qui précise pour quel processus, pour quel espace d'adressage, la correspondance est valide. La page des tables peut aussi contenir des entrées qui sont valides pour tous les processus en même temps. Pour cela, l'entrée contient un '''bit ''global''''', qui précise que cette correspondance est valide pour tous les processus, l'identifiant de processus est ignoré. Un défaut est que le partage d'une page entre plusieurs processus est impossible : soit on partage une page avec tous les processus, soit on l'alloue avec un seul processus.
L'intérêt de partager des pages entre tous les processus n'est pas évident, mais il le devient quand on se rappelle que le noyau de l'OS est mappé dans le haut de l'espace d'adressage. Et peu importe l'espace d'adressage, le noyau est toujours mappé de manière identique, les mêmes adresses logiques adressant la même adresse mémoire. En conséquence, les correspondances adresse physique-logique sont les mêmes pour le noyau, peu importe l'espace d'adressage. Le bit global accélère rapidement la traduction d'adresse pour l'accès au noyau.
Une solution alternative, plus simple, utilise une table des pages par processus lancé sur l'ordinateur, une table des pages unique par espace d'adressage. À chaque changement de processus, le registre qui mémorise la position de la table des pages est modifié pour pointer sur la bonne. C'est le système d'exploitation qui se charge de cette mise à jour.
Avec cette méthode, il est possible de partager une ou plusieurs pages entre plusieurs processus, en configurant les tables des pages convenablement. Les pages partagées sont mappées dans l'espace d'adressage de plusieurs processus, mais pas forcément au même endroit, pas forcément dans les mêmes adresses logiques. On peut placer la page partagée à l'adresse logique 0x0FFF pour un processus, à l'adresse logique 0xFF00 pour un autre processus, etc. Par contre, les entrées de la table des pages pour ces adresses pointent vers la même adresse physique.
[[File:Vm5.png|centre|vignette|upright=2|Tables des pages de plusieurs processus.]]
===La taille des pages===
La taille des pages varie suivant le processeur et le système d'exploitation et tourne souvent autour de 4 kibioctets. Les processeurs actuels gèrent plusieurs tailles différentes pour les pages : 4 kibioctets par défaut, 2 mébioctets, voire 1 à 4 gibioctets pour les pages les plus larges. Les pages de 4 kibioctets sont les pages par défaut, les autres tailles de page sont appelées des ''pages larges''. La taille optimale pour les pages dépend de nombreux paramètres et il n'y a pas de taille qui convienne à tout le monde. Certaines applications gagnent à utiliser des pages larges, d'autres vont au contraire perdre drastiquement en performance en les utilisant.
Le désavantage principal des pages larges est qu'elles favorisent la fragmentation mémoire. Si un programme veut réserver une portion de mémoire, pour une structure de donnée quelconque, il doit réserver une portion dont la taille est multiple de la taille d'une page. Par exemple, un programme ayant besoin de 110 kibioctets allouera 28 pages de 4 kibioctets, soit 120 kibioctets : 2 kibioctets seront perdus. Par contre, avec des pages larges de 2 mébioctets, on aura une perte de 2048 - 110 = 1938 kibioctets. En somme, des morceaux de mémoire seront perdus, car les pages sont trop grandes pour les données qu'on veut y mettre. Le résultat est que le programme qui utilise les pages larges utilisent plus de mémoire et ce d'autant plus qu'il utilise des données de petite taille. Un autre désavantage est qu'elles se marient mal avec certaines techniques d'optimisations de type ''copy-on-write''.
Mais l'avantage est que la traduction des adresses est plus performante. Une taille des pages plus élevée signifie moins de pages, donc des tables des pages plus petites. Et des pages des tables plus petites n'ont pas besoin de beaucoup de niveaux de hiérarchie, voire peuvent se limiter à des tables des pages simples, ce qui rend la traduction d'adresse plus simple et plus rapide. De plus, les programmes ont une certaine localité spatiale, qui font qu'ils accèdent souvent à des données proches. La traduction d'adresse peut alors profiter de systèmes de mise en cache dont nous parlerons dans le prochain chapitre, et ces systèmes de cache marchent nettement mieux avec des pages larges.
Il faut noter que la taille des pages est presque toujours une puissance de deux. Cela a de nombreux avantages, mais n'est pas une nécessité. Par exemple, le tout premier processeur avec de la pagination, le super-ordinateur Atlas, avait des pages de 3 kibioctets. L'avantage principal est que la traduction de l'adresse physique en adresse logique est trivial avec une puissance de deux. Cela garantit que l'on peut diviser l'adresse en un numéro de page et un ''offset'' : la traduction demande juste de remplacer les bits de poids forts par le numéro de page voulu. Sans cela, la traduction d'adresse implique des divisions et des multiplications, qui sont des opérations assez couteuses.
===Les entrées de la table des pages===
Avant de poursuivre, faisons un rapide rappel sur les entrées de la table des pages. Nous venons de voir que la table des pages contient de nombreuses informations : un bit ''valid'' pour la mémoire virtuelle, des bits ''dirty'' et ''accessed'' utilisés par l'OS, des bits de protection mémoire, un bit ''global'' et un potentiellement un identifiant de processus, etc. Étudions rapidement le format de la table des pages sur un processeur x86 32 bits.
* Elle contient d'abord le numéro de page physique.
* Les bits AVL sont inutilisés et peuvent être configurés à loisir par l'OS.
* Le bit G est le bit ''global''.
* Le bit PS vaut 0 pour une page de 4 kibioctets, mais est mis à 1 pour une page de 4 mébioctets dans le cas où le processus utilise des pages larges.
* Le bit D est le bit ''dirty''.
* Le bit A est le bit ''accessed''.
* Le bit PCD indique que la page ne peut pas être cachée, dans le sens où le processeur ne peut copier son contenu dans le cache et doit toujours lire ou écrire cette page directement dans la RAM.
* Le bit PWT indique que les écritures doivent mettre à jour le cache et la page en RAM (dans le chapitre sur le cache, on verra qu'il force le cache à se comporter comme un cache ''write-through'' pour cette page).
* Le bit U/S précise si la page est accessible en mode noyau ou utilisateur.
* Le bit R/W indique si la page est accessible en écriture, toutes les pages sont par défaut accessibles en lecture.
* Le bit P est le bit ''valid''.
[[File:PDE.png|centre|vignette|upright=2.5|Table des pages des processeurs Intel 32 bits.]]
==Comparaison des différentes techniques d'abstraction mémoire==
Pour résumer, l'abstraction mémoire permet de gérer : la relocation, la protection mémoire, l'isolation des processus, la mémoire virtuelle, l'extension de l'espace d'adressage, le partage de mémoire, etc. Elles sont souvent implémentées en même temps. Ce qui fait qu'elles sont souvent confondues, alors que ce sont des concepts sont différents. Ces liens sont résumés dans le tableau ci-dessous.
{|class="wikitable"
|-
!
! colspan="5" | Avec abstraction mémoire
! rowspan="2" | Sans abstraction mémoire
|-
!
! Relocation matérielle
! Segmentation en mode réel (x86)
! Segmentation, général
! Architectures à capacités
! Pagination
|-
! Abstraction matérielle des processus
| colspan="4" | Oui, relocation matérielle
| Oui, liée à la traduction d'adresse
| Emulée via relocation logicielle au démarrage du programme.
|-
! Mémoire virtuelle
| colspan="2" | Non, sauf émulation logicielle
| colspan="3" | Oui, gérée par le processeur et l'OS
| Non, sauf émulation logicielle
|-
! Extension de l'espace d'adressage
| colspan="2" | Oui : registre de base élargi
| colspan="2" | Oui : adresse de base élargie dans la table des segments
| ''Physical Adress Extension'' des processeurs 32 bits
| Commutation de banques
|-
! Protection mémoire
| Registre limite
| Aucune
| colspan="2" | Registre limite, droits d'accès aux segments
| Gestion des droits d'accès aux pages
| Possible, méthodes variées
|-
! Partage de mémoire
| colspan="2" | Non
| colspan="2" | Segment partagés
| Pages partagées
| Possible, méthodes variées
|}
===Les différents types de segmentation===
La segmentation regroupe plusieurs techniques franchement différentes, qui auraient gagné à être nommées différemment. La principale différence est l'usage de registres de relocation versus des registres de sélecteurs de segments. L'usage de registres de relocation est le fait de la relocation matérielle, mais aussi de la segmentation en mode réel des CPU x86. Par contre, l'usage de sélecteurs de segments est le fait des autres formes de segmentation, architectures à capacité inclues.
La différence entre les deux est le nombre de segments. L'usage de registres de relocation fait que le CPU ne gère qu'un petit nombre de segments de grande taille. La mémoire virtuelle est donc rarement implémentée vu que swapper des segments de grande taille est trop long, l'impact sur les performances est trop important. Sans compter que l'usage de registres de base se marie très mal avec la mémoire virtuelle. Vu qu'un segment peut être swappé ou déplacée n'importe quand, il faut invalider les registres de base au moment du swap/déplacement, ce qui n'est pas chose aisée. Aucun processeur ne gère cela, les méthodes pour n'existent tout simplement pas. L'usage de registres de base implique que la mémoire virtuelle est absente.
La protection mémoire est aussi plus limitée avec l'usage de registres de relocation. Elle se limite à des registres limite, mais la gestion des droits d'accès est limitée. En théorie, la segmentation en mode réel pourrait implémenter une version limitée de protection mémoire, avec une protection de l'espace exécutable. Mais ca n'a jamais été fait en pratique sur les processeurs x86.
Le partage de la mémoire est aussi difficile sur les architectures avec des registres de base. L'absence de table des segments fait que le partage d'un segment est basiquement impossible sans utiliser des méthodes complétement tordues, qui ne sont jamais implémentées en pratique.
===Segmentation versus pagination===
Par rapport à la pagination, la segmentation a des avantages et des inconvénients. Tous sont liés aux propriétés des segments et pages : les segments sont de grande taille et de taille variable, les pages sont petites et de taille fixe.
L'avantage principal de la segmentation est sa rapidité. Le fait que les segments sont de grande taille fait qu'on a pas besoin d'équivalent aux tables des pages inversée ou multiple, juste d'une table des segments toute simple. De plus, les échanges entre table des pages/segments et registres sont plus rares avec la segmentation. Par exemple, si un programme utilise un segment de 2 gigas, tous les accès dans le segment se feront avec une seule consultation de la table des segments. Alors qu'avec la pagination, il faudra une consultation de la table des pages chaque bloc de 4 kibioctet, au minimum.
Mais les désavantages sont nombreux. Le système d'exploitation doit agencer les segments en RAM, et c'est une tâche complexe. Le fait que les segments puisse changer de taille rend le tout encore plus complexe. Par exemple, si on colle les segments les uns à la suite des autres, changer la taille d'un segment demande de réorganiser tous les segments en RAM, ce qui demande énormément de copies RAM-RAM. Une autre possibilité est de laisser assez d'espace entre les segments, mais cet espace est alors gâché, dans le sens où on ne peut pas y placer un nouveau segment.
Swapper un segment est aussi très long, vu que les segments sont de grande taille, alors que swapper une page est très rapide.
==Un cas particulier : la z/architecture d'IBM==
IBM a sa propre gamme de ''mainframes'', qui utilisent des processeurs fait maison par IBM lui-même. Les CPU IBM ont leur propre jeu d'instruction CISC, appelé '''''z/architecture'''''. Et ce jeu d'instruction a des possibilités en termes d'adressage qui sont extrêmement intéressantes, en plus d'être assez inédites.
La ''z/architecture'' gère des pages de 4 kibioctets, en plus de pages larges introduites dans les dernières versions. La traduction d’adresse se fait cependant en passant des regroupements de 4 pages virtuelles, appelés des ''segments'', qui n'ont rien à voir avec les segments au sens de la segmentation. Pour éviter les confusions, je vais les appeler des ''super-pages''. Les super-pages sont des groupes de 4 pages virtuelles, ce qui veut dire que les 4 pages peuvent être très éloignées en mémoire physique. Les super-pages sont concrètement des intermédiaires utilisés pour la traduction d'adresse. La traduction d'adresse démarre à une table des super-pages, qui point vers une table des pages à plusieurs niveaux (5 maximum).
Mais ce n'est pas ce qui va nous intéresser dans ce qui suit. À la place, nous allons nous concentrer sur une fonctionnalité : les programmes ont accès à plusieurs espaces d'adressage simultanément. La ''z/architecture'' supporte 65 536 espaces d'adressage simultanés. Par contre, un programme ne peut en utiliser que 2048 en même temps. De plus, ils ne sont accessibles que quand le processeur est dans un mode de fonctionnement bien précis. Un processeur ''z/architecture'' peut en effet fonctionner dans deux modes, qu'on va appeler "standard" et "étendu".
L'existence de ces deux modes s'explique par le fait que les ''mainframes'' IBM ont évolués dans le temps. Les premiers ''mainframes'' n'avaient même pas de support basique de la mémoire virtuelle, le System/360 s'en passait totalement. C'est sur le System/370 que la pagination a été ajoutée. Le System/370 supportait deux espaces d'adressage virtuel par programme, là où les implémentations classiques n'en utilisent qu'un par programme. L'ESA/370 a ajouté la possibilité d'utiliser 16 espaces d'adressage par programme, mais l'a fait d'une manière à garder la compatibilité avec le System/370. Le processeur a donc deux modes : un avec deux espaces d'adressage par programme, un autre pouvant monter jusqu’à 16.
===Le mode standard de la ''z/architecture''===
En mode standard, un programme exécuté sur la ''z/architecture'' utilise deux espaces d'adressages : l''''espace d'adressage primaire''' où se situent le programme exécuté et les données importantes, un '''espace d'adressage secondaire''' utilisé pour des tâches/données annexes.
: Un troisième espace d'adressage l''''espace d'adressage ''home''''', est réservé au système d'exploitation. Il a pour particularité de ne pas avoir d'ASN. Il n'est concrètement accessible qu'en mode superviseur, donc par le système d'exploitation. C'est en quelque sorte un espace d'adressage pour le noyau, séparé des espaces d'adressage utilisateur. Nous n'en parlerons pas dans ce qui suit.
Mais comment le processeur pour utiliser ces espaces d'adressages ? Par exemple, s'il veut faire une lecture, comment précise-t-il l'espace d'adressage courant ? La réponse est que le processeur peut être dans deux états : soit primaire, soit secondaire. Dans le premier état, toutes les lectures/écritures se font dans l'espace d'adressage primaire. Dans le second, elles se font toutes dans l'espace d'adressage secondaire. Il peut passer de l'un à l'autre en utilisant l'instruction ''Set Address Space Control''. Le processeur dispose aussi d'instructions ''MOVE TO PRIMARY'' et ''MOVE TO SECONDARY'' pour échanger des données dans les deux espaces d'adressage.
L'espace d'adressage primaire a une table des pages dédiée, séparée de celle de l'espace d'adressage secondaire. Ou plutôt devrais-je dire une table des super-pages. La table des pages est une table des pages emboitée. Il y a peu à détailler dessus, si ce n'est pour cette histoire de segments/super-pages. L'adresse de la table des super-page est mémorisée dans le registre de contrôle CR1 pour l'espace d'adressage primaire, CR7 pour l'espace d'adressage secondaire. L'un ou l'autre est utilisé, selon que le processeur est en mode "primaire" ou "secondaire".
===Les registres d'accès de la ''z/architecture''===
Un programme peut donc passer de l'espace d'adressage primaire à secondaire (ou inversement) en utilisant l’instruction ''SET ADDRESS SPACE CONTROL''. Mais il peut en profiter pour passer dans un mode où il a accès à 2048 espaces d'adressage différents, en même temps. Le processeur n'a pas besoin d'utiliser d'instructions particulières pour copier des données d'un espace d'adressage à un autre, un simple MOV suffit. Le mode en question est appelé le '''mode ''access register'''''.
Le mode ''access register'' donne accès aux '''registres d'accès''', qui lui donnent son nom. Les registres d'accès mémorisent des identifiants qui permettent de sélectionner un espace d'adressage parmi 2048. Il y a en tout 16 registres d'accès, chacun étant appairé avec un registre général. Par exemple, le registre général numéro 5 est appairé au registre d'accès de même numéro. En mode ''access register'', toute lecture/écriture lit l'adresse dans un registre général et le registre d'accès associé indique dans quel espace d'adressage se trouve cette adresse. Par exemple, une lecture utilise l'adresse dans le registre général numéro 7, l'espace d'adressage associé sera dans le registre d'accès numéro 7.
Les registres d'accès peuvent être modifiés par des instructions classiques, en espace utilisateur. Elles sont nommées COPY ACCESS, EXTRACT ACCESS, LOAD ACCESS MULTIPLE, LOAD ADDRESS EXTENDED, SET ACCESS et STORE ACCESS MULTIPLE. Elles permettent de copier un registre d'accès dans un autre, de charger le contenu d'un registre d'accès depuis la mémoire RAM, d'enregistrer les registres d'accès dans la mémoire RAM, de copier une constante dans un registre d'accès. Mais quel est le contenu de ces registres ?
Une implémentation simple numéroterait les espaces d'adressage et mettrait les numéros dans les registres d'accès. Un espace d'adressage serait associé à une table des super-pages, reste à mémoriser la correspondance ''numéro d'espace d'adressage - adresse de la table des super-pages''. Pour cela, l'idéal est de mémoriser ces correspondances dans un tableau en mémoire RAM. Le numéro d'espace d'adressage est alors un indice, qui pointe sur l'entrée adéquate dans cette table. Il devrait y avoir un tableau par programme, qui serait protégé en écriture et seulement accessible par le système d'exploitation, comme le serait une table des pages. Mais ce n'est pas exactement ce qui est fait.
===L'utilisation des registres d'accès===
La différence est qu'il n'y a pas une table de correspondance par programme, mais un système à deux tableaux imbriqués. Le premier tableau possède 65 536 entrées, une par espace d'adressage supporté par le processeur. C'est donc une table partagée par tous les programmes. Par contre, la seconde table est bien unique pour chaque programme, chaque programme a la sienne. Faire ainsi permet de simplifier le partage de la mémoire. La table partagée est l''''''Access Space Table Entry''''' (ASTE), alors que la seconde table est la '''liste privée''' (''access list''), sous-entendu "privée pour un programme".
La liste privée mémorise une liste des espaces d'adressage auquel le programme peut accéder, auxquels le système d'exploitation lui a accordé l'accès. Pour chaque espace d'adressage, elle renvoie vers l'entrée de l'ASTE associée. En plus de mémoriser l'adresse de cette entrée, elle mémorise les droits d'accès que le programme a sur l'espace d'adressage associé. Par exemple, si deux programmes partagent un espace d'adressage, l'un pourra y avoir accès en lecture seule, l'autre pourra avoir un accès total. Pour cela, ils ont des droits différents dans la liste privée, mais l'entrée de l'ASTE sera la même.
[[File:Tradiction d'adresse sur la zarchitecture d'IBM.png|centre|vignette|upright=2|Traduction d'adresse sur la zarchitecture d'IBM.]]
Il y a donc une vérification des droits d'accès, suivi d'un accès à une table des super-pages globale. Si un programme veut accéder à un espace d'adressage qui lui est interdit, l'accès mémoire stoppera dans la liste privée. Le processeur utilisera l'identifiant d'espace d'adressage, mais la liste privée n'aura pas d'entrée associée. Elle lèvera alors une exception matérielle et le système d'exploitation gérera la situation, vraisemblablement en tuant le programme fautif. Mais si l'accès est autorisé, alors le programme accède à la table des super-pages et l'accès mémoire se poursuit.
Je dis une liste d'accès privée, mais il y en a deux en réalité. Mais la distinction exacte demanderait de parler de ''threads'' et de processus. Pour ceux qui sont familiers avec ces concepts, chaque ''thread'' a sa propre liste privée, avec en plus une liste par processus, partagée entre tous les ''threads''. Deux listes de 1024 entrées maximum, cela fait 2048 entrées au total. Les deux listes portent les noms de ''Dispatchable Unit Access List'' et de ''PASN-AL (Primary Address Space Access List''. Inutile de retenir ces deux termes, cependant.
Un registre d'accès mémorise un '''''Access List Entry Token''''' ou ALET. L'ALET contient l'indice d'une entrée dans la liste privée. Elle contient aussi un bit qui indique quelle liste privée utiliser, entre la ''Dispatchable Unit Access List'' et la ''PASN-AL (Primary Address Space Access List''. Précisons cependant que deux valeurs font exceptions : 0 pointe vers l'espace d'adressage principal, 1 vers l'espace d'adressage secondaire. Les autres valeurs sont configurables.
{|class="wikitable"
|+ Registre d'accès
|-
! 0000000 !! L (choix de la liste privée !! ASLEN !! ALEN
|-
| 0000000 || Choix de la liste privée || Identifiant de protection || Indice dans la liste privée
|}
Lors d'une lecture/écriture, l'ALETs est utilisé pour sélectionner l'espace d'adressage. Les valeurs 0 et 1 sont traitées telles quelles, l'espace d'adressage est déterminé automatiquement par le processeur. Pour les autres valeurs, récupérer l'adresse mémoire demande de consulter la liste d'accès privée. L'ALET est envoyé à la liste d'accès privée, et cette dernière fournit l'adresse de l'entrée de l'ASTE à consulter. Le processeur connait l'adresse de la liste privée, car son adresse mémoire est mémorisée dans les registres de contrôle numéro 2 et 5.
: Précisons que le registre d'accès numéro 0 contient obligatoirement la valeur 0, ce qui fait qu'il pointe vers l'espace d'adressage primaire.
[[File:Traduction d'adresse - trois espaces d'adressage, zarchitecture d'IBM.png|centre|vignette|upright=2|Traduction d'adresse - trois espaces d'adressage, zarchitecture d'IBM.]]
Une subtilité est que le système d'exploitation peut à tout moment modifier le contenu de la liste privée. Il se peut qu'un ALET qui correspondait à un espace d'adressage se mette à pointer vers un autre. Pour détecter cette situation, le processeur attribue un identifiant, à chaque espace d'adressage. L'identifiant est mémorisé dans les entrées de la liste privée, de l'ASTE et dans les registres d'accès (dans les bits de poids fort de l'ALET). L'identifiant est modifié dans l'ALET à chaque fois que le système d'exploitation modifie l'entrée. Si un programme fait un accès avec un registre d'accès, mais que l'identifiant ne correspond pas avec celui dans l'ALET/liste privée, c'est signe que l'espace d'adressage a été swappé, une exception matérielle est levée.
Un tel fonctionnement vous aura sans doute fait penser à une architecture à capacité, mais la z/architecture n'en est pas une. Malgré des points de ressemblance assez importants, il y a des différences fondamentales avec les architectures à capacité.
* Le z/architecture ne supporte pas la segmentation, qui est nécessaire pour les architectures à capacité. Sur la z/architecture, ce sont des espaces d'adressage qui sont sélectionnés, pas des segments.
* Les registres d'accès ne sont pas des registres de capacité, car on peut les modifier avec des instructions en espace utilisateur. En comparaison, modifier les registres de capacité ne peut se faire qu'en espace noyau, sous l'égide du système d'exploitation.
* Les droits d'accès sont dans une table séparée des pointeurs, pas concaténés à ceux-ci. La gestion des droits d'accès est gérée par les tables des pages.
* Les identifiants ALET ne peuvent pas être passés à un autre programme. Deux ALETs identiques correspondent à deux espaces d'adressages différents dans deux programmes différents. L'identifiant peut être identique, il pointera vers l'entrée de même numéro dans les deux listes privées, mais l'adresse dans l'entrée peut être différente.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=L'espace d'adressage du processeur
| prevText=L'espace d'adressage du processeur
| next=Les méthodes de synchronisation entre processeur et périphériques
| nextText=Les méthodes de synchronisation entre processeur et périphériques
}}
</noinclude>
kvzh1f5nncz3b9sah8hy2sn3xdb8vjs
775336
775335
2026-10-16T21:11:22Z
Mewtow
31375
/* Un cas particulier : la z/architecture d'IBM */
775336
wikitext
text/x-wiki
Pour introduire ce chapitre, nous devons faire un rappel sur le concept d{{'}}'''espace d'adressage'''. Pour rappel, un espace d'adressage correspond à l'ensemble des adresses utilisables par le processeur. Par exemple, si je prends un processeur 16 bits, il peut adresser en tout 2^16 = 65536 adresses, l'ensemble de ces adresses forme son espace d'adressage. Intuitivement, on s'attend à ce qu'il y ait correspondance avec les adresses de la mémoire RAM. J'entends par là que l'adresse 1209 de l'espace d'adressage correspond à l'adresse 1209 en mémoire RAM. C'est là une hypothèse parfaitement raisonnable et on voit mal comment ce pourrait ne pas être le cas.
Mais les processeurs modernes utilisent des techniques d{{'}}'''abstraction mémoire''' qui font que ce n'est pas le cas. Avec ces techniques, l'adresse 1209 de l'espace d'adressage correspond en réalité à l'adresse 9999 en mémoire RAM, voire n'est pas en RAM. L'abstraction mémoire fait que l'espace d'adressage regroupe des adresses fictives, qui doivent être traduites en adresses mémoires réelles pour être utilisées. Les adresses de l'espace d'adressage portent le nom d{{'}}'''adresses logiques''', alors que les adresses de la mémoire RAM sont appelées '''adresses physiques'''.
L'intérêt de l'abstraction matérielle n'est pas évident. Aussi, avant de parler de comment l'abstraction mémoire fonctionne, nous allons voir à quoi elle sert. Nous allons voir qu'elle a plusieurs utilisations différentes, qui sont absolument nécessaires sur tous les ordinateurs personnels modernes. Tous les processeurs modernes la prennent en charge, les systèmes d'exploitation coopérent avec le processeur pour l'utiliser au mieux.
==L'abstraction mémoire permet plusieurs fonctionnalités complémentaires==
La fonctionnalité la plus connue est la mémoire virtuelle, dont vous avez peut-être déjà entendu parler, peut-être que le nom vous dit quelque chose. Si ce n'est pas le cas, nous la détailleront dans la suite. Elle permet concrétement à un programme d'utiliser plus de mémoire qu'il n'y a de RAM installé dans un ordinateur, en utilisant le disque dur comme solution de secours. Mais d'autres fonctionnalités moins évidentes sont permises par l'abstraction mémoire. Et la première que nous allons voir est l'abstraction des processus.
: En général, une adresse logique correspond à une seule adresse physique. Mais beaucoup de fonctionnalités avancées ne respectent pas cette règle.
===L'abstraction matérielle des processus===
Les systèmes d'exploitation modernes sont dits multi-tâche, à savoir qu'ils sont capables d'exécuter plusieurs logiciels en même temps. Et ce même si un seul processeur est présent dans l'ordinateur : les logiciels sont alors exécutés à tour de rôle. Toutefois, cela amène un paquet de problèmes qu'il faut résoudre au mieux. Par exemple, les programmes exécutés doivent se partager la mémoire RAM, ce qui ne vient pas sans problèmes. Le problème principal est que les programmes ne doivent pas lire ou écrire dans les données d'un autre, sans quoi on se retrouverait rapidement avec des problèmes. Il faut donc introduire des mécanismes d{{'}}'''isolement des processus''', pour isoler les programmes les uns des autres.
Un de ces mécanismes est l{{'}}'''abstraction matérielle des processus''', une technique qui fait que chaque programme a son propre espace d'adressage. Chaque programme a l'impression d'avoir accès à tout l'espace d'adressage, de l'adresse 0 à l'adresse maximale gérée par le processeur. Évidemment, il s'agit d'une illusion maintenue justement grâce à la traduction d'adresse. Les espaces d'adressage contiennent des adresses logiques, les adresses de la RAM sont des adresses physiques, la nécessité de l'abstraction mémoire est évidente.
Implémenter l'abstraction mémoire peut se faire de plusieurs manières. Mais dans tous les cas, il faut que la correspondance adresse logique - physique change d'un programme à l'autre. Ce qui est normal, vu que les deux processus sont placés à des endroits différents en RAM physique. La conséquence est qu'avec l'abstraction mémoire, une adresse logique correspond à plusieurs adresses physiques. Une même adresse logique dans deux processus différents correspond à deux adresses phsiques différentes, une par processus. Une adresse logique dans un processus correspondra à l'adresse physique X, la même adresse dans un autre processus correspondra à l'adresse Y.
Les adresses physiques qui partagent la même adresse logique sont alors appelées des '''adresses homonymes'''. Le choix de la bonne adresse étant réalisé par un mécanisme matériel et dépend du programme en cours. Le mécanisme pour choisir la bonne adresse dépend du processeur, mais il y en a deux grands types :
* La première consiste à utiliser l'identifiant de processus CPU, vu au chapitre précédent. C'est, pour rappel, un numéro attribué à chaque processus par le processeur. L'identifiant du processus en cours d'exécution est mémorisé dans un registre du processeur. La traduction d'adresse utilise cet identifiant, en plus de l'adresse logique, pour déterminer l'adresse physique.
* La seconde solution mémorise les correspondances adresses logiques-physique dans des tables en mémoire RAM, qui sont différentes pour chaque programme. Les tables sont accédées à chaque accès mémoire, afin de déterminer l'adresse physique.
===Le partage de la mémoire===
L'isolation des processus est très importante sur les systèmes d'exploitation modernes. Cependant, il existe quelques situations où elle doit être contournée ou du moins mise en pause. Les situations sont multiples : gestion de bibliothèques partagées, communication entre processus, usage de ''threads'', etc. Elles impliquent toutes un '''partage de mémoire''', à savoir qu'une portion de mémoire RAM est partagée entre plusieurs programmes. Le partage de mémoire est une sorte de brèche de l'isolation des processus, mais qui est autorisée car elle est utile.
Un cas intéressant est celui des '''bibliothèques partagées'''. Les bibliothèques sont des collections de fonctions regroupées ensemble, dans une seule unité de code. Un programme qui utilise une bibliothèque peut appeler n’importe quelle fonction présente dans la bibliothèque. La bibliothèque peut être simplement inclue dans le programme lui-même, on parle alors de bibliothèques statiques. De telles bibliothèques fonctionnent très bien, mais avec un petit défaut pour les bibliothèques très utilisées : plusieurs programmes qui utilisent la même bibliothèque vont chacun l'inclure dans leur code, ce qui fera doublon.
Pour éviter cela, les OS modernes gèrent des bibliothèques partagées, à savoir qu'un seul exemplaire de la bibliothèque est partagé entre plusieurs programmes. Chaque programme peut exécuter une fonction de la bibliothèque quand il le souhaite, en effectuant un branchement adéquat. Mais cela implique que la bibliothèque soit présente dans l'espace d'adressage du programme en question. Une bibliothèque est donc présente dans plusieurs espaces d'adressage, alors qu'il n'y en a qu'un seul exemplaire en mémoire RAM.
[[File:Ogg vorbis libs and application dia.svg|centre|vignette|upright=2|Exemple de bibliothèques, avec Ogg vorbis.]]
D'autres situations demandent de partager de la mémoire entre deux programmes. Par exemple, les systèmes d'exploitation modernes gèrent nativement des systèmes de '''communication inter-processus''', très utilisés par les programmes modernes pour échanger des données. Et la plupart demandant de partager un bout de mémoire entre processus, même si c'est seulement temporairement. Typiquement, deux processus partagent un intervalle d'adresse où l'un écrit les données à l'autre, l'autre lisant les données envoyées.
Une dernière utilisation de la mémoire partagée est l{{'}}'''accès direct au noyau'''. Sur les systèmes d'exploitations moderne, dans l'espace d'adressage de chaque programme, les adresses hautes sont remplies avec une partie du noyau ! Évidemment, ces adresses sont accessibles uniquement en lecture, pas en écriture. Pas question de modifier le noyau de l'OS ! De plus, il s'agit d'une portion du noyau dont on sait que la consultation ne pose pas de problèmes de sécurité.
Le programme peut lire des données dans cette portion du noyau, mais aussi exécuter les fonctions du noyau qui sont dedans. L'idée est d'éviter des appels systèmes trop fréquents. Au lieu d'effectuer un véritable appel système, avec une interruption logicielle, le programme peut exécuter des appels systèmes simplifiés, de simples appels de fonctions couplés avec un changement de niveau de privilège (passage en espace noyau nécessaire).
[[File:AMD64-canonical--48-bit.png|vignette|Répartition des adresses entre noyau (jaune/orange) et programme (verte), sur les systèmes x86-64 bits, avec des adresses physiques de 48 bits.]]
L'espace d'adressage est donc séparé en deux portions : l'OS d'un côté, le programme de l'autre. La répartition des adresses entre noyau et programme varie suivant l'OS ou le processeur utilisé. Sur les PC x86 32 bits, Linux attribuait 3 gigas pour les programmes et 1 giga pour le noyau, Windows attribuait 2 gigas à chacun. Sur les systèmes x86 64 bits, l'espace d'adressage d'un programme est coupé en trois, comme illustré ci-contre : une partie basse de 2^48 octets, une partie haute de même taille, et un bloc d'adresses invalides entre les deux. Les adresses basses sont utilisées pour le programme, les adresses hautes pour le noyau, il n'y a rien entre les deux.
Avec le partage de mémoire, plusieurs adresses logiques correspondent à la même adresse physique. Tel processus verra la zone de mémoire partagée à l'adresse X, l'autre la verra à l'adresse Y. Mais il s'agira de la même portion de mémoire physique, avec une seule adresse physique. En clair, lorsque deux processus partagent une même zone de mémoire, la zone sera mappées à des adresses logiques différentes. Les adresses logiques sont alors appelées des '''adresses synonymes''', terme qui trahit le fait qu'elles correspondent à la même adresse physique.
===La mémoire virtuelle===
Toutes les adresses ne sont pas forcément occupées par de la mémoire RAM, s'il n'y a pas assez de RAM installée. Par exemple, un processeur 32 bits peut adresser 4 gibioctets de RAM, même si seulement 3 gibioctets sont installés dans l'ordinateur. L'espace d'adressage contient donc 1 gigas d'adresses inutilisées, et il faut éviter ce surplus d'adresses pose problème.
Sans mémoire virtuelle, seule la mémoire réellement installée est utilisable. Si un programme utilise trop de mémoire, il est censé se rendre compte qu'il n'a pas accès à tout l'espace d'adressage. Quand il demandera au système d'exploitation de lui réserver de la mémoire, le système d'exploitation le préviendra qu'il n'y a plus de mémoire libre. Par exemple, si un programme tente d'utiliser 4 gibioctets sur un ordinateur avec 3 gibioctets de mémoire, il ne pourra pas. Pareil s'il veut utiliser 2 gibioctets de mémoire sur un ordinateur avec 4 gibioctets, mais dont 3 gibioctets sont déjà utilisés par d'autres programmes. Dans les deux cas, l'illusion tombe à plat.
Les techniques de '''mémoire virtuelle''' font que l'espace d'adressage est utilisable au complet, même s'il n'y a pas assez de mémoire installée dans l'ordinateur ou que d'autres programmes utilisent de la RAM. Par exemple, sur un processeur 32 bits, le programme aura accès à 4 gibioctets de RAM, même si d'autres programmes utilisent la RAM, même s'il n'y a que 2 gibioctets de RAM d'installés dans l'ordinateur.
Pour cela, on utilise une partie des mémoires de masse (disques durs) d'un ordinateur en remplacement de la mémoire physique manquante. Le système d'exploitation crée sur le disque dur un fichier, appelé le ''swapfile'' ou '''fichier de ''swap''''', qui est utilisé comme mémoire RAM supplémentaire. Il mémorise le surplus de données et de programmes qui ne peut pas être mis en mémoire RAM.
[[File:Vm1.png|centre|vignette|upright=2.0|Mémoire virtuelle et fichier de Swap.]]
Une technique naïve de mémoire virtuelle serait la suivante. Avant de l'aborder, précisons qu'il s'agit d'une technique abordée à but pédagogique, mais qui n'est implémentée nulle part tellement elle est lente et inefficace. Un espace d'adressage de 4 gigas ne contient que 3 gigas de RAM, ce qui fait 1 giga d'adresses inutilisées. Les accès mémoire aux 3 gigas de RAM se font normalement, mais l'accès aux adresses inutilisées lève une exception matérielle "Memory Unavailable". La routine d'interruption de cette exception accède alors au ''swapfile'' et récupère les données associées à cette adresse. La mémoire virtuelle est alors émulée par le système d'exploitation.
Le défaut de cette méthode est que l'accès au giga manquant est toujours très lent, parce qu'il se fait depuis le disque dur. D'autres techniques de mémoire virtuelle logicielle font beaucoup mieux, mais nous allons les passer sous silence, vu qu'on peut faire mieux, avec l'aide du matériel.
L'idée est de charger les données dont le programme a besoin dans la RAM, et de déplacer les autres sur le disque dur. Par exemple, imaginons la situation suivante : un programme a besoin de 4 gigas de mémoire, mais ne dispose que de 2 gigas de mémoire installée. On peut imaginer découper l'espace d'adressage en 2 blocs de 2 gigas, qui sont chargés à la demande. Si le programme accède aux adresses basses, on charge les 2 gigas d'adresse basse en RAM. S'il accède aux adresses hautes, on charge les 2 gigas d'adresse haute dans la RAM après avoir copié les adresses basses sur le ''swapfile''.
On perd du temps dans les copies de données entre RAM et ''swapfile'', mais on gagne en performance vu que tous les accès mémoire se font en RAM. Du fait de la localité temporelle, le programme utilise les données chargées depuis le swapfile durant un bon moment avant de passer au bloc suivant. La RAM est alors utilisée comme une sorte de cache alors que les données sont placées dans une mémoire fictive représentée par l'espace d'adressage et qui correspond au disque dur.
Mais avec cette technique, la correspondance entre adresses du programme et adresses de la RAM change au cours du temps. Les adresses de la RAM correspondent d'abord aux adresses basses, puis aux adresses hautes, et ainsi de suite. On a donc besoin d'abstraction mémoire. Les correspondances entre adresse logique et physique peuvent varier avec le temps, ce qui permet de déplacer des données de la RAM vers le disque dur ou inversement. Une adresse logique peut correspondre à une adresse physique, ou bien à une donnée swappée sur le disque dur. C'est l'unité de traduction d'adresse qui se charge de faire la différence. Si une correspondance entre adresse logique et physique est trouvée, elle l'utilise pour traduire les adresses. Si aucune correspondance n'est trouvée, alors elle laisse la main au système d'exploitation pour charger la donnée en RAM. Une fois la donnée chargée en RAM, les correspondances entre adresse logique et physiques sont modifiées de manière à ce que l'adresse logique pointe vers la donnée chargée.
===L'extension d'adressage===
Une autre fonctionnalité rendue possible par l'abstraction mémoire est l{{'}}'''extension d'adressage'''. Elle permet d'utiliser plus de mémoire que l'espace d'adressage ne le permet. Par exemple, utiliser 7 gigas de RAM sur un processeur 32 bits, dont l'espace d'adressage ne gère que 4 gigas. L'extension d'adresse est l'exact inverse de la mémoire virtuelle. La mémoire virtuelle sert quand on a moins de mémoire que d'adresses, l'extension d'adresse sert quand on a plus de mémoire que d'adresses.
Il y a quelques chapitres, nous avions vu que c'est possible via la commutation de banques. Mais l'abstraction mémoire est une méthode alternative. Que ce soit avec la commutation de banques ou avec l'abstraction mémoire, les adresses envoyées à la mémoire doivent être plus longues que les adresses gérées par le processeur. La différence est que l'abstraction mémoire étend les adresses d'une manière différente.
Une implémentation possible de l'extension d'adressage fait usage de l'abstraction matérielle des processus. Chaque processus a son propre espace d'adressage, mais ceux-ci sont placés à des endroits différents dans la mémoire physique. Par exemple, sur un ordinateur avec 16 gigas de RAM, mais un espace d'adressage de 2 gigas, on peut remplir la RAM en lançant 8 processus différents et chaque processus aura accès à un bloc de 2 gigas de RAM, pas plus, il ne peut pas dépasser cette limite. Ainsi, chaque processus est limité par son espace d'adressage, mais on remplit la mémoire avec plusieurs processus, ce qui compense. Il s'agit là de l'implémentation la plus simple, qui a en plus l'avantage d'avoir la meilleure compatibilité logicielle. De simples changements dans le système d'exploitation suffisent à l'implémenter.
[[File:Extension de l'espace d'adressage.png|centre|vignette|upright=1.5|Extension de l'espace d'adressage]]
Un autre implémentation donne plusieurs espaces d'adressage différents à chaque processus, et a donc accès à autant de mémoire que permis par la somme de ces espaces d'adressage. Par exemple, sur un ordinateur avec 16 gigas de RAM et un espace d'adressage de 4 gigas, un programme peut utiliser toute la RAM en utilisant 4 espaces d'adressage distincts. On passe d'un espace d'adressage à l'autre en changeant la correspondance adresse logique-physique. L'inconvénient est que la compatibilité logicielle est assez mauvaise. Modifier l'OS ne suffit pas, les programmeurs doivent impérativement concevoir leurs programmes pour qu'ils utilisent explicitement plusieurs espaces d'adressage.
Les deux implémentations font usage des adresses logiques homonymes, mais à l'intérieur d'un même processus. Pour rappel, cela veut dire qu'une adresse logique correspond à des adresses physiques différentes. Rien d'étonnant vu qu'on utilise plusieurs espaces d'adressage, comme pour l'abstraction des processus, sauf que cette fois-ci, on a plusieurs espaces d'adressage par processus. Prenons l'exemple où on a 8 gigas de RAM sur un processeur 32 bits, dont l'espace d'adressage ne gère que 4 gigas. L'idée est qu'une adresse correspondra à une adresse dans les premiers 4 gigas, ou dans les seconds 4 gigas. L'adresse logique X correspondra d'abord à une adresse physique dans les premiers 4 gigas, puis à une adresse physique dans les seconds 4 gigas.
===La protection mémoire===
La '''protection mémoire''' regroupe des techniques très différentes les unes des autres, qui visent à améliorer la sécurité des programmes et des systèmes d'exploitation. Elles visent à empêcher de lire, d'écrire ou d'exécuter certaines portions de mémoire. Sans elle, les programmes peuvent techniquement lire ou écrire les données des autres, ce qui causent des situations non-prévues par le programmeur, avec des conséquences qui vont d'un joli plantage à des failles de sécurité dangereuses.
La première technique de protection mémoire est l{{'}}'''isolation des processus''', qu'on a vue plus haut. Elle garantit que chaque programme n'a accès qu'à certaines portions dédiées de la mémoire et rend le reste de la mémoire inaccessible en lecture et en écriture. Le système d'exploitation attribue à chaque programme une ou plusieurs portions de mémoire rien que pour lui, auquel aucun autre programme ne peut accéder. Un tel programme, isolé des autres, s'appelle un '''processus''', d'où le nom de cet objectif. Toute tentative d'accès à une partie de la mémoire non autorisée déclenche une exception matérielle (rappelez-vous le chapitre sur les interruptions) qui est traitée par une routine du système d'exploitation. Généralement, le programme fautif est sauvagement arrêté et un message d'erreur est affiché à l'écran.
La '''protection de l'espace exécutable''' empêche d’exécuter quoique ce soit provenant de certaines zones de la mémoire. En effet, certaines portions de la mémoire sont censées contenir uniquement des données, sans aucun programme ou code exécutable. Cependant, des virus informatiques peuvent se cacher dedans et d’exécuter depuis celles-ci. Ou encore, des failles de sécurités peuvent permettre à un attaquant d'injecter du code exécutable malicieux dans des données, ce qui peut lui permettre de lire les données manipulées par un programme, prendre le contrôle de la machine, injecter des virus, ou autre. Pour éviter cela, le système d'exploitation peut marquer certaines zones mémoire comme n'étant pas exécutable. Toute tentative d’exécuter du code localisé dans ces zones entraîne la levée d'une exception ou d'une erreur et le système d'exploitation réagit en conséquence. Là encore, le processeur doit détecter les exécutions non autorisées.
D'autres méthodes de protection mémoire visent à limiter des actions dangereuses. Pour cela, le processeur et l'OS gèrent des '''droits d'accès''', qui interdisent certaines actions pour des programmes non-autorisés. Lorsqu'on exécute une opération interdite, le système d’exploitation et/ou le processeur réagissent en conséquence. La première technique de ce genre n'est autre que la séparation entre espace noyau et utilisateur, vue dans le chapitre sur les interruptions. Mais il y en a d'autres, comme nous le verrons dans ce chapitre.
==La MMU==
La traduction des adresses logiques en adresses physiques se fait par un circuit spécialisé, appelé la '''''Memory Management Unit''''' (MMU). De nos jours, elle est intégrée dans le processeur, à la suite de l'unité mémoire. Mais il a existé des processeurs avec une MMU externe, soudée sur la carte mère.
La traduction des adresses logiques en adresses physiques demande d'accéder à des données sont en mémoire RAM, qui sont gérés par le système d'exploitation. Aussi, les processeurs modernes incorporent des mémoires caches appelées des '''''Translation Lookaside Buffers''''', ou encore TLB. Nous nous pouvons pas parler des TLB pour le moment, car nous n'avons pas encore abordé le chapitre sur les mémoires caches, mais un chapitre entier sera dédié aux TLB d'ici peu.
[[File:MMU principle updated.png|centre|vignette|upright=2|MMU.]]
===Les MMU intégrées au processeur===
D'ordinaire, la MMU est intégrée au processeur. Et elle peut l'être de deux manières. La première en fait un circuit séparé, relié au bus d'adresse. La seconde fusionne la MMU avec l'unité de calcul d'adresse. La première solution est surtout utilisée avec une technique d'abstraction mémoire appelée la pagination, alors que l'autre l'est avec une autre méthode appelée la segmentation. La raison est que la traduction d'adresse avec la segmentation est assez simple : elle demande d'additionner le contenu d'un registre avec l'adresse logique, ce qui est le genre de calcul qu'une unité de calcul d'adresse sait déjà faire. La fusion est donc assez évidente.
Pour donner un exemple, l'Intel 8086 fusionnait l'unité de calcul d'adresse et la MMU. Précisément, il utilisait un même additionneur pour incrémenter le ''program counter'' et effectuer des calculs d'adresse liés à la segmentation. Il aurait été logique d'ajouter les pointeurs de pile avec, mais ce n'était pas possible. La raison est que le pointeur de pile ne peut pas être envoyé directement sur le bus d'adresse, vu qu'il doit passer par une phase de traduction en adresse physique liée à la segmentation.
[[File:80186 arch.png|centre|vignette|upright=2|Intel 8086, microarchitecture.]]
===Les MMU séparées du processeur, sur la carte mère===
Avant d'être intégrées au processeur, les MMU étaient des circuits séparés, placés sur la carte mère, entre le processeur et la mémoire. Elle étaient connectées en plein milieu du bus d'adresse. A savoir que le bus d'adresse entrait dans la MMU et en ressortait. Le processeur envoyait des adresses à la MMU, qui renvoyait une adresse physique. La MMU était facultative, à savoir que le système pouvait fonctionner sans MMU d'installée, mais l'abstraction mémoire n’était alors pas disponible.
Par exemple, les processeurs Motorola 68000 et 68010 pouvaient être combinés avec une MMU de type Motorola 68451. Elle supportait des versions simplifiées de la segmentation et de la pagination. Au minimum, elle ajoutait un support de la protection mémoire contre certains accès non-autorisés. La gestion de la mémoire virtuelle proprement dit n'était possible que si le processeur utilisé était un Motorola 68010, en raison de la manière dont le 68000 gérait ses accès mémoire. La MMU 68451 gérait un espace d'adressage de 16 mébioctets, découpé en maximum 32 pages/segments. On pouvait dépasser cette limite de 32 segments/pages en combinant plusieurs 68451.
Le Motorola 68851 était une MMU qui était prévue pour fonctionner de paire avec le Motorola 68020. Elle gérait la pagination pour un espace d'adressage de 32 bits.
Les processeurs suivants, les 68030, 68040, et 68060, avaient une MMU interne au processeur.
==La relocation matérielle==
Pour rappel, les systèmes d'exploitation moderne permettent de lancer plusieurs programmes en même temps et les laissent se partager la mémoire. Dans le cas le plus simple, qui n'est pas celui des OS modernes, le système d'exploitation découpe la mémoire en blocs d'adresses contiguës qui sont appelés des '''segments''', ou encore des ''partitions mémoire''. Les segments correspondent à un bloc de mémoire RAM. C'est-à-dire qu'un segment de 259 mébioctets sera un segment continu de 259 mébioctets dans la mémoire physique comme dans la mémoire logique. Dans ce qui suit, un segment contient un programme en cours d'exécution, comme illustré ci-dessous.
[[File:CPT Memory Addressable.svg|centre|vignette|upright=2|Espace d'adressage segmenté.]]
Le système d'exploitation mémorise la position de chaque segment en mémoire, ainsi que d'autres informations annexes. Le tout est regroupé dans la '''table de segment''', un tableau dont chaque case est attribuée à un programme/segment. La table des segments est un tableau numéroté, chaque segment ayant un numéro qui précise sa position dans le tableau. Chaque case, chaque entrée, contient un '''descripteur de segment''' qui regroupe plusieurs informations sur le segment : son adresse de base, sa taille, diverses informations.
===La relocation avec la relocation matérielle : le registre de base===
Un segment peut être placé n'importe où en RAM physique et sa position en RAM change à chaque exécution. Le programme est chargé à une adresse, celle du début du segment, qui change à chaque chargement du programme. Et toutes les adresses utilisées par le programme doivent être corrigées lors du chargement du programme, généralement par l'OS. Cette correction s'appelle la '''relocation''', et elle consiste à ajouter l'adresse de début du segment à chaque adresse manipulée par le programme.
[[File:Relocation assistée par matériel.png|centre|vignette|upright=2.5|Relocation.]]
La relocation matérielle fait que la relocation est faite par le processeur, pas par l'OS. La relocation est intégrée dans le processeur par l'intégration d'un registre : le '''registre de base''', aussi appelé '''registre de relocation'''. Il mémorise l'adresse à laquelle commence le segment, la première adresse du programme. Pour effectuer la relocation, le processeur ajoute automatiquement l'adresse de base à chaque accès mémoire, en allant la chercher dans le registre de relocation.
[[File:Registre de base de segment.png|centre|vignette|upright=2|Registre de base de segment.]]
Le processeur s'occupe de la relocation des segments et le programme compilé n'en voit rien. Pour le dire autrement, les programmes manipulent des adresses logiques, qui sont traduites par le processeur en adresses physiques. La traduction se fait en ajoutant le contenu du registre de relocation à l'adresse logique. De plus, cette méthode fait que chaque programme a son propre espace d'adressage.
[[File:CPU created logical address presentation.png|centre|vignette|upright=2|Traduction d'adresse avec la relocation matérielle.]]
Le système d'exploitation mémorise les adresses de base pour chaque programme, dans la table des segments. Le registre de base est mis à jour automatiquement lors de chaque changement de segment. Pour cela, le registre de base est accessible via certaines instructions, accessibles en espace noyau, plus rarement en espace utilisateur. Le registre de segment est censé être adressé implicitement, vu qu'il est unique. Si ce n'est pas le cas, il est possible d'écrire dans ce registre de segment, qui est alors adressable.
===La protection mémoire avec la relocation matérielle : le registre limite===
Sans restrictions supplémentaires, la taille maximale d'un segment est égale à la taille complète de l'espace d'adressage. Sur les processeurs 32 bits, un segment a une taille maximale de 2^32 octets, soit 4 gibioctets. Mais il est possible de limiter la taille du segment à 2 gibioctets, 1 gibioctet, 64 Kibioctets, ou toute autre taille. La limite est définie lors de la création du segment, mais elle peut cependant évoluer au cours de l'exécution du programme, grâce à l'allocation mémoire.
Le processeur vérifie à chaque accès mémoire que celui-ci se fait bien dans le segment, qu'il ne déborde pas en-dehors. C'est possible qu'une adresse calculée sorte du segment, à la suite d'un bug ou d'une erreur de programmation, voire pire. Et le processeur doit éviter de tels '''débordements de segments'''.
A chaque accès mémoire, le processeur compare l'adresse accédée et vérifie qu'elle est bien dans le segment. Pour cela, il y a deux solutions. La première part du principe que le segment est placé en mémoire entre l'adresse de base et l'adresse limite. Il suffit de mémoriser l'adresse limite, l'adresse physique à ne pas dépasser. Une autre solution mémorise la taille du segment. La table des segments doit donc mémoriser, en plus de l'adresse de base : soit l'adresse maximale du segment, soit la taille du segment. D'autres informations peuvent être ajoutées, comme on le verra plus tard, mais cela complexifie la table des segments.
De plus, le processeur se voit ajouter un '''registre limite''', qui mémorise soit la taille du segment, soit l'adresse limite. Les deux registres, base et limite, sont utilisés pour vérifier si un programme qui lit/écrit de la mémoire en-dehors de son segment attitré : au-delà pour le registre limite, en-deça pour le registre de base. Le processeur vérifie pour chaque accès mémoire ne déborde pas au-delà du segment qui lui est allouée, ce qui n'arrive que si l'adresse d'accès dépasse la valeur du registre limite. Pour les accès en-dessous du segment, il suffit de vérifier si l'addition de relocation déborde, tout débordement signifiant erreur de protection mémoire.
[[File:Registre limite.png|centre|vignette|upright=2|Registre limite]]
Utiliser la taille du segment a de nombreux avantages. L'un d'entre eux se manifeste quand on déplace un segment en mémoire RAM. Le descripteur doit alors être mis à jour, et c'est plus facile quand on utilise la taille du segment. Si on utilise l'adresse limite, il faut mettre à jour à la fois l'adresse de base et l'adresse limite, dans le descripteur. En utilisant la taille, seule l'adresse de base doit être modifiée, vu que le segment n'a pas changé de taille. Un autre avantage est lié aux performances, mais nous devons faire un détour pour le comprendre.
La taille du segment est équivalent à l'adresse logique maximale possible. Par exemple, si un segment fait 256 octets, les adresses logiques possibles vont de 0 à 255, 256 est donc à la fois la taille du segment et l'adresse logique à partir de laquelle on déborde du segment. Et cela marche si on remplace 256 par n'importe quelle valeur : vu que le segment commence à l'adresse 0, sa taille en octets indique l'adresse de dépassement. Interpréter la taille du segment comme une adresse logique fait que les tests avec le registre limite sont plus performants, voyons pourquoi.
En utilisant l'adresse physique limite, on doit faire la relocation, puis comparer l'adresse calculée avec l'adresse limite. Le calcul d'adresse doit se faire avant la vérification. En utilisant la taille, on doit comparer l'adresse logique avec la taille du segment. On peut alors faire le test de débordement avant ou pendant la relocation. Les deux peuvent être faits en parallèle, dans deux circuits distincts, ce qui améliore un peu le temps d'un accès mémoire. Quelques processeurs en ont profité, mais on verra cela dans la section sur la segmentation.
[[File:Comparaison entre adresse limite physique et logique.png|centre|vignette|upright=2|Comparaison entre adresse limite physique et logique]]
Les registres de base et limite sont altérés uniquement par le système d'exploitation et ne sont accessibles qu'en espace noyau. Lorsque le système d'exploitation charge un programme, ou reprend son exécution, il charge les adresses de début/fin du segment dans ces registres. D'ailleurs, ces deux registres doivent être sauvegardés et restaurés lors de chaque interruption. Par contre, et c'est assez évident, ils ne le sont pas lors d'un appel de fonction. Cela fait une différence de plus entre interruption et appels de fonctions.
: Il faut noter que le registre limite et le registre de base sont parfois fusionnés en un seul registre, qui contient un descripteur de segment tout entier.
Pour information, la relocation matérielle avec un registre limite a été implémentée sur plusieurs processeurs assez anciens, notamment sur les anciens supercalculateurs de marque CDC. Un exemple est le fameux CDC 6600, qui implémentait cette technique.
===La mémoire virtuelle avec la relocation matérielle===
Il est possible d'implémenter la mémoire virtuelle avec la relocation matérielle. Pour cela, il faut swapper des segments entiers sur le disque dur. Les segments sont placés en mémoire RAM et leur taille évolue au fur et à mesure que les programmes demandent du rab de mémoire RAM. Lorsque la mémoire est pleine, ou qu'un programme demande plus de mémoire que disponible, des segments entiers sont sauvegardés dans le ''swapfile'', pour faire de la place.
Faire ainsi de demande juste de mémoriser si un segment est en mémoire RAM ou non, ainsi que la position des segments swappés dans le ''swapfile''. Pour cela, il faut modifier la table des segments, afin d'ajouter un '''bit de swap''' qui précise si le segment en question est swappé ou non. Lorsque le système d'exploitation veut swapper un segment, il le copie dans le ''swapfile'' et met ce bit à 1. Lorsque l'OS recharge ce segment en RAM, il remet ce bit à 0. La gestion de la position des segments dans le ''swapfile'' est le fait d'une structure de données séparée de la table des segments.
L'OS exécute chaque programme l'un après l'autre, à tour de rôle. Lorsque le tour d'un programme arrive, il consulte la table des segments pour récupérer les adresses de base et limite, mais il vérifie aussi le bit de swap. Si le bit de swap est à 0, alors l'OS se contente de charger les adresses de base et limite dans les registres adéquats. Mais sinon, il démarre une routine d'interruption qui charge le segment voulu en RAM, depuis le ''swapfile''. C'est seulement une fois le segment chargé que l'on connait son adresse de base/limite et que le chargement des registres de relocation peut se faire.
Un défaut évident de cette méthode est que l'on swappe des programmes entiers, qui sont généralement assez imposants. Les segments font généralement plusieurs centaines de mébioctets, pour ne pas dire plusieurs gibioctets, à l'époque actuelle. Ils étaient plus petits dans l'ancien temps, mais la mémoire était alors plus lente. Toujours est-il que la copie sur le disque dur des segments est donc longue, lente, et pas vraiment compatible avec le fait que les programmes s'exécutent à tour de rôle. Et ca explique pourquoi la relocation matérielle n'est presque jamais utilisée avec de la mémoire virtuelle.
===L'extension d'adressage avec la relocation matérielle===
Passons maintenant à la dernière fonctionnalité implémentable avec la traduction d'adresse : l'extension d'adressage. Elle permet d'utiliser plus de mémoire que ne le permet l'espace d'adressage. Par exemple, utiliser plus de 64 kibioctets de mémoire sur un processeur 16 bits. Pour cela, les adresses envoyées à la mémoire doivent être plus longues que les adresses gérées par le processeur.
L'extension des adresses se fait assez simplement avec la relocation matérielle : il suffit que le registre de base soit plus long. Prenons l'exemple d'un processeur aux adresses de 16 bits, mais qui est reliée à un bus d'adresse de 24 bits. L'espace d'adressage fait juste 64 kibioctets, mais le bus d'adresse gère 16 mébioctets de RAM. On peut utiliser les 16 mébioctets de RAM à une condition : que le registre de base fasse 24 bits, pas 16.
Un défaut de cette approche est qu'un programme ne peut pas utiliser plus de mémoire que ce que permet l'espace d'adressage. Mais par contre, on peut placer chaque programme dans des portions différentes de mémoire. Imaginons par exemple que l'on ait un processeur 16 bits, mais un bus d'adresse de 20 bits. Il est alors possible de découper la mémoire en 16 blocs de 64 kibioctets, chacun attribué à un segment/programme, qu'on sélectionne avec les 4 bits de poids fort de l'adresse. Il suffit de faire démarrer les segments au bon endroit en RAM, et cela demande juste que le registre de base le permette. C'est une sorte d'émulation de la commutation de banques.
==La segmentation en mode réel des processeurs x86==
Avant de passer à la suite, nous allons voir la technique de segmentation de l'Intel 8086, un des tout premiers processeurs 16 bits. Il s'agissait d'une forme très simple de segmentation, sans aucune forme de protection mémoire, ni même de mémoire virtuelle, ce qui le place à part des autres formes de segmentation. Il s'agit d'une amélioration de la relocation matérielle, qui avait pour but de permettre d'utiliser plus de 64 kibioctets de mémoire, ce qui était la limite maximale sur les processeurs 16 bits de l'époque.
Par la suite, la segmentation s'améliora et ajouta un support complet de la mémoire virtuelle et de la protection mémoire. L'ancienne forme de segmentation fut alors appelé le '''mode réel''', et la nouvelle forme de segmentation fut appelée le '''mode protégé'''. Le mode protégé rajoute la protection mémoire, en ajoutant des registres limite et une gestion des droits d'accès aux segments, absents en mode réel. De plus, il ajoute un support de la mémoire virtuelle grâce à l'utilisation d'une des segments digne de ce nom, table qui est absente en mode réel ! Pour le moment, voyons le mode réel.
===Les segments en mode réel===
[[File:Typical computer data memory arrangement.png|vignette|upright=0.5|Typical computer data memory arrangement]]
La segmentation en mode réel sépare la pile, le tas, le code machine et les données constantes dans quatre segments distincts.
* Le segment '''''text''''', qui contient le code machine du programme, de taille fixe.
* Le segment '''''data''''' contient des données de taille fixe qui occupent de la mémoire de façon permanente, des constantes, des variables globales, etc.
* Le segment pour la '''pile''', de taille variable.
* le reste est appelé le '''tas''', de taille variable.
Un point important est que sur ces processeurs, il n'y a pas de table des segments proprement dit. Chaque programme gére de lui-même les adresses de base des segments qu'il manipule. Il n'est en rien aidé par une table des segments gérée par le système d'exploitation.
===Les registres de segments en mode réel===
Chaque segment subit la relocation indépendamment des autres. Pour cela, le processeur intégre plusieurs registres de base, un par segment. Notons que cette solution ne marche que si le nombre de segments par programme est limité, à une dizaine de segments tout au plus. Les processeurs x86 utilisaient cette méthode, et n'associaient que 4 à 6 registres de segments par programme.
Les processeurs 8086 et le 286 avaient quatre registres de segment : un pour le code, un autre pour les données, et un pour la pile, le quatrième étant un registre facultatif laissé à l'appréciation du programmeur. Ils sont nommés CS (''code segment''), DS (''data segment''), SS (''Stack segment''), et ES (''Extra segment''). Le 386 rajouta deux registres, les registres FS et GS, qui sont utilisés pour les segments de données. Les processeurs post-386 ont donc 6 registres de segment.
Les registres CS et SS sont adressés implicitement, en fonction de l'instruction exécutée. Les instructions de la pile manipulent le segment associé à la pile, le chargement des instructions se fait dans le segment de code, les instructions arithmétiques et logiques vont chercher leurs opérandes sur le tas, etc. Et donc, toutes les instructions sont chargées depuis le segment pointé par CS, les instructions de gestion de la pile (PUSH et POP) utilisent le segment pointé par SS.
Les segments DS et ES sont, eux aussi, adressés implicitement. Pour cela, les instructions LOAD/STORE sont dupliquées : il y a une instruction LOAD pour le segment DS, une autre pour le segment ES. D'autres instructions lisent leurs opérandes dans un segment par défaut, mais on peut changer ce choix par défaut en précisant le segment voulu. Un exemple est celui de l'instruction CMPSB, qui compare deux octets/bytes : le premier est chargé depuis le segment DS, le second depuis le segment ES.
Un autre exemple est celui de l'instruction MOV avec un opérande en mémoire. Elle lit l'opérande en mémoire depuis le segment DS par défaut. Il est possible de préciser le segment de destination si celui-ci n'est pas DS. Par exemple, l'instruction MOV [A], AX écrit le contenu du registre AX dans l'adresse A du segment DS. Par contre, l'instruction MOV ES:[A], copie le contenu du registre AX das l'adresse A, mais dans le segment ES.
===La traduction d'adresse en mode réel===
La segmentation en mode réel a pour seul but de permettre à un programme de dépasser la limite des 64 KB autorisée par les adresses de 16 bits. L'idée est que chaque segment a droit à son propre espace de 64 KB. On a ainsi 64 Kb pour le code machine, 64 KB pour la pile, 64 KB pour un segment de données, etc. Les registres de segment mémorisaient la base du segment, les adresses calculées par l'ALU étant des ''offsets''. Ce sont tous des registres de 16 bits, mais ils ne mémorisent pas des adresses physiques de 16 bits, comme nous allons le voir.
[[File:Table des segments dans un banc de registres.png|centre|vignette|upright=2|Table des segments dans un banc de registres.]]
L'Intel 8086 utilisait des adresses de 20 bits, ce qui permet d'adresser 1 mébioctet de RAM. Vous pouvez vous demander comment on peut obtenir des adresses de 20 bits alors que les registres de segments font tous 16 bits ? Cela tient à la manière dont sont calculées les adresses physiques. Le registre de segment n'est pas additionné tel quel avec le décalage : à la place, le registre de segment est décalé de 4 rangs vers la gauche. Le décalage de 4 rangs vers la gauche fait que chaque segment a une adresse qui est multiple de 16. Le fait que le décalage soit de 16 bits fait que les segments ont une taille de 64 kibioctets.
{|class="wikitable"
|-
| <code> </code><code style="background:#DED">0000 0110 1110 1111</code><code>0000</code>
| Registre de segment -
| 16 bits, décalé de 4 bits vers la gauche
|-
| <code>+ </code><code style="background:#DDF">0001 0010 0011 0100</code>
| Décalage/''Offset''
| 16 bits
|-
| colspan="3" |
|-
| <code> </code><code style="background:#FDF">0000 1000 0001 0010 0100</code>
| Adresse finale
| 20 bits
|}
Vous aurez peut-être remarqué que le calcul peut déborder, dépasser 20 bits. Mais nous reviendrons là-dessus plus bas. L'essentiel est que la MMU pour la segmentation en mode réel se résume à quelques registres et des additionneurs/soustracteurs.
Un exemple est l'Intel 8086, un des tout premier processeur Intel. Le processeur était découpé en deux portions : l'interface mémoire et le reste du processeur. L'interface mémoire est appelée la '''''Bus Interface Unit''''', et le reste du processeur est appelé l{{'}}'''''Execution Unit'''''. L'interface mémoire contenait les registres de segment, au nombre de 4, ainsi qu'un additionneur utilisé pour traduire les adresses logiques en adresses physiques. Elle contenait aussi une file d'attente où étaient préchargées les instructions.
Sur le 8086, la MMU est fusionnée avec les circuits de gestion du ''program counter''. Les registres de segment sont regroupés avec le ''program counter'' dans un même banc de registres, et un additionneur unique est utilisé à la fois à incrémenter le ''program counter'' et pour gérer la segmentation. L'additionneur est donc mutualisé entre segmentation et ''program counter''. En somme, il n'y a pas vraiment de MMU dédiée, mais un super-circuit en charge du Fetch et de la mémoire virtuelle, ainsi que du préchargement des instructions. Nous en reparlerons au chapitre suivant.
[[File:80186 arch.png|centre|vignette|upright=2.5|Architecture du 8086, du 80186 et de ses variantes.]]
La MMU du 286 était fusionnée avec l'unité de calcul d'adresse. Elle contient les registres de segments, un comparateur pour détecter les accès hors-segment, et plusieurs additionneurs. Il y a un additionneur pour les calculs d'adresse proprement dit, suivi d'un additionneur pour la relocation.
[[File:Intel i80286 arch.svg|centre|vignette|upright=3|Intel i80286 arch]]
===La segmentation en mode réel accepte plusieurs segments de code/données===
Les programmes peuvent parfaitement répartir leur code machine dans plusieurs segments de code. La limite de 64 KB par segment est en effet assez limitante, et il n'était pas rare qu'un programme stocke son code dans deux ou trois segments. Il en est de même avec les données, qui peuvent être réparties dans deux ou trois segments séparés. La seule exception est la pile : elle est forcément dans un segment unique et ne peut pas dépasser 64 KB.
Pour gérer plusieurs segments de code/donnée, il faut changer de segment à la volée suivant les besoins, en modifiant les registres de segment. Il s'agit de la technique de '''commutation de segment'''. Pour cela, tous les registres de segment, à l'exception de CS, peuvent être altérés par une instruction d'accès mémoire, soit avec une instruction MOV, soit en y copiant le sommet de la pile avec une instruction de dépilage POP. L'absence de sécurité fait que la gestion de ces registres est le fait du programmeur, qui doit redoubler de prudence pour ne pas faire n'importe quoi.
Pour le code machine, le répartir dans plusieurs segments posait des problèmes au niveau des branchements. Si la plupart des branchements sautaient vers une instruction dans le même segment, quelques rares branchements sautaient vers du code machine dans un autre segment. Intel avait prévu le coup et disposait de deux instructions de branchement différentes pour ces deux situations : les '''''near jumps''''' et les '''''far jumps'''''. Les premiers sont des branchements normaux, qui précisent juste l'adresse à laquelle brancher, qui correspond à la position de la fonction dans le segment. Les seconds branchent vers une instruction dans un autre segment, et doivent préciser deux choses : l'adresse de base du segment de destination, et la position de la destination dans le segment. Le branchement met à jour le registre CS avec l'adresse de base, avant de faire le branchement. Ces derniers étaient plus lents, car on n'avait pas à changer de segment et mettre à jour l'état du processeur.
Il y avait la même pour l'instruction d'appel de fonction, avec deux versions de cette instruction. La première version, le '''''near call''''' est un appel de fonction normal, la fonction appelée est dans le segment en cours. Avec la seconde version, le '''''far call''''', la fonction appelée est dans un segment différent. L'instruction a là aussi besoin de deux opérandes : l'adresse de base du segment de destination, et la position de la fonction dans le segment. Un ''far call'' met à jour le registre CS avec l'adresse de base, ce qui fait que les ''far call'' sont plus lents que les ''near call''. Il existe aussi la même chose, pour les instructions de retour de fonction, avec une instruction de retour de fonction normale et une instruction de retour qui renvoie vers un autre segment, qui sont respectivement appelées '''''near return''''' et '''''far return'''''. Là encore, il faut préciser l'adresse du segment de destination dans le second cas.
La même chose est possible pour les segments de données. Sauf que cette fois-ci, ce sont les pointeurs qui sont modifiés. pour rappel, les pointeurs sont, en programmation, des variables qui contiennent des adresses. Lors de la compilation, ces pointeurs sont placés soit dans un registre, soit dans les instructions (adressage absolu), ou autres. Ici, il existe deux types de pointeurs, appelés '''''near pointer''''' et '''''far pointer'''''. Vous l'avez deviné, les premiers sont utilisés pour localiser les données dans le segment en cours d'utilisation, alors que les seconds pointent vers une donnée dans un autre segment. Là encore, la différence est que le premier se contente de donner la position dans le segment, alors que les seconds rajoutent l'adresse de base du segment. Les premiers font 16 bits, alors que les seconds en font 32 : 16 bits pour l'adresse de base et 16 pour l{{'}}''offset''.
===L'occupation de l'espace d'adressage par les segments===
Nous venons de voir qu'un programme pouvait utiliser plus de 4-6 segments, avec la commutation de segment. Mais d'autres programmes faisaient l'inverse, à savoir qu'ils se débrouillaient avec seulement 1 ou 2 segments. Suivant le nombre de segments utilisés, la configuration des registres n'était pas la même. Les configurations possibles sont appelées des ''modèle mémoire'', et il y en a en tout 6. En voici la liste :
{| class="wikitable"
|-
! Modèle mémoire !! Configuration des segments !! Configuration des registres || Pointeurs utilisés || Branchements utilisés
|-
| Tiny* || Segment unique pour tout le programme || CS=DS=SS || ''near'' uniquement || ''near'' uniquement
|-
| Small || Segment de donnée séparé du segment de code, pile dans le segment de données || DS=SS || ''near'' uniquement || ''near'' uniquement
|-
| Medium || Plusieurs segments de code unique, un seul segment de données || CS, DS et SS sont différents || ''near'' et ''far'' || ''near'' uniquement
|-
| Compact || Segment de code unique, plusieurs segments de données || CS, DS et SS sont différents || ''near'' uniquement || ''near'' et ''far''
|-
| Large || Plusieurs segments de code, plusieurs segments de données || CS, DS et SS sont différents || ''near'' et ''far'' || ''near'' et ''far''
|}
Un programme est censé utiliser maximum 4-6 segments de 64 KB, ce qui permet d'adresser maximum 64 * 6 = 384 KB de RAM, soit bien moins que le mébioctet de mémoire théoriquement adressable. Mais ce défaut est en réalité contourné par la commutation de segment, qui permettait d'adresser la totalité de la RAM si besoin. Une second manière de contourner cette limite est que plusieurs processus peuvent s'exécuter sur un seul processeur, si l'OS le permet. Ce n'était pas le cas à l'époque du DOS, qui était un OS mono-programmé, mais c'était en théorie possible. La limite est de 6 segments par programme/processus, en exécuter plusieurs permet d'utiliser toute la mémoire disponible rapidement.
[[File:Overlapping realmode segments.svg|vignette|Segments qui se recouvrent en mode réel.]]
Vous remarquerez qu'avec des registres de segments de 16 bits, on peut gérer 65536 segments différents, chacun de 64 KB. Et 65 536 segments de 64 kibioctets, ça ne rentre pas dans le mébioctet de mémoire permis avec des adresses de 20 bits. La raison est que plusieurs couples segment+''offset'' pointent vers la même adresse. En tout, chaque adresse peut être adressée par 4096 couples segment+''offset'' différents.
L'avantage de cette méthode est que des segments peuvent se recouvrir, à savoir que la fin de l'un se situe dans le début de l'autre, comme illustré ci-contre. Cela permet en théorie de partager de la mémoire entre deux processus. Mais la technique est tout sauf pratique et est donc peu utilisée. Elle demande de placer minutieusement les segments en RAM, et les données à partager dans les segments. En pratique, les programmeurs et OS utilisent des segments qui ne se recouvrent pas et sont disjoints en RAM.
Le nombre maximal de segments disjoints se calcule en prenant la taille de la RAM, qu'on divise par la taille d'un segment. Le calcul donne : 1024 kibioctets / 64 kibioctets = 16 segments disjoints. Un autre calcul prend le nombre de segments divisé par le nombre d'adresses aliasées, ce qui donne 65536 / 4096 = 16. Seulement 16 segments, c'est peu. En comptant les segments utilisés par l'OS et ceux utilisés par le programme, la limite est vite atteinte si le programme utilise la commutation de segment.
===Le mode réel sur les 286 et plus : la ligne d'adresse A20===
Pour résumer, le registre de segment contient des adresses de 20 bits, dont les 4 bits de poids faible sont à 0. Et il se voit ajouter un ''offset'' de 16 bits. Intéressons-nous un peu à l'adresse maximale que l'on peut calculer avec ce système. Nous allons l'appeler l{{'}}'''adresse maximale de segmentation'''. Elle vaut :
{|class="wikitable"
|-
| <code> </code><code style="background:#DED">1111 1111 1111 1111</code><code>0000</code>
| Registre de segment -
| 16 bits, décalé de 4 bits vers la gauche
|-
| <code>+ </code><code style="background:#DDF">1111 1111 1111 1111</code>
| Décalage/''Offset''
| 16 bits
|-
| colspan="3" |
|-
| <code> </code><code style="background:#FDF">1 0000 1111 1111 1110 1111</code>
| Adresse finale
| 20 bits
|}
Le résultat n'est pas l'adresse maximale codée sur 20 bits, car l'addition déborde. Elle donne un résultat qui dépasse l'adresse maximale permis par les 20 bits, il y a un 21ème bit en plus. De plus, les 20 bits de poids faible ont une valeur bien précise. Ils donnent la différence entre l'adresse maximale permise sur 20 bit, et l'adresse maximale de segmentation. Les bits 1111 1111 1110 1111 traduits en binaire donnent 65 519; auxquels il faut ajouter l'adresse 1 0000 0000 0000 0000. En tout, cela fait 65 520 octets adressables en trop. En clair : on dépasse la limite du mébioctet de 65 520 octets. Le résultat est alors très différent selon que l'on parle des processeurs avant le 286 ou après.
Avant le 286, le bus d'adresse faisait exactement 20 bits. Les adresses calculées ne pouvaient pas dépasser 20 bits. L'addition générait donc un débordement d'entier, géré en arithmétique modulaire. En clair, les bits de poids fort au-delà du vingtième sont perdus. Le calcul de l'adresse débordait et retournait au début de la mémoire, sur les 65 520 premiers octets de la mémoire RAM.
[[File:IBM PC Memory areas.svg|vignette|IBM PC Memory Map, la ''High memory area'' est en jaune.]]
Le 80286 en mode réel gère des adresses de base de 24 bits, soit 4 bits de plus que le 8086. Le résultat est qu'il n'y a pas de débordement. Les bits de poids fort sont conservés, même au-delà du 20ème. En clair, la segmentation permettait de réellement adresser 65 530 octets au-delà de la limite de 1 mébioctet. La portion de mémoire adressable était appelé la '''''High memory area''''', qu'on va abrévier en HMA.
{| class="wikitable"
|+ Espace d'adressage du 286
|-
! Adresses en héxadécimal !! Zone de mémoire
|-
| 10 FFF0 à FF FFFF || Mémoire étendue, au-delà du premier mébioctet
|-
| 10 0000 à 10 FFEF || ''High Memory Area''
|-
| 0 à 0F FFFF || Mémoire adressable en mode réel
|}
En conséquence, les applications peuvent utiliser plus d'un mébioctet de RAM, mais au prix d'une rétrocompatibilité imparfaite. Quelques programmes DOS ne marchaient pus à cause de ça. D'autres fonctionnaient convenablement et pouvaient adresser les 65 520 octets en plus.
Pour résoudre ce problème, les carte mères ajoutaient un petit circuit relié au 21ème bit d'adresse, nommé A20 (pas d'erreur, les fils du bus d'adresse sont numérotés à partir de 0). Le circuit en question pouvait mettre à zéro le fil d'adresse, ou au contraire le laisser tranquille. En le forçant à 0, le calcul des adresses déborde comme dans le mode réel des 8086. Mais s'il ne le fait pas, la ''high memory area'' est adressable. Le circuit était une simple porte ET, qui combinait le 21ème bit d'adresse avec un '''signal de commande A20''' provenant d'ailleurs.
Le signal de commande A20 était géré par le contrôleur de clavier, qui était soudé à la carte mère. Le contrôleur en question ne gérait pas que le clavier, il pouvait aussi RESET le processeur, alors gérer le signal de commande A20 n'était pas si problématique. Quitte à avoir un microcontrôleur sur la carte mère, autant s'en servir au maximum... La gestion du bus d'adresse étaitdonc gérable au clavier. D'autres carte mères faisaient autrement et préféraient ajouter un interrupteur, pour activer ou non la mise à 0 du 21ème bit d'adresse.
: Il faut noter que le signal de commande A20 était mis à 1 en mode protégé, afin que le 21ème bit d'adresse soit activé.
Le 386 ajouta deux registres de segment, les registres FS et GS, ainsi que le '''mode ''virtual 8086'''''. Ce dernier permet d’exécuter des programmes en mode réel alors que le système d'exploitation s'exécute en mode protégé. C'est une technique de virtualisation matérielle qui permet d'émuler un 8086 sur un 386. L'avantage est que la compatibilité avec les programmes anciens écrits pour le 8086 est conservée, tout en profitant de la protection mémoire. Tous les processeurs x86 qui ont suivi supportent ce mode virtuel 8086.
==La segmentation avec une table des segments==
La '''segmentation avec une table des segments''' est apparue sur des processeurs assez anciens, le tout premier étant le Burrough 5000. Elle a ensuite été utilisée sur les processeurs x86 des PCs, à partir du 286 d'Intel. Elle est aujourd'hui abandonnée sur les jeux d'instruction x86. Tout comme la segmentation en mode réel, la segmentation attribue plusieurs segments par programmes ! Et cela a des répercutions sur la manière dont la traduction d'adresse est effectuée.
===Pourquoi plusieurs segments par programme ?===
L'utilité d'avoir plusieurs segments par programme n'est pas évidente, mais elle le devient quand on se plonge dans le passé. Dans le passé, les programmeurs devaient faire avec une quantité de mémoire limitée et il n'était pas rare que certains programmes utilisent plus de mémoire que disponible sur la machine. Mais les programmeurs concevaient leurs programmes en fonction.
[[File:Overlay Programming.svg|vignette|upright=1|Overlay Programming]]
L'idée était d'implémenter un système de mémoire virtuelle, mais émulé en logiciel, appelé l{{'}}'''''overlaying'''''. Le programme était découpé en plusieurs morceaux, appelés des ''overlays''. Les ''overlays'' les plus importants étaient en permanence en RAM, mais les autres étaient faisaient un va-et-vient entre RAM et disque dur. Ils étaient chargés en RAM lors de leur utilisation, puis sauvegardés sur le disque dur quand ils étaient inutilisés. Le va-et-vient des ''overlays'' entre RAM et disque dur était réalisé en logiciel, par le programme lui-même. Le matériel n'intervenait pas, comme c'est le cas avec la mémoire virtuelle.
Avec la segmentation, un programme peut utiliser la technique des ''overlays'', mais avec l'aide du matériel. Il suffit de mettre chaque ''overlay'' dans son propre segment, et laisser la segmentation faire. Les segments sont swappés en tout ou rien : on doit swapper tout un segment en entier. L'intérêt est que la gestion du ''swapping'' est grandement facilitée, vu que c'est le système d'exploitation qui s'occupe de swapper les segments sur le disque dur ou de charger des segments en RAM. Pas besoin pour le programmeur de coder quoique ce soit. Par contre, cela demande l'intervention du programmeur, qui doit découper le programme en segments/''overlays'' de lui-même. Sans cela, la segmentation n'est pas très utile.
L{{'}}''overlaying'' est une forme de '''segmentation à granularité grossière''', à savoir que le programme est découpé en segments de grande taille. L'usage classique est d'avoir un segment pour la pile, un autre pour le code exécutable, un autre pour le reste. Éventuellement, on peut découper les trois segments précédents en deux ou trois segments, rarement au-delà. Les segments sont alors peu nombreux, guère plus d'une dizaine par programme. D'où le terme de ''granularité grossière''.
La '''segmentation à granularité fine''' pousse le concept encore plus loin. Avec elle, il y a idéalement un segment par entité manipulée par le programme, un segment pour chaque structure de donnée et/ou chaque objet. Par exemple, un tableau aura son propre segment, ce qui est idéal pour détecter les accès hors tableau. Pour les listes chainées, chaque élément de la liste aura son propre segment. Et ainsi de suite, chaque variable agrégée (non-primitive), chaque structure de donnée, chaque objet, chaque instance d'une classe, a son propre segment. Diverses fonctionnalités supplémentaires peuvent être ajoutées, ce qui transforme le processeur en véritable processeur orienté objet, mais passons ces détails pour le moment.
Vu que les segments correspondent à des objets manipulés par le programme, on peut deviner que leur nombre évolue au cours du temps. En effet, les programmes modernes peuvent demander au système d'exploitation du rab de mémoire pour allouer une nouvelle structure de données. Avec la segmentation à granularité fine, cela demande d'allouer un nouveau segment à chaque nouvelle allocation mémoire, à chaque création d'une nouvelle structure de données ou d'un objet. De plus, les programmes peuvent libérer de la mémoire, en supprimant les structures de données ou objets dont ils n'ont plus besoin. Avec la segmentation à granularité fine, cela revient à détruire le segment alloué pour ces objets/structures de données. Le nombre de segments est donc dynamique, il change au cours de l'exécution du programme.
===Les tables de segments avec la segmentation===
La présence de plusieurs segments par programme a un impact sur la table des segments. Avec la relocation matérielle, elle conte nait un segment par programme. Chaque entrée, chaque ligne de la table des segment, mémorisait l'adresse de base, l'adresse limite, un bit de présence pour la mémoire virtuelle et des autorisations liées à la protection mémoire. Avec la segmentation, les choses sont plus compliquées, car il y a plusieurs segments par programme. Les entrées ne sont pas modifiées, mais elles sont organisées différemment.
Avec cette forme de segmentation, la table des segments doit respecter plusieurs contraintes. Premièrement, il y a plusieurs segments par programmes. Deuxièmement, le nombre de segments est variable : certains programmes se contenteront d'un seul segment, d'autres de dizaine, d'autres plusieurs centaines, etc. Il y a typiquement deux manières de faire : soit utiliser une table des segments uniques, utiliser une table des segment par programme.
Il est possible d'utiliser une table des segment unique qui mémorise tous les segments de tous les processus, système d'exploitation inclut. On parle alors de '''table des segment globale'''. Mais cette solution n'est pas utilisée avec la segmentation proprement dite. Elle est utilisée sur les architectures à capacité qu'on détaillera vers la fin du chapitre, dans une section dédiée. À la place, la segmentation utilise une table de segment par processus/programme, chacun ayant une '''table des segment locale'''.
Dans les faits, les choses sont plus compliquées. Le système d'exploitation doit savoir où se trouvent les tables de segment locale pour chaque programme. Pour cela, il a besoin d'utiliser une table de segment globale, dont chaque entrée pointe non pas vers un segment, mais vers une table de segment locale. Lorsque l'OS effectue une commutation de contexte, il lit la table des segment globale, pour récupérer un pointeur vers celle-ci. Ce pointeur est alors chargé dans un registre du processeur, qui mémorise l'adresse de la table locale, ce qui sert lors des accès mémoire.
Une telle organisation fait que les segments d'un processus/programme sont invisibles pour les autres, il y a une certaine forme de sécurité. Un programme ne connait que sa table de segments locale, il n'a pas accès directement à la table des segments globales. Tout accès mémoire se passera à travers la table de segment locale, il ne sait pas où se trouvent les autres tables de segment locales.
Les processeurs x86 sont dans ce cas : ils utilisent une table de segment globale couplée à autant de table des segments qu'il y a de processus en cours d'exécution. La table des segments globale s'appelle la '''''Global Descriptor Table''''' et elle peut contenir 8192 segments maximum, ce qui permet le support de 8192 processus différents. Les tables de segments locales sont appelées les '''''Local Descriptor Table''''' et elles font aussi 8192 segments maximum, ce qui fait 8192 segments par programme maximum. Il faut noter que la table de segment globale peut mémoriser des pointeurs vers les routines d'interruption, certaines données partagées (le tampon mémoire pour le clavier) et quelques autres choses, qui n'ont pas leur place dans les tables de segment locales.
===La relocation avec la segmentation===
La table des segments locale mémorise les adresses de base et limite de chaque segment, ainsi que d'autres méta-données. Les informations pour un segment sont regroupés dans un '''descripteur de segment''', qui est codé sur plusieurs octets, et qui regroupe : adresse de base, adresse limite, bit de présence en RAM, méta-données de protection mémoire.
La table des segments est un tableau dans lequel les descripteurs de segment sont placés les uns à la suite des autres en mémoire RAM. La table des segments est donc un tableau de segment. Les segments d'un programme sont numérotés, le nombre s'appelant un '''indice de segment''', appelé '''sélecteur de segment''' dans la terminologie Intel. L'indice de segment n'est autre que l'indice du segment dans ce tableau.
[[File:Global Descriptor table.png|centre|vignette|upright=2|Table des segments locale.]]
Il n'y a pas de registre de segment proprement dit, qui mémoriserait l'adresse de base. À la place, les segments sont adressés de manière indirecte. À la place, les registres de segment mémorisent des sélecteurs de segment. Ils sont utilisés pour lire l'adresse de base/limite dans la table de segment en mémoire RAM. Pour cela, un registre mémorise l'adresse de la table de segment locale, sa position en mémoire RAM.
Toute lecture ou écriture se fait en deux temps, en deux accès mémoire, consécutifs. Premièrement, le numéro de segment est utilisé pour adresser la table des segment. La lecture récupère alors un pointeur vers ce segment. Deuxièmement, ce pointeur est utilisé pour faire la lecture ou écriture. Plus précisément, la première lecture récupère un descripteur de segment qui contient l'adresse de base, le pointeur voulu, mais aussi l'adresse limite et d'autres informations.
[[File:Segmentation avec table des segments.png|centre|vignette|upright=2|Segmentation avec table des segments]]
L'accès à la table des segments se fait automatiquement à chaque accès mémoire. La conséquence est que chaque accès mémoire demande d'en faire deux : un pour lire la table des segments, l'autre pour l'accès lui-même. Il s'agit en quelque sorte d'une forme d'adressage indirect mémoire.
Un point important est que si le premier accès ne fait qu'une simple lecture dans un tableau, le second accès implique des calculs d'adresse. En effet, le premier accès récupère l'adresse de base du segment, mais le second accès sélectionne une donnée dans le segment, ce qui demande de calculer son adresse. L'adresse finale se déduit en combinant l'adresse de base avec un décalage (''offset'') qui donne la position de la donnée dans ce segment. L'indice de segment est utilisé pour récupérer l'adresse de base du segment. Une fois cette adresse de base connue, on lui additionne le décalage pour obtenir l'adresse finale.
[[File:Table des segments.png|centre|vignette|upright=2|Traduction d'adresse avec une table des segments.]]
Pour effectuer automatiquement l'accès à la table des segments, le processeur doit contenir un registre supplémentaire, qui contient l'adresse de la table de segment, afin de la localiser en mémoire RAM. Nous appellerons ce registre le '''pointeur de table'''. Le pointeur de table est combiné avec l'indice de segment pour adresser le descripteur de segment adéquat.
[[File:Segment 2.svg|centre|vignette|upright=2|Traduction d'adresse avec une table des segments, ici appelée table globale des de"scripteurs (terminologie des processeurs Intel x86).]]
Un point important est que la table des segments n'est pas accessible pour le programme en cours d'exécution. Il ne peut pas lire le contenu de la table des segments, et encore moins la modifier. L'accès se fait seulement de manière indirecte, en faisant usage des indices de segments, mais c'est un adressage indirect. Seul le système d'exploitation peut lire ou écrire la table des segments directement.
Plus haut, j'ai dit que tout accès mémoire impliquait deux accès mémoire : un pour charger le descripteur de segment, un autre pour la lecture/écriture proprement dite. Cependant, cela aurait un impact bien trop grand sur les performances. Dans les faits, les processeurs avec segmentations intégraient un '''cache de descripteurs de segments''', pour limiter la casse. Quand un descripteur de segment est lu depuis la RAM, il est copié dans ce cache. Les accès ultérieurs accédent au descripteur dans le cache, pas besoin de passer par la RAM. L'intel 386 avait un cache de ce type.
===La protection mémoire : les accès hors-segments===
Comme avec la relocation matérielle, le processeur détecte les débordements de segment. Pour cela, il compare l'adresse logique accédée avec l'adresse limite, ou compare la taille limite avec le décalage. De nombreux processeurs, comme l'Intel 386, préféraient utiliser la taille du segment, pour une question d'optimisation. En effet, si on compare l'adresse finale avec l'adresse limite, on doit faire la relocation avant de comparer l'adresse relocatée. Mais en utilisant la taille, ce n'est pas le cas : on peut faire la comparaison avant, pendant ou après la relocation.
Un détail à prendre en compte est la taille de la donnée accédée. Sans cela, la comparaison serait très simple : on vérifie si ''décalage <= taille du segment'', ou on compare des adresses de la même manière. Mais imaginez qu'on accède à une donnée de 4 octets : il se peut que l'adresse de ces 4 octets rentre dans le segment, mais que quelques octets débordent. Par exemple, les deux premiers octets sont dans le segment, mais pas les deux suivants. La vraie comparaison est alors : ''décalage + 4 octets <= taille du segment''.
Mais il est possible de faire le calcul autrement, et quelques processeurs comme l'Intel 386 ne s'en sont pas privé. Il calculait la différence ''taille du segment - décalage'', et vérifiait le résultat. Le processeur gérait des données de 1, 2 et 4 octets, ce qui fait que le résultat devait être entre 0 et 3. Le processeur prenait le résultat de la soustraction, et vérifiait alors que les 30 bits de poids fort valaient bien 0. Il vérifiait aussi que les deux bits de poids faible avaient la bonne valeur.
[[File:Vm7.svg|centre|vignette|upright=2|Traduction d'adresse avec vérification des accès hors-segment.]]
Une nouveauté fait son apparition avec la segmentation : la '''gestion des droits d'accès'''. Par exemple, il est possible d'interdire d'exécuter le contenu d'un segment, ce qui fournit une protection contre certaines failles de sécurité ou certains virus. Lorsqu'on exécute une opération interdite, le processeur lève une exception matérielle, à charge du système d'exploitation de gérer la situation.
Pour cela, chaque segment se voit attribuer un certain nombre d'autorisations d'accès qui indiquent si l'on peut lire ou écrire dedans, si celui-ci contient un programme exécutable, etc. Les autorisations pour chaque segment sont placées dans le descripteur de segment. Elles se résument généralement à quelques bits, qui indiquent si le segment est accesible en lecture/écriture ou exécutable. Le tout est souvent concaténé dans un ou deux '''octets de droits d'accès'''.
L'implémentation de la protection mémoire dépend du CPU considéré. Les CPU microcodés peuvent en théorie utiliser le microcode. Lorsqu'une instruction mémoire s'exécute, le microcode effectue trois étapes : lire le descripteur de segment, faire les tests de protection mémoire, exécuter la lecture/écriture ou lever une exception. Létape de test est réalisée avec un ou plusieurs micro-branchements. Par exemple, une écriture va tester le bit R/W du descripteur, qui indique si on peut écrire dans le segment, en utilisant un micro-branchement. Le micro-branchement enverra vers une routine du microcode en cas d'erreur.
Les tests de protection mémoire demandent cependant de tester beaucoup de conditions différentes. Par exemple, le CPU Intel 386 testait moins d'une dizaine de conditions pour certaines instructions. Il est cependant possible de faire plusieurs comparaisons en parallèle en rusant un peu. Il suffit de mémoriser les octets de droits d'accès dans un registre interne, de masquer les bits non-pertinents, et de faire une comparaison avec une constante adéquate, qui encode la valeur que doivent avoir ces bits.
Une solution alternative utiliser un circuit combinatoire pour faire les tests de protection mémoire. Les tests sont alors faits en parallèles, plutôt qu'un par un par des micro-branchements. Par contre, le cout en matériel est assez important. Il faut ajouter ce circuit combinatoire, ce qui demande pas mal de circuits.
===La mémoire virtuelle avec la segmentation===
La mémoire virtuelle est une fonctionnalité souvent implémentée sur les processeurs qui gèrent la segmentation, alors que les processeurs avec relocation matérielle s'en passaient. Il faut dire que l'implémentation de la mémoire virtuelle est beaucoup plus simple avec la segmentation, comparé à la relocation matérielle. Le remplacement des registres de base par des sélecteurs de segment facilite grandement l'implémentation.
Le problème de la mémoire virtuelle est que les segments peuvent être swappés sur le disque dur n'importe quand, sans que le programme soit prévu. Le swapping est réalisé par une interruption de l'OS, qui peut interrompre le programme n'importe quand. Et si un segment est swappé, le registre de base correspondant devient invalide, il point sur une adresse en RAM où le segment était, mais n'est plus. De plus, les segments peuvent être déplacés en mémoire, là encore n'importe quand et d'une manière invisible par le programme, ce qui fait que les registres de base adéquats doivent être modifiés.
Si le programme entier est swappé d'un coup, comme avec la relocation matérielle simple, cela ne pose pas de problèmes. Mais dès qu'on utilise plusieurs registres de base par programme, les choses deviennent soudainement plus compliquées. Le problème est qu'il n'y a pas de mécanismes pour choisir et invalider le registre de base adéquat quand un segment est déplacé/swappé. En théorie, on pourrait imaginer des systèmes qui résolvent le problème au niveau de l'OS, mais tous ont des problèmes qui font que l'implémentation est compliquée ou que les performances sont ridicules.
L'usage d'une table des segments accédée à chaque accès résout complètement le problème. La table des segments est accédée à chaque accès mémoire, elle sait si le segment est swappé ou non, chaque accès vérifie si le segment est en mémoire et quelle est son adresse de base. On peut changer le segment de place n'importe quand, le prochain accès récupérera des informations à jour dans la table des segments.
L'implémentation de la mémoire virtuelle avec la segmentation est simple : il suffit d'ajouter un bit dans les descripteurs de segments, qui indique si le segment est swappé ou non. Tout le reste, la gestion de ce bit, du swap, et tout ce qui est nécessaire, est délégué au système d'exploitation. Lors de chaque accès mémoire, le processeur vérifie ce bit avant de faire la traduction d'adresse, et déclenche une exception matérielle si le bit indique que le segment est swappé. L'exception matérielle est gérée par l'OS.
===Le partage de segments===
Il est possible de partager un segment entre plusieurs applications. Cela peut servir pour partager des données entre deux programmes : un segment de données partagées est alors partagé entre deux programmes. Partager un segment de code est utile pour les bibliothèques partagées : la bibliothèque est placée dans un segment dédié, qui est partagé entre les programmes qui l'utilisent. Partager un segment de code est aussi utile quand plusieurs instances d'une même application sont lancés simultanément : le code n'ayant pas de raison de changer, celui-ci est partagé entre toutes les instances. Mais ce n'est là qu'un exemple.
La première solution pour cela est de configurer les tables de segment convenablement. Le même segment peut avoir des droits d'accès différents selon les processus. Les adresses de base/limite sont identiques, mais les tables des segments ont alors des droits d'accès différents. Mais cette méthode de partage des segments a plusieurs défauts.
Premièrement, les sélecteurs de segments ne sont pas les mêmes d'un processus à l'autre, pour un même segment. Le segment partagé peut correspondre au segment numéro 80 dans le premier processus, au segment numéro 1092 dans le second processus. Rien n'impose que les sélecteurs de segment soient les mêmes d'un processus à l'autre, pour un segment identique.
Deuxièmement, les adresses limite et de base sont dupliquées dans plusieurs tables de segments. En soi, cette redondance est un souci mineur. Mais une autre conséquence est une question de sécurité : que se passe-t-il si jamais un processus a une table des segments corrompue ? Il se peut que pour un segment identique, deux processus n'aient pas la même adresse limite, ce qui peut causer des failles de sécurité. Un processus peut alors subir un débordement de tampon, ou tout autre forme d'attaque.
[[File:Vm9.png|centre|vignette|upright=2|Illustration du partage d'un segment entre deux applications.]]
Une seconde solution, complémentaire, utilise une table de segment globale, qui mémorise des segments partagés ou accessibles par tous les processus. Les défauts de la méthode précédente disparaissent avec cette technique : un segment est identifié par un sélecteur unique pour tous les processus, il n'y a pas de duplication des descripteurs de segment. Par contre, elle a plusieurs défauts.
Le défaut principal est que cette table des segments est accessible par tous les processus, impossible de ne partager ses segments qu'avec certains pas avec les autres. Un autre défaut est que les droits d'accès à un segment partagé sont identiques pour tous les processus. Impossible d'avoir un segment partagé accessible en lecture seule pour un processus, mais accessible en écriture pour un autre. Il est possible de corriger ces défauts, mais nous en parlerons dans la section sur les architectures à capacité.
===L'extension d'adresse avec la segmentation===
L'extension d'adresse est possible avec la segmentation, de la même manière qu'avec la relocation matérielle. Il suffit juste que les adresses de base soient aussi grandes que le bus d'adresse. Mais il y a une différence avec la relocation matérielle : un même programme peut utiliser plus de mémoire qu'il n'y en a dans l'espace d'adressage. La raison est simple : un segment peut prendre tout l'espace d'adressage, et il y a plusieurs segments par programme.
Pour donner un exemple, prenons un processeur 16 bits, qui peut adresser 64 kibioctets, associé à une mémoire de 4 mébioctets. Il est possible de placer le code machine dans les premiers 64k de la mémoire, la pile du programme dans les 64k suivants, le tas dans les 64k encore après, et ainsi de suite. Le programme dépasse donc les 64k de mémoire de l'espace d'adressage. Ce genre de chose est impossible avec la relocation, où un programme est limité par l'espace d'adressage.
===Le mode protégé des processeurs x86===
L'Intel 80286, aussi appelé 286, ajouta un mode de segmentation séparé du mode réel, qui ajoute une protection mémoire à la segmentation, ce qui lui vaut le nom de '''mode protégé'''. Dans ce mode, les registres de segment ne contiennent pas des adresses de base, mais des sélecteurs de segments qui sont utilisés pour l'accès à la table des segments en mémoire RAM.
Le 286 bootait en mode réel, puis le système d'exploitation devait faire quelques manipulations pour passer en mode protégé. Le 286 était pensé pour être rétrocompatible au maximum avec le 80186. Mais les différences entre le 286 et le 8086 étaient majeures, au point que les applications devaient être réécrites intégralement pour profiter du mode protégé. Un mode de compatibilité permettait cependant aux applications destinées au 8086 de fonctionner, avec même de meilleures performances. Aussi, le mode protégé resta inutilisé sur la plupart des applications exécutées sur le 286.
Vint ensuite le processeur 80386, renommé en 386 quelques années plus tard. Sur ce processeur, les modes réel et protégé sont conservés tel quel, à une différence près : toutes les adresses passent à 32 bits, qu'il s'agisse des adresses de base, limite ou des ''offsets''. Le processeur peut donc adresser un grand nombre de segments : 2^32, soit plus de 4 milliards. Les segments grandissent aussi et passent de 64 KB maximum à 4 gibioctets maximum. Mais surtout : le 386 ajouta le support de la pagination en plus de la segmentation. Ces modifications ont été conservées sur les processeurs 32 bits ultérieurs.
Les processeurs x86 gèrent deux types de tables des segments : une table locale pour chaque processus, et une table globale partagée entre tous les processus. Il ne peut y avoir qu'une table locale d'active, vu que le processeur ne peut exécuter qu'un seul processus en même temps. Chaque table locale définit 8192 segments, pareil pour la table globale. La table globale est utilisée pour les segments du noyau et la mémoire partagée entre processus. Un défaut est qu'un segment partagé par la table globale est visible par tous les processus, avec les mêmes droits d'accès. Ce qui fait que cette méthode était peu utilisée en pratique. La table globale mémorise aussi des pointeurs vers les tables locales, avec un descripteur de segment par table locale.
Sur les processeurs x86 32 bits, un descripteur de segment est organisé comme suit, pour les architectures 32 bits. On y trouve l'adresse de base et la taille limite, ainsi que de nombreux bits de contrôle.
Le premier groupe de bits de contrôle est l'octet en bleu à droite. Il contient :
* le bit P qui indique que l'entrée contient un descripteur valide, qu'elle n'est pas vide ;
* deux bits DPL qui indiquent le niveau de privilège du segment (noyau, utilisateur, les deux intermédiaires spécifiques au x86) ;
* un bit S qui précise si le segment est de type système (utiles pour l'OS) ou un segment de code/données.
* un champ Type qui contient les bits suivants :
** un bit E qui indique si le segment contient du code exécutable ou non ;
** le bit RW qui indique s'il est en lecture seule ou non ;;
** Un bit A qui indique que le segment a récemment été accédé, information utile pour l'OS;
** un bit DC assez spécifiques.
En haut à gauche, en bleu, on trouve deux bits :
* Le bit G indique comment interpréter la taille contenue dans le descripteur : 0 si la taille est exprimée en octets, 1 si la taille est un nombre de pages de 4 kibioctets. Ce bit précise si on utilise la segmentation seule, ou combinée avec la pagination.
* Le bit DB précise si l'on utilise des segments en mode de compatibilité 16 bits ou des segments 32 bits.
[[File:SegmentDescriptor.svg|centre|vignette|upright=3|Segment Descriptor]]
Les indices de segment sont appelés des sélecteurs de segment. Ils ont une taille de 16 bits, mais 3 bits sont utilisés pour encoder des méta-données. Le numéro de segment est donc codé sur 13 bits, ce qui permettait de gérer maximum 8192 segments par table de segment (locale ou globale). Les 16 bits sont organisés comme suit :
* 13 bits pour le numéro du segment dans la table des segments, l'indice de segment proprement dit ;
* un bit qui précise s'il faut accéder à la table des segments globale ou locale ;
* deux bits qui indiquent le niveau de privilège de l'accès au segment (les 4 niveaux de protection, dont l'espace noyau et utilisateur).
[[File:SegmentSelector.svg|centre|vignette|upright=1.5|Sélecteur de segment 16 bit.]]
En tout, l'indice permet de gérer 8192 segments pour la table locale et 8192 segments de la table globale.
====La MMU du 386/486 : cache de segment, protection mémoire====
La MMU du 386 et celle du 486 étaient assez similaires. Elles étaient plus complexes que celle du 186 et du 286. Elle contenait un additionneur pour les calculs d'adresse et un comparateur pour tester si l'accès mémoire déborde d'un segment. Le test de débordement se faisait en parallèle du calcul de l'adresse finale, comme sur le 286.
L'additionneur était un additionneur trois-opérandes, qui additionnait l'adresse à lire/écrire, l'adresse de base du segment, et un décalage intégré dans l'instruction. En clair, l'unité de segmentation n'était pas qu'une MMU, elle prenait en charge une partie du calcul d'adresse. L'avantage est que cela permettait de gérer les modes d'adressage "base + décalage" et "base + indice + décalage" très facilement, en utilisant un minimum de circuits. Une conséquence de cette organisation était que l'usage d'un décalage était gratuit. Par contre, dès qu'on utilisait l'adressage "Base + Indice", avec ou sans décalage, l'instruction prenait un cycle de plus à s'exécuter, parce qu'il fallait faire l'addition "Base + Indice" dans l'ALU entière.
Le CPU 386 était le premier à implémenter la protection mémoire avec des segments. Pour cela, il intégrait une '''''Protection Test Unit''''', séparée du microcode, qu'on va abrévier en PTU. Précisément, il s'agissait d'un PLA (''Programmable Logic Array''), une sorte d'intermédiaire entre circuit logique fait sur mesure et mémoire ROM, qu'on a déjà abordé dans le chapitre sur les mémoires ROM. Mais cette unité ne faisait pas tout, le microcode était aussi impliqué. La PTU sera détaillée dans la section suivante.
Pour améliorer les performances, le 386 et le 486 intégraient un '''cache de descripteurs de segment''', aussi appelé le cache de descripteurs. Lorsqu'un descripteur état chargé pour la première fois, il était copié dans le cache de descripteurs de segment. Les accès mémoire ultérieur lisaient le descripteur de segment depuis ce cache, pas depuis la table des segments en RAM. Le cache de descripteurs gère aussi bien les segments en mode réel qu'en mode protégé. Récupérer l'adresse de base depuis cache se fait un peu différemment en mode réel et protégé, mais le cache gère cela tout seul. Idem pour récupérer la taille/adresse limite.
[[File:Microarchitecture du 386, avec focus sur la segmentation.png|centre|vignette|upright=2|Microarchitecture du 386 et du 486, avec focus sur la segmentation.]]
En mode réel, la taille des segment est censée être limitée à 64 kibioctets. Mais le processeur ne vérifiait pas si cette limite était dépassée. À la place, il utilisait la limite précisée dans le cache de segments. Pire que ça : le cache de segment n'était pas réinitialisé quand on passe du mode réel au mode protégé, et réciproquement. Et cette propriété a été à l'origine de l''''''unreal mode'''''. Il s'agissait d'un mode réel amélioré, capable d'utiliser des segments de 4 gibioctets et des adresses de 32 bits.
Passer en mode ''unreal'' pouvait se faire de deux manières. Il était possible d'altérer le cache de descripteur en utilisant l'instruction non-documentée LOADALL. Elle permettait de charger les descripteurs de segments dans le cache de descripteurs, avec une taille arbitraire. Une autre solution, beaucoup plus complexe sur le 386, demandait d'entrer en mode protégé pour configurer des segments de grande taille, de charger leurs descripteurs dans le cache de descripteur, puis de revenir en mode réel. En mode réel, les descripteurs dans le cache étaient encore disponibles et on pouvait les lire dans le cache.
====L'implémentation de la protection mémoire sur le 386====
La protection mémoire teste la valeur des bits P, S, X, E, R/W. Elle teste aussi les niveaux de privilège, avec deux bits DPL et CPL. En tout, le processeur pouvait tester 148 conditions différentes en parallèle dans la PTU. Cependant, les niveaux de privilèges étaient pré-traités par le microcode. Le microcode vérifiait aussi s'il y avait une erreur en terme d’anneau mémoire, avec par "exemple un segment en mode noyau accédé alors que le CPU est en espace utilisateur. Il fournissait alors un résultat sur deux bits, qui indiquait s'il y avait une erreur ou non, que la PTU utilisait.
Mais toutes les conditions n'étaient pas pertinentes à un instant t. Par exemple, il est pertinent de vérifier si le bit R/W était cohérent si l'instruction à exécuter est une écriture. Mais il n'y a pas besoin de tester le bit E qui indique qu'un segment est exécutable ou non, pour une lecture. En tout, le processeur pouvait se retrouver dans 33 situations possibles, chacune demandant de tester un sous-ensemble des 148 conditions. Pour préciser quel sous-ensembles tester, la PTU recevait un code opération, généré par le microcode.
Pour faire les tests de protection mémoire, le microcode avait une micro-opération nommée ''protection test operation'', qui envoyait les droits d'accès à la PTU. Lors de l'exécution d'une ''protection test operation'', le PLA recevait un descripteur de segment, lu depuis la mémoire RAM, ainsi qu'un code opération provenant du microcode.
{|class="wikitable"
|+ Entrée de la ''Protection Test Unit''
|-
! 15 - 14 !! 13 - 12 !! 11 !! 10 !! 9 !! 8 !! 7 !! 6 !! 5-0
|-
| P1 , P2 || || P || S || X || E || R/W || A || Code opération
|-
| Niveaux de privilèges cohérents/erreur || || Segment présent en mémoire ou swappé || S || X || Segment exécutable ou non || Segment accesible en lecture/écriture || Segment récemment accédé || Code opération
|}
Il fournissait en sortie un bit qui indiquait si une erreur de protection mémoire avait eu lieu ou non. Il fournissait aussi une adresse de 12 bits, utilisée seulement en cas d'erruer. Elle pointait dans le microcode, sur un code levant une exception en cas d'erreur. Enfin, la PTU fournissait 4 bits pouvant être testés par un branchement dans le microcode. L'un d'entre eux demandait de tester s'il y a un accès hors-limite, les autres étaient assez peu reliés à la protection mémoire.
Un détail est que le chargement du descripteur de segment est réalisé par une fonction dans le microcode. Elle est appliquée pour toutes les instructions ou situations qui demandent de faire un accès mémoire. Et les tests de protection mémoire sont réalisés dans cette fonction, pas après elle. Vu qu'il s'agit d'une fonction exécutée quelque soit l'instruction, le microcode doit transférer le code opération à cette fonction. Le microcode est pour cela associé à un registre interne, dans lequel le code opération est mémorisé, avant d'appeler la fonction. Le microcode a une micro-opération PTSAV (''Protection Save'') pour mémoriser le code opération dans ce registre. Dans la fonction qui charge le descripteur, une micro-opération PTOVRR (''Protection Override'') lit le code opération dans ce registre, et lance les tests nécessaires.
Il faut noter que le PLA était certes plus rapide que de tester les conditions une par une, mais il était assez lent. La PTU mettait environ 3 cycles d'horloges pour rendre son résultat. Le microcode en profitait alors pour exécuter des micro-opérations durant ces 3 cycles d'attente. Par exemple, le microcode pouvait en profiter pour lire l'adresse de base dans le descripteur, si elle n'a pas été chargée avant (les descripteur était chargé en deux fois). Il fallait cependant que les trois micro-opérations soient valides, peu importe qu'il y ait une erreur de protection mémoire ou non. Ou du moins, elles produisaient un résultat qui n'est pas utilisé en cas d'erreur. Si ce n'était pas possible, le microcode ajoutait des NOP pendant ce temps d'attente de 3 cycles.
Le bit A du descripteur de segment indique que le segment a récemment été accédé. Il est mis à jour après les tests de protection mémoire, quand ceux-ci indiquent que l'accès mémoire est autorisé. Le bit A est mis à 1 si la PTU l'autorise. Pour cela, la PTU utilise un des 4 bits de sortie mentionnés plus haut : l'un d'entre eux indique que le bit A doit être mis à 1. La mise à jour est ensuite réalisée par le microcode, qui utilise trois micro-opérations pour le mettre à jour.
====Le ''Hardware task switching'' des CPU x86====
Les systèmes d’exploitation modernes peuvent lancer plusieurs logiciels en même temps. Les logiciels sont alors exécutés à tour de rôle. Passer d'un programme à un autre est ce qui s'appelle une commutation de contexte. Lors d'une commutation de contexte, l'état du processeur est sauvegardé, afin que le programme stoppé puisse reprendre là où il était. Il arrivera un moment où le programme stoppé redémarrera et il doit reprendre dans l'état exact où il s'est arrêté. Deuxièmement, le programme à qui c'est le tour restaure son état. Cela lui permet de revenir là où il était avant d'être stoppé. Il y a donc une sauvegarde et une restauration des registres.
Divers processeurs incorporent des optimisations matérielles pour rendre la commutation de contexte plus rapide. Ils peuvent sauvegarder et restaurer les registres du processeur automatiquement lors d'une interruption de commutation de contexte. Les registres sont sauvegardés dans des structures de données en mémoire RAM, appelées des '''contextes matériels'''. Sur les processeurs x86, il s'agit de la technique d{{'}}''Hardware Task Switching''. Fait intéressant, le ''Hardware Task Switching'' se base beaucoup sur les segments mémoires.
Avec ''Hardware Task Switching'', chaque contexte matériel est mémorisé dans son propre segment mémoire, séparé des autres. Les segments pour les contextes matériels sont appelés des '''''Task State Segment''''' (TSS). Un TSS mémorise tous les registres généraux, le registre d'état, les pointeurs de pile, le ''program counter'' et quelques registres de contrôle du processeur. Par contre, les registres flottants ne sont pas sauvegardés, de même que certaines registres dit SIMD que nous n'avons pas encore abordé. Et c'est un défaut qui fait que le ''Hardware Task Switching'' n'est plus utilisé.
Le programme en cours d'exécution connait l'adresse du TSS qui lui est attribué, car elle est mémorisée dans un registre appelé le '''''Task Register'''''. En plus de pointer sur le TSS, ce registre contient aussi les adresses de base et limite du segment en cours. Pour être plus précis, le ''Task Register'' ne mémorise pas vraiment l'adresse du TSS. À la place, elle mémorise le numéro du segment, le numéro du TSS. Le numéro est codé sur 16 bits, ce qui explique que 65 536 segments sont adressables. Les instructions LDR et STR permettent de lire/écrire ce numéro de segment dans le ''Task Register''.
Le démarrage d'un programme a lieu automatiquement dans plusieurs circonstances. La première est une instruction de branchement CALL ou JMP adéquate. Le branchement fournit non pas une adresse à laquelle brancher, mais un numéro de segment qui pointe vers un TSS. Cela permet à une routine du système d'exploitation de restaurer les registres et de démarrer le programme en une seule instruction de branchement. Une seconde circonstance est une interruption matérielle ou une exception, mais nous la mettons de côté. Le ''Task Register'' est alors initialisé avec le numéro de segment fournit. S'en suit la procédure suivante :
* Le ''Task Register'' est utilisé pour adresser la table des segments, pour récupérer un pointeur vers le TSS associé.
* Le pointeur est utilisé pour une seconde lecture, qui adresse le TSS directement. Celle-ci restaure les registres du processeur.
En clair, on va lire le ''TSS descriptor'' dans la GDT, puis on l'utilise pour restaurer les registres du processeur.
[[File:Hardware Task Switching x86.png|centre|vignette|upright=2|Hardware Task Switching x86]]
===La segmentation sur les processeurs Burrough B5000 et plus===
Le Burrough B5000 est un très vieil ordinateur, commercialisé à partir de l'année 1961. Ses successeurs reprennent globalement la même architecture. C'était une machine à pile, doublé d'une architecture taguée, choses très rare de nos jours. Mais ce qui va nous intéresser dans ce chapitre est que ce processeur incorporait la segmentation, avec cependant une différence de taille : un programme avait accès à un grand nombre de segments. La limite était de 1024 segments par programme ! Il va de soi que des segments plus petits favorise l'implémentation de la mémoire virtuelle, mais complexifie la relocation et le reste, comme nous allons le voir.
Le processeur gère deux types de segments : les segments de données et de procédure/fonction. Les premiers mémorisent un bloc de données, dont le contenu est laissé à l'appréciation du programmeur. Les seconds sont des segments qui contiennent chacun une procédure, une fonction. L'usage des segments est donc différent de ce qu'on a sur les processeurs x86, qui n'avaient qu'un segment unique pour l'intégralité du code machine. Un seul segment de code machine x86 est découpé en un grand nombre de segments de code sur les processeurs Burrough.
La table des segments contenait 1024 entrées de 48 bits chacune. Fait intéressant, chaque entrée de la table des segments pouvait mémoriser non seulement un descripteur de segment, mais aussi une valeur flottante ou d'autres types de données ! Parler de table des segments est donc quelque peu trompeur, car cette table ne gère pas que des segments, mais aussi des données. La documentation appelaiat cette table la '''''Program Reference Table''''', ou PRT.
La raison de ce choix quelque peu bizarre est que les instructions ne gèrent pas d'adresses proprement dit. Tous les accès mémoire à des données en-dehors de la pile passent par la segmentation, ils précisent tous un indice de segment et un ''offset''. Pour éviter d'allouer un segment pour chaque donnée, les concepteurs du processeur ont décidé qu'une entrée pouvait contenir directement la donnée entière à lire/écrire.
La PRT supporte trois types de segments/descripteurs : les descripteurs de données, les descripteurs de programme et les descripteurs d'entrées-sorties. Les premiers décrivent des segments de données. Les seconds sont associés aux segments de procédure/fonction et sont utilisés pour les appels de fonction (qui passent, eux aussi, par la segmentation). Le dernier type de descripteurs sert pour les appels systèmes et les communications avec l'OS ou les périphériques.
Chaque entrée de la PRT contient un ''tag'', une suite de bit qui indique le type de l'entrée : est-ce qu'elle contient un descripteur de segment, une donnée, autre. Les descripteurs contiennent aussi un ''bit de présence'' qui indique si le segment a été swappé ou non. Car oui, les segments pouvaient être swappés sur ce processeur, ce qui n'est pas étonnant vu que les segments sont plus petits sur cette architecture. Le descripteur contient aussi l'adresse de base du segment ainsi que sa taille, et diverses informations pour le retrouver sur le disque dur s'il est swappé.
: L'adresse mémorisée ne faisait que 15 bits, ce qui permettait d'adresse 32 kibi-mots, soit 192 kibioctets de mémoire. Diverses techniques d'extension d'adressage étaient disponibles pour contourner cette limitation. Outre l'usage de l{{'}}''overlay'', le processeur et l'OS géraient aussi des identifiants d'espace d'adressage et en fournissaient plusieurs par processus. Les processeurs Borrough suivants utilisaient des adresses plus grandes, de 20 bits, ce qui tempérait le problème.
[[File:B6700Word.jpg|centre|vignette|upright=2|Structure d'un mot mémoire sur le B6700.]]
==Les architectures à capacités==
Les architectures à capacité utilisent la segmentation à granularité fine, mais ajoutent des mécanismes de protection mémoire assez particuliers, qui font que les architectures à capacité se démarquent du reste. Les architectures de ce type sont très rares et sont des processeurs assez anciens. Le premier d'entre eux était le Plessey System 250, qui date de 1969. Il fu suivi par le CAP computer, vendu entre les années 70 et 77. En 1978, le System/38 d'IBM a eu un petit succès commercial. En 1980, la Flex machine a aussi été vendue, mais à très peu d'examplaires, comme les autres architectures à capacité. Et enfin, en 1981, l'architecture à capacité la plus connue, l'Intel iAPX 432 a été commercialisée. Depuis, la seule architecture de ce type est en cours de développement. Il s'agit de l'architecture CHERI, dont la mise en projet date de 2014.
===Le partage de la mémoire sur les architectures à capacités===
Le partage de segment est grandement modifié sur les architectures à capacité. Avec la segmentation normale, il y a une table de segment par processus. Les conséquences sont assez nombreuses, mais la principale est que partager un segment entre plusieurs processus est compliqué. Les défauts ont été évoqués plus haut. Les sélecteurs de segments ne sont pas les mêmes d'un processus à l'autre, pour un même segment. De plus, les adresses limite et de base sont dupliquées dans plusieurs tables de segments, et cela peut causer des problèmes de sécurité si une table des segments est modifiée et pas l'autre. Et il y a d'autres problèmes, tout aussi importants.
[[File:Partage des segments avec la segmentation.png|centre|vignette|upright=1.5|Partage des segments avec la segmentation]]
À l'opposé, les architectures à capacité utilisent une table des segments unique pour tous les processus. La table des segments unique sera appelée dans de ce qui suit la '''table des segments globale''', ou encore la table globale. En conséquence, les adresses de base et limite ne sont présentes qu'en un seul exemplaire par segment, au lieu d'être dupliquées dans autant de processus que nécessaire. De plus, cela garantit que l'indice de segment est le même quel que soit le processus qui l'utilise.
Un défaut de cette approche est au niveau des droits d'accès. Avec la segmentation normale, les droits d'accès pour un segment sont censés changer d'un processus à l'autre. Par exemple, tel processus a accès en lecture seule au segment, l'autre seulement en écriture, etc. Mais ici, avec une table des segments uniques, cela ne marche plus : incorporer les droits d'accès dans la table des segments ferait que tous les processus auraient les mêmes droits d'accès au segment. Et il faut trouver une solution.
===Les capacités sont des pointeurs protégés===
Pour éviter cela, les droits d'accès sont combinés avec les sélecteurs de segments. Les sélecteurs des segments sont remplacés par des '''capacités''', des pointeurs particuliers formés en concaténant l'indice de segment avec les droits d'accès à ce segment. Si un programme veut accéder à une adresse, il fournit une capacité de la forme "sélecteur:droits d'accès", et un décalage qui indique la position de l'adresse dans le segment.
Il est impossible d'accéder à un segment sans avoir la capacité associée, c'est là une sécurité importante. Un accès mémoire demande que l'on ait la capacité pour sélectionner le bon segment, mais aussi que les droits d'accès en permettent l'accès demandé. Par contre, les capacités peuvent être passées d'un programme à un autre sans problème, les deux programmes pourront accéder à un segment tant qu'ils disposent de la capacité associée.
[[File:Comparaison entre capacités et adresses segmentées.png|centre|vignette|upright=2.5|Comparaison entre capacités et adresses segmentées]]
Mais cette solution a deux problèmes très liés. Au niveau des sélecteurs de segment, le problème est que les sélecteur ont une portée globale. Avant, l'indice de segment était interne à un programme, un sélecteur ne permettait pas d'accéder au segment d'un autre programme. Sur les architectures à capacité, les sélecteurs ont une portée globale. Si un programme arrive à forger un sélecteur qui pointe vers un segment d'un autre programme, il peut théoriquement y accéder, à condition que les droits d'accès le permettent. Et c'est là qu'intervient le second problème : les droits d'accès ne sont plus protégés par l'espace noyau. Les droits d'accès étaient dans la table de segment, accessible uniquement en espace noyau, ce qui empêchait un processus de les modifier. Avec une capacité, il faut ajouter des mécanismes de protection qui empêchent un programme de modifier les droits d'accès à un segment et de générer un indice de segment non-prévu.
La première sécurité est qu'un programme ne peut pas créer une capacité, seul le système d'exploitation le peut. Les capacités sont forgées lors de l'allocation mémoire, ce qui est du ressort de l'OS. Pour rappel, un programme qui veut du rab de mémoire RAM peut demander au système d'exploitation de lui allouer de la mémoire supplémentaire. Le système d'exploitation renvoie alors un pointeurs qui pointe vers un nouveau segment. Le pointeur est une capacité. Il doit être impossible de forger une capacité, en-dehors d'une demande d'allocation mémoire effectuée par l'OS. Typiquement, la forge d'une capacité se fait avec des instructions du processeur, que seul l'OS peut éxecuter (pensez à une instruction qui n'est accessible qu'en espace noyau).
La seconde protection est que les capacités ne peuvent pas être modifiées sans raison valable, que ce soit pour l'indice de segment ou les droits d'accès. L'indice de segment ne peut pas être modifié, quelqu'en soit la raison. Pour les droits d'accès, la situation est plus compliquée. Il est possible de modifier ses droits d'accès, mais sous conditions. Réduire les droits d'accès d'une capacité est possible, que ce soit en espace noyau ou utilisateur, pas l'OS ou un programme utilisateur, avec une instruction dédiée. Mais augmenter les droits d'accès, seul l'OS peut le faire avec une instruction précise, souvent exécutable seulement en espace noyau.
Les capacités peuvent être copiées, et même transférées d'un processus à un autre. Les capacités peuvent être détruites, ce qui permet de libérer la mémoire utilisée par un segment. La copie d'une capacité est contrôlée par l'OS et ne peut se faire que sous conditions. La destruction d'une capacité est par contre possible par tous les processus. La destruction ne signifie pas que le segment est effacé, il est possible que d'autres processus utilisent encore des copies de la capacité, et donc le segment associé. On verra quand la mémoire est libérée plus bas.
Protéger les capacités demande plusieurs conditions. Premièrement, le processeur doit faire la distinction entre une capacité et une donnée. Deuxièmement, les capacités ne peuvent être modifiées que par des instructions spécifiques, dont l'exécution est protégée, réservée au noyau. En clair, il doit y avoir une séparation matérielle des capacités, qui sont placées dans des registres séparés. Pour cela, deux solutions sont possibles : soit les capacités remplacent les adresses et sont dispersées en mémoire, soit elles sont regroupées dans un segment protégé.
====La liste des capacités====
Avec la première solution, on regroupe les capacités dans un segment protégé. Chaque programme a accès à un certain nombre de segments et à autant de capacités. Les capacités d'un programme sont souvent regroupées dans une '''liste de capacités''', appelée la '''''C-list'''''. Elle est généralement placée en mémoire RAM. Elle est ce qu'il reste de la table des segments du processus, sauf que cette table ne contient pas les adresses du segment, qui sont dans la table globale. Tout se passe comme si la table des segments de chaque processus est donc scindée en deux : la table globale partagée entre tous les processus contient les informations sur les limites des segments, la ''C-list'' mémorise les droits d'accès et les sélecteurs pour identifier chaque segment. C'est un niveau d'indirection supplémentaire par rapport à la segmentation usuelle.
[[File:Architectures à capacité.png|centre|vignette|upright=2|Architectures à capacité]]
La liste de capacité est lisible par le programme, qui peut copier librement les capacités dans les registres. Par contre, la liste des capacités est protégée en écriture. Pour le programme, il est impossible de modifier les capacités dedans, impossible d'en rajouter, d'en forger, d'en retirer. De même, il ne peut pas accéder aux segments des autres programmes : il n'a pas les capacités pour adresser ces segments.
Pour protéger la ''C-list'' en écriture, la solution la plus utilisée consiste à placer la ''C-list'' dans un segment dédié. Le processeur gère donc plusieurs types de segments : les segments de capacité pour les ''C-list'', les autres types segments pour le reste. Un défaut de cette approche est que les adresses/capacités sont séparées des données. Or, les programmeurs mixent souvent adresses et données, notamment quand ils doivent manipuler des structures de données comme des listes chainées, des arbres, des graphes, etc.
L'usage d'une ''C-list'' permet de se passer de la séparation entre espace noyau et utilisateur ! Les segments de capacité sont eux-mêmes adressés par leur propre capacité, avec une capacité par segment de capacité. Le programme a accès à la liste de capacité, comme l'OS, mais leurs droits d'accès ne sont pas les mêmes. Le programme a une capacité vers la ''C-list'' qui n'autorise pas l'écriture, l'OS a une autre capacité qui accepte l'écriture. Les programmes ne pourront pas forger les capacités permettant de modifier les segments de capacité. Une méthode alternative est de ne permettre l'accès aux segments de capacité qu'en espace noyau, mais elle est redondante avec la méthode précédente et moins puissante.
====Les capacités dispersées, les architectures taguées====
Une solution alternative laisse les capacités dispersées en mémoire. Les capacités remplacent les adresses/pointeurs, et elles se trouvent aux mêmes endroits : sur la pile, dans le tas. Comme c'est le cas dans les programmes modernes, chaque allocation mémoire renvoie une capacité, que le programme gére comme il veut. Il peut les mettre dans des structures de données, les placer sur la pile, dans des variables en mémoire, etc. Mais il faut alors distinguer si un mot mémoire contient une capacité ou une autre donnée, les deux ne devant pas être mixés.
Pour cela, chaque mot mémoire se voit attribuer un certain bit qui indique s'il s'agit d'un pointeur/capacité ou d'autre chose. Mais cela demande un support matériel, ce qui fait que le processeur devient ce qu'on appelle une ''architecture à tags'', ou ''tagged architectures''. Ici, elles indiquent si le mot mémoire contient une adresse:capacité ou une donnée.
[[File:Architectures à capacité sans liste de capacité.png|centre|vignette|upright=2|Architectures à capacité sans liste de capacité]]
L'inconvénient est le cout en matériel de cette solution. Il faut ajouter un bit à chaque case mémoire, le processeur doit vérifier les tags avant chaque opération d'accès mémoire, etc. De plus, tous les mots mémoire ont la même taille, ce qui force les capacités à avoir la même taille qu'un entier. Ce qui est compliqué.
===Les registres de capacité===
Les architectures à capacité disposent de registres spécialisés pour les capacités, séparés pour les entiers. La raison principale est une question de sécurité, mais aussi une solution pragmatique au fait que capacités et entiers n'ont pas la même taille. Les registres dédiés aux capacités ne mémorisent pas toujours des capacités proprement dites. À la place, ils mémorisent des descripteurs de segment, qui contiennent l'adresse de base, limite et les droits d'accès. Ils sont utilisés pour la relocation des accès mémoire ultérieurs. Ils sont en réalité identiques aux registres de relocation, voire aux registres de segments. Leur utilité est d'accélérer la relocation, entre autres.
Les processeurs à capacité ne gèrent pas d'adresses proprement dit, comme pour la segmentation avec plusieurs registres de relocation. Les accès mémoire doivent préciser deux choses : à quel segment on veut accéder, à quelle position dans le segment se trouve la donnée accédée. La première information se trouve dans le mal nommé "registre de capacité", la seconde information est fournie par l'instruction d'accès mémoire soit dans un registre (Base+Index), soit en adressage base+''offset''.
Les registres de capacités sont accessibles à travers des instructions spécialisées. Le processeur ajoute des instructions LOAD/STORE pour les échanges entre table des segments et registres de capacité. Ces instructions sont disponibles en espace utilisateur, pas seulement en espace noyau. Lors du chargement d'une capacité dans ces registres, le processeur vérifie que la capacité chargée est valide, et que les droits d'accès sont corrects. Puis, il accède à la table des segments, récupère les adresses de base et limite, et les mémorise dans le registre de capacité. Les droits d'accès et d'autres méta-données sont aussi mémorisées dans le registre de capacité. En somme, l'instruction de chargement prend une capacité et charge un descripteur de segment dans le registre.
Avec ce genre de mécanismes, il devient difficile d’exécuter certains types d'attaques, ce qui est un gage de sureté de fonctionnement indéniable. Du moins, c'est la théorie, car tout repose sur l'intégrité des listes de capacité. Si on peut modifier celles-ci, alors il devient facile de pouvoir accéder à des objets auxquels on n’aurait pas eu droit.
===Le recyclage de mémoire matériel===
Les architectures à capacité séparent les adresses/capacités des nombres entiers. Et cela facilite grandement l'implémentation de la ''garbage collection'', ou '''recyclage de la mémoire''', à savoir un ensemble de techniques logicielles qui visent à libérer la mémoire inutilisée.
Rappelons que les programmes peuvent demander à l'OS un rab de mémoire pour y placer quelque chose, généralement une structure de donnée ou un objet. Mais il arrive un moment où cet objet n'est plus utilisé par le programme. Il peut alors demander à l'OS de libérer la portion de mémoire réservée. Sur les architectures à capacité, cela revient à libérer un segment, devenu inutile. La mémoire utilisée par ce segment est alors considérée comme libre, et peut être utilisée pour autre chose. Mais il arrive que les programmes ne libèrent pas le segment en question. Soit parce que le programmeur a mal codé son programme, soit parce que le compilateur n'a pas fait du bon travail ou pour d'autres raisons.
Pour éviter cela, les langages de programmation actuels incorporent des '''''garbage collectors''''', des morceaux de code qui scannent la mémoire et détectent les segments inutiles. Pour cela, ils doivent identifier les adresses manipulées par le programme. Si une adresse pointe vers un objet, alors celui-ci est accessible, il sera potentiellement utilisé dans le futur. Mais si aucune adresse ne pointe vers l'objet, alors il est inaccessible et ne sera plus jamais utilisé dans le futur. On peut libérer les objets inaccessibles.
Identifier les adresses est cependant très compliqué sur les architectures normales. Sur les processeurs modernes, les ''garbage collectors'' scannent la pile à la recherche des adresses, et considèrent tout mot mémoire comme une adresse potentielle. Mais les architectures à capacité rendent le recyclage de la mémoire très facile. Un segment est accessible si le programme dispose d'une capacité qui pointe vers ce segment, rien de plus. Et les capacités sont facilement identifiables : soit elles sont dans la liste des capacités, soit on peut les identifier à partir de leur ''tag''.
Le recyclage de mémoire était parfois implémenté directement en matériel. En soi, son implémentation est assez simple, et peu être réalisé dans le microcode d'un processeur. Une autre solution consiste à utiliser un second processeur, spécialement dédié au recyclage de mémoire, qui exécute un programme spécialement codé pour. Le programme en question est placé dans une mémoire ROM, reliée directement à ce second processeur.
===L'intel iAPX 432===
Voyons maintenat une architecture à capacité assez connue : l'Intel iAPX 432. Oui, vous avez bien lu : Intel a bel et bien réalisé un processeur orienté objet dans sa jeunesse. La conception du processeur Intel iAPX 432 commença en 1975, afin de créer un successeur digne de ce nom aux processeurs 8008 et 8080.
La conception du processeur Intel iAPX 432 commença en 1975, afin de créer un successeur digne de ce nom aux processeurs 8008 et 8080. Ce processeur s'est très faiblement vendu en raison de ses performances assez désastreuses et de défauts techniques certains. Par exemple, ce processeur était une machine à pile à une époque où celles-ci étaient tombées en désuétude, il ne pouvait pas effectuer directement de calculs avec des constantes entières autres que 0 et 1, ses instructions avaient un alignement bizarre (elles étaient bit-alignées). Il avait été conçu pour maximiser la compatibilité avec le langage ADA, un langage assez peu utilisé, sans compter que le compilateur pour ce processeur était mauvais.
====Les segments prédéfinis de l'Intel iAPX 432====
L'Intel iAPX432 gère plusieurs types de segments. Rien d'étonnant à cela, les Burrough géraient eux aussi plusieurs types de segments, à savoir des segments de programmes, des segments de données, et des segments d'I/O. C'est la même chose sur l'Intel iAPX 432, mais en bien pire !
Les segments de données sont des segments génériques, dans lequels on peut mettre ce qu'on veut, suivant les besoins du programmeur. Ils sont tous découpés en deux parties de tailles égales : une partie contenant les données de l'objet et une partie pour les capacités. Les capacités d'un segment pointent vers d'autres segments, ce qui permet de créer des structures de données assez complexes. La ligne de démarcation peut être placée n'importe où dans le segment, les deux portions ne sont pas de taille identique, elles ont des tailles qui varient de segment en segment. Il est même possible de réserver le segment entier à des données sans y mettre de capacités, ou inversement. Les capacités et données sont adressées à partir de la ligne de démarcation, qui sert d'adresse de base du segment. Suivant l'instruction utilisée, le processeur accède à la bonne portion du segment.
Le processeur supporte aussi d'autres segments pré-définis, qui sont surtout utilisés par le système d'exploitation :
* Des segments d'instructions, qui contiennent du code exécutable, typiquement un programme ou des fonctions, parfois des ''threads''.
* Des segments de processus, qui mémorisent des processus entiers. Ces segments contiennent des capacités qui pointent vers d'autres segments, notamment un ou plusieurs segments de code, et des segments de données.
* Des segments de domaine, pour les modules ou bibliothèques dynamiques.
* Des segments de contexte, utilisés pour mémoriser l'état d'un processus, utilisés par l'OS pour faire de la commutation de contexte.
* Des segments de message, utilisés pour la communication entre processus par l'intermédiaire de messages.
* Et bien d'autres encores.
Sur l'Intel iAPX 432, chaque processus est considéré comme un objet à part entière, qui a son propre segment de processus. De même, l'état du processeur (le programme qu'il est en train d’exécuter, son état, etc.) est stocké en mémoire dans un segment de contexte. Il en est de même pour chaque fonction présente en mémoire : elle était encapsulée dans un segment, sur lequel seules quelques manipulations étaient possibles (l’exécuter, notamment). Et ne parlons pas des appels de fonctions qui stockaient l'état de l'appelé directement dans un objet spécial. Bref, de nombreux objets système sont prédéfinis par le processeur : les objets stockant des fonctions, les objets stockant des processus, etc.
L'Intel 432 possédait dans ses circuits un ''garbage collector'' matériel. Pour faciliter son fonctionnement, certains bits de l'objet permettaient de savoir si l'objet en question pouvait être supprimé ou non.
====Le support de la segmentation sur l'Intel iAPX 432====
La table des segments est une table hiérarchique, à deux niveaux. Le premier niveau est une ''Object Table Directory'', qui réside toujours en mémoire RAM. Elle contient des descripteurs qui pointent vers des tables secondaires, appelées des ''Object Table''. Il y a plusieurs ''Object Table'', typiquement une par processus. Plusieurs processus peuvent partager la même ''Object Table''. Les ''Object Table'' peuvent être swappées, mais pas l{{'}}''Object Table Directory''.
Une capacité tient compte de l'organisation hiérarchique de la table des segments. Elle contient un indice qui précise quelle ''Object Table'' utiliser, et l'indice du segment dans cette ''Object Table''. Le premier indice adresse l{{'}}''Object Table Directory'' et récupère un descripteur de segment qui pointe sur la bonne ''Object Table''. Le second indice est alors utilisé pour lire l'adresse de base adéquate dans cette ''Object Table''. La capacité contient aussi des droits d'accès en lecture, écriture, suppression et copie. Il y a aussi un champ pour le type, qu'on verra plus bas. Au fait : les capacités étaient appelées des ''Access Descriptors'' dans la documentation officielle.
Une capacité fait 32 bits, avec un octet utilisé pour les droits d'accès, laissant 24 bits pour adresser les segments. Le processeur gérait jusqu'à 2^24 segments/objets différents, pouvant mesurer jusqu'à 64 kibioctets chacun, ce qui fait 2^40 adresses différentes, soit 1024 gibioctets. Les 24 bits pour adresser les segments sont partagés moitié-moitié pour l'adressage des tables, ce qui fait 4096 ''Object Table'' différentes dans l{{'}}''Object Table Directory'', et chaque ''Object Table'' contient 4096 segments.
====Le jeu d'instruction de l'Intel iAPX 432====
L'Intel iAPX 432 est une machine à pile. Le jeu d'instruction de l'Intel iAPX 432 gère pas moins de 230 instructions différentes. Il gére deux types d'instructions : les instructions normales, et celles qui manipulent des segments/objets. Les premières permettent de manipuler des nombres entiers, des caractères, des chaînes de caractères, des tableaux, etc.
Les secondes sont spécialement dédiées à la manipulation des capacités. Il y a une instruction pour copier une capacité, une autre pour invalider une capacité, une autre pour augmenter ses droits d'accès (instruction sécurisée, exécutable seulement sous certaines conditions), une autre pour restreindre ses droits d'accès. deux autres instructions créent un segment et renvoient la capacité associée, la première créant un segment typé, l'autre non.
le processeur gérait aussi des instructions spécialement dédiées à la programmation système et idéales pour programmer des systèmes d'exploitation. De nombreuses instructions permettaient ainsi de commuter des processus, faire des transferts de messages entre processus, etc. Environ 40 % du micro-code était ainsi spécialement dédié à ces instructions spéciales.
Les instructions sont de longueur variable et peuvent prendre n'importe quelle taille comprise entre 10 et 300 bits, sans vraiment de restriction de taille. Les bits d'une instruction sont regroupés en 4 grands blocs, 4 champs, qui ont chacun une signification particulière.
* Le premier est l'opcode de l'instruction.
* Le champ référence, doit être interprété différemment suivant la donnée à manipuler. Si cette donnée est un entier, un caractère ou un flottant, ce champ indique l'emplacement de la donnée en mémoire. Alors que si l'instruction manipule un objet, ce champ spécifie la capacité de l'objet en question. Ce champ est assez complexe et il est sacrément bien organisé.
* Le champ format, n'utilise que 4 bits et a pour but de préciser si les données à manipuler sont en mémoire ou sur la pile.
* Le champ classe permet de dire combien de données différentes l'instruction va devoir manipuler, et quelles seront leurs tailles.
[[File:Encodage des instructions de l'Intel iAPX-432.png|centre|vignette|upright=2|Encodage des instructions de l'Intel iAPX-432.]]
====Le support de l'orienté objet sur l'Intel iAPX 432====
L'Intel 432 permet de définir des objets, qui correspondent aux classes des langages orientés objets. L'Intel 432 permet, à partir de fonctions définies par le programmeur, de créer des '''''domain objects''''', qui correspondent à une classe. Un ''domain object'' est un segment de capacité, dont les capacités pointent vers des fonctions ou un/plusieurs objets. Les fonctions et les objets sont chacun placés dans un segment. Une partie des fonctions/objets sont publics, ce qui signifie qu'ils sont accessibles en lecture par l'extérieur. Les autres sont privées, inaccessibles aussi bien en lecture qu'en écriture.
L'exécution d'une fonction demande que le branchement fournisse deux choses : une capacité vers le ''domain object'', et la position de la fonction à exécuter dans le segment. La position permet de localiser la capacité de la fonction à exécuter. En clair, on accède au ''domain object'' d'abord, pour récupérer la capacité qui pointe vers la fonction à exécuter.
Il est aussi possible pour le programmeur de définir de nouveaux types non supportés par le processeur, en faisant appel au système d'exploitation de l'ordinateur. Au niveau du processeur, chaque objet est typé au niveau de son object descriptor : celui-ci contient des informations qui permettent de déterminer le type de l'objet. Chaque type se voit attribuer un domain object qui contient toutes les fonctions capables de manipuler les objets de ce type et que l'on appelle le type manager. Lorsque l'on veut manipuler un objet d'un certain type, il suffit d'accéder à une capacité spéciale (le TCO) qui pointera dans ce type manager et qui précisera quel est l'objet à manipuler (en sélectionnant la bonne entrée dans la liste de capacité). Le type d'un objet prédéfini par le processeur est ainsi spécifié par une suite de 8 bits, tandis que le type d'un objet défini par le programmeur est défini par la capacité spéciale pointant vers son type manager.
===Conclusion===
Pour ceux qui veulent en savoir plus, je conseille la lecture de ce livre, disponible gratuitement sur internet (merci à l'auteur pour cette mise à disposition) :
* [https://homes.cs.washington.edu/~levy/capabook/ Capability-Based Computer Systems].
Voici un document qui décrit le fonctionnement de l'Intel iAPX432 :
* [https://homes.cs.washington.edu/~levy/capabook/Chapter9.pdf The Intel iAPX 432 ]
==La pagination==
Avec la pagination, la mémoire est découpée en blocs de taille fixe, appelés des '''pages mémoires'''. La taille des pages varie suivant le processeur et le système d'exploitation et tourne souvent autour de 4 kibioctets. Mais elles sont de taille fixe : on ne peut pas en changer la taille. C'est la différence avec les segments, qui sont de taille variable. Le contenu d'une page en mémoire fictive est rigoureusement le même que le contenu de la page correspondante en mémoire physique.
L'espace d'adressage est découpé en '''pages logiques''', alors que la mémoire physique est découpée en '''pages physique''' de même taille. Les pages logiques correspondent soit à une page physique, soit à une page swappée sur le disque dur. Quand une page logique est associée à une page physique, les deux ont le même contenu, mais pas les mêmes adresses. Les pages logiques sont numérotées, en partant de 0, afin de pouvoir les identifier/sélectionner. Même chose pour les pages physiques, qui sont elles aussi numérotées en partant de 0.
[[File:Principe de la pagination.png|centre|vignette|upright=2|Principe de la pagination.]]
Pour information, le tout premier processeur avec un système de mémoire virtuelle était le super-ordinateur Atlas. Il utilisait la pagination, et non la segmentation. Mais il fallu du temps avant que la méthode de la pagination prenne son essor dans les processeurs commerciaux x86.
Un point important est que la pagination implique une coopération entre OS et hardware, les deux étant fortement mélés. Une partie des informations de cette section auraient tout autant leur place dans le wikilivre sur les systèmes d'exploitation, mais il est plus simple d'en parler ici.
===La mémoire virtuelle : le ''swapping'' et le remplacement des pages mémoires===
Le système d'exploitation mémorise des informations sur toutes les pages existantes dans une '''table des pages'''. C'est un tableau où chaque ligne est associée à une page logique. Une ligne contient un bit ''Valid'' qui indique si la page logique associée est swappée sur le disque dur ou non, et la position de la page physique correspondante en mémoire RAM. Elle peut aussi contenir des bits pour la protection mémoire, et bien d'autres. Les lignes sont aussi appelées des ''entrées de la table des pages''
[[File:Gestionnaire de mémoire virtuelle - Pagination et swapping.png|centre|vignette|upright=2|Table des pages.]]
De plus, le système d'exploitation conserve une '''liste des pages vides'''. Le nom est assez clair : c'est une liste de toutes les pages de la mémoire physique qui sont inutilisées, qui ne sont allouées à aucun processus. Ces pages sont de la mémoire libre, utilisable à volonté. La liste des pages vides est mise à jour à chaque fois qu'un programme réserve de la mémoire, des pages sont alors prises dans cette liste et sont allouées au programme demandeur.
====Les défauts de page====
Lorsque l'on veut traduire l'adresse logique d'une page mémoire, le processeur vérifie le bit ''Valid'' et l'adresse physique. Si le bit ''Valid'' est à 1 et que l'adresse physique est présente, la traduction d'adresse s'effectue normalement. Mais si ce n'est pas le cas, l'entrée de la table des pages ne contient pas de quoi faire la traduction d'adresse. Soit parce que la page est swappée sur le disque dur et qu'il faut la copier en RAM, soit parce que les droits d'accès ne le permettent pas, soit parce que la page n'a pas encore été allouée, etc. On fait alors face à un '''défaut de page'''. Un défaut de page a lieu quand la MMU ne peut pas associer l'adresse logique à une adresse physique, quelque qu'en soit la raison.
Il existe deux types de défauts de page : mineurs et majeurs. Un '''défaut de page majeur''' a lieu quand on veut accéder à une page déplacée sur le disque dur. Un défaut de page majeur lève une exception matérielle dont la routine rapatriera la page en mémoire RAM. S'il y a de la place en mémoire RAM, il suffit d'allouer une page vide et d'y copier la page chargée depuis le disque dur. Mais si ce n'est par le cas, on va devoir faire de la place en RAM en déplaçant une page mémoire de la RAM vers le disque dur. Dans tous les cas, c'est le système d'exploitation qui s'occupe du chargement de la page, le processeur n'est pas impliqué. Une fois la page chargée, la table des pages est mise à jour et la traduction d'adresse peut recommencer. Si je dis recommencer, c'est car l'accès mémoire initial est rejoué à l'identique, sauf que la traduction d'adresse réussit cette fois-ci.
Un '''défaut de page mineur''' a lieu dans des circonstances pas très intuitives : la page est en mémoire physique, mais l'adresse physique de la page n'est pas accessible. Par exemple, il est possible que des sécurités empêchent de faire la traduction d'adresse, pour des raisons de protection mémoire. Une autre raison est la gestion des adresses synonymes, qui surviennent quand on utilise des libraires partagées entre programmes, de la communication inter-processus, des optimisations de type ''copy-on-write'', etc. Enfin, une dernière raison est que la page a été allouée à un programme par le système d'exploitation, mais qu'il n'a pas encore attribué sa position en mémoire. Pour comprendre comment c'est possible, parlons rapidement de l'allocation paresseuse.
Imaginons qu'un programme fasse une demande d'allocation mémoire et se voit donc attribuer une ou plusieurs pages logiques. L'OS peut alors réagir de deux manières différentes. La première est d'attribuer une page physique immédiatement, en même temps que la page logique. En faisant ainsi, on ne peut pas avoir de défaut mineur, sauf en cas de problème de protection mémoire. Cette solution est simple, on l'appelle l{{'}}'''allocation immédiate'''. Une autre solution consiste à attribuer une page logique, mais l'allocation de la page physique se fait plus tard. Elle a lieu la première fois que le programme tente d'écrire/lire dans la page physique. Un défaut mineur a lieu, et c'est lui qui force l'OS à attribuer une page physique pour la page logique demandée. On parle alors d{{'}}'''allocation paresseuse'''. L'avantage est que l'on gagne en performance si des pages logiques sont allouées mais utilisées, ce qui peut arriver.
Une optimisation permise par l'existence des défauts mineurs est le '''''copy-on-write'''''. Le but est d'optimiser la copie d'une page logique dans une autre. L'idée est que la copie est retardée quand elle est vraiment nécessaire, à savoir quand on écrit dans la copie. Tant que l'on ne modifie pas la copie, les deux pages logiques, originelle et copiée, pointent vers la même page physique. A quoi bon avoir deux copies avec le même contenu ? Par contre, la page physique est marquée en lecture seule. La moindre écriture déclenche une erreur de protection mémoire, et un défaut mineur. Celui-ci est géré par l'OS, qui effectue alors la copie dans une nouvelle page physique.
Je viens de dire que le système d'exploitation gère les défauts de page majeurs/mineurs. Un défaut de page déclenche une exception matérielle, qui passe la main au système d'exploitation. Le système d'exploitation doit alors déterminer ce qui a levé l'exception, notamment identifier si c'est un défaut de page mineur ou majeur. Pour cela, le processeur a un ou plusieurs '''registres de statut''' qui indique l'état du processeur, qui sont utiles pour gérer les défauts de page. Ils indiquent quelle est l'adresse fautive, si l'accès était une lecture ou écriture, si l'accès a eu lieu en espace noyau ou utilisateur (les espaces mémoire ne sont pas les mêmes), etc. Les registres en question varient grandement d'une architecture de processeur à l'autre, aussi on ne peut pas dire grand chose de plus sur le sujet. Le reste est de toute façon à voir dans un cours sur les systèmes d'exploitation.
====Le remplacement des pages====
Les pages virtuelles font référence soit à une page en mémoire physique, soit à une page sur le disque dur. Mais l'on ne peut pas lire une page directement depuis le disque dur. Les pages sur le disque dur doivent être chargées en RAM, avant d'être utilisables. Ce n'est possible que si on a une page mémoire vide, libre. Si ce n'est pas le cas, on doit faire de la place en swappant une page sur le disque dur. Les pages font ainsi une sorte de va et vient entre le fichier d'échange et la RAM, suivant les besoins. Tout cela est effectué par une routine d'interruption du système d'exploitation, le processeur n'ayant pas vraiment de rôle là-dedans.
Supposons que l'on veuille faire de la place en RAM pour une nouvelle page. Dans une implémentation naïve, on trouve une page à évincer de la mémoire, qui est copiée dans le ''swapfile''. Toutes les pages évincées sont alors copiées sur le disque dur, à chaque remplacement. Néanmoins, cette implémentation naïve peut cependant être améliorée si on tient compte d'un point important : si la page a été modifiée depuis le dernier accès. Si le programme/processeur a écrit dans la page, alors celle-ci a été modifiée et doit être sauvegardée sur le ''swapfile'' si elle est évincée. Par contre, si ce n'est pas le cas, la page est soit initialisée, soit déjà présente à l'identique dans le ''swapfile''.
Mais cette optimisation demande de savoir si une écriture a eu lieu dans la page. Pour cela, on ajoute un '''''dirty bit''''' à chaque entrée de la table des pages, juste à côté du bit ''Valid''. Il indique si une écriture a eu lieu dans la page depuis qu'elle a été chargée en RAM. Ce bit est mis à jour par le processeur, automatiquement, lors d'une écriture. Par contre, il est remis à zéro par le système d'exploitation, quand la page est chargée en RAM. Si le programme se voit allouer de la mémoire, il reçoit une page vide, et ce bit est initialisé à 0. Il est mis à 1 si la mémoire est utilisée. Quand la page est ensuite swappée sur le disque dur, ce bit est remis à 0 après la sauvegarde.
Sur la majorité des systèmes d'exploitation, il est possible d'interdire le déplacement de certaines pages sur le disque dur. Ces pages restent alors en mémoire RAM durant un temps plus ou moins long, parfois en permanence. Cette possibilité simplifie la vie des programmeurs qui conçoivent des systèmes d'exploitation : essayez d'exécuter l'interruption pour les défauts de page alors que la page contenant le code de l'interruption est placée sur le disque dur ! Là encore, cela demande d'ajouter un bit dans chaque entrée de la table des pages, qui indique si la page est swappable ou non. Le bit en question s'appelle souvent le '''bit ''swappable'''''.
====Les algorithmes de remplacement des pages pris en charge par l'OS====
Le choix de la page doit être fait avec le plus grand soin et il existe différents algorithmes qui permettent de décider quelle page supprimer de la RAM. Leur but est de swapper des pages qui ne seront pas accédées dans le futur, pour éviter d'avoir à faire triop de va-et-vient entre RAM et ''swapfile''. Les données qui sont censées être accédées dans le futur doivent rester en RAM et ne pas être swappées, autant que possible. Les algorithmes les plus simples pour le choix de page à évincer sont les suivants.
Le plus simple est un algorithme aléatoire : on choisit la page au hasard. Mine de rien, cet algorithme est très simple à implémenter et très rapide à exécuter. Il ne demande pas de modifier la table des pages, ni même d'accéder à celle-ci pour faire son choix. Ses performances sont surprenamment correctes, bien que largement en-dessous de tous les autres algorithmes.
L'algorithme FIFO supprime la donnée qui a été chargée dans la mémoire avant toutes les autres. Cet algorithme fonctionne bien quand un programme manipule des tableaux de grande taille, mais fonctionne assez mal dans le cas général.
L'algorithme LRU supprime la donnée qui été lue ou écrite pour la dernière fois avant toutes les autres. C'est théoriquement le plus efficace dans la majorité des situations. Malheureusement, son implémentation est assez complexe et les OS doivent modifier la table des pages pour l'implémenter.
L'algorithme le plus utilisé de nos jours est l{{'}}'''algorithme NRU''' (''Not Recently Used''), une simplification drastique du LRU. Il fait la différence entre les pages accédées il y a longtemps et celles accédées récemment, d'une manière très binaire. Les deux types de page sont appelés respectivement les '''pages froides''' et les '''pages chaudes'''. L'OS swappe en priorité les pages froides et ne swappe de page chaude que si aucune page froide n'est présente. L'algorithme est simple : il choisit la page à évincer au hasard parmi une page froide. Si aucune page froide n'est présente, alors il swappe au hasard une page chaude.
Pour implémenter l'algorithme NRU, l'OS mémorise, dans chaque entrée de la table des pages, si la page associée est froide ou chaude. Pour cela, il met à 0 ou 1 un bit dédié : le '''bit ''Accessed'''''. La différence avec le bit ''dirty'' est que le bit ''dirty'' est mis à jour uniquement lors des écritures, alors que le bit ''Accessed'' l'est aussi lors d'une lecture. Uen lecture met à 1 le bit ''Accessed'', mais ne touche pas au bit ''dirty''. Les écritures mettent les deux bits à 1.
Implémenter l'algorithme NRU demande juste de mettre à jour le bit ''Accessed'' de chaque entrée de la table des pages. Et sur les architectures modernes, le processeur s'en charge automatiquement. A chaque accès mémoire, que ce soit en lecture ou en écriture, le processeur met à 1 ce bit. Par contre, le système d'exploitation le met à 0 à intervalles réguliers. En conséquence, quand un remplacement de page doit avoir lieu, les pages chaudes ont de bonnes chances d'avoir le bit ''Accessed'' à 1, alors que les pages froides l'ont à 0. Ce n'est pas certain, et on peut se trouver dans des cas où ce n'est pas le cas. Par exemple, si un remplacement a lieu juste après la remise à zéro des bits ''Accessed''. Le choix de la page à remplacer est donc imparfait, mais fonctionne bien en pratique.
Tous les algorithmes précédents ont chacun deux variantes : une locale, et une globale. Avec la version locale, la page qui va être rapatriée sur le disque dur est une page réservée au programme qui est la cause du page miss. Avec la version globale, le système d'exploitation va choisir la page à virer parmi toutes les pages présentes en mémoire vive.
===La protection mémoire avec la pagination===
Avec la pagination, chaque page a des '''droits d'accès''' précis, qui permettent d'autoriser ou interdire les accès en lecture, écriture, exécution, etc. La table des pages mémorise les autorisations pour chaque page, sous la forme d'une suite de bits où chaque bit autorise/interdit une opération bien précise. En pratique, les tables de pages modernes disposent de trois bits : un qui autorise/interdit les accès en lecture, un qui autorise/interdit les accès en écriture, un qui autorise/interdit l'éxecution du contenu de la page.
Le format exact de la suite de bits a cependant changé dans le temps sur les processeurs x86 modernes. Par exemple, avant le passage au 64 bits, les CPU et OS ne pouvaient pas marquer une page mémoire comme non-exécutable. C'est seulement avec le passage au 64 bits qu'a été ajouté un bit pour interdire l'exécution de code depuis une page. Ce bit, nommé '''bit NX''', est à 0 si la page n'est pas exécutable et à 1 sinon. Le processeur vérifie à chaque chargement d'instruction si le bit NX de page lue est à 1. Sinon, il lève une exception matérielle et laisse la main à l'OS.
Une amélioration de cette protection est la technique dite du '''''Write XOR Execute''''', abréviée WxX. Elle consiste à interdire les pages d'être à la fois accessibles en écriture et exécutables. Il est possible de changer les autorisations en cours de route, ceci dit.
Les premiers IBM 360 disposaient d'un mécanisme de protection mémoire totalement différent, sans registres limite/base. Ce mécanisme de protection attribue à chaque programme une '''clé de protection''', qui consiste en un nombre unique de 4 bits (chaque programme a donc une clé différente de ses collègues). La mémoire est fragmentée en blocs de même taille, de 2 kibioctets. Le processeur mémorise, pour chacun de ses blocs, la clé de protection du programme qui a réservé ce bloc. À chaque accès mémoire, le processeur compare la clé de protection du programme en cours d’exécution et celle du bloc de mémoire de destination. Si les deux clés sont différentes, alors un programme a effectué un accès hors des clous et il se fait sauvagement arrêter.
===La traduction d'adresse avec la pagination===
Comme dit plus haut, les pages sont numérotées, de 0 à une valeur maximale, afin de les identifier. Le numéro en question est appelé le '''numéro de page'''. Il est utilisé pour dire au processeur : je veux lire une donnée dans la page numéro 20, la page numéro 90, etc. Une fois qu'on a le numéro de page, on doit alors préciser la position de la donnée dans la page, appelé le '''décalage''', ou encore l{{'}}''offset''.
Le numéro de page et le décalage se déduisent à partir de l'adresse, en divisant l'adresse par la taille de la page. Le quotient obtenu donne le numéro de la page, alors que le reste est le décalage. Les processeurs actuels utilisent tous des pages dont la taille est une puissance de deux, ce qui fait que ce calcul est fortement simplifié. Sous cette condition, le numéro de page correspond aux bits de poids fort de l'adresse, alors que le décalage est dans les bits de poids faible.
Le numéro de page existe en deux versions : un numéro de page physique qui identifie une page en mémoire physique, et un numéro de page logique qui identifie une page dans la mémoire virtuelle. Traduire l'adresse logique en adresse physique demande de remplacer le numéro de la page logique en un numéro de page physique.
[[File:Phycical address.JPG|centre|vignette|upright=2|Traduction d'adresse avec la pagination.]]
====Les tables des pages simples====
Dans le cas le plus simple, il n'y a qu'une seule table des pages, qui est adressée par les numéros de page logique. La table des pages est un vulgaire tableau d'adresses physiques, placées les unes à la suite des autres. Avec cette méthode, la table des pages a autant d'entrée qu'il y a de pages logiques en mémoire virtuelle. Accéder à la mémoire nécessite donc d’accéder d'abord à la table des pages en mémoire, de calculer l'adresse de l'entrée voulue, et d’y accéder.
[[File:Table des pages.png|centre|vignette|upright=2|Table des pages.]]
La table des pages est souvent stockée dans la mémoire RAM, son adresse est connue du processeur, mémorisée dans un registre spécialisé du processeur. Le processeur effectue automatiquement le calcul d'adresse à partir de l'adresse de base et du numéro de page logique.
[[File:Address translation (32-bit).png|centre|vignette|upright=2|Address translation (32-bit)]]
====Les tables des pages inversées====
Sur certains systèmes, notamment sur les architectures 64 bits ou plus, le nombre de pages est très important. Sur les ordinateurs x86 récents, les adresses sont en pratique de 48 bits, les bits de poids fort étant ignorés en pratique, ce qui fait en tout 68 719 476 736 pages. Chaque entrée de la table des pages fait au minimum 48 bits, mais fait plus en pratique : partons sur 64 bits par entrée, soit 8 octets. Cela fait 549 755 813 888 octets pour la table des pages, soit plusieurs centaines de gibioctets ! Une table des pages normale serait tout simplement impraticable.
Pour résoudre ce problème, on a inventé les '''tables des pages inversées'''. L'idée derrière celles-ci est l'inverse de la méthode précédente. La méthode précédente stocke, pour chaque page logique, son numéro de page physique. Les tables des pages inversées font l'inverse : elles stockent, pour chaque numéro de page physique, la page logique qui correspond. Avec cette méthode table des pages contient ainsi autant d'entrées qu'il y a de pages physiques. Elle est donc plus petite qu'avant, vu que la mémoire physique est plus petite que la mémoire virtuelle.
Quand le processeur veut convertir une adresse virtuelle en adresse physique, la MMU recherche le numéro de page de l'adresse virtuelle dans la table des pages. Le numéro de l'entrée à laquelle se trouve ce morceau d'adresse virtuelle est le morceau de l'adresse physique. Pour faciliter le processus de recherche dans la page, la table des pages inversée est ce que l'on appelle une table de hachage. C'est cette solution qui est utilisée sur les processeurs Power PC.
[[File:Table des pages inversée.jpg|centre|vignette|upright=2|Table des pages inversée.]]
====Les tables des pages multiples par espace d'adressage====
Dans les deux cas précédents, il y a une table des pages unique. Cependant, les concepteurs de processeurs et de systèmes d'exploitation ont remarqué que les adresses les plus hautes et/ou les plus basses sont les plus utilisées, alors que les adresses situées au milieu de l'espace d'adressage sont peu utilisées en raison du fonctionnement de la pile et du tas. Il y a donc une partie de la table des pages qui ne sert à rien et est utilisé pour des adresses inutilisées. C'est une source d'économie d'autant plus importante que les tables des pages sont de plus en plus grosses.
Pour profiter de cette observation, les concepteurs d'OS ont décidé de découper l'espace d'adressage en plusieurs sous-espaces d'adressage de taille identique : certains localisés dans les adresses basses, d'autres au milieu, d'autres tout en haut, etc. Et vu que l'espace d'adressage est scindé en plusieurs parties, la table des pages l'est aussi, elle est découpée en plusieurs sous-tables. Si un sous-espace d'adressage n'est pas utilisé, il n'y a pas besoin d'utiliser de la mémoire pour stocker la table des pages associée. On ne stocke que les tables des pages pour les espaces d'adressage utilisés, ceux qui contiennent au moins une donnée.
L'utilisation de plusieurs tables des pages ne fonctionne que si le système d'exploitation connaît l'adresse de chaque table des pages (celle de la première entrée). Pour cela, le système d'exploitation utilise une super-table des pages, qui stocke les adresses de début des sous-tables de chaque sous-espace. En clair, la table des pages est organisé en deux niveaux, la super-table étant le premier niveau et les sous-tables étant le second niveau.
L'adresse est structurée de manière à tirer profit de cette organisation. Les bits de poids fort de l'adresse sélectionnent quelle table de second niveau utiliser, les bits du milieu de l'adresse sélectionne la page dans la table de second niveau et le reste est interprété comme un ''offset''. Un accès à la table des pages se fait comme suit. Les bits de poids fort de l'adresse sont envoyés à la table de premier niveau, et sont utilisés pour récupérer l'adresse de la table de second niveau adéquate. Les bits au milieu de l'adresse sont envoyés à la table de second niveau, pour récupérer le numéro de page physique. Le tout est combiné avec l{{'}}''offset'' pour obtenir l'adresse physique finale.
[[File:Table des pages hiérarchique.png|centre|vignette|upright=2|Table des pages hiérarchique.]]
On peut aussi aller plus loin et découper la table des pages de manière hiérarchique, chaque sous-espace d'adressage étant lui aussi découpé en sous-espaces d'adressages. On a alors une table de premier niveau, plusieurs tables de second niveau, encore plus de tables de troisième niveau, et ainsi de suite. Cela peut aller jusqu'à 5 niveaux sur les processeurs x86 64 bits modernes. On parle alors de '''tables des pages emboitées'''. Dans ce cours, la table des pages désigne l'ensemble des différents niveaux de cette organisation, toutes les tables inclus. Seules les tables du dernier niveau mémorisent des numéros de page physiques, les autres tables mémorisant des pointeurs, des adresses vers le début des tables de niveau inférieur. Un exemple sera donné plus bas, dans la section suivante.
====L'exemple des processeurs x86====
Pour rendre les explications précédentes plus concrètes, nous allons prendre l'exemple des processeur x86 anciens, de type 32 bits. Les processeurs de ce type utilisaient deux types de tables des pages : une table des page unique et une table des page hiérarchique. Les deux étaient utilisées dans cas séparés. La table des page unique était utilisée pour les pages larges et encore seulement en l'absence de la technologie ''physical adress extension'', dont on parlera plus bas. Les autres cas utilisaient une table des page hiérarchique, à deux niveaux, trois niveaux, voire plus.
Une table des pages unique était utilisée pour les pages larges (de 2 mébioctets et plus). Pour les pages de 4 mébioctets, il y avait une unique table des pages, adressée par les 10 bits de poids fort de l'adresse, les bits restants servant comme ''offset''. La table des pages contenait 1024 entrées de 4 octets chacune, ce qui fait en tout 4 kibioctet pour la table des pages. La table des page était alignée en mémoire sur un bloc de 4 kibioctet (sa taille).
[[File:X86 Paging 4M.svg|centre|vignette|upright=2|X86 Paging 4M]]
Pour les pages de 4 kibioctets, les processeurs x86-32 bits utilisaient une table des page hiérarchique à deux niveaux. Les 10 bits de poids fort l'adresse adressaient la table des page maitre, appelée le directoire des pages (''page directory''), les 10 bits précédents servaient de numéro de page logique, et les 12 bits restants servaient à indiquer la position de l'octet dans la table des pages. Les entrées de chaque table des pages, mineure ou majeure, faisaient 32 bits, soit 4 octets. Vous remarquerez que la table des page majeure a la même taille que la table des page unique obtenue avec des pages larges (de 4 mébioctets).
[[File:X86 Paging 4K.svg|centre|vignette|upright=2|X86 Paging 4K]]
La technique du '''''physical adress extension''''' (PAE), utilisée depuis le Pentium Pro, permettait aux processeurs x86 32 bits d'adresser plus de 4 gibioctets de mémoire, en utilisant des adresses physiques de 64 bits. Les adresses virtuelles de 32 bits étaient traduites en adresses physiques de 64 bits grâce à une table des pages adaptée. Cette technologie permettait d'adresser plus de 4 gibioctets de mémoire au total, mais avec quelques limitations. Notamment, chaque programme ne pouvait utiliser que 4 gibioctets de mémoire RAM pour lui seul. Mais en lançant plusieurs programmes, on pouvait dépasser les 4 gibioctets au total. Pour cela, les entrées de la table des pages passaient à 64 bits au lieu de 32 auparavant.
La table des pages gardait 2 niveaux pour les pages larges en PAE.
[[File:X86 Paging PAE 2M.svg|centre|vignette|upright=2|X86 Paging PAE 2M]]
Par contre, pour les pages de 4 kibioctets en PAE, elle était modifiée de manière à ajouter un niveau de hiérarchie, passant de deux niveaux à trois.
[[File:X86 Paging PAE 4K.svg|centre|vignette|upright=2|X86 Paging PAE 4K]]
En 64 bits, la table des pages est une table des page hiérarchique avec 5 niveaux. Seuls les 48 bits de poids faible des adresses sont utilisés, les 16 restants étant ignorés.
[[File:X86 Paging 64bit.svg|centre|vignette|upright=2|X86 Paging 64bit]]
===La TLB et les caches associés===
En théorie, la table des pages est censée être accédée à chaque accès mémoire. Mais pour éviter d'avoir à lire la table des pages en mémoire RAM à chaque accès mémoire, les concepteurs de processeurs ont décidé d'implanter un cache dédié, le '''''translation lookaside buffer''''', ou TLB. Le TLB stocke au minimum de quoi faire la traduction entre adresse virtuelle et adresse physique, à savoir une correspondance entre numéro de page logique et numéro de page physique. Pour faire plus général, il stocke des entrées de la table des pages.
[[File:MMU principle updated.png|centre|vignette|upright=2.0|MMU avec une TLB.]]
Les accès à la table des pages sont gérés de deux façons : soit le processeur gère tout seul la situation, soit il délègue cette tâche au système d’exploitation. Sur les processeurs anciens, le système d'exploitation gère le parcours de la table des pages. Mais cette solution logicielle n'a pas de bonnes performances. D'autres processeurs gèrent eux-mêmes le défaut d'accès à la TLB et vont chercher d'eux-mêmes les informations nécessaires dans la table des pages. Ils disposent de circuits, les '''''page table walkers''''' (PTW), qui s'occupent eux-mêmes du défaut.
Les ''page table walkers'' contiennent des registres qui leur permettent de faire leur travail. Le plus important est celui qui mémorise la position de la table des pages en mémoire RAM, dont nous avons parlé plus haut. Les PTW ont besoin, pour faire leur travail, de mémoriser l'adresse physique de la table des pages, ou du moins l'adresse de la table des pages de niveau 1 pour des tables des pages hiérarchiques. Mais d'autres registres existent. Toutes les informations nécessaires pour gérer les défauts de TLB sont stockées dans des registres spécialisés appelés des '''tampons de PTW''' (PTW buffers).
===L'abstraction matérielle des processus===
[[File:Memoire virtuelle.svg|vignette|Mémoire virtuelle]]
Il est possible d'implémenter l'abstraction matérielle des processus avec la pagination. En clair, chaque programme lancé sur l'ordinateur dispose de son propre espace d'adressage, ce qui fait que la même adresse logique ne pointera pas sur la même adresse physique dans deux programmes différents. Pour cela, il y a plusieurs méthodes.
La première solution n'utilise qu'une seule table des pages. Pour cela, chaque entrée indique à quel processus elle est associée. Pour cela, chaque entrée contient un '''identifiant de processus''', un numéro qui précise pour quel processus, pour quel espace d'adressage, la correspondance est valide. La page des tables peut aussi contenir des entrées qui sont valides pour tous les processus en même temps. Pour cela, l'entrée contient un '''bit ''global''''', qui précise que cette correspondance est valide pour tous les processus, l'identifiant de processus est ignoré. Un défaut est que le partage d'une page entre plusieurs processus est impossible : soit on partage une page avec tous les processus, soit on l'alloue avec un seul processus.
L'intérêt de partager des pages entre tous les processus n'est pas évident, mais il le devient quand on se rappelle que le noyau de l'OS est mappé dans le haut de l'espace d'adressage. Et peu importe l'espace d'adressage, le noyau est toujours mappé de manière identique, les mêmes adresses logiques adressant la même adresse mémoire. En conséquence, les correspondances adresse physique-logique sont les mêmes pour le noyau, peu importe l'espace d'adressage. Le bit global accélère rapidement la traduction d'adresse pour l'accès au noyau.
Une solution alternative, plus simple, utilise une table des pages par processus lancé sur l'ordinateur, une table des pages unique par espace d'adressage. À chaque changement de processus, le registre qui mémorise la position de la table des pages est modifié pour pointer sur la bonne. C'est le système d'exploitation qui se charge de cette mise à jour.
Avec cette méthode, il est possible de partager une ou plusieurs pages entre plusieurs processus, en configurant les tables des pages convenablement. Les pages partagées sont mappées dans l'espace d'adressage de plusieurs processus, mais pas forcément au même endroit, pas forcément dans les mêmes adresses logiques. On peut placer la page partagée à l'adresse logique 0x0FFF pour un processus, à l'adresse logique 0xFF00 pour un autre processus, etc. Par contre, les entrées de la table des pages pour ces adresses pointent vers la même adresse physique.
[[File:Vm5.png|centre|vignette|upright=2|Tables des pages de plusieurs processus.]]
===La taille des pages===
La taille des pages varie suivant le processeur et le système d'exploitation et tourne souvent autour de 4 kibioctets. Les processeurs actuels gèrent plusieurs tailles différentes pour les pages : 4 kibioctets par défaut, 2 mébioctets, voire 1 à 4 gibioctets pour les pages les plus larges. Les pages de 4 kibioctets sont les pages par défaut, les autres tailles de page sont appelées des ''pages larges''. La taille optimale pour les pages dépend de nombreux paramètres et il n'y a pas de taille qui convienne à tout le monde. Certaines applications gagnent à utiliser des pages larges, d'autres vont au contraire perdre drastiquement en performance en les utilisant.
Le désavantage principal des pages larges est qu'elles favorisent la fragmentation mémoire. Si un programme veut réserver une portion de mémoire, pour une structure de donnée quelconque, il doit réserver une portion dont la taille est multiple de la taille d'une page. Par exemple, un programme ayant besoin de 110 kibioctets allouera 28 pages de 4 kibioctets, soit 120 kibioctets : 2 kibioctets seront perdus. Par contre, avec des pages larges de 2 mébioctets, on aura une perte de 2048 - 110 = 1938 kibioctets. En somme, des morceaux de mémoire seront perdus, car les pages sont trop grandes pour les données qu'on veut y mettre. Le résultat est que le programme qui utilise les pages larges utilisent plus de mémoire et ce d'autant plus qu'il utilise des données de petite taille. Un autre désavantage est qu'elles se marient mal avec certaines techniques d'optimisations de type ''copy-on-write''.
Mais l'avantage est que la traduction des adresses est plus performante. Une taille des pages plus élevée signifie moins de pages, donc des tables des pages plus petites. Et des pages des tables plus petites n'ont pas besoin de beaucoup de niveaux de hiérarchie, voire peuvent se limiter à des tables des pages simples, ce qui rend la traduction d'adresse plus simple et plus rapide. De plus, les programmes ont une certaine localité spatiale, qui font qu'ils accèdent souvent à des données proches. La traduction d'adresse peut alors profiter de systèmes de mise en cache dont nous parlerons dans le prochain chapitre, et ces systèmes de cache marchent nettement mieux avec des pages larges.
Il faut noter que la taille des pages est presque toujours une puissance de deux. Cela a de nombreux avantages, mais n'est pas une nécessité. Par exemple, le tout premier processeur avec de la pagination, le super-ordinateur Atlas, avait des pages de 3 kibioctets. L'avantage principal est que la traduction de l'adresse physique en adresse logique est trivial avec une puissance de deux. Cela garantit que l'on peut diviser l'adresse en un numéro de page et un ''offset'' : la traduction demande juste de remplacer les bits de poids forts par le numéro de page voulu. Sans cela, la traduction d'adresse implique des divisions et des multiplications, qui sont des opérations assez couteuses.
===Les entrées de la table des pages===
Avant de poursuivre, faisons un rapide rappel sur les entrées de la table des pages. Nous venons de voir que la table des pages contient de nombreuses informations : un bit ''valid'' pour la mémoire virtuelle, des bits ''dirty'' et ''accessed'' utilisés par l'OS, des bits de protection mémoire, un bit ''global'' et un potentiellement un identifiant de processus, etc. Étudions rapidement le format de la table des pages sur un processeur x86 32 bits.
* Elle contient d'abord le numéro de page physique.
* Les bits AVL sont inutilisés et peuvent être configurés à loisir par l'OS.
* Le bit G est le bit ''global''.
* Le bit PS vaut 0 pour une page de 4 kibioctets, mais est mis à 1 pour une page de 4 mébioctets dans le cas où le processus utilise des pages larges.
* Le bit D est le bit ''dirty''.
* Le bit A est le bit ''accessed''.
* Le bit PCD indique que la page ne peut pas être cachée, dans le sens où le processeur ne peut copier son contenu dans le cache et doit toujours lire ou écrire cette page directement dans la RAM.
* Le bit PWT indique que les écritures doivent mettre à jour le cache et la page en RAM (dans le chapitre sur le cache, on verra qu'il force le cache à se comporter comme un cache ''write-through'' pour cette page).
* Le bit U/S précise si la page est accessible en mode noyau ou utilisateur.
* Le bit R/W indique si la page est accessible en écriture, toutes les pages sont par défaut accessibles en lecture.
* Le bit P est le bit ''valid''.
[[File:PDE.png|centre|vignette|upright=2.5|Table des pages des processeurs Intel 32 bits.]]
==Comparaison des différentes techniques d'abstraction mémoire==
Pour résumer, l'abstraction mémoire permet de gérer : la relocation, la protection mémoire, l'isolation des processus, la mémoire virtuelle, l'extension de l'espace d'adressage, le partage de mémoire, etc. Elles sont souvent implémentées en même temps. Ce qui fait qu'elles sont souvent confondues, alors que ce sont des concepts sont différents. Ces liens sont résumés dans le tableau ci-dessous.
{|class="wikitable"
|-
!
! colspan="5" | Avec abstraction mémoire
! rowspan="2" | Sans abstraction mémoire
|-
!
! Relocation matérielle
! Segmentation en mode réel (x86)
! Segmentation, général
! Architectures à capacités
! Pagination
|-
! Abstraction matérielle des processus
| colspan="4" | Oui, relocation matérielle
| Oui, liée à la traduction d'adresse
| Emulée via relocation logicielle au démarrage du programme.
|-
! Mémoire virtuelle
| colspan="2" | Non, sauf émulation logicielle
| colspan="3" | Oui, gérée par le processeur et l'OS
| Non, sauf émulation logicielle
|-
! Extension de l'espace d'adressage
| colspan="2" | Oui : registre de base élargi
| colspan="2" | Oui : adresse de base élargie dans la table des segments
| ''Physical Adress Extension'' des processeurs 32 bits
| Commutation de banques
|-
! Protection mémoire
| Registre limite
| Aucune
| colspan="2" | Registre limite, droits d'accès aux segments
| Gestion des droits d'accès aux pages
| Possible, méthodes variées
|-
! Partage de mémoire
| colspan="2" | Non
| colspan="2" | Segment partagés
| Pages partagées
| Possible, méthodes variées
|}
===Les différents types de segmentation===
La segmentation regroupe plusieurs techniques franchement différentes, qui auraient gagné à être nommées différemment. La principale différence est l'usage de registres de relocation versus des registres de sélecteurs de segments. L'usage de registres de relocation est le fait de la relocation matérielle, mais aussi de la segmentation en mode réel des CPU x86. Par contre, l'usage de sélecteurs de segments est le fait des autres formes de segmentation, architectures à capacité inclues.
La différence entre les deux est le nombre de segments. L'usage de registres de relocation fait que le CPU ne gère qu'un petit nombre de segments de grande taille. La mémoire virtuelle est donc rarement implémentée vu que swapper des segments de grande taille est trop long, l'impact sur les performances est trop important. Sans compter que l'usage de registres de base se marie très mal avec la mémoire virtuelle. Vu qu'un segment peut être swappé ou déplacée n'importe quand, il faut invalider les registres de base au moment du swap/déplacement, ce qui n'est pas chose aisée. Aucun processeur ne gère cela, les méthodes pour n'existent tout simplement pas. L'usage de registres de base implique que la mémoire virtuelle est absente.
La protection mémoire est aussi plus limitée avec l'usage de registres de relocation. Elle se limite à des registres limite, mais la gestion des droits d'accès est limitée. En théorie, la segmentation en mode réel pourrait implémenter une version limitée de protection mémoire, avec une protection de l'espace exécutable. Mais ca n'a jamais été fait en pratique sur les processeurs x86.
Le partage de la mémoire est aussi difficile sur les architectures avec des registres de base. L'absence de table des segments fait que le partage d'un segment est basiquement impossible sans utiliser des méthodes complétement tordues, qui ne sont jamais implémentées en pratique.
===Segmentation versus pagination===
Par rapport à la pagination, la segmentation a des avantages et des inconvénients. Tous sont liés aux propriétés des segments et pages : les segments sont de grande taille et de taille variable, les pages sont petites et de taille fixe.
L'avantage principal de la segmentation est sa rapidité. Le fait que les segments sont de grande taille fait qu'on a pas besoin d'équivalent aux tables des pages inversée ou multiple, juste d'une table des segments toute simple. De plus, les échanges entre table des pages/segments et registres sont plus rares avec la segmentation. Par exemple, si un programme utilise un segment de 2 gigas, tous les accès dans le segment se feront avec une seule consultation de la table des segments. Alors qu'avec la pagination, il faudra une consultation de la table des pages chaque bloc de 4 kibioctet, au minimum.
Mais les désavantages sont nombreux. Le système d'exploitation doit agencer les segments en RAM, et c'est une tâche complexe. Le fait que les segments puisse changer de taille rend le tout encore plus complexe. Par exemple, si on colle les segments les uns à la suite des autres, changer la taille d'un segment demande de réorganiser tous les segments en RAM, ce qui demande énormément de copies RAM-RAM. Une autre possibilité est de laisser assez d'espace entre les segments, mais cet espace est alors gâché, dans le sens où on ne peut pas y placer un nouveau segment.
Swapper un segment est aussi très long, vu que les segments sont de grande taille, alors que swapper une page est très rapide.
==Un cas particulier : la z/architecture d'IBM==
IBM a sa propre gamme de ''mainframes'', qui utilisent des processeurs fait maison par IBM lui-même. Les CPU IBM ont leur propre jeu d'instruction CISC, appelé '''''z/architecture'''''. Et ce jeu d'instruction a des possibilités en termes d'adressage qui sont extrêmement intéressantes, en plus d'être assez inédites.
Nous allons nous concentrer sur une fonctionnalité : les programmes ont accès à plusieurs espaces d'adressage simultanément. La ''z/architecture'' supporte 65 536 espaces d'adressage simultanés. Par contre, un programme ne peut en utiliser que 2048 en même temps. De plus, ils ne sont accessibles que quand le processeur est dans un mode de fonctionnement bien précis. Un processeur ''z/architecture'' peut en effet fonctionner dans deux modes, qu'on va appeler "standard" et "étendu".
L'existence de ces deux modes s'explique par le fait que les ''mainframes'' IBM ont évolués dans le temps. Les premiers ''mainframes'' n'avaient même pas de support basique de la mémoire virtuelle, le System/360 s'en passait totalement. C'est sur le System/370 que la pagination a été ajoutée. Le System/370 supportait deux espaces d'adressage virtuel par programme, là où les implémentations classiques n'en utilisent qu'un par programme. L'ESA/370 a ajouté la possibilité d'utiliser 16 espaces d'adressage par programme, mais l'a fait d'une manière à garder la compatibilité avec le System/370. Le processeur a donc deux modes : un avec deux espaces d'adressage par programme, un autre pouvant monter jusqu’à 16.
===Le System/370===
Le System/370 gérait des pages de 4 kibioctets, les pages larges ayant été introduites plus tard. La traduction d’adresse se fait en passant des regroupements de plusieurs pages virtuelles, appelés des ''segments'', qui n'ont rien à voir avec les segments au sens de la segmentation. Pour éviter les confusions, je vais les appeler des ''super-pages''. Les super-pages font 64 kibioctets ou 1 mébioctet, et regroupent plusieurs pages de 2 ou 4 mébioctets. Les pages d'une super-page peuvent être très éloignées en mémoire physique. Les super-pages sont concrètement des intermédiaires utilisés pour la traduction d'adresse. La traduction d'adresse démarre à une table des super-pages, qui point vers une table des pages à plusieurs niveaux (5 maximum).
Le System/370 était un système multi-processeur, ce qui a amené à quelques spécificités en termes d'adressage mémoire. La page numéro 0 sert pour le vecteur d'interruption et d'autres données importantes, pour des données importantes. Et chaque processeur a besoin de sa propre table d'interruption. Pour cela, chaque processeur réserve une page dédiée pour son vecteur d'interruption. Quand ce processeur accède à une adresse située dans la page numéro 0, l'adresse est modifiée pour tomber dans cette page réservée. Notons que le numéro de page converti est un numéro de page physique ! Le processeur fait donc une première traduction d'adresse pour déterminer l'adresse physique, et celle-ci est corrigée une seconde fois en cas d'accès à la première page mémoire. Le tout porte le nom de technique de '''''prefixing'''''.
Le System/370 supporte deux espaces d'adressages par programme : l''''espace d'adressage primaire''' où se situent le programme exécuté et les données importantes, un '''espace d'adressage secondaire''' utilisé pour des tâches/données annexes. Un troisième espace d'adressage l''''espace d'adressage ''home''''', est réservé au système d'exploitation. Il a pour particularité de ne pas avoir d'ASN. Il n'est concrètement accessible qu'en mode superviseur, donc par le système d'exploitation. C'est en quelque sorte un espace d'adressage pour le noyau, séparé des espaces d'adressage utilisateur. Nous n'en parlerons pas dans ce qui suit.
Mais comment le processeur pour utiliser ces espaces d'adressages ? Par exemple, s'il veut faire une lecture, comment précise-t-il l'espace d'adressage courant ? La réponse est que le processeur peut être dans deux états : soit primaire, soit secondaire. Dans le premier état, toutes les lectures/écritures se font dans l'espace d'adressage primaire. Dans le second, elles se font toutes dans l'espace d'adressage secondaire. Il peut passer de l'un à l'autre en utilisant l'instruction ''Set Address Space Control''. Le processeur dispose aussi d'instructions ''MOVE TO PRIMARY'' et ''MOVE TO SECONDARY'' pour échanger des données dans les deux espaces d'adressage.
L'espace d'adressage primaire a une table des pages dédiée, séparée de celle de l'espace d'adressage secondaire. Ou plutôt devrais-je dire une table des super-pages. La table des pages est une table des pages emboitée. Il y a peu à détailler dessus, si ce n'est pour cette histoire de segments/super-pages. L'adresse de la table des super-page est mémorisée dans le registre de contrôle CR1 pour l'espace d'adressage primaire, CR7 pour l'espace d'adressage secondaire. L'un ou l'autre est utilisé, selon que le processeur est en mode "primaire" ou "secondaire".
===Les registres d'accès de la ''z/architecture''===
Un programme peut donc passer de l'espace d'adressage primaire à secondaire (ou inversement) en utilisant l’instruction ''SET ADDRESS SPACE CONTROL''. Mais il peut en profiter pour passer dans un mode où il a accès à 2048 espaces d'adressage différents, en même temps. Le processeur n'a pas besoin d'utiliser d'instructions particulières pour copier des données d'un espace d'adressage à un autre, un simple MOV suffit. Le mode en question est appelé le '''mode ''access register'''''.
Le mode ''access register'' donne accès aux '''registres d'accès''', qui lui donnent son nom. Les registres d'accès mémorisent des identifiants qui permettent de sélectionner un espace d'adressage parmi 2048. Il y a en tout 16 registres d'accès, chacun étant appairé avec un registre général. Par exemple, le registre général numéro 5 est appairé au registre d'accès de même numéro. En mode ''access register'', toute lecture/écriture lit l'adresse dans un registre général et le registre d'accès associé indique dans quel espace d'adressage se trouve cette adresse. Par exemple, une lecture utilise l'adresse dans le registre général numéro 7, l'espace d'adressage associé sera dans le registre d'accès numéro 7.
Les registres d'accès peuvent être modifiés par des instructions classiques, en espace utilisateur. Elles sont nommées COPY ACCESS, EXTRACT ACCESS, LOAD ACCESS MULTIPLE, LOAD ADDRESS EXTENDED, SET ACCESS et STORE ACCESS MULTIPLE. Elles permettent de copier un registre d'accès dans un autre, de charger le contenu d'un registre d'accès depuis la mémoire RAM, d'enregistrer les registres d'accès dans la mémoire RAM, de copier une constante dans un registre d'accès. Mais quel est le contenu de ces registres ?
Une implémentation simple numéroterait les espaces d'adressage et mettrait les numéros dans les registres d'accès. Un espace d'adressage serait associé à une table des super-pages, reste à mémoriser la correspondance ''numéro d'espace d'adressage - adresse de la table des super-pages''. Pour cela, l'idéal est de mémoriser ces correspondances dans un tableau en mémoire RAM. Le numéro d'espace d'adressage est alors un indice, qui pointe sur l'entrée adéquate dans cette table. Il devrait y avoir un tableau par programme, qui serait protégé en écriture et seulement accessible par le système d'exploitation, comme le serait une table des pages. Mais ce n'est pas exactement ce qui est fait.
===L'utilisation des registres d'accès===
La différence est qu'il n'y a pas une table de correspondance par programme, mais un système à deux tableaux imbriqués. Le premier tableau possède 65 536 entrées, une par espace d'adressage supporté par le processeur. C'est donc une table partagée par tous les programmes. Par contre, la seconde table est bien unique pour chaque programme, chaque programme a la sienne. Faire ainsi permet de simplifier le partage de la mémoire. La table partagée est l''''''Access Space Table Entry''''' (ASTE), alors que la seconde table est la '''liste privée''' (''access list''), sous-entendu "privée pour un programme".
La liste privée mémorise une liste des espaces d'adressage auquel le programme peut accéder, auxquels le système d'exploitation lui a accordé l'accès. Pour chaque espace d'adressage, elle renvoie vers l'entrée de l'ASTE associée. En plus de mémoriser l'adresse de cette entrée, elle mémorise les droits d'accès que le programme a sur l'espace d'adressage associé. Par exemple, si deux programmes partagent un espace d'adressage, l'un pourra y avoir accès en lecture seule, l'autre pourra avoir un accès total. Pour cela, ils ont des droits différents dans la liste privée, mais l'entrée de l'ASTE sera la même.
[[File:Tradiction d'adresse sur la zarchitecture d'IBM.png|centre|vignette|upright=2|Traduction d'adresse sur la zarchitecture d'IBM.]]
Il y a donc une vérification des droits d'accès, suivi d'un accès à une table des super-pages globale. Si un programme veut accéder à un espace d'adressage qui lui est interdit, l'accès mémoire stoppera dans la liste privée. Le processeur utilisera l'identifiant d'espace d'adressage, mais la liste privée n'aura pas d'entrée associée. Elle lèvera alors une exception matérielle et le système d'exploitation gérera la situation, vraisemblablement en tuant le programme fautif. Mais si l'accès est autorisé, alors le programme accède à la table des super-pages et l'accès mémoire se poursuit.
Je dis une liste d'accès privée, mais il y en a deux en réalité. Mais la distinction exacte demanderait de parler de ''threads'' et de processus. Pour ceux qui sont familiers avec ces concepts, chaque ''thread'' a sa propre liste privée, avec en plus une liste par processus, partagée entre tous les ''threads''. Deux listes de 1024 entrées maximum, cela fait 2048 entrées au total. Les deux listes portent les noms de ''Dispatchable Unit Access List'' et de ''PASN-AL (Primary Address Space Access List''. Inutile de retenir ces deux termes, cependant.
Un registre d'accès mémorise un '''''Access List Entry Token''''' ou ALET. L'ALET contient l'indice d'une entrée dans la liste privée. Elle contient aussi un bit qui indique quelle liste privée utiliser, entre la ''Dispatchable Unit Access List'' et la ''PASN-AL (Primary Address Space Access List''. Précisons cependant que deux valeurs font exceptions : 0 pointe vers l'espace d'adressage principal, 1 vers l'espace d'adressage secondaire. Les autres valeurs sont configurables.
{|class="wikitable"
|+ Registre d'accès
|-
! 0000000 !! L (choix de la liste privée !! ASLEN !! ALEN
|-
| 0000000 || Choix de la liste privée || Identifiant de protection || Indice dans la liste privée
|}
Lors d'une lecture/écriture, l'ALETs est utilisé pour sélectionner l'espace d'adressage. Les valeurs 0 et 1 sont traitées telles quelles, l'espace d'adressage est déterminé automatiquement par le processeur. Pour les autres valeurs, récupérer l'adresse mémoire demande de consulter la liste d'accès privée. L'ALET est envoyé à la liste d'accès privée, et cette dernière fournit l'adresse de l'entrée de l'ASTE à consulter. Le processeur connait l'adresse de la liste privée, car son adresse mémoire est mémorisée dans les registres de contrôle numéro 2 et 5.
: Précisons que le registre d'accès numéro 0 contient obligatoirement la valeur 0, ce qui fait qu'il pointe vers l'espace d'adressage primaire.
[[File:Traduction d'adresse - trois espaces d'adressage, zarchitecture d'IBM.png|centre|vignette|upright=2|Traduction d'adresse - trois espaces d'adressage, zarchitecture d'IBM.]]
Une subtilité est que le système d'exploitation peut à tout moment modifier le contenu de la liste privée. Il se peut qu'un ALET qui correspondait à un espace d'adressage se mette à pointer vers un autre. Pour détecter cette situation, le processeur attribue un identifiant, à chaque espace d'adressage. L'identifiant est mémorisé dans les entrées de la liste privée, de l'ASTE et dans les registres d'accès (dans les bits de poids fort de l'ALET). L'identifiant est modifié dans l'ALET à chaque fois que le système d'exploitation modifie l'entrée. Si un programme fait un accès avec un registre d'accès, mais que l'identifiant ne correspond pas avec celui dans l'ALET/liste privée, c'est signe que l'espace d'adressage a été swappé, une exception matérielle est levée.
Un tel fonctionnement vous aura sans doute fait penser à une architecture à capacité, mais la z/architecture n'en est pas une. Malgré des points de ressemblance assez importants, il y a des différences fondamentales avec les architectures à capacité.
* Le z/architecture ne supporte pas la segmentation, qui est nécessaire pour les architectures à capacité. Sur la z/architecture, ce sont des espaces d'adressage qui sont sélectionnés, pas des segments.
* Les registres d'accès ne sont pas des registres de capacité, car on peut les modifier avec des instructions en espace utilisateur. En comparaison, modifier les registres de capacité ne peut se faire qu'en espace noyau, sous l'égide du système d'exploitation.
* Les droits d'accès sont dans une table séparée des pointeurs, pas concaténés à ceux-ci. La gestion des droits d'accès est gérée par les tables des pages.
* Les identifiants ALET ne peuvent pas être passés à un autre programme. Deux ALETs identiques correspondent à deux espaces d'adressages différents dans deux programmes différents. L'identifiant peut être identique, il pointera vers l'entrée de même numéro dans les deux listes privées, mais l'adresse dans l'entrée peut être différente.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=L'espace d'adressage du processeur
| prevText=L'espace d'adressage du processeur
| next=Les méthodes de synchronisation entre processeur et périphériques
| nextText=Les méthodes de synchronisation entre processeur et périphériques
}}
</noinclude>
a9ie5bcbo0307td1zj8u3m0gi84zszd
775337
775336
2026-10-16T21:15:28Z
Mewtow
31375
/* Un cas particulier : la z/architecture d'IBM */
775337
wikitext
text/x-wiki
Pour introduire ce chapitre, nous devons faire un rappel sur le concept d{{'}}'''espace d'adressage'''. Pour rappel, un espace d'adressage correspond à l'ensemble des adresses utilisables par le processeur. Par exemple, si je prends un processeur 16 bits, il peut adresser en tout 2^16 = 65536 adresses, l'ensemble de ces adresses forme son espace d'adressage. Intuitivement, on s'attend à ce qu'il y ait correspondance avec les adresses de la mémoire RAM. J'entends par là que l'adresse 1209 de l'espace d'adressage correspond à l'adresse 1209 en mémoire RAM. C'est là une hypothèse parfaitement raisonnable et on voit mal comment ce pourrait ne pas être le cas.
Mais les processeurs modernes utilisent des techniques d{{'}}'''abstraction mémoire''' qui font que ce n'est pas le cas. Avec ces techniques, l'adresse 1209 de l'espace d'adressage correspond en réalité à l'adresse 9999 en mémoire RAM, voire n'est pas en RAM. L'abstraction mémoire fait que l'espace d'adressage regroupe des adresses fictives, qui doivent être traduites en adresses mémoires réelles pour être utilisées. Les adresses de l'espace d'adressage portent le nom d{{'}}'''adresses logiques''', alors que les adresses de la mémoire RAM sont appelées '''adresses physiques'''.
L'intérêt de l'abstraction matérielle n'est pas évident. Aussi, avant de parler de comment l'abstraction mémoire fonctionne, nous allons voir à quoi elle sert. Nous allons voir qu'elle a plusieurs utilisations différentes, qui sont absolument nécessaires sur tous les ordinateurs personnels modernes. Tous les processeurs modernes la prennent en charge, les systèmes d'exploitation coopérent avec le processeur pour l'utiliser au mieux.
==L'abstraction mémoire permet plusieurs fonctionnalités complémentaires==
La fonctionnalité la plus connue est la mémoire virtuelle, dont vous avez peut-être déjà entendu parler, peut-être que le nom vous dit quelque chose. Si ce n'est pas le cas, nous la détailleront dans la suite. Elle permet concrétement à un programme d'utiliser plus de mémoire qu'il n'y a de RAM installé dans un ordinateur, en utilisant le disque dur comme solution de secours. Mais d'autres fonctionnalités moins évidentes sont permises par l'abstraction mémoire. Et la première que nous allons voir est l'abstraction des processus.
: En général, une adresse logique correspond à une seule adresse physique. Mais beaucoup de fonctionnalités avancées ne respectent pas cette règle.
===L'abstraction matérielle des processus===
Les systèmes d'exploitation modernes sont dits multi-tâche, à savoir qu'ils sont capables d'exécuter plusieurs logiciels en même temps. Et ce même si un seul processeur est présent dans l'ordinateur : les logiciels sont alors exécutés à tour de rôle. Toutefois, cela amène un paquet de problèmes qu'il faut résoudre au mieux. Par exemple, les programmes exécutés doivent se partager la mémoire RAM, ce qui ne vient pas sans problèmes. Le problème principal est que les programmes ne doivent pas lire ou écrire dans les données d'un autre, sans quoi on se retrouverait rapidement avec des problèmes. Il faut donc introduire des mécanismes d{{'}}'''isolement des processus''', pour isoler les programmes les uns des autres.
Un de ces mécanismes est l{{'}}'''abstraction matérielle des processus''', une technique qui fait que chaque programme a son propre espace d'adressage. Chaque programme a l'impression d'avoir accès à tout l'espace d'adressage, de l'adresse 0 à l'adresse maximale gérée par le processeur. Évidemment, il s'agit d'une illusion maintenue justement grâce à la traduction d'adresse. Les espaces d'adressage contiennent des adresses logiques, les adresses de la RAM sont des adresses physiques, la nécessité de l'abstraction mémoire est évidente.
Implémenter l'abstraction mémoire peut se faire de plusieurs manières. Mais dans tous les cas, il faut que la correspondance adresse logique - physique change d'un programme à l'autre. Ce qui est normal, vu que les deux processus sont placés à des endroits différents en RAM physique. La conséquence est qu'avec l'abstraction mémoire, une adresse logique correspond à plusieurs adresses physiques. Une même adresse logique dans deux processus différents correspond à deux adresses phsiques différentes, une par processus. Une adresse logique dans un processus correspondra à l'adresse physique X, la même adresse dans un autre processus correspondra à l'adresse Y.
Les adresses physiques qui partagent la même adresse logique sont alors appelées des '''adresses homonymes'''. Le choix de la bonne adresse étant réalisé par un mécanisme matériel et dépend du programme en cours. Le mécanisme pour choisir la bonne adresse dépend du processeur, mais il y en a deux grands types :
* La première consiste à utiliser l'identifiant de processus CPU, vu au chapitre précédent. C'est, pour rappel, un numéro attribué à chaque processus par le processeur. L'identifiant du processus en cours d'exécution est mémorisé dans un registre du processeur. La traduction d'adresse utilise cet identifiant, en plus de l'adresse logique, pour déterminer l'adresse physique.
* La seconde solution mémorise les correspondances adresses logiques-physique dans des tables en mémoire RAM, qui sont différentes pour chaque programme. Les tables sont accédées à chaque accès mémoire, afin de déterminer l'adresse physique.
===Le partage de la mémoire===
L'isolation des processus est très importante sur les systèmes d'exploitation modernes. Cependant, il existe quelques situations où elle doit être contournée ou du moins mise en pause. Les situations sont multiples : gestion de bibliothèques partagées, communication entre processus, usage de ''threads'', etc. Elles impliquent toutes un '''partage de mémoire''', à savoir qu'une portion de mémoire RAM est partagée entre plusieurs programmes. Le partage de mémoire est une sorte de brèche de l'isolation des processus, mais qui est autorisée car elle est utile.
Un cas intéressant est celui des '''bibliothèques partagées'''. Les bibliothèques sont des collections de fonctions regroupées ensemble, dans une seule unité de code. Un programme qui utilise une bibliothèque peut appeler n’importe quelle fonction présente dans la bibliothèque. La bibliothèque peut être simplement inclue dans le programme lui-même, on parle alors de bibliothèques statiques. De telles bibliothèques fonctionnent très bien, mais avec un petit défaut pour les bibliothèques très utilisées : plusieurs programmes qui utilisent la même bibliothèque vont chacun l'inclure dans leur code, ce qui fera doublon.
Pour éviter cela, les OS modernes gèrent des bibliothèques partagées, à savoir qu'un seul exemplaire de la bibliothèque est partagé entre plusieurs programmes. Chaque programme peut exécuter une fonction de la bibliothèque quand il le souhaite, en effectuant un branchement adéquat. Mais cela implique que la bibliothèque soit présente dans l'espace d'adressage du programme en question. Une bibliothèque est donc présente dans plusieurs espaces d'adressage, alors qu'il n'y en a qu'un seul exemplaire en mémoire RAM.
[[File:Ogg vorbis libs and application dia.svg|centre|vignette|upright=2|Exemple de bibliothèques, avec Ogg vorbis.]]
D'autres situations demandent de partager de la mémoire entre deux programmes. Par exemple, les systèmes d'exploitation modernes gèrent nativement des systèmes de '''communication inter-processus''', très utilisés par les programmes modernes pour échanger des données. Et la plupart demandant de partager un bout de mémoire entre processus, même si c'est seulement temporairement. Typiquement, deux processus partagent un intervalle d'adresse où l'un écrit les données à l'autre, l'autre lisant les données envoyées.
Une dernière utilisation de la mémoire partagée est l{{'}}'''accès direct au noyau'''. Sur les systèmes d'exploitations moderne, dans l'espace d'adressage de chaque programme, les adresses hautes sont remplies avec une partie du noyau ! Évidemment, ces adresses sont accessibles uniquement en lecture, pas en écriture. Pas question de modifier le noyau de l'OS ! De plus, il s'agit d'une portion du noyau dont on sait que la consultation ne pose pas de problèmes de sécurité.
Le programme peut lire des données dans cette portion du noyau, mais aussi exécuter les fonctions du noyau qui sont dedans. L'idée est d'éviter des appels systèmes trop fréquents. Au lieu d'effectuer un véritable appel système, avec une interruption logicielle, le programme peut exécuter des appels systèmes simplifiés, de simples appels de fonctions couplés avec un changement de niveau de privilège (passage en espace noyau nécessaire).
[[File:AMD64-canonical--48-bit.png|vignette|Répartition des adresses entre noyau (jaune/orange) et programme (verte), sur les systèmes x86-64 bits, avec des adresses physiques de 48 bits.]]
L'espace d'adressage est donc séparé en deux portions : l'OS d'un côté, le programme de l'autre. La répartition des adresses entre noyau et programme varie suivant l'OS ou le processeur utilisé. Sur les PC x86 32 bits, Linux attribuait 3 gigas pour les programmes et 1 giga pour le noyau, Windows attribuait 2 gigas à chacun. Sur les systèmes x86 64 bits, l'espace d'adressage d'un programme est coupé en trois, comme illustré ci-contre : une partie basse de 2^48 octets, une partie haute de même taille, et un bloc d'adresses invalides entre les deux. Les adresses basses sont utilisées pour le programme, les adresses hautes pour le noyau, il n'y a rien entre les deux.
Avec le partage de mémoire, plusieurs adresses logiques correspondent à la même adresse physique. Tel processus verra la zone de mémoire partagée à l'adresse X, l'autre la verra à l'adresse Y. Mais il s'agira de la même portion de mémoire physique, avec une seule adresse physique. En clair, lorsque deux processus partagent une même zone de mémoire, la zone sera mappées à des adresses logiques différentes. Les adresses logiques sont alors appelées des '''adresses synonymes''', terme qui trahit le fait qu'elles correspondent à la même adresse physique.
===La mémoire virtuelle===
Toutes les adresses ne sont pas forcément occupées par de la mémoire RAM, s'il n'y a pas assez de RAM installée. Par exemple, un processeur 32 bits peut adresser 4 gibioctets de RAM, même si seulement 3 gibioctets sont installés dans l'ordinateur. L'espace d'adressage contient donc 1 gigas d'adresses inutilisées, et il faut éviter ce surplus d'adresses pose problème.
Sans mémoire virtuelle, seule la mémoire réellement installée est utilisable. Si un programme utilise trop de mémoire, il est censé se rendre compte qu'il n'a pas accès à tout l'espace d'adressage. Quand il demandera au système d'exploitation de lui réserver de la mémoire, le système d'exploitation le préviendra qu'il n'y a plus de mémoire libre. Par exemple, si un programme tente d'utiliser 4 gibioctets sur un ordinateur avec 3 gibioctets de mémoire, il ne pourra pas. Pareil s'il veut utiliser 2 gibioctets de mémoire sur un ordinateur avec 4 gibioctets, mais dont 3 gibioctets sont déjà utilisés par d'autres programmes. Dans les deux cas, l'illusion tombe à plat.
Les techniques de '''mémoire virtuelle''' font que l'espace d'adressage est utilisable au complet, même s'il n'y a pas assez de mémoire installée dans l'ordinateur ou que d'autres programmes utilisent de la RAM. Par exemple, sur un processeur 32 bits, le programme aura accès à 4 gibioctets de RAM, même si d'autres programmes utilisent la RAM, même s'il n'y a que 2 gibioctets de RAM d'installés dans l'ordinateur.
Pour cela, on utilise une partie des mémoires de masse (disques durs) d'un ordinateur en remplacement de la mémoire physique manquante. Le système d'exploitation crée sur le disque dur un fichier, appelé le ''swapfile'' ou '''fichier de ''swap''''', qui est utilisé comme mémoire RAM supplémentaire. Il mémorise le surplus de données et de programmes qui ne peut pas être mis en mémoire RAM.
[[File:Vm1.png|centre|vignette|upright=2.0|Mémoire virtuelle et fichier de Swap.]]
Une technique naïve de mémoire virtuelle serait la suivante. Avant de l'aborder, précisons qu'il s'agit d'une technique abordée à but pédagogique, mais qui n'est implémentée nulle part tellement elle est lente et inefficace. Un espace d'adressage de 4 gigas ne contient que 3 gigas de RAM, ce qui fait 1 giga d'adresses inutilisées. Les accès mémoire aux 3 gigas de RAM se font normalement, mais l'accès aux adresses inutilisées lève une exception matérielle "Memory Unavailable". La routine d'interruption de cette exception accède alors au ''swapfile'' et récupère les données associées à cette adresse. La mémoire virtuelle est alors émulée par le système d'exploitation.
Le défaut de cette méthode est que l'accès au giga manquant est toujours très lent, parce qu'il se fait depuis le disque dur. D'autres techniques de mémoire virtuelle logicielle font beaucoup mieux, mais nous allons les passer sous silence, vu qu'on peut faire mieux, avec l'aide du matériel.
L'idée est de charger les données dont le programme a besoin dans la RAM, et de déplacer les autres sur le disque dur. Par exemple, imaginons la situation suivante : un programme a besoin de 4 gigas de mémoire, mais ne dispose que de 2 gigas de mémoire installée. On peut imaginer découper l'espace d'adressage en 2 blocs de 2 gigas, qui sont chargés à la demande. Si le programme accède aux adresses basses, on charge les 2 gigas d'adresse basse en RAM. S'il accède aux adresses hautes, on charge les 2 gigas d'adresse haute dans la RAM après avoir copié les adresses basses sur le ''swapfile''.
On perd du temps dans les copies de données entre RAM et ''swapfile'', mais on gagne en performance vu que tous les accès mémoire se font en RAM. Du fait de la localité temporelle, le programme utilise les données chargées depuis le swapfile durant un bon moment avant de passer au bloc suivant. La RAM est alors utilisée comme une sorte de cache alors que les données sont placées dans une mémoire fictive représentée par l'espace d'adressage et qui correspond au disque dur.
Mais avec cette technique, la correspondance entre adresses du programme et adresses de la RAM change au cours du temps. Les adresses de la RAM correspondent d'abord aux adresses basses, puis aux adresses hautes, et ainsi de suite. On a donc besoin d'abstraction mémoire. Les correspondances entre adresse logique et physique peuvent varier avec le temps, ce qui permet de déplacer des données de la RAM vers le disque dur ou inversement. Une adresse logique peut correspondre à une adresse physique, ou bien à une donnée swappée sur le disque dur. C'est l'unité de traduction d'adresse qui se charge de faire la différence. Si une correspondance entre adresse logique et physique est trouvée, elle l'utilise pour traduire les adresses. Si aucune correspondance n'est trouvée, alors elle laisse la main au système d'exploitation pour charger la donnée en RAM. Une fois la donnée chargée en RAM, les correspondances entre adresse logique et physiques sont modifiées de manière à ce que l'adresse logique pointe vers la donnée chargée.
===L'extension d'adressage===
Une autre fonctionnalité rendue possible par l'abstraction mémoire est l{{'}}'''extension d'adressage'''. Elle permet d'utiliser plus de mémoire que l'espace d'adressage ne le permet. Par exemple, utiliser 7 gigas de RAM sur un processeur 32 bits, dont l'espace d'adressage ne gère que 4 gigas. L'extension d'adresse est l'exact inverse de la mémoire virtuelle. La mémoire virtuelle sert quand on a moins de mémoire que d'adresses, l'extension d'adresse sert quand on a plus de mémoire que d'adresses.
Il y a quelques chapitres, nous avions vu que c'est possible via la commutation de banques. Mais l'abstraction mémoire est une méthode alternative. Que ce soit avec la commutation de banques ou avec l'abstraction mémoire, les adresses envoyées à la mémoire doivent être plus longues que les adresses gérées par le processeur. La différence est que l'abstraction mémoire étend les adresses d'une manière différente.
Une implémentation possible de l'extension d'adressage fait usage de l'abstraction matérielle des processus. Chaque processus a son propre espace d'adressage, mais ceux-ci sont placés à des endroits différents dans la mémoire physique. Par exemple, sur un ordinateur avec 16 gigas de RAM, mais un espace d'adressage de 2 gigas, on peut remplir la RAM en lançant 8 processus différents et chaque processus aura accès à un bloc de 2 gigas de RAM, pas plus, il ne peut pas dépasser cette limite. Ainsi, chaque processus est limité par son espace d'adressage, mais on remplit la mémoire avec plusieurs processus, ce qui compense. Il s'agit là de l'implémentation la plus simple, qui a en plus l'avantage d'avoir la meilleure compatibilité logicielle. De simples changements dans le système d'exploitation suffisent à l'implémenter.
[[File:Extension de l'espace d'adressage.png|centre|vignette|upright=1.5|Extension de l'espace d'adressage]]
Un autre implémentation donne plusieurs espaces d'adressage différents à chaque processus, et a donc accès à autant de mémoire que permis par la somme de ces espaces d'adressage. Par exemple, sur un ordinateur avec 16 gigas de RAM et un espace d'adressage de 4 gigas, un programme peut utiliser toute la RAM en utilisant 4 espaces d'adressage distincts. On passe d'un espace d'adressage à l'autre en changeant la correspondance adresse logique-physique. L'inconvénient est que la compatibilité logicielle est assez mauvaise. Modifier l'OS ne suffit pas, les programmeurs doivent impérativement concevoir leurs programmes pour qu'ils utilisent explicitement plusieurs espaces d'adressage.
Les deux implémentations font usage des adresses logiques homonymes, mais à l'intérieur d'un même processus. Pour rappel, cela veut dire qu'une adresse logique correspond à des adresses physiques différentes. Rien d'étonnant vu qu'on utilise plusieurs espaces d'adressage, comme pour l'abstraction des processus, sauf que cette fois-ci, on a plusieurs espaces d'adressage par processus. Prenons l'exemple où on a 8 gigas de RAM sur un processeur 32 bits, dont l'espace d'adressage ne gère que 4 gigas. L'idée est qu'une adresse correspondra à une adresse dans les premiers 4 gigas, ou dans les seconds 4 gigas. L'adresse logique X correspondra d'abord à une adresse physique dans les premiers 4 gigas, puis à une adresse physique dans les seconds 4 gigas.
===La protection mémoire===
La '''protection mémoire''' regroupe des techniques très différentes les unes des autres, qui visent à améliorer la sécurité des programmes et des systèmes d'exploitation. Elles visent à empêcher de lire, d'écrire ou d'exécuter certaines portions de mémoire. Sans elle, les programmes peuvent techniquement lire ou écrire les données des autres, ce qui causent des situations non-prévues par le programmeur, avec des conséquences qui vont d'un joli plantage à des failles de sécurité dangereuses.
La première technique de protection mémoire est l{{'}}'''isolation des processus''', qu'on a vue plus haut. Elle garantit que chaque programme n'a accès qu'à certaines portions dédiées de la mémoire et rend le reste de la mémoire inaccessible en lecture et en écriture. Le système d'exploitation attribue à chaque programme une ou plusieurs portions de mémoire rien que pour lui, auquel aucun autre programme ne peut accéder. Un tel programme, isolé des autres, s'appelle un '''processus''', d'où le nom de cet objectif. Toute tentative d'accès à une partie de la mémoire non autorisée déclenche une exception matérielle (rappelez-vous le chapitre sur les interruptions) qui est traitée par une routine du système d'exploitation. Généralement, le programme fautif est sauvagement arrêté et un message d'erreur est affiché à l'écran.
La '''protection de l'espace exécutable''' empêche d’exécuter quoique ce soit provenant de certaines zones de la mémoire. En effet, certaines portions de la mémoire sont censées contenir uniquement des données, sans aucun programme ou code exécutable. Cependant, des virus informatiques peuvent se cacher dedans et d’exécuter depuis celles-ci. Ou encore, des failles de sécurités peuvent permettre à un attaquant d'injecter du code exécutable malicieux dans des données, ce qui peut lui permettre de lire les données manipulées par un programme, prendre le contrôle de la machine, injecter des virus, ou autre. Pour éviter cela, le système d'exploitation peut marquer certaines zones mémoire comme n'étant pas exécutable. Toute tentative d’exécuter du code localisé dans ces zones entraîne la levée d'une exception ou d'une erreur et le système d'exploitation réagit en conséquence. Là encore, le processeur doit détecter les exécutions non autorisées.
D'autres méthodes de protection mémoire visent à limiter des actions dangereuses. Pour cela, le processeur et l'OS gèrent des '''droits d'accès''', qui interdisent certaines actions pour des programmes non-autorisés. Lorsqu'on exécute une opération interdite, le système d’exploitation et/ou le processeur réagissent en conséquence. La première technique de ce genre n'est autre que la séparation entre espace noyau et utilisateur, vue dans le chapitre sur les interruptions. Mais il y en a d'autres, comme nous le verrons dans ce chapitre.
==La MMU==
La traduction des adresses logiques en adresses physiques se fait par un circuit spécialisé, appelé la '''''Memory Management Unit''''' (MMU). De nos jours, elle est intégrée dans le processeur, à la suite de l'unité mémoire. Mais il a existé des processeurs avec une MMU externe, soudée sur la carte mère.
La traduction des adresses logiques en adresses physiques demande d'accéder à des données sont en mémoire RAM, qui sont gérés par le système d'exploitation. Aussi, les processeurs modernes incorporent des mémoires caches appelées des '''''Translation Lookaside Buffers''''', ou encore TLB. Nous nous pouvons pas parler des TLB pour le moment, car nous n'avons pas encore abordé le chapitre sur les mémoires caches, mais un chapitre entier sera dédié aux TLB d'ici peu.
[[File:MMU principle updated.png|centre|vignette|upright=2|MMU.]]
===Les MMU intégrées au processeur===
D'ordinaire, la MMU est intégrée au processeur. Et elle peut l'être de deux manières. La première en fait un circuit séparé, relié au bus d'adresse. La seconde fusionne la MMU avec l'unité de calcul d'adresse. La première solution est surtout utilisée avec une technique d'abstraction mémoire appelée la pagination, alors que l'autre l'est avec une autre méthode appelée la segmentation. La raison est que la traduction d'adresse avec la segmentation est assez simple : elle demande d'additionner le contenu d'un registre avec l'adresse logique, ce qui est le genre de calcul qu'une unité de calcul d'adresse sait déjà faire. La fusion est donc assez évidente.
Pour donner un exemple, l'Intel 8086 fusionnait l'unité de calcul d'adresse et la MMU. Précisément, il utilisait un même additionneur pour incrémenter le ''program counter'' et effectuer des calculs d'adresse liés à la segmentation. Il aurait été logique d'ajouter les pointeurs de pile avec, mais ce n'était pas possible. La raison est que le pointeur de pile ne peut pas être envoyé directement sur le bus d'adresse, vu qu'il doit passer par une phase de traduction en adresse physique liée à la segmentation.
[[File:80186 arch.png|centre|vignette|upright=2|Intel 8086, microarchitecture.]]
===Les MMU séparées du processeur, sur la carte mère===
Avant d'être intégrées au processeur, les MMU étaient des circuits séparés, placés sur la carte mère, entre le processeur et la mémoire. Elle étaient connectées en plein milieu du bus d'adresse. A savoir que le bus d'adresse entrait dans la MMU et en ressortait. Le processeur envoyait des adresses à la MMU, qui renvoyait une adresse physique. La MMU était facultative, à savoir que le système pouvait fonctionner sans MMU d'installée, mais l'abstraction mémoire n’était alors pas disponible.
Par exemple, les processeurs Motorola 68000 et 68010 pouvaient être combinés avec une MMU de type Motorola 68451. Elle supportait des versions simplifiées de la segmentation et de la pagination. Au minimum, elle ajoutait un support de la protection mémoire contre certains accès non-autorisés. La gestion de la mémoire virtuelle proprement dit n'était possible que si le processeur utilisé était un Motorola 68010, en raison de la manière dont le 68000 gérait ses accès mémoire. La MMU 68451 gérait un espace d'adressage de 16 mébioctets, découpé en maximum 32 pages/segments. On pouvait dépasser cette limite de 32 segments/pages en combinant plusieurs 68451.
Le Motorola 68851 était une MMU qui était prévue pour fonctionner de paire avec le Motorola 68020. Elle gérait la pagination pour un espace d'adressage de 32 bits.
Les processeurs suivants, les 68030, 68040, et 68060, avaient une MMU interne au processeur.
==La relocation matérielle==
Pour rappel, les systèmes d'exploitation moderne permettent de lancer plusieurs programmes en même temps et les laissent se partager la mémoire. Dans le cas le plus simple, qui n'est pas celui des OS modernes, le système d'exploitation découpe la mémoire en blocs d'adresses contiguës qui sont appelés des '''segments''', ou encore des ''partitions mémoire''. Les segments correspondent à un bloc de mémoire RAM. C'est-à-dire qu'un segment de 259 mébioctets sera un segment continu de 259 mébioctets dans la mémoire physique comme dans la mémoire logique. Dans ce qui suit, un segment contient un programme en cours d'exécution, comme illustré ci-dessous.
[[File:CPT Memory Addressable.svg|centre|vignette|upright=2|Espace d'adressage segmenté.]]
Le système d'exploitation mémorise la position de chaque segment en mémoire, ainsi que d'autres informations annexes. Le tout est regroupé dans la '''table de segment''', un tableau dont chaque case est attribuée à un programme/segment. La table des segments est un tableau numéroté, chaque segment ayant un numéro qui précise sa position dans le tableau. Chaque case, chaque entrée, contient un '''descripteur de segment''' qui regroupe plusieurs informations sur le segment : son adresse de base, sa taille, diverses informations.
===La relocation avec la relocation matérielle : le registre de base===
Un segment peut être placé n'importe où en RAM physique et sa position en RAM change à chaque exécution. Le programme est chargé à une adresse, celle du début du segment, qui change à chaque chargement du programme. Et toutes les adresses utilisées par le programme doivent être corrigées lors du chargement du programme, généralement par l'OS. Cette correction s'appelle la '''relocation''', et elle consiste à ajouter l'adresse de début du segment à chaque adresse manipulée par le programme.
[[File:Relocation assistée par matériel.png|centre|vignette|upright=2.5|Relocation.]]
La relocation matérielle fait que la relocation est faite par le processeur, pas par l'OS. La relocation est intégrée dans le processeur par l'intégration d'un registre : le '''registre de base''', aussi appelé '''registre de relocation'''. Il mémorise l'adresse à laquelle commence le segment, la première adresse du programme. Pour effectuer la relocation, le processeur ajoute automatiquement l'adresse de base à chaque accès mémoire, en allant la chercher dans le registre de relocation.
[[File:Registre de base de segment.png|centre|vignette|upright=2|Registre de base de segment.]]
Le processeur s'occupe de la relocation des segments et le programme compilé n'en voit rien. Pour le dire autrement, les programmes manipulent des adresses logiques, qui sont traduites par le processeur en adresses physiques. La traduction se fait en ajoutant le contenu du registre de relocation à l'adresse logique. De plus, cette méthode fait que chaque programme a son propre espace d'adressage.
[[File:CPU created logical address presentation.png|centre|vignette|upright=2|Traduction d'adresse avec la relocation matérielle.]]
Le système d'exploitation mémorise les adresses de base pour chaque programme, dans la table des segments. Le registre de base est mis à jour automatiquement lors de chaque changement de segment. Pour cela, le registre de base est accessible via certaines instructions, accessibles en espace noyau, plus rarement en espace utilisateur. Le registre de segment est censé être adressé implicitement, vu qu'il est unique. Si ce n'est pas le cas, il est possible d'écrire dans ce registre de segment, qui est alors adressable.
===La protection mémoire avec la relocation matérielle : le registre limite===
Sans restrictions supplémentaires, la taille maximale d'un segment est égale à la taille complète de l'espace d'adressage. Sur les processeurs 32 bits, un segment a une taille maximale de 2^32 octets, soit 4 gibioctets. Mais il est possible de limiter la taille du segment à 2 gibioctets, 1 gibioctet, 64 Kibioctets, ou toute autre taille. La limite est définie lors de la création du segment, mais elle peut cependant évoluer au cours de l'exécution du programme, grâce à l'allocation mémoire.
Le processeur vérifie à chaque accès mémoire que celui-ci se fait bien dans le segment, qu'il ne déborde pas en-dehors. C'est possible qu'une adresse calculée sorte du segment, à la suite d'un bug ou d'une erreur de programmation, voire pire. Et le processeur doit éviter de tels '''débordements de segments'''.
A chaque accès mémoire, le processeur compare l'adresse accédée et vérifie qu'elle est bien dans le segment. Pour cela, il y a deux solutions. La première part du principe que le segment est placé en mémoire entre l'adresse de base et l'adresse limite. Il suffit de mémoriser l'adresse limite, l'adresse physique à ne pas dépasser. Une autre solution mémorise la taille du segment. La table des segments doit donc mémoriser, en plus de l'adresse de base : soit l'adresse maximale du segment, soit la taille du segment. D'autres informations peuvent être ajoutées, comme on le verra plus tard, mais cela complexifie la table des segments.
De plus, le processeur se voit ajouter un '''registre limite''', qui mémorise soit la taille du segment, soit l'adresse limite. Les deux registres, base et limite, sont utilisés pour vérifier si un programme qui lit/écrit de la mémoire en-dehors de son segment attitré : au-delà pour le registre limite, en-deça pour le registre de base. Le processeur vérifie pour chaque accès mémoire ne déborde pas au-delà du segment qui lui est allouée, ce qui n'arrive que si l'adresse d'accès dépasse la valeur du registre limite. Pour les accès en-dessous du segment, il suffit de vérifier si l'addition de relocation déborde, tout débordement signifiant erreur de protection mémoire.
[[File:Registre limite.png|centre|vignette|upright=2|Registre limite]]
Utiliser la taille du segment a de nombreux avantages. L'un d'entre eux se manifeste quand on déplace un segment en mémoire RAM. Le descripteur doit alors être mis à jour, et c'est plus facile quand on utilise la taille du segment. Si on utilise l'adresse limite, il faut mettre à jour à la fois l'adresse de base et l'adresse limite, dans le descripteur. En utilisant la taille, seule l'adresse de base doit être modifiée, vu que le segment n'a pas changé de taille. Un autre avantage est lié aux performances, mais nous devons faire un détour pour le comprendre.
La taille du segment est équivalent à l'adresse logique maximale possible. Par exemple, si un segment fait 256 octets, les adresses logiques possibles vont de 0 à 255, 256 est donc à la fois la taille du segment et l'adresse logique à partir de laquelle on déborde du segment. Et cela marche si on remplace 256 par n'importe quelle valeur : vu que le segment commence à l'adresse 0, sa taille en octets indique l'adresse de dépassement. Interpréter la taille du segment comme une adresse logique fait que les tests avec le registre limite sont plus performants, voyons pourquoi.
En utilisant l'adresse physique limite, on doit faire la relocation, puis comparer l'adresse calculée avec l'adresse limite. Le calcul d'adresse doit se faire avant la vérification. En utilisant la taille, on doit comparer l'adresse logique avec la taille du segment. On peut alors faire le test de débordement avant ou pendant la relocation. Les deux peuvent être faits en parallèle, dans deux circuits distincts, ce qui améliore un peu le temps d'un accès mémoire. Quelques processeurs en ont profité, mais on verra cela dans la section sur la segmentation.
[[File:Comparaison entre adresse limite physique et logique.png|centre|vignette|upright=2|Comparaison entre adresse limite physique et logique]]
Les registres de base et limite sont altérés uniquement par le système d'exploitation et ne sont accessibles qu'en espace noyau. Lorsque le système d'exploitation charge un programme, ou reprend son exécution, il charge les adresses de début/fin du segment dans ces registres. D'ailleurs, ces deux registres doivent être sauvegardés et restaurés lors de chaque interruption. Par contre, et c'est assez évident, ils ne le sont pas lors d'un appel de fonction. Cela fait une différence de plus entre interruption et appels de fonctions.
: Il faut noter que le registre limite et le registre de base sont parfois fusionnés en un seul registre, qui contient un descripteur de segment tout entier.
Pour information, la relocation matérielle avec un registre limite a été implémentée sur plusieurs processeurs assez anciens, notamment sur les anciens supercalculateurs de marque CDC. Un exemple est le fameux CDC 6600, qui implémentait cette technique.
===La mémoire virtuelle avec la relocation matérielle===
Il est possible d'implémenter la mémoire virtuelle avec la relocation matérielle. Pour cela, il faut swapper des segments entiers sur le disque dur. Les segments sont placés en mémoire RAM et leur taille évolue au fur et à mesure que les programmes demandent du rab de mémoire RAM. Lorsque la mémoire est pleine, ou qu'un programme demande plus de mémoire que disponible, des segments entiers sont sauvegardés dans le ''swapfile'', pour faire de la place.
Faire ainsi de demande juste de mémoriser si un segment est en mémoire RAM ou non, ainsi que la position des segments swappés dans le ''swapfile''. Pour cela, il faut modifier la table des segments, afin d'ajouter un '''bit de swap''' qui précise si le segment en question est swappé ou non. Lorsque le système d'exploitation veut swapper un segment, il le copie dans le ''swapfile'' et met ce bit à 1. Lorsque l'OS recharge ce segment en RAM, il remet ce bit à 0. La gestion de la position des segments dans le ''swapfile'' est le fait d'une structure de données séparée de la table des segments.
L'OS exécute chaque programme l'un après l'autre, à tour de rôle. Lorsque le tour d'un programme arrive, il consulte la table des segments pour récupérer les adresses de base et limite, mais il vérifie aussi le bit de swap. Si le bit de swap est à 0, alors l'OS se contente de charger les adresses de base et limite dans les registres adéquats. Mais sinon, il démarre une routine d'interruption qui charge le segment voulu en RAM, depuis le ''swapfile''. C'est seulement une fois le segment chargé que l'on connait son adresse de base/limite et que le chargement des registres de relocation peut se faire.
Un défaut évident de cette méthode est que l'on swappe des programmes entiers, qui sont généralement assez imposants. Les segments font généralement plusieurs centaines de mébioctets, pour ne pas dire plusieurs gibioctets, à l'époque actuelle. Ils étaient plus petits dans l'ancien temps, mais la mémoire était alors plus lente. Toujours est-il que la copie sur le disque dur des segments est donc longue, lente, et pas vraiment compatible avec le fait que les programmes s'exécutent à tour de rôle. Et ca explique pourquoi la relocation matérielle n'est presque jamais utilisée avec de la mémoire virtuelle.
===L'extension d'adressage avec la relocation matérielle===
Passons maintenant à la dernière fonctionnalité implémentable avec la traduction d'adresse : l'extension d'adressage. Elle permet d'utiliser plus de mémoire que ne le permet l'espace d'adressage. Par exemple, utiliser plus de 64 kibioctets de mémoire sur un processeur 16 bits. Pour cela, les adresses envoyées à la mémoire doivent être plus longues que les adresses gérées par le processeur.
L'extension des adresses se fait assez simplement avec la relocation matérielle : il suffit que le registre de base soit plus long. Prenons l'exemple d'un processeur aux adresses de 16 bits, mais qui est reliée à un bus d'adresse de 24 bits. L'espace d'adressage fait juste 64 kibioctets, mais le bus d'adresse gère 16 mébioctets de RAM. On peut utiliser les 16 mébioctets de RAM à une condition : que le registre de base fasse 24 bits, pas 16.
Un défaut de cette approche est qu'un programme ne peut pas utiliser plus de mémoire que ce que permet l'espace d'adressage. Mais par contre, on peut placer chaque programme dans des portions différentes de mémoire. Imaginons par exemple que l'on ait un processeur 16 bits, mais un bus d'adresse de 20 bits. Il est alors possible de découper la mémoire en 16 blocs de 64 kibioctets, chacun attribué à un segment/programme, qu'on sélectionne avec les 4 bits de poids fort de l'adresse. Il suffit de faire démarrer les segments au bon endroit en RAM, et cela demande juste que le registre de base le permette. C'est une sorte d'émulation de la commutation de banques.
==La segmentation en mode réel des processeurs x86==
Avant de passer à la suite, nous allons voir la technique de segmentation de l'Intel 8086, un des tout premiers processeurs 16 bits. Il s'agissait d'une forme très simple de segmentation, sans aucune forme de protection mémoire, ni même de mémoire virtuelle, ce qui le place à part des autres formes de segmentation. Il s'agit d'une amélioration de la relocation matérielle, qui avait pour but de permettre d'utiliser plus de 64 kibioctets de mémoire, ce qui était la limite maximale sur les processeurs 16 bits de l'époque.
Par la suite, la segmentation s'améliora et ajouta un support complet de la mémoire virtuelle et de la protection mémoire. L'ancienne forme de segmentation fut alors appelé le '''mode réel''', et la nouvelle forme de segmentation fut appelée le '''mode protégé'''. Le mode protégé rajoute la protection mémoire, en ajoutant des registres limite et une gestion des droits d'accès aux segments, absents en mode réel. De plus, il ajoute un support de la mémoire virtuelle grâce à l'utilisation d'une des segments digne de ce nom, table qui est absente en mode réel ! Pour le moment, voyons le mode réel.
===Les segments en mode réel===
[[File:Typical computer data memory arrangement.png|vignette|upright=0.5|Typical computer data memory arrangement]]
La segmentation en mode réel sépare la pile, le tas, le code machine et les données constantes dans quatre segments distincts.
* Le segment '''''text''''', qui contient le code machine du programme, de taille fixe.
* Le segment '''''data''''' contient des données de taille fixe qui occupent de la mémoire de façon permanente, des constantes, des variables globales, etc.
* Le segment pour la '''pile''', de taille variable.
* le reste est appelé le '''tas''', de taille variable.
Un point important est que sur ces processeurs, il n'y a pas de table des segments proprement dit. Chaque programme gére de lui-même les adresses de base des segments qu'il manipule. Il n'est en rien aidé par une table des segments gérée par le système d'exploitation.
===Les registres de segments en mode réel===
Chaque segment subit la relocation indépendamment des autres. Pour cela, le processeur intégre plusieurs registres de base, un par segment. Notons que cette solution ne marche que si le nombre de segments par programme est limité, à une dizaine de segments tout au plus. Les processeurs x86 utilisaient cette méthode, et n'associaient que 4 à 6 registres de segments par programme.
Les processeurs 8086 et le 286 avaient quatre registres de segment : un pour le code, un autre pour les données, et un pour la pile, le quatrième étant un registre facultatif laissé à l'appréciation du programmeur. Ils sont nommés CS (''code segment''), DS (''data segment''), SS (''Stack segment''), et ES (''Extra segment''). Le 386 rajouta deux registres, les registres FS et GS, qui sont utilisés pour les segments de données. Les processeurs post-386 ont donc 6 registres de segment.
Les registres CS et SS sont adressés implicitement, en fonction de l'instruction exécutée. Les instructions de la pile manipulent le segment associé à la pile, le chargement des instructions se fait dans le segment de code, les instructions arithmétiques et logiques vont chercher leurs opérandes sur le tas, etc. Et donc, toutes les instructions sont chargées depuis le segment pointé par CS, les instructions de gestion de la pile (PUSH et POP) utilisent le segment pointé par SS.
Les segments DS et ES sont, eux aussi, adressés implicitement. Pour cela, les instructions LOAD/STORE sont dupliquées : il y a une instruction LOAD pour le segment DS, une autre pour le segment ES. D'autres instructions lisent leurs opérandes dans un segment par défaut, mais on peut changer ce choix par défaut en précisant le segment voulu. Un exemple est celui de l'instruction CMPSB, qui compare deux octets/bytes : le premier est chargé depuis le segment DS, le second depuis le segment ES.
Un autre exemple est celui de l'instruction MOV avec un opérande en mémoire. Elle lit l'opérande en mémoire depuis le segment DS par défaut. Il est possible de préciser le segment de destination si celui-ci n'est pas DS. Par exemple, l'instruction MOV [A], AX écrit le contenu du registre AX dans l'adresse A du segment DS. Par contre, l'instruction MOV ES:[A], copie le contenu du registre AX das l'adresse A, mais dans le segment ES.
===La traduction d'adresse en mode réel===
La segmentation en mode réel a pour seul but de permettre à un programme de dépasser la limite des 64 KB autorisée par les adresses de 16 bits. L'idée est que chaque segment a droit à son propre espace de 64 KB. On a ainsi 64 Kb pour le code machine, 64 KB pour la pile, 64 KB pour un segment de données, etc. Les registres de segment mémorisaient la base du segment, les adresses calculées par l'ALU étant des ''offsets''. Ce sont tous des registres de 16 bits, mais ils ne mémorisent pas des adresses physiques de 16 bits, comme nous allons le voir.
[[File:Table des segments dans un banc de registres.png|centre|vignette|upright=2|Table des segments dans un banc de registres.]]
L'Intel 8086 utilisait des adresses de 20 bits, ce qui permet d'adresser 1 mébioctet de RAM. Vous pouvez vous demander comment on peut obtenir des adresses de 20 bits alors que les registres de segments font tous 16 bits ? Cela tient à la manière dont sont calculées les adresses physiques. Le registre de segment n'est pas additionné tel quel avec le décalage : à la place, le registre de segment est décalé de 4 rangs vers la gauche. Le décalage de 4 rangs vers la gauche fait que chaque segment a une adresse qui est multiple de 16. Le fait que le décalage soit de 16 bits fait que les segments ont une taille de 64 kibioctets.
{|class="wikitable"
|-
| <code> </code><code style="background:#DED">0000 0110 1110 1111</code><code>0000</code>
| Registre de segment -
| 16 bits, décalé de 4 bits vers la gauche
|-
| <code>+ </code><code style="background:#DDF">0001 0010 0011 0100</code>
| Décalage/''Offset''
| 16 bits
|-
| colspan="3" |
|-
| <code> </code><code style="background:#FDF">0000 1000 0001 0010 0100</code>
| Adresse finale
| 20 bits
|}
Vous aurez peut-être remarqué que le calcul peut déborder, dépasser 20 bits. Mais nous reviendrons là-dessus plus bas. L'essentiel est que la MMU pour la segmentation en mode réel se résume à quelques registres et des additionneurs/soustracteurs.
Un exemple est l'Intel 8086, un des tout premier processeur Intel. Le processeur était découpé en deux portions : l'interface mémoire et le reste du processeur. L'interface mémoire est appelée la '''''Bus Interface Unit''''', et le reste du processeur est appelé l{{'}}'''''Execution Unit'''''. L'interface mémoire contenait les registres de segment, au nombre de 4, ainsi qu'un additionneur utilisé pour traduire les adresses logiques en adresses physiques. Elle contenait aussi une file d'attente où étaient préchargées les instructions.
Sur le 8086, la MMU est fusionnée avec les circuits de gestion du ''program counter''. Les registres de segment sont regroupés avec le ''program counter'' dans un même banc de registres, et un additionneur unique est utilisé à la fois à incrémenter le ''program counter'' et pour gérer la segmentation. L'additionneur est donc mutualisé entre segmentation et ''program counter''. En somme, il n'y a pas vraiment de MMU dédiée, mais un super-circuit en charge du Fetch et de la mémoire virtuelle, ainsi que du préchargement des instructions. Nous en reparlerons au chapitre suivant.
[[File:80186 arch.png|centre|vignette|upright=2.5|Architecture du 8086, du 80186 et de ses variantes.]]
La MMU du 286 était fusionnée avec l'unité de calcul d'adresse. Elle contient les registres de segments, un comparateur pour détecter les accès hors-segment, et plusieurs additionneurs. Il y a un additionneur pour les calculs d'adresse proprement dit, suivi d'un additionneur pour la relocation.
[[File:Intel i80286 arch.svg|centre|vignette|upright=3|Intel i80286 arch]]
===La segmentation en mode réel accepte plusieurs segments de code/données===
Les programmes peuvent parfaitement répartir leur code machine dans plusieurs segments de code. La limite de 64 KB par segment est en effet assez limitante, et il n'était pas rare qu'un programme stocke son code dans deux ou trois segments. Il en est de même avec les données, qui peuvent être réparties dans deux ou trois segments séparés. La seule exception est la pile : elle est forcément dans un segment unique et ne peut pas dépasser 64 KB.
Pour gérer plusieurs segments de code/donnée, il faut changer de segment à la volée suivant les besoins, en modifiant les registres de segment. Il s'agit de la technique de '''commutation de segment'''. Pour cela, tous les registres de segment, à l'exception de CS, peuvent être altérés par une instruction d'accès mémoire, soit avec une instruction MOV, soit en y copiant le sommet de la pile avec une instruction de dépilage POP. L'absence de sécurité fait que la gestion de ces registres est le fait du programmeur, qui doit redoubler de prudence pour ne pas faire n'importe quoi.
Pour le code machine, le répartir dans plusieurs segments posait des problèmes au niveau des branchements. Si la plupart des branchements sautaient vers une instruction dans le même segment, quelques rares branchements sautaient vers du code machine dans un autre segment. Intel avait prévu le coup et disposait de deux instructions de branchement différentes pour ces deux situations : les '''''near jumps''''' et les '''''far jumps'''''. Les premiers sont des branchements normaux, qui précisent juste l'adresse à laquelle brancher, qui correspond à la position de la fonction dans le segment. Les seconds branchent vers une instruction dans un autre segment, et doivent préciser deux choses : l'adresse de base du segment de destination, et la position de la destination dans le segment. Le branchement met à jour le registre CS avec l'adresse de base, avant de faire le branchement. Ces derniers étaient plus lents, car on n'avait pas à changer de segment et mettre à jour l'état du processeur.
Il y avait la même pour l'instruction d'appel de fonction, avec deux versions de cette instruction. La première version, le '''''near call''''' est un appel de fonction normal, la fonction appelée est dans le segment en cours. Avec la seconde version, le '''''far call''''', la fonction appelée est dans un segment différent. L'instruction a là aussi besoin de deux opérandes : l'adresse de base du segment de destination, et la position de la fonction dans le segment. Un ''far call'' met à jour le registre CS avec l'adresse de base, ce qui fait que les ''far call'' sont plus lents que les ''near call''. Il existe aussi la même chose, pour les instructions de retour de fonction, avec une instruction de retour de fonction normale et une instruction de retour qui renvoie vers un autre segment, qui sont respectivement appelées '''''near return''''' et '''''far return'''''. Là encore, il faut préciser l'adresse du segment de destination dans le second cas.
La même chose est possible pour les segments de données. Sauf que cette fois-ci, ce sont les pointeurs qui sont modifiés. pour rappel, les pointeurs sont, en programmation, des variables qui contiennent des adresses. Lors de la compilation, ces pointeurs sont placés soit dans un registre, soit dans les instructions (adressage absolu), ou autres. Ici, il existe deux types de pointeurs, appelés '''''near pointer''''' et '''''far pointer'''''. Vous l'avez deviné, les premiers sont utilisés pour localiser les données dans le segment en cours d'utilisation, alors que les seconds pointent vers une donnée dans un autre segment. Là encore, la différence est que le premier se contente de donner la position dans le segment, alors que les seconds rajoutent l'adresse de base du segment. Les premiers font 16 bits, alors que les seconds en font 32 : 16 bits pour l'adresse de base et 16 pour l{{'}}''offset''.
===L'occupation de l'espace d'adressage par les segments===
Nous venons de voir qu'un programme pouvait utiliser plus de 4-6 segments, avec la commutation de segment. Mais d'autres programmes faisaient l'inverse, à savoir qu'ils se débrouillaient avec seulement 1 ou 2 segments. Suivant le nombre de segments utilisés, la configuration des registres n'était pas la même. Les configurations possibles sont appelées des ''modèle mémoire'', et il y en a en tout 6. En voici la liste :
{| class="wikitable"
|-
! Modèle mémoire !! Configuration des segments !! Configuration des registres || Pointeurs utilisés || Branchements utilisés
|-
| Tiny* || Segment unique pour tout le programme || CS=DS=SS || ''near'' uniquement || ''near'' uniquement
|-
| Small || Segment de donnée séparé du segment de code, pile dans le segment de données || DS=SS || ''near'' uniquement || ''near'' uniquement
|-
| Medium || Plusieurs segments de code unique, un seul segment de données || CS, DS et SS sont différents || ''near'' et ''far'' || ''near'' uniquement
|-
| Compact || Segment de code unique, plusieurs segments de données || CS, DS et SS sont différents || ''near'' uniquement || ''near'' et ''far''
|-
| Large || Plusieurs segments de code, plusieurs segments de données || CS, DS et SS sont différents || ''near'' et ''far'' || ''near'' et ''far''
|}
Un programme est censé utiliser maximum 4-6 segments de 64 KB, ce qui permet d'adresser maximum 64 * 6 = 384 KB de RAM, soit bien moins que le mébioctet de mémoire théoriquement adressable. Mais ce défaut est en réalité contourné par la commutation de segment, qui permettait d'adresser la totalité de la RAM si besoin. Une second manière de contourner cette limite est que plusieurs processus peuvent s'exécuter sur un seul processeur, si l'OS le permet. Ce n'était pas le cas à l'époque du DOS, qui était un OS mono-programmé, mais c'était en théorie possible. La limite est de 6 segments par programme/processus, en exécuter plusieurs permet d'utiliser toute la mémoire disponible rapidement.
[[File:Overlapping realmode segments.svg|vignette|Segments qui se recouvrent en mode réel.]]
Vous remarquerez qu'avec des registres de segments de 16 bits, on peut gérer 65536 segments différents, chacun de 64 KB. Et 65 536 segments de 64 kibioctets, ça ne rentre pas dans le mébioctet de mémoire permis avec des adresses de 20 bits. La raison est que plusieurs couples segment+''offset'' pointent vers la même adresse. En tout, chaque adresse peut être adressée par 4096 couples segment+''offset'' différents.
L'avantage de cette méthode est que des segments peuvent se recouvrir, à savoir que la fin de l'un se situe dans le début de l'autre, comme illustré ci-contre. Cela permet en théorie de partager de la mémoire entre deux processus. Mais la technique est tout sauf pratique et est donc peu utilisée. Elle demande de placer minutieusement les segments en RAM, et les données à partager dans les segments. En pratique, les programmeurs et OS utilisent des segments qui ne se recouvrent pas et sont disjoints en RAM.
Le nombre maximal de segments disjoints se calcule en prenant la taille de la RAM, qu'on divise par la taille d'un segment. Le calcul donne : 1024 kibioctets / 64 kibioctets = 16 segments disjoints. Un autre calcul prend le nombre de segments divisé par le nombre d'adresses aliasées, ce qui donne 65536 / 4096 = 16. Seulement 16 segments, c'est peu. En comptant les segments utilisés par l'OS et ceux utilisés par le programme, la limite est vite atteinte si le programme utilise la commutation de segment.
===Le mode réel sur les 286 et plus : la ligne d'adresse A20===
Pour résumer, le registre de segment contient des adresses de 20 bits, dont les 4 bits de poids faible sont à 0. Et il se voit ajouter un ''offset'' de 16 bits. Intéressons-nous un peu à l'adresse maximale que l'on peut calculer avec ce système. Nous allons l'appeler l{{'}}'''adresse maximale de segmentation'''. Elle vaut :
{|class="wikitable"
|-
| <code> </code><code style="background:#DED">1111 1111 1111 1111</code><code>0000</code>
| Registre de segment -
| 16 bits, décalé de 4 bits vers la gauche
|-
| <code>+ </code><code style="background:#DDF">1111 1111 1111 1111</code>
| Décalage/''Offset''
| 16 bits
|-
| colspan="3" |
|-
| <code> </code><code style="background:#FDF">1 0000 1111 1111 1110 1111</code>
| Adresse finale
| 20 bits
|}
Le résultat n'est pas l'adresse maximale codée sur 20 bits, car l'addition déborde. Elle donne un résultat qui dépasse l'adresse maximale permis par les 20 bits, il y a un 21ème bit en plus. De plus, les 20 bits de poids faible ont une valeur bien précise. Ils donnent la différence entre l'adresse maximale permise sur 20 bit, et l'adresse maximale de segmentation. Les bits 1111 1111 1110 1111 traduits en binaire donnent 65 519; auxquels il faut ajouter l'adresse 1 0000 0000 0000 0000. En tout, cela fait 65 520 octets adressables en trop. En clair : on dépasse la limite du mébioctet de 65 520 octets. Le résultat est alors très différent selon que l'on parle des processeurs avant le 286 ou après.
Avant le 286, le bus d'adresse faisait exactement 20 bits. Les adresses calculées ne pouvaient pas dépasser 20 bits. L'addition générait donc un débordement d'entier, géré en arithmétique modulaire. En clair, les bits de poids fort au-delà du vingtième sont perdus. Le calcul de l'adresse débordait et retournait au début de la mémoire, sur les 65 520 premiers octets de la mémoire RAM.
[[File:IBM PC Memory areas.svg|vignette|IBM PC Memory Map, la ''High memory area'' est en jaune.]]
Le 80286 en mode réel gère des adresses de base de 24 bits, soit 4 bits de plus que le 8086. Le résultat est qu'il n'y a pas de débordement. Les bits de poids fort sont conservés, même au-delà du 20ème. En clair, la segmentation permettait de réellement adresser 65 530 octets au-delà de la limite de 1 mébioctet. La portion de mémoire adressable était appelé la '''''High memory area''''', qu'on va abrévier en HMA.
{| class="wikitable"
|+ Espace d'adressage du 286
|-
! Adresses en héxadécimal !! Zone de mémoire
|-
| 10 FFF0 à FF FFFF || Mémoire étendue, au-delà du premier mébioctet
|-
| 10 0000 à 10 FFEF || ''High Memory Area''
|-
| 0 à 0F FFFF || Mémoire adressable en mode réel
|}
En conséquence, les applications peuvent utiliser plus d'un mébioctet de RAM, mais au prix d'une rétrocompatibilité imparfaite. Quelques programmes DOS ne marchaient pus à cause de ça. D'autres fonctionnaient convenablement et pouvaient adresser les 65 520 octets en plus.
Pour résoudre ce problème, les carte mères ajoutaient un petit circuit relié au 21ème bit d'adresse, nommé A20 (pas d'erreur, les fils du bus d'adresse sont numérotés à partir de 0). Le circuit en question pouvait mettre à zéro le fil d'adresse, ou au contraire le laisser tranquille. En le forçant à 0, le calcul des adresses déborde comme dans le mode réel des 8086. Mais s'il ne le fait pas, la ''high memory area'' est adressable. Le circuit était une simple porte ET, qui combinait le 21ème bit d'adresse avec un '''signal de commande A20''' provenant d'ailleurs.
Le signal de commande A20 était géré par le contrôleur de clavier, qui était soudé à la carte mère. Le contrôleur en question ne gérait pas que le clavier, il pouvait aussi RESET le processeur, alors gérer le signal de commande A20 n'était pas si problématique. Quitte à avoir un microcontrôleur sur la carte mère, autant s'en servir au maximum... La gestion du bus d'adresse étaitdonc gérable au clavier. D'autres carte mères faisaient autrement et préféraient ajouter un interrupteur, pour activer ou non la mise à 0 du 21ème bit d'adresse.
: Il faut noter que le signal de commande A20 était mis à 1 en mode protégé, afin que le 21ème bit d'adresse soit activé.
Le 386 ajouta deux registres de segment, les registres FS et GS, ainsi que le '''mode ''virtual 8086'''''. Ce dernier permet d’exécuter des programmes en mode réel alors que le système d'exploitation s'exécute en mode protégé. C'est une technique de virtualisation matérielle qui permet d'émuler un 8086 sur un 386. L'avantage est que la compatibilité avec les programmes anciens écrits pour le 8086 est conservée, tout en profitant de la protection mémoire. Tous les processeurs x86 qui ont suivi supportent ce mode virtuel 8086.
==La segmentation avec une table des segments==
La '''segmentation avec une table des segments''' est apparue sur des processeurs assez anciens, le tout premier étant le Burrough 5000. Elle a ensuite été utilisée sur les processeurs x86 des PCs, à partir du 286 d'Intel. Elle est aujourd'hui abandonnée sur les jeux d'instruction x86. Tout comme la segmentation en mode réel, la segmentation attribue plusieurs segments par programmes ! Et cela a des répercutions sur la manière dont la traduction d'adresse est effectuée.
===Pourquoi plusieurs segments par programme ?===
L'utilité d'avoir plusieurs segments par programme n'est pas évidente, mais elle le devient quand on se plonge dans le passé. Dans le passé, les programmeurs devaient faire avec une quantité de mémoire limitée et il n'était pas rare que certains programmes utilisent plus de mémoire que disponible sur la machine. Mais les programmeurs concevaient leurs programmes en fonction.
[[File:Overlay Programming.svg|vignette|upright=1|Overlay Programming]]
L'idée était d'implémenter un système de mémoire virtuelle, mais émulé en logiciel, appelé l{{'}}'''''overlaying'''''. Le programme était découpé en plusieurs morceaux, appelés des ''overlays''. Les ''overlays'' les plus importants étaient en permanence en RAM, mais les autres étaient faisaient un va-et-vient entre RAM et disque dur. Ils étaient chargés en RAM lors de leur utilisation, puis sauvegardés sur le disque dur quand ils étaient inutilisés. Le va-et-vient des ''overlays'' entre RAM et disque dur était réalisé en logiciel, par le programme lui-même. Le matériel n'intervenait pas, comme c'est le cas avec la mémoire virtuelle.
Avec la segmentation, un programme peut utiliser la technique des ''overlays'', mais avec l'aide du matériel. Il suffit de mettre chaque ''overlay'' dans son propre segment, et laisser la segmentation faire. Les segments sont swappés en tout ou rien : on doit swapper tout un segment en entier. L'intérêt est que la gestion du ''swapping'' est grandement facilitée, vu que c'est le système d'exploitation qui s'occupe de swapper les segments sur le disque dur ou de charger des segments en RAM. Pas besoin pour le programmeur de coder quoique ce soit. Par contre, cela demande l'intervention du programmeur, qui doit découper le programme en segments/''overlays'' de lui-même. Sans cela, la segmentation n'est pas très utile.
L{{'}}''overlaying'' est une forme de '''segmentation à granularité grossière''', à savoir que le programme est découpé en segments de grande taille. L'usage classique est d'avoir un segment pour la pile, un autre pour le code exécutable, un autre pour le reste. Éventuellement, on peut découper les trois segments précédents en deux ou trois segments, rarement au-delà. Les segments sont alors peu nombreux, guère plus d'une dizaine par programme. D'où le terme de ''granularité grossière''.
La '''segmentation à granularité fine''' pousse le concept encore plus loin. Avec elle, il y a idéalement un segment par entité manipulée par le programme, un segment pour chaque structure de donnée et/ou chaque objet. Par exemple, un tableau aura son propre segment, ce qui est idéal pour détecter les accès hors tableau. Pour les listes chainées, chaque élément de la liste aura son propre segment. Et ainsi de suite, chaque variable agrégée (non-primitive), chaque structure de donnée, chaque objet, chaque instance d'une classe, a son propre segment. Diverses fonctionnalités supplémentaires peuvent être ajoutées, ce qui transforme le processeur en véritable processeur orienté objet, mais passons ces détails pour le moment.
Vu que les segments correspondent à des objets manipulés par le programme, on peut deviner que leur nombre évolue au cours du temps. En effet, les programmes modernes peuvent demander au système d'exploitation du rab de mémoire pour allouer une nouvelle structure de données. Avec la segmentation à granularité fine, cela demande d'allouer un nouveau segment à chaque nouvelle allocation mémoire, à chaque création d'une nouvelle structure de données ou d'un objet. De plus, les programmes peuvent libérer de la mémoire, en supprimant les structures de données ou objets dont ils n'ont plus besoin. Avec la segmentation à granularité fine, cela revient à détruire le segment alloué pour ces objets/structures de données. Le nombre de segments est donc dynamique, il change au cours de l'exécution du programme.
===Les tables de segments avec la segmentation===
La présence de plusieurs segments par programme a un impact sur la table des segments. Avec la relocation matérielle, elle conte nait un segment par programme. Chaque entrée, chaque ligne de la table des segment, mémorisait l'adresse de base, l'adresse limite, un bit de présence pour la mémoire virtuelle et des autorisations liées à la protection mémoire. Avec la segmentation, les choses sont plus compliquées, car il y a plusieurs segments par programme. Les entrées ne sont pas modifiées, mais elles sont organisées différemment.
Avec cette forme de segmentation, la table des segments doit respecter plusieurs contraintes. Premièrement, il y a plusieurs segments par programmes. Deuxièmement, le nombre de segments est variable : certains programmes se contenteront d'un seul segment, d'autres de dizaine, d'autres plusieurs centaines, etc. Il y a typiquement deux manières de faire : soit utiliser une table des segments uniques, utiliser une table des segment par programme.
Il est possible d'utiliser une table des segment unique qui mémorise tous les segments de tous les processus, système d'exploitation inclut. On parle alors de '''table des segment globale'''. Mais cette solution n'est pas utilisée avec la segmentation proprement dite. Elle est utilisée sur les architectures à capacité qu'on détaillera vers la fin du chapitre, dans une section dédiée. À la place, la segmentation utilise une table de segment par processus/programme, chacun ayant une '''table des segment locale'''.
Dans les faits, les choses sont plus compliquées. Le système d'exploitation doit savoir où se trouvent les tables de segment locale pour chaque programme. Pour cela, il a besoin d'utiliser une table de segment globale, dont chaque entrée pointe non pas vers un segment, mais vers une table de segment locale. Lorsque l'OS effectue une commutation de contexte, il lit la table des segment globale, pour récupérer un pointeur vers celle-ci. Ce pointeur est alors chargé dans un registre du processeur, qui mémorise l'adresse de la table locale, ce qui sert lors des accès mémoire.
Une telle organisation fait que les segments d'un processus/programme sont invisibles pour les autres, il y a une certaine forme de sécurité. Un programme ne connait que sa table de segments locale, il n'a pas accès directement à la table des segments globales. Tout accès mémoire se passera à travers la table de segment locale, il ne sait pas où se trouvent les autres tables de segment locales.
Les processeurs x86 sont dans ce cas : ils utilisent une table de segment globale couplée à autant de table des segments qu'il y a de processus en cours d'exécution. La table des segments globale s'appelle la '''''Global Descriptor Table''''' et elle peut contenir 8192 segments maximum, ce qui permet le support de 8192 processus différents. Les tables de segments locales sont appelées les '''''Local Descriptor Table''''' et elles font aussi 8192 segments maximum, ce qui fait 8192 segments par programme maximum. Il faut noter que la table de segment globale peut mémoriser des pointeurs vers les routines d'interruption, certaines données partagées (le tampon mémoire pour le clavier) et quelques autres choses, qui n'ont pas leur place dans les tables de segment locales.
===La relocation avec la segmentation===
La table des segments locale mémorise les adresses de base et limite de chaque segment, ainsi que d'autres méta-données. Les informations pour un segment sont regroupés dans un '''descripteur de segment''', qui est codé sur plusieurs octets, et qui regroupe : adresse de base, adresse limite, bit de présence en RAM, méta-données de protection mémoire.
La table des segments est un tableau dans lequel les descripteurs de segment sont placés les uns à la suite des autres en mémoire RAM. La table des segments est donc un tableau de segment. Les segments d'un programme sont numérotés, le nombre s'appelant un '''indice de segment''', appelé '''sélecteur de segment''' dans la terminologie Intel. L'indice de segment n'est autre que l'indice du segment dans ce tableau.
[[File:Global Descriptor table.png|centre|vignette|upright=2|Table des segments locale.]]
Il n'y a pas de registre de segment proprement dit, qui mémoriserait l'adresse de base. À la place, les segments sont adressés de manière indirecte. À la place, les registres de segment mémorisent des sélecteurs de segment. Ils sont utilisés pour lire l'adresse de base/limite dans la table de segment en mémoire RAM. Pour cela, un registre mémorise l'adresse de la table de segment locale, sa position en mémoire RAM.
Toute lecture ou écriture se fait en deux temps, en deux accès mémoire, consécutifs. Premièrement, le numéro de segment est utilisé pour adresser la table des segment. La lecture récupère alors un pointeur vers ce segment. Deuxièmement, ce pointeur est utilisé pour faire la lecture ou écriture. Plus précisément, la première lecture récupère un descripteur de segment qui contient l'adresse de base, le pointeur voulu, mais aussi l'adresse limite et d'autres informations.
[[File:Segmentation avec table des segments.png|centre|vignette|upright=2|Segmentation avec table des segments]]
L'accès à la table des segments se fait automatiquement à chaque accès mémoire. La conséquence est que chaque accès mémoire demande d'en faire deux : un pour lire la table des segments, l'autre pour l'accès lui-même. Il s'agit en quelque sorte d'une forme d'adressage indirect mémoire.
Un point important est que si le premier accès ne fait qu'une simple lecture dans un tableau, le second accès implique des calculs d'adresse. En effet, le premier accès récupère l'adresse de base du segment, mais le second accès sélectionne une donnée dans le segment, ce qui demande de calculer son adresse. L'adresse finale se déduit en combinant l'adresse de base avec un décalage (''offset'') qui donne la position de la donnée dans ce segment. L'indice de segment est utilisé pour récupérer l'adresse de base du segment. Une fois cette adresse de base connue, on lui additionne le décalage pour obtenir l'adresse finale.
[[File:Table des segments.png|centre|vignette|upright=2|Traduction d'adresse avec une table des segments.]]
Pour effectuer automatiquement l'accès à la table des segments, le processeur doit contenir un registre supplémentaire, qui contient l'adresse de la table de segment, afin de la localiser en mémoire RAM. Nous appellerons ce registre le '''pointeur de table'''. Le pointeur de table est combiné avec l'indice de segment pour adresser le descripteur de segment adéquat.
[[File:Segment 2.svg|centre|vignette|upright=2|Traduction d'adresse avec une table des segments, ici appelée table globale des de"scripteurs (terminologie des processeurs Intel x86).]]
Un point important est que la table des segments n'est pas accessible pour le programme en cours d'exécution. Il ne peut pas lire le contenu de la table des segments, et encore moins la modifier. L'accès se fait seulement de manière indirecte, en faisant usage des indices de segments, mais c'est un adressage indirect. Seul le système d'exploitation peut lire ou écrire la table des segments directement.
Plus haut, j'ai dit que tout accès mémoire impliquait deux accès mémoire : un pour charger le descripteur de segment, un autre pour la lecture/écriture proprement dite. Cependant, cela aurait un impact bien trop grand sur les performances. Dans les faits, les processeurs avec segmentations intégraient un '''cache de descripteurs de segments''', pour limiter la casse. Quand un descripteur de segment est lu depuis la RAM, il est copié dans ce cache. Les accès ultérieurs accédent au descripteur dans le cache, pas besoin de passer par la RAM. L'intel 386 avait un cache de ce type.
===La protection mémoire : les accès hors-segments===
Comme avec la relocation matérielle, le processeur détecte les débordements de segment. Pour cela, il compare l'adresse logique accédée avec l'adresse limite, ou compare la taille limite avec le décalage. De nombreux processeurs, comme l'Intel 386, préféraient utiliser la taille du segment, pour une question d'optimisation. En effet, si on compare l'adresse finale avec l'adresse limite, on doit faire la relocation avant de comparer l'adresse relocatée. Mais en utilisant la taille, ce n'est pas le cas : on peut faire la comparaison avant, pendant ou après la relocation.
Un détail à prendre en compte est la taille de la donnée accédée. Sans cela, la comparaison serait très simple : on vérifie si ''décalage <= taille du segment'', ou on compare des adresses de la même manière. Mais imaginez qu'on accède à une donnée de 4 octets : il se peut que l'adresse de ces 4 octets rentre dans le segment, mais que quelques octets débordent. Par exemple, les deux premiers octets sont dans le segment, mais pas les deux suivants. La vraie comparaison est alors : ''décalage + 4 octets <= taille du segment''.
Mais il est possible de faire le calcul autrement, et quelques processeurs comme l'Intel 386 ne s'en sont pas privé. Il calculait la différence ''taille du segment - décalage'', et vérifiait le résultat. Le processeur gérait des données de 1, 2 et 4 octets, ce qui fait que le résultat devait être entre 0 et 3. Le processeur prenait le résultat de la soustraction, et vérifiait alors que les 30 bits de poids fort valaient bien 0. Il vérifiait aussi que les deux bits de poids faible avaient la bonne valeur.
[[File:Vm7.svg|centre|vignette|upright=2|Traduction d'adresse avec vérification des accès hors-segment.]]
Une nouveauté fait son apparition avec la segmentation : la '''gestion des droits d'accès'''. Par exemple, il est possible d'interdire d'exécuter le contenu d'un segment, ce qui fournit une protection contre certaines failles de sécurité ou certains virus. Lorsqu'on exécute une opération interdite, le processeur lève une exception matérielle, à charge du système d'exploitation de gérer la situation.
Pour cela, chaque segment se voit attribuer un certain nombre d'autorisations d'accès qui indiquent si l'on peut lire ou écrire dedans, si celui-ci contient un programme exécutable, etc. Les autorisations pour chaque segment sont placées dans le descripteur de segment. Elles se résument généralement à quelques bits, qui indiquent si le segment est accesible en lecture/écriture ou exécutable. Le tout est souvent concaténé dans un ou deux '''octets de droits d'accès'''.
L'implémentation de la protection mémoire dépend du CPU considéré. Les CPU microcodés peuvent en théorie utiliser le microcode. Lorsqu'une instruction mémoire s'exécute, le microcode effectue trois étapes : lire le descripteur de segment, faire les tests de protection mémoire, exécuter la lecture/écriture ou lever une exception. Létape de test est réalisée avec un ou plusieurs micro-branchements. Par exemple, une écriture va tester le bit R/W du descripteur, qui indique si on peut écrire dans le segment, en utilisant un micro-branchement. Le micro-branchement enverra vers une routine du microcode en cas d'erreur.
Les tests de protection mémoire demandent cependant de tester beaucoup de conditions différentes. Par exemple, le CPU Intel 386 testait moins d'une dizaine de conditions pour certaines instructions. Il est cependant possible de faire plusieurs comparaisons en parallèle en rusant un peu. Il suffit de mémoriser les octets de droits d'accès dans un registre interne, de masquer les bits non-pertinents, et de faire une comparaison avec une constante adéquate, qui encode la valeur que doivent avoir ces bits.
Une solution alternative utiliser un circuit combinatoire pour faire les tests de protection mémoire. Les tests sont alors faits en parallèles, plutôt qu'un par un par des micro-branchements. Par contre, le cout en matériel est assez important. Il faut ajouter ce circuit combinatoire, ce qui demande pas mal de circuits.
===La mémoire virtuelle avec la segmentation===
La mémoire virtuelle est une fonctionnalité souvent implémentée sur les processeurs qui gèrent la segmentation, alors que les processeurs avec relocation matérielle s'en passaient. Il faut dire que l'implémentation de la mémoire virtuelle est beaucoup plus simple avec la segmentation, comparé à la relocation matérielle. Le remplacement des registres de base par des sélecteurs de segment facilite grandement l'implémentation.
Le problème de la mémoire virtuelle est que les segments peuvent être swappés sur le disque dur n'importe quand, sans que le programme soit prévu. Le swapping est réalisé par une interruption de l'OS, qui peut interrompre le programme n'importe quand. Et si un segment est swappé, le registre de base correspondant devient invalide, il point sur une adresse en RAM où le segment était, mais n'est plus. De plus, les segments peuvent être déplacés en mémoire, là encore n'importe quand et d'une manière invisible par le programme, ce qui fait que les registres de base adéquats doivent être modifiés.
Si le programme entier est swappé d'un coup, comme avec la relocation matérielle simple, cela ne pose pas de problèmes. Mais dès qu'on utilise plusieurs registres de base par programme, les choses deviennent soudainement plus compliquées. Le problème est qu'il n'y a pas de mécanismes pour choisir et invalider le registre de base adéquat quand un segment est déplacé/swappé. En théorie, on pourrait imaginer des systèmes qui résolvent le problème au niveau de l'OS, mais tous ont des problèmes qui font que l'implémentation est compliquée ou que les performances sont ridicules.
L'usage d'une table des segments accédée à chaque accès résout complètement le problème. La table des segments est accédée à chaque accès mémoire, elle sait si le segment est swappé ou non, chaque accès vérifie si le segment est en mémoire et quelle est son adresse de base. On peut changer le segment de place n'importe quand, le prochain accès récupérera des informations à jour dans la table des segments.
L'implémentation de la mémoire virtuelle avec la segmentation est simple : il suffit d'ajouter un bit dans les descripteurs de segments, qui indique si le segment est swappé ou non. Tout le reste, la gestion de ce bit, du swap, et tout ce qui est nécessaire, est délégué au système d'exploitation. Lors de chaque accès mémoire, le processeur vérifie ce bit avant de faire la traduction d'adresse, et déclenche une exception matérielle si le bit indique que le segment est swappé. L'exception matérielle est gérée par l'OS.
===Le partage de segments===
Il est possible de partager un segment entre plusieurs applications. Cela peut servir pour partager des données entre deux programmes : un segment de données partagées est alors partagé entre deux programmes. Partager un segment de code est utile pour les bibliothèques partagées : la bibliothèque est placée dans un segment dédié, qui est partagé entre les programmes qui l'utilisent. Partager un segment de code est aussi utile quand plusieurs instances d'une même application sont lancés simultanément : le code n'ayant pas de raison de changer, celui-ci est partagé entre toutes les instances. Mais ce n'est là qu'un exemple.
La première solution pour cela est de configurer les tables de segment convenablement. Le même segment peut avoir des droits d'accès différents selon les processus. Les adresses de base/limite sont identiques, mais les tables des segments ont alors des droits d'accès différents. Mais cette méthode de partage des segments a plusieurs défauts.
Premièrement, les sélecteurs de segments ne sont pas les mêmes d'un processus à l'autre, pour un même segment. Le segment partagé peut correspondre au segment numéro 80 dans le premier processus, au segment numéro 1092 dans le second processus. Rien n'impose que les sélecteurs de segment soient les mêmes d'un processus à l'autre, pour un segment identique.
Deuxièmement, les adresses limite et de base sont dupliquées dans plusieurs tables de segments. En soi, cette redondance est un souci mineur. Mais une autre conséquence est une question de sécurité : que se passe-t-il si jamais un processus a une table des segments corrompue ? Il se peut que pour un segment identique, deux processus n'aient pas la même adresse limite, ce qui peut causer des failles de sécurité. Un processus peut alors subir un débordement de tampon, ou tout autre forme d'attaque.
[[File:Vm9.png|centre|vignette|upright=2|Illustration du partage d'un segment entre deux applications.]]
Une seconde solution, complémentaire, utilise une table de segment globale, qui mémorise des segments partagés ou accessibles par tous les processus. Les défauts de la méthode précédente disparaissent avec cette technique : un segment est identifié par un sélecteur unique pour tous les processus, il n'y a pas de duplication des descripteurs de segment. Par contre, elle a plusieurs défauts.
Le défaut principal est que cette table des segments est accessible par tous les processus, impossible de ne partager ses segments qu'avec certains pas avec les autres. Un autre défaut est que les droits d'accès à un segment partagé sont identiques pour tous les processus. Impossible d'avoir un segment partagé accessible en lecture seule pour un processus, mais accessible en écriture pour un autre. Il est possible de corriger ces défauts, mais nous en parlerons dans la section sur les architectures à capacité.
===L'extension d'adresse avec la segmentation===
L'extension d'adresse est possible avec la segmentation, de la même manière qu'avec la relocation matérielle. Il suffit juste que les adresses de base soient aussi grandes que le bus d'adresse. Mais il y a une différence avec la relocation matérielle : un même programme peut utiliser plus de mémoire qu'il n'y en a dans l'espace d'adressage. La raison est simple : un segment peut prendre tout l'espace d'adressage, et il y a plusieurs segments par programme.
Pour donner un exemple, prenons un processeur 16 bits, qui peut adresser 64 kibioctets, associé à une mémoire de 4 mébioctets. Il est possible de placer le code machine dans les premiers 64k de la mémoire, la pile du programme dans les 64k suivants, le tas dans les 64k encore après, et ainsi de suite. Le programme dépasse donc les 64k de mémoire de l'espace d'adressage. Ce genre de chose est impossible avec la relocation, où un programme est limité par l'espace d'adressage.
===Le mode protégé des processeurs x86===
L'Intel 80286, aussi appelé 286, ajouta un mode de segmentation séparé du mode réel, qui ajoute une protection mémoire à la segmentation, ce qui lui vaut le nom de '''mode protégé'''. Dans ce mode, les registres de segment ne contiennent pas des adresses de base, mais des sélecteurs de segments qui sont utilisés pour l'accès à la table des segments en mémoire RAM.
Le 286 bootait en mode réel, puis le système d'exploitation devait faire quelques manipulations pour passer en mode protégé. Le 286 était pensé pour être rétrocompatible au maximum avec le 80186. Mais les différences entre le 286 et le 8086 étaient majeures, au point que les applications devaient être réécrites intégralement pour profiter du mode protégé. Un mode de compatibilité permettait cependant aux applications destinées au 8086 de fonctionner, avec même de meilleures performances. Aussi, le mode protégé resta inutilisé sur la plupart des applications exécutées sur le 286.
Vint ensuite le processeur 80386, renommé en 386 quelques années plus tard. Sur ce processeur, les modes réel et protégé sont conservés tel quel, à une différence près : toutes les adresses passent à 32 bits, qu'il s'agisse des adresses de base, limite ou des ''offsets''. Le processeur peut donc adresser un grand nombre de segments : 2^32, soit plus de 4 milliards. Les segments grandissent aussi et passent de 64 KB maximum à 4 gibioctets maximum. Mais surtout : le 386 ajouta le support de la pagination en plus de la segmentation. Ces modifications ont été conservées sur les processeurs 32 bits ultérieurs.
Les processeurs x86 gèrent deux types de tables des segments : une table locale pour chaque processus, et une table globale partagée entre tous les processus. Il ne peut y avoir qu'une table locale d'active, vu que le processeur ne peut exécuter qu'un seul processus en même temps. Chaque table locale définit 8192 segments, pareil pour la table globale. La table globale est utilisée pour les segments du noyau et la mémoire partagée entre processus. Un défaut est qu'un segment partagé par la table globale est visible par tous les processus, avec les mêmes droits d'accès. Ce qui fait que cette méthode était peu utilisée en pratique. La table globale mémorise aussi des pointeurs vers les tables locales, avec un descripteur de segment par table locale.
Sur les processeurs x86 32 bits, un descripteur de segment est organisé comme suit, pour les architectures 32 bits. On y trouve l'adresse de base et la taille limite, ainsi que de nombreux bits de contrôle.
Le premier groupe de bits de contrôle est l'octet en bleu à droite. Il contient :
* le bit P qui indique que l'entrée contient un descripteur valide, qu'elle n'est pas vide ;
* deux bits DPL qui indiquent le niveau de privilège du segment (noyau, utilisateur, les deux intermédiaires spécifiques au x86) ;
* un bit S qui précise si le segment est de type système (utiles pour l'OS) ou un segment de code/données.
* un champ Type qui contient les bits suivants :
** un bit E qui indique si le segment contient du code exécutable ou non ;
** le bit RW qui indique s'il est en lecture seule ou non ;;
** Un bit A qui indique que le segment a récemment été accédé, information utile pour l'OS;
** un bit DC assez spécifiques.
En haut à gauche, en bleu, on trouve deux bits :
* Le bit G indique comment interpréter la taille contenue dans le descripteur : 0 si la taille est exprimée en octets, 1 si la taille est un nombre de pages de 4 kibioctets. Ce bit précise si on utilise la segmentation seule, ou combinée avec la pagination.
* Le bit DB précise si l'on utilise des segments en mode de compatibilité 16 bits ou des segments 32 bits.
[[File:SegmentDescriptor.svg|centre|vignette|upright=3|Segment Descriptor]]
Les indices de segment sont appelés des sélecteurs de segment. Ils ont une taille de 16 bits, mais 3 bits sont utilisés pour encoder des méta-données. Le numéro de segment est donc codé sur 13 bits, ce qui permettait de gérer maximum 8192 segments par table de segment (locale ou globale). Les 16 bits sont organisés comme suit :
* 13 bits pour le numéro du segment dans la table des segments, l'indice de segment proprement dit ;
* un bit qui précise s'il faut accéder à la table des segments globale ou locale ;
* deux bits qui indiquent le niveau de privilège de l'accès au segment (les 4 niveaux de protection, dont l'espace noyau et utilisateur).
[[File:SegmentSelector.svg|centre|vignette|upright=1.5|Sélecteur de segment 16 bit.]]
En tout, l'indice permet de gérer 8192 segments pour la table locale et 8192 segments de la table globale.
====La MMU du 386/486 : cache de segment, protection mémoire====
La MMU du 386 et celle du 486 étaient assez similaires. Elles étaient plus complexes que celle du 186 et du 286. Elle contenait un additionneur pour les calculs d'adresse et un comparateur pour tester si l'accès mémoire déborde d'un segment. Le test de débordement se faisait en parallèle du calcul de l'adresse finale, comme sur le 286.
L'additionneur était un additionneur trois-opérandes, qui additionnait l'adresse à lire/écrire, l'adresse de base du segment, et un décalage intégré dans l'instruction. En clair, l'unité de segmentation n'était pas qu'une MMU, elle prenait en charge une partie du calcul d'adresse. L'avantage est que cela permettait de gérer les modes d'adressage "base + décalage" et "base + indice + décalage" très facilement, en utilisant un minimum de circuits. Une conséquence de cette organisation était que l'usage d'un décalage était gratuit. Par contre, dès qu'on utilisait l'adressage "Base + Indice", avec ou sans décalage, l'instruction prenait un cycle de plus à s'exécuter, parce qu'il fallait faire l'addition "Base + Indice" dans l'ALU entière.
Le CPU 386 était le premier à implémenter la protection mémoire avec des segments. Pour cela, il intégrait une '''''Protection Test Unit''''', séparée du microcode, qu'on va abrévier en PTU. Précisément, il s'agissait d'un PLA (''Programmable Logic Array''), une sorte d'intermédiaire entre circuit logique fait sur mesure et mémoire ROM, qu'on a déjà abordé dans le chapitre sur les mémoires ROM. Mais cette unité ne faisait pas tout, le microcode était aussi impliqué. La PTU sera détaillée dans la section suivante.
Pour améliorer les performances, le 386 et le 486 intégraient un '''cache de descripteurs de segment''', aussi appelé le cache de descripteurs. Lorsqu'un descripteur état chargé pour la première fois, il était copié dans le cache de descripteurs de segment. Les accès mémoire ultérieur lisaient le descripteur de segment depuis ce cache, pas depuis la table des segments en RAM. Le cache de descripteurs gère aussi bien les segments en mode réel qu'en mode protégé. Récupérer l'adresse de base depuis cache se fait un peu différemment en mode réel et protégé, mais le cache gère cela tout seul. Idem pour récupérer la taille/adresse limite.
[[File:Microarchitecture du 386, avec focus sur la segmentation.png|centre|vignette|upright=2|Microarchitecture du 386 et du 486, avec focus sur la segmentation.]]
En mode réel, la taille des segment est censée être limitée à 64 kibioctets. Mais le processeur ne vérifiait pas si cette limite était dépassée. À la place, il utilisait la limite précisée dans le cache de segments. Pire que ça : le cache de segment n'était pas réinitialisé quand on passe du mode réel au mode protégé, et réciproquement. Et cette propriété a été à l'origine de l''''''unreal mode'''''. Il s'agissait d'un mode réel amélioré, capable d'utiliser des segments de 4 gibioctets et des adresses de 32 bits.
Passer en mode ''unreal'' pouvait se faire de deux manières. Il était possible d'altérer le cache de descripteur en utilisant l'instruction non-documentée LOADALL. Elle permettait de charger les descripteurs de segments dans le cache de descripteurs, avec une taille arbitraire. Une autre solution, beaucoup plus complexe sur le 386, demandait d'entrer en mode protégé pour configurer des segments de grande taille, de charger leurs descripteurs dans le cache de descripteur, puis de revenir en mode réel. En mode réel, les descripteurs dans le cache étaient encore disponibles et on pouvait les lire dans le cache.
====L'implémentation de la protection mémoire sur le 386====
La protection mémoire teste la valeur des bits P, S, X, E, R/W. Elle teste aussi les niveaux de privilège, avec deux bits DPL et CPL. En tout, le processeur pouvait tester 148 conditions différentes en parallèle dans la PTU. Cependant, les niveaux de privilèges étaient pré-traités par le microcode. Le microcode vérifiait aussi s'il y avait une erreur en terme d’anneau mémoire, avec par "exemple un segment en mode noyau accédé alors que le CPU est en espace utilisateur. Il fournissait alors un résultat sur deux bits, qui indiquait s'il y avait une erreur ou non, que la PTU utilisait.
Mais toutes les conditions n'étaient pas pertinentes à un instant t. Par exemple, il est pertinent de vérifier si le bit R/W était cohérent si l'instruction à exécuter est une écriture. Mais il n'y a pas besoin de tester le bit E qui indique qu'un segment est exécutable ou non, pour une lecture. En tout, le processeur pouvait se retrouver dans 33 situations possibles, chacune demandant de tester un sous-ensemble des 148 conditions. Pour préciser quel sous-ensembles tester, la PTU recevait un code opération, généré par le microcode.
Pour faire les tests de protection mémoire, le microcode avait une micro-opération nommée ''protection test operation'', qui envoyait les droits d'accès à la PTU. Lors de l'exécution d'une ''protection test operation'', le PLA recevait un descripteur de segment, lu depuis la mémoire RAM, ainsi qu'un code opération provenant du microcode.
{|class="wikitable"
|+ Entrée de la ''Protection Test Unit''
|-
! 15 - 14 !! 13 - 12 !! 11 !! 10 !! 9 !! 8 !! 7 !! 6 !! 5-0
|-
| P1 , P2 || || P || S || X || E || R/W || A || Code opération
|-
| Niveaux de privilèges cohérents/erreur || || Segment présent en mémoire ou swappé || S || X || Segment exécutable ou non || Segment accesible en lecture/écriture || Segment récemment accédé || Code opération
|}
Il fournissait en sortie un bit qui indiquait si une erreur de protection mémoire avait eu lieu ou non. Il fournissait aussi une adresse de 12 bits, utilisée seulement en cas d'erruer. Elle pointait dans le microcode, sur un code levant une exception en cas d'erreur. Enfin, la PTU fournissait 4 bits pouvant être testés par un branchement dans le microcode. L'un d'entre eux demandait de tester s'il y a un accès hors-limite, les autres étaient assez peu reliés à la protection mémoire.
Un détail est que le chargement du descripteur de segment est réalisé par une fonction dans le microcode. Elle est appliquée pour toutes les instructions ou situations qui demandent de faire un accès mémoire. Et les tests de protection mémoire sont réalisés dans cette fonction, pas après elle. Vu qu'il s'agit d'une fonction exécutée quelque soit l'instruction, le microcode doit transférer le code opération à cette fonction. Le microcode est pour cela associé à un registre interne, dans lequel le code opération est mémorisé, avant d'appeler la fonction. Le microcode a une micro-opération PTSAV (''Protection Save'') pour mémoriser le code opération dans ce registre. Dans la fonction qui charge le descripteur, une micro-opération PTOVRR (''Protection Override'') lit le code opération dans ce registre, et lance les tests nécessaires.
Il faut noter que le PLA était certes plus rapide que de tester les conditions une par une, mais il était assez lent. La PTU mettait environ 3 cycles d'horloges pour rendre son résultat. Le microcode en profitait alors pour exécuter des micro-opérations durant ces 3 cycles d'attente. Par exemple, le microcode pouvait en profiter pour lire l'adresse de base dans le descripteur, si elle n'a pas été chargée avant (les descripteur était chargé en deux fois). Il fallait cependant que les trois micro-opérations soient valides, peu importe qu'il y ait une erreur de protection mémoire ou non. Ou du moins, elles produisaient un résultat qui n'est pas utilisé en cas d'erreur. Si ce n'était pas possible, le microcode ajoutait des NOP pendant ce temps d'attente de 3 cycles.
Le bit A du descripteur de segment indique que le segment a récemment été accédé. Il est mis à jour après les tests de protection mémoire, quand ceux-ci indiquent que l'accès mémoire est autorisé. Le bit A est mis à 1 si la PTU l'autorise. Pour cela, la PTU utilise un des 4 bits de sortie mentionnés plus haut : l'un d'entre eux indique que le bit A doit être mis à 1. La mise à jour est ensuite réalisée par le microcode, qui utilise trois micro-opérations pour le mettre à jour.
====Le ''Hardware task switching'' des CPU x86====
Les systèmes d’exploitation modernes peuvent lancer plusieurs logiciels en même temps. Les logiciels sont alors exécutés à tour de rôle. Passer d'un programme à un autre est ce qui s'appelle une commutation de contexte. Lors d'une commutation de contexte, l'état du processeur est sauvegardé, afin que le programme stoppé puisse reprendre là où il était. Il arrivera un moment où le programme stoppé redémarrera et il doit reprendre dans l'état exact où il s'est arrêté. Deuxièmement, le programme à qui c'est le tour restaure son état. Cela lui permet de revenir là où il était avant d'être stoppé. Il y a donc une sauvegarde et une restauration des registres.
Divers processeurs incorporent des optimisations matérielles pour rendre la commutation de contexte plus rapide. Ils peuvent sauvegarder et restaurer les registres du processeur automatiquement lors d'une interruption de commutation de contexte. Les registres sont sauvegardés dans des structures de données en mémoire RAM, appelées des '''contextes matériels'''. Sur les processeurs x86, il s'agit de la technique d{{'}}''Hardware Task Switching''. Fait intéressant, le ''Hardware Task Switching'' se base beaucoup sur les segments mémoires.
Avec ''Hardware Task Switching'', chaque contexte matériel est mémorisé dans son propre segment mémoire, séparé des autres. Les segments pour les contextes matériels sont appelés des '''''Task State Segment''''' (TSS). Un TSS mémorise tous les registres généraux, le registre d'état, les pointeurs de pile, le ''program counter'' et quelques registres de contrôle du processeur. Par contre, les registres flottants ne sont pas sauvegardés, de même que certaines registres dit SIMD que nous n'avons pas encore abordé. Et c'est un défaut qui fait que le ''Hardware Task Switching'' n'est plus utilisé.
Le programme en cours d'exécution connait l'adresse du TSS qui lui est attribué, car elle est mémorisée dans un registre appelé le '''''Task Register'''''. En plus de pointer sur le TSS, ce registre contient aussi les adresses de base et limite du segment en cours. Pour être plus précis, le ''Task Register'' ne mémorise pas vraiment l'adresse du TSS. À la place, elle mémorise le numéro du segment, le numéro du TSS. Le numéro est codé sur 16 bits, ce qui explique que 65 536 segments sont adressables. Les instructions LDR et STR permettent de lire/écrire ce numéro de segment dans le ''Task Register''.
Le démarrage d'un programme a lieu automatiquement dans plusieurs circonstances. La première est une instruction de branchement CALL ou JMP adéquate. Le branchement fournit non pas une adresse à laquelle brancher, mais un numéro de segment qui pointe vers un TSS. Cela permet à une routine du système d'exploitation de restaurer les registres et de démarrer le programme en une seule instruction de branchement. Une seconde circonstance est une interruption matérielle ou une exception, mais nous la mettons de côté. Le ''Task Register'' est alors initialisé avec le numéro de segment fournit. S'en suit la procédure suivante :
* Le ''Task Register'' est utilisé pour adresser la table des segments, pour récupérer un pointeur vers le TSS associé.
* Le pointeur est utilisé pour une seconde lecture, qui adresse le TSS directement. Celle-ci restaure les registres du processeur.
En clair, on va lire le ''TSS descriptor'' dans la GDT, puis on l'utilise pour restaurer les registres du processeur.
[[File:Hardware Task Switching x86.png|centre|vignette|upright=2|Hardware Task Switching x86]]
===La segmentation sur les processeurs Burrough B5000 et plus===
Le Burrough B5000 est un très vieil ordinateur, commercialisé à partir de l'année 1961. Ses successeurs reprennent globalement la même architecture. C'était une machine à pile, doublé d'une architecture taguée, choses très rare de nos jours. Mais ce qui va nous intéresser dans ce chapitre est que ce processeur incorporait la segmentation, avec cependant une différence de taille : un programme avait accès à un grand nombre de segments. La limite était de 1024 segments par programme ! Il va de soi que des segments plus petits favorise l'implémentation de la mémoire virtuelle, mais complexifie la relocation et le reste, comme nous allons le voir.
Le processeur gère deux types de segments : les segments de données et de procédure/fonction. Les premiers mémorisent un bloc de données, dont le contenu est laissé à l'appréciation du programmeur. Les seconds sont des segments qui contiennent chacun une procédure, une fonction. L'usage des segments est donc différent de ce qu'on a sur les processeurs x86, qui n'avaient qu'un segment unique pour l'intégralité du code machine. Un seul segment de code machine x86 est découpé en un grand nombre de segments de code sur les processeurs Burrough.
La table des segments contenait 1024 entrées de 48 bits chacune. Fait intéressant, chaque entrée de la table des segments pouvait mémoriser non seulement un descripteur de segment, mais aussi une valeur flottante ou d'autres types de données ! Parler de table des segments est donc quelque peu trompeur, car cette table ne gère pas que des segments, mais aussi des données. La documentation appelaiat cette table la '''''Program Reference Table''''', ou PRT.
La raison de ce choix quelque peu bizarre est que les instructions ne gèrent pas d'adresses proprement dit. Tous les accès mémoire à des données en-dehors de la pile passent par la segmentation, ils précisent tous un indice de segment et un ''offset''. Pour éviter d'allouer un segment pour chaque donnée, les concepteurs du processeur ont décidé qu'une entrée pouvait contenir directement la donnée entière à lire/écrire.
La PRT supporte trois types de segments/descripteurs : les descripteurs de données, les descripteurs de programme et les descripteurs d'entrées-sorties. Les premiers décrivent des segments de données. Les seconds sont associés aux segments de procédure/fonction et sont utilisés pour les appels de fonction (qui passent, eux aussi, par la segmentation). Le dernier type de descripteurs sert pour les appels systèmes et les communications avec l'OS ou les périphériques.
Chaque entrée de la PRT contient un ''tag'', une suite de bit qui indique le type de l'entrée : est-ce qu'elle contient un descripteur de segment, une donnée, autre. Les descripteurs contiennent aussi un ''bit de présence'' qui indique si le segment a été swappé ou non. Car oui, les segments pouvaient être swappés sur ce processeur, ce qui n'est pas étonnant vu que les segments sont plus petits sur cette architecture. Le descripteur contient aussi l'adresse de base du segment ainsi que sa taille, et diverses informations pour le retrouver sur le disque dur s'il est swappé.
: L'adresse mémorisée ne faisait que 15 bits, ce qui permettait d'adresse 32 kibi-mots, soit 192 kibioctets de mémoire. Diverses techniques d'extension d'adressage étaient disponibles pour contourner cette limitation. Outre l'usage de l{{'}}''overlay'', le processeur et l'OS géraient aussi des identifiants d'espace d'adressage et en fournissaient plusieurs par processus. Les processeurs Borrough suivants utilisaient des adresses plus grandes, de 20 bits, ce qui tempérait le problème.
[[File:B6700Word.jpg|centre|vignette|upright=2|Structure d'un mot mémoire sur le B6700.]]
==Les architectures à capacités==
Les architectures à capacité utilisent la segmentation à granularité fine, mais ajoutent des mécanismes de protection mémoire assez particuliers, qui font que les architectures à capacité se démarquent du reste. Les architectures de ce type sont très rares et sont des processeurs assez anciens. Le premier d'entre eux était le Plessey System 250, qui date de 1969. Il fu suivi par le CAP computer, vendu entre les années 70 et 77. En 1978, le System/38 d'IBM a eu un petit succès commercial. En 1980, la Flex machine a aussi été vendue, mais à très peu d'examplaires, comme les autres architectures à capacité. Et enfin, en 1981, l'architecture à capacité la plus connue, l'Intel iAPX 432 a été commercialisée. Depuis, la seule architecture de ce type est en cours de développement. Il s'agit de l'architecture CHERI, dont la mise en projet date de 2014.
===Le partage de la mémoire sur les architectures à capacités===
Le partage de segment est grandement modifié sur les architectures à capacité. Avec la segmentation normale, il y a une table de segment par processus. Les conséquences sont assez nombreuses, mais la principale est que partager un segment entre plusieurs processus est compliqué. Les défauts ont été évoqués plus haut. Les sélecteurs de segments ne sont pas les mêmes d'un processus à l'autre, pour un même segment. De plus, les adresses limite et de base sont dupliquées dans plusieurs tables de segments, et cela peut causer des problèmes de sécurité si une table des segments est modifiée et pas l'autre. Et il y a d'autres problèmes, tout aussi importants.
[[File:Partage des segments avec la segmentation.png|centre|vignette|upright=1.5|Partage des segments avec la segmentation]]
À l'opposé, les architectures à capacité utilisent une table des segments unique pour tous les processus. La table des segments unique sera appelée dans de ce qui suit la '''table des segments globale''', ou encore la table globale. En conséquence, les adresses de base et limite ne sont présentes qu'en un seul exemplaire par segment, au lieu d'être dupliquées dans autant de processus que nécessaire. De plus, cela garantit que l'indice de segment est le même quel que soit le processus qui l'utilise.
Un défaut de cette approche est au niveau des droits d'accès. Avec la segmentation normale, les droits d'accès pour un segment sont censés changer d'un processus à l'autre. Par exemple, tel processus a accès en lecture seule au segment, l'autre seulement en écriture, etc. Mais ici, avec une table des segments uniques, cela ne marche plus : incorporer les droits d'accès dans la table des segments ferait que tous les processus auraient les mêmes droits d'accès au segment. Et il faut trouver une solution.
===Les capacités sont des pointeurs protégés===
Pour éviter cela, les droits d'accès sont combinés avec les sélecteurs de segments. Les sélecteurs des segments sont remplacés par des '''capacités''', des pointeurs particuliers formés en concaténant l'indice de segment avec les droits d'accès à ce segment. Si un programme veut accéder à une adresse, il fournit une capacité de la forme "sélecteur:droits d'accès", et un décalage qui indique la position de l'adresse dans le segment.
Il est impossible d'accéder à un segment sans avoir la capacité associée, c'est là une sécurité importante. Un accès mémoire demande que l'on ait la capacité pour sélectionner le bon segment, mais aussi que les droits d'accès en permettent l'accès demandé. Par contre, les capacités peuvent être passées d'un programme à un autre sans problème, les deux programmes pourront accéder à un segment tant qu'ils disposent de la capacité associée.
[[File:Comparaison entre capacités et adresses segmentées.png|centre|vignette|upright=2.5|Comparaison entre capacités et adresses segmentées]]
Mais cette solution a deux problèmes très liés. Au niveau des sélecteurs de segment, le problème est que les sélecteur ont une portée globale. Avant, l'indice de segment était interne à un programme, un sélecteur ne permettait pas d'accéder au segment d'un autre programme. Sur les architectures à capacité, les sélecteurs ont une portée globale. Si un programme arrive à forger un sélecteur qui pointe vers un segment d'un autre programme, il peut théoriquement y accéder, à condition que les droits d'accès le permettent. Et c'est là qu'intervient le second problème : les droits d'accès ne sont plus protégés par l'espace noyau. Les droits d'accès étaient dans la table de segment, accessible uniquement en espace noyau, ce qui empêchait un processus de les modifier. Avec une capacité, il faut ajouter des mécanismes de protection qui empêchent un programme de modifier les droits d'accès à un segment et de générer un indice de segment non-prévu.
La première sécurité est qu'un programme ne peut pas créer une capacité, seul le système d'exploitation le peut. Les capacités sont forgées lors de l'allocation mémoire, ce qui est du ressort de l'OS. Pour rappel, un programme qui veut du rab de mémoire RAM peut demander au système d'exploitation de lui allouer de la mémoire supplémentaire. Le système d'exploitation renvoie alors un pointeurs qui pointe vers un nouveau segment. Le pointeur est une capacité. Il doit être impossible de forger une capacité, en-dehors d'une demande d'allocation mémoire effectuée par l'OS. Typiquement, la forge d'une capacité se fait avec des instructions du processeur, que seul l'OS peut éxecuter (pensez à une instruction qui n'est accessible qu'en espace noyau).
La seconde protection est que les capacités ne peuvent pas être modifiées sans raison valable, que ce soit pour l'indice de segment ou les droits d'accès. L'indice de segment ne peut pas être modifié, quelqu'en soit la raison. Pour les droits d'accès, la situation est plus compliquée. Il est possible de modifier ses droits d'accès, mais sous conditions. Réduire les droits d'accès d'une capacité est possible, que ce soit en espace noyau ou utilisateur, pas l'OS ou un programme utilisateur, avec une instruction dédiée. Mais augmenter les droits d'accès, seul l'OS peut le faire avec une instruction précise, souvent exécutable seulement en espace noyau.
Les capacités peuvent être copiées, et même transférées d'un processus à un autre. Les capacités peuvent être détruites, ce qui permet de libérer la mémoire utilisée par un segment. La copie d'une capacité est contrôlée par l'OS et ne peut se faire que sous conditions. La destruction d'une capacité est par contre possible par tous les processus. La destruction ne signifie pas que le segment est effacé, il est possible que d'autres processus utilisent encore des copies de la capacité, et donc le segment associé. On verra quand la mémoire est libérée plus bas.
Protéger les capacités demande plusieurs conditions. Premièrement, le processeur doit faire la distinction entre une capacité et une donnée. Deuxièmement, les capacités ne peuvent être modifiées que par des instructions spécifiques, dont l'exécution est protégée, réservée au noyau. En clair, il doit y avoir une séparation matérielle des capacités, qui sont placées dans des registres séparés. Pour cela, deux solutions sont possibles : soit les capacités remplacent les adresses et sont dispersées en mémoire, soit elles sont regroupées dans un segment protégé.
====La liste des capacités====
Avec la première solution, on regroupe les capacités dans un segment protégé. Chaque programme a accès à un certain nombre de segments et à autant de capacités. Les capacités d'un programme sont souvent regroupées dans une '''liste de capacités''', appelée la '''''C-list'''''. Elle est généralement placée en mémoire RAM. Elle est ce qu'il reste de la table des segments du processus, sauf que cette table ne contient pas les adresses du segment, qui sont dans la table globale. Tout se passe comme si la table des segments de chaque processus est donc scindée en deux : la table globale partagée entre tous les processus contient les informations sur les limites des segments, la ''C-list'' mémorise les droits d'accès et les sélecteurs pour identifier chaque segment. C'est un niveau d'indirection supplémentaire par rapport à la segmentation usuelle.
[[File:Architectures à capacité.png|centre|vignette|upright=2|Architectures à capacité]]
La liste de capacité est lisible par le programme, qui peut copier librement les capacités dans les registres. Par contre, la liste des capacités est protégée en écriture. Pour le programme, il est impossible de modifier les capacités dedans, impossible d'en rajouter, d'en forger, d'en retirer. De même, il ne peut pas accéder aux segments des autres programmes : il n'a pas les capacités pour adresser ces segments.
Pour protéger la ''C-list'' en écriture, la solution la plus utilisée consiste à placer la ''C-list'' dans un segment dédié. Le processeur gère donc plusieurs types de segments : les segments de capacité pour les ''C-list'', les autres types segments pour le reste. Un défaut de cette approche est que les adresses/capacités sont séparées des données. Or, les programmeurs mixent souvent adresses et données, notamment quand ils doivent manipuler des structures de données comme des listes chainées, des arbres, des graphes, etc.
L'usage d'une ''C-list'' permet de se passer de la séparation entre espace noyau et utilisateur ! Les segments de capacité sont eux-mêmes adressés par leur propre capacité, avec une capacité par segment de capacité. Le programme a accès à la liste de capacité, comme l'OS, mais leurs droits d'accès ne sont pas les mêmes. Le programme a une capacité vers la ''C-list'' qui n'autorise pas l'écriture, l'OS a une autre capacité qui accepte l'écriture. Les programmes ne pourront pas forger les capacités permettant de modifier les segments de capacité. Une méthode alternative est de ne permettre l'accès aux segments de capacité qu'en espace noyau, mais elle est redondante avec la méthode précédente et moins puissante.
====Les capacités dispersées, les architectures taguées====
Une solution alternative laisse les capacités dispersées en mémoire. Les capacités remplacent les adresses/pointeurs, et elles se trouvent aux mêmes endroits : sur la pile, dans le tas. Comme c'est le cas dans les programmes modernes, chaque allocation mémoire renvoie une capacité, que le programme gére comme il veut. Il peut les mettre dans des structures de données, les placer sur la pile, dans des variables en mémoire, etc. Mais il faut alors distinguer si un mot mémoire contient une capacité ou une autre donnée, les deux ne devant pas être mixés.
Pour cela, chaque mot mémoire se voit attribuer un certain bit qui indique s'il s'agit d'un pointeur/capacité ou d'autre chose. Mais cela demande un support matériel, ce qui fait que le processeur devient ce qu'on appelle une ''architecture à tags'', ou ''tagged architectures''. Ici, elles indiquent si le mot mémoire contient une adresse:capacité ou une donnée.
[[File:Architectures à capacité sans liste de capacité.png|centre|vignette|upright=2|Architectures à capacité sans liste de capacité]]
L'inconvénient est le cout en matériel de cette solution. Il faut ajouter un bit à chaque case mémoire, le processeur doit vérifier les tags avant chaque opération d'accès mémoire, etc. De plus, tous les mots mémoire ont la même taille, ce qui force les capacités à avoir la même taille qu'un entier. Ce qui est compliqué.
===Les registres de capacité===
Les architectures à capacité disposent de registres spécialisés pour les capacités, séparés pour les entiers. La raison principale est une question de sécurité, mais aussi une solution pragmatique au fait que capacités et entiers n'ont pas la même taille. Les registres dédiés aux capacités ne mémorisent pas toujours des capacités proprement dites. À la place, ils mémorisent des descripteurs de segment, qui contiennent l'adresse de base, limite et les droits d'accès. Ils sont utilisés pour la relocation des accès mémoire ultérieurs. Ils sont en réalité identiques aux registres de relocation, voire aux registres de segments. Leur utilité est d'accélérer la relocation, entre autres.
Les processeurs à capacité ne gèrent pas d'adresses proprement dit, comme pour la segmentation avec plusieurs registres de relocation. Les accès mémoire doivent préciser deux choses : à quel segment on veut accéder, à quelle position dans le segment se trouve la donnée accédée. La première information se trouve dans le mal nommé "registre de capacité", la seconde information est fournie par l'instruction d'accès mémoire soit dans un registre (Base+Index), soit en adressage base+''offset''.
Les registres de capacités sont accessibles à travers des instructions spécialisées. Le processeur ajoute des instructions LOAD/STORE pour les échanges entre table des segments et registres de capacité. Ces instructions sont disponibles en espace utilisateur, pas seulement en espace noyau. Lors du chargement d'une capacité dans ces registres, le processeur vérifie que la capacité chargée est valide, et que les droits d'accès sont corrects. Puis, il accède à la table des segments, récupère les adresses de base et limite, et les mémorise dans le registre de capacité. Les droits d'accès et d'autres méta-données sont aussi mémorisées dans le registre de capacité. En somme, l'instruction de chargement prend une capacité et charge un descripteur de segment dans le registre.
Avec ce genre de mécanismes, il devient difficile d’exécuter certains types d'attaques, ce qui est un gage de sureté de fonctionnement indéniable. Du moins, c'est la théorie, car tout repose sur l'intégrité des listes de capacité. Si on peut modifier celles-ci, alors il devient facile de pouvoir accéder à des objets auxquels on n’aurait pas eu droit.
===Le recyclage de mémoire matériel===
Les architectures à capacité séparent les adresses/capacités des nombres entiers. Et cela facilite grandement l'implémentation de la ''garbage collection'', ou '''recyclage de la mémoire''', à savoir un ensemble de techniques logicielles qui visent à libérer la mémoire inutilisée.
Rappelons que les programmes peuvent demander à l'OS un rab de mémoire pour y placer quelque chose, généralement une structure de donnée ou un objet. Mais il arrive un moment où cet objet n'est plus utilisé par le programme. Il peut alors demander à l'OS de libérer la portion de mémoire réservée. Sur les architectures à capacité, cela revient à libérer un segment, devenu inutile. La mémoire utilisée par ce segment est alors considérée comme libre, et peut être utilisée pour autre chose. Mais il arrive que les programmes ne libèrent pas le segment en question. Soit parce que le programmeur a mal codé son programme, soit parce que le compilateur n'a pas fait du bon travail ou pour d'autres raisons.
Pour éviter cela, les langages de programmation actuels incorporent des '''''garbage collectors''''', des morceaux de code qui scannent la mémoire et détectent les segments inutiles. Pour cela, ils doivent identifier les adresses manipulées par le programme. Si une adresse pointe vers un objet, alors celui-ci est accessible, il sera potentiellement utilisé dans le futur. Mais si aucune adresse ne pointe vers l'objet, alors il est inaccessible et ne sera plus jamais utilisé dans le futur. On peut libérer les objets inaccessibles.
Identifier les adresses est cependant très compliqué sur les architectures normales. Sur les processeurs modernes, les ''garbage collectors'' scannent la pile à la recherche des adresses, et considèrent tout mot mémoire comme une adresse potentielle. Mais les architectures à capacité rendent le recyclage de la mémoire très facile. Un segment est accessible si le programme dispose d'une capacité qui pointe vers ce segment, rien de plus. Et les capacités sont facilement identifiables : soit elles sont dans la liste des capacités, soit on peut les identifier à partir de leur ''tag''.
Le recyclage de mémoire était parfois implémenté directement en matériel. En soi, son implémentation est assez simple, et peu être réalisé dans le microcode d'un processeur. Une autre solution consiste à utiliser un second processeur, spécialement dédié au recyclage de mémoire, qui exécute un programme spécialement codé pour. Le programme en question est placé dans une mémoire ROM, reliée directement à ce second processeur.
===L'intel iAPX 432===
Voyons maintenat une architecture à capacité assez connue : l'Intel iAPX 432. Oui, vous avez bien lu : Intel a bel et bien réalisé un processeur orienté objet dans sa jeunesse. La conception du processeur Intel iAPX 432 commença en 1975, afin de créer un successeur digne de ce nom aux processeurs 8008 et 8080.
La conception du processeur Intel iAPX 432 commença en 1975, afin de créer un successeur digne de ce nom aux processeurs 8008 et 8080. Ce processeur s'est très faiblement vendu en raison de ses performances assez désastreuses et de défauts techniques certains. Par exemple, ce processeur était une machine à pile à une époque où celles-ci étaient tombées en désuétude, il ne pouvait pas effectuer directement de calculs avec des constantes entières autres que 0 et 1, ses instructions avaient un alignement bizarre (elles étaient bit-alignées). Il avait été conçu pour maximiser la compatibilité avec le langage ADA, un langage assez peu utilisé, sans compter que le compilateur pour ce processeur était mauvais.
====Les segments prédéfinis de l'Intel iAPX 432====
L'Intel iAPX432 gère plusieurs types de segments. Rien d'étonnant à cela, les Burrough géraient eux aussi plusieurs types de segments, à savoir des segments de programmes, des segments de données, et des segments d'I/O. C'est la même chose sur l'Intel iAPX 432, mais en bien pire !
Les segments de données sont des segments génériques, dans lequels on peut mettre ce qu'on veut, suivant les besoins du programmeur. Ils sont tous découpés en deux parties de tailles égales : une partie contenant les données de l'objet et une partie pour les capacités. Les capacités d'un segment pointent vers d'autres segments, ce qui permet de créer des structures de données assez complexes. La ligne de démarcation peut être placée n'importe où dans le segment, les deux portions ne sont pas de taille identique, elles ont des tailles qui varient de segment en segment. Il est même possible de réserver le segment entier à des données sans y mettre de capacités, ou inversement. Les capacités et données sont adressées à partir de la ligne de démarcation, qui sert d'adresse de base du segment. Suivant l'instruction utilisée, le processeur accède à la bonne portion du segment.
Le processeur supporte aussi d'autres segments pré-définis, qui sont surtout utilisés par le système d'exploitation :
* Des segments d'instructions, qui contiennent du code exécutable, typiquement un programme ou des fonctions, parfois des ''threads''.
* Des segments de processus, qui mémorisent des processus entiers. Ces segments contiennent des capacités qui pointent vers d'autres segments, notamment un ou plusieurs segments de code, et des segments de données.
* Des segments de domaine, pour les modules ou bibliothèques dynamiques.
* Des segments de contexte, utilisés pour mémoriser l'état d'un processus, utilisés par l'OS pour faire de la commutation de contexte.
* Des segments de message, utilisés pour la communication entre processus par l'intermédiaire de messages.
* Et bien d'autres encores.
Sur l'Intel iAPX 432, chaque processus est considéré comme un objet à part entière, qui a son propre segment de processus. De même, l'état du processeur (le programme qu'il est en train d’exécuter, son état, etc.) est stocké en mémoire dans un segment de contexte. Il en est de même pour chaque fonction présente en mémoire : elle était encapsulée dans un segment, sur lequel seules quelques manipulations étaient possibles (l’exécuter, notamment). Et ne parlons pas des appels de fonctions qui stockaient l'état de l'appelé directement dans un objet spécial. Bref, de nombreux objets système sont prédéfinis par le processeur : les objets stockant des fonctions, les objets stockant des processus, etc.
L'Intel 432 possédait dans ses circuits un ''garbage collector'' matériel. Pour faciliter son fonctionnement, certains bits de l'objet permettaient de savoir si l'objet en question pouvait être supprimé ou non.
====Le support de la segmentation sur l'Intel iAPX 432====
La table des segments est une table hiérarchique, à deux niveaux. Le premier niveau est une ''Object Table Directory'', qui réside toujours en mémoire RAM. Elle contient des descripteurs qui pointent vers des tables secondaires, appelées des ''Object Table''. Il y a plusieurs ''Object Table'', typiquement une par processus. Plusieurs processus peuvent partager la même ''Object Table''. Les ''Object Table'' peuvent être swappées, mais pas l{{'}}''Object Table Directory''.
Une capacité tient compte de l'organisation hiérarchique de la table des segments. Elle contient un indice qui précise quelle ''Object Table'' utiliser, et l'indice du segment dans cette ''Object Table''. Le premier indice adresse l{{'}}''Object Table Directory'' et récupère un descripteur de segment qui pointe sur la bonne ''Object Table''. Le second indice est alors utilisé pour lire l'adresse de base adéquate dans cette ''Object Table''. La capacité contient aussi des droits d'accès en lecture, écriture, suppression et copie. Il y a aussi un champ pour le type, qu'on verra plus bas. Au fait : les capacités étaient appelées des ''Access Descriptors'' dans la documentation officielle.
Une capacité fait 32 bits, avec un octet utilisé pour les droits d'accès, laissant 24 bits pour adresser les segments. Le processeur gérait jusqu'à 2^24 segments/objets différents, pouvant mesurer jusqu'à 64 kibioctets chacun, ce qui fait 2^40 adresses différentes, soit 1024 gibioctets. Les 24 bits pour adresser les segments sont partagés moitié-moitié pour l'adressage des tables, ce qui fait 4096 ''Object Table'' différentes dans l{{'}}''Object Table Directory'', et chaque ''Object Table'' contient 4096 segments.
====Le jeu d'instruction de l'Intel iAPX 432====
L'Intel iAPX 432 est une machine à pile. Le jeu d'instruction de l'Intel iAPX 432 gère pas moins de 230 instructions différentes. Il gére deux types d'instructions : les instructions normales, et celles qui manipulent des segments/objets. Les premières permettent de manipuler des nombres entiers, des caractères, des chaînes de caractères, des tableaux, etc.
Les secondes sont spécialement dédiées à la manipulation des capacités. Il y a une instruction pour copier une capacité, une autre pour invalider une capacité, une autre pour augmenter ses droits d'accès (instruction sécurisée, exécutable seulement sous certaines conditions), une autre pour restreindre ses droits d'accès. deux autres instructions créent un segment et renvoient la capacité associée, la première créant un segment typé, l'autre non.
le processeur gérait aussi des instructions spécialement dédiées à la programmation système et idéales pour programmer des systèmes d'exploitation. De nombreuses instructions permettaient ainsi de commuter des processus, faire des transferts de messages entre processus, etc. Environ 40 % du micro-code était ainsi spécialement dédié à ces instructions spéciales.
Les instructions sont de longueur variable et peuvent prendre n'importe quelle taille comprise entre 10 et 300 bits, sans vraiment de restriction de taille. Les bits d'une instruction sont regroupés en 4 grands blocs, 4 champs, qui ont chacun une signification particulière.
* Le premier est l'opcode de l'instruction.
* Le champ référence, doit être interprété différemment suivant la donnée à manipuler. Si cette donnée est un entier, un caractère ou un flottant, ce champ indique l'emplacement de la donnée en mémoire. Alors que si l'instruction manipule un objet, ce champ spécifie la capacité de l'objet en question. Ce champ est assez complexe et il est sacrément bien organisé.
* Le champ format, n'utilise que 4 bits et a pour but de préciser si les données à manipuler sont en mémoire ou sur la pile.
* Le champ classe permet de dire combien de données différentes l'instruction va devoir manipuler, et quelles seront leurs tailles.
[[File:Encodage des instructions de l'Intel iAPX-432.png|centre|vignette|upright=2|Encodage des instructions de l'Intel iAPX-432.]]
====Le support de l'orienté objet sur l'Intel iAPX 432====
L'Intel 432 permet de définir des objets, qui correspondent aux classes des langages orientés objets. L'Intel 432 permet, à partir de fonctions définies par le programmeur, de créer des '''''domain objects''''', qui correspondent à une classe. Un ''domain object'' est un segment de capacité, dont les capacités pointent vers des fonctions ou un/plusieurs objets. Les fonctions et les objets sont chacun placés dans un segment. Une partie des fonctions/objets sont publics, ce qui signifie qu'ils sont accessibles en lecture par l'extérieur. Les autres sont privées, inaccessibles aussi bien en lecture qu'en écriture.
L'exécution d'une fonction demande que le branchement fournisse deux choses : une capacité vers le ''domain object'', et la position de la fonction à exécuter dans le segment. La position permet de localiser la capacité de la fonction à exécuter. En clair, on accède au ''domain object'' d'abord, pour récupérer la capacité qui pointe vers la fonction à exécuter.
Il est aussi possible pour le programmeur de définir de nouveaux types non supportés par le processeur, en faisant appel au système d'exploitation de l'ordinateur. Au niveau du processeur, chaque objet est typé au niveau de son object descriptor : celui-ci contient des informations qui permettent de déterminer le type de l'objet. Chaque type se voit attribuer un domain object qui contient toutes les fonctions capables de manipuler les objets de ce type et que l'on appelle le type manager. Lorsque l'on veut manipuler un objet d'un certain type, il suffit d'accéder à une capacité spéciale (le TCO) qui pointera dans ce type manager et qui précisera quel est l'objet à manipuler (en sélectionnant la bonne entrée dans la liste de capacité). Le type d'un objet prédéfini par le processeur est ainsi spécifié par une suite de 8 bits, tandis que le type d'un objet défini par le programmeur est défini par la capacité spéciale pointant vers son type manager.
===Conclusion===
Pour ceux qui veulent en savoir plus, je conseille la lecture de ce livre, disponible gratuitement sur internet (merci à l'auteur pour cette mise à disposition) :
* [https://homes.cs.washington.edu/~levy/capabook/ Capability-Based Computer Systems].
Voici un document qui décrit le fonctionnement de l'Intel iAPX432 :
* [https://homes.cs.washington.edu/~levy/capabook/Chapter9.pdf The Intel iAPX 432 ]
==La pagination==
Avec la pagination, la mémoire est découpée en blocs de taille fixe, appelés des '''pages mémoires'''. La taille des pages varie suivant le processeur et le système d'exploitation et tourne souvent autour de 4 kibioctets. Mais elles sont de taille fixe : on ne peut pas en changer la taille. C'est la différence avec les segments, qui sont de taille variable. Le contenu d'une page en mémoire fictive est rigoureusement le même que le contenu de la page correspondante en mémoire physique.
L'espace d'adressage est découpé en '''pages logiques''', alors que la mémoire physique est découpée en '''pages physique''' de même taille. Les pages logiques correspondent soit à une page physique, soit à une page swappée sur le disque dur. Quand une page logique est associée à une page physique, les deux ont le même contenu, mais pas les mêmes adresses. Les pages logiques sont numérotées, en partant de 0, afin de pouvoir les identifier/sélectionner. Même chose pour les pages physiques, qui sont elles aussi numérotées en partant de 0.
[[File:Principe de la pagination.png|centre|vignette|upright=2|Principe de la pagination.]]
Pour information, le tout premier processeur avec un système de mémoire virtuelle était le super-ordinateur Atlas. Il utilisait la pagination, et non la segmentation. Mais il fallu du temps avant que la méthode de la pagination prenne son essor dans les processeurs commerciaux x86.
Un point important est que la pagination implique une coopération entre OS et hardware, les deux étant fortement mélés. Une partie des informations de cette section auraient tout autant leur place dans le wikilivre sur les systèmes d'exploitation, mais il est plus simple d'en parler ici.
===La mémoire virtuelle : le ''swapping'' et le remplacement des pages mémoires===
Le système d'exploitation mémorise des informations sur toutes les pages existantes dans une '''table des pages'''. C'est un tableau où chaque ligne est associée à une page logique. Une ligne contient un bit ''Valid'' qui indique si la page logique associée est swappée sur le disque dur ou non, et la position de la page physique correspondante en mémoire RAM. Elle peut aussi contenir des bits pour la protection mémoire, et bien d'autres. Les lignes sont aussi appelées des ''entrées de la table des pages''
[[File:Gestionnaire de mémoire virtuelle - Pagination et swapping.png|centre|vignette|upright=2|Table des pages.]]
De plus, le système d'exploitation conserve une '''liste des pages vides'''. Le nom est assez clair : c'est une liste de toutes les pages de la mémoire physique qui sont inutilisées, qui ne sont allouées à aucun processus. Ces pages sont de la mémoire libre, utilisable à volonté. La liste des pages vides est mise à jour à chaque fois qu'un programme réserve de la mémoire, des pages sont alors prises dans cette liste et sont allouées au programme demandeur.
====Les défauts de page====
Lorsque l'on veut traduire l'adresse logique d'une page mémoire, le processeur vérifie le bit ''Valid'' et l'adresse physique. Si le bit ''Valid'' est à 1 et que l'adresse physique est présente, la traduction d'adresse s'effectue normalement. Mais si ce n'est pas le cas, l'entrée de la table des pages ne contient pas de quoi faire la traduction d'adresse. Soit parce que la page est swappée sur le disque dur et qu'il faut la copier en RAM, soit parce que les droits d'accès ne le permettent pas, soit parce que la page n'a pas encore été allouée, etc. On fait alors face à un '''défaut de page'''. Un défaut de page a lieu quand la MMU ne peut pas associer l'adresse logique à une adresse physique, quelque qu'en soit la raison.
Il existe deux types de défauts de page : mineurs et majeurs. Un '''défaut de page majeur''' a lieu quand on veut accéder à une page déplacée sur le disque dur. Un défaut de page majeur lève une exception matérielle dont la routine rapatriera la page en mémoire RAM. S'il y a de la place en mémoire RAM, il suffit d'allouer une page vide et d'y copier la page chargée depuis le disque dur. Mais si ce n'est par le cas, on va devoir faire de la place en RAM en déplaçant une page mémoire de la RAM vers le disque dur. Dans tous les cas, c'est le système d'exploitation qui s'occupe du chargement de la page, le processeur n'est pas impliqué. Une fois la page chargée, la table des pages est mise à jour et la traduction d'adresse peut recommencer. Si je dis recommencer, c'est car l'accès mémoire initial est rejoué à l'identique, sauf que la traduction d'adresse réussit cette fois-ci.
Un '''défaut de page mineur''' a lieu dans des circonstances pas très intuitives : la page est en mémoire physique, mais l'adresse physique de la page n'est pas accessible. Par exemple, il est possible que des sécurités empêchent de faire la traduction d'adresse, pour des raisons de protection mémoire. Une autre raison est la gestion des adresses synonymes, qui surviennent quand on utilise des libraires partagées entre programmes, de la communication inter-processus, des optimisations de type ''copy-on-write'', etc. Enfin, une dernière raison est que la page a été allouée à un programme par le système d'exploitation, mais qu'il n'a pas encore attribué sa position en mémoire. Pour comprendre comment c'est possible, parlons rapidement de l'allocation paresseuse.
Imaginons qu'un programme fasse une demande d'allocation mémoire et se voit donc attribuer une ou plusieurs pages logiques. L'OS peut alors réagir de deux manières différentes. La première est d'attribuer une page physique immédiatement, en même temps que la page logique. En faisant ainsi, on ne peut pas avoir de défaut mineur, sauf en cas de problème de protection mémoire. Cette solution est simple, on l'appelle l{{'}}'''allocation immédiate'''. Une autre solution consiste à attribuer une page logique, mais l'allocation de la page physique se fait plus tard. Elle a lieu la première fois que le programme tente d'écrire/lire dans la page physique. Un défaut mineur a lieu, et c'est lui qui force l'OS à attribuer une page physique pour la page logique demandée. On parle alors d{{'}}'''allocation paresseuse'''. L'avantage est que l'on gagne en performance si des pages logiques sont allouées mais utilisées, ce qui peut arriver.
Une optimisation permise par l'existence des défauts mineurs est le '''''copy-on-write'''''. Le but est d'optimiser la copie d'une page logique dans une autre. L'idée est que la copie est retardée quand elle est vraiment nécessaire, à savoir quand on écrit dans la copie. Tant que l'on ne modifie pas la copie, les deux pages logiques, originelle et copiée, pointent vers la même page physique. A quoi bon avoir deux copies avec le même contenu ? Par contre, la page physique est marquée en lecture seule. La moindre écriture déclenche une erreur de protection mémoire, et un défaut mineur. Celui-ci est géré par l'OS, qui effectue alors la copie dans une nouvelle page physique.
Je viens de dire que le système d'exploitation gère les défauts de page majeurs/mineurs. Un défaut de page déclenche une exception matérielle, qui passe la main au système d'exploitation. Le système d'exploitation doit alors déterminer ce qui a levé l'exception, notamment identifier si c'est un défaut de page mineur ou majeur. Pour cela, le processeur a un ou plusieurs '''registres de statut''' qui indique l'état du processeur, qui sont utiles pour gérer les défauts de page. Ils indiquent quelle est l'adresse fautive, si l'accès était une lecture ou écriture, si l'accès a eu lieu en espace noyau ou utilisateur (les espaces mémoire ne sont pas les mêmes), etc. Les registres en question varient grandement d'une architecture de processeur à l'autre, aussi on ne peut pas dire grand chose de plus sur le sujet. Le reste est de toute façon à voir dans un cours sur les systèmes d'exploitation.
====Le remplacement des pages====
Les pages virtuelles font référence soit à une page en mémoire physique, soit à une page sur le disque dur. Mais l'on ne peut pas lire une page directement depuis le disque dur. Les pages sur le disque dur doivent être chargées en RAM, avant d'être utilisables. Ce n'est possible que si on a une page mémoire vide, libre. Si ce n'est pas le cas, on doit faire de la place en swappant une page sur le disque dur. Les pages font ainsi une sorte de va et vient entre le fichier d'échange et la RAM, suivant les besoins. Tout cela est effectué par une routine d'interruption du système d'exploitation, le processeur n'ayant pas vraiment de rôle là-dedans.
Supposons que l'on veuille faire de la place en RAM pour une nouvelle page. Dans une implémentation naïve, on trouve une page à évincer de la mémoire, qui est copiée dans le ''swapfile''. Toutes les pages évincées sont alors copiées sur le disque dur, à chaque remplacement. Néanmoins, cette implémentation naïve peut cependant être améliorée si on tient compte d'un point important : si la page a été modifiée depuis le dernier accès. Si le programme/processeur a écrit dans la page, alors celle-ci a été modifiée et doit être sauvegardée sur le ''swapfile'' si elle est évincée. Par contre, si ce n'est pas le cas, la page est soit initialisée, soit déjà présente à l'identique dans le ''swapfile''.
Mais cette optimisation demande de savoir si une écriture a eu lieu dans la page. Pour cela, on ajoute un '''''dirty bit''''' à chaque entrée de la table des pages, juste à côté du bit ''Valid''. Il indique si une écriture a eu lieu dans la page depuis qu'elle a été chargée en RAM. Ce bit est mis à jour par le processeur, automatiquement, lors d'une écriture. Par contre, il est remis à zéro par le système d'exploitation, quand la page est chargée en RAM. Si le programme se voit allouer de la mémoire, il reçoit une page vide, et ce bit est initialisé à 0. Il est mis à 1 si la mémoire est utilisée. Quand la page est ensuite swappée sur le disque dur, ce bit est remis à 0 après la sauvegarde.
Sur la majorité des systèmes d'exploitation, il est possible d'interdire le déplacement de certaines pages sur le disque dur. Ces pages restent alors en mémoire RAM durant un temps plus ou moins long, parfois en permanence. Cette possibilité simplifie la vie des programmeurs qui conçoivent des systèmes d'exploitation : essayez d'exécuter l'interruption pour les défauts de page alors que la page contenant le code de l'interruption est placée sur le disque dur ! Là encore, cela demande d'ajouter un bit dans chaque entrée de la table des pages, qui indique si la page est swappable ou non. Le bit en question s'appelle souvent le '''bit ''swappable'''''.
====Les algorithmes de remplacement des pages pris en charge par l'OS====
Le choix de la page doit être fait avec le plus grand soin et il existe différents algorithmes qui permettent de décider quelle page supprimer de la RAM. Leur but est de swapper des pages qui ne seront pas accédées dans le futur, pour éviter d'avoir à faire triop de va-et-vient entre RAM et ''swapfile''. Les données qui sont censées être accédées dans le futur doivent rester en RAM et ne pas être swappées, autant que possible. Les algorithmes les plus simples pour le choix de page à évincer sont les suivants.
Le plus simple est un algorithme aléatoire : on choisit la page au hasard. Mine de rien, cet algorithme est très simple à implémenter et très rapide à exécuter. Il ne demande pas de modifier la table des pages, ni même d'accéder à celle-ci pour faire son choix. Ses performances sont surprenamment correctes, bien que largement en-dessous de tous les autres algorithmes.
L'algorithme FIFO supprime la donnée qui a été chargée dans la mémoire avant toutes les autres. Cet algorithme fonctionne bien quand un programme manipule des tableaux de grande taille, mais fonctionne assez mal dans le cas général.
L'algorithme LRU supprime la donnée qui été lue ou écrite pour la dernière fois avant toutes les autres. C'est théoriquement le plus efficace dans la majorité des situations. Malheureusement, son implémentation est assez complexe et les OS doivent modifier la table des pages pour l'implémenter.
L'algorithme le plus utilisé de nos jours est l{{'}}'''algorithme NRU''' (''Not Recently Used''), une simplification drastique du LRU. Il fait la différence entre les pages accédées il y a longtemps et celles accédées récemment, d'une manière très binaire. Les deux types de page sont appelés respectivement les '''pages froides''' et les '''pages chaudes'''. L'OS swappe en priorité les pages froides et ne swappe de page chaude que si aucune page froide n'est présente. L'algorithme est simple : il choisit la page à évincer au hasard parmi une page froide. Si aucune page froide n'est présente, alors il swappe au hasard une page chaude.
Pour implémenter l'algorithme NRU, l'OS mémorise, dans chaque entrée de la table des pages, si la page associée est froide ou chaude. Pour cela, il met à 0 ou 1 un bit dédié : le '''bit ''Accessed'''''. La différence avec le bit ''dirty'' est que le bit ''dirty'' est mis à jour uniquement lors des écritures, alors que le bit ''Accessed'' l'est aussi lors d'une lecture. Uen lecture met à 1 le bit ''Accessed'', mais ne touche pas au bit ''dirty''. Les écritures mettent les deux bits à 1.
Implémenter l'algorithme NRU demande juste de mettre à jour le bit ''Accessed'' de chaque entrée de la table des pages. Et sur les architectures modernes, le processeur s'en charge automatiquement. A chaque accès mémoire, que ce soit en lecture ou en écriture, le processeur met à 1 ce bit. Par contre, le système d'exploitation le met à 0 à intervalles réguliers. En conséquence, quand un remplacement de page doit avoir lieu, les pages chaudes ont de bonnes chances d'avoir le bit ''Accessed'' à 1, alors que les pages froides l'ont à 0. Ce n'est pas certain, et on peut se trouver dans des cas où ce n'est pas le cas. Par exemple, si un remplacement a lieu juste après la remise à zéro des bits ''Accessed''. Le choix de la page à remplacer est donc imparfait, mais fonctionne bien en pratique.
Tous les algorithmes précédents ont chacun deux variantes : une locale, et une globale. Avec la version locale, la page qui va être rapatriée sur le disque dur est une page réservée au programme qui est la cause du page miss. Avec la version globale, le système d'exploitation va choisir la page à virer parmi toutes les pages présentes en mémoire vive.
===La protection mémoire avec la pagination===
Avec la pagination, chaque page a des '''droits d'accès''' précis, qui permettent d'autoriser ou interdire les accès en lecture, écriture, exécution, etc. La table des pages mémorise les autorisations pour chaque page, sous la forme d'une suite de bits où chaque bit autorise/interdit une opération bien précise. En pratique, les tables de pages modernes disposent de trois bits : un qui autorise/interdit les accès en lecture, un qui autorise/interdit les accès en écriture, un qui autorise/interdit l'éxecution du contenu de la page.
Le format exact de la suite de bits a cependant changé dans le temps sur les processeurs x86 modernes. Par exemple, avant le passage au 64 bits, les CPU et OS ne pouvaient pas marquer une page mémoire comme non-exécutable. C'est seulement avec le passage au 64 bits qu'a été ajouté un bit pour interdire l'exécution de code depuis une page. Ce bit, nommé '''bit NX''', est à 0 si la page n'est pas exécutable et à 1 sinon. Le processeur vérifie à chaque chargement d'instruction si le bit NX de page lue est à 1. Sinon, il lève une exception matérielle et laisse la main à l'OS.
Une amélioration de cette protection est la technique dite du '''''Write XOR Execute''''', abréviée WxX. Elle consiste à interdire les pages d'être à la fois accessibles en écriture et exécutables. Il est possible de changer les autorisations en cours de route, ceci dit.
Les premiers IBM 360 disposaient d'un mécanisme de protection mémoire totalement différent, sans registres limite/base. Ce mécanisme de protection attribue à chaque programme une '''clé de protection''', qui consiste en un nombre unique de 4 bits (chaque programme a donc une clé différente de ses collègues). La mémoire est fragmentée en blocs de même taille, de 2 kibioctets. Le processeur mémorise, pour chacun de ses blocs, la clé de protection du programme qui a réservé ce bloc. À chaque accès mémoire, le processeur compare la clé de protection du programme en cours d’exécution et celle du bloc de mémoire de destination. Si les deux clés sont différentes, alors un programme a effectué un accès hors des clous et il se fait sauvagement arrêter.
===La traduction d'adresse avec la pagination===
Comme dit plus haut, les pages sont numérotées, de 0 à une valeur maximale, afin de les identifier. Le numéro en question est appelé le '''numéro de page'''. Il est utilisé pour dire au processeur : je veux lire une donnée dans la page numéro 20, la page numéro 90, etc. Une fois qu'on a le numéro de page, on doit alors préciser la position de la donnée dans la page, appelé le '''décalage''', ou encore l{{'}}''offset''.
Le numéro de page et le décalage se déduisent à partir de l'adresse, en divisant l'adresse par la taille de la page. Le quotient obtenu donne le numéro de la page, alors que le reste est le décalage. Les processeurs actuels utilisent tous des pages dont la taille est une puissance de deux, ce qui fait que ce calcul est fortement simplifié. Sous cette condition, le numéro de page correspond aux bits de poids fort de l'adresse, alors que le décalage est dans les bits de poids faible.
Le numéro de page existe en deux versions : un numéro de page physique qui identifie une page en mémoire physique, et un numéro de page logique qui identifie une page dans la mémoire virtuelle. Traduire l'adresse logique en adresse physique demande de remplacer le numéro de la page logique en un numéro de page physique.
[[File:Phycical address.JPG|centre|vignette|upright=2|Traduction d'adresse avec la pagination.]]
====Les tables des pages simples====
Dans le cas le plus simple, il n'y a qu'une seule table des pages, qui est adressée par les numéros de page logique. La table des pages est un vulgaire tableau d'adresses physiques, placées les unes à la suite des autres. Avec cette méthode, la table des pages a autant d'entrée qu'il y a de pages logiques en mémoire virtuelle. Accéder à la mémoire nécessite donc d’accéder d'abord à la table des pages en mémoire, de calculer l'adresse de l'entrée voulue, et d’y accéder.
[[File:Table des pages.png|centre|vignette|upright=2|Table des pages.]]
La table des pages est souvent stockée dans la mémoire RAM, son adresse est connue du processeur, mémorisée dans un registre spécialisé du processeur. Le processeur effectue automatiquement le calcul d'adresse à partir de l'adresse de base et du numéro de page logique.
[[File:Address translation (32-bit).png|centre|vignette|upright=2|Address translation (32-bit)]]
====Les tables des pages inversées====
Sur certains systèmes, notamment sur les architectures 64 bits ou plus, le nombre de pages est très important. Sur les ordinateurs x86 récents, les adresses sont en pratique de 48 bits, les bits de poids fort étant ignorés en pratique, ce qui fait en tout 68 719 476 736 pages. Chaque entrée de la table des pages fait au minimum 48 bits, mais fait plus en pratique : partons sur 64 bits par entrée, soit 8 octets. Cela fait 549 755 813 888 octets pour la table des pages, soit plusieurs centaines de gibioctets ! Une table des pages normale serait tout simplement impraticable.
Pour résoudre ce problème, on a inventé les '''tables des pages inversées'''. L'idée derrière celles-ci est l'inverse de la méthode précédente. La méthode précédente stocke, pour chaque page logique, son numéro de page physique. Les tables des pages inversées font l'inverse : elles stockent, pour chaque numéro de page physique, la page logique qui correspond. Avec cette méthode table des pages contient ainsi autant d'entrées qu'il y a de pages physiques. Elle est donc plus petite qu'avant, vu que la mémoire physique est plus petite que la mémoire virtuelle.
Quand le processeur veut convertir une adresse virtuelle en adresse physique, la MMU recherche le numéro de page de l'adresse virtuelle dans la table des pages. Le numéro de l'entrée à laquelle se trouve ce morceau d'adresse virtuelle est le morceau de l'adresse physique. Pour faciliter le processus de recherche dans la page, la table des pages inversée est ce que l'on appelle une table de hachage. C'est cette solution qui est utilisée sur les processeurs Power PC.
[[File:Table des pages inversée.jpg|centre|vignette|upright=2|Table des pages inversée.]]
====Les tables des pages multiples par espace d'adressage====
Dans les deux cas précédents, il y a une table des pages unique. Cependant, les concepteurs de processeurs et de systèmes d'exploitation ont remarqué que les adresses les plus hautes et/ou les plus basses sont les plus utilisées, alors que les adresses situées au milieu de l'espace d'adressage sont peu utilisées en raison du fonctionnement de la pile et du tas. Il y a donc une partie de la table des pages qui ne sert à rien et est utilisé pour des adresses inutilisées. C'est une source d'économie d'autant plus importante que les tables des pages sont de plus en plus grosses.
Pour profiter de cette observation, les concepteurs d'OS ont décidé de découper l'espace d'adressage en plusieurs sous-espaces d'adressage de taille identique : certains localisés dans les adresses basses, d'autres au milieu, d'autres tout en haut, etc. Et vu que l'espace d'adressage est scindé en plusieurs parties, la table des pages l'est aussi, elle est découpée en plusieurs sous-tables. Si un sous-espace d'adressage n'est pas utilisé, il n'y a pas besoin d'utiliser de la mémoire pour stocker la table des pages associée. On ne stocke que les tables des pages pour les espaces d'adressage utilisés, ceux qui contiennent au moins une donnée.
L'utilisation de plusieurs tables des pages ne fonctionne que si le système d'exploitation connaît l'adresse de chaque table des pages (celle de la première entrée). Pour cela, le système d'exploitation utilise une super-table des pages, qui stocke les adresses de début des sous-tables de chaque sous-espace. En clair, la table des pages est organisé en deux niveaux, la super-table étant le premier niveau et les sous-tables étant le second niveau.
L'adresse est structurée de manière à tirer profit de cette organisation. Les bits de poids fort de l'adresse sélectionnent quelle table de second niveau utiliser, les bits du milieu de l'adresse sélectionne la page dans la table de second niveau et le reste est interprété comme un ''offset''. Un accès à la table des pages se fait comme suit. Les bits de poids fort de l'adresse sont envoyés à la table de premier niveau, et sont utilisés pour récupérer l'adresse de la table de second niveau adéquate. Les bits au milieu de l'adresse sont envoyés à la table de second niveau, pour récupérer le numéro de page physique. Le tout est combiné avec l{{'}}''offset'' pour obtenir l'adresse physique finale.
[[File:Table des pages hiérarchique.png|centre|vignette|upright=2|Table des pages hiérarchique.]]
On peut aussi aller plus loin et découper la table des pages de manière hiérarchique, chaque sous-espace d'adressage étant lui aussi découpé en sous-espaces d'adressages. On a alors une table de premier niveau, plusieurs tables de second niveau, encore plus de tables de troisième niveau, et ainsi de suite. Cela peut aller jusqu'à 5 niveaux sur les processeurs x86 64 bits modernes. On parle alors de '''tables des pages emboitées'''. Dans ce cours, la table des pages désigne l'ensemble des différents niveaux de cette organisation, toutes les tables inclus. Seules les tables du dernier niveau mémorisent des numéros de page physiques, les autres tables mémorisant des pointeurs, des adresses vers le début des tables de niveau inférieur. Un exemple sera donné plus bas, dans la section suivante.
====L'exemple des processeurs x86====
Pour rendre les explications précédentes plus concrètes, nous allons prendre l'exemple des processeur x86 anciens, de type 32 bits. Les processeurs de ce type utilisaient deux types de tables des pages : une table des page unique et une table des page hiérarchique. Les deux étaient utilisées dans cas séparés. La table des page unique était utilisée pour les pages larges et encore seulement en l'absence de la technologie ''physical adress extension'', dont on parlera plus bas. Les autres cas utilisaient une table des page hiérarchique, à deux niveaux, trois niveaux, voire plus.
Une table des pages unique était utilisée pour les pages larges (de 2 mébioctets et plus). Pour les pages de 4 mébioctets, il y avait une unique table des pages, adressée par les 10 bits de poids fort de l'adresse, les bits restants servant comme ''offset''. La table des pages contenait 1024 entrées de 4 octets chacune, ce qui fait en tout 4 kibioctet pour la table des pages. La table des page était alignée en mémoire sur un bloc de 4 kibioctet (sa taille).
[[File:X86 Paging 4M.svg|centre|vignette|upright=2|X86 Paging 4M]]
Pour les pages de 4 kibioctets, les processeurs x86-32 bits utilisaient une table des page hiérarchique à deux niveaux. Les 10 bits de poids fort l'adresse adressaient la table des page maitre, appelée le directoire des pages (''page directory''), les 10 bits précédents servaient de numéro de page logique, et les 12 bits restants servaient à indiquer la position de l'octet dans la table des pages. Les entrées de chaque table des pages, mineure ou majeure, faisaient 32 bits, soit 4 octets. Vous remarquerez que la table des page majeure a la même taille que la table des page unique obtenue avec des pages larges (de 4 mébioctets).
[[File:X86 Paging 4K.svg|centre|vignette|upright=2|X86 Paging 4K]]
La technique du '''''physical adress extension''''' (PAE), utilisée depuis le Pentium Pro, permettait aux processeurs x86 32 bits d'adresser plus de 4 gibioctets de mémoire, en utilisant des adresses physiques de 64 bits. Les adresses virtuelles de 32 bits étaient traduites en adresses physiques de 64 bits grâce à une table des pages adaptée. Cette technologie permettait d'adresser plus de 4 gibioctets de mémoire au total, mais avec quelques limitations. Notamment, chaque programme ne pouvait utiliser que 4 gibioctets de mémoire RAM pour lui seul. Mais en lançant plusieurs programmes, on pouvait dépasser les 4 gibioctets au total. Pour cela, les entrées de la table des pages passaient à 64 bits au lieu de 32 auparavant.
La table des pages gardait 2 niveaux pour les pages larges en PAE.
[[File:X86 Paging PAE 2M.svg|centre|vignette|upright=2|X86 Paging PAE 2M]]
Par contre, pour les pages de 4 kibioctets en PAE, elle était modifiée de manière à ajouter un niveau de hiérarchie, passant de deux niveaux à trois.
[[File:X86 Paging PAE 4K.svg|centre|vignette|upright=2|X86 Paging PAE 4K]]
En 64 bits, la table des pages est une table des page hiérarchique avec 5 niveaux. Seuls les 48 bits de poids faible des adresses sont utilisés, les 16 restants étant ignorés.
[[File:X86 Paging 64bit.svg|centre|vignette|upright=2|X86 Paging 64bit]]
===La TLB et les caches associés===
En théorie, la table des pages est censée être accédée à chaque accès mémoire. Mais pour éviter d'avoir à lire la table des pages en mémoire RAM à chaque accès mémoire, les concepteurs de processeurs ont décidé d'implanter un cache dédié, le '''''translation lookaside buffer''''', ou TLB. Le TLB stocke au minimum de quoi faire la traduction entre adresse virtuelle et adresse physique, à savoir une correspondance entre numéro de page logique et numéro de page physique. Pour faire plus général, il stocke des entrées de la table des pages.
[[File:MMU principle updated.png|centre|vignette|upright=2.0|MMU avec une TLB.]]
Les accès à la table des pages sont gérés de deux façons : soit le processeur gère tout seul la situation, soit il délègue cette tâche au système d’exploitation. Sur les processeurs anciens, le système d'exploitation gère le parcours de la table des pages. Mais cette solution logicielle n'a pas de bonnes performances. D'autres processeurs gèrent eux-mêmes le défaut d'accès à la TLB et vont chercher d'eux-mêmes les informations nécessaires dans la table des pages. Ils disposent de circuits, les '''''page table walkers''''' (PTW), qui s'occupent eux-mêmes du défaut.
Les ''page table walkers'' contiennent des registres qui leur permettent de faire leur travail. Le plus important est celui qui mémorise la position de la table des pages en mémoire RAM, dont nous avons parlé plus haut. Les PTW ont besoin, pour faire leur travail, de mémoriser l'adresse physique de la table des pages, ou du moins l'adresse de la table des pages de niveau 1 pour des tables des pages hiérarchiques. Mais d'autres registres existent. Toutes les informations nécessaires pour gérer les défauts de TLB sont stockées dans des registres spécialisés appelés des '''tampons de PTW''' (PTW buffers).
===L'abstraction matérielle des processus===
[[File:Memoire virtuelle.svg|vignette|Mémoire virtuelle]]
Il est possible d'implémenter l'abstraction matérielle des processus avec la pagination. En clair, chaque programme lancé sur l'ordinateur dispose de son propre espace d'adressage, ce qui fait que la même adresse logique ne pointera pas sur la même adresse physique dans deux programmes différents. Pour cela, il y a plusieurs méthodes.
La première solution n'utilise qu'une seule table des pages. Pour cela, chaque entrée indique à quel processus elle est associée. Pour cela, chaque entrée contient un '''identifiant de processus''', un numéro qui précise pour quel processus, pour quel espace d'adressage, la correspondance est valide. La page des tables peut aussi contenir des entrées qui sont valides pour tous les processus en même temps. Pour cela, l'entrée contient un '''bit ''global''''', qui précise que cette correspondance est valide pour tous les processus, l'identifiant de processus est ignoré. Un défaut est que le partage d'une page entre plusieurs processus est impossible : soit on partage une page avec tous les processus, soit on l'alloue avec un seul processus.
L'intérêt de partager des pages entre tous les processus n'est pas évident, mais il le devient quand on se rappelle que le noyau de l'OS est mappé dans le haut de l'espace d'adressage. Et peu importe l'espace d'adressage, le noyau est toujours mappé de manière identique, les mêmes adresses logiques adressant la même adresse mémoire. En conséquence, les correspondances adresse physique-logique sont les mêmes pour le noyau, peu importe l'espace d'adressage. Le bit global accélère rapidement la traduction d'adresse pour l'accès au noyau.
Une solution alternative, plus simple, utilise une table des pages par processus lancé sur l'ordinateur, une table des pages unique par espace d'adressage. À chaque changement de processus, le registre qui mémorise la position de la table des pages est modifié pour pointer sur la bonne. C'est le système d'exploitation qui se charge de cette mise à jour.
Avec cette méthode, il est possible de partager une ou plusieurs pages entre plusieurs processus, en configurant les tables des pages convenablement. Les pages partagées sont mappées dans l'espace d'adressage de plusieurs processus, mais pas forcément au même endroit, pas forcément dans les mêmes adresses logiques. On peut placer la page partagée à l'adresse logique 0x0FFF pour un processus, à l'adresse logique 0xFF00 pour un autre processus, etc. Par contre, les entrées de la table des pages pour ces adresses pointent vers la même adresse physique.
[[File:Vm5.png|centre|vignette|upright=2|Tables des pages de plusieurs processus.]]
===La taille des pages===
La taille des pages varie suivant le processeur et le système d'exploitation et tourne souvent autour de 4 kibioctets. Les processeurs actuels gèrent plusieurs tailles différentes pour les pages : 4 kibioctets par défaut, 2 mébioctets, voire 1 à 4 gibioctets pour les pages les plus larges. Les pages de 4 kibioctets sont les pages par défaut, les autres tailles de page sont appelées des ''pages larges''. La taille optimale pour les pages dépend de nombreux paramètres et il n'y a pas de taille qui convienne à tout le monde. Certaines applications gagnent à utiliser des pages larges, d'autres vont au contraire perdre drastiquement en performance en les utilisant.
Le désavantage principal des pages larges est qu'elles favorisent la fragmentation mémoire. Si un programme veut réserver une portion de mémoire, pour une structure de donnée quelconque, il doit réserver une portion dont la taille est multiple de la taille d'une page. Par exemple, un programme ayant besoin de 110 kibioctets allouera 28 pages de 4 kibioctets, soit 120 kibioctets : 2 kibioctets seront perdus. Par contre, avec des pages larges de 2 mébioctets, on aura une perte de 2048 - 110 = 1938 kibioctets. En somme, des morceaux de mémoire seront perdus, car les pages sont trop grandes pour les données qu'on veut y mettre. Le résultat est que le programme qui utilise les pages larges utilisent plus de mémoire et ce d'autant plus qu'il utilise des données de petite taille. Un autre désavantage est qu'elles se marient mal avec certaines techniques d'optimisations de type ''copy-on-write''.
Mais l'avantage est que la traduction des adresses est plus performante. Une taille des pages plus élevée signifie moins de pages, donc des tables des pages plus petites. Et des pages des tables plus petites n'ont pas besoin de beaucoup de niveaux de hiérarchie, voire peuvent se limiter à des tables des pages simples, ce qui rend la traduction d'adresse plus simple et plus rapide. De plus, les programmes ont une certaine localité spatiale, qui font qu'ils accèdent souvent à des données proches. La traduction d'adresse peut alors profiter de systèmes de mise en cache dont nous parlerons dans le prochain chapitre, et ces systèmes de cache marchent nettement mieux avec des pages larges.
Il faut noter que la taille des pages est presque toujours une puissance de deux. Cela a de nombreux avantages, mais n'est pas une nécessité. Par exemple, le tout premier processeur avec de la pagination, le super-ordinateur Atlas, avait des pages de 3 kibioctets. L'avantage principal est que la traduction de l'adresse physique en adresse logique est trivial avec une puissance de deux. Cela garantit que l'on peut diviser l'adresse en un numéro de page et un ''offset'' : la traduction demande juste de remplacer les bits de poids forts par le numéro de page voulu. Sans cela, la traduction d'adresse implique des divisions et des multiplications, qui sont des opérations assez couteuses.
===Les entrées de la table des pages===
Avant de poursuivre, faisons un rapide rappel sur les entrées de la table des pages. Nous venons de voir que la table des pages contient de nombreuses informations : un bit ''valid'' pour la mémoire virtuelle, des bits ''dirty'' et ''accessed'' utilisés par l'OS, des bits de protection mémoire, un bit ''global'' et un potentiellement un identifiant de processus, etc. Étudions rapidement le format de la table des pages sur un processeur x86 32 bits.
* Elle contient d'abord le numéro de page physique.
* Les bits AVL sont inutilisés et peuvent être configurés à loisir par l'OS.
* Le bit G est le bit ''global''.
* Le bit PS vaut 0 pour une page de 4 kibioctets, mais est mis à 1 pour une page de 4 mébioctets dans le cas où le processus utilise des pages larges.
* Le bit D est le bit ''dirty''.
* Le bit A est le bit ''accessed''.
* Le bit PCD indique que la page ne peut pas être cachée, dans le sens où le processeur ne peut copier son contenu dans le cache et doit toujours lire ou écrire cette page directement dans la RAM.
* Le bit PWT indique que les écritures doivent mettre à jour le cache et la page en RAM (dans le chapitre sur le cache, on verra qu'il force le cache à se comporter comme un cache ''write-through'' pour cette page).
* Le bit U/S précise si la page est accessible en mode noyau ou utilisateur.
* Le bit R/W indique si la page est accessible en écriture, toutes les pages sont par défaut accessibles en lecture.
* Le bit P est le bit ''valid''.
[[File:PDE.png|centre|vignette|upright=2.5|Table des pages des processeurs Intel 32 bits.]]
==Comparaison des différentes techniques d'abstraction mémoire==
Pour résumer, l'abstraction mémoire permet de gérer : la relocation, la protection mémoire, l'isolation des processus, la mémoire virtuelle, l'extension de l'espace d'adressage, le partage de mémoire, etc. Elles sont souvent implémentées en même temps. Ce qui fait qu'elles sont souvent confondues, alors que ce sont des concepts sont différents. Ces liens sont résumés dans le tableau ci-dessous.
{|class="wikitable"
|-
!
! colspan="5" | Avec abstraction mémoire
! rowspan="2" | Sans abstraction mémoire
|-
!
! Relocation matérielle
! Segmentation en mode réel (x86)
! Segmentation, général
! Architectures à capacités
! Pagination
|-
! Abstraction matérielle des processus
| colspan="4" | Oui, relocation matérielle
| Oui, liée à la traduction d'adresse
| Emulée via relocation logicielle au démarrage du programme.
|-
! Mémoire virtuelle
| colspan="2" | Non, sauf émulation logicielle
| colspan="3" | Oui, gérée par le processeur et l'OS
| Non, sauf émulation logicielle
|-
! Extension de l'espace d'adressage
| colspan="2" | Oui : registre de base élargi
| colspan="2" | Oui : adresse de base élargie dans la table des segments
| ''Physical Adress Extension'' des processeurs 32 bits
| Commutation de banques
|-
! Protection mémoire
| Registre limite
| Aucune
| colspan="2" | Registre limite, droits d'accès aux segments
| Gestion des droits d'accès aux pages
| Possible, méthodes variées
|-
! Partage de mémoire
| colspan="2" | Non
| colspan="2" | Segment partagés
| Pages partagées
| Possible, méthodes variées
|}
===Les différents types de segmentation===
La segmentation regroupe plusieurs techniques franchement différentes, qui auraient gagné à être nommées différemment. La principale différence est l'usage de registres de relocation versus des registres de sélecteurs de segments. L'usage de registres de relocation est le fait de la relocation matérielle, mais aussi de la segmentation en mode réel des CPU x86. Par contre, l'usage de sélecteurs de segments est le fait des autres formes de segmentation, architectures à capacité inclues.
La différence entre les deux est le nombre de segments. L'usage de registres de relocation fait que le CPU ne gère qu'un petit nombre de segments de grande taille. La mémoire virtuelle est donc rarement implémentée vu que swapper des segments de grande taille est trop long, l'impact sur les performances est trop important. Sans compter que l'usage de registres de base se marie très mal avec la mémoire virtuelle. Vu qu'un segment peut être swappé ou déplacée n'importe quand, il faut invalider les registres de base au moment du swap/déplacement, ce qui n'est pas chose aisée. Aucun processeur ne gère cela, les méthodes pour n'existent tout simplement pas. L'usage de registres de base implique que la mémoire virtuelle est absente.
La protection mémoire est aussi plus limitée avec l'usage de registres de relocation. Elle se limite à des registres limite, mais la gestion des droits d'accès est limitée. En théorie, la segmentation en mode réel pourrait implémenter une version limitée de protection mémoire, avec une protection de l'espace exécutable. Mais ca n'a jamais été fait en pratique sur les processeurs x86.
Le partage de la mémoire est aussi difficile sur les architectures avec des registres de base. L'absence de table des segments fait que le partage d'un segment est basiquement impossible sans utiliser des méthodes complétement tordues, qui ne sont jamais implémentées en pratique.
===Segmentation versus pagination===
Par rapport à la pagination, la segmentation a des avantages et des inconvénients. Tous sont liés aux propriétés des segments et pages : les segments sont de grande taille et de taille variable, les pages sont petites et de taille fixe.
L'avantage principal de la segmentation est sa rapidité. Le fait que les segments sont de grande taille fait qu'on a pas besoin d'équivalent aux tables des pages inversée ou multiple, juste d'une table des segments toute simple. De plus, les échanges entre table des pages/segments et registres sont plus rares avec la segmentation. Par exemple, si un programme utilise un segment de 2 gigas, tous les accès dans le segment se feront avec une seule consultation de la table des segments. Alors qu'avec la pagination, il faudra une consultation de la table des pages chaque bloc de 4 kibioctet, au minimum.
Mais les désavantages sont nombreux. Le système d'exploitation doit agencer les segments en RAM, et c'est une tâche complexe. Le fait que les segments puisse changer de taille rend le tout encore plus complexe. Par exemple, si on colle les segments les uns à la suite des autres, changer la taille d'un segment demande de réorganiser tous les segments en RAM, ce qui demande énormément de copies RAM-RAM. Une autre possibilité est de laisser assez d'espace entre les segments, mais cet espace est alors gâché, dans le sens où on ne peut pas y placer un nouveau segment.
Swapper un segment est aussi très long, vu que les segments sont de grande taille, alors que swapper une page est très rapide.
==Un cas particulier : la z/architecture d'IBM==
IBM a sa propre gamme de ''mainframes'', qui utilisent des processeurs fait maison par IBM lui-même. Les CPU IBM ont leur propre jeu d'instruction CISC, appelé '''''z/architecture'''''. Et ce jeu d'instruction a des possibilités en termes d'adressage qui sont extrêmement intéressantes, en plus d'être assez inédites.
Nous allons nous concentrer sur une fonctionnalité : les programmes ont accès à plusieurs espaces d'adressage simultanément. La ''z/architecture'' supporte 65 536 espaces d'adressage simultanés. Par contre, un programme ne peut en utiliser que 2048 en même temps. De plus, ils ne sont accessibles que quand le processeur est dans un mode de fonctionnement bien précis. Un processeur ''z/architecture'' peut en effet fonctionner dans deux modes, qu'on va appeler "standard" et "étendu".
L'existence de ces deux modes s'explique par le fait que les ''mainframes'' IBM ont évolués dans le temps. Les premiers ''mainframes'' n'avaient même pas de support basique de la mémoire virtuelle, le System/360 s'en passait totalement. C'est sur le System/370 que la pagination a été ajoutée. Le System/370 supportait deux espaces d'adressage virtuel par programme, là où les implémentations classiques n'en utilisent qu'un par programme. L'ESA/370 a ajouté la possibilité d'utiliser 16 espaces d'adressage par programme, mais l'a fait d'une manière à garder la compatibilité avec le System/370. Le processeur a donc deux modes : un avec deux espaces d'adressage par programme, un autre pouvant monter jusqu’à 16.
La taille des adresses a aussi évoluée dans le temps : partant de 24 bits sur le System/360, elle est montée à 31 bits sur le System/370, pour arriver à 64 bits sur le jeu d'instruction z/architecture proprement dit. Sur le System/370, il était possible de switcher à la volée entre adressage 24 et 31 bits, avec les instructions ''Branch and save and
set mode'' (BASSM) et ''Branch And Set Mode'' (BSM).
===Le System/370===
Le System/370 gérait des pages de 4 kibioctets, les pages larges ayant été introduites plus tard. La traduction d’adresse se fait en passant des regroupements de plusieurs pages virtuelles, appelés des ''segments'', qui n'ont rien à voir avec les segments au sens de la segmentation. Pour éviter les confusions, je vais les appeler des ''super-pages''. Les super-pages font 64 kibioctets ou 1 mébioctet, et regroupent plusieurs pages de 2 ou 4 mébioctets. Les pages d'une super-page peuvent être très éloignées en mémoire physique. Les super-pages sont concrètement des intermédiaires utilisés pour la traduction d'adresse. La traduction d'adresse démarre à une table des super-pages, qui point vers une table des pages à plusieurs niveaux (5 maximum).
Le System/370 était un système multi-processeur, ce qui a amené à quelques spécificités en termes d'adressage mémoire. La page numéro 0 sert pour le vecteur d'interruption et d'autres données importantes, pour des données importantes. Et chaque processeur a besoin de sa propre table d'interruption. Pour cela, chaque processeur réserve une page dédiée pour son vecteur d'interruption. Quand ce processeur accède à une adresse située dans la page numéro 0, l'adresse est modifiée pour tomber dans cette page réservée. Notons que le numéro de page converti est un numéro de page physique ! Le processeur fait donc une première traduction d'adresse pour déterminer l'adresse physique, et celle-ci est corrigée une seconde fois en cas d'accès à la première page mémoire. Le tout porte le nom de technique de '''''prefixing'''''.
Le System/370 supporte deux espaces d'adressages par programme : l''''espace d'adressage primaire''' où se situent le programme exécuté et les données importantes, un '''espace d'adressage secondaire''' utilisé pour des tâches/données annexes. Un troisième espace d'adressage l''''espace d'adressage ''home''''', est réservé au système d'exploitation. Il a pour particularité de ne pas avoir d'ASN. Il n'est concrètement accessible qu'en mode superviseur, donc par le système d'exploitation. C'est en quelque sorte un espace d'adressage pour le noyau, séparé des espaces d'adressage utilisateur. Nous n'en parlerons pas dans ce qui suit.
Mais comment le processeur pour utiliser ces espaces d'adressages ? Par exemple, s'il veut faire une lecture, comment précise-t-il l'espace d'adressage courant ? La réponse est que le processeur peut être dans deux états : soit primaire, soit secondaire. Dans le premier état, toutes les lectures/écritures se font dans l'espace d'adressage primaire. Dans le second, elles se font toutes dans l'espace d'adressage secondaire. Il peut passer de l'un à l'autre en utilisant l'instruction ''Set Address Space Control''. Le processeur dispose aussi d'instructions ''MOVE TO PRIMARY'' et ''MOVE TO SECONDARY'' pour échanger des données dans les deux espaces d'adressage.
L'espace d'adressage primaire a une table des pages dédiée, séparée de celle de l'espace d'adressage secondaire. Ou plutôt devrais-je dire une table des super-pages. La table des pages est une table des pages emboitée. Il y a peu à détailler dessus, si ce n'est pour cette histoire de segments/super-pages. L'adresse de la table des super-page est mémorisée dans le registre de contrôle CR1 pour l'espace d'adressage primaire, CR7 pour l'espace d'adressage secondaire. L'un ou l'autre est utilisé, selon que le processeur est en mode "primaire" ou "secondaire".
===Les registres d'accès de la ''z/architecture''===
Un programme peut donc passer de l'espace d'adressage primaire à secondaire (ou inversement) en utilisant l’instruction ''SET ADDRESS SPACE CONTROL''. Mais il peut en profiter pour passer dans un mode où il a accès à 2048 espaces d'adressage différents, en même temps. Le processeur n'a pas besoin d'utiliser d'instructions particulières pour copier des données d'un espace d'adressage à un autre, un simple MOV suffit. Le mode en question est appelé le '''mode ''access register'''''.
Le mode ''access register'' donne accès aux '''registres d'accès''', qui lui donnent son nom. Les registres d'accès mémorisent des identifiants qui permettent de sélectionner un espace d'adressage parmi 2048. Il y a en tout 16 registres d'accès, chacun étant appairé avec un registre général. Par exemple, le registre général numéro 5 est appairé au registre d'accès de même numéro. En mode ''access register'', toute lecture/écriture lit l'adresse dans un registre général et le registre d'accès associé indique dans quel espace d'adressage se trouve cette adresse. Par exemple, une lecture utilise l'adresse dans le registre général numéro 7, l'espace d'adressage associé sera dans le registre d'accès numéro 7.
Les registres d'accès peuvent être modifiés par des instructions classiques, en espace utilisateur. Elles sont nommées COPY ACCESS, EXTRACT ACCESS, LOAD ACCESS MULTIPLE, LOAD ADDRESS EXTENDED, SET ACCESS et STORE ACCESS MULTIPLE. Elles permettent de copier un registre d'accès dans un autre, de charger le contenu d'un registre d'accès depuis la mémoire RAM, d'enregistrer les registres d'accès dans la mémoire RAM, de copier une constante dans un registre d'accès. Mais quel est le contenu de ces registres ?
Une implémentation simple numéroterait les espaces d'adressage et mettrait les numéros dans les registres d'accès. Un espace d'adressage serait associé à une table des super-pages, reste à mémoriser la correspondance ''numéro d'espace d'adressage - adresse de la table des super-pages''. Pour cela, l'idéal est de mémoriser ces correspondances dans un tableau en mémoire RAM. Le numéro d'espace d'adressage est alors un indice, qui pointe sur l'entrée adéquate dans cette table. Il devrait y avoir un tableau par programme, qui serait protégé en écriture et seulement accessible par le système d'exploitation, comme le serait une table des pages. Mais ce n'est pas exactement ce qui est fait.
===L'utilisation des registres d'accès===
La différence est qu'il n'y a pas une table de correspondance par programme, mais un système à deux tableaux imbriqués. Le premier tableau possède 65 536 entrées, une par espace d'adressage supporté par le processeur. C'est donc une table partagée par tous les programmes. Par contre, la seconde table est bien unique pour chaque programme, chaque programme a la sienne. Faire ainsi permet de simplifier le partage de la mémoire. La table partagée est l''''''Access Space Table Entry''''' (ASTE), alors que la seconde table est la '''liste privée''' (''access list''), sous-entendu "privée pour un programme".
La liste privée mémorise une liste des espaces d'adressage auquel le programme peut accéder, auxquels le système d'exploitation lui a accordé l'accès. Pour chaque espace d'adressage, elle renvoie vers l'entrée de l'ASTE associée. En plus de mémoriser l'adresse de cette entrée, elle mémorise les droits d'accès que le programme a sur l'espace d'adressage associé. Par exemple, si deux programmes partagent un espace d'adressage, l'un pourra y avoir accès en lecture seule, l'autre pourra avoir un accès total. Pour cela, ils ont des droits différents dans la liste privée, mais l'entrée de l'ASTE sera la même.
[[File:Tradiction d'adresse sur la zarchitecture d'IBM.png|centre|vignette|upright=2|Traduction d'adresse sur la zarchitecture d'IBM.]]
Il y a donc une vérification des droits d'accès, suivi d'un accès à une table des super-pages globale. Si un programme veut accéder à un espace d'adressage qui lui est interdit, l'accès mémoire stoppera dans la liste privée. Le processeur utilisera l'identifiant d'espace d'adressage, mais la liste privée n'aura pas d'entrée associée. Elle lèvera alors une exception matérielle et le système d'exploitation gérera la situation, vraisemblablement en tuant le programme fautif. Mais si l'accès est autorisé, alors le programme accède à la table des super-pages et l'accès mémoire se poursuit.
Je dis une liste d'accès privée, mais il y en a deux en réalité. Mais la distinction exacte demanderait de parler de ''threads'' et de processus. Pour ceux qui sont familiers avec ces concepts, chaque ''thread'' a sa propre liste privée, avec en plus une liste par processus, partagée entre tous les ''threads''. Deux listes de 1024 entrées maximum, cela fait 2048 entrées au total. Les deux listes portent les noms de ''Dispatchable Unit Access List'' et de ''PASN-AL (Primary Address Space Access List''. Inutile de retenir ces deux termes, cependant.
Un registre d'accès mémorise un '''''Access List Entry Token''''' ou ALET. L'ALET contient l'indice d'une entrée dans la liste privée. Elle contient aussi un bit qui indique quelle liste privée utiliser, entre la ''Dispatchable Unit Access List'' et la ''PASN-AL (Primary Address Space Access List''. Précisons cependant que deux valeurs font exceptions : 0 pointe vers l'espace d'adressage principal, 1 vers l'espace d'adressage secondaire. Les autres valeurs sont configurables.
{|class="wikitable"
|+ Registre d'accès
|-
! 0000000 !! L (choix de la liste privée !! ASLEN !! ALEN
|-
| 0000000 || Choix de la liste privée || Identifiant de protection || Indice dans la liste privée
|}
Lors d'une lecture/écriture, l'ALETs est utilisé pour sélectionner l'espace d'adressage. Les valeurs 0 et 1 sont traitées telles quelles, l'espace d'adressage est déterminé automatiquement par le processeur. Pour les autres valeurs, récupérer l'adresse mémoire demande de consulter la liste d'accès privée. L'ALET est envoyé à la liste d'accès privée, et cette dernière fournit l'adresse de l'entrée de l'ASTE à consulter. Le processeur connait l'adresse de la liste privée, car son adresse mémoire est mémorisée dans les registres de contrôle numéro 2 et 5.
: Précisons que le registre d'accès numéro 0 contient obligatoirement la valeur 0, ce qui fait qu'il pointe vers l'espace d'adressage primaire.
[[File:Traduction d'adresse - trois espaces d'adressage, zarchitecture d'IBM.png|centre|vignette|upright=2|Traduction d'adresse - trois espaces d'adressage, zarchitecture d'IBM.]]
Une subtilité est que le système d'exploitation peut à tout moment modifier le contenu de la liste privée. Il se peut qu'un ALET qui correspondait à un espace d'adressage se mette à pointer vers un autre. Pour détecter cette situation, le processeur attribue un identifiant, à chaque espace d'adressage. L'identifiant est mémorisé dans les entrées de la liste privée, de l'ASTE et dans les registres d'accès (dans les bits de poids fort de l'ALET). L'identifiant est modifié dans l'ALET à chaque fois que le système d'exploitation modifie l'entrée. Si un programme fait un accès avec un registre d'accès, mais que l'identifiant ne correspond pas avec celui dans l'ALET/liste privée, c'est signe que l'espace d'adressage a été swappé, une exception matérielle est levée.
Un tel fonctionnement vous aura sans doute fait penser à une architecture à capacité, mais la z/architecture n'en est pas une. Malgré des points de ressemblance assez importants, il y a des différences fondamentales avec les architectures à capacité.
* Le z/architecture ne supporte pas la segmentation, qui est nécessaire pour les architectures à capacité. Sur la z/architecture, ce sont des espaces d'adressage qui sont sélectionnés, pas des segments.
* Les registres d'accès ne sont pas des registres de capacité, car on peut les modifier avec des instructions en espace utilisateur. En comparaison, modifier les registres de capacité ne peut se faire qu'en espace noyau, sous l'égide du système d'exploitation.
* Les droits d'accès sont dans une table séparée des pointeurs, pas concaténés à ceux-ci. La gestion des droits d'accès est gérée par les tables des pages.
* Les identifiants ALET ne peuvent pas être passés à un autre programme. Deux ALETs identiques correspondent à deux espaces d'adressages différents dans deux programmes différents. L'identifiant peut être identique, il pointera vers l'entrée de même numéro dans les deux listes privées, mais l'adresse dans l'entrée peut être différente.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=L'espace d'adressage du processeur
| prevText=L'espace d'adressage du processeur
| next=Les méthodes de synchronisation entre processeur et périphériques
| nextText=Les méthodes de synchronisation entre processeur et périphériques
}}
</noinclude>
iiaw88kywx4p1c71ieg2jnzj68awhi9
775338
775337
2026-10-16T21:18:18Z
Mewtow
31375
/* Les registres d'accès de la z/architecture */
775338
wikitext
text/x-wiki
Pour introduire ce chapitre, nous devons faire un rappel sur le concept d{{'}}'''espace d'adressage'''. Pour rappel, un espace d'adressage correspond à l'ensemble des adresses utilisables par le processeur. Par exemple, si je prends un processeur 16 bits, il peut adresser en tout 2^16 = 65536 adresses, l'ensemble de ces adresses forme son espace d'adressage. Intuitivement, on s'attend à ce qu'il y ait correspondance avec les adresses de la mémoire RAM. J'entends par là que l'adresse 1209 de l'espace d'adressage correspond à l'adresse 1209 en mémoire RAM. C'est là une hypothèse parfaitement raisonnable et on voit mal comment ce pourrait ne pas être le cas.
Mais les processeurs modernes utilisent des techniques d{{'}}'''abstraction mémoire''' qui font que ce n'est pas le cas. Avec ces techniques, l'adresse 1209 de l'espace d'adressage correspond en réalité à l'adresse 9999 en mémoire RAM, voire n'est pas en RAM. L'abstraction mémoire fait que l'espace d'adressage regroupe des adresses fictives, qui doivent être traduites en adresses mémoires réelles pour être utilisées. Les adresses de l'espace d'adressage portent le nom d{{'}}'''adresses logiques''', alors que les adresses de la mémoire RAM sont appelées '''adresses physiques'''.
L'intérêt de l'abstraction matérielle n'est pas évident. Aussi, avant de parler de comment l'abstraction mémoire fonctionne, nous allons voir à quoi elle sert. Nous allons voir qu'elle a plusieurs utilisations différentes, qui sont absolument nécessaires sur tous les ordinateurs personnels modernes. Tous les processeurs modernes la prennent en charge, les systèmes d'exploitation coopérent avec le processeur pour l'utiliser au mieux.
==L'abstraction mémoire permet plusieurs fonctionnalités complémentaires==
La fonctionnalité la plus connue est la mémoire virtuelle, dont vous avez peut-être déjà entendu parler, peut-être que le nom vous dit quelque chose. Si ce n'est pas le cas, nous la détailleront dans la suite. Elle permet concrétement à un programme d'utiliser plus de mémoire qu'il n'y a de RAM installé dans un ordinateur, en utilisant le disque dur comme solution de secours. Mais d'autres fonctionnalités moins évidentes sont permises par l'abstraction mémoire. Et la première que nous allons voir est l'abstraction des processus.
: En général, une adresse logique correspond à une seule adresse physique. Mais beaucoup de fonctionnalités avancées ne respectent pas cette règle.
===L'abstraction matérielle des processus===
Les systèmes d'exploitation modernes sont dits multi-tâche, à savoir qu'ils sont capables d'exécuter plusieurs logiciels en même temps. Et ce même si un seul processeur est présent dans l'ordinateur : les logiciels sont alors exécutés à tour de rôle. Toutefois, cela amène un paquet de problèmes qu'il faut résoudre au mieux. Par exemple, les programmes exécutés doivent se partager la mémoire RAM, ce qui ne vient pas sans problèmes. Le problème principal est que les programmes ne doivent pas lire ou écrire dans les données d'un autre, sans quoi on se retrouverait rapidement avec des problèmes. Il faut donc introduire des mécanismes d{{'}}'''isolement des processus''', pour isoler les programmes les uns des autres.
Un de ces mécanismes est l{{'}}'''abstraction matérielle des processus''', une technique qui fait que chaque programme a son propre espace d'adressage. Chaque programme a l'impression d'avoir accès à tout l'espace d'adressage, de l'adresse 0 à l'adresse maximale gérée par le processeur. Évidemment, il s'agit d'une illusion maintenue justement grâce à la traduction d'adresse. Les espaces d'adressage contiennent des adresses logiques, les adresses de la RAM sont des adresses physiques, la nécessité de l'abstraction mémoire est évidente.
Implémenter l'abstraction mémoire peut se faire de plusieurs manières. Mais dans tous les cas, il faut que la correspondance adresse logique - physique change d'un programme à l'autre. Ce qui est normal, vu que les deux processus sont placés à des endroits différents en RAM physique. La conséquence est qu'avec l'abstraction mémoire, une adresse logique correspond à plusieurs adresses physiques. Une même adresse logique dans deux processus différents correspond à deux adresses phsiques différentes, une par processus. Une adresse logique dans un processus correspondra à l'adresse physique X, la même adresse dans un autre processus correspondra à l'adresse Y.
Les adresses physiques qui partagent la même adresse logique sont alors appelées des '''adresses homonymes'''. Le choix de la bonne adresse étant réalisé par un mécanisme matériel et dépend du programme en cours. Le mécanisme pour choisir la bonne adresse dépend du processeur, mais il y en a deux grands types :
* La première consiste à utiliser l'identifiant de processus CPU, vu au chapitre précédent. C'est, pour rappel, un numéro attribué à chaque processus par le processeur. L'identifiant du processus en cours d'exécution est mémorisé dans un registre du processeur. La traduction d'adresse utilise cet identifiant, en plus de l'adresse logique, pour déterminer l'adresse physique.
* La seconde solution mémorise les correspondances adresses logiques-physique dans des tables en mémoire RAM, qui sont différentes pour chaque programme. Les tables sont accédées à chaque accès mémoire, afin de déterminer l'adresse physique.
===Le partage de la mémoire===
L'isolation des processus est très importante sur les systèmes d'exploitation modernes. Cependant, il existe quelques situations où elle doit être contournée ou du moins mise en pause. Les situations sont multiples : gestion de bibliothèques partagées, communication entre processus, usage de ''threads'', etc. Elles impliquent toutes un '''partage de mémoire''', à savoir qu'une portion de mémoire RAM est partagée entre plusieurs programmes. Le partage de mémoire est une sorte de brèche de l'isolation des processus, mais qui est autorisée car elle est utile.
Un cas intéressant est celui des '''bibliothèques partagées'''. Les bibliothèques sont des collections de fonctions regroupées ensemble, dans une seule unité de code. Un programme qui utilise une bibliothèque peut appeler n’importe quelle fonction présente dans la bibliothèque. La bibliothèque peut être simplement inclue dans le programme lui-même, on parle alors de bibliothèques statiques. De telles bibliothèques fonctionnent très bien, mais avec un petit défaut pour les bibliothèques très utilisées : plusieurs programmes qui utilisent la même bibliothèque vont chacun l'inclure dans leur code, ce qui fera doublon.
Pour éviter cela, les OS modernes gèrent des bibliothèques partagées, à savoir qu'un seul exemplaire de la bibliothèque est partagé entre plusieurs programmes. Chaque programme peut exécuter une fonction de la bibliothèque quand il le souhaite, en effectuant un branchement adéquat. Mais cela implique que la bibliothèque soit présente dans l'espace d'adressage du programme en question. Une bibliothèque est donc présente dans plusieurs espaces d'adressage, alors qu'il n'y en a qu'un seul exemplaire en mémoire RAM.
[[File:Ogg vorbis libs and application dia.svg|centre|vignette|upright=2|Exemple de bibliothèques, avec Ogg vorbis.]]
D'autres situations demandent de partager de la mémoire entre deux programmes. Par exemple, les systèmes d'exploitation modernes gèrent nativement des systèmes de '''communication inter-processus''', très utilisés par les programmes modernes pour échanger des données. Et la plupart demandant de partager un bout de mémoire entre processus, même si c'est seulement temporairement. Typiquement, deux processus partagent un intervalle d'adresse où l'un écrit les données à l'autre, l'autre lisant les données envoyées.
Une dernière utilisation de la mémoire partagée est l{{'}}'''accès direct au noyau'''. Sur les systèmes d'exploitations moderne, dans l'espace d'adressage de chaque programme, les adresses hautes sont remplies avec une partie du noyau ! Évidemment, ces adresses sont accessibles uniquement en lecture, pas en écriture. Pas question de modifier le noyau de l'OS ! De plus, il s'agit d'une portion du noyau dont on sait que la consultation ne pose pas de problèmes de sécurité.
Le programme peut lire des données dans cette portion du noyau, mais aussi exécuter les fonctions du noyau qui sont dedans. L'idée est d'éviter des appels systèmes trop fréquents. Au lieu d'effectuer un véritable appel système, avec une interruption logicielle, le programme peut exécuter des appels systèmes simplifiés, de simples appels de fonctions couplés avec un changement de niveau de privilège (passage en espace noyau nécessaire).
[[File:AMD64-canonical--48-bit.png|vignette|Répartition des adresses entre noyau (jaune/orange) et programme (verte), sur les systèmes x86-64 bits, avec des adresses physiques de 48 bits.]]
L'espace d'adressage est donc séparé en deux portions : l'OS d'un côté, le programme de l'autre. La répartition des adresses entre noyau et programme varie suivant l'OS ou le processeur utilisé. Sur les PC x86 32 bits, Linux attribuait 3 gigas pour les programmes et 1 giga pour le noyau, Windows attribuait 2 gigas à chacun. Sur les systèmes x86 64 bits, l'espace d'adressage d'un programme est coupé en trois, comme illustré ci-contre : une partie basse de 2^48 octets, une partie haute de même taille, et un bloc d'adresses invalides entre les deux. Les adresses basses sont utilisées pour le programme, les adresses hautes pour le noyau, il n'y a rien entre les deux.
Avec le partage de mémoire, plusieurs adresses logiques correspondent à la même adresse physique. Tel processus verra la zone de mémoire partagée à l'adresse X, l'autre la verra à l'adresse Y. Mais il s'agira de la même portion de mémoire physique, avec une seule adresse physique. En clair, lorsque deux processus partagent une même zone de mémoire, la zone sera mappées à des adresses logiques différentes. Les adresses logiques sont alors appelées des '''adresses synonymes''', terme qui trahit le fait qu'elles correspondent à la même adresse physique.
===La mémoire virtuelle===
Toutes les adresses ne sont pas forcément occupées par de la mémoire RAM, s'il n'y a pas assez de RAM installée. Par exemple, un processeur 32 bits peut adresser 4 gibioctets de RAM, même si seulement 3 gibioctets sont installés dans l'ordinateur. L'espace d'adressage contient donc 1 gigas d'adresses inutilisées, et il faut éviter ce surplus d'adresses pose problème.
Sans mémoire virtuelle, seule la mémoire réellement installée est utilisable. Si un programme utilise trop de mémoire, il est censé se rendre compte qu'il n'a pas accès à tout l'espace d'adressage. Quand il demandera au système d'exploitation de lui réserver de la mémoire, le système d'exploitation le préviendra qu'il n'y a plus de mémoire libre. Par exemple, si un programme tente d'utiliser 4 gibioctets sur un ordinateur avec 3 gibioctets de mémoire, il ne pourra pas. Pareil s'il veut utiliser 2 gibioctets de mémoire sur un ordinateur avec 4 gibioctets, mais dont 3 gibioctets sont déjà utilisés par d'autres programmes. Dans les deux cas, l'illusion tombe à plat.
Les techniques de '''mémoire virtuelle''' font que l'espace d'adressage est utilisable au complet, même s'il n'y a pas assez de mémoire installée dans l'ordinateur ou que d'autres programmes utilisent de la RAM. Par exemple, sur un processeur 32 bits, le programme aura accès à 4 gibioctets de RAM, même si d'autres programmes utilisent la RAM, même s'il n'y a que 2 gibioctets de RAM d'installés dans l'ordinateur.
Pour cela, on utilise une partie des mémoires de masse (disques durs) d'un ordinateur en remplacement de la mémoire physique manquante. Le système d'exploitation crée sur le disque dur un fichier, appelé le ''swapfile'' ou '''fichier de ''swap''''', qui est utilisé comme mémoire RAM supplémentaire. Il mémorise le surplus de données et de programmes qui ne peut pas être mis en mémoire RAM.
[[File:Vm1.png|centre|vignette|upright=2.0|Mémoire virtuelle et fichier de Swap.]]
Une technique naïve de mémoire virtuelle serait la suivante. Avant de l'aborder, précisons qu'il s'agit d'une technique abordée à but pédagogique, mais qui n'est implémentée nulle part tellement elle est lente et inefficace. Un espace d'adressage de 4 gigas ne contient que 3 gigas de RAM, ce qui fait 1 giga d'adresses inutilisées. Les accès mémoire aux 3 gigas de RAM se font normalement, mais l'accès aux adresses inutilisées lève une exception matérielle "Memory Unavailable". La routine d'interruption de cette exception accède alors au ''swapfile'' et récupère les données associées à cette adresse. La mémoire virtuelle est alors émulée par le système d'exploitation.
Le défaut de cette méthode est que l'accès au giga manquant est toujours très lent, parce qu'il se fait depuis le disque dur. D'autres techniques de mémoire virtuelle logicielle font beaucoup mieux, mais nous allons les passer sous silence, vu qu'on peut faire mieux, avec l'aide du matériel.
L'idée est de charger les données dont le programme a besoin dans la RAM, et de déplacer les autres sur le disque dur. Par exemple, imaginons la situation suivante : un programme a besoin de 4 gigas de mémoire, mais ne dispose que de 2 gigas de mémoire installée. On peut imaginer découper l'espace d'adressage en 2 blocs de 2 gigas, qui sont chargés à la demande. Si le programme accède aux adresses basses, on charge les 2 gigas d'adresse basse en RAM. S'il accède aux adresses hautes, on charge les 2 gigas d'adresse haute dans la RAM après avoir copié les adresses basses sur le ''swapfile''.
On perd du temps dans les copies de données entre RAM et ''swapfile'', mais on gagne en performance vu que tous les accès mémoire se font en RAM. Du fait de la localité temporelle, le programme utilise les données chargées depuis le swapfile durant un bon moment avant de passer au bloc suivant. La RAM est alors utilisée comme une sorte de cache alors que les données sont placées dans une mémoire fictive représentée par l'espace d'adressage et qui correspond au disque dur.
Mais avec cette technique, la correspondance entre adresses du programme et adresses de la RAM change au cours du temps. Les adresses de la RAM correspondent d'abord aux adresses basses, puis aux adresses hautes, et ainsi de suite. On a donc besoin d'abstraction mémoire. Les correspondances entre adresse logique et physique peuvent varier avec le temps, ce qui permet de déplacer des données de la RAM vers le disque dur ou inversement. Une adresse logique peut correspondre à une adresse physique, ou bien à une donnée swappée sur le disque dur. C'est l'unité de traduction d'adresse qui se charge de faire la différence. Si une correspondance entre adresse logique et physique est trouvée, elle l'utilise pour traduire les adresses. Si aucune correspondance n'est trouvée, alors elle laisse la main au système d'exploitation pour charger la donnée en RAM. Une fois la donnée chargée en RAM, les correspondances entre adresse logique et physiques sont modifiées de manière à ce que l'adresse logique pointe vers la donnée chargée.
===L'extension d'adressage===
Une autre fonctionnalité rendue possible par l'abstraction mémoire est l{{'}}'''extension d'adressage'''. Elle permet d'utiliser plus de mémoire que l'espace d'adressage ne le permet. Par exemple, utiliser 7 gigas de RAM sur un processeur 32 bits, dont l'espace d'adressage ne gère que 4 gigas. L'extension d'adresse est l'exact inverse de la mémoire virtuelle. La mémoire virtuelle sert quand on a moins de mémoire que d'adresses, l'extension d'adresse sert quand on a plus de mémoire que d'adresses.
Il y a quelques chapitres, nous avions vu que c'est possible via la commutation de banques. Mais l'abstraction mémoire est une méthode alternative. Que ce soit avec la commutation de banques ou avec l'abstraction mémoire, les adresses envoyées à la mémoire doivent être plus longues que les adresses gérées par le processeur. La différence est que l'abstraction mémoire étend les adresses d'une manière différente.
Une implémentation possible de l'extension d'adressage fait usage de l'abstraction matérielle des processus. Chaque processus a son propre espace d'adressage, mais ceux-ci sont placés à des endroits différents dans la mémoire physique. Par exemple, sur un ordinateur avec 16 gigas de RAM, mais un espace d'adressage de 2 gigas, on peut remplir la RAM en lançant 8 processus différents et chaque processus aura accès à un bloc de 2 gigas de RAM, pas plus, il ne peut pas dépasser cette limite. Ainsi, chaque processus est limité par son espace d'adressage, mais on remplit la mémoire avec plusieurs processus, ce qui compense. Il s'agit là de l'implémentation la plus simple, qui a en plus l'avantage d'avoir la meilleure compatibilité logicielle. De simples changements dans le système d'exploitation suffisent à l'implémenter.
[[File:Extension de l'espace d'adressage.png|centre|vignette|upright=1.5|Extension de l'espace d'adressage]]
Un autre implémentation donne plusieurs espaces d'adressage différents à chaque processus, et a donc accès à autant de mémoire que permis par la somme de ces espaces d'adressage. Par exemple, sur un ordinateur avec 16 gigas de RAM et un espace d'adressage de 4 gigas, un programme peut utiliser toute la RAM en utilisant 4 espaces d'adressage distincts. On passe d'un espace d'adressage à l'autre en changeant la correspondance adresse logique-physique. L'inconvénient est que la compatibilité logicielle est assez mauvaise. Modifier l'OS ne suffit pas, les programmeurs doivent impérativement concevoir leurs programmes pour qu'ils utilisent explicitement plusieurs espaces d'adressage.
Les deux implémentations font usage des adresses logiques homonymes, mais à l'intérieur d'un même processus. Pour rappel, cela veut dire qu'une adresse logique correspond à des adresses physiques différentes. Rien d'étonnant vu qu'on utilise plusieurs espaces d'adressage, comme pour l'abstraction des processus, sauf que cette fois-ci, on a plusieurs espaces d'adressage par processus. Prenons l'exemple où on a 8 gigas de RAM sur un processeur 32 bits, dont l'espace d'adressage ne gère que 4 gigas. L'idée est qu'une adresse correspondra à une adresse dans les premiers 4 gigas, ou dans les seconds 4 gigas. L'adresse logique X correspondra d'abord à une adresse physique dans les premiers 4 gigas, puis à une adresse physique dans les seconds 4 gigas.
===La protection mémoire===
La '''protection mémoire''' regroupe des techniques très différentes les unes des autres, qui visent à améliorer la sécurité des programmes et des systèmes d'exploitation. Elles visent à empêcher de lire, d'écrire ou d'exécuter certaines portions de mémoire. Sans elle, les programmes peuvent techniquement lire ou écrire les données des autres, ce qui causent des situations non-prévues par le programmeur, avec des conséquences qui vont d'un joli plantage à des failles de sécurité dangereuses.
La première technique de protection mémoire est l{{'}}'''isolation des processus''', qu'on a vue plus haut. Elle garantit que chaque programme n'a accès qu'à certaines portions dédiées de la mémoire et rend le reste de la mémoire inaccessible en lecture et en écriture. Le système d'exploitation attribue à chaque programme une ou plusieurs portions de mémoire rien que pour lui, auquel aucun autre programme ne peut accéder. Un tel programme, isolé des autres, s'appelle un '''processus''', d'où le nom de cet objectif. Toute tentative d'accès à une partie de la mémoire non autorisée déclenche une exception matérielle (rappelez-vous le chapitre sur les interruptions) qui est traitée par une routine du système d'exploitation. Généralement, le programme fautif est sauvagement arrêté et un message d'erreur est affiché à l'écran.
La '''protection de l'espace exécutable''' empêche d’exécuter quoique ce soit provenant de certaines zones de la mémoire. En effet, certaines portions de la mémoire sont censées contenir uniquement des données, sans aucun programme ou code exécutable. Cependant, des virus informatiques peuvent se cacher dedans et d’exécuter depuis celles-ci. Ou encore, des failles de sécurités peuvent permettre à un attaquant d'injecter du code exécutable malicieux dans des données, ce qui peut lui permettre de lire les données manipulées par un programme, prendre le contrôle de la machine, injecter des virus, ou autre. Pour éviter cela, le système d'exploitation peut marquer certaines zones mémoire comme n'étant pas exécutable. Toute tentative d’exécuter du code localisé dans ces zones entraîne la levée d'une exception ou d'une erreur et le système d'exploitation réagit en conséquence. Là encore, le processeur doit détecter les exécutions non autorisées.
D'autres méthodes de protection mémoire visent à limiter des actions dangereuses. Pour cela, le processeur et l'OS gèrent des '''droits d'accès''', qui interdisent certaines actions pour des programmes non-autorisés. Lorsqu'on exécute une opération interdite, le système d’exploitation et/ou le processeur réagissent en conséquence. La première technique de ce genre n'est autre que la séparation entre espace noyau et utilisateur, vue dans le chapitre sur les interruptions. Mais il y en a d'autres, comme nous le verrons dans ce chapitre.
==La MMU==
La traduction des adresses logiques en adresses physiques se fait par un circuit spécialisé, appelé la '''''Memory Management Unit''''' (MMU). De nos jours, elle est intégrée dans le processeur, à la suite de l'unité mémoire. Mais il a existé des processeurs avec une MMU externe, soudée sur la carte mère.
La traduction des adresses logiques en adresses physiques demande d'accéder à des données sont en mémoire RAM, qui sont gérés par le système d'exploitation. Aussi, les processeurs modernes incorporent des mémoires caches appelées des '''''Translation Lookaside Buffers''''', ou encore TLB. Nous nous pouvons pas parler des TLB pour le moment, car nous n'avons pas encore abordé le chapitre sur les mémoires caches, mais un chapitre entier sera dédié aux TLB d'ici peu.
[[File:MMU principle updated.png|centre|vignette|upright=2|MMU.]]
===Les MMU intégrées au processeur===
D'ordinaire, la MMU est intégrée au processeur. Et elle peut l'être de deux manières. La première en fait un circuit séparé, relié au bus d'adresse. La seconde fusionne la MMU avec l'unité de calcul d'adresse. La première solution est surtout utilisée avec une technique d'abstraction mémoire appelée la pagination, alors que l'autre l'est avec une autre méthode appelée la segmentation. La raison est que la traduction d'adresse avec la segmentation est assez simple : elle demande d'additionner le contenu d'un registre avec l'adresse logique, ce qui est le genre de calcul qu'une unité de calcul d'adresse sait déjà faire. La fusion est donc assez évidente.
Pour donner un exemple, l'Intel 8086 fusionnait l'unité de calcul d'adresse et la MMU. Précisément, il utilisait un même additionneur pour incrémenter le ''program counter'' et effectuer des calculs d'adresse liés à la segmentation. Il aurait été logique d'ajouter les pointeurs de pile avec, mais ce n'était pas possible. La raison est que le pointeur de pile ne peut pas être envoyé directement sur le bus d'adresse, vu qu'il doit passer par une phase de traduction en adresse physique liée à la segmentation.
[[File:80186 arch.png|centre|vignette|upright=2|Intel 8086, microarchitecture.]]
===Les MMU séparées du processeur, sur la carte mère===
Avant d'être intégrées au processeur, les MMU étaient des circuits séparés, placés sur la carte mère, entre le processeur et la mémoire. Elle étaient connectées en plein milieu du bus d'adresse. A savoir que le bus d'adresse entrait dans la MMU et en ressortait. Le processeur envoyait des adresses à la MMU, qui renvoyait une adresse physique. La MMU était facultative, à savoir que le système pouvait fonctionner sans MMU d'installée, mais l'abstraction mémoire n’était alors pas disponible.
Par exemple, les processeurs Motorola 68000 et 68010 pouvaient être combinés avec une MMU de type Motorola 68451. Elle supportait des versions simplifiées de la segmentation et de la pagination. Au minimum, elle ajoutait un support de la protection mémoire contre certains accès non-autorisés. La gestion de la mémoire virtuelle proprement dit n'était possible que si le processeur utilisé était un Motorola 68010, en raison de la manière dont le 68000 gérait ses accès mémoire. La MMU 68451 gérait un espace d'adressage de 16 mébioctets, découpé en maximum 32 pages/segments. On pouvait dépasser cette limite de 32 segments/pages en combinant plusieurs 68451.
Le Motorola 68851 était une MMU qui était prévue pour fonctionner de paire avec le Motorola 68020. Elle gérait la pagination pour un espace d'adressage de 32 bits.
Les processeurs suivants, les 68030, 68040, et 68060, avaient une MMU interne au processeur.
==La relocation matérielle==
Pour rappel, les systèmes d'exploitation moderne permettent de lancer plusieurs programmes en même temps et les laissent se partager la mémoire. Dans le cas le plus simple, qui n'est pas celui des OS modernes, le système d'exploitation découpe la mémoire en blocs d'adresses contiguës qui sont appelés des '''segments''', ou encore des ''partitions mémoire''. Les segments correspondent à un bloc de mémoire RAM. C'est-à-dire qu'un segment de 259 mébioctets sera un segment continu de 259 mébioctets dans la mémoire physique comme dans la mémoire logique. Dans ce qui suit, un segment contient un programme en cours d'exécution, comme illustré ci-dessous.
[[File:CPT Memory Addressable.svg|centre|vignette|upright=2|Espace d'adressage segmenté.]]
Le système d'exploitation mémorise la position de chaque segment en mémoire, ainsi que d'autres informations annexes. Le tout est regroupé dans la '''table de segment''', un tableau dont chaque case est attribuée à un programme/segment. La table des segments est un tableau numéroté, chaque segment ayant un numéro qui précise sa position dans le tableau. Chaque case, chaque entrée, contient un '''descripteur de segment''' qui regroupe plusieurs informations sur le segment : son adresse de base, sa taille, diverses informations.
===La relocation avec la relocation matérielle : le registre de base===
Un segment peut être placé n'importe où en RAM physique et sa position en RAM change à chaque exécution. Le programme est chargé à une adresse, celle du début du segment, qui change à chaque chargement du programme. Et toutes les adresses utilisées par le programme doivent être corrigées lors du chargement du programme, généralement par l'OS. Cette correction s'appelle la '''relocation''', et elle consiste à ajouter l'adresse de début du segment à chaque adresse manipulée par le programme.
[[File:Relocation assistée par matériel.png|centre|vignette|upright=2.5|Relocation.]]
La relocation matérielle fait que la relocation est faite par le processeur, pas par l'OS. La relocation est intégrée dans le processeur par l'intégration d'un registre : le '''registre de base''', aussi appelé '''registre de relocation'''. Il mémorise l'adresse à laquelle commence le segment, la première adresse du programme. Pour effectuer la relocation, le processeur ajoute automatiquement l'adresse de base à chaque accès mémoire, en allant la chercher dans le registre de relocation.
[[File:Registre de base de segment.png|centre|vignette|upright=2|Registre de base de segment.]]
Le processeur s'occupe de la relocation des segments et le programme compilé n'en voit rien. Pour le dire autrement, les programmes manipulent des adresses logiques, qui sont traduites par le processeur en adresses physiques. La traduction se fait en ajoutant le contenu du registre de relocation à l'adresse logique. De plus, cette méthode fait que chaque programme a son propre espace d'adressage.
[[File:CPU created logical address presentation.png|centre|vignette|upright=2|Traduction d'adresse avec la relocation matérielle.]]
Le système d'exploitation mémorise les adresses de base pour chaque programme, dans la table des segments. Le registre de base est mis à jour automatiquement lors de chaque changement de segment. Pour cela, le registre de base est accessible via certaines instructions, accessibles en espace noyau, plus rarement en espace utilisateur. Le registre de segment est censé être adressé implicitement, vu qu'il est unique. Si ce n'est pas le cas, il est possible d'écrire dans ce registre de segment, qui est alors adressable.
===La protection mémoire avec la relocation matérielle : le registre limite===
Sans restrictions supplémentaires, la taille maximale d'un segment est égale à la taille complète de l'espace d'adressage. Sur les processeurs 32 bits, un segment a une taille maximale de 2^32 octets, soit 4 gibioctets. Mais il est possible de limiter la taille du segment à 2 gibioctets, 1 gibioctet, 64 Kibioctets, ou toute autre taille. La limite est définie lors de la création du segment, mais elle peut cependant évoluer au cours de l'exécution du programme, grâce à l'allocation mémoire.
Le processeur vérifie à chaque accès mémoire que celui-ci se fait bien dans le segment, qu'il ne déborde pas en-dehors. C'est possible qu'une adresse calculée sorte du segment, à la suite d'un bug ou d'une erreur de programmation, voire pire. Et le processeur doit éviter de tels '''débordements de segments'''.
A chaque accès mémoire, le processeur compare l'adresse accédée et vérifie qu'elle est bien dans le segment. Pour cela, il y a deux solutions. La première part du principe que le segment est placé en mémoire entre l'adresse de base et l'adresse limite. Il suffit de mémoriser l'adresse limite, l'adresse physique à ne pas dépasser. Une autre solution mémorise la taille du segment. La table des segments doit donc mémoriser, en plus de l'adresse de base : soit l'adresse maximale du segment, soit la taille du segment. D'autres informations peuvent être ajoutées, comme on le verra plus tard, mais cela complexifie la table des segments.
De plus, le processeur se voit ajouter un '''registre limite''', qui mémorise soit la taille du segment, soit l'adresse limite. Les deux registres, base et limite, sont utilisés pour vérifier si un programme qui lit/écrit de la mémoire en-dehors de son segment attitré : au-delà pour le registre limite, en-deça pour le registre de base. Le processeur vérifie pour chaque accès mémoire ne déborde pas au-delà du segment qui lui est allouée, ce qui n'arrive que si l'adresse d'accès dépasse la valeur du registre limite. Pour les accès en-dessous du segment, il suffit de vérifier si l'addition de relocation déborde, tout débordement signifiant erreur de protection mémoire.
[[File:Registre limite.png|centre|vignette|upright=2|Registre limite]]
Utiliser la taille du segment a de nombreux avantages. L'un d'entre eux se manifeste quand on déplace un segment en mémoire RAM. Le descripteur doit alors être mis à jour, et c'est plus facile quand on utilise la taille du segment. Si on utilise l'adresse limite, il faut mettre à jour à la fois l'adresse de base et l'adresse limite, dans le descripteur. En utilisant la taille, seule l'adresse de base doit être modifiée, vu que le segment n'a pas changé de taille. Un autre avantage est lié aux performances, mais nous devons faire un détour pour le comprendre.
La taille du segment est équivalent à l'adresse logique maximale possible. Par exemple, si un segment fait 256 octets, les adresses logiques possibles vont de 0 à 255, 256 est donc à la fois la taille du segment et l'adresse logique à partir de laquelle on déborde du segment. Et cela marche si on remplace 256 par n'importe quelle valeur : vu que le segment commence à l'adresse 0, sa taille en octets indique l'adresse de dépassement. Interpréter la taille du segment comme une adresse logique fait que les tests avec le registre limite sont plus performants, voyons pourquoi.
En utilisant l'adresse physique limite, on doit faire la relocation, puis comparer l'adresse calculée avec l'adresse limite. Le calcul d'adresse doit se faire avant la vérification. En utilisant la taille, on doit comparer l'adresse logique avec la taille du segment. On peut alors faire le test de débordement avant ou pendant la relocation. Les deux peuvent être faits en parallèle, dans deux circuits distincts, ce qui améliore un peu le temps d'un accès mémoire. Quelques processeurs en ont profité, mais on verra cela dans la section sur la segmentation.
[[File:Comparaison entre adresse limite physique et logique.png|centre|vignette|upright=2|Comparaison entre adresse limite physique et logique]]
Les registres de base et limite sont altérés uniquement par le système d'exploitation et ne sont accessibles qu'en espace noyau. Lorsque le système d'exploitation charge un programme, ou reprend son exécution, il charge les adresses de début/fin du segment dans ces registres. D'ailleurs, ces deux registres doivent être sauvegardés et restaurés lors de chaque interruption. Par contre, et c'est assez évident, ils ne le sont pas lors d'un appel de fonction. Cela fait une différence de plus entre interruption et appels de fonctions.
: Il faut noter que le registre limite et le registre de base sont parfois fusionnés en un seul registre, qui contient un descripteur de segment tout entier.
Pour information, la relocation matérielle avec un registre limite a été implémentée sur plusieurs processeurs assez anciens, notamment sur les anciens supercalculateurs de marque CDC. Un exemple est le fameux CDC 6600, qui implémentait cette technique.
===La mémoire virtuelle avec la relocation matérielle===
Il est possible d'implémenter la mémoire virtuelle avec la relocation matérielle. Pour cela, il faut swapper des segments entiers sur le disque dur. Les segments sont placés en mémoire RAM et leur taille évolue au fur et à mesure que les programmes demandent du rab de mémoire RAM. Lorsque la mémoire est pleine, ou qu'un programme demande plus de mémoire que disponible, des segments entiers sont sauvegardés dans le ''swapfile'', pour faire de la place.
Faire ainsi de demande juste de mémoriser si un segment est en mémoire RAM ou non, ainsi que la position des segments swappés dans le ''swapfile''. Pour cela, il faut modifier la table des segments, afin d'ajouter un '''bit de swap''' qui précise si le segment en question est swappé ou non. Lorsque le système d'exploitation veut swapper un segment, il le copie dans le ''swapfile'' et met ce bit à 1. Lorsque l'OS recharge ce segment en RAM, il remet ce bit à 0. La gestion de la position des segments dans le ''swapfile'' est le fait d'une structure de données séparée de la table des segments.
L'OS exécute chaque programme l'un après l'autre, à tour de rôle. Lorsque le tour d'un programme arrive, il consulte la table des segments pour récupérer les adresses de base et limite, mais il vérifie aussi le bit de swap. Si le bit de swap est à 0, alors l'OS se contente de charger les adresses de base et limite dans les registres adéquats. Mais sinon, il démarre une routine d'interruption qui charge le segment voulu en RAM, depuis le ''swapfile''. C'est seulement une fois le segment chargé que l'on connait son adresse de base/limite et que le chargement des registres de relocation peut se faire.
Un défaut évident de cette méthode est que l'on swappe des programmes entiers, qui sont généralement assez imposants. Les segments font généralement plusieurs centaines de mébioctets, pour ne pas dire plusieurs gibioctets, à l'époque actuelle. Ils étaient plus petits dans l'ancien temps, mais la mémoire était alors plus lente. Toujours est-il que la copie sur le disque dur des segments est donc longue, lente, et pas vraiment compatible avec le fait que les programmes s'exécutent à tour de rôle. Et ca explique pourquoi la relocation matérielle n'est presque jamais utilisée avec de la mémoire virtuelle.
===L'extension d'adressage avec la relocation matérielle===
Passons maintenant à la dernière fonctionnalité implémentable avec la traduction d'adresse : l'extension d'adressage. Elle permet d'utiliser plus de mémoire que ne le permet l'espace d'adressage. Par exemple, utiliser plus de 64 kibioctets de mémoire sur un processeur 16 bits. Pour cela, les adresses envoyées à la mémoire doivent être plus longues que les adresses gérées par le processeur.
L'extension des adresses se fait assez simplement avec la relocation matérielle : il suffit que le registre de base soit plus long. Prenons l'exemple d'un processeur aux adresses de 16 bits, mais qui est reliée à un bus d'adresse de 24 bits. L'espace d'adressage fait juste 64 kibioctets, mais le bus d'adresse gère 16 mébioctets de RAM. On peut utiliser les 16 mébioctets de RAM à une condition : que le registre de base fasse 24 bits, pas 16.
Un défaut de cette approche est qu'un programme ne peut pas utiliser plus de mémoire que ce que permet l'espace d'adressage. Mais par contre, on peut placer chaque programme dans des portions différentes de mémoire. Imaginons par exemple que l'on ait un processeur 16 bits, mais un bus d'adresse de 20 bits. Il est alors possible de découper la mémoire en 16 blocs de 64 kibioctets, chacun attribué à un segment/programme, qu'on sélectionne avec les 4 bits de poids fort de l'adresse. Il suffit de faire démarrer les segments au bon endroit en RAM, et cela demande juste que le registre de base le permette. C'est une sorte d'émulation de la commutation de banques.
==La segmentation en mode réel des processeurs x86==
Avant de passer à la suite, nous allons voir la technique de segmentation de l'Intel 8086, un des tout premiers processeurs 16 bits. Il s'agissait d'une forme très simple de segmentation, sans aucune forme de protection mémoire, ni même de mémoire virtuelle, ce qui le place à part des autres formes de segmentation. Il s'agit d'une amélioration de la relocation matérielle, qui avait pour but de permettre d'utiliser plus de 64 kibioctets de mémoire, ce qui était la limite maximale sur les processeurs 16 bits de l'époque.
Par la suite, la segmentation s'améliora et ajouta un support complet de la mémoire virtuelle et de la protection mémoire. L'ancienne forme de segmentation fut alors appelé le '''mode réel''', et la nouvelle forme de segmentation fut appelée le '''mode protégé'''. Le mode protégé rajoute la protection mémoire, en ajoutant des registres limite et une gestion des droits d'accès aux segments, absents en mode réel. De plus, il ajoute un support de la mémoire virtuelle grâce à l'utilisation d'une des segments digne de ce nom, table qui est absente en mode réel ! Pour le moment, voyons le mode réel.
===Les segments en mode réel===
[[File:Typical computer data memory arrangement.png|vignette|upright=0.5|Typical computer data memory arrangement]]
La segmentation en mode réel sépare la pile, le tas, le code machine et les données constantes dans quatre segments distincts.
* Le segment '''''text''''', qui contient le code machine du programme, de taille fixe.
* Le segment '''''data''''' contient des données de taille fixe qui occupent de la mémoire de façon permanente, des constantes, des variables globales, etc.
* Le segment pour la '''pile''', de taille variable.
* le reste est appelé le '''tas''', de taille variable.
Un point important est que sur ces processeurs, il n'y a pas de table des segments proprement dit. Chaque programme gére de lui-même les adresses de base des segments qu'il manipule. Il n'est en rien aidé par une table des segments gérée par le système d'exploitation.
===Les registres de segments en mode réel===
Chaque segment subit la relocation indépendamment des autres. Pour cela, le processeur intégre plusieurs registres de base, un par segment. Notons que cette solution ne marche que si le nombre de segments par programme est limité, à une dizaine de segments tout au plus. Les processeurs x86 utilisaient cette méthode, et n'associaient que 4 à 6 registres de segments par programme.
Les processeurs 8086 et le 286 avaient quatre registres de segment : un pour le code, un autre pour les données, et un pour la pile, le quatrième étant un registre facultatif laissé à l'appréciation du programmeur. Ils sont nommés CS (''code segment''), DS (''data segment''), SS (''Stack segment''), et ES (''Extra segment''). Le 386 rajouta deux registres, les registres FS et GS, qui sont utilisés pour les segments de données. Les processeurs post-386 ont donc 6 registres de segment.
Les registres CS et SS sont adressés implicitement, en fonction de l'instruction exécutée. Les instructions de la pile manipulent le segment associé à la pile, le chargement des instructions se fait dans le segment de code, les instructions arithmétiques et logiques vont chercher leurs opérandes sur le tas, etc. Et donc, toutes les instructions sont chargées depuis le segment pointé par CS, les instructions de gestion de la pile (PUSH et POP) utilisent le segment pointé par SS.
Les segments DS et ES sont, eux aussi, adressés implicitement. Pour cela, les instructions LOAD/STORE sont dupliquées : il y a une instruction LOAD pour le segment DS, une autre pour le segment ES. D'autres instructions lisent leurs opérandes dans un segment par défaut, mais on peut changer ce choix par défaut en précisant le segment voulu. Un exemple est celui de l'instruction CMPSB, qui compare deux octets/bytes : le premier est chargé depuis le segment DS, le second depuis le segment ES.
Un autre exemple est celui de l'instruction MOV avec un opérande en mémoire. Elle lit l'opérande en mémoire depuis le segment DS par défaut. Il est possible de préciser le segment de destination si celui-ci n'est pas DS. Par exemple, l'instruction MOV [A], AX écrit le contenu du registre AX dans l'adresse A du segment DS. Par contre, l'instruction MOV ES:[A], copie le contenu du registre AX das l'adresse A, mais dans le segment ES.
===La traduction d'adresse en mode réel===
La segmentation en mode réel a pour seul but de permettre à un programme de dépasser la limite des 64 KB autorisée par les adresses de 16 bits. L'idée est que chaque segment a droit à son propre espace de 64 KB. On a ainsi 64 Kb pour le code machine, 64 KB pour la pile, 64 KB pour un segment de données, etc. Les registres de segment mémorisaient la base du segment, les adresses calculées par l'ALU étant des ''offsets''. Ce sont tous des registres de 16 bits, mais ils ne mémorisent pas des adresses physiques de 16 bits, comme nous allons le voir.
[[File:Table des segments dans un banc de registres.png|centre|vignette|upright=2|Table des segments dans un banc de registres.]]
L'Intel 8086 utilisait des adresses de 20 bits, ce qui permet d'adresser 1 mébioctet de RAM. Vous pouvez vous demander comment on peut obtenir des adresses de 20 bits alors que les registres de segments font tous 16 bits ? Cela tient à la manière dont sont calculées les adresses physiques. Le registre de segment n'est pas additionné tel quel avec le décalage : à la place, le registre de segment est décalé de 4 rangs vers la gauche. Le décalage de 4 rangs vers la gauche fait que chaque segment a une adresse qui est multiple de 16. Le fait que le décalage soit de 16 bits fait que les segments ont une taille de 64 kibioctets.
{|class="wikitable"
|-
| <code> </code><code style="background:#DED">0000 0110 1110 1111</code><code>0000</code>
| Registre de segment -
| 16 bits, décalé de 4 bits vers la gauche
|-
| <code>+ </code><code style="background:#DDF">0001 0010 0011 0100</code>
| Décalage/''Offset''
| 16 bits
|-
| colspan="3" |
|-
| <code> </code><code style="background:#FDF">0000 1000 0001 0010 0100</code>
| Adresse finale
| 20 bits
|}
Vous aurez peut-être remarqué que le calcul peut déborder, dépasser 20 bits. Mais nous reviendrons là-dessus plus bas. L'essentiel est que la MMU pour la segmentation en mode réel se résume à quelques registres et des additionneurs/soustracteurs.
Un exemple est l'Intel 8086, un des tout premier processeur Intel. Le processeur était découpé en deux portions : l'interface mémoire et le reste du processeur. L'interface mémoire est appelée la '''''Bus Interface Unit''''', et le reste du processeur est appelé l{{'}}'''''Execution Unit'''''. L'interface mémoire contenait les registres de segment, au nombre de 4, ainsi qu'un additionneur utilisé pour traduire les adresses logiques en adresses physiques. Elle contenait aussi une file d'attente où étaient préchargées les instructions.
Sur le 8086, la MMU est fusionnée avec les circuits de gestion du ''program counter''. Les registres de segment sont regroupés avec le ''program counter'' dans un même banc de registres, et un additionneur unique est utilisé à la fois à incrémenter le ''program counter'' et pour gérer la segmentation. L'additionneur est donc mutualisé entre segmentation et ''program counter''. En somme, il n'y a pas vraiment de MMU dédiée, mais un super-circuit en charge du Fetch et de la mémoire virtuelle, ainsi que du préchargement des instructions. Nous en reparlerons au chapitre suivant.
[[File:80186 arch.png|centre|vignette|upright=2.5|Architecture du 8086, du 80186 et de ses variantes.]]
La MMU du 286 était fusionnée avec l'unité de calcul d'adresse. Elle contient les registres de segments, un comparateur pour détecter les accès hors-segment, et plusieurs additionneurs. Il y a un additionneur pour les calculs d'adresse proprement dit, suivi d'un additionneur pour la relocation.
[[File:Intel i80286 arch.svg|centre|vignette|upright=3|Intel i80286 arch]]
===La segmentation en mode réel accepte plusieurs segments de code/données===
Les programmes peuvent parfaitement répartir leur code machine dans plusieurs segments de code. La limite de 64 KB par segment est en effet assez limitante, et il n'était pas rare qu'un programme stocke son code dans deux ou trois segments. Il en est de même avec les données, qui peuvent être réparties dans deux ou trois segments séparés. La seule exception est la pile : elle est forcément dans un segment unique et ne peut pas dépasser 64 KB.
Pour gérer plusieurs segments de code/donnée, il faut changer de segment à la volée suivant les besoins, en modifiant les registres de segment. Il s'agit de la technique de '''commutation de segment'''. Pour cela, tous les registres de segment, à l'exception de CS, peuvent être altérés par une instruction d'accès mémoire, soit avec une instruction MOV, soit en y copiant le sommet de la pile avec une instruction de dépilage POP. L'absence de sécurité fait que la gestion de ces registres est le fait du programmeur, qui doit redoubler de prudence pour ne pas faire n'importe quoi.
Pour le code machine, le répartir dans plusieurs segments posait des problèmes au niveau des branchements. Si la plupart des branchements sautaient vers une instruction dans le même segment, quelques rares branchements sautaient vers du code machine dans un autre segment. Intel avait prévu le coup et disposait de deux instructions de branchement différentes pour ces deux situations : les '''''near jumps''''' et les '''''far jumps'''''. Les premiers sont des branchements normaux, qui précisent juste l'adresse à laquelle brancher, qui correspond à la position de la fonction dans le segment. Les seconds branchent vers une instruction dans un autre segment, et doivent préciser deux choses : l'adresse de base du segment de destination, et la position de la destination dans le segment. Le branchement met à jour le registre CS avec l'adresse de base, avant de faire le branchement. Ces derniers étaient plus lents, car on n'avait pas à changer de segment et mettre à jour l'état du processeur.
Il y avait la même pour l'instruction d'appel de fonction, avec deux versions de cette instruction. La première version, le '''''near call''''' est un appel de fonction normal, la fonction appelée est dans le segment en cours. Avec la seconde version, le '''''far call''''', la fonction appelée est dans un segment différent. L'instruction a là aussi besoin de deux opérandes : l'adresse de base du segment de destination, et la position de la fonction dans le segment. Un ''far call'' met à jour le registre CS avec l'adresse de base, ce qui fait que les ''far call'' sont plus lents que les ''near call''. Il existe aussi la même chose, pour les instructions de retour de fonction, avec une instruction de retour de fonction normale et une instruction de retour qui renvoie vers un autre segment, qui sont respectivement appelées '''''near return''''' et '''''far return'''''. Là encore, il faut préciser l'adresse du segment de destination dans le second cas.
La même chose est possible pour les segments de données. Sauf que cette fois-ci, ce sont les pointeurs qui sont modifiés. pour rappel, les pointeurs sont, en programmation, des variables qui contiennent des adresses. Lors de la compilation, ces pointeurs sont placés soit dans un registre, soit dans les instructions (adressage absolu), ou autres. Ici, il existe deux types de pointeurs, appelés '''''near pointer''''' et '''''far pointer'''''. Vous l'avez deviné, les premiers sont utilisés pour localiser les données dans le segment en cours d'utilisation, alors que les seconds pointent vers une donnée dans un autre segment. Là encore, la différence est que le premier se contente de donner la position dans le segment, alors que les seconds rajoutent l'adresse de base du segment. Les premiers font 16 bits, alors que les seconds en font 32 : 16 bits pour l'adresse de base et 16 pour l{{'}}''offset''.
===L'occupation de l'espace d'adressage par les segments===
Nous venons de voir qu'un programme pouvait utiliser plus de 4-6 segments, avec la commutation de segment. Mais d'autres programmes faisaient l'inverse, à savoir qu'ils se débrouillaient avec seulement 1 ou 2 segments. Suivant le nombre de segments utilisés, la configuration des registres n'était pas la même. Les configurations possibles sont appelées des ''modèle mémoire'', et il y en a en tout 6. En voici la liste :
{| class="wikitable"
|-
! Modèle mémoire !! Configuration des segments !! Configuration des registres || Pointeurs utilisés || Branchements utilisés
|-
| Tiny* || Segment unique pour tout le programme || CS=DS=SS || ''near'' uniquement || ''near'' uniquement
|-
| Small || Segment de donnée séparé du segment de code, pile dans le segment de données || DS=SS || ''near'' uniquement || ''near'' uniquement
|-
| Medium || Plusieurs segments de code unique, un seul segment de données || CS, DS et SS sont différents || ''near'' et ''far'' || ''near'' uniquement
|-
| Compact || Segment de code unique, plusieurs segments de données || CS, DS et SS sont différents || ''near'' uniquement || ''near'' et ''far''
|-
| Large || Plusieurs segments de code, plusieurs segments de données || CS, DS et SS sont différents || ''near'' et ''far'' || ''near'' et ''far''
|}
Un programme est censé utiliser maximum 4-6 segments de 64 KB, ce qui permet d'adresser maximum 64 * 6 = 384 KB de RAM, soit bien moins que le mébioctet de mémoire théoriquement adressable. Mais ce défaut est en réalité contourné par la commutation de segment, qui permettait d'adresser la totalité de la RAM si besoin. Une second manière de contourner cette limite est que plusieurs processus peuvent s'exécuter sur un seul processeur, si l'OS le permet. Ce n'était pas le cas à l'époque du DOS, qui était un OS mono-programmé, mais c'était en théorie possible. La limite est de 6 segments par programme/processus, en exécuter plusieurs permet d'utiliser toute la mémoire disponible rapidement.
[[File:Overlapping realmode segments.svg|vignette|Segments qui se recouvrent en mode réel.]]
Vous remarquerez qu'avec des registres de segments de 16 bits, on peut gérer 65536 segments différents, chacun de 64 KB. Et 65 536 segments de 64 kibioctets, ça ne rentre pas dans le mébioctet de mémoire permis avec des adresses de 20 bits. La raison est que plusieurs couples segment+''offset'' pointent vers la même adresse. En tout, chaque adresse peut être adressée par 4096 couples segment+''offset'' différents.
L'avantage de cette méthode est que des segments peuvent se recouvrir, à savoir que la fin de l'un se situe dans le début de l'autre, comme illustré ci-contre. Cela permet en théorie de partager de la mémoire entre deux processus. Mais la technique est tout sauf pratique et est donc peu utilisée. Elle demande de placer minutieusement les segments en RAM, et les données à partager dans les segments. En pratique, les programmeurs et OS utilisent des segments qui ne se recouvrent pas et sont disjoints en RAM.
Le nombre maximal de segments disjoints se calcule en prenant la taille de la RAM, qu'on divise par la taille d'un segment. Le calcul donne : 1024 kibioctets / 64 kibioctets = 16 segments disjoints. Un autre calcul prend le nombre de segments divisé par le nombre d'adresses aliasées, ce qui donne 65536 / 4096 = 16. Seulement 16 segments, c'est peu. En comptant les segments utilisés par l'OS et ceux utilisés par le programme, la limite est vite atteinte si le programme utilise la commutation de segment.
===Le mode réel sur les 286 et plus : la ligne d'adresse A20===
Pour résumer, le registre de segment contient des adresses de 20 bits, dont les 4 bits de poids faible sont à 0. Et il se voit ajouter un ''offset'' de 16 bits. Intéressons-nous un peu à l'adresse maximale que l'on peut calculer avec ce système. Nous allons l'appeler l{{'}}'''adresse maximale de segmentation'''. Elle vaut :
{|class="wikitable"
|-
| <code> </code><code style="background:#DED">1111 1111 1111 1111</code><code>0000</code>
| Registre de segment -
| 16 bits, décalé de 4 bits vers la gauche
|-
| <code>+ </code><code style="background:#DDF">1111 1111 1111 1111</code>
| Décalage/''Offset''
| 16 bits
|-
| colspan="3" |
|-
| <code> </code><code style="background:#FDF">1 0000 1111 1111 1110 1111</code>
| Adresse finale
| 20 bits
|}
Le résultat n'est pas l'adresse maximale codée sur 20 bits, car l'addition déborde. Elle donne un résultat qui dépasse l'adresse maximale permis par les 20 bits, il y a un 21ème bit en plus. De plus, les 20 bits de poids faible ont une valeur bien précise. Ils donnent la différence entre l'adresse maximale permise sur 20 bit, et l'adresse maximale de segmentation. Les bits 1111 1111 1110 1111 traduits en binaire donnent 65 519; auxquels il faut ajouter l'adresse 1 0000 0000 0000 0000. En tout, cela fait 65 520 octets adressables en trop. En clair : on dépasse la limite du mébioctet de 65 520 octets. Le résultat est alors très différent selon que l'on parle des processeurs avant le 286 ou après.
Avant le 286, le bus d'adresse faisait exactement 20 bits. Les adresses calculées ne pouvaient pas dépasser 20 bits. L'addition générait donc un débordement d'entier, géré en arithmétique modulaire. En clair, les bits de poids fort au-delà du vingtième sont perdus. Le calcul de l'adresse débordait et retournait au début de la mémoire, sur les 65 520 premiers octets de la mémoire RAM.
[[File:IBM PC Memory areas.svg|vignette|IBM PC Memory Map, la ''High memory area'' est en jaune.]]
Le 80286 en mode réel gère des adresses de base de 24 bits, soit 4 bits de plus que le 8086. Le résultat est qu'il n'y a pas de débordement. Les bits de poids fort sont conservés, même au-delà du 20ème. En clair, la segmentation permettait de réellement adresser 65 530 octets au-delà de la limite de 1 mébioctet. La portion de mémoire adressable était appelé la '''''High memory area''''', qu'on va abrévier en HMA.
{| class="wikitable"
|+ Espace d'adressage du 286
|-
! Adresses en héxadécimal !! Zone de mémoire
|-
| 10 FFF0 à FF FFFF || Mémoire étendue, au-delà du premier mébioctet
|-
| 10 0000 à 10 FFEF || ''High Memory Area''
|-
| 0 à 0F FFFF || Mémoire adressable en mode réel
|}
En conséquence, les applications peuvent utiliser plus d'un mébioctet de RAM, mais au prix d'une rétrocompatibilité imparfaite. Quelques programmes DOS ne marchaient pus à cause de ça. D'autres fonctionnaient convenablement et pouvaient adresser les 65 520 octets en plus.
Pour résoudre ce problème, les carte mères ajoutaient un petit circuit relié au 21ème bit d'adresse, nommé A20 (pas d'erreur, les fils du bus d'adresse sont numérotés à partir de 0). Le circuit en question pouvait mettre à zéro le fil d'adresse, ou au contraire le laisser tranquille. En le forçant à 0, le calcul des adresses déborde comme dans le mode réel des 8086. Mais s'il ne le fait pas, la ''high memory area'' est adressable. Le circuit était une simple porte ET, qui combinait le 21ème bit d'adresse avec un '''signal de commande A20''' provenant d'ailleurs.
Le signal de commande A20 était géré par le contrôleur de clavier, qui était soudé à la carte mère. Le contrôleur en question ne gérait pas que le clavier, il pouvait aussi RESET le processeur, alors gérer le signal de commande A20 n'était pas si problématique. Quitte à avoir un microcontrôleur sur la carte mère, autant s'en servir au maximum... La gestion du bus d'adresse étaitdonc gérable au clavier. D'autres carte mères faisaient autrement et préféraient ajouter un interrupteur, pour activer ou non la mise à 0 du 21ème bit d'adresse.
: Il faut noter que le signal de commande A20 était mis à 1 en mode protégé, afin que le 21ème bit d'adresse soit activé.
Le 386 ajouta deux registres de segment, les registres FS et GS, ainsi que le '''mode ''virtual 8086'''''. Ce dernier permet d’exécuter des programmes en mode réel alors que le système d'exploitation s'exécute en mode protégé. C'est une technique de virtualisation matérielle qui permet d'émuler un 8086 sur un 386. L'avantage est que la compatibilité avec les programmes anciens écrits pour le 8086 est conservée, tout en profitant de la protection mémoire. Tous les processeurs x86 qui ont suivi supportent ce mode virtuel 8086.
==La segmentation avec une table des segments==
La '''segmentation avec une table des segments''' est apparue sur des processeurs assez anciens, le tout premier étant le Burrough 5000. Elle a ensuite été utilisée sur les processeurs x86 des PCs, à partir du 286 d'Intel. Elle est aujourd'hui abandonnée sur les jeux d'instruction x86. Tout comme la segmentation en mode réel, la segmentation attribue plusieurs segments par programmes ! Et cela a des répercutions sur la manière dont la traduction d'adresse est effectuée.
===Pourquoi plusieurs segments par programme ?===
L'utilité d'avoir plusieurs segments par programme n'est pas évidente, mais elle le devient quand on se plonge dans le passé. Dans le passé, les programmeurs devaient faire avec une quantité de mémoire limitée et il n'était pas rare que certains programmes utilisent plus de mémoire que disponible sur la machine. Mais les programmeurs concevaient leurs programmes en fonction.
[[File:Overlay Programming.svg|vignette|upright=1|Overlay Programming]]
L'idée était d'implémenter un système de mémoire virtuelle, mais émulé en logiciel, appelé l{{'}}'''''overlaying'''''. Le programme était découpé en plusieurs morceaux, appelés des ''overlays''. Les ''overlays'' les plus importants étaient en permanence en RAM, mais les autres étaient faisaient un va-et-vient entre RAM et disque dur. Ils étaient chargés en RAM lors de leur utilisation, puis sauvegardés sur le disque dur quand ils étaient inutilisés. Le va-et-vient des ''overlays'' entre RAM et disque dur était réalisé en logiciel, par le programme lui-même. Le matériel n'intervenait pas, comme c'est le cas avec la mémoire virtuelle.
Avec la segmentation, un programme peut utiliser la technique des ''overlays'', mais avec l'aide du matériel. Il suffit de mettre chaque ''overlay'' dans son propre segment, et laisser la segmentation faire. Les segments sont swappés en tout ou rien : on doit swapper tout un segment en entier. L'intérêt est que la gestion du ''swapping'' est grandement facilitée, vu que c'est le système d'exploitation qui s'occupe de swapper les segments sur le disque dur ou de charger des segments en RAM. Pas besoin pour le programmeur de coder quoique ce soit. Par contre, cela demande l'intervention du programmeur, qui doit découper le programme en segments/''overlays'' de lui-même. Sans cela, la segmentation n'est pas très utile.
L{{'}}''overlaying'' est une forme de '''segmentation à granularité grossière''', à savoir que le programme est découpé en segments de grande taille. L'usage classique est d'avoir un segment pour la pile, un autre pour le code exécutable, un autre pour le reste. Éventuellement, on peut découper les trois segments précédents en deux ou trois segments, rarement au-delà. Les segments sont alors peu nombreux, guère plus d'une dizaine par programme. D'où le terme de ''granularité grossière''.
La '''segmentation à granularité fine''' pousse le concept encore plus loin. Avec elle, il y a idéalement un segment par entité manipulée par le programme, un segment pour chaque structure de donnée et/ou chaque objet. Par exemple, un tableau aura son propre segment, ce qui est idéal pour détecter les accès hors tableau. Pour les listes chainées, chaque élément de la liste aura son propre segment. Et ainsi de suite, chaque variable agrégée (non-primitive), chaque structure de donnée, chaque objet, chaque instance d'une classe, a son propre segment. Diverses fonctionnalités supplémentaires peuvent être ajoutées, ce qui transforme le processeur en véritable processeur orienté objet, mais passons ces détails pour le moment.
Vu que les segments correspondent à des objets manipulés par le programme, on peut deviner que leur nombre évolue au cours du temps. En effet, les programmes modernes peuvent demander au système d'exploitation du rab de mémoire pour allouer une nouvelle structure de données. Avec la segmentation à granularité fine, cela demande d'allouer un nouveau segment à chaque nouvelle allocation mémoire, à chaque création d'une nouvelle structure de données ou d'un objet. De plus, les programmes peuvent libérer de la mémoire, en supprimant les structures de données ou objets dont ils n'ont plus besoin. Avec la segmentation à granularité fine, cela revient à détruire le segment alloué pour ces objets/structures de données. Le nombre de segments est donc dynamique, il change au cours de l'exécution du programme.
===Les tables de segments avec la segmentation===
La présence de plusieurs segments par programme a un impact sur la table des segments. Avec la relocation matérielle, elle conte nait un segment par programme. Chaque entrée, chaque ligne de la table des segment, mémorisait l'adresse de base, l'adresse limite, un bit de présence pour la mémoire virtuelle et des autorisations liées à la protection mémoire. Avec la segmentation, les choses sont plus compliquées, car il y a plusieurs segments par programme. Les entrées ne sont pas modifiées, mais elles sont organisées différemment.
Avec cette forme de segmentation, la table des segments doit respecter plusieurs contraintes. Premièrement, il y a plusieurs segments par programmes. Deuxièmement, le nombre de segments est variable : certains programmes se contenteront d'un seul segment, d'autres de dizaine, d'autres plusieurs centaines, etc. Il y a typiquement deux manières de faire : soit utiliser une table des segments uniques, utiliser une table des segment par programme.
Il est possible d'utiliser une table des segment unique qui mémorise tous les segments de tous les processus, système d'exploitation inclut. On parle alors de '''table des segment globale'''. Mais cette solution n'est pas utilisée avec la segmentation proprement dite. Elle est utilisée sur les architectures à capacité qu'on détaillera vers la fin du chapitre, dans une section dédiée. À la place, la segmentation utilise une table de segment par processus/programme, chacun ayant une '''table des segment locale'''.
Dans les faits, les choses sont plus compliquées. Le système d'exploitation doit savoir où se trouvent les tables de segment locale pour chaque programme. Pour cela, il a besoin d'utiliser une table de segment globale, dont chaque entrée pointe non pas vers un segment, mais vers une table de segment locale. Lorsque l'OS effectue une commutation de contexte, il lit la table des segment globale, pour récupérer un pointeur vers celle-ci. Ce pointeur est alors chargé dans un registre du processeur, qui mémorise l'adresse de la table locale, ce qui sert lors des accès mémoire.
Une telle organisation fait que les segments d'un processus/programme sont invisibles pour les autres, il y a une certaine forme de sécurité. Un programme ne connait que sa table de segments locale, il n'a pas accès directement à la table des segments globales. Tout accès mémoire se passera à travers la table de segment locale, il ne sait pas où se trouvent les autres tables de segment locales.
Les processeurs x86 sont dans ce cas : ils utilisent une table de segment globale couplée à autant de table des segments qu'il y a de processus en cours d'exécution. La table des segments globale s'appelle la '''''Global Descriptor Table''''' et elle peut contenir 8192 segments maximum, ce qui permet le support de 8192 processus différents. Les tables de segments locales sont appelées les '''''Local Descriptor Table''''' et elles font aussi 8192 segments maximum, ce qui fait 8192 segments par programme maximum. Il faut noter que la table de segment globale peut mémoriser des pointeurs vers les routines d'interruption, certaines données partagées (le tampon mémoire pour le clavier) et quelques autres choses, qui n'ont pas leur place dans les tables de segment locales.
===La relocation avec la segmentation===
La table des segments locale mémorise les adresses de base et limite de chaque segment, ainsi que d'autres méta-données. Les informations pour un segment sont regroupés dans un '''descripteur de segment''', qui est codé sur plusieurs octets, et qui regroupe : adresse de base, adresse limite, bit de présence en RAM, méta-données de protection mémoire.
La table des segments est un tableau dans lequel les descripteurs de segment sont placés les uns à la suite des autres en mémoire RAM. La table des segments est donc un tableau de segment. Les segments d'un programme sont numérotés, le nombre s'appelant un '''indice de segment''', appelé '''sélecteur de segment''' dans la terminologie Intel. L'indice de segment n'est autre que l'indice du segment dans ce tableau.
[[File:Global Descriptor table.png|centre|vignette|upright=2|Table des segments locale.]]
Il n'y a pas de registre de segment proprement dit, qui mémoriserait l'adresse de base. À la place, les segments sont adressés de manière indirecte. À la place, les registres de segment mémorisent des sélecteurs de segment. Ils sont utilisés pour lire l'adresse de base/limite dans la table de segment en mémoire RAM. Pour cela, un registre mémorise l'adresse de la table de segment locale, sa position en mémoire RAM.
Toute lecture ou écriture se fait en deux temps, en deux accès mémoire, consécutifs. Premièrement, le numéro de segment est utilisé pour adresser la table des segment. La lecture récupère alors un pointeur vers ce segment. Deuxièmement, ce pointeur est utilisé pour faire la lecture ou écriture. Plus précisément, la première lecture récupère un descripteur de segment qui contient l'adresse de base, le pointeur voulu, mais aussi l'adresse limite et d'autres informations.
[[File:Segmentation avec table des segments.png|centre|vignette|upright=2|Segmentation avec table des segments]]
L'accès à la table des segments se fait automatiquement à chaque accès mémoire. La conséquence est que chaque accès mémoire demande d'en faire deux : un pour lire la table des segments, l'autre pour l'accès lui-même. Il s'agit en quelque sorte d'une forme d'adressage indirect mémoire.
Un point important est que si le premier accès ne fait qu'une simple lecture dans un tableau, le second accès implique des calculs d'adresse. En effet, le premier accès récupère l'adresse de base du segment, mais le second accès sélectionne une donnée dans le segment, ce qui demande de calculer son adresse. L'adresse finale se déduit en combinant l'adresse de base avec un décalage (''offset'') qui donne la position de la donnée dans ce segment. L'indice de segment est utilisé pour récupérer l'adresse de base du segment. Une fois cette adresse de base connue, on lui additionne le décalage pour obtenir l'adresse finale.
[[File:Table des segments.png|centre|vignette|upright=2|Traduction d'adresse avec une table des segments.]]
Pour effectuer automatiquement l'accès à la table des segments, le processeur doit contenir un registre supplémentaire, qui contient l'adresse de la table de segment, afin de la localiser en mémoire RAM. Nous appellerons ce registre le '''pointeur de table'''. Le pointeur de table est combiné avec l'indice de segment pour adresser le descripteur de segment adéquat.
[[File:Segment 2.svg|centre|vignette|upright=2|Traduction d'adresse avec une table des segments, ici appelée table globale des de"scripteurs (terminologie des processeurs Intel x86).]]
Un point important est que la table des segments n'est pas accessible pour le programme en cours d'exécution. Il ne peut pas lire le contenu de la table des segments, et encore moins la modifier. L'accès se fait seulement de manière indirecte, en faisant usage des indices de segments, mais c'est un adressage indirect. Seul le système d'exploitation peut lire ou écrire la table des segments directement.
Plus haut, j'ai dit que tout accès mémoire impliquait deux accès mémoire : un pour charger le descripteur de segment, un autre pour la lecture/écriture proprement dite. Cependant, cela aurait un impact bien trop grand sur les performances. Dans les faits, les processeurs avec segmentations intégraient un '''cache de descripteurs de segments''', pour limiter la casse. Quand un descripteur de segment est lu depuis la RAM, il est copié dans ce cache. Les accès ultérieurs accédent au descripteur dans le cache, pas besoin de passer par la RAM. L'intel 386 avait un cache de ce type.
===La protection mémoire : les accès hors-segments===
Comme avec la relocation matérielle, le processeur détecte les débordements de segment. Pour cela, il compare l'adresse logique accédée avec l'adresse limite, ou compare la taille limite avec le décalage. De nombreux processeurs, comme l'Intel 386, préféraient utiliser la taille du segment, pour une question d'optimisation. En effet, si on compare l'adresse finale avec l'adresse limite, on doit faire la relocation avant de comparer l'adresse relocatée. Mais en utilisant la taille, ce n'est pas le cas : on peut faire la comparaison avant, pendant ou après la relocation.
Un détail à prendre en compte est la taille de la donnée accédée. Sans cela, la comparaison serait très simple : on vérifie si ''décalage <= taille du segment'', ou on compare des adresses de la même manière. Mais imaginez qu'on accède à une donnée de 4 octets : il se peut que l'adresse de ces 4 octets rentre dans le segment, mais que quelques octets débordent. Par exemple, les deux premiers octets sont dans le segment, mais pas les deux suivants. La vraie comparaison est alors : ''décalage + 4 octets <= taille du segment''.
Mais il est possible de faire le calcul autrement, et quelques processeurs comme l'Intel 386 ne s'en sont pas privé. Il calculait la différence ''taille du segment - décalage'', et vérifiait le résultat. Le processeur gérait des données de 1, 2 et 4 octets, ce qui fait que le résultat devait être entre 0 et 3. Le processeur prenait le résultat de la soustraction, et vérifiait alors que les 30 bits de poids fort valaient bien 0. Il vérifiait aussi que les deux bits de poids faible avaient la bonne valeur.
[[File:Vm7.svg|centre|vignette|upright=2|Traduction d'adresse avec vérification des accès hors-segment.]]
Une nouveauté fait son apparition avec la segmentation : la '''gestion des droits d'accès'''. Par exemple, il est possible d'interdire d'exécuter le contenu d'un segment, ce qui fournit une protection contre certaines failles de sécurité ou certains virus. Lorsqu'on exécute une opération interdite, le processeur lève une exception matérielle, à charge du système d'exploitation de gérer la situation.
Pour cela, chaque segment se voit attribuer un certain nombre d'autorisations d'accès qui indiquent si l'on peut lire ou écrire dedans, si celui-ci contient un programme exécutable, etc. Les autorisations pour chaque segment sont placées dans le descripteur de segment. Elles se résument généralement à quelques bits, qui indiquent si le segment est accesible en lecture/écriture ou exécutable. Le tout est souvent concaténé dans un ou deux '''octets de droits d'accès'''.
L'implémentation de la protection mémoire dépend du CPU considéré. Les CPU microcodés peuvent en théorie utiliser le microcode. Lorsqu'une instruction mémoire s'exécute, le microcode effectue trois étapes : lire le descripteur de segment, faire les tests de protection mémoire, exécuter la lecture/écriture ou lever une exception. Létape de test est réalisée avec un ou plusieurs micro-branchements. Par exemple, une écriture va tester le bit R/W du descripteur, qui indique si on peut écrire dans le segment, en utilisant un micro-branchement. Le micro-branchement enverra vers une routine du microcode en cas d'erreur.
Les tests de protection mémoire demandent cependant de tester beaucoup de conditions différentes. Par exemple, le CPU Intel 386 testait moins d'une dizaine de conditions pour certaines instructions. Il est cependant possible de faire plusieurs comparaisons en parallèle en rusant un peu. Il suffit de mémoriser les octets de droits d'accès dans un registre interne, de masquer les bits non-pertinents, et de faire une comparaison avec une constante adéquate, qui encode la valeur que doivent avoir ces bits.
Une solution alternative utiliser un circuit combinatoire pour faire les tests de protection mémoire. Les tests sont alors faits en parallèles, plutôt qu'un par un par des micro-branchements. Par contre, le cout en matériel est assez important. Il faut ajouter ce circuit combinatoire, ce qui demande pas mal de circuits.
===La mémoire virtuelle avec la segmentation===
La mémoire virtuelle est une fonctionnalité souvent implémentée sur les processeurs qui gèrent la segmentation, alors que les processeurs avec relocation matérielle s'en passaient. Il faut dire que l'implémentation de la mémoire virtuelle est beaucoup plus simple avec la segmentation, comparé à la relocation matérielle. Le remplacement des registres de base par des sélecteurs de segment facilite grandement l'implémentation.
Le problème de la mémoire virtuelle est que les segments peuvent être swappés sur le disque dur n'importe quand, sans que le programme soit prévu. Le swapping est réalisé par une interruption de l'OS, qui peut interrompre le programme n'importe quand. Et si un segment est swappé, le registre de base correspondant devient invalide, il point sur une adresse en RAM où le segment était, mais n'est plus. De plus, les segments peuvent être déplacés en mémoire, là encore n'importe quand et d'une manière invisible par le programme, ce qui fait que les registres de base adéquats doivent être modifiés.
Si le programme entier est swappé d'un coup, comme avec la relocation matérielle simple, cela ne pose pas de problèmes. Mais dès qu'on utilise plusieurs registres de base par programme, les choses deviennent soudainement plus compliquées. Le problème est qu'il n'y a pas de mécanismes pour choisir et invalider le registre de base adéquat quand un segment est déplacé/swappé. En théorie, on pourrait imaginer des systèmes qui résolvent le problème au niveau de l'OS, mais tous ont des problèmes qui font que l'implémentation est compliquée ou que les performances sont ridicules.
L'usage d'une table des segments accédée à chaque accès résout complètement le problème. La table des segments est accédée à chaque accès mémoire, elle sait si le segment est swappé ou non, chaque accès vérifie si le segment est en mémoire et quelle est son adresse de base. On peut changer le segment de place n'importe quand, le prochain accès récupérera des informations à jour dans la table des segments.
L'implémentation de la mémoire virtuelle avec la segmentation est simple : il suffit d'ajouter un bit dans les descripteurs de segments, qui indique si le segment est swappé ou non. Tout le reste, la gestion de ce bit, du swap, et tout ce qui est nécessaire, est délégué au système d'exploitation. Lors de chaque accès mémoire, le processeur vérifie ce bit avant de faire la traduction d'adresse, et déclenche une exception matérielle si le bit indique que le segment est swappé. L'exception matérielle est gérée par l'OS.
===Le partage de segments===
Il est possible de partager un segment entre plusieurs applications. Cela peut servir pour partager des données entre deux programmes : un segment de données partagées est alors partagé entre deux programmes. Partager un segment de code est utile pour les bibliothèques partagées : la bibliothèque est placée dans un segment dédié, qui est partagé entre les programmes qui l'utilisent. Partager un segment de code est aussi utile quand plusieurs instances d'une même application sont lancés simultanément : le code n'ayant pas de raison de changer, celui-ci est partagé entre toutes les instances. Mais ce n'est là qu'un exemple.
La première solution pour cela est de configurer les tables de segment convenablement. Le même segment peut avoir des droits d'accès différents selon les processus. Les adresses de base/limite sont identiques, mais les tables des segments ont alors des droits d'accès différents. Mais cette méthode de partage des segments a plusieurs défauts.
Premièrement, les sélecteurs de segments ne sont pas les mêmes d'un processus à l'autre, pour un même segment. Le segment partagé peut correspondre au segment numéro 80 dans le premier processus, au segment numéro 1092 dans le second processus. Rien n'impose que les sélecteurs de segment soient les mêmes d'un processus à l'autre, pour un segment identique.
Deuxièmement, les adresses limite et de base sont dupliquées dans plusieurs tables de segments. En soi, cette redondance est un souci mineur. Mais une autre conséquence est une question de sécurité : que se passe-t-il si jamais un processus a une table des segments corrompue ? Il se peut que pour un segment identique, deux processus n'aient pas la même adresse limite, ce qui peut causer des failles de sécurité. Un processus peut alors subir un débordement de tampon, ou tout autre forme d'attaque.
[[File:Vm9.png|centre|vignette|upright=2|Illustration du partage d'un segment entre deux applications.]]
Une seconde solution, complémentaire, utilise une table de segment globale, qui mémorise des segments partagés ou accessibles par tous les processus. Les défauts de la méthode précédente disparaissent avec cette technique : un segment est identifié par un sélecteur unique pour tous les processus, il n'y a pas de duplication des descripteurs de segment. Par contre, elle a plusieurs défauts.
Le défaut principal est que cette table des segments est accessible par tous les processus, impossible de ne partager ses segments qu'avec certains pas avec les autres. Un autre défaut est que les droits d'accès à un segment partagé sont identiques pour tous les processus. Impossible d'avoir un segment partagé accessible en lecture seule pour un processus, mais accessible en écriture pour un autre. Il est possible de corriger ces défauts, mais nous en parlerons dans la section sur les architectures à capacité.
===L'extension d'adresse avec la segmentation===
L'extension d'adresse est possible avec la segmentation, de la même manière qu'avec la relocation matérielle. Il suffit juste que les adresses de base soient aussi grandes que le bus d'adresse. Mais il y a une différence avec la relocation matérielle : un même programme peut utiliser plus de mémoire qu'il n'y en a dans l'espace d'adressage. La raison est simple : un segment peut prendre tout l'espace d'adressage, et il y a plusieurs segments par programme.
Pour donner un exemple, prenons un processeur 16 bits, qui peut adresser 64 kibioctets, associé à une mémoire de 4 mébioctets. Il est possible de placer le code machine dans les premiers 64k de la mémoire, la pile du programme dans les 64k suivants, le tas dans les 64k encore après, et ainsi de suite. Le programme dépasse donc les 64k de mémoire de l'espace d'adressage. Ce genre de chose est impossible avec la relocation, où un programme est limité par l'espace d'adressage.
===Le mode protégé des processeurs x86===
L'Intel 80286, aussi appelé 286, ajouta un mode de segmentation séparé du mode réel, qui ajoute une protection mémoire à la segmentation, ce qui lui vaut le nom de '''mode protégé'''. Dans ce mode, les registres de segment ne contiennent pas des adresses de base, mais des sélecteurs de segments qui sont utilisés pour l'accès à la table des segments en mémoire RAM.
Le 286 bootait en mode réel, puis le système d'exploitation devait faire quelques manipulations pour passer en mode protégé. Le 286 était pensé pour être rétrocompatible au maximum avec le 80186. Mais les différences entre le 286 et le 8086 étaient majeures, au point que les applications devaient être réécrites intégralement pour profiter du mode protégé. Un mode de compatibilité permettait cependant aux applications destinées au 8086 de fonctionner, avec même de meilleures performances. Aussi, le mode protégé resta inutilisé sur la plupart des applications exécutées sur le 286.
Vint ensuite le processeur 80386, renommé en 386 quelques années plus tard. Sur ce processeur, les modes réel et protégé sont conservés tel quel, à une différence près : toutes les adresses passent à 32 bits, qu'il s'agisse des adresses de base, limite ou des ''offsets''. Le processeur peut donc adresser un grand nombre de segments : 2^32, soit plus de 4 milliards. Les segments grandissent aussi et passent de 64 KB maximum à 4 gibioctets maximum. Mais surtout : le 386 ajouta le support de la pagination en plus de la segmentation. Ces modifications ont été conservées sur les processeurs 32 bits ultérieurs.
Les processeurs x86 gèrent deux types de tables des segments : une table locale pour chaque processus, et une table globale partagée entre tous les processus. Il ne peut y avoir qu'une table locale d'active, vu que le processeur ne peut exécuter qu'un seul processus en même temps. Chaque table locale définit 8192 segments, pareil pour la table globale. La table globale est utilisée pour les segments du noyau et la mémoire partagée entre processus. Un défaut est qu'un segment partagé par la table globale est visible par tous les processus, avec les mêmes droits d'accès. Ce qui fait que cette méthode était peu utilisée en pratique. La table globale mémorise aussi des pointeurs vers les tables locales, avec un descripteur de segment par table locale.
Sur les processeurs x86 32 bits, un descripteur de segment est organisé comme suit, pour les architectures 32 bits. On y trouve l'adresse de base et la taille limite, ainsi que de nombreux bits de contrôle.
Le premier groupe de bits de contrôle est l'octet en bleu à droite. Il contient :
* le bit P qui indique que l'entrée contient un descripteur valide, qu'elle n'est pas vide ;
* deux bits DPL qui indiquent le niveau de privilège du segment (noyau, utilisateur, les deux intermédiaires spécifiques au x86) ;
* un bit S qui précise si le segment est de type système (utiles pour l'OS) ou un segment de code/données.
* un champ Type qui contient les bits suivants :
** un bit E qui indique si le segment contient du code exécutable ou non ;
** le bit RW qui indique s'il est en lecture seule ou non ;;
** Un bit A qui indique que le segment a récemment été accédé, information utile pour l'OS;
** un bit DC assez spécifiques.
En haut à gauche, en bleu, on trouve deux bits :
* Le bit G indique comment interpréter la taille contenue dans le descripteur : 0 si la taille est exprimée en octets, 1 si la taille est un nombre de pages de 4 kibioctets. Ce bit précise si on utilise la segmentation seule, ou combinée avec la pagination.
* Le bit DB précise si l'on utilise des segments en mode de compatibilité 16 bits ou des segments 32 bits.
[[File:SegmentDescriptor.svg|centre|vignette|upright=3|Segment Descriptor]]
Les indices de segment sont appelés des sélecteurs de segment. Ils ont une taille de 16 bits, mais 3 bits sont utilisés pour encoder des méta-données. Le numéro de segment est donc codé sur 13 bits, ce qui permettait de gérer maximum 8192 segments par table de segment (locale ou globale). Les 16 bits sont organisés comme suit :
* 13 bits pour le numéro du segment dans la table des segments, l'indice de segment proprement dit ;
* un bit qui précise s'il faut accéder à la table des segments globale ou locale ;
* deux bits qui indiquent le niveau de privilège de l'accès au segment (les 4 niveaux de protection, dont l'espace noyau et utilisateur).
[[File:SegmentSelector.svg|centre|vignette|upright=1.5|Sélecteur de segment 16 bit.]]
En tout, l'indice permet de gérer 8192 segments pour la table locale et 8192 segments de la table globale.
====La MMU du 386/486 : cache de segment, protection mémoire====
La MMU du 386 et celle du 486 étaient assez similaires. Elles étaient plus complexes que celle du 186 et du 286. Elle contenait un additionneur pour les calculs d'adresse et un comparateur pour tester si l'accès mémoire déborde d'un segment. Le test de débordement se faisait en parallèle du calcul de l'adresse finale, comme sur le 286.
L'additionneur était un additionneur trois-opérandes, qui additionnait l'adresse à lire/écrire, l'adresse de base du segment, et un décalage intégré dans l'instruction. En clair, l'unité de segmentation n'était pas qu'une MMU, elle prenait en charge une partie du calcul d'adresse. L'avantage est que cela permettait de gérer les modes d'adressage "base + décalage" et "base + indice + décalage" très facilement, en utilisant un minimum de circuits. Une conséquence de cette organisation était que l'usage d'un décalage était gratuit. Par contre, dès qu'on utilisait l'adressage "Base + Indice", avec ou sans décalage, l'instruction prenait un cycle de plus à s'exécuter, parce qu'il fallait faire l'addition "Base + Indice" dans l'ALU entière.
Le CPU 386 était le premier à implémenter la protection mémoire avec des segments. Pour cela, il intégrait une '''''Protection Test Unit''''', séparée du microcode, qu'on va abrévier en PTU. Précisément, il s'agissait d'un PLA (''Programmable Logic Array''), une sorte d'intermédiaire entre circuit logique fait sur mesure et mémoire ROM, qu'on a déjà abordé dans le chapitre sur les mémoires ROM. Mais cette unité ne faisait pas tout, le microcode était aussi impliqué. La PTU sera détaillée dans la section suivante.
Pour améliorer les performances, le 386 et le 486 intégraient un '''cache de descripteurs de segment''', aussi appelé le cache de descripteurs. Lorsqu'un descripteur état chargé pour la première fois, il était copié dans le cache de descripteurs de segment. Les accès mémoire ultérieur lisaient le descripteur de segment depuis ce cache, pas depuis la table des segments en RAM. Le cache de descripteurs gère aussi bien les segments en mode réel qu'en mode protégé. Récupérer l'adresse de base depuis cache se fait un peu différemment en mode réel et protégé, mais le cache gère cela tout seul. Idem pour récupérer la taille/adresse limite.
[[File:Microarchitecture du 386, avec focus sur la segmentation.png|centre|vignette|upright=2|Microarchitecture du 386 et du 486, avec focus sur la segmentation.]]
En mode réel, la taille des segment est censée être limitée à 64 kibioctets. Mais le processeur ne vérifiait pas si cette limite était dépassée. À la place, il utilisait la limite précisée dans le cache de segments. Pire que ça : le cache de segment n'était pas réinitialisé quand on passe du mode réel au mode protégé, et réciproquement. Et cette propriété a été à l'origine de l''''''unreal mode'''''. Il s'agissait d'un mode réel amélioré, capable d'utiliser des segments de 4 gibioctets et des adresses de 32 bits.
Passer en mode ''unreal'' pouvait se faire de deux manières. Il était possible d'altérer le cache de descripteur en utilisant l'instruction non-documentée LOADALL. Elle permettait de charger les descripteurs de segments dans le cache de descripteurs, avec une taille arbitraire. Une autre solution, beaucoup plus complexe sur le 386, demandait d'entrer en mode protégé pour configurer des segments de grande taille, de charger leurs descripteurs dans le cache de descripteur, puis de revenir en mode réel. En mode réel, les descripteurs dans le cache étaient encore disponibles et on pouvait les lire dans le cache.
====L'implémentation de la protection mémoire sur le 386====
La protection mémoire teste la valeur des bits P, S, X, E, R/W. Elle teste aussi les niveaux de privilège, avec deux bits DPL et CPL. En tout, le processeur pouvait tester 148 conditions différentes en parallèle dans la PTU. Cependant, les niveaux de privilèges étaient pré-traités par le microcode. Le microcode vérifiait aussi s'il y avait une erreur en terme d’anneau mémoire, avec par "exemple un segment en mode noyau accédé alors que le CPU est en espace utilisateur. Il fournissait alors un résultat sur deux bits, qui indiquait s'il y avait une erreur ou non, que la PTU utilisait.
Mais toutes les conditions n'étaient pas pertinentes à un instant t. Par exemple, il est pertinent de vérifier si le bit R/W était cohérent si l'instruction à exécuter est une écriture. Mais il n'y a pas besoin de tester le bit E qui indique qu'un segment est exécutable ou non, pour une lecture. En tout, le processeur pouvait se retrouver dans 33 situations possibles, chacune demandant de tester un sous-ensemble des 148 conditions. Pour préciser quel sous-ensembles tester, la PTU recevait un code opération, généré par le microcode.
Pour faire les tests de protection mémoire, le microcode avait une micro-opération nommée ''protection test operation'', qui envoyait les droits d'accès à la PTU. Lors de l'exécution d'une ''protection test operation'', le PLA recevait un descripteur de segment, lu depuis la mémoire RAM, ainsi qu'un code opération provenant du microcode.
{|class="wikitable"
|+ Entrée de la ''Protection Test Unit''
|-
! 15 - 14 !! 13 - 12 !! 11 !! 10 !! 9 !! 8 !! 7 !! 6 !! 5-0
|-
| P1 , P2 || || P || S || X || E || R/W || A || Code opération
|-
| Niveaux de privilèges cohérents/erreur || || Segment présent en mémoire ou swappé || S || X || Segment exécutable ou non || Segment accesible en lecture/écriture || Segment récemment accédé || Code opération
|}
Il fournissait en sortie un bit qui indiquait si une erreur de protection mémoire avait eu lieu ou non. Il fournissait aussi une adresse de 12 bits, utilisée seulement en cas d'erruer. Elle pointait dans le microcode, sur un code levant une exception en cas d'erreur. Enfin, la PTU fournissait 4 bits pouvant être testés par un branchement dans le microcode. L'un d'entre eux demandait de tester s'il y a un accès hors-limite, les autres étaient assez peu reliés à la protection mémoire.
Un détail est que le chargement du descripteur de segment est réalisé par une fonction dans le microcode. Elle est appliquée pour toutes les instructions ou situations qui demandent de faire un accès mémoire. Et les tests de protection mémoire sont réalisés dans cette fonction, pas après elle. Vu qu'il s'agit d'une fonction exécutée quelque soit l'instruction, le microcode doit transférer le code opération à cette fonction. Le microcode est pour cela associé à un registre interne, dans lequel le code opération est mémorisé, avant d'appeler la fonction. Le microcode a une micro-opération PTSAV (''Protection Save'') pour mémoriser le code opération dans ce registre. Dans la fonction qui charge le descripteur, une micro-opération PTOVRR (''Protection Override'') lit le code opération dans ce registre, et lance les tests nécessaires.
Il faut noter que le PLA était certes plus rapide que de tester les conditions une par une, mais il était assez lent. La PTU mettait environ 3 cycles d'horloges pour rendre son résultat. Le microcode en profitait alors pour exécuter des micro-opérations durant ces 3 cycles d'attente. Par exemple, le microcode pouvait en profiter pour lire l'adresse de base dans le descripteur, si elle n'a pas été chargée avant (les descripteur était chargé en deux fois). Il fallait cependant que les trois micro-opérations soient valides, peu importe qu'il y ait une erreur de protection mémoire ou non. Ou du moins, elles produisaient un résultat qui n'est pas utilisé en cas d'erreur. Si ce n'était pas possible, le microcode ajoutait des NOP pendant ce temps d'attente de 3 cycles.
Le bit A du descripteur de segment indique que le segment a récemment été accédé. Il est mis à jour après les tests de protection mémoire, quand ceux-ci indiquent que l'accès mémoire est autorisé. Le bit A est mis à 1 si la PTU l'autorise. Pour cela, la PTU utilise un des 4 bits de sortie mentionnés plus haut : l'un d'entre eux indique que le bit A doit être mis à 1. La mise à jour est ensuite réalisée par le microcode, qui utilise trois micro-opérations pour le mettre à jour.
====Le ''Hardware task switching'' des CPU x86====
Les systèmes d’exploitation modernes peuvent lancer plusieurs logiciels en même temps. Les logiciels sont alors exécutés à tour de rôle. Passer d'un programme à un autre est ce qui s'appelle une commutation de contexte. Lors d'une commutation de contexte, l'état du processeur est sauvegardé, afin que le programme stoppé puisse reprendre là où il était. Il arrivera un moment où le programme stoppé redémarrera et il doit reprendre dans l'état exact où il s'est arrêté. Deuxièmement, le programme à qui c'est le tour restaure son état. Cela lui permet de revenir là où il était avant d'être stoppé. Il y a donc une sauvegarde et une restauration des registres.
Divers processeurs incorporent des optimisations matérielles pour rendre la commutation de contexte plus rapide. Ils peuvent sauvegarder et restaurer les registres du processeur automatiquement lors d'une interruption de commutation de contexte. Les registres sont sauvegardés dans des structures de données en mémoire RAM, appelées des '''contextes matériels'''. Sur les processeurs x86, il s'agit de la technique d{{'}}''Hardware Task Switching''. Fait intéressant, le ''Hardware Task Switching'' se base beaucoup sur les segments mémoires.
Avec ''Hardware Task Switching'', chaque contexte matériel est mémorisé dans son propre segment mémoire, séparé des autres. Les segments pour les contextes matériels sont appelés des '''''Task State Segment''''' (TSS). Un TSS mémorise tous les registres généraux, le registre d'état, les pointeurs de pile, le ''program counter'' et quelques registres de contrôle du processeur. Par contre, les registres flottants ne sont pas sauvegardés, de même que certaines registres dit SIMD que nous n'avons pas encore abordé. Et c'est un défaut qui fait que le ''Hardware Task Switching'' n'est plus utilisé.
Le programme en cours d'exécution connait l'adresse du TSS qui lui est attribué, car elle est mémorisée dans un registre appelé le '''''Task Register'''''. En plus de pointer sur le TSS, ce registre contient aussi les adresses de base et limite du segment en cours. Pour être plus précis, le ''Task Register'' ne mémorise pas vraiment l'adresse du TSS. À la place, elle mémorise le numéro du segment, le numéro du TSS. Le numéro est codé sur 16 bits, ce qui explique que 65 536 segments sont adressables. Les instructions LDR et STR permettent de lire/écrire ce numéro de segment dans le ''Task Register''.
Le démarrage d'un programme a lieu automatiquement dans plusieurs circonstances. La première est une instruction de branchement CALL ou JMP adéquate. Le branchement fournit non pas une adresse à laquelle brancher, mais un numéro de segment qui pointe vers un TSS. Cela permet à une routine du système d'exploitation de restaurer les registres et de démarrer le programme en une seule instruction de branchement. Une seconde circonstance est une interruption matérielle ou une exception, mais nous la mettons de côté. Le ''Task Register'' est alors initialisé avec le numéro de segment fournit. S'en suit la procédure suivante :
* Le ''Task Register'' est utilisé pour adresser la table des segments, pour récupérer un pointeur vers le TSS associé.
* Le pointeur est utilisé pour une seconde lecture, qui adresse le TSS directement. Celle-ci restaure les registres du processeur.
En clair, on va lire le ''TSS descriptor'' dans la GDT, puis on l'utilise pour restaurer les registres du processeur.
[[File:Hardware Task Switching x86.png|centre|vignette|upright=2|Hardware Task Switching x86]]
===La segmentation sur les processeurs Burrough B5000 et plus===
Le Burrough B5000 est un très vieil ordinateur, commercialisé à partir de l'année 1961. Ses successeurs reprennent globalement la même architecture. C'était une machine à pile, doublé d'une architecture taguée, choses très rare de nos jours. Mais ce qui va nous intéresser dans ce chapitre est que ce processeur incorporait la segmentation, avec cependant une différence de taille : un programme avait accès à un grand nombre de segments. La limite était de 1024 segments par programme ! Il va de soi que des segments plus petits favorise l'implémentation de la mémoire virtuelle, mais complexifie la relocation et le reste, comme nous allons le voir.
Le processeur gère deux types de segments : les segments de données et de procédure/fonction. Les premiers mémorisent un bloc de données, dont le contenu est laissé à l'appréciation du programmeur. Les seconds sont des segments qui contiennent chacun une procédure, une fonction. L'usage des segments est donc différent de ce qu'on a sur les processeurs x86, qui n'avaient qu'un segment unique pour l'intégralité du code machine. Un seul segment de code machine x86 est découpé en un grand nombre de segments de code sur les processeurs Burrough.
La table des segments contenait 1024 entrées de 48 bits chacune. Fait intéressant, chaque entrée de la table des segments pouvait mémoriser non seulement un descripteur de segment, mais aussi une valeur flottante ou d'autres types de données ! Parler de table des segments est donc quelque peu trompeur, car cette table ne gère pas que des segments, mais aussi des données. La documentation appelaiat cette table la '''''Program Reference Table''''', ou PRT.
La raison de ce choix quelque peu bizarre est que les instructions ne gèrent pas d'adresses proprement dit. Tous les accès mémoire à des données en-dehors de la pile passent par la segmentation, ils précisent tous un indice de segment et un ''offset''. Pour éviter d'allouer un segment pour chaque donnée, les concepteurs du processeur ont décidé qu'une entrée pouvait contenir directement la donnée entière à lire/écrire.
La PRT supporte trois types de segments/descripteurs : les descripteurs de données, les descripteurs de programme et les descripteurs d'entrées-sorties. Les premiers décrivent des segments de données. Les seconds sont associés aux segments de procédure/fonction et sont utilisés pour les appels de fonction (qui passent, eux aussi, par la segmentation). Le dernier type de descripteurs sert pour les appels systèmes et les communications avec l'OS ou les périphériques.
Chaque entrée de la PRT contient un ''tag'', une suite de bit qui indique le type de l'entrée : est-ce qu'elle contient un descripteur de segment, une donnée, autre. Les descripteurs contiennent aussi un ''bit de présence'' qui indique si le segment a été swappé ou non. Car oui, les segments pouvaient être swappés sur ce processeur, ce qui n'est pas étonnant vu que les segments sont plus petits sur cette architecture. Le descripteur contient aussi l'adresse de base du segment ainsi que sa taille, et diverses informations pour le retrouver sur le disque dur s'il est swappé.
: L'adresse mémorisée ne faisait que 15 bits, ce qui permettait d'adresse 32 kibi-mots, soit 192 kibioctets de mémoire. Diverses techniques d'extension d'adressage étaient disponibles pour contourner cette limitation. Outre l'usage de l{{'}}''overlay'', le processeur et l'OS géraient aussi des identifiants d'espace d'adressage et en fournissaient plusieurs par processus. Les processeurs Borrough suivants utilisaient des adresses plus grandes, de 20 bits, ce qui tempérait le problème.
[[File:B6700Word.jpg|centre|vignette|upright=2|Structure d'un mot mémoire sur le B6700.]]
==Les architectures à capacités==
Les architectures à capacité utilisent la segmentation à granularité fine, mais ajoutent des mécanismes de protection mémoire assez particuliers, qui font que les architectures à capacité se démarquent du reste. Les architectures de ce type sont très rares et sont des processeurs assez anciens. Le premier d'entre eux était le Plessey System 250, qui date de 1969. Il fu suivi par le CAP computer, vendu entre les années 70 et 77. En 1978, le System/38 d'IBM a eu un petit succès commercial. En 1980, la Flex machine a aussi été vendue, mais à très peu d'examplaires, comme les autres architectures à capacité. Et enfin, en 1981, l'architecture à capacité la plus connue, l'Intel iAPX 432 a été commercialisée. Depuis, la seule architecture de ce type est en cours de développement. Il s'agit de l'architecture CHERI, dont la mise en projet date de 2014.
===Le partage de la mémoire sur les architectures à capacités===
Le partage de segment est grandement modifié sur les architectures à capacité. Avec la segmentation normale, il y a une table de segment par processus. Les conséquences sont assez nombreuses, mais la principale est que partager un segment entre plusieurs processus est compliqué. Les défauts ont été évoqués plus haut. Les sélecteurs de segments ne sont pas les mêmes d'un processus à l'autre, pour un même segment. De plus, les adresses limite et de base sont dupliquées dans plusieurs tables de segments, et cela peut causer des problèmes de sécurité si une table des segments est modifiée et pas l'autre. Et il y a d'autres problèmes, tout aussi importants.
[[File:Partage des segments avec la segmentation.png|centre|vignette|upright=1.5|Partage des segments avec la segmentation]]
À l'opposé, les architectures à capacité utilisent une table des segments unique pour tous les processus. La table des segments unique sera appelée dans de ce qui suit la '''table des segments globale''', ou encore la table globale. En conséquence, les adresses de base et limite ne sont présentes qu'en un seul exemplaire par segment, au lieu d'être dupliquées dans autant de processus que nécessaire. De plus, cela garantit que l'indice de segment est le même quel que soit le processus qui l'utilise.
Un défaut de cette approche est au niveau des droits d'accès. Avec la segmentation normale, les droits d'accès pour un segment sont censés changer d'un processus à l'autre. Par exemple, tel processus a accès en lecture seule au segment, l'autre seulement en écriture, etc. Mais ici, avec une table des segments uniques, cela ne marche plus : incorporer les droits d'accès dans la table des segments ferait que tous les processus auraient les mêmes droits d'accès au segment. Et il faut trouver une solution.
===Les capacités sont des pointeurs protégés===
Pour éviter cela, les droits d'accès sont combinés avec les sélecteurs de segments. Les sélecteurs des segments sont remplacés par des '''capacités''', des pointeurs particuliers formés en concaténant l'indice de segment avec les droits d'accès à ce segment. Si un programme veut accéder à une adresse, il fournit une capacité de la forme "sélecteur:droits d'accès", et un décalage qui indique la position de l'adresse dans le segment.
Il est impossible d'accéder à un segment sans avoir la capacité associée, c'est là une sécurité importante. Un accès mémoire demande que l'on ait la capacité pour sélectionner le bon segment, mais aussi que les droits d'accès en permettent l'accès demandé. Par contre, les capacités peuvent être passées d'un programme à un autre sans problème, les deux programmes pourront accéder à un segment tant qu'ils disposent de la capacité associée.
[[File:Comparaison entre capacités et adresses segmentées.png|centre|vignette|upright=2.5|Comparaison entre capacités et adresses segmentées]]
Mais cette solution a deux problèmes très liés. Au niveau des sélecteurs de segment, le problème est que les sélecteur ont une portée globale. Avant, l'indice de segment était interne à un programme, un sélecteur ne permettait pas d'accéder au segment d'un autre programme. Sur les architectures à capacité, les sélecteurs ont une portée globale. Si un programme arrive à forger un sélecteur qui pointe vers un segment d'un autre programme, il peut théoriquement y accéder, à condition que les droits d'accès le permettent. Et c'est là qu'intervient le second problème : les droits d'accès ne sont plus protégés par l'espace noyau. Les droits d'accès étaient dans la table de segment, accessible uniquement en espace noyau, ce qui empêchait un processus de les modifier. Avec une capacité, il faut ajouter des mécanismes de protection qui empêchent un programme de modifier les droits d'accès à un segment et de générer un indice de segment non-prévu.
La première sécurité est qu'un programme ne peut pas créer une capacité, seul le système d'exploitation le peut. Les capacités sont forgées lors de l'allocation mémoire, ce qui est du ressort de l'OS. Pour rappel, un programme qui veut du rab de mémoire RAM peut demander au système d'exploitation de lui allouer de la mémoire supplémentaire. Le système d'exploitation renvoie alors un pointeurs qui pointe vers un nouveau segment. Le pointeur est une capacité. Il doit être impossible de forger une capacité, en-dehors d'une demande d'allocation mémoire effectuée par l'OS. Typiquement, la forge d'une capacité se fait avec des instructions du processeur, que seul l'OS peut éxecuter (pensez à une instruction qui n'est accessible qu'en espace noyau).
La seconde protection est que les capacités ne peuvent pas être modifiées sans raison valable, que ce soit pour l'indice de segment ou les droits d'accès. L'indice de segment ne peut pas être modifié, quelqu'en soit la raison. Pour les droits d'accès, la situation est plus compliquée. Il est possible de modifier ses droits d'accès, mais sous conditions. Réduire les droits d'accès d'une capacité est possible, que ce soit en espace noyau ou utilisateur, pas l'OS ou un programme utilisateur, avec une instruction dédiée. Mais augmenter les droits d'accès, seul l'OS peut le faire avec une instruction précise, souvent exécutable seulement en espace noyau.
Les capacités peuvent être copiées, et même transférées d'un processus à un autre. Les capacités peuvent être détruites, ce qui permet de libérer la mémoire utilisée par un segment. La copie d'une capacité est contrôlée par l'OS et ne peut se faire que sous conditions. La destruction d'une capacité est par contre possible par tous les processus. La destruction ne signifie pas que le segment est effacé, il est possible que d'autres processus utilisent encore des copies de la capacité, et donc le segment associé. On verra quand la mémoire est libérée plus bas.
Protéger les capacités demande plusieurs conditions. Premièrement, le processeur doit faire la distinction entre une capacité et une donnée. Deuxièmement, les capacités ne peuvent être modifiées que par des instructions spécifiques, dont l'exécution est protégée, réservée au noyau. En clair, il doit y avoir une séparation matérielle des capacités, qui sont placées dans des registres séparés. Pour cela, deux solutions sont possibles : soit les capacités remplacent les adresses et sont dispersées en mémoire, soit elles sont regroupées dans un segment protégé.
====La liste des capacités====
Avec la première solution, on regroupe les capacités dans un segment protégé. Chaque programme a accès à un certain nombre de segments et à autant de capacités. Les capacités d'un programme sont souvent regroupées dans une '''liste de capacités''', appelée la '''''C-list'''''. Elle est généralement placée en mémoire RAM. Elle est ce qu'il reste de la table des segments du processus, sauf que cette table ne contient pas les adresses du segment, qui sont dans la table globale. Tout se passe comme si la table des segments de chaque processus est donc scindée en deux : la table globale partagée entre tous les processus contient les informations sur les limites des segments, la ''C-list'' mémorise les droits d'accès et les sélecteurs pour identifier chaque segment. C'est un niveau d'indirection supplémentaire par rapport à la segmentation usuelle.
[[File:Architectures à capacité.png|centre|vignette|upright=2|Architectures à capacité]]
La liste de capacité est lisible par le programme, qui peut copier librement les capacités dans les registres. Par contre, la liste des capacités est protégée en écriture. Pour le programme, il est impossible de modifier les capacités dedans, impossible d'en rajouter, d'en forger, d'en retirer. De même, il ne peut pas accéder aux segments des autres programmes : il n'a pas les capacités pour adresser ces segments.
Pour protéger la ''C-list'' en écriture, la solution la plus utilisée consiste à placer la ''C-list'' dans un segment dédié. Le processeur gère donc plusieurs types de segments : les segments de capacité pour les ''C-list'', les autres types segments pour le reste. Un défaut de cette approche est que les adresses/capacités sont séparées des données. Or, les programmeurs mixent souvent adresses et données, notamment quand ils doivent manipuler des structures de données comme des listes chainées, des arbres, des graphes, etc.
L'usage d'une ''C-list'' permet de se passer de la séparation entre espace noyau et utilisateur ! Les segments de capacité sont eux-mêmes adressés par leur propre capacité, avec une capacité par segment de capacité. Le programme a accès à la liste de capacité, comme l'OS, mais leurs droits d'accès ne sont pas les mêmes. Le programme a une capacité vers la ''C-list'' qui n'autorise pas l'écriture, l'OS a une autre capacité qui accepte l'écriture. Les programmes ne pourront pas forger les capacités permettant de modifier les segments de capacité. Une méthode alternative est de ne permettre l'accès aux segments de capacité qu'en espace noyau, mais elle est redondante avec la méthode précédente et moins puissante.
====Les capacités dispersées, les architectures taguées====
Une solution alternative laisse les capacités dispersées en mémoire. Les capacités remplacent les adresses/pointeurs, et elles se trouvent aux mêmes endroits : sur la pile, dans le tas. Comme c'est le cas dans les programmes modernes, chaque allocation mémoire renvoie une capacité, que le programme gére comme il veut. Il peut les mettre dans des structures de données, les placer sur la pile, dans des variables en mémoire, etc. Mais il faut alors distinguer si un mot mémoire contient une capacité ou une autre donnée, les deux ne devant pas être mixés.
Pour cela, chaque mot mémoire se voit attribuer un certain bit qui indique s'il s'agit d'un pointeur/capacité ou d'autre chose. Mais cela demande un support matériel, ce qui fait que le processeur devient ce qu'on appelle une ''architecture à tags'', ou ''tagged architectures''. Ici, elles indiquent si le mot mémoire contient une adresse:capacité ou une donnée.
[[File:Architectures à capacité sans liste de capacité.png|centre|vignette|upright=2|Architectures à capacité sans liste de capacité]]
L'inconvénient est le cout en matériel de cette solution. Il faut ajouter un bit à chaque case mémoire, le processeur doit vérifier les tags avant chaque opération d'accès mémoire, etc. De plus, tous les mots mémoire ont la même taille, ce qui force les capacités à avoir la même taille qu'un entier. Ce qui est compliqué.
===Les registres de capacité===
Les architectures à capacité disposent de registres spécialisés pour les capacités, séparés pour les entiers. La raison principale est une question de sécurité, mais aussi une solution pragmatique au fait que capacités et entiers n'ont pas la même taille. Les registres dédiés aux capacités ne mémorisent pas toujours des capacités proprement dites. À la place, ils mémorisent des descripteurs de segment, qui contiennent l'adresse de base, limite et les droits d'accès. Ils sont utilisés pour la relocation des accès mémoire ultérieurs. Ils sont en réalité identiques aux registres de relocation, voire aux registres de segments. Leur utilité est d'accélérer la relocation, entre autres.
Les processeurs à capacité ne gèrent pas d'adresses proprement dit, comme pour la segmentation avec plusieurs registres de relocation. Les accès mémoire doivent préciser deux choses : à quel segment on veut accéder, à quelle position dans le segment se trouve la donnée accédée. La première information se trouve dans le mal nommé "registre de capacité", la seconde information est fournie par l'instruction d'accès mémoire soit dans un registre (Base+Index), soit en adressage base+''offset''.
Les registres de capacités sont accessibles à travers des instructions spécialisées. Le processeur ajoute des instructions LOAD/STORE pour les échanges entre table des segments et registres de capacité. Ces instructions sont disponibles en espace utilisateur, pas seulement en espace noyau. Lors du chargement d'une capacité dans ces registres, le processeur vérifie que la capacité chargée est valide, et que les droits d'accès sont corrects. Puis, il accède à la table des segments, récupère les adresses de base et limite, et les mémorise dans le registre de capacité. Les droits d'accès et d'autres méta-données sont aussi mémorisées dans le registre de capacité. En somme, l'instruction de chargement prend une capacité et charge un descripteur de segment dans le registre.
Avec ce genre de mécanismes, il devient difficile d’exécuter certains types d'attaques, ce qui est un gage de sureté de fonctionnement indéniable. Du moins, c'est la théorie, car tout repose sur l'intégrité des listes de capacité. Si on peut modifier celles-ci, alors il devient facile de pouvoir accéder à des objets auxquels on n’aurait pas eu droit.
===Le recyclage de mémoire matériel===
Les architectures à capacité séparent les adresses/capacités des nombres entiers. Et cela facilite grandement l'implémentation de la ''garbage collection'', ou '''recyclage de la mémoire''', à savoir un ensemble de techniques logicielles qui visent à libérer la mémoire inutilisée.
Rappelons que les programmes peuvent demander à l'OS un rab de mémoire pour y placer quelque chose, généralement une structure de donnée ou un objet. Mais il arrive un moment où cet objet n'est plus utilisé par le programme. Il peut alors demander à l'OS de libérer la portion de mémoire réservée. Sur les architectures à capacité, cela revient à libérer un segment, devenu inutile. La mémoire utilisée par ce segment est alors considérée comme libre, et peut être utilisée pour autre chose. Mais il arrive que les programmes ne libèrent pas le segment en question. Soit parce que le programmeur a mal codé son programme, soit parce que le compilateur n'a pas fait du bon travail ou pour d'autres raisons.
Pour éviter cela, les langages de programmation actuels incorporent des '''''garbage collectors''''', des morceaux de code qui scannent la mémoire et détectent les segments inutiles. Pour cela, ils doivent identifier les adresses manipulées par le programme. Si une adresse pointe vers un objet, alors celui-ci est accessible, il sera potentiellement utilisé dans le futur. Mais si aucune adresse ne pointe vers l'objet, alors il est inaccessible et ne sera plus jamais utilisé dans le futur. On peut libérer les objets inaccessibles.
Identifier les adresses est cependant très compliqué sur les architectures normales. Sur les processeurs modernes, les ''garbage collectors'' scannent la pile à la recherche des adresses, et considèrent tout mot mémoire comme une adresse potentielle. Mais les architectures à capacité rendent le recyclage de la mémoire très facile. Un segment est accessible si le programme dispose d'une capacité qui pointe vers ce segment, rien de plus. Et les capacités sont facilement identifiables : soit elles sont dans la liste des capacités, soit on peut les identifier à partir de leur ''tag''.
Le recyclage de mémoire était parfois implémenté directement en matériel. En soi, son implémentation est assez simple, et peu être réalisé dans le microcode d'un processeur. Une autre solution consiste à utiliser un second processeur, spécialement dédié au recyclage de mémoire, qui exécute un programme spécialement codé pour. Le programme en question est placé dans une mémoire ROM, reliée directement à ce second processeur.
===L'intel iAPX 432===
Voyons maintenat une architecture à capacité assez connue : l'Intel iAPX 432. Oui, vous avez bien lu : Intel a bel et bien réalisé un processeur orienté objet dans sa jeunesse. La conception du processeur Intel iAPX 432 commença en 1975, afin de créer un successeur digne de ce nom aux processeurs 8008 et 8080.
La conception du processeur Intel iAPX 432 commença en 1975, afin de créer un successeur digne de ce nom aux processeurs 8008 et 8080. Ce processeur s'est très faiblement vendu en raison de ses performances assez désastreuses et de défauts techniques certains. Par exemple, ce processeur était une machine à pile à une époque où celles-ci étaient tombées en désuétude, il ne pouvait pas effectuer directement de calculs avec des constantes entières autres que 0 et 1, ses instructions avaient un alignement bizarre (elles étaient bit-alignées). Il avait été conçu pour maximiser la compatibilité avec le langage ADA, un langage assez peu utilisé, sans compter que le compilateur pour ce processeur était mauvais.
====Les segments prédéfinis de l'Intel iAPX 432====
L'Intel iAPX432 gère plusieurs types de segments. Rien d'étonnant à cela, les Burrough géraient eux aussi plusieurs types de segments, à savoir des segments de programmes, des segments de données, et des segments d'I/O. C'est la même chose sur l'Intel iAPX 432, mais en bien pire !
Les segments de données sont des segments génériques, dans lequels on peut mettre ce qu'on veut, suivant les besoins du programmeur. Ils sont tous découpés en deux parties de tailles égales : une partie contenant les données de l'objet et une partie pour les capacités. Les capacités d'un segment pointent vers d'autres segments, ce qui permet de créer des structures de données assez complexes. La ligne de démarcation peut être placée n'importe où dans le segment, les deux portions ne sont pas de taille identique, elles ont des tailles qui varient de segment en segment. Il est même possible de réserver le segment entier à des données sans y mettre de capacités, ou inversement. Les capacités et données sont adressées à partir de la ligne de démarcation, qui sert d'adresse de base du segment. Suivant l'instruction utilisée, le processeur accède à la bonne portion du segment.
Le processeur supporte aussi d'autres segments pré-définis, qui sont surtout utilisés par le système d'exploitation :
* Des segments d'instructions, qui contiennent du code exécutable, typiquement un programme ou des fonctions, parfois des ''threads''.
* Des segments de processus, qui mémorisent des processus entiers. Ces segments contiennent des capacités qui pointent vers d'autres segments, notamment un ou plusieurs segments de code, et des segments de données.
* Des segments de domaine, pour les modules ou bibliothèques dynamiques.
* Des segments de contexte, utilisés pour mémoriser l'état d'un processus, utilisés par l'OS pour faire de la commutation de contexte.
* Des segments de message, utilisés pour la communication entre processus par l'intermédiaire de messages.
* Et bien d'autres encores.
Sur l'Intel iAPX 432, chaque processus est considéré comme un objet à part entière, qui a son propre segment de processus. De même, l'état du processeur (le programme qu'il est en train d’exécuter, son état, etc.) est stocké en mémoire dans un segment de contexte. Il en est de même pour chaque fonction présente en mémoire : elle était encapsulée dans un segment, sur lequel seules quelques manipulations étaient possibles (l’exécuter, notamment). Et ne parlons pas des appels de fonctions qui stockaient l'état de l'appelé directement dans un objet spécial. Bref, de nombreux objets système sont prédéfinis par le processeur : les objets stockant des fonctions, les objets stockant des processus, etc.
L'Intel 432 possédait dans ses circuits un ''garbage collector'' matériel. Pour faciliter son fonctionnement, certains bits de l'objet permettaient de savoir si l'objet en question pouvait être supprimé ou non.
====Le support de la segmentation sur l'Intel iAPX 432====
La table des segments est une table hiérarchique, à deux niveaux. Le premier niveau est une ''Object Table Directory'', qui réside toujours en mémoire RAM. Elle contient des descripteurs qui pointent vers des tables secondaires, appelées des ''Object Table''. Il y a plusieurs ''Object Table'', typiquement une par processus. Plusieurs processus peuvent partager la même ''Object Table''. Les ''Object Table'' peuvent être swappées, mais pas l{{'}}''Object Table Directory''.
Une capacité tient compte de l'organisation hiérarchique de la table des segments. Elle contient un indice qui précise quelle ''Object Table'' utiliser, et l'indice du segment dans cette ''Object Table''. Le premier indice adresse l{{'}}''Object Table Directory'' et récupère un descripteur de segment qui pointe sur la bonne ''Object Table''. Le second indice est alors utilisé pour lire l'adresse de base adéquate dans cette ''Object Table''. La capacité contient aussi des droits d'accès en lecture, écriture, suppression et copie. Il y a aussi un champ pour le type, qu'on verra plus bas. Au fait : les capacités étaient appelées des ''Access Descriptors'' dans la documentation officielle.
Une capacité fait 32 bits, avec un octet utilisé pour les droits d'accès, laissant 24 bits pour adresser les segments. Le processeur gérait jusqu'à 2^24 segments/objets différents, pouvant mesurer jusqu'à 64 kibioctets chacun, ce qui fait 2^40 adresses différentes, soit 1024 gibioctets. Les 24 bits pour adresser les segments sont partagés moitié-moitié pour l'adressage des tables, ce qui fait 4096 ''Object Table'' différentes dans l{{'}}''Object Table Directory'', et chaque ''Object Table'' contient 4096 segments.
====Le jeu d'instruction de l'Intel iAPX 432====
L'Intel iAPX 432 est une machine à pile. Le jeu d'instruction de l'Intel iAPX 432 gère pas moins de 230 instructions différentes. Il gére deux types d'instructions : les instructions normales, et celles qui manipulent des segments/objets. Les premières permettent de manipuler des nombres entiers, des caractères, des chaînes de caractères, des tableaux, etc.
Les secondes sont spécialement dédiées à la manipulation des capacités. Il y a une instruction pour copier une capacité, une autre pour invalider une capacité, une autre pour augmenter ses droits d'accès (instruction sécurisée, exécutable seulement sous certaines conditions), une autre pour restreindre ses droits d'accès. deux autres instructions créent un segment et renvoient la capacité associée, la première créant un segment typé, l'autre non.
le processeur gérait aussi des instructions spécialement dédiées à la programmation système et idéales pour programmer des systèmes d'exploitation. De nombreuses instructions permettaient ainsi de commuter des processus, faire des transferts de messages entre processus, etc. Environ 40 % du micro-code était ainsi spécialement dédié à ces instructions spéciales.
Les instructions sont de longueur variable et peuvent prendre n'importe quelle taille comprise entre 10 et 300 bits, sans vraiment de restriction de taille. Les bits d'une instruction sont regroupés en 4 grands blocs, 4 champs, qui ont chacun une signification particulière.
* Le premier est l'opcode de l'instruction.
* Le champ référence, doit être interprété différemment suivant la donnée à manipuler. Si cette donnée est un entier, un caractère ou un flottant, ce champ indique l'emplacement de la donnée en mémoire. Alors que si l'instruction manipule un objet, ce champ spécifie la capacité de l'objet en question. Ce champ est assez complexe et il est sacrément bien organisé.
* Le champ format, n'utilise que 4 bits et a pour but de préciser si les données à manipuler sont en mémoire ou sur la pile.
* Le champ classe permet de dire combien de données différentes l'instruction va devoir manipuler, et quelles seront leurs tailles.
[[File:Encodage des instructions de l'Intel iAPX-432.png|centre|vignette|upright=2|Encodage des instructions de l'Intel iAPX-432.]]
====Le support de l'orienté objet sur l'Intel iAPX 432====
L'Intel 432 permet de définir des objets, qui correspondent aux classes des langages orientés objets. L'Intel 432 permet, à partir de fonctions définies par le programmeur, de créer des '''''domain objects''''', qui correspondent à une classe. Un ''domain object'' est un segment de capacité, dont les capacités pointent vers des fonctions ou un/plusieurs objets. Les fonctions et les objets sont chacun placés dans un segment. Une partie des fonctions/objets sont publics, ce qui signifie qu'ils sont accessibles en lecture par l'extérieur. Les autres sont privées, inaccessibles aussi bien en lecture qu'en écriture.
L'exécution d'une fonction demande que le branchement fournisse deux choses : une capacité vers le ''domain object'', et la position de la fonction à exécuter dans le segment. La position permet de localiser la capacité de la fonction à exécuter. En clair, on accède au ''domain object'' d'abord, pour récupérer la capacité qui pointe vers la fonction à exécuter.
Il est aussi possible pour le programmeur de définir de nouveaux types non supportés par le processeur, en faisant appel au système d'exploitation de l'ordinateur. Au niveau du processeur, chaque objet est typé au niveau de son object descriptor : celui-ci contient des informations qui permettent de déterminer le type de l'objet. Chaque type se voit attribuer un domain object qui contient toutes les fonctions capables de manipuler les objets de ce type et que l'on appelle le type manager. Lorsque l'on veut manipuler un objet d'un certain type, il suffit d'accéder à une capacité spéciale (le TCO) qui pointera dans ce type manager et qui précisera quel est l'objet à manipuler (en sélectionnant la bonne entrée dans la liste de capacité). Le type d'un objet prédéfini par le processeur est ainsi spécifié par une suite de 8 bits, tandis que le type d'un objet défini par le programmeur est défini par la capacité spéciale pointant vers son type manager.
===Conclusion===
Pour ceux qui veulent en savoir plus, je conseille la lecture de ce livre, disponible gratuitement sur internet (merci à l'auteur pour cette mise à disposition) :
* [https://homes.cs.washington.edu/~levy/capabook/ Capability-Based Computer Systems].
Voici un document qui décrit le fonctionnement de l'Intel iAPX432 :
* [https://homes.cs.washington.edu/~levy/capabook/Chapter9.pdf The Intel iAPX 432 ]
==La pagination==
Avec la pagination, la mémoire est découpée en blocs de taille fixe, appelés des '''pages mémoires'''. La taille des pages varie suivant le processeur et le système d'exploitation et tourne souvent autour de 4 kibioctets. Mais elles sont de taille fixe : on ne peut pas en changer la taille. C'est la différence avec les segments, qui sont de taille variable. Le contenu d'une page en mémoire fictive est rigoureusement le même que le contenu de la page correspondante en mémoire physique.
L'espace d'adressage est découpé en '''pages logiques''', alors que la mémoire physique est découpée en '''pages physique''' de même taille. Les pages logiques correspondent soit à une page physique, soit à une page swappée sur le disque dur. Quand une page logique est associée à une page physique, les deux ont le même contenu, mais pas les mêmes adresses. Les pages logiques sont numérotées, en partant de 0, afin de pouvoir les identifier/sélectionner. Même chose pour les pages physiques, qui sont elles aussi numérotées en partant de 0.
[[File:Principe de la pagination.png|centre|vignette|upright=2|Principe de la pagination.]]
Pour information, le tout premier processeur avec un système de mémoire virtuelle était le super-ordinateur Atlas. Il utilisait la pagination, et non la segmentation. Mais il fallu du temps avant que la méthode de la pagination prenne son essor dans les processeurs commerciaux x86.
Un point important est que la pagination implique une coopération entre OS et hardware, les deux étant fortement mélés. Une partie des informations de cette section auraient tout autant leur place dans le wikilivre sur les systèmes d'exploitation, mais il est plus simple d'en parler ici.
===La mémoire virtuelle : le ''swapping'' et le remplacement des pages mémoires===
Le système d'exploitation mémorise des informations sur toutes les pages existantes dans une '''table des pages'''. C'est un tableau où chaque ligne est associée à une page logique. Une ligne contient un bit ''Valid'' qui indique si la page logique associée est swappée sur le disque dur ou non, et la position de la page physique correspondante en mémoire RAM. Elle peut aussi contenir des bits pour la protection mémoire, et bien d'autres. Les lignes sont aussi appelées des ''entrées de la table des pages''
[[File:Gestionnaire de mémoire virtuelle - Pagination et swapping.png|centre|vignette|upright=2|Table des pages.]]
De plus, le système d'exploitation conserve une '''liste des pages vides'''. Le nom est assez clair : c'est une liste de toutes les pages de la mémoire physique qui sont inutilisées, qui ne sont allouées à aucun processus. Ces pages sont de la mémoire libre, utilisable à volonté. La liste des pages vides est mise à jour à chaque fois qu'un programme réserve de la mémoire, des pages sont alors prises dans cette liste et sont allouées au programme demandeur.
====Les défauts de page====
Lorsque l'on veut traduire l'adresse logique d'une page mémoire, le processeur vérifie le bit ''Valid'' et l'adresse physique. Si le bit ''Valid'' est à 1 et que l'adresse physique est présente, la traduction d'adresse s'effectue normalement. Mais si ce n'est pas le cas, l'entrée de la table des pages ne contient pas de quoi faire la traduction d'adresse. Soit parce que la page est swappée sur le disque dur et qu'il faut la copier en RAM, soit parce que les droits d'accès ne le permettent pas, soit parce que la page n'a pas encore été allouée, etc. On fait alors face à un '''défaut de page'''. Un défaut de page a lieu quand la MMU ne peut pas associer l'adresse logique à une adresse physique, quelque qu'en soit la raison.
Il existe deux types de défauts de page : mineurs et majeurs. Un '''défaut de page majeur''' a lieu quand on veut accéder à une page déplacée sur le disque dur. Un défaut de page majeur lève une exception matérielle dont la routine rapatriera la page en mémoire RAM. S'il y a de la place en mémoire RAM, il suffit d'allouer une page vide et d'y copier la page chargée depuis le disque dur. Mais si ce n'est par le cas, on va devoir faire de la place en RAM en déplaçant une page mémoire de la RAM vers le disque dur. Dans tous les cas, c'est le système d'exploitation qui s'occupe du chargement de la page, le processeur n'est pas impliqué. Une fois la page chargée, la table des pages est mise à jour et la traduction d'adresse peut recommencer. Si je dis recommencer, c'est car l'accès mémoire initial est rejoué à l'identique, sauf que la traduction d'adresse réussit cette fois-ci.
Un '''défaut de page mineur''' a lieu dans des circonstances pas très intuitives : la page est en mémoire physique, mais l'adresse physique de la page n'est pas accessible. Par exemple, il est possible que des sécurités empêchent de faire la traduction d'adresse, pour des raisons de protection mémoire. Une autre raison est la gestion des adresses synonymes, qui surviennent quand on utilise des libraires partagées entre programmes, de la communication inter-processus, des optimisations de type ''copy-on-write'', etc. Enfin, une dernière raison est que la page a été allouée à un programme par le système d'exploitation, mais qu'il n'a pas encore attribué sa position en mémoire. Pour comprendre comment c'est possible, parlons rapidement de l'allocation paresseuse.
Imaginons qu'un programme fasse une demande d'allocation mémoire et se voit donc attribuer une ou plusieurs pages logiques. L'OS peut alors réagir de deux manières différentes. La première est d'attribuer une page physique immédiatement, en même temps que la page logique. En faisant ainsi, on ne peut pas avoir de défaut mineur, sauf en cas de problème de protection mémoire. Cette solution est simple, on l'appelle l{{'}}'''allocation immédiate'''. Une autre solution consiste à attribuer une page logique, mais l'allocation de la page physique se fait plus tard. Elle a lieu la première fois que le programme tente d'écrire/lire dans la page physique. Un défaut mineur a lieu, et c'est lui qui force l'OS à attribuer une page physique pour la page logique demandée. On parle alors d{{'}}'''allocation paresseuse'''. L'avantage est que l'on gagne en performance si des pages logiques sont allouées mais utilisées, ce qui peut arriver.
Une optimisation permise par l'existence des défauts mineurs est le '''''copy-on-write'''''. Le but est d'optimiser la copie d'une page logique dans une autre. L'idée est que la copie est retardée quand elle est vraiment nécessaire, à savoir quand on écrit dans la copie. Tant que l'on ne modifie pas la copie, les deux pages logiques, originelle et copiée, pointent vers la même page physique. A quoi bon avoir deux copies avec le même contenu ? Par contre, la page physique est marquée en lecture seule. La moindre écriture déclenche une erreur de protection mémoire, et un défaut mineur. Celui-ci est géré par l'OS, qui effectue alors la copie dans une nouvelle page physique.
Je viens de dire que le système d'exploitation gère les défauts de page majeurs/mineurs. Un défaut de page déclenche une exception matérielle, qui passe la main au système d'exploitation. Le système d'exploitation doit alors déterminer ce qui a levé l'exception, notamment identifier si c'est un défaut de page mineur ou majeur. Pour cela, le processeur a un ou plusieurs '''registres de statut''' qui indique l'état du processeur, qui sont utiles pour gérer les défauts de page. Ils indiquent quelle est l'adresse fautive, si l'accès était une lecture ou écriture, si l'accès a eu lieu en espace noyau ou utilisateur (les espaces mémoire ne sont pas les mêmes), etc. Les registres en question varient grandement d'une architecture de processeur à l'autre, aussi on ne peut pas dire grand chose de plus sur le sujet. Le reste est de toute façon à voir dans un cours sur les systèmes d'exploitation.
====Le remplacement des pages====
Les pages virtuelles font référence soit à une page en mémoire physique, soit à une page sur le disque dur. Mais l'on ne peut pas lire une page directement depuis le disque dur. Les pages sur le disque dur doivent être chargées en RAM, avant d'être utilisables. Ce n'est possible que si on a une page mémoire vide, libre. Si ce n'est pas le cas, on doit faire de la place en swappant une page sur le disque dur. Les pages font ainsi une sorte de va et vient entre le fichier d'échange et la RAM, suivant les besoins. Tout cela est effectué par une routine d'interruption du système d'exploitation, le processeur n'ayant pas vraiment de rôle là-dedans.
Supposons que l'on veuille faire de la place en RAM pour une nouvelle page. Dans une implémentation naïve, on trouve une page à évincer de la mémoire, qui est copiée dans le ''swapfile''. Toutes les pages évincées sont alors copiées sur le disque dur, à chaque remplacement. Néanmoins, cette implémentation naïve peut cependant être améliorée si on tient compte d'un point important : si la page a été modifiée depuis le dernier accès. Si le programme/processeur a écrit dans la page, alors celle-ci a été modifiée et doit être sauvegardée sur le ''swapfile'' si elle est évincée. Par contre, si ce n'est pas le cas, la page est soit initialisée, soit déjà présente à l'identique dans le ''swapfile''.
Mais cette optimisation demande de savoir si une écriture a eu lieu dans la page. Pour cela, on ajoute un '''''dirty bit''''' à chaque entrée de la table des pages, juste à côté du bit ''Valid''. Il indique si une écriture a eu lieu dans la page depuis qu'elle a été chargée en RAM. Ce bit est mis à jour par le processeur, automatiquement, lors d'une écriture. Par contre, il est remis à zéro par le système d'exploitation, quand la page est chargée en RAM. Si le programme se voit allouer de la mémoire, il reçoit une page vide, et ce bit est initialisé à 0. Il est mis à 1 si la mémoire est utilisée. Quand la page est ensuite swappée sur le disque dur, ce bit est remis à 0 après la sauvegarde.
Sur la majorité des systèmes d'exploitation, il est possible d'interdire le déplacement de certaines pages sur le disque dur. Ces pages restent alors en mémoire RAM durant un temps plus ou moins long, parfois en permanence. Cette possibilité simplifie la vie des programmeurs qui conçoivent des systèmes d'exploitation : essayez d'exécuter l'interruption pour les défauts de page alors que la page contenant le code de l'interruption est placée sur le disque dur ! Là encore, cela demande d'ajouter un bit dans chaque entrée de la table des pages, qui indique si la page est swappable ou non. Le bit en question s'appelle souvent le '''bit ''swappable'''''.
====Les algorithmes de remplacement des pages pris en charge par l'OS====
Le choix de la page doit être fait avec le plus grand soin et il existe différents algorithmes qui permettent de décider quelle page supprimer de la RAM. Leur but est de swapper des pages qui ne seront pas accédées dans le futur, pour éviter d'avoir à faire triop de va-et-vient entre RAM et ''swapfile''. Les données qui sont censées être accédées dans le futur doivent rester en RAM et ne pas être swappées, autant que possible. Les algorithmes les plus simples pour le choix de page à évincer sont les suivants.
Le plus simple est un algorithme aléatoire : on choisit la page au hasard. Mine de rien, cet algorithme est très simple à implémenter et très rapide à exécuter. Il ne demande pas de modifier la table des pages, ni même d'accéder à celle-ci pour faire son choix. Ses performances sont surprenamment correctes, bien que largement en-dessous de tous les autres algorithmes.
L'algorithme FIFO supprime la donnée qui a été chargée dans la mémoire avant toutes les autres. Cet algorithme fonctionne bien quand un programme manipule des tableaux de grande taille, mais fonctionne assez mal dans le cas général.
L'algorithme LRU supprime la donnée qui été lue ou écrite pour la dernière fois avant toutes les autres. C'est théoriquement le plus efficace dans la majorité des situations. Malheureusement, son implémentation est assez complexe et les OS doivent modifier la table des pages pour l'implémenter.
L'algorithme le plus utilisé de nos jours est l{{'}}'''algorithme NRU''' (''Not Recently Used''), une simplification drastique du LRU. Il fait la différence entre les pages accédées il y a longtemps et celles accédées récemment, d'une manière très binaire. Les deux types de page sont appelés respectivement les '''pages froides''' et les '''pages chaudes'''. L'OS swappe en priorité les pages froides et ne swappe de page chaude que si aucune page froide n'est présente. L'algorithme est simple : il choisit la page à évincer au hasard parmi une page froide. Si aucune page froide n'est présente, alors il swappe au hasard une page chaude.
Pour implémenter l'algorithme NRU, l'OS mémorise, dans chaque entrée de la table des pages, si la page associée est froide ou chaude. Pour cela, il met à 0 ou 1 un bit dédié : le '''bit ''Accessed'''''. La différence avec le bit ''dirty'' est que le bit ''dirty'' est mis à jour uniquement lors des écritures, alors que le bit ''Accessed'' l'est aussi lors d'une lecture. Uen lecture met à 1 le bit ''Accessed'', mais ne touche pas au bit ''dirty''. Les écritures mettent les deux bits à 1.
Implémenter l'algorithme NRU demande juste de mettre à jour le bit ''Accessed'' de chaque entrée de la table des pages. Et sur les architectures modernes, le processeur s'en charge automatiquement. A chaque accès mémoire, que ce soit en lecture ou en écriture, le processeur met à 1 ce bit. Par contre, le système d'exploitation le met à 0 à intervalles réguliers. En conséquence, quand un remplacement de page doit avoir lieu, les pages chaudes ont de bonnes chances d'avoir le bit ''Accessed'' à 1, alors que les pages froides l'ont à 0. Ce n'est pas certain, et on peut se trouver dans des cas où ce n'est pas le cas. Par exemple, si un remplacement a lieu juste après la remise à zéro des bits ''Accessed''. Le choix de la page à remplacer est donc imparfait, mais fonctionne bien en pratique.
Tous les algorithmes précédents ont chacun deux variantes : une locale, et une globale. Avec la version locale, la page qui va être rapatriée sur le disque dur est une page réservée au programme qui est la cause du page miss. Avec la version globale, le système d'exploitation va choisir la page à virer parmi toutes les pages présentes en mémoire vive.
===La protection mémoire avec la pagination===
Avec la pagination, chaque page a des '''droits d'accès''' précis, qui permettent d'autoriser ou interdire les accès en lecture, écriture, exécution, etc. La table des pages mémorise les autorisations pour chaque page, sous la forme d'une suite de bits où chaque bit autorise/interdit une opération bien précise. En pratique, les tables de pages modernes disposent de trois bits : un qui autorise/interdit les accès en lecture, un qui autorise/interdit les accès en écriture, un qui autorise/interdit l'éxecution du contenu de la page.
Le format exact de la suite de bits a cependant changé dans le temps sur les processeurs x86 modernes. Par exemple, avant le passage au 64 bits, les CPU et OS ne pouvaient pas marquer une page mémoire comme non-exécutable. C'est seulement avec le passage au 64 bits qu'a été ajouté un bit pour interdire l'exécution de code depuis une page. Ce bit, nommé '''bit NX''', est à 0 si la page n'est pas exécutable et à 1 sinon. Le processeur vérifie à chaque chargement d'instruction si le bit NX de page lue est à 1. Sinon, il lève une exception matérielle et laisse la main à l'OS.
Une amélioration de cette protection est la technique dite du '''''Write XOR Execute''''', abréviée WxX. Elle consiste à interdire les pages d'être à la fois accessibles en écriture et exécutables. Il est possible de changer les autorisations en cours de route, ceci dit.
Les premiers IBM 360 disposaient d'un mécanisme de protection mémoire totalement différent, sans registres limite/base. Ce mécanisme de protection attribue à chaque programme une '''clé de protection''', qui consiste en un nombre unique de 4 bits (chaque programme a donc une clé différente de ses collègues). La mémoire est fragmentée en blocs de même taille, de 2 kibioctets. Le processeur mémorise, pour chacun de ses blocs, la clé de protection du programme qui a réservé ce bloc. À chaque accès mémoire, le processeur compare la clé de protection du programme en cours d’exécution et celle du bloc de mémoire de destination. Si les deux clés sont différentes, alors un programme a effectué un accès hors des clous et il se fait sauvagement arrêter.
===La traduction d'adresse avec la pagination===
Comme dit plus haut, les pages sont numérotées, de 0 à une valeur maximale, afin de les identifier. Le numéro en question est appelé le '''numéro de page'''. Il est utilisé pour dire au processeur : je veux lire une donnée dans la page numéro 20, la page numéro 90, etc. Une fois qu'on a le numéro de page, on doit alors préciser la position de la donnée dans la page, appelé le '''décalage''', ou encore l{{'}}''offset''.
Le numéro de page et le décalage se déduisent à partir de l'adresse, en divisant l'adresse par la taille de la page. Le quotient obtenu donne le numéro de la page, alors que le reste est le décalage. Les processeurs actuels utilisent tous des pages dont la taille est une puissance de deux, ce qui fait que ce calcul est fortement simplifié. Sous cette condition, le numéro de page correspond aux bits de poids fort de l'adresse, alors que le décalage est dans les bits de poids faible.
Le numéro de page existe en deux versions : un numéro de page physique qui identifie une page en mémoire physique, et un numéro de page logique qui identifie une page dans la mémoire virtuelle. Traduire l'adresse logique en adresse physique demande de remplacer le numéro de la page logique en un numéro de page physique.
[[File:Phycical address.JPG|centre|vignette|upright=2|Traduction d'adresse avec la pagination.]]
====Les tables des pages simples====
Dans le cas le plus simple, il n'y a qu'une seule table des pages, qui est adressée par les numéros de page logique. La table des pages est un vulgaire tableau d'adresses physiques, placées les unes à la suite des autres. Avec cette méthode, la table des pages a autant d'entrée qu'il y a de pages logiques en mémoire virtuelle. Accéder à la mémoire nécessite donc d’accéder d'abord à la table des pages en mémoire, de calculer l'adresse de l'entrée voulue, et d’y accéder.
[[File:Table des pages.png|centre|vignette|upright=2|Table des pages.]]
La table des pages est souvent stockée dans la mémoire RAM, son adresse est connue du processeur, mémorisée dans un registre spécialisé du processeur. Le processeur effectue automatiquement le calcul d'adresse à partir de l'adresse de base et du numéro de page logique.
[[File:Address translation (32-bit).png|centre|vignette|upright=2|Address translation (32-bit)]]
====Les tables des pages inversées====
Sur certains systèmes, notamment sur les architectures 64 bits ou plus, le nombre de pages est très important. Sur les ordinateurs x86 récents, les adresses sont en pratique de 48 bits, les bits de poids fort étant ignorés en pratique, ce qui fait en tout 68 719 476 736 pages. Chaque entrée de la table des pages fait au minimum 48 bits, mais fait plus en pratique : partons sur 64 bits par entrée, soit 8 octets. Cela fait 549 755 813 888 octets pour la table des pages, soit plusieurs centaines de gibioctets ! Une table des pages normale serait tout simplement impraticable.
Pour résoudre ce problème, on a inventé les '''tables des pages inversées'''. L'idée derrière celles-ci est l'inverse de la méthode précédente. La méthode précédente stocke, pour chaque page logique, son numéro de page physique. Les tables des pages inversées font l'inverse : elles stockent, pour chaque numéro de page physique, la page logique qui correspond. Avec cette méthode table des pages contient ainsi autant d'entrées qu'il y a de pages physiques. Elle est donc plus petite qu'avant, vu que la mémoire physique est plus petite que la mémoire virtuelle.
Quand le processeur veut convertir une adresse virtuelle en adresse physique, la MMU recherche le numéro de page de l'adresse virtuelle dans la table des pages. Le numéro de l'entrée à laquelle se trouve ce morceau d'adresse virtuelle est le morceau de l'adresse physique. Pour faciliter le processus de recherche dans la page, la table des pages inversée est ce que l'on appelle une table de hachage. C'est cette solution qui est utilisée sur les processeurs Power PC.
[[File:Table des pages inversée.jpg|centre|vignette|upright=2|Table des pages inversée.]]
====Les tables des pages multiples par espace d'adressage====
Dans les deux cas précédents, il y a une table des pages unique. Cependant, les concepteurs de processeurs et de systèmes d'exploitation ont remarqué que les adresses les plus hautes et/ou les plus basses sont les plus utilisées, alors que les adresses situées au milieu de l'espace d'adressage sont peu utilisées en raison du fonctionnement de la pile et du tas. Il y a donc une partie de la table des pages qui ne sert à rien et est utilisé pour des adresses inutilisées. C'est une source d'économie d'autant plus importante que les tables des pages sont de plus en plus grosses.
Pour profiter de cette observation, les concepteurs d'OS ont décidé de découper l'espace d'adressage en plusieurs sous-espaces d'adressage de taille identique : certains localisés dans les adresses basses, d'autres au milieu, d'autres tout en haut, etc. Et vu que l'espace d'adressage est scindé en plusieurs parties, la table des pages l'est aussi, elle est découpée en plusieurs sous-tables. Si un sous-espace d'adressage n'est pas utilisé, il n'y a pas besoin d'utiliser de la mémoire pour stocker la table des pages associée. On ne stocke que les tables des pages pour les espaces d'adressage utilisés, ceux qui contiennent au moins une donnée.
L'utilisation de plusieurs tables des pages ne fonctionne que si le système d'exploitation connaît l'adresse de chaque table des pages (celle de la première entrée). Pour cela, le système d'exploitation utilise une super-table des pages, qui stocke les adresses de début des sous-tables de chaque sous-espace. En clair, la table des pages est organisé en deux niveaux, la super-table étant le premier niveau et les sous-tables étant le second niveau.
L'adresse est structurée de manière à tirer profit de cette organisation. Les bits de poids fort de l'adresse sélectionnent quelle table de second niveau utiliser, les bits du milieu de l'adresse sélectionne la page dans la table de second niveau et le reste est interprété comme un ''offset''. Un accès à la table des pages se fait comme suit. Les bits de poids fort de l'adresse sont envoyés à la table de premier niveau, et sont utilisés pour récupérer l'adresse de la table de second niveau adéquate. Les bits au milieu de l'adresse sont envoyés à la table de second niveau, pour récupérer le numéro de page physique. Le tout est combiné avec l{{'}}''offset'' pour obtenir l'adresse physique finale.
[[File:Table des pages hiérarchique.png|centre|vignette|upright=2|Table des pages hiérarchique.]]
On peut aussi aller plus loin et découper la table des pages de manière hiérarchique, chaque sous-espace d'adressage étant lui aussi découpé en sous-espaces d'adressages. On a alors une table de premier niveau, plusieurs tables de second niveau, encore plus de tables de troisième niveau, et ainsi de suite. Cela peut aller jusqu'à 5 niveaux sur les processeurs x86 64 bits modernes. On parle alors de '''tables des pages emboitées'''. Dans ce cours, la table des pages désigne l'ensemble des différents niveaux de cette organisation, toutes les tables inclus. Seules les tables du dernier niveau mémorisent des numéros de page physiques, les autres tables mémorisant des pointeurs, des adresses vers le début des tables de niveau inférieur. Un exemple sera donné plus bas, dans la section suivante.
====L'exemple des processeurs x86====
Pour rendre les explications précédentes plus concrètes, nous allons prendre l'exemple des processeur x86 anciens, de type 32 bits. Les processeurs de ce type utilisaient deux types de tables des pages : une table des page unique et une table des page hiérarchique. Les deux étaient utilisées dans cas séparés. La table des page unique était utilisée pour les pages larges et encore seulement en l'absence de la technologie ''physical adress extension'', dont on parlera plus bas. Les autres cas utilisaient une table des page hiérarchique, à deux niveaux, trois niveaux, voire plus.
Une table des pages unique était utilisée pour les pages larges (de 2 mébioctets et plus). Pour les pages de 4 mébioctets, il y avait une unique table des pages, adressée par les 10 bits de poids fort de l'adresse, les bits restants servant comme ''offset''. La table des pages contenait 1024 entrées de 4 octets chacune, ce qui fait en tout 4 kibioctet pour la table des pages. La table des page était alignée en mémoire sur un bloc de 4 kibioctet (sa taille).
[[File:X86 Paging 4M.svg|centre|vignette|upright=2|X86 Paging 4M]]
Pour les pages de 4 kibioctets, les processeurs x86-32 bits utilisaient une table des page hiérarchique à deux niveaux. Les 10 bits de poids fort l'adresse adressaient la table des page maitre, appelée le directoire des pages (''page directory''), les 10 bits précédents servaient de numéro de page logique, et les 12 bits restants servaient à indiquer la position de l'octet dans la table des pages. Les entrées de chaque table des pages, mineure ou majeure, faisaient 32 bits, soit 4 octets. Vous remarquerez que la table des page majeure a la même taille que la table des page unique obtenue avec des pages larges (de 4 mébioctets).
[[File:X86 Paging 4K.svg|centre|vignette|upright=2|X86 Paging 4K]]
La technique du '''''physical adress extension''''' (PAE), utilisée depuis le Pentium Pro, permettait aux processeurs x86 32 bits d'adresser plus de 4 gibioctets de mémoire, en utilisant des adresses physiques de 64 bits. Les adresses virtuelles de 32 bits étaient traduites en adresses physiques de 64 bits grâce à une table des pages adaptée. Cette technologie permettait d'adresser plus de 4 gibioctets de mémoire au total, mais avec quelques limitations. Notamment, chaque programme ne pouvait utiliser que 4 gibioctets de mémoire RAM pour lui seul. Mais en lançant plusieurs programmes, on pouvait dépasser les 4 gibioctets au total. Pour cela, les entrées de la table des pages passaient à 64 bits au lieu de 32 auparavant.
La table des pages gardait 2 niveaux pour les pages larges en PAE.
[[File:X86 Paging PAE 2M.svg|centre|vignette|upright=2|X86 Paging PAE 2M]]
Par contre, pour les pages de 4 kibioctets en PAE, elle était modifiée de manière à ajouter un niveau de hiérarchie, passant de deux niveaux à trois.
[[File:X86 Paging PAE 4K.svg|centre|vignette|upright=2|X86 Paging PAE 4K]]
En 64 bits, la table des pages est une table des page hiérarchique avec 5 niveaux. Seuls les 48 bits de poids faible des adresses sont utilisés, les 16 restants étant ignorés.
[[File:X86 Paging 64bit.svg|centre|vignette|upright=2|X86 Paging 64bit]]
===La TLB et les caches associés===
En théorie, la table des pages est censée être accédée à chaque accès mémoire. Mais pour éviter d'avoir à lire la table des pages en mémoire RAM à chaque accès mémoire, les concepteurs de processeurs ont décidé d'implanter un cache dédié, le '''''translation lookaside buffer''''', ou TLB. Le TLB stocke au minimum de quoi faire la traduction entre adresse virtuelle et adresse physique, à savoir une correspondance entre numéro de page logique et numéro de page physique. Pour faire plus général, il stocke des entrées de la table des pages.
[[File:MMU principle updated.png|centre|vignette|upright=2.0|MMU avec une TLB.]]
Les accès à la table des pages sont gérés de deux façons : soit le processeur gère tout seul la situation, soit il délègue cette tâche au système d’exploitation. Sur les processeurs anciens, le système d'exploitation gère le parcours de la table des pages. Mais cette solution logicielle n'a pas de bonnes performances. D'autres processeurs gèrent eux-mêmes le défaut d'accès à la TLB et vont chercher d'eux-mêmes les informations nécessaires dans la table des pages. Ils disposent de circuits, les '''''page table walkers''''' (PTW), qui s'occupent eux-mêmes du défaut.
Les ''page table walkers'' contiennent des registres qui leur permettent de faire leur travail. Le plus important est celui qui mémorise la position de la table des pages en mémoire RAM, dont nous avons parlé plus haut. Les PTW ont besoin, pour faire leur travail, de mémoriser l'adresse physique de la table des pages, ou du moins l'adresse de la table des pages de niveau 1 pour des tables des pages hiérarchiques. Mais d'autres registres existent. Toutes les informations nécessaires pour gérer les défauts de TLB sont stockées dans des registres spécialisés appelés des '''tampons de PTW''' (PTW buffers).
===L'abstraction matérielle des processus===
[[File:Memoire virtuelle.svg|vignette|Mémoire virtuelle]]
Il est possible d'implémenter l'abstraction matérielle des processus avec la pagination. En clair, chaque programme lancé sur l'ordinateur dispose de son propre espace d'adressage, ce qui fait que la même adresse logique ne pointera pas sur la même adresse physique dans deux programmes différents. Pour cela, il y a plusieurs méthodes.
La première solution n'utilise qu'une seule table des pages. Pour cela, chaque entrée indique à quel processus elle est associée. Pour cela, chaque entrée contient un '''identifiant de processus''', un numéro qui précise pour quel processus, pour quel espace d'adressage, la correspondance est valide. La page des tables peut aussi contenir des entrées qui sont valides pour tous les processus en même temps. Pour cela, l'entrée contient un '''bit ''global''''', qui précise que cette correspondance est valide pour tous les processus, l'identifiant de processus est ignoré. Un défaut est que le partage d'une page entre plusieurs processus est impossible : soit on partage une page avec tous les processus, soit on l'alloue avec un seul processus.
L'intérêt de partager des pages entre tous les processus n'est pas évident, mais il le devient quand on se rappelle que le noyau de l'OS est mappé dans le haut de l'espace d'adressage. Et peu importe l'espace d'adressage, le noyau est toujours mappé de manière identique, les mêmes adresses logiques adressant la même adresse mémoire. En conséquence, les correspondances adresse physique-logique sont les mêmes pour le noyau, peu importe l'espace d'adressage. Le bit global accélère rapidement la traduction d'adresse pour l'accès au noyau.
Une solution alternative, plus simple, utilise une table des pages par processus lancé sur l'ordinateur, une table des pages unique par espace d'adressage. À chaque changement de processus, le registre qui mémorise la position de la table des pages est modifié pour pointer sur la bonne. C'est le système d'exploitation qui se charge de cette mise à jour.
Avec cette méthode, il est possible de partager une ou plusieurs pages entre plusieurs processus, en configurant les tables des pages convenablement. Les pages partagées sont mappées dans l'espace d'adressage de plusieurs processus, mais pas forcément au même endroit, pas forcément dans les mêmes adresses logiques. On peut placer la page partagée à l'adresse logique 0x0FFF pour un processus, à l'adresse logique 0xFF00 pour un autre processus, etc. Par contre, les entrées de la table des pages pour ces adresses pointent vers la même adresse physique.
[[File:Vm5.png|centre|vignette|upright=2|Tables des pages de plusieurs processus.]]
===La taille des pages===
La taille des pages varie suivant le processeur et le système d'exploitation et tourne souvent autour de 4 kibioctets. Les processeurs actuels gèrent plusieurs tailles différentes pour les pages : 4 kibioctets par défaut, 2 mébioctets, voire 1 à 4 gibioctets pour les pages les plus larges. Les pages de 4 kibioctets sont les pages par défaut, les autres tailles de page sont appelées des ''pages larges''. La taille optimale pour les pages dépend de nombreux paramètres et il n'y a pas de taille qui convienne à tout le monde. Certaines applications gagnent à utiliser des pages larges, d'autres vont au contraire perdre drastiquement en performance en les utilisant.
Le désavantage principal des pages larges est qu'elles favorisent la fragmentation mémoire. Si un programme veut réserver une portion de mémoire, pour une structure de donnée quelconque, il doit réserver une portion dont la taille est multiple de la taille d'une page. Par exemple, un programme ayant besoin de 110 kibioctets allouera 28 pages de 4 kibioctets, soit 120 kibioctets : 2 kibioctets seront perdus. Par contre, avec des pages larges de 2 mébioctets, on aura une perte de 2048 - 110 = 1938 kibioctets. En somme, des morceaux de mémoire seront perdus, car les pages sont trop grandes pour les données qu'on veut y mettre. Le résultat est que le programme qui utilise les pages larges utilisent plus de mémoire et ce d'autant plus qu'il utilise des données de petite taille. Un autre désavantage est qu'elles se marient mal avec certaines techniques d'optimisations de type ''copy-on-write''.
Mais l'avantage est que la traduction des adresses est plus performante. Une taille des pages plus élevée signifie moins de pages, donc des tables des pages plus petites. Et des pages des tables plus petites n'ont pas besoin de beaucoup de niveaux de hiérarchie, voire peuvent se limiter à des tables des pages simples, ce qui rend la traduction d'adresse plus simple et plus rapide. De plus, les programmes ont une certaine localité spatiale, qui font qu'ils accèdent souvent à des données proches. La traduction d'adresse peut alors profiter de systèmes de mise en cache dont nous parlerons dans le prochain chapitre, et ces systèmes de cache marchent nettement mieux avec des pages larges.
Il faut noter que la taille des pages est presque toujours une puissance de deux. Cela a de nombreux avantages, mais n'est pas une nécessité. Par exemple, le tout premier processeur avec de la pagination, le super-ordinateur Atlas, avait des pages de 3 kibioctets. L'avantage principal est que la traduction de l'adresse physique en adresse logique est trivial avec une puissance de deux. Cela garantit que l'on peut diviser l'adresse en un numéro de page et un ''offset'' : la traduction demande juste de remplacer les bits de poids forts par le numéro de page voulu. Sans cela, la traduction d'adresse implique des divisions et des multiplications, qui sont des opérations assez couteuses.
===Les entrées de la table des pages===
Avant de poursuivre, faisons un rapide rappel sur les entrées de la table des pages. Nous venons de voir que la table des pages contient de nombreuses informations : un bit ''valid'' pour la mémoire virtuelle, des bits ''dirty'' et ''accessed'' utilisés par l'OS, des bits de protection mémoire, un bit ''global'' et un potentiellement un identifiant de processus, etc. Étudions rapidement le format de la table des pages sur un processeur x86 32 bits.
* Elle contient d'abord le numéro de page physique.
* Les bits AVL sont inutilisés et peuvent être configurés à loisir par l'OS.
* Le bit G est le bit ''global''.
* Le bit PS vaut 0 pour une page de 4 kibioctets, mais est mis à 1 pour une page de 4 mébioctets dans le cas où le processus utilise des pages larges.
* Le bit D est le bit ''dirty''.
* Le bit A est le bit ''accessed''.
* Le bit PCD indique que la page ne peut pas être cachée, dans le sens où le processeur ne peut copier son contenu dans le cache et doit toujours lire ou écrire cette page directement dans la RAM.
* Le bit PWT indique que les écritures doivent mettre à jour le cache et la page en RAM (dans le chapitre sur le cache, on verra qu'il force le cache à se comporter comme un cache ''write-through'' pour cette page).
* Le bit U/S précise si la page est accessible en mode noyau ou utilisateur.
* Le bit R/W indique si la page est accessible en écriture, toutes les pages sont par défaut accessibles en lecture.
* Le bit P est le bit ''valid''.
[[File:PDE.png|centre|vignette|upright=2.5|Table des pages des processeurs Intel 32 bits.]]
==Comparaison des différentes techniques d'abstraction mémoire==
Pour résumer, l'abstraction mémoire permet de gérer : la relocation, la protection mémoire, l'isolation des processus, la mémoire virtuelle, l'extension de l'espace d'adressage, le partage de mémoire, etc. Elles sont souvent implémentées en même temps. Ce qui fait qu'elles sont souvent confondues, alors que ce sont des concepts sont différents. Ces liens sont résumés dans le tableau ci-dessous.
{|class="wikitable"
|-
!
! colspan="5" | Avec abstraction mémoire
! rowspan="2" | Sans abstraction mémoire
|-
!
! Relocation matérielle
! Segmentation en mode réel (x86)
! Segmentation, général
! Architectures à capacités
! Pagination
|-
! Abstraction matérielle des processus
| colspan="4" | Oui, relocation matérielle
| Oui, liée à la traduction d'adresse
| Emulée via relocation logicielle au démarrage du programme.
|-
! Mémoire virtuelle
| colspan="2" | Non, sauf émulation logicielle
| colspan="3" | Oui, gérée par le processeur et l'OS
| Non, sauf émulation logicielle
|-
! Extension de l'espace d'adressage
| colspan="2" | Oui : registre de base élargi
| colspan="2" | Oui : adresse de base élargie dans la table des segments
| ''Physical Adress Extension'' des processeurs 32 bits
| Commutation de banques
|-
! Protection mémoire
| Registre limite
| Aucune
| colspan="2" | Registre limite, droits d'accès aux segments
| Gestion des droits d'accès aux pages
| Possible, méthodes variées
|-
! Partage de mémoire
| colspan="2" | Non
| colspan="2" | Segment partagés
| Pages partagées
| Possible, méthodes variées
|}
===Les différents types de segmentation===
La segmentation regroupe plusieurs techniques franchement différentes, qui auraient gagné à être nommées différemment. La principale différence est l'usage de registres de relocation versus des registres de sélecteurs de segments. L'usage de registres de relocation est le fait de la relocation matérielle, mais aussi de la segmentation en mode réel des CPU x86. Par contre, l'usage de sélecteurs de segments est le fait des autres formes de segmentation, architectures à capacité inclues.
La différence entre les deux est le nombre de segments. L'usage de registres de relocation fait que le CPU ne gère qu'un petit nombre de segments de grande taille. La mémoire virtuelle est donc rarement implémentée vu que swapper des segments de grande taille est trop long, l'impact sur les performances est trop important. Sans compter que l'usage de registres de base se marie très mal avec la mémoire virtuelle. Vu qu'un segment peut être swappé ou déplacée n'importe quand, il faut invalider les registres de base au moment du swap/déplacement, ce qui n'est pas chose aisée. Aucun processeur ne gère cela, les méthodes pour n'existent tout simplement pas. L'usage de registres de base implique que la mémoire virtuelle est absente.
La protection mémoire est aussi plus limitée avec l'usage de registres de relocation. Elle se limite à des registres limite, mais la gestion des droits d'accès est limitée. En théorie, la segmentation en mode réel pourrait implémenter une version limitée de protection mémoire, avec une protection de l'espace exécutable. Mais ca n'a jamais été fait en pratique sur les processeurs x86.
Le partage de la mémoire est aussi difficile sur les architectures avec des registres de base. L'absence de table des segments fait que le partage d'un segment est basiquement impossible sans utiliser des méthodes complétement tordues, qui ne sont jamais implémentées en pratique.
===Segmentation versus pagination===
Par rapport à la pagination, la segmentation a des avantages et des inconvénients. Tous sont liés aux propriétés des segments et pages : les segments sont de grande taille et de taille variable, les pages sont petites et de taille fixe.
L'avantage principal de la segmentation est sa rapidité. Le fait que les segments sont de grande taille fait qu'on a pas besoin d'équivalent aux tables des pages inversée ou multiple, juste d'une table des segments toute simple. De plus, les échanges entre table des pages/segments et registres sont plus rares avec la segmentation. Par exemple, si un programme utilise un segment de 2 gigas, tous les accès dans le segment se feront avec une seule consultation de la table des segments. Alors qu'avec la pagination, il faudra une consultation de la table des pages chaque bloc de 4 kibioctet, au minimum.
Mais les désavantages sont nombreux. Le système d'exploitation doit agencer les segments en RAM, et c'est une tâche complexe. Le fait que les segments puisse changer de taille rend le tout encore plus complexe. Par exemple, si on colle les segments les uns à la suite des autres, changer la taille d'un segment demande de réorganiser tous les segments en RAM, ce qui demande énormément de copies RAM-RAM. Une autre possibilité est de laisser assez d'espace entre les segments, mais cet espace est alors gâché, dans le sens où on ne peut pas y placer un nouveau segment.
Swapper un segment est aussi très long, vu que les segments sont de grande taille, alors que swapper une page est très rapide.
==Un cas particulier : la z/architecture d'IBM==
IBM a sa propre gamme de ''mainframes'', qui utilisent des processeurs fait maison par IBM lui-même. Les CPU IBM ont leur propre jeu d'instruction CISC, appelé '''''z/architecture'''''. Et ce jeu d'instruction a des possibilités en termes d'adressage qui sont extrêmement intéressantes, en plus d'être assez inédites.
Nous allons nous concentrer sur une fonctionnalité : les programmes ont accès à plusieurs espaces d'adressage simultanément. La ''z/architecture'' supporte 65 536 espaces d'adressage simultanés. Par contre, un programme ne peut en utiliser que 2048 en même temps. De plus, ils ne sont accessibles que quand le processeur est dans un mode de fonctionnement bien précis. Un processeur ''z/architecture'' peut en effet fonctionner dans deux modes, qu'on va appeler "standard" et "étendu".
L'existence de ces deux modes s'explique par le fait que les ''mainframes'' IBM ont évolués dans le temps. Les premiers ''mainframes'' n'avaient même pas de support basique de la mémoire virtuelle, le System/360 s'en passait totalement. C'est sur le System/370 que la pagination a été ajoutée. Le System/370 supportait deux espaces d'adressage virtuel par programme, là où les implémentations classiques n'en utilisent qu'un par programme. L'ESA/370 a ajouté la possibilité d'utiliser 16 espaces d'adressage par programme, mais l'a fait d'une manière à garder la compatibilité avec le System/370. Le processeur a donc deux modes : un avec deux espaces d'adressage par programme, un autre pouvant monter jusqu’à 16.
La taille des adresses a aussi évoluée dans le temps : partant de 24 bits sur le System/360, elle est montée à 31 bits sur le System/370, pour arriver à 64 bits sur le jeu d'instruction z/architecture proprement dit. Sur le System/370, il était possible de switcher à la volée entre adressage 24 et 31 bits, avec les instructions ''Branch and save and
set mode'' (BASSM) et ''Branch And Set Mode'' (BSM).
===Le System/370===
Le System/370 gérait des pages de 4 kibioctets, les pages larges ayant été introduites plus tard. La traduction d’adresse se fait en passant des regroupements de plusieurs pages virtuelles, appelés des ''segments'', qui n'ont rien à voir avec les segments au sens de la segmentation. Pour éviter les confusions, je vais les appeler des ''super-pages''. Les super-pages font 64 kibioctets ou 1 mébioctet, et regroupent plusieurs pages de 2 ou 4 mébioctets. Les pages d'une super-page peuvent être très éloignées en mémoire physique. Les super-pages sont concrètement des intermédiaires utilisés pour la traduction d'adresse. La traduction d'adresse démarre à une table des super-pages, qui point vers une table des pages à plusieurs niveaux (5 maximum).
Le System/370 était un système multi-processeur, ce qui a amené à quelques spécificités en termes d'adressage mémoire. La page numéro 0 sert pour le vecteur d'interruption et d'autres données importantes, pour des données importantes. Et chaque processeur a besoin de sa propre table d'interruption. Pour cela, chaque processeur réserve une page dédiée pour son vecteur d'interruption. Quand ce processeur accède à une adresse située dans la page numéro 0, l'adresse est modifiée pour tomber dans cette page réservée. Notons que le numéro de page converti est un numéro de page physique ! Le processeur fait donc une première traduction d'adresse pour déterminer l'adresse physique, et celle-ci est corrigée une seconde fois en cas d'accès à la première page mémoire. Le tout porte le nom de technique de '''''prefixing'''''.
Le System/370 supporte deux espaces d'adressages par programme : l''''espace d'adressage primaire''' où se situent le programme exécuté et les données importantes, un '''espace d'adressage secondaire''' utilisé pour des tâches/données annexes. Un troisième espace d'adressage l''''espace d'adressage ''home''''', est réservé au système d'exploitation. Il a pour particularité de ne pas avoir d'ASN. Il n'est concrètement accessible qu'en mode superviseur, donc par le système d'exploitation. C'est en quelque sorte un espace d'adressage pour le noyau, séparé des espaces d'adressage utilisateur. Nous n'en parlerons pas dans ce qui suit.
Mais comment le processeur pour utiliser ces espaces d'adressages ? Par exemple, s'il veut faire une lecture, comment précise-t-il l'espace d'adressage courant ? La réponse est que le processeur peut être dans deux états : soit primaire, soit secondaire. Dans le premier état, toutes les lectures/écritures se font dans l'espace d'adressage primaire. Dans le second, elles se font toutes dans l'espace d'adressage secondaire. Il peut passer de l'un à l'autre en utilisant l'instruction ''Set Address Space Control''. Le processeur dispose aussi d'instructions ''MOVE TO PRIMARY'' et ''MOVE TO SECONDARY'' pour échanger des données dans les deux espaces d'adressage.
L'espace d'adressage primaire a une table des pages dédiée, séparée de celle de l'espace d'adressage secondaire. Ou plutôt devrais-je dire une table des super-pages. La table des pages est une table des pages emboitée. Il y a peu à détailler dessus, si ce n'est pour cette histoire de segments/super-pages. L'adresse de la table des super-page est mémorisée dans le registre de contrôle CR1 pour l'espace d'adressage primaire, CR7 pour l'espace d'adressage secondaire. L'un ou l'autre est utilisé, selon que le processeur est en mode "primaire" ou "secondaire".
Un programme peut donc passer de l'espace d'adressage primaire à secondaire (ou inversement) en utilisant l’instruction ''SET ADDRESS SPACE CONTROL''.
===Les registres d'accès de l'ESA370===
Si le System/370 permettait d'utiliser deux espaces d'adressage par programme, l'ESA 370 a poussé la logique nettement plus loin. Il a ajouté un mode de fonctionnement supplémentaire, qui permet d'utiliser jusqu'à 2048 espaces d'adressage différents, en même temps. Le processeur n'a pas besoin d'utiliser d'instructions particulières pour copier des données d'un espace d'adressage à un autre, un simple MOV suffit. Le mode en question est appelé le '''mode ''access register'''''. Il est complémentaire du mode à deux espaces d'adressage, pour des raisons de compatibilité, l'ancien mode à deux esapces d'adressage a été conservé.
Le mode ''access register'' donne accès aux '''registres d'accès''', qui lui donnent son nom. Les registres d'accès mémorisent des identifiants qui permettent de sélectionner un espace d'adressage parmi 2048. Il y a en tout 16 registres d'accès, chacun étant appairé avec un registre général. Par exemple, le registre général numéro 5 est appairé au registre d'accès de même numéro. En mode ''access register'', toute lecture/écriture lit l'adresse dans un registre général et le registre d'accès associé indique dans quel espace d'adressage se trouve cette adresse. Par exemple, une lecture utilise l'adresse dans le registre général numéro 7, l'espace d'adressage associé sera dans le registre d'accès numéro 7.
Les registres d'accès peuvent être modifiés par des instructions classiques, en espace utilisateur. Elles sont nommées COPY ACCESS, EXTRACT ACCESS, LOAD ACCESS MULTIPLE, LOAD ADDRESS EXTENDED, SET ACCESS et STORE ACCESS MULTIPLE. Elles permettent de copier un registre d'accès dans un autre, de charger le contenu d'un registre d'accès depuis la mémoire RAM, d'enregistrer les registres d'accès dans la mémoire RAM, de copier une constante dans un registre d'accès. Mais quel est le contenu de ces registres ?
Une implémentation simple numéroterait les espaces d'adressage et mettrait les numéros dans les registres d'accès. Un espace d'adressage serait associé à une table des super-pages, reste à mémoriser la correspondance ''numéro d'espace d'adressage - adresse de la table des super-pages''. Pour cela, l'idéal est de mémoriser ces correspondances dans un tableau en mémoire RAM. Le numéro d'espace d'adressage est alors un indice, qui pointe sur l'entrée adéquate dans cette table. Il devrait y avoir un tableau par programme, qui serait protégé en écriture et seulement accessible par le système d'exploitation, comme le serait une table des pages. Mais ce n'est pas exactement ce qui est fait.
===L'utilisation des registres d'accès===
La différence est qu'il n'y a pas une table de correspondance par programme, mais un système à deux tableaux imbriqués. Le premier tableau possède 65 536 entrées, une par espace d'adressage supporté par le processeur. C'est donc une table partagée par tous les programmes. Par contre, la seconde table est bien unique pour chaque programme, chaque programme a la sienne. Faire ainsi permet de simplifier le partage de la mémoire. La table partagée est l''''''Access Space Table Entry''''' (ASTE), alors que la seconde table est la '''liste privée''' (''access list''), sous-entendu "privée pour un programme".
La liste privée mémorise une liste des espaces d'adressage auquel le programme peut accéder, auxquels le système d'exploitation lui a accordé l'accès. Pour chaque espace d'adressage, elle renvoie vers l'entrée de l'ASTE associée. En plus de mémoriser l'adresse de cette entrée, elle mémorise les droits d'accès que le programme a sur l'espace d'adressage associé. Par exemple, si deux programmes partagent un espace d'adressage, l'un pourra y avoir accès en lecture seule, l'autre pourra avoir un accès total. Pour cela, ils ont des droits différents dans la liste privée, mais l'entrée de l'ASTE sera la même.
[[File:Tradiction d'adresse sur la zarchitecture d'IBM.png|centre|vignette|upright=2|Traduction d'adresse sur la zarchitecture d'IBM.]]
Il y a donc une vérification des droits d'accès, suivi d'un accès à une table des super-pages globale. Si un programme veut accéder à un espace d'adressage qui lui est interdit, l'accès mémoire stoppera dans la liste privée. Le processeur utilisera l'identifiant d'espace d'adressage, mais la liste privée n'aura pas d'entrée associée. Elle lèvera alors une exception matérielle et le système d'exploitation gérera la situation, vraisemblablement en tuant le programme fautif. Mais si l'accès est autorisé, alors le programme accède à la table des super-pages et l'accès mémoire se poursuit.
Je dis une liste d'accès privée, mais il y en a deux en réalité. Mais la distinction exacte demanderait de parler de ''threads'' et de processus. Pour ceux qui sont familiers avec ces concepts, chaque ''thread'' a sa propre liste privée, avec en plus une liste par processus, partagée entre tous les ''threads''. Deux listes de 1024 entrées maximum, cela fait 2048 entrées au total. Les deux listes portent les noms de ''Dispatchable Unit Access List'' et de ''PASN-AL (Primary Address Space Access List''. Inutile de retenir ces deux termes, cependant.
Un registre d'accès mémorise un '''''Access List Entry Token''''' ou ALET. L'ALET contient l'indice d'une entrée dans la liste privée. Elle contient aussi un bit qui indique quelle liste privée utiliser, entre la ''Dispatchable Unit Access List'' et la ''PASN-AL (Primary Address Space Access List''. Précisons cependant que deux valeurs font exceptions : 0 pointe vers l'espace d'adressage principal, 1 vers l'espace d'adressage secondaire. Les autres valeurs sont configurables.
{|class="wikitable"
|+ Registre d'accès
|-
! 0000000 !! L (choix de la liste privée !! ASLEN !! ALEN
|-
| 0000000 || Choix de la liste privée || Identifiant de protection || Indice dans la liste privée
|}
Lors d'une lecture/écriture, l'ALETs est utilisé pour sélectionner l'espace d'adressage. Les valeurs 0 et 1 sont traitées telles quelles, l'espace d'adressage est déterminé automatiquement par le processeur. Pour les autres valeurs, récupérer l'adresse mémoire demande de consulter la liste d'accès privée. L'ALET est envoyé à la liste d'accès privée, et cette dernière fournit l'adresse de l'entrée de l'ASTE à consulter. Le processeur connait l'adresse de la liste privée, car son adresse mémoire est mémorisée dans les registres de contrôle numéro 2 et 5.
: Précisons que le registre d'accès numéro 0 contient obligatoirement la valeur 0, ce qui fait qu'il pointe vers l'espace d'adressage primaire.
[[File:Traduction d'adresse - trois espaces d'adressage, zarchitecture d'IBM.png|centre|vignette|upright=2|Traduction d'adresse - trois espaces d'adressage, zarchitecture d'IBM.]]
Une subtilité est que le système d'exploitation peut à tout moment modifier le contenu de la liste privée. Il se peut qu'un ALET qui correspondait à un espace d'adressage se mette à pointer vers un autre. Pour détecter cette situation, le processeur attribue un identifiant, à chaque espace d'adressage. L'identifiant est mémorisé dans les entrées de la liste privée, de l'ASTE et dans les registres d'accès (dans les bits de poids fort de l'ALET). L'identifiant est modifié dans l'ALET à chaque fois que le système d'exploitation modifie l'entrée. Si un programme fait un accès avec un registre d'accès, mais que l'identifiant ne correspond pas avec celui dans l'ALET/liste privée, c'est signe que l'espace d'adressage a été swappé, une exception matérielle est levée.
Un tel fonctionnement vous aura sans doute fait penser à une architecture à capacité, mais la z/architecture n'en est pas une. Malgré des points de ressemblance assez importants, il y a des différences fondamentales avec les architectures à capacité.
* Le z/architecture ne supporte pas la segmentation, qui est nécessaire pour les architectures à capacité. Sur la z/architecture, ce sont des espaces d'adressage qui sont sélectionnés, pas des segments.
* Les registres d'accès ne sont pas des registres de capacité, car on peut les modifier avec des instructions en espace utilisateur. En comparaison, modifier les registres de capacité ne peut se faire qu'en espace noyau, sous l'égide du système d'exploitation.
* Les droits d'accès sont dans une table séparée des pointeurs, pas concaténés à ceux-ci. La gestion des droits d'accès est gérée par les tables des pages.
* Les identifiants ALET ne peuvent pas être passés à un autre programme. Deux ALETs identiques correspondent à deux espaces d'adressages différents dans deux programmes différents. L'identifiant peut être identique, il pointera vers l'entrée de même numéro dans les deux listes privées, mais l'adresse dans l'entrée peut être différente.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=L'espace d'adressage du processeur
| prevText=L'espace d'adressage du processeur
| next=Les méthodes de synchronisation entre processeur et périphériques
| nextText=Les méthodes de synchronisation entre processeur et périphériques
}}
</noinclude>
s4arofhkcl3kxne97sbfih04ynah1fp
775339
775338
2026-10-16T21:18:33Z
Mewtow
31375
/* L'utilisation des registres d'accès */
775339
wikitext
text/x-wiki
Pour introduire ce chapitre, nous devons faire un rappel sur le concept d{{'}}'''espace d'adressage'''. Pour rappel, un espace d'adressage correspond à l'ensemble des adresses utilisables par le processeur. Par exemple, si je prends un processeur 16 bits, il peut adresser en tout 2^16 = 65536 adresses, l'ensemble de ces adresses forme son espace d'adressage. Intuitivement, on s'attend à ce qu'il y ait correspondance avec les adresses de la mémoire RAM. J'entends par là que l'adresse 1209 de l'espace d'adressage correspond à l'adresse 1209 en mémoire RAM. C'est là une hypothèse parfaitement raisonnable et on voit mal comment ce pourrait ne pas être le cas.
Mais les processeurs modernes utilisent des techniques d{{'}}'''abstraction mémoire''' qui font que ce n'est pas le cas. Avec ces techniques, l'adresse 1209 de l'espace d'adressage correspond en réalité à l'adresse 9999 en mémoire RAM, voire n'est pas en RAM. L'abstraction mémoire fait que l'espace d'adressage regroupe des adresses fictives, qui doivent être traduites en adresses mémoires réelles pour être utilisées. Les adresses de l'espace d'adressage portent le nom d{{'}}'''adresses logiques''', alors que les adresses de la mémoire RAM sont appelées '''adresses physiques'''.
L'intérêt de l'abstraction matérielle n'est pas évident. Aussi, avant de parler de comment l'abstraction mémoire fonctionne, nous allons voir à quoi elle sert. Nous allons voir qu'elle a plusieurs utilisations différentes, qui sont absolument nécessaires sur tous les ordinateurs personnels modernes. Tous les processeurs modernes la prennent en charge, les systèmes d'exploitation coopérent avec le processeur pour l'utiliser au mieux.
==L'abstraction mémoire permet plusieurs fonctionnalités complémentaires==
La fonctionnalité la plus connue est la mémoire virtuelle, dont vous avez peut-être déjà entendu parler, peut-être que le nom vous dit quelque chose. Si ce n'est pas le cas, nous la détailleront dans la suite. Elle permet concrétement à un programme d'utiliser plus de mémoire qu'il n'y a de RAM installé dans un ordinateur, en utilisant le disque dur comme solution de secours. Mais d'autres fonctionnalités moins évidentes sont permises par l'abstraction mémoire. Et la première que nous allons voir est l'abstraction des processus.
: En général, une adresse logique correspond à une seule adresse physique. Mais beaucoup de fonctionnalités avancées ne respectent pas cette règle.
===L'abstraction matérielle des processus===
Les systèmes d'exploitation modernes sont dits multi-tâche, à savoir qu'ils sont capables d'exécuter plusieurs logiciels en même temps. Et ce même si un seul processeur est présent dans l'ordinateur : les logiciels sont alors exécutés à tour de rôle. Toutefois, cela amène un paquet de problèmes qu'il faut résoudre au mieux. Par exemple, les programmes exécutés doivent se partager la mémoire RAM, ce qui ne vient pas sans problèmes. Le problème principal est que les programmes ne doivent pas lire ou écrire dans les données d'un autre, sans quoi on se retrouverait rapidement avec des problèmes. Il faut donc introduire des mécanismes d{{'}}'''isolement des processus''', pour isoler les programmes les uns des autres.
Un de ces mécanismes est l{{'}}'''abstraction matérielle des processus''', une technique qui fait que chaque programme a son propre espace d'adressage. Chaque programme a l'impression d'avoir accès à tout l'espace d'adressage, de l'adresse 0 à l'adresse maximale gérée par le processeur. Évidemment, il s'agit d'une illusion maintenue justement grâce à la traduction d'adresse. Les espaces d'adressage contiennent des adresses logiques, les adresses de la RAM sont des adresses physiques, la nécessité de l'abstraction mémoire est évidente.
Implémenter l'abstraction mémoire peut se faire de plusieurs manières. Mais dans tous les cas, il faut que la correspondance adresse logique - physique change d'un programme à l'autre. Ce qui est normal, vu que les deux processus sont placés à des endroits différents en RAM physique. La conséquence est qu'avec l'abstraction mémoire, une adresse logique correspond à plusieurs adresses physiques. Une même adresse logique dans deux processus différents correspond à deux adresses phsiques différentes, une par processus. Une adresse logique dans un processus correspondra à l'adresse physique X, la même adresse dans un autre processus correspondra à l'adresse Y.
Les adresses physiques qui partagent la même adresse logique sont alors appelées des '''adresses homonymes'''. Le choix de la bonne adresse étant réalisé par un mécanisme matériel et dépend du programme en cours. Le mécanisme pour choisir la bonne adresse dépend du processeur, mais il y en a deux grands types :
* La première consiste à utiliser l'identifiant de processus CPU, vu au chapitre précédent. C'est, pour rappel, un numéro attribué à chaque processus par le processeur. L'identifiant du processus en cours d'exécution est mémorisé dans un registre du processeur. La traduction d'adresse utilise cet identifiant, en plus de l'adresse logique, pour déterminer l'adresse physique.
* La seconde solution mémorise les correspondances adresses logiques-physique dans des tables en mémoire RAM, qui sont différentes pour chaque programme. Les tables sont accédées à chaque accès mémoire, afin de déterminer l'adresse physique.
===Le partage de la mémoire===
L'isolation des processus est très importante sur les systèmes d'exploitation modernes. Cependant, il existe quelques situations où elle doit être contournée ou du moins mise en pause. Les situations sont multiples : gestion de bibliothèques partagées, communication entre processus, usage de ''threads'', etc. Elles impliquent toutes un '''partage de mémoire''', à savoir qu'une portion de mémoire RAM est partagée entre plusieurs programmes. Le partage de mémoire est une sorte de brèche de l'isolation des processus, mais qui est autorisée car elle est utile.
Un cas intéressant est celui des '''bibliothèques partagées'''. Les bibliothèques sont des collections de fonctions regroupées ensemble, dans une seule unité de code. Un programme qui utilise une bibliothèque peut appeler n’importe quelle fonction présente dans la bibliothèque. La bibliothèque peut être simplement inclue dans le programme lui-même, on parle alors de bibliothèques statiques. De telles bibliothèques fonctionnent très bien, mais avec un petit défaut pour les bibliothèques très utilisées : plusieurs programmes qui utilisent la même bibliothèque vont chacun l'inclure dans leur code, ce qui fera doublon.
Pour éviter cela, les OS modernes gèrent des bibliothèques partagées, à savoir qu'un seul exemplaire de la bibliothèque est partagé entre plusieurs programmes. Chaque programme peut exécuter une fonction de la bibliothèque quand il le souhaite, en effectuant un branchement adéquat. Mais cela implique que la bibliothèque soit présente dans l'espace d'adressage du programme en question. Une bibliothèque est donc présente dans plusieurs espaces d'adressage, alors qu'il n'y en a qu'un seul exemplaire en mémoire RAM.
[[File:Ogg vorbis libs and application dia.svg|centre|vignette|upright=2|Exemple de bibliothèques, avec Ogg vorbis.]]
D'autres situations demandent de partager de la mémoire entre deux programmes. Par exemple, les systèmes d'exploitation modernes gèrent nativement des systèmes de '''communication inter-processus''', très utilisés par les programmes modernes pour échanger des données. Et la plupart demandant de partager un bout de mémoire entre processus, même si c'est seulement temporairement. Typiquement, deux processus partagent un intervalle d'adresse où l'un écrit les données à l'autre, l'autre lisant les données envoyées.
Une dernière utilisation de la mémoire partagée est l{{'}}'''accès direct au noyau'''. Sur les systèmes d'exploitations moderne, dans l'espace d'adressage de chaque programme, les adresses hautes sont remplies avec une partie du noyau ! Évidemment, ces adresses sont accessibles uniquement en lecture, pas en écriture. Pas question de modifier le noyau de l'OS ! De plus, il s'agit d'une portion du noyau dont on sait que la consultation ne pose pas de problèmes de sécurité.
Le programme peut lire des données dans cette portion du noyau, mais aussi exécuter les fonctions du noyau qui sont dedans. L'idée est d'éviter des appels systèmes trop fréquents. Au lieu d'effectuer un véritable appel système, avec une interruption logicielle, le programme peut exécuter des appels systèmes simplifiés, de simples appels de fonctions couplés avec un changement de niveau de privilège (passage en espace noyau nécessaire).
[[File:AMD64-canonical--48-bit.png|vignette|Répartition des adresses entre noyau (jaune/orange) et programme (verte), sur les systèmes x86-64 bits, avec des adresses physiques de 48 bits.]]
L'espace d'adressage est donc séparé en deux portions : l'OS d'un côté, le programme de l'autre. La répartition des adresses entre noyau et programme varie suivant l'OS ou le processeur utilisé. Sur les PC x86 32 bits, Linux attribuait 3 gigas pour les programmes et 1 giga pour le noyau, Windows attribuait 2 gigas à chacun. Sur les systèmes x86 64 bits, l'espace d'adressage d'un programme est coupé en trois, comme illustré ci-contre : une partie basse de 2^48 octets, une partie haute de même taille, et un bloc d'adresses invalides entre les deux. Les adresses basses sont utilisées pour le programme, les adresses hautes pour le noyau, il n'y a rien entre les deux.
Avec le partage de mémoire, plusieurs adresses logiques correspondent à la même adresse physique. Tel processus verra la zone de mémoire partagée à l'adresse X, l'autre la verra à l'adresse Y. Mais il s'agira de la même portion de mémoire physique, avec une seule adresse physique. En clair, lorsque deux processus partagent une même zone de mémoire, la zone sera mappées à des adresses logiques différentes. Les adresses logiques sont alors appelées des '''adresses synonymes''', terme qui trahit le fait qu'elles correspondent à la même adresse physique.
===La mémoire virtuelle===
Toutes les adresses ne sont pas forcément occupées par de la mémoire RAM, s'il n'y a pas assez de RAM installée. Par exemple, un processeur 32 bits peut adresser 4 gibioctets de RAM, même si seulement 3 gibioctets sont installés dans l'ordinateur. L'espace d'adressage contient donc 1 gigas d'adresses inutilisées, et il faut éviter ce surplus d'adresses pose problème.
Sans mémoire virtuelle, seule la mémoire réellement installée est utilisable. Si un programme utilise trop de mémoire, il est censé se rendre compte qu'il n'a pas accès à tout l'espace d'adressage. Quand il demandera au système d'exploitation de lui réserver de la mémoire, le système d'exploitation le préviendra qu'il n'y a plus de mémoire libre. Par exemple, si un programme tente d'utiliser 4 gibioctets sur un ordinateur avec 3 gibioctets de mémoire, il ne pourra pas. Pareil s'il veut utiliser 2 gibioctets de mémoire sur un ordinateur avec 4 gibioctets, mais dont 3 gibioctets sont déjà utilisés par d'autres programmes. Dans les deux cas, l'illusion tombe à plat.
Les techniques de '''mémoire virtuelle''' font que l'espace d'adressage est utilisable au complet, même s'il n'y a pas assez de mémoire installée dans l'ordinateur ou que d'autres programmes utilisent de la RAM. Par exemple, sur un processeur 32 bits, le programme aura accès à 4 gibioctets de RAM, même si d'autres programmes utilisent la RAM, même s'il n'y a que 2 gibioctets de RAM d'installés dans l'ordinateur.
Pour cela, on utilise une partie des mémoires de masse (disques durs) d'un ordinateur en remplacement de la mémoire physique manquante. Le système d'exploitation crée sur le disque dur un fichier, appelé le ''swapfile'' ou '''fichier de ''swap''''', qui est utilisé comme mémoire RAM supplémentaire. Il mémorise le surplus de données et de programmes qui ne peut pas être mis en mémoire RAM.
[[File:Vm1.png|centre|vignette|upright=2.0|Mémoire virtuelle et fichier de Swap.]]
Une technique naïve de mémoire virtuelle serait la suivante. Avant de l'aborder, précisons qu'il s'agit d'une technique abordée à but pédagogique, mais qui n'est implémentée nulle part tellement elle est lente et inefficace. Un espace d'adressage de 4 gigas ne contient que 3 gigas de RAM, ce qui fait 1 giga d'adresses inutilisées. Les accès mémoire aux 3 gigas de RAM se font normalement, mais l'accès aux adresses inutilisées lève une exception matérielle "Memory Unavailable". La routine d'interruption de cette exception accède alors au ''swapfile'' et récupère les données associées à cette adresse. La mémoire virtuelle est alors émulée par le système d'exploitation.
Le défaut de cette méthode est que l'accès au giga manquant est toujours très lent, parce qu'il se fait depuis le disque dur. D'autres techniques de mémoire virtuelle logicielle font beaucoup mieux, mais nous allons les passer sous silence, vu qu'on peut faire mieux, avec l'aide du matériel.
L'idée est de charger les données dont le programme a besoin dans la RAM, et de déplacer les autres sur le disque dur. Par exemple, imaginons la situation suivante : un programme a besoin de 4 gigas de mémoire, mais ne dispose que de 2 gigas de mémoire installée. On peut imaginer découper l'espace d'adressage en 2 blocs de 2 gigas, qui sont chargés à la demande. Si le programme accède aux adresses basses, on charge les 2 gigas d'adresse basse en RAM. S'il accède aux adresses hautes, on charge les 2 gigas d'adresse haute dans la RAM après avoir copié les adresses basses sur le ''swapfile''.
On perd du temps dans les copies de données entre RAM et ''swapfile'', mais on gagne en performance vu que tous les accès mémoire se font en RAM. Du fait de la localité temporelle, le programme utilise les données chargées depuis le swapfile durant un bon moment avant de passer au bloc suivant. La RAM est alors utilisée comme une sorte de cache alors que les données sont placées dans une mémoire fictive représentée par l'espace d'adressage et qui correspond au disque dur.
Mais avec cette technique, la correspondance entre adresses du programme et adresses de la RAM change au cours du temps. Les adresses de la RAM correspondent d'abord aux adresses basses, puis aux adresses hautes, et ainsi de suite. On a donc besoin d'abstraction mémoire. Les correspondances entre adresse logique et physique peuvent varier avec le temps, ce qui permet de déplacer des données de la RAM vers le disque dur ou inversement. Une adresse logique peut correspondre à une adresse physique, ou bien à une donnée swappée sur le disque dur. C'est l'unité de traduction d'adresse qui se charge de faire la différence. Si une correspondance entre adresse logique et physique est trouvée, elle l'utilise pour traduire les adresses. Si aucune correspondance n'est trouvée, alors elle laisse la main au système d'exploitation pour charger la donnée en RAM. Une fois la donnée chargée en RAM, les correspondances entre adresse logique et physiques sont modifiées de manière à ce que l'adresse logique pointe vers la donnée chargée.
===L'extension d'adressage===
Une autre fonctionnalité rendue possible par l'abstraction mémoire est l{{'}}'''extension d'adressage'''. Elle permet d'utiliser plus de mémoire que l'espace d'adressage ne le permet. Par exemple, utiliser 7 gigas de RAM sur un processeur 32 bits, dont l'espace d'adressage ne gère que 4 gigas. L'extension d'adresse est l'exact inverse de la mémoire virtuelle. La mémoire virtuelle sert quand on a moins de mémoire que d'adresses, l'extension d'adresse sert quand on a plus de mémoire que d'adresses.
Il y a quelques chapitres, nous avions vu que c'est possible via la commutation de banques. Mais l'abstraction mémoire est une méthode alternative. Que ce soit avec la commutation de banques ou avec l'abstraction mémoire, les adresses envoyées à la mémoire doivent être plus longues que les adresses gérées par le processeur. La différence est que l'abstraction mémoire étend les adresses d'une manière différente.
Une implémentation possible de l'extension d'adressage fait usage de l'abstraction matérielle des processus. Chaque processus a son propre espace d'adressage, mais ceux-ci sont placés à des endroits différents dans la mémoire physique. Par exemple, sur un ordinateur avec 16 gigas de RAM, mais un espace d'adressage de 2 gigas, on peut remplir la RAM en lançant 8 processus différents et chaque processus aura accès à un bloc de 2 gigas de RAM, pas plus, il ne peut pas dépasser cette limite. Ainsi, chaque processus est limité par son espace d'adressage, mais on remplit la mémoire avec plusieurs processus, ce qui compense. Il s'agit là de l'implémentation la plus simple, qui a en plus l'avantage d'avoir la meilleure compatibilité logicielle. De simples changements dans le système d'exploitation suffisent à l'implémenter.
[[File:Extension de l'espace d'adressage.png|centre|vignette|upright=1.5|Extension de l'espace d'adressage]]
Un autre implémentation donne plusieurs espaces d'adressage différents à chaque processus, et a donc accès à autant de mémoire que permis par la somme de ces espaces d'adressage. Par exemple, sur un ordinateur avec 16 gigas de RAM et un espace d'adressage de 4 gigas, un programme peut utiliser toute la RAM en utilisant 4 espaces d'adressage distincts. On passe d'un espace d'adressage à l'autre en changeant la correspondance adresse logique-physique. L'inconvénient est que la compatibilité logicielle est assez mauvaise. Modifier l'OS ne suffit pas, les programmeurs doivent impérativement concevoir leurs programmes pour qu'ils utilisent explicitement plusieurs espaces d'adressage.
Les deux implémentations font usage des adresses logiques homonymes, mais à l'intérieur d'un même processus. Pour rappel, cela veut dire qu'une adresse logique correspond à des adresses physiques différentes. Rien d'étonnant vu qu'on utilise plusieurs espaces d'adressage, comme pour l'abstraction des processus, sauf que cette fois-ci, on a plusieurs espaces d'adressage par processus. Prenons l'exemple où on a 8 gigas de RAM sur un processeur 32 bits, dont l'espace d'adressage ne gère que 4 gigas. L'idée est qu'une adresse correspondra à une adresse dans les premiers 4 gigas, ou dans les seconds 4 gigas. L'adresse logique X correspondra d'abord à une adresse physique dans les premiers 4 gigas, puis à une adresse physique dans les seconds 4 gigas.
===La protection mémoire===
La '''protection mémoire''' regroupe des techniques très différentes les unes des autres, qui visent à améliorer la sécurité des programmes et des systèmes d'exploitation. Elles visent à empêcher de lire, d'écrire ou d'exécuter certaines portions de mémoire. Sans elle, les programmes peuvent techniquement lire ou écrire les données des autres, ce qui causent des situations non-prévues par le programmeur, avec des conséquences qui vont d'un joli plantage à des failles de sécurité dangereuses.
La première technique de protection mémoire est l{{'}}'''isolation des processus''', qu'on a vue plus haut. Elle garantit que chaque programme n'a accès qu'à certaines portions dédiées de la mémoire et rend le reste de la mémoire inaccessible en lecture et en écriture. Le système d'exploitation attribue à chaque programme une ou plusieurs portions de mémoire rien que pour lui, auquel aucun autre programme ne peut accéder. Un tel programme, isolé des autres, s'appelle un '''processus''', d'où le nom de cet objectif. Toute tentative d'accès à une partie de la mémoire non autorisée déclenche une exception matérielle (rappelez-vous le chapitre sur les interruptions) qui est traitée par une routine du système d'exploitation. Généralement, le programme fautif est sauvagement arrêté et un message d'erreur est affiché à l'écran.
La '''protection de l'espace exécutable''' empêche d’exécuter quoique ce soit provenant de certaines zones de la mémoire. En effet, certaines portions de la mémoire sont censées contenir uniquement des données, sans aucun programme ou code exécutable. Cependant, des virus informatiques peuvent se cacher dedans et d’exécuter depuis celles-ci. Ou encore, des failles de sécurités peuvent permettre à un attaquant d'injecter du code exécutable malicieux dans des données, ce qui peut lui permettre de lire les données manipulées par un programme, prendre le contrôle de la machine, injecter des virus, ou autre. Pour éviter cela, le système d'exploitation peut marquer certaines zones mémoire comme n'étant pas exécutable. Toute tentative d’exécuter du code localisé dans ces zones entraîne la levée d'une exception ou d'une erreur et le système d'exploitation réagit en conséquence. Là encore, le processeur doit détecter les exécutions non autorisées.
D'autres méthodes de protection mémoire visent à limiter des actions dangereuses. Pour cela, le processeur et l'OS gèrent des '''droits d'accès''', qui interdisent certaines actions pour des programmes non-autorisés. Lorsqu'on exécute une opération interdite, le système d’exploitation et/ou le processeur réagissent en conséquence. La première technique de ce genre n'est autre que la séparation entre espace noyau et utilisateur, vue dans le chapitre sur les interruptions. Mais il y en a d'autres, comme nous le verrons dans ce chapitre.
==La MMU==
La traduction des adresses logiques en adresses physiques se fait par un circuit spécialisé, appelé la '''''Memory Management Unit''''' (MMU). De nos jours, elle est intégrée dans le processeur, à la suite de l'unité mémoire. Mais il a existé des processeurs avec une MMU externe, soudée sur la carte mère.
La traduction des adresses logiques en adresses physiques demande d'accéder à des données sont en mémoire RAM, qui sont gérés par le système d'exploitation. Aussi, les processeurs modernes incorporent des mémoires caches appelées des '''''Translation Lookaside Buffers''''', ou encore TLB. Nous nous pouvons pas parler des TLB pour le moment, car nous n'avons pas encore abordé le chapitre sur les mémoires caches, mais un chapitre entier sera dédié aux TLB d'ici peu.
[[File:MMU principle updated.png|centre|vignette|upright=2|MMU.]]
===Les MMU intégrées au processeur===
D'ordinaire, la MMU est intégrée au processeur. Et elle peut l'être de deux manières. La première en fait un circuit séparé, relié au bus d'adresse. La seconde fusionne la MMU avec l'unité de calcul d'adresse. La première solution est surtout utilisée avec une technique d'abstraction mémoire appelée la pagination, alors que l'autre l'est avec une autre méthode appelée la segmentation. La raison est que la traduction d'adresse avec la segmentation est assez simple : elle demande d'additionner le contenu d'un registre avec l'adresse logique, ce qui est le genre de calcul qu'une unité de calcul d'adresse sait déjà faire. La fusion est donc assez évidente.
Pour donner un exemple, l'Intel 8086 fusionnait l'unité de calcul d'adresse et la MMU. Précisément, il utilisait un même additionneur pour incrémenter le ''program counter'' et effectuer des calculs d'adresse liés à la segmentation. Il aurait été logique d'ajouter les pointeurs de pile avec, mais ce n'était pas possible. La raison est que le pointeur de pile ne peut pas être envoyé directement sur le bus d'adresse, vu qu'il doit passer par une phase de traduction en adresse physique liée à la segmentation.
[[File:80186 arch.png|centre|vignette|upright=2|Intel 8086, microarchitecture.]]
===Les MMU séparées du processeur, sur la carte mère===
Avant d'être intégrées au processeur, les MMU étaient des circuits séparés, placés sur la carte mère, entre le processeur et la mémoire. Elle étaient connectées en plein milieu du bus d'adresse. A savoir que le bus d'adresse entrait dans la MMU et en ressortait. Le processeur envoyait des adresses à la MMU, qui renvoyait une adresse physique. La MMU était facultative, à savoir que le système pouvait fonctionner sans MMU d'installée, mais l'abstraction mémoire n’était alors pas disponible.
Par exemple, les processeurs Motorola 68000 et 68010 pouvaient être combinés avec une MMU de type Motorola 68451. Elle supportait des versions simplifiées de la segmentation et de la pagination. Au minimum, elle ajoutait un support de la protection mémoire contre certains accès non-autorisés. La gestion de la mémoire virtuelle proprement dit n'était possible que si le processeur utilisé était un Motorola 68010, en raison de la manière dont le 68000 gérait ses accès mémoire. La MMU 68451 gérait un espace d'adressage de 16 mébioctets, découpé en maximum 32 pages/segments. On pouvait dépasser cette limite de 32 segments/pages en combinant plusieurs 68451.
Le Motorola 68851 était une MMU qui était prévue pour fonctionner de paire avec le Motorola 68020. Elle gérait la pagination pour un espace d'adressage de 32 bits.
Les processeurs suivants, les 68030, 68040, et 68060, avaient une MMU interne au processeur.
==La relocation matérielle==
Pour rappel, les systèmes d'exploitation moderne permettent de lancer plusieurs programmes en même temps et les laissent se partager la mémoire. Dans le cas le plus simple, qui n'est pas celui des OS modernes, le système d'exploitation découpe la mémoire en blocs d'adresses contiguës qui sont appelés des '''segments''', ou encore des ''partitions mémoire''. Les segments correspondent à un bloc de mémoire RAM. C'est-à-dire qu'un segment de 259 mébioctets sera un segment continu de 259 mébioctets dans la mémoire physique comme dans la mémoire logique. Dans ce qui suit, un segment contient un programme en cours d'exécution, comme illustré ci-dessous.
[[File:CPT Memory Addressable.svg|centre|vignette|upright=2|Espace d'adressage segmenté.]]
Le système d'exploitation mémorise la position de chaque segment en mémoire, ainsi que d'autres informations annexes. Le tout est regroupé dans la '''table de segment''', un tableau dont chaque case est attribuée à un programme/segment. La table des segments est un tableau numéroté, chaque segment ayant un numéro qui précise sa position dans le tableau. Chaque case, chaque entrée, contient un '''descripteur de segment''' qui regroupe plusieurs informations sur le segment : son adresse de base, sa taille, diverses informations.
===La relocation avec la relocation matérielle : le registre de base===
Un segment peut être placé n'importe où en RAM physique et sa position en RAM change à chaque exécution. Le programme est chargé à une adresse, celle du début du segment, qui change à chaque chargement du programme. Et toutes les adresses utilisées par le programme doivent être corrigées lors du chargement du programme, généralement par l'OS. Cette correction s'appelle la '''relocation''', et elle consiste à ajouter l'adresse de début du segment à chaque adresse manipulée par le programme.
[[File:Relocation assistée par matériel.png|centre|vignette|upright=2.5|Relocation.]]
La relocation matérielle fait que la relocation est faite par le processeur, pas par l'OS. La relocation est intégrée dans le processeur par l'intégration d'un registre : le '''registre de base''', aussi appelé '''registre de relocation'''. Il mémorise l'adresse à laquelle commence le segment, la première adresse du programme. Pour effectuer la relocation, le processeur ajoute automatiquement l'adresse de base à chaque accès mémoire, en allant la chercher dans le registre de relocation.
[[File:Registre de base de segment.png|centre|vignette|upright=2|Registre de base de segment.]]
Le processeur s'occupe de la relocation des segments et le programme compilé n'en voit rien. Pour le dire autrement, les programmes manipulent des adresses logiques, qui sont traduites par le processeur en adresses physiques. La traduction se fait en ajoutant le contenu du registre de relocation à l'adresse logique. De plus, cette méthode fait que chaque programme a son propre espace d'adressage.
[[File:CPU created logical address presentation.png|centre|vignette|upright=2|Traduction d'adresse avec la relocation matérielle.]]
Le système d'exploitation mémorise les adresses de base pour chaque programme, dans la table des segments. Le registre de base est mis à jour automatiquement lors de chaque changement de segment. Pour cela, le registre de base est accessible via certaines instructions, accessibles en espace noyau, plus rarement en espace utilisateur. Le registre de segment est censé être adressé implicitement, vu qu'il est unique. Si ce n'est pas le cas, il est possible d'écrire dans ce registre de segment, qui est alors adressable.
===La protection mémoire avec la relocation matérielle : le registre limite===
Sans restrictions supplémentaires, la taille maximale d'un segment est égale à la taille complète de l'espace d'adressage. Sur les processeurs 32 bits, un segment a une taille maximale de 2^32 octets, soit 4 gibioctets. Mais il est possible de limiter la taille du segment à 2 gibioctets, 1 gibioctet, 64 Kibioctets, ou toute autre taille. La limite est définie lors de la création du segment, mais elle peut cependant évoluer au cours de l'exécution du programme, grâce à l'allocation mémoire.
Le processeur vérifie à chaque accès mémoire que celui-ci se fait bien dans le segment, qu'il ne déborde pas en-dehors. C'est possible qu'une adresse calculée sorte du segment, à la suite d'un bug ou d'une erreur de programmation, voire pire. Et le processeur doit éviter de tels '''débordements de segments'''.
A chaque accès mémoire, le processeur compare l'adresse accédée et vérifie qu'elle est bien dans le segment. Pour cela, il y a deux solutions. La première part du principe que le segment est placé en mémoire entre l'adresse de base et l'adresse limite. Il suffit de mémoriser l'adresse limite, l'adresse physique à ne pas dépasser. Une autre solution mémorise la taille du segment. La table des segments doit donc mémoriser, en plus de l'adresse de base : soit l'adresse maximale du segment, soit la taille du segment. D'autres informations peuvent être ajoutées, comme on le verra plus tard, mais cela complexifie la table des segments.
De plus, le processeur se voit ajouter un '''registre limite''', qui mémorise soit la taille du segment, soit l'adresse limite. Les deux registres, base et limite, sont utilisés pour vérifier si un programme qui lit/écrit de la mémoire en-dehors de son segment attitré : au-delà pour le registre limite, en-deça pour le registre de base. Le processeur vérifie pour chaque accès mémoire ne déborde pas au-delà du segment qui lui est allouée, ce qui n'arrive que si l'adresse d'accès dépasse la valeur du registre limite. Pour les accès en-dessous du segment, il suffit de vérifier si l'addition de relocation déborde, tout débordement signifiant erreur de protection mémoire.
[[File:Registre limite.png|centre|vignette|upright=2|Registre limite]]
Utiliser la taille du segment a de nombreux avantages. L'un d'entre eux se manifeste quand on déplace un segment en mémoire RAM. Le descripteur doit alors être mis à jour, et c'est plus facile quand on utilise la taille du segment. Si on utilise l'adresse limite, il faut mettre à jour à la fois l'adresse de base et l'adresse limite, dans le descripteur. En utilisant la taille, seule l'adresse de base doit être modifiée, vu que le segment n'a pas changé de taille. Un autre avantage est lié aux performances, mais nous devons faire un détour pour le comprendre.
La taille du segment est équivalent à l'adresse logique maximale possible. Par exemple, si un segment fait 256 octets, les adresses logiques possibles vont de 0 à 255, 256 est donc à la fois la taille du segment et l'adresse logique à partir de laquelle on déborde du segment. Et cela marche si on remplace 256 par n'importe quelle valeur : vu que le segment commence à l'adresse 0, sa taille en octets indique l'adresse de dépassement. Interpréter la taille du segment comme une adresse logique fait que les tests avec le registre limite sont plus performants, voyons pourquoi.
En utilisant l'adresse physique limite, on doit faire la relocation, puis comparer l'adresse calculée avec l'adresse limite. Le calcul d'adresse doit se faire avant la vérification. En utilisant la taille, on doit comparer l'adresse logique avec la taille du segment. On peut alors faire le test de débordement avant ou pendant la relocation. Les deux peuvent être faits en parallèle, dans deux circuits distincts, ce qui améliore un peu le temps d'un accès mémoire. Quelques processeurs en ont profité, mais on verra cela dans la section sur la segmentation.
[[File:Comparaison entre adresse limite physique et logique.png|centre|vignette|upright=2|Comparaison entre adresse limite physique et logique]]
Les registres de base et limite sont altérés uniquement par le système d'exploitation et ne sont accessibles qu'en espace noyau. Lorsque le système d'exploitation charge un programme, ou reprend son exécution, il charge les adresses de début/fin du segment dans ces registres. D'ailleurs, ces deux registres doivent être sauvegardés et restaurés lors de chaque interruption. Par contre, et c'est assez évident, ils ne le sont pas lors d'un appel de fonction. Cela fait une différence de plus entre interruption et appels de fonctions.
: Il faut noter que le registre limite et le registre de base sont parfois fusionnés en un seul registre, qui contient un descripteur de segment tout entier.
Pour information, la relocation matérielle avec un registre limite a été implémentée sur plusieurs processeurs assez anciens, notamment sur les anciens supercalculateurs de marque CDC. Un exemple est le fameux CDC 6600, qui implémentait cette technique.
===La mémoire virtuelle avec la relocation matérielle===
Il est possible d'implémenter la mémoire virtuelle avec la relocation matérielle. Pour cela, il faut swapper des segments entiers sur le disque dur. Les segments sont placés en mémoire RAM et leur taille évolue au fur et à mesure que les programmes demandent du rab de mémoire RAM. Lorsque la mémoire est pleine, ou qu'un programme demande plus de mémoire que disponible, des segments entiers sont sauvegardés dans le ''swapfile'', pour faire de la place.
Faire ainsi de demande juste de mémoriser si un segment est en mémoire RAM ou non, ainsi que la position des segments swappés dans le ''swapfile''. Pour cela, il faut modifier la table des segments, afin d'ajouter un '''bit de swap''' qui précise si le segment en question est swappé ou non. Lorsque le système d'exploitation veut swapper un segment, il le copie dans le ''swapfile'' et met ce bit à 1. Lorsque l'OS recharge ce segment en RAM, il remet ce bit à 0. La gestion de la position des segments dans le ''swapfile'' est le fait d'une structure de données séparée de la table des segments.
L'OS exécute chaque programme l'un après l'autre, à tour de rôle. Lorsque le tour d'un programme arrive, il consulte la table des segments pour récupérer les adresses de base et limite, mais il vérifie aussi le bit de swap. Si le bit de swap est à 0, alors l'OS se contente de charger les adresses de base et limite dans les registres adéquats. Mais sinon, il démarre une routine d'interruption qui charge le segment voulu en RAM, depuis le ''swapfile''. C'est seulement une fois le segment chargé que l'on connait son adresse de base/limite et que le chargement des registres de relocation peut se faire.
Un défaut évident de cette méthode est que l'on swappe des programmes entiers, qui sont généralement assez imposants. Les segments font généralement plusieurs centaines de mébioctets, pour ne pas dire plusieurs gibioctets, à l'époque actuelle. Ils étaient plus petits dans l'ancien temps, mais la mémoire était alors plus lente. Toujours est-il que la copie sur le disque dur des segments est donc longue, lente, et pas vraiment compatible avec le fait que les programmes s'exécutent à tour de rôle. Et ca explique pourquoi la relocation matérielle n'est presque jamais utilisée avec de la mémoire virtuelle.
===L'extension d'adressage avec la relocation matérielle===
Passons maintenant à la dernière fonctionnalité implémentable avec la traduction d'adresse : l'extension d'adressage. Elle permet d'utiliser plus de mémoire que ne le permet l'espace d'adressage. Par exemple, utiliser plus de 64 kibioctets de mémoire sur un processeur 16 bits. Pour cela, les adresses envoyées à la mémoire doivent être plus longues que les adresses gérées par le processeur.
L'extension des adresses se fait assez simplement avec la relocation matérielle : il suffit que le registre de base soit plus long. Prenons l'exemple d'un processeur aux adresses de 16 bits, mais qui est reliée à un bus d'adresse de 24 bits. L'espace d'adressage fait juste 64 kibioctets, mais le bus d'adresse gère 16 mébioctets de RAM. On peut utiliser les 16 mébioctets de RAM à une condition : que le registre de base fasse 24 bits, pas 16.
Un défaut de cette approche est qu'un programme ne peut pas utiliser plus de mémoire que ce que permet l'espace d'adressage. Mais par contre, on peut placer chaque programme dans des portions différentes de mémoire. Imaginons par exemple que l'on ait un processeur 16 bits, mais un bus d'adresse de 20 bits. Il est alors possible de découper la mémoire en 16 blocs de 64 kibioctets, chacun attribué à un segment/programme, qu'on sélectionne avec les 4 bits de poids fort de l'adresse. Il suffit de faire démarrer les segments au bon endroit en RAM, et cela demande juste que le registre de base le permette. C'est une sorte d'émulation de la commutation de banques.
==La segmentation en mode réel des processeurs x86==
Avant de passer à la suite, nous allons voir la technique de segmentation de l'Intel 8086, un des tout premiers processeurs 16 bits. Il s'agissait d'une forme très simple de segmentation, sans aucune forme de protection mémoire, ni même de mémoire virtuelle, ce qui le place à part des autres formes de segmentation. Il s'agit d'une amélioration de la relocation matérielle, qui avait pour but de permettre d'utiliser plus de 64 kibioctets de mémoire, ce qui était la limite maximale sur les processeurs 16 bits de l'époque.
Par la suite, la segmentation s'améliora et ajouta un support complet de la mémoire virtuelle et de la protection mémoire. L'ancienne forme de segmentation fut alors appelé le '''mode réel''', et la nouvelle forme de segmentation fut appelée le '''mode protégé'''. Le mode protégé rajoute la protection mémoire, en ajoutant des registres limite et une gestion des droits d'accès aux segments, absents en mode réel. De plus, il ajoute un support de la mémoire virtuelle grâce à l'utilisation d'une des segments digne de ce nom, table qui est absente en mode réel ! Pour le moment, voyons le mode réel.
===Les segments en mode réel===
[[File:Typical computer data memory arrangement.png|vignette|upright=0.5|Typical computer data memory arrangement]]
La segmentation en mode réel sépare la pile, le tas, le code machine et les données constantes dans quatre segments distincts.
* Le segment '''''text''''', qui contient le code machine du programme, de taille fixe.
* Le segment '''''data''''' contient des données de taille fixe qui occupent de la mémoire de façon permanente, des constantes, des variables globales, etc.
* Le segment pour la '''pile''', de taille variable.
* le reste est appelé le '''tas''', de taille variable.
Un point important est que sur ces processeurs, il n'y a pas de table des segments proprement dit. Chaque programme gére de lui-même les adresses de base des segments qu'il manipule. Il n'est en rien aidé par une table des segments gérée par le système d'exploitation.
===Les registres de segments en mode réel===
Chaque segment subit la relocation indépendamment des autres. Pour cela, le processeur intégre plusieurs registres de base, un par segment. Notons que cette solution ne marche que si le nombre de segments par programme est limité, à une dizaine de segments tout au plus. Les processeurs x86 utilisaient cette méthode, et n'associaient que 4 à 6 registres de segments par programme.
Les processeurs 8086 et le 286 avaient quatre registres de segment : un pour le code, un autre pour les données, et un pour la pile, le quatrième étant un registre facultatif laissé à l'appréciation du programmeur. Ils sont nommés CS (''code segment''), DS (''data segment''), SS (''Stack segment''), et ES (''Extra segment''). Le 386 rajouta deux registres, les registres FS et GS, qui sont utilisés pour les segments de données. Les processeurs post-386 ont donc 6 registres de segment.
Les registres CS et SS sont adressés implicitement, en fonction de l'instruction exécutée. Les instructions de la pile manipulent le segment associé à la pile, le chargement des instructions se fait dans le segment de code, les instructions arithmétiques et logiques vont chercher leurs opérandes sur le tas, etc. Et donc, toutes les instructions sont chargées depuis le segment pointé par CS, les instructions de gestion de la pile (PUSH et POP) utilisent le segment pointé par SS.
Les segments DS et ES sont, eux aussi, adressés implicitement. Pour cela, les instructions LOAD/STORE sont dupliquées : il y a une instruction LOAD pour le segment DS, une autre pour le segment ES. D'autres instructions lisent leurs opérandes dans un segment par défaut, mais on peut changer ce choix par défaut en précisant le segment voulu. Un exemple est celui de l'instruction CMPSB, qui compare deux octets/bytes : le premier est chargé depuis le segment DS, le second depuis le segment ES.
Un autre exemple est celui de l'instruction MOV avec un opérande en mémoire. Elle lit l'opérande en mémoire depuis le segment DS par défaut. Il est possible de préciser le segment de destination si celui-ci n'est pas DS. Par exemple, l'instruction MOV [A], AX écrit le contenu du registre AX dans l'adresse A du segment DS. Par contre, l'instruction MOV ES:[A], copie le contenu du registre AX das l'adresse A, mais dans le segment ES.
===La traduction d'adresse en mode réel===
La segmentation en mode réel a pour seul but de permettre à un programme de dépasser la limite des 64 KB autorisée par les adresses de 16 bits. L'idée est que chaque segment a droit à son propre espace de 64 KB. On a ainsi 64 Kb pour le code machine, 64 KB pour la pile, 64 KB pour un segment de données, etc. Les registres de segment mémorisaient la base du segment, les adresses calculées par l'ALU étant des ''offsets''. Ce sont tous des registres de 16 bits, mais ils ne mémorisent pas des adresses physiques de 16 bits, comme nous allons le voir.
[[File:Table des segments dans un banc de registres.png|centre|vignette|upright=2|Table des segments dans un banc de registres.]]
L'Intel 8086 utilisait des adresses de 20 bits, ce qui permet d'adresser 1 mébioctet de RAM. Vous pouvez vous demander comment on peut obtenir des adresses de 20 bits alors que les registres de segments font tous 16 bits ? Cela tient à la manière dont sont calculées les adresses physiques. Le registre de segment n'est pas additionné tel quel avec le décalage : à la place, le registre de segment est décalé de 4 rangs vers la gauche. Le décalage de 4 rangs vers la gauche fait que chaque segment a une adresse qui est multiple de 16. Le fait que le décalage soit de 16 bits fait que les segments ont une taille de 64 kibioctets.
{|class="wikitable"
|-
| <code> </code><code style="background:#DED">0000 0110 1110 1111</code><code>0000</code>
| Registre de segment -
| 16 bits, décalé de 4 bits vers la gauche
|-
| <code>+ </code><code style="background:#DDF">0001 0010 0011 0100</code>
| Décalage/''Offset''
| 16 bits
|-
| colspan="3" |
|-
| <code> </code><code style="background:#FDF">0000 1000 0001 0010 0100</code>
| Adresse finale
| 20 bits
|}
Vous aurez peut-être remarqué que le calcul peut déborder, dépasser 20 bits. Mais nous reviendrons là-dessus plus bas. L'essentiel est que la MMU pour la segmentation en mode réel se résume à quelques registres et des additionneurs/soustracteurs.
Un exemple est l'Intel 8086, un des tout premier processeur Intel. Le processeur était découpé en deux portions : l'interface mémoire et le reste du processeur. L'interface mémoire est appelée la '''''Bus Interface Unit''''', et le reste du processeur est appelé l{{'}}'''''Execution Unit'''''. L'interface mémoire contenait les registres de segment, au nombre de 4, ainsi qu'un additionneur utilisé pour traduire les adresses logiques en adresses physiques. Elle contenait aussi une file d'attente où étaient préchargées les instructions.
Sur le 8086, la MMU est fusionnée avec les circuits de gestion du ''program counter''. Les registres de segment sont regroupés avec le ''program counter'' dans un même banc de registres, et un additionneur unique est utilisé à la fois à incrémenter le ''program counter'' et pour gérer la segmentation. L'additionneur est donc mutualisé entre segmentation et ''program counter''. En somme, il n'y a pas vraiment de MMU dédiée, mais un super-circuit en charge du Fetch et de la mémoire virtuelle, ainsi que du préchargement des instructions. Nous en reparlerons au chapitre suivant.
[[File:80186 arch.png|centre|vignette|upright=2.5|Architecture du 8086, du 80186 et de ses variantes.]]
La MMU du 286 était fusionnée avec l'unité de calcul d'adresse. Elle contient les registres de segments, un comparateur pour détecter les accès hors-segment, et plusieurs additionneurs. Il y a un additionneur pour les calculs d'adresse proprement dit, suivi d'un additionneur pour la relocation.
[[File:Intel i80286 arch.svg|centre|vignette|upright=3|Intel i80286 arch]]
===La segmentation en mode réel accepte plusieurs segments de code/données===
Les programmes peuvent parfaitement répartir leur code machine dans plusieurs segments de code. La limite de 64 KB par segment est en effet assez limitante, et il n'était pas rare qu'un programme stocke son code dans deux ou trois segments. Il en est de même avec les données, qui peuvent être réparties dans deux ou trois segments séparés. La seule exception est la pile : elle est forcément dans un segment unique et ne peut pas dépasser 64 KB.
Pour gérer plusieurs segments de code/donnée, il faut changer de segment à la volée suivant les besoins, en modifiant les registres de segment. Il s'agit de la technique de '''commutation de segment'''. Pour cela, tous les registres de segment, à l'exception de CS, peuvent être altérés par une instruction d'accès mémoire, soit avec une instruction MOV, soit en y copiant le sommet de la pile avec une instruction de dépilage POP. L'absence de sécurité fait que la gestion de ces registres est le fait du programmeur, qui doit redoubler de prudence pour ne pas faire n'importe quoi.
Pour le code machine, le répartir dans plusieurs segments posait des problèmes au niveau des branchements. Si la plupart des branchements sautaient vers une instruction dans le même segment, quelques rares branchements sautaient vers du code machine dans un autre segment. Intel avait prévu le coup et disposait de deux instructions de branchement différentes pour ces deux situations : les '''''near jumps''''' et les '''''far jumps'''''. Les premiers sont des branchements normaux, qui précisent juste l'adresse à laquelle brancher, qui correspond à la position de la fonction dans le segment. Les seconds branchent vers une instruction dans un autre segment, et doivent préciser deux choses : l'adresse de base du segment de destination, et la position de la destination dans le segment. Le branchement met à jour le registre CS avec l'adresse de base, avant de faire le branchement. Ces derniers étaient plus lents, car on n'avait pas à changer de segment et mettre à jour l'état du processeur.
Il y avait la même pour l'instruction d'appel de fonction, avec deux versions de cette instruction. La première version, le '''''near call''''' est un appel de fonction normal, la fonction appelée est dans le segment en cours. Avec la seconde version, le '''''far call''''', la fonction appelée est dans un segment différent. L'instruction a là aussi besoin de deux opérandes : l'adresse de base du segment de destination, et la position de la fonction dans le segment. Un ''far call'' met à jour le registre CS avec l'adresse de base, ce qui fait que les ''far call'' sont plus lents que les ''near call''. Il existe aussi la même chose, pour les instructions de retour de fonction, avec une instruction de retour de fonction normale et une instruction de retour qui renvoie vers un autre segment, qui sont respectivement appelées '''''near return''''' et '''''far return'''''. Là encore, il faut préciser l'adresse du segment de destination dans le second cas.
La même chose est possible pour les segments de données. Sauf que cette fois-ci, ce sont les pointeurs qui sont modifiés. pour rappel, les pointeurs sont, en programmation, des variables qui contiennent des adresses. Lors de la compilation, ces pointeurs sont placés soit dans un registre, soit dans les instructions (adressage absolu), ou autres. Ici, il existe deux types de pointeurs, appelés '''''near pointer''''' et '''''far pointer'''''. Vous l'avez deviné, les premiers sont utilisés pour localiser les données dans le segment en cours d'utilisation, alors que les seconds pointent vers une donnée dans un autre segment. Là encore, la différence est que le premier se contente de donner la position dans le segment, alors que les seconds rajoutent l'adresse de base du segment. Les premiers font 16 bits, alors que les seconds en font 32 : 16 bits pour l'adresse de base et 16 pour l{{'}}''offset''.
===L'occupation de l'espace d'adressage par les segments===
Nous venons de voir qu'un programme pouvait utiliser plus de 4-6 segments, avec la commutation de segment. Mais d'autres programmes faisaient l'inverse, à savoir qu'ils se débrouillaient avec seulement 1 ou 2 segments. Suivant le nombre de segments utilisés, la configuration des registres n'était pas la même. Les configurations possibles sont appelées des ''modèle mémoire'', et il y en a en tout 6. En voici la liste :
{| class="wikitable"
|-
! Modèle mémoire !! Configuration des segments !! Configuration des registres || Pointeurs utilisés || Branchements utilisés
|-
| Tiny* || Segment unique pour tout le programme || CS=DS=SS || ''near'' uniquement || ''near'' uniquement
|-
| Small || Segment de donnée séparé du segment de code, pile dans le segment de données || DS=SS || ''near'' uniquement || ''near'' uniquement
|-
| Medium || Plusieurs segments de code unique, un seul segment de données || CS, DS et SS sont différents || ''near'' et ''far'' || ''near'' uniquement
|-
| Compact || Segment de code unique, plusieurs segments de données || CS, DS et SS sont différents || ''near'' uniquement || ''near'' et ''far''
|-
| Large || Plusieurs segments de code, plusieurs segments de données || CS, DS et SS sont différents || ''near'' et ''far'' || ''near'' et ''far''
|}
Un programme est censé utiliser maximum 4-6 segments de 64 KB, ce qui permet d'adresser maximum 64 * 6 = 384 KB de RAM, soit bien moins que le mébioctet de mémoire théoriquement adressable. Mais ce défaut est en réalité contourné par la commutation de segment, qui permettait d'adresser la totalité de la RAM si besoin. Une second manière de contourner cette limite est que plusieurs processus peuvent s'exécuter sur un seul processeur, si l'OS le permet. Ce n'était pas le cas à l'époque du DOS, qui était un OS mono-programmé, mais c'était en théorie possible. La limite est de 6 segments par programme/processus, en exécuter plusieurs permet d'utiliser toute la mémoire disponible rapidement.
[[File:Overlapping realmode segments.svg|vignette|Segments qui se recouvrent en mode réel.]]
Vous remarquerez qu'avec des registres de segments de 16 bits, on peut gérer 65536 segments différents, chacun de 64 KB. Et 65 536 segments de 64 kibioctets, ça ne rentre pas dans le mébioctet de mémoire permis avec des adresses de 20 bits. La raison est que plusieurs couples segment+''offset'' pointent vers la même adresse. En tout, chaque adresse peut être adressée par 4096 couples segment+''offset'' différents.
L'avantage de cette méthode est que des segments peuvent se recouvrir, à savoir que la fin de l'un se situe dans le début de l'autre, comme illustré ci-contre. Cela permet en théorie de partager de la mémoire entre deux processus. Mais la technique est tout sauf pratique et est donc peu utilisée. Elle demande de placer minutieusement les segments en RAM, et les données à partager dans les segments. En pratique, les programmeurs et OS utilisent des segments qui ne se recouvrent pas et sont disjoints en RAM.
Le nombre maximal de segments disjoints se calcule en prenant la taille de la RAM, qu'on divise par la taille d'un segment. Le calcul donne : 1024 kibioctets / 64 kibioctets = 16 segments disjoints. Un autre calcul prend le nombre de segments divisé par le nombre d'adresses aliasées, ce qui donne 65536 / 4096 = 16. Seulement 16 segments, c'est peu. En comptant les segments utilisés par l'OS et ceux utilisés par le programme, la limite est vite atteinte si le programme utilise la commutation de segment.
===Le mode réel sur les 286 et plus : la ligne d'adresse A20===
Pour résumer, le registre de segment contient des adresses de 20 bits, dont les 4 bits de poids faible sont à 0. Et il se voit ajouter un ''offset'' de 16 bits. Intéressons-nous un peu à l'adresse maximale que l'on peut calculer avec ce système. Nous allons l'appeler l{{'}}'''adresse maximale de segmentation'''. Elle vaut :
{|class="wikitable"
|-
| <code> </code><code style="background:#DED">1111 1111 1111 1111</code><code>0000</code>
| Registre de segment -
| 16 bits, décalé de 4 bits vers la gauche
|-
| <code>+ </code><code style="background:#DDF">1111 1111 1111 1111</code>
| Décalage/''Offset''
| 16 bits
|-
| colspan="3" |
|-
| <code> </code><code style="background:#FDF">1 0000 1111 1111 1110 1111</code>
| Adresse finale
| 20 bits
|}
Le résultat n'est pas l'adresse maximale codée sur 20 bits, car l'addition déborde. Elle donne un résultat qui dépasse l'adresse maximale permis par les 20 bits, il y a un 21ème bit en plus. De plus, les 20 bits de poids faible ont une valeur bien précise. Ils donnent la différence entre l'adresse maximale permise sur 20 bit, et l'adresse maximale de segmentation. Les bits 1111 1111 1110 1111 traduits en binaire donnent 65 519; auxquels il faut ajouter l'adresse 1 0000 0000 0000 0000. En tout, cela fait 65 520 octets adressables en trop. En clair : on dépasse la limite du mébioctet de 65 520 octets. Le résultat est alors très différent selon que l'on parle des processeurs avant le 286 ou après.
Avant le 286, le bus d'adresse faisait exactement 20 bits. Les adresses calculées ne pouvaient pas dépasser 20 bits. L'addition générait donc un débordement d'entier, géré en arithmétique modulaire. En clair, les bits de poids fort au-delà du vingtième sont perdus. Le calcul de l'adresse débordait et retournait au début de la mémoire, sur les 65 520 premiers octets de la mémoire RAM.
[[File:IBM PC Memory areas.svg|vignette|IBM PC Memory Map, la ''High memory area'' est en jaune.]]
Le 80286 en mode réel gère des adresses de base de 24 bits, soit 4 bits de plus que le 8086. Le résultat est qu'il n'y a pas de débordement. Les bits de poids fort sont conservés, même au-delà du 20ème. En clair, la segmentation permettait de réellement adresser 65 530 octets au-delà de la limite de 1 mébioctet. La portion de mémoire adressable était appelé la '''''High memory area''''', qu'on va abrévier en HMA.
{| class="wikitable"
|+ Espace d'adressage du 286
|-
! Adresses en héxadécimal !! Zone de mémoire
|-
| 10 FFF0 à FF FFFF || Mémoire étendue, au-delà du premier mébioctet
|-
| 10 0000 à 10 FFEF || ''High Memory Area''
|-
| 0 à 0F FFFF || Mémoire adressable en mode réel
|}
En conséquence, les applications peuvent utiliser plus d'un mébioctet de RAM, mais au prix d'une rétrocompatibilité imparfaite. Quelques programmes DOS ne marchaient pus à cause de ça. D'autres fonctionnaient convenablement et pouvaient adresser les 65 520 octets en plus.
Pour résoudre ce problème, les carte mères ajoutaient un petit circuit relié au 21ème bit d'adresse, nommé A20 (pas d'erreur, les fils du bus d'adresse sont numérotés à partir de 0). Le circuit en question pouvait mettre à zéro le fil d'adresse, ou au contraire le laisser tranquille. En le forçant à 0, le calcul des adresses déborde comme dans le mode réel des 8086. Mais s'il ne le fait pas, la ''high memory area'' est adressable. Le circuit était une simple porte ET, qui combinait le 21ème bit d'adresse avec un '''signal de commande A20''' provenant d'ailleurs.
Le signal de commande A20 était géré par le contrôleur de clavier, qui était soudé à la carte mère. Le contrôleur en question ne gérait pas que le clavier, il pouvait aussi RESET le processeur, alors gérer le signal de commande A20 n'était pas si problématique. Quitte à avoir un microcontrôleur sur la carte mère, autant s'en servir au maximum... La gestion du bus d'adresse étaitdonc gérable au clavier. D'autres carte mères faisaient autrement et préféraient ajouter un interrupteur, pour activer ou non la mise à 0 du 21ème bit d'adresse.
: Il faut noter que le signal de commande A20 était mis à 1 en mode protégé, afin que le 21ème bit d'adresse soit activé.
Le 386 ajouta deux registres de segment, les registres FS et GS, ainsi que le '''mode ''virtual 8086'''''. Ce dernier permet d’exécuter des programmes en mode réel alors que le système d'exploitation s'exécute en mode protégé. C'est une technique de virtualisation matérielle qui permet d'émuler un 8086 sur un 386. L'avantage est que la compatibilité avec les programmes anciens écrits pour le 8086 est conservée, tout en profitant de la protection mémoire. Tous les processeurs x86 qui ont suivi supportent ce mode virtuel 8086.
==La segmentation avec une table des segments==
La '''segmentation avec une table des segments''' est apparue sur des processeurs assez anciens, le tout premier étant le Burrough 5000. Elle a ensuite été utilisée sur les processeurs x86 des PCs, à partir du 286 d'Intel. Elle est aujourd'hui abandonnée sur les jeux d'instruction x86. Tout comme la segmentation en mode réel, la segmentation attribue plusieurs segments par programmes ! Et cela a des répercutions sur la manière dont la traduction d'adresse est effectuée.
===Pourquoi plusieurs segments par programme ?===
L'utilité d'avoir plusieurs segments par programme n'est pas évidente, mais elle le devient quand on se plonge dans le passé. Dans le passé, les programmeurs devaient faire avec une quantité de mémoire limitée et il n'était pas rare que certains programmes utilisent plus de mémoire que disponible sur la machine. Mais les programmeurs concevaient leurs programmes en fonction.
[[File:Overlay Programming.svg|vignette|upright=1|Overlay Programming]]
L'idée était d'implémenter un système de mémoire virtuelle, mais émulé en logiciel, appelé l{{'}}'''''overlaying'''''. Le programme était découpé en plusieurs morceaux, appelés des ''overlays''. Les ''overlays'' les plus importants étaient en permanence en RAM, mais les autres étaient faisaient un va-et-vient entre RAM et disque dur. Ils étaient chargés en RAM lors de leur utilisation, puis sauvegardés sur le disque dur quand ils étaient inutilisés. Le va-et-vient des ''overlays'' entre RAM et disque dur était réalisé en logiciel, par le programme lui-même. Le matériel n'intervenait pas, comme c'est le cas avec la mémoire virtuelle.
Avec la segmentation, un programme peut utiliser la technique des ''overlays'', mais avec l'aide du matériel. Il suffit de mettre chaque ''overlay'' dans son propre segment, et laisser la segmentation faire. Les segments sont swappés en tout ou rien : on doit swapper tout un segment en entier. L'intérêt est que la gestion du ''swapping'' est grandement facilitée, vu que c'est le système d'exploitation qui s'occupe de swapper les segments sur le disque dur ou de charger des segments en RAM. Pas besoin pour le programmeur de coder quoique ce soit. Par contre, cela demande l'intervention du programmeur, qui doit découper le programme en segments/''overlays'' de lui-même. Sans cela, la segmentation n'est pas très utile.
L{{'}}''overlaying'' est une forme de '''segmentation à granularité grossière''', à savoir que le programme est découpé en segments de grande taille. L'usage classique est d'avoir un segment pour la pile, un autre pour le code exécutable, un autre pour le reste. Éventuellement, on peut découper les trois segments précédents en deux ou trois segments, rarement au-delà. Les segments sont alors peu nombreux, guère plus d'une dizaine par programme. D'où le terme de ''granularité grossière''.
La '''segmentation à granularité fine''' pousse le concept encore plus loin. Avec elle, il y a idéalement un segment par entité manipulée par le programme, un segment pour chaque structure de donnée et/ou chaque objet. Par exemple, un tableau aura son propre segment, ce qui est idéal pour détecter les accès hors tableau. Pour les listes chainées, chaque élément de la liste aura son propre segment. Et ainsi de suite, chaque variable agrégée (non-primitive), chaque structure de donnée, chaque objet, chaque instance d'une classe, a son propre segment. Diverses fonctionnalités supplémentaires peuvent être ajoutées, ce qui transforme le processeur en véritable processeur orienté objet, mais passons ces détails pour le moment.
Vu que les segments correspondent à des objets manipulés par le programme, on peut deviner que leur nombre évolue au cours du temps. En effet, les programmes modernes peuvent demander au système d'exploitation du rab de mémoire pour allouer une nouvelle structure de données. Avec la segmentation à granularité fine, cela demande d'allouer un nouveau segment à chaque nouvelle allocation mémoire, à chaque création d'une nouvelle structure de données ou d'un objet. De plus, les programmes peuvent libérer de la mémoire, en supprimant les structures de données ou objets dont ils n'ont plus besoin. Avec la segmentation à granularité fine, cela revient à détruire le segment alloué pour ces objets/structures de données. Le nombre de segments est donc dynamique, il change au cours de l'exécution du programme.
===Les tables de segments avec la segmentation===
La présence de plusieurs segments par programme a un impact sur la table des segments. Avec la relocation matérielle, elle conte nait un segment par programme. Chaque entrée, chaque ligne de la table des segment, mémorisait l'adresse de base, l'adresse limite, un bit de présence pour la mémoire virtuelle et des autorisations liées à la protection mémoire. Avec la segmentation, les choses sont plus compliquées, car il y a plusieurs segments par programme. Les entrées ne sont pas modifiées, mais elles sont organisées différemment.
Avec cette forme de segmentation, la table des segments doit respecter plusieurs contraintes. Premièrement, il y a plusieurs segments par programmes. Deuxièmement, le nombre de segments est variable : certains programmes se contenteront d'un seul segment, d'autres de dizaine, d'autres plusieurs centaines, etc. Il y a typiquement deux manières de faire : soit utiliser une table des segments uniques, utiliser une table des segment par programme.
Il est possible d'utiliser une table des segment unique qui mémorise tous les segments de tous les processus, système d'exploitation inclut. On parle alors de '''table des segment globale'''. Mais cette solution n'est pas utilisée avec la segmentation proprement dite. Elle est utilisée sur les architectures à capacité qu'on détaillera vers la fin du chapitre, dans une section dédiée. À la place, la segmentation utilise une table de segment par processus/programme, chacun ayant une '''table des segment locale'''.
Dans les faits, les choses sont plus compliquées. Le système d'exploitation doit savoir où se trouvent les tables de segment locale pour chaque programme. Pour cela, il a besoin d'utiliser une table de segment globale, dont chaque entrée pointe non pas vers un segment, mais vers une table de segment locale. Lorsque l'OS effectue une commutation de contexte, il lit la table des segment globale, pour récupérer un pointeur vers celle-ci. Ce pointeur est alors chargé dans un registre du processeur, qui mémorise l'adresse de la table locale, ce qui sert lors des accès mémoire.
Une telle organisation fait que les segments d'un processus/programme sont invisibles pour les autres, il y a une certaine forme de sécurité. Un programme ne connait que sa table de segments locale, il n'a pas accès directement à la table des segments globales. Tout accès mémoire se passera à travers la table de segment locale, il ne sait pas où se trouvent les autres tables de segment locales.
Les processeurs x86 sont dans ce cas : ils utilisent une table de segment globale couplée à autant de table des segments qu'il y a de processus en cours d'exécution. La table des segments globale s'appelle la '''''Global Descriptor Table''''' et elle peut contenir 8192 segments maximum, ce qui permet le support de 8192 processus différents. Les tables de segments locales sont appelées les '''''Local Descriptor Table''''' et elles font aussi 8192 segments maximum, ce qui fait 8192 segments par programme maximum. Il faut noter que la table de segment globale peut mémoriser des pointeurs vers les routines d'interruption, certaines données partagées (le tampon mémoire pour le clavier) et quelques autres choses, qui n'ont pas leur place dans les tables de segment locales.
===La relocation avec la segmentation===
La table des segments locale mémorise les adresses de base et limite de chaque segment, ainsi que d'autres méta-données. Les informations pour un segment sont regroupés dans un '''descripteur de segment''', qui est codé sur plusieurs octets, et qui regroupe : adresse de base, adresse limite, bit de présence en RAM, méta-données de protection mémoire.
La table des segments est un tableau dans lequel les descripteurs de segment sont placés les uns à la suite des autres en mémoire RAM. La table des segments est donc un tableau de segment. Les segments d'un programme sont numérotés, le nombre s'appelant un '''indice de segment''', appelé '''sélecteur de segment''' dans la terminologie Intel. L'indice de segment n'est autre que l'indice du segment dans ce tableau.
[[File:Global Descriptor table.png|centre|vignette|upright=2|Table des segments locale.]]
Il n'y a pas de registre de segment proprement dit, qui mémoriserait l'adresse de base. À la place, les segments sont adressés de manière indirecte. À la place, les registres de segment mémorisent des sélecteurs de segment. Ils sont utilisés pour lire l'adresse de base/limite dans la table de segment en mémoire RAM. Pour cela, un registre mémorise l'adresse de la table de segment locale, sa position en mémoire RAM.
Toute lecture ou écriture se fait en deux temps, en deux accès mémoire, consécutifs. Premièrement, le numéro de segment est utilisé pour adresser la table des segment. La lecture récupère alors un pointeur vers ce segment. Deuxièmement, ce pointeur est utilisé pour faire la lecture ou écriture. Plus précisément, la première lecture récupère un descripteur de segment qui contient l'adresse de base, le pointeur voulu, mais aussi l'adresse limite et d'autres informations.
[[File:Segmentation avec table des segments.png|centre|vignette|upright=2|Segmentation avec table des segments]]
L'accès à la table des segments se fait automatiquement à chaque accès mémoire. La conséquence est que chaque accès mémoire demande d'en faire deux : un pour lire la table des segments, l'autre pour l'accès lui-même. Il s'agit en quelque sorte d'une forme d'adressage indirect mémoire.
Un point important est que si le premier accès ne fait qu'une simple lecture dans un tableau, le second accès implique des calculs d'adresse. En effet, le premier accès récupère l'adresse de base du segment, mais le second accès sélectionne une donnée dans le segment, ce qui demande de calculer son adresse. L'adresse finale se déduit en combinant l'adresse de base avec un décalage (''offset'') qui donne la position de la donnée dans ce segment. L'indice de segment est utilisé pour récupérer l'adresse de base du segment. Une fois cette adresse de base connue, on lui additionne le décalage pour obtenir l'adresse finale.
[[File:Table des segments.png|centre|vignette|upright=2|Traduction d'adresse avec une table des segments.]]
Pour effectuer automatiquement l'accès à la table des segments, le processeur doit contenir un registre supplémentaire, qui contient l'adresse de la table de segment, afin de la localiser en mémoire RAM. Nous appellerons ce registre le '''pointeur de table'''. Le pointeur de table est combiné avec l'indice de segment pour adresser le descripteur de segment adéquat.
[[File:Segment 2.svg|centre|vignette|upright=2|Traduction d'adresse avec une table des segments, ici appelée table globale des de"scripteurs (terminologie des processeurs Intel x86).]]
Un point important est que la table des segments n'est pas accessible pour le programme en cours d'exécution. Il ne peut pas lire le contenu de la table des segments, et encore moins la modifier. L'accès se fait seulement de manière indirecte, en faisant usage des indices de segments, mais c'est un adressage indirect. Seul le système d'exploitation peut lire ou écrire la table des segments directement.
Plus haut, j'ai dit que tout accès mémoire impliquait deux accès mémoire : un pour charger le descripteur de segment, un autre pour la lecture/écriture proprement dite. Cependant, cela aurait un impact bien trop grand sur les performances. Dans les faits, les processeurs avec segmentations intégraient un '''cache de descripteurs de segments''', pour limiter la casse. Quand un descripteur de segment est lu depuis la RAM, il est copié dans ce cache. Les accès ultérieurs accédent au descripteur dans le cache, pas besoin de passer par la RAM. L'intel 386 avait un cache de ce type.
===La protection mémoire : les accès hors-segments===
Comme avec la relocation matérielle, le processeur détecte les débordements de segment. Pour cela, il compare l'adresse logique accédée avec l'adresse limite, ou compare la taille limite avec le décalage. De nombreux processeurs, comme l'Intel 386, préféraient utiliser la taille du segment, pour une question d'optimisation. En effet, si on compare l'adresse finale avec l'adresse limite, on doit faire la relocation avant de comparer l'adresse relocatée. Mais en utilisant la taille, ce n'est pas le cas : on peut faire la comparaison avant, pendant ou après la relocation.
Un détail à prendre en compte est la taille de la donnée accédée. Sans cela, la comparaison serait très simple : on vérifie si ''décalage <= taille du segment'', ou on compare des adresses de la même manière. Mais imaginez qu'on accède à une donnée de 4 octets : il se peut que l'adresse de ces 4 octets rentre dans le segment, mais que quelques octets débordent. Par exemple, les deux premiers octets sont dans le segment, mais pas les deux suivants. La vraie comparaison est alors : ''décalage + 4 octets <= taille du segment''.
Mais il est possible de faire le calcul autrement, et quelques processeurs comme l'Intel 386 ne s'en sont pas privé. Il calculait la différence ''taille du segment - décalage'', et vérifiait le résultat. Le processeur gérait des données de 1, 2 et 4 octets, ce qui fait que le résultat devait être entre 0 et 3. Le processeur prenait le résultat de la soustraction, et vérifiait alors que les 30 bits de poids fort valaient bien 0. Il vérifiait aussi que les deux bits de poids faible avaient la bonne valeur.
[[File:Vm7.svg|centre|vignette|upright=2|Traduction d'adresse avec vérification des accès hors-segment.]]
Une nouveauté fait son apparition avec la segmentation : la '''gestion des droits d'accès'''. Par exemple, il est possible d'interdire d'exécuter le contenu d'un segment, ce qui fournit une protection contre certaines failles de sécurité ou certains virus. Lorsqu'on exécute une opération interdite, le processeur lève une exception matérielle, à charge du système d'exploitation de gérer la situation.
Pour cela, chaque segment se voit attribuer un certain nombre d'autorisations d'accès qui indiquent si l'on peut lire ou écrire dedans, si celui-ci contient un programme exécutable, etc. Les autorisations pour chaque segment sont placées dans le descripteur de segment. Elles se résument généralement à quelques bits, qui indiquent si le segment est accesible en lecture/écriture ou exécutable. Le tout est souvent concaténé dans un ou deux '''octets de droits d'accès'''.
L'implémentation de la protection mémoire dépend du CPU considéré. Les CPU microcodés peuvent en théorie utiliser le microcode. Lorsqu'une instruction mémoire s'exécute, le microcode effectue trois étapes : lire le descripteur de segment, faire les tests de protection mémoire, exécuter la lecture/écriture ou lever une exception. Létape de test est réalisée avec un ou plusieurs micro-branchements. Par exemple, une écriture va tester le bit R/W du descripteur, qui indique si on peut écrire dans le segment, en utilisant un micro-branchement. Le micro-branchement enverra vers une routine du microcode en cas d'erreur.
Les tests de protection mémoire demandent cependant de tester beaucoup de conditions différentes. Par exemple, le CPU Intel 386 testait moins d'une dizaine de conditions pour certaines instructions. Il est cependant possible de faire plusieurs comparaisons en parallèle en rusant un peu. Il suffit de mémoriser les octets de droits d'accès dans un registre interne, de masquer les bits non-pertinents, et de faire une comparaison avec une constante adéquate, qui encode la valeur que doivent avoir ces bits.
Une solution alternative utiliser un circuit combinatoire pour faire les tests de protection mémoire. Les tests sont alors faits en parallèles, plutôt qu'un par un par des micro-branchements. Par contre, le cout en matériel est assez important. Il faut ajouter ce circuit combinatoire, ce qui demande pas mal de circuits.
===La mémoire virtuelle avec la segmentation===
La mémoire virtuelle est une fonctionnalité souvent implémentée sur les processeurs qui gèrent la segmentation, alors que les processeurs avec relocation matérielle s'en passaient. Il faut dire que l'implémentation de la mémoire virtuelle est beaucoup plus simple avec la segmentation, comparé à la relocation matérielle. Le remplacement des registres de base par des sélecteurs de segment facilite grandement l'implémentation.
Le problème de la mémoire virtuelle est que les segments peuvent être swappés sur le disque dur n'importe quand, sans que le programme soit prévu. Le swapping est réalisé par une interruption de l'OS, qui peut interrompre le programme n'importe quand. Et si un segment est swappé, le registre de base correspondant devient invalide, il point sur une adresse en RAM où le segment était, mais n'est plus. De plus, les segments peuvent être déplacés en mémoire, là encore n'importe quand et d'une manière invisible par le programme, ce qui fait que les registres de base adéquats doivent être modifiés.
Si le programme entier est swappé d'un coup, comme avec la relocation matérielle simple, cela ne pose pas de problèmes. Mais dès qu'on utilise plusieurs registres de base par programme, les choses deviennent soudainement plus compliquées. Le problème est qu'il n'y a pas de mécanismes pour choisir et invalider le registre de base adéquat quand un segment est déplacé/swappé. En théorie, on pourrait imaginer des systèmes qui résolvent le problème au niveau de l'OS, mais tous ont des problèmes qui font que l'implémentation est compliquée ou que les performances sont ridicules.
L'usage d'une table des segments accédée à chaque accès résout complètement le problème. La table des segments est accédée à chaque accès mémoire, elle sait si le segment est swappé ou non, chaque accès vérifie si le segment est en mémoire et quelle est son adresse de base. On peut changer le segment de place n'importe quand, le prochain accès récupérera des informations à jour dans la table des segments.
L'implémentation de la mémoire virtuelle avec la segmentation est simple : il suffit d'ajouter un bit dans les descripteurs de segments, qui indique si le segment est swappé ou non. Tout le reste, la gestion de ce bit, du swap, et tout ce qui est nécessaire, est délégué au système d'exploitation. Lors de chaque accès mémoire, le processeur vérifie ce bit avant de faire la traduction d'adresse, et déclenche une exception matérielle si le bit indique que le segment est swappé. L'exception matérielle est gérée par l'OS.
===Le partage de segments===
Il est possible de partager un segment entre plusieurs applications. Cela peut servir pour partager des données entre deux programmes : un segment de données partagées est alors partagé entre deux programmes. Partager un segment de code est utile pour les bibliothèques partagées : la bibliothèque est placée dans un segment dédié, qui est partagé entre les programmes qui l'utilisent. Partager un segment de code est aussi utile quand plusieurs instances d'une même application sont lancés simultanément : le code n'ayant pas de raison de changer, celui-ci est partagé entre toutes les instances. Mais ce n'est là qu'un exemple.
La première solution pour cela est de configurer les tables de segment convenablement. Le même segment peut avoir des droits d'accès différents selon les processus. Les adresses de base/limite sont identiques, mais les tables des segments ont alors des droits d'accès différents. Mais cette méthode de partage des segments a plusieurs défauts.
Premièrement, les sélecteurs de segments ne sont pas les mêmes d'un processus à l'autre, pour un même segment. Le segment partagé peut correspondre au segment numéro 80 dans le premier processus, au segment numéro 1092 dans le second processus. Rien n'impose que les sélecteurs de segment soient les mêmes d'un processus à l'autre, pour un segment identique.
Deuxièmement, les adresses limite et de base sont dupliquées dans plusieurs tables de segments. En soi, cette redondance est un souci mineur. Mais une autre conséquence est une question de sécurité : que se passe-t-il si jamais un processus a une table des segments corrompue ? Il se peut que pour un segment identique, deux processus n'aient pas la même adresse limite, ce qui peut causer des failles de sécurité. Un processus peut alors subir un débordement de tampon, ou tout autre forme d'attaque.
[[File:Vm9.png|centre|vignette|upright=2|Illustration du partage d'un segment entre deux applications.]]
Une seconde solution, complémentaire, utilise une table de segment globale, qui mémorise des segments partagés ou accessibles par tous les processus. Les défauts de la méthode précédente disparaissent avec cette technique : un segment est identifié par un sélecteur unique pour tous les processus, il n'y a pas de duplication des descripteurs de segment. Par contre, elle a plusieurs défauts.
Le défaut principal est que cette table des segments est accessible par tous les processus, impossible de ne partager ses segments qu'avec certains pas avec les autres. Un autre défaut est que les droits d'accès à un segment partagé sont identiques pour tous les processus. Impossible d'avoir un segment partagé accessible en lecture seule pour un processus, mais accessible en écriture pour un autre. Il est possible de corriger ces défauts, mais nous en parlerons dans la section sur les architectures à capacité.
===L'extension d'adresse avec la segmentation===
L'extension d'adresse est possible avec la segmentation, de la même manière qu'avec la relocation matérielle. Il suffit juste que les adresses de base soient aussi grandes que le bus d'adresse. Mais il y a une différence avec la relocation matérielle : un même programme peut utiliser plus de mémoire qu'il n'y en a dans l'espace d'adressage. La raison est simple : un segment peut prendre tout l'espace d'adressage, et il y a plusieurs segments par programme.
Pour donner un exemple, prenons un processeur 16 bits, qui peut adresser 64 kibioctets, associé à une mémoire de 4 mébioctets. Il est possible de placer le code machine dans les premiers 64k de la mémoire, la pile du programme dans les 64k suivants, le tas dans les 64k encore après, et ainsi de suite. Le programme dépasse donc les 64k de mémoire de l'espace d'adressage. Ce genre de chose est impossible avec la relocation, où un programme est limité par l'espace d'adressage.
===Le mode protégé des processeurs x86===
L'Intel 80286, aussi appelé 286, ajouta un mode de segmentation séparé du mode réel, qui ajoute une protection mémoire à la segmentation, ce qui lui vaut le nom de '''mode protégé'''. Dans ce mode, les registres de segment ne contiennent pas des adresses de base, mais des sélecteurs de segments qui sont utilisés pour l'accès à la table des segments en mémoire RAM.
Le 286 bootait en mode réel, puis le système d'exploitation devait faire quelques manipulations pour passer en mode protégé. Le 286 était pensé pour être rétrocompatible au maximum avec le 80186. Mais les différences entre le 286 et le 8086 étaient majeures, au point que les applications devaient être réécrites intégralement pour profiter du mode protégé. Un mode de compatibilité permettait cependant aux applications destinées au 8086 de fonctionner, avec même de meilleures performances. Aussi, le mode protégé resta inutilisé sur la plupart des applications exécutées sur le 286.
Vint ensuite le processeur 80386, renommé en 386 quelques années plus tard. Sur ce processeur, les modes réel et protégé sont conservés tel quel, à une différence près : toutes les adresses passent à 32 bits, qu'il s'agisse des adresses de base, limite ou des ''offsets''. Le processeur peut donc adresser un grand nombre de segments : 2^32, soit plus de 4 milliards. Les segments grandissent aussi et passent de 64 KB maximum à 4 gibioctets maximum. Mais surtout : le 386 ajouta le support de la pagination en plus de la segmentation. Ces modifications ont été conservées sur les processeurs 32 bits ultérieurs.
Les processeurs x86 gèrent deux types de tables des segments : une table locale pour chaque processus, et une table globale partagée entre tous les processus. Il ne peut y avoir qu'une table locale d'active, vu que le processeur ne peut exécuter qu'un seul processus en même temps. Chaque table locale définit 8192 segments, pareil pour la table globale. La table globale est utilisée pour les segments du noyau et la mémoire partagée entre processus. Un défaut est qu'un segment partagé par la table globale est visible par tous les processus, avec les mêmes droits d'accès. Ce qui fait que cette méthode était peu utilisée en pratique. La table globale mémorise aussi des pointeurs vers les tables locales, avec un descripteur de segment par table locale.
Sur les processeurs x86 32 bits, un descripteur de segment est organisé comme suit, pour les architectures 32 bits. On y trouve l'adresse de base et la taille limite, ainsi que de nombreux bits de contrôle.
Le premier groupe de bits de contrôle est l'octet en bleu à droite. Il contient :
* le bit P qui indique que l'entrée contient un descripteur valide, qu'elle n'est pas vide ;
* deux bits DPL qui indiquent le niveau de privilège du segment (noyau, utilisateur, les deux intermédiaires spécifiques au x86) ;
* un bit S qui précise si le segment est de type système (utiles pour l'OS) ou un segment de code/données.
* un champ Type qui contient les bits suivants :
** un bit E qui indique si le segment contient du code exécutable ou non ;
** le bit RW qui indique s'il est en lecture seule ou non ;;
** Un bit A qui indique que le segment a récemment été accédé, information utile pour l'OS;
** un bit DC assez spécifiques.
En haut à gauche, en bleu, on trouve deux bits :
* Le bit G indique comment interpréter la taille contenue dans le descripteur : 0 si la taille est exprimée en octets, 1 si la taille est un nombre de pages de 4 kibioctets. Ce bit précise si on utilise la segmentation seule, ou combinée avec la pagination.
* Le bit DB précise si l'on utilise des segments en mode de compatibilité 16 bits ou des segments 32 bits.
[[File:SegmentDescriptor.svg|centre|vignette|upright=3|Segment Descriptor]]
Les indices de segment sont appelés des sélecteurs de segment. Ils ont une taille de 16 bits, mais 3 bits sont utilisés pour encoder des méta-données. Le numéro de segment est donc codé sur 13 bits, ce qui permettait de gérer maximum 8192 segments par table de segment (locale ou globale). Les 16 bits sont organisés comme suit :
* 13 bits pour le numéro du segment dans la table des segments, l'indice de segment proprement dit ;
* un bit qui précise s'il faut accéder à la table des segments globale ou locale ;
* deux bits qui indiquent le niveau de privilège de l'accès au segment (les 4 niveaux de protection, dont l'espace noyau et utilisateur).
[[File:SegmentSelector.svg|centre|vignette|upright=1.5|Sélecteur de segment 16 bit.]]
En tout, l'indice permet de gérer 8192 segments pour la table locale et 8192 segments de la table globale.
====La MMU du 386/486 : cache de segment, protection mémoire====
La MMU du 386 et celle du 486 étaient assez similaires. Elles étaient plus complexes que celle du 186 et du 286. Elle contenait un additionneur pour les calculs d'adresse et un comparateur pour tester si l'accès mémoire déborde d'un segment. Le test de débordement se faisait en parallèle du calcul de l'adresse finale, comme sur le 286.
L'additionneur était un additionneur trois-opérandes, qui additionnait l'adresse à lire/écrire, l'adresse de base du segment, et un décalage intégré dans l'instruction. En clair, l'unité de segmentation n'était pas qu'une MMU, elle prenait en charge une partie du calcul d'adresse. L'avantage est que cela permettait de gérer les modes d'adressage "base + décalage" et "base + indice + décalage" très facilement, en utilisant un minimum de circuits. Une conséquence de cette organisation était que l'usage d'un décalage était gratuit. Par contre, dès qu'on utilisait l'adressage "Base + Indice", avec ou sans décalage, l'instruction prenait un cycle de plus à s'exécuter, parce qu'il fallait faire l'addition "Base + Indice" dans l'ALU entière.
Le CPU 386 était le premier à implémenter la protection mémoire avec des segments. Pour cela, il intégrait une '''''Protection Test Unit''''', séparée du microcode, qu'on va abrévier en PTU. Précisément, il s'agissait d'un PLA (''Programmable Logic Array''), une sorte d'intermédiaire entre circuit logique fait sur mesure et mémoire ROM, qu'on a déjà abordé dans le chapitre sur les mémoires ROM. Mais cette unité ne faisait pas tout, le microcode était aussi impliqué. La PTU sera détaillée dans la section suivante.
Pour améliorer les performances, le 386 et le 486 intégraient un '''cache de descripteurs de segment''', aussi appelé le cache de descripteurs. Lorsqu'un descripteur état chargé pour la première fois, il était copié dans le cache de descripteurs de segment. Les accès mémoire ultérieur lisaient le descripteur de segment depuis ce cache, pas depuis la table des segments en RAM. Le cache de descripteurs gère aussi bien les segments en mode réel qu'en mode protégé. Récupérer l'adresse de base depuis cache se fait un peu différemment en mode réel et protégé, mais le cache gère cela tout seul. Idem pour récupérer la taille/adresse limite.
[[File:Microarchitecture du 386, avec focus sur la segmentation.png|centre|vignette|upright=2|Microarchitecture du 386 et du 486, avec focus sur la segmentation.]]
En mode réel, la taille des segment est censée être limitée à 64 kibioctets. Mais le processeur ne vérifiait pas si cette limite était dépassée. À la place, il utilisait la limite précisée dans le cache de segments. Pire que ça : le cache de segment n'était pas réinitialisé quand on passe du mode réel au mode protégé, et réciproquement. Et cette propriété a été à l'origine de l''''''unreal mode'''''. Il s'agissait d'un mode réel amélioré, capable d'utiliser des segments de 4 gibioctets et des adresses de 32 bits.
Passer en mode ''unreal'' pouvait se faire de deux manières. Il était possible d'altérer le cache de descripteur en utilisant l'instruction non-documentée LOADALL. Elle permettait de charger les descripteurs de segments dans le cache de descripteurs, avec une taille arbitraire. Une autre solution, beaucoup plus complexe sur le 386, demandait d'entrer en mode protégé pour configurer des segments de grande taille, de charger leurs descripteurs dans le cache de descripteur, puis de revenir en mode réel. En mode réel, les descripteurs dans le cache étaient encore disponibles et on pouvait les lire dans le cache.
====L'implémentation de la protection mémoire sur le 386====
La protection mémoire teste la valeur des bits P, S, X, E, R/W. Elle teste aussi les niveaux de privilège, avec deux bits DPL et CPL. En tout, le processeur pouvait tester 148 conditions différentes en parallèle dans la PTU. Cependant, les niveaux de privilèges étaient pré-traités par le microcode. Le microcode vérifiait aussi s'il y avait une erreur en terme d’anneau mémoire, avec par "exemple un segment en mode noyau accédé alors que le CPU est en espace utilisateur. Il fournissait alors un résultat sur deux bits, qui indiquait s'il y avait une erreur ou non, que la PTU utilisait.
Mais toutes les conditions n'étaient pas pertinentes à un instant t. Par exemple, il est pertinent de vérifier si le bit R/W était cohérent si l'instruction à exécuter est une écriture. Mais il n'y a pas besoin de tester le bit E qui indique qu'un segment est exécutable ou non, pour une lecture. En tout, le processeur pouvait se retrouver dans 33 situations possibles, chacune demandant de tester un sous-ensemble des 148 conditions. Pour préciser quel sous-ensembles tester, la PTU recevait un code opération, généré par le microcode.
Pour faire les tests de protection mémoire, le microcode avait une micro-opération nommée ''protection test operation'', qui envoyait les droits d'accès à la PTU. Lors de l'exécution d'une ''protection test operation'', le PLA recevait un descripteur de segment, lu depuis la mémoire RAM, ainsi qu'un code opération provenant du microcode.
{|class="wikitable"
|+ Entrée de la ''Protection Test Unit''
|-
! 15 - 14 !! 13 - 12 !! 11 !! 10 !! 9 !! 8 !! 7 !! 6 !! 5-0
|-
| P1 , P2 || || P || S || X || E || R/W || A || Code opération
|-
| Niveaux de privilèges cohérents/erreur || || Segment présent en mémoire ou swappé || S || X || Segment exécutable ou non || Segment accesible en lecture/écriture || Segment récemment accédé || Code opération
|}
Il fournissait en sortie un bit qui indiquait si une erreur de protection mémoire avait eu lieu ou non. Il fournissait aussi une adresse de 12 bits, utilisée seulement en cas d'erruer. Elle pointait dans le microcode, sur un code levant une exception en cas d'erreur. Enfin, la PTU fournissait 4 bits pouvant être testés par un branchement dans le microcode. L'un d'entre eux demandait de tester s'il y a un accès hors-limite, les autres étaient assez peu reliés à la protection mémoire.
Un détail est que le chargement du descripteur de segment est réalisé par une fonction dans le microcode. Elle est appliquée pour toutes les instructions ou situations qui demandent de faire un accès mémoire. Et les tests de protection mémoire sont réalisés dans cette fonction, pas après elle. Vu qu'il s'agit d'une fonction exécutée quelque soit l'instruction, le microcode doit transférer le code opération à cette fonction. Le microcode est pour cela associé à un registre interne, dans lequel le code opération est mémorisé, avant d'appeler la fonction. Le microcode a une micro-opération PTSAV (''Protection Save'') pour mémoriser le code opération dans ce registre. Dans la fonction qui charge le descripteur, une micro-opération PTOVRR (''Protection Override'') lit le code opération dans ce registre, et lance les tests nécessaires.
Il faut noter que le PLA était certes plus rapide que de tester les conditions une par une, mais il était assez lent. La PTU mettait environ 3 cycles d'horloges pour rendre son résultat. Le microcode en profitait alors pour exécuter des micro-opérations durant ces 3 cycles d'attente. Par exemple, le microcode pouvait en profiter pour lire l'adresse de base dans le descripteur, si elle n'a pas été chargée avant (les descripteur était chargé en deux fois). Il fallait cependant que les trois micro-opérations soient valides, peu importe qu'il y ait une erreur de protection mémoire ou non. Ou du moins, elles produisaient un résultat qui n'est pas utilisé en cas d'erreur. Si ce n'était pas possible, le microcode ajoutait des NOP pendant ce temps d'attente de 3 cycles.
Le bit A du descripteur de segment indique que le segment a récemment été accédé. Il est mis à jour après les tests de protection mémoire, quand ceux-ci indiquent que l'accès mémoire est autorisé. Le bit A est mis à 1 si la PTU l'autorise. Pour cela, la PTU utilise un des 4 bits de sortie mentionnés plus haut : l'un d'entre eux indique que le bit A doit être mis à 1. La mise à jour est ensuite réalisée par le microcode, qui utilise trois micro-opérations pour le mettre à jour.
====Le ''Hardware task switching'' des CPU x86====
Les systèmes d’exploitation modernes peuvent lancer plusieurs logiciels en même temps. Les logiciels sont alors exécutés à tour de rôle. Passer d'un programme à un autre est ce qui s'appelle une commutation de contexte. Lors d'une commutation de contexte, l'état du processeur est sauvegardé, afin que le programme stoppé puisse reprendre là où il était. Il arrivera un moment où le programme stoppé redémarrera et il doit reprendre dans l'état exact où il s'est arrêté. Deuxièmement, le programme à qui c'est le tour restaure son état. Cela lui permet de revenir là où il était avant d'être stoppé. Il y a donc une sauvegarde et une restauration des registres.
Divers processeurs incorporent des optimisations matérielles pour rendre la commutation de contexte plus rapide. Ils peuvent sauvegarder et restaurer les registres du processeur automatiquement lors d'une interruption de commutation de contexte. Les registres sont sauvegardés dans des structures de données en mémoire RAM, appelées des '''contextes matériels'''. Sur les processeurs x86, il s'agit de la technique d{{'}}''Hardware Task Switching''. Fait intéressant, le ''Hardware Task Switching'' se base beaucoup sur les segments mémoires.
Avec ''Hardware Task Switching'', chaque contexte matériel est mémorisé dans son propre segment mémoire, séparé des autres. Les segments pour les contextes matériels sont appelés des '''''Task State Segment''''' (TSS). Un TSS mémorise tous les registres généraux, le registre d'état, les pointeurs de pile, le ''program counter'' et quelques registres de contrôle du processeur. Par contre, les registres flottants ne sont pas sauvegardés, de même que certaines registres dit SIMD que nous n'avons pas encore abordé. Et c'est un défaut qui fait que le ''Hardware Task Switching'' n'est plus utilisé.
Le programme en cours d'exécution connait l'adresse du TSS qui lui est attribué, car elle est mémorisée dans un registre appelé le '''''Task Register'''''. En plus de pointer sur le TSS, ce registre contient aussi les adresses de base et limite du segment en cours. Pour être plus précis, le ''Task Register'' ne mémorise pas vraiment l'adresse du TSS. À la place, elle mémorise le numéro du segment, le numéro du TSS. Le numéro est codé sur 16 bits, ce qui explique que 65 536 segments sont adressables. Les instructions LDR et STR permettent de lire/écrire ce numéro de segment dans le ''Task Register''.
Le démarrage d'un programme a lieu automatiquement dans plusieurs circonstances. La première est une instruction de branchement CALL ou JMP adéquate. Le branchement fournit non pas une adresse à laquelle brancher, mais un numéro de segment qui pointe vers un TSS. Cela permet à une routine du système d'exploitation de restaurer les registres et de démarrer le programme en une seule instruction de branchement. Une seconde circonstance est une interruption matérielle ou une exception, mais nous la mettons de côté. Le ''Task Register'' est alors initialisé avec le numéro de segment fournit. S'en suit la procédure suivante :
* Le ''Task Register'' est utilisé pour adresser la table des segments, pour récupérer un pointeur vers le TSS associé.
* Le pointeur est utilisé pour une seconde lecture, qui adresse le TSS directement. Celle-ci restaure les registres du processeur.
En clair, on va lire le ''TSS descriptor'' dans la GDT, puis on l'utilise pour restaurer les registres du processeur.
[[File:Hardware Task Switching x86.png|centre|vignette|upright=2|Hardware Task Switching x86]]
===La segmentation sur les processeurs Burrough B5000 et plus===
Le Burrough B5000 est un très vieil ordinateur, commercialisé à partir de l'année 1961. Ses successeurs reprennent globalement la même architecture. C'était une machine à pile, doublé d'une architecture taguée, choses très rare de nos jours. Mais ce qui va nous intéresser dans ce chapitre est que ce processeur incorporait la segmentation, avec cependant une différence de taille : un programme avait accès à un grand nombre de segments. La limite était de 1024 segments par programme ! Il va de soi que des segments plus petits favorise l'implémentation de la mémoire virtuelle, mais complexifie la relocation et le reste, comme nous allons le voir.
Le processeur gère deux types de segments : les segments de données et de procédure/fonction. Les premiers mémorisent un bloc de données, dont le contenu est laissé à l'appréciation du programmeur. Les seconds sont des segments qui contiennent chacun une procédure, une fonction. L'usage des segments est donc différent de ce qu'on a sur les processeurs x86, qui n'avaient qu'un segment unique pour l'intégralité du code machine. Un seul segment de code machine x86 est découpé en un grand nombre de segments de code sur les processeurs Burrough.
La table des segments contenait 1024 entrées de 48 bits chacune. Fait intéressant, chaque entrée de la table des segments pouvait mémoriser non seulement un descripteur de segment, mais aussi une valeur flottante ou d'autres types de données ! Parler de table des segments est donc quelque peu trompeur, car cette table ne gère pas que des segments, mais aussi des données. La documentation appelaiat cette table la '''''Program Reference Table''''', ou PRT.
La raison de ce choix quelque peu bizarre est que les instructions ne gèrent pas d'adresses proprement dit. Tous les accès mémoire à des données en-dehors de la pile passent par la segmentation, ils précisent tous un indice de segment et un ''offset''. Pour éviter d'allouer un segment pour chaque donnée, les concepteurs du processeur ont décidé qu'une entrée pouvait contenir directement la donnée entière à lire/écrire.
La PRT supporte trois types de segments/descripteurs : les descripteurs de données, les descripteurs de programme et les descripteurs d'entrées-sorties. Les premiers décrivent des segments de données. Les seconds sont associés aux segments de procédure/fonction et sont utilisés pour les appels de fonction (qui passent, eux aussi, par la segmentation). Le dernier type de descripteurs sert pour les appels systèmes et les communications avec l'OS ou les périphériques.
Chaque entrée de la PRT contient un ''tag'', une suite de bit qui indique le type de l'entrée : est-ce qu'elle contient un descripteur de segment, une donnée, autre. Les descripteurs contiennent aussi un ''bit de présence'' qui indique si le segment a été swappé ou non. Car oui, les segments pouvaient être swappés sur ce processeur, ce qui n'est pas étonnant vu que les segments sont plus petits sur cette architecture. Le descripteur contient aussi l'adresse de base du segment ainsi que sa taille, et diverses informations pour le retrouver sur le disque dur s'il est swappé.
: L'adresse mémorisée ne faisait que 15 bits, ce qui permettait d'adresse 32 kibi-mots, soit 192 kibioctets de mémoire. Diverses techniques d'extension d'adressage étaient disponibles pour contourner cette limitation. Outre l'usage de l{{'}}''overlay'', le processeur et l'OS géraient aussi des identifiants d'espace d'adressage et en fournissaient plusieurs par processus. Les processeurs Borrough suivants utilisaient des adresses plus grandes, de 20 bits, ce qui tempérait le problème.
[[File:B6700Word.jpg|centre|vignette|upright=2|Structure d'un mot mémoire sur le B6700.]]
==Les architectures à capacités==
Les architectures à capacité utilisent la segmentation à granularité fine, mais ajoutent des mécanismes de protection mémoire assez particuliers, qui font que les architectures à capacité se démarquent du reste. Les architectures de ce type sont très rares et sont des processeurs assez anciens. Le premier d'entre eux était le Plessey System 250, qui date de 1969. Il fu suivi par le CAP computer, vendu entre les années 70 et 77. En 1978, le System/38 d'IBM a eu un petit succès commercial. En 1980, la Flex machine a aussi été vendue, mais à très peu d'examplaires, comme les autres architectures à capacité. Et enfin, en 1981, l'architecture à capacité la plus connue, l'Intel iAPX 432 a été commercialisée. Depuis, la seule architecture de ce type est en cours de développement. Il s'agit de l'architecture CHERI, dont la mise en projet date de 2014.
===Le partage de la mémoire sur les architectures à capacités===
Le partage de segment est grandement modifié sur les architectures à capacité. Avec la segmentation normale, il y a une table de segment par processus. Les conséquences sont assez nombreuses, mais la principale est que partager un segment entre plusieurs processus est compliqué. Les défauts ont été évoqués plus haut. Les sélecteurs de segments ne sont pas les mêmes d'un processus à l'autre, pour un même segment. De plus, les adresses limite et de base sont dupliquées dans plusieurs tables de segments, et cela peut causer des problèmes de sécurité si une table des segments est modifiée et pas l'autre. Et il y a d'autres problèmes, tout aussi importants.
[[File:Partage des segments avec la segmentation.png|centre|vignette|upright=1.5|Partage des segments avec la segmentation]]
À l'opposé, les architectures à capacité utilisent une table des segments unique pour tous les processus. La table des segments unique sera appelée dans de ce qui suit la '''table des segments globale''', ou encore la table globale. En conséquence, les adresses de base et limite ne sont présentes qu'en un seul exemplaire par segment, au lieu d'être dupliquées dans autant de processus que nécessaire. De plus, cela garantit que l'indice de segment est le même quel que soit le processus qui l'utilise.
Un défaut de cette approche est au niveau des droits d'accès. Avec la segmentation normale, les droits d'accès pour un segment sont censés changer d'un processus à l'autre. Par exemple, tel processus a accès en lecture seule au segment, l'autre seulement en écriture, etc. Mais ici, avec une table des segments uniques, cela ne marche plus : incorporer les droits d'accès dans la table des segments ferait que tous les processus auraient les mêmes droits d'accès au segment. Et il faut trouver une solution.
===Les capacités sont des pointeurs protégés===
Pour éviter cela, les droits d'accès sont combinés avec les sélecteurs de segments. Les sélecteurs des segments sont remplacés par des '''capacités''', des pointeurs particuliers formés en concaténant l'indice de segment avec les droits d'accès à ce segment. Si un programme veut accéder à une adresse, il fournit une capacité de la forme "sélecteur:droits d'accès", et un décalage qui indique la position de l'adresse dans le segment.
Il est impossible d'accéder à un segment sans avoir la capacité associée, c'est là une sécurité importante. Un accès mémoire demande que l'on ait la capacité pour sélectionner le bon segment, mais aussi que les droits d'accès en permettent l'accès demandé. Par contre, les capacités peuvent être passées d'un programme à un autre sans problème, les deux programmes pourront accéder à un segment tant qu'ils disposent de la capacité associée.
[[File:Comparaison entre capacités et adresses segmentées.png|centre|vignette|upright=2.5|Comparaison entre capacités et adresses segmentées]]
Mais cette solution a deux problèmes très liés. Au niveau des sélecteurs de segment, le problème est que les sélecteur ont une portée globale. Avant, l'indice de segment était interne à un programme, un sélecteur ne permettait pas d'accéder au segment d'un autre programme. Sur les architectures à capacité, les sélecteurs ont une portée globale. Si un programme arrive à forger un sélecteur qui pointe vers un segment d'un autre programme, il peut théoriquement y accéder, à condition que les droits d'accès le permettent. Et c'est là qu'intervient le second problème : les droits d'accès ne sont plus protégés par l'espace noyau. Les droits d'accès étaient dans la table de segment, accessible uniquement en espace noyau, ce qui empêchait un processus de les modifier. Avec une capacité, il faut ajouter des mécanismes de protection qui empêchent un programme de modifier les droits d'accès à un segment et de générer un indice de segment non-prévu.
La première sécurité est qu'un programme ne peut pas créer une capacité, seul le système d'exploitation le peut. Les capacités sont forgées lors de l'allocation mémoire, ce qui est du ressort de l'OS. Pour rappel, un programme qui veut du rab de mémoire RAM peut demander au système d'exploitation de lui allouer de la mémoire supplémentaire. Le système d'exploitation renvoie alors un pointeurs qui pointe vers un nouveau segment. Le pointeur est une capacité. Il doit être impossible de forger une capacité, en-dehors d'une demande d'allocation mémoire effectuée par l'OS. Typiquement, la forge d'une capacité se fait avec des instructions du processeur, que seul l'OS peut éxecuter (pensez à une instruction qui n'est accessible qu'en espace noyau).
La seconde protection est que les capacités ne peuvent pas être modifiées sans raison valable, que ce soit pour l'indice de segment ou les droits d'accès. L'indice de segment ne peut pas être modifié, quelqu'en soit la raison. Pour les droits d'accès, la situation est plus compliquée. Il est possible de modifier ses droits d'accès, mais sous conditions. Réduire les droits d'accès d'une capacité est possible, que ce soit en espace noyau ou utilisateur, pas l'OS ou un programme utilisateur, avec une instruction dédiée. Mais augmenter les droits d'accès, seul l'OS peut le faire avec une instruction précise, souvent exécutable seulement en espace noyau.
Les capacités peuvent être copiées, et même transférées d'un processus à un autre. Les capacités peuvent être détruites, ce qui permet de libérer la mémoire utilisée par un segment. La copie d'une capacité est contrôlée par l'OS et ne peut se faire que sous conditions. La destruction d'une capacité est par contre possible par tous les processus. La destruction ne signifie pas que le segment est effacé, il est possible que d'autres processus utilisent encore des copies de la capacité, et donc le segment associé. On verra quand la mémoire est libérée plus bas.
Protéger les capacités demande plusieurs conditions. Premièrement, le processeur doit faire la distinction entre une capacité et une donnée. Deuxièmement, les capacités ne peuvent être modifiées que par des instructions spécifiques, dont l'exécution est protégée, réservée au noyau. En clair, il doit y avoir une séparation matérielle des capacités, qui sont placées dans des registres séparés. Pour cela, deux solutions sont possibles : soit les capacités remplacent les adresses et sont dispersées en mémoire, soit elles sont regroupées dans un segment protégé.
====La liste des capacités====
Avec la première solution, on regroupe les capacités dans un segment protégé. Chaque programme a accès à un certain nombre de segments et à autant de capacités. Les capacités d'un programme sont souvent regroupées dans une '''liste de capacités''', appelée la '''''C-list'''''. Elle est généralement placée en mémoire RAM. Elle est ce qu'il reste de la table des segments du processus, sauf que cette table ne contient pas les adresses du segment, qui sont dans la table globale. Tout se passe comme si la table des segments de chaque processus est donc scindée en deux : la table globale partagée entre tous les processus contient les informations sur les limites des segments, la ''C-list'' mémorise les droits d'accès et les sélecteurs pour identifier chaque segment. C'est un niveau d'indirection supplémentaire par rapport à la segmentation usuelle.
[[File:Architectures à capacité.png|centre|vignette|upright=2|Architectures à capacité]]
La liste de capacité est lisible par le programme, qui peut copier librement les capacités dans les registres. Par contre, la liste des capacités est protégée en écriture. Pour le programme, il est impossible de modifier les capacités dedans, impossible d'en rajouter, d'en forger, d'en retirer. De même, il ne peut pas accéder aux segments des autres programmes : il n'a pas les capacités pour adresser ces segments.
Pour protéger la ''C-list'' en écriture, la solution la plus utilisée consiste à placer la ''C-list'' dans un segment dédié. Le processeur gère donc plusieurs types de segments : les segments de capacité pour les ''C-list'', les autres types segments pour le reste. Un défaut de cette approche est que les adresses/capacités sont séparées des données. Or, les programmeurs mixent souvent adresses et données, notamment quand ils doivent manipuler des structures de données comme des listes chainées, des arbres, des graphes, etc.
L'usage d'une ''C-list'' permet de se passer de la séparation entre espace noyau et utilisateur ! Les segments de capacité sont eux-mêmes adressés par leur propre capacité, avec une capacité par segment de capacité. Le programme a accès à la liste de capacité, comme l'OS, mais leurs droits d'accès ne sont pas les mêmes. Le programme a une capacité vers la ''C-list'' qui n'autorise pas l'écriture, l'OS a une autre capacité qui accepte l'écriture. Les programmes ne pourront pas forger les capacités permettant de modifier les segments de capacité. Une méthode alternative est de ne permettre l'accès aux segments de capacité qu'en espace noyau, mais elle est redondante avec la méthode précédente et moins puissante.
====Les capacités dispersées, les architectures taguées====
Une solution alternative laisse les capacités dispersées en mémoire. Les capacités remplacent les adresses/pointeurs, et elles se trouvent aux mêmes endroits : sur la pile, dans le tas. Comme c'est le cas dans les programmes modernes, chaque allocation mémoire renvoie une capacité, que le programme gére comme il veut. Il peut les mettre dans des structures de données, les placer sur la pile, dans des variables en mémoire, etc. Mais il faut alors distinguer si un mot mémoire contient une capacité ou une autre donnée, les deux ne devant pas être mixés.
Pour cela, chaque mot mémoire se voit attribuer un certain bit qui indique s'il s'agit d'un pointeur/capacité ou d'autre chose. Mais cela demande un support matériel, ce qui fait que le processeur devient ce qu'on appelle une ''architecture à tags'', ou ''tagged architectures''. Ici, elles indiquent si le mot mémoire contient une adresse:capacité ou une donnée.
[[File:Architectures à capacité sans liste de capacité.png|centre|vignette|upright=2|Architectures à capacité sans liste de capacité]]
L'inconvénient est le cout en matériel de cette solution. Il faut ajouter un bit à chaque case mémoire, le processeur doit vérifier les tags avant chaque opération d'accès mémoire, etc. De plus, tous les mots mémoire ont la même taille, ce qui force les capacités à avoir la même taille qu'un entier. Ce qui est compliqué.
===Les registres de capacité===
Les architectures à capacité disposent de registres spécialisés pour les capacités, séparés pour les entiers. La raison principale est une question de sécurité, mais aussi une solution pragmatique au fait que capacités et entiers n'ont pas la même taille. Les registres dédiés aux capacités ne mémorisent pas toujours des capacités proprement dites. À la place, ils mémorisent des descripteurs de segment, qui contiennent l'adresse de base, limite et les droits d'accès. Ils sont utilisés pour la relocation des accès mémoire ultérieurs. Ils sont en réalité identiques aux registres de relocation, voire aux registres de segments. Leur utilité est d'accélérer la relocation, entre autres.
Les processeurs à capacité ne gèrent pas d'adresses proprement dit, comme pour la segmentation avec plusieurs registres de relocation. Les accès mémoire doivent préciser deux choses : à quel segment on veut accéder, à quelle position dans le segment se trouve la donnée accédée. La première information se trouve dans le mal nommé "registre de capacité", la seconde information est fournie par l'instruction d'accès mémoire soit dans un registre (Base+Index), soit en adressage base+''offset''.
Les registres de capacités sont accessibles à travers des instructions spécialisées. Le processeur ajoute des instructions LOAD/STORE pour les échanges entre table des segments et registres de capacité. Ces instructions sont disponibles en espace utilisateur, pas seulement en espace noyau. Lors du chargement d'une capacité dans ces registres, le processeur vérifie que la capacité chargée est valide, et que les droits d'accès sont corrects. Puis, il accède à la table des segments, récupère les adresses de base et limite, et les mémorise dans le registre de capacité. Les droits d'accès et d'autres méta-données sont aussi mémorisées dans le registre de capacité. En somme, l'instruction de chargement prend une capacité et charge un descripteur de segment dans le registre.
Avec ce genre de mécanismes, il devient difficile d’exécuter certains types d'attaques, ce qui est un gage de sureté de fonctionnement indéniable. Du moins, c'est la théorie, car tout repose sur l'intégrité des listes de capacité. Si on peut modifier celles-ci, alors il devient facile de pouvoir accéder à des objets auxquels on n’aurait pas eu droit.
===Le recyclage de mémoire matériel===
Les architectures à capacité séparent les adresses/capacités des nombres entiers. Et cela facilite grandement l'implémentation de la ''garbage collection'', ou '''recyclage de la mémoire''', à savoir un ensemble de techniques logicielles qui visent à libérer la mémoire inutilisée.
Rappelons que les programmes peuvent demander à l'OS un rab de mémoire pour y placer quelque chose, généralement une structure de donnée ou un objet. Mais il arrive un moment où cet objet n'est plus utilisé par le programme. Il peut alors demander à l'OS de libérer la portion de mémoire réservée. Sur les architectures à capacité, cela revient à libérer un segment, devenu inutile. La mémoire utilisée par ce segment est alors considérée comme libre, et peut être utilisée pour autre chose. Mais il arrive que les programmes ne libèrent pas le segment en question. Soit parce que le programmeur a mal codé son programme, soit parce que le compilateur n'a pas fait du bon travail ou pour d'autres raisons.
Pour éviter cela, les langages de programmation actuels incorporent des '''''garbage collectors''''', des morceaux de code qui scannent la mémoire et détectent les segments inutiles. Pour cela, ils doivent identifier les adresses manipulées par le programme. Si une adresse pointe vers un objet, alors celui-ci est accessible, il sera potentiellement utilisé dans le futur. Mais si aucune adresse ne pointe vers l'objet, alors il est inaccessible et ne sera plus jamais utilisé dans le futur. On peut libérer les objets inaccessibles.
Identifier les adresses est cependant très compliqué sur les architectures normales. Sur les processeurs modernes, les ''garbage collectors'' scannent la pile à la recherche des adresses, et considèrent tout mot mémoire comme une adresse potentielle. Mais les architectures à capacité rendent le recyclage de la mémoire très facile. Un segment est accessible si le programme dispose d'une capacité qui pointe vers ce segment, rien de plus. Et les capacités sont facilement identifiables : soit elles sont dans la liste des capacités, soit on peut les identifier à partir de leur ''tag''.
Le recyclage de mémoire était parfois implémenté directement en matériel. En soi, son implémentation est assez simple, et peu être réalisé dans le microcode d'un processeur. Une autre solution consiste à utiliser un second processeur, spécialement dédié au recyclage de mémoire, qui exécute un programme spécialement codé pour. Le programme en question est placé dans une mémoire ROM, reliée directement à ce second processeur.
===L'intel iAPX 432===
Voyons maintenat une architecture à capacité assez connue : l'Intel iAPX 432. Oui, vous avez bien lu : Intel a bel et bien réalisé un processeur orienté objet dans sa jeunesse. La conception du processeur Intel iAPX 432 commença en 1975, afin de créer un successeur digne de ce nom aux processeurs 8008 et 8080.
La conception du processeur Intel iAPX 432 commença en 1975, afin de créer un successeur digne de ce nom aux processeurs 8008 et 8080. Ce processeur s'est très faiblement vendu en raison de ses performances assez désastreuses et de défauts techniques certains. Par exemple, ce processeur était une machine à pile à une époque où celles-ci étaient tombées en désuétude, il ne pouvait pas effectuer directement de calculs avec des constantes entières autres que 0 et 1, ses instructions avaient un alignement bizarre (elles étaient bit-alignées). Il avait été conçu pour maximiser la compatibilité avec le langage ADA, un langage assez peu utilisé, sans compter que le compilateur pour ce processeur était mauvais.
====Les segments prédéfinis de l'Intel iAPX 432====
L'Intel iAPX432 gère plusieurs types de segments. Rien d'étonnant à cela, les Burrough géraient eux aussi plusieurs types de segments, à savoir des segments de programmes, des segments de données, et des segments d'I/O. C'est la même chose sur l'Intel iAPX 432, mais en bien pire !
Les segments de données sont des segments génériques, dans lequels on peut mettre ce qu'on veut, suivant les besoins du programmeur. Ils sont tous découpés en deux parties de tailles égales : une partie contenant les données de l'objet et une partie pour les capacités. Les capacités d'un segment pointent vers d'autres segments, ce qui permet de créer des structures de données assez complexes. La ligne de démarcation peut être placée n'importe où dans le segment, les deux portions ne sont pas de taille identique, elles ont des tailles qui varient de segment en segment. Il est même possible de réserver le segment entier à des données sans y mettre de capacités, ou inversement. Les capacités et données sont adressées à partir de la ligne de démarcation, qui sert d'adresse de base du segment. Suivant l'instruction utilisée, le processeur accède à la bonne portion du segment.
Le processeur supporte aussi d'autres segments pré-définis, qui sont surtout utilisés par le système d'exploitation :
* Des segments d'instructions, qui contiennent du code exécutable, typiquement un programme ou des fonctions, parfois des ''threads''.
* Des segments de processus, qui mémorisent des processus entiers. Ces segments contiennent des capacités qui pointent vers d'autres segments, notamment un ou plusieurs segments de code, et des segments de données.
* Des segments de domaine, pour les modules ou bibliothèques dynamiques.
* Des segments de contexte, utilisés pour mémoriser l'état d'un processus, utilisés par l'OS pour faire de la commutation de contexte.
* Des segments de message, utilisés pour la communication entre processus par l'intermédiaire de messages.
* Et bien d'autres encores.
Sur l'Intel iAPX 432, chaque processus est considéré comme un objet à part entière, qui a son propre segment de processus. De même, l'état du processeur (le programme qu'il est en train d’exécuter, son état, etc.) est stocké en mémoire dans un segment de contexte. Il en est de même pour chaque fonction présente en mémoire : elle était encapsulée dans un segment, sur lequel seules quelques manipulations étaient possibles (l’exécuter, notamment). Et ne parlons pas des appels de fonctions qui stockaient l'état de l'appelé directement dans un objet spécial. Bref, de nombreux objets système sont prédéfinis par le processeur : les objets stockant des fonctions, les objets stockant des processus, etc.
L'Intel 432 possédait dans ses circuits un ''garbage collector'' matériel. Pour faciliter son fonctionnement, certains bits de l'objet permettaient de savoir si l'objet en question pouvait être supprimé ou non.
====Le support de la segmentation sur l'Intel iAPX 432====
La table des segments est une table hiérarchique, à deux niveaux. Le premier niveau est une ''Object Table Directory'', qui réside toujours en mémoire RAM. Elle contient des descripteurs qui pointent vers des tables secondaires, appelées des ''Object Table''. Il y a plusieurs ''Object Table'', typiquement une par processus. Plusieurs processus peuvent partager la même ''Object Table''. Les ''Object Table'' peuvent être swappées, mais pas l{{'}}''Object Table Directory''.
Une capacité tient compte de l'organisation hiérarchique de la table des segments. Elle contient un indice qui précise quelle ''Object Table'' utiliser, et l'indice du segment dans cette ''Object Table''. Le premier indice adresse l{{'}}''Object Table Directory'' et récupère un descripteur de segment qui pointe sur la bonne ''Object Table''. Le second indice est alors utilisé pour lire l'adresse de base adéquate dans cette ''Object Table''. La capacité contient aussi des droits d'accès en lecture, écriture, suppression et copie. Il y a aussi un champ pour le type, qu'on verra plus bas. Au fait : les capacités étaient appelées des ''Access Descriptors'' dans la documentation officielle.
Une capacité fait 32 bits, avec un octet utilisé pour les droits d'accès, laissant 24 bits pour adresser les segments. Le processeur gérait jusqu'à 2^24 segments/objets différents, pouvant mesurer jusqu'à 64 kibioctets chacun, ce qui fait 2^40 adresses différentes, soit 1024 gibioctets. Les 24 bits pour adresser les segments sont partagés moitié-moitié pour l'adressage des tables, ce qui fait 4096 ''Object Table'' différentes dans l{{'}}''Object Table Directory'', et chaque ''Object Table'' contient 4096 segments.
====Le jeu d'instruction de l'Intel iAPX 432====
L'Intel iAPX 432 est une machine à pile. Le jeu d'instruction de l'Intel iAPX 432 gère pas moins de 230 instructions différentes. Il gére deux types d'instructions : les instructions normales, et celles qui manipulent des segments/objets. Les premières permettent de manipuler des nombres entiers, des caractères, des chaînes de caractères, des tableaux, etc.
Les secondes sont spécialement dédiées à la manipulation des capacités. Il y a une instruction pour copier une capacité, une autre pour invalider une capacité, une autre pour augmenter ses droits d'accès (instruction sécurisée, exécutable seulement sous certaines conditions), une autre pour restreindre ses droits d'accès. deux autres instructions créent un segment et renvoient la capacité associée, la première créant un segment typé, l'autre non.
le processeur gérait aussi des instructions spécialement dédiées à la programmation système et idéales pour programmer des systèmes d'exploitation. De nombreuses instructions permettaient ainsi de commuter des processus, faire des transferts de messages entre processus, etc. Environ 40 % du micro-code était ainsi spécialement dédié à ces instructions spéciales.
Les instructions sont de longueur variable et peuvent prendre n'importe quelle taille comprise entre 10 et 300 bits, sans vraiment de restriction de taille. Les bits d'une instruction sont regroupés en 4 grands blocs, 4 champs, qui ont chacun une signification particulière.
* Le premier est l'opcode de l'instruction.
* Le champ référence, doit être interprété différemment suivant la donnée à manipuler. Si cette donnée est un entier, un caractère ou un flottant, ce champ indique l'emplacement de la donnée en mémoire. Alors que si l'instruction manipule un objet, ce champ spécifie la capacité de l'objet en question. Ce champ est assez complexe et il est sacrément bien organisé.
* Le champ format, n'utilise que 4 bits et a pour but de préciser si les données à manipuler sont en mémoire ou sur la pile.
* Le champ classe permet de dire combien de données différentes l'instruction va devoir manipuler, et quelles seront leurs tailles.
[[File:Encodage des instructions de l'Intel iAPX-432.png|centre|vignette|upright=2|Encodage des instructions de l'Intel iAPX-432.]]
====Le support de l'orienté objet sur l'Intel iAPX 432====
L'Intel 432 permet de définir des objets, qui correspondent aux classes des langages orientés objets. L'Intel 432 permet, à partir de fonctions définies par le programmeur, de créer des '''''domain objects''''', qui correspondent à une classe. Un ''domain object'' est un segment de capacité, dont les capacités pointent vers des fonctions ou un/plusieurs objets. Les fonctions et les objets sont chacun placés dans un segment. Une partie des fonctions/objets sont publics, ce qui signifie qu'ils sont accessibles en lecture par l'extérieur. Les autres sont privées, inaccessibles aussi bien en lecture qu'en écriture.
L'exécution d'une fonction demande que le branchement fournisse deux choses : une capacité vers le ''domain object'', et la position de la fonction à exécuter dans le segment. La position permet de localiser la capacité de la fonction à exécuter. En clair, on accède au ''domain object'' d'abord, pour récupérer la capacité qui pointe vers la fonction à exécuter.
Il est aussi possible pour le programmeur de définir de nouveaux types non supportés par le processeur, en faisant appel au système d'exploitation de l'ordinateur. Au niveau du processeur, chaque objet est typé au niveau de son object descriptor : celui-ci contient des informations qui permettent de déterminer le type de l'objet. Chaque type se voit attribuer un domain object qui contient toutes les fonctions capables de manipuler les objets de ce type et que l'on appelle le type manager. Lorsque l'on veut manipuler un objet d'un certain type, il suffit d'accéder à une capacité spéciale (le TCO) qui pointera dans ce type manager et qui précisera quel est l'objet à manipuler (en sélectionnant la bonne entrée dans la liste de capacité). Le type d'un objet prédéfini par le processeur est ainsi spécifié par une suite de 8 bits, tandis que le type d'un objet défini par le programmeur est défini par la capacité spéciale pointant vers son type manager.
===Conclusion===
Pour ceux qui veulent en savoir plus, je conseille la lecture de ce livre, disponible gratuitement sur internet (merci à l'auteur pour cette mise à disposition) :
* [https://homes.cs.washington.edu/~levy/capabook/ Capability-Based Computer Systems].
Voici un document qui décrit le fonctionnement de l'Intel iAPX432 :
* [https://homes.cs.washington.edu/~levy/capabook/Chapter9.pdf The Intel iAPX 432 ]
==La pagination==
Avec la pagination, la mémoire est découpée en blocs de taille fixe, appelés des '''pages mémoires'''. La taille des pages varie suivant le processeur et le système d'exploitation et tourne souvent autour de 4 kibioctets. Mais elles sont de taille fixe : on ne peut pas en changer la taille. C'est la différence avec les segments, qui sont de taille variable. Le contenu d'une page en mémoire fictive est rigoureusement le même que le contenu de la page correspondante en mémoire physique.
L'espace d'adressage est découpé en '''pages logiques''', alors que la mémoire physique est découpée en '''pages physique''' de même taille. Les pages logiques correspondent soit à une page physique, soit à une page swappée sur le disque dur. Quand une page logique est associée à une page physique, les deux ont le même contenu, mais pas les mêmes adresses. Les pages logiques sont numérotées, en partant de 0, afin de pouvoir les identifier/sélectionner. Même chose pour les pages physiques, qui sont elles aussi numérotées en partant de 0.
[[File:Principe de la pagination.png|centre|vignette|upright=2|Principe de la pagination.]]
Pour information, le tout premier processeur avec un système de mémoire virtuelle était le super-ordinateur Atlas. Il utilisait la pagination, et non la segmentation. Mais il fallu du temps avant que la méthode de la pagination prenne son essor dans les processeurs commerciaux x86.
Un point important est que la pagination implique une coopération entre OS et hardware, les deux étant fortement mélés. Une partie des informations de cette section auraient tout autant leur place dans le wikilivre sur les systèmes d'exploitation, mais il est plus simple d'en parler ici.
===La mémoire virtuelle : le ''swapping'' et le remplacement des pages mémoires===
Le système d'exploitation mémorise des informations sur toutes les pages existantes dans une '''table des pages'''. C'est un tableau où chaque ligne est associée à une page logique. Une ligne contient un bit ''Valid'' qui indique si la page logique associée est swappée sur le disque dur ou non, et la position de la page physique correspondante en mémoire RAM. Elle peut aussi contenir des bits pour la protection mémoire, et bien d'autres. Les lignes sont aussi appelées des ''entrées de la table des pages''
[[File:Gestionnaire de mémoire virtuelle - Pagination et swapping.png|centre|vignette|upright=2|Table des pages.]]
De plus, le système d'exploitation conserve une '''liste des pages vides'''. Le nom est assez clair : c'est une liste de toutes les pages de la mémoire physique qui sont inutilisées, qui ne sont allouées à aucun processus. Ces pages sont de la mémoire libre, utilisable à volonté. La liste des pages vides est mise à jour à chaque fois qu'un programme réserve de la mémoire, des pages sont alors prises dans cette liste et sont allouées au programme demandeur.
====Les défauts de page====
Lorsque l'on veut traduire l'adresse logique d'une page mémoire, le processeur vérifie le bit ''Valid'' et l'adresse physique. Si le bit ''Valid'' est à 1 et que l'adresse physique est présente, la traduction d'adresse s'effectue normalement. Mais si ce n'est pas le cas, l'entrée de la table des pages ne contient pas de quoi faire la traduction d'adresse. Soit parce que la page est swappée sur le disque dur et qu'il faut la copier en RAM, soit parce que les droits d'accès ne le permettent pas, soit parce que la page n'a pas encore été allouée, etc. On fait alors face à un '''défaut de page'''. Un défaut de page a lieu quand la MMU ne peut pas associer l'adresse logique à une adresse physique, quelque qu'en soit la raison.
Il existe deux types de défauts de page : mineurs et majeurs. Un '''défaut de page majeur''' a lieu quand on veut accéder à une page déplacée sur le disque dur. Un défaut de page majeur lève une exception matérielle dont la routine rapatriera la page en mémoire RAM. S'il y a de la place en mémoire RAM, il suffit d'allouer une page vide et d'y copier la page chargée depuis le disque dur. Mais si ce n'est par le cas, on va devoir faire de la place en RAM en déplaçant une page mémoire de la RAM vers le disque dur. Dans tous les cas, c'est le système d'exploitation qui s'occupe du chargement de la page, le processeur n'est pas impliqué. Une fois la page chargée, la table des pages est mise à jour et la traduction d'adresse peut recommencer. Si je dis recommencer, c'est car l'accès mémoire initial est rejoué à l'identique, sauf que la traduction d'adresse réussit cette fois-ci.
Un '''défaut de page mineur''' a lieu dans des circonstances pas très intuitives : la page est en mémoire physique, mais l'adresse physique de la page n'est pas accessible. Par exemple, il est possible que des sécurités empêchent de faire la traduction d'adresse, pour des raisons de protection mémoire. Une autre raison est la gestion des adresses synonymes, qui surviennent quand on utilise des libraires partagées entre programmes, de la communication inter-processus, des optimisations de type ''copy-on-write'', etc. Enfin, une dernière raison est que la page a été allouée à un programme par le système d'exploitation, mais qu'il n'a pas encore attribué sa position en mémoire. Pour comprendre comment c'est possible, parlons rapidement de l'allocation paresseuse.
Imaginons qu'un programme fasse une demande d'allocation mémoire et se voit donc attribuer une ou plusieurs pages logiques. L'OS peut alors réagir de deux manières différentes. La première est d'attribuer une page physique immédiatement, en même temps que la page logique. En faisant ainsi, on ne peut pas avoir de défaut mineur, sauf en cas de problème de protection mémoire. Cette solution est simple, on l'appelle l{{'}}'''allocation immédiate'''. Une autre solution consiste à attribuer une page logique, mais l'allocation de la page physique se fait plus tard. Elle a lieu la première fois que le programme tente d'écrire/lire dans la page physique. Un défaut mineur a lieu, et c'est lui qui force l'OS à attribuer une page physique pour la page logique demandée. On parle alors d{{'}}'''allocation paresseuse'''. L'avantage est que l'on gagne en performance si des pages logiques sont allouées mais utilisées, ce qui peut arriver.
Une optimisation permise par l'existence des défauts mineurs est le '''''copy-on-write'''''. Le but est d'optimiser la copie d'une page logique dans une autre. L'idée est que la copie est retardée quand elle est vraiment nécessaire, à savoir quand on écrit dans la copie. Tant que l'on ne modifie pas la copie, les deux pages logiques, originelle et copiée, pointent vers la même page physique. A quoi bon avoir deux copies avec le même contenu ? Par contre, la page physique est marquée en lecture seule. La moindre écriture déclenche une erreur de protection mémoire, et un défaut mineur. Celui-ci est géré par l'OS, qui effectue alors la copie dans une nouvelle page physique.
Je viens de dire que le système d'exploitation gère les défauts de page majeurs/mineurs. Un défaut de page déclenche une exception matérielle, qui passe la main au système d'exploitation. Le système d'exploitation doit alors déterminer ce qui a levé l'exception, notamment identifier si c'est un défaut de page mineur ou majeur. Pour cela, le processeur a un ou plusieurs '''registres de statut''' qui indique l'état du processeur, qui sont utiles pour gérer les défauts de page. Ils indiquent quelle est l'adresse fautive, si l'accès était une lecture ou écriture, si l'accès a eu lieu en espace noyau ou utilisateur (les espaces mémoire ne sont pas les mêmes), etc. Les registres en question varient grandement d'une architecture de processeur à l'autre, aussi on ne peut pas dire grand chose de plus sur le sujet. Le reste est de toute façon à voir dans un cours sur les systèmes d'exploitation.
====Le remplacement des pages====
Les pages virtuelles font référence soit à une page en mémoire physique, soit à une page sur le disque dur. Mais l'on ne peut pas lire une page directement depuis le disque dur. Les pages sur le disque dur doivent être chargées en RAM, avant d'être utilisables. Ce n'est possible que si on a une page mémoire vide, libre. Si ce n'est pas le cas, on doit faire de la place en swappant une page sur le disque dur. Les pages font ainsi une sorte de va et vient entre le fichier d'échange et la RAM, suivant les besoins. Tout cela est effectué par une routine d'interruption du système d'exploitation, le processeur n'ayant pas vraiment de rôle là-dedans.
Supposons que l'on veuille faire de la place en RAM pour une nouvelle page. Dans une implémentation naïve, on trouve une page à évincer de la mémoire, qui est copiée dans le ''swapfile''. Toutes les pages évincées sont alors copiées sur le disque dur, à chaque remplacement. Néanmoins, cette implémentation naïve peut cependant être améliorée si on tient compte d'un point important : si la page a été modifiée depuis le dernier accès. Si le programme/processeur a écrit dans la page, alors celle-ci a été modifiée et doit être sauvegardée sur le ''swapfile'' si elle est évincée. Par contre, si ce n'est pas le cas, la page est soit initialisée, soit déjà présente à l'identique dans le ''swapfile''.
Mais cette optimisation demande de savoir si une écriture a eu lieu dans la page. Pour cela, on ajoute un '''''dirty bit''''' à chaque entrée de la table des pages, juste à côté du bit ''Valid''. Il indique si une écriture a eu lieu dans la page depuis qu'elle a été chargée en RAM. Ce bit est mis à jour par le processeur, automatiquement, lors d'une écriture. Par contre, il est remis à zéro par le système d'exploitation, quand la page est chargée en RAM. Si le programme se voit allouer de la mémoire, il reçoit une page vide, et ce bit est initialisé à 0. Il est mis à 1 si la mémoire est utilisée. Quand la page est ensuite swappée sur le disque dur, ce bit est remis à 0 après la sauvegarde.
Sur la majorité des systèmes d'exploitation, il est possible d'interdire le déplacement de certaines pages sur le disque dur. Ces pages restent alors en mémoire RAM durant un temps plus ou moins long, parfois en permanence. Cette possibilité simplifie la vie des programmeurs qui conçoivent des systèmes d'exploitation : essayez d'exécuter l'interruption pour les défauts de page alors que la page contenant le code de l'interruption est placée sur le disque dur ! Là encore, cela demande d'ajouter un bit dans chaque entrée de la table des pages, qui indique si la page est swappable ou non. Le bit en question s'appelle souvent le '''bit ''swappable'''''.
====Les algorithmes de remplacement des pages pris en charge par l'OS====
Le choix de la page doit être fait avec le plus grand soin et il existe différents algorithmes qui permettent de décider quelle page supprimer de la RAM. Leur but est de swapper des pages qui ne seront pas accédées dans le futur, pour éviter d'avoir à faire triop de va-et-vient entre RAM et ''swapfile''. Les données qui sont censées être accédées dans le futur doivent rester en RAM et ne pas être swappées, autant que possible. Les algorithmes les plus simples pour le choix de page à évincer sont les suivants.
Le plus simple est un algorithme aléatoire : on choisit la page au hasard. Mine de rien, cet algorithme est très simple à implémenter et très rapide à exécuter. Il ne demande pas de modifier la table des pages, ni même d'accéder à celle-ci pour faire son choix. Ses performances sont surprenamment correctes, bien que largement en-dessous de tous les autres algorithmes.
L'algorithme FIFO supprime la donnée qui a été chargée dans la mémoire avant toutes les autres. Cet algorithme fonctionne bien quand un programme manipule des tableaux de grande taille, mais fonctionne assez mal dans le cas général.
L'algorithme LRU supprime la donnée qui été lue ou écrite pour la dernière fois avant toutes les autres. C'est théoriquement le plus efficace dans la majorité des situations. Malheureusement, son implémentation est assez complexe et les OS doivent modifier la table des pages pour l'implémenter.
L'algorithme le plus utilisé de nos jours est l{{'}}'''algorithme NRU''' (''Not Recently Used''), une simplification drastique du LRU. Il fait la différence entre les pages accédées il y a longtemps et celles accédées récemment, d'une manière très binaire. Les deux types de page sont appelés respectivement les '''pages froides''' et les '''pages chaudes'''. L'OS swappe en priorité les pages froides et ne swappe de page chaude que si aucune page froide n'est présente. L'algorithme est simple : il choisit la page à évincer au hasard parmi une page froide. Si aucune page froide n'est présente, alors il swappe au hasard une page chaude.
Pour implémenter l'algorithme NRU, l'OS mémorise, dans chaque entrée de la table des pages, si la page associée est froide ou chaude. Pour cela, il met à 0 ou 1 un bit dédié : le '''bit ''Accessed'''''. La différence avec le bit ''dirty'' est que le bit ''dirty'' est mis à jour uniquement lors des écritures, alors que le bit ''Accessed'' l'est aussi lors d'une lecture. Uen lecture met à 1 le bit ''Accessed'', mais ne touche pas au bit ''dirty''. Les écritures mettent les deux bits à 1.
Implémenter l'algorithme NRU demande juste de mettre à jour le bit ''Accessed'' de chaque entrée de la table des pages. Et sur les architectures modernes, le processeur s'en charge automatiquement. A chaque accès mémoire, que ce soit en lecture ou en écriture, le processeur met à 1 ce bit. Par contre, le système d'exploitation le met à 0 à intervalles réguliers. En conséquence, quand un remplacement de page doit avoir lieu, les pages chaudes ont de bonnes chances d'avoir le bit ''Accessed'' à 1, alors que les pages froides l'ont à 0. Ce n'est pas certain, et on peut se trouver dans des cas où ce n'est pas le cas. Par exemple, si un remplacement a lieu juste après la remise à zéro des bits ''Accessed''. Le choix de la page à remplacer est donc imparfait, mais fonctionne bien en pratique.
Tous les algorithmes précédents ont chacun deux variantes : une locale, et une globale. Avec la version locale, la page qui va être rapatriée sur le disque dur est une page réservée au programme qui est la cause du page miss. Avec la version globale, le système d'exploitation va choisir la page à virer parmi toutes les pages présentes en mémoire vive.
===La protection mémoire avec la pagination===
Avec la pagination, chaque page a des '''droits d'accès''' précis, qui permettent d'autoriser ou interdire les accès en lecture, écriture, exécution, etc. La table des pages mémorise les autorisations pour chaque page, sous la forme d'une suite de bits où chaque bit autorise/interdit une opération bien précise. En pratique, les tables de pages modernes disposent de trois bits : un qui autorise/interdit les accès en lecture, un qui autorise/interdit les accès en écriture, un qui autorise/interdit l'éxecution du contenu de la page.
Le format exact de la suite de bits a cependant changé dans le temps sur les processeurs x86 modernes. Par exemple, avant le passage au 64 bits, les CPU et OS ne pouvaient pas marquer une page mémoire comme non-exécutable. C'est seulement avec le passage au 64 bits qu'a été ajouté un bit pour interdire l'exécution de code depuis une page. Ce bit, nommé '''bit NX''', est à 0 si la page n'est pas exécutable et à 1 sinon. Le processeur vérifie à chaque chargement d'instruction si le bit NX de page lue est à 1. Sinon, il lève une exception matérielle et laisse la main à l'OS.
Une amélioration de cette protection est la technique dite du '''''Write XOR Execute''''', abréviée WxX. Elle consiste à interdire les pages d'être à la fois accessibles en écriture et exécutables. Il est possible de changer les autorisations en cours de route, ceci dit.
Les premiers IBM 360 disposaient d'un mécanisme de protection mémoire totalement différent, sans registres limite/base. Ce mécanisme de protection attribue à chaque programme une '''clé de protection''', qui consiste en un nombre unique de 4 bits (chaque programme a donc une clé différente de ses collègues). La mémoire est fragmentée en blocs de même taille, de 2 kibioctets. Le processeur mémorise, pour chacun de ses blocs, la clé de protection du programme qui a réservé ce bloc. À chaque accès mémoire, le processeur compare la clé de protection du programme en cours d’exécution et celle du bloc de mémoire de destination. Si les deux clés sont différentes, alors un programme a effectué un accès hors des clous et il se fait sauvagement arrêter.
===La traduction d'adresse avec la pagination===
Comme dit plus haut, les pages sont numérotées, de 0 à une valeur maximale, afin de les identifier. Le numéro en question est appelé le '''numéro de page'''. Il est utilisé pour dire au processeur : je veux lire une donnée dans la page numéro 20, la page numéro 90, etc. Une fois qu'on a le numéro de page, on doit alors préciser la position de la donnée dans la page, appelé le '''décalage''', ou encore l{{'}}''offset''.
Le numéro de page et le décalage se déduisent à partir de l'adresse, en divisant l'adresse par la taille de la page. Le quotient obtenu donne le numéro de la page, alors que le reste est le décalage. Les processeurs actuels utilisent tous des pages dont la taille est une puissance de deux, ce qui fait que ce calcul est fortement simplifié. Sous cette condition, le numéro de page correspond aux bits de poids fort de l'adresse, alors que le décalage est dans les bits de poids faible.
Le numéro de page existe en deux versions : un numéro de page physique qui identifie une page en mémoire physique, et un numéro de page logique qui identifie une page dans la mémoire virtuelle. Traduire l'adresse logique en adresse physique demande de remplacer le numéro de la page logique en un numéro de page physique.
[[File:Phycical address.JPG|centre|vignette|upright=2|Traduction d'adresse avec la pagination.]]
====Les tables des pages simples====
Dans le cas le plus simple, il n'y a qu'une seule table des pages, qui est adressée par les numéros de page logique. La table des pages est un vulgaire tableau d'adresses physiques, placées les unes à la suite des autres. Avec cette méthode, la table des pages a autant d'entrée qu'il y a de pages logiques en mémoire virtuelle. Accéder à la mémoire nécessite donc d’accéder d'abord à la table des pages en mémoire, de calculer l'adresse de l'entrée voulue, et d’y accéder.
[[File:Table des pages.png|centre|vignette|upright=2|Table des pages.]]
La table des pages est souvent stockée dans la mémoire RAM, son adresse est connue du processeur, mémorisée dans un registre spécialisé du processeur. Le processeur effectue automatiquement le calcul d'adresse à partir de l'adresse de base et du numéro de page logique.
[[File:Address translation (32-bit).png|centre|vignette|upright=2|Address translation (32-bit)]]
====Les tables des pages inversées====
Sur certains systèmes, notamment sur les architectures 64 bits ou plus, le nombre de pages est très important. Sur les ordinateurs x86 récents, les adresses sont en pratique de 48 bits, les bits de poids fort étant ignorés en pratique, ce qui fait en tout 68 719 476 736 pages. Chaque entrée de la table des pages fait au minimum 48 bits, mais fait plus en pratique : partons sur 64 bits par entrée, soit 8 octets. Cela fait 549 755 813 888 octets pour la table des pages, soit plusieurs centaines de gibioctets ! Une table des pages normale serait tout simplement impraticable.
Pour résoudre ce problème, on a inventé les '''tables des pages inversées'''. L'idée derrière celles-ci est l'inverse de la méthode précédente. La méthode précédente stocke, pour chaque page logique, son numéro de page physique. Les tables des pages inversées font l'inverse : elles stockent, pour chaque numéro de page physique, la page logique qui correspond. Avec cette méthode table des pages contient ainsi autant d'entrées qu'il y a de pages physiques. Elle est donc plus petite qu'avant, vu que la mémoire physique est plus petite que la mémoire virtuelle.
Quand le processeur veut convertir une adresse virtuelle en adresse physique, la MMU recherche le numéro de page de l'adresse virtuelle dans la table des pages. Le numéro de l'entrée à laquelle se trouve ce morceau d'adresse virtuelle est le morceau de l'adresse physique. Pour faciliter le processus de recherche dans la page, la table des pages inversée est ce que l'on appelle une table de hachage. C'est cette solution qui est utilisée sur les processeurs Power PC.
[[File:Table des pages inversée.jpg|centre|vignette|upright=2|Table des pages inversée.]]
====Les tables des pages multiples par espace d'adressage====
Dans les deux cas précédents, il y a une table des pages unique. Cependant, les concepteurs de processeurs et de systèmes d'exploitation ont remarqué que les adresses les plus hautes et/ou les plus basses sont les plus utilisées, alors que les adresses situées au milieu de l'espace d'adressage sont peu utilisées en raison du fonctionnement de la pile et du tas. Il y a donc une partie de la table des pages qui ne sert à rien et est utilisé pour des adresses inutilisées. C'est une source d'économie d'autant plus importante que les tables des pages sont de plus en plus grosses.
Pour profiter de cette observation, les concepteurs d'OS ont décidé de découper l'espace d'adressage en plusieurs sous-espaces d'adressage de taille identique : certains localisés dans les adresses basses, d'autres au milieu, d'autres tout en haut, etc. Et vu que l'espace d'adressage est scindé en plusieurs parties, la table des pages l'est aussi, elle est découpée en plusieurs sous-tables. Si un sous-espace d'adressage n'est pas utilisé, il n'y a pas besoin d'utiliser de la mémoire pour stocker la table des pages associée. On ne stocke que les tables des pages pour les espaces d'adressage utilisés, ceux qui contiennent au moins une donnée.
L'utilisation de plusieurs tables des pages ne fonctionne que si le système d'exploitation connaît l'adresse de chaque table des pages (celle de la première entrée). Pour cela, le système d'exploitation utilise une super-table des pages, qui stocke les adresses de début des sous-tables de chaque sous-espace. En clair, la table des pages est organisé en deux niveaux, la super-table étant le premier niveau et les sous-tables étant le second niveau.
L'adresse est structurée de manière à tirer profit de cette organisation. Les bits de poids fort de l'adresse sélectionnent quelle table de second niveau utiliser, les bits du milieu de l'adresse sélectionne la page dans la table de second niveau et le reste est interprété comme un ''offset''. Un accès à la table des pages se fait comme suit. Les bits de poids fort de l'adresse sont envoyés à la table de premier niveau, et sont utilisés pour récupérer l'adresse de la table de second niveau adéquate. Les bits au milieu de l'adresse sont envoyés à la table de second niveau, pour récupérer le numéro de page physique. Le tout est combiné avec l{{'}}''offset'' pour obtenir l'adresse physique finale.
[[File:Table des pages hiérarchique.png|centre|vignette|upright=2|Table des pages hiérarchique.]]
On peut aussi aller plus loin et découper la table des pages de manière hiérarchique, chaque sous-espace d'adressage étant lui aussi découpé en sous-espaces d'adressages. On a alors une table de premier niveau, plusieurs tables de second niveau, encore plus de tables de troisième niveau, et ainsi de suite. Cela peut aller jusqu'à 5 niveaux sur les processeurs x86 64 bits modernes. On parle alors de '''tables des pages emboitées'''. Dans ce cours, la table des pages désigne l'ensemble des différents niveaux de cette organisation, toutes les tables inclus. Seules les tables du dernier niveau mémorisent des numéros de page physiques, les autres tables mémorisant des pointeurs, des adresses vers le début des tables de niveau inférieur. Un exemple sera donné plus bas, dans la section suivante.
====L'exemple des processeurs x86====
Pour rendre les explications précédentes plus concrètes, nous allons prendre l'exemple des processeur x86 anciens, de type 32 bits. Les processeurs de ce type utilisaient deux types de tables des pages : une table des page unique et une table des page hiérarchique. Les deux étaient utilisées dans cas séparés. La table des page unique était utilisée pour les pages larges et encore seulement en l'absence de la technologie ''physical adress extension'', dont on parlera plus bas. Les autres cas utilisaient une table des page hiérarchique, à deux niveaux, trois niveaux, voire plus.
Une table des pages unique était utilisée pour les pages larges (de 2 mébioctets et plus). Pour les pages de 4 mébioctets, il y avait une unique table des pages, adressée par les 10 bits de poids fort de l'adresse, les bits restants servant comme ''offset''. La table des pages contenait 1024 entrées de 4 octets chacune, ce qui fait en tout 4 kibioctet pour la table des pages. La table des page était alignée en mémoire sur un bloc de 4 kibioctet (sa taille).
[[File:X86 Paging 4M.svg|centre|vignette|upright=2|X86 Paging 4M]]
Pour les pages de 4 kibioctets, les processeurs x86-32 bits utilisaient une table des page hiérarchique à deux niveaux. Les 10 bits de poids fort l'adresse adressaient la table des page maitre, appelée le directoire des pages (''page directory''), les 10 bits précédents servaient de numéro de page logique, et les 12 bits restants servaient à indiquer la position de l'octet dans la table des pages. Les entrées de chaque table des pages, mineure ou majeure, faisaient 32 bits, soit 4 octets. Vous remarquerez que la table des page majeure a la même taille que la table des page unique obtenue avec des pages larges (de 4 mébioctets).
[[File:X86 Paging 4K.svg|centre|vignette|upright=2|X86 Paging 4K]]
La technique du '''''physical adress extension''''' (PAE), utilisée depuis le Pentium Pro, permettait aux processeurs x86 32 bits d'adresser plus de 4 gibioctets de mémoire, en utilisant des adresses physiques de 64 bits. Les adresses virtuelles de 32 bits étaient traduites en adresses physiques de 64 bits grâce à une table des pages adaptée. Cette technologie permettait d'adresser plus de 4 gibioctets de mémoire au total, mais avec quelques limitations. Notamment, chaque programme ne pouvait utiliser que 4 gibioctets de mémoire RAM pour lui seul. Mais en lançant plusieurs programmes, on pouvait dépasser les 4 gibioctets au total. Pour cela, les entrées de la table des pages passaient à 64 bits au lieu de 32 auparavant.
La table des pages gardait 2 niveaux pour les pages larges en PAE.
[[File:X86 Paging PAE 2M.svg|centre|vignette|upright=2|X86 Paging PAE 2M]]
Par contre, pour les pages de 4 kibioctets en PAE, elle était modifiée de manière à ajouter un niveau de hiérarchie, passant de deux niveaux à trois.
[[File:X86 Paging PAE 4K.svg|centre|vignette|upright=2|X86 Paging PAE 4K]]
En 64 bits, la table des pages est une table des page hiérarchique avec 5 niveaux. Seuls les 48 bits de poids faible des adresses sont utilisés, les 16 restants étant ignorés.
[[File:X86 Paging 64bit.svg|centre|vignette|upright=2|X86 Paging 64bit]]
===La TLB et les caches associés===
En théorie, la table des pages est censée être accédée à chaque accès mémoire. Mais pour éviter d'avoir à lire la table des pages en mémoire RAM à chaque accès mémoire, les concepteurs de processeurs ont décidé d'implanter un cache dédié, le '''''translation lookaside buffer''''', ou TLB. Le TLB stocke au minimum de quoi faire la traduction entre adresse virtuelle et adresse physique, à savoir une correspondance entre numéro de page logique et numéro de page physique. Pour faire plus général, il stocke des entrées de la table des pages.
[[File:MMU principle updated.png|centre|vignette|upright=2.0|MMU avec une TLB.]]
Les accès à la table des pages sont gérés de deux façons : soit le processeur gère tout seul la situation, soit il délègue cette tâche au système d’exploitation. Sur les processeurs anciens, le système d'exploitation gère le parcours de la table des pages. Mais cette solution logicielle n'a pas de bonnes performances. D'autres processeurs gèrent eux-mêmes le défaut d'accès à la TLB et vont chercher d'eux-mêmes les informations nécessaires dans la table des pages. Ils disposent de circuits, les '''''page table walkers''''' (PTW), qui s'occupent eux-mêmes du défaut.
Les ''page table walkers'' contiennent des registres qui leur permettent de faire leur travail. Le plus important est celui qui mémorise la position de la table des pages en mémoire RAM, dont nous avons parlé plus haut. Les PTW ont besoin, pour faire leur travail, de mémoriser l'adresse physique de la table des pages, ou du moins l'adresse de la table des pages de niveau 1 pour des tables des pages hiérarchiques. Mais d'autres registres existent. Toutes les informations nécessaires pour gérer les défauts de TLB sont stockées dans des registres spécialisés appelés des '''tampons de PTW''' (PTW buffers).
===L'abstraction matérielle des processus===
[[File:Memoire virtuelle.svg|vignette|Mémoire virtuelle]]
Il est possible d'implémenter l'abstraction matérielle des processus avec la pagination. En clair, chaque programme lancé sur l'ordinateur dispose de son propre espace d'adressage, ce qui fait que la même adresse logique ne pointera pas sur la même adresse physique dans deux programmes différents. Pour cela, il y a plusieurs méthodes.
La première solution n'utilise qu'une seule table des pages. Pour cela, chaque entrée indique à quel processus elle est associée. Pour cela, chaque entrée contient un '''identifiant de processus''', un numéro qui précise pour quel processus, pour quel espace d'adressage, la correspondance est valide. La page des tables peut aussi contenir des entrées qui sont valides pour tous les processus en même temps. Pour cela, l'entrée contient un '''bit ''global''''', qui précise que cette correspondance est valide pour tous les processus, l'identifiant de processus est ignoré. Un défaut est que le partage d'une page entre plusieurs processus est impossible : soit on partage une page avec tous les processus, soit on l'alloue avec un seul processus.
L'intérêt de partager des pages entre tous les processus n'est pas évident, mais il le devient quand on se rappelle que le noyau de l'OS est mappé dans le haut de l'espace d'adressage. Et peu importe l'espace d'adressage, le noyau est toujours mappé de manière identique, les mêmes adresses logiques adressant la même adresse mémoire. En conséquence, les correspondances adresse physique-logique sont les mêmes pour le noyau, peu importe l'espace d'adressage. Le bit global accélère rapidement la traduction d'adresse pour l'accès au noyau.
Une solution alternative, plus simple, utilise une table des pages par processus lancé sur l'ordinateur, une table des pages unique par espace d'adressage. À chaque changement de processus, le registre qui mémorise la position de la table des pages est modifié pour pointer sur la bonne. C'est le système d'exploitation qui se charge de cette mise à jour.
Avec cette méthode, il est possible de partager une ou plusieurs pages entre plusieurs processus, en configurant les tables des pages convenablement. Les pages partagées sont mappées dans l'espace d'adressage de plusieurs processus, mais pas forcément au même endroit, pas forcément dans les mêmes adresses logiques. On peut placer la page partagée à l'adresse logique 0x0FFF pour un processus, à l'adresse logique 0xFF00 pour un autre processus, etc. Par contre, les entrées de la table des pages pour ces adresses pointent vers la même adresse physique.
[[File:Vm5.png|centre|vignette|upright=2|Tables des pages de plusieurs processus.]]
===La taille des pages===
La taille des pages varie suivant le processeur et le système d'exploitation et tourne souvent autour de 4 kibioctets. Les processeurs actuels gèrent plusieurs tailles différentes pour les pages : 4 kibioctets par défaut, 2 mébioctets, voire 1 à 4 gibioctets pour les pages les plus larges. Les pages de 4 kibioctets sont les pages par défaut, les autres tailles de page sont appelées des ''pages larges''. La taille optimale pour les pages dépend de nombreux paramètres et il n'y a pas de taille qui convienne à tout le monde. Certaines applications gagnent à utiliser des pages larges, d'autres vont au contraire perdre drastiquement en performance en les utilisant.
Le désavantage principal des pages larges est qu'elles favorisent la fragmentation mémoire. Si un programme veut réserver une portion de mémoire, pour une structure de donnée quelconque, il doit réserver une portion dont la taille est multiple de la taille d'une page. Par exemple, un programme ayant besoin de 110 kibioctets allouera 28 pages de 4 kibioctets, soit 120 kibioctets : 2 kibioctets seront perdus. Par contre, avec des pages larges de 2 mébioctets, on aura une perte de 2048 - 110 = 1938 kibioctets. En somme, des morceaux de mémoire seront perdus, car les pages sont trop grandes pour les données qu'on veut y mettre. Le résultat est que le programme qui utilise les pages larges utilisent plus de mémoire et ce d'autant plus qu'il utilise des données de petite taille. Un autre désavantage est qu'elles se marient mal avec certaines techniques d'optimisations de type ''copy-on-write''.
Mais l'avantage est que la traduction des adresses est plus performante. Une taille des pages plus élevée signifie moins de pages, donc des tables des pages plus petites. Et des pages des tables plus petites n'ont pas besoin de beaucoup de niveaux de hiérarchie, voire peuvent se limiter à des tables des pages simples, ce qui rend la traduction d'adresse plus simple et plus rapide. De plus, les programmes ont une certaine localité spatiale, qui font qu'ils accèdent souvent à des données proches. La traduction d'adresse peut alors profiter de systèmes de mise en cache dont nous parlerons dans le prochain chapitre, et ces systèmes de cache marchent nettement mieux avec des pages larges.
Il faut noter que la taille des pages est presque toujours une puissance de deux. Cela a de nombreux avantages, mais n'est pas une nécessité. Par exemple, le tout premier processeur avec de la pagination, le super-ordinateur Atlas, avait des pages de 3 kibioctets. L'avantage principal est que la traduction de l'adresse physique en adresse logique est trivial avec une puissance de deux. Cela garantit que l'on peut diviser l'adresse en un numéro de page et un ''offset'' : la traduction demande juste de remplacer les bits de poids forts par le numéro de page voulu. Sans cela, la traduction d'adresse implique des divisions et des multiplications, qui sont des opérations assez couteuses.
===Les entrées de la table des pages===
Avant de poursuivre, faisons un rapide rappel sur les entrées de la table des pages. Nous venons de voir que la table des pages contient de nombreuses informations : un bit ''valid'' pour la mémoire virtuelle, des bits ''dirty'' et ''accessed'' utilisés par l'OS, des bits de protection mémoire, un bit ''global'' et un potentiellement un identifiant de processus, etc. Étudions rapidement le format de la table des pages sur un processeur x86 32 bits.
* Elle contient d'abord le numéro de page physique.
* Les bits AVL sont inutilisés et peuvent être configurés à loisir par l'OS.
* Le bit G est le bit ''global''.
* Le bit PS vaut 0 pour une page de 4 kibioctets, mais est mis à 1 pour une page de 4 mébioctets dans le cas où le processus utilise des pages larges.
* Le bit D est le bit ''dirty''.
* Le bit A est le bit ''accessed''.
* Le bit PCD indique que la page ne peut pas être cachée, dans le sens où le processeur ne peut copier son contenu dans le cache et doit toujours lire ou écrire cette page directement dans la RAM.
* Le bit PWT indique que les écritures doivent mettre à jour le cache et la page en RAM (dans le chapitre sur le cache, on verra qu'il force le cache à se comporter comme un cache ''write-through'' pour cette page).
* Le bit U/S précise si la page est accessible en mode noyau ou utilisateur.
* Le bit R/W indique si la page est accessible en écriture, toutes les pages sont par défaut accessibles en lecture.
* Le bit P est le bit ''valid''.
[[File:PDE.png|centre|vignette|upright=2.5|Table des pages des processeurs Intel 32 bits.]]
==Comparaison des différentes techniques d'abstraction mémoire==
Pour résumer, l'abstraction mémoire permet de gérer : la relocation, la protection mémoire, l'isolation des processus, la mémoire virtuelle, l'extension de l'espace d'adressage, le partage de mémoire, etc. Elles sont souvent implémentées en même temps. Ce qui fait qu'elles sont souvent confondues, alors que ce sont des concepts sont différents. Ces liens sont résumés dans le tableau ci-dessous.
{|class="wikitable"
|-
!
! colspan="5" | Avec abstraction mémoire
! rowspan="2" | Sans abstraction mémoire
|-
!
! Relocation matérielle
! Segmentation en mode réel (x86)
! Segmentation, général
! Architectures à capacités
! Pagination
|-
! Abstraction matérielle des processus
| colspan="4" | Oui, relocation matérielle
| Oui, liée à la traduction d'adresse
| Emulée via relocation logicielle au démarrage du programme.
|-
! Mémoire virtuelle
| colspan="2" | Non, sauf émulation logicielle
| colspan="3" | Oui, gérée par le processeur et l'OS
| Non, sauf émulation logicielle
|-
! Extension de l'espace d'adressage
| colspan="2" | Oui : registre de base élargi
| colspan="2" | Oui : adresse de base élargie dans la table des segments
| ''Physical Adress Extension'' des processeurs 32 bits
| Commutation de banques
|-
! Protection mémoire
| Registre limite
| Aucune
| colspan="2" | Registre limite, droits d'accès aux segments
| Gestion des droits d'accès aux pages
| Possible, méthodes variées
|-
! Partage de mémoire
| colspan="2" | Non
| colspan="2" | Segment partagés
| Pages partagées
| Possible, méthodes variées
|}
===Les différents types de segmentation===
La segmentation regroupe plusieurs techniques franchement différentes, qui auraient gagné à être nommées différemment. La principale différence est l'usage de registres de relocation versus des registres de sélecteurs de segments. L'usage de registres de relocation est le fait de la relocation matérielle, mais aussi de la segmentation en mode réel des CPU x86. Par contre, l'usage de sélecteurs de segments est le fait des autres formes de segmentation, architectures à capacité inclues.
La différence entre les deux est le nombre de segments. L'usage de registres de relocation fait que le CPU ne gère qu'un petit nombre de segments de grande taille. La mémoire virtuelle est donc rarement implémentée vu que swapper des segments de grande taille est trop long, l'impact sur les performances est trop important. Sans compter que l'usage de registres de base se marie très mal avec la mémoire virtuelle. Vu qu'un segment peut être swappé ou déplacée n'importe quand, il faut invalider les registres de base au moment du swap/déplacement, ce qui n'est pas chose aisée. Aucun processeur ne gère cela, les méthodes pour n'existent tout simplement pas. L'usage de registres de base implique que la mémoire virtuelle est absente.
La protection mémoire est aussi plus limitée avec l'usage de registres de relocation. Elle se limite à des registres limite, mais la gestion des droits d'accès est limitée. En théorie, la segmentation en mode réel pourrait implémenter une version limitée de protection mémoire, avec une protection de l'espace exécutable. Mais ca n'a jamais été fait en pratique sur les processeurs x86.
Le partage de la mémoire est aussi difficile sur les architectures avec des registres de base. L'absence de table des segments fait que le partage d'un segment est basiquement impossible sans utiliser des méthodes complétement tordues, qui ne sont jamais implémentées en pratique.
===Segmentation versus pagination===
Par rapport à la pagination, la segmentation a des avantages et des inconvénients. Tous sont liés aux propriétés des segments et pages : les segments sont de grande taille et de taille variable, les pages sont petites et de taille fixe.
L'avantage principal de la segmentation est sa rapidité. Le fait que les segments sont de grande taille fait qu'on a pas besoin d'équivalent aux tables des pages inversée ou multiple, juste d'une table des segments toute simple. De plus, les échanges entre table des pages/segments et registres sont plus rares avec la segmentation. Par exemple, si un programme utilise un segment de 2 gigas, tous les accès dans le segment se feront avec une seule consultation de la table des segments. Alors qu'avec la pagination, il faudra une consultation de la table des pages chaque bloc de 4 kibioctet, au minimum.
Mais les désavantages sont nombreux. Le système d'exploitation doit agencer les segments en RAM, et c'est une tâche complexe. Le fait que les segments puisse changer de taille rend le tout encore plus complexe. Par exemple, si on colle les segments les uns à la suite des autres, changer la taille d'un segment demande de réorganiser tous les segments en RAM, ce qui demande énormément de copies RAM-RAM. Une autre possibilité est de laisser assez d'espace entre les segments, mais cet espace est alors gâché, dans le sens où on ne peut pas y placer un nouveau segment.
Swapper un segment est aussi très long, vu que les segments sont de grande taille, alors que swapper une page est très rapide.
==Un cas particulier : la z/architecture d'IBM==
IBM a sa propre gamme de ''mainframes'', qui utilisent des processeurs fait maison par IBM lui-même. Les CPU IBM ont leur propre jeu d'instruction CISC, appelé '''''z/architecture'''''. Et ce jeu d'instruction a des possibilités en termes d'adressage qui sont extrêmement intéressantes, en plus d'être assez inédites.
Nous allons nous concentrer sur une fonctionnalité : les programmes ont accès à plusieurs espaces d'adressage simultanément. La ''z/architecture'' supporte 65 536 espaces d'adressage simultanés. Par contre, un programme ne peut en utiliser que 2048 en même temps. De plus, ils ne sont accessibles que quand le processeur est dans un mode de fonctionnement bien précis. Un processeur ''z/architecture'' peut en effet fonctionner dans deux modes, qu'on va appeler "standard" et "étendu".
L'existence de ces deux modes s'explique par le fait que les ''mainframes'' IBM ont évolués dans le temps. Les premiers ''mainframes'' n'avaient même pas de support basique de la mémoire virtuelle, le System/360 s'en passait totalement. C'est sur le System/370 que la pagination a été ajoutée. Le System/370 supportait deux espaces d'adressage virtuel par programme, là où les implémentations classiques n'en utilisent qu'un par programme. L'ESA/370 a ajouté la possibilité d'utiliser 16 espaces d'adressage par programme, mais l'a fait d'une manière à garder la compatibilité avec le System/370. Le processeur a donc deux modes : un avec deux espaces d'adressage par programme, un autre pouvant monter jusqu’à 16.
La taille des adresses a aussi évoluée dans le temps : partant de 24 bits sur le System/360, elle est montée à 31 bits sur le System/370, pour arriver à 64 bits sur le jeu d'instruction z/architecture proprement dit. Sur le System/370, il était possible de switcher à la volée entre adressage 24 et 31 bits, avec les instructions ''Branch and save and
set mode'' (BASSM) et ''Branch And Set Mode'' (BSM).
===Le System/370===
Le System/370 gérait des pages de 4 kibioctets, les pages larges ayant été introduites plus tard. La traduction d’adresse se fait en passant des regroupements de plusieurs pages virtuelles, appelés des ''segments'', qui n'ont rien à voir avec les segments au sens de la segmentation. Pour éviter les confusions, je vais les appeler des ''super-pages''. Les super-pages font 64 kibioctets ou 1 mébioctet, et regroupent plusieurs pages de 2 ou 4 mébioctets. Les pages d'une super-page peuvent être très éloignées en mémoire physique. Les super-pages sont concrètement des intermédiaires utilisés pour la traduction d'adresse. La traduction d'adresse démarre à une table des super-pages, qui point vers une table des pages à plusieurs niveaux (5 maximum).
Le System/370 était un système multi-processeur, ce qui a amené à quelques spécificités en termes d'adressage mémoire. La page numéro 0 sert pour le vecteur d'interruption et d'autres données importantes, pour des données importantes. Et chaque processeur a besoin de sa propre table d'interruption. Pour cela, chaque processeur réserve une page dédiée pour son vecteur d'interruption. Quand ce processeur accède à une adresse située dans la page numéro 0, l'adresse est modifiée pour tomber dans cette page réservée. Notons que le numéro de page converti est un numéro de page physique ! Le processeur fait donc une première traduction d'adresse pour déterminer l'adresse physique, et celle-ci est corrigée une seconde fois en cas d'accès à la première page mémoire. Le tout porte le nom de technique de '''''prefixing'''''.
Le System/370 supporte deux espaces d'adressages par programme : l''''espace d'adressage primaire''' où se situent le programme exécuté et les données importantes, un '''espace d'adressage secondaire''' utilisé pour des tâches/données annexes. Un troisième espace d'adressage l''''espace d'adressage ''home''''', est réservé au système d'exploitation. Il a pour particularité de ne pas avoir d'ASN. Il n'est concrètement accessible qu'en mode superviseur, donc par le système d'exploitation. C'est en quelque sorte un espace d'adressage pour le noyau, séparé des espaces d'adressage utilisateur. Nous n'en parlerons pas dans ce qui suit.
Mais comment le processeur pour utiliser ces espaces d'adressages ? Par exemple, s'il veut faire une lecture, comment précise-t-il l'espace d'adressage courant ? La réponse est que le processeur peut être dans deux états : soit primaire, soit secondaire. Dans le premier état, toutes les lectures/écritures se font dans l'espace d'adressage primaire. Dans le second, elles se font toutes dans l'espace d'adressage secondaire. Il peut passer de l'un à l'autre en utilisant l'instruction ''Set Address Space Control''. Le processeur dispose aussi d'instructions ''MOVE TO PRIMARY'' et ''MOVE TO SECONDARY'' pour échanger des données dans les deux espaces d'adressage.
L'espace d'adressage primaire a une table des pages dédiée, séparée de celle de l'espace d'adressage secondaire. Ou plutôt devrais-je dire une table des super-pages. La table des pages est une table des pages emboitée. Il y a peu à détailler dessus, si ce n'est pour cette histoire de segments/super-pages. L'adresse de la table des super-page est mémorisée dans le registre de contrôle CR1 pour l'espace d'adressage primaire, CR7 pour l'espace d'adressage secondaire. L'un ou l'autre est utilisé, selon que le processeur est en mode "primaire" ou "secondaire".
Un programme peut donc passer de l'espace d'adressage primaire à secondaire (ou inversement) en utilisant l’instruction ''SET ADDRESS SPACE CONTROL''.
===Les registres d'accès de l'ESA370===
Si le System/370 permettait d'utiliser deux espaces d'adressage par programme, l'ESA 370 a poussé la logique nettement plus loin. Il a ajouté un mode de fonctionnement supplémentaire, qui permet d'utiliser jusqu'à 2048 espaces d'adressage différents, en même temps. Le processeur n'a pas besoin d'utiliser d'instructions particulières pour copier des données d'un espace d'adressage à un autre, un simple MOV suffit. Le mode en question est appelé le '''mode ''access register'''''. Il est complémentaire du mode à deux espaces d'adressage, pour des raisons de compatibilité, l'ancien mode à deux esapces d'adressage a été conservé.
Le mode ''access register'' donne accès aux '''registres d'accès''', qui lui donnent son nom. Les registres d'accès mémorisent des identifiants qui permettent de sélectionner un espace d'adressage parmi 2048. Il y a en tout 16 registres d'accès, chacun étant appairé avec un registre général. Par exemple, le registre général numéro 5 est appairé au registre d'accès de même numéro. En mode ''access register'', toute lecture/écriture lit l'adresse dans un registre général et le registre d'accès associé indique dans quel espace d'adressage se trouve cette adresse. Par exemple, une lecture utilise l'adresse dans le registre général numéro 7, l'espace d'adressage associé sera dans le registre d'accès numéro 7.
Les registres d'accès peuvent être modifiés par des instructions classiques, en espace utilisateur. Elles sont nommées COPY ACCESS, EXTRACT ACCESS, LOAD ACCESS MULTIPLE, LOAD ADDRESS EXTENDED, SET ACCESS et STORE ACCESS MULTIPLE. Elles permettent de copier un registre d'accès dans un autre, de charger le contenu d'un registre d'accès depuis la mémoire RAM, d'enregistrer les registres d'accès dans la mémoire RAM, de copier une constante dans un registre d'accès. Mais quel est le contenu de ces registres ?
Une implémentation simple numéroterait les espaces d'adressage et mettrait les numéros dans les registres d'accès. Un espace d'adressage serait associé à une table des super-pages, reste à mémoriser la correspondance ''numéro d'espace d'adressage - adresse de la table des super-pages''. Pour cela, l'idéal est de mémoriser ces correspondances dans un tableau en mémoire RAM. Le numéro d'espace d'adressage est alors un indice, qui pointe sur l'entrée adéquate dans cette table. Il devrait y avoir un tableau par programme, qui serait protégé en écriture et seulement accessible par le système d'exploitation, comme le serait une table des pages. Mais ce n'est pas exactement ce qui est fait.
La différence est qu'il n'y a pas une table de correspondance par programme, mais un système à deux tableaux imbriqués. Le premier tableau possède 65 536 entrées, une par espace d'adressage supporté par le processeur. C'est donc une table partagée par tous les programmes. Par contre, la seconde table est bien unique pour chaque programme, chaque programme a la sienne. Faire ainsi permet de simplifier le partage de la mémoire. La table partagée est l''''''Access Space Table Entry''''' (ASTE), alors que la seconde table est la '''liste privée''' (''access list''), sous-entendu "privée pour un programme".
La liste privée mémorise une liste des espaces d'adressage auquel le programme peut accéder, auxquels le système d'exploitation lui a accordé l'accès. Pour chaque espace d'adressage, elle renvoie vers l'entrée de l'ASTE associée. En plus de mémoriser l'adresse de cette entrée, elle mémorise les droits d'accès que le programme a sur l'espace d'adressage associé. Par exemple, si deux programmes partagent un espace d'adressage, l'un pourra y avoir accès en lecture seule, l'autre pourra avoir un accès total. Pour cela, ils ont des droits différents dans la liste privée, mais l'entrée de l'ASTE sera la même.
[[File:Tradiction d'adresse sur la zarchitecture d'IBM.png|centre|vignette|upright=2|Traduction d'adresse sur la zarchitecture d'IBM.]]
Il y a donc une vérification des droits d'accès, suivi d'un accès à une table des super-pages globale. Si un programme veut accéder à un espace d'adressage qui lui est interdit, l'accès mémoire stoppera dans la liste privée. Le processeur utilisera l'identifiant d'espace d'adressage, mais la liste privée n'aura pas d'entrée associée. Elle lèvera alors une exception matérielle et le système d'exploitation gérera la situation, vraisemblablement en tuant le programme fautif. Mais si l'accès est autorisé, alors le programme accède à la table des super-pages et l'accès mémoire se poursuit.
Je dis une liste d'accès privée, mais il y en a deux en réalité. Mais la distinction exacte demanderait de parler de ''threads'' et de processus. Pour ceux qui sont familiers avec ces concepts, chaque ''thread'' a sa propre liste privée, avec en plus une liste par processus, partagée entre tous les ''threads''. Deux listes de 1024 entrées maximum, cela fait 2048 entrées au total. Les deux listes portent les noms de ''Dispatchable Unit Access List'' et de ''PASN-AL (Primary Address Space Access List''. Inutile de retenir ces deux termes, cependant.
Un registre d'accès mémorise un '''''Access List Entry Token''''' ou ALET. L'ALET contient l'indice d'une entrée dans la liste privée. Elle contient aussi un bit qui indique quelle liste privée utiliser, entre la ''Dispatchable Unit Access List'' et la ''PASN-AL (Primary Address Space Access List''. Précisons cependant que deux valeurs font exceptions : 0 pointe vers l'espace d'adressage principal, 1 vers l'espace d'adressage secondaire. Les autres valeurs sont configurables.
{|class="wikitable"
|+ Registre d'accès
|-
! 0000000 !! L (choix de la liste privée !! ASLEN !! ALEN
|-
| 0000000 || Choix de la liste privée || Identifiant de protection || Indice dans la liste privée
|}
Lors d'une lecture/écriture, l'ALETs est utilisé pour sélectionner l'espace d'adressage. Les valeurs 0 et 1 sont traitées telles quelles, l'espace d'adressage est déterminé automatiquement par le processeur. Pour les autres valeurs, récupérer l'adresse mémoire demande de consulter la liste d'accès privée. L'ALET est envoyé à la liste d'accès privée, et cette dernière fournit l'adresse de l'entrée de l'ASTE à consulter. Le processeur connait l'adresse de la liste privée, car son adresse mémoire est mémorisée dans les registres de contrôle numéro 2 et 5.
: Précisons que le registre d'accès numéro 0 contient obligatoirement la valeur 0, ce qui fait qu'il pointe vers l'espace d'adressage primaire.
[[File:Traduction d'adresse - trois espaces d'adressage, zarchitecture d'IBM.png|centre|vignette|upright=2|Traduction d'adresse - trois espaces d'adressage, zarchitecture d'IBM.]]
Une subtilité est que le système d'exploitation peut à tout moment modifier le contenu de la liste privée. Il se peut qu'un ALET qui correspondait à un espace d'adressage se mette à pointer vers un autre. Pour détecter cette situation, le processeur attribue un identifiant, à chaque espace d'adressage. L'identifiant est mémorisé dans les entrées de la liste privée, de l'ASTE et dans les registres d'accès (dans les bits de poids fort de l'ALET). L'identifiant est modifié dans l'ALET à chaque fois que le système d'exploitation modifie l'entrée. Si un programme fait un accès avec un registre d'accès, mais que l'identifiant ne correspond pas avec celui dans l'ALET/liste privée, c'est signe que l'espace d'adressage a été swappé, une exception matérielle est levée.
Un tel fonctionnement vous aura sans doute fait penser à une architecture à capacité, mais la z/architecture n'en est pas une. Malgré des points de ressemblance assez importants, il y a des différences fondamentales avec les architectures à capacité.
* Le z/architecture ne supporte pas la segmentation, qui est nécessaire pour les architectures à capacité. Sur la z/architecture, ce sont des espaces d'adressage qui sont sélectionnés, pas des segments.
* Les registres d'accès ne sont pas des registres de capacité, car on peut les modifier avec des instructions en espace utilisateur. En comparaison, modifier les registres de capacité ne peut se faire qu'en espace noyau, sous l'égide du système d'exploitation.
* Les droits d'accès sont dans une table séparée des pointeurs, pas concaténés à ceux-ci. La gestion des droits d'accès est gérée par les tables des pages.
* Les identifiants ALET ne peuvent pas être passés à un autre programme. Deux ALETs identiques correspondent à deux espaces d'adressages différents dans deux programmes différents. L'identifiant peut être identique, il pointera vers l'entrée de même numéro dans les deux listes privées, mais l'adresse dans l'entrée peut être différente.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=L'espace d'adressage du processeur
| prevText=L'espace d'adressage du processeur
| next=Les méthodes de synchronisation entre processeur et périphériques
| nextText=Les méthodes de synchronisation entre processeur et périphériques
}}
</noinclude>
epiv7ggj8f947qzc382e5ajq2gcdtt7
775340
775339
2026-10-16T21:27:40Z
Mewtow
31375
/* Un cas particulier : la z/architecture d'IBM */
775340
wikitext
text/x-wiki
Pour introduire ce chapitre, nous devons faire un rappel sur le concept d{{'}}'''espace d'adressage'''. Pour rappel, un espace d'adressage correspond à l'ensemble des adresses utilisables par le processeur. Par exemple, si je prends un processeur 16 bits, il peut adresser en tout 2^16 = 65536 adresses, l'ensemble de ces adresses forme son espace d'adressage. Intuitivement, on s'attend à ce qu'il y ait correspondance avec les adresses de la mémoire RAM. J'entends par là que l'adresse 1209 de l'espace d'adressage correspond à l'adresse 1209 en mémoire RAM. C'est là une hypothèse parfaitement raisonnable et on voit mal comment ce pourrait ne pas être le cas.
Mais les processeurs modernes utilisent des techniques d{{'}}'''abstraction mémoire''' qui font que ce n'est pas le cas. Avec ces techniques, l'adresse 1209 de l'espace d'adressage correspond en réalité à l'adresse 9999 en mémoire RAM, voire n'est pas en RAM. L'abstraction mémoire fait que l'espace d'adressage regroupe des adresses fictives, qui doivent être traduites en adresses mémoires réelles pour être utilisées. Les adresses de l'espace d'adressage portent le nom d{{'}}'''adresses logiques''', alors que les adresses de la mémoire RAM sont appelées '''adresses physiques'''.
L'intérêt de l'abstraction matérielle n'est pas évident. Aussi, avant de parler de comment l'abstraction mémoire fonctionne, nous allons voir à quoi elle sert. Nous allons voir qu'elle a plusieurs utilisations différentes, qui sont absolument nécessaires sur tous les ordinateurs personnels modernes. Tous les processeurs modernes la prennent en charge, les systèmes d'exploitation coopérent avec le processeur pour l'utiliser au mieux.
==L'abstraction mémoire permet plusieurs fonctionnalités complémentaires==
La fonctionnalité la plus connue est la mémoire virtuelle, dont vous avez peut-être déjà entendu parler, peut-être que le nom vous dit quelque chose. Si ce n'est pas le cas, nous la détailleront dans la suite. Elle permet concrétement à un programme d'utiliser plus de mémoire qu'il n'y a de RAM installé dans un ordinateur, en utilisant le disque dur comme solution de secours. Mais d'autres fonctionnalités moins évidentes sont permises par l'abstraction mémoire. Et la première que nous allons voir est l'abstraction des processus.
: En général, une adresse logique correspond à une seule adresse physique. Mais beaucoup de fonctionnalités avancées ne respectent pas cette règle.
===L'abstraction matérielle des processus===
Les systèmes d'exploitation modernes sont dits multi-tâche, à savoir qu'ils sont capables d'exécuter plusieurs logiciels en même temps. Et ce même si un seul processeur est présent dans l'ordinateur : les logiciels sont alors exécutés à tour de rôle. Toutefois, cela amène un paquet de problèmes qu'il faut résoudre au mieux. Par exemple, les programmes exécutés doivent se partager la mémoire RAM, ce qui ne vient pas sans problèmes. Le problème principal est que les programmes ne doivent pas lire ou écrire dans les données d'un autre, sans quoi on se retrouverait rapidement avec des problèmes. Il faut donc introduire des mécanismes d{{'}}'''isolement des processus''', pour isoler les programmes les uns des autres.
Un de ces mécanismes est l{{'}}'''abstraction matérielle des processus''', une technique qui fait que chaque programme a son propre espace d'adressage. Chaque programme a l'impression d'avoir accès à tout l'espace d'adressage, de l'adresse 0 à l'adresse maximale gérée par le processeur. Évidemment, il s'agit d'une illusion maintenue justement grâce à la traduction d'adresse. Les espaces d'adressage contiennent des adresses logiques, les adresses de la RAM sont des adresses physiques, la nécessité de l'abstraction mémoire est évidente.
Implémenter l'abstraction mémoire peut se faire de plusieurs manières. Mais dans tous les cas, il faut que la correspondance adresse logique - physique change d'un programme à l'autre. Ce qui est normal, vu que les deux processus sont placés à des endroits différents en RAM physique. La conséquence est qu'avec l'abstraction mémoire, une adresse logique correspond à plusieurs adresses physiques. Une même adresse logique dans deux processus différents correspond à deux adresses phsiques différentes, une par processus. Une adresse logique dans un processus correspondra à l'adresse physique X, la même adresse dans un autre processus correspondra à l'adresse Y.
Les adresses physiques qui partagent la même adresse logique sont alors appelées des '''adresses homonymes'''. Le choix de la bonne adresse étant réalisé par un mécanisme matériel et dépend du programme en cours. Le mécanisme pour choisir la bonne adresse dépend du processeur, mais il y en a deux grands types :
* La première consiste à utiliser l'identifiant de processus CPU, vu au chapitre précédent. C'est, pour rappel, un numéro attribué à chaque processus par le processeur. L'identifiant du processus en cours d'exécution est mémorisé dans un registre du processeur. La traduction d'adresse utilise cet identifiant, en plus de l'adresse logique, pour déterminer l'adresse physique.
* La seconde solution mémorise les correspondances adresses logiques-physique dans des tables en mémoire RAM, qui sont différentes pour chaque programme. Les tables sont accédées à chaque accès mémoire, afin de déterminer l'adresse physique.
===Le partage de la mémoire===
L'isolation des processus est très importante sur les systèmes d'exploitation modernes. Cependant, il existe quelques situations où elle doit être contournée ou du moins mise en pause. Les situations sont multiples : gestion de bibliothèques partagées, communication entre processus, usage de ''threads'', etc. Elles impliquent toutes un '''partage de mémoire''', à savoir qu'une portion de mémoire RAM est partagée entre plusieurs programmes. Le partage de mémoire est une sorte de brèche de l'isolation des processus, mais qui est autorisée car elle est utile.
Un cas intéressant est celui des '''bibliothèques partagées'''. Les bibliothèques sont des collections de fonctions regroupées ensemble, dans une seule unité de code. Un programme qui utilise une bibliothèque peut appeler n’importe quelle fonction présente dans la bibliothèque. La bibliothèque peut être simplement inclue dans le programme lui-même, on parle alors de bibliothèques statiques. De telles bibliothèques fonctionnent très bien, mais avec un petit défaut pour les bibliothèques très utilisées : plusieurs programmes qui utilisent la même bibliothèque vont chacun l'inclure dans leur code, ce qui fera doublon.
Pour éviter cela, les OS modernes gèrent des bibliothèques partagées, à savoir qu'un seul exemplaire de la bibliothèque est partagé entre plusieurs programmes. Chaque programme peut exécuter une fonction de la bibliothèque quand il le souhaite, en effectuant un branchement adéquat. Mais cela implique que la bibliothèque soit présente dans l'espace d'adressage du programme en question. Une bibliothèque est donc présente dans plusieurs espaces d'adressage, alors qu'il n'y en a qu'un seul exemplaire en mémoire RAM.
[[File:Ogg vorbis libs and application dia.svg|centre|vignette|upright=2|Exemple de bibliothèques, avec Ogg vorbis.]]
D'autres situations demandent de partager de la mémoire entre deux programmes. Par exemple, les systèmes d'exploitation modernes gèrent nativement des systèmes de '''communication inter-processus''', très utilisés par les programmes modernes pour échanger des données. Et la plupart demandant de partager un bout de mémoire entre processus, même si c'est seulement temporairement. Typiquement, deux processus partagent un intervalle d'adresse où l'un écrit les données à l'autre, l'autre lisant les données envoyées.
Une dernière utilisation de la mémoire partagée est l{{'}}'''accès direct au noyau'''. Sur les systèmes d'exploitations moderne, dans l'espace d'adressage de chaque programme, les adresses hautes sont remplies avec une partie du noyau ! Évidemment, ces adresses sont accessibles uniquement en lecture, pas en écriture. Pas question de modifier le noyau de l'OS ! De plus, il s'agit d'une portion du noyau dont on sait que la consultation ne pose pas de problèmes de sécurité.
Le programme peut lire des données dans cette portion du noyau, mais aussi exécuter les fonctions du noyau qui sont dedans. L'idée est d'éviter des appels systèmes trop fréquents. Au lieu d'effectuer un véritable appel système, avec une interruption logicielle, le programme peut exécuter des appels systèmes simplifiés, de simples appels de fonctions couplés avec un changement de niveau de privilège (passage en espace noyau nécessaire).
[[File:AMD64-canonical--48-bit.png|vignette|Répartition des adresses entre noyau (jaune/orange) et programme (verte), sur les systèmes x86-64 bits, avec des adresses physiques de 48 bits.]]
L'espace d'adressage est donc séparé en deux portions : l'OS d'un côté, le programme de l'autre. La répartition des adresses entre noyau et programme varie suivant l'OS ou le processeur utilisé. Sur les PC x86 32 bits, Linux attribuait 3 gigas pour les programmes et 1 giga pour le noyau, Windows attribuait 2 gigas à chacun. Sur les systèmes x86 64 bits, l'espace d'adressage d'un programme est coupé en trois, comme illustré ci-contre : une partie basse de 2^48 octets, une partie haute de même taille, et un bloc d'adresses invalides entre les deux. Les adresses basses sont utilisées pour le programme, les adresses hautes pour le noyau, il n'y a rien entre les deux.
Avec le partage de mémoire, plusieurs adresses logiques correspondent à la même adresse physique. Tel processus verra la zone de mémoire partagée à l'adresse X, l'autre la verra à l'adresse Y. Mais il s'agira de la même portion de mémoire physique, avec une seule adresse physique. En clair, lorsque deux processus partagent une même zone de mémoire, la zone sera mappées à des adresses logiques différentes. Les adresses logiques sont alors appelées des '''adresses synonymes''', terme qui trahit le fait qu'elles correspondent à la même adresse physique.
===La mémoire virtuelle===
Toutes les adresses ne sont pas forcément occupées par de la mémoire RAM, s'il n'y a pas assez de RAM installée. Par exemple, un processeur 32 bits peut adresser 4 gibioctets de RAM, même si seulement 3 gibioctets sont installés dans l'ordinateur. L'espace d'adressage contient donc 1 gigas d'adresses inutilisées, et il faut éviter ce surplus d'adresses pose problème.
Sans mémoire virtuelle, seule la mémoire réellement installée est utilisable. Si un programme utilise trop de mémoire, il est censé se rendre compte qu'il n'a pas accès à tout l'espace d'adressage. Quand il demandera au système d'exploitation de lui réserver de la mémoire, le système d'exploitation le préviendra qu'il n'y a plus de mémoire libre. Par exemple, si un programme tente d'utiliser 4 gibioctets sur un ordinateur avec 3 gibioctets de mémoire, il ne pourra pas. Pareil s'il veut utiliser 2 gibioctets de mémoire sur un ordinateur avec 4 gibioctets, mais dont 3 gibioctets sont déjà utilisés par d'autres programmes. Dans les deux cas, l'illusion tombe à plat.
Les techniques de '''mémoire virtuelle''' font que l'espace d'adressage est utilisable au complet, même s'il n'y a pas assez de mémoire installée dans l'ordinateur ou que d'autres programmes utilisent de la RAM. Par exemple, sur un processeur 32 bits, le programme aura accès à 4 gibioctets de RAM, même si d'autres programmes utilisent la RAM, même s'il n'y a que 2 gibioctets de RAM d'installés dans l'ordinateur.
Pour cela, on utilise une partie des mémoires de masse (disques durs) d'un ordinateur en remplacement de la mémoire physique manquante. Le système d'exploitation crée sur le disque dur un fichier, appelé le ''swapfile'' ou '''fichier de ''swap''''', qui est utilisé comme mémoire RAM supplémentaire. Il mémorise le surplus de données et de programmes qui ne peut pas être mis en mémoire RAM.
[[File:Vm1.png|centre|vignette|upright=2.0|Mémoire virtuelle et fichier de Swap.]]
Une technique naïve de mémoire virtuelle serait la suivante. Avant de l'aborder, précisons qu'il s'agit d'une technique abordée à but pédagogique, mais qui n'est implémentée nulle part tellement elle est lente et inefficace. Un espace d'adressage de 4 gigas ne contient que 3 gigas de RAM, ce qui fait 1 giga d'adresses inutilisées. Les accès mémoire aux 3 gigas de RAM se font normalement, mais l'accès aux adresses inutilisées lève une exception matérielle "Memory Unavailable". La routine d'interruption de cette exception accède alors au ''swapfile'' et récupère les données associées à cette adresse. La mémoire virtuelle est alors émulée par le système d'exploitation.
Le défaut de cette méthode est que l'accès au giga manquant est toujours très lent, parce qu'il se fait depuis le disque dur. D'autres techniques de mémoire virtuelle logicielle font beaucoup mieux, mais nous allons les passer sous silence, vu qu'on peut faire mieux, avec l'aide du matériel.
L'idée est de charger les données dont le programme a besoin dans la RAM, et de déplacer les autres sur le disque dur. Par exemple, imaginons la situation suivante : un programme a besoin de 4 gigas de mémoire, mais ne dispose que de 2 gigas de mémoire installée. On peut imaginer découper l'espace d'adressage en 2 blocs de 2 gigas, qui sont chargés à la demande. Si le programme accède aux adresses basses, on charge les 2 gigas d'adresse basse en RAM. S'il accède aux adresses hautes, on charge les 2 gigas d'adresse haute dans la RAM après avoir copié les adresses basses sur le ''swapfile''.
On perd du temps dans les copies de données entre RAM et ''swapfile'', mais on gagne en performance vu que tous les accès mémoire se font en RAM. Du fait de la localité temporelle, le programme utilise les données chargées depuis le swapfile durant un bon moment avant de passer au bloc suivant. La RAM est alors utilisée comme une sorte de cache alors que les données sont placées dans une mémoire fictive représentée par l'espace d'adressage et qui correspond au disque dur.
Mais avec cette technique, la correspondance entre adresses du programme et adresses de la RAM change au cours du temps. Les adresses de la RAM correspondent d'abord aux adresses basses, puis aux adresses hautes, et ainsi de suite. On a donc besoin d'abstraction mémoire. Les correspondances entre adresse logique et physique peuvent varier avec le temps, ce qui permet de déplacer des données de la RAM vers le disque dur ou inversement. Une adresse logique peut correspondre à une adresse physique, ou bien à une donnée swappée sur le disque dur. C'est l'unité de traduction d'adresse qui se charge de faire la différence. Si une correspondance entre adresse logique et physique est trouvée, elle l'utilise pour traduire les adresses. Si aucune correspondance n'est trouvée, alors elle laisse la main au système d'exploitation pour charger la donnée en RAM. Une fois la donnée chargée en RAM, les correspondances entre adresse logique et physiques sont modifiées de manière à ce que l'adresse logique pointe vers la donnée chargée.
===L'extension d'adressage===
Une autre fonctionnalité rendue possible par l'abstraction mémoire est l{{'}}'''extension d'adressage'''. Elle permet d'utiliser plus de mémoire que l'espace d'adressage ne le permet. Par exemple, utiliser 7 gigas de RAM sur un processeur 32 bits, dont l'espace d'adressage ne gère que 4 gigas. L'extension d'adresse est l'exact inverse de la mémoire virtuelle. La mémoire virtuelle sert quand on a moins de mémoire que d'adresses, l'extension d'adresse sert quand on a plus de mémoire que d'adresses.
Il y a quelques chapitres, nous avions vu que c'est possible via la commutation de banques. Mais l'abstraction mémoire est une méthode alternative. Que ce soit avec la commutation de banques ou avec l'abstraction mémoire, les adresses envoyées à la mémoire doivent être plus longues que les adresses gérées par le processeur. La différence est que l'abstraction mémoire étend les adresses d'une manière différente.
Une implémentation possible de l'extension d'adressage fait usage de l'abstraction matérielle des processus. Chaque processus a son propre espace d'adressage, mais ceux-ci sont placés à des endroits différents dans la mémoire physique. Par exemple, sur un ordinateur avec 16 gigas de RAM, mais un espace d'adressage de 2 gigas, on peut remplir la RAM en lançant 8 processus différents et chaque processus aura accès à un bloc de 2 gigas de RAM, pas plus, il ne peut pas dépasser cette limite. Ainsi, chaque processus est limité par son espace d'adressage, mais on remplit la mémoire avec plusieurs processus, ce qui compense. Il s'agit là de l'implémentation la plus simple, qui a en plus l'avantage d'avoir la meilleure compatibilité logicielle. De simples changements dans le système d'exploitation suffisent à l'implémenter.
[[File:Extension de l'espace d'adressage.png|centre|vignette|upright=1.5|Extension de l'espace d'adressage]]
Un autre implémentation donne plusieurs espaces d'adressage différents à chaque processus, et a donc accès à autant de mémoire que permis par la somme de ces espaces d'adressage. Par exemple, sur un ordinateur avec 16 gigas de RAM et un espace d'adressage de 4 gigas, un programme peut utiliser toute la RAM en utilisant 4 espaces d'adressage distincts. On passe d'un espace d'adressage à l'autre en changeant la correspondance adresse logique-physique. L'inconvénient est que la compatibilité logicielle est assez mauvaise. Modifier l'OS ne suffit pas, les programmeurs doivent impérativement concevoir leurs programmes pour qu'ils utilisent explicitement plusieurs espaces d'adressage.
Les deux implémentations font usage des adresses logiques homonymes, mais à l'intérieur d'un même processus. Pour rappel, cela veut dire qu'une adresse logique correspond à des adresses physiques différentes. Rien d'étonnant vu qu'on utilise plusieurs espaces d'adressage, comme pour l'abstraction des processus, sauf que cette fois-ci, on a plusieurs espaces d'adressage par processus. Prenons l'exemple où on a 8 gigas de RAM sur un processeur 32 bits, dont l'espace d'adressage ne gère que 4 gigas. L'idée est qu'une adresse correspondra à une adresse dans les premiers 4 gigas, ou dans les seconds 4 gigas. L'adresse logique X correspondra d'abord à une adresse physique dans les premiers 4 gigas, puis à une adresse physique dans les seconds 4 gigas.
===La protection mémoire===
La '''protection mémoire''' regroupe des techniques très différentes les unes des autres, qui visent à améliorer la sécurité des programmes et des systèmes d'exploitation. Elles visent à empêcher de lire, d'écrire ou d'exécuter certaines portions de mémoire. Sans elle, les programmes peuvent techniquement lire ou écrire les données des autres, ce qui causent des situations non-prévues par le programmeur, avec des conséquences qui vont d'un joli plantage à des failles de sécurité dangereuses.
La première technique de protection mémoire est l{{'}}'''isolation des processus''', qu'on a vue plus haut. Elle garantit que chaque programme n'a accès qu'à certaines portions dédiées de la mémoire et rend le reste de la mémoire inaccessible en lecture et en écriture. Le système d'exploitation attribue à chaque programme une ou plusieurs portions de mémoire rien que pour lui, auquel aucun autre programme ne peut accéder. Un tel programme, isolé des autres, s'appelle un '''processus''', d'où le nom de cet objectif. Toute tentative d'accès à une partie de la mémoire non autorisée déclenche une exception matérielle (rappelez-vous le chapitre sur les interruptions) qui est traitée par une routine du système d'exploitation. Généralement, le programme fautif est sauvagement arrêté et un message d'erreur est affiché à l'écran.
La '''protection de l'espace exécutable''' empêche d’exécuter quoique ce soit provenant de certaines zones de la mémoire. En effet, certaines portions de la mémoire sont censées contenir uniquement des données, sans aucun programme ou code exécutable. Cependant, des virus informatiques peuvent se cacher dedans et d’exécuter depuis celles-ci. Ou encore, des failles de sécurités peuvent permettre à un attaquant d'injecter du code exécutable malicieux dans des données, ce qui peut lui permettre de lire les données manipulées par un programme, prendre le contrôle de la machine, injecter des virus, ou autre. Pour éviter cela, le système d'exploitation peut marquer certaines zones mémoire comme n'étant pas exécutable. Toute tentative d’exécuter du code localisé dans ces zones entraîne la levée d'une exception ou d'une erreur et le système d'exploitation réagit en conséquence. Là encore, le processeur doit détecter les exécutions non autorisées.
D'autres méthodes de protection mémoire visent à limiter des actions dangereuses. Pour cela, le processeur et l'OS gèrent des '''droits d'accès''', qui interdisent certaines actions pour des programmes non-autorisés. Lorsqu'on exécute une opération interdite, le système d’exploitation et/ou le processeur réagissent en conséquence. La première technique de ce genre n'est autre que la séparation entre espace noyau et utilisateur, vue dans le chapitre sur les interruptions. Mais il y en a d'autres, comme nous le verrons dans ce chapitre.
==La MMU==
La traduction des adresses logiques en adresses physiques se fait par un circuit spécialisé, appelé la '''''Memory Management Unit''''' (MMU). De nos jours, elle est intégrée dans le processeur, à la suite de l'unité mémoire. Mais il a existé des processeurs avec une MMU externe, soudée sur la carte mère.
La traduction des adresses logiques en adresses physiques demande d'accéder à des données sont en mémoire RAM, qui sont gérés par le système d'exploitation. Aussi, les processeurs modernes incorporent des mémoires caches appelées des '''''Translation Lookaside Buffers''''', ou encore TLB. Nous nous pouvons pas parler des TLB pour le moment, car nous n'avons pas encore abordé le chapitre sur les mémoires caches, mais un chapitre entier sera dédié aux TLB d'ici peu.
[[File:MMU principle updated.png|centre|vignette|upright=2|MMU.]]
===Les MMU intégrées au processeur===
D'ordinaire, la MMU est intégrée au processeur. Et elle peut l'être de deux manières. La première en fait un circuit séparé, relié au bus d'adresse. La seconde fusionne la MMU avec l'unité de calcul d'adresse. La première solution est surtout utilisée avec une technique d'abstraction mémoire appelée la pagination, alors que l'autre l'est avec une autre méthode appelée la segmentation. La raison est que la traduction d'adresse avec la segmentation est assez simple : elle demande d'additionner le contenu d'un registre avec l'adresse logique, ce qui est le genre de calcul qu'une unité de calcul d'adresse sait déjà faire. La fusion est donc assez évidente.
Pour donner un exemple, l'Intel 8086 fusionnait l'unité de calcul d'adresse et la MMU. Précisément, il utilisait un même additionneur pour incrémenter le ''program counter'' et effectuer des calculs d'adresse liés à la segmentation. Il aurait été logique d'ajouter les pointeurs de pile avec, mais ce n'était pas possible. La raison est que le pointeur de pile ne peut pas être envoyé directement sur le bus d'adresse, vu qu'il doit passer par une phase de traduction en adresse physique liée à la segmentation.
[[File:80186 arch.png|centre|vignette|upright=2|Intel 8086, microarchitecture.]]
===Les MMU séparées du processeur, sur la carte mère===
Avant d'être intégrées au processeur, les MMU étaient des circuits séparés, placés sur la carte mère, entre le processeur et la mémoire. Elle étaient connectées en plein milieu du bus d'adresse. A savoir que le bus d'adresse entrait dans la MMU et en ressortait. Le processeur envoyait des adresses à la MMU, qui renvoyait une adresse physique. La MMU était facultative, à savoir que le système pouvait fonctionner sans MMU d'installée, mais l'abstraction mémoire n’était alors pas disponible.
Par exemple, les processeurs Motorola 68000 et 68010 pouvaient être combinés avec une MMU de type Motorola 68451. Elle supportait des versions simplifiées de la segmentation et de la pagination. Au minimum, elle ajoutait un support de la protection mémoire contre certains accès non-autorisés. La gestion de la mémoire virtuelle proprement dit n'était possible que si le processeur utilisé était un Motorola 68010, en raison de la manière dont le 68000 gérait ses accès mémoire. La MMU 68451 gérait un espace d'adressage de 16 mébioctets, découpé en maximum 32 pages/segments. On pouvait dépasser cette limite de 32 segments/pages en combinant plusieurs 68451.
Le Motorola 68851 était une MMU qui était prévue pour fonctionner de paire avec le Motorola 68020. Elle gérait la pagination pour un espace d'adressage de 32 bits.
Les processeurs suivants, les 68030, 68040, et 68060, avaient une MMU interne au processeur.
==La relocation matérielle==
Pour rappel, les systèmes d'exploitation moderne permettent de lancer plusieurs programmes en même temps et les laissent se partager la mémoire. Dans le cas le plus simple, qui n'est pas celui des OS modernes, le système d'exploitation découpe la mémoire en blocs d'adresses contiguës qui sont appelés des '''segments''', ou encore des ''partitions mémoire''. Les segments correspondent à un bloc de mémoire RAM. C'est-à-dire qu'un segment de 259 mébioctets sera un segment continu de 259 mébioctets dans la mémoire physique comme dans la mémoire logique. Dans ce qui suit, un segment contient un programme en cours d'exécution, comme illustré ci-dessous.
[[File:CPT Memory Addressable.svg|centre|vignette|upright=2|Espace d'adressage segmenté.]]
Le système d'exploitation mémorise la position de chaque segment en mémoire, ainsi que d'autres informations annexes. Le tout est regroupé dans la '''table de segment''', un tableau dont chaque case est attribuée à un programme/segment. La table des segments est un tableau numéroté, chaque segment ayant un numéro qui précise sa position dans le tableau. Chaque case, chaque entrée, contient un '''descripteur de segment''' qui regroupe plusieurs informations sur le segment : son adresse de base, sa taille, diverses informations.
===La relocation avec la relocation matérielle : le registre de base===
Un segment peut être placé n'importe où en RAM physique et sa position en RAM change à chaque exécution. Le programme est chargé à une adresse, celle du début du segment, qui change à chaque chargement du programme. Et toutes les adresses utilisées par le programme doivent être corrigées lors du chargement du programme, généralement par l'OS. Cette correction s'appelle la '''relocation''', et elle consiste à ajouter l'adresse de début du segment à chaque adresse manipulée par le programme.
[[File:Relocation assistée par matériel.png|centre|vignette|upright=2.5|Relocation.]]
La relocation matérielle fait que la relocation est faite par le processeur, pas par l'OS. La relocation est intégrée dans le processeur par l'intégration d'un registre : le '''registre de base''', aussi appelé '''registre de relocation'''. Il mémorise l'adresse à laquelle commence le segment, la première adresse du programme. Pour effectuer la relocation, le processeur ajoute automatiquement l'adresse de base à chaque accès mémoire, en allant la chercher dans le registre de relocation.
[[File:Registre de base de segment.png|centre|vignette|upright=2|Registre de base de segment.]]
Le processeur s'occupe de la relocation des segments et le programme compilé n'en voit rien. Pour le dire autrement, les programmes manipulent des adresses logiques, qui sont traduites par le processeur en adresses physiques. La traduction se fait en ajoutant le contenu du registre de relocation à l'adresse logique. De plus, cette méthode fait que chaque programme a son propre espace d'adressage.
[[File:CPU created logical address presentation.png|centre|vignette|upright=2|Traduction d'adresse avec la relocation matérielle.]]
Le système d'exploitation mémorise les adresses de base pour chaque programme, dans la table des segments. Le registre de base est mis à jour automatiquement lors de chaque changement de segment. Pour cela, le registre de base est accessible via certaines instructions, accessibles en espace noyau, plus rarement en espace utilisateur. Le registre de segment est censé être adressé implicitement, vu qu'il est unique. Si ce n'est pas le cas, il est possible d'écrire dans ce registre de segment, qui est alors adressable.
===La protection mémoire avec la relocation matérielle : le registre limite===
Sans restrictions supplémentaires, la taille maximale d'un segment est égale à la taille complète de l'espace d'adressage. Sur les processeurs 32 bits, un segment a une taille maximale de 2^32 octets, soit 4 gibioctets. Mais il est possible de limiter la taille du segment à 2 gibioctets, 1 gibioctet, 64 Kibioctets, ou toute autre taille. La limite est définie lors de la création du segment, mais elle peut cependant évoluer au cours de l'exécution du programme, grâce à l'allocation mémoire.
Le processeur vérifie à chaque accès mémoire que celui-ci se fait bien dans le segment, qu'il ne déborde pas en-dehors. C'est possible qu'une adresse calculée sorte du segment, à la suite d'un bug ou d'une erreur de programmation, voire pire. Et le processeur doit éviter de tels '''débordements de segments'''.
A chaque accès mémoire, le processeur compare l'adresse accédée et vérifie qu'elle est bien dans le segment. Pour cela, il y a deux solutions. La première part du principe que le segment est placé en mémoire entre l'adresse de base et l'adresse limite. Il suffit de mémoriser l'adresse limite, l'adresse physique à ne pas dépasser. Une autre solution mémorise la taille du segment. La table des segments doit donc mémoriser, en plus de l'adresse de base : soit l'adresse maximale du segment, soit la taille du segment. D'autres informations peuvent être ajoutées, comme on le verra plus tard, mais cela complexifie la table des segments.
De plus, le processeur se voit ajouter un '''registre limite''', qui mémorise soit la taille du segment, soit l'adresse limite. Les deux registres, base et limite, sont utilisés pour vérifier si un programme qui lit/écrit de la mémoire en-dehors de son segment attitré : au-delà pour le registre limite, en-deça pour le registre de base. Le processeur vérifie pour chaque accès mémoire ne déborde pas au-delà du segment qui lui est allouée, ce qui n'arrive que si l'adresse d'accès dépasse la valeur du registre limite. Pour les accès en-dessous du segment, il suffit de vérifier si l'addition de relocation déborde, tout débordement signifiant erreur de protection mémoire.
[[File:Registre limite.png|centre|vignette|upright=2|Registre limite]]
Utiliser la taille du segment a de nombreux avantages. L'un d'entre eux se manifeste quand on déplace un segment en mémoire RAM. Le descripteur doit alors être mis à jour, et c'est plus facile quand on utilise la taille du segment. Si on utilise l'adresse limite, il faut mettre à jour à la fois l'adresse de base et l'adresse limite, dans le descripteur. En utilisant la taille, seule l'adresse de base doit être modifiée, vu que le segment n'a pas changé de taille. Un autre avantage est lié aux performances, mais nous devons faire un détour pour le comprendre.
La taille du segment est équivalent à l'adresse logique maximale possible. Par exemple, si un segment fait 256 octets, les adresses logiques possibles vont de 0 à 255, 256 est donc à la fois la taille du segment et l'adresse logique à partir de laquelle on déborde du segment. Et cela marche si on remplace 256 par n'importe quelle valeur : vu que le segment commence à l'adresse 0, sa taille en octets indique l'adresse de dépassement. Interpréter la taille du segment comme une adresse logique fait que les tests avec le registre limite sont plus performants, voyons pourquoi.
En utilisant l'adresse physique limite, on doit faire la relocation, puis comparer l'adresse calculée avec l'adresse limite. Le calcul d'adresse doit se faire avant la vérification. En utilisant la taille, on doit comparer l'adresse logique avec la taille du segment. On peut alors faire le test de débordement avant ou pendant la relocation. Les deux peuvent être faits en parallèle, dans deux circuits distincts, ce qui améliore un peu le temps d'un accès mémoire. Quelques processeurs en ont profité, mais on verra cela dans la section sur la segmentation.
[[File:Comparaison entre adresse limite physique et logique.png|centre|vignette|upright=2|Comparaison entre adresse limite physique et logique]]
Les registres de base et limite sont altérés uniquement par le système d'exploitation et ne sont accessibles qu'en espace noyau. Lorsque le système d'exploitation charge un programme, ou reprend son exécution, il charge les adresses de début/fin du segment dans ces registres. D'ailleurs, ces deux registres doivent être sauvegardés et restaurés lors de chaque interruption. Par contre, et c'est assez évident, ils ne le sont pas lors d'un appel de fonction. Cela fait une différence de plus entre interruption et appels de fonctions.
: Il faut noter que le registre limite et le registre de base sont parfois fusionnés en un seul registre, qui contient un descripteur de segment tout entier.
Pour information, la relocation matérielle avec un registre limite a été implémentée sur plusieurs processeurs assez anciens, notamment sur les anciens supercalculateurs de marque CDC. Un exemple est le fameux CDC 6600, qui implémentait cette technique.
===La mémoire virtuelle avec la relocation matérielle===
Il est possible d'implémenter la mémoire virtuelle avec la relocation matérielle. Pour cela, il faut swapper des segments entiers sur le disque dur. Les segments sont placés en mémoire RAM et leur taille évolue au fur et à mesure que les programmes demandent du rab de mémoire RAM. Lorsque la mémoire est pleine, ou qu'un programme demande plus de mémoire que disponible, des segments entiers sont sauvegardés dans le ''swapfile'', pour faire de la place.
Faire ainsi de demande juste de mémoriser si un segment est en mémoire RAM ou non, ainsi que la position des segments swappés dans le ''swapfile''. Pour cela, il faut modifier la table des segments, afin d'ajouter un '''bit de swap''' qui précise si le segment en question est swappé ou non. Lorsque le système d'exploitation veut swapper un segment, il le copie dans le ''swapfile'' et met ce bit à 1. Lorsque l'OS recharge ce segment en RAM, il remet ce bit à 0. La gestion de la position des segments dans le ''swapfile'' est le fait d'une structure de données séparée de la table des segments.
L'OS exécute chaque programme l'un après l'autre, à tour de rôle. Lorsque le tour d'un programme arrive, il consulte la table des segments pour récupérer les adresses de base et limite, mais il vérifie aussi le bit de swap. Si le bit de swap est à 0, alors l'OS se contente de charger les adresses de base et limite dans les registres adéquats. Mais sinon, il démarre une routine d'interruption qui charge le segment voulu en RAM, depuis le ''swapfile''. C'est seulement une fois le segment chargé que l'on connait son adresse de base/limite et que le chargement des registres de relocation peut se faire.
Un défaut évident de cette méthode est que l'on swappe des programmes entiers, qui sont généralement assez imposants. Les segments font généralement plusieurs centaines de mébioctets, pour ne pas dire plusieurs gibioctets, à l'époque actuelle. Ils étaient plus petits dans l'ancien temps, mais la mémoire était alors plus lente. Toujours est-il que la copie sur le disque dur des segments est donc longue, lente, et pas vraiment compatible avec le fait que les programmes s'exécutent à tour de rôle. Et ca explique pourquoi la relocation matérielle n'est presque jamais utilisée avec de la mémoire virtuelle.
===L'extension d'adressage avec la relocation matérielle===
Passons maintenant à la dernière fonctionnalité implémentable avec la traduction d'adresse : l'extension d'adressage. Elle permet d'utiliser plus de mémoire que ne le permet l'espace d'adressage. Par exemple, utiliser plus de 64 kibioctets de mémoire sur un processeur 16 bits. Pour cela, les adresses envoyées à la mémoire doivent être plus longues que les adresses gérées par le processeur.
L'extension des adresses se fait assez simplement avec la relocation matérielle : il suffit que le registre de base soit plus long. Prenons l'exemple d'un processeur aux adresses de 16 bits, mais qui est reliée à un bus d'adresse de 24 bits. L'espace d'adressage fait juste 64 kibioctets, mais le bus d'adresse gère 16 mébioctets de RAM. On peut utiliser les 16 mébioctets de RAM à une condition : que le registre de base fasse 24 bits, pas 16.
Un défaut de cette approche est qu'un programme ne peut pas utiliser plus de mémoire que ce que permet l'espace d'adressage. Mais par contre, on peut placer chaque programme dans des portions différentes de mémoire. Imaginons par exemple que l'on ait un processeur 16 bits, mais un bus d'adresse de 20 bits. Il est alors possible de découper la mémoire en 16 blocs de 64 kibioctets, chacun attribué à un segment/programme, qu'on sélectionne avec les 4 bits de poids fort de l'adresse. Il suffit de faire démarrer les segments au bon endroit en RAM, et cela demande juste que le registre de base le permette. C'est une sorte d'émulation de la commutation de banques.
==La segmentation en mode réel des processeurs x86==
Avant de passer à la suite, nous allons voir la technique de segmentation de l'Intel 8086, un des tout premiers processeurs 16 bits. Il s'agissait d'une forme très simple de segmentation, sans aucune forme de protection mémoire, ni même de mémoire virtuelle, ce qui le place à part des autres formes de segmentation. Il s'agit d'une amélioration de la relocation matérielle, qui avait pour but de permettre d'utiliser plus de 64 kibioctets de mémoire, ce qui était la limite maximale sur les processeurs 16 bits de l'époque.
Par la suite, la segmentation s'améliora et ajouta un support complet de la mémoire virtuelle et de la protection mémoire. L'ancienne forme de segmentation fut alors appelé le '''mode réel''', et la nouvelle forme de segmentation fut appelée le '''mode protégé'''. Le mode protégé rajoute la protection mémoire, en ajoutant des registres limite et une gestion des droits d'accès aux segments, absents en mode réel. De plus, il ajoute un support de la mémoire virtuelle grâce à l'utilisation d'une des segments digne de ce nom, table qui est absente en mode réel ! Pour le moment, voyons le mode réel.
===Les segments en mode réel===
[[File:Typical computer data memory arrangement.png|vignette|upright=0.5|Typical computer data memory arrangement]]
La segmentation en mode réel sépare la pile, le tas, le code machine et les données constantes dans quatre segments distincts.
* Le segment '''''text''''', qui contient le code machine du programme, de taille fixe.
* Le segment '''''data''''' contient des données de taille fixe qui occupent de la mémoire de façon permanente, des constantes, des variables globales, etc.
* Le segment pour la '''pile''', de taille variable.
* le reste est appelé le '''tas''', de taille variable.
Un point important est que sur ces processeurs, il n'y a pas de table des segments proprement dit. Chaque programme gére de lui-même les adresses de base des segments qu'il manipule. Il n'est en rien aidé par une table des segments gérée par le système d'exploitation.
===Les registres de segments en mode réel===
Chaque segment subit la relocation indépendamment des autres. Pour cela, le processeur intégre plusieurs registres de base, un par segment. Notons que cette solution ne marche que si le nombre de segments par programme est limité, à une dizaine de segments tout au plus. Les processeurs x86 utilisaient cette méthode, et n'associaient que 4 à 6 registres de segments par programme.
Les processeurs 8086 et le 286 avaient quatre registres de segment : un pour le code, un autre pour les données, et un pour la pile, le quatrième étant un registre facultatif laissé à l'appréciation du programmeur. Ils sont nommés CS (''code segment''), DS (''data segment''), SS (''Stack segment''), et ES (''Extra segment''). Le 386 rajouta deux registres, les registres FS et GS, qui sont utilisés pour les segments de données. Les processeurs post-386 ont donc 6 registres de segment.
Les registres CS et SS sont adressés implicitement, en fonction de l'instruction exécutée. Les instructions de la pile manipulent le segment associé à la pile, le chargement des instructions se fait dans le segment de code, les instructions arithmétiques et logiques vont chercher leurs opérandes sur le tas, etc. Et donc, toutes les instructions sont chargées depuis le segment pointé par CS, les instructions de gestion de la pile (PUSH et POP) utilisent le segment pointé par SS.
Les segments DS et ES sont, eux aussi, adressés implicitement. Pour cela, les instructions LOAD/STORE sont dupliquées : il y a une instruction LOAD pour le segment DS, une autre pour le segment ES. D'autres instructions lisent leurs opérandes dans un segment par défaut, mais on peut changer ce choix par défaut en précisant le segment voulu. Un exemple est celui de l'instruction CMPSB, qui compare deux octets/bytes : le premier est chargé depuis le segment DS, le second depuis le segment ES.
Un autre exemple est celui de l'instruction MOV avec un opérande en mémoire. Elle lit l'opérande en mémoire depuis le segment DS par défaut. Il est possible de préciser le segment de destination si celui-ci n'est pas DS. Par exemple, l'instruction MOV [A], AX écrit le contenu du registre AX dans l'adresse A du segment DS. Par contre, l'instruction MOV ES:[A], copie le contenu du registre AX das l'adresse A, mais dans le segment ES.
===La traduction d'adresse en mode réel===
La segmentation en mode réel a pour seul but de permettre à un programme de dépasser la limite des 64 KB autorisée par les adresses de 16 bits. L'idée est que chaque segment a droit à son propre espace de 64 KB. On a ainsi 64 Kb pour le code machine, 64 KB pour la pile, 64 KB pour un segment de données, etc. Les registres de segment mémorisaient la base du segment, les adresses calculées par l'ALU étant des ''offsets''. Ce sont tous des registres de 16 bits, mais ils ne mémorisent pas des adresses physiques de 16 bits, comme nous allons le voir.
[[File:Table des segments dans un banc de registres.png|centre|vignette|upright=2|Table des segments dans un banc de registres.]]
L'Intel 8086 utilisait des adresses de 20 bits, ce qui permet d'adresser 1 mébioctet de RAM. Vous pouvez vous demander comment on peut obtenir des adresses de 20 bits alors que les registres de segments font tous 16 bits ? Cela tient à la manière dont sont calculées les adresses physiques. Le registre de segment n'est pas additionné tel quel avec le décalage : à la place, le registre de segment est décalé de 4 rangs vers la gauche. Le décalage de 4 rangs vers la gauche fait que chaque segment a une adresse qui est multiple de 16. Le fait que le décalage soit de 16 bits fait que les segments ont une taille de 64 kibioctets.
{|class="wikitable"
|-
| <code> </code><code style="background:#DED">0000 0110 1110 1111</code><code>0000</code>
| Registre de segment -
| 16 bits, décalé de 4 bits vers la gauche
|-
| <code>+ </code><code style="background:#DDF">0001 0010 0011 0100</code>
| Décalage/''Offset''
| 16 bits
|-
| colspan="3" |
|-
| <code> </code><code style="background:#FDF">0000 1000 0001 0010 0100</code>
| Adresse finale
| 20 bits
|}
Vous aurez peut-être remarqué que le calcul peut déborder, dépasser 20 bits. Mais nous reviendrons là-dessus plus bas. L'essentiel est que la MMU pour la segmentation en mode réel se résume à quelques registres et des additionneurs/soustracteurs.
Un exemple est l'Intel 8086, un des tout premier processeur Intel. Le processeur était découpé en deux portions : l'interface mémoire et le reste du processeur. L'interface mémoire est appelée la '''''Bus Interface Unit''''', et le reste du processeur est appelé l{{'}}'''''Execution Unit'''''. L'interface mémoire contenait les registres de segment, au nombre de 4, ainsi qu'un additionneur utilisé pour traduire les adresses logiques en adresses physiques. Elle contenait aussi une file d'attente où étaient préchargées les instructions.
Sur le 8086, la MMU est fusionnée avec les circuits de gestion du ''program counter''. Les registres de segment sont regroupés avec le ''program counter'' dans un même banc de registres, et un additionneur unique est utilisé à la fois à incrémenter le ''program counter'' et pour gérer la segmentation. L'additionneur est donc mutualisé entre segmentation et ''program counter''. En somme, il n'y a pas vraiment de MMU dédiée, mais un super-circuit en charge du Fetch et de la mémoire virtuelle, ainsi que du préchargement des instructions. Nous en reparlerons au chapitre suivant.
[[File:80186 arch.png|centre|vignette|upright=2.5|Architecture du 8086, du 80186 et de ses variantes.]]
La MMU du 286 était fusionnée avec l'unité de calcul d'adresse. Elle contient les registres de segments, un comparateur pour détecter les accès hors-segment, et plusieurs additionneurs. Il y a un additionneur pour les calculs d'adresse proprement dit, suivi d'un additionneur pour la relocation.
[[File:Intel i80286 arch.svg|centre|vignette|upright=3|Intel i80286 arch]]
===La segmentation en mode réel accepte plusieurs segments de code/données===
Les programmes peuvent parfaitement répartir leur code machine dans plusieurs segments de code. La limite de 64 KB par segment est en effet assez limitante, et il n'était pas rare qu'un programme stocke son code dans deux ou trois segments. Il en est de même avec les données, qui peuvent être réparties dans deux ou trois segments séparés. La seule exception est la pile : elle est forcément dans un segment unique et ne peut pas dépasser 64 KB.
Pour gérer plusieurs segments de code/donnée, il faut changer de segment à la volée suivant les besoins, en modifiant les registres de segment. Il s'agit de la technique de '''commutation de segment'''. Pour cela, tous les registres de segment, à l'exception de CS, peuvent être altérés par une instruction d'accès mémoire, soit avec une instruction MOV, soit en y copiant le sommet de la pile avec une instruction de dépilage POP. L'absence de sécurité fait que la gestion de ces registres est le fait du programmeur, qui doit redoubler de prudence pour ne pas faire n'importe quoi.
Pour le code machine, le répartir dans plusieurs segments posait des problèmes au niveau des branchements. Si la plupart des branchements sautaient vers une instruction dans le même segment, quelques rares branchements sautaient vers du code machine dans un autre segment. Intel avait prévu le coup et disposait de deux instructions de branchement différentes pour ces deux situations : les '''''near jumps''''' et les '''''far jumps'''''. Les premiers sont des branchements normaux, qui précisent juste l'adresse à laquelle brancher, qui correspond à la position de la fonction dans le segment. Les seconds branchent vers une instruction dans un autre segment, et doivent préciser deux choses : l'adresse de base du segment de destination, et la position de la destination dans le segment. Le branchement met à jour le registre CS avec l'adresse de base, avant de faire le branchement. Ces derniers étaient plus lents, car on n'avait pas à changer de segment et mettre à jour l'état du processeur.
Il y avait la même pour l'instruction d'appel de fonction, avec deux versions de cette instruction. La première version, le '''''near call''''' est un appel de fonction normal, la fonction appelée est dans le segment en cours. Avec la seconde version, le '''''far call''''', la fonction appelée est dans un segment différent. L'instruction a là aussi besoin de deux opérandes : l'adresse de base du segment de destination, et la position de la fonction dans le segment. Un ''far call'' met à jour le registre CS avec l'adresse de base, ce qui fait que les ''far call'' sont plus lents que les ''near call''. Il existe aussi la même chose, pour les instructions de retour de fonction, avec une instruction de retour de fonction normale et une instruction de retour qui renvoie vers un autre segment, qui sont respectivement appelées '''''near return''''' et '''''far return'''''. Là encore, il faut préciser l'adresse du segment de destination dans le second cas.
La même chose est possible pour les segments de données. Sauf que cette fois-ci, ce sont les pointeurs qui sont modifiés. pour rappel, les pointeurs sont, en programmation, des variables qui contiennent des adresses. Lors de la compilation, ces pointeurs sont placés soit dans un registre, soit dans les instructions (adressage absolu), ou autres. Ici, il existe deux types de pointeurs, appelés '''''near pointer''''' et '''''far pointer'''''. Vous l'avez deviné, les premiers sont utilisés pour localiser les données dans le segment en cours d'utilisation, alors que les seconds pointent vers une donnée dans un autre segment. Là encore, la différence est que le premier se contente de donner la position dans le segment, alors que les seconds rajoutent l'adresse de base du segment. Les premiers font 16 bits, alors que les seconds en font 32 : 16 bits pour l'adresse de base et 16 pour l{{'}}''offset''.
===L'occupation de l'espace d'adressage par les segments===
Nous venons de voir qu'un programme pouvait utiliser plus de 4-6 segments, avec la commutation de segment. Mais d'autres programmes faisaient l'inverse, à savoir qu'ils se débrouillaient avec seulement 1 ou 2 segments. Suivant le nombre de segments utilisés, la configuration des registres n'était pas la même. Les configurations possibles sont appelées des ''modèle mémoire'', et il y en a en tout 6. En voici la liste :
{| class="wikitable"
|-
! Modèle mémoire !! Configuration des segments !! Configuration des registres || Pointeurs utilisés || Branchements utilisés
|-
| Tiny* || Segment unique pour tout le programme || CS=DS=SS || ''near'' uniquement || ''near'' uniquement
|-
| Small || Segment de donnée séparé du segment de code, pile dans le segment de données || DS=SS || ''near'' uniquement || ''near'' uniquement
|-
| Medium || Plusieurs segments de code unique, un seul segment de données || CS, DS et SS sont différents || ''near'' et ''far'' || ''near'' uniquement
|-
| Compact || Segment de code unique, plusieurs segments de données || CS, DS et SS sont différents || ''near'' uniquement || ''near'' et ''far''
|-
| Large || Plusieurs segments de code, plusieurs segments de données || CS, DS et SS sont différents || ''near'' et ''far'' || ''near'' et ''far''
|}
Un programme est censé utiliser maximum 4-6 segments de 64 KB, ce qui permet d'adresser maximum 64 * 6 = 384 KB de RAM, soit bien moins que le mébioctet de mémoire théoriquement adressable. Mais ce défaut est en réalité contourné par la commutation de segment, qui permettait d'adresser la totalité de la RAM si besoin. Une second manière de contourner cette limite est que plusieurs processus peuvent s'exécuter sur un seul processeur, si l'OS le permet. Ce n'était pas le cas à l'époque du DOS, qui était un OS mono-programmé, mais c'était en théorie possible. La limite est de 6 segments par programme/processus, en exécuter plusieurs permet d'utiliser toute la mémoire disponible rapidement.
[[File:Overlapping realmode segments.svg|vignette|Segments qui se recouvrent en mode réel.]]
Vous remarquerez qu'avec des registres de segments de 16 bits, on peut gérer 65536 segments différents, chacun de 64 KB. Et 65 536 segments de 64 kibioctets, ça ne rentre pas dans le mébioctet de mémoire permis avec des adresses de 20 bits. La raison est que plusieurs couples segment+''offset'' pointent vers la même adresse. En tout, chaque adresse peut être adressée par 4096 couples segment+''offset'' différents.
L'avantage de cette méthode est que des segments peuvent se recouvrir, à savoir que la fin de l'un se situe dans le début de l'autre, comme illustré ci-contre. Cela permet en théorie de partager de la mémoire entre deux processus. Mais la technique est tout sauf pratique et est donc peu utilisée. Elle demande de placer minutieusement les segments en RAM, et les données à partager dans les segments. En pratique, les programmeurs et OS utilisent des segments qui ne se recouvrent pas et sont disjoints en RAM.
Le nombre maximal de segments disjoints se calcule en prenant la taille de la RAM, qu'on divise par la taille d'un segment. Le calcul donne : 1024 kibioctets / 64 kibioctets = 16 segments disjoints. Un autre calcul prend le nombre de segments divisé par le nombre d'adresses aliasées, ce qui donne 65536 / 4096 = 16. Seulement 16 segments, c'est peu. En comptant les segments utilisés par l'OS et ceux utilisés par le programme, la limite est vite atteinte si le programme utilise la commutation de segment.
===Le mode réel sur les 286 et plus : la ligne d'adresse A20===
Pour résumer, le registre de segment contient des adresses de 20 bits, dont les 4 bits de poids faible sont à 0. Et il se voit ajouter un ''offset'' de 16 bits. Intéressons-nous un peu à l'adresse maximale que l'on peut calculer avec ce système. Nous allons l'appeler l{{'}}'''adresse maximale de segmentation'''. Elle vaut :
{|class="wikitable"
|-
| <code> </code><code style="background:#DED">1111 1111 1111 1111</code><code>0000</code>
| Registre de segment -
| 16 bits, décalé de 4 bits vers la gauche
|-
| <code>+ </code><code style="background:#DDF">1111 1111 1111 1111</code>
| Décalage/''Offset''
| 16 bits
|-
| colspan="3" |
|-
| <code> </code><code style="background:#FDF">1 0000 1111 1111 1110 1111</code>
| Adresse finale
| 20 bits
|}
Le résultat n'est pas l'adresse maximale codée sur 20 bits, car l'addition déborde. Elle donne un résultat qui dépasse l'adresse maximale permis par les 20 bits, il y a un 21ème bit en plus. De plus, les 20 bits de poids faible ont une valeur bien précise. Ils donnent la différence entre l'adresse maximale permise sur 20 bit, et l'adresse maximale de segmentation. Les bits 1111 1111 1110 1111 traduits en binaire donnent 65 519; auxquels il faut ajouter l'adresse 1 0000 0000 0000 0000. En tout, cela fait 65 520 octets adressables en trop. En clair : on dépasse la limite du mébioctet de 65 520 octets. Le résultat est alors très différent selon que l'on parle des processeurs avant le 286 ou après.
Avant le 286, le bus d'adresse faisait exactement 20 bits. Les adresses calculées ne pouvaient pas dépasser 20 bits. L'addition générait donc un débordement d'entier, géré en arithmétique modulaire. En clair, les bits de poids fort au-delà du vingtième sont perdus. Le calcul de l'adresse débordait et retournait au début de la mémoire, sur les 65 520 premiers octets de la mémoire RAM.
[[File:IBM PC Memory areas.svg|vignette|IBM PC Memory Map, la ''High memory area'' est en jaune.]]
Le 80286 en mode réel gère des adresses de base de 24 bits, soit 4 bits de plus que le 8086. Le résultat est qu'il n'y a pas de débordement. Les bits de poids fort sont conservés, même au-delà du 20ème. En clair, la segmentation permettait de réellement adresser 65 530 octets au-delà de la limite de 1 mébioctet. La portion de mémoire adressable était appelé la '''''High memory area''''', qu'on va abrévier en HMA.
{| class="wikitable"
|+ Espace d'adressage du 286
|-
! Adresses en héxadécimal !! Zone de mémoire
|-
| 10 FFF0 à FF FFFF || Mémoire étendue, au-delà du premier mébioctet
|-
| 10 0000 à 10 FFEF || ''High Memory Area''
|-
| 0 à 0F FFFF || Mémoire adressable en mode réel
|}
En conséquence, les applications peuvent utiliser plus d'un mébioctet de RAM, mais au prix d'une rétrocompatibilité imparfaite. Quelques programmes DOS ne marchaient pus à cause de ça. D'autres fonctionnaient convenablement et pouvaient adresser les 65 520 octets en plus.
Pour résoudre ce problème, les carte mères ajoutaient un petit circuit relié au 21ème bit d'adresse, nommé A20 (pas d'erreur, les fils du bus d'adresse sont numérotés à partir de 0). Le circuit en question pouvait mettre à zéro le fil d'adresse, ou au contraire le laisser tranquille. En le forçant à 0, le calcul des adresses déborde comme dans le mode réel des 8086. Mais s'il ne le fait pas, la ''high memory area'' est adressable. Le circuit était une simple porte ET, qui combinait le 21ème bit d'adresse avec un '''signal de commande A20''' provenant d'ailleurs.
Le signal de commande A20 était géré par le contrôleur de clavier, qui était soudé à la carte mère. Le contrôleur en question ne gérait pas que le clavier, il pouvait aussi RESET le processeur, alors gérer le signal de commande A20 n'était pas si problématique. Quitte à avoir un microcontrôleur sur la carte mère, autant s'en servir au maximum... La gestion du bus d'adresse étaitdonc gérable au clavier. D'autres carte mères faisaient autrement et préféraient ajouter un interrupteur, pour activer ou non la mise à 0 du 21ème bit d'adresse.
: Il faut noter que le signal de commande A20 était mis à 1 en mode protégé, afin que le 21ème bit d'adresse soit activé.
Le 386 ajouta deux registres de segment, les registres FS et GS, ainsi que le '''mode ''virtual 8086'''''. Ce dernier permet d’exécuter des programmes en mode réel alors que le système d'exploitation s'exécute en mode protégé. C'est une technique de virtualisation matérielle qui permet d'émuler un 8086 sur un 386. L'avantage est que la compatibilité avec les programmes anciens écrits pour le 8086 est conservée, tout en profitant de la protection mémoire. Tous les processeurs x86 qui ont suivi supportent ce mode virtuel 8086.
==La segmentation avec une table des segments==
La '''segmentation avec une table des segments''' est apparue sur des processeurs assez anciens, le tout premier étant le Burrough 5000. Elle a ensuite été utilisée sur les processeurs x86 des PCs, à partir du 286 d'Intel. Elle est aujourd'hui abandonnée sur les jeux d'instruction x86. Tout comme la segmentation en mode réel, la segmentation attribue plusieurs segments par programmes ! Et cela a des répercutions sur la manière dont la traduction d'adresse est effectuée.
===Pourquoi plusieurs segments par programme ?===
L'utilité d'avoir plusieurs segments par programme n'est pas évidente, mais elle le devient quand on se plonge dans le passé. Dans le passé, les programmeurs devaient faire avec une quantité de mémoire limitée et il n'était pas rare que certains programmes utilisent plus de mémoire que disponible sur la machine. Mais les programmeurs concevaient leurs programmes en fonction.
[[File:Overlay Programming.svg|vignette|upright=1|Overlay Programming]]
L'idée était d'implémenter un système de mémoire virtuelle, mais émulé en logiciel, appelé l{{'}}'''''overlaying'''''. Le programme était découpé en plusieurs morceaux, appelés des ''overlays''. Les ''overlays'' les plus importants étaient en permanence en RAM, mais les autres étaient faisaient un va-et-vient entre RAM et disque dur. Ils étaient chargés en RAM lors de leur utilisation, puis sauvegardés sur le disque dur quand ils étaient inutilisés. Le va-et-vient des ''overlays'' entre RAM et disque dur était réalisé en logiciel, par le programme lui-même. Le matériel n'intervenait pas, comme c'est le cas avec la mémoire virtuelle.
Avec la segmentation, un programme peut utiliser la technique des ''overlays'', mais avec l'aide du matériel. Il suffit de mettre chaque ''overlay'' dans son propre segment, et laisser la segmentation faire. Les segments sont swappés en tout ou rien : on doit swapper tout un segment en entier. L'intérêt est que la gestion du ''swapping'' est grandement facilitée, vu que c'est le système d'exploitation qui s'occupe de swapper les segments sur le disque dur ou de charger des segments en RAM. Pas besoin pour le programmeur de coder quoique ce soit. Par contre, cela demande l'intervention du programmeur, qui doit découper le programme en segments/''overlays'' de lui-même. Sans cela, la segmentation n'est pas très utile.
L{{'}}''overlaying'' est une forme de '''segmentation à granularité grossière''', à savoir que le programme est découpé en segments de grande taille. L'usage classique est d'avoir un segment pour la pile, un autre pour le code exécutable, un autre pour le reste. Éventuellement, on peut découper les trois segments précédents en deux ou trois segments, rarement au-delà. Les segments sont alors peu nombreux, guère plus d'une dizaine par programme. D'où le terme de ''granularité grossière''.
La '''segmentation à granularité fine''' pousse le concept encore plus loin. Avec elle, il y a idéalement un segment par entité manipulée par le programme, un segment pour chaque structure de donnée et/ou chaque objet. Par exemple, un tableau aura son propre segment, ce qui est idéal pour détecter les accès hors tableau. Pour les listes chainées, chaque élément de la liste aura son propre segment. Et ainsi de suite, chaque variable agrégée (non-primitive), chaque structure de donnée, chaque objet, chaque instance d'une classe, a son propre segment. Diverses fonctionnalités supplémentaires peuvent être ajoutées, ce qui transforme le processeur en véritable processeur orienté objet, mais passons ces détails pour le moment.
Vu que les segments correspondent à des objets manipulés par le programme, on peut deviner que leur nombre évolue au cours du temps. En effet, les programmes modernes peuvent demander au système d'exploitation du rab de mémoire pour allouer une nouvelle structure de données. Avec la segmentation à granularité fine, cela demande d'allouer un nouveau segment à chaque nouvelle allocation mémoire, à chaque création d'une nouvelle structure de données ou d'un objet. De plus, les programmes peuvent libérer de la mémoire, en supprimant les structures de données ou objets dont ils n'ont plus besoin. Avec la segmentation à granularité fine, cela revient à détruire le segment alloué pour ces objets/structures de données. Le nombre de segments est donc dynamique, il change au cours de l'exécution du programme.
===Les tables de segments avec la segmentation===
La présence de plusieurs segments par programme a un impact sur la table des segments. Avec la relocation matérielle, elle conte nait un segment par programme. Chaque entrée, chaque ligne de la table des segment, mémorisait l'adresse de base, l'adresse limite, un bit de présence pour la mémoire virtuelle et des autorisations liées à la protection mémoire. Avec la segmentation, les choses sont plus compliquées, car il y a plusieurs segments par programme. Les entrées ne sont pas modifiées, mais elles sont organisées différemment.
Avec cette forme de segmentation, la table des segments doit respecter plusieurs contraintes. Premièrement, il y a plusieurs segments par programmes. Deuxièmement, le nombre de segments est variable : certains programmes se contenteront d'un seul segment, d'autres de dizaine, d'autres plusieurs centaines, etc. Il y a typiquement deux manières de faire : soit utiliser une table des segments uniques, utiliser une table des segment par programme.
Il est possible d'utiliser une table des segment unique qui mémorise tous les segments de tous les processus, système d'exploitation inclut. On parle alors de '''table des segment globale'''. Mais cette solution n'est pas utilisée avec la segmentation proprement dite. Elle est utilisée sur les architectures à capacité qu'on détaillera vers la fin du chapitre, dans une section dédiée. À la place, la segmentation utilise une table de segment par processus/programme, chacun ayant une '''table des segment locale'''.
Dans les faits, les choses sont plus compliquées. Le système d'exploitation doit savoir où se trouvent les tables de segment locale pour chaque programme. Pour cela, il a besoin d'utiliser une table de segment globale, dont chaque entrée pointe non pas vers un segment, mais vers une table de segment locale. Lorsque l'OS effectue une commutation de contexte, il lit la table des segment globale, pour récupérer un pointeur vers celle-ci. Ce pointeur est alors chargé dans un registre du processeur, qui mémorise l'adresse de la table locale, ce qui sert lors des accès mémoire.
Une telle organisation fait que les segments d'un processus/programme sont invisibles pour les autres, il y a une certaine forme de sécurité. Un programme ne connait que sa table de segments locale, il n'a pas accès directement à la table des segments globales. Tout accès mémoire se passera à travers la table de segment locale, il ne sait pas où se trouvent les autres tables de segment locales.
Les processeurs x86 sont dans ce cas : ils utilisent une table de segment globale couplée à autant de table des segments qu'il y a de processus en cours d'exécution. La table des segments globale s'appelle la '''''Global Descriptor Table''''' et elle peut contenir 8192 segments maximum, ce qui permet le support de 8192 processus différents. Les tables de segments locales sont appelées les '''''Local Descriptor Table''''' et elles font aussi 8192 segments maximum, ce qui fait 8192 segments par programme maximum. Il faut noter que la table de segment globale peut mémoriser des pointeurs vers les routines d'interruption, certaines données partagées (le tampon mémoire pour le clavier) et quelques autres choses, qui n'ont pas leur place dans les tables de segment locales.
===La relocation avec la segmentation===
La table des segments locale mémorise les adresses de base et limite de chaque segment, ainsi que d'autres méta-données. Les informations pour un segment sont regroupés dans un '''descripteur de segment''', qui est codé sur plusieurs octets, et qui regroupe : adresse de base, adresse limite, bit de présence en RAM, méta-données de protection mémoire.
La table des segments est un tableau dans lequel les descripteurs de segment sont placés les uns à la suite des autres en mémoire RAM. La table des segments est donc un tableau de segment. Les segments d'un programme sont numérotés, le nombre s'appelant un '''indice de segment''', appelé '''sélecteur de segment''' dans la terminologie Intel. L'indice de segment n'est autre que l'indice du segment dans ce tableau.
[[File:Global Descriptor table.png|centre|vignette|upright=2|Table des segments locale.]]
Il n'y a pas de registre de segment proprement dit, qui mémoriserait l'adresse de base. À la place, les segments sont adressés de manière indirecte. À la place, les registres de segment mémorisent des sélecteurs de segment. Ils sont utilisés pour lire l'adresse de base/limite dans la table de segment en mémoire RAM. Pour cela, un registre mémorise l'adresse de la table de segment locale, sa position en mémoire RAM.
Toute lecture ou écriture se fait en deux temps, en deux accès mémoire, consécutifs. Premièrement, le numéro de segment est utilisé pour adresser la table des segment. La lecture récupère alors un pointeur vers ce segment. Deuxièmement, ce pointeur est utilisé pour faire la lecture ou écriture. Plus précisément, la première lecture récupère un descripteur de segment qui contient l'adresse de base, le pointeur voulu, mais aussi l'adresse limite et d'autres informations.
[[File:Segmentation avec table des segments.png|centre|vignette|upright=2|Segmentation avec table des segments]]
L'accès à la table des segments se fait automatiquement à chaque accès mémoire. La conséquence est que chaque accès mémoire demande d'en faire deux : un pour lire la table des segments, l'autre pour l'accès lui-même. Il s'agit en quelque sorte d'une forme d'adressage indirect mémoire.
Un point important est que si le premier accès ne fait qu'une simple lecture dans un tableau, le second accès implique des calculs d'adresse. En effet, le premier accès récupère l'adresse de base du segment, mais le second accès sélectionne une donnée dans le segment, ce qui demande de calculer son adresse. L'adresse finale se déduit en combinant l'adresse de base avec un décalage (''offset'') qui donne la position de la donnée dans ce segment. L'indice de segment est utilisé pour récupérer l'adresse de base du segment. Une fois cette adresse de base connue, on lui additionne le décalage pour obtenir l'adresse finale.
[[File:Table des segments.png|centre|vignette|upright=2|Traduction d'adresse avec une table des segments.]]
Pour effectuer automatiquement l'accès à la table des segments, le processeur doit contenir un registre supplémentaire, qui contient l'adresse de la table de segment, afin de la localiser en mémoire RAM. Nous appellerons ce registre le '''pointeur de table'''. Le pointeur de table est combiné avec l'indice de segment pour adresser le descripteur de segment adéquat.
[[File:Segment 2.svg|centre|vignette|upright=2|Traduction d'adresse avec une table des segments, ici appelée table globale des de"scripteurs (terminologie des processeurs Intel x86).]]
Un point important est que la table des segments n'est pas accessible pour le programme en cours d'exécution. Il ne peut pas lire le contenu de la table des segments, et encore moins la modifier. L'accès se fait seulement de manière indirecte, en faisant usage des indices de segments, mais c'est un adressage indirect. Seul le système d'exploitation peut lire ou écrire la table des segments directement.
Plus haut, j'ai dit que tout accès mémoire impliquait deux accès mémoire : un pour charger le descripteur de segment, un autre pour la lecture/écriture proprement dite. Cependant, cela aurait un impact bien trop grand sur les performances. Dans les faits, les processeurs avec segmentations intégraient un '''cache de descripteurs de segments''', pour limiter la casse. Quand un descripteur de segment est lu depuis la RAM, il est copié dans ce cache. Les accès ultérieurs accédent au descripteur dans le cache, pas besoin de passer par la RAM. L'intel 386 avait un cache de ce type.
===La protection mémoire : les accès hors-segments===
Comme avec la relocation matérielle, le processeur détecte les débordements de segment. Pour cela, il compare l'adresse logique accédée avec l'adresse limite, ou compare la taille limite avec le décalage. De nombreux processeurs, comme l'Intel 386, préféraient utiliser la taille du segment, pour une question d'optimisation. En effet, si on compare l'adresse finale avec l'adresse limite, on doit faire la relocation avant de comparer l'adresse relocatée. Mais en utilisant la taille, ce n'est pas le cas : on peut faire la comparaison avant, pendant ou après la relocation.
Un détail à prendre en compte est la taille de la donnée accédée. Sans cela, la comparaison serait très simple : on vérifie si ''décalage <= taille du segment'', ou on compare des adresses de la même manière. Mais imaginez qu'on accède à une donnée de 4 octets : il se peut que l'adresse de ces 4 octets rentre dans le segment, mais que quelques octets débordent. Par exemple, les deux premiers octets sont dans le segment, mais pas les deux suivants. La vraie comparaison est alors : ''décalage + 4 octets <= taille du segment''.
Mais il est possible de faire le calcul autrement, et quelques processeurs comme l'Intel 386 ne s'en sont pas privé. Il calculait la différence ''taille du segment - décalage'', et vérifiait le résultat. Le processeur gérait des données de 1, 2 et 4 octets, ce qui fait que le résultat devait être entre 0 et 3. Le processeur prenait le résultat de la soustraction, et vérifiait alors que les 30 bits de poids fort valaient bien 0. Il vérifiait aussi que les deux bits de poids faible avaient la bonne valeur.
[[File:Vm7.svg|centre|vignette|upright=2|Traduction d'adresse avec vérification des accès hors-segment.]]
Une nouveauté fait son apparition avec la segmentation : la '''gestion des droits d'accès'''. Par exemple, il est possible d'interdire d'exécuter le contenu d'un segment, ce qui fournit une protection contre certaines failles de sécurité ou certains virus. Lorsqu'on exécute une opération interdite, le processeur lève une exception matérielle, à charge du système d'exploitation de gérer la situation.
Pour cela, chaque segment se voit attribuer un certain nombre d'autorisations d'accès qui indiquent si l'on peut lire ou écrire dedans, si celui-ci contient un programme exécutable, etc. Les autorisations pour chaque segment sont placées dans le descripteur de segment. Elles se résument généralement à quelques bits, qui indiquent si le segment est accesible en lecture/écriture ou exécutable. Le tout est souvent concaténé dans un ou deux '''octets de droits d'accès'''.
L'implémentation de la protection mémoire dépend du CPU considéré. Les CPU microcodés peuvent en théorie utiliser le microcode. Lorsqu'une instruction mémoire s'exécute, le microcode effectue trois étapes : lire le descripteur de segment, faire les tests de protection mémoire, exécuter la lecture/écriture ou lever une exception. Létape de test est réalisée avec un ou plusieurs micro-branchements. Par exemple, une écriture va tester le bit R/W du descripteur, qui indique si on peut écrire dans le segment, en utilisant un micro-branchement. Le micro-branchement enverra vers une routine du microcode en cas d'erreur.
Les tests de protection mémoire demandent cependant de tester beaucoup de conditions différentes. Par exemple, le CPU Intel 386 testait moins d'une dizaine de conditions pour certaines instructions. Il est cependant possible de faire plusieurs comparaisons en parallèle en rusant un peu. Il suffit de mémoriser les octets de droits d'accès dans un registre interne, de masquer les bits non-pertinents, et de faire une comparaison avec une constante adéquate, qui encode la valeur que doivent avoir ces bits.
Une solution alternative utiliser un circuit combinatoire pour faire les tests de protection mémoire. Les tests sont alors faits en parallèles, plutôt qu'un par un par des micro-branchements. Par contre, le cout en matériel est assez important. Il faut ajouter ce circuit combinatoire, ce qui demande pas mal de circuits.
===La mémoire virtuelle avec la segmentation===
La mémoire virtuelle est une fonctionnalité souvent implémentée sur les processeurs qui gèrent la segmentation, alors que les processeurs avec relocation matérielle s'en passaient. Il faut dire que l'implémentation de la mémoire virtuelle est beaucoup plus simple avec la segmentation, comparé à la relocation matérielle. Le remplacement des registres de base par des sélecteurs de segment facilite grandement l'implémentation.
Le problème de la mémoire virtuelle est que les segments peuvent être swappés sur le disque dur n'importe quand, sans que le programme soit prévu. Le swapping est réalisé par une interruption de l'OS, qui peut interrompre le programme n'importe quand. Et si un segment est swappé, le registre de base correspondant devient invalide, il point sur une adresse en RAM où le segment était, mais n'est plus. De plus, les segments peuvent être déplacés en mémoire, là encore n'importe quand et d'une manière invisible par le programme, ce qui fait que les registres de base adéquats doivent être modifiés.
Si le programme entier est swappé d'un coup, comme avec la relocation matérielle simple, cela ne pose pas de problèmes. Mais dès qu'on utilise plusieurs registres de base par programme, les choses deviennent soudainement plus compliquées. Le problème est qu'il n'y a pas de mécanismes pour choisir et invalider le registre de base adéquat quand un segment est déplacé/swappé. En théorie, on pourrait imaginer des systèmes qui résolvent le problème au niveau de l'OS, mais tous ont des problèmes qui font que l'implémentation est compliquée ou que les performances sont ridicules.
L'usage d'une table des segments accédée à chaque accès résout complètement le problème. La table des segments est accédée à chaque accès mémoire, elle sait si le segment est swappé ou non, chaque accès vérifie si le segment est en mémoire et quelle est son adresse de base. On peut changer le segment de place n'importe quand, le prochain accès récupérera des informations à jour dans la table des segments.
L'implémentation de la mémoire virtuelle avec la segmentation est simple : il suffit d'ajouter un bit dans les descripteurs de segments, qui indique si le segment est swappé ou non. Tout le reste, la gestion de ce bit, du swap, et tout ce qui est nécessaire, est délégué au système d'exploitation. Lors de chaque accès mémoire, le processeur vérifie ce bit avant de faire la traduction d'adresse, et déclenche une exception matérielle si le bit indique que le segment est swappé. L'exception matérielle est gérée par l'OS.
===Le partage de segments===
Il est possible de partager un segment entre plusieurs applications. Cela peut servir pour partager des données entre deux programmes : un segment de données partagées est alors partagé entre deux programmes. Partager un segment de code est utile pour les bibliothèques partagées : la bibliothèque est placée dans un segment dédié, qui est partagé entre les programmes qui l'utilisent. Partager un segment de code est aussi utile quand plusieurs instances d'une même application sont lancés simultanément : le code n'ayant pas de raison de changer, celui-ci est partagé entre toutes les instances. Mais ce n'est là qu'un exemple.
La première solution pour cela est de configurer les tables de segment convenablement. Le même segment peut avoir des droits d'accès différents selon les processus. Les adresses de base/limite sont identiques, mais les tables des segments ont alors des droits d'accès différents. Mais cette méthode de partage des segments a plusieurs défauts.
Premièrement, les sélecteurs de segments ne sont pas les mêmes d'un processus à l'autre, pour un même segment. Le segment partagé peut correspondre au segment numéro 80 dans le premier processus, au segment numéro 1092 dans le second processus. Rien n'impose que les sélecteurs de segment soient les mêmes d'un processus à l'autre, pour un segment identique.
Deuxièmement, les adresses limite et de base sont dupliquées dans plusieurs tables de segments. En soi, cette redondance est un souci mineur. Mais une autre conséquence est une question de sécurité : que se passe-t-il si jamais un processus a une table des segments corrompue ? Il se peut que pour un segment identique, deux processus n'aient pas la même adresse limite, ce qui peut causer des failles de sécurité. Un processus peut alors subir un débordement de tampon, ou tout autre forme d'attaque.
[[File:Vm9.png|centre|vignette|upright=2|Illustration du partage d'un segment entre deux applications.]]
Une seconde solution, complémentaire, utilise une table de segment globale, qui mémorise des segments partagés ou accessibles par tous les processus. Les défauts de la méthode précédente disparaissent avec cette technique : un segment est identifié par un sélecteur unique pour tous les processus, il n'y a pas de duplication des descripteurs de segment. Par contre, elle a plusieurs défauts.
Le défaut principal est que cette table des segments est accessible par tous les processus, impossible de ne partager ses segments qu'avec certains pas avec les autres. Un autre défaut est que les droits d'accès à un segment partagé sont identiques pour tous les processus. Impossible d'avoir un segment partagé accessible en lecture seule pour un processus, mais accessible en écriture pour un autre. Il est possible de corriger ces défauts, mais nous en parlerons dans la section sur les architectures à capacité.
===L'extension d'adresse avec la segmentation===
L'extension d'adresse est possible avec la segmentation, de la même manière qu'avec la relocation matérielle. Il suffit juste que les adresses de base soient aussi grandes que le bus d'adresse. Mais il y a une différence avec la relocation matérielle : un même programme peut utiliser plus de mémoire qu'il n'y en a dans l'espace d'adressage. La raison est simple : un segment peut prendre tout l'espace d'adressage, et il y a plusieurs segments par programme.
Pour donner un exemple, prenons un processeur 16 bits, qui peut adresser 64 kibioctets, associé à une mémoire de 4 mébioctets. Il est possible de placer le code machine dans les premiers 64k de la mémoire, la pile du programme dans les 64k suivants, le tas dans les 64k encore après, et ainsi de suite. Le programme dépasse donc les 64k de mémoire de l'espace d'adressage. Ce genre de chose est impossible avec la relocation, où un programme est limité par l'espace d'adressage.
===Le mode protégé des processeurs x86===
L'Intel 80286, aussi appelé 286, ajouta un mode de segmentation séparé du mode réel, qui ajoute une protection mémoire à la segmentation, ce qui lui vaut le nom de '''mode protégé'''. Dans ce mode, les registres de segment ne contiennent pas des adresses de base, mais des sélecteurs de segments qui sont utilisés pour l'accès à la table des segments en mémoire RAM.
Le 286 bootait en mode réel, puis le système d'exploitation devait faire quelques manipulations pour passer en mode protégé. Le 286 était pensé pour être rétrocompatible au maximum avec le 80186. Mais les différences entre le 286 et le 8086 étaient majeures, au point que les applications devaient être réécrites intégralement pour profiter du mode protégé. Un mode de compatibilité permettait cependant aux applications destinées au 8086 de fonctionner, avec même de meilleures performances. Aussi, le mode protégé resta inutilisé sur la plupart des applications exécutées sur le 286.
Vint ensuite le processeur 80386, renommé en 386 quelques années plus tard. Sur ce processeur, les modes réel et protégé sont conservés tel quel, à une différence près : toutes les adresses passent à 32 bits, qu'il s'agisse des adresses de base, limite ou des ''offsets''. Le processeur peut donc adresser un grand nombre de segments : 2^32, soit plus de 4 milliards. Les segments grandissent aussi et passent de 64 KB maximum à 4 gibioctets maximum. Mais surtout : le 386 ajouta le support de la pagination en plus de la segmentation. Ces modifications ont été conservées sur les processeurs 32 bits ultérieurs.
Les processeurs x86 gèrent deux types de tables des segments : une table locale pour chaque processus, et une table globale partagée entre tous les processus. Il ne peut y avoir qu'une table locale d'active, vu que le processeur ne peut exécuter qu'un seul processus en même temps. Chaque table locale définit 8192 segments, pareil pour la table globale. La table globale est utilisée pour les segments du noyau et la mémoire partagée entre processus. Un défaut est qu'un segment partagé par la table globale est visible par tous les processus, avec les mêmes droits d'accès. Ce qui fait que cette méthode était peu utilisée en pratique. La table globale mémorise aussi des pointeurs vers les tables locales, avec un descripteur de segment par table locale.
Sur les processeurs x86 32 bits, un descripteur de segment est organisé comme suit, pour les architectures 32 bits. On y trouve l'adresse de base et la taille limite, ainsi que de nombreux bits de contrôle.
Le premier groupe de bits de contrôle est l'octet en bleu à droite. Il contient :
* le bit P qui indique que l'entrée contient un descripteur valide, qu'elle n'est pas vide ;
* deux bits DPL qui indiquent le niveau de privilège du segment (noyau, utilisateur, les deux intermédiaires spécifiques au x86) ;
* un bit S qui précise si le segment est de type système (utiles pour l'OS) ou un segment de code/données.
* un champ Type qui contient les bits suivants :
** un bit E qui indique si le segment contient du code exécutable ou non ;
** le bit RW qui indique s'il est en lecture seule ou non ;;
** Un bit A qui indique que le segment a récemment été accédé, information utile pour l'OS;
** un bit DC assez spécifiques.
En haut à gauche, en bleu, on trouve deux bits :
* Le bit G indique comment interpréter la taille contenue dans le descripteur : 0 si la taille est exprimée en octets, 1 si la taille est un nombre de pages de 4 kibioctets. Ce bit précise si on utilise la segmentation seule, ou combinée avec la pagination.
* Le bit DB précise si l'on utilise des segments en mode de compatibilité 16 bits ou des segments 32 bits.
[[File:SegmentDescriptor.svg|centre|vignette|upright=3|Segment Descriptor]]
Les indices de segment sont appelés des sélecteurs de segment. Ils ont une taille de 16 bits, mais 3 bits sont utilisés pour encoder des méta-données. Le numéro de segment est donc codé sur 13 bits, ce qui permettait de gérer maximum 8192 segments par table de segment (locale ou globale). Les 16 bits sont organisés comme suit :
* 13 bits pour le numéro du segment dans la table des segments, l'indice de segment proprement dit ;
* un bit qui précise s'il faut accéder à la table des segments globale ou locale ;
* deux bits qui indiquent le niveau de privilège de l'accès au segment (les 4 niveaux de protection, dont l'espace noyau et utilisateur).
[[File:SegmentSelector.svg|centre|vignette|upright=1.5|Sélecteur de segment 16 bit.]]
En tout, l'indice permet de gérer 8192 segments pour la table locale et 8192 segments de la table globale.
====La MMU du 386/486 : cache de segment, protection mémoire====
La MMU du 386 et celle du 486 étaient assez similaires. Elles étaient plus complexes que celle du 186 et du 286. Elle contenait un additionneur pour les calculs d'adresse et un comparateur pour tester si l'accès mémoire déborde d'un segment. Le test de débordement se faisait en parallèle du calcul de l'adresse finale, comme sur le 286.
L'additionneur était un additionneur trois-opérandes, qui additionnait l'adresse à lire/écrire, l'adresse de base du segment, et un décalage intégré dans l'instruction. En clair, l'unité de segmentation n'était pas qu'une MMU, elle prenait en charge une partie du calcul d'adresse. L'avantage est que cela permettait de gérer les modes d'adressage "base + décalage" et "base + indice + décalage" très facilement, en utilisant un minimum de circuits. Une conséquence de cette organisation était que l'usage d'un décalage était gratuit. Par contre, dès qu'on utilisait l'adressage "Base + Indice", avec ou sans décalage, l'instruction prenait un cycle de plus à s'exécuter, parce qu'il fallait faire l'addition "Base + Indice" dans l'ALU entière.
Le CPU 386 était le premier à implémenter la protection mémoire avec des segments. Pour cela, il intégrait une '''''Protection Test Unit''''', séparée du microcode, qu'on va abrévier en PTU. Précisément, il s'agissait d'un PLA (''Programmable Logic Array''), une sorte d'intermédiaire entre circuit logique fait sur mesure et mémoire ROM, qu'on a déjà abordé dans le chapitre sur les mémoires ROM. Mais cette unité ne faisait pas tout, le microcode était aussi impliqué. La PTU sera détaillée dans la section suivante.
Pour améliorer les performances, le 386 et le 486 intégraient un '''cache de descripteurs de segment''', aussi appelé le cache de descripteurs. Lorsqu'un descripteur état chargé pour la première fois, il était copié dans le cache de descripteurs de segment. Les accès mémoire ultérieur lisaient le descripteur de segment depuis ce cache, pas depuis la table des segments en RAM. Le cache de descripteurs gère aussi bien les segments en mode réel qu'en mode protégé. Récupérer l'adresse de base depuis cache se fait un peu différemment en mode réel et protégé, mais le cache gère cela tout seul. Idem pour récupérer la taille/adresse limite.
[[File:Microarchitecture du 386, avec focus sur la segmentation.png|centre|vignette|upright=2|Microarchitecture du 386 et du 486, avec focus sur la segmentation.]]
En mode réel, la taille des segment est censée être limitée à 64 kibioctets. Mais le processeur ne vérifiait pas si cette limite était dépassée. À la place, il utilisait la limite précisée dans le cache de segments. Pire que ça : le cache de segment n'était pas réinitialisé quand on passe du mode réel au mode protégé, et réciproquement. Et cette propriété a été à l'origine de l''''''unreal mode'''''. Il s'agissait d'un mode réel amélioré, capable d'utiliser des segments de 4 gibioctets et des adresses de 32 bits.
Passer en mode ''unreal'' pouvait se faire de deux manières. Il était possible d'altérer le cache de descripteur en utilisant l'instruction non-documentée LOADALL. Elle permettait de charger les descripteurs de segments dans le cache de descripteurs, avec une taille arbitraire. Une autre solution, beaucoup plus complexe sur le 386, demandait d'entrer en mode protégé pour configurer des segments de grande taille, de charger leurs descripteurs dans le cache de descripteur, puis de revenir en mode réel. En mode réel, les descripteurs dans le cache étaient encore disponibles et on pouvait les lire dans le cache.
====L'implémentation de la protection mémoire sur le 386====
La protection mémoire teste la valeur des bits P, S, X, E, R/W. Elle teste aussi les niveaux de privilège, avec deux bits DPL et CPL. En tout, le processeur pouvait tester 148 conditions différentes en parallèle dans la PTU. Cependant, les niveaux de privilèges étaient pré-traités par le microcode. Le microcode vérifiait aussi s'il y avait une erreur en terme d’anneau mémoire, avec par "exemple un segment en mode noyau accédé alors que le CPU est en espace utilisateur. Il fournissait alors un résultat sur deux bits, qui indiquait s'il y avait une erreur ou non, que la PTU utilisait.
Mais toutes les conditions n'étaient pas pertinentes à un instant t. Par exemple, il est pertinent de vérifier si le bit R/W était cohérent si l'instruction à exécuter est une écriture. Mais il n'y a pas besoin de tester le bit E qui indique qu'un segment est exécutable ou non, pour une lecture. En tout, le processeur pouvait se retrouver dans 33 situations possibles, chacune demandant de tester un sous-ensemble des 148 conditions. Pour préciser quel sous-ensembles tester, la PTU recevait un code opération, généré par le microcode.
Pour faire les tests de protection mémoire, le microcode avait une micro-opération nommée ''protection test operation'', qui envoyait les droits d'accès à la PTU. Lors de l'exécution d'une ''protection test operation'', le PLA recevait un descripteur de segment, lu depuis la mémoire RAM, ainsi qu'un code opération provenant du microcode.
{|class="wikitable"
|+ Entrée de la ''Protection Test Unit''
|-
! 15 - 14 !! 13 - 12 !! 11 !! 10 !! 9 !! 8 !! 7 !! 6 !! 5-0
|-
| P1 , P2 || || P || S || X || E || R/W || A || Code opération
|-
| Niveaux de privilèges cohérents/erreur || || Segment présent en mémoire ou swappé || S || X || Segment exécutable ou non || Segment accesible en lecture/écriture || Segment récemment accédé || Code opération
|}
Il fournissait en sortie un bit qui indiquait si une erreur de protection mémoire avait eu lieu ou non. Il fournissait aussi une adresse de 12 bits, utilisée seulement en cas d'erruer. Elle pointait dans le microcode, sur un code levant une exception en cas d'erreur. Enfin, la PTU fournissait 4 bits pouvant être testés par un branchement dans le microcode. L'un d'entre eux demandait de tester s'il y a un accès hors-limite, les autres étaient assez peu reliés à la protection mémoire.
Un détail est que le chargement du descripteur de segment est réalisé par une fonction dans le microcode. Elle est appliquée pour toutes les instructions ou situations qui demandent de faire un accès mémoire. Et les tests de protection mémoire sont réalisés dans cette fonction, pas après elle. Vu qu'il s'agit d'une fonction exécutée quelque soit l'instruction, le microcode doit transférer le code opération à cette fonction. Le microcode est pour cela associé à un registre interne, dans lequel le code opération est mémorisé, avant d'appeler la fonction. Le microcode a une micro-opération PTSAV (''Protection Save'') pour mémoriser le code opération dans ce registre. Dans la fonction qui charge le descripteur, une micro-opération PTOVRR (''Protection Override'') lit le code opération dans ce registre, et lance les tests nécessaires.
Il faut noter que le PLA était certes plus rapide que de tester les conditions une par une, mais il était assez lent. La PTU mettait environ 3 cycles d'horloges pour rendre son résultat. Le microcode en profitait alors pour exécuter des micro-opérations durant ces 3 cycles d'attente. Par exemple, le microcode pouvait en profiter pour lire l'adresse de base dans le descripteur, si elle n'a pas été chargée avant (les descripteur était chargé en deux fois). Il fallait cependant que les trois micro-opérations soient valides, peu importe qu'il y ait une erreur de protection mémoire ou non. Ou du moins, elles produisaient un résultat qui n'est pas utilisé en cas d'erreur. Si ce n'était pas possible, le microcode ajoutait des NOP pendant ce temps d'attente de 3 cycles.
Le bit A du descripteur de segment indique que le segment a récemment été accédé. Il est mis à jour après les tests de protection mémoire, quand ceux-ci indiquent que l'accès mémoire est autorisé. Le bit A est mis à 1 si la PTU l'autorise. Pour cela, la PTU utilise un des 4 bits de sortie mentionnés plus haut : l'un d'entre eux indique que le bit A doit être mis à 1. La mise à jour est ensuite réalisée par le microcode, qui utilise trois micro-opérations pour le mettre à jour.
====Le ''Hardware task switching'' des CPU x86====
Les systèmes d’exploitation modernes peuvent lancer plusieurs logiciels en même temps. Les logiciels sont alors exécutés à tour de rôle. Passer d'un programme à un autre est ce qui s'appelle une commutation de contexte. Lors d'une commutation de contexte, l'état du processeur est sauvegardé, afin que le programme stoppé puisse reprendre là où il était. Il arrivera un moment où le programme stoppé redémarrera et il doit reprendre dans l'état exact où il s'est arrêté. Deuxièmement, le programme à qui c'est le tour restaure son état. Cela lui permet de revenir là où il était avant d'être stoppé. Il y a donc une sauvegarde et une restauration des registres.
Divers processeurs incorporent des optimisations matérielles pour rendre la commutation de contexte plus rapide. Ils peuvent sauvegarder et restaurer les registres du processeur automatiquement lors d'une interruption de commutation de contexte. Les registres sont sauvegardés dans des structures de données en mémoire RAM, appelées des '''contextes matériels'''. Sur les processeurs x86, il s'agit de la technique d{{'}}''Hardware Task Switching''. Fait intéressant, le ''Hardware Task Switching'' se base beaucoup sur les segments mémoires.
Avec ''Hardware Task Switching'', chaque contexte matériel est mémorisé dans son propre segment mémoire, séparé des autres. Les segments pour les contextes matériels sont appelés des '''''Task State Segment''''' (TSS). Un TSS mémorise tous les registres généraux, le registre d'état, les pointeurs de pile, le ''program counter'' et quelques registres de contrôle du processeur. Par contre, les registres flottants ne sont pas sauvegardés, de même que certaines registres dit SIMD que nous n'avons pas encore abordé. Et c'est un défaut qui fait que le ''Hardware Task Switching'' n'est plus utilisé.
Le programme en cours d'exécution connait l'adresse du TSS qui lui est attribué, car elle est mémorisée dans un registre appelé le '''''Task Register'''''. En plus de pointer sur le TSS, ce registre contient aussi les adresses de base et limite du segment en cours. Pour être plus précis, le ''Task Register'' ne mémorise pas vraiment l'adresse du TSS. À la place, elle mémorise le numéro du segment, le numéro du TSS. Le numéro est codé sur 16 bits, ce qui explique que 65 536 segments sont adressables. Les instructions LDR et STR permettent de lire/écrire ce numéro de segment dans le ''Task Register''.
Le démarrage d'un programme a lieu automatiquement dans plusieurs circonstances. La première est une instruction de branchement CALL ou JMP adéquate. Le branchement fournit non pas une adresse à laquelle brancher, mais un numéro de segment qui pointe vers un TSS. Cela permet à une routine du système d'exploitation de restaurer les registres et de démarrer le programme en une seule instruction de branchement. Une seconde circonstance est une interruption matérielle ou une exception, mais nous la mettons de côté. Le ''Task Register'' est alors initialisé avec le numéro de segment fournit. S'en suit la procédure suivante :
* Le ''Task Register'' est utilisé pour adresser la table des segments, pour récupérer un pointeur vers le TSS associé.
* Le pointeur est utilisé pour une seconde lecture, qui adresse le TSS directement. Celle-ci restaure les registres du processeur.
En clair, on va lire le ''TSS descriptor'' dans la GDT, puis on l'utilise pour restaurer les registres du processeur.
[[File:Hardware Task Switching x86.png|centre|vignette|upright=2|Hardware Task Switching x86]]
===La segmentation sur les processeurs Burrough B5000 et plus===
Le Burrough B5000 est un très vieil ordinateur, commercialisé à partir de l'année 1961. Ses successeurs reprennent globalement la même architecture. C'était une machine à pile, doublé d'une architecture taguée, choses très rare de nos jours. Mais ce qui va nous intéresser dans ce chapitre est que ce processeur incorporait la segmentation, avec cependant une différence de taille : un programme avait accès à un grand nombre de segments. La limite était de 1024 segments par programme ! Il va de soi que des segments plus petits favorise l'implémentation de la mémoire virtuelle, mais complexifie la relocation et le reste, comme nous allons le voir.
Le processeur gère deux types de segments : les segments de données et de procédure/fonction. Les premiers mémorisent un bloc de données, dont le contenu est laissé à l'appréciation du programmeur. Les seconds sont des segments qui contiennent chacun une procédure, une fonction. L'usage des segments est donc différent de ce qu'on a sur les processeurs x86, qui n'avaient qu'un segment unique pour l'intégralité du code machine. Un seul segment de code machine x86 est découpé en un grand nombre de segments de code sur les processeurs Burrough.
La table des segments contenait 1024 entrées de 48 bits chacune. Fait intéressant, chaque entrée de la table des segments pouvait mémoriser non seulement un descripteur de segment, mais aussi une valeur flottante ou d'autres types de données ! Parler de table des segments est donc quelque peu trompeur, car cette table ne gère pas que des segments, mais aussi des données. La documentation appelaiat cette table la '''''Program Reference Table''''', ou PRT.
La raison de ce choix quelque peu bizarre est que les instructions ne gèrent pas d'adresses proprement dit. Tous les accès mémoire à des données en-dehors de la pile passent par la segmentation, ils précisent tous un indice de segment et un ''offset''. Pour éviter d'allouer un segment pour chaque donnée, les concepteurs du processeur ont décidé qu'une entrée pouvait contenir directement la donnée entière à lire/écrire.
La PRT supporte trois types de segments/descripteurs : les descripteurs de données, les descripteurs de programme et les descripteurs d'entrées-sorties. Les premiers décrivent des segments de données. Les seconds sont associés aux segments de procédure/fonction et sont utilisés pour les appels de fonction (qui passent, eux aussi, par la segmentation). Le dernier type de descripteurs sert pour les appels systèmes et les communications avec l'OS ou les périphériques.
Chaque entrée de la PRT contient un ''tag'', une suite de bit qui indique le type de l'entrée : est-ce qu'elle contient un descripteur de segment, une donnée, autre. Les descripteurs contiennent aussi un ''bit de présence'' qui indique si le segment a été swappé ou non. Car oui, les segments pouvaient être swappés sur ce processeur, ce qui n'est pas étonnant vu que les segments sont plus petits sur cette architecture. Le descripteur contient aussi l'adresse de base du segment ainsi que sa taille, et diverses informations pour le retrouver sur le disque dur s'il est swappé.
: L'adresse mémorisée ne faisait que 15 bits, ce qui permettait d'adresse 32 kibi-mots, soit 192 kibioctets de mémoire. Diverses techniques d'extension d'adressage étaient disponibles pour contourner cette limitation. Outre l'usage de l{{'}}''overlay'', le processeur et l'OS géraient aussi des identifiants d'espace d'adressage et en fournissaient plusieurs par processus. Les processeurs Borrough suivants utilisaient des adresses plus grandes, de 20 bits, ce qui tempérait le problème.
[[File:B6700Word.jpg|centre|vignette|upright=2|Structure d'un mot mémoire sur le B6700.]]
==Les architectures à capacités==
Les architectures à capacité utilisent la segmentation à granularité fine, mais ajoutent des mécanismes de protection mémoire assez particuliers, qui font que les architectures à capacité se démarquent du reste. Les architectures de ce type sont très rares et sont des processeurs assez anciens. Le premier d'entre eux était le Plessey System 250, qui date de 1969. Il fu suivi par le CAP computer, vendu entre les années 70 et 77. En 1978, le System/38 d'IBM a eu un petit succès commercial. En 1980, la Flex machine a aussi été vendue, mais à très peu d'examplaires, comme les autres architectures à capacité. Et enfin, en 1981, l'architecture à capacité la plus connue, l'Intel iAPX 432 a été commercialisée. Depuis, la seule architecture de ce type est en cours de développement. Il s'agit de l'architecture CHERI, dont la mise en projet date de 2014.
===Le partage de la mémoire sur les architectures à capacités===
Le partage de segment est grandement modifié sur les architectures à capacité. Avec la segmentation normale, il y a une table de segment par processus. Les conséquences sont assez nombreuses, mais la principale est que partager un segment entre plusieurs processus est compliqué. Les défauts ont été évoqués plus haut. Les sélecteurs de segments ne sont pas les mêmes d'un processus à l'autre, pour un même segment. De plus, les adresses limite et de base sont dupliquées dans plusieurs tables de segments, et cela peut causer des problèmes de sécurité si une table des segments est modifiée et pas l'autre. Et il y a d'autres problèmes, tout aussi importants.
[[File:Partage des segments avec la segmentation.png|centre|vignette|upright=1.5|Partage des segments avec la segmentation]]
À l'opposé, les architectures à capacité utilisent une table des segments unique pour tous les processus. La table des segments unique sera appelée dans de ce qui suit la '''table des segments globale''', ou encore la table globale. En conséquence, les adresses de base et limite ne sont présentes qu'en un seul exemplaire par segment, au lieu d'être dupliquées dans autant de processus que nécessaire. De plus, cela garantit que l'indice de segment est le même quel que soit le processus qui l'utilise.
Un défaut de cette approche est au niveau des droits d'accès. Avec la segmentation normale, les droits d'accès pour un segment sont censés changer d'un processus à l'autre. Par exemple, tel processus a accès en lecture seule au segment, l'autre seulement en écriture, etc. Mais ici, avec une table des segments uniques, cela ne marche plus : incorporer les droits d'accès dans la table des segments ferait que tous les processus auraient les mêmes droits d'accès au segment. Et il faut trouver une solution.
===Les capacités sont des pointeurs protégés===
Pour éviter cela, les droits d'accès sont combinés avec les sélecteurs de segments. Les sélecteurs des segments sont remplacés par des '''capacités''', des pointeurs particuliers formés en concaténant l'indice de segment avec les droits d'accès à ce segment. Si un programme veut accéder à une adresse, il fournit une capacité de la forme "sélecteur:droits d'accès", et un décalage qui indique la position de l'adresse dans le segment.
Il est impossible d'accéder à un segment sans avoir la capacité associée, c'est là une sécurité importante. Un accès mémoire demande que l'on ait la capacité pour sélectionner le bon segment, mais aussi que les droits d'accès en permettent l'accès demandé. Par contre, les capacités peuvent être passées d'un programme à un autre sans problème, les deux programmes pourront accéder à un segment tant qu'ils disposent de la capacité associée.
[[File:Comparaison entre capacités et adresses segmentées.png|centre|vignette|upright=2.5|Comparaison entre capacités et adresses segmentées]]
Mais cette solution a deux problèmes très liés. Au niveau des sélecteurs de segment, le problème est que les sélecteur ont une portée globale. Avant, l'indice de segment était interne à un programme, un sélecteur ne permettait pas d'accéder au segment d'un autre programme. Sur les architectures à capacité, les sélecteurs ont une portée globale. Si un programme arrive à forger un sélecteur qui pointe vers un segment d'un autre programme, il peut théoriquement y accéder, à condition que les droits d'accès le permettent. Et c'est là qu'intervient le second problème : les droits d'accès ne sont plus protégés par l'espace noyau. Les droits d'accès étaient dans la table de segment, accessible uniquement en espace noyau, ce qui empêchait un processus de les modifier. Avec une capacité, il faut ajouter des mécanismes de protection qui empêchent un programme de modifier les droits d'accès à un segment et de générer un indice de segment non-prévu.
La première sécurité est qu'un programme ne peut pas créer une capacité, seul le système d'exploitation le peut. Les capacités sont forgées lors de l'allocation mémoire, ce qui est du ressort de l'OS. Pour rappel, un programme qui veut du rab de mémoire RAM peut demander au système d'exploitation de lui allouer de la mémoire supplémentaire. Le système d'exploitation renvoie alors un pointeurs qui pointe vers un nouveau segment. Le pointeur est une capacité. Il doit être impossible de forger une capacité, en-dehors d'une demande d'allocation mémoire effectuée par l'OS. Typiquement, la forge d'une capacité se fait avec des instructions du processeur, que seul l'OS peut éxecuter (pensez à une instruction qui n'est accessible qu'en espace noyau).
La seconde protection est que les capacités ne peuvent pas être modifiées sans raison valable, que ce soit pour l'indice de segment ou les droits d'accès. L'indice de segment ne peut pas être modifié, quelqu'en soit la raison. Pour les droits d'accès, la situation est plus compliquée. Il est possible de modifier ses droits d'accès, mais sous conditions. Réduire les droits d'accès d'une capacité est possible, que ce soit en espace noyau ou utilisateur, pas l'OS ou un programme utilisateur, avec une instruction dédiée. Mais augmenter les droits d'accès, seul l'OS peut le faire avec une instruction précise, souvent exécutable seulement en espace noyau.
Les capacités peuvent être copiées, et même transférées d'un processus à un autre. Les capacités peuvent être détruites, ce qui permet de libérer la mémoire utilisée par un segment. La copie d'une capacité est contrôlée par l'OS et ne peut se faire que sous conditions. La destruction d'une capacité est par contre possible par tous les processus. La destruction ne signifie pas que le segment est effacé, il est possible que d'autres processus utilisent encore des copies de la capacité, et donc le segment associé. On verra quand la mémoire est libérée plus bas.
Protéger les capacités demande plusieurs conditions. Premièrement, le processeur doit faire la distinction entre une capacité et une donnée. Deuxièmement, les capacités ne peuvent être modifiées que par des instructions spécifiques, dont l'exécution est protégée, réservée au noyau. En clair, il doit y avoir une séparation matérielle des capacités, qui sont placées dans des registres séparés. Pour cela, deux solutions sont possibles : soit les capacités remplacent les adresses et sont dispersées en mémoire, soit elles sont regroupées dans un segment protégé.
====La liste des capacités====
Avec la première solution, on regroupe les capacités dans un segment protégé. Chaque programme a accès à un certain nombre de segments et à autant de capacités. Les capacités d'un programme sont souvent regroupées dans une '''liste de capacités''', appelée la '''''C-list'''''. Elle est généralement placée en mémoire RAM. Elle est ce qu'il reste de la table des segments du processus, sauf que cette table ne contient pas les adresses du segment, qui sont dans la table globale. Tout se passe comme si la table des segments de chaque processus est donc scindée en deux : la table globale partagée entre tous les processus contient les informations sur les limites des segments, la ''C-list'' mémorise les droits d'accès et les sélecteurs pour identifier chaque segment. C'est un niveau d'indirection supplémentaire par rapport à la segmentation usuelle.
[[File:Architectures à capacité.png|centre|vignette|upright=2|Architectures à capacité]]
La liste de capacité est lisible par le programme, qui peut copier librement les capacités dans les registres. Par contre, la liste des capacités est protégée en écriture. Pour le programme, il est impossible de modifier les capacités dedans, impossible d'en rajouter, d'en forger, d'en retirer. De même, il ne peut pas accéder aux segments des autres programmes : il n'a pas les capacités pour adresser ces segments.
Pour protéger la ''C-list'' en écriture, la solution la plus utilisée consiste à placer la ''C-list'' dans un segment dédié. Le processeur gère donc plusieurs types de segments : les segments de capacité pour les ''C-list'', les autres types segments pour le reste. Un défaut de cette approche est que les adresses/capacités sont séparées des données. Or, les programmeurs mixent souvent adresses et données, notamment quand ils doivent manipuler des structures de données comme des listes chainées, des arbres, des graphes, etc.
L'usage d'une ''C-list'' permet de se passer de la séparation entre espace noyau et utilisateur ! Les segments de capacité sont eux-mêmes adressés par leur propre capacité, avec une capacité par segment de capacité. Le programme a accès à la liste de capacité, comme l'OS, mais leurs droits d'accès ne sont pas les mêmes. Le programme a une capacité vers la ''C-list'' qui n'autorise pas l'écriture, l'OS a une autre capacité qui accepte l'écriture. Les programmes ne pourront pas forger les capacités permettant de modifier les segments de capacité. Une méthode alternative est de ne permettre l'accès aux segments de capacité qu'en espace noyau, mais elle est redondante avec la méthode précédente et moins puissante.
====Les capacités dispersées, les architectures taguées====
Une solution alternative laisse les capacités dispersées en mémoire. Les capacités remplacent les adresses/pointeurs, et elles se trouvent aux mêmes endroits : sur la pile, dans le tas. Comme c'est le cas dans les programmes modernes, chaque allocation mémoire renvoie une capacité, que le programme gére comme il veut. Il peut les mettre dans des structures de données, les placer sur la pile, dans des variables en mémoire, etc. Mais il faut alors distinguer si un mot mémoire contient une capacité ou une autre donnée, les deux ne devant pas être mixés.
Pour cela, chaque mot mémoire se voit attribuer un certain bit qui indique s'il s'agit d'un pointeur/capacité ou d'autre chose. Mais cela demande un support matériel, ce qui fait que le processeur devient ce qu'on appelle une ''architecture à tags'', ou ''tagged architectures''. Ici, elles indiquent si le mot mémoire contient une adresse:capacité ou une donnée.
[[File:Architectures à capacité sans liste de capacité.png|centre|vignette|upright=2|Architectures à capacité sans liste de capacité]]
L'inconvénient est le cout en matériel de cette solution. Il faut ajouter un bit à chaque case mémoire, le processeur doit vérifier les tags avant chaque opération d'accès mémoire, etc. De plus, tous les mots mémoire ont la même taille, ce qui force les capacités à avoir la même taille qu'un entier. Ce qui est compliqué.
===Les registres de capacité===
Les architectures à capacité disposent de registres spécialisés pour les capacités, séparés pour les entiers. La raison principale est une question de sécurité, mais aussi une solution pragmatique au fait que capacités et entiers n'ont pas la même taille. Les registres dédiés aux capacités ne mémorisent pas toujours des capacités proprement dites. À la place, ils mémorisent des descripteurs de segment, qui contiennent l'adresse de base, limite et les droits d'accès. Ils sont utilisés pour la relocation des accès mémoire ultérieurs. Ils sont en réalité identiques aux registres de relocation, voire aux registres de segments. Leur utilité est d'accélérer la relocation, entre autres.
Les processeurs à capacité ne gèrent pas d'adresses proprement dit, comme pour la segmentation avec plusieurs registres de relocation. Les accès mémoire doivent préciser deux choses : à quel segment on veut accéder, à quelle position dans le segment se trouve la donnée accédée. La première information se trouve dans le mal nommé "registre de capacité", la seconde information est fournie par l'instruction d'accès mémoire soit dans un registre (Base+Index), soit en adressage base+''offset''.
Les registres de capacités sont accessibles à travers des instructions spécialisées. Le processeur ajoute des instructions LOAD/STORE pour les échanges entre table des segments et registres de capacité. Ces instructions sont disponibles en espace utilisateur, pas seulement en espace noyau. Lors du chargement d'une capacité dans ces registres, le processeur vérifie que la capacité chargée est valide, et que les droits d'accès sont corrects. Puis, il accède à la table des segments, récupère les adresses de base et limite, et les mémorise dans le registre de capacité. Les droits d'accès et d'autres méta-données sont aussi mémorisées dans le registre de capacité. En somme, l'instruction de chargement prend une capacité et charge un descripteur de segment dans le registre.
Avec ce genre de mécanismes, il devient difficile d’exécuter certains types d'attaques, ce qui est un gage de sureté de fonctionnement indéniable. Du moins, c'est la théorie, car tout repose sur l'intégrité des listes de capacité. Si on peut modifier celles-ci, alors il devient facile de pouvoir accéder à des objets auxquels on n’aurait pas eu droit.
===Le recyclage de mémoire matériel===
Les architectures à capacité séparent les adresses/capacités des nombres entiers. Et cela facilite grandement l'implémentation de la ''garbage collection'', ou '''recyclage de la mémoire''', à savoir un ensemble de techniques logicielles qui visent à libérer la mémoire inutilisée.
Rappelons que les programmes peuvent demander à l'OS un rab de mémoire pour y placer quelque chose, généralement une structure de donnée ou un objet. Mais il arrive un moment où cet objet n'est plus utilisé par le programme. Il peut alors demander à l'OS de libérer la portion de mémoire réservée. Sur les architectures à capacité, cela revient à libérer un segment, devenu inutile. La mémoire utilisée par ce segment est alors considérée comme libre, et peut être utilisée pour autre chose. Mais il arrive que les programmes ne libèrent pas le segment en question. Soit parce que le programmeur a mal codé son programme, soit parce que le compilateur n'a pas fait du bon travail ou pour d'autres raisons.
Pour éviter cela, les langages de programmation actuels incorporent des '''''garbage collectors''''', des morceaux de code qui scannent la mémoire et détectent les segments inutiles. Pour cela, ils doivent identifier les adresses manipulées par le programme. Si une adresse pointe vers un objet, alors celui-ci est accessible, il sera potentiellement utilisé dans le futur. Mais si aucune adresse ne pointe vers l'objet, alors il est inaccessible et ne sera plus jamais utilisé dans le futur. On peut libérer les objets inaccessibles.
Identifier les adresses est cependant très compliqué sur les architectures normales. Sur les processeurs modernes, les ''garbage collectors'' scannent la pile à la recherche des adresses, et considèrent tout mot mémoire comme une adresse potentielle. Mais les architectures à capacité rendent le recyclage de la mémoire très facile. Un segment est accessible si le programme dispose d'une capacité qui pointe vers ce segment, rien de plus. Et les capacités sont facilement identifiables : soit elles sont dans la liste des capacités, soit on peut les identifier à partir de leur ''tag''.
Le recyclage de mémoire était parfois implémenté directement en matériel. En soi, son implémentation est assez simple, et peu être réalisé dans le microcode d'un processeur. Une autre solution consiste à utiliser un second processeur, spécialement dédié au recyclage de mémoire, qui exécute un programme spécialement codé pour. Le programme en question est placé dans une mémoire ROM, reliée directement à ce second processeur.
===L'intel iAPX 432===
Voyons maintenat une architecture à capacité assez connue : l'Intel iAPX 432. Oui, vous avez bien lu : Intel a bel et bien réalisé un processeur orienté objet dans sa jeunesse. La conception du processeur Intel iAPX 432 commença en 1975, afin de créer un successeur digne de ce nom aux processeurs 8008 et 8080.
La conception du processeur Intel iAPX 432 commença en 1975, afin de créer un successeur digne de ce nom aux processeurs 8008 et 8080. Ce processeur s'est très faiblement vendu en raison de ses performances assez désastreuses et de défauts techniques certains. Par exemple, ce processeur était une machine à pile à une époque où celles-ci étaient tombées en désuétude, il ne pouvait pas effectuer directement de calculs avec des constantes entières autres que 0 et 1, ses instructions avaient un alignement bizarre (elles étaient bit-alignées). Il avait été conçu pour maximiser la compatibilité avec le langage ADA, un langage assez peu utilisé, sans compter que le compilateur pour ce processeur était mauvais.
====Les segments prédéfinis de l'Intel iAPX 432====
L'Intel iAPX432 gère plusieurs types de segments. Rien d'étonnant à cela, les Burrough géraient eux aussi plusieurs types de segments, à savoir des segments de programmes, des segments de données, et des segments d'I/O. C'est la même chose sur l'Intel iAPX 432, mais en bien pire !
Les segments de données sont des segments génériques, dans lequels on peut mettre ce qu'on veut, suivant les besoins du programmeur. Ils sont tous découpés en deux parties de tailles égales : une partie contenant les données de l'objet et une partie pour les capacités. Les capacités d'un segment pointent vers d'autres segments, ce qui permet de créer des structures de données assez complexes. La ligne de démarcation peut être placée n'importe où dans le segment, les deux portions ne sont pas de taille identique, elles ont des tailles qui varient de segment en segment. Il est même possible de réserver le segment entier à des données sans y mettre de capacités, ou inversement. Les capacités et données sont adressées à partir de la ligne de démarcation, qui sert d'adresse de base du segment. Suivant l'instruction utilisée, le processeur accède à la bonne portion du segment.
Le processeur supporte aussi d'autres segments pré-définis, qui sont surtout utilisés par le système d'exploitation :
* Des segments d'instructions, qui contiennent du code exécutable, typiquement un programme ou des fonctions, parfois des ''threads''.
* Des segments de processus, qui mémorisent des processus entiers. Ces segments contiennent des capacités qui pointent vers d'autres segments, notamment un ou plusieurs segments de code, et des segments de données.
* Des segments de domaine, pour les modules ou bibliothèques dynamiques.
* Des segments de contexte, utilisés pour mémoriser l'état d'un processus, utilisés par l'OS pour faire de la commutation de contexte.
* Des segments de message, utilisés pour la communication entre processus par l'intermédiaire de messages.
* Et bien d'autres encores.
Sur l'Intel iAPX 432, chaque processus est considéré comme un objet à part entière, qui a son propre segment de processus. De même, l'état du processeur (le programme qu'il est en train d’exécuter, son état, etc.) est stocké en mémoire dans un segment de contexte. Il en est de même pour chaque fonction présente en mémoire : elle était encapsulée dans un segment, sur lequel seules quelques manipulations étaient possibles (l’exécuter, notamment). Et ne parlons pas des appels de fonctions qui stockaient l'état de l'appelé directement dans un objet spécial. Bref, de nombreux objets système sont prédéfinis par le processeur : les objets stockant des fonctions, les objets stockant des processus, etc.
L'Intel 432 possédait dans ses circuits un ''garbage collector'' matériel. Pour faciliter son fonctionnement, certains bits de l'objet permettaient de savoir si l'objet en question pouvait être supprimé ou non.
====Le support de la segmentation sur l'Intel iAPX 432====
La table des segments est une table hiérarchique, à deux niveaux. Le premier niveau est une ''Object Table Directory'', qui réside toujours en mémoire RAM. Elle contient des descripteurs qui pointent vers des tables secondaires, appelées des ''Object Table''. Il y a plusieurs ''Object Table'', typiquement une par processus. Plusieurs processus peuvent partager la même ''Object Table''. Les ''Object Table'' peuvent être swappées, mais pas l{{'}}''Object Table Directory''.
Une capacité tient compte de l'organisation hiérarchique de la table des segments. Elle contient un indice qui précise quelle ''Object Table'' utiliser, et l'indice du segment dans cette ''Object Table''. Le premier indice adresse l{{'}}''Object Table Directory'' et récupère un descripteur de segment qui pointe sur la bonne ''Object Table''. Le second indice est alors utilisé pour lire l'adresse de base adéquate dans cette ''Object Table''. La capacité contient aussi des droits d'accès en lecture, écriture, suppression et copie. Il y a aussi un champ pour le type, qu'on verra plus bas. Au fait : les capacités étaient appelées des ''Access Descriptors'' dans la documentation officielle.
Une capacité fait 32 bits, avec un octet utilisé pour les droits d'accès, laissant 24 bits pour adresser les segments. Le processeur gérait jusqu'à 2^24 segments/objets différents, pouvant mesurer jusqu'à 64 kibioctets chacun, ce qui fait 2^40 adresses différentes, soit 1024 gibioctets. Les 24 bits pour adresser les segments sont partagés moitié-moitié pour l'adressage des tables, ce qui fait 4096 ''Object Table'' différentes dans l{{'}}''Object Table Directory'', et chaque ''Object Table'' contient 4096 segments.
====Le jeu d'instruction de l'Intel iAPX 432====
L'Intel iAPX 432 est une machine à pile. Le jeu d'instruction de l'Intel iAPX 432 gère pas moins de 230 instructions différentes. Il gére deux types d'instructions : les instructions normales, et celles qui manipulent des segments/objets. Les premières permettent de manipuler des nombres entiers, des caractères, des chaînes de caractères, des tableaux, etc.
Les secondes sont spécialement dédiées à la manipulation des capacités. Il y a une instruction pour copier une capacité, une autre pour invalider une capacité, une autre pour augmenter ses droits d'accès (instruction sécurisée, exécutable seulement sous certaines conditions), une autre pour restreindre ses droits d'accès. deux autres instructions créent un segment et renvoient la capacité associée, la première créant un segment typé, l'autre non.
le processeur gérait aussi des instructions spécialement dédiées à la programmation système et idéales pour programmer des systèmes d'exploitation. De nombreuses instructions permettaient ainsi de commuter des processus, faire des transferts de messages entre processus, etc. Environ 40 % du micro-code était ainsi spécialement dédié à ces instructions spéciales.
Les instructions sont de longueur variable et peuvent prendre n'importe quelle taille comprise entre 10 et 300 bits, sans vraiment de restriction de taille. Les bits d'une instruction sont regroupés en 4 grands blocs, 4 champs, qui ont chacun une signification particulière.
* Le premier est l'opcode de l'instruction.
* Le champ référence, doit être interprété différemment suivant la donnée à manipuler. Si cette donnée est un entier, un caractère ou un flottant, ce champ indique l'emplacement de la donnée en mémoire. Alors que si l'instruction manipule un objet, ce champ spécifie la capacité de l'objet en question. Ce champ est assez complexe et il est sacrément bien organisé.
* Le champ format, n'utilise que 4 bits et a pour but de préciser si les données à manipuler sont en mémoire ou sur la pile.
* Le champ classe permet de dire combien de données différentes l'instruction va devoir manipuler, et quelles seront leurs tailles.
[[File:Encodage des instructions de l'Intel iAPX-432.png|centre|vignette|upright=2|Encodage des instructions de l'Intel iAPX-432.]]
====Le support de l'orienté objet sur l'Intel iAPX 432====
L'Intel 432 permet de définir des objets, qui correspondent aux classes des langages orientés objets. L'Intel 432 permet, à partir de fonctions définies par le programmeur, de créer des '''''domain objects''''', qui correspondent à une classe. Un ''domain object'' est un segment de capacité, dont les capacités pointent vers des fonctions ou un/plusieurs objets. Les fonctions et les objets sont chacun placés dans un segment. Une partie des fonctions/objets sont publics, ce qui signifie qu'ils sont accessibles en lecture par l'extérieur. Les autres sont privées, inaccessibles aussi bien en lecture qu'en écriture.
L'exécution d'une fonction demande que le branchement fournisse deux choses : une capacité vers le ''domain object'', et la position de la fonction à exécuter dans le segment. La position permet de localiser la capacité de la fonction à exécuter. En clair, on accède au ''domain object'' d'abord, pour récupérer la capacité qui pointe vers la fonction à exécuter.
Il est aussi possible pour le programmeur de définir de nouveaux types non supportés par le processeur, en faisant appel au système d'exploitation de l'ordinateur. Au niveau du processeur, chaque objet est typé au niveau de son object descriptor : celui-ci contient des informations qui permettent de déterminer le type de l'objet. Chaque type se voit attribuer un domain object qui contient toutes les fonctions capables de manipuler les objets de ce type et que l'on appelle le type manager. Lorsque l'on veut manipuler un objet d'un certain type, il suffit d'accéder à une capacité spéciale (le TCO) qui pointera dans ce type manager et qui précisera quel est l'objet à manipuler (en sélectionnant la bonne entrée dans la liste de capacité). Le type d'un objet prédéfini par le processeur est ainsi spécifié par une suite de 8 bits, tandis que le type d'un objet défini par le programmeur est défini par la capacité spéciale pointant vers son type manager.
===Conclusion===
Pour ceux qui veulent en savoir plus, je conseille la lecture de ce livre, disponible gratuitement sur internet (merci à l'auteur pour cette mise à disposition) :
* [https://homes.cs.washington.edu/~levy/capabook/ Capability-Based Computer Systems].
Voici un document qui décrit le fonctionnement de l'Intel iAPX432 :
* [https://homes.cs.washington.edu/~levy/capabook/Chapter9.pdf The Intel iAPX 432 ]
==La pagination==
Avec la pagination, la mémoire est découpée en blocs de taille fixe, appelés des '''pages mémoires'''. La taille des pages varie suivant le processeur et le système d'exploitation et tourne souvent autour de 4 kibioctets. Mais elles sont de taille fixe : on ne peut pas en changer la taille. C'est la différence avec les segments, qui sont de taille variable. Le contenu d'une page en mémoire fictive est rigoureusement le même que le contenu de la page correspondante en mémoire physique.
L'espace d'adressage est découpé en '''pages logiques''', alors que la mémoire physique est découpée en '''pages physique''' de même taille. Les pages logiques correspondent soit à une page physique, soit à une page swappée sur le disque dur. Quand une page logique est associée à une page physique, les deux ont le même contenu, mais pas les mêmes adresses. Les pages logiques sont numérotées, en partant de 0, afin de pouvoir les identifier/sélectionner. Même chose pour les pages physiques, qui sont elles aussi numérotées en partant de 0.
[[File:Principe de la pagination.png|centre|vignette|upright=2|Principe de la pagination.]]
Pour information, le tout premier processeur avec un système de mémoire virtuelle était le super-ordinateur Atlas. Il utilisait la pagination, et non la segmentation. Mais il fallu du temps avant que la méthode de la pagination prenne son essor dans les processeurs commerciaux x86.
Un point important est que la pagination implique une coopération entre OS et hardware, les deux étant fortement mélés. Une partie des informations de cette section auraient tout autant leur place dans le wikilivre sur les systèmes d'exploitation, mais il est plus simple d'en parler ici.
===La mémoire virtuelle : le ''swapping'' et le remplacement des pages mémoires===
Le système d'exploitation mémorise des informations sur toutes les pages existantes dans une '''table des pages'''. C'est un tableau où chaque ligne est associée à une page logique. Une ligne contient un bit ''Valid'' qui indique si la page logique associée est swappée sur le disque dur ou non, et la position de la page physique correspondante en mémoire RAM. Elle peut aussi contenir des bits pour la protection mémoire, et bien d'autres. Les lignes sont aussi appelées des ''entrées de la table des pages''
[[File:Gestionnaire de mémoire virtuelle - Pagination et swapping.png|centre|vignette|upright=2|Table des pages.]]
De plus, le système d'exploitation conserve une '''liste des pages vides'''. Le nom est assez clair : c'est une liste de toutes les pages de la mémoire physique qui sont inutilisées, qui ne sont allouées à aucun processus. Ces pages sont de la mémoire libre, utilisable à volonté. La liste des pages vides est mise à jour à chaque fois qu'un programme réserve de la mémoire, des pages sont alors prises dans cette liste et sont allouées au programme demandeur.
====Les défauts de page====
Lorsque l'on veut traduire l'adresse logique d'une page mémoire, le processeur vérifie le bit ''Valid'' et l'adresse physique. Si le bit ''Valid'' est à 1 et que l'adresse physique est présente, la traduction d'adresse s'effectue normalement. Mais si ce n'est pas le cas, l'entrée de la table des pages ne contient pas de quoi faire la traduction d'adresse. Soit parce que la page est swappée sur le disque dur et qu'il faut la copier en RAM, soit parce que les droits d'accès ne le permettent pas, soit parce que la page n'a pas encore été allouée, etc. On fait alors face à un '''défaut de page'''. Un défaut de page a lieu quand la MMU ne peut pas associer l'adresse logique à une adresse physique, quelque qu'en soit la raison.
Il existe deux types de défauts de page : mineurs et majeurs. Un '''défaut de page majeur''' a lieu quand on veut accéder à une page déplacée sur le disque dur. Un défaut de page majeur lève une exception matérielle dont la routine rapatriera la page en mémoire RAM. S'il y a de la place en mémoire RAM, il suffit d'allouer une page vide et d'y copier la page chargée depuis le disque dur. Mais si ce n'est par le cas, on va devoir faire de la place en RAM en déplaçant une page mémoire de la RAM vers le disque dur. Dans tous les cas, c'est le système d'exploitation qui s'occupe du chargement de la page, le processeur n'est pas impliqué. Une fois la page chargée, la table des pages est mise à jour et la traduction d'adresse peut recommencer. Si je dis recommencer, c'est car l'accès mémoire initial est rejoué à l'identique, sauf que la traduction d'adresse réussit cette fois-ci.
Un '''défaut de page mineur''' a lieu dans des circonstances pas très intuitives : la page est en mémoire physique, mais l'adresse physique de la page n'est pas accessible. Par exemple, il est possible que des sécurités empêchent de faire la traduction d'adresse, pour des raisons de protection mémoire. Une autre raison est la gestion des adresses synonymes, qui surviennent quand on utilise des libraires partagées entre programmes, de la communication inter-processus, des optimisations de type ''copy-on-write'', etc. Enfin, une dernière raison est que la page a été allouée à un programme par le système d'exploitation, mais qu'il n'a pas encore attribué sa position en mémoire. Pour comprendre comment c'est possible, parlons rapidement de l'allocation paresseuse.
Imaginons qu'un programme fasse une demande d'allocation mémoire et se voit donc attribuer une ou plusieurs pages logiques. L'OS peut alors réagir de deux manières différentes. La première est d'attribuer une page physique immédiatement, en même temps que la page logique. En faisant ainsi, on ne peut pas avoir de défaut mineur, sauf en cas de problème de protection mémoire. Cette solution est simple, on l'appelle l{{'}}'''allocation immédiate'''. Une autre solution consiste à attribuer une page logique, mais l'allocation de la page physique se fait plus tard. Elle a lieu la première fois que le programme tente d'écrire/lire dans la page physique. Un défaut mineur a lieu, et c'est lui qui force l'OS à attribuer une page physique pour la page logique demandée. On parle alors d{{'}}'''allocation paresseuse'''. L'avantage est que l'on gagne en performance si des pages logiques sont allouées mais utilisées, ce qui peut arriver.
Une optimisation permise par l'existence des défauts mineurs est le '''''copy-on-write'''''. Le but est d'optimiser la copie d'une page logique dans une autre. L'idée est que la copie est retardée quand elle est vraiment nécessaire, à savoir quand on écrit dans la copie. Tant que l'on ne modifie pas la copie, les deux pages logiques, originelle et copiée, pointent vers la même page physique. A quoi bon avoir deux copies avec le même contenu ? Par contre, la page physique est marquée en lecture seule. La moindre écriture déclenche une erreur de protection mémoire, et un défaut mineur. Celui-ci est géré par l'OS, qui effectue alors la copie dans une nouvelle page physique.
Je viens de dire que le système d'exploitation gère les défauts de page majeurs/mineurs. Un défaut de page déclenche une exception matérielle, qui passe la main au système d'exploitation. Le système d'exploitation doit alors déterminer ce qui a levé l'exception, notamment identifier si c'est un défaut de page mineur ou majeur. Pour cela, le processeur a un ou plusieurs '''registres de statut''' qui indique l'état du processeur, qui sont utiles pour gérer les défauts de page. Ils indiquent quelle est l'adresse fautive, si l'accès était une lecture ou écriture, si l'accès a eu lieu en espace noyau ou utilisateur (les espaces mémoire ne sont pas les mêmes), etc. Les registres en question varient grandement d'une architecture de processeur à l'autre, aussi on ne peut pas dire grand chose de plus sur le sujet. Le reste est de toute façon à voir dans un cours sur les systèmes d'exploitation.
====Le remplacement des pages====
Les pages virtuelles font référence soit à une page en mémoire physique, soit à une page sur le disque dur. Mais l'on ne peut pas lire une page directement depuis le disque dur. Les pages sur le disque dur doivent être chargées en RAM, avant d'être utilisables. Ce n'est possible que si on a une page mémoire vide, libre. Si ce n'est pas le cas, on doit faire de la place en swappant une page sur le disque dur. Les pages font ainsi une sorte de va et vient entre le fichier d'échange et la RAM, suivant les besoins. Tout cela est effectué par une routine d'interruption du système d'exploitation, le processeur n'ayant pas vraiment de rôle là-dedans.
Supposons que l'on veuille faire de la place en RAM pour une nouvelle page. Dans une implémentation naïve, on trouve une page à évincer de la mémoire, qui est copiée dans le ''swapfile''. Toutes les pages évincées sont alors copiées sur le disque dur, à chaque remplacement. Néanmoins, cette implémentation naïve peut cependant être améliorée si on tient compte d'un point important : si la page a été modifiée depuis le dernier accès. Si le programme/processeur a écrit dans la page, alors celle-ci a été modifiée et doit être sauvegardée sur le ''swapfile'' si elle est évincée. Par contre, si ce n'est pas le cas, la page est soit initialisée, soit déjà présente à l'identique dans le ''swapfile''.
Mais cette optimisation demande de savoir si une écriture a eu lieu dans la page. Pour cela, on ajoute un '''''dirty bit''''' à chaque entrée de la table des pages, juste à côté du bit ''Valid''. Il indique si une écriture a eu lieu dans la page depuis qu'elle a été chargée en RAM. Ce bit est mis à jour par le processeur, automatiquement, lors d'une écriture. Par contre, il est remis à zéro par le système d'exploitation, quand la page est chargée en RAM. Si le programme se voit allouer de la mémoire, il reçoit une page vide, et ce bit est initialisé à 0. Il est mis à 1 si la mémoire est utilisée. Quand la page est ensuite swappée sur le disque dur, ce bit est remis à 0 après la sauvegarde.
Sur la majorité des systèmes d'exploitation, il est possible d'interdire le déplacement de certaines pages sur le disque dur. Ces pages restent alors en mémoire RAM durant un temps plus ou moins long, parfois en permanence. Cette possibilité simplifie la vie des programmeurs qui conçoivent des systèmes d'exploitation : essayez d'exécuter l'interruption pour les défauts de page alors que la page contenant le code de l'interruption est placée sur le disque dur ! Là encore, cela demande d'ajouter un bit dans chaque entrée de la table des pages, qui indique si la page est swappable ou non. Le bit en question s'appelle souvent le '''bit ''swappable'''''.
====Les algorithmes de remplacement des pages pris en charge par l'OS====
Le choix de la page doit être fait avec le plus grand soin et il existe différents algorithmes qui permettent de décider quelle page supprimer de la RAM. Leur but est de swapper des pages qui ne seront pas accédées dans le futur, pour éviter d'avoir à faire triop de va-et-vient entre RAM et ''swapfile''. Les données qui sont censées être accédées dans le futur doivent rester en RAM et ne pas être swappées, autant que possible. Les algorithmes les plus simples pour le choix de page à évincer sont les suivants.
Le plus simple est un algorithme aléatoire : on choisit la page au hasard. Mine de rien, cet algorithme est très simple à implémenter et très rapide à exécuter. Il ne demande pas de modifier la table des pages, ni même d'accéder à celle-ci pour faire son choix. Ses performances sont surprenamment correctes, bien que largement en-dessous de tous les autres algorithmes.
L'algorithme FIFO supprime la donnée qui a été chargée dans la mémoire avant toutes les autres. Cet algorithme fonctionne bien quand un programme manipule des tableaux de grande taille, mais fonctionne assez mal dans le cas général.
L'algorithme LRU supprime la donnée qui été lue ou écrite pour la dernière fois avant toutes les autres. C'est théoriquement le plus efficace dans la majorité des situations. Malheureusement, son implémentation est assez complexe et les OS doivent modifier la table des pages pour l'implémenter.
L'algorithme le plus utilisé de nos jours est l{{'}}'''algorithme NRU''' (''Not Recently Used''), une simplification drastique du LRU. Il fait la différence entre les pages accédées il y a longtemps et celles accédées récemment, d'une manière très binaire. Les deux types de page sont appelés respectivement les '''pages froides''' et les '''pages chaudes'''. L'OS swappe en priorité les pages froides et ne swappe de page chaude que si aucune page froide n'est présente. L'algorithme est simple : il choisit la page à évincer au hasard parmi une page froide. Si aucune page froide n'est présente, alors il swappe au hasard une page chaude.
Pour implémenter l'algorithme NRU, l'OS mémorise, dans chaque entrée de la table des pages, si la page associée est froide ou chaude. Pour cela, il met à 0 ou 1 un bit dédié : le '''bit ''Accessed'''''. La différence avec le bit ''dirty'' est que le bit ''dirty'' est mis à jour uniquement lors des écritures, alors que le bit ''Accessed'' l'est aussi lors d'une lecture. Uen lecture met à 1 le bit ''Accessed'', mais ne touche pas au bit ''dirty''. Les écritures mettent les deux bits à 1.
Implémenter l'algorithme NRU demande juste de mettre à jour le bit ''Accessed'' de chaque entrée de la table des pages. Et sur les architectures modernes, le processeur s'en charge automatiquement. A chaque accès mémoire, que ce soit en lecture ou en écriture, le processeur met à 1 ce bit. Par contre, le système d'exploitation le met à 0 à intervalles réguliers. En conséquence, quand un remplacement de page doit avoir lieu, les pages chaudes ont de bonnes chances d'avoir le bit ''Accessed'' à 1, alors que les pages froides l'ont à 0. Ce n'est pas certain, et on peut se trouver dans des cas où ce n'est pas le cas. Par exemple, si un remplacement a lieu juste après la remise à zéro des bits ''Accessed''. Le choix de la page à remplacer est donc imparfait, mais fonctionne bien en pratique.
Tous les algorithmes précédents ont chacun deux variantes : une locale, et une globale. Avec la version locale, la page qui va être rapatriée sur le disque dur est une page réservée au programme qui est la cause du page miss. Avec la version globale, le système d'exploitation va choisir la page à virer parmi toutes les pages présentes en mémoire vive.
===La protection mémoire avec la pagination===
Avec la pagination, chaque page a des '''droits d'accès''' précis, qui permettent d'autoriser ou interdire les accès en lecture, écriture, exécution, etc. La table des pages mémorise les autorisations pour chaque page, sous la forme d'une suite de bits où chaque bit autorise/interdit une opération bien précise. En pratique, les tables de pages modernes disposent de trois bits : un qui autorise/interdit les accès en lecture, un qui autorise/interdit les accès en écriture, un qui autorise/interdit l'éxecution du contenu de la page.
Le format exact de la suite de bits a cependant changé dans le temps sur les processeurs x86 modernes. Par exemple, avant le passage au 64 bits, les CPU et OS ne pouvaient pas marquer une page mémoire comme non-exécutable. C'est seulement avec le passage au 64 bits qu'a été ajouté un bit pour interdire l'exécution de code depuis une page. Ce bit, nommé '''bit NX''', est à 0 si la page n'est pas exécutable et à 1 sinon. Le processeur vérifie à chaque chargement d'instruction si le bit NX de page lue est à 1. Sinon, il lève une exception matérielle et laisse la main à l'OS.
Une amélioration de cette protection est la technique dite du '''''Write XOR Execute''''', abréviée WxX. Elle consiste à interdire les pages d'être à la fois accessibles en écriture et exécutables. Il est possible de changer les autorisations en cours de route, ceci dit.
Les premiers IBM 360 disposaient d'un mécanisme de protection mémoire totalement différent, sans registres limite/base. Ce mécanisme de protection attribue à chaque programme une '''clé de protection''', qui consiste en un nombre unique de 4 bits (chaque programme a donc une clé différente de ses collègues). La mémoire est fragmentée en blocs de même taille, de 2 kibioctets. Le processeur mémorise, pour chacun de ses blocs, la clé de protection du programme qui a réservé ce bloc. À chaque accès mémoire, le processeur compare la clé de protection du programme en cours d’exécution et celle du bloc de mémoire de destination. Si les deux clés sont différentes, alors un programme a effectué un accès hors des clous et il se fait sauvagement arrêter.
===La traduction d'adresse avec la pagination===
Comme dit plus haut, les pages sont numérotées, de 0 à une valeur maximale, afin de les identifier. Le numéro en question est appelé le '''numéro de page'''. Il est utilisé pour dire au processeur : je veux lire une donnée dans la page numéro 20, la page numéro 90, etc. Une fois qu'on a le numéro de page, on doit alors préciser la position de la donnée dans la page, appelé le '''décalage''', ou encore l{{'}}''offset''.
Le numéro de page et le décalage se déduisent à partir de l'adresse, en divisant l'adresse par la taille de la page. Le quotient obtenu donne le numéro de la page, alors que le reste est le décalage. Les processeurs actuels utilisent tous des pages dont la taille est une puissance de deux, ce qui fait que ce calcul est fortement simplifié. Sous cette condition, le numéro de page correspond aux bits de poids fort de l'adresse, alors que le décalage est dans les bits de poids faible.
Le numéro de page existe en deux versions : un numéro de page physique qui identifie une page en mémoire physique, et un numéro de page logique qui identifie une page dans la mémoire virtuelle. Traduire l'adresse logique en adresse physique demande de remplacer le numéro de la page logique en un numéro de page physique.
[[File:Phycical address.JPG|centre|vignette|upright=2|Traduction d'adresse avec la pagination.]]
====Les tables des pages simples====
Dans le cas le plus simple, il n'y a qu'une seule table des pages, qui est adressée par les numéros de page logique. La table des pages est un vulgaire tableau d'adresses physiques, placées les unes à la suite des autres. Avec cette méthode, la table des pages a autant d'entrée qu'il y a de pages logiques en mémoire virtuelle. Accéder à la mémoire nécessite donc d’accéder d'abord à la table des pages en mémoire, de calculer l'adresse de l'entrée voulue, et d’y accéder.
[[File:Table des pages.png|centre|vignette|upright=2|Table des pages.]]
La table des pages est souvent stockée dans la mémoire RAM, son adresse est connue du processeur, mémorisée dans un registre spécialisé du processeur. Le processeur effectue automatiquement le calcul d'adresse à partir de l'adresse de base et du numéro de page logique.
[[File:Address translation (32-bit).png|centre|vignette|upright=2|Address translation (32-bit)]]
====Les tables des pages inversées====
Sur certains systèmes, notamment sur les architectures 64 bits ou plus, le nombre de pages est très important. Sur les ordinateurs x86 récents, les adresses sont en pratique de 48 bits, les bits de poids fort étant ignorés en pratique, ce qui fait en tout 68 719 476 736 pages. Chaque entrée de la table des pages fait au minimum 48 bits, mais fait plus en pratique : partons sur 64 bits par entrée, soit 8 octets. Cela fait 549 755 813 888 octets pour la table des pages, soit plusieurs centaines de gibioctets ! Une table des pages normale serait tout simplement impraticable.
Pour résoudre ce problème, on a inventé les '''tables des pages inversées'''. L'idée derrière celles-ci est l'inverse de la méthode précédente. La méthode précédente stocke, pour chaque page logique, son numéro de page physique. Les tables des pages inversées font l'inverse : elles stockent, pour chaque numéro de page physique, la page logique qui correspond. Avec cette méthode table des pages contient ainsi autant d'entrées qu'il y a de pages physiques. Elle est donc plus petite qu'avant, vu que la mémoire physique est plus petite que la mémoire virtuelle.
Quand le processeur veut convertir une adresse virtuelle en adresse physique, la MMU recherche le numéro de page de l'adresse virtuelle dans la table des pages. Le numéro de l'entrée à laquelle se trouve ce morceau d'adresse virtuelle est le morceau de l'adresse physique. Pour faciliter le processus de recherche dans la page, la table des pages inversée est ce que l'on appelle une table de hachage. C'est cette solution qui est utilisée sur les processeurs Power PC.
[[File:Table des pages inversée.jpg|centre|vignette|upright=2|Table des pages inversée.]]
====Les tables des pages multiples par espace d'adressage====
Dans les deux cas précédents, il y a une table des pages unique. Cependant, les concepteurs de processeurs et de systèmes d'exploitation ont remarqué que les adresses les plus hautes et/ou les plus basses sont les plus utilisées, alors que les adresses situées au milieu de l'espace d'adressage sont peu utilisées en raison du fonctionnement de la pile et du tas. Il y a donc une partie de la table des pages qui ne sert à rien et est utilisé pour des adresses inutilisées. C'est une source d'économie d'autant plus importante que les tables des pages sont de plus en plus grosses.
Pour profiter de cette observation, les concepteurs d'OS ont décidé de découper l'espace d'adressage en plusieurs sous-espaces d'adressage de taille identique : certains localisés dans les adresses basses, d'autres au milieu, d'autres tout en haut, etc. Et vu que l'espace d'adressage est scindé en plusieurs parties, la table des pages l'est aussi, elle est découpée en plusieurs sous-tables. Si un sous-espace d'adressage n'est pas utilisé, il n'y a pas besoin d'utiliser de la mémoire pour stocker la table des pages associée. On ne stocke que les tables des pages pour les espaces d'adressage utilisés, ceux qui contiennent au moins une donnée.
L'utilisation de plusieurs tables des pages ne fonctionne que si le système d'exploitation connaît l'adresse de chaque table des pages (celle de la première entrée). Pour cela, le système d'exploitation utilise une super-table des pages, qui stocke les adresses de début des sous-tables de chaque sous-espace. En clair, la table des pages est organisé en deux niveaux, la super-table étant le premier niveau et les sous-tables étant le second niveau.
L'adresse est structurée de manière à tirer profit de cette organisation. Les bits de poids fort de l'adresse sélectionnent quelle table de second niveau utiliser, les bits du milieu de l'adresse sélectionne la page dans la table de second niveau et le reste est interprété comme un ''offset''. Un accès à la table des pages se fait comme suit. Les bits de poids fort de l'adresse sont envoyés à la table de premier niveau, et sont utilisés pour récupérer l'adresse de la table de second niveau adéquate. Les bits au milieu de l'adresse sont envoyés à la table de second niveau, pour récupérer le numéro de page physique. Le tout est combiné avec l{{'}}''offset'' pour obtenir l'adresse physique finale.
[[File:Table des pages hiérarchique.png|centre|vignette|upright=2|Table des pages hiérarchique.]]
On peut aussi aller plus loin et découper la table des pages de manière hiérarchique, chaque sous-espace d'adressage étant lui aussi découpé en sous-espaces d'adressages. On a alors une table de premier niveau, plusieurs tables de second niveau, encore plus de tables de troisième niveau, et ainsi de suite. Cela peut aller jusqu'à 5 niveaux sur les processeurs x86 64 bits modernes. On parle alors de '''tables des pages emboitées'''. Dans ce cours, la table des pages désigne l'ensemble des différents niveaux de cette organisation, toutes les tables inclus. Seules les tables du dernier niveau mémorisent des numéros de page physiques, les autres tables mémorisant des pointeurs, des adresses vers le début des tables de niveau inférieur. Un exemple sera donné plus bas, dans la section suivante.
====L'exemple des processeurs x86====
Pour rendre les explications précédentes plus concrètes, nous allons prendre l'exemple des processeur x86 anciens, de type 32 bits. Les processeurs de ce type utilisaient deux types de tables des pages : une table des page unique et une table des page hiérarchique. Les deux étaient utilisées dans cas séparés. La table des page unique était utilisée pour les pages larges et encore seulement en l'absence de la technologie ''physical adress extension'', dont on parlera plus bas. Les autres cas utilisaient une table des page hiérarchique, à deux niveaux, trois niveaux, voire plus.
Une table des pages unique était utilisée pour les pages larges (de 2 mébioctets et plus). Pour les pages de 4 mébioctets, il y avait une unique table des pages, adressée par les 10 bits de poids fort de l'adresse, les bits restants servant comme ''offset''. La table des pages contenait 1024 entrées de 4 octets chacune, ce qui fait en tout 4 kibioctet pour la table des pages. La table des page était alignée en mémoire sur un bloc de 4 kibioctet (sa taille).
[[File:X86 Paging 4M.svg|centre|vignette|upright=2|X86 Paging 4M]]
Pour les pages de 4 kibioctets, les processeurs x86-32 bits utilisaient une table des page hiérarchique à deux niveaux. Les 10 bits de poids fort l'adresse adressaient la table des page maitre, appelée le directoire des pages (''page directory''), les 10 bits précédents servaient de numéro de page logique, et les 12 bits restants servaient à indiquer la position de l'octet dans la table des pages. Les entrées de chaque table des pages, mineure ou majeure, faisaient 32 bits, soit 4 octets. Vous remarquerez que la table des page majeure a la même taille que la table des page unique obtenue avec des pages larges (de 4 mébioctets).
[[File:X86 Paging 4K.svg|centre|vignette|upright=2|X86 Paging 4K]]
La technique du '''''physical adress extension''''' (PAE), utilisée depuis le Pentium Pro, permettait aux processeurs x86 32 bits d'adresser plus de 4 gibioctets de mémoire, en utilisant des adresses physiques de 64 bits. Les adresses virtuelles de 32 bits étaient traduites en adresses physiques de 64 bits grâce à une table des pages adaptée. Cette technologie permettait d'adresser plus de 4 gibioctets de mémoire au total, mais avec quelques limitations. Notamment, chaque programme ne pouvait utiliser que 4 gibioctets de mémoire RAM pour lui seul. Mais en lançant plusieurs programmes, on pouvait dépasser les 4 gibioctets au total. Pour cela, les entrées de la table des pages passaient à 64 bits au lieu de 32 auparavant.
La table des pages gardait 2 niveaux pour les pages larges en PAE.
[[File:X86 Paging PAE 2M.svg|centre|vignette|upright=2|X86 Paging PAE 2M]]
Par contre, pour les pages de 4 kibioctets en PAE, elle était modifiée de manière à ajouter un niveau de hiérarchie, passant de deux niveaux à trois.
[[File:X86 Paging PAE 4K.svg|centre|vignette|upright=2|X86 Paging PAE 4K]]
En 64 bits, la table des pages est une table des page hiérarchique avec 5 niveaux. Seuls les 48 bits de poids faible des adresses sont utilisés, les 16 restants étant ignorés.
[[File:X86 Paging 64bit.svg|centre|vignette|upright=2|X86 Paging 64bit]]
===La TLB et les caches associés===
En théorie, la table des pages est censée être accédée à chaque accès mémoire. Mais pour éviter d'avoir à lire la table des pages en mémoire RAM à chaque accès mémoire, les concepteurs de processeurs ont décidé d'implanter un cache dédié, le '''''translation lookaside buffer''''', ou TLB. Le TLB stocke au minimum de quoi faire la traduction entre adresse virtuelle et adresse physique, à savoir une correspondance entre numéro de page logique et numéro de page physique. Pour faire plus général, il stocke des entrées de la table des pages.
[[File:MMU principle updated.png|centre|vignette|upright=2.0|MMU avec une TLB.]]
Les accès à la table des pages sont gérés de deux façons : soit le processeur gère tout seul la situation, soit il délègue cette tâche au système d’exploitation. Sur les processeurs anciens, le système d'exploitation gère le parcours de la table des pages. Mais cette solution logicielle n'a pas de bonnes performances. D'autres processeurs gèrent eux-mêmes le défaut d'accès à la TLB et vont chercher d'eux-mêmes les informations nécessaires dans la table des pages. Ils disposent de circuits, les '''''page table walkers''''' (PTW), qui s'occupent eux-mêmes du défaut.
Les ''page table walkers'' contiennent des registres qui leur permettent de faire leur travail. Le plus important est celui qui mémorise la position de la table des pages en mémoire RAM, dont nous avons parlé plus haut. Les PTW ont besoin, pour faire leur travail, de mémoriser l'adresse physique de la table des pages, ou du moins l'adresse de la table des pages de niveau 1 pour des tables des pages hiérarchiques. Mais d'autres registres existent. Toutes les informations nécessaires pour gérer les défauts de TLB sont stockées dans des registres spécialisés appelés des '''tampons de PTW''' (PTW buffers).
===L'abstraction matérielle des processus===
[[File:Memoire virtuelle.svg|vignette|Mémoire virtuelle]]
Il est possible d'implémenter l'abstraction matérielle des processus avec la pagination. En clair, chaque programme lancé sur l'ordinateur dispose de son propre espace d'adressage, ce qui fait que la même adresse logique ne pointera pas sur la même adresse physique dans deux programmes différents. Pour cela, il y a plusieurs méthodes.
La première solution n'utilise qu'une seule table des pages. Pour cela, chaque entrée indique à quel processus elle est associée. Pour cela, chaque entrée contient un '''identifiant de processus''', un numéro qui précise pour quel processus, pour quel espace d'adressage, la correspondance est valide. La page des tables peut aussi contenir des entrées qui sont valides pour tous les processus en même temps. Pour cela, l'entrée contient un '''bit ''global''''', qui précise que cette correspondance est valide pour tous les processus, l'identifiant de processus est ignoré. Un défaut est que le partage d'une page entre plusieurs processus est impossible : soit on partage une page avec tous les processus, soit on l'alloue avec un seul processus.
L'intérêt de partager des pages entre tous les processus n'est pas évident, mais il le devient quand on se rappelle que le noyau de l'OS est mappé dans le haut de l'espace d'adressage. Et peu importe l'espace d'adressage, le noyau est toujours mappé de manière identique, les mêmes adresses logiques adressant la même adresse mémoire. En conséquence, les correspondances adresse physique-logique sont les mêmes pour le noyau, peu importe l'espace d'adressage. Le bit global accélère rapidement la traduction d'adresse pour l'accès au noyau.
Une solution alternative, plus simple, utilise une table des pages par processus lancé sur l'ordinateur, une table des pages unique par espace d'adressage. À chaque changement de processus, le registre qui mémorise la position de la table des pages est modifié pour pointer sur la bonne. C'est le système d'exploitation qui se charge de cette mise à jour.
Avec cette méthode, il est possible de partager une ou plusieurs pages entre plusieurs processus, en configurant les tables des pages convenablement. Les pages partagées sont mappées dans l'espace d'adressage de plusieurs processus, mais pas forcément au même endroit, pas forcément dans les mêmes adresses logiques. On peut placer la page partagée à l'adresse logique 0x0FFF pour un processus, à l'adresse logique 0xFF00 pour un autre processus, etc. Par contre, les entrées de la table des pages pour ces adresses pointent vers la même adresse physique.
[[File:Vm5.png|centre|vignette|upright=2|Tables des pages de plusieurs processus.]]
===La taille des pages===
La taille des pages varie suivant le processeur et le système d'exploitation et tourne souvent autour de 4 kibioctets. Les processeurs actuels gèrent plusieurs tailles différentes pour les pages : 4 kibioctets par défaut, 2 mébioctets, voire 1 à 4 gibioctets pour les pages les plus larges. Les pages de 4 kibioctets sont les pages par défaut, les autres tailles de page sont appelées des ''pages larges''. La taille optimale pour les pages dépend de nombreux paramètres et il n'y a pas de taille qui convienne à tout le monde. Certaines applications gagnent à utiliser des pages larges, d'autres vont au contraire perdre drastiquement en performance en les utilisant.
Le désavantage principal des pages larges est qu'elles favorisent la fragmentation mémoire. Si un programme veut réserver une portion de mémoire, pour une structure de donnée quelconque, il doit réserver une portion dont la taille est multiple de la taille d'une page. Par exemple, un programme ayant besoin de 110 kibioctets allouera 28 pages de 4 kibioctets, soit 120 kibioctets : 2 kibioctets seront perdus. Par contre, avec des pages larges de 2 mébioctets, on aura une perte de 2048 - 110 = 1938 kibioctets. En somme, des morceaux de mémoire seront perdus, car les pages sont trop grandes pour les données qu'on veut y mettre. Le résultat est que le programme qui utilise les pages larges utilisent plus de mémoire et ce d'autant plus qu'il utilise des données de petite taille. Un autre désavantage est qu'elles se marient mal avec certaines techniques d'optimisations de type ''copy-on-write''.
Mais l'avantage est que la traduction des adresses est plus performante. Une taille des pages plus élevée signifie moins de pages, donc des tables des pages plus petites. Et des pages des tables plus petites n'ont pas besoin de beaucoup de niveaux de hiérarchie, voire peuvent se limiter à des tables des pages simples, ce qui rend la traduction d'adresse plus simple et plus rapide. De plus, les programmes ont une certaine localité spatiale, qui font qu'ils accèdent souvent à des données proches. La traduction d'adresse peut alors profiter de systèmes de mise en cache dont nous parlerons dans le prochain chapitre, et ces systèmes de cache marchent nettement mieux avec des pages larges.
Il faut noter que la taille des pages est presque toujours une puissance de deux. Cela a de nombreux avantages, mais n'est pas une nécessité. Par exemple, le tout premier processeur avec de la pagination, le super-ordinateur Atlas, avait des pages de 3 kibioctets. L'avantage principal est que la traduction de l'adresse physique en adresse logique est trivial avec une puissance de deux. Cela garantit que l'on peut diviser l'adresse en un numéro de page et un ''offset'' : la traduction demande juste de remplacer les bits de poids forts par le numéro de page voulu. Sans cela, la traduction d'adresse implique des divisions et des multiplications, qui sont des opérations assez couteuses.
===Les entrées de la table des pages===
Avant de poursuivre, faisons un rapide rappel sur les entrées de la table des pages. Nous venons de voir que la table des pages contient de nombreuses informations : un bit ''valid'' pour la mémoire virtuelle, des bits ''dirty'' et ''accessed'' utilisés par l'OS, des bits de protection mémoire, un bit ''global'' et un potentiellement un identifiant de processus, etc. Étudions rapidement le format de la table des pages sur un processeur x86 32 bits.
* Elle contient d'abord le numéro de page physique.
* Les bits AVL sont inutilisés et peuvent être configurés à loisir par l'OS.
* Le bit G est le bit ''global''.
* Le bit PS vaut 0 pour une page de 4 kibioctets, mais est mis à 1 pour une page de 4 mébioctets dans le cas où le processus utilise des pages larges.
* Le bit D est le bit ''dirty''.
* Le bit A est le bit ''accessed''.
* Le bit PCD indique que la page ne peut pas être cachée, dans le sens où le processeur ne peut copier son contenu dans le cache et doit toujours lire ou écrire cette page directement dans la RAM.
* Le bit PWT indique que les écritures doivent mettre à jour le cache et la page en RAM (dans le chapitre sur le cache, on verra qu'il force le cache à se comporter comme un cache ''write-through'' pour cette page).
* Le bit U/S précise si la page est accessible en mode noyau ou utilisateur.
* Le bit R/W indique si la page est accessible en écriture, toutes les pages sont par défaut accessibles en lecture.
* Le bit P est le bit ''valid''.
[[File:PDE.png|centre|vignette|upright=2.5|Table des pages des processeurs Intel 32 bits.]]
==Comparaison des différentes techniques d'abstraction mémoire==
Pour résumer, l'abstraction mémoire permet de gérer : la relocation, la protection mémoire, l'isolation des processus, la mémoire virtuelle, l'extension de l'espace d'adressage, le partage de mémoire, etc. Elles sont souvent implémentées en même temps. Ce qui fait qu'elles sont souvent confondues, alors que ce sont des concepts sont différents. Ces liens sont résumés dans le tableau ci-dessous.
{|class="wikitable"
|-
!
! colspan="5" | Avec abstraction mémoire
! rowspan="2" | Sans abstraction mémoire
|-
!
! Relocation matérielle
! Segmentation en mode réel (x86)
! Segmentation, général
! Architectures à capacités
! Pagination
|-
! Abstraction matérielle des processus
| colspan="4" | Oui, relocation matérielle
| Oui, liée à la traduction d'adresse
| Emulée via relocation logicielle au démarrage du programme.
|-
! Mémoire virtuelle
| colspan="2" | Non, sauf émulation logicielle
| colspan="3" | Oui, gérée par le processeur et l'OS
| Non, sauf émulation logicielle
|-
! Extension de l'espace d'adressage
| colspan="2" | Oui : registre de base élargi
| colspan="2" | Oui : adresse de base élargie dans la table des segments
| ''Physical Adress Extension'' des processeurs 32 bits
| Commutation de banques
|-
! Protection mémoire
| Registre limite
| Aucune
| colspan="2" | Registre limite, droits d'accès aux segments
| Gestion des droits d'accès aux pages
| Possible, méthodes variées
|-
! Partage de mémoire
| colspan="2" | Non
| colspan="2" | Segment partagés
| Pages partagées
| Possible, méthodes variées
|}
===Les différents types de segmentation===
La segmentation regroupe plusieurs techniques franchement différentes, qui auraient gagné à être nommées différemment. La principale différence est l'usage de registres de relocation versus des registres de sélecteurs de segments. L'usage de registres de relocation est le fait de la relocation matérielle, mais aussi de la segmentation en mode réel des CPU x86. Par contre, l'usage de sélecteurs de segments est le fait des autres formes de segmentation, architectures à capacité inclues.
La différence entre les deux est le nombre de segments. L'usage de registres de relocation fait que le CPU ne gère qu'un petit nombre de segments de grande taille. La mémoire virtuelle est donc rarement implémentée vu que swapper des segments de grande taille est trop long, l'impact sur les performances est trop important. Sans compter que l'usage de registres de base se marie très mal avec la mémoire virtuelle. Vu qu'un segment peut être swappé ou déplacée n'importe quand, il faut invalider les registres de base au moment du swap/déplacement, ce qui n'est pas chose aisée. Aucun processeur ne gère cela, les méthodes pour n'existent tout simplement pas. L'usage de registres de base implique que la mémoire virtuelle est absente.
La protection mémoire est aussi plus limitée avec l'usage de registres de relocation. Elle se limite à des registres limite, mais la gestion des droits d'accès est limitée. En théorie, la segmentation en mode réel pourrait implémenter une version limitée de protection mémoire, avec une protection de l'espace exécutable. Mais ca n'a jamais été fait en pratique sur les processeurs x86.
Le partage de la mémoire est aussi difficile sur les architectures avec des registres de base. L'absence de table des segments fait que le partage d'un segment est basiquement impossible sans utiliser des méthodes complétement tordues, qui ne sont jamais implémentées en pratique.
===Segmentation versus pagination===
Par rapport à la pagination, la segmentation a des avantages et des inconvénients. Tous sont liés aux propriétés des segments et pages : les segments sont de grande taille et de taille variable, les pages sont petites et de taille fixe.
L'avantage principal de la segmentation est sa rapidité. Le fait que les segments sont de grande taille fait qu'on a pas besoin d'équivalent aux tables des pages inversée ou multiple, juste d'une table des segments toute simple. De plus, les échanges entre table des pages/segments et registres sont plus rares avec la segmentation. Par exemple, si un programme utilise un segment de 2 gigas, tous les accès dans le segment se feront avec une seule consultation de la table des segments. Alors qu'avec la pagination, il faudra une consultation de la table des pages chaque bloc de 4 kibioctet, au minimum.
Mais les désavantages sont nombreux. Le système d'exploitation doit agencer les segments en RAM, et c'est une tâche complexe. Le fait que les segments puisse changer de taille rend le tout encore plus complexe. Par exemple, si on colle les segments les uns à la suite des autres, changer la taille d'un segment demande de réorganiser tous les segments en RAM, ce qui demande énormément de copies RAM-RAM. Une autre possibilité est de laisser assez d'espace entre les segments, mais cet espace est alors gâché, dans le sens où on ne peut pas y placer un nouveau segment.
Swapper un segment est aussi très long, vu que les segments sont de grande taille, alors que swapper une page est très rapide.
==Un cas particulier : la z/architecture d'IBM==
IBM a sa propre gamme de ''mainframes'', qui utilisent des processeurs fait maison par IBM lui-même. Les CPU IBM ont leur propre jeu d'instruction CISC, appelé '''''z/architecture'''''. Et ce jeu d'instruction a des possibilités en termes d'adressage qui sont extrêmement intéressantes, en plus d'être assez inédites. Nous allons nous concentrer sur une fonctionnalité : les programmes ont accès à plusieurs espaces d'adressage simultanément. La ''z/architecture'' supporte 65 536 espaces d'adressage simultanés !
L'existence de ces deux modes s'explique par le fait que les ''mainframes'' IBM ont évolués dans le temps. Les premiers ''mainframes'' n'avaient même pas de support basique de la mémoire virtuelle, le System/360 s'en passait totalement. C'est sur le System/370 que la pagination a été ajoutée. Le System/370 supportait deux espaces d'adressage virtuel par programme, là où les implémentations classiques n'en utilisent qu'un par programme. L'ESA/370 a ajouté la possibilité d'utiliser 16 espaces d'adressage par programme, mais l'a fait d'une manière à garder la compatibilité avec le System/370. Le processeur a donc deux modes : un avec deux espaces d'adressage par programme, un autre pouvant monter jusqu’à 16.
La taille des adresses a aussi évoluée dans le temps : partant de 24 bits sur le System/360, elle est montée à 31 bits sur le System/370, pour arriver à 64 bits sur le jeu d'instruction z/architecture proprement dit. Sur le System/370, il était possible de switcher à la volée entre adressage 24 et 31 bits, avec les instructions ''Branch and save and
set mode'' (BASSM) et ''Branch And Set Mode'' (BSM).
Un point important est qu'IBM a mit le paquet en termes de compatibilité. Les programmes qui fonctionnaient sur le System/360 fonctionnent aussi sur le System/370,sur l'ESA 370, et sur les processeurs z/architecture récents. En conséquence, toute fonctionnalité introduite sur un modèle a été reprise à l'identique sur les modèles suivants. Aussi, pour comprendre comment la z/architecture gère les espaces d'adressage, on n'a pas le choix : on doit étudier chaque ''mainframe'' d'Ibm dans l'ordre chronologique. Suivre une approche historique permetttra de bien comprendre pourquoi un processeur ''z/architecture'' peut fonctionner dans deux modes, qu'on va appeler "standard" et "étendu", l'un avec seulement deux espaces d'adressage par programme, l'autre permettra de monter à 2048.
===Le System/370===
Le System/370 gérait des pages de 4 kibioctets, les pages larges ayant été introduites plus tard. La traduction d’adresse se fait en passant des regroupements de plusieurs pages virtuelles, appelés des ''segments'', qui n'ont rien à voir avec les segments au sens de la segmentation. Pour éviter les confusions, je vais les appeler des ''super-pages''. Les super-pages font 64 kibioctets ou 1 mébioctet, et regroupent plusieurs pages de 2 ou 4 mébioctets. Les pages d'une super-page peuvent être très éloignées en mémoire physique. Les super-pages sont concrètement des intermédiaires utilisés pour la traduction d'adresse. La traduction d'adresse démarre à une table des super-pages, qui point vers une table des pages à plusieurs niveaux (5 maximum).
Le System/370 était un système multi-processeur, ce qui a amené à quelques spécificités en termes d'adressage mémoire. La page numéro 0 sert pour le vecteur d'interruption et d'autres données importantes, pour des données importantes. Et chaque processeur a besoin de sa propre table d'interruption. Pour cela, chaque processeur réserve une page dédiée pour son vecteur d'interruption. Quand ce processeur accède à une adresse située dans la page numéro 0, l'adresse est modifiée pour tomber dans cette page réservée. Notons que le numéro de page converti est un numéro de page physique ! Le processeur fait donc une première traduction d'adresse pour déterminer l'adresse physique, et celle-ci est corrigée une seconde fois en cas d'accès à la première page mémoire. Le tout porte le nom de technique de '''''prefixing'''''.
Le System/370 supporte deux espaces d'adressages par programme : l''''espace d'adressage primaire''' où se situent le programme exécuté et les données importantes, un '''espace d'adressage secondaire''' utilisé pour des tâches/données annexes. Un troisième espace d'adressage l''''espace d'adressage ''home''''', est réservé au système d'exploitation. Il a pour particularité de ne pas avoir d'ASN. Il n'est concrètement accessible qu'en mode superviseur, donc par le système d'exploitation. C'est en quelque sorte un espace d'adressage pour le noyau, séparé des espaces d'adressage utilisateur. Nous n'en parlerons pas dans ce qui suit.
Mais comment le processeur pour utiliser ces espaces d'adressages ? Par exemple, s'il veut faire une lecture, comment précise-t-il l'espace d'adressage courant ? La réponse est que le processeur peut être dans deux états : soit primaire, soit secondaire. Dans le premier état, toutes les lectures/écritures se font dans l'espace d'adressage primaire. Dans le second, elles se font toutes dans l'espace d'adressage secondaire. Il peut passer de l'un à l'autre en utilisant l'instruction ''Set Address Space Control''. Le processeur dispose aussi d'instructions ''MOVE TO PRIMARY'' et ''MOVE TO SECONDARY'' pour échanger des données dans les deux espaces d'adressage.
L'espace d'adressage primaire a une table des pages dédiée, séparée de celle de l'espace d'adressage secondaire. Ou plutôt devrais-je dire une table des super-pages. La table des pages est une table des pages emboitée. Il y a peu à détailler dessus, si ce n'est pour cette histoire de segments/super-pages. L'adresse de la table des super-page est mémorisée dans le registre de contrôle CR1 pour l'espace d'adressage primaire, CR7 pour l'espace d'adressage secondaire. L'un ou l'autre est utilisé, selon que le processeur est en mode "primaire" ou "secondaire".
Un programme peut donc passer de l'espace d'adressage primaire à secondaire (ou inversement) en utilisant l’instruction ''SET ADDRESS SPACE CONTROL''.
===Les registres d'accès de l'ESA370===
Si le System/370 permettait d'utiliser deux espaces d'adressage par programme, l'ESA 370 a poussé la logique nettement plus loin. Il a ajouté un mode de fonctionnement supplémentaire, qui permet d'utiliser jusqu'à 2048 espaces d'adressage différents, en même temps. Le processeur n'a pas besoin d'utiliser d'instructions particulières pour copier des données d'un espace d'adressage à un autre, un simple MOV suffit. Le mode en question est appelé le '''mode ''access register'''''. Il est complémentaire du mode à deux espaces d'adressage, pour des raisons de compatibilité, l'ancien mode à deux esapces d'adressage a été conservé.
Le mode ''access register'' donne accès aux '''registres d'accès''', qui lui donnent son nom. Les registres d'accès mémorisent des identifiants qui permettent de sélectionner un espace d'adressage parmi 2048. Il y a en tout 16 registres d'accès, chacun étant appairé avec un registre général. Par exemple, le registre général numéro 5 est appairé au registre d'accès de même numéro. En mode ''access register'', toute lecture/écriture lit l'adresse dans un registre général et le registre d'accès associé indique dans quel espace d'adressage se trouve cette adresse. Par exemple, une lecture utilise l'adresse dans le registre général numéro 7, l'espace d'adressage associé sera dans le registre d'accès numéro 7.
Les registres d'accès peuvent être modifiés par des instructions classiques, en espace utilisateur. Elles sont nommées COPY ACCESS, EXTRACT ACCESS, LOAD ACCESS MULTIPLE, LOAD ADDRESS EXTENDED, SET ACCESS et STORE ACCESS MULTIPLE. Elles permettent de copier un registre d'accès dans un autre, de charger le contenu d'un registre d'accès depuis la mémoire RAM, d'enregistrer les registres d'accès dans la mémoire RAM, de copier une constante dans un registre d'accès. Mais quel est le contenu de ces registres ?
Une implémentation simple numéroterait les espaces d'adressage et mettrait les numéros dans les registres d'accès. Un espace d'adressage serait associé à une table des super-pages, reste à mémoriser la correspondance ''numéro d'espace d'adressage - adresse de la table des super-pages''. Pour cela, l'idéal est de mémoriser ces correspondances dans un tableau en mémoire RAM. Le numéro d'espace d'adressage est alors un indice, qui pointe sur l'entrée adéquate dans cette table. Il devrait y avoir un tableau par programme, qui serait protégé en écriture et seulement accessible par le système d'exploitation, comme le serait une table des pages. Mais ce n'est pas exactement ce qui est fait.
La différence est qu'il n'y a pas une table de correspondance par programme, mais un système à deux tableaux imbriqués. Le premier tableau possède 65 536 entrées, une par espace d'adressage supporté par le processeur. C'est donc une table partagée par tous les programmes. Par contre, la seconde table est bien unique pour chaque programme, chaque programme a la sienne. Faire ainsi permet de simplifier le partage de la mémoire. La table partagée est l''''''Access Space Table Entry''''' (ASTE), alors que la seconde table est la '''liste privée''' (''access list''), sous-entendu "privée pour un programme".
La liste privée mémorise une liste des espaces d'adressage auquel le programme peut accéder, auxquels le système d'exploitation lui a accordé l'accès. Pour chaque espace d'adressage, elle renvoie vers l'entrée de l'ASTE associée. En plus de mémoriser l'adresse de cette entrée, elle mémorise les droits d'accès que le programme a sur l'espace d'adressage associé. Par exemple, si deux programmes partagent un espace d'adressage, l'un pourra y avoir accès en lecture seule, l'autre pourra avoir un accès total. Pour cela, ils ont des droits différents dans la liste privée, mais l'entrée de l'ASTE sera la même.
[[File:Tradiction d'adresse sur la zarchitecture d'IBM.png|centre|vignette|upright=2|Traduction d'adresse sur la zarchitecture d'IBM.]]
Il y a donc une vérification des droits d'accès, suivi d'un accès à une table des super-pages globale. Si un programme veut accéder à un espace d'adressage qui lui est interdit, l'accès mémoire stoppera dans la liste privée. Le processeur utilisera l'identifiant d'espace d'adressage, mais la liste privée n'aura pas d'entrée associée. Elle lèvera alors une exception matérielle et le système d'exploitation gérera la situation, vraisemblablement en tuant le programme fautif. Mais si l'accès est autorisé, alors le programme accède à la table des super-pages et l'accès mémoire se poursuit.
Je dis une liste d'accès privée, mais il y en a deux en réalité. Mais la distinction exacte demanderait de parler de ''threads'' et de processus. Pour ceux qui sont familiers avec ces concepts, chaque ''thread'' a sa propre liste privée, avec en plus une liste par processus, partagée entre tous les ''threads''. Deux listes de 1024 entrées maximum, cela fait 2048 entrées au total. Les deux listes portent les noms de ''Dispatchable Unit Access List'' et de ''PASN-AL (Primary Address Space Access List''. Inutile de retenir ces deux termes, cependant.
Un registre d'accès mémorise un '''''Access List Entry Token''''' ou ALET. L'ALET contient l'indice d'une entrée dans la liste privée. Elle contient aussi un bit qui indique quelle liste privée utiliser, entre la ''Dispatchable Unit Access List'' et la ''PASN-AL (Primary Address Space Access List''. Précisons cependant que deux valeurs font exceptions : 0 pointe vers l'espace d'adressage principal, 1 vers l'espace d'adressage secondaire. Les autres valeurs sont configurables.
{|class="wikitable"
|+ Registre d'accès
|-
! 0000000 !! L (choix de la liste privée !! ASLEN !! ALEN
|-
| 0000000 || Choix de la liste privée || Identifiant de protection || Indice dans la liste privée
|}
Lors d'une lecture/écriture, l'ALETs est utilisé pour sélectionner l'espace d'adressage. Les valeurs 0 et 1 sont traitées telles quelles, l'espace d'adressage est déterminé automatiquement par le processeur. Pour les autres valeurs, récupérer l'adresse mémoire demande de consulter la liste d'accès privée. L'ALET est envoyé à la liste d'accès privée, et cette dernière fournit l'adresse de l'entrée de l'ASTE à consulter. Le processeur connait l'adresse de la liste privée, car son adresse mémoire est mémorisée dans les registres de contrôle numéro 2 et 5.
: Précisons que le registre d'accès numéro 0 contient obligatoirement la valeur 0, ce qui fait qu'il pointe vers l'espace d'adressage primaire.
[[File:Traduction d'adresse - trois espaces d'adressage, zarchitecture d'IBM.png|centre|vignette|upright=2|Traduction d'adresse - trois espaces d'adressage, zarchitecture d'IBM.]]
Une subtilité est que le système d'exploitation peut à tout moment modifier le contenu de la liste privée. Il se peut qu'un ALET qui correspondait à un espace d'adressage se mette à pointer vers un autre. Pour détecter cette situation, le processeur attribue un identifiant, à chaque espace d'adressage. L'identifiant est mémorisé dans les entrées de la liste privée, de l'ASTE et dans les registres d'accès (dans les bits de poids fort de l'ALET). L'identifiant est modifié dans l'ALET à chaque fois que le système d'exploitation modifie l'entrée. Si un programme fait un accès avec un registre d'accès, mais que l'identifiant ne correspond pas avec celui dans l'ALET/liste privée, c'est signe que l'espace d'adressage a été swappé, une exception matérielle est levée.
Un tel fonctionnement vous aura sans doute fait penser à une architecture à capacité, mais la z/architecture n'en est pas une. Malgré des points de ressemblance assez importants, il y a des différences fondamentales avec les architectures à capacité.
* Le z/architecture ne supporte pas la segmentation, qui est nécessaire pour les architectures à capacité. Sur la z/architecture, ce sont des espaces d'adressage qui sont sélectionnés, pas des segments.
* Les registres d'accès ne sont pas des registres de capacité, car on peut les modifier avec des instructions en espace utilisateur. En comparaison, modifier les registres de capacité ne peut se faire qu'en espace noyau, sous l'égide du système d'exploitation.
* Les droits d'accès sont dans une table séparée des pointeurs, pas concaténés à ceux-ci. La gestion des droits d'accès est gérée par les tables des pages.
* Les identifiants ALET ne peuvent pas être passés à un autre programme. Deux ALETs identiques correspondent à deux espaces d'adressages différents dans deux programmes différents. L'identifiant peut être identique, il pointera vers l'entrée de même numéro dans les deux listes privées, mais l'adresse dans l'entrée peut être différente.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=L'espace d'adressage du processeur
| prevText=L'espace d'adressage du processeur
| next=Les méthodes de synchronisation entre processeur et périphériques
| nextText=Les méthodes de synchronisation entre processeur et périphériques
}}
</noinclude>
5jzemrw1x2ykz4gvegit6a9dgeyroih
775341
775340
2026-10-16T21:35:05Z
Mewtow
31375
/* Un cas particulier : la z/architecture d'IBM */
775341
wikitext
text/x-wiki
Pour introduire ce chapitre, nous devons faire un rappel sur le concept d{{'}}'''espace d'adressage'''. Pour rappel, un espace d'adressage correspond à l'ensemble des adresses utilisables par le processeur. Par exemple, si je prends un processeur 16 bits, il peut adresser en tout 2^16 = 65536 adresses, l'ensemble de ces adresses forme son espace d'adressage. Intuitivement, on s'attend à ce qu'il y ait correspondance avec les adresses de la mémoire RAM. J'entends par là que l'adresse 1209 de l'espace d'adressage correspond à l'adresse 1209 en mémoire RAM. C'est là une hypothèse parfaitement raisonnable et on voit mal comment ce pourrait ne pas être le cas.
Mais les processeurs modernes utilisent des techniques d{{'}}'''abstraction mémoire''' qui font que ce n'est pas le cas. Avec ces techniques, l'adresse 1209 de l'espace d'adressage correspond en réalité à l'adresse 9999 en mémoire RAM, voire n'est pas en RAM. L'abstraction mémoire fait que l'espace d'adressage regroupe des adresses fictives, qui doivent être traduites en adresses mémoires réelles pour être utilisées. Les adresses de l'espace d'adressage portent le nom d{{'}}'''adresses logiques''', alors que les adresses de la mémoire RAM sont appelées '''adresses physiques'''.
L'intérêt de l'abstraction matérielle n'est pas évident. Aussi, avant de parler de comment l'abstraction mémoire fonctionne, nous allons voir à quoi elle sert. Nous allons voir qu'elle a plusieurs utilisations différentes, qui sont absolument nécessaires sur tous les ordinateurs personnels modernes. Tous les processeurs modernes la prennent en charge, les systèmes d'exploitation coopérent avec le processeur pour l'utiliser au mieux.
==L'abstraction mémoire permet plusieurs fonctionnalités complémentaires==
La fonctionnalité la plus connue est la mémoire virtuelle, dont vous avez peut-être déjà entendu parler, peut-être que le nom vous dit quelque chose. Si ce n'est pas le cas, nous la détailleront dans la suite. Elle permet concrétement à un programme d'utiliser plus de mémoire qu'il n'y a de RAM installé dans un ordinateur, en utilisant le disque dur comme solution de secours. Mais d'autres fonctionnalités moins évidentes sont permises par l'abstraction mémoire. Et la première que nous allons voir est l'abstraction des processus.
: En général, une adresse logique correspond à une seule adresse physique. Mais beaucoup de fonctionnalités avancées ne respectent pas cette règle.
===L'abstraction matérielle des processus===
Les systèmes d'exploitation modernes sont dits multi-tâche, à savoir qu'ils sont capables d'exécuter plusieurs logiciels en même temps. Et ce même si un seul processeur est présent dans l'ordinateur : les logiciels sont alors exécutés à tour de rôle. Toutefois, cela amène un paquet de problèmes qu'il faut résoudre au mieux. Par exemple, les programmes exécutés doivent se partager la mémoire RAM, ce qui ne vient pas sans problèmes. Le problème principal est que les programmes ne doivent pas lire ou écrire dans les données d'un autre, sans quoi on se retrouverait rapidement avec des problèmes. Il faut donc introduire des mécanismes d{{'}}'''isolement des processus''', pour isoler les programmes les uns des autres.
Un de ces mécanismes est l{{'}}'''abstraction matérielle des processus''', une technique qui fait que chaque programme a son propre espace d'adressage. Chaque programme a l'impression d'avoir accès à tout l'espace d'adressage, de l'adresse 0 à l'adresse maximale gérée par le processeur. Évidemment, il s'agit d'une illusion maintenue justement grâce à la traduction d'adresse. Les espaces d'adressage contiennent des adresses logiques, les adresses de la RAM sont des adresses physiques, la nécessité de l'abstraction mémoire est évidente.
Implémenter l'abstraction mémoire peut se faire de plusieurs manières. Mais dans tous les cas, il faut que la correspondance adresse logique - physique change d'un programme à l'autre. Ce qui est normal, vu que les deux processus sont placés à des endroits différents en RAM physique. La conséquence est qu'avec l'abstraction mémoire, une adresse logique correspond à plusieurs adresses physiques. Une même adresse logique dans deux processus différents correspond à deux adresses phsiques différentes, une par processus. Une adresse logique dans un processus correspondra à l'adresse physique X, la même adresse dans un autre processus correspondra à l'adresse Y.
Les adresses physiques qui partagent la même adresse logique sont alors appelées des '''adresses homonymes'''. Le choix de la bonne adresse étant réalisé par un mécanisme matériel et dépend du programme en cours. Le mécanisme pour choisir la bonne adresse dépend du processeur, mais il y en a deux grands types :
* La première consiste à utiliser l'identifiant de processus CPU, vu au chapitre précédent. C'est, pour rappel, un numéro attribué à chaque processus par le processeur. L'identifiant du processus en cours d'exécution est mémorisé dans un registre du processeur. La traduction d'adresse utilise cet identifiant, en plus de l'adresse logique, pour déterminer l'adresse physique.
* La seconde solution mémorise les correspondances adresses logiques-physique dans des tables en mémoire RAM, qui sont différentes pour chaque programme. Les tables sont accédées à chaque accès mémoire, afin de déterminer l'adresse physique.
===Le partage de la mémoire===
L'isolation des processus est très importante sur les systèmes d'exploitation modernes. Cependant, il existe quelques situations où elle doit être contournée ou du moins mise en pause. Les situations sont multiples : gestion de bibliothèques partagées, communication entre processus, usage de ''threads'', etc. Elles impliquent toutes un '''partage de mémoire''', à savoir qu'une portion de mémoire RAM est partagée entre plusieurs programmes. Le partage de mémoire est une sorte de brèche de l'isolation des processus, mais qui est autorisée car elle est utile.
Un cas intéressant est celui des '''bibliothèques partagées'''. Les bibliothèques sont des collections de fonctions regroupées ensemble, dans une seule unité de code. Un programme qui utilise une bibliothèque peut appeler n’importe quelle fonction présente dans la bibliothèque. La bibliothèque peut être simplement inclue dans le programme lui-même, on parle alors de bibliothèques statiques. De telles bibliothèques fonctionnent très bien, mais avec un petit défaut pour les bibliothèques très utilisées : plusieurs programmes qui utilisent la même bibliothèque vont chacun l'inclure dans leur code, ce qui fera doublon.
Pour éviter cela, les OS modernes gèrent des bibliothèques partagées, à savoir qu'un seul exemplaire de la bibliothèque est partagé entre plusieurs programmes. Chaque programme peut exécuter une fonction de la bibliothèque quand il le souhaite, en effectuant un branchement adéquat. Mais cela implique que la bibliothèque soit présente dans l'espace d'adressage du programme en question. Une bibliothèque est donc présente dans plusieurs espaces d'adressage, alors qu'il n'y en a qu'un seul exemplaire en mémoire RAM.
[[File:Ogg vorbis libs and application dia.svg|centre|vignette|upright=2|Exemple de bibliothèques, avec Ogg vorbis.]]
D'autres situations demandent de partager de la mémoire entre deux programmes. Par exemple, les systèmes d'exploitation modernes gèrent nativement des systèmes de '''communication inter-processus''', très utilisés par les programmes modernes pour échanger des données. Et la plupart demandant de partager un bout de mémoire entre processus, même si c'est seulement temporairement. Typiquement, deux processus partagent un intervalle d'adresse où l'un écrit les données à l'autre, l'autre lisant les données envoyées.
Une dernière utilisation de la mémoire partagée est l{{'}}'''accès direct au noyau'''. Sur les systèmes d'exploitations moderne, dans l'espace d'adressage de chaque programme, les adresses hautes sont remplies avec une partie du noyau ! Évidemment, ces adresses sont accessibles uniquement en lecture, pas en écriture. Pas question de modifier le noyau de l'OS ! De plus, il s'agit d'une portion du noyau dont on sait que la consultation ne pose pas de problèmes de sécurité.
Le programme peut lire des données dans cette portion du noyau, mais aussi exécuter les fonctions du noyau qui sont dedans. L'idée est d'éviter des appels systèmes trop fréquents. Au lieu d'effectuer un véritable appel système, avec une interruption logicielle, le programme peut exécuter des appels systèmes simplifiés, de simples appels de fonctions couplés avec un changement de niveau de privilège (passage en espace noyau nécessaire).
[[File:AMD64-canonical--48-bit.png|vignette|Répartition des adresses entre noyau (jaune/orange) et programme (verte), sur les systèmes x86-64 bits, avec des adresses physiques de 48 bits.]]
L'espace d'adressage est donc séparé en deux portions : l'OS d'un côté, le programme de l'autre. La répartition des adresses entre noyau et programme varie suivant l'OS ou le processeur utilisé. Sur les PC x86 32 bits, Linux attribuait 3 gigas pour les programmes et 1 giga pour le noyau, Windows attribuait 2 gigas à chacun. Sur les systèmes x86 64 bits, l'espace d'adressage d'un programme est coupé en trois, comme illustré ci-contre : une partie basse de 2^48 octets, une partie haute de même taille, et un bloc d'adresses invalides entre les deux. Les adresses basses sont utilisées pour le programme, les adresses hautes pour le noyau, il n'y a rien entre les deux.
Avec le partage de mémoire, plusieurs adresses logiques correspondent à la même adresse physique. Tel processus verra la zone de mémoire partagée à l'adresse X, l'autre la verra à l'adresse Y. Mais il s'agira de la même portion de mémoire physique, avec une seule adresse physique. En clair, lorsque deux processus partagent une même zone de mémoire, la zone sera mappées à des adresses logiques différentes. Les adresses logiques sont alors appelées des '''adresses synonymes''', terme qui trahit le fait qu'elles correspondent à la même adresse physique.
===La mémoire virtuelle===
Toutes les adresses ne sont pas forcément occupées par de la mémoire RAM, s'il n'y a pas assez de RAM installée. Par exemple, un processeur 32 bits peut adresser 4 gibioctets de RAM, même si seulement 3 gibioctets sont installés dans l'ordinateur. L'espace d'adressage contient donc 1 gigas d'adresses inutilisées, et il faut éviter ce surplus d'adresses pose problème.
Sans mémoire virtuelle, seule la mémoire réellement installée est utilisable. Si un programme utilise trop de mémoire, il est censé se rendre compte qu'il n'a pas accès à tout l'espace d'adressage. Quand il demandera au système d'exploitation de lui réserver de la mémoire, le système d'exploitation le préviendra qu'il n'y a plus de mémoire libre. Par exemple, si un programme tente d'utiliser 4 gibioctets sur un ordinateur avec 3 gibioctets de mémoire, il ne pourra pas. Pareil s'il veut utiliser 2 gibioctets de mémoire sur un ordinateur avec 4 gibioctets, mais dont 3 gibioctets sont déjà utilisés par d'autres programmes. Dans les deux cas, l'illusion tombe à plat.
Les techniques de '''mémoire virtuelle''' font que l'espace d'adressage est utilisable au complet, même s'il n'y a pas assez de mémoire installée dans l'ordinateur ou que d'autres programmes utilisent de la RAM. Par exemple, sur un processeur 32 bits, le programme aura accès à 4 gibioctets de RAM, même si d'autres programmes utilisent la RAM, même s'il n'y a que 2 gibioctets de RAM d'installés dans l'ordinateur.
Pour cela, on utilise une partie des mémoires de masse (disques durs) d'un ordinateur en remplacement de la mémoire physique manquante. Le système d'exploitation crée sur le disque dur un fichier, appelé le ''swapfile'' ou '''fichier de ''swap''''', qui est utilisé comme mémoire RAM supplémentaire. Il mémorise le surplus de données et de programmes qui ne peut pas être mis en mémoire RAM.
[[File:Vm1.png|centre|vignette|upright=2.0|Mémoire virtuelle et fichier de Swap.]]
Une technique naïve de mémoire virtuelle serait la suivante. Avant de l'aborder, précisons qu'il s'agit d'une technique abordée à but pédagogique, mais qui n'est implémentée nulle part tellement elle est lente et inefficace. Un espace d'adressage de 4 gigas ne contient que 3 gigas de RAM, ce qui fait 1 giga d'adresses inutilisées. Les accès mémoire aux 3 gigas de RAM se font normalement, mais l'accès aux adresses inutilisées lève une exception matérielle "Memory Unavailable". La routine d'interruption de cette exception accède alors au ''swapfile'' et récupère les données associées à cette adresse. La mémoire virtuelle est alors émulée par le système d'exploitation.
Le défaut de cette méthode est que l'accès au giga manquant est toujours très lent, parce qu'il se fait depuis le disque dur. D'autres techniques de mémoire virtuelle logicielle font beaucoup mieux, mais nous allons les passer sous silence, vu qu'on peut faire mieux, avec l'aide du matériel.
L'idée est de charger les données dont le programme a besoin dans la RAM, et de déplacer les autres sur le disque dur. Par exemple, imaginons la situation suivante : un programme a besoin de 4 gigas de mémoire, mais ne dispose que de 2 gigas de mémoire installée. On peut imaginer découper l'espace d'adressage en 2 blocs de 2 gigas, qui sont chargés à la demande. Si le programme accède aux adresses basses, on charge les 2 gigas d'adresse basse en RAM. S'il accède aux adresses hautes, on charge les 2 gigas d'adresse haute dans la RAM après avoir copié les adresses basses sur le ''swapfile''.
On perd du temps dans les copies de données entre RAM et ''swapfile'', mais on gagne en performance vu que tous les accès mémoire se font en RAM. Du fait de la localité temporelle, le programme utilise les données chargées depuis le swapfile durant un bon moment avant de passer au bloc suivant. La RAM est alors utilisée comme une sorte de cache alors que les données sont placées dans une mémoire fictive représentée par l'espace d'adressage et qui correspond au disque dur.
Mais avec cette technique, la correspondance entre adresses du programme et adresses de la RAM change au cours du temps. Les adresses de la RAM correspondent d'abord aux adresses basses, puis aux adresses hautes, et ainsi de suite. On a donc besoin d'abstraction mémoire. Les correspondances entre adresse logique et physique peuvent varier avec le temps, ce qui permet de déplacer des données de la RAM vers le disque dur ou inversement. Une adresse logique peut correspondre à une adresse physique, ou bien à une donnée swappée sur le disque dur. C'est l'unité de traduction d'adresse qui se charge de faire la différence. Si une correspondance entre adresse logique et physique est trouvée, elle l'utilise pour traduire les adresses. Si aucune correspondance n'est trouvée, alors elle laisse la main au système d'exploitation pour charger la donnée en RAM. Une fois la donnée chargée en RAM, les correspondances entre adresse logique et physiques sont modifiées de manière à ce que l'adresse logique pointe vers la donnée chargée.
===L'extension d'adressage===
Une autre fonctionnalité rendue possible par l'abstraction mémoire est l{{'}}'''extension d'adressage'''. Elle permet d'utiliser plus de mémoire que l'espace d'adressage ne le permet. Par exemple, utiliser 7 gigas de RAM sur un processeur 32 bits, dont l'espace d'adressage ne gère que 4 gigas. L'extension d'adresse est l'exact inverse de la mémoire virtuelle. La mémoire virtuelle sert quand on a moins de mémoire que d'adresses, l'extension d'adresse sert quand on a plus de mémoire que d'adresses.
Il y a quelques chapitres, nous avions vu que c'est possible via la commutation de banques. Mais l'abstraction mémoire est une méthode alternative. Que ce soit avec la commutation de banques ou avec l'abstraction mémoire, les adresses envoyées à la mémoire doivent être plus longues que les adresses gérées par le processeur. La différence est que l'abstraction mémoire étend les adresses d'une manière différente.
Une implémentation possible de l'extension d'adressage fait usage de l'abstraction matérielle des processus. Chaque processus a son propre espace d'adressage, mais ceux-ci sont placés à des endroits différents dans la mémoire physique. Par exemple, sur un ordinateur avec 16 gigas de RAM, mais un espace d'adressage de 2 gigas, on peut remplir la RAM en lançant 8 processus différents et chaque processus aura accès à un bloc de 2 gigas de RAM, pas plus, il ne peut pas dépasser cette limite. Ainsi, chaque processus est limité par son espace d'adressage, mais on remplit la mémoire avec plusieurs processus, ce qui compense. Il s'agit là de l'implémentation la plus simple, qui a en plus l'avantage d'avoir la meilleure compatibilité logicielle. De simples changements dans le système d'exploitation suffisent à l'implémenter.
[[File:Extension de l'espace d'adressage.png|centre|vignette|upright=1.5|Extension de l'espace d'adressage]]
Un autre implémentation donne plusieurs espaces d'adressage différents à chaque processus, et a donc accès à autant de mémoire que permis par la somme de ces espaces d'adressage. Par exemple, sur un ordinateur avec 16 gigas de RAM et un espace d'adressage de 4 gigas, un programme peut utiliser toute la RAM en utilisant 4 espaces d'adressage distincts. On passe d'un espace d'adressage à l'autre en changeant la correspondance adresse logique-physique. L'inconvénient est que la compatibilité logicielle est assez mauvaise. Modifier l'OS ne suffit pas, les programmeurs doivent impérativement concevoir leurs programmes pour qu'ils utilisent explicitement plusieurs espaces d'adressage.
Les deux implémentations font usage des adresses logiques homonymes, mais à l'intérieur d'un même processus. Pour rappel, cela veut dire qu'une adresse logique correspond à des adresses physiques différentes. Rien d'étonnant vu qu'on utilise plusieurs espaces d'adressage, comme pour l'abstraction des processus, sauf que cette fois-ci, on a plusieurs espaces d'adressage par processus. Prenons l'exemple où on a 8 gigas de RAM sur un processeur 32 bits, dont l'espace d'adressage ne gère que 4 gigas. L'idée est qu'une adresse correspondra à une adresse dans les premiers 4 gigas, ou dans les seconds 4 gigas. L'adresse logique X correspondra d'abord à une adresse physique dans les premiers 4 gigas, puis à une adresse physique dans les seconds 4 gigas.
===La protection mémoire===
La '''protection mémoire''' regroupe des techniques très différentes les unes des autres, qui visent à améliorer la sécurité des programmes et des systèmes d'exploitation. Elles visent à empêcher de lire, d'écrire ou d'exécuter certaines portions de mémoire. Sans elle, les programmes peuvent techniquement lire ou écrire les données des autres, ce qui causent des situations non-prévues par le programmeur, avec des conséquences qui vont d'un joli plantage à des failles de sécurité dangereuses.
La première technique de protection mémoire est l{{'}}'''isolation des processus''', qu'on a vue plus haut. Elle garantit que chaque programme n'a accès qu'à certaines portions dédiées de la mémoire et rend le reste de la mémoire inaccessible en lecture et en écriture. Le système d'exploitation attribue à chaque programme une ou plusieurs portions de mémoire rien que pour lui, auquel aucun autre programme ne peut accéder. Un tel programme, isolé des autres, s'appelle un '''processus''', d'où le nom de cet objectif. Toute tentative d'accès à une partie de la mémoire non autorisée déclenche une exception matérielle (rappelez-vous le chapitre sur les interruptions) qui est traitée par une routine du système d'exploitation. Généralement, le programme fautif est sauvagement arrêté et un message d'erreur est affiché à l'écran.
La '''protection de l'espace exécutable''' empêche d’exécuter quoique ce soit provenant de certaines zones de la mémoire. En effet, certaines portions de la mémoire sont censées contenir uniquement des données, sans aucun programme ou code exécutable. Cependant, des virus informatiques peuvent se cacher dedans et d’exécuter depuis celles-ci. Ou encore, des failles de sécurités peuvent permettre à un attaquant d'injecter du code exécutable malicieux dans des données, ce qui peut lui permettre de lire les données manipulées par un programme, prendre le contrôle de la machine, injecter des virus, ou autre. Pour éviter cela, le système d'exploitation peut marquer certaines zones mémoire comme n'étant pas exécutable. Toute tentative d’exécuter du code localisé dans ces zones entraîne la levée d'une exception ou d'une erreur et le système d'exploitation réagit en conséquence. Là encore, le processeur doit détecter les exécutions non autorisées.
D'autres méthodes de protection mémoire visent à limiter des actions dangereuses. Pour cela, le processeur et l'OS gèrent des '''droits d'accès''', qui interdisent certaines actions pour des programmes non-autorisés. Lorsqu'on exécute une opération interdite, le système d’exploitation et/ou le processeur réagissent en conséquence. La première technique de ce genre n'est autre que la séparation entre espace noyau et utilisateur, vue dans le chapitre sur les interruptions. Mais il y en a d'autres, comme nous le verrons dans ce chapitre.
==La MMU==
La traduction des adresses logiques en adresses physiques se fait par un circuit spécialisé, appelé la '''''Memory Management Unit''''' (MMU). De nos jours, elle est intégrée dans le processeur, à la suite de l'unité mémoire. Mais il a existé des processeurs avec une MMU externe, soudée sur la carte mère.
La traduction des adresses logiques en adresses physiques demande d'accéder à des données sont en mémoire RAM, qui sont gérés par le système d'exploitation. Aussi, les processeurs modernes incorporent des mémoires caches appelées des '''''Translation Lookaside Buffers''''', ou encore TLB. Nous nous pouvons pas parler des TLB pour le moment, car nous n'avons pas encore abordé le chapitre sur les mémoires caches, mais un chapitre entier sera dédié aux TLB d'ici peu.
[[File:MMU principle updated.png|centre|vignette|upright=2|MMU.]]
===Les MMU intégrées au processeur===
D'ordinaire, la MMU est intégrée au processeur. Et elle peut l'être de deux manières. La première en fait un circuit séparé, relié au bus d'adresse. La seconde fusionne la MMU avec l'unité de calcul d'adresse. La première solution est surtout utilisée avec une technique d'abstraction mémoire appelée la pagination, alors que l'autre l'est avec une autre méthode appelée la segmentation. La raison est que la traduction d'adresse avec la segmentation est assez simple : elle demande d'additionner le contenu d'un registre avec l'adresse logique, ce qui est le genre de calcul qu'une unité de calcul d'adresse sait déjà faire. La fusion est donc assez évidente.
Pour donner un exemple, l'Intel 8086 fusionnait l'unité de calcul d'adresse et la MMU. Précisément, il utilisait un même additionneur pour incrémenter le ''program counter'' et effectuer des calculs d'adresse liés à la segmentation. Il aurait été logique d'ajouter les pointeurs de pile avec, mais ce n'était pas possible. La raison est que le pointeur de pile ne peut pas être envoyé directement sur le bus d'adresse, vu qu'il doit passer par une phase de traduction en adresse physique liée à la segmentation.
[[File:80186 arch.png|centre|vignette|upright=2|Intel 8086, microarchitecture.]]
===Les MMU séparées du processeur, sur la carte mère===
Avant d'être intégrées au processeur, les MMU étaient des circuits séparés, placés sur la carte mère, entre le processeur et la mémoire. Elle étaient connectées en plein milieu du bus d'adresse. A savoir que le bus d'adresse entrait dans la MMU et en ressortait. Le processeur envoyait des adresses à la MMU, qui renvoyait une adresse physique. La MMU était facultative, à savoir que le système pouvait fonctionner sans MMU d'installée, mais l'abstraction mémoire n’était alors pas disponible.
Par exemple, les processeurs Motorola 68000 et 68010 pouvaient être combinés avec une MMU de type Motorola 68451. Elle supportait des versions simplifiées de la segmentation et de la pagination. Au minimum, elle ajoutait un support de la protection mémoire contre certains accès non-autorisés. La gestion de la mémoire virtuelle proprement dit n'était possible que si le processeur utilisé était un Motorola 68010, en raison de la manière dont le 68000 gérait ses accès mémoire. La MMU 68451 gérait un espace d'adressage de 16 mébioctets, découpé en maximum 32 pages/segments. On pouvait dépasser cette limite de 32 segments/pages en combinant plusieurs 68451.
Le Motorola 68851 était une MMU qui était prévue pour fonctionner de paire avec le Motorola 68020. Elle gérait la pagination pour un espace d'adressage de 32 bits.
Les processeurs suivants, les 68030, 68040, et 68060, avaient une MMU interne au processeur.
==La relocation matérielle==
Pour rappel, les systèmes d'exploitation moderne permettent de lancer plusieurs programmes en même temps et les laissent se partager la mémoire. Dans le cas le plus simple, qui n'est pas celui des OS modernes, le système d'exploitation découpe la mémoire en blocs d'adresses contiguës qui sont appelés des '''segments''', ou encore des ''partitions mémoire''. Les segments correspondent à un bloc de mémoire RAM. C'est-à-dire qu'un segment de 259 mébioctets sera un segment continu de 259 mébioctets dans la mémoire physique comme dans la mémoire logique. Dans ce qui suit, un segment contient un programme en cours d'exécution, comme illustré ci-dessous.
[[File:CPT Memory Addressable.svg|centre|vignette|upright=2|Espace d'adressage segmenté.]]
Le système d'exploitation mémorise la position de chaque segment en mémoire, ainsi que d'autres informations annexes. Le tout est regroupé dans la '''table de segment''', un tableau dont chaque case est attribuée à un programme/segment. La table des segments est un tableau numéroté, chaque segment ayant un numéro qui précise sa position dans le tableau. Chaque case, chaque entrée, contient un '''descripteur de segment''' qui regroupe plusieurs informations sur le segment : son adresse de base, sa taille, diverses informations.
===La relocation avec la relocation matérielle : le registre de base===
Un segment peut être placé n'importe où en RAM physique et sa position en RAM change à chaque exécution. Le programme est chargé à une adresse, celle du début du segment, qui change à chaque chargement du programme. Et toutes les adresses utilisées par le programme doivent être corrigées lors du chargement du programme, généralement par l'OS. Cette correction s'appelle la '''relocation''', et elle consiste à ajouter l'adresse de début du segment à chaque adresse manipulée par le programme.
[[File:Relocation assistée par matériel.png|centre|vignette|upright=2.5|Relocation.]]
La relocation matérielle fait que la relocation est faite par le processeur, pas par l'OS. La relocation est intégrée dans le processeur par l'intégration d'un registre : le '''registre de base''', aussi appelé '''registre de relocation'''. Il mémorise l'adresse à laquelle commence le segment, la première adresse du programme. Pour effectuer la relocation, le processeur ajoute automatiquement l'adresse de base à chaque accès mémoire, en allant la chercher dans le registre de relocation.
[[File:Registre de base de segment.png|centre|vignette|upright=2|Registre de base de segment.]]
Le processeur s'occupe de la relocation des segments et le programme compilé n'en voit rien. Pour le dire autrement, les programmes manipulent des adresses logiques, qui sont traduites par le processeur en adresses physiques. La traduction se fait en ajoutant le contenu du registre de relocation à l'adresse logique. De plus, cette méthode fait que chaque programme a son propre espace d'adressage.
[[File:CPU created logical address presentation.png|centre|vignette|upright=2|Traduction d'adresse avec la relocation matérielle.]]
Le système d'exploitation mémorise les adresses de base pour chaque programme, dans la table des segments. Le registre de base est mis à jour automatiquement lors de chaque changement de segment. Pour cela, le registre de base est accessible via certaines instructions, accessibles en espace noyau, plus rarement en espace utilisateur. Le registre de segment est censé être adressé implicitement, vu qu'il est unique. Si ce n'est pas le cas, il est possible d'écrire dans ce registre de segment, qui est alors adressable.
===La protection mémoire avec la relocation matérielle : le registre limite===
Sans restrictions supplémentaires, la taille maximale d'un segment est égale à la taille complète de l'espace d'adressage. Sur les processeurs 32 bits, un segment a une taille maximale de 2^32 octets, soit 4 gibioctets. Mais il est possible de limiter la taille du segment à 2 gibioctets, 1 gibioctet, 64 Kibioctets, ou toute autre taille. La limite est définie lors de la création du segment, mais elle peut cependant évoluer au cours de l'exécution du programme, grâce à l'allocation mémoire.
Le processeur vérifie à chaque accès mémoire que celui-ci se fait bien dans le segment, qu'il ne déborde pas en-dehors. C'est possible qu'une adresse calculée sorte du segment, à la suite d'un bug ou d'une erreur de programmation, voire pire. Et le processeur doit éviter de tels '''débordements de segments'''.
A chaque accès mémoire, le processeur compare l'adresse accédée et vérifie qu'elle est bien dans le segment. Pour cela, il y a deux solutions. La première part du principe que le segment est placé en mémoire entre l'adresse de base et l'adresse limite. Il suffit de mémoriser l'adresse limite, l'adresse physique à ne pas dépasser. Une autre solution mémorise la taille du segment. La table des segments doit donc mémoriser, en plus de l'adresse de base : soit l'adresse maximale du segment, soit la taille du segment. D'autres informations peuvent être ajoutées, comme on le verra plus tard, mais cela complexifie la table des segments.
De plus, le processeur se voit ajouter un '''registre limite''', qui mémorise soit la taille du segment, soit l'adresse limite. Les deux registres, base et limite, sont utilisés pour vérifier si un programme qui lit/écrit de la mémoire en-dehors de son segment attitré : au-delà pour le registre limite, en-deça pour le registre de base. Le processeur vérifie pour chaque accès mémoire ne déborde pas au-delà du segment qui lui est allouée, ce qui n'arrive que si l'adresse d'accès dépasse la valeur du registre limite. Pour les accès en-dessous du segment, il suffit de vérifier si l'addition de relocation déborde, tout débordement signifiant erreur de protection mémoire.
[[File:Registre limite.png|centre|vignette|upright=2|Registre limite]]
Utiliser la taille du segment a de nombreux avantages. L'un d'entre eux se manifeste quand on déplace un segment en mémoire RAM. Le descripteur doit alors être mis à jour, et c'est plus facile quand on utilise la taille du segment. Si on utilise l'adresse limite, il faut mettre à jour à la fois l'adresse de base et l'adresse limite, dans le descripteur. En utilisant la taille, seule l'adresse de base doit être modifiée, vu que le segment n'a pas changé de taille. Un autre avantage est lié aux performances, mais nous devons faire un détour pour le comprendre.
La taille du segment est équivalent à l'adresse logique maximale possible. Par exemple, si un segment fait 256 octets, les adresses logiques possibles vont de 0 à 255, 256 est donc à la fois la taille du segment et l'adresse logique à partir de laquelle on déborde du segment. Et cela marche si on remplace 256 par n'importe quelle valeur : vu que le segment commence à l'adresse 0, sa taille en octets indique l'adresse de dépassement. Interpréter la taille du segment comme une adresse logique fait que les tests avec le registre limite sont plus performants, voyons pourquoi.
En utilisant l'adresse physique limite, on doit faire la relocation, puis comparer l'adresse calculée avec l'adresse limite. Le calcul d'adresse doit se faire avant la vérification. En utilisant la taille, on doit comparer l'adresse logique avec la taille du segment. On peut alors faire le test de débordement avant ou pendant la relocation. Les deux peuvent être faits en parallèle, dans deux circuits distincts, ce qui améliore un peu le temps d'un accès mémoire. Quelques processeurs en ont profité, mais on verra cela dans la section sur la segmentation.
[[File:Comparaison entre adresse limite physique et logique.png|centre|vignette|upright=2|Comparaison entre adresse limite physique et logique]]
Les registres de base et limite sont altérés uniquement par le système d'exploitation et ne sont accessibles qu'en espace noyau. Lorsque le système d'exploitation charge un programme, ou reprend son exécution, il charge les adresses de début/fin du segment dans ces registres. D'ailleurs, ces deux registres doivent être sauvegardés et restaurés lors de chaque interruption. Par contre, et c'est assez évident, ils ne le sont pas lors d'un appel de fonction. Cela fait une différence de plus entre interruption et appels de fonctions.
: Il faut noter que le registre limite et le registre de base sont parfois fusionnés en un seul registre, qui contient un descripteur de segment tout entier.
Pour information, la relocation matérielle avec un registre limite a été implémentée sur plusieurs processeurs assez anciens, notamment sur les anciens supercalculateurs de marque CDC. Un exemple est le fameux CDC 6600, qui implémentait cette technique.
===La mémoire virtuelle avec la relocation matérielle===
Il est possible d'implémenter la mémoire virtuelle avec la relocation matérielle. Pour cela, il faut swapper des segments entiers sur le disque dur. Les segments sont placés en mémoire RAM et leur taille évolue au fur et à mesure que les programmes demandent du rab de mémoire RAM. Lorsque la mémoire est pleine, ou qu'un programme demande plus de mémoire que disponible, des segments entiers sont sauvegardés dans le ''swapfile'', pour faire de la place.
Faire ainsi de demande juste de mémoriser si un segment est en mémoire RAM ou non, ainsi que la position des segments swappés dans le ''swapfile''. Pour cela, il faut modifier la table des segments, afin d'ajouter un '''bit de swap''' qui précise si le segment en question est swappé ou non. Lorsque le système d'exploitation veut swapper un segment, il le copie dans le ''swapfile'' et met ce bit à 1. Lorsque l'OS recharge ce segment en RAM, il remet ce bit à 0. La gestion de la position des segments dans le ''swapfile'' est le fait d'une structure de données séparée de la table des segments.
L'OS exécute chaque programme l'un après l'autre, à tour de rôle. Lorsque le tour d'un programme arrive, il consulte la table des segments pour récupérer les adresses de base et limite, mais il vérifie aussi le bit de swap. Si le bit de swap est à 0, alors l'OS se contente de charger les adresses de base et limite dans les registres adéquats. Mais sinon, il démarre une routine d'interruption qui charge le segment voulu en RAM, depuis le ''swapfile''. C'est seulement une fois le segment chargé que l'on connait son adresse de base/limite et que le chargement des registres de relocation peut se faire.
Un défaut évident de cette méthode est que l'on swappe des programmes entiers, qui sont généralement assez imposants. Les segments font généralement plusieurs centaines de mébioctets, pour ne pas dire plusieurs gibioctets, à l'époque actuelle. Ils étaient plus petits dans l'ancien temps, mais la mémoire était alors plus lente. Toujours est-il que la copie sur le disque dur des segments est donc longue, lente, et pas vraiment compatible avec le fait que les programmes s'exécutent à tour de rôle. Et ca explique pourquoi la relocation matérielle n'est presque jamais utilisée avec de la mémoire virtuelle.
===L'extension d'adressage avec la relocation matérielle===
Passons maintenant à la dernière fonctionnalité implémentable avec la traduction d'adresse : l'extension d'adressage. Elle permet d'utiliser plus de mémoire que ne le permet l'espace d'adressage. Par exemple, utiliser plus de 64 kibioctets de mémoire sur un processeur 16 bits. Pour cela, les adresses envoyées à la mémoire doivent être plus longues que les adresses gérées par le processeur.
L'extension des adresses se fait assez simplement avec la relocation matérielle : il suffit que le registre de base soit plus long. Prenons l'exemple d'un processeur aux adresses de 16 bits, mais qui est reliée à un bus d'adresse de 24 bits. L'espace d'adressage fait juste 64 kibioctets, mais le bus d'adresse gère 16 mébioctets de RAM. On peut utiliser les 16 mébioctets de RAM à une condition : que le registre de base fasse 24 bits, pas 16.
Un défaut de cette approche est qu'un programme ne peut pas utiliser plus de mémoire que ce que permet l'espace d'adressage. Mais par contre, on peut placer chaque programme dans des portions différentes de mémoire. Imaginons par exemple que l'on ait un processeur 16 bits, mais un bus d'adresse de 20 bits. Il est alors possible de découper la mémoire en 16 blocs de 64 kibioctets, chacun attribué à un segment/programme, qu'on sélectionne avec les 4 bits de poids fort de l'adresse. Il suffit de faire démarrer les segments au bon endroit en RAM, et cela demande juste que le registre de base le permette. C'est une sorte d'émulation de la commutation de banques.
==La segmentation en mode réel des processeurs x86==
Avant de passer à la suite, nous allons voir la technique de segmentation de l'Intel 8086, un des tout premiers processeurs 16 bits. Il s'agissait d'une forme très simple de segmentation, sans aucune forme de protection mémoire, ni même de mémoire virtuelle, ce qui le place à part des autres formes de segmentation. Il s'agit d'une amélioration de la relocation matérielle, qui avait pour but de permettre d'utiliser plus de 64 kibioctets de mémoire, ce qui était la limite maximale sur les processeurs 16 bits de l'époque.
Par la suite, la segmentation s'améliora et ajouta un support complet de la mémoire virtuelle et de la protection mémoire. L'ancienne forme de segmentation fut alors appelé le '''mode réel''', et la nouvelle forme de segmentation fut appelée le '''mode protégé'''. Le mode protégé rajoute la protection mémoire, en ajoutant des registres limite et une gestion des droits d'accès aux segments, absents en mode réel. De plus, il ajoute un support de la mémoire virtuelle grâce à l'utilisation d'une des segments digne de ce nom, table qui est absente en mode réel ! Pour le moment, voyons le mode réel.
===Les segments en mode réel===
[[File:Typical computer data memory arrangement.png|vignette|upright=0.5|Typical computer data memory arrangement]]
La segmentation en mode réel sépare la pile, le tas, le code machine et les données constantes dans quatre segments distincts.
* Le segment '''''text''''', qui contient le code machine du programme, de taille fixe.
* Le segment '''''data''''' contient des données de taille fixe qui occupent de la mémoire de façon permanente, des constantes, des variables globales, etc.
* Le segment pour la '''pile''', de taille variable.
* le reste est appelé le '''tas''', de taille variable.
Un point important est que sur ces processeurs, il n'y a pas de table des segments proprement dit. Chaque programme gére de lui-même les adresses de base des segments qu'il manipule. Il n'est en rien aidé par une table des segments gérée par le système d'exploitation.
===Les registres de segments en mode réel===
Chaque segment subit la relocation indépendamment des autres. Pour cela, le processeur intégre plusieurs registres de base, un par segment. Notons que cette solution ne marche que si le nombre de segments par programme est limité, à une dizaine de segments tout au plus. Les processeurs x86 utilisaient cette méthode, et n'associaient que 4 à 6 registres de segments par programme.
Les processeurs 8086 et le 286 avaient quatre registres de segment : un pour le code, un autre pour les données, et un pour la pile, le quatrième étant un registre facultatif laissé à l'appréciation du programmeur. Ils sont nommés CS (''code segment''), DS (''data segment''), SS (''Stack segment''), et ES (''Extra segment''). Le 386 rajouta deux registres, les registres FS et GS, qui sont utilisés pour les segments de données. Les processeurs post-386 ont donc 6 registres de segment.
Les registres CS et SS sont adressés implicitement, en fonction de l'instruction exécutée. Les instructions de la pile manipulent le segment associé à la pile, le chargement des instructions se fait dans le segment de code, les instructions arithmétiques et logiques vont chercher leurs opérandes sur le tas, etc. Et donc, toutes les instructions sont chargées depuis le segment pointé par CS, les instructions de gestion de la pile (PUSH et POP) utilisent le segment pointé par SS.
Les segments DS et ES sont, eux aussi, adressés implicitement. Pour cela, les instructions LOAD/STORE sont dupliquées : il y a une instruction LOAD pour le segment DS, une autre pour le segment ES. D'autres instructions lisent leurs opérandes dans un segment par défaut, mais on peut changer ce choix par défaut en précisant le segment voulu. Un exemple est celui de l'instruction CMPSB, qui compare deux octets/bytes : le premier est chargé depuis le segment DS, le second depuis le segment ES.
Un autre exemple est celui de l'instruction MOV avec un opérande en mémoire. Elle lit l'opérande en mémoire depuis le segment DS par défaut. Il est possible de préciser le segment de destination si celui-ci n'est pas DS. Par exemple, l'instruction MOV [A], AX écrit le contenu du registre AX dans l'adresse A du segment DS. Par contre, l'instruction MOV ES:[A], copie le contenu du registre AX das l'adresse A, mais dans le segment ES.
===La traduction d'adresse en mode réel===
La segmentation en mode réel a pour seul but de permettre à un programme de dépasser la limite des 64 KB autorisée par les adresses de 16 bits. L'idée est que chaque segment a droit à son propre espace de 64 KB. On a ainsi 64 Kb pour le code machine, 64 KB pour la pile, 64 KB pour un segment de données, etc. Les registres de segment mémorisaient la base du segment, les adresses calculées par l'ALU étant des ''offsets''. Ce sont tous des registres de 16 bits, mais ils ne mémorisent pas des adresses physiques de 16 bits, comme nous allons le voir.
[[File:Table des segments dans un banc de registres.png|centre|vignette|upright=2|Table des segments dans un banc de registres.]]
L'Intel 8086 utilisait des adresses de 20 bits, ce qui permet d'adresser 1 mébioctet de RAM. Vous pouvez vous demander comment on peut obtenir des adresses de 20 bits alors que les registres de segments font tous 16 bits ? Cela tient à la manière dont sont calculées les adresses physiques. Le registre de segment n'est pas additionné tel quel avec le décalage : à la place, le registre de segment est décalé de 4 rangs vers la gauche. Le décalage de 4 rangs vers la gauche fait que chaque segment a une adresse qui est multiple de 16. Le fait que le décalage soit de 16 bits fait que les segments ont une taille de 64 kibioctets.
{|class="wikitable"
|-
| <code> </code><code style="background:#DED">0000 0110 1110 1111</code><code>0000</code>
| Registre de segment -
| 16 bits, décalé de 4 bits vers la gauche
|-
| <code>+ </code><code style="background:#DDF">0001 0010 0011 0100</code>
| Décalage/''Offset''
| 16 bits
|-
| colspan="3" |
|-
| <code> </code><code style="background:#FDF">0000 1000 0001 0010 0100</code>
| Adresse finale
| 20 bits
|}
Vous aurez peut-être remarqué que le calcul peut déborder, dépasser 20 bits. Mais nous reviendrons là-dessus plus bas. L'essentiel est que la MMU pour la segmentation en mode réel se résume à quelques registres et des additionneurs/soustracteurs.
Un exemple est l'Intel 8086, un des tout premier processeur Intel. Le processeur était découpé en deux portions : l'interface mémoire et le reste du processeur. L'interface mémoire est appelée la '''''Bus Interface Unit''''', et le reste du processeur est appelé l{{'}}'''''Execution Unit'''''. L'interface mémoire contenait les registres de segment, au nombre de 4, ainsi qu'un additionneur utilisé pour traduire les adresses logiques en adresses physiques. Elle contenait aussi une file d'attente où étaient préchargées les instructions.
Sur le 8086, la MMU est fusionnée avec les circuits de gestion du ''program counter''. Les registres de segment sont regroupés avec le ''program counter'' dans un même banc de registres, et un additionneur unique est utilisé à la fois à incrémenter le ''program counter'' et pour gérer la segmentation. L'additionneur est donc mutualisé entre segmentation et ''program counter''. En somme, il n'y a pas vraiment de MMU dédiée, mais un super-circuit en charge du Fetch et de la mémoire virtuelle, ainsi que du préchargement des instructions. Nous en reparlerons au chapitre suivant.
[[File:80186 arch.png|centre|vignette|upright=2.5|Architecture du 8086, du 80186 et de ses variantes.]]
La MMU du 286 était fusionnée avec l'unité de calcul d'adresse. Elle contient les registres de segments, un comparateur pour détecter les accès hors-segment, et plusieurs additionneurs. Il y a un additionneur pour les calculs d'adresse proprement dit, suivi d'un additionneur pour la relocation.
[[File:Intel i80286 arch.svg|centre|vignette|upright=3|Intel i80286 arch]]
===La segmentation en mode réel accepte plusieurs segments de code/données===
Les programmes peuvent parfaitement répartir leur code machine dans plusieurs segments de code. La limite de 64 KB par segment est en effet assez limitante, et il n'était pas rare qu'un programme stocke son code dans deux ou trois segments. Il en est de même avec les données, qui peuvent être réparties dans deux ou trois segments séparés. La seule exception est la pile : elle est forcément dans un segment unique et ne peut pas dépasser 64 KB.
Pour gérer plusieurs segments de code/donnée, il faut changer de segment à la volée suivant les besoins, en modifiant les registres de segment. Il s'agit de la technique de '''commutation de segment'''. Pour cela, tous les registres de segment, à l'exception de CS, peuvent être altérés par une instruction d'accès mémoire, soit avec une instruction MOV, soit en y copiant le sommet de la pile avec une instruction de dépilage POP. L'absence de sécurité fait que la gestion de ces registres est le fait du programmeur, qui doit redoubler de prudence pour ne pas faire n'importe quoi.
Pour le code machine, le répartir dans plusieurs segments posait des problèmes au niveau des branchements. Si la plupart des branchements sautaient vers une instruction dans le même segment, quelques rares branchements sautaient vers du code machine dans un autre segment. Intel avait prévu le coup et disposait de deux instructions de branchement différentes pour ces deux situations : les '''''near jumps''''' et les '''''far jumps'''''. Les premiers sont des branchements normaux, qui précisent juste l'adresse à laquelle brancher, qui correspond à la position de la fonction dans le segment. Les seconds branchent vers une instruction dans un autre segment, et doivent préciser deux choses : l'adresse de base du segment de destination, et la position de la destination dans le segment. Le branchement met à jour le registre CS avec l'adresse de base, avant de faire le branchement. Ces derniers étaient plus lents, car on n'avait pas à changer de segment et mettre à jour l'état du processeur.
Il y avait la même pour l'instruction d'appel de fonction, avec deux versions de cette instruction. La première version, le '''''near call''''' est un appel de fonction normal, la fonction appelée est dans le segment en cours. Avec la seconde version, le '''''far call''''', la fonction appelée est dans un segment différent. L'instruction a là aussi besoin de deux opérandes : l'adresse de base du segment de destination, et la position de la fonction dans le segment. Un ''far call'' met à jour le registre CS avec l'adresse de base, ce qui fait que les ''far call'' sont plus lents que les ''near call''. Il existe aussi la même chose, pour les instructions de retour de fonction, avec une instruction de retour de fonction normale et une instruction de retour qui renvoie vers un autre segment, qui sont respectivement appelées '''''near return''''' et '''''far return'''''. Là encore, il faut préciser l'adresse du segment de destination dans le second cas.
La même chose est possible pour les segments de données. Sauf que cette fois-ci, ce sont les pointeurs qui sont modifiés. pour rappel, les pointeurs sont, en programmation, des variables qui contiennent des adresses. Lors de la compilation, ces pointeurs sont placés soit dans un registre, soit dans les instructions (adressage absolu), ou autres. Ici, il existe deux types de pointeurs, appelés '''''near pointer''''' et '''''far pointer'''''. Vous l'avez deviné, les premiers sont utilisés pour localiser les données dans le segment en cours d'utilisation, alors que les seconds pointent vers une donnée dans un autre segment. Là encore, la différence est que le premier se contente de donner la position dans le segment, alors que les seconds rajoutent l'adresse de base du segment. Les premiers font 16 bits, alors que les seconds en font 32 : 16 bits pour l'adresse de base et 16 pour l{{'}}''offset''.
===L'occupation de l'espace d'adressage par les segments===
Nous venons de voir qu'un programme pouvait utiliser plus de 4-6 segments, avec la commutation de segment. Mais d'autres programmes faisaient l'inverse, à savoir qu'ils se débrouillaient avec seulement 1 ou 2 segments. Suivant le nombre de segments utilisés, la configuration des registres n'était pas la même. Les configurations possibles sont appelées des ''modèle mémoire'', et il y en a en tout 6. En voici la liste :
{| class="wikitable"
|-
! Modèle mémoire !! Configuration des segments !! Configuration des registres || Pointeurs utilisés || Branchements utilisés
|-
| Tiny* || Segment unique pour tout le programme || CS=DS=SS || ''near'' uniquement || ''near'' uniquement
|-
| Small || Segment de donnée séparé du segment de code, pile dans le segment de données || DS=SS || ''near'' uniquement || ''near'' uniquement
|-
| Medium || Plusieurs segments de code unique, un seul segment de données || CS, DS et SS sont différents || ''near'' et ''far'' || ''near'' uniquement
|-
| Compact || Segment de code unique, plusieurs segments de données || CS, DS et SS sont différents || ''near'' uniquement || ''near'' et ''far''
|-
| Large || Plusieurs segments de code, plusieurs segments de données || CS, DS et SS sont différents || ''near'' et ''far'' || ''near'' et ''far''
|}
Un programme est censé utiliser maximum 4-6 segments de 64 KB, ce qui permet d'adresser maximum 64 * 6 = 384 KB de RAM, soit bien moins que le mébioctet de mémoire théoriquement adressable. Mais ce défaut est en réalité contourné par la commutation de segment, qui permettait d'adresser la totalité de la RAM si besoin. Une second manière de contourner cette limite est que plusieurs processus peuvent s'exécuter sur un seul processeur, si l'OS le permet. Ce n'était pas le cas à l'époque du DOS, qui était un OS mono-programmé, mais c'était en théorie possible. La limite est de 6 segments par programme/processus, en exécuter plusieurs permet d'utiliser toute la mémoire disponible rapidement.
[[File:Overlapping realmode segments.svg|vignette|Segments qui se recouvrent en mode réel.]]
Vous remarquerez qu'avec des registres de segments de 16 bits, on peut gérer 65536 segments différents, chacun de 64 KB. Et 65 536 segments de 64 kibioctets, ça ne rentre pas dans le mébioctet de mémoire permis avec des adresses de 20 bits. La raison est que plusieurs couples segment+''offset'' pointent vers la même adresse. En tout, chaque adresse peut être adressée par 4096 couples segment+''offset'' différents.
L'avantage de cette méthode est que des segments peuvent se recouvrir, à savoir que la fin de l'un se situe dans le début de l'autre, comme illustré ci-contre. Cela permet en théorie de partager de la mémoire entre deux processus. Mais la technique est tout sauf pratique et est donc peu utilisée. Elle demande de placer minutieusement les segments en RAM, et les données à partager dans les segments. En pratique, les programmeurs et OS utilisent des segments qui ne se recouvrent pas et sont disjoints en RAM.
Le nombre maximal de segments disjoints se calcule en prenant la taille de la RAM, qu'on divise par la taille d'un segment. Le calcul donne : 1024 kibioctets / 64 kibioctets = 16 segments disjoints. Un autre calcul prend le nombre de segments divisé par le nombre d'adresses aliasées, ce qui donne 65536 / 4096 = 16. Seulement 16 segments, c'est peu. En comptant les segments utilisés par l'OS et ceux utilisés par le programme, la limite est vite atteinte si le programme utilise la commutation de segment.
===Le mode réel sur les 286 et plus : la ligne d'adresse A20===
Pour résumer, le registre de segment contient des adresses de 20 bits, dont les 4 bits de poids faible sont à 0. Et il se voit ajouter un ''offset'' de 16 bits. Intéressons-nous un peu à l'adresse maximale que l'on peut calculer avec ce système. Nous allons l'appeler l{{'}}'''adresse maximale de segmentation'''. Elle vaut :
{|class="wikitable"
|-
| <code> </code><code style="background:#DED">1111 1111 1111 1111</code><code>0000</code>
| Registre de segment -
| 16 bits, décalé de 4 bits vers la gauche
|-
| <code>+ </code><code style="background:#DDF">1111 1111 1111 1111</code>
| Décalage/''Offset''
| 16 bits
|-
| colspan="3" |
|-
| <code> </code><code style="background:#FDF">1 0000 1111 1111 1110 1111</code>
| Adresse finale
| 20 bits
|}
Le résultat n'est pas l'adresse maximale codée sur 20 bits, car l'addition déborde. Elle donne un résultat qui dépasse l'adresse maximale permis par les 20 bits, il y a un 21ème bit en plus. De plus, les 20 bits de poids faible ont une valeur bien précise. Ils donnent la différence entre l'adresse maximale permise sur 20 bit, et l'adresse maximale de segmentation. Les bits 1111 1111 1110 1111 traduits en binaire donnent 65 519; auxquels il faut ajouter l'adresse 1 0000 0000 0000 0000. En tout, cela fait 65 520 octets adressables en trop. En clair : on dépasse la limite du mébioctet de 65 520 octets. Le résultat est alors très différent selon que l'on parle des processeurs avant le 286 ou après.
Avant le 286, le bus d'adresse faisait exactement 20 bits. Les adresses calculées ne pouvaient pas dépasser 20 bits. L'addition générait donc un débordement d'entier, géré en arithmétique modulaire. En clair, les bits de poids fort au-delà du vingtième sont perdus. Le calcul de l'adresse débordait et retournait au début de la mémoire, sur les 65 520 premiers octets de la mémoire RAM.
[[File:IBM PC Memory areas.svg|vignette|IBM PC Memory Map, la ''High memory area'' est en jaune.]]
Le 80286 en mode réel gère des adresses de base de 24 bits, soit 4 bits de plus que le 8086. Le résultat est qu'il n'y a pas de débordement. Les bits de poids fort sont conservés, même au-delà du 20ème. En clair, la segmentation permettait de réellement adresser 65 530 octets au-delà de la limite de 1 mébioctet. La portion de mémoire adressable était appelé la '''''High memory area''''', qu'on va abrévier en HMA.
{| class="wikitable"
|+ Espace d'adressage du 286
|-
! Adresses en héxadécimal !! Zone de mémoire
|-
| 10 FFF0 à FF FFFF || Mémoire étendue, au-delà du premier mébioctet
|-
| 10 0000 à 10 FFEF || ''High Memory Area''
|-
| 0 à 0F FFFF || Mémoire adressable en mode réel
|}
En conséquence, les applications peuvent utiliser plus d'un mébioctet de RAM, mais au prix d'une rétrocompatibilité imparfaite. Quelques programmes DOS ne marchaient pus à cause de ça. D'autres fonctionnaient convenablement et pouvaient adresser les 65 520 octets en plus.
Pour résoudre ce problème, les carte mères ajoutaient un petit circuit relié au 21ème bit d'adresse, nommé A20 (pas d'erreur, les fils du bus d'adresse sont numérotés à partir de 0). Le circuit en question pouvait mettre à zéro le fil d'adresse, ou au contraire le laisser tranquille. En le forçant à 0, le calcul des adresses déborde comme dans le mode réel des 8086. Mais s'il ne le fait pas, la ''high memory area'' est adressable. Le circuit était une simple porte ET, qui combinait le 21ème bit d'adresse avec un '''signal de commande A20''' provenant d'ailleurs.
Le signal de commande A20 était géré par le contrôleur de clavier, qui était soudé à la carte mère. Le contrôleur en question ne gérait pas que le clavier, il pouvait aussi RESET le processeur, alors gérer le signal de commande A20 n'était pas si problématique. Quitte à avoir un microcontrôleur sur la carte mère, autant s'en servir au maximum... La gestion du bus d'adresse étaitdonc gérable au clavier. D'autres carte mères faisaient autrement et préféraient ajouter un interrupteur, pour activer ou non la mise à 0 du 21ème bit d'adresse.
: Il faut noter que le signal de commande A20 était mis à 1 en mode protégé, afin que le 21ème bit d'adresse soit activé.
Le 386 ajouta deux registres de segment, les registres FS et GS, ainsi que le '''mode ''virtual 8086'''''. Ce dernier permet d’exécuter des programmes en mode réel alors que le système d'exploitation s'exécute en mode protégé. C'est une technique de virtualisation matérielle qui permet d'émuler un 8086 sur un 386. L'avantage est que la compatibilité avec les programmes anciens écrits pour le 8086 est conservée, tout en profitant de la protection mémoire. Tous les processeurs x86 qui ont suivi supportent ce mode virtuel 8086.
==La segmentation avec une table des segments==
La '''segmentation avec une table des segments''' est apparue sur des processeurs assez anciens, le tout premier étant le Burrough 5000. Elle a ensuite été utilisée sur les processeurs x86 des PCs, à partir du 286 d'Intel. Elle est aujourd'hui abandonnée sur les jeux d'instruction x86. Tout comme la segmentation en mode réel, la segmentation attribue plusieurs segments par programmes ! Et cela a des répercutions sur la manière dont la traduction d'adresse est effectuée.
===Pourquoi plusieurs segments par programme ?===
L'utilité d'avoir plusieurs segments par programme n'est pas évidente, mais elle le devient quand on se plonge dans le passé. Dans le passé, les programmeurs devaient faire avec une quantité de mémoire limitée et il n'était pas rare que certains programmes utilisent plus de mémoire que disponible sur la machine. Mais les programmeurs concevaient leurs programmes en fonction.
[[File:Overlay Programming.svg|vignette|upright=1|Overlay Programming]]
L'idée était d'implémenter un système de mémoire virtuelle, mais émulé en logiciel, appelé l{{'}}'''''overlaying'''''. Le programme était découpé en plusieurs morceaux, appelés des ''overlays''. Les ''overlays'' les plus importants étaient en permanence en RAM, mais les autres étaient faisaient un va-et-vient entre RAM et disque dur. Ils étaient chargés en RAM lors de leur utilisation, puis sauvegardés sur le disque dur quand ils étaient inutilisés. Le va-et-vient des ''overlays'' entre RAM et disque dur était réalisé en logiciel, par le programme lui-même. Le matériel n'intervenait pas, comme c'est le cas avec la mémoire virtuelle.
Avec la segmentation, un programme peut utiliser la technique des ''overlays'', mais avec l'aide du matériel. Il suffit de mettre chaque ''overlay'' dans son propre segment, et laisser la segmentation faire. Les segments sont swappés en tout ou rien : on doit swapper tout un segment en entier. L'intérêt est que la gestion du ''swapping'' est grandement facilitée, vu que c'est le système d'exploitation qui s'occupe de swapper les segments sur le disque dur ou de charger des segments en RAM. Pas besoin pour le programmeur de coder quoique ce soit. Par contre, cela demande l'intervention du programmeur, qui doit découper le programme en segments/''overlays'' de lui-même. Sans cela, la segmentation n'est pas très utile.
L{{'}}''overlaying'' est une forme de '''segmentation à granularité grossière''', à savoir que le programme est découpé en segments de grande taille. L'usage classique est d'avoir un segment pour la pile, un autre pour le code exécutable, un autre pour le reste. Éventuellement, on peut découper les trois segments précédents en deux ou trois segments, rarement au-delà. Les segments sont alors peu nombreux, guère plus d'une dizaine par programme. D'où le terme de ''granularité grossière''.
La '''segmentation à granularité fine''' pousse le concept encore plus loin. Avec elle, il y a idéalement un segment par entité manipulée par le programme, un segment pour chaque structure de donnée et/ou chaque objet. Par exemple, un tableau aura son propre segment, ce qui est idéal pour détecter les accès hors tableau. Pour les listes chainées, chaque élément de la liste aura son propre segment. Et ainsi de suite, chaque variable agrégée (non-primitive), chaque structure de donnée, chaque objet, chaque instance d'une classe, a son propre segment. Diverses fonctionnalités supplémentaires peuvent être ajoutées, ce qui transforme le processeur en véritable processeur orienté objet, mais passons ces détails pour le moment.
Vu que les segments correspondent à des objets manipulés par le programme, on peut deviner que leur nombre évolue au cours du temps. En effet, les programmes modernes peuvent demander au système d'exploitation du rab de mémoire pour allouer une nouvelle structure de données. Avec la segmentation à granularité fine, cela demande d'allouer un nouveau segment à chaque nouvelle allocation mémoire, à chaque création d'une nouvelle structure de données ou d'un objet. De plus, les programmes peuvent libérer de la mémoire, en supprimant les structures de données ou objets dont ils n'ont plus besoin. Avec la segmentation à granularité fine, cela revient à détruire le segment alloué pour ces objets/structures de données. Le nombre de segments est donc dynamique, il change au cours de l'exécution du programme.
===Les tables de segments avec la segmentation===
La présence de plusieurs segments par programme a un impact sur la table des segments. Avec la relocation matérielle, elle conte nait un segment par programme. Chaque entrée, chaque ligne de la table des segment, mémorisait l'adresse de base, l'adresse limite, un bit de présence pour la mémoire virtuelle et des autorisations liées à la protection mémoire. Avec la segmentation, les choses sont plus compliquées, car il y a plusieurs segments par programme. Les entrées ne sont pas modifiées, mais elles sont organisées différemment.
Avec cette forme de segmentation, la table des segments doit respecter plusieurs contraintes. Premièrement, il y a plusieurs segments par programmes. Deuxièmement, le nombre de segments est variable : certains programmes se contenteront d'un seul segment, d'autres de dizaine, d'autres plusieurs centaines, etc. Il y a typiquement deux manières de faire : soit utiliser une table des segments uniques, utiliser une table des segment par programme.
Il est possible d'utiliser une table des segment unique qui mémorise tous les segments de tous les processus, système d'exploitation inclut. On parle alors de '''table des segment globale'''. Mais cette solution n'est pas utilisée avec la segmentation proprement dite. Elle est utilisée sur les architectures à capacité qu'on détaillera vers la fin du chapitre, dans une section dédiée. À la place, la segmentation utilise une table de segment par processus/programme, chacun ayant une '''table des segment locale'''.
Dans les faits, les choses sont plus compliquées. Le système d'exploitation doit savoir où se trouvent les tables de segment locale pour chaque programme. Pour cela, il a besoin d'utiliser une table de segment globale, dont chaque entrée pointe non pas vers un segment, mais vers une table de segment locale. Lorsque l'OS effectue une commutation de contexte, il lit la table des segment globale, pour récupérer un pointeur vers celle-ci. Ce pointeur est alors chargé dans un registre du processeur, qui mémorise l'adresse de la table locale, ce qui sert lors des accès mémoire.
Une telle organisation fait que les segments d'un processus/programme sont invisibles pour les autres, il y a une certaine forme de sécurité. Un programme ne connait que sa table de segments locale, il n'a pas accès directement à la table des segments globales. Tout accès mémoire se passera à travers la table de segment locale, il ne sait pas où se trouvent les autres tables de segment locales.
Les processeurs x86 sont dans ce cas : ils utilisent une table de segment globale couplée à autant de table des segments qu'il y a de processus en cours d'exécution. La table des segments globale s'appelle la '''''Global Descriptor Table''''' et elle peut contenir 8192 segments maximum, ce qui permet le support de 8192 processus différents. Les tables de segments locales sont appelées les '''''Local Descriptor Table''''' et elles font aussi 8192 segments maximum, ce qui fait 8192 segments par programme maximum. Il faut noter que la table de segment globale peut mémoriser des pointeurs vers les routines d'interruption, certaines données partagées (le tampon mémoire pour le clavier) et quelques autres choses, qui n'ont pas leur place dans les tables de segment locales.
===La relocation avec la segmentation===
La table des segments locale mémorise les adresses de base et limite de chaque segment, ainsi que d'autres méta-données. Les informations pour un segment sont regroupés dans un '''descripteur de segment''', qui est codé sur plusieurs octets, et qui regroupe : adresse de base, adresse limite, bit de présence en RAM, méta-données de protection mémoire.
La table des segments est un tableau dans lequel les descripteurs de segment sont placés les uns à la suite des autres en mémoire RAM. La table des segments est donc un tableau de segment. Les segments d'un programme sont numérotés, le nombre s'appelant un '''indice de segment''', appelé '''sélecteur de segment''' dans la terminologie Intel. L'indice de segment n'est autre que l'indice du segment dans ce tableau.
[[File:Global Descriptor table.png|centre|vignette|upright=2|Table des segments locale.]]
Il n'y a pas de registre de segment proprement dit, qui mémoriserait l'adresse de base. À la place, les segments sont adressés de manière indirecte. À la place, les registres de segment mémorisent des sélecteurs de segment. Ils sont utilisés pour lire l'adresse de base/limite dans la table de segment en mémoire RAM. Pour cela, un registre mémorise l'adresse de la table de segment locale, sa position en mémoire RAM.
Toute lecture ou écriture se fait en deux temps, en deux accès mémoire, consécutifs. Premièrement, le numéro de segment est utilisé pour adresser la table des segment. La lecture récupère alors un pointeur vers ce segment. Deuxièmement, ce pointeur est utilisé pour faire la lecture ou écriture. Plus précisément, la première lecture récupère un descripteur de segment qui contient l'adresse de base, le pointeur voulu, mais aussi l'adresse limite et d'autres informations.
[[File:Segmentation avec table des segments.png|centre|vignette|upright=2|Segmentation avec table des segments]]
L'accès à la table des segments se fait automatiquement à chaque accès mémoire. La conséquence est que chaque accès mémoire demande d'en faire deux : un pour lire la table des segments, l'autre pour l'accès lui-même. Il s'agit en quelque sorte d'une forme d'adressage indirect mémoire.
Un point important est que si le premier accès ne fait qu'une simple lecture dans un tableau, le second accès implique des calculs d'adresse. En effet, le premier accès récupère l'adresse de base du segment, mais le second accès sélectionne une donnée dans le segment, ce qui demande de calculer son adresse. L'adresse finale se déduit en combinant l'adresse de base avec un décalage (''offset'') qui donne la position de la donnée dans ce segment. L'indice de segment est utilisé pour récupérer l'adresse de base du segment. Une fois cette adresse de base connue, on lui additionne le décalage pour obtenir l'adresse finale.
[[File:Table des segments.png|centre|vignette|upright=2|Traduction d'adresse avec une table des segments.]]
Pour effectuer automatiquement l'accès à la table des segments, le processeur doit contenir un registre supplémentaire, qui contient l'adresse de la table de segment, afin de la localiser en mémoire RAM. Nous appellerons ce registre le '''pointeur de table'''. Le pointeur de table est combiné avec l'indice de segment pour adresser le descripteur de segment adéquat.
[[File:Segment 2.svg|centre|vignette|upright=2|Traduction d'adresse avec une table des segments, ici appelée table globale des de"scripteurs (terminologie des processeurs Intel x86).]]
Un point important est que la table des segments n'est pas accessible pour le programme en cours d'exécution. Il ne peut pas lire le contenu de la table des segments, et encore moins la modifier. L'accès se fait seulement de manière indirecte, en faisant usage des indices de segments, mais c'est un adressage indirect. Seul le système d'exploitation peut lire ou écrire la table des segments directement.
Plus haut, j'ai dit que tout accès mémoire impliquait deux accès mémoire : un pour charger le descripteur de segment, un autre pour la lecture/écriture proprement dite. Cependant, cela aurait un impact bien trop grand sur les performances. Dans les faits, les processeurs avec segmentations intégraient un '''cache de descripteurs de segments''', pour limiter la casse. Quand un descripteur de segment est lu depuis la RAM, il est copié dans ce cache. Les accès ultérieurs accédent au descripteur dans le cache, pas besoin de passer par la RAM. L'intel 386 avait un cache de ce type.
===La protection mémoire : les accès hors-segments===
Comme avec la relocation matérielle, le processeur détecte les débordements de segment. Pour cela, il compare l'adresse logique accédée avec l'adresse limite, ou compare la taille limite avec le décalage. De nombreux processeurs, comme l'Intel 386, préféraient utiliser la taille du segment, pour une question d'optimisation. En effet, si on compare l'adresse finale avec l'adresse limite, on doit faire la relocation avant de comparer l'adresse relocatée. Mais en utilisant la taille, ce n'est pas le cas : on peut faire la comparaison avant, pendant ou après la relocation.
Un détail à prendre en compte est la taille de la donnée accédée. Sans cela, la comparaison serait très simple : on vérifie si ''décalage <= taille du segment'', ou on compare des adresses de la même manière. Mais imaginez qu'on accède à une donnée de 4 octets : il se peut que l'adresse de ces 4 octets rentre dans le segment, mais que quelques octets débordent. Par exemple, les deux premiers octets sont dans le segment, mais pas les deux suivants. La vraie comparaison est alors : ''décalage + 4 octets <= taille du segment''.
Mais il est possible de faire le calcul autrement, et quelques processeurs comme l'Intel 386 ne s'en sont pas privé. Il calculait la différence ''taille du segment - décalage'', et vérifiait le résultat. Le processeur gérait des données de 1, 2 et 4 octets, ce qui fait que le résultat devait être entre 0 et 3. Le processeur prenait le résultat de la soustraction, et vérifiait alors que les 30 bits de poids fort valaient bien 0. Il vérifiait aussi que les deux bits de poids faible avaient la bonne valeur.
[[File:Vm7.svg|centre|vignette|upright=2|Traduction d'adresse avec vérification des accès hors-segment.]]
Une nouveauté fait son apparition avec la segmentation : la '''gestion des droits d'accès'''. Par exemple, il est possible d'interdire d'exécuter le contenu d'un segment, ce qui fournit une protection contre certaines failles de sécurité ou certains virus. Lorsqu'on exécute une opération interdite, le processeur lève une exception matérielle, à charge du système d'exploitation de gérer la situation.
Pour cela, chaque segment se voit attribuer un certain nombre d'autorisations d'accès qui indiquent si l'on peut lire ou écrire dedans, si celui-ci contient un programme exécutable, etc. Les autorisations pour chaque segment sont placées dans le descripteur de segment. Elles se résument généralement à quelques bits, qui indiquent si le segment est accesible en lecture/écriture ou exécutable. Le tout est souvent concaténé dans un ou deux '''octets de droits d'accès'''.
L'implémentation de la protection mémoire dépend du CPU considéré. Les CPU microcodés peuvent en théorie utiliser le microcode. Lorsqu'une instruction mémoire s'exécute, le microcode effectue trois étapes : lire le descripteur de segment, faire les tests de protection mémoire, exécuter la lecture/écriture ou lever une exception. Létape de test est réalisée avec un ou plusieurs micro-branchements. Par exemple, une écriture va tester le bit R/W du descripteur, qui indique si on peut écrire dans le segment, en utilisant un micro-branchement. Le micro-branchement enverra vers une routine du microcode en cas d'erreur.
Les tests de protection mémoire demandent cependant de tester beaucoup de conditions différentes. Par exemple, le CPU Intel 386 testait moins d'une dizaine de conditions pour certaines instructions. Il est cependant possible de faire plusieurs comparaisons en parallèle en rusant un peu. Il suffit de mémoriser les octets de droits d'accès dans un registre interne, de masquer les bits non-pertinents, et de faire une comparaison avec une constante adéquate, qui encode la valeur que doivent avoir ces bits.
Une solution alternative utiliser un circuit combinatoire pour faire les tests de protection mémoire. Les tests sont alors faits en parallèles, plutôt qu'un par un par des micro-branchements. Par contre, le cout en matériel est assez important. Il faut ajouter ce circuit combinatoire, ce qui demande pas mal de circuits.
===La mémoire virtuelle avec la segmentation===
La mémoire virtuelle est une fonctionnalité souvent implémentée sur les processeurs qui gèrent la segmentation, alors que les processeurs avec relocation matérielle s'en passaient. Il faut dire que l'implémentation de la mémoire virtuelle est beaucoup plus simple avec la segmentation, comparé à la relocation matérielle. Le remplacement des registres de base par des sélecteurs de segment facilite grandement l'implémentation.
Le problème de la mémoire virtuelle est que les segments peuvent être swappés sur le disque dur n'importe quand, sans que le programme soit prévu. Le swapping est réalisé par une interruption de l'OS, qui peut interrompre le programme n'importe quand. Et si un segment est swappé, le registre de base correspondant devient invalide, il point sur une adresse en RAM où le segment était, mais n'est plus. De plus, les segments peuvent être déplacés en mémoire, là encore n'importe quand et d'une manière invisible par le programme, ce qui fait que les registres de base adéquats doivent être modifiés.
Si le programme entier est swappé d'un coup, comme avec la relocation matérielle simple, cela ne pose pas de problèmes. Mais dès qu'on utilise plusieurs registres de base par programme, les choses deviennent soudainement plus compliquées. Le problème est qu'il n'y a pas de mécanismes pour choisir et invalider le registre de base adéquat quand un segment est déplacé/swappé. En théorie, on pourrait imaginer des systèmes qui résolvent le problème au niveau de l'OS, mais tous ont des problèmes qui font que l'implémentation est compliquée ou que les performances sont ridicules.
L'usage d'une table des segments accédée à chaque accès résout complètement le problème. La table des segments est accédée à chaque accès mémoire, elle sait si le segment est swappé ou non, chaque accès vérifie si le segment est en mémoire et quelle est son adresse de base. On peut changer le segment de place n'importe quand, le prochain accès récupérera des informations à jour dans la table des segments.
L'implémentation de la mémoire virtuelle avec la segmentation est simple : il suffit d'ajouter un bit dans les descripteurs de segments, qui indique si le segment est swappé ou non. Tout le reste, la gestion de ce bit, du swap, et tout ce qui est nécessaire, est délégué au système d'exploitation. Lors de chaque accès mémoire, le processeur vérifie ce bit avant de faire la traduction d'adresse, et déclenche une exception matérielle si le bit indique que le segment est swappé. L'exception matérielle est gérée par l'OS.
===Le partage de segments===
Il est possible de partager un segment entre plusieurs applications. Cela peut servir pour partager des données entre deux programmes : un segment de données partagées est alors partagé entre deux programmes. Partager un segment de code est utile pour les bibliothèques partagées : la bibliothèque est placée dans un segment dédié, qui est partagé entre les programmes qui l'utilisent. Partager un segment de code est aussi utile quand plusieurs instances d'une même application sont lancés simultanément : le code n'ayant pas de raison de changer, celui-ci est partagé entre toutes les instances. Mais ce n'est là qu'un exemple.
La première solution pour cela est de configurer les tables de segment convenablement. Le même segment peut avoir des droits d'accès différents selon les processus. Les adresses de base/limite sont identiques, mais les tables des segments ont alors des droits d'accès différents. Mais cette méthode de partage des segments a plusieurs défauts.
Premièrement, les sélecteurs de segments ne sont pas les mêmes d'un processus à l'autre, pour un même segment. Le segment partagé peut correspondre au segment numéro 80 dans le premier processus, au segment numéro 1092 dans le second processus. Rien n'impose que les sélecteurs de segment soient les mêmes d'un processus à l'autre, pour un segment identique.
Deuxièmement, les adresses limite et de base sont dupliquées dans plusieurs tables de segments. En soi, cette redondance est un souci mineur. Mais une autre conséquence est une question de sécurité : que se passe-t-il si jamais un processus a une table des segments corrompue ? Il se peut que pour un segment identique, deux processus n'aient pas la même adresse limite, ce qui peut causer des failles de sécurité. Un processus peut alors subir un débordement de tampon, ou tout autre forme d'attaque.
[[File:Vm9.png|centre|vignette|upright=2|Illustration du partage d'un segment entre deux applications.]]
Une seconde solution, complémentaire, utilise une table de segment globale, qui mémorise des segments partagés ou accessibles par tous les processus. Les défauts de la méthode précédente disparaissent avec cette technique : un segment est identifié par un sélecteur unique pour tous les processus, il n'y a pas de duplication des descripteurs de segment. Par contre, elle a plusieurs défauts.
Le défaut principal est que cette table des segments est accessible par tous les processus, impossible de ne partager ses segments qu'avec certains pas avec les autres. Un autre défaut est que les droits d'accès à un segment partagé sont identiques pour tous les processus. Impossible d'avoir un segment partagé accessible en lecture seule pour un processus, mais accessible en écriture pour un autre. Il est possible de corriger ces défauts, mais nous en parlerons dans la section sur les architectures à capacité.
===L'extension d'adresse avec la segmentation===
L'extension d'adresse est possible avec la segmentation, de la même manière qu'avec la relocation matérielle. Il suffit juste que les adresses de base soient aussi grandes que le bus d'adresse. Mais il y a une différence avec la relocation matérielle : un même programme peut utiliser plus de mémoire qu'il n'y en a dans l'espace d'adressage. La raison est simple : un segment peut prendre tout l'espace d'adressage, et il y a plusieurs segments par programme.
Pour donner un exemple, prenons un processeur 16 bits, qui peut adresser 64 kibioctets, associé à une mémoire de 4 mébioctets. Il est possible de placer le code machine dans les premiers 64k de la mémoire, la pile du programme dans les 64k suivants, le tas dans les 64k encore après, et ainsi de suite. Le programme dépasse donc les 64k de mémoire de l'espace d'adressage. Ce genre de chose est impossible avec la relocation, où un programme est limité par l'espace d'adressage.
===Le mode protégé des processeurs x86===
L'Intel 80286, aussi appelé 286, ajouta un mode de segmentation séparé du mode réel, qui ajoute une protection mémoire à la segmentation, ce qui lui vaut le nom de '''mode protégé'''. Dans ce mode, les registres de segment ne contiennent pas des adresses de base, mais des sélecteurs de segments qui sont utilisés pour l'accès à la table des segments en mémoire RAM.
Le 286 bootait en mode réel, puis le système d'exploitation devait faire quelques manipulations pour passer en mode protégé. Le 286 était pensé pour être rétrocompatible au maximum avec le 80186. Mais les différences entre le 286 et le 8086 étaient majeures, au point que les applications devaient être réécrites intégralement pour profiter du mode protégé. Un mode de compatibilité permettait cependant aux applications destinées au 8086 de fonctionner, avec même de meilleures performances. Aussi, le mode protégé resta inutilisé sur la plupart des applications exécutées sur le 286.
Vint ensuite le processeur 80386, renommé en 386 quelques années plus tard. Sur ce processeur, les modes réel et protégé sont conservés tel quel, à une différence près : toutes les adresses passent à 32 bits, qu'il s'agisse des adresses de base, limite ou des ''offsets''. Le processeur peut donc adresser un grand nombre de segments : 2^32, soit plus de 4 milliards. Les segments grandissent aussi et passent de 64 KB maximum à 4 gibioctets maximum. Mais surtout : le 386 ajouta le support de la pagination en plus de la segmentation. Ces modifications ont été conservées sur les processeurs 32 bits ultérieurs.
Les processeurs x86 gèrent deux types de tables des segments : une table locale pour chaque processus, et une table globale partagée entre tous les processus. Il ne peut y avoir qu'une table locale d'active, vu que le processeur ne peut exécuter qu'un seul processus en même temps. Chaque table locale définit 8192 segments, pareil pour la table globale. La table globale est utilisée pour les segments du noyau et la mémoire partagée entre processus. Un défaut est qu'un segment partagé par la table globale est visible par tous les processus, avec les mêmes droits d'accès. Ce qui fait que cette méthode était peu utilisée en pratique. La table globale mémorise aussi des pointeurs vers les tables locales, avec un descripteur de segment par table locale.
Sur les processeurs x86 32 bits, un descripteur de segment est organisé comme suit, pour les architectures 32 bits. On y trouve l'adresse de base et la taille limite, ainsi que de nombreux bits de contrôle.
Le premier groupe de bits de contrôle est l'octet en bleu à droite. Il contient :
* le bit P qui indique que l'entrée contient un descripteur valide, qu'elle n'est pas vide ;
* deux bits DPL qui indiquent le niveau de privilège du segment (noyau, utilisateur, les deux intermédiaires spécifiques au x86) ;
* un bit S qui précise si le segment est de type système (utiles pour l'OS) ou un segment de code/données.
* un champ Type qui contient les bits suivants :
** un bit E qui indique si le segment contient du code exécutable ou non ;
** le bit RW qui indique s'il est en lecture seule ou non ;;
** Un bit A qui indique que le segment a récemment été accédé, information utile pour l'OS;
** un bit DC assez spécifiques.
En haut à gauche, en bleu, on trouve deux bits :
* Le bit G indique comment interpréter la taille contenue dans le descripteur : 0 si la taille est exprimée en octets, 1 si la taille est un nombre de pages de 4 kibioctets. Ce bit précise si on utilise la segmentation seule, ou combinée avec la pagination.
* Le bit DB précise si l'on utilise des segments en mode de compatibilité 16 bits ou des segments 32 bits.
[[File:SegmentDescriptor.svg|centre|vignette|upright=3|Segment Descriptor]]
Les indices de segment sont appelés des sélecteurs de segment. Ils ont une taille de 16 bits, mais 3 bits sont utilisés pour encoder des méta-données. Le numéro de segment est donc codé sur 13 bits, ce qui permettait de gérer maximum 8192 segments par table de segment (locale ou globale). Les 16 bits sont organisés comme suit :
* 13 bits pour le numéro du segment dans la table des segments, l'indice de segment proprement dit ;
* un bit qui précise s'il faut accéder à la table des segments globale ou locale ;
* deux bits qui indiquent le niveau de privilège de l'accès au segment (les 4 niveaux de protection, dont l'espace noyau et utilisateur).
[[File:SegmentSelector.svg|centre|vignette|upright=1.5|Sélecteur de segment 16 bit.]]
En tout, l'indice permet de gérer 8192 segments pour la table locale et 8192 segments de la table globale.
====La MMU du 386/486 : cache de segment, protection mémoire====
La MMU du 386 et celle du 486 étaient assez similaires. Elles étaient plus complexes que celle du 186 et du 286. Elle contenait un additionneur pour les calculs d'adresse et un comparateur pour tester si l'accès mémoire déborde d'un segment. Le test de débordement se faisait en parallèle du calcul de l'adresse finale, comme sur le 286.
L'additionneur était un additionneur trois-opérandes, qui additionnait l'adresse à lire/écrire, l'adresse de base du segment, et un décalage intégré dans l'instruction. En clair, l'unité de segmentation n'était pas qu'une MMU, elle prenait en charge une partie du calcul d'adresse. L'avantage est que cela permettait de gérer les modes d'adressage "base + décalage" et "base + indice + décalage" très facilement, en utilisant un minimum de circuits. Une conséquence de cette organisation était que l'usage d'un décalage était gratuit. Par contre, dès qu'on utilisait l'adressage "Base + Indice", avec ou sans décalage, l'instruction prenait un cycle de plus à s'exécuter, parce qu'il fallait faire l'addition "Base + Indice" dans l'ALU entière.
Le CPU 386 était le premier à implémenter la protection mémoire avec des segments. Pour cela, il intégrait une '''''Protection Test Unit''''', séparée du microcode, qu'on va abrévier en PTU. Précisément, il s'agissait d'un PLA (''Programmable Logic Array''), une sorte d'intermédiaire entre circuit logique fait sur mesure et mémoire ROM, qu'on a déjà abordé dans le chapitre sur les mémoires ROM. Mais cette unité ne faisait pas tout, le microcode était aussi impliqué. La PTU sera détaillée dans la section suivante.
Pour améliorer les performances, le 386 et le 486 intégraient un '''cache de descripteurs de segment''', aussi appelé le cache de descripteurs. Lorsqu'un descripteur état chargé pour la première fois, il était copié dans le cache de descripteurs de segment. Les accès mémoire ultérieur lisaient le descripteur de segment depuis ce cache, pas depuis la table des segments en RAM. Le cache de descripteurs gère aussi bien les segments en mode réel qu'en mode protégé. Récupérer l'adresse de base depuis cache se fait un peu différemment en mode réel et protégé, mais le cache gère cela tout seul. Idem pour récupérer la taille/adresse limite.
[[File:Microarchitecture du 386, avec focus sur la segmentation.png|centre|vignette|upright=2|Microarchitecture du 386 et du 486, avec focus sur la segmentation.]]
En mode réel, la taille des segment est censée être limitée à 64 kibioctets. Mais le processeur ne vérifiait pas si cette limite était dépassée. À la place, il utilisait la limite précisée dans le cache de segments. Pire que ça : le cache de segment n'était pas réinitialisé quand on passe du mode réel au mode protégé, et réciproquement. Et cette propriété a été à l'origine de l''''''unreal mode'''''. Il s'agissait d'un mode réel amélioré, capable d'utiliser des segments de 4 gibioctets et des adresses de 32 bits.
Passer en mode ''unreal'' pouvait se faire de deux manières. Il était possible d'altérer le cache de descripteur en utilisant l'instruction non-documentée LOADALL. Elle permettait de charger les descripteurs de segments dans le cache de descripteurs, avec une taille arbitraire. Une autre solution, beaucoup plus complexe sur le 386, demandait d'entrer en mode protégé pour configurer des segments de grande taille, de charger leurs descripteurs dans le cache de descripteur, puis de revenir en mode réel. En mode réel, les descripteurs dans le cache étaient encore disponibles et on pouvait les lire dans le cache.
====L'implémentation de la protection mémoire sur le 386====
La protection mémoire teste la valeur des bits P, S, X, E, R/W. Elle teste aussi les niveaux de privilège, avec deux bits DPL et CPL. En tout, le processeur pouvait tester 148 conditions différentes en parallèle dans la PTU. Cependant, les niveaux de privilèges étaient pré-traités par le microcode. Le microcode vérifiait aussi s'il y avait une erreur en terme d’anneau mémoire, avec par "exemple un segment en mode noyau accédé alors que le CPU est en espace utilisateur. Il fournissait alors un résultat sur deux bits, qui indiquait s'il y avait une erreur ou non, que la PTU utilisait.
Mais toutes les conditions n'étaient pas pertinentes à un instant t. Par exemple, il est pertinent de vérifier si le bit R/W était cohérent si l'instruction à exécuter est une écriture. Mais il n'y a pas besoin de tester le bit E qui indique qu'un segment est exécutable ou non, pour une lecture. En tout, le processeur pouvait se retrouver dans 33 situations possibles, chacune demandant de tester un sous-ensemble des 148 conditions. Pour préciser quel sous-ensembles tester, la PTU recevait un code opération, généré par le microcode.
Pour faire les tests de protection mémoire, le microcode avait une micro-opération nommée ''protection test operation'', qui envoyait les droits d'accès à la PTU. Lors de l'exécution d'une ''protection test operation'', le PLA recevait un descripteur de segment, lu depuis la mémoire RAM, ainsi qu'un code opération provenant du microcode.
{|class="wikitable"
|+ Entrée de la ''Protection Test Unit''
|-
! 15 - 14 !! 13 - 12 !! 11 !! 10 !! 9 !! 8 !! 7 !! 6 !! 5-0
|-
| P1 , P2 || || P || S || X || E || R/W || A || Code opération
|-
| Niveaux de privilèges cohérents/erreur || || Segment présent en mémoire ou swappé || S || X || Segment exécutable ou non || Segment accesible en lecture/écriture || Segment récemment accédé || Code opération
|}
Il fournissait en sortie un bit qui indiquait si une erreur de protection mémoire avait eu lieu ou non. Il fournissait aussi une adresse de 12 bits, utilisée seulement en cas d'erruer. Elle pointait dans le microcode, sur un code levant une exception en cas d'erreur. Enfin, la PTU fournissait 4 bits pouvant être testés par un branchement dans le microcode. L'un d'entre eux demandait de tester s'il y a un accès hors-limite, les autres étaient assez peu reliés à la protection mémoire.
Un détail est que le chargement du descripteur de segment est réalisé par une fonction dans le microcode. Elle est appliquée pour toutes les instructions ou situations qui demandent de faire un accès mémoire. Et les tests de protection mémoire sont réalisés dans cette fonction, pas après elle. Vu qu'il s'agit d'une fonction exécutée quelque soit l'instruction, le microcode doit transférer le code opération à cette fonction. Le microcode est pour cela associé à un registre interne, dans lequel le code opération est mémorisé, avant d'appeler la fonction. Le microcode a une micro-opération PTSAV (''Protection Save'') pour mémoriser le code opération dans ce registre. Dans la fonction qui charge le descripteur, une micro-opération PTOVRR (''Protection Override'') lit le code opération dans ce registre, et lance les tests nécessaires.
Il faut noter que le PLA était certes plus rapide que de tester les conditions une par une, mais il était assez lent. La PTU mettait environ 3 cycles d'horloges pour rendre son résultat. Le microcode en profitait alors pour exécuter des micro-opérations durant ces 3 cycles d'attente. Par exemple, le microcode pouvait en profiter pour lire l'adresse de base dans le descripteur, si elle n'a pas été chargée avant (les descripteur était chargé en deux fois). Il fallait cependant que les trois micro-opérations soient valides, peu importe qu'il y ait une erreur de protection mémoire ou non. Ou du moins, elles produisaient un résultat qui n'est pas utilisé en cas d'erreur. Si ce n'était pas possible, le microcode ajoutait des NOP pendant ce temps d'attente de 3 cycles.
Le bit A du descripteur de segment indique que le segment a récemment été accédé. Il est mis à jour après les tests de protection mémoire, quand ceux-ci indiquent que l'accès mémoire est autorisé. Le bit A est mis à 1 si la PTU l'autorise. Pour cela, la PTU utilise un des 4 bits de sortie mentionnés plus haut : l'un d'entre eux indique que le bit A doit être mis à 1. La mise à jour est ensuite réalisée par le microcode, qui utilise trois micro-opérations pour le mettre à jour.
====Le ''Hardware task switching'' des CPU x86====
Les systèmes d’exploitation modernes peuvent lancer plusieurs logiciels en même temps. Les logiciels sont alors exécutés à tour de rôle. Passer d'un programme à un autre est ce qui s'appelle une commutation de contexte. Lors d'une commutation de contexte, l'état du processeur est sauvegardé, afin que le programme stoppé puisse reprendre là où il était. Il arrivera un moment où le programme stoppé redémarrera et il doit reprendre dans l'état exact où il s'est arrêté. Deuxièmement, le programme à qui c'est le tour restaure son état. Cela lui permet de revenir là où il était avant d'être stoppé. Il y a donc une sauvegarde et une restauration des registres.
Divers processeurs incorporent des optimisations matérielles pour rendre la commutation de contexte plus rapide. Ils peuvent sauvegarder et restaurer les registres du processeur automatiquement lors d'une interruption de commutation de contexte. Les registres sont sauvegardés dans des structures de données en mémoire RAM, appelées des '''contextes matériels'''. Sur les processeurs x86, il s'agit de la technique d{{'}}''Hardware Task Switching''. Fait intéressant, le ''Hardware Task Switching'' se base beaucoup sur les segments mémoires.
Avec ''Hardware Task Switching'', chaque contexte matériel est mémorisé dans son propre segment mémoire, séparé des autres. Les segments pour les contextes matériels sont appelés des '''''Task State Segment''''' (TSS). Un TSS mémorise tous les registres généraux, le registre d'état, les pointeurs de pile, le ''program counter'' et quelques registres de contrôle du processeur. Par contre, les registres flottants ne sont pas sauvegardés, de même que certaines registres dit SIMD que nous n'avons pas encore abordé. Et c'est un défaut qui fait que le ''Hardware Task Switching'' n'est plus utilisé.
Le programme en cours d'exécution connait l'adresse du TSS qui lui est attribué, car elle est mémorisée dans un registre appelé le '''''Task Register'''''. En plus de pointer sur le TSS, ce registre contient aussi les adresses de base et limite du segment en cours. Pour être plus précis, le ''Task Register'' ne mémorise pas vraiment l'adresse du TSS. À la place, elle mémorise le numéro du segment, le numéro du TSS. Le numéro est codé sur 16 bits, ce qui explique que 65 536 segments sont adressables. Les instructions LDR et STR permettent de lire/écrire ce numéro de segment dans le ''Task Register''.
Le démarrage d'un programme a lieu automatiquement dans plusieurs circonstances. La première est une instruction de branchement CALL ou JMP adéquate. Le branchement fournit non pas une adresse à laquelle brancher, mais un numéro de segment qui pointe vers un TSS. Cela permet à une routine du système d'exploitation de restaurer les registres et de démarrer le programme en une seule instruction de branchement. Une seconde circonstance est une interruption matérielle ou une exception, mais nous la mettons de côté. Le ''Task Register'' est alors initialisé avec le numéro de segment fournit. S'en suit la procédure suivante :
* Le ''Task Register'' est utilisé pour adresser la table des segments, pour récupérer un pointeur vers le TSS associé.
* Le pointeur est utilisé pour une seconde lecture, qui adresse le TSS directement. Celle-ci restaure les registres du processeur.
En clair, on va lire le ''TSS descriptor'' dans la GDT, puis on l'utilise pour restaurer les registres du processeur.
[[File:Hardware Task Switching x86.png|centre|vignette|upright=2|Hardware Task Switching x86]]
===La segmentation sur les processeurs Burrough B5000 et plus===
Le Burrough B5000 est un très vieil ordinateur, commercialisé à partir de l'année 1961. Ses successeurs reprennent globalement la même architecture. C'était une machine à pile, doublé d'une architecture taguée, choses très rare de nos jours. Mais ce qui va nous intéresser dans ce chapitre est que ce processeur incorporait la segmentation, avec cependant une différence de taille : un programme avait accès à un grand nombre de segments. La limite était de 1024 segments par programme ! Il va de soi que des segments plus petits favorise l'implémentation de la mémoire virtuelle, mais complexifie la relocation et le reste, comme nous allons le voir.
Le processeur gère deux types de segments : les segments de données et de procédure/fonction. Les premiers mémorisent un bloc de données, dont le contenu est laissé à l'appréciation du programmeur. Les seconds sont des segments qui contiennent chacun une procédure, une fonction. L'usage des segments est donc différent de ce qu'on a sur les processeurs x86, qui n'avaient qu'un segment unique pour l'intégralité du code machine. Un seul segment de code machine x86 est découpé en un grand nombre de segments de code sur les processeurs Burrough.
La table des segments contenait 1024 entrées de 48 bits chacune. Fait intéressant, chaque entrée de la table des segments pouvait mémoriser non seulement un descripteur de segment, mais aussi une valeur flottante ou d'autres types de données ! Parler de table des segments est donc quelque peu trompeur, car cette table ne gère pas que des segments, mais aussi des données. La documentation appelaiat cette table la '''''Program Reference Table''''', ou PRT.
La raison de ce choix quelque peu bizarre est que les instructions ne gèrent pas d'adresses proprement dit. Tous les accès mémoire à des données en-dehors de la pile passent par la segmentation, ils précisent tous un indice de segment et un ''offset''. Pour éviter d'allouer un segment pour chaque donnée, les concepteurs du processeur ont décidé qu'une entrée pouvait contenir directement la donnée entière à lire/écrire.
La PRT supporte trois types de segments/descripteurs : les descripteurs de données, les descripteurs de programme et les descripteurs d'entrées-sorties. Les premiers décrivent des segments de données. Les seconds sont associés aux segments de procédure/fonction et sont utilisés pour les appels de fonction (qui passent, eux aussi, par la segmentation). Le dernier type de descripteurs sert pour les appels systèmes et les communications avec l'OS ou les périphériques.
Chaque entrée de la PRT contient un ''tag'', une suite de bit qui indique le type de l'entrée : est-ce qu'elle contient un descripteur de segment, une donnée, autre. Les descripteurs contiennent aussi un ''bit de présence'' qui indique si le segment a été swappé ou non. Car oui, les segments pouvaient être swappés sur ce processeur, ce qui n'est pas étonnant vu que les segments sont plus petits sur cette architecture. Le descripteur contient aussi l'adresse de base du segment ainsi que sa taille, et diverses informations pour le retrouver sur le disque dur s'il est swappé.
: L'adresse mémorisée ne faisait que 15 bits, ce qui permettait d'adresse 32 kibi-mots, soit 192 kibioctets de mémoire. Diverses techniques d'extension d'adressage étaient disponibles pour contourner cette limitation. Outre l'usage de l{{'}}''overlay'', le processeur et l'OS géraient aussi des identifiants d'espace d'adressage et en fournissaient plusieurs par processus. Les processeurs Borrough suivants utilisaient des adresses plus grandes, de 20 bits, ce qui tempérait le problème.
[[File:B6700Word.jpg|centre|vignette|upright=2|Structure d'un mot mémoire sur le B6700.]]
==Les architectures à capacités==
Les architectures à capacité utilisent la segmentation à granularité fine, mais ajoutent des mécanismes de protection mémoire assez particuliers, qui font que les architectures à capacité se démarquent du reste. Les architectures de ce type sont très rares et sont des processeurs assez anciens. Le premier d'entre eux était le Plessey System 250, qui date de 1969. Il fu suivi par le CAP computer, vendu entre les années 70 et 77. En 1978, le System/38 d'IBM a eu un petit succès commercial. En 1980, la Flex machine a aussi été vendue, mais à très peu d'examplaires, comme les autres architectures à capacité. Et enfin, en 1981, l'architecture à capacité la plus connue, l'Intel iAPX 432 a été commercialisée. Depuis, la seule architecture de ce type est en cours de développement. Il s'agit de l'architecture CHERI, dont la mise en projet date de 2014.
===Le partage de la mémoire sur les architectures à capacités===
Le partage de segment est grandement modifié sur les architectures à capacité. Avec la segmentation normale, il y a une table de segment par processus. Les conséquences sont assez nombreuses, mais la principale est que partager un segment entre plusieurs processus est compliqué. Les défauts ont été évoqués plus haut. Les sélecteurs de segments ne sont pas les mêmes d'un processus à l'autre, pour un même segment. De plus, les adresses limite et de base sont dupliquées dans plusieurs tables de segments, et cela peut causer des problèmes de sécurité si une table des segments est modifiée et pas l'autre. Et il y a d'autres problèmes, tout aussi importants.
[[File:Partage des segments avec la segmentation.png|centre|vignette|upright=1.5|Partage des segments avec la segmentation]]
À l'opposé, les architectures à capacité utilisent une table des segments unique pour tous les processus. La table des segments unique sera appelée dans de ce qui suit la '''table des segments globale''', ou encore la table globale. En conséquence, les adresses de base et limite ne sont présentes qu'en un seul exemplaire par segment, au lieu d'être dupliquées dans autant de processus que nécessaire. De plus, cela garantit que l'indice de segment est le même quel que soit le processus qui l'utilise.
Un défaut de cette approche est au niveau des droits d'accès. Avec la segmentation normale, les droits d'accès pour un segment sont censés changer d'un processus à l'autre. Par exemple, tel processus a accès en lecture seule au segment, l'autre seulement en écriture, etc. Mais ici, avec une table des segments uniques, cela ne marche plus : incorporer les droits d'accès dans la table des segments ferait que tous les processus auraient les mêmes droits d'accès au segment. Et il faut trouver une solution.
===Les capacités sont des pointeurs protégés===
Pour éviter cela, les droits d'accès sont combinés avec les sélecteurs de segments. Les sélecteurs des segments sont remplacés par des '''capacités''', des pointeurs particuliers formés en concaténant l'indice de segment avec les droits d'accès à ce segment. Si un programme veut accéder à une adresse, il fournit une capacité de la forme "sélecteur:droits d'accès", et un décalage qui indique la position de l'adresse dans le segment.
Il est impossible d'accéder à un segment sans avoir la capacité associée, c'est là une sécurité importante. Un accès mémoire demande que l'on ait la capacité pour sélectionner le bon segment, mais aussi que les droits d'accès en permettent l'accès demandé. Par contre, les capacités peuvent être passées d'un programme à un autre sans problème, les deux programmes pourront accéder à un segment tant qu'ils disposent de la capacité associée.
[[File:Comparaison entre capacités et adresses segmentées.png|centre|vignette|upright=2.5|Comparaison entre capacités et adresses segmentées]]
Mais cette solution a deux problèmes très liés. Au niveau des sélecteurs de segment, le problème est que les sélecteur ont une portée globale. Avant, l'indice de segment était interne à un programme, un sélecteur ne permettait pas d'accéder au segment d'un autre programme. Sur les architectures à capacité, les sélecteurs ont une portée globale. Si un programme arrive à forger un sélecteur qui pointe vers un segment d'un autre programme, il peut théoriquement y accéder, à condition que les droits d'accès le permettent. Et c'est là qu'intervient le second problème : les droits d'accès ne sont plus protégés par l'espace noyau. Les droits d'accès étaient dans la table de segment, accessible uniquement en espace noyau, ce qui empêchait un processus de les modifier. Avec une capacité, il faut ajouter des mécanismes de protection qui empêchent un programme de modifier les droits d'accès à un segment et de générer un indice de segment non-prévu.
La première sécurité est qu'un programme ne peut pas créer une capacité, seul le système d'exploitation le peut. Les capacités sont forgées lors de l'allocation mémoire, ce qui est du ressort de l'OS. Pour rappel, un programme qui veut du rab de mémoire RAM peut demander au système d'exploitation de lui allouer de la mémoire supplémentaire. Le système d'exploitation renvoie alors un pointeurs qui pointe vers un nouveau segment. Le pointeur est une capacité. Il doit être impossible de forger une capacité, en-dehors d'une demande d'allocation mémoire effectuée par l'OS. Typiquement, la forge d'une capacité se fait avec des instructions du processeur, que seul l'OS peut éxecuter (pensez à une instruction qui n'est accessible qu'en espace noyau).
La seconde protection est que les capacités ne peuvent pas être modifiées sans raison valable, que ce soit pour l'indice de segment ou les droits d'accès. L'indice de segment ne peut pas être modifié, quelqu'en soit la raison. Pour les droits d'accès, la situation est plus compliquée. Il est possible de modifier ses droits d'accès, mais sous conditions. Réduire les droits d'accès d'une capacité est possible, que ce soit en espace noyau ou utilisateur, pas l'OS ou un programme utilisateur, avec une instruction dédiée. Mais augmenter les droits d'accès, seul l'OS peut le faire avec une instruction précise, souvent exécutable seulement en espace noyau.
Les capacités peuvent être copiées, et même transférées d'un processus à un autre. Les capacités peuvent être détruites, ce qui permet de libérer la mémoire utilisée par un segment. La copie d'une capacité est contrôlée par l'OS et ne peut se faire que sous conditions. La destruction d'une capacité est par contre possible par tous les processus. La destruction ne signifie pas que le segment est effacé, il est possible que d'autres processus utilisent encore des copies de la capacité, et donc le segment associé. On verra quand la mémoire est libérée plus bas.
Protéger les capacités demande plusieurs conditions. Premièrement, le processeur doit faire la distinction entre une capacité et une donnée. Deuxièmement, les capacités ne peuvent être modifiées que par des instructions spécifiques, dont l'exécution est protégée, réservée au noyau. En clair, il doit y avoir une séparation matérielle des capacités, qui sont placées dans des registres séparés. Pour cela, deux solutions sont possibles : soit les capacités remplacent les adresses et sont dispersées en mémoire, soit elles sont regroupées dans un segment protégé.
====La liste des capacités====
Avec la première solution, on regroupe les capacités dans un segment protégé. Chaque programme a accès à un certain nombre de segments et à autant de capacités. Les capacités d'un programme sont souvent regroupées dans une '''liste de capacités''', appelée la '''''C-list'''''. Elle est généralement placée en mémoire RAM. Elle est ce qu'il reste de la table des segments du processus, sauf que cette table ne contient pas les adresses du segment, qui sont dans la table globale. Tout se passe comme si la table des segments de chaque processus est donc scindée en deux : la table globale partagée entre tous les processus contient les informations sur les limites des segments, la ''C-list'' mémorise les droits d'accès et les sélecteurs pour identifier chaque segment. C'est un niveau d'indirection supplémentaire par rapport à la segmentation usuelle.
[[File:Architectures à capacité.png|centre|vignette|upright=2|Architectures à capacité]]
La liste de capacité est lisible par le programme, qui peut copier librement les capacités dans les registres. Par contre, la liste des capacités est protégée en écriture. Pour le programme, il est impossible de modifier les capacités dedans, impossible d'en rajouter, d'en forger, d'en retirer. De même, il ne peut pas accéder aux segments des autres programmes : il n'a pas les capacités pour adresser ces segments.
Pour protéger la ''C-list'' en écriture, la solution la plus utilisée consiste à placer la ''C-list'' dans un segment dédié. Le processeur gère donc plusieurs types de segments : les segments de capacité pour les ''C-list'', les autres types segments pour le reste. Un défaut de cette approche est que les adresses/capacités sont séparées des données. Or, les programmeurs mixent souvent adresses et données, notamment quand ils doivent manipuler des structures de données comme des listes chainées, des arbres, des graphes, etc.
L'usage d'une ''C-list'' permet de se passer de la séparation entre espace noyau et utilisateur ! Les segments de capacité sont eux-mêmes adressés par leur propre capacité, avec une capacité par segment de capacité. Le programme a accès à la liste de capacité, comme l'OS, mais leurs droits d'accès ne sont pas les mêmes. Le programme a une capacité vers la ''C-list'' qui n'autorise pas l'écriture, l'OS a une autre capacité qui accepte l'écriture. Les programmes ne pourront pas forger les capacités permettant de modifier les segments de capacité. Une méthode alternative est de ne permettre l'accès aux segments de capacité qu'en espace noyau, mais elle est redondante avec la méthode précédente et moins puissante.
====Les capacités dispersées, les architectures taguées====
Une solution alternative laisse les capacités dispersées en mémoire. Les capacités remplacent les adresses/pointeurs, et elles se trouvent aux mêmes endroits : sur la pile, dans le tas. Comme c'est le cas dans les programmes modernes, chaque allocation mémoire renvoie une capacité, que le programme gére comme il veut. Il peut les mettre dans des structures de données, les placer sur la pile, dans des variables en mémoire, etc. Mais il faut alors distinguer si un mot mémoire contient une capacité ou une autre donnée, les deux ne devant pas être mixés.
Pour cela, chaque mot mémoire se voit attribuer un certain bit qui indique s'il s'agit d'un pointeur/capacité ou d'autre chose. Mais cela demande un support matériel, ce qui fait que le processeur devient ce qu'on appelle une ''architecture à tags'', ou ''tagged architectures''. Ici, elles indiquent si le mot mémoire contient une adresse:capacité ou une donnée.
[[File:Architectures à capacité sans liste de capacité.png|centre|vignette|upright=2|Architectures à capacité sans liste de capacité]]
L'inconvénient est le cout en matériel de cette solution. Il faut ajouter un bit à chaque case mémoire, le processeur doit vérifier les tags avant chaque opération d'accès mémoire, etc. De plus, tous les mots mémoire ont la même taille, ce qui force les capacités à avoir la même taille qu'un entier. Ce qui est compliqué.
===Les registres de capacité===
Les architectures à capacité disposent de registres spécialisés pour les capacités, séparés pour les entiers. La raison principale est une question de sécurité, mais aussi une solution pragmatique au fait que capacités et entiers n'ont pas la même taille. Les registres dédiés aux capacités ne mémorisent pas toujours des capacités proprement dites. À la place, ils mémorisent des descripteurs de segment, qui contiennent l'adresse de base, limite et les droits d'accès. Ils sont utilisés pour la relocation des accès mémoire ultérieurs. Ils sont en réalité identiques aux registres de relocation, voire aux registres de segments. Leur utilité est d'accélérer la relocation, entre autres.
Les processeurs à capacité ne gèrent pas d'adresses proprement dit, comme pour la segmentation avec plusieurs registres de relocation. Les accès mémoire doivent préciser deux choses : à quel segment on veut accéder, à quelle position dans le segment se trouve la donnée accédée. La première information se trouve dans le mal nommé "registre de capacité", la seconde information est fournie par l'instruction d'accès mémoire soit dans un registre (Base+Index), soit en adressage base+''offset''.
Les registres de capacités sont accessibles à travers des instructions spécialisées. Le processeur ajoute des instructions LOAD/STORE pour les échanges entre table des segments et registres de capacité. Ces instructions sont disponibles en espace utilisateur, pas seulement en espace noyau. Lors du chargement d'une capacité dans ces registres, le processeur vérifie que la capacité chargée est valide, et que les droits d'accès sont corrects. Puis, il accède à la table des segments, récupère les adresses de base et limite, et les mémorise dans le registre de capacité. Les droits d'accès et d'autres méta-données sont aussi mémorisées dans le registre de capacité. En somme, l'instruction de chargement prend une capacité et charge un descripteur de segment dans le registre.
Avec ce genre de mécanismes, il devient difficile d’exécuter certains types d'attaques, ce qui est un gage de sureté de fonctionnement indéniable. Du moins, c'est la théorie, car tout repose sur l'intégrité des listes de capacité. Si on peut modifier celles-ci, alors il devient facile de pouvoir accéder à des objets auxquels on n’aurait pas eu droit.
===Le recyclage de mémoire matériel===
Les architectures à capacité séparent les adresses/capacités des nombres entiers. Et cela facilite grandement l'implémentation de la ''garbage collection'', ou '''recyclage de la mémoire''', à savoir un ensemble de techniques logicielles qui visent à libérer la mémoire inutilisée.
Rappelons que les programmes peuvent demander à l'OS un rab de mémoire pour y placer quelque chose, généralement une structure de donnée ou un objet. Mais il arrive un moment où cet objet n'est plus utilisé par le programme. Il peut alors demander à l'OS de libérer la portion de mémoire réservée. Sur les architectures à capacité, cela revient à libérer un segment, devenu inutile. La mémoire utilisée par ce segment est alors considérée comme libre, et peut être utilisée pour autre chose. Mais il arrive que les programmes ne libèrent pas le segment en question. Soit parce que le programmeur a mal codé son programme, soit parce que le compilateur n'a pas fait du bon travail ou pour d'autres raisons.
Pour éviter cela, les langages de programmation actuels incorporent des '''''garbage collectors''''', des morceaux de code qui scannent la mémoire et détectent les segments inutiles. Pour cela, ils doivent identifier les adresses manipulées par le programme. Si une adresse pointe vers un objet, alors celui-ci est accessible, il sera potentiellement utilisé dans le futur. Mais si aucune adresse ne pointe vers l'objet, alors il est inaccessible et ne sera plus jamais utilisé dans le futur. On peut libérer les objets inaccessibles.
Identifier les adresses est cependant très compliqué sur les architectures normales. Sur les processeurs modernes, les ''garbage collectors'' scannent la pile à la recherche des adresses, et considèrent tout mot mémoire comme une adresse potentielle. Mais les architectures à capacité rendent le recyclage de la mémoire très facile. Un segment est accessible si le programme dispose d'une capacité qui pointe vers ce segment, rien de plus. Et les capacités sont facilement identifiables : soit elles sont dans la liste des capacités, soit on peut les identifier à partir de leur ''tag''.
Le recyclage de mémoire était parfois implémenté directement en matériel. En soi, son implémentation est assez simple, et peu être réalisé dans le microcode d'un processeur. Une autre solution consiste à utiliser un second processeur, spécialement dédié au recyclage de mémoire, qui exécute un programme spécialement codé pour. Le programme en question est placé dans une mémoire ROM, reliée directement à ce second processeur.
===L'intel iAPX 432===
Voyons maintenat une architecture à capacité assez connue : l'Intel iAPX 432. Oui, vous avez bien lu : Intel a bel et bien réalisé un processeur orienté objet dans sa jeunesse. La conception du processeur Intel iAPX 432 commença en 1975, afin de créer un successeur digne de ce nom aux processeurs 8008 et 8080.
La conception du processeur Intel iAPX 432 commença en 1975, afin de créer un successeur digne de ce nom aux processeurs 8008 et 8080. Ce processeur s'est très faiblement vendu en raison de ses performances assez désastreuses et de défauts techniques certains. Par exemple, ce processeur était une machine à pile à une époque où celles-ci étaient tombées en désuétude, il ne pouvait pas effectuer directement de calculs avec des constantes entières autres que 0 et 1, ses instructions avaient un alignement bizarre (elles étaient bit-alignées). Il avait été conçu pour maximiser la compatibilité avec le langage ADA, un langage assez peu utilisé, sans compter que le compilateur pour ce processeur était mauvais.
====Les segments prédéfinis de l'Intel iAPX 432====
L'Intel iAPX432 gère plusieurs types de segments. Rien d'étonnant à cela, les Burrough géraient eux aussi plusieurs types de segments, à savoir des segments de programmes, des segments de données, et des segments d'I/O. C'est la même chose sur l'Intel iAPX 432, mais en bien pire !
Les segments de données sont des segments génériques, dans lequels on peut mettre ce qu'on veut, suivant les besoins du programmeur. Ils sont tous découpés en deux parties de tailles égales : une partie contenant les données de l'objet et une partie pour les capacités. Les capacités d'un segment pointent vers d'autres segments, ce qui permet de créer des structures de données assez complexes. La ligne de démarcation peut être placée n'importe où dans le segment, les deux portions ne sont pas de taille identique, elles ont des tailles qui varient de segment en segment. Il est même possible de réserver le segment entier à des données sans y mettre de capacités, ou inversement. Les capacités et données sont adressées à partir de la ligne de démarcation, qui sert d'adresse de base du segment. Suivant l'instruction utilisée, le processeur accède à la bonne portion du segment.
Le processeur supporte aussi d'autres segments pré-définis, qui sont surtout utilisés par le système d'exploitation :
* Des segments d'instructions, qui contiennent du code exécutable, typiquement un programme ou des fonctions, parfois des ''threads''.
* Des segments de processus, qui mémorisent des processus entiers. Ces segments contiennent des capacités qui pointent vers d'autres segments, notamment un ou plusieurs segments de code, et des segments de données.
* Des segments de domaine, pour les modules ou bibliothèques dynamiques.
* Des segments de contexte, utilisés pour mémoriser l'état d'un processus, utilisés par l'OS pour faire de la commutation de contexte.
* Des segments de message, utilisés pour la communication entre processus par l'intermédiaire de messages.
* Et bien d'autres encores.
Sur l'Intel iAPX 432, chaque processus est considéré comme un objet à part entière, qui a son propre segment de processus. De même, l'état du processeur (le programme qu'il est en train d’exécuter, son état, etc.) est stocké en mémoire dans un segment de contexte. Il en est de même pour chaque fonction présente en mémoire : elle était encapsulée dans un segment, sur lequel seules quelques manipulations étaient possibles (l’exécuter, notamment). Et ne parlons pas des appels de fonctions qui stockaient l'état de l'appelé directement dans un objet spécial. Bref, de nombreux objets système sont prédéfinis par le processeur : les objets stockant des fonctions, les objets stockant des processus, etc.
L'Intel 432 possédait dans ses circuits un ''garbage collector'' matériel. Pour faciliter son fonctionnement, certains bits de l'objet permettaient de savoir si l'objet en question pouvait être supprimé ou non.
====Le support de la segmentation sur l'Intel iAPX 432====
La table des segments est une table hiérarchique, à deux niveaux. Le premier niveau est une ''Object Table Directory'', qui réside toujours en mémoire RAM. Elle contient des descripteurs qui pointent vers des tables secondaires, appelées des ''Object Table''. Il y a plusieurs ''Object Table'', typiquement une par processus. Plusieurs processus peuvent partager la même ''Object Table''. Les ''Object Table'' peuvent être swappées, mais pas l{{'}}''Object Table Directory''.
Une capacité tient compte de l'organisation hiérarchique de la table des segments. Elle contient un indice qui précise quelle ''Object Table'' utiliser, et l'indice du segment dans cette ''Object Table''. Le premier indice adresse l{{'}}''Object Table Directory'' et récupère un descripteur de segment qui pointe sur la bonne ''Object Table''. Le second indice est alors utilisé pour lire l'adresse de base adéquate dans cette ''Object Table''. La capacité contient aussi des droits d'accès en lecture, écriture, suppression et copie. Il y a aussi un champ pour le type, qu'on verra plus bas. Au fait : les capacités étaient appelées des ''Access Descriptors'' dans la documentation officielle.
Une capacité fait 32 bits, avec un octet utilisé pour les droits d'accès, laissant 24 bits pour adresser les segments. Le processeur gérait jusqu'à 2^24 segments/objets différents, pouvant mesurer jusqu'à 64 kibioctets chacun, ce qui fait 2^40 adresses différentes, soit 1024 gibioctets. Les 24 bits pour adresser les segments sont partagés moitié-moitié pour l'adressage des tables, ce qui fait 4096 ''Object Table'' différentes dans l{{'}}''Object Table Directory'', et chaque ''Object Table'' contient 4096 segments.
====Le jeu d'instruction de l'Intel iAPX 432====
L'Intel iAPX 432 est une machine à pile. Le jeu d'instruction de l'Intel iAPX 432 gère pas moins de 230 instructions différentes. Il gére deux types d'instructions : les instructions normales, et celles qui manipulent des segments/objets. Les premières permettent de manipuler des nombres entiers, des caractères, des chaînes de caractères, des tableaux, etc.
Les secondes sont spécialement dédiées à la manipulation des capacités. Il y a une instruction pour copier une capacité, une autre pour invalider une capacité, une autre pour augmenter ses droits d'accès (instruction sécurisée, exécutable seulement sous certaines conditions), une autre pour restreindre ses droits d'accès. deux autres instructions créent un segment et renvoient la capacité associée, la première créant un segment typé, l'autre non.
le processeur gérait aussi des instructions spécialement dédiées à la programmation système et idéales pour programmer des systèmes d'exploitation. De nombreuses instructions permettaient ainsi de commuter des processus, faire des transferts de messages entre processus, etc. Environ 40 % du micro-code était ainsi spécialement dédié à ces instructions spéciales.
Les instructions sont de longueur variable et peuvent prendre n'importe quelle taille comprise entre 10 et 300 bits, sans vraiment de restriction de taille. Les bits d'une instruction sont regroupés en 4 grands blocs, 4 champs, qui ont chacun une signification particulière.
* Le premier est l'opcode de l'instruction.
* Le champ référence, doit être interprété différemment suivant la donnée à manipuler. Si cette donnée est un entier, un caractère ou un flottant, ce champ indique l'emplacement de la donnée en mémoire. Alors que si l'instruction manipule un objet, ce champ spécifie la capacité de l'objet en question. Ce champ est assez complexe et il est sacrément bien organisé.
* Le champ format, n'utilise que 4 bits et a pour but de préciser si les données à manipuler sont en mémoire ou sur la pile.
* Le champ classe permet de dire combien de données différentes l'instruction va devoir manipuler, et quelles seront leurs tailles.
[[File:Encodage des instructions de l'Intel iAPX-432.png|centre|vignette|upright=2|Encodage des instructions de l'Intel iAPX-432.]]
====Le support de l'orienté objet sur l'Intel iAPX 432====
L'Intel 432 permet de définir des objets, qui correspondent aux classes des langages orientés objets. L'Intel 432 permet, à partir de fonctions définies par le programmeur, de créer des '''''domain objects''''', qui correspondent à une classe. Un ''domain object'' est un segment de capacité, dont les capacités pointent vers des fonctions ou un/plusieurs objets. Les fonctions et les objets sont chacun placés dans un segment. Une partie des fonctions/objets sont publics, ce qui signifie qu'ils sont accessibles en lecture par l'extérieur. Les autres sont privées, inaccessibles aussi bien en lecture qu'en écriture.
L'exécution d'une fonction demande que le branchement fournisse deux choses : une capacité vers le ''domain object'', et la position de la fonction à exécuter dans le segment. La position permet de localiser la capacité de la fonction à exécuter. En clair, on accède au ''domain object'' d'abord, pour récupérer la capacité qui pointe vers la fonction à exécuter.
Il est aussi possible pour le programmeur de définir de nouveaux types non supportés par le processeur, en faisant appel au système d'exploitation de l'ordinateur. Au niveau du processeur, chaque objet est typé au niveau de son object descriptor : celui-ci contient des informations qui permettent de déterminer le type de l'objet. Chaque type se voit attribuer un domain object qui contient toutes les fonctions capables de manipuler les objets de ce type et que l'on appelle le type manager. Lorsque l'on veut manipuler un objet d'un certain type, il suffit d'accéder à une capacité spéciale (le TCO) qui pointera dans ce type manager et qui précisera quel est l'objet à manipuler (en sélectionnant la bonne entrée dans la liste de capacité). Le type d'un objet prédéfini par le processeur est ainsi spécifié par une suite de 8 bits, tandis que le type d'un objet défini par le programmeur est défini par la capacité spéciale pointant vers son type manager.
===Conclusion===
Pour ceux qui veulent en savoir plus, je conseille la lecture de ce livre, disponible gratuitement sur internet (merci à l'auteur pour cette mise à disposition) :
* [https://homes.cs.washington.edu/~levy/capabook/ Capability-Based Computer Systems].
Voici un document qui décrit le fonctionnement de l'Intel iAPX432 :
* [https://homes.cs.washington.edu/~levy/capabook/Chapter9.pdf The Intel iAPX 432 ]
==La pagination==
Avec la pagination, la mémoire est découpée en blocs de taille fixe, appelés des '''pages mémoires'''. La taille des pages varie suivant le processeur et le système d'exploitation et tourne souvent autour de 4 kibioctets. Mais elles sont de taille fixe : on ne peut pas en changer la taille. C'est la différence avec les segments, qui sont de taille variable. Le contenu d'une page en mémoire fictive est rigoureusement le même que le contenu de la page correspondante en mémoire physique.
L'espace d'adressage est découpé en '''pages logiques''', alors que la mémoire physique est découpée en '''pages physique''' de même taille. Les pages logiques correspondent soit à une page physique, soit à une page swappée sur le disque dur. Quand une page logique est associée à une page physique, les deux ont le même contenu, mais pas les mêmes adresses. Les pages logiques sont numérotées, en partant de 0, afin de pouvoir les identifier/sélectionner. Même chose pour les pages physiques, qui sont elles aussi numérotées en partant de 0.
[[File:Principe de la pagination.png|centre|vignette|upright=2|Principe de la pagination.]]
Pour information, le tout premier processeur avec un système de mémoire virtuelle était le super-ordinateur Atlas. Il utilisait la pagination, et non la segmentation. Mais il fallu du temps avant que la méthode de la pagination prenne son essor dans les processeurs commerciaux x86.
Un point important est que la pagination implique une coopération entre OS et hardware, les deux étant fortement mélés. Une partie des informations de cette section auraient tout autant leur place dans le wikilivre sur les systèmes d'exploitation, mais il est plus simple d'en parler ici.
===La mémoire virtuelle : le ''swapping'' et le remplacement des pages mémoires===
Le système d'exploitation mémorise des informations sur toutes les pages existantes dans une '''table des pages'''. C'est un tableau où chaque ligne est associée à une page logique. Une ligne contient un bit ''Valid'' qui indique si la page logique associée est swappée sur le disque dur ou non, et la position de la page physique correspondante en mémoire RAM. Elle peut aussi contenir des bits pour la protection mémoire, et bien d'autres. Les lignes sont aussi appelées des ''entrées de la table des pages''
[[File:Gestionnaire de mémoire virtuelle - Pagination et swapping.png|centre|vignette|upright=2|Table des pages.]]
De plus, le système d'exploitation conserve une '''liste des pages vides'''. Le nom est assez clair : c'est une liste de toutes les pages de la mémoire physique qui sont inutilisées, qui ne sont allouées à aucun processus. Ces pages sont de la mémoire libre, utilisable à volonté. La liste des pages vides est mise à jour à chaque fois qu'un programme réserve de la mémoire, des pages sont alors prises dans cette liste et sont allouées au programme demandeur.
====Les défauts de page====
Lorsque l'on veut traduire l'adresse logique d'une page mémoire, le processeur vérifie le bit ''Valid'' et l'adresse physique. Si le bit ''Valid'' est à 1 et que l'adresse physique est présente, la traduction d'adresse s'effectue normalement. Mais si ce n'est pas le cas, l'entrée de la table des pages ne contient pas de quoi faire la traduction d'adresse. Soit parce que la page est swappée sur le disque dur et qu'il faut la copier en RAM, soit parce que les droits d'accès ne le permettent pas, soit parce que la page n'a pas encore été allouée, etc. On fait alors face à un '''défaut de page'''. Un défaut de page a lieu quand la MMU ne peut pas associer l'adresse logique à une adresse physique, quelque qu'en soit la raison.
Il existe deux types de défauts de page : mineurs et majeurs. Un '''défaut de page majeur''' a lieu quand on veut accéder à une page déplacée sur le disque dur. Un défaut de page majeur lève une exception matérielle dont la routine rapatriera la page en mémoire RAM. S'il y a de la place en mémoire RAM, il suffit d'allouer une page vide et d'y copier la page chargée depuis le disque dur. Mais si ce n'est par le cas, on va devoir faire de la place en RAM en déplaçant une page mémoire de la RAM vers le disque dur. Dans tous les cas, c'est le système d'exploitation qui s'occupe du chargement de la page, le processeur n'est pas impliqué. Une fois la page chargée, la table des pages est mise à jour et la traduction d'adresse peut recommencer. Si je dis recommencer, c'est car l'accès mémoire initial est rejoué à l'identique, sauf que la traduction d'adresse réussit cette fois-ci.
Un '''défaut de page mineur''' a lieu dans des circonstances pas très intuitives : la page est en mémoire physique, mais l'adresse physique de la page n'est pas accessible. Par exemple, il est possible que des sécurités empêchent de faire la traduction d'adresse, pour des raisons de protection mémoire. Une autre raison est la gestion des adresses synonymes, qui surviennent quand on utilise des libraires partagées entre programmes, de la communication inter-processus, des optimisations de type ''copy-on-write'', etc. Enfin, une dernière raison est que la page a été allouée à un programme par le système d'exploitation, mais qu'il n'a pas encore attribué sa position en mémoire. Pour comprendre comment c'est possible, parlons rapidement de l'allocation paresseuse.
Imaginons qu'un programme fasse une demande d'allocation mémoire et se voit donc attribuer une ou plusieurs pages logiques. L'OS peut alors réagir de deux manières différentes. La première est d'attribuer une page physique immédiatement, en même temps que la page logique. En faisant ainsi, on ne peut pas avoir de défaut mineur, sauf en cas de problème de protection mémoire. Cette solution est simple, on l'appelle l{{'}}'''allocation immédiate'''. Une autre solution consiste à attribuer une page logique, mais l'allocation de la page physique se fait plus tard. Elle a lieu la première fois que le programme tente d'écrire/lire dans la page physique. Un défaut mineur a lieu, et c'est lui qui force l'OS à attribuer une page physique pour la page logique demandée. On parle alors d{{'}}'''allocation paresseuse'''. L'avantage est que l'on gagne en performance si des pages logiques sont allouées mais utilisées, ce qui peut arriver.
Une optimisation permise par l'existence des défauts mineurs est le '''''copy-on-write'''''. Le but est d'optimiser la copie d'une page logique dans une autre. L'idée est que la copie est retardée quand elle est vraiment nécessaire, à savoir quand on écrit dans la copie. Tant que l'on ne modifie pas la copie, les deux pages logiques, originelle et copiée, pointent vers la même page physique. A quoi bon avoir deux copies avec le même contenu ? Par contre, la page physique est marquée en lecture seule. La moindre écriture déclenche une erreur de protection mémoire, et un défaut mineur. Celui-ci est géré par l'OS, qui effectue alors la copie dans une nouvelle page physique.
Je viens de dire que le système d'exploitation gère les défauts de page majeurs/mineurs. Un défaut de page déclenche une exception matérielle, qui passe la main au système d'exploitation. Le système d'exploitation doit alors déterminer ce qui a levé l'exception, notamment identifier si c'est un défaut de page mineur ou majeur. Pour cela, le processeur a un ou plusieurs '''registres de statut''' qui indique l'état du processeur, qui sont utiles pour gérer les défauts de page. Ils indiquent quelle est l'adresse fautive, si l'accès était une lecture ou écriture, si l'accès a eu lieu en espace noyau ou utilisateur (les espaces mémoire ne sont pas les mêmes), etc. Les registres en question varient grandement d'une architecture de processeur à l'autre, aussi on ne peut pas dire grand chose de plus sur le sujet. Le reste est de toute façon à voir dans un cours sur les systèmes d'exploitation.
====Le remplacement des pages====
Les pages virtuelles font référence soit à une page en mémoire physique, soit à une page sur le disque dur. Mais l'on ne peut pas lire une page directement depuis le disque dur. Les pages sur le disque dur doivent être chargées en RAM, avant d'être utilisables. Ce n'est possible que si on a une page mémoire vide, libre. Si ce n'est pas le cas, on doit faire de la place en swappant une page sur le disque dur. Les pages font ainsi une sorte de va et vient entre le fichier d'échange et la RAM, suivant les besoins. Tout cela est effectué par une routine d'interruption du système d'exploitation, le processeur n'ayant pas vraiment de rôle là-dedans.
Supposons que l'on veuille faire de la place en RAM pour une nouvelle page. Dans une implémentation naïve, on trouve une page à évincer de la mémoire, qui est copiée dans le ''swapfile''. Toutes les pages évincées sont alors copiées sur le disque dur, à chaque remplacement. Néanmoins, cette implémentation naïve peut cependant être améliorée si on tient compte d'un point important : si la page a été modifiée depuis le dernier accès. Si le programme/processeur a écrit dans la page, alors celle-ci a été modifiée et doit être sauvegardée sur le ''swapfile'' si elle est évincée. Par contre, si ce n'est pas le cas, la page est soit initialisée, soit déjà présente à l'identique dans le ''swapfile''.
Mais cette optimisation demande de savoir si une écriture a eu lieu dans la page. Pour cela, on ajoute un '''''dirty bit''''' à chaque entrée de la table des pages, juste à côté du bit ''Valid''. Il indique si une écriture a eu lieu dans la page depuis qu'elle a été chargée en RAM. Ce bit est mis à jour par le processeur, automatiquement, lors d'une écriture. Par contre, il est remis à zéro par le système d'exploitation, quand la page est chargée en RAM. Si le programme se voit allouer de la mémoire, il reçoit une page vide, et ce bit est initialisé à 0. Il est mis à 1 si la mémoire est utilisée. Quand la page est ensuite swappée sur le disque dur, ce bit est remis à 0 après la sauvegarde.
Sur la majorité des systèmes d'exploitation, il est possible d'interdire le déplacement de certaines pages sur le disque dur. Ces pages restent alors en mémoire RAM durant un temps plus ou moins long, parfois en permanence. Cette possibilité simplifie la vie des programmeurs qui conçoivent des systèmes d'exploitation : essayez d'exécuter l'interruption pour les défauts de page alors que la page contenant le code de l'interruption est placée sur le disque dur ! Là encore, cela demande d'ajouter un bit dans chaque entrée de la table des pages, qui indique si la page est swappable ou non. Le bit en question s'appelle souvent le '''bit ''swappable'''''.
====Les algorithmes de remplacement des pages pris en charge par l'OS====
Le choix de la page doit être fait avec le plus grand soin et il existe différents algorithmes qui permettent de décider quelle page supprimer de la RAM. Leur but est de swapper des pages qui ne seront pas accédées dans le futur, pour éviter d'avoir à faire triop de va-et-vient entre RAM et ''swapfile''. Les données qui sont censées être accédées dans le futur doivent rester en RAM et ne pas être swappées, autant que possible. Les algorithmes les plus simples pour le choix de page à évincer sont les suivants.
Le plus simple est un algorithme aléatoire : on choisit la page au hasard. Mine de rien, cet algorithme est très simple à implémenter et très rapide à exécuter. Il ne demande pas de modifier la table des pages, ni même d'accéder à celle-ci pour faire son choix. Ses performances sont surprenamment correctes, bien que largement en-dessous de tous les autres algorithmes.
L'algorithme FIFO supprime la donnée qui a été chargée dans la mémoire avant toutes les autres. Cet algorithme fonctionne bien quand un programme manipule des tableaux de grande taille, mais fonctionne assez mal dans le cas général.
L'algorithme LRU supprime la donnée qui été lue ou écrite pour la dernière fois avant toutes les autres. C'est théoriquement le plus efficace dans la majorité des situations. Malheureusement, son implémentation est assez complexe et les OS doivent modifier la table des pages pour l'implémenter.
L'algorithme le plus utilisé de nos jours est l{{'}}'''algorithme NRU''' (''Not Recently Used''), une simplification drastique du LRU. Il fait la différence entre les pages accédées il y a longtemps et celles accédées récemment, d'une manière très binaire. Les deux types de page sont appelés respectivement les '''pages froides''' et les '''pages chaudes'''. L'OS swappe en priorité les pages froides et ne swappe de page chaude que si aucune page froide n'est présente. L'algorithme est simple : il choisit la page à évincer au hasard parmi une page froide. Si aucune page froide n'est présente, alors il swappe au hasard une page chaude.
Pour implémenter l'algorithme NRU, l'OS mémorise, dans chaque entrée de la table des pages, si la page associée est froide ou chaude. Pour cela, il met à 0 ou 1 un bit dédié : le '''bit ''Accessed'''''. La différence avec le bit ''dirty'' est que le bit ''dirty'' est mis à jour uniquement lors des écritures, alors que le bit ''Accessed'' l'est aussi lors d'une lecture. Uen lecture met à 1 le bit ''Accessed'', mais ne touche pas au bit ''dirty''. Les écritures mettent les deux bits à 1.
Implémenter l'algorithme NRU demande juste de mettre à jour le bit ''Accessed'' de chaque entrée de la table des pages. Et sur les architectures modernes, le processeur s'en charge automatiquement. A chaque accès mémoire, que ce soit en lecture ou en écriture, le processeur met à 1 ce bit. Par contre, le système d'exploitation le met à 0 à intervalles réguliers. En conséquence, quand un remplacement de page doit avoir lieu, les pages chaudes ont de bonnes chances d'avoir le bit ''Accessed'' à 1, alors que les pages froides l'ont à 0. Ce n'est pas certain, et on peut se trouver dans des cas où ce n'est pas le cas. Par exemple, si un remplacement a lieu juste après la remise à zéro des bits ''Accessed''. Le choix de la page à remplacer est donc imparfait, mais fonctionne bien en pratique.
Tous les algorithmes précédents ont chacun deux variantes : une locale, et une globale. Avec la version locale, la page qui va être rapatriée sur le disque dur est une page réservée au programme qui est la cause du page miss. Avec la version globale, le système d'exploitation va choisir la page à virer parmi toutes les pages présentes en mémoire vive.
===La protection mémoire avec la pagination===
Avec la pagination, chaque page a des '''droits d'accès''' précis, qui permettent d'autoriser ou interdire les accès en lecture, écriture, exécution, etc. La table des pages mémorise les autorisations pour chaque page, sous la forme d'une suite de bits où chaque bit autorise/interdit une opération bien précise. En pratique, les tables de pages modernes disposent de trois bits : un qui autorise/interdit les accès en lecture, un qui autorise/interdit les accès en écriture, un qui autorise/interdit l'éxecution du contenu de la page.
Le format exact de la suite de bits a cependant changé dans le temps sur les processeurs x86 modernes. Par exemple, avant le passage au 64 bits, les CPU et OS ne pouvaient pas marquer une page mémoire comme non-exécutable. C'est seulement avec le passage au 64 bits qu'a été ajouté un bit pour interdire l'exécution de code depuis une page. Ce bit, nommé '''bit NX''', est à 0 si la page n'est pas exécutable et à 1 sinon. Le processeur vérifie à chaque chargement d'instruction si le bit NX de page lue est à 1. Sinon, il lève une exception matérielle et laisse la main à l'OS.
Une amélioration de cette protection est la technique dite du '''''Write XOR Execute''''', abréviée WxX. Elle consiste à interdire les pages d'être à la fois accessibles en écriture et exécutables. Il est possible de changer les autorisations en cours de route, ceci dit.
Les premiers IBM 360 disposaient d'un mécanisme de protection mémoire totalement différent, sans registres limite/base. Ce mécanisme de protection attribue à chaque programme une '''clé de protection''', qui consiste en un nombre unique de 4 bits (chaque programme a donc une clé différente de ses collègues). La mémoire est fragmentée en blocs de même taille, de 2 kibioctets. Le processeur mémorise, pour chacun de ses blocs, la clé de protection du programme qui a réservé ce bloc. À chaque accès mémoire, le processeur compare la clé de protection du programme en cours d’exécution et celle du bloc de mémoire de destination. Si les deux clés sont différentes, alors un programme a effectué un accès hors des clous et il se fait sauvagement arrêter.
===La traduction d'adresse avec la pagination===
Comme dit plus haut, les pages sont numérotées, de 0 à une valeur maximale, afin de les identifier. Le numéro en question est appelé le '''numéro de page'''. Il est utilisé pour dire au processeur : je veux lire une donnée dans la page numéro 20, la page numéro 90, etc. Une fois qu'on a le numéro de page, on doit alors préciser la position de la donnée dans la page, appelé le '''décalage''', ou encore l{{'}}''offset''.
Le numéro de page et le décalage se déduisent à partir de l'adresse, en divisant l'adresse par la taille de la page. Le quotient obtenu donne le numéro de la page, alors que le reste est le décalage. Les processeurs actuels utilisent tous des pages dont la taille est une puissance de deux, ce qui fait que ce calcul est fortement simplifié. Sous cette condition, le numéro de page correspond aux bits de poids fort de l'adresse, alors que le décalage est dans les bits de poids faible.
Le numéro de page existe en deux versions : un numéro de page physique qui identifie une page en mémoire physique, et un numéro de page logique qui identifie une page dans la mémoire virtuelle. Traduire l'adresse logique en adresse physique demande de remplacer le numéro de la page logique en un numéro de page physique.
[[File:Phycical address.JPG|centre|vignette|upright=2|Traduction d'adresse avec la pagination.]]
====Les tables des pages simples====
Dans le cas le plus simple, il n'y a qu'une seule table des pages, qui est adressée par les numéros de page logique. La table des pages est un vulgaire tableau d'adresses physiques, placées les unes à la suite des autres. Avec cette méthode, la table des pages a autant d'entrée qu'il y a de pages logiques en mémoire virtuelle. Accéder à la mémoire nécessite donc d’accéder d'abord à la table des pages en mémoire, de calculer l'adresse de l'entrée voulue, et d’y accéder.
[[File:Table des pages.png|centre|vignette|upright=2|Table des pages.]]
La table des pages est souvent stockée dans la mémoire RAM, son adresse est connue du processeur, mémorisée dans un registre spécialisé du processeur. Le processeur effectue automatiquement le calcul d'adresse à partir de l'adresse de base et du numéro de page logique.
[[File:Address translation (32-bit).png|centre|vignette|upright=2|Address translation (32-bit)]]
====Les tables des pages inversées====
Sur certains systèmes, notamment sur les architectures 64 bits ou plus, le nombre de pages est très important. Sur les ordinateurs x86 récents, les adresses sont en pratique de 48 bits, les bits de poids fort étant ignorés en pratique, ce qui fait en tout 68 719 476 736 pages. Chaque entrée de la table des pages fait au minimum 48 bits, mais fait plus en pratique : partons sur 64 bits par entrée, soit 8 octets. Cela fait 549 755 813 888 octets pour la table des pages, soit plusieurs centaines de gibioctets ! Une table des pages normale serait tout simplement impraticable.
Pour résoudre ce problème, on a inventé les '''tables des pages inversées'''. L'idée derrière celles-ci est l'inverse de la méthode précédente. La méthode précédente stocke, pour chaque page logique, son numéro de page physique. Les tables des pages inversées font l'inverse : elles stockent, pour chaque numéro de page physique, la page logique qui correspond. Avec cette méthode table des pages contient ainsi autant d'entrées qu'il y a de pages physiques. Elle est donc plus petite qu'avant, vu que la mémoire physique est plus petite que la mémoire virtuelle.
Quand le processeur veut convertir une adresse virtuelle en adresse physique, la MMU recherche le numéro de page de l'adresse virtuelle dans la table des pages. Le numéro de l'entrée à laquelle se trouve ce morceau d'adresse virtuelle est le morceau de l'adresse physique. Pour faciliter le processus de recherche dans la page, la table des pages inversée est ce que l'on appelle une table de hachage. C'est cette solution qui est utilisée sur les processeurs Power PC.
[[File:Table des pages inversée.jpg|centre|vignette|upright=2|Table des pages inversée.]]
====Les tables des pages multiples par espace d'adressage====
Dans les deux cas précédents, il y a une table des pages unique. Cependant, les concepteurs de processeurs et de systèmes d'exploitation ont remarqué que les adresses les plus hautes et/ou les plus basses sont les plus utilisées, alors que les adresses situées au milieu de l'espace d'adressage sont peu utilisées en raison du fonctionnement de la pile et du tas. Il y a donc une partie de la table des pages qui ne sert à rien et est utilisé pour des adresses inutilisées. C'est une source d'économie d'autant plus importante que les tables des pages sont de plus en plus grosses.
Pour profiter de cette observation, les concepteurs d'OS ont décidé de découper l'espace d'adressage en plusieurs sous-espaces d'adressage de taille identique : certains localisés dans les adresses basses, d'autres au milieu, d'autres tout en haut, etc. Et vu que l'espace d'adressage est scindé en plusieurs parties, la table des pages l'est aussi, elle est découpée en plusieurs sous-tables. Si un sous-espace d'adressage n'est pas utilisé, il n'y a pas besoin d'utiliser de la mémoire pour stocker la table des pages associée. On ne stocke que les tables des pages pour les espaces d'adressage utilisés, ceux qui contiennent au moins une donnée.
L'utilisation de plusieurs tables des pages ne fonctionne que si le système d'exploitation connaît l'adresse de chaque table des pages (celle de la première entrée). Pour cela, le système d'exploitation utilise une super-table des pages, qui stocke les adresses de début des sous-tables de chaque sous-espace. En clair, la table des pages est organisé en deux niveaux, la super-table étant le premier niveau et les sous-tables étant le second niveau.
L'adresse est structurée de manière à tirer profit de cette organisation. Les bits de poids fort de l'adresse sélectionnent quelle table de second niveau utiliser, les bits du milieu de l'adresse sélectionne la page dans la table de second niveau et le reste est interprété comme un ''offset''. Un accès à la table des pages se fait comme suit. Les bits de poids fort de l'adresse sont envoyés à la table de premier niveau, et sont utilisés pour récupérer l'adresse de la table de second niveau adéquate. Les bits au milieu de l'adresse sont envoyés à la table de second niveau, pour récupérer le numéro de page physique. Le tout est combiné avec l{{'}}''offset'' pour obtenir l'adresse physique finale.
[[File:Table des pages hiérarchique.png|centre|vignette|upright=2|Table des pages hiérarchique.]]
On peut aussi aller plus loin et découper la table des pages de manière hiérarchique, chaque sous-espace d'adressage étant lui aussi découpé en sous-espaces d'adressages. On a alors une table de premier niveau, plusieurs tables de second niveau, encore plus de tables de troisième niveau, et ainsi de suite. Cela peut aller jusqu'à 5 niveaux sur les processeurs x86 64 bits modernes. On parle alors de '''tables des pages emboitées'''. Dans ce cours, la table des pages désigne l'ensemble des différents niveaux de cette organisation, toutes les tables inclus. Seules les tables du dernier niveau mémorisent des numéros de page physiques, les autres tables mémorisant des pointeurs, des adresses vers le début des tables de niveau inférieur. Un exemple sera donné plus bas, dans la section suivante.
====L'exemple des processeurs x86====
Pour rendre les explications précédentes plus concrètes, nous allons prendre l'exemple des processeur x86 anciens, de type 32 bits. Les processeurs de ce type utilisaient deux types de tables des pages : une table des page unique et une table des page hiérarchique. Les deux étaient utilisées dans cas séparés. La table des page unique était utilisée pour les pages larges et encore seulement en l'absence de la technologie ''physical adress extension'', dont on parlera plus bas. Les autres cas utilisaient une table des page hiérarchique, à deux niveaux, trois niveaux, voire plus.
Une table des pages unique était utilisée pour les pages larges (de 2 mébioctets et plus). Pour les pages de 4 mébioctets, il y avait une unique table des pages, adressée par les 10 bits de poids fort de l'adresse, les bits restants servant comme ''offset''. La table des pages contenait 1024 entrées de 4 octets chacune, ce qui fait en tout 4 kibioctet pour la table des pages. La table des page était alignée en mémoire sur un bloc de 4 kibioctet (sa taille).
[[File:X86 Paging 4M.svg|centre|vignette|upright=2|X86 Paging 4M]]
Pour les pages de 4 kibioctets, les processeurs x86-32 bits utilisaient une table des page hiérarchique à deux niveaux. Les 10 bits de poids fort l'adresse adressaient la table des page maitre, appelée le directoire des pages (''page directory''), les 10 bits précédents servaient de numéro de page logique, et les 12 bits restants servaient à indiquer la position de l'octet dans la table des pages. Les entrées de chaque table des pages, mineure ou majeure, faisaient 32 bits, soit 4 octets. Vous remarquerez que la table des page majeure a la même taille que la table des page unique obtenue avec des pages larges (de 4 mébioctets).
[[File:X86 Paging 4K.svg|centre|vignette|upright=2|X86 Paging 4K]]
La technique du '''''physical adress extension''''' (PAE), utilisée depuis le Pentium Pro, permettait aux processeurs x86 32 bits d'adresser plus de 4 gibioctets de mémoire, en utilisant des adresses physiques de 64 bits. Les adresses virtuelles de 32 bits étaient traduites en adresses physiques de 64 bits grâce à une table des pages adaptée. Cette technologie permettait d'adresser plus de 4 gibioctets de mémoire au total, mais avec quelques limitations. Notamment, chaque programme ne pouvait utiliser que 4 gibioctets de mémoire RAM pour lui seul. Mais en lançant plusieurs programmes, on pouvait dépasser les 4 gibioctets au total. Pour cela, les entrées de la table des pages passaient à 64 bits au lieu de 32 auparavant.
La table des pages gardait 2 niveaux pour les pages larges en PAE.
[[File:X86 Paging PAE 2M.svg|centre|vignette|upright=2|X86 Paging PAE 2M]]
Par contre, pour les pages de 4 kibioctets en PAE, elle était modifiée de manière à ajouter un niveau de hiérarchie, passant de deux niveaux à trois.
[[File:X86 Paging PAE 4K.svg|centre|vignette|upright=2|X86 Paging PAE 4K]]
En 64 bits, la table des pages est une table des page hiérarchique avec 5 niveaux. Seuls les 48 bits de poids faible des adresses sont utilisés, les 16 restants étant ignorés.
[[File:X86 Paging 64bit.svg|centre|vignette|upright=2|X86 Paging 64bit]]
===La TLB et les caches associés===
En théorie, la table des pages est censée être accédée à chaque accès mémoire. Mais pour éviter d'avoir à lire la table des pages en mémoire RAM à chaque accès mémoire, les concepteurs de processeurs ont décidé d'implanter un cache dédié, le '''''translation lookaside buffer''''', ou TLB. Le TLB stocke au minimum de quoi faire la traduction entre adresse virtuelle et adresse physique, à savoir une correspondance entre numéro de page logique et numéro de page physique. Pour faire plus général, il stocke des entrées de la table des pages.
[[File:MMU principle updated.png|centre|vignette|upright=2.0|MMU avec une TLB.]]
Les accès à la table des pages sont gérés de deux façons : soit le processeur gère tout seul la situation, soit il délègue cette tâche au système d’exploitation. Sur les processeurs anciens, le système d'exploitation gère le parcours de la table des pages. Mais cette solution logicielle n'a pas de bonnes performances. D'autres processeurs gèrent eux-mêmes le défaut d'accès à la TLB et vont chercher d'eux-mêmes les informations nécessaires dans la table des pages. Ils disposent de circuits, les '''''page table walkers''''' (PTW), qui s'occupent eux-mêmes du défaut.
Les ''page table walkers'' contiennent des registres qui leur permettent de faire leur travail. Le plus important est celui qui mémorise la position de la table des pages en mémoire RAM, dont nous avons parlé plus haut. Les PTW ont besoin, pour faire leur travail, de mémoriser l'adresse physique de la table des pages, ou du moins l'adresse de la table des pages de niveau 1 pour des tables des pages hiérarchiques. Mais d'autres registres existent. Toutes les informations nécessaires pour gérer les défauts de TLB sont stockées dans des registres spécialisés appelés des '''tampons de PTW''' (PTW buffers).
===L'abstraction matérielle des processus===
[[File:Memoire virtuelle.svg|vignette|Mémoire virtuelle]]
Il est possible d'implémenter l'abstraction matérielle des processus avec la pagination. En clair, chaque programme lancé sur l'ordinateur dispose de son propre espace d'adressage, ce qui fait que la même adresse logique ne pointera pas sur la même adresse physique dans deux programmes différents. Pour cela, il y a plusieurs méthodes.
La première solution n'utilise qu'une seule table des pages. Pour cela, chaque entrée indique à quel processus elle est associée. Pour cela, chaque entrée contient un '''identifiant de processus''', un numéro qui précise pour quel processus, pour quel espace d'adressage, la correspondance est valide. La page des tables peut aussi contenir des entrées qui sont valides pour tous les processus en même temps. Pour cela, l'entrée contient un '''bit ''global''''', qui précise que cette correspondance est valide pour tous les processus, l'identifiant de processus est ignoré. Un défaut est que le partage d'une page entre plusieurs processus est impossible : soit on partage une page avec tous les processus, soit on l'alloue avec un seul processus.
L'intérêt de partager des pages entre tous les processus n'est pas évident, mais il le devient quand on se rappelle que le noyau de l'OS est mappé dans le haut de l'espace d'adressage. Et peu importe l'espace d'adressage, le noyau est toujours mappé de manière identique, les mêmes adresses logiques adressant la même adresse mémoire. En conséquence, les correspondances adresse physique-logique sont les mêmes pour le noyau, peu importe l'espace d'adressage. Le bit global accélère rapidement la traduction d'adresse pour l'accès au noyau.
Une solution alternative, plus simple, utilise une table des pages par processus lancé sur l'ordinateur, une table des pages unique par espace d'adressage. À chaque changement de processus, le registre qui mémorise la position de la table des pages est modifié pour pointer sur la bonne. C'est le système d'exploitation qui se charge de cette mise à jour.
Avec cette méthode, il est possible de partager une ou plusieurs pages entre plusieurs processus, en configurant les tables des pages convenablement. Les pages partagées sont mappées dans l'espace d'adressage de plusieurs processus, mais pas forcément au même endroit, pas forcément dans les mêmes adresses logiques. On peut placer la page partagée à l'adresse logique 0x0FFF pour un processus, à l'adresse logique 0xFF00 pour un autre processus, etc. Par contre, les entrées de la table des pages pour ces adresses pointent vers la même adresse physique.
[[File:Vm5.png|centre|vignette|upright=2|Tables des pages de plusieurs processus.]]
===La taille des pages===
La taille des pages varie suivant le processeur et le système d'exploitation et tourne souvent autour de 4 kibioctets. Les processeurs actuels gèrent plusieurs tailles différentes pour les pages : 4 kibioctets par défaut, 2 mébioctets, voire 1 à 4 gibioctets pour les pages les plus larges. Les pages de 4 kibioctets sont les pages par défaut, les autres tailles de page sont appelées des ''pages larges''. La taille optimale pour les pages dépend de nombreux paramètres et il n'y a pas de taille qui convienne à tout le monde. Certaines applications gagnent à utiliser des pages larges, d'autres vont au contraire perdre drastiquement en performance en les utilisant.
Le désavantage principal des pages larges est qu'elles favorisent la fragmentation mémoire. Si un programme veut réserver une portion de mémoire, pour une structure de donnée quelconque, il doit réserver une portion dont la taille est multiple de la taille d'une page. Par exemple, un programme ayant besoin de 110 kibioctets allouera 28 pages de 4 kibioctets, soit 120 kibioctets : 2 kibioctets seront perdus. Par contre, avec des pages larges de 2 mébioctets, on aura une perte de 2048 - 110 = 1938 kibioctets. En somme, des morceaux de mémoire seront perdus, car les pages sont trop grandes pour les données qu'on veut y mettre. Le résultat est que le programme qui utilise les pages larges utilisent plus de mémoire et ce d'autant plus qu'il utilise des données de petite taille. Un autre désavantage est qu'elles se marient mal avec certaines techniques d'optimisations de type ''copy-on-write''.
Mais l'avantage est que la traduction des adresses est plus performante. Une taille des pages plus élevée signifie moins de pages, donc des tables des pages plus petites. Et des pages des tables plus petites n'ont pas besoin de beaucoup de niveaux de hiérarchie, voire peuvent se limiter à des tables des pages simples, ce qui rend la traduction d'adresse plus simple et plus rapide. De plus, les programmes ont une certaine localité spatiale, qui font qu'ils accèdent souvent à des données proches. La traduction d'adresse peut alors profiter de systèmes de mise en cache dont nous parlerons dans le prochain chapitre, et ces systèmes de cache marchent nettement mieux avec des pages larges.
Il faut noter que la taille des pages est presque toujours une puissance de deux. Cela a de nombreux avantages, mais n'est pas une nécessité. Par exemple, le tout premier processeur avec de la pagination, le super-ordinateur Atlas, avait des pages de 3 kibioctets. L'avantage principal est que la traduction de l'adresse physique en adresse logique est trivial avec une puissance de deux. Cela garantit que l'on peut diviser l'adresse en un numéro de page et un ''offset'' : la traduction demande juste de remplacer les bits de poids forts par le numéro de page voulu. Sans cela, la traduction d'adresse implique des divisions et des multiplications, qui sont des opérations assez couteuses.
===Les entrées de la table des pages===
Avant de poursuivre, faisons un rapide rappel sur les entrées de la table des pages. Nous venons de voir que la table des pages contient de nombreuses informations : un bit ''valid'' pour la mémoire virtuelle, des bits ''dirty'' et ''accessed'' utilisés par l'OS, des bits de protection mémoire, un bit ''global'' et un potentiellement un identifiant de processus, etc. Étudions rapidement le format de la table des pages sur un processeur x86 32 bits.
* Elle contient d'abord le numéro de page physique.
* Les bits AVL sont inutilisés et peuvent être configurés à loisir par l'OS.
* Le bit G est le bit ''global''.
* Le bit PS vaut 0 pour une page de 4 kibioctets, mais est mis à 1 pour une page de 4 mébioctets dans le cas où le processus utilise des pages larges.
* Le bit D est le bit ''dirty''.
* Le bit A est le bit ''accessed''.
* Le bit PCD indique que la page ne peut pas être cachée, dans le sens où le processeur ne peut copier son contenu dans le cache et doit toujours lire ou écrire cette page directement dans la RAM.
* Le bit PWT indique que les écritures doivent mettre à jour le cache et la page en RAM (dans le chapitre sur le cache, on verra qu'il force le cache à se comporter comme un cache ''write-through'' pour cette page).
* Le bit U/S précise si la page est accessible en mode noyau ou utilisateur.
* Le bit R/W indique si la page est accessible en écriture, toutes les pages sont par défaut accessibles en lecture.
* Le bit P est le bit ''valid''.
[[File:PDE.png|centre|vignette|upright=2.5|Table des pages des processeurs Intel 32 bits.]]
==Comparaison des différentes techniques d'abstraction mémoire==
Pour résumer, l'abstraction mémoire permet de gérer : la relocation, la protection mémoire, l'isolation des processus, la mémoire virtuelle, l'extension de l'espace d'adressage, le partage de mémoire, etc. Elles sont souvent implémentées en même temps. Ce qui fait qu'elles sont souvent confondues, alors que ce sont des concepts sont différents. Ces liens sont résumés dans le tableau ci-dessous.
{|class="wikitable"
|-
!
! colspan="5" | Avec abstraction mémoire
! rowspan="2" | Sans abstraction mémoire
|-
!
! Relocation matérielle
! Segmentation en mode réel (x86)
! Segmentation, général
! Architectures à capacités
! Pagination
|-
! Abstraction matérielle des processus
| colspan="4" | Oui, relocation matérielle
| Oui, liée à la traduction d'adresse
| Emulée via relocation logicielle au démarrage du programme.
|-
! Mémoire virtuelle
| colspan="2" | Non, sauf émulation logicielle
| colspan="3" | Oui, gérée par le processeur et l'OS
| Non, sauf émulation logicielle
|-
! Extension de l'espace d'adressage
| colspan="2" | Oui : registre de base élargi
| colspan="2" | Oui : adresse de base élargie dans la table des segments
| ''Physical Adress Extension'' des processeurs 32 bits
| Commutation de banques
|-
! Protection mémoire
| Registre limite
| Aucune
| colspan="2" | Registre limite, droits d'accès aux segments
| Gestion des droits d'accès aux pages
| Possible, méthodes variées
|-
! Partage de mémoire
| colspan="2" | Non
| colspan="2" | Segment partagés
| Pages partagées
| Possible, méthodes variées
|}
===Les différents types de segmentation===
La segmentation regroupe plusieurs techniques franchement différentes, qui auraient gagné à être nommées différemment. La principale différence est l'usage de registres de relocation versus des registres de sélecteurs de segments. L'usage de registres de relocation est le fait de la relocation matérielle, mais aussi de la segmentation en mode réel des CPU x86. Par contre, l'usage de sélecteurs de segments est le fait des autres formes de segmentation, architectures à capacité inclues.
La différence entre les deux est le nombre de segments. L'usage de registres de relocation fait que le CPU ne gère qu'un petit nombre de segments de grande taille. La mémoire virtuelle est donc rarement implémentée vu que swapper des segments de grande taille est trop long, l'impact sur les performances est trop important. Sans compter que l'usage de registres de base se marie très mal avec la mémoire virtuelle. Vu qu'un segment peut être swappé ou déplacée n'importe quand, il faut invalider les registres de base au moment du swap/déplacement, ce qui n'est pas chose aisée. Aucun processeur ne gère cela, les méthodes pour n'existent tout simplement pas. L'usage de registres de base implique que la mémoire virtuelle est absente.
La protection mémoire est aussi plus limitée avec l'usage de registres de relocation. Elle se limite à des registres limite, mais la gestion des droits d'accès est limitée. En théorie, la segmentation en mode réel pourrait implémenter une version limitée de protection mémoire, avec une protection de l'espace exécutable. Mais ca n'a jamais été fait en pratique sur les processeurs x86.
Le partage de la mémoire est aussi difficile sur les architectures avec des registres de base. L'absence de table des segments fait que le partage d'un segment est basiquement impossible sans utiliser des méthodes complétement tordues, qui ne sont jamais implémentées en pratique.
===Segmentation versus pagination===
Par rapport à la pagination, la segmentation a des avantages et des inconvénients. Tous sont liés aux propriétés des segments et pages : les segments sont de grande taille et de taille variable, les pages sont petites et de taille fixe.
L'avantage principal de la segmentation est sa rapidité. Le fait que les segments sont de grande taille fait qu'on a pas besoin d'équivalent aux tables des pages inversée ou multiple, juste d'une table des segments toute simple. De plus, les échanges entre table des pages/segments et registres sont plus rares avec la segmentation. Par exemple, si un programme utilise un segment de 2 gigas, tous les accès dans le segment se feront avec une seule consultation de la table des segments. Alors qu'avec la pagination, il faudra une consultation de la table des pages chaque bloc de 4 kibioctet, au minimum.
Mais les désavantages sont nombreux. Le système d'exploitation doit agencer les segments en RAM, et c'est une tâche complexe. Le fait que les segments puisse changer de taille rend le tout encore plus complexe. Par exemple, si on colle les segments les uns à la suite des autres, changer la taille d'un segment demande de réorganiser tous les segments en RAM, ce qui demande énormément de copies RAM-RAM. Une autre possibilité est de laisser assez d'espace entre les segments, mais cet espace est alors gâché, dans le sens où on ne peut pas y placer un nouveau segment.
Swapper un segment est aussi très long, vu que les segments sont de grande taille, alors que swapper une page est très rapide.
==Un cas particulier : la z/architecture d'IBM==
IBM a sa propre gamme de ''mainframes'', qui utilisent des processeurs fait maison par IBM lui-même. Les CPU IBM ont leur propre jeu d'instruction CISC, appelé '''''z/architecture'''''. Et ce jeu d'instruction a des possibilités en termes d'adressage qui sont extrêmement intéressantes, en plus d'être assez inédites.
Les premiers ''mainframes'' n'avaient même pas de support basique de la mémoire virtuelle, le System/360 s'en passait totalement. C'est sur le System/370 que la pagination a été ajoutée. Le System/370 supportait deux espaces d'adressage virtuel par programme, là où les implémentations classiques n'en utilisent qu'un par programme. L'ESA/370 a ajouté la possibilité d'utiliser 16 espaces d'adressage par programme, mais l'a fait d'une manière à garder la compatibilité avec le System/370. Le processeur a donc deux modes : un avec deux espaces d'adressage par programme, un autre pouvant monter jusqu’à 16.
La taille des adresses a aussi évoluée dans le temps : partant de 24 bits sur le System/360, elle est montée à 31 bits sur le System/370, pour arriver à 64 bits sur le jeu d'instruction z/architecture proprement dit. Sur le System/370, il était possible de switcher à la volée entre adressage 24 et 31 bits, avec les instructions ''Branch and save and
set mode'' (BASSM) et ''Branch And Set Mode'' (BSM).
Un point important est qu'IBM a mit le paquet en termes de compatibilité. Les programmes qui fonctionnaient sur le System/360 fonctionnent aussi sur le System/370,sur l'ESA 370, et sur les processeurs z/architecture récents. En conséquence, toute fonctionnalité introduite sur un modèle a été reprise à l'identique sur les modèles suivants. Aussi, pour comprendre comment la z/architecture gère les espaces d'adressage, on n'a pas le choix : on doit étudier chaque ''mainframe'' d'Ibm dans l'ordre chronologique. Suivre une approche historique permetttra de bien comprendre pourquoi un processeur ''z/architecture'' peut fonctionner dans deux modes, qu'on va appeler "standard" et "étendu", l'un avec seulement deux espaces d'adressage par programme, l'autre permettra de monter à 2048.
===Le System/370===
Le System/370 gérait des pages de 4 kibioctets, les pages larges ayant été introduites plus tard. La traduction d’adresse se fait en passant des regroupements de plusieurs pages virtuelles, appelés des ''segments'', qui n'ont rien à voir avec les segments au sens de la segmentation. Pour éviter les confusions, je vais les appeler des ''super-pages''. Les super-pages font 64 kibioctets ou 1 mébioctet, et regroupent plusieurs pages de 2 ou 4 mébioctets. Les pages d'une super-page peuvent être très éloignées en mémoire physique. Les super-pages sont concrètement des intermédiaires utilisés pour la traduction d'adresse. La traduction d'adresse démarre à une table des super-pages, qui point vers une table des pages à plusieurs niveaux (5 maximum).
Le System/370 était un système multi-processeur, ce qui a amené à quelques spécificités en termes d'adressage mémoire. La page numéro 0 sert pour le vecteur d'interruption et d'autres données importantes, pour des données importantes. Et chaque processeur a besoin de sa propre table d'interruption. Pour cela, chaque processeur réserve une page dédiée pour son vecteur d'interruption. Quand ce processeur accède à une adresse située dans la page numéro 0, l'adresse est modifiée pour tomber dans cette page réservée. Notons que le numéro de page converti est un numéro de page physique ! Le processeur fait donc une première traduction d'adresse pour déterminer l'adresse physique, et celle-ci est corrigée une seconde fois en cas d'accès à la première page mémoire. Le tout porte le nom de technique de '''''prefixing'''''.
Le System/370 supporte deux espaces d'adressages par programme : l''''espace d'adressage primaire''' où se situent le programme exécuté et les données importantes, un '''espace d'adressage secondaire''' utilisé pour des tâches/données annexes. Un troisième espace d'adressage l''''espace d'adressage ''home''''', est réservé au système d'exploitation. Il a pour particularité de ne pas avoir d'ASN. Il n'est concrètement accessible qu'en mode superviseur, donc par le système d'exploitation. C'est en quelque sorte un espace d'adressage pour le noyau, séparé des espaces d'adressage utilisateur. Nous n'en parlerons pas dans ce qui suit.
Mais comment le processeur pour utiliser ces espaces d'adressages ? Par exemple, s'il veut faire une lecture, comment précise-t-il l'espace d'adressage courant ? La réponse est que le processeur peut être dans deux états : soit primaire, soit secondaire. Dans le premier état, toutes les lectures/écritures se font dans l'espace d'adressage primaire. Dans le second, elles se font toutes dans l'espace d'adressage secondaire. Il peut passer de l'un à l'autre en utilisant l'instruction ''Set Address Space Control''. Le processeur dispose aussi d'instructions ''MOVE TO PRIMARY'' et ''MOVE TO SECONDARY'' pour échanger des données dans les deux espaces d'adressage.
L'espace d'adressage primaire a une table des pages dédiée, séparée de celle de l'espace d'adressage secondaire. Ou plutôt devrais-je dire une table des super-pages. La table des pages est une table des pages emboitée. Il y a peu à détailler dessus, si ce n'est pour cette histoire de segments/super-pages. L'adresse de la table des super-page est mémorisée dans le registre de contrôle CR1 pour l'espace d'adressage primaire, CR7 pour l'espace d'adressage secondaire. L'un ou l'autre est utilisé, selon que le processeur est en mode "primaire" ou "secondaire".
Un programme peut donc passer de l'espace d'adressage primaire à secondaire (ou inversement) en utilisant l’instruction ''SET ADDRESS SPACE CONTROL''.
===Les registres d'accès de l'ESA370===
Si le System/370 permettait d'utiliser deux espaces d'adressage par programme, l'ESA 370 a poussé la logique nettement plus loin. Il a ajouté un mode de fonctionnement supplémentaire, qui permet d'utiliser jusqu'à 2048 espaces d'adressage différents, en même temps. Le processeur n'a pas besoin d'utiliser d'instructions particulières pour copier des données d'un espace d'adressage à un autre, un simple MOV suffit. Le mode en question est appelé le '''mode ''access register'''''. Il est complémentaire du mode à deux espaces d'adressage, pour des raisons de compatibilité, l'ancien mode à deux esapces d'adressage a été conservé.
Le mode ''access register'' donne accès aux '''registres d'accès''', qui lui donnent son nom. Les registres d'accès mémorisent des identifiants qui permettent de sélectionner un espace d'adressage parmi 2048. Il y a en tout 16 registres d'accès, chacun étant appairé avec un registre général. Par exemple, le registre général numéro 5 est appairé au registre d'accès de même numéro. En mode ''access register'', toute lecture/écriture lit l'adresse dans un registre général et le registre d'accès associé indique dans quel espace d'adressage se trouve cette adresse. Par exemple, une lecture utilise l'adresse dans le registre général numéro 7, l'espace d'adressage associé sera dans le registre d'accès numéro 7.
Les registres d'accès peuvent être modifiés par des instructions classiques, en espace utilisateur. Elles sont nommées COPY ACCESS, EXTRACT ACCESS, LOAD ACCESS MULTIPLE, LOAD ADDRESS EXTENDED, SET ACCESS et STORE ACCESS MULTIPLE. Elles permettent de copier un registre d'accès dans un autre, de charger le contenu d'un registre d'accès depuis la mémoire RAM, d'enregistrer les registres d'accès dans la mémoire RAM, de copier une constante dans un registre d'accès. Mais quel est le contenu de ces registres ?
Une implémentation simple numéroterait les espaces d'adressage et mettrait les numéros dans les registres d'accès. Un espace d'adressage serait associé à une table des super-pages, reste à mémoriser la correspondance ''numéro d'espace d'adressage - adresse de la table des super-pages''. Pour cela, l'idéal est de mémoriser ces correspondances dans un tableau en mémoire RAM. Le numéro d'espace d'adressage est alors un indice, qui pointe sur l'entrée adéquate dans cette table. Il devrait y avoir un tableau par programme, qui serait protégé en écriture et seulement accessible par le système d'exploitation, comme le serait une table des pages. Mais ce n'est pas exactement ce qui est fait.
La différence est qu'il n'y a pas une table de correspondance par programme, mais un système à deux tableaux imbriqués. Le premier tableau possède 65 536 entrées, une par espace d'adressage supporté par le processeur. C'est donc une table partagée par tous les programmes. Par contre, la seconde table est bien unique pour chaque programme, chaque programme a la sienne. Faire ainsi permet de simplifier le partage de la mémoire. La table partagée est l''''''Access Space Table Entry''''' (ASTE), alors que la seconde table est la '''liste privée''' (''access list''), sous-entendu "privée pour un programme".
La liste privée mémorise une liste des espaces d'adressage auquel le programme peut accéder, auxquels le système d'exploitation lui a accordé l'accès. Pour chaque espace d'adressage, elle renvoie vers l'entrée de l'ASTE associée. En plus de mémoriser l'adresse de cette entrée, elle mémorise les droits d'accès que le programme a sur l'espace d'adressage associé. Par exemple, si deux programmes partagent un espace d'adressage, l'un pourra y avoir accès en lecture seule, l'autre pourra avoir un accès total. Pour cela, ils ont des droits différents dans la liste privée, mais l'entrée de l'ASTE sera la même.
[[File:Tradiction d'adresse sur la zarchitecture d'IBM.png|centre|vignette|upright=2|Traduction d'adresse sur la zarchitecture d'IBM.]]
Il y a donc une vérification des droits d'accès, suivi d'un accès à une table des super-pages globale. Si un programme veut accéder à un espace d'adressage qui lui est interdit, l'accès mémoire stoppera dans la liste privée. Le processeur utilisera l'identifiant d'espace d'adressage, mais la liste privée n'aura pas d'entrée associée. Elle lèvera alors une exception matérielle et le système d'exploitation gérera la situation, vraisemblablement en tuant le programme fautif. Mais si l'accès est autorisé, alors le programme accède à la table des super-pages et l'accès mémoire se poursuit.
Je dis une liste d'accès privée, mais il y en a deux en réalité. Mais la distinction exacte demanderait de parler de ''threads'' et de processus. Pour ceux qui sont familiers avec ces concepts, chaque ''thread'' a sa propre liste privée, avec en plus une liste par processus, partagée entre tous les ''threads''. Deux listes de 1024 entrées maximum, cela fait 2048 entrées au total. Les deux listes portent les noms de ''Dispatchable Unit Access List'' et de ''PASN-AL (Primary Address Space Access List''. Inutile de retenir ces deux termes, cependant.
Un registre d'accès mémorise un '''''Access List Entry Token''''' ou ALET. L'ALET contient l'indice d'une entrée dans la liste privée. Elle contient aussi un bit qui indique quelle liste privée utiliser, entre la ''Dispatchable Unit Access List'' et la ''PASN-AL (Primary Address Space Access List''. Précisons cependant que deux valeurs font exceptions : 0 pointe vers l'espace d'adressage principal, 1 vers l'espace d'adressage secondaire. Les autres valeurs sont configurables.
{|class="wikitable"
|+ Registre d'accès
|-
! 0000000 !! L (choix de la liste privée !! ASLEN !! ALEN
|-
| 0000000 || Choix de la liste privée || Identifiant de protection || Indice dans la liste privée
|}
Lors d'une lecture/écriture, l'ALETs est utilisé pour sélectionner l'espace d'adressage. Les valeurs 0 et 1 sont traitées telles quelles, l'espace d'adressage est déterminé automatiquement par le processeur. Pour les autres valeurs, récupérer l'adresse mémoire demande de consulter la liste d'accès privée. L'ALET est envoyé à la liste d'accès privée, et cette dernière fournit l'adresse de l'entrée de l'ASTE à consulter. Le processeur connait l'adresse de la liste privée, car son adresse mémoire est mémorisée dans les registres de contrôle numéro 2 et 5.
: Précisons que le registre d'accès numéro 0 contient obligatoirement la valeur 0, ce qui fait qu'il pointe vers l'espace d'adressage primaire.
[[File:Traduction d'adresse - trois espaces d'adressage, zarchitecture d'IBM.png|centre|vignette|upright=2|Traduction d'adresse - trois espaces d'adressage, zarchitecture d'IBM.]]
Une subtilité est que le système d'exploitation peut à tout moment modifier le contenu de la liste privée. Il se peut qu'un ALET qui correspondait à un espace d'adressage se mette à pointer vers un autre. Pour détecter cette situation, le processeur attribue un identifiant, à chaque espace d'adressage. L'identifiant est mémorisé dans les entrées de la liste privée, de l'ASTE et dans les registres d'accès (dans les bits de poids fort de l'ALET). L'identifiant est modifié dans l'ALET à chaque fois que le système d'exploitation modifie l'entrée. Si un programme fait un accès avec un registre d'accès, mais que l'identifiant ne correspond pas avec celui dans l'ALET/liste privée, c'est signe que l'espace d'adressage a été swappé, une exception matérielle est levée.
Un tel fonctionnement vous aura sans doute fait penser à une architecture à capacité, mais la z/architecture n'en est pas une. Malgré des points de ressemblance assez importants, il y a des différences fondamentales avec les architectures à capacité.
* Le z/architecture ne supporte pas la segmentation, qui est nécessaire pour les architectures à capacité. Sur la z/architecture, ce sont des espaces d'adressage qui sont sélectionnés, pas des segments.
* Les registres d'accès ne sont pas des registres de capacité, car on peut les modifier avec des instructions en espace utilisateur. En comparaison, modifier les registres de capacité ne peut se faire qu'en espace noyau, sous l'égide du système d'exploitation.
* Les droits d'accès sont dans une table séparée des pointeurs, pas concaténés à ceux-ci. La gestion des droits d'accès est gérée par les tables des pages.
* Les identifiants ALET ne peuvent pas être passés à un autre programme. Deux ALETs identiques correspondent à deux espaces d'adressages différents dans deux programmes différents. L'identifiant peut être identique, il pointera vers l'entrée de même numéro dans les deux listes privées, mais l'adresse dans l'entrée peut être différente.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=L'espace d'adressage du processeur
| prevText=L'espace d'adressage du processeur
| next=Les méthodes de synchronisation entre processeur et périphériques
| nextText=Les méthodes de synchronisation entre processeur et périphériques
}}
</noinclude>
0puoort49m0bp594m1dio0qqf3c56y2
Neurosciences/Les rythmes circadiens
0
68379
775320
700505
2026-10-16T16:41:31Z
Mewtow
31375
775320
wikitext
text/x-wiki
Vous avez peut-être déjà entendu parler de l'horloge biologique, cette expression métaphorique étant notamment utilisée pour parler des rythmes de sommeil. L'horloge biologique est un terme assez parlant. Il implique que le corps a une certaine "mesure du temps", qui lui fait savoir quand il fait jour, quand il fait nuit. Et ce sans se reposer sur la lumière du jour.
La preuve de l'existence de cette horloge biologique a été faite par la fameuse expérience de Michel Siffre, spéléologue français. Il s'est isolé dans une grotte, aménagée pour y vivre convenablement pendant un mois, isolé du monde extérieur, dans un environnement à température et luminosité constante, sans interactions sociales. De telles expériences permettent à un sujet de ne pas avoir la moindre indication sur le rythme jour-nuit extérieur. Il s'avéra que celui-ci garda un certain rythme journalier, que ce soit pour ses durées et phases d'éveil/sommeil ou pour sa température, et bien d'autres paramètres. Son sommeil était cependant quelque peu dégradé et ses rythmes de sommeil étaient légèrement décalés.
Ces expériences, réalisées aussi bien sur des humains que des animaux, des plantes ou des bactéries, montrent que les sujets gardent une rythmicité circadienne marquée, même sans indices extérieurs. Les scientifiques ont donc déduit que les rythmes circadiens ne sont pas imposés par l'environnement, mais sont générés par quelque chose d'interne au sujet : une '''horloge biologique'''.
En clair, le corps a une horloge biologique dont la période proche de 24 heures, avec cependant un léger décalage. La période de l'horloge biologique diffère quelque peu du rythme journalier de 24 heures, pouvant aller de 22 à 27 heures, suivant les sujets. Ce décalage n'est pas corrigé quand le sujet est placé en isolation et reste relativement constant. Par contre, en milieu naturel, ce décalage semble ne pas exister. Tout se passe comme si l'action de l'environnement permettait de resynchroniser l'horloge biologique avec le cycle jour-nuit réel.
Pour cela, les scientifiques ont postulé que la lumière ambiante, la température et d'autres paramètres comme l'heure des repas, influençaient l'horloge biologique, pour la resynchroniser; Ces évènements sont appelés des '''zeitbegers'''. Ces deux observations sont autant de contraintes qui suffisamment à définir un rythme dit circadien : un rythme endogène, proche de 24 heures, influencé par différents zeitbegers. Mais il faut de plus que ce rythme reste de 24 heures, quelle que soit la température extérieure, certains rythmes biologiques ayant une période variable selon la température.
Outre le sommeil, d'autres fonctions organiques suivent aussi un rythme journalier (nycthéméral diraient certains). Par exemple, notre température corporelle suit un rythme de 24 heures. L'adage veut ainsi que notre température soit maximale au réveil et diminue progressivement durant la journée, atteignant un minimum le soir avant de dormir. Et on pourrait aussi citer les rythmes de la pression sanguine, de la motilité intestinale, de la production d'urine ou de certaines hormones.
De plus, l'espèce humaine n'est pas la seule à avoir de tels rythmes : de nombreux animaux ont aussi un rythme veille-sommeil d'une période de 24 heures, un tel rythme s'observant aussi pour leur température ou d'autres paramètres corporels. Même certaines plantes, ou encore des bactéries, suivent un rythme journalier. Qui plus est ces rythmes sont indépendants de la luminosité, même si la lumière semble resynchroniser des rythmes défaillants. Ces rythmes d'une durée de 24 heures sont appelés des '''rythmes circadiens'''.
[[File:Biological clock human-fr.svg|centre|vignette|upright=3.0|Illustration des différents rythmes circadiens humains.]]
==Anatomie et physiologie de l'horloge biologique==
Cette horloge biologique peut être conceptuellement divisée en plusieurs sous-systèmes, qui correspondent chacun à une caractéristique de l'horloge biologique. Premièrement, on trouve l'horloge elle-même, à savoir quelque chose qui oscille à un rythme de 24 heures. Mais cette horloge est aussi soumise à un mécanisme de resynchronisation, qui capte différents zeitbegers et agit sur la synchronisation de l'horloge (sur sa phase diraient les physiciens). Enfin, cette horloge doit aussi être reliée à un système effecteur qui lui permet d'agir sur divers paramètres, comme la température ou l'activité cérébrale (pour influencer l'état de veille). Horloge biologique, mécanisme de synchronisation et système effecteur sont anatomiquement séparés, du moins dans une certaine mesure.
===L'horloge génétique===
Divers expériences ont montré que des cellules individuelles sont capables de soutenir un rythme circadien, notamment dans la production de certaines protéines. Il en va ainsi de certaines bactéries, qui synthétisent certaines protéines avec une période de presque 24 heures, même soumise à température et luminosité constante. Une première approche consiste donc à chercher le substrat de l'horloge biologique directement dans nos cellules. Et effectivement, toute cellule un tant soit peu évoluée possède un rythme circadien, d'origine génétique. Celui-ci provient d'un processus cyclique, qui fait intervenir des concentration en protéines fluctuante, régulées par l'activation et la désactivation cyclique de gènes. De telles '''horloges génétiques''' sont cependant différentes selon l'espèce abordée : les gènes et protéines en cause ne sont pas les mêmes selon que l'on parle d'un mammifère, d'un insecte, d'une plante ou d'une cyanobactérie. L'horloge biologique des mammifère est cependant la plus étudiée, car la plus représentative.
{|
|[[File:Circadian clock of mammals.PNG|vignette|upright=1.8|Horloge génétique des mammifères.]]
|[[File:Circadian clock of drosophila.PNG|vignette|upright=2.4|Horloge génétique d'une drosophile.]]
|}
L'horloge génétique est exprimée dans toutes les cellules du corps, ce qui fait que chacune d'entre elle possède ainsi sa propre rythmicité, qui n'est pas forcément synchronisée avec celles de ses voisines. Il doit fatalement exister un mécanisme de synchronisation qui permet aux cellules d'un organisme de fonctionner à l'unisson. Ce mécanisme sert ainsi d'horloge primaire, qui donne le "LA" aux horloges secondaires de chaque cellule.
Une partie de ce mécanisme est liée à la '''mélatonine''', une hormone qui influence le cerveau, mais aussi le reste du corps de par son action hormonale. Elle se lie à deux types de récepteurs, qui portent les noms de récepteurs MT1 et MT2, qui sont localisées au niveau du noyau suprachiasmatique, de l'hippocampe, du cervelet et des aires dopaminergiques. L'activation des récepteurs MT1 entraine une somnolence, voire un endormissement, ce qui explique l'effet hypnotique de la mélatonine. Par contre, l'activation des récepteurs MT2 entraine une avance de l'horloge biologique.
===Le système de synchronisation du rythme circadien===
[[File:Circadian rhythm.svg|vignette|upright=1|Illustration détaillée du système de régulation des rythmes circadiens.</br>MCRGC : Magno-Cellular Retinal Ganglional Cell, les cellules ganglionnaires de la rétine.</br>RHT : Retino-Hypothalamic-tract, le faisceau rétino-hypothalamique.</br>SCN : Supra-Chiasmatic Nucleus, le noyau suprachiasmatique.</br>Pineal Gland : la glande pinéale.]]
Il existe diverses aires cérébrales impliquées dans la régulation du rythme circadien, qui regroupe :
* les ''cellules ganglionnaires de la rétine'', qui sont sensibles à la lumière bleue et captent la luminosité du soleil ;
* le ''noyau suprachiasmatique'', un noyau de l'hypothalamus qui est l'aire de l'horloge circadienne primaire ;
* la ''glande pinéale'', qui produit la mélatonine, une hormone qui synchronise les cellules.
Le tout est relié par divers faisceaux nerveux, les deux principaux étant :
* le ''faisceau rétino-hypothalamique'', qui relie la rétine au noyau suprachiasmatique ;
* le ''faisceau pinéal'', qui relie le noyau supra-chiasmatique à la glande pinéale.
[[File:Light, suprachiasmatic nuclei (SCN), and the pinealmelatonin circuit.jpg|centre|vignette|upright=2.0|Illustration simplifiée du système de régulation des rythmes circadiens.</br>SCN : Supra-Chiasmatic Nucleus, le noyau suprachiasmatique.</br>Pineal Gland : la glande pinéale.]]
La synchronisation des horloges biologiques commence directement dans la rétine. La rétine contient une classe spéciale de cellules ganglionnaires, les '''cellules ganglionnaires photosensibles''', qui perçoivent la luminosité ambiante sans pour autant servir dans le processus de perception visuelle. Elles contiennent un pigment photorécepteur spécial, la '''mélanopsine''', sensible à la lumière bleue. Ces cellules ganglionnaires photosensibles informent le noyau supra-chiasmatique de l'éclairement. Ces informations auraient une influence directe sur l'horloge biologique génétique des cellules du noyau supra-chiasmatique, mais cette influence est encore mal connue.
L'horloge biologique est localisée dans une structure cérébrale bien précise, un noyau de l'hypothalamus : le '''noyau supra-chiasmatique'''. Les preuves à l'appui de cette affirmation proviennent de l'étude des lésions de cette structure, qui font disparaitre toute rythmicité circadienne. Ces lésions entrainent notamment des dégradations du sommeil, qui devient complétement aléatoire, que ce soit pour les durées de sommeil ou les heures d'endormissement. Mieux : les greffes de noyaux supra-chiasmatique sains permettent de totalement guérir des souris dont le noyau supra-chiasmatique aurait été lésé. De quoi établir la localisation de l'horloge biologique primaire dans ce noyau. Ce noyau reçoit des afférences de la part des cellules ganglionnaires photosensibles de la rétine, qui l'informe de la luminosité. Quand il fait nuit, la rétine stimule directement le noyau suprachiasmatique, ce qui lui indique qu'il fait nuit. Le jour, la rétine ne stimule pas le noyau suprachiasmatique, ce qui lui indique qu'il fait jour. Le noyau suprachiasmatique se remet à l'heure en fonction de se que lui envoie la rétine.
La '''glande pinéale''' a la taille d'un grain de riz et est localisée entre les deux morceaux du thalamus. Elle est présente chez tous les vertébrés, à quelques exceptions près. Elle produit de la mélatonine, qui se disperse dans le corps et le cerveau. Les cellules qui produisent la mélatonine dans la glande pinéale sont des cellules spécialisées qu'on ne retrouve pas ailleurs dans le corps : elles portent le nom de pinéalocyte. Ils synthétisent la mélatonine à partir de sérotonine, d'où l'influence de ce neurotransmetteur dans le sommeil. Les pinéalocytes secrètent la mélatonine produite en réaction à la perception de noradrénaline.
[[File:Sécrétion de mélatonine par les pinéalocytes.png|centre|vignette|upright=2.0|Sécrétion de mélatonine par les pinéalocytes.]]
==Les troubles du rythme circadien==
L'horloge biologique et ses systèmes annexes ne sont clairement pas infaillibles. Toute lésion de ses composants peut entrainer des défaillances, qui se manifestent essentiellement par des troubles du sommeil. Ainsi, les aveugles ont du mal à synchroniser leurs rythmes de sommeil, d'autres personnes ont du mal à s'endormir en soirée, et ainsi de suite. Dans certains cas, ces défaillances sont mineures. Ainsi en est-il de personne qui sont légèrement du soir ou matin, chose relativement banale. Mais dans certains cas, ces troubles entrainement de véritables syndromes cliniques, que tout psychiatre ou médecin de sommeil peut aisément reconnaitre. Ceux-ci sont au nombre de quatre, certains y ajoutant en plus les syndromes de décalage horaire ou de travail de nuit, quoique la dénomination de trouble pour parler de conséquences de faits sociaux puisse porter à débat.
===La classification des maladies du rythme circadien===
Les maladies qui altèrent le rythme circadien sont assez facile à décrire. Elles se caractérisent toutes par des horaires d'endormissement ou de réveil anormales. Par contre, la durée totale de sommeil est normale et la qualité du sommeil est préservée. En clair, il n'y a pas d’insomnie au sens strict. Du moins, c'est le cas si les heures de travail sont adaptées pour coller avec les horaires de lever/coucher. Le patient ne se sent vraiment reposé que si on le laisse dormir à ses horaires anormales, lui imposer des horaires de sommeil "normale" ayant de fortes chances de nuire à la qualité de son sommeil. Ces maladies sont permanentes et perturbent fortement la vie des personnes atteintes, mais ne sont pas vraiment des maladies graves.
{|class="wikitable"
|-
!Retard de phase.
|Le sujet a une horloge retardée, mais dont la période est bien de 24 heures. Cela se traduit par endormissement retardé, qui a souvent lieu après 2 heures du matin, parfois plus. Le sujet a aussi du mal à se réveiller le matin et a tendance à se réveiller tard, vers midi. Le sujet se sent généralement plus productif en soirée.
|[[File:Crsd dspd fr 100.svg|200px|Illustration du rythme sommeil-veille d'un individu atteint de retard de phase.]]
|-
!Avance de phase.
|Le sujet a une horloge qui avance sur l'horaire, mais dont la période est bien de 24 heures. Cela se traduit par endormissement précoce, qui a souvent lieu vers 18 heures de l'après-midi. Le sujet se réveille tôt le matin, vers 4 à 5 heures du matin.
|[[File:Crsd aspd sp fr 100.svg|200px|Illustration du rythme sommeil-veille d'un individu atteint de retard de phase.]]
|-
!Rythme différent de 24 heures.
|Le sujet a un rythme circadien dont la période n'est pas de 24 heures.
|[[File:Crsd frsd fr 100.svg|200px|Illustration du rythme sommeil-veille d'un individu atteint d'un rythme circadien différent de 24 heures.]]
|-
!Absence de rythme circadien.
|Disparition totale de toute rythmicité circadienne.
|[[File:Crsd iswr fr 100.svg|200px|Crsd iswr fr 100]]
|}
Pour ce qui est de la prévalence, le retard de phase est le plus fréquent, suivi par l'avance de phase, puis par les autres troubles du rythme circadien. Pour le retard de phase, on estime que près de 0.2% de la population a un syndrome de retard de phase, avec cependant une prévalence de 5 à 15% chez les adolescents. Pour l'avance de phase, elle est très rare chez les enfants et adolescents, mais sa prévalence augmente avec l'âge. On estime que près de 1% des personnes de plus de 40 ans en sont atteint, surtout des personnes âgées. Pour les autres troubles, ils sont beaucoup plus rares. Le rythme différent de 24 heures est surtout observé chez les aveugles, qui ne peuvent pas profiter de l'effet synchronisant de la lumière du jour. Les cas sans cécité sont très rares et sont surtout observés chez des personnes atteintes de lourdes pathologies neurologiques. Enfin, l'absence de rythme circadien est l'absence de rythme circadien est observée presque exclusivement chez les patients atteints de maladies neurologiques et il est rare de l'observer chez un patient sain. Les patients sont presque toujours des patients déments, atteints d'un Alzheimer ou d'une autre pathologie neurodégénérative, qui en est à un stade très avancé au point où le patient est totalement dépendant.
L'origine de ces troubles varie beaucoup suivant la maladie. L'origine du rythme circadien différent de 24 heure est surtout expliqué par la cécité ou une maladie neurologique sous-jacente. Quant à l'absence de rythme circadien est presque toujours la conséquence d'une maladie neurodégénérative, très rarement d'un traumatisme crânien, d'une tumeur, ou de toute affection neurologique capable de perturber le fonctionnement de l'hypothalamus. Elle survient surtout chez les personnes qui ont subi une lésion des noyaux supra-chiasmatiques. Pour ce qui du retard ou de l'avance de phase, les choses sont plus compliquées. L'origine du retard de phase est mal comprise, mais on estime qu'elle est multifactorielle et implique des facteurs génétiques, environnementaux, sociaux, psychologiques, etc. Des polymorphismes de gènes liés à l'horloge biologique pourraient être impliqués dans la genèse de ce trouble, mais rien de certain à l'heure actuelle. Pour l'avance de phase, c'est la même chose.
Les seuls cas expliqués d'avance de phase sont ceux de la forme "familiale", dont l'origine est une mutation du gène PER2. Ces patients ont une avance de phase assez marquée, qui les fait s'endormir entre 4 et 6 heures plus tôt que la normale. De plus, la période de leur horloge biologique est plus courte que pour les sujets sains : elle est de seulement 22 heures pour les patients atteints, contre un peu plus de 24 heures pour un sujet sain. Les patients touchés ont très souvent un parent touché, de même que des frères et sœurs atteints eux aussi. Le trouble se transmet à la descendance et la maladie a une transmission de type "autosomale dominante" (ce qui veut dire qu'elle est transmise par les chromosomes non-sexuels et que l’allèle concerné n'est pas récessif).
{|class="wikitable"
|-
!
! Origine
! Patient-type
|-
! Retard de phase
| rowspan="2" |
* Multifactorielle, avec potentiellement une forte implication de la génétique.
* Existence d'une forme familiale pour certains cas d'avance de phase.
| Adolescent, plus rarement adulte
|-
! Avance de phase
| Adulte d'un âge avancé.
|-
! Rythme circadien différent de 24 heures
| Cécité.
| Patient totalement aveugle.
|-
! Absence de rythme circadien
| Maladie neurodégénérative avancée, plus rarement lésion à l'hypothalamus.
| Patient dément, hospitalisé et dépendant.
|}
===Les traitements===
Les traitements de ces troubles sont relativement similaires.
Le premier d'entre eux est la '''thérapie comportementale''', utilisée pour les retards de phase, et dans une moindre mesure pour les avances de phase. Cette thérapie pousse le sujet à modifier son comportement pour faciliter un endormissement précoce, limitant le retard de phase (dont une partie a une origine indéniablement comportementale).
La '''luminothérapie''', à savoir l'exposition à une lumière similaire à la lumière du jour à un instant adéquat de la journée, donne aussi de bons résultats. Elle peut être réalisée à l’hôpital (ce qui est assez rare) ou en ambulatoire. Divers dispositifs sont en vente libre pour faciliter le traitement en ambulatoire. Il s'agit de lampes qui émettent une couleur bleue/blanche (le plus souvent une lumière bleue claire), auxquelles on doit s'exposer soit au début de la journée (en cas de retard de phase), soit durant la soirée (en cas d'avance de phase). Il est possible de lire, de regarder une vidéo, de travailler ou de faire autre chose en même temps que l'exposition à la lumière bleue; tant que la lumière de la lampe est dans le champ de vision périphérique du patient (sans quoi elle n'a aucun effet).
Cette thérapie peut être secondée par l''''administration de mélatonine''' au bon instant de la journée, juste avant l'endormissement pour le retard de phase, ou le matin pour l'avance de phase. Divers médicaments peuvent aussi mimer l'effet de la mélatonine, de part leur structure chimique similaire à celle de la mélatonine. De tels agonistes de la mélatonine, sont cependant moins utilisés, certains ayant quelques effets secondaires, ou étant utilisés pour d'autres indications thérapeutiques (comme l'agomélatine, utilisée pour soigner les épisodes dépressifs).
<noinclude>
{{NavChapitre | book=Neurosciences
| prev=La motricité digestive
| prevText=La motricité digestive
| next=L'éveil, le sommeil et la vigilance
| nextText=L'éveil, le sommeil et la vigilance
}}
</noinclude>
{{AutoCat}}
3hczmjp4kox6j5zqs9y8i6744fy5yxb
775321
775320
2026-10-16T16:56:15Z
Mewtow
31375
/* Le système de synchronisation du rythme circadien */
775321
wikitext
text/x-wiki
Vous avez peut-être déjà entendu parler de l'horloge biologique, cette expression métaphorique étant notamment utilisée pour parler des rythmes de sommeil. L'horloge biologique est un terme assez parlant. Il implique que le corps a une certaine "mesure du temps", qui lui fait savoir quand il fait jour, quand il fait nuit. Et ce sans se reposer sur la lumière du jour.
La preuve de l'existence de cette horloge biologique a été faite par la fameuse expérience de Michel Siffre, spéléologue français. Il s'est isolé dans une grotte, aménagée pour y vivre convenablement pendant un mois, isolé du monde extérieur, dans un environnement à température et luminosité constante, sans interactions sociales. De telles expériences permettent à un sujet de ne pas avoir la moindre indication sur le rythme jour-nuit extérieur. Il s'avéra que celui-ci garda un certain rythme journalier, que ce soit pour ses durées et phases d'éveil/sommeil ou pour sa température, et bien d'autres paramètres. Son sommeil était cependant quelque peu dégradé et ses rythmes de sommeil étaient légèrement décalés.
Ces expériences, réalisées aussi bien sur des humains que des animaux, des plantes ou des bactéries, montrent que les sujets gardent une rythmicité circadienne marquée, même sans indices extérieurs. Les scientifiques ont donc déduit que les rythmes circadiens ne sont pas imposés par l'environnement, mais sont générés par quelque chose d'interne au sujet : une '''horloge biologique'''.
En clair, le corps a une horloge biologique dont la période proche de 24 heures, avec cependant un léger décalage. La période de l'horloge biologique diffère quelque peu du rythme journalier de 24 heures, pouvant aller de 22 à 27 heures, suivant les sujets. Ce décalage n'est pas corrigé quand le sujet est placé en isolation et reste relativement constant. Par contre, en milieu naturel, ce décalage semble ne pas exister. Tout se passe comme si l'action de l'environnement permettait de resynchroniser l'horloge biologique avec le cycle jour-nuit réel.
Pour cela, les scientifiques ont postulé que la lumière ambiante, la température et d'autres paramètres comme l'heure des repas, influençaient l'horloge biologique, pour la resynchroniser; Ces évènements sont appelés des '''zeitbegers'''. Ces deux observations sont autant de contraintes qui suffisamment à définir un rythme dit circadien : un rythme endogène, proche de 24 heures, influencé par différents zeitbegers. Mais il faut de plus que ce rythme reste de 24 heures, quelle que soit la température extérieure, certains rythmes biologiques ayant une période variable selon la température.
Outre le sommeil, d'autres fonctions organiques suivent aussi un rythme journalier (nycthéméral diraient certains). Par exemple, notre température corporelle suit un rythme de 24 heures. L'adage veut ainsi que notre température soit maximale au réveil et diminue progressivement durant la journée, atteignant un minimum le soir avant de dormir. Et on pourrait aussi citer les rythmes de la pression sanguine, de la motilité intestinale, de la production d'urine ou de certaines hormones.
De plus, l'espèce humaine n'est pas la seule à avoir de tels rythmes : de nombreux animaux ont aussi un rythme veille-sommeil d'une période de 24 heures, un tel rythme s'observant aussi pour leur température ou d'autres paramètres corporels. Même certaines plantes, ou encore des bactéries, suivent un rythme journalier. Qui plus est ces rythmes sont indépendants de la luminosité, même si la lumière semble resynchroniser des rythmes défaillants. Ces rythmes d'une durée de 24 heures sont appelés des '''rythmes circadiens'''.
[[File:Biological clock human-fr.svg|centre|vignette|upright=3.0|Illustration des différents rythmes circadiens humains.]]
==Anatomie et physiologie de l'horloge biologique==
Cette horloge biologique peut être conceptuellement divisée en plusieurs sous-systèmes, qui correspondent chacun à une caractéristique de l'horloge biologique. Premièrement, on trouve l'horloge elle-même, à savoir quelque chose qui oscille à un rythme de 24 heures. Mais cette horloge est aussi soumise à un mécanisme de resynchronisation, qui capte différents zeitbegers et agit sur la synchronisation de l'horloge (sur sa phase diraient les physiciens). Enfin, cette horloge doit aussi être reliée à un système effecteur qui lui permet d'agir sur divers paramètres, comme la température ou l'activité cérébrale (pour influencer l'état de veille). Horloge biologique, mécanisme de synchronisation et système effecteur sont anatomiquement séparés, du moins dans une certaine mesure.
===L'horloge génétique===
Divers expériences ont montré que des cellules individuelles sont capables de soutenir un rythme circadien, notamment dans la production de certaines protéines. Il en va ainsi de certaines bactéries, qui synthétisent certaines protéines avec une période de presque 24 heures, même soumise à température et luminosité constante. Une première approche consiste donc à chercher le substrat de l'horloge biologique directement dans nos cellules. Et effectivement, toute cellule un tant soit peu évoluée possède un rythme circadien, d'origine génétique. Celui-ci provient d'un processus cyclique, qui fait intervenir des concentration en protéines fluctuante, régulées par l'activation et la désactivation cyclique de gènes. De telles '''horloges génétiques''' sont cependant différentes selon l'espèce abordée : les gènes et protéines en cause ne sont pas les mêmes selon que l'on parle d'un mammifère, d'un insecte, d'une plante ou d'une cyanobactérie. L'horloge biologique des mammifère est cependant la plus étudiée, car la plus représentative.
{|
|[[File:Circadian clock of mammals.PNG|vignette|upright=1.8|Horloge génétique des mammifères.]]
|[[File:Circadian clock of drosophila.PNG|vignette|upright=2.4|Horloge génétique d'une drosophile.]]
|}
L'horloge génétique est exprimée dans toutes les cellules du corps, ce qui fait que chacune d'entre elle possède ainsi sa propre rythmicité, qui n'est pas forcément synchronisée avec celles de ses voisines. Il doit fatalement exister un mécanisme de synchronisation qui permet aux cellules d'un organisme de fonctionner à l'unisson. Ce mécanisme sert ainsi d'horloge primaire, qui donne le "LA" aux horloges secondaires de chaque cellule.
Une partie de ce mécanisme est liée à la '''mélatonine''', une hormone qui influence le cerveau, mais aussi le reste du corps de par son action hormonale. Elle se lie à deux types de récepteurs, qui portent les noms de récepteurs MT1 et MT2, qui sont localisées au niveau du noyau suprachiasmatique, de l'hippocampe, du cervelet et des aires dopaminergiques. L'activation des récepteurs MT1 entraine une somnolence, voire un endormissement, ce qui explique l'effet hypnotique de la mélatonine. Par contre, l'activation des récepteurs MT2 entraine une avance de l'horloge biologique.
===Le système de synchronisation du rythme circadien===
[[File:Circadian rhythm.svg|vignette|upright=1|Illustration détaillée du système de régulation des rythmes circadiens.</br>MCRGC : Magno-Cellular Retinal Ganglional Cell, les cellules ganglionnaires de la rétine.</br>RHT : Retino-Hypothalamic-tract, le faisceau rétino-hypothalamique.</br>SCN : Supra-Chiasmatic Nucleus, le noyau suprachiasmatique.</br>Pineal Gland : la glande pinéale.]]
Il existe diverses aires cérébrales impliquées dans la régulation du rythme circadien, qui regroupe :
* les ''cellules ganglionnaires de la rétine'', qui sont sensibles à la lumière bleue et captent la luminosité du soleil ;
* le ''noyau supra-chiasmatique'', un noyau de l'hypothalamus qui est l'aire de l'horloge circadienne primaire ;
* la ''glande pinéale'', qui produit la mélatonine, une hormone qui synchronise les cellules.
Le tout est relié par divers faisceaux nerveux, les deux principaux étant :
* le ''faisceau rétino-hypothalamique'', qui relie la rétine au noyau supra-chiasmatique ;
* le ''faisceau pinéal'', qui relie le noyau supra-chiasmatique à la glande pinéale.
[[File:Light, suprachiasmatic nuclei (SCN), and the pinealmelatonin circuit.jpg|centre|vignette|upright=2.0|Illustration simplifiée du système de régulation des rythmes circadiens.</br>SCN : Supra-Chiasmatic Nucleus, le noyau suprachiasmatique.</br>Pineal Gland : la glande pinéale.]]
La synchronisation des horloges biologiques commence directement dans la rétine. La rétine contient une classe spéciale de cellules ganglionnaires, les '''cellules ganglionnaires photosensibles''', qui perçoivent la luminosité ambiante sans pour autant servir dans le processus de perception visuelle. Elles contiennent un pigment photorécepteur spécial, la '''mélanopsine''', sensible à la lumière bleue. Ces cellules ganglionnaires photosensibles informent le noyau supra-chiasmatique de l'éclairement. Ces informations auraient une influence directe sur l'horloge biologique génétique des cellules du noyau supra-chiasmatique, mais cette influence est encore mal connue.
L'horloge biologique est localisée dans une structure cérébrale bien précise, un noyau de l'hypothalamus : le '''noyau supra-chiasmatique'''. Les preuves à l'appui de cette affirmation proviennent de l'étude des lésions de cette structure, qui font disparaitre toute rythmicité circadienne. Ces lésions entrainent notamment des dégradations du sommeil, qui devient complétement aléatoire, que ce soit pour les durées de sommeil ou les heures d'endormissement. Mieux : les greffes de noyaux supra-chiasmatique sains permettent de totalement guérir des souris dont le noyau supra-chiasmatique aurait été lésé. De quoi établir la localisation de l'horloge biologique primaire dans ce noyau. Ce noyau reçoit des afférences de la part des cellules ganglionnaires photosensibles de la rétine, qui l'informe de la luminosité. Quand il fait nuit, la rétine stimule directement le noyau supra-chiasmatique, ce qui lui indique qu'il fait nuit. Le jour, la rétine ne stimule pas le noyau supra-chiasmatique, ce qui lui indique qu'il fait jour. Le noyau supra-chiasmatique se remet à l'heure en fonction de se que lui envoie la rétine.
La '''glande pinéale''' a la taille d'un grain de riz et est localisée entre les deux morceaux du thalamus. Elle est présente chez tous les vertébrés, à quelques exceptions près. Elle produit de la mélatonine, qui se disperse dans le corps et le cerveau. Les cellules qui produisent la mélatonine dans la glande pinéale sont des cellules spécialisées qu'on ne retrouve pas ailleurs dans le corps : elles portent le nom de pinéalocyte. Ils synthétisent la mélatonine à partir de sérotonine, d'où l'influence de ce neurotransmetteur dans le sommeil. Les pinéalocytes secrètent la mélatonine produite en réaction à la perception de noradrénaline.
[[File:Sécrétion de mélatonine par les pinéalocytes.png|centre|vignette|upright=2.0|Sécrétion de mélatonine par les pinéalocytes.]]
Tous les vertébrés ont un noyau supra-chiasmatique, une glande pinéale, et une rétine sensible à la lumière. Cependant, le câblage et la fonction de ces structures ne sont exactement les mêmes d'une espèce à l'autre. Il y a notamment une différence entre mammifères et oiseaux. Chez les mammifères, l'horloge biologique est dans le noyau supra-chiasmatique, la glande pinéale est totalement sous les ordres du noyau supra-chiasmatique, la rétine permet au noyau supra-chiasmatique de se synchroniser avec le rythme jour-nuit. Mais chez les oiseaux, tout est plus compliqué, et plus intéressant.
Chez les oiseaux, le noyau supra-chiasmatique est subdivisé en deux : une section sensibles aux ''inputs'' de la rétine, une autre qui exprime l'horloge biologique proprement dite, la première synchronise la seconde. Mais aussi et surtout : la glande pinéale a sa propre horloge biologique, au même titre que le noyau supra-chiasmatique. Leur glande pinéale n'est pas une simple glande qui sécrète de la mélatonine, contrairement à ce qu'elle est chez les mammifères. Elle est sensible à la lumière, ce qui lui permet d'entrainer la sécrétion de mélatonine en fonction du rythme jour-nuit. Et ce n'est pas une fonction inutile : chez les petits oiseaux, la lumière du jour pénètre un peu à travers les plumes et les os du crâne, suffisamment pour agir sur la glande pinéale. La glande pinéale perçoit donc directement la lumière du jour !
Les deux horloges biologiques se synchronisent en s’inhibant mutuellement. Le rapport de force entre noyau supra-chiasmatique et glande pinéale dépend de l'espèce. Chez les pigeons, retirer la glande pinéale laisse des rythmes circadiens, certes dégradés, alors qu'une lésion du noyau supra-chiasmatique abolit totalement tout rythme circadien. La glande pinéale est donc secondaire chez ces oiseaux. Par contre, chez les oiseaux-chanteurs, une lésion de la glande pinéale abolit totalement les rythmes circadiens, ce qui n'est pas le cas pour les lésions du noyau supra-chiasmatique. le rapport de force est donc inversé en faveur de la glande pinéale, chez les oiseaux-chanteurs. Chez les hiboux et chouettes, la glande pinéale est elle très réduite, proche de e qu'on a chez les mammifères.
==Les troubles du rythme circadien==
L'horloge biologique et ses systèmes annexes ne sont clairement pas infaillibles. Toute lésion de ses composants peut entrainer des défaillances, qui se manifestent essentiellement par des troubles du sommeil. Ainsi, les aveugles ont du mal à synchroniser leurs rythmes de sommeil, d'autres personnes ont du mal à s'endormir en soirée, et ainsi de suite. Dans certains cas, ces défaillances sont mineures. Ainsi en est-il de personne qui sont légèrement du soir ou matin, chose relativement banale. Mais dans certains cas, ces troubles entrainement de véritables syndromes cliniques, que tout psychiatre ou médecin de sommeil peut aisément reconnaitre. Ceux-ci sont au nombre de quatre, certains y ajoutant en plus les syndromes de décalage horaire ou de travail de nuit, quoique la dénomination de trouble pour parler de conséquences de faits sociaux puisse porter à débat.
===La classification des maladies du rythme circadien===
Les maladies qui altèrent le rythme circadien sont assez facile à décrire. Elles se caractérisent toutes par des horaires d'endormissement ou de réveil anormales. Par contre, la durée totale de sommeil est normale et la qualité du sommeil est préservée. En clair, il n'y a pas d’insomnie au sens strict. Du moins, c'est le cas si les heures de travail sont adaptées pour coller avec les horaires de lever/coucher. Le patient ne se sent vraiment reposé que si on le laisse dormir à ses horaires anormales, lui imposer des horaires de sommeil "normale" ayant de fortes chances de nuire à la qualité de son sommeil. Ces maladies sont permanentes et perturbent fortement la vie des personnes atteintes, mais ne sont pas vraiment des maladies graves.
{|class="wikitable"
|-
!Retard de phase.
|Le sujet a une horloge retardée, mais dont la période est bien de 24 heures. Cela se traduit par endormissement retardé, qui a souvent lieu après 2 heures du matin, parfois plus. Le sujet a aussi du mal à se réveiller le matin et a tendance à se réveiller tard, vers midi. Le sujet se sent généralement plus productif en soirée.
|[[File:Crsd dspd fr 100.svg|200px|Illustration du rythme sommeil-veille d'un individu atteint de retard de phase.]]
|-
!Avance de phase.
|Le sujet a une horloge qui avance sur l'horaire, mais dont la période est bien de 24 heures. Cela se traduit par endormissement précoce, qui a souvent lieu vers 18 heures de l'après-midi. Le sujet se réveille tôt le matin, vers 4 à 5 heures du matin.
|[[File:Crsd aspd sp fr 100.svg|200px|Illustration du rythme sommeil-veille d'un individu atteint de retard de phase.]]
|-
!Rythme différent de 24 heures.
|Le sujet a un rythme circadien dont la période n'est pas de 24 heures.
|[[File:Crsd frsd fr 100.svg|200px|Illustration du rythme sommeil-veille d'un individu atteint d'un rythme circadien différent de 24 heures.]]
|-
!Absence de rythme circadien.
|Disparition totale de toute rythmicité circadienne.
|[[File:Crsd iswr fr 100.svg|200px|Crsd iswr fr 100]]
|}
Pour ce qui est de la prévalence, le retard de phase est le plus fréquent, suivi par l'avance de phase, puis par les autres troubles du rythme circadien. Pour le retard de phase, on estime que près de 0.2% de la population a un syndrome de retard de phase, avec cependant une prévalence de 5 à 15% chez les adolescents. Pour l'avance de phase, elle est très rare chez les enfants et adolescents, mais sa prévalence augmente avec l'âge. On estime que près de 1% des personnes de plus de 40 ans en sont atteint, surtout des personnes âgées. Pour les autres troubles, ils sont beaucoup plus rares. Le rythme différent de 24 heures est surtout observé chez les aveugles, qui ne peuvent pas profiter de l'effet synchronisant de la lumière du jour. Les cas sans cécité sont très rares et sont surtout observés chez des personnes atteintes de lourdes pathologies neurologiques. Enfin, l'absence de rythme circadien est l'absence de rythme circadien est observée presque exclusivement chez les patients atteints de maladies neurologiques et il est rare de l'observer chez un patient sain. Les patients sont presque toujours des patients déments, atteints d'un Alzheimer ou d'une autre pathologie neurodégénérative, qui en est à un stade très avancé au point où le patient est totalement dépendant.
L'origine de ces troubles varie beaucoup suivant la maladie. L'origine du rythme circadien différent de 24 heure est surtout expliqué par la cécité ou une maladie neurologique sous-jacente. Quant à l'absence de rythme circadien est presque toujours la conséquence d'une maladie neurodégénérative, très rarement d'un traumatisme crânien, d'une tumeur, ou de toute affection neurologique capable de perturber le fonctionnement de l'hypothalamus. Elle survient surtout chez les personnes qui ont subi une lésion des noyaux supra-chiasmatiques. Pour ce qui du retard ou de l'avance de phase, les choses sont plus compliquées. L'origine du retard de phase est mal comprise, mais on estime qu'elle est multifactorielle et implique des facteurs génétiques, environnementaux, sociaux, psychologiques, etc. Des polymorphismes de gènes liés à l'horloge biologique pourraient être impliqués dans la genèse de ce trouble, mais rien de certain à l'heure actuelle. Pour l'avance de phase, c'est la même chose.
Les seuls cas expliqués d'avance de phase sont ceux de la forme "familiale", dont l'origine est une mutation du gène PER2. Ces patients ont une avance de phase assez marquée, qui les fait s'endormir entre 4 et 6 heures plus tôt que la normale. De plus, la période de leur horloge biologique est plus courte que pour les sujets sains : elle est de seulement 22 heures pour les patients atteints, contre un peu plus de 24 heures pour un sujet sain. Les patients touchés ont très souvent un parent touché, de même que des frères et sœurs atteints eux aussi. Le trouble se transmet à la descendance et la maladie a une transmission de type "autosomale dominante" (ce qui veut dire qu'elle est transmise par les chromosomes non-sexuels et que l’allèle concerné n'est pas récessif).
{|class="wikitable"
|-
!
! Origine
! Patient-type
|-
! Retard de phase
| rowspan="2" |
* Multifactorielle, avec potentiellement une forte implication de la génétique.
* Existence d'une forme familiale pour certains cas d'avance de phase.
| Adolescent, plus rarement adulte
|-
! Avance de phase
| Adulte d'un âge avancé.
|-
! Rythme circadien différent de 24 heures
| Cécité.
| Patient totalement aveugle.
|-
! Absence de rythme circadien
| Maladie neurodégénérative avancée, plus rarement lésion à l'hypothalamus.
| Patient dément, hospitalisé et dépendant.
|}
===Les traitements===
Les traitements de ces troubles sont relativement similaires.
Le premier d'entre eux est la '''thérapie comportementale''', utilisée pour les retards de phase, et dans une moindre mesure pour les avances de phase. Cette thérapie pousse le sujet à modifier son comportement pour faciliter un endormissement précoce, limitant le retard de phase (dont une partie a une origine indéniablement comportementale).
La '''luminothérapie''', à savoir l'exposition à une lumière similaire à la lumière du jour à un instant adéquat de la journée, donne aussi de bons résultats. Elle peut être réalisée à l’hôpital (ce qui est assez rare) ou en ambulatoire. Divers dispositifs sont en vente libre pour faciliter le traitement en ambulatoire. Il s'agit de lampes qui émettent une couleur bleue/blanche (le plus souvent une lumière bleue claire), auxquelles on doit s'exposer soit au début de la journée (en cas de retard de phase), soit durant la soirée (en cas d'avance de phase). Il est possible de lire, de regarder une vidéo, de travailler ou de faire autre chose en même temps que l'exposition à la lumière bleue; tant que la lumière de la lampe est dans le champ de vision périphérique du patient (sans quoi elle n'a aucun effet).
Cette thérapie peut être secondée par l''''administration de mélatonine''' au bon instant de la journée, juste avant l'endormissement pour le retard de phase, ou le matin pour l'avance de phase. Divers médicaments peuvent aussi mimer l'effet de la mélatonine, de part leur structure chimique similaire à celle de la mélatonine. De tels agonistes de la mélatonine, sont cependant moins utilisés, certains ayant quelques effets secondaires, ou étant utilisés pour d'autres indications thérapeutiques (comme l'agomélatine, utilisée pour soigner les épisodes dépressifs).
<noinclude>
{{NavChapitre | book=Neurosciences
| prev=La motricité digestive
| prevText=La motricité digestive
| next=L'éveil, le sommeil et la vigilance
| nextText=L'éveil, le sommeil et la vigilance
}}
</noinclude>
{{AutoCat}}
4prgtngly8azc6b8m497u8kbnavx22n
Neurosciences/L'activité électrique du cerveau
0
69437
775322
762831
2026-10-16T17:01:50Z
Mewtow
31375
775322
wikitext
text/x-wiki
Le cerveau a une activité électrique relativement soutenue : les neurones sont parcourus par des potentiels d'action, qui ne sont ni plus ni moins que des signaux électriques. L'ensemble des potentiels d'action à un moment donné causent une activité électrique mesurable à la surface du crâne, de l'ordre de quelques microvolts. Cette activité électrique cérébrale peut se mesurer avec un instrument composé de plusieurs électrodes placées à la surface du crâne : l''''électroencéphalographe''', ou EEG. Celui-ci est utilisé pour étudier le fonctionnement du cerveau, diagnostiquer certaines maladies qui modifient l'activité électrique cérébrale et observer le déroulement du sommeil.
Dans ce chapitre, nous allons parler de l'activité électrique normale du cerveau, ainsi que d'une pathologie liée l’'''épilepsie'''. L'activité électrique subit aussi des variations au cours de la veille et du sommeil. Mais nous laissons ces variations pour le chapitre sur le sommeil : ce chapitre se limite à quelques généralités et à l’épilepsie.
==L'activité électrique cérébrale non-pathologique==
Si l'ensemble du cerveau a une activité électrique, celle-ci n'est pas tout à fait uniforme : si une aire cérébrale s'active plus qu'une autre, les variations de potentiel seront plus importantes près de cette aire cérébrale que sur une surface éloignée. Généralement, seules les aires cérébrales du cortex, situées sous la surface du crâne, donnent des variations significatives sur l'EEG : le signal des autres aires cérébrales est atténué par le cortex situé au-dessus. Pour localiser la source de l'activité cérébrale, la mesure du potentiel s'effectue souvent avec plusieurs électrodes disposées régulièrement sur la surface du crâne.
Les mesures montrent que l'activité électrique est quelque peu structurée et possède quelques régularités assez visibles sur les mesures à l'EEG. Contrairement à ce qu'on pourrait croire, les neurones ont tendance à synchroniser leurs émissions, à émettre plus ou moins en même temps. Sur l'EEG, cela se traduit par une activité cyclique, périodique (à quelques variations près) : on observe des '''rythmes cérébraux''', parfois improprement appelés ondes cérébrales. Au-dessus de ces rythmes, on observe des fluctuations aléatoires de l'activité électrique. Parmi ces fluctuations, certaines sont causées par une perception extérieure ou une activité intellectuelle : ce sont des '''potentiels évoqués'''.
[[File:SimulationNeuralOscillations.png|centre|vignette|upright=1.5|Illustration des rythmes cérébraux, en fonction des potentiels d'action individuels émis par chaque neurone cortical.]]
===Les rythmes cérébraux===
Chaque rythme cérébral a une fréquence bien particulière, et on classe arbitrairement les ondes cérébrales dans 4 à 6 bandes de fréquences nommées alpha, bêta, gamma, delta, et thêta. Les scientifiques ont remarqué que chaque bande de fréquence correspond à des niveaux d'éveil et de sommeil différents. De plus, ces ondes cérébrales sont localisées dans des zones différentes du cerveau : les ondes gamma et bêta sont essentiellement localisées dans les lobes frontaux, alors que les rythmes Alpha sont localisées dans le cortex sensoriel et moteur.
{|class="wikitable"
|-
!Onde cérébrale
!Bande de fréquence
|-
!Delta
|Inférieur à 4 hertz
|-
!Thêta
|4 à 7 hertz
|-
!Alpha
|8 à 15 hertz
|-
!Bêta
|16 à 31 hertz
|-
!Gamma
|Plus de 32 hertz
|}
Une activité rythmique peut s'observer à de nombreux niveau : au niveau d'un neurone isolé, au niveau d'un petit réseau de neurones, au niveau du cerveau.
Pour ce qui est des neurones isolés, il existe des '''neurones pacemakers''' émettent des potentiels d'action à des intervalles réguliers, même s'ils ne sont pas stimulés par d'autres neurones.
Pour les petits réseaux de neurones, composés de quelques centaines ou milliers de neurones, voire plus, des mécanismes de synchronisation peuvent se mettre en place par les synapses. Vu qu'un neurone tend à ^émettre un potentiel d'action quand il est stimulé par un autre, deux neurones reliés entre eux par une synapse tendent à émettre des potentiels d'actions en même temps. Dans une assemblée de neurones fortement reliés entre eux, ce mécanisme tend à synchroniser les neurones entre eux, de sorte qu'ils émettent des potentiels d'action plus ou moins en même temps.
Pour ce qui est du niveau cérébral, les rythmes cérébraux trouvent leur origine dans le fait que certaines aires cérébrales sont reliées et forment des boucles dans lesquelles des influx nerveux se propagent cycliquement. Les boucles les plus importantes relient les neurones du cortex au thalamus, d'où leur nom de '''boucles thalamico-corticales'''. Ces boucles thalamico-corticales sont au nombre de deux, la boucle ventrobasale impliquant le thalamus ventrolatéral (à la base et sur le côté) alors que la boucle centrolatérale implique le thalamus centrolatéral (au centre et sur l'intérieur). La boucle ventrobasale s'occupe du traitement des informations sensorielles, alors que la boucle centrolatérale dédiée à la conscience et à la pensée. Lors de l'éveil, les sensations rentrent librement dans la boucle ventrobasale. Mais lors du sommeil, le thalamus ventrobasal ne laisse pas passer les informations sensorielles : les deux boucles thalamico-corticales sont en circuit fermé. Ce comportement est commandé par des neurones réticulaires, eux-mêmes commandés par la formation réticulée (une aire cérébrale dédiée à la gestion de l'éveil, de l'attention, et de l'activation physiologique).
===Les potentiels évoqués===
[[File:ComponentsofERP.svg|vignette|Exemple d'ERP.]]
L'étude des potentiels évoqués est plus complexe, vu qu'ils sont noyés dans l'activité électrique aléatoire de base. Distinguer ce qui vient des rythmes, de l'activité aléatoire et potentiels évoqués est quelque peu compliqué. La méthode la plus utilisée est de répéter les mesures sur un grand nombre de sujets et de faire la moyenne entre elles. Ce faisant, les variations aléatoires sont annulées, de même que les rythmes (du fait de leur déphasage), alors que les potentiels évoqués restent. On voit alors que certains stimulus déclenchent systématiquement des potentiels évoqués après un délai fixe. Par exemple, quand on capte un stimulus verbal, un potentiel évoqué apparait environ 400 millisecondes après sa perception Ce stimulus a reçu son propre nom et est étudié pour sa relation avec les processus de traitement du langage. C'est d'ailleurs loin d'être le seul : de nombreux potentiels évoqués ont reçu un nom et ont été identifié comme entités à étudier.
Les potentiels évoqués peuvent être des potentiels positifs, où la tension augmente comparé à son niveau de base, ou des potentiels négatifs où la tension diminue. Cela se retient dans la notation des potentiels évoqués, qui en tient compte. La notation de ces potentiels est composée d'une lettre suivie d'un nombre. La lettre indique si le potentiel est positif ou négatif : P pour positif et N pour négatif. Le nombre est le délai entre la présentation du stimulus et l'apparition du potentiel évoqué. Pour donner un exemple, reprenons le potentiel évoqué mentionné précédemment, où un stimulus verbal déclenche un potentiel négatif 400 ms après : ce potentiel est appelé le potentiel N400.
==Les crises épileptiques==
Les crises épileptiques sont des crises où l'activité cérébrale s'emballe dans une portion plus ou moins importante du cerveau. L'idée que se fait le grand public se fait d'une crise d'épilepsie ressemble globalement à une perte de connaissance avec des convulsions. Pour rappel, les convulsions sont des contractions musculaires involontaires et assez violentes, qui peuvent toucher toute partie du corps. De telles crises existent, sont très fréquentes, et correspondent à un cas où une crise d'épilepsie embrase tout le cerveau. Mais il existe des crises épileptiques sans convulsions, ou sans perte de conscience, voire avec des symptômes sensoriels, cognitifs, et autres. Malgré la présentation très variée des crises épileptiques, leur mécanisme est le même : l'activité électrique cérébrale s'emballe. Cela se voit bien lorsqu'on fait passer en EEG pendant une crise, comme montré ci-dessous.
[[File:Spike-waves.png|centre|vignette|upright=2|EEG d'une crise d'épilepsie : le patient déclenche une crise au beau milieu de l'EEG. On voit le changement de l'activité électrique, clairement visible sur l'EEG.]]
La présentation symptomatique d'une crise épileptique est très variable et la classification des crises épileptiques est assez complexe, comme vous allons le voir dans ce qui suit. Pour simplifier le diagnostic, les médecins ont classé les épilepsies selon leurs symptômes,. La classification de ce type la plus connue est l'''International classification of seizure types'', datée de 1981. Elle a ensuite été suivie par la ''ILAE classification of seizure types 2017'', plus moderne. Dans cette dernière, la classification de ces crises se fait sur trois points, auquel nous allons ajouter un quatrième.
* Le premier point est si la crise est focale ou généralisée, à savoir si elle touche seulement un hémisphère cérébral ou tout le cerveau.
* Le second point est si la crise s’accompagne ou non d'une perte de conscience.
* Le troisième point est les symptômes moteurs qui se manifestent lors de la crise.
===Les manifestations motrices d'une crise épileptique===
Les crises épileptiques impliquent souvent des manifestations motrices. Dans les grandes lignes, on distingue les convulsions, les pertes de tonus musculaire, et les automatismes moteurs.
La plupart impliquent des '''convulsions''', des contractions musculaires involontaires, même si ce n'est pas systématique. On distingue trois types de convulsions :
* les ''convulsions toniques'', où la contraction est prolongée avec une contraction complète des muscles :
* les ''convulsions cloniques'' où les contractions sont très courtes et se succèdent rapidement ;
* les ''convulsions myocloniques'' sont des convulsions cloniques très courtes, de moins de quelques secondes, parfois moins de la seconde.
Une crise épileptique convulsive peut présenter soit des convulsions cloniques, soit des convulsions toniques, soit des convulsions myocloniques, soit une succession des trois. D'autres crises n'impliquent pas de convulsions, qu'elles soient toniques ou cloniques. Certaines crises épileptiques manifestent un mélange de plusieurs types de convulsions, ou une succession de celles-ci. Les myoclonies sans pertes de consciences ne sont pas exclusives aux crises épileptiques. Certains patients sains en ont avant de s'endormir sans que cela ne soit le signe d'une quelconque maladie.
Il peut aussi y avoir une '''atonie''', à savoir une perte totale de tonus musculaire, avec ou sans perte de conscience. Elle dure quelques secondes, survient soudainement, et touche souvent un côté du corps ou un membre. Dans le cas le plus sévère, le patient chute brutalement et perd tout contrôle musculaire. Il perd connaissance dans la plupart des cas, mais pas toujours. Dans d'autres, la perte de tonus musculaire ne touche qu'un bras, qu'une jambe, un seul côté du corps.
À l'inverse, d'autres crises épileptiques se caractérisent par des '''spasmes épileptiques'''. Un spasme est une contraction musculaire spontanée, soudaine, parfois douloureuse. De tels spasmes d’origine épileptique touchent souvent les bras, jambes, mains et poignets. Typiquement, on observe le plus souvent une extension des bras ou des jambes, ou encore du poignet. Ils sont plus fréquents chez les enfants, où ils sont appelés des spasmes infantiles. Le patient garde souvent conscience lors de crises de spasmes épileptiques, mais ce n'est clairement pas systématique.
Le patient peut aussi effectuer des mouvements complexes sont appelés des '''automatismes'''. Le sujet peut par exemple effectuer des mouvements simples, comme se gratter, mastiquer sa nourriture, déambuler, marcher en ligne droite, etc. Mais il peut aussi faire des choses bien plus compliquées, comme déplacer un meuble par exemple.
Tout cela permet de distinguer plusieurs types purs de crises épileptiques, dans lesquels on n'observe qu'une manifestation motrice précise, pas un mélange de plusieurs :
* les crises toniques ;
* les crises cloniques ;
* les crises myocloniques;
* les crises atoniques ;
* les crises d'automatisme ;
* les crises de spasmes épileptiques.
===La conscience lors d'une crise d'épilepsie===
Les crises épileptiques sont souvent accompagnées d'une '''altération de la conscience''' bien que ce ne soit pas systématique. Cela peut aller d'une perte de connaissance complète, à une altération plus légère de la conscience avec amnésie. Il arrive que le sujet continue à effectuer des mouvements ou certains automatismes alors que sa conscience est altérée, voire abolie. Le sujet peut alors ressembler à quelqu'un qui fait une crise de somnambulisme, même si ce n'est vrai qu'en apparence.
Le critère de la perte de conscience est encore utilisé dans la classification des crises épileptiques. Mais dans certains cas, il est difficile de savoir si le patient a perdu conscience, notamment si les crises sont très courtes et ne durent que quelques secondes. Il existe des crises d'épilepsie qui se manifestent seulement par une perte de conscience isolée, sans symptôme moteur. Il s'agit des '''crises d'absence'''. Durant ces crises, le sujet devient mutique et immobile durant un temps inférieur à la minute. Ces crises ne durent que quelques dizaines de secondes, rarement plus. La survenue de la crise d'absence est soudaine, son entourage remarque qu'il cesse brusquement ce qu'il était en train de faire, voit que le patient regarde dans le vide. Le patient reste debout, mais ne réagit à aucun stimulus : l'appeler par son nom ou le toucher est inutile. La crise cesse généralement aussi rapidement qu'elle a commencé.
===Les crises focales et généralisées===
Les classifications des crises épileptiques distinguent en premier lieu crise focale et généralisée. La différence entre les deux tient au fait que l'embrasement cérébral est soit limité à une portion limitée du cerveau, soit envahit tout le cerveau. Les '''crises focales''' touchent un seul hémisphère, parfois seulement partiellement, tandis que les '''crises généralisées''' touchent les deux hémisphères, tout le cerveau. Il faut cependant signaler que les crises focales peuvent évoluer vers une crise généralisée, bien que ce ne soit pas systématique.
Les crises focales ont vraisemblablement une origine assez différente des crises généralisées. Les crises focales naissent à partir d'un amas/réseau de neurones hyper-excitable, appelé '''foyer épileptique'''. Ce foyer peut naitre à la suite d'une lésion, peu importe que celle-ci provienne d'un traumatisme crânien ou d'un AVC, mais certaines lésions naissent spontanément, sans origine déterminée. Les crises naissent quand ce foyer épileptique manifeste une bouffée spontanée de potentiels d'action. L'hyperactivité électrique va ensuite se propager aux alentours du foyer, passant de synapses en synapses et peut finir par toucher une grande partie du cerveau. Dans la majorité des cas, la bouffée épileptique reste confinée dans une zone épileptogéne assez limitée : la crise ne se propage pas bien loin dans le cerveau. Mais dans d'autres cas, plus rares, la crise peut se généraliser à l'ensemble du cerveau : la crise focale évolue alors en crise généralisée. Ce phénomène fait que les chercheurs classifient les crises focales selon qu'elles se généralisent ou non.
Les crises généralisées touchent les deux hémisphères, et entrainent généralement des pertes de conscience, accompagnée ou non de manifestations motrices. La '''crise tonique-clonique''' est la présentation auquel le grand public pense quand on évoque une crise généralisée. Elle se déroule en plusieurs phases successives très caractéristiques :
* La première phase, appelée '''phase tonique''' se traduit par une contraction complète des muscles, ainsi qu'une perte de connaissance totale. Le patient tombe alors par terre, sans même essayer de se rattraper. Plus inquiétant, sa respiration s'arrête durant la phase tonique. Dans le cas où la crise se poursuit trop longtemps, le sujet finit même par avoir le teint bleuté à la fin de la crise tonique. Mais fort heureusement, la crise tonique ne dure que quelques secondes à quelques minutes.
* Vient ensuite la '''phase clonique''', où le patient est pris de contractions et de soubresauts aléatoires. La respiration reprend, mais est totalement désorganisée et son rythme est totalement aléatoire. Le patient reste inconscient.
* Puis vient la phase de '''coma''' : le patient reste inconscient, mais sa respiration reprendre normalement et ses muscles sont totalement décontractés, sans tonus. La couleur du patient revient progressivement à la normale.
* Enfin, la crise cesse et le patient reprend conscience. S’ensuit une période où le patient est confus, ne se souvient plus de la crise, a mal à la tête.
===Les classifications des crises épileptiques===
L{{'}}''International classification of seizure types'' de 1981 et l{{'}}''ILAE classification of seizure types 2017'' classent les crises épileptiques différemment.
Mais les deux classifications ont beaucoup en commun, et distinguent crises d'absence, crises atones, et crises avec convulsions.
====L{{'}}''International classification of seizure types'' de 1981====
L{{'}}''International classification of seizure types'' de 1981 distingue les crises généralisées et partielles.
Le classement des crises généralisées distingues celles avec et sans convulsions.
{|class="wikitable"
|+ ''International classification of seizure types'', 1981
|-
! colspan="2" | Type de crise épileptique généralisée
! Description
! Perte de conscience
|-
! rowspan="4" | Crises convulsives
! Crise clonique
| rowspan="2" | Convulsions cloniques
| Perte de conscience
|-
! Crise myoclonique
| Pas de perte de conscience
|-
! Crise tonique
| Convulsions toniques durant plusieurs minutes/heures
| Très fréquente mais pas systématique.
|-
! Crise tonique-clonique
| colspan="2" | Crise en trois temps : une crise clonique avec perte de conscience, suivie par une crise tonique, elle-même suivie par un coma temporaire de quelques minutes.
|-
| colspan="3" |
|-
! rowspan="2" | Crises non-convulsives
! Crise d'absence
| colspan="2" | Interruption de la conscience, sans chute.
|-
! Crise atone
| Perte totale du tonus musculaire
| Très fréquente mais pas systématique.
|}
Les crises focales pures sont elles subdivisées en deux sous-types selon que la conscience est abolie lors de la crise ou non : on parle de crises focales simples si la conscience n'est pas touchée, et de crise complexe quand la conscience est altérée. Et quand on parle d'altération de la conscience lors d’une crise focale complexe, cela ne signifie pas que le sujet perd conscience, ni même qu'il reste inactif (même si c'est fréquent).
{|class="wikitable"
|-
! Type de crise épileptique focale
! Description
! Sous-types
|-
! Crise simple
| Pas d'altération de la conscience.
| Les symptômes sont assez différents selon la localisation des aires touchées par la crise. On distingue ainsi :
* les crises avec des symptômes moteurs ;
* les crises avec des symptômes sensoriels ;
* les crises avec des symptômes comportementaux ;
* les crises avec des symptômes liés au système nerveux autonome.
|-
! Crise complexe
| Altération de la conscience.
| La classification des crises complexes se borne à faire la distinction entre :
* crises où la perturbation de la conscience apparait au début de la crise (crises complexes pures) ;
* et celles où la conscience disparait pendant la crise (crises simples qui évoluent en crises complexes).
|}
Les crises focales qui évoluent en crises généralisées sont un second type de crises focales. Elles sont classées en trois sous-types suivant leur évolution :
* crises simples qui évoluent vers une crise généralisée ;
* crises complexes qui évoluent en crise généralisée ;
* crises simples qui évoluent en crises complexes, qui elle-mêmes évoluent en crise généralisée ;
====L{{'}}''ILAE classification of seizure types 2017''====
Dans l{{'}}''ILAE classification of seizure types 2017'', la classification est beaucoup plus simple. Elle distingue les crises focales des crises généralisées, avec un troisième type pour les crises dont le caractère focal/généralisé est inconnu, et subdivise ces trois types en crises motrices et non-motrices. Pour les crises focales, on ajoute un critères quant à l'altération de conscience ou son absence.
L{{'}}''ILAE classification of seizure types 2017'' distingue les crises généralisées avec symptômes moteurs et celles sans. La différence est que certaines crises d'épilepsies se caractérisent par des contractures musculaires, mais sans convulsions proprement dit. Les crises généralisées non-motrices ne sont autre que les crises d'absence proprement dit. Les crises généralisées motrices regroupent les crises convulsives, les crises atoniques et quelques autres. Pour les crises généralisées, on oublie le critère sur l'altération de la conscience, car la conscience est très souvent altérée dans les crises généralisées. Ajouter une subdivision suivant une éventuelle perte de conscience serait donc redondant.
{|class="wikitable"
|+ ''ILAE classification of seizure types 2017'', Types de crises épileptiques généralisées
|-
! rowspan="10" | Crises motrices
|-
| Crise tonique-clonique
|-
| Crise tonique
|-
| Crise clonique
|-
| Crise myoclonique
|-
| Crise atonique
|-
| Crise myoclonique-atonique
|-
| Crise tonique-clonique-atonique
|-
| Spasme épileptiques
|-
| Autres
|-
| colspan="3" |
|-
! rowspan="4" | Crises non-motrices (crise d'absence)
| typique
|-
| Atypique
|-
| Myoclonique
|-
| Myoclonies limitées aux yeux (''eyelid myoclonia'')
|}
Pour les crises focales, elle mesure deux critères : s'il y a ou non altération de la conscience, et les crises avec ou sans symptômes moteurs. Les crises motrices sont classées suivant le symptôme moteur observé, les manifestations motrices manifestées. On a ainsi des crises focales clonique, toniques, atoniques, myocloniques, à automatisme, avec spasmes épileptiques, etc. Les crises non-motrices, sans manifestations motrices, sont classées de la même manière que les crises simples, avec les crises avec symptômes sensoriels, cognitifs, émotionnels, autonomiques, et un cinquième type de crise appelé les crises avec arrêt comportemental (le patient s’arrête de faire ce qu'il faisait durant quelques secondes/minutes).
L'avantage de cette classification est qu'elle reconnait que des crises clonique, toniques, atoniques, myocloniques, à automatisme peuvent être focales et ne sont pas systématiquement généralisées. La distinction entre crises simples et complexes, peu parlante pour le grand public, est aussi supprimée, car peu utile en pratique. Les crises peuvent se généraliser, mais la classification ne garde qu'un cas, du fait de la suppression de la distinction crise simple/complexe : celui où une crise focale se généralise, typiquement en crise tonico-clonique.
==L'épilepsie==
L{{'}}'''épilepsie''' est une maladie chronique qui se manifeste par des crises d'épilepsies récurrentes. Un point important est qu'une simple crise épileptique ne suffit pas à diagnostiquer une épilepsie. Des crises isolées sont possibles chez des patients non-épileptiques, par exemple parce qu'ils ont une maladie cérébrale qui cause des crises. C'est un point important dans le diagnostic d'une épilepsie : cette maladie est considérée comme étant une maladie chronique et de long-terme, qu'on peut certes guérir, là où une crise unique est peut-être un évènement temporaire , un effet secondaire de médicament, le symptôme temporaire d'une autre maladie, etc. Pour éviter de diagnostiquer des épilepsies à tord et à travers, le neurologue doit éliminer toute maladie pouvant causer de telles crises, ainsi qu'une origine médicamenteuse. De plus, les crises épileptiques doivent se répéter pour que le diagnostic d'épilepsie soit posé, et les crises doivent être séparées par un intervalle de temps suffisant (typiquement 24 heures, au moins). Le diagnostic fait appel à la clinique, mais aussi à l'électroencéphalographie. Un EEG est évidemment anormal lors d'une crise d'épilepsie, mais il peut aussi montrer des anomalies en-dehors des crises, ce qui aide le diagnostic.
===La distinction entre crises provoquées et non-provoquées===
Pour faciliter le diagnostic d'une épilepsie, on distingue les crises épileptiques dites "provoquées", de celles qui ne le sont pas. Les crises provoquées sont liées à un évènement temporaire, qui favorise l'apparition d'une crise. Par exemple, une simple fièvre peut déclencher des convulsions, surtout chez le petit enfant : on parle alors de '''crises fébriles'''. Environ 2 à 5% des enfants ont une telle crise, la majorité des enfants n'ont qu'une seule crise et les cas où les crises sont récurrentes sont assez rares. De telles crises sont rarement inquiétantes et il est d'ailleurs très rare que ces enfants soient atteints d'épilepsie à l'âge adulte ou lors de l'adolescence, bien que ces crises fébriles puissent rarement indiquer un terrain favorable. Les crises provoquées surviennent aussi quand le cerveau est en souffrance, comme lors d'un AVC, pendant une méningite, juste après un traumatisme crânien, etc. Dans les grandes lignes, voici les causes les plus fréquentes des crises symptomatiques : les traumatismes crâniens, les AVC, les tumeurs et cancers du cerveau, les infections cérébrales (encéphalopathies, méningites), certaines maladies auto-immunes telle la sclérose en plaque, les causes métaboliques (hypoglycémie, hypocalcémie, insuffisance hépatique), la prise de certains médicaments, l'excès d'alcool ou de drogues, l'arrêt de l'alcool.
Les crises non-provoquées sont celles qui surviennent alors qu'aucune des raisons précédente n'est présente. Le sujet déclenche une crise épileptique, mais sans raison apparente, sans que son cerveau soit sous l'effet d'un stress quelconque. Divers facteurs peuvent favoriser la survenue d'une crise épileptique non-provoquée chez un patient totalement sain, ou chez un sujet prédisposé. Par exemple, le manque de sommeil favorise la survenue d'une crise. L'exposition à certains stimulus visuels est aussi un facteur déclenchant (d'où l'avertissement présent dans tous les jeux vidéo). Une crise non-provoquée unique n'est pas suffisant pour diagnostiquer l’épilepsie : 50% des personnes ayant subit une crise non-provoquée ont juste une crise unique, sans aucune répétition ultérieure. Les crises épileptiques doivent se répéter pour que le diagnostic d'épilepsie soit posé, et les crises doivent être séparées par un intervalle de temps suffisant (typiquement 24 heures, au moins). Seule la répétition de plusieurs crises non-provoquées permet de poser un diagnostic d'épilepsie. Certaines formes d'épilepsies peuvent cependant être diagnostiquées après une seule crise grâce à un électro-encéphalogramme, si l'EEG est clairement anormal, mais elles sont loin d'être une majorité.
===Les formes familiales et symptomatiques===
Les causes de l’épilepsie sont nombreuses, et varient considérablement chez les patients atteints. Le grand public pense le plus souvent que l'épilepsie est une maladie d'origine génétique. Mais ce n'est réellement le cas que pour une partie des épilepsies, dites '''familales''', dont l'origine est un dysfonctionnement des canaux ioniques et de certains récepteurs synaptiques. D'autres épilepsies ne sont que des conséquences d'un trauma cérébral ancien, comme les épilepsies qui s'installent suite à un AVC ou un traumatisme crânien. On parle alors d'épilepsies '''symptomatiques''', dans le sens où elles ne sont qu'une conséquence d'une maladie cérébrale indépendante : encéphalite, conséquence d'un AVC, tumeur au cerveau, etc. Par exemple, certains patients développent une épilepsie après une méningite ou une encéphalite, parfois pendant l'infection, ou quelques jours/semaines/mois après. D'autres formes d'épilepsie sont liée à une maladie auto-immune, où le corps produits des anticorps qui attaquent certains récepteurs synaptiques ou des les canaux ioniques.
Il est assez rare que la transmission de l'épilepsie soit héréditaire. La raison à cela est que la majorité des prédispositions génétiques à l’épilepsie sont codées par plusieurs gènes : la transmission de l'ensemble de ces gènes à l'enfant est alors difficile. De ce fait, il arrive qu'un individu ait des prédispositions génétiques qui favorisent la survenue d'une épilepsie suite à un traumatisme cérébral ou un stress quelconque. Mais les cas où l'épilepsie se transmet de parents à enfants sont assez rares. C'est ce qui explique que l'épilepsie est assez commune dans diverses maladies génétiques, telle la trisomie 21 ou le syndrome de l'X fragile. Il existe cependant un faible nombre d'épilepsies qui se transmettent via un seul gène, et qui sont donc facilement transmissibles à la descendance.
Il existe de nombreux syndromes épileptiques reconnus chez le nouveau-né, l'enfant et l'adolescent. Parmi les épilepsies du nouveau-né, on peut citer l''''épilepsie néonatale familiale bénigne''' et les '''benign neonatal seizures'''. Ces deux syndromes sont des formes rares d'épilepsie du nouveau-né, où les convulsions apparaissent dès les premiers jours de vie et cessent à partir du sixième mois de vie. Elles se caractérisent par des crises d'épilepsie focales, de type cloniques pour l'épilepsie néonatale familiale bénigne, clonique et/ou tonique pour les ''benign neonatal seizures''. Elles sont liées à des mutations des gènes KCNQ2 et KCNQ3, ainsi que du gène SCN2A pour certains cas de ''benign neonatal seizures''. Les anomalies se localisent sur le chromosome numéro 20, plus rarement sur le 8ᵉ chromosome. Ces gènes codent les canaux ionique au potassium dépendant du voltage. Les canaux potassiques sont alors moins perméables que la normale, ce qui augmente la teneur en potassium dans les neurones, comparé à la normale. Ces deux syndromes sont relativement bénins et répondent bien aux traitements médicamenteux.
Les deux autres syndromes d'épilepsies du nouveau-né ne sont pas bénigne comme le sont les deux précédentes. L{{'}}''Early Myoclonic Encephalopathy'' et le syndrome d'Ohtahara. Elles ont des causes similaires et présentent des anomalies caractéristiques très similaires sur l'EEG. Les deux se manifestent par des crises focales ou généralisées : surtout des spasmes épileptiques pour le syndrome d'Ohtahara, des spasmes accompagnés de crises toniques pour l'autre. La mortalité des très grande dans ces deux maladies, et les enfants qui survient ont généralement un retard mental et moteur important.
Comme autre exemple d'épilepsie mono-génétique, les '''épilepsies myocloniques progressives''' viennent en second. Elles sont causées par des mutations du chromosome 6. Enfin, citons l''''épilepsie frontale nocturne''', causée par une mutation autosomale dominante du chromosome 20.
===Les traitements de l'épilepsie ===
Le traitement de l'épilepsie se fait principalement avec des '''médicaments anti-épileptiques'''. Une autre option, réservée aux cas les plus graves et résistants aux traitements, est la '''chirurgie'''. Celle-ci consiste à retirer la zone où naissent et partent les décharges électriques à l'origine des crises. Une autre option, réservée à certains cas et aujourd’hui de plus en plus rare, consiste à sectionner le corps calleux, pour éviter que les crises se propagent dans l'autre hémisphère que celui de naissance de la crise. La stimulation du nerf vague est aussi efficace, bien que très peu utilisée. Outre les médications, l'éducation du patient sur la maladie est d'une importance capitale : il faut informer le patient sur les mesures à prendre pour éviter les facteurs déclenchant d'une crise.
Les anti-épileptiques réduisent l'activité cérébrale générale, en agissant sur les neurotransmetteurs GABA et glutamate, ou en agissant sur les canaux ioniques. Ces anti-épileptiques sont le plus souvent :
* des '''modulateurs allostériques positifs des récepteurs GABA''', à savoir des barbituriques ou des benzodiazépines ;
* des '''antagonistes compétitifs du glutamate''' ;
* des '''bloqueurs des canaux ioniques au sodium et au calcium'''.
Parmi les médicaments anti-épileptiques, les plus connus et utilisés sont le valproate de sodium, la carbamazépine, la phénytoïne, les barbituriques, et les benzodiazépines. Le type d'épilepsie joue beaucoup dans le choix du traitement : par exemple, certains traitements sont efficaces sur les crises tonico-cloniques mais inefficaces sur les crises atoniques, tandis que d'autres ont une efficacité inverse. Par exemple, il semblerait que l'ethosuximide soit plus efficace que les autres traitements sur les crises d'absences. De même, les crises d'absences sont bien soignées par le valproate de sodium, alors que la carbamazépine les aggrave !
==En savoir plus==
Ceux qui sont veulent en savoir plus sur l'épilepsie peuvent consulter le site de l'ILAE, ainsi que leur site sur le diagnostic de l'épilepsie :
* [http://www.ilae.org/ site de l'ILAE] ;
<noinclude>
{{NavChapitre | book=Neurosciences
| prev=Le système ventriculaire
| prevText=Le système ventriculaire
| next=La barrière hémato-encéphalique
| nextText=La barrière hémato-encéphalique
}}
</noinclude>
{{AutoCat}}
blqwxim6pvjvhbm3ioaiqlz82ie6xg9
775323
775322
2026-10-16T17:03:35Z
Mewtow
31375
775323
wikitext
text/x-wiki
Le cerveau a une activité électrique relativement soutenue : les neurones sont parcourus par des potentiels d'action, qui ne sont ni plus ni moins que des signaux électriques. L'ensemble des potentiels d'action à un moment donné causent une activité électrique mesurable à la surface du crâne, de l'ordre de quelques microvolts. Cette activité électrique cérébrale peut se mesurer avec un instrument composé de plusieurs électrodes placées à la surface du crâne : l''''électroencéphalographe''', ou EEG. Il est utilisé pour étudier le fonctionnement du cerveau, diagnostiquer certaines maladies cérébrales et observer le déroulement du sommeil.
Dans ce chapitre, nous allons parler de l'activité électrique normale du cerveau, ainsi que d'une pathologie liée l’'''épilepsie'''. L'activité électrique subit aussi des variations au cours de la veille et du sommeil. Mais nous laissons ces variations pour le chapitre sur le sommeil : ce chapitre se limite à quelques généralités et à l’épilepsie.
==L'activité électrique cérébrale non-pathologique==
Si l'ensemble du cerveau a une activité électrique, celle-ci n'est pas tout à fait uniforme : si une aire cérébrale s'active plus qu'une autre, les variations de potentiel seront plus importantes près de cette aire cérébrale que sur une surface éloignée. Généralement, seules les aires cérébrales du cortex, situées sous la surface du crâne, donnent des variations significatives sur l'EEG : le signal des autres aires cérébrales est atténué par le cortex situé au-dessus. Pour localiser la source de l'activité cérébrale, la mesure du potentiel s'effectue souvent avec plusieurs électrodes disposées régulièrement sur la surface du crâne.
Les mesures montrent que l'activité électrique est quelque peu structurée et possède quelques régularités assez visibles sur les mesures à l'EEG. Contrairement à ce qu'on pourrait croire, les neurones ont tendance à synchroniser leurs émissions, à émettre plus ou moins en même temps. Sur l'EEG, cela se traduit par une activité cyclique, périodique (à quelques variations près) : on observe des '''rythmes cérébraux''', parfois improprement appelés ondes cérébrales. Au-dessus de ces rythmes, on observe des fluctuations aléatoires de l'activité électrique. Parmi ces fluctuations, certaines sont causées par une perception extérieure ou une activité intellectuelle : ce sont des '''potentiels évoqués'''.
[[File:SimulationNeuralOscillations.png|centre|vignette|upright=1.5|Illustration des rythmes cérébraux, en fonction des potentiels d'action individuels émis par chaque neurone cortical.]]
===Les rythmes cérébraux===
Chaque rythme cérébral a une fréquence bien particulière, et on classe arbitrairement les ondes cérébrales dans 4 à 6 bandes de fréquences nommées alpha, bêta, gamma, delta, et thêta. Les scientifiques ont remarqué que chaque bande de fréquence correspond à des niveaux d'éveil et de sommeil différents. De plus, ces ondes cérébrales sont localisées dans des zones différentes du cerveau : les ondes gamma et bêta sont essentiellement localisées dans les lobes frontaux, alors que les rythmes Alpha sont localisées dans le cortex sensoriel et moteur.
{|class="wikitable"
|-
!Onde cérébrale
!Bande de fréquence
|-
!Delta
|Inférieur à 4 hertz
|-
!Thêta
|4 à 7 hertz
|-
!Alpha
|8 à 15 hertz
|-
!Bêta
|16 à 31 hertz
|-
!Gamma
|Plus de 32 hertz
|}
Une activité rythmique peut s'observer à de nombreux niveau : au niveau d'un neurone isolé, au niveau d'un petit réseau de neurones, au niveau du cerveau.
Pour ce qui est des neurones isolés, il existe des '''neurones pacemakers''' émettent des potentiels d'action à des intervalles réguliers, même s'ils ne sont pas stimulés par d'autres neurones.
Pour les petits réseaux de neurones, composés de quelques centaines ou milliers de neurones, voire plus, des mécanismes de synchronisation peuvent se mettre en place par les synapses. Vu qu'un neurone tend à ^émettre un potentiel d'action quand il est stimulé par un autre, deux neurones reliés entre eux par une synapse tendent à émettre des potentiels d'actions en même temps. Dans une assemblée de neurones fortement reliés entre eux, ce mécanisme tend à synchroniser les neurones entre eux, de sorte qu'ils émettent des potentiels d'action plus ou moins en même temps.
Pour ce qui est du niveau cérébral, les rythmes cérébraux trouvent leur origine dans le fait que certaines aires cérébrales sont reliées et forment des boucles dans lesquelles des influx nerveux se propagent cycliquement. Les boucles les plus importantes relient les neurones du cortex au thalamus, d'où leur nom de '''boucles thalamico-corticales'''. Ces boucles thalamico-corticales sont au nombre de deux, la boucle ventrobasale impliquant le thalamus ventrolatéral (à la base et sur le côté) alors que la boucle centrolatérale implique le thalamus centrolatéral (au centre et sur l'intérieur). La boucle ventrobasale s'occupe du traitement des informations sensorielles, alors que la boucle centrolatérale dédiée à la conscience et à la pensée. Lors de l'éveil, les sensations rentrent librement dans la boucle ventrobasale. Mais lors du sommeil, le thalamus ventrobasal ne laisse pas passer les informations sensorielles : les deux boucles thalamico-corticales sont en circuit fermé. Ce comportement est commandé par des neurones réticulaires, eux-mêmes commandés par la formation réticulée (une aire cérébrale dédiée à la gestion de l'éveil, de l'attention, et de l'activation physiologique).
===Les potentiels évoqués===
[[File:ComponentsofERP.svg|vignette|Exemple d'ERP.]]
L'étude des potentiels évoqués est plus complexe, vu qu'ils sont noyés dans l'activité électrique aléatoire de base. Distinguer ce qui vient des rythmes, de l'activité aléatoire et potentiels évoqués est quelque peu compliqué. La méthode la plus utilisée est de répéter les mesures sur un grand nombre de sujets et de faire la moyenne entre elles. Ce faisant, les variations aléatoires sont annulées, de même que les rythmes (du fait de leur déphasage), alors que les potentiels évoqués restent. On voit alors que certains stimulus déclenchent systématiquement des potentiels évoqués après un délai fixe. Par exemple, quand on capte un stimulus verbal, un potentiel évoqué apparait environ 400 millisecondes après sa perception Ce stimulus a reçu son propre nom et est étudié pour sa relation avec les processus de traitement du langage. C'est d'ailleurs loin d'être le seul : de nombreux potentiels évoqués ont reçu un nom et ont été identifié comme entités à étudier.
Les potentiels évoqués peuvent être des potentiels positifs, où la tension augmente comparé à son niveau de base, ou des potentiels négatifs où la tension diminue. Cela se retient dans la notation des potentiels évoqués, qui en tient compte. La notation de ces potentiels est composée d'une lettre suivie d'un nombre. La lettre indique si le potentiel est positif ou négatif : P pour positif et N pour négatif. Le nombre est le délai entre la présentation du stimulus et l'apparition du potentiel évoqué. Pour donner un exemple, reprenons le potentiel évoqué mentionné précédemment, où un stimulus verbal déclenche un potentiel négatif 400 ms après : ce potentiel est appelé le potentiel N400.
==Les crises épileptiques==
Les crises épileptiques sont des crises où l'activité cérébrale s'emballe dans une portion plus ou moins importante du cerveau. L'idée que se fait le grand public se fait d'une crise d'épilepsie ressemble globalement à une perte de connaissance avec des convulsions. Pour rappel, les convulsions sont des contractions musculaires involontaires et assez violentes, qui peuvent toucher toute partie du corps. De telles crises existent, sont très fréquentes, et correspondent à un cas où une crise d'épilepsie embrase tout le cerveau. Mais il existe des crises épileptiques sans convulsions, ou sans perte de conscience, voire avec des symptômes sensoriels, cognitifs, et autres. Malgré la présentation très variée des crises épileptiques, leur mécanisme est le même : l'activité électrique cérébrale s'emballe. Cela se voit bien lorsqu'on fait passer en EEG pendant une crise, comme montré ci-dessous.
[[File:Spike-waves.png|centre|vignette|upright=2|EEG d'une crise d'épilepsie : le patient déclenche une crise au beau milieu de l'EEG. On voit le changement de l'activité électrique, clairement visible sur l'EEG.]]
La présentation symptomatique d'une crise épileptique est très variable et la classification des crises épileptiques est assez complexe, comme vous allons le voir dans ce qui suit. Pour simplifier le diagnostic, les médecins ont classé les épilepsies selon leurs symptômes,. La classification de ce type la plus connue est l'''International classification of seizure types'', datée de 1981. Elle a ensuite été suivie par la ''ILAE classification of seizure types 2017'', plus moderne. Dans cette dernière, la classification de ces crises se fait sur trois points, auquel nous allons ajouter un quatrième.
* Le premier point est si la crise est focale ou généralisée, à savoir si elle touche seulement un hémisphère cérébral ou tout le cerveau.
* Le second point est si la crise s’accompagne ou non d'une perte de conscience.
* Le troisième point est les symptômes moteurs qui se manifestent lors de la crise.
===Les manifestations motrices d'une crise épileptique===
Les crises épileptiques impliquent souvent des manifestations motrices. Dans les grandes lignes, on distingue les convulsions, les pertes de tonus musculaire, et les automatismes moteurs.
La plupart impliquent des '''convulsions''', des contractions musculaires involontaires, même si ce n'est pas systématique. On distingue trois types de convulsions :
* les ''convulsions toniques'', où la contraction est prolongée avec une contraction complète des muscles :
* les ''convulsions cloniques'' où les contractions sont très courtes et se succèdent rapidement ;
* les ''convulsions myocloniques'' sont des convulsions cloniques très courtes, de moins de quelques secondes, parfois moins de la seconde.
Une crise épileptique convulsive peut présenter soit des convulsions cloniques, soit des convulsions toniques, soit des convulsions myocloniques, soit une succession des trois. D'autres crises n'impliquent pas de convulsions, qu'elles soient toniques ou cloniques. Certaines crises épileptiques manifestent un mélange de plusieurs types de convulsions, ou une succession de celles-ci. Les myoclonies sans pertes de consciences ne sont pas exclusives aux crises épileptiques. Certains patients sains en ont avant de s'endormir sans que cela ne soit le signe d'une quelconque maladie.
Il peut aussi y avoir une '''atonie''', à savoir une perte totale de tonus musculaire, avec ou sans perte de conscience. Elle dure quelques secondes, survient soudainement, et touche souvent un côté du corps ou un membre. Dans le cas le plus sévère, le patient chute brutalement et perd tout contrôle musculaire. Il perd connaissance dans la plupart des cas, mais pas toujours. Dans d'autres, la perte de tonus musculaire ne touche qu'un bras, qu'une jambe, un seul côté du corps.
À l'inverse, d'autres crises épileptiques se caractérisent par des '''spasmes épileptiques'''. Un spasme est une contraction musculaire spontanée, soudaine, parfois douloureuse. De tels spasmes d’origine épileptique touchent souvent les bras, jambes, mains et poignets. Typiquement, on observe le plus souvent une extension des bras ou des jambes, ou encore du poignet. Ils sont plus fréquents chez les enfants, où ils sont appelés des spasmes infantiles. Le patient garde souvent conscience lors de crises de spasmes épileptiques, mais ce n'est clairement pas systématique.
Le patient peut aussi effectuer des mouvements complexes sont appelés des '''automatismes'''. Le sujet peut par exemple effectuer des mouvements simples, comme se gratter, mastiquer sa nourriture, déambuler, marcher en ligne droite, etc. Mais il peut aussi faire des choses bien plus compliquées, comme déplacer un meuble par exemple.
Tout cela permet de distinguer plusieurs types purs de crises épileptiques, dans lesquels on n'observe qu'une manifestation motrice précise, pas un mélange de plusieurs :
* les crises toniques ;
* les crises cloniques ;
* les crises myocloniques;
* les crises atoniques ;
* les crises d'automatisme ;
* les crises de spasmes épileptiques.
===La conscience lors d'une crise d'épilepsie===
Les crises épileptiques sont souvent accompagnées d'une '''altération de la conscience''' bien que ce ne soit pas systématique. Cela peut aller d'une perte de connaissance complète, à une altération plus légère de la conscience avec amnésie. Il arrive que le sujet continue à effectuer des mouvements ou certains automatismes alors que sa conscience est altérée, voire abolie. Le sujet peut alors ressembler à quelqu'un qui fait une crise de somnambulisme, même si ce n'est vrai qu'en apparence.
Le critère de la perte de conscience est encore utilisé dans la classification des crises épileptiques. Mais dans certains cas, il est difficile de savoir si le patient a perdu conscience, notamment si les crises sont très courtes et ne durent que quelques secondes. Il existe des crises d'épilepsie qui se manifestent seulement par une perte de conscience isolée, sans symptôme moteur. Il s'agit des '''crises d'absence'''. Durant ces crises, le sujet devient mutique et immobile durant un temps inférieur à la minute. Ces crises ne durent que quelques dizaines de secondes, rarement plus. La survenue de la crise d'absence est soudaine, son entourage remarque qu'il cesse brusquement ce qu'il était en train de faire, voit que le patient regarde dans le vide. Le patient reste debout, mais ne réagit à aucun stimulus : l'appeler par son nom ou le toucher est inutile. La crise cesse généralement aussi rapidement qu'elle a commencé.
===Les crises focales et généralisées===
Les classifications des crises épileptiques distinguent en premier lieu crise focale et généralisée. La différence entre les deux tient au fait que l'embrasement cérébral est soit limité à une portion limitée du cerveau, soit envahit tout le cerveau. Les '''crises focales''' touchent un seul hémisphère, parfois seulement partiellement, tandis que les '''crises généralisées''' touchent les deux hémisphères, tout le cerveau. Il faut cependant signaler que les crises focales peuvent évoluer vers une crise généralisée, bien que ce ne soit pas systématique.
Les crises focales ont vraisemblablement une origine assez différente des crises généralisées. Les crises focales naissent à partir d'un amas/réseau de neurones hyper-excitable, appelé '''foyer épileptique'''. Ce foyer peut naitre à la suite d'une lésion, peu importe que celle-ci provienne d'un traumatisme crânien ou d'un AVC, mais certaines lésions naissent spontanément, sans origine déterminée. Les crises naissent quand ce foyer épileptique manifeste une bouffée spontanée de potentiels d'action. L'hyperactivité électrique va ensuite se propager aux alentours du foyer, passant de synapses en synapses et peut finir par toucher une grande partie du cerveau. Dans la majorité des cas, la bouffée épileptique reste confinée dans une zone épileptogéne assez limitée : la crise ne se propage pas bien loin dans le cerveau. Mais dans d'autres cas, plus rares, la crise peut se généraliser à l'ensemble du cerveau : la crise focale évolue alors en crise généralisée. Ce phénomène fait que les chercheurs classifient les crises focales selon qu'elles se généralisent ou non.
Les crises généralisées touchent les deux hémisphères, et entrainent généralement des pertes de conscience, accompagnée ou non de manifestations motrices. La '''crise tonique-clonique''' est la présentation auquel le grand public pense quand on évoque une crise généralisée. Elle se déroule en plusieurs phases successives très caractéristiques :
* La première phase, appelée '''phase tonique''' se traduit par une contraction complète des muscles, ainsi qu'une perte de connaissance totale. Le patient tombe alors par terre, sans même essayer de se rattraper. Plus inquiétant, sa respiration s'arrête durant la phase tonique. Dans le cas où la crise se poursuit trop longtemps, le sujet finit même par avoir le teint bleuté à la fin de la crise tonique. Mais fort heureusement, la crise tonique ne dure que quelques secondes à quelques minutes.
* Vient ensuite la '''phase clonique''', où le patient est pris de contractions et de soubresauts aléatoires. La respiration reprend, mais est totalement désorganisée et son rythme est totalement aléatoire. Le patient reste inconscient.
* Puis vient la phase de '''coma''' : le patient reste inconscient, mais sa respiration reprendre normalement et ses muscles sont totalement décontractés, sans tonus. La couleur du patient revient progressivement à la normale.
* Enfin, la crise cesse et le patient reprend conscience. S’ensuit une période où le patient est confus, ne se souvient plus de la crise, a mal à la tête.
===Les classifications des crises épileptiques===
L{{'}}''International classification of seizure types'' de 1981 et l{{'}}''ILAE classification of seizure types 2017'' classent les crises épileptiques différemment.
Mais les deux classifications ont beaucoup en commun, et distinguent crises d'absence, crises atones, et crises avec convulsions.
====L{{'}}''International classification of seizure types'' de 1981====
L{{'}}''International classification of seizure types'' de 1981 distingue les crises généralisées et partielles.
Le classement des crises généralisées distingues celles avec et sans convulsions.
{|class="wikitable"
|+ ''International classification of seizure types'', 1981
|-
! colspan="2" | Type de crise épileptique généralisée
! Description
! Perte de conscience
|-
! rowspan="4" | Crises convulsives
! Crise clonique
| rowspan="2" | Convulsions cloniques
| Perte de conscience
|-
! Crise myoclonique
| Pas de perte de conscience
|-
! Crise tonique
| Convulsions toniques durant plusieurs minutes/heures
| Très fréquente mais pas systématique.
|-
! Crise tonique-clonique
| colspan="2" | Crise en trois temps : une crise clonique avec perte de conscience, suivie par une crise tonique, elle-même suivie par un coma temporaire de quelques minutes.
|-
| colspan="3" |
|-
! rowspan="2" | Crises non-convulsives
! Crise d'absence
| colspan="2" | Interruption de la conscience, sans chute.
|-
! Crise atone
| Perte totale du tonus musculaire
| Très fréquente mais pas systématique.
|}
Les crises focales pures sont elles subdivisées en deux sous-types selon que la conscience est abolie lors de la crise ou non : on parle de crises focales simples si la conscience n'est pas touchée, et de crise complexe quand la conscience est altérée. Et quand on parle d'altération de la conscience lors d’une crise focale complexe, cela ne signifie pas que le sujet perd conscience, ni même qu'il reste inactif (même si c'est fréquent).
{|class="wikitable"
|-
! Type de crise épileptique focale
! Description
! Sous-types
|-
! Crise simple
| Pas d'altération de la conscience.
| Les symptômes sont assez différents selon la localisation des aires touchées par la crise. On distingue ainsi :
* les crises avec des symptômes moteurs ;
* les crises avec des symptômes sensoriels ;
* les crises avec des symptômes comportementaux ;
* les crises avec des symptômes liés au système nerveux autonome.
|-
! Crise complexe
| Altération de la conscience.
| La classification des crises complexes se borne à faire la distinction entre :
* crises où la perturbation de la conscience apparait au début de la crise (crises complexes pures) ;
* et celles où la conscience disparait pendant la crise (crises simples qui évoluent en crises complexes).
|}
Les crises focales qui évoluent en crises généralisées sont un second type de crises focales. Elles sont classées en trois sous-types suivant leur évolution :
* crises simples qui évoluent vers une crise généralisée ;
* crises complexes qui évoluent en crise généralisée ;
* crises simples qui évoluent en crises complexes, qui elle-mêmes évoluent en crise généralisée ;
====L{{'}}''ILAE classification of seizure types 2017''====
Dans l{{'}}''ILAE classification of seizure types 2017'', la classification est beaucoup plus simple. Elle distingue les crises focales des crises généralisées, avec un troisième type pour les crises dont le caractère focal/généralisé est inconnu, et subdivise ces trois types en crises motrices et non-motrices. Pour les crises focales, on ajoute un critères quant à l'altération de conscience ou son absence.
L{{'}}''ILAE classification of seizure types 2017'' distingue les crises généralisées avec symptômes moteurs et celles sans. La différence est que certaines crises d'épilepsies se caractérisent par des contractures musculaires, mais sans convulsions proprement dit. Les crises généralisées non-motrices ne sont autre que les crises d'absence proprement dit. Les crises généralisées motrices regroupent les crises convulsives, les crises atoniques et quelques autres. Pour les crises généralisées, on oublie le critère sur l'altération de la conscience, car la conscience est très souvent altérée dans les crises généralisées. Ajouter une subdivision suivant une éventuelle perte de conscience serait donc redondant.
{|class="wikitable"
|+ ''ILAE classification of seizure types 2017'', Types de crises épileptiques généralisées
|-
! rowspan="10" | Crises motrices
|-
| Crise tonique-clonique
|-
| Crise tonique
|-
| Crise clonique
|-
| Crise myoclonique
|-
| Crise atonique
|-
| Crise myoclonique-atonique
|-
| Crise tonique-clonique-atonique
|-
| Spasme épileptiques
|-
| Autres
|-
| colspan="3" |
|-
! rowspan="4" | Crises non-motrices (crise d'absence)
| typique
|-
| Atypique
|-
| Myoclonique
|-
| Myoclonies limitées aux yeux (''eyelid myoclonia'')
|}
Pour les crises focales, elle mesure deux critères : s'il y a ou non altération de la conscience, et les crises avec ou sans symptômes moteurs. Les crises motrices sont classées suivant le symptôme moteur observé, les manifestations motrices manifestées. On a ainsi des crises focales clonique, toniques, atoniques, myocloniques, à automatisme, avec spasmes épileptiques, etc. Les crises non-motrices, sans manifestations motrices, sont classées de la même manière que les crises simples, avec les crises avec symptômes sensoriels, cognitifs, émotionnels, autonomiques, et un cinquième type de crise appelé les crises avec arrêt comportemental (le patient s’arrête de faire ce qu'il faisait durant quelques secondes/minutes).
L'avantage de cette classification est qu'elle reconnait que des crises clonique, toniques, atoniques, myocloniques, à automatisme peuvent être focales et ne sont pas systématiquement généralisées. La distinction entre crises simples et complexes, peu parlante pour le grand public, est aussi supprimée, car peu utile en pratique. Les crises peuvent se généraliser, mais la classification ne garde qu'un cas, du fait de la suppression de la distinction crise simple/complexe : celui où une crise focale se généralise, typiquement en crise tonico-clonique.
==L'épilepsie==
L{{'}}'''épilepsie''' est une maladie chronique qui se manifeste par des crises d'épilepsies récurrentes. Un point important est qu'une simple crise épileptique ne suffit pas à diagnostiquer une épilepsie. Des crises isolées sont possibles chez des patients non-épileptiques, par exemple parce qu'ils ont une maladie cérébrale qui cause des crises. C'est un point important dans le diagnostic d'une épilepsie : cette maladie est considérée comme étant une maladie chronique et de long-terme, qu'on peut certes guérir, là où une crise unique est peut-être un évènement temporaire , un effet secondaire de médicament, le symptôme temporaire d'une autre maladie, etc. Pour éviter de diagnostiquer des épilepsies à tord et à travers, le neurologue doit éliminer toute maladie pouvant causer de telles crises, ainsi qu'une origine médicamenteuse. De plus, les crises épileptiques doivent se répéter pour que le diagnostic d'épilepsie soit posé, et les crises doivent être séparées par un intervalle de temps suffisant (typiquement 24 heures, au moins). Le diagnostic fait appel à la clinique, mais aussi à l'électroencéphalographie. Un EEG est évidemment anormal lors d'une crise d'épilepsie, mais il peut aussi montrer des anomalies en-dehors des crises, ce qui aide le diagnostic.
===La distinction entre crises provoquées et non-provoquées===
Pour faciliter le diagnostic d'une épilepsie, on distingue les crises épileptiques dites "provoquées", de celles qui ne le sont pas. Les crises provoquées sont liées à un évènement temporaire, qui favorise l'apparition d'une crise. Par exemple, une simple fièvre peut déclencher des convulsions, surtout chez le petit enfant : on parle alors de '''crises fébriles'''. Environ 2 à 5% des enfants ont une telle crise, la majorité des enfants n'ont qu'une seule crise et les cas où les crises sont récurrentes sont assez rares. De telles crises sont rarement inquiétantes et il est d'ailleurs très rare que ces enfants soient atteints d'épilepsie à l'âge adulte ou lors de l'adolescence, bien que ces crises fébriles puissent rarement indiquer un terrain favorable. Les crises provoquées surviennent aussi quand le cerveau est en souffrance, comme lors d'un AVC, pendant une méningite, juste après un traumatisme crânien, etc. Dans les grandes lignes, voici les causes les plus fréquentes des crises symptomatiques : les traumatismes crâniens, les AVC, les tumeurs et cancers du cerveau, les infections cérébrales (encéphalopathies, méningites), certaines maladies auto-immunes telle la sclérose en plaque, les causes métaboliques (hypoglycémie, hypocalcémie, insuffisance hépatique), la prise de certains médicaments, l'excès d'alcool ou de drogues, l'arrêt de l'alcool.
Les crises non-provoquées sont celles qui surviennent alors qu'aucune des raisons précédente n'est présente. Le sujet déclenche une crise épileptique, mais sans raison apparente, sans que son cerveau soit sous l'effet d'un stress quelconque. Divers facteurs peuvent favoriser la survenue d'une crise épileptique non-provoquée chez un patient totalement sain, ou chez un sujet prédisposé. Par exemple, le manque de sommeil favorise la survenue d'une crise. L'exposition à certains stimulus visuels est aussi un facteur déclenchant (d'où l'avertissement présent dans tous les jeux vidéo). Une crise non-provoquée unique n'est pas suffisant pour diagnostiquer l’épilepsie : 50% des personnes ayant subit une crise non-provoquée ont juste une crise unique, sans aucune répétition ultérieure. Les crises épileptiques doivent se répéter pour que le diagnostic d'épilepsie soit posé, et les crises doivent être séparées par un intervalle de temps suffisant (typiquement 24 heures, au moins). Seule la répétition de plusieurs crises non-provoquées permet de poser un diagnostic d'épilepsie. Certaines formes d'épilepsies peuvent cependant être diagnostiquées après une seule crise grâce à un électro-encéphalogramme, si l'EEG est clairement anormal, mais elles sont loin d'être une majorité.
===Les formes familiales et symptomatiques===
Les causes de l’épilepsie sont nombreuses, et varient considérablement chez les patients atteints. Le grand public pense le plus souvent que l'épilepsie est une maladie d'origine génétique. Mais ce n'est réellement le cas que pour une partie des épilepsies, dites '''familales''', dont l'origine est un dysfonctionnement des canaux ioniques et de certains récepteurs synaptiques. D'autres épilepsies ne sont que des conséquences d'un trauma cérébral ancien, comme les épilepsies qui s'installent suite à un AVC ou un traumatisme crânien. On parle alors d'épilepsies '''symptomatiques''', dans le sens où elles ne sont qu'une conséquence d'une maladie cérébrale indépendante : encéphalite, conséquence d'un AVC, tumeur au cerveau, etc. Par exemple, certains patients développent une épilepsie après une méningite ou une encéphalite, parfois pendant l'infection, ou quelques jours/semaines/mois après. D'autres formes d'épilepsie sont liée à une maladie auto-immune, où le corps produits des anticorps qui attaquent certains récepteurs synaptiques ou des les canaux ioniques.
Il est assez rare que la transmission de l'épilepsie soit héréditaire. La raison à cela est que la majorité des prédispositions génétiques à l’épilepsie sont codées par plusieurs gènes : la transmission de l'ensemble de ces gènes à l'enfant est alors difficile. De ce fait, il arrive qu'un individu ait des prédispositions génétiques qui favorisent la survenue d'une épilepsie suite à un traumatisme cérébral ou un stress quelconque. Mais les cas où l'épilepsie se transmet de parents à enfants sont assez rares. C'est ce qui explique que l'épilepsie est assez commune dans diverses maladies génétiques, telle la trisomie 21 ou le syndrome de l'X fragile. Il existe cependant un faible nombre d'épilepsies qui se transmettent via un seul gène, et qui sont donc facilement transmissibles à la descendance.
Il existe de nombreux syndromes épileptiques reconnus chez le nouveau-né, l'enfant et l'adolescent. Parmi les épilepsies du nouveau-né, on peut citer l''''épilepsie néonatale familiale bénigne''' et les '''benign neonatal seizures'''. Ces deux syndromes sont des formes rares d'épilepsie du nouveau-né, où les convulsions apparaissent dès les premiers jours de vie et cessent à partir du sixième mois de vie. Elles se caractérisent par des crises d'épilepsie focales, de type cloniques pour l'épilepsie néonatale familiale bénigne, clonique et/ou tonique pour les ''benign neonatal seizures''. Elles sont liées à des mutations des gènes KCNQ2 et KCNQ3, ainsi que du gène SCN2A pour certains cas de ''benign neonatal seizures''. Les anomalies se localisent sur le chromosome numéro 20, plus rarement sur le 8ᵉ chromosome. Ces gènes codent les canaux ionique au potassium dépendant du voltage. Les canaux potassiques sont alors moins perméables que la normale, ce qui augmente la teneur en potassium dans les neurones, comparé à la normale. Ces deux syndromes sont relativement bénins et répondent bien aux traitements médicamenteux.
Les deux autres syndromes d'épilepsies du nouveau-né ne sont pas bénigne comme le sont les deux précédentes. L{{'}}''Early Myoclonic Encephalopathy'' et le syndrome d'Ohtahara. Elles ont des causes similaires et présentent des anomalies caractéristiques très similaires sur l'EEG. Les deux se manifestent par des crises focales ou généralisées : surtout des spasmes épileptiques pour le syndrome d'Ohtahara, des spasmes accompagnés de crises toniques pour l'autre. La mortalité des très grande dans ces deux maladies, et les enfants qui survient ont généralement un retard mental et moteur important.
Comme autre exemple d'épilepsie mono-génétique, les '''épilepsies myocloniques progressives''' viennent en second. Elles sont causées par des mutations du chromosome 6. Enfin, citons l''''épilepsie frontale nocturne''', causée par une mutation autosomale dominante du chromosome 20.
===Les traitements de l'épilepsie ===
Le traitement de l'épilepsie se fait principalement avec des '''médicaments anti-épileptiques'''. Une autre option, réservée aux cas les plus graves et résistants aux traitements, est la '''chirurgie'''. Celle-ci consiste à retirer la zone où naissent et partent les décharges électriques à l'origine des crises. Une autre option, réservée à certains cas et aujourd’hui de plus en plus rare, consiste à sectionner le corps calleux, pour éviter que les crises se propagent dans l'autre hémisphère que celui de naissance de la crise. La stimulation du nerf vague est aussi efficace, bien que très peu utilisée. Outre les médications, l'éducation du patient sur la maladie est d'une importance capitale : il faut informer le patient sur les mesures à prendre pour éviter les facteurs déclenchant d'une crise.
Les anti-épileptiques réduisent l'activité cérébrale générale, en agissant sur les neurotransmetteurs GABA et glutamate, ou en agissant sur les canaux ioniques. Ces anti-épileptiques sont le plus souvent :
* des '''modulateurs allostériques positifs des récepteurs GABA''', à savoir des barbituriques ou des benzodiazépines ;
* des '''antagonistes compétitifs du glutamate''' ;
* des '''bloqueurs des canaux ioniques au sodium et au calcium'''.
Parmi les médicaments anti-épileptiques, les plus connus et utilisés sont le valproate de sodium, la carbamazépine, la phénytoïne, les barbituriques, et les benzodiazépines. Le type d'épilepsie joue beaucoup dans le choix du traitement : par exemple, certains traitements sont efficaces sur les crises tonico-cloniques mais inefficaces sur les crises atoniques, tandis que d'autres ont une efficacité inverse. Par exemple, il semblerait que l'ethosuximide soit plus efficace que les autres traitements sur les crises d'absences. De même, les crises d'absences sont bien soignées par le valproate de sodium, alors que la carbamazépine les aggrave !
==En savoir plus==
Ceux qui sont veulent en savoir plus sur l'épilepsie peuvent consulter le site de l'ILAE, ainsi que leur site sur le diagnostic de l'épilepsie :
* [http://www.ilae.org/ site de l'ILAE] ;
<noinclude>
{{NavChapitre | book=Neurosciences
| prev=Le système ventriculaire
| prevText=Le système ventriculaire
| next=La barrière hémato-encéphalique
| nextText=La barrière hémato-encéphalique
}}
</noinclude>
{{AutoCat}}
kyrowhjc7n4strjrzash5g7wd17gx1r
Docker/Kubernetes
0
73440
775318
761619
2026-10-16T13:33:43Z
JackPotte
5426
/* Commandes kubectl */
775318
wikitext
text/x-wiki
<noinclude>{{Docker}}</noinclude>
{{w|Kubernetes}} est un logiciel open-source sorti en 2015 pour orchestrer plusieurs conteneurs avec des options de quotas, {{wt|scalabilité horizontale}}, déploiement sans coupure de service et relance automatique en cas d'indisponibilité. Par ailleurs, il nécessite d'utiliser des images Docker déjà construites.
Pour ces raisons, il est plus orienté pour la production que pour le développement<ref>https://kubernetes.io/docs/setup/</ref>.
== Installation ==
=== Client ===
La configuration peut être stockée dans des fichiers .yaml et modifiée par la commande <code>kubectl</code><ref>https://kubernetes.io/fr/docs/reference/kubectl/overview/</ref>, ou via l'interface graphique ''Dashboard''<ref>https://kubernetes.io/fr/docs/tasks/access-application-cluster/web-ui-dashboard/</ref>. De plus, il est nécessaire de lancer un cluster contenant les conteneurs via <code>kubeadm</code><ref>https://kubernetes.io/docs/setup/production-environment/tools/kubeadm/install-kubeadm/</ref>.
==== Debian ====
Sur Debian ou ses dérivés<ref>https://kubernetes.io/docs/setup/production-environment/tools/kubeadm/install-kubeadm/</ref> :
<pre>
sudo apt-get install -y apt-transport-https ca-certificates curl gpg
curl -fsSL https://pkgs.k8s.io/core:/stable:/v1.29/deb/Release.key | sudo gpg --dearmor -o /etc/apt/keyrings/kubernetes-apt-keyring.gpg
echo 'deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg] https://pkgs.k8s.io/core:/stable:/v1.29/deb/ /' | sudo tee /etc/apt/sources.list.d/kubernetes.list
sudo apt-get update
sudo apt-get install -y kubelet kubeadm kubectl
sudo apt-mark hold kubelet kubeadm kubectl
</pre>
Test :
<pre>
kubectl version
</pre>
Aide :
<pre>
kubectl --help
</pre>
==== k9s ====
Pour éviter de taper chaque commande, il existe une interface graphique gratuite nommée k9s, pouvant exploiter la conf de kubectl : https://k9scli.io/topics/install/.
Cela nécessite le gestionnaire de paquets Homebrew :
<pre>
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
</pre>
Puis :
<pre>
brew install derailed/k9s/k9s
</pre>
Par exemple on peut déclencher un cronjob avec "t" ou rentrer dans un conteneur en shell avec "s".
{{remarque|si on ne voit aucun conteneur au premier lancement, changer l'espace de nom en tapant <code>:namespace</code> pour voir la liste puis entrée après sélection.}}
{{remarque|pour voir uniquement les conteneurs en cours sur k9s, taper : <code>/running</code>}}
== Types ==
=== pod ===
Un ''pod'' désigne un groupe de conteneur géré par Kubernetes<ref>https://kubernetes.io/fr/docs/concepts/workloads/pods/pod/</ref>.
C'est à travers lui que l'on peut configurer les readyness probe et liveness probe, permettant respectivement de bloquer le déploiement d'un conteneur en erreur et de le redémarrer s'il est en erreur<ref>https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/</ref>.
=== replica set ===
Un ''replica set'' désigne un ensemble de pods, dont le nombre maximum peut y être défini.
=== secret ===
Liste de variables d'environnement chiffrées, généralement des mots de passe.
Ils sont chiffrés via "kubeseal", à partir d'un fichier YAML non versionné contenant les chaines encodées en base64.
Ce fichier contient plusieurs paramètres, par exemple le type. Il en existe huit, "opaque" étant celui par défaut<ref>https://kubernetes.io/docs/concepts/configuration/secret/#secret-types</ref>.
La commande de chiffrement quant-à-elle permet de définir le scope des secrets<ref>https://github.com/bitnami-labs/sealed-secrets#scopes</ref> :
* strict (par défaut) : le nom et le namespace ne peuvent pas changer sans rechiffrer.
* namespace-wide : utilisable dans tout le namespace.
* cluster-wide : utilisable dans tout le cluster.
Toutefois on peut aussi les mettre dans le .yaml. Ex :
<pre>
metadata:
annotations:
sealedsecrets.bitnami.com/cluster-wide: "true"
</pre>
=== CronJob ===
Lance un conteneur dédié à une tâche planifiée (cron), à chaque fois qu'elle tourne<ref>https://kubernetes.io/docs/concepts/workloads/controllers/cron-jobs/</ref>.
=== Ingress ===
Accès à l'application (DNS).
=== PVC ===
Persistent volume : stocke des données indépendamment du redémarrage des pods.
== Commandes kubectl ==
=== get ===
Liste des éléments du serveur :
<pre>
kubectl get -A all
</pre>
=== create ===
Pour (re)lancer un job<ref>https://serverfault.com/questions/809632/is-it-possible-to-rerun-kubernetes-job</ref> :
<pre>
kubectl create job --from=job/mon_job mon_nouveau_job -n <namespace>
</pre>
=== logs ===
Pour voir le log de la sortie standard d'un conteneur :
<pre>
kubectl logs -f mon_conteneur
</pre>
=== exec ===
Pour rentrer dans un conteneur :
<pre>
kubectl exec -it mon_conteneur -- /bin/bash
</pre>
=== scale ===
Pour augmenter le nombre de pods dans un replicatset. Par exemple si on peut passer le nombre de 2 à 3 sur "mysql"<ref>https://jamesdefabia.github.io/docs/user-guide/kubectl/kubectl_scale/</ref> :
<pre>
kubectl scale --current-replicas=2 --replicas=3 deployment/mysql
</pre>
=== rollout ===
Pour redéployer. Ex :
<pre>
kubectl rollout restart deployment/mysql
</pre>
=== apply ===
Pour envoyer sur le serveur. Ex :
<pre>
kubectl apply -f service.yaml
</pre>
=== delete ===
Pour supprimer le serveur. Ex :
<pre>
kubectl delete service mon-service
</pre>
== Références ==
{{Références}}
10sclyx4izktbyt782phl1rxzwq2kkv
775319
775318
2026-10-16T13:34:00Z
JackPotte
5426
/* delete */
775319
wikitext
text/x-wiki
<noinclude>{{Docker}}</noinclude>
{{w|Kubernetes}} est un logiciel open-source sorti en 2015 pour orchestrer plusieurs conteneurs avec des options de quotas, {{wt|scalabilité horizontale}}, déploiement sans coupure de service et relance automatique en cas d'indisponibilité. Par ailleurs, il nécessite d'utiliser des images Docker déjà construites.
Pour ces raisons, il est plus orienté pour la production que pour le développement<ref>https://kubernetes.io/docs/setup/</ref>.
== Installation ==
=== Client ===
La configuration peut être stockée dans des fichiers .yaml et modifiée par la commande <code>kubectl</code><ref>https://kubernetes.io/fr/docs/reference/kubectl/overview/</ref>, ou via l'interface graphique ''Dashboard''<ref>https://kubernetes.io/fr/docs/tasks/access-application-cluster/web-ui-dashboard/</ref>. De plus, il est nécessaire de lancer un cluster contenant les conteneurs via <code>kubeadm</code><ref>https://kubernetes.io/docs/setup/production-environment/tools/kubeadm/install-kubeadm/</ref>.
==== Debian ====
Sur Debian ou ses dérivés<ref>https://kubernetes.io/docs/setup/production-environment/tools/kubeadm/install-kubeadm/</ref> :
<pre>
sudo apt-get install -y apt-transport-https ca-certificates curl gpg
curl -fsSL https://pkgs.k8s.io/core:/stable:/v1.29/deb/Release.key | sudo gpg --dearmor -o /etc/apt/keyrings/kubernetes-apt-keyring.gpg
echo 'deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg] https://pkgs.k8s.io/core:/stable:/v1.29/deb/ /' | sudo tee /etc/apt/sources.list.d/kubernetes.list
sudo apt-get update
sudo apt-get install -y kubelet kubeadm kubectl
sudo apt-mark hold kubelet kubeadm kubectl
</pre>
Test :
<pre>
kubectl version
</pre>
Aide :
<pre>
kubectl --help
</pre>
==== k9s ====
Pour éviter de taper chaque commande, il existe une interface graphique gratuite nommée k9s, pouvant exploiter la conf de kubectl : https://k9scli.io/topics/install/.
Cela nécessite le gestionnaire de paquets Homebrew :
<pre>
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
</pre>
Puis :
<pre>
brew install derailed/k9s/k9s
</pre>
Par exemple on peut déclencher un cronjob avec "t" ou rentrer dans un conteneur en shell avec "s".
{{remarque|si on ne voit aucun conteneur au premier lancement, changer l'espace de nom en tapant <code>:namespace</code> pour voir la liste puis entrée après sélection.}}
{{remarque|pour voir uniquement les conteneurs en cours sur k9s, taper : <code>/running</code>}}
== Types ==
=== pod ===
Un ''pod'' désigne un groupe de conteneur géré par Kubernetes<ref>https://kubernetes.io/fr/docs/concepts/workloads/pods/pod/</ref>.
C'est à travers lui que l'on peut configurer les readyness probe et liveness probe, permettant respectivement de bloquer le déploiement d'un conteneur en erreur et de le redémarrer s'il est en erreur<ref>https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/</ref>.
=== replica set ===
Un ''replica set'' désigne un ensemble de pods, dont le nombre maximum peut y être défini.
=== secret ===
Liste de variables d'environnement chiffrées, généralement des mots de passe.
Ils sont chiffrés via "kubeseal", à partir d'un fichier YAML non versionné contenant les chaines encodées en base64.
Ce fichier contient plusieurs paramètres, par exemple le type. Il en existe huit, "opaque" étant celui par défaut<ref>https://kubernetes.io/docs/concepts/configuration/secret/#secret-types</ref>.
La commande de chiffrement quant-à-elle permet de définir le scope des secrets<ref>https://github.com/bitnami-labs/sealed-secrets#scopes</ref> :
* strict (par défaut) : le nom et le namespace ne peuvent pas changer sans rechiffrer.
* namespace-wide : utilisable dans tout le namespace.
* cluster-wide : utilisable dans tout le cluster.
Toutefois on peut aussi les mettre dans le .yaml. Ex :
<pre>
metadata:
annotations:
sealedsecrets.bitnami.com/cluster-wide: "true"
</pre>
=== CronJob ===
Lance un conteneur dédié à une tâche planifiée (cron), à chaque fois qu'elle tourne<ref>https://kubernetes.io/docs/concepts/workloads/controllers/cron-jobs/</ref>.
=== Ingress ===
Accès à l'application (DNS).
=== PVC ===
Persistent volume : stocke des données indépendamment du redémarrage des pods.
== Commandes kubectl ==
=== get ===
Liste des éléments du serveur :
<pre>
kubectl get -A all
</pre>
=== create ===
Pour (re)lancer un job<ref>https://serverfault.com/questions/809632/is-it-possible-to-rerun-kubernetes-job</ref> :
<pre>
kubectl create job --from=job/mon_job mon_nouveau_job -n <namespace>
</pre>
=== logs ===
Pour voir le log de la sortie standard d'un conteneur :
<pre>
kubectl logs -f mon_conteneur
</pre>
=== exec ===
Pour rentrer dans un conteneur :
<pre>
kubectl exec -it mon_conteneur -- /bin/bash
</pre>
=== scale ===
Pour augmenter le nombre de pods dans un replicatset. Par exemple si on peut passer le nombre de 2 à 3 sur "mysql"<ref>https://jamesdefabia.github.io/docs/user-guide/kubectl/kubectl_scale/</ref> :
<pre>
kubectl scale --current-replicas=2 --replicas=3 deployment/mysql
</pre>
=== rollout ===
Pour redéployer. Ex :
<pre>
kubectl rollout restart deployment/mysql
</pre>
=== apply ===
Pour envoyer sur le serveur. Ex :
<pre>
kubectl apply -f service.yaml
</pre>
=== delete ===
Pour supprimer du serveur<ref>https://kubernetes.io/docs/reference/kubectl/generated/kubectl_delete/</ref>. Ex :
<pre>
kubectl delete service mon-service
</pre>
== Références ==
{{Références}}
91qm32lgw77yg3twdwda002q4iez0z9