Приклад Intel мінімального рівня x86 з мінімальною експлуатацією
Приклад голого металу з усіма необхідними котельними плитами . Усі основні частини висвітлюються нижче.
Тестований на Ubuntu 15.10 QEMU 2.3.0 та реального апаратного гостя Lenovo ThinkPad T400 .
Intel Керівництво Volume 3 Система Керівництво по програмуванню - 325384-056US вересня 2015 охоплює SMP в розділах 8, 9 і 10.
Таблиця 8-1. "Трансляція INIT-SIPI-SIPI Послідовність та вибір тайм-аутів" містить приклад, який в основному просто працює:
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
У цьому коді:
Більшість операційних систем унеможливить більшість цих операцій за допомогою кільця 3 (користувацькі програми).
Тож вам потрібно написати власне ядро, щоб вільно грати з ним: програма userland Linux не працюватиме.
Спочатку працює один процесор, який називається завантажувальним процесором (BSP).
Він повинен розбудити інші (звані Прикладні процесори (AP)) через спеціальні переривання під назвою Inter Processor Interrupts (IPI) .
Ці переривання можна здійснити програмуванням розширеного програмованого контролера переривань (APIC) через регістр команд Interrupt (ICR)
Формат ICR задокументований за адресою: 10.6 "ВИДАЧА ІНТЕРПРОЦЕССЬКИХ ІНТЕРРУПТІВ"
IPI відбувається, як тільки ми пишемо в ICR.
ICR_LOW визначається в 8.4.4 "Приклад ініціалізації MP" як:
ICR_LOW EQU 0FEE00300H
Магічне значення 0FEE00300
- це адреса пам'яті ICR, як це зафіксовано в Таблиці 10-1 "Місцева адресна карта регістрів APIC"
У прикладі використовується найпростіший можливий метод: він встановлює ICR для передачі широкомовні IPI, які доставляються всім іншим процесорам, крім поточного.
Але також можливо, і рекомендується деякими , отримати інформацію про процесори за допомогою спеціальних структур даних, встановлених в BIOS, таких як таблиці ACPI або таблиці конфігурації MP від Intel, і розбудити лише ті, які вам потрібні одна за одною.
XX
в 000C46XXH
кодує адресу першої інструкції, яку виконує процесор як:
CS = XX * 0x100
IP = 0
Пам'ятайте, що CS множить адреси на0x10
, тому фактична адреса пам'яті першої інструкції:
XX * 0x1000
Так, якщо, наприклад XX == 1
, процесор запуститься в 0x1000
.
Тоді ми повинні переконатися, що в цьому місці пам'яті може працювати 16-бітний код реального режиму, наприклад із:
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
Використання скрипта для посилання - це ще одна можливість.
Петлі затримки є дратівливою частиною для роботи: не існує надто простого способу зробити такий сон точно.
Можливі методи включають:
- ПДФО (використовується в моєму прикладі)
- HPET
- відкалібруйте час зайнятого циклу за вказаним вище та використовуйте його замість цього
Пов'язане: Як відобразити число на екрані і спати одну секунду за допомогою складання DOS x86?
Я думаю, що початковий процесор повинен бути в захищеному режимі, щоб це працювало, коли ми пишемо на адресу, 0FEE00300H
яка занадто висока для 16-бітних
Для спілкування між процесорами ми можемо використовувати спінлок на головному процесі та змінювати блокування з другого ядра.
Ми повинні забезпечити виконання запису в пам'ять, наприклад, наскрізь wbinvd
.
Спільний стан між процесорами
8.7.1 "Стан логічних процесорів" говорить:
Наведені нижче характеристики є частиною архітектурного стану логічних процесорів в рамках процесорів Intel 64 або IA-32, що підтримують технологію Intel Hyper-Threading. Особливості можна розділити на три групи:
- Дублюється для кожного логічного процесора
- Ділиться логічними процесорами у фізичному процесорі
- Спільний або дубльований, залежно від реалізації
Наступні функції дублюються для кожного логічного процесора:
- Реєстри загального призначення (EAX, EBX, ECX, EDX, ESI, EDI, ESP та EBP)
- Реєстри сегментів (CS, DS, SS, ES, FS та GS)
- Реєстри EFLAGS та EIP. Зауважте, що регістри CS та EIP / RIP для кожного логічного процесора вказують на потік інструкцій для потоку, який виконує логічний процесор.
- x87 регістри FPU (ST0 - ST7, слово статусу, слово керування, тегове слово, вказівник операнду даних та вказівник інструкції)
- MMX регістри (від MM0 до MM7)
- Регістри XMM (XMM0 до XMM7) та регістр MXCSR
- Регістри управління та регістри вказівників системної таблиці (GDTR, LDTR, IDTR, регістр завдань)
- Реєстри налагодження (DR0, DR1, DR2, DR3, DR6, DR7) та MSR-адреси управління налагодженнями
- Глобальний статус перевірки машини (IA32_MCG_STATUS) та можливості перевірки машини (IA32_MCG_CAP)
- Теплові годинникові модуляції та управління MSR управлінням потужністю ACPI
- Лічильники MSR лічильника часу
- Більшість інших реєстрів MSR, включаючи таблицю атрибутів сторінки (PAT). Дивіться винятки нижче.
- Місцеві регістри APIC.
- Додаткові регістри загального призначення (R8-R15), регістри XMM (XMM8-XMM15), регістр управління, IA32_EFER на процесорах Intel 64.
Логічні процесори поділяються на такі функції:
- Регістри діапазону пам'яті (MTRR)
Незалежно від того, поділяються чи дублюються такі функції, що стосується впровадження:
- IA32_MISC_ENABLE MSR (адреса MSR 1A0H)
- MSR архітектури машинної перевірки (MCA) (крім MSR IA32_MCG_STATUS та IA32_MCG_CAP)
- Контроль ефективності моніторингу та протидії MSR
Обмін кешами обговорюється на:
Hyperthreads Intel мають більший обмін кешами та конвеєрами, ніж окремі ядра: /superuser/133082/hyper-threading-and-dual-core-whats-the-difference/995858#995858
Ядро Linux 4.2
Основна дія ініціалізації виявляється в arch/x86/kernel/smpboot.c
.
Приклад з мінімальною експлуатацією на бареметах ARM
Тут я надаю мінімальний приклад ARMv8 aarch64 для 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 вище за течією .
Зберіть і запустіть:
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 \
;
У цьому прикладі ми ставимо CPU 0 у цикл спінклока, і він виходить лише з CPU 1, звільняє спінлок.
Після замикання, CPU 0 потім робить виклик напівгосту, який змушує QEMU вийти.
Якщо ви запускаєте QEMU лише з одного процесора -smp 1
, то симуляція просто вічно зависає на спинлок.
CPU 1 прокинувся за допомогою інтерфейсу PSCI, детальніше на сторінці: ARM: Start / Wakeup / Bringup інших ядер CP / процесора та передача початкової адреси виконання?
У версії за течією також є декілька налаштувань, щоб змусити її працювати на gem5, так що ви можете експериментувати і з характеристиками продуктивності.
Я не перевіряв це на справжньому обладнання, тому я не впевнений, наскільки це портативно. Наступна бібліографія Raspberry Pi може бути цікавою:
Цей документ містить деякі вказівки щодо використання примітивів синхронізації ARM, які потім можна використовувати для розваг із різними ядрами: http://infocenter.arm.com/help/topic/com.arm.doc.dht0008a/DHT0008A_arm_synchronization_primitive.pdf
Тестовано на Ubuntu 18.10, GCC 8.2.0, Binutils 2.31.1, QEMU 2.12.0.
Наступні кроки для більш зручної програмованості
Попередні приклади пробуджують вторинний процесор і виконують основну синхронізацію пам'яті з виділеними інструкціями, що є хорошим початком.
Але для спрощення програмування багатоядерних систем, наприклад, як POSIX pthreads
, вам також потрібно буде вивчити наступні теми:
налаштування перериває і запускає таймер, який періодично визначає, який потік буде запускатися зараз. Це відомо як попереджувальне багатопотокове читання .
Така система також потребує збереження та відновлення реєстрів потоків під час їх запуску та зупинки.
Можливо також існувати багатозадачні системи багатозадачності, але вони можуть вимагати від вас змінити код так, щоб виходили всі потоки (наприклад, з pthread_yield
реалізацією), і збалансувати робоче навантаження стає важче.
Ось кілька спрощених прикладів таймеру голого металу:
вирішувати конфлікти пам’яті. Зокрема, для кожного потоку потрібен унікальний стек, якщо ви хочете кодувати на C або інших мовах високого рівня.
Ви можете просто обмежити потоки, щоб вони мали фіксований максимальний розмір стека, але приємнішим способом вирішити це є підказка, яка забезпечує ефективні стеки "необмеженого розміру".
Ось наївний баретальний приклад aarch64, який може підірватися, якщо стек зросте занадто глибоким
Це кілька вагомих причин використовувати ядро Linux або якусь іншу операційну систему :-)
Примітиви синхронізації пам'яті користувача
Хоча запуск / зупинка / управління потоками, як правило, виходять за межі області користувальницьких даних, проте ви можете використовувати інструкції по збірці з потоків користувачів для синхронізації доступу до пам'яті без потенційно дорожчих системних викликів.
Звичайно, слід віддавати перевагу використанню бібліотек, які переносять ці примітиви низького рівня. Стандарт C ++ сам зробив великі успіхи на тих <mutex>
і <atomic>
заголовки, і , зокрема , з std::memory_order
. Я не впевнений, чи охоплює вона всі можливі семантики пам'яті, які можна досягти, але це просто може.
Більш тонка семантика є особливо актуальною в контексті блокування вільних структур даних , що може запропонувати переваги в роботі в певних випадках. Для їх реалізації вам, ймовірно, доведеться трохи дізнатися про різні типи бар'єрів пам’яті: https://preshing.com/20120710/memory-barriers-are-like-source-control-operations/
Наприклад, Boost, наприклад, має кілька реалізацій безкоштовних контейнерів на веб- сайті: https://www.boost.org/doc/libs/1_63_0/doc/html/lockfree.html
Такі інструкції користувача також використовуються для реалізації futex
системного виклику Linux , який є одним з основних примітивів синхронізації в Linux. man futex
4.15 читає:
Системний виклик futex () забезпечує метод очікування, поки певна умова стане справжньою. Зазвичай він використовується як блокуюча конструкція в контексті синхронізації спільної пам'яті. При використанні футексів більшість операцій синхронізації виконуються в просторі користувача. Програма користувальницького простору використовує системний виклик futex () лише тоді, коли є ймовірність, що програма повинна блокуватись довший час, поки умова не стане справжньою. Інші операції futex () можуть використовуватися для пробудження будь-яких процесів або потоків, які чекають певної умови.
Сама назва syscall означає "Швидкий користувацький простір XXX".
Ось мінімально непотрібний приклад C ++ x86_64 / aarch64 з вбудованою вставкою, який ілюструє базове використання таких інструкцій переважно для розваги:
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 вище за течією .
Можливий вихід:
my_non_atomic_ulong 15264
my_arch_non_atomic_ulong 15267
З цього ми бачимо, що інструкція префікса x86 LOCK / aarch64 LDADD
зробила додавання атомним: без нього у нас є багато змагальних умов для багатьох доповнень, а загальний підрахунок в кінці менше, ніж синхронізований 20000.
Дивитися також:
Тестується в Ubuntu 19.04 amd64 та в користувальницькому режимі QEMU aarch64.