Intel X86, минимально работоспособный, из неокрашенного металла
Пример работоспособного оголенного металла со всеми необходимыми образцами . Все основные части описаны ниже.
Протестировано на Ubuntu 15.10 QEMU 2.3.0 и Lenovo ThinkPad T400 настоящий аппаратный гость .
Руководство по системному программированию Intel * Том 1, 1011 *, 325384-056RU Сентябрь 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 (пользовательские программы).
Так что вам нужно написать свое собственное ядро, чтобы свободно с ним играть: пользовательская программа Linux не будет работать.
Сначала запускается один процессор, называемый процессором начальной загрузки (BSP).
Он должен активировать другие (называемые процессорами приложений (AP)) через специальные прерывания, называемые Межпроцессорные прерывания (IPI) .
Эти прерывания могут быть выполнены путем программирования расширенного программируемого контроллера прерываний (APIC) через регистр команд прерывания (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
Использование сценария компоновщика - еще одна возможность.
Петли задержки - раздражающая деталь для работы: не существует супер простого способа точно выполнить такие сны.
Возможные методы включают в себя:
- PIT (используется в моем примере)
- 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 управления отладкой
- MSR проверки состояния машины (IA32_MCG_STATUS) и возможности проверки машины (IA32_MCG_CAP)
- Тепловая тактовая модуляция и управление питанием ACPI MSR
- Счетчик меток времени 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
Совместное использование кэша обсуждается по адресу:
Гиперпотоки Intel имеют больший общий доступ к кешу и конвейеру, чем отдельные ядра: https://superuser.com/questions/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 upstream .
Собрать и запустить:
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, освобождающим спин-блокировку.
После спин-блокировки ЦП 0 затем выполняет выход из полухоста , который заставляет QEMU завершиться.
Если вы запускаете QEMU только с одним процессором с -smp 1
, то симуляция просто навсегда зависает на спин-блокировке.
CPU 1 просыпается с интерфейсом PSCI, более подробную информацию можно получить по адресу: ARM: Запуск / пробуждение / включение других ядер / AP ЦП и адреса начала выполнения передачи?
В вышестоящей версии также есть несколько настроек, чтобы заставить его работать на gem5, так что вы также можете поэкспериментировать с характеристиками производительности.
Я не тестировал его на реальном оборудовании, поэтому я не уверен, насколько это портативно. Следующая библиография Raspberry Pi может представлять интерес:
Этот документ содержит некоторые рекомендации по использованию примитивов синхронизации ARM, которые затем можно использовать для забавных вещей с несколькими ядрами: http://infocenter.arm.com/help/topic/com.arm.doc.dht0008a/DHT0008A_arm_synchronization_primitives.pdf
Протестировано на Ubuntu 18.10, GCC 8.2.0, Binutils 2.31.1, QEMU 2.12.0.
Следующие шаги для более удобного программирования
Предыдущие примеры пробуждают вторичный ЦП и выполняют базовую синхронизацию памяти с выделенными инструкциями, что является хорошим началом.
Но чтобы сделать многоядерные системы простыми в программировании, например, как и POSIX pthreads
, вам также необходимо перейти к следующим более сложным темам:
установка прерывает и запускает таймер, который периодически решает, какой поток будет запущен сейчас. Это известно как вытесняющая многопоточность .
Такая система также должна сохранять и восстанавливать регистры потоков по мере их запуска и остановки.
Также возможно иметь не вытесняющие многозадачные системы, но они могут потребовать, чтобы вы изменили ваш код так, чтобы каждый поток давал (например, с реализацией pthread_yield
), и стало труднее балансировать рабочие нагрузки.
Вот несколько упрощенных примеров таймера с голым металлом:
имеет дело с конфликтами памяти. Примечательно, что каждому потоку понадобится уникальный стек.
Вы можете просто ограничить потоки фиксированным максимальным размером стека, но лучший способ справиться с этим - использовать paging , который обеспечивает эффективные стеки "неограниченного размера".
Вот несколько веских причин для использования ядра Linux или другой операционной системы: -)