7

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

4

1 回答 1

12

警告:我在这里假设为 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 的计算机极为罕见(例如,您可能永远无法测试您的代码),所以不费心支持这些计算机是很有意义的。如果您完全支持这些旧计算机;我建议将它们视为“仅单 CPU”,如果本地 APIC 为“82489DX”,则根本不启动任何其他 CPU/s。出于这个原因,我不会在这里描述用于启动它们的方法(如果你好奇,它在英特尔的“多进程规范”中有描述)。

对于 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 微秒的延迟。

避免这些问题;我使用另一种方法,如下所示:

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。我见过人们使用“向除自己之外的所有人广播 IPI”功能同时启动所有 CPU——这是错误的、损坏的和狡猾的(除非你正在编写固件,否则不要这样做)。这样做的问题是某些 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]) 这意味着最终会有多个蹦床。

于 2013-05-03T21:48:27.100 回答