Cours "Architecture des Systèmes Multi-Processeurs"
TP2 : Déploiement de code sur processeur programmable
(pirouz.bazargan-sabet@…)
A. Objectifs
Le but de ce second TP est de déployer et d'exécuter une application logicielle (écrite en langage C) sur une architecture matérielle comportant un processeur MIPS32. Vous devrez utiliser un cross-compilateur pour générer le code binaire correspondant à cette application, ainsi que le code binaire correspondant au système d'exploitation. Vous devrez charger ce code binaire dans la mémoire du prototype virtuel, lancer la simulation, et analyser comment le processeur accède à la mémoire et au terminal TTY.
L'architecture matérielle de ce TP2 est très proche de celle du TP1. La seule différence est qu'on remplace le maître "câblé" par un processeur programmable MIPS32 possédant un cache instruction et un cache de données, et qu'on introduit une seconde mémoire de type ROM, contenant le code de boot. On a donc un maître et trois cibles :
- PibusSegBcu : Arbitre du bus
- PibusMips32Xcache : Processeur MIPS32 avec ses caches
- PibusSimpleRam : ROM contenant le code de boot
- PibusSimpleRam : RAM contenant instructions et données
- PibusMultiTty : Contrôleur de terminal TTY
Le processeur MIPS32 peut démarrer une instruction à chaque cycle grâce à son architecture pipeline. Le fonctionnement détaillé des mémoires caches sera étudié dans les TP3 et TP4. Pour ce TP2, nous avons juste besoin de savoir que le processeur possède deux caches séparés pour les instructions et pour les données. Les ligne de caches ont une largeur de 16 octets (soit 4 mots de 32 bits), et le contrôleur du cache déclenche une transaction sur le Pibus dans les quatre cas suivants :
- miss instruction : le processeur cherche à lire une instruction qui ne se trouve pas dans son cache instruction. Le processeur est gelé pendant le traitement du MISS, et le contrôleur de cache effectue une transaction de type rafale consistant à lire en mémoire une ligne de cache complête (soit 4 transferts élémentaires correspondant à 4 mots de 32 bits sur le Pibus).
- miss data : le processeur cherche à exécuter une lecture de donnée cachable (instruction lw ou lb), et celle-ci n'est pas dans son cache data. Le processeur est gelé, et le contrôleur de cache effectue une transaction de type rafale consistant à lire une ligne de cache complête (soit 4 transferts élémentaires correspondant à 4 mots de 32 bits sur le Pibus).
- read uncached : le processeur cherche à exécuter une lecture de donnée non cachable (par exemple la lecture dans un registre adressable du composant PibusMultiTty). Le processeur est gelé pendant tout le temps nécessaire au contrôleur de cache pour effectuer une transaction simple (transferer un seul mot de 32 bits) sur le Pibus.
- Write : le processeur cherche à exécuter une écriture de donnée (instruction sw ou sb). Le processeur n'est généralement pas gelé, grâce au tampon d'écritures postées, et le contrôleur de cache effectue une transaction simple consistant à écrire un seul mot de 32 bits sur le Pibus.
B. Pour démarrer
L'archive attachment:multi_tp2.tgz contient les fichiers dont vous aurez besoin. Créez un répertoire de travail tp2, et décompressez l'archive dans ce répertoire. Outre les fichiers tp2_top.cpp, et tp2.desc, vous trouverez un répertoire soft servant à générer le logiciel exécuté par le processeur MIPS32.
C. Modélisation de l'architecture matérielle
Editez le fichier tp2_top.cpp qui contient une description - incomplête - de l'architecture du système. Modifiez les valeurs des arguments du constructeur du composant proc. Ces arguments définissent les caractéristiques des caches et du tampon des écritures postées. On choisira des lignes de cache de 4 mots de 32 bits, pas d'associativité, et une capacité totale de 1 Koctets pour chacun des deux caches. On choisira une profondeur de 8 mots pour le tampon d'écritures postées. Vous devez consulter le fichier pibus_mips32_xcache.h pour obtenir des informations sur le prototype du constructeur et sur les valeurs possibles pour les arguments.
On rappelle que les modèles des composants matériels sont consultables dans le répertoire :
/users/outil/soc/soclib-lip6/pibus
Question C1 Quelles valeurs doivent prendre les paramètres icache_words, icache_sets, icache_ways, dcache_words, dcache_sets, dcache_ways , wbuf_depth pour donner aux caches les caractéristiques demandées ci-dessus?
L'espace adressable défini par cette architecture matérielle contient huit segments adressables :
- Le segment seg_reset contient le code de boot. Il sera assigné au composant rom, avec l'adresse de base 0xBFC00000, et il possède une taille de 4 Koctets.
- Le segment seg_kcode contient le code du système d'exploitation. Il sera assigné au composant ram, avec l'adresse de base 0x80000000, et il possède une taille de 16 Koctets.
- Le segment seg_kunc contient les données non cachables du système d'exploitation. Il sera assigné au composant ram, avec l'adresse de base 0x81000000, et il possède une taille de 4 Koctets.
- Le segment seg_kdata contient les données cachables du système d'exploitation. Il sera assigné au composant ram, avec l'adresse de base 0x82000000, et il possède une taille de 64 Koctets.
- Le segment seg_code contient le code de l'application. Il sera assigné au composant ram, avec l'adresse de base 0x00400000, et il possède une taille de 16 Koctets.
- Le segment seg_data contient les données globales de l'application. Il sera assigné au composant ram, à l'adresse de base 0x01000000, et il possède une taille de 16 Koctets.
- Le segment seg_stack contient la pile d'exécution du programme. Il sera assigné au composant ram à l'adresse de base 0x02000000, et il possède une taille de 16 Koctets.
- Le segment seg_tty correspondant aux registres adressables du composant PIBUS_MULTI_TTY. Il possède pour adresse de base 0x90000000, et possède une taille de 16 octets.
Question C2 Pourquoi le segment seg_reset n'est-il pas assigné au même composant matériel que les 6 autres segments mémoire seg_kcode, seg_kdata, seg_kunc, seg_stack, seg_code, et seg_data ?
Question C3 Expliquer pourquoi le segment seg_tty doit être non cachable.
Question C4 Parmi les 8 segments utilisés dans cette architecture, quels sont les segments protégés (c'est à dire accessibles seulement quand le processeur est en mode superviseur). Comment est réalisée cette protection ?
Complétez le fichier tp2_top.cpp pour définir la segmentation de de l'espace adressable. Il faut définir les valeurs des adresses de base et longueurs des segments au début du fichier, et il faut complêter la table des segments.
Les composants matériel ram et rom de cette architecture doivent être préchargés. Ce pré-chargement est une facilité permise par le protoypage virtuel : Dans une vraie machine, le chargement en mémoire du code binaire exécutable est réalisé par un programme particulier, (appellé loader), qui va lire sur le disque le fichier contenant le code binaire et le charge en mémoire, aux adresses qui ont été spécifiées au moment de la compilation. Comme ce chargement est très long (puisqu'il nécessite en principe d'intégrer dans l'architecture un contrôleur de disque, qui est un périphérique lent), on utilise un raccourci, et ce chargement est réalisé (en temps nul) par le constructeur du composant matériel PibusSimpleRam lui-même, avant de lancer la simulation. Le code binaire exécutable peut être contenu dans un ou plusieurs fichiers.
Pour réaliser le chargement du code binaire, on doit utiliser un loader, qui est un objet C++ qu'il faut passer comme argument au constructeur du composant PibusSimpleRam. Le constructeur du loader prend lui-même pour argument le cheminom définissant le fichier contenant le code binaire. Si le code binaire est contenu dans plusieurs fichiers séparés, il faut donner autant de cheminoms que de fichiers.
Dans notre cas, on compilera séparément le code système et le code utilisateur, et on aura donc deux fichiers contenant du code binaire, sys.bin et app.bin, qui seront stockés dans le répertoire soft.
Complétez dans le fichier tp2_top.cpp le constructeur du composant loader qui permet de pré-charger dans les deux composants rom et ram le code binaire qui sera exécuté par le processeur MIP32.
Quand toutes ces modifications sont faites, vous pouvez compilez le fichier tp2_top.cpp pour générer l'exécutable de simulation simul.x en lançant la commande :
$ soclib-cc -p tp2.desc -t systemcass -o simul.x
A ce point vous disposez d'un simulateur de la plate-forme matérielle, mais il reste à générer le code binaire qui doit être exécuté sur cette plate-forme...
D. Système d'exploitation: GIET
Le programme exécuté dans ce TP , est une application logicielle qui s'exécute en mode utilisateur, sous le contrôle d'un petit système d'exploitation appelé GIET (Gestionnaire d'Interruptions, Exceptions et Trappes).
Le GIET fournit principalement trois services:
- un gestionnaire d'exceptions permettant de traiter les erreurs des programmes utilisateurs.
- un gestionnaire d'interruptions supportant un mécanismes d'interruptions vectorisées.
- un gestionnaires d'appels systèmes fournissant en particulier des fonctions d'accès aux périphériques.
Les deux principales limitations du GIET, qui le différencient d'un vrai système d'exploitation, sont l'absence de support pour la mémoire virtuelle, et l'absence de support pour la création dynamique de tâches.
Le code est donc séparé en deux parties: les fichiers contenant le code qui s'exécute en mode superviseur sont contenus dans le répertoire sys, tandis que les fichiers contenant le code qui s'exécute en mode utilisateur sont contenus dans le répertoire app.
- Le fichier sys_handler.c est écrit en C. Il est dans le répertoire sys, et contient le code du gestionnaire d'appels systèmes, chargé d'appeler la fonction système correspondant au service demandé.
- Le fichier exc_handler.c est écrit en C. Il est dans le répertoire sys, et contient le code du gestionnaire d'exceptions, chargé de la signalisation et du traitement des erreurs détectées dans les programmes utilisateurs.
- Le fichier irq_handler.c est écrit en C. Il est dans le répertoire sys, et contient le code du gestionnaire d'interruptions, ainsi que les routines de traitement des interruptions (ISR).
- Le fichier ctx_handler.c est écrit en C. Il est dans le répertoire sys, et contient le code du gestionnaire de changements de contexte, utilisé lorsque un processeur exécute plusieurs tâches en multiplexage temporel.
- Le fichier drivers.c contient les fonctions d'accès aux périphériques. Il rassemble donc les pilotes de tous les périphériques de la machine.
- Le fichier common.c est écrit en C. Il est dans le répertoire sys, et contient les fonctions générales du système d'exploitation, telles que les primitives de synchronisation entre tâches.
- Le fichier giet.s est écrit en assembleur MIPS32. Il est dans le répertoire sys, et contient la fonction qui analyse la cause de l'appel au GIET, et la fonction de sauvegarde/restauration de contexte.
- Le fichier stdio.c est écrit en C. Il est dans le répertoire app car il contient l'ensemble des appels systèmes qui peuvent être utilisés par un programme utilisateur écrit en C. Le nom de ce fichier provient du fait que la plupart des appels systèmes sont utilisés pour accéder aux périphériques.
Le code source du GIET est accessible et stocké dans le répertoire suivant :
/users/enseig/alain/giet_2011/
Pour entrer dans le code du GIET, une bonne méthode consiste à analyser l'appel système proctime(), qui ne fait pas intervenir de périphérique, et ne possède aucun argument : Le processeur MIPS32 possède différents registres protégés (c'est à dire accessibles uniquement en mode superviseur, au moyen des instructions mtc0 et mfc0). Parmi ces registres, le registre COUNT est initialisé à 0 au boot de la machine, et incrémenté à chaque cycle. L'appel système proctime() renvoie la valeur du registre COUNT, ce qui permet par exemple de mesurer la durée d'exécution d'un calcul.
Question D1 Quelles informations un programme utilisateur doit-il fournir au système d'exploitation lorsqu'il exécute un appel système ? Quelle est la technique utilisée par le GIET pour transmettre ces informations?
Question D2 Ouvrez le fichier giet.s. Que contiennent les deux tableaux _cause_vector[16] et _syscall_vector[32]? Par quoi sont-ils indexés ? Dans quels fichiers ces tableaux sont-ils initialisés ?
Question D3 En analysant successivement le contenu des fichiers stdio.c, giet.s, sys_handler.c, drivers.c, donnez précisément la suite d'appels de fonctions déclenchée par l'appel système proctime().
Question D4 Donnez une estimation du coût (en nombre de cycles) de cet appel système, entre le branchement à la fonction proctime(), et le retour de la fonction.
E. Génération du code binaire
Dans cette partie vous allez générer le code binaire qui sera exécuté par le processeur MIPS32, en utilisant le cross-compilateur GCC. Placez-vous dans le répertoire soft.
Le code utilisateur et le code système doivent être compilés séparément, pour générer deux fichiers binaires distincts sys.bin et app.bin.
Le répertoire soft contient six fichiers, que vous retrouverez dans tous les TPs.
- Le fichier main.c contient le code C de l'application utilisateur. Dans ce TP, cette application se contente d'afficher en boucle le célèbre message "hello world" sur l'écran du terminal TTY. Ce code s'exécute en mode utilisateur, et utilise des appels système pour accéder au périphérique TTY. Après compilation, le fichier objet main.o doit donc être lié au fichier objet stdio.o, qui contient le code des appels systèmes, pour générer le fichier app.bin contenant tout le code exécutable en mode utilisateur.
- Le fichier reset.s contient le code assembleur qui effectue le boot de la machine et lance l'application. Ce code s'exécute en mode superviseur, et le fichier objet reset.o devra donc, après compilation, être lié aux fichiers objets contenant le code du système (giet.o, drivers.o, common.o, ctx_handler.o, irq_handler.o, sys_handler.o, exc_handler.o), pour générer le fichier binaire sys.bin contenant tout le code exécutable en mode superviseur.
- Le fichier config.h permet de configurer le GIET: il définit les valeurs des deux paramètres NB_PROCS (nombre de processeurs de l'a plate-forme matérielles) et NB_MAXTASKS (nombre maximal de täches exécutées par un processeur).
- Le fichier sys.ld contient les directives utilisées par l'éditeur de liens lors de la génération du fichier sys.bin.
- Le fichier app.ld contient les directives utilisées par l'éditeur de liens lors de la génération du fichier app.bin.
- Le fichier seg.ld définit les adresses de base des différents segments connus du logiciel. Ces informations concernent aussi bien le code système que le code utilisateur, et le fichier seg.ld est inclus dans les deux fichiers sys.ld et app.ld.
On commence par la génération du code système : fichier sys.bin.
Le fichier reset.s contient le code de boot, qui est exécuté systématiquement lors du démarrage du système (c'est-à-dire lorsque le processeur se branche inconditionnellement à l'adresse 0xBFC00000, après activation du signal RESET). En général, le code de boot a pour principale fonction d'initialiser les périphériques, et de charger le code système en mémoire. Dans notre cas, le code de boot est très simple, puisqu'on considère que le code système est déjà chargé en mémoire au démarrage de la machine. En revanche, le GIET ne fournissant pas de support pour le lancement dynamique des applications, c'est le code de boot qui doit lancer le programme utilisateur.
Pour accéder aux registres protégés du processeur, le code de boot est écrit en assembleur. Il est évidemment très dépendant de l'architecture de la plate-forme matérielle, puisqu'il dépend du nombre de processeurs, du nombre et du type des périphériques. Il variera donc d'un TP à l'autre.
Question E1 Donnez trois raisons qui justifient que le code boot s'exécute nécessairement en mode superviseur.
Complétez le fichier reset.s, pour initialiser le pointeur de pile (registre $29).
Le GIET ne permet pas à l'utilisateur de lancer de façon interactive une nouvelle application (à travers un shell), lorsque la machine a déjà démarré. Pour contourner cette difficulté, c'est le code de boot qui se charge de lancer l'application utilisateur.
Question E2 Quelle est la convention (non standard) permettant au code de boot du GIET de récupérer l'adresse de la première instruction de la fonction main() ?
Avant de démarrer la compilation, commencez par définir les variables d'environnement GIET_SYS_PATH, GIET_APP_PATH, AS, CC, LD, DU
> export GIET_SYS_PATH=/users/enseig/alain/giet_2011/sys > export GIET_APP_PATH=/users/enseig/alain/giet_2011/app > export AS=/opt/gcc-cross-mipsel/8.2.0/bin/mipsel-unknown-elf-as > export CC=/opt/gcc-cross-mipsel/8.2.0/bin/mipsel-unknown-elf-gcc > export LD=/opt/gcc-cross-mipsel/8.2.0/bin/mipsel-unknown-elf-ld > export DU=/opt/gcc-cross-mipsel/8.2.0/bin/mipsel-unknown-elf-objdump
Compilez successivement les deux fichiers assembleurs reset.s et giet.s pour générer les fichiers objet correspondant dans le répertoire soft.
> $AS -g -mips32 -o reset.o reset.s > $AS -g -mips32 -o giet.o $GIET_SYS_PATH/giet.s
Compilez successivement les 6 fichiers drivers.c, common.c, ctx_handler.c, irq_handler.c, sys_handler.c, exc_handler.c pour générer les fichiers objet dans le répertoire soft.
> $CC -Wall -mno-gpopt -ffreestanding -mips32 -I$GIET_SYS_PATH -I. -c -o drivers.o $GIET_SYS_PATH/drivers.c > $CC -Wall -mno-gpopt -ffreestanding -mips32 -I$GIET_SYS_PATH -I. -c -o common.o $GIET_SYS_PATH/common.c > $CC -Wall -mno-gpopt -ffreestanding -mips32 -I$GIET_SYS_PATH -I. -c -o ctx_handler.o $GIET_SYS_PATH/ctx_handler.c > $CC -Wall -mno-gpopt -ffreestanding -mips32 -I$GIET_SYS_PATH -I. -c -o irq_handler.o $GIET_SYS_PATH/irq_handler.c > $CC -Wall -mno-gpopt -ffreestanding -mips32 -I$GIET_SYS_PATH -I. -c -o sys_handler.o $GIET_SYS_PATH/sys_handler.c > $CC -Wall -mno-gpopt -ffreestanding -mips32 -I$GIET_SYS_PATH -I. -c -o exc_handler.o $GIET_SYS_PATH/exc_handler.c
Dans la phase d'édition des liens qui suit la phase de compilation, le fichier de directives sys.ld, a pour principale fonction d'indiquer dans quels segments mémoire doivent être regroupés les différents objets résultant de la compilation. Les adresses de base de ces segments sont définies dans le fichier seg.ld. On précise également les adresses de base des segments correspondant aux périphériques, puisque ces adresses sont utilisées par l'OS pour accéder à ces périphériques.
Complétez le fichier seg.ld pour définir les adresses de base des 8 segments connus du logiciel. La double définition des adresses de base est une cause d'erreur fréquente : Les adresses de base utilisées par le logiciel sont définies dans le fichier seg.ld, alors que les adresses de base utilisées par le matériel sont définies dans le fichier tp2_top.cpp.
Question E3 Que se passe-t-il si les adresses définies dans ces deux fichiers ne sont pas égales entre elles ? Que se passe-t-il si l'adresse construite par le processeur ne correspond à aucun segment défini dans l'architecture ?
Question E4 En analysant le contenu du fichier sys.ld, déterminez quels sont les objets logiciels placés dans chacun des 2 segments qui contiennent du code système exécutable : seg_reset, seg_kcode.
Lancez l'édition de liens pour générer le fichier sys.bin contenant le code binaire exécutable, suivant les directives contenues dans le fichier sys.ld.
> $LD -o sys.bin -T sys.ld reset.o giet.o drivers.o common.o ctx_handler.o irq_handler.o sys_handler.o exc_handler.o
Désassembler ce code binaire pour obtenir une version lisible dans le fichier sys.bin.txt.
> $DU -D sys.bin > sys.bin.txt
Question E5 En analysant le contenu du fichier sys.bin.txt, déterminez la longueur effective des deux segments seg_reset et seg_kcode.
On attaque maintenant la génération du code utilisateur: fichier app.bin.
Question E6 Complétez le fichier main.c. Ce programme doit réaliser les mêmes opérations que le processeur cablé utilisé dans le TP1 : Il exécute une boucle infinie, dans laquelle il affiche la chaîne de caractères « hello world\n » sur le terminal TTY, puis se bloque en attendant qu'un caractère soit saisi au clavier, avant de passer à l'itération suivante de la boucle. On utilisera pour cela les deux appels systèmes définis dans le fichier stdio.c : tty_puts() et tty_getc(). On trouvera dans le fichier stdio.h les prototypes définissant les arguments de ces deux fonctions.
L'appel système tty_getc(), qui s'exécute en mode utilisateur, fait appel (à travers une instruction syscall), à la fonction système _tty_read(), qui s'exécute en mode superviseur.
Question E7 En analysant le code de l'appel système tty_getc() (que vous trouverez dans le ficher stdio.c) et le code de la fonction système _tty_read() (que vous trouverez dans le fichier drivers.c), expliquez le mécanisme qui rend cet appel système bloquant (c'est à dire qu'il ne rend la main au programme appelant que quand au moins un caractère a été saisi au clavier). En d'autre termes, laquelle des deux fonctions contient-elle la boucle d'attente? Expliquez pourquoi.
Compilez successivement les 2 fichiers stdio.c et main.c pour générer les fichiers objet dans le répertoire soft.
> $CC -Wall -mno-gpopt -ffreestanding -mips32 -I$GIET_APP_PATH -I. -c -o stdio.o $GIET_APP_PATH/stdio.c > $CC -Wall -mno-gpopt -ffreestanding -mips32 -I$GIET_APP_PATH -I. -c -o main.o main.c
Lancez l'édition de liens pour générer le fichier app.bin contenant le code binaire exécutable, suivant les directives contenues dans le fichier app.ld.
> $LD -o app.bin -T app.ld stdio.o main.o
Désassembler ce code binaire pour obtenir une version lisible dans le fichier app.bin.txt.
> $DU -D app.bin > app.bin.txt
Question E8 En analysant le contenu du fichier app.bin.txt, déterminez la longueur effective du segment seg_code.
Question E9 Ecrivez un makefile qui automatise toutes les étapes de génération des deux fichiers sys.bin et app.bin.
F. Exécution du code binaire sur le prototype virtuel
Le code de boot utilisé ici (contenu dans le fichier reset.s) est très simple. Dans une vraie machine le code binaire du système est stocké sur le disque, et le code de boot doit donc charger en mémoire le code du système d'exploitation, en utilisant un contrôleur de disque, ce qui peut nécessiter plusieurs millions de cycles. C'est pourquoi on appelle ce code démarrage le boot-loader. Comme nous sommes en prototypage virtuel (c'est à dire en simulation), on utilise le loader décrit dans la section C ci dessus, et tout le code binaire contenu dans les fichiers sys.bin et app.bin est pré-chargé dans les deux composants mémoire rom et ram avant de démarrer la simulation. Ce raccourci simplifie énormément le code de boot...
Lancez l'exécution du code binaire sur le prototype virtuel défini par le fichier simul.x, en vous plaçant dans le répertoire tp2, et en lançant la commande :
$ ./simul.x
Pour bien comprendre ce qui se passe, re-exécutez la simulation, en traçant les valeurs des signaux du bus ainsi que les états internes des composants matériels, et en sauvegardant la trace dans un fichier tmp. Il faut utiliser l'argument -DEBUG pour activer la trace, et l'argument -NCYCLES pour limiter le nombre de cycles simulés (et donc la taille du fichier généré).
$ ./simul.x -DEBUG 0 -NCYCLES 5000 > tmp
Question F1 En analysant la trace d'exécution, dites à quoi correspond la première transaction sur le bus ? A quel cycle le processeur exécute-t-il la première instruction du code de boot ? A quoi correspond la deuxième transaction sur le bus ?
Question F2 À quel cycle s'exécute la première instruction de la fonction main() ?
Question F3 À quel cycle commence la première transaction correspondant à la lecture de la chaîne de caractères « hello world » ?
Question F4 À quel cycle intervient la première écriture d'un caractère vers le terminal TTY ?
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.
Attachments (2)
- tp2_topcell.png (19.0 KB) - added by 15 years ago.
- multi_tp2.tgz (4.3 KB) - added by 13 years ago.
Download all attachments as: .zip