Jak wygląda język asemblera?


243

Dawno, dawno temu, aby na przykład napisać asembler x86, miałbyś instrukcję mówiącą: „załaduj rejestr EDX wartością 5”, „zwiększ rejestr EDX” itp.

W nowoczesnych procesorach, które mają 4 rdzenie (lub nawet więcej), na poziomie kodu maszynowego wygląda to tak, jakby były 4 oddzielne procesory (tj. Czy są tylko 4 różne rejestry „EDX”)? Jeśli tak, kiedy powiesz „zwiększ rejestr EDX”, co decyduje o zwiększeniu rejestru EDX procesora? Czy w asemblerze x86 jest teraz koncepcja „kontekstu procesora” lub „wątku”?

Jak działa komunikacja / synchronizacja między rdzeniami?

Jeśli piszesz system operacyjny, jaki mechanizm jest udostępniany sprzętowo, aby umożliwić zaplanowanie wykonania na różnych rdzeniach? Czy to jakieś specjalne uprzywilejowane instrukcje?

Jeśli piszesz optymalizujący kompilator / kod bajtowy maszyny wirtualnej dla procesora wielordzeniowego, co musisz wiedzieć konkretnie o, powiedzmy, x86, aby wygenerować kod, który działa wydajnie na wszystkich rdzeniach?

Jakie zmiany wprowadzono do kodu maszynowego x86 w celu obsługi funkcji wielordzeniowej?


2
Tutaj jest podobne (choć nie identyczne) pytanie: stackoverflow.com/questions/714905/…
Nathan Fellman

Odpowiedzi:


153

To nie jest bezpośrednia odpowiedź na pytanie, ale odpowiedź na pytanie pojawiające się w komentarzach. Zasadniczo pytanie brzmi, jakie wsparcie sprzętowe zapewnia dla operacji wielowątkowych.

Nicholas Flynt miał rację , przynajmniej jeśli chodzi o x86. W środowisku wielowątkowym (hiperwątkowość, wielordzeniowy lub wieloprocesorowy) wątek Bootstrap (zwykle wątek 0 w rdzeniu 0 w procesorze 0) rozpoczyna pobieranie kodu z adresu 0xfffffff0. Wszystkie pozostałe wątki uruchamiane są w specjalnym stanie uśpienia zwanym Wait-for-SIPI . W ramach inicjalizacji wątek główny wysyła specjalne przerwanie między procesorem (IPI) przez APIC o nazwie SIPI (Startup IPI) do każdego wątku w systemie plików WFS. SIPI zawiera adres, z którego ten wątek powinien rozpocząć pobieranie kodu.

Ten mechanizm pozwala każdemu wątkowi wykonać kod z innego adresu. Wszystko, czego potrzeba, to wsparcie oprogramowania dla każdego wątku w celu skonfigurowania własnych tabel i kolejek wiadomości. System operacyjny używa ich do faktycznego planowania wielowątkowego.

Jeśli chodzi o rzeczywisty zespół, jak napisał Nicholas, nie ma różnicy między zespołami dla aplikacji jedno- lub wielowątkowej. Każdy wątek logiczny ma własny zestaw rejestrów, więc zapisywanie:

mov edx, 0

zaktualizuje tylko EDXdla aktualnie działającego wątku . Nie ma możliwości modyfikacji EDXna innym procesorze za pomocą pojedynczej instrukcji asemblera. Potrzebujesz jakiegoś wywołania systemowego, aby poprosić system operacyjny, aby nakazał innemu wątkowi uruchomienie kodu, który zaktualizuje swój własny EDX.


2
Dzięki za wypełnienie luki w odpowiedzi Mikołaja. Oznaczam teraz Twoją jako zaakceptowaną odpowiedź .... podaje konkretne szczegóły, którymi byłem zainteresowany ... chociaż byłoby lepiej, gdyby istniała jedna odpowiedź, która połączyłaby twoje informacje i Mikołaja.
Paul Hollingsworth,

3
To nie odpowiada na pytanie, skąd pochodzą wątki. Rdzenie i procesory to kwestia sprzętowa, ale w jakiś sposób należy tworzyć wątki w oprogramowaniu. Skąd główny wątek wie, gdzie wysłać SIPI? A może sam SIPI tworzy nowy wątek?
rich remer

7
@richremer: Wygląda na to, że mylisz wątki HW i SW. Wątek HW zawsze istnieje. Czasami śpi. Sam SIPI budzi wątek HW i pozwala mu uruchomić SW. Od systemu operacyjnego i systemu BIOS zależy, które wątki HW będą działać, a które procesy i wątki SW będą działać na każdym wątku.
Nathan Fellman

2
Wiele dobrych i zwięzłych informacji tutaj, ale to duży temat - więc pytania mogą pozostać. Istnieje kilka przykładów kompletnych jąder „gołych kości”, które uruchamiają się z dysków USB lub „dyskietek” - oto wersja x86_32 napisana w asemblerze przy użyciu starych deskryptorów TSS, które mogą faktycznie uruchamiać wielowątkowy kod C ( github. com / duanev / oz-x86-32-asm-003 ), ale nie ma standardowej obsługi bibliotek. To trochę więcej, niż prosiłeś, ale może odpowiedzieć na niektóre z tych długich pytań.
duanev

87

Przykład minimalnego uruchomienia systemu Intel x86

Przykład z gołego metalu do pracy ze wszystkimi wymaganymi płytami grzewczymi . Wszystkie główne części są omówione poniżej.

Testowane na prawdziwym sprzęcie Ubuntu 15.10 QEMU 2.3.0 i Lenovo ThinkPad T400 .

Intel Manual Volume 3 System Programming Guide - 325384-056US września 2015 r okładki SMP w rozdziałach 8, 9 i 10.

Tabela 8-1. „Transmisja INIT-SIPI-SIPI Sekwencja i wybór limitów czasu” zawiera przykład, który w zasadzie działa:

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

Na tym kodzie:

  1. Większość systemów operacyjnych uniemożliwia większość tych operacji w pierścieniu 3 (programy użytkownika).

    Musisz więc napisać własne jądro, aby swobodnie się z nim bawić: program Linux dla użytkowników nie będzie działał.

  2. Na początku działa pojedynczy procesor, zwany procesorem ładowania początkowego (BSP).

    Musi obudzić pozostałe (zwane procesorami aplikacji (AP)) za pomocą specjalnych przerwań zwanych przerwaniami między procesorami (IPI) .

    Przerwania te można wykonać, programując zaawansowany programowalny kontroler przerwań (APIC) za pomocą rejestru poleceń przerwań (ICR)

    Format ICR jest udokumentowany pod adresem: 10.6 „WYDAWANIE PRZERWÓW INTERPROCESOROWYCH”

    IPI ma miejsce, gdy tylko piszemy do ICR.

  3. ICR_LOW zdefiniowano w 8.4.4 „Przykład inicjalizacji MP” jako:

    ICR_LOW EQU 0FEE00300H
    

    Magiczną wartością 0FEE00300jest adres pamięci ICR, jak udokumentowano w Tabeli 10-1 „Lokalna mapa adresów rejestru APIC”

  4. W tym przykładzie użyto najprostszej możliwej metody: ustawia ona ICR do wysyłania IPI emisji, które są dostarczane do wszystkich innych procesorów oprócz bieżącego.

    Ale jest również możliwe i zalecane przez niektórych , aby uzyskać informacje o procesorach poprzez specjalne struktury danych ustawione przez BIOS, takie jak tabele ACPI lub tabela konfiguracji MP firmy Intel i wybudzaj tylko te, których potrzebujesz jeden po drugim.

  5. XXin 000C46XXHkoduje adres pierwszej instrukcji, którą procesor wykona jako:

    CS = XX * 0x100
    IP = 0
    

    Pamiętaj, że CS zwielokrotnia adresy0x10 , więc rzeczywisty adres pamięci pierwszej instrukcji to:

    XX * 0x1000
    

    Więc jeśli na przykład XX == 1 procesor rozpocznie się od 0x1000.

    Musimy wtedy upewnić się, że w tym miejscu pamięci działa 16-bitowy kod trybu rzeczywistego, np .:

    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
    

    Inną możliwością jest użycie skryptu linkera.

  6. Pętle opóźniające są denerwującą częścią do pracy: nie ma super prostego sposobu, aby dokładnie spać.

    Możliwe metody obejmują:

    • PIT (używane w moim przykładzie)
    • HPET
    • skalibruj czas zajętej pętli za pomocą powyższego i użyj jej zamiast tego

    Powiązane: Jak wyświetlić liczbę na ekranie i spać przez sekundę z zestawem DOS x86?

  7. Myślę, że początkowy procesor musi być w trybie chronionym, aby to działało, ponieważ piszemy na adres, 0FEE00300Hktóry jest zbyt wysoki dla 16-bitów

  8. Aby komunikować się między procesorami, możemy użyć blokady na głównym procesie i zmodyfikować blokadę z drugiego rdzenia.

    Powinniśmy upewnić się, że zapisywanie pamięci zostało wykonane, np wbinvd. Poprzez .

Stan współdzielony między procesorami

8.7.1 „Stan procesorów logicznych” mówi:

Poniższe funkcje są częścią stanu logicznego procesorów w procesorach Intel 64 lub IA-32 obsługujących technologię Intel Hyper-Threading. Funkcje można podzielić na trzy grupy:

  • Duplikowane dla każdego procesora logicznego
  • Współdzielone przez procesory logiczne w procesorze fizycznym
  • Udostępnione lub zduplikowane, w zależności od implementacji

Następujące funkcje są duplikowane dla każdego procesora logicznego:

  • Rejestry ogólnego przeznaczenia (EAX, EBX, ECX, EDX, ESI, EDI, ESP i EBP)
  • Rejestry segmentowe (CS, DS, SS, ES, FS i GS)
  • Rejestry EFLAGS i EIP. Należy zauważyć, że rejestry CS i EIP / RIP dla każdego procesora logicznego wskazują strumień instrukcji dla wątku wykonywanego przez procesor logiczny.
  • Rejestry x87 FPU (ST0 do ST7, słowo statusu, słowo kontrolne, słowo znacznika, wskaźnik operandu danych i wskaźnik instrukcji)
  • Rejestry MMX (od MM0 do MM7)
  • Rejestry XMM (od XMM0 do XMM7) i rejestr MXCSR
  • Rejestry kontrolne i rejestry wskaźników tabeli systemowej (GDTR, LDTR, IDTR, rejestr zadań)
  • Rejestry debugowania (DR0, DR1, DR2, DR3, DR6, DR7) i MSR kontroli debugowania
  • Globalny status kontroli komputera (IA32_MCG_STATUS) i możliwość kontroli maszyny (IA32_MCG_CAP) MSR
  • Modulacja termiczna zegara i kontrolery zarządzania zasilaniem ACPI
  • Licznik znaczników czasu MSR
  • Większość innych rejestrów MSR, w tym tabela atrybutów strony (PAT). Zobacz wyjątki poniżej.
  • Lokalne rejestry APIC.
  • Dodatkowe rejestry ogólnego przeznaczenia (R8-R15), rejestry XMM (XMM8-XMM15), rejestr kontrolny, IA32_EFER na procesorach Intel 64.

Procesory logiczne współużytkują następujące funkcje:

  • Rejestry zakresu typów pamięci (MTRR)

To, czy następujące funkcje są udostępniane czy duplikowane, zależy od implementacji:

  • IA32_MISC_ENABLE MSR (adres MSR 1A0H)
  • MSR architektury kontroli maszyny (MCA) (z wyjątkiem MSR IA32_MCG_STATUS i IA32_MCG_CAP)
  • Kontrola monitorowania wydajności i licznik MSR

Udostępnianie pamięci podręcznej omówiono na stronie:

Hyperthreads Intel mają większą pamięć podręczną i współużytkowanie potoku niż oddzielne rdzenie: /superuser/133082/hyper-threading-and-dual-core-whats-the-difference/995858#995858

Jądro Linux 4.2

Wydaje się, że główna akcja inicjalizacyjna ma miejsce arch/x86/kernel/smpboot.c .

Przykład minimalnego uruchomienia ARM bez systemu operacyjnego

Tutaj podaję minimalny uruchamialny przykład ARMv8 aarch64 dla 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 w górę .

Złóż i uruchom:

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

W tym przykładzie umieściliśmy CPU 0 w pętli blokady, i wychodzi ona tylko z CPU 1 zwalniającą blokadę.

Po zablokowaniu CPU 0 wykonuje następnie wywołanie wyjścia semihost, co powoduje, że QEMU kończy pracę.

Jeśli uruchomisz QEMU z jednym procesorem -smp 1, wówczas symulacja wisi na zawsze na spinlocku.

CPU 1 jest budzony z interfejsem PSCI, więcej szczegółów na: ARM: Start / Wakeup / Bringup innych rdzeni CPU / AP i przekazać adres początkowy wykonania?

Wersja upstream ma również kilka poprawek, aby działała na gem5, więc możesz eksperymentować z charakterystyką wydajności.

Nie testowałem tego na prawdziwym sprzęcie, więc nie jestem pewien, jak przenośny. Interesująca może być następująca bibliografia Raspberry Pi:

Ten dokument zawiera wskazówki dotyczące korzystania z operacji podstawowych synchronizacji ARM, których można następnie używać do zabawy z wieloma rdzeniami: http://infocenter.arm.com/help/topic/com.arm.doc.dht0008a/DHT0008A_arm_synchronization_primitives.pdf

Testowane na Ubuntu 18.10, GCC 8.2.0, Binutils 2.31.1, QEMU 2.12.0.

Kolejne kroki dla wygodniejszego programowania

Poprzednie przykłady budzą dodatkowy procesor i wykonują podstawową synchronizację pamięci za pomocą dedykowanych instrukcji, co jest dobrym początkiem.

Aby jednak ułatwić programowanie systemów wielordzeniowych, np. POSIX pthreads , należy również przejść do następujących bardziej zaangażowanych tematów:

  • Instalator przerywa i uruchamia licznik, który okresowo decyduje, który wątek zostanie uruchomiony. Jest to znane jako zapobiegawcza wielowątkowość .

    Taki system musi także zapisywać i przywracać rejestry wątków podczas ich uruchamiania i zatrzymywania.

    Możliwe są również nieprzewidywalne systemy wielozadaniowe, ale mogą one wymagać modyfikacji kodu, tak aby każdy wątek przynosił (np. Z pthread_yieldimplementacją), i trudniej było zrównoważyć obciążenia.

    Oto kilka uproszczonych przykładów timera bez systemu metalowego:

  • radzić sobie z konfliktami pamięci. W szczególności każdy wątek będzie wymagał unikalnego stosu, jeśli chcesz pisać w C lub innych językach wysokiego poziomu.

    Możesz po prostu ograniczyć wątki, aby mieć ustalony maksymalny rozmiar stosu, ale lepszym sposobem radzenia sobie z tym jest stronicowanie, które pozwala na wydajne stosy „nieograniczonego rozmiaru”.

    Oto naiwny przykład z czystego metalu aarch64, który wybuchłby, gdyby stos urósł zbyt głęboko

Oto kilka dobrych powodów, aby używać jądra Linux lub innego systemu operacyjnego :-)

Prymitywy synchronizacji pamięci użytkownika

Chociaż uruchamianie / zatrzymywanie wątków / zarządzanie wątkami jest zasadniczo poza obszarem użytkownika, możesz jednak użyć instrukcji montażu z wątków użytkownika, aby zsynchronizować dostęp do pamięci bez potencjalnie droższych wywołań systemowych.

Oczywiście powinieneś preferować używanie bibliotek, które przenośnie owijają te prymitywy niskiego poziomu. Sam standard C ++ poczynił ogromne postępy w zakresie nagłówków <mutex>i <atomic>nagłówków, aw szczególności zstd::memory_order . Nie jestem pewien, czy obejmuje całą możliwą semantykę pamięci możliwą do osiągnięcia, ale może po prostu.

Bardziej subtelna semantyka jest szczególnie istotna w kontekście struktur danych bez blokowania , które w niektórych przypadkach mogą zapewnić korzyści w zakresie wydajności. Aby je wdrożyć, prawdopodobnie będziesz musiał dowiedzieć się trochę o różnych typach barier pamięci: https://preshing.com/20120710/memory-barriers-are-like-source-control-operations/

Na przykład Boost ma pewne implementacje kontenerów bez blokady pod adresem : https://www.boost.org/doc/libs/1_63_0/doc/html/lockfree.html

Wydaje się, że takie instrukcje użytkownika są używane do implementacji futexwywołania systemowego Linux , które jest jednym z głównych prymitywów synchronizacji w systemie Linux. man futex4.15 brzmi:

Wywołanie systemowe futex () zapewnia metodę oczekiwania na spełnienie określonego warunku. Zwykle jest stosowany jako konstrukcja blokująca w kontekście synchronizacji pamięci współdzielonej. Podczas korzystania z futexów większość operacji synchronizacji jest wykonywana w przestrzeni użytkownika. Program przestrzeni użytkownika używa wywołania systemowego futex () tylko wtedy, gdy jest prawdopodobne, że program będzie musiał blokować przez dłuższy czas, aż warunek się spełni. Inne operacje futex () mogą być użyte do wybudzenia dowolnych procesów lub wątków oczekujących na określony warunek.

Syscall sama nazwa oznacza „Fast Userspace XXX”.

Oto minimalny bezużyteczny przykład C ++ x86_64 / aarch64 z wbudowanym zestawem, który ilustruje podstawowe użycie takich instrukcji głównie dla zabawy:

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 w górę .

Możliwe wyjście:

my_non_atomic_ulong 15264
my_arch_non_atomic_ulong 15267

Z tego wynika, że ​​przedrostek x86 LDADDinstrukcji LOCK / aarch64 spowodował, że dodanie było atomowe: bez niego mamy warunki wyścigu dla wielu dodatków, a całkowita liczba na końcu jest mniejsza niż zsynchronizowany 20000.

Zobacz też:

Testowane w Ubuntu 19.04 amd64 i w trybie użytkownika aEM64 QEMU.


Jakiego asemblera używasz do skompilowania swojego przykładu? GAS wydaje się nie lubić twojego #include(traktuje to jako komentarz), NASM, FASM, YASM nie znają składni AT&T, więc to nie może być ich ... więc co to jest?
Ruslan

@ Ruslan gcc, #includepochodzi z preprocesora C. Skorzystaj z Makefiledostarczonej instrukcji, jak wyjaśniono w sekcji „ Pierwsze kroki” : github.com/cirosantilli/x86-bare-metal-examples/blob/… Jeśli to nie zadziała, otwórz problem z GitHub.
Ciro Santilli 29 冠状 病 六四 事件 法轮功

na x86, co się stanie, jeśli rdzeń zda sobie sprawę, że nie ma już procesów gotowych do uruchomienia w kolejce? (co może się zdarzać od czasu do czasu w bezczynnym systemie). Czy rdzeń blokuje się w strukturze pamięci wspólnej, dopóki nie pojawi się nowe zadanie? (prawdopodobnie nie jest dobre, czy zużywa dużo energii) czy nazywa coś takiego jak HLT do spania, dopóki nie nastąpi przerwa? (w takim przypadku, kto jest odpowiedzialny za przebudzenie tego rdzenia?)
tigrou,

@tigrou nie jestem pewien, ale uważam za niezwykle prawdopodobne, że implementacja Linuksa wprowadzi go w stan zasilania aż do następnego (prawdopodobnie timera) przerwania, szczególnie na ARM, gdzie moc jest kluczowa. Spróbowałbym szybko sprawdzić, czy można to zaobserwować w konkretny sposób za pomocą śladu instrukcji symulatora z systemem Linux, może to być: github.com/cirosantilli/linux-kernel-module-cheat/tree/…
Ciro Santilli 郝海东 冠状 病法轮功 事件 法轮功

1
Niektóre informacje (specyficzne dla x86 / Windows) można znaleźć tutaj (patrz „Bezczynny wątek”). TL; DR: gdy na procesorze nie ma wątku wykonalnego, procesor jest wysyłany do wątku bezczynności. Wraz z niektórymi innymi zadaniami ostatecznie wywoła procedurę bezczynności zarejestrowanego procesora zarządzania energią (za pośrednictwem sterownika dostarczonego przez dostawcę procesora, np .: Intel). Może to doprowadzić procesor do głębszego stanu C (np .: C0 -> C3) w celu zmniejszenia zużycia energii.
tigrou

43

Jak rozumiem, każdy „rdzeń” jest kompletnym procesorem, z własnym zestawem rejestrów. Zasadniczo BIOS zaczyna od uruchomienia jednego rdzenia, a następnie system operacyjny może „uruchomić” inne rdzenie, inicjując je i wskazując kodem do uruchomienia itp.

Synchronizacja odbywa się przez system operacyjny. Zasadniczo każdy procesor uruchamia inny proces dla systemu operacyjnego, więc funkcja wielowątkowości systemu operacyjnego odpowiada za wybór procesu, który dotknie, która pamięć i co zrobić w przypadku kolizji pamięci.


28
co nasuwa pytanie: jakie instrukcje są dostępne dla systemu operacyjnego, aby to zrobić?
Paul Hollingsworth,

4
Jest na to szereg uprzywilejowanych instrukcji, ale jest to problem z systemem operacyjnym, a nie z kodem aplikacji. Jeśli kod aplikacji chce być wielowątkowy, musi wywołać funkcje systemu operacyjnego, aby wykonać „magię”.
sharptooth

2
BIOS zwykle określa, ile rdzeni jest dostępnych i przekazuje te informacje do systemu operacyjnego, gdy zostanie o to poproszony. Istnieją standardy, które BIOS (i sprzęt) muszą spełniać, aby dostęp do specyfiki sprzętu (procesory, rdzenie, szyna PCI, karty PCI, mysz, klawiatura, grafika, ISA, PCI-E / X, pamięć itp.) Dla różnych komputerów wygląda tak samo z punktu widzenia systemu operacyjnego. Jeśli BIOS nie zgłasza, że ​​istnieją cztery rdzenie, system operacyjny zwykle zakłada, że ​​jest tylko jeden rdzeń. Może być nawet ustawienie BIOS do eksperymentowania.
Olof Forshell

1
To fajne, ale co jeśli piszesz program bez systemu metalowego?
Alexander Ryan Baggett

3
@AlexanderRyanBaggett,? Co to w ogóle jest? Powtarzając, kiedy mówimy „pozostaw to systemowi operacyjnemu”, unikamy pytania, ponieważ pytaniem jest, w jaki sposób system operacyjny to robi? Jakich instrukcji montażu używa?
Pacerier

39

Często zadawane pytania dotyczące nieoficjalnego SMP logo przepełnienia stosu


Dawno, dawno temu, aby napisać asembler x86, na przykład, będziesz miał instrukcje stwierdzające: „załaduj rejestr EDX wartością 5”, „zwiększ rejestr EDX” itp. Z nowoczesnymi procesorami, które mają 4 rdzenie (lub nawet więcej) , czy na poziomie kodu maszynowego wygląda to tak, jakby były 4 oddzielne procesory (tj. czy są tylko 4 różne rejestry „EDX”)?

Dokładnie. Istnieją 4 zestawy rejestrów, w tym 4 oddzielne wskaźniki instrukcji.

Jeśli tak, kiedy powiesz „zwiększ rejestr EDX”, co decyduje o zwiększeniu rejestru EDX procesora?

Procesor, który wykonał tę instrukcję, oczywiście. Pomyśl o tym jako o 4 zupełnie różnych mikroprocesorach, które po prostu współużytkują tę samą pamięć.

Czy w asemblerze x86 jest teraz koncepcja „kontekstu procesora” lub „wątku”?

Nie. Asembler tłumaczy instrukcje tak jak zawsze. Brak zmian.

Jak działa komunikacja / synchronizacja między rdzeniami?

Ponieważ dzielą tę samą pamięć, jest to głównie kwestia logiki programu. Chociaż obecnie istnieje mechanizm przerwań między procesorami , nie jest on konieczny i nie był pierwotnie obecny w pierwszych dwurdzeniowych procesorach x86.

Jeśli piszesz system operacyjny, jaki mechanizm jest udostępniany sprzętowo, aby umożliwić zaplanowanie wykonania na różnych rdzeniach?

Harmonogram faktycznie się nie zmienia, z tym wyjątkiem, że nieco bardziej ostrożnie podchodzi do krytycznych sekcji i rodzajów używanych blokad. Przed SMP kod jądra ostatecznie wywoływał program planujący, który sprawdzałby kolejkę uruchamiania i wybierał proces do uruchomienia jako następny wątek. (Procesy w jądrze przypominają wątki.) Jądro SMP uruchamia dokładnie ten sam kod, jeden wątek na raz, po prostu teraz krytyczne blokowanie sekcji musi być bezpieczne dla SMP, aby upewnić się, że dwa rdzenie nie mogą przypadkowo wybrać ten sam PID.

Czy to jakieś specjalne uprzywilejowane instrukcje?

Nie. Rdzenie po prostu działają w tej samej pamięci z tymi samymi starymi instrukcjami.

Jeśli piszesz optymalizujący kompilator / kod bajtowy maszyny wirtualnej dla procesora wielordzeniowego, co musisz wiedzieć konkretnie o, powiedzmy, x86, aby wygenerować kod, który działa wydajnie na wszystkich rdzeniach?

Uruchamiasz ten sam kod co poprzednio. Jądro Unixa lub Windowsa wymagało zmiany.

Możesz podsumować moje pytanie jako „Jakie zmiany zostały wprowadzone w kodzie maszynowym x86 w celu obsługi funkcji wielordzeniowej?”

Nic nie było konieczne. Pierwsze systemy SMP używały dokładnie takiego samego zestawu instrukcji jak uniprocesory. Teraz nastąpiło wiele zmian w architekturze x86 i zillionów nowych instrukcji, aby przyspieszyć, ale żadne z nich nie było konieczne dla SMP.

Aby uzyskać więcej informacji, zobacz Specyfikację procesorów Intel .


Aktualizacja: na wszystkie dalsze pytania można odpowiedzieć, po prostu całkowicie akceptując, że n -way wielordzeniowy procesor to prawie 1 dokładnie to samo, co n oddzielnych procesorów, które współużytkują tę samą pamięć. 2 Nie zadano ważnego pytania: w jaki sposób napisano program, aby działał na więcej niż jednym rdzeniu w celu zwiększenia wydajności? Odpowiedź brzmi: jest napisany przy użyciu biblioteki wątków, takiej jak Pthreads. Niektóre biblioteki wątków używają „zielonych wątków”, które nie są widoczne dla systemu operacyjnego, i nie otrzymają oddzielnych rdzeni, ale dopóki biblioteka wątków używa funkcji wątku jądra, twój program wątkowy będzie automatycznie wielordzeniowy.
1. Aby zachować zgodność wsteczną, tylko pierwszy rdzeń uruchamia się po zresetowaniu, a kilka innych czynności typu sterownik należy zrobić, aby odpalić pozostałe.
2. Oczywiście dzielą także wszystkie urządzenia peryferyjne.


3
Zawsze myślę, że „wątek” jest koncepcją oprogramowania, co sprawia, że ​​trudno mi zrozumieć procesor wielordzeniowy. Problem polega na tym, w jaki sposób kody mogą powiedzieć rdzeniu „Zamierzam utworzyć wątek działający w rdzeniu 2”? Czy jest do tego jakiś specjalny kod asemblera?
demonguy

2
@demonguy: Nie, nie ma specjalnych instrukcji dla czegoś takiego. Poprosisz system operacyjny o uruchomienie wątku na określonym rdzeniu poprzez ustawienie maski koligacji (która mówi „ten wątek może działać na tym zestawie rdzeni logicznych”). To całkowicie problem z oprogramowaniem. Każdy rdzeń procesora (wątek sprzętowy) działa niezależnie w systemie Linux (lub Windows). Do współpracy z innymi wątkami sprzętowymi używają wspólnych struktur danych. Ale nigdy „bezpośrednio” nie uruchamiasz wątku na innym procesorze. Mówisz systemowi operacyjnemu, że chcesz mieć nowy wątek, i zapisuje to w strukturze danych, które system operacyjny widzi na innym rdzeniu.
Peter Cordes,

2
Mogę to powiedzieć, ale w jaki sposób umieść kody w określonym rdzeniu?
demonguy

4
@demonguy ... (uproszczony) ... każdy rdzeń współdzieli obraz systemu operacyjnego i uruchamia go w tym samym miejscu. Tak więc dla 8 rdzeni jest to 8 „procesów sprzętowych” działających w jądrze. Każde z nich wywołuje tę samą funkcję harmonogramu, która sprawdza tabelę procesów pod kątem możliwego do uruchomienia procesu lub wątku. (To jest kolejka uruchomieniowa. ) Tymczasem programy z wątkami działają bez świadomości podstawowej natury SMP. Po prostu rozwidlają (2) lub coś i informują jądro, że chcą uruchomić. Zasadniczo rdzeń znajduje proces, a nie proces znajdujący rdzeń.
DigitalRoss

1
W rzeczywistości nie musisz przerywać jednego rdzenia od drugiego. Pomyśl o tym w ten sposób: wszystko, czego potrzebowałeś wcześniej komunikować, zostało dobrze przekazane za pomocą mechanizmów programowych. Te same mechanizmy oprogramowania nadal działają. Więc potoki, wywołania jądra, tryb uśpienia / budzenia, wszystkie te rzeczy ... nadal działają jak wcześniej. Nie każdy proces działa na tym samym procesorze, ale mają te same struktury danych do komunikacji, co wcześniej. Wysiłek związany z przejściem na SMP ogranicza się głównie do tego, aby stare zamki działały w bardziej równoległym środowisku.
DigitalRoss

10

Jeśli piszesz optymalizujący kompilator / kod bajtowy maszyny wirtualnej dla procesora wielordzeniowego, co musisz wiedzieć konkretnie o, powiedzmy, x86, aby wygenerować kod, który działa wydajnie na wszystkich rdzeniach?

Jako ktoś, kto pisze optymalizujące maszyny wirtualne kompilatora / kodu bajtowego, mogę ci w tym pomóc.

Nie musisz nic wiedzieć o x86, aby wygenerować kod, który działa wydajnie na wszystkich rdzeniach.

Jednak może być konieczne zapoznanie się z cmpxchg i przyjaciółmi, aby napisać poprawnie działający kod na wszystkich rdzeniach. Programowanie wielordzeniowe wymaga użycia synchronizacji i komunikacji między wątkami wykonania.

Być może musisz wiedzieć coś o x86, aby generować kod, który działa wydajnie na x86 w ogóle.

Są inne rzeczy, których warto się nauczyć:

Powinieneś dowiedzieć się o możliwościach systemu operacyjnego (Linux, Windows lub OSX), które pozwalają na uruchamianie wielu wątków. Powinieneś dowiedzieć się o interfejsach API do paralelizacji, takich jak OpenMP i bloki wątków, lub nadchodzący „Grand Central” OSX 10.6 „Snow Leopard”.

Zastanów się, czy Twój kompilator powinien być automatycznie równoległy, czy też autor aplikacji skompilowanych przez Twój kompilator musi dodać specjalną składnię lub wywołania API do swojego programu, aby skorzystać z wielu rdzeni.


Czy nie masz wielu popularnych maszyn wirtualnych, takich jak .NET i Java, które mają problem z tym, że ich główny proces GC jest objęty blokadami i zasadniczo jednokrotnie?
Marco van de Voort,

9

Każdy rdzeń wykonuje się z innego obszaru pamięci. Twój system operacyjny skieruje rdzeń na twój program, a rdzeń wykona twój program. Twój program nie będzie wiedział, że istnieje więcej niż jeden rdzeń lub na którym rdzeń jest wykonywany.

Nie ma też dodatkowych instrukcji dostępnych tylko dla systemu operacyjnego. Rdzenie te są identyczne z układami jedno-rdzeniowymi. Każdy rdzeń uruchamia część systemu operacyjnego, która będzie obsługiwać komunikację ze wspólnymi obszarami pamięci używanymi do wymiany informacji w celu znalezienia następnego obszaru pamięci do wykonania.

Jest to uproszczenie, ale daje podstawowe wyobrażenie o tym, jak to się robi. Więcej informacji o multicores i multiprocesorach na Embedded.com zawiera wiele informacji na ten temat ... Temat ten bardzo szybko się komplikuje!


Myślę, że należy tu nieco bardziej rozróżnić, jak ogólnie działa wielordzeniowy i jak duży wpływ ma system operacyjny. „Każdy rdzeń wykonuje się z innego obszaru pamięci” jest moim zdaniem zbyt mylący. Przede wszystkim używanie wielu rdzeni w zasadzie tego nie potrzebuje, i łatwo można zauważyć, że W przypadku programu wątkowego CHCESZ, aby dwa rdzenie dwa działały na tym samym segmencie tekstu i danych (podczas gdy każdy rdzeń potrzebuje również indywidualnych zasobów, takich jak stos) .
Volker Stolz

@ShiDoiSi Dlatego moja odpowiedź zawiera tekst „To uproszczenie” .
Gerhard

5

Kod zestawu przełoży się na kod maszynowy, który zostanie wykonany na jednym rdzeniu. Jeśli chcesz, aby był wielowątkowy, będziesz musiał użyć prymitywów systemu operacyjnego, aby kilkakrotnie uruchomić ten kod na różnych procesorach lub różne fragmenty kodu na różnych rdzeniach - każdy rdzeń wykona osobny wątek. Każdy wątek będzie widział tylko jeden rdzeń, na którym aktualnie wykonuje.


4
Chciałem powiedzieć coś takiego, ale w jaki sposób system operacyjny przydziela wątki rdzeniom? Wyobrażam sobie, że istnieją pewne uprzywilejowane instrukcje montażu, które to osiągają. Jeśli tak, myślę, że to jest odpowiedź, której autor szuka.
A. Levy,

Nie ma na to instrukcji, jest to obowiązek harmonogramu systemu operacyjnego. W Win32 istnieją funkcje systemu operacyjnego, takie jak SetThreadAffinityMask, a kod może je wywoływać, ale jest to system operacyjny i wpływa na harmonogram, nie jest to instrukcja procesora.
sharptooth

2
Musi istnieć kod OpCode, inaczej system operacyjny też nie byłby w stanie tego zrobić.
Matthew Whited

1
W rzeczywistości nie jest to kod operacji do planowania - bardziej przypomina to, że dostajesz jedną kopię systemu operacyjnego na procesor, dzieląc przestrzeń pamięci; ilekroć rdzeń wraca do jądra (syscall lub interrupt), sprawdza te same struktury danych w pamięci, aby zdecydować, który wątek ma zostać uruchomiony.
pjc50

1
@ A.Levy: Gdy zaczynasz wątek z powinowactwem, który pozwala mu działać tylko na innym rdzeniu, nie przechodzi on od razu do drugiego rdzenia. Ma kontekst zapisany w pamięci, podobnie jak normalny przełącznik kontekstu. Pozostałe wątki sprzętowe widzą swoje wpisy w strukturach danych programu planującego, a jeden z nich ostatecznie zdecyduje, że uruchomi wątek. Tak więc z punktu widzenia pierwszego rdzenia: piszesz do wspólnej struktury danych, a ostatecznie kod systemu operacyjnego na innym rdzeniu (wątku sprzętowym) zauważy go i uruchomi.
Peter Cordes,

3

Nie dzieje się tak wcale w instrukcjach maszyn; rdzenie udają odrębne procesory i nie mają żadnych specjalnych możliwości komunikowania się ze sobą. Istnieją dwa sposoby komunikowania się:

  • dzielą fizyczną przestrzeń adresową. Sprzęt obsługuje spójność pamięci podręcznej, więc jeden procesor zapisuje na adres pamięci, który odczytuje inny.

  • współużytkują APIC (programowalny kontroler przerwań). Jest to pamięć odwzorowana na fizyczną przestrzeń adresową i może być używana przez jeden procesor do sterowania innymi, włączania lub wyłączania ich, wysyłania przerwań itp.

http://www.cheesecake.org/sac/smp.html to dobra referencja z głupim adresem URL.


2
W rzeczywistości nie udostępniają APIC. Każdy logiczny procesor ma swój własny. APIC komunikują się między sobą, ale są one osobne.
Nathan Fellman

Synchronizują (a nie komunikują się) w jeden podstawowy sposób - poprzez prefiks LOCK (instrukcja „xchg mem, reg” zawiera niejawne żądanie blokady), która biegnie do kołka blokującego, który biegnie do wszystkich magistral, skutecznie informując ich, że CPU (właściwie każde urządzenie masterujące magistralę) chce wyłącznego dostępu do magistrali. W końcu do pinu LOCKA (potwierdzenie) powróci sygnał informujący CPU, że ma on teraz wyłączny dostęp do magistrali. Ponieważ urządzenia zewnętrzne są znacznie wolniejsze niż wewnętrzne działanie CPU, sekwencja LOCK / LOCKA może wymagać wielu setek cykli procesora.
Olof Forshell

1

Główną różnicą między aplikacją jedno- i wielowątkową jest to, że ta pierwsza ma jeden stos, a druga ma jeden dla każdego wątku. Kod jest generowany nieco inaczej, ponieważ kompilator zakłada, że ​​rejestry segmentów danych i stosu (ds i ss) nie są równe. Oznacza to, że pośrednictwo przez rejestry ebp i esp, które domyślnie są zarejestrowane w rejestrze ss, również nie będzie domyślnie ustawione na ds (ponieważ ds! = Ss). I odwrotnie, pośrednictwo przez inne rejestry, które domyślnie ustawione na ds, nie będą domyślnie ustawione na ss.

Wątki dzielą się wszystkim innym, w tym obszarami danych i kodu. Dzielą się również procedurami lib, więc upewnij się, że są bezpieczne dla wątków. Procedura, która sortuje obszar w pamięci RAM, może być wielowątkowa, aby przyspieszyć. Wątki będą wtedy uzyskiwać dostęp, porównywać i porządkować dane w tym samym obszarze pamięci fizycznej i wykonywać ten sam kod, ale używając różnych zmiennych lokalnych do kontrolowania odpowiedniej części sortowania. Dzieje się tak, ponieważ wątki mają różne stosy, w których zawarte są zmienne lokalne. Ten rodzaj programowania wymaga starannego dostrojenia kodu, aby zredukować kolizje danych między rdzeniami (w pamięciach podręcznych i pamięci RAM), co z kolei skutkuje szybszym kodem z dwoma lub więcej wątkami niż z jednym. Oczywiście nie dostrojony kod będzie często szybszy z jednym procesorem niż z dwoma lub więcej. Debugowanie jest trudniejsze, ponieważ standardowy punkt przerwania „int 3” nie będzie miał zastosowania, ponieważ chcesz przerwać określony wątek, a nie wszystkie. Punkty przerwania rejestru debugowania również nie rozwiązują tego problemu, chyba że można je ustawić na określonym procesorze wykonującym określony wątek, który ma zostać przerwany.

Inny wielowątkowy kod może obejmować różne wątki działające w różnych częściach programu. Ten rodzaj programowania nie wymaga takiego samego strojenia i dlatego jest o wiele łatwiejszy do nauczenia.


0

W każdej architekturze obsługującej wiele procesorów, w porównaniu z poprzednimi wersjami jednoprocesorowymi, dodano instrukcje synchronizacji między rdzeniami. Ponadto masz instrukcje postępowania ze spójnością pamięci podręcznej, buforami opróżniania i podobnymi operacjami niskiego poziomu, z którymi musi sobie radzić system operacyjny. W przypadku jednoczesnych architektur wielowątkowych, takich jak IBM POWER6, IBM Cell, Sun Niagara i Intel „Hyperthreading”, możesz także zobaczyć nowe instrukcje określania priorytetów między wątkami (takie jak ustawianie priorytetów i jawne podawanie procesora, gdy nie ma nic do zrobienia) .

Ale podstawowa semantyka jednowątkowa jest taka sama, wystarczy dodać dodatkowe funkcje do obsługi synchronizacji i komunikacji z innymi rdzeniami.

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.