Projet système PC : 2021 - FLORES Morgane, FOURNIER Emma

De Ensiwiki
Aller à : navigation, rechercher
Fouremma floresmo logo.png
Titre du projet DominOS
Cadre Projet Système

Équipe Morgane FLORES Emma FOURNIER
Encadrants Yves Denneulin , Gregory Mounie, Patrick Reignier

Présentation

Voici la description approfondie du projet du module "Système d'exploitation" réalisé par Morgane FLORES et Emma FOURNIER du 07 au 25 juin 2021 à temps plein dans le cadre de notre formation en alternance.

Vous y retrouverez tout ce que nous avons mis en œuvre pour ce projet, à savoir comment nous nous sommes organisées, notre interprétation de chaque phase et notre journal de bord détaillé pour suivre notre avancement.

Équipe

Morgane FLORES

Emma FOURNIER

Organisation

Nous avons réalisé l'ensemble des phases en pair-programming. Nous avons utilisé l'extension Live Share de Visual Studio Code pour coder en parallèle.

Nous avons rempli à tour de rôle notre page Ensiwiki.

Les commits sur GitLab ont été réalisés depuis un même compte pour plus de simplicité mais nous avons bien réalisé chacun des commits ensemble.

Phases du développement

Phase 1 : Prise en main de l'environnement

100 %

Au cours de cette première phase, nous avons pris en main le squelette de code fourni et réalisé un premier parcours de différentes pages Ensiwiki à notre disposition (page principale, roadmap, spécifications, ...). Nous avons aussi réalisé le code nécessaire pour faire fonctionner la fonction printf côté noyau (implémentation de la fonction console_putbytes). Cette phase n'a pas représentée de difficulté particulière. Nous avons suivi les indications du premier TP de PSE.

Phase 2 : Création et lancement de processus de niveau noyau

100 %

Au cours de cette deuxième phase, nous avons pu implémenter différents concepts clés pour la suite du projet. Après avoir lu les spécifications et les pages Ensiwiki relatives à cette phase, nous avons pu mettre en place notre table des processus (table de structures d'informations sur les processus), notre fonction de changement de contexte et une première fonction d'ordonnancement (implémentant l'algorithme du tourniquet). Nous avons réalisé plusieurs tests d'aller-retours entre processus.

Nous avons aussi mis en place notre horloge (timer) et une première gestion des interruptions (masque/démasquage des canaux d'interruption, enregistrement d'un traitant d'interruption dans la table des vecteurs d'interruption). Nous avons ensuite pu faire en sorte que notre fonction d'ordonnancement soit appelée suite à une interruption de l'horloge.

Cette phase a nécessité un peu de temps pour bien comprendre les différents concepts et construire une base solide pour la suite du projet. Nous avons suivi les indications de plusieurs TP de PSE, notamment un TP sur les processus et un TP sur le timer et les interruptions. Nous avons utilisé les fonctions assembleur de changement de contexte et le traitant de l'interruption de l'horloge fournies dans ces TP.

Voici les primitives systèmes implémentées à l'issue de cette phase : cons_write, getpid, getprio, start, clock_settings, current_clock.

Phase 3 : Ordonnancement, création dynamique et terminaison de processus de niveau noyau

100 %

Déroulé

Au cours de cette troisième phase, nous avons poursuivi les implémentations de la phase précédente :

  • Nous avons implémenté un système d'ordonnancement qui utilise une file d'attente à priorité (queue.h) ordonnant les processus selon leur priorité (et assurant un ordre FIFO pour les processus de même priorité). Nous avons aussi permis la modification de la priorité d'un processus en implémentant la primitive système chprio.
  • Nous avons géré l'endormissement des processus (primitive système clock_wait). Il a fallu ajouter un nouvel état de processus ASLEEP et ajouter un attribut dans notre structure décrivant un processus pour sauvegarder l'heure de réveil de ce dernier (l'heure de réveil est tout simplement le paramètre de clock_wait : elle est exprimée en nombre d'interruptions depuis le démarrage du système).
  • Nous avons géré la terminaison des processus (primitives systèmes exit et kill). Nous avons aussi fait en sorte que l'instruction return un_entier provoque l'invocation de la primitive système exit : pour ce faire, nous avons dû coder, en assembleur, une fonction empilant la valeur de retour et appelant exit. C'est l'adresse de cette fonction qui doit être empilée au niveau de la pile de chaque processus à sa création (merci Nathan Vignal et Valentin d'Emmanuelle pour votre aide !). Pour la suite de ce wiki, cette fonction de terminaison s'appelle exit_handler.
  • Nous avons aussi mis en place tout le système de filiation des processus. Nous avons implémenté l'arbre de filiation (avec pointeur vers le processus père et liste de processus fils dans la structure représentant un processus). Cela a impliqué un certain nombre de changements dans nos fonctions pour prendre en compte cette filiation et les spécifications associées :
    • Nous avons notamment modifié les primitives systèmes start, kill et exit (définition du processus père, ajout au processus fils, notifier la mort du processus père à ses processus fils, ...).
    • Nous avons mis en place la primitive système waitpid et dû gérer des nouveaux états de processus (bloqué en attente d'un fils, zombie, ...).

Voici les primitives systèmes implémentées à l'issue de cette phase : chprio, waitpid, kill, exit.

Conseils

Cette phase nous a demandé beaucoup de réflexion et une lecture approfondie des spécifications décrites sur Ensiwiki. Prenez bien le temps de lire toutes les spécifications demandées pour les implémenter correctement ! Après avoir fini l'implémentation, nous avons pu commencer à tester nos primitives avec les tests fournis dans la partie user que nous avons déplacé dans la partie kernel (avec quelques modifications).

Ces premiers tests ont entraîné une phase de correction de notre code pour correctement implémenter des spécifications que nous avions mal comprises ou pas bien expliquées : nous conseillons de lancer les tests au plus tôt ! Les tests permettent de comprendre quel fonctionnement est attendu.

Nous avons suivi les indications des TP de PSE pour nous guider mais cependant, il faut bien prendre en compte que les instructions des TP ne correspondent pas forcément aux spécifications exigées pour les primitives systèmes. Nous conseillons de réaliser les primitives systèmes au fur et à mesure pour ne pas se retrouver dans une situation où il faut refactor les fonctions codées en suivant les TP pour coller aux primitives systèmes attendues. Les TP doivent être vu comme des indications plus que comme des consignes.

Ressources

Les TP qui nous ont aidées sont : le TP portant sur l'ordonnancement, le TP sur l'endormissement des processus et le TP sur le traitement la terminaison.

Pour la filiation, nous avons utilisé les spécifications sur le cycle de vie des processus.

Pour les primitives systèmes, nous nous sommes basées sur la page expliquant les spécifications de toutes les primitives. Attention : certaines signatures sont différentes dans le fichier de test fourni.

Phase 4 : Gestion des communications et synchronisation de processus de niveau noyau

100 %

Déroulé

Au cours de cette quatrième phase,

  • Nous avons implémenté une structure représentant une file de message et ensuite un tableau de files de messages
  • Nous avons ajouté deux états possibles pour un processus : bloqué sur file vide et bloqué sur file pleine
  • Nous avons ajouté deux attributs dans notre structure représentant un processus : un attribut message représentant le message lu ou à écrire et un attribut représentant l'éventuelle file sur laquelle le processus est en attente sur file vide ou pleine
  • Nous avons implémenté toutes les primitives systèmes associées aux files de messages

Nous avions mal compris les spécifications des primitives preceive et psend (notamment le terme immédiatement), ce qui nous a obligé à modifier notre code. En effet,

  • pour psend : si un processus est en attente sur file vide, le processus courant lui remet directement le message (modifie son attribut message)
  • pour preceive : après avoir lu, si un processus est en attente sur file pleine, le processus courant écrit la valeur du processus en attente dans la file (à partir de l'attribut message)

Dans les deux cas, le processus qui était en attente redevient activable (et éventuellement actif s'il était plus prioritaire que le processus courant). Après s'être débloqué, on sait que le message a été nécessairement lu ou déposé.

Voici les primitives systèmes implémentées à l'issue de cette phase : pcount, pcreate, pdelete, preset, psend, preceive.

Conseils

Pour cette phase, nous conseillons de :

  • lire et relire les spécifications (nous ne les avions pas bien implémentées et cela nous a obligé à réécrire une partie de notre code).
  • faire passer les tests fournis côté kernel : à nouveau cela aide à comprendre les spécifications.

Ressources

Pour cette phase, nous avons uniquement utilisé les spécifications du projet sur Ensiwiki.

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

100 %

Déroulé

Au cours de cette cinquième phase,

  • 1) Nous avons mis en place la gestion de deux piles pour un processus utilisateur : une pile kernel/superviseur et une pile user/utilisateur (qui ressemble beaucoup à l'ancienne pile simple du processus que nous avons utilisé entre les phases 1 et 4). A la création d'un processus utilisateur (primitive start) : il faut empiler un certain nombre de paramètres au niveau de la pile kernel pour simuler un retour d'interruption (quand le processus est créé : il est en mode superviseur et quand le processus sera élu pour la première fois : il passera du mode superviseur au mode utilisateur grâce à ce retour d'interruption simulé). Ce wiki explique très bien quels sont les paramètres à empiler dans chacune des piles et dans quel ordre. Pour résumer :
    • pour la pile utilisateur (à allouer avec user_stack_alloc) :
      • empiler l'argument de la fonction exécutée par le processus
      • puis empiler la fonction de terminaison (avec quelques modifications par rapport à l'ancienne fonction de terminaison exit_handler : voir en dessous)
    • pour la pile kernel (à allouer avec mem_alloc) :
      • empiler le Stack Segment SS pour le mode utilisateur (constante USER_DS)
      • puis empiler le sommet de la pile utilisateur
      • puis empiler le registre eflags pour le mode utilisateur (0x202)
      • puis empiler le Code Segment CS pour le mode utilisateur (constante USER_CS)
      • puis empiler l'adresse de la fonction exécutée par le processus
      • puis empiler l'adresse d'une fonction assembleur permettant à votre processus de passer pour la première fois en mode utilisateur par retour d'interruption (faisant iret).

Tous ces éléments empilés sur la pile kernel permettront de simuler un retour d'interruption la première fois que le processus créé est élu : ensuite, ce sont les instructions int et iret qui empileront / dépileront ces paramètres au niveau de la pile kernel.

  • 2) Côté utilisateur, nous avons mis en place une bibliothèque des appels systèmes. Pour chaque appel système, on développe une fonction qui :
    • sauvegarde les registres eax, ebx, ecx, edx, esi et edi en pile utilisateur (push)
    • passe les paramètres de l'appel dans les registres (eax contient le numéro de l'appel système)
    • exécute l'instruction int $49 (interruption)
    • restaure les registres (sauf eax qui contient le résultat de l'appel)
  • 3) Côté kernel, nous avons implémenté le traitant de l'interruption 49 qui réalise les actions suivantes :
    • il sauvegarde les registres eax, ebx, ecx, edx, esi et edi en pile kernel (push)
    • il change les valeurs des registres %ds, %es, %fs, %gs pour le mode superviseur (il leur donne la valeur KERNEL_DS)
    • il appelle une fonction C qui acquitte le bon traitement de l'interruption (outb(0x20, 0x20)) et qui traite l'appel système (eax contient le numéro unique de l'appel et les autres registres contiennent les paramètres de l'appel : comme on vient de les empiler, on peut les récupérer en tant que paramètres de la fonction). En fonction du numéro de l'appel système, cette fonction appelle la bonne primitive système.
    • il (re)change les valeurs des registres %ds, %es, %fs, %gs pour le mode utilisateur (il leur donne la valeur USER_DS)
    • il restaure les registres
    • exécute l'instruction iret
  • 4) Finalement, dans la fonction kernel_start :
    • On crée le processus kernel idle.
    • On crée un premier processus utilisateur qui exécute la fonction user_start. Côté utilisateur, vous pouvez implémenter user_start et faire vos appels systèmes ici.
  • 5) Autres éléments à réaliser (en vrac) :
    • A chaque changement de contexte (passant d'un processus à un autre), il faut placer au niveau de la TSS l'adresse la plus haute de la pile kernel du processus élu ainsi que la valeur KERNEL_DS (les adresses où écrire ces données sont citées ici).
    • Il faut enregistrer le traitant de l'interruption 49 dans l'IDT (table des vecteurs d'interruption) : attention, cette fois le DPL vaut 3 et non 0 comme expliqué ici.
    • Il faut créer une nouvelle fonction de terminaison côté utilisateur (qui réalise l'appel système exit). L'adresse de cette fonction doit être empilée au niveau de la pile utilisateur à la création du processus. L'adresse de cette fonction doit donc être connue côté Kernel ! Pour ce faire, il faut reproduire ce qui a été fait pour la fonction user_start (voir les fichiers kernel/kernel.lds et user/crt0.S).
    • Il peut être nécessaire de définir les valeurs des registres %ds, %es, %fs, %gs pour le mode superviseur dans les traitants des interruptions 32 et 33 (si vous faites la phase 6). En testant sur une VM, les tests ne passaient pas sans !

Conseils

Pour cette phase, nous conseillons de :

  • aller pas à pas :
    • implémenter tout d'abord la simulation du retour d'interruption pour réussir à passer en mode utilisateur à la création d'un processus : pour tester le bon passage en mode utilisateur, vous pouvez utiliser gdb (et afficher le pointeur de pile esp par exemple ou faire un assert(0) dans user_start et voir les informations sur le blue screen : oui oui, à la guerre comme à la guerre).
    • puis passer à la bibliothèque d'appels systèmes.
  • bien tester la gestion de la fonction de terminaison côté utilisateur : elle réserve quelques surprises ...
  • faire passer les tests fournis côté utilisateur : à nouveau cela aide à comprendre les spécifications et à trouver les derniers bugs !
  • autres conseils en vrac :
    • écrire le minimum en assembleur et utiliser des fonctions C
    • quand on définit le traitant de l'interruption 49 dans l'IDT : DPL == 3
    • la premier processus utilisateur lance user_start

Ressources

Pour cette phase, nous avons réalisé un gros travail de documentation pour bien comprendre ce qu'il fallait faire. Tout d'abord, nous avons utilisé les indications de ce groupe, qui sont à la fois claires et précises. Pour ce qui est de la gestion de la fonction de terminaison côté utilisateur, nous avons utilisé les informations de ce groupe pour comprendre l'idée générale (qu'il faut "forcer" l'adresse de la fonction de terminaison côté utilisateur) ainsi que ces informations pour avoir plus de détails (notamment la valeur de l'adresse).

Nous avons aussi trouvé (après avoir finalisé la phase 5) ce diaporama sur Ensiwiki où les différents éléments à réaliser sont bien détaillés.

Plus généralement :

Phase 6 : Gestion du clavier et implémentation d'un pilote de console

100 %

Déroulé

Au cours de cette sixième phase,

  • Nous avons implémenté une structure représentant le tampon du clavier (tampon circulaire)
  • Nous avons implémenté un traitant pour l'interruption 33 (les entrées clavier). Ce traitant :
    • sauvegarde les registres eax, ebx, ecx, edx, esi et edi en pile kernel (push)
    • change les valeurs des registres %ds, %es, %fs, %gs pour le mode superviseur (il leur donne la valeur KERNEL_DS)
    • acquitte l'interruption (comme pour la gestion de l'horloge);
    • appelle notamment la fonction do_scancode (comme expliquée dans les spécifications)
    • (re)change les valeurs des registres %ds, %es, %fs, %gs pour le mode utilisateur (il leur donne la valeur USER_DS)
    • restaure les registres

Il faut initialiser ce traitant dans l'IDT !

  • Nous avons implémenté la fonction keyboard_data (qui est appelée par do_scancode : comme expliqué dans les spécifications)
    • Il faut implémenter cette fonction de façon à ce qu'elle recopie les nouveaux caractères disponibles dans le tampon associé au clavier. Lorsque le tampon du clavier est plein, ces caractères sont ignorés.
    • Au cours de la saisie au clavier, il est possible d'éditer le contenu du tampon associé au clavier : la frappe de la touche backspace, signalée par le caractère de code ASCII 127, provoque la suppression du tampon du dernier caractère entré sauf si la ligne courante est vide. Pour la suppression d'un caractère, il faut faire : ASCII 8 puis ASCII 32 puis ASCII 8 à la suite (comme indiqué dans la page Ensiwiki des spécifications).
  • Nous avons implémenté les primitives systèmes cons_echo et cons_read. Pour ce faire,
    • Nous avons implémenté la primitive cons_read telle que décrite dans les spécifications.
    • Nous avons ajouté un état bloqué E/S pour nos processus :
      • un processus qui exécute la primitive cons_read doit se bloquer jusqu'à ce que des caractères soient effectivement disponibles (c'est à dire si le caractère 13 est contenu dans le tampon ou lorsque l'utilisateur appuie sur Entrée).
      • la primitive système cons_read prélève une ligne contenue dans le tampon associé au clavier. Si aucune ligne n'est disponible, l'appelant est bloqué jusqu'à la frappe du prochain caractère de fin de ligne (ASCII 13).
    • Nous avons gérer l'écho des caractères lorsque l'utilisateur tape au clavier. Attention, les caractères ne sont pas traités de la même façon entre l'echo (quand on tape au clavier) et le print (quand on appelle printf ou cons_write). A nouveau, les spécifications expliquent tout !
    • Nous avons adapté les tests donnés par le squelette du projet afin que la signature de cons_read soit celle décrite par Ensiwiki.

Voici les primitives systèmes implémentées à l'issue de cette phase : cons_echo et cons_read.

Conseils

Pour cette phase, nous conseillons de :

  • bien réfléchir aux comportements attendus et préciser tout ajout fait aux spécifications !

Ressources

Pour cette phase, nous avons utilisé les documentations proposées dans Ensiwiki, à savoir les spécifications clavier et les détails de cons_read et de cons_echo

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

100 %

Déroulé

Au cours de cette septième phase,

  • Nous avons créé un processus utilisateur représentant notre shell.
  • Nous avons créé une structure représentant une commande shell et un tableau de commandes représentant toutes les commandes pouvant être lancées dans notre shell (un peu comme pour la fonction test_proc du fichier user/test.c).
  • Ce processus exécute une fonction principale qui affiche le prompt, lit les commandes shell et les exécute.
  • Nous avons créé une fonction qui permet de récupérer 0, 1 ou plusieurs arguments pour une commande donnée.
  • Pour chaque commande (sauf quelques exceptions), nous avons implémenté une fonction qui créé un processus exécutant l'appel système associé à la commande saisie.

Nous avons implémenté les commandes suivantes :

  • ps : Affiche la liste des processus courants (avec informations)
  • test : Lance l'interface interactive de tests
  • exit : Quitte le shell
  • sleep <nb_secondes> : Attend pendant <nb_secondes> secondes
  • clear : Efface le contenu de la console
  • kill <pid> : Tue le processus de pid <pid> (sauf idle de pid 0)
  • logo : Affiche le logo de notre shell
  • print <une_chaîne> : Affiche la chaîne <une_chaîne>
  • toggle-clock : Active / désactive l'affichage du nombre d'interruptions en haut à droit de la console
  • chprio <pid> <prio> : Donne la priorité <prio> au processus de pid <pid> (seulement si la nouvelle priorité est plus basse que la priorité actuelle et sauf pour idle de pid 0)
  • sysinfo : Affiche différentes informations sur le système
  • echo : Active / désactive le mode echo
  • uptime : Affiche le nombre d'interruptions depuis le démarrage du système
  • help : Affiche l'ensemble des commandes disponibles
  • loop : Lance un processus qui effectue une boucle infinie : à utiliser pour tester les autres commandes (ce processus n'est pas attendu par le shell)
  • waitpid <pid> : Permet au shell d'attendre son fils de pid <pid> (utile si on l'utilise avec loop)

Pour implémenter ces commandes, nous avons ajouté les primitives / appels systèmes suivants : ps, ps_free, sleep, clear, toggle_clock, sysinfo et toggle_echo.

NB : Les processus créés pour exécuter les appels systèmes associés aux commandes shell sont toujours attendus par le shell (le processus parent) sauf pour la commande loop. La commande (et l'appel système) waitpid est directement réalisé par le shell.

Conseils

  • On pourrait être tenté de coder vite (et mal) le shell : prendre le temps de bien organiser son shell / de factoriser le code.
  • Il n'y a pas de difficulté particulière à part la gestion des paramètres des commandes : strtok peut vous aider !
  • Vous pouvez modifier le Makefile côté user pour inclure les fichiers shared/strtoul.c et shared/strtol.c pour avoir accès à des fonctions particulières qui peuvent pour aider à manipuler les chaînes (comme strtoul).

Ressources

Pour cette phase, nous avons utilisé les ressources suivantes :

Extensions

A la fin du projet, nous avons eu le temps de réaliser quelques extensions :

  • 0) Cela ne constitue pas vraiment une extension, mais nous avons testé notre système sur une VM VirtualBox (cela permet de débusquer quelques derniers bugs mémoire).
  • 1) Tout d'abord, nous avons complété les commandes disponibles pour notre shell. Pour plus de simplicité, nous les avons toutes présentées au niveau de la phase 7.
  • 2) Nous avons mis en place la gestion du caractère spécial CTRL+L : faire CTRL+L provoque l'effacement de l'écran à l'exception de la dernière ligne écrite qui devient la première ligne de l'écran. Cette commande est valide dans le shell mais aussi en dehors.
  • 3) Nous avons mis en place la gestion du caractère spécial CTRL+G : faire CTRL+G émet un bip (avec le speaker). Pour ce faire, nous avons utilisé ce tutoriel.
  • 4) Enfin, nous avons essayé de communiquer avec la carte son. Un enseignant nous a conseillé d'utiliser la carte SOUND BLASTER 16. Nous avons réussi à jouer un enregistrement musical (après beaucoup de grésillements !). Pour ce faire :
    • Comme notre système ne possède pas de système de fichier, nous avons dû récupérer un fichier WAV et le convertir en tableau d'octets en C (avec le logiciel bintoc).
    • Nous avons utilisé ce tutoriel pour la mise en place de la carte son en général.
    • Nous avons mis en place le code minimal pour lire l'équivalent du tampon de la carte son (le début de l'enregistrement)
    • En gérant l'interruption 37 et l'IRQ5 (interruption levée par la carte son pour demander le remplissage de son tampon), nous avons pu jouer l'enregistrement entier.
    • Cependant,le code restait spécifique à cette enregistrement donné.
  • 4bis) Pour rendre quelque chose de propre, nous avons donc mis en place une commande shell hello qui réalise l'appel système play_magic_sound. La primitive système associée joue un enregistrement court (de taille inférieure à celle du tampon de la carte son). Nous avons enlevé la gestion de l'interruption 37 et l'IRQ5 car nous n'avons pas eu le temps d'améliorer notre premier résultat.
    • Limites : Les versions récentes de QEMU (>= 4.0) ne gèrent pas bien la carte son SOUND BLASTER 16. Notamment, l'affichage est figé pendant que du son est joué et un grésillement peut apparaître en début d'enregistrement.

NB : La manipulation de la carte son (points 4 et 4bis) constitue une preuve de concept. Le code que nous avons produit est spécifique à un enregistrement. Nous souhaitions voir s'il était possible de jouer un son. Avec plus de temps, nous aurions pu essayer de rendre ce code générique.

Tests kernel

100 %

Floresmo fouremma tests kernel.png

Tests utilisateurs

100 %

Floresmo fouremma tests user.png

Démonstration de l'interprète de commandes

Floresmo fouremma demo 1.png

Floresmo fouremma demo 2.png

Floresmo fouremma demo 3.png

Plannings

Planning prévisionnel

Au lancement du projet, nous avons défini notre planification en fonction de la difficulté des phases. Cette difficulté a été établie suivant la lecture des objectifs de chaque phase (plus ou moins longues et difficiles) ainsi que des retours de nos prédécesseurs sur ce projet. Nous avons par exemple pu identifier que la phase 3 et la phase 5 semblaient être les plus longues. Voici notre planning prévisionnel :

Fouremma floresmo planning prev.png

Remarque : Nous avons considéré notre planification prévisionnelle sans prendre en compte la réalisation d'extensions. Nous n'envisagions d'en réaliser que dans le cas où nous venions à terminer le projet en avance.

Planning effectif

Voici le planning réel de notre avancement sur les différentes phases :

Fouremma floresmo planning reel.png

Remarque: Nous avons consacré la dernière demi-journée du projet à la documentation de cette page Ensiwiki et aux derniers tests.

Journal de bord

Semaine 1

Lundi 7 juin

⇒ Début de la Phase 1

  • Prise en main de l'environnement de développement
  • Lecture des documentations (phases du projet, rendus attendus, …)
  • Code du printf côté kernel et tests

⇒ Fin de la Phase 1

⇒ Début de la Phase 2

  • Lecture des documentations relatives à la notion de processus et de changement de contexte entre deux processus
  • Implémentation de la structure contenant les informations d'un processus et de la table des processus
  • Tests pour "passer d'un processus à un autre"

Mardi 8 juin

Suite de la phase 2

  • Passage d'un processus à l'autre
  • Aller-retour entre les processus
  • Ordonnancement selon l'algorithme du tourniquet
  • Lecture de la documentation sur le timer
  • Implémentation du timer

Mercredi 9 juin

Suite de la phase 2

  • Ordonnancement avec timer et tests

⇒ Fin de la Phase 2

⇒ Début de la Phase 3

  • Lecture des documentations relatives à l'ordonnancement, la terminaison et l'endormissement
  • Ordonnancement avec file d'attente à priorité
  • Endormissement d'un processus

Nous avons aussi passé notre soutenance pour notre projet du module de Conception d'Applications Web.

Jeudi 10 juin

Suite de la phase 3

  • Terminaison d'un processus et tests
  • Lecture des spécifications relatives à la filiation des processus
  • Conception de l'implémentation de la filiation
  • Modification de la structure d'un processus pour qu'il puisse prendre un argument
  • Phase de refactor : Implémentation des primitives systèmes (presque terminée pour celles de la Phase 3). Nous avions utilisé les noms de fonction donnés dans les TP. Nous avons dû renommer les fonctions déjà créées et tester à nouveau.
    • Primitives implémentées : cons_write, getpid, getprio, chpro, start, kill, wait_clock, clock_settings, current_clock

Vendredi 11 juin

Suite de la phase 3

  • Implémentation du système de filiation : arbre de filiation, adaptation des primitives start, kill et exit en conséquence
  • Implémentation des pritimives systèmes : suite et fin pour la Phase 3
    • waitpid
  • Lancement des premiers tests fournis côte kernel (tests 1 à 9)
    • quelques corrections associées (sur start - chprio - waitpid - wait_clock)

Semaine 2

Lundi 14 juin

Suite de la phase 3

  • Tous les tests kernel pouvant être lancés jusqu'à la phase 3 fonctionnent ! \o/ [tests 1 à 9]

⇒ Fin de la Phase 3

⇒ Début de la Phase 4

  • Lecture des spécifications relatives aux files de messages
  • Mise en place des structures de données et constantes nécessaires aux files de messages
  • Implémentation des primitives pcreate, pdelete, psend, preceive, preset, pcount
  • Tests des files de messages
  • Lancement du reste des tests fournis côté kernel [tests 10 à 20]
    • Le test 13 ne passe pas !

Mardi 15 juin

Suite de la phase 4

  • Refactor / correction pour les files de messages
    • Pour chaque file de message, on utilise deux files d'attente : une pour les processus en attente sur file vide et une pour les processus en attente sur file pleine
    • Refactor de preceive : si un processus est en attente sur file pleine, on dépose immédiatement son message dans la file
    • Refactor de psend : si un processus est en attente sur file vide, on lui transmet immédiatement le message du processus courant
    • Refactor de pcreate : Un test causait un integer overflow !
  • Tous les tests fournis passent côté kernel (sauf le test 19 qui ne peut fonctionner qu'après la phase 6)
  • Mise au propre du code réalisé pour la phase 4 (commentaires, factorisation de fonctions, ...)

⇒ Fin de la Phase 4

⇒ Début de la Phase 5

  • Lecture de la documentation relative aux interruptions et aux segments mémoires.
  • Lecture d'un certain nombre de pages Ensiwiki d'anciens projets systèmes (notamment celui-ci).
  • Préparation du plan de bataille
  • Mise en place des piles utilisateur et superviseur pour un processus et modification de la primitive système start avec :
    • Allocation des deux piles (mem_alloc et user_stack_alloc)
    • Préparation de la simulation d'un retour d'interruption avec changement de privilège après la création du processus (créé en mode superviseur et repassant en mode utilisateur tout de suite après)
      • Au niveau de la pile superviseur : On empile USER_DS, puis le sommet de la pile utilisateur, puis 0x202 (valeur d'initialisation du registre EFLAGS en mode utilisateur) puis USER_CS puis le pointeur de la fonction à lancer puis un pointeur vers une fonction assembleur que nous avons implémentée (qui donne la valeur USER_DS aux registres DS, ES, FS et GS et appel iret).
      • Au niveau de la pile utilisateur : on empile l'argument puis notre fonction assembleur faisant un appel à la primitive système exit

Mercredi 16 juin

Suite de la phase 5

  • Tests du bon fonctionnement du passage du mode superviseur au mode utilisateur après la création d'un processus
    • Tests avec GDB (vérification de la valeur de registres, des valeurs empilées et du pointeur de pile)
  • Écriture de l'adresse la plus haute du sommet de pile superviseur et écriture de la valeur KERNEL_DS au niveau de la TSS à chaque changement de contexte (les adresses sont données ici)
  • Implémentation du traitant de l'interruption 49 côté kernel (qui appelle une fonction C nommée sys_call appelant la primitive système en fonction d'un numéro passé en paramètre)
  • Modification de la fonction définissant le traitant pour une interruption donnée : DPL à 0 ou 3 selon un paramètre booléen (voir la page Ensiwiki Aspects techniques)
  • Réalisation d'un premier appel système depuis le mode utilisateur : getpid
    • Côté utilisateur : réalisation du code assembleur qui passe les paramètre dans les registres et lève l'interruption (instruction int)
    • Côté superviseur : appel de la primitive système getpid dans la fonction C sys_call

Jeudi 17 juin

Suite de la phase 5

  • Implémentation des autres appels systèmes (passage de paramètres par les registres et levée d'interruptions côté utilisateur / adaptation du traitant de l'interruption 49 côté superviseur)
  • Résolution de deux problèmes sur la terminaison d'un processus :
    • Un appel explicite à exit provoquait une erreur de code 0 : résolu en augmentant la taille de la pile superviseur
    • Notre fonction de terminaison exit_handler (faisant appel à exit et dont l'adresse est empilée au niveau de la pile utilisateur) ne pouvait être appelée en mode utilisateur car elle était encore dans l'espace superviseur ! Nous avons dû "placer" cette fonction côté utilisateur et utiliser son adresse fixe côté superviseur (nous avons prévu de documenter ce problème dans notre description de la phase 5 !).
  • Nous avons pu commencer à lancer les tests fournis côté utilisateur. Après une correction mineure (mais trouvée au bout d'une bonne heure sur gdb), presque tous les tests fournis passent en mode utilisateur !

Vendredi 18 juin

Suite de la phase 5

  • Implémentation de la protection des accès (pour faire passer le test 18)
    • On vérifie (dans le programme) que les variables pointeurs passées en paramètre des appels systèmes soient bien dans la zone utilisateur pour les processus utilisateurs (par exemple, pour les primitives start, waitpid, ...).
    • Nous avons aussi testé que des exceptions sont levées si on essaye d'accéder à une zone mémoire de l'espace superviseur depuis le mode utilisateur ou si on appelle directement une primitive système depuis le mode utilisateur (sans interruption) : on obtient un blue screen ! (pour une fois c'est ce que l'on veut).

⇒ Fin de la phase 5

⇒ Début de la phase 6

  • Lecture de la documentation relative au clavier et à la console
  • Implémentation d'une structure représentant le tampon du clavier (tampon circulaire)
  • Implémentation du traitant de l'interruption 33 qui appelle do_scancode et implémentation de keyboard_data qui est appelée par do_scancode (comme expliquées dans les spécifications)
  • Modification de la fonction d'affichage d'un caractère à l'écran pour gérer la différence entre l'echo (quand on tape au clavier) et le print (quand on appelle printf ou cons_write)
  • Implémentation des primitives systèmes cons_echo et cons_read
  • Pour l'implémentation de cons_read :
    • Nous avons modifié les tests pour que l'appel / la primitive système cons_read ait la signature donnée dans les spécifications sur Ensiwiki
    • Ajout d'un nouvel état : bloqué sur tampon clavier
    • On bloque le processus tant que le tampon du clavier ne contient pas le caractère 13 (touche entrée) sinon on lit le nombre de caractères souhaité
    • On débloque le processus quand le caractère 13 a été saisi

Semaine 3

Lundi 21 Juin

Suite de la phase 6

  • Revue de code sur les changements réalisés pendant le week-end (en vue de pousser sur notre dépôt git)
    • refactor des types int
    • correction d'un bug après test de notre OS sur une VM VirtualBox
    • différenciation des processus kernel (dont idle) et des processus utilisateur
  • complétion de notre page Ensiwiki (phases 5 et 6)
  • Adaptation de la primitive cons_read :
    • ajout du caractère de fin de chaîne après les caractères lus (le paramètre length ne le prend pas en compte)
    • en revanche, on ne vérifie pas que l'espace alloué pour stocker les caractères est suffisant : c'est au programmeur de le faire !

⇒ Fin de la phase 6

⇒ Début de la phase 7

  • Lecture des spécifications relatives à l'interprète de commandes
  • Liste des commandes que nous souhaitons implémenter
  • Implémentation d'une fonction qui lit une ligne de commandes, la parse et exécute la commande associée (commande avec 0 ou 1 paramètre entier seulement)
  • Implémentation de nos premières commandes shell : ps, test, exit et sleep.
  • Ajout des primitives systèmes ps et sleep.

Mardi 22 Juin

Suite de la phase 7

  • Refactor/correctif : enlever tous les ../shared/ dans les mentions include (ce n'est pas nécessaire !)
  • Ajout du fichier strtoul.c pour la compilation dans user/Makefile (sinon impossible d'utiliser les fonctions qu'il contient!)
  • Implémentation d'autres commandes shell :
    • clear (pour effacer l'écran)
    • kill
    • logo
    • print [une chaîne de caractères],
    • toggle-clock (masque ou affiche le nombre d'interruptions en haut à droite de la console),
    • chprio (seulement pour baisser la priorité),
    • sysinfo,
    • echo (activation / désactivation du mode echo),
    • uptime (nombre d'interruptions depuis le démarrage du système),
    • help,
    • loop (boucle infinie pour tester les autres commandes sur un autre processus)
  • Ajout des primitives systèmes : clear, toggle_clock, sysinfo et toggle_echo
  • Implémentation d'une fonction qui peut lire un nombre quelconque de paramètres pour une commande shell
  • Rédaction d'un README (pour présenter architecture et choix techniques)

Mercredi 23 Juin

Suite de la phase 7

  • Implémentation d'une autre commande shell :
    • waitpid

⇒ Fin de la phase 7

⇒ Début des extensions

  • Implémentation de quelques extensions :
    • CTRL+L : efface ce qui est affiché sur la console à l'exception de la ligne courante
    • CTRL+G : effectue un beep sonore (possible en manipulant le speaker). Pour ce faire, nous avons utilisé ce tutoriel.
  • Réflexions sur plusieurs sujets d'extensions que nous avions envisagés mais finalement évincés :
    • Implémentation de la commande CTRL+C
      • Difficile de le faire rapidement (besoin d'une gestion des signaux)
    • Gestion des flèches → et ← : modifier une ligne en cours d'écriture
      • Faisable mais ne nous intéressait pas tellement
    • Gestion des flèches ↓ et ↑ : historique des commandes
      • Difficile avec la façon de fonctionner de cons_read
    • Scroll dans le shell avec la molette de la souris
      • Faisable mais occupe une certaine place en mémoire...

Nous avons pris beaucoup de temps pour réfléchir à ce que nous allions faire. Nous voulions que l'implémentation des extensions respectent l'architecture générale de notre système.

Jeudi 24 Juin

  • Courte séance de refactoring : les primitives systèmes ps et sysinfo remplissent maintenant des structures d'informations spécifiques. L'affichage de ces informations est réalisé par le processus utilisateur qui effectue les appels systèmes associés
    • Ajout d'une primitive psfree pour libérer la mémoire allouée par la primitive ps (pour stocker les informations des processus créés)
  • Ajout de différentes captures d'écran sur notre page Ensiwiki
  • Travail sur une extension pour manipuler la carte son (SOUND BLASTER 16). Nous avons réussi à jouer une musique !
    • Pour ce faire nous avons utilisé ce tutoriel et le logiciel bintoc pour convertir notre enregistrement en tableau C (une obligation sans système de fichier)
    • Finalement, cette extension prend la forme d'une commande shell (hello) qui lance un enregistrement disant "bonjour !" avec la voix de quelqu'un de bien connu ...
    • Cette extension est perfectible : on entend un grésillement en début d’enregistrement (peut être à cause de qemu)
  • Adaptation de notre README et de notre page Ensiwiki à nos derniers changements

Vendredi 25 Juin

  • Relecture et correction de notre page wiki et de notre README
  • Tests finaux
  • Soutenance
  • Bonnes vacances !

Difficultés

  • Une difficulté majeure est la maîtrise de la documentation (parfois éparse ou parcellaire pour certaines phases). Les phases qui sont complexes sont en fait les phases les moins documentées. Sans les wikis des anciens élèves, cela aurait pris beaucoup plus de temps. La documentation est parfois (volontairement ?) floue sur des sujets qui mériterait d'être mieux creusés car difficiles à comprendre pour les néophytes en systèmes d'exploitation (comme l'implémentation de la fonction de terminaison côté utilisateur qui réalise l'appel système exit lors de la phase 5).
  • Le code du squelette du projet n'est pas à jour pour la primitive cons_read. Il a fallu adapter le code fourni : cela peut induire en erreur les programmeurs.
  • Il est primordiale de faire passer les tests fournis côté kernel dès que possible : il faut prévoir un bon moment pour corriger son programme et les faire tous passer (certaines spécifications sont plus précises quand on voit les tests). Quand vous passerez en mode user, vous en serez récompensé !
  • Nous n'avions pas bien compris les spécifications des primitives pour les files de message : nous avons dû recoder notre implémentation pour faire passer les tests.

Compléments aux spécifications initiales

Cette partie présente les compléments aux spécifications initiales que nous avons apportés pour notre système ainsi que différents éléments de configuration spécifiques.

Configuration

  • Nombre maximum de processus : 30
  • Longueur maximale du nom d'un processus : 30 caractères
  • Nombre maximum de files de messages : 10
  • Fréquence de l'horloge : 100 Hz et fréquence d'ordonnancement : 50Hz (dans tous les cas, la fréquence de l'horloge doit être un multiple de la fréquence d'ordonnancement : le code le vérifie)
  • La taille du tampon du clavier correspond aux nombres de caractères affichables sur l'écran
  • Une commande shell ne peut pas être plus longue que 40 caractères

Spécifications supplémentaires

  • Nous avons utilisé la signature de cons_read donnée dans les spécifications et pas celle des tests (unsigned long cons_read(char *string, unsigned long length)). Nous avons donc adapté les tests fournis à cette signature.
  • La primitive cons_read ajoute le caractère de fin de chaîne. Cependant, elle ne vérifie pas qu'assez d'espace a été alloué pour contenir la chaîne lue : c'est à l'utilisateur de l'appel système de le faire.
  • Dans le cas des files de message, la primitive chprio replace selon leur nouvelle priorité les processus bloqués sur file pleine ou sur file vide dans leur file d'attente respective.
  • Nous affichons le nombre d'interruptions depuis le démarrage du système en haut à droite de la console.
  • La caractère spécial \f fonctionne avec la fonction printf.

Primitives et appels systèmes supplémentaires

Pour implémenter nos commandes shell, nous avons ajouté les primitives / appels systèmes suivants :

  • ps : remplit une structure représentant les informations sur les processus créés (*)(**)
  • sleep : endort le processus courant pendant un nombre de secondes défini en paramètres
  • clear : efface l'écran de la console
  • toggle_clock : active / désactive l'affichage du nombre d'interruptions en haut à droite de la console
  • sysinfo : remplit une structure représentant des informations sur le système (*)
  • toggle_echo : active / désactive le mode echo

(*) Les primitives ps et sysinfo remplissent des structures (dont les types sont connus côté user et côté kernel et sont déclarés dans le répertoire shared) réunissant les informations associées (respectivement, les informations de tous les processus créés et les informations sur le système). Dans notre implémentation, c'est le shell qui s'occupe de déclarer ses structures, de réaliser les appels systèmes et ensuite de formater et d'afficher les informations.

(**) L'appel à la primitive ps doit toujours être suivi de l'appel à la primitive ps_free qui libère l'espace alloué par la primitive ps. Il a été nécessaire de procéder ainsi car l'appelant ne connaît pas le nombre de processus créés à l'avance. C'est la primitive ps qui alloue la mémoire (en mode kernel).

Choix techniques

Cette partie présente différents choix techniques que nous avons pris pour notre implémentation.

Structures de données

Fouremma floresmo data structure.png

(*) Nous avons utilisé une file d'attente à priorité dans les cas suivants :

  • pour l'ordonnancement : on utilise une file d'attente à priorité des processus prêts (attribut prio)
  • pour la filiation : pour chaque processus parent, on utilise une file des processus fils (attribut state : pour trouver facilement le premier fils zombie)
  • pour les files de messages : pour chaque file de message, on utilise :
    • une file d'attente à priorité des processus bloqués sur file vide (attribut prio)
    • une file d'attente à priorité des processus bloqués sur file vide (attribut prio)
  • pour la gestion du clavier : on utilise une file d'attente à priorité des processus bloqués sur les entrées clavier (attribut prio)

Autres choix

  • Nous passons les paramètres des appels systèmes par les registres (avant de lever l'interruption $49) : le registre %eax correspond au numéro de l'appel système.
  • Nous avons utilisé les types définis dans le fichier shared/stdint.h (int8_t, uint8_t, ...) uniquement quand nous voulions contrôler la taille des données manipulées. Par exemple,
    • pour écrire dans la zone mémoire représentant l'écran
    • pour enregistrer un traitant d'interruption
    • pour définir les piles systèmes et utilisateurs
    • etc...

Dans les autres cas, nous avons préféré utiliser les types de base comme dans les signatures des primitives systèmes pour plus de cohérence.

Limites de notre implémentation

Les limites de notre implémentation (que nous avons identifiées) concernent essentiellement la console, le clavier et les extensions :

  • Lorsqu'on appuie sur la touche backspace, l'effacement des caractères spéciaux (par exemple, Ctrl+C, les flèches, ...) et des tabulations n'est pas géré : l'effacement se fait caractère par caractère. Ainsi, il peut rester des caractères affichées à l'écran alors que le tampon du clavier et vide.
  • Lorsqu'on appuie sur la touche backspace, il n'est possible d'effacer que la ligne courante.
  • Si on ajoute un espace après une commande shell sans argument ou après le dernier argument d'une commande shell, la commande ne sera pas reconnue : il faut taper sur la touche Entrée directement.
  • Nous avons conservé la gestion des exceptions de base : si une exception est levée, un blue screen apparaît.
  • Pour notre extension, nous utilisons la carte son SOUND BLASTER 16. Les versions récentes de QEMU (>= 4.0) ne gèrent pas bien cette carte son. Notamment, l'affichage est figé pendant que du son est joué et un grésillement peut apparaître en début d'enregistrement.
  • La manipulation de la carte son constitue une preuve de concept. Le code que nous avons produit est spécifique à un enregistrement. Nous souhaitions voir s'il était possible de jouer un son. Avec plus de temps, nous aurions pu essayer de rendre ce code générique.

Ressources externes

Voici toutes les ressources externes que nous avons utilisés tout au long de ce projet :

Binaire

Pour tester notre système !

Voici le binaire de notre système à télécharger : kernel.bin.

Lancer la commande suivante : qemu-system-i386 -m 64M -kernel <chemin_vers_le_binaire> -soundhw sb16 -soundhw pcspk.

Les options permettent de faire fonctionner le speaker et la carte son.