Changes between Version 45 and Version 46 of AS6-TME-B5


Ignore:
Timestamp:
Feb 22, 2022, 7:17:01 PM (3 years ago)
Author:
franck
Comment:

--

Legend:

Unmodified
Added
Removed
Modified
  • AS6-TME-B5

    v45 v46  
    99La majorité des réponses aux questions sont dans le cours, 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 :-)
    1010Quand 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.
    11 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.
     11Dans 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.
    1212
    1313==
     
    119119'''
    120120 * C'est le **tick**. Un tick d'horloge est la durée entre deux IRQ du timer.
    121  * Dans l'état actuel du code, on fait une commutation de thread à chaque tick. Dans un système plus évolué, on a la notion de **quantum** qui correspond pas un nombre fini de tick, par exemple 1 quantum = 10 ticks. Ce quantum peut varier au cours du temps pour donner plus de temps à certains threads (au démarrage par exemple ou pour des tâches que l'on veut favoriser)
     121 * Dans l'état actuel du code, on fait une commutation de thread à chaque tick. Dans un système plus évolué, on a la notion de **quantum** qui correspond à un nombre fini de ticks, par exemple 1 quantum = 10 ticks. Ce quantum peut varier au cours du temps pour donner plus de temps à certains threads (au démarrage par exemple ou pour des tâches que l'on veut favoriser)
    122122'''
    123123}}}
     
    175175 * C'est la fonction `thread_load()` qui se charge du chargement de la restauration du contexte du thread entrant (nouvellement élu).
    176176 * Quand on sort de la fonction `thread_load()`, il y a en effet 2 cas:
    177    * Le thread entrant n'a jamais été élu. Dans ce cas, le `jr $31` va nous faire entrer dans la fonction `thread_bootstrap()` dont le but est lancer le thread en allant chercher les informations dans la structure `thread_s` du thread nouvellement élu, à savoir
     177   * Le thread entrant n'a jamais été élu. Dans ce cas, le `jr $31` va nous faire entrer dans la fonction `thread_bootstrap()` dont le but est de lancer le thread en allant chercher les informations dans la structure `thread_s` du thread nouvellement élu, à savoir
    178178     * la fonction de démarrage `_start()` ou `thread_start()`
    179179     * la fonction principale du thread (uniquement pour les threads standards, c'est inutile pour le thread `main()`, on sait que c'est `main()`)
     
    183183      * Pour qu'on ne rentre pas dans une boucle sans fin, la valeur de retour de `thread_save()` après une restauration de contexte est `0` (alors que c'est `1` après une sauvegarde). On teste donc cette valeur de retour de `thread_save()` pour savoir ce qu'on doit faire ensuite.
    184184      * On sort ensuite de `sched_switch()` et on revient dans `thread_yield()` (actuellement c'est le seul cas, mais nous verrons d'autres fonctions appelant `sched_switch())`.
    185       * Après, on revient dans un syscall ou dans l'ISR du timer, suivant l'événement qui avait abouti à la perte du processeur par le thread courrant.
     185      * Après, on revient dans un syscall ou dans l'ISR du timer, suivant l'événement qui avait abouti à la perte du processeur par le thread courant.
    186186   
    187187'''
     
    213213   * une dans l'espace utilisateur pour la pile et peut-être d'autres informations (que nous verrons plus tard)
    214214   * une dans l'espace noyau, pour le contexte du thread et ses propriétés.
    215  * Lors de la création d'un thread, il faudrait allouer deux structures dynamiquement, mais c'est impossible dans l'état actuel du code car nous n'avons pas d'allocateur de mémoire dynamique.
     215 * Lors de la création d'un thread, il faudrait allouer deux structures dynamiquement, mais c'est impossible dans l'état actuel du code, car nous n'avons pas d'allocateur de mémoire dynamique.
    216216 * Nous avons donc fait un choix simplificateur et une seule structure entièrement dans l'espace utilisateur. C'est un choix temporaire.
    217217'''
     
    294294'''
    295295 * Quand un thread rend le processeur, il le reprendra plus tard et reviendra précisément dans la fonction où il l'avait perdu (sauf si c'est une sortie définitive avec `thread_exit()` bien sûr).
    296  * Quand on entre dans une fonction C, on sait que l'on peut modifier les registres temporaires car ils ne contiennent rien pour la fonction appelante. S'ils contiennent quelque-chose d'important la fonction appelante doit sauver leur valeur avant d'entrer dans la fonction appelée.
     296 * Quand on entre dans une fonction C, on sait que l'on peut modifier les registres temporaires, car ils ne contiennent rien pour la fonction appelante. S'ils contiennent quelque chose d'important, la fonction appelante doit sauver leur valeur avant d'entrer dans la fonction appelée.
    297297 * Par contre, la fonction appelante suppose que les registres persistants conservent leur valeur, c.-à-d. qu'ils ne sont pas modifiés par la fonction appelée.
    298  * C'est vrai pour toutes les fonctions, c'est donc vrai aussi pour la fonction `thread_save()`. Elle peut modifier les registres temporaires mais pas les registres persistants.
     298 * C'est vrai pour toutes les fonctions, c'est donc vrai aussi pour la fonction `thread_save()`. Elle peut modifier les registres temporaires, mais pas les registres persistants.
    299299 * C'est donc seul les registres persistants qu'elle sauve et qui seront restaurés par la fonction `thread_load()` qui sortira de `thread_save()` sans modification des registres persistants.
    300300'''
     
    342342'''
    343343 * Quend on sort de `thread_load()`, on entre dans la fonction `_start()` (après un passage par `thread_bootstrap()` et `thread_launch()`). Or on ne sort jamais de `_start()`, on sort de l'application avec `exit()`.
    344  * On n'exécute rien après `thread_load()` mais si ça devait se produire alors c'est un kernel panic !
    345 '''
    346 }}}
    347 1. Dans quelle pile s'exécute la fonction `kinit()` ? dans quelle section est-elle ? Pourquoi elle n'est que temporaire ?
     344 * On n'exécute rien après `thread_load()`, mais si ça devait se produire alors c'est un kernel panic !
     345'''
     346}}}
     3471. Dans quelle pile s'exécute la fonction `kinit()` ? Dans quelle section est-elle ? Pourquoi n'est-elle que temporaire ?
    348348{{{#!protected ------------------------------------------------------------------
    349349'''
    350350 * `kinit()` utilise une pile temporaire en haut du segment `.kdata`. Dès qu'on entre dans une application, on utilise la pile de l'application et on ne revient plus jamais sur la pile de `kinit()`.
    351  * Dans le cas général, il y a toujours une application et un thread en cours, et le processeur utilise la pile courrante.
    352  * Dans la version actuelle du code, il n'y a qu'une pile par thread, utilisée à la fois par les fonctions utilisateur et par le kernel lors des syscall ou des ISR, mais bientôt, nous aurons 2 piles par thread, une pour le code utilisateur et une pour le code noyau.
     351 * Dans le cas général, il y a toujours une application et un thread en cours, et le processeur utilise la pile courante.
     352 * Dans la version actuelle du code, il n'y a qu'une pile par thread qui est utilisée à la fois par les fonctions utilisateur et par le kernel lors des syscall ou des ISR, mais bientôt, nous aurons 2 piles par thread, une pour le code utilisateur et une pour le code noyau.
    353353'''
    354354}}}
     
    356356{{{#!protected ------------------------------------------------------------------
    357357'''
    358  * Non, ça ne sert à rien, le contexte restauré ne contient rien dans ces registres, **mais** quand appelle `thread_load()`, on ne veut pas savoir si c'est pour la première fois pour ce thread ou si c'est une vraie restauration. C'est le `jr $31` qui retournera dans `sched_switch()` ou ira dans `thread_launch()`. Alors, on accepte de restaurer des registres inutilement, c'est juste la première fois.
     358 * Non, ça ne sert à rien, le contexte restauré ne contient rien dans ces registres, **,mais** quand appelle `thread_load()`, on ne veut pas savoir si c'est pour la première fois pour ce thread ou si c'est une vraie restauration. C'est le `jr $31` qui retournera dans `sched_switch()` ou ira dans `thread_launch()`. Alors, on accepte de restaurer des registres inutilement, c'est juste la première fois.
    359359'''
    360360}}}
     
    370370 * Deux possibilités :
    371371   1. Soit le noyau abandonne le service et rend une erreur au thread pour l'informer que le service ne peut pas être rendu, et donc que le thread doit re-tenter sa chance plus tard ou faire autre chose. Pour l'analogie, vous dites à votre client de partir et de, s'il veut, revenir plus tard ou pas.
    372    2. Soit le noyau demande un changement de thread avec `thread_yield()` pour faire quelque-chose d'utile pour un autre thread. Pour l'analogie, vous dite à votre client d'attendre et vous allez faire autre chose. Le client attend et tente de rentrer dès qu'un autre client sort du restaurant. Notez que dans cette analogie, il n'y a pas de file d'attente, le dernier client arrivé sera peut-être le premier servi.
     372   2. Soit le noyau demande un changement de thread avec `thread_yield()` pour faire quelque chose d'utile pour un autre thread. Pour l'analogie, vous dites à votre client d'attendre et vous allez faire autre chose. Le client attend et tente de rentrer dès qu'un autre client sort du restaurant. Notez que dans cette analogie, il n'y a pas de file d'attente, le dernier client arrivé sera peut-être le premier servi.
    373373 * Dans la version actuelle du code, le thread qui attend la ressource reste dans l'état READY et donc il sera élu par l'ordonnanceur quand son tour viendra pour tester à nouveau la ressource et la prendre si elle est disponible, sinon il subit à nouveau un `thread_yield()`.
    374374 * Ce comportement sera modifié en introduisant un état `WAIT` pour les threads afin de l'ordonnanceur ne donne pas le processeur à un thread dont la ressource attendue n'est pas disponible.
     
    382382
    383383
    384 Pour la partie pratique, je ne vous fait pas modifier, ou pire écrire, la gestion des threads, mais je vous invite à lire le code. Vous allez changer la manière de lire les caractères du TTY pour la rendre plus efficace. Tous les changements seront faits dans le fichier `kernel/harch.c`.
     384Pour la partie pratique de la séance, je ne vous fais pas modifier, ou pire écrire, la gestion des threads, mais je vous invite à lire le code. Vous allez changer la manière de lire les caractères du TTY pour la rendre plus efficace. Tous les changements seront faits dans le fichier `kernel/harch.c`.
    385385
    386386== Etat du code par une lecture directe
    387387
    388 Le code proposé fonctionne mais il a un problème que nous allons résoudre. Pour illustrer ce problème, nous allons partir d'un exemple.
     388Le code proposé fonctionne, mais il a un problème que nous allons résoudre. Pour illustrer ce problème, nous allons partir d'un exemple.
    389389
    390390**uapp/main.c**
     
    543543* **`T0`** appelle tty_read() qui cède le processeur à **`T1`** en l'absence de frappes.
    544544* Le thread **`T0`** demande des lectures à chaque qu'il a le processeur, **`T1`** prend le temps qui lui est donné jusqu'à l'IRQ du TIMER.
    545 * Si l'utilisateur frappe beaucoup de touches pendant que **`T0`** n'a pas le processeur. Le caractères lus doivent être stockés quelque-part dans le contrôleur de TTY pour ne pas les perdre. Mais si cette mémoire est trop petite, on risque de perdre des caractères.
     545* Si l'utilisateur frappe beaucoup de touches pendant que **`T0`** n'a pas le processeur. Les caractères lus doivent être stockés quelque part dans le contrôleur de TTY pour ne pas les perdre. Mais si cette mémoire est trop petite, on risque de perdre des caractères.
    546546
    547547 [[Image(htdocs:img/IRQTTY_1.png,nolink,width=600)]]
    548548
    549 L'idée va être d'utiliser l'IRQ du TTY pour réagir à chaque frappe du clavier pendant l'exécution de **`T1`** pour lire le clavier et stocker les caractères dans une file d'attente. Sur le schéma ci-dessous est représentée l'exécution de l'isr du TTY qui vole des cycle à **`T1`** pour lire le caractère reçu par le contrôleur de TTY.
     549L'idée va être d'utiliser l'IRQ du TTY pour réagir à chaque frappe du clavier pendant l'exécution de **`T1`** pour lire le clavier et stocker les caractères dans une file d'attente. Sur le schéma ci-dessous est représentée l'exécution de l'isr du TTY qui vole des cycles à **`T1`** pour lire le caractère reçu par le contrôleur de TTY.
    550550
    551551 [[Image(htdocs:img/IRQTTY_2.png,nolink,width=600)]]
     
    553553=== Mise en place d'une FIFO entre l'isr du TTY et la fonction `tty_read()`
    554554
    555 Le caractère lu est mis dans une structure FIFO (First In First Out). Le schéma ci-dessous illustre le fonctionnement de la FIFO. Une fifo simple a un écrivain et un lecteur. L'écrivain écrit des données avec une commande `push()` tant que la FIFO n'est pas pleine. Si elle elle pleine, il y a deux comportements possibles : l'écrivain attend de la place ou l'écrivain jette la donnée, ça dépend ce qu'on veut. Ici, on jettera, parce qu'on n'a pas le moyen de ralentir le flux de données (les frappes du clavier). Le lecteur lit les données avec `pull()` tant que la FIFO n'est pas vide.
     555Le caractère lu est mis dans une structure FIFO (First In First Out). Le schéma ci-dessous illustre le fonctionnement de la FIFO. Une fifo simple a un écrivain et un lecteur. L'écrivain écrit des données avec une commande `push()` tant que la FIFO n'est pas pleine. Si, il y a deux comportements possibles : l'écrivain attend de la place ou alors l'écrivain jette la donnée, ça dépend de ce qu'on veut. Ici, on jettera, parce qu'on n'a pas le moyen de ralentir le flux de données (les frappes du clavier). Le lecteur lit les données avec `pull()` tant que la FIFO n'est pas vide.
    556556
    557557 [[Image(htdocs:img/FIFO_TTY.png,nolink,width=400)]]
     
    602602
    603603Les schémas ci-dessous le comportement de la FIFO.
    604 1. A l'initialisation comme la structure est dans les variables globales, les pointeur `pt_read` et `pt_write` sont à 0. La fifo est vide puisque `pt_read == pt_write`.
    605 1. On écrit `A` et on incrémente `pt_write`, `pt_read` ne bouge pas puisque l'on de lit pas.
     6041. A l'initialisation comme la structure est dans les variables globales, les pointeurs `pt_read` et `pt_write` sont à 0. La fifo est vide puisque `pt_read == pt_write`.
     6051. On écrit `A` et on incrémente `pt_write`, `pt_read` ne bouge pas puisque l'on ne lit pas.
    6066061. On écrit `B` et `C`.
    6076071. On lit `A` et on incrémente `pt_read`, on peut lire parce que `pt_read != pt_write` et donc la FIFO n'est pas vide
    6086081. On écrit `D` et `E`. Lors de l'incrément de `pt_write` on revient à 0 à cause du modulo `size`
    609 1. On écrit `F` et ce sera fini parce la fifo est pleine `(pt_write + 1)%size == pt_read`, si on veut écrire à nouveau, il faut lire.
     6091. On écrit `F` et ce sera fini parce que la fifo est pleine `(pt_write + 1)%size == pt_read`, si on veut écrire à nouveau, il faut lire.
    610610
    611611 [[Image(htdocs:img/FIFO.png,nolink,width=600)]]
    612 
    613 {{{#!protected
    614 {{{#!c
    615 static int tty_fifo_push (struct tty_fifo_s *fifo, int c)
    616 {
    617     int pt_write_next = (fifo->pt_write + 1) % sizeof(fifo->data);
    618     if (pt_write_next != fifo->pt_read) {
    619         fifo->data [fifo->pt_write] = c;
    620         fifo->pt_write = pt_write_next;
    621         return 1;
    622     }
    623     return 0;
    624 }
    625 }}}
    626 }}}
    627612
    628613=== Utilisation de la FIFO
     
    637622
    638623{{{#!protected
    639 Pour la correction, je vous met le code sans le ranger dans des fonctions, histoire de vous aider, mais pas trop...
     624Pour la correction, je vous mets le code sans le ranger dans des fonctions, afin de vous aider, mais pas trop...
    640625
    641626{{{#!c