ภาษาแอสเซมบลีของมัลติคอร์มีลักษณะอย่างไร


243

กาลครั้งหนึ่งเมื่อต้องการเขียนแอสเซมเบลอร์ x86 คุณจะมีคำแนะนำที่ระบุ "โหลดการลงทะเบียน EDX ด้วยค่า 5", "การเพิ่ม EDX" การลงทะเบียนเป็นต้น

ด้วยซีพียูสมัยใหม่ที่มี 4 คอร์ (หรือมากกว่านั้น) ที่ระดับรหัสเครื่องมันดูเหมือนกับซีพียูที่แยกกัน 4 ตัว (เช่นมีเรจิสเตอร์ "EDX" ที่แตกต่างกัน 4 ตัว)? หากเป็นเช่นนั้นเมื่อคุณพูดว่า "การเพิ่มการลงทะเบียน EDX" การพิจารณาการลงทะเบียน EDX ของ CPU ใดจะเพิ่มขึ้น มีแนวคิด "บริบทของ CPU" หรือ "เธรด" ในแอสเซมเบลอร์ x86 หรือไม่?

การสื่อสาร / การซิงโครไนซ์ระหว่างแกนทำงานอย่างไร

หากคุณกำลังเขียนระบบปฏิบัติการกลไกใดที่จะถูกเปิดเผยผ่านฮาร์ดแวร์เพื่อให้คุณสามารถกำหนดเวลาดำเนินการบนแกนที่ต่างกันได้ เป็นคำสั่งพิเศษที่มีสิทธิพิเศษหรือไม่?

หากคุณกำลังเขียนการเพิ่มประสิทธิภาพ VM / คอมไพเลอร์ bytecode สำหรับ CPU แบบมัลติคอร์สิ่งที่คุณจำเป็นต้องรู้โดยเฉพาะเกี่ยวกับการพูด x86 เพื่อให้มันสร้างรหัสที่ทำงานอย่างมีประสิทธิภาพทั่วทุกแกน?

มีการเปลี่ยนแปลงอะไรในรหัสเครื่อง x86 เพื่อรองรับฟังก์ชั่นมัลติคอร์?


2
มีคำถามที่คล้ายกัน (แต่ไม่เหมือนกัน) ที่นี่: stackoverflow.com/questions/714905/…
นาธาน Fellman

คำตอบ:


153

นี่ไม่ใช่คำตอบสำหรับคำถามโดยตรง แต่เป็นคำตอบของคำถามที่ปรากฏในความคิดเห็น โดยพื้นฐานแล้วคำถามคือสิ่งที่สนับสนุนฮาร์ดแวร์ให้กับการดำเนินการแบบมัลติเธรด

Nicholas Flynt ถูกต้องอย่างน้อยก็เกี่ยวกับ x86 ในสภาพแวดล้อมหลายเธรด (Hyper-Threading, แบบ multi-core หรือประมวลผลแบบหลาย) ที่ด้ายเงินทุน (ปกติด้าย 0 0 ในหลักในการประมวลผล 0) 0xfffffff0เริ่มต้นขึ้นเรียกรหัสจากที่อยู่ เธรดอื่นทั้งหมดเริ่มต้นในสถานะสลีปพิเศษที่เรียกว่าWait-for-SIPIรอสำหรับในฐานะส่วนหนึ่งของการเริ่มต้นเธรดหลักจะส่ง inter-processor-interrupt (IPI) พิเศษผ่าน APIC ที่เรียกว่า SIPI (Startup IPI) ไปยังแต่ละเธรดที่อยู่ใน WFS SIPI มีที่อยู่ซึ่งเธรดควรเริ่มดึงรหัส

กลไกนี้อนุญาตให้แต่ละเธรดเรียกใช้งานรหัสจากที่อยู่อื่น สิ่งที่จำเป็นคือการสนับสนุนซอฟต์แวร์สำหรับแต่ละเธรดเพื่อตั้งค่าตารางและคิวการส่งข้อความของตัวเอง ระบบปฏิบัติการใช้สิ่งที่ต้องทำเพื่อกำหนดตารางเวลาแบบมัลติเธรดจริง

เท่าที่แอสเซมบลีที่แท้จริงเกี่ยวข้องตามที่นิโคลัสเขียนไม่มีความแตกต่างระหว่างแอสเซมบลีสำหรับโปรแกรมประยุกต์เธรดเดียวหรือหลายเธรด แต่ละเธรดแบบโลจิคัลมีชุดรีจิสเตอร์ของตัวเองดังนั้นการเขียน:

mov edx, 0

จะอัปเดตEDXสำหรับเธรดที่กำลังทำงานอยู่เท่านั้น ไม่มีวิธีแก้ไขEDXในตัวประมวลผลอื่นโดยใช้คำสั่งชุดประกอบเดียว คุณต้องเรียงลำดับของการเรียกระบบที่จะถาม OS EDXที่จะบอกหัวข้ออื่นที่จะเรียกใช้รหัสที่จะอัปเดตของตัวเอง


2
ขอบคุณที่เติมช่องว่างในคำตอบของนิโคลัส ทำเครื่องหมายว่าคุณเป็นคำตอบที่ได้รับการยอมรับในขณะนี้ .... ให้รายละเอียดเฉพาะที่ฉันสนใจ ... แม้ว่ามันจะดีกว่าถ้ามีคำตอบเดียวที่ให้ข้อมูลของคุณกับนิโคลัสที่รวมกันทั้งหมด
Paul Hollingsworth

3
นี่ไม่ได้ตอบคำถามที่ว่ากระทู้มาจากไหน Cores และตัวประมวลผลเป็นสิ่งฮาร์ดแวร์ แต่อย่างใดเธรดต้องถูกสร้างขึ้นในซอฟต์แวร์ เธรดหลักรู้ได้อย่างไรว่าจะส่ง SIPI ได้อย่างไร หรือ SIPI เองสร้างเธรดใหม่หรือไม่
รวย remer

7
@richremer: ดูเหมือนว่าคุณสับสนหัวข้อ HW และ SW SW เธรด HW มีอยู่เสมอ บางครั้งมันหลับ SIPI ปลุกเธรด HW และอนุญาตให้รัน SW มันขึ้นอยู่กับระบบปฏิบัติการและ BIOS ในการตัดสินใจว่าจะใช้เธรด HW ใดและกระบวนการใดและเธรด SW จะทำงานในแต่ละเธรด HW
Nathan Fellman

2
มีข้อมูลที่ดีและกระชับจำนวนมากที่นี่ แต่นี่เป็นหัวข้อใหญ่ - คำถามจึงมีอิทธิพล มีตัวอย่างบางส่วนของที่สมบูรณ์แบบ "กระดูกเปลือย" มีเมล็ดในป่าที่บูตจากไดรฟ์ USB หรือ "ฟลอปปี้" ดิสก์ - นี่คือรุ่น x86_32 เขียนในประกอบการใช้อธิบาย TSS เก่าที่สามารถทำงานได้จริงรหัส C แบบมัลติเธรด ( GitHub com / duanev / oz-x86-32-asm-003 ) แต่ไม่มีการสนับสนุนไลบรารีมาตรฐาน ค่อนข้างมากกว่าที่คุณถาม แต่อาจจะสามารถตอบคำถามที่รออยู่ได้
duanev

87

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

ในรหัสนั้น:

  1. ระบบปฏิบัติการส่วนใหญ่จะทำให้การดำเนินการส่วนใหญ่เป็นไปไม่ได้จาก ring 3 (โปรแกรมผู้ใช้)

    ดังนั้นคุณต้องเขียนเคอร์เนลของคุณเองเพื่อเล่นอย่างอิสระด้วย: โปรแกรม userland Linux จะไม่ทำงาน

  2. ในตอนแรกตัวประมวลผลเดียวจะเรียกใช้ตัวประมวลผล bootstrap (BSP)

    มันจะต้องตื่นขึ้นมาคนอื่น ๆ (เรียกว่าโปรเซสเซอร์ Application (AP)) ผ่านการขัดจังหวะพิเศษที่เรียกว่าInterrupts อินเตอร์ Processor (IPI)

    การขัดจังหวะเหล่านั้นสามารถทำได้โดยการตั้งโปรแกรม Advanced Programmable Interrupt Controller (APIC) ผ่าน Interrupt command register (ICR)

    รูปแบบของ ICR ได้รับการบันทึกไว้ที่: 10.6 "การหยุดชะงักระหว่างอุปกรณ์ ISSUING INTERPROCESSOR"

    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
    

    การใช้สคริปต์ linker เป็นไปได้อีกอย่างหนึ่ง

  6. การวนรอบล่าช้าเป็นส่วนที่น่ารำคาญในการทำงาน: ไม่มีวิธีที่ง่ายที่สุดในการนอนหลับอย่างแม่นยำ

    วิธีการที่เป็นไปได้ ได้แก่ :

    • PIT (ใช้ในตัวอย่างของฉัน)
    • HPET
    • ปรับเวลาของลูปที่ยุ่งกับด้านบนและใช้แทน

    ที่เกี่ยวข้อง: วิธีแสดงหมายเลขบนหน้าจอและและพักเป็นเวลาหนึ่งวินาทีด้วยชุดประกอบ DOS x86?

  7. ฉันคิดว่าตัวประมวลผลเริ่มต้นต้องอยู่ในโหมดที่ได้รับการป้องกันเพื่อให้ทำงานได้ในขณะที่เราเขียนถึงที่อยู่0FEE00300Hซึ่งสูงเกินไปสำหรับ 16 บิต

  8. ในการสื่อสารระหว่างโปรเซสเซอร์เราสามารถใช้ 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 futex4.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


แอสเซมเบลอร์ใดที่คุณใช้เพื่อรวบรวมตัวอย่างของคุณ GAS ดูเหมือนจะไม่ชอบ#include(ใช้เป็นความคิดเห็น), NASM, FASM, YASM ไม่รู้จักไวยากรณ์ AT&T ดังนั้นจึงไม่สามารถเป็นได้ ... ดังนั้นมันคืออะไร?
Ruslan

@Ruslan gcc, #includeมาจาก preprocessor ซี ใช้สิ่งที่Makefileเตรียมไว้ตามที่อธิบายไว้ในส่วนเริ่มต้นใช้งาน: github.com/cirosantilli/x86-bare-metal-examples/blob/…หากไม่ได้ผลให้เปิดปัญหา GitHub
Ciro Santilli 郝海东冠状病六四事件法轮功

บน x86 จะเกิดอะไรขึ้นถ้าแกนประมวลผลรู้ว่าไม่มีกระบวนการใดพร้อมที่จะทำงานในคิวอีกต่อไป? (ซึ่งอาจเกิดขึ้นเป็นครั้งคราวในระบบว่าง) แกนหมุนในโครงสร้างหน่วยความจำที่แชร์จนกว่าจะมีงานใหม่หรือไม่? (อาจจะไม่ดีมันจะใช้พลังงานมาก) มันเรียกบางอย่างเช่น HLT นอนจนกว่าจะมีการขัดจังหวะหรือไม่? (ในกรณีที่ว่าใครเป็นผู้รับผิดชอบที่จะตื่นขึ้นหลักที่?)
tigrou

@tigrou ไม่แน่ใจ แต่ฉันคิดว่ามันเป็นไปได้อย่างมากที่การใช้งาน Linux จะทำให้มันอยู่ในสถานะพลังงานจนกระทั่งขัดจังหวะต่อไป ฉันจะพยายามอย่างรวดเร็วเพื่อดูว่าสามารถสังเกตได้อย่างง่ายดายด้วยการติดตามคำสั่งของโปรแกรมจำลองที่ใช้ Linux หรือไม่อาจเป็น: github.com/cirosantilli/linux-kernel-module-cheat/tree/
Ciro Santilli 郝海东冠状病六四事件法轮功

1
ข้อมูลบางอย่าง (เฉพาะ x86 / Windows) สามารถดูได้ที่นี่ (ดู "Idle Thread") TL; DR: เมื่อไม่มีเธรดที่รันได้บน CPU CPU จะถูกส่งไปยังเธรดที่ไม่ทำงาน นอกเหนือจากงานอื่น ๆ แล้วในที่สุดมันจะเรียกรูทีนตัวประมวลผลการจัดการพลังงานที่ไม่ได้ใช้งานในที่สุด (ผ่านไดร์เวอร์ที่จัดทำโดยผู้จำหน่าย CPU เช่น Intel) สิ่งนี้อาจเปลี่ยน CPU ไปเป็นสถานะ C ที่ลึกกว่า (เช่น: C0 -> C3) เพื่อลดการใช้พลังงาน
tigrou

43

ตามที่ฉันเข้าใจแล้ว "คอร์" แต่ละตัวเป็นโปรเซสเซอร์ที่สมบูรณ์พร้อมชุดรีจิสเตอร์ของตัวเอง โดยพื้นฐานแล้ว BIOS จะเริ่มต้นคุณด้วยการใช้งานคอร์หนึ่งตัวจากนั้นระบบปฏิบัติการสามารถ "เริ่มต้น" คอร์อื่น ๆ ได้โดยการเริ่มต้นและชี้ไปที่โค้ดเพื่อรันเป็นต้น

การซิงโครไนซ์ทำได้โดยระบบปฏิบัติการ โดยทั่วไปโปรเซสเซอร์แต่ละตัวใช้กระบวนการที่แตกต่างกันสำหรับระบบปฏิบัติการดังนั้นการทำงานแบบมัลติเธรดของระบบปฏิบัติการจะทำหน้าที่ตัดสินว่ากระบวนการใดที่จะไปแตะที่หน่วยความจำและสิ่งที่ต้องทำในกรณีที่หน่วยความจำชนกัน


28
คำถามใดที่: ถามว่าระบบปฏิบัติการมีคำแนะนำอะไรให้ทำบ้าง?
Paul Hollingsworth

4
มีชุดของคำแนะนำส่วนตัวสำหรับสิ่งนั้น แต่เป็นปัญหาของระบบปฏิบัติการไม่ใช่รหัสแอปพลิเคชัน หากต้องการแอพพลิเคชั่นรหัสแบบมัลติเธรดจะต้องเรียกใช้ฟังก์ชั่นระบบปฏิบัติการเพื่อทำการ "วิเศษ"
sharptooth

2
BIOS มักจะระบุจำนวนคอร์ที่มีอยู่และจะส่งข้อมูลนี้ไปยังระบบปฏิบัติการเมื่อถูกถาม มีมาตรฐานที่ BIOS (และฮาร์ดแวร์) จะต้องสอดคล้องกับการเข้าถึงฮาร์ดแวร์เฉพาะ (โปรเซสเซอร์, คอร์, บัส PCI, การ์ด PCI, เมาส์, คีย์บอร์ด, กราฟิก, ISA, PCI-E / X, หน่วยความจำ ฯลฯ ) สำหรับพีซีที่แตกต่างกัน ดูเหมือนกันจากมุมมองของระบบปฏิบัติการ หากไบออสไม่รายงานว่ามีสี่คอร์ระบบปฏิบัติการมักจะสันนิษฐานว่ามีเพียงหนึ่งคอร์ อาจมีการตั้งค่า BIOS เพื่อทดสอบด้วย
Olof Forshell

1
มันยอดเยี่ยมและทั้งหมด แต่ถ้าคุณเขียนโปรแกรมโลหะเปลือย
Alexander Ryan Baggett

3
@AlexanderRyanBaggett,? อะไรกันนะ? การทำซ้ำเมื่อเราพูดว่า "ปล่อยให้ระบบปฏิบัติการ" เราหลีกเลี่ยงคำถามเพราะคำถามคือระบบปฏิบัติการนั้นเป็นอย่างไร คำแนะนำในการประกอบอะไรที่ใช้?
Pacerier

39

คำถามที่พบบ่อย SMP อย่างไม่เป็นทางการ โลโก้สแต็คล้น


กาลครั้งหนึ่งในการเขียนแอสเซมเบลอร์ x86 คุณจะมีคำแนะนำที่ระบุ "โหลดการลงทะเบียน EDX ด้วยค่า 5", "การเพิ่มการลงทะเบียน EDX" ฯลฯ ด้วยซีพียูสมัยใหม่ที่มี 4 คอร์ (หรือมากกว่านั้น) ที่ระดับรหัสเครื่องมันดูเหมือนว่ามี CPU แยกกัน 4 ตัว (เช่นมีการลงทะเบียน "EDX" ที่แตกต่างกันเพียง 4 ตัว)?

เผง การลงทะเบียนมี 4 ชุดรวมถึงตัวชี้คำสั่งแยก 4 ชุด

หากเป็นเช่นนั้นเมื่อคุณพูดว่า "การเพิ่มการลงทะเบียน EDX" การพิจารณาการลงทะเบียน EDX ของ CPU ใดจะเพิ่มขึ้น

ซีพียูที่ดำเนินการคำสั่งนั้นตามธรรมชาติ คิดว่ามันเป็นไมโครโปรเซสเซอร์ที่แตกต่างกัน 4 ตัวที่แชร์หน่วยความจำเดียวกัน

มีแนวคิด "บริบทของ CPU" หรือ "เธรด" ในแอสเซมเบลอร์ x86 หรือไม่?

ไม่ผู้ประกอบเพิ่งแปลคำแนะนำเหมือนที่เคยทำมา ไม่มีการเปลี่ยนแปลง

การสื่อสาร / การซิงโครไนซ์ระหว่างแกนทำงานอย่างไร

เนื่องจากพวกเขาแชร์หน่วยความจำเดียวกันส่วนใหญ่มันเป็นเรื่องของตรรกะของโปรแกรม แม้ว่าตอนนี้จะมีกลไกการขัดจังหวะระหว่างตัวประมวลผลแต่ก็ไม่จำเป็นและไม่ได้มีอยู่ในระบบ dual-CPU x86 ตัวแรก

หากคุณกำลังเขียนระบบปฏิบัติการกลไกใดที่จะถูกเปิดเผยผ่านฮาร์ดแวร์เพื่อให้คุณสามารถกำหนดเวลาดำเนินการบนแกนที่ต่างกันได้

ตัวกำหนดตารางเวลาจะไม่เปลี่ยนแปลงยกเว้นว่าจะมีความระมัดระวังมากขึ้นเล็กน้อยเกี่ยวกับส่วนที่สำคัญและประเภทของการล็อกที่ใช้ ก่อน SMP รหัสเคอร์เนลจะเรียกตัวกำหนดตารางเวลาในที่สุดซึ่งจะดูที่คิวการทำงานและเลือกกระบวนการเพื่อให้ทำงานเป็นเธรดถัดไป (กระบวนการที่ใช้เคอร์เนลมีลักษณะคล้ายกับเธรดมาก) เคอร์เนล SMP รันรหัสเดียวกันที่แน่นอนครั้งละหนึ่งเธรดซึ่งตอนนี้การล็อกส่วนที่สำคัญจำเป็นต้องปลอดภัยสำหรับ SMP เพื่อให้แน่ใจว่าสองคอร์ไม่สามารถเลือกได้โดยไม่ตั้งใจ PID เดียวกัน

มันเป็นคำแนะนำพิเศษที่มีสิทธิพิเศษไหม?

ไม่แกนประมวลผลทั้งหมดทำงานในหน่วยความจำเดียวกันพร้อมกับคำสั่งเดิม

หากคุณกำลังเขียนการเพิ่มประสิทธิภาพ VM / คอมไพเลอร์ bytecode สำหรับ CPU แบบมัลติคอร์สิ่งที่คุณจำเป็นต้องรู้โดยเฉพาะเกี่ยวกับการพูด x86 เพื่อให้มันสร้างรหัสที่ทำงานอย่างมีประสิทธิภาพทั่วทุกแกน?

คุณเรียกใช้รหัสเดียวกันเหมือนก่อน มันเป็นเคอร์เนล Unix หรือ Windows ที่ต้องเปลี่ยน

คุณสามารถสรุปคำถามของฉันว่า "มีการเปลี่ยนแปลงอะไรบ้างกับรหัสเครื่อง x86 เพื่อรองรับการทำงานแบบมัลติคอร์?"

ไม่มีอะไรจำเป็น ระบบ SMP แรกใช้ชุดคำสั่งเดียวกับหน่วยประมวลผลกลาง ตอนนี้มีวิวัฒนาการของสถาปัตยกรรม x86 และคำสั่งใหม่จำนวนมากเพื่อให้สิ่งต่าง ๆ ดำเนินไปเร็วขึ้น แต่ไม่มีความจำเป็นใด ๆสำหรับ SMP

สำหรับข้อมูลเพิ่มเติมโปรดดูที่อินเทลมัลติข้อมูลจำเพาะ


ปรับปรุง:ทุกคำถามที่ติดตามสามารถตอบได้โดยเพียงแค่สมบูรณ์ยอมรับว่าn -way multicore CPU เป็นเกือบ1สิ่งเดียวกันเป็นnประมวลผลแยกต่างหากที่เพิ่งใช้หน่วยความจำเดียวกัน 2 มีคำถามสำคัญที่ไม่ถามว่า: โปรแกรมที่เขียนเพื่อทำงานบนแกนหลักมากกว่าหนึ่งแกนเพื่อประสิทธิภาพที่เพิ่มขึ้นได้อย่างไร และคำตอบคือ: มันเขียนโดยใช้ไลบรารีเธรดเช่นPthreads ไลบรารีเธรดบางตัวใช้ "เธรดเขียว" ที่มองไม่เห็นในระบบปฏิบัติการและเหล่านั้นจะไม่ได้รับคอร์แยกกัน แต่ตราบใดที่ไลบรารีเธรดใช้คุณลักษณะเคอร์เนลเธรดแล้วโปรแกรมเธรดของคุณจะเป็นมัลติคอร์โดยอัตโนมัติ
1. เพื่อความเข้ากันได้ย้อนหลังเฉพาะแกนแรกเริ่มต้นที่การตั้งค่าใหม่และสิ่งที่ประเภทของไดรเวอร์ไม่กี่ต้องทำเพื่อไฟขึ้นส่วนที่เหลือ
2. พวกเขายังแบ่งปันอุปกรณ์ต่อพ่วงทั้งหมดตามธรรมชาติ


3
ฉันมักจะคิดว่า "เธรด" เป็นแนวคิดซอฟต์แวร์ซึ่งทำให้ฉันยากที่จะเข้าใจตัวประมวลผลแบบมัลติคอร์ปัญหาคือรหัสจะบอกแกนได้อย่างไร "ฉันจะสร้างเธรดที่รันในคอร์ 2" มีรหัสแอสเซมบลีพิเศษที่ต้องทำหรือไม่?
demonguy

2
@demonguy: ไม่ไม่มีคำสั่งพิเศษสำหรับอะไรแบบนั้น คุณขอให้ระบบปฏิบัติการเรียกใช้เธรดของคุณบนแกนที่เฉพาะเจาะจงโดยการตั้งค่ารูปแบบความสัมพันธ์ (ซึ่งระบุว่า "เธรดนี้สามารถทำงานบนชุดของแกนตรรกะนี้") มันเป็นปัญหาซอฟต์แวร์อย่างสมบูรณ์ CPU core แต่ละตัว (เธรดฮาร์ดแวร์) นั้นใช้ Linux (หรือ Windows) อย่างอิสระ เมื่อต้องการทำงานร่วมกับเธรดฮาร์ดแวร์อื่นพวกเขาใช้โครงสร้างข้อมูลที่ใช้ร่วมกัน แต่คุณไม่เคย "โดยตรง" เริ่มหัวข้อใน CPU ที่แตกต่างกัน คุณบอกระบบปฏิบัติการที่คุณต้องการให้มีเธรดใหม่และสร้างบันทึกย่อในโครงสร้างข้อมูลที่ระบบปฏิบัติการบนคอร์อื่นเห็น
Peter Cordes

2
ฉันบอกระบบปฏิบัติการนั้นได้อย่างไร แต่ระบบปฏิบัติการวางรหัสลงบนแกนหลักที่เฉพาะเจาะจงได้อย่างไร
demonguy

4
@demonguy ... (ลดความซับซ้อน) ... แต่ละคอร์ใช้ร่วมกันอิมเมจระบบปฏิบัติการและเริ่มทำงานในตำแหน่งเดียวกัน ดังนั้นสำหรับ 8 คอร์นั่นคือ 8 "กระบวนการฮาร์ดแวร์" ที่ทำงานในเคอร์เนล แต่ละคนเรียกใช้ฟังก์ชันตัวจัดตารางเวลาเดียวกันที่ตรวจสอบตารางกระบวนการสำหรับกระบวนการหรือเธรดที่รันได้ (นั่นคือคิวการทำงาน ) ในขณะเดียวกันโปรแกรมที่มีเธรดจะทำงานโดยไม่มีการรับรู้ถึงลักษณะของ SMP พื้นฐาน พวกเขาเพียงแค่แยก (2) หรืออะไรสักอย่างแล้วปล่อยให้เคอร์เนลรู้ว่าพวกเขาต้องการเรียกใช้ โดยพื้นฐานแล้วแกนประมวลผลจะค้นหากระบวนการแทนการค้นหาแกน
DigitalRoss

1
คุณไม่จำเป็นต้องขัดจังหวะแกนกลางอันใดอันหนึ่งจากอีกแกนหนึ่ง คิดแบบนี้ทุกอย่างที่คุณต้องการในการสื่อสารมาก่อนก็สามารถสื่อสารได้ดีกับกลไกซอฟต์แวร์ กลไกซอฟต์แวร์เดียวกันยังคงทำงานต่อไป ดังนั้นท่อ, การโทรเคอร์เนล, การนอนหลับ / ปลุกทุกอย่างที่ ... พวกเขายังคงทำงานเหมือนเดิม ไม่ใช่ทุกกระบวนการที่ทำงานบน CPU เดียวกัน แต่มีโครงสร้างข้อมูลเดียวกันสำหรับการสื่อสารเหมือนที่เคยมีมา ความพยายามในการดำเนินการ SMP ส่วนใหญ่ถูก จำกัด ให้การล็อกเก่าทำงานในสภาพแวดล้อมแบบขนานมากขึ้น
DigitalRoss

10

หากคุณกำลังเขียนการเพิ่มประสิทธิภาพ VM / คอมไพเลอร์ bytecode สำหรับ CPU แบบมัลติคอร์สิ่งที่คุณจำเป็นต้องรู้โดยเฉพาะเกี่ยวกับการพูด x86 เพื่อให้มันสร้างรหัสที่ทำงานอย่างมีประสิทธิภาพในทุกแกน?

ในฐานะคนที่เขียนการเพิ่มประสิทธิภาพ VM ของคอมไพเลอร์ / bytecode ฉันอาจช่วยคุณได้ที่นี่

คุณไม่จำเป็นต้องรู้อะไรเกี่ยวกับ x86 เพื่อให้มันสร้างโค้ดที่ทำงานได้อย่างมีประสิทธิภาพในทุกคอร์

อย่างไรก็ตามคุณอาจจำเป็นต้องรู้เกี่ยวกับ cmpxchg และเพื่อนเพื่อที่จะเขียนโค้ดที่ทำงานอย่างถูกต้องในทุกคอร์ การเขียนโปรแกรมมัลติคอร์จำเป็นต้องใช้การซิงโครไนซ์และการสื่อสารระหว่างเธรดของการดำเนินการ

คุณอาจจำเป็นต้องรู้อะไรบางอย่างเกี่ยวกับ x86 เพื่อให้มันสร้างโค้ดที่ทำงานได้อย่างมีประสิทธิภาพใน x86 โดยทั่วไป

มีสิ่งอื่น ๆ ที่เป็นประโยชน์สำหรับคุณที่จะเรียนรู้:

คุณควรเรียนรู้เกี่ยวกับสิ่งอำนวยความสะดวกที่ OS (Linux หรือ Windows หรือ OSX) จัดเตรียมไว้เพื่อให้คุณสามารถเรียกใช้หลายเธรด คุณควรเรียนรู้เกี่ยวกับ API การทำให้เป็นคู่ขนานเช่น OpenMP และ Threading Building Blocks หรือ "Grand Leopard" ของ OSX 10.6 "Snow Central"

คุณควรพิจารณาว่าคอมไพเลอร์ของคุณควรเป็นแบบขนานอัตโนมัติหรือถ้าผู้เขียนแอปพลิเคชันที่คอมไพล์โดยคอมไพเลอร์ของคุณจำเป็นต้องเพิ่มไวยากรณ์พิเศษหรือการเรียก API ลงในโปรแกรมของเขาเพื่อใช้ประโยชน์จากหลายแกน


ไม่มีวีเอ็มเอ็มยอดนิยมหลายตัวเช่น. NET และ Java มีปัญหาที่กระบวนการ GC หลักของพวกเขาครอบคลุมอยู่ในการล็อกและการทำเธรดเดี่ยวแบบพื้นฐาน?
Marco van de Voort

9

แต่ละแกนประมวลผลจากพื้นที่หน่วยความจำที่แตกต่างกัน ระบบปฏิบัติการของคุณจะชี้เป็นแกนกลางที่โปรแกรมของคุณและหลักจะดำเนินการโปรแกรมของคุณ โปรแกรมของคุณจะไม่ทราบว่ามีมากกว่าหนึ่งคอร์หรือแกนที่มันกำลังดำเนินการ

นอกจากนี้ยังไม่มีคำแนะนำเพิ่มเติมสำหรับระบบปฏิบัติการเท่านั้น แกนเหล่านี้เหมือนกับชิปแกนเดี่ยว แต่ละคอร์รันส่วนหนึ่งของระบบปฏิบัติการที่จะจัดการการสื่อสารไปยังพื้นที่หน่วยความจำทั่วไปที่ใช้สำหรับการแลกเปลี่ยนข้อมูลเพื่อค้นหาพื้นที่หน่วยความจำถัดไปที่จะดำเนินการ

นี่คือการทำให้เข้าใจง่าย แต่ให้แนวคิดพื้นฐานเกี่ยวกับวิธีการทำ เพิ่มเติมเกี่ยวกับมัลติคอร์และมัลติโปรเซสเซอร์ บน Embedded.com มีข้อมูลมากมายเกี่ยวกับหัวข้อนี้ ... หัวข้อนี้ซับซ้อนอย่างรวดเร็ว!


ฉันคิดว่าควรแยกความแตกต่างให้ดีขึ้นอีกเล็กน้อยที่นี่วิธีการทำงานของมัลติคอร์โดยทั่วไปและอิทธิพลของระบบปฏิบัติการ "แต่ละคอร์ทำการประมวลผลจากหน่วยความจำที่แตกต่างกัน" เป็นการเข้าใจผิดในความคิดของฉัน สิ่งแรกและสำคัญที่สุดการใช้หลายคอร์ในหลักการไม่จำเป็นต้องใช้และคุณสามารถเห็นได้อย่างง่ายดายว่าสำหรับโปรแกรมที่มีเธรดที่คุณต้องการสองคอร์สองทำงานในข้อความและส่วนข้อมูลเดียวกัน (ในขณะที่แต่ละคอร์ต้องการทรัพยากรแต่ละรายการเช่นสแต็ค) .
Volker Stolz

@ShiDoiSi นั่นคือเหตุผลที่คำตอบของฉันมีข้อความ"นี่คือความเรียบง่ายเป็น"
แกร์ฮาร์ด

5

รหัสแอสเซมบลีจะแปลเป็นรหัสเครื่องที่จะดำเนินการในหนึ่งคอร์ หากคุณต้องการให้มีหลายเธรดคุณจะต้องใช้ระบบปฏิบัติการพื้นฐานเพื่อเริ่มรหัสนี้ในโปรเซสเซอร์ที่แตกต่างกันหลายครั้งหรือรหัสที่แตกต่างกันในแกนที่แตกต่างกัน - แต่ละแกนจะรันเธรดแยกต่างหาก แต่ละเธรดจะเห็นแกนเดียวที่กำลังดำเนินการอยู่


4
ฉันกำลังจะพูดบางอย่างเช่นนี้ แต่แล้วระบบปฏิบัติการจะจัดสรรเธรดให้กับแกนประมวลผลอย่างไร ฉันคิดว่ามีคำแนะนำในการประกอบพิเศษที่จะทำให้สิ่งนี้สำเร็จ ถ้าเป็นเช่นนั้นฉันคิดว่านั่นเป็นคำตอบที่ผู้เขียนกำลังมองหา
A. Levy

ไม่มีคำสั่งสำหรับสิ่งนั้นนั่นเป็นหน้าที่ของตัวกำหนดตารางเวลาระบบปฏิบัติการ มีฟังก์ชั่นระบบปฏิบัติการเช่น SetThreadAffinityMask ใน Win32 และรหัสสามารถเรียกพวกเขาได้ แต่มันเป็นสิ่งที่ระบบปฏิบัติการและมีผลต่อตัวกำหนดเวลาไม่ใช่คำสั่งตัวประมวลผล
sharptooth

2
ต้องมี OpCode ไม่เช่นนั้นระบบปฏิบัติการจะไม่สามารถทำได้
Matthew Whited

1
ไม่ใช่ opcode สำหรับการตั้งเวลาจริงๆ - มันเหมือนกับว่าคุณได้รับหนึ่งสำเนาของระบบปฏิบัติการต่อโปรเซสเซอร์แชร์พื้นที่หน่วยความจำ เมื่อใดก็ตามที่คอร์เข้าสู่เคอร์เนลอีกครั้ง (syscall หรืออินเตอร์รัปต์) มันจะดูโครงสร้างข้อมูลเดียวกันในหน่วยความจำเพื่อตัดสินใจว่าเธรดใดที่จะทำงานถัดไป
pjc50

1
@ A.Levy: เมื่อคุณเริ่มหัวข้อที่มีความสัมพันธ์ที่มีเพียงปล่อยให้มันทำงานบนหลักที่แตกต่างกันก็ไม่ได้ทันทีย้ายไปที่หลักอื่น ๆ มีการบันทึกบริบทลงในหน่วยความจำเช่นเดียวกับการสลับบริบทปกติ เธรดฮาร์ดแวร์อื่น ๆ จะเห็นรายการในโครงสร้างข้อมูลตัวกำหนดตารางเวลาและหนึ่งในนั้นจะตัดสินใจว่าจะรันเธรดในที่สุด ดังนั้นจากมุมมองของแกนหลักแรก: คุณเขียนไปยังโครงสร้างข้อมูลที่ใช้ร่วมกันและในที่สุดรหัสระบบปฏิบัติการบนคอร์อื่น (เธรดฮาร์ดแวร์) จะสังเกตเห็นมันและเรียกใช้มัน
Peter Cordes

3

มันไม่ได้ทำในคำแนะนำเครื่องเลย; คอร์แสร้งทำเป็นซีพียูที่แตกต่างและไม่มีความสามารถพิเศษใด ๆ สำหรับการพูดคุยกัน มีสองวิธีในการสื่อสาร:

  • พวกเขาแบ่งปันพื้นที่ที่อยู่ทางกายภาพ ฮาร์ดแวร์จัดการการเชื่อมโยงกันของแคชดังนั้นหนึ่ง CPU เขียนไปยังที่อยู่หน่วยความจำซึ่งคนอื่นอ่าน

  • พวกเขาแบ่งปัน APIC (คอนโทรลเลอร์ขัดจังหวะแบบตั้งโปรแกรมได้) นี่คือหน่วยความจำที่แมปลงในพื้นที่ที่อยู่จริงและสามารถใช้โดยโปรเซสเซอร์หนึ่งเพื่อควบคุมอื่น ๆ เปิดหรือปิดส่งขัดจังหวะ ฯลฯ

http://www.cheesecake.org/sac/smp.htmlเป็นการอ้างอิงที่ดีกับ url โง่


2
อันที่จริงพวกเขาไม่แบ่งปัน APIC โลจิคัล CPU แต่ละตัวมีหนึ่งตัว APICs สื่อสารระหว่างกัน แต่มันแยกกัน
นาธาน Fellman

พวกมันซิงโครไนซ์ (แทนที่จะสื่อสาร) ด้วยวิธีพื้นฐานเดียวและนั่นคือผ่านคำนำหน้า LOCK (คำสั่ง "xchg mem, reg" มีการร้องขอการล็อคโดยปริยาย) ซึ่งวิ่งไปที่หมุดล็อคซึ่งวิ่งไปที่รถบัสทุกคัน (อันที่จริงอุปกรณ์รถบัสต้นแบบ) ต้องการเข้าถึงรถบัสพิเศษ ในที่สุดสัญญาณจะกลับไปที่พิน LOCKA (ตอบรับ) ที่บอกซีพียูว่าตอนนี้มีการเข้าถึงบัสแบบเอกสิทธิ์ เนื่องจากอุปกรณ์ภายนอกช้ากว่าการทำงานภายในของ CPU ลำดับ LOCK / LOCKA อาจต้องใช้ CPU หลายร้อยรอบ
Olof Forshell

1

ความแตกต่างที่สำคัญระหว่างแอปพลิเคชันแบบเดี่ยวและแบบมัลติเธรดคือแบบก่อนหน้ามีหนึ่งสแต็กและแบบหลังมีหนึ่งแบบสำหรับแต่ละหัวข้อ รหัสถูกสร้างขึ้นแตกต่างกันบ้างเนื่องจากคอมไพเลอร์จะถือว่าข้อมูลและการลงทะเบียนเซ็กเมนต์สแต็ก (ds และ ss) ไม่เท่ากัน ซึ่งหมายความว่าการอ้อมผ่าน ebp และ esp ลงทะเบียนว่าค่าเริ่มต้นของการลงทะเบียน ss จะไม่เริ่มต้นที่ ds (เพราะ ds! = ss) ในทางกลับกันการอ้อมผ่านรีจิสเตอร์อื่นซึ่งค่าดีฟอลต์เป็น ds จะไม่เป็นค่าดีฟอลต์เป็น ss

เธรดจะแบ่งปันทุกอย่างรวมถึงพื้นที่ข้อมูลและรหัส พวกเขายังแชร์รูทีน lib เพื่อให้แน่ใจว่าพวกเขาปลอดภัยต่อเธรด โพรซีเดอร์ที่เรียงลำดับพื้นที่ใน RAM สามารถเป็นแบบมัลติเธรดเพื่อเร่งความเร็วของสิ่งต่างๆ จากนั้นเธรดจะสามารถเข้าถึงการเปรียบเทียบและสั่งซื้อข้อมูลในพื้นที่หน่วยความจำกายภาพเดียวกันและดำเนินการรหัสเดียวกัน แต่ใช้ตัวแปรท้องถิ่นที่แตกต่างกันเพื่อควบคุมส่วนที่เกี่ยวข้องของการเรียงลำดับ หลักสูตรนี้เป็นเพราะเธรดมีสแต็กต่างกันที่มีตัวแปรโลคัลอยู่ การเขียนโปรแกรมประเภทนี้ต้องการการปรับแต่งโค้ดอย่างระมัดระวังเพื่อลดการชนกันของข้อมูลระหว่างคอร์ (ในแคชและ RAM) ซึ่งจะส่งผลให้โค้ดที่เร็วขึ้นด้วยเธรดอย่างน้อยสองเธรดมากกว่าหนึ่งรายการ แน่นอนว่าโค้ดที่ไม่ได้รับการปรับแต่งนั้นมักจะเร็วกว่าเมื่อใช้ตัวประมวลผลเดียวมากกว่าตัวประมวลผลสองตัวหรือมากกว่า การดีบักนั้นท้าทายกว่าเนื่องจากจุดพัก "int 3" มาตรฐานจะไม่สามารถใช้งานได้เนื่องจากคุณต้องการขัดจังหวะเธรดที่ระบุและไม่ใช่ทั้งหมด จุดพักการดีบักการลงทะเบียนไม่สามารถแก้ปัญหานี้ได้เว้นแต่คุณจะสามารถตั้งค่าบนตัวประมวลผลเฉพาะที่เรียกใช้งานเธรดเฉพาะที่คุณต้องการขัดจังหวะ

รหัสแบบมัลติเธรดอื่นอาจเกี่ยวข้องกับเธรดที่แตกต่างกันซึ่งทำงานในส่วนต่าง ๆ ของโปรแกรม การเขียนโปรแกรมประเภทนี้ไม่ต้องการการปรับแต่งแบบเดียวกันและง่ายต่อการเรียนรู้


0

สิ่งที่ถูกเพิ่มในสถาปัตยกรรมที่มีความสามารถในการประมวลผลหลายตัวเทียบกับชุดประมวลผลเดี่ยวที่มาก่อนหน้านี้เป็นคำแนะนำในการซิงโครไนซ์ระหว่างคอร์ นอกจากนี้คุณมีคำแนะนำในการจัดการกับความสอดคล้องกันของแคชการล้างบัฟเฟอร์และการดำเนินงานระดับต่ำที่คล้ายกันซึ่งระบบปฏิบัติการต้องรับมือด้วย ในกรณีของสถาปัตยกรรมแบบมัลติเธรดพร้อมกันเช่น IBM POWER6, IBM Cell, Sun Niagara และ Intel "Hyperthreading" คุณมีแนวโน้มที่จะเห็นคำแนะนำใหม่เพื่อจัดลำดับความสำคัญระหว่างเธรด (เช่นการตั้งค่าลำดับความสำคัญและให้ผลผลิตโปรเซสเซอร์อย่างชัดเจนเมื่อไม่มีอะไรทำ) .

แต่ความหมายของเธรดเดี่ยวพื้นฐานนั้นเหมือนกันคุณเพียงเพิ่มสิ่งอำนวยความสะดวกเพิ่มเติมเพื่อจัดการการซิงโครไนซ์และการสื่อสารกับคอร์อื่น

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.