# Fonctionnement d'un ordinateur/Le partage de l'espace d'adressage : avec et sans multiprogrammation

Après avoir vu qu'un processeur pouvait gérer plusieurs espaces d'adressage, nous allons voir comment les programmes gèrent les espaces d'adressage. Nous allons étudier le cas où un seul programme d'éxecute, mais aussi celui où plusieurs programmes partagent la mémoire. Nous allons voir les deux cas, l'un après l'autre. Mais avant toute chose, parlons de la protection mémoire.

## La protection mémoire : généralités

Sans protection particulière, les programmes peuvent techniquement lire ou écrire les données des autres. Si un programme pouvait modifier les données d'un autre programme, on se retrouverait rapidement avec une situation non prévue par le programmeur, avec des conséquences qui vont d'un joli plantage à des failles de sécurité dangereuses. Il faut donc introduire des mécanismes de protection mémoire, pour isoler les programmes les uns des autres, et éviter toute modification problématique. La protection mémoire regroupe plusieurs techniques assez variées, qui ont des objectifs différents. Elles ont pour point commun de faire intervenir à des niveaux divers le système d'exploitation et le processeur.
Le premier objectif est l'isolation des processus. 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 utilisaeur, vue dans le chapitre sur les interruptions. Mais il y en a d'autres, comme nous le verrons dans ce chapitre.

## Un seul espace d'adressage, non-partagé

Le cas le plus simple est celui où il n'y a pas de système d'exploitation et où un seul programme s'exécute sur l'ordinateur. Le programme a alors accès à tout l'espace d'adressage. L'usage qu'il fait du ou des espaces d'adressage dépend de si on est sur une architecture Von Neumann ou Harvard.
Sur une architecture Von Neumann, le programme et ses données sont placées dans le même espace d'adressage. Le programme organise la mémoire en plusieurs sections, dans lesquelles le programme range des données différentes. Typiquement, on trouve quatre sections, qui regroupent des données suivant leur utilisation. Voici ces trois sections :
* Le segment text 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.
* Le segment pour la pile, de taille variable.
* le reste est appelé le tas, de taille variable.
Sur une architecture Harvard, le programme est placé dans un espace d'adressage à part du reste. Le problème, c'est que cet espace d'adressage ne contient pas que le code machine à exécuter. Il contient aussi des constantes, à savoir des données qui gardent la même valeur lors de l'exécution du programme. Elles peuvent être lues, mais pas modifiées durant l'exécution du programme. L'accès à ces constantes demande d'aller lire celles-ci dans l'autre espace d'adressage, pour les copier dans l'autre espace d'adressage. Pour cela, les architectures Harvard modifiées ajoutent des instructions pour copier les constantes d'un espace d'adressage à l'autre.
La pile et le tas sont de taille variable, ce qui veut dire qu'ils peuvent grandir ou diminuer à volonté, contrairement au reste. Entre le tas et la pile, on trouve un espace de mémoire inutilisée, qui peut être réquisitionné selon les besoins. La pile commence généralement à l'adresse la plus haute et grandit en descendant, alors que le tas grandit en remontant vers les adresses hautes. Il s'agit là d'une convention, rien de plus. Il est possible d'inverser la pile et le tas sans problème, c'est juste que cette organisation est rentrée dans les usages.
Il va de soi que cette vision de l'espace d'adressage ne tient pas compte des périphériques. C'est-à-dire que les schémas précédents partent du principe qu'on a un espace d'adressage séparé pour les périphériques. Dans le cas où les entrées-sorties sont mappées en mémoire, l'organisation est plus compliquée. Généralement, les adresses associées aux périphériques sont placées juste au-dessus de la pile, dans les adresses hautes.
La protection mémoire avec seul espace d'adressage non-partagé est très simple. On n'a pas besoin d'isolation des processus, la gestion des droits d'accès est minimale quand elle existe. Par contre, on a besoin de protection de l'espace exécutable. Sur les architectures Harvard, le code machine et les données sont dans des espaces d'adressage séparés, et le code machine est dans une ROM, la protection de l'espace exécutable est donc garantie. Sur une architecture Von Neumann, elle ne l'est pas du tout.

## Un seul espace d'adressage, partagé avec le système d'exploitation

Maintenant, étudions le cas où le programme partage la mémoire avec le système d'exploitation. Sur les systèmes d'exploitation les plus simples, on ne peut lancer qu'un seul programme à la fois. Le système d'exploitation réserve une portion de taille fixe réservée au système d'exploitation, alors que le reste de la mémoire est utilisé pour le reste et notamment pour le programme à exécuter. Le programme est placé à un endroit en RAM qui est toujours le même.

### L'OS utilise soit les adresses basses, soit les adresses hautes

D'ordinaire, le système d'exploitation est est en mémoire RAM, comme le programme à lancer. Il faut alors copier le système d'exploitation dans la RAM, depuis une mémoire de masse. Sur d'autres systèmes, le système d'exploitation est placé dans une mémoire ROM. C'est le cas si le système d'exploitation prend très peu de mémoire, comme c'est le cas sur les systèmes les plus anciens, ou encore sur certains systèmes embarqués rudimentaires. Les deux cas font généralement un usage différent de l'espace d'adressage.
Si le système d'exploitation est copié en mémoire RAM, il est généralement placé dans les premières adresses, les adresses basses. A l'inverse, un OS en mémoire ROM est généralement placé à la fin de la mémoire, dans les adresses hautes. Mais tout cela n'est qu'une convention, et les exceptions sont monnaie courante. Il existe aussi une organisation intermédiaire, où le système d'exploitation est chargé en RAM, mais utilise des mémoires ROM annexes, qui servent souvent pour accéder aux périphériques. On a alors un mélange des deux techniques précédentes : l'OS est situé au début de la mémoire, alors que les périphériques sont à la fin, et les programmes au milieu.

### La protection mémoire quand l'espace d'adressage est partagé avec l'OS

Sur de tels systèmes, il n'y a pas besoin d'isolation des processus, juste de protection de l'espace exécutable. Protéger les données de l'OS contre une erreur ou malveillance d'un programme utilisateur est nécessaire. Notons qu'il s'agit d'une protection en écriture, pas en lecture. Le programme peut parfaitement lire les données de l'OS sans problèmes, et c'est même nécessaire pour certaines opérations courantes, comme les appels systèmes. Par contre, la protection de l'espace exécutable doit rendre impossible au programme d'écrire dans la portion mémoire du système d'exploitation.
Dans le cas où le système d'exploitation est placé dans une mémoire ROM, il n'y a pas besoin de faire grand chose. Une écriture dans une ROM n'est pas possible, ce qui fait que l'OS est protégé. Le processeur peut détecter ce genre d'accès et terminer le programme fautif, mais ce n'est pas une nécessité. Mais dans le cas où le système d'exploitation est chargé en RAM, tout change. Il devient possible pour un programme d'aller écrire dans la portion réservée à l'OS et d'écraser le code de l'OS. Chose qu'il faut absolument empêcher.
La solution la plus courante est d'interdire les écritures d'un programme de dépasser une certaine limite, en-dessous ou au-dessus de laquelle se trouve le système d’exploitation. Pour cela, le processeur incorpore un registre limite, qui contient l'adresse limite au-delà de laquelle un programme peut pas aller. Quand un programme applicatif accède à la mémoire, l'adresse à laquelle il accède est comparée au contenu du registre limite. Si cette adresse est inférieure/supérieure au registre limite, le programme cherche à accéder à une donnée placée dans la mémoire réservée au système : l’accès mémoire est interdit, une exception matérielle est levée et l'OS affiche un message d'erreur. Dans le cas contraire, l'accès mémoire est autorisé et notre programme s’exécute normalement.

## Un seul espace d'adressage, partagé entre plusieurs programmes

Les systèmes d’exploitation modernes implémentent la multiprogrammation, le fait de pouvoir lancer 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. Les programmes s’exécutent à tour de rôle sur un même processeur, ils partagent la mémoire RAM, etc. Le partage du processeur est géré au niveau logiciel par le système d'exploitation, et il ne nous intéressera pas ici. Par contre, le partage de la RAM et demande la coopération du logiciel et du matériel, ce qui nous intéressera dans ce chapitre.
Un point important est le partage de la RAM entre les différents programmes. Le système d'exploitation répartit les différents programmes dans la mémoire RAM et chaque programme se voit attribuer un ou plusieurs blocs de mémoire. Ils sont appelés des partitions mémoire, ou encore des segments. Dans ce qui va suivre, nous allons parler de segments, pour simplifier les explications. Nous allons partir du principe qu'un programme est égal à un segment, pour simplifier les explications, mais sachez qu'un programme peut être éclaté en plusieurs segments dispersés dans la mémoire, et même être conçu pour ! Nous en reparlerons dans le chapitre sur le mémoire virtuelle.
Toutefois, cela amène un paquet de problèmes qu'il faut résoudre au mieux. Le premier problème est tout simplement de placer les segments dans des sections vides de l'espace d'adressage, mais c'est quelque chose qui est du ressort du système d'exploitation proprement dit. Par contre, cela implique qu'un segment peut être placé n'importe où en RAM et sa position en RAM change à chaque exécution. En conséquence, les adresses des branchements et des données ne sont jamais les mêmes d'une exécution à l'autre. L'usage de branchements relatifs résout en partie le problème, mais il reste à corriger les adresses des données.
Pour résoudre ce problème, le compilateur considère que le segment commence à l'adresse zéro. En clair, les programmes sont conçus sans tenir compte des autres programmes en mémoire, à savoir qu'ils sont compilés de manière à accéder à toutes les adresses disponibles à partir de l'adresse zéro, à tout l'espace d'adressage. Mais l'OS ou le processeur corrigent les adresses internes au segment, en décalant toutes les adresses du segment à partir de sa base. Cette correction est en général réalisée par l'OS, mais il existe aussi des techniques matérielles, que nous verrons dans la suite du chapitre.
Il faut aussi prendre en compte le phénomène de l'allocation mémoire. Derrière ce nom barbare, se cache quelque chose de simple : les programmes peuvent déclamer de la mémoire au système d'exploitation, pour y placer des données. Les langages de programmation bas niveau supportent des fonctions comme malloc, qui permettent de demander un bloc de mémoire de N octets à l'OS, qui doit alors accommoder la demande. S'il n'y a pas assez de mémoire, l'appel échoue et le programme doit gérer la situation. De même, un programme peut libérer de la mémoire qu'il n'utilise plus avec des fonctions comme free. Avec des segments, cela revient à changer la taille d'un segment, plus précisément à la fin du segment. Et tout cela est géré par le système d'exploitation, aussi on laisse cela pour plus tard.
Un dernier problème est que les programmes peuvent lire ou écrire dans le segment d'un autre. Si un programme pouvait modifier les données d'un autre programme, on se retrouverait rapidement avec une situation non prévue par le programmeur, avec des conséquences qui vont d'un joli plantage à des failles de sécurité dangereuses. Il faut donc introduire des mécanismes de protection mémoire, pour isoler les segments les uns des autres. Nous parlerons de ces mécanismes dans quelques chapitres.
Il arrive cependant que des programmes partagent une même zone de mémoire, pour échanger des données. En effet, les systèmes d'exploitation modernes gèrent nativement des systèmes de communication inter-processus, très utilisés par les programmes modernes. Les implémentations les plus simples consistent soit à partager un bout de mémoire entre processus, soit à communiquer par l’intermédiaire d'un fichier partagé. Et le partage de la mémoire entre deux processus est très simple avec un espace d'adressage unique. Il suffit de manipuler la protection mémoire pour qu'elle autorise aux deux programmes d'accéder à un même segment. Les adresses utilisées par les deux programmes sont les mêmes.

### La protection mémoire avec des registres de base et limite

Chaque programme commence à une adresse précise et se termine à une autre. Les accès mémoire d'un programme doivent donc rester dans cet intervalle d'adresse. Pour cela, le système d'exploitation mémorise l'adresse de départ et de fin de chaque segment. Le processeur utilise ces deux adresses pour vérifier que les accès mémoire sont dans les clous. Quand il démarre un programme, le système d'exploitation charge ces deux adresses dans le processeur, dans deux registres spécialisés : le registre de base pour l'adresse du début du segment, le registre limite pour l'adresse de fin du segment.
Les deux registres servent à vérifier si un programme qui lit/écrit de la mémoire au-delà de sa partition. Le processeur vérifie pour chaque accès mémoire ne déborde pas au-delà de la partition qui lui est allouée, ce qui n'arrive que si l'adresse d'accès dépasse la valeur du registre limite. A noter que le registre de base est parfois utilisé pour la relocation matérielle, à savoir que le processeur ajoute automatiquement l'adresse de base à chaque accès mémoire. La relocation garantit que les adresses utilisées commencent à l'adresse de base, grâce au registre de base. Du moins, c'est le cas si l'addition ne déborde pas au-delà de la mémoire physique, tout débordement signifiant erreur de protection mémoire.
Les deux registres ne sont accessibles que pour le système d'exploitation et ne sont généralement accessibles qu'en espace noyau. Lorsque le processeur exécute un programme, ou reprend son exécution, il charge les limites des partitions dans ces deux registres. Ces deux registres doivent être sauvegardés en cas d'interruption, mais pas d'appel de fonction.

### Les clés de protection

Les premiers IBM 360 disposaient d'un mécanisme de protection mémoire assez simple. 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.

## Un espace d'adressage par processus : l'abstraction matérielle de processus

L'usage de partitions mémoire est assez complexe, mais est encore en cours aujourd'hui, sous des formes plus ou moins élaborées. Mais de nos jours, la relocation est gérée autrement. La différence principale est que l'on a pas un espace d'adressage partagé entre plusieurs programmes. Grâce à diverses fonctionnalités du processeur, chaque programme a son propre espace d'adressage rien que pour lui ! Le fait que chaque programme ait son propre espace d'adressage ne porte pas de nom, mais on pourrait l'appeler abstraction matérielle des processus. Nous verrons comment elle est implémentée dans la section suivante, qui porte sur l'abstraction mémoire, mais nous pouvons donner quelques explications sur l'abstraction des processus.

### Le noyau est mappé en mémoire

Le noyau du système d'exploitation a son propre espace d'adressage, séparé des autres. C'est une sécurité qui isole le noyau et les programmes, et les force à communiquer à travers des appels systèmes. Cependant, sachez que cette isolation n'est pas parfaite, volontairement. Dans l'espace d'adressage d'un programme, les adresses hautes sont remplies avec une partie du noyau ! Le programme peut utiliser cette portion du noyau avec des appels systèmes simplifiés, qui ne sont pas des interruptions, mais des appels systèmes couplés avec un changement de niveau de privilège (passage en espace noyau nécessaire). L'idée est d'éviter des appels systèmes trop fréquents. É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é.
L'espace d'adressage est donc séparé en deux portions : l'OS d'un côté, le programme de l'autre. On retrouve la même chose que ce qu'on avait avec un espace d'adressage unique, partagé entre un OS et un programme. 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 64 bits, la situation est plus complexe.
Sur les systèmes x86 64 bits, l'espace d'adressage est en théorie coupé en deux, la moitié basse pour le programme, la moitié haute pour le noyau. Mais en réalité, les systèmes x86 64 bits actuels ont un bus mémoire de 48 bits. Il manque donc 64 - 48 = 16 bits d'adresses, qui ne peuvent pas être utilisés pour adresser quoi que ce soit. Le résultat est que l'on a un bloc d'adresses invalides entre les adresses du noyau (les adresses hautes) et les adresses du programme (les adresses basses).
| 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. | Répartition des adresses entre noyau (jaune/orange) et programme (verte), sur les systèmes x86-64 bits, avec des adresses physiques de 57 bits. |

### La communication inter-processus et les threads

L'abstraction des processus fait que deux programmes ne peuvent pas se marcher sur les pieds. L'isolation des processus est donc garantie. Mais un gros problème est alors celui de la communication inter-processus, à savoir faire communiquer plusieurs processus entre eux. Il arrive régulièrement que des applications doivent coopérer pour faire leur travail, et échanger des données. L'isolation des processus met des bâtons dans les roues de ce partage.
Un moyen pour est de partager une portion de mémoire, accessible aux deux processus. Par exemple, l'un peut écrire dans cette zone, l'autre peut lire dedans. Mais l'isolation des processus fait que le partage de la mémoire est plus compliqué. Imaginez que deux programmes veulent partager une même zone de mémoire, pour échanger des données. La portion de mémoire sera placée à une certaine adresse physique en mémoire RAM. Mais cette adresse ne sera pas la même pour les deux programmes, vu qu'ils sont dans deux espaces d'adressage distincts. On peut résoudre ce problème, mais avec des mécanismes assez compliqués, dépendant des techniques de "mémoire virtuelle" qu'on verra au chapitre suivant.
Une autre méthode est de regrouper plusieurs programmes dans un seul processus, afin qu'ils partagent le même morceau de mémoire. Les programmes portent alors le nom de threads. Les threads d'un même processus partagent le même espace d'adressage. Ils partagent généralement certains segments : ils se partagent le code, le tas et les données statiques. Par contre, chaque thread dispose de sa propre pile d'appel.

### Les identifiants de processus intégrés au processeur

Pour simplifier la gestion de plusieurs processus, le processeur numérote chaque espace d'adressage. Le numéro est donc spécifique à chaque processus, ce qui fait qu'il est appelé les identifiants de processus CPU, aussi appelés identifiants d'espace d'adressage. Le processeur mémorise l'identifiant du programme en cours d'exécution dans un registre dédié. Le registre d'identifiant est modifié à chaque changement de processus, à chaque commutation de contexte.
L'identifiant de processus CPU est utilisé lors des accès mémoire, afin de ne se pas se tromper d'espace d'adressage. Il est utilisé pour les accès au cache, entre autres. Il sert à savoir si une donnée dans le cache appartient à tel ou tel processus, ce qui est utile pour la protection mémoire. Sans cela, chaque processus peut en théorie accéder à des données qui ne sont pas à lui dans le cache, en envoyant l'adresse adéquate. Nous en reparlerons dans le chapitre sur les mémoires caches.
Un défaut de cette méthode est que l'identifiant de processus est généralement codé sur une dizaine de bits, alors que le système d'exploitation utilise des identifiants de processus beaucoup plus larges, de 32 à 64 bits sur les CPU 32/64 bits. L'OS doit gérer la correspondance entre identifiants de processus CPU et ceux de l'OS. Parfois, pour cette raison, les OS n'utilisent pas toujours ce système d'identifiant de processus CPU.