二、10.文件系统

news2024/12/23 9:43:55

硬盘是低速设备,其读写单位是扇区,为了避免频繁访问硬盘,操作系统不会有了一扇区数据就去读写一次磁盘,往往等数据积攒到“足够大小”时才一次性访问硬盘,这足够大小的数据就是块,硬盘读写单位是扇区,因此一个块是由多个扇区组成的,块大小是扇区大小的整数倍。

块是文件系统的读写单位,因此文件至少要占据一个块,当文件体积大于 1 个块时,文件肯定被拆分成多个块来存储,那么问题来了,这多个块该如何组织到一起?现在要讨论文件的组织方式了。拿 FAT 文件系统来说, FAT 称为文件分配表,在此文件系统中存储的文件,其所有的块被用于链式结构来组织,在每个块的最后存储下一个块的地址,从而块与块之间串联到一起,这样一来,文件可以不连续存储,文件中的块可以分布在各个零散的空间中,有效地利用了存储空间,或者说是提升了磁盘的利用率,相当于节省了空间。

但是按照FAT访问结点时必须从头开始遍历块结点,而且每访问一个结点,就要涉及一次硬盘寻道,使得对原本低速的设备访问更加频繁了。也许连微软自己也受不了它了,后来推出了 NTFS 文件系统 。


下面讨论下另 一种文件组织形式, UINX 操作系统中的索引结构一一inode

UNIX 文件系统比较先进,它将文件以索引结构来组织,避免了访问某一数据块需要从头把其前所有数据块再遍历一次的缺点。采用索引结构的文件系统,文件中的块依然可以分散到不连续的零散空间中,保留了磁盘高利用率的优点,更重要的是文件系统为每个文件的所有块建立了一个索引表,索引表就是块地址数组,每个数组元素就是块的地址,如且元素下标是文件块的索引,第 n 个数组元素指向文件中的第 n 个块,这样访问任意一个块的时候,只要从索引表中获得块地址就可以了,速度大大提升。包含此索引表的索引结构称为 inode,即 index node,索引结点,用来索引、跟踪一个文件的所有块。强调下, inode 是文件索引结构组织形式的具体体现,必须为每个文件单独配备一个这样的元信息数据结构,因此在UINX 文件系统中,一个文件必须对应一个 inode,磁盘中有多少文件就有多少 inode。

对于较小的文件,占用块少,使用直接块索引表,而对于较大的文件,占用块较多,UNIX 使用一级间接块索引表

image-20230817102144907

在 inode 结构中,几乎囊括了一个文件的所有信息,

文件系统为实现文件管理方案,必然会创造出一些辅助管理的数据结构,只要用于管理、控制文件相关信息的数据结构都被称为 FCB (File Contd Block),即文件控制块, inode 也是这种结构,因此 inode 是 FCB 的一种。形象一点地说 inode 相当于通往文件实体数据块的大门,这一点的作用类似于内存段的段描述符,只不过 inode 是文件实体数据块的描述符,里面规定了访问此文件数据块的权限、属主等安全方面的条件,有文件数据块总大小等描述信息,有文件实体数据块的具体地址。

要想通过文件系统获得文件的实体,必须先要找到文件的 inode,从这个意义上来说, inode 等同于文件。

inode 的数量等于文件的数量,为方便管理,分区中所有文件的 inode 通过一个大表格来维护,此表格称为 inode table,在计算机中表格都可以用数组来表示,因此 inode table 本质上就是 inode 数组,数组元素的下标便是文件 inode 的编号。

文件的数量间接决定了分区空间的利用率,说“间接”的原因是文件可大可小,每个文件大小不一 ,比如对于大小分别为 10KB 和 10GB 的文件,同一个 16GB 大小的分区可分别容纳的文件数必然相差甚远。因此一个分区的利用率分为 inode 的利用率和磁盘空间利用率两种


文件名应该存储在和目录相关的地方

目录和文件都用 inode 来表示,因此目录也是文件,只是目录是包含文件的文件 。 为了表述清楚这两种文件,我们这里称目录为目录文件, 一般意义上的文件称为普通文件 。 在 Linux 中,只要是文件就一定会有个与之匹配的 inode。按理说,既然同一种 inode 同时表示目录和普通文件,这说明在 inode 中它们是没区别的,那文件系统是如何区分目录和文件呢?

如果该 inode 表示的是普通文件,此 inode 指向的数据块中的内容应该是普通文件自己的数据。如果该 inode表示的是目录文件,此 inode 指向的数据块中的内容应该是该目录下的目录项,咱们只会支持目录文件和普通文件(不会存在管道文件、 socket 之类),因此目录中要么是普通文件的目录项,要么是目录文件的目录项。

目录相当于个文件列表(或者是表格),每个文件在目录中都是一个 entry(条目、项),各个 entry 中的内容包括文件名、文件类型,为了定位文件的数据, entry 中至少还要包括 inode 编号, 这个 entry 是目录中各个文件的描述,它称为目录项,目录项中至少要包括文件名、文件类型及文件对应的 inode 编号

目录项中包含文件名、 inode 编号和文件类型,它们三个的作用有两个:

  1. 标识此 inode 表示的文件是目录,还是普通文件,也就是 inode 所指向数据块中的内容是什么
  2. 将文件名与 inode 做个绑定关联,这样用户便可以通过文件名来找到文件的实体数据

通过文件名找文件实体数据块的流程是 :

  1. 在目录中找到文件名所在的目录项 。
  2. 从目录项中获取 inode 编号 。
  3. 用 inode 编号作为 inode 数组的索引下标,找到 inodeo
  4. 从该 inode 中获取数据块的地址,读取数据块 。

总结梳理下以上所介绍的内容。

  1. 每个文件都有自己单独的 inode, inode 是文件实体数据块在文件系统上的元信息。
  2. 所有文件的 inode 集中管理,形成 inode 数组,每个 inode 的编号就是在该 inode 数组中的下标。
  3. inode 中的前 12 个直接数据块指针和后 3 个间接块索引表用于指向文件的数据块实体。
  4. 文件系统中并不存在具体称为“目录”的数据结构,同样也没有称为“普通文件”的数据结构,统一用同一种 inode 表示。 inode 表示的文件是普通文件,还是目录文件,取决于 inode 所指向数据块中的实际内容是什么,即数据块中的内容要么是普通文件本身的数据,要么是目录中的目录工页。
  5. 目录项仅存在于 inode 指向的数据块中,有目录项的数据块就是目录,目录项所属的 inode 指向的所有数据块便是目录。
  6. 目录项中记录的是文件名、文件 inode 的编号和文件类型,目录项起到的作用有两个, 一是粘合文件名及 inode,使文件名和 inode 关联绑定,二是标识此 inode 所指向的数据块中的数据类型(比如是普通文件,还是目录,当然还有更多的类型)。
  7. inode 是文件的“实质”,但它并不能直接引用,必须通过文件名找到文件名所在的目录项,然后从该目录项中获得 inode 的编号,然后用此编号到 inode 数组中去找相关的 inode,最终找到文件的数据块。

inode1目录下有目录d3和普通文件f2

我们需要在某个固定地方去获取文件系统元信息的配置,这个地方就是超级块,超级块是保存文件系统元信息的元信息。

文件系统是针对各个分区来管理的, inode 代表文件,因此各分区都有自己的 inode 数组。 尽管各分区可创建的最大文件数是固定的,但这并不表示所有分区的最大文件数都相同,各分区可创建的最大文件数是在为分区创建文件系统(格式化〉时设置的(如在用 mkfs 工具为分区创建文件系统时就有 inode 数量的设置〉,我们可以为不同的分区设置不同的文件数 。 总之,各分区 inode 数组长度是固定的,等于最大文件数。既然 inode 数量是有限的,必须要有一种管理 inode 使用情况的方法,我们已经会用位图管理内存了,因此咱们也用位图来管理 inode 的使用情况,好,现在又多了 一个元信息, inode 位图。

除了文件系统的元信息外,就剩下可用的空闲块了,文件系统被创建出来的目的就是为了合理科学地管理这些空闲块,空闲块也是有限的,因此空闲块的使用情况也需要被跟踪,所以咱们也要为这些空闲块准备个位图。


咱们所讨论出来的内容己经足够实现一个简单的文件系统了,总结一下它们有: inode 数组的地址及大小、 inode 位图地址及大小、根目录的地址和大小、空闲块位图的地址和大小,以上这几类信息要在超级块中保存。

image-20230817105822384

超级块是文件系统元信息的“配置文件”,它是在为分区创建文件系统时创建的,所有有关文件系统元信息的配置都在超级块中,因此超级块的位置和大小不能再被“配置”了,必须是固定的,它被固定存储在各分区的第 2 个扇区,通常是占用一个扇区的大小,具体大小与实际文件系统类型为准。

image-20230820104457071

创建文件系统

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

/* 超级块 */
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));//保证编译后超级块实例大小位512字节

为方便写程序,咱们的数据块大小与扇区大小一致,即 1 块等于 1 扇区,

/* inode结构 */
struct inode {
    uint32_t i_no;    // inode编号,inode 数组中的下标

    /* 当此inode是文件时,i_size是指文件大小,
	若此inode是目录,i_size是指该目录下所有目录项大小之和*/
    uint32_t i_size;

    uint32_t i_open_cnts;   // 记录此文件被打开的次数,用于关闭文件时回收资源
    bool write_deny;	   // 写文件不能并行,进程写文件前检查此标识

    /* i_sectors[0-11]是直接块, i_sectors[12]用来存储一级间接块指针 */
    uint32_t i_sectors[13];
    struct list_elem inode_tag;//用于加入“已打开的inode列表”
};
#define MAX_FILE_NAME_LEN  16	 // 最大文件名长度

/* 目录结构 */
struct dir {
   struct 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;	      // 文件类型
};
#define MAX_FILES_PER_PART 4096	    // 每个分区所支持最大创建的文件数
#define BITS_PER_SECTOR 4096	    // 每扇区的位数
#define SECTOR_SIZE 512		    // 扇区字节大小
#define BLOCK_SIZE SECTOR_SIZE	    // 块字节大小

/* 文件类型 */
enum file_types {
   FT_UNKNOWN,	  // 不支持的文件类型
   FT_REGULAR,	  // 普通文件
   FT_DIRECTORY	  // 目录
};

创建文件系统

/* 格式化分区,也就是初始化分区的元信息,创建文件系统 */
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);// inode 数组占用的扇区数 , 这是由 inode 的尺寸和数量决定的
    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;//根目录的 inode 编号为 0,也就是说 inode 数组中第 0 个 inode 我们留给了根目录。
    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);

创建文件系统就是创建文件系统所需要的元信息,这包括超级块位置及大小、 空闲块位图的位置及大小 、inode 位图的位置及大小 、 inode 数组的位置及大小、 空闲块起始地址、 根目录起始地址 。 创建步骤如下:

  1. 根据分区 part 大小, 计算分区文件系统各元信息需要的扇区数及位置 。
  2. 在 内 存中 创建超级块,将以上步骤计算的元信息写入超级块。
  3. 将超级块写入磁盘。
  4. 将元信息写入磁盘上各自的位置。
  5. 将根目录写入磁盘 。
    struct disk* hd = part->my_disk;//获取分区 part 自己所属的硬盘 hd
    /*******************************
     * 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);

文件系统的主要工作是资源管理,跟踪资源的状态是通过位图来实现的,因此创建文件系统就是创建各种资源的位图,位图肯定是在内存中先创建好,然后再将位图持久化到硬盘,"持久化”是指把数据写到可以长久保存信息的存储介质上,永远保存。

分区挂载的目的是为了使用分区上的数据,这免不了数据资源的增删改查,为了跟踪分区中的数据资源使用情况,肯定还是需要这些资源的位图。而位图的操作肯定是在内存中,但挂载分区前内存中可没有位图,这就需要事先把这些位图提前持久化到磁盘上(这正是咱们目前所做的工作),挂载分区时再把位图从硬盘上加载到内存。

创建的块位图大小是通过除法向上取整得到的,因此位图中最后 1 扇区的末尾在大多数情况下都会有多余的位,也就是说这些多余的位并不代表某个资源,或者说是这些位指代的“真正资源”并不存在,而是指向了硬盘上“其他不属于位图所指代资源的数据”,若在位图操作中强行使用这些位的话,必然会损坏那些资源外的数据。我们现在持久化到磁盘上的位图是以扇区为单位的,将来挂载分区时,再从磁盘上恢复的位图也会包括扇区末尾那些无效、多余的位,因此现在必须将这些多余位初始为 1 ,将来就不会再分配这些位对应的资源了,避免了出现错误的情况。

/***************************************
 * 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);
}
/* 在磁盘上搜索文件系统,若没有则格式化分区创建文件系统 */
void filesys_init() {
    uint8_t channel_no = 0, dev_no, part_idx = 0;

    /* sb_buf用来存储从硬盘上读入的超级块 */
    struct super_block* sb_buf = (struct super_block*)sys_malloc(SECTOR_SIZE);

    if (sb_buf == NULL) {
        PANIC("alloc memory failed!");
    }
    printk("searching filesystem......\n");
    while (channel_no < channel_cnt) {//通道遍历
        dev_no = 0;
        while(dev_no < 2) {//磁盘遍历
            if (dev_no == 0) {   // 跨过裸盘hd60M.img
                dev_no++;
                continue;
            }
            struct disk* hd = &channels[channel_no].devices[dev_no];
            struct partition* part = hd->prim_parts;
            while(part_idx < 12) {   // 4个主分区+8个逻辑分区
                if (part_idx == 4) {  // 开始处理逻辑分区
                    part = hd->logic_parts;
                }

                /* channels数组是全局变量,默认值为0,disk属于其嵌套结构,
                  * partition又为disk的嵌套结构,因此partition中的成员默认也为0.
                  * 若partition未初始化,则partition中的成员仍为0. 
                  * 下面处理存在的分区. */
                if (part->sec_cnt != 0) {  // 如果分区存在
                    memset(sb_buf, 0, SECTOR_SIZE);

                    /* 读出分区的超级块,根据魔数是否正确来判断是否存在文件系统 */
                    ide_read(hd, part->start_lba + 1, sb_buf, 1);   

                    /* 只支持自己的文件系统.若磁盘上已经有文件系统就不再格式化了 */
                    if (sb_buf->magic == 0x19590318) {
                        printk("%s has filesystem\n", part->name);
                    } else { // 其它文件系统不支持,一律按无文件系统处理
                        printk("formatting %s`s partition %s......\n", hd->name, part->name);
                        partition_format(part);
                    }
                }
                part_idx++;
                part++;	// 下一分区
            }
            dev_no++;	// 下一磁盘
        }
        channel_no++;	// 下一通道
    }
    sys_free(sb_buf);
}

Linux 内核所在的分区是默认分区,自系统启动后就以该分区为默认分区,该分区的根目录是固定存在的,要想使用其他新分区的话,需要用 mount 命令手动把新的分区挂载到默认分区的某个目录下,这就是上面所说的“拿”出来。尽管其他分区都有自己的根目录,但是默认分区的根目录才是所有分区的父目录,因此挂载分区之后,整个路径树就像一串葡萄。分区不用的时候还可以通过 umount 命令卸载,这就是上面所说的“收”起来。

mount 命令将一个分区挂载到默认分区(操作系统所在分区),但是我们的操作系统在一个裸盘上,其上没有分区,更没有文件系统,那就不用考虑分区挂载到哪个目录下,因此咱们要实现的挂载功能很简单一一直接选择待操作的分区。

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

struct partition* cur_part;	 // 默认情况下操作的是哪个分区

/* 在分区链表中找到名为part_name(arg)的分区,并将其指针赋值给cur_part */
// list_traversal 的回调函数
static bool mount_partition(struct list_elem* pelem/*分区partion中part_tag的地址*/, int arg) {
    char* part_name = (char*)arg;//将arg还原为字符指针
    struct partition* part = elem2entry(struct partition, part_tag, pelem);
    if (!strcmp(part->name, part_name)) {//对比列表中分区的分区名和传入的分区名,相等返回0
        cur_part = part;
        struct disk* hd = cur_part->my_disk;

        /* sb_buf用来存储从硬盘上读入的超级块(1扇区大小) */
        struct super_block* sb_buf = (struct super_block*)sys_malloc(SECTOR_SIZE);

        /* 在内存中创建分区cur_part的超级块 */
        cur_part->sb = (struct super_block*)sys_malloc(sizeof(struct super_block));
        if (cur_part->sb == NULL) {
            PANIC("alloc memory failed!");
        }

        /* 读入超级块 */
        memset(sb_buf, 0, SECTOR_SIZE);
        ide_read(hd, cur_part->start_lba + 1, sb_buf, 1);
        
        /* 把sb_buf中超级块的信息复制到分区的超级块sb中。*/
        memcpy(cur_part->sb, sb_buf, sizeof(struct super_block));
        //超级块虽然占一扇区,但我们的struct super_block结构并没有一扇区大小,所以只把超级块中有用的信息复制过来

        /********** 将硬盘上的块位图读入到内存,完成了块位图的加载*********/
        cur_part->block_bitmap.bits = (uint8_t*)sys_malloc(sb_buf->block_bitmap_sects * SECTOR_SIZE);
        if (cur_part->block_bitmap.bits == NULL) {
            PANIC("alloc memory failed!");
        }
        //块位图长度 = 块位图占用扇区数 * 扇区长度
        cur_part->block_bitmap.btmp_bytes_len = sb_buf->block_bitmap_sects * SECTOR_SIZE;
        /* 从硬盘上读入块位图到分区的block_bitmap.bits */
        ide_read(hd, sb_buf->block_bitmap_lba, cur_part->block_bitmap.bits, sb_buf->block_bitmap_sects);
        /*************************************************************/

        /**********     将硬盘上的inode位图读入到内存    ************/
        cur_part->inode_bitmap.bits = (uint8_t*)sys_malloc(sb_buf->inode_bitmap_sects * SECTOR_SIZE);
        if (cur_part->inode_bitmap.bits == NULL) {
            PANIC("alloc memory failed!");
        }
        cur_part->inode_bitmap.btmp_bytes_len = sb_buf->inode_bitmap_sects * SECTOR_SIZE;
        /* 从硬盘上读入inode位图到分区的inode_bitmap.bits */
        ide_read(hd, sb_buf->inode_bitmap_lba, cur_part->inode_bitmap.bits, sb_buf->inode_bitmap_sects);   
        /*************************************************************/

        list_init(&cur_part->open_inodes);
        printk("mount %s done!\n", part->name);

        /* 此处返回true是为了迎合主调函数list_traversal的实现,与函数本身功能无关。
      只有返回true时list_traversal才会停止遍历,减少了后面元素无意义的遍历.*/
        return true;
    }
    return false;     // 使list_traversal继续遍历
}

/* 在磁盘上搜索文件系统,若没有则格式化分区创建文件系统 */
void filesys_init() {
    ...
    /* 确定默认操作的分区 */
    char default_part[8] = "sdb1";//默认分区的名称
    /* 挂载分区 */
    list_traversal(&partition_list, mount_partition, (int)default_part);//遍历分区列表元素,直到mount_partition(list_elem, default_part)返回true或遍历结束
}

个人觉得, inode 其实也可称为“文件数据块描述符”(纯属个人杜撰,仅是为了突显与文件描述符的区别),用于描述文件的存储、权限等。但 inode 是操作系统为自己的文件系统准备的数据结构,它用于文件存储的管理,与用户关系不大,咱们要介绍的文件描述符才是与用户息息相关的。

文件描述符即 file descriptor,但凡叫“描述符”的数据结构都用于描述一个对象,文件描述符所描述的对象是文件的操作。为了搞清楚文件描述符的意义,咱们先看下它与尬。d陆的区别和联系。

读写文件的本质是:先通过文件的 inode 找到文件数据块的扇区地址,随后读写该扇区,从而实现了文件的读写。几乎所有的操作系统都允许一个进程同时、多次、打开同一个文件(并不关闭),同样该文件也可以被多个不同的进程同时打开。为实现文件任意位置的读写,执行读写操作时可以指定偏移量作为该文件内的起始地址,此偏移量相当于文件内的指针。也就是说,该文件每被打开一次,文件读写的偏移量都可以任意指定,即对同一个文件的多次读写都是各自操作各自的,任意一个文件操作的偏移量都不影响其他文件操作的偏移量。注意,这里所说的是“互不影响”是指文件内的“偏移量”,井不是文件内容,因为文件内容是共享的,对文件内容的修改必然会相互影响。

另外,通常情况下对文件的操作都只是读写文件的一小部分数据,即使想读写整个文件的话,由于一般文件的体积都较大,而内存缓冲区较小,所以文件的读写井不是一次性从头到尾操作整个文件,往往是通过连续多次的小数据量读写完成的,下一次读写的位置必须以上一次的读写位置为起始,因此,文件系统需要把任意时刻的偏移量记录下来。问题来了,偏移量应该记录在哪里呢? inode 中肯定不记录这些与文件操作相关的数据,人家只把有限的空间记录与存储相关的信息。为解决这个问题, Linux 提供了称为“文件结构”的数据结构(也称为 file 结构),专门用于记录与文件操作相关的信息,每次打开一个文件就会产生一个文件结构,多次打开该文件就为该文件生成多个文件结构,各自文件操作的偏移量分别记录在不同的文件结构中,从而实现了“即使同一个文件被同时多次打开,各自操作的偏移量也互不影响”的灵活性。文件结构的逻辑表示如图 14-15 所示,这是文件结构中最基本的数据成员。

image-20230817142018029

Linux 把所有的“文件结构”组织到一起形成数组统一管理,该数组称为文件表

inode用于描述文件存储相关信息,文件结构用于描述“文件打开”后,文件读写偏移量等信息。文件与 inode一一对应,一个文件仅有一个 inode , 一个 inode 仅对应一个文件。一个文件可以被多次打开,因此一个inode 可以有多个文件结构,多个文件结构可以对应同一个 inode.

通常情况下不会把“真正的、庞大的”文件表塞到狭小的 PCB 中, 一般只要在PCB 中建立个文件描述符数组就可以了,该数组成员不需要是真正的文件结构(至少包括 3 个成员),出于简单处理,咱们用 int 整型就足够了,用它存储文件表中文件结构的下标,如果您愿意的话,可以用位图或链表为任务支持更多,甚至无限的最大打开文件数。当用户进程打开文件时,文件系统给用户进程返回的是该进程 PCB 中文件描述符数组下标值,也就是文件描述符。

image-20230817142817189

某进程把文件描述符作为参数提交给文件系统时,文件系统用此文件描述符在该进程的 PCB 中的文件描述符数组中索引对应的元素,从该元素中获取对应的文件结构的下标,用该下标在文件表中索引相应的文件结构,从该文件结构中获取文件的 inode,最终找到了文件的数据块 。 提示一下,若该 inode 在 inode 队列中不存在,此时会多一个处理过程:文件系统会从硬盘上将该 inode 加载到 inode 队列中,并使文件结构中的“ inode 指向它,将来介绍 inode 操作相关的函数时会再次提到。

其实 open 操作的本质就是创建相应文件描述符的过程,剧透一下, PCB 中文件描述符数组是提前在 task struct 中构建好的,文件表也是提前构建好的全局数据结构, inode 队列也已经构建好了,因此笼统地说,创建文件描述符的过程就是逐层在这三个数据结构中找空位,在该空位填充好数据后返回该位置的地址,比如:

  1. 在全局的 inode 队列中新建一 inode (这肯定是在空位置处新建),然后返回该 inode 地址。
  2. 在全局的文件表中的找一空位,在该位置填充文件结构,使其创 inode 指向上一步中返回的 inode 地址,然后返回本文件结构在文件表中的下标值 。
  3. 在 PCB 中的文件描述符数组中找一空位,使该位置的值指向上一步中返回的文件结构下标,井返回本文件描述符在文件描述符数组中的下标值。

文件描述符的实现

#define MAX_FILES_OPEN_PER_PROC 8 //每个任务可以打开的文件数是8

/* 进程或线程的pcb,程序控制块 */
struct task_struct {
    ...

    /* 此任务自上cpu运行后至今占用了多少cpu嘀嗒数,
 * 也就是此任务执行了多久*/
    uint32_t elapsed_ticks;

    int32_t fd_table[MAX_FILES_OPEN_PER_PROC];	// 文件描述符数组

    /* general_tag的作用是用于线程在一般的队列中的结点 */
    struct list_elem general_tag;				
    ...
};


/* 初始化线程基本信息 */
void init_thread(struct task_struct* pthread, char* name, int prio) {
	...

    /* 预留标准输入输出 */
    //0 是标准输入, 1 是标准输出, 2 是标准错误
    pthread->fd_table[0] = 0;
    pthread->fd_table[1] = 1;
    pthread->fd_table[2] = 2;
    /* 其余的全置为-1 */
    uint8_t fd_idx = 3;
    while (fd_idx < MAX_FILES_OPEN_PER_PROC) {
        pthread->fd_table[fd_idx] = -1;
        fd_idx++;
    }

    pthread->stack_magic = 0x19870916;	  // 自定义的魔数
}

文件操作相关的基础函数

inode 操作有关的函数

/* 用来存储inode位置 */
struct inode_position {
    bool	 two_sec;	// inode是否跨扇区
    uint32_t sec_lba;	// inode所在的扇区号
    uint32_t off_size;	// inode在扇区内的字节偏移量
};

/* 获取inode所在的扇区和扇区内的偏移量 */
static void inode_locate(struct partition* part, uint32_t inode_no, struct inode_position* inode_pos) {
    /* inode_table在硬盘上是连续的 */
    ASSERT(inode_no < 4096);
    uint32_t inode_table_lba = part->sb->inode_table_lba;//已经将分区通过函数mount_partition挂载了,因此可以从分区超级块的 inode_table_Iba 中获取 inode_table 的扇区地址,

    uint32_t inode_size = sizeof(struct inode);
    uint32_t off_size = inode_no * inode_size; // 第inode_no号i结点相对于inode_table_lba的字节偏移量
    uint32_t off_sec  = off_size / 512; // 第inode_no号i结点相对于inode_table_lba的扇区偏移量
    uint32_t off_size_in_sec = off_size % 512; // 待查找的inode所在扇区中的起始地址

    /* 判断此i结点是否跨越2个扇区 */
    //判断该 inode 所在扇区的起始地址到扇区结束之间的“剩余空间”是否大于 inode 尺寸
    uint32_t left_in_sec = 512 - off_size_in_sec;
    if (left_in_sec < inode_size ) {	  // 若扇区内剩下的空间不足以容纳一个inode,必然是i结点跨越了2个扇区
        inode_pos->two_sec = true;
    } else {				  // 否则,所查找的inode未跨扇区
        inode_pos->two_sec = false;
    }
    inode_pos->sec_lba = inode_table_lba + off_sec;
    inode_pos->off_size = off_size_in_sec;
}

/* 将inode写入到分区part */
void inode_sync(struct partition* part, struct inode* inode, void* io_buf) { // io_buf是用于硬盘io的缓冲区
    uint8_t inode_no = inode->i_no;
    struct inode_position inode_pos;
    inode_locate(part, inode_no, &inode_pos);	       // inode位置信息会存入inode_pos
    ASSERT(inode_pos.sec_lba <= (part->start_lba + part->sec_cnt));

    /* 硬盘中的inode中的成员inode_tag和i_open_cnts是不需要的,
    * 它们只在内存中记录链表位置和被多少进程共享 */
    struct inode pure_inode;
    memcpy(&pure_inode, inode, sizeof(struct inode));

    /* 以下inode的三个成员只存在于内存中,现在将inode同步到硬盘,清掉这三项即可 */
    pure_inode.i_open_cnts = 0;
    pure_inode.write_deny = false;	 // 置为false,以保证在硬盘中读出时为可写
    pure_inode.inode_tag.prev = pure_inode.inode_tag.next = NULL;

    char* inode_buf = (char*)io_buf;
    if (inode_pos.two_sec) {	    // 若是跨了两个扇区,就要读出两个扇区再写入两个扇区
        /* 读写硬盘是以扇区为单位,若写入的数据小于一扇区,要将原硬盘上的内容先读出来再和新数据拼成一扇区后再写入  */
        ide_read(part->my_disk, inode_pos.sec_lba, inode_buf, 2);	// inode在format中写入硬盘时是连续写入的,所以读入2块扇区

        /* 开始将待写入的inode拼入到这2个扇区中的相应位置 */
        memcpy((inode_buf + inode_pos.off_size), &pure_inode, sizeof(struct inode));

        /* 将拼接好的数据再写入磁盘 */
        ide_write(part->my_disk, inode_pos.sec_lba, inode_buf, 2);
    } else {			    // 若只是一个扇区
        ide_read(part->my_disk, inode_pos.sec_lba, inode_buf, 1);
        memcpy((inode_buf + inode_pos.off_size), &pure_inode, sizeof(struct inode));
        ide_write(part->my_disk, inode_pos.sec_lba, inode_buf, 1);
    }
}

/* 根据i结点号返回相应的i结点 */
struct inode* inode_open(struct partition* part, uint32_t inode_no) {
    /* 先在已打开inode链表中找inode,此链表是为提速创建的缓冲区 */
    //每打开 1 个 inode 时,先在此缓存中查找该 inode,找到后则直接返回 inode 指针,若未找到,再从磁盘上加载该 inode 到此缓存中,然后再返回其指针。
    struct list_elem* elem = part->open_inodes.head.next;
    struct inode* inode_found;
    while (elem != &part->open_inodes.tail) {
        inode_found = elem2entry(struct inode, inode_tag, elem);
        if (inode_found->i_no == inode_no) {
            inode_found->i_open_cnts++;
            return inode_found;
        }
        elem = elem->next;
    }

    /*由于open_inodes链表中找不到,下面从硬盘上读入此inode并加入到此链表 */
    struct inode_position inode_pos;

    /* inode位置信息会存入inode_pos, 包括inode所在扇区地址和扇区内的字节偏移量 */
    inode_locate(part, inode_no, &inode_pos);

    /* 为使通过sys_malloc创建的新inode被所有任务共享,
     * 需要将inode置于内核空间,故需要临时
     * 将cur_pbc->pgdir置为NULL */
    struct task_struct* cur = running_thread();
    uint32_t* cur_pagedir_bak = cur->pgdir;//保存当前任务的页目录地址
    cur->pgdir = NULL;
    /* 以上三行代码完成后下面分配的内存将位于内核区 */
    inode_found = (struct inode*)sys_malloc(sizeof(struct inode));
    /* 恢复pgdir */
    cur->pgdir = cur_pagedir_bak;

    char* inode_buf;
    if (inode_pos.two_sec) {	// 考虑跨扇区的情况
        inode_buf = (char*)sys_malloc(1024);

        /* i结点表是被partition_format函数连续写入扇区的,
   		* 所以下面可以连续读出来 */
        ide_read(part->my_disk, inode_pos.sec_lba, inode_buf, 2);
    } else {	// 否则,所查找的inode未跨扇区,一个扇区大小的缓冲区足够
        inode_buf = (char*)sys_malloc(512);
        ide_read(part->my_disk, inode_pos.sec_lba, inode_buf, 1);
    }
    memcpy(inode_found, inode_buf + inode_pos.off_size, sizeof(struct inode));

    /* 因为一会很可能要用到此inode,故将其插入到队首便于提前检索到 */
    list_push(&part->open_inodes, &inode_found->inode_tag);
    inode_found->i_open_cnts = 1;

    sys_free(inode_buf);
    return inode_found;
}

/* 关闭inode或减少inode的打开数 */
void inode_close(struct inode* inode) {
    /* 若没有进程再打开此文件,将此inode去掉并释放空间 */
    enum intr_status old_status = intr_disable();
    if (--inode->i_open_cnts == 0) {//此inode是否未被打开
        list_remove(&inode->inode_tag);	  // 将i结点从part->open_inodes中去掉
        /* inode_open时为实现inode被所有进程共享,
        * 已经在sys_malloc为inode分配了内核空间,
        * 释放inode时也要确保释放的是内核内存池 */
        struct task_struct* cur = running_thread();
        uint32_t* cur_pagedir_bak = cur->pgdir;
        cur->pgdir = NULL;
        sys_free(inode);
        cur->pgdir = cur_pagedir_bak;
    }
    intr_set_status(old_status);
}

/* 初始化new_inode */
void inode_init(uint32_t inode_no, struct inode* new_inode) {
    new_inode->i_no = inode_no;
    new_inode->i_size = 0;
    new_inode->i_open_cnts = 0;
    new_inode->write_deny = false;

    /* 初始化块索引数组i_sector */
    uint8_t sec_idx = 0;
    while (sec_idx < 13) {
        /* i_sectors[12]为一级间接块地址 */
        new_inode->i_sectors[sec_idx] = 0;
        sec_idx++;
    }
}

为什么不提前为 inode 分配扇区?是这样的,创建 inode 时不为其分配扇区,只有在真正写文件时才分配扇区,理由是首先不知道文件大小,因此不知道分配多少个扇区合适,提前分配的扇区都是浪费空间。其次,文件创建后未必马上会写数据,真正需要往文件中写数据时根据实际数据量大小再分配扇区,效率更高一些

创建 Inode 时不为其分配扇区,只有在真正写文件时才分配扇区,理由是首先不知道文件大小,因此不知道分配多少个扇区合适,提前分配的扇区都是浪费空间。其次,文件创建后未必马上会写数据,真正需要往文件中写数据时根据实际数据量大小再分配扇区,效率更高一些。

文件相关的函数

/* 文件结构 */
struct file {
   uint32_t fd_pos;      // 记录当前文件操作的偏移地址,以0为起始,最大为文件大小-1
   uint32_t fd_flag; //文件操作标识,如只读、读写
   struct inode* fd_inode; //指向inode队列( part-> open_inodes )中的 inode
};

/* 标准输入输出描述符 */
enum std_fd {
   stdin_no,   // 0 标准输入
   stdout_no,  // 1 标准输出
   stderr_no   // 2 标准错误
};

/* 位图类型 */
enum bitmap_type {
   INODE_BITMAP,     // inode位图
   BLOCK_BITMAP	     // 块位图
};

#define MAX_FILE_OPEN 32    // 系统可打开的最大文件数
#define DEFAULT_SECS 1

/* 文件表 */
struct file file_table[MAX_FILE_OPEN];//最多可同时打开 MAX_FILE_OPEN 次文件

/* 从文件表file_table中获取一个空闲位,成功返回下标,失败返回-1 */
int32_t get_free_slot_in_global(void) {
    uint32_t fd_idx = 3;
    while (fd_idx < MAX_FILE_OPEN) {
        if (file_table[fd_idx].fd_inode == NULL) {
            break;
        }
        fd_idx++;
    }
    if (fd_idx == MAX_FILE_OPEN) {
        printk("exceed max open files\n");
        return -1;
    }
    return fd_idx;
}

/* 将全局描述符下标安装到进程或线程自己的文件描述符数组fd_table中,
 * 成功返回下标,失败返回-1 */
int32_t pcb_fd_install(int32_t globa_fd_idx) {
    struct task_struct* cur = running_thread();
    uint8_t local_fd_idx = 3; // 跨过stdin,stdout,stderr三个标准文件描述符
    while (local_fd_idx < MAX_FILES_OPEN_PER_PROC) {
        if (cur->fd_table[local_fd_idx] == -1) {	// -1表示free_slot,可用
            cur->fd_table[local_fd_idx] = globa_fd_idx;
            break;
        }
        local_fd_idx++;
    }
    if (local_fd_idx == MAX_FILES_OPEN_PER_PROC) {
        printk("exceed max open files_per_proc\n");
        return -1;
    }
    return local_fd_idx;
}

/* 分配一个i结点,返回i结点号 */
int32_t inode_bitmap_alloc(struct partition* part) {
    int32_t bit_idx = bitmap_scan(&part->inode_bitmap, 1);//在位图中请求一位
    if (bit_idx == -1) {
        return -1;
    }
    bitmap_set(&part->inode_bitmap, bit_idx, 1);
    return bit_idx;
}

/* 分配1个扇区,返回其扇区地址 */
int32_t block_bitmap_alloc(struct partition* part) {
    int32_t bit_idx = bitmap_scan(&part->block_bitmap, 1);
    if (bit_idx == -1) {
        return -1;
    }
    bitmap_set(&part->block_bitmap, bit_idx, 1);
    /* 和inode_bitmap_malloc不同,此处返回的不是位图索引,而是具体可用的扇区地址 */
    return (part->sb->data_start_lba + bit_idx);
} 

/* 将内存中bitmap第bit_idx位所在的512字节同步到硬盘 */
//因为硬盘是以扇区为读写单位,所以内存中的数据也要一次操作 1 扇区
void bitmap_sync(struct partition* part, uint32_t bit_idx, uint8_t btmp_type) {
    uint32_t off_sec = bit_idx / 4096;  // 本i结点索引相对于位图的扇区偏移量
    uint32_t off_size = off_sec * BLOCK_SIZE;  // 本i结点索引相对于位图的字节偏移量
    uint32_t sec_lba;
    uint8_t* bitmap_off;

    /* 需要被同步到硬盘的位图只有inode_bitmap和block_bitmap */
    /*分别计算出位图的扇区地址 sec_lba 和宇节偏移量 bitmap_off, bitmap_off 是待同步数据的起始地址,最后通过 ide_write 将位图写入硬盘*/
    switch (btmp_type) {
        case INODE_BITMAP:
            sec_lba = part->sb->inode_bitmap_lba + off_sec;
            bitmap_off = part->inode_bitmap.bits + off_size;
            break;

        case BLOCK_BITMAP: 
            sec_lba = part->sb->block_bitmap_lba + off_sec;
            bitmap_off = part->block_bitmap.bits + off_size;
            break;
    }
    ide_write(part->my_disk, sec_lba, bitmap_off, 1);
}

目录相关函数

struct dir root_dir;             // 根目录

/* 打开根目录 */
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* 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 */
bool search_dir_entry(struct partition* part, struct dir* pdir, \
                      const char* name, struct dir_entry* dir_e) {
    uint32_t block_cnt = 140;	 // block总数 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) {//将目录inode的i_sectors中的前12个扇区地址录入到all_blocks中
        all_blocks[block_idx] = pdir->inode->i_sectors[block_idx];
        block_idx++;
    }
    block_idx = 0;

    if (pdir->inode->i_sectors[12] != 0) {	// 若含有一级间接块表
        //从硬盘的扇区地址i_sectors[12]处获取1扇区数据,此数据是128个间接块地址,将其复制到all_blocks+12字节处
        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;
}

/* 关闭目录 */
void dir_close(struct dir* dir) {
    /*************      根目录不能关闭     ***************
     *1 根目录自打开后就不应该关闭,否则还需要再次open_root_dir();
     *2 root_dir所在的内存是低端1M之内,并非在堆中,free会出问题 */
    if (dir == &root_dir) {
        /* 不做任何处理直接返回*/
        return;
    }
    inode_close(dir->inode);
    sys_free(dir);
}

/* 在内存中初始化目录项p_de */
//将文件名拷贝到目录项p_de->filename中,用inode_no为p_de->i_no赋值,用file_type为p_de->f_type赋值
void create_dir_entry(char* filename, uint32_t inode_no, uint8_t file_type, struct dir_entry* p_de){
    ASSERT(strlen(filename) <=  MAX_FILE_NAME_LEN);

    /* 初始化目录项 */
    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由主调函数提供 */
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;
}

路径解析相关函数

什么是路径解析昵?简单来说,就是把路径按照路径分隔符拆分成多层文件名,逐层在磁盘上查找以确认文件名是否存在。比如路径“/a/b/c ”会被分别拆分成“ a”,“ b”,~飞 Linux 中一切皆文件,因此同一目录下不能出现同名的普通文件和目录。从路径上咱们可以猜出“a”和“b”都是目录(但也有可能“a ”只是个普通文件,用户误把它当成目录),“c”可能是普通文件,也可能是目录。不管猜的对不对,文件系统必须逐层到目录项中去确认。最左边的’/’表示根目录,首先在根目录下查找 a,根据目录项的文件类型,发现它是目录,继续在 a 目录中查找 b,同样发现其是目录,然后在 b 目录中查找。 c 也许是普通文件,也可能是目录,这还是要根据目录项来判断。当然了,在逐层查找的过程中,也许某层路径就不存在了,比如在 a 目录中未找到文件 b 时,后面的子路径 c 就不需要再查了。

大伙儿不要觉得只有在查找文件时才需要路径解析,路径解析是在任何时刻都需要的功能,比如命令“ cd /a/b”,也要先从根目录/开始,逐层解析 a 和 b 目录。

/* 将最上层路径名称解析出来 */
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 */
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;
}

实现文件检索功能

#define MAX_PATH_LEN 512	    // 路径最大长度

/* 文件类型 */
enum file_types {
   FT_UNKNOWN,	  // 不支持的文件类型
   FT_REGULAR,	  // 普通文件
   FT_DIRECTORY	  // 目录
};

/* 打开文件的选项 */
enum oflags {
   O_RDONLY,	  // 只读
   O_WRONLY,	  // 只写
   O_RDWR,	  // 读写
   O_CREAT = 4	  // 创建
};

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


/* 搜索文件pathname,若找到则返回其inode号,否则返回-1 */
static int search_file(const char* pathname, struct path_search_record* searched_record) {
    /* 如果待查找的是根目录,为避免下面无用的查找,直接返回已知根目录信息 */
    if (!strcmp(pathname, "/") || !strcmp(pathname, "/.") || !strcmp(pathname, "/..")) {
        searched_record->parent_dir = &root_dir;
        searched_record->file_type = FT_DIRECTORY;
        searched_record->searched_path[0] = 0;	   // 搜索路径置空
        return 0;
    }

    uint32_t path_len = strlen(pathname);
    /* 保证pathname至少是这样的路径/x且小于最大长度 */
    ASSERT(pathname[0] == '/' && path_len > 1 && path_len < MAX_PATH_LEN);
    char* sub_path = (char*)pathname;
    struct dir* parent_dir = &root_dir;	
    struct dir_entry dir_e;

    /* 记录路径解析出来的各级名称,如路径"/a/b/c",
    * 数组name每次的值分别是"a","b","c" */
    char name[MAX_FILE_NAME_LEN] = {0};

    searched_record->parent_dir = parent_dir;
    searched_record->file_type = FT_UNKNOWN;
    uint32_t parent_inode_no = 0;  // 父目录的inode号

    sub_path = path_parse(sub_path, name);
    while (name[0]) {	   // 若第一个字符就是结束符,结束循环
        /* 记录查找过的路径,但不能超过searched_path的长度512字节 */
        ASSERT(strlen(searched_record->searched_path) < 512);

        /* 记录已存在的父目录 */
        strcat(searched_record->searched_path, "/");
        strcat(searched_record->searched_path, name);

        /* 在所给的目录中查找文件 */
        if (search_dir_entry(cur_part, parent_dir, name, &dir_e)) {
            memset(name, 0, MAX_FILE_NAME_LEN);
            /* 若sub_path不等于NULL,也就是未结束时继续拆分路径 */
            if (sub_path) {
                sub_path = path_parse(sub_path, name);
            }

            if (FT_DIRECTORY == dir_e.f_type) {   // 如果被打开的是目录
                parent_inode_no = parent_dir->inode->i_no;
                dir_close(parent_dir);
                parent_dir = dir_open(cur_part, dir_e.i_no); // 更新父目录
                searched_record->parent_dir = parent_dir;
                continue;
            } else if (FT_REGULAR == dir_e.f_type) {	 // 若是普通文件
                searched_record->file_type = FT_REGULAR;
                return dir_e.i_no;
            }
        } else {		   //若找不到,则返回-1
            /* 找不到目录项时,要留着parent_dir不要关闭,
	  		* 若是创建新文件的话需要在parent_dir中创建 */
            return -1;
        }
    }

    /* 执行到此,必然是遍历了完整路径并且查找的文件或目录只有同名目录存在 */
    dir_close(searched_record->parent_dir);	      

    /* 保存被查找目录的直接父目录 */
    searched_record->parent_dir = dir_open(cur_part, parent_inode_no);	   
    searched_record->file_type = FT_DIRECTORY;
    return dir_e.i_no;
}

创建文件

实现file_create

创建文件需要考虑哪些工作。

  1. 文件需要 inode 来描述大小、位置等属性 ,所以创建文件就要创建其 inode 。这就涉及到向 inode_bitmap 申请位图来获得 inode 号,因此 inode_bitmap 会被更新, inode_table 数组中的某项也会由新的 inode 填充 。
  2. inode->i_sectors 是文件具体存储的扇区地址,这需要向 block_bitmap 申请可用位来获得可用的块(在我们这里,为简化处理, 1 块等于 1 扇区),因此 block_bitmap 会被更新,分区的数据区 data_start_lba以后的某个扇区会被分配
  3. 新增加的文件必然存在于某个目录,所以该目录的 inode->i_size 会增加个目录项的大小 。 此新增加的文件对应的目录项需要写入该目录的 inode->i_sectors[] 中的某个扇区,原有扇区可能己满,所以有可能要申请新扇区来存储目录项
  4. 若其中某步操作失败,需要回滚之前己成功的操作。
  5. inode_bitmap 、 block_bitmap、新文件的 inode 及文件所在目录的 inode,这些位于内存中已经被改变的数据要同步到硬盘。

创建新文件的顺序是:创建文件 i 结点 -> 文件描述符fd ->目录项

/* 创建文件,若成功则返回文件描述符,否则返回-1 */
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();//从file_table中获取空闲文件结构的下标
    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:
            /* 新文件的inode内存分配失败时,回滚file_table中的位图状态 */
            memset(&file_table[fd_idx], 0, sizeof(struct file)); //没有break,是累加回滚
        case 2:
            /* file_table中申请空闲文件结构失败,释放新文件inode内存 */
            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

/* 打开或创建文件成功后,返回文件描述符,否则返回-1 */
int32_t sys_open(const char* pathname, uint8_t flags) {
    /* 若输入为"/a/"则直接认定为目录,对目录要用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;
    }

    //目前只支持 sys_open("xxx", O_CREAT | O_XXX)的用法
    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);
            // 其余为打开文件
    }

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

/* 在磁盘上搜索文件系统,若没有则格式化分区创建文件系统 */
void filesys_init() {
    ...

    /* 确定默认操作的分区 */
    char default_part[8] = "sdb1";
    /* 挂载分区 */
    list_traversal(&partition_list, mount_partition, (int)default_part);

    /* 将当前分区的根目录打开 */
    open_root_dir(cur_part);

    /* 初始化文件表 */
    uint32_t fd_idx = 0;
    while (fd_idx < MAX_FILE_OPEN) {
        file_table[fd_idx++].fd_inode = NULL;
    }
}

在文件系统上创建第一个文件

int main(void) {
    put_str("I am kernel\n");
    init_all();
    process_execute(u_prog_a, "u_prog_a");
    process_execute(u_prog_b, "u_prog_b");
    thread_start("k_thread_a", 31, k_thread_a, "I am thread_a");
    thread_start("k_thread_b", 31, k_thread_b, "I am thread_b");
    sys_open("/file1", O_CREAT);
    while(1);
    return 0;
}

文件的打开

/* 打开编号为inode_no的inode对应的文件,若成功则返回文件描述符,否则返回-1 */
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;   // file_table中该文件write_deny置为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);
}

改进sys_open

/* 打开或创建文件成功后,返回文件描述符,否则返回-1 */
int32_t sys_open(const char* pathname, uint8_t flags) {
    ...

    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:
            /* 其余情况均为打开已存在文件:
    		* O_RDONLY,O_WRONLY,O_RDWR */
            fd = file_open(inode_no, flags);
    }

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

/* 将文件描述符转化为文件表的下标 */
//原理就是将 local_fd 作为下标代入数组 fd_table,"fd_table[local_fd]"的值便是文件表的下标。
static uint32_t fd_local2global(uint32_t local_fd) {
   struct task_struct* cur = running_thread();
   int32_t global_fd = cur->fd_table[local_fd];  
   ASSERT(global_fd >= 0 && global_fd < MAX_FILE_OPEN);
   return (uint32_t)global_fd;
} 

/* 关闭文件描述符fd指向的文件,成功返回0,否则返回-1 */
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;
}
int main(void) {
   put_str("I am kernel\n");
   init_all();
   process_execute(u_prog_a, "u_prog_a");
   process_execute(u_prog_b, "u_prog_b");
   thread_start("k_thread_a", 31, k_thread_a, "I am thread_a");
   thread_start("k_thread_b", 31, k_thread_b, "I am thread_b");

   uint32_t fd = sys_open("/file1", O_RDONLY);
   printf("fd:%d\n", fd);
   sys_close(fd);
   printf("%d closed now\n", fd);
   while(1);
   return 0;
}

实现文件写入

实现file_write

处理需要分配新数据块的情况,下面分三种情况讨论。

  1. 若己经使用的扇区数在 12 块之内,新增了若干块后,文件大小还在 12 块之内,直接分配所需的块并把块地址写入 i sectors 数组中即可。
  2. 若己经使用的块数在 12 块之内,新增了若干块后,文件大小超过了 12 块,这种情况下所申请的块除了要写入 i sector 数组,还要创建一级间接块表并写入块地址。
  3. 若已经使用的扇区数超过了 12 块,这种情况下要在一级间接块表中创建间接块项,间接块项就
    是在一级间接块表中记录间接块地址的条目,姑且这么叫吧。
/* 把buf中的count个字节写入file,成功则返回写入的字节数,失败则返回-1 */
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);   // 同步一级间接块表到硬盘
        } 
    }

    /* 到目前为止,all_blocks 中包含可继续使用的、含有剩余空间的块地址,以及新数据要占用的新的块地址 */
    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;

        /* 判断此次写入硬盘的数据大小 */
        /* size_left表示剩下未写入硬盘的数据量,当 size_left小于本次所写入块的剩余空间时,chunk_size就等于size_left,这属于剩余的数据不足一个块,往最后一个块中写剩余数据时的情况,如果size_left大于等于sec_left_bytes,那就把剩下的空闲空间写满,即chunk_size等于sec_left_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;
}

改进 sys_write 及 write 系统调用

/* 将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;
    }
}
enum SYSCALL_NR {
    SYS_GETPID,
    SYS_WRITE,
    SYS_MALLOC,
    SYS_FREE
};

//新增系统调用
/* 把buf中count个字符写入文件描述符fd */
uint32_t write(int32_t fd, const void* buf, uint32_t count) {
   return _syscall3(SYS_WRITE, fd, buf, count);
}
/* 格式化输出字符串format */
uint32_t printf(const char* format, ...) {
   va_list args;
   va_start(args, format);	       // 使args指向format
   char buf[1024] = {0};	       // 用于存储拼接后的字符串
   vsprintf(buf, format, args);
   va_end(args);
   return write(1, buf, strlen(buf)); 
}

把数据写入文件

int main(void) {
   put_str("I am kernel\n");
   init_all();
   process_execute(u_prog_a, "u_prog_a");
   process_execute(u_prog_b, "u_prog_b");
   thread_start("k_thread_a", 31, k_thread_a, "I am thread_a");
   thread_start("k_thread_b", 31, k_thread_b, "I am thread_b");

   uint32_t fd = sys_open("/file1", O_RDWR);
   printf("fd:%d\n", fd);
   sys_write(fd, "hello,world\n", 12);
   sys_close(fd);
   printf("%d closed now\n", fd);
   while(1);
   return 0;
}

读取文件

实现file_read

/* 从文件file中读取count个字节写入buf, 返回读出的字节数,若到文件尾则返回-1 */
int32_t file_read(struct file* file, void* buf, uint32_t count) {
    uint8_t* buf_dst = (uint8_t*)buf;
    uint32_t size = count, size_left = size;

    /* 若要读取的字节数超过了文件可读的剩余量, 就用剩余量做为待读取的字节数 */
    if ((file->fd_pos + count) > file->fd_inode->i_size)	{
        size = file->fd_inode->i_size - file->fd_pos;
        size_left = size;
        if (size == 0) {	   // 若到文件尾则返回-1
            return -1;
        }
    }

    uint8_t* io_buf = sys_malloc(BLOCK_SIZE);
    if (io_buf == NULL) {
        printk("file_read: sys_malloc for io_buf failed\n");
    }
    uint32_t* all_blocks = (uint32_t*)sys_malloc(BLOCK_SIZE + 48);	  // 用来记录文件所有的块地址
    if (all_blocks == NULL) {
        printk("file_read: sys_malloc for all_blocks failed\n");
        return -1;
    }

    uint32_t block_read_start_idx = file->fd_pos / BLOCK_SIZE; // 数据所在块的起始地址
    uint32_t block_read_end_idx = (file->fd_pos + size) / BLOCK_SIZE; // 数据所在块的终止地址
    uint32_t read_blocks = block_read_start_idx - block_read_end_idx; // 如增量为0,表示数据在同一扇区
    ASSERT(block_read_start_idx < 139 && block_read_end_idx < 139);

    int32_t indirect_block_table;       // 用来获取一级间接表地址
    uint32_t block_idx;		       // 获取待读的块地址 

    /* 以下开始构建all_blocks块地址数组,专门存储用到的块地址(本程序中块大小同扇区大小) */
    if (read_blocks == 0) {       // 在同一扇区内读数据,不涉及到跨扇区读取
        ASSERT(block_read_end_idx == block_read_start_idx);
        if (block_read_end_idx < 12 ) {	   // 待读的数据在12个直接块之内
            block_idx = block_read_end_idx;
            all_blocks[block_idx] = file->fd_inode->i_sectors[block_idx];
        } else {		// 若用到了一级间接块表,需要将表中间接块读进来
            indirect_block_table = file->fd_inode->i_sectors[12];
            ide_read(cur_part->my_disk, indirect_block_table, all_blocks + 12, 1);
        }
    } else {      // 若要读多个块
        /* 第一种情况: 起始块和终止块属于直接块*/
        if (block_read_end_idx < 12 ) {	  // 数据结束所在的块属于直接块
            block_idx = block_read_start_idx; 
            while (block_idx <= block_read_end_idx) {
                all_blocks[block_idx] = file->fd_inode->i_sectors[block_idx]; 
                block_idx++;
            }
        } else if (block_read_start_idx < 12 && block_read_end_idx >= 12) {
            /* 第二种情况: 待读入的数据跨越直接块和间接块两类*/
            /* 先将直接块地址写入all_blocks */
            block_idx = block_read_start_idx;
            while (block_idx < 12) {
                all_blocks[block_idx] = file->fd_inode->i_sectors[block_idx];
                block_idx++;
            }
            ASSERT(file->fd_inode->i_sectors[12] != 0);	    // 确保已经分配了一级间接块表

            /* 再将间接块地址写入all_blocks */
            indirect_block_table = file->fd_inode->i_sectors[12];
            ide_read(cur_part->my_disk, indirect_block_table, all_blocks + 12, 1); // 将一级间接块表读进来写入到第13个块的位置之后
        } 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); // 将一级间接块表读进来写入到第13个块的位置之后
        } 
    }

    /* 用到的块地址已经收集到all_blocks中,下面开始读数据 */
    uint32_t sec_idx, sec_lba, sec_off_bytes, sec_left_bytes, chunk_size;
    uint32_t bytes_read = 0;
    while (bytes_read < size) {	      // 直到读完为止
        sec_idx = file->fd_pos / BLOCK_SIZE;
        sec_lba = all_blocks[sec_idx];
        sec_off_bytes = file->fd_pos % BLOCK_SIZE;
        sec_left_bytes = BLOCK_SIZE - sec_off_bytes;
        chunk_size = size_left < sec_left_bytes ? size_left : sec_left_bytes; // 待读入的数据大小

        memset(io_buf, 0, BLOCK_SIZE);
        ide_read(cur_part->my_disk, sec_lba, io_buf, 1);
        memcpy(buf_dst, io_buf + sec_off_bytes, chunk_size);

        buf_dst += chunk_size;
        file->fd_pos += chunk_size;
        bytes_read += chunk_size;
        size_left -= chunk_size;
    }
    sys_free(all_blocks);
    sys_free(io_buf);
    return bytes_read;
}

实现 sys_read 与功能验证

/* 从文件描述符fd指向的文件中读取count个字节到buf,若成功则返回读出的字节数,到文件尾则返回-1 */
int32_t sys_read(int32_t fd, void* buf, uint32_t count) {
    if (fd < 0) {
        printk("sys_read: fd error\n");
        return -1;
    }
    ASSERT(buf != NULL);
    uint32_t _fd = fd_local2global(fd);
    return file_read(&file_table[_fd], buf, count);   
}

int main(void) {
    put_str("I am kernel\n");
    init_all();
    process_execute(u_prog_a, "u_prog_a");
    process_execute(u_prog_b, "u_prog_b");
    thread_start("k_thread_a", 31, k_thread_a, "I am thread_a");
    thread_start("k_thread_b", 31, k_thread_b, "I am thread_b");

    uint32_t fd = sys_open("/file1", O_RDWR);
    printf("open /file1, fd:%d\n", fd);
    char buf[64] = {0};
    int read_bytes = sys_read(fd, buf, 18);
    printf("1_ read %d bytes:\n%s\n", read_bytes, buf);

    memset(buf, 0, 64);
    read_bytes = sys_read(fd, buf, 6);
    printf("2_ read %d bytes:\n%s", read_bytes, buf);

    memset(buf, 0, 64);
    read_bytes = sys_read(fd, buf, 6);
    printf("3_ read %d bytes:\n%s", read_bytes, buf);

    printf("________  close file1 and reopen  ________\n");
    sys_close(fd);
    fd = sys_open("/file1", O_RDWR);
    memset(buf, 0, 64);
    read_bytes = sys_read(fd, buf, 24);
    printf("4_ read %d bytes:\n%s", read_bytes, buf);

    sys_close(fd);
    while(1);
    return 0;
}

实现文件读写指针定位功能

上次咱们读取 file1 时,由于己经读到了文件末尾, sys_read 返回了-1,为了从头读取文件,咱们还特意把文件关闭重新打开一次,这太麻烦了,我们必须提供不关闭文件就能自由设置读写位直的方法。
为了统一,咱们系统中的系统调用对应的内核实现,都是在系统调用名前加上 sys一前缀,因此我们本节要实现的就是 sys_lseek。

Iseek 原型是off_t lseek(int fd, off_t offset, int whence),fd 是文件描述符, offset 是偏移量, whence是 offset 的"参照物",函数功能是设置文件读写指针fd_pos 为参照物+偏移量的值,也就是说,文件指针具体的位置不仅取决于 offset,还取决于 whence。 其中 offset 是 typedef 自定义的类型,相当于 signed int,有符号整型,因此 offset 是可正可负的值。

whence 取值有三种取值。

  • SEEK_SET,offset 的参照物是文件开始处,也就是将读写位置指针设置为距文件开头偏移 offset 个字节处。
  • SEEK_CUR,offset 的参照物是当前读写位置,也就是将读写位置指针设置为当前位置+offset。
  • SEEK_END,offset 的参照物是文件尺寸大小,即文件最后一个字节的下一个字节,也就是将读写位置指针设置为文件尺寸+offset。

这里说一下 SEEK_END,也许您觉得 offset 的参照物应该是文件的最后一个字节,很遗憾这是"错觉"。因为这不是文件末尾,文件末尾是指文件最后一个字节的下一个字节处,即超出文件大小的第 1 个字节,这就是读完文件时很多函数都会返回 EOF (-1) 的原因,不属于文件范围了嘛。文件的读写位置指针是fd_pos,fd_pos 始终指向下一个可读写的位置,它是以 0 为起始的偏移量,因此文件末尾是指文件大小。

/* 文件读写位置偏移量 */
enum whence {
    SEEK_SET = 1,
    SEEK_CUR,
    SEEK_END
};

/* 重置用于文件读写操作的偏移指针,成功时返回新的偏移量,出错时返回-1 */
//文件的读写位置是由文件结构中的 fd_pos 决定的,因此 sys_lseek 的原理是将 whence+offset 转换为 fd_pos
int32_t sys_lseek(int32_t fd, int32_t offset, uint8_t whence) {
    if (fd < 0) {
        printk("sys_lseek: fd error\n");
        return -1;
    }
    ASSERT(whence > 0 && whence < 4);
    uint32_t _fd = fd_local2global(fd);
    struct file* pf = &file_table[_fd];//指向对应文件结构
    int32_t new_pos = 0;   //新的偏移量必须位于文件大小之内
    int32_t file_size = (int32_t)pf->fd_inode->i_size;
    switch (whence) {
            /* SEEK_SET 新的读写位置是相对于文件开头再增加offset个位移量 */
        case SEEK_SET:
            new_pos = offset;
            break;

            /* SEEK_CUR 新的读写位置是相对于当前的位置增加offset个位移量 */
        case SEEK_CUR:	// offse可正可负
            new_pos = (int32_t)pf->fd_pos + offset;
            break;

            /* SEEK_END 新的读写位置是相对于文件尺寸再增加offset个位移量 */
        case SEEK_END:	   // 此情况下,offset应该为负值
            new_pos = file_size + offset;
    }
    if (new_pos < 0 || new_pos > (file_size - 1)) {	 
        return -1;
    }
    pf->fd_pos = new_pos;
    return pf->fd_pos;
}
int main(void) {
    put_str("I am kernel\n");
    init_all();
    process_execute(u_prog_a, "u_prog_a");
    process_execute(u_prog_b, "u_prog_b");
    thread_start("k_thread_a", 31, k_thread_a, "I am thread_a");
    thread_start("k_thread_b", 31, k_thread_b, "I am thread_b");

    uint32_t fd = sys_open("/file1", O_RDWR);
    printf("open /file1, fd:%d\n", fd);
    char buf[64] = {0};
    int read_bytes = sys_read(fd, buf, 18);
    printf("1_ read %d bytes:\n%s\n", read_bytes, buf);

    memset(buf, 0, 64);
    read_bytes = sys_read(fd, buf, 6);
    printf("2_ read %d bytes:\n%s", read_bytes, buf);

    memset(buf, 0, 64);
    read_bytes = sys_read(fd, buf, 6);
    printf("3_ read %d bytes:\n%s", read_bytes, buf);

    printf("________  SEEK_SET 0  ________\n");
    sys_lseek(fd, 0, SEEK_SET);//读写位置指针置0
    memset(buf, 0, 64);
    read_bytes = sys_read(fd, buf, 24);
    printf("4_ read %d bytes:\n%s", read_bytes, buf);

    sys_close(fd);
    while(1);
    return 0;
}

实现文件删除功能

回收 inode

与 inode 相关的资源有:

  1. inode 位图
  2. inode_table
  3. inode 中 i_sectors[0 ~ 11] 中的直接块和一级间接索引块表 i_sectors[12] 中的间接块
  4. 一级间接索引块表本身的扇区地址

inode 的使用情况是由 inode 位图来控制的,从 inode 位图中把 inode 分配出去后,无论该 inode 中原来是否有数据,创建后一律会被新数据覆盖,因此在回收 inode 时只要在 inode 位图中的相应位置。就可以了,没必要在 inode_table 中真正擦除该 inode,就像删除文件时不需要真正把文件数据块中的数据擦除一样。

/* 将硬盘分区part上的inode清空 */
void inode_delete(struct partition* part, uint32_t inode_no, void* io_buf) {
    ASSERT(inode_no < 4096);
    struct inode_position inode_pos;
    inode_locate(part, inode_no, &inode_pos);     // inode位置信息会存入inode_pos
    ASSERT(inode_pos.sec_lba <= (part->start_lba + part->sec_cnt));

    char* inode_buf = (char*)io_buf;
    if (inode_pos.two_sec) {   // inode跨扇区,读入2个扇区
        /* 将原硬盘上的内容先读出来 */
        ide_read(part->my_disk, inode_pos.sec_lba, inode_buf, 2);
        /* 将inode_buf清0 */
        memset((inode_buf + inode_pos.off_size), 0, sizeof(struct inode));
        /* 用清0的内存数据覆盖磁盘 */
        ide_write(part->my_disk, inode_pos.sec_lba, inode_buf, 2);
    } else {    // 未跨扇区,只读入1个扇区就好
        /* 将原硬盘上的内容先读出来 */
        ide_read(part->my_disk, inode_pos.sec_lba, inode_buf, 1);
        /* 将inode_buf清0 */
        memset((inode_buf + inode_pos.off_size), 0, sizeof(struct inode));
        /* 用清0的内存数据覆盖磁盘 */
        ide_write(part->my_disk, inode_pos.sec_lba, inode_buf, 1);
    }
}

/* 回收inode的数据块和inode本身 */
void inode_release(struct partition* part, uint32_t inode_no) {
    struct inode* inode_to_del = inode_open(part, inode_no);
    ASSERT(inode_to_del->i_no == inode_no);

    /* 1 回收inode占用的所有块 */
    uint8_t block_idx = 0, block_cnt = 12;
    uint32_t block_bitmap_idx;
    uint32_t all_blocks[140] = {0};	  //12个直接块+128个间接块

    /* a 先将前12个直接块存入all_blocks */
    while (block_idx < 12) {
        all_blocks[block_idx] = inode_to_del->i_sectors[block_idx];
        block_idx++;
    }

    /* b 如果一级间接块表存在,将其128个间接块读到all_blocks[12~], 并释放一级间接块表所占的扇区 */
    if (inode_to_del->i_sectors[12] != 0) {
        ide_read(part->my_disk, inode_to_del->i_sectors[12], all_blocks + 12, 1);
        block_cnt = 140;

        /* 回收一级间接块表占用的扇区 */
        block_bitmap_idx = inode_to_del->i_sectors[12] - part->sb->data_start_lba;
        ASSERT(block_bitmap_idx > 0);
        bitmap_set(&part->block_bitmap, block_bitmap_idx, 0);
        bitmap_sync(cur_part, block_bitmap_idx, BLOCK_BITMAP);
    }

    /* c inode所有的块地址已经收集到all_blocks中,下面逐个回收 */
    block_idx = 0;
    while (block_idx < block_cnt) {
        if (all_blocks[block_idx] != 0) {
            block_bitmap_idx = 0;
            block_bitmap_idx = all_blocks[block_idx] - part->sb->data_start_lba;
            ASSERT(block_bitmap_idx > 0);
            bitmap_set(&part->block_bitmap, block_bitmap_idx, 0);//位图置0
            bitmap_sync(cur_part, block_bitmap_idx, BLOCK_BITMAP);//同步位图到硬盘
        }
        block_idx++; 
    }

    /*2 回收该inode所占用的inode */
    bitmap_set(&part->inode_bitmap, inode_no, 0);  
    bitmap_sync(cur_part, inode_no, INODE_BITMAP);

    /******     以下inode_delete是调试用的    ******
       * 此函数会在inode_table中将此inode清0,
       * 但实际上是不需要的,inode分配是由inode位图控制的,
       * 硬盘上的数据不需要清0,可以直接覆盖*/
    void* io_buf = sys_malloc(1024);
    inode_delete(part, inode_no, io_buf);
    sys_free(io_buf);
    /***********************************************/

    inode_close(inode_to_del);
}

删除目录项

文件名是以目录项的形式存在的,删除文件必须在目录中将其目录项擦除。下面看一下删除目录项相关的工作。

  1. 在文件所在的目录中擦除该文件的目录项,使其为 0 。
  2. 根目录是必须存在的,它是文件读写的根基,不应该被清空,它至少要保留 1 个块。如果目录项独占 1 个块,并且该块不是根目录最后一个块的话,将其回收。
  3. 目录 inode 的 i_size 是目录项大小的总和,因此还要将 i size 减去一个目录项的单位大小。
  4. 目录 inode 改变后,要同步到硬盘。
/* 把分区part目录pdir中编号为inode_no的目录项删除 */
bool delete_dir_entry(struct partition* part, struct dir* pdir, uint32_t inode_no, void* io_buf) {
    struct inode* dir_inode = pdir->inode;
    uint32_t block_idx = 0, all_blocks[140] = {0};
    
    /* 收集目录全部块地址 */
    while (block_idx < 12) {
        all_blocks[block_idx] = dir_inode->i_sectors[block_idx];
        block_idx++;
    }
    if (dir_inode->i_sectors[12]) {
        ide_read(part->my_disk, dir_inode->i_sectors[12], all_blocks + 12, 1);
    }

    /* 目录项在存储时保证不会跨扇区 */
    uint32_t dir_entry_size = part->sb->dir_entry_size;
    uint32_t dir_entrys_per_sec = (SECTOR_SIZE / dir_entry_size);       // 每扇区最大的目录项数目
    struct dir_entry* dir_e = (struct dir_entry*)io_buf;   
    struct dir_entry* dir_entry_found = NULL;
    uint8_t dir_entry_idx, dir_entry_cnt;
    bool is_dir_first_block = false;     // 当前块是否是目录的第1个块 

    /* 遍历所有块,寻找目录项 */
    block_idx = 0;
    while (block_idx < 140) {
        is_dir_first_block = false;
        if (all_blocks[block_idx] == 0) {
            block_idx++;
            continue;
        }
        dir_entry_idx = dir_entry_cnt = 0;
        memset(io_buf, 0, SECTOR_SIZE);
        /* 读取扇区,获得目录项 */
        ide_read(part->my_disk, all_blocks[block_idx], io_buf, 1);

        /* 遍历所有的目录项,统计该扇区的目录项数量及是否有待删除的目录项 */
        while (dir_entry_idx < dir_entrys_per_sec) {
            if ((dir_e + dir_entry_idx)->f_type != FT_UNKNOWN) {
                if (!strcmp((dir_e + dir_entry_idx)->filename, ".")) { 
                    is_dir_first_block = true;
                } else if (strcmp((dir_e + dir_entry_idx)->filename, ".") && 
                           strcmp((dir_e + dir_entry_idx)->filename, "..")) {
                    dir_entry_cnt++;     // 统计此扇区内的目录项个数,用来判断删除目录项后是否回收该扇区
                    if ((dir_e + dir_entry_idx)->i_no == inode_no) {	  // 如果找到此i结点,就将其记录在dir_entry_found
                        ASSERT(dir_entry_found == NULL);  // 确保目录中只有一个编号为inode_no的inode,找到一次后dir_entry_found就不再是NULL
                        dir_entry_found = dir_e + dir_entry_idx;
                        /* 找到后也继续遍历,统计总共的目录项数 */
                    }
                }
            }
            dir_entry_idx++;
        } 

        /* 若此扇区未找到该目录项,继续在下个扇区中找 */
        if (dir_entry_found == NULL) {
            block_idx++;
            continue;
        }

        /* 在此扇区中找到目录项后,清除该目录项并判断是否回收扇区,随后退出循环直接返回 */
        ASSERT(dir_entry_cnt >= 1);
        /* 除目录第1个扇区外,若该扇区上只有该目录项自己,则将整个扇区回收 */
        if (dir_entry_cnt == 1 && !is_dir_first_block) {
            /* a 在块位图中回收该块 */
            uint32_t block_bitmap_idx = all_blocks[block_idx] - part->sb->data_start_lba;
            bitmap_set(&part->block_bitmap, block_bitmap_idx, 0);
            bitmap_sync(cur_part, block_bitmap_idx, BLOCK_BITMAP);

            /* b 将块地址从数组i_sectors或索引表中去掉 */
            if (block_idx < 12) {
                dir_inode->i_sectors[block_idx] = 0;
            } else {    // 在一级间接索引表中擦除该间接块地址
                /*先判断一级间接索引表中间接块的数量,如果仅有这1个间接块,连同间接索引表所在的块一同回收 */
                uint32_t indirect_blocks = 0;
                uint32_t indirect_block_idx = 12;
                while (indirect_block_idx < 140) {
                    if (all_blocks[indirect_block_idx] != 0) {
                        indirect_blocks++;
                    }
                }
                ASSERT(indirect_blocks >= 1);  // 包括当前间接块

                if (indirect_blocks > 1) {	  // 间接索引表中还包括其它间接块,仅在索引表中擦除当前这个间接块地址
                    all_blocks[block_idx] = 0; 
                    ide_write(part->my_disk, dir_inode->i_sectors[12], all_blocks + 12, 1); 
                } else {	// 间接索引表中就当前这1个间接块,直接把间接索引表所在的块回收,然后擦除间接索引表块地址
                    /* 回收间接索引表所在的块 */
                    block_bitmap_idx = dir_inode->i_sectors[12] - part->sb->data_start_lba;
                    bitmap_set(&part->block_bitmap, block_bitmap_idx, 0);
                    bitmap_sync(cur_part, block_bitmap_idx, BLOCK_BITMAP);

                    /* 将间接索引表地址清0 */
                    dir_inode->i_sectors[12] = 0;
                }
            }
        } else { // 仅将该目录项清空
            memset(dir_entry_found, 0, dir_entry_size);
            ide_write(part->my_disk, all_blocks[block_idx], io_buf, 1);
        }

        /* 更新i结点信息并同步到硬盘 */
        ASSERT(dir_inode->i_size >= dir_entry_size);
        dir_inode->i_size -= dir_entry_size;
        memset(io_buf, 0, SECTOR_SIZE * 2);
        inode_sync(part, dir_inode, io_buf);

        return true;
    }
    /* 所有块中未找到则返回false,若出现这种情况应该是serarch_file出错了 */
    return false;
}

实现 sys_unlink 与功能验证

/* 删除文件(非目录),成功返回0,失败返回-1 */
int32_t sys_unlink(const char* pathname) {
    ASSERT(strlen(pathname) < MAX_PATH_LEN);

    /* 先检查待删除的文件是否存在 */
    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);
    if (inode_no == -1) {
        printk("file %s not found!\n", pathname);
        dir_close(searched_record.parent_dir);
        return -1;
    }
    if (searched_record.file_type == FT_DIRECTORY) {
        printk("can`t delete a direcotry with unlink(), use rmdir() to instead\n");
        dir_close(searched_record.parent_dir);
        return -1;
    }

    /* 检查是否在已打开文件列表(文件表)中 */
    uint32_t file_idx = 0;
    while (file_idx < MAX_FILE_OPEN) {
        if (file_table[file_idx].fd_inode != NULL && (uint32_t)inode_no == file_table[file_idx].fd_inode->i_no) {
            break;
        }
        file_idx++;
    }
    if (file_idx < MAX_FILE_OPEN) {
        dir_close(searched_record.parent_dir);
        printk("file %s is in use, not allow to delete!\n", pathname);
        return -1;
    }
    ASSERT(file_idx == MAX_FILE_OPEN);

    /* 为delete_dir_entry申请缓冲区 */
    void* io_buf = sys_malloc(SECTOR_SIZE + SECTOR_SIZE);
    if (io_buf == NULL) {
        dir_close(searched_record.parent_dir);
        printk("sys_unlink: malloc for io_buf failed\n");
        return -1;
    }

    struct dir* parent_dir = searched_record.parent_dir;  
    delete_dir_entry(cur_part, parent_dir, inode_no, io_buf);
    inode_release(cur_part, inode_no);
    sys_free(io_buf);
    dir_close(searched_record.parent_dir);
    return 0;   // 成功删除文件 
}
int main(void) {
   put_str("I am kernel\n");
   init_all();
   process_execute(u_prog_a, "u_prog_a");
   process_execute(u_prog_b, "u_prog_b");
   thread_start("k_thread_a", 31, k_thread_a, "I am thread_a");
   thread_start("k_thread_b", 31, k_thread_b, "I am thread_b");
   printf("/file1 delete %s!\n", sys_unlink("/file1") == 0 ? "done" : "fail");
   while(1);
   return 0;
}

创建目录

实现 sys_mkdir 创建目录

建目录所涉及的工作包括。

  1. 确认待创建的新目录在文件系统上不存在。
  2. 为新目录创建 inode 。
  3. 为新目录分配 1 个块存储该目录中的目录项。
  4. 在新目录中创建两个目录项 “.” 和 “…” 这是每个目录都必须存在的两个目录项 。
  5. 在新目录的父目录中添加新目录的目录项。
  6. 将以上资源的变更同步到硬盘。
/* 创建目录pathname,成功返回0,失败返回-1 */
int32_t sys_mkdir(const char* pathname) {
    uint8_t rollback_step = 0;	       // 用于操作失败时回滚各资源状态
    void* io_buf = sys_malloc(SECTOR_SIZE * 2);
    if (io_buf == NULL) {
        printk("sys_mkdir: sys_malloc for io_buf failed\n");
        return -1;
    }

    struct path_search_record searched_record;
    memset(&searched_record, 0, sizeof(struct path_search_record));
    int inode_no = -1;
    inode_no = search_file(pathname, &searched_record);
    if (inode_no != -1) {      // 如果找到了同名目录或文件,失败返回
        printk("sys_mkdir: file or directory %s exist!\n", pathname);
        rollback_step = 1;
        goto rollback;
    } else {	     // 若未找到,也要判断是在最终目录没找到还是某个中间目录不存在
        uint32_t pathname_depth = path_depth_cnt((char*)pathname);
        uint32_t path_searched_depth = path_depth_cnt(searched_record.searched_path);
        /* 先判断是否把pathname的各层目录都访问到了,即是否在某个中间目录就失败了 */
        if (pathname_depth != path_searched_depth){ // 说明并没有访问到全部的路径,某个中间目录是不存在的
            printk("sys_mkdir: can`t access %s, subpath %s is`t exist\n", pathname, searched_record.searched_path);
            rollback_step = 1;
            goto rollback;
        }
    }

    struct dir* parent_dir = searched_record.parent_dir;
    /* 目录名称后可能会有字符'/',所以最好直接用searched_record.searched_path,无'/' */
    char* dirname = strrchr(searched_record.searched_path, '/') + 1;

    inode_no = inode_bitmap_alloc(cur_part); 
    if (inode_no == -1) {
        printk("sys_mkdir: allocate inode failed\n");
        rollback_step = 1;
        goto rollback;
    }

    struct inode new_dir_inode;
    inode_init(inode_no, &new_dir_inode);	    // 初始化i结点

    uint32_t block_bitmap_idx = 0;     // 用来记录block对应于block_bitmap中的索引
    int32_t block_lba = -1;
    /* 为目录分配一个块,用来写入目录.和.. */
    block_lba = block_bitmap_alloc(cur_part);
    if (block_lba == -1) {
        printk("sys_mkdir: block_bitmap_alloc for create directory failed\n");
        rollback_step = 2;
        goto rollback;
    }
    new_dir_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);

    /* 将当前目录的目录项'.'和'..'写入目录 */
    memset(io_buf, 0, SECTOR_SIZE * 2);	 // 清空io_buf
    struct dir_entry* p_de = (struct dir_entry*)io_buf;

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

    p_de++;
    /* 初始化当前目录".." */
    memcpy(p_de->filename, "..", 2);
    p_de->i_no = parent_dir->inode->i_no;
    p_de->f_type = FT_DIRECTORY;
    ide_write(cur_part->my_disk, new_dir_inode.i_sectors[0], io_buf, 1);

    new_dir_inode.i_size = 2 * cur_part->sb->dir_entry_size;

    /* 在父目录中添加自己的目录项 */
    struct dir_entry new_dir_entry;
    memset(&new_dir_entry, 0, sizeof(struct dir_entry));
    create_dir_entry(dirname, inode_no, FT_DIRECTORY, &new_dir_entry);
    memset(io_buf, 0, SECTOR_SIZE * 2);	 // 清空io_buf
    if (!sync_dir_entry(parent_dir, &new_dir_entry, io_buf)) {	  // sync_dir_entry中将block_bitmap通过bitmap_sync同步到硬盘
        printk("sys_mkdir: sync_dir_entry to disk failed!\n");
        rollback_step = 2;
        goto rollback;
    }

    /* 父目录的inode同步到硬盘 */
    memset(io_buf, 0, SECTOR_SIZE * 2);
    inode_sync(cur_part, parent_dir->inode, io_buf);

    /* 将新创建目录的inode同步到硬盘 */
    memset(io_buf, 0, SECTOR_SIZE * 2);
    inode_sync(cur_part, &new_dir_inode, io_buf);

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

    sys_free(io_buf);

    /* 关闭所创建目录的父目录 */
    dir_close(searched_record.parent_dir);
    return 0;

    /*创建文件或目录需要创建相关的多个资源,若某步失败则会执行到下面的回滚步骤 */
    rollback:	     // 因为某步骤操作失败而回滚
    switch (rollback_step) {
        case 2:
            bitmap_set(&cur_part->inode_bitmap, inode_no, 0);	 // 如果新文件的inode创建失败,之前位图中分配的inode_no也要恢复 
        case 1:
            /* 关闭所创建目录的父目录 */
            dir_close(searched_record.parent_dir);
            break;
    }
    sys_free(io_buf);
    return -1;
}

创建目录也是由多个步骤完成的,因此创建目录的工作是个事务,具有原子性,即要么所有步骤都完成,要么一个都不做,若其中某个步骤失败,必须将之前完成的操作回漆到之前的状态。在函数开头定义的rollback_step 用于记录回滚的步骤。接下来早早申请了 2 扇区大小的缓冲区给 io_buf,后面很多操作都要用到它。

创建目录功能验证

int main(void) {
    put_str("I am kernel\n");
    init_all();
    process_execute(u_prog_a, "u_prog_a");
    process_execute(u_prog_b, "u_prog_b");
    thread_start("k_thread_a", 31, k_thread_a, "I am thread_a");
    thread_start("k_thread_b", 31, k_thread_b, "I am thread_b");
    printf("/dir1/subdir1 create %s!\n", sys_mkdir("/dir1/subdir1") == 0 ? "done" : "fail");
    printf("/dir1 create %s!\n", sys_mkdir("/dir1") == 0 ? "done" : "fail");
    printf("now, /dir1/subdir1 create %s!\n", sys_mkdir("/dir1/subdir1") == 0 ? "done" : "fail");
    int fd = sys_open("/dir1/subdir1/file2", O_CREAT|O_RDWR);
    if (fd != -1) {
        printf("/dir1/subdir1/file2 create done!\n");
        sys_write(fd, "Catch me if you can!\n", 21);
        sys_lseek(fd, 0, SEEK_SET);
        char buf[32] = {0};
        sys_read(fd, buf, 21); 
        printf("/dir1/subdir1/file2 says:\n%s", buf);
        sys_close(fd);
    }
    while(1);
    return 0;
}

遍历目录

打开目录和关闭目录

/* 目录打开成功后返回目录指针,失败返回NULL */
struct dir* sys_opendir(const char* name) {
    ASSERT(strlen(name) < MAX_PATH_LEN);
    /* 如果是根目录'/',直接返回&root_dir */
    if (name[0] == '/' && (name[1] == 0 || name[0] == '.')) {
        return &root_dir;
    }

    /* 先检查待打开的目录是否存在 */
    struct path_search_record searched_record;
    memset(&searched_record, 0, sizeof(struct path_search_record));
    int inode_no = search_file(name, &searched_record);
    struct dir* ret = NULL;
    if (inode_no == -1) {	 // 如果找不到目录,提示不存在的路径 
        printk("In %s, sub path %s not exist\n", name, searched_record.searched_path); 
    } else {
        if (searched_record.file_type == FT_REGULAR) {
            printk("%s is regular file!\n", name);
        } else if (searched_record.file_type == FT_DIRECTORY) {
            ret = dir_open(cur_part, inode_no);
        }
    }
    dir_close(searched_record.parent_dir);
    return ret;
}

/* 成功关闭目录dir返回0,失败返回-1 */
int32_t sys_closedir(struct dir* dir) {
    int32_t ret = -1;
    if (dir != NULL) {
        dir_close(dir);
        ret = 0;
    }
    return ret;
}
int main(void) {
    put_str("I am kernel\n");
    init_all();
    process_execute(u_prog_a, "u_prog_a");
    process_execute(u_prog_b, "u_prog_b");
    thread_start("k_thread_a", 31, k_thread_a, "I am thread_a");
    thread_start("k_thread_b", 31, k_thread_b, "I am thread_b");
    struct dir* p_dir = sys_opendir("/dir1/subdir1");
    if (p_dir) {
        printf("/dir1/subdir1 open done!\n");
        if (sys_closedir(p_dir) == 0) {
            printf("/dir1/subdir1 close done!\n");
        } else {
            printf("/dir1/subdir1 close fail!\n");
        }
    } else {
        printf("/dir1/subdir1 open fail!\n");
    }
    while(1);
    return 0;
}

读取一个目录项

读取目录实际上就是读取目录中的目录项。

在Linux中,readdir 每次返回目录的一个目录项地址,因此遍历目录需要循环调用 readdir。

/* 读取目录,成功返回1个目录项,失败返回NULL */
struct dir_entry* dir_read(struct dir* dir) {
    struct dir_entry* dir_e = (struct dir_entry*)dir->dir_buf;
    struct inode* dir_inode = dir->inode; 
    uint32_t all_blocks[140] = {0}, block_cnt = 12;
    uint32_t block_idx = 0, dir_entry_idx = 0;
    while (block_idx < 12) {
        all_blocks[block_idx] = dir_inode->i_sectors[block_idx];
        block_idx++;
    }
    if (dir_inode->i_sectors[12] != 0) {	     // 若含有一级间接块表
        ide_read(cur_part->my_disk, dir_inode->i_sectors[12], all_blocks + 12, 1);
        block_cnt = 140;
    }
    block_idx = 0;

    uint32_t cur_dir_entry_pos = 0;	  // 当前目录项的偏移,此项用来判断是否是之前已经返回过的目录项
    uint32_t dir_entry_size = cur_part->sb->dir_entry_size;
    uint32_t dir_entrys_per_sec = SECTOR_SIZE / dir_entry_size;	 // 1扇区内可容纳的目录项个数
    /* 因为此目录内可能删除了某些文件或子目录,所以要遍历所有块 */
    while (block_idx < block_cnt) {
        if (dir->dir_pos >= dir_inode->i_size) {
            return NULL;
        }
        if (all_blocks[block_idx] == 0) {     // 如果此块地址为0,即空块,继续读出下一块
            block_idx++;
            continue;
        }
        memset(dir_e, 0, SECTOR_SIZE);
        ide_read(cur_part->my_disk, all_blocks[block_idx], dir_e, 1);
        dir_entry_idx = 0;
        /* 遍历扇区内所有目录项 */
        while (dir_entry_idx < dir_entrys_per_sec) {
            if ((dir_e + dir_entry_idx)->f_type) {	 // 如果f_type不等于0,即不等于FT_UNKNOWN
                /* 判断是不是最新的目录项,避免返回曾经已经返回过的目录项 */
                if (cur_dir_entry_pos < dir->dir_pos) {
                    cur_dir_entry_pos += dir_entry_size;
                    dir_entry_idx++;
                    continue;
                }
                ASSERT(cur_dir_entry_pos == dir->dir_pos);
                dir->dir_pos += dir_entry_size;	      // 更新为新位置,即下一个返回的目录项地址
                return dir_e + dir_entry_idx; 
            }
            dir_entry_idx++;
        }
        block_idx++;
    }
    return NULL;
}

实现 sys_readdir 及 sys_rewinddir

遍历目录的操作中,经常会用到目录回绕的功能,也就是使目录的“游标” dir_pos 回到 0,它与 lseek 功能类似,只不过是针对目录的,避免了将目录先关闭再重新打开的繁琐。在 Linux 中目录回绕是用函数 rewinddir 实现的,其原型是:void rewinddin(DIR *dirp),我们也按照此形式实现。

/* 读取目录dir的1个目录项,成功后返回其目录项地址,到目录尾时或出错时返回NULL */
struct dir_entry* sys_readdir(struct dir* dir) {
   ASSERT(dir != NULL);
   return dir_read(dir);
}

/* 把目录dir的指针dir_pos置0 */
void sys_rewinddir(struct dir* dir) {
   dir->dir_pos = 0;
}
int main(void) {
    put_str("I am kernel\n");
    init_all();
    /********  测试代码  ********/
    struct dir* p_dir = sys_opendir("/dir1/subdir1");
    if (p_dir) {
        printf("/dir1/subdir1 open done!\ncontent:\n");
        char* type = NULL;
        struct dir_entry* dir_e = NULL;
        while((dir_e = sys_readdir(p_dir))) { 
            if (dir_e->f_type == FT_REGULAR) {
                type = "regular";
            } else {
                type = "directory";
            }
            printf("      %s   %s\n", type, dir_e->filename);
        }
        if (sys_closedir(p_dir) == 0) {
            printf("/dir1/subdir1 close done!\n");
        } else {
            printf("/dir1/subdir1 close fail!\n");
        }
    } else {
        printf("/dir1/subdir1 open fail!\n");
    }
    /********  测试代码  ********/
    while(1);
    return 0;
}

删除目录

删除目录与判断空目录

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

/* 判断目录是否为空 */
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 */
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排查i_sectors[1~12],保证child_dir是空目录
      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;
}
int main(void) {
   put_str("I am kernel\n");
   init_all();
/********  测试代码  ********/
   printf("/dir1 content before delete /dir1/subdir1:\n");
   struct dir* dir = sys_opendir("/dir1/");
   char* type = NULL;
   struct dir_entry* dir_e = NULL;
   while((dir_e = sys_readdir(dir))) { 
      if (dir_e->f_type == FT_REGULAR) {
	 type = "regular";
      } else {
	 type = "directory";
      }
      printf("      %s   %s\n", type, dir_e->filename);
   }
   printf("try to delete nonempty directory /dir1/subdir1\n");
   if (sys_rmdir("/dir1/subdir1") == -1) {
      printf("sys_rmdir: /dir1/subdir1 delete fail!\n");
   }

   printf("try to delete /dir1/subdir1/file2\n");
   if (sys_rmdir("/dir1/subdir1/file2") == -1) {
      printf("sys_rmdir: /dir1/subdir1/file2 delete fail!\n");
   } 
   if (sys_unlink("/dir1/subdir1/file2") == 0 ) {
      printf("sys_unlink: /dir1/subdir1/file2 delete done\n");
   }
   
   printf("try to delete directory /dir1/subdir1 again\n");
   if (sys_rmdir("/dir1/subdir1") == 0) {
      printf("/dir1/subdir1 delete done!\n");
   }

   printf("/dir1 content after delete /dir1/subdir1:\n");
   sys_rewinddir(dir);
   while((dir_e = sys_readdir(dir))) { 
      if (dir_e->f_type == FT_REGULAR) {
	 type = "regular";
      } else {
	 type = "directory";
      }
      printf("      %s   %s\n", type, dir_e->filename);
   }

/********  测试代码  ********/
   while(1);
   return 0;
}

任务的工作目录

显示当前工作目录的原理及基础代码

在任何目录中都有目录项"…“,它表示父目录。也就是说,有了”…“,无论我们身处任何一级的子目录,都可以“顺藤摸瓜”找到根目录。因此咱们具体的做法是先通过”…"获取当前目录的父目录,在父目录中搜索当前目录的目录项,从目录项中获取当前目录名称,然后再向上找父目录的父目录,再从中获得父目录的名称……沿着目录树层层而上,就能构建出当前目录的绝对路径。

/* 获得父目录的inode编号 */
static uint32_t get_parent_dir_inode_nr(uint32_t child_inode_nr, void* io_buf) {
    struct inode* child_dir_inode = inode_open(cur_part, child_inode_nr);
    /* 目录中的目录项".."中包括父目录inode编号,".."位于目录的第0块 */
    uint32_t block_lba = child_dir_inode->i_sectors[0];
    ASSERT(block_lba >= cur_part->sb->data_start_lba);
    inode_close(child_dir_inode);
    ide_read(cur_part->my_disk, block_lba, io_buf, 1);   
    struct dir_entry* dir_e = (struct dir_entry*)io_buf;
    /* 第0个目录项是".",第1个目录项是".." */
    ASSERT(dir_e[1].i_no < 4096 && dir_e[1].f_type == FT_DIRECTORY);
    return dir_e[1].i_no;      // 返回..即父目录的inode编号
}

/* 在inode编号为p_inode_nr的目录中查找inode编号为c_inode_nr的子目录的名字,
 * 将名字存入缓冲区path.成功返回0,失败返-1 */
static int get_child_dir_name(uint32_t p_inode_nr, uint32_t c_inode_nr, char* path, void* io_buf) {
    struct inode* parent_dir_inode = inode_open(cur_part, p_inode_nr);
    /* 填充all_blocks,将该目录的所占扇区地址全部写入all_blocks */
    uint8_t block_idx = 0;
    uint32_t all_blocks[140] = {0}, block_cnt = 12;
    while (block_idx < 12) {
        all_blocks[block_idx] = parent_dir_inode->i_sectors[block_idx];
        block_idx++;
    }
    if (parent_dir_inode->i_sectors[12]) {	// 若包含了一级间接块表,将共读入all_blocks.
        ide_read(cur_part->my_disk, parent_dir_inode->i_sectors[12], all_blocks + 12, 1);
        block_cnt = 140;
    }
    inode_close(parent_dir_inode);

    struct dir_entry* dir_e = (struct dir_entry*)io_buf;
    uint32_t dir_entry_size = cur_part->sb->dir_entry_size;
    uint32_t dir_entrys_per_sec = (512 / dir_entry_size);
    block_idx = 0;
    /* 遍历所有块 */
    while(block_idx < block_cnt) {
        if(all_blocks[block_idx]) {      // 如果相应块不为空则读入相应块
            ide_read(cur_part->my_disk, all_blocks[block_idx], io_buf, 1);
            uint8_t dir_e_idx = 0;
            /* 遍历每个目录项 */
            while(dir_e_idx < dir_entrys_per_sec) {
                if ((dir_e + dir_e_idx)->i_no == c_inode_nr) {
                    strcat(path, "/");
                    strcat(path, (dir_e + dir_e_idx)->filename);
                    return 0;
                }
                dir_e_idx++;
            }
        }
        block_idx++;
    }
    return -1;
}

实现 sys_getcwd

/* 进程或线程的pcb,程序控制块 */
struct task_struct {
   ...
   uint32_t cwd_inode_nr;	 // 进程所在的工作目录的inode编号
   uint32_t stack_magic;	 // 用这串数字做栈的边界标记,用于检测栈的溢出
};

/* 初始化线程基本信息 */
void init_thread(struct task_struct* pthread, char* name, int prio) {
   ...
   pthread->cwd_inode_nr = 0;	    // 以根目录做为默认工作路径
   pthread->stack_magic = 0x19870916;	  // 自定义的魔数
}
/* 把当前工作目录绝对路径写入buf, size是buf的大小. 
 当buf为NULL时,由操作系统分配存储工作路径的空间并返回地址
 失败则返回NULL */
char* sys_getcwd(char* buf, uint32_t size) {
    /* 确保buf不为空,若用户进程提供的buf为NULL,
   系统调用getcwd中要为用户进程通过malloc分配内存 */
    ASSERT(buf != NULL);
    void* io_buf = sys_malloc(SECTOR_SIZE);
    if (io_buf == NULL) {
        return NULL;
    }

    struct task_struct* cur_thread = running_thread();
    int32_t parent_inode_nr = 0;
    int32_t child_inode_nr = cur_thread->cwd_inode_nr;
    ASSERT(child_inode_nr >= 0 && child_inode_nr < 4096);      // 最大支持4096个inode
    /* 若当前目录是根目录,直接返回'/' */
    if (child_inode_nr == 0) {
        buf[0] = '/';
        buf[1] = 0;
        return buf;
    }

    memset(buf, 0, size);
    char full_path_reverse[MAX_PATH_LEN] = {0};	  // 用来做全路径缓冲区

    /* 从下往上逐层找父目录,直到找到根目录为止.
    * 当child_inode_nr为根目录的inode编号(0)时停止,
    * 即已经查看完根目录中的目录项 */
    while ((child_inode_nr)) {
        parent_inode_nr = get_parent_dir_inode_nr(child_inode_nr, io_buf);
        if (get_child_dir_name(parent_inode_nr, child_inode_nr, full_path_reverse, io_buf) == -1) {	  // 或未找到名字,失败退出
            sys_free(io_buf);
            return NULL;
        }
        child_inode_nr = parent_inode_nr;
    }
    ASSERT(strlen(full_path_reverse) <= size);
    /* 至此full_path_reverse中的路径是反着的,
     * 即子目录在前(左),父目录在后(右) ,
     * 现将full_path_reverse中的路径反置 */
    char* last_slash;	// 用于记录字符串中最后一个斜杠地址
    while ((last_slash = strrchr(full_path_reverse, '/'))) {
        uint16_t len = strlen(buf);
        strcpy(buf + len, last_slash);
        /* 在full_path_reverse中添加结束字符,做为下一次执行strcpy中last_slash的边界 */
        *last_slash = 0;
    }
    sys_free(io_buf);
    return buf;
}

实现 sys_chdir 改变工作目录

/* 更改当前工作目录为绝对路径path,成功则返回0,失败返回-1 */
int32_t sys_chdir(const char* path) {
    int32_t ret = -1;
    struct path_search_record searched_record;  
    memset(&searched_record, 0, sizeof(struct path_search_record));
    int inode_no = search_file(path, &searched_record);
    if (inode_no != -1) {
        if (searched_record.file_type == FT_DIRECTORY) {
            running_thread()->cwd_inode_nr = inode_no;//修改当前目录的inode
            ret = 0;
        } else {
            printk("sys_chdir: %s is regular file or other!\n", path);
        }
    }
    dir_close(searched_record.parent_dir); 
    return ret;
}
int main(void) {
    put_str("I am kernel\n");
    init_all();
    /********  测试代码  ********/
    char cwd_buf[32] = {0};
    sys_getcwd(cwd_buf, 32);
    printf("cwd:%s\n", cwd_buf);
    sys_chdir("/dir1");
    printf("change cwd now\n");
    sys_getcwd(cwd_buf, 32);
    printf("cwd:%s\n", cwd_buf);
    /********  测试代码  ********/
    while(1);
    return 0;
}

获得文件属性

要想在用户环境下获得文件属性,需要提供类 stat 的系统调用,

实现 sys_stat

/* 文件属性结构体 */
struct stat {
    uint32_t st_ino;		 // inode编号
    uint32_t st_size;		 // 尺寸
    enum file_types st_filetype;	 // 文件类型
};

/* 在buf中填充文件结构相关信息,成功时返回0,失败返回-1 */
int32_t sys_stat(const char* path, struct stat* buf) {
    /* 若直接查看根目录'/' */
    if (!strcmp(path, "/") || !strcmp(path, "/.") || !strcmp(path, "/..")) {
        buf->st_filetype = FT_DIRECTORY;
        buf->st_ino = 0;
        buf->st_size = root_dir.inode->i_size;
        return 0;
    }

    int32_t ret = -1;	// 默认返回值
    struct path_search_record searched_record;
    memset(&searched_record, 0, sizeof(struct path_search_record));   // 记得初始化或清0,否则栈中信息不知道是什么
    int inode_no = search_file(path, &searched_record);
    if (inode_no != -1) {
        struct inode* obj_inode = inode_open(cur_part, inode_no);   // 只为获得文件大小
        buf->st_size = obj_inode->i_size;
        inode_close(obj_inode);
        buf->st_filetype = searched_record.file_type;
        buf->st_ino = inode_no;
        ret = 0;
    } else {
        printk("sys_stat: %s not found\n", path);
    }
    dir_close(searched_record.parent_dir);
    return ret;
}
int main(void) {
    put_str("I am kernel\n");
    init_all();
    /********  测试代码  ********/
    struct stat obj_stat;
    sys_stat("/", &obj_stat);
    printf("/`s info\n   i_no:%d\n   size:%d\n   filetype:%s\n", \
           obj_stat.st_ino, obj_stat.st_size, \
           obj_stat.st_filetype == 2 ? "directory" : "regular");
    sys_stat("/dir1", &obj_stat);
    printf("/dir1`s info\n   i_no:%d\n   size:%d\n   filetype:%s\n", \
           obj_stat.st_ino, obj_stat.st_size, \
           obj_stat.st_filetype == 2 ? "directory" : "regular");
    /********  测试代码  ********/
    while(1);
    return 0;
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/910175.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

交叉熵--损失函数

目录 交叉熵&#xff08;Cross Entropy&#xff09; 【预备知识】 【信息量】 【信息熵】 【相对熵】 【交叉熵】 交叉熵&#xff08;Cross Entropy&#xff09; 是Shannon信息论中一个重要概念&#xff0c; 主要用于度量两个概率分布间的差异性信息。 语言模型的性能…

FlashAttention算法详解

这篇文章的目的是详细的解释Flash Attention&#xff0c;为什么要解释FlashAttention呢&#xff1f;因为FlashAttention 是一种重新排序注意力计算的算法&#xff0c;它无需任何近似即可加速注意力计算并减少内存占用。所以作为目前LLM的模型加速它是一个非常好的解决方案&…

ThreadLocal深度解析

简介 在并发编程中&#xff0c;导致并发bug的问题都会归结于对共享变量的操作不当。多个线程同时读写同一共享变量存在并发问题&#xff0c;我们可以利用写时复制、不变性来突破对原数据的写操作&#xff0c;没有写就没有并发问题&#xff0c;而本篇文章所介绍的技术是突破共享…

基于swing的销售管理系统java仓库库存信息jsp源代码mysql

本项目为前几天收费帮学妹做的一个项目&#xff0c;Java EE JSP项目&#xff0c;在工作环境中基本使用不到&#xff0c;但是很多学校把这个当作编程入门的项目来做&#xff0c;故分享出本项目供初学者参考。 一、项目描述 基于swing的销售管理系统 系统有1权限&#xff1a;管…

spring依赖注入详解(上)

一、Bean销毁的过程 如果bean销毁时会执行的场景 1、设置DestroyMethodName为(inferred) // 先把DestroyMethodName设置为(inferred) Component public class TestBeanPostProcessor implements MergedBeanDefinitionPostProcessor {Overridepublic void postProcessMergedBe…

Hive(一)

一、DDL 1、数据库操作 1&#xff09;、创建数据库 语法&#xff1a; CREATE DATABASE [IF NOT EXISTS] database_name [COMMENT database_comment] [LOCATION hdfs_path] [WITH DBPROPERTIES (property_nameproperty_value, ...)]; 案例&#xff1a; &#xff08;1&…

vue element-ui 菜单管理使用图标选择器组件

目录 &#x1f31f;前言&#x1f31f;安装&#x1f31f;main.js配置&#x1f31f;页面使用&#x1f31f;效果展示 &#x1f31f;前言 哈喽小伙伴们&#xff0c;本文为大家介绍一下 VueElementUI 中图标选择器组件的使用方法&#xff1b;一起来看下吧。 &#x1f31f;安装 np…

RN 尝鲜之旅

React Native 一直没使用过&#xff0c;闲来无事&#xff0c;还是尝鲜了一下下。 目前还没有出新手村&#xff0c;所以写的东西不一定具有任何参考价值&#xff0c;见谅。 关于 RN 的一些说明 RN 与 R RN 与 R 的区别&#xff1a;来自掘金的一篇文章 RN 与 R 不一样&#xf…

报道 | 9月国际运筹优化会议汇总

封面图来源&#xff1a; https://www.pexels.com/zh-cn/photo/1181406/ 九月召开会议汇总&#xff1a; The 96th meeting of the EURO Working Group on Multiple Criteria Decision Aiding (EWG-MCDA) Location: Paris, France Important dates: Conference: September 202…

嵌入式系统常用的开发板

今天&#xff0c;了解一下常用的开发板&#xff0c;像stm32是最近了解的&#xff0c;esp8266系列是之前大三下上物联网导论课程时候接触的&#xff0c;一些树莓派&#xff0c;Arduino听说过&#xff0c;但了解不多。

MySQL索引常见术语(索引下推、索引覆盖、最左匹配等)

一:背景 我们在面试中都知道,对于MySQL索引是必问的。大家也应该都知道MySQL的数据结构,什么是索引。其中在面试中,面试官也经常问,你做过哪些优化?本文主要是介绍MySQL索引的一些常见术语,比如索引下推、索引覆盖、最左匹配等,这些其实也是MySQL优化的一部分,能够熟练…

谷歌浏览器推出全新功能:可自动检测恶意软件!

近日&#xff0c;谷歌正在测试 Chrome 浏览器的一项新功能。该功能可在已安装的扩展程序从 Chrome 网上商城删除时向用户发出恶意软件提示警告。 在Chrome 应用商店里一直有人源源不断的发布浏览器扩展程序&#xff0c;有很多都会通过弹出式广告和重定向广告进行推广。 这些扩…

社交工程和钓鱼攻击防范: 分析针对人类心理和社交工程的攻击技术,并介绍预防这些攻击的方法

第一章&#xff1a;引言 随着科技的不断进步&#xff0c;网络安全问题愈发凸显。在这个数字化时代&#xff0c;社交工程和钓鱼攻击成为黑客们获取敏感信息的常用手段。这些攻击不是基于技术漏洞&#xff0c;而是利用人类心理弱点来进行。本文将深入探讨社交工程和钓鱼攻击的原…

一个改进型的差分运放分析

在使用单个集成运放构成的加减运算电路时&#xff0c;存在两个缺点&#xff1a;一是电阻的选取和调整不方便&#xff0c;而是对于每个信号源的输入电阻均较小&#xff08;即相对于信号源内阻&#xff0c;电路的输入阻抗较小&#xff09;。 因此可以采用下图的两级电路实现差分比…

Obsidian 入门使用手册

文章目录 一、Obsidian 入门1.1 什么是 Obsidian1.2 安装 Obsidian 二、Obsidian 配置2.1 创建第一个笔记2.2 设置界面语言使用中文2.3 主题 三、小结 一、Obsidian 入门 1.1 什么是 Obsidian Obsidian 是一款基于 Markdown 语法编辑的笔记软件。与传统的 Markdown 软件不同的…

图神经网络与分子表征:1. 分子图和图神经网络基础

CSDN的朋友们大家好&#xff0c;好久没写系列文章了。 近期读了很多图神经网络&#xff08;GNN&#xff09;和分子表征&#xff08;molecular representation&#xff09;的论文&#xff0c;正好最近不是很忙&#xff0c;所以我决定把自己的学习过程记录下来&#xff0c;与大家…

Python 在logging.config.dictConfig()日志配置方式下,使用自定义的Handler处理程序

文章目录 一、基于 RotatingFileHandler 的自定义处理程序二、基于 TimedRotatingFileHandler 的自定义处理程序 Python logging模块的基本使用、进阶使用详解 Python logging.handlers模块&#xff0c;RotatingFileHandler、TimedRotatingFileHandler 处理器各参数详细介绍 …

pandas连接查询

df1数据如下 df2数据如下 连接查询代码 -1 import pandas as pddf1 pd.DataFrame({id:[1001,1002,1003,1004],name:[Hu,Dotu,Evp,Swe]}) df2 pd.DataFrame({id:[1001,1001,1003,1004, 1003],course:[c1,c2,c3,c2,c1],score:[100, 98, 64, 84, 69]})result pd.merge(df1, df…

【C++初阶】vector容器

&#x1f466;个人主页&#xff1a;Weraphael ✍&#x1f3fb;作者简介&#xff1a;目前学习C和算法 ✈️专栏&#xff1a;C航路 &#x1f40b; 希望大家多多支持&#xff0c;咱一起进步&#xff01;&#x1f601; 如果文章对你有帮助的话 欢迎 评论&#x1f4ac; 点赞&#x1…

0007Java程序设计-jsp问卷调查系统设计与实现

摘 要 随着社会不断进步与发展&#xff0c;生活节奏不断加快&#xff0c;信息已经成为我们生活中不可缺少的一部分&#xff0c;很多企业需要掌握大量的信息来了解特定用户的需求&#xff0c;传统的做法是组织大量的人力物力对用户散发调查表&#xff0c;然后对收集的信息进行统…