{{{ #!html

TP5 : GDB server & architectures clusterisées

}}} [[PageOutline]] = 1 Objectifs = Ce cinquième TP a un double objectif : D'une part, on présente l'outil '''GDB Server''' qui est le principal outil permettant de déverminer une application logicielle s'exécutant sur une architecture matérielle prototypée avec SoCLib. D'autre part, on introduit les architectures clusterisées utilisant deux niveaux d'interconnexion et donc deux niveaux d'indexation pour les composants matériels. = 2 Outil GDB Server = L'outil GDB est un outil de debug très utilisé, qui fait partie de la même famille d'outils logiciels libres que le compilateur GCC que vous connaissez déjà (voir [http://www.gnu.org/software/gdb/ Gnu GDB]). Le '''GDB Server''' est un composant matériel qui s'interface entre le processeur et le contrôleur de cache. Dans cette position stratégique, il peut surveiller et contrôler toutes les communications entre le processeur et le reste de la plate-forme matérielle (principalement la mémoire). [[Image(soclib_tp5_gdb_server.png)]] Le comportement du composant matériel '''GDB server''' est lui-même contrôlé par une application logicielle interactive, appelée '''GDB client''', qui peut s'exécuter sur une autre station de travail que celle qui simule l'exécution de la plate-forme modélisée avec SoCLib. En agissant sur le GDB Server (par l'intermédiaire du client GDB), on peut donc faire deux choses : * contrôler le processeur (pour exécuter un programme en mode instruction par instruction par exemple), * contrôler le reste du système (en effectuant directement des commandes de lecture ou d'écriture vers la mémoire ou vers un périphérique). La documentation de l'outil '''GDB Server''' peut être consultée [https://www.soclib.fr/trac/dev/wiki/Tools/GdbServer ici]. Le '''GDB Server''' permet: * de poser des points d'arrêt dans le programme en cours d'exécution * d'exécuter le programme pas à pas (instruction par instruction) * de visualiser le contenu des registres du processeur * de visualiser la valeur stockée à n'importe quelle adresse dans l'espace adressable * de modifier le contenu de la mémoire ou des registres des processeurs Le simulateur (qui contient le serveur GDB) et le client GDB peuvent s'exécuter sur deux stations de travail différentes, puisque les communications entre le client GDB et le serveur GDB utilisent un canal TCP. == 2.1 Modification de la top-cell == Pour utiliser le '''GDB Server''', tous les processeurs dont on souhaite prendre le contrôle doivent être instanciés dans un mode particulier lorsqu'on définit l'architecture de la '''top-cell'''. Il faut remplacer l'instanciation habituelle du processeur: {{{ VciXcacheWrapper proc("proc", ...); }}} par une instanciation faisant appel au GDB Server {{{ VciXcacheWrapper > proc("proc", ...); }}} Il ne fautpas oublier d'inclure le "header" dans la top-cell {{{ #include "gdbserver.h" }}} Il faut également modifier le fichier de description de l'architecture utilisé par soclib-cc (fichier `platform.desc`), et remplacer la ligne: {{{ Uses('caba:vci_xcache_wrapper', iss_t = 'common:mips32el') }}} par la ligne : {{{ Uses('caba:vci_xcache_wrapper', iss_t = 'common:gdb_iss', gdb_iss_t = 'common:mips32el') }}} == 2.2 lancement de la simulation == Le GDB serveur peut être configuré pour fonctionner dans différents modes: * On souhaite lancer le simulateur dans un mode où le processeur est "gelé", ce qui signifie que le GDB serveur ne transmet aucune requête du processeur vers le cache, en attendant la connexion du client GDB. IL faut pour cela activer l'attribut '''F''' (Frozen). * Par défaur, le GDB Server considère que tout appel système (branchement à l'adresse 0x800000180) est un point d'arrêt. On peut supprimer ce point d'arrêt implicite en activant l'attribut '''X'''. Ces deux modes peuvent être définis grâce à la variable d'environnement SOCLIB_GDB avant de lancer le simulateur : {{{ $ export SOCLIB_GDB=FX }}} Lorsqu'on a fini d'utiliser le GDB server, et qu'on veut revenir dans le mode ou le simulateur démarre normalement, il faut re-modifier la variable d'environnement, en désactivant les deux attributs '''F''' et '''X''': {{{ $ export SOCLIB_GDB= }}} == 2.3 lancement du client GDB == Une fois que le simulateur est lancé, il faut lancer dans une autre fenêtre, l'exécution du client GDB adapté au type de processeur instancié dans la plate-forme, en lui passant en argument le nom du fichier contenant le code binaire exécuté par les processeurs de l'architecture. Pour un processeur MIPS32: {{{ mipsel-unknown-elf-gdb soft/bin.soft }}} La première commande à taper dans GDB est la commande permettant de connecter le client GDB au GDB Server : {{{ (gdb) target remote localhost:2346 }}} == 2.4 identification des processeurs == L'outil GDB standard permet d'analyser le comportement d'applications multi-threads s'exécutant sur une architecture monoprocesseur. Nous souhaitons ici contrôler une architecture multi-processeurs. Pour pouvoir réutiliser le client GDB standard, le GDB Server considère chaque processeur comme un thread. Pour obtenir le nombre de processeurs contrôlables par GDB, il faut taper la commande : {{{ (gdb) info threads }}} Attention : la numérotation des threads ne correspond pas nécessairement à la numérotation des processors (processor_id) == 2.5 Points d'arrêt == La commande ''break'' (br) permet de poser des points d'arrêt correspondant à la détection d'une adresse particulière dans le registre PC d'un processeur quelconque de l'architecture simulée : {{{ (gdb) br *0x8000180 }}} Dans ce cas, tous les processeurs de la plate-forme s'arrêtent simultanément. On peut également utiliser un nom de fonction plutôt qu'une adresse hexadécimale {{{ (gdb) br function_name }}} Pour supprimer un point d'arrêt, il faut lancer la commande qui retourne la liste des points d'arrêts enregistrés avec leur numéro: {{{ (gdb) info br }}} Puis lancer la commande de destruction, en désignant le point d'arrêt par son numéro : {{{ (gdb) delete n }}} == 2.6 Exécution == Lorsque l'exécution est arrété, la commande ''continue'' (c) permet de relancer l'exécution jusqu'au prochain point d'arrêt. Tous les processeurs reprennent leur exécution. {{{ (gdb) c }}} La commande ''stepi'' permet de relancer l'exécution d'une seule instruction assembleur. Seul le processeur qui a détecté un point d'arrêt est concerné. Les autres processeurs restent bloqués: {{{ (gdb) stepi }}} == 2.7 Observation des registres == La commande ''info'' permet d'afficher différentes informations. {{{ (gdb) info }}} Sans argument, cette commande donne la liste des informations disponibles. {{{ (gdb) info reg }}} Avec l'argument ''reg'', elle affiche le contenu des registres du processeur qui a détecté un point d'arrêt. Si on veut observer les valeurs contenues dans les registres d'un autre processeur, il faut préalablement utiliser la commande ''thread n'' (où n est le numéro du thread représentant le processeur). {{{ (gdb) thread n (gdb) info reg }}} == 2.8 Observation/modification de la mémoire == Pour observer les valeurs contenues en mémoire, on peut utiliser la commande ''examine'' (x) : {{{ (gdb) x/10x 0x400000 }}} Cette commande va afficher en hexadécimal 10 mots de 32 bits à partir de l'adresse 0x400000. Pour modifier une valeur en mémoire, on peut utiliser la commande ''set'' : {{{ (gdb) set *(int*)0x400000 = val }}} Cette commande écrit la valeur val (de type int) à l'adresse 0x400000. == 2.9 Utilisation == A titre d'exercice, vous allez utiliser le '''GDB server''' sur l'architecture mono-processeur du TP4, sur laquelle vous exécuterez successivement deux applications logicielles contenant chacune un bug que vous devez localiser puis corriger. Les deux bugs (différents) qui ont été introduits dans chacune de ces deux applications ne sont pas visibles à la compilation, et n'apparaissent qu'à l'exécution. Créez un répertoire de travail TP5, dans lequel vous recopierez les fichiers permettant de générer l'architecture monoprocesseur du TP4. Téléchargez et décompressez l'archive [attachment:soclib_tp5.tgz soclib_tp5.tgz] dans ce répertoire. {{{ $ tar xzvf soclib_tp5.tgz }}} L'archive qui vous est fournie contient deux répertoires '''soft_gcd''' et '''soft_prime'''. * Le répertoire '''soft_gcd''' contient tous les fichiers nécessaires à la génération du code binaire de l'application de calcul du PGCD que vous connaissez déjà. * Le répertoire '''soft_prime''' contient tous les fichiers nécessaires à la génération du code binaire d'une autre application logicielle réalisant le calcul des nombres premiers. '''Question''' : Placez-vous dans le répertoire '''soft_gcd''', compilez l'application logicielle en utilisant le Makefile qui vous est fourni. Retournez dans le répertoire '''TP5''' pour générer le simulateur '''simulator.x'''. Lancez l'exécution du simulateur en redéfinissant les chemins d'accès au code binaire: {{{ $ ./simulator.x -SYS soft_gcd/sys.bin -APP soft_gcd/app.bin }}} Vous devez constater que vous n'obtenez pas le résultat attendu. Modifiez le fichier '''tp4_top.cpp''' et le fichier '''tp4.desc''', ainsi que le fichier '''Makefile''' pour introduire le GDB Server dans l'architecture comme indiqué ci-dessus, en remplaçant partout ''tp4'' par ''tp5'' dans les noms de fichier. Regénérez le simulateur en utilisant soclib-cc. Lancez l'exécution du simulateur dans une première fenêtre de travail, après avoir redéfini la variable d'environnement SOCLIB_GDB comme indiqué ci-dessus. Ouvrez dans une seconde fenêtre le fichier '''sys.bin.txt''', de façon à pouvoir suivre - instruction par instruction - le programme en cours d'exécution, depuis la première instruction du code de boot (adresse 0xbfc00000). Lancez le client GDB dans une troisième fenêtre, connectez-le au simulateur. Commencez à exécuter le programme instruction par instruction avec la commande (''stepi''). Après chaque commande (''stepi''), il faut lancer une commande (''info r'') pour afficher le contenu des registres internes du processeur, et vérifier que le comportement de la machine est conforme à ce qu'on attend. Le premier dysfonctionnement apparaît assez rapidement... '''Question''' : Après avoir localisé le bug, corrigez-le et vérifiez que le calcul du PGCD s'exécute correctement. '''Question''' : Refaites le même exercice avec l'application logicielle contenue dans le répertoire '''soft_prime'''. Quand les deux bugs ont été localisés et corrigés, vous pouvez attaquer l'étape suivante. = 3 Architectures Clusterisées = Nous appellons ''architecture clusterisée'' une architecture dans laquelle on utilise un double système d'index pour repérer les initiateurs et les cibles VCI. Un '''cluster''' est un sous-système regroupant généralement plusieurs initiateurs et plusieurs cibles VCI, communiquant entre eux par un interconnect local (bus ou crossbar). Chaque composant est donc repèré par un couple (cluster_index, local_index). Les architectures clusterisées sont généralement des architectures NUMA (Non Uniform Memory Access). On trouve en effet un banc mémoire physique par cluster, afin d'éviter de remplacer le goulot d'étranglement du bus partagé par un autre goulot d'étranglement qui serait l'accès à un unique banc mémoire. La mémoire est donc physiquement distribuée, même si l'espace d'adressage reste partagé. NUMA signifie donc deux chose : * n'importe quel initiateur peut directement adresser n'importe quelle cible, * la latence et la consommation énérgétique seront beaucoup plus faible lorsque qu'un processeur accède à la mémoire locale (dans le même cluster) que lorsqu'il accède à une mémoire distante (dans un autre cluster). On a généralement un mécanisme d'interconnexion local dans chaque cluster (bus ou crossbar) pour les communications locales, et un interconnect global (généralement un micro-réseau intégré sur puce) pour les communications inter-clusters. Les architectures NUMA clusterisées visent donc deux objectifs: * D'un point de vue performance, on peut réduire la consommation énergétique et la latence des communications en regroupant dans un même cluster les composants qui communiquent beaucoup entre eux. * D'un point de vue électrique, le découpage en clusters permet de résoudre en partie les problèmes d'horlogerie, puisque chaque cluster peut être implanté dans un domaine d'horloge séparé (approche GALS : Globally Asynchronous / Locally Synchronous). Le franchissement des frontières d'horlogre est alors la responsabilité du micro-réseau assurant les communications inter-clusters. Remarquez cependant que la capacité de stockage de la mémoire embarquée dans les clusters n'est jamais suffisante, et que l'accès à la mémoire externe à la puce reste un goulot d'étranglement. == 3.1 structuration des adresses et des identifiants == Pour faciliter le décodage des adresses, on décompose les bits de poids fort de l'adresse VCI en deux champs GADR et LADR, avec la contrainte suivante : || GADR || LADR || OFFSET || * le décodage du champs GADR définit complêtement le numéro du cluster cible. * le décodage du champs LADR permet lui de déterminer l'index local de la cible dans un cluster. Le nombre de bits des champs GADR et LADR est une caractéristique de chaque architecture. De la même façon, les identifiants des composants (SRCID ou TGTID) sont également décomposés en deux champs CID (cluster_index) et LID (local_index): || CID || LID || Cette organisation hiérarchique à deux niveaux impose évidemment que les valeurs des champs GADR des adresses de base des segments associés aux cibles d'un même cluster soient égales entre elles (ou appartiennent à un même ensemble de valeurs caractéristiques de ce cluster) == 3.2 architecture à 4 clusters == On souhaite modéliser une architecture structurée en quatre clusters. Chaque cluster contiendra un processeur MIPS32 (avec son contrôleur de cache), une mémoire RAM, et un ou deux contrôleurs de périphériques: * Le cluster 0 contient le contrôleur de disque IOC. * Le cluster 1 contient le contrôleur DMA et et le contrôleur d'interruptions ICU. * Le cluster 2 contient le contrôleur d'écran graphique FBF et le contrôleur de timers TIM. * Le cluster 3 contient la ROM de boot et le contrôleur TTY. * On se dispensera d'instancier le coprocesseur GCD dans cette architecture. On supposera que chacun des periphériques TIM, TTY et DMA possède 4 canaux, de sorte que le canal [i] soit utilisé par le processeur P[i]. On utilisera un composant '''vci_local_crossbar''' comme interconnect local (voir documentation [https://www.soclib.fr/trac/dev/wiki/Component/VciXcacheWrapper ici]) , et on utilisera le composant '''vci_vgmn''' comme interconnect global (voir documentation [https://www.soclib.fr/trac/dev/wiki/Component/VciXcacheWrapper ici]) . [[Image(soclib_tp5_archi_clusters.png)]] Pour ce qui concerne les interruptions: * Les 4 lignes d'interruption IRQ_TIM[3:0] du périphérique TIM seront connectée aux entrées IRQ_IN[3:0] du composant ICU. * Les 4 lignes d'interruption IRQ_DMA[3:0] du périphérique DMA seront connectée aux entrées IRQ_IN[7:4] du composant ICU. * Les 4 lignes d'interruption IRQ_TTY[3:0] du périphérique TTY seront connectée aux entrées IRQ_IN[11:8] du composant ICU. * La ligne d'interruption unique du périphérique IOC sera connectée à l'entrée IRQ_IN[12] du composant ICU. On définit 16 segments pour cette plate-forme, qui peuvent être protégés ou non, suivant la valeur du bit d'adresse A31. '''Cluster 0''' * segment '''seg_kcode''' pour le code système à l'adresse imposée 0x80000000, protégé. * segment '''seg_kdata''' pour les données cachables du système d'exploitation, protégé. * segment '''seg_kunc''' pour les données non-cachables du système d'exploitation, protégé. * segment '''seg_ioc''' associé au contrôleur de disque, protégé. * segment '''seg_stack0''' pour la pile de la tâche qui s'exécute sur le processeur P0, non protégé. '''Cluster 1''' * segment '''seg_dma''' associé au contrôleur DMA, protégé. * segment '''seg_icu''' associé au composant ICU, protégé. * segment '''seg_stack1''' pour la pile de la tâche qui s'exécute sur le processeur P1, non protégé. '''Cluster 2''' * segment '''seg_fbf''' associé au frame buffer, protégé. * segment '''seg_tim''' associé au composant TIM, protégé. * segment '''seg_stack2''' pour la pile de la tâche qui s'exécute sur le processeur P2, non protégé. '''Cluster 3''' * segment '''seg_code''' pour le code des programmes utilisateur, non protégé. * segment '''seg_data''' pour les données globale des programmes utilisateur, non protégé. * segment '''seg_tty''' associé au composant TTY, protégé. * segment '''seg_rom''' associé à la ROM de boot, à l'adresse imposée 0xBFC00000, protégé. * segment '''seg_stack3''' pour la pile de la tâche qui s'exécute sur le processeur P3, non protégé. '''Question''' : Pourquoi a-t-on choisi de placer les 4 piles d'exécution dans 4 clusters différents ? '''Question''' : Pourquoi ne peut-on pas utiliser directement (c'est à dire sans décodage) les 2 bits de poids fort de l'adresse pour désigner le cluster ? '''Question''' : Proposez des adresses de base pour ces 16 segments, en tenant compte du fait que le crossbar local ne doit décoder que les bits (LADR) de l'adresse, et que le réseau global ne doit décoder que les bits (GADR) de l'adresse. Recommandation : on utilisera les 4 bits A[31:28] pour le champs GADR, en considérant que seuls les 2 bits A[29:28] sont réellement discriminants pour désigner le cluster visé. On utilisera les 4 bits A[27:24] pour le champs LADR. '''Question''' : Sur combien de bits doivent être codés les sous-champs GID et LID du champs VCI SRCID ? '''Question''' : Quelle critique peut-on formuler contre cette architecture, pour ce qui concerne le cablage des lignes d'interruption ? '''Question''' : Complétez le fichier '''tp5_top_cluster.cpp''' décrivant cette architecture. Il faut préciser les valeurs des adresses de base et les longueurs des segments. Il faut définir les arguments des constructeurs des composants matériels, et il faut définir la net-list. == 3.3 application '"hello" == Placez-vous dans le répertoire '''soft_hello'''. Pour valider l'architecture matérielle (y compris les interruptions), commencez par exécuter le programme d'affichage interactif du message ''hello world'', en parallèle sur chacun des 4 processeurs. Le routage des interruptions vers chacun des 4 processeurs est contrôlé par logiciel grâce aux registres de masque du composant ICU, qui sont définis dans le code de boot. On fera en sorte que le processeur P[i] recoive les interruptions IRQ_TTY[i], IRQ_TIM[i], et IRQ_DMA[i]. L'interruption IRQ_IOC sera routée vers le processeur P[0]. Le code de boot, contenu dans le fichier '''reset.s''', doit supporter des applications logicielles où les 4 processeurs exécutent 4 programmes différents. Les 4 processeurs exécutent le même code de boot (puisqu'ils se branchent à la même adresse 0xBFC00000), mais certaines actions dépendent du proc_id : * Chaque processeur initialise son propre pointeur de pile, à une valeur dépendante du proc_id. * Tous les processeurs initialisent, de façon concurrente, le vecteur d'interruptions. * Chaque processeur configure le registre de masque de son propre canal du concentrateur d'interruption. * Chaque processeur initialise son propre registre SR. * En sortie du code de boot, chaque processeur se branche à une adresse différente dans le code utilisateur, dépendante du proc_id. '''Question''': Pourquoi utilise-t-on une technique redondante où les 4 processeurs écrivent les mêmes valeurs pour initialiser le vecteur d'interruption? '''Question''': Complétez le code de boot dans le fichier '''reset.s'''. Modifiez le fichier '''seg.ld''' pour définir les adresses de bases des 16 segments. Modifiez le fichier '''config.h''' pour définir le nombre de processeurs. '''Question''' : lancez la simulation: Les quatre processeurs doivent exécuter le même programme interactif, chacun sur son propre terminal TTY. Si ce n'est pas le cas, vous pouvez soit activer la trace cycle par cycle, soit utiliser le '''GDB Server''', suivant que vous soupçonnez une erreur dans le matériel ou dans le logiciel... == 3.4 Application "transpose" == On veut maintenant exécuter une application parallèle multi-tâches coopérative, pour exploiter le parallélisme de l'architecture matérielle. On s'intéresse à une application de traitement d'image réalisant une transposition (X <-> Y) de chaque image d'un flux d'images chargées depuis le disque. On utilisera le même flux d'image que dans le TP4, c'est à dire le fichier '''images.raw''', qui contiennt une vingtaine d'images de 128 lignes de 128 pixels codées en 256 niveaux de gris. L'application logicielle contenue dans le fichier '''main.c''' du répertoire '''soft_transpose''' est découpée en trois tâches logicielles, qui doivvent s'exécuter en parallèle sur trois processeurs différents, le quatrième processeur étant non utilisé. Les trois tâches communiquent entre elles à travers deux tampons de communication en mémoire appelés'''buf_in''' et '''buf_out'''. Chaque tampon peut stocker une image complête [[Image(soclib_tp5_transpose.png)]] * La tâche '''load''' lit une image sur le disque et copie cette image dans le tampon buf_in. * La tâche '''transpose''' lit l'image présente dans le tampon buf_in, effectue la transposition et écrit l'image résultante dans le tampon buf_out. * La tâche '''display''' lit l'image présente dans le tampon buf_out, et affiche cette image sur l'écran graphique. en l'écrivant dans le frame buffer. Les deux tampons de communication buf_in et buf_out sont protégés par les deux variables de synchronisation '''buf_in_empty''' et '''buf_out_empty''', qui supportent un protocole de synchronisation de type SET/RESET en mode utilisateur: * La tâche productrice attend que la variable de synchronisation passe à 0 avant d'écrire, et force cette variable à 1 quand elle a fini de remplir le tampon. * La tâche consommatrice attend que la variable de synchronisation passe à 1 avant de lire, et force cette variable à 0 quand elle a fini de vider le tampon. '''Question''' : Modifiez le fichier '''main.c''' dans le répertoire '''soft_transpose''' pour introduire les synchronisations manquantes dans le code des tâches. '''Question''' : Modifiez le fichier '''reset.s''' dans le répertoire '''soft_transpose''' pour que les tâches '''load''', '''transpose''', et '''display''' s'exécutent sur les processeurs 0, 1 et 2 respectivement. '''Question''' : Lancez la simulation. Quelle est la fréquence d'affichage? (inverse du nombre de cycles entre deux affichages). Il faut évidemment définir sur la ligne de commande le nom du fichier contenant les images (paramètre -IOCFILE), ainsi que la taille du frame buffer (paramètre -FBFSIZE). '''Question''' : Pourquoi, dans le fichier '''tp5_cluster_top.cpp''' décrivant l'architecture matérielle, le segment '''seg_data''' a-t-il été défini comme non cachable? '''Question''' : L'inconvénient du mécanisme de synchronisation par bascule SET/RESET est que les deux tâches productrice et consommatrice ne peuvent s'exécuter en parallèle. Dans l'application logicielle proposée, quelles tâches s'exécutent effectivement en parallèle? '''Question''' : Comment pourrait-on modifier l'application logicielle pour augmenter le parallélisme et augmenter ainsi la fréquence d'affichage des images, sans augmenter le nombre de processeurs utilisés? = 4 Compte_rendu = Il ne vous est pas demandé de compte-rendu pour ce TP, mais on vous demandera une démonstration de votre simulateur au début du TP de la semaine suivante...