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?
Odpowiedzi:
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:
zaktualizuje tylko
EDX
dla aktualnie działającego wątku . Nie ma możliwości modyfikacjiEDX
na 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łasnyEDX
.źródło
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:
Na tym kodzie:
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ł.
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.
ICR_LOW zdefiniowano w 8.4.4 „Przykład inicjalizacji MP” jako:
Magiczną wartością
0FEE00300
jest adres pamięci ICR, jak udokumentowano w Tabeli 10-1 „Lokalna mapa adresów rejestru APIC”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.
XX
in000C46XXH
koduje adres pierwszej instrukcji, którą procesor wykona jako:Pamiętaj, że CS zwielokrotnia adresy
0x10
, więc rzeczywisty adres pamięci pierwszej instrukcji to:Więc jeśli na przykład
XX == 1
procesor rozpocznie się od0x1000
.Musimy wtedy upewnić się, że w tym miejscu pamięci działa 16-bitowy kod trybu rzeczywistego, np .:
Inną możliwością jest użycie skryptu linkera.
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ą:
Powiązane: Jak wyświetlić liczbę na ekranie i spać przez sekundę z zestawem DOS x86?
Myślę, że początkowy procesor musi być w trybie chronionym, aby to działało, ponieważ piszemy na adres,
0FEE00300H
który jest zbyt wysoki dla 16-bitówAby 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:
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:
GitHub w górę .
Złóż i uruchom:
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_yield
implementacją), 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
futex
wywołania systemowego Linux , które jest jednym z głównych prymitywów synchronizacji w systemie Linux.man futex
4.15 brzmi: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
GitHub w górę .
Możliwe wyjście:
Z tego wynika, że przedrostek x86
LDADD
instrukcji 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.
źródło
#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?gcc
,#include
pochodzi z preprocesora C. Skorzystaj zMakefile
dostarczonej 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.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.
źródło
Często zadawane pytania dotyczące nieoficjalnego SMP
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.
źródło
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.
źródło
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!
źródło
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.
źródło
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.
źródło
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.
źródło
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.
źródło