searchusermenu
  • 发布文章
  • 消息中心
点赞
收藏
评论
分享
原创

QEMU同步脏页原理

2023-10-13 07:12:08
58
0

数据结构

qemu脏页记录涉及到三种数据结构的位图,分别是:

  1. 查询位图:下发位图查询的ioctl命令到内核,该位图由qemu分配内存,kvm填充,是临时的,这里简称查询位图。
  2. 全局位图:位图查询到之后,有一个全局变量专用于保存此位图,这里简称存储位图。该位图描述了qemu进程分给虚机使用的RAM内存的脏页情况,即虚机内存在`RAM Address space`地址空间的脏页情况,这是跟踪虚机内存脏页的核心数据结构。有三种场景需要使用,跟踪虚机显存变化、跟踪迁移过程虚机内存变化、TCG模式下跟踪内存变化。qemu初始设计时将一个内存页状态用一个字节来表示,其中的3位分别用来表示这三种场景下内存的状态。这个设计的实现有诸多问题,后面就改进成用三个独立的位图来跟踪三种场景下虚机内存的变化。本文关注的,是迁移这个场景下用于跟踪虚机内存变化的位图。
  3. RAMBlock位图:主机上跟踪qemu进程分配给虚机的内存的位图,该内存虚机可以读写,用于跟踪虚机对内存的写操作,如果有写操作内存页会变脏,需要重新拷贝。qemu在迁移内存的时候,依据这个位图判断是否拷贝内存页。

以下分别介绍这三类位图的数据结构:

- 查询位图涉及两个数据结构,一个是内核KVM_GET_DIRTY_LOG ioctl命令字接收的参数`kvm_dirty_log` ,该数据结构由qemu分配,作为ioctl命令字的参数传入到kvm,由kvm填充后返回。该数据结构是临时使用,但命令返回的位图,qemu会缓存起来,方便后面的引用。另外一个数据结构就是缓存从kvm得到的位图,由于位图查询的单元是一个slot,因此`KVMSlot`就作为缓存位图的数据结构。

```

/* for KVM_GET_DIRTY_LOG */

struct kvm_dirty_log {

        __u32 slot; /* 要查询的内存区域所在的slot id */

        __u32 padding1;

        union { /* slot区域包含的所有内存的状态,每个位表示一个内存页 */

                void *dirty_bitmap; /* one bit per page */

                __u64 padding2;

        };

};

```

- qemu启动时需要为虚机分配内存空间,首先通过mmap向内核申请内存,然后以slot为单位向kvm注册这段内存区域,因此可以说,qemu与kvm打交道,如果内存相关的操作,都以slot为单位进行。比如qemu注册内存操作,首先从kvm查询空闲slot,然后将内存起止区间填入,向内核注册;再比如qemu开启脏页统计操作,首先遍历`memory_listeners`上所有`MemoryListener`,找到包含的虚机物理地址区间`MemoryRegionSection`,起止地址都是GPA,然后遍历虚机的所有slot,找到`MemoryRegionSection`对应的slot,最后依然通过slot向内核传递地址区间。

- qemu向kvm下发ioctl查询脏页时,以前的做法是将位图放到`kvm_dirty_log`结构的`dirty_bitmap`域中,然后同步给dirty_memory全局位图,新版本的qemu新增了一个动作,将位图缓存到KVMSlot的dirty_bitmap域中,就是说,新版本qemu kvm_dirty_log中的dirty_bitmap和KVMSlot中的dirty-bitmap值是相同的,这样改动的原因是其它的脏页同步手段也可以往KVMSlot的脏页位图中填入脏页信息,比如Dirty Ring。

typedef struct KVMSlot

{

    hwaddr start_addr; /* slot包含的内存区域的起始物理地址 */

    ram_addr_t memory_size; /* slot包含的内存区域大小 */

    void *ram; /* 指向内存区域的起始虚拟地址,当要拷贝虚机内存内容是需要它 */

    int slot; /* slot在全局slot数组中的索引 */

    ......

    /* Dirty bitmap cache for the slot */

    unsigned long *dirty_bmap; /* 用来缓存向内核查询脏页时返回的位图 */

    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起始地址在ramlist地址空间的偏移 */

} KVMSlot;

查询位图

- 全局的内存位图有三个,除了描述迁移状态的,还有描述显存以及跟踪TCG模式下内存的。分析下面的结构体,迁移的内存位图保存在`ram_list.dirty_memory[DIRTY_MEMORY_MIGRATION ]->block[]`指向的内存空间,block[]数组的每个元素是一个指向整型变量的指针,该指针指向位图所在的内存,位图一共有`DIRTY_MEMORY_BLOCK_SIZE`个比特位,可以描述`DIRTY_MEMORY_BLOCK_SIZE`个内存页的脏状态

/* 全局内存位图,描述三个状态,用三个单独的位图来表示 */

#define DIRTY_MEMORY_VGA       0

#define DIRTY_MEMORY_CODE      1

#define DIRTY_MEMORY_MIGRATION 2

#define DIRTY_MEMORY_NUM       3        /* num of dirty bits */

 

#define DIRTY_MEMORY_BLOCK_SIZE ((ram_addr_t)256 * 1024 * 8)

typedef struct {

......

/* block是一个二维数组,也是指针数组,每个元素指向一个2M大小的内存区域

 * 这段区域全部用来存放位图,如果页的大小是4k,那么一个block[i]指向的位图

 * 空间,可以描述8G大小的内存脏页情况

 * */

    unsigned long *blocks[];

} DirtyMemoryBlocks;

 

typedef struct RAMList {

......

    QLIST_HEAD(, RAMBlock) blocks; /* 所有RAMBlock的链表头 */

    DirtyMemoryBlocks *dirty_memory[DIRTY_MEMORY_NUM]; /* 全局位图 */

} RAMList;

 

extern RAMList ram_list;

- 迁移全局位图数据结构图

 

全局位图

 

RAMBlock位图

struct RAMBlock {

......

    uint8_t *host; /* block在主机上的起始虚拟地址 */

......

    ram_addr_t offset; /* block在ramlist空间的偏移 */

    ram_addr_t used_length; /* block的大小 */

    ram_addr_t max_length;

......

    /* dirty bitmap used during migration */

    unsigned long *bmap; /* 存放该block在迁移所有迭代中的脏页信息位图 */

......

    /*

     * bitmap to track already cleared dirty bitmap.  When the bit is

     * set, it means the corresponding memory chunk needs a log-clear.

     * Set this up to non-NULL to enable the capability to postpone

     * and split clearing of dirty bitmap on the remote node (e.g.,

     * KVM).  The bitmap will be set only when doing global sync.

     *

     * NOTE: this bitmap is different comparing to the other bitmaps

     * in that one bit can represent multiple guest pages (which is

     * decided by the `clear_bmap_shift' variable below).  On

     * destination side, this should always be NULL, and the variable

     * `clear_bmap_shift' is meaningless.

     */

    unsigned long *clear_bmap; /* 引入KVM_CLEAR_DIRTY_LOG特性后的清零位图 */

    uint8_t clear_bmap_shift;

};

全局位图初始化

- 全局位图描述的是qemu为虚拟机分配的可读写内存,也就是可读写内存的元数据,因此它在qemu为虚机分配内存的时候初始化,而分配内存主要有两种场景,一是虚机启动、二是内存热添加,都调用的`ram_block_add`接口从主机映射一段qemu进程的地址空间,分配给虚机,全局位图的初始化也随之进行。

```

/* 添加RAMBlock入口 */

ram_block_add

qemu_anon_ram_alloc /* mmap映射qemu进程匿名文件地址空间,实现内存分配 */

if (new_ram_size > old_ram_size) { /* 如果映射的内存比原来的大,全局的位图也要扩展才能描述完整的内存空间 */

        dirty_memory_extend(old_ram_size, new_ram_size); /* 扩展全局的位图 */

    }

```

- 分析全局位图的扩展函数

```

dirty_memory_extend

/* 根据DIRTY_MEMORY_BLOCK_SIZE表示

 * 数组ramlist.dirty_memory[N->block[ ] 中一个block包含的位图个数

 * 内存大小除以DIRTY_MEMORY_BLOCK_SIZE表示描述内存大小需要多少个block

 * 由上一节知道,一个block可以描述8G的内存空间,如果增加的内存大小超过8G

 * 就需要增加block存放位图,这里计算出描述旧/新的内存空间需要的block数

 * */

    ram_addr_t old_num_blocks = DIV_ROUND_UP(old_ram_size,

                                             DIRTY_MEMORY_BLOCK_SIZE);

    ram_addr_t new_num_blocks = DIV_ROUND_UP(new_ram_size,

                                             DIRTY_MEMORY_BLOCK_SIZE);

/* 检查新旧内存空间,需要的block数是否一样,如果一样

 * 则原来的位图足够描述新增的空间,不需要扩展block

 * 反之,需要扩展block

 * */

    /* Only need to extend if block count increased */

    if (new_num_blocks <= old_num_blocks) {

        return;

    }

/* 针对三种用途的位图,都需要对应扩展,因此遍历 */

    for (i = 0; i < DIRTY_MEMORY_NUM; i++) {

        DirtyMemoryBlocks *old_blocks;

        DirtyMemoryBlocks *new_blocks;

        int j;

    

        old_blocks = qatomic_rcu_read(&ram_list.dirty_memory[i]);

        new_blocks = g_malloc(sizeof(*new_blocks) +

                              sizeof(new_blocks->blocks[0]) * new_num_blocks);

/* 将老的位图的block拷贝 */

        if (old_num_blocks) {

            memcpy(new_blocks->blocks, old_blocks->blocks,

                   old_num_blocks * sizeof(old_blocks->blocks[0]));

        }   

/* 扩展位图空间,按block的粒度分配位图空间 */

        for (j = old_num_blocks; j < new_num_blocks; j++) {

            new_blocks->blocks[j] = bitmap_new(DIRTY_MEMORY_BLOCK_SIZE);

        }

/* 将新分配好的位图设置到全局位图上 */

        qatomic_rcu_set(&ram_list.dirty_memory[i], new_blocks);

 

        if (old_blocks) {

            g_free_rcu(old_blocks, rcu);

        }

    }

同步流程

- 迁移的每轮迭代开始前,会同步脏页的信息,分三步进行,首先从kvm获取一段内存区域的脏页状态,然后存放到全局的dirty_memory位图中,之后在迁移开始前qemu将dirty_memory位图中包含的脏页信息同步到每个RAMBlock的位图中,用于判断RAMBlock中的页是否发送。如下图所示:

查询

- 想要查询脏页,首先得开启脏页记录,这个动作通过`KVM_SET_USER_MEMORY_REGION` ioctl完成,KVM流程如下图所示:

 

- 开启脏页记录后,KVM在每次虚机退出时,检查pml buffer是否有新增脏页,如果有,将其同步到memslot的脏页位图中,方便qemu在下一次访问时从memslot中获取脏页信息,其流程如下图所示:

1. dirty bitmap

- 我们重点分析qemu侧获取脏页信息后怎么处理,首先分析qemu的获取位图的流程,这里存在两种方式,一种是传统的dirty bitmap,另一种是dirty ring,这两种方式不能同时存在,但共同的目标都是将虚机的脏页位图保存到KVMSlot结构体的dirty_bmap中,首先分析dirty bitmap的流程:

/* kvm脏页同步实现入口 */

kvm_log_sync

kvm_physical_sync_dirty_bitmap

kvm_slot_get_dirty_log /* 从kvm获取脏页信息 */

/* 设置kvm填充脏页信息的位图dirty_bmap指向KVMSlot->dirty_bmap */

d.dirty_bitmap = slot->dirty_bmap;

/* 要查询脏页位图的slot由KVMSlot->slot指定 */

     d.slot = slot->slot | (slot->as_id << 16);

     /* 调用KVM_GET_DIRTY_LOG ioctl命令字查询脏页位图,命令返回后结果缓存到KVMSlot中 */

     kvm_vm_ioctl(s, KVM_GET_DIRTY_LOG, &d);

- 如下图所示,通过`KVM_GET_DIRTY_LOG`查询到的脏页信息保存在dirty_bmap中,它能够反映整个slot的脏页情况,干净的页和脏页都有记录。

2. dirty ring

- dirty ring是另一种查询脏页信息的方式,它的目标和dirty bitmap一致,都是填充slot中的dirty_bmap位图,但dirty ring并非一次性将slot中的dirty_bmap填充,而是通过kvm-reaper线程周期性的填充多次,每次填充一个bit位,流程如下:

/* kvm-reaper线程 */

kvm_dirty_ring_reaper_thread

/* 周期性检查每个vcpu的dirty ring上是否由脏页产生

 * 如果有,将slot的dirty_bmap中对应的bit位置位,标记为脏 */

while (true) {

sleep(1);

kvm_dirty_ring_reap

kvm_dirty_ring_reap_locked

/* 遍历每个vcpu,获取新增的脏页条目 */

CPU_FOREACH(cpu) {

         total += kvm_dirty_ring_reap_one(s, cpu);

     }

}

- 分析每个vcpu上的脏页条目获取流程:

kvm_dirty_ring_reap_one

/* 取出vcpu上维护的和kvm通过mmap共享的dirty ring */

struct kvm_dirty_gfn *dirty_gfns = cpu->kvm_dirty_gfns

/* 取出环的大小 */

ring_size = s->kvm_dirty_ring_size

/* 取出上一次查询在环中取脏页条目的位置,从上次的位置继续取脏页条目 */

fetch = cpu->kvm_fetch_index

/* 遍历环上的条目直到没有新增脏页条目 */

while (true) {

/* 取出条目 */

cur = &dirty_gfns[fetch % ring_size]

/* 判断该条目中表示的页是否为脏,如果不为脏,表示没有新增的脏页,停止循环 */

if (!dirty_gfn_is_dirtied(cur)) {

            break;

        }

        /* 如果判断有脏页条目,将slot的位图中对应的bit置位 */

        kvm_dirty_ring_mark_page

         /* 从KVMState中找到对应的slot */

         kml = s->as[as_id].ml;

     mem = &kml->slots[slot_id];

     /* 将slot的dirty_bmap中对应的bit置位*/

     set_bit(offset, mem->dirty_bmap);

- 从上面的流程可以看到,dirty ring获取脏页信息时,不是一次性获取整个slot的位图信息,而是周期性查询,每次更新一个页,最终填满整个dirty_bmap位图,如下图所示:

 

- 使能dirty ring之后,原有的kvm_log_sync函数,`kvm_slot_get_dirty_log`将不在有效,它调用到`KVM_GET_DIRTY_LOG` ioctl命令查询时,KVM会检查是否与dirty ring冲突,如果冲突,则会返回`ENXIO`,如下:

kvm_vm_ioctl

case KVM_GET_DIRTY_LOG:

kvm_vm_ioctl_get_dirty_log

kvm_get_dirty_log_protect

/* Dirty ring tracking is exclusive to dirty log tracking */

if (kvm->dirty_ring_size)

return -ENXIO;

保存

- 从KVM获取到脏页信息存放KVMSlot的dirty_bmap之后,这只是个临时存放位图的结构,紧接着会将其保存到全局的dirty_memory结构中,流程如下:

```

kvm_log_sync

kvm_physical_sync_dirty_bitmap

/* 从内核获取脏页信息 */

kvm_slot_get_dirty_log

/* 将脏页信息保存到ram_list.dirty_memory[]中  */

kvm_slot_sync_dirty_pages

```

- 详细分析脏页信息保存的原理和位图设置:

```

kvm_slot_sync_dirty_pages

/* 取出slot在ram_addr_t空间的偏移,dirty_memory描述的是整个虚机

 * ram_addr_t地址空间的脏页情况,因此需要根据ram_start_offset

 * 计算slot起始地址在dirty_memory脏页位图中的第几位

 * */

    ram_addr_t start = slot->ram_start_offset;

    /* slot包含的内存大小,除以页大小,得到slot包含的内存页数 */

    ram_addr_t pages = slot->memory_size / qemu_real_host_page_size;

    /* 输入slot的起始偏移和包含的内存页个数,以及slot的脏页信息

     * 将其保存到dirty_memory中

     * */

    cpu_physical_memory_set_dirty_lebitmap(slot->dirty_bmap, start, pages);

```

- 分析保存脏页信息到位图的详细过程,如下图所示,qemu设计了`ram_list.dirty_memory[]`用于存放一个虚机的在不同场景下的内存脏页的位图,第一个是VGA、第二个是CODE、第三个是MIGRATION,统一由dirty_memory的数组维护,对于迁移过程中脏页的保存,核心动作就是slot中的脏页位图保存到dirty_memory[DIRTY_MEMORY_MIGRATION]的位图上。

- slot是dirty_memory[DIRTY_MEMORY_MIGRATION]位图的一个子集,需要根据`slot.ram_start_offset`找到它在整个MIGRATION位图的位置,而且dirty_memory位图是以block为单位的,每个block包含了`DIRTY_MEMORY_BLOCK_SIZE`个bit位,因此`slot.ram_start_offset`除了用于计算slot第一个页对应bit在dirty_memory中的偏移,还用来计算第一个页在第几个block以及block中的偏移。

 

- 保存位图到dirty bits的过程由`cpu_physical_memory_set_dirty_lebitmap`函数实现,该函数主要针对两种情况进行处理:

  1. 如果要保存的位图`bitmap`,其起始地址`start`在`dirty bits`地址区间是long对齐的,那么保存位图的方式比较简单,就是按照`long`长度逐一取出位图,保存到`dirty bits`中。
  2. 如果要保存的位图`bitmap`,其起始地址`start`在`dirty bits`地址区间不是long对齐的,从`bitmap`中取出位图的方式相同,都是按照`long`长度取出位图,但保存位图到`dirty bits`时,需要逐一判断`long`位图,取出其中的1bit,然后将`dirty bits`中对应的bit位置1。

static inline void cpu_physical_memory_set_dirty_lebitmap(unsigned long *bitmap,

                                                          ram_addr_t start,

                                                          ram_addr_t pages)

{

    unsigned long i, j;

    unsigned long page_number, c;

    hwaddr addr;

    ram_addr_t ram_addr;

    /* 计算要保存pages个页位图,需要多少个long型

     * HOST_LONG_BITS表示一个long型的变量包含的比特位数

     * 如果long的长度是64位,则包含64bit,每个bit可以描述一个页的脏状态

     * 一个long可以表示64个页的脏状态,以下操作将slot包含的内存页数

     * 向上取整并对齐64,得到的len就是64对齐的。

     * 可以由len/64个long变量来存放pages个页需要的位图

     **/

    unsigned long len = (pages + HOST_LONG_BITS - 1) / HOST_LONG_BITS;

    unsigned long hpratio = qemu_real_host_page_size / TARGET_PAGE_SIZE;

    /* 根据slot的起始地址start,计算slot的第一个页在dirty_memory中对应的bit位

 * 需要由多少个long型位图表示,page代表long型的位图个数

 * */

    unsigned long page = BIT_WORD(start >> TARGET_PAGE_BITS);

/* 如果slot的起始地址对应的bit位,恰好是long对齐的

 * 那么就可以直接通过拷贝long型位图到dirty_memory,以此保存位图

 * */

    /* start address is aligned at the start of a word? */

    if ((((page * BITS_PER_LONG) << TARGET_PAGE_BITS) == start) &&

        (hpratio == 1)) {

        unsigned long **blocks[DIRTY_MEMORY_NUM];

        unsigned long idx;

        unsigned long offset;

        long k;

        /* 计算要保存的pages位图可以由多少个long型位图表示 */

        long nr = BITS_TO_LONGS(pages);

/* dirty_memory[]中的一个block可以表示DIRTY_MEMORY_BLOCK_SIZE个

 * bit位,(start >> TARGET_PAGE_BITS)表示slot包含的内存第一个页对应bit位

 * 如果超过一个block可以表示的bit范围,需要存放到下一个

 *  (start >> TARGET_PAGE_BITS) / DIRTY_MEMORY_BLOCK_SIZE表示

 * slot包含的内存第一个页对应bit位落在第几个block位图中 */

        idx = (start >> TARGET_PAGE_BITS) / DIRTY_MEMORY_BLOCK_SIZE;

        /* offset表示slot包含的内存第一个页的位在block位图中的偏移 */

        offset = BIT_WORD((start >> TARGET_PAGE_BITS) %

                          DIRTY_MEMORY_BLOCK_SIZE);

 

        WITH_RCU_READ_LOCK_GUARD() {

         /* 取出dirty_memory中的三个位图,保存到block临时变量中 */

            for (i = 0; i < DIRTY_MEMORY_NUM; i++) {

                blocks[i] =

                    qatomic_rcu_read(&ram_list.dirty_memory[i])->blocks;

            }

     /*

        * 在slot的起始页bit位是long对齐的情况下,只需要每次拷贝一个long型长度的位图

        * nr表示slot包含的内存可以占用多少个long型的位图

        * 逐个从slot的dirty_bmap中取出保存到dirty_memory中

        **/

            for (k = 0; k < nr; k++) {

                if (bitmap[k]) {

                    unsigned long temp = leul_to_cpu(bitmap[k]);

 

                    qatomic_or(&blocks[DIRTY_MEMORY_VGA][idx][offset], temp);

 

                    if (global_dirty_tracking) {

                        qatomic_or(

                                &blocks[DIRTY_MEMORY_MIGRATION][idx][offset],

                                temp);

                    }

                }

 

                if (++offset >= BITS_TO_LONGS(DIRTY_MEMORY_BLOCK_SIZE)) {

                    offset = 0;

                    idx++;

                }

            }

        }

    } else {

     /* 如果start地址在dirty bits位图中不是long对齐的 */

        if (!global_dirty_tracking) {

            clients &= ~(1 << DIRTY_MEMORY_MIGRATION);

        }

 

        /*

         * bitmap-traveling is faster than memory-traveling (for addr...)

         * especially when most of the memory is not dirty.

         */

        for (i = 0; i < len; i++) {

            if (bitmap[i] != 0) {

                /* 从bitmap中按照long长度逐一取出位图 */

                c = leul_to_cpu(bitmap[i]);

                do {

                 /* 从c的低位开始统计0的个数,直到遇到1

                  * 即从c的右边开始数0的位数,直到遇到1

                  * */

                    j = ctzl(c);

                    /* 获取到1的位置j后,将其从c中清除,方便下一次统计 */

                    c &= ~(1ul << j);

                    /* 根据j的位置算出在dirty bits位图的位置,将其置位 */

                    page_number = (i * HOST_LONG_BITS + j) * hpratio;

                    addr = page_number * TARGET_PAGE_SIZE;

                    ram_addr = start + addr;

                    cpu_physical_memory_set_dirty_range(ram_addr,

                                       TARGET_PAGE_SIZE * hpratio, clients);

                } while (c != 0);

            }

        }

    }

}

- 分析保存动作是或操作而不是直接赋值的原因,dirty_memory中的bit位有两种状态:

  1. 置位:表示该脏页qemu虽然查询到了,但是还没有更新到RAMBlock的位图中,只有从dirty_memory读取信息到RAMBlock的bmap后,dirty_memory中的脏页位才会清零,详细分析见下一小节。因此原来的页是脏的,如果新一轮查询是干净的,不能直接将其清零,因为qemu还没有使用这个信息(没有发送脏页),需要保留脏状态,直到qemu使用后(将这个状态保存到RAMBlock的位图中),才能清零,因此需要进行或操作。如果新一轮查询是脏的,那么正好,这一轮和上一轮查询结果一样,都是脏页的,将页设置为脏,虽然上一轮查询的信息qemu没有用到(也没有发送脏页),但本轮和上一轮结果一样,可以直接使用这一轮的结果。这样进行或操作的目的,是保证无论原来的页是脏的还是干净的,经过本轮的查询,上一轮应该发送的页仍然不会被漏掉,只是会推迟到本轮发送。
  2. 清零:两种情况会出现dirty_memory中bit清零,第一种是上轮查询到该位对应页是干净的,第二种是上轮查询到该位对应页是脏的,但qemu已经发送出去了,两种情况下,如果本轮查询到页状态是干净的,都应该置位,表示页变脏页了。因此同样需要做或操作。

- 总结一下,由于bit位为1表示脏页,反之表示干净页,或操作的本质是将本轮查询的脏页保存到dirty_memory中,但对于dirty_memory中原来存在的脏页信息,需要保留下来。

RAMBlock同步

- 脏页位图信息被保存到`ram_list.dirty_memory`之后,上面介绍到可以用在三个场合,这里分析在迁移时候怎么用这个信息,迁移有三个地方用到了`ram_list.dirty_memory`保存的位图,将位图信息取出,同步到RAMBlock的bmap中,分别是:

  1. 迁移开始前,脏页日志记录打开之后
  2. 每轮迁移迭代开始前
  3. 迁移进入最后一轮迭代前

- 同步脏页位图的流程如下:

```

migration_bitmap_sync_precopy

migration_bitmap_sync

ramblock_sync_dirty_bitmap

cpu_physical_memory_sync_dirty_bitmap

```

- 同步的核心目的是将dirty_memory中的位图信息同步到RAMBlock的位图中,同时将dirty_memory中的位清零,如下图所示:

- 分析同步位图到RAMBlock的bmap细节:

/* 函数的参数用于保存从dirty_memory同步的位图信息

 * rb表示要同步的RAMBlock,start表示从RAMBlock区间的什么位置开始同步

 * 通常是0,表示从RAMBlock的区间开始处,length表示要同步到的RAMBlock的长度 */

cpu_physical_memory_sync_dirty_bitmap(RAMBlock *rb, ram_addr_t start, ram_addr_t length)

/* start + rb->offset 表示RAMBlock开始同步的页在ram address space的位置,长度是字节

 * (start + rb->offset) >> TARGET_PAGE_BITS 表示RAMBlock开始同步的页距离ram address space起始位置多少个页

 * BIT_WORD((start + rb->offset) >> TARGET_PAGE_BITS)表示,将页用位图表示后可以占用多少个long型的位图

 * 因此word表示RAMBlock开始同步的页距ram address space起始位置有多少个long型的位图

 * */                                         

unsigned long word = BIT_WORD((start + rb->offset) >> TARGET_PAGE_BITS);

/* 取出要同步到RAMBlock的位图 */

unsigned long *dest = rb->bmap;

/* 如果要同步的RAMBlock的起始位置对应页的bit恰好是long位图对齐的,那么可以直接将dirty_memory中位图按照

 * long型位图的长度依次拷贝到bmap中,这里就是判断是否属于这种情况 */

    if (((word * BITS_PER_LONG) << TARGET_PAGE_BITS) ==  (start + rb->offset))) {

     /* 计算要拷贝多少个long型的位图 */

int nr = BITS_TO_LONGS(length >> TARGET_PAGE_BITS);

/* 计算开始拷贝的页的位图在第几个block */

unsigned long idx = (word * BITS_PER_LONG) / DIRTY_MEMORY_BLOCK_SIZE;

/* 计算开始拷贝的页的位图在block中的偏移 */

unsigned long offset = BIT_WORD((word * BITS_PER_LONG) % DIRTY_MEMORY_BLOCK_SIZE);

/* 因为允许从RAMBlock位图的任意位置开始同步

 * 因此计算起始同步的位置对应的位距RAMBlock起始区间有多少个long型位图

 * 如果start为0,page也是0,那么表示从RAMBlock起始区间开始同步位图

 **/

       unsigned long page = BIT_WORD(start >> TARGET_PAGE_BITS);

       /* 取出dirty_memory的位图信息 */

       src = qatomic_rcu_read(&ram_list.dirty_memory[DIRTY_MEMORY_MIGRATION])->blocks;

       /* 从page开始拷贝long型位图到RAMBlock的bmap位图上,拷贝nr个long型位图 */

       for (k = page; k < page + nr; k++) {

            if (src[idx][offset]) {

             /* 将dirty_memory中的位图读取到bits中,完成后将dirty_memory对应的区域清零 */

                unsigned long bits = qatomic_xchg(&src[idx][offset], 0);

                unsigned long new_dirty;

                /* 保存bmap中原来干净脏页位,这里用于计算新增的脏页数,并非用于同步操作 */

                new_dirty = ~dest[k];

                /* 核心的同步操作,将从dirty_memory中取到的位图信息保存到RAMBlock的bmap中

                 * dest指向的bmap位图,这里同样是或操作,对于原来是脏页的位,bmap中保持不变

                 * 对于原来是干净的位,如果dirty_memory中为脏,则结果为脏,同步到bmap中 */

                dest[k] |= bits;

                /* 计算新增的脏页数 */

                new_dirty &= bits;

                /* 累加新增的脏页数 */

                num_dirty += ctpopl(new_dirty);

            }

        }                               

}        

 

发送脏页

- 脏页位图dirty_bitmap同步到RAMBlock->bmap中之后,迁移线程依据此信息查找RAMBlock中所有脏页,然后发送,下面介绍迁移线程从位图查脏页的原理,流程图如下:

- 迁移线程根据RAMBlock中的bmap信息,查寻脏页,如果有脏页,在发送之前将位图清零,每发送一个页就清零对应脏位,如下图所示:

- 具体发送流程如下:

/* 开始查找并发送RAMBlock里面的脏页 */

ram_find_and_save_block

/* 从给定的RAMBlock的位图中查找下一个被置位的 bit,找到第一个脏页对应位就停止 */

find_dirty_block

migration_bitmap_find_dirty

find_next_bit

/* 如果RAMBlock中有脏页,开始查找并发送 */

ram_save_host_page

/* 遍历位图中的每个bit,检查是否置位,如果置位,完成两个事情

 * 1. 如果使能了KVM_CLEAR_DIRTY_LOG特性,脏页查询中的重保护动作将从脏页查询的系统调用时段推迟到用户态发送脏页时进行

 *    因此这里需要检查是否开启此特性,如果开启,调用kvm_log_clear回调重保护kvm中的页表

 * 2. 如果bit被设置成1,将其清零,因为后面即将发送脏页,因此在发送前将其清零,表示该脏页已经发送。同时,将迁移的剩余脏页数减1,更新统计信息方便计算脏页速率和判断是否进入最后一轮迭代

 * */

migration_bitmap_clear_dirty

/* 执行上面介绍的第一个事情,发送系统调用重保护kvm中的页表 */

memory_region_clear_dirty_bitmap

/* 如果bit被置位,将其清零 */

test_and_clear_bit

/* 更新统计信息 */

rs->migration_dirty_pages--

0条评论
作者已关闭评论
小梅
19文章数
0粉丝数
小梅
19 文章 | 0 粉丝
原创

QEMU同步脏页原理

2023-10-13 07:12:08
58
0

数据结构

qemu脏页记录涉及到三种数据结构的位图,分别是:

  1. 查询位图:下发位图查询的ioctl命令到内核,该位图由qemu分配内存,kvm填充,是临时的,这里简称查询位图。
  2. 全局位图:位图查询到之后,有一个全局变量专用于保存此位图,这里简称存储位图。该位图描述了qemu进程分给虚机使用的RAM内存的脏页情况,即虚机内存在`RAM Address space`地址空间的脏页情况,这是跟踪虚机内存脏页的核心数据结构。有三种场景需要使用,跟踪虚机显存变化、跟踪迁移过程虚机内存变化、TCG模式下跟踪内存变化。qemu初始设计时将一个内存页状态用一个字节来表示,其中的3位分别用来表示这三种场景下内存的状态。这个设计的实现有诸多问题,后面就改进成用三个独立的位图来跟踪三种场景下虚机内存的变化。本文关注的,是迁移这个场景下用于跟踪虚机内存变化的位图。
  3. RAMBlock位图:主机上跟踪qemu进程分配给虚机的内存的位图,该内存虚机可以读写,用于跟踪虚机对内存的写操作,如果有写操作内存页会变脏,需要重新拷贝。qemu在迁移内存的时候,依据这个位图判断是否拷贝内存页。

以下分别介绍这三类位图的数据结构:

- 查询位图涉及两个数据结构,一个是内核KVM_GET_DIRTY_LOG ioctl命令字接收的参数`kvm_dirty_log` ,该数据结构由qemu分配,作为ioctl命令字的参数传入到kvm,由kvm填充后返回。该数据结构是临时使用,但命令返回的位图,qemu会缓存起来,方便后面的引用。另外一个数据结构就是缓存从kvm得到的位图,由于位图查询的单元是一个slot,因此`KVMSlot`就作为缓存位图的数据结构。

```

/* for KVM_GET_DIRTY_LOG */

struct kvm_dirty_log {

        __u32 slot; /* 要查询的内存区域所在的slot id */

        __u32 padding1;

        union { /* slot区域包含的所有内存的状态,每个位表示一个内存页 */

                void *dirty_bitmap; /* one bit per page */

                __u64 padding2;

        };

};

```

- qemu启动时需要为虚机分配内存空间,首先通过mmap向内核申请内存,然后以slot为单位向kvm注册这段内存区域,因此可以说,qemu与kvm打交道,如果内存相关的操作,都以slot为单位进行。比如qemu注册内存操作,首先从kvm查询空闲slot,然后将内存起止区间填入,向内核注册;再比如qemu开启脏页统计操作,首先遍历`memory_listeners`上所有`MemoryListener`,找到包含的虚机物理地址区间`MemoryRegionSection`,起止地址都是GPA,然后遍历虚机的所有slot,找到`MemoryRegionSection`对应的slot,最后依然通过slot向内核传递地址区间。

- qemu向kvm下发ioctl查询脏页时,以前的做法是将位图放到`kvm_dirty_log`结构的`dirty_bitmap`域中,然后同步给dirty_memory全局位图,新版本的qemu新增了一个动作,将位图缓存到KVMSlot的dirty_bitmap域中,就是说,新版本qemu kvm_dirty_log中的dirty_bitmap和KVMSlot中的dirty-bitmap值是相同的,这样改动的原因是其它的脏页同步手段也可以往KVMSlot的脏页位图中填入脏页信息,比如Dirty Ring。

typedef struct KVMSlot

{

    hwaddr start_addr; /* slot包含的内存区域的起始物理地址 */

    ram_addr_t memory_size; /* slot包含的内存区域大小 */

    void *ram; /* 指向内存区域的起始虚拟地址,当要拷贝虚机内存内容是需要它 */

    int slot; /* slot在全局slot数组中的索引 */

    ......

    /* Dirty bitmap cache for the slot */

    unsigned long *dirty_bmap; /* 用来缓存向内核查询脏页时返回的位图 */

    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起始地址在ramlist地址空间的偏移 */

} KVMSlot;

查询位图

- 全局的内存位图有三个,除了描述迁移状态的,还有描述显存以及跟踪TCG模式下内存的。分析下面的结构体,迁移的内存位图保存在`ram_list.dirty_memory[DIRTY_MEMORY_MIGRATION ]->block[]`指向的内存空间,block[]数组的每个元素是一个指向整型变量的指针,该指针指向位图所在的内存,位图一共有`DIRTY_MEMORY_BLOCK_SIZE`个比特位,可以描述`DIRTY_MEMORY_BLOCK_SIZE`个内存页的脏状态

/* 全局内存位图,描述三个状态,用三个单独的位图来表示 */

#define DIRTY_MEMORY_VGA       0

#define DIRTY_MEMORY_CODE      1

#define DIRTY_MEMORY_MIGRATION 2

#define DIRTY_MEMORY_NUM       3        /* num of dirty bits */

 

#define DIRTY_MEMORY_BLOCK_SIZE ((ram_addr_t)256 * 1024 * 8)

typedef struct {

......

/* block是一个二维数组,也是指针数组,每个元素指向一个2M大小的内存区域

 * 这段区域全部用来存放位图,如果页的大小是4k,那么一个block[i]指向的位图

 * 空间,可以描述8G大小的内存脏页情况

 * */

    unsigned long *blocks[];

} DirtyMemoryBlocks;

 

typedef struct RAMList {

......

    QLIST_HEAD(, RAMBlock) blocks; /* 所有RAMBlock的链表头 */

    DirtyMemoryBlocks *dirty_memory[DIRTY_MEMORY_NUM]; /* 全局位图 */

} RAMList;

 

extern RAMList ram_list;

- 迁移全局位图数据结构图

 

全局位图

 

RAMBlock位图

struct RAMBlock {

......

    uint8_t *host; /* block在主机上的起始虚拟地址 */

......

    ram_addr_t offset; /* block在ramlist空间的偏移 */

    ram_addr_t used_length; /* block的大小 */

    ram_addr_t max_length;

......

    /* dirty bitmap used during migration */

    unsigned long *bmap; /* 存放该block在迁移所有迭代中的脏页信息位图 */

......

    /*

     * bitmap to track already cleared dirty bitmap.  When the bit is

     * set, it means the corresponding memory chunk needs a log-clear.

     * Set this up to non-NULL to enable the capability to postpone

     * and split clearing of dirty bitmap on the remote node (e.g.,

     * KVM).  The bitmap will be set only when doing global sync.

     *

     * NOTE: this bitmap is different comparing to the other bitmaps

     * in that one bit can represent multiple guest pages (which is

     * decided by the `clear_bmap_shift' variable below).  On

     * destination side, this should always be NULL, and the variable

     * `clear_bmap_shift' is meaningless.

     */

    unsigned long *clear_bmap; /* 引入KVM_CLEAR_DIRTY_LOG特性后的清零位图 */

    uint8_t clear_bmap_shift;

};

全局位图初始化

- 全局位图描述的是qemu为虚拟机分配的可读写内存,也就是可读写内存的元数据,因此它在qemu为虚机分配内存的时候初始化,而分配内存主要有两种场景,一是虚机启动、二是内存热添加,都调用的`ram_block_add`接口从主机映射一段qemu进程的地址空间,分配给虚机,全局位图的初始化也随之进行。

```

/* 添加RAMBlock入口 */

ram_block_add

qemu_anon_ram_alloc /* mmap映射qemu进程匿名文件地址空间,实现内存分配 */

if (new_ram_size > old_ram_size) { /* 如果映射的内存比原来的大,全局的位图也要扩展才能描述完整的内存空间 */

        dirty_memory_extend(old_ram_size, new_ram_size); /* 扩展全局的位图 */

    }

```

- 分析全局位图的扩展函数

```

dirty_memory_extend

/* 根据DIRTY_MEMORY_BLOCK_SIZE表示

 * 数组ramlist.dirty_memory[N->block[ ] 中一个block包含的位图个数

 * 内存大小除以DIRTY_MEMORY_BLOCK_SIZE表示描述内存大小需要多少个block

 * 由上一节知道,一个block可以描述8G的内存空间,如果增加的内存大小超过8G

 * 就需要增加block存放位图,这里计算出描述旧/新的内存空间需要的block数

 * */

    ram_addr_t old_num_blocks = DIV_ROUND_UP(old_ram_size,

                                             DIRTY_MEMORY_BLOCK_SIZE);

    ram_addr_t new_num_blocks = DIV_ROUND_UP(new_ram_size,

                                             DIRTY_MEMORY_BLOCK_SIZE);

/* 检查新旧内存空间,需要的block数是否一样,如果一样

 * 则原来的位图足够描述新增的空间,不需要扩展block

 * 反之,需要扩展block

 * */

    /* Only need to extend if block count increased */

    if (new_num_blocks <= old_num_blocks) {

        return;

    }

/* 针对三种用途的位图,都需要对应扩展,因此遍历 */

    for (i = 0; i < DIRTY_MEMORY_NUM; i++) {

        DirtyMemoryBlocks *old_blocks;

        DirtyMemoryBlocks *new_blocks;

        int j;

    

        old_blocks = qatomic_rcu_read(&ram_list.dirty_memory[i]);

        new_blocks = g_malloc(sizeof(*new_blocks) +

                              sizeof(new_blocks->blocks[0]) * new_num_blocks);

/* 将老的位图的block拷贝 */

        if (old_num_blocks) {

            memcpy(new_blocks->blocks, old_blocks->blocks,

                   old_num_blocks * sizeof(old_blocks->blocks[0]));

        }   

/* 扩展位图空间,按block的粒度分配位图空间 */

        for (j = old_num_blocks; j < new_num_blocks; j++) {

            new_blocks->blocks[j] = bitmap_new(DIRTY_MEMORY_BLOCK_SIZE);

        }

/* 将新分配好的位图设置到全局位图上 */

        qatomic_rcu_set(&ram_list.dirty_memory[i], new_blocks);

 

        if (old_blocks) {

            g_free_rcu(old_blocks, rcu);

        }

    }

同步流程

- 迁移的每轮迭代开始前,会同步脏页的信息,分三步进行,首先从kvm获取一段内存区域的脏页状态,然后存放到全局的dirty_memory位图中,之后在迁移开始前qemu将dirty_memory位图中包含的脏页信息同步到每个RAMBlock的位图中,用于判断RAMBlock中的页是否发送。如下图所示:

查询

- 想要查询脏页,首先得开启脏页记录,这个动作通过`KVM_SET_USER_MEMORY_REGION` ioctl完成,KVM流程如下图所示:

 

- 开启脏页记录后,KVM在每次虚机退出时,检查pml buffer是否有新增脏页,如果有,将其同步到memslot的脏页位图中,方便qemu在下一次访问时从memslot中获取脏页信息,其流程如下图所示:

1. dirty bitmap

- 我们重点分析qemu侧获取脏页信息后怎么处理,首先分析qemu的获取位图的流程,这里存在两种方式,一种是传统的dirty bitmap,另一种是dirty ring,这两种方式不能同时存在,但共同的目标都是将虚机的脏页位图保存到KVMSlot结构体的dirty_bmap中,首先分析dirty bitmap的流程:

/* kvm脏页同步实现入口 */

kvm_log_sync

kvm_physical_sync_dirty_bitmap

kvm_slot_get_dirty_log /* 从kvm获取脏页信息 */

/* 设置kvm填充脏页信息的位图dirty_bmap指向KVMSlot->dirty_bmap */

d.dirty_bitmap = slot->dirty_bmap;

/* 要查询脏页位图的slot由KVMSlot->slot指定 */

     d.slot = slot->slot | (slot->as_id << 16);

     /* 调用KVM_GET_DIRTY_LOG ioctl命令字查询脏页位图,命令返回后结果缓存到KVMSlot中 */

     kvm_vm_ioctl(s, KVM_GET_DIRTY_LOG, &d);

- 如下图所示,通过`KVM_GET_DIRTY_LOG`查询到的脏页信息保存在dirty_bmap中,它能够反映整个slot的脏页情况,干净的页和脏页都有记录。

2. dirty ring

- dirty ring是另一种查询脏页信息的方式,它的目标和dirty bitmap一致,都是填充slot中的dirty_bmap位图,但dirty ring并非一次性将slot中的dirty_bmap填充,而是通过kvm-reaper线程周期性的填充多次,每次填充一个bit位,流程如下:

/* kvm-reaper线程 */

kvm_dirty_ring_reaper_thread

/* 周期性检查每个vcpu的dirty ring上是否由脏页产生

 * 如果有,将slot的dirty_bmap中对应的bit位置位,标记为脏 */

while (true) {

sleep(1);

kvm_dirty_ring_reap

kvm_dirty_ring_reap_locked

/* 遍历每个vcpu,获取新增的脏页条目 */

CPU_FOREACH(cpu) {

         total += kvm_dirty_ring_reap_one(s, cpu);

     }

}

- 分析每个vcpu上的脏页条目获取流程:

kvm_dirty_ring_reap_one

/* 取出vcpu上维护的和kvm通过mmap共享的dirty ring */

struct kvm_dirty_gfn *dirty_gfns = cpu->kvm_dirty_gfns

/* 取出环的大小 */

ring_size = s->kvm_dirty_ring_size

/* 取出上一次查询在环中取脏页条目的位置,从上次的位置继续取脏页条目 */

fetch = cpu->kvm_fetch_index

/* 遍历环上的条目直到没有新增脏页条目 */

while (true) {

/* 取出条目 */

cur = &dirty_gfns[fetch % ring_size]

/* 判断该条目中表示的页是否为脏,如果不为脏,表示没有新增的脏页,停止循环 */

if (!dirty_gfn_is_dirtied(cur)) {

            break;

        }

        /* 如果判断有脏页条目,将slot的位图中对应的bit置位 */

        kvm_dirty_ring_mark_page

         /* 从KVMState中找到对应的slot */

         kml = s->as[as_id].ml;

     mem = &kml->slots[slot_id];

     /* 将slot的dirty_bmap中对应的bit置位*/

     set_bit(offset, mem->dirty_bmap);

- 从上面的流程可以看到,dirty ring获取脏页信息时,不是一次性获取整个slot的位图信息,而是周期性查询,每次更新一个页,最终填满整个dirty_bmap位图,如下图所示:

 

- 使能dirty ring之后,原有的kvm_log_sync函数,`kvm_slot_get_dirty_log`将不在有效,它调用到`KVM_GET_DIRTY_LOG` ioctl命令查询时,KVM会检查是否与dirty ring冲突,如果冲突,则会返回`ENXIO`,如下:

kvm_vm_ioctl

case KVM_GET_DIRTY_LOG:

kvm_vm_ioctl_get_dirty_log

kvm_get_dirty_log_protect

/* Dirty ring tracking is exclusive to dirty log tracking */

if (kvm->dirty_ring_size)

return -ENXIO;

保存

- 从KVM获取到脏页信息存放KVMSlot的dirty_bmap之后,这只是个临时存放位图的结构,紧接着会将其保存到全局的dirty_memory结构中,流程如下:

```

kvm_log_sync

kvm_physical_sync_dirty_bitmap

/* 从内核获取脏页信息 */

kvm_slot_get_dirty_log

/* 将脏页信息保存到ram_list.dirty_memory[]中  */

kvm_slot_sync_dirty_pages

```

- 详细分析脏页信息保存的原理和位图设置:

```

kvm_slot_sync_dirty_pages

/* 取出slot在ram_addr_t空间的偏移,dirty_memory描述的是整个虚机

 * ram_addr_t地址空间的脏页情况,因此需要根据ram_start_offset

 * 计算slot起始地址在dirty_memory脏页位图中的第几位

 * */

    ram_addr_t start = slot->ram_start_offset;

    /* slot包含的内存大小,除以页大小,得到slot包含的内存页数 */

    ram_addr_t pages = slot->memory_size / qemu_real_host_page_size;

    /* 输入slot的起始偏移和包含的内存页个数,以及slot的脏页信息

     * 将其保存到dirty_memory中

     * */

    cpu_physical_memory_set_dirty_lebitmap(slot->dirty_bmap, start, pages);

```

- 分析保存脏页信息到位图的详细过程,如下图所示,qemu设计了`ram_list.dirty_memory[]`用于存放一个虚机的在不同场景下的内存脏页的位图,第一个是VGA、第二个是CODE、第三个是MIGRATION,统一由dirty_memory的数组维护,对于迁移过程中脏页的保存,核心动作就是slot中的脏页位图保存到dirty_memory[DIRTY_MEMORY_MIGRATION]的位图上。

- slot是dirty_memory[DIRTY_MEMORY_MIGRATION]位图的一个子集,需要根据`slot.ram_start_offset`找到它在整个MIGRATION位图的位置,而且dirty_memory位图是以block为单位的,每个block包含了`DIRTY_MEMORY_BLOCK_SIZE`个bit位,因此`slot.ram_start_offset`除了用于计算slot第一个页对应bit在dirty_memory中的偏移,还用来计算第一个页在第几个block以及block中的偏移。

 

- 保存位图到dirty bits的过程由`cpu_physical_memory_set_dirty_lebitmap`函数实现,该函数主要针对两种情况进行处理:

  1. 如果要保存的位图`bitmap`,其起始地址`start`在`dirty bits`地址区间是long对齐的,那么保存位图的方式比较简单,就是按照`long`长度逐一取出位图,保存到`dirty bits`中。
  2. 如果要保存的位图`bitmap`,其起始地址`start`在`dirty bits`地址区间不是long对齐的,从`bitmap`中取出位图的方式相同,都是按照`long`长度取出位图,但保存位图到`dirty bits`时,需要逐一判断`long`位图,取出其中的1bit,然后将`dirty bits`中对应的bit位置1。

static inline void cpu_physical_memory_set_dirty_lebitmap(unsigned long *bitmap,

                                                          ram_addr_t start,

                                                          ram_addr_t pages)

{

    unsigned long i, j;

    unsigned long page_number, c;

    hwaddr addr;

    ram_addr_t ram_addr;

    /* 计算要保存pages个页位图,需要多少个long型

     * HOST_LONG_BITS表示一个long型的变量包含的比特位数

     * 如果long的长度是64位,则包含64bit,每个bit可以描述一个页的脏状态

     * 一个long可以表示64个页的脏状态,以下操作将slot包含的内存页数

     * 向上取整并对齐64,得到的len就是64对齐的。

     * 可以由len/64个long变量来存放pages个页需要的位图

     **/

    unsigned long len = (pages + HOST_LONG_BITS - 1) / HOST_LONG_BITS;

    unsigned long hpratio = qemu_real_host_page_size / TARGET_PAGE_SIZE;

    /* 根据slot的起始地址start,计算slot的第一个页在dirty_memory中对应的bit位

 * 需要由多少个long型位图表示,page代表long型的位图个数

 * */

    unsigned long page = BIT_WORD(start >> TARGET_PAGE_BITS);

/* 如果slot的起始地址对应的bit位,恰好是long对齐的

 * 那么就可以直接通过拷贝long型位图到dirty_memory,以此保存位图

 * */

    /* start address is aligned at the start of a word? */

    if ((((page * BITS_PER_LONG) << TARGET_PAGE_BITS) == start) &&

        (hpratio == 1)) {

        unsigned long **blocks[DIRTY_MEMORY_NUM];

        unsigned long idx;

        unsigned long offset;

        long k;

        /* 计算要保存的pages位图可以由多少个long型位图表示 */

        long nr = BITS_TO_LONGS(pages);

/* dirty_memory[]中的一个block可以表示DIRTY_MEMORY_BLOCK_SIZE个

 * bit位,(start >> TARGET_PAGE_BITS)表示slot包含的内存第一个页对应bit位

 * 如果超过一个block可以表示的bit范围,需要存放到下一个

 *  (start >> TARGET_PAGE_BITS) / DIRTY_MEMORY_BLOCK_SIZE表示

 * slot包含的内存第一个页对应bit位落在第几个block位图中 */

        idx = (start >> TARGET_PAGE_BITS) / DIRTY_MEMORY_BLOCK_SIZE;

        /* offset表示slot包含的内存第一个页的位在block位图中的偏移 */

        offset = BIT_WORD((start >> TARGET_PAGE_BITS) %

                          DIRTY_MEMORY_BLOCK_SIZE);

 

        WITH_RCU_READ_LOCK_GUARD() {

         /* 取出dirty_memory中的三个位图,保存到block临时变量中 */

            for (i = 0; i < DIRTY_MEMORY_NUM; i++) {

                blocks[i] =

                    qatomic_rcu_read(&ram_list.dirty_memory[i])->blocks;

            }

     /*

        * 在slot的起始页bit位是long对齐的情况下,只需要每次拷贝一个long型长度的位图

        * nr表示slot包含的内存可以占用多少个long型的位图

        * 逐个从slot的dirty_bmap中取出保存到dirty_memory中

        **/

            for (k = 0; k < nr; k++) {

                if (bitmap[k]) {

                    unsigned long temp = leul_to_cpu(bitmap[k]);

 

                    qatomic_or(&blocks[DIRTY_MEMORY_VGA][idx][offset], temp);

 

                    if (global_dirty_tracking) {

                        qatomic_or(

                                &blocks[DIRTY_MEMORY_MIGRATION][idx][offset],

                                temp);

                    }

                }

 

                if (++offset >= BITS_TO_LONGS(DIRTY_MEMORY_BLOCK_SIZE)) {

                    offset = 0;

                    idx++;

                }

            }

        }

    } else {

     /* 如果start地址在dirty bits位图中不是long对齐的 */

        if (!global_dirty_tracking) {

            clients &= ~(1 << DIRTY_MEMORY_MIGRATION);

        }

 

        /*

         * bitmap-traveling is faster than memory-traveling (for addr...)

         * especially when most of the memory is not dirty.

         */

        for (i = 0; i < len; i++) {

            if (bitmap[i] != 0) {

                /* 从bitmap中按照long长度逐一取出位图 */

                c = leul_to_cpu(bitmap[i]);

                do {

                 /* 从c的低位开始统计0的个数,直到遇到1

                  * 即从c的右边开始数0的位数,直到遇到1

                  * */

                    j = ctzl(c);

                    /* 获取到1的位置j后,将其从c中清除,方便下一次统计 */

                    c &= ~(1ul << j);

                    /* 根据j的位置算出在dirty bits位图的位置,将其置位 */

                    page_number = (i * HOST_LONG_BITS + j) * hpratio;

                    addr = page_number * TARGET_PAGE_SIZE;

                    ram_addr = start + addr;

                    cpu_physical_memory_set_dirty_range(ram_addr,

                                       TARGET_PAGE_SIZE * hpratio, clients);

                } while (c != 0);

            }

        }

    }

}

- 分析保存动作是或操作而不是直接赋值的原因,dirty_memory中的bit位有两种状态:

  1. 置位:表示该脏页qemu虽然查询到了,但是还没有更新到RAMBlock的位图中,只有从dirty_memory读取信息到RAMBlock的bmap后,dirty_memory中的脏页位才会清零,详细分析见下一小节。因此原来的页是脏的,如果新一轮查询是干净的,不能直接将其清零,因为qemu还没有使用这个信息(没有发送脏页),需要保留脏状态,直到qemu使用后(将这个状态保存到RAMBlock的位图中),才能清零,因此需要进行或操作。如果新一轮查询是脏的,那么正好,这一轮和上一轮查询结果一样,都是脏页的,将页设置为脏,虽然上一轮查询的信息qemu没有用到(也没有发送脏页),但本轮和上一轮结果一样,可以直接使用这一轮的结果。这样进行或操作的目的,是保证无论原来的页是脏的还是干净的,经过本轮的查询,上一轮应该发送的页仍然不会被漏掉,只是会推迟到本轮发送。
  2. 清零:两种情况会出现dirty_memory中bit清零,第一种是上轮查询到该位对应页是干净的,第二种是上轮查询到该位对应页是脏的,但qemu已经发送出去了,两种情况下,如果本轮查询到页状态是干净的,都应该置位,表示页变脏页了。因此同样需要做或操作。

- 总结一下,由于bit位为1表示脏页,反之表示干净页,或操作的本质是将本轮查询的脏页保存到dirty_memory中,但对于dirty_memory中原来存在的脏页信息,需要保留下来。

RAMBlock同步

- 脏页位图信息被保存到`ram_list.dirty_memory`之后,上面介绍到可以用在三个场合,这里分析在迁移时候怎么用这个信息,迁移有三个地方用到了`ram_list.dirty_memory`保存的位图,将位图信息取出,同步到RAMBlock的bmap中,分别是:

  1. 迁移开始前,脏页日志记录打开之后
  2. 每轮迁移迭代开始前
  3. 迁移进入最后一轮迭代前

- 同步脏页位图的流程如下:

```

migration_bitmap_sync_precopy

migration_bitmap_sync

ramblock_sync_dirty_bitmap

cpu_physical_memory_sync_dirty_bitmap

```

- 同步的核心目的是将dirty_memory中的位图信息同步到RAMBlock的位图中,同时将dirty_memory中的位清零,如下图所示:

- 分析同步位图到RAMBlock的bmap细节:

/* 函数的参数用于保存从dirty_memory同步的位图信息

 * rb表示要同步的RAMBlock,start表示从RAMBlock区间的什么位置开始同步

 * 通常是0,表示从RAMBlock的区间开始处,length表示要同步到的RAMBlock的长度 */

cpu_physical_memory_sync_dirty_bitmap(RAMBlock *rb, ram_addr_t start, ram_addr_t length)

/* start + rb->offset 表示RAMBlock开始同步的页在ram address space的位置,长度是字节

 * (start + rb->offset) >> TARGET_PAGE_BITS 表示RAMBlock开始同步的页距离ram address space起始位置多少个页

 * BIT_WORD((start + rb->offset) >> TARGET_PAGE_BITS)表示,将页用位图表示后可以占用多少个long型的位图

 * 因此word表示RAMBlock开始同步的页距ram address space起始位置有多少个long型的位图

 * */                                         

unsigned long word = BIT_WORD((start + rb->offset) >> TARGET_PAGE_BITS);

/* 取出要同步到RAMBlock的位图 */

unsigned long *dest = rb->bmap;

/* 如果要同步的RAMBlock的起始位置对应页的bit恰好是long位图对齐的,那么可以直接将dirty_memory中位图按照

 * long型位图的长度依次拷贝到bmap中,这里就是判断是否属于这种情况 */

    if (((word * BITS_PER_LONG) << TARGET_PAGE_BITS) ==  (start + rb->offset))) {

     /* 计算要拷贝多少个long型的位图 */

int nr = BITS_TO_LONGS(length >> TARGET_PAGE_BITS);

/* 计算开始拷贝的页的位图在第几个block */

unsigned long idx = (word * BITS_PER_LONG) / DIRTY_MEMORY_BLOCK_SIZE;

/* 计算开始拷贝的页的位图在block中的偏移 */

unsigned long offset = BIT_WORD((word * BITS_PER_LONG) % DIRTY_MEMORY_BLOCK_SIZE);

/* 因为允许从RAMBlock位图的任意位置开始同步

 * 因此计算起始同步的位置对应的位距RAMBlock起始区间有多少个long型位图

 * 如果start为0,page也是0,那么表示从RAMBlock起始区间开始同步位图

 **/

       unsigned long page = BIT_WORD(start >> TARGET_PAGE_BITS);

       /* 取出dirty_memory的位图信息 */

       src = qatomic_rcu_read(&ram_list.dirty_memory[DIRTY_MEMORY_MIGRATION])->blocks;

       /* 从page开始拷贝long型位图到RAMBlock的bmap位图上,拷贝nr个long型位图 */

       for (k = page; k < page + nr; k++) {

            if (src[idx][offset]) {

             /* 将dirty_memory中的位图读取到bits中,完成后将dirty_memory对应的区域清零 */

                unsigned long bits = qatomic_xchg(&src[idx][offset], 0);

                unsigned long new_dirty;

                /* 保存bmap中原来干净脏页位,这里用于计算新增的脏页数,并非用于同步操作 */

                new_dirty = ~dest[k];

                /* 核心的同步操作,将从dirty_memory中取到的位图信息保存到RAMBlock的bmap中

                 * dest指向的bmap位图,这里同样是或操作,对于原来是脏页的位,bmap中保持不变

                 * 对于原来是干净的位,如果dirty_memory中为脏,则结果为脏,同步到bmap中 */

                dest[k] |= bits;

                /* 计算新增的脏页数 */

                new_dirty &= bits;

                /* 累加新增的脏页数 */

                num_dirty += ctpopl(new_dirty);

            }

        }                               

}        

 

发送脏页

- 脏页位图dirty_bitmap同步到RAMBlock->bmap中之后,迁移线程依据此信息查找RAMBlock中所有脏页,然后发送,下面介绍迁移线程从位图查脏页的原理,流程图如下:

- 迁移线程根据RAMBlock中的bmap信息,查寻脏页,如果有脏页,在发送之前将位图清零,每发送一个页就清零对应脏位,如下图所示:

- 具体发送流程如下:

/* 开始查找并发送RAMBlock里面的脏页 */

ram_find_and_save_block

/* 从给定的RAMBlock的位图中查找下一个被置位的 bit,找到第一个脏页对应位就停止 */

find_dirty_block

migration_bitmap_find_dirty

find_next_bit

/* 如果RAMBlock中有脏页,开始查找并发送 */

ram_save_host_page

/* 遍历位图中的每个bit,检查是否置位,如果置位,完成两个事情

 * 1. 如果使能了KVM_CLEAR_DIRTY_LOG特性,脏页查询中的重保护动作将从脏页查询的系统调用时段推迟到用户态发送脏页时进行

 *    因此这里需要检查是否开启此特性,如果开启,调用kvm_log_clear回调重保护kvm中的页表

 * 2. 如果bit被设置成1,将其清零,因为后面即将发送脏页,因此在发送前将其清零,表示该脏页已经发送。同时,将迁移的剩余脏页数减1,更新统计信息方便计算脏页速率和判断是否进入最后一轮迭代

 * */

migration_bitmap_clear_dirty

/* 执行上面介绍的第一个事情,发送系统调用重保护kvm中的页表 */

memory_region_clear_dirty_bitmap

/* 如果bit被置位,将其清零 */

test_and_clear_bit

/* 更新统计信息 */

rs->migration_dirty_pages--

文章来自个人专栏
外设中断
9 文章 | 1 订阅
0条评论
作者已关闭评论
作者已关闭评论
0
0