4MMPS Yield

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

Dans cette étape du mini-projet, les processus vont se passer la main explicitement (dans les parties suivantes, le changement de processus sera déclenché par l'interruption de l'horloge, comme dans les systèmes classiques). Pour garantir qu'il n'y a pas « d'interférence » (et pour permettre le blocage du système), vous devez désactiver les interruptions. Le plus simple pour cela est d'enlever l'appel à la fonction sti() que vous aviez ajouté à la fin de votre fonction kernel_start.

Pour commencer, on va mettre au point le mécanisme de passage d'un processus à l'autre. Dans le système réalisé, il y aura donc seulement 2 processus :

  • le processus idle (qui a par convention le pid 0) est le processus par défaut : en effet, dans un système, il doit toujours y avoir au moins un processus actif, car le système ne doit jamais s'arrêter ;
  • le processus proc1 (de pid 1) qui représentera un processus de calcul quelconque.

Passage d'un processus à l'autre

Dans une première étape, on va implanter simplement le passage du processus idle au processus proc1. Plus précisément, le code de ces processus sera :

void idle(void)
{
    printf("[idle] je tente de passer la main a proc1...\n");
    ctx_sw(..., ...);
}

void proc1(void)
{
    printf("[proc1] idle m'a donne la main\n");
    printf("[proc1] j'arrete le systeme\n");
    hlt();
}

Vous devrez donc voir apparaitre à l'écran la trace de idle suivi des deux traces de proc1, puis le système se bloquera grâce au hlt.

Pour implanter ce petit test, vous aurez besoin de définir la structure des processus décrite plus haut. Précisément, le type structure de processus que vous devez définir comprendra :

  • le pid du processus, sous la forme d'un entier (signé, car traditionnellement la fonction de création d'un processus renvoie -1 en cas d'erreur) ;
  • la zone de sauvegarde des registres du processeur : il s'agira tout simplement d'un tableau d'entiers, puisqu'on manipule des registres 32 bits (note : réfléchissez pour trouver quels registres généraux doivent être sauvegardés) ;
  • la pile d'exécution du processus, qui sera définie comme un tableau d'entiers d'une taille fixée par une constante, par exemple 512.

Ensuite, il faut définir la table des processus, c'est à dire tout simplement un tableau de structure de processus, dont la taille est là encore une constante prédéfinie dans le système. Dans cette première étape, cette constante sera fixée à 2.

Lors du démarrage du système, il faut initialiser les structures de processus de idle et proc1 avec des valeurs pertinentes, c'est à dire :

  • pour les deux processus, son pid ;
  • pour proc1 la case de la zone de sauvegarde des registres correspondant à %esp doit pointer sur le sommet de pile, et la case en sommet de pile doit contenir l'adresse de la fonction proc1 : c'est nécessaire comme expliqué plus haut pour gérer la première exécution de proc1.

Il n'est pas nécessaire d'initialiser la pile d'exécution du processus idle : en fait, ce processus n'utilisera pas la pile allouée dans sa structure de processus mais directement la pile du noyau (i.e. celle qui est utilisée par la fonction kernel_start notamment). De même, il n'est pas nécessaire d'initialiser la zone de sauvegarde de %esp pour idle puisque ce processus sera exécuté directement par la fonction kernel_start, qui devrait donc ressembler au squelette ci-dessous :

void kernel_start(void)
{
    // initialisation des structures de processus
    ...
    // demarrage du processus par defaut
    idle();
}

Enfin, il faut implanter la fonction de changement de contexte void ctx_sw(int *, int *) qui sera appelée par le processus idle pour passer la main à proc1. Dans ce premier exercice, on va se contenter d'écrire la moitié la plus simple de cette fonction, à savoir la restauration du contexte de proc1 à partir de sa zone de sauvegarde.

La fonction de changement de contexte prend en paramètre les adresses des zones de sauvegarde des registres du processus à arrêter (premier paramètre) et du processus à démarrer (deuxième paramètre). Cette fonction doit être écrite en assembleur, dans un fichier .S, puisqu'on va manipuler directement les registres du processeur. On donne ci-dessous le squelette du fichier en question :

    .text
    .globl ctx_sw
// Structure de la pile en entree :
//   %esp + 4 : adresse de l'ancien contexte
//   %esp + 8 : adresse du nouveau contexte
ctx_sw:
    // A COMPLETER !
    // Sauvegarde de l'ancien contexte (a faire plus tard)

    // Restauration du nouveau contexte (a faire maintenant)

    // Activation du nouveau processus :
    ret

On note que ctx_sw est une fonction un peu particulière, car on ne met pas en place de cadre de pile : en effet, il ne faut justement pas modifier les pointeurs de pile que l'on veut sauvegarder.

On peut maintenant comprendre plus précisément le fonctionnement de l'initialisation de la pile de proc1 que l'on a fait plus haut : lors de la restauration des registres de proc1, la valeur de %esp restaurée sera un pointeur sur le sommet de la pile de proc1, qui contient l'adresse de la fonction proc1. Lorsqu'on arrivera sur le ret à la fin du changement de contexte, c'est donc cette adresse qui sera dépilée et à laquelle le flot d'exécution sautera.

Aller-retour entre les processus

Une fois que le test précédent fonctionne correctement, on va l'étendre pour faire revenir l'exécution à idle après être passé par proc1 (et pour se convaincre que cela marche vraiment, on va faire l'aller-retour 3 fois...). Le code des processus sera donc maintenant :

void idle(void)
{
    for (int i = 0; i < 3; i++) {
        printf("[idle] je tente de passer la main a proc1...\n");
        ctx_sw(..., ...);
        printf("[idle] proc1 m'a redonne la main\n");
    }
    printf("[idle] je bloque le systeme\n");
    hlt();
}

void proc1(void)
{
    for (;;) {
        printf("[proc1] idle m'a donne la main\n");
        printf("[proc1] je tente de lui la redonner...\n");
        ctx_sw(..., ...);
    }
}

Pour que cela fonctionne, il faut compléter la fonction ctx_sw pour implanter la partie sauvegarde des registres : cette partie est très symétrique avec la restauration déjà écrite.

Remarque : comme on l'a suggéré plus haut, il n'est pas nécessaire de sauvegarder et restaurer les registres scratch (%eax, %edx, %ecx et %eflags) car le compilateur ne laisse aucune valeur importante dedans lors de l'appel à une fonction. Lorsqu'on branchera le changement de contexte sur l'interruption horloge par contre, il est possible que ces registres contiennent des valeurs critiques (par exemple : si on a été interrompu entre une instruction cmp et un branchement conditionnel, les flags sont importants !), mais dans ce cas, %eax, %edx et %ecx seront sauvegardés par le traitant d'interruption et les flags le sont automatiquement lors du passage en mode interruption : le changement de contexte n'a donc jamais besoin de s'en préoccuper.