使用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;
日志还没设计,不过暂时预留日志的位置。
由于实现的非常之乱,变量也不规整,因此目前不开放代码。