多核汇编语言是什么样的?


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/...
内森·费尔曼

Answers:


153

这不是问题的直接答案,而是对注释中出现的问题的答案。本质上,问题是硬件为多线程操作提供了哪些支持。

Nicholas Flynt至少在x86 方面做到了。在多线程环境(超线程,多核或多处理器)中,Bootstrap线程(通常是处理器0的内核0中的线程0)启动从address提取代码0xfffffff0。所有其他线程都在称为Wait-for-SIPI的特殊睡眠状态下启动。作为其初始化的一部分,主线程通过APIC向WFS中的每个线程发送一个特殊的处理器间中断(IPI),称为SIPI(启动IPI)。SIPI包含该线程应从中开始获取代码的地址。

这种机制允许每个线程从不同的地址执行代码。所需要的就是为每个线程建立自己的表和消息传递队列提供软件支持。操作系统使用它们来执行实际的多线程调度。

就实际的程序集而言,正如Nicholas所写,对于单线程或多线程应用程序,程序集之间没有区别。每个逻辑线程都有自己的寄存器集,因此编写:

mov edx, 0

将仅EDX针对当前正在运行的线程进行更新。无法EDX使用单个汇编指令在另一个处理器上进行修改。您需要某种系统调用来要求OS告诉另一个线程运行将更新其自身代码的代码EDX


2
感谢您填补尼古拉斯回答中的空白。现在将您的答案标记为可接受的答案。...提供我感兴趣的具体细节...虽然最好是有一个答案将您的信息和Nicholas的所有知识结合起来。
Paul Hollingsworth,2009年

3
这不能回答线程从何而来的问题。内核和处理器是硬件,但是必须以某种方式在软件中创建线程。主线程如何知道将SIPI发送到哪里?还是SIPI本身会创建一个新线程?
rich remer 2014年

7
@richremer:似乎您在混淆硬件线程和软件线程。硬件线程始终存在。有时它睡着了。SIPI本身会唤醒硬件线程并允许其运行软件。由OS和BIOS决定运行哪些HW线程,以及在每个HW线程上运行哪些进程和SW线程。
内森·费尔曼

2
这里提供了许多简洁明了的信息,但这是一个重要的话题-因此问题会持续存在。有一些从USB驱动器或“软盘”启动的完整“裸露”内核的例子-这是一个使用旧的TSS描述符以汇编器编写的x86_32版本,该描述符实际上可以运行多线程C代码(github。 com / duanev / oz-x86-32-asm-003),但不支持标准库。超出您的要求,但它也许可以回答一些缠绵的问题。
duanev

87

英特尔x86最小可运行裸机示例

具有所有必需样板的可运行裸机示例。所有主要部分均在下面介绍。

在Ubuntu 15.10 QEMU 2.3.0和Lenovo ThinkPad T400 真实硬件客户机上进行了测试

英特尔手册卷3系统编程指南- 325384-056US 2015年9月盖SMP在章8,9和10。

表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环(用户程序)无法进行大多数操作。

    因此,您需要编写自己的内核以自由使用它:userland Linux程序将无法运行。

  2. 首先,运行单个处理器,称为引导处理器(BSP)。

    它必须通过称为处理器间中断(IPI)的特殊中断唤醒其他处理器(称为应用处理器(AP)

    可以通过中断命令寄存器(ICR)对高级可编程中断控制器(APIC)进行编程来完成这些中断。

    ICR的格式记录在:10.6“ ISSUING INTERPROCESSOR INTERRUPTS”

    一旦我们写到ICR,IPI就发生了。

  3. ICR_LOW在8.4.4“ MP初始化示例”中定义为:

    ICR_LOW EQU 0FEE00300H
    

    神奇的值0FEE00300是ICR的存储器地址,如表10-1“本地APIC寄存器地址映射”中所述

  4. 在示例中使用了最简单的方法:它将ICR设置为发送广播IPI,这些IPI将传递到除当前处理器之外的所有其他处理器。

    但是也有可能,并且有人建议通过 BIOS设置的特殊数据结构(例如ACPI表或Intel的MP配置表)获取有关处理器的信息,然后仅逐个唤醒您需要的那些信息。

  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(在我的示例中使用)
    • 高温PET
    • 使用上述方法校准繁忙循环的时间,并改用它

    相关:如何在屏幕上显示数字并使用DOS x86组件睡眠一秒钟?

  7. 我认为初始处理器需要处于保护模式才能正常工作,因为我们写入的地址0FEE00300H对于16位而言太高了

  8. 为了在处理器之间进行通信,我们可以在主进程上使用自旋锁,然后从第二个内核修改锁。

    我们应该确保完成内存写回操作,例如通过wbinvd

处理器之间的共享状态

8.7.1“逻辑处理器的状态”说:

以下功能是支持Intel超线程技术的Intel 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寄存器(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
  • 其他大多数MSR寄存器,包括页面属性表(PAT)。请参阅下面的异常。
  • 本地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

缓存共享在以下位置讨论:

英特尔超线程比单独的内核具有更大的缓存和管道共享: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退出。

如果仅使用一个CPU启动QEMU -smp 1,则模拟将永远挂在自旋锁上。

通过PSCI接口唤醒了CPU 1,更多详细信息位于: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内核或其他操作系统的一些很好的理由:-)

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

这种用户态指令似乎也用于实现Linux futex系统调用,这是Linux中主要的同步原语之一。man futex4.15读取:

futex()系统调用提供了一种等待直到特定条件变为真的方法。它通常在共享内存同步的上下文中用作阻止结构。使用futex时,大多数同步操作在用户空间中执行。用户空间程序仅在程序必须阻塞更长的时间直到条件变为真时才使用futex()系统调用。其他futex()操作可用于唤醒等待特定条件的任何进程或线程。

syscall名称本身的意思是“快速用户空间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语法,因此不能成为它们...那是什么?
罗斯兰

@Ruslan gcc#include来自C预处理器。Makefile按照入门部分中的说明使用提供的内容:github.com/cirosantilli/x86-bare-metal-examples/blob/…如果不起作用,请打开GitHub问题。
Ciro Santilli郝海东冠状病六四事件法轮功

在x86上,如果内核意识到队列中没有准备好运行的进程会怎样?(在空闲系统上可能会不时发生)。在执行新任务之前,核心自旋是否会锁定共享内存结构?(可能不好用吗?)它会调用HLT之类的方法进入睡眠状态直到出现中断吗?(在那种情况下,谁负责唤醒该内核?)
tigrou

@tigrou不确定,但是我发现Linux实现极有可能将其置于电源状态,直到下一次(可能是计时器)中断为止,尤其是在电源是关键的ARM上。我会很快试一试,看是否能具体轻松地运行Linux模拟器的指令跟踪观察,它可能是:github.com/cirosantilli/linux-kernel-module-cheat/tree/...
西罗桑蒂利郝海东冠状病六四事件法轮功

1
一些信息(特定于x86 / Windows)可以在此处找到(请参阅“空闲线程”)。TL; DR:当CPU上不存在可运行线程时,CPU将分派到空闲线程。连同其他一些任务一起,它将最终调用已注册的电源管理处理器空闲例程(通过CPU供应商提供的驱动程序,例如Intel)。为了减少功耗,这可能会将CPU转换为更深的C状态(例如:C0-> C3)。
tigrou

43

据我了解,每个“核心”都是一个完整的处理器,具有自己的寄存器集。基本上,BIOS是从一个核心运行开始的,然后操作系统可以通过初始化其他核心并将它们指向要运行的代码等来“启动”其他核心。

同步由操作系统完成。通常,每个处理器为OS运行一个不同的进程,因此操作系统的多线程功能负责确定哪个进程接触哪个内存,以及在发生内存冲突的情况下该做什么。


28
哪一个引起了问题:操作系统可以使用哪些指令来执行此操作?
Paul Hollingsworth,2009年

4
为此有一组特权说明,但这是操作系统的问题,而不是应用程序代码。如果应用程序代码希望是多线程的,则必须调用操作系统函数来实现“魔术”。
Sharptooth

2
BIOS通常会确定有多少个内核可用,并将在询问时将此信息传递给OS。BIOS(和硬件)必须符合一些标准,以便可以访问不同PC的硬件规格(处理器,内核,PCI总线,PCI卡,鼠标,键盘,图形,ISA,PCI-E / X,内存等)从操作系统的角度来看,它看起来相同。如果BIOS没有报告有四个内核,则操作系统通常会假设只有一个。甚至可能会有BIOS设置可以尝试。
Olof Forshell

1
太酷了,但是如果您要编写裸机程序该怎么办呢?
Alexander Ryan Baggett

3
@AlexanderRyanBaggett ,?那还算什么 重申一句,当我们说“留给操作系统”时,我们回避了这个问题,因为问题是操作系统如何做到这一点?它使用什么组装说明?
起搏器

39

非官方的SMP常见问题解答 堆栈溢出徽标


曾几何时,例如,编写x86汇编程序时,您将需要说明“加载EDX寄存器值为5”,“递增EDX”寄存器等指令。对于具有4个内核(甚至更多)的现代CPU ,在机器代码级别上,是否看起来像有4个独立的CPU(即,是否只有4个不同的“ EDX”寄存器)?

究竟。有4套寄存器,包括4个独立的指令指针。

如果是这样,当您说“增加EDX寄存器”时,由什么决定哪个CPU的EDX寄存器增加?

自然地,执行该指令的CPU。可以将其视为4个完全不同的微处理器,它们仅共享同一内存。

x86汇编器中现在有“ CPU上下文”或“线程”概念吗?

不会。汇编程序会像往常一样翻译指令。那里没有变化。

内核之间的通信/同步如何工作?

由于它们共享相同的内存,因此主要取决于程序逻辑。尽管现在有一个处理器间中断机制,但它不是必需的,并且最初并不存在于第一个双CPU x86系统中。

如果您正在编写操作系统,则通过硬件公开什么机制以允许您计划在不同内核上的执行?

调度程序实际上并没有改变,只是对关键部分和所使用的锁的类型稍加小心。在SMP之前,内核代码最终将调用调度程序,该调度程序将查看运行队列并选择要作为下一个线程运行的进程。(内核的过程看起来很像线程。)SMP内核一次运行完全相同的代码,一次只运行一个线程,只是现在关键的节锁定必须是SMP安全的,以确保两个内核不会意外被选中。相同的PID

这是一些特殊的特权指令吗?

否。所有内核都以相同的旧指令在同一内存中运行。

如果您正在为多核CPU编写优化的编译器/字节码VM,那么您需要特别了解x86,以使其生成可在所有内核上高效运行的代码?

您运行与以前相同的代码。需要更改的是Unix或Windows内核。

您可以将问题概括为“对x86机​​器代码进行了哪些更改以支持多核功能?”

没什么必要的。第一个SMP系统使用与单处理器完全相同的指令集。现在,已经有大量的x86架构演变和成千上万的新指令来使运行速度更快,但是没有必要对于SMP来说。

有关更多信息,请参阅英特尔多处理器规范。


更新:所有的后续问题可以通过只接受完全,一个回答ň三通多核CPU几乎是1完全一样的东西ñ单独的处理器,只是共享相同的内存。2 没有问一个重要的问题:如何编写一个程序在多个内核上运行以提高性能?答案是:它是使用诸如Pthreads之类的线程库编写的某些线程库使用操作系统不可见的“绿色线程”,而这些线程库不会获得单独的内核,但是只要线程库使用内核线程功能,您的线程程序就会自动成为多核。
1.为了向后兼容,只有第一个内核在复位时启动,并且需要执行一些驱动程序类型的操作来启动其余内核。
2.他们自然也共享所有外围设备。


3
我一直认为“线程”是一个软件概念,这使我很难理解多核处理器,问题是,代码如何告诉内核“我将创建一个在内核2中运行的线程”?有什么特殊的汇编代码吗?
demonguy 2015年

2
@demonguy:不,没有类似的特殊说明。您通过设置亲和性掩码(表示“此线程可以在这组逻辑内核上运行”),要求操作系统在特定内核上运行线程。这完全是软件问题。每个CPU内核(硬件线程)都独立运行Linux(或Windows)。为了与其他硬件线程一起使用,它们使用共享数据结构。但是,您永远不要“直接”在其他CPU上启动线程。您告诉操作系统您想要一个新线程,它会在另一个内核的操作系统可以看到的数据结构中做一个记录。
彼得·科德斯

2
我可以告诉os,但是os如何将代码放入特定的内核?
demonguy 2015年

4
@demonguy ...(简体)...每个内核共享OS映像并在同一位置开始运行。因此,对于8个内核,这就是内核中运行的8个“硬件进程”。每个调用相同的调度程序功能,该功能检查进程表中是否存在可运行的进程或线程。(这是运行队列。)同时,带有线程的程序在不了解底层SMP性质的情况下工作。他们只是用fork(2)之类的东西,让内核知道他们要运行。本质上,核心找到流程,而不是流程找到核心。
DigitalRoss

1
您实际上不需要将一个核心与另一个核心打断。以这种方式考虑:与软件机制进行交流之前您需要交流的所有内容都可以。相同的软件机制继续起作用。因此,管道,内核调用,睡眠/唤醒等所有内容……它们仍然像以前一样工作。并非每个进程都在同一CPU上运行,但是它们具有与以前相同的通信数据结构。进行SMP的工作主要限于使旧锁在更并行的环境中工作。
DigitalRoss

10

如果您正在为多核CPU编写优化的编译器/字节码VM,那么您需要特别了解x86,以使其生成可在所有内核上高效运行的代码?

作为编写优化编译器/字节码VM的人,我可能会在这里为您提供帮助。

您无需专门了解x86即可使其生成可在所有内核上高效运行的代码。

但是,您可能需要了解cmpxchg和朋友,才能编写可在所有内核上正确运行的代码。多核编程需要在执行线程之间使用同步和通信。

您可能需要了解一些有关x86的知识,以使其生成通常可以在x86上有效运行的代码。

还有其他一些对您学习有用的东西:

您应该了解操作系统(Linux,Windows或OSX)提供的功能,以允许您运行多个线程。您应该了解并行化API,例如OpenMP和线程构建模块,或OSX 10.6“ Snow Leopard”即将推出的“ Grand Central”。

您应该考虑编译器是否应该自动并行化,或者由编译器编译的应用程序的作者是否需要在其程序中添加特殊的语法或API调用以利用多个内核。


是否没有像.NET和Java这样的几种流行VM出现问题,它们的主要GC进程被锁覆盖并且基本上是单线程的?
Marco van de Voort,2009年

9

每个内核从不同的存储区执行。您的操作系统将核心指向您的程序,而该核心将执行您的程序。您的程序将不会意识到存在多个内核或正在执行的内核。

也没有仅适用于操作系统的其他说明。这些核心与单核心芯片相同。每个内核都运行操作系统的一部分,该操作系统将处理与用于信息交换的公共存储区的通信,以查找下一个要执行的存储区。

这是一个简化,但是它为您提供了完成方法的基本思路。 在Embedded.com上,有关多核和多处理器的更多信息,有很多与此主题相关的信息。


我认为,在这里应该更仔细地区分多核的工作原理,以及操作系统的影响程度。在我看来,“每个内核都从不同的内存区域执行”。首先,原则上使用多个内核不需要这样做,并且您可以轻松地看到,对于一个线程程序,您需要两个内核,两个工作在相同的文本和数据段上(而每个内核也需要像栈一样的单独资源) 。
Volker Stolz,

@ShiDoiSi这就是为什么我的答案包含文本“ This is a simplification”的原因
格哈德

5

汇编代码将转换为将在一个内核上执行的机器代码。如果您希望它是多线程的,则必须使用操作系统原语在不同的处理器上多次启动此代码,或者在不同的内核上启动不同的代码-每个内核将执行一个单独的线程。每个线程只会看到其当前正在执行的一个核心。


4
我本来要说这样的话,但是操作系统如何将线程分配给内核?我想象有一些特权汇编指令可以完成此任务。如果是这样,我认为这就是作者想要的答案。
A. Levy,

对此没有任何说明,这是操作系统调度程序的职责。Win32中有一些操作系统功能,例如SetThreadAffinityMask,代码可以调用它们,但这是操作系统的内容,会影响调度程序,而不是处理器指令。
Sharptooth

2
必须有一个操作码,否则操作系统也将无法执行该操作。
Matthew Whited

1
并不是真正的调度操作码-更像是每个处理器获得一份操作系统副本,共享一个内存空间;每当内核重新进入内核(系统调用或中断)时,它就会查看内存中相同的数据结构,以决定下一个要运行的线程。
pjc50

1
@ A.Levy:当您以仅允许其在其他内核上运行的亲和力启动线程时,它不会立即移至另一个内核。就像普通的上下文切换一样,它的上下文已保存到内存中。其他硬件线程在调度程序数据结构中看到其条目,其中一个最终将决定运行该线程。因此,从第一个内核的角度来看:您写入共享数据结构,最终另一个内核(硬件线程)上的OS代码将注意到它并运行它。
彼得·科德斯

3

根本没有机器指令来完成。内核假装为不同的CPU,并且没有任何特殊的功能可以相互通信。他们有两种交流方式:

  • 它们共享物理地址空间。硬件处理高速缓存一致性,因此一个CPU写入另一个地址读取的内存地址。

  • 它们共享一个APIC(可编程中断控制器)。这是映射到物理地址空间的内存,一个处理器可以使用它来控制其他处理器,打开或关闭它们,发送中断等。

http://www.cheesecake.org/sac/smp.html是一个很好的参考,其中包含一个愚蠢的网址。


2
他们实际上并不共享APIC。每个逻辑CPU都有自己的一个。APIC之间相互通信,但它们是分开的。
内森·费尔曼

它们以一种基本方式进行同步(而不是通信),即通过LOCK前缀(指令“ xchg mem,reg”包含隐式锁定请求)进行同步,该前缀运行至锁定销,该销向所有总线运行,有效地告诉他们CPU (实际上是任何总线主控设备)都希望以独占方式访问总线。最终,一个信号将返回到LOCKA(确认)引脚,告知CPU现在它具有对总线的独占访问权。由于外部设备比CPU的内部运行慢得多,因此LOCK / LOCKA序列可能需要数百个CPU周期才能完成。
Olof Forshell

1

单线程和多线程应用程序之间的主要区别在于,前者只有一个堆栈,而后者每个线程都有一个堆栈。由于编译器将假定数据和堆栈段寄存器(ds和ss)不相等,因此生成的代码有所不同。这意味着通过默认为ss寄存器的ebp和esp寄存器进行的间接寻址也不会默认为ds(因为ds!= ss)。相反,通过其他默认为ds的寄存器进行的间接寻址将不会默认为ss。

线程共享其他所有内容,包括数据和代码区域。它们还共享lib例程,因此请确保它们是线程安全的。可以对RAM中的区域进行排序的过程是多线程的,以加快处理速度。然后,线程将访问,比较和排序同一物理内存区域中的数据,并执行同一代码,但使用不同的局部变量来控制它们各自的排序部分。当然,这是因为线程具有包含本地变量的不同堆栈。这种类型的编程需要仔细调整代码,以减少内核间数据冲突(在高速缓存和RAM中),从而导致使用两个或多个线程的代码比仅使用一个线程的代码更快。当然,一个处理器的未调优代码通常比两个或两个以上的处理器更快。调试更具挑战性,因为标准的“ int 3”断点将不适用,因为您要中断特定线程而不是全部中断。调试寄存器断点也不能解决此问题,除非您可以在执行要中断的特定线程的特定处理器上设置它们。

其他多线程代码可能涉及在程序的不同部分中运行的不同线程。这种类型的编程不需要相同类型的调整,因此更容易学习。


0

与之前的单处理器变体相比,每种具有多处理能力的体系结构上都添加了内核之间同步的指令。此外,您还具有处理缓存一致性,刷新缓冲区以及操作系统必须处理的类似低级操作的说明。在同时使用多线程体系结构(例如IBM POWER6,IBM Cell,Sun Niagara和Intel“超线程”)的情况下,您还倾向于看到新的指令来区分线程之间的优先级(例如设置优先级并在无事可做时显式产生处理器) 。

但是基本的单线程语义是相同的,您只需添加额外的功能来处理与其他内核的同步和通信。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.