Как выглядит многоядерный язык ассемблера?

243

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

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

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

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

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

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

Пол Холлингсворт
источник
2
Здесь есть похожий (хотя и не идентичный) вопрос: stackoverflow.com/questions/714905/…
Натан Феллман

Ответы:

153

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

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

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

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

mov edx, 0

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

Натан Феллман
источник
2
Спасибо за заполнение пробела в ответе Николая. Отметьте ваш ответ как принятый сейчас ... дает конкретные детали, которые меня интересовали ... хотя было бы лучше, если бы был один ответ, в котором бы ваша информация и все Николас были объединены.
Пол Холлингсворт
3
Это не отвечает на вопрос, откуда берутся нити. Ядра и процессоры - вещь аппаратная, но каким-то образом потоки должны создаваться программно. Как основной поток узнает, куда отправить SIPI? Или сам SIPI создает новую тему?
богатый ремер
7
@richremer: Кажется, вы путаете HW-потоки и SW-потоки. HW-поток всегда существует. Иногда это спит. Сам SIPI пробуждает поток HW и позволяет ему запускать SW. Это зависит от ОС и BIOS, чтобы решить, какие потоки HW работают, а какие процессы и потоки SW работают в каждом потоке HW.
Натан Феллман
2
Здесь много хорошей и краткой информации, но это большая тема, поэтому вопросы могут затянуться. Есть несколько примеров полных «голых» ядер в дикой природе, которые загружаются с USB-накопителей или «дискет» - вот версия x86_32, написанная на ассемблере с использованием старых дескрипторов TSS, которые на самом деле могут запускать многопоточный код C ( github. com / duanev / oz-x86-32-asm-003 ), но поддержка стандартной библиотеки отсутствует. Чуть больше, чем вы просили, но, возможно, он может ответить на некоторые из этих затяжных вопросов.
Дуанев
87

Пример минимального запуска Intel x86 для неизолированного металла

Работоспособный пример из чистого металла со всеми необходимыми образцами . Все основные части описаны ниже.

Протестировано на Ubuntu 15.10 QEMU 2.3.0 и Lenovo ThinkPad T400 с реальным аппаратным гостем .

Руководство Intel по системному программированию, том 3, 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 или таблицу конфигурации Intel MP, и запускать только те, которые вам нужны, по очереди.

  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 для каждого логического процессора указывают на поток команд для потока, выполняемого логическим процессором.
  • Регистры FPU x87 (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
  • Большинство других регистров 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 имеют больший общий объем кэша и конвейера, чем отдельные ядра: /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 освобождает спин-блокировку.

После спин-блокировки ЦП 0 выполняет вызов выхода из полухоста, который заставляет QEMU выйти.

Если вы запускаете QEMU только с одним процессором -smp 1, то симуляция просто навсегда зависает на спин-блокировке.

CPU 1 разбудился с помощью интерфейса PSCI, более подробную информацию можно найти по адресу: ARM: Запустите / включите / включите другие ядра / AP CPU и передайте начальный адрес выполнения?

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

    Вот несколько упрощенных примеров таймера с голым металлом:

  • иметь дело с конфликтами памяти. В частности, каждому потоку понадобится уникальный стек, если вы хотите кодировать на 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 futex4.15 гласит:

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

Само имя системного вызова означает «Fast Userspace 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

Из этого мы видим, что LDADDинструкция x86 LOCK prefix / aarch64 сделала добавление атомарным: без него у нас есть условия гонки для многих добавлений, и общее количество в конце меньше, чем синхронизированные 20000.

Смотрите также:

Протестировано в Ubuntu 19.04 amd64 и в пользовательском режиме QEMU aarch64.

Сиро Сантилли 郝海东 冠状 病 六四 事件 法轮功
источник
Какой ассемблер вы используете для компиляции вашего примера? ГАЗу, похоже, не нравятся ваши #include(принимает это как комментарий), NASM, FASM, YASM не знают синтаксиса AT & T, поэтому они не могут быть ими ... так что же это?
Руслан
@Ruslan gcc, #includeпроисходит от препроцессора Си. Используйте Makefileпредоставленное, как описано в разделе «Начало работы»: github.com/cirosantilli/x86-bare-metal-examples/blob/… Если это не сработает, откройте проблему GitHub.
Сиро Сантилли 郝海东 冠状 病 六四 事件 法轮功
на x86, что произойдет, если ядро ​​поймет, что в очереди больше нет процессов, готовых для запуска? (что может происходить время от времени в режиме ожидания). Вращается ли ядро ​​в структуре разделяемой памяти до появления новой задачи? (вероятно, не очень хорошо, если он будет использовать много энергии) это вызывает что-то вроде HLT для сна, пока не произойдет прерывание? (в таком случае кто ответственен за то, чтобы разбудить это ядро?)
tigrou
@tigrou не уверен, но я нахожу крайне вероятным, что реализация Linux переведет его в состояние питания до следующего (вероятного таймера) прерывания, особенно на ARM, где питание является ключевым. Я хотел бы быстро попытаться выяснить, можно ли это конкретно наблюдать с помощью трассировки инструкций симулятора под управлением Linux, это может быть: github.com/cirosantilli/linux-kernel-module-cheat/tree/…
Сиро Сантилли 郝海东 冠状 病法轮功 事件 法轮功
1
Некоторая информация (специфичная для x86 / Windows) может быть найдена здесь (см. «Idle Thread»). TL; DR: если в ЦП нет работающего потока, ЦП отправляется в свободный поток. Наряду с некоторыми другими задачами, он в конечном счете вызовет зарегистрированную подпрограмму простоя процессора управления питанием (через драйвер, предоставляемый поставщиком ЦП, например, Intel). Это может перевести CPU в более глубокое состояние C (например, C0 -> C3), чтобы снизить энергопотребление.
Tigrou
43

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

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

Николас Флинт
источник
28
однако возникает вопрос: какие инструкции доступны операционной системе для этого?
Пол Холлингсворт
4
Для этого есть набор привилегированных инструкций, но это проблема операционной системы, а не кода приложения. Если код приложения хочет быть многопоточным, он должен вызывать функции операционной системы, чтобы сделать «магию».
sharptooth
2
BIOS обычно определяет количество доступных ядер и передает эту информацию в ОС по запросу. Существуют стандарты, которым должен соответствовать BIOS (и оборудование), чтобы доступ к аппаратным особенностям (процессоры, ядра, шина PCI, карты PCI, мышь, клавиатура, графика, ISA, PCI-E / X, память и т. Д.) Для разных ПК выглядит так же с точки зрения ОС. Если BIOS не сообщает о наличии четырех ядер, ОС обычно предполагает, что есть только одно ядро. Там может быть даже настройка BIOS для экспериментов.
Олоф Форшелл,
1
Это круто и все, но что, если вы пишете голую программу?
Александр Райан Баггетт
3
@AlexanderRyanBaggett,? Что это ещё? Повторяя, когда мы говорим «предоставьте это ОС», мы избегаем вопроса, потому что вопрос в том, как ОС делает это тогда? Какие инструкции по сборке он использует?
Pacerier
39

Неофициальный SMP FAQ логотип переполнения стека


Давным-давно, например, для написания ассемблера 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. Они также разделяют все периферийные устройства, естественно.

DigitalRoss
источник
3
Я всегда думаю, что «поток» - это программная концепция, которая затрудняет понимание многоядерного процессора, проблема в том, как коды могут сказать ядру «Я собираюсь создать поток, работающий в ядре 2»? Есть ли специальный ассемблерный код для этого?
demonguy
2
@demonguy: Нет, никаких специальных инструкций для чего-либо подобного нет. Вы просите ОС запустить ваш поток на определенном ядре, установив маску сходства (которая говорит: «Этот поток может работать на этом наборе логических ядер»). Это полностью программная проблема. Каждое ядро ​​процессора (аппаратный поток) независимо работает под управлением Linux (или Windows). Для совместной работы с другими аппаратными потоками они используют общие структуры данных. Но вы никогда не «напрямую» запускаете поток на другом процессоре. Вы сообщаете ОС, что хотите создать новый поток, и она делает пометку в структуре данных, которую видит ОС на другом ядре.
Питер Кордес
2
Я могу сказать об этом, но как вы положили коды на конкретное ядро?
demonguy
4
@demonguy ... (упрощенно) ... каждое ядро ​​разделяет образ ОС и запускает его в одном и том же месте. Итак, для 8 ядер это 8 «аппаратных процессов», работающих в ядре. Каждый вызывает одну и ту же функцию планировщика, которая проверяет таблицу процессов на работоспособный процесс или поток. (Это очередь выполнения. ) Между тем, программы с потоками работают без понимания основной природы SMP. Они просто форк (2) или что-то еще и позволяют ядру знать, что они хотят работать. По сути, ядро ​​находит процесс, а не процесс поиска ядра.
DigitalRoss
1
Вам на самом деле не нужно прерывать одно ядро ​​от другого. Подумайте об этом следующим образом: все, что вам нужно было для общения, до того, как оно было передано просто с помощью программных механизмов. Те же программные механизмы продолжают работать. Итак, каналы, вызовы ядра, сон / пробуждение и все такое ... они все еще работают как раньше. Не все процессы работают на одном и том же процессоре, но имеют те же структуры данных для связи, что и раньше. Усилия по созданию SMP в основном сводятся к тому, чтобы заставить старые блокировки работать в более параллельной среде.
DigitalRoss
10

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

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

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

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

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

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

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

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

Алекс Браун
источник
Разве у некоторых популярных виртуальных машин, таких как .NET и Java, нет проблемы, заключающейся в том, что их основной процесс GC покрыт блокировками и в основном однопоточен?
Марко ван де Воорт
9

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

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

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

Gerhard
источник
Я думаю, что здесь следует более тщательно различать, как многоядерный процессор работает в целом и как влияет ОС. «Каждое ядро ​​выполняется из разных областей памяти», на мой взгляд, слишком обманчиво. Прежде всего, использование нескольких ядер в принципе не нуждается в этом, и вы легко можете увидеть, что для многопоточной программы вы ХОТИТЕ, чтобы два ядра работали с одинаковыми сегментами текста и данных (в то время как каждому ядру также нужны отдельные ресурсы, такие как стек) ,
Фолькер Штольц
@ShiDoiSi Вот почему мой ответ содержит текст «Это упрощение» .
Герхард
5

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

Sharptooth
источник
4
Я собирался сказать что-то вроде этого, но тогда как ОС распределяет потоки по ядрам? Я предполагаю, что есть некоторые привилегированные инструкции по сборке, которые выполняют это. Если так, то я думаю, что это ответ, который ищет автор.
А. Леви
Там нет инструкции для этого, это обязанность планировщика операционной системы. В Win32 есть функции операционной системы, такие как SetThreadAffinityMask, и код может их вызывать, но это вещи операционной системы и влияющие на планировщик, а не инструкция процессора.
sharptooth
2
Должен быть OpCode, иначе операционная система не сможет это сделать.
Мэтью Уайтед
1
Не совсем код операции для планирования - это больше похоже на то, что вы получаете одну копию ОС на процессор, разделяя пространство памяти; всякий раз, когда ядро ​​повторно входит в ядро ​​(системный вызов или прерывание), оно смотрит на те же структуры данных в памяти, чтобы решить, какой поток следует запустить дальше.
pjc50
1
@ A.Levy: Когда вы запускаете поток со сродством, которое позволяет ему работать только на другом ядре, он не сразу переходит на другое ядро. Его контекст сохранен в памяти, как обычный переключатель контекста. Другие аппаратные потоки видят его запись в структурах данных планировщика, и один из них в конечном итоге решит, что он запустит поток. Итак, с точки зрения первого ядра: вы пишете в общую структуру данных, и в конечном итоге код ОС на другом ядре (аппаратном потоке) заметит его и запустит.
Питер Кордес
3

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

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

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

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

pjc50
источник
2
На самом деле они не разделяют APIC. Каждый логический процессор имеет свой собственный. АПИКы общаются между собой, но они разделены.
Натан Феллман
Они синхронизируются (а не связываются друг с другом) одним основным способом, то есть с помощью префикса LOCK (инструкция «xchg mem, reg» содержит неявный запрос блокировки), который запускается на вывод блокировки, который проходит ко всем шинам, эффективно сообщая им, что процессор (фактически любое устройство для управления шинами) хочет эксклюзивный доступ к шине. В конце концов сигнал вернется на вывод LOCKA (подтверждение), сообщая ЦПУ, что теперь у него есть эксклюзивный доступ к шине. Поскольку внешние устройства работают намного медленнее, чем внутренние процессы ЦП, для выполнения последовательности LOCK / LOCKA может потребоваться несколько сотен циклов ЦП.
Олоф Форшелл
1

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

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

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

Олоф Форшелл
источник
0

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

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

jakobengblom2
источник