数据结构
QEMU
typedef struct KVMSlot
{
hwaddr start_addr; /* 虚机内存区间起始地址(GPA) */
ram_addr_t memory_size; /* 虚机内存区间长度 */
void *ram; /* 虚机内存区间对应的主机虚拟地址起始内存的指针,通过该指针可以查看内存页内容 */
int slot; /* 在虚机所拥有的内存slot数组的索引 */
int flags;
int old_flags;
/* Dirty bitmap cache for the slot */
unsigned long *dirty_bmap; /* slot内存区间的脏页位图,通过查询kvm得到 */
unsigned long dirty_bmap_size;
/* Cache of the address space ID */
int as_id; /* slot所在地址空间在整个虚机地址空间数组的索引 */
/* Cache of the offset in ram address space */
ram_addr_t ram_start_offset;/* slot表示的内存区间对应主机虚拟地址区间起始地址,即相对RAMBlock->host的偏移 */
} KVMSlot;
- TODO
typedef struct KVMMemoryListener {
MemoryListener listener;
KVMSlot *slots;
int as_id;
} KVMMemoryListener;
KVM
- TODO
内存注册接口
- qemu为虚机分配内存后,通过MR可以查到GPA与HVA的映射关系,这段映射关系需要通知kvm,方便kvm在处理虚机缺页时将SPTE的信息同步到qemu用户态进程地址空间的页表,因为kvm的缺页处理是解决GPA到HPA的映射,但如果这个映射关系不同步到qemu进程的页表,建立HVA到HPA的映射,qemu将无法通过HVA查找到HPA。qemu MR可以表示一段虚机内存。
- kvm是内核模块,它处理缺页时完全可以直接走主机的缺页处理流程,然后将获得的HPA填入EPT的页表,为什么要绕个圈子通过GPA->HVA->HPA实现缺页处理呢?因为如果直接走缺页,那么qemu进程的页表就不会被填充,qemu就不知道自己为虚机分配的内存,哪些已经被使用,哪些还没有使用。kvm暗戳戳地满足了虚机的缺页要求但qemu不知道,那qemu还怎么管理虚机内存呢。
- 为了让qemu/kvm相互知道虚机的内存使用情况,一开始qemu为虚机分配好内存的之后,就需要知会到kvm虚机GPA与HVA的映射关系。这个就是QEMU的内存注册。kvm为内存注册提供了vm ioctl接口,命令字为
KVM_SET_USER_MEMORY_REGION
,传入的参数是个kvm_userspace_memory_region
数据结构,如下:
Capability: KVM_CAP_USER_MEM /* 1 */
Architectures: all
Type: vm ioctl
Parameters: struct kvm_userspace_memory_region (in)
Returns: 0 on success, -1 on error
struct kvm_userspace_memory_region {
__u32 slot; /* 2 */
__u32 flags; /* 3 */
__u64 guest_phys_addr; /* 4 */
__u64 memory_size; /* bytes */ /* 5 */
__u64 userspace_addr; /* start of the userspace allocated memory */ /* 6 */
};
1. kvm ioctl接口对应的cap字段,当查询到kvm不支持KVM_CAP_USER_MEM cap时,表示不支持这个接口
2. 要在哪个slot上注册内存区间
3. flags有两个取值,KVM_MEM_LOG_DIRTY_PAGES和KVM_MEM_READONLY,用来指示kvm针对这段内存应该做的事情。KVM_MEM_LOG_DIRTY_PAGES用来开启内存脏页,KVM_MEM_READONLY用来开启内存只读。
4. 虚机内存区间起始物理地址
5. 虚机内存区间大小
6. 虚机内存区间对应的主机虚拟地址
- qemu在下发命令字
KVM_SET_USER_MEMORY_REGION
传入参数时,内存区间的物理地址和主机虚拟地址是自己分配空间后设置的,区间大小也是自己设置的,但插槽号slot是查询kvm得到的,就是说,qemu注册内存必须放在一个空的插槽里。当注册内存完成后,qemu就认为这个插槽满了,不允许再注册内存。 - 注意:该接口除了用于内存注册,还会用来开启内存脏页。当内存迁移开始前,Qemu就会下发这个命令字并将flags标记设置为
KVM_MEM_LOG_DIRTY_PAGES
,这样KVM就会跟踪这段内存的脏页。
内存插槽
- qemu在初始化模拟虚机时会通过
KVM_CHECK_EXTENSION
命令字查询kvm支持的虚机内存最大插槽数,代码如下:
kvm_init
s->nr_slots = kvm_check_extension(s, KVM_CAP_NR_MEMSLOTS) /* 查询kvm支持的最大内存插槽数 */
- 内存接收到命令字后返回的内容如下:
kvm_vm_ioctl_check_extension /* kvm返回代码中定义的最大插槽数 */
case KVM_CAP_NR_MEMSLOTS:
r = KVM_USER_MEM_SLOTS;
- 获取插槽数之后,qemu在往内存地址空间
address_space_memory
注册listener时,会根据查询到的插槽数,分配对应数量的slot结构体,所有slot都分配一个id,此时这些slot是空闲的:
kvm_init
kvm_memory_listener_register(s, &s->memory_listener, &address_space_memory, 0);
kml->slots = g_malloc0(s->nr_slots * sizeof(KVMSlot)); /* 根据最大插槽数分配空间 */
for (i = 0; i < s->nr_slots; i++) {
kml->slots[i].slot = i;
}
kml->listener.region_add = kvm_region_add; /* 实现listener的region_add回调 */
......
memory_listener_register(&kml->listener, as); /* 将listener添加到全局的memory_listeners链表上 */
- 在qemu为虚机分配好内存后,会更新地址空间拓扑,触发全局memory_listeners链表上各个listener元素的region_add回调,最后会调用kvm的kvm_region_add实现:
kvm_region_add
kvm_set_phys_mem
/* register the new slot */
mem = kvm_alloc_slot(kml); /* 查找空闲的slot结构,判断依据是空闲的slot内存大小memory_size为0 */
mem->memory_size = size; /* slot表示的虚机内存区间长度 */
mem->start_addr = start_addr; /* slot表示的虚机内存区间起始物理地址(GPA) */
mem->ram = ram; /* slot表示的虚机内存区间起始物理地址对应的主机虚拟地址指针 */
mem->ram_start_offset = ram_start_offset; /* slot表示的虚机内存区间起始物理地址对应的主机虚拟地址(HVA) */
......
kvm_set_user_memory_region /* 注册到kvm的slot */
- 最后看一下分配空闲slot的实现流程:
kvm_alloc_slot
kvm_get_free_slot
for (i = 0; i < s->nr_slots; i++) {
if (kml->slots[i].memory_size == 0) { /* slot的内存大小为0表示slot空闲,如果是已经添加了内存的插槽,添加后会设置memory_size */
return &kml->slots[i];
}
}
内核数据结构
- kvm收到qemu注册内存的命令字会做些什么动作呢?从内存注册的目的来看,kvm需要维护虚机内存GPA到HVA的映射关系,当虚机缺页时,就通过这个数据结构来输入GPA查询HVA。kvm_memory_slot数据结构就是设计用来维护GPA到HVA关系的:
struct kvm_memory_slot {
gfn_t base_gfn; /* 1 */
unsigned long npages; /* 2 */
unsigned long *dirty_bitmap; /* 3 */
struct kvm_arch_memory_slot arch; /* 4 */
unsigned long userspace_addr; /* 5 */
u32 flags;
short id;
};
1. 虚机内存区间的物理页框号,内存插槽slot添加一块内存之后,就关联起虚机的一段内存了,base_gfn记录这个区间的起始页框号
2. 将内存区间的大小转化成页数,由npages记录
3. slot还提供对slot上内存的每个页进行脏页记录功能,脏页纪录功能开启时,kvm就会通过dirty_bitmap记录脏页,这在迁移内存时需要
4. 反向映射相关的结构
5. 内存区间对应的主机虚拟地址
- arch成员是记录反向映射相关的,当主机上需要回收内存页时,如果这块内存被虚机使用,需要将EPT页表上记录的页状态从存在改为不存在。我们需要定位指向同一物理页框号的所有EPT页表项。
- 怎么实现呢?首先内核有页框回收算法,它的反向映射可以定位指向同一页框的所有页表项,即通过主机物理页框号可以查到所有使用该页的虚拟地址HVA,获得HVA后,根据kvm维护的kvm_memory_slot可以查到对应的GPA。这个时候,有两种方法可以找到GPA落在哪个EPT页表项,第一种是遍历EPT,一层一层逐级往下查找,直到找到包含GPA的页表项。另一种就是维护反向映射reverse map结构,在kvm缺页处理创建EPT页表项时就记录GPA对应的页表项地址。arch.rmap成员就是维护这个关系的,如下:
struct kvm_arch_memory_slot {
struct kvm_rmap_head *rmap[KVM_NR_PAGE_SIZES]; /* 6 */
......
};
6. KVM_NR_PAGE_SIZES是qemu为虚机分配的不同大小的页的种类,比如2M,1G等,这里的宏定义为3种。kvm需要为不同页大小的页都维护
其对应的EPT页表项。每个rmap[i]是一个数组,它的内存是页对应的EPT页表地址,整个数组在内存注册时会分配内存空间,后面会分析
- kvm_memory_slot描述的是一段虚机内存区间,虚机有无数这个这样的内存段,它们都属于这个虚机,所有slot放在一起被kvm的地址空间管理,不同地址空间的两个slot没有任何关系。kvm->memslots维护了地址空间的数组,每个地址空间包含了无数个slot,如下:
struct kvm {
......
struct kvm_memslots *memslots[KVM_ADDRESS_SPACE_NUM]; /* 7 */
......
7. 目前地址空间的个数KVM_ADDRESS_SPACE_NUM被定义成2,因此memslots有两个成员,对应两个地址空间
struct kvm_memslots {
......
struct kvm_memory_slot memslots[KVM_MEM_SLOTS_NUM]; /* 地址空间维护的所有slot的数组 */
......
};
- 内存插槽,地址空间和kvm虚机的数据结构关系如下,内存的注册就是往kvm_memslots.memslots数组中添加slot元素,每添加一个slot,就注册了一个虚机内存区间
内存注册流程
- 以上所有都是为了说明kvm的内存注册流程,流程从ioctl接收命令字开始,最终会走到__kvm_set_memory_region函数,内存注册的主要工作在这个函数完成:
kvm_vm_ioctl
case KVM_SET_USER_MEMORY_REGION:
kvm_vm_ioctl_set_memory_region(kvm, &kvm_userspace_mem);
kvm_set_memory_region
__kvm_set_memory_region
int __kvm_set_memory_region(struct kvm *kvm,
const struct kvm_userspace_memory_region *mem)
{
as_id = mem->slot >> 16; /* 1 */
id = (u16)mem->slot;
slot = id_to_memslot(__kvm_memslots(kvm, as_id), id); /* 2 */
base_gfn = mem->guest_phys_addr >> PAGE_SHIFT; /* 3 */
npages = mem->memory_size >> PAGE_SHIFT; /* 4 */
new.id = id; /* 5 */
new.base_gfn = base_gfn;
new.npages = npages;
if (change == KVM_MR_CREATE) {
new.userspace_addr = mem->userspace_addr;
if (kvm_arch_create_memslot(kvm, &new, npages)) /* 10 */
goto out_free;
}
......
1. slot的低16位是插槽号,往上是地址空间的id,将插槽号和地址空间id都取出来
2. 根据地址空间id从kvm->memslots[]数组中找到对应的地址空间kvm_memslots,再根据slot id从kvm_memslots->memslots[]数组中找到对应的
slot结构体
3. 将新注册的kvm_memory_slot结构体信息搜集起来,放到新的slot中。
4. 填充这个slot的成员
- slot结构在kvm初始化就被创建出来了,但彼时这个结构是空闲的,不知道它要放多少内存页,反向映射的arch成员没有初始化,kvm_arch_create_memslot根据注册的内存页数量来初始化这个成员,如下:
int kvm_arch_create_memslot(struct kvm *kvm, struct kvm_memory_slot *slot,
unsigned long npages)
{
......
for (i = 0; i < KVM_NR_PAGE_SIZES; ++i) { /* 5 */
lpages = gfn_to_index(slot->base_gfn + npages - 1, /* 6 */
slot->base_gfn, level) + 1;
slot->arch.rmap[i] = kvzalloc(lpages * sizeof(*slot->arch.rmap[i]), GFP_KERNEL); /* 7 */
}
5. 针对不同大小的页,初始化slot的arch成员,不同大小的页,在slot都有单独的数据结构维护
6. 根据内存区间大小计算出页的个数
7. 为反向映射rmap[i]分配内存,可以看到分配了lpages个元素,因此QEMU注册的每个页,都有对应的EPT页表项地址存放在rmap二维数组中。
当kvm处理缺页走主机缺页流程后,填写EPT页表之后,也会把页表地址写到ramp中
Q&A
Q:内存插槽slot是模拟真实物理内存的slot吗?它们之间有啥关系没有?
A:这里的slot和物理内存的slot模拟没有关系,kvm只是设计用来做内存分配器用。