Escalade d'Elbrus - Reconnaissance au combat. Partie technique 2. Interruptions, exceptions, minuterie système

Nous continuons d'explorer Elbrus en y portant Embox .

Cet article est la deuxième partie d'un article technique sur l'architecture Elbrus. La première partie portait sur les piles, les registres, etc. Avant de lire cette partie, nous vous recommandons d'étudier la première, car elle traite des éléments de base de l'architecture Elbrus. Cette section se concentrera sur les temporisateurs, les interruptions et les exceptions. Encore une fois, ce n'est pas une documentation officielle. Pour cela, vous devez contacter les développeurs d'Elbrus à l' ICST .
Pour en venir à l'étude d'Elbrus, nous voulions démarrer rapidement le chronomètre, car, comme vous le savez, le multitâche préemptif ne fonctionne pas sans lui. Pour ce faire, il semblait suffisant d'implémenter le contrôleur d'interruption et le temporisateur lui-même, mais nous avons rencontré des difficultés inattendues , où irions-nous sans eux. Ils ont commencé à rechercher des capacités de débogage et ont découvert que les développeurs s'en sont occupés en introduisant plusieurs commandes qui vous permettent de déclencher diverses situations exceptionnelles. Par exemple, vous pouvez générer une exception d'un type spécial via les registres PSR (registre d'état du processeur) et UPSR (registre d'état du processeur utilisateur). Pour PSR, le bit exc_last_wish est l'indicateur d'exception exc_last_wish lors du retour de la procédure, et pour UPSR, exc_d_interrupt est l'indicateur d'interruption retardée générée par l'opération VFDI (Vérifier l'indicateur d'interruption retardée).

Le code est le suivant:

#define UPSR_DI (1 << 3) /*   .h  */ rrs %upsr, %r1 ors %r1, UPSR_DI, %r1 /* upsr |= UPSR_DI; */ rws %r1, %upsr vfdi /*      */ 

Lancé. Mais rien ne s'est produit, le système s'est bloqué quelque part, rien n'a été émis vers la console. En fait, nous l'avons vu lorsque nous avons essayé de démarrer l'interruption à partir de la minuterie, mais il y avait alors de nombreux composants, et ici, il était clair que quelque chose interrompait la progression séquentielle de notre programme, et le contrôle a été transféré vers la table des exceptions (en termes d'architecture Elbrus, il est plus correct de ne pas parler de la table interruptions et sur le tableau des exceptions). Nous avons supposé que le processeur avait néanmoins levé une exception, mais il y avait des «déchets» où il a transféré le contrôle. En fait, il transfère le contrôle à l'endroit même où nous avons mis l'image Embox, ce qui signifie qu'il y avait un point d'entrée - la fonction d'entrée.

Pour vérification, nous avons fait ce qui suit. Démarré un compteur d'entrées dans l'entrée (). Initialement, tous les processeurs commencent par des interruptions désactivées, entrent dans entry (), après quoi nous ne laissons qu'un seul cœur actif, tous les autres entrent dans une boucle sans fin. Une fois que le compteur est égal au nombre de CPU, nous considérons que tous les hits suivants en entrée sont des exceptions. Je vous rappelle qu'avant c'était comme décrit dans notre tout premier article sur Elbrus

  cpuid = __e2k_atomic32_add(1, &last_cpuid); if (cpuid > 1) { /* XXX currently we support only single core */ while(1); } /* copy of trap table */ memcpy((void*)0, &_t_entry, 0x1800); kernel_start(); 

L'a fait

  /* Since we enable exceptions only when all CPUs except the main one * reached the idle state (cpu_idle), we can rely that order and can * guarantee exceptions happen strictly after all CPUS entries. */ if (entries_count >= CPU_COUNT) { /* Entering here because of expection or interrupt */ e2k_trap_handler(regs); ... } /* It wasn't exception, so we decide this usual program execution, * that is, Embox started on CPU0 or CPU1 */ e2k_wait_all(); entries_count = __e2k_atomic32_add(1, &entries_count); if (entries_count > 1) { /* XXX currently we support only single core */ cpu_idle(); } e2k_kernel_start(); } 

Et enfin, nous avons vu la réaction à l'entrée de l'interruption (juste avec l'aide de printf, nous avons imprimé une ligne).

Ici, il convient d'expliquer qu'au départ, dans la première version, nous nous attendions à copier le tableau des exceptions, mais d'une part, il s'est avéré que c'était à notre adresse, et d'autre part, nous n'avons pas pu faire la copie correcte. J'ai dû réécrire les scripts de l'éditeur de liens, le point d'entrée dans le système et le gestionnaire d'interruption, c'est-à-dire que j'avais besoin de la partie assembleur, à ce sujet un peu plus tard.

Voici à quoi ressemble maintenant la partie de la partie modifiée de l'éditeur de liens de script:

 .text : { _start = .; _t_entry = .; /* Interrupt handler */ *(.ttable_entry0) . = _t_entry + 0x800; /* Syscall handler */ *(.ttable_entry1) . = _t_entry + 0x1000; /* longjmp handler */ *(.ttable_entry2) . = _t_entry + 0x1800; _t_entry_end = .; *(.e2k_entry) *(.cpu_idle) /* text */ } 

c'est-à-dire que nous avons supprimé la section d'entrée de la table des exceptions. La section cpu_idle s'y trouve également pour les CPU qui ne sont pas utilisés.

Voici à quoi ressemble la fonction d'entrée pour notre noyau actif, sur lequel Embox s'exécutera:

 static void e2k_kernel_start(void) { extern void kernel_start(void); int psr; /*    CPU “” */ while (idled_cpus_count < CPU_COUNT - 1) ; ... /*     ,     */ e2k_upsr_write(e2k_upsr_read() & ~UPSR_FE); kernel_start(); /*   Embox */ } 

Eh bien, selon l'instruction VFDI, une exception a été levée. Vous devez maintenant obtenir son numéro pour vous assurer qu'il s'agit de la bonne exception. Pour cela, Elbrus dispose de registres d'informations d'interruption TIR (Trap Info registers). Ils contiennent des informations sur les dernières commandes, c'est-à-dire la dernière partie de la trace. Trace se rassemble pendant l'exécution du programme et «se bloque» lors de la saisie d'une interruption. TIR comprend les parties basse (64 bits) et haute (64 bits). Le mot bas contient les drapeaux d'exception et le mot haut contient un pointeur vers l'instruction qui a conduit à l'exception et le numéro TIR actuel. Par conséquent, dans notre cas, exc_d_interrupt est le 4e bit.

Remarque Nous avons encore quelques malentendus concernant la profondeur (nombre) des TIR. La documentation fournit:
"La profondeur de la mémoire TIR, c'est-à-dire le nombre de registres d'informations sur les pièges, est déterminée
Macro TIR_NUM égale au nombre d'étages de pipeline de processeur requis pour
émettre toutes les situations spéciales possibles. TIR_NUM = 19; "
En pratique, nous voyons la profondeur = 1, et donc nous utilisons uniquement le registre TIR0.

Les spécialistes du MCST nous ont expliqué que tout est correct, et il n'y aura de TIR0 que pour les interruptions «précises», mais pour d'autres situations, il peut y avoir autre chose. Mais comme nous ne parlons que d'interruptions de temporisation, cela ne nous dérange pas.

Ok, regardons maintenant ce qui est nécessaire pour entrer / quitter correctement le gestionnaire d'exceptions. En fait, il est nécessaire de sauvegarder en entrée et de restaurer les 5 registres suivants en sortie. Trois registres de préparation de transfert de contrôle sont ctpr [1,2,3], et deux registres de contrôle de cycle sont ILCR (registre des valeurs initiales du compteur de cycles) et LSR (registre de l'état du cycle).

 .type ttable_entry0,@function ttable_entry0: setwd wsz = 0x10, nfx = 1; rrd %ctpr1, %dr1 rrd %ctpr2, %dr2 rrd %ctpr3, %dr3 rrd %ilcr, %dr4 rrd %lsr, %dr5 /* sizeof pt_regs */ getsp -(5 * 8), %dr0 std %dr1, [%dr0 + PT_CTRP1] /* regs->ctpr1 = ctpr1 */ std %dr2, [%dr0 + PT_CTRP2] /* regs->ctpr2 = ctpr2 */ std %dr3, [%dr0 + PT_CTRP3] /* regs->ctpr3 = ctpr3 */ std %dr4, [%dr0 + PT_ILCR] /* regs->ilcr = ilcr */ std %dr5, [%dr0 + PT_LSR] /* regs->lsr = lsr */ disp %ctpr1, e2k_entry ct %ctpr1 

En fait, c'est tout, après avoir quitté le gestionnaire d'exceptions, vous devez restaurer ces 5 registres.

Nous le faisons avec une macro:

 #define RESTORE_COMMON_REGS(regs) \ ({ \ uint64_t ctpr1 = regs->ctpr1, ctpr2 = regs->ctpr2, \ ctpr3 = regs->ctpr3, lsr = regs->lsr, \ ilcr = regs->ilcr; \ /* ctpr2 is restored first because of tight time constraints \ * on restoring ctpr2 and aaldv. */ \ E2K_SET_DSREG(ctpr1, ctpr1); \ E2K_SET_DSREG(ctpr2, ctpr2); \ E2K_SET_DSREG(ctpr3, ctpr3); \ E2K_SET_DSREG(lsr, lsr); \ E2K_SET_DSREG(ilcr, ilcr); \ }) 

Il est également important de ne pas oublier après la restauration des registres d'appeler l'opération DONE (Return from the hardware interrupt handler). Cette opération est notamment nécessaire pour traiter correctement les opérations de transfert de commande interrompues. Nous le faisons avec une macro:

 #define E2K_DONE \ do { \ asm volatile ("{nop 3} {done}" ::: "ctpr3"); \ } while (0) 

En fait, nous faisons le retour de l'interruption directement en code C en utilisant ces deux macros.
  /* Entering here because of expection or interrupt */ e2k_trap_handler(regs); RESTORE_COMMON_REGS(regs); E2K_DONE; 

Interruptions externes


Commençons par la façon d'activer les interruptions externes. Dans Elbrus, APIC (ou plutôt son analogue) est utilisé comme contrôleur d'interruption; Embox avait déjà ce pilote. Par conséquent, il était possible de prendre un minuteur système pour cela. Il y a deux minuteries, l'une qui est très similaire à PIT , l'autre LAPIC Timer , est également assez standard, donc cela n'a aucun sens d'en parler. Cela et cela semblaient simples, et cela et cela existaient déjà dans Embox, mais le pilote du temporisateur LAPIC semblait plus en perspective, en plus de la mise en œuvre du temporisateur PIT nous semblait plus non standard. Par conséquent, cela semblait plus facile à réaliser. De plus, la documentation officielle décrivait les registres APIC et LAPIC, qui étaient légèrement différents des originaux. Les apporter n'a aucun sens, comme vous pouvez le voir dans l'original.

En plus d'autoriser les interruptions dans APIC, vous devez activer la gestion des interruptions via les registres PSR / UPSR. Les deux registres ont des drapeaux pour activer les interruptions externes et les interruptions non masquables. MAIS ici, il est très important de noter que le registre PSR est local à la fonction (cela a été discuté dans la première partie technique ). Et cela signifie que si vous la placez dans une fonction, alors lorsque vous appelez toutes les fonctions suivantes, elle sera héritée, mais lorsque vous reviendrez de la fonction, elle reviendra à son état d'origine. D'où la question, mais comment gérer les interruptions?

Nous utilisons la solution suivante. Le registre PSR vous permet d'activer la gestion via UPSR, qui est déjà mondial (dont nous avons besoin). Par conséquent, nous activons le contrôle via UPSR directement (important!) Avant la fonction de connexion de base Embox:

  /* PSR is local register and makes sense only within a function, * so we set it here before kernel start. */ asm volatile ("rrs %%psr, %0" : "=r"(psr) :); psr |= (PSR_IE | PSR_NMIE | PSR_UIE); asm volatile ("rws %0, %%psr" : : "ri"(psr)); kernel_start(); 

D'une certaine manière, par hasard, après refactoring, j'ai pris et mis ces lignes dans une fonction distincte ... Et le registre est local à la fonction. Il est clair que tout est cassé :)

Donc, tout semble être activé dans le processeur, allez au contrôleur d'interruption.

Comme nous l'avons vu ci-dessus, les informations sur le numéro d'exception figurent dans le registre TIR. En outre, le 32e bit de ce registre signale qu'une interruption externe s'est produite.

Après avoir allumé la minuterie, quelques jours de tourments ont suivi, car aucune interruption n'a pu être obtenue. La raison était assez amusante. Il y a des pointeurs 64 bits dans Elbrus, et l'adresse de registre dans APIC est entrée dans uint32_t, c'est pourquoi nous les avons utilisés. Mais il s'est avéré que si vous avez besoin, par exemple, de convertir 0xF0000000 en un pointeur, vous n'obtiendrez pas 0xF0000000, mais 0xFFFFFFFFF0000000. Autrement dit, le compilateur étendra votre signe int non signé.

Ici, bien sûr, il était nécessaire d'utiliser uintptr_t, car, comme il s'est avéré, dans la norme C99, ce type de distribution est défini par l'implémentation.

Après avoir finalement vu le 32ème bit relevé dans TIR, nous avons commencé à chercher comment obtenir le numéro d'interruption. Cela s'est avéré assez simple, bien que pas du tout comme sur x86, c'est l'une des différences entre les implémentations LAPIC. Pour Elbrus, pour obtenir le numéro d'interruption, vous devez entrer dans le registre spécial LAPIC:

  #define APIC_VECT (0xFEE00000 + 0xFF0) 

où 0xFEE00000 est l'adresse de base des registres LAPIC.

C'est tout, il s'est avéré prendre à la fois le minuteur du système et le minuteur LAPIC.

Conclusion


Les informations fournies dans les deux premières parties techniques de l'article sur l'architecture Elbrus sont suffisantes pour implémenter les interruptions matérielles et le multitâche préemptif dans n'importe quel système d'exploitation. En fait, les captures d'écran données en témoignent.



Ce n'est pas la dernière partie technique de l'architecture Elbrus. Maintenant que nous maîtrisons la gestion de la mémoire (MMU) à Elbrus, nous espérons en parler bientôt. Nous en avons besoin non seulement pour la mise en œuvre des espaces d'adressage virtuels, mais aussi pour le travail normal avec les périphériques, car grâce à ce mécanisme, vous pouvez désactiver ou activer la mise en cache d'une zone spécifique de l'espace d'adressage.

Tout ce qui est écrit dans l'article se trouve dans le référentiel Embox . Vous pouvez également créer et exécuter, s'il existe bien sûr une plate-forme matérielle. Certes, un compilateur est nécessaire pour cela, et il ne peut être obtenu qu'au MCST . La documentation officielle peut y être demandée.

Source: https://habr.com/ru/post/fr447744/


All Articles