· 曾經,有位叫斑點的大牛,寫了一篇yaffs2源代碼分析,全網都在轉載,但是我研讀這片好文的時候,真心不爽,因爲這篇所使用的yaffs2源代碼太老了,現在看很多都對不上啊,那就本人自告奮勇的把裏面的源碼更新一遍,整體重新學習一遍,奉獻給大家!
· 在更新的時候,我發現yaffs2這麼多年來,核心的代碼變化不大,所以在複製時發現找對地方之後,代碼就沒什麼變化,有變化的地方,本文會當場指出。
· 本文使用的是 2019-01-31的版本,下載請見http://www.aleph1.co.uk/gitweb/?p=yaffs2.git;a=summary中的如下位置可下載:
1.前言
· 略。
2.yaffs 文件系統簡介
· 按理說這裏應該出現一些諸如“yaffs 是一種適合於 NAND Flash 的文件系統 XXXXX”之類的字眼,不過考慮到網絡上關於 yaffs/yaffs2 的介紹已經多如牛毛,所以同上,略。
3.本文內容組織
·本文將模仿《linux 內核源代碼情景分析》一書,以情景分析的方式對 yaffs2 文件系統的源代碼進行分析。首先將分析幾組底層函數,如存儲空間的分配和釋放等;其次分析文件邏輯地址映射;然後是垃圾收集機制;接下來…Sorry,本人還沒想好。😃
4.說明
· 因爲 yaffs2 貌似還在持續更新中,所以本文所列代碼可能和讀者手中的代碼不完全一致。
另外,本文讀者應熟悉 C 語言,熟悉 NAND Flash 的基本概念(如 block 和 page)。
Ok,步入正題。首先分析存儲空間的分配。
5.NAND Flash 存儲空間分配和釋放
· 我們知道 NAND Flash 的基本擦除單位是 Block,而基本寫入單位是 page。yaffs2 在分配存儲空間的時候是以 page 爲單位的,不過在 yaffs2 中把基本存儲單位稱爲 chunk,和page 是一樣的大小,在大多數情況下和 page 是一個意思。在下文中我們使用 chunk 這個詞,以保持和 yaffs2 的源代碼一致。
5.1 chunk 分配實現
· 我們先看存儲空間的分配(在 yaffs_guts.c 中。這個文件也是 yaffs2 文件系統的核心部分):
static int yaffs_alloc_chunk(struct yaffs_dev *dev, int use_reserver,
struct yaffs_block_info **block_ptr)
{
int ret_val;
struct yaffs_block_info *bi;
if (dev->alloc_block < 0) {
/* Get next block to allocate off */
dev->alloc_block = yaffs_find_alloc_block(dev);
dev->alloc_page = 0;
}
· 函數有三個參數, dev 是 yaffs_dev 結構的指針, yaffs2 用這個結構來記錄一個 NAND器件的屬性(如 block 和 page 的大小)和 系統運行過程中的一些統計值(如器件中可用chunk 的總數),還用這個結構維護着一組 NAND 操作函數(如讀、寫、刪除)的指針。
· 整個結構體比較大, 我們會按情景的不同分別分析。use_reserver 表示是否使用保留空間。yaffs2 文件系統並不會將所有的存儲空間全部用於存儲文件系統數據,而要空出 部分block 用於垃圾收集時使用。一般情況下這個參數都是 0,只有在垃圾收集時需要分配存儲空間的情況下將該參數置 1。yaffs_block_info 是描述 block 屬性的結構,主要由一些統計變量組成,比如該 block 內還剩多少空閒 page 等。我們同樣在具體情景中再分析這個結構中的字段含義。
· 函數首先判斷 dev->alloc_block 的值是否小於 0。yaffs_dev 結構內的 alloc_block 字段用於 記錄當前從中分配 chunk(page)的那個 block 的序號。當一個 block 內的所有 page 全部分配完畢時,就將這個字段置爲-1,下次進入該函數時就會重新挑選空閒的 block。這裏我們假定需要重新挑選空閒 block,因此進入
yaffs_find_alloc_block 函數:
[yaffs_alloc_chunk() => yaffs_find_alloc_block()]
static int yaffs_find_alloc_block(struct yaffs_dev *dev)
{
u32 i;
struct yaffs_block_info *bi;
if (dev->n_erased_blocks < 1) {
/* Hoosterman we've got a problem.
* Can't get space to gc
*/
yaffs_trace(YAFFS_TRACE_ERROR,
"yaffs tragedy: no more erased blocks");
return -1;
}
· dev->n_erased_blocks 記錄着器件內所有可供分配的 block 的數量。如果該值小於 1,那顯然是有問題了。不但正常的分配請求無法完成,就連垃圾收集都辦不到了。
for (i = dev->internal_start_block; i <= dev->internal_end_block; i++) {
dev->alloc_block_finder++;
if (dev->alloc_block_finder < (int)dev->internal_start_block
|| dev->alloc_block_finder > (int)dev->internal_end_block) {
dev->alloc_block_finder = dev->internal_start_block;
}
· internal_start_block 和 internal_end_block 分別是 yaffs2 使用的 block 的起始序號和結束序號。也就是說 yaffs2 文件系統不一定要佔據整個 Flash,可以只佔用其中的一部分。dev->alloc_block_finder 記錄着上次分配的塊的序號。如果已經分配到系統尾部,就從頭重新開始搜索可用塊。
bi = yaffs_get_block_info(dev, dev->alloc_block_finder);
if (bi->block_state == YAFFS_BLOCK_STATE_EMPTY) {
bi->block_state = YAFFS_BLOCK_STATE_ALLOCATING;
dev->seq_number++;
bi->seq_number = dev->seq_number;
dev->n_erased_blocks--;
yaffs_trace(YAFFS_TRACE_ALLOCATE,
"Allocated block %d, seq %d, %d left" ,
dev->alloc_block_finder, dev->seq_number,
dev->n_erased_blocks);
return dev->alloc_block_finder;
}
}
· yaffs_get_block_info 函數獲取指向 block 信息結構的指針,該函數比較簡單,就不詳細介紹了。yaffs_block_info 結構中的 block_state 成員描述該 block 的狀態,比如空,滿,已損壞,當前分配中,等等。因爲是要分配空閒塊,所以塊狀態必須是YAFFS_BLOCK_STATE_EMPTY,如果不是,就繼續測試下一個 block。找到以後將 block 狀態修改爲 YAFFS_BLOCK_STATE_ALLOCATING,表示當前正從該 block 中分配存儲空間。正常情況下,系統中只會有一個 block 處於該狀 態。另外還要更新統計量 n_erased_blocks 和seq_number。這個 seq_number 記錄着各 block 被分配出去的先後順序,以後在垃圾收集的時候會以此作爲判斷該 block 是否適合回收的依據。
· 現在讓我們返回到函數 yaffs_alloc_chunk 中。yaffs_check_alloc_available()函數檢查 Flash 上是否有足夠的可用空間,通過檢查後,就從當前供分配的 block 上切下一個chunk:
if (dev->alloc_block >= 0) {
bi = yaffs_get_block_info(dev, dev->alloc_block);
ret_val = (dev->alloc_block * dev->param.chunks_per_block) +
dev->alloc_page;
bi->pages_in_use++;
yaffs_set_chunk_bit(dev, dev->alloc_block, dev->alloc_page);
dev->alloc_page++;
dev->n_free_chunks--;
/* If the block is full set the state to full */
if (dev->alloc_page >= dev->param.chunks_per_block) {
bi->block_state = YAFFS_BLOCK_STATE_FULL;
dev->alloc_block = -1;
}
if (block_ptr)
*block_ptr = bi;
return ret_val;
}
dev->alloc_page 記錄着上次分配的 chunk 在 block 中的序號,每分配一次加 1。從這裏我們可以看出,系統在分配 chunk 的時候是從 block 的開頭到結尾按序分配的,直到一個 block 內的所有 chunk 全部分配完畢爲止。ret_val 是該 chunk 在整個 device 內的總序號。pages_in_use 記錄着該 block 中已分配使用的 page 的數量。系統在設備描述結構 yaffs_dev 中維護着一張位圖,該位圖的每一位都代表着 Flash上的一個 chunk 的狀態。yaffs_set_chunk_bit()將剛分配得到的 chunk 在位圖中的對應位置1,表明該塊已被使用。更新一些統計量後,就可以返回了。
5.2 chunk 釋放實現
看過 chunk 分配以後,我們再來 chunk 的釋放。和 chunk 分配不同的是,chunk 的釋放在大多數情況下並不釋放對應的物理介質,這是因爲 NAND 雖然可以按 page 寫,但只能按 block 擦除,所以物理介質的釋放要留到垃圾收集或一個 block 上的所有 page 全部變成空閒的時候才進行。 根據應用場合的不同,chunk 的釋放方式並不唯一,分別由yaffs_chunk_del 函數和 yaffs_soft_del_chunk 函數完 成。我們先看 yaffs_chunk_del:
void yaffs_chunk_del(struct yaffs_dev *dev, int chunk_id, int mark_flash, int lyn)
· chunk_id 就是要刪除的 chunk 的序號,mark_flash 參數用於 yaffs 一代的代碼中,yaffs2 不
使用該參數。
· 參數 lyn 在調用該函數時置成當前行號(LINE), 用於調試。
· 首先通過 yaffs_get_block_info 獲得 chunk 所在 block 的信息描述結構指針,然後就跑到
else 裏面去了。if 語句的判斷條件中有一 條!dev->param.is_yaffs2,所以對於 yaffs2 而言是不
會執行 if 分支的。在 else 分支裏面只是遞增一下統計計數就出來了,我們接着往下看。
if (bi->block_state == YAFFS_BLOCK_STATE_ALLOCATING ||
bi->block_state == YAFFS_BLOCK_STATE_FULL ||
bi->block_state == YAFFS_BLOCK_STATE_NEEDS_SCAN ||
bi->block_state == YAFFS_BLOCK_STATE_COLLECTING) {
dev->n_free_chunks++;
yaffs_clear_chunk_bit(dev, block, page);
bi->pages_in_use--;
if (bi->pages_in_use == 0 &&
!bi->has_shrink_hdr &&
bi->block_state != YAFFS_BLOCK_STATE_ALLOCATING &&
bi->block_state != YAFFS_BLOCK_STATE_NEEDS_SCAN) {
yaffs_block_became_dirty(dev, block);
}
}
}
· 首先要判斷一下該 block 上是否確實存在着可釋放的 chunk。block 不能爲空,不能是壞塊。
YAFFS_BLOCK_STATE_NEEDS_SCAN 表明正對該塊進行垃圾回收,我們後面會分析YAFFS_BLOCK_STATE_NEEDS_SCAN 在我手上的源代碼中似乎沒有用到。
· 通過判斷以後,所做的工作和 chunk 分配函數類似,只是一個遞增統計值,一個遞減。遞
減統計值以後還要判斷該 block 上的 page 是否已全部釋放,如 果已全部釋放,並且不是
當前分配塊,就通過 yaffs_block_became_dirty 函數刪除該 block,只要能通過刪除操作
(不是壞塊),該 block 就又可以用於分配了。
· 相比較來說,yaffs_soft_del_chunk 所做的工作就簡單多了。關鍵的代碼只有兩行:
static void yaffs_soft_del_chunk(struct yaffs_dev *dev, int chunk)
{
......
the_block->soft_del_pages++;
dev->n_free_chunks++;
......
}
· 這裏遞增的是 yaffs_blockInfo 結構中的另一個統計量 soft_del_pages,而沒有修改pages_in_use 成員,也沒有修改 chunk 狀態位圖。那麼,這兩個函數的應用場合有什麼區別呢?
· 一般來說,yaffs_chunk_del 用於文件內容的更新。比如我們要修改文件中的部分內容, 這時候 yaffs2 會分配新的 chunk,將更改後的內容寫入新 chunk 中,原 chunk 的內容自然就沒有用了,所以要將 pageInUse 減 1,並修改位圖;
· yaffs_soft_del_chunk 用於文件的刪除。yaffs2 在刪除文件的時候只是刪除該文件在內
存中的一些描述結構,而被刪除的文件所佔用 的 chunk 不會立即釋放,也就是不會刪除
文件內容,在後續的文件系統操作中一般也不會把這些 chunk 分配出去,直到系統進行垃
圾收集的時候纔有選擇地 釋放這些 chunk。熟悉 DOS 的朋友可能還記得,DOS 在刪除的文
件的時候也不會立即刪除文件內容,只是將文件名的第一個字符修改爲 0xA5,事後還可
以恢復文件內容。yaffs2 在這點上是類似的。
6.文件地址映射
· 上面說到, yaffs 文件系統在更新文件數據的時候,會分配一塊新的 chunk,也就是說, 同樣的文件偏移地址, 在該地址上的數據更新前和更新後, 其對應的 flash 上的存儲地址是不一樣的。那麼,如何根據文件內偏移地址確定 flash 存儲地址呢? 最容易想到的辦法,就是在內存中維護一張映射表。由於 flash 基本存儲單位是 chunk, 因此, 只要將以chunk 描述的文件偏移量作爲表索引, 將 flash chunk 序號作爲表內容,就可以解決該問題了。但是這個方法有幾個問題, 首先就是在做 seek 操作的時候,要從表項 0 開始按序搜索,對於大文件會消耗很 多時間; 其次是在建立映射表的時候,無法預計文件大小的變化,於是就可能在後來的操作中頻繁釋放分配內存以改變表長,造成內存碎片。yaffs 的解決方法 是將這張大的映射表拆分成若干個等長的小表, 並將這些小表組織成樹的結構, 方便管理。我們先看小表的定義:(位於yaffs_guts.h)
struct yaffs_tnode {
struct yaffs_tnode *internal[YAFFS_NTNODES_INTERNAL];
};
· YAFFS_NTNODES_INTERNAL 定義爲(YAFFS_NTNODES_LEVEL0 / 2),而YAFFS_NTNODES_LEVEL0 定義爲 16,所以這實際上是一個長度爲 8 的指針數組。不管是葉子節點還是非葉節點,都是這個結構。當節點爲非葉 節點時,數組中的每個元素都指向下一層子節點;當節點爲葉子節點時,該數組拆分爲 16 個 16 位長的短整數(也有例外,後面會說到),該短整數就是文件內容 在 flash 上的存儲位置(即 chunk 序號)。至於如何通過文件內偏移找到對應的 flash 存儲位置,源代碼所附文檔(Development/yaffs/Documentation/yaffs-notes2.html)已經有說明,俺就不在此處饒舌了。下面看具體函數。
· 爲了行文方便, 後文中將 yaffs_tnode 這個指針數組稱爲 “一組” Tnode,而將數組中的每個元素稱爲 “一個” Tnode。樹中的每個節點, 都是“一組”Tnode。
· 先看映射樹的節點的分配。
struct yaffs_tnode *yaffs_get_tnode(struct yaffs_dev *dev)
{
struct yaffs_tnode *tn = yaffs_alloc_raw_tnode(dev);
if (tn) {
memset(tn, 0, dev->tnode_size);
dev->n_tnodes++;
}
dev->checkpoint_blocks_required = 0; /* force recalculation */
return tn;
}
· 調用 yaffs_alloc_raw_tnode 分配節點, 然後將得到的節點初始化爲零。
struct yaffs_tnode *yaffs_alloc_raw_tnode(struct yaffs_dev *dev)
{
struct yaffs_allocator *allocator =
(struct yaffs_allocator *)dev->allocator;
struct yaffs_tnode *tn = NULL;
if (!allocator) {
BUG();
return NULL;
}
/* If there are none left make more */
if (!allocator->free_tnodes)
yaffs_create_tnodes(dev, YAFFS_ALLOCATION_NTNODES);
if (allocator->free_tnodes) {
tn = allocator->free_tnodes;
allocator->free_tnodes = allocator->free_tnodes->internal[0];
allocator->n_free_tnodes--;
}
return tn;
}
· 這裏和之前版本差異較大,yaffs_dev 結構中增加了一個yaffs_allocator 類型的結構allocator,專門來管分配,而free_tnodes作爲空閒的節點被放在了裏面。
· 當前所有空閒節點組成一個鏈表, allocator->free_tnodes 是這個鏈表的表頭。我們假定已經沒有空閒節點可用,需通過 yaffs_create_tnodes 創建一批新的節點。
static int yaffs_create_tnodes(struct yaffs_dev *dev, int n_tnodes)
{
......
new_tnodes = kmalloc(n_tnodes * dev->tnode_size, GFP_NOFS);
mem = (u8 *) new_tnodes;
yaffs_guts_initialise中對tnode_size初始化:
dev->tnode_size = (dev->tnode_width * YAFFS_NTNODES_LEVEL0) / 8;
· 上面說過,葉節點中一個 Tnode 的位寬默認爲 16 位,也就是可以表示 65536 個 chunk。對於時下的大容量 flash,chunk 的大小爲 2K,因 此在默認情況下 yaffs2 所能尋址的最大flash 空間就是 128M。爲了能將 yaffs2 用於大容量 flash 上,代碼作者試圖通過兩種手段解決這個問題。第一種手段就是這裏的 dev->tnode_width,通過增加單個 Tnode 的位寬, 就可以增加其所能表示的最大 chunk Id;另一種手段是我們後面將看到的 chunk group,通過將若干個 chunk 合成一組用同一個 id 來表示,也可以增加系統所能尋址的 chunk 範圍。
· 俺爲了簡單,分析的時候不考慮這兩種情況,因 此 tnode_width 取默認值 16,也不考慮將多個 chunk 合成一組的情況,只在遇到跟這兩種情況有關的代碼時作簡單說明。
· 在 32 位的系統中,指針的寬度爲 32 位,而 chunk id 的寬度爲 16 位,因此相同大小的Tnode 組,可以用來表示 N 個非葉 Tnode(作爲指針使用),也可以用來表示 N * 2 個葉子Tnode(作爲 chunk id 使用)。代碼中分別用 YAFFS_NTNODES_INTERNAL 和 YAFFS_NTNODES_LEVEL0 來表示。前者取值爲 8,後者取值爲 16。從這裏我們也可以看出,若將 yaffs2 用於 64 位系統需要作哪些修改。針對上一段敘述的問題,俺以爲在內存不緊張的情況下,不如將葉節點 Tnode 和非葉節點Tnode 都設爲一個指針的長度。
· 分配得到所需的內存後,就將這些空閒空間組成 Tnode 鏈表:
for (i = 0; i < n_tnodes - 1; i++) {
curr = (struct yaffs_tnode *)&mem[i * dev->tnode_size];
next = (struct yaffs_tnode *)&mem[(i + 1) * dev->tnode_size];
curr->internal[0] = next;
}
· 每組 Tnode 的第一個元素作爲指針指向下一組 Tnode。完成鏈表構造後,還要遞增統計量, 並將新得到的 Tnodes 掛入一個全局管理鏈表 yaffs_tnode_list:
allocator->n_free_tnodes += n_tnodes;
allocator->n_tnodes_created += n_tnodes;
/* Now add this bunch of tnodes to a list for freeing up.
* NB If we can't add this to the management list it isn't fatal
* but it just means we can't free this bunch of tnodes later.
*/
tnl = kmalloc(sizeof(struct yaffs_tnode_list), GFP_NOFS);
if (!tnl) {
yaffs_trace(YAFFS_TRACE_ERROR,
"Could not add tnodes to management list");
return YAFFS_FAIL;
} else {
tnl->tnodes = new_tnodes;
tnl->next = allocator->alloc_tnode_list;
allocator->alloc_tnode_list = tnl;
}
· 回到 yaffs_alloc_raw_tnode,創建了若干組新的 Tnode 以後,從中切下所需的 Tnode, 並修改空閒鏈表表頭指針:
if (allocator->free_tnodes) {
tn = allocator->free_tnodes;
allocator->free_tnodes = allocator->free_tnodes->internal[0];
allocator->n_free_tnodes--;
}
· 至此, 分配工作就完成了。相比較來說,釋放 Tnodes 的工作就簡單多了,簡單的鏈表和統計值操作:
static void yaffs_free_tnode(struct yaffs_dev *dev, struct yaffs_tnode *tn)
{
yaffs_free_raw_tnode(dev, tn);
dev->n_tnodes--;
dev->checkpoint_blocks_required = 0; /* force recalculation */
}
void yaffs_free_raw_tnode(struct yaffs_dev *dev, struct yaffs_tnode *tn)
{
struct yaffs_allocator *allocator = dev->allocator;
if (!allocator) {
BUG();
return;
}
if (tn) {
tn->internal[0] = allocator->free_tnodes;
allocator->free_tnodes = tn;
allocator->n_free_tnodes++;
}
dev->checkpoint_blocks_required = 0; /* force recalculation */
}
· 看過 Tnode 的分配和釋放,我們再來看看這些 Tnode 是如何使用的。在後文中,我們把以 chunk 爲單位的文件內偏移稱作邏輯 chunk id,文件內容在 flash 上的實際存儲位置稱作物理 chunk id。先看一個比較簡單的函數。
static void yaffs_load_tnode_0(struct yaffs_dev *dev, struct yaffs_tnode *tn,
unsigned pos, unsigned val)
· 這個函數將某個 Tnode 設置爲指定的值。tn 是指向一組 Tnode 的指針;pos 是所要設置的那個 Tnode 在該組 Tnode 中的索引;val 就是所要設置的值,也就是物理 chunk id。函數名中的 0 指映射樹的葉節點。函數開頭幾行如下:
pos &= YAFFS_TNODES_LEVEL0_MASK;
val >>= dev->chunk_grp_bits;
bit_in_map = pos * dev->tnode_width;
word_in_map = bit_in_map / 32;
bit_in_word = bit_in_map & (32 - 1);
mask = dev->tnode_mask << bit_in_word;
· 上面說過,一組 Tnode 中的 8 個指針在葉節點這一層轉換成 16 個 16 位寬的 chunk Id,因此需要 4 位二進制碼對其進行索引,這就是 YAFFS_TNODES_LEVEL0_MASK 的值。我們還說過這個 16 位值就是 chunk 在 flash 上的序號,當 flash 容量比較大, chunk 數量多時, 16位可能無法給 flash 上的所有 chunk 編號,這種情況下可以增加 chunk id 的位寬, 具體位寬的值記錄在 dev->tnodeWidth 中。yaffs2 允許使用非字節對齊的 tnodeWidth, 因此可能出現某個 chunk id 跨 32 位邊界存儲的情況。所以在下面的代碼中,需要分邊界前和邊界後兩部分處理:
map[word_in_map] &= ~mask;
map[word_in_map] |= (mask & (val << bit_in_word));
if (dev->tnode_width > (32 - bit_in_word)) {
bit_in_word = (32 - bit_in_word);
word_in_map++;
mask =
dev->tnode_mask >> bit_in_word;
map[word_in_map] &= ~mask;
map[word_in_map] |= (mask & (val >> bit_in_word));
}
· if 語句判斷當前 chunk 序號是否跨越當前 32 位邊界。整個代碼初看起來比較難理解,其實只要將 dev->tnode_width 以 16 或 32 代入, 就很好懂了。還有一個類似的函數yaffs_GetChunkGroupBase, 返回由 tn 和 pos 確定的一組 chunk 的起始序號,就不詳細分析了。
現在我們假設有這樣一個情景:已知文件偏移地址,要找到 flash 上對應的存儲地址,該
怎麼做呢?這項工作的主體是由函數 yaffs_find_tnode_0 完成的。
struct yaffs_tnode *yaffs_find_tnode_0(struct yaffs_dev *dev,
struct yaffs_file_var *file_struct,
u32 chunk_id)
{
struct yaffs_tnode *tn = file_struct->top;
u32 i;
int required_depth;
int level = file_struct->top_level;
· 函數參數中,file_struct 是指向文件描述結構的指針,該結構保存着文件大小、映射樹層高、映射樹頂層節點指針等信息。chunk_id 是邏輯 chunk id。
· file_struct->top 是映射樹頂層節點指針,file_struct->top_level 是映射樹層高。注意:當只有
一層時,層高爲 0。
/* First check we're tall enough (ie enough top_level) */
i = chunk_id >> YAFFS_TNODES_LEVEL0_BITS;
required_depth = 0;
while (i) {
i >>= YAFFS_TNODES_INTERNAL_BITS;
required_depth++;
}
if (required_depth > file_struct->top_level)
return NULL; /* Not tall enough, so we can't find it */
· 在 看這段代碼之前,我們先用一個例子來回顧一下映射樹的組成。假定我們有一個大小爲 128K 的文件,flash 的 page 大小爲 2K,那麼我們就需要 64 個 page(或者說 chunk)來存儲該文件。一組 Tnode 的 size 是 8 個指針,或者 16 個 16 位整數,所以我們需要 64 / 16= 4 組 Tnode 來存儲物理 chunk 序號。這 4 組 Tnode 就是映射樹的葉節點,也就是 Level0節點。由於這 4 組 Tnode 在內存中不一定連續,所以 我們需要另外一組 Tnode,將其作爲指針數組使用,這個指針數組的前 4 個元素分別指向 4 組 Level0 節點,而 file_struct->top就指向這 組作爲指針數組使用的 Tnode。隨着文件長度的增大,所需的葉節點越多,非葉節點也越多,樹也就越長越高。
· 回過頭來看代碼,首先是檢查函數參數 chunk_id 是否超過文件長度。作爲非葉節點使用的Tnode 每組有 8 個指針,需要 3 位二進制碼對其進行索引,因此 樹每長高一層,邏輯chunk_id 就多出 3 位。相反,每 3 位非零 chunk_id 就代表一層非葉節點。while 循環根據這個原則計算參數 chunk_id 所 對應的樹高。如果樹高超過了文件結構中保存的樹高,那就說明該邏輯 chunk_id 已經超出文件長度了。通過文件長度檢查之後,同樣根據上面的原則, 就可以 找到邏輯 chunk_id 對應的物理 chunk_id 了。具體的操作通過一個 while 循環完成:
/* Traverse down to level 0 */
while (level > 0 && tn) {
tn = tn->internal[(chunk_id >>
(YAFFS_TNODES_LEVEL0_BITS +
(level - 1) *
YAFFS_TNODES_INTERNAL_BITS)) &
YAFFS_TNODES_INTERNAL_MASK];
level--;
}
· 將返回值和邏輯 chunk id 作爲參數調用 yaffs_get_group_base,就可以得到物理chunk id 了。
下面我們看另一個情景,看看當文件長度增加的時候,映射樹是如何擴展的。主要函數爲
struct yaffs_tnode *yaffs_add_find_tnode_0(struct yaffs_dev *dev,
struct yaffs_file_var *file_struct,
u32 chunk_id,
struct yaffs_tnode *passed_tn)
· 函數的前幾行和 yaffs_find_tnode_0 一樣,對函數參數作一些檢查。通過檢查之後,首先看原映射樹是否有足夠的高度,如果高度不夠,就先將其“拔高”:
if (required_depth > file_struct->top_level) {
/* Not tall enough, gotta make the tree taller */
for (i = file_struct->top_level; i < required_depth; i++) {
tn = yaffs_get_tnode(dev);
if (tn) {
tn->internal[0] = file_struct->top;
file_struct->top = tn;
file_struct->top_level++;
} else {
yaffs_trace(YAFFS_TRACE_ERROR,
"yaffs: no more tnodes");
return NULL;
}
}
}
for 循環完成增加新層的功能。新增的每一層都只有一個節點(即一組Tnode),file_struct->top 始終指向最新分配的節點。將映射樹擴展到所需的高度之後,再根據需要將其“增肥”,擴展其“寬度”:
l = file_struct->top_level;
tn = file_struct->top;
if (l > 0) {
while (l > 0 && tn) {
x = (chunk_id >>
(YAFFS_TNODES_LEVEL0_BITS +
(l - 1) * YAFFS_TNODES_INTERNAL_BITS)) &
YAFFS_TNODES_INTERNAL_MASK;
if ((l > 1) && !tn->internal[x]) {
/* Add missing non-level-zero tnode */
tn->internal[x] = yaffs_get_tnode(dev);
if (!tn->internal[x])
return NULL;
} else if (l == 1) {
/* Looking from level 1 at level 0 */
if (passed_tn) {
/* If we already have one, release it */
if (tn->internal[x])
yaffs_free_tnode(dev,
tn->internal[x]);
tn->internal[x] = passed_tn;
} else if (!tn->internal[x]) {
/* Don't have one, none passed in */
tn->internal[x] = yaffs_get_tnode(dev);
if (!tn->internal[x])
return NULL;
}
}
tn = tn->internal[x];
l--;
}
} else {
/* top is level 0 */
if (passed_tn) {
memcpy(tn, passed_tn,
(dev->tnode_width * YAFFS_NTNODES_LEVEL0) / 8);
yaffs_free_tnode(dev, passed_tn);
}
}
· 上面“拔高”的時候是從下往上“蓋樓”,這裏“增肥”的時候是從上往下“擴展”。tn->internal[x]爲空表示下層節點尚未創建,需要通過 yaffs_free_tnode 分配之,就是“增肥”了。如果函數參數 passed_tn 有效,就用該組 Tnode 代替 level0 上原先的那組Tnode;否則按需分配新的 Tnode 組。所以這裏的函數名似乎應該取作yaffs_AddOrFindOrReplaceLevel0Tnode 更加恰當。不過這個新名字也太長了些…樹的創建、搜索和擴展說完了,下面該說什麼?..對了,收縮和刪除。不過看過創建搜
索擴展之後,收縮和刪除已經沒什麼味道了。主要函數有:
yaffs_soft_del_worker()
yaffs_prune_worker()
· 前者用於刪除,後者用於收縮。都是從 level0 開始,以遞歸的方式從葉節點向上刪,並釋放被刪除 Tnode 所對應的物理 chunk。遞歸,偉大的遞 歸啊…俺不想把這篇文章做成遞歸算法教程,除了遞歸這兩個函數也就不剩啥了,所以一概從略。而 yaffs_soft_del_worker 主要用於在刪除文件時資源的釋放。
7.文件系統對象
· 在 yaffs2 中,不管是文件還是目錄或者是鏈接,在內存都用一個結構體struct yaffs_obj 來描述。我們先簡要介紹一下這個結構體中的 幾個關鍵字段,然後再來看代碼。在後文中提到“文件”或“文件對象”,若不加特別說明,都指廣義的“文件”,既可以是文件,也可以是目錄。
u8 deleted:1; /* This should only apply to unlinked files. */
u8 soft_del:1; /* it has also been soft deleted */
u8 unlinked:1; /* An unlinked file.*/
· 這三個字段用於描述該文件對象在刪除過程中所處的階段。在刪除文件時,首先要將文件從原目錄移至一個特殊的系統目錄/unlinked,以此拒絕應用程序 對該文件的訪問,此時將 unlinked 置 1;然後判斷該文件長度是否爲 0,如果爲 0,該文件就可以直接刪除,此時將 deleted 置 1;如果不爲 0,就 將 deleted 和 soft_del 都置 1,表明該文件數據所佔據的 chunk 還沒有釋放,要留待後繼處理。
struct yaffs_obj *parent;
· 看名字就知道,該指針指向上層目錄。
int hdr_chunk;
· 每個文件在 flash 上都有一個文件頭,存儲着該文件的大小、所有者、創建修改時間等信息。hdr_chunk 就是該文件頭在 flash 上的 chunk 序號。
u32 obj_id;
· 每一個文件系統對象都被賦予一個唯一的編號,作爲對象標識,也用於將該對象掛入一個散列表,加快對象的搜索速度。
u32 variant_type; /* enum yaffs_object_type */
union yaffs_obj_var variant;
· 前者表示該對象的類型,是目錄、普通文件還是鏈接文件。後者是一個聯合體,根據對象類型的不同有不同的解釋。
· 其餘的成員變量,我們在後面結合函數一起分析。下面我們來看相關的函數。先看一個簡單的:
static struct yaffs_obj *yaffs_create_fake_dir(struct yaffs_dev *dev,
int number, u32 mode)
· 所謂 Fake Directory,就是僅存在於內存中,用於管理目的的目錄對象,比如我們上面提到的 unlinked 目錄。這種類型的目錄有一些特別的地方,如禁止改 名、禁止刪除等。由於對象僅存在於內存中,因此不涉及對硬件的操作,所以函數體很簡單。首先通過yaffs_create_obj 獲得一個新對 象,然後對其中的一些字段初始化。先把字段初始化看一下,順便再介紹一些字段:
· rename_allowed 表示是否允許改名,對於 fake 對象爲 0;
· unlink_allowed 表示是否允許刪除,對於 fake 對象同樣爲 0;
· yst_mode 就是 linux 中的訪問權限位;
· hdr_chunk 是對象頭所在 chunk,由於 fake 對象不佔 flash 存儲空間,所以置 0。
· 回過頭來看 yaffs_new_obj:
[ yaffs_create_fake_dir --> yaffs_new_obj ]
static struct yaffs_obj *yaffs_new_obj(struct yaffs_dev *dev, int number,
enum yaffs_obj_type type)
{
struct yaffs_obj *the_obj = NULL;
struct yaffs_tnode *tn = NULL;
if (number < 0)
number = yaffs_new_obj_id(dev);
if (type == YAFFS_OBJECT_TYPE_FILE) {
tn = yaffs_get_tnode(dev);
if (!tn)
return NULL;
}
the_obj = yaffs_alloc_empty_obj(dev);
if (!the_obj) {
if (tn)
yaffs_free_tnode(dev, tn);
return NULL;
}
· 前面說過,每個 yaffs_obj 都有一個唯一的序列號,這個序號既可以在創建對象的時候由上層函數指定,也可以由系統分配。如果 number < 0,那就表示由系統分配。序列號分配函數是 yaffs_new_obj_id。我們就不深入到這個函數內部了,只說明一下該函數做了些什麼:
· 系統爲了方便根據對象 id 找到對象本身,將每個對象都通過指針 hashLink 掛入了一個散列表,散列函數是 number % 256,所以這個散列表有 256 個表項。yaffs_new_obj_id 函數每次搜索 10 個表項,從中選取掛接鏈表長度最短的那一項,再根據表索引試圖計算出一個和該索引上掛接對象的 id 號不重複的 id。
· 分配到了 id 號和空閒對象後,再根據對象類型的不同作不同的處理。我們主要關心兩種情況,就是對象類型分別爲文件和目錄的時候:
case YAFFS_OBJECT_TYPE_FILE:
the_obj->variant.file_variant.file_size = 0;
the_obj->variant.file_variant.stored_size = 0;
the_obj->variant.file_variant.shrink_size =
yaffs_max_file_size(dev);
the_obj->variant.file_variant.top_level = 0;
the_obj->variant.file_variant.top = tn;
break;
case YAFFS_OBJECT_TYPE_DIRECTORY:
INIT_LIST_HEAD(&the_obj->variant.dir_variant.children);
INIT_LIST_HEAD(&the_obj->variant.dir_variant.dirty);
break;
· file_size 很好理解;top_level 就是映射樹層高,新建的文件層高爲0。還要預先分配一組Tnode 供該對象使用。 stored_size 和 shrink_size 用於 yaffs2 初始化時的 flash 掃描階段,這裏先跳過。如果該對象是目錄,那麼所做的工作只 是初始化子對象(就是該目錄下的文件或子目錄)雙向鏈表指針,前後指針都指向鏈表頭自身。
· 看過 Fake 對象創建,我們再看看普通對象的創建。按對象類型的不同,有四個函數分別用於創建普通文件、目錄、設備文件、符號鏈接和硬鏈接,它們分別是:
yaffs_create_dir
yaffs_create_file
yaffs_create_special
yaffs_create_symlink
yaffs_link_obj
· 這四個函數最終都調用 yaffs_create_obj 來完成創建對象的工作,只是調用參數不一樣。
static struct yaffs_obj *yaffs_create_obj(enum yaffs_obj_type type,
struct yaffs_obj *parent,
const YCHAR *name,
u32 mode,
u32 uid,
u32 gid,
struct yaffs_obj *equiv_obj,
const YCHAR *alias_str, u32 rdev)
·函數參數中,前面幾個都很好理解,分別是對象類型,上級目錄對象,文件名,訪問權限,
文件所屬 user id 和 group id; equiv_obj 是創建硬鏈接時的原始文件對象;alias_str 是 symlink 名稱;rdev 是設備文件的設備號。
· 函數首先檢查在父目錄中是否已存在同名文件,然後同樣調用 yaffs_new_obj 創建新對象。參數-1 表示由系統自行選擇對象 id。
in->hdr_chunk = 0;
in->valid = 1;
in->variant_type = type;
in->yst_mode = mode;
yaffs_attribs_init(in, gid, uid, rdev);
in->n_data_chunks = 0;
yaffs_set_obj_name(in, name);
in->dirty = 1;
yaffs_add_obj_to_dir(parent, in);
in->my_dev = parent->my_dev;
· 這裏列出的代碼省略了和 wince 相關的條件編譯部分。hdr_chunk 是對象頭所在 chunk,現在還沒有將對象寫入 flash,所以置爲0;該新對象 暫時還沒有數據,所以 n_data_chunks 是0。in->dirty = 1 表示該新對象信息還沒有寫入 flash。然後通過yaffs_add_obj_to_dir 將新對象掛入父對象的子對象鏈表。接下來根據對象類型作不同處理:
switch (type) {
case YAFFS_OBJECT_TYPE_SYMLINK:
in->variant.symlink_variant.alias = str;
break;
case YAFFS_OBJECT_TYPE_HARDLINK:
in->variant.hardlink_variant.equiv_obj = equiv_obj;
in->variant.hardlink_variant.equiv_id = equiv_obj->obj_id;
list_add(&in->hard_links, &equiv_obj->hard_links);
break;
case YAFFS_OBJECT_TYPE_FILE:
case YAFFS_OBJECT_TYPE_DIRECTORY:
case YAFFS_OBJECT_TYPE_SPECIAL:
case YAFFS_OBJECT_TYPE_UNKNOWN:
/* do nothing */
break;
}
· 對於最常用的文件對象和目錄對象不做任何處理;如果是 hardlink,就將新對象掛入原對象的 hard_links 鏈表。從這裏我們可以看出,yaffs2 在內存中是以鏈表的形式處理hardlink 的。在將 hardlink 存儲到 flash 上的時候,則是通過 obj_id 將兩者關聯起來。hardlink 本身佔用一個 chunk 存儲對象頭。
· 最後,通過 yaffs_update_oh 將新對象頭寫入 flash。
8. Yaffs2的垃圾收集機制
· yaffs2的垃圾收集過程實際上包括兩個方面:
· 一是對那些不再使用的page作物理上的刪除。我們在前面介紹chunk釋放函數的時候曾經看到,yaffs2在刪除chunk的時候僅僅是修改內存中的統計量,而真正的刪除工作要留到垃圾收集的時候做。
· 二是處理壞塊。在對flash進行寫操作的時候,我們也許要使用過一個block上的若干page之後才發現這是一個壞塊,此時該塊上已經有部分有用數據了,在垃圾收集的時候要對這種情況進行處理。
· flash在使用過一段時間之後,滿足以上兩種情況的block也許不止一個,那麼,yaffs2按照什麼樣的原則挑選· 合適的塊進行回收呢?我們看下面的函數:
int yaffs_block_ok_for_gc(struct yaffs_dev *dev, struct yaffs_block_info *bi)
{
if (!dev->param.is_yaffs2)
return 1; /* disqualification only applies to yaffs2. */
if (!bi->has_shrink_hdr)
return 1; /* can gc */
yaffs2_find_oldest_dirty_seq(dev);
/* Can't do gc of this block if there are any blocks older than this
* one that have discarded pages.
*/
return (bi->seq_number <= dev->oldest_dirty_seq);
}
void yaffs2_find_oldest_dirty_seq(struct yaffs_dev *dev)
{
if (!dev->param.is_yaffs2)
return;
if (!dev->oldest_dirty_seq)
yaffs_calc_oldest_dirty_seq(dev);
}
· 我們主要關心yaffs2。首先介紹一下什麼是 has_shrink_hdr。
· 還是要提到yaffs2的“軟”刪除機制。假定我們現在需要減小一個文件的長度,比如從128K縮減到64K,在執行close()系統調用之 後,yaffs2會將新的大小寫入文件頭,而這個文件頭是會立即寫入flash的,但是由於yaffs2使用軟刪除機制,原先那後面64K數據仍然殘留在 flash上,也就是說,出現了文件頭和文件內容不一致的情況。此時就將文件頭所在block的描述信息中的一個字段 has_shrink_hdr 置1,表明在垃圾回收時需要特別的處理。如果 has_shrink_hdr=0,那麼該塊是不需要特別的處理,是可以回收的;但是如果 has_shrink_hdr=1,那就需要注意了:如果我們所做的不僅僅是文件尺寸的收縮,而是文件的刪除,並且在物理刪除文件內容之前通過垃圾收集 機制將文件頭刪掉了,那麼殘留的文件內容就成了“沒娘要的孩子”,難以處理了。所以,我們必須先處理文件的殘留內容,然後處理文件頭。下面我們來看看yaffs2是如何實現處理這個目標的:
yaffs_calc_oldest_dirty_seq函數中:
for (i = dev->internal_start_block; i <= dev->internal_end_block; i++) {
if (b->block_state == YAFFS_BLOCK_STATE_FULL &&
(u32)(b->pages_in_use - b->soft_del_pages) <
dev->param.chunks_per_block &&
b->seq_number < seq) {
seq = b->seq_number;
block_no = i;
}
b++;
}
/* Can't do gc of this block if there are any blocks older than this
* one that have discarded pages.
*/
return (bi->seq_number <= dev->oldest_dirty_seq);
· 在分析這段代碼之前,我們再來回顧一下yaffs2的chunk分配過程和特點。如前文所述,yaffs2在分配chunk的時候遵循兩個原則: 一是在block內部嚴格從低地址的chunk向高地址的chunk按次序分配,二是一定要將一個block內的page全部分配完畢後才另行選擇 block進行分配。而且在分配的時候每挑選一個block就會遞增一個序號。這樣我們從block的序號就可以推斷出該block的分配順序。
· 除此之外,yaffs2會在應用程序作close()系統調用的時候將新的文件頭寫入flash。因此,我們可以作出這樣的結論:文件頭所在block的序號,一定大於等於文件內容所在block的序號。 這樣,如果一個block信息結構內的 has_shrink_hdr 字段爲1,並且該block的序號在系統中最小,我們就可以認爲該block上的所 有文件頭對應的文件已經沒有殘餘信息留在flash上了——這些殘餘信息如果存在,它們所在block的序號一定更小。有了這個結論,上面的代碼就不難理 解了,所以就不作解釋了。
· 這個函數返回之後,我們就知道函數參數所指向的block是否可以回收了。
9.總結
· 斑點大牛寫的文章的確是不一般,到現在都一點都沒有過時,關於這篇文章是否是原創這個問題,我想了想,如果說是轉載,我又不是一字未改,改動還不小,但是要是說是原創呢,內容又都是別人寫的,很尷尬,但是爲了這一天多的努力,我還是寫成原創吧,如果斑點大牛有所意見,通知一聲,立刻改。
如果覺得我的文章還有點收穫,就點個贊吧! d=====( ̄▽ ̄*)b