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.
Cir*_*四事件 73
可运行的裸机示例,包含所有必需的样板.所有主要部分均包含在下面.
在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)
在那段代码上:
大多数操作系统将使第3环(用户程序)无法完成大部分操作.
因此,您需要编写自己的内核以便随意使用它:用户域Linux程序将无法运行.
首先,运行一个处理器,称为自举处理器(BSP).
它必须通过称为处理器间中断(IPI)的特殊中断唤醒其他(称为应用程序处理器(AP)).
可以通过中断命令寄存器(ICR)编程高级可编程中断控制器(APIC)来完成这些中断
ICR的格式记录在:10.6"发布INTERPROCESSOR INTERRUPTS"
IPI会在我们写入ICR后立即发生.
ICR_LOW在8.4.4"MP初始化示例"中定义为:
ICR_LOW EQU 0FEE00300H
Run Code Online (Sandbox Code Playgroud)
神奇值0FEE00300是ICR的存储器地址,如表10-1"本地APIC寄存器地址映射"中所述.
在示例中使用了最简单的方法:它设置ICR以发送广播IPI,这些IPI被传送到除当前处理器之外的所有其他处理器.
但是,有些人也可以通过BIOS设置的特殊数据结构(如ACPI表或英特尔MP配置表)获取有关处理器的信息,并且只能逐个唤醒您需要的信息.
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)
使用链接描述文件是另一种可能性.
延迟循环是一个令人讨厌的工作部分:没有超级简单的方法来精确地进行这样的睡眠.
可能的方法包括:
我认为初始处理器需要处于保护模式,因为我们写入的地址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)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
主要的初始化动作似乎是在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
Run Code Online (Sandbox Code Playgroud)
组装并运行:
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运行不同的进程,因此操作系统的多线程功能负责决定哪个进程触摸哪个内存,以及在内存冲突的情况下该怎么做.
Dig*_*oss 37

究竟.有4组寄存器,包括4个独立的指令指针.
如果是这样,当你说"递增EDX寄存器"时,是什么决定了哪个CPU的EDX寄存器递增?
自然地执行该指令的CPU.可以把它想象成4个完全不同的微处理器,它们只是共享相同的内存.
现在x86汇编程序中是否存在"CPU上下文"或"线程"概念?
不.汇编程序只是像往常一样翻译指令.那里没有变化.
核心之间的通信/同步如何工作?
由于它们共享相同的内存,因此主要是程序逻辑问题.虽然现在有一个处理器间中断机制,但它并不是必需的,并且最初并不存在于第一个双CPU x86系统中.
如果您正在编写操作系统,那么通过硬件公开哪种机制可以让您在不同的内核上安排执行?
调度程序实际上不会更改,除了它更关注关键部分和使用的锁类型.在SMP之前,内核代码最终将调用调度程序,调度程序将查看运行队列并选择一个进程作为下一个线程运行.(内核的进程看起来很像线程.)SMP内核运行完全相同的代码,一次一个线程,只是现在关键部分锁定需要SMP安全,以确保两个内核不会意外选择相同的PID.
这是一些特殊的特权指示吗?
不会.核心只是在相同的内存中运行,使用相同的旧指令.
如果您正在为多核CPU编写优化编译器/字节码VM,那么您需要具体了解x86,以使其生成能够在所有内核中高效运行的代码?
您运行与以前相同的代码.这是需要改变的Unix或Windows内核.
您可以将我的问题总结为"对x86机器代码进行了哪些更改以支持多核功能?"
没有必要.第一个SMP系统使用与单处理器完全相同的指令集.现在,已经有大量的x86架构演变和数以万计的新指令使事情变得更快,但SMP 都不需要.
有关更多信息,请参阅英特尔多处理器规范.
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调用到他的节目采取多核心的优势.
每个Core都从不同的内存区域执行.您的操作系统将为您的程序指定一个核心,核心将执行您的程序.您的程序将不会意识到有多个核心或正在执行的核心.
此外,还没有其他指令仅适用于操作系统.这些内核与单核芯片相同.每个Core运行操作系统的一部分,该操作系统将处理与用于信息交换的公共存储区的通信,以找到要执行的下一个存储区.
这是一个简化,但它为您提供了如何完成的基本概念. 关于Embedded.com上的多核和多处理器的更多信息有很多关于这个主题的信息...这个主题变得非常复杂!
汇编代码将转换为将在一个核上执行的机器代码.如果您希望它是多线程的,您将不得不使用操作系统原语在不同的处理器上多次启动此代码或在不同的核心上启动不同的代码 - 每个核心将执行一个单独的线程.每个线程只会看到当前正在执行的一个核心.
| 归档时间: |
|
| 查看次数: |
42767 次 |
| 最近记录: |