曾几何时,编写x86汇编程序,你会得到说明“加载EDX寄存器的值为5”,“递增EDX”寄存器等等。
对于具有4个核心(甚至更多)的现代CPU,在机器代码级别看起来就像有4个独立的CPU(即只有4个不同的“EDX”寄存器)?如果是这样,当你说“递增EDX寄存器”时,是什么决定了哪个CPU的EDX寄存器递增?现在x86汇编程序中是否存在“CPU上下文”或“线程”概念?
核心之间的通信/同步如何工作?
如果您正在编写操作系统,那么通过硬件公开哪种机制可以让您在不同的核心上安排执行?这是一些特殊的特权指示吗?
如果您正在为多核CPU编写优化编译器/字节码VM,那么您需要具体了解x86,以使其生成能够在所有内核中高效运行的代码吗?
对x86机器代码进行了哪些更改以支持多核功能?
答案 0 :(得分:130)
这不是问题的直接答案,但它是对评论中出现的问题的回答。从本质上讲,问题是硬件对多线程操作的支持。
Nicholas Flynt had it right,至少关于x86。在多线程环境(超线程,多核或多处理器)中,引导线程(通常是处理器0中核心0中的线程0)开始从地址{{1}获取代码}。所有其他线程都以称为 Wait-for-SIPI 的特殊睡眠状态启动。作为初始化的一部分,主线程通过APIC向WFS中的每个线程发送称为SIPI(启动IPI)的特殊处理器间中断(IPI)。 SIPI包含该线程应该从该地址开始获取代码的地址。
此机制允许每个线程从不同的地址执行代码。所需要的只是每个线程的软件支持,以建立自己的表和消息队列。操作系统使用那些来进行实际的多线程调度。
就实际装配而言,正如Nicholas所写,单线程或多线程应用程序的程序集之间没有区别。每个逻辑线程都有自己的寄存器集,所以写:
0xfffffff0
只会更新当前正在运行的主题的mov edx, 0
。使用单个汇编指令无法在另一个处理器上修改EDX
。您需要某种系统调用来要求操作系统告诉另一个线程运行将更新其自己的EDX
的代码。
答案 1 :(得分:58)
Runnable bare metal example with all required boilerplate。所有主要部分均包含在下面。
在Ubuntu 15.10 QEMU 2.3.0和联想ThinkPad T400 real hardware guest上进行了测试。
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和#34;本地APIC寄存器地址映射"
示例中使用了最简单的方法:它设置ICR以发送传送到除当前处理器之外的所有其他处理器的广播IPI。
但也有可能and recommended by some通过BIOS设置的特殊数据结构获取有关处理器的信息,如ACPI tables or Intel's MP configuration table,并且只能逐个唤醒您需要的数据。
XX
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
使用链接描述文件是另一种可能性。
延迟循环是一个烦人的工作:没有超级简单的方法来准确地进行这样的睡眠。
可能的方法包括:
相关: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 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),控制寄存器,IA32_EFER on 英特尔64处理器。
逻辑处理器共享以下功能:
- 内存类型范围寄存器(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
主要的初始化操作似乎是arch/x86/kernel/smpboot.c
。
这里我为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
组装并运行:
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然后执行semihost exit call,这使QEMU退出。
如果只用一个带-smp 1
的CPU启动QEMU,那么模拟就会永久挂在自旋锁上。
CPU 1通过PSCI接口唤醒,详情请见:ARM: Start/Wakeup/Bringup the other CPU cores/APs and pass execution start address?
upstream version还有一些调整可以使它适用于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上测试。
答案 2 :(得分:42)
据我了解,每个“核心”都是一个完整的处理器,有自己的寄存器集。基本上,BIOS会在一个核心运行时启动,然后操作系统可以通过初始化它们并将它们指向要运行的代码等来“启动”其他核心。
同步由OS完成。通常,每个处理器都为操作系统运行不同的进程,因此操作系统的多线程功能负责决定哪个进程可以触摸哪个内存,以及在内存冲突的情况下该怎么做。
答案 3 :(得分:35)
<小时/> 曾几何时,为了编写x86汇编程序,你会得到说明“加载EDX寄存器的值为5”,“递增EDX”寄存器等等。现代CPU有4个核心(或者甚至更多),在机器代码级别,它看起来只有4个独立的CPU(即只有4个不同的“EDX”寄存器)?
<强>完全。有4组寄存器,包括4个独立的指令指针。
如果是这样,当你说“递增EDX寄存器”时,是什么决定哪个CPU的EDX寄存器递增?
自然地执行该指令的CPU。可以把它想象成4个完全不同的微处理器,它们只是共享相同的内存。
现在x86汇编程序中是否存在“CPU上下文”或“线程”概念?
否。汇编程序只是像往常一样翻译指令。没有变化。
核心之间的通信/同步如何工作?
由于它们共享相同的内存,因此主要是程序逻辑问题。虽然现在有inter-processor interrupt机制,但它并不是必需的,并且最初并不存在于第一个双CPU x86系统中。
如果您正在编写操作系统,那么通过硬件公开哪种机制可以让您在不同的核心上安排执行?
调度程序实际上不会更改,除了它更关注关键部分和使用的锁类型。在SMP之前,内核代码最终将调用调度程序,调度程序将查看运行队列并选择要作为下一个线程运行的进程。 (内核的进程看起来很像线程。)SMP内核运行完全相同的代码,一次一个线程,只是现在关键部分锁定需要SMP安全,以确保两个内核不会意外选择相同的PID。
是否有特殊优惠指示?
否。这些内核只使用相同的旧指令运行在同一个内存中。
如果您正在为多核CPU编写优化编译器/字节码VM,那么您需要具体了解x86,以使其生成能够在所有内核中高效运行的代码吗?
您运行与以前相同的代码。这是需要更改的Unix或Windows内核。
您可以将我的问题总结为“对x86机器代码进行了哪些更改以支持多核功能?”
没有必要。第一个SMP系统使用与单处理器完全相同的指令集。现在,已经有大量的x86架构演变和数以万计的新指令使事情变得更快,但是对于SMP来说,没有一个必要。
有关详细信息,请参阅Intel Multiprocessor Specification。
<小时/> 更新:所有后续问题都可以通过完全接受 n -way多核CPU几乎 1 完全相同的事情来回答as n 只是共享相同内存的独立处理器。 2 有一个重要问题没有被问到:如何编写一个程序在多个核心上运行性能更高?答案是:它是使用类似Pthreads.的线程库编写的。某些线程库使用操作系统不可见的“绿色线程”,而这些线程库不会获得单独的内核,但只要线程库使用内核线程功能,那么您的线程程序将自动成为多核。
答案 4 :(得分:9)
每个Core从不同的内存区域执行。您的操作系统将为您的程序指定一个核心,核心将执行您的程序。您的程序将不会意识到有多个核心或正在执行的核心。
也没有只有操作系统可用的附加说明。这些内核与单核芯片相同。每个Core运行操作系统的一部分,它将处理与用于信息交换的公共存储区的通信,以找到要执行的下一个存储区。
这是一个简化,但它为您提供了如何完成的基本概念。在Embedded.com上More about multicores and multiprocessors有很多关于这个主题的信息......这个话题很快就变得复杂了!
答案 5 :(得分:9)
如果您正在编写优化 用于多核的编译器/字节码VM CPU,你需要知道什么 特别是关于x86来做 它生成有效运行的代码 跨所有核心?
作为编写优化编译器/字节码VM的人,我可以在这里为您提供帮助。
您无需了解有关x86的任何内容,以使其生成可在所有内核中高效运行的代码。
但是,您可能需要了解cmpxchg和朋友才能编写在所有核心中运行正确的代码。多核编程需要在执行线程之间使用同步和通信。
您可能需要了解x86的某些内容,以使其生成一般在x86上高效运行的代码。
还有其他一些对你有用的东西:
您应该了解操作系统(Linux或Windows或OSX)提供的功能,以便您运行多个线程。您应该了解并行化API,例如OpenMP和Threading Building Blocks,或OSX 10.6“Snow Leopard”即将推出的“Grand Central”。
您应该考虑您的编译器是否应该自动并行化,或者编译器编译的应用程序的作者是否需要在其程序中添加特殊语法或API调用以利用多个核心。
答案 6 :(得分:5)
汇编代码将转换为将在一个核心上执行的机器代码。如果您希望它是多线程的,您将不得不使用操作系统原语在不同的处理器上多次启动此代码或在不同的核心上启动不同的代码 - 每个核心将执行一个单独的线程。每个线程只会看到当前正在执行的一个核心。
答案 7 :(得分:3)
根本没有在机器指令中完成;核心假装是不同的CPU,没有任何特殊的能力可以相互交谈。他们沟通的方式有两种:
他们共享物理地址空间。硬件处理缓存一致性,因此一个CPU写入另一个CPU读取的内存地址。
他们共享一个APIC(可编程中断控制器)。这是存储器映射到物理地址空间的,可以由一个处理器用来控制其他处理器,打开或关闭它们,发送中断等。
http://www.cheesecake.org/sac/smp.html是一个很好的参考,有一个愚蠢的网址。
答案 8 :(得分:1)
单线程应用程序和多线程应用程序之间的主要区别在于前者有一个堆栈,而后者每个线程有一个堆栈。由于编译器将假设数据和堆栈段寄存器(ds和ss)不相等,因此生成的代码有所不同。这意味着通过默认为ss寄存器的ebp和esp寄存器的间接也不会默认为ds(因为ds!= ss)。相反,通过默认为ds的其他寄存器的间接不会默认为ss。
线程共享其他所有内容,包括数据和代码区域。它们还共享lib例程,因此请确保它们是线程安全的。对RAM中的区域进行排序的过程可以是多线程的,以加快速度。然后,线程将访问,比较和排序相同物理存储区域中的数据并执行相同的代码,但使用不同的局部变量来控制它们各自的排序部分。这当然是因为线程具有不同的堆栈,其中包含局部变量。这种类型的编程需要仔细调整代码,以便减少核心间数据冲突(在高速缓存和RAM中),这反过来导致代码在两个或多个线程中比仅使用一个更快。当然,一个处理器的未调优代码通常比两个或更多代码更快。调试更具挑战性,因为标准的“int 3”断点将不适用,因为您想要中断特定线程而不是所有线程。除非您可以在执行要中断的特定线程的特定处理器上设置它们,否则调试寄存器断点不能解决此问题。
其他多线程代码可能涉及在程序的不同部分中运行的不同线程。这种类型的编程不需要相同类型的调整,因此更容易学习。
答案 9 :(得分:0)
与之前的单处理器变体相比,每个支持多处理的体系结构所添加的内容都是核心之间同步的指令。此外,您还有处理缓存一致性,刷新缓冲区以及操作系统必须处理的类似低级操作的说明。对于IBM POWER6,IBM Cell,Sun Niagara和Intel“超线程”等同步多线程体系结构的情况,您还倾向于看到新的指令来确定线程之间的优先级(例如设置优先级并在没有任何操作时显式生成处理器) 。
但基本的单线程语义是相同的,您只需添加额外的工具来处理与其他核心的同步和通信。