Projet système PC : 2017 - Mickael Brunel, Eliot Hautefeuille, Loïc Pauletto

De Ensiwiki
Aller à : navigation, rechercher
Lean.png
Titre du projet Projet système LeanOS
Cadre Ensimag

Équipe Mickael Brunel Eliot Hautefeuille Loic Pauletto
Encadrants François Broquedis, Gregory Mounie, Patrick Reignier


Équipe

L'équipe du projet est composée de trois alternants :

  • Mickael Brunel
  • Eliot Hautefeuille
  • Loïc Pauletto

Présentation et organisation du projet

L'objectif du projet système est la réalisation d'un noyau de système d'exploitation sur une architecture Intel. Avec un code minimaliste, nous devons créer un certain nombre de concepts clés associés aux systèmes d'exploitation, comme par exemple :

  • La création et l'exécution des processus
  • Leur synchronisation
  • Leur ordonnancement
  • La gestion des entrées/sorties (clavier, écran)
  • L'implémentation d'un interprète de commandes.

Suivi

Pour que nos enseignants puissent suivre notre avancement sur le projet, un journal de bord (présent ci-dessous) doit être rempli quotidiennement.

Organisation en phases

Le développement du projet est découpé en 7 phases.

  • Phase 1 : Prise en main de l'environnement
  • Phase 2 : Création et lancement de processus de niveau noyau
  • Phase 3 : Ordonnancement, création dynamique et terminaison de processus de niveau noyau
  • Phase 4 : Gestion des communications et synchronisation de processus de niveau noyau
  • Phase 5 : Séparation des espaces mémoire noyau et utilisateur : gestion de processus utilisateur
  • Phase 6 : Gestion du clavier et implémentation d'un pilote de console
  • Phase 7 : Implémentation d'un interprète de commandes

Les différentes phases sont détaillées sur la page : Projet système : roadmap

Ce découpage en phases permet à l'enseignant de savoir où en est le groupe. De plus, cela lui assure qu'un développement suivant ces étapes sera forcément plus simple à débugger.

Journal de bord quotidien

Nous utiliserons cette page wiki tout au long du développement de ce projet pour maintenir notre journal de bord, mais aussi pour documenter les différentes tâches effectuées.

08/06/2017

  • Lecture et analyse du sujet
  • Mise en place d'un conteneur docker pour travailler sur n'importe quelle machine
  • Gestion de l'affichage grâce au mini-projet effectué dans la matière "logiciel de base"

09/06/2017 - 10/06/2017

  • Création de la notion de processus
  • Mise en place du changement de contexte entre deux processus
  • Création de la notion d'interruption
  • Mise en place du timer
  • Correction des bugs concernant la gestion de l'affichage
  • Factorisation du code et reprise de celui-ci

12/06/2017

  • Mise en place de la fonction start
  • Gestion des paramètres dans les fonctions des processus
  • Lancement de plusieurs processus
  • Début de l'ordonnancement

13/06/2017

  • Reprise de l'ordonnancement
  • Mise en place du scheduler et de l'ordonnancement des processus

14/06/2017

  • Changement de gestion de projet (passage à gitlab avec issues)
  • Gestion de la filiation
  • Bug sur le exit

15/06/2017

  • Fixage du scheduler
  • Mise en place de wait_clock
  • Mise en place de waitpid
  • Mise en place de exit et de kill
  • Mise en place d'un système de test
  • Factorisation totale du projet
  • Passage des tests 1 à 4

16/06/2017

  • Passage des tests 5 à 9
  • Reprise de la gestion des processus tués

17/06/2017

  • Test wait_clock mis en place et passé
  • Début de la gestion de file des messages (tests 10 à 13)

19/06/2017

  • Débuggage des bugs sur les files de messages
  • Fin des différents tests (10 à 17 et 20)
  • Correction des bugs sur le waitclock de manière non élégante

20/06/2017

  • Réglage des problèmes de performance concernant le nombre de cycle par processus (dû à un nombre de printfs trop importants)
  • Correction des bugs sur le waitclock de manière propre
  • Phase 4 finit

21/06/2017

  • Début de la phase 5
  • Prise en main de la documentation
  • Réalisation de la libC
  • Début de la mise en place du clavier

22/06/2017

  • Correction des différents bugs sur la phase 5
  • Passage du ring0 au ring3 et inversement avec des bugs
  • Passage des premiers tests (1 à 3)

23/06/2017

  • Correction des bugs de stacks et de la TSS
  • Passage des tests 4 à 18 et du test 20

24/06/2017

  • Fin de la phase 6 : modification de la fonction cons_read

26/06/2017

  • Correction de la phase 6 : Passage du test 19
  • Réalisation de la phase 7
  • Ajout du planning dans la documentation

27/06/2017

  • Début d'implémentation de la stack réseau
  • Refactoring du terminal
  • Ajout du shutdown

Réalisation des différentes phases

Phase 1

Pour la phase 1, nous avons tout d'abord décidé de prendre en main le projet et de lire une grande partie de la documentation. Nous savions que cette lecture nous permettrait de gagner du temps ensuite. Nous avons également décidé de mettre en place un conteneur docker pour travailler sur n'importe quel type de machine. Même si ce choix n'a pas été complètement concluant, nous pensions qu'il nous ferait gagner un gain de temps. Malheureusement, cela n'a pas été le cas. La gestion de l'affichage a quant à elle été récupérée sur le mini-projet de l'an dernier.

Fin: 08/06/2017

100 %

Phase 2

Comme conseillé dans la roadmap du projet, nous avons décidé de séparer cette phase en deux parties :

  • Dans un premier temps la notion de processus et la gestion des changements de contexte
  • Dans un deuxième temps la mise en place du timer et des interruptions

Pour gagner du temps et des ressources durant cette phase, nous avons décidé de paralléliser les deux parties. Eliot s'est occupé de la partie timer et interruptions, tandis que Loïc et Mickaël ont avancé sur la notion de processus et de changement de contexte.

Fin: 10/06/2017

100 %

Phase 3

Cette phase a été longue mais a été importante pour le reste du projet. Il était malgré tout difficile de paralléliser les tâches. Pourtant, nous avons toujours réussi à travailler avec au moins deux sous-groupes. Même si nous avons pris du retard sur cette phase, le refactoring du projet va nous faire gagner du temps ensuite. Nous avons implémenté les différentes étapes de la vie d'un processus:

- Start: démarrage d'un processus. Cela nécessite de connaître la structure d'un processus, et d'initialiser la pile de ce dernier. Cela n'a pas posé trop de problème car le modèle avait été clairement établi à trois.

- Terminaison: la terminaison peut s'effectuer de diverses manières et présente de nombreux pièges algorithmiques. La terminaison peut prendre plusieurs formes: exit (le processus se termine de lui-même) ou kill (il est tué par un autre processus). Dans les deux cas, il faut libérer les espaces alloués et gérer correctement le changement d'état du processus. Il faut également penser au père qui pourrait être en attente de sa mort, et aux fils qui ne doivent pas survivre à leur père.

- Etat bloqué: lorsqu'un processus est en attente d'un événement (une information, un timing, la mort d'un fils, etc..), il ne doit plus prendre du temps processeur. Il faut pouvoir le réveiller au bon moment. Cet état a été rapidement développé.

- Waitpid: en lien avec la terminaison, la fonction waitpid permet de bloquer un processus jusqu'à la mort d'un de ses fils. Elle a nécessité quelques phases de débuggage, en lien avec la terminaison, pour s'assurer que le cycle de vie de tous les processus est respecté.

- Etat zombie: un processus qui a été tué passe en état zombie jusqu'à ce que son père le tue pour de bon, à sa propre mort ou lors d'un waitpid.

De plus, nous avons implémenté la mise en place de l'ordonnancement. Cela nous permet de changer d'état et de processus tel qu'indiqué dans la specification.

Fin: 16/06/2017

100 %

Phase 4

Cette phase introduit les files de messages. Les files sont basées sur le modèle producteurs-consommateurs où des processus (les producteurs) vont mettre des données sur la file et d'autres processus (les consommateurs) vont récupérer les données mises dans la file par les producteurs. Il faut bien gérer l'endormissement des processus si la file est pleine (pour les producteurs) ou si elle est vide (pour les consommateurs).

Nous avons choisi de paralléliser les deux tâches. Mickaël s'est occupé de la phase test de wait_clock, tandis que Loic et Eliot se sont occupés de la gestion de file de messages. La phase test étant courte Mickaël a aidé Loic et Eliot pour les débuggages de la file de messages.

Fin: 20/06/2017

Implémentation

100 %

Test

100 %

Phase 5

Comme toutes les autres phases, celle-ci est difficile à paralléliser. Loic et Eliot se sont occupés du passage du ring3 au ring0 et inversement, tandis que Mickael s'est occupé de la libC. La libC étant plus courte à réaliser, Mickael s'est ensuite occupé de la phase 6 en parallèle.

Lancement du mode utilisateur

Cette toute première étape consiste à vérifier que l'on est capable d'atteindre le code côté utilisateur. Il a fallu créer dans le noyau un contexte côté utilisateur afin que le noyau y accède en exécutant l'instruction iret.

Il est nécessaire de bien comprendre le contenu des piles d'exécution côté noyau et utilisateur afin de les remplir convenablement et de visualiser ce que fera iret.

Retour en mode noyau depuis l'utilisateur

L'étape précédente consistant à passer de l'utilisateur au noyau, celle-ci était logiquement le passage dans le sens inverse. Le but est, à l'issue de cette étape, de pouvoir effectuer des allers-retours entre modes noyau et utilisateur.

Le mécanisme de passage du mode utilisateur au mode noyau se fait par le biais d'une interruption logicielle (différente des interruptions matérielles utilisées par exemple pour les interruptions d'horloge) appelée explicitement par le mode utilisateur. De la même manière que l'étape précédente, la compréhension du contenu des piles est essentielle.

Bibliothèque d'appels système

Les appels systèmes codés côté noyau ne sont (heureusement) pas disponibles côté utilisateur, il faut donc les invoquer par l'interruption logicielle 49 susmentionnée. La fonction à exécuter est identifiable par un entier (une correspondance entier/fonction système est à mettre en place) et les arguments nécessaires sont également passés lors de l'interruption. Des vérifications peuvent être faites côté utilisateur telles que la validité des adresses (on ne peut raisonnablement accéder à des adresses dans le segment du noyau depuis le mode utilisateur).

Exit côté utilisateur

Tous les appels systèmes pouvant être correctement effectués depuis le mode utilisateur, il est tout à fait possible d'utiliser un exit explicite pour quitter un processus côté utilisateur. Cependant un problème se pose pour l'exit implicite (sur un return ou une fin de processus) : l'adresse de la fonction traitant cette fin de processus doit se trouver côté utilisateur mais le noyau doit la connaître lors de l'initialisation de la pile d'un processus. Plusieurs solutions plus ou moins satisfaisantes sont possibles, comme le mappage direct (s'inspirer de celui de user_start) ou le codage de la fonction côté noyau et sa copie dynamique côté utilisateur.

Tests

Une fois toutes ces étapes terminées, des tests fournis relativement complets peuvent être exécutés pour vérifier que rien n'a été oublié (des sauvegardes de registre, des mauvais remplissages de pile). Le système peut sembler bien marcher alors que ce n'est pas du tout le cas (nous avons par exemple rempli la pile noyau au de lieu de celle de l'utilisateur et avancé assez loin avant de nous en rendre compte).

Fin: 23/06/2017

Implémentation

100 %

Test

100 %

Phase 6

Cette phase s'est effectuée en parallèle de la phase 5. Elle a par la suite été reprise pour modifier la fonction cons_read qui ne correspondait pas à la documentation. Elle fut particulièrement laborieuse dû à un manque cruel de documentation concernant la fonction cons_read.

Elle correspond à l'implémentation d'une console. Dans un premier temps nous avons réalisé un pilote de clavier fonctionnant par interruption grâce aux appels système cons_read et cons_write. cons_read permet de récupérer et stocker dans un tampon les caractères tapés alors que cons_write affiche les caractères tapés à l'écran.

Fin: 26/06/2017

Implémentation

100 %

Test

100 %

Phase 7

L'interpréteur de commande a été développé en dernier lieu. Il permet d'avoir une interface avec l'utilisateur et démontre le bon fonctionnement du système. Diverses commandes ont été développées (dont la liste est accessible via la commande 'help'). Certaines permettent par exemple l'analyse du fonctionnement du système d'exploitation ('ps' pour les processus, 'sys_info' pour les processus et les files de messages).

Fin: 26/06/2017

Implémentation

100 %

Test

100 %

Choix techniques

IDE :

  • CLion

Gestionnaires de version:

  • Git
  • Gitlab pour la partie gestion de projet (issues)

Planning

Planning prévisionnel

Gant prevision bru haut paul.png

Planning réel

Gant reel bru haut paul.png

Bilan

Extension

Pour l'extension, nous avons tout d'abord décidé de rajouter quelques commandes au shell (lspci, fonctions liées aux files de messages, fonction lançant les tests etc). De plus, nous voulions développer la stack réseau. Malheureusement, nous n'avons pas pu réaliser cette extension. Voici les tâches que nous aurions réalisé si nous avions eu plus de temps.

  • Scanner les PCI (tâche réalisée)
  • Ecrire le driver associé à la carte Ethernet (adresse mac à récupérer)
  • Ecrire les protocoles de base (arp, IP, ICMP, UDP etc)
  • Ecrire la pile réseau et les passages entre les différentes couches
  • Convertir les messages reçus de big endian à little endian si besoin

Retours personnels

Si nous devions refaire le projet demain, nous changerions plusieurs choses :

  • Nous n'essaierions pas de mettre en place docker qui nous a fait perdre un après midi et qui n'a pas spécialement servi ensuite
  • Nous essaierons de paralléliser plus les tâches

Difficultés rencontrées

  • Phase 4 : Les files de messages ont été une fonctionnalité difficile à comprendre. La spécification peut donner lieu à plusieurs interprétations. En conséquence, lors de la validation des tests 13 et 14 le développement effectué était erroné. Il a fallu reprendre les fonctionnalités en ayant compris les tests.
  • Phase 5 : Concernant la séparation du mode kernel et du mode user. Cela ne demande finalement pas beaucoup de code, mais plutôt beaucoup de compréhension. Or les informations sont dures à trouver sur le wiki, il a fallu se tourner vers une documentation externe : OsDev
  • Phase 6 : La spécification du cons_read est tout sauf claire. De plus le test concernant cet appel système ne fonctionne pas (les prototypes sont différents).
  • Debug : Il est possible de débugger avec GDB le code écrit en espace user. La documentation n'est cependant pas écrite pour ça.

Debug

Nous avons pu débogguer coté utilisateur et coté kernel, à l'aide de cette configuration:

$ gcc -v
gcc version 4.7.0 (GCC)

$ gdb -v
GNU gdb (GDB) 7.4

$ cat ~/.gdbinit
define target hookpost-remote
    set architecture i386
    set directories 'pathToPsysProject/kernel:pathToPsysProject/shared:pathToPsysProject/user'
    file 'pathToPsysProject/kernel/kernel.bin'
    add-symbol-file 'pathToPsysProject/user/user.debug' 0x1000000
end

Il faudra les fichiers source coté utilisateur ( en ajoutant un `u` devant par exemple ) ( SAUF pour le fichier crt0.S ) Enfin, pour pouvoir débogguer le code assembleur coté utilisateur ( à l'exception de crt0.S ), il faudra ajouter `-g` dans les options de la compilation des fichiers assembleurs dans le Makefile coté utilisateur, exemple :

...
ASL=$(AS) -g -DASSEMBLER $(DEF) $(INC)
...

Documentation utilisée

Télécharger notre kernel.bin

Vous pouvez télécharger notre kernel.bin au lien suivant Fichier:Kernel brunemic hautefee paulettl.zip

Screenshot de l'application

Voici quelques screenshots de notre application

Screen1 lean.png


Screen2 lean.png


Screen3 lean.png


Screen4 lean.png