執行人: HotMercury, jason50123
專題解說影片
為了探索 Linux VFS (virtual file system) 介面及檔案系統實作機制,我們從無到有撰寫一個運作於 Linux 核心模式中的精簡檔案系統,原始程式碼約一千餘行,支援基本的檔案和目錄處理,同時也考慮到權限和並行處理的議題。本任務預計整合 jbd2,使得 simplefs 具備 journaling 特徵。
相關資訊:
在 linux v6.8 下編譯。
我們發現在新版 kernel 中 makefile 會強迫檢查 missing-prototypes
所以必須加上 prototype define 或是在 function 前面加上 static
此專案中有大量 i_atime
以及 i_mtime
但根據 commit 12cd440 可以知道說這個變數從 v6.7-rc1
版本開始,為了不讓 user 存取,就更新成 __i_atime
和 __i_mtime
,並提供了相關的 function 來初始化這些變數。
根據 commit 17bf23a 可以知道從 v6.8-rc1
版本以後。 block_write_full_page
已經由 block_write_full_folio
所取代,因此對程式碼做對應的修改。
TODO: 理解為什麼要從 page
換成 folio
VVSFS 參照 simplefs 的設計,其報告 Group17_report.pdf
值得學習,可作為類似 ext2 的檔案系統實作的參照。本專題可學習 VVSFS,彙整之前的開發紀錄,提供今年度 simplefs 的技術報告。
可從 nullfsvfs 探討起,說明一個沒有實際功能的檔案系統是如何與 Linux VFS 互動。
disk layout 中的 data block 要是有無意義的資訊將會導致,ls
, mkdir
, touch
等 command 出錯。
經過幾次實驗發現問題出自於,simplefs_file
讀取邏輯的問題,當使用 ls
時會尋找有效的 simplefs_file_ei_block
(extent block),再透過 simplefs_file_ei_block
找到 simplefs_file
(紀錄 name 跟 inode) 且終止條件為找到 0, 但如果在分配 file block 時沒有清空,就會發現後面的資訊都是無意義的,因此無法正確執行。
以下為 simplefs_file
所在的 block layout 可以看到未使用的地方都是亂數,這樣無法正確的分析資料結構,更可能造成無法結束的狀況。
sizeof(simplefs_file) -> 259bytes
目前採用 2 的方式
建立一個目錄的流程
simplefs_create
建立一個目錄,接下來透過 simplefs_new_inode
呼叫 simplefs_iget
設定對應的 inode number 以及 extent block 的資訊。
所以我們要在這裡將分配到的 block 都清空。
過往要偵測 Linux 核心模組是否存在記憶體洩漏 (memory leak),往往仰賴 kmemleak 一類的機制,不過通常需要重新編譯 Linux 核心,現在 kmodleak 藉由 eBPF 動態安插檢查程式碼到 Linux 核心,於是就可在不用重新編譯 Linux 核心的狀況下,動態檢測指定 Linux module ?
基本上照著 github 指示安裝,但是在 make
發生錯誤,這是 #pragma GCC poison 的錯誤
注意要用較新的 clang,如 clang-17,適度修改 Makefile
,以確保使用你安裝的 clang。
clang version,且也將 makefile 改成對應的版本依然會有錯誤,所以暫時註解掉就可以執行,但應該不能這樣。
在 libbpf-bootstrap 有找到這個問題的解法,但我還不知道怎麼解
然後使用後沒有預期的方式執行,暫時無解,目前猜是要對 kernel 做而外的設定。
issue 53 想要解決 memory leak 的問題,因此在底下重現他的實驗
接著使用 issue 53 會變成以下的輸出
接下來要解決的是來不及釋放的 memory
初始化的時候會呼叫 simplefs_init_inode_cache
-> kmem_cache_create_usercopy
可以分配一塊(simplefs_inode_cache 是一個 simplefs_inode_info) memory,為什麼不使用 kmallock 或是其他方法?
記憶體 api
ext2 在 destroy 的 code
rcu_barrier
會出現更多問題。以下為 rcu_barrier
解釋以及 simplefs 改動
Pseudo-code using rcu_barrier() is as follows:
1. Prevent any new RCU callbacks from being posted.
2. Execute rcu_barrier().
3. Allow the module to be unloaded.
@RoyHuang 問題
寫入檔案的時候(dd command), 也有 cache leak 的問題, tracing code 出現在 "block_wirte_begin/ generic_write_end" 這兩個裡面, 請教老師能否給個方向去解決
當我們刪除一個 file 後,會發現 ln -s
失效並出現以下 error,這是 kernel 中的 ELOOP error。
為了找出問題,所以要先確定 rm
, ln -s
做了什麼事,以下是 vfs_symlink
流程
journal filesystem 透過記錄尚未提交到檔案系統主部分的變更來保持檔案系統的完整性。這些變更目標會被記錄在稱為 journal (這裡還不確定是 jbd2 的還是 filesystem 要自己定義)的資料結構中,這通常是一個環形日誌。當系統崩潰或電源故障時,這類 filesystem 可以更快地重新上線,並且降低 filesystem 損壞的可能性。
journal mode
Rationale
我們在這裡舉一個例子並搭配 simplefs 的資料結構說明
刪除一個 file 的流程
simplefs_file
紀錄所有底下 file 或 directory 的資訊,所以必須刪除這項資訊。探討一 : 1->2 間發生 crash,會發生 storage leak,因為我們找不到對應的 inode 資訊,但其資料依然存在,且不可再被使用。
探討二 : 2->3 間發商 crash,inode 可以被釋放但是 block 依然不可使用
所以想要維持一制性,就必須使用 fsck 等工具來將整個 image 掃過一次,非常耗時。
technique
journal 的形式有很多種設計,注意的是內部設計應該也要考慮寫日誌時發生的 crash。
physical journals
將要寫入的 block 都預先複製成 journal,因此 recorvery 時只要將完整的 block 覆蓋或是丟掉未完成的 journal block,但這會造成效能瓶頸。
Logical journals
只記錄 metadata,這樣在速度以及效能上會有明顯的提升,相之而來的風險就是資料可能會是錯誤的,舉例來說 append 一個 file 時我們可以確保 metadata 是完整的但 append 的 block 並未寫入完成,可能會得到不正確的資料。
這個版本的 simplefs 有實作 journal,因此可以當作參考,取其精華,去其糟粕。
選擇 inode 當作 journal 依據或是 new device 擇一,但是在這個版本卻都使用到了,可以在 simplefs_parse_options
選擇方式
simplefs_sb_load_journal
-> jbd2_journal_init_inode
simplefs_load_journal
-> jbd2_journal_init_dev
設計原則
jbd2_journal_start
@journal : Journal to start transaction on
@nblcks : number of block buffer we might modify
開啟一個新的 handle
jbd2_journal_get_write_access
@handle: transaction to add buffer modifications to
@bh: bh to be used for metadata writes
將 buffer head 加入 handle
jbd2_journal_dirty_metadata
@handle: transaction to add buffer to.
@bh: buffer to mark
標記 bh 為已修改
jbd2_journal_stop
@handle: transaction to complete.
完成一個 transaction
jbd2_journal_flush
將 log flush 到原始位置
先描述 journal 相關的 structure 以及在做 journal 時,相關 data block 的順序
journal super block
Journal descriptor block
updated metadata block
commit block
updated metadata block
真正寫到 disk 的時候,會在 commit block
中記錄和 descriptor block
一樣的 sequence number,來表示完成這次 journal
的動作。我們知道完成一個 journal 主要會經過以下步驟
假如說我們今天有一個 file, 他原本只有一個 data block ,但現在變成兩個,所以我們第一步會先去修改他的 inode 以及 block bitmap。
先把 data 寫到 data block
先將上述 metadata 的修改做 journal (這邊目前覺得要存 data、inode bitmaps 以及 inode)
TxB(transection beginning) | inode bitmap | data block bitmap | TxE(transaction ending) |
---|
Checkpointing ,把這些 transaction 裡面存的這些修改過的 metadata block 寫到 disk 的 final location 當中。
在 journal superblock 裡面把這個 transaction 標記為 free
先參考kernel document guide使用 journal 的步驟
journal_t
的資料結構,此資料結構最後會被 jbd2_journal_destroy()
使用,並釋放那些 kernel memoryjbd2_journal_destroy() - Release a journal_t structure.
journal
儲存的地方不同,而會去呼叫不同的 linux kernel function
jbd2_journal_init_inode()
journal
存在 raw device(external journaling) 的話會呼叫 jbd2_journal_init_dev()
但這邊覺得很奇怪,為什麼可以只呼叫上面幾個 jbd 的 api 就可以完成 journal 了,跟我想像中的要以 transaction 為單位來做事不同。
且裡面指出對於 journal 設計應先去參考 ext4_load_journal()
這個 function,而去 trace 的結果得到以下的 function flow
ext4_fill_super
-> ext4_load_and_init_journal
-> ext4_load_journal()
-> ext4_get_journal()
/ext4_get_dev_journal()
ext4_get_journal()
-> ext4_get_journal_inode()
->jbd2_journal_init_inode()
-> ext4_init_journal_params()
ext4_get_dev_journal()
-> ext4_blkdev_get()
-> jbd2_journal_init_dev()
-> ext4_init_journal_params()
可以知道一開始在 ext4_fill_super()
的時候,就要去把 journal 相關的 device 設定好,於是這邊也在 simplefs 中的 super.c 先實作出幾個 function
因為目前還是沒有 trace 到 ext4 他對於每次的 journal 放在 disk 中的 block layout 是怎麼樣,所以還無法在 mkfs 裡面劃分出 journal 的相關區域。
然後裡面的 ext4_set_aops()
又會根據他的 journal 選擇的模式做出要去用哪一類型的 address_space_operation
,但如果是 order mode
、write back mode
就是使用 ext4 原先的 address_space_operations ext4_aops
但我們這邊目前先將 order mode
做出來,所以並不需要提供這樣類似的 function ,如果之後要在提供 data mode
就是要在這邊下手。
然後這邊我們先來看一下在做 write()
的時候,會用到 journal ,所以我們就先來看一下 ext4 在做 write system call 的時候主要的 function flow
write()
-> vfs_write()
-> write_iter()
-> ext4_file_write_iter()
-> generic_file_write_iter()
-> __generic_file_write_iter()
-> generic_perform_write()
-> ext4_write_begin()
-> ext4_writepage()
-> ext4_write_end()
而其中跟 journal 有關的操作則在 ext4_write_begin()
、ext4_write_end()
裡面去完成,所以這邊對這兩個 function 去進行分析
如何使用 external device
如何在 qemu + gdb 的環境中開啟 jbd2 的 debug 以及 ftrace 功能
kernel function tracer
、kernel function graph tracer
打開jbd2
debug 的相關設定,開啟完之後相關的 flag 會像下面得到 CONFIG_JBD2=y
、CONFIG_JBD2_DEBUG=y
如何檢查 diskimg 以及 external journal device 資料是否被寫入
sudo mount rootfs.img /rootfs
test.img
(真正寫 data 的)、journal.img
(journal external device)dirctory 是「目錄」,不是「資料夾」,注意用語!
以下實驗皆在 kernal v6.8 環境下並且以 qemu + gdb 方式進行
external journal device
,並在 mount simplefs filesystem 的時候透過 fill_super
一起掛載好。rootfs.img
並檢查裡面內容從上述實驗結果可以看到
在 external journal device 中,jbd2
都會以 block 為單位來管理,每個 block 都會有一個都會以 12 bytes 的struct journal_header_s
作為起始。
這邊先看到第一個 block 也就是 journal super block (本文後面簡稱 jsb)
而參考文件 journal superblock struct layout
我們可以得知
4 個 byte 是 jbd2 的 magic number
,接著 4 個 byte 會告訴我們是哪一類型的 block ,可以看到他的值為 0004
也就是 Journal superblock, v2
。
在來我們就要看另外一個比較重要的東西就是 Compatible feature
、Incompatible feature set
,因為這兩個的值會關乎到後面 descriptor block
中的 disk layout。
而這個 jsb
會在一開始透過 make journal device
的時候就會把它寫好,接著在 fill_super()
的時候會用 load_journal
來設定對應參數,並把 jsb
從對應位置讀上來並且檢查對應的 magic number
然後把 journal
的相關參數設定好。
而從上面的結果可以發現 Compatible feature
對應的位置 0x24
的值為 0 ,也就代表說 JBD2_FEATURE_INCOMPAT_CSUM_V3 is NOT set
, 所以 descriptor block
中的 descriptor blockdata layout
就會是struct journal_header_s
+ struct journal_block_tag_s
(TODO:改圖示呈現)
然後我們每個 transaction
都會用一個 descriptor block
和commit block
包起來。
接著第二個 block 就是我們的第一個 transaction
我們可以看到這邊的 offeset 0x4: h_block type
的地方 = 1 且 offeset 0x8: h_sequence
= 2 ,代表說第一個 transaction
的 log id
= 2 ,可以看到這個值跟 jsb
裡面 offeset 0x14: s_first
(第一個 transaction
的 id) ,所以表示該 transaction 為整個 journal 的第一筆。
TODO:下面的內容有問題要在修改
然後我們 descriptor block 裡面有設定的 前面12 byte 一樣是 journal header 而後面的則是journal_block_tag_s 而這個tag裡面的內容要先去看 JBD2_FEATURE_INCOMPAT_CSUM_V3 這個 incompat 的 feature 是否有設成這個 flag ,會依照是否有設定而有不同的disk layout
預計會像是 ext3 設計概念有一個 circular log 在 disk 中
disk layout
關於 transaction 這段,文件 中有一段話,我的想法是這個 descriptor 應該要有所有 update 的對應位置資料,但在 kernel 中卻找不到相關的 array 來紀錄,可能跟 open code 有關系但我不太懂 open code 意義。
The descriptor block contains an array of journal block tags that describe the final locations of the data blocks that follow in the journal. Descriptor blocks are open-coded instead of being completely described by a data structure, but here is the block structure anyway. Descriptor blocks consume at least 36 bytes, but use a full block:
descriptor 對應 data struct
整體流程
我們要實作的是 order mode 且要搭配 jbd2 api,先將 file data 的部份寫到對應的 fix 位置,接下來再將 metadata 寫入 log 位置,等確保 log 完成後可以做 check point 將資料從 log 寫入 fix,接著釋放。
jbd2 api 及 實作 function
我們會使用到 jbd2_journal_start
建立一個新的 handle,此時會設定需要的 block 數量,這裡預計會實作 simplefs_meta_trans_block
(這是模仿 ext3 handle = ext3_journal_start(inode,EXT3_DATA_TRANS_BLOCKS(inode->i_sb));
),接下來將 jbd2_journal_get_write_access
綁定對應的 bh,jbd2_journal_dirty_metadata
設為寫入狀態,jbd2_journal_stop
結束 transcation 接下來 jbd2_journal_flush
寫入 log
現在考慮一個議題 journal block 應該要放在哪裡?要有多大的空間?
jbd2_journal_init_inode
首先綁定對應的 inode,所以目前 mkfs 時會設定成以下狀態。
0 | 1 | 2 |
---|---|---|
empty | root | journal |
simplefs_inode_info
,何時設定 i_mode, i_uid 等值? 應該說如果是 simplefs_iget
如果是 new inode,是不是等於多設定了多餘的動作simplefs_iget
write_journal_inode
和 write_journal
來對 patition 進行 journal 相關的初始化動作,但在 ext4
中,如果是用 internal
的 journal ,會是怎麼樣去把 journal 相關的區域先劃分出來,然後整個 disk 的 layout 又是長什麼樣子?因為目前在 Ext4 document 中,僅有看到以下的示意圖
而且在他的 code 中可以看到,無論是用 internal/external 的 device,他的 mkfs 都會是一樣的方式,但這樣的行為我覺得很奇怪,為什麼今天如果是用 external device 去存 journal 的話我們還要在原本的 disk image 去規劃區域給他?
如何知道斷電後哪些 transaction 是沒有真的寫到 disk fix location 中的:
如果記在 journal super block
裡面不可行,因為如果記在 super block 裡面代表說每次在 journal 寫到 fix location 的時候 ,也需要把這個 journal super block
一起更新