wiki:AS6-TME-B5

Version 33 (modified by franck, 3 years ago) (diff)

--

Gestion des Threads

Rappels de cours

A. Questions

La majorité des réponses aux questions sont dans le cours ou dans le rappel du cours donné au début de cette page, c'est voulu. Les questions suivent à peu près l'ordre du cours, elles sont simples, mais vous avez besoin de comprendre le cours pour y répondre :-) Quand une question vous demande si quelque chose est vrai ou faux, ne répondez pas juste "oui" ou "non », mais justifiez vos réponses avec une petite phrase. Le but de ces questions est d'évaluer vos connaissances, donc plus vous êtes précis, mieux c'est. Vous avez un corrigé que vous devez consulter pour vous autocorriger, mais pour qu'il soit utile, lisez-le après avoir cherché vous-même les réponses. Dans certains cas, ce ne sera pas simple, mais tentez quand même une réponse, même si vous savez que c'est faux car ce sera plus simple de comprendre la réponse.

A.1. Questions générales

  1. Dites-en une phrase ce qu'est un processus informatique (selon Wikipédia)
  2. Est-ce qu'un processus utilisateur s'exécute toujours dans le mode utilisateur du MIPS ?
  3. Nous avons vu qu'un processus utilisateur peut faire des appels système, c'est-à-dire demander des services au noyau du système d'exploitation. Est-ce qu'un processus peut faire des interruptions et des exceptions ?
  4. Un processus dispose d'un espace d'adressage pour s'exécuter, qu'y met-il ?
  5. Dans un fichier exécutable, avant qu'il ne soit chargé en mémoire, on trouve le code du programme et les données globales. Est-ce qu'il y a aussi les piles d'exécution des threads ? Justifiez votre réponse.
  6. Un thread de processus informatique représente une exécution de ce processus. Il est défini par une pile d'exécution pour ses fonctions, un état des registres du processeur et des propriétés comme un état d'exécution (RUNNING, READY, DEAD, et d'autres que nous verront plus tard). Combien de threads a-t-on par processus au minimum et au maximum ?
  7. Tous les threads d'un processus se partagent le même espace d'adressage, et donc le même code, les mêmes variables globales, les mêmes variables dynamiques (nous les verrons dans un prochain cours). Est-ce qu'ils se partagent aussi les piles ?
  8. Lorsque l'on crée un nouveau thread (un nouveau fil d'exécution du processus), il faut indiquer sa fonction principale, c'est-à-dire la fonction par laquelle qu'il doit exécuter. Est-ce que le nouveau thread pourra appeler d'autres fonctions ?
  9. Est-ce qu'on peut créer deux threads avec la même fonction principale ?
  10. Combien d'arguments la fonction principale d'un thread peut-elle prendre et de quel type ?
  11. Que se passe-t-il lorsqu'on sort de la fonction principale d"un thread ?
  12. L'exécution en temps partagé est un mécanisme permettant d'exécuter plusieurs threads à tour de rôle sur le même processeur. Comment s'appelle le service du noyau chargé du changement de thread ?
  13. La phase de changement de thread a une certaine durée, c'est un temps perdu du point de vue de l'application. Comment nomme-t-on cette phase pour indiquer que c'est un temps perdu ?
  14. Pour l'exécution en temps partagé, le noyau applique une politique, laquelle définit l'ordre d'exécution. Si les threads sont toujours prêts à être exécutés et que le noyau les exécute à tour de rôle de manière équitable, comment se nomme cette politique ?
  15. Dans cette politique équitable, quelle est la fréquence type de changement de thread ? Donnez une justification.
  16. Comment nomme-t-on la durée entre deux interruptions d'horloge ? Ici, c'est le temps d'une instance d'exécution d'un thread.
  17. Le mécanisme de changement de thread (dont vous avez donné le nom précédemment) se déroule en 3 étapes, quelle que soit la politique suivie. Quelles sont ces étapes ?
  18. Comment se nomme la fonction qui provoque la perte du processeur par le thread en cours au profit d'un nouveau thread ?
  19. Qu'est-ce qui provoque un changement de thread sans que le thread n'en fasse lui-même la demande ?
  20. Dans le mécanisme de changement de thread, l'une des étapes est la sauvegarde du contexte, est-ce la même chose qu'un contexte de fonction ? Dites de quoi il est composé.
  21. Où est sauvé le contexte d'un thread ? Que pouvez-vous dire de la fonction de sauvegarde ? (langage, prototype, valeur de retour, etc.)
  22. Chaque thread dispose de sa propre pile d'exécution, doit-on aussi sauver la pile lors des changements de thread ?
  23. Après qu'un thread a été élu et que son contexte a été chargé dans le processeur, donnez le nom de la fonction responsable du chargement et dites où elle retourne ? (attention, il y a deux cas)

A.2. Questions sur l'implémentation

  1. Quelles sont les fonctions de l'API utilisateur des threads et les états de threads ? Indiquer les changements d'état provoqué par l'appel des fonctions de cette API. Regardez les transparents pour répondre.
  2. La structure thread_s rassemble les propriétés du thread, sa pile et le tableau de sauvegarde de son contexte. Cette structure est, dans l'état actuel du code` entièrement dans dans le segment des données globales de l'application. Pouvez-vous justifier cette situation et en discuter ?
  3. Le tableau de sauvegarde du contexte d'un thread est initialisé avec des valeurs qui seront chargées dans les registres du processeur au premier chargement du thread. Tous les registres n'ont pas besoin d'être initialisés avec une valeur. Seuls les registres $c0_sr ($12 du coprocesseur système) , $sp ($29 des GPR) et $ra ($31 des GPR) ont besoin d'avoir une valeur choisie. Pourquoi ?
  4. $c0_sr est initialisé avec 0x413, dite pourquoi.
  5. La fonction sched_switch() appelle d'abord l'électeur de thread qui choisit le thread entrant (qui gagne le processeur), puis sched_switch() sauve le contexte du thread sortant (qui perd le processeur) et charge le contexte du thread entrant, enfin sched_switch() change l'état du thread entrant à RUNNING. sched_switch() est appelée par thread_yield(). Pouvez-vous expliquer pourquoi avoir créé sched_switch() ? Ce n'est pas évident au premier abord, mais il y a une raison.
    void sched_switch (void) { //
      int th_curr = thread_current_idx;                        // n° du thread courant dans thread_tab
      int th_next = sched_elect ();                            // demande le numéro du prochain thread
      if (th_next != th_curr) {                                // Si c'est le même thread, ne rien faire !
        if (thread_save (thread_tab[th_curr]->context)) {      // sauve le ctx du thread sortant et rend 1
          thread_current_idx = th_next;                        // mise à jour de thread_current_idx
          thread_load (thread_tab[th_next]->context);          // chargement de contexte & sortie par jr $31
        }                                                      // donc de thread_save(), mais qui rend 0
      }
      thread_tab[thread_current_idx]->state= TH_STATE_RUNNING; // the thread choisi est dans l'état
    }
    int thread_yield (void) {
      thread_tab[thread_current_idx]->state = TH_STATE_READY;  // état futur du thread sortant 
      sched_switch ();                                         // changement de threads (ou pas)
      return 0;
    
  6. Quand un thread est élu pour la première fois, à la sortie de thread_load(), on appelle la fonction thread_bootstrap(). Retrouvez dans les transparents du cours les étapes qui vont mener à l'exécution de la fonction principale du thread élu, et expliquez-les.
  7. Un thread peut perdre le processeur pour 3 raisons (dans la version actuelle du code), quelles sont ces raisons ?
  8. Quand un thread TS perd le processeur pour une raison X à la date T, il entre dans le noyau par kentry, puis il y a une séquence d'appel de fonction jusqu'à la fonction thread_load() du thread entrant TE. Lorsqu'on sort de ce thread_load(), on est dans le nouveau thread TE. Plus tard, le thread TS sera élu à son tour et gagnera à nouveau le processeur en sortant lui aussi d'un thread_load(). En conséquence, on sortira de la séquence des appels qu'il y avait eu à la date T.
    Expliquez, en vous appuyant sur la description du comportement précédent, pourquoi on ne sauve pas les registres temporaires dans le contexte des threads.
  9. Dans le cours, nous suivons l'exécution du code au démarrage (vers le slide 37), nous pouvons voir que la fonction kinit() fait 3 choses importantes : (1) initialiser à 0 la section BSS (contenant les variables globales non explicitement initialisées dans le programme), (2) demander à l'architecture de s'initialiser et (3) lancer la première (et ici seule) application. Où sont définis les symboles __bss_origin, __bss_end, __main_thread, _start et quel est leur type ?
    void kinit (void)
    {
        kprintf (banner);
    // 1 
        extern int __bss_origin, __bss_end;                 
        for (int *a = &__bss_origin; a != &__bss_end; *a++ = 0);
    
    // 2 
        arch_init(20000);                                         // init architecture ; arg=tick
    
    // 3
        extern thread_t _main_thread;                             // thread struct pour main()
        extern int _start;                                        // _start() point d'entrée app.
        thread_create_kernel (&_main_thread, 0, 0, (int)&_start);
        thread_load (_main_thread.context);
     
        kpanic();
    }
    
  10. Dites ce que sont les arguments 2 et 3 de thread_create_kernel() dans le code de kinit() et pourquoi, ici, on les met à 0 ?
  11. Dans la fonction kinit(), que se passe-t-il quand on sort de thread_load()et pourquoi avoir mis l'appel à kpanic() ?
  12. Dans quelle pile s'exécute la fonction kinit() ? dans quelle section est-elle ? Pourquoi elle n'est que temporaire ?
  13. Pour le chargement de thread main() avec thread_load (_main_thread.context), on initialise les registres $16 à $23, $30, $c0_EPC, est-utile ? Si oui pourquoi ? Sinon, pourquoi faire ces initialisations ?
  14. Dans le deuxième TME2, vous avez dû modifier le code syscall_handler (gestionnaire de syscalls) pour le rendre interruptible. En effet, lorsque l'application demande un service au noyau, mais que le noyau ne peut pas le rendre immédiatement (comme la lecture d'une touche du clavier), si vous restez bloqué dans l'appel système en attendant la donnée et que les interruptions sont masquées, alors le noyau ne peut pas gérer les IRQ (pour le TME 2, il ne pouvait pas gérer l'IRQ du timer pour gérer le dépassement du temps de jeu).

B. Travaux pratiques