Intel x86 ตัวอย่างหลังเปล่าที่เรียกใช้ได้น้อยที่สุด
ตัวอย่างเช่นโลหะ Runnable เปลือยกับต้นแบบที่จำเป็นทั้งหมด ทุกส่วนที่สำคัญได้รับความคุ้มครองด้านล่าง
ผ่านการทดสอบบน Ubuntu 15.10 QEMU 2.3.0 และ Lenovo ThinkPad T400 ของผู้เข้าพักฮาร์ดแวร์จริง
Intel คู่มือเล่ม 3 การเขียนโปรแกรมระบบคู่มือ - 325384-056US กันยายน 2015ครอบคลุม SMP ในบทที่ 8, 9 และ 10
ตารางที่ 8-1 "Broadcast INIT-SIPI-SIPI Sequence และ Choice of Timeouts" มีตัวอย่างที่ใช้งานได้โดยทั่วไป:
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
ในรหัสนั้น:
ระบบปฏิบัติการส่วนใหญ่จะทำให้การดำเนินการส่วนใหญ่เป็นไปไม่ได้จาก ring 3 (โปรแกรมผู้ใช้)
ดังนั้นคุณต้องเขียนเคอร์เนลของคุณเองเพื่อเล่นอย่างอิสระด้วย: โปรแกรม userland Linux จะไม่ทำงาน
ในตอนแรกตัวประมวลผลเดียวจะเรียกใช้ตัวประมวลผล bootstrap (BSP)
มันจะต้องตื่นขึ้นมาคนอื่น ๆ (เรียกว่าโปรเซสเซอร์ Application (AP)) ผ่านการขัดจังหวะพิเศษที่เรียกว่าInterrupts อินเตอร์ Processor (IPI)
การขัดจังหวะเหล่านั้นสามารถทำได้โดยการตั้งโปรแกรม Advanced Programmable Interrupt Controller (APIC) ผ่าน Interrupt command register (ICR)
รูปแบบของ ICR ได้รับการบันทึกไว้ที่: 10.6 "การหยุดชะงักระหว่างอุปกรณ์ ISSUING INTERPROCESSOR"
IPI เกิดขึ้นทันทีที่เราเขียนถึง ICR
ICR_LOW ถูกกำหนดที่ 8.4.4 "ตัวอย่างการเริ่มต้น MP" เป็น:
ICR_LOW EQU 0FEE00300H
ค่าเวทย์มนตร์0FEE00300
คือที่อยู่หน่วยความจำของ ICR ตามที่ระบุไว้ในตารางที่ 10-1 "แผนที่ที่อยู่การลงทะเบียน APIC ท้องถิ่น"
วิธีที่ง่ายที่สุดที่เป็นไปได้นั้นถูกนำมาใช้ในตัวอย่าง: ตั้งค่า ICR เพื่อส่ง IPI แบบกระจายซึ่งถูกส่งไปยังตัวประมวลผลอื่น ๆ ทั้งหมดยกเว้นตัวประมวลผลปัจจุบัน
แต่ก็เป็นไปได้และแนะนำโดยบางคนเพื่อรับข้อมูลเกี่ยวกับโปรเซสเซอร์ผ่านการตั้งค่าโครงสร้างข้อมูลพิเศษโดย BIOS เช่นตาราง ACPI หรือตารางการกำหนดค่า MP ของ Intelและปลุกเฉพาะสิ่งที่คุณต้องการทีละตัว
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
การใช้สคริปต์ linker เป็นไปได้อีกอย่างหนึ่ง
การวนรอบล่าช้าเป็นส่วนที่น่ารำคาญในการทำงาน: ไม่มีวิธีที่ง่ายที่สุดในการนอนหลับอย่างแม่นยำ
วิธีการที่เป็นไปได้ ได้แก่ :
- PIT (ใช้ในตัวอย่างของฉัน)
- HPET
- ปรับเวลาของลูปที่ยุ่งกับด้านบนและใช้แทน
ที่เกี่ยวข้อง: วิธีแสดงหมายเลขบนหน้าจอและและพักเป็นเวลาหนึ่งวินาทีด้วยชุดประกอบ DOS x86?
ฉันคิดว่าตัวประมวลผลเริ่มต้นต้องอยู่ในโหมดที่ได้รับการป้องกันเพื่อให้ทำงานได้ในขณะที่เราเขียนถึงที่อยู่0FEE00300H
ซึ่งสูงเกินไปสำหรับ 16 บิต
ในการสื่อสารระหว่างโปรเซสเซอร์เราสามารถใช้ spinlock ในกระบวนการหลักและปรับเปลี่ยนการล็อคจากแกนที่สอง
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) และ MSRs ควบคุมการดีบัก
- การตรวจสอบสถานะโกลบอลของเครื่อง (IA32_MCG_STATUS) และความสามารถในการตรวจสอบเครื่อง (IA32_MCG_CAP) MSR
- การมอดูเลตความร้อนและ ACPI การควบคุมการจัดการพลังงาน MSR
- MSRs ตัวนับเวลา
- การลงทะเบียน MSR อื่น ๆ ส่วนใหญ่รวมถึงตารางแอตทริบิวต์เพจ (PAT) ดูข้อยกเว้นด้านล่าง
- APIC ท้องถิ่นลงทะเบียน
- รีจิสเตอร์วัตถุประสงค์ทั่วไปเพิ่มเติม (R8-R15), รีจิสเตอร์ XMM (XMM8-XMM15), รีจิสเตอร์ควบคุม, IA32_EFER บนโปรเซสเซอร์ Intel 64
คุณลักษณะต่อไปนี้แชร์โดยตัวประมวลผลเชิงตรรกะ:
- การลงทะเบียนช่วงประเภทหน่วยความจำ (MTRR)
ไม่ว่าจะเป็นคุณสมบัติที่ใช้ร่วมกันหรือทำซ้ำเป็นเฉพาะการใช้งาน
- IA32_MISC_ENABLE MSR (ที่อยู่ MSR 1A0H)
- สถาปัตยกรรมตรวจสอบเครื่อง (MCA) MSRs (ยกเว้น IA32_MCG_STATUS และ IA32_MCG_CAP MSRs)
- การควบคุมการตรวจสอบประสิทธิภาพและตัวนับ MSR
การแบ่งปันแคชมีการกล่าวถึงที่:
Intel Hyperthreads มีแคชและการแชร์ไปป์ไลน์ที่สูงกว่าแกนประมวลผลแยกกัน: /superuser/133082/hyper-threading-and-dual-core-whats-the-difference/995858#995858
Linux kernel 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 ปล่อยสปินล็อค
หลังจาก spinlock, CPU 0 จะทำการเรียก exit semihostซึ่งทำให้ QEMU ออกจากการทำงาน
ถ้าคุณเริ่มต้น QEMU ด้วย CPU เพียงตัวเดียว-smp 1
การจำลองก็แค่แฮงก์ตลอดไปบน spinlock
CPU 1 ถูกปลุกด้วยอินเตอร์เฟส PSCI, รายละเอียดเพิ่มเติมได้ที่: ARM: เริ่ม / ปลุก / ดึงคอร์ CPU อื่น / APs และส่งผ่านแอดเดรสเริ่มต้นการประมวลผล?
รุ่นต้นน้ำยังมีไม่กี่ปรับแต่งเพื่อให้ทำงานบน gem5 เพื่อให้คุณสามารถทดลองกับลักษณะการทำงานได้เป็นอย่างดี
ฉันไม่ได้ทดสอบกับฮาร์ดแวร์จริงดังนั้นและฉันไม่แน่ใจว่ามันพกพาได้อย่างไร บรรณานุกรม Raspberry Pi ต่อไปนี้อาจเป็นที่สนใจ:
เอกสารนี้ให้คำแนะนำบางอย่างเกี่ยวกับการใช้การซิงโครไนซ์แบบดั้งเดิม ARM ซึ่งคุณสามารถใช้เพื่อทำสิ่งสนุก ๆ กับหลายคอร์: http://infocenter.arm.com/help/topic/com.arm.doc.dht0008a/DHT0008A_arm_synchronization_primitives.pdf
ทดสอบบน Ubuntu 18.10, GCC 8.2.0, Binutils 2.31.1, QEMU 2.12.0
ขั้นตอนถัดไปเพื่อความสะดวกในการโปรแกรมได้มากขึ้น
ตัวอย่างก่อนหน้านี้ปลุก CPU รองและทำการซิงโครไนซ์หน่วยความจำพื้นฐานพร้อมคำแนะนำเฉพาะซึ่งเป็นการเริ่มต้นที่ดี
แต่เพื่อทำให้ระบบมัลติคอร์ง่ายต่อการตั้งโปรแกรมเช่นPOSIX pthreads
คุณจะต้องเข้าไปที่หัวข้อที่เกี่ยวข้องเพิ่มเติมดังต่อไปนี้:
การตั้งค่าขัดจังหวะและเรียกใช้ตัวจับเวลาที่ตัดสินใจเป็นระยะ ๆ ว่าเธรดใดจะทำงานทันที นี้เป็นที่รู้จักกันmultithreading ชิง
ระบบดังกล่าวยังต้องการบันทึกและกู้คืนการลงทะเบียนเธรดเมื่อเริ่มต้นและหยุดทำงาน
นอกจากนี้ยังเป็นไปได้ที่จะมีระบบมัลติทาสก์ที่ไม่ต้องห้าม แต่สิ่งเหล่านี้อาจต้องการให้คุณแก้ไขโค้ดของคุณเพื่อให้เธรดทุก ๆ pthread_yield
ติดตั้งใช้งาน)
นี่คือตัวอย่างตัวจับเวลาโลหะเปลือยแบบง่าย ๆ :
จัดการกับความขัดแย้งของหน่วยความจำ ยวดแต่ละกระทู้จะต้องมีกองที่ไม่ซ้ำกันหากคุณต้องการโค้ดในภาษา C หรือภาษาระดับสูงอื่น ๆ
คุณสามารถ จำกัด เธรดให้มีขนาดสแต็กสูงสุดคงที่ แต่วิธีที่ดีกว่าในการจัดการกับนี้คือการเพจซึ่งช่วยให้กองซ้อน "ไม่ จำกัด ขนาด" ที่มีประสิทธิภาพ
นี่คือตัวอย่างไร้เดียงสา aarch64 ไร้เดียงสาที่จะระเบิดขึ้นหากกองเติบโตลึกเกินไป
นี่คือเหตุผลที่ดีในการใช้เคอร์เนล Linux หรือระบบปฏิบัติการอื่น :-)
พื้นฐานการซิงโครไนซ์หน่วยความจำ Userland
แม้ว่าโดยทั่วไปการเริ่ม / หยุด / การจัดการเธรดจะอยู่นอกเหนือขอบเขตของ Userland คุณสามารถใช้คำแนะนำการประกอบจากเธรดของ Userland เพื่อซิงโครไนซ์การเข้าถึงหน่วยความจำโดยไม่ต้องใช้การเรียกระบบที่แพงกว่า
แน่นอนว่าคุณควรเลือกใช้ไลบรารีที่ห่อสิ่งพื้นฐานดั้งเดิมในระดับต่ำ มาตรฐาน 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 futex
4.15 อ่าน:
การเรียกระบบ futex () จัดเตรียมวิธีการรอจนกว่าเงื่อนไขบางอย่างจะเป็นจริง โดยทั่วไปจะใช้เป็นโครงสร้างการบล็อกในบริบทของการซิงโครไนซ์หน่วยความจำที่ใช้ร่วมกัน เมื่อใช้ futexes การดำเนินการซิงโครไนซ์ส่วนใหญ่จะดำเนินการในพื้นที่ของผู้ใช้ โปรแกรมพื้นที่ผู้ใช้ใช้การเรียกระบบ futex () เฉพาะเมื่อมีโอกาสที่โปรแกรมจะต้องบล็อกเป็นเวลานานกว่าจนกว่าเงื่อนไขจะเป็นจริง การดำเนินการ futex () อื่น ๆ สามารถใช้เพื่อปลุกกระบวนการหรือเธรดใด ๆ ที่รอเงื่อนไขเฉพาะ
ชื่อ syscall นั้นหมายถึง "Fast Userspace XXX"
นี่คือตัวอย่าง C ++ x86_64 / aarch64 ที่ไร้ประโยชน์น้อยที่สุดพร้อมชุดประกอบแบบอินไลน์ที่แสดงการใช้งานพื้นฐานของคำแนะนำดังกล่าวเพื่อความสนุกสนานเป็นส่วนใหญ่:
main.cpp
#include <atomic>
#include <cassert>
#include <iostream>
#include <thread>
#include <vector>
std::atomic_ulong my_atomic_ulong(0);
unsigned long my_non_atomic_ulong = 0;
#if defined(__x86_64__) || defined(__aarch64__)
unsigned long my_arch_atomic_ulong = 0;
unsigned long my_arch_non_atomic_ulong = 0;
#endif
size_t niters;
void threadMain() {
for (size_t i = 0; i < niters; ++i) {
my_atomic_ulong++;
my_non_atomic_ulong++;
#if defined(__x86_64__)
__asm__ __volatile__ (
"incq %0;"
: "+m" (my_arch_non_atomic_ulong)
:
:
);
// https://github.com/cirosantilli/linux-kernel-module-cheat#x86-lock-prefix
__asm__ __volatile__ (
"lock;"
"incq %0;"
: "+m" (my_arch_atomic_ulong)
:
:
);
#elif defined(__aarch64__)
__asm__ __volatile__ (
"add %0, %0, 1;"
: "+r" (my_arch_non_atomic_ulong)
:
:
);
// https://github.com/cirosantilli/linux-kernel-module-cheat#arm-lse
__asm__ __volatile__ (
"ldadd %[inc], xzr, [%[addr]];"
: "=m" (my_arch_atomic_ulong)
: [inc] "r" (1),
[addr] "r" (&my_arch_atomic_ulong)
:
);
#endif
}
}
int main(int argc, char **argv) {
size_t nthreads;
if (argc > 1) {
nthreads = std::stoull(argv[1], NULL, 0);
} else {
nthreads = 2;
}
if (argc > 2) {
niters = std::stoull(argv[2], NULL, 0);
} else {
niters = 10000;
}
std::vector<std::thread> threads(nthreads);
for (size_t i = 0; i < nthreads; ++i)
threads[i] = std::thread(threadMain);
for (size_t i = 0; i < nthreads; ++i)
threads[i].join();
assert(my_atomic_ulong.load() == nthreads * niters);
// We can also use the atomics direclty through `operator T` conversion.
assert(my_atomic_ulong == my_atomic_ulong.load());
std::cout << "my_non_atomic_ulong " << my_non_atomic_ulong << std::endl;
#if defined(__x86_64__) || defined(__aarch64__)
assert(my_arch_atomic_ulong == nthreads * niters);
std::cout << "my_arch_non_atomic_ulong " << my_arch_non_atomic_ulong << std::endl;
#endif
}
GitHub ต้นน้ำ
เอาต์พุตที่เป็นไปได้:
my_non_atomic_ulong 15264
my_arch_non_atomic_ulong 15267
จากนี้เราจะเห็นว่าคำสั่ง x86 LOCK LDADD
คำสั่ง/ aarch64 ทำให้อะตอมเพิ่มขึ้น: โดยไม่มีเรามีเงื่อนไขการแข่งขันในการเพิ่มจำนวนมากและการนับรวมในตอนท้ายนั้นน้อยกว่า 20000 ที่ซิงโครไนซ์
ดูสิ่งนี้ด้วย:
ทดสอบใน Ubuntu 19.04 amd64 และด้วยโหมดผู้ใช้ QEMU aarch64