Lab 5: File system, Spawn and Shell

Introduction

在本实验室中,你将实现spawn,这是一个库调用,用于加载和运行磁盘上的可执行文件。然后,您将充实内核和库操作系统,以便在控制台中运行shell。这些功能需要一个文件系统,本实验室介绍了一个简单的读写文件系统。

Getting Started

athena% git checkout -b lab5 origin/lab5

Branch lab5 set up to track remote branch refs/remotes/origin/lab5.

Switched to a new branch "lab5"

athena% git merge lab4

实验室这部分的主要新组件是文件系统环境,位于新的fs目录中。浏览这个目录中的所有文件,感受一下所有的内容都是新的。此外,在user和lib目录中有一些新的与文件系统相关的源文件,

fs/fs.c

Code that mainipulates the file system's on-disk structure.

fs/bc.c

A simple block cache built on top of our user-level page fault handling facility.

fs/ide.c

Minimal PIO-based (non-interrupt-driven) IDE driver code.

fs/serv.c

The file system server that interacts with client environments using file system IPCs.

lib/fd.c

Code that implements the general UNIX-like file descriptor interface.

lib/file.c

The driver for on-disk file type, implemented as a file system IPC client.

lib/console.c

The driver for console input/output file type.

lib/spawn.c

Code skeleton of the spawn library call.

在合并到新的实验室5代码之后,您应该再次运行来自实验室4的pingpong, primes和forktree测试用例。您需要注释掉kern/init.c中的ENV_CREATE(fs_fs)行,因为fs/fs.c试图执行一些I/O操作,而jos还不允许这样做。类似地,暂时将lib/exit.c中对close_all()的调用注释掉;这个函数会调用你稍后将在实验中实现的子例程,因此如果被调用,就会出现panic。如果你的lab 4代码不包含任何错误,测试用例应该可以正常运行。在它们起作用之前不要继续。开始练习1时,不要忘记取消这些行注释。

如果它们不起作用,请使用git diff lab4来检查所有更改,确保您为lab4(或之前)编写的任何代码没有从实验室5中丢失。确保四号实验室还能用。

File system preliminaries

文件系统基础知识

您将使用的文件系统比大多数“真正的”文件系统(包括xv6 UNIX的文件系统)简单得多,但它足够强大,能够提供基本特性:创建、读取、写入和删除组织在分层目录结构中的文件。

我们(至少目前)只开发了一个单用户操作系统,它提供了足够的保护来捕获bug,但无法保护多个相互怀疑的用户相互攻击。因此,我们的文件系统不支持UNIX中关于文件所有权或权限的概念。我们的文件系统目前也不支持硬链接、符号链接、时间戳或大多数UNIX文件系统支持的特殊设备文件。

On-Disk File System Structure

大多数UNIX文件系统将可用的磁盘空间划分为两种主要类型的区域:inode区域和数据区域。UNIX文件系统为文件系统中的每个文件分配一个inode。文件的inode保存了有关文件的关键元数据,如stat属性和指向数据块的指针。数据区域划分为更大的数据块(通常为8KB或更多),文件系统在其中存储文件数据和目录元数据。目录项包含了文件名和指向inode的指针。如果文件系统中的多个目录项引用了该文件的inode,则称该文件是硬链接(hard-linked)的。由于我们的文件系统不支持硬链接,我们不需要这种间接级别,因此可以进行方便的简化:我们的文件系统根本不使用inode,只是将文件(或子目录)的所有元数据存储在描述该文件的(唯一的)目录项中。

文件和目录逻辑上都由一系列数据块组成,这些数据块可能分散在磁盘上,就像环境的虚拟地址空间的页可以分散在物理内存中一样。文件系统环境隐藏了块布局的细节,提供了读取和写入文件中任意偏移位置的字节序列的接口。文件系统环境在内部处理对目录的所有修改,作为执行文件创建和删除等操作的一部分。我们的文件系统确实允许用户环境直接读取目录元数据(例如,使用read),这意味着用户环境可以自己执行目录扫描操作(例如,实现ls程序),而不必依赖对文件系统的额外特殊调用。这种目录扫描方法的缺点是,它使应用程序依赖于目录元数据的格式,在不更改或至少重新编译应用程序的情况下,很难更改文件系统的内部布局,这也是大多数现代UNIX变体不鼓励使用这种方法的原因。

Sectors and Blocks

大多数磁盘不能以字节粒度进行读写,而是以扇区为单位进行读写。在JOS中,扇区的长度为512字节。文件系统实际上以块为单位分配和使用磁盘存储。请注意这两个术语之间的区别:扇区长度是磁盘硬件的属性,而块长度是使用磁盘的操作系统的一个方面。文件系统的块大小必须是底层磁盘扇区大小的倍数。

UNIX xv6文件系统使用的块大小为512字节,与底层磁盘的扇区大小相同。然而,大多数现代文件系统使用更大的块大小,因为存储空间已经变得更便宜,并且在更大的粒度上管理存储更加有效。我们的文件系统将使用4096字节的块大小,方便地匹配处理器的页面大小。

Superblocks

文件系统通常会在磁盘上“容易找到”的位置保留某些磁盘块(例如最开始或最末尾),用于保存描述文件系统整体属性的元数据,例如块大小、磁盘大小、查找根目录所需的任何元数据、文件系统最后一次挂载的时间、文件系统最后一次检查错误的时间,等等。这些特殊的块称为超级块(superblock)。

我们的文件系统只有一个超级块,它始终位于磁盘的第1块。其布局由inc/fs.h中的struct Super定义块0通常用于保存启动加载程序和分区表,因此文件系统通常不会使用第一个磁盘块。许多“真正的”文件系统维护多个超级块,复制到磁盘上几个分布很广的区域中,这样,如果其中一个超级块损坏了,或者磁盘在该区域发生了介质错误,仍然可以找到其他超级块并用于访问文件系统。

File Meta-data

在我们的文件系统中描述文件的元数据的布局由inc/fs.h中的struct file描述。这个元数据包括文件的名称、大小、类型(普通文件或目录),以及指向组成该文件的块的指针。如前所述,我们没有inode,因此该元数据存储在磁盘上的一个目录项中。与大多数“真实的”文件系统不同,为了简单起见,我们将使用这个文件结构来表示同时出现在磁盘和内存中的文件元数据。

struct File中的f_direct数组包含了存储文件前10个(NDIRECT)块号的空间,我们称之为文件的直接块(direct block)。对于大小不超过10*4096 = 40KB的小文件,这意味着所有文件块的块号将直接放入文件结构本身。然而,对于较大的文件,我们需要一个地方来保存文件其余的块号。因此,对于任何大于40KB的文件,我们分配了一个额外的磁盘块,称为文件的间接块,用于存储最多4096/4 = 1024个额外的块号。因此,我们的文件系统允许文件大小达到1034块,即4兆字节多一点。为了支持更大的文件,“真正的”文件系统通常还支持双重或三重间接块。

Directories versus Regular Files

文件系统中的文件结构可以表示普通文件,也可以表示目录;这两种“文件”由文件结构中的type字段区分。文件系统以完全相同的方式管理普通文件和目录文件,只是它根本不解释与普通文件相关的数据块的内容,而文件系统将目录文件的内容解释为一系列文件结构,描述了该目录下的文件和子目录。

文件系统中的超级块包含一个文件结构(struct Super中的root字段)它保存了文件系统根目录的元数据。该目录文件的内容是一系列文件结构,描述了位于文件系统根目录中的文件和目录。根目录下的任何子目录又可能包含更多表示子目录的文件结构,以此类推。

The File System

这个实验室的目标不是让您实现整个文件系统,而是让您只实现某些关键组件。具体来说,用户将负责将数据块读取到块缓存中,并将它们刷写回磁盘;分配磁盘块;将文件偏移量映射到磁盘块;并在IPC接口中实现读、写和打开。因为您不会自己实现所有文件系统,所以熟悉所提供的代码和各种文件系统接口非常重要。

Disk Access

我们操作系统中的文件系统环境需要能够访问磁盘,但我们还没有在内核中实现任何磁盘访问功能。我们没有采用传统的“单体”操作系统策略,即向内核中添加一个IDE磁盘驱动器以及必要的系统调用以允许文件系统访问它,而是将IDE磁盘驱动器实现为用户级文件系统环境的一部分。我们仍然需要稍微修改内核,以便使文件系统环境具有实现磁盘访问所需的权限。

只要我们依赖轮询、基于“编程I/O”(PIO)的磁盘访问,并且不使用磁盘中断,就可以很容易地在用户空间中实现磁盘访问。在用户态也可以实现中断驱动的设备驱动程序(例如,L3和L4内核会这样做),但难度更大,因为内核必须对设备中断进行处理,并将其分发到正确的用户态环境。

x86处理器使用EFLAGS寄存器中的IOPL位来确定是否允许保护模式代码执行特殊的设备I/O指令,如in和OUT指令。由于我们需要访问的所有IDE磁盘寄存器都位于x86的I/O空间中,而不是内存映射,因此为文件系统环境赋予“I/O权限”是我们唯一需要做的,以便允许文件系统访问这些寄存器。实际上,EFLAGS寄存器中的IOPL位向内核提供了一种简单的“全有或全无”方法,用于控制用户态代码是否可以访问I/O空间。在我们的例子中,我们希望文件系统环境能够访问I/O空间,但是我们根本不希望任何其他环境能够访问I/O空间。

练习1

i386_init通过将类型ENV_TYPE_FS传递给创建环境的函数env_create来标识文件系统环境。在env.c中修改env_create,使其赋予文件系统环境的I/O权限,但绝不将该权限赋予任何其他环境。

确保您可以启动文件环境,而不会导致一般保护故障。你应该通过“fs i/o”考试。

The Block Cache

在我们的文件系统中,我们将借助处理器的虚拟内存系统实现一个简单的“缓冲区缓存”(实际上只是块缓存)。块缓存的代码在fs/bc.c中。

我们的文件系统只能处理3GB或更小的磁盘。我们保留了文件系统环境地址空间的一个大的、固定的3GB区域,从0x10000000 (DISKMAP)到0xD0000000 (DISKMAP+DISKMAX),作为磁盘的“内存映射”版本。例如,磁盘块0映射到虚拟地址0x10000000,磁盘块1映射到虚拟地址0x10001000,等等。fs/bc.c中的diskaddr函数实现了从磁盘块号到虚拟地址的转换(以及一些完整性检查)

由于我们的文件系统环境有自己的虚拟地址空间,独立于系统中所有其他环境的虚拟地址空间,并且文件系统环境只需要做一件事就是实现文件访问,因此以这种方式保留文件系统环境的大部分地址空间是合理的。对于32位机器上的真正的文件系统实现来说,这样做是很尴尬的,因为现代磁盘大于3GB。在具有64位地址空间的计算机上,这种缓冲区缓存管理方法可能仍然是合理的。

当然,将整个磁盘读入内存需要很长时间,因此我们将实现一种按需分页的形式,在这种形式中,我们只在磁盘映射区域中分配页,并在该区域出现缺页异常时从磁盘中读取相应的块。这样,我们就可以假装整个磁盘都在内存中。

练习2

实现fs/bc.c中的bc_pgfaultflush_block函数。bc_pgfault是一个缺页异常处理程序,就像您在上一个关于写时复制分支的实验中编写的那样,只是它的工作是从磁盘加载页以响应缺页异常。在写这段代码时,请记住:(1)addr不能对齐到块边界;(2)ide_read以扇区而不是块为单位操作。

flush_block函数在必要时将一个块写入磁盘。如果块不在块缓存中(即页没有映射),或者该页不是脏的,Flush_block不应该做任何操作。我们将使用VM硬件来跟踪磁盘块自最后一次读取或写入磁盘以来是否被修改过。要确定一个块是否需要写入,只需查看uvpt项中的PTE_D“dirty”位是否设置。(PTE_D比特位由处理器设置,以响应对该页的写操作。参见386参考手册第5章5.2.4.3。)在将块写入磁盘之后,flush_block应该使用sys_page_map清除PTE_D位。

使用make grade来测试你的代码。你的代码应该递"check_bc"、"check_super"和"check_bitmap"。

The Block Bitmap

在fs_init设置位图指针之后,我们可以将bitmap视为一个位数组,每个位代表磁盘上的一个块。例如,block_is_free函数检查给定的块在位图中是否标记为空闲。

练习3

在fs/fs.c中实现alloc_block时,使用free_block作为模型,该函数应在位图中找到一个空闲磁盘块,标记它已使用,并返回该块的编号。在分配块时,应该立即用flush_block将修改后的位图块刷新到磁盘,以保持文件系统的一致性。

使用make grade来测试你的代码。你的代码现在应该传递"alloc_block"。

File Operations

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

练习4

实现file_block_walk和file_get_block。file_block_walk将文件内的块偏移量映射到struct file中该块或间接块的指针,非常类似于pgdir_walk对页表所做的工作。File_get_block更进一步,映射到实际的磁盘块,在必要时分配一个新的磁盘块。

使用make grade来测试你的代码。你的代码应该传递"file_open"、"file_get_block"、"file_flush/file_truncated/file rewrite"和"testfile"。

File_block_walk和file_get_block是文件系统的主力神。例如,file_read和file_write只是file_get_block上的簿记工作,在分散的块和顺序缓冲区之间复制字节是必需的。

The file system interface

现在文件系统环境本身已经具备了必要的功能,还必须让希望使用该文件系统的其他环境能够访问它。因为其他环境不能直接调用文件系统环境中的函数,所以我们将通过构建在JOS IPC机制之上的远程过程调用(remote procedure call, RPC)来公开对文件系统环境的访问。从图形化的角度来看,下面是对文件系统服务器的调用(比如read)

虚线下面的所有内容只是将读取请求从普通环境获取到文件系统环境的机制。从头开始,read(我们提供的)工作于任何文件描述符,并简单地分派给适当的设备read函数,在本例中是devfile_read(我们可以有更多的设备类型,如管道)。Devfile_read专门实现了对磁盘文件的读取。这个函数和lib/file.c中的其他devfile_*函数实现了FS操作的客户端,它们的工作方式大致相同,将参数打包到一个请求结构中,调用fsipc发送IPC请求,然后解包并返回结果。fsipc函数只处理向服务器发送请求和接收应答的常见细节。

文件系统服务器的代码可以在fs/serv.c中找到。它在serve函数中循环,无止境地通过IPC接收请求,将请求分派给适当的处理函数,并通过IPC将结果发回。在读取的例子中,serve会分派给serve_read,后者负责处理特定于读取请求的IPC细节,例如解包请求结构,最后调用file_read来实际执行文件读取。

回想一下,JOS的IPC机制允许环境发送单个32位数字,并可选择共享一个页面。要从客户端向服务器发送请求,我们使用32位数字作为请求类型(文件系统服务器的rpc是编号的,就像系统调用编号一样),并将请求的参数存储在通过IPC共享的页面上的union Fsipc中。在客户端,我们总是在fsipcbuf上共享页面;在服务器端,我们将传入的请求页面映射到fsreq (0x0ffff000)。

服务器也通过IPC发回响应。我们使用32位数字作为函数的返回码。对于大多数rpc,这就是它们返回的全部内容。FSREQ_READ和FSREQ_STAT也返回数据,它们只是将数据写入客户端发送请求的页。不需要在响应IPC中发送此页面,因为客户端首先将其与文件系统服务器共享。此外,在其响应中,FSREQ_OPEN与客户端共享一个新的“Fd页面”。我们稍后将返回到文件描述符页面。

练习5

在fs/serv.c中实现serve_read。

Serve_read的繁重工作将由fs/fs.c中已经实现的file_read完成(而file_read只是对file_get_block的一些调用)。serve_read只需要提供用于文件读取的RPC接口。查看serve_set_size中的注释和代码,以获得服务器函数应该如何构造的基本概念。

使用make grade来测试你的代码。你的代码应该传递"serve_open/file_stat/file_close"和"file_read",得分为70/150。

练习6

在fs/serv.c中实现serve_write,在lib/file.c中实现devfile_write。

使用make grade来测试你的代码。你的代码应该传递"file_write"、"file_read之后是file_write"、"open"和"large file",这样才能得到90/150的分数。

Spawning Processes

我们已经给出了spawn的代码(参见lib/spawn.c),它会创建一个新环境,从文件系统加载程序映像,然后启动运行这个程序的子环境。然后,父进程继续独立于子进程运行。spawn函数实际上就像UNIX中的一个fork,后面是子进程的一个立即执行程序。

我们实现了spawn而不是unix风格的exec,因为spawn更容易在用户空间以“外核方式”实现,而不需要内核的特别帮助。考虑一下在用户空间中实现exec需要做些什么,并确保你理解为什么它更难。

练习7

Spawn依赖于新的系统调用sys_env_set_trapframe来初始化新创建的环境的状态。在kern/syscall.c中实现sys_env_set_trapframe(不要忘记在syscall()中分派新的系统调用)。

运行kern/init.c中的user/spawnhello程序测试代码,它将尝试从文件系统派生/hello。

使用make grade来测试你的代码。

Sharing library state across fork and spawn

UNIX文件描述符是一个通用的概念,它还包括管道、控制台I/O等。在JOS中,每种设备类型都有一个对应的struct Dev,其中包含了指向实现读/写等功能的函数的指针。对于该设备类型。lib/fd在此基础上实现了通用的类unix文件描述符接口。每个struct Fd都表示其设备类型,lib/ Fd .c中的大多数函数只是将操作分发给适当的struct Dev中的函数。

lib/fd还在每个应用程序环境的地址空间中维护文件描述符表区域,从FDTABLE开始。该区域为应用程序可以一次性打开的每个MAXFD(当前为32)的文件描述符分配一个页面大小(4KB)的地址空间。在任何给定的时间,当且仅当对应的文件描述符正在使用时,才会映射特定的文件描述符表页。每个文件描述符在FILEDATA开始的区域中都有一个可选的“数据页”,设备可以选择使用它。

我们希望在fork和spawn之间共享文件描述符状态,但文件描述符状态保存在用户空间内存中。现在,在fork上,内存会被标记为写时复制(copy-on-write),因此状态会被复制而不是共享。(这意味着环境将无法在它们自己没有打开的文件中查找,管道将无法跨分支工作。)在刷出时,内存会被留在后面,根本不会被复制。(实际上,衍生环境一开始没有打开的文件描述符。)

我们将修改fork,以确定某些内存区域由“库操作系统”使用,并且应该始终共享。我们不会在某个地方硬编码一个区域列表,而是在页表项中设置一个未使用的比特位(就像fork中的PTE_COW比特位一样)。

我们在inc/lib.h中定义了一个新的PTE_SHARE位。该位是Intel和AMD手册中标记为“可供软件使用”的三个PTE位之一。我们将建立这样一个约定:如果页表项设置了该比特位,则在fork和spawn中,页表项都应该直接从父节点复制到子节点。请注意,这与在写入时标记它不同:如第一段所述,我们希望确保将更新共享到页面。

练习8

修改lib/fork.c中的duppage以遵循新的约定。如果页表项设置了PTE_SHARE位,则直接复制映射。(大家应该使用PTE_SYSCALL,而不是0xfff,来屏蔽页表项的相关比特位。0xfff也会获取访问过的比特位和脏比特位。)

同样,在lib/spawn.c中实现copy_shared_pages。它应该遍历当前进程中的所有页表项(就像fork那样),将所有设置了PTE_SHARE位的页映射复制到子进程中。

Use make run-testpteshare to check that your code is behaving properly. You should see lines that say "fork handles PTE_SHARE right" and "spawn handles PTE_SHARE right".

Use make run-testfdsharing to check that file descriptors are shared properly. You should see lines that say "read in child succeeded" and "read in parent succeeded".

The keyboard interface

为了让shell正常工作,我们需要一种方式来键入它。QEMU一直在显示我们写入到CGA显示器和串行端口的输出,但到目前为止,我们只在内核监视器中获取输入。在QEMU中,在图形窗口中输入的内容会显示为从键盘到JOS的输入,而在控制台输入的内容会显示为串口上的字符。Kern /console.c已经包含了键盘和串行驱动程序,这些驱动程序从实验1开始就由内核监视器使用,但是现在需要将它们连接到系统的其余部分。

练习9

在kern/trap.c中,调用kbd_intr来处理IRQ_OFFSET+ irq_kd,调用serial_intr来处理IRQ_OFFSET+IRQ_SERIAL。

我们在lib/console.c中实现了控制台输入/输出文件类型。Kbd_intr和serial_intr用最近读取的输入填充缓冲区,而控制台文件类型清空缓冲区(控制台文件类型默认用于stdin/stdout,除非用户重定向它们)。

运行make run-testkbd并输入几行代码来测试代码。当你完成你的台词时,系统会将你的台词反馈给你。如果控制台和图形窗口都可用,请尝试同时在二者中输入。

The Shell

运行make Run -icode或make Run -icode-nox。这将运行你的内核并启动user/icode。Icode执行init命令,将控制台设置为文件描述符0和1(标准输入和标准输出)。然后它会生成sh (shell)。你应该能够运行以下命令:

Echo hello world | cat

猫lorem |猫

猫lorem |num

Cat lorem |num |num |num |num |num

lsfd

注意,用户库例程cprintf直接打印到控制台,而不使用文件描述符代码。这对于调试很有用,但对于通过管道连接到其他程序就不好了。要将输出打印到特定的文件描述符(例如,1,标准输出),请使用fprintf(1, "…",…)。printf("…",…)是打印到fd1的快捷方式。以user/ lsfdc为例。

练习10

shell不支持I/O重定向。如果能运行sh <script,而不是像上面那样手动输入脚本中的所有命令,那就太好了。将<的I/O重定向添加到user/sh.c。

在shell中输入sh <script测试实现

运行make Run -testshell来测试你的shell。Testshell只是将上述命令(也可以在fs/ Testshell .sh中找到)提供给shell,然后检查输出是否匹配fs/ Testshell .key。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值