KVM同步脏页原理


QEMU在内存迁移阶段首先会标脏所有内存页,然后通知KVM开启脏页统计。本章主要介绍KVM在脏页统计中的作用,首先介绍intel x86脏页统计的硬件基础,然后介绍KVM中脏页统计相关数据结构、脏页开启的IOCTL命令字介绍、脏页统计发起流程,最后实验验证上述分析。

硬件基础

  • KVM脏页统计离不开硬件支持,在Intel没有引入PML(Page Modification Logging)特性之前。主要利用SPTE的D状态位跟踪脏页。在引入PML之后,则通过PML跟踪脏页,其主要功能是记录虚机写内存页的行为并将内存页的地址GPA记录下来。我们分别介绍SPTE和PML。

SPTE

硬件要素

请添加图片描述
在这里插入图片描述

  • 上图为intel Page table各个字段定义,其中bit 6称为脏页标记位,我们知道intel Page table有两个作用,一个是指向真正的物理页,一个是指向包含下级页表内容的物理页,用于线性地址转化。
  • 当一个Page table指向真正物理页的条目(作为SPTE)时,如果CPU对该SPTE指向的物理页有写操作,硬件会将脏页标记位置位,CPU下一次写该页时会再次置位(需要软件清零脏页位),通过硬件置位-软件清零的手段,软件可以跟踪到虚拟机脏页状态。

工作流程

  • TODO

PML

硬件要素

在这里插入图片描述

  • PML是Intel为支持虚拟化场景下脏页跟踪而开发的硬件特性,使用该特性涉及到Intel的几个硬件元素,下面根据以上图示一一介绍:
  1. Accessed and Dirty flags:这是EPTP字段中位于bit 6的一个标志位,当设置此标志位后,它告诉CPU每当使用EPT查询HPA时,将页结构存放的表项中的Accessed位(bit 8)置1,对于指向物理页的页表项,当往指向的物理页中写数据时,将它的Dirty位(bit 9)置1。
  2. PML flag:这是VMCS VM-Exection Controls Field区域的一个标志位,用于使能PML特性。只有在Accessed and Dirty flags位开启时,才可以使能PML。
  3. PML Buffer:一块内存区域,用来存放上一次开启PML特性之后,CPU写过的物理页的地址,大小为4K,可以存放512条GPA。KVM就是通过这个区域来跟踪内存脏页。
  4. PML Address:用来保存PML Buffer的内存物理地址,它是VMCS VM-Execution control field的一个字段。
  5. PML index:用来保存PML Buffer的索引,长度16 bit,表示范围0-511,和PML Buffer中的条目对应。PML index用来保存下一次CPU写物理页保存GPA时在PML Buffer中的位置。PML index每次递减。

工作流程

在这里插入图片描述

  • 下面介绍开启PML特性之后,记录脏页的整个工作流程,如图所示,分以下5个步骤:
  1. 当CPU想要记录内存脏页,比如迁移开始时,首先设置Accessed and Dirty flags和PML flag标志,使能脏页记录功能。当CPU写VM的物理内存页时,如果发现Accessed and Dirty和PML都使能了,会首先将对应表项的Access和Dirty位置1,然后将GPA记录到PML Buffer中。每写一次物理内存页,PML Buffer就多一条GPA的记录,PML index减1。
  2. 当PML Buffer被填满时,会产生page-modification log-full event,然后触发VMExit。
  3. CPU退出到根模式的内核态之后,KVM会判断退出原因,如果是PML Buffer满引起的退出,会将PML Buffer的内容保存下来,然后PML Buffer的索引PML index会被重新置为511。
  4. PML Buffer的内容,就是CPU最近写过的内存页地址,它就是内存的脏数据,对于内存迁移来说,该数据可以用来评估虚机的内存变化量。如果PML Buffer中内容较少,那么Qemu可以将虚机暂停然后一次性拷贝完。
  5. 当CPU不想记录内存脏页,迁移已经完成后,就可以关闭PML特性。
  • 注意:上面的过程是Intel PML特性的通用使用过程,对于kvm的实现中,并不是每次迁移开始前才开启PML特性,而是在kvm模块初始化的过程中开启,PML Buffer的分配是在vcpu创建过程中。迁移结束也不会关闭PML特性。PML特性是一直存在于KVM模块的生命周期中的。

数据结构

用户态

  • RAMList
typedef struct {
   ......
   /* 存放记录脏页的位图,一个block元素可以表示的内存范围是256K个内存页 */
    unsigned long *blocks[];
} DirtyMemoryBlocks;

typedef struct RAMList {
	......
	/* 虚机占用的所有主机上的RAM内存块集合 */
    QLIST_HEAD(, RAMBlock) blocks;
    /* 用户态从内核获取虚机脏页信息时保存到的结构体 */
    DirtyMemoryBlocks *dirty_memory[DIRTY_MEMORY_NUM];
} RAMList;
  • RAMBlock
struct RAMBlock {
	...... 
	/* 内存区域表示的最大范围,单位是字节 */
	ram_addr_t max_length;
	/* dirty bitmap used during migration */
	/* 记录该内存区域脏页情况的位图,一个内存页对应一个bit位
	    它是一个长整形指针数组,整个数组元素长度加起来转换
	    成bit数等于max_length/4K 
	    */
    unsigned long *bmap;								
	......
}
  • 用户态与脏页统计有关的数据结构有以上两个,RAMList和RAMBlock,它们各自的数据结构图示如下,既然都是统计脏页,为什么需要两个数据结构呢?个人觉得,这两个数据结构的使用场景是不一样的,RAMList用在从内核获取脏页的时候,它表示脏页的粒度是kvm中的一个slot;而RAMBlock中的位图用来描述一个RAMBlock的脏页使用情况,它表示的脏页粒度是Qemu中的一个RAMBlock。在内存迁移统计脏页过程中,会依次使用这两个数据结构统计剩余内存的脏页数量。
    在这里插入图片描述

内核态

  • vcpu_vmx
struct vcpu_vmx {
	struct page *pml_pg; 
	......
}
  • vmcs_field_to_offset_table
vmcs_field_to_offset_table[] = { 
	......
	FIELD(GUEST_PML_INDEX, guest_pml_index),
	FIELD64(PML_ADDRESS, pml_address)
	......
}
  • kvm_userspace_memory_region
/* for KVM_SET_USER_MEMORY_REGION */
struct kvm_userspace_memory_region {
	/* 内存区间所在的插槽 */
	__u32 slot;								
	/* 当flags包含KVM_SET_USER_MEMORY_REGION标识时
	   表示开启这段内存的脏页记录  
	   */
	__u32 flags;					
	/* 虚机内存区间物理地址 */		
	__u64 guest_phys_addr;
	/* 虚机内存区间大小 */
	__u64 memory_size; /* bytes */
	/* 分配给这段内存区间的实际的用户态地址HVA */
	__u64 userspace_addr; /* start of the userspace allocated memory */
};
  • kvm_dirty_log
/* for KVM_GET_DIRTY_LOG */
struct kvm_dirty_log {
	__u32 slot;
	__u32 padding1;
	union {
		void *dirty_bitmap; /* one bit per page */
		__u64 padding2;
	};
};
  • kvm_memory_slot
struct kvm_memory_slot {
	gfn_t base_gfn;
	unsigned long npages;
	unsigned long *dirty_bitmap;
	u32 flags;
    ....
};

API

  • 为了让用于态统计虚机的脏页,内核提供了两个接口,分别是KVM_SET_USER_MEMORY_REGIONKVM_GET_DIRTY_LOG,这两个接口

脏页开启

  • 内核提供了两个命令供用户态统计虚机的脏页,KVM_SET_USER_MEMORY_REGION、KVM_GET_DIRTY_LOG,KVM_SET_USER_MEMORY_REGION命令字作用在vm的fd,用来通知kvm开启对某段内存区域的脏页跟踪,结构体kvm_userspace_memory_region 是用户态传入的参数,用来描述kvm应该跟踪的内存区域,如下:
Capability: KVM_CAP_USER_MEMORY
Architectures: all
Type: vm ioctl          /* 虚机vm ioctl命令字 */
Parameters: struct kvm_userspace_memory_region (in)
Returns: 0 on success, -1 on error
 
  struct kvm_userspace_memory_region {
    __u32 slot;
    __u32 flags;
    __u64 guest_phys_addr;
    __u64 memory_size; /* bytes */
    __u64 userspace_addr; /* start of the userspace allocated memory */
  };
  /* for kvm_memory_region::flags */
  #define KVM_MEM_LOG_DIRTY_PAGES    (1UL << 0)
  #define KVM_MEM_READONLY    (1UL << 1)

脏页获取

  • KVM_GET_DIRTY_LOG命令字作用在vm的fd,用来获取内核跟踪的脏页信息,结构体kvm_dirty_log作为参数用来指定要查询的内存slot,同时保存内核的脏页查询结果,如下:
Capability: basic
Architectures: all
Type: vm ioctl
Parameters: struct kvm_dirty_log (in/out)
Returns: 0 on success, -1 on error
 
  /* for KVM_GET_DIRTY_LOG */
  struct kvm_dirty_log {
    /* 输入,指定要查询脏页的内存slot */
    __u32 slot;
    __u32 padding;
    union {
        /* 输出,保存kvm查询到的脏页 */
        void __user *dirty_bitmap; /* one bit per page */
        __u64 padding;
    };
  };

流程

使能记录

  • KVM的PML特性在模块初始化的过程中完成,每创建一个vcpu,需要为其分配PML Buffer并初始化PML Index。
使能PML特性:
/* kvm模块初始化入口  */
vmx_init										
	kvm_init
		kvm_arch_hardware_setup
			vmx_x86_ops.hardware_setup
				hardware_setup
					/* 设置VMCS区域内存的内容
					   在VMLAUNCH和VMRESUME的时候被加载到VMCS区域
					 */
					setup_vmcs_config
						/* 设置SECONDARY_EXEC_ENABLE_PML标识使能PML */
						opt2 = SECONDARY_EXEC_VIRTUALIZE_APIC_ACCESSES | XXX | SECONDARY_EXEC_ENABLE_PML
						adjust_vmx_controls(min2, opt2, MSR_IA32_VMX_PROCBASED_CTLS2, &_cpu_based_2nd_exec_control)
						vmcs_conf->cpu_based_2nd_exec_ctrl = _cpu_based_2nd_exec_control;
设置PML Buffer地址和PML Index初始值:
/* vcpu创建入口 */
vmx_create_vcpu
	vmx_vcpu_setup
		/* 如果使能了PML特性,分配一个物理页给pml_pg用作PML Buffer */
		if (enable_pml) {
			vmx->pml_pg = alloc_page(GFP_KERNEL | __GFP_ZERO);
		}
		/* 将分配好的PML Buffer地址写入VMCS的对应区域,同时初始化PMLIndex为511  */
		if (enable_pml) {
			vmcs_write64(PML_ADDRESS, page_to_phys(vmx->pml_pg)); 
			vmcs_write16(GUEST_PML_INDEX, PML_ENTITY_NUM - 1);
		}

记录脏页

流程图

在这里插入图片描述

具体过程

  • 根据PML的硬件特性,每当CPU在Guest态根据EPT转换地址后,写数据到物理页,这时如果PML特性开启,在设置EPT页表项的Dirty位之后,还会将GPA地址写入PML Buffer。这里intel暗含的假设是之后Dirty标志位从0变成1,才能写PML Buffer,而Dirty标志位的变成1之后的清零动作是软件做的,因此如果想要记录一个slot的脏页,需要将引用这个slot的所有页表项的Dirty标志位都清零。开启脏页记录需要做以下两件事情:
  1. 清零Dirty位:KVM的实现中,在创建slot时,如果不想记录某个slot包含的所有物理页的是否为脏,需要默认将这些物理页对应的页表项的Dirty页置位,因为如果Dirty位是0,Guest态CPU写物理页时会将其置1并且填充GPA到PML Buffer,如果PML Buffer满了,就会触发VMExit,增加不必要的开销。反之,要记录脏页,首先需要将指向slot包含的所有物理页的spte的Dirty位清零,这里需要根据gfn找到指向该gfn对应页的spte,反向映射数组rmap就派上了用场。
  2. 物理页开启写保护:除了清零页表项的Dirty位,记录脏页还需要开启页的写保护,在脏页记录的过程中,所有slot包含的物理页变成只读,当CPU写访问这个页时,发生缺页异常,kvm会重新分配一个新的页给CPU。在脏页记录关闭后,才能将写保护去掉,slot包含的所有页变成可读写。
/* kvm ioctl入口 */
kvm_vm_ioctl
	/* slot命令字,内核根据传入的参数
	   对应地增加,删除和更新slot
	   这里要跟踪slot的脏页,算在更新里面
	   */
    case KVM_SET_USER_MEMORY_REGION
    kvm_vm_ioctl_set_memory_region
        kvm_set_memory_region
            __kvm_set_memory_region
            	/* 根据用户态传入的参数,设置slot,这里是开启脏页的记录*/
                kvm_set_memslot
                    kvm_arch_commit_memory_region           
                        kvm_mmu_slot_apply_flags
                        	/* 根据intel vmx的特性开启脏页记录 */
                        	if (new->flags & KVM_MEM_LOG_DIRTY_PAGES) {
                        		if (kvm_x86_ops.slot_enable_log_dirty) {
                        			/* 对于x86平台,如果intel提供了PML机制可以记录脏页,进入这个流程 */
									kvm_x86_ops.slot_enable_log_dirty(kvm, new)
									kvm_x86_ops.slot_enable_log_dirty   <=>   vmx_slot_enable_log_dirty
								} else {
									/* 在没有PML机制的情况下,使用页只读的方式记录脏页 
									 * 判断两种情况,分别处理:
									 * 1. 首次迭代时位图都标记为1,不需要标记4k小页,但需要标记
									 * 2M的大页为只读,这样kvm在处理虚机大页访问异常时会将其拆
									 * 分成小页。
									 * 2. 对于其它轮次的迭代,需要将所有4k页的页表项都标记为只读
									 */
                        			int level =
										kvm_dirty_log_manual_protect_and_init_set(kvm) ?
										PG_LEVEL_2M : PG_LEVEL_4K;
										kvm_mmu_slot_remove_write_access(kvm, new, level);
                            	}	
                          	}
  • 分析x86架构下使用PML机制记录脏页的流程,kernel中关于PML场景下的comment如下:
        /*
         * Dirty logging tracks sptes in 4k granularity, meaning that
         * large sptes have to be split.  If live migration succeeds,
         * the guest in the source machine will be destroyed and large
         * sptes will be created in the destination.  However, if the
         * guest continues to run in the source machine (for example if
         * live migration fails), small sptes will remain around and
         * cause bad performance.
         *
         * Scan sptes if dirty logging has been stopped, dropping those
         * which can be collapsed into a single large-page spte.  Later
         * page faults will create the large-page sptes.
         */
  • 流程如下:
vmx_slot_enable_log_dirty
	kvm_mmu_slot_apply_flags
		/* cpu_dirty_log_size存在表明开启了PML,使能脏页日志跟踪分两种情况 
         * 1: 对于4K普通的页,只需要清零D-bit状态位即可
         * 2: 对于2M大页,除了清零D-bit状态位,还需要使能写保护
         *    使得虚机访问大页页表时也能触发异常,陷入kvm后以4k页的粒度分配虚机的页
         *    从而将大页拆分为小页
         */
		if (kvm_x86_ops.cpu_dirty_log_size) {
		    /* 清零D状态位,方便硬件下一次统计 */
			kvm_mmu_slot_leaf_clear_dirty(kvm, new);
			   /* 这里通过rmap数据,遍历指向slot包含的所有物理页的spte
	 	 	    * 针对每个spte调用__rmap_clear_dirty函数 
 	 	 	    */
 			    slot_handle_leaf_4k(kvm, memslot, __rmap_clear_dirty, false)
			/* 开启所有2M大页页表项的写保护功能 */
			kvm_mmu_slot_remove_write_access(kvm, new, PG_LEVEL_2M);
		} else {
			kvm_mmu_slot_remove_write_access(kvm, new, PG_LEVEL_4K);
		}
		
/* 清零表项的Dirty位*/
__rmap_clear_dirty
	spte_clear_dirty
		/* 将kvm模块初始化时设置的Dirty默认值取出,用于设置spte的Dirty位 */
		spte &= ~shadow_dirty_mask                          			
		mmu_spte_update
			......
			__set_spte
				/* 更新spte表项 */
				WRITE_ONCE(*sptep, spte)
				
/* 设置slot包含的所有页写保护WP */
kvm_mmu_slot_remove_write_access
	slot_handle_large_level(kvm, memslot, slot_rmap_write_protect, false);
		__rmap_write_protect(kvm, rmap_head, false)
			spte_write_protect(sptep, pt_protect)
				/* 清零页表项的bit 1,不允许CPU写物理页 */
				spte = spte & ~PT_WRITABLE_MASK
				mmu_spte_update
					......
					__set_spte
					/* 更新spte表项 */
					WRITE_ONCE(*sptep, spte)
  • 脏页日志开启后,对于使用位图方式跟踪的4k页,缺页异常的快速处理逻辑中,会首先将脏页在slot的位图中标记,然后将页表项标记为可写,不会进入慢速处理分配页表的阶段,因为这些被标记为写保护的页,本身可能已经分配了物理地址,对于使用PML方式跟踪的4k页同样如此。但是对于PML方式跟踪的大页,这里仍然会开启写保护,其目的是让缺页的慢速路径去处理缺页,使其重新分配一块4K的物理页,从而实现缺页在脏页日志开启情况下大页拆小页的逻辑。代码如下:
fast_page_fault
       /*
         * Currently, to simplify the code, write-protection can
         * be removed in the fast path only if the SPTE was
         * write-protected for dirty-logging or access tracking.
         */
        if (fault->write &&
            spte_can_locklessly_be_made_writable(spte)) {
            new_spte |= PT_WRITABLE_MASK;

            /*
             * Do not fix write-permission on the large spte.  Since
             * we only dirty the first page into the dirty-bitmap in
             * fast_pf_fix_direct_spte(), other pages are missed
             * if its slot has dirty logging enabled.
             *
             * Instead, we let the slow page fault path create a
             * normal spte to fix the access.
             *
             * See the comments in kvm_arch_commit_memory_region().
             */
            if (sp->role.level > PG_LEVEL_4K)
                break;
        }

获取脏页

流程图

在这里插入图片描述

具体过程

  • 获取脏页的流程,实际上也分为了两个步骤,第一步是拷贝dirty bitmap,第二步是清零脏页的标志。
  • kvm在早期的实现中,将这两步合在一起,在KVM_GET_DIRTY_LOG ioctl中实现了,由于第二步需要拿mmu_lock锁修改页表,并且不知道哪些页需要清零,因此会清零虚机所有页,而vcpu缺页退出guest态时,kvm处理缺页这个操作需要竞争mmu_lock,长时间持有mmu_lock会影响vcpu的内存访问性能,因此第二步的开销很大。
  • kvm在之后改进了脏页获取流程,引入了KVM_CAP_MANUAL_DIRTY_LOG_PROTECT特性,将脏页获取分成了两个ioctl命令字,KVM_GET_DIRTY_LOGKVM_CLEAR_DIRTY_LOG,对应之前的两个步骤,第一个命令字只负责拷贝dirty bitmap,第二个命令只负责清零脏页,用户态获取到dirty bitmap之后,可以知道哪些页是脏的,因此KVM_CLEAR_DIRTY_LOG可以是虚机所有页集合的子集,在修改页表时效率会更高。
  • 下面分析具体代码:
kvm_vm_ioctl
    case KVM_GET_DIRTY_LOG: 
        kvm_vm_ioctl_get_dirty_log
            kvm_get_dirty_log_protect 	
                slots = __kvm_memslots(kvm, as_id);
                memslot = id_to_memslot(slots, id);
                /* x86架构使用PML机制获取脏页 */
                kvm_arch_sync_dirty_log
                    kvm_x86_ops.flush_log_dirty <=>   vmx_flush_log_dirty
                        /* kick每个vcpu让其vmexit,这样在vmexit的路径上可以更新pml buffer */
                        kvm_flush_pml_buffers
                            kvm_vcpu_kick
                /* dirty bitmap获取到之后,如果使能了KVM_CAP_MANUAL_DIRTY_LOG_PROTECT
                 * 特性,则跳过清零脏页的步骤,直接拷贝dirty bitmap */
              	if (kvm->manual_dirty_log_protect) {
              	    dirty_bitmap_buffer = dirty_bitmap;
              	} else {
              		/* 获取mmu_lock锁,修改页表,将对应位清零 */
					spin_lock(&kvm->mmu_lock);
						kvm_arch_mmu_enable_log_dirty_pt_masked(kvm, memslot,
													offset, mask);

					spin_unlock(&kvm->mmu_lock);
				}
                kvm_arch_flush_remote_tlbs_memslot
                /* 拷贝dirty bitmap到用户态 */
                copy_to_user(log->dirty_bitmap, dirty_bitmap_buffer, n)                   
  • 使用PML机制获取dirty bitmap时,只是将所有vcpu kick了一遍,具体的更新操作在vcpu从guest退出的路径上完成,简单分析下:
vmx_handle_exit
	if (enable_pml)
		vmx_flush_pml_buffer(vcpu);
			/* 遍历PML buffer的entry,将其更新到内存页所属的slot的dirty bitmap */
			for (; pml_idx < PML_ENTITY_NUM; pml_idx++) {
				gpa = pml_buf[pml_idx];
				kvm_vcpu_mark_page_dirty(vcpu, gpa >> PAGE_SHIFT);
					memslot = kvm_vcpu_gfn_to_memslot(vcpu, gfn);
					mark_page_dirty_in_slot(vcpu->kvm, memslot, gfn);
			}
  • 分析一下KVM_CAP_MANUAL_DIRTY_LOG_PROTECT特性下清零脏页的流程:
kvm_vm_ioctl
	case KVM_CLEAR_DIRTY_LOG:
		kvm_vm_ioctl_clear_dirty_log
			kvm_clear_dirty_log_protect
				/* 清零脏页前首先同步之前的脏页信息,使dirty bitmap处于最新状态 */
				kvm_arch_sync_dirty_log
				/* 将最新的脏页信息拷贝到用户态 */
				copy_from_user(dirty_bitmap_buffer, log->dirty_bitmap, n)
				/* 拿mmu_lock锁,准备清零脏页,也就是使能脏页日志 */
				spin_lock(&kvm->mmu_lock)
					kvm_arch_mmu_enable_log_dirty_pt_masked
				spin_unlock(&kvm->mmu_lock)

实验

  • 下面两个commit是基于dirty-bitmap机制脏页同步计算脏页速率的commit:
    https://github.com/qemu/qemu/commit/4998a37e4bf2bc47f76775e6e6a0cd50bacfb16a
    https://github.com/qemu/qemu/commit/826b8bc80cb191557a4ce7cf0e155b436d2d1afa
  • 11
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 9
    评论
评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

享乐主

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

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

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

打赏作者

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

抵扣说明:

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

余额充值