SIPI 能否以长模式从 BSP 运行 发送?

Can SIPI be sent from a BSP running in long mode?

目前我有一个在 x86 保护模式下的多处理操作系统 运行ning,我想让它 运行 在 x86_64 长模式下。它当前唤醒 AP 的逻辑是发送 SIPI-INIT-INIT:

// BSP already entered protected mode, set up page tables
uint32_t *icr = 0xfee00300;
*icr = 0x000c4500ul;        // send INIT
delay_us(10000);            // delay for 10 ms
while (*icr & 0x1000);      // wait until Send Pending bit is clear
for (int i = 0; i < 2; i++) {
    *icr = 0x000c4610ul;    // send SIPI
    delay_us(200);          // delay for 200 us
    while (*icr & 0x1000);  // wait until Send Pending bit is clear
}

该程序在 32 位保护模式下运行良好。

但是,我将操作系统修改为运行 64位长模式后,发送SIPI时逻辑就断了。在 QEMU 中,在执行 send SIPI 行之后,BSP 立即被重置(程序计数器变为 0xfff0)。

在 Intel 的软件开发人员手册第 3 卷第 8.4.4.1 节(典型的 BSP 初始化序列)中,它说 BSP 应该“切换到保护模式”。这个过程是否适用于长模式?我该如何调试这个问题?

这里有一些调试信息,如果有帮助的话:

CPU 在 64 位长模式下发送 SIPI 指令(movl [=15=]xc4610,(%rax))之前注册:

rax            0xfee00300          4276093696
rbx            0x40                64
rcx            0x0                 0
rdx            0x61                97
rsi            0x61                97
rdi            0x0                 0
rbp            0x1996ff78          0x1996ff78
rsp            0x1996ff38          0x1996ff38
r8             0x1996ff28          429326120
r9             0x2                 2
r10            0x0                 0
r11            0x0                 0
r12            0x0                 0
r13            0x0                 0
r14            0x0                 0
r15            0x0                 0
rip            0x1020d615          0x1020d615
eflags         0x97                [ IOPL=0 SF AF PF CF ]
cs             0x10                16
ss             0x18                24
ds             0x18                24
es             0x18                24
fs             0x18                24
gs             0x18                24
fs_base        0x0                 0
gs_base        0x0                 0
k_gs_base      0x0                 0
cr0            0x80000011          [ PG ET PE ]
cr2            0x0                 0
cr3            0x19948000          [ PDBR=12 PCID=0 ]
cr4            0x20                [ PAE ]
cr8            0x0                 0
efer           0x500               [ LMA LME ]
mxcsr          0x1f80              [ IM DM ZM OM UM PM ]

CPU在32位保护模式下发送SIPI指令(movl [=16=]xc4610,(%eax))前寄存器:

rax            0xfee00300          4276093696
rbx            0x40000             262144
rcx            0x0                 0
rdx            0x61                97
rsi            0x2                 2
rdi            0x102110eb          270602475
rbp            0x19968f4c          0x19968f4c
rsp            0x19968f04          0x19968f04
r8             0x0                 0
r9             0x0                 0
r10            0x0                 0
r11            0x0                 0
r12            0x0                 0
r13            0x0                 0
r14            0x0                 0
r15            0x0                 0
rip            0x1020d075          0x1020d075
eflags         0x97                [ IOPL=0 SF AF PF CF ]
cs             0x8                 8
ss             0x10                16
ds             0x10                16
es             0x10                16
fs             0x10                16
gs             0x10                16
fs_base        0x0                 0
gs_base        0x0                 0
k_gs_base      0x0                 0
cr0            0x80000015          [ PG ET EM PE ]
cr2            0x0                 0
cr3            0x19942000          [ PDBR=12 PCID=0 ]
cr4            0x30                [ PAE PSE ]
cr8            0x0                 0
efer           0x0                 [ ]
mxcsr          0x1f80              [ IM DM ZM OM UM PM ]

Can SIPI be sent from a BSP running in long mode?

是的。唯一重要的是您将正确的值写入正确的本地 APIC 寄存器(具有正确的延迟,有点——请参阅最后我的方法)。

However, after I modified the operating system to run in 64-bit long mode, the logic breaks when sending SIPI. In QEMU, immediately after executing the send SIPI line, the BSP is reset (program counter goes to 0xfff0).

我假设:

a) 有一个错误,本地 APIC 的寄存器地址不正确;当您尝试写入本地 APIC 的寄存器时导致三重故障。不要忘记长模式必须使用分页,即使 0xFEE00300 可能是正确的物理地址,它也可能是错误的虚拟地址(除非你在移植 OS 到长模式)。

b) 由于一些难以想象的原因,数据不正确,导致 SIPI 重新启动 BSP。

In Intel's software developer's manual volume 3, section 8.4.4.1 (Typical BSP Initialization Sequence), it says that BSP should "Switches to protected mode". Does this process apply to long mode?

英特尔的“典型 BSP 初始化序列”只是一个可能的示例,仅供固件开发人员使用。请注意,“供固件开发人员使用”意味着它不应被任何 OS.

使用

Intel 示例的主要问题是它将 INIT-SIPI-SIPI 序列广播到所有其他 CPUs(可能包括 CPUs,固件因故障而禁用,并且可能包括固件因其他原因禁用的 CPUs - 例如,因为用户禁用了超线程);并且未能检测到“CPU 存在但由于某种原因未能启动”(OS 应该向用户报告)。

另一个问题是,通常 OS 会希望在启动每个 AP 之前为每个 AP 预先分配一个堆栈(并在启动 AP 之前在某处存储“您应该用于堆栈的地址”),并且如果您同时启动未知数量的 CPU,则不能像那样为每个 AP 提供自己的堆栈。

本质上;固件使用(类似于)英特尔描述的示例,然后在“ACPI/MADT”ACPI table(and/or 非常旧的计算机的“多处理器规范 table”中构建信息 -它现在已经过时了)供 OS 使用;并且 OS 使用来自固件 table/s 的信息以正确的方式(供应商和平台中立)找到本地 APIC 的物理地址,并且只找到固件的 CPUs说是有效的,并确定那些 CPU/s 是否正在使用“本地 APIC”或“X2APIC”(它支持超过 256 个 APIC ID,并且在 CPU 数量巨大的情况下是必需的);然后在使用超时时一次只启动有效 CPUs 以便“CPU #123,我有证据存在,未能启动”可以报告给用户 and/or 记录。

我还应该指出,Intel 的示例在 Intel 的手册中已经存在大约 25 年(自引入 long 模式之前)几乎没有变化。

我的方法

英特尔算法的延迟很烦人,通常 CPU 会在第一个 SIPI 上启动,有时第二个 SIPI 会导致相同的 CPU 启动两次(如果您在 AP 启动代码中有任何类型的“started_CPUs++;”。

为了解决这些问题(并提高性能),AP 启动代码可以设置一个“我开始”标志,而不是在发送第一个 SIPI 之后有一个“delay_us(200);”,BSP 可以监视带有超时的“我开始”标志,如果 AP 已经启动,则跳过第二个 SIPI(以及剩余的超时时间)。在这种情况下,SIPI 之间的超时时间可以更长(例如 500 us 就可以了),更重要的是不需要那么精确;在发送第二个 SIPI 后(如果需要发送第二个 SIPI)可以重新使用相同的“等待超时标志”代码,超时时间更长。

仅此一项并不能完全解决“CPU 启动两次”的问题;并且它没有解决“AP 在第二个 SIPI 之后启动,但在超时到期后启动,所以现在有 2 个 AP 运行 并且 OS 只知道一个”。这些问题通过额外的同步得到修复 - 具体来说,AP 设置“我开始标志”,然后它可以等待 BSP 设置“如果你的 APIC ID 是......,你可以继续”值被设置(如果AP 检测到 APIC ID 值错误它可以执行“CLI 然后 HLT”循环以关闭自身)。

最后;如果您一次执行整个“INIT-SIPI-SIPI”序列一个CPU,那么如果有很多CPUs(例如至少整整一秒100 CPUs,因为发送 INIT 后有 10 毫秒的延迟)。这可以通过使用 2 种不同的方法显着减少:

a) 并行开始 CPUs。为了最好的情况; BSP 可以启动 1 个 AP,然后 BSP+AP 可以再启动 2 个 AP,然后 BSP+3 AP 可以再启动 4 个 AP,等等。这意味着 128 CPUs 可以在略多于 70 毫秒的时间内启动(而不是整整一秒)。为了使这项工作(为每个 AP 提供不同的值以用于堆栈等),最好使用多个 AP CPU 启动蹦床(例如,这样一个 AP 可以在不同的 AP 处执行“mov esp,[cs:stackPointer]”在 cs 中以不同的值开始,因为它来自 SIPI)。

b) 一次向多个 CPUs 发送多个 INIT;然后有一个 10 毫秒的延迟;然后一次执行一个 CPU 后面的“SIPI-SIPI”序列。这依赖于后来的“SIPI-SIPI”序列相对较快(与 INIT 后巨大的 10 毫秒延迟相比),并且 CPU 对 10 毫秒延迟的确切长度不太挑剔。例如;如果您向 4 CPUs 发送 4 个 INIT,并且您知道(在最坏的情况下)SIPI-SIPI 需要 1 ms 才能让 OS 确定 CPU 无法启动;那么在将 INIT 发送到 fourth/last CPU 和将第一个 SIPI 发送到 fourth/last CPU.

之间会有 13 毫秒的延迟

请注意,如果您够勇敢,可以将这两种方法结合起来(例如,您可以在 50 毫秒多一点的时间内开始 128 CPU 秒)。