
Continuamos explorando
Elbrus portándole
Embox .
Este artículo es la segunda parte de un artículo técnico sobre la arquitectura de Elbrus. La
primera parte trataba de pilas, registros, etc. Antes de leer esta parte, le recomendamos que estudie la primera, ya que habla sobre las cosas básicas de la arquitectura de Elbrus. Esta sección se centrará en temporizadores, interrupciones y excepciones. Esto, nuevamente, no es documentación oficial. Para ello, debe contactar a los desarrolladores de Elbrus en el
ICST .
Al llegar al estudio de Elbrus, queríamos iniciar rápidamente el temporizador porque, como saben, la multitarea preventiva no funciona sin ella. Para hacer esto, parecía suficiente implementar el controlador de interrupción y el temporizador en sí, pero nos topamos con
inesperadas dificultades esperadas, ¿a dónde iríamos sin ellas? Comenzaron a buscar capacidades de depuración y descubrieron que los desarrolladores se encargaron de esto mediante la introducción de varios comandos que le permiten generar varias situaciones excepcionales. Por ejemplo, puede generar una excepción de un tipo especial a través de los registros PSR (Registro de estado del procesador) y UPSR (Registro de estado del procesador del usuario). Para PSR, el bit exc_last_wish es el indicador de excepción exc_last_wish al regresar del procedimiento, y para UPSR, el exc_d_interrupt es el indicador de interrupción retardada generado por la operación VFDI (Comprobar indicador de interrupción retardada).
El código es el siguiente:
#define UPSR_DI (1 << 3) rrs %upsr, %r1 ors %r1, UPSR_DI, %r1 rws %r1, %upsr vfdi
Lanzado Pero no pasó nada, el sistema se colgó en alguna parte, no se emitió nada a la consola. En realidad, vimos esto cuando intentamos iniciar la interrupción desde el temporizador, pero luego había muchos componentes, y aquí estaba claro que algo interrumpía el progreso secuencial de nuestro programa, y el control se transfirió a la tabla de excepciones (en términos de arquitectura de Elbrus, es más correcto no hablar de la tabla interrupciones sobre una tabla de excepción). Asumimos que, sin embargo, el procesador lanzó una excepción, pero hubo algo de "basura" a la que transfirió el control. Resultó que transfiere el control al mismo lugar donde colocamos la imagen Embox, lo que significa que había un punto de entrada: la función de entrada.
Para la verificación, hicimos lo siguiente. Comenzó un contador de entradas en entry (). Inicialmente, todas las CPU comienzan con las interrupciones apagadas, entran en la entrada (), después de lo cual dejamos solo un núcleo activo, el resto va a un bucle sin fin. Una vez que el contador es igual al número de CPU, consideramos que todos los hits posteriores en la entrada son excepciones. Les recuerdo que antes era como se describe en
nuestro primer artículo sobre Elbrus cpuid = __e2k_atomic32_add(1, &last_cpuid); if (cpuid > 1) { while(1); } memcpy((void*)0, &_t_entry, 0x1800); kernel_start();
Lo hizo
if (entries_count >= CPU_COUNT) { e2k_trap_handler(regs); ... } e2k_wait_all(); entries_count = __e2k_atomic32_add(1, &entries_count); if (entries_count > 1) { cpu_idle(); } e2k_kernel_start(); }
Y finalmente vimos la reacción al entrar en la interrupción (solo con la ayuda de printf imprimimos una línea).
Aquí vale la pena explicar que inicialmente en la primera versión esperábamos copiar la tabla de excepciones, pero en primer lugar, resultó que estaba en nuestra dirección y, en segundo lugar, no pudimos hacer la copia correcta. Tuve que reescribir los scripts del enlazador, el punto de entrada al sistema y el controlador de interrupciones, es decir, necesitaba la parte del ensamblador, un poco más tarde.
Así es como se ve la parte de la parte modificada del enlazador de script:
.text : { _start = .; _t_entry = .; *(.ttable_entry0) . = _t_entry + 0x800; *(.ttable_entry1) . = _t_entry + 0x1000; *(.ttable_entry2) . = _t_entry + 0x1800; _t_entry_end = .; *(.e2k_entry) *(.cpu_idle) }
es decir, eliminamos la sección de entrada para la tabla de excepciones. La sección cpu_idle también se encuentra allí para aquellas CPU que no se usan.
Así es como se ve la función de entrada para nuestro núcleo activo, en el que se ejecutará Embox:
static void e2k_kernel_start(void) { extern void kernel_start(void); int psr; while (idled_cpus_count < CPU_COUNT - 1) ; ... e2k_upsr_write(e2k_upsr_read() & ~UPSR_FE); kernel_start(); }
Bueno, de acuerdo con la instrucción VFDI, se lanzó una excepción. Ahora necesita obtener su número para asegurarse de que esta sea la excepción correcta. Para esto, Elbrus tiene registros de información de interrupción TIR (registros de información de trampa). Contienen información sobre los últimos comandos, es decir, la parte final de la traza. La traza se reúne durante la ejecución del programa y se "congela" al entrar en una interrupción. TIR incluye las partes baja (64 bits) y alta (64 bits). La palabra baja contiene las banderas de excepción, y la palabra alta contiene un puntero a la instrucción que condujo a la excepción y al número TIR actual. En consecuencia, en nuestro caso, exc_d_interrupt es el cuarto bit.
Nota Todavía tenemos algunos malentendidos con respecto a la profundidad (número) de TIR. La documentación proporciona:
"Se determina la profundidad de la memoria TIR, es decir, el número de registros de información de trampas
Macro TIR_NUM igual al número de etapas de canalización de procesador requeridas para
emitiendo todas las situaciones especiales posibles. TIR_NUM = 19; "
En la práctica, vemos la profundidad = 1 y, por lo tanto, solo usamos el registro TIR0.
Los especialistas del MCST nos explicaron que todo es correcto y que solo habrá TIR0 para las interrupciones "precisas", pero para otras situaciones puede haber algo más. Pero como solo estamos hablando de interrupciones de temporizador, esto no nos molesta.
Ok, ahora veamos qué se necesita para ingresar / salir correctamente del controlador de excepciones. De hecho, es necesario guardar en la entrada y restaurar los siguientes 5 registros en la salida. Tres registros de preparación de transferencia de control son ctpr [1,2,3], y dos registros de control de ciclo son ILCR (Registro de valores iniciales del contador de ciclos) y LSR (Registro de estado del ciclo).
.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 getsp -(5 * 8), %dr0 std %dr1, [%dr0 + PT_CTRP1] std %dr2, [%dr0 + PT_CTRP2] std %dr3, [%dr0 + PT_CTRP3] std %dr4, [%dr0 + PT_ILCR] std %dr5, [%dr0 + PT_LSR] disp %ctpr1, e2k_entry ct %ctpr1
En realidad, eso es todo, después de salir del controlador de excepciones, debe restaurar estos 5 registros.
Hacemos esto con una macro:
#define RESTORE_COMMON_REGS(regs) \ ({ \ uint64_t ctpr1 = regs->ctpr1, ctpr2 = regs->ctpr2, \ ctpr3 = regs->ctpr3, lsr = regs->lsr, \ ilcr = regs->ilcr; \ \ 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); \ })
También es importante no olvidar después de la restauración de los registros para invocar la operación HECHO (Devolución desde el controlador de interrupción de hardware). Esta operación es necesaria, en particular, para procesar correctamente las operaciones de transferencia de control interrumpidas. Hacemos esto con una macro:
#define E2K_DONE \ do { \ asm volatile ("{nop 3} {done}" ::: "ctpr3"); \ } while (0)
En realidad, hacemos el retorno de la interrupción directamente en código C usando estas dos macros.
e2k_trap_handler(regs); RESTORE_COMMON_REGS(regs); E2K_DONE;
Interrupciones externas
Comencemos con cómo habilitar las interrupciones externas. En Elbrus, APIC (o más bien su análogo) se usa como controlador de interrupción; Embox ya tenía este controlador. Por lo tanto, fue posible recoger un temporizador del sistema para ello. Hay dos temporizadores, uno que es muy similar al
PIT , el otro
LAPIC Timer , también es bastante estándar, por lo que no tiene sentido hablar de ellos. Tanto eso como eso parecían simples, y eso y eso ya existía en Embox, pero el controlador del temporizador LAPIC parecía más en perspectiva, además de que la implementación del temporizador PIT nos parecía más no estándar. Por lo tanto, parecía más fácil de completar. Además, la documentación oficial describía los registros APIC y LAPIC, que eran ligeramente diferentes de los originales. Traerlos no tiene sentido, como puedes ver en el original.
Además de permitir interrupciones en APIC, debe habilitar el manejo de interrupciones a través de los registros PSR / UPSR. Ambos registros tienen banderas para habilitar interrupciones externas e interrupciones no enmascarables.
PERO aquí es muy importante tener en cuenta que el registro PSR es
local para la función (esto se discutió en la
primera parte técnica ). Y esto significa que si lo configura dentro de una función, cuando llame a todas las funciones posteriores, se heredará, pero cuando regrese de la función, volverá a su estado original. De ahí la pregunta, pero ¿cómo gestionar las interrupciones?
Usamos la siguiente solución. El registro PSR le permite habilitar la administración a través de UPSR, que ya es global (lo que necesitamos). Por lo tanto, habilitamos el control a través de UPSR directamente (¡importante!) Antes de la función de inicio de sesión principal de Embox:
asm volatile ("rrs %%psr, %0" : "=r"(psr) :); psr |= (PSR_IE | PSR_NMIE | PSR_UIE); asm volatile ("rws %0, %%psr" : : "ri"(psr)); kernel_start();
De alguna manera por casualidad después de refactorizar, tomé y puse estas líneas en una función separada ... Y el registro es local para la función. Está claro que todo se ha roto :)
Entonces, todo parece estar encendido en el procesador, vaya al controlador de interrupción.
Como hemos visto anteriormente, la información sobre el número de excepción está en el registro TIR. Además, el bit 32 en este registro informa que se ha producido una interrupción externa.
Después de encender el temporizador, siguieron un par de días de tormento, ya que no se pudo obtener ninguna interrupción. La razón fue lo suficientemente divertida. Hay punteros de 64 bits en Elbrus, y la dirección del registro en APIC entró en uint32_t, por eso los usamos. Pero resultó que si necesita, por ejemplo, convertir 0xF0000000 en un puntero, no obtendrá 0xF0000000, sino 0xFFFFFFFFF0000000. Es decir, el compilador expandirá su signo int sin firmar.
Aquí, por supuesto, era necesario usar uintptr_t, ya que, como resultó, en el estándar C99, este tipo de conversión está definida por la implementación.
Después de que finalmente vimos el bit 32 en relieve en TIR, comenzamos a buscar cómo obtener el número de interrupción. Resultó ser bastante simple, aunque no se parece en nada a x86, esta es una de las diferencias entre las implementaciones de LAPIC. Para Elbrus, para obtener el número de interrupción, debe ingresar al registro especial de LAPIC:
#define APIC_VECT (0xFEE00000 + 0xFF0)
donde 0xFEE00000 es la dirección base de los registros LAPIC.
Eso es todo, resultó recoger tanto el temporizador del sistema como el temporizador LAPIC.
Conclusión
La información proporcionada en las dos primeras partes técnicas del artículo sobre la arquitectura de Elbrus es suficiente para implementar interrupciones de hardware y multitarea preventiva en cualquier sistema operativo. En realidad, las capturas de pantalla dadas dan testimonio de esto.

Esta no es la última parte técnica sobre la arquitectura de Elbrus. Ahora estamos dominando la gestión de memoria (MMU) en Elbrus, esperamos hablar de ello pronto. Necesitamos esto no solo para la implementación de espacios de direcciones virtuales, sino también para el trabajo normal con periféricos, porque a través de este mecanismo puede deshabilitar o habilitar el almacenamiento en caché de un área específica del espacio de direcciones.
Todo lo que está escrito en el artículo se puede encontrar en el repositorio de
Embox . También puede compilar y ejecutar, por supuesto, si hay una plataforma de hardware. Es cierto que se necesita un compilador para esto, y solo se puede obtener en el
MCST . Se puede solicitar documentación oficial allí.