尝试做一个简单的文件系统

使用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;

日志还没设计,不过暂时预留日志的位置。

由于实现的非常之乱,变量也不规整,因此目前不开放代码。

 

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