Projet système PC : 2021 - PILLEYRE Alexandre, BOIS Clement, GARRIGUES Enki

De Ensiwiki
Aller à : navigation, rechercher
Calmos.png
Titre du projet CalmOS - Le projet estival
Cadre Projet système

Équipe Enki GARRIGUES Clement BOIS Alexandre PILLEYRE
Encadrants Yves Denneulin , Gregory Mounie, Patrick Reignier

Présentation

Voici notre journal de bord et bilan à propos du projet "Système d'exploitation" réalisé du 7 au 25 juin 2021 par notre équipe, dans le cadre de la seconde année en alternance.

Équipe

Enki GARRIGUES

Clement BOIS

Alexandre PILLEYRE

Organisation

Nous avons codé en pair-programming tout au long du projet. Dans un premier temps, une personne (Clément) a réalisé la majorité du développement tandis que les deux autres étaient chargés des tests, de la lecture de la documentation et de la création du wiki.

Par la suite, nous avons parallélisé les tâches afin de progresser plus rapidement sur des sujets indépendants.

Phases du développement

Phase 1 : Prise en main de l'environnement

100 %

Durant cette première phase, nous avons tous trois pris en main l'environnement et le squelette fourni, et pour cette phase, avons tous codé la fonction console_putbytes afin de se familiariser avec le début du projet. Par la suite, nous avons retenu la version de Clément pour cette phase bien que nous avions tous une version fonctionnelle, obtenue en suivant les indications du premier TP de PSE.

Phase 2 : Interruptions, Changements de contexte et Créations de processus

100 %

Cette phase a commencé par la lecture de la documentation ainsi que par la recherche de ressources au sein de cette documentation. C'est probablement ce qui nous a demandé le plus de temps, puisqu'il a fallu recouper les différentes informations présentes dans tous les documents mis à notre disposition. La documentation totale du projet est dense et savoir trouver rapidement des informations n'est pas immédiat au début du projet. Grâce à ce temps pris en lecture, les phases suivantes ont été centrées sur la compréhension des instructions / concepts, et non leur recherche.

Pour ce qui est de la partie code, le fait d'avoir à notre disposition le code du context_switch nous a permis de gagner un peu de temps d'implémentation, mais nous avons quand même fait attention à bien comprendre les différents concepts avant de se lancer sans réfléchir.

Nous nous sommes également rendu compte durant cette phase que le travail demandé dans les différents TPs de PSE n'était pas directement ce qui était attendu dans les spécifications du projet et qu'il nous fallait utiliser ces ressources comme une aide et non pas comme un mode d'emploi pour le projet.

Les processus sont une structure contenant toutes les informations de base (pid, priorité, état, piles, registres...) et une union d'attributs. Cette dernière permet d'avoir un espace en mémoire de la taille du plus grand attribut et de pouvoir écrire différents attributs au même endroit (pas au même moment). En résumé, tous les attributs liés à des états (temps d'endormissement, prochain processus à exécuter, valeur de retour zombie, pid du fils à attendre, ...) sont dans cette union. Clément a eu cette idée d'implémentation, qui fut intéressante mais complexe à des moments (nous en parlerons dans les phases suivantes).

Ainsi, nous avons mis en place notre table de processus, notre fonction de changement de contexte, notre horloge, une première gestion des interruptions et enfin plusieurs fonctions d'ordonnancements (tourniquet puis appel sur interruption de l'horloge).

Les primitives implémentées à cette phase : getpid, getprio, start, clock_settings, current_clock.

Phase 3 : Ordonnancement, terminaisons, endormissement et filiation de processus

100 %

Déroulement

Cette phase a été réalisée, en partie, en parallèle avec la 2.

  • La file d'attente des processus a pris en compte les priorités en triant les processus selon leur priorité. Le chprio a été mis à jour pour gérer le "vieillissement" et l'ordre des processus.
  • L'endormissement des processus a été géré avec wait_clock et waitpid.
  • La terminaison des processus est réalisée par kill, exit et une fonction interne stop. Les processus qui terminent "naturellement" (return ou fin de fonction) sont gérés par la fonction de terminaison assembleur que nous avons créée et ajoutée à la pile de chaque processus.
  • Lors de sa création, un processus obtient un parent. A la mort de ce dernier, le fils devient orphelin avec NOPID(-1) comme parent.

Nous nous sommes servis des TPs indiqués dans la Phase 3 de la roadmap comme guide pour avancer.

Difficultés

Nous nous sommes lancés rapidement dans le code et nous ne pensions pas forcément à tous les petits cas d'erreur. Pour pallier ce manque, nous avons fait passer les tests user (que nous avons déplacé dans kernel). Grâce à eux, nous avons pu trouver plusieurs cas précis d'erreur. Notre conseil est donc d'essayer de faire passer les tests le plus tôt possible, pour éviter d'accumuler les erreurs "invisibles".

Nous nous sommes aidés des TPs suivants : le TP portant sur l'ordonnancement / le TP sur le traitement la terminaison / le TP sur l'endormissement des processus.

Les primitives implémentées à cette phase : chprio, exit, kill, wait_clock, waitpid.

Phase 4 : Gestion des files de messages et de synchronisation

100 %

Wait_clock

Tout d'abord, nous avons implémenté le wait_clock qui n'a pas posé de problème important. Les tests sont rapidement passés.

Files de messages (et difficultés)

L'implémentation des files, et les contraintes de synchronisation qui vont avec, a été plus complexe qu'initialement prévue.

La création des structures et des fonctions a été assez simple au départ. Nous avions des files de messages et des processus qui attendent sur file vide ou sur file pleine.

Nous avons ensuite essayé de lancer les premiers tests et c'est là que les problèmes sont apparus. L'ordre des messages n'était pas le bon. Cela était dû à une incompréhension dans la documentation, et notamment dans les spécifications de psend et preceive :

  • Si la file est pleine et qu'un message est récupéré, cette file doit être immédiatement remplie avec le message du premier processus dans la file d'attente (sur file pleine)
  • Si la file est vide et qu'un message est déposé, ce dernier est directement remis au premier processus en attente sur file vide

Suite à cela, nous avons ajouté un attribut message dans l'union d'un processus pour le connaitre ou le modifier sans qu'un processus soit actif.

Ce processus en attente devient ensuite activable (ou actif), on sait que son attribut message a été modifié.


Nous vous conseillons de bien lire les spécifications et de bien réfléchir au fonctionnement avant de vous lancer dans le code (les dessins, c'est toujours utile). Les tests vous feront vite comprendre si c'est ce qui est attendu et pourront vous aider à résoudre les éventuels problèmes.

Phase 5 : Séparation des espaces mémoire noyau et utilisateur : gestion de processus utilisateur

100 %

Cette phase est probablement la plus difficile. Elle a nécessité plusieurs jours d'étude documentaire afin de prendre en main correctement les concepts nécessaires au bon fonctionnement tels que protection ring, segments et Task State Segment.

Notre recherche d'informations a été grandement aidé par OSDev.org et la documentation détaillée dans les résultats de 2019 - BALLEYDIER Loic, PIERRET Arnaud et 2019 - EXERTIER Bastien, FRITZ Anthony, ....

Chaque mot a son importance !


1ère étape : Pile user

Pour commencer, nous avons créé une pile user pour les processus lancés côté user (qui ont toujours une pile kernel comme n'importe quel processus).

Cette page wiki nous a beaucoup aidé pour comprendre tous les éléments à empiler.

Quelques précisions supplémentaires à toute leur explication :

  • N'oubliez pas que la pile user doit être allouée avec user_stack_alloc et que on empile du HAUT de la pile user vers le BAS.
  • Le sommet de la pile user est donc la dernière valeur empilée (et non le haut de la pile user).
  • La fonction de retour est particulière pour les processus user, nous en parlerons plus bas


2ème étape : Appels systèmes

Pour chaque appel système (liste des appels + console_putbytes pour le printf) :

  • Côté user :
    • Nous appelons une fonction assembleur avec les bons paramètres
    • Dans cette fonction assembleur,
      • Nous sauvegardons tous les registres (sauf eax qui sera la valeur de retour, donc inutile)
      • Nous remplissons les registres avec les paramètres passés à notre fonction assembleur (4(%esp), 8(%esp), ...)
        • Attention --> les registres ont été sauvegardés sur la pile...
      • Nous lançons l'interruption 49
      • Nous restaurons tous les registres
  • Côté kernel :
    • Nous récupérons l'interruption
      • Sauvegarde des registres
      • Passage en mode kernel (modification des registres ds, es, fs, gs)
      • Appel de la fonction C gérant l'interruption
      • Passage en mode user (modification des registres ds, es, fs, gs)
      • Restauration des registres
      • iret
    • Fonction C gérant l'interruption :
      • Vérification que les paramètres sont bien dans la partie user (pour ne pas modifier la partie kernel avec des pointeurs par exemple)
      • Appel de la fonction voulue

A propos de la gestion de l'interruption 49 côté kernel, elle se gère comme toute interruption, mais avec une subtilité.

Vous devriez avoir un bluescreen de trap OD. Petit coup de pouce : ce cours.


3ème étape : Fonction de retour user

Cette fonction est sensiblement la même que la fonction de retour des processus kernel, à un détail près.

En effet, cette fonction doit être côté user pour être autorisée à être lancée mais étant donné que la définition de la pile est côté kernel, ce dernier doit connaître où cette fonction se trouve.

Pour faire cela, 2 possibilités (non, un simple include ne fonctionne pas, kernel ne connait pas user et shared est dupliqué dans kernel et dans user) :

  • Forcer l'adresse de la fonction (en la plaçant à une adresse connue)
  • Ecrire la fonction puis chercher son adresse (nm -n user/user.debug --> pour avoir la pile des adresses)


Autre information importante

Vous pourrez peut-être remarquer que quand le système essaie de changer de contexte après un wait, un exit, ... il est perdu. Cela s'explique avec la TSS.

Voici 2 liens qui pourraient vous aider : Aspects techniques et ce groupe.


Ce fut une partie éprouvante, les erreurs rencontrées ont été dures à comprendre et à résoudre. Il est important de faire les étapes petit à petit en vérifiant qu'elles fonctionnent au fur et à mesure (l'ajout de certaines peut détruire des anciennes...).

Phase 6 : Développement d'un pilote de console et gestion du clavier

100 %

Dans un premier temps, nous avons implémenté un traitant d'interruption pour l'interruption 33 (IRQ1) comme détaillé dans les spécifications. Ce traitant sauvegarde les registres nécessaires, passe en mode kernel, acquitte l'interruption, appelle do_scancode (nous y reviendrons), repasse en mode user et restaure les registres.

Nous avons ensuite implémenté keyboard_data, appelée par do_scancode qui s'occupe d'afficher dans la console l'écho du clavier (si l'écho est activé) et d'envoyer dans le buffer du clavier les différents caractères. Pour la partie écho, nous avons simplement suivi la spécification du projet. Pour la partie d'envoi dans le buffer, nous avons choisi d'utiliser nos files de messages, précédemment implémentées dans la phase 4 en guise de buffer et nous envoyons donc à chaque interruption les caractères dans cette file.

Par la suite nous avons implémenté les différentes primitives système :

  • cons_echo : en fonction du paramètre donné, désactive ou active l'écho du clavier sur la console.
  • cons_write : écrit sur le terminal une chaîne de caractères, à la manière de console_putbytes.
  • cons_read : lit simplement un caractère dans le buffer du clavier et le retourne. Notre implémentation de cette fonction suit le comportement du cons_read présent dans les tests.
  • cons_readline : correspond à la primitive cons_read présente dans la documentation, c'est à dire celle qui attend que l'utilisateur ait tapé une ligne terminée par le caractère 13 (ou \r). Attention ici à bien lire la documentation, notamment lorsque la ligne que vous lisez est de taille supérieure à la taille donnée en argument de cons_readline.

Phase 7 : Implémentation d'un interprète de commandes

100 %

Pour cette phase, nous avons créé un shell (processus utilisateur qui cons_read en permanence et interprète les commandes).

Les différentes commandes disponibles :

  • help : Liste des commandes
  • ps : Affiche les processus en cours
  • clear : Efface le terminal
  • uptime : Affiche le temps actuel
  • test : Fait passer le test indiqué ou tous si 'auto' indiqué
  • sys_info : Affiche les informations du système
  • reboot : Redémarre le système
  • logo : Réinitialise l'affichage
  • beep : Emet un beep sonore
  • exit : Ferme le shell (kernel toujours actif)
  • echo <on, off> : Active / Désactive l'écho du clavier sur l'écran
  • cd <folder> : Change le répertoire courant
  • ls : Affiche le contenu du répertoire courant
  • cat <file> : Affiche le contenu d'un fichier
  • play <file> : Joue le fichier .mbp

Tests

Kernel:
100 %
User:
100 %

Extensions

Beep

Pour cette partie nous avons activé le buzzer du système pour effectuer des sons, ou beeps.

Deux fonctions beep différentes ont été créées :

  • Le premier est un beep classique, il s'agit d'un appel système, lançable par l'utilisateur. L'appel attend en paramètres une fréquence et une durée.
  • Le second beep est un beep asynchrone, pour les interruptions. Ce beep est lancé à une certaine fréquence et se termine après un certain nombre d'interruptions d'horloge. Il permet de jouer un son lorsque l'utilisateur fait un Ctrl+G (ou toutes autres applications relatives, par exemple, à la gestion du clavier, qui se fait via des interruptions). La création de cet appel était nécessaire puisque lorsque nous recevons la commande Ctrl+G nous étions en plein milieu d'une interruption et ne pouvions pas utilisé l'appel système de beep classique qui attendait un certain temps avec l'appel système wait_clock, appelant notre ordonnanceur.

Piano

Dans la continuité du beep, nous avons ajouté une fonctionnalité permettant de passer le clavier en mode piano.

En effet, la fréquence du beep est programmable, pour ainsi jouer les différentes notes de la gamme.

L'appui sur Ctrl+P (^P) transforme le clavier en piano (avec l'écho désactivé), permettant de jouer avec les touches sur la ligne 2 (de Q à K) et la ligne 1 pour les altérations (Z, E, T, Y, U).

Pour désactiver ce mode, il suffit d'appuyer sur ^P.

Jukebox

Pour finir avec la partie musicale, nous avons choisi de faire un parseur pour lire les fichiers et jouer les notes écrites.

Les fichiers sont des .mbp, format créé pour ce système avec un template personnalisé (décrit ci-dessous).

Le parseur commence par lire le tempo de la musique, puis lit chaque ligne du fichier et joue la note ou attend le silence souhaité.

Le template est le suivant :

  • 1 ligne pour le tempo : T 133
  • 1 ligne par
    • note : CD2 NO --> do 2 (C) dièse qui dure une noire
    • silence : P SO --> un soupir

Exemple de fichier : Fichier:Imperial march.pdf

Floppy Disk 1.44MB

L'objectif était de fournir des données au système sans nécessairement recompiler. Pour cela, nous avons mis en place gestion du Floppy Disk Controller pour accéder à une disquette virtuelle définie dans QEMU. Notre implémentation se restreint par simplicité aux disquettes 1.44MB et n'a pas pour objectif d'être performante. Elle est inspirée du tutoriel sur le sujet présent sur OSDev.org.

La gestion des disques utilise des fonctions virtuelles permettant d'ajouter d'autres supports (par exemple, disque dur) sans modifier la gestion du système de fichier.

Système de fichier FAT16

Afin de stocker des données variées sur notre support de stockage (disquette), nous avons mis en place un système de fichier. Le format FAT16 a été choisi pour son apparente simplicité et la bonne compatibilité avec les disquettes 1.44MB.

Notre implémentation est limitée à la lecture incluant les sous-dossiers et les noms de fichiers longs. La modification du contenu est supportée sans redimensionnement du fichier. De même la modification du système de fichier (ajout, suppression de fichiers et dossiers) n'a pas été implémenté faute de temps.

Souris & curseur

Nous avons commencé comme toujours par nous documenter sur cette partie notamment ici et ici. Nous nous sommes basé pour l'initialisation de la souris tout d'abord sur le code de SANiK donné dans un thread du site OSDev puis nous nous sommes orienté vers le code du SnowflakesOS disponible sur GitHub. Le plus compliqué pour cette partie, ou du moins ce qui a demandé le plus de temps a été de comprendre qu'il était important de démasquer l'IRQ2 pour avoir accès à l'IRQ12 (qui est l'interruption de la souris PS/2). Sitons OSDev : "Masking IRQ2 will cause the Slave PIC to stop raising IRQs.". Il est donc important, comme l'IRQ12 est une interruption du slave PIC, de bien démasquer cette interruption. Notons que dans l'état actuel du code, une souris avec 5 boutons et la molette de défilement sont pris en charge mais les fonctionnalités ne sont pas exploités.

Pour ce qui est du curseur, il est géré par la fonction callback de la souris et ne fait qu'appeler une fonction de la console qui change la couleur de fond d'une cellule de l'écran par une autre (ici change le noir par du vert) aux coordonnées données par la position de la souris sur le grille de 80 par 25.

Paint minimaliste & reboot-on-click

Après avoir ajouté la souris et la gestion de son curseur, et comme nous avions une fonction de la console permettant comme dit ci-dessus de changer la couleur de fond d'une cellule à une position donnée, nous avons décidé de pouvoir dessiner sur l'écran lors d'un clic de souris. Ainsi, en fonction du bouton cliqué (gauche, centre, droite), la couleur de fond des cellules survolées changera selon trois couleurs (respectivement bleu, vert, rouge). À cela s'ajoute une petite croix rouge en haut à droite de l'écran, permettant de reboot le système lors d'un clic, à n'importe quel moment.

Calmos2021.jpg

Journal de bord

Semaine 1

Lundi 7 juin

Début et fin de la Phase 1 :

  • Prise en main de l'environnement de travail
  • Prise en main du squelette du code
  • Découverte et première lecture de la documentation
  • Code de la fonction console_putbytes

Début de la Phase 2 :

  • Lecture de la documentation de cette partie
  • Recherche de ressources (par exemple vidéos)
  • Réalisation du code pour l'horloge
    • Code pour les interruptions
    • Code pour l'affichage de l'horloge
    • Code pour le calcul du temps écoulé

Mardi 8 juin

Poursuite de la Phase 2 :

  • Réalisation du code pour les processus
    • Code de changement de contexte
    • Première forme d'ordonnancement (ping pong)
    • Début de table des processus

Mercredi 9 juin

Fin de la Phase 2 :

  • Débuggage du travail effectué le Mardi
  • Fin du code de la Phase 2

Début de la Phase 3 :

  • Lecture de la documentation de cette partie
  • Recherche de ressources
  • Réalisation d'une première ébauche de code
    • Début du code d'ordonnancement
    • Début de la structure process_t

Jeudi 10 juin

Phase 3 :

  • Gestion des priorités

Début Phase 4 :

  • Lecture de la documentation
  • Recherche de ressources
  • Réalisation d'une fonction
    • Code de la fonction wait_clock

Note : Nous nous sommes rendu compte qu'il était vraiment important de finir la Phase 3 avant de commencer la Phase 4, puisque nous avons été contraints de revenir en arrière complétement afin de fixer le code.

Vendredi 11 juin

Phase 3 et 4 :

  • Rollback
  • Réécriture au propre

Semaine 2

Lundi 14 juin

Phase 4 :

  • Début de la programmation des files de messages

Mardi 15 juin

Phase 4 :

  • Continuité de la programmation des files de messages
  • Lancement des différents tests relatifs aux files de messages et fix en conséquence.

Début de la Phase 5 :

  • Lecture de la documentation, spécifications et aspects techniques
  • Recherche de ressources notamment sur le site osdev.org

Mercredi 16 juin

Fin de la Phase 4 :

  • Finalisation des files de messages
  • Validation des derniers tests kernel

Phase 5 :

  • Passage en Ring3

Jeudi 17 juin

Phase 5 :

  • Implémentation des premiers appels système

Vendredi 18 juin

Fin de la Phase 5 :

  • Finalisation des appels système
  • Corrections des valeurs sauvegardées en TSS

Début de la Phase 6 :

  • Gestion des interruptions clavier

Semaine 3

Lundi 21 juin

Fin de la Phase 6 :

  • Transformation du cons_read
  • Correction du cons_write

Debut et Fin Phase 7 :

  • Implémentation du shell et des commandes requises

Extension Music :

  • Ajout d'un beep lors d'une mauvaise frappe au clavier
  • Ajout d'une fonction pour appeler beep

Mardi 22 juin

Extension Floopy :

  • Début d'implémentation

Mercredi 23 juin

Extension Music :

  • Marche impériale jouée avec la nouvelle fonction beep

Extension Mouse

  • Code d'initialisation de la souris PS/2

Extension Mouse

  • Lecture/Ecriture fonctionnel

Extension FileSystem

  • Début d'implémentation du FAT16

Jeudi 24 juin

Extension Music :

  • Lecture de fichier musical personnalisé
  • Joue la musique écrite avec le template personnalisé

Extension Mouse :

  • Récupération de la position et des actions de la souris via IRQ12
  • Démasquage de l'IRQ2 pour faire fonctionner l'IRQ12
  • Ajout d'une croix rouge en haut à droite de l'écran pour reboot le système en cliquant dessus
  • Ajout de l'affichage du curseur
  • Ajout de la possibilité de "colorier" le fond de l'écran en Rouge vert ou bleu (clic droit, central, bleu respectivement)

Extension FileSystem

  • Lecture des dossiers incluant les LFN (nom de fichier long)
  • Lecture de fichiers courts

Vendredi 25 juin

Extension FileSystem

  • Gestion des clusters utilisés par les fichiers longs

Difficultés