Lab5实验报告

思考题

Thinking 5.1

如果通过 kseg0 读写设备,那么对于设备的写入会缓存到 Cache 中。这是一种错误的行为,在实际编写代码的时候这么做会引发不可预知的问题。

请思考:这么做这会引发什么问题?对于不同种类的设备(如我们提到的串口设备和 IDE 磁盘)的操作会有差异吗?可以从缓存的性质和缓存更新的策略来考虑。

  • 解:
    • 当外部设备产生中断信号或者更新数据时,此时Cache中之前旧的数据可能刚完成缓存,那么完成缓存的这一部分无法完成更新,则会发生错误的行为。
    • 对于串口设备来说,读写频繁,信号多,在相同的时间内发生错误的概论远高于IDE磁盘。

Thinking 5.2

查找代码中的相关定义,试回答一个磁盘块中最多能存储多少个文件控制块?一个目录下最多能有多少个文件?我们的文件系统支持的单个文件最大为多大?

  • 解:

    1
    char f_pad[BY2FILE - MAXNAMELEN - (3 + NDIRECT) * 4 - sizeof(void *)];
    • 文件控制块结构体中,有这样的一个属性 f_pad ,大小为 BY2FILE 256B256B ,则代表一个文件控制块的大小为 256B256B ,那么一个磁盘块 4KB4KB 最多存储 4KB/256B=164KB/256B=16 个文件控制块。
    • 一个目录 = 一个磁盘块全部用来存目录项,又一个目录项32=4B32位 = 4B;则一个目录 4KB4KB ,一个目录中有 4KB/4B=10244KB/4B=1024 个目录项;又一个目录项指向一个存有文件内容的 4KB4KB 磁盘块,一个磁盘块中有16个文件控制块,则一个目录下最多有 102416=16384=16K1024*16=16384=16K 个文件。
    • 一个文件控制块,描述一个文件;一个文件有 直接指针 + 间接指针 共1024个 ,每个指针指向一个磁盘块,存储着该文件的一部分文件数据。文件系统支持的单个文件最大,则表示1024个指针全部有效,一共指向了1024个磁盘块存着文件数据,又一个磁盘块 4KB4KB,则单个文件最大 10244KB=4MB1024*4KB=4MB

Thinking 5.3

请思考,在满足磁盘块缓存的设计的前提下,我们实验使用的内核支持的最大磁盘大小是多少?

  • 解:

    • 由于将 DISKMAP ~ DISKMAP+DISKMAX 这一段虚存地址空间作为缓冲区DISKMAX = 0x400000000x40000000 ,因此最多处理 1GB1GB

Thinking 5.4

在本实验中, fs/serv.h、 user/include/fs.h 等文件中出现了许多宏定义,试列举你认为较为重要的宏定义,同时进行解释,并描述其主要应用之处。

  • 解:

    • user/include/fd.h 这两个宏用来找 fd 对应的文件信息页面文件缓存区地址

      1
      2
      #define INDEX2FD(i) (FDTABLE + (i)*BY2PG)
      #define INDEX2DATA(i) (FILEBASE + (i)*PDMAP)
    • 文件服务函数调用号,可以重用 $user/lib/fsipc.c $ 的 fsipc() 函数,作为进程间通信的值,用户进程传给文件服务系统的 fs/servfs/servserve()

      1
      2
      3
      4
      5
      6
      7
      #define FSREQ_OPEN 1
      #define FSREQ_MAP 2
      #define FSREQ_SET_SIZE 3
      #define FSREQ_CLOSE 4
      #define FSREQ_DIRTY 5
      #define FSREQ_REMOVE 6
      #define FSREQ_SYNC 7

Thinking 5.5

在 Lab4“系统调用与 fork”的实验中我们实现了极为重要的 fork 函数。那么 fork 前后的父子进程是否会共享文件描述符和定位指针呢?请在完成上述练习的基础上编写一个程序进行验证。

  • 解:

    • fork 前后的父子进程共享文件描述符和定位指针

    • 验证程序:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      #include "lib.h"

      static char *msg = "This is the NEW message of the day!\n\n";
      static char *diff_msg = "This is a different massage of the day!\n\n";

      void umain()
      {
      int r;
      int fdnum;
      char buf[512];
      int n;

      if ((r = open("/newmotd", O_RDWR)) < 0) {
      user_panic("open /newmotd: %d", r);
      }
      fdnum = r;
      writef("open is good\n");

      if ((n = read(fdnum, buf, 511)) < 0) {
      user_panic("read /newmotd: %d", r);
      }
      if (strcmp(buf, diff_msg) != 0) {
      user_panic("read returned wrong data");
      }
      writef("read is good\n");

      int id;

      if ((id = fork()) == 0) {
      if ((n = read(fdnum, buf, 511)) < 0) {
      user_panic("child read /newmotd: %d", r);
      }
      if (strcmp(buf, diff_msg) != 0) {
      user_panic("child read returned wrong data");
      }
      writef("child read is good && child_fd == %d\n",r);
      struct Fd *fdd;
      fd_lookup(r,&fdd);
      writef("child_fd's offset == %d\n",fdd->fd_offset);
      }
      else {
      if((n = read(fdnum, buf, 511)) < 0) {
      user_panic("father read /newmotd: %d", r);
      }
      if (strcmp(buf, diff_msg) != 0) {
      user_panic("father read returned wrong data");
      }
      writef("father read is good && father_fd == %d\n",r);
      struct Fd *fdd;
      fd_lookup(r,&fdd);
      writef("father_fd's offset == %d\n",fdd->fd_offset);
      }
      }
    • 运行结果:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      main.c: main is start ...
      init.c: mips_init() is called
      Physical memory: 65536K available, base = 65536K, extended = 0K
      to memory 80401000 for struct page directory.
      to memory 80431000 for struct Pages.
      pmap.c: mips vm init success
      pageout: @@@___0x7f3fe000___@@@ ins a page
      pageout: @@@___0x40d000___@@@ ins a page
      FS is running
      FS can do I/O
      pageout: @@@___0x7f3fe000___@@@ ins a page
      pageout: @@@___0x407000___@@@ ins a page
      superblock is good
      diskno: 0
      diskno: 0
      read_bitmap is good
      diskno: 0
      alloc_block is good
      file_open is good
      file_get_block is good
      file_flush is good
      file_truncate is good
      diskno: 0
      file rewrite is good
      serve_open 00000400 ffff000 0x2
      open is good
      read is good
      father read is good && father_fd == 0
      father_fd's offset == 41
      [00000400] destroying 00000400
      [00000400] free env 00000400
      i am killed ...
      child read is good && child_fd == 0
      child_fd's offset == 41
      [00001402] destroying 00001402
      [00001402] free env 00001402
      i am killed ...

Thinking 5.6

请解释 File, Fd, Filefd 结构体及其各个域的作用。比如各个结构体会在哪些过程中被使用,是否对应磁盘上的物理实体还是单纯的内存数据等。说明形式自定,要求简洁明了,可大致勾勒出文件系统数据结构与物理实体的对应关系与设计框架。

  • 解:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    struct File {
    char f_name[MAXNAMELEN]; // 文件名称,最大长度MAXNAMELEN值为128
    uint32_t f_size; // 文件的大小,单位:字节
    uint32_t f_type; // 文件类型: 普通文件FTYPE_REG,目录FTYPE_DIR
    uint32_t f_direct[NDIRECT];// 文件的直接指针,用来记录文件的数据块在磁盘上的位置
    uint32_t f_indirect;// 指向一个间接磁盘块,用来存储指向文件内容的磁盘块的指针

    struct File *f_dir; // 指向文件所属的文件目录
    char f_pad[BY2FILE - MAXNAMELEN - (3 + NDIRECT) * 4 - sizeof(void *)];
    //为了让整数个文件结构体占用一个磁盘块,填充结构体中剩下的字节
    } __attribute__((aligned(4), packed));

    struct Fd {
    u_int fd_dev_id; // 外设的id。
    //用户使用fd.c的文件接口时,不同的dev_id会调取不同的文件服务函数。
    //比如File类型的文件服务函数为user/File.c的file_*()函数。
    u_int fd_offset; // 读写的偏移量
    //seek()时修改。
    //offset会被用来找起始filebno文件块号。
    u_int fd_omode; // 打开方式,包括只读、只写、读写
    //req和open结构体都会用到
    };

    struct Filefd {
    struct Fd f_fd; // file descriptor
    u_int f_fileid; // 文件的id
    //模1024后会用来在opentab[]里索引open结构体
    struct File f_file; // 对应文件的文件控制块
    };
    • 结构体均为内存数据,记录了文件信息。
    • Filefd 以及 fd 中的指向的文件控制块 File 中记录的磁盘指针对应物理实体。

Thinking 5.7

下图(文件系统服务时序图)中有多种不同形式的箭头,请解释这些不同箭头的差别,并思考我们的操作系统是如何实现对应类型的进程间通信的。

image-20230509001916040
  • 解:
    • ENV_CREATE(user_env)ENV_CREATE(fs_serv) 都是异步消息,由 init() 发出创建消息后, init() 函数即可返回执行后续步骤,由 fsuser 线程执行自己的初始化工作。
    • fs 线程初始化 serv_init()fs_init() 完成后,进入 serv() 函数,被 ipc_receive() 阻塞为ENV_NOT_RUNNABLE ,直到收到 user 线程的 ipc_send(fsreq) 被唤醒。
    • user 线程向 fs 线程 ipc_send(fsreq) 发送请求为同步消息,发送后自身进入阻塞ENV_NOT_RUNNABLE 等待被唤醒的fs线程服务结束时 ipc_send(dst_va) ,用户线程接收到数据后继续运行,此后 fs 线程进入阻塞,等待下次被用户唤醒。

实验体会

文件系统概述

  • 文件设备(filefile,即狭义的“文件”)、控制台(consoleconsole)和管道(pipepipe

文件系统的设计与实现

核心代码文件的主要功能(协助理解文件系统实现的总体框架):

  • toolstools 目录中存放的是构建时辅助工具的代码:fsformatfsformat 工具 ——创建磁盘镜像
  • fsfs 目录中存放的是文件系统服务进程的代码:通过IPC通信与用户进程 user/lib/fsipc.cuser/lib/fsipc.c 内的通信函数进行交互
    • fs.cfs.c​ :实现文件系统的基本功能函数
    • ide.cide.c:通过系统调用与磁盘镜像进行交互
    • serv.cserv.c :进程的主干函数
  • user/libuser/lib​ 目录下存放了用户程序的库函数:允许用户程序使用统一的接口,抽象地操作磁盘文件系统中的文件,以及控制台和管道等虚拟的文件。
    • fsipc.cfsipc.c :实现与文件系统服务进程的交互
    • file.cfile.c :实现文件系统的用户接口
    • fd.cfd.c :实现文件描述符

一、IDE磁盘驱动

该驱动程序将通过系统调用的方式陷入内核,对磁盘镜像进行读写操作(按一定顺序读写设备寄存器,来实现对外部设备的操作)。

  • kern/console.ckern/console.c :一个简单的控制台驱动程序
  • MMIOMMIO :在内核下使用内存映射I/O(MMIO)技术,通过控制台实现字符的输入输出
  • 磁盘驱动程序采用 MMIOMMIO 技术编写驱动,且本次要编写的驱动程序完全运行在用户空间

1、MMIO:内存映射I/O

外设是通过读写设备上的寄存器来进行数据通信,外设寄存器也称为 I/O 端口,主要用来访
问 I/O 设备。外设寄存器通常包括控制寄存器状态寄存器数据寄存器,这些寄存器被映射
到指定的物理地址空间。 (例如:在 GXemul 中, console 设备被映射到 0x10000000, simulated IDE disk 被映射到 0x13000000,等等 )

在 MIPS 的内核地址空间中(kseg0 和 kseg1 段)实现了硬件级别的物理地址和内核虚拟地址的转换机制,其中,对 kseg1kseg1 段地址的读写不经过 MMUMMU 映射,且不使用高速缓存,这正是外部设备驱动所需要的。因为我们是模拟的,所以可以通过简单地读写某些固定的内核虚拟地址来实现驱动程序的功能。

  • 物理地址 ==> kseg0kseg0 段的内核虚拟地址 : KADDR() ,给物理地址加上 0x800000000x80000000 (ULIM的值)

    物理地址 ==> kseg1kseg1 段的内核虚拟地址 :给物理地址加上 0xA00000000xA0000000

Exercise 5.1+5.2

IDE 磁盘驱动程序位于用户空间,但是用户进程不可以直接写内核虚拟地址,故而利用系统调用sys_write_devsys_read_dev 来实现。

  • syscall_write_dev 函数:

    1
    2
    int syscall_write_dev(void *va, u_int dev, u_int len); //user/lib/syscall_lib.c
    int sys_write_dev(u_int va, u_int pa, u_int len); //kern/syscall_all.c
    • va:用户虚拟地址

      devpa:设备的物理地址

      len:读写的长度(按字节计数)

    • 使用例子:

      1
      panic_on(syscall_write_dev(&temp, DEV_DISK_ADDRESS + DEV_DISK_ID, 4)); //fs/ide.c
  • syscall_read_dev 函数:

    1
    2
    int syscall_read_dev(void *va, u_int dev, u_int len); //user/lib/syscall_lib.c
    int sys_read_dev(u_int va, u_int pa, u_int len); //kern/syscall_all.c
    • va:用户虚拟地址

      devpa:设备的物理地址

      len:读写的长度(按字节计数)

    • 使用例子:

      1
      panic_on(syscall_read_dev(dst, DEV_DISK_ADDRESS + DEV_DISK_BUFFER, BY2SECT)); //fs/ide.c

2、IDE磁盘

磁盘的物理结构
  1. 扇区 (sector):扇区是磁盘执行读写操作的单位,一般是 512 字节
  2. 磁道 (track): 盘片上以盘片中心为圆心,不同半径的同心圆;
  3. 柱面 (cylinder): 硬盘中,不同盘片相同半径的磁道所组成的圆柱面;
  4. 磁头 (head): 每个磁盘有两个面,每个面都有一个磁头;
IDE 磁盘操作

GXemul 提供的 SimulatedIDEdiskSimulated IDE disk 的地址是 0x130000000x13000000 ,I/O 寄存器各偏移和对应功能如下:

偏移 效果 数据位宽
0x0000 写:设置下一次读写操作时的磁盘镜像偏移的字节数 4 字节
0x0008 写:设置高 32 位的偏移的字节数 4 字节
0x0010 写:设置下一次读写操作的磁盘编号 4 字节
0x0020 写:开始一次读写操作(写 0 表示读操作, 1 表示写操作) 4 字节
0x0030 读:获取上一次操作的状态返回值
(读 0 表示失败,非 0 则 表示成功)
4 字节
0x4000~0x41ff 读/写: 512 字节的读写缓存 ——

3、驱动程序编写

  • read_sector 函数:

    1
    extern int read_sector(int diskno, int offset);

    函数具体实现:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    # read sector at specified offset from the beginning of the disk image.
    LEAF(read_sector)
    sw a0, 0xB3000010
    # 设置IDE disk的ID,根据函数声明diskno是第一个参数,故而对应$a0寄存器
    # 0xB3000010=0x13000000+0x0010+0xA0000000 存储下一次读写操作的磁盘编号
    # 这句话表示我们将使用编号为 $a0 的磁盘
    sw a1, 0xB3000000
    # 设置IDE disk的offset,根据函数声明offset是第二个参数,故而对应$a1寄存器
    # 0xB3000000=0x13000000+0xA0000000 存储下一次读写操作时的磁盘镜像偏移的字节数
    # 这句话表示我们将在距离磁盘起始处offset 的位置开始进行磁盘操作
    li t0, 0
    sw t0, 0xB3000020
    # 0xB3000020=0x13000000+0x0020+0xA0000000 选择读/写操作
    # 这两句话置0,表示我们将读磁盘
    lw v0, 0xB3000030
    # 0xB3000030=0x13000000+0x0030+0xA0000000 获取上一次操作的状态返回值
    # 这句话表示将磁盘操作的状态码放入$v0中
    nop
    jr ra
    nop
    END(read_sector)
    • 当需要从磁盘的指定位置读取一个扇区时,内核驱动会调用 read_sector 函数来将磁盘中对应 sector 的数据读到设备缓冲区中。
      • 【注意】 所有的地址操作都需要将物理地址转换成虚拟地址。此处设备基地址对应的 kseg1kseg1 的内核虚拟地址是 0xB30000000xB3000000
      • 通过判断 read_sector 函数的返回值,就可以知道读取磁盘的操作是否成功。如果成功,将这个 sector 的数据 (512 bytes) 从设备缓冲区 (offset 0x40000x41ff0x4000-0x41ff) 中拷贝到目的位置。至此,就完成了对磁盘的读操作。
Exercise 5.3
  • QuestionQuestion:为什么 ide_read 针对 0x40000x41ff0x4000-0x41ff 的操作,是在判断 0x00300x0030 处状态返回值之进行;相反 ide_write 针对 0x40000x41ff0x4000-0x41ff 的操作,是在判断 0x00300x0030 处状态返回值之进行呢?

    答:

    • 读的时候 0x40000x4000 这片 bufferbuffer 是由硬盘设备完成写入的,检查 statusstatus 是检查硬盘写入 bufferbuffer 有没有问题;
    • 写的时候 0x40000x4000 这片 bufferbuffer 是由驱动完成写入的,检查 statusstatus 是检查驱动写入 bufferbuffer有没有问题,所以要先等驱动写完

    【这里的实现其实因为是在模拟器里面 所以有简化,实际上读status 它有可能返回一些正在操作中的flag,正确方法应该是循环忙等status】

  • ide_read() 函数(fs/ide.c)

    • 声明:

      1
      2
      3
      4
      5
      void ide_read(u_int diskno, u_int secno, void *dst, u_int nsecs);
      //diskno: 待操作的磁盘编号
      //secno: 第一个待操作的扇区编号
      //dst: 读出的数据存到地址为dst的地方去
      //nescs: 待操作的扇区数量
    • 作用:读磁盘

    • 实现:使用系统调用,参照 I/O 寄存器各偏移和对应功能赋值

    • 使用例子:

      1
      2
      //fs/fs.c ==> int read_block(u_int blockno, void **blk, u_int *isnew)
      ide_read(0, blockno * SECT2BLK, va, SECT2BLK);
  • ide_write() 函数(fs/ide.c)

    • 声明:

      1
      2
      3
      4
      5
      void ide_write(u_int diskno, u_int secno, void *src, u_int nsecs);
      //diskno: 待操作的磁盘编号
      //secno: 第一个待操作的扇区编号
      //src: 把地址src处存的内容写到磁盘指定区域去
      //nescs: 待操作的扇区数量
    • 作用:写磁盘

    • 实现:使用系统调用,参照 I/O 寄存器各偏移和对应功能赋值

    • 使用例子:

      1
      2
      //fs/fs.c ==> void write_block(u_int blockno)
      de_write(0, blockno * SECT2BLK, va, SECT2BLK);

二、文件系统结构

实现模拟磁盘的驱动程序以及磁盘上和操作系统中的文件系统结构,并实现文件系统操作的相关函数。

  • fsfs 目录:文件系统服务程序的代码
  • user/libuser/lib 目录的 file.cfd.cfsipc.cfile.c、fd.c、fsipc.c 文件:文件系统的用户库(允许用户程序调用其中
    的函数来操作文件系统 )
    • fsipc.cfsipc.c 实现了与文件系统服务进程基于 IPC 的交互
    • file.cfile.c 实现了文件系统的用户接口
    • fd.cfd.c 实现了文件描述符相关操作

文件系统服务进程和其他用户进程之间使用 Lab4 中实现的 IPC机制进行通信。

1、磁盘文件系统布局

  • Block 磁盘块:

    1
    2
    3
    4
    struct Block { //tools/fsformat.c
    uint8_t data[BY2BLK];
    uint32_t type;
    } disk[NBLOCK];
    • 性质:是虚拟概念,是操作系统与磁盘==交互==的最小单位
    • 目的:减小因扇区过多带来的寻址困难;(潜台词磁盘比扇区大)
    • 构成:操作系统决定磁盘块的大小(通常为 2n2^n 个扇区),将相邻的 2n2^n 个扇区组合在一起,形成磁盘块进行整体操作;

    【区分扇区】:扇区是真实存在的,是磁盘==读写==的基本单位,与操作系统无关。

  • Super Block 超级块:

    1
    2
    3
    4
    5
    struct Super {
    u_int s_magic; // 魔数: FS_MAGIC(常量),标识本文件系统
    u_int s_nblocks; // 1024,为本文件系统的磁盘块个数
    struct File s_root; // 根目录,f_type = FTYPE_DIR, f_name = "/"
    };
  • 管理空闲磁盘资源:位图法(Bitmap)

    tools/fsformat.ctools/fsformat.c​ :创建符合我们定义的文件系统镜像,将多个文件按照内核所定义的文件系统写入到磁盘镜像中 。

  • init_disk 函数( tools/fsformat.c )

    • 声明:

      1
      void init_disk();
    • 作用:将所有的块都标记为空闲块

    • 实现:计算所有磁盘块数量 ==> 把位图块置1 ==> 把最后一块位图块的靠后一部分置0 ==> 手动初始化值

      【注】:如果位图还有剩余,不能将最后一块位图块中靠后的一部分内容标记为空闲,因为这些位所对应的磁盘块并不存在,是不可使用的 。

  • block_is_free 函数( fs/fs.c )

    • 声明:

      1
      int block_is_free(u_int blockno);
    • 作用:判断指定磁盘磁盘块是否被占用

    • 实现:通过位图中的特定位来判断

    • 使用例子:

      1
      2
      3
      4
      //fs/fs.c ==> int read_block(u_int blockno, void **blk, u_int *isnew)
      if (bitmap && block_is_free(blockno)) {
      user_panic("reading free block %08x\n", blockno);
      }
Exercise 5.4
  • QuestionQuestion:为什么参数 blockno 的值不能为 0?

    答:因为0号磁盘块存的是磁盘的信息 ,不可以被清除,也就是不能释放置为空闲。

  • free_block 函数(fs/fs.c)

    • 声明:

      1
      void free_block(u_int blockno);
    • 作用:释放磁盘块号为 blockno 的磁盘块,置为空闲,即清除

    • 实现:

      1
      2
      bitmap[blockno / 32] |= (1 << (blockno % 32)); //把blockno磁盘块的bitmap置空闲
      bitmap[blockno / 32] &= (~(1 << (blockno % 32))); //把blockno磁盘块的bitmap置已用
    • 使用例子:

      1
      free_block(bno); //fs/fs.c ==> int alloc_block(void)

2、文件系统详细结构

  • 文件控制块: FileFile 结构体(用于描述和管理文件)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    struct File {
    char f_name[MAXNAMELEN]; // 文件名称,最大长度MAXNAMELEN值为128
    uint32_t f_size; // 文件的大小,单位:字节
    uint32_t f_type; // 文件类型: 普通文件FTYPE_REG,目录FTYPE_DIR
    uint32_t f_direct[NDIRECT];// 文件的直接指针,用来记录文件的数据块在磁盘上的位置
    uint32_t f_indirect;// 指向一个间接磁盘块,用来存储指向文件内容的磁盘块的指针

    struct File *f_dir; // 指向文件所属的文件目录
    char f_pad[BY2FILE - MAXNAMELEN - (3 + NDIRECT) * 4 - sizeof(void *)];
    //为了让整数个文件结构体占用一个磁盘块,填充结构体中剩下的字节
    } __attribute__((aligned(4), packed));
    • f_type

      • 普通文件 FTYPE_REG :其指向的磁盘块存储着文件内容
      • 目录 FTYPE_DIR :其指向的磁盘块存储着该目录下各个文件对应的的文件控制块
    • f_direct[NDIRECT]

      每个文件控制块设有 10 个直接指针;

      每个磁盘块的大小为 4KB,所以上述 10 个直接指针能够表示最大 40KB 的文件,而当文件的大小大于 40KB 时,就需要用到间接指针。

    • f_indirect

      为了简化计算,规定不使用间接磁盘块的前十个指针(10个直接指针)。

      • 使用方法:

        1
        2
        3
        4
        5
        6
        //int i; int bno; struct File *dirf;
        if (i < NDIRECT) {
        bno = dirf->f_direct[i];
        } else {
        bno = (disk[dirf->f_indirect].data)[i];
        }
    image-20230511105415482
  • 查找某个文件:

    1. 超级块中读取根目录的文件控制块
    2. 沿着目标路径,挨个查看当前目录所包含的文件是否与下一级目标文件同名,如此便能查找到最终的目标文件。
  • 镜像文件 fs.imgfs.img 作用:模拟与真实的磁盘文件设备的交互

Exercise 5.5
  • write_file 函数(tools/fsformat.c)【了解即可】

    • 声明:

      1
      void write_file(struct File *dirf, const char *path);
    • 作用:

      ​ 用于将宿主机上路径为 path 的文件写入磁盘镜像中 dirf 所指向的文件控制块所代表的目录下

    • 使用例子:

      1
      2
      //tools/fsformat.c ==> void write_directory(struct File *dirf, char *path)
      write_file(pdir, buf);
  • write_directory 函数(tools/fsformat.c)【了解即可】

    • 声明:

      1
      void write_directory(struct File *dirf, char *path);
    • 作用:

      ​ 用于将宿主机上路径为 path 的目录(及其下所有目录和文件)写入磁盘镜像中 dirf 所指向的文件控制块所代表的目录下

    • 使用例子:

      1
      2
      //tools/fsformat.c ==> int main(int argc, char **argv)
      write_directory(&super.s_root, name);
  • create_file 函数(tools/fsformat.c)

    • 声明:

      1
      struct File *create_file(struct File *dirf);
    • 作用:

      dirf 指向的文件控制块所代表的目录下分配一个文件控制块,并返回指向新分配的文件控制块的指针

    • 实现:

      1. 遍历 dirf 所代表的目录拥有的每一个磁盘块(包括直接和间接指向的磁盘块);
      2. 对每一个磁盘块,从头遍历该磁盘块上的所有文件控制块,找到第一个空的文件控制块并返回该文件控制块指针(空的文件控制块即文件控制块中 f->f_name[0] == '\0') 。
    • 使用例子:

      1
      2
      //tools/fsformat.c ==> void write_directory(struct File *dirf, char *path)
      struct File *pdir = create_file(dirf);
  • QuestionQuestion: 如何将一个文件或指定目录下的文件按照目录结构写入到 target/fs.imgtarget/fs.img

    答:上面的三个函数。

  • make_link_block 函数(tools/fsformat.c)

    • 作用:申请一个新的磁盘块,返回它的 blockno (副作用:已经自动将其归属于 dirf 目录)

    • 使用例子:

      int blockno = make_link_block(dirf, nblk);

      其中,dirf 是目录的文件控制块;nblk 是前面目录中目前已有的磁盘块数量。

  • next_block 函数(tools/fsformat.c)

    • 作用:得到下一个空闲磁盘块的 blockno,并将该磁盘块的 type 设置成传入参数的值
    • 使用例子:int bno = next_block(BLOCK_FILE);
  • save_block_link 函数(tools/fsformat.c)

    • 作用:将 blockno 对应的磁盘块,连接归属到 dirf 目录中 (【注意】后续需要我们自己额外手动改变 dirf->f_size += BY2BLK 的大小)
    • 使用例子:save_block_link(dirf, nblk, bno); nblkdirf中目前已有的磁盘块数量

3、块缓存

​ 借助虚拟内存来实现磁盘块缓存的设计,文件系统服务 是一个用户进程,一个进程可以拥有 4GB4GB 的虚拟内存空间,将 DISKMAP ~ DISKMAP+DISKMAX 这一段虚存地址空间 (0x10000000-0x4fffffff) 作为缓冲区,当磁盘读入内存时,缓冲区用来映射相关的页

​ 建立起磁盘地址空间和进程虚存地址空间之间的缓存映射:image-20230514131521632

  • read_block 函数(fs/fs.c)

    • 声明:

      1
      int read_block(u_int blockno, void **blk, u_int *isnew);
    • 作用:

      1. 将指定编号的磁盘块读入到内存中,并将 blockno 对应磁盘块的虚拟地址存到 (*blk) 当中;
      2. 如果 isnew 是非0的有效地址,就进行操作: 当blockno 对应磁盘块原本就已经 mapped 的时候,就让 *isnew = 0; ,反之,让*isnew = 1;
    • 实现:首先,检查这块磁盘块是否已经在内存中,如果不在,先用 syscall_mem_alloc 分配一页物理内存; 然后调用 ide_read 函数来读取磁盘上的数据到对应的虚存地址处。

    • 使用例子:

      1
      2
      3
      4
      //fs/fs.c ==> void read_super(void)
      if ((r = read_block(1, &blk, 0)) < 0) {
      //...
      }
  • write_block 函数(fs/fs.c)

    • 声明:

      1
      void write_block(u_int blockno);
    • 作用:把内存中的 block 的内容写到磁盘disk对应的磁盘块里(两个东东用的是同一个 blockno

      (Write the current contents of the block out to disk.)注意区分 block 和 disk

    • 实现:函数 diskaddride_write

    • 使用例子:

      1
      write_block(blockno);
  • file_get_block 函数(fs/fs.c)

    • 声明:

      1
      int file_get_block(struct File *f, u_int filebno, void **blk);
    • 作用:令 (*blk) 赋值为文件控制块 f 对应的文件 的第 filebno 个磁盘块 (即将某个指定的文件指向的磁盘块读入内存)

    • 实现:首先为即将读入内存的磁盘块利用函数 file_map_block 分配物理内存,然后使用 read_block 函数将磁盘内容以块为单位读入内存中的相应位置。

    • 使用例子:

      1
      2
      3
      4
      //fs/fs.c ==> int dir_alloc_file(struct File *dir, struct File **file)
      if ((r = file_get_block(dir, i, &blk)) < 0) {
      //...
      }
  • file_map_block 函数(fs/fs.c)

    • 声明:

      1
      int file_map_block(struct File *f, u_int filebno, u_int *diskbno, u_int alloc);
    • 作用:令 (*diskbno) 赋值为 文件控制块 f 对应的文件 的第 filebno 个磁盘块的 “disk block number” 。如果前面的磁盘块还不存在,且 alloc == 1 则当场给它分配一个。

    • 使用例子:

      1
      2
      3
      4
      //fs/fs.c ==> int file_get_block(struct File *f, u_int filebno, void **blk)
      if ((r = file_map_block(f, filebno, &diskbno, 1)) < 0) {
      //...
      }
  • file_block_walk 函数(fs/fs.c)

    • 声明:

      1
      int file_block_walk(struct File *f, u_int filebno, uint32_t **ppdiskbno, u_int alloc);
    • 作用:找 + 可能申请 maybe?

  • alloc_block 函数(fs/fs.c)

    • 声明:

      1
      int alloc_block(void);
    • 作用:Allocate a block – first find a free block in the bitmap, then map it into memory

  • block_is_mapped 函数(fs/fs.c)

    • 声明:

      1
      void *block_is_mapped(u_int blockno);
    • 作用:若blockno已经mapped,就返回它的虚拟缓存地址;反之,返回 NULL

    • 使用例子:void *va = block_is_mapped(blockno);

Exercise 5.6
  • diskaddr 函数(fs/fs.c)

    • 声明:

      1
      void *diskaddr(u_int blockno);
    • 作用:根据一个块的序号 (block number),计算这一磁盘块对应的虚存的起始地址

    • 使用例子:

      1
      2
      //fs/fs.c ==> void *block_is_mapped(u_int blockno)
      void *va = diskaddr(blockno);
Exercise 5.7
  • map_block 函数(fs/fs.c)

    • 声明:

      1
      int map_block(u_int blockno);
    • 作用:分配映射磁盘块需要的物理页面

    • 实现:

      1. 先使用 block_is_mapped 查看 blockno 对应磁盘块是否已被映射到内存,若已经建立了映射,直接返回 0 即可;
      2. 若没有建立映射,则调用 syscall_mem_alloc 函数分配一页物理页面。注意使用 diskaddr 函数将blockno 转换为对应虚拟地址后再传入 syscall_mem_alloc 函数
    • 使用例子:

      1
      2
      3
      4
      //fs/fs.c ==> int alloc_block(void)
      if ((r = map_block(bno)) < 0) {
      //...
      }
  • unmap_block 函数(fs/fs.c)

    • 声明:

      1
      int unmap_block(u_int blockno);
    • 作用:回收用来映射磁盘块的物理页面

    • 实现:

      1. 首先使用 block_is_mapped 函数获取对应磁盘块的虚拟起始地址;
      2. 如果该磁盘块不是空闲的(调用 block_is_free 函数)并且该磁盘块缓存被写过(调用
        block_is_dirty 函数),就调用 write_block 函数将磁盘块缓存写入磁盘disk
      3. 最后调用 syscall_mem_unmap 函数解除对应磁盘块缓存虚拟页面到物理页面的映射
    • 使用例子:同 map_block()

Exercise 5.8
  • dir_lookup 函数(fs/fs.c)

    • 声明:

      1
      int dir_lookup(struct File *dir, char *name, struct File **file);
    • 作用:在 dir 指向的文件控制块所代表的目录下寻找名为 name 的文件,如果找到了,就把它记录给(*file)

    • 实现:对每一个 dir 指向的文件控制块所管理的磁盘块进行以下操作:

      1. 使用 file_get_block 函数获取该磁盘块;
      2. 遍历该磁盘块上存储的所有文件控制块,如果存在某个文件控制块的 f_name 属性与 name 相同,就将 *file 设为该文件控制块,并返回 0。
    • 使用例子:

      1
      2
      3
      4
      //fs/fs.c ==> int walk_path(char *path, struct File **pdir, struct File **pfile, char *lastelem)
      if ((r = dir_lookup(dir, name, &file)) < 0) {
      //...
      }

三、文件系统的用户接口

为用户提供接口和机制使得用户程序能够使用文件系统,这主要通过一个用户态的文件系统服务来实现。

1、文件描述符

引入文件描述符等结构来抽象地表示一个进程所打开的文件,而不必关心文件实际的物理表示,是用户程序管理、操作文件的基础。

  • FileDiscripterFile Discripterfdfd,文件描述符

    是系统给用户提供的整数,用于其在描述符表 (DescriptorTableDescriptor Table) 中进行索引

    • 我们在作为操作系统的 使用者 进行文件 I/O 编程时,使用 open 在描述符表的指定位置存放被打开文件的信息;使用 close 将描述符表中指定位置的文件信息释放;在 writeread 时修改描述符表指定位置的文件信息。这里的【指定位置】即【文件描述符 fd】。

    • 当用户进程试图打开一个文件时,文件系统服务进程需要一个文件描述符来存储文件的基本信息和用户进程中关于文件的状态;同时,文件描述符也起到描述用户对于文件操作的作用。

      当用户进程向文件系统发送打开文件的请求时,文件系统进程会将这些基本信息记录在内存中, 然后由操作系统将用户进程请求的地址映射到同一个存储了文件描述符的物理页上(此部分代码位于 serv.cserv.cserve_open 函数内),因此==一个文件描述符至少需要独占一页的空间==。

      当用户进程获取了文件大小等基本信息后,再次向文件系统发送请求将文件内容映射到指定内存空间中。

  • struct Fd 结构体 和 struct Filefd 结构体

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    struct Fd {
    u_int fd_dev_id;
    u_int fd_offset;
    u_int fd_omode;
    };

    struct Filefd {
    struct Fd f_fd;
    u_int f_fileid;
    struct File f_file;
    };
    • Filefd 结构体的第一个成员就是 Fd,因此指向 Filefd 的指针同样指向这个 Fd 的起始位置,故可以进行强制转换。而转换的结果==仅仅是改变了该指针对一段内存的解释方式==。
Exercise 5.9
  • open 函数(user\lib\file.c)

    • 声明:

      1
      int open(const char *path, int mode);
    • 作用:接受文件路径 path 和模式 mode 作为输入参数,并返回文件描述符的编号

    • 实现:

      1. 首先使用 fd_allocfsipc_open 函数分配一个文件描述符;
      2. 之后使用 fd2data 函数获取文件描述符对应的数据缓存页地址,并从 Filefd 结构体中获取文件的 sizefileid 属性;
      3. 之后遍历文件内容,使用 fsipc_map 函数为文件内容分配页面并映射
      4. 最后使用 fd2num 函数返回文件描述符的编号
    • 使用例子:

      1
      2
      //tools/fsformat.c ==> void write_file(...)
      int fd = open(path, O_RDONLY);
Exercise 5.10

在多次读写同一文件描述符期间,我们希望能够从文件中前一次读写完毕的位置开始,继续读写数据。因此,文件描述符中需要维护一个指针,以帮助我们在文件中定位;在 实现 readwriteseek 等操作时,也需要更新该指针的值。

  • write 函数(user\lib\fd.c)

    • 声明:

      1
      int write(int fdnum, const void *buf, u_int n);
    • 作用:

    • 实现:

    • 使用例子:

  • read 函数(user\lib\fd.c)

    • 声明:

      1
      int read(int fdnum, void *buf, u_int n);
    • 作用:接收一个文件描述符编号 fdnum ,一个缓冲区指针 buf 和一个最大读取字节数 n 作为输入参数,返回成功读取的字节数,并更新文件描述符的偏移量

    • 实现:

      1. 首先使用 fd_lookupdev_lookup 函数获取文件描述符设备结构体。如果查找失败,则返回错误码;
      2. 接下来,函数检查文件描述符的打开模式是否允许读取操作;
      3. 然后,函数调用设备的读取函数 dev_read 从文件当前的偏移位置读取数据到缓冲区中;
      4. 最后,如果读取操作成功,则函数更新文件描述符的偏移量并返回成功读取的字节数;
    • 使用例子:

      1
      2
      3
      4
      5
      //user/lib/fd.c ==> int readn(int fdnum, void *buf, u_int n)
      for (tot = 0; tot < n; tot += m) {
      m = read(fdnum, (char *)buf + tot, n - tot);
      //...
      }

2、文件系统服务

MOS 操作系统中的文件系统服务通过 IPCIPC 的形式供其他进程调用,进行文件读写操作。

具体来说,在内核开始运行时,就启动了文件系统服务进程 ENV_CREATE ( fs_serv ),用户进程需要进行文件操作时,使用 ipc_sendipc_recvfs_serv 进行交互,完成操作。

fs/serv.c 中服务进程的主函数首先调用了 serve_init 函数准备好全局的文件打开记录表 opentab;然后调用 fs_init 函数来初始化文件系统;执行完文件系统的初始化后,调用 serve 函数,文件系统服务开始运行,等待其他程序的请求

  • fs_init 函数首先通过读取超级块的内容获知磁盘的基本信息;然后检查磁盘是否能够正常读写;最后调用 read_bitmap 函数检查磁盘块上的位图是否正确

  • 用户程序在发出文件系统操作请求时,将请求的内容放在对应的结构体中进行消息的传递, fs_serv 进程收到其他进行的 IPCIPC 请求后,IPCIPC 传递的消息包含了 请求的类型其他必要的参数,根据请求的类型执行相应的文件操作(文件的增、删、改、查等),将结果重新通过 IPCIPC 反馈给用户程序。

Exercise 5.11 + 5.12 + 5.13
  • user/lib/fsipc.c :定义了请求文件系统时用到的 IPCIPC 操作

    user/lib/file.c :定义了用户程序读写、创建、删除和修改文件的接口

  • serve_remove 函数 (fs/serv.c)

    • 声明:

      1
      void serve_remove(u_int envid, struct Fsreq_remove *rq);
    • 作用:Serve to remove a file specified by the path in rq.

  • fsipc_remove 函数 (user/lib/fsipc.c)

    • 声明:

      1
      int fsipc_remove(const char *path);
    • 作用:实现了一个客户端向 fs_serv 进程发送删除文件请求的功能。它接收一个文件路径字符串作为参数,并返回删除操作的结果。

    • 实现:

      1. 首先检查路径字符串的长度是否在0到 MAXPATHLEN 之间。如果不是,则返回错误码,表示路径字符串不合法;
      2. 接下来,函数将 IPC 缓冲区 fsipcbuf 视为一个 Fsreq_remove 结构体,代表将要发送的是 remove 请求,并将路径字符串复制到结构体的路径字段中;
      3. 最后,函数使用 fsipc 函数将删除请求发送到 fs_serv 进程,并返回删除操作的结果。

难点分析

2c9cbb5eeb11dc26dbcd921ad8a222e

  • 文件控制块—— BY2FILE = 256B256B

    扇区—— BY2SECT = 512B512B

    磁盘块——BY2BLK = BY2PG = 4KB4KB

    一个磁盘块中的扇区数量—— SECT2BLK = (BY2BLK / BY2SECT) = 88

    缓冲区—— DISKMAP ~ DISKMAP+DISKMAX = 0x100000000x4fffffff0x10000000-0x4fffffff

  • 一个目录 = 一个磁盘块全部用来存目录项

    一个目录项 = 指向一个磁盘块

    一个磁盘块 = 16个文件控制块

    一个文件控制块 = 描述一个文件

    一个文件有 直接指针 + 间接指针 共1024个 ,每个指针指向一个磁盘块 = 存储着该文件的一部分文件数据

  • struct File *dirf指向的文件控制块代表一个目录)引发的一系列操作:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    struct File *dirf; //假设已知 【dir_lookup是个好东西】

    //该目录下磁盘块的数量
    int nblk = dirf->f_size / BY2BLK;

    //遍历该目录下所有的磁盘块,得到对应的blockno
    for (int i = 0; i < nblk; ++i) {
    int bno;
    if (i < NDIRECT) {
    bno = dirf->f_direct[i];
    } else {
    bno = (disk[dirf->f_indirect].data)[i];
    }
    //......
    }

    //遍历该目录下所有的磁盘块,得到对应磁盘的文件控制块
    for (int i = 0; i < nblk; ++i) {
    void *blk;
    try(file_get_block(dirf, i, &blk));
    //......
    }

    //由blockno得到对应磁盘的文件控制块
    struct File *blk = (struct File *)(disk[bno].data);

    //由blk,遍历磁盘块上的所有文件控制块
    for (struct File *f = blk; f < blk + FILE2BLK; ++f) {
    //对f......
    }

    //根据某磁盘的文件控制块,判断该文件是否为空
    if (f->f_name[0] == '\0') {
    //...
    }

    //申请一个新的磁盘块
    struct File *blk = (struct File *)(disk[make_link_block(dirf, nblk)].data);

    //由指向某磁盘块的文件控制块,得到该磁盘块中的第一个文件控制块
    struct File *f = blk;
  • 一个 blockno 对应了两个地方:

    • 用户进程 文件系统服务 的虚拟缓存,里面有一块内存写的blockno 磁盘块的临时内容;

      void *va = diskaddr(blockno);

    • 同时,disk 磁盘中,对应的 blockno 磁盘块,是它实际模拟的硬件部分;

      blockno * SECT2BLK 表示它的第一个扇区

    ​ 合作使用有:ide_write(0, blockno * SECT2BLK, va, SECT2BLK); 【但还是直接用write_block 吧】

  • fs/fs/ 下部分函数的调用关系:【文件系统结构中的部分函数】

    image-20230514144459530
  • user/lib/user/lib/ 下部分函数调用关系:【文件系统的用户接口中的部分函数】

    image-20230515123748971

往年题lab5-1-exam

花叶学长的答案:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
//fs/ide.c
int time_read() {
int time = 0;
if (syscall_read_dev((u_int) & time, 0x15000000, 4) < 0)
user_panic("time_read panic");
if (syscall_read_dev((u_int) & time, 0x15000010, 4) < 0)
user_panic("time_read panic");
return time;
}

void raid0_write(u_int secno, void *src, u_int nsecs) {
int i;
for (i = secno; i < secno + nsecs; i++) {
if (i % 2 == 0) {
ide_write(1, i / 2, src + (i - secno) * 0x200, 1);
} else {
ide_write(2, i / 2, src + (i - secno) * 0x200, 1);
}
}
}

void raid0_read(u_int secno, void *dst, u_int nsecs) {
int i;
for (i = secno; i < secno + nsecs; i++) {
if (i % 2 == 0) {
ide_read(1, i / 2, dst + (i - secno) * 0x200, 1);
} else {
ide_read(2, i / 2, dst + (i - secno) * 0x200, 1);
}
}
}

往年题lab5-1-extra

花叶学长的答案:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
//fs/ide.c
int raid4_valid(u_int diskno) {
return !ide_read(diskno, 0, (void *) 0x13004000, 1);
}

#define MAXL (128)

int raid4_write(u_int blockno, void *src) {
int i;
int invalid = 0;
int check[MAXL];
for (i = 0; i < 8; i++) {
if (raid4_valid(i % 4 + 1)) {
ide_write(i % 4 + 1, 2 * blockno + i / 4, src + i * 0x200, 1);
} else { invalid++; }
}
if (!raid4_valid(5)) {
return invalid / 2 + 1;
}
int j, k;
for (i = 0; i < 2; i++) {
for (j = 0; j < MAXL; j++) {
check[j] = 0;
for (k = 0; k < 4; k++) {
check[j] ^= *(int *) (src + (4 * i + k) * 0x200 + j * 4);
}
}
ide_write(5, 2 * blockno + i, (void *) check, 1);
}
return invalid / 2;
}

int raid4_read(u_int blockno, void *dst) {
int i;
int invalid = 0;
int wrong = 0;
int check[2 * MAXL];
for (i = 1; i <= 5; i++) {
if (!raid4_valid(i)) {
invalid++;
wrong = i;
}
}
if (invalid > 1) {
return invalid;
}
for (i = 0; i < 8; i++) {
if (i % 4 + 1 != wrong) {
ide_read(i % 4 + 1, 2 * blockno + i / 4, dst + i * 0x200, 1);
}
}
if (wrong == 5) {
return 1;
}
int j, k;
ide_read(5, 2 * blockno, check, 2);
for (i = 0; i < 2; i++) {
for (j = 0; j < MAXL; j++) {
for (k = 0; k < 4; k++) {
check[i * MAXL + j] ^= *(int *) (dst + (4 * i + k) * 0x200 + j * 4);
}
}
}
if (!wrong) {
for (j = 0; j < 2 * MAXL; j++) {
if (check[j] != 0) {
return -1;
}
}
return 0;
}
wrong--;
user_bcopy(check, dst + wrong * 0x200, 0x200);
user_bcopy((void *) check + 0x200, dst + 0x800 + wrong * 0x200, 0x200);
return 1;
}

课上测试

lab5-1-Exam

考察类似于 ide_read() 仿写,题目给的步骤很清晰,一步一步使用 syscall_read_dev()syscall_write_dev() 系统调用实现即可。

lab5-1-Extra

本题考察 ide_read()ide_write() 函数的应用,实现一个简单的 SSDSSD ,具体题目见文章 Lab5-1-Extra-SSD题干

引入三个全局变量,分别表示闪存映射表物理块位图物理块累计擦除次数表即可。

  • 我的答案:【又一次感激涕零~ 题干描述清晰无歧义,也贴心地告诉了我们要“怎么实现?” “实现注意点?” 贴心得不得了~~】

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    u_int ssdtable[32];
    u_int ssdbitmap[32];//1 可写
    u_int ssdnum[32];
    void ssd_init() {
    for (u_int i = 0; i < 32; i++) {
    ssdtable[i] = 0xffffffff;
    ssdbitmap[i] = 1;
    ssdnum[i] = 0;
    }

    }
    int ssd_read(u_int logic_no, void *dst) {
    if (ssdtable[logic_no] == 0xffffffff) {
    return -1;
    }
    ide_read(0, ssdtable[logic_no], dst, 1);
    return 0;
    }
    void ssd_write(u_int logic_no, void *src) {
    if (ssdtable[logic_no] != 0xffffffff) {
    ssd_erase(logic_no);
    }
    //alloc----------------------
    u_int physical_no = 0xffffffff;
    for (u_int i = 0; i < 32; i++) {
    if (ssdbitmap[i] == 1) {
    if (physical_no == 0xffffffff) {
    physical_no = i;
    } else {
    if (ssdnum[i] < ssdnum[physical_no])
    physical_no = i;
    }
    }
    }
    if (ssdnum[physical_no] >= 5) {
    u_int help_no = 0xffffffff;
    u_int help_logic = 0xffffffff;
    for(u_int i = 0; i < 32; i++) {
    if (ssdbitmap[i] == 0) {
    if (help_no == 0xffffffff) {
    help_no = i;
    } else {
    if (ssdnum[i] < ssdnum[help_no])
    help_no = i;
    }
    }
    }
    for (u_int i = 0; i < 32; i++)
    if (ssdtable[i] == help_no) {
    help_logic = i;
    break;
    }
    u_int help_data[128];
    if (ssd_read(help_logic, (void *)help_data) != 0)
    user_panic("wrong in ssd_write's help_data\n");
    ide_write(0, physical_no, (void *)help_data, 1);
    ssdbitmap[physical_no] = 0;
    ssd_erase(help_logic);
    ssdtable[help_logic] = physical_no;
    physical_no = help_no;
    }
    //---------------------------
    ssdtable[logic_no] = physical_no;
    ide_write(0, physical_no, src, 1);
    ssdbitmap[physical_no] = 0;
    }
    void ssd_erase(u_int logic_no) {
    if (ssdtable[logic_no] == 0xffffffff) {
    return;
    }

    //all 0 in 物理块------------
    u_int zero[128];
    for (u_int i = 0; i < 128; i++)
    zero[i] = 0;
    ide_write(0, ssdtable[logic_no], (void *)zero, 1);
    //---------------------------
    ssdnum[ssdtable[logic_no]]++;
    ssdbitmap[ssdtable[logic_no]] = 1;

    ssdtable[logic_no] = 0xffffffff;
    }

lab5-2-Exam

我们在课下已经实现了函数 int open(const char *path, int mode) ,利用绝对路径(相对于根目录的路径)path 定位文件。在实际的用户程序中,完成同一任务时打开的多个文件往往存储在同一目录下,然而系统每次打开文件时都需要从根目录开始查找路径,从而重复访问相同的目录。

在本次题中,要求实现 int openat(int dirfd, const char *path, int mode) 函数,利用相对于目录 dirfd 的相对路径 path 定位并打开文件,其中文件描述符 dirfd 指向已通过 open() 打开的目录。相对路径 path 也可能包含路径分隔符 /,表示查找目录 dirfd 下嵌套的目录中的文件。

  • 实现思路:

    • 在 user/include/fsreq.h 中增加一个对于文件系统的请求类型 #define FSREQ_OPENAT 8 和请求结构体:

      1
      2
      3
      4
      5
      struct Fsreq_openat {
      u_int dir_fileid;
      char req_path[MAXPATHLEN];
      u_int req_omode;
      };
    • 在 user/lib/fsipc.c 中仿照 fsipc_open 实现 int fsipc_openat(u_int dir_fileid, const char *path, u_int omode, struct Fd *fd),完成对 Fsreq_openat 各个字段的赋值,并与文件系统服务进程进行通信。

    • 在 user/lib/file.c 中仿照 open 函数实现 int openat(int dirfd, const char *path, int mode),实现这一函数的相关提示:

      • 调用 fd_lookup 利用 dirfd 查找 dirfd 的文件描述符 struct Fd *dir
      • struct Fd *dir 指向的类型转换为 struct Filefd 后获得 dirfd 对应的 fileid
      • 调用 fsipc_openat 打开文件并完成对指针 fd 的赋值。
    • 在 fs/fs.c 中,仿照 walk_path 实现 int walk_path_at(struct File *par_dir, char *path, struct File **pdir, struct File **pfile, char *lastelem)

      par_dir 目录下按相对路径 path 查找文件,并仿照 file_open 实现 int file_openat(struct File *dir, char *path, struct File **file) 调用 walk_path_at 函数。

    • 在 fs/serv.c 中仿照 serve_open 实现 serve_openat 函数,并在 serve 函数中增加关于 openat 请求的判断。

      • 提示:可以参考以下实现,利用 dir_fileid 查找已经被打开的 dirfd 对应的文件控制块:

        1
        2
        3
        4
        5
        6
        struct Open *pOpen;
        if ((r = open_lookup(envid, rq->dir_fileid, &pOpen)) < 0) {
        ipc_send(envid, r, 0, 0);
        return;
        }
        struct File *dir = pOpen->o_file;
    • 上述函数中,需要在 user/include/lib.h 中增加 int openat(int dirfd, const char *path, int mode)int fsipc_openat(u_int, const char *, u_int, struct Fd *) 函数声明,在 fs/serv.h 中增加 int file_openat(struct File *dir, char *path, struct File **pfile) 函数声明。

lab5-2-Extra

本题考察修改镜像文件 fsformat 的方式,实现符号链接(Symbolic link),具体题目见文章 Lab5-2-Extra-Symbolic link题干

【后来发现自己的错误在:题干给的提示没有让修改 user/lib/file.c 中的 open 函数,于是我就没有想到过要修改这个,然后不管我怎么测试修改都不正确,哭唧唧~ 这个教训是不要寄所有希望于题干】

  • 鹿煜恒大佬的答案:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    //tools/fsformat.c
    void write_symlink(struct File *dirf, const char *path) {
    int iblk = 0, r = 0, n = sizeof(disk[0].data);
    struct File *target = create_file(dirf);
    char targetpath[2048] = {0};
    int len = readlink(path, targetpath, 2047);
    /* in case `create_file` is't filled */
    memcpy(disk[nextbno].data, targetpath, len);
    disk[nextbno].data[len]='\0';

    // Get file name with no path prefix.
    const char *fname = strrchr(path, '/');
    if (fname) {
    fname++;
    } else {
    fname = path;
    }
    strcpy(target->f_name, fname);

    target->f_size = 2048;
    target->f_type = FTYPE_LNK;

    save_block_link(target, 0 , next_block(BLOCK_DATA));
    }

    //user/lib/file.c
    int open(const char *path, int mode) {
    //......略

    if(ffd->f_file.f_type == FTYPE_LNK){
    return open(fd2data(fd), mode);
    }else{
    return fd2num(fd);
    }
    }

体会与感想

​ lab5 是OS课程的最后一个有上机考试的单元,终于结束了,十分感动ing~

​ 本单元我们成功搭建了一个简单的文件系统,值得注意的是,我们学会了修改镜像文件 fsformat ,也明白了:文件系统服务 本身也是一个用户进程。当然,我们实现的仅仅是一个非常简单的系统,实际工程设计中的文件系统远比这个要复杂很多,未来如果想要在这个领域有所建树,还是需要投入大量的时间精力进行学习探究。