目前我有一个多处理操作系统在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初始化序列)中;切换到保护模式";。这个过程适用于长模式吗?我应该如何调试这个问题?
以下是一些调试信息,如果有用的话:
在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 ]
SIPI是否可以从运行在长模式下的BSP发送?
是。唯一重要的是,将正确的值写入正确的本地APIC寄存器(具有正确的延迟,有点像——请参阅最后的方法)。
然而,在我修改操作系统以64位长模式运行后,发送SIPI时逻辑中断。在QEMU中,在执行发送SIPI行之后,BSP立即被重置(程序计数器变为0xfff0)。
我假设两者之一:
a) 有一个错误,本地APIC寄存器的地址不正确;当您尝试写入本地APIC的寄存器时,会导致三重故障。不要忘记,长模式必须使用分页,即使0xFEE000300可能是正确的物理地址,它也可能是错误的虚拟地址(除非您在将操作系统移植到长模式时通过标识映射特定页面来解决这一问题)。
b) 由于一些难以想象的原因,数据不正确,导致SIPI重新启动BSP。
在英特尔软件开发人员手册第3卷第8.4.4.1节(典型BSP初始化序列)中;切换到保护模式";。这个过程适用于长模式吗?
英特尔的"典型的BSP初始化序列";只是仅适用于固件开发人员的一个可能示例。注意,";用于固件开发人员";意味着任何操作系统都不应该使用它。
英特尔的例子的主要问题是,它将INIT-SIPI-SIPI序列广播到所有其他CPU(可能包括因故障而被固件禁用的CPU,也可能包括因其他原因(例如用户禁用超线程)而被固件停用的CPU);并且未能检测到";CPU存在但由于某种原因未能启动";(操作系统应向用户报告)。
另一个问题是,通常操作系统会在启动每个AP之前为其预先分配一个堆栈(并在启动AP之前将"应该用于堆栈的地址"存储在某个位置),如果同时启动未知数量的CPU,则不能给每个AP提供自己的堆栈。
本质上;固件使用(类似于)Intel描述的示例;ACPI/MADT";操作系统使用的ACPI表(和/或用于非常旧的计算机的"多处理器规格表"——现在已经过时了);并且OS使用来自固件的表的信息以正确的(供应商和平台无关的)方式找到本地APIC的物理地址,并且只找到固件所说的有效的CPU;本地APIC";或";X2APIC";(它支持超过256个APIC ID,如果有大量CPU,这是必要的);然后在使用超时的同时一次只启动一个有效的CPU;CPU#123,我有证据证明存在,未能启动";可以向用户报告和/或记录。
我还应该指出,英特尔的例子在英特尔的手册中已经存在了大约25年,基本上没有改变(自从引入长模式之前)。
我的方法
英特尔算法中的延迟令人讨厌,通常一个CPU会在第一个SIPI上启动,有时第二个SIPI会导致同一个CPU启动两次(如果AP启动代码中有任何类型的"started_CPUs++;
",就会引起问题)。
为了解决这些问题(并提高性能),AP启动代码可以设置一个";我开始";标志,而不是具有";CCD_ 5";在发送第一SIPI之后;我开始";如果AP已经启动,则跳过第二个SIPI(以及超时的剩余部分)。在这种情况下,SIPI之间的超时可以更长(例如500 us是可以的),更重要的是不必如此精确;并且相同的";等待具有超时的标志";代码可以在发送第二个SIPI之后(如果需要发送第二次SIPI)以更长的超时重新使用。
仅凭这一点并不能完全解决";CPU启动两次";问题并且它不能解决";AP在第二个SIPI之后启动,但在超时后启动,所以现在有2个AP在运行,OS只知道一个";。这些问题通过额外的同步来解决——具体地说;我开始标记";然后它可以等待BSP设置"0";如果您的APIC ID为…,则可以继续"要设置的值(并且如果AP检测到APIC ID值是错误的,它可以执行"CLI然后HLT"循环来关闭自己)。
最后;如果你做整个";INIT-SIPI-SIPI";一次排列一个CPU,那么如果有很多CPU,它可能会很慢(例如,由于发送INIT后的10ms延迟,100个CPU至少需要一整秒钟)。使用两种不同的方法可以显著减少这种情况:
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在cs
中以不同的值启动,因为这来自SIPI)。
b) 一次向多个CPU发送多个INIT;则具有一个10ms的延迟;然后进行后面的";SIPI-SIPI";一次对一个CPU进行排序。这依赖于后来的";SIPI-SIPI";序列相对较快(与INIT之后的巨大10ms延迟相比),并且CPU对10ms延迟的确切长度不太挑剔。例如如果您向4个CPU发送4个INIT,并且您知道(在最坏的情况下)SIPI-SIPI需要1毫秒,操作系统才能决定CPU无法启动;则在将INIT发送到第四/最后一个CPU和将第一SIPI发送到第四/最后一CPU之间将存在13ms的延迟。
请注意,如果你很勇敢,这两种方法可以结合使用(例如,你可以在50毫秒多一点的时间内启动128个CPU)。