OpenHarmony littlefs檔案系統存儲結構與IO性能優化分析
引言
随着科技的發展和網絡技術的進步,計算機存儲空間愈加緊張,存儲空間對檔案系統的功能需求越來越大,大規模的資料增長為檔案存儲、非結構化資料存儲提出了新的挑戰。
對于許多物聯網裝置而言,擁有一個小型且具有彈性的檔案系統至關重要,littlefs檔案系統應運而生。littlefs檔案系統在2017年由Christopher Haster開發,遵循Apache 2.0協定,被應用在ARM的IoT裝置Mbed作業系統。littlefs檔案系統能夠讓嵌入式系統在ROM和RAM資源有限的情況下,還具備檔案系統基本的掉電恢複、磨損均衡的功能。
littlefs是一種極簡的嵌入式檔案系統,适配于norflash,它所采用的檔案系統結構與運作機制,使得檔案系統的存儲結構更加緊湊,運作中對RAM的消耗更小。它的設計政策采用了與傳統“使用空間換時間”完全相反的“使用時間換空間”的政策,雖然它極大地壓縮了檔案系統存儲空間,但是運作時也增加了RAM的消耗,不可避免地帶來了随機讀寫時IO性能的降低。
目前,OpenAtom OpenHarmony(以下簡稱“OpenHarmony”) liteos_m核心采用了littlefs作為預設的檔案系統。本文着重介紹了littlefs檔案系統的存儲結構,并根據對讀寫過程的分析,解析引起littlefs檔案系統随機讀寫IO性能瓶頸的根本原因,然後提出一些提升littlefs随機讀寫IO性能優化政策。
littlefs檔案系統結構
檔案系統存儲結構資訊基本以SuperBlock為開端,然後尋找到檔案系統根節點,再根據根節點,逐漸拓展成一個檔案系統樹形結構體。littlefs也與此類似,以SuperBlock和根目錄為起點,建構了一個樹形存儲結構。不同的是littlefs的根("/")直接附加在SuperBlock之後,與其共享中繼資料對(metadata pair)。littlefs中目錄或者檔案都是以該根節點為起點,建構了與其他檔案系統類似的樹形結構。
littlefs檔案系統樹形存儲結構如下:

圖1 littlefs檔案系統樹形存儲結構示意圖
如圖1所示,存儲littlefs檔案系統中繼資料的結構為中繼資料對,即兩個互相輪轉、互為表裡的Block。存儲SuperBlock的中繼資料對固定存儲在block 0和block 1,并且檔案系統根目錄附加在SuperBlock的尾部,與SuperBlock共享中繼資料對。中繼資料的存儲是以tag的格式存儲在中繼資料對内,按照中繼資料的類型,将Tag分為标準檔案、目錄、使用者資料、中繼資料對尾部指針等類型。littlefs借助于這些不同類型tag資訊,将littlefs檔案系統組織成結構緊湊的樹形存儲結構體。例如tail類型的tag可以将比較大的目錄結構使用多個中繼資料對存儲,并且使用tail類型的tag将這些中繼資料對連接配接成一個單向的連結清單。而目錄類型的tag則直接指向該目錄的中繼資料對,例如"tag: dir_data"類型的tag指向目錄"/data"的中繼資料對,而該中繼資料對中又可以包含子目錄或者檔案(Inline類型或者outline類型)。
littlefs目錄存儲結構
littlefs目錄的引用為其父目錄中繼資料對(metadata pair)内的一個dir類型的Tag,而其内容則占用一個或者多個中繼資料對。一個目錄的中繼資料對内既可以包含子目錄引用的Tag,也可以包含屬于該目錄下檔案的Inline類型的Tag或者指向該檔案的CTZ跳表的CTZ類型Tag指針。最終littlefs通過一層層目錄或者檔案的索引,組成了檔案系統的樹形存儲結構。
littlefs檔案存儲方式
littlefs檔案系統為極簡的檔案系統,使用最小的存儲開銷,同時實作對小檔案(Bytes級别)和大檔案(MB級别)的支援,對小于一個Block八分之一長度的檔案,采用Inline類型的方式存儲,而大于或者等于Block八分之一長度的檔案則采用Outline的方式存儲(CTZ Skip-list)。
1.2.1 inline檔案存儲方式
Inline檔案存儲方式,如圖2所示,即将檔案内容與檔案名稱一同存儲在其父目錄的中繼資料對(metadata pair)内,一個Tag表示其名稱,一個Tag表示其内容。
圖2 littlefs Inline檔案存儲結構
1.2.2 outline檔案存儲方式
Outline檔案存儲方式,如圖3所示,檔案其父目錄的中繼資料對(metadata pair)内,一個Tag表示檔案名稱,另一個Tag為CTZ類型,其指向存儲檔案内容的連結清單頭。
圖3 littlefs Outline檔案存儲結構
CTZ跳表(CTZ skip-list)連結清單的特别之處是:
(1)CTZ跳表的頭部指向連結清單的結尾;
(2)CTZ跳表内Block内包含一個以上的跳轉指針。
若是使用正常連結清單,存儲檔案前一個資料塊包含指向後一個資料塊指針,那麼在檔案追加或者修改内容的時候,則需要存儲檔案起始塊到目标塊的所有内容拷貝到新塊内,并且更新對後一個資料塊的指針。而若是采用反向連結清單的方式,則在檔案追加或者修改内容的時候,則隻需要将存儲檔案目标塊到連結清單結尾的塊的所有内容拷貝到新塊内,然後更新對後一個資料塊内對前一個資料塊指向的指針,這樣對于檔案追加模式可以減少修改量。另外,為了加快索引,采用了跳表的方式,Block内包含一個以上的跳轉指針,規則為:若一個資料塊在CTZ skip-list連結清單内的索引值N能被 2^X整除的數,那麼他就存在指向N – 2^X的指針,指針的數目為ctz(N)+1。如表1,對于block 2,包含了2個指針,分别指向block 0和block 1,其它塊也是采用相同的規則。
表1 littlefs 塊的skip-list連結清單計算樣表
littlefs檔案讀寫流程
以上章節針對littlefs檔案系統結構進行了分析,接下來開始探讨littlefs内部的運作機制,以讀寫流程為例,分析littlefs随機讀寫的IO性能瓶頸。
需要提前了解的是,littlefs的檔案隻擁有一個緩存,寫時作為寫緩存使用,讀時作為讀緩存使用。
littlefs檔案讀過程
以下圖4是littlefs讀檔案的流程圖,在讀流程的開始先檢測先前是否有對檔案的寫操作,即檢測檔案緩存是否作為寫緩存。若是,則強制将緩存中的資料重新整理到存儲器,根據檔案類型和通路位置,或者直接從檔案所在的中繼資料對讀取,或者從存儲檔案内容的CTZ跳表内的塊内讀取,再将資料拷貝到使用者緩存沖,并從存儲器預讀取資料将檔案緩沖區填滿。具體過程如下:
圖4 littlefs檔案系統讀過程流程圖
littlefs檔案寫過程
以下圖5是littlefs寫檔案的流程圖,在寫流程的開始先檢測先前是否有對檔案的讀操作,即檢測檔案緩存是否作為讀緩存。若是,則清除緩存中的資料。若是APPEND類型的寫操作,則直接減寫位置定位到檔案末尾。若寫位置超過檔案長度,說明檔案結尾與寫位置間存在空洞,則使用0填充檔案中的空洞。對應Inline類型檔案,若推測到寫後,檔案長度超過了門檻值,則将檔案轉成Outline類型。對于Outline類型的檔案,若是修改檔案的内容,則需要申請新塊,并将目标塊内通路位置之前的所有内容都拷貝到新塊,将buffer中的使用者資料寫到緩沖區或者重新整理到存儲器。
注意:寫後并沒有立刻更新Inline檔案的commit,或者更新Outline檔案的CTZ跳表,這些操作被延遲在檔案關閉或者緩沖區再次作為讀緩存的時候強制檔案重新整理時更新。
圖5 littlefs檔案系統寫過程流程圖
littlefs檔案随機讀寫IO性能瓶頸分析
littlefs檔案隻有一個緩沖區,為讀寫複用。根據littlefs運作機制,若是對檔案先讀後寫,那麼僅需要直接将緩沖區的資料清空,然後申請一個新塊将目标塊内通路位置直接的資料拷貝到新塊中,然後寫資料到新塊。若是先寫後讀,那麼需要将資料重新整理到存儲器,同時更新檔案的CTZ跳表。在這個過程中,不僅涉及到重新整理資料到存儲器,而且涉及到配置設定新塊替換目标塊之後的所有塊進而更新CTZ跳表,出現多次費時的擦除塊動作。在随機讀寫的過程中,頻繁發生讀寫切換,也就頻繁地發生申請新塊、擦除新塊(非常費時)、資料搬移等等動作,嚴重地影響了IO性能。
littlefs讀寫IO性能優化政策
由“2.3 littlefs檔案随機讀寫IO性能瓶頸分析”章節描述可知,影響littlefs檔案随機讀寫IO性能的主要原因是檔案隻有一個的緩存且被讀寫複用,造成在讀寫切換的過程中頻繁地發生檔案重新整理,申請新塊,然後執行費時的塊擦除,再将CTZ跳表上塊内的block内容搬移到新塊,進而更新CTZ跳表,這嚴重影響了随機讀寫IO的性能。
是以,在RAM空間允許的情況下,可以考慮“使用空間換時間”的政策,适當地增加檔案緩存的數量,使一個檔案擁有多個緩沖區,而這些緩沖區對應着一個Block的大小,在一定的條件下一次重新整理一個Block,進而避免過多的資料搬移。另外,littlefs的政策是“使用時間換空間”,但是每個檔案都擁有一個緩沖區明顯浪費空間。因為在一段時間内,隻會有一定數量的檔案被執行讀或者寫,是以可以考慮建立一個擁有一定數量的緩存池,使緩存在檔案間共享。
圖6 littlefs優化政策
優化的政策如圖6所示,littlefs檔案緩存池為一個雙向連結清單fc_pool,緩存池随着被打開檔案的個數的增長而延長,直到使用者設定的最大限制;緩存池随着檔案的關閉而逐漸縮減。
每個緩存挂載在fc_pool緩存池雙向連結清單上,當緩存被寫或者被讀時,則将緩存移到連結清單開頭,那麼緩存池連結清單末尾的緩存則為待老化的緩存,可以優先被選擇回收。
在申請緩存時,優先從緩沖池連結清單末尾選擇空閑緩存;若無空閑緩存,則考慮搶占讀緩存;若緩存池既沒有空閑緩存也沒有讀緩存,在緩存池長度沒有達到優化限制的情況下,則建立新緩存,然後将新緩沖添加到連結清單頭;若緩存池既沒有空閑緩存也沒有讀緩存,并且緩存池長度已經達到使用者限制,那麼就需要從連結清單末尾搶占一個寫緩存,并強制占有該緩存的檔案執行重新整理,進而完成搶占。
在檔案被關閉或者重新整理時,主動釋放緩存到緩存池,挂載在雙向連結清單的末尾。
使用上述政策對檔案緩存進行優化,可以在一定程度上減少因更新檔案内容而執行的存儲器塊擦除動作,進而增加随機讀寫的IO性能,也間接地延長了NorFlash的壽命。
總結
通過本文的講解,相信大家對于littlefs檔案系統有了較為全面的了解。總的來說,littlefs是一種極簡的檔案系統,實作了檔案系統基本的資料緩存、掉電恢複、磨損均衡等功能,在資源相對富裕的環境中,開發者們可以對其運作機制甚至存儲結構進行“使用空間換時間”的優化政策,提升讀寫的IO性能。學會有效地利用檔案系統往往能起到事半功倍的作用,希望開發者能夠将所學知識有效應用到未來的開發工作中,進而提高開發工作的效率。