wiki:MultiCourseTP6

Cours "Architecture des Systèmes Multi-Processeurs"

TP6 : Interruptions vectorisées / communication avec les périphériques

(franck.wajsburt@…)

A. Objectifs

Le but de ce TP est d'analyser les mécanismes de communication par interruptions entre les périphériques et le système d'exploitation. Dans une première partie de ce TP, on illustre sur une architecture bi-processeurs le mécanisme des interruptions vectorisées en utilisant un Timer programmable, capable de générer des interrutions périodiques. Dans une seconde partie, on analyse en détail le mécanisme permettant à un programme de lire des catactères à partir d'un terminal TTY.

On dit qu'un périphérique est « mappé » en mémoire lorsqu'il possède des registres adressables par le logiciel (au moyen d'instructions de lecture ou d'écriture du type lw ou sw).

  • Les registres accessibles en écriture permettent au système d'exploitation de configurer les périphériques, ou de leur envoyer des commandes.
  • Les registres accessibles en lecture permettent au système d'exploitation d'obtenir des informations sur l'état du périphérique.

Pour communiquer avec le système d'exploitation, les périphériques utilisent des interruptions : Une interruption, ou IRQ (Interrupt ReQuest) est un signal Booleen actif à l'état haut, qui permet à un périphérique de "voler " quelques cycles à un processeur pour exécuter une ISR (Interrupt Service Routine). Ces ISR ont généralement pour rôle d'écrire dans des tampons mémoire appartenant au système d'exploitation. Ces tampons de communication sont spécifiques pour chaque type de périphérique.

L'architecture matérielle est identique à l'architecture matérielle définie pour le TP5, mais on utilisera seulement deux processeurs.

B. Architecture matérielle

L'archive attachment:multi_tp6.tgz contient les fichiers dont vous aurez besoin. Créez un répertoire de travail tp6, et copiez dans ce répertoire les fichiers tp5_top.cpp, et tp5.desc que vous avez déjà utilisés. Décompressez l'archive et copiez dans tp6 le répertoire soft contenant les deux applications logicielles utilisées dans ce TP: fichiers main_prime.c et main_pgcd.c.

C. Composants périphériques

Le composant matériel PibusIcu, est un concentrateur d'interruptions vectorisée. Ce composant est souvent appelé PIC (Programmable Interrupt Controller) dans les PCs. Le concentrateur d'interruptions utilisé ici peut concentrer jusque 32 lignes d'interruptions IRQ_IN[i] en entrée (provenant de différents périphériques), vers une seule ligne d'interruption en sortie IRQ_OUT (connectée à un processeur). Il contient un grand "OU" cablé: Il suffit qu'une seule entrée IRQ_IN[i] soit active (état haut) et non masquée, pour que IRQ_OUT passe à l'état haut.

Le composant ICU utilisé contient un encodeur de priorité qui implémente un mécanisme de priorité fixe : si plusieurs lignes d'interruption entrantes IRQ_IN[i] sont actives simultanément, le registre adressable IT_VECTOR contient l'index de l'interruption active qui a l'index le plus petit. Enfin, ce composant permet au logiciel de masquer individuellement chacune des 32 lignes d'interruption entrantes, en écrivant un mot de 32 bits dans le registre IT_MASK du composant PibusIcu.

Le composant PibusIcu est un périphérique multi-canaux: Quand il y a plusieurs processeurs P[k], le contrôleur d'interruptions possède autant de sorties IRQ_OUT[k] qu'il y a de processeurs P[k]. Chaque canal [k] correspond à une sortie IRQ_OUT[k], et se comporte comme un concentrateur d'interruptions indépendant. La seule chose partagée par les différents canaux sont les 32 signaux entrants IRQ_IN[i]. S'il y a plusieurs processeurs, le composant PibusIcu contient un registre de masque spécifique pour chaque canal, ce qui permet au système d'exploitation de décider, pour chaque interruption IRQ_IN[i], à quel processeur elle va être transmise.

Le composant matériel PibusMultiTimer est également un périphérique multi-canaux. Il contient plusieurs timers programmables. Chaque timer a pour fonction de générer des interruptions périodiques, programmables par logiciel. Chaque timer possède sa propre ligne d'interruption. Le code exécuté en cas d'interruption générée par le timer est défini par la routine de traitement de l'interruption _isr_timer (ISR signifie Interrupt Service Routine).

Le composant matériel PibusMultiTty a déjà été utilisé dans les TPs précédents. Il contrôle plusieurs terminaux TTY indépendants. Chaque terminal possède une ligne d'interruption qui lui permet de signaler qu'un caractère a été saisi au clavier. Cette interruption peut être utilisée par le système, lorsqu'on ne souhaite pas utiliser un mécanisme de scrutation pour acquérir les caractères du clavier. Le code exécuté en cas d'interruption générée par le TTY est défini par la routine de traitement de l'interruption _isr_tty_get.

Lisez la spécification fonctionnelle des composants PibusMultiTty, PibusMultiTimer et PibusIcu que vous trouverez dans l'en-tête des fichiers pibus_multi_tty.h, pibus_multi_timer.h et pibus_icu.h.

Question C1 : Pourquoi le composant PibusMultiTimer est-t-il une cible, et pas un maître sur le bus ? Quelle est la signification de l'argument ntimer du constructeur ? Quels sont les registres adressables de ce composant, quelles sont leurs adresses, et quelle est la fonctionnalité de chacun d'entre eux ?

Question C2 : Pourquoi le composant PibusIcu est-il une cible sur le bus ? Quelle est la signification de l'argument nirq du constructeur ? Quelle est la signification de l'argument nproc du constructeur ? Dans une architecture multi-processeurs, comment le logiciel peut-il aiguiller la ligne d'interruption connectée à l'entrée IRQ_IN[i] du composant ICU vers le processeur connecté à la sortie IRQ_OUT[j] du composant ICU ? Pour chaque port de sortie IRQ_OUT[i], le composant ICU contient plusieurs registres adressables. Quels sont ces registres ? Quelles sont leurs adresses ? Quelle est la fonctionnalité de chacun d'entre eux ?

Question C3 : Pourquoi l'adresse de base du segment associé au composant PibusIcu doit-elle être alignée sur un multiple de 32*8 octets ? Quel serait le coût matériel de relâcher cette contrainte ?

L'architecture générique définie dans le fichier tp5_top.cpp peut instancier de 1 à 8 processeurs. Quand il y a plusieurs processeurs, chaque processeur possède son propre terminal TTY et son propre timer. Pour une architecture contenant 2 processeurs, on aura donc 2 IRQs provenant du contrôleur TTY, et 2 IRQs provenant du contrôleur de timers.

Question C4 : En analysant le contenu du fichier tp5_top.cpp, précisez comment ces 4 lignes d'interruption sont connectées sur les ports IRQ_IN[i] du contrôleur ICU.

On utilisera des caches 4 fois associatifs ayant une capacités de 4 Koctets et des lignes de cache de 32 octets (8 mots). Modifiez dans le fichier tp5_top.cpp les valeurs par défaut des paramètres des caches, et générez l'exécutable de simulation simul.x.

D. Lancement des tâches

Dans le TP5, tous les processeurs exécutaient le même programme, sur des données différentes. Dans ce TP6, les deux processeurs vont exécuter des programmes différents:

  • Le fichier main_prime.c contient un premier programme qui calcule les 1000 premiers nombres premiers et les affiche sur le terminal TTY. Ce programme sera exécuté par le processeur 0.
  • Le fichier main_pgcd.c contient un programme interactif qui calcule le PGCD (Plus Grand Commun Diviseur) de deux nombres entiers X et Y saisis au clavier (sous forme de chaînes de caractères décimaux), et affiche le résultat sur l'écran du TTY. Ce programme sera exécuté par le processeur 1.

Sur une machine équipée d'un vrai système d'exploitation tel que LINUX ou WINDOWS, de nouvelles applications peuvent être lancées par l'OS sans qu'il soit nécessaire de redémarrer la machine. Il suffit à l'utilisateur de transmettre une commande au système par l'intermédiaire d'un shell. Puisque le GIET ne supporte pas la création dynamique des tâches, c'est le code de boot qui doit se charger de lancer l'exécution des programmes sur les deux processeurs: Les deux processeurs exécutent le même code de boot (rangé à l'adresse 0xBFC00000), mais ils se branchent à des adressent différentes dépendant du numéro du processeur. La convention imposée par le GIET est la suivante: Le segment seg_data doit commencer par une table de sauts, indexée par le numéro du processeur, et contenant les adresses des points d'entrée des différents programmes qui doivent être exécutés par les différents processeurs: L'entrée main[i] de cette table contient le point d'entrée (c'est à dire l'adresse de la première instruction) du programme qui sera exécuté par le processeur (i).

Question D1: Placez-vous dans le répertoire soft, et complétez le fichier reset.s, de façon à initialiser le pointeur de pile, le registre SR, et le registre EPC du processeur (1). Vous devez évidemment vous inspirer de ce qui est fait pour le processeur (0).

Générez les deux fichiers app.bin et sys.bin en utilisant le Makefile qui vous est fourni.

Question D2: Vérifiez dans le fichier app.bin.txt que le segment seg_data commence bien par une table contenant les adresses des deux fonctions main_prime() et main_pgcd(). Quelles sont ces deux addresses?

Question D3: Comment force-t-on GCC à construire cette table de sauts au début du segment seg_data ?

Placez-vous dans le répertoire tp6, et lancez l'exécution de ces deux programmes sur une architecture matérielle contenant deux processeurs en utilisant la commande:

> ./simul.x -NPROCS 2

Question D4 : Comment expliquez-vous que le programme de calcul du PGCD reste bloqué sur la saisie de l'opérande X ?

E. Activation du Timer

On veut maintenant activer les interruptions provenant du TIMER.

On rappelle qu'à chacune des lignes d'interruption est associée une routine d'interruption (ISR ou Interrupt Service Routine) qui est spécifique au périphérique qui a généré l'interruption, et qui est exécutée par le processeur lorsque les interruptions ne sont pas masquées. Activer les interruptions du TIMER est donc à équivalent à lancer sur chacun des 2 processeurs de l'architecture une tâche de fond, consistant à exécuter périodiquement l'ISR _isr_timer.

Question E1: Rappelez comment un processeur se branche à la routine ISR pertinente lorsqu'il reçoit une requête d'interruption. Analysez le code contenu dans les fichier giet.s et irq_handler.c, et décrivez la séquence d'appels de fonction entre le branchement à l'adresse 0x80000180 (point d'entrée dans le GIET) et le branchement à la routine _isr_timer.

Question E2: Que fait la routine d'interruption _isr_timer ?

On rappelle que le vecteur d'interruptions est un tableau en mémoire contenant les adresses des routines d'interruption associées aux différentes lignes d'interruption (IRQ) utilisées dans l'architecture. Ce tableau est indexé par le numéro de l'IRQ. Pour activer les interruptions, il faut faire pour chaque processeur (i), trois initialisations supplémentaires dans le code de boot :

  • Le processeur (i) doit initialiser toutes les entrées du vecteur d'interruptions qui lui sont destinées.
  • Le processeur (i) doit initialiser dans le le composant ICU le registre MASK[i] indiquant quelles interruptions entrantes (IRQ_IN[32]) doivent être routées vers le processeur (i).
  • Le processeur (i) doit initialiser le périphérique TIMER, en écrivant dans les registre PERIOD[i], puis dans le registre RUNNING[i].

Question E3: Complétez le fichier reset.s, pour initialiser le vecteur d'interruption avec les 2 entrées correspondant aux 2 IRQs associées au composant TIMER, en utilisant les résultats de la question C4. Le nom du tableau représentant le vecteur d'interruption ainsi que le nom de l'ISR associée au TIMER sont définis dans le fichier irq_handler.c.

Question E4: Complétez le fichier reset.s pour configurer le composant TIMER. On choisira une période de 50000 cycles pour le TIMER[0] et de 100000 cycles pour le TIMER[1]. La carte des registres du composant TIMER est définie dans l'en-tête du fichier pibus_multi_timer.h.

Question E5: Complétez le fichier reset.s pour initialiser le registre MASK[0] du composant ICU de façon à autoriser la transmission de l'IRQ provenant du TIMER[0] vers le processeur 0, et le registre MASK[1] de l'ICU pour autoriser la transmission de l'IRQ provenant du TIMER[1] vers le processeur 1. La carte des registres du composant ICU est définie dans l'en-tête du fichier pibus_icu.h.

Re-compilez le code système.

L'option -TRACE génère une trace d'exécution permettant d'analyser, cycle par cycle, le comportement du matériel. L'option -NCYCLES définit le nombre maximum de cycles exécutés. Pour obtenir une trace d'exécution entre le cycle from_cycle et le cycle to_cycle dans le fichier trace, il faut donc lancer la commande :

$ ./simul.x -NPROCS 2 -TRACE from_cycle -NCYCLES to_cycle > trace

On cherche dans les questions suivantes à évaluer le nombre de cycles nécessaires pour configurer les périphériques, ainsi que le nombre de cycles nécessaires pour traiter une interruption. Choisissez les bonnes valeurs pour les paramètres from_cycle et to_cycle, et lancez l'exécution en activant la trace.

Question E6: A quel cycle le processeur 0 écrit-il la première valeur dans le vecteur d'interruption ? pour répondre à cette question, il faut surveiller la valeur du signal sel_ram. A quel cycle le registre MASK[0] de l'ICU est-il configuré ? Pour répondre à cette question, il faut surveiller la valeur du signal sel_icu. A quel cycle le TIMER[0] est-il configuré ? Pour répondre à cette question, il faut surveiller les valeurs du signal sel_tim.

Question E7: A quel cycle le processeur 0 reçoit-il la première interruption du TIMER[0] ? A quel cycle l'interruption est-elle acquittée par l'ISR ? Pour répondre à ces questions, il faut surveiller le signal timer_irq[0].

Question E8: Décrivez en détail le traitement d'une interruption Timer par le processeur 0, entre l'activation du signal proc_irq[0] et le retour à l'exécution du programme interrompu. Il faut analyser la trace d'exécution, en vous appuyant sur le fichier sys.bin.txt pour déterminer l'adresse du GIET, l'adresse du gestionnaire d'interruptions, et l'adresse de la routine _isr_timer. Il faut suivre dans la trace la suite des appels de fonction entre le branchement au GIET (Gestionnaire d'Interruptions, Exceptions et Trappes), le branchement au gestionnaire d'interruption, le branchement à l'ISR, et la reprise du programme interrompu, en surveillant l'adresse de l'instruction demandée par le processeur. Vous devez relever précisément les dates de ces différents événements, calculer le nombre de cycles passés dans chaque étape de traitement, et en déduire le nombre de cycles total qui ont été "volés" à l'application interrompue.

Pour minimiser les attentes liées aux miss de cache, il est conseillé d'attendre la deuxième interruption pour faire cette analyse : avec un cache associatif, on peut espérer que le code du gestionnaire d'interruption, qui a été chargé dans le cache lors de la première interruption, est toujours dans le cache instruction lors de la deuxième interruption.

Question E9: Comment expliquez-vous que les messages affichées par l'ISR associée au TIMER[1] continuent à s'afficher sur le terminal du processeur (1), alors que le programme qui s'exécute sur le processeur(1) est toujours bloqué dans l'appel système tty_getw_irq() ?

F. Activation des interruptions TTY

Dans cette partie, nous allons activer les interruptions en provenance du TTY, pour débloquer le programme interactif de calcul du PGCD.

On rappelle ci-dessous le principe général de la communication asynchrone entre le périphérique TTY et le système d'exploitation:

Pour chaque terminal TTY[i], il existe un tampon mémoire permettant de stocker un caractère saisi au clavier, même si le programme qui doit utiliser ce caractère n'est pas prêt à le consommer. Ce tampon _tty_get_buf[i] est situé dans la zone mémoire protégée appartenant au système d'exploitation (adresses supérieures à 0x80000000). À ce tampon est associée une variable d'état _tty_get_full[i], utilisée pour la synchronisation, et indiquant si un caractère est disponible. Cette variable est de type int pour le langage C, mais elle est gérée comme une bascule set/reset. Cette bascule est mise à 1 (set) par le producteur, et remise à 0 (reset) par le consommateur.

  • Le producteur est le périphérique TTY[i], qui peut, à tout instant, générer une interruption qui va elle même lancer l'exécution du code de l'ISR _isr_tty_get pour écrire un caractère dans le tampon _tty_get_buf[i], et mettre à 1 la variable d'état _tty_get_full[i]. Tout se passe comme si le périphérique (matériel) avait lui-même écrit ces deux valeurs en mémoire.
  • Le consommateur est le système d'exploitation (logiciel), qui peut, à tout instant, exécuter le code de la fonction système _tty_read_irq pour tester si un caractère est disponible, et si c'est le cas, lire le caractère présent dans le tampon _tty_get_buf[i], le copier dans un autre tampon mémoire appartenant au programme utilisateur, et remettre à 0 la variable d'état _tty_get_full[i].

Dans l'architecture proposée, chaque processeur (i) possède son propre terminal TTY[i], et pour chaque terminal TTY[i], le composant matériel pibus_multi_tty possède une ligne d'interruption séparée p_irq_get[i] qui signale la disponibilité d'un caractère saisi au clavier du terminal TTY[i].

Question F1 : Le mécanisme de communication par interruption est asynchrone, puisque l'ISR écrit le caractère tapé au clavier dans un tampon intermédiaire appartenant au système d'exploitation, plutôt que dans le tampon mémoire du programme utilisateur auquel est destiné le caractère. Décrivez un scénario justifiant ce comportement asynchrone.

question F2 : Résumer la succession des étapes permettant à un programme utilisateur de récupérer la valeur d'un nombre saisi au clavier sous la forme d'une chaîne de caractères décimaux. Quelles sont les fonctions appelées ?

Question F3 : Analysez le code de l'ISR _isr_tty_get dans le fichier irq_handler.c. Que se passe-t-il si le tampon _tty_get_buf est plein au moment ou s'exécute l'ISR ?

Question F4 : Analysez le code de la fonction système _tty_read_irq() appelée par l'appel système tty_getw_irq() dans le fichier drivers.c. Que fait cette fonction ? Quels sont ses arguments ? Que se passe-t-il si le tampon est vide ? Comment le numéro du terminal TTY concerné est-il calculé ?

Question F5 : Analysez le code de l'appel système tty_getw_irq(). Quels sont les caractères spéciaux qui sont analysés et traités par cet appel système ? que se passe-t-il si le nombre de caractères décimaux saisis au clavier défini un nombre trop grand pour être codé sur 32 bits ?

Question F6 : Les tableaux _tty_get_buf[] et _tty_get_full[] sont déclarés dans le fichier drivers.c. Pourquoi ces variables doivent-elles être déclarées avec l'attribut volatile ? Dans quel segment mémoire ces variables sont-elles rangées ? Pourquoi ce segment doit-il être déclaré non cachable ?

Question F7 : Complétez le fichier reset.s pour initialiser les deux entrées du vecteur d'interruption correspondant aux deux IRQs provenant des deux terminaux TTY[0] et TTY[1] avec l'adresse _isr_tty_get.

Question F8 : Complétez le fichier reset.s pour modifier les registres MASK[0] et MASK[1] de l'ICU de façon à aiguiller l'IRQ provenant du TTY[0] vers le processeur 0, et l'IRQ provenant du TTY[1] vers le processeur 1. Profitez-en pour supprimer les interruptions provenant du TIMER[1], de façon à éviter de parasiter le programme interactif de calcul du PGCD. Cela peut être fait soit en modifiant la configuration du registre MASK[1] du composant ICU, soit en modifiant la configuration du registre RUNNING[1] du composant TIMER.

Relancez la génération du fichier sys.bin dans le répertoire soft, puis lancez la simulation, et vérifiez que le programme de calcul du PGCD s'exécute correctement.

G. Compte-rendu

Les réponses aux questions ci-dessus doivent être rédigées sous éditeur de texte et ce compte-rendu doit être rendu au début de la séance de TP suivante. De même, le simulateur, fera l'objet d'un contrôle (par binôme) au début de la séance de TP de la semaine suivante.

Last modified 13 months ago Last modified on Mar 23, 2023, 6:37:43 PM

Attachments (2)

Download all attachments as: .zip