1、qcow2檔案分布
對記憶體不了解的可跳過此部分
MMU虛拟位址和實體位址,TLB(Translation Lookaside Buffer)
4k一個page,需要一個位址存放,4K-->4B,4G-->4MB,100萬個頁, 100個程序需要400M
将頁表(一級頁表)分為 1024 個頁表(二級頁表),每個表(二級頁表)中包含 1024 個「頁表項」
頁表一定要覆寫全部虛拟位址空間,不分級的頁表就需要有 100 多萬個頁表項來映射,
而二級分頁則隻需要 1024 個頁表項
二級分頁再推廣到多級頁表
專門存放程式最常通路的頁表項的 Cache,這個 Cache 就是 TLB(Translation Lookaside Buffer)
qcow2檔案資料和TLB類似,L1,L2和cluster表一起形成三級表
目的:通過位址快速找到資料
cluster表中每個條目存放使用者資料,L2表條目存放cluster的位址,L1表條目存放L2表的起始位址,
這裡的位址指的是qcow2檔案内的偏移,根據這個位址可以在qcow2檔案内找到使用者資料。
2、結構
qemu-5.1.0/block/qcow2.h
typedef struct QCowHeader {
uint32_t magic; //QCOW magic string ("QFI\xfb")
uint32_t version; //Version number
uint64_t backing_file_offset; //backing file name
uint32_t backing_file_size; //less than 1024 bytes
uint32_t cluster_bits; //1 << cluster_bits is the cluster size, < 2M
uint64_t size; //Virtual disk size in bytes.
//L1 table limit of 32MB, with 2M cluster 2Eb, 512 byte 128GB
//L1/L2 layouts limit 64 PB
uint32_t crypt_method; //0 for no encryption, 1 for AES, 2 for LUKS
uint32_t l1_size;
//Number of entries in L1 table
uint64_t l1_table_offset; //active L1 talbe starts, aligned to a cluster
uint64_t refcount_table_offset; //the refcount table starts, aligned to a cluster
uint32_t refcount_table_clusters; //Number of clusters that the refcount table occupies
uint32_t nb_snapshots; //Number of snapshots contained in the image
uint64_t snapshots_offset; //snapshot table starts
uint64_t incompatible_features; //
uint64_t compatible_features; //
uint64_t autoclear_features; //https://git.qemu.org/?p=qemu.git;a=blob;f=docs/interop/qcow2.txt
uint32_t refcount_order; //Describes the width of a reference count block entry
uint32_t header_length; //Length of the header structure in bytes
uint8_t compression_type; //0: zlib, 1: zstd
uint8_t padding[7]; //
} QEMU_PACKED QCowHeader;
###偏移位址計算(從客戶機的磁盤裝置虛拟偏移位址轉換為主控端的鏡像檔案中的真實偏移位址)
Given an offset into the virtual disk, the offset into the image file can be obtained as follows:
// 每個cluster包含的L2表個數
l2_entries = (cluster_size / sizeof(uint64_t));
// offset在L2中的索引
l2_index = (offset / cluster_size) % l2_entries;
// offset所屬的L2在L1中的索引
l1_index = (offset / cluster_size) / l2_entries;
// 從L1中擷取L2的起始位址”并加載到記憶體
l2_table = load_cluster(l1_table[l1_index]);
// 從offset所在的L2中擷取所在簇的起始位址
cluster_offset = l2_table[l2_index];
// offset在鏡像中的真實位址 = 簇起始位址 + 簇内偏移
return cluster_offset + (offset % cluster_size);
###偏移位址所在簇的refcount擷取
每個cluster都含有2位元組的引用計數表,每個表項描述一個“refcount塊”的起始位址
每個refcount塊占用一個cluster,每個表項的大小為refcount_bit(qcow2必須為16)
// 每個refcount塊容納的refcount表項個數
refcount_block_entries = (cluster_size * 8 / refcount_bits)
// offset所在的refcount塊索引
refcount_block_index = (offset / cluster_size) % refcount_block_entries
// offset所在refcount塊在refcount表中索引
refcount_table_index = (offset / cluster_size) / refcount_block_entries
// 加載offset所在的refcount塊到記憶體
refcount_block = load_cluster(refcount_table[refcount_table_index]);
// 獲得offset所在簇的refcount值
return refcount_block[refcount_block_index];
與操作->右移->與操作。直到最後找到頂級表的索引,這裡的位址指的是qcow2檔案内的偏移
3、預配置設定(preallocation)政策
舊版本,不準确
preallocation setting time to create phy size
off 0.312s 196K
metadata 0.507s 844K
full 39.402s 4.0G
falloc 0.015s 4.0G
For this test each virtual disk is mounted and written to using dd
preallocation setting time to create MB/s
off 184.23s 729kB/ s
metadata 85.87s 1.6MB/ s
falloc 100.77s 1.3MB/ s
full 84.31s 1.6MB/ s
4、計算
鏡像的大小計算 https://my.oschina.net/LastRitter/blog/1542075
cluster_size -- 簇大小,預設為65536位元組(64K)
total_size -- 要配置設定的鏡像大小
refcount_bits -- refcount占用bit數,qcow2必須為16
按位元組對齊的鏡像大小
aligned_total_size = align_offset(total_size, cluster_size);
Header 大小
header_size = cluster_size;
L2表項個數
l2_num = aligned_total_size / cluster_size;
l2_num = align_offset(l2_num, cluster_size / sizeof(uint64_t));
L2表大小
l2_size = l2_num * sizeof(uint64_t);
L1表項個數
l1_num = l2_num * sizeof(uint64_t) / cluster_size;
l1_num = align_offset(l1_num, cluster_size / sizeof(uint64_t));
L1表大小
l1_size = l1_num * sizeof(uint64_t);
每個refcount的大小,以及一個refcount塊包含的refcount個數
refcount_size = refcount_bits / 8;
refcount_num = cluster_size / refcount_size;
refcount塊個數
refcount_block_num = (aligned_total_size + header_size + l1_size + l2_size) /
(cluster_size – refcount_size - refcount_size * sizeof(uint64_t) / cluster_size)
refcount塊大小
refcount_block_size = DIV_ROUND_UP(refcount_block_num, refcount_num) * cluster_size;
refcount表個數
refcount_table_num = refcount_block_num / refcount_num;
refcount_table_num = align_offset(refcount_block_num, cluster_size / sizeof(uint64_t));
refcount表大小
refcount_table_size = refcount_table_num * sizeof(uint64_t);
總大小
使用預設參數(簇大小為65526位元組,8位元組位址,refcount為16位,2位元組),
不考慮位元組對齊,不考慮快照的情況下,鏡像為10G的近似計算如下:
t = total_size;
c = cluster_size;
header_size = c;
l2_size = t/c * 8;
l1_size = t/c / (c/8) * 8;
rb_size = t/c * 2;
rt_size = t/c / (c/2) * 8;
image_size ≈ t + c + t/c * 8 + t/c / (c/8) * 8 + t/c * 2 + t/c / (c/2) * 8
= t + c + t/c*10 + t/(c*c)*80
≈ 10G + 1.63M
是以使用預設參數時,中繼資料的大小大概隻占磁盤資料大小的0.02%不到。新配置設定快照時,
初始需要的空間為當時的L1表大小,約為160位元組,幾乎可以忽略不計
(在修改後,所需空間會大幅增長,具體跟寫入的量有關)。
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、快照
複制一份L1表,将所有已被配置設定的L1和L2表項的最高位置位0,以及對應的refcount計數加1;
在快照表中新配置設定個表項,把L1屬性指向新建立的L1表,Header中的nb_snapshots加1;
當向鏡像寫入資料時,對應的L1或者L2表項為0,對應的refcount大于或等于2,
則需要重新配置設定對應的L2表項和簇,并讓L1表項指向新的L2表,L2表項執行新配置設定的簇。
簇配置設定(不預先配置設定時的算法,寫入時進行配置設定,讀取時不配置設定,直接填充0)
當要向偏移位址offset處寫入資料時,從Header的l1_table_offset中獲得L1表的起始偏移位址;
用前偏移位址offset的前“64 - l2_bits - cluster_bits“位作為索引從L1表中擷取對應L2表的描述符
(l2_bits為每個簇中儲存的L2表項個數,cluster_bits為每個簇大小的比特數);
如果L2表的表述符的最高位為0(未被配置設定或者是COW),則需要新配置設定L2表;
從refcount表和refcount塊中查找2塊未被使用的簇,标記為1;
初始化第一個簇為L2表,并使用它的起始位址初始化對應的L1表項;
初始化第二個簇,并使用它的位址初始化對應的L2表項;
使用第二個簇的首位址加上偏移位址的簇内偏移得出其真實位址;
在此位址進行資料寫入。