Núcleos de CPU o qué es SMP y qué come

Introduccion


Buen día, hoy me gustaría tocar un tema bastante simple que es casi desconocido para los programadores comunes, pero que es muy probable que cada uno de ustedes lo haya usado.
Estamos hablando del multiprocesamiento simétrico (popularmente - SMP), la arquitectura que se encuentra en todos los sistemas operativos multitarea y, por supuesto, es una parte integral de ellos. Todo el mundo sabe que cuantos más núcleos tenga un procesador, más potente será el procesador, sí, pero ¿cómo puede un sistema operativo usar varios núcleos al mismo tiempo? Algunos programadores no bajan a este nivel de abstracción, simplemente no lo necesitan, pero creo que todos estarán interesados ​​en cómo funciona SMP.

Multitarea y su implementación


Aquellos que alguna vez han estudiado arquitectura de computadoras saben que el procesador en sí no puede realizar varias tareas a la vez, la multitarea nos da solo el sistema operativo, que cambia estas tareas. Existen varios tipos de multitarea, pero la más adecuada, conveniente y ampliamente utilizada es desplazar la multitarea (puede leer sus aspectos principales en Wikipedia). Se basa en el hecho de que cada proceso (tarea) tiene su propia prioridad, lo que afecta la cantidad de tiempo de procesador que se le asignará. Cada tarea tiene un intervalo de tiempo durante el cual el proceso hace algo; una vez que expira el intervalo de tiempo, el sistema operativo transfiere el control a otra tarea. Surge la pregunta: cómo distribuir los recursos informáticos, como la memoria, los dispositivos, etc. entre procesos? Todo es muy simple: Windows lo hace por sí mismo, Linux usa un sistema de semáforo. Pero un núcleo no es serio, seguimos adelante.

Interrupciones y PIC


Quizás para algunos esto resulte ser una novedad, para otros no, pero la arquitectura i386 (hablaré sobre la arquitectura x86, ARM no cuenta, porque no estudié esta arquitectura y nunca la encontré (incluso al nivel de escribir algún servicio o programa residente)) usa interrupciones (solo hablaremos de interrupciones de hardware, IRQ) para notificar al SO o al programa sobre un evento. Por ejemplo, hay una interrupción 0x8 (para modos protegidos y largos, por ejemplo, 0x20, dependiendo de cómo configurar el PIC, más sobre eso más adelante), que es llamado por PIT, que, por ejemplo, puede generar interrupciones con cualquier frecuencia necesaria. Luego, el trabajo del sistema operativo para la distribución de segmentos de tiempo se reduce a 0, cuando se llama a una interrupción, el programa se detiene y se le da control, por ejemplo, al núcleo, que a su vez guarda los datos actuales del programa (registros, banderas, etc.) y le da control al siguiente proceso. .

Como probablemente haya entendido, las interrupciones son funciones (o procedimientos) que el equipo o el programa en sí llaman en algún momento. En total, el procesador admite 16 interrupciones en dos PIC. El procesador tiene banderas, y una de ellas es la bandera "I" - Control de interrupción. Al establecer este indicador en 0, el procesador no causará interrupciones de hardware. Pero también quiero señalar que existen los llamados NMI (interrupciones no enmascarables): los datos de interrupción se seguirán llamando, incluso si el bit I se establece en 0. Usando la programación PIC, puede desactivar los datos de interrupción, pero después de regresar de cualquier interrupción con IRET - nuevamente no serán prohibidos. Observo que desde un programa regular no puede rastrear la llamada de interrupción: su programa se detiene y solo se reanuda después de un tiempo, su programa ni siquiera se da cuenta (sí, puede verificar que se llamó a la interrupción, pero ¿por qué?

PIC - Controlador de interrupción programable

De Wiki:
Como regla general, es un dispositivo electrónico, a veces hecho como parte del propio procesador o chips complejos de su marco, cuyas entradas están conectadas eléctricamente a las salidas correspondientes de varios dispositivos. El número de entrada del controlador de interrupción se denomina "IRQ". Este número debe distinguirse de la prioridad de interrupción, así como del número de entrada en la tabla de vectores de interrupción (INT). Entonces, por ejemplo, en una PC IBM en modo real (MS-DOS se está ejecutando en este modo) del procesador, la interrupción del teclado estándar usa IRQ 1 e INT 9.

La plataforma original de PC de IBM utiliza un esquema de interrupción muy simple. El controlador de interrupción es un contador simple que itera secuencialmente sobre las señales de diferentes dispositivos o se reinicia al principio cuando se encuentra una nueva interrupción. En el primer caso, los dispositivos tienen la misma prioridad; en el segundo, los dispositivos con un número de serie más bajo (o más alto en el recuento) tienen una prioridad más alta.

Como comprenderá, este es un circuito electrónico que permite que los dispositivos envíen solicitudes de interrupción, por lo general, hay exactamente 2 de ellos.

Ahora, pasemos al tema del artículo.

SMP


Para implementar este estándar, se comenzaron a colocar nuevos esquemas en las placas base: APIC y ACPI. Hablemos del primero.

APIC - Controlador de interrupción programable avanzado, una versión mejorada de PIC. Se utiliza en sistemas multiprocesador y es una parte integral de todos los últimos procesadores Intel (y compatibles). APIC se utiliza para el reenvío de interrupciones complejas y para enviar interrupciones entre procesadores. Estas cosas no fueron posibles utilizando la especificación PIC anterior.

APIC local y IO APIC


En un sistema basado en APIC, cada procesador consta de un "núcleo" y un "APIC local". El APIC local es responsable de manejar la configuración de interrupción específica del procesador. Entre otras cosas, contiene una tabla de vectores local (LVT), que traduce eventos, como el "reloj interno" y otras fuentes de interrupción "locales", en un vector de interrupción (por ejemplo, el contacto LocalINT1 puede generar una excepción NMI mientras mantiene " 2 "a la entrada LVT correspondiente).

Puede encontrar más información sobre el APIC local en la "Guía de programación del sistema" de los procesadores Intel modernos.

Además, hay un APIC IO (por ejemplo, Intel 82093AA), que forma parte del conjunto de chips y proporciona control de interrupciones multiprocesador, incluida la distribución simétrica estática y dinámica de interrupciones para todos los procesadores. En sistemas con múltiples subsistemas de E / S, cada subsistema puede tener su propio conjunto de interrupciones.

Cada pin de interrupción se programa individualmente como disparo de borde o nivel. El vector de interrupción y la información de control de interrupción se pueden especificar para cada interrupción. El esquema de acceso indirecto a registros optimiza el espacio de memoria necesario para acceder a los registros internos de E / S APIC. Para aumentar la flexibilidad del sistema al asignar espacio de memoria, los dos registros de E / S APIC son reubicables, pero el valor predeterminado es 0xFEC00000.

Inicializando un APIC "local"


El APIC local se activa en el momento del arranque y se puede deshabilitar restableciendo el bit 11 IA32_APIC_BASE (MSR) (esto solo funciona con procesadores con una familia> 5, ya que Pentium no tiene dicho MSR), luego el procesador recibe sus interrupciones directamente del PIC 8259 compatible . Sin embargo, la guía de desarrollo de software de Intel establece que después de deshabilitar el APIC local a través de IA32_APIC_BASE, no podrá encenderlo hasta que se restablezca por completo. El APO IO también se puede configurar para funcionar en modo heredado para que emule un dispositivo 8259.

Los APIC locales se asignan a la página física FEE00xxx (consulte la Tabla 8-1 Intel P4 SPG). Esta dirección es la misma para cada APIC local que existe en la configuración, lo que significa que puede acceder directamente a los registros del núcleo APIC local en el que se está ejecutando actualmente su código. Tenga en cuenta que hay un MSR que define la base APIC real (disponible solo para procesadores con una familia> 5). MADT contiene una base APIC local, y en sistemas de 64 bits también puede contener un campo que especifica una redefinición de 64 bits de la dirección base, que debe usar en su lugar. Puede dejar la base APIC local solo donde la encuentre, o moverla a donde quiera. Nota: No creo que pueda moverlo más allá del cuarto GB de RAM.

Para permitir que el APIC local reciba interrupciones, debe configurar el Registro de vectores de interrupciones espurias. El valor correcto para este campo es el número IRQ que desea asignar a las falsas interrupciones con los 8 bits inferiores, y el octavo bit establecido en 1 para habilitar APIC (consulte la especificación para más detalles). Debe seleccionar un número de interrupción que tenga los 4 bits inferiores establecidos; La forma más fácil es usar 0xFF. Esto es importante para algunos procesadores más antiguos, porque para estos valores, los 4 bits inferiores deben establecerse en 1.

Deshabilite el 8259 PIC correctamente. Esto es casi tan importante como configurar APIC. Lo haces en dos pasos: enmascarar todas las interrupciones y reasignar la IRQ. Disfrazar todas las interrupciones las desactiva en el PIC. Reasignar interrupciones es lo que probablemente ya hizo cuando usó PIC: desea que las solicitudes de interrupción comiencen en 32 en lugar de 0 para evitar conflictos con las excepciones (en los modos de procesador protegido y largo (Largo), porque Las primeras 32 interrupciones son excepciones). Entonces debe evitar usar estos vectores de interrupción para otros fines. Esto es necesario porque, a pesar del hecho de que enmascaraste todas las interrupciones de PIC, aún podría lanzar falsas interrupciones, que luego se procesarían incorrectamente como excepciones en tu núcleo.
Pasemos a SMP.

Multitarea simétrica: inicialización


La secuencia de inicio es diferente para diferentes CPU. La Guía del programador Intel (Sección 7.5.4) contiene un protocolo de inicialización para procesadores Intel Xeon y no cubre procesadores más antiguos. Para un algoritmo general de "todo tipo de procesador", consulte la Especificación de multiprocesador Intel.

Para 80486 (con APIC 8249DX externo) debe usar IPIT INIT seguido de IPI "INIT level de-afirmar" sin ningún SIPI. Esto significa que no puede decirles dónde comenzar a ejecutar su código (la parte vectorial de SIPI), y siempre comienzan a ejecutar código de BIOS. En este caso, configura el valor de restablecimiento del BIOS CMOS en "arranque en caliente con salto lejano" (es decir, configura CMOS 0x0F a 10) para que el BIOS realice jmp far ~ [0: 0x0469], y luego establece el segmento y el desplazamiento Puntos de entrada AP en 0x0469.

El IPI de "desprestigio de nivel INIT" no es compatible con los nuevos procesadores (Pentium 4 e Intel Xeon), y AFAIK es completamente ignorado en estos procesadores.

Para los procesadores más nuevos (P6, Pentium 4) un SIPI es suficiente, pero no estoy seguro de que los procesadores Intel (Pentium) más antiguos o los procesadores de otros fabricantes necesiten un segundo SIPI. También es posible que exista un segundo SIPI en caso de una falla de entrega para el primer SIPI (ruido del bus, etc.).

Por lo general, envío el primer SIPI y luego espero para ver si el AP aumenta la cantidad de procesadores en ejecución. Si no aumenta este contador en unos pocos milisegundos, enviaré un segundo SIPI. Esto es diferente del algoritmo general de Intel (que tiene un retraso de 200 microsegundos entre SIPI), pero tratar de encontrar una fuente de tiempo que pueda medir con precisión el retraso de 200 microsegundos durante un inicio temprano no es tan simple. También descubrí que en el hardware real, si el retraso entre SIPI es demasiado largo (y no está utilizando mi método), el AP principal puede ejecutar el código de inicio de AP temprano para el sistema operativo dos veces (lo que en mi caso hará que el sistema operativo piense que tenemos el doble de procesadores de lo que realmente somos).

Puede transmitir estas señales en el bus para iniciar cada dispositivo presente. Sin embargo, también puede encender los procesadores que estaban especialmente deshabilitados (porque estaban "defectuosos").

Buscando información usando la tabla MT


Parte de la información (que puede no estar disponible en máquinas más nuevas) destinada al multiprocesamiento. Primero necesita encontrar la estructura de puntero flotante MP. Está alineado en un límite de 16 bytes y contiene una firma al comienzo de "_MP_" o 0x5F504D5F. El sistema operativo debe buscar en EBDA, el espacio ROM del BIOS y en el último kilobyte de "memoria base"; El tamaño de la memoria base se especifica en un valor de 2 bytes de 0x413 en kilobytes, menos 1 KB. Así es como se ve la estructura:

struct mp_floating_pointer_structure { char signature[4]; uint32_t configuration_table; uint8_t length; // In 16 bytes (eg 1 = 16 bytes, 2 = 32 bytes) uint8_t mp_specification_revision; uint8_t checksum; // This value should make all bytes in the table equal 0 when added together uint8_t default_configuration; // If this is not zero then configuration_table should be // ignored and a default configuration should be loaded instead uint32_t features; // If bit 7 is then the IMCR is present and PIC mode is being used, otherwise // virtual wire mode is; all other bits are reserved } 

Así es como se ve la tabla de configuración a la que apunta la estructura flotante del puntero:

 struct mp_configuration_table { char signature[4]; // "PCMP" uint16_t length; uint8_t mp_specification_revision; uint8_t checksum; // Again, the byte should be all bytes in the table add up to 0 char oem_id[8]; char product_id[12]; uint32_t oem_table; uint16_t oem_table_size; uint16_t entry_count; // This value represents how many entries are following this table uint32_t lapic_address; // This is the memory mapped address of the local APICs uint16_t extended_table_length; uint8_t extended_table_checksum; uint8_t reserved; } 

Después de la tabla de configuración hay entradas entry_count, que contienen más información sobre el sistema, seguido de una tabla extendida. Las entradas tienen 20 bytes para representar el procesador u 8 bytes para otra cosa. Así es como se ven el procesador APIC y los registros de E / S.

 struct entry_processor { uint8_t type; // Always 0 uint8_t local_apic_id; uint8_t local_apic_version; uint8_t flags; // If bit 0 is clear then the processor must be ignored // If bit 1 is set then the processor is the bootstrap processor uint32_t signature; uint32_t feature_flags; uint64_t reserved; } 

Aquí está la entrada IO APIC.

 struct entry_io_apic { uint8_t type; // Always 2 uint8_t id; uint8_t version; uint8_t flags; // If bit 0 is set then the entry should be ignored uint32_t address; // The memory mapped address of the IO APIC is memory } 

Buscando información con APIC


Puede encontrar la tabla MADT (APIC) en ACPI. La tabla enumera los APIC locales, cuyo número debe corresponder al número de núcleos en su procesador. Los detalles de esta tabla no están aquí, pero puede encontrarlos en Internet.

Lanzar AP


Después de haber recopilado la información, debe deshabilitar el PIC y prepararse para la E / S APIC. También necesita configurar el BSP del APIC local. Luego, inicie el AP utilizando SIPI.

Código para lanzar núcleos:

Observo que el vector que especifique al inicio indica la dirección de inicio: vector 0x8 - dirección 0x8000, vector 0x9 - dirección 0x9000, etc.

 // ------------------------------------------------------------------------------------------------ static u32 LocalApicIn(uint reg) { return MmioRead32(*g_localApicAddr + reg); } // ------------------------------------------------------------------------------------------------ static void LocalApicOut(uint reg, u32 data) { MmioWrite32(*g_localApicAddr + reg, data); } // ------------------------------------------------------------------------------------------------ void LocalApicInit() { // Clear task priority to enable all interrupts LocalApicOut(LAPIC_TPR, 0); // Logical Destination Mode LocalApicOut(LAPIC_DFR, 0xffffffff); // Flat mode LocalApicOut(LAPIC_LDR, 0x01000000); // All cpus use logical id 1 // Configure Spurious Interrupt Vector Register LocalApicOut(LAPIC_SVR, 0x100 | 0xff); } // ------------------------------------------------------------------------------------------------ uint LocalApicGetId() { return LocalApicIn(LAPIC_ID) >> 24; } // ------------------------------------------------------------------------------------------------ void LocalApicSendInit(uint apic_id) { LocalApicOut(LAPIC_ICRHI, apic_id << ICR_DESTINATION_SHIFT); LocalApicOut(LAPIC_ICRLO, ICR_INIT | ICR_PHYSICAL | ICR_ASSERT | ICR_EDGE | ICR_NO_SHORTHAND); while (LocalApicIn(LAPIC_ICRLO) & ICR_SEND_PENDING) ; } // ------------------------------------------------------------------------------------------------ void LocalApicSendStartup(uint apic_id, uint vector) { LocalApicOut(LAPIC_ICRHI, apic_id << ICR_DESTINATION_SHIFT); LocalApicOut(LAPIC_ICRLO, vector | ICR_STARTUP | ICR_PHYSICAL | ICR_ASSERT | ICR_EDGE | ICR_NO_SHORTHAND); while (LocalApicIn(LAPIC_ICRLO) & ICR_SEND_PENDING) ; } void SmpInit() { kprintf("Waking up all CPUs\n"); *g_activeCpuCount = 1; uint localId = LocalApicGetId(); // Send Init to all cpus except self for (uint i = 0; i < g_acpiCpuCount; ++i) { uint apicId = g_acpiCpuIds[i]; if (apicId != localId) { LocalApicSendInit(apicId); } } // wait PitWait(200); // Send Startup to all cpus except self for (uint i = 0; i < g_acpiCpuCount; ++i) { uint apicId = g_acpiCpuIds[i]; if (apicId != localId) LocalApicSendStartup(apicId, 0x8); } // Wait for all cpus to be active PitWait(10); while (*g_activeCpuCount != g_acpiCpuCount) { kprintf("Waiting... %d\n", *g_activeCpuCount); PitWait(10); } kprintf("All CPUs activated\n"); } 

 [org 0x8000] AP: jmp short bsp ;     -   BSP xor ax,ax mov ss,ax mov sp, 0x7c00 xor ax,ax mov ds,ax ; Mark CPU as active lock inc byte [ds:g_activeCpuCount] ;   ,   jmp zop bsp: xor ax,ax mov ds,ax mov dword[ds:g_activeCpuCount],0 mov dword[ds:g_activeCpuCount],0 mov word [ds:0x8000], 0x9090 ;  JMP   2 NOP' ;   ,   

Ahora, como comprenderá, para que el sistema operativo use muchos núcleos, debe configurar la pila para cada núcleo, cada núcleo, sus interrupciones, etc., pero lo más importante es que cuando se usa el multiprocesamiento simétrico, todos los recursos de los núcleos son los mismos: una memoria, un PCI, etc., y el sistema operativo solo puede paralelizar tareas entre los núcleos.

Espero que el artículo no sea lo suficientemente aburrido y bastante informativo. La próxima vez, creo, podemos hablar sobre cómo solían dibujar en la pantalla (y ahora dibujan), sin usar sombreadores y tarjetas de video geniales.

Buena suerte

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


All Articles