Objectifs de conception et concepts fournis
La première section énumère quelques prédicats d'une bonne programmation. Ceux-ci ont considérablement influencé le développement de Medos-2 et sont donc mentionnés ici. Le Medos-2 se présente au programmeur normal comme une collection de modules Modula-2 distincts. L'aspect orienté objet de ces modules (et du système) est présenté dans la section suivante. Le Medos-2 fournit un concept simple mais puissant pour l'exécution des programmes. Il est expliqué dans une autre section. Le mécanisme de gestion des ressources est expliqué dans l'avant-dernière section, et la dernière section énumère les concepts contribuant à l'ouverture de Medos-2.
Objectifs de conception
Plusieurs objectifs ont guidé le développement de Medos-2. Dès le début, l'intention était de créer un bon système d'exploitation de taille modeste. Mais que signifie le terme «bon système d'exploitation» ou «bon programme» en général ? La réponse varie d'un individu à l'autre.
Parmi l'ensemble des prédicats spécifiant la qualité d'un programme (par exemple modulaire, portable, efficace, général, bien structuré,...), le plus important est qu'un programme soit fiable. On ne peut pas dépendre d'un programme peu fiable. Un programme fiable doit être à la fois correct et compréhensible, c'est-à-dire que son comportement doit être prévisible pour ses utilisateurs. Un programme fiable doit cependant également être robuste aux erreurs courantes. En général, l'exactitude d'un programme ne peut pas être prouvée formellement. En utilisant des langages de programmation de haut niveau, la relecture, le débogage, les tests d'exécution et d'autres techniques, on peut tôt ou tard obtenir la conviction qu'un (certain morceau d'un) programme est correct. La robustesse aux erreurs est généralement obtenue par la redondance des données entreposées et des programmes accédant aux données. La redondance permet de détecter les incohérences d'état et d'éviter ainsi qu'une seule défaillance ne provoque de nombreuses erreurs successives (c'est-à-dire un désastre). La redondance peut bien sûr également aider à rétablir un état cohérent après une situation erronée, sans perte d'informations.
Un programme doit cependant aussi être simple, adaptable et, autant que possible, efficace. La simplicité est nécessaire pour rendre un programme compréhensible pour ses utilisateurs et pour son ou ses implémenteurs. Si un implémenteur ne comprend pas un algorithme à programmer, on ne peut pas s'attendre à ce qu'il fournisse un programme correct (et fiable). Il n'est cependant généralement pas facile de trouver des solutions simples pour résoudre un grand nombre de problèmes (plus ou moins) complexes. La théorie et l'expérience semblent toutes deux très importantes. La plupart des programmes volumineux sont écrits pour être utilisés pendant plusieurs (voire de nombreuses) années. Il est donc important qu'un programme soit structuré de manière à pouvoir s'adapter facilement aux «petits» changements des conditions préalables du programme, pouvant changer au fil des ans. Un programme doit bien sûr aussi être efficace, si cette efficacité n'entre pas en conflit avec les attributs susmentionnés des bons programmes. Il est pénible de vivre avec des programmes inutilement lents. Heureusement, la plupart du temps, les solutions simples (et pas simplistes !) sont aussi des solutions efficaces.
Si dans un cas donné, plusieurs des objectifs mentionnés sont contradictoires, la fiabilité doit être considérée comme l'objectif le plus important, certainement celui ne devant pas être soumis à un compromis.
Medos-2 en tant que collection de modules
Medos-2 se présente au programmeur comme une collection de modules de bibliothèque pouvant être importés à tout moment à partir de n'importe quel module écrit par l'utilisateur. Cette modularisation de l'interface du système d'exploitation et sa nature passive (un module exporte généralement seulement quelques routines) sont très souhaitables. Cela change simplement la vision du programmeur sur le système d'exploitation, n'étant plus un superviseur, c'est-à-dire un monolithe que le programmeur doit accepter, mais un ensemble de fonctionnalités fournies (services, gestionnaires de ressources, gestionnaires de périphériques,...) qui, en principe, peuvent être facilement modifiées et qu'un programmeur n'a pas besoin de connaître complètement. Dans ce qui suit, il est décrit comment cette interface orientée objet est fournie par des modules séparés dans Medos-2.
Selon le modèle dit objet, un système d'exploitation peut être décrit comme un ensemble de types d'objets, chacun pouvant être considéré comme une sorte de ressource. Certaines ressources ont une réalisation physique directe (périphériques d'entrées/sorties), d'autres sont «seulement» artificielles (processus, fichiers,...). Chaque ressource est décrite par une instance du type d'objet, c'est-à-dire par un objet. L'objet décrit l'état de la ressource, et une opération sur l'objet correspond à un changement d'état de la ressource. Par exemple, considérons le type Fichier et les opérations sur une instance d'un fichier : Créer, Supprimer, Écrire, Réinitialiser, Lire,... Dans ce modèle, une ressource (un objet) est passive, et un utilisateur d'une ressource doit seulement comprendre l'ensemble fixe d'opérations définies pour la ressource. La représentation de la ressource (par exemple sous forme d'informations stockées ou de matériel) et la mise en oeuvre des opérations sur la ressource ne sont pas essentielles pour son utilisateur. De nombreux systèmes d'exploitation développés sont orientés objet, par exemple CAP, iMAX, StarOS, Pilot et Solo. L'encapsulation des objets (c'est-à-dire la protection des ressources) est prise en charge par le matériel dans les trois premiers exemples. Medos-2, Pilot et Solo n'offrent tout simplement aucune protection absolue (c'est-à-dire incontournable) des ressources par logiciel.
La vue du modèle objet d'un système d'exploitation peut être facilement exprimée dans des langages de programmation de haut niveau prenant en charge les types de données abstraits (c'est-à-dire l'encapsulation de données). Dans les programmes écrits dans de tels langages, un type d'objet (un type de ressource ou un autre type de ressource) est exprimé comme un type abstrait, et un objet (une ressource ou un autre type de ressource) est exprimé comme une instance du type abstrait correspondant. Les types de données abstraits ne sont cependant directement pris en charge que par quelques langages de programmation de haut niveau, et ceux-ci ne sont pas largement acceptés. CLU, Concurrent Pascal, Euler, PORTAL et Simula 67 sont des exemples de tels langages.
Des modules séparés de Modula-2 peuvent toutefois être utilisés pour fournir des interfaces avec de nombreux aspects d'encapsulation de données souhaités. On peut essentiellement distinguer trois cas si un module séparé est utilisé de cette manière :
Un module exporte des routines opérant sur un ensemble de données (représentant une ressource). Comme il existe exactement une instance du type, l'instance n'a pas besoin d'être identifiée pour chaque routine appelée. Les données nécessaires à la représentation de l'instance sont simplement déclarées globalement dans le module d'implémentation correspondant et initialisées par le code d'initialisation du module.
Un module exporte un type et une collection de routines opérant sur des variables (instances) de ce type. Une variable du type exporté passée en paramètre à une routine exportée identifie l'instance sur laquelle l'opération correspondante doit être effectuée. Modula-2 permet d'exporter des types opaques. Dans ce cas, la structure du type exporté est inconnue des clients du module.
Un module peut fournir une interface à plusieurs abstractions de données, présentées dans l'une des deux variantes mentionnées ci-dessus.
Un module Modula-2 séparé fournissant des opérations sur exactement un ensemble de données (un objet) correspond le mieux à l'idée d'encapsulation de données. Les données représentant l'instance d'un type abstrait peuvent être totalement cachées dans la partie implémentation du module séparé et l'initialisation des données peut être garantie. De plus, il est impossible d'exécuter des opérations sur des données inexistantes car les données existent tant que le module est accessible (chargé).
Un module exportant un type et une collection de routines opérant sur des variables du type exporté supporte moins bien l'aspect d'encapsulation de données. Une variable du type exporté correspond à une instance du type abstrait, mais afin de conserver l'aspect de masquage, la variable est dans la plupart des cas effectivement une référence à un élément de données représentant une instance du type abstrait. La structure de l'élément de données peut ainsi être cachée dans la partie implémentation du module. L'allocation et la désallocation (création et suppression) d'instances du type de données abstrait doivent être explicitement effectuées par des appels de procédure. Mais la durée de vie d'un objet est idéalement la même que l'existence de la variable représentant l'objet. Des opérations sur des variables non initialisées peuvent se produire si la première opération exécutée sur une variable du type exporté n'est pas une opération créant l'objet correspondant. L'absence de désallocation automatique des éléments de données non référencés rend difficile la limitation de la durée de vie des objets que des programmes erronés «oublient» de désallouer explicitement.
La raison pour laquelle une interface est fournie à plusieurs types d'objets dans un module séparé est généralement soit un désir de commodité, soit un besoin de cacher (protéger) «quelque chose» étant autrement librement disponible pour les clients.
Depuis Medos-2, le module Terminal fournit des routines de lecture ou d'écriture sur le terminal standard. Le module fournit une interface à une seule ressource. Le module Frames fournit des routines pour l'allocation et la désallocation d'un segment de mémoire principale, appelé trame. Un paramètre de type FramePointer identifie la trame sur laquelle les routines fournies doivent être traitées. Le module DisplayDriver est du troisième type. Les ressources gérées par le module sont l'interface d'affichage, le bitmap par défaut et la police de caractères par défaut.
Exécution de programmes
Un programme est la formulation d'un algorithme dans une notation formalisée (langage de programmation). La définition presque toujours utilisée pour un processus, à savoir l'exécution d'un programme (séquentiel), n'a de sens que partiellement pour les programmeurs étudiant les programmes informatiques du «monde réel». De nombreux programmes informatiques utilisent explicitement la concurrence pour la description des algorithmes. La concurrence est ainsi exprimée soit par le langage de programmation, soit par des primitives du système d'exploitation. De nombreux langages de programmation de haut niveau fournissent des primitives pour exprimer la concurrence (par exemple Ada, Concurrent Pascal, Portal, Edison, Mesa), principalement en offrant la possibilité de déclarer des processus. De nombreux systèmes d'exploitation fournissent des primitives pour exécuter plusieurs programmes par un processus, soit l'un après l'autre, soit (moins souvent) par un mécanisme d'appel de programme.
Dans le texte suivant, le terme programme (ou programme source) désigne une description textuelle d'un algorithme formulé selon les règles d'un langage de programmation. Comme Medos-2 ne prend en charge essentiellement que l'exécution de programmes Modula-2, les programmes mentionnés seront généralement des programmes Modula-2. Si aucune confusion n'est possible, le terme programme peut également signifier un programme activé (en cours d'exécution).
Un processus est une unité d'activité effectuant séquentiellement des opérations sur des objets, par exemple en exécutant séquentiellement des instructions d'un programme Modula-2. En général, un programme peut être exécuté par un ou plusieurs processus concurrents, et un processus peut éventuellement exécuter une partie d'un ou plusieurs programmes. Comme un ordinateur n'a le plus souvent qu'un seul processeur central et que plusieurs processus peuvent être nécessaires pour exécuter des programmes, la plupart des systèmes d'exploitation incluent un planificateur de processus multiplexant le processeur entre les processus.
Le Medos-2 ne prend pas en charge la concurrence explicite dans les programmes écrits par l'utilisateur. Du point de vue du système d'exploitation, un seul processus exécute tous les programmes écrits par l'utilisateur. Le processus le fait en exécutant ce que l'on appelle des appels de programme ou des activations de programme. L'exécution d'un programme peut être considérée comme une généralisation de l'exécution d'une procédure. Tout programme peut contenir une instruction (un appel de programme) provoquant l'activation d'un programme comme un appel de procédure. Pendant l'exécution du programme appelé, l'appelant du programme est suspendu, et il reprend lorsque le programme appelé est terminé.
Tous les programmes activés forment une pile de programmes activés. Le premier programme de la pile est la partie résidente du système d'exploitation, c'est-à-dire l'interpréteur de commandes (partie résidente de celui-ci) avec tous les modules importés. Le programme en haut de la pile est le programme en cours d'exécution. Dans Medos-2, il existe cependant des différences essentielles entre l'activation d'un programme et l'activation d'une procédure Modula-2. Un programme est identifié par un nom de programme calculable (une chaîne de caractères).
Le programme appelant est également repris si un programme se termine par un crash (gestion des exceptions). Les ressources telles que la mémoire et les fichiers connectés appartiennent aux programmes et sont récupérées à nouveau lorsque le programme propriétaire se termine (gestion des ressources). Un programme ne peut être activé qu'une seule fois à la fois, c'est-à-dire qu'il n'y a pas d'instances, pas de récursivité (les programmes ne sont pas réentrants).
Le code d'un programme est chargé lorsque le programme est activé et est supprimé de la mémoire principale lorsque le programme se termine.
A tout moment, le niveau d'activation dynamique ou simplement le niveau de programme identifie de manière unique un programme activé. Le niveau de programme 0 est le premier programme activé, c'est-à-dire la partie résidente de Medos-2. Si le niveau de programme / appelle un programme, le programme activé est au niveau I+1. Le niveau du programme en cours d'exécution, c'est-à-dire le programme activé en haut de la pile d'activation, est ce qu'on appelle le niveau courant. L'appelant d'un programme peut indiquer par un paramètre à la procédure d'appel de programme que le programme appelant et le programme appelé peuvent partager des ressources chaque fois que cela est possible. Le niveau de programme le plus bas partageant des ressources avec le niveau courant est ce qu'on appelle le niveau partagé.
Un programme activé est représenté par un enregistrement d'activation de programme dans la pile. Actuellement, l'enregistrement d'activation contient une pile de travail ainsi que le code et les données globales de tous les modules chargés pour le programme appelé.
Chaque fois qu'un programme est activé, son module principal est chargé (instancié, activé). Tous les modules importés directement ou indirectement sont également chargés s'ils ne sont pas utilisés par des programmes déjà activés, c'est-à-dire s'ils ne sont pas déjà chargés. Dans ce dernier cas, le programme venant d'être appelé est lié aux modules déjà chargés. Cela est analogue aux procédures imbriquées d'un programme structuré en blocs, où les règles de portée garantissent que les objets déclarés dans le bloc englobant peuvent être consultés à partir d'une procédure interne.
Après l'exécution d'un programme, son enregistrement d'activation est supprimé de la pile. Les modules ayant été chargés lors de l'activation du programme sont supprimés. À ce moment, toutes les ressources (objets) appartenant au programme activé sont également renvoyées. De plus amples détails sur la gestion des ressources seront décrits dans la section suivante. Les principaux avantages du concept décrit pour l'exécution des programmes se situent dans les trois domaines suivants :
La pile de programmes activés permet une utilisation très élevée de la mémoire (sans aucune fragmentation). En général, l'architecture de Lilith ne prend pas en charge le déplacement des données utilisées en mémoire. Dans ces circonstances, les stratégies d'allocation de mémoire plus générales que le schéma de pile ne garantissent pas une utilisation de la mémoire comparablement élevée en raison d'une possible fragmentation de la mémoire. Dès le début, l'espace mémoire disponible (65 536 mots) était un facteur critique. Le système d'exploitation occupait environ 12 kmots et la mémoire de rafraîchissement pour l'affichage (le bitmap) utilisait 28,5 kmots. Il ne restait que 23,5 kmots pour l'exécution des programmes écrits par l'utilisateur. C'était à peine plus que ce étant nécessaire pour exécuter le compilateur Modula-2 et l'éditeur standard. (Plus tard, 65 536 mots supplémentaires de mémoire principale ont été ajoutés à toutes les machines, et le bitmap est maintenant alloué dans cette moitié (généralement non adressable) de la mémoire.)
La liaison d'un programme activé à des programmes activés à des niveaux inférieurs est très souhaitable. Ce mécanisme permet à chaque utilisateur d'étendre dynamiquement l'ensemble des fonctionnalités (modules) fournies par le système d'exploitation. Par exemple, cela peut être fait en exécutant un programme important (directement ou indirectement) les modules supplémentaires. Son module principal peut par exemple être l'interpréteur de commandes standard. Les programmes ordinaires écrits par l'utilisateur seront alors exécutés comme des programmes de niveau 2, et ils peuvent également utiliser les fonctionnalités fournies par le programme de niveau 1 (par exemple un module permettant la communication sur le réseau local). Le compilateur multi-passes Modula-2 utilise la même fonctionnalité. La partie commune à toutes les passes est exécutée comme un programme appelé modula. Ce programme active ensuite les passes en mode partagé. Les modules du programme modula sont utilisés pour la communication entre les différentes passes, et l'exécution d'une passe en mode partagé garantit que le tas et les fichiers inter-passes ne sont pas désalloués lorsqu'un programme de passe de compilation se termine.
Le concept d'activation d'un programme comme d'une procédure est très pratique pour les programmeurs. La liaison du programme activé aux programmes déjà activés est assez naturelle, en particulier pour les programmeurs habitués aux langages de programmation de haut niveau autorisant les déclarations de procédures imbriquées. Laisser les programmes activés être propriétaires de ressources (objets), tout comme les procédures activées sont «propriétaires» de variables déclarées localement, est un concept à la fois pratique et très puissant. La gestion des ressources est cependant l'objet de la section suivante.
Gestion des ressources
Dans une section précédente, il a été mentionné que les objets sont gérés par des modules séparés, c'est-à-dire qu'un module peut fournir des routines créant, supprimant et effectuant d'autres opérations sur un certain type d'objets. Dans Medos-2, la stratégie d'allocation et de désallocation des objets ainsi que les règles régissant l'accès aux objets peuvent être choisies librement par le concepteur du module fournissant les objets (le gestionnaire d'objets). Le Medos-2 est à cet égard un système d'exploitation mono-utilisateur très «libéral» ou ouvert. Cependant, il n'est généralement pas sans coût pour un module de fournir des objets. Le nombre maximal d'objets pouvant être fournis est donc limité, soit à un nombre maximal fixe, soit à un nombre maximal variable d'objets. La stratégie d'allocation des objets ne pose que peu de problèmes. Tant qu'un gestionnaire d'objets peut allouer un objet, il le fera généralement. Les règles contrôlant l'accès à un objet sont également très libérales dans la plupart des cas : un programme peut exécuter une opération sur un objet si l'objet (et donc aussi son gestionnaire) existe. Les règles de portée normales de Modula-2 peuvent bien sûr limiter la visibilité des objets existants et ainsi entraver l'accès à un objet spécifique, et le gestionnaire d'objets peut introduire des règles spéciales sur l'utilisation d'un certain type d'objets. Des difficultés surviennent cependant si un programme erroné «oublie» de désallouer un objet qu'il a alloué. Les gestionnaires d'objets doivent limiter la durée de vie des objets de telle sorte que les deux objectifs contradictoires suivants soient satisfaits : il doit être garanti qu'un objet existe lorsqu'il peut être utilisé légalement, c'est-à-dire que les objets référencés doivent exister aussi longtemps que des références à ces objets existent afin d'éviter les références suspendues. D'autre part, la durée de vie des objets doit être aussi courte que possible, afin de réduire ou d'éliminer les coûts des objets inutilisés. Si des objets non accessibles restent alloués à l'infini, un système finira tôt ou tard par planter (virtuellement) à cause du manque de ressources nécessaires à la création de nouveaux objets.
Dans les langages de programmation de type Pascal, la durée de vie d'un objet (par exemple, un type, une variable) est, à quelques exceptions près, égale au temps d'exécution de la procédure au cours de laquelle l'objet est déclaré. Les variables locales d'une procédure sont instanciées lorsque la procédure est appelée et sont supprimées lorsque la procédure se termine. Les règles de portée garantissent que les objets ne sont accessibles que tant qu'ils existent. Les langages de programmation prenant en charge les types de données abstraits contrôlent généralement la durée de vie des instances de types abstraits par les mêmes règles.
Le Modula-2 prend également en charge l'allocation et la désallocation explicites de variables. Ces variables sont référencées par des pointeurs. Il est cependant généralement impossible de connaître le nombre de pointeurs référençant une variable allouée dynamiquement. La conséquence est qu'une variable n'étant plus référencée ne peut pas être automatiquement récupérée lorsque la dernière référence à celle-ci est supprimée. La variable n'étant plus référencée doit rester allouée tant que la déclaration de type de pointeur correspondante existe. Comme la plupart des types de pointeurs sont déclarés globalement dans les programmes, un récupérateur de mémoire n'est normalement pas inclus dans un "allocateur de stockage" pour les programmes Modula-2 (ou Pascal). Une variable allouée reste allouée jusqu'à ce qu'elle soit explicitement libérée ou que le programme se termine. Dans ce dernier cas, toutes les déclarations de type de pointeur ont normalement disparu.
Si un programme Modula-2 est exécuté par Medos-2, une déclaration de type peut survivre à l'exécution d'un programme, car tous les modules importés par le programme ne sont pas «jetés» lorsque le programme se termine. Les modules utilisés pour l'exécution de niveaux de programme inférieurs restent activés. Afin de pouvoir néanmoins supprimer automatiquement les objets n'étant plus utilisés, le concept suivant a été introduit : un objet appartient à un programme activé.
Un objet appartenant au niveau de programme / ne peut être accessible qu'à partir du même niveau de programme ou de niveaux de programme supérieurs. Une référence à un objet appartenant au niveau de programme / est supposée être entreposée dans un programme activé à un niveau k supérieur ou égal à / ou dans un objet appartenant à un tel programme. Un objet peut donc être supprimé lorsque son programme propriétaire se termine, et qu'aucun autre objet ne détient de référence à l'objet. Dans une section précédente, il a été indiqué que chaque type d'objet (chaque type de ressource) est géré par un module séparé. Si un seul objet est fourni par un module et que cet objet existe tant que le module est activé, alors il n'y a pas de problèmes extraordinaires. L'objet existe tant qu'il est accessible. Lorsque, après la fin d'un programme, le module n'est plus importé, il est supprimé avec l'objet fourni. Le module Terminal de Medos-2 est un exemple de module de cette classe.
Plus problématiques sont les modules fournissant un ou plusieurs objets pouvant être créés explicitement par différentes activations de programme. De tels modules exportent généralement un type et un ensemble de routines effectuant des opérations sur des variables du type exporté. Dans de rares cas, un objet peut être entièrement décrit par une variable du type exporté. Dans de tels cas, la durée de vie d'un objet n'est bien sûr pas plus longue que la durée de vie de la variable le décrivant. En général, cependant, une variable du type exporté sert uniquement à identifier (contenir une référence à) l'objet fourni. Le module suivant illustre ce cas :
- DEFINITION MODULE Streams;
-
- EXPORT QUALIFIED
- Stream, Create, Remove, Read, EOS (*, ... *);
-
- TYPE Stream;
-
- PROCEDURE Create(VAR s: Stream; name: ARRAY OF CHAR; new: BOOLEAN);
- PROCEDURE Remove(VAR s: Stream);
- PROCEDURE Read(VAR s: Stream; VAR ch: CHAR);
- PROCEDURE EOS(VAR s: Stream): BOOLEAN;
-
- (* ... *)
- END Streams.
Un programme peut utiliser le module Streams pour lire des caractères. Une variable de type Stream représente un flux lorsqu'il a été créé et jusqu'à ce qu'il soit supprimé. Le module de programme suivant illustre l'utilisation d'un flux :
L'activation du programme possédant un objet créé explicitement doit généralement être déterminée lors de la création de l'objet. Dans la plupart des cas, il s'agira soit du programme en cours d'exécution, soit du programme partagé (le programme au niveau d'activation dynamique le plus bas partageant des ressources avec le programme en cours).
Le système d'exploitation Medos-2 fournit trois routines (dans le module Monitor), aidant les modules à contrôler la durée de vie des objets : les procédures de fonction CurrentLevel et SharedLevel, et la procédure appropriée TermProcedure. CurrentLevel renvoie le numéro de niveau du programme en cours d'exécution et SharedLevel renvoie le numéro de niveau du programme partagé. Par un appel à la procédure TermProcedure dans la partie initialisation d'un module, le module déclare une routine sans paramètre comme étant sa routine de réinitialisation. La routine de réinitialisation sera appelée chaque fois qu'un programme se termine, à condition que le programme ait été activé à un niveau supérieur à celui dans lequel le gestionnaire d'objets est chargé. Toutes les routines de réinitialisation définies sont appelées avant la suppression réelle du programme, et dans l'ordre inverse de leur déclaration en tant que procédures de réinitialisation.
Cette implémentation simple du module Streams illustre comment un gestionnaire d'objets peut utiliser ces trois routines pour la suppression automatique d'objets lorsqu'un programme se termine.
- IMPLEMENTATION MODULE Streams;
-
- FROM FileSystem IMPORT
- File, Response, Lookup, Close, ReadChar, WriteChar;
-
- FROM Monitor IMPORT CurrentLevel, SharedLevel, TermProcedure;
-
- CONST streams = 16;
-
- TYPE
- Stream=CARDINAL;
- StreamDesc=RECORD
- free:BOOLEAN;
- owner:CARDINAL;
- f:File
- END;
-
- VAR
- streamTab: ARRAY [0..streams-1] OF StreamDesc;
-
- PROCEDURE Create(VAR s:Stream; name:ARRAY OF CHAR; new:BOOLEAN);BEGIN
- s:=0; (* rechercher une entrée libre dans streamTab *)
- LOOP
- IF s >= streams THEN (* ... *) HALT END;
- IF streamTab[s].free THEN EXIT END;
- INC(s)
- END;
- WITH streamTab[s] DO
- Lookup(f,name,new);
- IF f.res<>done THEN (* ... *) HALT END;
- owner:=SharedLevel(); (* un flux est partageable *)
- free:=FALSE;
- END
- END Create;
-
- PROCEDURE Remove(VAR s:Stream);BEGIN
- IF(s>=streams) OR streamTab[s].free THEN RETURN END;
- WITH streamTab[s] DO Close(f); free:=TRUE END
- END Remove;
-
- PROCEDURE Read(VAR s:Stream;VAR ch:CHAR);BEGIN
- IF(s>=streams) OR streamTab[s].free THEN (* ... *) HALT END;
- ReadChar(streamTab[s].f, ch)
- END Read;
-
- PROCEDURE EOS(VAR s:Stream):BOOLEAN;BEGIN
- IF (s >= streams) OR streamTab[s].free THEN (* ... *) HALT END;
- RETURN streamTab[s].f.eof
- END EOS;
-
- (* ... *)
-
- PROCEDURE ResetLevel;
- VAR
- s:Stream;
- BEGIN
- FOR s:=0 TO streams-1 DO
- WITH streamTab[s] DO
- IF NOT free AND (owner >= CurrentLevel()) THEN Remove(s) END
- END
- END
- END ResetLevel;
-
- VAR
- s:Stream;
-
- BEGIN
- FOR s:=0 TO streams-1 DO streamTab[s].free := TRUE END;
- TermProcedure(ResetLevel)
- END Streams.
Les flux de données sont partageables. La procédure Create définit le propriétaire d'un nouveau flux de données égal au niveau actuellement partagé, c'est-à-dire à SharedLevel(). (Un gestionnaire d'objets ne peut fournir des objets partageables que si tous les objets référencés par lui dans son implémentation sont également partageables. Les fichiers fournis par le module FileSystem sont partageables !)
La procédure ResetLevel est appelée à chaque fois qu'un programme se termine. ResetLevel supprime tous les flux appartenant au programme se terminant ou appartenant à des niveaux de programme encore plus élevés. Le fichier nécessaire à l'implémentation d'un flux de données appartient au même niveau de programme que le flux de données. Le fait que le module Streams importe le module FileSystem et qu'il n'y ait pas d'importations circulaires entre les modules dans Medos-2 garantit que la partie d'initialisation du module Streams est exécutée après l'exécution de la partie d'initialisation du module FileSystem. La routine de réinitialisation du module Streams est donc définie après la routine de réinitialisation (c'est-à-dire la procédure ResetLevel) du module FileSystem. Comme les routines de réinitialisation sont appelées dans l'ordre inverse de leur annonce au module Monitor, la procédure ResetLevel du module Streams sera appelée avant une routine de réinitialisation éventuellement définie du module FileSystem. Cet ordre des appels de routine de réinitialisation empêche qu'un objet soit automatiquement supprimé avant qu'un objet contenant une référence à celui-ci ne soit supprimé.
L'exemple ci-dessus montre de manière informelle que les trois routines CurrentLevel, SharedLevel et TermProcedure fournies dans Medos-2 prennent en charge de manière simple et puissante la suppression automatique des objets n'étant plus utilisés. Elles constituent l'un des piliers de la capacité de Medos-2 à récupérer des ressources après un plantage du programme.
Un autre pilier de la récupérabilité fournie réside dans la programmation minutieuse des routines effectuant des opérations sur des objets. Une opération sur un objet doit idéalement être exécutée de manière indivisible afin de maintenir des données cohérentes. De graves problèmes peuvent survenir lorsqu'un programme plante pendant l'exécution d'une procédure exportée par un module restant activé après la suppression du programme terminé. Medos-2 récupère de la plupart des plantages de programme dans les «moments malheureux» en rendant au moins l'opération de suppression, c'est-à-dire la routine de réinitialisation, d'un gestionnaire d'objets réexécutable et insensible aux données incohérentes, et en ordonnant correctement les instructions effectuant des modifications sur un objet. Les états incohérents détectés, dont le système ne peut pas récupérer, conduisent cependant au suicide du système.
Concepts offrant une ouverture
L'ouverture souhaitée de Medos-2 repose sur plusieurs concepts :
Le fait que tout programme puisse exécuter des programmes et que les programmes activés ne soient pas uniquement liés au système résident rend le système extensible. Un programme de niveau 1 peut par exemple inclure une collection de modules fournissant un environnement totalement modifié pour les programmes de niveaux supérieurs. Les programmes exécutés au niveau 2 ou même à des niveaux supérieurs s'exécuteront effectivement sur un autre système d'exploitation. Le système XS-1 est implémenté de cette manière. XS-1 est un système mono-utilisateur fournissant une interface utilisateur intégrée et des fichiers structurés hiérarchiquement.
La liaison des programmes activés au moment du chargement et la méthode de recherche de modules non encore chargés sur les fichiers facilitent la substitution d'implémentations écrites par l'utilisateur aux modules publics, l'ajout et la correction de Medos-2 et la fourniture de nouveaux modules. En général, aucune distinction n'est faite entre les programmes ou modules fournis par l'utilisateur et ceux fournis par le système. Par conséquent, tout utilisateur est libre de développer les modules nécessaires à son application spéciale.
L'idée d'étendre le système d'exploitation standard par un programme non résident fournissant des fonctionnalités supplémentaires est également rendue possible par le fait que les gestionnaires d'objets non résidents peuvent récupérer des plantages de programme de la même manière que les modules résidents, à savoir en utilisant des routines de réinitialisation. Le nombre de gestionnaires d'objets peut changer au cours de la durée de vie du système.
Plusieurs modules résidents permettent aux programmes non résidents de fournir des routines étant en fait des implémentations nouvelles ou supplémentaires des modules résidents. La possibilité de déclarer des types de procédures et des variables de types de procédures dans Modula-2 facilite la fourniture de cette fonctionnalité. Par exemple, les routines Read et Write du module Terminal peuvent être remplacées par des routines écrites par l'utilisateur. Un programme non résident peut remplacer l'entrée du clavier par des caractères lus dans un fichier et ainsi introduire des fichiers de commandes. Un autre endroit du système où ce concept est fourni est dans le système de fichiers. Toutes les opérations sur un fichier sont codées dans un appel à l'une des deux procédures, à savoir la procédure FileCommand et DirectoryCommand exportées depuis le module FileSystem. Un module (résident ou non-résident) peut implémenter des fichiers sur un support et les rendre accessibles via le module FileSystem par un appel à la routine "create medium". Cette routine a besoin comme paramètre du nom du support sur lequel le module fournit les fichiers, et des deux routines correspondant à FileCommand et DirectoryCommand.
L'ouverture du système est également directement prise en charge par les interfaces du module. Un exemple de cela est le module FileSystem. L'utilisateur inexpérimenté peut utiliser les fichiers de manière très ordinaire. Les routines Lookup, Close, ReadChar, WriteChar,... permettent une utilisation simple des fichiers. Les utilisateurs plus exigeants (par exemple les programmeurs système) peuvent obtenir de meilleures performances et/ou une plus grande flexibilité en accédant directement aux tampons de fichiers, en construisant eux-mêmes les répertoires de fichiers, en effectuant des accès aléatoires aux fichiers,... Le même module FileSystem fournit également des routines permettant ce type de programmation «de bas niveau».
La possibilité d'accéder à tous les modules séparés du système résident, et même d'accéder au matériel si nécessaire, rend également le système plus ouvert. Ce type d'ouverture n'est pertinent ou utile que pour un nombre relativement limité de programmeurs expérimentés. L'accès direct au matériel peut bien sûr provoquer le crash du système, s'il n'est pas soigneusement programmé. Néanmoins, cette liberté est considérée comme essentielle par les utilisateurs du poste de travail.