liwen01 2024.06.23
前言
在嵌入式Linux裝置中,經常使用jffs2檔案系統來作為引數區的檔案系統格式。至於為什麼要使用jffs2來作為引數區的檔案系統,我猜大部分人都沒有做過多的思考。
jffs2在2001年被設計出來,距今已過二十多年,現在在嵌入式裝置中它還在被大量使用、說明這套設計本身是沒有問題。
但是,你是否有思考過,你的jffs2檔案系統使用是否正確、合理?如果你儲存檔案某天突然不見了,你要怎麼分析?是flash有壞塊,還是被jffs2垃圾回收處理掉了?亦或是應用程式誤刪除了?又要怎樣才能把它恢復回來?
先問幾個問題:
- 如果jffs2系統中資料頻繁更新會有什麼影響?
- 如果jffs2系統分割槽比較大會有什麼影響?
- 如果分割槽全部寫滿有什麼影響?
- 如果出現檔案或是資料丟失,可以恢復回來不?
(一)快閃記憶體檔案系統分類
我們前面介紹的FAT32、exFAT、ext4檔案系統,在快閃記憶體儲存裝置中,它們是透過FTL中間層使它們適用於快閃記憶體。
但是在嵌入式裝置開發中,我們有時候是直接基於快閃記憶體來使用,比如上面提到的,在flash中劃分為一個分割槽來用儲存引數。
jffs 有三個版本,jffs1出來後一兩年就被jffs2替代了,而jff3好像是有被定義,但是還未實現。
jffs1與jffs2 並不相容,基本上屬於重新實現,它們都是基於linux作業系統,flash儲存介質的一種檔案系統。雖然支援移植,但並未看到Linux系統之外的其它系統有在使用jffs檔案系統。
關於儲存介質、檔案系統、分割槽、格式化等內容,可以檢視前面的文章。
檔案系統(一):儲存介質、原理與架構 檔案系統(二):分割槽、格式化資料結構 檔案系統(三):嵌入式、計算機系統啟動流程與步驟 檔案系統(四):FAT32檔案系統實現原理 檔案系統(五):exFAT 檔案系統原理詳解 檔案系統(六):一文看懂linux ext4檔案系統工作原理 檔案系統(七):檔案系統崩潰一致性、方法、原理與侷限(二)JFFS1介紹
JFFS 檔案系統是2000年由 Axis Communications針對nor flash 設計的一個日誌檔案系統(Log-structured File System)。
它是基於日誌檔案系統(LFS)原理設計的一款檔案系統,關於LFS可以檢視文章:《檔案系統(七):檔案系統崩潰一致性、方法、原理與侷限》
(1)資料儲存
第一版本的JFFS是一個純日誌結構檔案系統,包含資料和後設資料的節點,按順序儲存在快閃記憶體晶片上,嚴格線性地遍歷可用的儲存空間。
掛載時系統會掃描整個儲存介質,讀取並解釋每個節點。原始節點中儲存的資料提供了足夠的資訊來重建整個目錄層次結構和每個inode在介質上的資料範圍的物理位置的完整對映。
(2)垃圾回收
採用日誌檔案系統,隨著資料的增、刪、改操作,jffs檔案系統的空間會被慢慢使用完,這個時候就需要啟動垃圾回收機制了。
在jffs1系統中,垃圾回收也是完全按照線性規則來回收,大致步驟如圖2.2 jffs1資料回收:
狀態1:資料按序儲存。這個時候並未開始垃圾回收
狀態2:開始垃圾回收,將最早節點中的有效資料移動到後面,標記原來資料為無效資料(髒資料)
狀態3:重複狀態2操作,直到髒資料空間達到可擦除的最小單位。
狀態4:將髒資料擦除,標記為空。
(3)缺點
從上面的資料分佈和垃圾回收機制,我們可以看出jffs v1版本的實現存在一些嚴重缺陷:
- 垃圾回收線性進行,透過寫入新節點以允許它擦除日誌中最舊的塊,即使被垃圾回收的塊僅包含乾淨的節點。
- 如果檔案系統中存在大量的靜態資料、垃圾回收的時候也會移動所有的靜態資料,雖然每個塊被擦除的次數完全相同、但這也意味著塊被擦除的次數比實際需要的要多。
- JFFS 不支援壓縮,在資源緊張的嵌入式系統中,這是一個比較重要的需求
針對jffs1中的缺陷,就有了jffs的第二個版本,也就是jffs2。
(三)JFFS2 資料佈局
(1)製作jff2映象檔案
- 建立測試目錄和檔案,在file1-4中隨意輸入一些資料
- 製作映象檔案
mkfs.jffs2 -s 0x100 -e 0x10000 -p 0x100000 -d jffs2_fs -o jffs2.img
- s表示頁大小,一般頁大小為256Byte,即0x100
- e表示擦除塊大小,一般塊大小為64KB,即0x10000
- p表示分割槽大小,在生成時會擦除分割槽大小的flash,x100000表示1MB
(2)載入jffs2映象檔案到PC機上
- 載入MTD塊裝置模組,使得MTD裝置能夠透過塊裝置介面進行訪問
sudo modprobe mtdblock
- 載入記憶體裝置模組,並配置虛擬的記憶體技術裝置
sudo modprobe mtdram total_size=1024 erase_size=64
modprobe
: 載入指定的核心模組,並自動處理模組之間的依賴關係。mtdram
: 需要載入的核心模組名稱,表示記憶體裝置的模擬。total_size=1024
: 指定虛擬MTD裝置的總大小,單位KB,這裡總大小為1MB。erase_size=64
: 指定虛擬MTD裝置的擦除塊大小,單位KB,這裡擦除塊大小為64KB。
- 將映象檔案複製到mtdblock0節點上
sudo dd if=jffs2.img of=/dev/mtdblock0
- 將映象檔案以jffs2型別掛載到指定目錄
sudo mount -t jffs2 /dev/mtdblock0 /home/biao/test/jffs2/jffs2_simulator/
(3)檢視分割槽資訊
- 使用jffs2dump檢視jffs映象資訊
- 檢視mtdblock0裝置節點上的資訊
可以看到/dev/mtdblock0裝置節點上的資料與jffs2.img映象檔案上的資料資訊是一致的。
不同的是,mtdblock0是實際分配的1M空間,除了有效資料空間外,其它都是空閒地址(Empty space)
(四)JFFS2資料解析
(1)檢視block資料
- 使用hexdump檢視mtdblock0節點前面2KB的資料,資料如下(...中為省略部分)
(2)資料結構定義
magic: 魔術數字,用來標識是一個有效的JFFS2項
nodetype: 節點型別,在jffs2.h中有定義7種型別
nodetype對應的值如下:
詳細的定義可以檢視mtd-utils/include/linux/jffs2.h
資料結構
所有的節點,都是以jffs2_unknown_node資料結構開始:幻數、型別、長度、CRC校驗,定義如下:
struct jffs2_unknown_node
{
/* All start like this */
jint16_t magic;
jint16_t nodetype;
jint32_t totlen; /* So we can skip over nodes we don't grok */
jint32_t hdr_crc;
} __attribute__((packed));
另外:
- 目錄資訊儲存在
jffs2_raw_dirent
結構體中 - 檔案中的實際資料資訊儲存在
jffs2_raw_inode
結構體中
結構體的詳細定義,可以在mtd-utils中的原始碼中找到
(3)目錄、檔案解析
按上面hexdump檢視的mtdblock0 RAW資料進行解析,可以發現,解析的資料與jffs2dump中檢視的資訊是一致的。
下面分析的這個test1目錄,是在我們最開始製作映象檔案的時候建立的目錄。
這裡需要注意的一點是,在分割槽的最開始,是一個jffs2_unknown_node資料結構頭,它的節點型別是JFFS2_NODETYPE_CLEANMARKER,表示清理標記節點,用於指示塊已被擦除,可以寫入新資料。
接下來的是test1目錄的目錄項節點和它的inode節點。它的inode節點裡面據段的數值是空,並沒有攜帶資料塊。
其它的幾個目錄test2、test3、test4資料結構也是類似。
檔案解析
下面分析的這個檔案,是製作映象檔案時建立的file1檔案,裡面存有18個位元組的a字串,檔案資訊如下:
下面根據hexdump中檢視的mtdblock0 RAW資料對file1檔案進行解析,如下表
與test1目錄不同,file1 有攜帶資料。上面表格中compr 欄位表示資料的壓縮型別。
資料壓縮型別定義如下:
#define JFFS2_COMPR_NONE 0x00
#define JFFS2_COMPR_ZERO 0x01
#define JFFS2_COMPR_RTIME 0x02
#define JFFS2_COMPR_RUBINMIPS 0x03
#define JFFS2_COMPR_COPY 0x04
#define JFFS2_COMPR_DYNRUBIN 0x05
#define JFFS2_COMPR_ZLIB 0x06
#define JFFS2_COMPR_LZO 0x07
與檔案中實際資料對比可以看到,這裡記錄的資料,是將18Byte位元組的18個a壓縮成了4個位元組。
(4)追加資料
使用echo新增資料到file1
echo"bbbbbbbbbbbbbbbbbbb" >> file1
檢視資料變化:
我們看到inode 6 新增加了一個版本記錄version2:
Inode node at 0x0000042c, totlen 0x0000004e, #ino 6, version 2, isize 38, csize 10, dsize 22, offset 16
而inode 6 表示的就是file1檔案。實際追加的資料是記錄在version2 節點中,而原來的18個位元組a資料,還是存在原來version1中的節點。
(5)修改資料
使用echo寫資料到檔案file1
echo"cccc" > file1
資料變化:
從上面我們可以看到:
- file1 的目錄項是沒有變的
- file1 的資料項是有3個修改記錄,也就是version1 - version4
為什麼會有3個修改記錄?實際上是執行上面一個操作它是兩個步驟完成的,也就是一個操作中有了兩個記錄。
version 1 :原始資料,未進行修改
version 2 :是上面執行echo "bbbbbbbbbbbbbbbbbbb" >> file1
命令在file1檔案末尾追加的資料
version 3 :執行echo "cccc" > file1
命令時,是先把file1檔案資料全部清空
version 4 :執行echo "cccc" > file1
命令時,把cccc字元寫入到file1檔案中的記錄
標記資料無效上面我們看到執行4個記錄之後,最後檔案中的資料是"cccc",但是之前的資料要怎麼處理呢?是直接刪除回收還是怎麼處理呢?
我們看到version 1 - version 3 的前面,有標記為 Obsolete Inode,它表示為一個過時的節點,也就是一個未知的節點,這個節點是不能夠被掛載解析的。
它在flash中實際的資料又有哪些變化呢?
使用hexdump檢視version 1 中的RAW資料
對比原始資料,只有一個位元組改變,nodetype
由原來的0xE002 改為了0xC002
0xC000的定義如下:
/* Compatibility flags. */
#define JFFS2_COMPAT_MASK 0xc000 /* What do to if an unknown nodetype is found */
(6)資料壓縮
下面我們往file1中一次寫入256K的0資料,看資料分佈會怎麼變化
dd if=/dev/zero of=file1 bs=256K count=1
執行第一遍結果如下:
執行到第二十遍結果如下:
實際寫入資料有256K*20 = 5120KB = 5M
為什麼實際寫入資料5M,但是MTD的空間只使用到0x0005ff64,也就是383K的空間呢?
因為我們使用的是dd if=/dev/zero of=file1 bs=256K count=1
命令寫入的資料都是0,全是0的資料是很容易壓縮的,這338K空間實際是壓縮後的使用空間。
看資料:
csize 32, dsize 4096,
實際資料4096位元組,壓縮後變成了32位元組。
(7)垃圾回收
在最前面製作映象檔案掛載虛擬MTD裝置的時候,我們分配的大小是1M空間,理論上我們操作的資料記錄超過1M就一定會進行垃圾回收,實際是不是這樣呢?
上面我們寫的全0資料是很容易壓縮,所以實際儲存的資料要比檔案小很多。這裡我們寫入隨機數,讓資料記錄快速寫滿整個分割槽空間,看jffs2是如何進行垃圾回收的。
dd if=/dev/urandom of=file1 bs=1K count=20
執行第一次,資料是按序分佈
執行第二次,資料開始跳躍分佈,資料分配到0x0000fa00地址就直接跳到0x000d000c位置開始儲存,中間間隔了0xc060c 個地址,也就是769K地址,實際是直接跳到了分割槽的後半段去分配。
第5遍寫入20K資料的時候,記錄資料分佈在0x000c000c-0x000dfc24的地址空間
執行第6次寫入20K隨機資料之後,資料空間分佈在0x000c000c-0x000ccc6c 的地址
從第5次到第6次資料寫入的時候,我們看到,第6次資料寫入的時候,已經對第5次寫入資料的空間進行了回收,所以在第6次寫完資料之後,可以看到實際剩餘的空閒塊比第5次寫完資料還多。
(8)資料結論
從上面的幾個簡單測試中我們可以看出下面幾點:
- 儲存的資料是可以被壓縮的
- 資料記錄不是線性儲存
- 垃圾回收時靜態資料(沒有被修改的檔案資料)不會被移動
- 垃圾回收有可能發生在空餘空間還有很多的情況
實際官方的說法是:
- 100次中有99次是從髒列表中選擇一個塊進行垃圾回收,以獲得最佳效能
- 剩下的1次是從乾淨塊中選擇一個塊,以確保資料在介質上移動並實現磨損均衡
具體詳細的實現邏輯,可以去jffs2的原始碼中查詢。
(五)JFFS2資料恢復
如果在開發或是在裝置使用過程中發現jffs2中的檔案丟失了,或者是裡面的資料丟失了,首先進行的第一步操作就是:停止往檔案系統中寫入任何資料
假設丟失的檔案是上面測試file1檔案
一般的操作流程為:
- 停止往jffs2系統中寫入任何資料
- 將jffs2檔案系統所在的分割槽全部備份一份
- 分析備份jffs2中的資料,看是否能找到file1的目錄節點
- 檢視file1檔案inode的操作版本,按最大版本號開始分析
- 看最後版本的操作時間,分析裝置在該時間段有做什麼操作
- 分析倒數第二版本與最後版本,看最後版本是什麼操作
透過上面方法,可以分析出資料丟失的大概原因,只有最後沒辦法的時候才去懷疑是否flash的扇區損壞了,因為分析flash是否損壞會破壞掉問題現場
如果資料丟失後想恢復回來,在資料還沒有被覆蓋的前提下,理論上是可以被恢復回來,恢復的難度就需要看丟失檔案的具體資料和大小以及被修改的次數了。
(六)JFFS2使用注意事項
透過上面分析,我們大概的瞭解了jffs2檔案系統的工作機制和原理,有幾個使用注意事項需要留意:
- jffs2有磨損平衡,但磨損平衡比較隨機。
- 因為資料是透過節點串起來的,所以它並不適合做大容量的檔案系統,一般不建議超過32M的檔案系統使用jff2
- 儘量避免頻繁地更新jffs2檔案系統裡的資料,一是磨損平衡問題、二是每次修改都會產生新資料記錄(version),不管修改的資料是多是少。少量資料的修改還會存在寫放大的問題。
- 對於低功耗裝置,關機前最好先正確解除安裝jffs2檔案系統,提高檔案系統一致性的保障
- 雖然jffs2是日誌檔案系統,資料丟失或是檔案系統異常有可能被修復,但是對於嵌入式裝置,一般沒有足夠的資源去做修復動作,所以對於關鍵資料的備份顯得尤為重要。
結尾
這裡介紹了嵌入式Linux系統中非常常用的jffs2檔案系統,jffs2檔案系統經過二十多年的驗證是沒有問題的,只是大家在使用的時候需要留意一下它的特性和侷限性,避免造成關鍵資料的丟失。