Che aspetto ha il linguaggio assembly multicore?


243

Una volta, per scrivere un assemblatore x86, ad esempio, avresti le istruzioni che affermavano "carica il registro EDX con il valore 5", "incrementa il registro EDX", ecc.

Con le moderne CPU che hanno 4 core (o anche più), a livello di codice macchina sembra che ci siano 4 CPU separate (cioè ci sono solo 4 distinti registri "EDX")? In tal caso, quando si dice "incrementa il registro EDX", cosa determina quale registro EDX della CPU viene incrementato? Esiste un concetto di "contesto CPU" o "thread" nell'assemblatore x86?

Come funziona la comunicazione / sincronizzazione tra i core?

Se stavi scrivendo un sistema operativo, quale meccanismo è esposto tramite hardware per consentirti di pianificare l'esecuzione su core diversi? Sono alcune istruzioni speciali privilegiate?

Se stavi scrivendo un compilatore / bytecode VM ottimizzato per una CPU multicore, cosa avresti bisogno di sapere in particolare, diciamo, x86 per farlo generare codice che funziona in modo efficiente su tutti i core?

Quali modifiche sono state apportate al codice macchina x86 per supportare la funzionalità multi-core?


2
C'è una domanda simile (anche se non identica) qui: stackoverflow.com/questions/714905/…
Nathan Fellman,

Risposte:


153

Questa non è una risposta diretta alla domanda, ma è una risposta a una domanda che appare nei commenti. In sostanza, la domanda è: quale supporto fornisce l'hardware all'operazione multi-thread.

Nicholas Flynt aveva ragione , almeno per quanto riguarda x86. In un ambiente multi-thread (Hyper-thread, multi-core o multiprocessore), il thread Bootstrap (in genere thread 0 nel core 0 nel processore 0) avvia il recupero del codice dall'indirizzo 0xfffffff0. Tutti gli altri thread si avviano in uno stato di sospensione speciale chiamato Wait-for-SIPI . Come parte della sua inizializzazione, il thread primario invia uno speciale inter-processore-interrupt (IPI) sull'APIC chiamato SIPI (Startup IPI) a ciascun thread che si trova in WFS. Il SIPI contiene l'indirizzo da cui quel thread dovrebbe iniziare a recuperare il codice.

Questo meccanismo consente a ciascun thread di eseguire codice da un indirizzo diverso. Tutto ciò che serve è il supporto software per ogni thread per impostare le proprie tabelle e code di messaggistica. Il sistema operativo utilizza quelli a fare la programmazione reale multi-threaded.

Per quanto riguarda l'assemblaggio vero e proprio, come ha scritto Nicholas, non c'è differenza tra gli assiemi per un'applicazione a thread singolo o multi thread. Ogni thread logico ha il proprio set di registri, quindi scrivendo:

mov edx, 0

verrà aggiornato solo EDXper il thread attualmente in esecuzione . Non è possibile modificare EDXsu un altro processore utilizzando una singola istruzione di assemblaggio. È necessaria una sorta di chiamata di sistema per chiedere al sistema operativo di dire a un altro thread di eseguire il codice che aggiornerà il proprio EDX.


2
Grazie per colmare il vuoto nella risposta di Nicholas. Ho contrassegnato la tua come risposta accettata ora .... fornisce i dettagli specifici a cui ero interessato ... anche se sarebbe meglio se ci fosse una sola risposta che avesse le tue informazioni e Nicholas 'tutte combinate.
Paul Hollingsworth,

3
Questo non risponde alla domanda da dove provengono i thread. I core e i processori sono una cosa hardware, ma in qualche modo i thread devono essere creati nel software. Come fa il thread principale a sapere dove inviare il SIPI? O la stessa SIPI crea un nuovo thread?
rich remer

7
@richremer: sembra che tu stia confondendo i thread HW e i thread SW. Il thread HW esiste sempre. A volte dorme. La stessa SIPI riattiva il thread HW e gli consente di eseguire SW. Spetta al sistema operativo e al BIOS decidere quali thread HW vengono eseguiti e quali processi e thread SW vengono eseguiti su ciascun thread HW.
Nathan Fellman,

2
Molte informazioni buone e concise qui, ma questo è un argomento importante, quindi le domande possono persistere. Ci sono alcuni esempi di kernel "bare bones" completi che si avviano da unità USB o dischi "floppy" - ecco una versione x86_32 scritta in assembler usando i vecchi descrittori TSS che possono effettivamente eseguire codice C multi-thread ( github. com / duanev / oz-x86-32-asm-003 ) ma non esiste un supporto libreria standard. Abbastanza un po 'di più di quello che hai chiesto, ma può forse rispondere ad alcune di quelle domande persistenti.
duanev,

87

Esempio baremetal eseguibile minimo Intel x86

Esempio di metallo nudo eseguibile con tutta la piastra della caldaia richiesta . Tutte le parti principali sono descritte di seguito.

Testato su Ubuntu 15.10 QEMU 2.3.0 e Lenovo ThinkPad T400 guest hardware reale .

La Guida alla programmazione del sistema Manuale del volume 3 di Intel - 325384-056US settembre 2015 copre SMP nei capitoli 8, 9 e 10.

Tabella 8-1 "Broadcast INIT-SIPI-SIPI Sequence and Choice of Timeouts" contiene un esempio che sostanzialmente funziona:

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

Su quel codice:

  1. La maggior parte dei sistemi operativi renderà impossibile la maggior parte di tali operazioni dall'anello 3 (programmi utente).

    Quindi è necessario scrivere il proprio kernel per giocare liberamente con esso: un programma Linux userland non funzionerà.

  2. Inizialmente, viene eseguito un singolo processore, chiamato bootstrap processor (BSP).

    Deve svegliare gli altri (chiamati Application Processors (AP)) attraverso speciali interrupt chiamati Inter Processor Interrupts (IPI) .

    Tali interruzioni possono essere eseguite programmando Advanced Programmable Interrupt Controller (APIC) tramite il registro dei comandi di interruzione (ICR)

    Il formato dell'ICR è documentato in: 10.6 "EMISSIONE DI INTERRUZIONI INTERPROCESSORI"

    L'IPI si verifica non appena scriviamo all'ICR.

  3. ICR_LOW è definito in 8.4.4 "Esempio di inizializzazione MP" come:

    ICR_LOW EQU 0FEE00300H
    

    Il valore magico 0FEE00300è l'indirizzo di memoria dell'ICR, come documentato nella Tabella 10-1 "Mappa dell'indirizzo del registro APIC locale"

  4. Nell'esempio viene utilizzato il metodo più semplice possibile: imposta l'ICR per inviare IPI broadcast che vengono consegnati a tutti gli altri processori tranne quello corrente.

    Ma è anche possibile, e consigliato da alcuni , ottenere informazioni sui processori attraverso speciali strutture di dati impostate dal BIOS come le tabelle ACPI o la tabella di configurazione MP di Intel e solo svegliare quelle necessarie una ad una.

  5. XXin 000C46XXHcodifica l'indirizzo della prima istruzione che il processore eseguirà come:

    CS = XX * 0x100
    IP = 0
    

    Ricorda che CS moltiplica gli indirizzi per0x10 , quindi l'indirizzo di memoria effettivo della prima istruzione è:

    XX * 0x1000
    

    Quindi, se per esempio XX == 1, il processore inizierà alle 0x1000.

    Dobbiamo quindi assicurarci che ci sia un codice in modalità reale a 16 bit da eseguire in quella posizione di memoria, ad esempio 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
    

    L'uso di uno script di linker è un'altra possibilità.

  6. I loop di ritardo sono una parte fastidiosa per iniziare a lavorare: non esiste un modo super semplice per fare esattamente tali dormienti.

    I metodi possibili includono:

    • PIT (usato nel mio esempio)
    • HPET
    • calibrare il tempo di un circuito occupato con quanto sopra e usarlo invece

    Correlati: Come visualizzare un numero sullo schermo e dormire per un secondo con l'assembly x86 DOS?

  7. Penso che il processore iniziale debba essere in modalità protetta perché questo funzioni mentre scriviamo per indirizzare 0FEE00300Hche è troppo alto per 16 bit

  8. Per comunicare tra processori, possiamo utilizzare uno spinlock sul processo principale e modificare il blocco dal secondo core.

    Dovremmo assicurarci che la scrittura della memoria sia terminata, ad es wbinvd.

Stato condiviso tra processori

8.7.1 "Stato dei processori logici" dice:

Le seguenti funzionalità fanno parte dello stato architettonico dei processori logici all'interno dei processori Intel 64 o IA-32 che supportano la tecnologia Intel Hyper-Threading. Le funzioni possono essere suddivise in tre gruppi:

  • Duplicato per ciascun processore logico
  • Condiviso da processori logici in un processore fisico
  • Condiviso o duplicato, a seconda dell'implementazione

Le seguenti funzionalità sono duplicate per ciascun processore logico:

  • Registri di uso generale (EAX, EBX, ECX, EDX, ESI, EDI, ESP ed EBP)
  • Registri di segmento (CS, DS, SS, ES, FS e GS)
  • Registri EFLAGS e EIP. Si noti che i registri CS ed EIP / RIP per ciascun processore logico puntano al flusso di istruzioni per il thread eseguito dal processore logico.
  • Registri FPU x87 (da ST0 a ST7, parola di stato, parola di controllo, parola tag, puntatore operando dati e puntatore istruzione)
  • Registri MMX (da MM0 a MM7)
  • Registri XMM (da XMM0 a XMM7) e registro MXCSR
  • Registri di controllo e registri dei puntatori della tabella di sistema (GDTR, LDTR, IDTR, registro attività)
  • Registri di debug (DR0, DR1, DR2, DR3, DR6, DR7) e MSR di controllo del debug
  • Stato globale controllo macchina (IA32_MCG_STATUS) e capacità di controllo macchina (IA32_MCG_CAP) MSR
  • MSR di controllo della gestione dell'alimentazione ACPI e modulazione termica
  • Contatori di timestamp MSR
  • La maggior parte degli altri registri MSR, inclusa la tabella degli attributi di pagina (PAT). Vedi le eccezioni di seguito.
  • Registri APIC locali.
  • Registri di uso generale aggiuntivi (R8-R15), registri XMM (XMM8-XMM15), registro di controllo, IA32_EFER su processori Intel 64.

Le seguenti funzioni sono condivise dai processori logici:

  • Registri di intervallo del tipo di memoria (MTRR)

Se le seguenti funzionalità sono condivise o duplicate è specifico dell'implementazione:

  • IA32_MISC_ENABLE MSR (indirizzo MSR 1A0H)
  • MSR di architettura di controllo macchina (MCA) (ad eccezione dei MSR IA32_MCG_STATUS e IA32_MCG_CAP)
  • Controllo del monitoraggio delle prestazioni e contatore MSR

La condivisione della cache è discussa su:

Gli hyperthread Intel hanno una maggiore condivisione della cache e della pipeline rispetto ai core separati: /superuser/133082/hyper-threading-and-dual-core-whats-the-difference/995858#995858

Kernel Linux 4.2

L'azione di inizializzazione principale sembra essere a arch/x86/kernel/smpboot.c.

Esempio baremetal minimo eseguibile ARM

Qui fornisco un esempio minimale di ARMv8 aarch64 eseguibile per 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 a monte .

Montare ed eseguire:

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 \
;

In questo esempio, mettiamo la CPU 0 in un loop di spinlock e si esce solo con CPU 1 che rilascia lo spinlock.

Dopo lo spinlock, CPU 0 esegue quindi una chiamata di uscita semihost che fa chiudere QEMU.

Se avvii QEMU con una sola CPU -smp 1, la simulazione si blocca per sempre sullo spinlock.

La CPU 1 viene riattivata con l'interfaccia PSCI, maggiori dettagli su: ARM: Start / Wakeup / Recupera gli altri core / AP della CPU e passa l'indirizzo iniziale dell'esecuzione?

La versione upstream ha anche alcune modifiche per farlo funzionare su gem5, in modo da poter sperimentare anche le caratteristiche delle prestazioni.

Non l'ho testato su hardware reale, quindi non sono sicuro di quanto sia portatile. Potrebbe essere interessante la seguente bibliografia su Raspberry Pi:

Questo documento fornisce alcune indicazioni sull'uso delle primitive di sincronizzazione ARM che è possibile utilizzare per fare cose divertenti con più core: http://infocenter.arm.com/help/topic/com.arm.doc.dht0008a/DHT0008A_arm_synchronization_primitives.pdf

Testato su Ubuntu 18.10, GCC 8.2.0, Binutils 2.31.1, QEMU 2.12.0.

Prossimi passi per una più comoda programmabilità

Gli esempi precedenti riattivano la CPU secondaria ed eseguono la sincronizzazione di base della memoria con istruzioni dedicate, il che è un buon inizio.

Ma per semplificare la programmazione dei sistemi multicore, ad esempio POSIX pthreads , è necessario approfondire i seguenti argomenti più coinvolti:

  • l'installazione si interrompe ed esegue un timer che decide periodicamente quale thread verrà eseguito ora. Questo è noto come multithreading preventivo .

    Tale sistema deve anche salvare e ripristinare i registri thread mentre vengono avviati e arrestati.

    È anche possibile disporre di sistemi multitasking non preventivi, ma questi potrebbero richiedere di modificare il codice in modo che ogni thread produca (ad esempio con pthread_yieldun'implementazione), e diventa più difficile bilanciare i carichi di lavoro.

    Ecco alcuni esempi di timer bare metal semplicistici:

  • affrontare i conflitti di memoria. In particolare, ogni thread avrà bisogno di uno stack univoco se si desidera codificare in C o altre lingue di alto livello.

    Potresti semplicemente limitare i thread ad avere una dimensione massima dello stack fissa, ma il modo migliore per gestirlo è con il paging che consente stack efficienti di "dimensioni illimitate".

    Ecco un ingenuo esempio barearch aarch64 che esploderebbe se lo stack cresce troppo in profondità

Questi sono alcuni buoni motivi per usare il kernel Linux o qualche altro sistema operativo :-)

Primitive di sincronizzazione della memoria di Userland

Sebbene l'avvio / arresto / gestione dei thread sia generalmente al di fuori dell'ambito dell'utente, è comunque possibile utilizzare le istruzioni di assemblaggio dei thread dell'utente per sincronizzare gli accessi alla memoria senza chiamate di sistema potenzialmente più costose.

Ovviamente dovresti preferire l'uso di librerie che avvolgono in modo portabile queste primitive di basso livello. Lo standard C ++ si è fatto grandi progressi sui <mutex>e <atomic>intestazioni, e in particolare con std::memory_order. Non sono sicuro se copre tutte le possibili semantiche di memoria ottenibili, ma potrebbe solo.

La semantica più sottile è particolarmente rilevante nel contesto di strutture dati senza blocco , che possono offrire vantaggi in termini di prestazioni in alcuni casi. Per implementarli, dovrai probabilmente imparare un po 'sui diversi tipi di barriere di memoria: https://preshing.com/20120710/memory-barriers-are-like-source-control-operations/

Boost, ad esempio, ha alcune implementazioni di container senza lock su: https://www.boost.org/doc/libs/1_63_0/doc/html/lockfree.html

Tali istruzioni per l'utente sembrano anche essere utilizzate per implementare la futexchiamata di sistema Linux , che è una delle principali primitive di sincronizzazione in Linux. man futex4.15 dice:

La chiamata di sistema futex () fornisce un metodo per attendere fino a quando una determinata condizione diventa vera. In genere viene utilizzato come costrutto di blocco nel contesto della sincronizzazione della memoria condivisa. Quando si usano i futex, la maggior parte delle operazioni di sincronizzazione viene eseguita nello spazio utente. Un programma spazio utente utilizza la chiamata di sistema futex () solo quando è probabile che il programma debba bloccarsi per un periodo più lungo fino a quando la condizione diventa vera. Altre operazioni futex () possono essere utilizzate per riattivare qualsiasi processo o thread in attesa di una particolare condizione.

Il nome syscall stesso significa "Fast Userspace XXX".

Ecco un esempio minimo inutile di C ++ x86_64 / aarch64 con assembly inline che illustra l'utilizzo di base di tali istruzioni principalmente per divertimento:

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 a monte .

Uscita possibile:

my_non_atomic_ulong 15264
my_arch_non_atomic_ulong 15267

Da ciò vediamo che l'istruzione x86 LOCK prefix / aarch64 ha LDADDreso atomica l'aggiunta: senza di essa abbiamo condizioni di gara su molti degli add, e il conteggio totale alla fine è inferiore al 20000 sincronizzato.

Guarda anche:

Testato in Ubuntu 19.04 amd64 e con modalità utente QEMU aarch64.


Quale assemblatore usi per compilare il tuo esempio? A GAS non sembra piacere il tuo #include(lo prende come un commento), NASM, FASM, YASM non conoscono la sintassi AT&T quindi non possono essere loro ... quindi cos'è?
Ruslan,

@Ruslan gcc, #includeproviene dal preprocessore C. Usa il Makefilefornito come spiegato nella sezione per iniziare: github.com/cirosantilli/x86-bare-metal-examples/blob/… Se ciò non funziona, apri un problema con GitHub.
Ciro Santilli 29 冠状 病 六四 事件 法轮功

su x86, cosa succede se un core si rende conto che non ci sono più processi pronti per essere eseguiti in coda? (che potrebbe accadere di tanto in tanto su un sistema inattivo). Lo spinlock principale sulla struttura della memoria condivisa fino a quando non vi è una nuova attività? (probabilmente non va bene userà molta energia) chiama qualcosa come HLT per dormire fino a quando non c'è un interruzione? (in quel caso chi è responsabile di svegliare quel nucleo?)
tigrou,

@tigrou non è sicuro, ma trovo estremamente probabile che l'implementazione di Linux lo metterà in uno stato di alimentazione fino al successivo interruzione (probabile timer), specialmente su ARM in cui l'alimentazione è la chiave. Vorrei provare rapidamente a vedere se ciò può essere osservato concretamente facilmente con una traccia di istruzioni di un simulatore che esegue Linux, potrebbe essere: github.com/cirosantilli/linux-kernel-module-cheat/tree/…
Ciro Santilli 郝海东 冠状 病法轮功 事件 法轮功

1
Alcune informazioni (specifiche per x86 / Windows) sono disponibili qui (consultare "Discussione inattiva"). TL; DR: quando non esiste alcun thread eseguibile su una CPU, la CPU viene inviata a un thread inattivo. Insieme ad alcune altre attività, alla fine chiamerà la routine inattiva del processore di gestione dell'alimentazione registrato (tramite un driver fornito dal fornitore della CPU, ad esempio: Intel). Ciò potrebbe spostare la CPU in uno stato C più profondo (ad esempio: C0 -> C3) per ridurre il consumo energetico.
Tigrou,

43

A quanto ho capito, ogni "core" è un processore completo, con un proprio set di registri. Fondamentalmente, il BIOS ti avvia con un core in esecuzione, quindi il sistema operativo può "avviare" altri core inizializzandoli e indicandoli sul codice da eseguire, ecc.

La sincronizzazione viene eseguita dal sistema operativo. In generale, ciascun processore sta eseguendo un processo diverso per il sistema operativo, quindi la funzionalità multi-threading del sistema operativo è incaricata di decidere quale processo deve toccare quale memoria e cosa fare in caso di collisione della memoria.


28
che pone la domanda però: quali istruzioni sono disponibili per il sistema operativo per fare questo?
Paul Hollingsworth,

4
C'è una serie di istruzioni privilegiate per questo, ma è il problema del sistema operativo, non il codice dell'applicazione. Se il codice dell'applicazione vuole essere multithread, deve chiamare le funzioni del sistema operativo per fare la "magia".
sharptooth,

2
Il BIOS di solito identificherà quanti core sono disponibili e passerà queste informazioni al sistema operativo quando richiesto. Esistono standard a cui il BIOS (e l'hardware) devono conformarsi in modo tale che l'accesso a specifiche hardware (processori, core, bus PCI, schede PCI, mouse, tastiera, grafica, ISA, PCI-E / X, memoria ecc.) Per PC diversi sembra lo stesso dal punto di vista del sistema operativo. Se il BIOS non segnala che ci sono quattro core, il sistema operativo suppone di solito che ce ne sia solo uno. Potrebbe anche esserci un'impostazione del BIOS con cui sperimentare.
Olof Forshell,

1
È fantastico e tutto, ma cosa succede se stai scrivendo un programma bare metal?
Alexander Ryan Baggett,

3
@AlexanderRyanBaggett,? Cos'è quello? Ribadendo, quando diciamo "lascialo al sistema operativo", stiamo evitando la domanda perché la domanda è come fa il sistema operativo allora? Quali istruzioni di montaggio utilizza?
Pacerier,

39

Le FAQ non ufficiali su SMP logo di overflow dello stack


Una volta, per scrivere un assemblatore x86, ad esempio, avresti le istruzioni che dicevano "carica il registro EDX con il valore 5", "incrementa il registro EDX", ecc. Con CPU moderne che hanno 4 core (o anche più) , a livello di codice macchina sembra che ci siano 4 CPU separate (cioè ci sono solo 4 distinti registri "EDX")?

Esattamente. Esistono 4 set di registri, inclusi 4 puntatori di istruzioni separati.

In tal caso, quando si dice "incrementa il registro EDX", cosa determina quale registro EDX della CPU viene incrementato?

La CPU che ha eseguito tale istruzione, naturalmente. Pensalo come 4 microprocessori completamente diversi che condividono semplicemente la stessa memoria.

Esiste un concetto di "contesto CPU" o "thread" nell'assemblatore x86?

No. L'assemblatore traduce semplicemente le istruzioni come sempre. Nessuna modifica lì.

Come funziona la comunicazione / sincronizzazione tra i core?

Poiché condividono la stessa memoria, si tratta principalmente di una logica di programma. Sebbene ora esista un meccanismo di interruzione tra processori , non è necessario e non era originariamente presente nei primi sistemi x86 a doppia CPU.

Se stavi scrivendo un sistema operativo, quale meccanismo è esposto tramite hardware per consentirti di pianificare l'esecuzione su core diversi?

Lo scheduler in realtà non cambia, tranne per il fatto che è leggermente più attento alle sezioni critiche e ai tipi di blocchi utilizzati. Prima di SMP, il codice del kernel avrebbe infine chiamato lo scheduler, che avrebbe esaminato la coda di esecuzione e scelto un processo da eseguire come thread successivo. (I processi nel kernel assomigliano molto ai thread.) Il kernel SMP esegue esattamente lo stesso codice, un thread alla volta, è solo che ora il blocco delle sezioni critiche deve essere sicuro di SMP per essere sicuro che due core non possano accidentalmente scegliere lo stesso PID.

Sono alcune istruzioni privilegiate speciali?

No. I core funzionano tutti nella stessa memoria con le stesse vecchie istruzioni.

Se stavi scrivendo un compilatore / bytecode VM ottimizzato per una CPU multicore, cosa avresti bisogno di sapere in particolare, diciamo, x86 per farlo generare codice che funziona in modo efficiente su tutti i core?

Esegui lo stesso codice di prima. È il kernel Unix o Windows che doveva cambiare.

Potresti riassumere la mia domanda come "Quali modifiche sono state apportate al codice macchina x86 per supportare la funzionalità multi-core?"

Niente era necessario. I primi sistemi SMP utilizzavano lo stesso set di istruzioni dei uniprocessori. Ora, ci sono state molte evoluzioni dell'architettura x86 e miliardi di nuove istruzioni per rendere le cose più veloci, ma nessuna era necessaria per SMP.

Per ulteriori informazioni, consultare la specifica del multiprocessore Intel .


Aggiornamento: è possibile rispondere a tutte le domande di follow-up semplicemente accettando completamente che una CPU multicore n- way è quasi 1 esattamente la stessa cosa di n processori separati che condividono solo la stessa memoria. 2 Non è stata posta una domanda importante: come viene scritto un programma per essere eseguito su più core per una maggiore performance? E la risposta è: è scritto usando una libreria di thread come Pthreads. Alcune librerie di thread usano "thread verdi" che non sono visibili al sistema operativo e quelli non avranno core separati, ma fintanto che la libreria di thread utilizza le funzionalità del thread del kernel, il programma thread sarà automaticamente multicore.
1. Per compatibilità con le versioni precedenti, solo il primo core si avvia al ripristino e alcune operazioni di tipo driver devono essere fatte per accendere quelle rimanenti.
2. Condividono anche tutte le periferiche, naturalmente.


3
Penso sempre che "thread" sia un concetto software, che mi rende difficile comprendere il processore multi-core, il problema è, come possono i codici dire a un core "Ho intenzione di creare un thread in esecuzione nel core 2"? Esiste un codice assembly speciale per farlo?
Demonguy,

2
@demonguy: No, non ci sono istruzioni speciali per nulla del genere. Chiedete al sistema operativo di eseguire il thread su un core specifico impostando una maschera di affinità (che dice "questo thread può essere eseguito su questo set di core logici"). È completamente un problema di software. Ogni core della CPU (thread hardware) esegue indipendentemente Linux (o Windows). Per collaborare con gli altri thread hardware, usano strutture di dati condivise. Ma non puoi mai "direttamente" avviare un thread su una CPU diversa. Dite al sistema operativo che vorreste avere un nuovo thread e prende nota in una struttura di dati che vede il sistema operativo su un altro core.
Peter Cordes,

2
Posso dirlo a OS, ma come può mettere i codici su core specifici?
Demonguy,

4
@demonguy ... (semplificato) ... ogni core condivide l'immagine del sistema operativo e inizia a eseguirla nello stesso posto. Quindi, per 8 core, sono 8 "processi hardware" in esecuzione nel kernel. Ognuno chiama la stessa funzione scheduler che controlla la tabella dei processi per un processo o thread eseguibile. (Questa è la coda di esecuzione. ) Nel frattempo, i programmi con thread funzionano senza consapevolezza della natura SMP sottostante. Devono solo fork (2) o qualcosa del genere e far sapere al kernel che vogliono eseguire. In sostanza, il core trova il processo, piuttosto che il processo che trova il core.
DigitalRoss

1
In realtà non è necessario interrompere un core da un altro. Pensaci in questo modo: tutto ciò di cui avevi bisogno per comunicare prima veniva comunicato bene con meccanismi software. Gli stessi meccanismi software continuano a funzionare. Quindi, pipe, chiamate del kernel, sleep / wakeup, tutta quella roba ... funzionano ancora come prima. Non tutti i processi sono in esecuzione sulla stessa CPU ma hanno le stesse strutture dati per la comunicazione di prima. Lo sforzo di fare SMP è per lo più limitato a far funzionare i vecchi blocchi in un ambiente più parallelo.
DigitalRoss,

10

Se stavi scrivendo un compilatore / bytecode VM ottimizzato per una CPU multicore, cosa avresti bisogno di sapere in particolare, diciamo, x86 per farlo generare codice che funziona in modo efficiente su tutti i core?

Come qualcuno che scrive ottimizzando le macchine virtuali compilatore / bytecode, potrei essere in grado di aiutarti qui.

Non è necessario sapere nulla di specifico su x86 per generare codice che funzioni in modo efficiente su tutti i core.

Tuttavia, potrebbe essere necessario conoscere cmpxchg e gli amici per poter scrivere codice che funzioni correttamente su tutti i core. La programmazione multicore richiede l'uso della sincronizzazione e della comunicazione tra thread di esecuzione.

Potrebbe essere necessario conoscere qualcosa su x86 per farlo generare codice che funziona in modo efficiente su x86 in generale.

Ci sono altre cose che sarebbe utile imparare:

È necessario conoscere le funzionalità fornite dal sistema operativo (Linux o Windows o OSX) per consentire l'esecuzione di più thread. Dovresti conoscere le API di parallelizzazione come OpenMP e Threading Building Blocks o l'imminente "Grand Central" di OSX 10.6 "Snow Leopard".

Dovresti considerare se il tuo compilatore dovrebbe essere auto-parallelizzante o se l'autore delle applicazioni compilate dal tuo compilatore deve aggiungere una sintassi speciale o chiamate API nel suo programma per sfruttare i core multipli.


Diverse macchine virtuali popolari come .NET e Java hanno il problema che il loro processo GC principale è coperto da blocchi e fondamentalmente a filetto singolo?
Marco van de Voort,

9

Ogni core viene eseguito da una diversa area di memoria. Il tuo sistema operativo punterà un core sul tuo programma e il core eseguirà il tuo programma. Il tuo programma non sarà consapevole che ci sono più di un core o su quale core sta eseguendo.

Non sono inoltre disponibili ulteriori istruzioni per il sistema operativo. Questi core sono identici ai chip single core. Ogni core esegue una parte del sistema operativo che gestirà la comunicazione con le aree di memoria comuni utilizzate per lo scambio di informazioni per trovare l'area di memoria successiva da eseguire.

Questa è una semplificazione ma ti dà l'idea di base di come è fatta. Maggiori informazioni su multicore e multiprocessori su Embedded.com contengono molte informazioni su questo argomento ... Questo argomento si complica molto rapidamente!


Penso che si dovrebbe distinguere un po 'più attentamente qui come il multicore funziona in generale e quanto influenza il sistema operativo. "Ogni core viene eseguito da una diversa area di memoria" è troppo fuorviante secondo me. Innanzitutto, l'utilizzo di più core in linea di principio non ha bisogno di questo, e puoi facilmente vedere che per un programma thread vorresti che due core due lavorassero sugli stessi segmenti di testo e dati (mentre ogni core necessita anche di risorse individuali come stack) .
Volker Stolz,

@ShiDoiSi Ecco perché la mia risposta contiene il testo "Questa è una semplificazione" .
Gerhard,

5

Il codice assembly verrà tradotto in codice macchina che verrà eseguito su un core. Se vuoi che sia multithread, dovrai usare le primitive del sistema operativo per avviare questo codice su processori diversi più volte o diversi pezzi di codice su core diversi - ogni core eseguirà un thread separato. Ogni thread vedrà solo un core su cui è attualmente in esecuzione.


4
Stavo per dire qualcosa del genere, ma come fa il sistema operativo a allocare i thread ai core? Immagino che ci siano alcune istruzioni di montaggio privilegiate che lo realizzano. In tal caso, penso che sia la risposta che l'autore sta cercando.
A. Levy,

Non ci sono istruzioni per questo, questo è il dovere dello scheduler del sistema operativo. Esistono funzioni del sistema operativo come SetThreadAffinityMask in Win32 e il codice può chiamarle, ma è roba del sistema operativo e influenza lo scheduler, non è un'istruzione del processore.
sharptooth,

2
Deve esserci un OpCode, altrimenti il ​​sistema operativo non sarebbe in grado di farlo.
Matthew Whited,

1
Non proprio un codice operativo per la pianificazione: è più come ottenere una copia del sistema operativo per processore, condividendo uno spazio di memoria; ogni volta che un core rientra nel kernel (syscall o interrupt), guarda le stesse strutture di dati in memoria per decidere quale thread eseguire successivamente.
pjc50,

1
@ A.Levy: quando si avvia un thread con un'affinità che lo consente solo di essere eseguito su un altro core, non si sposta immediatamente sull'altro core. Ha il suo contesto salvato in memoria, proprio come un normale cambio di contesto. Gli altri thread hardware vedono la sua voce nelle strutture dati dello scheduler e uno di loro deciderà infine che eseguirà il thread. Quindi dal punto di vista del primo core: scrivi su una struttura di dati condivisa e alla fine il codice del sistema operativo su un altro core (thread hardware) lo noterà e lo eseguirà.
Peter Cordes,

3

Non è affatto fatto nelle istruzioni della macchina; i core fingono di essere CPU distinte e non hanno alcuna capacità speciale di parlare tra loro. Esistono due modi in cui comunicano:

  • condividono lo spazio degli indirizzi fisici. L'hardware gestisce la coerenza della cache, quindi una CPU scrive su un indirizzo di memoria che un'altra legge.

  • condividono un APIC (controller di interrupt programmabile). Questa è la memoria mappata nello spazio degli indirizzi fisici e può essere utilizzata da un processore per controllare gli altri, accenderli o spegnerli, inviare interruzioni, ecc.

http://www.cheesecake.org/sac/smp.html è un buon riferimento con un URL sciocco.


2
In realtà non condividono un APIC. Ogni CPU logica ne ha una propria. Gli APIC comunicano tra loro, ma sono separati.
Nathan Fellman,

Si sincronizzano (piuttosto che comunicare) in un modo di base e cioè attraverso il prefisso LOCK (l'istruzione "xchg mem, reg" contiene una richiesta di blocco implicita) che corre verso il pin di blocco che corre su tutti i bus, dicendo loro che la CPU (in realtà qualsiasi dispositivo di masterizzazione del bus) desidera un accesso esclusivo al bus. Alla fine un segnale tornerà al pin LOCKA (riconoscimento) per dire alla CPU che ora ha accesso esclusivo al bus. Poiché i dispositivi esterni sono molto più lenti del funzionamento interno della CPU, per completare una sequenza LOCK / LOCKA potrebbero essere necessarie diverse centinaia di cicli CPU.
Olof Forshell,

1

La principale differenza tra un'applicazione a thread singolo e multi-thread è che il primo ha uno stack e il secondo ha uno per ogni thread. Il codice viene generato in modo leggermente diverso dal momento che il compilatore supporrà che i registri dei segmenti di dati e stack (ds e ss) non siano uguali. Questo significa che l'indirizzamento indiretto attraverso i registri ebp ed esp che di default al registro ss non sarà predefinito anche a ds (perché ds! = Ss). Al contrario, l'indirizzamento indiretto attraverso gli altri registri che per impostazione predefinita ds non verrà impostato su ss.

I thread condividono tutto il resto, compresi i dati e le aree di codice. Condividono anche routine lib, quindi assicurati che siano thread-safe. Una procedura che ordina un'area nella RAM può essere multi-thread per accelerare le cose. I thread quindi accederanno, confronteranno e ordineranno i dati nella stessa area di memoria fisica ed eseguiranno lo stesso codice ma usando variabili locali diverse per controllare le rispettive parti dell'ordinamento. Questo ovviamente perché i thread hanno stack diversi in cui sono contenute le variabili locali. Questo tipo di programmazione richiede un'attenta regolazione del codice in modo da ridurre le collisioni di dati inter-core (nella cache e nella RAM) che a loro volta si traducono in un codice che è più veloce con due o più thread rispetto a uno solo. Naturalmente, un codice non ottimizzato sarà spesso più veloce con un processore che con due o più. Debug è più impegnativo perché il breakpoint standard "int 3" non sarà applicabile poiché si desidera interrompere un thread specifico e non tutti. I punti di interruzione del registro di debug non risolvono questo problema, a meno che non sia possibile impostarli sul processore specifico eseguendo il thread specifico che si desidera interrompere.

Un altro codice multi-thread può comportare thread diversi in esecuzione in diverse parti del programma. Questo tipo di programmazione non richiede lo stesso tipo di messa a punto ed è quindi molto più facile da imparare.


0

Ciò che è stato aggiunto su ogni architettura che supporta il multiprocessing rispetto alle varianti a singolo processore che sono state precedute da esse sono le istruzioni per la sincronizzazione tra i core. Inoltre, hai le istruzioni per gestire la coerenza della cache, svuotare i buffer e operazioni simili di basso livello che un sistema operativo deve gestire. Nel caso di architetture multithread simultanee come IBM POWER6, IBM Cell, Sun Niagara e Intel "Hyperthreading", si tende anche a vedere nuove istruzioni per stabilire le priorità tra i thread (come l'impostazione delle priorità e la resa esplicita del processore quando non c'è nulla da fare) .

Ma la semantica di base a thread singolo è la stessa, basta aggiungere ulteriori funzionalità per gestire la sincronizzazione e la comunicazione con altri core.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.