1.4. Petit système d'exploitation
[Krakowiak (85)] Ce problème est consacré
à la réalisation d'un petit système doté d'un
moniteur d'enchaînement de travaux analogue à celui
étudié dans le problème 1. Le contexte de définition
du travail est très simplifié, par rapport à la
réalité, pour permettre d'aborder facilement quelques points de
cette réalisation. L'utilisation de cartes peut paraître
archaïque de nos jours, mais permet ici de prendre en compte simplement
l'asynchronisme des périphériques.
L'ordinateur comporte, outre le processeur et sa
mémoire, un lecteur de cartes, une imprimante et un terminal pour
l'opérateur. Un travail est constitué d'une paquet de cartes qui a
la structure suivante:
- une carte *JOB <nom de travail> <tmax>
<nlmax>,
- la suite des cartes contenant le texte source du
programme,
- une carte *DATA,
- la suite des cartes contenant les
données,
- une carte *FIN.
Les programmes sont écrits dans un langage unique. Ils
sont compilés en binaire absolu en mémoire, et sont
exécutés immédiatement. Le système d'exploitation,
compilateur compris, est entièrement résident en mémoire.
Il connaît l'adresse de lancement du compilateur, ainsi que l'adresse de
lancement d'un programme compilé, qui est toujours la
même.
Les cartes de commandes sont caractérisées par
un “*” en première colonne. Une carte comporte 80
caractères. Les paramètres figurant sur la carte *JOB
spécifient:
- le nom utilisé pour identifier le travail, et
qui sera imprimé en tête des listes de sorties,
- une durée maximale d'utilisation de
l'unité centrale,
- un nombre maximal de lignes à
imprimer.
Un train de travaux est constitué d'une suite de
travaux, terminée par une carte *FIN supplémentaire (le
train se termine donc par deux cartes *FIN consécutives).
L'opérateur prépare le train de travaux et lance son
exécution. Il communique avec le système à l'aide de son
terminal, et dispose de trois commandes, qu'il active en frappant le premier
caractère de la commande:
LANCER : lancer l'exécution d'un train de
travaux prêt dans le lecteur de cartes,
STOP : interrompre l'exécution
immédiatement (arrêt d'urgence),
TERMINER : interrompre l'exécution
après la fin du travail en cours.
Le système affiche sur le terminal de
l'opérateur le contenu des cartes *JOB au fur et à mesure
de leur lecture.
Le système assure une fonction
élémentaire de comptabilité: il imprime à la fin de
chaque travail le temps total d'unité centrale passé, le nombre de
cartes lues, le nombre de lignes imprimées.
Le système fournit aux utilisateurs trois appels au
superviseur:
SVC lire : lire une carte,
SVC écrire : imprimer une ligne,
SVC fin : fin d'exécution, retour au
système.
Ces instructions SVC ne sont pas directement
écrites par l'utilisateur mais insérées par le compilateur
dans le programme objet lorsque nécessaire.
Le pilotage des périphériques est assuré
par le processeur. Chaque périphérique envoie une interruption
à la fin du transfert d'une unité (une carte pour le lecteur de
cartes, une ligne complète pour l'imprimante, un caractère pour le
terminal de l'opérateur); un code identifie le périphérique
émetteur.
Les opérations sur les périphériques sont
réalisées de la façon suivante:
- lecteur de cartes ou imprimante, on dispose des
opérations:
- lancer_opération (périphérique, tampon) le
périphérique détermine le sens du transfert, et la
longueur. Le transfert est réalisé en DMA. À la fin du
transfert, le périphérique déclenche une
interruption.
- opération_correcte (périphérique) le
périphérique retourne un booléen qui est
vrai si la dernière opération
sur ce périphérique s'est déroulée
correctement.
- terminal opérateur, on dispose des
opérations suivantes:
- Lorsqu'un caractère
est reçu en provenance du terminal, une interruption
entrée_terminal est émise
vers le processeur.
- lire_terminal demande
à l'interface du terminal de délivrer le dernier caractère
reçu du terminal.
- écrire_terminal (caractère) demande
à l'interface du terminal d'envoyer le
caractère au terminal.
- Lorsque l'envoi d'un caractère vers le terminal est
achevé, une interruption
sortie_terminal est émise vers le
processeur.
Note: les interruptions de fin d'opération sont
toujours émises au bout d'un temps fini, inférieur à 0.5
seconde, même en cas d'erreur.
Une horloge est disponible, avec interruption au passage
à zéro. Le processeur a un mode maître et un mode esclave,
les interruptions d'horloge n'étant acceptées qu'en mode esclave.
La zone mémoire utilisée par le système est
protégée; une tentative d'accès dans cette zone par un
programme en mode esclave provoque un déroutement.
Toute interruption, appel au superviseur ou déroutement
entraîne le passage en mode maître. Le retour normal au programme
remet le mode esclave. Par ailleurs, deux opérations sont disponibles (en
mode maître) pour lancer ou arrêter un programme ou le compilateur,
et qui peuvent se décrire de la façon suivante:
type t_code = ( { code détermine la façon dont le programme est arrêté: }
NORMAL, { par appel SVC fin }
ERR_TEMPS, { par durée maximum atteinte }
ERR_LIGNES, { par nombre de lignes atteint }
ERR_TRAP, { par déroutement }
ERR_STOP ) { par décision de l'opérateur }
opération lancer_prog (e : adresse) : t_code;
début
sauver_état_moniteur;
aller à e; { passage en mode esclave }
fin;
opération arrêt_prog (code : t_code);
début
restaurer_état_moniteur; { restitue l'adresse d'appel de lancer_prog }
retourner (code);
fin;
A- Détailler
les procédures, internes au système d'exploitation, qui
gèrent les opérations sur chacun des
périphériques.
A.1- lire_carte (var
tampon : chaîne [80]) lance la lecture d'une carte dans le
tampon, attend la fin de l'opération
et la relance éventuellement en cas d'erreur.
A.2- imprimer (tampon :
chaîne [132]) attend que la ligne précédente
soit imprimée sans erreur et lance l'impression d'une nouvelle ligne
depuis le tampon.
A.3- envoi_term (tampon :
chaîne [], long : entier) assure l'envoi de la
chaîne constituée de
long caractères au
terminal.
B- En utilisant les procédures ci-dessus,
détailler les appels au superviseur proposés (SVC). On veillera
à garantir qu'une erreur dans un programme d'utilisateur ne mette pas en
jeu l'intégrité du système, et on assurera la
comptabilité minimale demandée.
C- Les commandes LANCER et TERMINER de l'opérateur
doivent être effectivement prises en compte uniquement lorsque le moniteur
est en train de s'exécuter. Ceci se fera simplement dans l'algorithme du
moniteur par consultation périodique de la présence d'une
commande. Par contre la commande STOP doit interrompre le programme (ou le
compilateur) s'il est en cours d'exécution, sans perturber le
fonctionnement du moniteur si c'est celui-ci qui s'exécute.
C.1- Proposer une solution pour cette prise en compte de
la commande STOP lorsque le programme est en cours.
Idée: faire faire la consultation lors des appels au
superviseur, des interruptions de fin de transfert ou lors des interruptions
d'horloge.
C.2- Détailler le traitement de toutes les
interruptions, en utilisant éventuellement ce qui a été
défini, ainsi que le déroutement et les appels au superviseur
(SVC).
D- Définir le traitement du moniteur
d'enchaînement des travaux, dans ce contexte.
Solution de l’exercice
1.4
1.4.1. Question A
1.4.1.1. Question A.1
Il faut lancer l'opération de lecture d'une carte, et
attendre l'interruption qui signale la fin de cette opération asynchrone.
En fait, on peut mettre à vrai un
booléen avant le lancement de l'opération, l'interruption
entraînant sa remise à faux.
On peut alors la relancer si elle est incorrecte.
var lecture_en_cours : booléen;
procédure lire_carte (var tampon : chaîne [80]);
début
répéter
lecture_en_cours := vrai; { pour savoir quand c'est terminé }
lancer_opération (lecteur, tampon); { opération asynchrone }
tant que lecture_en_cours faire fait; { attente }
jusqu'à opération_correcte (lecteur);
fin;
procédure interruption_carte;
début
lecture_en_cours := faux; { indication de fin d'opération }
fin;
1.4.1.2. Question A.2
Contrairement à la lecture de carte, où le
programme attend le contenu de la carte pour continuer, l'impression d'une ligne
peut se faire de façon autonome, pourvu que son exécution soit
garantie. Cette garantie ne peut être obtenue que si la ligne à
imprimer est mémorisée pour pouvoir relancer son impression si
l'opération n'a pas été correcte. Il s'ensuit que cette
relance doit être alors assurée par le sous-programme
d'interruption lui-même. Par ailleurs une demande d'impression doit
attendre que la précédente soit terminée. Ici encore un
booléen sera mis à vrai lors
de la demande, et remis à faux
lorsqu'elle sera exécutée.
var impression_en_cours : booléen;
tamp_loc : chaîne[132];
procédure imprimer (tampon : chaîne [132]);
début
tant que impression_en_cours faire fait; { attente fin de la précédente }
tamp_loc := tampon;
impression_en_cours := vrai; { indication opération en cours }
lancer_opération (imprimante, tamp_loc); { sans attente de fin }
fin;
procédure interruption_imprimante;
début
si opération_correcte (imprimante) alors
impression_en_cours := faux; { autorisation de la suivante }
sinon
lancer_opération (imprimante, tamp_loc); { sans attente de fin }
finsi;
fin;
1.4.1.3. Question A.3
Comme pour l'impression d'une ligne, l'envoi au terminal d'un
ensemble de caractères, peut se faire en même temps que la
poursuite du travail. Il suffit de mettre les caractères à envoyer
dans un tampon local. Notons qu'ici, la difficulté n'est pas liée
aux erreurs éventuelles, puisqu'elles ne sont pas
détectées, mais au fait que le transfert des caractères est
programmé, c'est-à-dire, que le logiciel doit les envoyer un par
un. Ce transfert peut être dirigé par l'interruption
sortie_terminal. En particulier, c'est
l'interruption de fin de transfert du dernier caractère qui libère
le terminal, et autorise la prise en compte d'une nouvelle demande.
var tamp_term_loc : chaîne [80]; { caractères à émettre }
long_loc, ind_term : entier;
term_occupé : booléen; { il y en a encore }
procédure envoi_term (tampon : chaîne [], long : entier);
début
tant que term_occupé faire fait; { attente de la fin du précédent }
tamp_term_loc := tampon;
long_loc := long;
term_occupé := vrai; { initialisation }
ind_term := 1;
écrire_terminal ( tamp_term_loc [0] ); { sortie du premier caractère }
fin; { les suivants sur interruption }
procédure interruption_sortie_terminal;
début
si ind_term < long_loc alors { non fin de sortie en cours }
écrire_terminal ( tamp_term_loc [ind_term] );
ind_term := ind_term + 1;
sinon
term_occupé := faux; { fin de l'envoi au terminal }
finsi;
fin;
1.4.2. Question B
Lors d'un appel au superviseur, il faut d'abord
déterminer duquel il s'agit, et appeler la procédure
correspondante.
procédure appel_SVC (nature : (lire, écrire, fin) );
début
cas nature dans
lire: lecture_contrôlée ( tamp_utilisateur, code_retour );
écrire: impression_contrôlée ( ligne_utilisateur );
fin: arrêt_prog ( NORMAL );
fincas;
fin;
La
lecture des cartes doit être contrôlée de façon
à éviter de délivrer à un programme une carte de
commande. S'il n'y a plus de carte de données à lire il faut lui
retourner le code de fin de fichier. Cela implique qu'il est possible de lire
une carte de commande lors d'une demande du programme, et qu'il faut la
mémoriser jusqu'à la demande de cette carte par le moniteur. Par
ailleurs, pour faire la comptabilité, il faut compter les cartes
effectivement lues pour le JOB en cours.
var commande_lue : booléen; { une carte de commande est présente,
initialisé à faux par le moniteur d'enchaînement }
tampon_carte : chaîne [80]; { tampon de carte de commande }
nb_cartes : entier; { nombre de cartes lues pour ce Job }
procédure lecture_contrôlée ( var t : chaîne[80]; var code : entier );
début
si non commande_lue alors
lire_carte (tampon_carte) ;
nb_cartes := nb_cartes + 1;
commande_lue := (tampon_carte [0] = '*');
finsi;
si commande_lue alors code := fin_fichier;
sinon t := tampon_carte ; code := OK; finsi;
fin;
L'impression
des lignes doit aussi être contrôlée de façon à
comptabiliser leur nombre et arrêter le programme s'il dépasse la
limite qu'il a annoncée. Notons que nous ne compterons pas les lignes
imprimées par le moniteur.
var nb_lignes, nlmax : entier;
procédure impression_contrôlée ( ligne : chaîne[132] );
début
si nb_ligne ≥ nlmax alors arrêt_prog (ERR_LIGNES) finsi;
nb_ligne := nb_ligne + 1 ;
imprimer (ligne);
fin;
1.4.3. Question C
1.4.3.1. Question C.1
Une commande de l'opérateur consiste en l'un des trois
caractères 'L', 'S' ou 'T'. Le sous-programme d'interruption
correspondant à
entrée_terminal peut donc consister
simplement en la lecture du caractère fourni, et en sa
mémorisation dans une variable globale s'il est l'un des trois. Il suffit
alors de comparer sa valeur à 'S' en différents endroits pour
garantir sa prise en compte effective:
- interruption du lecteur de cartes: il faut
arrêter la lecture, même si elle est avec
erreur.
- interruption de l'imprimante: il faut
remettre à faux le booléen
impression_en_cours sans consulter le
résultat de l'opération, de façon à libérer
l'imprimante, même s'il y a
erreur.
- interruption de sortie de terminal: il
n'est pas nécessaire de faire quelque chose, puisqu'il n'y a pas de
danger d'avoir une boucle infinie.
- interruption
d'horloge: comme elles ne peuvent survenir que si le processeur est en mode
esclave, on est certain que le programme ou le compilateur est actif à ce
moment. Il peut donc être arrêté pour donner le
contrôle au moniteur.
- appels au
superviseur: comme ils ne sont utilisés que par le programme ou le
compilateur, on peut également les arrêter pour donner le
contrôle au moniteur. Le test doit ici être fait avant
l'exécution des opérations de lecture ou d'écriture, ainsi
qu'après, car ces opérations peuvent être longues, à
cause des boucles en cas d'erreur. Par contre il est inutile de le faire en cas
de fin d'exécution.
1.4.3.2. Question C.2
Le traitement des interruptions est défini comme
suit:
var k_term : caractère; { dernier caractère de commande reçu }
procédure interruption_entrée_terminal;
var c : caractère;
début
c := lire_terminal;
si c = 'L' ou c = 'S' ou c = 'T' alors
k_term := c ; { simple mémorisation }
finsi;
fin;
procédure déroutement;
début
arrêt_prog ( ERR_TRAP );
fin;
var temps, tmax : entier;
procédure interruption_horloge;
début
sauver_contexte;
temps := temps + 1;
si temps = tmax alors arrêt_prog (ERR_TEMPS) finsi;
si k_term = 'S' alors arrêt_prog (ERR_STOP) finsi;
restitution_contexte;
fin;
procédure interruption_périphérique;
début
sauver_contexte;
cas demandeur dans
carte: interruption_carte;
imprimante: interruption_imprimante;
entrée_terminal: interruption_entrée_terminal;
sortie_terminal: interruption_sortie_terminal;
fincas;
restitution_contexte;
fin;
Les
procédures de lecture de cartes doivent être modifiées comme
suit:
var lecture_en_cours, fin_lecture : booléen;
procédure lire_carte (var tampon : chaîne [80]);
début
répéter
lecture_en_cours := vrai; { pour savoir quand c'est terminé }
lancer_opération (lecteur, tampon);
tant que lecture_en_cours faire fait; { attente }
jusqu'à fin_lecture;
fin;
procédure interruption_carte;
début
fin_lecture := (k_term = 'S' ou opération_correcte (lecteur));
lecture_en_cours := faux; { indication de fin d'opération }
fin;
Les
procédures d'impression sont modifiées comme suit (seule la
procédure d'interruption est en fait modifiée):
var impression_en_cours : booléen;
tamp_loc : chaîne[132];
procédure imprimer (tampon : chaîne [132]);
début
tant que impression_en_cours faire fait; { attente fin de la précédente }
tamp_loc := tampon;
impression_en_cours := vrai; { indication opération en cours }
lancer_opération (imprimante, tamp_loc); { sans attente de fin }
fin;
procédure interruption_imprimante;
début
si k_term = 'S' ou opération_correcte (imprimante) alors
impression_en_cours := faux; { autorisation de la suivante }
sinon
lancer_opération (imprimante, tamp_loc); { sans attente de fin }
finsi;
fin;
Le
traitement des appels au superviseur doit être modifié comme
suit:
procédure appel_SVC (nature : (lire, écrire, fin) );
début
si nature = fin alors arrêt_prog ( NORMAL ); finsi;
si k_term = 'S' alors arrêt_prog ( ERR_STOP ); finsi;
cas nature dans
lire: lecture_contrôlée ( tamp_utilisateur, code_retour );
écrire: impression_contrôlée ( ligne_utilisateur );
fincas;
si k_term = 'S' alors arrêt_prog ( ERR_STOP ); finsi;
fin;
1.4.4. Question D
Le moniteur d'enchaînement des travaux comporte d'abord
une procédure de recherche de carte de commande. Comme cette
procédure parcourt les cartes qui ne contiennent pas de '*' en
première colonne, elle doit vérifier que l'opérateur ne
demande pas l'arrêt immédiat du travail. Nous la décomposons
d'abord en une procédure de lecture d'une carte et de consultation, et
une procédure de recherche proprement dite.
procédure lecture_une_carte;
début
lire_carte (tampon_carte);
commande_lue := (tampon_carte [0] = '*');
si k_term = 'S' alors état := arrêt;
envoi_term ("STOP", 4);
finsi;
fin;
procédure recherche_carte_commande ;
début
tant que non commande_lue et état ≠ arrêt faire
lecture_une_carte;
fait;
fin;
Le
moniteur d'enchaînement des travaux est donné page suivante. Il
doit prendre en compte les commandes 'L' et 'T' de l'opérateur, et
assurer le lancement du compilateur puis du programme sauf si la compilation ne
s'est pas terminée normalement.
1.4.5. Remarques
Le système que nous venons de définir se compose
de trois couches:
- couche haute: moniteur d'enchaînement, et
contrôle des entrées-sorties demandées par les programmes
utilisateurs.
- couche moyenne: procédures
de gestion des périphériques, avec reprise en cas
d'erreur,
- couche basse: le traitement des
interruptions de périphériques et d'horloge, le traitement des
déroutements.
Les procédures de gestion du
lecteur de cartes et de l'imprimante sont réparties sur les trois
couches. Les procédures de gestion du terminal sont réparties sur
les deux couches basse et moyenne. Les SVC, les déroutements, le
débordement du temps sont transmis à la couche haute, qui prend
les décisions de poursuite ou d'arrêt du travail. Le
déroulement d'un tel ensemble est essentiellement séquentiel, les
interruptions étant en général le résultat d'une
commande préalable. Cependant, ceci n'est pas le cas pour l'interruption
d'horloge, et pour l'interruption dûe à la frappe d'un
caractère au clavier. L'interruption d'horloge ne peut survenir que
lorsque le compilateur ou le programme utilisateur sont en train de
s'exécuter. La décision d'arrêter alors le programme est
identique à l'appel au superviseur de fin de traitement. Par contre, la
frappe d'un caractère au clavier peut survenir à tout moment, et
doit être pris en compte en particulier lorsque le moniteur
d'enchaînement est en train de travailler. L'idée a
été de faire une gestion des entrées clavier rudimentaire,
puisque le sous-programme d'interruption correspondant assure simplement la
mémorisation du caractère frappé. Lorsque la machine est en
mode esclave, (compilation ou exécution), le sous-programme
d'interruption d'horloge et les appels au superviseur effectuent chacun la
consultation nécessaire. Lorsque la machine est en mode maître,
(moniteur d'enchaînement), c'est le moniteur lui-même qui assure
cette consultation dans son algorithme. Dans tous les cas, les sous-programmes
d'interruption des périphériques complètent la consultation
pour éviter les blocages.
La présence d'interruptions dans un système
entraîne un comportement non déterministe. La synchronisation entre
les procédures des différentes couches est obtenue simplement ici
par des variables communes, souvent des booléens. Ce n'est pas toujours
possible ainsi.
Noter les différentes “attentes actives” du
système, ce qui n'est pas trop gênant ici, car il n'y a rien
d'autre à faire, mais qu'il faudrait traiter autrement avec la
multiprogrammation.
programme moniteur_enchaînement ;
début
état := arrêt;
répéter
tant que état = arrêt faire
si k_term = 'L' alors état := normal; finsi;
fait; { attente opérateur }
envoi_term ("LANCER", 6);
commande_lue := faux;
recherche_carte_commande;
tant que état = normal faire
cas tampon_carte[1..3] dans
'JOB': début
envoi_term (tampon_carte);
imprimer (tampon_carte);
initialiser_comptabilité;
code_retour := lancer_prog (ad_compil);
si code_retour = NORMAL alors
recherche_carte_commande;
si état ≠ arrêt alors
commande_lue := ( tampon_carte[1..4] ≠ 'DATA' );
code_retour := lancer_prog (ad_prog);
finsi;
finsi;
imprimer_comptabilité ( code_retour );
si k_term = 'T' alors état := arrêt;
envoi_term ("TERMINER", 8);
finsi;
fin;
'FIN': début
lecture_une_carte;
si état ≠ arrêt et tampon[0..3] = '*FIN' alors
état := arrêt;
envoi_term ("FIN", 3);
finsi;
fin;
autre: commande_lue := faux;
fincas;
recherche_carte_commande;
fait;
finrépéter;
fin;