0%

Linux内核ARM64 cpufeature和errata的基本逻辑

基本逻辑

ARM64里先定义了feature/errata的全局的静态描述表,这些是当前内核可以支持的最大
feature/errata列表:struct arm64_cpu_capabilities arm64_features[], arm64_errata[]

如上静态表中的宏可能没有打开,内核重新把运行时的全局feature/errata表记录在:
struct arm64_cpu_capabilities *cpucap_ptrs[]

把经过检测得到的当前系统上支持的feature/errata保存在这个bitmap:system_cpucaps

系统全局的feature寄存器保存的位置,似乎是各个core上的feature寄存器通过一定整理后
保存到这个结构里。对于每个ID寄存器,都在arm64_ftr_regs有静态的定义。

1
2
3
4
5
6
7
8
static const struct __ftr_reg_entry {
u32 sys_id;
struct arm64_ftr_reg *reg;
+-> name/strict_mask/user_mask/sys_val/user_val
+-> struct arm64_ftr_override *override
/* 定义寄存器里各个域段的值,但是只定义了一部分 */
+-> struct arm64_ftr_bits *ftr_bits
} arm64_ftr_regs[] = { ... }

具体的执行流程如下:

1
2
3
4
5
6
7
8
9
10
11
start_kernel
/* arch/arm64/kernel/smp.c */
+-> smp_prepare_boot_cpu
/*
* 这里各个core把CPU ID寄存器中的值读出来保存在per-cpu的cpu_data里,再
* 更新arm64_ftr_regs[]中的对应项。(struct cpuinfo_arm64 *cpu_data.)
*/
+-> cpuinfo_store_boot_cpu
/* todo: 不清楚这里的逻辑 */
+-> init_cpu_features
+-> setup_boot_cpu_features
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
setup_boot_cpu_features
/* 把arm64_features/errata静态表中定义的cap/errata保存到cpucap_ptrs */
+-> init_cpucap_indirect_list
/*
* 注意,这里只处理特定cap类型,全部类型:system/local_cpu/boot_cpu/all。
* 大部分是system的。todo: 各种类型cap的语意?
*/
+-> setup_boot_cpu_capabilities
/*
* 检测对应的特性是否存在,如果存在记录在system_cpucaps,如果是boot_cpu,
* 记录在boot_cpucaps。
*
* 注意,errata也被当作特性,统一考虑。所以下面的函数中的capabilities
* 包括feature和errata。即cap = feature + errata。
*/
+-> update_cpu_capabilities
/* 对于满足条件的特性,调用cpu_enable回调,使能对应cap */
+-> enable_cpu_capabilities

/* 相关逻辑单独考虑 */
+-> apply_boot_alternatives

SCOPE_SYSTEM cap的检测和配置流程如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/* 拉起一号进程的函数 */
kernel_init
+-> kernel_init_freeable
/* 这里启动所有从核 */
+-> smp_init
...
+-> secondary_start_kernel
...
/* SCOPE_LOCAL_CPU cap的检测和配置*/
+-> check_lock_cpu_capabilities
...

/* 这里从核已经起来 */

+-> smp_cpus_done
+-> setup_system_features

KVM虚拟机里的ID寄存器

CPU ID寄存器本质上其实只是一个标记,对应功能的使能会另有寄存器控制。ARM64的vCPU
在EL1读CPU ID寄存器时可能会(可以配置的)trap到EL2,这就给了KVM模拟vCPU CPU ID寄存
器的机会。KVM可以把vCPU的CPU ID寄存器静态写死,QEMU也可以通过KVM_SET_ONE_REG这个
ioctl接口调整KVM vCPU内部数据结构里的CPU ID值。

ARM64 KVM的异常向量表的定义在linux/arch/arm64/kvm/hyp/hyp-entry.S。发生异常,trap
到EL2,执行异常向量,然后执行__guest_exit,然后执行虚拟机退出的处理。所以,vCPU
在EL1读CPU ID寄存器,触发trap到EL2的模拟流程大致如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/* 拉起虚拟机以及虚拟机退出处理逻辑都在这里,我们只关注虚拟机退出的处理 */
kvm_arch_vcpu_ioctl_run
[...]
+-> handle_exit
+-> handle_trap_exceptions
/*
* 通过退出原因拿到对应的处理函数,所有的退出处理函数在arm_exit_handlers
* 这个表里。这里我们主要看访问CPU ID寄存器的trap,这个时候拿到的处理
* 函数应该是kvm_handle_sys_reg。
*/
+-> exit_handler // kvm_handle_sys_reg, 定义在arch/arm64/kvm/sys_regs.c。
+-> desc = &sys_regs_descs[sr_idx]
/*
* 可见是通过access回调去读模拟的CPU ID的值的,读写的操作都是在这个
* 函数里通过具体系统寄存器对应的access回调函数完成的。对于读操作,
* 完整的模拟过程需要把读出来的系统寄存器的值更新到被模拟MRS指令的
* GPR上,这个由下面的vcpu_set_reg完成,这个函数把读到的值更新到vCPU
* 的对应GPR上,下次vCPU上位后,对应GPR自然就是读到的值。
*/
+-> perform_access
+-> vcpu_set_reg

可以看到,这把所有的系统寄存器都定义在sys_regs_descs数组里,不同回调函数完成不同
的功能。比如,access处理KVM模拟guest系统寄存器读写的逻辑,通过ioctl KVM_SET_ONE_REG/
KVM_GET_ONE_REG进来的访问最终会走到get_user/set_user。val提示系统寄存器哪些域段
是ioctl KVM_SET_ONE_REG可写的,多一个控制约束而已。

QEMU可以通过KVM_SET_ONE_REG控制vCPU的CPU ID寄存器的值,从而控制暴露给VM的vCPU特性。
这个特性的一个常见用处就是做不同代CPU上的VM迁移,不同代CPU上的特性可能有不同,VM
在不同代CPU迁移时可能会出问题,一个简单的办法就是QEMU控制VM上的特性,使得VM上的
特性是所有不同种类CPU上特性的最小集。

QEMU里对如上实现的vCPU做了进一步的封装(主要是针对X86 vCPU),定义了种类繁多的vCPU
类型,QEMU里把整个逻辑叫做QEMU CPU model,具体的说明可以参考这里