UBIFS設計簡介 – A Brief Introduce to the Design of UBIFS

來源:互聯網
上載者:User

項目閑暇,想瞭解下UBIFS,就先從UBIFS的設計文檔翻譯開始吧,以後有機會有時間能分析下UBIFS源碼

 

flash memory檔案系統需要異地更新(out-of-place updates). 這是因為flash儲存在寫之前必須擦除, 並且每次擦除後只能寫一次。如果擦除塊很小並且擦除速度很快,那麼擦除塊可以看作是磁碟扇區,然而事實並非如此。讀取整個擦除快,擦除它然後回寫更新的資料, 與寫更新的資料到一個已經擦除好的擦除塊相比可能需要花費100倍甚至更多的時間。換句話說,原地更新(in-place-updates)要比異地更新慢很多。這主要是flash memory的擦除過程非常消耗時間。

 

異地更新引入了垃圾收集技術。資料在異地更新,那麼未經處理資料所在的擦除塊就包含了廢棄的資料。最終,檔案系統必然會消耗掉空的擦除塊,同時一些擦除塊包含了廢棄的資料。為了能夠寫入新的資料,就要回收這些包含廢棄資料的擦除塊。定位包含廢棄資料擦除塊,移動有效資料到其他擦除塊的過程稱為garbage collection

 

垃圾收集需要藉助於node-structure. 為了能夠garbage colleciton一個擦除塊, 一個檔案系統必須能夠標識儲存在擦除塊上的資料。 這和檔案系統常用的索引問題正好相反。檔案系統是通過檔案名稱字找到屬於檔案的資料, 而garbage collection則是通過data找到所屬的檔案(或者不屬於檔案)。解決這個問題的辦法就是把檔案metadata和data儲存在一起。 metadata和data的複合體我們稱之為node. 每一個node記錄了那個檔案擁有這個node,  以及什麼資料(比如data所在檔案的offset,
data 長度等)儲存在這個node中。 JFFS2和UBIFS都利用了基於node的設計。這使得他們能夠直接讀取eraseblock來決定哪些資料需要搬運到其他eraseblock,哪些資料可以拋棄。

 

JFFS2和UBIFS的最大不同是: UBIFS隱藏檔index在flash的某處;而JFFS2則是存放這些索引到記憶體中(記憶體index的建立是在檔案系統mount階段進行的),這導致了JFFS2檔案系統支援的最大尺寸限制,因為mount時間和記憶體消耗是和flash memory的大小成正比的。而UBIFS正是為克服這個限制設計的

 

不幸的是,儲存index到flash memory中是非常複雜的,因為index本身也不得不out-of-place update. 當一部分index需要異地update, 那麼引用update部分的的index也必須update並且也是異地update. 解決這種傳導updates的辦法是使用wandering tree

 

UBIFS的wandering tree是使用B+樹, 僅僅樹的葉子節點儲存檔案資訊,他們是檔案系統的有效資料, 而樹的內部元素則是index nodes。 因此一棵wandering tree包含了兩個部分,樹的內部節點是檔案樹的結構,而樹的葉子節點則儲存了檔案的有效資料。對檔案系統的更新包含了建立新的葉子節點並加到樹中,或者替換wandering 樹中的葉子。 對葉子的更新,必然要替換parent index節點,並且一直傳遞到樹的root node. 要更新的index nodes數目等於樹的高度。
這也引出了另外一個問題,root node的位置,在UBIFS中, root index node被儲存在master node中

 

(繼續)

master node用來記錄flash結構的位置,而這些結構的邏輯位置又不是固定的,這樣就可以通過master找到這些結構。mast node本身是儲存在邏輯擦除塊(LEB) one 和two中的。 LEB由UBI提供到物理擦除塊的映射,因此理論上LEB one LEB two可能在flash介質的任意位置(嚴格的講,是UBI device)。使用兩個擦除塊是為了保持master node的備份,以便能夠進行recovery。有兩種情況可能導致master node的損壞。第一種是在寫master node
的瞬間出現了掉電;第二種情況是flash介質本身會出現退化損壞。第一種情況recovery過程可以使用前一個版本的master node; 而第二種情況recovery 無法確定哪一個是有效master node 版本。對於後一種情況,可以通過一個使用者空間的工具來分析介質上的所有nodes來修正或者重新建立損壞或丟失的node. master node的兩個copies可以協助確定發生了什麼情況。

 

第一個LEB不是LEB1, 而是LEB0。LEB0儲存著superblock node. 超級塊節點儲存著檔案系統基本不變的參數。比如,flash的布局(刪除塊尺寸,刪除塊數目等)。當前僅有一種情況,超級塊node可能需要重寫:自動resize。UBIFS當前可以有條件的resize, 能夠resize的最大尺寸是在檔案系統建立的時候就標識的。之所以需要resize機制,是因為flash分區的確切尺寸隨著壞塊的存在可能發生變化。所以當使用mkfs.ubifs建立檔案系統時,擦除塊的最大數目和已使用的擦除塊數目記錄在superblock
node中,當UBIFS被mount到一個分區上,如果分區的擦除塊數目大於記錄在superblock node中的已使用數目並且小於擦除塊的最大數目,那麼UBIFS檔案系統自動resize到分區的實際尺寸

 

實際上在UBIFS檔案系統建立時有6個地區位置是固定的:

1. Superblock節點 LEB0

2. Master節點 LEB1 LEB2, 正常情況下,這裡個擦除塊儲存相同的資料

3.  log area

4. LEB properties tree area

5. orphan area

6. main area

前兩個區,已經在上面描述過了。超級塊是LEB0,超級塊節點一直在offset0,超級塊LEB使用UBI's 原子LEB change 函數,確保這個LEB成功更新或者不變。下一個地區是master node區。它佔據LEB1 LEB2。一般來說,這兩個LEBs儲存著相同的資料。對master node的寫操作是LEB順序的位置直到沒有空閑空間,此時LEBs被重新unmapped然後從offset 0開始繼續寫(這個過程UBI會重新map一個乾淨的erased LEB)。注意master node LEBs不能同時unmapped因為這將使檔案系統暫時沒有有效master
node。

Log 是UBIFS日誌的一部分。UBIFS日誌是用來降低flash index的更新頻率。回憶一下, wandering樹的上面部分,也就是index nodes存放這些index. 更新檔案系統的leaf node必然要增加或者替換wandering樹,同時還要更新所有的父節點。每次寫leaf node都立刻更新flash上的index node, 這種更新的效率是非常低的。 因為相同的index nodes可能會被重複的寫,特別是樹的上層節點。UBIFS通過日誌,僅僅寫leaf nodes但並不立刻更新on-flash的index.
注意memory中的index是立刻更新的。 周期性的,當日誌系統認為日誌足夠滿時,進行提交。提交過程包括寫入memory index以及相應的master node

 

日誌的存在意味著當UBIFS被mounted後,flash上的index是到期的,為了得到最新的index, 日誌中的leaf node必須被讀出來然後reindexed. 這個過程稱之為replay。注意日誌越大,replay需要的時間越長,mount所花費的時間越長。另一方面,日誌越大需要提交的頻率越低,檔案系統也更高效。日誌的尺寸可以通過mkfs.ubifs參數確定,因此可以根據需求調整檔案系統。預設情況下UBIFS不使用fast unmount選項,而是在unmounting前執行一次提交。這就促使日誌幾乎為空白,這樣下一次檔案系統重新mount時,mount速度非常的塊。commit的過程本身很快,大概需要幾分之一秒,因此unmount
時commit是一個很好的權衡。

 

注意commit過程本身不會從日誌中移動leaf節點。 而是修改日誌本身,也就是修改日誌的記錄位置。log包含兩種類型的節點,commit start node 記錄一個commit已經開始;reference nodes 記錄組成Journal的LEBs的序號。這些LEBs叫做buds, 所以說日誌包含log 和 buds; log 的尺寸是固定的可以認為是一個circular buffer。提交後,前一個reference nodes不再需要

After a commit, the reference nodes that recorded the previous position of the journal are no longer needed so the tail of the log is erased at the same rate that the head of the log is extended. While the commit-start node records the start of commit, the
end of commit is defined to be when the master node is written, because the master node points to the new position of the log tail.

如果由於系統掉電等導致的系統unmounted uncleanly導致的提交沒完成,那麼replay過程replay新舊日誌

Replay process很複雜,表現在以下幾個方面。

第一, 葉子節點必須按照順序replay。因為UBIFS使用了multiheaded joural。分葉節點的順序並不是log中bud擦除區塊引述的順序。 為了對葉子節點排序, 每個節點包含了一個64bit的序號。replay首先讀日誌中所有的分葉節點 按照這個序號插入一個RB樹,然後按照順序處理這個RB樹對in-memory index進行更新。

 

另外一個複雜是replay必須考慮刪除和truncation的情況。有兩種類型的刪除:1. inode刪除(對應檔案和目錄的刪除);2. 目錄項的刪除對應著unlinking和重新命名。UBIFS inode記錄在inode node中,記錄著目錄項的links number或者叫links count。當刪除一個inode時, links count為0的inode node寫入到日誌中。 對於1,instead of adding that leaf node to the index, it is
removed from the index along with all index entries for nodes with that inode number.  對於2,一個directory entry node被寫到日誌中,但是目錄項內的inode number 被設定為0, 注意一個目錄項有兩個inode number,一個是父目錄的inode number, 另外一個是目錄項對應檔案或目錄本身的inode number。當replay 過程碰到一個inode number為零的目錄項時,刪除目錄項的index而不是加入它

 

Truncate會改變檔案的長度。事實上,truncate除了能減少檔案長度也能夠擴充檔案的長度。 對於UBIFS檔案系統,擴充檔案長度不需要額外的處理。對於檔案系統而言,通過truncate來擴充檔案長度就是在檔案中建立hole, 這些hole被假定內容都是0。UBIFS不index這些holes也不為這些holes儲存任何nodes。當UBIFS尋找index時發現沒有index,那麼就意味著這是一個hole。另一方面,如果truncate減少檔案長度,那麼所有落在新檔案長度外的data nodes都要從index中移除。為了處理這種truncate,truncate
nodes被寫到日誌中記錄新舊檔案長度,replay處理把已刪除的data nodes從相應的index entries中移除。

 

下一個複雜是replay必須保證LEB properities tree(LPT)更新。LEB properties 記錄main area所有的LEBs的三個屬性:free space, dirty space,是否為index eraseblock or not。注意不要把index nodes, non-index nodes和index eraseblock混淆了,index eraseblock的意思是eraseblock僅僅包含index nodes, non-index eraseblock僅僅包含non-index
nodes。Free space是一個eraseblock結尾尚未被寫的位元組數,這些空間還可以寫入更多的nodes.Dirty space是指被廢棄的nodes和padding佔用的空間,是潛在的可被garbage colleciton回收的部分。LEB屬性可以用來發現空閑空間,加到日誌中,或者index中,或者找到最髒的擦除塊給garbage collect。每次一個node被寫時,node所在的eraseblock的空閑空間就要減少。每次一個node被廢棄或者一個padding node被寫,或者truncate
deletion node被寫,所在eraseblock的dirty space必須增加。當一個eraseblock被分配給index,那麼必須記錄這個資訊,一個index eraseblock即便有空閑空間也不應該分配給journal,否則會導致index nodes和no-index nodes混在同一個eraseblock中。原因是budgeting有這個需求,budgeting 將在後面討論

 

一般來說,index子系統會注意到LEB properties子系統對LEB properties的修改。在gargage collected 把擦除塊加到日誌過程中,LEB properties會引起的replay複雜性。如同index, LPT area僅僅當commit發生時才會更新;如同index, on-flash LPT從mount時刻開始就是out-of-date,必須要通過replay過程更新。所以garbage collected LEB在on-flash的LEB properties僅僅反映的是最後一次commit後的狀態。replay開始更新LEB
properties,然而這些改變有的發生在garbage collected之前,有的發生在之後。依賴於garbage發生的時間,最終的LEB property會不相同。為了處理這種情況,replay插入reference到RB樹中,代表LEB被加到日誌中的時間點。That enables the replay to correctly adjust the LEB property values when the replay RB-tree is applied to the index.

 

replay的另外一個複雜性是recovery對replay的影響。UBIFS檔案系統是否乾淨的unmounted被記錄在master node上。 如果在mount時沒有在master node看到這個標記,那麼特定的條件觸發recovery來修複檔案系統。replay受到兩方面的影響。第一,一個bud eraseblock可能被損壞 比如eraseblock正在被寫時unclean unmount發生了。第二, 一個log eraseblock可能基於同樣的原因被損壞。replay把eraseblock傳送給recovery來修複這些eraseblock上的node。如果檔案系統以可讀寫方式mount,那麼recovery需要做必要的fix,recovered
UBIFS檔案系統的完整性如同unclean unmount沒有發生一樣完美。 如果檔案系統是以唯讀方式mount的,那麼recovery被延遲到下一個讀寫mount

 

最後的複雜性是 on-flash index引用的分葉節點可能已經不再存在。 當節點被刪除並且包含節點的eraseblock被garbage collected。一般來說,刪除的葉子節點不影響replay因為他們不是index。然而有些時候index的確需要讀取葉子節點以便更新index. 比如directory entry nodes和extended attribute entry nodes。在UBIFS中,一個目錄包含一個inode node和一個directory entry node。訪問index是通過使用node
key, node key是64-bit值來標識這個node。在大部分情況下,node key唯一的標識node。所以對index的訪問只要藉助這個key就可以了, 不幸的是,目錄項和擴充屬性項惟一的標識資訊是名字,可能非常的長(在UBIFS中最多255個字元),為了縮減到64-bit,名字就需要hashed為29-bit的值,當兩個不同的名字對應相同hash值時,被稱作hash collision。在這種情況下,必須讀取leaf node比較儲存在leaf node中的名字來解決hash collision。所以當被刪除的leaf節點已經由於GC不存在了。It
turns out that it does not matter. Directory entry nodes (and extended attribute entry nodes) are only ever added or removed - they are never replaced because the information they contain never changes. So the outcome of the name comparison is known even though
the node contained one of the names is gone. When adding a hashed-key node, there will be no match. When removing a hashed-key node, there will always be a match, either to an existing node, or to a missing node that has the correct key.  為了提供這種特殊的index updating,
使用另外一套函數

 

log area之後是LPT area. log area的尺寸在檔案系統建立的時候就已經固定下來,因此log area起始位置加log area的尺寸就是LPT area的起始位置。當前LPT area的尺寸是根據LEB size以及檔案系統建立時的最大LEB數計算的。和log area一樣, LPT area永遠不會耗盡空間。和log area不同的是LPT area不是順序的,而是隨機的。此外,LEB properties資料可能非常的大,因此要考慮可擴充性。解決辦法是儲存LEB properties到wandering
tree中。 事實上LPT區更像是一個小規模的檔案系統。它有自己的LEB properties - 也就是說LEB properties area的LEB properties 稱為ltab。它有自己的garbage collection。它有自己的節點結構,封裝nodes儘可能的緊密。然而,和index一樣,LPT區僅僅在commit時更新。因此on-flash index和on-flash LPT代表的是檔案系統最後一次更新前的狀態。和當前檔案系統狀態的差異,體現在日誌內的nodes上。

 

LPT有兩種稍微不同的形式: small model和big model。

small model是整個LEB properties table可以寫入一個eraseblock。在這種情況下,LPT GC寫整個表,因此使得所有其他的LPT區的eraseblocks可重用;big model, LPT GC選擇髒的LPT eraseblocks, 這個eraseblock標記那些包含髒nodes的LEB,然後把這些髒節電寫出(這也是提交的一部分)。此外,在big model情況下,一個LEB numbers的表被儲存起來(儲存在什麼地方),以便在UBIFS第一次mount後,這個LPT表不需要被掃描。small
model,因為只有表很小,假定掃描整個表是很快的。

UBIFS的一個主要工作是訪問wandering tree中的index。為了達到高效,index nodes被cached到記憶體結構中稱為tree node cache(TNC). TNC是B+樹就如同on-flash上的index一樣,除了包含上次提交以來的所有變化。TNC節點被稱作znodes。從另外一個角度看,znode在on-flash稱作index node,  index node在memory中稱為znode。最初沒有znodes,當一個index被lookup,index nodes被讀取並且加到TNC中作為znodes.
當一個znode需要修改,他被mark為dirty直到下次提交後再被mark為clean。在任何時候UBIFS記憶體回收器可以決定釋放TNC中的clean znodes,因此所用記憶體的數目是和當前正在使用的index成比例而不是整個index的尺寸。此外,掛在TNC上的leaf node cache(LNC) 僅僅被目錄項和擴充屬性項使用。LNC僅僅在需要衝突解決或者readdir操作時需要cache nodes。因為LNC是掛在TNC上,因此在TNC做回收時也能很有效回收LNC

 

要求TNC在提交時盡量不影響到UBIFS其他的操作 使得TNC有點複雜。提交被分成了兩個部分,第一部分是commit start,在commit start時用down擷取semaphore擷取commit semaphore來防止其他人對日誌的更新。同時,TNC子系統產生一個dirty znodes的鏈表,計算好這些znodes要寫到flash上的位置。然後釋放commit semaphore, 並啟用一個新的日誌,此時提交仍在進行中。提交的第二部分是commit end. 在commit end期間,TNC
寫新的index nodes而無須任何TNC lock。那是因為TNC可以在新的index被寫入flash的同時被更新。在更新期間通過標記這個TNC為copy-on-write。如果一個正在被提交的znode需要被修改,那麼copy這個znode,使commit看到是未修改的znode。此外,大部分commit是UBIFS後台線程執行的所以使用者線程在commit執行時幾乎不需要等待。

 

注意LPT和TNC有相同的提交策略,也是用B+樹實現的wandering trees, 因此LPT和TNC有相似的代碼

 

UBIFS和JFFS2有三個主要區別:

1. UBIFS有on-flash index,而JFFS2沒有,因此UBIFS在這點上是可擴充的

2. UBIFS運行在UBI上面,而UBI又運行在MTD子系統上,而JFFS直接運行在MTD上,UBIFS可以受益於UBI提供的wear-leveling和error handling, 當然也要承擔因此帶來的flash space,memory和其他被UBI佔用的資源代價

3. UBIFS允許writeback寫回操作

 

writeback是一個VFS提供的功能,允許資料緩衝在cache中,而不是立刻寫入存貯介質。這提高了系統更有效因為對一個檔案update操作的局部性原理。支援writeback的困難是需要預知檔案系統的空閑空間使得cache不會超過這個空閑空間。預知空閑空間對UBIFS是困難的,因此引入了另外一個子系統叫做budgeting。預知的困難主要在以下幾個方面

 

第一UBIFS支援transparent compression。 因為壓縮導致無法提前預知需要的空間,budgeting 必須按最壞的情況考慮,假定沒有壓縮存在,然而在很多情況下這個假設很不好。為了克服這種情況,budgeting在檢測到空間不足的情況下,將強迫writeback。實際上譯者覺得隨著flash磁碟空間的增大 這真的不是個問題,只要用沒有壓縮假設就好了,在這時候去強迫writeback不知道會不會增加複雜性(以後我會研究的),如果答案是yes 那麼就假設沒有壓縮好了。

 

budgeting的第二個痛點是garbage collection不能保證可以回收所有的dirty space。UBIFS garbage collection每次處理一個eraseblock。對於NAND flash寫的最小單位是NAND pages。當dirty space小於NAND pages時,那麼這個空間是不能回收的。當一個eraseblock的dirty space小於最小I/O尺寸時,那麼這個空間叫做dead space。 dead space是不可回收的

類色的還有dark space. Dark space 是eraseblock上的dirty space小於最大的node尺寸。在最壞的情況下,檔案系統充滿了最大尺寸的nodes,GC無法釋放出一片空間來存在一個對大尺寸的node。所以在最壞的情況下,dark space是不被回收的,在最好的情況又是可以回收的。UBIFS budgeting 必須假定最壞的的情況,所以dead space和dark space都是認為停用。然而,如果空間不夠但是有很多dark space, budgeting將運行garbage
collection來看看是否可以回收空閑空間。

 

第三個原因 cached資料可能是flash上已經廢棄的資料。Whether or not that is the case is not always known, and what the difference in compression may be is certainly not known. 這也是budgeting 在空間不足時強制writeback的另外一個原因。僅僅在嘗試writeback, garbage collection以及提交日誌後,budgeting才能決定放棄,並返回ENOSPC

 

當然這就意味著UBIFS在檔案系統空間接近滿時效率變低。事實上,所有的flash檔案系統在flash變滿時都會變得低效。可能是因為一個empty eraseblock可能正在後台擦除,當然更大的可能是garbage collection正在工作。

 

第四個原因是deletions和truncations需要寫新nodes,因此如果檔案系統耗盡空間時,刪除變得不可能,因為沒有空間寫一個deletion inode node或者truncation node. 為了防止這種情況, UBIFS一直保留一部分空間以允許deletions和truncations

 

下面一個UBIFS area是orphan area. orphan是一個inode number,對應的inode node已經提交到index並且link count為0。這種情況發生在刪除一個已經開啟的檔案然後運行commit。在正常情況下inode會在檔案關閉時刪除。然而unclean unmount的情況下,需要考慮orphans。在unclean unmount後,orphans' inodes必須要被刪除,這意味著要麼掃描整個index找到他們,或者把orphans儲存在flash的某處,UBIFS正是使用後一種方式。

 

orphan地區是介於LPT area和main area之間,由固定數目LEBs組成。orphan area的LEBs數目是在檔案系統建立過程標定的。最小數目是1. (儲存下,吃飯去) orphan area的尺寸應該保證它能夠容納下系統可能存在orphans的數量。一個LEB可以存放的orphan的數量是:

(leb_size - 32) / 8

比如:一個15872 byte的LEB可以容納下1980個orphans, 所以一個LEB足夠了

 

Orpans存放在RB-tree。當一個inode's link計數變為0時, inode number 被加到RB-tree。It is removed from the tree when the inode is deleted.  Any new orphans that are in the orphan tree when the commit is run, are written to the orphan area in 1 or more orphan nodes. If the orphan
area is full, it is consolidated to make space. Orphan一直有足夠的空間,因為代碼會檢驗以確保user建立多於允許數目的orphans

 

 UBIFS的最後一個區是main area. main area包含節點群組成檔案系統的data和index。一個main area LEB可以是一個index eraseblock或者non-index eraseblock。一個non-index eraseblock可以是一個bud(journal部分)或者已經被提交了。一個bud當前可能是journal heads。一個LEB包含提交的nodes 如果包含空閑空間仍然可以成為bud。因此一個bud LED有一個位移標識日誌nodes的開始位置,儘管大多數情況下offset是0

 

 

 

 

 

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.