发送到不处于等待 SIPI 状态的活动 AP 的启动 IPI 会发生什么

What happens to a Startup IPI sent to an Active AP that is not in a Wait-for-SIPI state

在之前的 中,玛格丽特·布鲁姆说:

Waking the APs

This is achieved by inssuing a INIT-SIPI-SIPI (ISS) sequence to the all the APs.

The BSP that will send the ISS sequence using as destination the shorthand All excluding self, thereby targeting all the APs.

A SIPI (Startup Inter Processor Interrupt) is ignored by all the CPUs that are waked by the time they receive it, thus the second SIPI is ignored if the first one suffices to wake up the target processors. It is advised by Intel for compatibility reason.

多年来我一直在编写多处理代码,我对硬件的观察是,在某些处理器上,它似乎与声明的不同。我很确定我已经观察到应用程序处理器 (AP) 在收到启动 IPI 时修改了它们的指令指针,即使它处于活动状态(不在等待启动 IPI 中)也是如此。

是否有任何英特尔文档说明 AP 在 收到 启动 IPI 时不处于等待启动 IPI 状态时将执行的操作,或记录行为未定义?我似乎无法在 Intel Software Documentation Manuals or the supplementary Intel document Minimal Boot Loader for Intel® Architecture.

中找到 明确的 答案

通常我会编写初始化代码来初始化和启动 AP,假设 AP 可能会获得 SIPI 并在 active 状态(不是在等待启动 IPI 状态)。

我正在尝试确定 Margaret Bloom 关于第二个启动 IPI 将被先前已唤醒的 AP 忽略的说法的准确性。

简答

  • 一些 CPU 会在第二个 SIPI
  • 上重新启动
  • 不知道第2个SIPI是哪个CPU重启,因为我防范太久了
  • 我没有检查过,但我认为英特尔的文档没有指定 "SIPI received by running CPU" 案例的行为
  • 如果 Intel 的文档确实指定了 Intel CPUs 的行为,那么这并不意味着来自其他供应商(AMD、VIA、SiS、Cyrix 等)的 CPUs 的行为与 Intel CPUs 相同。 Intel的手册只是"guaranteed"(不包括errata/specification更新)适用于Intel的CPUs.

更长的答案

当我第一次开始实施 multi-CPU 支持时(10 多年前),我遵循了 Intel 的启动程序(来自 Intel 的多处理器规范,在 INIT、SIPI 和 SIPI 之间有时间延迟),并且在 AP 之后开始它增加了一个 number_of_CPU_running 计数器(例如 lock inc)。

我发现一些 CPUs 在收到第二个 SIPI 时会重新启动;在某些计算机上,number_of_CPU_running 计数器会增加两次(例如,使用 BSP 和 3 AP CPUs,number_of_CPU_running 计数器最终可能是 7 而不是 4)。

自从我一直在使用内存同步来避免这个问题。具体来说,发送 CPU 在尝试开始接收 CPU 之前设置一个变量 (state = 0),if/when 接收 CPU 开始它更改变量 (state = 1) 并等待再次更改变量,当发送 CPU 发现变量已更改(通过接收 CPU)时,它更改变量 (state = 2)允许接收 CPU 继续。

此外;为了提高性能,在发送第一个 SIPI 后的延迟期间,发送 CPU 监视该变量,如果接收 CPU 更改变量,它将取消延迟并且根本不会发送第二个 IPI .我还显着增加了最后的延迟,因为它只有在出现故障时才会过期(并且您不想假设 CPU 在启动得太晚时无法启动,并以 CPU 结束who-knows-what 因为 OS 改变了内存的内容,等等)。

换句话说,我主要忽略了 Intel 的 "Application Processor Startup" 程序(例如,来自 Intel 多处理器规范的 B.4 部分)并且我的发送代码 CPU 确实:

    set synchronization variable (state = 0)
    send INIT IPI
    wait 10 milliseconds
    send SIPI IPI
    calculate time-out value ("now + 200 microseconds")
    while time-out hasn't expired {
        if the synchronization variable was changed jump to the "CPU_started" code
    }
    send a second SIPI IPI
    calculate time-out value ("now + 500 milliseconds")
    while time-out hasn't expired {
        if the synchronization variable was changed jump to the "CPU_started" code
    }
    do "CPU failed to start" error handling and return

CPU_started:
    set synchronization variable (state = 2) to let the started CPU know it can continue

我的接收代码 CPU 是这样做的:

    get info from trampoline (address of stack this CPU needs to use, etc), because sending CPU may change the info after it knows this CPU started
    set synchronization variable (state = 1)
    while synchronization variable remains unchanged (state == 1) {
        pause (can't continue until sending CPU knows this CPU started)
    }
    initialize the CPU (setup protected mode or long mode, etc) and enter the kernel

注意 1:取决于周围的代码(例如,如果同步变量在蹦床中并且 OS 回收蹦床以在不久之后启动其他 CPUs);发送 CPU 可能需要等待接收 CPU 最后一次更改同步变量(以便发送 CPU 知道 recycle/reset 同步变量是安全的).

注意 2:CPU "almost always" 从第一个 SIPI 开始,可以合理地假设第二个 SIPI 只存在于第一个 SIPI 得到 lost/corrupted 并且合理的情况下假设 200 微秒延迟是保守的最坏情况。由于这些原因,我的 "cancel the time-out and skip the second SIPI" 方法可能会将这对 200 毫秒延迟减少 4 倍(例如,100 uS 而不是 400 uS)。 10 毫秒的延迟(在 INIT IPI 和第一个 SIPI 之间)可以摊销(例如发送 INIT 到 N CPUs,然后延迟 10 毫秒,然后为 N CPUs 中的每一个做剩余的事情一次一个);并且你可以"snowball" AP CPU启动(例如使用BSP启动一组N CPUs,然后使用1+N CPUs并行启动(1+N)*MCPUs,然后用1+N*MCPUs开始(1+N*M)*LCPUs等,换句话说;开始255CPUs 使用 Intel 的方法增加了 2.64 秒的延迟;但是使用足够先进的代码,这可以减少到不到 0.05 秒。

注意 3:"broadcast INIT-SIPI-SIPI" 方法已损坏,OS 永远不应使用(因为它使检测 "CPU failed to start" 变得困难,因为它可以启动 CPU s 是有故障的,并且因为它可以启动 CPU 由于其他原因而被禁用的 s - 例如 hyper-threading 被用户在固件设置中禁用)。可悲的是,英特尔的手册有一些示例代码描述了面向固件开发人员的 "broadcast INIT-SIPI-SIPI" 方法(其中 "broadcast INIT-SIPI-SIPI" 方法有意义且安全),初学者看到这个示例并(错误地)假设OS 可以使用这种方法。

我认为我的陈述是正确的,直到错误

我并不是说应该忽略有缺陷的硬件,而是必须首先评估它们的影响。
我想提醒一下 reader 虽然我对此事有自己的看法,但我希望这个答案尽可能保持中立。
为了完全满足这个目的,我尝试为我的陈述提供 来源

虽然我确实相信其他用户的体验,但我不能仅将我的信念建立在记忆之上(因为它们无法验证)1并且我'期待有人用证据更正我引用的陈述。

我知道这是一个不受欢迎的观点,我希望它不会被认为是完全错误的。


首先,与往常一样,计算机都归结为标准。虽然英特尔在手册中记录了他们 CPUs 的 MP 行为,但更进一步并做出了适当的 MultiProcessor specification.
该规范的重要性在于它在行业中的作用,这 不是英特尔 CPU 的工作方式 ,据我所知,这是 the only x86 SMP industry reference
AMD 和 Cyrix 推动了 OpenPIC specification but quoting Wikipedia:

No x86 motherboard was released with OpenPIC however.[3] After the OpenPIC's failure in the x86 market, AMD licensed the Intel APIC Architecture for its AMD Athlon and later processors.

在 MP-specification 的附录 B4 中存在行

If the target processor is in the halted state immediately after RESET or INIT, a STARTUP IPI causes it to leave that state and start executing. The effect is to set CS:IP to VV00:0000h.

如评论中所述,我将 if 解析为更强的 *iif

不幸的是,如前所述,引用的句子只是一个充分条件。所以它不能用来推断 SIPI 在 运行ning CPU 上的行为。

然而我个人认为这是一个错误,规范作者的意图是使用SIPI唤醒一个CPU在wait-for-SIPI状态。

随着集成 APIC 的出现以及 INIT IPI 的修订,专门引入了 SIPI,以管理 AP 的启动。
SIPI 对 BSP 没有影响(根据英特尔的手册,它永远不会进入 wait-for-SIPI 状态)并且很明显 should对 运行ning CPU.
没有影响 SIPI 的用处,除了是 non-maskeable 并且不需要启用 LAPIC 之外,还在于避免 运行ning 从重置向量和 AP 的热启动标志的需要。

从设计的角度来看,让 SIPI 作用于 运行ning CPUs 是没有意义的。 CPUs 始终以 INIT IPI 作为第一个 IPI 重新启动。

所以,我有信心将引用的语句解析为口语化的英语,默认也是必要条件 .

我相信这会设置 SIPI 在 woke-up CPU 上的官方 行为,即忽略它们.

事实 1:所有主要 x86 制造商都遵循 industry-standard MP 规范,尽管含糊不清,但其目的是设置 SIPI 的行为。

Pentium Spec Update 的第 98 页似乎证实了这一点,至少对于奔腾(可能是后来的英特尔,其中可能包括 AMD,因为他们已经从英特尔购买了 LAPIC 许可证):

If an INIT IPI is then sent to the halted upgrade component, it will be latched and kept pending until a STARTUP IPI is received. From the time the STARTUP IPI is received the CPU will respond to further INIT IPls but will ignore any STARTUP IPls. It will not respond to future STARTUP IPls until a RESET assertion or an INIT assertion (INIT Pin or INIT IPI) happens again.

The 75-, 90, and 100-MHz Pentium processors, when used as a primary processor, will never respond to a STARTUP IPI at any time. It will ignore the STARTUP IPI with no effects.

To shutdown the processors the operating system should only use the INIT IPI, STARTUP IPls should never be used once the processors are running.


如果如果有CPU不忽略后续 IPI 的问题。
虽然这个问题还有待解决,但我们现在已经把它变成了问题 "Are there buggy CPUs that ... ?".
这是一个巨大的 leap-forward,因为我们现在可以看到现有的 OSes 如何处理它。

我不会讨论 Windows,虽然我知道这是一个很大的缺席,但我现在没有心情深入研究 Windows 二进制文件。
我可能会稍后再做。

Linux

Linux 发送了两个 SIPI,我在这个循环中没有看到任何反馈。 The code is in smpboot.c 其中我们清楚地看到 num_starts 设置为 2.
我不会讨论 LAPIC 和 82489DX APIC 之间的区别,特别是后者没有 SIPI2

但是我们可以看到Linux如何遵循英特尔的算法,而不是担心第二个SIPI。
在循环中,执行num_starts次,向目标AP发送SIPI。

评论里已经指出trampoline是幂等的,Linux作为同步机制。
这与我的经验不符,当然 Linux 在 CPU 之间同步代码,但那是在启动 之后 AP 是 运行宁.
事实上,AP 执行的第一个 C 代码的蹦床是 start_secondary 并且它似乎不是幂等的(set_cpu_online 稍后在主体中调用,如果那算的话)。

最后,如果程序员想要防止双重 SIPI,他们会尽早放置同步逻辑以避免以后处理复杂情况。
蹦床就处理 SME 和漏洞修复而言,为什么要在 before dealin 之前这样做SIPI-SIPI 问题?

这么晚进行如此严格的检查对我来说毫无意义。

免费 BSD
我想包括一个 BSD OS 因为 BSD 代码以非常干净和健壮着称。
我能够找到一个包含 Free BSD 源代码的 GitHub(非官方)存储库,虽然我对该代码不太自信,但我已经 found the routine that starts an AP in mp_x86.c

Free BSD也使用了Intel的算法。 令我感到有趣的是,消息来源还解释了为什么需要第二个 SIPI:P5 处理器(P54C Pentium 系列?)由于错误而忽略了第一个 SIPI:

/*
* next we do a STARTUP IPI: the previous INIT IPI might still be
* latched, (P5 bug) this 1st STARTUP would then terminate
* immediately, and the previously started INIT IPI would continue. OR
* the previous INIT IPI has already run. and this STARTUP IPI will
* run. OR the previous INIT IPI was ignored. and this STARTUP IPI
* will run.
*/

我无法找到此声明的来源,我唯一的线索是在旧 android(即 Linux)内核上找到的勘误表 AP11 of the Pentium Specification Update
今天 Linux seems to have dropped 支持那些旧的 buggy LAPIC。

考虑到 详细评论 我认为没有必要检查代码的幂等性直到假设检查。
BSD 代码显然是在考虑注释假设的情况下编写的。

事实 2:两个主流 OSes 认为 SIPI 错误的发生频率不足以值得处理。

在搜索 Internet 时,我在 gem5 simulator with the title X86: Only recognize the first startup IPI after INIT or reset 中找到了一个提交。
显然,他们一开始弄错了,然后改正了。


下一步是尝试查找一些在线文档。
我首先在 Google Patents 中搜索,虽然弹出了很多有趣的结果(包括 APIC ID 的分配方式),但关于 SIPI,我只在专利 Method and apparatus for initiating execution of an application processor in a clustered multiprocessor system:

中找到了这段文字

STARTUP IPIs do not cause any change of State in the target processor (except for the change to the instruction pointer), and can be issued only one time after RESET or after an INIT IPI reception or pin assertion.

Wikipedia lists VIA as the only other x86 manufacturer still present..
我试着寻找 VIA 手册,但是 it seems they are not public?

关于过去的厂商,我根本找不到有没有生产过MPCPU的。例如。 Cyrix 6x86MX 根本没有 APIC,因此它们可能仅由外部 APIC(不支持 SIPI)放入 MP 系统。

下一步是查看 所有 AMD 和 Intel 勘误表,看看是否有关于 SIPI 的内容。
但是,勘误表是错误,因此问题变成了寻找 non-existence 的证明(即 存在错误的 LAPIC 吗?)这很难找到(仅仅是因为很难找到错误并且有很多 micro-architectures)。

我的理解是 the P54C 附带了第一个集成 APIC(今天已知的 LAPIC),我查阅了勘误表,但没有发现任何有关 SIPI 处理的信息。
然而,理解勘误表的全部后果并非易事。

然后我转到 Pentium Pro Errata(这是下一个 uarch,P6)并发现对 SIPI 的处理不正确,但不完全是我们正在寻找的:

3AP. INIT_IPI After STARTUP_IPI-STARTUP_IPI Sequence May Cause

AP to Execute at 0h**
PROBLEM: The MP Specification states that to wake up an application processor (AP), the interprocessor interrupt sequence INIT_IPI, STARTUP_IPI, STARTUP_IPI should be sent to that processor. On the Pentium Pro processor, an INIT_IPI, STARTUP_IPI sequence will also work. However, if the INIT_IPI, STARTUP_IPI, STARTUP_IPI sequence is sent to an AP, an internal race condition may occur in the APIC logic which leaves the processor in an incorrect state. Operation will be correct in this state, but if another INIT_IPI is sent to the processor, the processor will not stop execution as expected, and will instead begin execution at linear address 0h. In order for the race condition to cause this incorrect state, the system’s core to bus clock ratio must be 5:2 or greater.

IMPLICATION: If a system is using a core to bus clock ratio of 5:2 or greater, and the sequence INIT_IPI, STARTUP_IPI, STARTUP_IPI is generated on the APIC bus to wake up an AP, and then at some later time another INIT_IPI is sent to the processor, that processor may attempt to execute at linear address 0h, and will execute random opcodes. Some operating systems do generate this sequence when attempting to shut the system down, and in a multiprocessor system, may hang after taking the processors offline. The effect seen will be that the OS may not restart the system if ‘shutdown and restart’ or the equivalent is selected upon exiting the operating system. If an operating system gives the user the capability to take an AP offline using an INIT_IPI (Intel has not identified any operating systems which currently have this capability), this option should not be used.

WORKAROUND: BIOS code should execute a single STARTUP_IPI to wake up an application processor. Operating systems, however, will issue an INIT_IPI, STARTUP_IPI, STARTUP_IPI sequence, as recommended in the MP specification. It is possible that BIOS code may contain a workaround for this erratum in systems with C0 or subsequent steppings of Pentium Pro processor silicon. No workaround is available for the B0 stepping of the Pentium Pro processor.

状态:有关受影响的步进,请参阅本节开头的更改摘要Table。

这个 AP3 勘误很有趣,因为:

  1. 确认INIT-SIPI序列足以启动AP。从 MP 规范和 Free BSD 代码中可以明显看出这一点。
  2. 它可能会导致类似于重启的行为。该错误将使 INIT IPI(在 INIT-SIPI-SIPI 序列之后)restart AP 在 0h(线性,大概在初始化)。
    如果 BIOS 使用 INIT-SIPI-SIPI 来使用 AP,稍后 OS 尝试再次使用该序列,则第一个 INIT 将启动 AP。
    但是,这不会导致可预测的行为,除非 LAPIC 处于损坏状态,任何 SIPI 都将被接受。

有趣的是,在同一个勘误表中甚至还有一个导致 "the opposite behaviour": 8AP 的错误。 AP 在 INIT# 或 INIT_IPI 后不响应 STARTUP_IPI 在低功耗模式下

我还检查了 Pentium II、Pentium II Xeon、Pentium III、Pentium 4 勘误表,没有发现任何关于 SIPI 的新内容。

据我了解,第一个支持 SMP 的 AMD 处理器是基于 Palomino uarch 的 Athlon MP。
我已经检查了 Athlon MP 的修订指南,但什么也没发现,检查了修订 in this list,什么也没发现。

不幸的是,我对非 AMD 非 Intel x86 CPUs 没有什么经验。我无法找到哪些二级制造商包含 LAPIC。

事实 3:非 AMD/Intel 制造商的官方文档很难找到,勘误也不容易搜索到。勘误表中没有包含与在 运行ning 处理器上接受 SIPI 相关的错误,但存在大量 LAPIC 错误,使此类错误的存在变得合理。


最后一步是硬件测试。
虽然这个测试不能排除其他行为的存在,但至少是记录(蹩脚的)代码。
文档化的代码很好,因为它可以被其他研究人员用来重复实验,它可以是仔细检查错误并构成证明。
简而言之,它是科学的。

从未见过CPU后续SIPI重新启动它的情况,但这并不重要,因为有一辆越野车CPU就足够了确认错误的存在。
我太年轻、太穷、太人性化,无法对所有国会议员 CPU 进行广泛的 bug-free 分析。
所以,相反,我做了一个测试 运行 它。

事实 4:Whiskey lake、Haswell、Kaby lake 和 Ivy Bridge 均忽略后续 SIPI。
欢迎其他人在 AMD 和更早的 CPUs 上进行测试。
同样,这并不构成证据,但重要的是 正确地描述事情的状态
我们拥有的数据越多,我们对错误的了解就越准确。

测试包括引导 AP 并使它们递增计数器并进入无限循环(使用 jmp $hlt,结果相同)。
同时,BSP 将每 n 秒发送一个 SIPI,其中 n 至少为 2(但由于非常不精确的计时机制,它可能更多) ), 并打印计数器。

如果计数器停留在k-1,其中k是AP的数量,那么次要SIPI被忽略。

有一些技术细节需要解决。

首先,引导加载程序是遗留的(不是​​ UEFI),我不想读取另一个扇区,所以我希望它适合 512 字节,所以我在 BSP 和 AP 之间共享引导顺序。

其次,有些代码必须仅由 BSP 执行,但在进入保护模式之前(例如视频模式设置)所以我使用了一个标志 (init) 而不是检查 BSP 标志在 IA32_APIC_BASE_MSR 寄存器中(稍后将 AP 与 BSP 分开)。

第三,我走了一些捷径。 SIPI 在 8000h 启动 CPU,所以我跳到 0000h:7c00h。计时是通过端口 80h 技巧完成的,它非常不精确,但应该足够了。 GDT 使用空条目。计数器打印在顶部下方几行以避免被某些显示器裁剪。

如果修改循环以包含 INIT IPI,则计数器会定期递增。

请注意,此代码不受支持。

BITS 16
ORG 7c00h

%define IA32_APIC_BASE_MSR 1bh
%define SVR_REG 0f0h
%define ICRL_REG 0300h
%define ICRH_REG 0310h

xor ax, ax
mov ds, ax
mov ss, ax
xor sp, sp      ;This stack ought be enough

cmp BYTE [init], 0
je _get_pm

;Make the trampoline at 8000h
mov BYTE [8000h], 0eah
mov WORD [8001h], 7c00h
mov WORD [8003h], 0000h

mov ax, 0b800h
mov es, ax
mov ax, 0003h
int 10h
mov WORD [es:0000], 0941h

mov BYTE [init], 0

_get_pm:
;Mask interrupts
mov al, 0ffh
out 21h, al
out 0a1h, al

;THIS PART TO BE TESTED
;
;CAN BE REPLACED WITH A cli, SIPIs ARE NOT MASKEABLE
;THE cli REMOVES THE NEED FOR MASKING THE INTERRUPTS AND
;CAN BE PLACED ANYWHERE BEFORE ENTERING PM (BUT LEAVE xor ax, ax
;AS THE FIRST INSTRUCTION)

;Flush pending ones (See Michael Petch's comments)
sti
mov cx, 15
loop $   

lgdt [GDT]
mov eax, cr0
or al, 1
mov cr0, eax
sti

mov ax, 10h
mov es, ax
mov ds, ax
mov ss, ax
jmp 08h:DWORD __START32__

__START32__: 
 BITS 32

 mov ecx, IA32_APIC_BASE_MSR
 rdmsr
 or ax, (1<<11)          ;ENABLE LAPIC
 mov ecx, IA32_APIC_BASE_MSR
 wrmsr

 mov ebx, eax
 and ebx, 0ffff_f000h    ;APIC BASE

 or DWORD [ebx+SVR_REG], 100h

 test ax, 100h
 jnz __BSP__

__AP__: 
 lock inc BYTE [counter]

 jmp $            ;Don't use HLT just in case

__BSP__:
 xor edx, edx 
 mov DWORD [ebx+ICRH_REG], edx 
 mov DWORD [ebx+ICRL_REG], 000c4500h        ;INIT

 mov ecx, 10_000
.wait1:
 in al, 80h
 dec ecx
jnz .wait1 

.SIPI_loop:
 movzx eax, BYTE [counter]
 mov ecx, 100
 div ecx 
 add ax, 0930h
 mov WORD [0b8000h + 80*2*5], ax

 mov eax, edx 
 xor edx, edx
 mov ecx, 10
 div ecx
 add ax, 0930h
 mov WORD [0b8000h + 80*2*5 + 2], ax

 mov eax, edx
 xor edx, edx
 add ax, 0930h
 mov WORD [0b8000h + 80*2*5 + 4], ax

 xor edx, edx 
 mov DWORD [ebx+ICRH_REG], edx 
 mov DWORD [ebx+ICRL_REG], 000c4608h        ;SIPI at 8000h

 mov ecx, 2_000_000
.wait2:
 in al, 80h
 dec ecx
jnz .wait2

jmp .SIPI_loop


GDT dw 17h
    dd GDT
    dw 0

    dd 0000ffffh, 00cf9a00h
    dd 0000ffffh, 00cf9200h

counter db 0
init db 1

TIMES 510-($-$$) db 0
dw 0aa55h

结论

目前尚无定论,此事尚待解决。
reader 已经提交了一份事实清单。

预期的行为是忽略后续的 SIPI,需要两个 SIPI 是由于 "P5 bug"。
Linux 和 Free BSD 似乎不介意有问题的 SIPI 处理。
其他制造商似乎没有提供有关其 LAPIC 的文档,如果他们自己生产的话。
最近的英特尔硬件忽略了后续的 SIPI。


1尊重所有相关人员,不攻击任何人的信誉。我 确实 相信那里有错误 CPU 但也有错误的软件和错误的记忆。由于我不相信自己的旧记忆,我认为我仍然在尊重他人的谈话范围内要求别人不要相信他们模糊的记忆。

2 可能是因为当时的 MP 是用常规的 CPU 打包在一起并使用外部芯片(APIC)断言它们的 INIT# 完成的是启动它们的唯一方法(以及设置热复位向量)。但是那些年我还太小,还没有电脑。

根据我的测试,当不处于 wait-for-SIPI 状态时,SIPI 会被忽略。 我测试了Whiskey-lake 8565U,当然real-hardware测试不构成证明
我相信自 Pentium 4 以来的所有 Intel 处理器也具有相同的行为,但这只是 我的观点
在这个答案中,我只想展示测试结果。每个人都会得出自己的结论。