इंटेल x86 न्यूनतम रननीय नंगेमेट उदाहरण
सभी आवश्यक बॉयलरप्लेट के साथ चलने योग्य नंगे धातु का उदाहरण । सभी प्रमुख भाग नीचे दिए गए हैं।
Ubuntu 15.10 QEMU 2.3.0 और लेनोवो थिंकपैड T400 वास्तविक हार्डवेयर अतिथि पर परीक्षण किया गया ।
इंटेल मैनुअल माप 3 सिस्टम प्रोग्रामिंग गाइड - 325384-056US सितंबर 2015 अध्याय 8, 9 और 10 में शामिल हैं SMP।
तालिका 8-1। "ब्रॉडकास्ट INIT-SIPI-SIPI सीक्वेंस एंड चॉइस ऑफ टाइमआउट्स" में एक उदाहरण है जो मूल रूप से सिर्फ काम करता है:
MOV ESI, ICR_LOW ; Load address of ICR low dword into ESI.
MOV EAX, 000C4500H ; Load ICR encoding for broadcast INIT IPI
; to all APs into EAX.
MOV [ESI], EAX ; Broadcast INIT IPI to all APs
; 10-millisecond delay loop.
MOV EAX, 000C46XXH ; Load ICR encoding for broadcast SIPI IP
; to all APs into EAX, where xx is the vector computed in step 10.
MOV [ESI], EAX ; Broadcast SIPI IPI to all APs
; 200-microsecond delay loop
MOV [ESI], EAX ; Broadcast second SIPI IPI to all APs
; Waits for the timer interrupt until the timer expires
उस कोड पर:
अधिकांश ऑपरेटिंग सिस्टम रिंग 3 (उपयोगकर्ता कार्यक्रमों) से उन अधिकांश कार्यों को असंभव बना देंगे।
तो आपको इसके साथ स्वतंत्र रूप से खेलने के लिए अपना कर्नेल लिखने की आवश्यकता है: एक यूजरलैंड लिनक्स प्रोग्राम काम नहीं करेगा।
सबसे पहले, एक एकल प्रोसेसर चलता है, जिसे बूटस्ट्रैप प्रोसेसर (बीएसपी) कहा जाता है।
इसे इंटर प्रोसेसर्स इंटरप्ट (आईपीआई) नामक विशेष इंटरप्ट के माध्यम से अन्य लोगों (एप्लीकेशन प्रोसेसर (एपी) कहा जाता है) को जगाना चाहिए ।
इंटरप्ट कमांड रजिस्टर (ICR) के माध्यम से एडवांस प्रोग्रामेबल इंटरप्टेबल कंट्रोलर (APIC) की प्रोग्रामिंग करके उन रुकावटों को दूर किया जा सकता है।
ICR का प्रारूप प्रलेखित है: 10.6 "ISSUING INTERPROCESSOR INTERRUPTS"
जैसे ही हम ICR को लिखते हैं, IPI होता है।
ICR_LOW को 8.4.4 "एमपी इनिशियलाइज़ेशन उदाहरण" के रूप में परिभाषित किया गया है:
ICR_LOW EQU 0FEE00300H
मैजिक वैल्यू 0FEE00300
ICR का मेमोरी एड्रेस है, जैसा कि टेबल 10-1 "स्थानीय एपीआईसी रजिस्टर एड्रेस मैप" में प्रलेखित है।
उदाहरण में सबसे सरल संभव विधि का उपयोग किया जाता है: यह प्रसारण आईपीआई भेजने के लिए आईसीआर सेट करता है जो वर्तमान एक को छोड़कर अन्य सभी प्रोसेसर को दिया जाता है।
लेकिन यह भी संभव है, और कुछ के द्वारा अनुशंसित किया गया है , विशेष डेटा संरचनाओं के माध्यम से प्रोसेसर के बारे में जानकारी प्राप्त करने के लिए BIOS द्वारा एसीपीआई टेबल या इंटेल की एमपी कॉन्फ़िगरेशन तालिका जैसे सेटअप और केवल उन लोगों को जगाएं जिनकी आपको एक-एक जरूरत है।
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
लिंकर स्क्रिप्ट का उपयोग करना एक और संभावना है।
देरी छोरों काम करने के लिए एक कष्टप्रद हिस्सा हैं: ऐसी नींदों को ठीक से करने के लिए कोई सुपर सरल तरीका नहीं है।
संभावित तरीकों में शामिल हैं:
- PIT (मेरे उदाहरण में प्रयुक्त)
- HPET
- ऊपर के साथ एक व्यस्त लूप के समय को कैलिब्रेट करें, और इसके बजाय इसका उपयोग करें
संबंधित: स्क्रीन पर एक संख्या कैसे प्रदर्शित करें और डॉस x86 विधानसभा के साथ एक सेकंड के लिए सोएं?
मुझे लगता है कि प्रारंभिक प्रोसेसर को इसके लिए संरक्षित मोड में होना चाहिए क्योंकि हम पते पर लिखते हैं 0FEE00300H
जो 16-बिट्स के लिए बहुत अधिक है
प्रोसेसर के बीच संवाद करने के लिए, हम मुख्य प्रक्रिया पर एक स्पिनलॉक का उपयोग कर सकते हैं, और दूसरे कोर से लॉक को संशोधित कर सकते हैं।
हमें यह सुनिश्चित करना चाहिए कि मेमोरी राइट बैक किया गया है, उदाहरण के लिए wbinvd
।
प्रोसेसर के बीच साझा स्थिति
8.7.1 "लॉजिकल प्रोसेसर्स का राज्य" कहता है:
निम्नलिखित विशेषताएं इंटेल 64 या IA-32 प्रोसेसर के भीतर तार्किक हाइपर-थ्रेडिंग तकनीक का समर्थन करने वाले तार्किक प्रोसेसर की वास्तु स्थिति का हिस्सा हैं। सुविधाओं को तीन समूहों में विभाजित किया जा सकता है:
- प्रत्येक तार्किक प्रोसेसर के लिए डुप्लिकेट
- एक भौतिक प्रोसेसर में तार्किक प्रोसेसर द्वारा साझा किया गया
- कार्यान्वयन के आधार पर साझा या डुप्लिकेट किया गया
प्रत्येक तार्किक प्रोसेसर के लिए निम्नलिखित विशेषताएं दोहराई गई हैं:
- सामान्य प्रयोजन रजिस्टर (EAX, EBX, ECX, EDX, ESI, EDI, ESP और EBP)
- सेगमेंट रजिस्टर (CS, DS, SS, ES, FS और GS)
- EFLAGS और EIP रजिस्टर। ध्यान दें कि CS और EIP / RIP प्रत्येक लॉजिकल प्रोसेसर के लिए रजिस्टर करता है जो कि थ्रेड के लिए निर्देश स्ट्रीम को तार्किक प्रोसेसर द्वारा निष्पादित करता है।
- x87 FPU रजिस्टर (ST0 ST7 के माध्यम से, स्थिति शब्द, नियंत्रण शब्द, टैग शब्द, डेटा ऑपरेटर सूचक और अन्य सूचक)
- MMX रजिस्टर (MM7 MM7 के माध्यम से)
- एक्सएमएम रजिस्टर (एक्सएमएम 7 के माध्यम से एक्सएमएम 0) और एमएक्ससीएसआर रजिस्टर
- नियंत्रण रजिस्टर और सिस्टम टेबल पॉइंटर रजिस्टर (GDTR, LDTR, IDTR, कार्य रजिस्टर)
- डीबग रजिस्टर (DR0, DR1, DR2, DR3, DR6, DR7) और डीबग नियंत्रण MSRs
- मशीन की जाँच वैश्विक स्थिति (IA32_MCG_STATUS) और मशीन जाँच क्षमता (IA32_MCG_CAP) MSRs
- थर्मल घड़ी मॉड्यूलेशन और एसीपीआई पावर प्रबंधन MSRs को नियंत्रित करते हैं
- समय टिकट काउंटर MSRs
- पृष्ठ विशेषता तालिका (PAT) सहित अन्य MSR रजिस्टरों में से अधिकांश। नीचे अपवाद देखें।
- स्थानीय APIC रजिस्टर।
- अतिरिक्त सामान्य प्रयोजन रजिस्टर (R8-R15), एक्सएमएम रजिस्टर (XMM8-XMM15), इंटेल 64 प्रोसेसर पर नियंत्रण रजिस्टर, IA32_EFER।
निम्नलिखित विशेषताएं तार्किक प्रोसेसर द्वारा साझा की जाती हैं:
- मेमोरी प्रकार श्रेणी रजिस्टर (MTRRs)
क्या निम्नलिखित विशेषताएं साझा या दोहराई गई हैं, क्या कार्यान्वयन-विशिष्ट हैं:
- IA32_MISC_ENABLE MSR (MSR पता 1A0H)
- मशीन की जाँच वास्तुकला (MCA) MSRs (IA32_MCG_STATUS और IA32_MCG_CAP MSRs को छोड़कर)
- प्रदर्शन की निगरानी नियंत्रण और एमएसआर का मुकाबला
कैश शेयरिंग पर चर्चा की जाती है:
इंटेल हाइपरथ्रेड्स में अलग-अलग कोर की तुलना में अधिक कैश और पाइपलाइन साझाकरण है: /superuser/133082/hyper-threading-and-dual-core-whats-the-difference/99585899995858
लिनक्स कर्नेल 4.2
मुख्य प्रारंभिक कार्रवाई पर लगता है arch/x86/kernel/smpboot.c
।
एआरएम न्यूनतम रननीय नंगेमेट उदाहरण
यहाँ मैं QEMU के लिए एक कम से कम रनवेबल ARMv8 अराजकता 64 उदाहरण प्रदान करता हूँ:
.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
गिटहब ऊपर ।
इकट्ठा करें और चलाएं:
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 \
;
इस उदाहरण में, हम सीपीयू 0 को एक स्पिनलॉक लूप में रखते हैं, और यह केवल सीपीयू 1 के साथ बाहर निकलता है।
स्पिनलॉक के बाद, सीपीयू 0 फिर एक सेमीहोस्ट एग्जिट कॉल करता है जो QEMU को छोड़ देता है।
यदि आप QEMU को केवल एक सीपीयू के साथ शुरू करते हैं -smp 1
, तो सिमुलेशन सिर्फ स्पिनलॉक पर हमेशा के लिए लटका रहता है।
CPU 1 को PSCI इंटरफ़ेस के साथ जगाया गया है, ARM पर अधिक विवरण : Start / Wakeup / अन्य सीपीयू कोर / APs को लाएं और निष्पादन प्रारंभ पता पास करें?
नदी के ऊपर संस्करण भी इसलिए आप प्रदर्शन विशेषताओं के साथ साथ ही प्रयोग कर सकते हैं, यह gem5 पर काम करने के लिए कुछ बदलाव किया है।
मैंने इसे वास्तविक हार्डवेयर पर परीक्षण नहीं किया है, इसलिए मुझे यकीन नहीं है कि यह कितना पोर्टेबल है। निम्नलिखित रास्पबेरी पाई ग्रंथ सूची ब्याज की हो सकती है:
यह दस्तावेज़ एआरएम सिंक्रोनाइज़ेशन प्राइमेटिव्स का उपयोग करने पर कुछ मार्गदर्शन प्रदान करता है जिसका उपयोग आप कई कोर के साथ मज़ेदार चीजें करने के लिए कर सकते हैं: http://infocenter.arm.com/help/topic/com.arm.doc.dht0008a/DHT0008AA-arm_synchronization_primatics.pdf
उबंटू 18.10, जीसीसी 8.2.0, बिनुटिल्स 2.31.1, क्यूईएमयू 2.12.0 पर परीक्षण किया गया।
अधिक सुविधाजनक प्रोग्रामबिलिटी के लिए अगला कदम
पिछले उदाहरण माध्यमिक सीपीयू को जागृत करते हैं और समर्पित निर्देशों के साथ बुनियादी मेमोरी सिंक्रनाइज़ेशन करते हैं, जो एक अच्छी शुरुआत है।
लेकिन मल्टीकोर सिस्टम को प्रोग्राम में आसान बनाने के लिए, जैसे POSIX की तरह pthreads
, आपको निम्नलिखित अधिक शामिल विषयों में भी जाना होगा:
सेटअप बाधित होता है और एक टाइमर चलता है जो समय-समय पर तय करता है कि कौन सा धागा अब चलेगा। इसे प्रीमिटिव मल्टीथ्रेडिंग के रूप में जाना जाता है ।
इस तरह की प्रणाली को थ्रेड रजिस्टरों को बचाने और पुनर्स्थापित करने की भी आवश्यकता है क्योंकि वे शुरू और बंद हो जाते हैं।
गैर-प्रीमेप्टिव मल्टीटास्किंग सिस्टम होना भी संभव है, लेकिन उन लोगों को आपको अपने कोड को संशोधित करने की आवश्यकता हो सकती है ताकि हर थ्रेड उपज (जैसे pthread_yield
कार्यान्वयन के साथ ) हो, और वर्कलोड को संतुलित करना कठिन हो जाए।
यहाँ कुछ सरल नंगे धातु टाइमर उदाहरण हैं:
स्मृति संघर्ष के साथ सौदा। विशेष रूप से, यदि आप C या अन्य उच्च स्तरीय भाषाओं में कोड करना चाहते हैं , तो प्रत्येक थ्रेड को एक विशिष्ट स्टैक की आवश्यकता होगी ।
आप केवल एक अधिकतम अधिकतम स्टैक आकार के लिए थ्रेड्स को सीमित कर सकते हैं, लेकिन इससे निपटने का अच्छा तरीका पेजिंग के साथ है जो कुशल "असीमित आकार" के ढेर की अनुमति देता है।
यहाँ एक भोली अराजकता है 64 नंगेतर उदाहरण है कि अगर ढेर बहुत गहरा हो जाता है तो उड़ा देगा
लिनक्स कर्नेल या कुछ अन्य ऑपरेटिंग सिस्टम का उपयोग करने के कुछ अच्छे कारण हैं :-)
उपयोगकर्ताभूमि मेमोरी सिंक्रनाइज़ेशन प्राइमेटीज़
हालाँकि थ्रेड स्टार्ट / स्टॉप / मैनेजमेंट आमतौर पर उपयोगकर्ता दायरे से परे होता है, लेकिन आप संभावित रूप से अधिक महंगी सिस्टम कॉल के बिना मेमोरी एक्सेस को सिंक्रोनाइज़ करने के लिए यूजरलैंड थ्रेड्स से असेंबली निर्देशों का उपयोग कर सकते हैं।
आपको निश्चित रूप से पुस्तकालयों का उपयोग करना पसंद करना चाहिए जो इन निम्न स्तर की प्राथमिकताओं को आंशिक रूप से लपेटते हैं। सी ++ मानक ने स्वयं <mutex>
और <atomic>
हेडर पर और विशेष रूप से महान प्रगति की है std::memory_order
। मुझे यकीन नहीं है कि यह सभी संभव स्मृति शब्दार्थों को प्राप्त करता है, लेकिन यह सिर्फ हो सकता है।
अधिक सूक्ष्म शब्दार्थ विशेष रूप से लॉक मुक्त डेटा संरचनाओं के संदर्भ में प्रासंगिक हैं , जो कुछ मामलों में प्रदर्शन लाभ प्रदान कर सकते हैं। उन्हें लागू करने के लिए, आपको विभिन्न प्रकार के मेमोरी अवरोधों के बारे में थोड़ा सीखना होगा: https://preshing.com/20120710/memory-barriers-are-like-source-control-operations/
उदाहरण के लिए बूस्ट में कुछ लॉक फ्री कंटेनर कार्यान्वयन हैं: https://www.boost.org/doc/libs/1_63_0/doc/html/lockfree.html
लिनक्स futex
सिस्टम कॉल को लागू करने के लिए इस तरह के यूजरलैंड निर्देशों का भी उपयोग किया जाता है , जो लिनक्स में मुख्य सिंक्रोनाइज़ेशन प्राइमेटिव्स में से एक है। man futex
4.15 पढ़ता है:
फ़ुटेक्स () सिस्टम कॉल प्रतीक्षा के लिए एक विधि प्रदान करता है जब तक कि एक निश्चित स्थिति सच नहीं हो जाती। यह आमतौर पर साझा-मेमोरी सिंक्रनाइज़ेशन के संदर्भ में एक अवरुद्ध निर्माण के रूप में उपयोग किया जाता है। फ़ुटेक्स का उपयोग करते समय, अधिकांश सिंक्रनाइज़ेशन ऑपरेशन उपयोगकर्ता स्थान में किए जाते हैं। एक उपयोगकर्ता-स्पेस प्रोग्राम फ़ुटेक्स () सिस्टम कॉल को केवल तब ही नियोजित करता है जब यह संभावना होती है कि जब तक स्थिति सही न हो जाए, तब तक प्रोग्राम को अधिक समय तक ब्लॉक करना पड़ता है। अन्य फ़ुटेक्स () संचालन का उपयोग किसी भी प्रक्रिया या किसी विशेष स्थिति की प्रतीक्षा कर रहे थ्रेड्स को जगाने के लिए किया जा सकता है।
Syscall नाम का अर्थ है "फास्ट यूजरस्पेस XXX"।
यहाँ इनलाइन असेंबली के साथ एक न्यूनतम बेकार C ++ x86_64 / anarch64 उदाहरण है जो ऐसे निर्देशों के मूल उपयोग को अधिकांशतः मज़ेदार बनाता है:
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
}
गिटहब ऊपर ।
संभावित उत्पादन:
my_non_atomic_ulong 15264
my_arch_non_atomic_ulong 15267
इससे हम देखते हैं कि x86 LOCK उपसर्ग / anarch64 LDADD
निर्देश ने अतिरिक्त परमाणु बना दिया है: इसके बिना हमारे पास कई जोड़ पर दौड़ की स्थिति है, और अंत में कुल गणना सिंक्रनाइज़ 20000 से कम है।
यह सभी देखें:
Ubuntu 19.04 amd64 में और QEMU अराजकता 64 उपयोगकर्ता मोड के साथ परीक्षण किया गया।