assembly - 如何使用 APIC 创建 IPI 以唤醒 x86 程序集中的 SMP 的 AP?

标签 assembly x86 intel smp

在启动后环境(无操作系统)中,如何使用 BSP(第一核/处理器)为 AP(所有其他核/处理器)创建 IPI?本质上,当从一个内核开始时,如何唤醒并为其他内核设置指令指针?

最佳答案

警告:我在这里假设是 80x86。如果不是 80x86 那么我不知道 :-)

首先需要找出其他CPU的数量以及它们的APIC ID是什么,并确定本地APIC的物理地址。为此,您需要解析 ACPI 表(请参阅 ACPI 规范中的 MADT/APIC)。如果您找不到有效的 ACPI 表(例如计算机太旧),则有一个较旧的“多处理器规范”定义了自己的表,其中包含相同的信息。请注意,现在不推荐使用“多处理器规范”(并且有些计算机带有虚拟多处理器表),这就是您需要首先检查 ACPI 表的原因。

下一步是确定您拥有什么类型的本地 APIC。有 3 种情况 - 旧的外部“82489DX”本地 APIC(未内置于 CPU 本身)、xAPIC 和 x2APIC。

首先检查 CPUID 以确定本地 APIC 是否为 x2APIC。如果是,您有 2 个选择 - 您可以使用 x2APIC,或者您可以使用“xAPIC 兼容模式”。对于“xAPIC 兼容模式”,您只能使用 8 位 APIC ID,并且无法支持具有大量 CPU(例如 255 个或更多 CPU)的计算机。我建议使用 x2APIC(即使您不关心具有大量 CPU 的计算机),因为它的速度更快。如果您确实使用 x2APIC 模式,那么您需要将本地 APIC 切换到此模式。

否则,如果不是 x2APIC,则读取本地 APIC 的版本寄存器。如果本地 APIC 的版本是 0x10 或更高,则它的 xAPIC,如果它是 0x0F 或更低,那么它是外部“82489DX”本地 APIC。

旧的外部“82489DX”本地 APIC 用于 80486 和更旧的计算机,这些非常罕见(它们在 20 年前非常罕见,然后大多数都死了和/或被替换并丢弃了)。因为使用不同的序列来启动其他 CPU,并且因为具有这些本地 APIC 的计算机非常罕见(例如,您可能永远无法测试您的代码),所以不费心支持这些计算机是很有意义的。如果您完全支持这些旧计算机;如果本地 APIC 是“82489DX”,我建议将它们视为“仅单 CPU”,并且根本不启动任何其他 CPU。出于这个原因,我不会在这里描述用于启动它们的方法(如果你好奇的话,它在英特尔的“多进程规范”中有描述)。

对于 xAPIC 和 x2APIC,启动另一个 CPU 的顺序本质上是相同的(只是访问本地 APIC 的方式不同 - MSR 或内存映射)。我建议使用(例如)函数指针来隐藏这些差异;以便以后的代码可以通过调用“发送IPI”函数。函数指针而不关心本地 APIC 是 x2APIC 还是 xAPIC。

要真正启动另一个 CPU,您需要向它发送一系列 IPI(处理器间中断)。英特尔的方法是这样的:

Send an INIT IPI to the CPU you're starting
Wait for 10 ms
Send a STARTUP IPI to the CPU you're starting
Wait for 200 us
Send another STARTUP IPI to the CPU you're starting
Wait for 200 us
Wait for started CPU to set a flag (so you know it started)
    If flag was set by other CPU, other CPU was started successfully
    Else if time-out, other CPU failed to start

英特尔的方法有两个问题。通常,另一个 CPU 会由第一个 STARTUP IPI 启动,在某些情况下,这可能会导致问题(例如,如果另一个 CPU 的启动代码执行类似 total_CPUs++; 的操作,那么每个 CPU 可能会执行两次。要避免此问题,您可以添加额外的同步(例如,其他 CPU 在继续之前等待第一个 CPU 设置“我知道您已启动”标志)。英特尔方法的第二个问题是测量这些延迟。通常,操作系统启动其他 CPU,然后计算弄清楚 CPU 支持哪些功能以及之后存在哪些硬件,并且没有精确的计时器设置来准确测量那些 200 us 的延迟。

避免这些问题;我使用了一种替代方法,如下所示:
Send an INIT IPI to the CPU you're starting
Wait for 10 ms
Send a STARTUP IPI to the CPU you're starting
Wait for started CPU to set a flag (so you know it started) with a short timeout (e.g. 1 ms)
    If flag was set by other CPU, other CPU was started successfully
    Else if time-out
        Send another STARTUP IPI to the CPU you're starting
        Wait for started CPU to set a flag with a long timeout (e.g. 200 ms)
            If flag was set by other CPU, other CPU was started successfully
            Else if time-out, other CPU failed to start
If CPU started successfully
    Set flag to tell other CPU it can continue

另请注意,您需要单独启动 CPU。我见过人们同时启动所有 CPU,使用“将 IPI 广播给除自身之外的所有人”功能 - 这是错误的、损坏的和狡猾的(除非您正在编写固件,否则不要这样做)。这样做的问题是某些 CPU 可能有问题(例如,其 BIST/内置自检失败)并且某些 CPU 可能被禁用(例如,当固件中禁用超线程时,超线程);并且“将 IPI 广播给除自身之外的所有人”方法可以启动不应该启动的 CPU。

最后,对于具有大量 CPU 的计算机,如果您一次启动一个,则启动它们可能需要相对较长的时间。例如,如果启动每个 CPU 需要 11 毫秒,并且有 128 个 CPU,则需要 1.4 秒。如果您想快速启动,有一些方法可以避免这种情况。例如,第一个CPU可以启动第二个CPU,然后第1和第2个CPU可以启动第3和第4个CPU,然后这四个CPU可以启动接下来的四个CPU,依此类推,这样可以在77毫秒内启动128个CPU而不是 1.4 秒。

注意:我建议一次只启动一个 CPU,并在尝试任何类型的“并行启动”之前确保它可以工作(这是你在知道其余部分工作后可以担心的事情)。

其他 CPU/s 将开始执行的地址编码在 STARTUP IPI 的“向量”字段中。 CPU 将开始执行代码(在实模式下) CS = vector * 256IP = 0 .矢量字段是 8 位的,因此您可以使用的最高起始地址是 0x000FF000(实模式下为 0xFF00:0x0000)。然而,这是传统的 ROM 区域(实际上起始地址必须更低)。通常,您会将一小段启动代码复制到合适的地址中;启动代码处理同步(例如设置另一个 CPU 可以看到的“我开始”标志并等待被告知可以继续),然后执行诸如启用保护/长模式和在跳转到条目之前设置堆栈之类的事情指向操作系统的正常代码。这段启动代码叫做“AP CPU启动蹦床”。这也是让“并行启动”有点复杂的原因;因为每个正在启动的 CPU 都需要自己/单独的同步标志和堆栈;并且因为这些东西通常是用蹦床中的变量实现的(例如 mov esp,[cs:stackTop] ),这意味着最终会有多个蹦床。

关于assembly - 如何使用 APIC 创建 IPI 以唤醒 x86 程序集中的 SMP 的 AP?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/16364817/

相关文章:

assembly - x86 汇编语言中跳转到特定行

windows - 在调用 asm 函数之前在 C 中调用 printf 或不调用的神秘副作用?

c - GCC 汇编内联 : Function Body with Only Inlined Assembly Code

c - 包含在内联汇编中

assembly - 如何从多个 .asm 文件计算 TSR block 的总大小?

assembly - INT %ebx 出了什么问题?

c - C 内联汇编中的 PCLMULQDQ 指令

visual-c++ - 对于 VisualC++ 开发,Intel i7(4 核,8 个基于 HT 的逻辑核心)是否比 Intel Core 2 Quad 更好?

c++ - 我怎样才能创建一个可定制的仿函数接受 sycl 内核?

assembly - 使用 QtSpim 时,我在哪里可以看到程序输出以及在哪里可以输入值?