Projet système PC : 2012 – Valentin BOUSSON et Maxime CREUSOT

De Ensiwiki
Aller à : navigation, rechercher


Craign'OS
CraignOS.png

Développeurs Valentin BOUSSON
Maxime CREUSOT

Présentation du Projet

Le projet système fait partie du Projet de Spécialité de l'Ensimag (plus de détails sur le site officiel de l'école).

L'Équipe

Ce projet est réalisé par l'équipe suivante :

Tuteur de l'équipe : Grégory Mounié

Objectif

Le but du projet était de créer un système d'exploitation pour PC, sur une architecture Intel x86. Le système d'exploitation doit comporter au minimum les fonctionnalité suivantes :

  • Multiprocessus et utilisation de sémaphores pour les problèmes de concurrence.
  • Protection du noyau contre les mauvaises utilisations/attaques de l'utilisateur.
  • Gestion d'une console, VGA + clavier.
  • Interaction avec l'utilisateur à travers un interpréteur de commandes.

Les spécification détaillées sont ici.


Le projet s'inscrit à la suite des cours de système de 2A :SEPC, CSE et PSE. D'ailleurs, l'objectif des cours de PSE étant de coder les fonctionnalités essentielles à la gestion du multiprocessus (context_switch, scheduler,..), nous sommes repartis de la même (cf base) (presque), donnée par les enseignants.

L'objectif de l'équipe était, avant même la réalisation du cahier des charges, une compréhension la plus précise possible des mécanismes mis en jeu, de tout le code qui nous a été fourni en plus de celui que nous avons produit, en prenant le temps d'éviter l'"à peu près".

Déroulement

Le projet système s'est déroulé du 21 mai 2012 au 15 juin 2012.

Les encadrants fournissaient une feuille de route, détaillant les différentes étapes par lesquelles il était préférable de passer pour mener le projet à bien. Cela en commençant par la gestion de l'affichage à l'écran (pour au moins pouvoir débugger), et en terminant par l'interpréteur de commandes, afin de pouvoir contrôler dynamiquement notre système.

Des extensions sont également proposées : pilote carte graphique, pilote carte son, gestion de la mémoire virtuelle, ... Nous n'avons pas eu le temps d'aller plus loin que le cahier des charges minimum, pour le faire le mieux possible.


Notre planning s'est découpé assez grossièrement en :

  • 1ère semaine : mise en place de l'horloge, des structures de processus et du scheduler,
  • 2ème semaine : mise au point, compréhension de la segmentation, des modes sur le x86,
  • 3ème semaine : mise en place fonctionnelle du mode protégé, de la console et de l'interpréteur de commandes,
  • 4ème semaine : corrections, tests et optimisations.

Réalisation

Choix d'implémentation

Parmi tous le code écrit, nous détaillons quelques points sur lesquels nous avons du faire un choix au niveau de la conception.

  • A propos de l'état interne des processus, il est défini par un type énuméré comprenant ACTIF, ACTIVABLE, ENDORMI, BLOQUE_FILS, BLOQUE_SEMAPHORE, BLOQUE_ENTREE. Chacun de ces état est aussi paramétré par des champs détaillant quand et comment le processus doit se comporter (par exemple : date_reveil, si le processus es endormi). Cependant, pour certains états, il nous a fallu mettre les processus concernés dans une liste de priorité pour pouvoir trouver le plus prioritaire rapidement. Par exemple, lors d'un appel à l'ordonnanceur, il faut effectivement trouver le plus prioritaire des activables. Cela est également nécessaire pour les processus bloqués sur un sémaphore particulier. Grace à la façon de gérer les files de priorités donnée par les encadrants, nous avons décidé de ne laisser qu'un seul champ de chaînage dans la structure d'un processus. En effet, son appartenance à une file en particulier dépend directement de son état, et un processus n'est que dans un seul état à la fois. On gère ainsi plusieurs file, en étant sur de ne pas avoir d'interconnexion. De plus, le scheduler factorise ses transitions d'un état à l'autre, d'une file à l'autre, pour simplifier le code et le rendre plutôt modulaire.


  • Nous avons passé du temps à comprendre les changements de niveau de privilège du processeur, et les exécutions détaillées que nous avons fait sur papier peuvent-être mises sous forme de "diaporama". Voici une exécution détaillée (le plus possible), depuis l'initialisation jusqu'après le premier changement de contexte.


  • Un point important qui nous a posé problème (détaillé sur le diapo précédent), est celui de l'exécution du processus "idle". En fait, ce dernier se doit d'exécuter des instructions privilégiées (hlt(), cli(), sti()) et doit par la même occasion s'exécuter en mode noyau. Comme chaque processus possède deux piles : une user pour l'exécution du code du processus et une kernel pour les appels noyau et changements de contextes forcés. "idle" est à part puisqu'il doit rester en mode noyau. Pour l'initialisation, après avoir créé toutes les structures, nous lançons un appel procédural à idle(), qui permet d'exécuter celui-ci sur la pile kernel globale. Cela ne pose pas plus de problème puisque lors d'une interruption, le pointeur de pile, le pointeur de code et les descripteurs de segments associés à la pile kernel global sont sauvegardés dans la pile kernel de "idle". On a donc bien remplacé la pile user de "idle" (inutile, du coup) par la pile kernel globale. On a donc des changements de contextes fonctionnels.

Primitives systèmes utilisables du coté utilisateur

Voici toutes les primitives systèmes que propose notre noyau. Nombres d'entre elles sont imposées par le cahier des charge, mais certaines ont été ajoutées par nous, pour faciliter la trace, et la création de l'interpréteur de commande. Elles sont réparties en plusieurs catégories :

Gestion des processus

int getpid(void); Retourne le pid du processus appeleant.
int start(int (*ptfunc)(void *), unsigned long ssize, int prio, const char *name, void *arg); Crée un nouveau processus paramétré par l'appelant.
void exit(int retval); Appel la destruction du processus appelant.
int waitpid(int pid, int *retvalp); Attend un fils pour sa destruction.
int kill(int pid); Tue un processus.
int ps(void); Affiche un résumé de l'état des processus.
int getprio(int pid); Retourne la priorité du processus appelant.
int chprio(int pid, int newprio); Change la priorité d'un processus.

Gestion des semaphores

int screate(short int count); Crée un sémaphore, initialisé à count.
int sdelete(int sem); Détruit un sémaphore.
int sreset(int sem, short int count); Réinitialise un sémaphore.
int scount(int sem); Renvoie la valeur courante du sémaphore.
int signal(int sem); Opération V() sur un sémaphore.
int signaln(int sem, short int count); succession atomique de count opérations V().
int wait(int sem); opération P() bloquante.
int try_wait(int sem); Opération P() non bloquante.
int sinfo(void); Affiche un résumé de l'état courant des sémaphores.

Gestion de l'horloge système

unsigned long current_clock(); Retourne la valeur courante du compteur système.
void wait_clock(unsigned long clock); Attend une valeur précise du compteur système.
void clock_settings(unsigned long *quartz, unsigned long *ticks); Retourne les paramètres de l'horloge système.

Gestion de la console

void cons_echo(int on); Active/Désactive l'echo de la console
int cons_write(const char *str, long size); Ecrit a l'écran
unsigned long cons_read(char *string, unsigned long length); Attend une entrée du clavier
void cons_modif_couleur(int couleur); Modifie la couleur actuelle de la console
void beep(); Fait faire un beep au PC (ne marche pas sur machine virtuelle)

Manuel

Voici les commandes reconnues par l'interpréteur, telle qu'elle sont décrite par la commande "help" :

Liste des commandes disponibles


Leur effet est assez explicite, mais notons qu'il est possible de lancer chacun de ces processus en tâche de fond grâce à l'ajout de "&" comme dernier mot de la commande. Dans le cas contraire, le shell se bloquera dans l'attente de la terminaison du fils, ou de sa terminaison forcée, générée sur l'appui de Ctrl+C.

Tests

Parmi les commandes fournies par l'interpréteur, on a la possibilité de lancer plusieurs tests : Les tests suivant passent.

  • test_division0
  • test_privileges
  • test_protection
  • test_profs : fournis (18/20 passent)

Des problèmes liés à la surcharge en processus apparaissent avec le test "fou", qui soulève encore des bugs non corrigés. Les tests fournis ne passant pas sont ceux liés à l'optimisation de l'algorithme d'allocation des sémaphores, et ceux liés à la bufferisation des entrées clavier.

Bilan

Nous avons réussi à terminer tout ce que nous avions prévu de faire : le cahier des charges minimal, bien réalisé : bien structuré et bien compris par nous deux. Nous avons pu rajouter des petites extensions, comme la gestion des processus en taches de fond, le beep, le Control-C sur le shell. Nous sommes plutôt fiers de nos résultats, même en sachant que un tel projet n'a pas vocation à fournir un produit utilisable, mais est bel est bien pédagogique.

Nous avons beaucoup appris de ce système, notamment concernant la mise en application concrète des principes vus en cours, et des notions de C et d'assembleur x86 avancé. Les fonctionnalités fournies par les processeurs Intel de la famille des x86 sont vastes, et nous avons pris beaucoup de temps pour comprendre comment les changement de modes s'effectuaient. Il est assez impressionnant de voir tout ce qu'un vrai processeur fournit, en matière d'aide pour la conception de systèmes.

Environnement de travail

Nous avons utilisé pour développer cet OS les outils suivant :

  • Mercurial, un gestionnaire de version distribué.
  • VirtualBox, machine virtuelle.
  • vim & Emacs, des éditeurs de texte.
  • La chaîne de compilation classique sous Linux, notamment gcc.
  • gdb, ddd.

Références / Documentation