gmem (guest_memfd)
gmem 的全称可能是 guest memfd。
和 UPM 不同,gmem 不再使用 tmpfs 作为 backend 了,而是自创的 file system kvm_guest_memory
。
KVM_SET_MEMORY_ATTRIBUTES
和 fallocate(gmem_fd)
(PUNCH HOLE)之间的区别
KVM_SET_MEMORY_ATTRIBUTES | PUNCH_HOLE | |
---|---|---|
更新 XArray | ✔️ | |
KVM 页表 SPTE 更新 | ✔️ | ✔️ |
TDX Module 页表更新(PAGE.REMOVE ) |
✔️ | ✔️ |
filemap 更新 unpin page | ✔️ |
KVM_SET_MEMORY_ATTRIBUTES
的参数是一段 GFN (start, end),以及这一段 GFN 需要被置上的 attributes。主要负责设置 xarray,同时在 TDX Module 中 drop 掉 page:tdh.mem.page.remove
,也就是 unmap 掉 TDX Module(当然还有 VMM)中 SEPT 中对于 GPA -> HPA
的映射。但是因为我们的 gmem 里的 filemap 没有被 invalidate,page 还是被 pin 着的,下一次我们从 gmem 里 get 一个 gfn 对应的 pfn 的时候(kvm_gmem_get_pfn()
)还是那一个 page。PFN 并没有变。
fallocate(gmem_fd)
并没有动 xarray,而是也在 KVM 中移除了 GPA->HPA
的映射,并执行了 tdh.mem.page.remove
移除 TDX Module 里的映射,之后还对 filemap 进行了更新,表示我们 gmem 的这段内存空了,后面再分配我们不能保证和之前是同一个页(同一个 PFN 了)。执行了 TDH.PHYMEM.PAGE.WBINVD
。感觉主要是为了 unmap kernel 这里 HVA -> HPA
的映射,也就是说无意改变 page 的属性,而是仅仅把这片 GFN 空间给 unmap 掉。
大多数时候这两个 call 需要被同时调用:尽管它们在中间的功能有一些重合(对于 KVM 和 TDX 页表的更新)。但是也没关系,重合就重合了。就像注释里说的:
kvm_convert_memory_private_mr
// 这里调用了 SET_ATTRUBUTES
kvm_set_memory_attributes_private
//...
if (need_discard) {
/*
* With KVM_SET_MEMORY_ATTRIBUTES by kvm_set_memory_attributes(),
* this operation on underlying fd is **only for releasing unnecessary pages**.
* 因为毕竟页表都已经更新过了。把 page release 的原因可能是因为我们不能保证
* 还会 convert 回 private,如果后面不会再 convert 回 private,那么这部分 page
* 还在 filemap 里 pin 着回占用系统的内存空间,所以应该释放掉。
*/
ram_block_convert_range(rb, offset, size, to_private);
}
Gmem QEMU Implementation
[RFC PATCH v2 00/21] QEMU gmem implemention - Xiaoyao Li
Add gmem support in QEMU's RAMBlock
so that each RAM can have both hva-based shared memory and gmem_fd based private memory. 可以看到,gmem 和 UPM 的区别是不需要 shared memory 也使用 memfd 的形式了。
struct RAMBlock {
//...
int gmem_fd;
//...
}
目前是通过 QEMU memory backend 的 cmdline 加上 private=on
来指定的:
$qemu -object sw-protected-vm,id=sp-vm0 \
-object memory-backend-ram,id=mem0,size=1G,private=on \
-machine q35,kernel_irqchip=split,confidential-guest-support=sp-vm0,memory-backend=mem0 \
Memory region 是否含有 private memory 是通过判断其包含的 RAMBlock 是否设置了 gmem_fd
来确定的,也就是:
bool memory_region_has_gmem_fd(MemoryRegion *mr)
{
return mr->ram_block && mr->ram_block->gmem_fd >= 0;
}
好像 UPM reserve 必须是 off。gmem 这里 reserve 应该可以为 true 了。
[RFC PATCH v2 02/21] RAMBlock: Add support of KVM private gmem
RAM_KVM_GMEM
是 RAMBlock->flags
可能置上的一个 bit。
当用户在 QEMU cmdline 指定了比如说 TDX 时,backend->kvm_gmem
会被置为 true,从而会为创建的 RAMBlock
的 flags 置上 RAM_KVM_GMEM
这个 bit。
kvm_create_guest_memfd()
QEMU
当 RAMBlock
的 RAM_KVM_GMEM
这个 flag 置上时,我们不仅会为 RB 创建普通内存,也会申请一个 gmem_fd
。
ram_block_add
if (kvm_enabled() && new_block->flags & RAM_KVM_GMEM && new_block->gmem_fd < 0)
new_block->gmem_fd = kvm_create_guest_memfd()
int kvm_create_guest_memfd(uint64_t size, uint64_t flags, Error **errp)
{
//...
struct kvm_create_guest_memfd gmem = {
.size = size,
.flags = flags,
};
//...
fd = kvm_vm_ioctl(kvm_state, KVM_CREATE_GUEST_MEMFD, &gmem);
//...
}
HostMem: Add mechanism to opt in kvm gmem via MachineState
Add a new member "kvm_gmem" to memory backends. When it's set to true, it enables RAM_KVM_GMEM in ram_flags.
The mechanism is that MachineState::require_kvm_gmem is supposed to be set by any VMs that requires KVM gmem as private memory, e.g., TDX VM.
Kvm: Enable KVM_SET_USER_MEMORY_REGION2
for memslot
Switch to KVM_SET_USER_MEMORY_REGION2
when supported by KVM. 也就是说尽可能使用 KVM_SET_USER_MEMORY_REGION2
。
With KVM_SET_USER_MEMORY_REGION2
, QEMU can set up memory region that backend'ed both by hva-based shared memory and gmem fd based private memory.
Kvm: Introduce support for memory_attributes
kvm_set_memory_attributes()
/ kvm_set_memory_attributes_private()
/ kvm_set_memory_attributes_shared()
QEMU
不言而喻。
static int kvm_set_memory_attributes(hwaddr start, hwaddr size, uint64_t attr)
{
struct kvm_memory_attributes attrs;
int r;
attrs.attributes = attr;
attrs.address = start;
attrs.size = size;
attrs.flags = 0;
r = kvm_vm_ioctl(kvm_state, KVM_SET_MEMORY_ATTRIBUTES, &attrs);
// error handling...
return r;
}
int kvm_set_memory_attributes_private(hwaddr start, hwaddr size)
{
return kvm_set_memory_attributes(start, size, KVM_MEMORY_ATTRIBUTE_PRIVATE);
}
int kvm_set_memory_attributes_shared(hwaddr start, hwaddr size)
{
return kvm_set_memory_attributes(start, size, 0);
}
kvm/memory: Introduce the infrastructure to set the default shared/private value
Introduce new flag RAM_DEFAULT_PRIVATE
for RAMBlock (ram_block->flags
). It's used to indicate the default attribute, private or not. 还记得吗,之前我们也为 ram_block->flags
引入了一个 RAM_KVM_GMEM
flag。
Set the RAM range to private explicitly when it's default private.
static void kvm_set_phys_mem(KVMMemoryListener *kml, MemoryRegionSection *section, bool add)
{
KVMSlot *mem;
int err;
MemoryRegion *mr = section->mr;
bool writable = !mr->readonly && !mr->rom_device;
hwaddr start_addr, size, slot_size, mr_offset;
ram_addr_t ram_start_offset;
void *ram;
if (!memory_region_is_ram(mr)) {
if (writable || !kvm_readonly_mem_allowed) {
return;
} else if (!mr->romd_mode) {
/* If the memory device is not in romd_mode, then we actually want
* to remove the kvm memory slot so all accesses will trap. */
add = false;
}
}
size = kvm_align_section(section, &start_addr);
if (!size) {
return;
}
/* The offset of the kvmslot within the memory region */
mr_offset = section->offset_within_region + start_addr -
section->offset_within_address_space;
/* use aligned delta to align the ram address and offset */
ram = memory_region_get_ram_ptr(mr) + mr_offset;
ram_start_offset = memory_region_get_ram_addr(mr) + mr_offset;
if (!add) {
do {
slot_size = MIN(kvm_max_slot_size, size);
mem = kvm_lookup_matching_slot(kml, start_addr, slot_size);
if (!mem) {
return;
}
if (mem->flags & KVM_MEM_LOG_DIRTY_PAGES) {
/*
* NOTE: We should be aware of the fact that here we're only
* doing a best effort to sync dirty bits. No matter whether
* we're using dirty log or dirty ring, we ignored two facts:
*
* (1) dirty bits can reside in hardware buffers (PML)
*
* (2) after we collected dirty bits here, pages can be dirtied
* again before we do the final KVM_SET_USER_MEMORY_REGION to
* remove the slot.
*
* Not easy. Let's cross the fingers until it's fixed.
*/
if (kvm_state->kvm_dirty_ring_size) {
kvm_dirty_ring_reap_locked(kvm_state, NULL);
if (kvm_state->kvm_dirty_ring_with_bitmap) {
kvm_slot_sync_dirty_pages(mem);
kvm_slot_get_dirty_log(kvm_state, mem);
}
} else {
kvm_slot_get_dirty_log(kvm_state, mem);
}
kvm_slot_sync_dirty_pages(mem);
}
/* unregister the slot */
g_free(mem->dirty_bmap);
mem->dirty_bmap = NULL;
mem->memory_size = 0;
mem->flags = 0;
err = kvm_set_user_memory_region(kml, mem, false);
if (err) {
fprintf(stderr, "%s: error unregistering slot: %s\n",
__func__, strerror(-err));
abort();
}
start_addr += slot_size;
size -= slot_size;
kml->nr_used_slots--;
} while (size);
return;
}
/* register the new slot */
do {
slot_size = MIN(kvm_max_slot_size, size);
mem = kvm_alloc_slot(kml);
mem->as_id = kml->as_id;
mem->memory_size = slot_size;
mem->start_addr = start_addr;
mem->ram_start_offset = ram_start_offset;
mem->ram = ram;
mem->flags = kvm_mem_flags(mr);
mem->gmem_fd = mr->ram_block->gmem_fd;
mem->ofs = (uint8_t*)ram - mr->ram_block->host;
kvm_slot_init_dirty_bitmap(mem);
err = kvm_set_user_memory_region(kml, mem, true);
// err handling
if (memory_region_is_default_private(mr)) {
err = kvm_set_memory_attributes_private(start_addr, slot_size);
// err handling
//...
}
start_addr += slot_size;
ram_start_offset += slot_size;
ram += slot_size;
size -= slot_size;
kml->nr_used_slots++;
} while (size);
}
[RFC PATCH v2 05/21] kvm: Enable KVM_SET_USER_MEMORY_REGION2 for memslot
QEMU does the shared-private conversion on KVM_MEMORY_EXIT
and discards the memory.
ram_block_convert_range()
QEMU
int ram_block_convert_range(RAMBlock *rb, uint64_t start, size_t length,
bool shared_to_private)
{
// sanity checks
if (shared_to_private) {
//...
// 如果时 shared_to_private 的情况,那么我们需要从 normal memfd 里 drop 掉对应的 page
// 然后在 gmemfd 里重新分配 private 的 page,原因很简单,memfd 里的 page 并不是加密的
fd = rb->fd;
} else {
// 如果要转回 shared,那么我们需要把 gmem 里的 private 的 page drop 掉。
fd = rb->gmem_fd;
}
return ram_block_discard_range_fd(rb, start, length, fd);
}
kvm_convert_memory()
QEMU
static int kvm_convert_memory(hwaddr start, hwaddr size, bool to_private)
{
//...
// 当我们起 CGS guest 时,我们是有两个 fd 的:rb-fd 和 rb->gmem_fd
// convert memory 包含两个步骤,一个是 drop 原来的,一个是 alloc 新的
// 此处负责 alloc 新的,但是并不是直接 alloc,而是先只是把 attribute
// 置上,等到发生 page fault 的时候再进行分配?无论我们要 private 还是 shared 的 page
if (to_private) {
ret = kvm_set_memory_attributes_private(start, size);
} else {
ret = kvm_set_memory_attributes_shared(start, size);
}
//...
addr = memory_region_get_ram_ptr(section.mr) + section.offset_within_region;
rb = qemu_ram_block_from_host(addr, false, &offset);
/*
* With KVM_SET_MEMORY_ATTRIBUTES by kvm_set_memory_attributes(),
* operation on underlying file descriptor is only for releasing
* unnecessary pages.
*/
// 根据转的方向,决定如何 drop 掉原来的 page:fd 还是 gmem_fd。
ram_block_convert_range(rb, offset, size, to_private);
//...
}
Gmem KVM Implementation
[RFC PATCH v12 00/33] KVM: guest_memfd() and per-page attributes - Sean Christopherson
目前最新版本是 v12, v10 以及之前是 Peng, Chao 发的,名字是 UPM,后来 Sean 发的叫做 gmem。
Motivation: If a TDX host accesses TDX-protected guest memory, machine check can happen which can further crash the running host system, this is terrible for multi-tenant configurations. The host accesses include those from KVM userspace like QEMU.
实现分为两部分,一部分是 base patch set,另一部分为 several fixes(可能因为 Sean 不想发下一版了?)
KVM_EXIT_MEMORY_FAULT
This is an exit reason to let KVM notify QEMU that some memory related issues need to be handled.
kvm_range_has_memory_attributes
KVM
// Returns true if gfns in the range [@start, @end) have attributes * matching @attrs.
// 只要有一个 gfn match attribute 就行
bool kvm_range_has_memory_attributes(struct kvm *kvm, gfn_t start, gfn_t end, unsigned long attrs)
{
XA_STATE(xas, &kvm->mem_attr_array, start);
unsigned long index;
bool has_attrs;
void *entry;
//...
has_attrs = true;
for (index = start; index < end; index++) {
// ...
entry = xas_next(&xas);
// ...
if (xas.xa_index != index || xa_to_value(entry) != attrs) {
has_attrs = false;
break;
}
}
//...
return has_attrs;
}
kvm_handle_gfn_range()
/ virt/kvm/kvm_main.c
注意,有一个重名的函数定义在 arch/x86/kvm/mmu/mmu.c
中。需要区分。
参考 __kvm_handle_hva_range()
。
kvm_vm_set_mem_attributes()
KVM
做了两件事:
- 在 XArray 中更改 page 的属性;
- 在 KVM 的页表中移除
GFN->PFN
的映射,同时调用 REMOVE 从 TDX Module 的页表中移除此页。
/* Set @attributes for the gfn range [@start, @end). */
static int kvm_vm_set_mem_attributes(struct kvm *kvm, gfn_t start, gfn_t end, unsigned long attributes)
{
// 这里并没有设置 arg attribute,所以这里的 unmap 会
struct kvm_mmu_notifier_range unmap_range = {
.start = start,
.end = end,
.handler = kvm_mmu_unmap_gfn_range,
.on_lock = kvm_mmu_invalidate_begin,
.before_unlock = (void *)kvm_null_fn,
.on_unlock = (void *)kvm_null_fn,
.flush_on_ret = true,
.may_block = true,
};
struct kvm_mmu_notifier_range post_set_range = {
.start = start,
.end = end,
.arg.attributes = attributes,
.handler = kvm_arch_post_set_memory_attributes,
.on_lock = (void *)kvm_null_fn,
.before_unlock = kvm_mmu_invalidate_end,
.on_unlock = (void *)kvm_null_fn,
.may_block = true,
};
void *entry;
//...
entry = attributes ? xa_mk_value(attributes) : NULL;
//...
// 出于安全性考虑,先 reserve 一下这段 GFN range 对应的 xarray
for (i = start; i < end; i++) {
r = xa_reserve(&kvm->mem_attr_array, i, GFP_KERNEL_ACCOUNT);
//...
}
// 对于每一个 gfn 区间,调用 on_lock() 和 handler,也就
// kvm_mmu_invalidate_begin 和 kvm_arch_pre_set_memory_attributes
// 主要就是把 KVM 里的 spt 里的对应区间移除掉,如果是 TDX 可能还会 PAGE.REMOVE(handle_changed_spte)
kvm_handle_gfn_range(kvm, &unmap_range);
// 这是中间真正 set 的部分
// 对于每一个 gfn,设置其 attributes
for (i = start; i < end; i++) {
r = xa_err(xa_store(&kvm->mem_attr_array, i, entry, GFP_KERNEL_ACCOUNT));
//...
}
// 对于每一个 gfn 区间,调用 on_lock() 和 handler,也就
// kvm_mmu_invalidate_end 和 kvm_arch_post_set_memory_attributes
// 好像主要是关于 huge page 相关的。
kvm_handle_gfn_range(kvm, &post_set_range);
//...
}
[RFC PATCH v12 01/33] KVM: Tweak kvm_hva_range and hva_handler_t to allow reusing for gfn ranges
就是重命名了下 "struct kvm_hva_range
" 到 "kvm_mmu_notifier_range
"。因为后者更能反映作用:让 MMU Notifier 传进来表示哪个空间有 update。
还有一个地方就是把
typedef bool (*hva_handler_t)(struct kvm *kvm, struct kvm_gfn_range *range);
重命名成为了
typedef bool (*gfn_handler_t)(struct kvm *kvm, struct kvm_gfn_range *range);
也是因为后者更能反应作用:handle 的是一片 GFN 而不是 HVA。
基本上 No function update intended。
[RFC PATCH v12 02/33] KVM: Use gfn instead of hva for mmu_notifier_retry
在发生 page fault 并 handle 的时候,会 check 看看这个 page fault 是不是 stale 的,如果是表示这个 page fault 过期了,那么我们就没有必要去处理了。整个 check 是基于 hva 的,详见 mmu_invalidate_retry_hva
。
但是因为引入 private memory 后,page fault 不一定会附带 hva 信息。所以我们改为 check gfn。
原来 hva 是在 kvm_mmu_notifier_range->lock()
的时候设置在 mmu_invalidate_range_start
和 mmu_invalidate_range_end
里的,这个 patch 将这一步改到了 kvm_mmu_notifier_range->handler()
中。因为在调用 handler 的时候,hva 已经转成了 gfn,因此我们已经有了 gfn 信息。
[RFC PATCH v12 03/33] KVM: PPC: Drop dead code related to KVM_ARCH_WANT_MMU_NOTIFIER
Minor change.
[RFC PATCH v12 04/33] KVM: PPC: Return '1' unconditionally for KVM_CAP_SYNC_MMU
Minor change.
[RFC PATCH v12 05/33] KVM: Convert KVM_ARCH_WANT_MMU_NOTIFIER to CONFIG_KVM_GENERIC_MMU_NOTIFIER
就像 subject 说的那样。
[RFC PATCH v12 06/33] KVM: Introduce KVM_SET_USER_MEMORY_REGION2
添加了一个新的 KVM CAP 表示是否支持 REGION2。
添加了一个新的 ioctl KVM_SET_USER_MEMORY_REGION2。
添加了一个新的结构体:kvm_userspace_memory_region2
,是对结构体 kvm_userspace_memory_region
的扩展(直接在后面加了几个 field,可以保证兼容性?)。加的两个主要的 fields 是 __u64 gmem_offset
和 __u32 gmem_fd
。
struct kvm_userspace_memory_region2
KVM, QEMU
struct kvm_userspace_memory_region2 {
__u32 slot;
__u32 flags;
__u64 guest_phys_addr;
__u64 memory_size;
__u64 userspace_addr;
// ----- 上面都是 kvm_userspace_memory_region 原有的内容 -----
// QEMU 你不是要插一个新的内存条吗,你得告诉我对应 gmem_fd 的哪片区域
// 这样当我想访问某一个 GPA 的时候,我就知道要从 gmem_fd 的 gmem_offset 处
// 从 gmem_fd 处拿数据了。可以看 kvm_gmem_bind()
__u64 gmem_offset;
// gmem_fd 是 QEMU 先 create 了 fd 之后,再传给 KVM 的
__u32 gmem_fd;
__u32 pad1;
__u64 pad2[14];
};
?[RFC PATCH v12 07/33] KVM: Add KVM_EXIT_MEMORY_FAULT exit to report faults to userspace
KVM_EXIT_MEMORY_FAULT
indicates the vCPU has encountered a memory fault that could not be resolved by KVM.
KVM_EXIT
开头,鉴定为 KVM exit 到 QEMU 的 reason。用来告诉 QEMU implicit conversions^ between private and shared memory。
Note! KVM_EXIT_MEMORY_FAULT is unique among all KVM exit reasons in that it accompanies a return code of '-1', not '0'! errno will always be set to EFAULT or EHWPOISON when KVM exits with KVM_EXIT_MEMORY_FAULT, userspace should assume kvm_run.exit_reason is stale/undefined for all other error numbers.
这个原因没太看懂。
[RFC PATCH v12 08/33] KVM: Add a dedicated mmu_notifier flag for reclaiming freed memory
好像是 AMD SEV 相关的。
[RFC PATCH v12 09/33] KVM: Drop .on_unlock() mmu_notifier hook
把 kvm_mmu_notifier_range
里的 on_unlock()
去掉了。去掉的原因是:
as doing so resulted in .on_lock()
and .on_unlock()
having divergent and asymmetric behavior, and set future developers up for failure, i.e. all but asked for bugs where KVM relied using .on_unlock() to try to run a callback while holding mmu_lock.
Opportunistically add a lockdep assertion in kvm_mmu_invalidate_end() to guard against future bugs of this nature.
[RFC PATCH v12 10/33] KVM: Set the stage for handling only shared mappings in mmu_notifier events
Add 2 flags to struct kvm_gfn_range
:
struct kvm_gfn_range {
//...
bool only_private;
bool only_shared;
//...
};
这样有三种 valid 的组合,only private,only shared 和 private-shared mixed。
可以看函数 kvm_tdp_mmu_unmap_gfn_range()
对于这四种情况的处理。
[RFC PATCH v12 11/33] KVM: Introduce per-page memory attributes
KVM CAP: KVM_CAP_MEMORY_ATTRIBUTES
KVM IOCTL: KVM_SET_MEMORY_ATTRIBUTES
, KVM_GET_SUPPORTED_MEMORY_ATTRIBUTES
KConfig: CONFIG_KVM_GENERIC_MEMORY_ATTRIBUTES
Introduce two ioctls (advertised by KVM_CAP_MEMORY_ATTRIBUTES
) to allow userspace to operate on the per-page memory attributes:
-
KVM_SET_MEMORY_ATTRIBUTES
to set the per-page memory attributes to a guest memory range. -
KVM_GET_SUPPORTED_MEMORY_ATTRIBUTES
to return the KVM supported memory attributes. 注意,是获取支持的 memory attributes,而不是拿到某一个 page 的 attributes,这意味着对于上面的 SET ioctl,我们并没有一个对应的 GET ioctl。这么做的原因在这个 patchset 的一个 comment 里 Sean 写明了:The plan is that memory attributes will be 100% userspace driven, i.e. that KVM will never add its own attributes. That's why there is (currently) noKVM_GET_MEMORY_ATTRIBUTES
, the intended usage model is that userspace is fully responsible for managing attributes, and so should never need to query information that it already knows.
Use an xarray to store the per-page attributes internally. 这个 xarray 是全局的,也就是记录了整个 VM 的 memory。
struct kvm {
//...
#ifdef CONFIG_KVM_GENERIC_MEMORY_ATTRIBUTES
// 这个 xarray 的 index 是每一个 gfn
struct xarray mem_attr_array;
#endif
//...
};
Setting memory attributes is roughly analogous to mprotect()
on memory that is mapped into the guest.
[RFC PATCH v12 12/33] mm: Add AS_UNMOVABLE to mark mapping as completely unmovable
没太看懂。
[RFC PATCH v12 13/33] security: Export security_inode_init_security_anon() for use by KVM
expose 了一下 security/security.c
中的 security_inode_init_security_anon()
函数给 KVM 用。
[RFC PATCH v12 14/33] KVM: Add KVM_CREATE_GUEST_MEMFD ioctl() for guest-specific backing memory
这是一个大 patch,有将近一千行的改动,应该是这个 patch set 最主要的部分。
KVM CAP: KVM_CAP_GUEST_MEMFD
: 表示 KVM 支不支持 guest memfd。
KConfig: CONFIG_KVM_PRIVATE_MEM
,打开这个 config 会自动打开 CONFIG_XARRAY_MULTI
,表示我们要用到 XARRAY 里 multi indices 这个 feature。
IOCTL: KVM_CREATE_GUEST_MEMFD
添加了一个新文件:virt/kvm/guest_mem.c
需要说明的是,
- 普通的 memfd^(不是 gmem)是直接通过
memfd_create
^ 这个 syscall 来 create 的, - 到了 gmem 这里,需要通过 Userspace 调用 ioctl 来 create。QEMU 里 gmem 以及 memfd 的 backend 的实现可以印证这一点。也就是说,gmem_fd 的创建由 Userspace 来负责。
kvm_vm_ioctl
case KVM_CREATE_GUEST_MEMFD: {
struct kvm_create_guest_memfd guest_memfd;
//...
r = kvm_gmem_create(kvm, &guest_memfd);
__kvm_gmem_create
kvm_gmem_mnt
/ kvm_gmem_fs
/ kvm_gmem_init()
/ kvm_gmem_exit()
KVM
gmem 也被设计成了一个新的 file system,名字叫做 kvm_guest_memory
。
static struct vfsmount *kvm_gmem_mnt;
// 定义了一个新的文件系统,名字叫 kvm_guest_memory
// 之前 UPM 用的是 tmpfs 作为 backend,这次我们自己创建一个 fs
static struct file_system_type kvm_gmem_fs = {
.name = "kvm_guest_memory",
// 相当于就 specify 了初始化和回收的函数
.init_fs_context = kvm_gmem_init_fs_context,
.kill_sb = kill_anon_super,
};
// This function is called in kvm_init()
int kvm_gmem_init(void)
{
kvm_gmem_mnt = kern_mount(&kvm_gmem_fs);
//...
/* For giggles. Userspace can never map this anyways. */
kvm_gmem_mnt->mnt_flags |= MNT_NOEXEC;
}
// This function is called in kvm_init()
void kvm_gmem_exit(void)
{
kern_unmount(kvm_gmem_mnt);
kvm_gmem_mnt = NULL;
}
kvm_gmem_iops
/ kvm_gmem_getattr()
/ kvm_gmem_setattr
KVM
// 这两个函数都还处于未实现的状态,因为目前我们还调用不到 mkdir,所以现在还暂时不用实现
static const struct inode_operations kvm_gmem_iops = {
.getattr = kvm_gmem_getattr,
.setattr = kvm_gmem_setattr,
};
static int kvm_gmem_getattr(struct mnt_idmap *idmap, const struct path *path,
struct kstat *stat, u32 request_mask,
unsigned int query_flags)
{
struct inode *inode = path->dentry->d_inode;
/* TODO */
generic_fillattr(idmap, request_mask, inode, stat);
return 0;
}
static int kvm_gmem_setattr(struct mnt_idmap *idmap, struct dentry *dentry,
struct iattr *attr)
{
/* TODO */
return -EINVAL;
}
struct kvm_gmem_fops
/ kvm_gmem_fallocate()
/ kvm_gmem_punch_hole()
/ kvm_gmem_allocate()
/ kvm_gmem_release()
KVM
因为其是一个 file_operations
类型,所以说明我们需要 gmem 创建出来的 fd 支持一些系统调用,最主要的是 fallocate()
这个系统调用,可以用来操作里面的数据。
kvm_gmem_allocate()
没看懂,为什么要 folio_put()
?
static const struct file_operations kvm_gmem_fops = {
.open = generic_file_open,
.release = kvm_gmem_release,
.fallocate = kvm_gmem_fallocate,
};
static long kvm_gmem_fallocate(struct file *file, int mode, loff_t offset, loff_t len)
{
int ret;
// gmem 的 fd 必须要 keep size,也就是说我们是不支持改变大小的
if (!(mode & FALLOC_FL_KEEP_SIZE))
return -EOPNOTSUPP;
// 除了这两个 flags,其他的 flag 我们暂时都是不支持的
if (mode & ~(FALLOC_FL_KEEP_SIZE | FALLOC_FL_PUNCH_HOLE))
return -EOPNOTSUPP;
// 检查一下有没有对齐
if (!PAGE_ALIGNED(offset) || !PAGE_ALIGNED(len))
return -EINVAL;
// 比如当 post-copy 进行到 discard^ 阶段,我们需要对 page 置空的时候
// 会使用到这个 flag
if (mode & FALLOC_FL_PUNCH_HOLE)
ret = kvm_gmem_punch_hole(file_inode(file), offset, len);
// 当不是这个 flag 时,表示的是我们需要 allocate?
// 好像目前 gmem KVM 和 QEMU 实现里暂时还调用不到这里
else
ret = kvm_gmem_allocate(file_inode(file), offset, len);
if (!ret)
file_modified(file);
return ret;
}
// 可以看出来和 kvm_gmem_release 还是挺像的,只不过是 range 不一样
static long kvm_gmem_punch_hole(struct inode *inode, loff_t offset, loff_t len)
{
// gmem_list 是一个 kvm_gmem 的列表,表示了所有的 gmem_fd
struct list_head *gmem_list = &inode->i_mapping->private_list;
pgoff_t start = offset >> PAGE_SHIFT;
pgoff_t end = (offset + len) >> PAGE_SHIFT;
struct kvm_gmem *gmem;
list_for_each_entry(gmem, gmem_list, entry) {
//...
// 对于 每一个 gmem_fd,invalidate 这段 range
// 有了 bindings,给定一个 GPA 范围 [start, end),我们可以找到这个范围所映射到的 kvm_memory_slot
// 所以这里的 start, end 都是 GPA,在 begin/end 函数里面,会对比找到 start, end 所对应的一个或者几个 gmem_fd
// 然后执行对应的函数。
// 对于 TDX,最终会调用到 TDH.MEM.PAGE.REMOVE
kvm_gmem_invalidate_begin(gmem, start, end);
/*
* Cache KVM for the following kvm_gmem_issue_arch_invalidate().
* Opptunistically, verify that all gmem in the list are bound
* to the same guest.
*/
if (!kvm)
kvm = gmem->kvm;
else
WARN_ON_ONCE(kvm != gmem->kvm);
}
// 对于 TDX,就是执行 TDH.PHYMEM.PAGE.WBINVD
kvm_gmem_issue_arch_invalidate(kvm, inode, start, end);
// 应该会减每一个 page 的 refcount,将 page 从 page cache 里移除
// 尽管 [offset, offset + len - 1] 可以仅仅是一个 2M huge folio
// 的 512 个 page 的其中一个,但是这整个 folio 仍然会被从 page cache
// 中移除,需要注意。
truncate_inode_pages_range(inode->i_mapping, offset, offset + len - 1);
//...
list_for_each_entry(gmem, gmem_list, entry)
kvm_gmem_invalidate_end(gmem, start, end);
//...
}
static long kvm_gmem_allocate(struct inode *inode, loff_t offset, loff_t len)
{
//...
// offset 表示从这个 gmem_fd 里的第几个 page 开始 allocate
// len 表示要 allocate 几个
// offset + len 不能超过 inode 创建的大小,也就是不可以时 resizable 的
if (offset + len > i_size_read(inode))
return -EINVAL;
//...
start = offset >> PAGE_SHIFT;
end = (offset + len) >> PAGE_SHIFT;
//...
for (index = start; index < end; ) {
struct folio *folio;
// signal 可以打断相关的代码
// ...
// 找到 page cache 里对应的页的描述结构 folio
folio = kvm_gmem_get_folio(inode, index);
//...
// Get the index of the next folio.
// 因为我们在循环里,所以继续找下一个 folio
// 为什么不直接 index++? 因为要考虑到大页的情况
index = folio_next_index(folio);
//...
// folio refcount 减一,为 0 时内存被收回。
folio_put(folio);
//...
// 允许调度
cond_resched();
}
//...
}
// 在 Usersapce 调用 close(gmem_fd) 触发
// scope 仅仅是一个 gmem_fd,不是这个 VM 所有的
static int kvm_gmem_release(struct inode *inode, struct file *file)
{
struct kvm_gmem *gmem = file->private_data;
struct kvm_memory_slot *slot;
struct kvm *kvm = gmem->kvm;
unsigned long index;
//...
// 既然都是 release 了,那 binding 里的所有内容都要清空了
xa_for_each(&gmem->bindings, index, slot)
slot->gmem.file = NULL;
//...
//...
// 执行标准的 invalidate 流程
// 对于 TDX,这个会执行 unmap,也就是 TDH.MEM.PAGE.REMOVE
kvm_gmem_invalidate_begin(gmem, 0, -1ul);
// 这个会执行 cache invalidation,也就是 TDH.PHYMEM.PAGE.WBINVD
kvm_gmem_issue_arch_invalidate(gmem->kvm, file_inode(file), 0, -1ul);
kvm_gmem_invalidate_end(gmem, 0, -1ul);
// 在全局链表里删除这个 kvm_gmem
list_del(&gmem->entry);
//...
// usercount - 1,变为 0 的话 destroy VM
// 当所有 gmem_fd 都被 close 后,会变为 0
kvm_put_kvm(kvm);
//...
}
kvm_gmem_get_file()
KVM
一个 helper 函数,找到 kvm_memory_slot
对应的 file。
kvm_memory_slot
和 gmem_fd
是多对一的关系。gmem_fd
和 file
是一对一的,所以这两个也是多对一的。
这个函数调用了 get_file_rcu()
,所以会对文件的 f_count
加 1。
static struct file *kvm_gmem_get_file(struct kvm_memory_slot *slot)
{
struct file *file;
//...
file = rcu_dereference(slot->gmem.file);
if (file && !get_file_rcu(file))
file = NULL;
//...
return file;
}
kvm_gmem_error_page()
/ KVM
当 Clean page cache page 的时候会调用到这里。进而调用 kvm_gmem_invalidate_begin()
进行真正的 page cleaning。
me_pagecache_clean
truncate_error_page
.error_remove_page = kvm_gmem_error_page
static int kvm_gmem_error_page(struct address_space *mapping, struct page *page)
{
//...
start = page->index;
end = start + thp_nr_pages(page);
list_for_each_entry(gmem, gmem_list, entry)
kvm_gmem_invalidate_begin(gmem, start, end);
// 有一堆注释还没来得及看
list_for_each_entry(gmem, gmem_list, entry)
kvm_gmem_invalidate_end(gmem, start, end);
//...
}
kvm_gmem_create()
/ __kvm_gmem_create()
KVM
和 UPM 不同,backend 不再是使用 tmpfs 了。
kvm_vm_ioctl
case KVM_CREATE_GUEST_MEMFD: {
kvm_gmem_create
int kvm_gmem_create(struct kvm *kvm, struct kvm_create_guest_memfd *args)
{
// some sanity checks
// 这里的 kvm_gmem_mnt 表示了我们要以 gmem fs 作为 backend,而不是 tmpfs
return __kvm_gmem_create(kvm, size, flags, kvm_gmem_mnt);
}
static int __kvm_gmem_create(struct kvm *kvm, loff_t size, u64 flags, struct vfsmount *mnt)
{
const char *anon_name = "[kvm-gmem]";
const struct qstr qname = QSTR_INIT(anon_name, strlen(anon_name));
struct kvm_gmem *gmem;
struct inode *inode;
struct file *file;
int fd, err;
// 从 super_block 中分配一个 匿名的 inode 出来。
inode = alloc_anon_inode(mnt->mnt_sb);
//...
// 初始化这个 inode
err = security_inode_init_security_anon(inode, &qname, NULL);
//...
// 设置一些 inode 的一些域
// i_private 是 void* 类型的,可以放任何想放的数据
inode->i_private = (void *)(unsigned long)flags;
inode->i_op = &kvm_gmem_iops;
inode->i_mapping->a_ops = &kvm_gmem_aops;
inode->i_mode |= S_IFREG;
inode->i_size = size;
mapping_set_gfp_mask(inode->i_mapping, GFP_HIGHUSER);
mapping_set_large_folios(inode->i_mapping);
mapping_set_unmovable(inode->i_mapping);
//...
// 从没有用的 fd 中分配一个 fd number
fd = get_unused_fd_flags(0);
//...
file = alloc_file_pseudo(inode, mnt, "kvm-gmem", O_RDWR, &kvm_gmem_fops);
//...
file->f_flags |= O_LARGEFILE;
file->f_mapping = inode->i_mapping;
// 给 struct kvm_gmem 分配空间
gmem = kzalloc(sizeof(*gmem), GFP_KERNEL);
//...
// 给 usercount + 1,如果变成了 0,kvm 会被 destroy
// usercount 会在每次 close 一个 gmem_fd 的时候减一
// 所以我们在 create 的时候要 + 1。
kvm_get_kvm(kvm);
gmem->kvm = kvm;
xa_init(&gmem->bindings);
file->private_data = gmem;
list_add(&gmem->entry, &inode->i_mapping->private_list);
fd_install(fd, file);
// 返回我们创建的 fd
return fd;
// error handling...
}
kvm_gmem_invalidate_begin()
KVM
我们要 invalidate gmem 内存区域内的一个区间,这个区间里有很多的 pages,需要逐个进行 unmap。有三种情况可能会调用到 kvm_gmem_invalidate_begin()
:
-
kvm_gmem_error_page()
:当 gmem 这个 file system 对应的 page cache 里的某一个 page 要被 clean 时,会调用到这里。 -
kvm_gmem_release()
:当要完全释放掉一个 file 时会调用。对应的是 Userspace 执行close(gmem_fd)
时。 -
kvm_gmem_punch_hole()
:对应的是 Userspace 执行fallocate(fd, FALLOC_FL_PUNCH_HOLE | FALLOC_FL_KEEP_SIZE
。
static void kvm_gmem_invalidate_begin(struct kvm_gmem *gmem, pgoff_t start, pgoff_t end)
{
struct kvm_memory_slot *slot;
struct kvm *kvm = gmem->kvm;
unsigned long index;
bool flush = false;
//...
kvm_mmu_invalidate_begin(kvm);
// 对于这个区间内的每一个 gpa,找到对应的 kvm_memory_slot(内存条)
// 然后 unmap 掉这个 gpa(kvm_mmu_unmap_gfn_range)。
xa_for_each_range(&gmem->bindings, index, slot, start, end - 1) {
pgoff_t pgoff = slot->gmem.pgoff;
struct kvm_gfn_range gfn_range = {
.start = slot->base_gfn + max(pgoff, start) - pgoff,
.end = slot->base_gfn + min(pgoff + slot->npages, end) - pgoff,
.slot = slot,
.may_block = true,
};
flush |= kvm_mmu_unmap_gfn_range(kvm, &gfn_range);
}
if (flush)
kvm_flush_remote_tlbs(kvm);
//...
}
kvm_gmem_invalidate_end()
/ KVM
没啥,就是调了下 kvm_mmu_invalidate_end()
。
static void kvm_gmem_invalidate_end(struct kvm_gmem *gmem, pgoff_t start, pgoff_t end)
{
//...
// gmem->bindings^
if (xa_find(&gmem->bindings, &start, end - 1, XA_PRESENT))
kvm_mmu_invalidate_end(kvm);
//...
}
KVM_MEM_LOG_DIRTY_PAGES
/ KVM_MEM_READONLY
/ KVM_MEM_PRIVATE
KVM
这三个 flags 是让 userspace 看到的,它们可以在 set memory region 的时候选择以下的 flag:
// KVM_MEM_LOG_DIRTY_PAGES 表示要让 KVM 来 track 对于这个 slot memory 的 write
#define KVM_MEM_LOG_DIRTY_PAGES (1UL << 0)
// KVM_MEM_READONLY 表示这个 slot 是只读的
#define KVM_MEM_READONLY (1UL << 1)
// 比如 KVM_MEM_PRIVATE 表示这可能是 TDX
#define KVM_MEM_PRIVATE (1UL << 2)
struct gmem
KVM
每一个 kvm_memory_slot
有一个此结构体,因为多个 slot 可以共享同一个 gmem_fd 的不同区间,所以 pgoff
表示的是这个 slot 在 gmem_fd 里的起始地址。
这里面的 struct file *file
表示的是 fd 对应的 file,里面的 private_data
表示的是一个 struct kvm_gmem
。
struct {
struct file __rcu *file;
pgoff_t pgoff;
} gmem;
kvm_slot_can_be_private()
KVM
static inline bool kvm_slot_can_be_private(const struct kvm_memory_slot *slot)
{
return slot && (slot->flags & KVM_MEM_PRIVATE);
}
kvm_mem_is_private()
KVM
通过查看 KVM 里的 XArray 来检查一个 gfn 是不是 private 的:
static inline bool kvm_mem_is_private(struct kvm *kvm, gfn_t gfn)
{
return IS_ENABLED(CONFIG_KVM_PRIVATE_MEM) && kvm_get_memory_attributes(kvm, gfn) & KVM_MEMORY_ATTRIBUTE_PRIVATE;
}
struct kvm_create_guest_memfd
KVM
调用 KVM_CREATE_GUEST_MEMFD
这个 ioctl 的时候 Userspace 需要传进来的参数。
和 UPM 不一样了,UPM 是通过 syscall 来创建 fd,而 gmem 是通过 ioctl。
struct kvm_create_guest_memfd {
// 这段 memfd 所代表的大小
__u64 size;
// 要置上哪些 flag?
__u64 flags;
__u64 reserved[6];
};
struct kvm_gmem
KVM
QEMU 里的一个 gmem_fd
被一个 RAMBlock
所保有,这说明并不是整个 VM 只有一个 gmem fd,而是每一个 RAMBlock
都有一个,kvm_gmem
结构体对应的就是一个 gmem_fd
,在每次 QEMU 调用 KVM_CREATE_GUEST_MEMFD
ioctl 的时候被初始化出来。
此结构体和一个 gmem_fd
是对应的。
struct kvm_gmem {
struct kvm *kvm;
// 唯一 set 的地方:
// kvm_vm_ioctl
// kvm_vm_ioctl_set_memory_region
// kvm_set_memory_region
// __kvm_set_memory_region
// kvm_gmem_bind
// 有了 bindings,给定一个 GPA 范围 [start, end),我们可以找到这个范围所映射到的 kvm_memory_slot
// 这个 bindings 会在很多时候使用,比如通过一个 gfn 找到对应的 slot
struct xarray bindings;
// 没有用,纯纯为了能让 kvm_gmem 们连起来而加的属性
struct list_head entry;
};
kvm_gmem_bind()
/ kvm_gmem_unbind()
KVM
对代码有所简化一些不必要的部分。
kvm_memory_slot
和 gmem_fd
是多对一的关系,不同的 kvm_memory_slot
可以共用 gmem_fd
的不同范围。
把传进来的 kvm_memory_slot
所对应的内存空间绑定到 gmem_fd
中以 offset
作为起始点的区域,完成 QEMU 的请求。
kvm_vm_ioctl
kvm_vm_ioctl_set_memory_region
kvm_set_memory_region
__kvm_set_memory_region
if (mem->flags & KVM_MEM_PRIVATE)
r = kvm_gmem_bind(kvm, new, mem->gmem_fd, mem->gmem_offset);
// fd: gmem_fd
int kvm_gmem_bind(struct kvm *kvm, struct kvm_memory_slot *slot, unsigned int fd, loff_t offset)
{
loff_t size = slot->npages << PAGE_SHIFT;
//...
// 得到 gmem_fd 背后的 struct *file
file = fget(fd);
//...
// 这个 struct file 的 private_data 字段是一个 kvm_gmem 类型
gmem = file->private_data;
//...
inode = file_inode(file);
// some checks...
//...
// 计算出要绑定的 range: [start, end)
start = offset >> PAGE_SHIFT;
end = start + slot->npages;
//...
// 确保 [start, end) 区间是空的,也就是并没有 set 值,可以被我们使用,不然就报错
if(xa_find(&gmem->bindings, &start, end - 1, XA_PRESENT))
goto err;
//...
// slot->gmem 并不是 kvm_gmem 类型的
// 它是一个 struct,详见 struct gmem^
slot->gmem.file = file;
slot->gmem.pgoff = start;
// 把 [start, end) 这个 range 作为 key,将其 value 置为 slot
// After this, any loads from "start" to "end" will return "slot".
xa_store_range(&gmem->bindings, start, end - 1, slot, GFP_KERNEL);
//...
/*
* Drop the reference to the file, even on success. The file pins KVM,
* not the other way 'round. Active bindings are invalidated if the
* file is closed before memslots are destroyed.
*/
fput(file);
//...
}
kvm_destroy_vm
kvm_free_memslots
kvm_free_memslot
kvm_gmem_unbind
kvm_commit_memory_region
case KVM_MR_DELETE:
kvm_free_memslot
kvm_gmem_unbind
void kvm_gmem_unbind(struct kvm_memory_slot *slot)
{
unsigned long start = slot->gmem.pgoff;
unsigned long end = start + slot->npages;
//...
file = kvm_gmem_get_file(slot);
//...
// 重设 bindings 对应的区间为 NULL
xa_store_range(&gmem->bindings, start, end - 1, NULL, GFP_KERNEL);
slot->gmem.file = NULL;
//...
// 因为前面 get 了 file,所以要 put
fput(file);
}
kvm_gmem_get_folio()
KVM
struct folio
可以粗略理解为一个 struct page
,也就是对于一个物理页的描述。
这个函数的作用是给定一个文件和偏移,返回偏移所在位置的对应 page cache 中页的描述结构(struct folio
)。
// path 1
// 第一次分配内存
kvm_gmem_allocate
kvm_gmem_get_folio
// path 2
// 后续的使用
kvm_gmem_get_pfn
kvm_gmem_get_folio
static struct folio *kvm_gmem_get_folio(struct file *file, pgoff_t index)
{
struct folio *folio;
// huge page related...
// 看下一个 patch
// 拿一个 huge folio
folio = kvm_gmem_get_huge_folio(inode, index);
// filemap_grab_folio^
// 拿到对应位置的页的描述结构(struct folio)
// f_mapping 是一个 AddressSpace 类型,表示的应该是 page cache
if (!folio)
// grab a folio from page cache
folio = filemap_grab_folio(file->f_mapping, index);
//...
/*
* Use the up-to-date flag to track whether or not the memory has been
* zeroed before being handed off to the guest. There is no backing
* storage for the memory, so the folio will remain up-to-date until
* it's removed.
*
* TODO: Skip clearing pages when trusted firmware will do it when
* assigning memory to the guest.
*/
// 从上面的注释可以得出,只有在第一次拿到这个 page 时候才有可能是 not up-to-date 的
// 给了 guest 之后,因为这变成了一个 gmem 的 page,而 gmem 没有 backend,所以其必不可能
// 变回 not up-to-date 的。(up-to-date 应该表示 backend(disk) 和 page cache 里的内容是一致的,
// 也就是 backend 的更新及时传到了 page cache 里面)所以这里表示第一次拿这个 page
if (!folio_test_uptodate(folio)) {
//...
for (i = 0; i < nr_pages; i++)
// 第一次拿到 page,清空 page 内容,交给 guest 用。
clear_highpage(folio_page(folio, i));
folio_mark_uptodate(folio);
}
//...
// 不像其他的 file,因为我们的 memory 并没有 backend,所以 accessed, referenced, and dirty flags
// 是可以忽略的。并且 memory 是 unevictable 的。
return folio;
}
kvm_gmem_get_pfn()
KVM
这也是整个 patch 重要的部分,其他的 函数大多数是偏静态的设置值,比如 ioctl Create MEMFD 或者 SET_MEMORY_REGION 的时候,那么当 guest run 的时候,如何动态地给内存数据呢?
内存 page 往 TDX Module 里的添加并不由 gmem 负责,gmem 只是在发生 page fault 的时候,或者需要拿到 pfn 的时候(比如 import page),从 kernel 里拿一个新鲜的 page 或者一个已有的 page 的 pfn,提供给 kvm page fault handler,分配内存由 vendor 的实现比如 TDX patchset 来负责,而 gmem 是 generic 的。
kvm_faultin_pfn
__kvm_faultin_pfn
kvm_faultin_pfn_private
kvm_gmem_get_pfn
tdx_mig_stream_ioctl
case KVM_TDX_MIG_IMPORT_MEM:
tdx_mig_stream_import_mem
kvm_gmem_get_pfn
int kvm_gmem_get_pfn(struct kvm *kvm, struct kvm_memory_slot *slot,
gfn_t gfn, kvm_pfn_t *pfn, int *max_order)
{
// 先算出来这个 page 是 slot 里的第几个 page,然后加上这个 slot 在 gmem_fd 里的偏移
// 即可得出 index,可以看出,index 和 gfn 的映射大致是线性的。
pgoff_t index = gfn - slot->base_gfn + slot->gmem.pgoff;
struct kvm_gmem *gmem;
struct folio *folio;
struct page *page;
struct file *file;
int r;
// 使用 helper 函数得到 file
// 会让 file 的 f_count 加 1。
file = kvm_gmem_get_file(slot);
//...
gmem = file->private_data;
//...
// 根据 index 拿到 gmem file system 的 page cache 对应的 page 的描述结构 folio
folio = kvm_gmem_get_folio(file_inode(file), index);
//...
// 拿到对应的 struct page
page = folio_file_page(folio, index);
// 得到这个 page 真实的 PFN,这样子才能填到
// fault->pfn 里面,让 KVM 的 pf handler 做下一步的处理
// 比如调用 TDX 来分配。gmem 是 generic 的,并不 specific to TDX。
*pfn = page_to_pfn(page);
//...
}
[RFC PATCH v12 15/33] KVM: Add transparent hugepage support for dedicated guest memory
就像名字那样,Add transparent hugepage support.
添加了一个新的 flag:KVM_GUEST_MEMFD_ALLOW_HUGEPAGE
。这个 flag 在 Userspace 调用 KVM_CREATE_GUEST_MEMFD
这个 ioctl 的时候可以指定。
其他的大更改都融入到了上一个 patch 所引入的 kvm_gmem_get_folio()
中。
kvm_gmem_get_huge_folio()
KVM
static struct folio *kvm_gmem_get_huge_folio(struct inode *inode, pgoff_t index)
{
// huge_index 是 index 根据大页大小的向下取整
unsigned long huge_index = round_down(index, HPAGE_PMD_NR);
unsigned long flags = (unsigned long)inode->i_private;
struct address_space *mapping = inode->i_mapping;
gfp_t gfp = mapping_gfp_mask(mapping);
struct folio *folio;
// sanity checks...
// 如果这个区间 page cache 里已经有这个 page 了,就不需要 alloc page 了
// 只需要直接返回 NULL,然后在后续 call filemap_grab_folio() 就好了。
if (filemap_range_has_page(mapping, huge_index << PAGE_SHIFT,
(huge_index + HPAGE_PMD_NR - 1) << PAGE_SHIFT))
return NULL;
// 先分配一个 folio 出来
folio = filemap_alloc_folio(gfp, HPAGE_PMD_ORDER);
// sanity checks...
// 把这个 folio 放到 mapping 也就是 page cache 里面
filemap_add_folio(mapping, folio, huge_index, gfp)
// error checks...
return folio;
//...
}
[RFC PATCH v12 16/33] KVM: x86: "Reset" vcpu->run->exit_reason early in KVM_RUN
Because KVM already returns -EFAULT
in many paths, there's a relatively high probability that KVM could return -EFAULT
without setting run->exit_reason
, in which case reporting KVM_EXIT_UNKNOWN
is much better than reporting
whatever exit reason happened to be in the run structure.
[RFC PATCH v12 17/33] KVM: x86: Disallow hugepages when memory attributes are mixed
Private-shared explicit conversion / implicit conversion
Explicit conversion: happens when guest explicitly calls into KVM to map a range (as private or shared), KVM then exits to userspace to perform the map/unmap operations. (比如 TDCALL Map GPA).
Implicit conversion: happens in KVM page fault handler where KVM exits to userspace for an implicit conversion when the page is in a different state than requested (private or shared).
Explicit conversion 导致的 exit 的 reason 是 KVM_EXIT_HYPERCALL (KVM_HC_MAP_GPA_RANGE)
。
Implicit conversion 导致的 exit 的 reason 是 KVM_EXIT_MEMORY_FAULT
。(不用 KVM_EXIT_HYPERCALL
的原因是这本来就不是一个 hypercall)。
两种方式都会调用到 kvm_convert_memory()
。
KVM gmem Fixup Patces
[PATCH 00/13] KVM: guest_memfd fixes - Sean Christopherson
UPM
这个应该是 guest_memfd 的前身?
Unmapping guest Private Memory
AMD SEV-SNP also has use case.
目前还没有 Upstream。
memfd_restricted()
itself is implemented as a shim layer on top of real memory file systems (currently tmpfs). Pages in restrictedmem
are marked as unmovable and unevictable. But in future this might be changed.
KVM uses the new restrictedmem_get_page()
to obtain the physical memory page.
QEMU 在 set kvm memory slot 的时候所有 restricted_fd 是一样的,应该说有两个 restricted fd,一个是 pc rom,一个是 RAM。
Motivation
If a TDX host accesses private memory, MCE can happen which can further crash the host, this is terrible for multi-tenant configurations. The host accesses include those from KVM userspace like QEMU. Introduce new mm and KVM interfaces so QEMU can still manage guest memory via a fd-based approach, but it can never access the guest memory content (但是 QEMU 的 RAMBlock 里记录着 HVA,如果 QEMU 访问了会发生什么).
好好理解一下这句话,这是要解决的核心问题:
Normally, KVM populates EPT by using a HVA from core mm page table (e.g. x86 userspace page table). This requires guest memory being mmaped into KVM userspace, but this is also the source where the mentioned crash issue can happen. **Guest
memory doesn't have to be mmaped into KVM userspace.**
KVM 和 QEMU 直接访问 private memory 都会造成 MCE,UPM 只是限制了 QEMU(Userspace)访问 private 内存,而不是 KVM。
KVM: [PATCH v10 0/9] KVM: mm: fd-based approach for supporting KVM - Chao Peng
QEMU: UPM 的 patch 还没有往社区发过,不过 gmem 的发过了。
Why not using memfd
directly rather than memfd_restrcited
?
Normally KVM uses memfd memory via mmapping the memfd into KVM userspace (e.g. QEMU) and then using the mmaped virtual address to setup the mapping in the KVM secondary page table (e.g. EPT).
- 用
memfd
,别有用心的 userspace 程序可以用 offset 访问这个 memfd 的地址,从而造成 host crash。 - 用
memfd_restricted
,KVM can directly interact with core-mm without the need to expose the memoy content into KVM userspace. 所以 QEMU 是禁止访问这个 fd 的 offset 地址的。
这是禁止 Userspace 访问的实现方式:By default memfd_restricted()
prevents userspace read
, write
and mmap
. By defining new bit in the 'flags', it can be extended to support other restricted semantics in the future.
kvm_encrypt_reg_region()
QEMU
调用 KVM_SET_MEMORY_ATTRIBUTES
to encrypt size
memory from start
.
这个函数好像也可以 private to shared。
int kvm_encrypt_reg_region(hwaddr start, hwaddr size, bool reg_region)
{
struct kvm_memory_attributes attr;
// 看来 bool 表示这个是不是 private 的?
attr.attributes = reg_region ? KVM_MEMORY_ATTRIBUTE_PRIVATE : 0;
attr.address = start;
attr.size = size;
attr.flags = 0;
r = kvm_vm_ioctl(kvm_state, KVM_SET_MEMORY_ATTRIBUTES, &attr);
//...
}
kvm_convert_memory()
QEMU
kvm_encrypt_reg_region()
只是从 shared 到 private,但是 kvm_convert_memory
是双向的。并且后者调用了前者。
这个函数在很多地方会调用到,比如:
- migration destination 端要 load 时。
- guest 想 map 一个 GPA,调用 TDCALL TDG_VP_VMCALL_MAP_GPA 到 KVM,KVM 又到 QEMU 来调用
kvm_convert_memory
来处理。 - 发生 page fault VM Exit 出来
KVM_EXIT_MEMORY_FAULT
,然后由kvm_convert_memory
来处理。
tdx_handle_map_gpa
kvm_convert_memory
ram_load_update_cgs_bmap
kvm_convert_memory
case KVM_EXIT_MEMORY_FAULT:
kvm_convert_memory
// 传进来的时 GPA
int kvm_convert_memory(hwaddr start, hwaddr size, bool shared_to_private, int cpu_index)
{
//...
// 找到 GPA 所对应的 MemoryRegionSection
section = memory_region_find(get_system_memory(), start, size);
// ...
addr = memory_region_get_ram_ptr(section.mr) + section.offset_within_region;
rb = qemu_ram_block_from_host(addr, false, &offset);
// postcopy 中,destination 发生了 page fault
// 这个 page fault 发生在应该为 private 的 page
if (is_postcopy_private_fault(rb, offset, shared_to_private)) {
//...
// 要把发生 page fault 的地址发送回 src
postcopy_add_private_fault_to_pending_list(rb, offset, start, cpu_index);
// 调用 SET_ATTRIBUTE 这个 ioctl 来转换一下
kvm_encrypt_reg_region(start, size, true);
// 非 migration 的情况,
} else {
// 调用 SET_ATTRIBUTE 这个 ioctl 来转换一下
kvm_encrypt_reg_region(start, size, shared_to_private);
//...
// 调用 VFIO 的 callback,暂时先不考虑
// ...
// discard 掉转换前的 page
ram_block_convert_range(rb, offset, size, shared_to_private);
}
// error checking...
memory_region_unref(section.mr);
}
kvm_convert_memory
ram_block_convert_range
// 这个函数可以是双向的转换
int ram_block_convert_range(RAMBlock *rb, uint64_t start, size_t length, bool shared_to_private)
{
// checking...
// 如果是要转为 private,那么需要丢掉 normal fd 对应的 page 内容,因为已经无用了。
if (shared_to_private) {
ret = ram_block_discard_range(rb, start, length);
} else {
ret = ram_block_discard_range_fd(rb, start, length, rb->restricted_fd);
}
//...
uint64_t bit_start = start >> TARGET_PAGE_BITS;
uint64_t bit_length = length >> TARGET_PAGE_BITS;
// 更新 cgs_bmap 的状态
if (shared_to_private) {
bitmap_set(rb->cgs_bmap, bit_start, bit_length);
} else {
bitmap_clear(rb->cgs_bmap, bit_start, bit_length);
}
}
UPM Kernel/KVM Patches
Touches both core mm and KVM code.
A single KVM memslot
can maintain both private memory through private fd (restricted_fd
/restricted_offset
) and shared (unencrypted) memory through userspace mmaped HVA (userspace_addr
).
Introduces new KVM_EXIT_MEMORY_FAULT
exit to allow userspace to get the chance on decision-making for shared / private memory conversion.
Introduces new KVM ioctl KVM_SET_MEMORY_ATTRIBUTES
to maintain whether a page is private or shared. This ioctl allows userspace to convert a page between private and shared.
要理解这个 patchset,首先要明白有几方:
- KVM
- KVM Userspace (QEMU)
- Core MM
首先 QEMU 通过和 MM 交互获得 fd,再将 fd 传给 KVM,这样 KVM 就也可以使用这个 fd 和 Core MM 来交互了。
static const struct file_operations restrictedmem_fops = {
.release = restrictedmem_release,
.fallocate = restrictedmem_fallocate,
};
restrictedmem_release()
KVM
Userspace close(restricted_fd)
的时候会触发到这里:
static int restrictedmem_release(struct inode *inode, struct file *file)
{
struct restrictedmem_data *data = inode->i_mapping->private_data;
// 这个 fd 本质背后还是 tmpfs 的 fd
// 那么 fput 应该会释放掉 tmpfs 的所有 memory 吧。
fput(data->memfd);
kfree(data);
return 0;
}
memfd_restricted
Syscall
SYSCALL_DEFINE2(memfd_restricted, unsigned int, flags, int, mount_fd)
restrictedmem_create
restrictedmem_file_create
restrictedmem_create()
/ restrictedmem_file_create()
/ Kernel
static int restrictedmem_create(struct vfsmount *mount)
{
struct file *file, *restricted_file;
int fd, err;
fd = get_unused_fd_flags(0);
//...
// 可以看到内存是从 tmpfs 来申请的
if (mount)
file = shmem_file_setup_with_mnt(mount, "memfd:restrictedmem", 0, VM_NORESERVE);
else
file = shmem_file_setup("memfd:restrictedmem", 0, VM_NORESERVE);
//...
file->f_mode |= FMODE_LSEEK | FMODE_PREAD | FMODE_PWRITE;
file->f_flags |= O_LARGEFILE;
restricted_file = restrictedmem_file_create(file);
//...
fd_install(fd, restricted_file);
return fd;
//...
}
// 根据一个 tmpfs 的 struct file
// 创建一个 restrictedmem 的 struct file
static struct file *restrictedmem_file_create(struct file *memfd)
{
struct restrictedmem_data *data;
struct address_space *mapping;
struct inode *inode;
struct file *file;
data = kzalloc(sizeof(*data), GFP_KERNEL);
//...
// memfd 就是我们从 tmpfs 拿到的 fd
data->memfd = memfd;
//...
inode = alloc_anon_inode(restrictedmem_mnt->mnt_sb);
//...
inode->i_mode |= S_IFREG;
// operation 设置为 memfd 独有的而不是 tmpfs 的
inode->i_op = &restrictedmem_iops;
inode->i_mapping->private_data = data;
file = alloc_file_pseudo(inode, restrictedmem_mnt, "restrictedmem", O_RDWR, &restrictedmem_fops);
//...
file->f_flags |= O_LARGEFILE;
/*
* These pages are currently unmovable so don't place them into movable
* pageblocks (e.g. CMA and ZONE_MOVABLE).
*/
mapping = memfd->f_mapping;
mapping_set_unevictable(mapping);
mapping_set_gfp_mask(mapping, mapping_gfp_mask(mapping) & ~__GFP_MOVABLE);
return file;
}
restrictedmem_fallocate()
/ restrictedmem_punch_hole()
KVM
如果是要 PUNCH HOLE 那么调用自己的函数,其他情况 fallback 到 tmpfs 的 fallocate()
的函数。
static long restrictedmem_fallocate(struct file *file, int mode, loff_t offset, loff_t len)
{
struct restrictedmem_data *data = file->f_mapping->private_data;
struct file *memfd = data->memfd;
if (mode & FALLOC_FL_PUNCH_HOLE)
return restrictedmem_punch_hole(data, mode, offset, len);
// 这里用的是 tmpfs 的 fallocate 函数
return memfd->f_op->fallocate(memfd, mode, offset, len);
}
static long restrictedmem_punch_hole(struct restrictedmem_data *data, int mode, loff_t offset, loff_t len)
{
int ret;
pgoff_t start, end;
struct file *memfd = data->memfd;
// align check
start = offset >> PAGE_SHIFT;
end = (offset + len) >> PAGE_SHIFT;
restrictedmem_invalidate_start(data, start, end);
// 调用 tmpfs 独有的 fallocate 函数
ret = memfd->f_op->fallocate(memfd, mode, offset, len);
restrictedmem_invalidate_end(data, start, end);
return ret;
}
restrictedmem_invalidate_start()
/ kvm_restrictedmem_invalidate_begin()
KVM
static void kvm_restrictedmem_invalidate_begin(struct restrictedmem_notifier *notifier,
pgoff_t start, pgoff_t end)
{
struct kvm_memory_slot *slot = container_of(notifier,
struct kvm_memory_slot,
notifier);
struct kvm *kvm = slot->kvm;
gfn_t gfn_start, gfn_end;
struct kvm_gfn_range gfn_range;
int idx, fw_idx;
//...
gfn_range.start = gfn_start;
gfn_range.end = gfn_end;
gfn_range.slot = slot;
gfn_range.pte = __pte(0);
gfn_range.may_block = true;
gfn_range.flags = KVM_GFN_RANGE_FLAGS_RESTRICTED_MEM;
//...
// 重点就在于调用这个函数
if (kvm_unmap_gfn_range(kvm, &gfn_range))
kvm_flush_remote_tlbs(kvm);
//...
}
kvm_vm_ioctl_set_mem_attributes()
KVM
这个函数主要是做两个事:
- 把传进来的 range 的 xarray 相关部分设置为对应的 attribute
- 为了让 guest 访问内存的时候不出现过时的数据,所以 unmap 掉这个 range 的 page,这样 guest 访问才会触发 page fault,KVM 会查看 xarray 里的 attribute 从而进行新的 mapping(比如 TDX 的一些 SEAMCALL)。
static int kvm_vm_ioctl_set_mem_attributes(struct kvm *kvm, struct kvm_memory_attributes *attrs)
{
gfn_t start, end;
unsigned long i;
void *entry;
int idx;
//...
// checks...
//...
// 根据要设置的 attribute 的值确定 entry 应该是这个值还是 NULL
// 也就是说,是要 set 还是 unset
entry = attrs->attributes ? xa_mk_value(attrs->attributes) : NULL;
// if TDX or not
if (kvm_arch_has_private_mem(kvm)) {
//...
// 一些记录工作
//...
}
//...
// 注意,并不是一个 range (start, end) 来存的
// 而是对这个 range 内的每一个 index 来存的
for (i = start; i < end; i++)
xa_store(&kvm->mem_attr_array, i, entry, GFP_KERNEL_ACCOUNT)
//...
// if TDX or not
if (kvm_arch_has_private_mem(kvm)) {
//...
// 我们假设所有的 store 都成功了
kvm_unmap_mem_range(kvm, start, end, attrs->attributes);
// 一些记录工作
//...
}
// copy to user things...
}
[PATCH V10 1/9] mm: Introduce memfd_restricted system call to create restricted user memory
这个 Patch 改的是 MM 的内容,不是 KVM 的,可以不细看。
Restricted from userspace access through ordinary MMU operations (e.g. read/write/mmap).
Pages in restrictedmem are marked as unmovable and unevictable, this is required for current confidential usage. But in future this might be changed.
The userspace restricted memfd can be fallocate-ed or hole-punched from userspace. When hole-punched, KVM can get notified through invalidate_start/invalidate_end() callbacks, KVM then gets chance to remove any mapped entries of the range in the secondary page tables.
RESTRICTEDMEM 是基于 TMPFS 封装的一层,所以 CONFIG_RESTRICTEDMEM
也是依赖于 CONFIG_TMPFS
的
KVM uses the new restrictedmem_get_page()
to obtain the physical memory page.
restrictedmem_get_page()
/ kvm_restricted_mem_get_pfn()
KVM
根据 restrictedmem_fd 里的 offset 拿到对应的 struct page
结构。
这个 offset 应该是从 GFN 里映射过来的。所以本质上是根据一个 GFN,找到对应的 PFN 以及这个 PFN 对应的页的描述结构 struct page
。
kvm_restricted_mem_get_pfn
restrictedmem_get_page
int restrictedmem_get_page(struct file *file, pgoff_t offset, struct page **pagep, int *order)
{
struct restrictedmem_data *data = file->f_mapping->private_data;
struct file *memfd = data->memfd;
struct folio *folio;
struct page *page;
int ret;
// 先拿到 folio
ret = shmem_get_folio(file_inode(memfd), offset, &folio, SGP_WRITE);
// erro handling...
// 再根据 folio 的 offset 拿到第几个 page
page = folio_file_page(folio, offset);
*pagep = page;
// THP-related...
SetPageUptodate(page);
unlock_page(page);
return 0;
}
[PATCH V10 2/9] KVM: Introduce per-page memory attributes
Introduce two ioctls (advertised by KVM_CAP_MEMORY_ATTRIBUTES
) to allow userspace to operate on the per-page memory attributes.
-
KVM_SET_MEMORY_ATTRIBUTES
to set the per-page memory attributes to a guest memory range. -
KVM_GET_SUPPORTED_MEMORY_ATTRIBUTES
to return the KVM supported memory attributes.
VM internally uses xarray
to store the per-page memory attributes.
UPM 的 xarray 的长度就是启动 TD 时 mem 的 size,并不是 lazy 的。bit 为 1 就表示这个 page 应该是 private 的(实际上这个 page 的属性真的改成 private 可能是 lazy 的)。
struct kvm {
//...
#ifdef CONFIG_HAVE_KVM_MEMORY_ATTRIBUTES
struct xarray mem_attr_array;
#endif
//...
}
[PATCH V10 3/9] KVM: Extend the memslot to support fd-based private memory
It's valueless and sometimes can cause problem to allow userspace to access guest private memory.
主要是加了一个 struct:
struct kvm_userspace_memory_region_ext {
struct kvm_userspace_memory_region region;
__u64 restricted_offset;
__u32 restricted_fd;
__u32 pad1;
__u64 pad2[14];
};
Allow userspace to instruct KVM to provide guest memory through restricted_fd
. GPA is mapped at the restricted_offset
of restricted_fd
and the size is memory_size
.,这种设计的好处可能是一个 fd 可以被多个 memory slot 来使用,只需要保证它们 map 的 offset 不冲突就可以。
The extended memslot can still have the userspace_addr
(HVA). When use, a single memslot can maintain both private memory through restricted_fd
and shared memory through userspace_addr
.
A restrictedmem_notifier
field is also added to the memslot structure to allow the restricted_fd's backing store to notify KVM the memory change:
struct kvm_memory_slot {
//...
struct restrictedmem_notifier notifier;
};
[PATCH V10 4/9] KVM: Add KVM_EXIT_MEMORY_FAULT exit
添加了一个新的 exit 类型,allows userspace to handle memory-related errors. It indicates an error happens in KVM at guest memory range [gpa, gpa+size)
.
[PATCH V10 5/9] KVM: Use gfn instead of hva for mmu_notifier_retry
Currently in mmu_notifier
invalidate path, hva range is recorded and then checked against by mmu_notifier_retry_hva()
in the page fault handling path. However, for the to be introduced private memory, a page fault may not have a hva associated(即使 private pages 没有 map 到 Userspace,那也应该有 HVA 呀,什么 page fault 会没有 HVA 呢), checking gfn(gpa) makes more sense.
FNAME(page_fault) // page fault handler
if (is_page_fault_stale(vcpu, fault, mmu_seq))
goto out_unlock;
总之这个就是做的一个 check 的 process。
[PATCH V10 6/9] KVM: Unmap existing mappings when change the memory attributes
以下这段话不难理解:
Unmap^ the existing guest mappings when memory attribute is changed between shared and private. This is needed because shared pages and private pages are from different backends, unmapping existing ones gives a chance for page fault handler to re-populate the mappings according to the new attribute.
kvm_unmap_mem_range()
KVM
static void kvm_unmap_mem_range(struct kvm *kvm, gfn_t start, gfn_t end, unsigned long attrs)
{
struct kvm_gfn_range gfn_range;
struct kvm_memory_slot *slot;
struct kvm_memslots *slots;
struct kvm_memslot_iter iter;
int i;
int r = 0;
gfn_range.attrs = attrs;
gfn_range.may_block = true;
gfn_range.flags = KVM_GFN_RANGE_FLAGS_SET_MEM_ATTR;
for (i = 0; i < KVM_ADDRESS_SPACE_NUM; i++) {
slots = __kvm_memslots(kvm, i);
kvm_for_each_memslot_in_gfn_range(&iter, slots, start, end) {
slot = iter.slot;
gfn_range.start = max(start, slot->base_gfn);
gfn_range.end = min(end, slot->base_gfn + slot->npages);
// 和这个 memslot 没有重合的地方,所以 continue 继续找下一个 slot
if (gfn_range.start >= gfn_range.end)
continue;
gfn_range.slot = slot;
// 只 unmap 重合的区间。
r |= kvm_unmap_gfn_range(kvm, &gfn_range);
// 看下一个 patch,针对一个页有大页也有小页的情况
kvm_arch_set_memory_attributes(kvm, slot, attrs, gfn_range.start, gfn_range.end);
}
}
// error handling...
}
[PATCH V10 7/9] KVM: Update lpage info when private/shared memory are mixed
针对当一个大页里既有 private 的小页,也有 shared 的小页的情况。
A large page with mixed private/shared subpages can't be mapped as large page since its sub private/shared pages are from different memory backends and may also treated by architecture differently. When private/shared memory are mixed in a large page, the current lpage_info is not sufficient to decide whether the page can be mapped as large page or not and additional private/shared mixed information is needed.
[PATCH V10 8/9] KVM: Handle page fault for private memory
给 struct kvm_page_fault
add 了一个 is_private
的属性,表示发生 page fault 的 access 是 shared 还是 private 的(这个是通过 fault 的 gfn 来得知,具体看 gfn_shared_mask
,这个是 TDX Module 设计的时候发生 page fault 就有的信息)。同时期望 architecture code 比如说 TDX 来置上。
具体信息请看 page fault in TDX^。
记住 page fault 指的是 Guest 访问 Guest memory 时产生的,而不是在 Host 访问时产生的。
[PATCH V10 9/9] KVM: Enable and expose KVM_MEM_PRIVATE
Register/unregister private memslot to fd-based memory backing store restrictedmem
and implement the callbacks for restrictedmem_notifier
:
-
invalidate_start
()/invalidate_end
() to zap the existing memory mappings in the KVM page table. -
error()
to requestKVM_REQ_MEMORY_MCE
and later exit to userspace withKVM_EXIT_SHUTDOWN
.
invalidate_start
和 invalidate_end
什么时候才会被触发呢?是不是 migratepages
就可以触发到 MM,从而 notify 到 KVM 来 invalidate mappings。
UPM QEMU Patch
UPM QEMU patch 一般和 TDX patch 是在一起的,gmem 是分开的。
和 gmem 一样,也是 RAMBlock 里两个 fd,一个正常 fd
一个 restricted_fd
。
struct RAMBlock {
//...
int fd;
// qemu_memfd_restricted() 创建的
int restricted_fd;
//...
}
为什么要这么设计,这样不会浪费内存吗?原因是我们要支持 shared to private 的转换。
尽管对于 shared pages,RAMBlock 里存的是 fd,但是最后传给 KVM 的时候是 userspace_addr,也就是 HVA。
memfd_restricted()
是一个 syscall,可以创建 restricted memfd。
memory_region_set_restricted_fd()
QEMU
void memory_region_set_restricted_fd(MemoryRegion *mr, int fd)
{
if (mr->ram_block) {
mr->ram_block->restricted_fd = fd;
}
}
priv_memfd_backend_memory_alloc()
QEMU
host_memory_backend_memory_complete
bc->alloc = priv_memfd_backend_memory_alloc;
static void priv_memfd_backend_memory_alloc(HostMemoryBackend *backend, Error **errp)
{
HostMemoryBackendPrivateMemfd *m = MEMORY_BACKEND_MEMFD_PRIVATE(backend);
uint32_t ram_flags;
char *name;
int fd, priv_fd;
unsigned int flags;
//...
fd = qemu_memfd_create("memory-backend-memfd-shared", backend->size, m->hugetlb, m->hugetlbsize, 0, errp);
//...
name = host_memory_backend_get_name(backend);
ram_flags = backend->share ? RAM_SHARED : 0;
ram_flags |= backend->reserve ? 0 : RAM_NORESERVE;
memory_region_init_ram_from_fd(backend->mr, OBJECT(backend), name, backend->size, ram_flags, fd, 0, errp);
//...
flags = 0;
mount_fd = -1;
//...
priv_fd = qemu_memfd_restricted(backend->size, flags, mount_fd, errp);
//...
// backend 的整个 RAM 共用这一个 restricted_fd
// 不同的 kvm memory slot 可能用的都是不同的 offset~
memory_region_set_restricted_fd(backend->mr, priv_fd);
ram_block_alloc_cgs_bitmap(backend->mr->ram_block);
}