本文将介绍 Linux 内核中,AMD SEV 机密虚拟机对虚拟机号 ASID 的管理设计,基于的内核版本为 Linux 5.10。
数据结构
AMD SEV 中对 ASID 管理的核心代码位于 arch/x86/kvm/svm/sev.c 中,以下是与之相关的数据结构定义:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| static DEFINE_MUTEX(sev_bitmap_lock);
unsigned int max_sev_asid;
static unsigned int min_sev_asid;
static unsigned long *sev_asid_bitmap;
static unsigned long *sev_reclaim_asid_bitmap;
|
AMD SEV 的 ASID 管理采用了 双位图 的设计,包含一张分配位图 sev_asid_bitmap 和一张回收位图 sev_reclaim_asid_bitmap。这样的设计有助于将分配和回收的操作尽可能分离来提升性能。
初始化
上述数据结构的初始化发生在 sev_hardware_setup 中,它的基本调用栈为:
1 2 3 4
| kvm_arch_hardware_setup -> .hardware_setup -> svm_hardware_setup -> sev_hardware_setup
|
具体初始化操作如下:
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
| int __init sev_hardware_setup(void) { struct sev_user_data_status *status; int rc;
max_sev_asid = cpuid_ecx(0x8000001F);
if (!svm_sev_enabled()) return 1;
min_sev_asid = cpuid_edx(0x8000001F);
sev_asid_bitmap = bitmap_zalloc(max_sev_asid, GFP_KERNEL); if (!sev_asid_bitmap) return 1;
sev_reclaim_asid_bitmap = bitmap_zalloc(max_sev_asid, GFP_KERNEL); if (!sev_reclaim_asid_bitmap) return 1;
[...] }
|
分配与回收
分配
ASID 的分配主要发生在 SEV 虚拟机初始化函数 sev_guest_init 中,它的基本调用栈为:
1 2 3 4
| kvm_arch_vm_ioctl(ioctl: KVM_MEMORY_ENCRYPT_OP) -> .mem_enc_op -> svm_mem_enc_op(sev_cmd.id: KVM_SEV_INIT) -> sev_guest_init
|
它将调用 sev_asid_new 进行 ASID 号的分配:
1 2 3 4 5 6 7 8 9 10 11 12 13
| static int sev_guest_init(struct kvm *kvm, struct kvm_sev_cmd *argp) { struct kvm_sev_info *sev = &to_kvm_svm(kvm)->sev_info; int asid, ret;
[...]
asid = sev_asid_new(); if (asid < 0) return ret;
[...] }
|
分配 ASID 的具体操作如下:
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
| static int sev_asid_new(void) { bool retry = true; int pos;
mutex_lock(&sev_bitmap_lock);
again: pos = find_next_zero_bit(sev_asid_bitmap, max_sev_asid, min_sev_asid - 1); if (pos >= max_sev_asid) { if (retry && __sev_recycle_asids()) { retry = false; goto again; } mutex_unlock(&sev_bitmap_lock); return -EBUSY; }
__set_bit(pos, sev_asid_bitmap);
mutex_unlock(&sev_bitmap_lock);
return pos + 1; }
|
回收
ASID 的回收主要发生在 SEV 虚拟机释放函数 sev_asid_free 中,它的基本调用栈如下:
1 2 3 4 5
| kvm_arch_destroy_vm -> .vm_destroy -> svm_vm_destroy -> sev_vm_destroy -> sev_asid_free
|
它并不是直接将分配位图 sev_asid_bitmap 中对应的位置为空闲,而是设置专门的回收位图 sev_reclaim_asid_bitmap。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| static void sev_asid_free(int asid) { struct svm_cpu_data *sd; int cpu, pos;
mutex_lock(&sev_bitmap_lock);
pos = asid - 1; __set_bit(pos, sev_reclaim_asid_bitmap);
for_each_possible_cpu(cpu) { sd = per_cpu(svm_data, cpu); sd->sev_vmcbs[pos] = NULL; }
mutex_unlock(&sev_bitmap_lock); }
|
现在回到上面分配 ASID 时,在分配位图无空闲位的时候会调用的 __sev_recycle_asids:
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
| static bool __sev_recycle_asids(void) { int pos;
pos = find_next_bit(sev_reclaim_asid_bitmap, max_sev_asid, min_sev_asid - 1); if (pos >= max_sev_asid) return false;
if (sev_flush_asids()) return false;
bitmap_xor(sev_asid_bitmap, sev_asid_bitmap, sev_reclaim_asid_bitmap, max_sev_asid); bitmap_zero(sev_reclaim_asid_bitmap, max_sev_asid);
return true; }
|
其中有一个可能令人困惑的点:就是为什么要进行 TLB 的刷新?我的理解是:此时进行 TLB 刷新其实是一种 懒刷新 (或者说批量刷新)的设计。
一种直观的想法是,在 SEV 虚拟机释放时(sev_asid_free),TLB 中残存的 TLB 条目已经是冗余的,应该进行 TLB 的刷新。但是这会使得每次虚拟机释放都伴随着一次 TLB 的刷新,效率相对较低。事实上,此时即便不进行刷新也并不会影响虚拟地址转换的正确进行,因为在分配位图中,该 ASID 还是处于被占用的状态,它不会被分配给新的虚拟机,因此也就不会发生新创建的虚拟机通过 TLB 残存条目完成错误地址转换的情况。这样安全的状态将持续到回收位图合并到分配位图前,在此之后,新分配的虚拟机将可能被分配得到具有残存 TLB 表项的 ASID 号,因此在此之前需要将 TLB 进行刷新。