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

Pau*_*rth 231 cpu x86 assembly multicore smp

曾几何时,为了编写x86汇编程序,你会得到一条说明"加载EDX寄存器的值为5","递增EDX"寄存器等的指令.

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

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

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

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

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

Nat*_*man 141

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

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

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

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

mov edx, 0
Run Code Online (Sandbox Code Playgroud)

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

  • @richremer:好像你让HW线程和SW线程混淆了.HW线程始终存在.有时它睡着了.SIPI本身唤醒HW线程并允许它运行SW.由OS和BIOS决定运行哪些HW线程,以及在每个HW线程上运行哪些进程和SW线程. (5认同)
  • 这里有很多好的和简洁的信息,但这是一个很大的话题 - 所以问题可能会挥之不去。有几个从 USB 驱动器或“软盘”启动的完整“裸机”内核的例子 - 这是使用旧的 TSS 描述符用汇编器编写的 x86_32 版本,它可以实际运行多线程 C 代码(https: //github.com/duanev/oz-x86-32-asm-003) 但没有标准库支持。比您要求的要多得多,但它可能可以回答一些挥之不去的问题。 (3认同)
  • 感谢您填补尼古拉斯回答中的空白。现在已将您的答案标记为已接受的答案……提供了我感兴趣的具体细节……尽管如果有一个答案将您的信息和 Nicholas 的所有信息结合起来会更好。 (2认同)
  • 这不能回答线程来自何处的问题.核心和处理器是硬件,但必须以某种方式在软件中创建线程.主线程如何知道SIPI的发送位置?或者SIPI本身是否创建了一个新线程? (2认同)

Cir*_*四事件 73

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

可运行的裸机示例,包含所有必需的样板.所有主要部分均包含在下面.

在Ubuntu 15.10 QEMU 2.3.0和联想ThinkPad T400 真实硬件客户机上测试过.

" 英特尔手册第3卷系统编程指南 - 325384-056US 2015年9月"在第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
Run Code Online (Sandbox Code Playgroud)

在那段代码上:

  1. 大多数操作系统将使第3环(用户程序)无法完成大部分操作.

    因此,您需要编写自己的内核以便随意使用它:用户域Linux程序将无法运行.

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

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

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

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

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

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

    ICR_LOW EQU 0FEE00300H
    
    Run Code Online (Sandbox Code Playgroud)

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

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

    但是,有些人也可以通过BIOS设置的特殊数据结构(如ACPI表或英特尔MP配置表)获取有关处理器的信息,并且只能逐个唤醒您需要的信息.

  5. XXin 000C46XXH将处理器将执行的第一条指令的地址编码为:

    CS = XX * 0x100
    IP = 0
    
    Run Code Online (Sandbox Code Playgroud)

    请记住,CS将地址乘以0x10,因此第一条指令的实际内存地址为:

    XX * 0x1000
    
    Run Code Online (Sandbox Code Playgroud)

    因此,例如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
    
    Run Code Online (Sandbox Code Playgroud)

    使用链接描述文件是另一种可能性.

  6. 延迟循环是一个令人讨厌的工作部分:没有超级简单的方法来精确地进行这样的睡眠.

    可能的方法包括:

    • PIT(在我的例子中使用)
    • HPET
    • 用上面的方法校准繁忙循环的时间,然后使用它

    相关:如何在屏幕上显示一个数字,并使用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注册.
  • 英特尔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?
     * 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
Run Code Online (Sandbox Code Playgroud)

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 \
;
Run Code Online (Sandbox Code Playgroud)

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

在自旋锁之后,CPU 0然后执行半主机退出调用,这使得QEMU退出.

如果只用一个CPU启动QEMU -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上测试.


Nic*_*ynt 42

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

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

  • 但这确实提出了一个问题:操作系统可以使用哪些指令来执行此操作? (28认同)
  • 有一组特权说明,但这是操作系统的问题,而不是应用程序代码.如果应用程序代码想要多线程,则必须调用操作系统函数来执行"魔术". (4认同)
  • 这很酷,但是如果您正在编写裸机程序怎么办? (3认同)
  • BIOS通常会识别可用的内核数量,并在询问时将此信息传递给操作系统.BIOS(和硬件)必须符合标准,以便为不同的PC访问硬件细节(处理器,内核,PCI总线,PCI卡,鼠标,键盘,图形,ISA,PCI-E/X,内存等)从操作系统的角度看起来是一样的.如果BIOS没有报告有四个内核,操作系统通常会认为只有一个内核.甚至可能有BIOS设置进行试验. (2认同)
  • @AlexanderRyanBaggett ,? 甚至是什么?重申,当我们说"把它留给操作系统"时,我们正在回避这个问题,因为问题是操作系统是如何做到的呢?它使用什么装配说明? (2认同)

Dig*_*oss 37

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


曾几何时,编写x86汇编程序,你会得到说明"加载EDX寄存器的值为5","递增EDX"寄存器等等.现代CPU有4个核心(甚至更多) ,在机器代码级别,它看起来只有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认同)
  • @demonguy ...(简化)...每个核心共享操作系统映像并开始在同一个地方运行它.因此,对于8个内核,这是内核中运行的8个"硬件进程".每个调用相同的调度程序函数,该函数检查进程表以查找可运行的进程或线程.(这是*运行队列.*)同时,具有线程的程序在不了解底层SMP性质的情况下工作.他们只是fork(2)或者其他东西让内核知道他们想要运行.从本质上讲,核心是找到流程,而不是找到核心的流程. (3认同)
  • @demonguy:不,没有针对此类的特殊说明。您可以通过设置关联掩码(表示“此线程可以在这组逻辑核心上运行”)来要求操作系统在特定核心上运行您的线程。完全是软件问题。每个 CPU 内核(硬件线程)独立运行 Linux(或 Windows)。为了与其他硬件线程一起工作,它们使用共享数据结构。但是您永远不会“直接”在不同的 CPU 上启动线程。你告诉操作系统你想要一个新线程,它会在另一个内核上的操作系统看到的数据结构中做一个注释。 (2认同)
  • 我可以告诉os,但是os如何将代码放入特定的内核? (2认同)

Ale*_*own 10

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

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

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

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

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

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

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

你应该考虑,如果你的编译器应该是自动parallelising,或者如果你的编译器编译的应用程序的作者需要添加特殊的语法或API调用到他的节目采取多核心的优势.


Ger*_*ard 9

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

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

这是一个简化,但它为您提供了如何完成的基本概念. 关于Embedded.com上的多核和多处理器的更多信息有很多关于这个主题的信息...这个主题变得非常复杂!


sha*_*oth 5

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

  • 我打算说这样的话,但是OS如何将线程分配给核心?我想有一些特权装配说明可以实现这一点.如果是这样,我认为这是作者正在寻找的答案. (4认同)
  • 必须有一个OpCode,否则操作系统也无法做到. (2认同)
  • 并不是真正的调度操作码——更像是每个处理器获得一个操作系统副本,共享内存空间;每当内核重新进入内核(系统调用或中断)时,它都会查看内存中的相同数据结构,以确定接下来要运行的线程。 (2认同)
  • @A.Levy:当您启动一个具有亲和力的线程时,该线程只允许它在不同的核心上运行,它不会“立即”移动到另一个核心。它的上下文保存到内存中,就像普通的上下文切换一样。其他硬件线程在调度程序数据结构中看到其条目,其中一个将最终决定运行该线程。因此,从第一个核心的角度来看:**您写入共享数据结构,最终另一个核心(硬件线程)上的操作系统代码会注意到它并运行它。** (2认同)

归档时间:

查看次数:

42767 次

最近记录:

6 年,1 月 前