31 juillet 2020
Un système d’exploitation est un logiciel qui agit comme intermédiaire entre
Le système d’exploitation fournit un “environnement” dans lesquels les programmes d’application peuvent s’exécuter de façon
Le système d’exploitation fournit une API (Application Programming Interface), une bibliothèque d’appels de fonctions par lesquels les programmes d’application lui demandent d’exécuter une action.
Illustration, pendant l’exécution du programme C suivant
l’appel de printf
demande l’écriture d’une chaîne sur la “sortie standard” associée au processus, ce qui conduira (peut-être) le système d’exploitation à demander au “gestionnaire d’interface graphique” d’afficher des caractères dans une fenêtre, ce qui passera par des demandes d’accès au pilote de la carte graphique.
Ensuite, en retournant la valeur 0, la fonction main
signale au système la fin du programme, en fournissant le code de retour qui par convention signifie une fin normale.
Précision : printf()
est définie dans la bibliothèque standard du langage C*. Elle fait appel à la fonction write
qui fait partie de l’API du noyau du système d’exploitation. De même, le return
du main
entraîne l’exécution de l’appel système _exit()
.
Mais avant cette exécution, le système d’exploitation aura chargé le programme, c’est-à-dire :
Le matériel, les besoins des utilisateurs, et les systèmes d’exploitation ont évolué conjointement.
Dans les premiers ordinateurs, on ne pouvait charger qu’un programme à la fois dans une mémoire de quelques milliers d’octets.
On constate alors que, lorsqu’un programme exécute une opération d’entrée/sortie (sur bande magnétique, sur un terminal, …), le processeur doit attendre le résultat pour continuer : des milliers de “cycles” sont gaspillés pendant ces temps morts.
Avec une mémoire plus grande, on peut envisager d’avoir plusieurs tâches (programmes). Quand une tâche est bloquée en attente d’une réponse, le processeur peut faire avancer une autre tâche.
Le système d’exploitation est alors chargé de réveiller/endormir les tâches en fonction des requêtes d’entrée-sortie soumises par les tâches, et de l’arrivée des résultats, et de choisir une des tâches pour l’activer.
Ce “multitraitement” est facilité par l’introduction de mécanismes matériels spécifiques dans la conception des processeurs.
Dans une approche très simplifiée, le processeur d’un ordinateur “classique” possède un compteur de programme (PC = program counter), registre qui contient l’adresse de la prochaine instruction à exécuter.
Les circuits du processeur exécutent un cycle
“Passer à la suivante” consiste
Les interruptions ont été introduites initialement pour traiter les “exceptions arithmétiques” (débordements et autres).
Lorsque l’unité arithmétique détecte un débordement, un signal électrique est “levé”.
En présence de ce signal, la valeur 0 est chargée dans le PC. A cette adresse se trouve le code à exécuter en cas de débordement arithmétique.
L’exécution normale est donc interrompue, “détournée” vers une “routine de traitement de l’exception”.
A la fin de cette routine se trouve une instruction “retour d’interruption” qui permettra de revenir au code qui a été interrompu.
Il faut pour cela que le PC ait été sauvegardé lors de l’interruption, soit dans un registre spécial, soit dans une pile.
Une première idée, pour un système de multitraitement, serait d’interroger périodiquement (polling) les périphériques pour savoir si ils ont terminé le travail qu’on leur a demandé.
Mais il est beaucoup plus efficace que le périphérique lui-même indique qu’il requiert l’intervention du système d’exploitation (frappe sur un clavier, arrivée d’une trame sur une interface réseau, réponse d’un contrôleur de disques à qui on a demandé de lire un bloc, signal envoyé par un timer après un délai programmé, …).
Pour cela le périphérique “lève” un signal électrique qui représente une interruption matérielle, traitée comme précédemment.
En pratique, ces signaux sont reçus par un circuit “contrôleur d’interruptions”, qui transmettra au processeur un numéro d’interruption. Ce numéro permettra au processeur de trouver l’adresse du code à exécuter dans une “table de vecteurs d’interruption”1.
Un exemple sur la plateforme Arduino pour micro-contrôleurs. Sur cette plateforme
setup()
est appelée au démarrage du programme,loop()
est ensuite exécutée en boucle.attachInterrupt()
permet d’“armer” une interruption : elle indique quelle fonction sera exécutée quand l’évènement indiqué se produira./*
* Utilisation des interruptions sous Arduino.
*
* Objectif : quand on appuie sur le bouton, l'état de la LED Change.
*
* Fonctionnement :
* - dans setup(), la fonction changeState est associée à la montée du signal
* provenant du bouton
* - la fonction changeState() inverse (true <-> false) l'indicateur "on".
* - la fonction loop() reflète l'état de cet indicateur sur la LED.
*/
const int ledPin = 13;
const int buttonPin = 2;
boolean on = true;
void changeState() {
on = ! on;
}
void setup() {
pinMode(ledPin, OUTPUT);
pinMode(buttonPin, INPUT);
attachInterrupt(digitalPinToInterrupt(buttonPin), changeState, RISING);
}
void loop() {
if (on) {
digitalWrite(ledPin, HIGH);
}
else {
digitalWrite(ledPin, LOW);
}
}
En pratique, on veut souvent éviter d’interrompre le déroulement d’une routine de traitement. Pour cela on introduit un “masque d’interruptions”. Quand une interruption est masquée, elle est mise en attente, et sera prise en compte quand l’exécution de l’instruction RTI
“démasquera” les interruptions.
Plus généralement, il peut exister des niveaux d’interruptions : au niveau N, le processeur ne peut être interrompu que par une interruption de niveau plus élevé.
Enfin, il existe des instructions machine qui, quand elles s’exécutent, déclenchent une interruption. (SYSCALL
, SYSENTER
, TRAP
, int xxx
, …)
Ces instructions sont utilisées en particulier pour faire des “appels systèmes”.
Pour faire un appel système, le processus demandeur, qui s’exécute en mode normal (non privilégié) :
Exemple, en assembleur x86 sous MS-DOS 2.0 16 bits
org 0x100 ; adresse de chargement
mov dx,msg ; adresse chaine
mov cx,13 ; longueur chaine
mov bx,1 ; sortie standard
mov ah,0x40 ; service = "write device"
int 0x21
mov ah,0x4C ; service = "terminer le programme"
int 0x21
msg db "Hello, world!"
L’exécution de l’instruction int 0x21
provoque une interruption logicielle qui
0x21
en hexadécimal) dans la table de vecteurs d’interruption. Cette interruption regroupe les services d’entrée-sortie sous DOS.À partir de là, le système d’exploitation exécute les actions nécessaires (écriture sur l’écran) et relance l’exécution de la tâche interrompue en revenant au mode normal (ou pas, si il s’agissait d’arrêter le programme).
Note : UNIX comportait au départ 80 appels système. les systèmes actuels en ont plusieurs centaines.
Un système d’exploitation fournit des services aux programmes qui tournent dessus. Pour le programmeur d’application, ces services simplifient l’accès aux ressources.
Par exemple, quand un programmeur veut faire afficher quelque chose à l’écran, il écrit System.out.println("Coucou")
sans avoir besoin de connaître le type de carte écran, ou de carte réseau. Le système d’exploitation fournit une abstraction du matériel à travers une API.
Mais pour que ça se passe bien, il ne ne fait pas que les programmes utilisateurs puissent agir eux-mêmes directement sur le matériel en contournant l’API.
Les programmes d’application devront passer par des appels système, qui donneront la main au système d’exploitation, qui agira sur le matériel après avoir vérifié que ce qu’on lui demande est acceptable.
Pour cela, les processeurs2 ont (au moins) deux modes de fonctionnement3
Certaines instructions (accès aux périphériques par exemple) ne peuvent être exécutées qu’en mode privilégié. En mode normal, toute tentative d’exécuter de telles instructions provoquent une exception “instruction illégale”.
En mode normal, certaines instructions (int
, sysenter
, …) déclenchent une interruption logicielle qui fait repasser en mode privilégié. Elles servent à réaliser des “appels système”.
Nous verrons plus loin les mécanismes de protection mémoire qui font qu’une tâche qui s’exécute en mode normal ne pourra accéder qu’à la partie de la mémoire qui lui est affectée.
Les instructions (et les données) du système d’exploitation sont dans une zone mémoire inaccessible aux programmes “normaux”.
C’est l’exécution d’une interruption logicielle qui fait basculer le processeur en mode privilégié, et le dirige vers un “guichet d’entrée”, où sont vérifiés les paramètres qui indiquent le service voulu.
Un système d’exploitation est un ensemble de logiciels destiné à faciliter l’utilisation (l’exploitation) d’une machine par les programmes d’application.
Le système d’exploitation prend en charge l’accès aux ressources matérielles (mémoire, périphériques etc.).
Un système multi-tâches fait tourner plusieurs programmes présents en mémoire simultanément.
Pour tenir ce rôle avec efficacité, des mécanismes matériels ont été introduits dans la conception des processeurs dans les années 50-60 :
Ces mécanismes permettent de faire respecter des principes :
Lecture : https://www.ssi.gouv.fr/guide/recommandations-pour-la-mise-en-place-de-cloisonnement-systeme/
INT
, SYSENTER
, …)Un vecteur d’interruption est une structure de données qui contient en particulier l’adresse d’une routine de traitement d’interruption.↩
quand ils sont destinés aux systèmes mutitâches. Ce n’est pas le cas des micro-contrôleurs des cartes Arduino “de base”.↩
Sur l’architecture x86 des PC, il y a 4 modes, appelés “anneaux de protection” (rings). La plupart des systèmes d’exploitation n’en utilisent que 2.↩