Érase una vez, para escribir el ensamblador x86, por ejemplo, tendría instrucciones que indicaran "cargue el registro EDX con el valor 5", "incremente 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? ¿Existe ahora un concepto de "contexto de CPU" o "hilo" en el ensamblador x86?
¿Cómo funciona la comunicación / sincronización entre los núcleos?
Si estaba escribiendo un sistema operativo, ¿qué mecanismo está expuesto a través del hardware para permitirle programar la ejecución en diferentes núcleos? ¿Son algunas instrucciones especiales privilegiadas?
Si estuviera escribiendo un VM compilador / bytecode optimizador para una CPU multinúcleo, ¿qué necesitaría saber específicamente sobre, por ejemplo, x86 para que genere código que se ejecute eficientemente en todos los núcleos?
¿Qué cambios se han realizado en el código de máquina x86 para admitir la funcionalidad multinúcleo?
Respuestas:
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 brinda 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 subprocesos múltiples (Hyper-threading, multi-core o multiprocesador), el subproceso Bootstrap (generalmente el subproceso 0 en el núcleo 0 en el procesador 0) comienza a buscar el código de 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) sobre el APIC llamado SIPI (Startup IPI) a cada subproceso que se encuentra 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 mensajes. El sistema operativo utiliza los que hacer la programación multi-roscado real.
En lo que respecta al ensamblaje real, como escribió Nicholas, no hay diferencia entre los ensamblajes para una aplicación de subproceso único o multiproceso. Cada hilo lógico tiene su propio conjunto de registros, por lo que escribe:
solo se actualizará
EDX
para el hilo actualmente en ejecución . No hay forma de modificarEDX
en otro procesador usando 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á el suyoEDX
.fuente
Ejemplo de metal desnudo ejecutable mínimo x86 de Intel
Ejemplo de metal desnudo ejecutable con todas las repeticiones requeridas . Todas las partes principales se cubren a continuación.
Probado en Ubuntu 15.10 QEMU 2.3.0 y en el invitado de hardware real Lenovo ThinkPad T400 .
La Guía de programación del sistema Intel Manual Volumen 3 - 325384-056US Septiembre 2015 cubre SMP en los capítulos 8, 9 y 10.
Tabla 8-1. "Broadcast INIT-SIPI-SIPI Sequence and Choice of Timeouts" contiene un ejemplo que básicamente funciona:
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).
Por lo tanto, debe escribir su propio kernel para jugar libremente con él: un programa Linux de usuario no funcionará.
Al principio, se ejecuta un único procesador, llamado procesador de arranque (BSP).
Debe despertar a los otros (llamados procesadores de aplicaciones (AP)) a través de interrupciones especiales llamadas interrupciones entre procesadores (IPI) .
Esas interrupciones pueden realizarse 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 "EMISIÓN DE INTERRUPCIONES INTERPROCESADORAS"
El IPI ocurre tan pronto como escribimos al ICR.
ICR_LOW se define en 8.4.4 "Ejemplo de inicialización MP" como:
El valor mágico
0FEE00300
es la dirección de memoria del ICR, como se documenta en la Tabla 10-1 "Mapa de dirección de registro APIC local"El método más simple posible se utiliza en el ejemplo: configura el ICR para enviar IPI de difusión que se entregan a todos los demás procesadores, excepto el actual.
Pero también es posible, y recomendado por algunos , obtener información sobre los procesadores a través de estructuras de datos especiales configuradas por el BIOS como las tablas ACPI o la tabla de configuración MP de Intel y solo despertar las que necesita una por una.
XX
En000C46XXH
codifica la dirección de la primera instrucción que el procesador ejecutará como:Recuerde que CS multiplica las direcciones por
0x10
, por lo que la dirección de memoria real de la primera instrucción es:Entonces, por ejemplo
XX == 1
, el procesador comenzará a las0x1000
.Luego debemos asegurarnos de que haya un código de modo real de 16 bits para ejecutar en esa ubicación de memoria, por ejemplo, con:
Usar un script vinculador es otra posibilidad.
Los bucles de retardo son una parte molesta para comenzar a trabajar: no hay una forma súper simple de hacer tales duermas con precisión.
Los posibles métodos incluyen:
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 una dirección
0FEE00300H
que es demasiado alta para 16 bitsPara comunicarnos entre procesadores, podemos usar un spinlock en el proceso principal y modificar el bloqueo desde el segundo núcleo.
Deberíamos 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:
El intercambio de caché se discute en:
Los hyperthreads de Intel tienen un mayor intercambio de caché y canalización que los núcleos separados: /superuser/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
.Ejemplo de metal desnudo ejecutable mínimo ARM
Aquí proporciono un ejemplo ARMv8 aarch64 mínimo ejecutable para QEMU:
GitHub aguas arriba .
Montar y ejecutar:
En este ejemplo, colocamos la CPU 0 en un bucle de spinlock, y solo sale cuando la CPU 1 libera el spinlock.
Después del spinlock, la CPU 0 realiza una llamada de salida de semihost que hace que QEMU se cierre.
Si inicia QEMU con solo una CPU
-smp 1
, entonces la simulación simplemente se bloquea para siempre en el spinlock.La CPU 1 se ha despertado con la interfaz PSCI, más detalles en: ARM: ¿Iniciar / Activar / Activar los otros núcleos / AP de la CPU y pasar la dirección de inicio de ejecución?
La versión ascendente también tiene algunos ajustes para que funcione en gem5, por lo que también puede experimentar con las características de rendimiento.
No lo he probado en hardware real, así que no estoy seguro de lo portátil que es. La siguiente bibliografía de Raspberry Pi puede ser de interés:
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
Probado en Ubuntu 18.10, GCC 8.2.0, Binutils 2.31.1, QEMU 2.12.0.
Próximos pasos para una programabilidad más conveniente
Los ejemplos anteriores despiertan la CPU secundaria y sincronizan la memoria básica con instrucciones dedicadas, lo cual es un buen comienzo.
Pero para hacer que los sistemas multinúcleo sean fáciles de programar, por ejemplo, como POSIX
pthreads
, también deberá abordar los siguientes temas más involucrados:la configuración interrumpe y ejecuta un temporizador que periódicamente decide qué hilo se ejecutará ahora. Esto se conoce como subprocesamiento múltiple preventivo .
Dicho sistema también necesita guardar y restaurar registros de subprocesos a medida que se inician y se detienen.
También es posible tener sistemas multitarea no preventivos, pero estos pueden requerir que modifique su código para que todos los hilos rindan (por ejemplo, con una
pthread_yield
implementación), y se hace más difícil equilibrar las cargas de trabajo.Aquí hay algunos ejemplos simplistas de temporizadores de metal desnudo:
lidiar con conflictos de memoria. En particular, cada hilo necesitará una pila única si desea codificar en C u otros lenguajes de alto nivel.
Podrías limitar los hilos para que tengan un tamaño de pila máximo fijo, pero la mejor manera de lidiar con esto es con paginación que permite pilas eficientes de "tamaño ilimitado".
Aquí hay un ejemplo ingenuo de aarch64 baremetal que explotaría si la pila crece demasiado
Esas son algunas buenas razones para usar el kernel de Linux o algún otro sistema operativo :-)
Userland primitivas de sincronización de memoria
Aunque el inicio / detención / administración de subprocesos generalmente está más allá del alcance del usuario, sin embargo, puede usar las instrucciones de ensamblaje de los subprocesos del usuario para sincronizar los accesos a la memoria sin llamadas al sistema potencialmente más costosas.
Por supuesto, debería preferir el uso de bibliotecas que envuelvan de forma portátil estas primitivas de bajo nivel. El estándar de C ++ se ha hecho grandes avances en los
<mutex>
y<atomic>
las cabeceras, y en particular constd::memory_order
. No estoy seguro de si cubre todas las semánticas de memoria posibles, pero podría serlo.La semántica más sutil es particularmente relevante en el contexto de estructuras de datos sin bloqueo , que pueden ofrecer beneficios de rendimiento en ciertos casos. Para implementarlos, es probable que tenga que aprender un poco sobre los diferentes tipos de barreras de memoria: https://preshing.com/20120710/memory-barriers-are-like-source-control-operations/
Boost, por ejemplo, tiene algunas implementaciones de contenedores sin bloqueo en: https://www.boost.org/doc/libs/1_63_0/doc/html/lockfree.html
Dichas instrucciones de usuario también parecen usarse para implementar la
futex
llamada al sistema Linux , que es una de las primitivas de sincronización principales en Linux.man futex
4.15 lee:El nombre de syscall en sí significa "Fast Userspace XXX".
Aquí hay un ejemplo mínimo inútil de C ++ x86_64 / aarch64 con ensamblaje en línea que ilustra el uso básico de tales instrucciones principalmente para divertirse:
main.cpp
GitHub aguas arriba .
Salida posible:
De esto vemos que el prefijo x86 LOCK / aarch64
LDADD
instrucción hizo que la suma fuera atómica: sin ella tenemos condiciones de carrera en muchas de las adiciones, y el recuento total al final es menor que el 20000 sincronizado.Ver también:
Probado en Ubuntu 19.04 amd64 y con el modo de usuario QEMU aarch64.
fuente
#include
(lo toma como un comentario), NASM, FASM, YASM no conocen la sintaxis de AT&T, por lo que no pueden ser ellos ... entonces, ¿qué es?gcc
,#include
proviene del preprocesador C. Utilice loMakefile
proporcionado como se explica en la sección de inicio: github.com/cirosantilli/x86-bare-metal-examples/blob/… Si eso no funciona, abra un problema de GitHub.Según tengo entendido, cada "núcleo" es un procesador completo, con su propio conjunto de registros. Básicamente, el BIOS comienza con un núcleo en ejecución, y luego el sistema operativo puede "iniciar" otros núcleos inicializándolos y apuntándolos al código a ejecutar, etc.
La sincronización la realiza 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 toca qué memoria y qué hacer en caso de una colisión de memoria.
fuente
Las preguntas frecuentes no oficiales de SMP
Érase una vez, para escribir el ensamblador x86, por ejemplo, tendría instrucciones que indicaran "cargue el registro EDX con el valor 5", "incremente el registro EDX", etc. Con 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)?
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.
¿Existe ahora un concepto de "contexto de CPU" o "hilo" en el ensamblador x86?
No. El ensamblador solo traduce las instrucciones como siempre hacía. No hay cambios allí.
¿Cómo funciona la comunicación / sincronización entre los núcleos?
Como comparten la misma memoria, es principalmente una cuestión de lógica del programa. Aunque ahora hay un mecanismo de interrupción entre procesadores , no es necesario y originalmente no estaba presente en los primeros sistemas x86 de doble CPU.
Si estaba escribiendo un sistema operativo, ¿qué mecanismo está expuesto 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 sobre las secciones críticas y los tipos de bloqueos utilizados. Antes de SMP, el código del kernel eventualmente llamaría al planificador, que miraría la cola de ejecución y elegiría un proceso para ejecutar como el siguiente subproceso. (Los procesos en el núcleo se parecen mucho a los hilos). El núcleo 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 elegir accidentalmente El mismo PID.
¿Es alguna instrucción privilegiada especial (es)?
No. Todos los núcleos se ejecutan en la misma memoria con las mismas instrucciones anteriores.
Si estuviera escribiendo un VM compilador / bytecode optimizador para una CPU multinúcleo, ¿qué necesitaría saber específicamente sobre, por ejemplo, x86 para que genere código que se ejecute eficientemente en todos los núcleos?
Ejecutas el mismo código que antes. Es el núcleo de Unix o Windows el 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 multinúcleo?"
Nada era necesario Los primeros sistemas SMP utilizaron exactamente el mismo conjunto de instrucciones que los uniprocesadores. Ahora, ha habido una gran cantidad de evolución de la arquitectura x86 y miles de millones de nuevas instrucciones para acelerar el proceso, pero ninguna fue necesaria para SMP.
Para obtener más información, consulte la Especificación de multiprocesador Intel .
Actualización: todas las preguntas de seguimiento se pueden responder simplemente aceptando que una CPU multinúcleo n- way es casi 1 exactamente lo mismo que n procesadores separados 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 esos no obtendrán núcleos separados, pero siempre que la biblioteca de subprocesos use funciones de subprocesos del núcleo, su programa de subprocesos será automáticamente multinúcleo.
1. Para la compatibilidad con versiones anteriores, solo el primer núcleo se inicia en el reinicio, y algunas cosas de tipo controlador deben hacerse para activar los restantes.
2. También comparten todos los periféricos, naturalmente.
fuente
Como alguien que escribe la optimización de VM de compilador / bytecode, puedo ayudarlo aquí.
No necesita saber nada específicamente sobre x86 para que genere código que se ejecute de manera eficiente en todos los núcleos.
Sin embargo, es posible que necesite saber acerca de cmpxchg y sus amigos para escribir código que se ejecute correctamente en todos los núcleos. La programación multinúcleo requiere el uso de sincronización y comunicación entre hilos 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 le sería útil aprender:
Debe conocer las facilidades que ofrece el sistema operativo (Linux o Windows u OSX) para permitirle ejecutar múltiples subprocesos. Debería aprender acerca de las API de paralelización, como OpenMP y Threading Building Blocks, o el próximo "Grand Central" de OSX 10.6 "Snow Leopard".
Debe considerar si su compilador debe estar en paralelo automáticamente, o si el autor de las aplicaciones compiladas por su compilador necesita agregar sintaxis especial o llamadas a la API en su programa para aprovechar los múltiples núcleos.
fuente
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 se dará cuenta de que hay más de un núcleo o en qué núcleo se está ejecutando.
Tampoco hay instrucciones adicionales solo disponibles 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 a las áreas de memoria comunes utilizadas para el intercambio de información para encontrar la siguiente área de memoria para ejecutar.
Esta es una simplificación, pero le da la idea básica de cómo se hace. Más información sobre multinúcleos y multiprocesadores en Embedded.com tiene mucha información sobre este tema ... ¡Este tema se complica muy rápidamente!
fuente
El código de ensamblaje se traducirá en código de máquina que se ejecutará en un núcleo. Si desea que sea multiproceso, deberá 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 subproceso diferente. Cada hilo solo verá un núcleo en el que se está ejecutando actualmente.
fuente
No se hace en las instrucciones de la máquina en absoluto; los núcleos pretenden ser CPU distintas y no tienen capacidades especiales para comunicarse entre ellos. Hay dos formas de comunicarse:
Comparten el espacio de direcciones físicas. El hardware maneja la coherencia de la memoria 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 la memoria asignada en el espacio de direcciones físicas, y puede ser utilizada por un procesador para controlar los demás, encenderlos o apagarlos, enviar interrupciones, etc.
http://www.cheesecake.org/sac/smp.html es una buena referencia con una url tonta.
fuente
La principal diferencia entre una aplicación de subprocesos simples y múltiples es que la primera tiene una pila y la segunda tiene una para cada subproceso. El código se genera de manera algo diferente ya que el compilador asumirá que los registros de datos y segmentos 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 tampoco lo hará en ds (porque ds! = Ss). Por el contrario, la indirección a través de los otros registros que predeterminan a ds no lo hará a ss.
Los hilos comparten todo lo demás, incluidas las áreas de datos y códigos. También comparten rutinas lib, así que asegúrese de que sean seguras para subprocesos. Un procedimiento que clasifica un área en la RAM puede ser multiproceso para acelerar las cosas. Luego, los subprocesos accederán, compararán y ordenarán datos en la misma área de memoria física y ejecutarán el mismo código pero utilizando diferentes variables locales para controlar su respectiva parte del género. Por supuesto, esto se debe a que los subprocesos tienen diferentes pilas donde están contenidas las variables locales. Este tipo de programación requiere un ajuste cuidadoso del código para que se reduzcan 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 ajustar a menudo será más rápido con un procesador que con dos o más. La depuración es más difícil porque el punto de interrupción estándar "int 3" no será aplicable ya que desea interrumpir un hilo específico y no todos. Los puntos de interrupción del registro de depuración tampoco resuelven este problema a menos que pueda establecerlos en el procesador específico ejecutando 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.
fuente
Lo que se ha agregado en cada arquitectura con capacidad de multiprocesamiento en comparación con las variantes de procesador único que vinieron antes que ellas son instrucciones para sincronizar entre núcleos. Además, tiene instrucciones para lidiar con la coherencia de la memoria caché, las memorias intermedias de vaciado y operaciones similares de bajo nivel con las que tiene que 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 ceder explícitamente el procesador cuando no hay nada que hacer) .
Pero la semántica básica de un solo hilo es la misma, solo agrega funciones adicionales para manejar la sincronización y la comunicación con otros núcleos.
fuente