Version 73 (modified by 14 years ago) (diff) | ,
---|
TP4 : Interruptions et architectures multi-processeurs
1 Objectif
Le but de ce quatrième TP est double :
D'un côté, on souhaite introduire de nouveaux périphériques supportant la communication par interruption, et analyser les mécanismes de communication entre un programme utilisateur et un périphérique.
D'un autre côté, on souhaite modéliser des architectures comportant plusieurs processeurs programmables.
2. Interruptions vectorisées
Nous allons introduire deux types de périphériques utilisant des interruptions pour communiquer avec le système d'exploitation:
- Un périphérique caractère (tel qu'un contrôleur TTY) supporte des requêtes de lecture ou d'écriture d'un seul caractère. Ce type de périphérique se comporte comme une cible sur le bus, puisqu'il ne peut que recevoir des commandes provenant d'un processeur, et qu'il n'a pas la possibilité de lire ou d'écrire lui-même en mémoire.
- Un périphériques bloc, tel qu'un contrôleur de disque, doit tranférer de grosses quantités de données entre la mémoire et l'extérieur. Les transferts se font par blocs (un bloc contenant généralement 512 octets), et ces périphériques ont généralement une capacité DMA : Ils sont à la fois maître et cible sur le bus, cat ils peuvent directement lire ou écrire en mémoire.
2.1 composants matériels
Lorsque le nombre de périphériques augmente, le nombre de lignes d'interruption augmente également, et il faut un mécanisme permettant de concentrer plusieurs dizaines de requêtes d'interruption vers un seul signal connecté au processeur.
Le composant vci_icu est un contrôleur d'interruptions vectorisées. C'est une cible VCI dont vous trouverez la spécification fonctionnelle ici. Prenez le temps de la lire.
Le composant vci_multi_timer est également une cible VCI contenant un nombre queconque de timers programmables capables de générer des interruptions périodiques. On trouvera la spécification fonctionnelle de ce composant ici.
Le composant vci_block_device est un contrôleur de périphérique de stockage externe (disque ou mémoire flash). Ce composant IOC est à la fois un initiateur VCI, capable de lire et d'écrire dans la mémoire, et une cible qui peut recevoir des commandes de configuration. Puisque nous sommes en simulation, le composant IOC gère un unique fichier, stocké sur le disque de la station de travail qui exécute le simulateur. Le nom de ce fichier est un argument du constructeur. On trouvera la spécification fonctionnelle de ce composant ici.
Le composant vci_frame_buffer est un contrôleur d'écran graphique. C'est une cible VCI qui est vue comme un tampon mémoire directement adressable de M lignes de N pixels, dans lequel le logiciel peut lire ou écrire. Le contenu de ce buffer est parcouru périodiquement, et son contenu est affiché sur l'écran graphique externe. On trouvera la spécification fonctionnelle de ce composant ici.
Le composant vci_dma est un composant matériel qui peut être programmé par le système d'exploitation pour éffectuer de gros transferts de données, tel que la copie d'une image d'un tampon mémoire situé dans l'espace utilisateur vers le tampon mémoire du composant FBF. On trouvera la spécification fonctionnelle de ce composant DMA ici.
Les deux composants IOC et DMA étant à la fois initiateur et cible, on obtient finalement une architecture possédant trois initiateurs VCI (indexés de 0 à 2), et 9 cibles VCI (indexées de 0 à 8), conformément au schéma ci-dessous.
Les lignes d'interruptions ne passent pas par le réseau VCI : chaque ligne d'interruption d'un périphérique est directement connectée aux ports d'entrée p_irq_in[i] du composant ICU :
- La ligne d'interruption du TIMER est connectée au port p_irq_in[0]
- la ligne d'interruption du TTY est connectée au port p_irq_in[1]
- la ligne d'interruption du contrôleur IOC est connectée au port p_irq_in[2]
- la ligne d'interruption du contrôleur DMA est connectée au port p_irq_in[3]
Le système d'exploitation (GIET) associe à chaque ligne d'interruption une routine de traitement spécifique, appelée ISR (Interrupt Service Routine), qui est exécutée par le processeur lorsque la ligne d'interruption est activée par le périphérique, et que les interruptions ne sont pas masquées. Il s'agit donc pour le périphérique de "voler" quelques cycles du processeur pour lui permettre d'exécuter un peu de code. L'ISR permet généralement au périphérique de signaler un événement en allant écrire dans un emplacement prédéfini en mémoire.
2.2 Communication entre l'OS et le contrôleur TTY
Dans le TP3, le programme utilisateur utilise l'appel système tty_getc() pour lire un caracère. Cet appel système bloquant contient une boucle de scrutation dans laquelle, à chaque tour de boucle, on effectue une transaction sur le bus pour lire la valeur du registre STATUS du terminal TTY concerné. On ne sort de cette boucle que lorsqu'un caractère a effectivement été saisi au clavier.
Dans ce TP4, le programme utilisateur utilisera l'appel système tty_get_irq() pour lire un caractère. Cet appel système utilise un tampon mémoire partagé _tty_get_buf, protégé par une variable de synchronisation -tty_get_full. Ces deux variables appartiennent au système d'exploitation et sont stockées dans le segment seg_kunc, qui est à la fois protégé (non accessible par les programmes utilisateur) et non cachable. Plutôt que d'effectuer une scrutation sur le registre STATUS du contrôleur TTY, l'appel système tty_get_irq() teste la variable _tty_get_full rangée en mémoire. Si la valeur de cette variable indique qu'il n'y a pas de caractère disponible, le système d'exploitation peut (en principe) attribuer le processeur à un autre programme utilisateur. C'est la routine d'interruption (ISR) associée au terminal TTY qui se charge d'écrire le code ASCII du caractère dans le tampon _tty_get_buf, et de forcer à 1 la variable de synchronsation _tty_get_full pour signaler que le tampon est plein. Cette variable de synchronisation est remise à 0 par l'appel système tty_getc_irq() lorsque le caractère est transféré du tampon système tty_get_buf vers le tampon mémoire défini par l'utilisateur.
Dans une architecture monoprocesseur, le processeur peut exécuter plusieurs tâches (plusieurs programmes utilisateurs) en pseudo-paralléliseme, par multiplexage temporel. Chaque tâche possède alors son propre terminal écran/clavier, mais ces différents terminaux sont tous contrôlés par le même contrôleur TTY : Chaque terminal a son propre jeu de 4 registres, pour communiquer avec l'OS. Le GIET supporte au plus 8 processeurs, et au plus 4 tâches par processeur. Le GIET supporte donc au plus
32 terminaux ecran/clavier, et définit donc deux tableaux _tty_get_buf[32] et _tty_get_full[32], indexés
par le numéro du terminal concerné.
Question : Comment les deux entités communicantes (l'ISR et l'appel système) calculent-elles l'index du terminal concerné ? La réponse se trouve dans les fichier drivers.c et isr.s.
Question : Que fait la routine d'interruption ISR déclenchée par le périphérique TTY lorsqu'un caractère est frappé alors que la variable _tty_get_full[i] vaut 1 ? La réponse se trouve dans le fichier isr.s.
Question : Quel est l'avantage de ce type de communication par interruption, comparé au mécanisme de scrutation utilisé dans le TP3 ?
2.2 Communication entre l'OS et le contrôleur IOC
Les deux appels système ioc_read() et ioc_write() permettent à un programme utilisateur de demander au GIET de réaliser un transfert de données entre un tampon mémoire utilisateur et le périphérique de stockage externe. Tous les transferts se font par blocs de 512 octets.
Question : Editez le fichier stdio.c pour déterminer la signification des trois arguments de ces appels système. Editez le fichier drivers.c pour voir ce que font réellement les deux fonctions système _ioc_read() et _ioc_write() associées. Quand ces fonctions rendent-elle la main au programme utilisateur?
L'appel système ioc_completed(), qui appelle lui-même la fonction système _ioc_completed permet au programme utilisateur de se mettre en attente sur la fin d'un transfert. C'est donc une fonction bloquante qui ne rend la main au programme utilisateur que lorsque la variable de synch'onisation _ioc_busy a repris la valeur 0.
Question : Quelle fonction met la variable _ioc_busy à 1 ? Quelle fonction remet cette variable à 0 ? QUelles sont les deux services rendus par cette variable de synchronisation ? Dans quel segment doit-elle être rangée?
3 Modélisation de l'architecture matérielle
L'archive soclib_tp4.tgz contient différents fichiers dont vous aurez besoin pour ce TP. Créez un répertoire de travail spécifique TP4, recopiez l'archive dans ce répertoire, et décompressez-la:
$ tar xzvf soclib_tp4.tgz
Outre les fichiers qui permettent de générer le simulateur de l'architecture matérielle, cette archive contient également un fichier images.raw qui contient une séquence d'images, et le sous-répertoire soft qui est utilisé pour la génération du logiciel embarqué.
Question : Complêtez le fichier tp4_top.cpp pour définir les adresses de base et les tailles des cinq segments associés aux composants ICU, TIMER, IOC, FBF et DMA, et pour introduire ces 5 segments dans la table des segments. Ces segments sont-ils cachables ou non cachables?
Question : Complétez le fichier tp4_top.cpp pour définir les paramètres des constructeurs des cinq nouveaux composants ICU, TIMER, IOC, FBF et DMA. Pour le composant IOC on fera en sorte que le cheminom désignant le fichier externe puisse être redéfini par un paramètre sur la ligne de commande au lancement du simulateur. Pour le composant FBF, on choisira une taille d'écran de 128 lignes de 128 pixels.
Question : Complétez la net-list dans le fichier tp4_top.cpp pour connecter les 4 lignes d'interruption utilisées dans cette architecture mono-processeur.
Question : Complétez le fichier tp4_top.desc pour pouvoir utiliser soclib-cc, et utilisez le Makefile pour pour générer le simulateur.
4. Logiciel embarqué
Le répertoire soft contient les quatre fichiers ldscript, reset.s, main.c, et Makefile permettant de générer le logiciel embarqué.
4.1 Code de boot
Puisqu'on utilise des interruptions, le code de boot doit initialiser le vecteur d'interruption (c'est à dire le tableau indexé par le numéro d'interruption, et contenant les adresses des différentes routines d'interruption). Il doit également initialiser le composant ICU, pour démasquer les interruptions qu'on veut autoriser.
Question : En ouvrant le fichier isr.s, déterminez les nom des quatre ISRs associées aux composants TIMER, TTY, IOC et DMA. Modifiez le fichier reset.s pour initialiser les entrées correspondantes du vecteur d'interruption.
Question : Le fichier icu.h contient la carte des registres adressables du composant ICU. Ouvrez ce fichier (rangé) dans le répertoire du GIET) pour déterminer l'offset du registre ICU_MASK_SET, et complétez le fichier reset.s pour démasquer les quatre lignes d'interruption utilisées : irq_in[0], irq_in[1], irq_in[2], irq_in[3].
4.2 Activation du TIMER
On va commencer par exécuter un programme main_0 très simple, qui se contente d'activer la génération d'interruptions périodiques par le TIMER. Consultez le fichier stdio.c pour déterminer quels sont les deux appels système qui permettent de définir la période et d'autoriser le TIMER à générer les interruptions périodiques.
Question : modifiez le fichier main_0.c pour que le TIMER génère des interruptions avec une période de 100000 cycles.
4.3 Utilisation du contrôleur TTY
On veut maintenant utiliser le contrôleur TTY en écrivant un petit interprêteur de commandes, qui exécute une boucle infinie dans laquelle il lit des commandes tapées au clavier (un seul caractère à la fois) et les exécute. On traitera au minimum les trois commandes suivantes:
- a : activation des interruptions générées par le TIMER
- d : desactivation des interruptions générées par le TIMER
- q : sortie de l'interprêteur par l'appel système exit()
Question : Complétez le fichier main_0.c pour coder cet interprêteur de commandes en utilisant les appels système tty_getc_irq() et tty_puts(). Compilez en utilisant le Makefile du répertoire soft, et exécutez ce programme interactif sur le simulateur.
4.4 Contrôleur IOC et contrôleur d'écran graphique
On veut utiliser le contrôleur IOC pour charger dans un tampon mémoire du programme utilisateur une séquence d'images stockées dans le fichier images.raw, avant d'afficher ces images sur l'écran graphique contrôlé par le composant FBF. Le fichier images.raw contient des images de 128 lignes de 128 pixels codées en 256 niveaux de gris (un octet par pixel.
Question : Quel est l'encombrement d'une image en nombre de blocs?
Question : Ecrivez un nouveau programme main_1 dans un fichier main_1.c. Vous utiliserez les appels système ioc_read() et ioc_completed() pour charger une image dans un tampon mémoire de 128*128 octets tab[128][128], déclaré dans la fonction main(). Dans quel segment sera rangéé ce tableau tab[128][128]? Utilisez l'appel système fbf_write() pour afficher cette image sur l'écran graphique. N'oubliez pas de tester systématiquement la valeur du code de retour chaque fois que vous utilisez un appel système. Modifiez le Makefile pour utiliser main_1.c au lieu de main_0.c, compilez le logiciel embarqué, et exécutez-le sur le simulateur.
Question : Complétez le fichier main_1.c pour encapsuler cette séquence chargement / affichage dans une boucle de façon à afficher successivement les images du fichier images.raw. On pourra utiliser l'appel système tty_getc_irq pour rendre interactif le passage à l'image suivante.
5. Architecture multi-processeurs générique
On veut maintenant modéliser une architecture matérielle multi-processeur générique, où le nombre de processeurs est défini par un paramètre NPROCS sur le ligne de commande.
5.1 architecture matérielle
Modifiez le fichier tp3_top.cpp et renommez-le tp3_top_multi.cpp . Chaque processeur aura son propre concentrateur d'interruption (un composant ICU par processeur). Comme chaque processeur peut - en principe - exécuter plusieurs tâches en pseudo-parallélisme, et que chaque tâche possède un terminal privé, on peut avoir un grand nombre de terminaux. On aura donc un contrôleur TTY indépendant pour chaque processeur. Pour ce qui concerne les TIMERs, on souhaite avoir un timer indépendant pour chaque processeur, mais puisque le composant VciTimer peut contenir jusque 256 timers indépendants, on utilisera un seul composant VciTimer.
- Le GIET ne pouvant supporter que 8 processeurs, on vérifiera que le paramètre NPROCS est inférieur ou égal à 8.
- Il faut définir des tableaux de pointeurs pour les composants répliqués PROC[i], TTY[i] et ICU[i], ainsi que pour les différents signaux connectés à ces composants.
- Il faut utiliser une boucle indexée par l'index du processeur pour les constructteurs des composants PROC[i], ICU[i], et TTY[i].
- Pour l'index des initiateurs (SRCID), on utilisera les valeurs 0 à (NPROCS-1) pour les processeurs, et les valeurs NPROCS et (NPROCS+1) pour le composant IOC et pour le composant DMA respectivement.
- Pour l'index des cibles (TGTID), on utilisera les valeurs (0) à (6) pour les composants matériels non répliqués (ROM, RAM GCD, TIMER, IOC, DMA, FBF). On utilisera les valeurs (7) à ( 7 + NPROCS -1) pour les composants TTY[i]. On utilisera les valeurs (7 + NPROCS) à (7 + 2*NPROCS -1) pour les composants ICU[i].
- il faut définir NPROCS segments pour les composants ICU[i]. L'adresse de base du segment associé au composant ICU[i] est définie comme : seg_icu_base + 0x00100000 * proc_id (cette contrainte est imposée par le GIET).
- Il faut définir NPROCS segments pour les NPROCS TTY[i]. L'adresse de base du segment associé au composant ICU[i] est définie comme : seg_tty_base + 0x00100000 * proc_id (cette contrainte est imposée par le GIET).
- Il faut utiliser des boucles indexées par l'index du processeur pour connecter les composants répliqués.
- Les interruptions non-répliquées (IRQ_IOC, IRQ_DMA) seront connectées au contrôleur d'interruptions ICU[0]. Les interruptions répliquées IRQ_TTY[i] et IRQ_TIMER[i] seront connectées au concentrateur d'interruptions ICU[i].
Question : Modifiez le fichier tp4_top.cpp pour rendre l'architecture générique, conformément aux recommandations ci-dessus.
Question : Modifiez le fichier tp4_top.desc pour créer un fichier tp4_top_multi.desc, et modifiez le fichier Makefile en conséquence pour générer un simulateur générique simulator_multi.x.
5.2 Code de boot
Un vrai système d'exploitation tel que LINUX (ou MutekH) permet de créer dynamiquement de nouvelles tâches (i.e. de nouveaux programmes utilisateurs) alors que la machine est déjà démarrée (mécanisme pthread_create() pour les threads POSIX, ou mécanisme fork/exec pour les processus UNIX). Le GIET n'est pas un vrai système d'exploitation, car il ne supporte pas la création ou la destruction dynamique de tâches : Les tâches doivent être créées une fois pour toute au démarrage de la machine, c'est à dire dans le code de boot.
Bien que le GIET supporte le fonctionnement multi-tâches (un seul processeur exécute plusieurs tâches en pseudo-parallélisme par multiplexage temporel), on va utiliser ici un mécanisme plus simple où chaque processeur n'exécute qu'une seule tâche, qui lui est assignée dans le code de boot.
Le mécanisme est le suivant :
- Tous les processeurs exécutent le même code de boot, mais l'exécution de ce code dépend de l'index du processeur. L'index du processeur (proc_id) étant stocké dans un registre système (registre $15 pour le processeur MIPS32), sa valeur peut être testée par le logiciel. Cette valeur est initialisée par le constructeur, ce qui modélise une valeur cablée lors de la fabrication de la puce.
- Les tâches s'exécutant en parallèle, chaque tâche (et donc chaque processeur) doit disposer de sa propre pile d'exécution. On définit un seul segment pour les différentes piles d'exécution, mais les ponteurs de pile des différents processeurs doivent être initialisés à des valeurs différentes en fonction du proc_id.
- Chaque tâche correspondant à un programme utilisateur différent, les points d'entrée de chaque tache (registre EPC dans le cas du MIPS32) doivent être initialisées à des valeurs différentes (main_0, main_1, main_2, etc...) en fonction du proc_id.
Question : Modifiez le fichier reset.s pour initialiser le pointeur de pile ($29 dans le cas du MIPS32) à une valeur dépendant du proc_id. Chaque tâche disposera d'une pile de 64 Koctets. Quelle doit être la taille minimale du segment de pile défini dans la table des segments de la top-cell?
Question : Modifiez le fichier reset.s pour initialiser le point d'entrée (registre EPC dans le cas du MIPS32) à une valeur dépendant du proc_id. On définira une table de sauts indexé par le proc_id et contenant les adresses main_0 à main_7. Ces adresses correspondent aux 8 point d'entrée de 8 programmes utilisateurs susceptibles de s'exécuter sur l'architecture multi-processeurs.
Question : Modifiez le fichier Makefile du répertoire soft pour que soient compilés le fichier teset.s ainsi modifié, et les deux programmes main_1.c et main_1.c de la section 4.
Question : Lancez l'exécution sur le simulateur simulator_multi.x.
5.3 Exécution paralléle de programmes non-coopératifs
On souhaite pour commencer lancer l'exécution parallèle des deux programmes main_0 et main_1 définis précédemment sur une architecture à deux processeurs.
Question : Modifiez le fichier Makefile du répertoire soft pour générer le logiciel embarqué, puis lancez la simulation sur une architecture à 2 processeurs.
5.4 Programme coopératif multi-tâches
6 Compte-rendu
Il ne vous est pas demandé de compte-rendu pour ce TP, mais on vous demandera une démonstration au début du TP de la semaine suivante...
Attachments (3)
- soclib_tp4_mono.png (35.2 KB) - added by 14 years ago.
- images.tgz (230.9 KB) - added by 14 years ago.
- soclib_tp4.tgz (6.7 KB) - added by 10 years ago.
Download all attachments as: .zip