AMD SEV机密虚拟机ASID管理
本文将介绍 Linux 内核中,AMD SEV 机密虚拟机对虚拟机号 ASID 的管理设计,基于的内核版本为 Linux 5.10。
数据结构
AMD SEV 中对 ASID 管理的核心代码位于 arch/x86/kvm/svm/sev.c
中,以下是与之相关的数据结构定义:
/* 定义保护位图的互斥锁 */
static DEFINE_MUTEX(sev_bitmap_lock);
/* SEV 同时支持的最大 ASID */
unsigned int max_sev_asid;
/* SEV 应该使用的最小的 ASID */
static unsigned int min_sev_asid;
/* 待分配的 ASID 位图 */
static unsigned long *sev_asid_bitmap;
/* 回收的 ASID 位图 */
static unsigned long *sev_reclaim_asid_bitmap;
AMD SEV 的 ASID 管理采用了 双位图 的设计,包含一张分配位图 sev_asid_bitmap
和一张回收位图 sev_reclaim_asid_bitmap
。这样的设计有助于将分配和回收的操作尽可能分离来提升性能。
初始化
上述数据结构的初始化发生在 sev_hardware_setup
中,它的基本调用栈为:
kvm_arch_hardware_setup
-> .hardware_setup
-> svm_hardware_setup
-> sev_hardware_setup
具体初始化操作如下:
int __init sev_hardware_setup(void)
{
struct sev_user_data_status *status;
int rc;
/* 通过硬件探测初始化最大 ASID */
max_sev_asid = cpuid_ecx(0x8000001F);
if (!svm_sev_enabled())
return 1;
/* 初始化最小 ASID */
min_sev_asid = cpuid_edx(0x8000001F);
/* 初始化 ASID 位图 */
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;
/* 后面为与 SEV 平台状态相关的初始化,我们在此不关注 */
[...]
}
分配与回收
分配
ASID 的分配主要发生在 SEV 虚拟机初始化函数 sev_guest_init
中,它的基本调用栈为:
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 号的分配:
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 的具体操作如下:
static int sev_asid_new(void)
{
bool retry = true;
int pos;
mutex_lock(&sev_bitmap_lock);
again:
/* 在位图 sev_asid_bitmap 中找 (min_sev_asid - 1, sev_asid_bitmap] 之间的空闲位 */
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);
/* +1 是因为 ASID 从 1 开始(0 为 hypervisor 所有) */
return pos + 1;
}
回收
ASID 的回收主要发生在 SEV 虚拟机释放函数 sev_asid_free
中,它的基本调用栈如下:
kvm_arch_destroy_vm
-> .vm_destroy
-> svm_vm_destroy
-> sev_vm_destroy
-> sev_asid_free
它并不是直接将分配位图 sev_asid_bitmap
中对应的位置为空闲,而是设置专门的回收位图 sev_reclaim_asid_bitmap
。
static void sev_asid_free(int asid)
{
struct svm_cpu_data *sd;
int cpu, pos;
mutex_lock(&sev_bitmap_lock);
/* 将当前 ASID 对应的回收位图中对应的位置为 1 */
pos = asid - 1;
__set_bit(pos, sev_reclaim_asid_bitmap);
/* 清除 vmcb 结构 */
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
:
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;
/* 刷新 TLB */
if (sev_flush_asids())
return false;
/*
* 将回收位图合并到分配位图中
* (分配:1, 回收:1) -> (分配:0)
* (分配:1, 回收:0) -> (分配:1)
*/
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 进行刷新。