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:
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:
ICR_LOW EQU 0FEE00300H
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
in 000C46XXH
koduje 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.
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?
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ów
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_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:
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 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.