可以从以长模式运行的 BSP 发送 SIPI 吗?
Posted
技术标签:
【中文标题】可以从以长模式运行的 BSP 发送 SIPI 吗?【英文标题】:Can SIPI be sent from a BSP running in long mode? 【发布时间】:2022-01-05 20:55:53 【问题描述】:目前我有一个在 x86 保护模式下运行的多处理操作系统,我想让它在 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)。
在英特尔的软件开发人员手册第 3 卷第 8.4.4.1 节(典型 BSP 初始化序列)中,它说 BSP 应该“切换到保护模式”。此过程是否适用于长模式?我应该如何调试这个问题?
如果有帮助,这里有一些调试信息:
在 64 位长模式下发送 SIPI 指令 (movl $0xc4610,(%rax)
) 之前的 CPU 寄存器:
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 ]
在 32 位保护模式下发送 SIPI 指令 (movl $0xc4610,(%eax)
) 之前的 CPU 寄存器:
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 ]
【问题讨论】:
【参考方案1】:是否可以从以长模式运行的 BSP 发送 SIPI?
是的。唯一重要的是您将正确的值写入正确的本地 APIC 寄存器(具有正确的延迟,有点 - 见最后我的方法)。
但是,在我将操作系统修改为以 64 位长模式运行后,发送 SIPI 时逻辑中断。在 QEMU 中,执行发送 SIPI 行后,BSP 立即复位(程序计数器变为 0xfff0)。
我会假设:
a) 存在错误,本地 APIC 寄存器的地址不正确;当您尝试写入本地 APIC 的寄存器时会导致三重错误。不要忘记长模式必须使用分页,即使 0xFEE00300 可能是正确的物理地址,它也可能是错误的虚拟地址(除非您在将操作系统移植到长模式时通过标识映射该特定页面来处理这一点)。
b) 由于某种难以想象的原因,数据不正确,导致 SIPI 重新启动 BSP。
在英特尔的软件开发人员手册第 3 卷第 8.4.4.1 节(典型 BSP 初始化序列)中,它说 BSP 应该“切换到保护模式”。这个过程是否适用于长模式?
英特尔的“典型 BSP 初始化序列”只是仅适用于固件开发人员的一个可能示例。请注意,“面向固件开发人员”意味着它不应该被任何操作系统使用。
英特尔示例的主要问题是它将 INIT-SIPI-SIPI 序列广播到所有其他 CPU(可能包括固件因故障而禁用的 CPU,也可能包括固件因其他原因而禁用的 CPU -例如因为用户禁用了超线程);并且未能检测到“CPU 存在但由于某种原因无法启动”(操作系统应向用户报告)。
另一个问题是,通常操作系统会希望在启动之前为每个 AP 预先分配一个堆栈(并在启动 AP 之前将“您应该用于堆栈的地址”存储在某处),并且您不能给每个 AP如果您同时启动未知数量的 CPU,则像这样 AP 自己的堆栈。
基本上;固件使用(类似于)英特尔描述的示例,然后在“ACPI/MADT”ACPI 表(和/或非常旧的计算机的“多处理器规范表” - 现在已经过时)中构建信息供操作系统使用;并且操作系统使用固件表中的信息以正确(供应商和平台中立)的方式查找本地 APIC 的物理地址,并仅查找固件认为有效的 CPU 并确定这些 CPU/s 是否有效使用“本地 APIC”或“X2APIC”(支持超过 256 个 APIC ID,如果有大量 CPU,则这是必需的);然后在使用超时时一次只启动一个有效的 CPU,以便可以向用户报告和/或记录“我有证据的 CPU #123 无法启动”。
我还应该指出,英特尔的示例在英特尔的手册中已经存在了大约 25 年(自从引入长模式之前),几乎没有变化。
我的方法
Intel 算法的延迟很烦人,通常一个 CPU 会在第一个 SIPI 上启动,有时第二个 SIPI 会导致同一个 CPU 启动两次(如果你有任何类型的“started_CPUs++;
”会导致问题在 AP 启动代码中)。
为了解决这些问题(并提高性能),AP 启动代码可以设置一个“我开始”标志,而不是在发送第一个 SIPI 之后有一个“delay_us(200);
”,BSP 可以监控“我开始”标记超时,如果 AP 已经启动,则跳过第二个 SIPI(以及超时的剩余部分)。在这种情况下,SIPI 之间的超时时间可以更长(例如 500 us 就可以了),更重要的是不需要那么精确;并且在发送第二个 SIPI(如果需要发送第二个 SIPI)后,可以重复使用相同的“等待标志超时”代码,超时时间更长。
仅此一项并不能完全解决“CPU 启动两次”的问题;并且它没有解决“AP 在第二个 SIPI 之后启动,但在超时过期后启动,所以现在有 2 个 AP 在运行,操作系统只知道一个”。这些问题通过额外的同步得到解决 - 具体来说,AP 设置“我开始标志”,然后它可以等待 BSP 设置“如果您的 APIC ID 是......,您可以继续”要设置的值(并且如果AP 检测到 APIC ID 值错误,它可以执行“CLI 然后 HLT”循环以自行关闭)。
最后;如果您一次执行整个“INIT-SIPI-SIPI”序列一个 CPU,那么如果有很多 CPU,它可能会很慢(例如,由于发送 INIT 后的 10 毫秒延迟,100 个 CPU 至少需要一整秒)。这可以通过使用 2 种不同的方法来显着减少:
a) 并行启动 CPU。为了最好的情况; BSP 可以启动 1 个 AP,然后 BSP+AP 可以再启动 2 个 AP,然后 BSP+3 个 AP 可以再启动 4 个 AP,依此类推。这意味着 128 个 CPU 可以在略多于 70 毫秒的时间内启动(而不是超过一秒) .为了使这项工作(给每个 AP 不同的值用于堆栈等),最好使用多个 AP CPU 启动蹦床(例如,这样一个 AP 可以执行“mov esp,[cs:stackPointer]
”,其中不同的 AP 以不同的值在 @ 987654324@,因为它来自 SIPI)。
b) 一次向多个 CPU 发送多个 INIT;然后有一个 10 毫秒的延迟;然后一次执行后面的“SIPI-SIPI”序列一个 CPU。这依赖于后来的“SIPI-SIPI”序列相对较快(与 INIT 之后的巨大 10 毫秒延迟相比),并且 CPU 对 10 毫秒延迟的确切长度不太挑剔。例如;如果您向 4 个 CPU 发送 4 个 INIT,并且您知道(在最坏的情况下)SIPI-SIPI 需要 1 ms 让操作系统决定 CPU 无法启动;那么在将 INIT 发送到第四个/最后一个 CPU 和将第一个 SIPI 发送到第四个/最后一个 CPU 之间会有 13 毫秒的延迟。
请注意,如果您足够勇敢,可以将这两种方法结合使用(例如,您可以在 50 毫秒多一点的时间内启动 128 个 CPU)。
【讨论】:
我能够找出我的操作系统中的问题。实际的问题是,在gdb中,当BSP执行send SIPI
时,AP执行了很多代码。由于我的 AP 代码尚未准备好,AP 会产生三重故障,重置整个机器(即 BSP 和 AP)。在 gdb 中,BSP 似乎由于执行 send SIPI
而被重置。以上是关于可以从以长模式运行的 BSP 发送 SIPI 吗?的主要内容,如果未能解决你的问题,请参考以下文章
怎么设置将LINUX的启动模式从以图形界面启动改为以文本模式启动