Les premiers pas
On ne trouve pas dans la littérature une définition claire de ce qu'est un système d'exploitation. Des termes comme moniteur, superviseur et système exécutif sont essentiellement synonymes de système d'exploitation. La plupart des auteurs, cependant, conviennent de la nécessité d'un système d'exploitation et soutiennent que la principale raison d'avoir un système d'exploitation est la nécessité de partager des ressources. P. Brinch Hansen a écrit il y a une dizaine d'années qu'«un système d'exploitation est un ensemble de procédures manuelles et automatiques qui permettent à un groupe de personnes de partager efficacement une installation informatique». Ailleurs, il est indiqué que «le système d'exploitation est la partie du logiciel prenant en charge le partage des ressources». Le partage d'une installation informatique entre plusieurs (ou de nombreux) utilisateurs était, et est toujours, l'un des principaux problèmes résolus par les systèmes d'exploitation dans les installations coûteuses. Les ordinateurs personnels ne devraient cependant pas, par définition, être partagés entre différents utilisateurs, du moins pas en même temps. Comme les terminaux, ils sont utilisés exclusivement par une seule personne à la fois. Il faut donc répondre à deux questions avant de concevoir un système d'exploitation pour un ordinateur personnel : la première : pourquoi un système d'exploitation est-il nécessaire ? La seconde : si un système d'exploitation est nécessaire, pourquoi un nouveau système d'exploitation est-il nécessaire ? Les réponses à ces deux questions constituent également une réponse importante à la question des similitudes et des différences entre les systèmes d'exploitation.
La réponse à la première question est qu'un système d'exploitation permet de partager les ressources de manière très générale. Bien que le partage traditionnel des ordinateurs entre utilisateurs ne soit plus nécessaire pour un ordinateur purement personnel, le partage des ressources fourni par les systèmes d'exploitation dans d'autres domaines reste très important pour l'utilisateur. Quelques exemples illustrent cela : les systèmes de fichiers prennent en charge l'entreposage de nombreux fichiers sur un seul ou plusieurs disques, c'est-à-dire que de nombreux fichiers peuvent partager un ou plusieurs disques. Des programmes plus ou moins indépendants et même peu fiables peuvent être exécutés les uns après les autres, sans se gêner mutuellement, c'est-à-dire que la mémoire principale et l'unité centrale de traitement sont partagées par plusieurs calculs de manière pratique. Dans la plupart des cas cependant, le partage d'une ressource entre des calculs par ailleurs indépendants nécessite de fournir une sorte de protection afin de garantir l'intégrité de la ressource. Le partage de supports d'entreposage secondaires entre plusieurs fichiers permet, par exemple, de communiquer des informations non seulement entre utilisateurs mais, ce qui est au moins aussi important, également entre différents calculs d'un même utilisateur. L'environnement standardisé (machine abstraite) fourni par les systèmes d'exploitation rend possible l'échange de programmes et de routines entre utilisateurs, c'est-à-dire le partage de programmes et de routines. Un environnement standard cache également les différences de configuration, au moins dans les aspects les plus fréquemment utilisés. De plus, il contribue à rendre les logiciels plus utilisables par tous. Un codage standardisé des informations sur les fichiers augmente l'utilité des programmes. De nombreux systèmes d'exploitation recommandent aux programmeurs de coder un texte dans une séquence de caractères ASCII. Comme la plupart des programmeurs suivent ce conseil, il s'avère que le texte écrit par un programme (par exemple un éditeur) peut être lu par de nombreux autres programmes (compilateurs, générateurs de références croisées, formateurs de texte, programmes d'impression,...) et pas seulement par le programme lui-même.
Que peut-on attendre d'un nouveau système d'exploitation qu'aucun système existant ne propose ? La pire propriété d'un nouveau système d'exploitation est qu'il est nouveau. Les anciens programmes doivent être adaptés ou même reprogrammés, les nouveaux systèmes ont tendance à être moins fiables,... Pourquoi ne pas adapter un système interactif (à temps partagé) existant (par exemple UNIX) ou un système en temps réel à l'ordinateur personnel Lilith ? La réponse à cette question est similaire à celle de questions telles que : pourquoi ne pas acheter un système mini- ou micro-ordinateur existant et l'utiliser comme ordinateur personnel ? Pourquoi proposer et implémenter encore un autre langage de programmation ? De nombreuses hypothèses formulées il y a une décennie lors de la conception des systèmes d'exploitation actuels ont depuis changé. La plupart des systèmes d'exploitation (et en fait les plus performants d'entre eux) sont des systèmes à usage spécifique afin de fournir le service souhaité de manière pratique et économique. Les hypothèses de base modifiées impliquent que de meilleurs systèmes d'exploitation pourraient être écrits pour nos besoins actuels (et spécifiques). L'interface d'un système d'exploitation spécialisé et, même à un degré plus élevé, sa mise en oeuvre sont bien sûr influencées par les hypothèses formulées pour sa conception. A partir des années 1980, la taille des composantes matériels et leur prix ont tellement diminué qu'il est désormais presque indispensable de "donner" à chaque programmeur sérieux son propre ordinateur réel (et personnel) et non pas seulement une machine virtuelle dans un système informatique centralisé et partagé. Les conséquences sont que l'utilisateur peut se voir offrir un service interactif bien meilleur (temps de réponse plus court, réponses "intelligentes" à chaque frappe de touche,...) et que le système d'exploitation n'a plus à partager les ressources entre différents utilisateurs. Les développements des langages de programmation (Ada, Concurrent Pascal, Mesa, Modula-2, Pascal et bien d'autres) ainsi que le fait que chaque utilisateur dispose de son ordinateur privé, modifient considérablement la vision de l'environnement de programmation dans son ensemble. Un système d'exploitation pour un poste de travail moderne ne doit plus, par exemple, se contenter de fournir une machine en langage assembleur améliorée (et protégée matériellement) comme élément de base.
Historique du développement
Voici un bref historique du développement de Medos-2 : la conception principale a été réalisée entre le printemps 1979 et l'automne 1979, en tant que tâche à temps partiel en plus des tâches éducatives et du travail sur le compilateur Modula-2. La tâche de programmation a commencé en octobre 1979 et le premier test exécuté sur l'ordinateur Lilith a été réalisé le 23 décembre 1979. À cette époque, la fiabilité de Lilith était très faible. Le temps moyen entre deux pannes de mémoire principale était d'environ 10 minutes, le temps de chargement du programme de test à partir d'une petite cassette à bande magnétique à peu près le même ! La première version de Medos-2 (version 1) a été publiée en avril 1980. Au cours de l'été 1980, le compilateur croisé Modula-2 a été transporté d'un PDP 11/35 vers Lilith en une semaine. La deuxième version de Medos-2 a été publiée en octobre 1980. Elle comportait quelques modifications mineures dans l'interface du système de fichiers. La troisième version, sortie en juin 1981, incluait un mécanisme amélioré de récupération des erreurs dans les programmes utilisateur. A cette époque, plusieurs machines étaient équipées d'une mémoire principale de 128 kmots. L'espace d'adressage normal de Lilith n'est cependant que de 65536 cellules. Le Medos-2 a été adapté pour ne pas entreposer le bitmap de l'affichage dans la partie normalement adressable de la mémoire principale. La quatrième version du système est sortie en juin 1982. Son interface avec l'affichage a été modifiée, elle fournit une méthode d'allocation plus générale pour les mémoires principales supérieures à 64 kmots et elle utilise une technique de mise en mémoire tampon améliorée pour les fichiers sur disque. La version la plus récente du système (Medos-2 version 4.2) date de décembre 1982. Elle prend en charge l'identification des utilisateurs. Cela semble nécessaire si plusieurs postes de travail sont intégrés dans un système distribué par un réseau (local).
Entre-temps, de nombreux paquets et programmes ont été développés pour Lilith, tous basés sur Medos-2. La liste des programmes disponibles comprend les éléments les plus courants comme les éditeurs, les compilateurs, un débogueur et des utilitaires de fichiers ainsi qu'un système de base de données relationnelle et un nouveau système d'exploitation construit sur Medos-2.
Medos-2 et l'environnement donné
Le développement du système d'exploitation Medos-2 n'est qu'une partie d'un projet plus vaste : la construction d'un ordinateur personnel relativement puissant. En gros, chaque système d'exploitation doit fournir des fonctionnalités prenant en charge les applications souhaitées de la machine cible, dans ce cas le domaine de l'informatique personnelle. Certains des objectifs initiaux de Medos-2 sont énumérés dans la section suivante. Cependant, un système d'exploitation doit également s'adapter parfaitement à la machine cible et doit être bien implémenté. Deux piliers majeurs, sur lesquels Medos-2 est construit, doivent être caractérisés avant que la conception de Medos-2 ne soit discutée plus en détail, à savoir le matériel de Lilith et le langage de programmation Modula-2. La conception du matériel et du langage de programmation faisaient partie du projet visant au développement de la station de travail mono-utilisateur Lilith.
Objectifs de Medos-2
Lorsque la conception de Medos-2 a commencé au printemps 1979, Lilith était principalement considérée comme un ordinateur de table de travail dans un département informatique orienté logiciel. Deux domaines d'application essentiels étaient envisagés pour un poste de travail personnel dans un tel environnement, à savoir l'assistance de la machine pour effectuer des tâches de bureau répétitives telles que la préparation d'un document, la rédaction d'une lettre et l'envoi d'un message à un collègue, ou comme outil de développement, de débogage et d'exécution de programmes.
La première classe de problèmes peut être traitée en permettant aux utilisateurs d'exécuter un ou plusieurs programmes «standard» spécifiques à l'application (par exemple, des éditeurs, des formateurs, des programmes d'impression,...). Le principal avantage d'un ordinateur personnel pour ces applications réside dans son interface utilisateur potentiellement meilleure par rapport à celle d'un système à temps partagé ordinaire. Des temps de réponse plus courts, des informations plus nombreuses et de meilleure qualité sur l'écran (graphiques), une réponse à chaque frappe de touche et la disponibilité d'un dispositif de pointage (souris) sont quelques-uns des points clefs. Pour disposer d'un outil encore plus performant pour ce type d'applications, il est souhaitable de disposer de postes de travail connectés entre eux et à certains ordinateurs serveurs par un réseau local. Il est toujours pratique et plus économique de placer des périphériques coûteux, volumineux, bruyants et/ou rarement utilisés tels que des imprimantes, des unités de disques de grande capacité, des unités de bande magnétique et des multiplexeurs de communication dans des emplacements centraux et de les partager entre plusieurs utilisateurs.
Les programmeurs que Medos-2 est censé aider sont principalement des informaticiens, des ingénieurs en logiciel et des étudiants. Ces personnes aiment être libres dans l'utilisation de leur ordinateur personnel. Le système d'exploitation ne doit pas empêcher un programmeur de programmer et de tester même des logiciels orientés matériel comme un pilote pour une nouvelle interface, ne doit pas prescrire une certaine disposition sur l'affichage, ni remplir la mémoire avec du code résident. Ce qu'il faut, c'est un système exécutif résident relativement petit qui peut charger un programme à partir d'un fichier sur le disque et l'exécuter. Aucun mécanisme de protection absolu n'est nécessaire. Les mécanismes de protection supportés par le matériel tendent à être de la catégorie permettant "tout ou rien" ne semblant pas être d'une grande aide s'il est souhaitable, de temps à autre, d'utiliser des fonctionnalités spéciales ou nouvelles d'une certaine machine. L'aspect "ouverture" souhaité du système d'exploitation est important dans un environnement expérimental. Lorsque Medos-2 a été conçu, nous n'avions cependant aucune idée claire des conséquences que cet aspect avait sur un système d'exploitation, bien que B. W. Lampson ait plaidé en faveur d'un système d'exploitation ouvert à utilisateur unique en 1974. Pilot, un système d'exploitation développé par Xerox pour les ordinateurs personnels, est un exemple moderne de système ne fournissant aucun mécanisme de protection absolue. Pilot n'est cependant pas un petit système.
Il a également été décidé assez tôt que le système d'exploitation et tous les programmes utilisateur devaient être programmés dans le même langage de programmation, à savoir Modula-2. La règle de n'avoir qu'un seul langage de programmation disponible n'était pas considérée comme une restriction. Au contraire, il y a de nombreux avantages à programmer tous les programmes dans le même langage de programmation de haut niveau. Voici quelques exemples : Un seul compilateur doit être fourni. Les appels au système d'exploitation peuvent être vérifiés (au moins syntaxiquement) au moment de la compilation. L'interface avec le système d'exploitation peut être définie de telle sorte qu'il n'y ait aucune distinction entre les routines écrites par l'utilisateur, les routines de bibliothèque et les routines fournies par le système d'exploitation. On s'attendait également à ce que le concept de module de Modula-2 aide à structurer les programmes en unités gérables et fournisse un mécanisme de protection, sans doute contournable, mais avec une granularité beaucoup plus fine que les mécanismes absolus habituellement utilisés par les systèmes d'exploitation.
Le petit exécutif résident devait inclure un système de fichiers, simplement parce que le chargeur résident devait charger les programmes à partir de fichiers. Le système de fichiers doit être très robuste contre toutes les pannes matérielles, logicielles et utilisateur. Cette robustesse est souhaitable principalement parce que le système ne fournit aucun mécanisme de protection absolue et parce qu'une perte d'informations sur un disque peut souvent s'avérer irréversible. Bien entendu, le système de fichiers doit également être efficace à la fois en termes d'espace mémoire utilisé et en termes de temps d'accès, bien qu'aucune application en temps réel n'ait été envisagée pour Medos-2. Les performances générales d'une station de travail mono-utilisateur (et d'autres systèmes informatiques à usage général) dépendent cependant fortement de l'efficacité de son système de fichiers.
Un autre problème, devant être anticipé dès le début, était que plusieurs programmes d'application pouvaient s'avérer trop volumineux pour être entreposés dans la mémoire principale. Le premier programme critique était le compilateur Modula-2. Le compilateur était déjà programmé pour un mini-ordinateur avec un espace d'adressage relativement petit (PDP-11), et donc partitionné en plusieurs passes. Un bon support du chargement et de l'exécution des passes du compilateur et de la communication entre les passes était considéré comme adéquat dès le début, d'autant plus que Lilith ne supporte pas l'implémentation de la mémoire virtuelle.
Pour conclure la section, quelques phrases clefs caractérisant les objectifs de Medos-2 sont énumérées :
- Il doit être un système mono-utilisateur.
- Il doit prendre en charge l'exécution de programmes écrits en Modula-2.
- Il doit lui-même être programmé en Modula-2.
- Il doit permettre une utilisation efficace de la mémoire principale.
- Il doit être ouvert.
- Il doit inclure un système de fichiers robuste et efficace.
D'autres aspects du système d'exploitation n'étaient pas explicitement visés par Medos-2, par exemple : la concurrence dans les programmes utilisateur n'était pas considérée comme importante au début (aucune application en temps réel n'est prise en charge). Le support multi-utilisateur et la multiprogrammation n'étaient pas non plus visés. Le système de fichiers n'avait pas à prendre en charge les applications de base de données générales (par exemple les transactions atomiques, l'entreposage stable,...).
L'ordinateur cible Lilith
Le système d'exploitation Medos-2 est conçu pour le matériel informatique Lilith. Le matériel de Lilith est décrit dans un rapport de Niklaus Wirth. Cette section donne un bref aperçu d'une configuration Lilith typique et énumère certaines de ses principales caractéristiques matérielles.
Le matériel Lilith
Le matériel est une machine 16 bits et se compose d'une unité centrale de traitement microcodée, d'une mémoire multiport avec initialement 64 kword(16), d'interfaces pour un affichage à balayage raster, d'un unité de disque, d'un clavier, d'un dispositif de suivi de curseur (souris), d'un port de communication désynchronisé V-24 (RS-232) et d'une horloge de ligne. Plus tard, la mémoire principale a été étendue à 128 kword et un contrôleur pour un réseau local de type Ethernet à 3 MHz a été développé pour les stations de travail. Des interfaces pour une imprimante à faisceau laser (Canon LBP 10), pour un unité de disque d'environ 450 Mo (Fujitsu M 2351 A), pour un réseau X.25 et pour d'autres périphériques ont également été développées.
Le processeur central (CPU) possède une unité logique et arithmétique (ALU) de 16 bits basée sur quatre unités de bits AMD 2901. Une pile de registres pouvant contenir jusqu'à 16 entrées prend en charge l'évaluation des expressions. La mémoire de microcode est une mémoire PROM pour 2048 instructions de 40 bits chacune. Le microprocesseur fonctionne à un cycle d'horloge de base de 150 ns, le temps nécessaire pour interpréter une micro-instruction. Les instructions de macrocode les plus fréquentes correspondent en moyenne à environ 5 micro-instructions. Aucun mécanisme matériel n'a été fourni pour les traductions d'adresses (adresses virtuelles en adresses physiques) et aucun mécanisme de protection n'est pris en charge. Les périphériques ne sont pas cartographiés en mémoire, les interfaces sont connectées à un bus d'entrées/sorties séparé.
L'affichage est basé sur la technique de balayage raster utilisant 592 lignes de 768 points chacune. Chacun des 454 656 points est représenté dans la mémoire principale par un bit. L'ensemble du bitmap occupe donc 28 416 mots, soit environ 43 % de la mémoire principale initiale. Un deuxième type d'affichage fournissant 832 lignes de 640 points chacune a également été développé. Le bitmap pour cet affichage est un peu plus grand (33 280 mots), mais comme la mémoire principale contient généralement 128 kmots maintenant, la fraction utilisée pour le bitmap d'affichage est raisonnable (22 à 23 %). La représentation de chaque point (élément d'image) dans la mémoire principale accessible par programme rend l'affichage également adapté au texte, aux diagrammes techniques et aux graphiques en général. Dans le cas du texte, chaque caractère est généré en copiant le motif de points du caractère à l'endroit approprié de l'ensemble du bitmap de l'écran. Cela est fait par logiciel, soutenu par des routines microcodées, correspondant à des instructions spéciales. Cette solution offre la possibilité d'afficher des caractères selon différentes polices de caractères (c'est-à-dire que la taille, l'épaisseur, l'inclinaison et le style des caractères affichés sont modifiables par logiciel).
Un unité de disquette Honeywell Bull D120 pour disques amovibles est fourni. La capacité d'un disque est d'environ 9,8 Mo. Les cartouches de disque sont formatées par le fabricant avec 50 secteurs par piste et avec 392 pistes sur chacune de ses deux surfaces. 256 octets peuvent être entreposés dans chaque secteur. Le temps de positionnement moyen en rotation du lecteur est de 8,3 ms, le temps moyen de positionnement de la tête d'environ 65 ms.
La souris est un périphérique transmettant à l'ordinateur des signaux représentant les mouvements de la souris sur une surface plane (par exemple une table). Ces mouvements sont traduits (par un logiciel) en une position du curseur affichée sur l'écran. La précision de la position peut être aussi élevée que la résolution de l'écran. La souris contient également trois boutons poussoirs (touches) étant pratiques pour donner des commandes après avoir positionné la souris.
L'architecture Lilith
L'ensemble d'instructions de l'ordinateur Lilith est basé sur une architecture en pile. Ce M-code a été conçu de manière à pouvoir être généré facilement par un compilateur Modula-2 (ou des compilateurs pour d'autres langages de programmation de type Pascal) et à pouvoir également être exécuté efficacement. L'efficacité de l'exécution est due en partie au codage dense des instructions et en partie au fait que l'ensemble d'instructions permet une utilisation intensive des registres de base internes au processeur et de la pile de registres courts interne au processeur. La haute densité du code est obtenue non seulement par l'adressage implicite des résultats intermédiaires dans les expressions, mais surtout en fournissant différentes longueurs d'adresse et des modes d'adressage appropriés. La plupart des adresses dans les instructions sont des décalages relativement petits par rapport à l'un des registres de base.
Contrairement à la plupart des autres architectures implémentées, l'architecture M-code prend également en charge l'exécution de programmes partitionnés en plusieurs modules. Le mot module doit ici être compris comme synonyme d'unité de compilation. Un module Modula-2 compilé séparément en est un exemple typique. A chaque module chargé appartient un cadre de données pour ses données globales et un cadre de code pour le code de ses procédures. Une table à un emplacement fixe dans la mémoire principale, la table des cadres de données, contient les adresses des cadres de données des modules chargés. Une référence au cadre de code correspondant est entreposée dans le premier mot de son cadre de données. Tous les modules sont accessibles via la table des cadres de données. L'index de l'entrée dans cette table, le numéro de module, est utilisé pour l'adressage d'un module particulier dans le code. Les instructions M-code accédant aux données d'autres modules ou appellent des procédures dans d'autres modules utilisent généralement le numéro de module comme référence.
Lors de l'exécution d'une procédure déclarée dans un module donné, les adresses de base de la trame de données et de la trame de code correspondantes sont entreposées dans deux registres de base (appelés G et F). Le code et les données globales au sein du module de la procédure peuvent donc être adressés efficacement uniquement par des déplacement.
L'instruction suivante à exécuter est adressée par le registre PC, un déplacement d'octet par rapport au début de la trame de code (c'est-à-dire par rapport à F). Un appel à une procédure d'un autre module (ou un transfert de contrôle d'une coroutine à une autre) attribue implicitement de nouvelles valeurs aux registres F, G et PC.
Les instructions M-code pour les appels de procédure ne contiennent pas les déplacements des points d'entrée des procédures dans le bloc de code correspondant. Au lieu de cela, un index de la table d'entrées de procédure, appelé numéro de procédure, est spécifié dans l'instruction. La table d'entrées de procédure est allouée au début du bloc de code et contient les déplacements d'octets des points d'entrée de toutes les procédures du module correspondant.
Les données locales des procédures sont allouées dans une pile d'enregistrements d'activation de procédure. Chaque coroutine se voit allouer une zone de travail contiguë en mémoire, appelée bloc de pile. Le bloc de pile contient le descripteur de processus à son début. Le reste du bloc de pile contient l'espace de travail du processus. Quatre registres d'adresses pointent vers le bloc de pile du processus actuellement exécuté. Elles sont appelées P, L, S et H. P pointe vers le descripteur de processus au début du cadre de pile (P pour Process pointer), L pointe vers l'enregistrement d'activation en haut de la pile (L pour Local data), S pointe vers le premier emplacement libre de la pile (S pour Stack pointer) et H pointe vers l'extrémité supérieure de la pile (H pour High limit).
Les adresses des cadres de pile sont utilisées lorsque le contrôle est transféré d'une coroutine à une autre. Ce transfert de coroutine peut être soit explicitement programmé, soit implicitement invoqué par une erreur d'exécution détectée par l'interpréteur de M-code microcodé, soit être provoqué par une interruption. Huit gestionnaires d'interruption peuvent être définis, un pour chaque ligne d'interruption. Les adresses des coroutines correspondantes (processus pilotes) sont entreposées dans une table, le vecteur d'interruption, à un emplacement fixe de la mémoire principale et sont donc connues de l'interpréteur de code M. Un schéma de priorité et un schéma d'activation d'interruption contrôlent la manière dont les interruptions sont traitées par l'interpréteur de M-code.
L'architecture M-code comprend plusieurs instructions pour des opérations spécifiques à la machine. Ces instructions peuvent être classées en instructions d'entrées/sorties, instructions pour les opérations sur les bitmaps (écran) et instructions pour le déplacement de blocs dans la mémoire principale. Dans l'ordinateur Lilith, les interfaces sont contrôlées par plusieurs instructions M-code (et non par l'accès à des emplacements mémoire fixes). Quatre instructions M-code fonctionnent sur des bitmaps. L'efficacité souhaitée de ces opérations a obligé à les microcoder. Les données peuvent être déplacées dans toute la mémoire principale par une seule instruction M-code.
Trois autres concepts du M-code doivent être mentionnés : De nombreuses instructions utilisent des adresses absolues comme référence aux données accédées (variables). Il est donc impossible de déplacer des trames de données ou des trames de pile dans l'espace d'adressage après leur chargement ou leur création. Le M-code fournit uniquement une cartographie direct des adresses de mémoire virtuelle vers les adresses de mémoire physique. Le M-code ne fournit aucun mécanisme de protection absolue.
Les adresses fournies par le M-code sont des adresses de mots de 16 bits de large. L'espace d'adressage de 64 000 mots en résultant est cependant trop petit pour adresser la totalité de la mémoire physique contenant généralement 128 000 mots. La conséquence est que toutes les données référencées par des instructions de M-code arbitraires doivent résider dans les 64 000 mots généralement adressables de la mémoire principale (c'est-à-dire dans la partie contiguë de la mémoire principale commençant à l'adresse zéro). Seules les données référencées par quelques instructions spécialisées ou référencées de manière contrôlable peuvent être entreposées à un emplacement quelconque de la mémoire principale. En particulier, seules les polices de caractères, les bitmaps, les trames de code et les données non accessibles par des instructions de M-code arbitraires peuvent être entreposes à un emplacement quelconque de la mémoire principale.
Le langage de programmation Modula-2
Le langage de programmation Modula-2 est un descendant de ses ancêtres directs Pascal et Modula. Il est conçu pour satisfaire aux exigences de la conception de systèmes de haut niveau ainsi qu'à celles de la programmation de bas niveau de parties interagissant directement avec le matériel donné. En gros, Modula-2 comprend tous les aspects de Pascal à l'exception des fichiers. Le concept important de module, la syntaxe moderne, l'aspect multiprogrammation et les fonctionnalités de bas niveau ont été influencés par Modula. Dans le reste de ce page, les ajouts les plus importants, par rapport à Pascal, sont expliqués. Ceux-ci sont regroupés dans le concept de module, les modules séparés et la compilation séparée, les coroutines, les types de procédures et les fonctionnalités de bas niveau (dépendantes de l'implémentation).
Modules
Le module dans Modula-2 est une structure syntaxique prenant en charge la modularisation des programmes. Une déclaration de module est presque identique à la déclaration d'une procédure sans paramètre. Il existe trois différences essentielles entre les procédures et les modules :
Les règles contrôlant la visibilité (ou la validité) des objets, appelées règles de portée, sont différentes. Un objet visible à l'extérieur d'un module n'est visible à l'intérieur du module que s'il est explicitement importé, et un objet déclaré à l'intérieur de la portée d'un module peut être rendu visible à l'extérieur du module par une exportation explicite.
La durée de vie des objets déclarés à l'intérieur d'un module est égale à la durée de vie des objets dans la portée englobant le module (c'est-à-dire de la procédure englobant le module).
Le corps du module (partie instruction) est exécuté lorsque l'environnement du module est instancié. Le corps sert généralement à l'initialisation des objets déclarés à l'intérieur du module.
Un exemple de Medos-2, un petit planificateur de coroutine dans le programme Comint, devrait illustrer le concept de module :
- MODULE Scheduler;
-
- FROM SYSTEM IMPORT ADDRESS, PROCESS, NEWPROCESS, TRANSFER;
- EXPORT CreateProcess,Pass;
-
- CONST procs=3;
- VAR
- ptab:ARRAY[0..procs-1] OF PROCESS;
- cur,top:[0..procs];
-
- PROCEDURE CreateProcess(proc:PROC;addr:ADDRESS;size:CARDINAL);BEGIN
- IF top<procs THEN
- NEWPROCESS(proc, addr, size, ptab[top]); INC(top)
- ELSE
- HALT
- END
- END CreateProcess;
-
- PROCEDURE Pass;
- VAR
- old:CARDINAL;
- BEGIN
- old:=cur;
- cur:=(cur+1) MOD top;
- IF old <> cur THEN
- TRANSFER(ptab[old], ptab[cur])
- END
- END Pass;
-
- BEGIN
- cur:=0;
- top:=1
- END Scheduler.
Le module Scheduler possède six objets locaux, à savoir une constante procs, trois variables ptab, cur et top, et deux procédures CreateProcess et Pass. Il exporte les deux procédures et cache la constante et les variables. Quatre objets sont importés du module SYSTEM, à savoir ADDRESS, PROCESS, NEWPROCESS et TRANSFER. Le corps du module initialise les variables cur et top. L'exemple montre comment les objets peuvent être cachés et comment l'accès depuis l'extérieur est limité aux objets explicitement exportés, dans ce cas aux procédures CreateProcess et Pass. Cela permet de garantir des invariants sur les objets locaux du module indépendamment d'éventuelles erreurs dans l'environnement et permet ainsi de comprendre le module sans avoir à étudier au préalable son environnement complet (dans ce cas environ 830 lignes supplémentaires de texte Modula-2).
Modules séparés et compilation séparée
Un programme Modula-2 est constitué soit d'un seul module (et indépendant), soit de plusieurs modules séparés. L'environnement de chaque module séparé est considéré comme l'univers dans lequel les modules compilés séparément sont connus. Un programme est constitué dans ce cas d'un module principal ainsi que de tous les modules qu'il importe directement ou indirectement de l'univers. Deux concepts principaux soutiennent la décomposition des programmes en plusieurs modules au niveau global, à savoir le mode d'exportation qualifié et la division d'un module séparé en une partie spécifiant l'interface avec lui, son module de définition, et une partie spécifiant la réalisation, son module d'implémentation.
L'exportation qualifiée sert à éviter les conflits entre des identificateurs portant le même nom exportés depuis différents modules dans la même portée englobante. Cela pose particulièrement problème lorsqu'un module séparé est écrit car son auteur peut ne pas connaître tous les objets exportés. Si la procédure Pass est exportée en mode qualifié depuis le module Scheduler, la procédure doit alors être désignée comme Scheduler.Pass dans l'environnement du module Scheduler. Toutes les exportations depuis des modules séparés doivent être effectuées en mode qualifié. Par conséquent, un auteur de module doit simplement choisir un nom de module n'existant pas déjà dans son univers afin d'éviter les conflits de noms. (Cette règle facilite également la vie du compilateur Modula-2, car il n'a qu'à rechercher des modules complets dans l'univers.)
Un module séparé fournissant (exportant) des objets pour d'autres modules doit être divisé en un module de définition et un module d'implémentation. Le module de définition décrit l'interface avec le module séparé au moins syntaxiquement et peut donc être considéré comme un contrat du module avec ses importateurs (clients). Le module de définition contient toutes les déclarations nécessaires à une spécification complète de l'interface. Les procédures sont par exemple définies par leurs entêtes complets. L'exemple suivant montre le module Scheduler comme un module distinct.
Le module d'implémentation correspondant contient la réalisation du module séparé, c'est-à-dire des «choses» n'ayant pas besoin d'être connues des clients du module. En général, tous les objets déclarés dans le module de définition sont définis implicitement dans le module d'implémentation. Les procédures sont l'exception ; elles doivent être à nouveau déclarées dans le module d'implémentation, mais cette fois-ci, y compris leur corps.
- IMPLEMENTATION MODULE Scheduler;
-
- FROM SYSTEM IMPORT ADDRESS, PROCESS, NEWPROCESS, TRANSFER;
-
- CONST
- procs=3;
-
- VAR
- ptab:ARRAY[0..procs-1] OF PROCESS;
- cur,top:[0..procs];
-
- PROCEDURE CreateProcess(proc:PROC;addr:ADDRESS;size:CARDINAL);BEGIN
- IF top<procs THEN
- NEWPROCESS(proc, addr, size, ptab[top]); INC(top)
- ELSE
- HALT
- END
- END CreateProcess;
-
- PROCEDURE Pass;
- VAR
- old:CARDINAL;
- BEGIN
- old:=cur;
- cur:=(cur+1) MOD top;
- IF old<>cur THEN
- TRANSFER(ptab[old], ptab[cur])
- END
- END Pass;
-
- BEGIN
- cur := 0; top := 1
- END Scheduler.
La séparation d'un module séparé en une description d'interface et une partie d'implémentation est avantageuse et cruciale. Elle permet de modifier l'implémentation d'un module à chaque fois que nécessaire, voire d'avoir plusieurs implémentations du même module.
La compilation séparée de modules implique une vérification complète des types au-delà des limites des modules, en particulier entre les modules séparés du même programme. Le compilateur Modula-2 pour Lilith génère un fichier de symboles lors de la compilation d'un module de définition. Le fichier de symboles contient un codage symbolique du module de définition et est considéré comme représentant le module séparé dans les compilations ultérieures. Le compilateur lit le fichier de symboles lorsque l'implémentation du module séparé ou une autre unité de compilation important le module (un module de définition ou d'implémentation) est compilée.
Il est évident que le compilateur, éventuellement assisté par un fabriquant de liaison de programme (linker), doit vérifier que toutes les références à un module séparé sont basées sur la même description d'interface, c'est-à-dire sur le même fichier de symboles. Pour cette vérification, le compilateur génère un horodatage, appelé clef de module, lorsqu'un module de définition est compilé et l'inclut dans le fichier de symboles. Le nom du module ainsi que la clef du module identifient par la suite de manière unique un module (ou une certaine version d'un module).
Lors de la compilation d'un module d'implémentation, le compilateur écrit le code généré dans un fichier objet. Il copie également le nom du module et sa clef dans le fichier ainsi que les paires nom-clef de tous les modules référencés par le module. Cela permet à un linker ou à un linking-loader (tel que fourni par Medos-2) de vérifier par de simples tests de correspondance nom-clef si toutes les références à un certain module sont basées sur la même description d'interface (c'est-à-dire le fichier de symboles). Le format du fichier de symboles est donné dans la page format du fichier objet dans Format des fichiers de code objet.
Coroutines
Le Modula-2 ne fournit pas de processus «généraux» comme le font de nombreux autres langages de programmation temps réel ou systèmes (Ada, Concurrent Pascal, PORTAL,...). Il inclut à la place, grosso modo, les mécanismes nécessaires à la mise en ouvre de processus, à savoir un mécanisme simple pour gérer les coroutines et la possibilité d'encapsuler un ordonnanceur défini par l'utilisateur dans un module (bibliothèque) séparé.
Les exemples utilisés pour illustrer le concept de module montrent un ordonnanceur très simple défini par l'utilisateur. Il planifie les processus avec l'algorithme round-robin. Les coroutines sont référencées par des variables de type PROCESS. La procédure NEWPROCESS crée une nouvelle coroutine, à partir d'une procédure sans paramètre et d'un segment de mémoire. La procédure TRANSFER transfère le «contrôle» d'une coroutine à une autre.
Dans Modula-2, une interruption est considérée comme un transfert de contrôle à un moment imprévisible. Elle peut être considérée comme équivalente à une instruction TRANSFER(interrupted, driver) étant effectivement insérée dans le programme chaque fois qu'une demande d'interruption est acceptée. La variable driver désigne la coroutine (processus) gérant la demande, tandis que la variable interrupted se verra attribuer la coroutine suspendue.
Dans le M-code de l'architecture Lilith, chacun des huit signaux d'interruption est associé à ses propres variables interrupted et driver à des emplacements fixes (dans le vecteur d'interruption). Un schéma de priorité et un schéma d'activation d'interruption permettent de désactiver d'autres interruptions pendant qu'une interruption est traitée.
Types de procédures
Le concept de types de procédures, bien que rarement utilisé, s'est révélé à la fois puissant et important pour fournir l'ouverture souhaitée dans Medos-2. Normalement, les procédures sont simplement considérées comme des parties de programme ou des textes spécifiant des actions sur des variables. Une procédure peut cependant également être considérée comme un objet d'un certain type. De ce point de vue, une déclaration de procédure est un type spécial de déclaration de constante, la valeur de cette constante étant une procédure. Modula-2 permet de définir des types dont les valeurs sont des procédures, appelées types de procédures. Les variables et les paramètres de procédure des types de procédures peuvent être déclarés. La déclaration de type de procédure spécifie le nombre et les types de paramètres, et s'il s'agit d'une procédure de fonction, également le type du résultat. Ainsi, les procédures (déclarées globalement) peuvent être affectées à des variables (d'un type de procédure compatible) ou être transmises comme paramètre réel à une procédure.
Fonctionnalités de bas niveau
Le Modula-2 et son compilateur pour Lilith fournissent des fonctionnalités de bas niveau (c'est-à-dire dépendantes de l'implémentation et/ou de la machine) étant importantes pour l'implémentation de Medos-2. Les plus importantes d'entre elles seront énumérées, accompagnées d'une courte explication. La spécification de l'adresse absolue d'une variable dans sa déclaration permet d'accéder aux variables à des emplacements fixes dans la mémoire. Le code M définit certaines variables à des emplacements fixes (par exemple, le vecteur d'interruption et la table de trames de données). L'arithmétique sur les adresses est rendue possible grâce au type ADDRESS. Les variables de type ADDRESS sont compatibles avec tout type de pointeur et avec CARDINAL. Les fonctions SIZE, TSIZE et ADR prennent également en charge le calcul d'adresse.
Un paramètre de procédure formelle de type ARRAY OF WORD peut être remplacé par un paramètre réel de n'importe quel type. Dans la procédure appelée, le paramètre réel ne serait généralement pas inspecté, mais plutôt copié vers ou depuis un support de stockage secondaire, par exemple.
Les fonctions de transfert de type permettent de donner facilement une autre interprétation à la valeur d'une expression, c'est-à-dire que son modèle de bits est supposé être la valeur d'un autre type. Dans Medos-2, une fonction de transfert de type est utilisée, par exemple, pour attribuer la valeur d'une expression de type CARDINAL décrivant le code d'initialisation d'un programme à une variable de type PROC.
Enfin, les procédures de code représentent des instructions de M-code ne pouvant pas être générées par des instructions Modula-2 ordinaires. L'appel d'une telle procédure de code génère du code en ligne. Des exemples de telles instructions sont les instructions de contrôle de périphériques et les instructions fonctionnant sur des bitmaps d'affichage.