|
|||||||||||||||||||||||||||||||||||||||||||||||||||
|
L'Amiga utilise de manière peu orthodoxe les sept niveaux d'interruption du 68000. Grâce à Paula, il est capable de délivrer pas moins de 15 sources d'interruptions différentes, classées par priorité. Comme on était en droit de s'y attendre, Exec tire pleinement parti de cette particularité du matériel, à tel point que les interruptions sont la base même de son noyau multitâche. Exec décode les demandes d'interruptions, les évalue et se branche sur les routines adéquates, en fonction d'un certain nombre de paramètres que vous allons voir ici. En plus des niveaux de priorité, Exec distingue deux types d'interruptions : les interruptions en provenance du matériel, bien sûr, mais aussi celles provoquées par logiciel. Enfin. Exec permet l'inhibition complète et la restitution de toutes les interruptions sur simple demande d'une application. Comment ça marche Avant qu'une routine d'interruption ne soit appelée, plusieurs étapes, tant matérielles que logicielles, sont exécutées. La séquence exacte de ce qui se passe est la suivante : 1. Un périphérique quelconque décide de causer une interruption et envoie pour ce faire un signal à Paula. 2. Paula prend en compte cette demande et la "note" en positionnant le bit correspondant du registre matériel INTREQ. Puis elle examine le bit correspondant dans INTENA pour déterminer si cette interruption est autorisée ou non. Si et seulement si c'est le cas, Paula la transmet au 68000, non sans avoir auparavant transformé le niveau de priorité en un acceptable par le microprocesseur. 3. Le 68000 décode à son tour cette demande d'interruption et détermine si elle est valide ou non. Une interruption n'est valide que lorsque son niveau de priorité est supérieur à celui dans lequel se trouve le microprocesseur au moment de la demande. 4. Si la demande d'interruption est valide, le processeur passe alors en mode superviseur (s'il ne l'était pas déjà), et sauvegarde sur la pile les informations relatives à son état actuel (registres SR et PC), puis il se place dans le même niveau de priorité que celui de l'interruption. 5. Le 68000 cherche maintenant dans sa table d'auto-vecteurs d'interruptions, l'adresse de la routine à exécuter (note sur un 68000, cette table se trouve aux adresses $64 à $7C incluses. Sur un 68020 ou 68030, un registre supplémentaire nommé VBR - pour Vector Base Register - permet de reloger cette table n'importe où dans l'espace d'adressage du processeur). 6. Le 68000 saute à l'adresse lue dans la table. Il s'y trouve une routine d'Exec qui doit maintenant décoder à nouveau cette demande d'interruption, afin d'en retrouver l'originaire. Ceci est réalisé au moyen des registres matériels INTREQ et INTENA. Une fois l'origine de l'interruption déterminée, Exec recherche dans la structure ExecBase l'adresse de la routine à exécuter pour ce type d'interruption. 7. Exec appelle finalement cette routine qui peut, dans le cadre de serveurs d'interruption, en appeler encore d'autres. Comme on peut le voir, cela est loin d'être simple ! Au retour de chaque interruption, Exec détermine si le temps imparti à la tâche en cours est écoulé ou non, ou si une tâche a reçu un signal qu'elle attendait (via Wait() ou WaitPort()), auquel cas le gestionnaire de tâches est appelé et le temps d'occupation du processeur pour chaque tâche est recalculé. Comme vous le voyez, Exec repose lourdement sur les interruptions pour gérer son multitâche. Si, pour une raison quelconque, les interruptions ne survenaient plus, la tâche courante utiliserait tout le temps processeur pour elle toute seule, plus rien ne venant la forcer à l'abandonner quelques instants. Les priorités des interruptions Les interruptions sont classées par priorité à deux niveaux : d'abord par le 68000 lui-même, puis par le logiciel qui, à l'intérieur de chaque niveau de priorité du processeur, introduit des pseudo-priorités. Ces pseudo-priorités déterminent l'ordre d'exécution de chaque interruption pour une priorité 68000 donnée. Il existe en fait deux types de gestionnaires d'interruptions : les "handlers" (gestionnaires), qui en sont les propriétaires exclusifs, et les serveurs, qui se partagent l'interruption. Le tableau ci-dessous indique pour chaque niveau d'interruption, son type de gestionnaire. ![]() Contrôle des interruptions Revenons maintenant d'un peu plus près à Exec. Pour mettre en place une interruption, il faut remplir devinez quoi ? Une structure, oui. Elle se nomme Interrupt et est définie ainsi : ![]() Note : très peu de fonctions du système d'exploitation peuvent être appelées depuis une interruption. Notamment, toutes les fonctions faisant appel, directement ou indirectement, au gestionnaire de mémoire d'Exec sont à prohiber. En général, les fonctions suivantes sont utilisables sans danger depuis une interruption : Alert(), Disable(), Enable(), Signal(), Cause(), PutMsg(), RelPlyMsg(), FindPort(), FindTask(). Les gestionnaires Comme on l'a déjà mentionné auparavant, un "handler" (gestionnaire) d'interruption est une routine qui gère exclusivement cette interruption. Il ne peut y avoir qu'un seul gestionnaire pour une interruption donnée. Un gestionnaire est appelé par Exec comme s'il s'agissait d'une sous-routine avec une instruction JSR. La dernière instruction du gestionnaire doit donc être RTS, et non RTE. En entrée, certains registres du processeur contiennent des informations pertinentes : ce sont notamment A0, qui pointe sur l'adresse de base des puces spécialisées ($DFF000), A1 qui contient le champ is_Data de la structure Interrupt, A5 pointe sur la routine d'interruption elle-même (Exec effectue en effet un JSR (A5)) et A6 pointe sur ExecBase. Les registres D0-D1/A0-A1/A5-A6 sont librement modifiables, les autres doivent absolument être préservés. De plus, il appartient au gestionnaire d'effacer la demande d'interruption dans le registre matériel INTREQ. On installe un gestionnaire à l'aide de la fonction SetIntVector(), qui s'utilise comme suit :
"IntNumber" contient le numéro de l'interruption désirée, par exemple INTB_BLIT (voir "hardware/intbits.h" et "hardware/intbits.i"), et "interrupt" pointe sur votre structure Interrupt, correctement initialisée. Cette fonction retourne un pointeur sur la structure Interrupt du gestionnaire précédemment installé, s'il y en avait un. Il ne faudra pas oublier de remettre le vecteur dans son état initial lorsque votre programme se terminera, avec SetIntVector(IntNum, oldHandler). Les serveurs Contrairement aux gestionnaires, les serveurs d'interruptions se partagent une même interruption. Exec maintient une liste des serveurs déclarés pour chaque interruption, et les appelle tour à tour. Rappelons toutefois que seules les interruptions PORTS, COPER, VERTB, EXTER et NMI supportent des serveurs ; les autres requièrent obligatoirement un gestionnaire. Avant d'appeler le serveur suivant dans la chaîne, Exec teste le drapeau Z du 68000 ; si ce drapeau n'est pas positionné, Exec arrête là le traitement de l'interruption. Un moyen simple de positionner le drapeau Z consiste à utiliser l'instruction MOVEQ :
Ou :
Comme pour un gestionnaire, un serveur se termine obligatoirement par une instruction RTS. Par contre, seuls les registres A1 et A5 contiennent une information valide : A1 contient le champ is_Data de la structure Interrupt et A5 pointe sur la routine d'interruption elle-même. Il ne faut pas s'attendre à trouver quoique ce soit de particulier dans A0 ni dans A6 (en fait, seul le premier serveur de la chaîne, celui qui a la plus haute priorité, reçoit effectivement l'adresse des puces spécialisées dans A0 et un pointeur sur ExecBase dans A6. Mais ces registres étant librement modifiables par le serveur, rien ne garantit aux suivants qu'ils seront encore valides). Note : un bogue dans le serveur d'interruption VERTB de la graphics.library assume que quoiqu'il arrive, A0 pointe sur les puces spécialisées ($DFF000). La priorité de ce serveur est de 10. Donc, si vous ajoutez un serveur VERTB de priorité supérieure à 10, vous devez absolument terminer votre routine par les trois instructions : ![]() On installe un serveur d'interruption à l'aide de la fonction AddIntServer(), qui s'utilise comme suit :
Les paramètres sont les mêmes que pour SetIntVector(), à la différence qu'AddIntServer() ne retourne aucune valeur en réponse. Pour enlever votre serveur de la chaîne en fin de programme, utilisez RemIntServeur(IntNum, interrupt). Listings Bon, on arrête le massacre je vous laisse le loisir de découvrir un petit programme de mon cru qui initialise et ajoute un serveur VERTB. Le programme principal est en C et la routine d'interruption, en assembleur. Listing 1
Listing 2
Listing 3 (makefile pour IntServ)
Mise à jour de mai 2025 : une archive contenant le listing adapté à vbcc, et avec l'exécutable compilé par vbcc, a été réalisée par Yann-Gaël Guéhéneuc et est disponible sur obligement.free.fr/files/antinterruptions.lha. Interruptions générées par Exec Les différentes interruptions que nous avons vues jusqu'à présent étaient toutes générées par Paula, suite à un évènement matériel. Mais Exec permet également la génération d'interruptions totalement logicielles. On ne voit pas très bien à priori à quoi des interruptions logicielles peuvent bien servir... Alors qu'il est souvent primordial de pouvoir réagir au quart de millionième de seconde près au VBL ou à la fin d'une transmission série, un petit délai (dû à un grand nombre de tâches tournant concurremment) dans un protocole de communication entre deux processus n'est que très rarement pénalisant. En fait, c'est justement là leur utilité : permettre de synchroniser le plus justement possible deux tâches entre elles. Le principe est très simple : étant donné que les messages sont chaînés les uns à la suite des autres dans un port, un message particulier peut mettre un certain temps avant d'être traité par la tâche réceptrice. Or, dans certains cas, il est primordial que ce message soit immédiatement pris en compte, quitte à ce que les autres patientent un peu, bien au chaud qu'ils sont dans leur liste d'attente. Pour ce faire, on décide de suspendre provisoirement le système multitâche - grâce à une interruption, donc - et de gérer tout de suite l'évènement intervenu. On le voit, les interruptions logicielles agissent exactement comme les interruptions matérielles, à ceci près qu'elles sont générées non pas par le matériel, mais par le logiciel (en fait, elles utilisent le niveau d'interruption 1, bit 2 de INTENA). Mise en oeuvre d'une interruption logicielle Une interruption logicielle s'initialise exactement de la même manière qu'une interruption matérielle, au moyen d'une structure Interrupt correctement remplie. Attention ici à bien spécifier dans le noeud, le type NT_INTERRUPT et non NT_SOFTINT, qui est un drapeau interne à Exec. Seules cinq priorités sont possibles : -32, -16, 0, +16 et +32 ; toute autre valeur est illégale. Les champs is_Code et is_Data sont initialisés comme pour une interruption matérielle. Une fois déclenchée, l'interruption logicielle s'exécute dans un environnement identique à celui des interruptions matérielles : mode superviseur, retour au gestionnaire d'Exec par l'instruction RTS et protection obligatoire des registres A2-A4 et D2-D7. Si elle survient en cours de fonctionnement "normal" du système (mode multitâche), l'interruption est exécutée immédiatement ; sinon, si elle est déclenchée au cours d'une autre interruption logicielle, elle devra attendre que celle-ci se termine ; enfin, si elle est déclenchée au cours d'une interruption matérielle, elle devra attendre que tous les serveurs éventuels restant à exécuter aient fini leur travail. Ceci permet par exemple à une interruption de haut niveau de se décharger d'une tâche ingrate et gourmande en temps-machine sur une autre interruption de priorité moindre. Il existe deux moyens de déclencher une interruption logicielle : soit en appelant la fonction Cause(), soit en postant un message dans un port de type PA_SOFTINT. Dans le premier cas, on transmet à Cause() l'adresse de la structure Interrupt concernée et l'interruption se déclenche suivant les conditions énoncées plus haut. Dans le second cas, on initialise un MsgPort comme illustré ci-dessous : ![]() ![]() Emploi avec les périphériques logiciels Comme les interruptions matérielles, les interruptions logicielles ne peuvent appeler qu'un nombre réduit de fonctions du système. Une particularité est à noter concernant les devices : normalement, il est interdit d'utiliser DoIO() et BeginIO() dans une routine d'interruption, mais le timer.device et l'audio.device font exception à cette règle. Cela permet par exemple de relancer directement une requête de l'un de ces devices depuis l'interruption, sans que le programme principal n'ait besoin d'intervenir. L'exemple Comme le veut la tradition, nous allons terminer cette étude des interruptions logicielles par un petit exemple. En l'occurrence, il ne s'agit ni plus ni moins que d'un petit compteur de 1/2 secondes. On aurait certes pu le programmer d'une autre manière, mais c'est la seule idée de programme qui me soit passé par la tête...
|