使用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;
日誌還沒設計,不過暫時預留日誌的位置。
由於實現的非常之亂,變量也不規整,因此目前不開放代碼。