嘗試做一個簡單的文件系統

使用B+樹作爲文件系統的主要數據結構,用來儲存文件描述符,文件描述符用來儲存文件的具體信息(在磁盤上的位置,大小,時間等)。

文件描述符參考了FAT32中用來描述文件信息的結構,但有較大的區別。每個文件描述符32佔用字節,分爲兩種:用於描述符文件信息的描述符和用於儲存文件名的描述符,兩種結構以及相關結構如下:

//文件屬性
typedef struct {
    byte dpl : 2;            //文件權限;分區權限 0,1,2,3
    byte extname : 1;        //下一項描述符類型,=0,文件描述符;=1,擴展文件名描述符
    byte isext : 1;            //本描述符是否爲擴展描述符,=1
    byte data : 1;            //=1文件正常,=0此文件可能存在問題;作爲分區描述符時,=1說明此分區是系統分區,
    byte en_folder : 1;        //說明floder項有效,以此文件名作爲後面文件的目錄基準,同目錄下的文件應該只有一個文件的此項爲1
    byte hide : 1;            //=1隱藏文件;=1隱藏分區
    byte del : 1;            //=1已刪除文件,相當於放入回收站,標記刪除,爲了保證可以恢復;=1分區刪除
}fileAtt;

//文件日期結構
typedef struct {
    uint32 s : 6;        //秒
    uint32 m : 6;        //分
    uint32 h : 5;        //時
    uint32 day : 5;        //日
    uint32 month : 4;    //月
    uint32 year : 6;    //年,使用是將此值加上基準年份等於時間
}fDate, * _fdate;

//用於描述符文件信息的文件描述符
typedef struct {
    byte ms : 7;            //創建時間的10毫秒位,ms*10=大約的創建時間,現在感覺這項沒什麼用
    byte dis : 1;            //=0擴展文件名描述符,=1文件描述符,此項恆等於1
    fileAtt fatt;            //文件屬性
    char name[FTNAME_SIZE];            //文件名,佔用6字節
    fDate createDate;        //創建日期
    fDate lastVisitDate;    //最後訪問日期
    fDate lastModifiedDate;    //最後修改日期
    uint32 size;                //文件長度,單位4kb,因此一個文件最大爲4096GB=4TB
    uint32 position;            //在分區內的偏移位置,單位4kb,最大檢索16tb,因此一個分區的最大爲16tb
    uint16 offset : 12;        //文件佔用的最後一個4kb內的偏移
    uint16 extnum : 4;            //擴展描述符數量,最大爲15個,當此值=15時,應當查看最後一個擴展描述符,確定是否還有擴展描述符
    uint16 folder;                //文件夾包含數量,如果fatt.en_folder=1,說明此文件描述符組爲文件夾描述符,則folder包含了該文件夾的文件數量
}fileTable, * _filetable;

//用於儲存文件名的文件名描述符
typedef struct extName {
	byte size : 5;			//本項文本串長度
	byte ext : 1;			//1=有擴展項,此項爲了避免文件描述符指出的15項的限制,以期能夠儲存更多文件項
	byte start : 1;			//=1起始項,爲擴展文件名描述符組的首項
	byte dis : 1;			//=0擴展文件名描述符,=1文件描述符,此項應該恆等於0
	char name[31];			//文件名
}extName, * _extname;

//文件項聯合體,一個文件項可能是文件描述符或者擴展文件名描述項
typedef union {
	fileTable ft;			//文件描述符
	extName en;			//擴展文件名描述符
}fileItems, * _fileitems;

在內部節點中,所有描述符均爲文件名描述符,因爲B+樹的內部節點只是用來幫助查找文件,並不儲存文件信息。

在葉結點中,文件名描述符跟在文件描述符後面(如果文件名太長的話),由於一個文件描述符儲存的信息有限,因此做了以下規定:

1.如果時間太大,則使用多個描述符疊加的方法,比如文件描述符的創建時間createDate項,基準年份假定爲2020,創建年份爲2100,因此createDate.year項儲存的值應當爲80,但是日期結構最大隻能儲存64,因此需要將80二進制形式的低6位放進第一個描述符,然後第二個描述符儲存剩下的高位。如果還存不進可以以此類推,幾乎可以儲存無限長的時間。

2.描述符的size、position、offset項,這兩個項一般不存在值太大的情況,因此不可疊加,但由於文件可能是是分散儲存的,這種情況下需要使用多個描述符來儲存這三項。一組連續的用來描述同一個文件的文件描述符成爲文件描述符組,最大的情況由1個首文件描述符、15個擴展文件描述符、15個擴展文件名描述符構成,因此一個文件最多允許使用31個描述符來表示自己,最多允許將一個文件拆成16個位置來儲存,不夠的話就需要試着整理磁盤碎片了(當然,我也設想過可以把一個文件拆成多個子文件來儲存,但這樣可能會降低效率,還是使用強制性的規定比較好)

3.folder項用來儲存文件夾內包含的一級文件數量(文件夾內的第一層文件),只有在fatt項的en_folder屬性=1時纔有效,此時文件描述符組儲存的時文件夾,無用的項要置空。

4.每一個文件描述符組對應一個文件或文件夾,文件名是絕對路徑,例:"test/a.txt",表示在根目錄的test文件夾下的a.txt文件。這樣做是爲了方便計算機索引磁盤文件。

5.由於是絕對路徑,所以文件夾描述符組後面若干個描述符組就是該文件夾內部的文件,文件夾操作便需要逐個讀取其後面的文件描述符組,因此當一個文件夾內部包含的文件過多(特別是文件夾過多),會使展示文件夾列表變得非常慢,我暫時沒想都解決辦法。

6.文件描述符組在節點內部以文件名順序進行排序。

 

由於B+樹的結構,使節點具有兩種類型,葉節點和內部節點。

葉節點大小爲80kb,可以儲存2409個文件描述符,除了是B+樹的一部分,所有葉節點也可以組成一個雙向循環鏈表,第一個節點儲存了文件名最小的文件描述符,被稱爲首葉節點,按照B+樹的刪除插入方式,正常情況下第一個葉節點不會改變。當然,以後重寫代碼的時候可能會修改。

內部節點大小爲52kb,可以儲存1401個文件名描述符,用來索引葉節點。

結構如下:

//B+樹內部節點,52kb
#define BNODE_NUM 1401
#define BNODE_SIZE 52*1024
#define CHILD_TYPE_BNODE 0	//childType屬性,內部節點類型
#define CHILD_TYPE_LNODE 3	//childType屬性,葉節點類型
typedef struct BTreeNode {
	extName name[BNODE_NUM];			//文件名描述符數組
	uint32 child[BNODE_NUM + 1];		//子節點指針數組,可能爲內部節點或葉節點,使用時,要強制轉換爲結點指針後使用
	uint16 name_off[BNODE_NUM - 1];		//記錄每個文件名的起始下標,從第二個文件名開始
	uint32 parent;			//父節點指針
	uint16 name_off_num:14;				//記錄name_off數組的有效長度
	uint16 childType:2;					//子節點類型,0=內部節點,3=葉節點
	uint16 namenum;						//記錄name數組的長度
}BTreeNode, * _btreenode, * _bn;

//B+樹葉節點
#define LNODE_NUM 2409
//葉節點,80kb
#define LNODE_SIZE 80*1024
typedef struct LeafNode {
	fileItems fi[LNODE_NUM];			//文件描述符數組
	uint16 file_off[LNODE_NUM - 1];		//文件偏移數組,描述文件的起始描述符位下標,由於第一個文件項的下標肯定爲0,因此從第二個文件描述符開始。
	uint16 finum;						//fi數組有效項的長度
	uint16 file_off_num;				//file_off數組有效項的數量
	uint32 prev;				//前驅節點
	uint32 next;				//後繼節點
	uint32 parent;			//父節點指針
}LeafNode, * _leafnode, * _ln;

兩種節點大小不一樣就是爲了4kb對齊,52kb和80kb爲最小公倍數,如果太大,在寫入磁盤時會寫的太多,也會影響查找效率。同時,由於硬盤的同一個扇區如果被寫入次數太多,會縮短扇區壽命,特別是固態硬盤,是有寫入次數限制的,因此,節點在被讀取後,只有發生修改,纔可以寫入硬盤。我也想過是否應該在某個節點寫入達到一定次數之後,重寫到其他地方,目前沒有實現。

另一點,由於所有文件名不可能是一樣長的,所以B+樹中的數組並非等長的,因此設計了偏移數組,來索引每個文件描述符組的第一個描述符。但是爲了減少佔用儲存空間,偏移數組不索引描述符數組第一個元素,因爲肯定是從0開始的,不過現在想真的不怎麼值得,讓程序變得更加複雜了。

數據管理,如何確定那個扇區寫入了數據,哪些扇區是空的?我決定參考linux下的ext文件系統的實現機制,使用位圖來表示,0代表空,1代表被佔用或者不可用(損壞的),將讀寫單位劃分爲塊,每個塊由若干個扇區組成,必須是2冪次方,一個位代表一個塊。將每個位圖放在塊組的最後面,一個跨組的大小爲一個塊的字節數*8塊,即如果一個塊的大小爲512字節,則塊組的大小爲512*8=4096個塊。

對於引導扇區,也是參考fat32的設計,不過增加了一些,結構如下:

//引導扇區結構
//512BYTES
#define BOOTCodeSize (512-64-2-82)
typedef struct BOOTLoder {
	/*0*/	uint8 jmpBOOT[3];			//跳轉代碼
	/*3*/	char OEM[8];				//此文件系統開發者名字
	/*11*/	uint16 BytePerSec;			//每扇區字節數
	/*13*/	uint8 Unit;					//文件系統的單元,爲BytePerSec的倍數,例:若UNIT=8,BytePerSec=512,則文件系統的單位爲4kb
	/*14*/	uint16 ResvdSecCnt;			//BOOT記錄佔用的單元數
	/*16*/	uint8 Resvered0;
	/*17*/	uint16 RootEntCnt;			//根目錄文件最大數
	/*19*/	uint16 TotUnit16;			//單元總數
	/*21*/	uint8 Media;				//介質描述符
	/*22*/	uint16 BlockSize;			//每個塊佔用的單元數,最大爲32MB
	/*24*/	uint16 SecPerTrk;			//每個磁道的扇區數
	/*26*/	uint16 NumHeads;			//磁頭數
	/*28*/	uint32 HiddSec;				//隱藏單元數
	/*32*/	uint32 TotUnit32;			//如果TotUnit16=0,則由這裏給出單元總數
	/*36*/	uint8 DrvNum;				//驅動器號
	/*37*/	uint8 Resvered1;			//保留字節,置空
	/*38*/	uint8 BootSig;				//擴展引導標記,0x29
	/*39*/	uint32 VolID;				//卷序列號
	/*43*/	char VolLab[11];			//卷標
	/*54*/	char FileSysType[8];		//文件系統屬性
	/*62*/	uint16 VerNum[2];			//版本號,VerNum[0]爲主版本號,VerNum[1]爲子版本號
	/*66*/	uint16 BNodeSize;			//內部節點長度(byte)
	/*68*/	uint16 LNodeSize;			//葉節點長度(byte)
	/*70*/	uint32 LogBlockAddr;		//日誌塊地址,單位:塊
	/*74*/	uint32 LBNum;				//一個日誌塊佔用塊數
	/*78*/	uint32 InfoBlockAddr;		//信息塊地址
	/*82*/	uint8 boot[BOOTCodeSize];	//boot代碼
	/*446*/	DPT dpt[4];					//4個分區表
	/*510*/	uint16 BOOTSign;			//引導扇區標誌,0xAA55
}BOOTLoder, * _bootloder;

日誌還沒設計,不過暫時預留日誌的位置。

由於實現的非常之亂,變量也不規整,因此目前不開放代碼。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章