ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

QEMU QED 镜像格式规格全解:从磁盘布局、两级页表到一致性检查

QEMU QED 镜像格式规格全解:从磁盘布局、两级页表到一致性检查 QEMU QED 镜像格式规格全解从磁盘布局、两级页表到一致性检查【免费下载链接】qemuOfficial QEMU mirror. Please see https://www.qemu.org/contribute/ for how to submit changes to QEMU. Pull Requests are disabled. Please only use release tarballs from the QEMU website.项目地址: https://gitcode.com/gh_mirrors/qe/qemuQEDQEMU Enhanced Disk Format是 QEMU 自带的一种精简、可扩展的磁盘镜像格式采用簇cluster两级页表L1/L2的经典结构实现按需分配。本文以 QEMU 仓库中的 docs/interop/qed_spec.rst 官方规格文档为骨架结合 block/qed.h、block/qed.c、block/qed-cluster.c、block/qed-table.c、block/qed-check.c 等源码实现完整讲解 QED 的文件布局、Header 字段、三组特性位机制、L1/L2 页表寻址、逻辑偏移翻译、零簇优化以及崩溃一致性检查的底层原理。读完本文你将能从字节级理解 QED 镜像的每一个字段并掌握用qemu-img创建、检查和调试 QED 镜像的实操方法。QED 镜像的总体布局QED 文件由一系列等长的**簇cluster**组成文件按簇对齐排列----------------------------------- | cluster0 | cluster1 | cluster2 | ... | -----------------------------------规格文档中定义了三类簇header 簇第一个簇以header开头头部记录了普通簇从何处开始因此 header 本身具备可扩展性可以在头部与首个普通簇之间存放额外的格式信息数据簇data cluster存储真正的镜像内容L2 表L2 table与L1 表L1 table元数据簇L1 与 L2 表由一个或多个连续的簇构成。一个基本约束是正常情况下文件大小是簇大小的整数倍。如果文件大小不是簇的整数倍那么最后一个簇之后的多余信息在写入数据时可能无法被保留合法的额外信息应当放在 header 与第一个普通簇之间的空间内。规格同时强调磁盘上所有字段均为小端little-endian字节序。这一点在源码中得到了严格的印证——block/qed.c 中的qed_header_le_to_cpu()与qed_header_cpu_to_le()使用le32_to_cpu/le64_to_cpu系列函数在读写头时做字节序转换block/qed-table.c 中的qed_read_table()/qed_write_table()同样对表中的每个 64 位偏移做le64_to_cpu/cpu_to_le64转换。注QED 与 qcow2 最大的理念差异在于元数据寻址结构。qcow2 使用L1 → L2 → 数据簇的类似结构但附带 refcount 表而 QED 用两级页表直接完成逻辑地址到文件偏移的翻译并通过 QED_F_NEED_CHECK 特性位配合打开时的一致性检查来保证崩溃安全省去了维护引用计数的复杂度。Header 结构与字段详解QED 的 header 固定位于文件起始处offset 0。规格文档给出如下结构定义Header { uint32_t magic; /* QED\0 */ uint32_t cluster_size; /* in bytes */ uint32_t table_size; /* for L1 and L2 tables, in clusters */ uint32_t header_size; /* in clusters */ uint64_t features; /* format feature bits */ uint64_t compat_features; /* compat feature bits */ uint64_t autoclear_features; /* self-resetting feature bits */ uint64_t l1_table_offset; /* in bytes */ uint64_t image_size; /* total logical image size, in bytes */ /* if (features QED_F_BACKING_FILE) */ uint32_t backing_filename_offset; /* in bytes from start of header */ uint32_t backing_filename_size; /* in bytes */ }该结构与源码中的QEDHeader结构体见 block/qed.h#L86-L103完全一致magic、cluster_size、table_size、header_size为 32 位小端整数三组特性位、l1_table_offset、image_size为 64 位整数两个 backing 文件名描述字段为 32 位整数。整个结构使用QEMU_PACKED修饰保证磁盘布局与定义严格一致。各字段的约束与语义规格文档对各字段规定了明确的合法范围源码bdrv_qed_do_open()在打开镜像时逐项校验见 block/qed.c#L406-L558字段约束说明源码校验magic必须为QED\0探针函数bdrv_qed_probe()通过比对 magic 返回识别置信度 100block/qed.c#L33-L45s-header.magic ! QED_MAGIC时拒绝打开cluster_size必须是 2 的幂范围 [2^12, 2^26]即 4 KB ~ 64 MB簇大小影响元数据密度与 I/O 粒度子簇大小的请求需要读-改-写qed_is_cluster_size_valid()block/qed.c#L150-L160table_size必须是 2 的幂范围 [1, 16]单位簇决定 L1/L2 表大小表中条目数 table_size * cluster_size / 8qed_is_table_size_valid()block/qed.c#L162-L172header_size单位是簇header 与普通簇之间全部信息所占的簇数backing 文件名必须存储在前header_size个簇内校验header_size * cluster_size不溢出block/qed.c#L465-L469l1_table_offset必须为cluster_size的整数倍L1 表首字节在文件中的偏移qed_check_table_offset()block/qed.h#L286-L298image_size必须为 512 字节的整数倍客户机看到的块设备逻辑大小qed_is_image_size_valid()block/qed.c#L174-L184backing_filename_offset/backing_filename_size以字节偏移, 字节长度二元组描述一个字符串字符串不以 NUL 结尾、无对齐约束必须位于前header_size簇内可为绝对路径或相对镜像文件的相对路径打开时校验偏移长度不越界读取后补 NULqed_read_string()block/qed.c#L198-L212关于默认值源码中定义QED_DEFAULT_CLUSTER_SIZE 6553664 KB见 block/qed.h#L47、QED_DEFAULT_TABLE_SIZE 44 个簇见 block/qed.h#L80创建镜像时这两个参数都有默认值见下文创建 QED 镜像。基于特性位出现的字段backing_filename_offset与backing_filename_size是仅在features设置QED_F_BACKING_FILE位时才被使用的字段。规格特别强调无论特性位是否置位这些字段在 header 中始终占据固定空间——也就是说 header 的物理大小是恒定的特性位只决定字段是否有效这为格式扩展提供了稳定的基址。这一点与QEDHeader结构体固定声明全部字段的实现方式相呼应。三组特性位不兼容、向后兼容与自清除QED 把格式扩展能力拆分为三组 64 位位图规格文档给出了各自的打开策略features格式特性位如果镜像设置了未知的features位必须拒绝打开。不向后兼容的格式变更必须使用features位。compat_features兼容特性位可以安全打开未知位被直接忽略代表向后兼容的格式变更。autoclear_features自清除特性位可以安全打开但前提是先清除未知位。这是优雅降级机制——老程序打开时把未知位抹掉新程序再次打开时可以检测到该特性已失效。目前规格中定义的features位有三个常量见 block/qed.h#L52-L58位名称含义0x01QED_F_BACKING_FILE镜像使用了 backing file基础镜像0x02QED_F_NEED_CHECK镜像在关闭前未干净结束使用前需要先做一致性检查0x04QED_F_BACKING_FORMAT_NO_PROBEbacking file 是 raw 裸盘镜像禁止对其做格式自动探测避免内容中恰好出现 magic 常量时被误判为某种镜像格式而compat_features与autoclear_features当前没有任何已定义的位源码中QED_COMPAT_FEATURE_MASK 0、QED_AUTOCLEAR_FEATURE_MASK 0见 block/qed.h#L64-L65它们是为未来扩展预留的机制。源码中的三层处理逻辑打开镜像时 block/qed.c#L421-L430 的处理与规格一一对应未知features位 → 拒绝打开if (s-header.features ~QED_FEATURE_MASK)直接返回-ENOTSUP并报告Unsupported QED features未知compat_features位 → 忽略打开路径中不做任何处理兼容即可未知autoclear_features位 → 清除并落盘当文件可写且非 inactive 时将autoclear_features与已知掩码求交后写回 header随后执行 flushblock/qed.c#L502-L520确保未知位被持久化地敲掉。需要特别注意的是规格在说明backing_filename_offset/size时强调基于特性位的字段仅在对应特性设置时使用对QED_F_BACKING_FORMAT_NO_PROBE的处理源码在读取 backing 文件名后会把bs-backing_format强制置为rawblock/qed.c#L497-L499这正是规格所述确保 raw backing 文件永远不会被探测为其他镜像格式的具体实现。两级页表L1 与 L2 的寻址结构QED 的元数据核心是一个两级页表完成逻辑偏移 → 文件内簇偏移的翻译---------- | L1 table | ---------- ,------ | ------. ---------- | ---------- | L2 table | ... | L2 table | ---------- ---------- ,------ | ------. ---------- | ---------- | Data | ... | Data | ---------- ----------规格给出表的抽象定义#define TABLE_NOFFSETS (table_size * cluster_size / sizeof(uint64_t)) Table { uint64_t offsets[TABLE_NOFFSETS]; }一张表由一个或多个连续簇组成table_size决定表的大小。例如cluster_size 64 KB、table_size 4时每张表占 256 KB可容纳 32768 个 64 位偏移条目。L1 表固定存在源码在打开时无条件分配并加载 L1 表见 block/qed.c#L522-L529而L2 表按需分配——这决定了镜像的理论最大逻辑容量。规格给出约束式header.image_size TABLE_NOFFSETS * TABLE_NOFFSETS * header.cluster_size即 L1 表能容纳的最大簇数TABLE_NOFFSETS乘以每个 L2 表能覆盖的最大簇数TABLE_NOFFSETS再乘以簇大小。源码qed_max_image_size()block/qed.c#L139-L148正是这样计算的table_entries (table_size * cluster_size) / sizeof(uint64_t)最大镜像 table_entries * cluster_size * table_entries。以默认 64 KB 簇 4 簇表计算每表 32768 个条目最大镜像约 2^15 × 2^15 × 64 KB 64 TB 级别。特殊偏移值0 与 1L1、L2 与数据簇的偏移都必须按cluster_size对齐且以下两个值具有特殊语义0 —— 未分配unallocated该 L2 表或数据簇尚未分配1 —— 零簇zero仅数据簇该数据簇内容全为零且没有实际分配簇。源码中qed_offset_is_unalloc_cluster()判定offset 0、qed_offset_is_zero_cluster()判定offset 1见 block/qed.h#L309-L323。保留位约定未来的格式扩展可能需要在每个偏移上附带每项per-offset信息因此规格规定偏移的最低 12 位保留并必须为零当cluster_size大于 2^124 KB时会有更多低位置零的空闲位。实践中 64 KB 簇意味着偏移低 16 位均为零。L1/L2 表在源码中的读写与缓存字节序block/qed-table.c 的qed_read_table()从文件读入整张表后逐项le64_to_cpuqed_write_table()先做cpu_to_le64再按扇区对齐写回并可选 flush。L1 写入口qed_write_l1_table()会在写前触发块调试事件BLKDBG_L1_UPDATEblock/qed-table.c#L122-L128。L2 写入口qed_write_l2_table()对应BLKDBG_L2_UPDATE而qed_read_l2_table()在缓存未命中加载时触发BLKDBG_L2_LOADblock/qed-table.c#L137-L173这些调试事件可配合blkdebug驱动做故障注入测试。L2 写穿缓存block/qed-l2-cache.c 实现了一个写穿write-throughL2 缓存每个打开中的镜像维护一个 L2 缓存避免重复读取最近使用过的 L2 表条目带引用计数多个并发 I/O 请求可共享同一 L2 表缓存上限为MAX_L2_CACHE_SIZE 50个条目block/qed-l2-cache.c#L58-L59满时先淘汰未被引用的条目全部被占用则临时扩容。缓存提交发生在 L2 表已落盘且 L1 已更新指向之后保证写穿语义。逻辑偏移翻译从 guest 地址到文件偏移规格给出了逻辑偏移的位结构table_bits table_bits cluster_bits -------- -------- --------------- ------------------------------------- | L1 index | L2 index | byte offset | -------------------------------------即一个逻辑地址从高到低依次切分为 L1 索引、L2 索引和簇内字节偏移。规格同时给出翻译伪代码offset_mask ~(cluster_size - 1) # 用于掩掉文件内簇偏移的低位 def logical_to_cluster_offset(l1_index, l2_index, byte_offset): l2_offset l1_table[l1_index] l2_table load_table(l2_offset) cluster_offset l2_table[l2_index] offset_mask return cluster_offset byte_offset注意伪代码中的关键步骤cluster_offset offset_mask由于低 12 位及簇对齐的更多低位保留置零翻译时用掩码剔除这些保留位后再加回簇内偏移。源码中的实际翻译路径逻辑翻译的核心函数是qed_find_cluster()block/qed-cluster.c#L91-L142其执行顺序为限制请求长度到 L2 边界*len MIN(*len, (((pos s-l1_shift) 1) s-l1_shift) - pos)确保单个请求只作用于一张 L2 表取 L1 项l2_offset s-l1_table-offsets[qed_l1_index(s, pos)]若为 0未分配返回QED_CLUSTER_L1校验 L2 表偏移合法对齐且在文件范围内后通过qed_read_l2_table()加载命中 L2 缓存则直接复用取 L2 项offset request-l2_table-table[qed_l2_index(s, pos)]分类返回偏移为 0 →QED_CLUSTER_L2L2 中存在但未分配偏移为 1 →QED_CLUSTER_ZERO零簇偏移合法 →QED_CLUSTER_FOUND已分配的数据簇偏移非法 →-EINVAL。索引计算直接映射规格中的位切分qed_l1_index()用pos l1_shiftqed_l2_index()用(pos l2_shift) l2_mask其中l2_shift ctz32(cluster_size)簇大小的以 2 为底对数、l2_mask table_nelems - 1、l1_shift l2_shift ctz32(table_nelems)这些派生参数在打开时计算block/qed.c#L459-L463。qed_count_contiguous_clusters()block/qed-cluster.c#L30-L62还会顺带统计连续簇的数目对已分配簇要求偏移严格递增一个cluster_size对未分配簇和零簇分别要求连续相同这样一次请求就能覆盖尽可能大的连续范围减少元数据访问次数。未分配簇与零簇的读写语义未分配区域读访问未分配区域会落到 backing file如果没有 backing file则读出全零。若 backing file 比镜像小超出 backing file 末尾的未分配区域同样读零。写写入未分配区域会触发按需分配——分配新的数据簇若对应的 L2 表也未分配则同时分配一张新 L2 表。新数据簇先用 backing file 内容无 backing 则为零填充再叠加本次写入的数据。这种写时分配allocate-on-write正是镜像能保持精简的根本原因。源码中qed_alloc_clusters()block/qed.c#L227-L232从s-file_size文件当前末尾打开时向下取整到簇边界开始顺序分配qed_new_l2_table()block/qed.c#L246-L256分配新的零初始化 L2 表分配型写请求通过table_lock互斥保护并且存在分配型写队列 plug/unplug机制qed_plug_allocating_write_reqs()/qed_unplug_allocating_write_reqs()确保同一时刻只有一个分配型写在进行。零数据簇零数据簇是节省空间地存储全零区域的手段读零簇直接产生零不会去读取 backing file——规格用note明确点出零簇与未分配簇的本质区别在于零簇会阻断对 backing file 的读取写零簇会分配一个新数据簇新簇以零填充后叠加写入数据。对应到代码bdrv_qed_co_readv()遇到QED_CLUSTER_ZERO时直接返回零而遇到QED_CLUSTER_L1/L2未分配时进入 backing 文件读取路径bdrv_qed_co_pwrite_zeroes()则把整簇清零的写请求优化为写一个偏移为 1 的零簇条目从而避免实际分配数据簇。这为qemu-img等工具执行大范围清零操作提供了空间效率保障。一致性检查与 QED_F_NEED_CHECK为什么需要它QED_F_NEED_CHECK位用于在可能因崩溃或断电使镜像进入不一致状态的操作开始之前把镜像标记为脏。脏镜像的元数据可能不一致因此打开时必须先检查。规格说明该位在操作开始前置位操作正常完成后清除如果中途崩溃镜像会带着该位再次被打开从而触发检查与修复。检查的不变量invariants规格列举了三条必须满足的不变量每个簇只能被引用一次L1/L2 表中同一簇被引用多次视为不一致corruption没有任何引用的簇视为泄漏leaked偏移必须在镜像文件大小范围内且按cluster_size对齐表偏移必须距离文件末尾至少table_size * cluster_size字节确保整张表有空间完整存放。检查流程一致性检查从l1_table_offset出发扫描全部 L2 表当检查结束、除泄漏外没有其他错误时可以清除QED_F_NEED_CHECK位镜像即可正常访问。源码实现qed_checkblock/qed-check.c 的qed_check()block/qed-check.c#L224-L254按规格流程实现按文件大小计算总簇数nclusters分配一个已引用簇位图used_clusters每簇 1 bitqed_check_l1_table()先标记 L1 表自身所占簇然后遍历 L1 表的每个条目对未分配条目直接跳过对非法 L2 偏移qed_check_table_offset()失败在fix模式下清零修复并计入corruptions_fixed否则计入corruptions合法时把 L2 表所占簇标记为已用读取 L2 表后交给qed_check_l2_table()qed_check_l2_table()遍历每个数据簇偏移跳过未分配0与零簇1统计allocated_clusters与fragmented_clusters相邻但不连续的簇对非法偏移执行同样的清零修复合法偏移调用qed_set_used_clusters()标记——若某簇已被标记过则累加corruptions这正是每个簇只能被引用一次不变量block/qed-check.c#L44-L62的落地修复模式下有非法 L2 的表整体写回qed_write_l2_table_sync有非法 L1 条目时写回 L1 表qed_write_l1_table_sync全图扫描成功后qed_check_for_leaks()从header_size之后逐簇检查位图未被引用的簇计入leaksblock/qed-check.c#L188-L198若没有无法修复的损坏与 I/O 错误qed_check_mark_clean()先bdrv_co_flush确保修复落盘再清除QED_F_NEED_CHECK并写回 headerblock/qed-check.c#L203-L221。打开时的自动检查与定时器bdrv_qed_do_open()在加载 L1 表之后执行规格所述的打开即检查策略block/qed.c#L531-L548若未显式处于 check 模式、且镜像带着QED_F_NEED_CHECK位则在可写时调用qed_check(s, result, true)带修复自动恢复对只读镜像则放行打开——由于不可能发生写操作潜在的不一致不会恶化这有助于从损坏镜像中恢复数据。写入路径上还有一个巧妙的延迟清理定时器QED_NEED_CHECK_TIMEOUT 5秒block/qed.h#L83。分配型写完成并 flush 后定时器在 5 秒后再次 flush 并清除QED_F_NEED_CHECKqed_need_check_timer()block/qed.c#L286-L312。定时器基于QEMU_CLOCK_VIRTUAL因此挂起如迁移期间不会修改镜像文件。若在这 5 秒窗口内崩溃镜像只是保持脏标记下次打开时自动检查即可。实操用 qemu-img 创建与检查 QED 镜像QED 驱动注册的创建选项定义在qed_create_optsblock/qed.c#L1618-L1650选项含义默认值size虚拟磁盘大小guest 看到的逻辑大小须为 512 字节倍数必填backing_file基础镜像文件名无不使用 backingbacking_fmt基础镜像的格式自动探测除非设置了backing_fmtraw对应QED_F_BACKING_FORMAT_NO_PROBE语义cluster_size簇大小字节6553664 KBtable_sizeL1/L2 表大小簇4对应的命令行用法# 创建一个 10 GB 的 QED 镜像默认 cluster_size64Ktable_size4 qemu-img create -f qed disk.qed 10G # 显式指定簇大小与表大小 qemu-img create -f qed -o cluster_size64K,table_size4 disk.qed 10G # 基于 raw 基础镜像创建差异镜像backing_fmtraw 会置 QED_F_BACKING_FORMAT_NO_PROBE qemu-img create -f qed -o backing_filebase.raw,backing_fmtraw overlay.qed 10G创建后的镜像可直接作为块设备使用通过-drive filedisk.qed,formatqed挂载或qemu-nbd导出也可用qemu-img check触发一致性检查# 执行一致性检查对应 bdrv_qed_co_check → qed_checkfixfalse qemu-img check disk.qed # 检查并修复损坏fixtrue会清零非法偏移并清除 NEED_CHECK 位 qemu-img check -r all disk.qedqemu-img check输出的corruptions、leaks、corruptions_fixed、fragmented_clusters等统计项与 block/qed-check.c 中BdrvCheckResult的字段一一对应可据此判断镜像健康状况。此外QED 镜像同样支持qemu-img info查看虚拟大小/簇大小/backing 文件等信息经由bdrv_co_get_info以及qemu-img convert、qemu-img snapshot等通用操作。一个可以手工验证的细节规格规定image_size必须是 512 的倍数、cluster_size必须为 2 的幂且在 [4 KB, 64 MB] 内。若手工构造不合法的镜像头例如篡改cluster_size为非 2 的幂qemu-img open会依次触发qed_is_cluster_size_valid()/qed_is_table_size_valid()/qed_is_image_size_valid()的校验并报告 QED cluster size is invalid 之类的错误——这正是 block/qed.c#L431-L457 的校验链在起作用可作为验证格式约束的手段。测试与验证资源QED 格式的健壮性在仓库的测试体系中得到了覆盖qemu-iotests大量 iotests 用例把qed列为受支持的格式例如 tests/qemu-iotests/004_supported_fmt raw qcow qcow2 qed vdi vmdk vhdx luks覆盖qemu-img info/create等基础操作tests/qemu-iotests/027_supported_fmt vmdk qcow qcow2 qed专门覆盖**一致性检查check**行为是阅读qed_check预期结果的最佳参考image-fuzzertests/image-fuzzer/runner.py 将qed列入可模糊测试的格式列表与 raw、vmdk、vdi、qcow2、vpc 等同通过随机篡改镜像内容验证 QEMU 对畸形 QED 镜像的容错能力这与规格中关于偏移合法性、对齐约束的强制校验直接相关。小结从磁盘布局到字节序从 Header 的 13 个字段到三组特性位从 L1/L2 两级页表到逻辑偏移翻译从零簇优化到QED_F_NEED_CHECK一致性检查QED 以相当精炼的设计实现了精简分配 崩溃安全的镜像能力。理解 docs/interop/qed_spec.rst 这份规格再对照 block/qed.h 与 block/qed.c 等实现既能帮助你在排障时读懂镜像字节也能为设计自己的块设备格式提供一份完整、可落地的参照模板。【免费下载链接】qemuOfficial QEMU mirror. Please see https://www.qemu.org/contribute/ for how to submit changes to QEMU. Pull Requests are disabled. Please only use release tarballs from the QEMU website.项目地址: https://gitcode.com/gh_mirrors/qe/qemu创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表