yaffs2源代碼分析(最新版)

· 曾經,有位叫斑點的大牛,寫了一篇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

發佈了15 篇原創文章 · 獲贊 19 · 訪問量 4678
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章