マルチコアアセンブリ言語はどのように見えますか?


243

昔々、たとえばx86アセンブラを作成するには、「EDXレジスタに値5をロードする」、「EDXレジスタをインクリメントする」などの指示があります。

4コア(またはそれ以上)を備えた最新のCPUでは、マシンコードレベルで4つの別個のCPUがあるように見えますか(つまり、4つの異なる「EDX」レジスタしかない)。もしそうなら、「EDXレジスタをインクリメントする」と言うとき、どのCPUのEDXレジスタがインクリメントされるかを決定するものは何ですか?現在、x86アセンブラに「CPUコンテキスト」または「スレッド」の概念はありますか?

コア間の通信/同期はどのように機能しますか?

オペレーティングシステムを作成している場合、ハードウェアを介して公開されているメカニズムによって、さまざまなコアでの実行をスケジュールできますか?特別な特権付きの指示ですか?

マルチコアCPU用の最適化コンパイラ/バイトコードVMを作成している場合、すべてのコアで効率的に実行されるコードを生成するために、たとえばx86について特に何を知る必要がありますか?

マルチコア機能をサポートするためにx86マシンコードにどのような変更が加えられましたか?


2
:ここでは(同一ではないが)同様の質問がありますstackoverflow.com/questions/714905/...
ネイサンFellman

回答:


153

これは質問に対する直接の回答ではありませんが、コメントに表示される質問に対する回答です。基本的に、問題はハードウェアがマルチスレッド操作にどのようなサポートを提供するかです。

ニコラス・フリントは、少なくともx86に関してはそれを正しくしていました。マルチスレッド環境(ハイパースレッディング、マルチコアまたはマルチプロセッサ)では、ブートストラップスレッド(通常はプロセッサ0のコア0のスレッド0)がアドレスからコードのフェッチを開始します0xfffffff0。他のすべてのスレッドは、SIPIと呼ばれる特別なスリープ状態で起動します。初期化の一部として、プライマリスレッドは、WFSにある各スレッドにSIPI(スタートアップIPI)と呼ばれるAPICを介して特別なプロセッサ間割り込み(IPI)を送信します。SIPIには、そのスレッドがコードのフェッチを開始するアドレスが含まれています。

このメカニズムにより、各スレッドは異なるアドレスからコードを実行できます。必要なのは、各スレッドが独自のテーブルとメッセージングキューをセットアップするためのソフトウェアサポートです。OSはこれらを使用して、実際のマルチスレッドスケジューリングを行います。

実際のアセンブリに関しては、Nicholasが書いたように、シングルスレッドアプリケーションとマルチスレッドアプリケーションのアセンブリに違いはありません。各論理スレッドには独自のレジスタセットがあるため、次のように記述します。

mov edx, 0

現在実行中のスレッドEDXに対してのみ更新されますEDX単一のアセンブリ命令を使用して別のプロセッサで変更する方法はありません。独自のスレッドを更新するコードを実行するように別のスレッドに指示するようOSに要求するには、何らかのシステムコールが必要EDXです。


2
ニコラスの答えのギャップを埋めてくれてありがとう。今、あなたの回答を承認済みの回答としてマークしました。...私が興味を持った具体的な詳細を示します...あなたの情報とニコラスのすべてを組み合わせた単一の回答があった方がよいでしょう。
ポールホリングスワース

3
これは、スレッドがどこから来たのかという質問には答えません。コアとプロセッサはハードウェアのものですが、どういうわけかソフトウェアでスレッドを作成する必要があります。プライマリスレッドはSIPIの送信先をどのようにして知るのですか?または、SIPI自体が新しいスレッドを作成しますか?
リッチremer

7
@richremer:ハードウェアスレッドとソフトウェアスレッドを混同しているようです。HWスレッドは常に存在します。時々それは眠っています。SIPI自体がHWスレッドを起動し、SWを実行できるようにします。実行するHWスレッド、および各HWスレッドで実行するプロセスとSWスレッドを決定するのは、OSとBIOSです。
Nathan Fellman、2014

2
ここにはたくさんの良い簡潔な情報がありますが、これは大きなトピックなので、質問が残るかもしれません。USBドライブまたは「フロッピー」ディスクから起動する実際の完全な「ベアボーン」カーネルの例がいくつかあります。これは、マルチスレッドCコードを実際に実行できる古いTSS記述子(github。 com / duanev / oz-x86-32-asm-003)が、標準ライブラリのサポートはありません。あなたが求めたよりもかなり多いですが、それはおそらくそれらの残っている質問のいくつかに答えることができます。
duanev 2018

87

Intel x86最小実行可能ベアメタルの例

必要なすべてのボイラープレートを備えた実行可能なベアメタルの例。主要な部分はすべて以下でカバーされています。

Ubuntu 15.10 QEMU 2.3.0およびLenovo ThinkPad T400の実際のハードウェアゲストでテスト済み。

325384-056US 2015年9月-インテル・マニュアル第3巻のシステム・プログラミング・ガイドの章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

そのコードについて:

  1. ほとんどのオペレーティングシステムでは、リング3(ユーザープログラム)からこれらの操作のほとんどを実行できなくなります。

    そのため、自由に遊ぶには独自のカーネルを作成する必要があります。ユーザーランドのLinuxプログラムは機能しません。

  2. 最初は、ブートストラッププロセッサ(BSP)と呼ばれる単一のプロセッサが実行されます。

    プロセッサ間割り込み(IPI)と呼ばれる特別な割り込みを通じて、他の割り込み(アプリケーションプロセッサ(AP)と呼ばれる)を起動する必要があります。

    これらの割り込みは、割り込みコマンドレジスタ(ICR)を介してAdvanced Programmable Interrupt Controller(APIC)をプログラミングすることで実行できます。

    ICRのフォーマットは次の場所に記載されています:10.6 "ISSUING INTERPROCESSOR INTERRUPTS"

    ICRに書き込むとすぐにIPIが発生します。

  3. ICR_LOWは、8.4.4「MP初期化の例」で次のように定義されています。

    ICR_LOW EQU 0FEE00300H
    

    0FEE00300表10-1「ローカルAPICレジスタアドレスマップ」に記載されているように、マジック値はICRのメモリアドレスです。

  4. この例では、最も簡単な方法が使用されています。これは、現在のIPプロセッサを除く他のすべてのプロセッサに配信されるブロードキャストIPIを送信するようにICRを設定します。

    ただし、ACPIテーブルやIntelのMP構成テーブルなど BIOSによってセットアップされた特別なデータ構造を介してプロセッサに関する情報を取得し、必要なものだけをウェイクアップすることも可能であり、一部では推奨されています。

  5. XXin 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
    

    リンカースクリプトを使用することもできます。

  6. 遅延ループは機能するのに面倒な部分です。そのようなスリープを正確に行うための超単純な方法はありません。

    可能な方法は次のとおりです。

    • PIT(私の例で使用)
    • HPET
    • 上記でビジーループの時間を較正し、代わりにそれを使用します

    関連:画面に数値を表示し、DOS x86アセンブリで1秒間スリープする方法

  7. 0FEE00300H16ビットに対して高すぎるアドレスに書き込むときに、これが機能するには、初期プロセッサがプロテクトモードである必要があると思います。

  8. プロセッサ間で通信するには、メインプロセスでスピンロックを使用し、2番目のコアのロックを変更します。

    メモリの書き戻しが確実に行われるようにする必要がありますwbinvd

プロセッサ間の共有状態

8.7.1 "論理プロセッサの状態"は次のように述べています。

以下の機能は、インテルハイパースレッディングテクノロジーをサポートするインテル64またはIA-32プロセッサー内の論理プロセッサーのアーキテクチャー状態の一部です。機能は次の3つのグループに分類できます。

  • 論理プロセッサーごとに複製
  • 物理プロセッサーの論理プロセッサーで共有
  • 実装に応じて共有または複製

次の機能は、各論理プロセッサで複製されます。

  • 汎用レジスター(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)とデバッグ制御MSR
  • マシンチェックグローバルステータス(IA32_MCG_STATUS)およびマシンチェック機能(IA32_MCG_CAP)MSR
  • サーマルクロック変調およびACPI電源管理制御MSR
  • タイムスタンプカウンターMSR
  • ページ属性テーブル(PAT)を含む、他のほとんどのMSRレジスタ。以下の例外を参照してください。
  • ローカルAPICレジスタ。
  • 追加の汎用レジスター(R8-R15)、XMMレジスター(XMM8-XMM15)、制御レジスター、Intel 64プロセッサー上のIA32_EFER。

次の機能は、論理プロセッサによって共有されます。

  • メモリタイプ範囲レジスタ(MTRR)

次の機能を共有するか複製するかは実装固有です。

  • IA32_MISC_ENABLE MSR(MSRアドレス1A0H)
  • マシンチェックアーキテクチャ(MCA)MSR(IA32_MCG_STATUSおよびIA32_MCG_CAP MSRを除く)
  • パフォーマンス監視制御とカウンターMSR

キャッシュ共有については、以下で説明されています。

Intelハイパースレッドは、個別のコアよりもキャッシュとパイプラインの共有が優れています。https//superuser.com/questions/133082/hyper-threading-and-dual-core-whats-the-difference/995858#995858

Linuxカーネル4.2

主な初期化アクションはにあるようarch/x86/kernel/smpboot.cです。

ARM最小実行可能ベアメタルの例

ここでは、QEMUの最小限の実行可能なARMv8 aarch64の例を示します。

.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がスピンロックを解放することでのみ終了します。

スピンロックの後、CPU 0はセミホスト終了呼び出しを行い、QEMUを終了します。

QEMUを1つのCPUだけで起動すると-smp 1、シミュレーションはスピンロックで永久にハングします。

CPU 1はPSCIインターフェースでウェイクアップされます。詳細は、ARM:他のCPUコア/ APを起動/ウェイクアップ/起動して、実行開始アドレスを渡しますか?

上流のバージョンでは、あなたが同様の性能特性を試すことができるようにも、それは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)、次のより複雑なトピックに進む必要があります。

  • 割り込みを設定し、現在実行するスレッドを定期的に決定するタイマーを実行します。これは、プリエンプティブマルチスレッドと呼ばれます。

    このようなシステムは、スレッドレジスタが開始および停止されるときに、それらを保存および復元する必要もあります。

    非プリエンプティブなマルチタスクシステムを使用することも可能ですが、その場合は、コードを変更してすべてのスレッドが(たとえば、pthread_yield実装で)利回りになるようにし、ワークロードのバランスをとるのが難しくなる可能性があります。

    次に、単純なベアメタルタイマーの例をいくつか示します。

  • メモリの競合に対処します。特に、Cまたは他の高水準言語でコーディングする場合は、各スレッドに一意のスタックが必要です。

    スレッドを制限して最大スタックサイズを固定することもできますが、これに対処するためのより良い方法は、効率的な「無制限のサイズ」スタックを可能にするページングを使用することです。

    以下は、スタックが深くなりすぎると爆破する素朴なaarch64ベアメタルの例です

これらは、Linuxカーネルまたは他のオペレーティングシステムを使用するいくつかの正当な理由です:-)

ユーザーランドメモリ同期プリミティブ

スレッドの開始/停止/管理は一般にユーザーランドの範囲を超えていますが、ユーザーランドスレッドからのアセンブリ命令を使用して、より高価なシステムコールを行うことなくメモリアクセスを同期できます。

もちろん、これらの低レベルのプリミティブを移植可能にラップするライブラリーの使用を好むはずです。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にロックフリーのコンテナー実装があります

このようなユーザーランド命令は、Linux futexの主要な同期プリミティブの1つであるLinux システムコールの実装にも使用されているようです。man futex4.15読み取り:

futex()システムコールは、特定の条件がtrueになるまで待機するメソッドを提供します。これは通常、共有メモリ同期のコンテキストでブロッキング構造として使用されます。futexを使用する場合、同期操作の大部分はユーザー空間で実行されます。ユーザー空間プログラムは、条件がtrueになるまでプログラムがより長い時間ブロックしなければならない可能性が高い場合にのみ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プレフィックス/ aarch64 LDADD命令が加算をアトミックにしたことがわかります。これがないと、多くの加算で競合状態が発生し、最後の合計カウントは同期された20000未満になります。

以下も参照してください。

Ubuntu 19.04 amd64およびQEMU aarch64ユーザーモードでテスト済み。


例をコンパイルするためにどのアセンブラを使用していますか?GASはあなたのことを好きではないようです#include(コメントとして受け取ります)、NASM、FASM、YASMはAT&T構文を知らないので、AT&T構文ではないので...何ですか?
ルスラン

@Ruslan gcc#includeCプリプロセッサから来ています。Makefile開始セクションで説明されているように提供されているgithub.com/cirosantilli/x86-bare-metal-examples/blob/…を使用します。それでも機能しない場合は、GitHubの問題を開いてください。
Ciro Santilli郝海东冠状病六四事件法轮功

x86では、キューで実行する準備ができているプロセスがないことをコアが認識した場合はどうなりますか?(アイドル状態のシステムでは時々発生する可能性があります)。新しいタスクがあるまで、コアは共有メモリ構造でスピンロックしますか?(おそらく良いことではありませんが、それは多くの電力を使用します)割り込みがあるまでスリープするためにHLTのようなものを呼び出しますか?(その場合、そのコアをウェイクアップする責任があるのは誰ですか?)
tigrou '10

@tigrouはわかりませんが、特に電源が重要なARMでは、Linux実装が次の(タイマーのような)割り込みまで電源状態になる可能性が非常に高いと思います。Linuxを実行しているシミュレーターの命令トレースで具体的に簡単に観察できるかどうかをすぐに確認します。それは次のようになります。github.com
cheat/tree

1
いくつかの情報(x86 / Windowsに固有)はここにあります(「アイドルスレッド」を参照)。TL; DR:CPUに実行可能なスレッドが存在しない場合、CPUはアイドルスレッドにディスパッチされます。他のいくつかのタスクと一緒に、最終的には登録された電源管理プロセッサのアイドルルーチンを呼び出します(例:IntelなどのCPUベンダーが提供するドライバーを介して)。これにより、消費電力を削減するために、CPUをより深いC状態(例:C0-> C3)に移行する場合があります。
tigrou

43

私が理解しているように、各「コア」は独自のレジスタセットを持つ完全なプロセッサです。基本的に、BIOSは1つのコアを実行して開始し、オペレーティングシステムは他のコアを初期化し、実行するコードをポイントすることで、他のコアを「開始」できます。

同期はOSによって行われます。一般に、各プロセッサはOSに対して異なるプロセスを実行しているため、オペレーティングシステムのマルチスレッド機能は、どのプロセスがどのメモリにアクセスするか、およびメモリの衝突が発生した場合の対処法を決定します。


28
しかし、これは問題を引き起こします:これを行うためにオペレーティングシステムが利用できる命令は何ですか?
ポールホリングスワース

4
そのための一連の特権的な命令がありますが、それはアプリケーションコードではなく、オペレーティングシステムの問題です。アプリケーションコードをマルチスレッド化する場合は、オペレーティングシステム関数を呼び出して「マジック」を実行する必要があります。
シャープトゥース

2
BIOSは通常、使用可能なコアの数を識別し、要求されたときにこの情報をOSに渡します。さまざまなPCのハードウェア仕様(プロセッサ、コア、PCIバス、PCIカード、マウス、キーボード、グラフィックス、ISA、PCI-E / X、メモリなど)にアクセスできるように、BIOS(およびハードウェア)が準拠しなければならない標準があります。 OSの観点からは同じに見えます。BIOSが4つのコアがあることを報告しない場合、OSは通常1つしかないと想定します。実験するBIOS設定もあるかもしれません。
Olof Forshell、2011

1
それはすばらしいですが、ベアメタルプログラムを作成している場合はどうでしょうか。
アレクサンダーライアンバゲット2017年

3
@AlexanderRyanBaggett 、? それは何ですか?繰り返しますが、「OSに任せる」と言うとき、問題はOSがそれをどのように行うのかということなので、問題を回避しています。それはどの組立説明書を使用しますか?
パセリエ2017年

39

非公式SMP FAQ スタックオーバーフローロゴ


昔々x86アセンブラーを作成するには、たとえば、「EDXレジスターに値5をロードする」、「EDXレジスターをインクリメントする」などの指示があります。4コア(またはそれ以上)を備えた最新のCPUの場合、マシンコードレベルでは、4つの別個のCPUがあるように見えますか(つまり、4つの異なる「EDX」レジスタしかない)。

丁度。4つの個別の命令ポインタを含む4セットのレジスタがあります。

もしそうなら、「EDXレジスタをインクリメントする」と言うとき、どのCPUのEDXレジスタがインクリメントされるかを決定するものは何ですか?

当然、その命令を実行したCPU。同じメモリを単に共有している4つのまったく異なるマイクロプロセッサと考えてください。

現在、x86アセンブラに「CPUコンテキスト」または「スレッド」の概念はありますか?

いいえ。アセンブラは、いつものように命令を変換するだけです。変更はありません。

コア間の通信/同期はどのように機能しますか?

それらは同じメモリを共有するので、それはほとんどプログラムロジックの問題です。現在、プロセッサ間割り込みメカニズムがありますが、これは必要ではなく、最初のデュアルCPU x86システムには元々存在していませんでした。

オペレーティングシステムを作成している場合、ハードウェアを介して公開されているメカニズムによって、さまざまなコアでの実行をスケジュールできますか?

スケジューラは実際には変更されませんが、クリティカルセクションと使用されるロックのタイプについて少し注意深くなります。SMPの前に、カーネルコードは最終的にスケジューラを呼び出し、スケジューラは実行キューを調べて、次のスレッドとして実行するプロセスを選択します。(カーネルへのプロセスはスレッドによく似ています。)SMPカーネルはまったく同じコードを一度に1つのスレッドで実行します。2つのコアが誤って選択しないようにするには、クリティカルセクションロックをSMPセーフにする必要があるだけです。同じPID。

特別な特権付きの命令ですか?

いいえ。コアはすべて同じメモリで同じ古い命令で実行されています。

マルチコアCPU用の最適化コンパイラ/バイトコードVMを作成している場合、すべてのコアで効率的に実行されるコードを生成するために、たとえばx86について特に何を知る必要がありますか?

以前と同じコードを実行します。変更が必要なのは、UnixまたはWindowsカーネルです。

私の質問を「マルチコア機能をサポートするためにx86マシンコードにどのような変更が加えられたのか」と要約できます。

何も必要ありませんでした。最初のSMPシステムは、ユニプロセッサとまったく同じ命令セットを使用していました。現在、x86アーキテクチャは大幅に進化しており、処理を高速化するための膨大な数の新しい命令がありますが、SMPには必要ありません。

詳細については、Intel Multiprocessor Specificationを参照してください。


更新:すべてのフォローアップの質問はちょうど完全にすることを受け入れることで答えることができるのn -wayマルチコアCPUがほとんどである1とまったく同じものn個だけで同じメモリを共有する別のプロセッサ。2 質問されていない重要な質問がありました。パフォーマンスを向上させるために、プログラムを複数のコアで実行するように作成するにはどうすればよいですか?そしてその答えは、Pthreadsのようなスレッドライブラリを使用して記述されていることです一部のスレッドライブラリは、OSからは見えない「グリーンスレッド」を使用し、それらは個別のコアを取得しませんが、スレッドライブラリがカーネルスレッド機能を使用している限り、スレッド化されたプログラムは自動的にマルチコアになります。
1.下位互換性のために、リセット時に最初のコアのみが起動し、残りのコアを起動するためにいくつかのドライバータイプの処理を実行する必要があります。
2.当然、すべての周辺機器も共有します。


3
私はいつも「スレッド」はソフトウェアコンセプトだと思っています。マルチコアプロセッサを理解するのが難しくなります。問題は、コードがコアに「コア2で実行されるスレッドを作成する」とどのように伝えることができるかです。それを行うための特別なアセンブリコードはありますか?
demonguy 2015

2
@demonguy:いいえ、そのようなことに対する特別な指示はありません。アフィニティマスクを設定して、OSに特定のコアでスレッドを実行するように依頼します(「このスレッドは、この一連の論理コアで実行できます」)。それは完全にソフトウェアの問題です。各CPUコア(ハードウェアスレッド)は、独立してLinux(またはWindows)を実行しています。他のハードウェアスレッドと連携するには、共有データ構造を使用します。ただし、別のCPUでスレッドを「直接」開始することはありません。新しいスレッドを作成することをOSに指示すると、別のコアのOSが認識するデータ構造にメモが作成されます。
Peter Cordes

2
私はそれをosに伝えることができますが、どのようにosが特定のコアにコードを置くのですか?
demonguy

4
@demonguy ...(簡略化)...各コアはOSイメージを共有し、同じ場所で実行を開始します。つまり、8つのコアの場合、これはカーネルで実行される8つの「ハードウェアプロセス」です。それぞれが、実行可能なプロセスまたはスレッドのプロセステーブルをチェックする同じスケジューラー関数を呼び出します。(それが実行キューです。)一方、スレッドを含むプログラムは、基になるSMPの性質を意識せずに動作します。彼らは単にfork(2)か何かをして、実行したいことをカーネルに知らせます。基本的に、コアは、コアを見つけるプロセスではなく、プロセスを見つけます。
DigitalRoss 2016年

1
あるコアを別のコアに割り込む必要はありません。このように考えてください。以前通信する必要があったすべては、ソフトウェアメカニズムでうまく通信されました。同じソフトウェアメカニズムが機能し続けます。つまり、パイプ、カーネルコール、スリープ/ウェイクアップなど、すべてが以前と同じように機能します。すべてのプロセスが同じCPUで実行されているわけではありませんが、それらは以前と同じ通信用の同じデータ構造を持っています。SMPへの取り組みは、ほとんどの場合、古いロックをより並列な環境で機能させることに限定されています。
DigitalRoss 2017年

10

マルチコアCPU用の最適化コンパイラ/バイトコードVMを作成している場合、すべてのコアで効率的に実行されるコードを生成するために、たとえばx86について特に何を知る必要がありますか?

最適化コンパイラ/バイトコードVMを書く人として、ここであなたを助けることができるかもしれません。

すべてのコアにわたって効率的に実行されるコードを生成するために、x86について特に何も知る必要はありません。

ただし、すべてのコアで正しく実行されるコードを作成するには、cmpxchgとその仲間について知る必要がある場合があります。マルチコアプログラミングでは、実行スレッド間の同期と通信を使用する必要があります。

一般的にx86で効率的に実行されるコードを生成するために、x86についての知識が必要になる場合があります。

あなたが学ぶのに役立つ他の事柄があります:

複数のスレッドを実行できるようにするためにOS(LinuxまたはWindowsまたはOSX)が提供する機能について学ぶ必要があります。OpenMPやスレッディングビルディングブロックなどの並列化API、またはOSX 10.6「Snow Leopard」の近日公開予定の「Grand Central」について学ぶ必要があります。

コンパイラーを自動並列化する必要があるかどうか、またはコンパイラーによってコンパイルされたアプリケーションの作成者が特別な構文またはAPI呼び出しをプログラムに追加して複数のコアを利用する必要があるかどうかを検討する必要があります。


.NETやJavaのような人気のあるVMには、メインのGCプロセスがロックに覆われ、基本的にシングルスレッド化されているという問題がありませんか?
マルコファンデフォールト

9

各コアは異なるメモリ領域から実行されます。オペレーティングシステムはコアをプログラムに向け、コアはプログラムを実行します。プログラムは、複数のコアがあること、またはどのコアで実行されているかを認識しません。

また、オペレーティングシステムでのみ使用できる追加の命令もありません。これらのコアは、シングルコアチップと同じです。各コアは、実行する次のメモリ領域を見つけるための情報交換に使用される共通メモリ領域への通信を処理するオペレーティングシステムの一部を実行します。

これは単純化ですが、それがどのように行われるかについての基本的な考え方を提供します。 Embedded.comのマルチコアとマルチプロセッサの詳細には、このトピックに関する多くの情報があります...このトピックは非常に複雑になります!


ここでは、マルチコアが一般的にどのように機能するか、およびOSがどの程度影響を与えるかについて、もう少し注意深く区別する必要があると思います。「各コアは異なるメモリ領域から実行されます」は、私の意見では誤解を招きすぎています。何よりもまず、原則として複数のコアを使用する場合、これは必要ありません。スレッド化されたプログラムの場合、2つのコアが同じテキストとデータセグメントで動作することを容易に確認できます(各コアにはスタックなどの個別のリソースも必要です)。 。
Volker Stolz、

@ShiDoiSiそのため、私の回答に「これは簡略化ですというテキストが含まれています。
ゲルハルト

5

アセンブリコードは、1つのコアで実行されるマシンコードに変換されます。マルチスレッド化する場合は、オペレーティングシステムのプリミティブを使用して、このコードを異なるプロセッサで数回、または異なるコアの異なるコードで開始する必要があります。各コアは個別のスレッドを実行します。各スレッドは、現在実行されているコアを1つだけ認識します。


4
私はこのようなことを言うつもりでしたが、OSはどのようにコアにスレッドを割り当てますか?これを実現する特権付きのアセンブリ命令がいくつかあると思います。もしそうなら、それは著者が探している答えだと思います。
A.レビー

そのための指示はありません。それがオペレーティングシステムスケジューラの義務です。Win32にはSetThreadAffinityMaskのようなオペレーティングシステム関数があり、コードはそれらを呼び出すことができますが、それはオペレーティングシステムのものであり、スケジューラーに影響を与えます。これはプロセッサーの命令ではありません。
シャープトゥース2009年

2
OpCodeが存在する必要があります。そうでない場合、オペレーティングシステムはそれを実行できません。
マシューホワイト、

1
スケジューリング用のオペコードではありません-プロセッサごとにOSのコピーを1つ取得し、メモリ空間を共有するようなものです。コアがカーネル(syscallまたは割り込み)に再び入るときはいつでも、メモリ内の同じデータ構造を調べて、次に実行するスレッドを決定します。
pjc50 2009年

1
@ A.Levy:別のコアでのみ実行できるアフィニティでスレッドを開始しても、すぐに他のコア移動しません。通常のコンテキストスイッチと同様に、コンテキストがメモリに保存されます。他のハードウェアスレッドは、スケジューラデータ構造内のエントリを確認し、そのうちの1つは最終的にスレッドを実行することを決定します。したがって、最初のコアの観点から、共有データ構造に書き込み、最終的に別のコア(ハードウェアスレッド)上のOSコードがそれに気づき、実行します。
Peter Cordes

3

機械語命令ではまったく行われません。コアは別個のCPUになりすまし、互いに通信するための特別な機能はありません。通信方法は2つあります。

  • それらは物理アドレス空間を共有します。ハードウェアはキャッシュコヒーレンシを処理するため、1つのCPUが別のCPUが読み取るメモリアドレスに書き込みます。

  • それらはAPIC(プログラマブル割り込みコントローラー)を共有します。これは、物理アドレス空間にマップされたメモリであり、1つのプロセッサが他のプロセッサを制御したり、それらをオンまたはオフにしたり、割り込みを送信したりするために使用できます。

http://www.cheesecake.org/sac/smp.htmlは、愚かなURLの優れたリファレンスです。


2
実際にはAPICを共有していません。各論理CPUには独自のCPUがあります。APICは相互に通信しますが、別々です。
Nathan Fellman、

それらは1つの基本的な方法で(通信ではなく)同期し、LOCKプレフィックス(命令「xchg mem、reg」には暗黙のロック要求が含まれます)を介して実行されます。 (実際には任意のバスマスタリングデバイス)がバスへの排他的アクセスを必要としています。最終的に信号がLOCKA(確認)ピンに戻り、CPUがバスに排他的にアクセスできるようになったことをCPUに通知します。外部デバイスはCPUの内部動作よりもはるかに遅いため、LOCK / LOCKAシーケンスを完了するには何百ものCPUサイクルが必要になる場合があります。
Olof Forshell、2011

1

シングルスレッドアプリケーションとマルチスレッドアプリケーションの主な違いは、前者には1つのスタックがあり、後者にはスレッドごとに1つあるということです。コンパイラはデータとスタックセグメントレジスタ(dsとss)が等しくないと想定するため、コードは多少異なります。つまり、デフォルトでssレジスターになっているebpおよびespレジスターを介した間接指定では、デフォルトでdsになりません(ds!= ssのため)。逆に、デフォルトがdsである他のレジスターを介した間接指定は、デフォルトでssにはなりません。

スレッドは、データおよびコード領域を含む他のすべてを共有します。また、libルーチンを共有しているため、スレッドセーフであることを確認してください。RAM内の領域を並べ替えるプロシージャは、マルチスレッド化して高速化できます。スレッドは、同じ物理メモリ領域内のデータにアクセス、比較、および順序付けし、同じコードを実行しますが、ソートのそれぞれの部分を制御するために異なるローカル変数を使用します。もちろん、これは、ローカル変数が含まれるスタックがスレッドごとに異なるためです。このタイプのプログラミングでは、(キャッシュとRAMでの)コア間のデータの衝突が減少するようにコードを注意深く調整する必要があります。これにより、コードが1つだけの場合よりも2つ以上のスレッドで高速になります。もちろん、未調整のコードは、多くの場合、2つ以上のプロセッサよりも1つのプロセッサの方が高速です。すべてのスレッドではなく特定のスレッドに割り込みたいため、標準の「int 3」ブレークポイントは適用できないため、デバッグはより困難です。デバッグレジスタのブレークポイントも、割り込みたい特定のスレッドを実行している特定のプロセッサに設定できない限り、この問題を解決しません。

他のマルチスレッドコードには、プログラムの異なる部分で実行される異なるスレッドが含まれる場合があります。このタイプのプログラミングは、同じ種類のチューニングを必要としないため、学習がはるかに簡単です。


0

すべてのマルチプロセッシング対応アーキテクチャーに追加されたものは、それらの前に登場したシングルプロセッサーのバリアントと比較して、コア間で同期するための命令です。また、キャッシュの一貫性、バッファのフラッシュ、およびOSが処理する必要がある同様の低レベルの操作を処理するための指示もあります。IBM POWER6、IBM Cell、Sun Niagara、Intelの「ハイパースレッディング」などの同時マルチスレッドアーキテクチャの場合、スレッド間で優先順位を付ける新しい命令が表示される傾向があります(優先順位を設定し、何もする必要がないときに明示的にプロセッサを生成するなど)。 。

ただし、基本的な単一スレッドのセマンティクスは同じで、同期と他のコアとの通信を処理するための追加機能を追加するだけです。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.