QCOW2实现原理的一般性思考

    1 为什么地址索引表被设计成多级?

    qcow2的快照之所以速度快,是因为它只需要操作元数据。要理解快照的原理,先看看qcow2文件头中地址索引的原理。qcow2设计了L1,L2两个表,用来存放地址。为什么是两个?

    考虑一个一般性的地址查找问题,现在有一个地址为000,条目为8的表存放了数据。如下:

    

                图1

    我手拿一个三位数的地址想取对应的数据,需要挨个读取表中每一条地址信息,比较是否和手中的地址相等,相等取对应的数据。如果我恰好拿到的地址是7则需要查找至少8次才能去到相应的数据。

    换种方式,现在存放数据的表从1张变成2张,每张表4个条目,存放了同样的8条数据,再多加一张地址表用来记录数据表的地址,地址表的地址是000,如下:

    

                图2

    现在我要取地址7的数据,首先把地址的高1位取出来,值为1,表示记录数据的数据表,它的地址被地址表的第1个条目记录着,我们把地址表的第1个条目的内容A1 取出来,根据地址A1找到数据表;然后把地址的低2位取出来,值为11,表示数据被数据表的第11个条目记录着。现在数据表已经找到,根据索引11最后找到对应的条目,取出内容d7。

    从上面可以看出,一张表最坏的情况下要查找8次,两张表最坏的情况下查找6次(地址表2次+数据表4次)。查找效率上,多级表要高,但多级表把原本一张表可以存储的数据表拆成了两张,同时多出一张地址表。增加了存储的空间,这样看,多级表的设计是一个典型的用空间换时间的例子。

    新华字典根据拼音查找汉字,把拼音也分成了两级,根据首字母找到二级表的页数,在找到的页数上根据余下的字母查找匹配的拼音,取出它对应的页数(页数相当于数据表中的数据)。

    上面的二级表举例也给出了多级表查找的方法。首先将多级表最低一级的条目数-1作为掩码(011),和地址做&&操作,取出数据在最低级表中的索引。然后将地址右移2位(最低级表的条目数用2的幂级数表示时对应的幂),多级表的次低一级的条目数-1作为掩码,和右移后地址做&&操作,得到数据在次低一级表中的索引。以此类推。

    拿上面的两级表举例,地址由3bit或者大于3bit的数表示。假设地址bit位数为8。

    两级表结构。二级表总条目数4,一级表总条目数2。

    地址00000100对应数据位于:    二级表的第00个条目;对应一级表的第1个条目

    两级表结构。二级表总条目数2,一级表总条目数4,共4张二级表。

    地址00000100对应数据位于:    二级表的第0个条目;对应一级表的第10个条目

    多级表的数据查找算法,用三步来概括:与操作->右移->与操作。直到最后找到顶级表的索引为止。

    2 qcow2的L1,L2表是干什么的?

    用户把qcow2看成磁盘,写入数据的时候,用户给qcow2两个东西,一个是存放数据的地址,一个是要存放的数据;读出数据的时候,用户同样给qcow2两个东西,一个是要读取数据的地址,一个是读取的长度。

    这中间,qcow2怎么存放这个数据,对用户来说都无所谓,只要用户取数据的时候,qcow2能根据用户传入的地址找到用户数据即可。那么问题来了,qcow2是像普通青年一样,用一张表来存放用户的数据(如图1),还是走文艺风,用多级表来存放用户的数据?qcow2采用了后者,使用L1,L2和cluster三张表管理用户数据。cluster表中每个条目存放用户数据,L2表条目存放cluster的地址,L1表条目存放L2表的起始地址,这里的地址指的是qcow2文件内的偏移,根据这个地址可以在qcow2文件内找到用户数据。L1,L2和cluster表一起形成三级表,通过“与操作->右移->与操作”的算法可以索引到用户数据。

    因此,L1,L2表不能和cluster独立开来,这三张表一起,作为索引表,用于查找用户数据,解决的是地址存储的问题。

    3 怎么根据L1,L2和cluster表查找用户数据?

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

中的有索引算法介绍:

368 Given a offset into the virtual disk, the offset into the image file can be

369 obtained as follows:

370

371     l2_entries = (cluster_size / sizeof(uint64_t))

372

373     l2_index = (offset / cluster_size) % l2_entries

374     l1_index = (offset / cluster_size) / l2_entries

375

376     l2_table = load_cluster(l1_table[l1_index]);

377     cluster_offset = l2_table[l2_index];

378

379     return cluster_offset + (offset % cluster_size)

    可以做如下解释:

    a 用户地址同cluster表的条目数做&&操作,得到数据在cluster表中的索引。这里我们认为1byte就是一个条目,如果cluster大小为cluster_size byte,那条目数就是cluster_size。如下:

        offset % cluster_size    

    b 用户地址右移N bit (cluster_size = 2 的N次幂)得到新的地址offset_new。如下:

        offset / cluster_size

    c  新地址同L2表的条目数做&&操作,得到在L2表中的索引,对应的cluster地址指向存放用户数据的cluster。如下:

        l2_index = (offset / cluster_size) % l2_entries

    d  新地址右移M bit(l2_entries = 2的M次幂)得到下一轮操作的地址offset_new_next。如下:

        (offset / cluster_size) / l2_entries = offset_new_next

    e L1表的条目分配不受限制,所以可以认为条目数无穷大,offset_new_next对L1表条目数&&操作就是它本身。如下:

        l1_index = (offset / cluster_size) / l2_entries

    f 通过L1表索引找到L2表地址,通过L2表索引找到cluster地址:

        l2_table = load_cluster(l1_table[l1_index])

        cluster_offset = l2_table[l2_index]

    g 根据cluster地址和cluster中的偏移得到用户数据在一个字节上的内容:

        cluster_offset + (offset % cluster_size)

    4 用户读写qcow2磁盘的单位是字节,但qcow2管理数据的单位是cluster,怎么理解?

    每个L2表条目记录一个cluster地址,因此一条L2表管理着一个cluster地址的分配。假设cluster大小为256KB(默认值),当用户写入的数据超过一个cluster时,qcow2就会新分配一个cluster用于存取用户数据,L2表就会新增一个条目记录cluster的地址。所以qcow2管理数据的单位是cluster,只有用户写入数据的范围超过一个cluster的大小,qcow2才分配新的数据空间。

    L2表大小是一个cluster,一旦被分配所有条目的存储空间就固定下来,表的每条对应一个cluster大小的用户地址范围,假设面对一个新的qcow2磁盘,用户往磁盘的0~256kb区间写入了数据,然后在512kb~768kb区间又写了数据。则L2表的内容如下:

    

            图3

    注意:cluster0 address和cluster2 address的值理论上应该是相邻一个cluster。尽管用户在一个很大的磁盘区间前后写入两个数据,qcow2处理时也会把他们紧挨着存放。这就是为什么qcow2磁盘只会一点点增大的原因。

    假设cluster0 address=0x140000,那么cluster2 address=0x140000+0x40000(256KB)=0x180000

    另外,cluster大小可以在创建qcow2磁盘时指定。比如,创建大小5G,cluster大小512KB的qcow2磁盘d:

    qemu-img create -f qcow2 -o cluster_size=524288 d 5G

    5 qcow2的引用计数表和引用计数块是干什么的?

    refcount table:引用计数表;refcount blocks:引用计数块。

    两张表只处理一个问题:cluster的引用计数。如果用一张表,表中每个条目记录一个cluster的引用计数,也可以达到目录,但两张表可以提高索引效率,与用L1,L2 ,cluster表存储用户数据的目的相同。

    引用计数块的每个条目存放了cluster的引用计数,引用计数表存放的是引用计数块的起始地址。

    qcow2为啥要记录cluster的引用计数?

    qcow2要实现快照这个高级特性,怎么实现?通过写时复制(cow),复制对象是cluster数据块。快照的普遍实现原理就是利用cow,在做快照时将cluster标记为只读,后续有写操作时先检查cluster是否只读,如果是就复制一份再写。所以必须有一个标记用来表明cluster是否是只读的,但仅仅是一个标记还不够,因为对同一个qcow2可能快照很多次,重复标记只读对删除快照没有帮助,删除快照时,对于做了多次快照的cluster,qcow2怎么知道哪些cluster需要被真正删除,哪些还在被其它快照引用呢?

    所以简单实用标记来记录只读属性没有用,因此qcow2引入了引用计数表和引用计数块,这两张表用来记录cluster的引用计数。

    cluster引用计数为0:这个cluster没有被使用。

    cluster引用计数为1:这个cluster正在被使用。

    cluster引用计数为2或者以上:这个cluster正在被使用,并且有快照包含了这个cluster,写这个cluster之前需要执行cow。

    有了引用计数这个基础功能,快照这个高级特性才得以实现。

    因此可以说,引用计数表和引用计数块是为了实现qcow2快照而设计的。

    6 怎么根据refcount table和refcount blocks索引用户数据所在cluster的引用计数?

    qcow2文档有以下的介绍:

329 Given a offset into the image file, the refcount of its cluster can be obtained

330 as follows:

331

332     refcount_block_entries = (cluster_size * 8 / refcount_bits)

333

334     refcount_block_index = (offset / cluster_size) % refcount_block_entries

335     refcount_table_index = (offset / cluster_size) / refcount_block_entries

336

337     refcount_block = load_cluster(refcount_table[refcount_table_index]);

338     return refcount_block[refcount_block_index];

    a 算出用户数据处于第几个cluster上,得到结果N,如下:

    (offset / cluster_size)= N

    b N同引用计数块表的条目数做&&操作,得到第N个cluster在引用计数块表中的索引,如下:

    refcount_block_index = N % refcount_block_entries

    c 将N右移Mbit(refcount_block_entries = 2的M次幂),得到用来计算引用计数表索引的新地址N_new,如下:

    N / refcount_block_entries = N_new

    d 引用计数表的条目分配不受限制,所以可以认为条目数无穷大,N_new对引用计数表条目数&&操作就是它本身:

    refcount_table_index = N_new = N / refcount_block_entries

 

qcow2是一种用于磁盘镜像的格式,它采用了row/cow(Read-Only / Copy-On-Write)快照技术。 首先,我们来解释一下row/cow的概念。当创建一个基础镜像时,qcow2使用row模式,即只读模式。这意味着任何修改都不会直接应用在基础镜像上,而是在cow镜像中进行。 当需要对镜像进行修改时,会创建一个cow镜像作为基础镜像的副本。cow镜像会记录所有修改的数据块。在读取数据时,qcow2首先检查cow镜像中是否存在所需的数据块,如果存在,则直接从cow镜像读取。如果不存在,则从基础镜像中读取。 当需要保存修改后的镜像时,qcow2会将cow镜像中的数据块合并到基础镜像中,这样就完成了修改的永久保存。 使用row/cow快照技术的好处是节省了存储空间。由于只记录了修改的数据块,所以cow镜像通常只占用少量空间,而且可以共享基础镜像的不变部分。这意味着可以同时创建多个cow镜像,在相同的基础镜像上进行不同的修改,并且只需要存储修改的差异部分,节省了存储空间。 此外,row/cow快照技术还具有更好的性能。由于只有数据块发生变化的部分需要读写,因此可以大大提高传输速度和磁盘操作效率。 总之,qcow2的row/cow快照技术通过读取基础镜像和cow镜像的方式,实现了节省存储空间和提高性能的效果。它在虚拟化环境中被广泛应用,并且有利于提高资源利用率和系统性能。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

享乐主

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值