Как выглядит многоядерный язык ассемблера? - PullRequest
214 голосов
/ 11 июня 2009

Давным-давно, например, для написания ассемблера x86, вы должны будете получить инструкции о том, что «загрузить регистр EDX со значением 5», «увеличить регистр EDX» и т. Д.

С современными процессорами, которые имеют 4 ядра (или даже больше), на уровне машинного кода это выглядит так, как будто есть 4 отдельных процессора (т.е. есть только 4 отдельных регистра "EDX")? Если да, то когда вы говорите «увеличить регистр EDX», что определяет, какой регистр EDX ЦП увеличивается? Есть ли в ассемблере x86 понятие «контекст процессора» или «нить»?

Как работает связь / синхронизация между ядрами?

Если вы писали операционную систему, какой механизм предоставляется через оборудование, чтобы позволить вам планировать выполнение на разных ядрах? Это какие-то специальные привилегированные инструкции?

Если бы вы писали оптимизирующую виртуальную машину компилятора / байт-кода для многоядерного процессора, что вам нужно было бы конкретно знать, скажем, о x86, чтобы он генерировал код, эффективно работающий на всех ядрах?

Какие изменения были внесены в машинный код x86 для поддержки многоядерных функций?

Ответы [ 10 ]

131 голосов
/ 13 июня 2009

Это не прямой ответ на вопрос, но это ответ на вопрос, который появляется в комментариях. По сути, вопрос в том, какую поддержку аппаратное обеспечение оказывает многопоточным операциям.

Николас Флинт все сделал правильно , по крайней мере, в отношении x86. В многопоточной среде (Hyper-Threading, Multi-Core или Multi-CPU) поток Bootstrap (обычно поток 0 в ядре 0 в процессоре 0) запускает выборку кода с адреса 0xfffffff0. Все остальные потоки запускаются в специальном состоянии сна под названием Wait-for-SIPI . В рамках своей инициализации основной поток отправляет специальное межпроцессорное прерывание (IPI) через APIC, называемое SIPI (Startup IPI), каждому потоку в WFS. SIPI содержит адрес, с которого этот поток должен начать извлекать код.

Этот механизм позволяет каждому потоку выполнять код с другого адреса. Все, что нужно, это программная поддержка для каждого потока, чтобы настроить свои собственные таблицы и очереди сообщений. ОС использует те для выполнения фактического многопоточного планирования.

Что касается фактической сборки, как писал Николас, нет никакой разницы между сборками для однопоточного или многопоточного приложения. Каждый логический поток имеет свой собственный набор регистров, поэтому пишем:

mov edx, 0

будет обновлять EDX только для текущего запущенного потока . Нет способа изменить EDX на другом процессоре, используя одну инструкцию по сборке. Вам нужен какой-то системный вызов, чтобы попросить ОС сообщить другому потоку о запуске кода, который обновит свой собственный EDX.

59 голосов

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

На этот код:

  1. В большинстве операционных систем большинство этих операций невозможно выполнить из кольца 3 (пользовательские программы).

    Так что вам нужно написать свое собственное ядро, чтобы свободно с ним играть: пользовательская программа Linux не будет работать.

  2. Сначала запускается один процессор, называемый процессором начальной загрузки (BSP).

    Он должен активировать другие (называемые процессорами приложений (AP)) через специальные прерывания, называемые Межпроцессорные прерывания (IPI) .

    Эти прерывания могут быть выполнены путем программирования расширенного программируемого контроллера прерываний (APIC) через регистр команд прерывания (ICR)

    Формат ICR задокументирован по адресу: 10.6 «ВЫПУСК ПРЕРЫВАНИЯ МЕЖПРОЦЕССОРА»

    IPI происходит, как только мы пишем в ICR.

  3. ICR_LOW определяется в 8.4.4 «Пример инициализации MP» как:

    ICR_LOW EQU 0FEE00300H
    

    Магическое значение 0FEE00300 является адресом памяти ICR, как указано в Таблице 10-1 «Карта адресов локального регистра APIC»

  4. В примере используется самый простой из возможных методов: он устанавливает ICR для отправки широковещательных IPI, которые доставляются всем другим процессорам, кроме текущего.

    Но также возможно, и рекомендовано некоторыми , получить информацию о процессорах через специальные структуры данных, настроенные BIOS, такие как таблицы ACPI или таблица конфигурации MP Intel и только разбудите тех, кто вам нужен, по одному.

  5. 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
    

    Использование сценария компоновщика - еще одна возможность.

  6. Петли задержки - раздражающая деталь для работы: не существует супер простого способа точно выполнить такие сны.

    Возможные методы включают в себя:

    • PIT (используется в моем примере)
    • HPET
    • откалибруйте время занятого цикла с помощью вышеуказанного и используйте его вместо

    Связано: Как вывести число на экран и как спать в течение одной секунды при сборке DOS x86?

  7. Я думаю, что исходный процессор должен быть в защищенном режиме, чтобы он работал, когда мы пишем по адресу 0FEE00300H, который слишком велик для 16-битных

  8. Для обмена данными между процессорами мы можем использовать спин-блокировку основного процесса и изменить блокировку из второго ядра.

    Мы должны убедиться, что обратная запись в память выполнена, например, через 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 или другой операционной системы: -)

42 голосов
/ 11 июня 2009

Насколько я понимаю, каждое «ядро» представляет собой законченный процессор с собственным набором регистров. По сути, BIOS запускает вас с одним запущенным ядром, а затем операционная система может «запустить» другие ядра, инициализируя их и указывая на код для запуска и т. Д.

Синхронизация выполняется ОС. Как правило, на каждом процессоре для ОС выполняется отдельный процесс, поэтому многопоточность операционной системы отвечает за решение, какой процесс касается какой памяти, и что делать в случае конфликта памяти.

36 голосов
/ 08 февраля 2013

Неофициальный FAQ по SMP stack overflow logo


Давным-давно, например, для написания ассемблера x86, вы должны будете получить инструкции, в которых «загружать регистр EDX значением 5», «увеличивать регистр EDX» и т. Д. В современных процессорах, имеющих 4 ядра (или даже больше), на уровне машинного кода это просто выглядит так, как будто есть 4 отдельных ЦП (т.е. есть только 4 отдельных регистра "EDX")?

Точно. Существует 4 набора регистров, включая 4 отдельных указателя команд.

Если это так, когда вы говорите «увеличить регистр EDX», что определяет, какой регистр EDX ЦП увеличивается?

Процессор, который выполнил эту инструкцию, естественно. Представьте, что это 4 совершенно разных микропроцессора, которые просто используют одну и ту же память.

Есть ли в ассемблере x86 понятие "контекст процессора" или "нить"?

Нет. Ассемблер просто переводит инструкции, как всегда. Без изменений.

Как работает связь / синхронизация между ядрами?

Поскольку они совместно используют одну и ту же память, это в основном вопрос программной логики. Хотя теперь существует механизм межпроцессорного прерывания , он не является обязательным и изначально не присутствовал в первых двухпроцессорных системах x86.

Если вы писали операционную систему, какой механизм предоставляется через оборудование, чтобы позволить вам планировать выполнение на разных ядрах?

Планировщик фактически не меняется, за исключением того, что он немного более осторожен в отношении критических секций и типов используемых блокировок. До SMP код ядра в конечном итоге вызывал планировщик, который просматривал очередь выполнения и выбирал процесс для запуска в качестве следующего потока. (Процессы в ядре очень похожи на потоки.) Ядро SMP выполняет один и тот же код по одному потоку за раз, просто теперь блокировка критических секций должна быть безопасной для SMP, чтобы два ядра не могли случайно выбрать тот же PID.

Это какие-то специальные привилегированные инструкции?

Нет. Все ядра работают в одной и той же памяти с теми же старыми инструкциями.

Если бы вы писали оптимизирующую виртуальную машину компилятора / байт-кода для многоядерного процессора, что вам нужно было бы знать конкретно о, скажем, x86, чтобы он генерировал код, эффективно работающий на всех ядрах?

Вы запускаете тот же код, что и раньше. Это ядро ​​Unix или Windows, которое нужно изменить.

Вы можете обобщить мой вопрос как «Какие изменения были внесены в машинный код x86 для поддержки многоядерных функций?»

Ничего не нужно было. Первые SMP-системы использовали тот же набор команд, что и однопроцессорные. Сейчас произошла значительная эволюция архитектуры x86 и миллионы новых инструкций для ускорения работы, но ни одна не была необходимой для SMP.

Для получения дополнительной информации см. Спецификация многопроцессорной системы Intel .


Обновление: на все последующие вопросы можно ответить, просто полностью признав, что многоядерный процессор с n -двью почти 1 точно такой же, как n отдельные процессоры, которые совместно используют одну и ту же память. 2 Был задан важный вопрос, который не задавался: как программа, написанная для работы более чем на одном ядре, для большей производительности? И ответ таков: он написан с использованием библиотеки потоков, такой как Pthreads. Некоторые библиотеки потоков используют "зеленые потоки", которые не видны ОС, и они не получат отдельные ядра, но пока поскольку библиотека потоков использует функции потоков ядра, тогда ваша многопоточная программа автоматически будет многоядерной.
1. Для обеспечения обратной совместимости при перезагрузке запускается только первое ядро, и для запуска оставшихся необходимо выполнить несколько действий типа драйвера.
2. Они также разделяют все периферийные устройства, естественно.
9 голосов
/ 11 июня 2009

Если вы писали оптимизацию компилятор / байт-код VM для многоядерности CPU, что вам нужно знать конкретно о, скажем, x86 сделать он генерирует код, который работает эффективно по всем ядрам?

Как человек, который пишет оптимизирующие виртуальные машины компилятора / байт-кода, я могу помочь вам здесь.

Вам не нужно ничего конкретно знать о x86, чтобы он генерировал код, эффективно работающий на всех ядрах.

Однако вам может понадобиться знать о cmpxchg и его друзьях, чтобы написать код, который правильно запускает во всех ядрах. Многоядерное программирование требует использования синхронизации и связи между потоками выполнения.

Возможно, вам нужно что-то знать о x86, чтобы он генерировал код, который в целом работает на x86.

Есть и другие вещи, которые вам было бы полезно узнать:

Вам следует ознакомиться с возможностями ОС (Linux, Windows или OSX), позволяющими запускать несколько потоков. Вы должны узнать об API-интерфейсах распараллеливания, таких как OpenMP и Threading Building Blocks, или о готовящемся к выпуску "Grand Central" OSX 10.6 "Snow Leopard".

Вы должны подумать, должен ли ваш компилятор выполнять автоматическое распараллеливание, или если автору приложений, скомпилированных вашим компилятором, нужно добавить специальный синтаксис или вызовы API в свою программу, чтобы использовать преимущества нескольких ядер.

9 голосов
/ 11 июня 2009

Каждое ядро ​​выполняется из другой области памяти. Ваша операционная система направит ядро ​​на вашу программу, а ядро ​​выполнит вашу программу. Ваша программа не будет знать, что существует более одного ядра или на каком ядре она выполняется.

Также нет дополнительной инструкции, доступной только для операционной системы. Эти ядра идентичны одноядерным чипам. Каждое ядро ​​выполняет часть операционной системы, которая будет обрабатывать связь с общими областями памяти, используемыми для обмена информацией, чтобы найти следующую область памяти для выполнения.

Это упрощение, но оно дает вам базовое представление о том, как это делается. Подробнее о многоядерных и многопроцессорных системах на Embedded.com содержит много информации по этой теме ... Эта тема очень быстро усложняется!

5 голосов
/ 11 июня 2009

Код сборки будет переведен в машинный код, который будет выполняться на одном ядре. Если вы хотите, чтобы он был многопоточным, вам придется использовать примитивы операционной системы, чтобы запускать этот код на разных процессорах несколько раз или разные куски кода на разных ядрах - каждое ядро ​​будет выполнять отдельный поток. Каждый поток увидит только одно ядро, на котором он сейчас работает.

3 голосов
/ 27 октября 2009

Это не сделано в машинных инструкциях вообще; ядра претендуют на то, чтобы быть отдельными процессорами и не имеют никаких специальных возможностей для общения друг с другом. Есть два способа общения:

  • они разделяют физическое адресное пространство. Аппаратное обеспечение обеспечивает когерентность кэша, поэтому один процессор записывает в адрес памяти, который читает другой.

  • они совместно используют APIC (программируемый контроллер прерываний). Это память, отображаемая в физическое адресное пространство, и может использоваться одним процессором для управления другими, их включения или выключения, отправки прерываний и т. Д.

http://www.cheesecake.org/sac/smp.html - хорошая ссылка с глупым URL.

1 голос
/ 22 февраля 2011

Основное различие между одно- и многопоточным приложением состоит в том, что первое имеет один стек, а второе - один для каждого потока. Код генерируется несколько иначе, так как компилятор будет считать, что регистры сегментов данных и стека (ds и ss) не равны. Это означает, что косвенное обращение через регистры ebp и esp, которые по умолчанию к регистру ss, также не будут по умолчанию к ds (потому что ds! = Ss). И наоборот, косвенное обращение через другие регистры, которые по умолчанию равны ds, не будут равны ss.

Потоки разделяют все остальное, включая области данных и кода. Они также разделяют подпрограммы lib, поэтому убедитесь, что они потокобезопасны. Процедура, которая сортирует область в ОЗУ, может быть многопоточной, чтобы ускорить процесс. Затем потоки будут получать доступ, сравнивать и упорядочивать данные в одной и той же области физической памяти и выполнять один и тот же код, но с использованием разных локальных переменных для управления своей соответствующей частью сортировки. Это, конечно, потому что потоки имеют разные стеки, в которых содержатся локальные переменные. Этот тип программирования требует тщательной настройки кода, чтобы уменьшить количество конфликтов между ядрами (в кэш-памяти и оперативной памяти), что, в свою очередь, приводит к тому, что код работает быстрее с двумя или более потоками, чем с одним. Конечно, невыполненный код часто будет быстрее с одним процессором, чем с двумя или более. Отладка является более сложной задачей, потому что стандартная точка останова «int 3» не будет применяться, так как вы хотите прервать определенный поток, а не все из них. Точки останова регистра отладки также не решают эту проблему, если только вы не можете установить их на конкретном процессоре, выполняющем конкретный поток, который вы хотите прервать.

В другом многопоточном коде могут использоваться разные потоки, выполняющиеся в разных частях программы. Этот тип программирования не требует такой же настройки, и поэтому его гораздо легче освоить.

0 голосов
/ 18 августа 2009

То, что было добавлено в каждую многопроцессорную архитектуру по сравнению с однопроцессорными вариантами, которые были до них, - это инструкции по синхронизации между ядрами. Кроме того, у вас есть инструкции, чтобы иметь дело с когерентностью кэша, очищающими буферами и подобными операциями низкого уровня, с которыми сталкивается ОС. В случае одновременных многопоточных архитектур, таких как IBM POWER6, IBM Cell, Sun Niagara и Intel "Hyperthreading", вы также склонны видеть новые инструкции для определения приоритетов между потоками (например, установка приоритетов и явная уступка процессора, когда нечего делать) ,

Но базовая однопотоковая семантика одинакова, вы просто добавляете дополнительные возможности для синхронизации и связи с другими ядрами.

...