yaffs2 源碼分析

yaffs2源代碼情景分析 (小壽轉載)

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的源代 碼一致。
我們先看存儲空間的分配(在yaffs_guts.c中。這個文件也是yaffs2文件系統的核心部分):
static int yaffs_AllocateChunk(yaffs_Device * dev, int useReserve,
yaffs_BlockInfo **blockUsedPtr)
{
int retVal;
yaffs_BlockInfo *bi;

if (dev->allocationBlock < 0) {
/* Get next block to allocate off */
dev->allocationBlock = yaffs_FindBlockForAllocation(dev);
dev->allocationPage = 0;
}
函 數有三個參數,dev是yaffs_Device結構的指針,yaffs2用這個結構來記錄一個NAND器件的屬性(如block和page的大小)和系 統運行過程中的一些統計值(如器件中可用chunk的總數),還用這個結構維護着一組NAND操作函數(如讀、寫、刪除)的指針。整個結構體比較大,我們 會按情景的不同分別分析。useReserve表示是否使用保留空間。yaffs2文件系統並不會將所有的存儲空間全部用於存儲文件系統數據,而要空出部 分block用於垃圾收集時使用。一般情況下這個參數都是0,只有在垃圾收集時需要分配存儲空間的情況下將該參數置1。yaffs_BlockInfo是 描述block屬性的結構,主要由一些統計變量組成,比如該block內還剩多少空閒page等。我們同樣在具體情景中再分析這個結構中的字段含義。
函數首先判斷dev->allocationBlock的值是否小於0。yaffs_Device結構內的allocationBlock字段用於記錄當前從中分配chunk(page)的那個block的序號當一個block內的所有page全部分配完畢時,就將這個字段置爲-1,下次進入該函數時就會重新挑選空閒的block 。這裏我們假定需要重新挑選空閒block,因此進入yaffs_FindBlockForAllocation函數:

[yaffs_AllocateChunk() => yaffs_FindBlockForAllocation()]
static int yaffs_FindBlockForAllocation(yaffs_Device * dev)
{
int i;
yaffs_BlockInfo *bi;
if (dev->nErasedBlocks < 1) {
/* Hoosterman we've got a problem.
* Can't get space to gc
*/
T(YAFFS_TRACE_ERROR,
(TSTR("yaffs tragedy: no more eraased blocks" TENDSTR)));
return -1;
}
dev->nErasedBlocks記錄着器件內所有可供分配的block的數量。如果該值小於1,那顯然是有問題了。不但正常的分配請求無法完成,就連垃圾收集都辦不到了。
for (i = dev->internalStartBlock; i <= dev->internalEndBlock; i++) {
dev->allocationBlockFinder++;
if (dev->allocationBlockFinder < dev->internalStartBlock
|| dev->allocationBlockFinder > dev->internalEndBlock) {
dev->allocationBlockFinder = dev->internalStartBlock;
internalStartBlock和internalEndBlock分別是yaffs2使用的block的起始序號和結束序號。也就是說yaffs2文件系統不一定要佔據整個Flash,可以只佔用其中的一部分。
dev->allocationBlockFinder記錄着上次分配的塊的序號。如果已經分配到系統尾部,就從頭重新開始搜索可用塊。
bi = yaffs_GetBlockInfo(dev, dev->allocationBlockFinder);
if (bi->blockState == YAFFS_BLOCK_STATE_EMPTY) {
bi->blockState = YAFFS_BLOCK_STATE_ALLOCATING;
dev->sequenceNumber++;
bi->sequenceNumber = dev->sequenceNumber;
dev->nErasedBlocks--;
T(YAFFS_TRACE_ALLOCATE,
(TSTR("Allocated block %d, seq %d, %d left" TENDSTR),
dev->allocationBlockFinder, dev->sequenceNumber,
dev->nErasedBlocks));
return dev->allocationBlockFinder;
}
yaffs_GetBlockInfo 函數獲取指向block信息結構的指針,該函數比較簡單,就不詳細介紹了。yaffs_BlockInfo結構中的blockState成員描述該 block的狀態,比如空,滿,已損壞,當前分配中,等等。因爲是要分配空閒塊,所以塊狀態必須是 YAFFS_BLOCK_STATE_EMPTY,如果不是,就繼續測試下一個block。找到以後將block狀態修改爲 YAFFS_BLOCK_STATE_ALLOCATING,表示當前正從該block中分配存儲空間。正常情況下,系統中只會有一個block處於該狀 態。另外還要更新統計量ErasedBlocks和sequenceNumber。這個sequenceNumber記錄着各block被分配出去的先後順序,以後在垃圾收集的時候會以此作爲判斷該block是否適合回收的依據。
現在讓我們返回到函數 yaffs_AllocateChunk中。yaffs_CheckSpaceForAllocation()函數檢查Flash上是否有足夠的可用空間,通過檢查後,就從當前供分配的block上切下一個chunk:
if (dev->allocationBlock >= 0) {
bi = yaffs_GetBlockInfo(dev, dev->allocationBlock);
retVal = (dev->allocationBlock * dev->nChunksPerBlock) +
dev->allocationPage;
bi->pagesInUse++;
yaffs_SetChunkBit (dev, dev->allocationBlock,
dev->allocationPage);
dev->allocationPage++;
dev->nFreeChunks--;
/* If the block is full set the state to full */
if (dev->allocationPage >= dev->nChunksPerBlock) {
bi->blockState = YAFFS_BLOCK_STATE_FULL;
dev->allocationBlock = -1;
}
if(blockUsedPtr)
*blockUsedPtr = bi;
return retVal;
}
dev->allocationPage記錄着上次分配的chunk在block中的序號,每分配一次加1 。從這裏我們可以看出,系統在分配chunk的時候是從block的開頭到結尾按序分配的,直到一個block內的所有chunk全部分配完畢爲止。retVal是該chunk在整個device內的總序號。PagesInUse記錄着該block中已分配使用的page的數量。
系統在設備描述結構yaffs_Device 中維護着一張位圖,該位圖的每一位都代表着Flash上的一個chunk的狀態。yaffs_SetChunkBit()將剛分配得到的chunk在位圖中的對應位置1,表明該塊已被使用。更新一些統計量後,就可以返回了。
上面就是我們分析的第一個函數,整個過程比較長,因爲要順帶介紹一些重要的數據結構。接下來的分析相對就會簡短一些,要不您看着累,俺打字更累:-)


作者:斑點
Email: [email protected]

看 過chunk分配以後,我們再來chunk的釋放。和chunk分配不同的是,chunk的釋放在大多數情況下並不釋放對應的物理介質,這是因爲NAND 雖然可以按page寫,但只能按block擦除,所以物理介質的釋放要留到垃圾收集或一個block上的所有page全部變成空閒的時候才進行。根據應用 場合的不同,chunk的釋放方式並不唯一,分別由yaffs_DeleteChunk函數和yaffs_SoftDeleteChunk函數完成。我們 先看yaffs_DeleteChunk:

void yaffs_DeleteChunk(yaffs_Device * dev, int chunkId, int markNAND, int lyn)

chunkId就是要刪除的chunk的序號,markNand參數用於yaffs一代的代碼中,yaffs2不使用該參數。
參數lyn在調用該函數時置成當前行號(__LINE__),用於調試。
首 先通過yaffs_GetBlockInfo獲得chunk所在block的信息描述結構指針,然後就跑到else裏面去了。if語句的判斷條件中有一 條!dev->isYaffs2,所以對於yaffs2而言是不會執行if分支的。在else分支裏面只是遞增一下統計計數就出來了,我們接着往下 看。

if (bi->blockState == YAFFS_BLOCK_STATE_ALLOCATING ||
bi->blockState == YAFFS_BLOCK_STATE_FULL ||
bi->blockState == YAFFS_BLOCK_STATE_NEEDS_SCANNING ||
bi->blockState == YAFFS_BLOCK_STATE_COLLECTING) {
dev->nFreeChunks++;
yaffs_ClearChunkBit(dev, block, page);
bi->pagesInUse--;
if (bi->pagesInUse == 0 &&
!bi->hasShrinkHeader &&
bi->blockState != YAFFS_BLOCK_STATE_ALLOCATING &&
bi->blockState != YAFFS_BLOCK_STATE_NEEDS_SCANNING) {
yaffs_BlockBecameDirty(dev, block);
}
} else {
/* T(("Bad news deleting chunk %d/n",chunkId)); */
}
首 先要判斷一下該block上是否確實存在着可釋放的chunk。block不能爲空,不能是壞塊。 YAFFS_BLOCK_STATE_NEEDS_SCANNING表明正對該塊進行垃圾回收,我們後面會分析; YAFFS_BLOCK_STATE_NEEDS_SCANNING在我手上的源代碼中似乎沒有用到。
通過判斷以後,所做的工作和chunk分 配函數類似,只是一個遞增統計值,一個遞減。遞減統計值以後還要判斷該block上的page是否已全部釋放,如果已全部釋放,並且不是當前分配塊,就通 過yaffs_BlockBecameDirty函數刪除該block,只要能通過刪除操作(不是壞塊),該block就又可以用於分配了。
相比較來說,yaffs_SoftDeleteChunk所做的工作就簡單多了。關鍵的代碼只有兩行:
static void yaffs_SoftDeleteChunk(yaffs_Device * dev, int chunk)
{
……
theBlock->softDeletions++;
dev->nFreeChunks++;
……
}
這裏遞增的是yaffs_blockInfo結構中的另一個統計量 softDeletions,而沒有修改pagesInUse成員,也沒有修改chunk狀態位圖。那麼,這兩個函數的應用場合有什麼區別呢?
一般來說,yaffs_DeleteChunk用於文件內容的更新。比如我們要修改文件中的部分內容,這時候yaffs2會分配新的chunk,將更改後的內容寫入新chunk中,原chunk的內容自然就沒有用了,所以要將pageInUse減1,並修改位圖;
yaffs_SoftDeleteChunk 用於文件的刪除。yaffs2在刪除文件的時候只是刪除該文件在內存中的一些描述結構,而被刪除的文件所佔用的chunk不會立即釋放,也就是不會刪除文 件內容,在後續的文件系統操作中一般也不會把這些chunk分配出去,直到系統進行垃圾收集的時候纔有選擇地釋放這些chunk。熟悉DOS的朋友可能還 記得,DOS在刪除的文件的時候也不會立即刪除文件內容,只是將文件名的第一個字符修改爲0xA5,事後還可以恢復文件內容。yaffs2在這點上是類似 的。


1.文件地址映射
上面說到,yaffs文件系統在更新文件數據的時候,會分配一塊新的chunk,也就是說,同 樣的文件偏移地址,在該地址上的數據更新前和更新後,其對應的flash上的存儲地址是不一樣的。那麼,如何根據文件內偏移地址確定flash存儲地址 呢?最容易想到的辦法,就是在內存中維護一張映射表。由於flash基本存儲單位是chunk,因此,只要將以chunk描述的文件偏移量作爲表索引,將 flash chunk序號作爲表內容,就可以解決該問題了。但是這個方法有幾個問題,首先就是在做seek操作的時候,要從表項0開始按序搜索,對於大文件會消耗很 多時間;其次是在建立映射表的時候,無法預計文件大小的變化,於是就可能在後來的操作中頻繁釋放分配內存以改變表長,造成內存碎片。yaffs的解決方法 是將這張大的映射表拆分成若干個等長的小表,並將這些小表組織成樹的結構,方便管理。我們先看小表的定義:
union yaffs_Tnode_union {
union yaffs_Tnode_union *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)已經有說明,俺就不在此處饒舌了。下面看具體 函數。
(to be continued)


作者:斑點
Email: [email protected]

爲了行文方便,後文中將yaffs_Tnode這個指針數組稱爲“一組”Tnode,而將數組中的每個元素稱爲“一個”Tnode。樹中的每個節點,都是“一組”Tnode。
先看映射樹的節點的分配。
static yaffs_Tnode *yaffs_GetTnode(yaffs_Device * dev)
{
yaffs_Tnode *tn = yaffs_GetTnodeRaw(dev);

if(tn)
memset(tn, 0, (dev->tnodeWidth * YAFFS_NTNODES_LEVEL0)/8);

return tn;
}
調用yaffs_GetTnodeRaw分配節點,然後將得到的節點初始化爲零。
static yaffs_Tnode *yaffs_GetTnodeRaw(yaffs_Device * dev)
{
yaffs_Tnode *tn = NULL;

/* If there are none left make more */
if (!dev->freeTnodes) {
yaffs_CreateTnodes(dev, YAFFS_ALLOCATION_NTNODES);
}
當前所有空閒節點組成一個鏈表,dev->freeTnodes是這個鏈表的表頭。我們假定已經沒有空閒節點可用,需通過yaffs_CreateTnodes創建一批新的節點。

static int yaffs_CreateTnodes(yaffs_Device * dev, int nTnodes)
{
......
tnodeSize = (dev->tnodeWidth * YAFFS_NTNODES_LEVEL0)/8;
newTnodes = YMALLOC(nTnodes * tnodeSize);
mem = (__u8 *)newTnodes;

上 面說過,葉節點中一個Tnode的位寬默認爲16位,也就是可以表示65536個chunk。對於時下的大容量flash,chunk的大小爲2K,因此 在默認情況下yaffs2所能尋址的最大flash空間就是128M。爲了能將yaffs2用於大容量flash上,代碼作者試圖通過兩種手段解決這個問 題。第一種手段就是這裏的dev->tnodeWidth,通過增加單個Tnode的位寬,就可以增加其所能表示的最大chunk Id;另一種手段是我們後面將看到的chunk group,通過將若干個chunk合成一組用同一個id來表示,也可以增加系統所能尋址的chunk範圍。俺爲了簡單,分析的時候不考慮這兩種情況,因 此tnodeWidth取默認值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 < nTnodes -1; i++) {
curr = (yaffs_Tnode *) &mem[i * tnodeSize];
next = (yaffs_Tnode *) &mem[(i+1) * tnodeSize];
curr->internal[0] = next;
}
每組Tnode的第一個元素作爲指針指向下一組Tnode。完成鏈表構造後,還要遞增統計量,並將新得到的Tnodes掛入一個全局管理鏈表yaffs_TnodeList:
dev->nFreeTnodes += nTnodes;
dev->nTnodesCreated += nTnodes;
tnl = YMALLOC(sizeof(yaffs_TnodeList));
if (!tnl) {
T(YAFFS_TRACE_ERROR,
(TSTR
("yaffs: Could not add tnodes to management list" TENDSTR)));
} else {
tnl->tnodes = newTnodes;
tnl->next = dev->allocatedTnodeList;
dev->allocatedTnodeList = tnl;
}

回到yaffs_GetTnodeRaw,創建了若干組新的Tnode以後,從中切下所需的Tnode,並修改空閒鏈表表頭指針:

if (dev->freeTnodes) {
tn = dev->freeTnodes;
dev->freeTnodes = dev->freeTnodes->internal[0];
dev->nFreeTnodes--;
}

至此,分配工作就完成了。相比較來說,釋放Tnodes的工作就簡單多了,簡單的鏈表和統計值操作:
static void yaffs_FreeTnode(yaffs_Device * dev, yaffs_Tnode * tn)
{
if (tn) {
tn->internal[0] = dev->freeTnodes;
dev->freeTnodes = tn;
dev->nFreeTnodes++;
}
}



作者:斑點
Email: [email protected]

看過Tnode的分配和釋放,我們再來看看這些Tnode是如何使用的。在後文中,我們把以chunk爲單位的文件內偏移稱作邏輯chunk id,文件內容在flash上的實際存儲位置稱作物理chunk id。先看一個比較簡單的函數。

void yaffs_PutLevel0Tnode(yaffs_Device *dev, yaffs_Tnode *tn, unsigned pos, unsigned val)
這個函數將某個Tnode設置爲指定的值。tn是指向一組Tnode的指針;pos是所要設置的那個Tnode在該組Tnode中的索引;val就是所要設置的值,也就是物理chunk id。函數名中的Level0指映射樹的葉節點。函數開頭幾行如下:

pos &= YAFFS_TNODES_LEVEL0_MASK;
val >>= dev->chunkGroupBits;
bitInMap = pos * dev->tnodeWidth;
wordInMap = bitInMap /32;
bitInWord = bitInMap & (32 -1);
mask = dev->tnodeMask << bitInWord;

上 面說過,一組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[wordInMap] &= ~mask;    //清零
map[wordInMap] |= (mask & (val << bitInWord));    //置位

if(dev->tnodeWidth > (32-bitInWord)) {    //當chunk序號跨越32位邊界
bitInWord = (32 - bitInWord);    //計算出已讀取部分
wordInMap++;    //讀取位置+1
mask = dev->tnodeMask >> (/*dev->tnodeWidth -*/ bitInWord);    //未讀取部分
map[wordInMap] &= ~mask;    //清零
map[wordInMap] |= (mask & (val >> bitInWord));    //置位
}

if 語句判斷當前chunk序號是否跨越當前32位邊界。整個代碼初看起來比較難理解,其實只要將 dev->tnodeWidth以16或32代入,就很好懂了。還有一個類似的函數yaffs_GetChunkGroupBase,返回由tn和 pos確定的一組chunk的起始序號,就不詳細分析了。

現在我們假設有這樣一個情景:已知文件偏移地址,要找到flash上對應的存儲地址,該怎麼做呢?這項工作的主體是由函數yaffs_FindLevel0Tnode完成的。

static yaffs_Tnode *yaffs_FindLevel0Tnode(yaffs_Device * dev,
yaffs_FileStructure * fStruct,
__u32 chunkId)
{

yaffs_Tnode *tn = fStruct->top;
__u32 i;
int requiredTallness;
int level = fStruct->topLevel;
函數參數中,fStruct是指向文件描述結構的指針,該結構保存着文件大小、映射樹層高、映射樹頂層節點指針等信息。chunkId是邏輯chunk id。
fStruct->top是映射樹頂層節點指針,fStruct->topLevel是映射樹層高。注意:當只有一層時,層高爲0。
/* First check we're tall enough (ie enough topLevel) */
i = chunkId >> YAFFS_TNODES_LEVEL0_BITS;
requiredTallness = 0;
while (i) {
i >>= YAFFS_TNODES_INTERNAL_BITS;
requiredTallness++;
}
if (requiredTallness > fStruct->topLevel) {
/* Not tall enough, so we can't find it, return NULL. */
return NULL;
}
在 看這段代碼之前,我們先用一個例子來回顧一下映射樹的組成。假定我們有一個大小爲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節點,而fStruct->top就指向這 組作爲指針數組使用的Tnode。隨着文件長度的增大,所需的葉節點越多,非葉節點也越多,樹也就越長越高。
回過頭來看代碼,首先是檢查函數參 數chunkId是否超過文件長度。作爲非葉節點使用的Tnode每組有8個指針,需要3位二進制碼對其進行索引,因此樹每長高一層,邏輯chunkId 就多出3位。相反,每3位非零chunkId就代表一層非葉節點。while循環根據這個原則計算參數chunkId所對應的樹高。如果樹高超過了文件結 構中保存的樹高,那就說明該邏輯chunkId已經超出文件長度了。通過文件長度檢查之後,同樣根據上面的原則,就可以找到邏輯chunkId對應的物理 chunkId了。具體的操作通過一個while循環完成:

/* Traverse down to level 0 */
while (level > 0 && tn) {
tn = tn->
internal[(chunkId >>
( YAFFS_TNODES_LEVEL0_BITS +
(level - 1) *
YAFFS_TNODES_INTERNAL_BITS)
) &
YAFFS_TNODES_INTERNAL_MASK];
level--;

}

return tn;

將返回值和邏輯chunk id作爲參數調用yaffs_GetChunkGroupBase,就可以得到物理chunk id了。

注:
1. 歡迎轉載,轉載時請註明作者
2. 未經作者同意,不得用於商業目的
3. 俺近日犯頭痛,更新會慢一點



作者:斑點
Email: [email protected]

下面我們看另一個情景,看看當文件長度增加的時候,映射樹是如何擴展的。主要函數爲
static yaffs_Tnode *yaffs_AddOrFindLevel0Tnode(yaffs_Device * dev,
yaffs_FileStructure * fStruct,
__u32 chunkId,
yaffs_Tnode *passedTn)
函數的前幾行和yaffs_FindLevel0Tnode一樣,對函數參數作一些檢查。通過檢查之後,首先看原映射樹是否有足夠的高度,如果高度不夠,就先將其“拔高”:
if (requiredTallness > fStruct->topLevel) {
/* Not tall enough,gotta make the tree taller */
for (i = fStruct->topLevel; i < requiredTallness; i++) {
tn = yaffs_GetTnode(dev);
if (tn) {
tn->internal[0] = fStruct->top;
fStruct->top = tn;
} else {
T(YAFFS_TRACE_ERROR,
(TSTR("yaffs: no more tnodes" TENDSTR)));
}
}
fStruct->topLevel = requiredTallness;
}
for循環完成增加新層的功能。新增的每一層都只有一個節點(即一組Tnode),fStruct->top始終指向最新分配的節點。將映射樹擴展到所需的高度之後,再根據需要將其“增肥”,擴展其“寬度”:
l = fStruct->topLevel;
tn = fStruct->top;
if(l > 0) {
while (l > 0 && tn) {
x = (chunkId >>
( 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_GetTnode(dev);
} else if(l == 1) {
/* Looking from level 1 at level 0 */
if (passedTn) {
/* If we already have one, then release it.*/
if(tn->internal[x])
yaffs_FreeTnode(dev,tn->internal[x]);
tn->internal[x] = passedTn;
} else if(!tn->internal[x]) {
/* Don't have one, none passed in */
tn->internal[x] = yaffs_GetTnode(dev);
}
}
tn = tn->internal[x];
l--;
}
}
上面“拔高”的時候是從下往上“蓋樓”,這裏“增肥”的時候是從上往下“擴展”。
tn->internal[x] 爲空表示下層節點尚未創建,需要通過yaffs_GetTnode分配之,就是“增肥”了。如果函數參數passedTn有效,就用該組Tnode代替 level0上原先的那組Tnode;否則按需分配新的Tnode組。所以這裏的函數名似乎應該取作 yaffs_AddOrFindOrReplaceLevel0Tnode更加恰當。不過這個新名字也太長了些……

樹的創建、搜索和擴展說完了,下面該說什麼?……對了,收縮和刪除。不過看過創建搜索擴展之後,收縮和刪除已經沒什麼味道了。主要函數有:
yaffs_DeleteWorker()
yaffs_SoftDeleteWorker()
yaffs_PruneWorker()
前 兩者用於刪除,第三個用於收縮。都是從level0開始,以遞歸的方式從葉節點向上刪,並釋放被刪除Tnode所對應的物理chunk。遞歸,偉大的遞歸 啊……俺不想把這篇文章做成遞歸算法教程,除了遞歸這三個函數也就不剩啥了,所以一概從略。唯一要說的就是yaffs_DeleteWorker和 yaffs_SoftDeleteWorker的區別,這兩個函數非常類似,只是在釋放物理chunk的時候分別調用yaffs_DeleteChunk 和yaffs_SoftDeleteChunk。其中函數yaffs_DeleteWorker在yaffs2中似乎是不用的,而 yaffs_SoftDeleteWorker主要用於在刪除文件時資源的釋放。

p.s. 這個星期犯頭疼,沒力氣寫東西,所以短一些;希望下星期能好起來。接下來的幾篇打算分析文件對象管理,大家捧個場 :-)



作者:斑點
Emai:[email protected]

7.文件系統對象
在 yaffs2中,不管是文件還是目錄或者是鏈接,在內存都用一個結構體yaffs_ObjectStruct來描述。我們先簡要介紹一下這個結構體中的幾 個關鍵字段,然後再來看代碼。在後文中提到“文件”或“文件對象”,若不加特別說明,都指廣義的“文件”,既可以是文件,也可以是目錄。
__u8 deleted:1; /* This should only apply to unlinked files. */
__u8 softDeleted:1; /* it has also been soft deleted */
__u8 unlinked:1; /* An unlinked file. The file should be in the unlinked directory.*/
這 三個字段用於描述該文件對象在刪除過程中所處的階段。在刪除文件時,首先要將文件從原目錄移至一個特殊的系統目錄/unlinked,以此拒絕應用程序對 該文件的訪問,此時將unlinked置1;然後判斷該文件長度是否爲0,如果爲0,該文件就可以直接刪除,此時將deleted置1;如果不爲0,就將 deleted和softDelted都置1,表明該文件數據所佔據的chunk還沒有釋放,要留待後繼處理。
struct yaffs_ObjectStruct *parent;
看名字就知道,該指針指向上層目錄。
int chunkId;
每個文件在flash上都有一個文件頭,存儲着該文件的大小、所有者、創建修改時間等信息。chunkId就是該文件頭在flash上的chunk序號。
__u32 objectId; /* the object id value */
每一個文件系統對象都被賦予一個唯一的編號,作爲對象標識,也用於將該對象掛入一個散列表,加快對象的搜索速度。
yaffs_ObjectType variantType;
yaffs_ObjectVariant variant;
前者表示該對象的類型,是目錄、普通文件還是鏈接文件。後者是一個聯合體,根據對象類型的不同有不同的解釋。
其餘的成員變量,我們在後面結合函數一起分析。

下面我們來看相關的函數。先看一個簡單的:
static yaffs_Object *yaffs_CreateFakeDirectory(yaffs_Device * dev, int number,
__u32 mode)
所 謂Fake Directory,就是僅存在於內存中,用於管理目的的目錄對象,比如我們上面提到的unlinked目錄。這種類型的目錄有一些特別的地方,如禁止改 名、禁止刪除等。由於對象僅存在於內存中,因此不涉及對硬件的操作,所以函數體很簡單。首先通過yaffs_CreateNewObject獲得一個新對 象,然後對其中的一些字段初始化。先把字段初始化看一下,順便再介紹一些字段:
renameAllowed表示是否允許改名,對於fake對象爲0;
unlinkAllowed表示是否允許刪除,對於fake對象同樣爲0;
yst_mode就是linux中的訪問權限位;
chunkId是對象頭所在chunk,由於fake對象不佔flash存儲空間,所以置0。
回過頭來看yaffs_CreateNewObject:
[yaffs_CreateFakeDirectory --> yaffs_CreateNewObject]
yaffs_Object *yaffs_CreateNewObject(yaffs_Device * dev, int number,
yaffs_ObjectType type)
{

yaffs_Object *theObject;

if (number < 0) {
number = yaffs_CreateNewObjectNumber(dev);
}

theObject = yaffs_AllocateEmptyObject(dev);
前 面說過,每個yaffs_Object都有一個唯一的序列號,這個序號既可以在創建對象的時候由上層函數指定,也可以由系統分配。如果number < 0,那就表示由系統分配。序列號分配函數是 yaffs_CreateNewObjectNumber。我們就不深入到這個函數內部了,只說明一下該函數做了些什麼:
系統爲了方便根據對象 id找到對象本身,將每個對象都通過指針hashLink掛入了一個散列表,散列函數是number % 256,所以這個散列表有256個表項。yaffs_CreateNewObjectNumber函數每次搜索10個表項,從中選取掛接鏈表長度最短的那 一項,再根據表索引試圖計算出一個和該索引上掛接對象的id號不重複的id。

分配到了id號和空閒對象後,再根據對象類型的不同作不同的處理。我們主要關心兩種情況,就是對象類型分別爲文件和目錄的時候:
case YAFFS_OBJECT_TYPE_FILE:
theObject->variant.fileVariant.fileSize = 0;
theObject->variant.fileVariant.scannedFileSize = 0;
theObject->variant.fileVariant.shrinkSize = 0xFFFFFFFF; /* max __u32 */
theObject->variant.fileVariant.topLevel = 0;
theObject->variant.fileVariant.top = yaffs_GetTnode(dev);
break;
case YAFFS_OBJECT_TYPE_DIRECTORY:
INIT_LIST_HEAD(&theObject->variant.directoryVariant.children);
break;
fileSize 很好理解;topLevel就是映射樹層高,新建的文件層高爲0。還要預先分配一組Tnode供該對象使用。scannedFileSize和 shrinkSize用於yaffs2初始化時的flash掃描階段,這裏先跳過。如果該對象是目錄,那麼所做的工作只是初始化子對象(就是該目錄下的文 件或子目錄)雙向鏈表指針,前後指針都指向鏈表頭自身。

看過Fake對象創建,我們再看看普通對象的創建。按對象類型的不同,有四個函數分別用於創建普通文件、目錄、設備文件、符號鏈接和硬鏈接,它們分別是:
yaffs_MknodFile;
yaffs_MknodDirectory;
yaffs_MknodSpecial;
yaffs_MknodSymLink;
yaffs_Link
這四個函數最終都調用yaffs_MknodObject來完成創建對象的工作,只是調用參數不一樣。
static yaffs_Object *yaffs_MknodObject(yaffs_ObjectType type,
yaffs_Object * parent,
const YCHAR * name,
__u32 mode,
__u32 uid,
__u32 gid,
yaffs_Object * equivalentObject,
const YCHAR * aliasString, __u32 rdev)
函 數參數中,前面幾個都很好理解,分別是對象類型,上級目錄對象,文件名,訪問權限,文件所屬user id和group id; equivalentObject是創建硬鏈接時的原始文件對象;aliasString是symLink名稱;rdev是設備文件的設備號。
函數首先檢查在父目錄中是否已存在同名文件,然後同樣調用yaffs_CreateNewObject創建新對象。參數-1表示由系統自行選擇對象id。
if (in) {
in->chunkId = -1;
in->valid = 1;
in->variantType = type;
in->yst_mode = mode;
in->yst_atime = in->yst_mtime = in->yst_ctime = Y_CURRENT_TIME;
in->yst_rdev = rdev;
in->yst_uid = uid;
in->yst_gid = gid;
in->nDataChunks = 0;
yaffs_SetObjectName(in, name);
in->dirty = 1;
yaffs_AddObjectToDirectory(parent, in);
in->myDev = parent->myDev;
這 裏列出的代碼省略了和wince相關的條件編譯部分。chunkId是對象頭所在chunk,現在還沒有將對象寫入flash,所以置爲-1;該新對象暫 時還沒有數據,所以nDataChunks是0。in->dirty = 1表示該新對象信息還沒有寫入flash。然後通過yaffs_AddObjectToDirectory將新對象掛入父對象的子對象鏈表。接下來根據對 象類型作不同處理:
switch (type) {
case YAFFS_OBJECT_TYPE_SYMLINK:
in->variant.symLinkVariant.alias =
yaffs_CloneString(aliasString);
break;
case YAFFS_OBJECT_TYPE_HARDLINK:
in->variant.hardLinkVariant.equivalentObject =
equivalentObject;
in->variant.hardLinkVariant.equivalentObjectId =
equivalentObject->objectId;
list_add(&in->hardLinks, &equivalentObject->hardLinks);
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,就將新對象掛入原對象的 hardLinks鏈表。從這裏我們可以看出,yaffs2在內存中是以鏈表的形式處理hardlink的。在將hardlink存儲到flash上的時 候,則是通過objectId將兩者關聯起來。Hardlink本身佔用一個chunk存儲對象頭。
最後,通過yaffs_UpdateObjectHeader將新對象頭寫入flash。




作者:斑點
Email: [email protected]

8. Yaffs2的垃圾收集機制
yaffs2的垃圾收集過程實際上包括兩個方面:
·一是對那些不再使用的page作物理上的刪除。我們在前面介紹chunk釋放函數的時候曾經看到,yaffs2在刪除chunk的時候僅僅是修改內存中的統計量,而真正的刪除工作要留到垃圾收集的時候做。
·二是處理壞塊。在對flash進行寫操作的時候,我們也許要使用過一個block上的若干page之後才發現這是一個壞塊,此時該塊上已經有部分有用數據了,在垃圾收集的時候要對這種情況進行處理。
flash在使用過一段時間之後,滿足以上兩種情況的block也許不止一個,那麼,yaffs2按照什麼樣的原則挑選合適的塊進行回收呢?我們看下面的函數:
static int yaffs_BlockNotDisqualifiedFromGC(yaffs_Device * dev,
yaffs_BlockInfo * bi)
這個函數用來判定給定的塊bi是否可以回收。
if (!dev->isYaffs2)
return 1; /* disqualification only applies to yaffs2. */
if (!bi->hasShrinkHeader)
return 1; /* can gc */
我們主要關心yaffs2。首先介紹一下什麼是 hasShrinkHeader。
還 是要提到yaffs2的“軟”刪除機制。假定我們現在需要減小一個文件的長度,比如從128K縮減到64K,在執行close()系統調用之 後,yaffs2會將新的大小寫入文件頭,而這個文件頭是會立即寫入flash的,但是由於yaffs2使用軟刪除機制,原先那後面64K數據仍然殘留在 flash上,也就是說,出現了文件頭和文件內容不一致的情況。此時就將文件頭所在block的描述信息中的一個字段hasShrinkHeader置 1,表明在垃圾回收時需要特別的處理。如果hasShrinkHeader=0,那麼該塊是不需要特別的處理,是可以回收的;但是如果 hasShrinkHeader=1,那就需要注意了:如果我們所做的不僅僅是文件尺寸的收縮,而是文件的刪除,並且在物理刪除文件內容之前通過垃圾收集 機制將文件頭刪掉了,那麼殘留的文件內容就成了“沒娘要的孩子”,難以處理了。所以,我們必須先處理文件的殘留內容,然後處理文件頭。 下面我們來看看yaffs2是如何實現處理這個目標的:
/* Find the oldest dirty sequence number if we don't know it and save it
* so we don't have to keep recomputing it.
*/
if (!dev->oldestDirtySequence) {
seq = dev->sequenceNumber;
for (i = dev->internalStartBlock; i <= dev->internalEndBlock;
i++) {
b = yaffs_GetBlockInfo(dev, i);
if (b->blockState == YAFFS_BLOCK_STATE_FULL &&
(b->pagesInUse - b->softDeletions) <
dev->nChunksPerBlock && b->sequenceNumber < seq) {
seq = b->sequenceNumber;
}
}
dev->oldestDirtySequence = seq;
}

/* Can't do gc of this block if there are any blocks older than this one that have
* discarded pages.
*/
return (bi->sequenceNumber <= dev->oldestDirtySequence);
在 分析這段代碼之前,我們再來回顧一下yaffs2的chunk分配過程和特點。如前文所述,yaffs2在分配chunk的時候遵循兩個原則:一是在 block內部嚴格從低地址的chunk向高地址的chunk按次序分配,二是一定要將一個block內的page全部分配完畢後才另行選擇block進 行分配。而且在分配的時候每挑選一個block就會遞增一個序號。這樣我們從block的序號就可以推斷出該block的分配順序。
除此之外,yaffs2會在應用程序作clsoe()系統調用的時候將新的文件頭寫入flash。因此,我們可以作出這樣的結論:文件頭所在block的序號,一定大於等於文件內容所在block的序號 。 這樣,如果一個block信息結構內的hasShrinkHeader字段爲1,並且該block的序號在系統中最小,我們就可以認爲該block上的所 有文件頭對應的文件已經沒有殘餘信息留在flash上了——這些殘餘信息如果存在,它們所在block的序號一定更小。有了這個結論,上面的代碼就不難理 解了,所以就不作解釋了。
這個函數返回之後,我們就知道函數參數所指向的block是否可以回收了。但是,一個flash上可以回收的block大多數情況下不止一個,如何挑選出最合適的一個呢?——且聽下回分解:-)

p.s. 最近廠裏比較忙,可能更新會慢下來。私人企業,大家都知道的,時間沒辦法保證啊。

這 陣子開始重讀yaffs2,發現上次看的時候有很多地方理解上還是有問題的,寫出來的東西也不嚴謹,最主要的是,文章不是很有條理。俺打算寫完垃圾收集就 暫停,待俺第二遍(也許還有第三遍,第四遍……)看結束的時候,整理一下思路,然後重新排標題,寫分析。不知諸位是否有興趣?

 

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