
??想了解更多關于開源的內容,請訪問:??
??51CTO 開源基礎軟件社區??
??https://ost.51cto.com??
前言
上一篇文章介紹了littlefs中的目錄操作,這一篇文章則將介紹littlefs中的文件讀寫操作。
本文會根據文件的存儲類型進行介紹,即inline文件和outline文件,其讀寫過程也有差別。另外還會介紹inline文件到outline文件的轉換,以及littlefs底層的讀寫API。
1、inline文件讀寫
因為inline文件數據存儲于其父目錄的元數據中,inline文件的讀寫實際上通過commit機制實現。讀是通過遍歷tag,寫則是通過commit一個INLINESTRUCT類型的tag。
對于inline文件的數據讀取,實際上就是從其父目錄的元數據中進行讀取,其過程已在commit機制中描述。
對于inline文件的寫入,即commit一個INLINESTRUCT類型的tag,大致過程如下:
![littlefs原理分析#[五]文件讀寫-開源基礎軟件社區 littlefs原理分析#[五]文件讀寫-開源基礎軟件社區](https://dl-harmonyos.51cto.com/images/202211/323d27a16b31eb8259d295d678dcb6485ca0dc.png?x-oss-process=image/resize,w_471,h_281)
2、inline文件轉outline文件
當文件大小超過1/8 block_size、或超過文件cache大小時,inline文件會轉為outline文件,該轉換過程在文件寫入過程中觸發。inline文件轉為outline文件之后就不會再轉回inline文件,即使對文件進行truncate操作。
轉換過程步驟如下:
- 為文件重分配塊,將inline數據寫入塊中。
- commit一個新的CTZSTRUCT類型的tag。
commit過程如下圖:
![littlefs原理分析#[五]文件讀寫-開源基礎軟件社區 littlefs原理分析#[五]文件讀寫-開源基礎軟件社區](https://dl-harmonyos.51cto.com/images/202211/83ac5388393216d10f70315c3522b8cfcbb704.png?x-oss-process=image/resize,w_471,h_281)
其中,CTZSTRUCT類型的tag中包含了新分配的文件跳表頭節點的塊指針。當讀取文件,遍歷tag時,檢測到CTZSTRUCT,就會從其中文件跳表頭節點的塊指針讀取文件數據。具體跳表中讀寫文件的過程在下小節中說明。
3、outline文件讀寫
回顧outline文件的存儲結構,其數據是用一個跳表進行存儲的:
![littlefs原理分析#[五]文件讀寫-開源基礎軟件社區 littlefs原理分析#[五]文件讀寫-開源基礎軟件社區](https://dl-harmonyos.51cto.com/images/202211/18f8e4e131b65abad8a012ff92eadb699f7c43.png?x-oss-process=image/resize,w_751,h_340)
outline文件的讀寫通過跳表的機制完成,commit時只需要commit帶有更新后的跳表頭的CTZSTRUCT tag。下面進行具體說明。
(1)outline文件讀操作
讀取數據的步驟如下:
- 調用lfs_ctz_find找到目標數據所在的塊。
- 調用lfs_bd_read進行讀取,該函數在后文進行分析。
其中,lfs_ctz_find函數從頭節點開始,通過塊頭處儲存的跳表節點塊指針進行遍歷、尋找目標塊位置。
跳表中塊指針按固定規律分布:對block n,如果n可以被2^x整除,那么該block就含有一個指向block n-2^x的塊指針。以block 4為例:
- 4可以被2^0整除,則block 4含有4-2^0即block 3的塊指針。
- 4可以被2^1整除,則block 4含有4-2^1即block 2的塊指針。
- 4可以被2^2整除,則block 4含有4-2^2即block 0的塊指針。
由此規律,又因為塊的大小是固定的,那么只要知道文件的偏移位置,就可以獲取該偏移位置所在block在跳表中的序號、該塊上有幾個塊指針等信息。lfs_ctz_find函數就是根據此規律進行查找:
- 獲取跳表中塊序號:根據文件偏移和塊大小計算,相關函數為lfs_ctz_index
- 獲取塊頭部塊指針數量:用ctz指令,ctz(塊序號)
(2)outline文件寫操作
outline文件寫入數據時又分為兩種情況,其寫入步驟也不同:
- 如果寫入數據后不超過當前塊,則調用lfs_bd_prog進行寫入。該步驟相對簡單。
- 如果寫入數據后超過當前塊:
- 調用lfs_ctz_find找到寫入位置所在的塊。
- 調用lfs_ctz_extend在寫入位置插入新的頭節點。
- 最后當調用lfs_file_sync或lfs_file_close時進行commit,實際將更新后的CTZSTRUCT tag寫入元數據。
當數據寫入后超過當前塊時,會涉及到跳表的更新,下面著重對這種情況進行說明。
lfs_ctz_extend
lfs_ctz_extend函數的作用是在文件寫入的位置插入新的頭節點。其步驟如下:
- 分配一個新塊作為新的頭節點,并調用lfs_bd_prog將原頭節點塊中的數據復制到新塊中。下圖中,調用lfs_bd_prog傳入的pcache參數為file->cache,lfs_bd_prog會先將數據寫入到file->cache中,等到需要進行flush操作時才將數據實際寫回block。
![littlefs原理分析#[五]文件讀寫-開源基礎軟件社區 littlefs原理分析#[五]文件讀寫-開源基礎軟件社區](https://dl-harmonyos.51cto.com/images/202211/a12298574c075b98a65820248c7d02b445084a.png?x-oss-process=image/resize,w_581,h_261)
- 將新的頭節點與左邊的后繼結點鏈接,右邊的舊的前繼節點被舍棄(但塊中內容不會被立即擦除):
![littlefs原理分析#[五]文件讀寫-開源基礎軟件社區 littlefs原理分析#[五]文件讀寫-開源基礎軟件社區](https://dl-harmonyos.51cto.com/images/202211/58737b2836dc4ed4d77728c6265b347813a5a6.png?x-oss-process=image/resize,w_578,h_261)
注:如果文件寫入位置位于文件末尾,則圖示中ctz block即為舊頭節點。調用lfs_file_seek函數可改變文件寫入位置。
commit后會寫入新的CTZSTRUCT tag,其過程如下:
![littlefs原理分析#[五]文件讀寫-開源基礎軟件社區 littlefs原理分析#[五]文件讀寫-開源基礎軟件社區](https://dl-harmonyos.51cto.com/images/202211/76aa6da137854ffaaac478484795c7edc7f166.png?x-oss-process=image/resize,w_471,h_281)
COW策略
outline文件寫入數據時是COW(copy-on-write)策略,lfs_ctz_extend函數插入新的頭節點時并不會將舊頭節點與后繼節點的鏈接斷掉。只有當最后將新的CTZSTRUCT tag寫入其父目錄的元數據中后,新的CTZSTRUCT tag中所包含的outline文件跳表頭節點才更新成功。
因此,如果發生掉電等異常情況導致outline文件的寫入操作未能完成時,其原有的數據也不會被丟棄。
如下圖,outline文件插入新的節點時不會去破壞原有的塊的數據。只有commit完成后,才會將新的頭節點寫入父目錄的元數據中,將原來的頭節點覆蓋。
![littlefs原理分析#[五]文件讀寫-開源基礎軟件社區 littlefs原理分析#[五]文件讀寫-開源基礎軟件社區](https://dl-harmonyos.51cto.com/images/202211/f878f9a8971a9f60c8a000e773227e3fae91a5.png?x-oss-process=image/resize,w_431,h_589)
4、block device讀寫
littlefs中block device相關的讀寫操作是其他各種上層讀寫操作的基礎,前文中提到的文件讀寫等操作均由block device相關的讀寫操作完成。block device相關讀寫操作是直接對具體的塊進行操作。文件讀寫、元數據commit過程中都是通過調用了block device相關的讀寫操作完成的。主要的相關函數為:
- lfs_bd_read:從源塊或cache中讀取數據。
- lfs_bd_prog:寫入數據到目標塊或cache。
- lfs_bd_flush:把cache中數據寫入到塊中。文件寫入后,只有當進行文件flush、sync或關閉操作時,才會調用lfs_bd_flush將數據實際寫入塊中,并將所有的更改進行commit。
以上函數利用cache或直接從塊中進行讀寫。
當直接從塊中進行讀寫時,是調用了用戶配置中提供的相關讀寫函數:
// Configuration provided during initialization of the littlefs
struct lfs_config {
...
// Read a region in a block. Negative error codes are propogated
// to the user.
int (*read)(const struct lfs_config *c, lfs_block_t block,
lfs_off_t off, void *buffer, lfs_size_t size);
// Program a region in a block. The block must have previously
// been erased. Negative error codes are propogated to the user.
// May return LFS_ERR_CORRUPT if the block should be considered bad.
int (*prog)(const struct lfs_config *c, lfs_block_t block,
lfs_off_t off, const void *buffer, lfs_size_t size);
// Erase a block. A block must be erased before being programmed.
// The state of an erased block is undefined. Negative error codes
// are propogated to the user.
// May return LFS_ERR_CORRUPT if the block should be considered bad.
int (*erase)(const struct lfs_config *c, lfs_block_t block);
// Sync the state of the underlying block device. Negative error codes
// are propogated to the user.
int (*sync)(const struct lfs_config *c);
...
};
(1)cache
block device讀寫函數均接受兩個cache,即rcache和pcache作為參數,用作讀緩存和寫緩存。具體作用見后面分析。
littlefs中cache共有以下幾種:
- 全局rcache,lfs->rcache。用作rcache參數。
- 全局pcache,lfs->pcache。讀寫元數據時用作pcache參數。
- 文件的cache,file->cache。當對文件進行讀寫操作時用作pcache參數。
(2)block device讀操作
lfs_bd_read將源塊中數據讀到目標buffer中。讀取過程中,根據數據是否在緩存中,分為以下幾種情況:
- 在pcache或rcache中:直接從cache中復制。
![littlefs原理分析#[五]文件讀寫-開源基礎軟件社區 littlefs原理分析#[五]文件讀寫-開源基礎軟件社區](https://dl-harmonyos.51cto.com/images/202211/d4b5bd9332b0fcd14c821055726ff373f86b93.png?x-oss-process=image/resize,w_221,h_121)
- 不在pcache和rcache中,且所需讀取大小小于一次能加載到cache中數據的大小:將源塊中數據加載到rcache,以便后面從rcache中讀。
![littlefs原理分析#[五]文件讀寫-開源基礎軟件社區 littlefs原理分析#[五]文件讀寫-開源基礎軟件社區](https://dl-harmonyos.51cto.com/images/202211/b1169e928266d01d3fd983bdb42338b782a6c1.png?x-oss-process=image/resize,w_261,h_121)
- 不在pcache和rcache中,且所需讀取大小不小于一次能加載到cache中數據的大小:直接從源塊中讀。
![littlefs原理分析#[五]文件讀寫-開源基礎軟件社區 littlefs原理分析#[五]文件讀寫-開源基礎軟件社區](https://dl-harmonyos.51cto.com/images/202211/f17d93108d950fcb3734953a725018757849be.png?x-oss-process=image/resize,w_271,h_121)
相關函數:
lfs_bd_read(lfs_t *lfs,
| const lfs_cache_t *pcache, lfs_cache_t *rcache, lfs_size_t hint,
| lfs_block_t block, lfs_off_t off,
| void *buffer, lfs_size_t size)
| // 1. 檢查是否已讀完,未讀完則繼續步驟,否則結束
|-> while (size > 0) ...
|
| // 2. 如果pcache中有緩存對應數據,則從pcache中讀
|-> if (pcache && block == pcache->block &&
| off < pcache->off + pcache->size) {
| if (off >= pcache->off) {
| // is already in pcache?
| diff = lfs_min(diff, pcache->size - (off-pcache->off));
| memcpy(data, &pcache->buffer[off-pcache->off], diff);
|
| data += diff;
| off += diff;
| size -= diff;
| continue;
| }
| // pcache takes priority
| diff = lfs_min(diff, pcache->off-off);
| }
|
| // 3. 如果rcache中有緩存對應數據,則從rcache中讀
|-> if (block == rcache->block &&
| off < rcache->off + rcache->size) {
| if (off >= rcache->off) {
| // is already in rcache?
| diff = lfs_min(diff, rcache->size - (off-rcache->off));
| memcpy(data, &rcache->buffer[off-rcache->off], diff);
|
| data += diff;
| off += diff;
| size -= diff;
| continue;
| }
| // rcache takes priority
| diff = lfs_min(diff, rcache->off-off);
| }
|
| // 4. 如果未命中cache且size大于等于read_size,
| // 則讀取內容大小超過cache一次加載的大小,此時從塊中讀
|-> if (size >= hint && off % lfs->cfg->read_size == 0 &&
| size >= lfs->cfg->read_size) {
| // bypass cache?
| diff = lfs_aligndown(diff, lfs->cfg->read_size);
| lfs->cfg->read(lfs->cfg, block, off, data, diff);
|
| data += diff;
| off += diff;
| size -= diff;
| continue;
| }
|
| // 5. 如果未命中cache且size小于read_size,則將塊數據加載到rcache
|-> rcache->block = block;
| rcache->off = lfs_aligndown(off, lfs->cfg->read_size);
| rcache->size = lfs_min(
| lfs_min(
| lfs_alignup(off + hint, lfs->cfg->read_size),
| lfs->cfg->block_size)
| - rcache->off,
| lfs->cfg->cache_size);
| int err = lfs->cfg->read(lfs->cfg, rcache->block,
| rcache->off, rcache->buffer, rcache->size);
(3)block device寫操作
lfs_bd_prog的作用是將源數據寫入到目標塊中。但實際上沒有立即將數據寫入的目標塊,而是先將數據復制到pcache中,等到flush操作時才將pcache中的數據寫到塊中:
![littlefs原理分析#[五]文件讀寫-開源基礎軟件社區 littlefs原理分析#[五]文件讀寫-開源基礎軟件社區](https://dl-harmonyos.51cto.com/images/202211/b68485e910a044f2fc002826da3490070d332a.png?x-oss-process=image/resize,w_221,h_121)
相關函數:
lfs_bd_prog(lfs_t *lfs,
| lfs_cache_t *pcache, lfs_cache_t *rcache, bool validate,
| lfs_block_t block, lfs_off_t off,
| const void *buffer, lfs_size_t size)
| // 1. 檢查是否已寫完,未寫完則繼續步驟,否則結束
|-> while (size > 0) ...
|
| // 2. 如果pcache已準備好,則將數據復制到pcache中
|-> if (block == pcache->block &&
| off >= pcache->off &&
| off < pcache->off + lfs->cfg->cache_size) {
| // already fits in pcache?
| lfs_size_t diff = lfs_min(size,
| lfs->cfg->cache_size - (off-pcache->off));
| memcpy(&pcache->buffer[off-pcache->off], data, diff);
|
| data += diff;
| off += diff;
| size -= diff;
|
| // 2.1 如果pcache已滿,則進行flush
|-> if (pcache->size == lfs->cfg->cache_size) {
| // eagerly flush out pcache if we fill up
| lfs_bd_flush(lfs, pcache, rcache, validate);
| continue;
| }
|
| // 3. 如果pcache未準備好,則準備pcache
|-> pcache->block = block;
| pcache->off = lfs_aligndown(off, lfs->cfg->prog_size);
| pcache->size = 0;
總結
本文介紹了littlefs中的文件讀寫機制,到這里littlefs大部分的操作就都已經做了分析了。下一篇文章將會介紹littlefs中的磨損均衡相關策略。
??想了解更多關于開源的內容,請訪問:??
??51CTO 開源基礎軟件社區??
??https://ost.51cto.com??。