Cours de Systèmes d'exploitation avancé

TDTP4 -- Ordonnanceur

A.B.Dragut et V. Risch
Vous êtes priés de rendre en fin de TP par mail:
  1. un log avec vos commandes pour chaque point des exercices à faire et
  2. une archive .tar.gz avec vos codes source.

Dans ces exercices

vous implémentez quatre des techniques d'ordonnancement les plus classiques, sur un mini-simulateur très simple dont on vous fournit le plus gros de la partie simulation effective.

Quoi de neuf

Les processus sont des programmes en cours d'exécution, maintenant on le sait assez bien, et qu'on a envie de voir tourner le plus possible, le plus rapidement possible, en le plus grand nombre possible.
Le nombre de CPU est malheureusement borné sur les systèmes actuels, et donc il faut ruser pour les faire passer à tour de rôle. Un des grands intêrets de toute la question du temps partagé est donné par les différences de vitesse entre le CPU et les périphériques, soit car il y a des humains dans le coup (clavier, etc.), soit par construction (disque dur, CDROM, etc.)
On tente donc d'optimiser divers critères, tout en sachant qu'on ne sait pas à l'avance combien de temps un processus prendra, ni comment sont structurés ses séries d'appels d'entrées-sorties.
Ici vous implémentez les techniques LIFO, FIFO, ROUND_ROBIN, et MULTILEVEL_QUEUE sur le mini-simulateur fourni.

exo_01, mise en place, et technique LIFO

Dans cet exercice

vous récupérez plusieurs fichiers avec les parties du simulateur, vous les étudiez et vous les complétez pour faire fonctionner la technique LIFO. Vous pourrez tranquillement ignorer une bonne partie du simulateur proprement dit (implémentation des méthodes des classes ProcInfo et ProcData). Vous travaillerez principalement sur les méthodes Scheduler::enQueueProc() qui rajoute un nouveau processus dans une file d'attente pour obtenir du temps CPU et Scheduler::electAProc(), qui élit un processus (parmi ceux de la file d'attente) pour avoir le CPU, tout en ayant une certaine idée de la déclaration des classes ProcInfo et ProcData. Pour en savoir plus, venez
par ici.

Technique LIFO

Last In First Out, signifiant le dernier entré est le premier sorti. La technique consiste à empiler les processus dans la file d'attente, et à les sortir par le même bout. Les méthodes push_back() et pop_back() sont les plus indiquées, ainsi que back().

Travail à suivre (c'est déjà fait, il faut juste bien le comprendre)


Pour des explications supplémentaires, venez par ici.

exo_02 Technique ROUND_ROBIN

Dans cet exercice

vous rajoutez la technique ROUND_ROBIN au simulateur.

ROUND_ROBIN

ou le tourniquet, chaque processus a droit à une tranche de temps et ensuite il passe derrière, pour laisser la place aux autres, chacun à tour de rôle.

Quoi de neuf

La différence fondamentale entre cette technique et la précédente est le bout par lequel on doit extraire les processus élus. Ici on les extrait par devant, avec front() et pop_front().

Travail à faire


Pour des explications supplémentaires, venez par ici.

exo_03 Technique FIFO

Dans cet exercice

vous rajoutez la technique FIFO au simulateur.

Technique FIFO

First In First Out, signifiant le premier entré est le premier sorti. Cette technique semble plus équitable, mais en fait les travaux longs font attendre les travaux courts qui se trouvent après eux. C'est comme une nationale, où l'on ne peut doubler les camions. La variante la plus simpliste (et primitive) ne rend pas le CPU même pendant les opérations d'entrée-sortie. Nous allons étudier une variante un peu améliorée, qui rend le CPU, mais qui favorise les processus commencés plus tôt.

Quoi de neuf

La variante du "Premier venu premier servi" que nous allons implémenter ici garde le premier processus sur le CPU tant qu'il ne fait pas d'entrée-sortie. De même, dès qu'il a fini avec l'"io", il reprend la main au premier moment d'élection. Ici on peut implémenter tout cela sans utiliser effectivement waitQueue. On considère que les priorités sont du pid le plus petit vers le pid le plus grand, et à chaque élection on recommence du début et on élit le premier en attente.

Travail à faire

  • Créez un nouveau sous-répertoire exo_03 (à côté des autres sous-répertoires exo_01 et exo_02).
  • Copiez les fichiers de l'exo 02 dans le sous-répertoire exo_03 (tout en renommant exo_03.cxx en exo_02.cxx,bien entendu)
  • Complétez ProcSched.cxx ainsi :
    • dans la méthode Scheduler::enQueueProc() qui rajoute un nouveau processus dans une file d'attente pour obtenir du temps CPU, rajoutez le cas vide
    •  case FIFO_SCHED: {
             break;
       }
      
    • dans la méthode Scheduler::electAProc() qui élit un processus (parmi ceux de la file d'attente) pour avoir le CPU,
      • dans le switch(thePolicy)
        • rajoutez un case FIFO_SCHED: et dans ce case
          • appelez la méthode Scheduler::displayQueue(&cerr) qui affiche les numéro des processus qui sont présents dans la file d'attente pour avoir le CPU comme pour les autres cas
          • balayez tous les éléments de pInfo -> procData (qui est ce vector de ProcData contenant les renseignements sur chaque processus), utilisant chosenProc comme indice qui varie (vous devez faire un static_cast<int>(pInfo -> procData . size()) pour la borne de la boucle, car size() renvoie un unsigned int, tandis que chosenProc est défini comme int).
            • pour le premier processus (donc élément de procData) qui est dans l'état ProcInfo::STAT_WAITING ( donc il faut comparer avec la donnée membre ProcData::procStatus de l'élément)
              • appelez ProcData::computeWaitDuration() comme pour les autres exercices
              • renvoyez chosenProc comme résultat
  • Compilez, et testez sur les exemples précédents.

Pour des explications supplémentaires, venez
par ici.

Tests et comparaisons

Travaillez dans le même répertoire que pour l'exercice précédent, i.e. dans exo_03. Récupérez également cette archive avec des exemples supplémentaires d'ici et décompressez-la bien entendu dans le dirsched (il s'agit donc de prog3.sched et prog4.sched). Faites ensuite tourner chaque méthode plusieurs fois sur tous les cinq prog.sched à la fois. Comparez les statistiques obtenues à la fin.
Il faut bien savoir que pour de vrais résultats statistiques il faudrait faire beaucoup plus d'expériences, avec plus de programmes, plus variés et plus longs. Le but de ces exercices a été plutôt une illustration de ces concepts qu'une évaluation statistique rigoureuse des différentes méthodes.

exo_04 Technique MULTILEVEL_QUEUE facultatif

Dans cet exercice

vous rajoutez la technique MULTILEVEL_QUEUE au simulateur.

MULTILEVEL_QUEUE

Le système assez complexe qu'Unix utilise, consistant en une collection de files chacune traitée comme un tourniquet. Un processus se voit assigné dans une file plutôt qu'une autre suite à un recalcul périodique de son utilisation du CPU. Plus il est gourmand, moins il a la prioritè.
Une fois les files (re)remplies, l'ordonnanceur fait tourner celle la plus prioritaire jusqu'à sa vidange complète. Après il passe à la file voisine, etc.
Le recalcul des priorités se fait périodiquement : typiquement on peut prendre un facteur de dix -- toutes les dix tranches de temps, on réassigne les priorités.
La formule (simplifiée), récursive, à utiliser est
Priorite(moment i) = (Utilisation CPU(depuis i-1 jusqu'a i) + Priorite(moment i-1))/2
Dans les vrais systèmes on rajoute des termes comme la priorité de base, ainsi que la valeur de nice.

Quoi de neuf

On doit se prendre une nouvelle donnée-membre de la classe Scheduler. Il s'agit du vector de deques mLevelQueue. De plus, dans la branche correspondante du switch de electAProc() il faut également prévoir le recalcul périodique des priorités, mais non pas à chaque appel.
C'est toujours enQueueProc() qui s'occupe de l'insertion dans la bonne file.
Pour simplifier, comme on l'a dit plus haut, le quantum de temps est constant.
Quand tout fonctionne, ça a l'air à peu prés comme ça, où on l'a lancé pour trois programmes.

-- Total 3 procs, now electing one among (0(2,1)) got # 2 prog2.sched ----------
+ prog0.sched+   prog1.sched  * prog2.sched* 
+io  x 8 8   +  cpu a 5 6     *cpu r 5 2   * 
+*^^^^^^^^^^^+  *^^^^^^^^     *cpu a 3 4   * 
+cpu x 7 7   +  cpu r 7 4     **^^^^^^     * 
+cpu a 5 6   +  io  x 8 8     *io  a 8 9   * 
+            +  cpu x 4 4     *cpu r 9 8   * 
+            +                *io  a 4 5   * 
----------------------------------------------------------------------------------
+ io 0       +   wait 0       * running 0  * 
  prog0.sched    prog1.sched    prog2.sched  
 io  x 8 8      cpu a 5 6      cpu r 5 4     
 cpu x 7 7      *^^^^^^^^      cpu a 3 4     
 *              cpu r 7 4      *             
 cpu a 5 6      io  x 8 8      io  a 8 9     
                cpu x 4 4      cpu r 9 6     
                               io  a 4 5     
  wait 0         wait 0         wait 0       
----------------------------------------------------------------------------------
-- Total 3 procs, now electing one among (0(0) 1() 2(1,2)) got # 0 prog0.sched ---
* prog0.sched*   prog1.sched    prog2.sched  
*io  x 8 8   *  cpu a 5 6      cpu r 5 4     
*cpu x 7 7   *  *^^^^^^^^      cpu a 3 4     
**^^         *  cpu r 7 4      *             
*cpu a 5 6   *  io  x 8 8      io  a 8 9     
*            *  cpu x 4 4      cpu r 9 6     
*            *                 io  a 4 5     
* running 0  *   wait 2         wait 2       

Dans le premier instantané, deux sont sur la file d'attente (file zéro, éléments 2 et 1), et un en train de "faire" des entrées-sorties. On vient d'élire prog2.sched et donc il passe en running priorité zéro, et c'est sa deuxième instruction qui est en cours.
Ensuite il finit sa tranche de temps, et donc passe en attente, et entre temps, l'entrés-sortie de prog0.sched est également terminée. L'ordonnanceur décide également de recalculer les priorités (on a choisi le bon moment du déroulement pour cet exemple) et c'est prog0.sched qui est maintenant élu.

Travail à faire

  • Créez un nouveau sous-répertoire exo_04 (à côté des autres sous-répertoires exo_01, exo_02 et exo_03).
  • Copiez les fichiers de l'exo 03 dans le sous-répertoire exo_04 (tout en renommant exo_03.cxx en exo_04.cxx,bien entendu)
  • Complétez ProcSched.cxx ainsi
    • dans la méthode Scheduler::enQueueProc() qui rajoute un nouveau processus dans une file d'attente pour obtenir du temps CPU,
      • dans le switch(thePolicy)
        • rajoutez le case MULTILEVEL_FEEDBACK_SCHED: et dedans
          • récupérez la valeur de la donnée membre ProcData::priorityLevel de l'élément indexé par procPid, qui donne ainsi l'indice de la file d'attente (i.e. du niveau ou doit résider ce processus)
          • si cette file n'existe pas, faites un resize() de mLevelQueue ( qui est le vector de toute les files, qu'on est en train de remettre à plat et regénérer)
          • rajoutez (avec push_back()) le procPid, par derrière, dans la file donnée par ProcData::priorityLevel (laquelle est une deque, et en même temps, elle est aussi un élément de mLevelQueue).
    • dans la méthode electAProc()
      • dans le switch(thePolicy)
        • rajoutez le case MULTILEVEL_FEEDBACK_SCHED: et dedans
          • incrémentez Scheduler::callCounter
          • s'il faut tout remettre à plat, i.e. sur la branche vraie du
            if(callCounter * allotedBaseTime >= mLevelTimeSlice) { // on doit recalculer
            
            • remettez à zéro callCounter
            • faites un resize(0) de mLevelQueue
            • recalculez les priorités, en appelant la méthode pInfo -> computePriorityLevel()
            • appelez la méthode Scheduler::enQueueAllProc(false), qui va s'occuper de la réinsertion
          • appelez Scheduler::displayQueue(&cerr) qui affiche les numéro des processus qui sont présents dans la file d'attente pour avoir le CPU comme pour les autres cas (attention, on a bien fini la branche oui, maintenant on fait toujours ceci et ce qui suit)
          • cherchez la première file non-vide (testez avec son size())
          • prenez-y le premier processus ( avec son front()) et éliminez-le de la file avec pop_front()
          • appelez la méthode ProcData::computeWaitDuration() comme pour les autres cas
          • renvoyez le numéro de ce premier processus
  • Compilez, et testez sur les exemples précédents.

Pour des explications supplémentaires, venez
par ici.

Tests et comparaisons

Travaillez dans le même répertoire que pour l'exercice précédent, i.e. dans exo_04. Faites tourner chaque méthode plusieurs fois sur tous les cinq prog.sched à la fois. Comparez les statistiques obtenues à la fin.
Il faut bien savoir que pour de vrais résultats statistiques il faudrait faire beaucoup plus d'expériences, avec plus de programmes, plus variés et plus longs. Le but de ces exercices a été plutôt une illustration de ces concepts qu'une évaluation statistique rigoureuse des différentes méthodes.