Як виглядає багатомовна збірна мова?


243

Колись, наприклад, щоб написати асемблер x86, ви мали б інструкції, що вказують "завантажувати регістр EDX зі значенням 5", "збільшувати регістр EDX" тощо.

З сучасними процесорами, які мають 4 ядра (або навіть більше), на рівні машинного коду це виглядає просто так, що є 4 окремі процесори (тобто є лише 4 чіткі регістри "EDX")? Якщо так, коли ви говорите "збільшити регістр EDX", що визначає, який реєстр EDX процесора збільшується? Чи є в асемблері x86 концепція "контекст процесора" чи "нитка"?

Як працює зв'язок / синхронізація між ядрами?

Якщо ви писали операційну систему, то який механізм відкривається за допомогою апаратних засобів, що дозволяє планувати виконання на різних ядрах? Це якісь спеціальні привілейовані інструкції?

Якщо ви писали оптимізаційний компілятор / байт-код VM для багатоядерного процесора, що б вам потрібно було знати конкретно про, скажімо, x86, щоб він згенерував код, який ефективно працює на всіх ядрах?

Які зміни були внесені до машинного коду x86 для підтримки багатоядерної функціональності?


2
Там дуже схожі (хоча і не ідентичні) питання тут: stackoverflow.com/questions/714905 / ...
Натан Fellman

Відповіді:


153

Це не пряма відповідь на питання, але це відповідь на питання, яке з’являється в коментарях. По суті, питання полягає в тому, яку підтримку обладнання надає багатопотокової операції.

Ніколас Флінт мав це право , принаймні щодо x86. У багатопотоковому середовищі (Hyper-Threading, багатоядерний або багатопроцесорний) потік 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 Керівництво 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

У цьому коді:

  1. Більшість операційних систем унеможливить більшість цих операцій за допомогою кільця 3 (користувацькі програми).

    Тож вам потрібно написати власне ядро, щоб вільно грати з ним: програма userland Linux не працюватиме.

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

    Він повинен розбудити інші (звані Прикладні процесори (AP)) через спеціальні переривання під назвою Inter Processor Interrupts (IPI) .

    Ці переривання можна здійснити програмуванням розширеного програмованого контролера переривань (APIC) через регістр команд Interrupt (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. Петлі затримки є дратівливою частиною для роботи: не існує надто простого способу зробити такий сон точно.

    Можливі методи включають:

    • ПДФО (використовується в моєму прикладі)
    • 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-адреси управління налагодженнями
  • Глобальний статус перевірки машини (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 futex4.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.


Який асемблер ви використовуєте для складання свого прикладу? GAS, здається, не подобається вашим #include(сприймає це як коментар), NASM, FASM, YASM не знають синтаксису AT&T, тому вони не можуть бути ними ... так що це?
Руслан

@Ruslan gcc, #includeпоходить від препроцесора C. Використовуйте Makefileнадане, як пояснено в розділі "Початок роботи": github.com/cirosantilli/x86-bare-metal-examples/blob/… Якщо це не працює, відкрийте проблему GitHub.
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功

на x86, що трапиться, якщо ядро ​​зрозуміє, що в черзі немає готових процесів? (що може час від часу траплятися в режимі очікування). Чи є основна спінлок на структурі спільної пам'яті, поки не з’явиться нове завдання? (напевно, це не добре, що це буде використовувати багато сил), чи викликає це щось на зразок HLT спати, поки не буде перерви? (у такому випадку хто відповідає за пробудження цього ядра?)
tigrou

@tigrou не впевнений, але я вважаю надзвичайно ймовірним, що реалізація Linux поставить його в стан живлення до наступного (швидше за все таймера) переривання, особливо в ARM, де живлення є ключовою. Я б спробував швидко зрозуміти, чи можна це легко зрозуміти за допомогою інструментального сліду тренажера під управлінням Linux, це може бути: github.com/cirosantilli/linux-kernel-module-cheat/tree/…
Ciro Santilli 郝海东 冠状 病六四 事件 法轮功

1
Деякі відомості (характерні для x86 / Windows) можна знайти тут (див. "Холодна нитка"). TL; DR: коли в процесорі немає потоку, який можна виконати, процесор відправляється в непрацюючий потік. Поряд з деякими іншими завданнями, він в кінцевому підсумку викличе зареєстрований процесор управління потужністю в режимі очікування (через драйвер, наданий постачальником процесора, наприклад: Intel). Це може перейти центральний процесор до більш глибокого стану C (наприклад: C0 -> C3), щоб зменшити споживання електроенергії.
tigrou

43

Як я розумію, кожне «ядро» - це повноцінний процесор, зі своїм набором реєстру. В основному, BIOS запускає вас одним запущеним ядром, і тоді операційна система може «запустити» інші ядра, ініціалізуючи їх і вказуючи на код для запуску тощо.

Синхронізація здійснюється ОС. Як правило, кожен процесор виконує інший процес для ОС, тому багатопотокова функціональність операційної системи відповідає за вирішення того, який процес доторкається до якої пам'яті, і що робити у випадку зіткнення пам'яті.


28
що не задає питання, але: Які інструкції доступні операційній системі для цього?
Пол Холлінгсворт

4
Для цього є набір привілейованих інструкцій, але це проблема операційної системи, а не код програми. Якщо код програми хоче бути багатопоточним, він повинен викликати функції операційної системи, щоб зробити "магію".
shartooth

2
Зазвичай BIOS визначає, скільки ядер доступно, і передасть цю інформацію в ОС, коли його запитують. Існують стандарти, яким BIOS (і апаратне забезпечення) повинні відповідати таким чином, що доступ до специфіки обладнання (процесори, ядра, шина PCI, карти PCI, миша, клавіатура, графіка, ISA, PCI-E / X, пам'ять тощо) для різних ПК виглядає так само з точки зору ОС. Якщо BIOS не повідомляє про наявність чотирьох ядер, ОС зазвичай вважає, що існує лише одне. Можливо, навіть є параметр BIOS, з яким можна експериментувати.
Олоф Форшелл

1
Це круто, і все, але що робити, якщо ви пишете програму з голого металу?
Олександр Райан Багетт

3
@AlexanderRyanBaggett,? Що це навіть? Повторюючи, коли ми говоримо "залиште це ОС", ми уникаємо питання, оскільки питання полягає в тому, як це робить тоді ОС? Які інструкції по збірці він використовує?
Печер'є

39

Неофіційний FAQ щодо SMP логотип переповнення стека


Колись, наприклад, щоб написати асемблер x86, ви мали б інструкції, що вказують "завантажувати регістр EDX зі значенням 5", "збільшувати регістр EDX" тощо. З сучасними процесорами, які мають 4 ядра (або навіть більше) , на рівні машинного коду це просто виглядає, що є 4 окремі процесори (тобто є лише 4 чіткі регістри "EDX")?

Саме так. Існує 4 набори регістрів, включаючи 4 окремі вказівні інструкції.

Якщо так, коли ви говорите "збільшити регістр EDX", що визначає, який реєстр EDX процесора збільшується?

Процесор, який виконав цю інструкцію, природно. Подумайте про це як про 4 абсолютно різних мікропроцесори, які просто поділяють одну пам'ять.

Чи є в асемблері x86 концепція "контекст процесора" чи "нитка"?

Ні. Асемблер просто перекладає інструкції, як це було завжди. Ніяких змін там немає.

Як працює зв'язок / синхронізація між ядрами?

Оскільки вони поділяють однакову пам'ять, це здебільшого питання програмної логіки. Хоча зараз існує міжпроцесорний механізм переривання , це не потрібно і спочатку не було в перших системах x86 з подвійним процесором x86.

Якщо ви писали операційну систему, то який механізм відкривається за допомогою апаратних засобів, що дозволяє планувати виконання на різних ядрах?

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

Це якісь спеціальні пільгові інструкції?

Ні. Ядра просто всі працюють в одній пам'яті з тими ж старими інструкціями.

Якщо ви писали оптимізаційний компілятор / байт-код VM для багатоядерного процесора, що б вам потрібно було знати конкретно про, скажімо, x86, щоб він згенерував код, який ефективно працює на всіх ядрах?

Ви запускаєте той самий код, що і раніше. Це ядро ​​Unix або Windows, яке потрібно змінити.

Ви можете узагальнити моє запитання як "Які зміни були внесені до машинного коду x86 для підтримки багатоядерних функцій?"

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

Для отримання додаткової інформації див . Специфікацію мультипроцесора Intel .


Оновлення: на всі подальші запитання можна відповісти, повністю визнавши, що n -way багатоядерний процесор майже 1 саме те саме, що і n окремих процесорів, які просто поділяють ту саму пам'ять. 2 Не було задано важливого питання: як написана програма для запуску на більшій ядрі для отримання більшої продуктивності? І відповідь така: вона пишеться за допомогою бібліотеки потоків, як Pthreads. Деякі бібліотеки потоків використовують "зелені нитки", які не видно ОС, і ті не отримають окремих ядер, але поки бібліотека потоків використовує функції потоку ядра, то ваша потокова програма автоматично буде багатоядерною.
1. Для зворотної сумісності запускається лише перше ядро, а для запуску решти потрібно зробити кілька речей типу драйверів.
2. Вони також діляться всіма периферійними пристроями, природно.


3
Я завжди думаю, що "потік" - це програмне поняття, яке ускладнює розуміння багатоядерного процесора. Проблема полягає в тому, як коди можуть сказати ядро ​​"Я збираюся створити потік, що працює в ядрі 2"? Чи є якийсь спеціальний код складання для цього?
demonguy

2
@demonguy: Ні, немає спеціальної інструкції для подібного. Ви просите ОС запустити свій потік на певному ядрі, встановивши маску спорідненості (яка говорить "цей потік може працювати на цьому наборі логічних ядер"). Це повністю програмне забезпечення. Кожне ядро ​​процесора (апаратний потік) незалежно працює під управлінням Linux (або Windows). Для роботи разом з іншими апаратними потоками вони використовують спільні структури даних. Але ви ніколи не «безпосередньо» запускаєте потік на іншому процесорі. Ви повідомляєте ОС, що хочете мати новий потік, і вона робить примітку в структурі даних, яку бачить ОС на іншому ядрі.
Пітер Кордес

2
Я можу сказати, що це os, але як ОС покласти коди на певне ядро?
demonguy

4
@demonguy ... (спрощено) ... кожне ядро ​​ділиться зображенням ОС і починає запускати його там же. Отже, для 8 ядер це 8 «апаратних процесів», що працюють в ядрі. Кожен викликає ту саму функцію планувальника, яка перевіряє таблицю процесів на предмет запущеного процесу або потоку. (Це черга запуску. ) Тим часом програми з потоками працюють без усвідомлення основної природи SMP. Вони просто розщеплюють (2) або щось таке і дають ядро ​​знати, що вони хочуть запустити. По суті, ядро ​​знаходить процес, а не процес знаходження ядра.
DigitalRoss

1
Насправді вам не потрібно переривати одне ядро ​​від іншого. Подумайте про це так: все, що вам потрібно було спілкуватися раніше, було добре передано за допомогою програмних механізмів. Ті ж програмні механізми продовжують працювати. Отже, труби, дзвінки в ядрі, сон / пробудження, все таке ... вони все ще працюють, як раніше. Не кожен процес працює на одному і тому ж процесорі, але вони мають ті ж структури даних для зв'язку, що і раніше. Зусилля, спрямовані на перехід SMP, здебільшого обмежуються тим, щоб старі замки працювали в більш паралельному середовищі.
DigitalRoss

10

Якщо ви писали оптимізаційний компілятор / байт-код VM для багатоядерного процесора, що б вам потрібно було знати конкретно про, скажімо, x86, щоб він згенерував код, який ефективно працює на всіх ядрах?

Як хтось, хто пише оптимізацію компіляторів / байт-кодів, я можу вам тут допомогти.

Вам не потрібно знати нічого конкретно про x86, щоб змусити його генерувати код, який ефективно працює на всіх ядрах.

Однак вам може знадобитися знати про cmpxchg та друзів для того, щоб написати код, який правильно працює на всіх ядрах. Багатоядерне програмування вимагає використання синхронізації та зв'язку між потоками виконання.

Вам може знадобитися щось про x86, щоб він генерував код, який ефективно працює на x86 взагалі.

Є й інші речі, про які вам було б корисно дізнатися:

Ви повинні дізнатися про засоби, які ОС (Linux або Windows або OSX) надає для запуску декількох потоків. Ви повинні дізнатись про API паралелізації, таких як OpenMP і Threading Building Blocks, або OSX 10.6 "Snow Leopard", майбутній "Grand Central".

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


У вас немає декількох популярних віртуальних машин, таких як .NET і Java, що їхній основний процес GC покритий замками і принципово однопоточним?
Marco van de Voort

9

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

Також немає додаткової інструкції, доступної лише для Операційної системи. Ці сердечники ідентичні одноядерним мікросхемам. Кожне ядро ​​виконує частину операційної системи, яка буде обробляти зв’язок із загальними областями пам'яті, які використовуються для обміну інформацією, щоб знайти наступну область пам'яті, яку потрібно виконати.

Це спрощення, але воно дає вам основне уявлення про те, як це робиться. Більше про багатоядерні та багатопроцесорні програми на Embedded.com мають багато інформації на цю тему ... Ця тема ускладнюється дуже швидко!


Я думаю, тут слід трохи ретельніше розрізнити, як працює багатоядерність в цілому і на скільки впливає ОС. "Кожне ядро, яке виконується з іншої області пам'яті", на мою думку, є занадто оманливим. Перш за все, використання декількох ядер у принципах цього не потребує, і ви можете легко побачити, що для потокової програми ви хотіли б, щоб два ядра працювали над одним і тим же сегментом тексту та даних (хоча кожне ядро ​​також потребує індивідуальних ресурсів, таких як стек) .
Волкер

@ShiDoiSi Саме тому моя відповідь містить текст "Це спрощення" .
Герхард

5

Код складання перетвориться на машинний код, який буде виконаний на одному ядрі. Якщо ви хочете, щоб він був багатопоточним, вам доведеться використовувати примітиви операційної системи, щоб запустити цей код на різних процесорах кілька разів або різні фрагменти коду на різних ядрах - кожне ядро ​​виконає окремий потік. Кожен потік побачить лише одне ядро, на якому він зараз виконується.


4
Я збирався сказати щось подібне, але як тоді ОС розподіляє нитки для ядер? Я думаю, що є кілька привілейованих інструкцій по збірці, які це виконують. Якщо так, я думаю, що це відповідь, яку шукає автор.
А. Леві

Для цього немає інструкції, це обов'язок планувальника операційних систем. У Win32 є такі функції операційної системи, як SetThreadAffinityMask, і код може викликати їх, але це операційна система і впливає на планувальника, це не інструкція процесора.
shartooth

2
Повинно бути OpCode, інакше операційна система також не змогла би це зробити.
Метью Уайт

1
Насправді не опкод для планування - це більше схоже на те, що ви отримуєте одну копію ОС на один процесор, обмінюючись простором пам'яті; щоразу, коли ядро ​​повторно входить у ядро ​​(систематичне виклик або переривання), воно переглядає ті самі структури даних у пам'яті, щоб вирішити, який потік слід запустити далі.
pjc50

1
@ A.Levy: Коли ви запускаєте потік із спорідненістю, яка лише дозволяє йому працювати на іншому ядрі, вона не одразу переходить до іншого ядра. Цей контекст зберігається в пам'яті, як і звичайний контекстний комутатор. Інші апаратні потоки бачать його запис у структурі даних планувальника, і одна з них врешті вирішить, що вона запустить потік. Отже, з точки зору першого ядра: ви записуєте до спільної структури даних і, зрештою, код ОС на іншому ядрі (апаратний потік) помітить це і запустить його.
Пітер Кордес

3

Це зовсім не робиться в машинних інструкціях; сердечники прикидаються окремими процесорами і не мають особливих можливостей для спілкування один з одним. Є два способи спілкування:

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

  • вони поділяють APIC (програмований контролер переривання). Це пам'ять, відображена у фізичному адресному просторі, і може використовувати один процесор для управління іншими, вмикання або вимкнення, надсилання переривань тощо.

http://www.cheesecake.org/sac/smp.html - хороша довідка з нерозумним URL-адресою.


2
Насправді вони не поділяють APIC. У кожного логічного процесора є свій. APIC спілкуються між собою, але вони є окремими.
Натан Фелман

Вони синхронізують (а не спілкуються) одним основним способом, і це через префікс LOCK (інструкція "xchg mem, reg" містить неявний запит блокування), який працює на штифт блокування, який працює на всі шини, ефективно повідомляючи їм, що процесор (насправді будь-який пристрій для управління шиною) хоче ексклюзивний доступ до шини. Врешті-решт сигнал повернеться до штифта LOCKA (підтвердити), який повідомляє процесору, що тепер він має ексклюзивний доступ до шини. Оскільки зовнішні пристрої значно повільніші, ніж внутрішня робота процесора, послідовність LOCK / LOCKA може зажадати багатьох сотень циклів процесора для завершення.
Олоф Форшелл

1

Основна відмінність одно- та багатопотокової програми полягає в тому, що перший має один стек, а другий - по одному для кожного потоку. Код генерується дещо інакше, оскільки компілятор вважатиме, що регістри даних та стека (ds та ss) не рівні. Це означає, що непряме через ebp та esp реєструє, що за замовчуванням до регістру ss також не буде за замовчуванням ds (тому що ds! = Ss). І навпаки, непряме через інші регістри, за замовчуванням яких ds не буде за замовчуванням для ss.

Нитки поділяють все інше, включаючи області даних та коду. Вони також діляться процедурами ліб, тому переконайтеся, що вони безпечні для ниток. Процедура, яка сортує область в оперативній пам’яті, може бути багатопотоковою, щоб прискорити роботу. Потім потоки отримуватимуть доступ, порівнюватимуть і упорядковуватимуть дані в тій же області фізичної пам’яті та виконувати один і той же код, але використовують різні локальні змінні для управління відповідною їх частиною. Це, звичайно, тому, що потоки мають різні стеки, де містяться локальні змінні. Цей тип програмування вимагає ретельної настройки коду, щоб зменшити зіткнення між основними даними (у кешах та оперативної пам’яті), що в свою чергу призводить до швидшого коду з двома або більше потоками, ніж із одним. Звичайно, нерегульований код часто буде швидше з одним процесором, ніж з двома і більше. Виправити налагодження складніше, тому що стандартна точка розриву "int 3" не буде застосована, оскільки ви хочете перервати певний потік і не всі з них. Точки зупинки реєстру налагодження також не вирішують цю проблему, якщо ви не можете встановити їх на конкретному процесорі, виконуючи певний потік, який ви хочете перервати.

Інший багатопотоковий код може включати різні потоки, що працюють в різних частинах програми. Цей тип програмування не вимагає однакового налаштування, а тому набагато простіше засвоїти.


0

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

Але основна семантика з одним потоком однакова, ви просто додаєте додаткові засоби для синхронізації та зв'язку з іншими ядрами.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.