10.4 QCOW2虚拟机镜像


10.4.1 QCOW2格式

typedef struct QCowHeader {

    uint32_t magic;// 'Q''F' 'I' 0xb0

    uint32_t version;

  //backing 文件名字符串在相对于qcow2文件起始位置的偏移,无null结束符

    uint64_t backing_file_offset;

    uint32_tbacking_file_size; //文件名字符串的长度

    // cluster_bits决定了怎样将镜像偏移地址转换为到在镜像文件中偏移地址,其决定了在一个簇中,将拿偏移地址的多少位(低位)来作为索引。L2表占据一个单独的簇,包含若干8字节的项,cluster_bits最少用3bits作为L2表的索引

    uint32_t cluster_bits;

    uint64_t size; //该镜像对应block设备的大小(以字节为单位)

    uint32_t crypt_method;//0表示无加密, 1表示AES

    uint32_t l1_size; //L1table 元素的项数(每项8字节)

    uint64_tl1_table_offset;//L1 table相对于qcow2文件起始位置的偏移

    uint64_trefcount_table_offset;//refcount_table相对于文件起始位置的偏移

    uint32_t refcount_table_clusters;//refcount_table的大小,单位为cluster

    uint32_t nb_snapshots;//镜像中snapshots的数量

    uint64_tsnapshots_offset;// 相对于文件起始位置的偏移

 

     //下面在version 3中有效,本文只分析道version 2

    uint64_tincompatible_features;

    uint64_tcompatible_features;

    uint64_tautoclear_features;

 

    uint32_trefcount_order;

    uint32_theader_length;

} QCowHeader;

 

一个典型的镜像文件,其布局如下:

一个QCowHeader, 如上描述;

在下一个簇开始,存放L1 table;

refcount table,仍然是簇对齐的;

一个或者多个的refcount blocks;

Snapshot headers,第一个header要求簇对齐,之后的header要求8字节对齐;

L2 tables,每一个table占据一个单独的cluster;

镜像数据clusters。

 

下面以cluster_bits为12时,分析两级映射表是如何工作的, 设block disk的的字节号为addr:

addr[11:0] cluster内偏移

addr[20: 12] 在L2表中的偏移,此时L2表有512项(l2_bits  = (cluster_bits - 3)= 9)

addr[63:21] 在L1中的偏移

 

所以L1 table的最小值为(l1_size为项数)

l1_size = round_up(disk_size / (cluster_size * l2_size),cluster_size)

 

为了将磁盘镜像地址映射到镜像文件偏移,需要经历以下几步:

1. 通过qcow2 header中的l1_table_offset字段获取L1 table的地址;

2. 使用高(64 - l2_bits - cluser_bits)位的地址来索引L1 table,L1 table是一个数组,数组元素是一个64位的数;

3. 通过L1 table中的表项来获取L2 table的地址;

4. 通过L2 table中的表项来获取cluster的地址;

5. 剩余的cluster_bits位来索引cluster内的位置。

如果找到的L1 table或L2 table的地址偏移为0,则表示磁盘镜像对应的区域尚未分配。

 

引用计数
  每一个cluster都有一个引用计数,当没有任何快照再使用这个cluster时,cluster可以被删除。 针对每一个cluster的2个字节的引用计数,存放在cluster sized blocks。通过refcount_table_offset字段可以获取到refcount table的位置,refcount_table_clusters字段给出refcount table的大小(单位为cluster),refcount table给出了这些refcount blocks在镜像文件中的偏移地址。 qcow2有一个优化处理,任何一个L1或L2表项指向的cluster的引用计数为1,则L1/L2表项的最高有效位被置上“copied”标记。这表明没有快照在使用这个cluster,所以这个cluster可以马上写入数据,而不需要复制一份给快照使用。

 

压缩
qcow2镜像格式支持压缩特性,其允许每一个cluster独立的通过zlib进行压缩。从L2 table中获取cluster offset的流程如下:

·        如果clusteroffset的第二最高有效位是1,则这是一个被压缩的cluster;

·        clusteroffset中之后的cluster_bits - 8 位是这个压缩过的cluster的大小,单位是sectors;

·        clusteroffset剩余的位是压缩的cluster在文件中的实际偏移地址。


加密
qcow2格式,也支持针对cluster的加密。
如果QCowHeader中的crypt_method字段被置为1,则会采用一个16个字符的密码作为128位AES key。
每一个Cluster中的每一个sector都是通过AES密码块链接模式来单独加密,采用sector的偏移地址(小端模式)来作为128位初始化向量的头64位

 

Copy-on-Write特性
一个qcow2支持增量存储,增量部分仅保存镜像的变化部分,从而不实际影响到原有磁盘的内容。仅当clusters中的内容跟原镜像不一样的时候,这些cluster才会被保存到增量镜像中。写时复制的实现方式比较简单。  当要从增量镜像中读取一个cluster时,qemu会先检查这个cluster在增量镜像中有没有被分配。如果没有,则会去读原始镜像中的对应位置。

 

下面在来看看snapshot的头定义:

typedef struct QEMU_PACKED QCowSnapshotHeader {

    /* header is 8 bytealigned */

    uint64_tl1_table_offset;//快照拥有原始l1_table的副本

    uint32_t l1_size;

 

    uint16_t id_str_size;//快照id长度

    uint16_t name_size;//快照名字长度

 

    uint32_t date_sec;//快照生成时间

    uint32_t date_nsec;

    uint64_tvm_clock_nsec;

 

// 表示作为快照的一部分被保存的虚拟机状态的大小。这个状态被保存在原来L1 table的位置,直接在镜像header的后面

    uint32_tvm_state_size;   

   uint32_textra_data_size; /* for extension */

    /* extra data follows*/

    /* id_str follows */

    /* name follows  */

} QCowSnapshotHeader;

建立一个快照,就会添加一个QCowSnapshotHeader,然后复制一份L1 table,同时会增加所有L2 table和数据clusters的被L1 table引用的引用计数。打完快照之后,如果任何在这个镜像中的L2 table或者data clusters被修改了——也就是,如果一个cluster的引用计数大于1,且"copied"标记被置上了——qemu则会先复制一份这个cluster,然后再写入数据。就这样,所有的快照都不会被修改

 

10.4.2 Qemu中QCOW2源码架构分析

10.4.2.1 qcow2模块对上层接口

static BlockDriver bdrv_qcow2 = { (qcow.c)

    .format_name        = "qcow2",

    .instance_size      = sizeof(BDRVQcowState),

    .bdrv_probe         = qcow2_probe, //判断文件是否为qcow2

    .bdrv_open          = qcow2_open, //打开qcow2 image,加载l1table

    .bdrv_close         = qcow2_close,

   .bdrv_reopen_prepare  =qcow2_reopen_prepare,

    .bdrv_create        = qcow2_create,//根据用户参数创建空镜像

    .bdrv_co_is_allocated= qcow2_co_is_allocated,//产看对应磁盘扇是否创建

    .bdrv_set_key       = qcow2_set_key,//设置加密key

    .bdrv_make_empty    = qcow2_make_empty,

 

    //下面3个为读写与flush

    .bdrv_co_readv          = qcow2_co_readv,

    .bdrv_co_writev         = qcow2_co_writev,

   .bdrv_co_flush_to_os    =qcow2_co_flush_to_os,

 

   .bdrv_co_write_zeroes   =qcow2_co_write_zeroes,//清除指定sectors

    .bdrv_co_discard        = qcow2_co_discard,//回收指定

    .bdrv_truncate          = qcow2_truncate,

   .bdrv_write_compressed  =qcow2_write_compressed,

     //snapshot相关操作

   .bdrv_snapshot_create   =qcow2_snapshot_create,

   .bdrv_snapshot_goto     =qcow2_snapshot_goto,

   .bdrv_snapshot_delete   =qcow2_snapshot_delete,

   .bdrv_snapshot_list     =qcow2_snapshot_list,

   .bdrv_snapshot_load_tmp     =qcow2_snapshot_load_tmp,

    .bdrv_get_info      = qcow2_get_info,

     //vmstate的存储于恢复

   .bdrv_save_vmstate    =qcow2_save_vmstate,

    .bdrv_load_vmstate    = qcow2_load_vmstate,

 

   .bdrv_change_backing_file   =qcow2_change_backing_file,

 

   .bdrv_invalidate_cache      =qcow2_invalidate_cache,

 

    .create_options =qcow2_create_options,

    .bdrv_check =qcow2_check,

};

这里分析读与写虚拟disk的例子

(1) qcow2_co_readv:

a. 每次取在一个cluster内的数据,cluster在文件中的位置由qcow2_get_cluster_offset得到, qcow2_get_cluster_offset先根据addr的到L2 table 的对应值,然后根据该值返回不同的类别

enum {

   QCOW2_CLUSTER_UNALLOCATED, //该cluster为分配

    QCOW2_CLUSTER_NORMAL,

   QCOW2_CLUSTER_COMPRESSED, //压缩类别

    QCOW2_CLUSTER_ZERO //内容为全0

};

b.根据qcow2_get_cluster_offset的返回内别做不同处理:

  case QCOW2_CLUSTER_UNALLOCATED:如果存在与back file中则从backfile中获取

  case QCOW2_CLUSTER_NORMAL:bdrv_co_readv直接读取文件对应位置

  case QCOW2_CLUSTER_ZERO:直接设为全0

  case QCOW2_CLUSTER_COMPRESSED:用qcow2_decompress_cluster读取

c. 循环a-b只到读取所有cluster

 

(2) qcow2_co_writev

a. 用qcow2_alloc_cluster_offset得到一个cluster(qcow2_alloc_cluster_offset对已存在的cluster直接返回文件中的位置,对未分配的cluster会先分配在返回其位置)

b. 若为加密方式则调用qcow2_encrypt_sectors

c. bdrv_co_writev写数据

d. 更新L2 Table cow2_alloc_cluster_link_l2

e. 循环a-d只到写完所有cluster

 

10.4.2.2 ref table的管理

这里先分析如何根据cluster_id定位reftable中的位置

static int get_refcount(BlockDriverState *bs, int64_tcluster_index) {

    refcount_table_index =cluster_index >> (s->cluster_bits - REFCOUNT_SHIFT);

    if(refcount_table_index >= s->refcount_table_size)

        return 0;

    refcount_block_offset= s->refcount_table[refcount_table_index];

    .......

 

    ret =qcow2_cache_get(bs, s->refcount_block_cache, refcount_block_offset,

        (void**)&refcount_block);

 

    block_index =cluster_index &

        ((1 <<(s->cluster_bits - REFCOUNT_SHIFT)) - 1);

    refcount =be16_to_cpu(refcount_block[block_index]);

 

    ret =qcow2_cache_put(bs, s->refcount_block_cache,

        (void**) &refcount_block);

    if (ret < 0) {

        return ret;

    }

 

    return refcount;

}

refcount_block_cache的引入在与优化refcount的管理,当cache中数据已存在时不需要在读磁盘

 

10.4.2.3 vmstate管理

static int qcow2_save_vmstate(BlockDriverState *bs, constuint8_t *buf,

                              int64_t pos, int size) {

    ......

   BLKDBG_EVENT(bs->file, BLKDBG_VMSTATE_SAVE);

    bs->growable = 1;

    ret = bdrv_pwrite(bs,qcow2_vm_state_offset(s) + pos, buf, size);

    bs->growable =growable;

    return ret;

}

static int64_t qcow2_vm_state_offset(BDRVQcowState *s)

{

    return(int64_t)s->l1_vm_state_index << (s->cluster_bits + s->l2_bits);

}

vmstate被写入了由qcow2_vm_state_offset返回的位置

qcow2_open中对l1_vm_state_index设置了初值:

s->l1_vm_state_index = size_to_l1(s, header.size);

static inline int size_to_l1(BDRVQcowState *s, int64_t size)

{

    int shift =s->cluster_bits + s->l2_bits;

    return (size + (1ULL<< shift) - 1) >> shift;

}

即其对应的cluster号在disk所有数据的末尾,当disk size变化时该值会跟着变化

 

10.4.2.4 SnapShot管理

这里分析qcow2_snapshot_create的流程(sn_info由调用者声称):

qcow2_snapshot_create(BlockDriverState *bs, QEMUSnapshotInfo*sn_info)

{

    a. 根据sn_info声称header中与时间和str相关字段

    sn->disk_size =bs->total_sectors * BDRV_SECTOR_SIZE;

    sn->vm_state_size =sn_info->vm_state_size;

    sn->date_sec =sn_info->date_sec;

    sn->date_nsec =sn_info->date_nsec;

    sn->vm_clock_nsec =sn_info->vm_clock_nsec;

    b. 分配一个L1 Table,并复制原l1tablen

       l1_table_offset =qcow2_alloc_clusters(bs, s->l1_size * sizeof(uint64_t));

    sn->l1_table_offset= l1_table_offset;

    sn->l1_size =s->l1_size;

 

    l1_table =g_malloc(s->l1_size * sizeof(uint64_t));

    for(i = 0; i <s->l1_size; i++) {

        l1_table[i] =cpu_to_be64(s->l1_table[i]);

    }

 

    ret =bdrv_pwrite(bs->file, sn->l1_table_offset, l1_table,

                     s->l1_size * sizeof(uint64_t));

 

c. 增加所有L2 table和数据clusters的被L1 table引用的引用计数

  ret =qcow2_update_snapshot_refcount(bs, s->l1_table_offset, s->l1_size, 1);

 

d. 添加snapshot

    new_snapshot_list =g_malloc((s->nb_snapshots + 1) * sizeof(QCowSnapshot));

    if (s->snapshots) {

       memcpy(new_snapshot_list, s->snapshots,

              s->nb_snapshots * sizeof(QCowSnapshot));

        old_snapshot_list= s->snapshots;

    }

    s->snapshots =new_snapshot_list;

   s->snapshots[s->nb_snapshots++] = *sn;

 

    ret =qcow2_write_snapshots(bs);

}

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值