QEMU注册内存到KVM流程

数据结构

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只是设计用来做内存分配器用。

  • 4
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

享乐主

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值