歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
您现在的位置: Linux教程網 >> UnixLinux >  >> Linux綜合 >> Linux資訊 >> 更多Linux

JFFS2 文件系統及新特性介紹

1. 為什麼需要 JFFS2

這一小節首先介紹了閃存相對於磁盤介質的特別之處,然後分析了將磁盤文件系統運行在閃存上的不足,同時也給出了我們使用 JFFS2 的理由。

1.1 閃存(Flash Memory) 的特性和限制

這裡所介紹的閃存的特性和限制都是從上層的文件系統的角度來看的,而不會涉及到具體的物理特性。總的來說,有兩種類型的 flash memory: NOR flash 和 NAND flash. 先介紹一下這兩種閃存所具有的共同特性。

A) 閃存的最小尋址單位是字節(byte),而不是磁盤上的扇區(sector)。這意味著我們可以從一塊閃存的任意偏移(offset)讀數據,但並不表明對閃存寫操作也是以字節為單位進行的。我們會在下面的闡述中找到答案。

B) 當一塊閃存處在干淨的狀態時(被擦寫過,但是還沒有寫操作發生),在這塊flash上的每一位(bit)都是邏輯1。

C) 閃存上的每一位(bit)可以被寫操作置成邏輯0。 可是把邏輯 0 置成邏輯 1 卻不能按位(bit)來操作,而只能按擦寫塊(erase block)為單位進行擦寫操作。擦寫塊的大小從 4K 到128K 不等。從上層來看,擦寫所完成的功能就是把擦寫塊內的每一位都重設置(reset)成邏輯 1。

D) 閃存的使用壽命是有限的。具體來說,閃存的使用壽命是由擦寫塊的最大可擦寫次數來決定的。超過了最大可擦寫次數,這個擦寫塊就成為壞塊(bad block)了。因此為了避免某個擦寫塊被過度擦寫,以至於它先於其他的擦寫塊達到最大可擦寫次數,我們應該在盡量小的影響性能的前提下,使擦寫操作均勻的分布在每個擦寫塊上。這個過程叫做磨損平衡(wear leveling)。

NOR flash 與 NAND flash 的不同之處:

A) NOR flash 讀/寫操作的基本單位是字節;而 NAND flash 又把擦寫塊分成頁(page), 頁是寫操作的基本單位,一般一個頁的大小是 512 或 2K 個字節。對於一個頁的重復寫操作次數是有限制的,不同廠商生產的 NAND flash 有不同的限制,有些是一次,有些是四次,六次或十次。

B) 按照現在的技術水平,一般來說NOR flash擦寫塊的最大可擦寫次數在十萬次左右,NAND flash擦寫塊的最大可擦寫次數在百萬次左右。

1.2 閃存轉換層

將磁盤文件系統(ext2, FAT)運行在閃存上的很自然的方法就是在文件系統和閃存之間提供一個閃存轉換層(Flash Translation Layer), 它的功能就是將底層的閃存模擬成一個具有 512字節扇區大小的標准塊設備(block device)。對於文件系統來說,就像工作在一個普通的塊設備上一樣,沒有任何的差別。

圖一

一個閃存轉換層的最簡單的實現就是將模擬的塊設備一對一的映射到閃存上。舉例來說,當上層的文件系統要寫一個塊設備的扇區時,閃存轉換層要做下面的操作來完成這個寫請求:

1 將這個扇區所在擦寫塊地數據讀到內存中,放在緩存(buffer)中

2 將緩存中與這個扇區對應的內容用新的內容替換掉

3 對該擦寫塊執行擦寫操作

4 將緩沖中的數據寫回該擦寫塊

這種實現方式的缺點是很明顯的:

1 效率低,對一個扇區的更新要重寫整個擦寫塊上的數據,造成數據帶寬很大的浪費。

2 沒有提供磨損平衡,那些被頻繁更新的數據所在擦寫塊將首先變成壞塊。

3 非常不安全,很容易引起數據的丟失。如果在上面的第三步和第四步之間發生了突然掉電(power loss),那麼整個擦寫塊中的數據就全部丟失了。這在突然掉電經常發生的嵌入式系統中是不能接受的。

MTD 中的內核模塊 mtdblock 就是基於這種機制實現的,同時還作了一些優化。只有當文件系統的寫請求超過了一個擦寫塊的邊界的時候,它才會執行對閃存的擦寫,寫回操作。

因此,為了解決上面這種實現方式的問題,閃存轉換層需要做更多的事情。閃存轉換層不能只實現這種一對一的映射,而需要將模擬塊設備的扇區存儲在閃存的不同位置,並且維持扇區到閃存的映射關系。更進一步,閃存轉換層還必須能理解上層文件系統的語義,否則閃存轉換層沒辦法做垃圾回收(Garbage Collection)。這樣實現最大的問題就是效率不高,具體來說,閃存轉換層為了能理解上層文件系統的語義,必須對文件系統的每個寫請求進行解析,這勢必帶來寫操作性能的下降。另外要求文件系統下面的一層去理解文件系統的語義,很顯然這不是最好的解決方式。我們還有很好的解決問題的方法,就是實現一個特別針對閃存的文件系統。而 JFFS2 就是一個這樣的文件系統。

回頁首

2. JFFS2

有 JFFS2 就要有 JFFS v1,沒錯,JFFS v1 最初是由瑞典的 Axis Communications AB 公司開發的,使用在他們的嵌入式設備中,並且在 1999 年末基於 GNU GPL 發布出來。最初的發布版本基於 Linux 內核 2.0,後來 RedHat 將它移植到 Linux 內核 2.2,做了大量的測試和 bug fix 的工作使它穩定下來,並且對簽約客戶提供商業支持。但是在使用的過程中,JFFS v1 設計中的局限被不斷的暴露出來。於是在 2001 年初的時候,RedHat 決定實現一個新的閃存文件系統,這就是現在的 JFFS2。下面將詳細介紹 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 中最復雜,也是最富有挑戰性的部分。




Copyright © Linux教程網 All Rights Reserved