文件系統(八):Linux JFFS2文件系統工作原理、優勢與局限
liwen01 2024.06.23
前言
在嵌入式Linux設備中,經常使用jffs2文件系統來作為參數區的文件系統格式。至于為什么要使用jffs2來作為參數區的文件系統,我猜大部分人都沒有做過多的思考。
jffs2在2001年被設計出來,距今已過二十多年,現在在嵌入式設備中它還在被大量使用、說明這套設計本身是沒有問題。
但是,你是否有思考過,你的jffs2文件系統使用是否正確、合理?如果你存儲文件某天突然不見了,你要怎么分析?是flash有壞塊,還是被jffs2垃圾回收處理掉了?亦或是應用程序誤刪除了?又要怎樣才能把它恢復回來?
先問幾個問題:
- 如果jffs2系統中數據頻繁更新會有什么影響?
- 如果jffs2系統分區比較大會有什么影響?
- 如果分區全部寫滿有什么影響?
- 如果出現文件或是數據丟失,可以恢復回來不?
(一)閃存文件系統分類
圖1.1 閃存文件系統層次結構我們前面介紹的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文件系統經過二十多年的驗證是沒有問題的,只是大家在使用的時候需要留意一下它的特性和局限性,避免造成關鍵數據的丟失。

在嵌入式Linux設備中,經常使用jffs2文件系統來作為參數區的文件系統格式。至于為什么要使用jffs2來作為參數區的文件系統,我猜大部分人都沒有做過多的思考。你的jffs2文件系統使用是否正確、合理?如果你存儲文件某天突然不見了,你要怎么分析?是flash有壞塊,還是被jffs2垃圾回收處理掉了?亦或是應用程序誤刪除了?又要怎樣才能把它恢復回來?
浙公網安備 33010602011771號