assembly - maquina - ¿Cómo es el lenguaje ensamblador multinúcleo?
lenguaje maquina ejemplos (10)
Una vez, para escribir el ensamblador x86, por ejemplo, tendría instrucciones que indiquen "cargar el registro EDX con el valor 5", "incrementar el registro EDX", etc.
Con las CPU modernas que tienen 4 núcleos (o incluso más), a nivel de código de máquina, ¿parece que hay 4 CPU separadas (es decir, solo hay 4 registros "EDX" distintos)? Si es así, cuando dice "incrementar el registro EDX", ¿qué determina qué registro EDX de la CPU se incrementa? ¿Hay un concepto de "contexto de CPU" o "hilo" en el ensamblador x86 ahora?
¿Cómo funciona la comunicación / sincronización entre los núcleos?
Si estaba escribiendo un sistema operativo, ¿qué mecanismo se expone a través del hardware para permitirle programar la ejecución en diferentes núcleos? ¿Es alguna instrucción especial privilegiada?
Si estuviera escribiendo una VM de compilación / código de optimización para una CPU multinúcleo, ¿qué necesitaría saber específicamente, por ejemplo, x86 para que genere código que se ejecute de manera eficiente en todos los núcleos?
¿Qué cambios se han realizado en el código de máquina x86 para admitir la funcionalidad de múltiples núcleos?
Si estuviera escribiendo una VM de compilación / código de optimización para una CPU multinúcleo, ¿qué necesitaría saber específicamente, por ejemplo, x86 para que genere código que se ejecute de manera eficiente en todos los núcleos?
Como alguien que escribe optimizando las máquinas virtuales de compilador / bytecode, puedo ayudarlo aquí.
No necesita saber nada específico acerca de x86 para que genere código que se ejecute de manera eficiente en todos los núcleos.
Sin embargo, es posible que necesite saber sobre cmpxchg y sus amigos para escribir el código que se ejecuta correctamente en todos los núcleos. La programación multinúcleo requiere el uso de la sincronización y la comunicación entre subprocesos de ejecución.
Es posible que necesite saber algo sobre x86 para que genere código que se ejecute de manera eficiente en x86 en general.
Hay otras cosas que te sería útil aprender:
Debe obtener información sobre las instalaciones que proporciona el sistema operativo (Linux o Windows o OSX) para permitirle ejecutar varios subprocesos. Debería obtener información sobre las API de paralelización, como OpenMP y Threading Building Blocks, o el próximo "Grand Central" de "Snow Leopard" de OSX 10.6.
Debe considerar si su compilador debería ser auto-paralelizador, o si el autor de las aplicaciones compiladas por su compilador necesita agregar una sintaxis especial o llamadas a la API en su programa para aprovechar los múltiples núcleos.
Mínimo ejecutable ejemplo de metal desnudo Intel x86
Ejemplo de metal desnudo ejecutable con toda la placa de calderas requerida . Todas las partes principales están cubiertas a continuación.
Probado en Ubuntu 15.10 QEMU 2.3.0 y Lenovo ThinkPad T400.
La Guía de programación del sistema Intel Manual Volume 3 - 325384-056US septiembre de 2015 cubre SMP en los capítulos 8, 9 y 10.
Tabla 8-1. "Secuencia de transmisión INIT-SIPI-SIPI y elección de tiempos de espera" contiene un ejemplo que, básicamente, simplemente funciona:
MOV ESI, ICR_LOW ; Load address of ICR low dword into ESI.
MOV EAX, 000C4500H ; Load ICR encoding for broadcast INIT IPI
; to all APs into EAX.
MOV [ESI], EAX ; Broadcast INIT IPI to all APs
; 10-millisecond delay loop.
MOV EAX, 000C46XXH ; Load ICR encoding for broadcast SIPI IP
; to all APs into EAX, where xx is the vector computed in step 10.
MOV [ESI], EAX ; Broadcast SIPI IPI to all APs
; 200-microsecond delay loop
MOV [ESI], EAX ; Broadcast second SIPI IPI to all APs
; Waits for the timer interrupt until the timer expires
En ese código:
La mayoría de los sistemas operativos harán que la mayoría de esas operaciones sean imposibles desde el anillo 3 (programas de usuario).
Entonces necesitas escribir tu propio kernel para jugar libremente con él: un programa de Linux para usuarios no funcionará.
Al principio, se ejecuta un solo procesador, llamado el procesador de arranque (BSP).
Debe activar los otros (llamados Procesadores de aplicaciones (AP)) a través de interrupciones especiales llamadas Interrupciones entre procesadores (IPI) .
Esas interrupciones se pueden hacer programando el Controlador de interrupción programable avanzado (APIC) a través del registro de comando de interrupción (ICR)
El formato del ICR se documenta en: 10.6 "EMITIR INTERRUPTORES INTERPROCESORES"
El IPI sucede tan pronto como escribimos al ICR.
ICR_LOW se define en 8.4.4 "Ejemplo de inicialización de MP" como:
ICR_LOW EQU 0FEE00300H
El valor mágico
0FEE00300
es la dirección de memoria del ICR, como se documenta en la Tabla 10-1 "Mapa de direcciones del registro APIC local"El método más simple posible se usa en el ejemplo: configura el ICR para enviar IPI de difusión que se entregan a todos los otros procesadores, excepto al actual.
Pero también es posible, y recomendado por algunos , obtener información sobre los procesadores a través de la configuración de estructuras de datos especiales por parte del BIOS, como las tablas ACPI o la tabla de configuración MP de Intel, y solo reactivar las que necesita una por una.
XX
en000C46XXH
codifica la dirección de la primera instrucción que el procesador ejecutará como:CS = XX * 0x100 IP = 0
Recuerde que CS multiplica las direcciones por
0x10
, por lo que la dirección de memoria real de la primera instrucción es:XX * 0x1000
Entonces, si por ejemplo
XX == 1
, el procesador se iniciará en0x1000
.Luego debemos asegurarnos de que se ejecute un código de modo real de 16 bits en esa ubicación de memoria, por ejemplo, con:
cld mov $init_len, %ecx mov $init, %esi mov 0x1000, %edi rep movsb .code16 init: xor %ax, %ax mov %ax, %ds /* Do stuff. */ hlt .equ init_len, . - init
El uso de un script de enlace es otra posibilidad.
Los bucles de retardo son una parte molesta para comenzar a trabajar: no hay una forma súper simple de hacer ese tipo de sueño con precisión.
Los posibles métodos incluyen:
- PIT (usado en mi ejemplo)
- HPET
- calibre el tiempo de un bucle ocupado con lo anterior, y utilícelo en su lugar
Relacionado: ¿Cómo mostrar un número en la pantalla y dormir durante un segundo con el ensamblaje DOS x86?
Creo que el procesador inicial debe estar en modo protegido para que esto funcione mientras escribimos en la dirección
0FEE00300H
que es demasiado alto para 16 bits.Para comunicarnos entre los procesadores, podemos usar un spinlock en el proceso principal y modificar el bloqueo desde el segundo núcleo.
Debemos asegurarnos de que se realiza la escritura de la memoria, por ejemplo, a través de
wbinvd
.
Estado compartido entre procesadores
8.7.1 "Estado de los procesadores lógicos" dice:
Las siguientes funciones forman parte del estado arquitectónico de los procesadores lógicos dentro de los procesadores Intel 64 o IA-32 que admiten la tecnología Intel Hyper-Threading. Las características se pueden subdividir en tres grupos:
- Duplicado para cada procesador lógico.
- Compartido por procesadores lógicos en un procesador físico.
- Compartido o duplicado, dependiendo de la implementación.
Las siguientes funciones están duplicadas para cada procesador lógico:
- Registros de propósito general (EAX, EBX, ECX, EDX, ESI, EDI, ESP y EBP)
- Registros de segmento (CS, DS, SS, ES, FS y GS)
- Registros EFLAGS y EIP. Tenga en cuenta que los registros CS y EIP / RIP para cada procesador lógico apuntan a la secuencia de instrucciones del subproceso que está ejecutando el procesador lógico.
- x87 registros FPU (ST0 a ST7, palabra de estado, palabra de control, palabra de etiqueta, puntero de operando de datos y puntero de instrucción)
- Registros MMX (MM0 a MM7)
- Registros XMM (XMM0 a XMM7) y el registro MXCSR
- Registros de control y registros de puntero de la tabla del sistema (GDTR, LDTR, IDTR, registro de tareas)
- Registros de depuración (DR0, DR1, DR2, DR3, DR6, DR7) y los MSR de control de depuración
- Estado global de comprobación de la máquina (IA32_MCG_STATUS) y capacidad de comprobación de la máquina (IA32_MCG_CAP) MSRs
- Modulación de reloj térmico y control de gestión de potencia ACPI MSRs.
- Contador de marca de tiempo MSRs
- La mayoría de los otros registros MSR, incluida la tabla de atributos de página (PAT). Vea las excepciones a continuación.
- Registros locales APIC.
- Registros de propósito general adicionales (R8-R15), registros XMM (XMM8-XMM15), registro de control, IA32_EFER en procesadores Intel 64.
Las siguientes características son compartidas por los procesadores lógicos:
- Registros de rango de tipo de memoria (MTRRs)
Si las siguientes funciones están compartidas o duplicadas es específica de la implementación:
- IA32_MISC_ENABLE MSR (dirección MSR 1A0H)
- MSR de arquitectura de comprobación de máquinas (MCA) (excepto para los MSR IA32_MCG_STATUS y IA32_MCG_CAP)
- Control de monitoreo de desempeño y contador MSRs.
El intercambio de caché se discute en:
- http://.com/questions/4802565/multiple-threads-and-cpu-cache
- ¿Pueden múltiples CPU / núcleos acceder a la misma RAM simultáneamente?
Los hyperthreads de Intel tienen un mayor intercambio de caché y canalización que los núcleos separados: https://superuser.com/questions/133082/hyper-threading-and-dual-core-whats-the-difference/995858#995858
Kernel de linux 4.2
La principal acción de inicialización parece estar en arch/x86/kernel/smpboot.c
.
Ejemplos ARM
ARM parece ser un poco más fácil de configurar que x86 ya que tiene menos sobrecarga histórica, aquí hay dos ejemplos mínimos que se pueden ejecutar:
- frambuesa pi 2: https://github.com/dwelch67/raspberrypi/tree/a09771a1d5a0b53d8e7a461948dc226c5467aeec/multi00
- frambuesa pi 3: https://github.com/bztsrc/raspi3-tutorial/tree/a3f069b794aeebef633dbe1af3610784d55a0efa/02_multicorec
TODO: revisa esos ejemplos, y explícalos mejor aquí.
Este documento proporciona una guía sobre el uso de primitivas de sincronización ARM que luego puede usar para hacer cosas divertidas con múltiples núcleos: http://infocenter.arm.com/help/topic/com.arm.doc.dht0008a/DHT0008A_arm_synchronization_primitives.pdf
Las preguntas no oficiales sobre SMP
Una vez, para escribir el ensamblador x86, por ejemplo, tendría instrucciones que indicaban "cargar el registro EDX con el valor 5", "incrementar el registro EDX", etc. Con CPU modernas que tienen 4 núcleos (o incluso más) En el nivel del código de la máquina, ¿parece que hay 4 CPU separadas (es decir, solo hay 4 registros "EDX" distintos)?Exactamente. Hay 4 conjuntos de registros, incluidos 4 punteros de instrucción separados.
Si es así, cuando dice "incrementar el registro EDX", ¿qué determina qué registro EDX de la CPU se incrementa?
La CPU que ejecutó esa instrucción, naturalmente. Piense en ello como 4 microprocesadores completamente diferentes que simplemente comparten la misma memoria.
¿Hay un concepto de "contexto de CPU" o "hilo" en el ensamblador x86 ahora?
No. El ensamblador simplemente traduce instrucciones como siempre lo hizo. No hay cambios allí.
¿Cómo funciona la comunicación / sincronización entre los núcleos?
Ya que comparten la misma memoria, es principalmente una cuestión de lógica de programa. Aunque ahora existe un mecanismo de interrupción entre procesadores , no es necesario y no estuvo presente originalmente en los primeros sistemas x86 con doble CPU.
Si estaba escribiendo un sistema operativo, ¿qué mecanismo se expone a través del hardware para permitirle programar la ejecución en diferentes núcleos?
El programador en realidad no cambia, excepto que es un poco más cuidadoso acerca de las secciones críticas y los tipos de bloqueos utilizados. Antes de SMP, el código del kernel finalmente llamaría al programador, que buscaría en la cola de ejecución y elegiría un proceso para ejecutarse como el siguiente hilo. (Los procesos en el kernel se parecen mucho a los hilos.) El kernel SMP ejecuta exactamente el mismo código, un hilo a la vez, es solo que ahora el bloqueo de la sección crítica debe ser seguro para SMP para asegurarse de que dos núcleos no puedan seleccionarse accidentalmente el mismo PID.
¿Es alguna instrucción especial privilegiada?
No. Los núcleos solo se ejecutan en la misma memoria con las mismas instrucciones anteriores.
Si estuviera escribiendo una VM de compilación / código de optimización para una CPU multinúcleo, ¿qué necesitaría saber específicamente, por ejemplo, x86 para que genere código que se ejecute de manera eficiente en todos los núcleos?
Ejecutas el mismo código que antes. Es el kernel de Unix o Windows lo que necesitaba cambiar.
Podría resumir mi pregunta como "¿Qué cambios se han realizado en el código de máquina x86 para admitir la funcionalidad de varios núcleos?"
Nada era necesario. Los primeros sistemas SMP utilizaron exactamente el mismo conjunto de instrucciones que los procesadores. Ahora, ha habido una gran evolución en la arquitectura x86 y miles de nuevas instrucciones para hacer que las cosas vayan más rápido, pero ninguna era necesaria para SMP.
Para obtener más información, consulte la Especificación de multiprocesador Intel .
Actualización: todas las preguntas de seguimiento pueden responderse simplemente aceptando completamente que una CPU multinúcleo n- way es casi lo mismo que n procesadores independientes que solo comparten la misma memoria. 2 Hubo una pregunta importante que no se hizo: ¿cómo se escribe un programa para ejecutarse en más de un núcleo para obtener más rendimiento? Y la respuesta es: está escrito usando una biblioteca de hilos como Pthreads. Algunas bibliotecas de subprocesos usan "subprocesos verdes" que no son visibles para el sistema operativo, y no obtendrán núcleos separados, pero siempre que la biblioteca de subprocesos utilice las funciones de subproceso del kernel, su programa de subprocesos será automáticamente multinúcleo. 1. Para la compatibilidad con versiones anteriores, solo el primer núcleo se inicia al reiniciarse, y se deben hacer algunas cosas de tipo controlador para activar los restantes.2. También comparten todos los periféricos, naturalmente.
Cada núcleo se ejecuta desde un área de memoria diferente. Su sistema operativo apuntará un núcleo a su programa y el núcleo ejecutará su programa. Su programa no sabrá que hay más de un núcleo o en qué núcleo se está ejecutando.
Tampoco hay instrucciones adicionales disponibles solo para el sistema operativo. Estos núcleos son idénticos a los chips de un solo núcleo. Cada núcleo ejecuta una parte del sistema operativo que manejará la comunicación con las áreas de memoria comunes utilizadas para el intercambio de información para encontrar la siguiente área de memoria para ejecutar.
Esto es una simplificación pero le da la idea básica de cómo se hace. Más información sobre multicores y multiprocesadores en Embedded.com tiene mucha información sobre este tema ... ¡Este tema se complica muy rápidamente!
Como lo entiendo, cada "núcleo" es un procesador completo, con su propio conjunto de registros. Básicamente, la BIOS comienza con un núcleo en ejecución, y luego el sistema operativo puede "iniciar" otros núcleos al inicializarlos y señalarlos al código para ejecutar, etc.
La sincronización se realiza por el sistema operativo. En general, cada procesador ejecuta un proceso diferente para el sistema operativo, por lo que la funcionalidad de subprocesos múltiples del sistema operativo se encarga de decidir qué proceso llega a tocar qué memoria y qué hacer en caso de una colisión de memoria.
El código de ensamblaje se traducirá en un código de máquina que se ejecutará en un núcleo. Si desea que sea multiproceso, tendrá que usar primitivas del sistema operativo para iniciar este código en diferentes procesadores varias veces o diferentes partes de código en diferentes núcleos: cada núcleo ejecutará un hilo separado. Cada hilo solo verá un núcleo en el que se está ejecutando actualmente.
Esta no es una respuesta directa a la pregunta, pero es una respuesta a una pregunta que aparece en los comentarios. Esencialmente, la pregunta es qué soporte da el hardware a la operación de subprocesos múltiples.
Nicholas Flynt tenía razón , al menos con respecto a x86. En un entorno de múltiples subprocesos (Hyper-threading, multi-core o multiprocesador), el subproceso Bootstrap (normalmente el subproceso 0 en el núcleo 0 en el procesador 0) inicia la 0xfffffff0
del código desde la dirección 0xfffffff0
. Todos los otros subprocesos se inician en un estado de suspensión especial llamado Wait-for-SIPI . Como parte de su inicialización, el subproceso primario envía una interrupción especial entre procesadores (IPI) a través del APIC denominado SIPI (IPI de inicio) a cada subproceso que está en WFS. El SIPI contiene la dirección desde la cual ese hilo debería comenzar a buscar código.
Este mecanismo permite que cada hilo ejecute código desde una dirección diferente. Todo lo que se necesita es soporte de software para cada hilo para configurar sus propias tablas y colas de mensajería. El sistema operativo los utiliza para realizar la programación real de múltiples subprocesos.
En lo que se refiere al ensamblaje real, como escribió Nicholas, no hay diferencia entre los ensamblajes para una aplicación de un solo hilo o de varios hilos. Cada hilo lógico tiene su propio conjunto de registros, por lo que se escribe:
mov edx, 0
solo actualizará EDX
para el hilo actualmente en ejecución . No hay forma de modificar EDX
en otro procesador con una sola instrucción de ensamblaje. Necesita algún tipo de llamada al sistema para pedirle al sistema operativo que le diga a otro hilo que ejecute el código que actualizará su propio EDX
.
La principal diferencia entre una aplicación de uno o varios subprocesos es que la primera tiene una pila y la última tiene una para cada subproceso. El código se genera de manera algo diferente, ya que el compilador asumirá que los datos y los registros de segmento de pila (ds y ss) no son iguales. Esto significa que la indirección a través de los registros ebp y esp que están predeterminados en el registro ss no lo hará también en ds (porque ds! = Ss). A la inversa, el direccionamiento indirecto a través de los otros registros que por defecto a ds no lo hará por defecto a ss.
Los hilos comparten todo lo demás, incluidas las áreas de datos y código. También comparten rutinas de lib, así que asegúrate de que sean seguras para subprocesos. Un procedimiento que ordena un área en la RAM puede ser multihilo para acelerar las cosas. Los subprocesos luego accederán, compararán y ordenarán datos en la misma área de memoria física y ejecutarán el mismo código, pero usarán diferentes variables locales para controlar sus respectivas partes de la clasificación. Por supuesto, esto se debe a que los subprocesos tienen pilas diferentes donde están contenidas las variables locales. Este tipo de programación requiere un ajuste cuidadoso del código para reducir las colisiones de datos entre núcleos (en cachés y RAM), lo que a su vez da como resultado un código que es más rápido con dos o más subprocesos que con solo uno. Por supuesto, un código sin afinar a menudo será más rápido con un procesador que con dos o más. Depurar es más desafiante porque el punto de interrupción estándar "int 3" no será aplicable ya que desea interrumpir un subproceso específico y no todos. Los puntos de interrupción del registro de depuración tampoco solucionan este problema a menos que pueda establecerlos en el procesador específico que ejecuta el subproceso específico que desea interrumpir.
Otro código de subprocesos múltiples puede involucrar diferentes subprocesos que se ejecutan en diferentes partes del programa. Este tipo de programación no requiere el mismo tipo de ajuste y, por lo tanto, es mucho más fácil de aprender.
Lo que se ha agregado en cada arquitectura con capacidad de multiprocesamiento en comparación con las variantes de procesador único que se presentaron antes son instrucciones para sincronizar entre los núcleos. Además, tiene instrucciones para lidiar con la coherencia de la memoria caché, los búferes de descarga y operaciones similares de bajo nivel con las que debe lidiar un sistema operativo. En el caso de arquitecturas multiproceso simultáneas como IBM POWER6, IBM Cell, Sun Niagara e Intel "Hyperthreading", también tiende a ver nuevas instrucciones para priorizar entre subprocesos (como establecer prioridades y proporcionar explícitamente el procesador cuando no hay nada que hacer) .
Pero la semántica básica de un solo hilo es la misma, solo se agregan facilidades adicionales para manejar la sincronización y la comunicación con otros núcleos.
No se hace en las instrucciones de la máquina en absoluto; los núcleos pretenden ser CPU distintas y no tienen ninguna capacidad especial para comunicarse entre sí. Hay dos formas en que se comunican:
Ellos comparten el espacio de direcciones físicas. El hardware maneja la coherencia de caché, por lo que una CPU escribe en una dirección de memoria que otra lee.
comparten un APIC (controlador de interrupción programable). Esta es una memoria asignada en el espacio de direcciones físicas, y puede ser utilizada por un procesador para controlar los otros, activarlos o desactivarlos, enviar interrupciones, etc.
http://www.cheesecake.org/sac/smp.html es una buena referencia con una URL tonta.