¿Cómo se ve el lenguaje ensamblador multinúcleo?

243

É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?

Paul Hollingsworth
fuente
2
Aquí hay una pregunta similar (aunque no idéntica): stackoverflow.com/questions/714905/…
Nathan Fellman

Respuestas:

153

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:

mov edx, 0

solo se actualizará EDXpara el hilo actualmente en ejecución . No hay forma de modificar EDXen 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 suyo EDX.

Nathan Fellman
fuente
2
Gracias por llenar el vacío en la respuesta de Nicholas. Marqué la suya como la respuesta aceptada ahora ... da los detalles específicos que me interesaban ... aunque sería mejor si hubiera una sola respuesta que tuviera su información y la combinación de Nicholas.
Paul Hollingsworth
3
Esto no responde a la pregunta de dónde provienen los hilos. Los núcleos y los procesadores son una cuestión de hardware, pero de alguna manera deben crearse hilos en el software. ¿Cómo sabe el hilo primario dónde enviar el SIPI? ¿O el SIPI mismo crea un nuevo hilo?
Rich Remer
77
@richremer: Parece que estás confundiendo hilos HW y hilos SW. El hilo HW siempre existe. A veces está dormido. El SIPI mismo activa el hilo HW y le permite ejecutar SW. Depende del sistema operativo y del BIOS decidir qué subprocesos HW se ejecutan y qué procesos y subprocesos SW se ejecutan en cada subproceso HW.
Nathan Fellman
2
Aquí hay mucha información buena y concisa, pero este es un gran tema, por lo que las preguntas pueden persistir. Hay algunos ejemplos de núcleos "básicos" completos en la naturaleza que se inician desde unidades USB o discos "floppy". Aquí hay una versión x86_32 escrita en ensamblador que usa los descriptores TSS antiguos que pueden ejecutar código C multiproceso ( github. com / duanev / oz-x86-32-asm-003 ) pero no hay soporte de biblioteca estándar. Mucho más de lo que pediste, pero tal vez pueda responder algunas de esas preguntas persistentes.
duanev
87

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:

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:

  1. 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á.

  2. 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.

  3. ICR_LOW se define en 8.4.4 "Ejemplo de inicialización MP" como:

    ICR_LOW EQU 0FEE00300H
    

    El valor mágico 0FEE00300es la dirección de memoria del ICR, como se documenta en la Tabla 10-1 "Mapa de dirección de registro APIC local"

  4. 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.

  5. XXEn 000C46XXHcodifica la dirección de la primera instrucción que el procesador ejecutará como:

    CS = XX * 0x100
    IP = 0
    

    Recuerde que CS multiplica las direcciones por0x10 , por lo que la dirección de memoria real de la primera instrucción es:

    XX * 0x1000
    

    Entonces, por ejemplo XX == 1, el procesador comenzará a las 0x1000.

    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:

    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
    

    Usar un script vinculador es otra posibilidad.

  6. 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:

    • PIT (usado en mi ejemplo)
    • HPET
    • calibre el tiempo de un bucle ocupado con lo anterior y úselo en su lugar

    Relacionado: ¿Cómo mostrar un número en la pantalla y dormir durante un segundo con el ensamblaje DOS x86?

  7. Creo que el procesador inicial debe estar en modo protegido para que esto funcione mientras escribimos en una dirección 0FEE00300Hque es demasiado alta para 16 bits

  8. Para 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:

Las siguientes características son 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 características están duplicadas para cada procesador lógico:

  • Registros de uso general (EAX, EBX, ECX, EDX, ESI, EDI, ESP y EBP)
  • Registros de segmento (CS, DS, SS, ES, FS y GS)
  • EFLAGS y registros EIP. Tenga en cuenta que los registros CS y EIP / RIP para cada procesador lógico apuntan a la secuencia de instrucciones para el subproceso que ejecuta el procesador lógico.
  • Registros FPU x87 (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 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 verificación de máquina (IA32_MCG_STATUS) y capacidad de verificación de máquina (IA32_MCG_CAP) MSR
  • Modulación de reloj térmico y control de gestión de energía ACPI MSR
  • Contador de sello de tiempo MSR
  • La mayoría de los otros registros MSR, incluida la tabla de atributos de página (PAT). Ver las excepciones a continuación.
  • Registros locales de APIC.
  • Registros adicionales de propósito general (R8-R15), registros XMM (XMM8-XMM15), registro de control, IA32_EFER en procesadores Intel 64.

Los procesadores lógicos comparten las siguientes características:

  • Registros de rango de tipo de memoria (MTRR)

Si las siguientes características son compartidas o duplicadas es específico de la implementación:

  • IA32_MISC_ENABLE MSR (dirección MSR 1A0H)
  • MSR de arquitectura de verificación de máquina (MCA) (excepto los MSR IA32_MCG_STATUS e IA32_MCG_CAP)
  • Control de supervisión del rendimiento y contador de MSR

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:

.global mystart
mystart:
    /* Reset spinlock. */
    mov x0, #0
    ldr x1, =spinlock
    str x0, [x1]

    /* Read cpu id into x1.
     * TODO: cores beyond 4th?
     * Mnemonic: Main Processor ID Register
     */
    mrs x1, mpidr_el1
    ands x1, x1, 3
    beq cpu0_only
cpu1_only:
    /* Only CPU 1 reaches this point and sets the spinlock. */
    mov x0, 1
    ldr x1, =spinlock
    str x0, [x1]
    /* Ensure that CPU 0 sees the write right now.
     * Optional, but could save some useless CPU 1 loops.
     */
    dmb sy
    /* Wake up CPU 0 if it is sleeping on wfe.
     * Optional, but could save power on a real system.
     */
    sev
cpu1_sleep_forever:
    /* Hint CPU 1 to enter low power mode.
     * Optional, but could save power on a real system.
     */
    wfe
    b cpu1_sleep_forever
cpu0_only:
    /* Only CPU 0 reaches this point. */

    /* Wake up CPU 1 from initial sleep!
     * See:https://github.com/cirosantilli/linux-kernel-module-cheat#psci
     */
    /* PCSI function identifier: CPU_ON. */
    ldr w0, =0xc4000003
    /* Argument 1: target_cpu */
    mov x1, 1
    /* Argument 2: entry_point_address */
    ldr x2, =cpu1_only
    /* Argument 3: context_id */
    mov x3, 0
    /* Unused hvc args: the Linux kernel zeroes them,
     * but I don't think it is required.
     */
    hvc 0

spinlock_start:
    ldr x0, spinlock
    /* Hint CPU 0 to enter low power mode. */
    wfe
    cbz x0, spinlock_start

    /* Semihost exit. */
    mov x1, 0x26
    movk x1, 2, lsl 16
    str x1, [sp, 0]
    mov x0, 0
    str x0, [sp, 8]
    mov x1, sp
    mov w0, 0x18
    hlt 0xf000

spinlock:
    .skip 8

GitHub aguas arriba .

Montar y ejecutar:

aarch64-linux-gnu-gcc \
  -mcpu=cortex-a57 \
  -nostdlib \
  -nostartfiles \
  -Wl,--section-start=.text=0x40000000 \
  -Wl,-N \
  -o aarch64.elf \
  -T link.ld \
  aarch64.S \
;
qemu-system-aarch64 \
  -machine virt \
  -cpu cortex-a57 \
  -d in_asm \
  -kernel aarch64.elf \
  -nographic \
  -semihosting \
  -smp 2 \
;

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_yieldimplementació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 con std::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 futexllamada al sistema Linux , que es una de las primitivas de sincronización principales en Linux. man futex4.15 lee:

La llamada al sistema futex () proporciona un método para esperar hasta que una determinada condición se vuelva verdadera. Por lo general, se usa como una construcción de bloqueo en el contexto de la sincronización de memoria compartida. Cuando se usan futexes, la mayoría de las operaciones de sincronización se realizan en el espacio del usuario. Un programa de espacio de usuario emplea la llamada al sistema futex () solo cuando es probable que el programa tenga que bloquear durante más tiempo hasta que la condición se vuelva verdadera. Se pueden usar otras operaciones futex () para activar cualquier proceso o subproceso que espere una condición particular.

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

#include <atomic>
#include <cassert>
#include <iostream>
#include <thread>
#include <vector>

std::atomic_ulong my_atomic_ulong(0);
unsigned long my_non_atomic_ulong = 0;
#if defined(__x86_64__) || defined(__aarch64__)
unsigned long my_arch_atomic_ulong = 0;
unsigned long my_arch_non_atomic_ulong = 0;
#endif
size_t niters;

void threadMain() {
    for (size_t i = 0; i < niters; ++i) {
        my_atomic_ulong++;
        my_non_atomic_ulong++;
#if defined(__x86_64__)
        __asm__ __volatile__ (
            "incq %0;"
            : "+m" (my_arch_non_atomic_ulong)
            :
            :
        );
        // https://github.com/cirosantilli/linux-kernel-module-cheat#x86-lock-prefix
        __asm__ __volatile__ (
            "lock;"
            "incq %0;"
            : "+m" (my_arch_atomic_ulong)
            :
            :
        );
#elif defined(__aarch64__)
        __asm__ __volatile__ (
            "add %0, %0, 1;"
            : "+r" (my_arch_non_atomic_ulong)
            :
            :
        );
        // https://github.com/cirosantilli/linux-kernel-module-cheat#arm-lse
        __asm__ __volatile__ (
            "ldadd %[inc], xzr, [%[addr]];"
            : "=m" (my_arch_atomic_ulong)
            : [inc] "r" (1),
              [addr] "r" (&my_arch_atomic_ulong)
            :
        );
#endif
    }
}

int main(int argc, char **argv) {
    size_t nthreads;
    if (argc > 1) {
        nthreads = std::stoull(argv[1], NULL, 0);
    } else {
        nthreads = 2;
    }
    if (argc > 2) {
        niters = std::stoull(argv[2], NULL, 0);
    } else {
        niters = 10000;
    }
    std::vector<std::thread> threads(nthreads);
    for (size_t i = 0; i < nthreads; ++i)
        threads[i] = std::thread(threadMain);
    for (size_t i = 0; i < nthreads; ++i)
        threads[i].join();
    assert(my_atomic_ulong.load() == nthreads * niters);
    // We can also use the atomics direclty through `operator T` conversion.
    assert(my_atomic_ulong == my_atomic_ulong.load());
    std::cout << "my_non_atomic_ulong " << my_non_atomic_ulong << std::endl;
#if defined(__x86_64__) || defined(__aarch64__)
    assert(my_arch_atomic_ulong == nthreads * niters);
    std::cout << "my_arch_non_atomic_ulong " << my_arch_non_atomic_ulong << std::endl;
#endif
}

GitHub aguas arriba .

Salida posible:

my_non_atomic_ulong 15264
my_arch_non_atomic_ulong 15267

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.

Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功
fuente
¿Qué ensamblador usas para compilar tu ejemplo? A GAS no parece gustarle #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?
Ruslan
@Ruslan gcc, #includeproviene del preprocesador C. Utilice lo Makefileproporcionado 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.
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功
en x86, ¿qué sucede si un núcleo se da cuenta de que no hay más procesos listos para ejecutarse en la cola? (que puede ocurrir de vez en cuando en un sistema inactivo). ¿El spinlock central en la estructura de memoria compartida hasta que haya una nueva tarea? (probablemente no sea bueno porque usará mucha energía) ¿llama a algo como HLT para dormir hasta que haya una interrupción? (en ese caso, ¿quién es responsable de despertar ese núcleo?)
tigrou
@tigrou no estoy seguro, pero creo que es extremadamente probable que la implementación de Linux lo ponga en estado de energía hasta la próxima interrupción (probable temporizador), especialmente en ARM donde la energía es la clave. Intentaría rápidamente ver si eso se puede observar concretamente fácilmente con un seguimiento de instrucciones de un simulador que ejecuta Linux, podría ser: github.com/cirosantilli/linux-kernel-module-cheat/tree/…
Ciro Santilli 郝海东 冠状 病六四 事件 法轮功
1
Aquí puede encontrar alguna información (específica para x86 / Windows) (consulte "Hilo inactivo"). TL; DR: cuando no existe un subproceso ejecutable en una CPU, la CPU se envía a un subproceso inactivo. Junto con algunas otras tareas, finalmente llamará a la rutina inactiva registrada del procesador de administración de energía (a través de un controlador proporcionado por el proveedor de CPU, por ejemplo: Intel). Esto puede hacer que la CPU pase a un estado C más profundo (por ejemplo: C0 -> C3) para reducir el consumo de energía.
tigrou
43

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.

Nicholas Flynt
fuente
28
lo que plantea la pregunta: ¿Qué instrucciones hay disponibles para que el sistema operativo haga esto?
Paul Hollingsworth
44
Hay un conjunto de instrucciones privilegiadas para eso, pero es el problema del sistema operativo, no el código de la aplicación. Si el código de la aplicación quiere ser multiproceso, debe llamar a las funciones del sistema operativo para hacer la "magia".
sharptooth
2
El BIOS generalmente identificará cuántos núcleos están disponibles y pasará esta información al sistema operativo cuando se le solicite. Existen estándares que el BIOS (y el hardware) deben cumplir de manera tal que el acceso a los detalles del hardware (procesadores, núcleos, bus PCI, tarjetas PCI, mouse, teclado, gráficos, ISA, PCI-E / X, memoria, etc.) para diferentes PC se ve igual desde el punto de vista del sistema operativo. Si el BIOS no informa que hay cuatro núcleos, el sistema operativo generalmente asumirá que solo hay uno. Incluso puede haber una configuración de BIOS para experimentar.
Olof Forshell
1
Eso es genial y todo, pero ¿y si estás escribiendo un programa de metal desnudo?
Alexander Ryan Baggett
3
@AlexanderRyanBaggett,? ¿Qué es eso incluso? Reiterando, cuando decimos "déjelo al sistema operativo", estamos evitando la pregunta porque la pregunta es ¿cómo lo hace entonces el sistema operativo? ¿Qué instrucciones de montaje usa?
Pacerier
39

Las preguntas frecuentes no oficiales de SMP logotipo de desbordamiento de pila


É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.

DigitalRoss
fuente
3
Siempre pienso que "hilo" es un concepto de software, lo que me hace difícil entender el procesador multinúcleo, el problema es, ¿cómo pueden los códigos decirle a un núcleo "voy a crear un hilo ejecutándose en el núcleo 2"? ¿Hay algún código de ensamblaje especial para hacerlo?
demonguy
2
@demonguy: No, no hay instrucciones especiales para algo así. Pide al sistema operativo que ejecute su subproceso en un núcleo específico configurando una máscara de afinidad (que dice "este subproceso puede ejecutarse en este conjunto de núcleos lógicos"). Es completamente un problema de software. Cada núcleo de CPU (hilo de hardware) ejecuta Linux (o Windows) de forma independiente. Para trabajar junto con los otros hilos de hardware, utilizan estructuras de datos compartidos. Pero nunca inicia "directamente" un hilo en una CPU diferente. Le dice al sistema operativo que desea tener un nuevo hilo, y toma nota en una estructura de datos que ve el sistema operativo en otro núcleo.
Peter Cordes
2
Puedo decirlo, pero ¿cómo poner los códigos en un núcleo específico?
demonguy
44
@demonguy ... (simplificado) ... cada núcleo comparte la imagen del sistema operativo y comienza a ejecutarla en el mismo lugar. Entonces, para 8 núcleos, eso es 8 "procesos de hardware" que se ejecutan en el núcleo. Cada uno llama a la misma función de planificador que verifica la tabla de proceso para un proceso o subproceso ejecutable. (Esa es la cola de ejecución ) . Mientras tanto, los programas con subprocesos funcionan sin tener conciencia de la naturaleza subyacente de SMP. Simplemente bifurcan (2) o algo y le hacen saber al kernel que quieren correr. Esencialmente, el núcleo encuentra el proceso, en lugar de que el proceso encuentre el núcleo.
DigitalRoss
1
En realidad, no necesita interrumpir un núcleo de otro. Piénselo de esta manera: todo lo que necesitaba para comunicarse antes se comunicaba perfectamente con los mecanismos de software. Los mismos mecanismos de software siguen funcionando. Entonces, las tuberías, las llamadas del kernel, el sueño / activación, todas esas cosas ... todavía funcionan como antes. No todos los procesos se ejecutan en la misma CPU, pero tienen las mismas estructuras de datos para la comunicación que tenían antes. El esfuerzo para ir a SMP se limita principalmente a hacer que los bloqueos antiguos funcionen en un entorno más paralelo.
DigitalRoss
10

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?

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.

Alex Brown
fuente
¿No tiene varias máquinas virtuales populares como .NET y Java tienen el problema de que su proceso principal de GC está cubierto por bloqueos y fundamentalmente de un solo subproceso?
Marco van de Voort
9

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!

Gerhard
fuente
Creo que aquí debería distinguirse un poco más cuidadosamente cómo funciona el multinúcleo en general y cuánto influye el sistema operativo. "Cada núcleo se ejecuta desde una memoria diferente", es demasiado engañoso en mi opinión. En primer lugar, el uso de múltiples núcleos en principios no necesita esto, y puede ver fácilmente que para un programa enhebrado QUIERES que dos núcleos funcionen en los mismos segmentos de texto y datos (mientras que cada núcleo también necesita recursos individuales como la pila) .
Volker Stolz
@ShiDoiSi Es por eso que mi respuesta contiene el texto "Esta es una simplificación" .
Gerhard
5

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.

diente filoso
fuente
44
Iba a decir algo como esto, pero ¿cómo asigna el sistema operativo hilos a los núcleos? Me imagino que hay algunas instrucciones de ensamblaje privilegiadas que logran esto. Si es así, creo que esa es la respuesta que busca el autor.
A. Levy
No hay instrucciones para eso, ese es el deber del planificador del sistema operativo. Hay funciones del sistema operativo como SetThreadAffinityMask en Win32 y el código puede llamarlas, pero es material del sistema operativo y afecta al programador, no es una instrucción del procesador.
sharptooth
2
Debe haber un OpCode o de lo contrario el sistema operativo tampoco podría hacerlo.
Matthew Whited
1
Realmente no es un código de operación para la programación: es más como si obtuviera una copia del sistema operativo por procesador, compartiendo un espacio de memoria; cada vez que un núcleo vuelve a ingresar al núcleo (syscall o interrupt), observa las mismas estructuras de datos en la memoria para decidir qué subproceso ejecutará a continuación.
pjc50
1
@ A.Levy: cuando comienzas un hilo con una afinidad que solo le permite ejecutarse en un núcleo diferente, no se mueve inmediatamente al otro núcleo. Tiene su contexto guardado en la memoria, al igual que un cambio de contexto normal. Los otros subprocesos de hardware ven su entrada en las estructuras de datos del planificador, y uno de ellos finalmente decidirá que ejecutará el subproceso. Entonces, desde la perspectiva del primer núcleo: escribe en una estructura de datos compartida y, finalmente, el código del sistema operativo en otro núcleo (hilo de hardware) lo notará y lo ejecutará.
Peter Cordes
3

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.

pjc50
fuente
2
De hecho, no comparten un APIC. Cada CPU lógica tiene su propia. Los APIC se comunican entre ellos, pero están separados.
Nathan Fellman
Se sincronizan (en lugar de comunicarse) de una manera básica y es a través del prefijo LOCK (la instrucción "xchg mem, reg" contiene una solicitud de bloqueo implícita) que se ejecuta en el pin de bloqueo que se ejecuta en todos los buses de manera efectiva diciéndoles que la CPU (en realidad, cualquier dispositivo de masterización de bus) quiere acceso exclusivo al bus. Finalmente, una señal volverá al pin LOCKA (confirmación) que le indica a la CPU que ahora tiene acceso exclusivo al bus. Dado que los dispositivos externos son mucho más lentos que el funcionamiento interno de la CPU, una secuencia LOCK / LOCKA puede requerir muchos cientos de ciclos de CPU para completarse.
Olof Forshell
1

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.

Olof Forshell
fuente
0

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.

jakobengblom2
fuente