Che aspetto ha il linguaggio di assemblaggio multicore?

Una volta, per scrivere un assemblatore x86, ad esempio, si avrebbero istruzioni che indicano “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 proprio che ci siano 4 CPU separate (cioè ci sono solo 4 distinti registri “EDX”)? In tal caso, quando si dice “incrementare il registro EDX”, cosa determina quale registro EDX della CPU viene incrementato? Esiste ora un concetto “CPU context” o “thread” nell’assembler x86?

Come funziona la comunicazione / sincronizzazione tra i core?

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

Se stavate scrivendo un VM ottimizzatore / codice bytecode per una CPU multicore, cosa dovreste sapere in particolare su, ad esempio, x86 per far sì che generi codice che funzioni in modo efficiente su tutti i core?

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

Questa non è una risposta diretta alla domanda, ma è una risposta a una domanda che appare nei commenti. In sostanza, la domanda è: cosa supporta l’hardware nell’operazione multi-thread.

Nicholas Flynt aveva ragione , almeno per quanto riguarda x86. In un ambiente multi-thread (Hyper-threading, multi-core o multiprocessore), il thread Bootstrap (di solito il 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-processor-interrupt (IPI) sull’APIC chiamato SIPI (IPI di avvio) a ciascun thread presente in WFS. Il SIPI contiene l’indirizzo da cui quel thread dovrebbe iniziare il recupero del 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 configurare le proprie tabelle e le code di messaggistica. Il sistema operativo utilizza quelli per eseguire la pianificazione multi-thread effettiva.

Per quanto riguarda l’assemblaggio effettivo, come ha scritto Nicholas, non c’è differenza tra gli assemblaggi di un’applicazione singola filettata o multi-thread. Ogni thread logico ha il proprio set di registri, quindi scrivere:

 mov edx, 0 

aggiornerà solo EDX per il thread attualmente in esecuzione . Non c’è modo di modificare EDX su un altro processore usando una singola istruzione di assemblaggio. È necessario un qualche tipo di chiamata di sistema per chiedere al sistema operativo di dire a un altro thread di eseguire codice che aggiornerà il proprio EDX .

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

La sincronizzazione viene eseguita dal sistema operativo. Generalmente, ogni processore esegue un processo diverso per il sistema operativo, quindi la funzionalità multi-threading del sistema operativo è incaricata di decidere quale processo può toccare quale memoria e cosa fare in caso di collisione di memoria.

Esempio minimal runnable Intel x86 minimale

Esempio di metallo nudo eseguibile con tutte le piastre necessarie . Tutte le parti principali sono coperte di seguito.

Testato su Ubuntu 15.10 QEMU 2.3.0 e Lenovo ThinkPad T400.

Manuale di programmazione del sistema Intel Manual Volume 3 – 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 fondamentalmente funziona solo:

 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à imansible la maggior parte di queste operazioni dall’anello 3 (programmi utente).

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

  2. All’inizio, viene eseguito un singolo processore, chiamato processore bootstrap (BSP).

    Deve triggersre gli altri (chiamati Application Processor (AP)) tramite interrupt speciali chiamati Inter Processor Interrupts (IPI) .

    Questi interrupt possono essere eseguiti programmando l’Advanced Programmable Interrupt Controller (APIC) tramite il registro di comando Interrupt (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 “MP Initialization Example” 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 ansible: imposta l’ICR per inviare IPI broadcast che vengono consegnati a tutti gli altri processori tranne quello corrente.

    Ma è anche ansible, 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 risvegliare quelle necessarie ad una ad una.

  5. XX in 000C46XXH codifica l’indirizzo della prima istruzione che il processore eseguirà come:

     CS = XX * 0x100 IP = 0 

    Ricorda che CS moltiplica gli indirizzi di 0x10 , quindi l’effettivo indirizzo di memoria della prima istruzione è:

     XX * 0x1000 

    Quindi, se per esempio XX == 1 , il processore inizierà a 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 linker è un’altra possibilità.

  6. I ritardi dei loop sono una parte fastidiosa per funzionare: non esiste un modo super semplice per fare un tale sonno con precisione.

    I metodi possibili includono:

    • PIT (usato nel mio esempio)
    • HPET
    • calibrare l’ora di un ciclo occupato con quanto sopra, e usarlo al suo posto

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

  7. Penso che il processore iniziale debba essere in modalità protetta affinché funzioni come scriviamo per indirizzare 0FEE00300H che è troppo alto per 16-bit

  8. Per comunicare tra i processori, possiamo usare uno spinlock nel processo principale e modificare il blocco dal secondo core.

    Dovremmo assicurarci che la scrittura della memoria sia fatta, ad es. Tramite 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 funzionalità 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 segmenti (CS, DS, SS, ES, FS e GS)
  • EFLAGS e registri EIP. Notare che i registri CS ed EIP / RIP per ciascun processore logico puntano al stream di istruzioni per il thread che viene eseguito dal processore logico.
  • Registri FPU x87 (da ST0 a ST7, parola di stato, parola di controllo, parola tag, puntatore dell’operando di dati e puntatore di istruzioni)
  • Registri MMX (da MM0 a MM7)
  • Registri XMM (da XMM0 a XMM7) e il registro MXCSR
  • Registri di controllo e registri puntatori della tabella di sistema (GDTR, LDTR, IDTR, registro attività)
  • Registri di debug (DR0, DR1, DR2, DR3, DR6, DR7) e controllo MSR di debug
  • MSR controllo stato macchina (IA32_MCG_STATUS) e controllo macchina (IA32_MCG_CAP) della macchina
  • Modulazione orologio termico e MSR controllo gestione alimentazione ACPI
  • Contatore orario MSR
  • La maggior parte degli altri registri MSR, inclusa la tabella degli attributi della pagina (PAT). Vedi le eccezioni qui sotto.
  • Registri APIC locali.
  • Registri generali aggiuntivi (R8-R15), registri XMM (XMM8-XMM15), registro di controllo, IA32_EFER su processori Intel 64.

Le seguenti funzionalità sono condivise dai processori logici:

  • Registri di portata del tipo di memoria (MTRR)

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

  • IA32_MISC_ENABLE MSR (indirizzo MSR 1A0H)
  • MSR dell’MC (Machine Check Architecture) (ad eccezione degli MSR IA32_MCG_STATUS e IA32_MCG_CAP)
  • Controllo del monitoraggio delle prestazioni e contatore MSR

La condivisione della cache è discussa a:

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

Kernel Linux 4.2

L’azione di inizializzazione principale sembra essere in arch/x86/kernel/smpboot.c .

Esempi di ARM

ARM sembra essere un po ‘più facile da configurare rispetto a x86 in quanto ha un overhead meno storico, qui ci sono due esempi minimali eseguibili:

TODO: rivedi questi esempi e spiegali meglio qui.

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

Domande frequenti su SMP non ufficiale stack overflow logo


C’era una volta, per scrivere un assemblatore x86, per esempio, avresti 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 proprio che ci siano 4 CPU separate (cioè ci sono solo 4 distinti registri “EDX”)?

Esattamente. Esistono 4 serie di registri, tra cui 4 indicatori di istruzioni separati.

In tal caso, quando si dice “incrementare il registro EDX”, cosa determina quale registro EDX della CPU viene incrementato?

La CPU che ha eseguito quell’istruzione, naturalmente. Pensa a 4 microprocessori completamente diversi che condividono semplicemente la stessa memoria.

Esiste ora un concetto “CPU context” o “thread” nell’assembler x86?

No. L’assemblatore traduce semplicemente le istruzioni come sempre. Nessun cambiamento lì.

Come funziona la comunicazione / sincronizzazione tra i core?

Dal momento che condividono la stessa memoria, è principalmente una questione di logica del programma. Sebbene ora ci sia un meccanismo di interrupt 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 permetterti di pianificare l’esecuzione su diversi core?

Lo scheduler in realtà non cambia, tranne che è un po ‘più attentamente sulle sezioni critiche e sui tipi di lock 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 lo stesso codice esatto, un thread alla volta, è solo che ora il blocco della sezione critico deve essere SMP-safe per essere sicuro che due core non possano accidentalmente scegliere lo stesso PID.

Sono alcune istruzioni speciali privilegiate?

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

Se stavate scrivendo un VM ottimizzatore / codice bytecode per una CPU multicore, cosa dovreste sapere in particolare su, ad esempio, x86 per far sì che generi codice che funzioni in modo efficiente su tutti i core?

Hai eseguito lo stesso codice di prima. È il kernel Unix o Windows che ha bisogno di cambiare.

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

Niente era necessario. I primi sistemi SMP utilizzavano esattamente lo stesso set di istruzioni dei processori uniprocessori. Ora, c’è stata una grande quantità di evoluzione dell’architettura x86 e milioni di nuove istruzioni per rendere le cose più veloci, ma nessuna era necessaria per SMP.

Per ulteriori informazioni, consultare la Specifica multiprocessore Intel .


Aggiornamento: tutte le domande di follow-up possono essere risolte semplicemente accettando che una CPU multicore n -way è quasi esattamente la stessa cosa di n processori separati che condividono solo la stessa memoria. 2 C’era una domanda importante non posta: come è scritto un programma per funzionare su più di un core per maggiori prestazioni? E la risposta è: è scritto usando una libreria di thread come Pthreads. Alcune librerie di thread utilizzano “thread verdi” che non sono visibili al sistema operativo, e quelli non otterranno core separati, ma finché la libreria di thread utilizza le funzionalità del thread del kernel, il programma filettato sarà automaticamente multicore.


1. Per compatibilità con le versioni precedenti, solo il primo core si avvia al reset e alcune cose del tipo driver devono essere eseguite per accendere quelle rimanenti.
2. Condividono anche tutte le periferiche, naturalmente.

Ogni core viene eseguito da un’area di memoria diversa. Il tuo sistema operativo punta un core al tuo programma e il core eseguirà il tuo programma. Il tuo programma non sarà consapevole del fatto che esistono più core o su quale core è in esecuzione.

Non ci sono inoltre istruzioni aggiuntive disponibili solo per il sistema operativo. Questi core sono identici ai chip single core. Ogni core esegue una parte del sistema operativo che gestirà la comunicazione alle 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 su come è fatta. Maggiori informazioni su multicore e multiprocessori su Embedded.com contengono molte informazioni su questo argomento … Questo argomento si complica molto rapidamente!

Se stavate scrivendo un VM ottimizzatore / codice bytecode per una CPU multicore, cosa dovreste sapere in particolare su, ad esempio, x86 per far sì che generi codice che funzioni in modo efficiente su tutti i core?

Come qualcuno che scrive VM di compilazione / codice 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 scrivere codice che funzioni correttamente su tutti i core. La programmazione multicore richiede l’uso della sincronizzazione e della comunicazione tra i thread di esecuzione.

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

Ci sono altre cose che ti sarebbero utili per imparare:

Dovresti 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 OSX 10.6 “Grand Central” di Snow Leopard.

Dovresti considerare se il tuo compilatore debba essere auto-parallelizzato, o se l’autore delle applicazioni compilate dal tuo compilatore ha bisogno di aggiungere una speciale syntax o chiamate API nel suo programma per sfruttare i molteplici core.

Il codice assembly verrà convertito in codice macchina che verrà eseguito su un core. Se si desidera che sia multithreading, sarà necessario utilizzare le primitive del sistema operativo per avviare questo codice su processori diversi più volte o parti di codice differenti su core diversi: ogni core eseguirà un thread separato. Ogni thread vedrà solo un core su cui è attualmente in esecuzione.

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

  • loro condividono lo spazio di indirizzo fisico. L’hardware gestisce la coerenza della cache, quindi una CPU scrive su un indirizzo di memoria che l’altro legge.

  • condividono un APIC (programmable interrupt controller). Questa memoria è mappata nello spazio degli indirizzi fisico e può essere utilizzata da un processore per controllare gli altri, triggersrli o distriggersrli, inviare interrupt, ecc.

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

La principale differenza tra un’applicazione singola e una multi-thread è che il primo ha uno stack e quest’ultimo ha uno per ogni thread. Il codice viene generato in modo leggermente diverso dal momento che il compilatore supporrà che i registri di dati e di stack stack (ds e ss) non siano uguali. Ciò significa che l’indirezione tramite ebp e esp registri che l’impostazione predefinita per il registro ss non sarà predefinita anche per ds (perché ds! = Ss). Viceversa, l’indirezione tramite gli altri registri che di default in ds non imposterà automaticamente su ss.

I thread condividono tutto il resto, compresi i dati e le aree di codice. Inoltre condividono le routine lib per assicurarsi che siano thread-safe. Una procedura che ordina un’area nella RAM può essere multi-thread per velocizzare le cose. I thread accederanno quindi, confrontando e ordinando i dati nella stessa area di memoria fisica e eseguendo lo stesso codice ma utilizzando variabili locali differenti per controllare la rispettiva parte dell’ordinamento. Questo ovviamente perché i thread hanno stack diversi in cui sono contenute le variabili locali. Questo tipo di programmazione richiede un’accurata sintonizzazione del codice in modo da ridurre le collisioni inter-core dei dati (in cache e RAM) che a sua volta produce un codice che è più veloce con due o più thread rispetto a uno solo. Ovviamente, un codice non regolato sarà spesso più veloce con un processore che con due o più. Eseguire il debug è più difficile perché il punto di interruzione 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 ansible impostarli sullo specifico processore che esegue il thread specifico che si desidera interrompere.

Altro codice multi-threaded può coinvolgere thread diversi in esecuzione in diverse parti del programma. Questo tipo di programmazione non richiede lo stesso tipo di ottimizzazione ed è quindi molto più facile da imparare.

Ciò che è stato aggiunto su tutte le architetture con capacità multiprocessing rispetto alle varianti del processore singolo che le hanno precedute sono le istruzioni per la sincronizzazione tra i core. Inoltre, sono disponibili istruzioni per gestire la coerenza della cache, i buffer di svuotamento 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 la priorità tra i thread (come l’impostazione delle priorità e la generazione esplicita del processore quando non c’è niente 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.