Projet système PC : 2013 - Romain Koenig et Valmon Leymarie

De Ensiwiki
Aller à : navigation, rechercher


0x0E Trap'OS

Développeurs Romain Koenig
Valmon Leymarie

Présentation

Équipe

Notre équipe se compose de deux étudiants :

Nos encadrants sont Gregory Mounier et Damien DEJEAN.

Motivations

Parmi tout les choix possibles de projets de spécialité, nous avons choisi celui-ci afin de mettre en pratique les connaissances que nous avons acquises lors des cours de Systèmes d'exploitation et programmation concurrente, et Conception des Systèmes d'Exploitation. Dès le début, nous savions que le projet serait très difficile, mais nous voulions relever le défi et réaliser notre système d'exploitation.

Déroulement

Le projet s'est déroulé sur trois semaines, du mercredi 22 mai 2012 au mercredi 19 juin 2013 (avec une semaine de coupure de partiel au milieu). Le projet s'est déroulé pour l'essentiel à l'Ensimag, de 8h à 18h et chacun chez soi le soir de 8h à minuit.

Réalisation

Implémentation du cahier des charges

Préparation

Pour préparer le projet nous avons à disposition une base de fichiers qui permettent d'implémenter un noyau de base, sans aucune fonctionnalités. A cela nous avons rajouter tout le travail effectué pendant les cours de pratique du système d'exploitation de début d'année. Ces derniers importent la gestion de l'affichage à l'écran de caractères, la gestion de l'horloge grâce au mécanisme des interruptions, ainsi qu'un embryon de gestion des processus avec un ordonnancement basique (celui du tourniquet).

Phase 1 : Affichage à l'écran

Cette phase est très importante pour le bon déroulement du projet par la suite. En effet nombres de tests sont effectués en affichant des traces à l'écran à l'aide de "printf". Cette phase est réalisé pendant la pratique du système en début d'année, mais lors de sont importation, nous nous sommes assurés de la bonne fonctionnalités des différentes fonctions en réalisant des tests pour chaque fonction importés.

Phase 2 : Gestion de processus

C'est une phase importante car elle introduit des concepts clés pour la programmation de systèmes. Elle consiste à gérer:

  • La création de processus.
  • Le changement de contexte et l'ordonnancement entre deux processus.
  • Le timer et les interruptions, pour obtenir un système à 'temps partagé'.
  • La terminaison des processus.

Nous avons aussi repris la base de notre code de pratique du système, mais cette fois-ci, il y a eu beaucoup plus de modification à faire. Les fonctions de lancement d'un processus : start et de terminaison : exit et kill sont celles qui ont le plus changés et elle ont été mise à jour durant tout le projet.


Phase 3 : Ordonnancement

Le changement de contexte et l'ordonnancement permettent à plusieurs processus de s’exécuter en même temps, ceci permet d'implémenter un 'pseudo-parallélisme' sur la machine. L'ordonnancement consiste à gérer tout ce qui concerne le cycle de vie des processus, en mode superviseur. Pour bien implémenter cette partie qui est cruciale, il est important de bien comprendre ce que fait l'ordonnanceur et surtout le changement de contexte, il faut bien connaître tous les registres et leurs fonctionnalités. Surtout pour la phase 5 où l'on effectue des changements de contexte avec la mémoire virtuelle et pour la protection kernel-utilisateur.

Phase 4 : Communication entre processus

Pour permettre aux processus de communiquer entre eux, il faut mettre en place des outils de communications. En effet en mode utilisateur les processus ne peuvent pas accéder à la mémoire du noyau ni à la mémoire des autres processus. On met donc en places des appels systèmes, accessibles au processus pour que ces derniers peuvent communiquer.

Les files de messages

Les files de messages sont des espaces en mémoire ou un processus peut envoyer en entier dans la file, en fin de file, ou retirer une valeur. Si la file est pleine et qu'un processus veut envoyer une valeur dans la file, alors le processus se bloque sur la file et attend que cette dernière se vide pour pouvoir envoyer une valeur. Si la file est vide et qu'un processus veut retirer une valeur dans la file, alors le processus se bloque sur la file et attend que cette dernière se remplisse avant de pouvoir retirer une valeur. Pour ce faire les appels systèmes : pcreate, pdelete, psend, preceive, pcount, preset ont été implémentés. Exemple de scénario :

  • P1 : pcreate(2) //création de la file 1 de capacité 2
  • P2 : psend(1,3) //Envoie de la valeur 3 dans la file 1
  • P1 : preceive(1,mess) //Récupération de la valeur 3 dans la file 1
  • P3 : preceive(1,mess) //Récupération d'une valeur, file vide, se bloque
  • p1 : psend(1,4) // Envoie de la valeur 4 dans la file 1
    • p3 récupère la valeur 4 après avoir repris la main
  • P2 : pdelete(1) //Destruction de la file 1


API de mémoire partagée

L'implémentation de files de message fait partie du sujet, néanmoins cela restreint le partage d'information à de simple entiers, passés par copie. La solution proposée est donc d'implémenter une solution de partage de mémoire. Ainsi une même page physique peut être mappé pour plusieurs processus. Nous avons choisi une adresse de manière arbitraire pour placer cette page pour chaque processus par exemple 0x70000000. Et cette page physique sera accessible par plusieurs processus et permet ainsi la communication entre ces derniers.

Phase 5 : Mémoire virtuelle et Mode Kernel/User

C'est une phase délicate, elle de loin la plus difficile du projet et c'est dans cette phase que nous avons passé le plus de temps.

Mémoire virtuelle

L'implémentation de la mémoire virtuelle a pour objectif de réaliser une isolation forte des processus. En effet, à la fin de cette partie, chaque processus possède son propre espace d'adressage. Dans lequel nous mettrons :

  • le code de l'application
  • ses données
  • la pile
  • le noyau

Le noyau sera toujours mappé entre les adresse 0 et 1G alors que les processus seront mappés entre 1G et 4G pour 3G d'espaces mémoire virtuelles donc. Pour notre implémentation nous avons choisi de placer la code à l'adresse 1G (0x40000000), la pile utilisateur à l'adresse 2G (0x80000000) (Il est important de noter le sens d'évolution de la pile et d'éviter que celle-ci ne déborde sur le code). La pile kernel (phase suivante) à l'adresse 512M (0x20000000).

Le processus ne partage donc rien avec ses voisins, le seul moyen pour lui d'y arriver, c'est de faire appel au noyau pour mettre en place des mécanismes de partage de mémoire.

Dans cette partie il est important de comprendre le rôle joué par la partie matérielle : la MMU. c'est elle qui fait la traduction entre les adresses physiques et virtuelles. Pour ce faire elle utilise la page des tables qui est donné par le registre CR3. Il est donc important de bien comprendre dans son code quand on manipule des adresses virtuelles ou physiques, nous avons mis un certains temps à bien comprendre cette partie là.

Séparation User/Kernel
  • Séparation physique du noyau et de l'application

A partir de ce point, le code écrit pour le noyau et le code écrit pour les applications utilisateurs ne seront plus dans les même fichiers, nous séparons les deux codes. Ainsi il n'y a plus de fonctions de tests dans notre noyau, ces dernières sont désormais des applications utilisateurs que nous pouvons lancer sur notre noyau.

  • Réalisation d'une bibliothèque des appels noyau

Les applications utilisateurs n'ont pas le droit d'accéder au fonctions du noyau, mais elle peuvent en avoir besoin (pour la communication entre les processus utilisateurs par exemple). C'est pourquoi nous avons réalisé une bibliothèque d'appels système, ces fonctions seront écrites en assembleur et mises dans une bibliothèque qui jouera donc pour nos applications le rôle que joue la libc dans le système Unix. Ces fonctions feront une interruption : int 49 pour passer en mode kernel.

  • Écriture du module de récupération de l'int.

Puisqu'on fait une interruption dans la bibliothèque d'appel système, il faut bien écrire son traitant dans le kernel, dans ce traitant on identifie quel fonctions a fait appel à l'interruption avec l'aide des registres puis on l’exécute en mode superviseur.

  • Gestion de deux piles par processus.

Lors de l'appel à int 49, le processeur change automatiquement de pile, avec une adresse contenue dans la TSS, c'est pourquoi à partir de ce point là il a fallu gérer deux piles par processus, une en mode utilisateur, que nous avons choisis de placer à l'adresse 2G et une kernel que nous avons choisis de placer à l'adresse 512M. Il faut bien faire attention ici aussi au changement de contexte.


Bilan

Organisation du projet

Pour ce projet, nous n'étions que deux, cela simplifie largement la communication au sein du groupe. Durant la journée nous étions tout les deux à l'Ensimag ce qui permet de communiquer directement. Le soir nous pouvions être soit ensemble une fois encore, ou chacun chez soi. Dans ce dernier cas, chaque avancé de l'un des membres du groupe été reporté au second via mail, SMS ou même par messages dans le commit de GIT.

En ce qui concerne le volume horaire de travail, il y avait vraiment beaucoup de choses à faire et nous avons passé beaucoup de temps à débugger la phase 5. Nous avons passé un peu plus de 13h par jour tous les jours de ce projet.

Difficultés rencontrées

Une des plus grandes difficultés rencontrés était le débuggage, en effet celui-ci était long et fastidieux étant donné la complexité du projet. De plus même à l'aide d'outil puissant comme GDB et DDD certaines erreurs furent longues à trouver.

Nous avons aussi manqué de temps sur la fin, en effet deux semaines c'est assez court pour un projet de cette ampleur. Puis dans la précipitations des derniers jours nous n'avons pas pu être aussi efficaces que souhaité.


Références

  • Documentation intel: [1]
  • OSDev.org
  • Modern Operating system, dessign and implementation, Andrew Tanenbaum

Outils

  • Qemu: outil de virtualisation
  • Git: gestionnaire de versions
  • Vim, Emacs: éditeur de code
  • ddd: débogueur