2.3 log日志區
log是UBIFS日志的一部分。UBIFS使用日志的目的是為了減少對falsh索引的更新頻率。回憶一下,索引組成了游離樹的頂部分(只由索引節點組成),更新文件系統時,添加或者替代游離樹中的一個葉子節點時,該葉子節點的所有祖先索引節點都需要根據情況更新。如果當每次葉子節點被寫入索引都需要更新將會導致效率非常低下,因為多數相同的索引節點被反復地寫入,尤其樹的頭部。所以,UBIFS定義了一個日志,葉子節點被寫到這個日志里而不是立即添加到flash索引中。注意此時在內存中的索引需要更新(見TNC)。定期地,日志差不多滿了,它將會被提交。提交過程包括寫新的索引和主節點。日志的存在意味着當UBIFS被掛載時,存在flash的索引已經過時。為了更新它,必須讀日志中的葉子節點並重新索引。這個過程叫回放(replay)。注意,日志越大,回放花的時間越長,掛載UBIFS文件系統的時間也會越長。另一方面,一個大的日志很少被提交,這會使文件系統很有效率。日志的大小是mkfs.ubifs的一個參數,所以它可以被修改,從而滿足文件系統的需要。無論怎么樣,UBIFS默認不使用快速卸載(fast unmount)選項,取而代之的是卸載前會運行一次提交。這樣,當文件系統再次被掛載時,日志幾乎是空的,使掛載非常快速。這是一個很好的權衡協調,因為提交過程本身一般是非常快的,只花費一點點時間。
注意提交過程不是從日志中移走葉子節點,而是移動日志。log的目的就是記錄日志的位置。log包含兩種節點:一個是提交開始節點(commit start node),記錄着一個提交已經開始。另一個節點是相關節點(reference node),記錄着組成余下的日志的主存儲區(main area)的LEB數量。這些LEBs叫做芽(buds),所以日志由log和芽組成。log的大小是有限的,可以認為是一個環形緩沖區。提交過后,記錄着先前日志位置的相關節點已經不再需要了,所以log的尾部被擦除,同時log的頭部被延長。相對於提交開始節點記錄提交的開始,主節點的寫入表示提交的結束,因為主節點指向新的log的尾部。如果因為文件系統被不干凈地卸載導致提交沒有完成,然后回放操作會回放老的和新的日志(從而使得日志一致)。
由於幾種情況使回放操作變得復雜:
>> 第一種情況是葉子節點必須按順序回放。因為UBIFS使用一種多頭日志(multiheaded journal),寫入葉子節點的順序不是簡單的跟log中涉及到的芽擦除塊的順序一致。為了給葉子節點排序,每個節點包含了一個64bit的序列號,該號在文件系統活動時會增加。回放把日志中的所有葉子節點都讀出來,然后把他們放到一個紅黑樹中,這個紅黑樹是按照序列號存儲的。之后會按順序地處理紅黑樹,並實際情況更新內存中的索引。
>> 第二個復雜情況就是回放必須管理刪除和截斷。有兩種刪除。Inode節點刪除相當於刪除文件和目錄,以及目錄項刪除即刪除連接和重命名。在UBIFS中,inodes有一個一致的inode節點,inode節點記錄了目錄項連接號,更多地簡單認為是連接數目。當一個inode被刪除,一個連接數目為0的inode節點被寫入到日志中。在這種復雜情況下,不是將那個葉子節點添加到索引中,而是根據inode號沿着所有索引項,將它移除。如果刪除目錄項,一個目錄項的節點被寫到日志中,但是先前目錄項涉及到的inode號被設為0。注意目錄項中有兩個inode號。一個是其父目錄項的號,一個是其文件或子目錄項的號。刪除目錄項是后者被設置為0。當回放處理一個inode號為0的目錄項時,它會直接將那個目錄項從索引中移除而不是添加。
截斷即是改變文件的大小。事實上,截斷既可以延長文件的長度又可以縮短文件的長度。對於UBIFS,延長文件的長度不需要特殊的控制。
用文件系統的說法,通過截斷延長文件的長度會創建一個hole,這個hole在文件中是不能被寫入的,而且是全0位。UBIFS不索引holes,也不存儲任何對應於holes的節點。代替一個hole是不在那的索引項。當UBIFS尋找index,發現沒有索引項,那么它將定義為hole,並創建0數據。另外一方面,縮短文件長度的截斷需要將多余的節點從索引中移除。為了這種情況發生,截斷節點被寫到日志中,截斷節點記錄着老的和新的文件長度。回放通過刪除相關的索引項處理這些節點。
>> 第三個復雜情況是回放必須更新LPT區(LEB properties tree 邏輯擦除塊屬性樹)。LEB 屬性是在主存儲區中對於所有LEB都要知道三個值。這些值分別是:空閑空間,臟空間以及該擦除塊是否是索引擦除塊。注意索引節點和非索引節點永遠不在同一塊擦除塊中,因此一個索引擦除塊是一個只包含索引節點的擦除塊,一個非索引擦除塊也只包含非索引節點。空閑空間是指該擦除塊的結尾還沒被寫還可以填充更多的節點的區域的字節數。臟空間是指廢棄節點和填充的字節數,它們都是潛在可以被垃圾回收的。對於查找空閑空間用作日志或者索引,以及查找最臟的擦除塊做垃圾回收,LEB屬性是必要的。每寫入一個節點,就會減少那個擦除塊的空閑空間。每當廢棄一個節點或者填充節點以及截斷(或刪除)節點時,那個擦除塊的臟空間都需要增加。當一個擦除塊被申請為索引擦除塊,那必須要記錄一下。例如,一個有空閑空間的索引擦除塊就不會被申請用作日志,因為那樣它將會導致索引和非索引節點混合在一個擦除塊。后面預算章節將會進一步講述索引節點和非索引節點不能混合的理由。
一般來說,索引子系統自己負責將其LEB屬性改變通知LEB屬性子系統。當一個回收過的擦除塊被添加到日志后在回放時LEB 屬性的復雜度會增加。像索引一樣,LPT區域只在提交時才被更新。和索引一樣,存在flash上的LPT在掛載時已經過時,必須通過回放處理進行更新。所以flash上的 LEB 屬性反映的是最后一次提交時的狀態。回放將開始更新LEB屬性,雖然有的改變發生在垃圾回收之前有的在垃圾回收之后。
根據垃圾回收點的不同,最終的LEB 屬性的值將會是不同的。為了控制這個,回收插入一個引用到它的紅黑樹去描繪LEB添加到日志時候的點(使用log引用節點序列號)。當回放紅黑樹被應用到索引中時回放能正確地調整LEB 屬性值。
>> 第四個復雜情況是回放時恢復的效果。UBIFS在主節點記錄這文件系統是否被成功地卸載。如果是不干凈的卸載(unclean unmount),一定的錯誤條件會觸發文件系統的恢復。回放被兩種情況影響。第一,一個芽擦除塊正在寫的時候被不干凈地卸載了,它可能損壞。第二,同樣,log擦除塊可能在寫的時候被不干凈地卸載導致被損壞。回放會通過恢復這個擦除塊試圖修復其中的節點來處理這些情況。如果文件系統被掛載成可讀寫,那么恢復將做一些必要的修復。在這種情況下,被恢復的UBIFS文件系統的完整性和沒有遭遇過不干凈卸載一樣的完美。如果文件系統被掛載成只讀,恢復將一直等到文件系統被掛載成可讀寫才做恢復。
>> 最后一個復雜情況是索引中引用的相關的葉子節點可能已經不存在了。這個發生在當節點被刪除而且它所在的擦除塊隨后被垃圾回收處理了。一般來說,已刪除的葉子節點不會影響回放,因為它們不是索引的一部分。但是,索引結構一方面有時候更新索引時會讀葉子節點。在UBIFS中,一個目錄由一個inode節點和一個目錄項組成。可以使用一個節點密鑰(key)獲得索引,密鑰是一個64-bit的值來識別節點。在大多數情況下,這個節點密鑰可以用來唯一確認這個節點,所以索引更新用的就是密鑰。不幸的是,目錄項的指定信息是名字,它是一個很長的字符(在ubifs中達到255個字符)。為了將該信息擠到64-bit中,它的名字被hash到一個29-bit的值中,這個對於名字不是唯一地。當兩個名字給出來相同的hash值,這叫哈希沖突(hash collision)。在這種情況下,葉子節點必須被讀出來,通過比較存儲在葉子節點中的名字來解決沖突。如果因為上述原因,葉子節點丟失將會發生什么?實際上這個不會太糟糕。目錄項節點只會被添加和刪除,它們永遠不會被代替因為他們包含的信息永遠不改變。當增加一個hash 密鑰節點,將不會有匹配。當移除一個hash密鑰節點,通常會有一個匹配可能是已經存在的節點或者對一個有正確key丟掉的節點。為了提供更新這個特殊的索引用於回放,需要使用一個獨立設置的功能(表示在代碼的前綴“犯錯”)。
2.4 LPT區
Log區后面是LPT區。log區的大小在文件系統被創建的時候被定義,也就是LPT區的開始在文件系統創建時也固定了(因為它就跟在log區后)。目前,LPT區的大小是基於在文件系統創建時指定的LEB大小以及最大的LEB數目自動計算的。和log區一樣,LPT區也不超出空間。不像log區的是,LPT區的更新不是連續的,它們是隨機的。另外,LEB 屬性數據的數量潛在地非常巨大的,而且它必須是可擴展的。解決方法是存儲LEB 屬性到一個游離樹。實際上LPT區非常像一個微型的文件系統。它有自己的LEB 屬性,那就是LEB 屬性區的LEB 屬性(稱為ltab)。它還有自己的垃圾回收。它有自己的節點結構--是一個很小的bit級別的。而且,和索引一樣,LPT區只在提交時更新。因此flash上的索引和flash上的LPT描繪的是最后一次提交文件系統時的狀況。
它和真正的文件系統的不同點是被日志中的節點描述。
LPT實際上有兩個稍微不同的形式,稱為小模式(small mode)和大模式(big model)。使用小模式時。整個LEB 屬性表可以寫到一個擦除塊。在那種情況下,LPT垃圾回收就是寫整個表,這導致所有其他LPT區擦除塊可重復使用。在大模式下,垃圾回收僅選用臟LPT擦除塊,垃圾回首標記LEB的節點為臟並寫臟節點。當然,在大模式下,會存儲一個LEB數量的表之后UBIFS第一次掛載時,尋找空擦除塊不會搜尋整個LPT。在小模式下,我們假設搜尋整個表不是很慢的,因為它很小。
UBIFS的一個主要任務是讀取索引,索引是一個游離樹。為了使其更有效率,索引節點被緩存在內存中一個叫TNC(tree node cache,樹節點緩存)的結構里。TNC是一個B+樹,和flash上的索引相同的節點的節點。TNC的節點稱為znodes。另外一種看法是一個znode在flash上稱為一個索引節點,而一個索引節點在內存中稱為一個znode。初始化時是沒有znodes的。當在索引上搜尋時,需要讀索引節點,並將他們當作znodes添加到TNC。當一個znode需要改變,就在內存中將其標記為臟直到下一次提交它又再一次標記為干凈。在任何時候,UBIFS內存收縮機制(shrinker)可能決定釋放TNC中的干凈的znodes,以至於需要的內存和在使用的索引大小相稱,注意是索引的全部大小。另外,TNC的底部是一個LNC(leaf node cache,葉子節點緩存),它只用來存目錄項的。碰撞解決或是讀目錄操作的節點需要用LNC緩存。因為LNC依附於TNC,當TNC收縮時LNC也會收縮。
想要使得提交和UBIFS的其他操作產生盡可能少的沖突使得TNC更加復雜。為了達到這個目標,提交被分成兩個主要部分。第一個部分叫提交開始(commit start)。在提交開始期間,提交信號量down,防止這期間對日志的更新。在這期間,TNC子系統產生很多臟的znodes並找到他們將被寫入flash的位置。然后釋放提交信號量,一個新的日志開始被使用,而此時提交過程仍在繼續。
第二部分叫提交結束(commit end)。在提交結束期間,TNC寫新的索引節點而且是不使用任何鎖(即類似前面的信號量)。也就是說TNC可以更新並且同時新的index可以被寫到flash中。這是通過標記znodes完成的,稱為寫入時拷貝(copy-on-write)。如果一個znode提交時需要被修改,那么將拷貝一份,以至於提交看到的仍然是沒改變的znode。另外,提交是UBIFS的后台線程運行的,這樣用戶進程對於提交的只需等待很少的時間。
接下來LPT和TNC采用了相同的提交策略,他們都是使用B+樹實現的游離樹,從而導致了代碼方面很多的相似性。
UBIFS和JFFS2之間有三個重要的不同點。第一個已經提到過了:UBIFS有存儲在flash上的索引而JFFS2沒有(JFFS2的索引在內存中),所以UBIFS有可擴展性。第二個不同點是暗含的:UBIFS運行在UBI層,而UBI層運行在MTD層之上,而JFFS2直接運行在MTD層上。UBIFS得益於UBI的損益平衡和錯誤管理,這些占用的flash空間、內存和其它資源都是由UBI分配。第三個重要的不同點是UBIFS允許回寫(writeback).
回寫是VFS的一個特征,它允許寫data到緩存中而不是立即寫到介質中。這使系統響應潛在地更有效率,因為對同一個文件的更新可以組合在一起。回寫的困難之處是要求文件系統知道有多少空閑空間是有效的以至於緩存不要大於介質的空閑空間。對於UBIFS,這點是非常困難的,所以有個稱為預算(budgeting)的子系統專門做這個工作。困難有好幾個理由:
>> 第一個理由就是UBIFS支持透明的壓縮。因為我們提前不知道壓縮的數量,也不知道的需要的空間數量。預算必須假設最糟的情況---假設沒有壓縮。無論怎么樣,多數情況下是一個不好的假設。為了克服這個,當察覺到空間不足時預算開始強制回寫。
>> 第二個理由是垃圾回收不能保證回首所有的臟空間。UBIFS垃圾回收一次處理一個擦除塊。如果是NAND flash,一次只能寫一個完整的NAND頁。一個NAND 擦除塊由固定數量的nand頁組成。UBIFS稱nand頁大小為最小的I/O單元。因為UBIFS一次處理一個擦除塊,如果臟空間少於最小的I/O大小,它是不能被回收的,它將作為填充在一個NAND頁的結尾。當一個擦除塊的臟空間少於最小I/O大小,那個空間稱為死區(dead space)。死區是不可回收的。
類似於死區,還有一種暗區(dark space)。暗區是一個擦除塊的臟空間小於最大節點大小。最壞的情況,文件系統滿是最大大小的節點,垃圾回收在多片空閑空間將沒有結果。所以在最壞的情況下,暗區是不可回收的。在最好的情況下,它是可以回收的。UBIFS預算必須假設最壞的情況,所以死區和暗區都被假設為無效的。無論如何,如果沒有充足的空間,但是有很多暗區,預算自身會運行垃圾回收看是否能釋放更多的空間。
>> 第三個理由是緩存的數據可能是存儲在flash上的廢棄數據。是否是這種情況通常是不知道的,壓縮中有什么不同點一般也是不知道的。這也是當預算計算不充足空間時強制回寫的另一個原因。只有試着回寫、垃圾回收和提交日志后,預算將放棄並返回ENOSPC(沒有空間錯誤碼)。
當然,那就意味着當文件系統接近滿時,UBIFS將變得效率很低。實際上,所有falsh文件系統都是這樣。這是因為有一個空擦除塊在背后已經已擦除是不太可能的,更可能是垃圾收集的運行。
>> 第四個理由是刪除和截斷需要寫新節點。所以如果文件系統真的沒空間了,它將不可能刪除任何東西,因為已經沒有空間來寫刪除節點的節點或者截斷節點了。為了防止這種情況,UBIFS經常保留一些空間,允許刪除和截斷。
2.5 孤兒區
下一個UBIFS區是孤兒區(orphan area)。一個孤兒是一個節點數,計算的是一些已經被提交到索引的索引節點,它們的鏈接數為0。這個發生在當一個打開的文件被刪除(解除鏈接),然后執行了提交。正常情況下,該索引應該在文件被關閉的時候被刪除。然而,在不干凈的卸載的情況下,孤兒需要被考慮到。不干凈卸載后,無論是搜尋整個index還是保持一個list在flash的某處,孤兒節點必須被刪除,UBIFS實現的是后者的方案。
孤兒區是有固定數量的LEBs,位於LPT區域和主存儲區之間。孤兒區LEBs的數量當文件系統創建時指定。最小數量是1。
孤兒區的大小需要可以處理在同一時間預期的最大的孤兒數。孤兒區的大小可以適應在一個LEB中:
(leb_size-32)/8
例如,一個15872字節的LEB可以適應1980個orphans,所以一個LEB已經足夠了。
孤兒被累積在一個紅黑樹中。當inode節點的link數變為0,這個inode號被添加到這個紅黑樹。當inode被刪除,它將從tree中移除。當提交運行時,任何孤兒樹中新孤兒被寫到孤兒區,寫到1個或者更多的節點。如果orphan區已滿,空間將被擴大。通常會有總是有足夠的空間,因為驗證可以防止用戶創造超過所允許的最大孤兒數。
2.6 主存儲區
最后一個UBIFS區是主存儲區(main area)。主存儲區包含組成文件系統的數據和索引節點。一個主存儲區 LEB可能是一個索引擦除塊或者是一個非索引擦除塊。一個非索引擦除塊可能是一個芽或者已經被提交。一個芽可能是當前日志頭中的一個。一個包含提交過的節點的LEB如果還有空閑空間它仍然可以成為一個芽。因此一個芽LEB從日志開始的地方有一個偏移,盡管偏移通常為0。
更多學習參考:
[1] https://zh.wikipedia.org/wiki/UBIFS
[2] http://lwn.net/Articles/290057/
[3] http://lwn.net/Articles/276025/
[4] http://www.linux-mtd.infradead.org/faq/ubifs.html
[5] http://www.linux-mtd.infradead.org/doc/ubifs.html