固定链接 浅析 Qcow2 文件格式和基本应用

浅析 Qcow2 文件格式和基本应用

浅析 Qcow2 文件格式和基本应用

1. Qcow2 简介

Qcow2 是 QEMU 使用的磁盘镜像文件格式。它使用一个文件来表示一个块设备。与 raw 镜像格式相比,qcow2 具有如下优点:
– 文件更小
– 支持写时拷贝
– 支持快照
– 支持基于 Zlib 的数据压缩
– 支持 AES 加密

Qcow2 文件的数据布局如下:

Qcow2 File
Qcow2 header
Qcow2 header extension
refcount table
refcount block
L1 table
L2 table
data cluster
L2 table
data cluster

2. Header

Qcow2 文件的开始是 header,header 的格式如下:

  • magic 为一个特定的字符串 “QFI\xfb”,标识这是一个 qcow2 文件;
  • version 标识该文件对应的 qcow2 文件格式版本,合法的值为 2 或 3;
  • backing_file_offset 标识 backing_file 文件路径字符串相对于文件起始位置的偏移地址,该字符串不以 null 结尾,如果为 0 表示 image 没有 backingfile;
  • backing_file_size 表示 backing file 文件路径字符串长度,单位是字节数,必须小于 102 3字节。如果没有 backing file,则该字段未定义;
  • cluster_bits 表示簇的大小,每个簇大小不能小于 512 字节,不能超过 2MB;
  • size 为虚拟磁盘大小,即是该镜像表示的块设备大小,单位字节;
  • crypt_method 表示加密方法,0 表示不加密,1 表示 AES 加密,2 表示 LUKS 加密;
  • l1_size 表示当前激活的 L1 表中的表项个数;
  • l1_table_offset 为当前激活的 L1 表相对于镜像文件起始位置的偏移,簇的地址对齐;
  • refcount_table_offset 为 refcount 表相对于镜像文件起始位置的偏移,簇的地址对齐;
  • refcount_table_clusters 为 refcount 表包含的簇个数;
  • nb_snapshots 为镜像中 snapshots 个数;
  • snapshots_offset 为 snapshot 表相对于镜像文件起始位置的偏移,簇的地址对齐。

如果 version 字段是 3,则 header 还有一些字段如:incompatible_features、compatible_features、autoclear_features、refcount_order等。

Qemu 打开 qcow2 文件的代码如下:

接着 header 后面,就是一个可选的扩展节,每个扩展节的结构如下:

  • Byte 0~3:扩展类型

    可选扩展类型:
    0x00000000 – 扩展节的结束
    0xE2792ACA – backing file 格式
    0x6803f857 – Feature name 表
    0x23852875 – Bitmaps 扩展

  • Byte 4~7:扩展数据长度

  • Byte 8~n:扩展数据
  • Byte n~m:补齐到8的倍数

如果镜像文件有 backing file,则 backing file 的名字存储在扩展节和第一个簇结尾之间的空间。Qcow2 格式允许将对一个只读的基础镜像文件的修改存储到一个单独的 qcow2 镜像文件,基础镜像文件即做为新的镜像文件的 backing file。从新的镜像文件读数据时,如果新文件中有则从新文件读,否则从 backing file 读。

3. 簇引用计数管理

Qcow2 镜像文件以固定单位大小进行分配和组织,这个单位称为簇。不管是镜像文件对应的盘数据,还是镜像文件的元数据,均以簇为单位存储。

Qcow2 为每个簇维护一个引用计数。引用为 0 表示簇空闲,1 表示簇正被使用,大于等于 2 表示簇正被使用,并且任何对其的写操作需要执行一个写时拷贝操作。

引用计数采用两级表管理。第一级称为 refcount 表,表的大小存储在 header。Refcount 表可以跨多个簇,但需要连续存储。Refcount 表每项为一个指向第二级表称为 refcount 块的指针,每个 refcount 块大小为一个簇,表中每项存储一个对应簇的引用计数。

Refcount 表的表项
– Bit 0 – 8: 保留,设置为 0;
– Bit 9 – 63: 对应的 refcount 块在镜像文件中的偏移的 9-63 位,必须对齐簇地址。0 表示 refcount 块未分配,所有对应的引用计数为 0 。

Refcount 块的表项
Bit 0 – x: x = refcount_bits – 1,表示对应簇的引用计数。

给定一个镜像文件的偏移 host_offset,它所在簇的引用计数可计算如下:

  1. 每个 refcount 块包含的表项数
    refcount_block_entries = (cluster_size * 8 / refcount_bits)

  2. host_offset 在 refcount 块内的表项索引
    refcount_block_index = (host_offset / cluster_size) % refcount_block_entries

  3. host_offset 在 refcount 块编号
    refcount_table_index = (offset / cluster_size) / refcount_block_entries

  4. 加载 refcount 块
    refcount_block = load_cluster(refcount_table[refcount_table_index])

  5. 获得引用计数
    ref_count = refcount_block[refcount_block_index]

4. 磁盘地址到镜像文件偏移的转换

Qcow2 使用两级结构管理从磁盘地址到镜像文件偏移的映射,这个结构称为 L1 表和 L2 表。L1 表是变长的,L1 表的大小存储在 header 中,L1 表可使用多个簇,但必须连续存储。L2表 的大小为一个簇。

L1, L2 表项的内容如下:

L1 表项

  • Bit 0 – 8: 保留,设置为 0;
  • Bit 9 – 55: 对应的 L2 表在镜像文件中的偏移的 9-55 位。偏移必须簇对齐。如果偏移为 0,则该 L2 表和它描述的簇都未分配;
  • Bit 56 – 62: 保留,设置为 0;
  • Bit 63: 0 表示对应的 L2 表未使用或需要写时复制,1 表示引用计数为 1,该信息只对当前激活的 L1 表有效。

L2 表项

  • Bit 0 – 61: 簇描述符;
  • Bit 62: 0 为标准簇,1 为压缩簇;
  • Bit 63: 0 表示对应的簇未使用或需要写时复制,1 表示引用计数为1,该信息只对当前激活的 L1 表引用的 L2 表有效。

标准簇描述符

  • Bit 0: 1 表示该簇数据为全 0;
  • Bit 1 – 8: 保留,设置为 0;
  • Bit 9 – 55: 对应的簇在镜像文件中的偏移的 9-55 位。偏移必须簇对齐,如果偏移为 0,则该簇未分配;
  • Bit 56 – 61: 保留,设置为0。

压缩簇描述符

  • Bit 0 – x: x = 62 – (cluster_bits – 8),对应的簇在镜像文件中的偏移,可以不簇对齐;
  • Bit x+1 – 61: 压缩数据用到的额外的 512 字节扇区数。

对于一个未分配的簇,如果该簇的描述符的 Bit 0 为 1,则直接返回 0,否则从 backing file 读数据。如果没有 backing file 或者 backing file 比镜像文件小,则 backing file 中没有的数据返回 0。

给定一个磁盘内的逻辑地址 guest offset,可以通过如下计算得到其在镜像文件中的位置 host offset:

  1. 每个 L2 表中包含的表项数
    l2_entries = (cluster_size / sizeof(uint64_t))

  2. offset 所在 L2 表内的表项索引
    l2_index = (guest_offset / cluster_size) % l2_entries

  3. offset 所在 L2 表的编号
    l1_index = (guest_offset / cluster_size) / l2_entries

  4. 加载对应的 L2 表
    l2_table = load_cluster(l1_table[l1_index])

  5. 获得 offset 所在簇在镜像文件中的起始地址
    cluster_offset = l2_table[l2_index]

  6. 获得镜像文件内的地址
    host_offset = cluster_offset + (offset % cluster_size)

5. 快照

Qcow2 支持快照,方式是通过切换激活的 L1 表,使得暴露不同的簇集合给 guest。当创建快照时,L1 表将被拷贝,所有该 L1 表指向的 L2 表和数据簇的引用计数递增,这样写操作会引起写时复制。当加载快照时,新激活的 L1 表和所有其指向的 L2 表的表项的第 63 bit 的值需要根据 refcount 表重构。

所有的快照信息存储在 snapshot 表中。Snapshot 表必须连续存储,其起始偏移和长度记录在 header 中。Snapshot 表的表项是变长的,表项中包含一个快照对应的 L1 表在镜像文件中间的偏移,快照的 L1 表的表项数,快照 id 字符串的长度,快照名的长度,快照创建时间等信息。

参考
https://git.qemu.org/?p=qemu.git;a=blob;f=docs/interop/qcow2.txt

本文作者:汪黎

您的留言将激励我们越做越好