曾几何时,要编写 x86 汇编程序,例如,您会收到说明“加载 EDX 寄存器的值 5”、“增加 EDX 寄存器”等指令。
对于具有 4 个内核(甚至更多)的现代 CPU,在机器代码级别是否看起来像有 4 个独立的 CPU(即只有 4 个不同的“EDX”寄存器)?如果是这样,当您说“增加 EDX 寄存器”时,是什么决定了哪个 CPU 的 EDX 寄存器增加?现在 x86 汇编器中是否有“CPU 上下文”或“线程”概念?
核心之间的通信/同步如何工作?
如果您正在编写一个操作系统,通过硬件公开什么机制允许您在不同的内核上调度执行?它是一些特殊的特权指令吗?
如果您正在为多核 CPU 编写优化编译器/字节码 VM,您需要特别了解 x86 以使其生成可在所有内核上高效运行的代码?
对 x86 机器代码进行了哪些更改以支持多核功能?
这不是对问题的直接回答,而是对评论中出现的问题的回答。从本质上讲,问题是硬件为多核操作提供了什么支持,能够真正同时运行多个软件线程,而无需在它们之间进行软件上下文切换。 (有时称为 SMP system)。
Nicholas Flynt had it right,至少关于 x86。在多核环境(超线程、多核或多处理器)中,Bootstrap 内核(通常是处理器 0 中内核 0 中的硬件线程(又名逻辑内核)0)启动从地址 0xfffffff0
获取代码。所有其他内核(硬件线程)在称为 Wait-for-SIPI 的特殊睡眠状态下启动。作为其初始化的一部分,主内核通过 APIC 向 WFS 中的每个内核发送一个称为 SIPI(启动 IPI)的特殊处理器间中断 (IPI)。 SIPI 包含该内核应该开始获取代码的地址。
这种机制允许每个内核执行来自不同地址的代码。所需要的只是对每个硬件核心的软件支持,以建立自己的表和消息队列。
操作系统使用这些来执行软件任务的实际多线程调度。 (一个普通的操作系统只需要在启动时启动一次其他内核,除非你在热插拔 CPU,例如在虚拟机中。这与启动或迁移软件线程到这些内核是分开的。每个内核都在运行内核,如果没有其他事情要做,它会花时间调用 sleep 函数来等待中断。)
就实际程序集而言,正如 Nicholas 所写,单线程或多线程应用程序的程序集之间没有区别。每个内核都有自己的寄存器集(执行上下文),因此编写:
mov edx, 0
只会为当前运行的线程更新 EDX
。无法使用单个汇编指令在另一个处理器上修改 EDX
。您需要某种系统调用来要求操作系统告诉另一个线程运行将更新其自己的 EDX
的代码。
Intel x86 最小可运行裸机示例
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) 的处理器。它必须通过称为处理器间中断 (IPI) 的特殊中断唤醒其他处理器(称为应用处理器 (AP))。这些中断可以通过中断命令寄存器 (ICR) 对高级可编程中断控制器 (APIC) 进行编程来完成。ICR 的格式记录在: 10.6 “发出处理器中断” IPI 在我们写入 ICR 后立即发生。 ICR_LOW 在第 8.4.4 节“MP 初始化示例”中定义为: ICR_LOW EQU 0FEE00300H 魔术值 0FEE00300 是 ICR 的内存地址,如表 10-1“本地 APIC 寄存器地址映射”中所述 最简单的可能方法用于示例:它设置 ICR 以发送广播 IPI,这些 IPI 被传递到除当前处理器之外的所有其他处理器。但是也有可能,并且被一些人推荐,通过 BIOS 设置的特殊数据结构(如 ACPI 表或 Intel 的 MP 配置表)获取有关处理器的信息,并且只唤醒您需要的那些。 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 /* 做事。 */ hlt .equ init_len, . - init 使用链接描述文件是另一种可能性。延迟循环是一个令人讨厌的部分:没有超级简单的方法可以精确地进行这种睡眠。可能的方法包括:PIT(在我的示例中使用)HPET 使用上述校准繁忙循环的时间,并改用它相关:如何在屏幕上显示数字并使用 DOS x86 程序集休眠一秒钟?我认为初始处理器需要处于保护模式才能工作,因为我们写入地址 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)、控制寄存器、Intel 64 处理器上的 IA32_EFER。以下功能由逻辑处理器共享: 内存类型范围寄存器 (MTRR) 以下功能是否共享或复制取决于具体实现: IA32_MISC_ENABLE MSR(MSR 地址 1A0H) 机器检查架构 (MCA) MSR(IA32_MCG_STATUS 和 IA32_MCG_CAP 除外MSRs) 性能监控控制和计数器 MSRs
缓存共享在以下位置讨论:
多核 Intel CPU 中的高速缓存如何共享?
http://stackoverflow.com/questions/4802565/multiple-threads-and-cpu-cache
多个 CPU / 内核可以同时访问同一个 RAM 吗?
与单独的内核相比,英特尔超线程具有更大的缓存和管道共享: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
组装并运行:
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 参考书目可能会引起您的兴趣:
https://github.com/bztsrc/raspi3-tutorial/tree/a3f069b794aeebef633dbe1af3610784d55a0efa/02_multicorec
https://github.com/dwelch67/raspberrypi/tree/a09771a1d5a0b53d8e7a461948dc226c5467aeec/multi00
https://github.com/LdB-ECM/Raspberry-Pi/blob/3b628a2c113b3997ffdb408db03093b2953e4961/Multicore/SmartStart64.S
https://github.com/LdB-ECM/Raspberry-Pi/blob/3b628a2c113b3997ffdb408db03093b2953e4961/Multicore/SmartStart32.S
本文档提供了一些有关使用 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
x86 坑
处理内存冲突。值得注意的是,如果您想用 C 或其他高级语言进行编码,每个线程都需要一个唯一的堆栈。您可以将线程限制为具有固定的最大堆栈大小,但处理此问题的更好方法是分页,它允许有效的“无限大小”堆栈。这是一个天真的 aarch64 裸机示例,如果堆栈增长得太深,它会爆炸
这些是使用 Linux 内核或其他操作系统的一些很好的理由 :-)
用户态内存同步原语
尽管线程启动/停止/管理通常超出用户态范围,但是您可以使用来自用户态线程的汇编指令来同步内存访问,而无需潜在的更昂贵的系统调用。
您当然应该更喜欢使用可移植地包装这些低级原语的库。 C++ 标准本身在 <mutex>
和 <atomic>
标头上取得了很大进步,尤其是 std::memory_order
。我不确定它是否涵盖了所有可能实现的内存语义,但它只是可能。
更微妙的语义与 lock free data structures 的上下文特别相关,在某些情况下可以提供性能优势。要实现这些,您可能需要了解一些不同类型的内存屏障:https://preshing.com/20120710/memory-barriers-are-like-source-control-operations/
例如,Boost 在以下位置有一些无锁容器实现:https://www.boost.org/doc/libs/1_63_0/doc/html/lockfree.html
此类用户态指令似乎也用于实现 Linux futex
系统调用,这是 Linux 中的主要同步原语之一。 man futex
4.15 内容如下:
futex() 系统调用提供了一种等待某个条件成立的方法。它通常用作共享内存同步上下文中的阻塞构造。使用 futex 时,大多数同步操作都是在用户空间中执行的。用户空间程序仅在程序可能必须阻塞更长的时间直到条件变为真时才使用 futex() 系统调用。其他 futex() 操作可用于唤醒任何等待特定条件的进程或线程。
系统调用名称本身的意思是“快速用户空间 XXX”。
这是一个带有内联汇编的最小无用 C++ x86_64 / aarch64 示例,它说明了此类指令的基本用法,主要是为了好玩:
主文件
#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
从这里我们看到 x86 LOCK 前缀 / aarch64 LDADD
指令使加法成为原子:没有它,我们在许多加法上都有竞争条件,最后的总计数小于同步的 20000。
也可以看看:
x86 LOCK x86 汇编中的“lock”指令是什么意思? PAUSE x86 pause 指令如何在自旋锁中工作*并且*它可以在其他场景中使用吗?
LOCK x86 汇编中的“lock”指令是什么意思?
PAUSE x86 pause 指令如何在自旋锁中工作*并且*它可以在其他场景中使用吗?
ARM LDXR/STXR, LDAXR/STLXR: ARM64: LDXR/STXR vs LDAXR/STLXR LDADD 和其他原子 v8.1 加载修改存储指令:http://infocenter.arm.com/help/index.jsp?topic=/com .arm.doc.dui0801g/alc1476202791033.html WFE / SVE:ARM 中的 WFE 指令处理
LDXR/STXR、LDXR/STLXR:ARM64:LDXR/STXR 与 LDAXR/STLXR
LDADD 和其他原子 v8.1 加载修改存储说明:http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.dui0801g/alc1476202791033.html
WFE / SVE:ARM 中的 WFE 指令处理
std::atomic 到底是什么?
在 Ubuntu 19.04 amd64 和 QEMU aarch64 用户模式下测试。
#include
(将其作为评论),NASM、FASM、YASM 不知道 AT&T 语法,所以不可能是它们……那是什么?
gcc
, #include
来自 C 预处理器。按照入门部分中的说明使用提供的 Makefile
:github.com/cirosantilli/x86-bare-metal-examples/blob/… 如果这不起作用,请打开 GitHub 问题。
据我了解,每个“核心”都是一个完整的处理器,有自己的寄存器集。基本上,BIOS 以一个内核运行开始,然后操作系统可以通过初始化其他内核并将它们指向要运行的代码等来“启动”其他内核。
同步由操作系统完成。通常,每个处理器为操作系统运行不同的进程,因此操作系统的多线程功能负责决定哪个进程访问哪个内存,以及在内存冲突的情况下该怎么做。
非官方 SMP 常见问题解答
曾几何时,要编写 x86 汇编程序,例如,您会收到说明“将 EDX 寄存器的值加载为 5”、“增加 EDX”寄存器等指令。使用具有 4 个内核(甚至更多)的现代 CPU ,在机器代码级别,它只是看起来像有 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
1
n
2
如何编写一个程序以在多个内核上运行以获得更高的性能?
线程。
1. 为了向后兼容,只有第一个核心在复位时启动,并且需要做一些驱动类型的事情来启动其余的。 2. 他们自然也共享所有外围设备。
如果您正在为多核 CPU 编写优化编译器/字节码 VM,您需要特别了解 x86 以使其生成可在所有内核上高效运行的代码?
作为编写优化编译器/字节码虚拟机的人,我可以在这里为您提供帮助。
您无需了解任何有关 x86 的具体知识,即可生成可在所有内核上高效运行的代码。
但是,您可能需要了解 cmpxchg 和朋友,才能编写在所有内核上正确运行的代码。多核编程需要在执行线程之间使用同步和通信。
您可能需要了解一些关于 x86 的知识,以使其生成通常在 x86 上高效运行的代码。
还有其他一些对你有用的东西:
您应该了解操作系统(Linux 或 Windows 或 OSX)提供的允许您运行多个线程的设施。您应该了解并行化 API,例如 OpenMP 和 Threading Building Blocks,或 OSX 10.6 “Snow Leopard”即将推出的“Grand Central”。
您应该考虑您的编译器是否应该自动并行化,或者由您的编译器编译的应用程序的作者是否需要在他的程序中添加特殊语法或 API 调用以利用多个内核。
每个核心从不同的内存区域执行。您的操作系统会将内核指向您的程序,然后内核将执行您的程序。您的程序不会知道有多个内核或它正在执行哪个内核。
也没有仅适用于操作系统的附加指令。这些内核与单核芯片相同。每个核心运行操作系统的一部分,它将处理与用于信息交换的公共内存区域的通信,以找到下一个要执行的内存区域。
这是一个简化,但它为您提供了如何完成的基本概念。 Embedded.com 上的 More about multicores and multiprocessors 有很多关于这个主题的信息......这个主题很快就会变得复杂!
汇编代码将转换为将在一个内核上执行的机器代码。如果您希望它是多线程的,您将不得不使用操作系统原语在不同的处理器上多次启动此代码,或者在不同的内核上启动不同的代码 - 每个内核将执行一个单独的线程。每个线程只会看到它当前正在执行的一个内核。
它根本不是在机器指令中完成的。这些内核假装是不同的 CPU,并且没有任何特殊的相互交谈的能力。他们有两种沟通方式:
它们共享物理地址空间。硬件处理缓存一致性,因此一个 CPU 写入另一个 CPU 读取的内存地址。
它们共享一个 APIC(可编程中断控制器)。这是映射到物理地址空间的内存,一个处理器可以使用它来控制其他处理器,打开或关闭它们,发送中断等。
http://www.cheesecake.org/sac/smp.html 是一个很好的参考,带有一个愚蠢的 url。
单线程和多线程应用程序的主要区别在于前者有一个堆栈,而后者每个线程都有一个堆栈。由于编译器将假定数据和堆栈段寄存器(ds 和 ss)不相等,因此生成的代码略有不同。这意味着通过默认为 ss 寄存器的 ebp 和 esp 寄存器的间接寻址不会也默认为 ds(因为 ds!=ss)。相反,通过默认为 ds 的其他寄存器的间接寻址不会默认为 ss。
线程共享其他所有内容,包括数据和代码区域。它们还共享 lib 例程,因此请确保它们是线程安全的。对 RAM 中的区域进行排序的过程可以是多线程的以加快速度。然后线程将访问、比较和排序同一物理内存区域中的数据,并执行相同的代码,但使用不同的局部变量来控制它们各自的排序部分。这当然是因为线程有不同的堆栈,其中包含局部变量。这种类型的编程需要仔细调整代码,以减少内核间数据冲突(在高速缓存和 RAM 中),这反过来导致使用两个或更多线程的代码比仅使用一个线程更快。当然,未经调整的代码使用一个处理器通常会比使用两个或更多处理器更快。调试更具挑战性,因为标准的“int 3”断点将不适用,因为您想中断特定线程而不是所有线程。调试寄存器断点也不能解决这个问题,除非您可以在执行您要中断的特定线程的特定处理器上设置它们。
其他多线程代码可能涉及在程序的不同部分运行的不同线程。这种类型的编程不需要相同类型的调整,因此更容易学习。
我认为提问者可能希望通过让多个内核并行工作来使程序运行得更快。无论如何,这就是我想要的,但所有的答案都让我不明智。但是,我想我明白了:您无法将不同的线程同步到指令执行时间的准确性。因此,您无法让 4 个内核并行地对四个不同的数组元素进行乘法运算以将处理速度提高 4:1。相反,您必须将程序视为包含按顺序执行的主要块,例如
对某些数据进行 FFT 将结果放入矩阵中并找到它的特征值和特征向量 按特征值对后者进行排序,从第一步开始重复使用新数据
您可以做的是对第 1 步的结果运行第 2 步,同时在不同核心中对新数据运行第 1 步,并在第 2 步对下一个数据和步骤运行时在不同核心中对第 2 步的结果运行第 3 步1 之后在数据上运行。您可以在 Compaq Visual Fortran 和 Intel Fortran 中执行此操作,这是 CVF 的演变,通过为三个步骤编写三个单独的程序/子例程,而不是一个“调用”下一个它调用 API 来启动其线程。他们可以通过使用 COMMON 共享数据,这将是所有线程的 COMMON 数据内存。您必须研究手册直到头疼并进行试验,直到使它起作用,但我至少成功了一次。
与之前的单处理器变体相比,每个支持多处理的架构都添加了在内核之间同步的指令。此外,您还有处理缓存一致性、刷新缓冲区和操作系统必须处理的类似低级操作的指令。在同时多线程架构(如 IBM POWER6、IBM Cell、Sun Niagara 和英特尔“超线程”)的情况下,您还倾向于看到新指令来确定线程之间的优先级(例如设置优先级并在无事可做时显式让出处理器) .
但是基本的单线程语义是相同的,您只需添加额外的工具来处理与其他内核的同步和通信。
不定期副业成功案例分享