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