Qcow2 镜像文件格式

qcow2 镜像由大小固定的主机簇组成。客户机数据及镜像元数据均以簇为单位分配。

客户机看到的虚拟磁盘同样划分为相同大小的客户机簇。

qcow2 的全部数值均采用大端字节序。

附加字段(版本 3 及以后)

通常这些字段可选,可安全忽略。 若需设置后面的 B 字段而不关心前面的 A,可将 A 填零,等效于不存在。 正式兼容规则如下:

  1. 字段若不能忽略,否则无法正确处理文件,就必须有对应不兼容功能位。

  2. 若没有未知的不兼容功能位,未知附加字段可安全忽略,但重写头部时必须保留其值。

  1. 未被特定不兼容位改变时,显式值 0 的行为与字段不存在相同*。

(*) header_length 小于或等于字段偏移时,视为不存在。 版本 2 的所有附加字段均不存在。

104:        compression_type

            压缩簇采用的压缩方法,同一镜像全部压缩簇使用相同类型。

            设置 Compression type 不兼容位时,此字段必须存在且非零,
            表示非 deflate;否则必须不存在或为零,表示 deflate。

            可用值:
               - 0: deflate <https://www.ietf.org/rfc/rfc1951.txt>
               - 1: zstd <http://github.com/facebook/zstd>

            QEMU 将 deflate 称为 zlib <https://www.zlib.net/>,
            但此类型压缩簇不含 zlib 头。

105 - 111:  填充,内容见下文。

头部填充

header_length 必须是 8 的倍数,末尾字段未对齐时需填充。 填充必须为零,使现有或未来附加字段若落入其中,按上一节 [3.] 解释,即等效于该字段不存在。

头部扩展

镜像头后可保存可选扩展,每项结构如下:

Byte  0 -  3:   头扩展类型:
                    0x00000000 - 扩展区结束
                    0xe2792aca - 后备文件格式名字符串
                    0x6803f857 - 功能名称表
                    0x23852875 - 位图扩展
                    0x0537be77 - 全盘加密头指针
                    0x44415441 - 外部数据文件名字符串
                    other      - 未知扩展,可安全忽略

      4 -  7:   扩展数据长度。

      8 -  n:   扩展数据。

      n -  m:   填充至扩展大小为下一个 8 的倍数。

除非另有说明,同种扩展在镜像中最多一次。

存在后备文件时,其名称应保存在扩展区末尾到第一簇末尾的剩余空间。 此处不允许存其他数据,让实现修改头或添加扩展时,不损害不支持的兼容功能数据。 需要额外空间的兼容功能应使用头扩展。

字符串头部扩展

部分扩展只有一个字符串,如后备文件格式名、外部文件名。 扩展长度就是字符串长度,不以 \0 终止。 填充可能看起来像 NUL 结尾,但并不总需填充,也不保证填充为零。

功能名称表

可选头扩展,包含镜像使用的功能名称。 不认识某功能的应用,例如该功能是后来引入的,可用名称显示有用错误信息。

条目数由扩展数据长度决定,每项如下:

Byte       0:   功能类型,选择功能位图:
                    0: 不兼容功能
                    1: 兼容功能
                    2: 自动清除功能

           1:   所选功能位图中的位号,有效范围 0-63。

      2 - 47:   功能名称,零填充;达到完整长度时不一定以 NUL 终止。

位图扩展

可选头扩展,存储虚拟磁盘相关位图。 目前只有脏跟踪位图,记录某时刻以来的磁盘变化。

仅在对应自动清除位设置时才认为数据一致,见前述 autoclear_features。

字段如下:

Byte  0 -  3:  nb_bitmaps
               镜像内位图数量,至少 1。
               QEMU 当前每镜像最多支持 65535 个。

      4 -  7:  保留,必须为零。

      8 - 15:  bitmap_directory_size
               位图目录字节大小,是全部 nb_bitmaps 个条目的总大小。

     16 - 23:  bitmap_directory_offset
               位图目录在镜像内的起始偏移,必须按簇对齐。

全盘加密头指针

当且仅当 crypt_method 要求元数据时,必须存在此扩展。目前仅 LUKS 要求,其他方法必须不含。

提供加密附加数据的存储偏移及长度。

Byte  0 -  7:   加密头在镜像内的字节偏移,必须按簇对齐。
Byte  8 - 15:   已写加密头的字节长度。
                实际分配空间可能更大,会向上舍入到簇大小整数倍,
                未使用字节初始化为 0。

LUKS 方法的加密头如下。

头部簇的前 592 字节包含 LUKS 分区头,后面是密钥材料区域, 大小由密钥槽条带数和密钥大小决定。 分区头格式见 cryptsetup 源码的 docs/on-disk-format.pdf。

分区头的 payload-offset 按 LUKS 规范正常计算,即相对 LUKS 头起点的 头大小、密钥材料区和填充总和,无需按 qcow2 簇对齐。 qcow2 自己定义真实载荷位置,因此当前不使用此值,但仍应提供有效值。

密钥槽头的 key-material-offset 相对 qcow2 容器中 LUKS 头簇起点, 不是 qcow2 文件起点。

逻辑布局如下:

+-----------------------------+
| QCow2 文件头                |
| QCow2 头扩展 X              |
| QCow2 头扩展 FDE            |
| QCow2 头扩展 ...            |
| QCow2 头扩展 Z              |
+-----------------------------+
| ....其他 QCow2 表....       |
.                             .
.                             .
+-----------------------------+
| +-------------------------+ |
| | LUKS 分区头             | |
| +-------------------------+ |
| | LUKS 密钥材料 1         | |
| +-------------------------+ |
| | LUKS 密钥材料 2         | |
| +-------------------------+ |
| | LUKS 密钥材料 ...       | |
| +-------------------------+ |
| | LUKS 密钥材料 8         | |
| +-------------------------+ |
+-----------------------------+
| QCow2 簇载荷                |
.                             .
.                             .
.                             .
|                             |
+-----------------------------+

数据加密

头部请求加密时,每次读写必须解密/加密镜像载荷。镜像头和元数据始终不加密。

算法随方法而异:

  • AES:

    256 位密钥的 AES-CBC。

    初始化向量以 plain64 方法生成,以虚拟磁盘扇区作为 tweak 输入。

    由于多项设计缺陷影响安全,QEMU 系统仿真器不再支持。 仅命令行工具保留,用于向后兼容和取回数据。

  • LUKS:

    算法由 LUKS 头指定。

    初始化向量使用头部指定的方法,以物理磁盘扇区作为 tweak 输入。

主机簇管理

qcow2 为每个主机簇维护引用计数。0 表示空闲,1 表示使用中, 大于等于 2 表示使用中且任何写入都必须执行写时复制(COW)。

引用计数采用两级表。第一级 refcount table 大小可变,记录于头部, 可跨多簇,但在镜像文件中必须连续。

其指针指向第二级 refcount block,每块恰好一个簇。

足够大的表可保留超过 64 PB(56 位)的簇,前提是底层协议支持, 但 L1/L2 等元数据仍必须指向该界限之前的簇。

备注

QEMU 引用计数表上限为 8 MB。2 MB 簇、默认 refcount_order=4 时, 无法引用超过 2 EB(61 位)的主机资源;最坏情况下, 簇大小 512、refcount_order=6 时无法访问超过 32 GB(35 位)。

给定镜像文件偏移,其簇引用计数可这样获取:

refcount_block_entries = (cluster_size * 8 / refcount_bits)

refcount_block_index = (offset / cluster_size) % refcount_block_entries
refcount_table_index = (offset / cluster_size) / refcount_block_entries

refcount_block = load_cluster(refcount_table[refcount_table_index]);
return refcount_block[refcount_block_index];

引用计数表条目:

Bit  0 -  8:    保留,设为 0。

     9 - 63:    引用计数块起始文件偏移的第 9-63 位,必须按簇对齐。

                为 0 表示尚未分配该块,其管理的全部引用计数均为 0。

引用计数块条目,(x = refcount_bits - 1):

Bit  0 -  x:    簇的引用计数。
                若 refcount_bits 表示不足一字节的位宽,此处位 0 指最低有效位。

簇映射

客户机簇到主机簇的映射同样使用两级结构,称为 L1 和 L2 表。

L1 大小可变,记录于头部,可占多簇但必须连续。L2 恰好一个簇。

两级表影响最大虚拟文件大小。L1 大小固定时,较大簇使客户机可访问更多空间。 当前虚拟簇必须映射到低于 64 PB(56 位)的主机偏移, 未来可使用保留位放宽。簇增大时,压缩簇最大主机偏移反而减小: 2M 簇要求压缩簇低于 512 TB(49 位),放宽需不兼容的布局变更。

虚拟磁盘偏移转换为镜像文件偏移的算法:

l2_entries = (cluster_size / sizeof(uint64_t))        [*]

l2_index = (offset / cluster_size) % l2_entries
l1_index = (offset / cluster_size) / l2_entries

l2_table = load_cluster(l1_table[l1_index]);
cluster_offset = l2_table[l2_index];

return cluster_offset + (offset % cluster_size)

[*] this changes if Extended L2 Entries are enabled, see next section

L1 表条目:

Bit  0 -  8:    保留,设为 0。

     9 - 55:    L2 表起始文件偏移的第 9-55 位,必须按簇对齐。
                偏移为 0 表示 L2 表及其描述的全部簇未分配。

    56 - 62:    保留,设为 0。

         63:    L2 表未使用或需 COW 时为 0;引用计数恰为 1 时为 1。
                此信息仅在活动 L1 表中准确。

L2 表条目:

Bit  0 -  61:   簇描述符。

          62:   0:标准簇。
                1:压缩簇。

          63:   未使用、压缩或需 COW 的簇为 0。
                引用计数恰为 1 的标准簇为 1。
                仅从活动 L1 可达的 L2 表保证此信息准确。

                外部数据文件中,主机偏移与客户机偏移固定相等,
                因而全部客户机簇隐含引用计数为 1,已分配簇此位应为 1。

标准簇描述符:

Bit       0:    为 1 时簇读取为全零。主机簇偏移可描述预分配,
                但不会用于读取本簇;未分配时也不读取后备文件。

                版本 2 或扩展 L2 条目时,此位始终为 0。

     1 -  8:    保留,设为 0。

     9 - 55:    主机簇偏移第 9-55 位,必须按簇对齐。
                偏移为 0 且位 63 清除时,簇未分配。
                只有外部数据文件允许偏移为 0 且位 63 设置,表示真实主机偏移 0。

    56 - 61:    保留,设为 0。

压缩簇描述符,(x = 62 - (cluster_bits - 8)):

Bit  0 - x-1:   主机簇偏移,通常不按簇或扇区对齐。
                cluster_bits 足够小、字段覆盖超过位 55 时,高出的位必须为 0。

     x - 61:    除前字段偏移所在扇区外,压缩数据额外使用的 512 字节扇区数。
                部分扇区可能位于下一个连续主机簇。

                压缩数据不一定占满最后扇区,解压生成一个簇的数据后即停止。

                另一个压缩簇可映射到本压缩簇最后扇区的尾部。

簇未分配时,读取应转向后备文件,标准簇描述符位 0 已设置时除外。 没有后备文件,或后备文件小于镜像时,未覆盖部分读取为零。

扩展 L2 条目

头部 incompatible_features 的位 4 设置时使用扩展 L2。

标准数据簇分为 32 个等大的连续子簇,从簇起点开始。 子簇可独立分配,L2 记录每个状态。压缩簇没有子簇,处理与未启用该功能时相同。

扩展条目为 128 位,每表条目数:

l2_entries = (cluster_size / (2 * sizeof(uint64_t)))

前 64 位与上一节标准 L2 格式相同,但标准簇描述符位 0 例外。

后 64 位为以下子簇分配位图。

标准簇子簇分配位图:

Bit  0 - 31:    分配状态,每子簇一位。

                1:已分配,主机簇偏移必须有效。
                0:未分配,读取转向后备文件;无后备数据时返回零。

                从最低有效位开始分配,位 x 对应子簇 x。

    32 - 63     子簇读取为零,每子簇一位。

                1:读取为零,分配状态位必须清除,主机偏移可设置也可不设置。
                0:无作用。

                从最低有效位开始,位 x 对应子簇 x - 32。

压缩簇子簇分配位图:

Bit  0 - 63:    保留,设为 0。
                压缩簇没有子簇,不使用此字段。

快照

qcow2 支持内部快照,基本原理是切换活动 L1,将不同的主机簇集合暴露给客户机。

创建快照时复制 L1,并增加其可达全部 L2 和数据簇的引用计数, 使后续写入触发 COW,不被其他快照看到。

加载快照时,必须从引用计数表重建新活动 L1 及其引用全部 L2 的位 63, 因为非活动 L1 无需保持这些位准确。

全部快照的目录位于连续的快照表,起点与长度由 snapshots_offset 和 nb_snapshots 给出。 各条目长度随 ID、名称和额外数据变化。

快照表条目:

Byte 0 -  7:    快照 L1 表在文件内的起始偏移,按簇对齐。

     8 - 11:    快照 L1 表条目数。

    12 - 13:    快照唯一 ID 字符串长度。

    14 - 15:    快照名称长度。

    16 - 19:    快照创建时间,自纪元起的秒数。

    20 - 23:    创建时间不足一秒的部分,单位纳秒。

    24 - 31:    创建快照前客户机已运行的纳秒数。

    32 - 35:    虚拟机状态字节大小,未保存时为 0。
                状态从首个不描述普通客户机簇的 L1 条目对应的第一簇开始。
                即像客户机磁盘内容一样存储,但偏移超过客户机可见虚拟磁盘大小。

    36 - 39:    条目中额外数据的大小,用于未来格式扩展。

    variable:   未来扩展数据,未知字段必须忽略。
                当前定义如下,偏移相对快照条目:

                Byte 40 - 47:   虚拟机状态字节大小,未保存时为 0。
                                存在时忽略字节 32-35 的 32 位值。

                Byte 48 - 55:   快照虚拟磁盘字节大小。

                Byte 56 - 63:   快照时录制/重放的指令计数 icount。
                                未启用 icount 时设为 -1。

                版本 3 必须至少包含直到字节 55 的额外数据。

    variable:   快照唯一 ID 字符串,不以 NUL 终止。

    variable:   快照名称,不以 NUL 终止。

    variable:   将条目大小向上补齐到下一个 8 的倍数。

位图

位图扩展可保存与虚拟磁盘相关的位图,本节描述存储方式。

所有保存的位图都对应同一镜像内的虚拟磁盘,因此每个位图覆盖范围等于虚拟磁盘大小。

每位对应严格定义的磁盘范围。位 bit_nr 对应以下字节范围:

[bit_nr * bitmap_granularity .. (bit_nr + 1) * bitmap_granularity - 1]

粒度是每个位图自己的属性,见下文。

位图目录

每个保存的位图对应一个目录条目。目录在文件内连续, 偏移与长度由 bitmap_directory_offset、bitmap_directory_size 给出。 条目长度随名称和额外数据变化。

目录条目结构:

Byte 0 -  7:    bitmap_table_offset
                该位图的位图表在文件内的起始偏移,按簇对齐。

     8 - 11:    bitmap_table_size
                位图表条目数。

    12 - 15:    flags
                Bit
                  0: in_use
                     未正确保存,可能不一致。元数据对 qcow2 而言结构仍正确,
                     但 auto、大小等元数据或实际内容可能过时。

                  1: auto
                     必须反映任何应用对该 qcow2 虚拟磁盘的全部改变,
                     包括写入、快照切换等。类型必须为脏跟踪位图。

                  2: extra_data_compatible
                     软件不认识额外数据时有意义,当前 QEMU 不认识任何额外数据。
                     设置时可正常使用位图,但额外数据必须保持原样。
                     未设置时不得使用位图,但位图和额外数据都必须原样保留。

                Bits 3 - 31 保留,必须为 0。

         16:    type
                位图类型:
                  1: 脏跟踪位图。

                0、2 - 255 保留。

         17:    granularity_bits
                粒度位数,有效 0 - 63。
                QEMU 当前仅支持 9 - 31。

                粒度计算:
                    granularity = 1 << granularity_bits

                粒度表示位图一位对应的镜像字节数。

    18 - 19:    name_size
                名称大小,必须非零。QEMU 当前不支持超过 1023。

    20 - 23:    extra_data_size
                类型专属额外数据大小。

                当前未定义额外数据,此字段保留,应为零。
                非零时行为由 extra_data_compatible 决定。

    variable:   extra_data
                占 extra_data_size 字节。
                绝不能引用簇,也不能通过其他方式分配额外簇。

    variable:   name
                占 name_size 字节,不以 NUL 终止。
                在同一位图扩展的所有名称中必须唯一。

    variable:   填充至条目大小为下一个 8 的倍数,所有填充字节必须为零。

位图表

位图数据到主机簇的映射使用单级结构,称为位图表, 不同于引用计数和客户机簇映射的两级结构。

大小可变,记录于目录条目,可使用多簇,但必须在文件中连续。

位图表条目:

Bit       0:    位 9 - 55 非零时保留,必须为零。
                位 9 - 55 全零时:
                  0:簇应读取为全零。
                  1:簇应读取为全一。

     1 -  8:    保留,必须为零。

     9 - 55:    主机簇偏移第 9 - 55 位,必须按簇对齐。
                偏移 0 表示未分配,此时位 0 决定读取行为。

    56 - 63:    保留,必须为零。

位图数据

位图数据存于独立簇,由位图表描述。 给定位图数据字节偏移,文件偏移计算如下:

image_offset(bitmap_data_offset) =
    bitmap_table[bitmap_data_offset / cluster_size] +
        (bitmap_data_offset % cluster_size)

位图表条目位 9–55 全零时,此偏移未定义,见上文。

给定磁盘字节偏移 byte_nr 和粒度,对应位在镜像文件中的位偏移为:

bit_offset(byte_nr) =
    image_offset(byte_nr / granularity / 8) * 8 +
        (byte_nr / granularity) % 8

位图大小不是簇大小整数倍时,最后簇有未使用尾部位,必须为零。

脏跟踪位图

type 为 1 的位图即脏跟踪位图。

虚拟磁盘使用期间,位图可启用或禁用。 启用时所有写入都应反映在位图,置位表示对应磁盘范围在启用期间被写过, 未置位表示未写过。

软件无需每次写入或元数据变化都同步磁盘位图与内存表示。 不同步期间应设置 in_use。

文件中的启用状态由 auto 表示。设置时软件必须视位图为启用, 从虚拟磁盘首次写入起跟踪变化;未设置则禁用。