本文将介绍 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 进行刷新。