0 资料
- SVE spec 3.1 Exception model
- fpsimd.c - arch/arm64/kvm/fpsimd.c - Linux source code (v6.7-rc6) - Bootlin
- https://elixir.bootlin.com/linux/v6.7-rc7/C/ident/kvm_hyp_handle_fpsimd
1 ARMv8 SVE异常模型
1.1 ARMv8的硬件虚拟化支持
armv8在虚拟化扩展中提供了敏感指令和异常的捕获功能,可以通过配置hypervisor控制寄存器 hcr_el2 实现,它将vm执行的敏感指令或异常路由到EL2中,hypervisor捕获到相关异常后可为其模拟相关功能。其中 hcr_el2 寄存器的定义如下:

其中:TGE位只有在支持VHE的架构下才有效。当配置该位后所有需要路由到EL1的异常都会被路由到EL2。因此只要设置该位后,不再设置以上这些位,其对应的异常也会被路由到EL2下。
关于VHE特性支持
VHE是由HCR_EL2中的两个bits决定的。可以将这两个bits的功能总结如下:
- E2H:控制VHE是否使能(用于访问重定向)
- TGE:VHE使能后,控制EL0是host还是guest (用于异常重定向)

通过以上扩展以后,前面的问题看来都已经完美解决了。但是我们再回忆一下type 2 hypervisor的原理,这种方案中hypervisor只是host os的一个组件,由于host os本身就是普通的操作系统(如linux),而其通常被设计为运行在EL1异常等级。
若将其运行在EL2下,则需要在代码层面做较多改动,如在linux中使用vbar_el1访问异常向量表基地址寄存器,此时需要修改为vbar_el2。更麻烦的是el1下支持两个页表基地址寄存器ttbr0_el1和ttbr1_el1,而在armv8.0的el2异常等级中只包含一个ttbr0_el2寄存器。
因此为了不修改host os本身的代码,最初type2 hypervisor方案中host os被设计为运行在el1下,而hypervisor作为它的一个模块运行在EL2下,其架构如下:

此时由于hypervisor和host os位于不同的异常等级,因此它们之间的调用都需要通过异常完成,该流程需要执行上下文的保存和恢复,因此会影响hypervisor的效率。为了解决该问题,arm在armv8.1架构之后增加了vhe特性,从而使得host os不经改动就能运行于EL2中。
vhe主要是为了支持将host os运行在EL2中,以提高hypervisor和虚拟机之间的切换代价。
由于os内核(如linux)被设计为访问EL1异常等级下的系统寄存器,因此为了在不修改代码的情况下使其实际访问EL2寄存器,vhe实现了**寄存器重定向功能。**即在使能vhe之后,所有EL1寄存器的访问操作都被硬件转换为对相应EL2寄存器的访问,如OS访问ttbr0_el1寄存器,则会按下图方式被重定向到ttbr0_el2:

当然这里还有一个问题,hypervisor实际上还是有访问实际el1寄存器的需求,因此vhe也对其做了扩展,当访问实际el1寄存器时,则需要使用新的特殊指令。如需要访问实际的ttbr0_el1时,则可通过下图所示访问ttbr0_el12的方式实现:

当支持vhe且hcr.tge被设置后,不管hcr_el2.imo/fmo/amo是否被设置,所有EL0和EL1的异常都会被路由到EL2中。有了以上扩展之后,host os就可以不经任何修改,而像运行在EL1中一样运行于EL2中,此时系统架构变为如下形式:

SVE在EL3、EL2和EL1级别上添加了分层陷阱和使能控制。这些控制是通过以下系统寄存器字段来实现的:
- CPTR_EL3.EZ
- CPTR_EL2.TZ, when HCR_EL2.E2H == 0
- CPTR_EL2.ZEN, when HCR_EL2.E2H == 1
- CPACR_EL1.ZEN
其中,guest os运行在EL1,hypervisor运行在EL2,即 CPACR_EL1 控制了从 guest user/EL0 到 guest os/EL1 的trap,而 CPTR 控制了从 guest os/EL1 到 hypervisor/EL2 的trap。
1.2 CPACR_EL1
作用:控制跟踪、SVE、高级SIMD和浮点功能的访问。
当在当前安全状态下实施并启用EL2,并且HCR_EL2.{E2H, TGE} == {1, 1}时,该寄存器中的字段对EL0和EL1的执行没有影响。在这种情况下,由CPTR_EL2提供的控制被使用。
FPEN
当执行状态为EL1和EL0的指令访问高级SIMD和浮点寄存器时,会被陷阱捕获。如果EL2被实现并在当前安全状态中启用,且HCR_EL2.TGE为1,则会报告为ESR_ELx.EC值0x00到EL2。如果EL2未启用,则报告为ESR_ELx.EC值0x07到EL1。
- 在AArch64状态下,对FPCR、FPSR以及任何SIMD和浮点寄存器V0-V31的访问,包括它们作为D0-D31寄存器或S0-S31寄存器的视图;
将SVE指令在EL1和EL0的执行转移到EL1,或在当前安全状态下EL2已实现且启用时转移到EL2,此时HCR_EL2.TGE为1。异常使用ESR_ELx.EC值0x07进行报告。
由于CPACR_EL1.ZEN引发的陷阱具有优先权,优先于由CPACR_EL1.FPEN引发的陷阱。

ZEN
将SVE指令和直接访问ZCR_EL1系统寄存器的指令在EL1和EL0执行时陷入EL1,或在当前安全状态下实现和启用EL2时陷入EL2,且HCR_EL2.TGE为1。异常使用ESR_ELx.EC值0x19进行报告。由于CPACR_EL1.ZEN引发的陷阱优先于由CPACR_EL1.FPEN引发的陷阱。

MRS/MSR CPACR_EL1

访问重定向。
1.3 CPTR_EL2
作用:控制将对CPACR、CPACR_EL1、跟踪、活动监视器、SVE以及高级SIMD和浮点功能的访问陷入到EL2。

在当前安全状态下,当EL2被启用时,对访问高级SIMD和浮点寄存器的指令在EL2、EL1和EL0的执行陷入到EL2。异常情况使用ESR_ELx.EC值0x07进行报告。
在当前安全状态下,将SVE指令在EL2、EL1和EL0级别的执行陷入到EL2。异常使用ESR_ELx.EC值0x07进行报告。
由于CPTR_EL2.ZEN导致的陷阱优先于由CPTR_EL2.FPEN导致的陷阱:


疑问:
CPTR_EL2.FPEN/ZEN = 0b01,这种配置应用于什么场景?
1
2
3
4
5允许EL1执行,但禁止EL0执行
2 KVM: FPSIMD/SVE虚拟化支持
2.1 数据结构
1 | struct kvm_vcpu_arch { |

2.2 框架分析
相关函数:
1 | // arch/arm64/kvm/fpsimd.c |
代码流程:
1 | // vcpu首次运行和再次进入循环 |
- 关于TIF_FOREIGN_FPSTATE在哪些场景下设置、取消、更新?
该标识的作用是:判断当前pCPU上加载的是否为被调度线程的最近用户态上下文,其值为FALSE时可以减少一次上下文的恢复工作。该标识的引入可以将调度执行流中的SIMD恢复工作解耦出来并推迟到返回用户态之前。
一些情况会影响TIF_FOREIGN_FPSTATE,除了该标识的合法设置方式外(通过两个变量以判断同步情况)。因为除了用户态,内核态也可能使用FPSIMD能力,此时硬件FPSIMD状态的持有者就不再是用户了,用户应该即时保存这部分状态,同时设置TIF_FOREIGN_FPSTATE。
- 内核态中断/抢占场景下,FPSIMD状态应该如何维护?
调用链:preempt_enable => _schedule => context_switch => __switch_to => fpsimd_thread_switch
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26 >void fpsimd_thread_switch(struct task_struct *next)
>{
bool wrong_task, wrong_cpu;
if (!system_supports_fpsimd())
return;
__get_cpu_fpsimd_context();
/* Save unsaved fpsimd state, if any: */
fpsimd_save();
/*
* Fix up TIF_FOREIGN_FPSTATE to correctly describe next's
* state. For kernel threads, FPSIMD registers are never loaded
* and wrong_task and wrong_cpu will always be true.
*/
wrong_task = __this_cpu_read(fpsimd_last_state.st) !=
&next->thread.uw.fpsimd_state;
wrong_cpu = next->thread.fpsimd_cpu != smp_processor_id();
update_tsk_thread_flag(next, TIF_FOREIGN_FPSTATE,
wrong_task || wrong_cpu);
__put_cpu_fpsimd_context();
>}
vcpu首次进入guest,guest首次执行FPSIMD指令陷入hypervisor,保存恢复上下文后再次返回guest
guest已持有FPSIMD状态,再次陷入hypervisor,处理后返回guest执行:(陷入原因)
vcpu线程退出,hypervisor处理后直接返回
内层循环
外层循环
vcpu线程退出,需要在用户态处理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82 kvm_arch_vcpu_put_fp
+-> fpsimd_save_and_flush_cpu_state
+-> fpsimd_save()
+-> fpsimd_flush_cpu_state()
/*
* Ensure FPSIMD/SVE storage in memory for the loaded context is up to
* date with respect to the CPU registers. Note carefully that the
* current context is the context last bound to the CPU stored in
* last, if KVM is involved this may be the guest VM context rather
* than the host thread for the VM pointed to by current. This means
* that we must always reference the state storage via last rather
* than via current, if we are saving KVM state then it will have
* ensured that the type of registers to save is set in last->to_save.
*/
static void fpsimd_save(void);
这段话大致意思是要确保内存中加载的上下文中的FPSIMD/SVE存储与CPU寄存器的状态是最新的。需要注意的是,当前上下文是最后绑定到CPU的上下文,存储在last中。如果涉及到KVM,这可能是指与current指向的VM主线程相关的guest VM上下文,而不是主机线程的上下文。这意味着我们必须始终通过last引用状态存储,而不是通过current引用。如果正在保存KVM状态,那么last->to_save中将确保设置要保存的寄存器类型。
/*
* Invalidate any task's FPSIMD state that is present on this cpu.
* The FPSIMD context should be acquired with get_cpu_fpsimd_context()
* before calling this function.
*/
static void fpsimd_flush_cpu_state(void)
{
WARN_ON(!system_supports_fpsimd());
__this_cpu_write(fpsimd_last_state.st, NULL);
/*
* Leaving streaming mode enabled will cause issues for any kernel
* NEON and leaving streaming mode or ZA enabled may increase power
* consumption.
*/
if (system_supports_sme())
sme_smstop();
set_thread_flag(TIF_FOREIGN_FPSTATE);
}
do_notify_resume
+-> fpsimd_restore_current_state
/*
* Load the userland FPSIMD state of 'current' from memory, but only if the
* FPSIMD state already held in the registers is /not/ the most recent FPSIMD
* state of 'current'. This is called when we are preparing to return to
* userspace to ensure that userspace sees a good register state.
*/
void fpsimd_restore_current_state(void)
{
/*
* TIF_FOREIGN_FPSTATE is set on the init task and copied by
* arch_dup_task_struct() regardless of whether FP/SIMD is detected.
* Thus user threads can have this set even when FP/SIMD hasn't been
* detected.
*
* When FP/SIMD is detected, begin_new_exec() will set
* TIF_FOREIGN_FPSTATE via flush_thread() -> fpsimd_flush_thread(),
* and fpsimd_thread_switch() will set TIF_FOREIGN_FPSTATE when
* switching tasks. We detect FP/SIMD before we exec the first user
* process, ensuring this has TIF_FOREIGN_FPSTATE set and
* do_notify_resume() will call fpsimd_restore_current_state() to
* install the user FP/SIMD context.
*
* When FP/SIMD is not detected, nothing else will clear or set
* TIF_FOREIGN_FPSTATE prior to the first return to userspace, and
* we must clear TIF_FOREIGN_FPSTATE to avoid do_notify_resume()
* looping forever calling fpsimd_restore_current_state().
*/
if (!system_supports_fpsimd()) {
clear_thread_flag(TIF_FOREIGN_FPSTATE);
return;
}
get_cpu_fpsimd_context();
if (test_and_clear_thread_flag(TIF_FOREIGN_FPSTATE)) {
task_fpsimd_load();
fpsimd_bind_task_to_cpu();
}
put_cpu_fpsimd_context();
}
- vcpu线程被host调度
//TODO
最后整理于 2025-11-11