十一、文件系统

inode、间接块索引表、文件控制块FCB

硬盘读写单位是扇区,因此一个块是由多个扇区组成的,块大小是扇区大小的整数倍。块是文件系统的读写单位,因此文件至少要占一个块,当文件体积大于一个块时,文件要拆分为多个块来存储。
文件组织形式是对各个文件而言的,UNIX操作系统中的索引结构-inode。采用索引结构的文件系统,文件中的块依然可以分散到不连续的零散空间中,保留了磁盘高利用率的优点,更重要的是文件系统为每个文件的所有块建立了一个索引表,索引表就是块地址数组,即每个数组元素就是块的地址,数组元素下标是文件块的索引,第n个数组元素指向文件中的第n个块,这样访问任意一个块时候,只要从数组中获得块地址即可。必须为每一个文件都单独配备一个这样的元信息数据结构,因此 UNIX文件系统中,一个文件必须对应一个 inode。
十一、文件系统_第1张图片
当文件很大时,采取的策略是:每个索引表共有 15个索引项,暂时称此索引表为老索引表。前12个索引项是文件的前12个块地址,他们是文件的直接块。若文件大于12个块,那就再建立个新的索引表,新索引表称为一级间接块索引表,表中容纳256个块地址。此表占用一个物理块来存储,该物理块的地址存储到老索引表的第 13 个索引项中。这样文件就有12+256=268块,当文件块大于256块时,建立二级间接块索引表,各表项存储的是一级间接块索引表的地址,这样就可以有 256* 256个块了,然后在老索引表中的第14个索引项存储二级间接块索引表所在块的地址。这样总共可以有 (12+256+256*256) 个块。不行的话,在创建三级间接块索引表,表中各表项存储的二级间接块索引表的地址,这样就可以 256*256*256 个块,三级间接块索引表的地址储存在老索引表的第15个索引项中。文件最大可以(12+256+256*256+256*256*256)个块。
十一、文件系统_第2张图片
inode结构中,几乎囊括了一个文件的所有信息,权限、属性、时间、大小等等。
只要用于管理、控制文件相关信息的数据结构都被称为 FCB,即文件控制块,inode 也是这种结构,因此inode 也是FCB 的一种。注意:inode 是文件在文件系统上的元信息,要通过文件系统获得文件的实体,必须先要找到文件的 inode。
Linux中,文件系统是针对各个分区来管理磁盘空间的。在每一个分区中,inode 的数量是固定的,inode 的数量等于文件的数量,分区中所有文件的 inode 通过一个大表格来维护,此表格称为 inode_table ,用数组来表示,数组元素的下标便是文件 inode 编号。
一个分区的大小是固定的,但是文件大小不一,若两个 16GB 的分区,一个分区中储存 1M大小 的文件一个,另一个分区中 储存文件大小 1G 的文件也是一个储存,它们的 inode 利用率是一样的,但是磁盘空间利用率不一样。所以一个分区的利用率分为 inode 利用率和磁盘空间利用率。

目录项与目录简介

Linux中,目录和文件都用 inode 来表示,因此目录也是文件,只是目录是包含了文件的文件。所以有了目录文件和普通文件,是文件就要有 inode。如果inode 表示的普通文件,此 inode 指向的数据块中的内容应该是普通文件自己的数据。 inode 表示的目录文件,则指向的数据块中的内容应该是要么普通文件的目录项,要么是目录文件的目录项。
不管文件是 普通文件 还是 目录文件,所有的都存在于某个目录中。所以目录项内容分为普通文件和目录文件。
所以目录相当与一个文件列表,每个文件在目录中都有一个entry,每个entry 的内容至少包括 文件名、文件类型、文件的inode 编号。想想 ls -lai 这个命令出来的内容。
文件名:inode中并没有文件名,所以文件名存在于目录项中
文件类型:表明这个文件的类型是目录文件还是普通文件
inode编号:通过inode 编号可以在inode_table 中找到 inode的地址,这样可以找到该文件的实体块。
创建文件的本质就是创建了目录项和 inode。
总结:文件系统中的目录和普通文件统一用 inode 表示。inode表示的文件是普通文件,还是目录文件,取决于 inode 所指向的数据块中实际内容是什么。inode 是文件的实质,但它并不能直接引用,必须通过文件名找到文件名所在的目录项,然后从目录项中获得 inode 的编号,然后用编号到inode_table 数组中找相关的 inode,最后找到文件的数据块。
十一、文件系统_第3张图片

超级块与文件系统布局

超级快就是文件系统元信息的配置文件,它是在分区创建文件系统时创建的,所有有关文件系统元信息的配置都在超级块中,包括:inode_table 的地址和大小、inode 位图的地址和大小、根目录的地址和大小,空闲块位图的地址和大小。每一个分区都有一个超级块,超级块的位置和大小是固定的,在各个分区的第2个扇区,一个扇区大小。
文件系统是针对各个分区来管理的,inode 代表了文件,因此各分区都有自己的inode 数组。各分区可创建的最大文件数是在为分区创建文件系统(格式化)时设置的。所以各分区的inode_table 的数组长度是等于最大文件数的,我们用位图来管理 inode 的使用情况。
十一、文件系统_第4张图片
在分区的开始是OBR块,里面主要是内核加载器。通过OBR可以进入保护模式、开启分页模式、加载内核到内存。第二个块是超级块,后面是inode 位图和inode_table 数组,后面是根目录和空闲块区域,根目录和空闲块区域才是储存数据的区域。 
十一、文件系统_第5张图片

创建文件系统

创建超级块、i结点、目录项

在创建文件系统前,一些基础数据结构要先创建:超级块、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));
//inode 结构(硬盘中,通过读入一个扇区到内存来修改inode_table的值然后同步)
truct inode {
   uint32_t i_no;    // inode编号
/* 当此inode是文件时,i_size是指文件大小,
若此inode是目录,i_size是指该目录下所有目录项大小之和*/
   uint32_t i_size;
   uint32_t i_open_cnts;   // 记录此文件被打开的次数
   bool write_deny;	   // 写文件不能并行,进程写文件前检查此标识
   uint32_t i_sectors[13];
   struct list_elem inode_tag;
};
//目录项(硬盘中)
struct dir_entry {
   char filename[MAX_FILE_NAME_LEN];  // 普通文件或目录名称
   uint32_t i_no;					 // 普通文件或目录对应的inode编号
   enum file_types f_type;			 // 文件类型
};

//目录(内存中)

struct dir 
{
   struct inode* inode;   //指针,用于指向内存中的inode
   uint32_t dir_pos;	  // 记录在目录内的偏移
   uint8_t dir_buf[512];  // 目录的数据缓存
};

创建分区元信息

高级格式化分区,也就是初始化分区的元信息,具体包括:超级块位置和大小、空闲块位图的位置和大小、inode 位图的位置和大小、inode 数组的位置和大小、空闲块起始地址、根目录起始地址。 partition_format(struct partition* part),具体实现是:1,首先要根据分区 part 的大小,计算出来分区文件系统各元信息需要的扇区数和位置。2,在内存中创建超级块,将以上步骤计算的元信息写入超级块,将超级块写入磁盘,将元信息写入磁盘上各自的位置,将根目录写入磁盘。
注意的是:分区挂载的目的是为了使用分区上的数据,需要对这些数据增删查改,所以需要对相对应的位图进行操作,位图的操作是在内存中进行。我们的空闲块位图的处理方法比较简单粗暴,在将启动扇区、inode位图、inode数组写入硬盘后剩余的扇区为空闲扇区,然后向上除整得到空闲块位图的扇区数,然后用总空闲扇区的数量减去位图数量得到了。这样做的结果就造成了最后一扇区的最后有些位是多余的,所以我们要提前将这些多余的位设置为1,然后写入硬盘。
static void partition_format(struct partition* part) {
/* 为方便实现,一个块大小是一扇区 */
   uint32_t boot_sector_sects = 1;	  
   uint32_t super_block_sects = 1;
   uint32_t inode_bitmap_sects = DIV_ROUND_UP(MAX_FILES_PER_PART, BITS_PER_SECTOR);	   // I结点位图占用的扇区数.最多支持4096个文件
   uint32_t inode_table_sects = DIV_ROUND_UP(((sizeof(struct inode) * MAX_FILES_PER_PART)), SECTOR_SIZE);
   uint32_t used_sects = boot_sector_sects + super_block_sects + inode_bitmap_sects + inode_table_sects;
   uint32_t free_sects = part->sec_cnt - used_sects;  

/************** 简单处理块位图占据的扇区数 ***************/
   uint32_t block_bitmap_sects;
   block_bitmap_sects = DIV_ROUND_UP(free_sects, BITS_PER_SECTOR);
   /* block_bitmap_bit_len是位图中位的长度,也是可用块的数量 */
   uint32_t block_bitmap_bit_len = free_sects - block_bitmap_sects; 
   block_bitmap_sects = DIV_ROUND_UP(block_bitmap_bit_len, BITS_PER_SECTOR); 
/*********************************************************/
   
   /* 超级块初始化 */
   struct super_block sb;
   sb.magic = 0x19590318;
   sb.sec_cnt = part->sec_cnt;
   sb.inode_cnt = MAX_FILES_PER_PART;
   sb.part_lba_base = part->start_lba;

   sb.block_bitmap_lba = sb.part_lba_base + 2;	 // 第0块是引导块,第1块是超级块
   sb.block_bitmap_sects = block_bitmap_sects;

   sb.inode_bitmap_lba = sb.block_bitmap_lba + sb.block_bitmap_sects;
   sb.inode_bitmap_sects = inode_bitmap_sects;

   sb.inode_table_lba = sb.inode_bitmap_lba + sb.inode_bitmap_sects;
   sb.inode_table_sects = inode_table_sects; 

   sb.data_start_lba = sb.inode_table_lba + sb.inode_table_sects;
   sb.root_inode_no = 0;
   sb.dir_entry_size = sizeof(struct dir_entry);

   printk("%s info:\n", part->name);
   printk("   magic:0x%x\n   part_lba_base:0x%x\n   all_sectors:0x%x\n   inode_cnt:0x%x\n   block_bitmap_lba:0x%x\n   block_bitmap_sectors:0x%x\n   inode_bitmap_lba:0x%x\n   inode_bitmap_sectors:0x%x\n   inode_table_lba:0x%x\n   inode_table_sectors:0x%x\n   data_start_lba:0x%x\n", sb.magic, sb.part_lba_base, sb.sec_cnt, sb.inode_cnt, sb.block_bitmap_lba, sb.block_bitmap_sects, sb.inode_bitmap_lba, sb.inode_bitmap_sects, sb.inode_table_lba, sb.inode_table_sects, sb.data_start_lba);

   struct disk* hd = part->my_disk;
/*******************************
 * 1 将超级块写入本分区的1扇区 *
 ******************************/
   ide_write(hd, part->start_lba + 1, &sb, 1);
   printk("   super_block_lba:0x%x\n", part->start_lba + 1);

/* 找出数据量最大的元信息,用其尺寸做存储缓冲区*/
   uint32_t buf_size = (sb.block_bitmap_sects >= sb.inode_bitmap_sects ? sb.block_bitmap_sects : sb.inode_bitmap_sects);
   buf_size = (buf_size >= sb.inode_table_sects ? buf_size : sb.inode_table_sects) * SECTOR_SIZE;
   uint8_t* buf = (uint8_t*)sys_malloc(buf_size);	// 申请的内存由内存管理系统清0后返回
   
/**************************************
 * 2 将块位图初始化并写入sb.block_bitmap_lba *
 *************************************/
   /* 初始化块位图block_bitmap */
   buf[0] |= 0x01;       // 第0个块预留给根目录,位图中先占位
   uint32_t block_bitmap_last_byte = block_bitmap_bit_len / 8;
   uint8_t  block_bitmap_last_bit  = block_bitmap_bit_len % 8;
   uint32_t last_size = SECTOR_SIZE - (block_bitmap_last_byte % SECTOR_SIZE);	     // last_size是位图所在最后一个扇区中,不足一扇区的其余部分

   /* 1 先将位图最后一字节到其所在的扇区的结束全置为1,即超出实际块数的部分直接置为已占用*/
   memset(&buf[block_bitmap_last_byte], 0xff, last_size);
   
   /* 2 再将上一步中覆盖的最后一字节内的有效位重新置0 */
   uint8_t bit_idx = 0;
   while (bit_idx <= block_bitmap_last_bit) {
      buf[block_bitmap_last_byte] &= ~(1 << bit_idx++);
   }
   ide_write(hd, sb.block_bitmap_lba, buf, sb.block_bitmap_sects);

/***************************************
 * 3 将inode位图初始化并写入sb.inode_bitmap_lba *
 ***************************************/
   /* 先清空缓冲区*/
   memset(buf, 0, buf_size);
   buf[0] |= 0x1;      // 第0个inode分给了根目录
   /* 由于inode_table中共4096个inode,位图inode_bitmap正好占用1扇区,
    * 即inode_bitmap_sects等于1, 所以位图中的位全都代表inode_table中的inode,
    * 无须再像block_bitmap那样单独处理最后一扇区的剩余部分,
    * inode_bitmap所在的扇区中没有多余的无效位 */
   ide_write(hd, sb.inode_bitmap_lba, buf, sb.inode_bitmap_sects);

/***************************************
 * 4 将inode数组初始化并写入sb.inode_table_lba *
 ***************************************/
 /* 准备写inode_table中的第0项,即根目录所在的inode */
   memset(buf, 0, buf_size);  // 先清空缓冲区buf
   struct inode* i = (struct inode*)buf; 
   i->i_size = sb.dir_entry_size * 2;	 // .和..
   i->i_no = 0;   // 根目录占inode数组中第0个inode
   i->i_sectors[0] = sb.data_start_lba;	     // 由于上面的memset,i_sectors数组的其它元素都初始化为0 
   ide_write(hd, sb.inode_table_lba, buf, sb.inode_table_sects);
   
/***************************************
 * 5 将根目录初始化并写入sb.data_start_lba
 ***************************************/
   /* 写入根目录的两个目录项.和.. */
   memset(buf, 0, buf_size);
   struct dir_entry* p_de = (struct dir_entry*)buf;

   /* 初始化当前目录"." */
   memcpy(p_de->filename, ".", 1);
   p_de->i_no = 0;
   p_de->f_type = FT_DIRECTORY;
   p_de++;

   /* 初始化当前目录父目录".." */
   memcpy(p_de->filename, "..", 2);
   p_de->i_no = 0;   // 根目录的父目录依然是根目录自己
   p_de->f_type = FT_DIRECTORY;

   /* sb.data_start_lba已经分配给了根目录,里面是根目录的目录项 */
   ide_write(hd, sb.data_start_lba, buf, 1);

   printk("   root_dir_lba:0x%x\n", sb.data_start_lba);
   printk("%s format done\n", part->name);
   sys_free(buf);
}
当 inode 指代的是普通文件时,成员大小是文件的具体字节大小。当inode 指代的是目录时,成员大小是是此目录下所有目录项的大小之和。
然后通过partition_format()格式化函数来格式化每个分区,具体实现是通过三层循环:分别是两个通道、每个通道的主从硬盘、然后每个硬盘的各个分区。读取每个分区的超级块,检测里面的魔数,如果符合,说明之前已经格式化过了,如果魔数不符合,则格式化。

挂载分区

windows系统的分区盘符都是直接摆在那里的,但是Linux却不一样,分区使用的时候要拿出来,不使用的时候可以收起来。

Linux中内核所在的分区是默认分区,自系统启动后就以该分区为默认分区,该分区的根目录是固定存在的,要想使用其他新分区的话,需要用mount 命令手动将新的分区挂载到默认分区的某个目录下。尽管其他分区有自己的根目录,但是默认分区的根目录才是所有分区的父目录。分区不用的时候就要 umount 命令卸载。

挂载分区的实质就是把该分区文件系统的元信息从硬盘上读出来加载到内存中,这样硬盘资源的变化都用内存中元信息来跟踪。如果对硬盘资源有了操作,就要计时的将内存中的元信息同步写入到硬盘以持久化。

文件描述符

文件描述符的对象是文件的操作,读写文件的本质是:先通过文件的inode 找到文件数据块的扇区地址,随后读写该扇区,从而实现了文件的读写。操作系统都支持一个进程同时、多次、打开同一个文件,任意时刻的操作都有偏移量,比如写操作,写一个字偏移量要变化一个。Linux提供了称为“文件结构”的数据结构,专门用于记录与文件操作相关的信息,每次打开一个文件就会产生一个文件结构,从而实现了“即使同一个文件被同时多次打开,各自操作的偏移量也互不影响”的灵活性。Linux把所有的“文件结构”组织到一起形成数组统一管理,该数组被称为文件表。
十一、文件系统_第6张图片
//文件结构,打开一次文件就会产生一个文件结构
struct file {
   uint32_t fd_pos;      // 记录当前文件操作的偏移地址,以0为起始,最大为文件大小-1
   uint32_t fd_flag;
   struct inode* fd_inode;
};

inode 用于描述文件储存的相关信息,文件结构用于描述文件打开后,文件的读写偏移量等信息。文件与inode 一一对应,一个文件仅有一个inode,一个文件可以被打开多次,每次都会产生一个文件结构,因此一个inode 可以有多个文件结构,多个文件结构可以对应同一个inode。文件描述符只是个整数,文件描述符数组存在于进程的PCB中,并且文件描述符数组的元素是文件表的下标,就是说 fb_table[3]=7,fb_table[4]=18...,即文件描述符数组元素中的内容是文件表的下标,指向文件表中的某个文件结构。但是文件描述符却是文件描述符数组的下标。文件描述符为3的文件对应的文件表项是要到 fb_table[3]=7 找。注意 fb_table[0]=0,fb_table[1]=1,fb_table[2]=2.
当某进程把文件描述符作为参数提交给文件系统时,文件系统用此文件描述符在该进程的PCB中的文件描述符数组中索引对应的元素,从该元素中获取对应的文件结构的下标,用该下标在文件表中索引相应的文件结构,从该文件结构中获取文件的 inode,最终找到文件的数据块。
十一、文件系统_第7张图片
当用户进程打开文件时,文件系统给用户进程返回的该进程PCB中文件描述符组的元素值,也是文件表的下标值,即文件描述符。Linux中的文件操作通常是先用open 打开文件以获取该文件的文件描述符,然后将文件描述符作为参数传给 read 或 write 等函数对该文件进行读写操作。所以open 操作的本质就是创建相应文件描述符的过程,创建文件描述符的过程就是逐层在这三个数据结构中找空位,在该空位填充好数据后返回该位置的地址。
1,在全局的inode队列中新建一 inode,然后返回该inode 地址
2,在全局的文件表 中找一空位,在该位置填充文件结构,使 fd_inode 指向上一步中返回的 inode 地址,然后返回本地址结构在文件表中的下标值
3,在PCB中的文件描述符数组中找一空位,使该位置的值指向上一步中返回的文件结构的下标,并返回本文件描述符在文件描述符数组中的下标。
文件描述符数组中的前3个都是标准的文件描述符:0表示标准输入、1表示表述输出、2表示标准错误。

文件操作相关的函数

我们要实现文件及目录的创建、打开、读写操作,要首先写好相关的函数:inode 操作相关函数、文件相关函数、目录相关函数、路径解析相关函数

inode 操作相关函数

文件和目录本质上都是inode,因此任何有关文件及目录的操作都离不开 inode 的处理。我们需要修改 inode_table 中的内容来操作文件,inode 的操作主要有:
//inode节点号 在 inode_table 中的定位 函数
//功能:在分区part 的inode_table 中找到想要的inode 号 :inode_no 的位置:扇区号、扇区偏移字节、跨扇区,然后存入 inode_pos 中。
//实现:在inode_table 中,inode 号是从小到大因此相连的,通过inode 号 inode_no 与struct inode 的大小相乘,然后与一个扇区大小对比,就可以得到这个inode 在inode_table中的具体位置。
static void inode_locate(struct partition* part, uint32_t inode_no, struct inode_position* inode_pos)

//inode_table 同步 函数
//功能:将内存中 io_buf 的,将要同步的 inode 指针写入分区 part 中的 inode_table 中。
//实现:要想往硬盘中的 inode_table 中更新写入内存中的 inode,必须根据inode中的 inode号 知道这个 inode 在硬盘中的具体位置才能准确写入。首先要通过上面的函数定位这个inode。io_buf 是硬盘输入输出的缓冲区,从硬盘中读取内容之类的都会存放在 io_buf 中。因此我们需要根据 inode 的定位信息:扇区数和扇区偏移字节。扇区数决定了从 inode_table 中读取的扇区数,扇区偏移字节决定了这个扇区从低往上数具体的字节。我们根据定位信息从 inode_table 中读取一个或者两个扇区到 io_buf 中,然后通过偏移量来找到具体的字节,更新内存中的 io_buf,最后在写入到硬盘中。 
void inode_sync(struct partition* part, struct inode* inode, void* io_buf);

//inode 的内存打开函数(struct part 中inode 队列)
//功能:根据 inode_no 号在内存中找到 inode队列中相应的指针或者添加相应的 inode 链表
//实现:我们在 struct part 中就创建了inode 队列:part->open_inodes,表示已打开的 inode 队列,即已开打的文件的队列,它是已经打开的 inode 的缓存。以后想打开 inode(想打开一个文件),先在此缓存中找该 inode,若找到了就表示该文件之前就打开过,然后将元素的 i_open_cnts 加一,然后直接返回指针。若在队列中找不到该结点,就要从内核的arena(内核物理池)中申请。因为 inode 队列中的所有 inode 应该被所有任务共享,包括内核线程和进程,所以我们要把整个 inode 队列创建在内核空间中,inode 队列中的所有结点必须在内核的堆空间中创建,而不能是简单的局部变量。由于用户进程中 PCB->pgdir 的值不为NULL,按照 sys_malloc 的规则就会在 用户的进程自己的堆中分配内存,返回的虚拟地址就是用户进程的arena中的虚拟地址,每个用户进程都有自己的arena,即使虚拟地址的数值是一样的,另一个用户进程也不能访问到上个用户进程的分配的从arena返回的小内存的虚拟地址,因为每一个用户的arena都是单独的页框,并且都有自己单独的4G虚拟空间(页表也是单独分配的页框),小内存虽然虚拟地址一样,但是也是位于两个不同的arena页框中,只是相同虚拟地址被各自的页表映射到了不同的页框而已。所以我们要把 inode 结点的arena中,因为每个用户进程的页表的高四分之一就是内核的页表了。所以我们在申请 inode 前,要先定位,根据定位信息将该 inode 的一个扇区从硬盘中读出来到内存中。然后开始申请内存:先将 pgdir 置为 null,然后就调用 sys_malloc 在内核中申请,然后恢复 pgdir 的值。然后根据定位信息中的偏移字节信息将那一个扇区的偏移到的inode 大小复制到 arena申请的。
struct inode* inode_open(struct partition* part, uint32_t inode_no);

//inode 的内存关闭函数(struct part 中的 inode 队列)
//功能:将inode 中的 uint32_t i_open_cnts 减一,若减为 0,则在inode 队列中将其回收了
//实现: inode 队列是在内存中打开的队列,inode 表示文件,那么inode队列就表示打开的文件队列。函数执行后 inode 的 i_open_cnts 减1,若其值为 0,则说明 这个inode 代表的文件将要被 关闭了,就在 inode 队列中收回 inode。释放 内核物理池中的 struct inode 时,注意将 pgdir 置 NULL。
void inode_close(struct inode* inode)

文件操作相关函数

我们对文件进行创建时候首先要在文件表中获取一个空闲位(一个文件可以打开无数次,所以文件表中可以是同一个文件产生的)并且还要在文件描述符中申请一个空闲位,然后在 inode_table 中申请一个空闲的 inode,因此要在 inode 的位图 中申请一个空闲位。还要在 空闲块的位图 中申请空闲位。同步到硬盘后才能创建一个文件。
//文件表申请函数
//功能:在文件表中获取一个空闲位
//实现:文件表中的空闲位是该文件结构的 fd_inode 成员值为 NULL。因为如果一个文件被打开了,那么肯定会产生一个文件结构,里面就会给 fd_inode 赋值。注意的是要在文件表的范围内申请,并且0、1、2是留给了标准输入、输出和错误。
int32_t get_free_slot_in_global(void)

//文件描述符写入函数
//功能:将 globa_fd_idx 安装到线程或进程自己PCB中的文件描述符数组中
//实现:首先要找到一个空的文件描述符,0、1、2已经被用了。然后判断是否为-1,为 -1 则说明是可用的。
int32_t pcb_fd_install(int32_t globa_fd_idx)

//功能:根据内存中的 inode 的位图情况 申请 inode_table,返回 inode 结点号
int32_t inode_bitmap_alloc(struct partition* part)

//功能:根据内存中的空闲块位图的情况 申请 空闲块的地址,返回 扇区 的地址
int32_t block_bitmap_alloc(struct partition* part)

//功能:将内存中的 inode 的位图 和内存中的空闲块的位图 同步到硬盘中 (在内存的 struct part 结构中也有一份分区的元信息)
//实现:我们只看 位图 就行,因为数据块中的具体内容我们可以不管,如果 对应的位图是1,则说明就是已经被申请过了,内容是什么不重要。同步时候只需要将该 索引位 的512字节的这一个扇区同步到硬盘即可。
void bitmap_sync(struct partition* part, uint32_t bit_idx, uint8_t btmp_type)


目录操作相关函数

对于操作文件来说,目录是很重要的,增删查改都与目录有关。
//功能:在分区 part 上打开根目录,返回目录指针
void open_root_dir(struct partition* part) {
   root_dir.inode = inode_open(part, part->sb->root_inode_no);
   root_dir.dir_pos = 0;
}

//功能:在分区part上打开 i 结点为 inode_no 的目录并返回目录指针
//实现:在堆中创建一个struct dir,然后打开 inode
struct dir* dir_open(struct partition* part, uint32_t inode_no) {
   struct dir* pdir = (struct dir*)sys_malloc(sizeof(struct dir));
   pdir->inode = inode_open(part, inode_no);
   pdir->dir_pos = 0;
   return pdir;
}

//功能:在part分区内的 pdir目录内寻找名为 name 的文件或目录,找到后返回 true 并将其目录项存入 dir_e ,否则返回false
//实现:我们通过 inode_table 来索引到目录的 inode,然后确定了该 目录文件 占据了几个 数据块,这些数据块中存着的都是普通文件和目录文件的目录项。我们的目的是求出想要的目录项。所以我们需要将这几个数据块的目录项都读到内存中,然后通过对比名字是否相同,然后将找到的目录项存入变量中。具体步骤:首先我们确定该目录文件占用几个数据块:我们都会将打开的文件的 inode_table 项读入内存在物理池中申请的堆中,并将 inode 加入 inode 队列中。通过读取这个 inode_table 项,我们将这个项中的 12 个 或者 128个 数据块的地址写入内存 all_blocks[] 数组中,然后通过查看数组的每一项地址是否为 0 来确定数据块是否存在,然后将存在的数据块,根据地址一个一个的读入到内存中,比较它们的名字然后得出想要的目录项。
bool search_dir_entry(struct partition* part, struct dir* pdir, \
		     const char* name, struct dir_entry* dir_e) {
   uint32_t block_cnt = 140;	 // 12个直接块+128个一级间接块=140块
   /* 12个直接块大小+128个间接块,共560字节 */
   uint32_t* all_blocks = (uint32_t*)sys_malloc(48 + 512);
   if (all_blocks == NULL) {
      printk("search_dir_entry: sys_malloc for all_blocks failed");
      return false;
   }
   uint32_t block_idx = 0;
   while (block_idx < 12) {
      all_blocks[block_idx] = pdir->inode->i_sectors[block_idx];
      block_idx++;
   }
   block_idx = 0;
   if (pdir->inode->i_sectors[12] != 0) {	// 若含有一级间接块表
      ide_read(part->my_disk, pdir->inode->i_sectors[12], all_blocks + 12, 1);
   }
/* 至此,all_blocks存储的是该文件或目录的所有扇区地址 */
   /* 写目录项的时候已保证目录项不跨扇区,
    * 这样读目录项时容易处理, 只申请容纳1个扇区的内存 */
   uint8_t* buf = (uint8_t*)sys_malloc(SECTOR_SIZE);
   struct dir_entry* p_de = (struct dir_entry*)buf;	    // p_de为指向目录项的指针,值为buf起始地址
   uint32_t dir_entry_size = part->sb->dir_entry_size;
   uint32_t dir_entry_cnt = SECTOR_SIZE / dir_entry_size;   // 1扇区内可容纳的目录项个数
   /* 开始在所有块中查找目录项 */
   while (block_idx < block_cnt) {		  
   /* 块地址为0时表示该块中无数据,继续在其它块中找 */
      if (all_blocks[block_idx] == 0) {
	 block_idx++;
	 continue;
      }
      ide_read(part->my_disk, all_blocks[block_idx], buf, 1);
      uint32_t dir_entry_idx = 0;
      /* 遍历扇区中所有目录项 */
      while (dir_entry_idx < dir_entry_cnt) {
	 /* 若找到了,就直接复制整个目录项 */
	 if (!strcmp(p_de->filename, name)) {
	    memcpy(dir_e, p_de, dir_entry_size);
	    sys_free(buf);
	    sys_free(all_blocks);
	    return true;
	 }
	 dir_entry_idx++;
	 p_de++;
      }
      block_idx++;
      p_de = (struct dir_entry*)buf;  // 此时p_de已经指向扇区内最后一个完整目录项了,需要恢复p_de指向为buf
      memset(buf, 0, SECTOR_SIZE);	  // 将buf清0,下次再用
   }
   sys_free(buf);
   sys_free(all_blocks);
   return false;
}

//功能:关闭目录
//实现:关闭目录的本质是关闭目录的 inode 并释放目录占用的内存。注意的是根目录不能被关闭,根目录是所有目录的父目录,查找文件时必须从根目录开始。另外根目录root_dir 占用的是静态内存,它位于低端 1MB 之内,并非在堆中申请。
void dir_close(struct dir* dir) {
   if (dir == &root_dir) {
      return;
   }
   inode_close(dir->inode);
   sys_free(dir);
}

//功能:初始化目录项
void create_dir_entry(char* filename, uint32_t inode_no, uint8_t file_type, struct dir_entry* p_de) {
   memcpy(p_de->filename, filename, strlen(filename));
   p_de->i_no = inode_no;
   p_de->f_type = file_type;
}

//功能:将目录项p_de写入父目录parent_dir中,io_buf由主调函数提供。all_blocks[block_idx] 中储存的是 140 数据块的地址,没有一级索引表的地址。元信息要及时同步
//实现:当将目录项 p_de 写入内存中的目录 parent_dir 中时,我们的意思是要让p_de加入parent_dir 所指向的目录文件中,所以要注意的是内存中的内容和硬盘中的内容同步。
首先我们要在内存的堆中找到该目录文件的 inode_table 项,然后将 inode 的12个扇区地址 读入到 all_blocks[140] 中,然后从 all_blocks[0] -- all_blocks[139] 开始循环检查,如果在 all_blocks[12] 之前首先遇到为0的项,则说明这个项之前有扇区地址指向了一个数据块,但是这个数据块里面的目录项后来被删除了导致这项的扇区地址也清零了。所以我们优先将 目录项 p_de 优先安装在这个空项中:所以就需要根据 内存空闲块位图 申请一个数据块(同步硬盘空闲块位图),将扇区地址填入 内存inode_table项(同步更新iode) 和 all_block[ ]项  中,将 目录项 写入io_buf 中,写入硬盘这个扇区地址的空闲数据块中。如果在 all_block[12] 前首先遇到了不为 0  的项,那就说明此项的扇区地址中有了目录项,因此我们往后面在加一个目录项即可,我们把这个扇区读到 io_buf 中,然后这个数据块的目录项满了没,判断标准是目录项的文件类型是否为 FT_UNKNOW,如果没满就往里面加入该目录项,然后把 io_buf 重新写进 该硬盘。如果 all_blocks[12] 为0,那么就说明前面的12项都不为0,但是地址扇区的目录项都满了,需要用到一级间接块索引表。因此我们要通过 内存空闲块位图  申请一个空闲扇区(同步硬盘空闲块位图)作为一级间接块索引表,在 内存inode_table 项的 [12] 处填入这个扇区地址。然后在通过 内存空闲块位图 申请一个空闲扇区(同步到硬盘)作为第一个 一级间接块索引表的空闲块,然后在 all_blocks[12] 中填入这个地址,最后将 io_buf 中填入这个目录项,写入到这个硬盘地址中。如果在 all_blocks[12] 后遇到为 0 的项,说明已经存在了一级间接块索引表,我们通过 内存空闲块位图 申请一个空闲数据块,将扇区地址填入all_blocks[ ]项中,然后io_buf 写入硬盘。
bool sync_dir_entry(struct dir* parent_dir, struct dir_entry* p_de, void* io_buf) {
   struct inode* dir_inode = parent_dir->inode;
   uint32_t dir_size = dir_inode->i_size;
   uint32_t dir_entry_size = cur_part->sb->dir_entry_size;
   ASSERT(dir_size % dir_entry_size == 0);	 // dir_size应该是dir_entry_size的整数倍
   uint32_t dir_entrys_per_sec = (512 / dir_entry_size);       // 每扇区最大的目录项数目
   int32_t block_lba = -1;
   /* 将该目录的所有扇区地址(12个直接块+ 128个间接块)存入all_blocks */
   uint8_t block_idx = 0;
   uint32_t all_blocks[140] = {0};	  // all_blocks保存目录所有的块
   /* 将12个直接块存入all_blocks */
   while (block_idx < 12) {
      all_blocks[block_idx] = dir_inode->i_sectors[block_idx];
      block_idx++;
   }
   struct dir_entry* dir_e = (struct dir_entry*)io_buf;	       // dir_e用来在io_buf中遍历目录项
   int32_t block_bitmap_idx = -1;
   /* 开始遍历所有块以寻找目录项空位,若已有扇区中没有空闲位,
    * 在不超过文件大小的情况下申请新扇区来存储新目录项 */
   block_idx = 0;
   while (block_idx < 140) {  // 文件(包括目录)最大支持12个直接块+128个间接块=140个块
      block_bitmap_idx = -1;
      if (all_blocks[block_idx] == 0) {   // 在三种情况下分配块
	 block_lba = block_bitmap_alloc(cur_part);
	 if (block_lba == -1) {
	    printk("alloc block bitmap for sync_dir_entry failed\n");
	    return false;
	 }
      /* 每分配一个块就同步一次block_bitmap */
	 block_bitmap_idx = block_lba - cur_part->sb->data_start_lba;
	 ASSERT(block_bitmap_idx != -1);
	 bitmap_sync(cur_part, block_bitmap_idx, BLOCK_BITMAP);
	 block_bitmap_idx = -1;
	 if (block_idx < 12) {	    // 若是直接块
	    dir_inode->i_sectors[block_idx] = all_blocks[block_idx] = block_lba;
	 } else if (block_idx == 12) {	  // 若是尚未分配一级间接块表(block_idx等于12表示第0个间接块地址为0)
	    dir_inode->i_sectors[12] = block_lba;       // 将上面分配的块做为一级间接块表地址
	    block_lba = -1;
	    block_lba = block_bitmap_alloc(cur_part);	       // 再分配一个块做为第0个间接块
	    if (block_lba == -1) {
	       block_bitmap_idx = dir_inode->i_sectors[12] - cur_part->sb->data_start_lba;
	       bitmap_set(&cur_part->block_bitmap, block_bitmap_idx, 0);
	       dir_inode->i_sectors[12] = 0;
	       printk("alloc block bitmap for sync_dir_entry failed\n");
	       return false;
	    }
	 /* 每分配一个块就同步一次block_bitmap */
	    block_bitmap_idx = block_lba - cur_part->sb->data_start_lba;
	    ASSERT(block_bitmap_idx != -1);
	    bitmap_sync(cur_part, block_bitmap_idx, BLOCK_BITMAP);
	    all_blocks[12] = block_lba;
	    /* 把新分配的第0个间接块地址写入一级间接块表 */
	    ide_write(cur_part->my_disk, dir_inode->i_sectors[12], all_blocks + 12, 1);
	 } else {	   // 若是间接块未分配
	    all_blocks[block_idx] = block_lba;
	    /* 把新分配的第(block_idx-12)个间接块地址写入一级间接块表 */
	    ide_write(cur_part->my_disk, dir_inode->i_sectors[12], all_blocks + 12, 1);
	 }
	 /* 再将新目录项p_de写入新分配的间接块 */
	 memset(io_buf, 0, 512);
	 memcpy(io_buf, p_de, dir_entry_size);
	 ide_write(cur_part->my_disk, all_blocks[block_idx], io_buf, 1);
	 dir_inode->i_size += dir_entry_size;
	 return true;
      }
   /* 若第block_idx块已存在,将其读进内存,然后在该块中查找空目录项 */
      ide_read(cur_part->my_disk, all_blocks[block_idx], io_buf, 1); 
      /* 在扇区内查找空目录项 */
      uint8_t dir_entry_idx = 0;
      while (dir_entry_idx < dir_entrys_per_sec) {
	 if ((dir_e + dir_entry_idx)->f_type == FT_UNKNOWN) {	// FT_UNKNOWN为0,无论是初始化或是删除文件后,都会将f_type置为FT_UNKNOWN.
	    memcpy(dir_e + dir_entry_idx, p_de, dir_entry_size);    
	    ide_write(cur_part->my_disk, all_blocks[block_idx], io_buf, 1);
	    dir_inode->i_size += dir_entry_size;
	    return true;
	 }
	 dir_entry_idx++;
      }
      block_idx++;
   }   
   printk("directory is full!\n");
   return false;
}

路径解析操作相关函数

windows 中的路径分隔符是 \ ,Linux 的是 / 。路径解析就是把路径按照路径分隔符拆分成多层文件名,逐层在磁盘上查找以确认文件名是否存在。Linux中一切皆是文件,因此同一目录下不能出现同名的普通文件和目录。用户给出一个路径,Linux的文件系统必须要逐层到目录项中去确认。如:/a/b/c, 最左边的 / 是根目录,首先在根目录下查找 a,根据目录项的文件类型判断 a 是否是目录,是的话继续在 a 目录中查找 b,在判处是否为目录,是的话在 b 的目录中查找 c,c 就可以是普通文件也可以是目录文件了。

//功能: 将最上层路径名称解析出来储存到 name_store 

//实现:比如我们要解析:/a/b/c、///a/b/c,调用函数后 name_store的值为 'a',返回 “/b/c ”。 

static char* path_parse(char* pathname, char* name_store) {
   if (pathname[0] == '/') {   // 根目录不需要单独解析
    /* 路径中出现1个或多个连续的字符'/',将这些'/'跳过,如"///a/b" */
       while(*(++pathname) == '/');
   }
   /* 开始一般的路径解析 */
   while (*pathname != '/' && *pathname != 0) {
      *name_store++ = *pathname++;
   }
   if (pathname[0] == 0) {   // 若路径字符串为空则返回NULL
      return NULL;
   }
   return pathname; 
}

//功能:返回路径深度
//实现:比如 /a/b/c ,返回 3。不能简单的用 / 的数目来统计目录深度,因为 Linux 中 //a///b////c是合法的。
int32_t path_depth_cnt(char* pathname) {
   ASSERT(pathname != NULL);
   char* p = pathname;
   char name[MAX_FILE_NAME_LEN];       // 用于path_parse的参数做路径解析
   uint32_t depth = 0;
   /* 解析路径,从中拆分出各级名称 */ 
   p = path_parse(p, name);
   while (name[0]) {
      depth++;
      memset(name, 0, MAX_FILE_NAME_LEN);
      if (p) {	     // 如果p不等于NULL,继续分析路径
	p  = path_parse(p, name);
      }
   }
   return depth;
}


文件检索操作相关函数

在打开文件之前,文件系统要确认文件是否在磁盘上存在。在创建文件之前,文件系统要确认文件所在的目录中是否已有同名文件存在。

//用来记录查找文件过程中已找到的上级路径,也就是查找文件过程中"走过的地方"

//用这个结构的目的是想获得路径中“断链”的部分,如查找 a/b/c ,若找不到的话,我们想知道是c不存在,还是b或者a不存在。成员 searched_path[] 就是查找过程中不存在的路径,如果是 c 不存在,seached_path 的值为 “a/b/c”,若 b 不存在,值为 “ /a/b ”,并且找不到的话是未知类型 FT_UNKOEN。如果找得到的话,search_path[] 的值是直接父目录,file_type是 目录或者文件。

struct path_search_record {
   char  searched_path[MAX_PATH_LEN];	    // 查找过程中的父路径
   struct dir* parent_dir;		    // 文件或目录所在的直接父目录
   enum file_types file_type;		    // 找到的是普通文件还是目录,找不到将为未知类型(FT_UNKNOWN)
};

//功能:搜索文件pathname,若找到则返回其inode号,否则返回-1

//实现:如 /a/b/c,则一层一层解析。现在 根目录/ 中 0inode结点 中对应的数据块中找到 目录项a,就可以找到 a 的 inode结点了。然后通过找到的 a 的 inode 结点 找到 a的空闲数据块,然后找到 b目录项, 就可以找到 b目录文件的inode结点,然后就可以找到对应的 inode结点,从而找到对应的 数据块,就可以找到 c目录项。

创建文件

实现创建普通文件

//功能:在目录parent_dir 中以模式 flag 去创建普通文件 filename,若成功则返回文件描述符,即 pcb->fb_table 中的下标,否则返回-1

//实现:创建文件需要的操作:1,每个文件需要一个 inode 来描述大小、属性等等,所以创建文件要在 inode_table 中申请一个空闲位,所以我们我们需要现在 内存inode位图 中 ,因此要同步硬盘的位图。2,申请好 inode 后需要 空闲数据块 来储存数据,所以在 内存空闲数据位图 申请一个空闲位,并要同步硬盘。3,新文件必定要属于某一个目录,所以该目录的 inode->i_size 要增加个目录项的大小,并且新加的目录项还要写入该目录的 inode->i_sectors[] 中的某个扇区中,原有扇区可能已满,所以要申请新扇区来储存目录项。4,还要注意 inode_table、block_bitmap、新文件的inode 及文件所在目录的inode 这些同步问题。

因此我们创建文件的顺序:创建新文件i结点-》文件描述符fd-》目录项。

int32_t file_create(struct dir* parent_dir, char* filename, uint8_t flag) {
   /* 后续操作的公共缓冲区 */
   void* io_buf = sys_malloc(1024);
   if (io_buf == NULL) {
      printk("in file_creat: sys_malloc for io_buf failed\n");
      return -1;
   }
   uint8_t rollback_step = 0;	       // 用于操作失败时回滚各资源状态
   /* 为新文件分配inode */
   int32_t inode_no = inode_bitmap_alloc(cur_part); 
   if (inode_no == -1) {
      printk("in file_creat: allocate inode failed\n");
      return -1;
   }
/* 此inode要从堆中申请内存,不可生成局部变量(函数退出时会释放)
 * 因为file_table数组中的文件描述符的inode指针要指向它.*/
   struct inode* new_file_inode = (struct inode*)sys_malloc(sizeof(struct inode)); 
   if (new_file_inode == NULL) {
      printk("file_create: sys_malloc for inode failded\n");
      rollback_step = 1;
      goto rollback;
   }
   inode_init(inode_no, new_file_inode);	    // 初始化i结点
   /* 返回的是file_table数组的下标 */
   int fd_idx = get_free_slot_in_global();
   if (fd_idx == -1) {
      printk("exceed max open files\n");
      rollback_step = 2;
      goto rollback;
   }
   file_table[fd_idx].fd_inode = new_file_inode;
   file_table[fd_idx].fd_pos = 0;
   file_table[fd_idx].fd_flag = flag;
   file_table[fd_idx].fd_inode->write_deny = false;

   struct dir_entry new_dir_entry;
   memset(&new_dir_entry, 0, sizeof(struct dir_entry));

   create_dir_entry(filename, inode_no, FT_REGULAR, &new_dir_entry);	// create_dir_entry只是内存操作不出意外,不会返回失败

/* 同步内存数据到硬盘 */
   /* a 在目录parent_dir下安装目录项new_dir_entry, 写入硬盘后返回true,否则false */
   if (!sync_dir_entry(parent_dir, &new_dir_entry, io_buf)) {
      printk("sync dir_entry to disk failed\n");
      rollback_step = 3;
      goto rollback;
   }

   memset(io_buf, 0, 1024);
   /* b 将父目录i结点的内容同步到硬盘 */
   inode_sync(cur_part, parent_dir->inode, io_buf);

   memset(io_buf, 0, 1024);
   /* c 将新创建文件的i结点内容同步到硬盘 */
   inode_sync(cur_part, new_file_inode, io_buf);

   /* d 将inode_bitmap位图同步到硬盘 */
   bitmap_sync(cur_part, inode_no, INODE_BITMAP);

   /* e 将创建的文件i结点添加到open_inodes链表 */
   list_push(&cur_part->open_inodes, &new_file_inode->inode_tag);
   new_file_inode->i_open_cnts = 1;

   sys_free(io_buf);
   return pcb_fd_install(fd_idx);

/*创建文件需要创建相关的多个资源,若某步失败则会执行到下面的回滚步骤 */
rollback:
   switch (rollback_step) {
      case 3:
	 /* 失败时,将file_table中的相应位清空 */
	 memset(&file_table[fd_idx], 0, sizeof(struct file)); 
      case 2:
	 sys_free(new_file_inode);
      case 1:
	 /* 如果新文件的i结点创建失败,之前位图中分配的inode_no也要恢复 */
	 bitmap_set(&cur_part->inode_bitmap, inode_no, 0);
	 break;
   }
   sys_free(io_buf);
   return -1;
}

实现sys_open的创建功能

sys_open() 是 open()函数的内核级实现(系统调用)。create()函数可以创建文件,open()的功能很多,也可以创建文件。

//功能:接受两个参数:pathname 是待打开的文件,为绝对路径。flags 是打开标识。目录以字符 “/” 结尾,因此程序开头判断pathname 是否为目录,是对 pathname 的最后一个字判断,这个函数只支持文件打开,不支持目录打开。

//实现:

1,我们这个函数仅仅支持普通文件的打开和创建,所以要首先判断这个文件是不是目录文件:仅仅用最后的 / 来判断。如果是目录文件的话直接返回。

2,然后判断这个文件存不存在,如果存在先判断是不是目录文件,因为目录文件和普通文件不能同名,可能我们的路径最后没有 / ,但是是个目录。如果是目录就报错。如果不是目录就说明这个普通文件存在,接着判断 flags 的值是否有创建,有创建则报错退出,没有创建则返回文件标识符。

3,如果这个文件不存在,先要判断在路径哪一层断开的,如果是父层断开的,开始判断flags的值有没有创建,有创建就创建后返回标识符,没有创建报错返回。如果是中间层就断开了就报错返回。

int32_t sys_open(const char* pathname, uint8_t flags) {
  /* 对目录要用dir_open,这里只有open普通文件 */
   if (pathname[strlen(pathname) - 1] == '/') {
      printk("can`t open a directory %s\n",pathname);
      return -1;
   }
   ASSERT(flags <= 7);
   int32_t fd = -1;	   // 默认为找不到

   struct path_search_record searched_record;
   memset(&searched_record, 0, sizeof(struct path_search_record));

   /* 记录目录深度.帮助判断中间某个目录不存在的情况 */
   uint32_t pathname_depth = path_depth_cnt((char*)pathname);

   /* 先检查文件是否存在 */
   int inode_no = search_file(pathname, &searched_record);
   bool found = inode_no != -1 ? true : false; 

   if (searched_record.file_type == FT_DIRECTORY) {
      printk("can`t open a direcotry with open(), use opendir() to instead\n");
      dir_close(searched_record.parent_dir);
      return -1;
   }

   uint32_t path_searched_depth = path_depth_cnt(searched_record.searched_path);

   /* 先判断是否把pathname的各层目录都访问到了,即是否在某个中间目录就失败了 */
   if (pathname_depth != path_searched_depth) {   // 说明并没有访问到全部的路径,某个中间目录是不存在的
      printk("cannot access %s: Not a directory, subpath %s is`t exist\n", \
	    pathname, searched_record.searched_path);
      dir_close(searched_record.parent_dir);
      return -1;
   }

   /* 若是在最后一个路径上没找到,并且并不是要创建文件,直接返回-1 */
   if (!found && !(flags & O_CREAT)) {
      printk("in path %s, file %s is`t exist\n", \
	    searched_record.searched_path, \
	    (strrchr(searched_record.searched_path, '/') + 1));
      dir_close(searched_record.parent_dir);
      return -1;
   } else if (found && flags & O_CREAT) {  // 若要创建的文件已存在
      printk("%s has already exist!\n", pathname);
      dir_close(searched_record.parent_dir);
      return -1;
   }

   switch (flags & O_CREAT) {
      case O_CREAT:
	 printk("creating file\n");
	 fd = file_create(searched_record.parent_dir, (strrchr(pathname, '/') + 1), flags);
	 dir_close(searched_record.parent_dir);                                                                                                       break;			 														default;
      // 其余为打开文件																 fd = file_open(inode_no, flags);
   }

   /* 此fd是指任务pcb->fd_table数组中的元素下标,
    * 并不是指全局file_table中的下标 */
   return fd;
}

文件的打开与关闭

有关文件读写的函数都要用文件描述符作为参数,所以文件的读写操作通常是在文件被打开返回描述符之后。

文件的打开

//功能:打开编号为inode_no的inode对应的文件,若成功则返回文件描述符,否则返回-1

//实现:一个文件无论打开多少次每次都要对应一个文件结构,所以首先我们要获得一个文件结构的下标,这样就可以得到文件描述符了。在文件结构中储存着这个文件的基本信息:inode、权限、偏移地址。如果我们是以读和创建的方式打开文件的,返回文件描述符。如果是以写的方式打开文件的,就要检查inode中的write_deny位了,因为为了避免多个任务同时写文件而引起相互覆盖的混乱,所以我们要只保证同一时间只有一个任务在写文件,所以如果write_deny是0,表示没有任务写,我们把write_deny置1即可。如果write_deny的值是1,表示其他任务提前以写的方式执行了这个函数已经将write_deny置1了,所以报错退出。

所以顺序是:文件结构-》inode-》write_deny位-》fd

int32_t file_open(uint32_t inode_no, uint8_t flag) {
   int fd_idx = get_free_slot_in_global();
   if (fd_idx == -1) {
      printk("exceed max open files\n");
      return -1;
   }
   file_table[fd_idx].fd_inode = inode_open(cur_part, inode_no);
   file_table[fd_idx].fd_pos = 0;	     // 每次打开文件,要将fd_pos还原为0,即让文件内的指针指向开头
   file_table[fd_idx].fd_flag = flag;
   bool* write_deny = &file_table[fd_idx].fd_inode->write_deny; 

   if (flag & O_WRONLY || flag & O_RDWR) {	// 只要是关于写文件,判断是否有其它进程正写此文件
						// 若是读文件,不考虑write_deny
   /* 以下进入临界区前先关中断 */
      enum intr_status old_status = intr_disable();
      if (!(*write_deny)) {    // 若当前没有其它进程写该文件,将其占用.
	 *write_deny = true;   // 置为true,避免多个进程同时写此文件
	 intr_set_status(old_status);	  // 恢复中断
      } else {		// 直接失败返回
	 intr_set_status(old_status);
	 printk("file can`t be write now, try again later\n");
	 return -1;
      }
   }  // 若是读文件或创建文件,不用理会write_deny,保持默认
   return pcb_fd_install(fd_idx);
}

文件的关闭

//实现:首先将 内存中的inode的write_deny 的置0,表示这个写操作已经完成了,其他任务可以用了。然后将文件结构清零,使这个项可以被使用。然后 inode_close(file->fd_inode) :如果这个 内存堆中inode 打开数减一不为零,表示有其他认为在打开这个文件,则这个堆继续保留使用。若为零,表示没有堆使用了,则回收这个堆。最后回收文件描述符,使其可用。所以顺序是:write_deny-》文件结构-》inode-》fd

int32_t file_close(struct file* file) {
   if (file == NULL) {
      return -1;
   }
   file->fd_inode->write_deny = false;
   inode_close(file->fd_inode);
   file->fd_inode = NULL;   // 使文件结构可用
   return 0;
}


int32_t sys_close(int32_t fd) {
   int32_t ret = -1;   // 返回值默认为-1,即失败
   if (fd > 2) {
      uint32_t _fd = fd_local2global(fd);
      ret = file_close(&file_table[_fd]);
      running_thread()->fd_table[fd] = -1; // 使该文件描述符位可用
   }
   return ret;
}

文件的写入

sys_write 是系统调用 write 的内核实现。

//功能:把buf中的count个字节写入file,成功则返回写入的字节数,失败则返回-1

//实现:普通文件的数据是训练连续写入块中的,目录文件中目录项可以是离散的。如果是第一次写入数据要根据 内存空闲块位图 为其分配一个块地址,复制到 内存inode 中,然后同步。接着根据 内存inode 开始计算这个文件过去占了几个数据块了,根据 将要写入的字节数 判断将来会是几个数据块,然后算出还需要分配几个数据块出来。如果不需要分配数据块,就要判断文件已经占用的数据块数量是否大于12了。如果小于12,则直接可以算出 内存inode->i_sectors[,,],存入 all_blocks[block_idx]。如果大于12,则可以通过fd_inode->i_sectors[13],将 一级间接块的地址读出到all_blocks[]中。如果还需要分配 数据块,则要判断将来的总共使用的数据块是否大于12了。如果将来总共的数据块小于12,那么目前已经使用的数据块的最后一个块一般都不满,则要将块地址读入all_blocks[]中,然后在申请几个块满足需要,并将块地址写入all_blocks[]中。如果旧数据在12个块之内,但是将来总共的数据块在12个块之上,就需要申请间接块了。先将第11个块地址存入all_blocks[ ] 中,然后由 内存空闲块位图 申请一个块地址 作为间接块,地址存入 内存inode 中,然后申请将来需要的数据块,块地址存入 inode->i_sectors[ ] 和 all_blocks[ ] 中和 间接块中。注意元信息和 inode 、间接块的同步。如果旧数据就已经大于12了,需要 将间接块地址读入 all_blocks[ ] 中,然后申请空闲数据块写入到 all_blocks[ ]。此时可以 写数据块地址 已经收集到 all_blocks 中,下面开始写数据。all_blocks[ ]中包含可以继续使用的,含有剩余空间的块地址,以及新数据要占用的新的块地址。因此第一次写数据时候要把该块中的数据读出来,将新数据写入该块的空闲区域,之后新老数据一起写入硬盘,这样就实现了追加的功能。

int32_t file_write(struct file* file, const void* buf, uint32_t count) {
   if ((file->fd_inode->i_size + count) > (BLOCK_SIZE * 140))	{   // 文件目前最大只支持512*140=71680字节
      printk("exceed max file_size 71680 bytes, write file failed\n");
      return -1;
   }
   uint8_t* io_buf = sys_malloc(BLOCK_SIZE);
   if (io_buf == NULL) {
      printk("file_write: sys_malloc for io_buf failed\n");
      return -1;
   }
   uint32_t* all_blocks = (uint32_t*)sys_malloc(BLOCK_SIZE + 48);	  // 用来记录文件所有的块地址
   if (all_blocks == NULL) {
      printk("file_write: sys_malloc for all_blocks failed\n");
      return -1;
   }

   const uint8_t* src = buf;	    // 用src指向buf中待写入的数据 
   uint32_t bytes_written = 0;	    // 用来记录已写入数据大小
   uint32_t size_left = count;	    // 用来记录未写入数据大小
   int32_t block_lba = -1;			// 块地址
   uint32_t block_bitmap_idx = 0;   // 用来记录block对应于block_bitmap中的索引,做为参数传给bitmap_sync
   uint32_t sec_idx;	      // 用来索引扇区
   uint32_t sec_lba;	      // 扇区地址
   uint32_t sec_off_bytes;    // 扇区内字节偏移量
   uint32_t sec_left_bytes;   // 扇区内剩余字节量
   uint32_t chunk_size;	      // 每次写入硬盘的数据块大小
   int32_t indirect_block_table;      // 用来获取一级间接表地址
   uint32_t block_idx;				// 块索引

   /* 判断文件是否是第一次写,如果是,先为其分配一个块 */
   if (file->fd_inode->i_sectors[0] == 0) {
      block_lba = block_bitmap_alloc(cur_part);
      if (block_lba == -1) {
	 printk("file_write: block_bitmap_alloc failed\n");
	 return -1;
      }
      file->fd_inode->i_sectors[0] = block_lba;

      /* 每分配一个块就将位图同步到硬盘 */
      block_bitmap_idx = block_lba - cur_part->sb->data_start_lba;
      ASSERT(block_bitmap_idx != 0);
      bitmap_sync(cur_part, block_bitmap_idx, BLOCK_BITMAP);
   }

   /* 写入count个字节前,该文件已经占用的块数 */
   uint32_t file_has_used_blocks = file->fd_inode->i_size / BLOCK_SIZE + 1;

   /* 存储count字节后该文件将占用的块数 */
   uint32_t file_will_use_blocks = (file->fd_inode->i_size + count) / BLOCK_SIZE + 1;
   ASSERT(file_will_use_blocks <= 140);

   /* 通过此增量判断是否需要分配扇区,如增量为0,表示原扇区够用 */
   uint32_t add_blocks = file_will_use_blocks - file_has_used_blocks;

/* 开始将文件所有块地址收集到all_blocks,(系统中块大小等于扇区大小)
 * 后面都统一在all_blocks中获取写入扇区地址 */
   if (add_blocks == 0) { 
   /* 在同一扇区内写入数据,不涉及到分配新扇区 */
      if (file_has_used_blocks <= 12 ) {	// 文件数据量将在12块之内
	 block_idx = file_has_used_blocks - 1;  // 指向最后一个已有数据的扇区
	 all_blocks[block_idx] = file->fd_inode->i_sectors[block_idx];
      } else { 
      /* 未写入新数据之前已经占用了间接块,需要将间接块地址读进来 */
	 ASSERT(file->fd_inode->i_sectors[12] != 0);
         indirect_block_table = file->fd_inode->i_sectors[12];
	 ide_read(cur_part->my_disk, indirect_block_table, all_blocks + 12, 1);
      }
   } else {
   /* 若有增量,便涉及到分配新扇区及是否分配一级间接块表,下面要分三种情况处理 */
   /* 第一种情况:12个直接块够用*/
      if (file_will_use_blocks <= 12 ) {
      /* 先将有剩余空间的可继续用的扇区地址写入all_blocks */
	 block_idx = file_has_used_blocks - 1;
	 ASSERT(file->fd_inode->i_sectors[block_idx] != 0);
	 all_blocks[block_idx] = file->fd_inode->i_sectors[block_idx];

      /* 再将未来要用的扇区分配好后写入all_blocks */
	 block_idx = file_has_used_blocks;      // 指向第一个要分配的新扇区
	 while (block_idx < file_will_use_blocks) {
	    block_lba = block_bitmap_alloc(cur_part);
	    if (block_lba == -1) {
	       printk("file_write: block_bitmap_alloc for situation 1 failed\n");
	       return -1;
	    }

      /* 写文件时,不应该存在块未使用但已经分配扇区的情况,当文件删除时,就会把块地址清0 */
	    ASSERT(file->fd_inode->i_sectors[block_idx] == 0);     // 确保尚未分配扇区地址
	    file->fd_inode->i_sectors[block_idx] = all_blocks[block_idx] = block_lba;

	    /* 每分配一个块就将位图同步到硬盘 */
	    block_bitmap_idx = block_lba - cur_part->sb->data_start_lba;
	    bitmap_sync(cur_part, block_bitmap_idx, BLOCK_BITMAP);

	    block_idx++;   // 下一个分配的新扇区
	 }
      } else if (file_has_used_blocks <= 12 && file_will_use_blocks > 12) { 
	 /* 第二种情况: 旧数据在12个直接块内,新数据将使用间接块*/

      /* 先将有剩余空间的可继续用的扇区地址收集到all_blocks */
	 block_idx = file_has_used_blocks - 1;      // 指向旧数据所在的最后一个扇区
	 all_blocks[block_idx] = file->fd_inode->i_sectors[block_idx];

	 /* 创建一级间接块表 */
	 block_lba = block_bitmap_alloc(cur_part);
	 if (block_lba == -1) {
	    printk("file_write: block_bitmap_alloc for situation 2 failed\n");
	    return -1;
	 }

	 ASSERT(file->fd_inode->i_sectors[12] == 0);  // 确保一级间接块表未分配
	 /* 分配一级间接块索引表 */
	 indirect_block_table = file->fd_inode->i_sectors[12] = block_lba;

	 block_idx = file_has_used_blocks;	// 第一个未使用的块,即本文件最后一个已经使用的直接块的下一块
	 while (block_idx < file_will_use_blocks) {
	    block_lba = block_bitmap_alloc(cur_part);
	    if (block_lba == -1) {
	       printk("file_write: block_bitmap_alloc for situation 2 failed\n");
	       return -1;
	    }

	    if (block_idx < 12) {      // 新创建的0~11块直接存入all_blocks数组
	       ASSERT(file->fd_inode->i_sectors[block_idx] == 0);      // 确保尚未分配扇区地址
	       file->fd_inode->i_sectors[block_idx] = all_blocks[block_idx] = block_lba;
	    } else {     // 间接块只写入到all_block数组中,待全部分配完成后一次性同步到硬盘
	       all_blocks[block_idx] = block_lba;
	    }

	    /* 每分配一个块就将位图同步到硬盘 */
	    block_bitmap_idx = block_lba - cur_part->sb->data_start_lba;
	    bitmap_sync(cur_part, block_bitmap_idx, BLOCK_BITMAP);

	    block_idx++;   // 下一个新扇区
	 }
	 ide_write(cur_part->my_disk, indirect_block_table, all_blocks + 12, 1);      // 同步一级间接块表到硬盘
      } else if (file_has_used_blocks > 12) {
	 /* 第三种情况:新数据占据间接块*/
	 ASSERT(file->fd_inode->i_sectors[12] != 0); // 已经具备了一级间接块表
	 indirect_block_table = file->fd_inode->i_sectors[12];	 // 获取一级间接表地址

	 /* 已使用的间接块也将被读入all_blocks,无须单独收录 */
	 ide_read(cur_part->my_disk, indirect_block_table, all_blocks + 12, 1); // 获取所有间接块地址

	 block_idx = file_has_used_blocks;	  // 第一个未使用的间接块,即已经使用的间接块的下一块
	 while (block_idx < file_will_use_blocks) {
	    block_lba = block_bitmap_alloc(cur_part);
	    if (block_lba == -1) {
	       printk("file_write: block_bitmap_alloc for situation 3 failed\n");
	       return -1;
	    }
	    all_blocks[block_idx++] = block_lba;

	    /* 每分配一个块就将位图同步到硬盘 */
	    block_bitmap_idx = block_lba - cur_part->sb->data_start_lba;
	    bitmap_sync(cur_part, block_bitmap_idx, BLOCK_BITMAP);
	 }
	 ide_write(cur_part->my_disk, indirect_block_table, all_blocks + 12, 1);   // 同步一级间接块表到硬盘
      } 
   }

   bool first_write_block = true;      // 含有剩余空间的扇区标识
   /* 块地址已经收集到all_blocks中,下面开始写数据 */
   file->fd_pos = file->fd_inode->i_size - 1;   // 置fd_pos为文件大小-1,下面在写数据时随时更新
   while (bytes_written < count) {      // 直到写完所有数据
      memset(io_buf, 0, BLOCK_SIZE);
      sec_idx = file->fd_inode->i_size / BLOCK_SIZE;
      sec_lba = all_blocks[sec_idx];
      sec_off_bytes = file->fd_inode->i_size % BLOCK_SIZE;
      sec_left_bytes = BLOCK_SIZE - sec_off_bytes;

      /* 判断此次写入硬盘的数据大小 */
      chunk_size = size_left < sec_left_bytes ? size_left : sec_left_bytes;
      if (first_write_block) {
	 ide_read(cur_part->my_disk, sec_lba, io_buf, 1);
	 first_write_block = false;
      }
      memcpy(io_buf + sec_off_bytes, src, chunk_size);
      ide_write(cur_part->my_disk, sec_lba, io_buf, 1);
      printk("file write at lba 0x%x\n", sec_lba);    //调试,完成后去掉

      src += chunk_size;   // 将指针推移到下个新数据
      file->fd_inode->i_size += chunk_size;  // 更新文件大小
      file->fd_pos += chunk_size;   
      bytes_written += chunk_size;
      size_left -= chunk_size;
   }
   inode_sync(cur_part, file->fd_inode, io_buf);
   sys_free(all_blocks);
   sys_free(io_buf);
   return bytes_written;
}

 

//功能:将buf中连续count个字节写入文件描述符fd,成功则返回写入的字节数,失败返回-1

int32_t sys_write(int32_t fd, const void* buf, uint32_t count) {
   if (fd < 0) {
      printk("sys_write: fd error\n");
      return -1;
   }
   if (fd == stdout_no) {  
      char tmp_buf[1024] = {0};
      memcpy(tmp_buf, buf, count);
      console_put_str(tmp_buf);
      return count;
   }
   uint32_t _fd = fd_local2global(fd);
   struct file* wr_file = &file_table[_fd];
   if (wr_file->fd_flag & O_WRONLY || wr_file->fd_flag & O_RDWR) {
      uint32_t bytes_written  = file_write(wr_file, buf, count);
      return bytes_written;
   } else {
      console_put_str("sys_write: not allowed to write file without flag O_RDWR or O_WRONLY\n");
      return -1;
   }
}

读取文件



删除目录

我们删除空目录,在删除目录时先判断目录是否为空,不允许删除非空目录

//功能:判断目录是否为空

//实现:若目录下只有.和..这两个目录项则目录为空

bool dir_is_empty(struct dir* dir) {
   struct inode* dir_inode = dir->inode;
   /* 若目录下只有.和..这两个目录项则目录为空 */
   return (dir_inode->i_size == cur_part->sb->dir_entry_size * 2);
}


//功能:在父目录parent_dir中删除child_dir

//实现:我们要删除的child_dir 必须是个空目录,空目录只在inode->i_sectors[0]中有扇区,其它扇区都应该为空

int32_t dir_remove(struct dir* parent_dir, struct dir* child_dir) {
   struct inode* child_dir_inode  = child_dir->inode;
   /* 空目录只在inode->i_sectors[0]中有扇区,其它扇区都应该为空 */
   int32_t block_idx = 1;
   while (block_idx < 13) {
      ASSERT(child_dir_inode->i_sectors[block_idx] == 0);
      block_idx++;
   }
   void* io_buf = sys_malloc(SECTOR_SIZE * 2);
   if (io_buf == NULL) {
      printk("dir_remove: malloc for io_buf failed\n");
      return -1;
   }

   /* 在父目录parent_dir中删除子目录child_dir对应的目录项 */
   delete_dir_entry(cur_part, parent_dir, child_dir_inode->i_no, io_buf);

   /* 回收inode中i_secotrs中所占用的扇区,并同步inode_bitmap和block_bitmap */
   inode_release(cur_part, child_dir_inode->i_no);
   sys_free(io_buf);
   return 0;
}


实现sys_rmdir

//功能: 删除空目录,成功时返回0,失败时返回-1

//实现:先要确定目录是否存在,然后确定这个是目录还是普通文件,最后确定目录是否为空,最后删除

int32_t sys_rmdir(const char* pathname) {
   /* 先检查待删除的文件是否存在 */
   struct path_search_record searched_record;
   memset(&searched_record, 0, sizeof(struct path_search_record));
   int inode_no = search_file(pathname, &searched_record);
   ASSERT(inode_no != 0);
   int retval = -1;	// 默认返回值
   if (inode_no == -1) {
      printk("In %s, sub path %s not exist\n", pathname, searched_record.searched_path); 
   } else {
      if (searched_record.file_type == FT_REGULAR) {
	 printk("%s is regular file!\n", pathname);
      } else { 
	 struct dir* dir = dir_open(cur_part, inode_no);
	 if (!dir_is_empty(dir)) {	 // 非空目录不可删除
	    printk("dir %s is not empty, it is not allowed to delete a nonempty directory!\n", pathname);
	 } else {
	    if (!dir_remove(searched_record.parent_dir, dir)) {
	       retval = 0;
	    }
	 }
	 dir_close(dir);
      }
   }
   dir_close(searched_record.parent_dir);
   return retval;
}















你可能感兴趣的:(操作系统)