事务内存支持¶
POWER 内核对该功能的支持目前仅限于支持用户程序使用它。内核本身目前未使用它。
本文件旨在总结 Linux 如何支持它,以及您的用户程序可以预期什么样的行为。
基本概述¶
POWER8 处理器支持硬件事务内存(Hardware Transactional Memory),这是一项能够实现另一种形式的原子内存访问的功能。引入了几条新指令来界定事务;事务被保证要么原子地完成,要么回滚并撤销任何部分的更改。
一个简单的事务如下所示
begin_move_money:
tbegin
beq abort_handler
ld r4, SAVINGS_ACCT(r3)
ld r5, CURRENT_ACCT(r3)
subi r5, r5, 1
addi r4, r4, 1
std r4, SAVINGS_ACCT(r3)
std r5, CURRENT_ACCT(r3)
tend
b continue
abort_handler:
... test for odd failures ...
/* Retry the transaction if it failed because it conflicted with
* someone else: */
b begin_move_money
‘tbegin’ 指令表示起点,‘tend’ 表示终点。在这些点之间,处理器处于“事务性”(Transactional)状态;如果系统中没有与其他事务性或非事务性访问的冲突,任何内存引用将一次性完成。在此示例中,如果没有任何其他处理器触及 SAVINGS_ACCT(r3) 或 CURRENT_ACCT(r3),则该事务的完成方式就像是正常的顺序代码一样;已执行了将资金从活期账户到储蓄账户的原子移动。即使使用了普通的 ld/std 指令(注意没有 lwarx/stwcx),SAVINGS_ACCT(r3) 和 CURRENT_ACCT(r3) 也会两者同时更新,或者两者都不更新。
如果在此期间与事务所访问的内存位置发生冲突,事务将被 CPU 中止。寄存器和内存状态将回滚到 ‘tbegin’ 时的状态,并且控制流将从 ‘tbegin+4’ 继续。第二次将执行跳转到 abort_handler;中止处理程序可以检查失败的原因并进行重试。
带检查点的寄存器(Checkpointed registers)包括所有 GPR、FPR、VR/VSR、LR、CCR/CR、CTR、FPCSR 以及其他一些状态/标志寄存器;详情请参阅 ISA。
事务中止的原因¶
与其他处理器使用的缓存行发生冲突
信号
上下文切换
关于将导致事务中止的所有情况的完整文档,请参阅 ISA。
系统调用¶
从活动事务内部发起的系统调用将不会被执行,并且事务将被内核判死刑,失败代码为 TM_CAUSE_SYSCALL | TM_CAUSE_PERSISTENT。
从挂起事务内部发起的系统调用将正常执行,且事务不会被内核显式判死刑。然而,内核为执行该系统调用所做的工作可能会导致事务被硬件判死刑。系统调用是在挂起模式下执行的,因此任何副作用都将是持久的,与事务的成功或失败无关。内核不保证哪些系统调用会影响事务的成功。
如果通过库发起系统调用,在依赖系统调用在活动事务期间中止时必须小心。库可能会缓存值(这可能会给人一种成功的假象)或者在进入内核之前执行导致事务失败的操作(这可能会产生不同的失败代码)。例如 glibc 的 getpid() 和延迟符号解析。
信号¶
在事务期间交付信号(同步和异步)提供了第二个线程状态(ucontext/mcontext)来表示第二个事务寄存器状态。信号交付会进行 ‘treclaim’(回收)以捕获两个寄存器状态,因此信号会中止事务。传递给信号处理程序的常规 ucontext_t 表示带有检查点的/原始的寄存器状态;信号看起来像是发生在 ‘tbegin+4’ 处。
如果信号处理程序的 ucontext 设置了 uc_link,则说明已经交付了第二个 ucontext。为了将来的兼容性,应检查 MSR.TS 字段以确定事务状态——如果是这样,uc->uc_link 中的第二个 ucontext 表示信号发生时的活动事务寄存器。
对于 64 位进程,uc->uc_mcontext.regs->msr 是一个完整的 64 位 MSR,其 TS 字段显示了事务模式。
对于 32 位进程,mcontext 的 MSR 寄存器只有 32 位;高 32 位存储在第二个 ucontext 的 MSR 中,即在 uc->uc_link->uc_mcontext.regs->msr 中。高字包含事务状态 TS。
然而,基本的信号处理程序不需要感知事务,简单地从处理程序返回即可正确处理事情
感知事务的信号处理程序可以从第二个 ucontext 读取事务寄存器状态。这对于崩溃处理程序是必要的,例如,以确定导致 SIGSEGV 的指令的地址。
示例信号处理程序
void crash_handler(int sig, siginfo_t *si, void *uc)
{
ucontext_t *ucp = uc;
ucontext_t *transactional_ucp = ucp->uc_link;
if (ucp_link) {
u64 msr = ucp->uc_mcontext.regs->msr;
/* May have transactional ucontext! */
#ifndef __powerpc64__
msr |= ((u64)transactional_ucp->uc_mcontext.regs->msr) << 32;
#endif
if (MSR_TM_ACTIVE(msr)) {
/* Yes, we crashed during a transaction. Oops. */
fprintf(stderr, "Transaction to be restarted at 0x%llx, but "
"crashy instruction was at 0x%llx\n",
ucp->uc_mcontext.regs->nip,
transactional_ucp->uc_mcontext.regs->nip);
}
}
fix_the_problem(ucp->dar);
}
当在接收到信号的活动事务中时,我们需要小心栈。栈有可能在 tbegin 之后向上移回。这里最明显的情况是当 tbegin 在一个在 tend 之前返回的函数内部被调用时。在这种情况下,栈是带有检查点的事务内存状态的一部分。如果我们以非事务方式或在挂起状态下覆写它,我们就会陷入麻烦,因为如果我们遇到 tm 中止,程序计数器和栈指针将回到 tbegin,但我们内存中的栈将不再有效。
为了避免这种情况,在活动事务中接收信号时,我们需要使用来自带有检查点状态的栈指针,而不是来自投机状态的栈指针。这确保了信号上下文(以 tm 挂起状态写入)将写入回滚所需的栈下方。由于 treclaim,事务将被中止,因此在 tbegin 和信号之间写入的任何内存都将被回滚。
对于在非 TM 或挂起模式下接收到的信号,我们使用普通/未设检查点的栈指针。
在 sighandler 内部发起并在从 sighandler 返回内核时处于挂起状态的任何事务都将被回收和丢弃。
内核使用的失败原因代码¶
这些在 <asm/reg.h> 中定义,用于区分内核中止事务的不同原因
TM_CAUSE_RESCHED
线程被重新调度。
TM_CAUSE_TLBI
软件 TLB 无效。
TM_CAUSE_FAC_UNAV
FP/VEC/VSX 不可用陷阱。
TM_CAUSE_SYSCALL
来自活动事务的系统调用。
TM_CAUSE_SIGNAL
信号已交付。
TM_CAUSE_MISC
当前未使用。
TM_CAUSE_ALIGNMENT
对齐故障。
TM_CAUSE_EMULATE
触及了内存的仿真。
用户程序的中止处理程序可以将其作为 TEXASR[0:7] 进行检查。如果设置了第 7 位,则表明该错误被认为是持久的。例如,TM_CAUSE_ALIGNMENT 是持久的,而 TM_CAUSE_RESCHED 则不是。
GDB¶
GDB 和 ptrace 目前不感知 TM。如果在一个事务期间停止,看起来就像事务刚刚开始(呈现的是带有检查点的状态)。然后该事务将无法继续,并将走失败处理程序路径。此外,事务性的第二个寄存器状态将无法访问。目前可以在使用 TM 的程序上使用 GDB,但在事务内部的部分中无法合理使用。
POWER9¶
POWER9 上的 TM 在存储完整寄存器状态时存在问题。这在此提交中有描述
commit 4bb3c7a0208fc13ca70598efd109901a7cd45ae7
Author: Paul Mackerras <paulus@ozlabs.org>
Date: Wed Mar 21 21:32:01 2018 +1100
KVM: PPC: Book3S HV: Work around transactional memory bugs in POWER9
为了解决这个问题,不同的 POWER9 芯片以不同的方式启用了 TM。
在 POWER9N DD2.01 及以下版本中,TM 被禁用。即 HWCAP2[PPC_FEATURE2_HTM] 未设置。
在 POWER9N DD2.1 上,固件将 TM 配置为在发生 tm 挂起时始终中止事务。因此 tsuspend 将导致事务中止并回滚。内核异常也将导致事务中止并回滚,且异常不会发生。如果用户空间构造了一个启用 TM 挂起的 sigcontext,该 sigcontext 将被内核拒绝。该模式通过设置 HWCAP2[PPC_FEATURE2_HTM_NO_SUSPEND] 向用户通告。在此模式下未设置 HWCAP2[PPC_FEATURE2_HTM]。
在 POWER9N DD2.2 及更高版本上,KVM 和 POWERVM 为客户机模拟 TM(如 commit 4bb3c7a0208f 中所述),因此为客户机启用了 TM,即为客户机用户空间设置了 HWCAP2[PPC_FEATURE2_HTM]。大量使用 TM 挂起(tsuspend 或内核挂起)的客户机将导致陷入虚拟机监控程序,从而遭受性能 degradation(性能下降)。宿主机用户空间禁用了 TM,即未设置 HWCAP2[PPC_FEATURE2_HTM]。(尽管如果我们将来将模拟引入宿主机用户空间上下文切换中,我们可能会在某个时候启用它)。
POWER9C DD1.2 及更高版本仅在 POWERVM 上可用,因此 Linux 仅作为客户机运行。在这些系统上,TM 的模拟方式与 POWER9N DD2.2 相同。
从 POWER8 到 POWER9 的客户机迁移可用于 POWER9N DD2.2 和 POWER9C DD1.2。由于较早的 POWER9 处理器不支持 TM 模拟,因此不支持从 POWER8 到 POWER9 的迁移。
内核实现¶
h/rfid mtmsrd 怪癖¶
正如 ISA 中所定义的,rfid 有一个在早期异常处理中很有用的怪癖。当处于用户空间事务中并通过某些异常进入内核时,MSR 最终会变成 TM=0 且 TS=01(即 TM 关闭但 TM 挂起)。通常内核会想要更改 MSR 中的位并执行 rfid 来做到这一点。在这种情况下,rfid 的 SRR0 可以具有 TM = 0 且 TS = 00(即 TM 关闭且非事务),而生成的 MSR 将保留先前的 TM = 0 和 TS=01(即保持挂起状态)。这是架构中的一个怪癖,因为这通常是从 TS=01 到 TS=00(即挂起 -> 非事务)的转换,这是一个非法的转换。
架构在 rfid 的定义中通过以下行描述了这个怪癖
- if (MSR 29:31 ¬ = 0b010 | SRR1 29:31 ¬ = 0b000) then
MSR 29:31 <- SRR1 29:31
hrfid 和 mtmsrd 具有相同的怪癖。
Linux 内核在其早期异常处理中使用了这个怪癖。