JFFS2 文件系統(tǒng)及新特性介紹
JFFS2 是一個開放源碼的項目(www.infradead.org)。 它是在閃存上使用非常廣泛的讀/寫文件系統(tǒng),在嵌入式系統(tǒng)中被普遍的應用。這篇文章首先分析了在閃存上使用 JFFS2 的必要性,然后詳細的闡述了 JFFS2 實現(xiàn)的內部機制,包括日志結構的文件系統(tǒng),關鍵的數(shù)據(jù)結構,掛載過程和垃圾收集機制。同時也指出了 JFFS2 的局限性,并介紹了最新的針對 JFFS2 的不足進行改進的補丁程序。最后對 JFFS3 的設計思想和現(xiàn)在的開發(fā)狀況給予了簡單的介紹。
1. 為什么需要 JFFS2
本文引用地址:http://2s4d.com/article/257970.htm這一小節(jié)首先介紹了閃存相對于磁盤介質的特別之處,然后分析了將磁盤文件系統(tǒng)運行在閃存上的不足,同時也給出了我們使用 JFFS2 的理由。
1.1 閃存(Flash Memory) 的特性和限制
這里所介紹的閃存的特性和限制都是從上層的文件系統(tǒng)的角度來看的,而不會涉及到具體的物理特性??偟膩碚f,有兩種類型的 flash memory: NOR flash 和 NAND flash. 先介紹一下這兩種閃存所具有的共同特性。
A) 閃存的最小尋址單位是字節(jié)(byte),而不是磁盤上的扇區(qū)(sector)。這意味著我們可以從一塊閃存的任意偏移(offset)讀數(shù)據(jù),但并不表明對閃存寫操作也是以字節(jié)為單位進行的。我們會在下面的闡述中找到答案。
B) 當一塊閃存處在干凈的狀態(tài)時(被擦寫過,但是還沒有寫操作發(fā)生),在這塊flash上的每一位(bit)都是邏輯1。
C) 閃存上的每一位(bit)可以被寫操作置成邏輯0。 可是把邏輯 0 置成邏輯 1 卻不能按位(bit)來操作,而只能按擦寫塊(erase block)為單位進行擦寫操作。擦寫塊的大小從 4K 到128K 不等。從上層來看,擦寫所完成的功能就是把擦寫塊內的每一位都重設置(reset)成邏輯 1。
D) 閃存的使用壽命是有限的。具體來說,閃存的使用壽命是由擦寫塊的最大可擦寫次數(shù)來決定的。超過了最大可擦寫次數(shù),這個擦寫塊就成為壞塊(bad block)了。因此為了避免某個擦寫塊被過度擦寫,以至于它先于其他的擦寫塊達到最大可擦寫次數(shù),我們應該在盡量小的影響性能的前提下,使擦寫操作均勻的分布在每個擦寫塊上。這個過程叫做磨損平衡(wear leveling)。
NOR flash 與 NAND flash 的不同之處:
A) NOR flash 讀/寫操作的基本單位是字節(jié);而 NAND flash 又把擦寫塊分成頁(page), 頁是寫操作的基本單位,一般一個頁的大小是 512 或 2K 個字節(jié)。對于一個頁的重復寫操作次數(shù)是有限制的,不同廠商生產的 NAND flash 有不同的限制,有些是一次,有些是四次,六次或十次。
B) 按照現(xiàn)在的技術水平,一般來說NOR flash擦寫塊的最大可擦寫次數(shù)在十萬次左右,NAND flash擦寫塊的最大可擦寫次數(shù)在百萬次左右。
1.2 閃存轉換層
將磁盤文件系統(tǒng)(ext2, FAT)運行在閃存上的很自然的方法就是在文件系統(tǒng)和閃存之間提供一個閃存轉換層(Flash Translation Layer), 它的功能就是將底層的閃存模擬成一個具有 512字節(jié)扇區(qū)大小的標準塊設備(block device)。對于文件系統(tǒng)來說,就像工作在一個普通的塊設備上一樣,沒有任何的差別。
圖一
一個閃存轉換層的最簡單的實現(xiàn)就是將模擬的塊設備一對一的映射到閃存上。舉例來說,當上層的文件系統(tǒng)要寫一個塊設備的扇區(qū)時,閃存轉換層要做下面的操作來完成這個寫請求:
1 將這個扇區(qū)所在擦寫塊地數(shù)據(jù)讀到內存中,放在緩存(buffer)中
2 將緩存中與這個扇區(qū)對應的內容用新的內容替換掉
3 對該擦寫塊執(zhí)行擦寫操作
4 將緩沖中的數(shù)據(jù)寫回該擦寫塊
這種實現(xiàn)方式的缺點是很明顯的:
1 效率低,對一個扇區(qū)的更新要重寫整個擦寫塊上的數(shù)據(jù),造成數(shù)據(jù)帶寬很大的浪費。
2 沒有提供磨損平衡,那些被頻繁更新的數(shù)據(jù)所在擦寫塊將首先變成壞塊。
3 非常不安全,很容易引起數(shù)據(jù)的丟失。如果在上面的第三步和第四步之間發(fā)生了突然掉電(power loss),那么整個擦寫塊中的數(shù)據(jù)就全部丟失了。這在突然掉電經常發(fā)生的嵌入式系統(tǒng)中是不能接受的。
MTD 中的內核模塊 mtdblock 就是基于這種機制實現(xiàn)的,同時還作了一些優(yōu)化。只有當文件系統(tǒng)的寫請求超過了一個擦寫塊的邊界的時候,它才會執(zhí)行對閃存的擦寫,寫回操作。
因此,為了解決上面這種實現(xiàn)方式的問題,閃存轉換層需要做更多的事情。閃存轉換層不能只實現(xiàn)這種一對一的映射,而需要將模擬塊設備的扇區(qū)存儲在閃存的不同位置,并且維持扇區(qū)到閃存的映射關系。更進一步,閃存轉換層還必須能理解上層文件系統(tǒng)的語義,否則閃存轉換層沒辦法做垃圾回收(Garbage Collection)。這樣實現(xiàn)最大的問題就是效率不高,具體來說,閃存轉換層為了能理解上層文件系統(tǒng)的語義,必須對文件系統(tǒng)的每個寫請求進行解析,這勢必帶來寫操作性能的下降。另外要求文件系統(tǒng)下面的一層去理解文件系統(tǒng)的語義,很顯然這不是最好的解決方式。我們還有很好的解決問題的方法,就是實現(xiàn)一個特別針對閃存的文件系統(tǒng)。而 JFFS2 就是一個這樣的文件系統(tǒng)。
2. JFFS2
有 JFFS2 就要有 JFFS v1,沒錯,JFFS v1 最初是由瑞典的 Axis Communications AB 公司開發(fā)的,使用在他們的嵌入式設備中,并且在 1999 年末基于 GNU GPL 發(fā)布出來。最初的發(fā)布版本基于 Linux 內核 2.0,后來 RedHat 將它移植到 Linux 內核 2.2,做了大量的測試和 bug fix 的工作使它穩(wěn)定下來,并且對簽約客戶提供商業(yè)支持。但是在使用的過程中,JFFS v1 設計中的局限被不斷的暴露出來。于是在 2001 年初的時候,RedHat 決定實現(xiàn)一個新的閃存文件系統(tǒng),這就是現(xiàn)在的 JFFS2。下面將詳細介紹 JFFS2 設計中主要的思想,關鍵的數(shù)據(jù)結構和垃圾收集機制。這將為我們實現(xiàn)一個閃存上的文件系統(tǒng)提供很好的啟示。首先,JFFS2 是一個日志結構(log-structured)的文件系統(tǒng),包含數(shù)據(jù)和原數(shù)據(jù)(meta-data)的節(jié)點在閃存上順序的存儲。JFFS2 之所以選擇日志結構的存儲方式,是因為對閃存的更新應該是 out-of-place 的更新方式,而不是對磁盤的 in-place 的更新方式。在閃存上 in-place 更新方式的問題我們已經在閃存轉換層一節(jié)描述過了。
2.1 節(jié)點頭部定義和兼容性
JFFS2 將文件系統(tǒng)的數(shù)據(jù)和原數(shù)據(jù)以節(jié)點的形式存儲在閃存上,具體來說節(jié)點頭部的定義如下:
圖二
幻數(shù)屏蔽位:0x1985 用來標識 JFFS2 文件系統(tǒng)。
節(jié)點類型:JFFS2 自身定義了三種節(jié)點類型,但是考慮到文件系統(tǒng)可擴展性和兼容性,JFFS2從 ext2 借鑒了經驗,節(jié)點類型的最高兩位被用來定義節(jié)點的兼容屬性,具體來說有下面幾種兼容屬性:
JFFS2_FEATURE_INCOMPAT:當 JFFS2 發(fā)現(xiàn)了一個不能識別的節(jié)點類型,并且它的兼容屬性是 JFFS2_FEATURE_INCOMPAT,那么 JFFS2 必須拒絕掛載(mount)文件系統(tǒng)。
JFFS2_FEATURE_ROCOMPAT:當 JFFS2 發(fā)現(xiàn)了一個不能識別的節(jié)點類型,并且它的兼容屬性是 JFFS2_FEATURE_ROCOMPAT,那么 JFFS2 必須以只讀的方式掛載文件系統(tǒng)。
JFFS2_FEATURE_RWCOMPAT_DELETE:當 JFFS2 發(fā)現(xiàn)了一個不能識別的節(jié)點類型,并且它的兼容屬性是 JFFS2_FEATURE_RWCOMPAT_DELETE,那么在垃圾回收的時候,這個節(jié)點可以被刪除。
JFFS2_FEATURE_RWCOMPAT_COPY:當 JFFS2 發(fā)現(xiàn)了一個不能識別的節(jié)點類型,并且它的兼容屬性是 JFFS2_FEATURE_RWCOMPAT_COPY,那么在垃圾回收的時候,這個節(jié)點要被拷貝到新的位置。
節(jié)點總長度:包括節(jié)點頭和數(shù)據(jù)的長度。
節(jié)點頭部 CRC 校驗:包含節(jié)點頭部的校驗碼,為文件系統(tǒng)的可靠性提供了支持。
2.2 節(jié)點類型
JFFS2 定義了三種節(jié)點類型:
JFFS2_NODETYPE_INODE: INODE 節(jié)點包含了i-節(jié)點的原數(shù)據(jù)(i節(jié)點號,文件的組 ID, 屬主 id, 訪問時間,偏移,長度等),文件數(shù)據(jù)被附在 INODE 節(jié)點之后。除此之外,每個 INODE 節(jié)點還有一個版本號,它被用來維護屬于一個i-節(jié)點的所有 INODE 節(jié)點的全序關系。下面舉例來說明這個全序關系在 JFFS2 的使用:
圖三
因此,當文件系統(tǒng)從閃存上讀節(jié)點信息后,會生成下面的映射信息:
圖四
根據(jù)這個映射信息表,文件系統(tǒng)就知道到相應的 INODE 節(jié)點去讀取相應的文件內容。最后要說明的是,JFFS2 支持文件數(shù)據(jù)的壓縮存儲,因此在 INODE 節(jié)點中還包含了所使用的壓縮算法,在讀取數(shù)據(jù)的時候選擇相應的壓縮算法來解壓縮。
JFFS2_NODETYPE_DIRENT:DIRENT 節(jié)點就是把文件名與 i 節(jié)點對應起來。在 DIRENT節(jié)點中也有一個版本號,這個版本號的作用主要是用來刪除一個 dentry。具體來說,當我們要從一個目錄中刪除一個 dentry 時,我們要寫一個 DIRENT 節(jié)點,節(jié)點中的文件名與被刪除的 dentry 中的文件名相同,i 節(jié)點號置為 0,同時設置一個更高的版本號。
JFFS2_NODETYPE_CLEANMARKER:當一個擦寫塊被擦寫完畢后,CLEANMARKER 節(jié)點會被寫在 NOR flash 的開頭,或 NAND flash 的 OOB(Out-Of-Band) 區(qū)域來表明這是一個干凈,可寫的擦寫塊。在 JFFS v1 中,如果掃描到開頭的 1K 都是 0xFF 就認為這個擦寫塊是干凈的。但是在實際的測試中發(fā)現(xiàn),如果在擦寫的過程中突然掉電,擦寫塊上也可能會有大塊連續(xù) 0xFF,但是這并不表明這個擦寫塊是干凈的。于是我們需要 CLEANMARKER 節(jié)點來確切的標識一個干凈的擦寫塊。
2.3 JFFS2節(jié)點,擦寫塊在內存中的表示和操作
JFFS2 維護了幾個鏈表來管理擦寫塊,根據(jù)擦寫塊上的內容,一個擦寫塊會在不同的鏈表上。具體來說,當一個擦寫塊上都是合法(valid)的節(jié)點時,它會在 clean_list 上;當一個擦寫塊包含至少一個過時(obsolete)的節(jié)點時,它會在 dirty_list 上;當一個擦寫塊被擦寫完畢,并被寫入 CLEANMARKER 節(jié)點后,它會在 free_list 上。
通常情況下,JFFS2 順序的在擦寫塊上寫入不同的節(jié)點,直到一個擦寫塊被寫滿。此時 JFFS2 從 free_list 上取下一個擦寫塊,繼續(xù)從擦寫塊的開頭開始寫入節(jié)點。當 free_list 上擦寫塊的數(shù)量逐漸減少到一個預先設定的閥值的時候,垃圾回收就被觸發(fā)了,為文件系統(tǒng)清理出更多的可用擦寫塊。為了減少對內存的占用,JFFS2 并沒有把 i 節(jié)點所有的信息都保留在內存中,而只是把那些在請求到來時不能很快獲得的信息保留在內存中。具體來說,對于在閃存上的每個 i 節(jié)點,在內存里都有一個 struct jffs2_inode_cache 與之對應,這個結構里保存了 i 節(jié)點號,指向 i 節(jié)點的連接數(shù),以及一個指向屬于這個 i 節(jié)點的物理節(jié)點鏈表的指針。所有的 struct jffs2_inode_cache 存儲在一個哈希表中。閃存上的每個節(jié)點在內存中由一個 struct jffs2_raw_node_ref 表示,這個結構里保存了此節(jié)點的物理偏移,總長度,以及兩個指向 struct jffs2_raw_node_ref 的指針。一個指針指向此節(jié)點在物理擦寫塊上的下一個節(jié)點,另一個指針指向屬于同一個 i-節(jié)點的物理節(jié)點鏈表的下一個節(jié)點。
圖五
在閃存上的節(jié)點的起始偏移都是 4 字節(jié)對齊的,所以 struct jffs2_inode_cache 中flash_offset 的最低兩位沒有被用到。JFFS2 正好利用最低位作為此節(jié)點是否過時的標記。
下面舉一例來說明 JFFS2 是如何使用這些數(shù)據(jù)結構的。VFS 調用 iget() 來得到一個 i 節(jié)點的信息,當這個 i 節(jié)點不在緩存中的時候,VFS 就會調用 JFFS2 的 read_inode() 回調函數(shù)來得到 i 節(jié)點信息。傳給 read_inode() 的參數(shù)是 i 節(jié)點號,JFFS2 用這個 i 節(jié)點號從哈希表中查找相應的 struct jffs2_inode_cache,然后利用屬于這個 i 節(jié)點的節(jié)點鏈表從閃存上讀入節(jié)點信息,建立類似于表三的映射信息。
2.4 JFFS2 掛載過程
JFFS2 的掛載過程分為四個階段:
1) JFFS2 掃描閃存介質,檢查每個節(jié)點 CRC 校驗碼的合法性,同時分配了 struct jffs2_inode_cache 和 struct jffs2_raw_node_ref
2) 掃描每個 i 節(jié)點的物理節(jié)點鏈表,標識出過時的物理節(jié)點;對每一個合法的 dentry 節(jié)點,將相應的 jffs2_inode_cache 中的 nlink 加一。
3 找出 nlink 為 0 的 jffs2_inode_cache,釋放相應的節(jié)點。
4 釋放在掃描過程中使用的臨時信息。
2.5 JFFS2 垃圾回收機制
當 free_list 上的擦寫塊數(shù)太少了,垃圾回收就會被觸發(fā)。垃圾回收主要的任務就是回收那些已經過時的節(jié)點,但是除此之外它還要考慮磨損平衡的問題。因為如果一味的從 dirty_list上選取擦寫塊進行垃圾回收,那么 dirty_list 上的擦寫塊將先于 clean_list 上的擦寫塊被磨損壞。JFFS2 的處理方式是以 99% 的概率從 dirty_list,1% 的概率從 clean_list 上取一個擦寫塊下來。由此可以看出 JFFS2 的設計思想是偏向于性能,同時兼顧磨損平衡。對這個塊上每一個沒有過時的節(jié)點執(zhí)行相同的操作:
1 找出這個節(jié)點所屬的 i 節(jié)點號(見圖五)。
2 調用 iget(),建立這個 i 節(jié)點的文件映射表。
3 找出這個節(jié)點上沒有過時的數(shù)據(jù)內容,并且如果合法的數(shù)據(jù)太少,JFFS2 還會合并相鄰的節(jié)點。
4 將數(shù)據(jù)讀入倒緩存里,然后將它拷貝到新的擦寫塊上。
5 將回收的節(jié)點置為過時。
當擦寫塊上所有的節(jié)點都被置為過時,就可以擦寫這個擦寫塊,回收使用它。
3. JFFS2 的不足之處
3.1 掛載時間過長
JFFS2 的掛載過程需要對閃存從頭到尾的掃描,這個過程是很慢的,我們在測試中發(fā)現(xiàn),掛載一個 16M 的閃存有時需要半分鐘以上的時間。
3.2 磨損平衡的隨意性(random nature)
JFFS2 對磨損平衡是用概率的方法來解決的,這很難保證磨損平衡的確定性。在某些情況下,可能造成對擦寫塊不必要的擦寫操作;在某些情況下,又會引起對磨損平衡調整的不及時。
3.3 很差的擴展性
JFFS2 中有兩個地方的處理是 O(N) 的,這使得它的擴展性很差。
首先,掛載時間同閃存的大小,閃存上節(jié)點數(shù)目成正比。
其次,雖然 JFFS2 盡可能的減少內存的占用,但通過上面對 JFFS2 的介紹我們可以知道實際上它對內存的占用量是同 i 節(jié)點數(shù)和閃存上的節(jié)點數(shù)成正比的。
因此在實際應用中,JFFS2 最大能用在 128M 的閃存上。
4. JFFS2 的新特性
最近加入到 JFFS2 中的兩個補丁程序分別解決了上面提到的掛載時間過長和磨損平衡隨意性的問題。
4.1 磨損塊小結補丁程序(erase block summary patch)
這個補丁程序最基本的思想就是用空間來換時間。具體來說,就是將每個擦寫塊每個節(jié)點的原數(shù)據(jù)信息寫在這個擦寫塊的最后,當 JFFS2 掛載的時候,對每個擦寫塊只需要讀一次來讀取這個小結節(jié)點,因此大大減少了掛載時間。使用了磨損塊小結補丁程序,一個擦寫塊的結構就像下面這樣:
圖六
根據(jù)我們的測試,使用磨損塊小結補丁程序,掛載一個 12M 的閃存需要 2~3 秒,掛載一個 16M 的閃存需要 3~4 秒。
4.2 改進的磨損平衡補丁程序
這個補丁程序的基本思想是,記錄每個擦寫塊的擦寫次數(shù),當閃存上各個擦寫塊的擦寫次數(shù)的差距超過某個預定的閥值,開始進行磨損平衡的調整。調整的策略是,在垃圾回收時將擦寫次數(shù)小的擦寫塊上的數(shù)據(jù)遷移到擦寫次數(shù)大的擦寫塊上。這樣一來我們提高了磨損平衡的確定性,我們可以知道什么時候開始磨損平衡的調整,也可以知道選取哪些擦寫塊進行磨損平衡的調整。
4.3 擦寫塊頭部補丁程序
在寫改進的磨損平衡補丁程序的過程之中,我們需要記錄每個擦寫塊的擦寫次數(shù),這個信息需要記錄在各自的擦寫塊上??墒俏覀儼l(fā)現(xiàn) JFFS2 中缺少一種靈活的對每個擦寫塊的信息進行擴展的機制。于是我們?yōu)槊總€擦寫塊引入了擦寫塊頭部(header),這個頭部負責紀錄每個擦寫塊的信息(比如說擦寫次數(shù)),并且它提供了靈活的擴展機制,將來如果有新的信息需要記錄,可以很容易的加入到頭部之中。
5. JFFS3 簡介
雖然不斷有新的補丁程序來提高 JFFS2 的性能,但是不可擴展性是它最大的問題,但是這是它自身設計的先天缺陷,是沒有辦法靠后天來彌補的。因此我們需要一個全新的文件系統(tǒng),而 JFFS3 就是這樣的一個文件系統(tǒng),JFFS3 的設計目標是支持大容量閃存(>1TB)的文件系統(tǒng)。JFFS3 與 JFFS2 在設計上根本的區(qū)別在于,JFFS3 將索引信息存放在閃存上,而 JFFS2將索引信息保存在內存中。比如說,由給定的文件內的偏移定位到存儲介質上的物理偏移地址所需的信息,查找某個目錄下所有的目錄項所需的信息都是索引信息的一種。 JFFS3 現(xiàn)在還處于設計階段,文件系統(tǒng)的基本結構借鑒了 Reiser4 的設計思想,整個文件系統(tǒng)就是一個 B+ 樹。JFFS3 的發(fā)起者正工作于垃圾回收機制的設計,這是 JFFS3 中最復雜,也是最富有挑戰(zhàn)性的部分。JFFS3 的設計文檔可以在http://www.linux-mtd.infradead.org/doc/jffs3.html 得到,有興趣的讀者可以積極參與到 JFFS3 的設計中,發(fā)表自己的見解,參與討論。
致謝
在這里要特別感謝David Woodhouse, Artem B. Bityutskiy,Joern 和 Thomas Gleixner,在參與到JFFS2的開發(fā)過程中,這幾位主要的項目維護者(maintainer)不斷地給我?guī)椭?,耐心的回答我的問題,在與他們的討論過程中碰撞出很多智慧的火花和富有啟發(fā)性的思想,尤其是他們對我的補丁程序提出的”尖刻”的問題,讓我不斷的努力思考(thinking hard),不斷的完善它們。我想這種開放,無私,客觀的精神正是開源社區(qū)的精髓所在,吸引著越來越多的開發(fā)者參與進來,并使開源社區(qū)不斷的壯大。
評論