Seperti apa tampilan bahasa rakitan multicore?

243

Sekali waktu, untuk menulis assembler x86, misalnya, Anda akan mendapatkan instruksi yang menyatakan "muat register EDX dengan nilai 5", "tambah register EDX", dll.

Dengan CPU modern yang memiliki 4 core (atau bahkan lebih), pada level kode mesin apakah hanya terlihat seperti ada 4 CPU terpisah (yaitu apakah hanya ada 4 register "EDX" yang berbeda)? Jika demikian, ketika Anda mengatakan "menambah register EDX", apa yang menentukan register EDX CPU mana yang ditambahkan? Apakah ada konsep "konteks CPU" atau "utas" di assembler x86 sekarang?

Bagaimana cara kerja komunikasi / sinkronisasi antar core?

Jika Anda menulis sistem operasi, mekanisme apa yang terbuka melalui perangkat keras untuk memungkinkan Anda menjadwalkan eksekusi pada inti yang berbeda? Apakah ini instruksi khusus yang diprivatisasi?

Jika Anda menulis VM kompilator / bytecode pengoptimalan untuk CPU multicore, apa yang perlu Anda ketahui secara spesifik tentang, katakanlah, x86 untuk membuatnya menghasilkan kode yang berjalan efisien di semua core?

Perubahan apa yang telah dibuat pada kode mesin x86 untuk mendukung fungsionalitas multi-inti?

Paul Hollingsworth
sumber
2
Ada pertanyaan serupa (meskipun tidak identik) di sini: stackoverflow.com/questions/714905/…
Nathan Fellman

Jawaban:

153

Ini bukan jawaban langsung untuk pertanyaan, tetapi ini adalah jawaban untuk pertanyaan yang muncul di komentar. Pada dasarnya, pertanyaannya adalah dukungan apa yang diberikan hardware untuk operasi multi-threaded.

Nicholas Flynt benar , setidaknya tentang x86. Dalam lingkungan multi-utas (Hyper-threading, multi-core atau multi-prosesor), utas Bootstrap (biasanya utas 0 pada intis 0 pada prosesor 0) memulai pengambilan kode dari alamat 0xfffffff0. Semua utas lainnya memulai dalam kondisi tidur khusus yang disebut Tunggu-untuk-SIPI . Sebagai bagian dari inisialisasi, utas utama mengirimkan inter-prosesor-interupsi (IPI) khusus melalui APIC yang disebut SIPI (Startup IPI) ke setiap utas yang ada di WFS. SIPI berisi alamat dari mana utas itu harus mulai mengambil kode.

Mekanisme ini memungkinkan setiap utas untuk mengeksekusi kode dari alamat yang berbeda. Yang diperlukan hanyalah dukungan perangkat lunak untuk setiap utas untuk mengatur tabel dan antrian pengiriman pesan sendiri. OS menggunakan itu untuk melakukan penjadwalan multi-thread yang sebenarnya.

Sejauh perakitan sebenarnya, seperti yang ditulis Nicholas, tidak ada perbedaan antara rakitan untuk aplikasi berulir tunggal atau multi-berulir. Setiap utas logis memiliki set register sendiri, jadi tulislah:

mov edx, 0

hanya akan memperbarui EDXuntuk utas yang sedang berjalan . Tidak ada cara untuk memodifikasi EDXprosesor lain menggunakan instruksi perakitan tunggal. Anda memerlukan semacam panggilan sistem untuk meminta OS memberitahu thread lain untuk menjalankan kode yang akan memperbarui sendiri EDX.

Nathan Fellman
sumber
2
Terima kasih telah mengisi kekosongan dalam jawaban Nicholas. Tandai jawaban Anda sebagai jawaban yang diterima sekarang .... berikan detail spesifik yang saya minati ... walaupun akan lebih baik jika ada satu jawaban yang menggabungkan informasi Anda dan Nicholas.
Paul Hollingsworth
3
Ini tidak menjawab pertanyaan dari mana utas berasal. Core dan prosesor adalah perangkat keras, tetapi entah bagaimana utasnya harus dibuat dalam perangkat lunak. Bagaimana utas utama tahu ke mana harus mengirim SIPI? Atau apakah SIPI sendiri membuat utas baru?
Pengingat kaya
7
@richremer: Sepertinya Anda mengacaukan utas HW dan utas SW. Utas HW selalu ada. Terkadang tertidur. SIPI sendiri membangunkan utas HW dan memungkinkannya untuk menjalankan SW. Terserah OS dan BIOS untuk memutuskan utas HW mana yang berjalan, dan proses dan utas SW mana yang berjalan pada setiap utas HW.
Nathan Fellman
2
Banyak info bagus dan ringkas di sini, tapi ini adalah topik besar - jadi pertanyaan bisa tetap ada. Ada beberapa contoh lengkap kernel "telanjang tulang" di alam liar yang boot dari drive USB atau disk "floppy" - inilah versi x86_32 yang ditulis dalam assembler menggunakan deskriptor TSS lama yang benar-benar dapat menjalankan kode C multi-threaded ( github. com / duanev / oz-x86-32-asm-003 ) tetapi tidak ada dukungan perpustakaan standar. Cukup banyak dari yang Anda minta, tetapi mungkin bisa menjawab beberapa pertanyaan yang masih ada.
duanev
87

Intel x86 contoh baremetal runnable minimal

Runnable bare metal contoh dengan semua boilerplate yang dibutuhkan . Semua bagian utama dibahas di bawah ini.

Diuji pada Ubuntu 15.10 QEMU 2.3.0 dan tamu perangkat keras Lenovo ThinkPad T400 nyata .

The Intel Pedoman Volume 3 System Programming Guide - 325384-056US September 2015 meliputi SMP di bab 8, 9 dan 10.

Tabel 8-1. "Siaran INIT-SIPI-SIPI Urutan dan Pilihan Timeout" berisi contoh yang pada dasarnya hanya berfungsi:

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

Pada kode itu:

  1. Sebagian besar sistem operasi akan membuat sebagian besar operasi tersebut menjadi mustahil dari dering 3 (program pengguna).

    Jadi Anda perlu menulis kernel Anda sendiri untuk bermain bebas dengannya: program userland Linux tidak akan berfungsi.

  2. Pada awalnya, sebuah prosesor berjalan, yang disebut prosesor bootstrap (BSP).

    Itu harus membangunkan yang lain (disebut Application Processors (AP)) melalui interupsi khusus yang disebut Inter Processor Interrupts (IPI) .

    Interupsi tersebut dapat dilakukan dengan memprogram Advanced Programmable Interrupt Controller (APIC) melalui Interrupt command register (ICR)

    Format ICR didokumentasikan di: 10.6 "MENERBITKAN INTERROCESSOR INTERRUPTS"

    IPI terjadi segera setelah kami menulis ke ICR.

  3. ICR_LOW didefinisikan pada 8.4.4 "Contoh Inisialisasi MP" sebagai:

    ICR_LOW EQU 0FEE00300H
    

    Nilai ajaib 0FEE00300adalah alamat memori ICR, seperti yang didokumentasikan pada Tabel 10-1 "Peta Alamat Pendaftaran APIC Lokal"

  4. Metode paling sederhana yang mungkin digunakan dalam contoh: ini mengatur ICR untuk mengirim siaran IPI yang dikirim ke semua prosesor lain kecuali yang saat ini.

    Tetapi juga mungkin, dan direkomendasikan oleh beberapa orang , untuk mendapatkan informasi tentang prosesor melalui pengaturan struktur data khusus oleh BIOS seperti tabel ACPI atau tabel konfigurasi MP Intel dan hanya membangunkan yang Anda perlukan satu per satu.

  5. XXdi 000C46XXHmenyandikan alamat instruksi pertama yang akan dijalankan prosesor sebagai:

    CS = XX * 0x100
    IP = 0
    

    Ingatlah bahwa CS melipatgandakan alamat dengan0x10 , jadi alamat memori sebenarnya dari instruksi pertama adalah:

    XX * 0x1000
    

    Jadi jika misalnya XX == 1, prosesor akan mulai 0x1000.

    Kami kemudian harus memastikan bahwa ada kode mode nyata 16-bit untuk dijalankan di lokasi memori itu, misalnya dengan:

    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
    

    Menggunakan skrip linker adalah kemungkinan lain.

  6. Loop penundaan adalah bagian yang mengganggu untuk mulai bekerja: tidak ada cara super sederhana untuk melakukan tidur seperti itu secara tepat.

    Metode yang mungkin termasuk:

    • PIT (digunakan dalam contoh saya)
    • HPET
    • mengkalibrasi waktu loop sibuk dengan yang di atas, dan gunakan itu sebagai gantinya

    Terkait: Bagaimana menampilkan nomor di layar dan dan tidur selama satu detik dengan rakitan DOS x86?

  7. Saya pikir prosesor awal harus berada dalam mode terproteksi agar ini berfungsi saat kami menulis ke alamat 0FEE00300Hyang terlalu tinggi untuk 16-bit

  8. Untuk berkomunikasi antara prosesor, kita dapat menggunakan spinlock pada proses utama, dan memodifikasi kunci dari inti kedua.

    Kita harus memastikan bahwa memori write back dilakukan, misalnya melalui wbinvd.

Keadaan bersama antara prosesor

8.7.1 "Keadaan Prosesor Logis" mengatakan:

Fitur-fitur berikut adalah bagian dari keadaan arsitektur prosesor logis dalam prosesor Intel 64 atau IA-32 yang mendukung Teknologi Intel Hyper-Threading. Fitur-fitur dapat dibagi menjadi tiga kelompok:

  • Digandakan untuk setiap prosesor logis
  • Dibagikan oleh prosesor logis dalam prosesor fisik
  • Dibagi atau digandakan, tergantung pada implementasinya

Fitur-fitur berikut diduplikasi untuk setiap prosesor logis:

  • Register tujuan umum (EAX, EBX, ECX, EDX, ESI, EDI, ESP, dan EBP)
  • Register segmen (CS, DS, SS, ES, FS, dan GS)
  • Register EFLAGS dan EIP. Perhatikan bahwa CS dan EIP / RIP mendaftar untuk setiap prosesor logis mengarah ke aliran instruksi untuk utas yang dijalankan oleh prosesor logis.
  • x87 register FPU (ST0 hingga ST7, kata status, kata kontrol, kata tag, penunjuk operan data, dan penunjuk instruksi)
  • Register MMX (MM0 hingga MM7)
  • Register XMM (XMM0 hingga XMM7) dan register MXCSR
  • Register kontrol dan register penunjuk tabel sistem (GDTR, LDTR, IDTR, register tugas)
  • Register debug (DR0, DR1, DR2, DR3, DR6, DR7) dan kontrol debug MSR
  • Mesin memeriksa status global (IA32_MCG_STATUS) dan kemampuan pemeriksaan mesin (IA32_MCG_CAP) MSR
  • Modulasi jam termal dan ACPI MSRs kontrol manajemen daya
  • Penghitung cap waktu MSR
  • Sebagian besar register MSR lainnya, termasuk tabel atribut halaman (PAT). Lihat pengecualian di bawah ini.
  • Register APIC lokal.
  • Register tujuan umum tambahan (R8-R15), register XMM (XMM8-XMM15), register kontrol, IA32_EFER pada prosesor Intel 64.

Fitur-fitur berikut dibagikan oleh prosesor logis:

  • Register rentang jenis memori (MTRR)

Apakah fitur berikut ini dibagikan atau digandakan khusus untuk implementasi:

  • IA32_MISC_ENABLE MSR (alamat MSR 1A0H)
  • MSR arsitektur periksa mesin (MCA) (kecuali untuk MSR IA32_MCG_STATUS dan IA32_MCG_CAP)
  • Kontrol pemantauan kinerja dan counter MSR

Pembagian cache dibahas di:

Intel hyperthreads memiliki cache dan pembagian pipa yang lebih besar daripada core yang terpisah: /superuser/133082/hyper-threading-and-dual-core-whats-the-difference/995858#995858

Kernel Linux 4.2

Tindakan inisialisasi utama tampaknya di arch/x86/kernel/smpboot.c .

Contoh ARM minimal runnable runemable

Di sini saya memberikan contoh ARMv8 aarch64 runnable minimal untuk 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 hulu .

Merakit dan menjalankan:

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 \
;

Dalam contoh ini, kami menempatkan CPU 0 dalam putaran spinlock, dan hanya keluar dengan CPU 1 melepaskan spinlock.

Setelah spinlock, CPU 0 kemudian melakukan panggilan keluar semihost yang membuat QEMU berhenti.

Jika Anda memulai QEMU hanya dengan satu CPU -smp 1, maka simulasi hanya hang selamanya di spinlock.

CPU 1 dibangunkan dengan antarmuka PSCI, lebih detail di: ARM: Start / Wakeup / Bringup core / AP CPU lainnya dan berikan alamat mulai eksekusi?

Versi upstream juga memiliki beberapa penyesuaian untuk membuatnya bekerja pada gem5, sehingga Anda dapat bereksperimen dengan karakteristik kinerja juga.

Saya belum mengujinya pada perangkat keras asli, jadi dan saya tidak yakin seberapa portabel ini. Daftar pustaka Raspberry Pi berikut mungkin menarik:

Dokumen ini memberikan beberapa panduan tentang cara menggunakan primitif sinkronisasi ARM yang kemudian dapat Anda gunakan untuk melakukan hal-hal menyenangkan dengan banyak inti: http://infocenter.arm.com/help/topic/com.arm.doc.dht0008a/DHT0008A_arm_synchronization_primitive.pdf

Diuji pada Ubuntu 18.10, GCC 8.2.0, Binutils 2.31.1, QEMU 2.12.0.

Langkah selanjutnya untuk programabilitas yang lebih nyaman

Contoh sebelumnya membangunkan CPU sekunder dan melakukan sinkronisasi memori dasar dengan instruksi khusus, yang merupakan awal yang baik.

Tetapi untuk membuat sistem multicore mudah diprogram, misalnya seperti POSIX pthreads , Anda juga perlu masuk ke topik yang lebih terlibat berikut:

  • setup menyela dan menjalankan timer yang secara berkala memutuskan thread mana yang akan berjalan sekarang. Ini dikenal sebagai multithreading preemptive .

    Sistem seperti itu juga perlu menyimpan dan mengembalikan register utas saat diaktifkan dan dihentikan.

    Dimungkinkan juga untuk memiliki sistem multitasking non-preemptive, tetapi yang mungkin mengharuskan Anda untuk memodifikasi kode Anda sehingga setiap utas menghasilkan (misalnya dengan pthread_yield implementasi), dan itu menjadi lebih sulit untuk menyeimbangkan beban kerja.

    Berikut adalah beberapa contoh timer logam sederhana:

  • menangani konflik memori. Khususnya, setiap utas akan membutuhkan tumpukan unik jika Anda ingin kode dalam C atau bahasa tingkat tinggi lainnya.

    Anda bisa membatasi utas untuk memiliki ukuran tumpukan maksimum tetap, tetapi cara yang lebih baik untuk mengatasinya adalah dengan paging yang memungkinkan tumpukan "ukuran tak terbatas" yang efisien.

    Berikut adalah contoh baremetal aarch64 naif yang akan meledak jika tumpukan tumbuh terlalu dalam

Itulah beberapa alasan bagus untuk menggunakan kernel Linux atau sistem operasi lain :-)

Sinkronisasi memori Userland primitif

Meskipun thread start / stop / management umumnya di luar lingkup userland, namun Anda dapat menggunakan instruksi perakitan dari thread userland untuk menyinkronkan akses memori tanpa potensi panggilan sistem yang lebih mahal.

Anda tentu saja harus lebih suka menggunakan perpustakaan yang dapat membungkus primitif tingkat rendah ini. C ++ standar itu sendiri telah membuat kemajuan besar pada <mutex>dan <atomic>header, dan khususnya denganstd::memory_order . Saya tidak yakin apakah itu mencakup semua semantik memori yang mungkin dapat dicapai, tetapi mungkin saja.

Semantik yang lebih halus sangat relevan dalam konteks mengunci struktur data gratis , yang dapat menawarkan manfaat kinerja dalam kasus-kasus tertentu. Untuk mengimplementasikannya, Anda mungkin harus belajar sedikit tentang berbagai jenis hambatan memori: https://preshing.com/20120710/memory-barriers-are-like-source-control-operations/

Boost misalnya memiliki beberapa implementasi wadah bebas kunci di: https://www.boost.org/doc/libs/1_63_0/doc/html/lockfree.html

Instruksi userland semacam itu juga tampaknya digunakan untuk mengimplementasikan futexpanggilan sistem Linux , yang merupakan salah satu primitif sinkronisasi utama di Linux. man futex4.15 berbunyi:

Panggilan sistem futex () menyediakan metode untuk menunggu hingga kondisi tertentu menjadi benar. Ini biasanya digunakan sebagai konstruksi pemblokiran dalam konteks sinkronisasi memori bersama. Saat menggunakan futex, sebagian besar operasi sinkronisasi dilakukan di ruang pengguna. Program ruang pengguna menggunakan panggilan sistem futex () hanya bila ada kemungkinan program harus diblokir untuk waktu yang lebih lama hingga kondisinya menjadi benar. Operasi futex () lainnya dapat digunakan untuk membangunkan setiap proses atau utas yang menunggu kondisi tertentu.

Nama syscall itu sendiri berarti "Fast Userspace XXX".

Berikut adalah contoh C ++ x86_64 / aarch64 minimal yang tidak berguna dengan inline assembly yang menggambarkan penggunaan dasar dari instruksi semacam itu sebagian besar untuk bersenang-senang:

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

Output yang mungkin:

my_non_atomic_ulong 15264
my_arch_non_atomic_ulong 15267

Dari sini kita melihat bahwa instruksi awalan x86 LOCK / aarch64 LDADDmembuat atom tambahan: tanpanya kita memiliki kondisi balapan pada banyak add , dan jumlah total pada akhirnya kurang dari 20000 yang disinkronkan.

Lihat juga:

Diuji dalam Ubuntu 19.04 amd64 dan dengan mode pengguna QEMU aarch64.

Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功
sumber
Assembler apa yang Anda gunakan untuk mengkompilasi contoh Anda? GAS sepertinya tidak menyukai Anda #include(menganggapnya sebagai komentar), NASM, FASM, YASM tidak tahu sintaks AT&T sehingga tidak mungkin mereka ... jadi apa itu?
Ruslan
@Ruslan gcc, #includeberasal dari preprocessor C. Gunakan yang Makefiledisediakan seperti yang dijelaskan di bagian persiapan : github.com/cirosantilli/x86-bare-metal-examples/blob/… Jika itu tidak berhasil, buka masalah GitHub.
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功
pada x86, apa yang terjadi jika sebuah core menyadari tidak ada lagi proses yang siap dijalankan dalam antrian? (yang mungkin terjadi dari waktu ke waktu pada sistem siaga). Apakah core spinlock pada struktur memori bersama sampai ada tugas baru? (mungkin tidak baik apakah ini akan menggunakan banyak daya) apakah ia memanggil sesuatu seperti HLT untuk tidur sampai ada gangguan? (dalam hal ini siapa yang bertanggung jawab untuk membangunkan inti itu?)
tigrou
@tigrou tidak yakin, tapi saya merasa sangat mungkin bahwa implementasi Linux akan menempatkannya dalam status daya sampai interupsi berikutnya (kemungkinan timer), terutama pada ARM di mana daya adalah kunci. Saya akan mencoba dengan cepat untuk melihat apakah itu dapat diamati secara konkret dengan mudah dengan jejak instruksi dari simulator yang menjalankan Linux, mungkin: github.com/cirosantilli/linux-kernel-module-cheat/tree/…
Ciro Santilli 郝海东 冠状 病六四 事件 法轮功
1
Beberapa informasi (khusus untuk x86 / Windows) dapat ditemukan di sini (lihat "Thread Tidak Berguna"). TL; DR: ketika tidak ada utas yang dapat dijalankan pada CPU, CPU dikirim ke utas menganggur. Bersamaan dengan beberapa tugas lain, pada akhirnya akan memanggil prosesor idle yang terdaftar sebagai rutinitas manajemen daya (melalui driver yang disediakan oleh vendor CPU, misalnya: Intel). Ini mungkin mentransisikan CPU ke kondisi C yang lebih dalam (mis: C0 -> C3) untuk mengurangi konsumsi daya.
tigrou
43

Seperti yang saya pahami, setiap "inti" adalah prosesor yang lengkap, dengan set register sendiri. Pada dasarnya, BIOS memulai Anda dengan satu core berjalan, dan kemudian sistem operasi dapat "memulai" core lainnya dengan menginisialisasi mereka dan mengarahkan mereka pada kode yang akan dijalankan, dll.

Sinkronisasi dilakukan oleh OS. Secara umum, setiap prosesor menjalankan proses yang berbeda untuk OS, sehingga fungsionalitas multi-threading dari sistem operasi bertanggung jawab untuk memutuskan proses mana yang akan menyentuh memori mana, dan apa yang harus dilakukan jika terjadi kehancuran memori.

Nicholas Flynt
sumber
28
yang tidak menimbulkan pertanyaan: Instruksi apa yang tersedia untuk sistem operasi untuk melakukan ini?
Paul Hollingsworth
4
Ada satu set instruksi yang diprivatisasi untuk itu, tapi itu masalah sistem operasi, bukan kode aplikasi. Jika kode aplikasi ingin multithreaded, ia harus memanggil fungsi sistem operasi untuk melakukan "keajaiban".
sharptooth
2
BIOS biasanya akan mengidentifikasi berapa core yang tersedia dan akan meneruskan informasi ini ke OS ketika ditanya. Ada standar yang harus dipatuhi oleh BIOS (dan perangkat keras) sedemikian rupa sehingga akses ke perangkat keras tertentu (prosesor, inti, bus PCI, kartu PCI, mouse, keyboard, grafik, ISA, PCI-E / X, memori dll) untuk PC yang berbeda terlihat sama dari sudut pandang OS. Jika BIOS tidak melaporkan bahwa ada empat core, OS biasanya akan berasumsi bahwa hanya ada satu. Bahkan mungkin ada pengaturan BIOS untuk bereksperimen.
Olof Forshell
1
Itu keren dan semua tetapi bagaimana jika Anda menulis program bare-metal?
Alexander Ryan Baggett
3
@AlexanderRyanBaggett,? Apa itu? Mengulangi, ketika kita mengatakan "serahkan saja ke OS", kita menghindari pertanyaan karena pertanyaannya adalah bagaimana OS melakukannya? Instruksi perakitan apa yang digunakannya?
Pacerier
39

FAQ SMP Tidak Resmi menumpuk logo melimpah


Sekali waktu, untuk menulis assembler x86, misalnya, Anda akan memiliki instruksi yang menyatakan "memuat register EDX dengan nilai 5", "menambah register EDX", dll. Dengan CPU modern yang memiliki 4 core (atau bahkan lebih) , pada level kode mesin apakah itu hanya terlihat seperti ada 4 CPU terpisah (yaitu apakah hanya ada 4 register "EDX" yang berbeda)?

Persis. Ada 4 set register, termasuk 4 petunjuk instruksi terpisah.

Jika demikian, ketika Anda mengatakan "menambah register EDX", apa yang menentukan register EDX CPU mana yang ditambahkan?

CPU yang menjalankan instruksi itu, secara alami. Anggap saja sebagai 4 mikroprosesor yang sama sekali berbeda yang hanya berbagi memori yang sama.

Apakah ada konsep "konteks CPU" atau "utas" di assembler x86 sekarang?

Tidak. Assembler hanya menerjemahkan instruksi seperti biasanya. Tidak ada perubahan di sana.

Bagaimana cara kerja komunikasi / sinkronisasi antar core?

Karena mereka berbagi memori yang sama, sebagian besar masalah logika program. Meskipun sekarang ada mekanisme interupsi antar-prosesor , itu tidak perlu dan pada awalnya tidak ada dalam sistem x86 dual-CPU pertama.

Jika Anda menulis sistem operasi, mekanisme apa yang terbuka melalui perangkat keras untuk memungkinkan Anda menjadwalkan eksekusi pada inti yang berbeda?

Penjadwal sebenarnya tidak berubah, kecuali bahwa itu sedikit lebih hati-hati tentang bagian penting dan jenis kunci yang digunakan. Sebelum SMP, kode kernel pada akhirnya akan memanggil scheduler, yang akan melihat antrian run dan memilih proses untuk dijalankan sebagai utas berikutnya. (Proses ke kernel sangat mirip dengan thread.) Kernel SMP menjalankan kode yang sama persis, satu thread pada satu waktu, hanya saja sekarang penguncian bagian yang kritis perlu SMP-aman untuk memastikan dua core tidak dapat secara tidak sengaja mengambil PID yang sama.

Apakah ini instruksi khusus yang istimewa?

Tidak. Core hanya berjalan di memori yang sama dengan instruksi lama yang sama.

Jika Anda menulis VM kompilator / bytecode pengoptimalan untuk CPU multicore, apa yang perlu Anda ketahui secara spesifik tentang, katakanlah, x86 untuk membuatnya menghasilkan kode yang berjalan efisien di semua core?

Anda menjalankan kode yang sama seperti sebelumnya. Adalah kernel Unix atau Windows yang perlu diubah.

Anda dapat meringkas pertanyaan saya sebagai "Perubahan apa yang telah dibuat pada kode mesin x86 untuk mendukung fungsionalitas multi-inti?"

Tidak ada yang diperlukan. Sistem SMP pertama menggunakan set instruksi yang sama persis dengan uniprocessor. Sekarang, telah ada banyak evolusi arsitektur x86 dan berbagai instruksi baru untuk membuat segalanya berjalan lebih cepat, tetapi tidak ada yang diperlukan untuk SMP.

Untuk informasi lebih lanjut, lihat Spesifikasi Intel Multiprocessor .


Pembaruan: semua pertanyaan tindak lanjut dapat dijawab dengan hanya sepenuhnya menerima bahwa CPU multicore n -jalan hampir 1 persis sama dengan n prosesor terpisah yang hanya berbagi memori yang sama. 2 Ada pertanyaan penting yang tidak ditanyakan: bagaimana program ditulis untuk dijalankan pada lebih dari satu inti untuk kinerja yang lebih? Dan jawabannya adalah: ditulis menggunakan perpustakaan thread like Pthreads. Beberapa perpustakaan utas menggunakan "utas hijau" yang tidak terlihat oleh OS, dan mereka tidak akan mendapatkan inti yang terpisah, tetapi selama perpustakaan utas menggunakan fitur-fitur utas kernel maka program berulir Anda akan secara otomatis menjadi multicore.
1. Untuk kompatibilitas mundur, hanya core pertama yang dimulai pada reset, dan beberapa jenis driver perlu dilakukan untuk menjalankan yang tersisa.
2. Mereka juga berbagi semua periferal, secara alami.

DigitalRoss
sumber
3
Saya selalu berpikir "utas" adalah konsep perangkat lunak, yang membuat saya sulit untuk memahami prosesor multi-inti, masalahnya adalah, bagaimana kode bisa mengatakan inti "Saya akan membuat utas berjalan di inti 2"? Apakah ada kode perakitan khusus untuk melakukannya?
demonguy
2
@ Demonguy: Tidak, tidak ada instruksi khusus untuk hal seperti itu. Anda meminta OS untuk menjalankan utas Anda pada inti tertentu dengan menetapkan topeng afinitas (yang mengatakan "utas ini dapat berjalan pada rangkaian inti logis ini"). Ini sepenuhnya masalah perangkat lunak. Setiap inti CPU (utas perangkat keras) menjalankan Linux (atau Windows) secara independen. Untuk bekerja bersama dengan utas perangkat keras lainnya, mereka menggunakan struktur data bersama. Tetapi Anda tidak pernah "langsung" memulai utas pada CPU yang berbeda. Anda memberi tahu OS bahwa Anda ingin memiliki utas baru, dan itu membuat catatan dalam struktur data yang dilihat OS pada inti lain.
Peter Cordes
2
Saya dapat memberitahu os itu, tetapi bagaimana os meletakkan kode ke inti tertentu?
demonguy
4
@demonguy ... (disederhanakan) ... setiap inti berbagi gambar OS dan mulai menjalankannya di tempat yang sama. Jadi, untuk 8 core, itulah 8 "proses perangkat keras" yang berjalan di kernel. Masing-masing memanggil fungsi scheduler yang sama yang memeriksa tabel proses untuk proses atau utas yang bisa dijalankan. (Itu antrian run. ) Sementara itu, program dengan utas bekerja tanpa kesadaran akan sifat SMP yang mendasarinya. Mereka hanya melakukan fork (2) atau sesuatu dan membiarkan kernel tahu bahwa mereka ingin menjalankan. Pada dasarnya, inti menemukan proses, bukan proses menemukan inti.
DigitalRoss
1
Anda sebenarnya tidak perlu menyela satu inti dari inti lainnya. Pikirkan tentang cara ini: semua yang Anda butuhkan untuk berkomunikasi sebelum itu dikomunikasikan dengan baik dengan mekanisme perangkat lunak. Mekanisme perangkat lunak yang sama terus bekerja. Jadi, pipa, panggilan kernel, sleep / wakeup, semua itu ... masih berfungsi seperti sebelumnya. Tidak setiap proses berjalan pada CPU yang sama tetapi mereka memiliki struktur data yang sama untuk komunikasi seperti sebelumnya. Upaya untuk melanjutkan SMP sebagian besar terbatas untuk membuat kunci lama bekerja di lingkungan yang lebih paralel.
DigitalRoss
10

Jika Anda menulis VM kompilator / bytecode pengoptimalan untuk CPU multicore, apa yang perlu Anda ketahui secara spesifik tentang, katakanlah, x86 untuk membuatnya menghasilkan kode yang berjalan efisien di semua core?

Sebagai seseorang yang menulis optimizer compiler / bytecode VMs saya mungkin dapat membantu Anda di sini.

Anda tidak perlu tahu apa-apa tentang x86 untuk membuatnya menghasilkan kode yang berjalan efisien di semua inti.

Namun, Anda mungkin perlu tahu tentang cmpxchg dan teman-teman untuk menulis kode yang berjalan dengan benar di semua inti. Pemrograman multicore membutuhkan penggunaan sinkronisasi dan komunikasi antara utas eksekusi.

Anda mungkin perlu tahu sesuatu tentang x86 untuk membuatnya menghasilkan kode yang berjalan efisien pada x86 secara umum.

Ada hal-hal lain yang berguna bagi Anda untuk belajar:

Anda harus mempelajari tentang fasilitas yang disediakan OS (Linux atau Windows atau OSX) untuk memungkinkan Anda menjalankan banyak utas. Anda harus belajar tentang API paralelisasi seperti OpenMP dan Blok Bangunan Threading, atau OSX 10.6 "Snow Leopard" yang akan datang "Grand Central".

Anda harus mempertimbangkan apakah kompiler Anda harus memparalelkan otomatis, atau jika pembuat aplikasi yang dikompilasi oleh kompiler Anda perlu menambahkan sintaks khusus atau panggilan API ke dalam programnya untuk memanfaatkan beberapa inti.

Alex Brown
sumber
Tidak memiliki beberapa VM populer seperti .NET dan Java memiliki masalah bahwa proses GC utama mereka tercakup dalam kunci dan pada dasarnya terpisah?
Marco van de Voort
9

Setiap Core dijalankan dari area memori yang berbeda. Sistem operasi Anda akan menunjukkan inti pada program Anda dan inti akan menjalankan program Anda. Program Anda tidak akan menyadari bahwa ada lebih dari satu inti atau pada inti yang dijalankannya.

Juga tidak ada instruksi tambahan yang hanya tersedia untuk Sistem Operasi. Core ini identik dengan chip single core. Setiap Core menjalankan bagian dari Sistem Operasi yang akan menangani komunikasi ke area memori umum yang digunakan untuk pertukaran informasi untuk menemukan area memori berikutnya yang akan dieksekusi.

Ini adalah penyederhanaan tetapi memberi Anda ide dasar tentang bagaimana hal itu dilakukan. Lebih lanjut tentang multicores dan multiprosesor di Embedded.com memiliki banyak informasi tentang topik ini ... Topik ini menjadi rumit dengan sangat cepat!

Gerhard
sumber
Saya pikir orang harus membedakan sedikit lebih hati-hati di sini bagaimana multicore bekerja secara umum, dan seberapa besar pengaruh OS. "Setiap inti dijalankan dari memori yang berbeda," terlalu menyesatkan menurut saya. Pertama dan terutama, menggunakan beberapa inti dalam prinsip tidak memerlukan ini, dan Anda dapat dengan mudah melihat bahwa untuk program berulir Anda INGIN dua inti dua bekerja pada segmen teks dan data yang sama (sementara setiap inti juga membutuhkan sumber daya individu seperti tumpukan) .
Volker Stolz
@ShiDoiSi Itulah sebabnya jawaban saya berisi teks "Ini penyederhanaan" .
Gerhard
5

Kode assembly akan diterjemahkan menjadi kode mesin yang akan dieksekusi pada satu inti. Jika Anda ingin multithreaded, Anda harus menggunakan primitif sistem operasi untuk memulai kode ini pada prosesor yang berbeda beberapa kali atau potongan kode yang berbeda pada core yang berbeda - masing-masing inti akan menjalankan utas terpisah. Setiap utas hanya akan melihat satu inti yang sedang dieksekusi.

sharptooth
sumber
4
Saya akan mengatakan sesuatu seperti ini, tetapi kemudian bagaimana OS mengalokasikan utas ke inti? Saya membayangkan ada beberapa instruksi perakitan istimewa yang mencapai ini. Jika demikian, saya pikir itu adalah jawaban yang dicari penulis.
A. Levy
Tidak ada instruksi untuk itu, itu tugas penjadwal sistem operasi. Ada fungsi sistem operasi seperti SetThreadAffinityMask di Win32 dan kodenya bisa memanggil mereka, tapi itu sistem operasi dan mempengaruhi penjadwal, itu bukan instruksi prosesor.
sharptooth
2
Harus ada OpCode atau sistem operasi tidak akan dapat melakukannya juga.
Matthew Whited
1
Sebenarnya bukan opcode untuk penjadwalan - ini lebih seperti Anda mendapatkan satu salinan OS per prosesor, berbagi ruang memori; setiap kali sebuah inti kembali memasuki kernel (syscall atau interrupt), core akan melihat struktur data yang sama dalam memori untuk memutuskan utas apa yang akan dijalankan selanjutnya.
pjc50
1
@ A.Levy: Ketika Anda memulai utas dengan afinitas yang hanya memungkinkannya berjalan pada inti yang berbeda, itu tidak segera pindah ke inti lainnya. Konteksnya disimpan ke memori, seperti halnya saklar konteks normal. Utas perangkat keras lainnya melihat entri di struktur data penjadwal, dan salah satunya akhirnya akan memutuskan bahwa ia akan menjalankan utas. Jadi dari perspektif inti pertama: Anda menulis ke struktur data bersama dan akhirnya kode OS pada inti lain (utas perangkat keras) akan melihatnya dan menjalankannya.
Peter Cordes
3

Itu tidak dilakukan dalam instruksi mesin sama sekali; core berpura-pura menjadi CPU yang berbeda dan tidak memiliki kemampuan khusus untuk berbicara satu sama lain. Ada dua cara mereka berkomunikasi:

  • mereka berbagi ruang alamat fisik. Perangkat keras menangani koherensi cache, jadi satu CPU menulis ke alamat memori yang dibaca orang lain.

  • mereka berbagi APIC (pengontrol interupsi yang dapat diprogram). Ini adalah memori yang dipetakan ke dalam ruang alamat fisik, dan dapat digunakan oleh satu prosesor untuk mengontrol yang lain, menyalakan atau mematikannya, mengirim interupsi, dll.

http://www.cheesecake.org/sac/smp.html adalah referensi yang bagus dengan url konyol.

pjc50
sumber
2
Mereka sebenarnya tidak berbagi APIC. Setiap CPU logis memiliki CPU masing-masing. APIC berkomunikasi di antara mereka sendiri, tetapi mereka terpisah.
Nathan Fellman
Mereka menyinkronkan (daripada berkomunikasi) dalam satu cara dasar dan itu adalah melalui awalan LOCK (instruksi "xchg mem, reg" berisi permintaan kunci implisit) yang berjalan ke pin kunci yang berjalan ke semua bus secara efektif memberitahu mereka bahwa CPU (sebenarnya perangkat penguasaan bus) menginginkan akses eksklusif ke bus. Akhirnya sebuah sinyal akan kembali ke pin LOCKA (accept) yang memberi tahu CPU bahwa sekarang ia memiliki akses eksklusif ke bus. Karena perangkat eksternal jauh lebih lambat daripada cara kerja internal CPU, urutan LOCK / LOCKA mungkin membutuhkan ratusan siklus CPU untuk diselesaikan.
Olof Forshell
1

Perbedaan utama antara aplikasi single-dan multi-threaded adalah bahwa yang pertama memiliki satu tumpukan dan yang terakhir memiliki satu untuk setiap utas. Kode dihasilkan agak berbeda karena kompiler akan menganggap bahwa register segmen data dan stack (ds dan ss) tidak sama. Ini berarti bahwa tipuan melalui register ebp dan esp yang default ke register ss juga tidak akan default ke ds (karena ds! = Ss). Sebaliknya, tipuan melalui register lain yang default ke ds tidak akan default ke ss.

Utas berbagi segala sesuatu yang lain termasuk area data dan kode. Mereka juga berbagi rutinitas lib jadi pastikan bahwa mereka aman untuk thread. Sebuah prosedur yang mengurutkan suatu area dalam RAM dapat multi-threaded untuk mempercepat. Utas kemudian akan mengakses, membandingkan dan memesan data dalam area memori fisik yang sama dan mengeksekusi kode yang sama tetapi menggunakan variabel lokal yang berbeda untuk mengontrol masing-masing bagian dari pengurutan. Ini tentu saja karena utas memiliki tumpukan yang berbeda di mana variabel lokal terkandung. Jenis pemrograman ini membutuhkan penyetelan kode yang hati-hati sehingga tumbukan data antar-inti (dalam cache dan RAM) berkurang yang pada gilirannya menghasilkan kode yang lebih cepat dengan dua atau lebih utas daripada hanya dengan satu. Tentu saja, kode yang tidak disetel akan lebih cepat dengan satu prosesor daripada dua atau lebih. Men-debug lebih menantang karena breakpoint "int 3" standar tidak akan berlaku karena Anda ingin menginterupsi utas tertentu dan tidak semuanya. Breakpoint register debug tidak menyelesaikan masalah ini kecuali jika Anda dapat mengaturnya pada prosesor spesifik yang menjalankan utas tertentu yang ingin Anda sela.

Kode multi-utas lainnya mungkin melibatkan utas berbeda yang berjalan di berbagai bagian program. Jenis pemrograman ini tidak memerlukan penyetelan yang sama dan karenanya lebih mudah dipelajari.

Olof Forshell
sumber
0

Apa yang telah ditambahkan pada setiap arsitektur dengan kemampuan multiprosesing dibandingkan dengan varian prosesor tunggal yang datang sebelumnya adalah instruksi untuk menyinkronkan antar core. Juga, Anda memiliki instruksi untuk menangani koherensi cache, pembilasan buffer, dan operasi tingkat rendah serupa yang harus dihadapi oleh OS. Dalam kasus arsitektur multithread secara simultan seperti IBM POWER6, IBM Cell, Sun Niagara, dan Intel "Hyperthreading", Anda juga cenderung melihat instruksi baru untuk memprioritaskan di antara utas (seperti menetapkan prioritas dan secara eksplisit menghasilkan prosesor ketika tidak ada yang dilakukan) .

Tetapi semantik satu-thread dasar adalah sama, Anda hanya menambahkan fasilitas tambahan untuk menangani sinkronisasi dan komunikasi dengan core lainnya.

jakobengblom2
sumber