Nydus bootstrap 檔案解析
這是零零散散的關於 Nydus 學習筆記中的一段,主要是瞭解下 bootstrap 檔案的儲存格式。我們知道 nydus 會建立兩種型別的檔案:
- bootstrap: 儲存檔案系統的元資料
- blob: 儲存資料內容
這裡我們以一個很簡單的例子,來看一下 bootstrap 都儲存了哪些檔案系統的資訊。
環境準備
我們使用的測試例子很簡單,通過 nydus-image create 建立一個 bootstrap,這個 bootstrap 的輸入資料夾內容也很少。
$ mkdir fs $ touch fs/aaa $ date > fs/bbb $ nydus-image create --fs-version 5 --bootstrap output/bootstrap --blob-dir output fs
可以看出,該檔案系統一共有兩個檔案:一個空檔案,一個很簡單的文字檔案。
$ ls -l output/bootstrap -rw-r--r-- 1 vagrant vagrant 8832 Apr 26 06:55 output/bootstrap
bootstrap 檔案解析
我們使用 xxd
命令來輸出 bootstrap 檔案的 16 進位制內容,具體命令如下:
$ xxd -a -e output/bootstrap
xxd
比 hexdump
的優點是可以非常簡單的輸出 little-endian
的內容,即其 -e
選項,rafs super block 儲存的時候使用的就是 little-endian 的位元組順序。 -a
選項用於去除空行(全 0x00 的行)。 -g 1
選項設定一個位元組單獨一個欄位,可以幫助我們計數,預設的話一個欄位 4 個位元組,即一個 u32 。
而且 xxd
輸出預設就是 32 位一個欄位,正好是一個 u32 ,而 RafsV5SuperBlock
中很多資料就是 u32 型別的。該命令輸出結果一行對應 16 個位元組,也就是 4 個 u32 。每列寬度可以通過 -g
選項控制,預設為 2,即 4 個位元組。
下面輸出結果中第一列為地址,後 4 列為二進位制資料,之後部分,比如 SFAR...
為 ASCII 顯示字元,在這裡沒有意義,可以忽略。
RafsV5SuperBlock
先來看看 RafsV5SuperBlock
對應的資料。
第一個 16 位元組
00000000: 52414653 00000500 00002000 00100000 SFAR..... ......
前 16 個位元組,對應 RafsV5SuperBlock 的以下屬性:
-
s_magic: u32
: v5 magic number 是0x5241_4653
。
// rafs/src/metadata/layout/v5.rs const RAFSV5_SUPER_MAGIC: u32 = 0x5241_4653;
-
s_fs_version: u32
: 檔案系統版本,即500
。
// rafs/src/metadata/layout/mod.rs pub const RAFS_SUPER_VERSION_V5: u32 = 0x500;
s_sb_size: u32 s_block_size: u32
第二個 16 位元組
00000010: 00000016 00000000 00000003 00000000 ................
對應兩個屬性:
-
s_flags: u64
: 來自 RafsSuperFlags ,這裡值為 16,具體內容為COMPRESS_LZ4_BLOCK | DIGESTER_BLAKE3 | EXPLICIT_UID_GID
,如何算出來的如下所示:
// rafs/src/metadata/mod.rs const COMPRESS_LZ4_BLOCK = 0x0000_0002; const DIGESTER_BLAKE3 = 0x0000_0004; const EXPLICIT_UID_GID = 0x0000_0010;
-
s_inodes_count: u64
: 這裡值為3,即只有3個inode,一個根檔案,還有兩個普通檔案。
第三個 16 位元組
00000020: 00002000 00000000 00002010 00000000 . ....... ......
對應兩個屬性:
-
s_inode_table_offset: u64
: inode table 所在位置的位移,這裡為 0x2000 = 8192。注意這裡雖然是 u64 ,但是好像00002000 00000000
的順序還是需要以 4 位元組為單位,從右往左讀,而每個 4 位元組則是從左往右讀。 -
s_prefetch_table_offset: u64
: prefetch table 的位移,從相對值來說,比上面的 inode table 的位置 多了 0x10,即 16 個位元組。
第四個 16 位元組
00000030: 00002010 00000000 00000003 00000000 . ..............
對應三個屬性:
s_blob_table_offset: u64 s_inode_table_entries: u32 s_prefetch_table_entries: u32
第5個 16 位元組
00000040: 00000048 00000001 00002058 00000000 H.......X ......
對應三個屬性:
s_blob_table_size: u32 s_extended_blob_table_entries: u32 s_extended_blob_table_offset: u64
第5個 16 位元組及以後
00000050: 00000000 00000000 00000000 00000000 ................
RafsV5SuperBlock
結構體的最後一個屬性如下:
-
s_reserved: [u8; RAFSV5_SUPERBLOCK_RESERVED_SIZE]
我們從這個常量的定義可以看到:
// rafs/src/metadata/layout/v5.rs pub(crate) const RAFSV5_SUPERBLOCK_SIZE: usize = 8192; const RAFSV5_SUPERBLOCK_RESERVED_SIZE: usize = RAFSV5_SUPERBLOCK_SIZE - 80;
Inode Table
整個 super block 佔用 8192 位元組,其中前面我們看到的幾個屬性,佔用 80 個位元組,其餘部分為保留區域,以供擴充套件時使用。
RafsV5SuperBlock
中大部分都是預留的空位置,從下面一行可以看到,地址跳到了 0x2000,也是我們前面看到的 s_inode_table_offset
的值,這也意味著,這行開始的內容是 inode table 的內容。
在看上面的內容之前,我們先看看 inode table 的結構:
// rafs/src/metadata/layout/v5.rs pub struct RafsV5InodeTable { /// Inode offset array. pub data: Vec<u32>, }
注意這裡 inode table 是一個 vector,裡面存的不是 inode 物件,而是 inode 對應的 offset,而 vector 的索引就是 inode 對應的數值,每個 offset 都是 32 位型別,佔用 4 個位元組。而且索引的值為 inode -1 ,這樣 root inode 就儲存在 0 的位置上,一點都不浪費。
00002000: 00000413 00000424 00000435 00000000 ....$...5.......
我們看到了 3 個 offset,分別為 413、424 和 432,每個 offset 之間間隔 11 。這個 offset 是 inode table 元素對應在 bootstrap 檔案中的絕對位置的偏移量,比如 0x413 = 1043,而這個資料結構儲存的位置都是 8 位元組對齊的,所以這個值在儲存的時候,是右位移 3 位的,取出來的時候再左位移 3 位,所以真正的位移值為 1043 * 8 = 8344,也就是 16 進位制的 0x2098,在下面的分析中我們還會看到 inode table 的具體內容。
同理 0x424 對應的偏移為 0x2120,0x432 對應的偏移量是 0x2190。
注意: rafs v5 儲存都有對齊,為 8 位元組。由於在這裡的測試中只有 3 個檔案,所以儲存需要 3 個 u32,但是由於存在 8 位元組對齊的需求,這裡 3 個 u32 是 12 個位元組,所以需要補齊 4 個位元組,所以我們看到 0x2000 那一行最後補齊的全零的 u32。
pub(crate) const RAFSV5_ALIGNMENT: usize = 8;
Blob table
從 0x2010 開始儲存的是 blob table 內容。還是先來看看 blob table 的定義:
// rafs/src/metadata/layout/v5.rs #[derive(Clone, Debug, Default)] pub struct RafsV5BlobTable { /// Base blob information array. pub entries: Vec<Arc<BlobInfo>>, /// Extended blob information array. pub extended: RafsV5ExtBlobTable, }
注意,BlobInfo 結構是在 storage crate 中定義的。
// storage/src/device.rs /// Configuration information for a metadata/data blob object. /// /// The `BlobInfo` structure provides information for the storage subsystem to manage a blob file /// and serve blob IO requests for clients. #[derive(Clone, Debug, Default)] pub struct BlobInfo { /// The index of blob in RAFS blob table. blob_index: u32, /// A sha256 hex string generally. blob_id: String, /// 此處省略其餘屬性 }
前面我們已經看到,s_blob_table_offset 對應的值為 0x2010 ,s_blob_table_size 的值為 0x48 = 72 位元組,也就是儲存位置在 [2010 - 2058),2058 也正是 s_extended_blob_table_offset 的值。這部分資料如下:
00002010: 00000000 00000000 31343261 65373762 ........a241b77e 00002020: 38333362 32373532 63623763 38336231 b3382572c7bc1b38 00002030: 38623561 36393139 36326366 62343062 a5b89196fc26b04b 00002040: 37363666 34313962 63653062 33313137 f667b914b0ec7113 00002050: 37343061 32623835 00000001 00000000 a04758b2........ 00002050: 37343061 32623835 00000001 00000000 a04758b2........
注意上面 2050 這一行我又拷貝了一遍,並在 2058 前添加了 2 個空格來方便識別位置。
這裡我們 blob 的 id 是 a241b77eb3382572c7bc1b38a5b89196fc26b04bf667b914b0ec7113a04758b2
,可以和上面輸出最右側 ASCII 部分內容對照。
而且要注意的是上面的輸出左右對照稍微不太直觀,雖然列是從左到右,但是一列之中是從右到左的順序。比如 31343261
,對應的內容實際是 61323431
,即 a241
。
雖然上面看到的 blob table 的定義很複雜、屬性很多,但是我們的測試足夠簡單。RafsV5BlobTable 儲存到磁碟後,最開始的內容就是 BlobInfo 結構體。
但是在 RafsV5BlobTable 的 store 方法(用於序列化到磁碟的 RafsStore trait 所需)中,我們可以看到,對於每一個 blob info 物件,都會在前面寫兩個 readahead 屬性,這兩個屬性供佔用 8 個位元組,然後才是 blob id 。所以我們可以在上面 bootstrap 內容中,在 8 個自己的全零(預設值即是 0 )後面,才是 blob id。
w.write_all(&u32::to_le_bytes(entry.readahead_offset() as u32))?; w.write_all(&u32::to_le_bytes(entry.readahead_size() as u32))?; w.write_all(entry.blob_id().as_bytes())?;
儘管 BlobInfo 物件屬性很多,但是持久化到磁碟的內容卻不多,主要就上面這 3 個,外加一些對齊的位元組。
下面從 0x2058 開始是 RafsV5ExtBlobTable 的內容。其定義如下:
/// Rafs v5 on disk extended blob information table. #[derive(Clone, Debug, Default)] pub struct RafsV5ExtBlobTable { /// The vector index means blob index, every entry represents /// extended information of a blob. pub entries: Vec<Arc<RafsV5ExtBlobEntry>>, } /// Rafs v5 extended blob information on disk metadata. /// /// RafsV5ExtDBlobEntry is appended to the tail of bootstrap, /// can be used as an extended table for the original blob table. // This disk structure is well defined and rafs aligned. #[repr(C)] #[derive(Clone)] pub struct RafsV5ExtBlobEntry { /// Number of chunks in a blob file. pub chunk_count: u32, pub reserved1: [u8; 4], // -- 8 Bytes pub uncompressed_size: u64, // -- 16 Bytes pub compressed_size: u64, // -- 24 Bytes pub reserved2: [u8; RAFSV5_EXT_BLOB_RESERVED_SIZE], }
其中 RAFSV5_EXT_BLOB_RESERVED_SIZE
的值為 40。
對齊進行持久化的方法如下:
w.write_all(&u32::to_le_bytes(entry.chunk_count))?; w.write_all(&entry.reserved1)?; w.write_all(&u64::to_le_bytes(entry.uncompressed_size))?; w.write_all(&u64::to_le_bytes(entry.compressed_size))?; w.write_all(&entry.reserved2)?;
看完程式碼再來看一下儲存的資料內容。
00002050: 37343061 32623835 00000001 00000000 a04758b2........ 00002060: 00000040 00000000 00000035 00000000 @.......5.......
注意 0x2058 是從上面第 9 個位元組開始的,這裡 chunk_count 的值為 0x00000001,即只有一個 chunk。然後 uncompressed_size 屬性佔用 8 個位元組,在上面兩行中實際是跨行了,包括第一行的後四個和第二行的前四個位元組。其值為 0x40,即 64 位元組。同理,compressed_size 的值為 0x35,即 53 位元組。這也和我們在檔案系統上看到的是一樣的:
-rw-r--r-- 1 vagrant vagrant 53 Apr 26 06:55 a241b77eb3382572c7bc1b38a5b89196fc26b04bf667b914b0ec7113a04758b2
0x2060 行最後的 4 個位元組為保留位元組。
Blob table 之後寫入是 inode 資訊,這些資訊是通過 RafsV5InodeWrapper 結構表示的。Inode 資訊由 3 部分組成:
- Inode 結構體的資料
- xattrs
- Chunk info
我們還是先來看看一些關鍵資料結構的定義。
// rafs/src/metadata/layout/v5.rs pub struct RafsV5InodeWrapper<'a> { pub name: &'a OsStr, pub symlink: Option<&'a OsStr>, pub inode: &'a RafsV5Inode, } // 刪除了該方法的一些內容,主要保留了核心寫入到磁碟的資料 impl<'a> RafsStore for RafsV5InodeWrapper<'a> { fn store(&self, w: &mut dyn RafsIoWrite) -> Result<usize> { let mut size: usize = 0; // 1. 寫入 RafsV5Inode 內容,128 位元組 let inode_data = self.inode.as_ref(); w.write_all(inode_data)?; // 2. 寫入檔名,可變長度 let name = self.name.as_bytes(); w.write_all(name)?; } } pub struct RafsV5Inode { /// sha256(sha256(chunk) + ...), [char; RAFS_SHA256_LENGTH] pub i_digest: RafsDigest, // 32 /// parent inode number pub i_parent: u64, /// from fs stat() pub i_ino: u64, pub i_uid: u32, pub i_gid: u32, pub i_projid: u32, pub i_mode: u32, // 64 pub i_size: u64, pub i_blocks: u64, pub i_flags: RafsV5InodeFlags, pub i_nlink: u32, /// for dir, child start index pub i_child_index: u32, // 96 /// for dir, means child count. /// for regular file, means chunk info count. pub i_child_count: u32, /// file name size, [char; i_name_size] pub i_name_size: u16, /// symlink path size, [char; i_symlink_size] pub i_symlink_size: u16, // 104 // inode device block number, ignored for non-special files pub i_rdev: u32, // for alignment reason, we put nsec first pub i_mtime_nsec: u32, pub i_mtime: u64, // 120 pub i_reserved: [u8; 8], // 128 }
我們繼續對照 dump 出來的 bootstrap 資料來看一下寫入的具體內容。
00002070: 00000000 00000000 00000000 00000000 ................ 00002080: 00000000 00000000 00000000 00000000 ................ 00002090: 00000000 00000000 afbe1b2a 8b68b09e ........*.....h. 000020a0: ac7a3553 ec9df26a 5d07bafa 02097de0 S5z.j......].}.. 000020b0: 4c85264b 5749c4a7 00000000 00000000 K&.L..IW........ 000020c0: 00000001 00000000 000003e8 000003e8 ................ 000020d0: 00000000 000041ed 00000080 00000000 .....A.......... 000020e0: 00000001 00000000 00000000 00000000 ................ 000020f0: 00000002 00000002 00000002 00000001 ................ 00002100: 00000000 00000000 00000000 00000000 ................ 00002110: 00000000 00000000 0000002f 00000000 ......../.......
首先我們肉眼可見的 128 長度的 sha256 摘要,所以很容易定位到 RafsV5Inode 結構的內容。之後的 8 位元組表示 parent inode,這裡為 0。
然後到著看找到 2110 行的 2f
,這就是 ASCII 的 / ,也就是根目錄。再倒著往回數 26 個位元組,到了 i_name_size 屬性,這裡值為 1,即 /
長度為 1,再往上一個屬性,即 i_child_count ,這裡值為 2。其餘屬性這裡就不再詳細介紹。
00002120: b94913af a6a1f9f5 ea4d40a0 49c9dc36 [email protected] 00002130: c925cb9b b712c1ad ca939acc 62321fe4 ..%...........2b 00002140: 00000001 00000000 00000002 00000000 ................ 00002150: 000003e8 000003e8 00000000 000081a4 ................ 00002160: 00000000 00000000 00000000 00000000 ................ 00002170: 00000000 00000000 00000001 00000000 ................ 00002180: 00000000 00000003 00000000 00000000 ................ 00002190: 626767b2 00000000 00000000 00000000 .ggb............ 000021a0: 00616161 00000000 b232f6e2 e21610c0 aaa.......2.....
同樣下一個檔名為 aaa
,也可以從 00616161
看到。
000021a0: 00616161 00000000 b232f6e2 e21610c0 aaa.......2..... 000021b0: efe31e11 2532c9d6 2f8e943d e7082bfe ......2%=../.+.. 000021c0: 81da0118 d1192211 00000001 00000000 .....".......... 000021d0: 00000003 00000000 000003e8 000003e8 ................ 000021e0: 00000000 000081a4 00000040 00000000 ........@....... 000021f0: 00000001 00000000 00000000 00000000 ................ 00002200: 00000001 00000000 00000001 00000003 ................ 00002210: 00000000 00000000 62679767 00000000 ........g.gb.... 00002220: 00000000 00000000 00626262 00000000 ........bbb.....
然後是最後一個檔案 bbb
,也可以從 00626262
看到。它的父資料夾為 1,檔案大小為 0x40,這裡應該是壓縮後的檔案大小了。
最後是 chunk 資訊。我們來看看它的結構:
// rafs/src/metadata/layout/v5.rs pub struct RafsV5ChunkInfo { /// sha256(chunk), [char; RAFS_SHA256_LENGTH] pub block_id: RafsDigest, // 32 /// blob index. pub blob_index: u32, /// chunk flags pub flags: BlobChunkFlags, // 40 /// compressed size in blob pub compress_size: u32, /// uncompressed size in blob pub uncompress_size: u32, // 48 /// compressed offset in blob pub compress_offset: u64, // 56 /// uncompressed offset in blob pub uncompress_offset: u64, // 64 /// offset in file pub file_offset: u64, // 72 /// chunk index, it's allocated sequentially and starting from 0 for one blob. pub index: u32, /// reserved pub reserved: u32, //80 }
可以看出,它的長度是 80 個位元組,對應檔案的最後這部分:
00002230: ec5944de 690964ef 8274f1bf 7cf30f7c .DY..d.i..t.|..| 00002240: 62fc5b93 d8d8c7a0 3272f74b b91db007 .[.b....K.r2.... 00002250: 00000000 00000001 00000035 00000040 ........5...@... 00002260: 00000000 00000000 00000000 00000000 ................ 00002270: 00000000 00000000 00000000 00000000 ................
上面只是簡單分析了下 bootstrap 檔案的內容,至於 blob 資料檔案,則留待以後有時間再看了。