1. 磁盘分区
文件系统是建立在已经给磁盘分好区的基础之上的。分过区后磁盘的分布情况如下图,具体内容不展开。
使用的分区工具是fdisk,之前已经完成的内核是在一个裸盘上,所以这里新加了一个硬盘用来创建文件系统。
硬盘2有一个MBR扇区剩余都是拓展分区,拓展分区下有5个子拓展分区。
2. inode
UNIX文件系统是以索引结构组织的,好处是可以直接访问要访问的块不需要从头遍历。文件系统为每个文件建立一个索引表,索引表就是块地址数据,每个数组元素就是块的地址,数组元素的下标是文件块的索引。
包含此索引表的结构称为inode,即index inode,索引节点,用来索引、跟踪一个文件的所有块。inode是文件索引结构的具体体现,必须为每个文件都单独配备一个这样的数据结构,所以在UNIX文件系统中,一个文件对应一个inode,有多少文件就有多少个inode数据结构。
索引结构存储文件A的逻辑示意图如下所示。
这一使用的块大小为4KB一个块。在数据处理的时候比较简单,硬盘的一个块对应的一个内存页。
使用索引结构的缺点是索引表本身要占据一定的空间,如果文件很大的话就会占很多的块,那么inode的大小就需要很大。UNIX为了解决这一问题,将每个索引表中的15个索引项分为两部分,前12个索引项为文件的直接块,存储的是数据块的直接lba地址。当文件所占的数据块超过12个时,就再创建一个索引表称为一级间接块索引表。如果一个块的大小为4KB那么一个块大小的索引表可以存储1024个数据块(数据块地址为32位),那么此时文件的大小可以达到12+1024个块。若文件大小更大,则需要二级间接索引表,二级间接索引表的内容为一级间接索引表,这样文件的大小可以为12+1024+10241024。若更大则需要三级间接索引表,内容为二级间接索引表,大小可以达到12+10241024+1024 * 1024 * 1024。
如下图所示。
inode数据结构:
/*inode结构*/
struct inode
{
uint32_t i_no; //inode编号
uint32_t i_size; //当此inode是文件时,i_size是指文件大小,若是目录,i_size是指目录下所有目录项大小之和
uint32_t i_open_cnts; //记录此文件被打开的次数
bool write_deny; //写文件不能并行,进程写文件前检查此标志
uint32_t i_sectors[13]; //0~11是直接块,12永远存储一级间接块指针
struct list_elem inode_tag; //此inode的标识,用于加入已打开的inode列表
};
这里的系统中没有权限管理和时间,所以要比常规的inode内容要少。该inode中要记录inode的编号,用来结合inode位图索引inode。这里使用12个直接块和1个一级间接索引表。所以该系统中文件的大小最大为(12+1024)个块(4KB)。
3. 目录项与目录
用户在使用文件系统的使用,用的更多的是文件名而不是inode编号。如何将文件名和inode编号联系起来就是目录项的作用。同时一个inode不一定就是记录信息的不同文件,也有可能是一个目录,目录项中也要表示该目录项对应的inode中记录的内容是文件信息还是普通数据信息。
对应关系如下图所示。
图中inode1为一个目录的indoe,数据块中记录由n个目录项。其中目录项1为普通文件,指向indoe2。目录项n为目录类型,指向inode3,inode3中又记录着n个目录项。
目录项数据结构:
/*目录结构*/
struct dir
{
struct inode* inode; //该目录对应的inode,用于指向内存中的inode
uint32_t dir_pos; //记录在目录内的偏移
uint8_t dir_buf[512]; //目录的数据缓存
};
/*目录项结构*/
struct dir_entry
{
char filename[MAX_FILE_NAME_LEN]; //不同文件或目录名称
uint32_t i_no; //不同文件或目录对应的inode编号
enum file_types f_type; //文件类型
};
目录通常是需要在系统中有一个缓存的,这里的struct dir就是该缓存所需要的结构。目录项中记录有文件名、inode编号和文件类型,这样就将文件名和inode编号联系到一起了。
这里的目录项结构已经是很简化的了,仅仅满足了文件名和inode相关联。
4. 超级块与文件系统布局
inode需要有inode数据来记录,现在问题是inode数组的大小和地址又该如何记录。根目录是固定的,根目录又该如何存储。超级块中就需要记录像这样的元信息。这些元信息是事先规定好的,相当于该文件系统的配置信息。
超级块结构:
/*超级块*/
struct super_block
{
uint32_t magic; //用来表示文件系统类型,支持多文件系统的操作系统通过此标志来识别文件系统类型
uint32_t sec_cnt; //本分区总共的扇区数,数据块数
uint32_t inode_cnt; //本分区中inode数量
uint32_t part_lba_base; //本分区的起始lba地址
uint32_t block_bitmap_lba; //块位图本身起始扇区地址
uint32_t block_bitmap_sects; //块位图本身占用的扇区数量
uint32_t inode_bitmap_lba; //i节点位图起始扇区lba地址
uint32_t inode_bitmap_sects; //i节点位图占用的扇区数量
uint32_t inode_table_lba; //i节点表起始扇区lba地址
uint32_t inode_table_sects; //i节点表占用的扇区数量
uint32_t data_start_lba; //数据区开始的第一个扇区号
uint32_t root_inode_no; //根目录所在的i节点号
uint32_t dir_entry_size; //目录项大小
uint8_t pad[460]; //加上460字节,凑够512字节1扇区大小
}__attribute__ ((packed));
以上的内容就是该文件系统的超级块。结构体最后的__attribute__((packed))是在编译过程中内存大小严格按照程序所写的大小,因为这里要保证超级块的大小为512字节。
有了超级块,硬盘中的简单的文件系统就构建完成了,最后呈现出来的布局如下图所示。
参考书籍:《操作系统真相还原》-- 郑刚