Filière CSI 3A Séance 3 : Principes du fonctionnement Multi-Tâches Ce TD porte sur les mécanismes permettant à un ordinateur monoprocesseur de fonctionner en mode «multi-tâches», c est à dire d exécuter plusieurs programmes utilisateurs simultanément. On utilise pour cela la technique de multiplexage temporel, ou le processeur travaille successivement pour différents programmes durant des tranches de temps de longueur fixe. L objectif du TD est d identifier tous les problèmes liés à la création, à la sauvegarde, et à la restauration de tâches logicielles, et d analyser en détail le mécanisme de commutation de tâches. Partie 1 : Le principe Soit la plate-forme matérielle décrite ci-dessous : processeur mémoire icu timer Timer0 TTY 0 TTY 1 TTY 2 caches Timer1 bcu Cette plate-forme comporte un processeur MIPS R3000 et ses caches, une mémoire, trois terminaux TTY permettant d afficher des caractères et de lire des caractères depuis le clavier, un timer et un contrôleur d interruption (ICU). Sur cette plateforme matérielle, deux tâches logicielles T0 et T1 s exécutent en pseudoparallélisme sur le même processeur. Une tâche logicielle est un programme en cours d exécution, et son contexte d exécution est défini par les valeurs des registres internes du processeur (en particulier son PC et son pointeur de pile), et les valeurs stockées dans sa pile d exécution. Ce système n utilisant pas de mécanisme de mémoire virtuelle, les deux piles des deux tâches T0 et T1 doivent être rangées à des adresses différentes en mémoire. Q1 : Rappeler le principe du «pseudo-parallélisme» par multiplexage temporel. Quel est l événement qui va provoquer la commutation de tâches? Correction Le parallélisme réel des tâches ne peut être obtenu qu avec une architecture matérielle multi-processeur. Dans une architecture mono-processeur, les deux tâches T0 et T1 doivent se partager l unique processeur. Une seule tâche est donc réellement en cours d exécution à un instant donné, même si du point de vue de l utilisateur les deux tâches semblent progresser de manière indépendante l une de l autre. Le temps est découpé en tranches de durées égales. Les différentes tâches s exécutent à tour de rôle Cours-TD Systèmes Embarqués page - 1
pendant une tranche de temps de quelques millisecondes. (une milliseconde représente 1 million de cycles sur un processeur à 1 GHz ). La fin d une tranche de temps est signalée par un interruption périodique générée par un timer. Q2 : A l aide du chronogramme ci-dessous, décrire les différentes étapes du mécanisme de commutation de tâches, en supposant que c est le timer1 qui génére l interruption périodique signalant la fin d une tranche de temps. Correction On suppose que le timer1 a été programmé pour générer une interruption tous les X cycles. Le rythme de la commutation de tâches entre T0 et T1 est donc donné par cette valeur X. Etape 1 : le processeur exécute la tâche T0 Etape 2 : une interruption est générée par le timer, elle traverse le contrôleur d interruption, et atteint le processeur, puisqu elle n est pas masquée. Le processeur est dérouté vers le gestionnaire d exceptions que vous avez déjà analysé. Etape 3 : Le gestionnaire d interruption interroge le composant ICU pour obtenir le numéro de l interruption active, et appelle ensuite la fonction ISR (Interrupt Service Routine) associée à celle-ci. Etape 4 : - Cette fonction ISR commence par acquitter l interruption du timer. - L'ISR obtient le numéro de la tâche interrompue, - elle sauvegarde les valeurs des registres du processeur constituant le contexte matériel de la tâche T0 dans une zone mémoire particulière dédiée à T0. Cette zone est un tableau de 33 entiers. En conséquence, toutes les informations qui permettront plus tard à T0 d être relancée sont sauvegardées. - l ISR choisit une nouvelle tâche (T1). Elle peut charger les registres du processeur avec les valeurs du contexte de T1 depuis une zone de mémoire dédiée à T1, un autre tableau de 33 entiers. - Finalement, le gestionnaire d exception exécute un saut à l'adresse EPC restaurée et l instruction rfe, qui redonne la main à la tâche T1. Etape 5 : Le processeur exécute la tâche T1 Etape 6 : après X cycles après, une nouvelle interruption arrive. Etape 7 : saut à isr_timer1 Etape 8 : sauvegarde du contexte de T1 et restitution de celui de T0 Etape 9 : etc Cours-TD Systèmes Embarqués page - 2
3 4 7 8 T1 5 T0 1 9 2 6 T (en cycles) On se propose maintenant d analyser en détail le code réalisant cette commutation de tâches. Ce code est composé de plusieurs fichiers dont la plupart sont déjà connus. Les fichiers «système» que vous ne devez pas modifier: giet.s contient le code du gestionnaire d interruptions (déjà étudié) syscalls_user.c contient les fonctions C encapsulant les appels systèmes syscalls_kernel.s contient les routines assembleur réalisant les appels systèmes. Les fichiers spécifiques à cette application sont : reset.s contient le code exécuté par le processeur lors de l initialisation du système. Dans cette section, on effectue principalement l'initialisation des périphériques matériels, c'est-à-dire l'initialisation du timer pour qu il génère une interruption tous les X cycles et l'initialisation de l ICU pour démasquer l interruption du timer, puis on initialise les zones de mémoire contenant les contextes matériels des tâches. On n'initialise pas la zone de contexte correspondant à la tâche qui sera lancée au démarrage. Enfin on initialise les registre nécessaire à la première tâche (TO) et on la lance. Ce fichier contient également la réservation de mémoire des piles de chacune des tâches, des zones de mémoire pour la sauvegarde des contextes matériels des tâches, et une variable globale définissant le numéro de la tâche en cours d exécution. tache.c contient le code source C de la tâche à exécuter. Ici T0 et T1 sont deux instances de la même tâche. Ce programme est une «mini-calculette», qui lit deux nombres saisis au clavier et affiche leur somme sur le terminal associé. Chaque tâche possède son propre terminal TTY écran/clavier. isr_timer1.s contient le code exécuté par le processeur lorsque survient une interruption du timer1. (ISR signifie «Interrupt Service Routine»). C est cette fonction Cours-TD Systèmes Embarqués page - 3
qui acquitte l interruption ET qui se charge du choix de la nouvelle tâche et de la commutation des contextes. Q3 : En examinant le code de giet.s, décrire précisément ce qui se passe quand une interruption du timer survient jusqu au branchement à la fonction isr_timer1. Analysez ensuite le code de la fonction isr_timer1 et vérifier que ce code correspond bien aux traitements définis dans la question Q2. Le code est celui donné en annexe Correction T0 s exécute 1 2 excep :=0x80000080 EPC, SR, CR modifiés $26, $27 registres systèmes modifiés _cause_int 3 4 _isr_timer1: - acquittement du timer - calcul de l'adresse de sauvegarde du contexte de la tâche courante - sauvegarde du contexe - choix d'une nouvelle tâche - calcul de l'adresse de restauration de la nouvelle tâche - restauration du contexte - retour à EPC adresse de l'instruction interrompue 5 T1 s exécute 1 : T0 est en train de s exécuter 2 : - L interruption du timer se produit. - Le processeur sauvegarde dans EPC l adresse de la prochaine instruction de T0 à exécuter. Il modifie SR (status register) pour un passage en mode superviseur et l'interdiction des interruptions et CR (cause register), puis il saute à l adresse du gestionnaire d exceptions 0x80000080. - Les seuls registres que l on s autorise à utiliser ici sont les registres système $26 et $27. Les autres n ont pas encore été sauvegardés, et toute altération de l un d eux nuirait au futur fonctionnement correct de T0. Comme cela a été vu précédemment, l analyse du registre CR permet de déterminer la cause de l appel au giet. - On saute donc directement à l adresse cause_int, début de la fonction de gestion des interruptions externes. Cours-TD Systèmes Embarqués page - 4
3 : - La fonction cause_int consulte l ICU pour déterminer l origine de l interruption. Ici, c est le timer1 qui a généré l interruption. Le tableau interrupt_vector qui a été construit à l initialisation dans le code de reset, contient le vecteur d interruption, qui définit les adresses des différentes fonctions ISR. Dans le code de reset, on a spécifié que l adresse de traitement de l interruption pour le timer était isr_timer1. Le processeur va donc exécuter la fonction isr_timer1. La encore, on se contente des registres $26 et $27 pour l'analyse et le saut à la fonction isr_timer1. 4 : - La fonction isr_timer1 commence par acquitter l interruption du timer. L acquittement consiste à écrire la valeur 0 dans un registre du timer pour désarmer la requête d interruption - La fonction isr_timer1 obtient ensuite le numéro de la tâche courante et calcule l'adresse de la zone mémoire où le contexte va devoir être écrit. La variable globale thread_current contient le numéro de la tâche en cours. La variable globale thread_context pointe sur le tableau des contextes. Le contexte courant devra être rangé à l'adresse thread_context + 33*thread_curent*4. Le calcul effectué est du genre: tmp=thread_curent*4 ; tmp=tmp * 32 + tmp $27=tmp + thread_context - Pour la sauvegarde du contexte de la tâche sortante, les cases 0, 26, 27 et 28 du tableau servent à sauvegarder respectivement SR, LO, HI et CR. On utilise des offsets par rapport au registre de base $27 (pointeur sur le début de la zone de sauvegarde) pour sauver chaque registre. En particulier, on sauve le pointeur de pile $29 dans l élément 29 du tableau. On sauve EPC l'adresse de retour de la tâche sortante dans la case 33. - Le choix de la prochaine tâche à exécuter est extrêmement simple dans ce cas particulier où il n y a que deux tâches. Comme on exécutait la tâche T0, c est la tâche T1 qui doit reprendre son exécution. Après quelques instructions le registre $26 contient le numéro de la nouvelle tâche. Ce numéro est inscrit dans la variable thread_current. - Il reste à calculer l'adresse de la zone mémoire contenant la valeur des registres à restaurer pour la nouvelle tâche. L'adresse est mise dans $27, elle est obtenue par thread_context + 33*thread_curent*4 - Pour la restauration, on effectue l opération inverse à partir du tableau pointé par $27. La restauration de EPC permet de connaître l'adresse de retour de l'isr. - L'ISR se termine par un saut à l'adresse EPC et une restauration du status par rfe. 32 7 6 5 4 3 2 1 0 $7 $6 $5 $4 $3 $2 $1 SR 15 14 13 12 11 10 9 8 $15 $14 $13 $12 $11 $10 $9 $8 23 22 21 20 19 18 17 16 $23 $22 $21 $20 $19 $18 $17 $16 31 30 29 28 27 26 25 24 EPC $31 $30 $29 CR HI LO $25 $24 thread_context[thread_current] Cours-TD Systèmes Embarqués page - 5
5 : Le saut à l'adresse EPC permet de revenir dans la tâche qui avait été interrompue. On est à nouveau en mode user avec interruptions autorisées. Q4 : Comment faut-il initialiser le contexte de chaque tâche? Compléter le code en assembleur dans la routine reset. Que se passe t il lors de la première commutation, quand T1 remplace T0? ------------------------------------------------------------------------------- reset.s system initialization -------------------------------------------------------------------------------.section.reset,"ax",@progbits.align 2.set noreorder $28 pointeur des variables globales la $28, _gp global pointer install isr vector la $26, interrupt_vector la $27, isr_timer0 sw $27, ($26) la $27, isr_timer1 sw $27, 4($26) init thread contexts la $26, thread_context_1 address of context array ori $27, sw $27, 0($26) init SR li $27, sw $27, 4*4($26) init $4 argument of task la $27, sw $27, 29*4($26) init stack pointer la $27, sw $27, 32*4($26) init return addressd init peripherals la $26, seg_timer_base address of timer n 0 li $27, 0x100000 new period sw $27, 8($26) init period ori $27, $0, 0x3 mode= timer running / IRQ enabled sw $27, 4($26) start the timer la $26, seg_timer_base + 0x10 address of timer n 1 li $27, 0x1000 new period sw $27, 8($26) init period ori $27, $0, 0x3 mode= timer running / IRQ enabled sw $27, 4($26) start the timer la $26, seg_icu_base address of icu ori $27, $0, 0x2 set the mask sw $27, 4($26) set context of the task n 0 Cours-TD Systèmes Embarqués page - 6
la $26, _ initialisation du numero du thread sw $0, ($26) li $26, _ initialisation SR mtc0 $26,$12 la $29, _ initialisation du pointeur de pile li $4, _ initialisatio de l'argument de la tâche la $27, _ saut à la tache jr $27 rfe passage en mode user.set reorder -------------------------------------------------------------------------------.section.local,"aw",@progbits.globl thread_stack_0.globl thread_stack_1.space 1024*4 thread_stack_0 :.space 1024*4 thread_stack_1 : -------------------------------------------------------------------------------.section.klocal, "aw",@progbits.globl thread_context.globl thread_context_0.globl thread_context_1.globl thread_current thread_context: thread_context_0:.space 33*4 thread_context_1:.space 33*4 thread_current:.space 4 Correction Il faut initialiser les cases qui correspondent aux registres $29, SR, $4 et EPC, pour toutes les tâches sauf celle sui sera lancée par la routine reset. Donc ici on initialise que le contexte de T1: $29: Chaque tâche doit avoir sa propre pile. Il faut donc deux pointeurs de pile différents pour T0 et T1 ce qui se traduit par l affectation de l élément 29 de chaque tableau. Les piles des tâche sont déclarées dans la section local dans le fichier reset.s. Pour T0, le pointeur de pile est (thread_stack_0 4), et pour T1, le pointeur de pile sera (thread_stack_1 4). Le - 4 est nécessaire car c'est la case réservée dans la pile pour le paramètre de la fonction tache(). $4 : L élément 4 de chaque tableau contient le paramètre qui identifie la tâche, c'est-àdire le numéro du terminal associé à la tâche où se feront les affichages et les lectures du clavier. T0 utilisera TTY0 et T1 utilisera TTY1. EPC:L élément 32 de chaque tableau doit contenir l adresse où commence le code de la tâche. C est un pointeur vers la fonction tache. SR : L élément 0 de chaque tableau correspond au registre SR. En mettant 0xFF3C dans SR on met les 2 tâches en mode super-utilisateur. En effet, la copie d'un contexte se fait toujours en mode super-utilisateur avec interruptions interdites. Le passage en mode utilisateur sera réalisé lors de l'exécution de l'instruction rfe. Cours-TD Systèmes Embarqués page - 7
A la fin de la routine d'initialisation, on souhaite démarrer la tâche T0. Il suffit pour cela d'initialiser le registre $29, $4, SR et de sauter à l'adresse T0 en exécutant l'instruction rfe dans la foulée. Cours-TD Systèmes Embarqués page - 8