# JFFS2文件系統簡介
JFFS2 是一個日志結構(log-structured)的文件系統,
包含數據 和 原數據(meta-data)的節點在閃存上順序的存儲。
JFFS2 之所以選擇日志結構的存儲方式,是因為對閃存的更新應該是 out-of-place 的更新方式,而不是對磁盤的 in-place 的更新方式。在閃存上 in-place 更新方式的問題我們已經在閃存轉換層一節描述過了。
2.1 節點頭部定義和兼容性
JFFS2 將文件系統的數據和原數據以節點的形式存儲在閃存上,具體來說節點頭部的定義如下:
圖二
圖二
幻數屏蔽位:0x1985 用來標識 JFFS2 文件系統。
節點類型:JFFS2 自身定義了三種節點類型,但是考慮到文件系統可擴展性和兼容性,JFFS2從 ext2 借鑒了經驗,節點類型的最高兩位被用來定義節點的兼容屬性,具體來說有下面幾種兼容屬性:
JFFS2_FEATURE_INCOMPAT:當 JFFS2 發現了一個不能識別的節點類型,并且它的兼容屬性是 JFFS2_FEATURE_INCOMPAT,那么 JFFS2 必須拒絕掛載(mount)文件系統。
JFFS2_FEATURE_ROCOMPAT:當 JFFS2 發現了一個不能識別的節點類型,并且它的兼容屬性是 JFFS2_FEATURE_ROCOMPAT,那么 JFFS2 必須以只讀的方式掛載文件系統。
JFFS2_FEATURE_RWCOMPAT_DELETE:當 JFFS2 發現了一個不能識別的節點類型,并且它的兼容屬性是 JFFS2_FEATURE_RWCOMPAT_DELETE,那么在垃圾回收的時候,這個節點可以被刪除。
JFFS2_FEATURE_RWCOMPAT_COPY:當 JFFS2 發現了一個不能識別的節點類型,并且它的兼容屬性是 JFFS2_FEATURE_RWCOMPAT_COPY,那么在垃圾回收的時候,這個節點要被拷貝到新的位置。
節點總長度:包括節點頭和數據的長度。
節點頭部 CRC 校驗:包含節點頭部的校驗碼,為文件系統的可靠性提供了支持。
2.2 節點類型
JFFS2 定義了三種節點類型:
JFFS2_NODETYPE_INODE: INODE 節點包含了i-節點的原數據(i節點號,文件的組 ID, 屬主 id, 訪問時間,偏移,長度等),文件數據被附在 INODE 節點之后。除此之外,每個 INODE 節點還有一個版本號,它被用來維護屬于一個i-節點的所有 INODE 節點的全序關系。下面舉例來說明這個全序關系在 JFFS2 的使用:
圖三
圖三
因此,當文件系統從閃存上讀節點信息后,會生成下面的映射信息:
圖四
圖四
根據這個映射信息表,文件系統就知道到相應的 INODE 節點去讀取相應的文件內容。最后要說明的是,JFFS2 支持文件數據的壓縮存儲,因此在 INODE 節點中還包含了所使用的壓縮算法,在讀取數據的時候選擇相應的壓縮算法來解壓縮。
JFFS2_NODETYPE_DIRENT:DIRENT 節點就是把文件名與 i 節點對應起來。在 DIRENT節點中也有一個版本號,這個版本號的作用主要是用來刪除一個 dentry。具體來說,當我們要從一個目錄中刪除一個 dentry 時,我們要寫一個 DIRENT 節點,節點中的文件名與被刪除的 dentry 中的文件名相同,i 節點號置為 0,同時設置一個更高的版本號。
JFFS2_NODETYPE_CLEANMARKER:當一個擦寫塊被擦寫完畢后,CLEANMARKER 節點會被寫在 NOR flash 的開頭,或 NAND flash 的 OOB(Out-Of-Band) 區域來表明這是一個干凈,可寫的擦寫塊。在 JFFS v1 中,如果掃描到開頭的 1K 都是 0xFF 就認為這個擦寫塊是干凈的。但是在實際的測試中發現,如果在擦寫的過程中突然掉電,擦寫塊上也可能會有大塊連續 0xFF,但是這并不表明這個擦寫塊是干凈的。于是我們需要 CLEANMARKER 節點來確切的標識一個干凈的擦寫塊。
2.3 JFFS2節點,擦寫塊在內存中的表示和操作
JFFS2 維護了幾個鏈表來管理擦寫塊,根據擦寫塊上的內容,一個擦寫塊會在不同的鏈表上。具體來說,當一個擦寫塊上都是合法(valid)的節點時,它會在 clean_list 上;當一個擦寫塊包含至少一個過時(obsolete)的節點時,它會在 dirty_list 上;當一個擦寫塊被擦寫完畢,并被寫入 CLEANMARKER 節點后,它會在 free_list 上。
通常情況下,JFFS2 順序的在擦寫塊上寫入不同的節點,直到一個擦寫塊被寫滿。此時 JFFS2 從 free_list 上取下一個擦寫塊,繼續從擦寫塊的開頭開始寫入節點。當 free_list 上擦寫塊的數量逐漸減少到一個預先設定的閥值的時候,垃圾回收就被觸發了,為文件系統清理出更多的可用擦寫塊。為了減少對內存的占用,JFFS2 并沒有把 i 節點所有的信息都保留在內存中,而只是把那些在請求到來時不能很快獲得的信息保留在內存中。具體來說,對于在閃存上的每個 i 節點,在內存里都有一個 struct jffs2_inode_cache 與之對應,這個結構里保存了 i 節點號,指向 i 節點的連接數,以及一個指向屬于這個 i 節點的物理節點鏈表的指針。所有的 struct jffs2_inode_cache 存儲在一個哈希表中。閃存上的每個節點在內存中由一個 struct jffs2_raw_node_ref 表示,這個結構里保存了此節點的物理偏移,總長度,以及兩個指向 struct jffs2_raw_node_ref 的指針。一個指針指向此節點在物理擦寫塊上的下一個節點,另一個指針指向屬于同一個 i-節點的物理節點鏈表的下一個節點。
圖五
圖五
在閃存上的節點的起始偏移都是 4 字節對齊的,所以 struct jffs2_inode_cache 中flash_offset 的最低兩位沒有被用到。JFFS2 正好利用最低位作為此節點是否過時的標記。
下面舉一例來說明 JFFS2 是如何使用這些數據結構的。VFS 調用 iget() 來得到一個 i 節點的信息,當這個 i 節點不在緩存中的時候,VFS 就會調用 JFFS2 的 read_inode() 回調函數來得到 i 節點信息。傳給 read_inode() 的參數是 i 節點號,JFFS2 用這個 i 節點號從哈希表中查找相應的 struct jffs2_inode_cache,然后利用屬于這個 i 節點的節點鏈表從閃存上讀入節點信息,建立類似于表三的映射信息。
2.4 JFFS2 掛載過程
JFFS2 的掛載過程分為四個階段:
1) JFFS2 掃描閃存介質,檢查每個節點 CRC 校驗碼的合法性,同時分配了 struct jffs2_inode_cache 和 struct jffs2_raw_node_ref
2) 掃描每個 i 節點的物理節點鏈表,標識出過時的物理節點;對每一個合法的 dentry 節點,將相應的 jffs2_inode_cache 中的 nlink 加一。
3 找出 nlink 為 0 的 jffs2_inode_cache,釋放相應的節點。
4 釋放在掃描過程中使用的臨時信息。
2.5 JFFS2 垃圾回收機制
當 free_list 上的擦寫塊數太少了,垃圾回收就會被觸發。垃圾回收主要的任務就是回收那些已經過時的節點,但是除此之外它還要考慮磨損平衡的問題。因為如果一味的從 dirty_list上選取擦寫塊進行垃圾回收,那么 dirty_list 上的擦寫塊將先于 clean_list 上的擦寫塊被磨損壞。JFFS2 的處理方式是以 99% 的概率從 dirty_list,1% 的概率從 clean_list 上取一個擦寫塊下來。由此可以看出 JFFS2 的設計思想是偏向于性能,同時兼顧磨損平衡。對這個塊上每一個沒有過時的節點執行相同的操作:
1 找出這個節點所屬的 i 節點號(見圖五)。
2 調用 iget(),建立這個 i 節點的文件映射表。
3 找出這個節點上沒有過時的數據內容,并且如果合法的數據太少,JFFS2 還會合并相鄰的節點。
4 將數據讀入倒緩存里,然后將它拷貝到新的擦寫塊上。
5 將回收的節點置為過時。
當擦寫塊上所有的節點都被置為過時,就可以擦寫這個擦寫塊,回收使用它。
回頁首
3. JFFS2 的不足之處
3.1 掛載時間過長
JFFS2 的掛載過程需要對閃存從頭到尾的掃描,這個過程是很慢的,我們在測試中發現,掛載一個 16M 的閃存有時需要半分鐘以上的時間。
3.2 磨損平衡的隨意性(random nature)
JFFS2 對磨損平衡是用概率的方法來解決的,這很難保證磨損平衡的確定性。在某些情況下,可能造成對擦寫塊不必要的擦寫操作;在某些情況下,又會引起對磨損平衡調整的不及時。
3.3 很差的擴展性
JFFS2 中有兩個地方的處理是 O(N) 的,這使得它的擴展性很差。
首先,掛載時間同閃存的大小,閃存上節點數目成正比。
其次,雖然 JFFS2 盡可能的減少內存的占用,但通過上面對 JFFS2 的介紹我們可以知道實際上它對內存的占用量是同 i 節點數和閃存上的節點數成正比的。
因此在實際應用中,JFFS2 最大能用在 128M 的閃存上。
回頁首
4. JFFS2 的新特性
最近加入到 JFFS2 中的兩個補丁程序分別解決了上面提到的掛載時間過長和磨損平衡隨意性的問題。
4.1 磨損塊小結補丁程序(erase block summary patch)
這個補丁程序最基本的思想就是用空間來換時間。具體來說,就是將每個擦寫塊每個節點的原數據信息寫在這個擦寫塊的最后,當 JFFS2 掛載的時候,對每個擦寫塊只需要讀一次來讀取這個小結節點,因此大大減少了掛載時間。使用了磨損塊小結補丁程序,一個擦寫塊的結構就像下面這樣:
圖六
圖六
根據我們的測試,使用磨損塊小結補丁程序,掛載一個 12M 的閃存需要 2~3 秒,掛載一個 16M 的閃存需要 3~4 秒。
4.2 改進的磨損平衡補丁程序
這個補丁程序的基本思想是,記錄每個擦寫塊的擦寫次數,當閃存上各個擦寫塊的擦寫次數的差距超過某個預定的閥值,開始進行磨損平衡的調整。調整的策略是,在垃圾回收時將擦寫次數小的擦寫塊上的數據遷移到擦寫次數大的擦寫塊上。這樣一來我們提高了磨損平衡的確定性,我們可以知道什么時候開始磨損平衡的調整,也可以知道選取哪些擦寫塊進行磨損平衡的調整。
4.3 擦寫塊頭部補丁程序
在寫改進的磨損平衡補丁程序的過程之中,我們需要記錄每個擦寫塊的擦寫次數,這個信息需要記錄在各自的擦寫塊上。可是我們發現 JFFS2 中缺少一種靈活的對每個擦寫塊的信息進行擴展的機制。于是我們為每個擦寫塊引入了擦寫塊頭部(header),這個頭部負責紀錄每個擦寫塊的信息(比如說擦寫次數),并且它提供了靈活的擴展機制,將來如果有新的信息需要記錄,可以很容易的加入到頭部之中。
回頁首
5. JFFS3 簡介
雖然不斷有新的補丁程序來提高 JFFS2 的性能,但是不可擴展性是它最大的問題,但是這是它自身設計的先天缺陷,是沒有辦法靠后天來彌補的。因此我們需要一個全新的文件系統,而 JFFS3 就是這樣的一個文件系統,JFFS3 的設計目標是支持大容量閃存(>1TB)的文件系統。JFFS3 與 JFFS2 在設計上根本的區別在于,JFFS3 將索引信息存放在閃存上,而 JFFS2將索引信息保存在內存中。比如說,由給定的文件內的偏移定位到存儲介質上的物理偏移地址所需的信息,查找某個目錄下所有的目錄項所需的信息都是索引信息的一種。 JFFS3 現在還處于設計階段,文件系統的基本結構借鑒了 Reiser4 的設計思想,整個文件系統就是一個 B+ 樹。JFFS3 的發起者正工作于垃圾回收機制的設計,這是 JFFS3 中最復雜,也是最富有挑戰性的部分。JFFS3 的設計文檔可以在http://www.linux-mtd.infradead.org/doc/jffs3.html 得到,有興趣的讀者可以積極參與到 JFFS3 的設計中,發表自己的見解,參與討論。
- 前言
- 荔枝派TODO任務領取
- linux使用小貼士
- 入門篇
- 板卡介紹
- 開箱指南
- 燒錄啟動系統
- 聯網方法
- 鏡像使用
- 鏡像說明
- buildroot系統使用
- debian系統使用
- 外設操作
- 外設操作概覽
- 低速外設
- GPIO
- GPIO模擬低速接口
- UART
- PWM
- I2C
- SPI
- 高速接口
- SDIO
- USB
- EtherNet
- DVP CSI
- MIPI CSI
- 模擬外設
- CODEC
- LRADC
- 常見設備驅動
- USB攝像頭
- USB 3G/4G 網卡
- 舵機
- 開發篇
- UBOOT適配
- UBOOT編譯
- UBOOT配置
- UBOOT配置屏幕分辨率
- UBOOT配置SPI啟動
- Linux內核開發
- Linux內核編譯
- BSP Linux內核編譯.md
- Linux內核選項
- 外設驅動與設備樹
- RTL8723BS驅動
- 根文件系統定制
- buildroot定制系統
- buildroot添加軟件包
- openwrt定制系統
- emdebian定制系統
- camdriod開發
- camdriod編譯
- 主線Uboot引導Camdriod
- 系統鏡像打包
- XBOOT適配
- 荔枝運行XBOOT
- 應用篇
- 游戲機-基于EmulationStation
- 游戲機-gnuboy
- 語音識別-科大訊飛云
- GUI-QT5
- 語音識別-離線關鍵詞識別
- 路由器-Lichee Zero
- 投稿文章
- 荔枝派Zero開箱指南
- Zero i2c oled使用指南
- zero SPI LCD使用指南
- Zero u-boot編譯和使用指南
- TF WiFi使用方法
- Zero Ethernet使用指南
- Zero 移植Qt5.4.1
- ZeroSpiNorFlash啟動系統制作指南
- Visio-uboot-sunxi流程
- lichee 編譯踩坑記錄(ilichee ZERO)
- lichee_zero_外設GPIO接口
- TF WIFI 小白編
- 從零開始LicheePi Zero的開發
- 認識Zero的硬件
- 搭建Zero的開發環境
- 主線Uboot
- 主線kernel
- BSP kernel
- BSP內核啟動
- bsp內核的攝像頭使用
- BSP內核中的保留內存
- uboot啟動BSP內核常見錯誤
- BSP內核 FBTFT移植
- BSP內核啟動錯誤及警告解決
- buildroot 根文件系統
- emdebian 根文件系統
- SPI Flash 系統編譯
- sunxi-fel增加對16M 以上flash的支持
- overlayfs的使用
- jffs2系統掛載不上的常見原因
- JFFS2 文件系統簡介
- uboot對spi flash的識別
- bsp內核的SPI flash啟動
- Docker開發環境
- Docker 命令速查
- 基礎ubuntu系統配置
- docker離線鏡像
- Zero系統燒錄
- dd鏡像燒錄
- 分區鏡像燒錄
- SPI Flash系統燒錄
- 一鍵鏡像燒錄
- Zero外設把玩
- I2C操作
- PWM輸出
- CODEC的使用
- 以太網使用指南
- GPIO操作
- 文件IO方式
- C語言接口(mmap)
- Python操作GPIO
- pinctrl-sunxi介紹
- UART操作
- 點屏
- 點屏之RGB屏
- 點屏之SPI屏 ili9341
- 點屏之SPI OLED
- 點屏之I2C OLED
- 點屏之SPI屏 ili9488
- 點屏之MCU屏
- 點屏之觸摸屏驅動
- 點屏之simple-framebuffer
- 點屏之屏幕時序
- 時鐘控制器CCM
- 攝像頭
- BSP DVP攝像頭
- BSP MIPI 攝像頭
- 主線DVP攝像頭
- 主線 MIPI攝像頭
- SPI 操作
- 應用層開發
- 開機自啟動
- Segment Fault調試
- Zero通過OTG共享PC網絡
- USB攝像頭使用
- 基于QT的GUI開發
- 移植tslib
- 移植QT5.9.1
- 移植QT4.8.7
- QtCreator使用
- Qt5.x移植到Qt4.8
- Qt字體相關
- Qt移植總結
- Qt裁剪
- Qt去除鼠標指針顯示
- zero_imager使用
- 驅動開發
- 設備樹簡介
- GPU/DRM 顯示驅動
- sys下設備樹查看
- atmel觸摸屏驅動分析
- atmel觸摸屏中斷改輪詢
- uboot下gpio操作
- helloworld驅動編譯演示
- FBTFT分析
- 內核模塊靜態加載的順序
- SPI驅動分析
- SPI 驅動編寫
- Uboot開發
- 開機logo
- 看門狗的使用
- 關于系統reboot
- 內核printk等級設置