🎈Introduction

在这个实验中,我们将实现 spawn,一个加载磁盘中的可执行文件上并运行它的库函数。然后将充实操作系统的内核和库,使其足以在控制台上运行 shell。这些特性需要一个文件系统,本实验介绍了一个简单的读/写文件系统。

🧰File system preliminaries

我们将要创建的文件系统比大多数真实文件系统(包括 xv6 Unix)要简单得多,但它的功能强大到足以提供基本功能:创建、读取、写入和删除按层次目录结构组织的文件。
我们只开发了一个单用户操作系统,它提供的保护足以捕捉漏洞,但不能保护多个用户。因此,我们的文件系统不需要支持 UNIX 文件所有权或权限的概念。我们的文件系统目前也不像大多数 UNIX 文件系统那样支持硬链接、符号链接、时间戳或特殊设备文件。

😐On-Disk File System Structure

大多数 UNIX 文件系统将可用磁盘空间划分为两种主要类型的区域:

  • inode 区域:UNIX 文件系统为文件系统中的每个文件分配一个 inode;文件的 inode 保存有关文件的关键元数据,例如文件的 stat 属性和指向数据块的指针
  • 数据区域:数据区域被划分成更大(通常为 8KB 或更多)的数据块,文件系统在其中存储文件数据和目录元数据

目录项包含文件名和指向 inode 的指针;如果文件系统中有多个目录项引用该文件的 inode,则称该文件为硬链接。由于我们的文件系统不支持硬链接,我们不需要这种级别的间接寻址,因此可以进行方便的简化:根本不使 用 inode,而是将文件(或子目录)的所有元数据存储在描述该文件的(一个且唯一的)目录项中。
文件和目录在逻辑上都由一系列数据块组成,这些数据块可能分散在磁盘上,就像环境的虚拟地址空间的页面可以分散在物理内存中一样。
文件系统环境隐藏了块布局的细节,提供了在文件中以任意偏移量读取和写入字节序列的接口。作为执行文件创建和删除等操作的一部分,文件系统环境在内部处理对目录的所有修改。
我们的文件系统允许用户环境直接读取目录元数据(例如,使用 read),这意味着用户环境可以自己执行目录扫描操作(例如,实现 ls 程序),而不必依赖于对文件系统的额外特殊调用。这种目录扫描方法的缺点(也是大多数现代 UNIX 变体不鼓励这种方法的原因)是,它使应用程序依赖于目录元数据的格式,因此很难在不更改或至少不重新编译应用程序的情况下更改文件系统的内部布局。

🍭Sectors and Blocks

大多数磁盘不能以字节为粒度执行读写操作,而是以扇区为单位执行读写操作。在 JOS 中,每个扇区是 512 字节;文件系统实际上以块为单位分配和使用磁盘存储的。
请注意这两个术语之间的区别:扇区大小是磁盘硬件的属性,而块大小是使用磁盘的操作系统的一个定义。文件系统的块大小必须是基础磁盘扇区大小的倍数。
xv6 Unix 文件系统使用 512 字节的块大小,与底层磁盘的扇区大小相同。然而,大多数现代文件系统使用更大的块大小,因为存储空间很便宜,以更大的粒度管理存储更有效。我们的文件系统将使用 4096 字节的块大小,方便地匹配内存的页大小(PGSIZE)。

🍩Superblocks

文件系统通常在磁盘上的便于查找的位置(如最开始或最结束位置)保留某些磁盘块,以保存描述文件系统整体属性的元数据,如块大小、磁盘大小、查找根目录所需的任何元数据、上次装入文件系统的时间、文件保存的时间、上次检查系统是否有错误,以此类推。这些特殊块称为超级块。
我们的文件系统将正好有一个超级块,它将始终位于磁盘上的块 1。它的布局是由 inc/fs.h 中的 Super 结构体定义的。块 0 通常被保留用来保存引导加载程序和分区表,因此文件系统通常不使用第一个磁盘块。
许多真正的文件系统维护多个超级块,这些超级块在磁盘的多个间隔较远的区域中复制,因此,如果其中一个超级块损坏或磁盘在该区域中出现介质错误,则仍然可以找到其他超级块并用于访问文件系统。
🙄Lab 5 - 图1

🍧File Meta-data

文件系统中描述文件的元数据由 inc/fs.h 中的 File 结构体描述。此元数据包括文件的名称、大小、类型(常规文件或目录)以及指向组成文件的块的指针。如上所述,我们没有 inode,所以元数据存储在磁盘上的目录项中。与大多数真实文件系统不同,为了简单起见,我们将使用这个文件结构来表示文件元数据,因为它同时出现在磁盘和内存中。
File 结构体中的 f_direct 数组包含存储文件前10个(NDIRECT)块的块号的空间,我们称之为文件的 direct 块。对于大小在 10*4096=40KB 之内的小文件,所有文件块的块号都将直接在 File 结构体中。但是,对于较大的文件,我们需要一个位置来保存文件的其余块号。
因此,对于任何大于 40KB 的文件,我们会分配一个额外的磁盘块,称为文件的间接块,以容纳最多 4096 / 4=1024 个额外的块号。因此,我们的文件系统最大允许文件的大小达到 1034 个块,即略大于 4MB。为了支持更大的文件,真实的文件系统通常也支持双重和三重间接块。
🙄Lab 5 - 图2

🍫Directories versus Regular Files

文件系统中的文件结构可以表示常规文件或目录;这两种类型的文件通过 File 结构体中的 type 字段来区分。
文件系统以完全相同的方式管理常规文件和目录文件,只是它不解释与常规文件相关联的数据块的内容,而将目录文件的内容解释为描述目录中的文件和子目录的一系列 File 结构体。
文件系统中的 super block 包含一个文件结构(Super 结构体中的 root 字段),它保存文件系统根目录的元数据。这个目录文件的内容是一系列文件结构,描述文件系统根目录中的文件和目录。根目录中的任何子目录都可能包含更多表示子目录的文件结构,以此类推。

🧲The File System

本实验的目标不是实现整个文件系统,而是只实现某些关键组件。特别是:

  • 将块读入块缓存并将其刷新回磁盘
  • 分配磁盘块
  • 将文件偏移映射到磁盘块
  • 以及在 IPC 接口中实现读、写和打开

因为不会自己实现所有的文件系统,所以需要阅读所提供的代码和各种文件系统接口。

🙂Disk Access

我们操作系统中的文件系统环境需要能够访问磁盘,但是我们还没有在内核中实现任何磁盘访问功能。
我们没有采用传统的 monolithic 操作系统策略,即在内核中添加 IDE 磁盘驱动程序以及必要的系统调用以允许文件系统访问它;而是将 IDE 磁盘驱动程序作为用户级文件系统的一部分来实现。我们仍然需要稍微修改内核,以便进行设置,使文件系统环境具有实现磁盘访问本身所需的权限。
只要我们依赖于轮询、基于编程I/O(PIO)的磁盘访问,并且不使用磁盘中断,就可以很容易地在用户空间中实现磁盘访问。
x86 处理器使用 EFLAGS 寄存器中的 IOPL 位来确定是否允许保护模式下的代码执行特殊设备 I/O 指令,如:输入和输出指令。由于我们需要访问的所有 IDE 磁盘寄存器都位于 x86 的 I/O 空间中,而不是内存映射中,因此我们只需要为文件系统环境提供 I/O 特权就可以允许文件系统访问这些寄存器。
实际上,EFLAGS 寄存器中的 IOPL 位为内核提供了一种简单的全有或全无方法来控制用户模式代码是否可以访问 I/O 空间。在本例中,我们希望文件系统环境能够访问 I/O 空间,但我们不希望任何其他环境能够访问 I/O 空间。
Exercise 1
Question 1

🤩The Block Cache

在我们的文件系统中,我们将借助处理器的虚拟内存系统实现一个简单的缓冲区缓存。缓存的代码在 fs/bc.c中。
我们的文件系统将限于处理 3GB 或更小的磁盘。我们保留文件系统环境地址空间的一个足够大的、固定的 3GB 区域,从 0x10000000(DISKMAP)到 0xD0000000(DISKMAP+DISKMAX),作为磁盘的内存映射。
例如,磁盘块 0 映射到虚拟地址 0x10000000,磁盘块 1 映射到虚拟地址 0x10001000,依此类推。fs/bc.c 中的 diskaddr 函数实现了从磁盘块号到虚拟地址的转换以及一些健壮性检查。
由于我们的文件系统环境有自己的虚拟地址空间,独立于系统中所有其他环境的虚拟地址空间,并且文件系统环境只需要实现文件访问,因此以这种方式保留文件系统环境的大部分地址空间是合理的。在32位机器上实现真正的文件系统是很尴尬的,因为现代的磁盘大于 3GB,无法提供足够大的虚拟空间。这种缓冲区高速缓存管理方法在具有 64 位地址空间的机器上可能是可行的。
当然,将整个磁盘读入内存需要很长时间,因此我们将实现一种请求分页的形式,其中我们只在磁盘映射区域中分配页面,并从磁盘中读取相应的块以响应该区域中的页面错误。这样,我们可以假装整个磁盘都在内存中。
Exercise 2

😌The Block Bitmap

在 fs_init 设置位图指针之后,我们可以将位图视为一个压缩的位数组,磁盘上的每个块对应一个位。例如,请参阅 block_is_free,它只检查给定的块是否在位图中标记为 free。
Exercise 3

😨File Operations

我们在 fs/fs.c 中提供了各种函数,以实现解释和管理文件结构、扫描和管理目录文件条目以及从根目录遍历文件系统以解析绝对路径名所需的基本功能。通读 fs/fs.c 中的所有代码,确保在继续之前理解每个函数的作用。
Exercise 4

🥶The file system interface

既然我们在文件系统环境本身中拥有了必要的功能,那么我们就必须让希望使用文件系统的其他环境能够访问它。由于其他环境不能直接调用文件系统环境中的函数,因此我们将通过构建在 JOS 的 IPC 机制之上的远程过程调用(RPC)抽象来公开对文件系统环境的访问。以下是对文件系统服务(比如 read)的调用:

  1. Regular env FS env
  2. +---------------+ +---------------+
  3. | read | | file_read |
  4. | (lib/fd.c) | | (fs/fs.c) |
  5. ...|.......|.......|...|.......^.......|...............
  6. | v | | | | RPC mechanism
  7. | devfile_read | | serve_read |
  8. | (lib/file.c) | | (fs/serv.c) |
  9. | | | | ^ |
  10. | v | | | |
  11. | fsipc | | serve |
  12. | (lib/file.c) | | (fs/serv.c) |
  13. | | | | ^ |
  14. | v | | | |
  15. | ipc_send | | ipc_recv |
  16. | | | | ^ |
  17. +-------|-------+ +-------|-------+
  18. | |
  19. +-------------------+

虚线下的内容只是从常规环境到文件系统环境获取读取文件的请求机制。

🥪read()

read 使用传入的 fdnum 找到对应的文件描述符,并分派给对应的设备 read 函数,在上面的例子中是 devfile_read(devfile_read 实现针对磁盘上文件的读取)

  1. struct FdFile {
  2. int id;
  3. };
  4. struct Fd {
  5. int fd_dev_id;
  6. off_t fd_offset;
  7. int fd_omode;
  8. union {
  9. // File server files
  10. struct FdFile fd_file;
  11. };
  12. };
  13. // 设备描述符的定义
  14. struct Dev {
  15. int dev_id;
  16. const char* dev_name;
  17. ssize_t (*dev_read)(struct Fd* fd, void* buf, size_t len);
  18. ssize_t (*dev_write)(struct Fd* fd, const void* buf, size_t len);
  19. int (*dev_close)(struct Fd* fd);
  20. int (*dev_stat)(struct Fd* fd, struct Stat* stat);
  21. int (*dev_trunc)(struct Fd* fd, off_t length);
  22. };
  23. // 确认 fdnum 是存在并且映射了, 在内存 0xD0000000 上方寻找
  24. // 这里存储 FDTable
  25. int fd_lookup(int fdnum, struct Fd** fd_store) {
  26. struct Fd* fd;
  27. if (fdnum < 0 || fdnum >= MAXFD) {
  28. if (debug)
  29. cprintf("[%08x] bad fd %d\n", thisenv->env_id, fdnum);
  30. return -E_INVAL;
  31. }
  32. fd = INDEX2FD(fdnum);
  33. if (!(uvpd[PDX(fd)] & PTE_P) || !(uvpt[PGNUM(fd)] & PTE_P)) {
  34. if (debug)
  35. cprintf("[%08x] closed fd %d\n", thisenv->env_id, fdnum);
  36. return -E_INVAL;
  37. }
  38. *fd_store = fd;
  39. return 0;
  40. }
  41. // 通过设备的 ID 找到真正的设备描述符
  42. int dev_lookup(int dev_id, struct Dev** dev) {
  43. int i;
  44. for (i = 0; devtab[i]; i++)
  45. if (devtab[i]->dev_id == dev_id) {
  46. *dev = devtab[i];
  47. return 0;
  48. }
  49. cprintf("[%08x] unknown device type %d\n", thisenv->env_id, dev_id);
  50. *dev = 0;
  51. return -E_INVAL;
  52. }
  53. // 通过 fdnum 寻找对应的 Fd 与 Dev,
  54. // 并调用 Dev 结构体中的函数进行文件读取
  55. ssize_t read(int fdnum, void* buf, size_t n) {
  56. int r;
  57. struct Dev* dev;
  58. struct Fd* fd;
  59. if ((r = fd_lookup(fdnum, &fd)) < 0 ||
  60. (r = dev_lookup(fd->fd_dev_id, &dev)) < 0)
  61. return r;
  62. if ((fd->fd_omode & O_ACCMODE) == O_WRONLY) {
  63. cprintf("[%08x] read %d -- bad mode\n", thisenv->env_id, fdnum);
  64. return -E_INVAL;
  65. }
  66. if (!dev->dev_read)
  67. return -E_NOT_SUPP;
  68. return (*dev->dev_read)(fd, buf, n);
  69. }

🍣devfile_read() 与 fsipc()

lib/file.c 中的这个函数和其他 devfile_* 函数实现了签名类似针对不同设备类型的文件操作;它们的工作方式大致相同:将参数捆绑在请求结构中,调用 fsipc 发送 IPC 请求,解包并返回结果。fsipc 函数只处理向服务器发送请求和接收回复的功能。
JOS 的 IPC 机制允许环境发送一个 32 位的数字,并可以选择共享一个页面。为了从客户机向服务器发送请求,我们使用 32 位数字作为请求类型(文件系统服务器的 RPC 编号,就像系统调用编号一样),并将请求的参数存储在通过 IPC 共享的页面上的 fsipc 中;在客户端,我们总是将数据存在 fsipcbuf 共享页面。

  1. static ssize_t devfile_read(struct Fd* fd, void* buf, size_t n) {
  2. int r;
  3. fsipcbuf.read.req_fileid = fd->fd_file.id;
  4. fsipcbuf.read.req_n = n;
  5. if ((r = fsipc(FSREQ_READ, NULL)) < 0)
  6. return r;
  7. assert(r <= n);
  8. assert(r <= PGSIZE);
  9. memmove(buf, fsipcbuf.readRet.ret_buf, r);
  10. return r;
  11. }
  12. static int fsipc(unsigned type, void* dstva) {
  13. static envid_t fsenv;
  14. // 在所有的 env 中寻找文件系统 env
  15. if (fsenv == 0)
  16. fsenv = ipc_find_env(ENV_TYPE_FS);
  17. static_assert(sizeof(fsipcbuf) == PGSIZE);
  18. if (debug)
  19. cprintf("[%08x] fsipc %d %08x\n", thisenv->env_id, type,
  20. *(uint32_t*)&fsipcbuf);
  21. ipc_send(fsenv, type, &fsipcbuf, PTE_P | PTE_W | PTE_U);
  22. return ipc_recv(NULL, dstva, NULL);
  23. }

🍒serv.c

文件系统服务代码可以在 fs/serv.c 中找到。在 serve 函数中有一个永真循环,通过 IPC 无休止地接收请求,将该请求分派给相应的处理函数,并通过 IPC 将结果发送回。在上面的例子中,serve 将分派给 serve_read,它将处理特定于 read 请求的 IPC 细节,例如解包请求结构,最后调用 file_read 来实际执行文件读取。
在服务器端,我们将传入请求页面映射到 fsreq(0x0ffff000)。
服务器还通过 IPC 发回响应。我们使用 32 位数字作为函数的返回代码。对于大多数 rpc 来说,这就是它们的全部回报。FSREQ_READ 和 FSREQ_STAT 还返回数据,它们只需将数据写入客户端发送请求的页面。无需在响应 IPC 中发送此页,因为客户端首先与文件系统服务器共享。此外,在它的回应中,FSREQ 与客户共享一个新的 Fd 页面。
Exercise 5
Exercise 6

🦯Spawning Processes

已经提供了 spawn 的代码(参见 lib/spawn.c),它创建一个新的环境,将程序映像从文件系统加载到其中,然后启动并运行这个程序的子环境。然后父进程继续独立于子进程运行。spawn 函数的作用相当于在 UNIX 中先执行 fork,然后在子进程中执行一个 exec。
我们实现 spawn 而不是 UNIX 风格的 exec,因为 spawn 更容易以 exokernel fashion 的形式从用户空间实现,而无需内核的帮助。
Exercise 7

😎Sharing library state across fork and spawn

UNIX 文件描述符是一个通用的概念,它还包括 pipes、console I/O 等。在 JOS 中,每种设备类型都有一个对应的 Dev 结构体,带有指向为该类型实现的 read/write 等函数的指针。lib/ fd.c 在此基础上实现了通用的类 Unix 文件描述符接口。每个 Fd 结构体都指示其设备类型,lib/fd.c 中的大多数函数只是简单地将操作分派给适当 Dev 结构体中的函数。
lib/fd.c 还在每个应用程序环境的地址空间中维护从 FDTABLE(0xD0000000) 开始的 file descriptor table region。在这个区域每个 Fd 结构体都保留一个页。在任何给定时间,只有在使用相应的文件描述符时才映射特定的文件描述符表页。每个文件描述符在从 FILEDATA 开始的区域中都有一个可选的 data page,设备可以使用这些 data page。
我们希望跨 fork 和 spawn 共享文件描述符状态,但是文件描述符状态保存在用户空间内存中。而且在 fork 时,内存将被标记为 copy-on-write,因此状态将被复制而不是共享。(这意味着环境无法在自己没有打开的文件中进行查找,而且管道也不能跨 fork 工作)。
在 inc/lib.h 中定义了一个新的 PTE_SHARE 位。我们将建立这样一个约定:如果页表条目设置了这个位,那么 PTE 应该在 fork 和 spawn 时从父环境直接复制到子环境。
Exercise 8

🛶The keyboard interface

为了让 shell 正常工作,我们需要一种输入文字的方法。QEMU 一直在显示我们写入 VGA 显示器和串行端口的输出,但到目前为止,我们只在内核监视器中接收输入。kern/console.c 已经包含了从 lab1 开始内核监视器就一直使用的键盘和串行驱动程序,但是现在需要将它们附加到系统的其余部分。
Exercise 9

🛷The Shell

Exercise 10

⛳Exercise

😗No1

i386_init 通过将类型 ENV_type_FS 传递给环境创建函数 env_create 来标识文件系统环境。在 env.c 中修改 env_create,以便它赋予文件系统环境 I/O 权限,但从不将该权限赋予任何其他环境。 确保可以启动文件环境而不会导致一般保护故障。 使用 make grade 通过其中的 fs i/o 测试样例。

如果环境的类型满足 type == ENV_TYPE_FS,那么给予环境 I/O 权限。向 env_create 中添加如下代码:

  1. // LAB 5: Your code here.
  2. if (type == ENV_TYPE_FS) {
  3. e->env_tf.tf_eflags |= FL_IOPL_MASK;
  4. }

😖Q1

当您随后从一个环境切换到另一个环境时,是否必须执行其他操作以确保正确保存和还原此I/O权限设置?为什么?

不需要。环境切换时,会根据 Env 结构体中的 env_tf 保存或恢复它的上下文,包括 eflags 寄存器,所以不需要额外的操作。

☺No2

在 fs/bc.c 中实现 bc_pgfault 和 flush_block 函数。 使用 make grade 测试代码,应该通过 check_bc、check_super 和 check_bitmap。

🍡bc_pgfault()

bc_pgfault 是一个页错误处理程序,就像在上一个实验中为 copy-on-write fork 编写的一样,只是它的任务是响应页错误从磁盘加载页面;需要注意的是:

  1. addr 可能不与块边界对齐
  2. ide_read 在扇区而不是块中操作

    1. static void bc_pgfault(struct UTrapframe* utf) {
    2. void* addr = (void*)utf->utf_fault_va;
    3. uint32_t blockno = ((uint32_t)addr - DISKMAP) / BLKSIZE;
    4. int r;
    5. // 检查段错误是否发生在磁盘映射区域
    6. if (addr < (void*)DISKMAP || addr >= (void*)(DISKMAP + DISKSIZE))
    7. panic("page fault in FS: eip %08x, va %08x, err %04x", utf->utf_eip, addr,
    8. utf->utf_err);
    9. // 检查是否超过了最大的块数目
    10. if (super && blockno >= super->s_nblocks)
    11. panic("reading non-existent block %08x\n", blockno);
    12. // 在内存中分配一页, 从磁盘中读取内容到这个地址.
    13. // 提示: 需要将 addr 对齐, 可以使用 ide_read 读取文件.
    14. //
    15. // LAB 5: you code here:
    16. addr = ROUNDDOWN(addr, BLKSIZE);
    17. if ((r = sys_page_alloc(0, addr, PTE_P | PTE_U | PTE_W)) < 0) {
    18. panic("in bc_pgfault, sys_page_alloc: %e\n", r);
    19. }
    20. if ((r = ide_read(blockno * 8, addr, BLKSECTS)) < 0) {
    21. panic("in bc_pgfault, ide_read: %e\n", r);
    22. }
    23. // 因为我们从磁盘中新读取了文件, 所以清除脏位
    24. if ((r = sys_page_map(0, addr, 0, addr, uvpt[PGNUM(addr)] & PTE_SYSCALL)) < 0)
    25. panic("in bc_pgfault, sys_page_map: %e", r);
    26. // 确认我们读取的块已将被分配
    27. if (bitmap && block_is_free(blockno))
    28. panic("reading free block %08x\n", blockno);
    29. }

    🍮flush_block()

    flush_block 函数应该将块写入磁盘。如果块不在块缓存中,或者如果块没有被写数据,flush_block 就不应该做任何事情;要查看一个块是否需要写入,我们只需查看是否在 uvpt 条目中设置了 PTE_D dirty 位;将块写入磁盘后,flush_block 应使用 sys_page_map 清除 PTE_D 位。

    1. void flush_block(void* addr) {
    2. int r;
    3. uint32_t blockno = ((uint32_t)addr - DISKMAP) / BLKSIZE;
    4. if (addr < (void*)DISKMAP || addr >= (void*)(DISKMAP + DISKSIZE))
    5. panic("flush_block of bad va %08x", addr);
    6. // LAB 5: Your code here.
    7. addr = (void*)ROUNDDOWN(addr, BLKSIZE);
    8. if (va_is_mapped(addr) && va_is_dirty(addr)) {
    9. if ((r = ide_write(blockno * 8, addr, BLKSECTS)) < 0) {
    10. panic("in flush_block, ide_write: %e", r);
    11. }
    12. if ((r = sys_page_map(0, addr, 0, addr, uvpt[PGNUM(addr)] & PTE_SYSCALL)) <
    13. 0) {
    14. panic("in flush_block, sys_page_map: %e", r);
    15. }
    16. }
    17. }

    😰No3

    使用 free_block 作为参照,在 fs/fs.c 中实现 alloc_block。 使用 make grade 测试代码,应该通过 alloc_block。

在位图中找到一个空闲磁盘块,将其标记为 used,并返回该块的编号。分配块时,应立即用 flush_block 将更改后的位图块刷新到磁盘,以帮助实现文件系统的一致性。

  1. int alloc_block(void) {
  2. // The bitmap consists of one or more blocks. A single bitmap block
  3. // contains the in-use bits for BLKBITSIZE blocks. There are
  4. // super->s_nblocks blocks in the disk altogether.
  5. // LAB 5: Your code here.
  6. uint32_t blockno;
  7. // 寻找空闲的块
  8. for (blockno = 2; blockno < super->s_nblocks; blockno++) {
  9. if (block_is_free(blockno)) {
  10. // 在 bitmap 中标志为已经使用 0 为已使用 1为未使用
  11. // 使用异或操作相当巧妙
  12. bitmap[blockno / 32] &= ~(1 << (blockno % 32));
  13. // 将 bitmap 存储到磁盘中
  14. flush_block(&bitmap[blockno / 32]);
  15. return blockno;
  16. }
  17. }
  18. // 没有找到返回 -E_NO_DISK 错误
  19. return -E_NO_DISK;
  20. }

😶No4

实现 file_block_walk 和 file_get_block。 使用 make grade 测试代码,应该通过 file_open、file_get_block、file_flush/file_truncated/file rewrite 和testfile 测试样例。

🍨file_block_walk

将文件中的块偏移映射到 File 结构体或 indirect block 的指针,非常类似于 pgdir_walk 对页表所做的操作。
在文件 f 中找到磁盘块号为 fileno 的 slot;使用 *ppdiskbno 存储这个 slot(这个 slot 是 f->f_direct[] 的起始地址或者是 indirect block 的起始)。
当传递了 alloc 参数,这个函数将会分配一个必需 indirect block。
成功时返回 0,否则返回 <0:

  • -E_NOT_FOUND:函数需要分配 indirect block,但是 alloc 参数是 0
  • -E_NO_DISK:磁盘没有足够的空间分配 indirect block
  • -E_INVAL:fileno 超出范围(fileno >= NDIRECT + NINDIRECT)

提示:不要忘记清除你分配的块。

  1. static int file_block_walk(struct File* f,
  2. uint32_t filebno,
  3. uint32_t** ppdiskbno,
  4. bool alloc) {
  5. // LAB 5: Your code here.
  6. // 验证参数正确性
  7. int r;
  8. if (filebno >= NDIRECT + NINDIRECT) {
  9. return -E_INVAL;
  10. }
  11. // 如果直接在 File 中能找到, 则直接返回
  12. if (filebno < NDIRECT) {
  13. if (ppdiskbno)
  14. *ppdiskbno = &f->f_direct[filebno];
  15. return 0;
  16. }
  17. // 没有分配 indirect 块且不允许分配
  18. if (!alloc && !f->f_direct) {
  19. return -E_NOT_FOUND;
  20. }
  21. if (f->f_indirect == 0) {
  22. // // 分配磁盘块
  23. if ((r = alloc_block()) < 0) {
  24. return -E_NO_DISK;
  25. }
  26. // 设置 indirect
  27. f->f_indirect = r;
  28. // 初始化并刷新到磁盘
  29. memset(diskaddr(r), 0, BLKSIZE);
  30. flush_block(diskaddr(r));
  31. }
  32. if (ppdiskbno)
  33. *ppdiskbno = (uintptr_t*)diskaddr(f->f_indirect) + filebno - NDIRECT;
  34. return 0;
  35. }

🍪file_get_block

更进一步,映射到实际的磁盘块,如果需要,分配一个新的磁盘块。
将 *blk 设置为 f 的第 filebno 个磁盘块的虚拟地址。
成功时返回 0,不成功时返回 <0:

  • -E_NO_DISK:需要分配磁盘,但是空间已满
  • -E_INVAL:filebno 超出限制

提示:使用 file_block_walk 和 alloc_block

  1. int file_get_block(struct File* f, uint32_t filebno, char** blk) {
  2. // LAB 5: Your code here.
  3. int r;
  4. uint32_t* ppdiskbno;
  5. // 得到 indirect block 的地址
  6. if ((r = file_block_walk(f, filebno, &ppdiskbno, 1)) < 0) {
  7. return r;
  8. }
  9. // 上面的函数只能使得 indirect block 的磁盘被分配空间
  10. // 如果是 direct block 那么需要再进行分配, 那么 ppdiskno 为 0
  11. // 这里的 ppdiskbno 已经是 f->f_direct[filebno] 的地址
  12. if (*ppdiskbno == 0) {
  13. if ((r = alloc_block()) < 0) {
  14. return -E_NO_DISK;
  15. }
  16. *ppdiskbno = r;
  17. memset(diskaddr(r), 0, BLKSIZE);
  18. flush_block(diskaddr(r));
  19. }
  20. *blk = diskaddr(*ppdiskbno);
  21. return 0;
  22. }

😵No5

在 fs/serv.c 中实现 serve_read。 使用 make grade 测试代码,应该通过 serve_open/file_stat/file_close 和 file_read 测试样例,分数为70/150。

serve_read 的繁重工作将由 fs/fs.c 中已经实现的 file_read 来完成(反过来说,它只是对 file_get_block 的一系列调用)。serve_read 只需为文件读取提供 RPC 接口。查看 serve_set_size 中的注释和代码,以大致了解服务器功能的结构。
从现在寻找的位置 ipc->read.req_fileid 最多读取 ipc->read.req_n 个字节数据。返回从文件中读取的内容,并将其设置再 ipc->readRet,然后更新寻找位置。

  1. int serve_read(envid_t envid, union Fsipc* ipc) {
  2. int r;
  3. struct OpenFile* po;
  4. struct Fsreq_read* req = &ipc->read;
  5. struct Fsret_read* ret = &ipc->readRet;
  6. if (debug)
  7. cprintf("serve_read %08x %08x %08x\n", envid, req->req_fileid, req->req_n);
  8. // Lab 5: Your code here:
  9. // 得到已经打开的文件描述符
  10. if ((r = openfile_lookup(envid, req->req_fileid, &po)) < 0) {
  11. return r;
  12. }
  13. // 读取文件内容
  14. if ((r = file_read(po->o_file, ret->ret_buf, req->req_n,
  15. po->o_fd->fd_offset)) < 0) {
  16. return r;
  17. }
  18. // 更新偏移
  19. po->o_fd->fd_offset += r;
  20. return r;
  21. }

🍩No6

在 fs/serv.c 中实现 serve_write,在 lib/file.c 中实现 devfile_write。 使用 make grade 测试代码,应该通过 file_write、file_read after file_write、open、large file 测试样例,分数为 90/150。

🍰serve_write()

从 req->req_buf 向 req_fileid 文件中写入 req->req_n 个字节数据,其中写入的位置为 offset,并且更新对应的 offset。
成功时返回 0,失败时返回小于 0。

  1. int serve_write(envid_t envid, struct Fsreq_write* req) {
  2. int r;
  3. struct OpenFile* po;
  4. if (debug)
  5. cprintf("serve_write %08x %08x %08x\n", envid, req->req_fileid, req->req_n);
  6. // LAB 5: Your code here.
  7. // 找到打开的文件
  8. if ((r = openfile_lookup(envid, req->req_fileid, &po)) < 0) {
  9. return r;
  10. }
  11. // 写入数据
  12. int req_n = MIN(req->req_n, PGSIZE);
  13. if ((r = file_write(po->o_file, req->req_buf, req_n, po->o_fd->fd_offset)) <
  14. 0) {
  15. return r;
  16. }
  17. // 更新偏移
  18. po->o_fd->fd_offset += r;
  19. return r;
  20. }

🍛devfile_write()

从 buf 向 fd 最多写入 n 个字节的数据。成功时返回 0,错误时返回小于 0。

  1. static ssize_t devfile_write(struct Fd* fd, const void* buf, size_t n) {
  2. // Make an FSREQ_WRITE request to the file system server. Be
  3. // careful: fsipcbuf.write.req_buf is only so large, but
  4. // remember that write is always allowed to write *fewer*
  5. // bytes than requested.
  6. // LAB 5: Your code here
  7. int r;
  8. n = MIN(n, sizeof(fsipcbuf.write.req_buf));
  9. // 构建 IPC 数据
  10. memmove(fsipcbuf.write.req_buf, buf, n);
  11. fsipcbuf.write.req_fileid = fd->fd_file.id;
  12. fsipcbuf.write.req_n = n;
  13. // 进行 IPC 通讯
  14. if ((r = fsipc(FSREQ_WRITE, NULL)) < 0) {
  15. return r;
  16. }
  17. assert(r <= n);
  18. assert(r <= PGSIZE);
  19. return r;
  20. }

🥴No7

spawn 依赖于新的系统调用 sys_env_set_trapframe 来初始化新创建的环境的状态。在 kern/syscall.c 中实现 sys_env_set_trapframe(不要忘记在 syscall() 进行注册)。 使用 make grade 测试代码。

在 tf 中设置 envid 的中断栈帧。tf 应该被修改得符合 CPL 3 的格式,中断开始,并且 IOPL 是 0。
成功时返回 0,失败时返回小于 0,错误有:

  • -E_BAD_ENV:env 不存在或者没有权限

    1. static int sys_env_set_trapframe(envid_t envid, struct Trapframe* tf) {
    2. // LAB 5: Your code here.
    3. // Remember to check whether the user has supplied us with a good
    4. // address!
    5. int r;
    6. struct Env* env;
    7. // 寻找 env
    8. if ((r = envid2env(envid, &env, 1)) < 0) {
    9. return -E_BAD_ENV;
    10. }
    11. // 判断用户权限
    12. user_mem_assert(env, tf, sizeof(struct Trapframe), PTE_U);
    13. // 打开中断 关闭文件 IO
    14. tf->tf_eflags |= FL_IF;
    15. tf->tf_eflags &= ~FL_IOPL_MASK;
    16. // 这里不是很明白为什么???
    17. tf->tf_cs |= 3;
    18. env->env_tf = *tf;
    19. return 0;
    20. }

    😳No8

    更改 lib/fork.c 中的 duppage 以遵循新的约定:如果页表条目设置了 PTE_SHARE 位,只需直接复制映射(应该使用 PTE_SYSCALL(而不是 0xfff)来屏蔽页表条目中的相关位)。 同样,在 lib/spawn.c 中实现 copy_shared_pages。它应该遍历当前进程中的所有页表条目(就像 fork 那样),将设置了 PTE_SHARE 位的任何页映射复制到子进程中。

🌯duppage()

向其中添加如下代码,不再解释了。通过标志位映射即可:

  1. if (uvpt[pn] & PTE_SHARE) {
  2. if ((r = sys_page_map(sys_getenvid(), (void*)va, envid, (void*)va,
  3. uvpt[pn] & PTE_SYSCALL)) < 0) {
  4. return r;
  5. }
  6. }

🥖copy_shared_pages()

类似于 fork.c 中的 fork(),将父进程的地址空间复制给子进程。应该循环遍历当前进程中的所有页表条目将设置了 PTE_SHARE 位的任何页映射到子进程。

  1. static int copy_shared_pages(envid_t child) {
  2. // LAB 5: Your code here.
  3. int r;
  4. for (int pn = 0; pn < PGNUM(USTACKTOP); pn++) {
  5. if ((uvpd[pn >> 10] & PTE_P) && (uvpt[pn] & PTE_P) &&
  6. (uvpt[pn] & PTE_SHARE)) {
  7. // 设置了 PTE_SHARE 的全部映射
  8. if ((r = sys_page_map(0, PGADDR(pn >> 10, pn % 1024, 0), child,
  9. PGADDR(pn >> 10, pn % 1024, 0),
  10. uvpt[pn] & PTE_SYSCALL)) < 0)
  11. return r;
  12. }
  13. }
  14. return 0;
  15. }

😶No9

在 kern/trap.c 中,调用 kbdintr 来处理trap IRQ_OFFSET+IRQ_KBD,调用 serial_intr 来处理trap IRQ OFFSET+IRQ_serial。

只需要在 dispatch 中进行调用即可:

  1. case IRQ_OFFSET + IRQ_KBD:
  2. kbd_intr();
  3. return;
  4. case IRQ_OFFSET + IRQ_SERIAL:
  5. serial_intr();
  6. return;

🤐No10

shell 不支持 I/O 重定向。使用 sh<script 代替手动输入脚本中的所有命令往往是一个更好的选择。为 < 在 user/sh.c 中.添加 I/O 重定向。 运行 make run-testshell 来测试 shell。

打开 t 以读取文件描述符 0(环境将其用作标准输入)。
我们无法在特定的描述符上打开文件,因此请以 fd 的形式打开该文件,然后检查 fd 是否为0。
如果不是,则将 fd 复制到文件描述符 0 上,然后关闭原始的 fd。

  1. if ((fd = open(t, O_RDONLY)) < 0) {
  2. cprintf("open %s for read: %e", t, fd);
  3. exit();
  4. }
  5. if (fd != 0) {
  6. dup(fd, 0);
  7. close(fd);
  8. }