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?
Jawaban:
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:
hanya akan memperbarui
EDX
untuk utas yang sedang berjalan . Tidak ada cara untuk memodifikasiEDX
prosesor lain menggunakan instruksi perakitan tunggal. Anda memerlukan semacam panggilan sistem untuk meminta OS memberitahu thread lain untuk menjalankan kode yang akan memperbarui sendiriEDX
.sumber
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:
Pada kode itu:
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.
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.
ICR_LOW didefinisikan pada 8.4.4 "Contoh Inisialisasi MP" sebagai:
Nilai ajaib
0FEE00300
adalah alamat memori ICR, seperti yang didokumentasikan pada Tabel 10-1 "Peta Alamat Pendaftaran APIC Lokal"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.
XX
di000C46XXH
menyandikan alamat instruksi pertama yang akan dijalankan prosesor sebagai:Ingatlah bahwa CS melipatgandakan alamat dengan
0x10
, jadi alamat memori sebenarnya dari instruksi pertama adalah:Jadi jika misalnya
XX == 1
, prosesor akan mulai0x1000
.Kami kemudian harus memastikan bahwa ada kode mode nyata 16-bit untuk dijalankan di lokasi memori itu, misalnya dengan:
Menggunakan skrip linker adalah kemungkinan lain.
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:
Terkait: Bagaimana menampilkan nomor di layar dan dan tidur selama satu detik dengan rakitan DOS x86?
Saya pikir prosesor awal harus berada dalam mode terproteksi agar ini berfungsi saat kami menulis ke alamat
0FEE00300H
yang terlalu tinggi untuk 16-bitUntuk 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:
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:
GitHub hulu .
Merakit dan menjalankan:
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
futex
panggilan sistem Linux , yang merupakan salah satu primitif sinkronisasi utama di Linux.man futex
4.15 berbunyi: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
GitHub hulu .
Output yang mungkin:
Dari sini kita melihat bahwa instruksi awalan x86 LOCK / aarch64
LDADD
membuat 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.
sumber
#include
(menganggapnya sebagai komentar), NASM, FASM, YASM tidak tahu sintaks AT&T sehingga tidak mungkin mereka ... jadi apa itu?gcc
,#include
berasal dari preprocessor C. Gunakan yangMakefile
disediakan seperti yang dijelaskan di bagian persiapan : github.com/cirosantilli/x86-bare-metal-examples/blob/… Jika itu tidak berhasil, buka masalah GitHub.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.
sumber
FAQ SMP Tidak Resmi
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.
sumber
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.
sumber
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!
sumber
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.
sumber
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.
sumber
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.
sumber
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.
sumber