“多核”汇编语言是什么样的?
曾几何时,为了编写x86汇编程序,例如,您将会得到说明“将EDX寄存器加载到数值5”,“增加EDX”寄存器等。
对于具有4核(或更多)的现代CPU,在机器代码级,它看起来像是有4个独立的CPU(即只有4个不同的“EDX”寄存器)吗? 如果是这样,当你说“递增EDX寄存器”时,决定哪个CPU的EDX寄存器递增? x86汇编程序中是否有“CPU上下文”或“线程”的概念?
内核之间的通信/同步如何工作?
如果你正在编写一个操作系统,那么通过硬件公开了什么机制,让你能够在不同的内核上安排执行? 这是一些特殊的特权指令吗?
如果您正在为多核CPU编写一个优化的编译器/字节码虚拟机,那么您需要具体了解x86,才能生成可在所有内核之间高效运行的代码?
对x86机器代码进行了哪些更改以支持多核function?
这不是对问题的直接回答,而是对评论中出现的问题的回答。 本质上,问题是硬件对multithreading操作的支持。
Nicholas Flynt说得对 ,至less在x86方面。 在multithreading环境(超线程,多核或多处理器)中, 引导线程 (通常处理器0的核0中的线程0)启动从地址0xfffffff0
获取代码。 所有其他线程都以特殊的睡眠状态启动,称为Wait-for-SIPI 。 作为其初始化的一部分,主线程通过APIC发送一个特殊的处理器间中断(IPI),称为SIPI(启动IPI)给WFS中的每个线程。 SIPI包含该线程开始获取代码的地址。
这种机制允许每个线程从不同的地址执行代码。 所有需要的是为每个线程build立自己的表和消息队列的软件支持。 操作系统使用这些进行实际的multithreading调度。
就实际组装而言,正如Nicholas所写,单个线程或multithreading应用程序的程序集没有区别。 每个逻辑线程都有自己的寄存器集,所以写:
mov edx, 0
只会更新当前正在运行的线程的 EDX
。 使用单个汇编指令无法在另一个处理器上修改EDX
。 你需要某种系统调用来要求操作系统告诉另一个线程运行代码来更新自己的EDX
。
据我所知,每个“核心”是一个完整的处理器,拥有自己的寄存器集。 基本上,BIOS启动时只有一个内核在运行,然后操作系统可以通过初始化它们并指向代码运行等来“启动”其他内核。
同步由OS完成。 通常情况下,每个处理器对于操作系统运行的是不同的进程,因此操作系统的multithreadingfunction负责决定哪个进程接触哪个内存,以及在内存冲突的情况下如何处理。
非官方的SMP常见问题
曾几何时,为了编写x86汇编程序,例如,您将会得到说明“加载EDX寄存器值5”,“递增EDX”寄存器等的指令。对于具有4个内核(或甚至更多)的现代CPU, ,在机器代码级别,它看起来像是有4个独立的CPU(即只有4个不同的“EDX”寄存器)?
究竟。 有4组寄存器,包括4个独立的指令指针。
如果是这样,当你说“递增EDX寄存器”时,决定哪个CPU的EDX寄存器递增?
执行该指令的CPU自然就是。 把它看作是4个完全不同的微处理器,它们只是共享相同的内存。
x86汇编程序中是否有“CPU上下文”或“线程”的概念?
不,汇编程序只是像以前一样翻译指令。 那里没有变化。
内核之间的通信/同步如何工作?
由于它们共享相同的内存,所以主要是程序逻辑的问题。 虽然现在有一个处理器间中断机制,但是这并不是必需的,也不是最初出现在第一个双CPU x86系统中的。
如果你正在编写一个操作系统,那么通过硬件公开了什么机制,让你能够在不同的内核上安排执行?
调度程序实际上不会改变,除了对关键部分和所使用的锁的types稍微谨慎之外。 在SMP之前,内核代码最终会调用调度器,调度器将查看运行队列并select一个进程作为下一个线程运行。 (进程到内核看起来很像线程)SMP内核运行完全相同的代码,一次一个线程,只是现在临界区段locking需要SMP安全,以确保两个内核不会意外地select相同的PID。
这是一些特殊的特权指令吗?
没有。核心只用相同的旧指令运行在相同的内存中。
如果您正在为多核CPU编写一个优化的编译器/字节码虚拟机,那么您需要具体了解x86,才能生成可在所有内核之间高效运行的代码?
你像之前一样运行相同的代码。 这是需要更改的Unix或Windows内核。
你可以把我的问题总结为:“x86机器代码支持多核function有什么变化?
没有必要。 第一个SMP系统使用与单处理器完全相同的指令集。 现在,x86体系结构的演变已经有很多了,新的指令不断涌现,但是SMP没有必要 。
有关更多信息,请参阅英特尔多处理器规范 。
更新:所有后续问题都可以通过完全接受n路多核CPU几乎与n个分享相同内存的独立处理器完全相同。 2还有一个重要的问题没有被问到: 如何编写一个程序在多个核心上运行以获得更高性能? 答案是:它使用像Pthreads这样的线程库来编写。 某些线程库使用OS线程不可见的“绿色线程”,并且这些线程库不会获得单独的核心,但只要线程库使用内核线程function,则线程化程序将自动为多核。
1.为了向后兼容,只有第一个内核在复位时启动,并且需要执行一些驱动程序来启动剩余的内核。
他们也自然地分享所有的外围设备。
最小的可运行Intel x86裸机示例
可运行的裸机示例与所有需要的样板 。 所有主要部分都在下面。
testingUbuntu 15.10 QEMU 2.3.0和联想ThinkPad T400。
英特尔手册第三卷系统编程指南 – 325384-056CN 2015年9月第8章,第9章和第10章介绍了SMP。
表8-1。 “广播INIT-SIPI-SIPI序列和超时select”包含一个基本上正常工作的例子:
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)。
它必须通过称为处理器间中断(IPI)的特殊中断来唤醒其他的(称为应用处理器(AP))。
这些中断可以通过编程高级可编程中断控制器(APIC)通过中断命令寄存器(ICR)
ICR的格式logging在:10.6“发布代理程序中断”
一旦我们写信给ICR,IPI就会发生。
-
ICR_LOW在8.4.4“MP初始化示例”中定义为:
ICR_LOW EQU 0FEE00300H
魔术值
0FEE00300
是ICR的内存地址,如表10-1“本地APIC寄存器地址映射” -
在这个例子中使用了最简单的方法:它设置ICR来发送广播IPI,这些广播IPI被传送到除当前所有处理器以外的所有其他处理器。
但也有可能,也有人推荐,通过BIOS设置的特殊数据结构(如ACPI表或英特尔MPconfiguration表)获取有关处理器的信息,并且只唤醒您需要的那些。
-
000C46XXH
XX
编码处理器将执行的第一条指令的地址: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
使用链接脚本是另一种可能性。
-
延迟循环是一个烦人的工作部分:没有超级简单的方法来做这样的睡眠准确。
可能的方法包括:
- PIT(在我的例子中使用)
- HPET
- 用上面的方法校准一个忙碌循环的时间,然后用它来代替
相关: 如何在屏幕上显示一个数字,并与DOS x86程序集睡眠一秒钟?
-
我认为最初的处理器需要处于保护模式下才能工作,因为我们写地址为
0FEE00300H
,这对于16位来说太高了 -
为了在处理器之间进行通信,我们可以在主进程上使用自旋锁,并从第二个内核修改锁。
我们应该确保内存回写完成,例如通过
wbinvd
。
处理器之间共享状态
8.7.1“逻辑处理器的状态”说:
以下function是支持英特尔超线程技术的英特尔64或IA-32处理器内逻辑处理器体系结构状态的一部分。 这些function可以细分为三组:
- 重复每个逻辑处理器
- 由物理处理器中的逻辑处理器共享
- 共享或重复,取决于实施
每个逻辑处理器都具有以下function:
- 通用寄存器(EAX,EBX,ECX,EDX,ESI,EDI,ESP和EBP)
- 段寄存器(CS,DS,SS,ES,FS和GS)
- EFLAGS和EIP寄存器。 请注意,每个逻辑处理器的CS和EIP / RIP寄存器指向由逻辑处理器执行的线程的指令stream。
- x87 FPU寄存器(ST0到ST7,状态字,控制字,标签字,数据操作数指针和指令指针)
- MMX寄存器(MM0到MM7)
- XMM寄存器(XMM0到XMM7)和MXCSR寄存器
- 控制寄存器和系统表指针寄存器(GDTR,LDTR,IDTR,任务寄存器)
- debugging寄存器(DR0,DR1,DR2,DR3,DR6,DR7)和debugging控制MSR
- 机器检查全局状态(IA32_MCG_STATUS)和机器检查function(IA32_MCG_CAP)MSR
- 热时钟调制和ACPI电源pipe理控制MSR
- 时间戳计数器MSR
- 大多数其他MSR寄存器,包括页面属性表(PAT)。 请参阅下面的例外。
- 本地APIC寄存器。
- Intel 64位处理器上的其他通用寄存器(R8-R15),XMM寄存器(XMM8-XMM15),控制寄存器,IA32_EFER。
逻辑处理器共享以下function:
- 存储器types范围寄存器(MTRR)
以下function是共享的还是重复的是特定于实现的:
- IA32_MISC_ENABLE MSR(MSR地址1A0H)
- 机器检查体系结构(MCA)MSR(IA32_MCG_STATUS和IA32_MCG_CAP MSR除外)
- 性能监视控制和计数器MSR
高速caching共享的讨论如下: 多核CPU中的高速caching
英特尔超线程有更多的caching和pipe道共享比单独的核心: https : //superuser.com/questions/133082/hyper-threading-and-dual-core-whats-the-difference/995858#995858
Linux内核4.2
主要的初始化动作似乎在arch/x86/kernel/smpboot.c
。
每个Core都从不同的内存区域执行。 您的操作系统将在您的程序中指向一个核心,核心将执行您的程序。 你的程序不会意识到有多个内核或者正在执行哪个内核。
操作系统也没有附加指令。 这些核心与单核心芯片相同。 每个Core运行操作系统的一部分,该操作系统将处理与用于信息交换的公共存储区域的通信以find要执行的下一个存储区域。
这是一个简化,但它给你如何完成的基本想法。 有关 Embedded.com上的多核和多处理器的更多信息 ,请参阅有关此主题的大量信息…此主题变得非常快速!
如果您正在为多核CPU编写一个优化的编译器/字节码虚拟机,那么您需要具体了解x86,才能生成可在所有内核之间高效运行的代码?
作为编写优化编译器/字节码虚拟机的人,我可以在这里帮助你。
您不需要了解任何关于x86的具体内容,就可以生成在所有内核之间高效运行的代码。
但是,您可能需要了解cmpxchg和朋友才能编写在所有内核中正确运行的代码。 多核编程需要使用执行线程之间的同步和通信。
您可能需要了解一些有关x86的内容,以使其生成可在x86上高效运行的代码。
还有其他一些东西可以帮助你学习:
您应该了解操作系统(Linux或Windows或OSX)提供的设施,以允许您运行多个线程。 您应该了解并行化API,例如OpenMP和线程构build模块,或OSX 10.6“Snow Leopard”即将发布的“Grand Central”。
您应该考虑您的编译器是否应该自动并行化,或者如果编译器编译的应用程序的作者需要在其程序中添加特殊的语法或API调用以利用多个内核。
汇编代码将转换为将在一个内核上执行的机器代码。 如果你希望它是multithreading的,你将不得不使用操作系统原语在不同的处理器上多次启动这个代码,或者在不同的内核上启动不同的代码 – 每个内核将执行一个单独的线程。 每个线程只会看到它正在执行的一个核心。
这完全不在机器指令中完成; 内核假装是不同的CPU,并没有任何特殊的能力互相交谈。 他们有两种沟通方式:
-
他们共享物理地址空间。 硬件处理caching一致性,所以一个CPU写入另一个读取的内存地址。
-
他们共享一个APIC(可编程中断控制器)。 这是内存映射到物理地址空间,可以由一个处理器用来控制其他处理器,打开或closures它们,发送中断等。
单线程和multithreading应用程序的主要区别在于前者具有一个堆栈,后者每个线程具有一个堆栈。 由于编译器会假定数据和堆栈段寄存器(ds和ss)不相等,所以代码的生成有所不同。 这意味着通过默认为ss寄存器的ebp和esp寄存器的间接方式也不会默认为ds(因为ds!= ss)。 相反,通过其他默认为ds的寄存器间接不会默认为ss。
线程共享包括数据和代码区域在内的所有内容。 他们也共享lib例程,确保它们是线程安全的。 在RAM中sorting区域的过程可以是multithreading的,以加快速度。 然后,线程将访问,比较和sorting同一物理内存区域中的数据,并执行相同的代码,但使用不同的局部variables来控制它们各自的部分。 这当然是因为线程在包含局部variables的地方有不同的堆栈。 这种types的编程需要仔细调整代码,以便减less核心间数据冲突(在caching和RAM中),从而导致代码与两个或更multithreading相比更快。 当然,一个处理器的未调优代码通常比两个或更多的代码更快。 debugging更具挑战性,因为标准的“int 3”断点将不适用,因为您要中断特定的线程而不是所有的线程。 debugging寄存器断点不能解决这个问题,除非你可以在特定的处理器上执行你想要中断的特定线程。
其他multithreading代码可能涉及在程序的不同部分运行不同的线程。 这种types的编程不需要相同的调整,因此更容易学习。
与之前的单处理器变体相比,每个具有多处理能力的体系结构上添加的内容是在内核之间进行同步的说明。 此外,您还有处理caching一致性,刷新缓冲区以及操作系统必须处理的类似低级操作的指示。 在IBM POWER6,IBM Cell,Sun Niagara和Intel“超线程”等同步multithreading体系结构的情况下,您也倾向于看到新的指令,以便在线程之间进行优先级sorting(如设置优先级并在没有任何事情时明确产生处理器) 。
但是基本的单线程语义是一样的,你只需要添加额外的工具来处理与其他内核的同步和通信。