簡介
JFFS2的全名為JournallingFlashFileSystemVersion2(快閃記憶體日誌型檔案系統第2版),其功能就是管理在MTD設備上實現的日誌型檔案系統。與其他的存儲設備存儲方案相比,JFFS2並不準備提供讓傳統檔案系統也可以使用此類設備的轉換層。它只會直接在MTD設備上實現日誌結構的檔案系統。JFFS2會在安裝的時候,掃描MTD設備的日誌內容,並在RAM中重新建立檔案系統結構本身。
除了提供具有斷電可靠性的日誌結構檔案系統,JFFS2還會在它管理的MTD設備上實現“損耗平衡”和“數據壓縮”等特性。
詳述
節點頭部定義和兼容性
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 校驗:包含節點頭部的校驗碼,為檔案系統的可靠性提供了支持。
節點類型
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 節點來確切的標識一個乾淨的擦寫塊。
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 節點的節點鍊表從快閃記憶體上讀入節點信息,建立類似於表三的映射信息。
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 釋放在掃描過程中使用的臨時信息。
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 將回收的節點置為過時。
當擦寫塊上所有的節點都被置為過時,就可以擦寫這個擦寫塊,回收使用它。
不足之處
掛載時間過長
JFFS2 的掛載過程需要對快閃記憶體從頭到尾的掃描,這個過程是很慢的,我們在測試中發現,掛載一個 16M 的快閃記憶體有時需要半分鐘以上的時間。
磨損平衡的隨意性(random nature)
JFFS2 對磨損平衡是用機率的方法來解決的,這很難保證磨損平衡的確定性。在某些情況下,可能造成對擦寫塊不必要的擦寫操作;在某些情況下,又會引起對磨損平衡調整的不及時。
很差的擴展性
JFFS2 中有兩個地方的處理是 O(N) 的,這使得它的擴展性很差。
首先,掛載時間同快閃記憶體的大小,快閃記憶體上節點數目成正比。
其次,雖然 JFFS2 儘可能的減少記憶體的占用,但通過上面對 JFFS2 的介紹我們可以知道實際上它對記憶體的占用量是同 i 節點數和快閃記憶體上的節點數成正比的。
新特性
加入到 JFFS2 中的兩個補丁程式分別解決了上面提到的掛載時間過長和磨損平衡隨意性的問題。
磨損塊小結補丁程式
這個補丁程式最基本的思想就是用空間來換時間。具體來說,就是將每個擦寫塊每個節點的原數據信息寫在這個擦寫塊的最後,當 JFFS2 掛載的時候,對每個擦寫塊只需要讀一次來讀取這個小結節點,因此大大減少了掛載時間。使用了磨損塊小結補丁程式,一個擦寫塊的結構就像下面這樣:
圖五
根據我們的測試,使用磨損塊小結補丁程式,掛載一個 12M 的快閃記憶體需要 2~3 秒,掛載一個 16M 的快閃記憶體需要 3~4 秒。
改進的磨損平衡補丁程式
這個補丁程式的基本思想是,記錄每個擦寫塊的擦寫次數,當快閃記憶體上各個擦寫塊的擦寫次數的差距超過某個預定的閥值,開始進行磨損平衡的調整。調整的策略是,在垃圾回收時將擦寫次數小的擦寫塊上的數據遷移到擦寫次數大的擦寫塊上。這樣一來我們提高了磨損平衡的確定性,我們可以知道什麼時候開始磨損平衡的調整,也可以知道選取哪些擦寫塊進行磨損平衡的調整。
擦寫塊頭部補丁程式
在寫改進的磨損平衡補丁程式的過程之中,我們需要記錄每個擦寫塊的擦寫次數,這個信息需要記錄在各自的擦寫塊上。可是我們發現 JFFS2 中缺少一種靈活的對每個擦寫塊的信息進行擴展的機制。於是我們為每個擦寫塊引入了擦寫塊頭部(header),這個頭部負責紀錄每個擦寫塊的信息(比如說擦寫次數),並且它提供了靈活的擴展機制,將來如果有新的信息需要記錄,可以很容易的加入到頭部之中。