曾几何时,写x86汇编,例如,你将有指示,指出“加载EDX与价值5寄存器”,“递增EDX”登记,等等。
与拥有4个核心(甚至更多)现代的CPU,在机器代码级别它只是看起来像有4级独立的CPU(即在那里只是4个不同的“EDX”寄存器)?如果是这样,当你说“递增EDX注册”,是什么决定了CPU的EDX寄存器会增加吗?是否有x86汇编了“CPU上下文”或“线程”的概念呢?
如何在内核之间的通信/同步的工作吗?
如果你正在写一个操作系统,什么样的机制是通过硬件暴露,让您安排在不同内核上执行?它是一些特殊的priviledged指令(S)?
如果你正为多核CPU编写优化的编译器/字节码虚拟机,你会需要知道具体一下,比方说,86使它产生在所有核高效运行的代码?
什么样的变化已对x86机器代码,以支持多核功能?
这是不是直接回答这个问题,但它是一个问题的答案出现在评论的问题。从本质上讲,问题是什么支持硬件为多线程操作。
Nicholas Flynt had it right,至少对于86。在多线程环境(超线程,多核或多处理器),自举线程启动从地址0xfffffff0
取代码(通常是在核心0在处理器0线程0)。所有其他线程在一个特殊的睡眠状态启动,所谓的等待换SIPI。作为其初始化的一部分,主线程发送一个特殊的处理器间中断(IPI)在称为SIPI(启动IPI)到在WFS每个线程APIC。该SIPI包含来自该线程应该开始获取代码的地址。
这种机制允许每个线程从不同的地址执行代码。所有这一切需要的是每个线程建立了自己的表和邮件队列的软件支持。操作系统使用这些做实际的多线程调度。
至于实际装配而言,尼古拉斯写道,有一个单线程还是多线程应用程序的组件之间没有什么区别。每个逻辑线程都有自己的寄存器组,所以写:
mov edx, 0
将只更新EDX
当前运行的线程。有没有办法修改EDX
上使用单个汇编指令另一个处理器。你需要某种形式的系统调用的询问OS来告诉另一个线程来运行代码,将更新自己的EDX
。
什么已经在每一个多重功能的架构添加和他们相比,之前传出单处理器变种的指令内核之间的同步。此外,你必须说明处理高速缓存一致性,刷新缓冲区,以及类似的低级别操作的操作系统必须处理。在同时多线程架构,如IBM POWER6,IBM CELL,孙加拉,以及英特尔“超线程”的情况下,还往往会看到新的指令线程(比如设置优先级和明确屈服处理器时,没有什么可以做的)之间优先。
但基本的单线程语义是一样的,你只需要添加额外的设备来处理同步,并与其他内核通信。
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覆盖SMP中的章节8,9和10。
表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
该代码:
ICR_LOW EQU 0FEE00300H
魔法值0FEE00300
是ICR的内存地址,如表10-1记录“本地APIC注册地址映射”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
使用链接脚本是另一种可能性。0FEE00300H
这是太高了16位工作wbinvd
。8.7.1“逻辑处理器的状态”说:
以下特征是逻辑处理器内英特尔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在Intel 64处理器。
以下特征是通过逻辑处理器共享:
- 存储器类型范围寄存器(MTRRs)
无论以下功能共享或复制是实现特定的:
- 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退出解除对自旋锁。
自旋锁后,CPU 0,则做了semihost exit call这使得QEMU退出。
如果只用一个与-smp 1
CPU启动QEMU,然后模拟只是永远挂在自旋锁。
ARM: Start/Wakeup/Bringup the other CPU cores/APs and pass execution start address?:CPU 1在被唤醒与PSCI接口,更多的细节
该upstream version也有一些调整,使其在gem5工作,这样你就可以运行特性试验也是如此。
我没有测试它在真实的硬件,所以我不知道如何便携本是。下面树莓派参考书目可能会感兴趣:
本文提供有关使用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。
前面的例子中醒来次级CPU并且不与专用指令,这是一个良好的开端基本存储器的同步。
但是,为了使多核系统易于编程,例如像POSIX pthreads
,您还需要进入下面的更复杂的主题:
pthread_yield
实现),它越来越难以平衡工作负载来修改你的代码。
下面是一些简单的裸机计时器的例子:
x86 PIT这些都是使用Linux内核或某些其他操作系统:-)一些很好的理由
虽然线程启动/停止/管理一般都超出了用户空间范围,您可以使用,无论从用户级线程的汇编指令同步内存访问没有可能更昂贵的系统调用。
当然,你应该更喜欢使用可移植包装这些低级原语库。 C ++标准本身对<atomic>
头取得了很大的进步,特别是与std::memory_order
。我不知道,如果它涵盖了所有可能的内存语义可以实现的,但它只是可能。
更微妙的语义在lock free data structures的情况下,可提供在某些情况下的性能优势尤为重要。要实现这些,你可能有机会了解不同类型的内存屏障了一下:https://preshing.com/20120710/memory-barriers-are-like-source-control-operations/
升压例如有一些无锁容器实现的:https://www.boost.org/doc/libs/1_63_0/doc/html/lockfree.html
这里是一个最小的无用C ++ x86_64的/内联组件aarch64示例示出的大多为乐趣这样的指令基本用法:
main.cpp中
#include <atomic>
#include <cassert>
#include <iostream>
#include <thread>
#include <vector>
std::atomic_ulong my_atomic_ulong(0);
unsigned long my_non_atomic_ulong = 0;
#if defined(__x86_64__) || defined(__aarch64__)
unsigned long my_arch_atomic_ulong = 0;
unsigned long my_arch_non_atomic_ulong = 0;
#endif
size_t niters;
void threadMain() {
for (size_t i = 0; i < niters; ++i) {
my_atomic_ulong++;
my_non_atomic_ulong++;
#if defined(__x86_64__)
__asm__ __volatile__ (
"incq %0;"
: "+m" (my_arch_non_atomic_ulong)
:
:
);
// https://github.com/cirosantilli/linux-kernel-module-cheat#x86-lock-prefix
__asm__ __volatile__ (
"lock;"
"incq %0;"
: "+m" (my_arch_atomic_ulong)
:
:
);
#elif defined(__aarch64__)
__asm__ __volatile__ (
"add %0, %0, 1;"
: "+r" (my_arch_non_atomic_ulong)
:
:
);
// https://github.com/cirosantilli/linux-kernel-module-cheat#arm-lse
__asm__ __volatile__ (
"ldadd %[inc], xzr, [%[addr]];"
: "=m" (my_arch_atomic_ulong)
: [inc] "r" (1),
[addr] "r" (&my_arch_atomic_ulong)
:
);
#endif
}
}
int main(int argc, char **argv) {
size_t nthreads;
if (argc > 1) {
nthreads = std::stoull(argv[1], NULL, 0);
} else {
nthreads = 2;
}
if (argc > 2) {
niters = std::stoull(argv[2], NULL, 0);
} else {
niters = 10000;
}
std::vector<std::thread> threads(nthreads);
for (size_t i = 0; i < nthreads; ++i)
threads[i] = std::thread(threadMain);
for (size_t i = 0; i < nthreads; ++i)
threads[i].join();
assert(my_atomic_ulong.load() == nthreads * niters);
// We can also use the atomics direclty through `operator T` conversion.
assert(my_atomic_ulong == my_atomic_ulong.load());
std::cout << "my_non_atomic_ulong " << my_non_atomic_ulong << std::endl;
#if defined(__x86_64__) || defined(__aarch64__)
assert(my_arch_atomic_ulong == nthreads * niters);
std::cout << "my_arch_non_atomic_ulong " << my_arch_non_atomic_ulong << std::endl;
#endif
}
可能的输出:
my_non_atomic_ulong 15264
my_arch_non_atomic_ulong 15267
由此我们看到,86 LOCK前缀/ aarch64 LDADD
指令进行加法原子:没有它,我们有很多的加赛条件,并在年底的总数小于同步20000。
参见:What does the "lock" instruction mean in x86 assembly?
经测试在Ubuntu 19.04 amd64和与QEMU aarch64用户模式。
据我了解,每个“核心”是一个完整的处理器,拥有自己的寄存器组。基本上,BIOS开始你关闭一个内核上运行,然后操作系统可以通过初始化它们,并在代码指着他们跑,等“启动”其他核心
同步是由操作系统来完成。通常,每个处理器运行一个操作系统的不同工艺,所以在操作系统的多线程功能,负责决定的,其过程变得触及到内存,什么在存储碰撞的情况下做的。
究竟。有4组寄存器,其中包括4个单独的指令指针。
如果是这样,当你说“递增EDX注册”,是什么决定了CPU的EDX寄存器会增加吗?
该执行的指令,自然的CPU。把它看成是4个完全不同的微处理器被简单地共享相同的存储。
是否有x86汇编了“CPU上下文”或“线程”的概念呢?
号汇编器只是将说明喜欢它总是这样。没有变更。
如何在内核之间的通信/同步的工作吗?
由于它们共享相同的存储,它主要是程序逻辑的问题。虽然现在是一个inter-processor interrupt机制,这是没有必要的,最初没有出现在首批双CPU的x86系统。
如果你正在写一个操作系统,什么样的机制是通过硬件暴露,让您安排在不同内核上执行?
调度实际上并没有改变,但它是稍微仔细思考的关键部分和所使用的类型的锁。 SMP之前,内核代码最终会调用调度程序,这将看运行队列,并选择一个过程,作为下一个线程中运行。 (进程内核看上去很像线程)SMP内核运行完全相同的代码,每次一个线程,它只是现在关键部分锁定需要是SMP安全,以确保两个内核可以不小心挑相同的PID。
它是一些特殊的特权指令(S)?
号的核心只是都在用相同的旧指令相同的内存中运行。
如果你正为多核CPU编写优化的编译器/字节码虚拟机,你会需要知道具体一下,比方说,86使它产生在所有核高效运行的代码?
你像以前一样运行相同的代码。这是Unix或Windows内核在需要改变。
如“什么样的变化已对x86机器代码,以支持多核功能?”你可以总结一下我的问题
什么是必要的。第一SMP系统中使用的完全相同的指令设定为单处理器。现在,出现了x86架构的演变和新指令不计其数很大,使事情更快,但没有一个是必需的SMP。
欲了解更多信息,请参阅Intel Multiprocessor Specification。
如果你正为多核CPU编写优化的编译器/字节码虚拟机,你会需要知道具体一下,比方说,86使它产生在所有核高效运行的代码?
正如有人谁写优化的编译器/字节码的虚拟机可能是我能帮助你在这里。
你不需要知道具体是什么约86使它产生在所有核高效运行的代码。
但是,您可能需要了解CMPXCHG和朋友,以编写可以跨所有核心正确运行的代码。多核编程需要使用的执行线程之间的同步和通信的。
您可能需要了解一些关于86,使其产生高效运行在x86一般的代码。
还有其他的事情你学习这将是有益的:
你应该了解的OS(Linux或Windows或OSX)提供了允许您运行多个线程的设施。你应该了解并行化API,如OpenMP和线程构建模块,或OSX 10.6“雪豹”的即将出台的‘中央车站’。
你应该考虑,如果你的编译器应该是自动parallelising,或者如果你的编译器编译的应用程序的作者需要添加特殊的语法或API调用到他的节目采取多核心的优势。
每个核心从不同的内存区域执行。您的操作系统将在你的程序指向一个核心和核心将执行程序。你的程序将不知道,有一个以上的核心或在哪个内核正在执行。
也没有额外的指令只适用于操作系统。这些核是相同的单核芯片。每个核心运行将处理通信,以用于信息交换去寻找下一个内存区域执行常见的内存区域的操作系统的一部分。
这是一种简化,但它给你的是如何做的基本理念。在Embedded.com More about multicores and multiprocessors有很多关于这个主题的信息...这个话题就变得复杂非常快!
汇编代码将转化为将在一个核心中执行的机器代码。如果你希望它是多线程的,你将不得不使用的操作系统的原语开始在不同处理器上几次或不同的片上不同的内核代码的代码 - 每个内核将执行一个单独的线程。每个线程只能看到它正在执行一个内核上。
这不是在所有的机器指令完成的;核心假装是不同的CPU和不具备任何特殊能力的相互交谈。有两种沟通,他们的方法:
http://www.cheesecake.org/sac/smp.html是一个愚蠢的URL一个很好的参考。
单和多线程应用程序之间的主要区别在于,前者具有一个堆栈,后者具有一个用于每个线程。代码是稍有不同,因为编译器将假设数据和堆栈段寄存器(DS和ss)是不相等的生成。这意味着通过EBP该间接和ESP登记该默认到SS寄存器将不会也默认为DS(由于DS!= SS)。相反地,间接通过其他寄存器,其默认为DS不会默认为SS。
线程共享一切,包括数据和代码区。他们还分享LIB例程,以便确保它们是线程安全的。进行排序RAM中的区域可以是一个程序的多线程,以加快速度。然后线程将是访问,比较和在相同的物理存储器区域订货数据和执行相同的代码,但使用不同的局部变量,以控制其各自的排序的一部分。这当然是因为线程具有其中的局部变量包含不同的堆栈。这种类型的编程需要的代码,使得核心间的数据冲突(在高速缓存和RAM)被减小,这在其与两个或多个线程快的码又导致比它只有一个的仔细调谐。当然,一个非调谐码通常是与一个处理器速度快于具有两个或更多。为了调试是更具挑战性,因为标准“INT 3”断点不会因为要中断特定线程,而不是所有的人都适用。调试寄存器断点不解决这个问题,或者,除非你可以将它们在执行要中断线程特定的特定处理器上。
其它多线程代码可以包括在程序的不同部分运行的不同线程。这种类型的编程不需要同一种调整的,因此更容易学习。