4MMPS Processus SLE

De Ensiwiki
Aller à : navigation, rechercher
AttentionCette page est maintenue uniquement par les enseignants. Afin de ne pas perturber le déroulement des cours, elle n'a pas vocation à être modifiée par les élèves. Mais si vous avez des modifications à proposer, merci d'en discuter ou d'envoyer un e-mail aux auteurs de la page (cf. historique)
Mycomputer.png  Deuxième Année  CDROM.png  Informatique 
Fleche haut.png

Généralisation à N processus

Vous devez ensuite généraliser votre code pour N processus : pour les tests, on choisira N = 4.

On rajoute donc 2 nouveaux processus dans le système, proc2 et proc3 dont le code est similaire pour l'instant à celui de proc1.

La généralisation ne nécessite pas beaucoup de changements :

  • la fonction ordonnance doit être adaptée pour implanter la politique du tourniquet, qui active les processus dans l'ordre de leur pid : 0, 1, 2, 3, 0, 1, 2, 3, etc. ;
  • on vous recommande de factoriser le code de création et d'initialisation des processus proc1, proc2, et proc3 avec une fonction int32_t cree_processus(void (*code)(void), char *nom) qui prend en paramètre le code de la fonction à exécuter (ainsi que le nom du processus) et renvoie le pid du processus créé, ou -1 en cas d'erreur (i.e. si on a essayé de créer plus de processus que le nombre maximum).

Avant de passer à la suite: séances 1 et 2 qui fonctionnent!

La suite du projet va réutiliser le code que vous avez développé au cours des séances 1 et 2. Nous vous fournissons un module qui vous aidera à tester une partie de ce que vous avez implanté au cours de ces séances.

Vous devez télécharger l'archive Module_debug.tar.gz et l'extraire dans le dossier de votre projet (celui qui contient start.c). Cette archive contient les trois fichiers suivants:

  • check.h, fichier d'entête du module de tests à inclure dans start.c pour tester vos fonctions ;
  • check.o, l'implémentation du module de tests ;
  • my_print.o, l'implémentation d'un printf utilisé pour affiché les résultats des tests (au cas où votre printf ne fonctionnerait pas complètement).

Voici la liste des fonctionnalités exposées par check.h:

/* Teste la valeur de retour de ptr_mem(15, 15). */
void check_ptr_mem(uint16_t *(*ptr_mem)(uint32_t, uint32_t));

/* Appelle efface_ecran() et vérifie que l'écran est bien rempli
 * d'espaces (donc vide). */
void check_empty_screen(void (*empty_screen)(void));

/* Appelle place_curseur(15, 15) et vérifie la position du curseur. */
void check_set_cursor(void (*set_cursor)(uint32_t, uint32_t));

/* Appelle ecrit_car(15, 15, '0', 1, 1, 1) et vérifie la valeur du mot
 * de 16 bits correspondant. */
void check_write_char(void (*write_char)(uint32_t, uint32_t, char, uint32_t, uint32_t, uint32_t));

/* Sauvegarde le contenu de l'écran, appelle defilement() puis vérifie
 * si le nouveau contenu de l'écran correspond à l'écran sauvegardé
 * décalé d'une ligne vers le haut. */
void check_scrolling(void (*scroll_down)(void));

/* Appelle init_traitant_IT(num_IT, traitant) et vérifie la valeur de
 * l'entrée de l'IDT correspondante. */
void check_idt_entry(void (*init_traitant_IT)(uint32_t, void (*traitant)(void)), uint32_t num_IT, void (*traitant)(void));

/* Appelle masque_IRQ(0, false) et vérifie la valeur du masque des
 * IRQs. */
void check_IRQ(void (*masque_IRQ)(uint32_t, bool));

Pour utiliser ce module de tests, vous devez d'abord ajouter les fichiers objets check.o et my_print.o à la ligne de compilation de kernel.bin. Modifiez dans votre Makefile la ligne correspondante comme suit:

# generation du noyau
kernel.bin: kernel.lds $(OBJS) task_dump_screen.o
	$(LD) $(LDFLAGS) -e entry -Tkernel.lds $(OBJS) task_dump_screen.o check.o my_print.o -o $@

Ensuite, Vous devez modifier la fonction kernel_start du fichier start.c pour qu'elle fasse appel à une ou plusieurs des procédures check_ décrites ci-dessus. Notez que chacune de ces procédures prend la fonction que vous souhaitez tester en paramètres.

Par exemple, le code suivant teste la fonction ptr_mem de votre projet:

#include <check.h>

extern uint16_t *ptr_mem(uint32_t lig, uint32_t col);

void kernel_start(void)
{
    check_ptr_mem(ptr_mem);

    sti();
    while (1) {
        hlt();
    }
}

Nous vous conseillons de tester les fonctionnalités de votre projet une par une, et de commenter le reste du code qui pourrait se trouver dans kernel_start pour éviter de polluer l'affichage du résultat du test.

Notez que l'utilisation de pointeurs de fonction permet de s'abstraire du nom que vous avez donné à votre fonction ptr_mem. Par exemple, le code suivant fonctionnera tout aussi bien, en considérant que vous avez nommé la fonction ptr_mem "mon_super_ptr_mem":

#include <check.h>

extern uint16_t *mon_super_ptr_mem(uint32_t lig, uint32_t col);

void kernel_start(void)
{
    check_ptr_mem(mon_super_ptr_mem);

    sti();
    while (1) {
        hlt();
    }
}

Si votre implémentation fonctionne comme prévu, l'appel de toutes les fonctions de tests du module check dans kernel_start devrait générer un affichage de ce type:

4MMPS ecran debug.png

Attention : le jour de l'examen de TP, vous n'aurez bien sûr pas accès à ce module de mise au point : nous le fournissons uniquement pour vous aider pendant le projet, mais il ne vous dispense pas de vous entrainer à chercher les bugs de votre code vous-même.

Ordonnancement préemptif

Dans la majorité des systèmes actuels, ce ne sont pas les processus qui se passent la main : les basculements d'un processus à l'autre sont provoqués par des événements venant de l'horloge système, et s'enchainent suffisamment rapidement pour donner à l'utilisateur l'impression que les processus s'exécutent en parallèle.

On va donc connecter l'ordonnanceur à l'interruption horloge, ce qui en pratique ne nécessite que très peu de modifications par rapport à ce que vous avez fait avant.

Les processus de tests seront maintenant les suivants :

void idle(void)
{
    for (;;) {
        printf("[%s] pid = %i\n", mon_nom(), mon_pid());
        for (int32_t i = 0; i < 100 * 1000 * 1000; i++);
        sti();
        hlt();
        cli();
    }
}

void proc1(void) {
    for (;;) {
        printf("[%s] pid = %i\n", mon_nom(), mon_pid());
        for (int32_t i = 0; i < 100 * 1000 * 1000; i++);
        sti();
        hlt();
        cli();
    }
}

... (idem proc2 et proc3)

Vous devez bien sûr remettre dans la fonction kernel_start toutes les initialisations nécessaires à l'interruption horloge que vous aviez géré pendant la séance 2 (note : ne mettez pas d'appel à sti dans kernel_start : c'est la fonction idle qui activera les interruptions la première fois).

Vous devez penser à ajouter un appel à la fonction ordonnance à la fin de la fonction appelée par le traitant de l'interruption horloge, pour provoquer le changement de processus.

L'affichage obtenu doit être le même que pour la séance précédente : on doit voir les 2 processus prendre la main l'un après l'autre. Il est normal que dans cette phase, l'affichage du temps écoulé (fait à la séance 2) ne soit plus correct : en effet, les boucles d'attentes insérées dans le code des processus sont non-interruptibles. On verra comment implanter un véritable mécanisme d'attente dans la partie suivante.

Endormissement des processus

On va maintenant implanter un mécanisme permettant d'endormir un processus pendant un certain nombre de secondes, de façon similaire à la fonction sleep de la bibliothèque C standard. Il s'agit d'une simple fonction void dors(uint32_t nbr_secs) qui prend en paramètre le nombre de secondes pendant lequel le processus doit dormir.

Une façon simple de mettre en oeuvre ce mécanisme consiste à rajouter un état ENDORMI et à garantir que la fonction d'ordonnancement n'activera pas les processus dans cet état tant que leur heure de réveil n'est pas dépassée.

Pour gérer le réveil, il faut stocker dans la structure décrivant chaque processus l'heure à laquelle il doit se réveiller. On mesurera le temps en nombre de secondes écoulées depuis le démarrage du système (une information déjà disponible depuis la séance 2 et qu'il suffit de rendre accessible à l'ordonnanceur). C'est la fonction d'ordonnancement qui devra réveiller tous les processus dont l'heure de réveil est dépassée.

Vous pourrez tester votre implantation avec par exemple les 4 processus ci-dessous, en supposant que nbr_secondes soit la fonction qui renvoie le nombre de secondes écoulées depuis le démarrage du système :

void idle()
{
    for (;;) {
        sti();
        hlt();
        cli();
    }
}

void proc1(void)
{
    for (;;) {
        printf("[temps = %u] processus %s pid = %i\n", nbr_secondes(), mon_nom(), mon_pid());
        dors(2);
    }
}

void proc2(void)
{
    for (;;) {
        printf("[temps = %u] processus %s pid = %i\n", nbr_secondes(), mon_nom(), mon_pid());
        dors(3);
    }
}

void proc3(void)
{
    for (;;) {
        printf("[temps = %u] processus %s pid = %i\n", nbr_secondes(), mon_nom(), mon_pid());
        dors(5);
    }
}

Le processus idle n'a lui bien sûr pas le droit de s'endormir, sinon on risquerait de se retrouver dans un système sans aucun processus activable !

Terminaison des processus

On va maintenant permettre aux processus de se terminer : vous pourrez alors enlever la boucle infinie autour du code des processus pour vérifier qu'ils se terminent bien. Le processus idle n'a bien sûr pas le droit de se terminer !

Terminaison explicite d'un processus

Pour commencer, il faut implanter une fonction void fin_processus(void) qui va réaliser le travail de terminaison d'un processus. Dans une première implantation, un processus voulant se terminer devra explicitement appeler cette fonction, comme par exemple :

void proc1(void)
{
    for (int32_t i = 0; i < 2; i++) {
        printf("[temps = %u] processus %s pid = %i\n", nbr_secondes(), mon_nom(), mon_pid());
        dors(2);
    }
    fin_processus();
}

La fonction de terminaison doit marquer le processus actif (puisque c'est forcément lui qui l'appelle) comme étant dans l'état MORT et ensuite passer la main à la fonction d'ordonnancement(). Il faut bien sûr aussi garantir que la fonction d'ordonnancement n'activera jamais un processus mort !

A ce stade, il peut vous être utile pour mettre au point votre système d'implanter une fonction void affiche_etats(void) qui affiche (par exemple en haut à gauche de l'écran) l'état de chaque processus du système (par un simple parcours de la table des processus).

Terminaison automatique d'un processus

Evidemment, dans un vrai système, on n'a pas besoin d'insérer un appel à fin_processus à la fin du code de chaque processus : la terminaison se fait automatiquement.

Une façon simple d'implanter cette terminaison automatique consiste à initialiser le sommet de pile de chaque processus avec l'adresse d'une fonction gérant la terminaison de celui-ci. On rappelle qu'on doit toujours copier l'adresse de début de la fonction dans la pile avant le premier changement de contexte (il suffit de décaler cette adresse pour qu'elle soit sous le sommet de pile).

Création dynamique de processus

Maintenant que les processus peuvent se terminer, il est intéressant de pouvoir en créer dynamiquement (sinon, on va rapidement se retrouver avec un système qui fait idle tout le temps).

En pratique, vous ne devez pas avoir grand chose à changer pour permettre à un processus d'appeler lui-même la fonction de création d'un processus (sous réserve qu'elle soit implantée proprement). Cette fonction devra ré-utiliser une case de la table des processus actuellement allouée à un processus MORT, en vérifiant bien toujours qu'on ne dépasse pas le nombre maximum de processus dans le système.

A vous de créer des tests significatifs pour vérifier que la terminaison et la re-création des processus se passent bien.

Pour aller plus loin

Si vous avez fini en avance, vous pouvez étendre votre système comme il vous plaira. A titre d'exercice, vous pouvez travailler sur la version ISI du projet (mais attention : le jour de l'examen de TP, les questions porteront sur la version ISSC et SLE).