首页 文章

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

提问于
浏览
203

曾几何时,例如,要编写x86汇编程序,你会得到说明“加载EDX寄存器的值为5”,“递增EDX”寄存器等 .

对于具有4个内核(甚至更多)的现代CPU,在机器代码级别上它看起来就像有4个独立的CPU(即只有4个不同的“EDX”寄存器)?如果是这样,当你说“递增EDX寄存器”时,是什么决定了哪个CPU的EDX寄存器递增?现在x86汇编程序中是否存在“CPU上下文”或“线程”概念?

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

如果您正在编写操作系统,那么通过硬件公开哪种机制可以让您在不同的核心上安排执行?这是一些特殊的特权指示吗?

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

对x86机器代码进行了哪些更改以支持多核功能?

10 回答

  • 33

    每个Core都从不同的内存区域执行 . 您的操作系统将为您的程序指定一个核心,核心将执行您的程序 . 您的程序将不会意识到有多个核心或正在执行的核心 .

    此操作系统也没有其他指令可用 . 这些内核与单核芯片相同 . 每个Core运行操作系统的一部分,该操作系统将处理与用于信息交换的公共存储器区域的通信,以找到要执行的下一个存储区域 .

    这是一个简化,但它为您提供了如何完成的基本概念 . Embedded.com上的More about multicores and multiprocessors有很多关于这个主题的信息......这个话题很快就变得复杂了!

  • 42

    根本没有在机器指令中完成;核心假装是不同的CPU,没有任何特殊的能力可以相互交谈 . 他们沟通的方式有两种:

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

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

    http://www.cheesecake.org/sac/smp.html是一个很好的参考与愚蠢的网址 .

  • 120

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

    作为编写优化编译器/字节码VM的人,我可以在这里帮助您 .

    您无需了解有关x86的任何内容,以使其生成可在所有内核中高效运行的代码 .

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

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

    还有其他一些对你有用的东西:

    您应该了解OS(Linux或Windows或OSX)提供的功能,以允许您运行多个线程 . 您应该了解并行化API,例如OpenMP和Threading Building Blocks,或OSX 10.6“Snow Leopard”即将推出的“Grand Central” .

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

  • 3

    非官方SMP常见问题解答


    曾几何时,要写x86汇编程序,例如,你会有说明"load the EDX register with the value 5","increment the EDX"寄存器等 . 现代CPU有4个核心(甚至更多),在机器代码级别看起来就像那里是4个独立的CPU(即只有4个不同的"EDX"寄存器)?

    Exactly. There are 4 sets of registers, including 4 separate instruction pointers.

    如果是这样,当你说"increment the EDX register"时,是什么决定了哪个CPU的EDX寄存器增加了?

    The CPU that executed that instruction, naturally. Think of it as 4 entirely different microprocessors that are simply sharing the same memory.

    现在x86汇编程序中有"CPU context"或"thread"概念吗?

    No. The assembler just translates instructions like it always did. No changes there.

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

    Since they share the same memory, it's mostly a matter of program logic. Although there now is an inter-processor interrupt mechanism, it's not necessary and was not originally present in the first dual-CPU x86 systems.

    如果您正在编写操作系统,那么通过硬件公开哪种机制可以让您在不同的核心上安排执行?

    The scheduler actually doesn't change, except that it is slightly more carefully about critical sections and the types of locks used. Before SMP, kernel code would eventually call the scheduler, which would look at the run queue and pick a process to run as the next thread. (Processes to the kernel look a lot like threads.) The SMP kernel runs the exact same code, one thread at a time, it's just that now critical section locking needs to be SMP-safe to be sure two cores can't accidentally pick the same PID.

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

    No. The cores are just all running in the same memory with the same old instructions.

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

    You run the same code as before. It's the Unix or Windows kernel that needed to change.

    您可以将我的问题汇总为"What changes have been made to x86 machine code to support multi-core functionality?"

    Nothing was necessary. The first SMP systems used the exact same instruction set as uniprocessors. Now, there has been a great deal of x86 architecture evolution and zillions of new instructions to make things go faster, but none were necessary for SMP.

    有关更多信息,请参阅Intel Multiprocessor Specification .


    更新:所有后续问题都可以通过完全接受一个n路多核CPU几乎与n个共享相同内存的独立处理器完全相同的东西来回答.2有一个重要问题没有问:如何是为了获得更高性能而编写的在多个内核上运行的程序?答案是:它是使用像_239297这样的线程库编写的 . 有些线程库使用的是操作系统不可见的"green threads",而且这些线程库不会获得单独的内核,但只要线程库使用内核线程功能,那么你的线程程序将自动成为多核 .

    1.为了向后兼容,只有第一个核心在重置时启动,并且需要执行一些驱动程序类型的操作来启动剩余的核心 . 他们自然也会共享所有外围设备 .

  • 9

    最小可运行的Intel x86裸机示例

    Runnable bare metal example with all required boilerplate . 所有主要部分均包含在下面 .

    在Ubuntu 15.10 QEMU 2.3.0和联想ThinkPad T400上测试过 .

    Intel Manual Volume 3 System Programming Guide - 325384-056US September 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环(用户程序)无法完成大部分操作 .

    因此,您需要编写自己的内核以便随意使用它:用户区Linux程序不会工作 .

    • 首先,运行一个处理器,称为自举处理器(BSP) .

    它必须通过名为Inter Processor Interrupts (IPI)的特殊中断唤醒其他的(称为应用程序处理器(AP)) .

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

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

    IPI会在我们写入ICR后立即发生 .

    • ICR_LOW在8.4.4“MP初始化示例”中定义为:
    ICR_LOW EQU 0FEE00300H
    

    神奇值 0FEE00300 是ICR的内存地址,如表10-1所示"Local APIC Register Address Map"

    • 示例中使用了最简单的方法:它设置ICR以发送传送到除当前处理器之外的所有其他处理器的广播IPI .

    但是也可以通过BIOS设置的特殊数据结构来获取有关处理器的信息,如ACPI tables or Intel's MP configuration table,并且只能逐个唤醒您需要的数据 .

    • XX in 000C46XXH 对处理器将执行的第一条指令的地址进行编码:
    CS = XX * 0x100
    IP = 0
    

    记住CS multiples addresses by 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

    • 使用上述方法校准繁忙循环的时间,然后使用它

    相关:How to display a number on the screen and and sleep for one second with DOS x86 assembly?

    • 我认为初始处理器需要处于保护模式才能使其工作,因为我们写入地址 0FEE00300H ,这对于16位来说太高了

    • 要在处理器之间进行通信,我们可以在主进程上使用自旋锁,并从第二个核心修改锁 .

    我们应该确保完成内存回写,例如通过 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)MSRs热时钟调制和ACPI电源管理控制MSR时间戳计数器MSR大多数其他MSR寄存器,包括页面属性表(PAT) . 请参阅以下例外情况 . 本地APIC注册 . 英特尔64处理器上的附加通用寄存器(R8-R15),XMM寄存器(XMM8-XMM15),控制寄存器,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?
         */
        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! In gem5, CPU 1 starts
         * woken up from the start, So this is not needed.
         * TODO gem5 actually blows up if it tries to run this. Understad why.
         */
        /* PSCI 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
        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
        ldr x2, =semihost_args
        str x1, [x2, 0]
        mov x0, #0
        str x0, [x2, 8]
        mov x1, x2
        mov w0, 0x18
        hlt 0xf000
    semihost_args:
        .skip 16
    
    spinlock:
        .skip 8
    

    GitHub upstream .

    在这个例子中,我们将CPU 0置于自旋锁循环中,并且它仅在CPU 1释放自旋锁时退出 .

    CPU 1通过PSCI接口唤醒,更多详细信息请访问:ARM: Start/Wakeup/Bringup the other CPU cores/APs and pass execution start address?

    我无法与Ubuntu aarch64工具链链接,但我提供了一个非常详细的工作crosstool-NG设置:

    该设置还有一些调整,使其适用于gem5,因此您也可以尝试性能特征 .

    我没有在真正的硬件上测试它,所以我不确定这是多么便携 . 以下覆盆子Pi参考书目可能有兴趣:

    本文档提供了有关使用ARM同步原语的一些指导,然后您可以使用这些原语来执行多核的有趣操作:http://infocenter.arm.com/help/topic/com.arm.doc.dht0008a/DHT0008A_arm_synchronization_primitives.pdf

  • 0

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

  • 9

    与之前的单处理器变体相比,每个支持多处理的体系结构所添加的内容都是内核之间同步的指令 . 此外,您还有处理缓存一致性,刷新缓冲区以及操作系统必须处理的类似低级操作的说明 . 对于IBM POWER6,IBM Cell,Sun Niagara和Intel“超线程”等同时多线程体系结构的情况,您还倾向于看到新的指令来确定线程之间的优先级(例如设置优先级并在没有任何操作时显式生成处理器) .

    但基本的单线程语义是相同的,您只需添加额外的工具来处理与其他核心的同步和通信 .

  • 1

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

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

  • 5

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

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

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

  • 45

    这不是问题的直接答案,但它是对评论中出现的问题的答案 . 从本质上讲,问题是硬件对多线程操作的支持 .

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

    此机制允许每个线程从不同的地址执行代码 . 所需要的只是每个线程的软件支持,以 Build 自己的表和消息队列 . 操作系统使用它们来进行实际的多线程调度 .

    就实际装配而言,正如Nicholas所写,单线程或多线程应用程序的程序集之间没有区别 . 每个逻辑线程都有自己的寄存器集,所以写:

    mov edx, 0
    

    只会为当前运行的线程更新 EDX . 使用单个汇编指令无法在另一个处理器上修改 EDX . 您需要某种系统调用来要求操作系统告诉另一个线程运行将更新自己的 EDX 的代码 .

相关问题