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:
- 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.
- 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:

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:
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;