Dirty Ring脏页统计

Dirty Ring原理

  • dirty ring和dirty bitmap都用于实现vcpu访问内存页的统计,相对于dirty bitmap,dirty ring的扩展性较好,因此可以在大内存规格的虚机上开启此特性。dirty bitmap的统计粒度是一个slot,而dirty ring的统计粒度更细,可以精确到一个vcpu甚至一个内存页,并且统计可以在用户空间进行,这几个特性对脏页统计的实现更友好。下面对dirty ring实现原理进行分析。

整体结构

在这里插入图片描述

  • Dirty Ring整体结构如上图所示,每个vcpu维护一个ring,顾名思义ring一旦分配完成,填满了之后可以从头循环。这里ring的实现是一个数组,每个元素用来存放虚机的内存页框号(PFN - Page Frame Number)。当vcpu访问了物理页之后,PML Buffer被硬件填充,kvm会将PML Buffer中记录的GPA信息同步到dirty ring中。因为PML Buffer是per-cpu的,因此dirty ring也能够实现基于cpu粒度的脏页统计。
  • 为了实现脏页统计,dirty ring的entry被定义了三种状态,分别是空状态(empty)、脏状态(dirty)和已搜集状态(collected)。空状态表初始化或无效的状态;脏状态表示kvm将PML Buffer中的信息已经拷贝到dirty ring中,等待用户态查询的状态;已搜集状态表示用户态已经完成查询,等待kvm重新复位dirty ring条目的状态。
    在这里插入图片描述
  • 三种状态转换如下图所示,初始状态dirty ring的entry处于EMPTY状态,当vcpu线程进入guest态开始运行后,如果开启了PML特性,并清零了shadow ept的脏标志,intel硬件在vcpu访问完物理页之后会填充PML Buffer,每次vm exit或者PML Buffer满的时候,kvm会将PML Buffer的内容填充到dirty ring中,每一个虚机物理页对应dirty ring中的一个entry,被填充的entry标记为DIRTY状态,当用户态需要搜集脏页信息时,按序访问dirty ring中的内容,访问完成后将其标记为COLLECTED状态,同时下发KVM_RESET_DIRTY_RINGS命令字通知KVM,kvm发现dirty ring中有entry为COLLECTED状态,会重新将其复位为EMPTY状态,同时清零shadow ept的脏标志,让硬件再次可以填充PML Buffer。

脏页维护原理

在这里插入图片描述

  • 脏页信息的维护原理如上图所示,dirty ring中维护了三个索引,分别用来指向kvm存放脏页entry的位置(dirty index),用户态搜集脏页entry的位置(collect index),以及kvm复位脏页entry的位置。当kvm需要从PML Buffer将GPA内容填入dirty ring时,首先取dirty index,将GPA填入dirty index指向的entry,然后dirty index加1,如果有多个GPA,依次填入dirty ring的entry中,dirty index依次增加;当qemu需要搜集脏页信息时,首先取collect index,将其指向的entry依次读出,然后collect index增加,直到collect index和dirty index的位置相同,说明没有新增的脏页了,用户态搜集脏页完成,将entry标记为COLLECTED态,通过KVM_RESET_DIRTY_RINGS命令字通知kvm复位dirty ring;kvm在合适的时机触发dirty ring的状态复位,首先找到reset index,依次将它之后的entry复位,直到位置和collect index位置相同,说明接下来的entry用户态还没有搜集,不能复位了。

数据结构

用户态

  • CPUState
    CPUState中维护了从内核mmap映射的dirty ring数据结构以及qemu获取dirty ring entry的索引
struct CPUState {
    ......
    struct kvm_dirty_gfn *kvm_dirty_gfns;   /* 与内核共享的dirty ring */
    uint32_t kvm_fetch_index;               /* 搜集脏页时的起始索引,即上面所说的Collect index */
}
  • kvm_dirty_gfn
    dirty ring entry, 通过slot和offset表示虚拟机的GFN,通过flags维护entry的状态机
/*
 * KVM dirty GFN flags, defined as:
 *
 * |---------------+---------------+--------------|
 * | bit 1 (reset) | bit 0 (dirty) | Status       |
 * |---------------+---------------+--------------|
 * |             0 |             0 | Invalid GFN  |
 * |             0 |             1 | Dirty GFN    |
 * |             1 |             X | GFN to reset |
 * |---------------+---------------+--------------|
 *
 * Lifecycle of a dirty GFN goes like:
 *
 *      dirtied         collected        reset
 * 00 -----------> 01 -------------> 1X -------+
 *  ^                                          |
 *  |                                          |
 *  +------------------------------------------+
 */
struct kvm_dirty_gfn {
    __u32 flags;
    __u32 slot;
    __u64 offset;
};

内核态

  • kvm
struct kvm {
	......
	/* dirty ring大小:dirty ring的实现是一个数组,其大小就是数组元素的个数
	 * qemu在ioctl创建虚拟机之后,会查询内核是否具有dirty ring能力,如果有,
	 * 会通过KVM_CAP_DIRTY_LOG_RING命令字携带的参数设置dirty ring的大小
	 */
    u32 dirty_ring_size;	
}
  • kvm_vcpu
struct kvm_vcpu {
	......
	/* vcpu维护的dirty ring结构,针对每个vcpu,记录其最近访问的内存页的页框号 */
    struct kvm_dirty_ring dirty_ring;
}
  • kvm_dirty_ring
    dirty ring核心数据结构,维护dirty ring的元数据以及dirty ring本身,元数据包括脏页push位置索引和已搜集页reset位置索引
struct kvm_dirty_ring {
	/* kvm填入PML Buffer的索引,每填入一个dirty index加1 */
    u32 dirty_index;    
    /* kvm复位entry为empty状态的索引,每复位一个
     * 需要将其对应的shadow ept表项对应的dirty位清零
     */
    u32 reset_index;
    u32 size;
   /* kvm在实现dirty ring时软件定义了一个ring size的上限
    * 它小于等于kvm真实分配的ring的大小,即soft_limit <= size
    * 当dirty ring被填充的entry大于soft_limit时,kvm会抛出
    * KVM_EXIT_DIRTY_RING_FULL异常,让vcpu退出到用户态 
    * 这个机制可以用来保证dirty ring不会真正的被填满 */
    u32 soft_limit;
    /* 与用户态共享的dirty ring */
    struct kvm_dirty_gfn *dirty_gfns;   
    int index;
};

API

配置Dirty Ring

  • 配置dirty ring的主要目的就是设置ring数组的大小。qemu在调用了KVM_CREATE_VM ioctl命令字之后,会立即通过KVM_ENABLE_CAP ioctl命令字使能KVM_CAP_DIRTY_LOG_RING。KVM_ENABLE_CAP的携带的结构体kvm_enable_cap描述了要使能的扩展cap和需要的参数。这里就是KVM_CAP_DIRTY_LOG_RING特性,参数是ring size,放在了kvm_enable_cap结构体args域的第一个元素
1. vm ioctl
/* 虚机vm fd的命令字,用于使能虚机KVM_CAP_DIRTY_LOG_RING能力 */
cmd: KVM_ENABLE_CAP
 
2. parameter
/* for KVM_ENABLE_CAP */
struct kvm_enable_cap {
    /* in */
    __u32 cap;      /* 携带KVM_CAP_DIRTY_LOG_RING */
    __u32 flags;
    __u64 args[4];  /* args[0]存放dirty ring size */
    __u8  pad[64];
};
 
3. dirty ring cap
Capability: KVM_CAP_DIRTY_LOG_RING
Architectures: x86
Parameters: args[0] - size of the dirty log ring

初始化Dirty Ring

  • dirty ring size被设置后,放在kvm结构体中,当qemu下发KVM_CREATE_VCPU ioctl命令字的时候,kvm会检查dirty ring size是否被设置,如果设置了就初始化实现dirty ring的数组
Cmd: KVM_CREATE_VCPU
Capability: basic
Architectures: all
Type: vm ioctl
Parameters: vcpu id (apic id on x86)
Returns: vcpu fd on success, -1 on error

映射Dirty Ring

  • 内核态与用户态共享dirty ring数据结构,该共享区域通过mmap实现,通过vcpu的fd映射固定的偏移(KVM_DIRTY_LOG_PAGE_OFFSET * PAGE_SIZE)。映射长度通过调用KVM_CAP_DIRTY_LOG_RING查看。

复位Dirty Ring

  • qemu搜集到dirty ring的信息之后,除了将entry的状态设置为collected,还需要下发ioctl命令字通知kvm复位dirty ring的entry,让kvm清零页表项的脏位。这时候qemu会通过KVM_RESET_DIRTY_RINGS ioctl命令字通知kvm

流程

  • dirty ring的工作流程如下图所示:
    在这里插入图片描述

初始化

  • dirty ring的初始化工作分为两个部分:配置大小,分配并映射空间。第一部分是检查内核是否支持dirty ring并设置其大小。第二部分是内核每创建了一个vcpu,QEMU映射其dirty ring到自己的进程地址空间。实现dirty ring的共享。
一:配置大小
/* 虚拟机初始化入口 */
kvm_init
    /* 首先通过KVM_CREATE_VM命令字创建一个虚拟机 */
    kvm_ioctl(s, KVM_CREATE_VM, type)
    /* 通过KVM_CHECK_EXTENSION命令字查询kvm是否支持KVM_CAP_DIRTY_LOG_RING的能力 */
    kvm_vm_check_extension(s, KVM_CAP_DIRTY_LOG_RING)
        /******* kvm path start ********/
        kvm_vm_ioctl
        case KVM_CHECK_EXTENSION:
            kvm_vm_ioctl_check_extension_generic(kvm, arg)
            switch (arg)
            case KVM_CAP_DIRTY_LOG_RING:
            /* 内核通过 KVM_DIRTY_LOG_PAGE_OFFSET 宏控制dirty ring相关实现
             * 当 KVM_DIRTY_LOG_PAGE_OFFSET 未定义或者等于0时,表示没有未
             * 实现,反之内核实现了相关功能 */
#if KVM_DIRTY_LOG_PAGE_OFFSET > 0       
                return KVM_DIRTY_RING_MAX_ENTRIES * sizeof(struct kvm_dirty_gfn);
#else
                return 0;
#endif
        /******* kvm path end **********/
    /* 通过KVM_ENABLE_CAP命令将用户配置的dirty ring大小传递给内核进行配置 */
    kvm_vm_enable_cap(s, KVM_CAP_DIRTY_LOG_RING, 0, ring_size)
        /******* kvm path start ********/
        kvm_vm_ioctl
        case KVM_ENABLE_CAP:
            kvm_vm_ioctl_enable_cap_generic(kvm, &cap)
           switch (cap->cap)
            case KVM_CAP_DIRTY_LOG_RING:
                /* 取出QEMU传入的dirty ring大小,让kvm维护起来
                 * 之后每次创建vcpu的时候,据此判断是否初始化dirty ring */
                kvm_vm_ioctl_enable_dirty_log_ring(kvm, cap->args[0])
                    kvm->dirty_ring_size = size
        /******* kvm path start ********/
    /* 如果kvm设置成功,设置QEMU KVMState结构体对应的参数,用于控制QEMU的脏页搜集行为
     * 并区分dirty bitmap。之后在搜集脏页时根据kvm_dirty_ring_enabled标志决定使用
     * dirty binmap还是dirty ring。reaper线程是否初始化也根据此标志判断 */
    s->kvm_dirty_ring_size = ring_size;
    s->kvm_dirty_ring_enabled = true;
 
二:分配并映射空间
/* vcpu初始化入口 */
kvm_init_vcpu
    /* 创建vcpu */
    kvm_get_vcpu
        /* QEMU下发KVM_CREATE_VCPU命令字创建vcpu */
        kvm_vm_ioctl(s, KVM_CREATE_VCPU, (void *)vcpu_id)
            /******* kvm path start ********/
            kvm_vm_ioctl
            case KVM_CREATE_VCPU:
                kvm_vm_ioctl_create_vcpu(kvm, arg)
                    /* kvm创建vcpu */
                    kvm_arch_vcpu_create(vcpu);
                    /* vcpu创建完成后,如果发现dirty ring被使能
                     * 为其分配空间,完成vcpu的dirty ring初始化 */
                    if (kvm->dirty_ring_size)
                        kvm_dirty_ring_alloc(&vcpu->dirty_ring, id, kvm->dirty_ring_size);    
            /******* kvm path end *********/
    /* 获取vcpu可mmap的内存大小 */
    mmap_size = kvm_ioctl(s, KVM_GET_VCPU_MMAP_SIZE, 0)
    /* 映射kvm提供的整个vcpu可映射空间 */
    cpu->kvm_run = mmap(NULL, mmap_size, PROT_READ | PROT_WRITE, MAP_SHARED, cpu->kvm_fd, 0)
    /* 映射dirty ring到QEMU的进程地址空间 */
    cpu->kvm_dirty_gfns = mmap(NULL, s->kvm_dirty_ring_size, PROT_READ | PROT_WRITE, MAP_SHARED,
                     cpu->kvm_fd, PAGE_SIZE * KVM_DIRTY_LOG_PAGE_OFFSET)

脏页记录

  • dirty ring的脏页记录主要由kvm负责,其流程和dirty bitmap实现脏页统计的类似,只在具体脏页统计接口实现上有区别
vmx_handle_exit
    /* 每次vm exit之后,如果使能了pml特性,硬件会在退出前会更新PML buffer
     * 这个时候根据PML buffer中的GPA,标记bitmap
     */
    vmx_flush_pml_buffer
        pml_buf = page_address(vmx->pml_pg);
        for (; pml_idx < PML_ENTITY_NUM; pml_idx++) {
            /* 遍历PML Buffer中的每个条目,根据GPA地址标记对应的bit或者ring entry */
            gpa = pml_buf[pml_idx];
            kvm_vcpu_mark_page_dirty(vcpu, gpa >> PAGE_SHIFT);
                /* 如果虚机开启了dirty ring,使用,反之则用dirty bitmap */
                if (kvm->dirty_ring_size)
                    kvm_dirty_ring_push(kvm_dirty_ring_get(kvm), slot, rel_gfn)
                else
                    set_bit_le(rel_gfn, memslot->dirty_bitmap);
        }
        
/* 脏页统计实现 */
kvm_dirty_ring_push
    /* 根据dirty index取出填入gfn的目的ring entry */
    entry = &ring->dirty_gfns[ring->dirty_index & (ring->size - 1)]
    entry->slot = slot;              /* 填入gfn所在的 slot */
    entry->offset = offset;          /* 填入gfn所在的 slot的偏移 */
    /* 将entry状态设置为脏 */
    kvm_dirty_gfn_set_dirtied(entry);
        gfn->flags = KVM_DIRTY_GFN_F_DIRTY
        
/* 进入guest态的入口 */
vcpu_enter_guest
	/* 如果dirty ring使能,检查dirty ring,计算已经使用的entry的总和
	 * 实际上就是处于COLLECTED+DIRTY状态总和,如果大于了soft_limit
	 * 表示已经达到了软件定义的上限需要标记KVM_EXIT_DIRTY_RING_FULL */
	if (unlikely(vcpu->kvm->dirty_ring_size &&
		kvm_dirty_ring_soft_full(&vcpu->dirty_ring))) {
		vcpu->run->exit_reason = KVM_EXIT_DIRTY_RING_FULL;
	}

脏页搜集

  • 使用dirty ring机制实现的脏页统计,它的脏页搜集在QEMU中直接可以做,不需要通过ioctl命令字通知内核。QEMU的脏页搜集核心函数是kvm_dirty_ring_reap。有三种场景下需要调用reap函数。首先是普通的搜集场景。其次是kvm上报异常,dirty ring满的时候,需要搜集脏页,然后让kvm重置dirty ring中的表项。还有一种场景是需要刷新dirty ring的时候,这时kvm首先会kick所有vcpu让它们退出guest态,kvm在处理异常退出时将PML buffer中的内容同步到dirty ring中,这样用户态的QEMU也能获取最新的dirty ring信息。同时,QEMU还为搜集内存脏页专门创建了一个名为kvm-reaper的线程。
1. 普通场景
/* 虚机初始化入口 */
kvm_init
    /* 如果使能了dirty ring,创建kvm-reaper线程
     * 搜集dirty ring记录的脏页信息 */
    if (s->kvm_dirty_ring_enabled)
        kvm_dirty_ring_reaper_init
            kvm_dirty_ring_reaper_thread
                kvm_dirty_ring_reap
                    kvm_dirty_ring_reap_locked
                        kvm_dirty_ring_reap_one
2. dirty ring满的时候
kvm_cpu_exec
    switch (run->exit_reason)
    case KVM_EXIT_DIRTY_RING_FULL:
        kvm_dirty_ring_reap
3. 脏页信息同步时候
kvm_memory_listener_register
    kml->listener.log_sync_global = kvm_log_sync_global
        kvm_log_sync_global
            kvm_dirty_ring_flush
                kvm_dirty_ring_reap

状态复位

  • 一旦用户态搜集动作完成,需要下发ioctl通知kvm将dirty ring entry复位,以便下一次迭代。因此这个动作时在脏页搜集之后立马做的。kvm解析KVM_RESET_DIRTY_RINGS命令字之后,会做两个核心动作,一是将dirty ring的entry状态设置成invalid,另一个时更新shadow ept页表,将每个entry对应页表项的脏位清零。这样cpu在开启的PML特性之后,会在访问页表项对应物理页之后,硬件会标脏表项中对应的脏位
用户态:
kvm_dirty_ring_reap
    kvm_dirty_ring_reap_locked
        kvm_vm_ioctl(s, KVM_RESET_DIRTY_RINGS)
内核态:
kvm_vm_ioctl
    case KVM_RESET_DIRTY_RINGS:
    kvm_vm_ioctl_reset_dirty_pages
        kvm_dirty_ring_reset
            /* 将dirty ring的entry使无效 */
            kvm_dirty_gfn_set_invalid
                gfn->flags = 0;
            kvm_reset_dirty_gfn
            /* 清零gfn对应的页表项中的脏位 */
                kvm_arch_mmu_enable_log_dirty_pt_masked
  • 3
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

享乐主

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

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

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

打赏作者

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

抵扣说明:

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

余额充值