MIT6.828

tech2025-10-21  2

Lab 5: File system, Spawn and Shell

Introduction

本节中,我们将实现spawn,一个能够加载并运行硬盘上可执行文件的库调用,之后会扩招我们的jos内核和操作系统库使其能在控制台上运行shell,这些功能需要文件系统支持,本实验会介绍一个简单的读/写文件系统。

Getting Started

使用git获取并切换到分支lab5

athena% cd ~/6.828/lab athena% add git athena% git pull Already up-to-date. 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 Merge made by recursive. … athena%

合并后进入fs目录,浏览该目录下的所有文件,以了解所有新内容。 此外,在用户和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.

在合并代码之后,确保pingpong,primes,forktree等程序能够通过测试,在开始练习1前注释掉 kern/init.c的ENV_CREATE(fs_fs)和lib/exit.c中的close_all().

File system preliminaries

本节中,我们要使用的文件系统要比大多是真正的文件系统(包括xv6 unix)简单.但是它已经足够提供以下基本功能了:创建,读取,写入,删除在一个分级的目录结构体下组织的文件。

我们在开发的只是一个单用户操作系统,它提供了足够的保护来抓取BUG,但不能保护多个相互怀疑的用户相互攻击。因此,我们的文件系统不支持UNIX的文件所有权或权限概念。与大多数UNIX文件系统一样,我们的文件系统目前也不支持硬链接、符号链接、时间戳或特殊设备文件。

On-Disk File System Structure

大多数UNIX文件系统将可用的硬盘空间划分为两种类型的区域:i节点区域和数据区。UNIX文件系统会为每个文件分配一个i节点,i节点中保留了对应文件的关键元数据,例如文件状态属性和指向其数据块的指针。数据区被划分为更大的数据块(一般是8KB大小也可能更大),文件系统在其中存储文件数据和目录元数据。目录项包含文件名和指向i节点的指针。如果文件系统中的多个目录项指向同一文件的i节点,则该文件称为硬链接文件,因为我们的文件系统不支持硬链接,所以我们不需要该级别的间接寻址,因此可以方便的简化,我们的文件系统也根本不会使用索引节点,而只会在描述该文件的(唯一)目录项中存储文件(或子目录)的所有元数据。 从逻辑上说,文件和目录都由一系列的数据块组成,这些数据块可能离散地分布在硬盘的各个地方就像单个进程的虚拟地址空间所拥有的页在物理内存上离散分布一样。文件系统环境隐藏了块布局的细节,并提供了接口用于在文件内的任意偏移量处读/写一串字节。文件系统环境在内部处理对目录的所有修改,这是执行诸如创建和删除文件之类的操作的一部分。 我们的文件系统确实允许用户环境直接读取目录元数据(例如,通过read),这意味着用户环境可以自己执行目录扫描操作(例如,执行ls程序),而不必依赖于其他特殊调用。 这种目录扫描方法的缺点以及大多数现代UNIX变体操作系统不鼓励这样做的原因是,它使应用程序依赖于目录元数据的格式,从而难以更改文件系统的内部布局,目录元数据格式如果发生变化,那么读取其数据的应用程序代码相应地也要发生改变,至少得重新编译应用程序。

Sectors and Blocks

大多数磁盘无法按字节粒度执行读写操作,而是以扇区为单位执行读写操作。在JOS中,每个扇区均为512字节。文件系统实际上以块为单位分配并使用磁盘存储。注意两个术语之间的差别:扇区大小是磁盘硬件的属性,而块大小是使用磁盘的操作系统的基本分配单位。文件系统的块大小必须是基础磁盘的扇区大小的倍数。 xv6文件系统使用512字节的块大小,与基础磁盘的扇区大小相同。但是,大多数现代文件系统使用更大的块大小,因为管理存储空间的开销小的多,并且以较大的粒度管理存储更为有效。我们的文件系统将使用4096字节的块大小,可以方便地匹配处理器的页面大小。

Superblocks

文件系统通常会在硬盘上易于查找的位置保留一些块(如非常前或非常后的位置)来保存描述整个文件系统属性的元数据(例如块大小,硬盘大小,查找根目录所需元数据,文件系统上次装载的时间,文件系统上次检查错误的时间等等)。这些特殊的块被称为超级块。 我们的文件系统将只有一个超级块,在硬盘上是第1块,inc/fs.h中定义的super结构体描述了它的布局,第0块通常用于保存boot loaders和分区表,所以文件系统通常不会用到最起始的块。很多真正的文件系统会维护多个超级块,这些超级块在磁盘的几个间隔很宽的区域中复制,因此,如果其中一个超级块损坏,或者磁盘在该区域中出现介质错误仍然可以找到其它超级块并用来访问文件系统。 JOS的硬盘布局:

File Meta-data

在JOS的文件系统中,inc/fs.h中的file结构体描述了文件的元数据的布局。该元数据包括文件的名称。大小。类型(常规文件或目录)以及指向组成文件的块的指针。如上所述,我们没有索引结点,因此这些元数据存储在磁盘上的一个目录项中。与大多数真正的文件系统不同,为了简单起见,我们将使用这种文件结构来表示文件元数据,因为它同时出现在磁盘和内存中。 struct File中的f_direct数据包含存储文件前10个(NDIRECT)块的块号的空间,我们称之为文件的直接索引块。对于大小不到10*4096B=40KB的小文件,使用f_direct就够了。但是,对于较大的文件,我们分配一个额外的磁盘块,称为文件的间接索引块,以容纳4096/4=1024个额外的块号。 因此,我们的文件系统允许的文件大小最多为1034块,即超过4MB一点点。为了支持更大的文件,真正的文件系统通常还支持二级间接索引块和三级间接索引块。 file结构体:

Directories versus Regular Files

JOS文件系统中的file结构体可以用来表示常规文件或者目录,两者用file结构体中的type域进行区分,文件系统实际上用同一种方式来管理这两种类型的文件,除了它不解释与常规文件相关联的数据块的内容,而文件系统将目录文件的内容解释为描述目录内的文件和子目录的一系列结构。 文件系统中的超级块包含一个file结构体(struct Super中的root字段),它保存文件系统根目录的元数据。该目录文件的内容是一系列file结构体,描述位于文件系统根目录中的文件和目录。根目录中的任何子目录又可能包含更多表示子目录的file 结构体,依次类推。

The File System

本实验的目标不是让你实现整个文件系统,而是让你只实现某些关键组件。你将负责将数据块读入数据块缓存,并将其刷新回磁盘;分配磁盘块;将文件偏移量映射到磁盘块;以及在IPC接口中实现读、写和打开。因为你不会实现所有的文件系统,所以熟悉所提供的代码和文件系统接口是非常重要的。

Disk Access

JOS操作系统中的文件系统进程需要讷讷够访问磁盘,但是我们的内核中还未实现任何访问磁盘的功能,我们没有采用传统的“单块”操作系统策略,即在内核中添加一个集成开发环境磁盘驱动程序,以及允许文件系统访问它的必要系统调用,而是将集成开发环境磁盘驱动程序实现为用户级文件系统的一部分。我们需要修改内核,以便进行设置,使文件系统进程拥有实现磁盘访问所需的特权。 只要我们使用轮询,基于磁盘的可编程I/O访问,不使用硬盘中断就很容易在用户空间实现磁盘访问。也可以在用户态下实现中断驱动的设备驱动程序(例如L3和L4内核就是这么做的),但是这更加困难,因为内核必须现场设备中断,并将它们分派到正确的用户态进程。 x86处理器通过EFLAGS的IOPL位来决定保护模式下的代码是否有权限执行特殊的设备I/O指令(如IN和OUT指令)。因为我们需要访问的所有集成开发环境磁盘寄存器都位于x86的I/O空间,而不是内存映射的空间,所以为了允许文件系统访问这些寄存器,我们唯一需要做的事是给文件系统进程赋予“I/O特权”。实际上,EFLAGS寄存器中的IOPL位为内核提供了一种简单的“all-or-nothing”的方法来控制用户态代码是否可以访问I/O空间,但我们不希望任何其他进程访问I/O空间。 练习1:1386_init通过进程创建函数env_create传递的类型ENV_TYPE_FS来识别文件系统进程,修改env.c中的env_create,这样它就会给予文件系统进程I/O特权而不会给任何其他进程该特权。确保能够启动文件系统进程并且不发生常规保护错误,在make grade中应当能通过fs i/o的测试。

在env_create中加个判断,是文件系统进程时,将IOPL(访问I/O需要的特权等级)位修改为FL_IOPL_3(用户态进程可以访问)即可(这里修改标志位不能使用read_eflags和write_elfags,因为进程第一次被调度时恢复环境使用的trapframe是由env_alloc等函数初始化的,而非陷入内核态时构建的。):

if(ENV_TYPE_FS == env->env_type) { //设置文件系统进程的IO特权。 env->env_tf.tf_eflags |= FL_IOPL_3; }

问题 当你随后从一个进程切换到另一个进程时,是否还需要执行其他操作以确保正确保存和还原此I / O特权设置? 为什么? 答:不需要,进程切换时会构建/弹出trapframe来保存/恢复现场环境,因此文件系统进程的EFLAGS并不会应用到其他进程上。

测试通过: 请注意,本实验中的GNUmakefile文件将QEMU设置为使用文件obj / kern / kernel.img作为磁盘0的映像(在DOS / Windows下通常为“ Drive C”),并使用(新)文件 obj / fs / fs.img作为磁盘1(“驱动器D”)的映像。 在本实验中,我们的文件系统只能接触磁盘1; 磁盘0仅用于引导内核。 如果您设法以某种方式损坏任何一个磁盘映像,只需键入以下命令即可将它们重置为原始的“原始”版本:

$ rm obj/kern/kernel.img obj/fs/fs.img $ make

或者键入:

$ make clean $ make

The Block Cache

在我们的文件系统中,我们将在处理器的虚存系统的帮助下实现一个简单的缓冲区缓存(实际上就是一个块缓存)。块缓存的代码在fs/bc.c中。 我们的文件系统只能处理3GB以下的硬盘,我们在文件系统进程的地址空间中保留了3GB大小的固定区域,从0X10000000(DISKMAP)到0XD0000000(DISKMAP+DISKMAX),作为磁盘的“内存映射”版本。例如磁盘块0映射到虚拟地址0x10000000,磁盘块1映射到虚拟地址0x10001000,依次类推。fs/bs.c中的diskaddr函数实现了从磁盘块号到虚拟地址的转换(以及一些完整性检查) 因为我们的文件系统进程有其独立的虚拟地址空间,并且它要做的唯一工作就是实现文件访问,因此在文件系统进程的虚拟地址空间中保留这么大的区域是合理的。 当然,将整块硬盘读入内存会非常耗时,所以我们以请求分页的形式实现,我们只在磁盘映射区域分配页和从磁盘中读取相应的块来响应一个发生的页面错误。这种方式下,就像整个硬盘都在内存中一样。 练习2:实现fs/bc.c中的bc_pgfault和flush_block函数,bc_pgfault是一个页面错误处理函数就像先前实验中写过的copy-on-write fork一样,只是它的工作是从磁盘加载页面来响应页面错误。写这些代码时,需要注意两点:(1)addr可能没有对齐到block边界。(2)ide_read是操作sectors而不是blocks. 如果需要,flush_block函数应该将一个块写到磁盘上。如果块不在块缓存中(也就是说,页面没有被映射)或者脏位为0,那么flush_block不应该做任何事情。我们将使用虚拟机硬件来跟踪磁盘块自上次从磁盘读取或写入磁盘后是否被修改过。要查看一个块是否需要写入,我们可以只查看在uvpt项中是否设置了PTE_D脏位。(PTE_D位由处理器设置,以响应于对该页的写入)将块写入磁盘后,flush_block应该使用sys_page_map清除PTE_D位。 使用make grade测试代码,代码应该通过check_bc, check_super和check_bitmap. 代码比较简单,按注释一步步写就行了,注意fs.c中不能使用curenv,page_alloc这样的内核代码。

bc_pgfault:

int r; addr = ROUNDDOWN(addr,PGSIZE); //地址按页面大小向下对齐 if(va_is_mapped(addr)&&va_is_dirty(addr)) //当页面在缓存中且页面被修改过时调用该函数才写入磁盘 { if((r = ide_write(blockno*BLKSECTS, addr, BLKSECTS))) //写入一块到磁盘中 panic("flush_block:ide_write failed - %e\n",r); if((r = sys_page_map(0,addr,0,addr,uvpt[PGNUM(addr)]&PTE_SYSCALL)))//清除脏位 panic("flush_block:sys_page_map failed - %e\n",r); }

flush_block:

int r; addr = ROUNDDOWN(addr,PGSIZE); //地址按页面大小向下对齐 if(va_is_mapped(addr)&&va_is_dirty(addr)) //当页面在缓存中且页面被修改过时调用该函数才写入磁盘 { if((r = ide_write(blockno*BLKSECTS, addr, BLKSECTS))) //写入一块到磁盘中 panic("flush_block:ide_write failed - %e\n",r); if((r = sys_page_map(0,addr,0,addr,uvpt[PGNUM(addr)]&PTE_SYSCALL)))//清除脏位 panic("flush_block:sys_page_map failed - %e\n",r); }

测试通过:

fs / fs.c中的fs_init函数示范了如何使用块缓存。 初始化块高速缓存后,它仅将指针存储到超级全局变量中的磁盘映射区域中。 此后,我们可以简单地从上层结构中读取它们,就像它们在内存中一样,并且页面错误处理程序将根据需要从磁盘中读取它们。

The Block Bitmap

在fs_init函数设置位示图指针后,我们可以把位示图看做一个压缩后的位数组,每位对应硬盘中的一块,例如block_is_free函数中就通过检查位示图块中的每位来判断对应块是否是空闲的。 练习3:参考free_block实现fs/fs.c中的alloc_block函数,该函数应当在位示图块中找到一个空闲的磁盘块,然后将其标记为已使用,并返回该块的块号,当分配一块时,你应该立即使用flush_block来将修改过的块写入磁盘,以使文件系统保持一致性。 使用make grade来测试代码.代码应当通过alloc_block的测试。 block_is_free:

bool block_is_free(uint32_t blockno) { if (super == 0 || blockno >= super->s_nblocks) return 0; if (bitmap[blockno / 32] & (1 << (blockno % 32))) return 1; return 0; }

由该函数可知位示图块可以看做一个32列,super->nblocks/32行的数组,列号从右往左递增,行号从上往下递增,0代表已使用,1代表空闲。 了解清楚位示图块的布局后,alloc_block写起来也比较简单:

int r = -E_NO_DISK; //遍历位示图块 for(int block_number=2;block_number<=super->s_nblocks;block_number++) { //如果该块空闲,则分配该块并标记为已使用,并将该块内容写入到硬盘上 if(block_is_free(block_number)) { bitmap[block_number/32] &= ~(1<<(block_number%32)); flush_block(diskaddr(block_number)); r = block_number; break; } } return r;

测试通过: 写代码还是不够小心,写完这个函数测试make grade连练习2的测试都过不去了,找BUG找了半天才发现不知道啥时候在bc_pgfault的if判断后面加了个分号,导致直接panic了。。。

File Operations

我们在fs/fs.c中提供了各种函数来实现基本的功能,你将需要这些功能来解释和管理File结构体、扫描和管理目录文件的目录项,以及从root目录遍历文件系统以解析绝对路径名(absolute pathname)。阅读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”测试。 写代码之前,一定要通读fs.c中的源码,了解file_block_walk及file_get_block的功能和用法。 file_block_walk:

//根据文件的逻辑块号获得其对应的物理块号所在的地址,并保存到*ppdiskbno中 //当逻辑块号大于等于NDIRECT时,该函数还承担分配间接索引块的工作 static int file_block_walk(struct File *f, uint32_t filebno, uint32_t **ppdiskbno, bool alloc) { // LAB 5: Your code here. //panic("file_block_walk not implemented"); if(filebno>=NDIRECT+NINDIRECT||filebno<0) return -E_INVAL; if(filebno<NDIRECT) { *ppdiskbno = &f->f_direct[filebno]; return 0; } if(!f->f_indirect && !alloc) return -E_NOT_FOUND; if(!f->f_indirect) //文件无间接索引块且alloc为1则分配一个间接索引块 { int r = alloc_block(); if(r<0) //硬盘中无可用块 return r; f->f_indirect = r; memset(diskaddr(r),0,BLKSIZE);//分配一个空闲块后,应将其读入缓存并将空闲块中数据清空再写回硬盘。 flush_block(diskaddr(r)); } uint32_t* inblock_addr = (uint32_t*)(diskaddr(f->f_indirect)); //读取间接索引块的内容 // *ppdiskbno = &inblock_addr[filebno - NDIRECT]; *ppdiskbno = inblock_addr + (filebno-NDIRECT); //获得逻辑块号对应的物理块号所在的地址 return 0; }

file_get_block:

//根据文件的逻辑块号获得其在文件系统进程中虚拟地址空间中映射的地址 //当文件的逻辑块不存在对应的物理块时,还承担为其分配物理块的工作。 //该函数用法可以参考file_write int file_get_block(struct File *f, uint32_t filebno, char **blk) { // LAB 5: Your code here. //panic("file_get_block not implemented"); uint32_t* p_bno; int r = file_block_walk(f, filebno, &p_bno, 1); if(r) return r; if(0 == (*p_bno)) //为未分配的块分配一个物理块 { r = alloc_block(); if(r<0) return r; *p_bno = r; memset(diskaddr(r),0,BLKSIZE);//清空物理块数据 flush_block(diskaddr(r)); } *blk = diskaddr(*p_bno); return 0; }

刚写完代码测试时老是通不过,找了半天发现错在对alloc_block的返回值的判断处,因为JOS中很多函数的返回值不是0就是负数,所以很多判断的地方为了省事,我都使用的是if®而非if(r<0),结果就在判断alloc_block的返回值时出错了。。。上述代码还有一个要注意的点是在分配一个空闲块后一定要记得清除空闲块中原有的数据,防止读到其他文件残留的垃圾数据。 测试通过:

The file system interface

现在文件系统进程内部有了有了必要的函数,我们必须让其他希望能够使用文件系统的进程能够访问到这些函数,因为其他进程不能直接调用文件系统进程内部的函数,所以我们将通过构建在JOS IPC机制之上的远程过程调用(RPC)、抽象来公开对文件系统环境的访问。从图形上看,下面是其他进程对文件系统服务(比如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进行编号,就像对系统调用进行编号一样),并将请求的参数存储在联合Fsipc上通过IPC共享的页面。在客户端,我们总是在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_get_block的一堆调用)完成。 serve_read只需提供RPC接口即可读取文件。 查看serve_set_size中的注释和代码,以大致了解服务器功能的结构。使用make grade测试您的代码。 您的代码应通过“ serve_open / file_stat / file_close”和“ file_read”,得分为70/150。

首先我们得过一遍用户进程调用read的过程: lib/fd.c/read中首先查找文件描述符和设备id是否合法,再看打开文件时的模式(调用open时会设置o_mode)是否允许读,如果允许则调用dev_read:

ssize_t read(int fdnum, void *buf, size_t n) { int r; struct Dev *dev; struct Fd *fd; if ((r = fd_lookup(fdnum, &fd)) < 0 //查找对应文件描述符和设备id是否存在 || (r = dev_lookup(fd->fd_dev_id, &dev)) < 0) return r; if ((fd->fd_omode & O_ACCMODE) == O_WRONLY) { //打开文件的模式是只写,那么不能进行读操作 cprintf("[%08x] read %d -- bad mode\n", thisenv->env_id, fdnum); return -E_INVAL; } if (!dev->dev_read) return -E_NOT_SUPP; return (*dev->dev_read)(fd, buf, n); }

devfile_read中会将请求相关数据打包到fspicbuf中,然后调用fsipc:

static ssize_t devfile_read(struct Fd *fd, void *buf, size_t n) { // Make an FSREQ_READ request to the file system server after // filling fsipcbuf.read with the request arguments. The // bytes read will be written back to fsipcbuf by the file // system server. int r; //打包请求相关数据到fsipcbuf fsipcbuf.read.req_fileid = fd->fd_file.id; //设置req_fileid为fd在打开文件表中的索引 fsipcbuf.read.req_n = n; //要读取的字节数 if ((r = fsipc(FSREQ_READ, NULL)) < 0) return r; assert(r <= n); assert(r <= PGSIZE); memmove(buf, fsipcbuf.readRet.ret_buf, r); //将返回的数据移到指定缓冲区中 return r; }

而fsipc在查找到文件系统进程id后,向文件系统进程发送文件操作请求,然后等待接收数据:

static int fsipc(unsigned type, void *dstva) { static envid_t fsenv; if (fsenv == 0) fsenv = ipc_find_env(ENV_TYPE_FS); //查找文件系统进程id static_assert(sizeof(fsipcbuf) == PGSIZE); if (debug) cprintf("[%08x] fsipc %d %08x\n", thisenv->env_id, type, *(uint32_t *)&fsipcbuf); ipc_send(fsenv, type, &fsipcbuf, PTE_P | PTE_W | PTE_U); //向文件系统进程发送文件操作请求 return ipc_recv(NULL, dstva, NULL); //等待接收数据 }

文件系统进程在接收到数据后,serve函数会根据ipc_recv的返回值(即ipc_send中代入的type)调用到对应的serve_read,然后调用file_read读取文件数据并保存到readRet的缓冲区中。 serve_read函数具体实现如下:

int serve_read(envid_t envid, union Fsipc *ipc) { struct Fsreq_read *req = &ipc->read; struct Fsret_read *ret = &ipc->readRet; if (debug) cprintf("serve_read %08x %08x %08x\n", envid, req->req_fileid, req->req_n); // Lab 5: Your code here: struct OpenFile *o; int r; if((r = openfile_lookup(envid, req->req_fileid, &o)) < 0) //查找请求读取的文件对象的打开文件表项是否存在 return r; if((r = file_read(o->o_file,ret->ret_buf, req->req_n, o->o_fd->fd_offset))<0) //读取文件数据 return r; o->o_fd->fd_offset += r; //更新文件当前偏移量 return r; }

make grade测试:

注意:打开文件表项和fd page是一一对应的,参考serve_init函数 练习6.在fs / serv.c中实现serve_write,在lib / file.c中实现devfile_write。使用make grade测试您的代码。 您的代码应通过“ file_write”,“ file_write之后的file_read”,“ open”和“大文件”,得分为90/150。 这两个函数的实现可以参考devfile_read和serve_read,比较简单。具体代码如下: devfile_write:

static ssize_t devfile_write(struct Fd *fd, const void *buf, size_t n) { // Make an FSREQ_WRITE request to the file system server. Be // careful: fsipcbuf.write.req_buf is only so large, but // remember that write is always allowed to write *fewer* // bytes than requested. // LAB 5: Your code here int r; fsipcbuf.write.req_fileid = fd->fd_file.id; //设置req_fileid为fd在打开文件表中的索引 fsipcbuf.write.req_n = MIN(sizeof(fsipcbuf.write.req_buf),n); //写入字节数不能超过req_buf memmove(fsipcbuf.write.req_buf,buf,fsipcbuf.write.req_n); if((r = fsipc(FSREQ_WRITE, NULL))<0) return r; assert(r <= n); assert(r <= PGSIZE); return r; //panic("devfile_write not implemented"); }

serve_write:

int serve_write(envid_t envid, struct Fsreq_write *req) { if (debug) cprintf("serve_write %08x %08x %08x\n", envid, req->req_fileid, req->req_n); // LAB 5: Your code here. //panic("serve_write not implemented"); struct OpenFile *o; int r; if((r = openfile_lookup(envid,req->req_fileid,&o))<0) //查找请求写入的文件的打开文件表项是否存在 return r; if((r = file_write(o->o_file,req->req_buf,req->req_n,o->o_fd->fd_offset))<0) //将req->req_buf中的数据写入文件 return r; o->o_fd->fd_offset += r; return r; }

make grade测试通过:

Spawning Processes

lib / spawn.c中已经给出了spawn的代码,该代码创建一个新进程,将文件系统中的程序映像加载到其中,然后启动运行该程序的子进程。 然后,父进程将独立于子进程继续运行。 在UNIX中,spawn函数的作用类似于fork后,让子进程中立即执行exec。 我们实现了spawn而不是UNIX风格的exec,因为spawn更容易从用户空间以“ 外内核方式”实现,而无需内核的特殊帮助。 考虑一下要在用户空间中实现exec所必须执行的操作,为什么这样做更难? 答:因为fork后,子进程已经建立了对应的虚拟地址空间,exec要将程序装入子进程的虚拟地址空间首先还得解除已有的映射关系。

练习7. spawn依赖新的系统调用sys_env_set_trapframe来初始化新创建的环境的状态。 在kern / syscall.c中实现sys_env_set_trapframe(不要忘记在syscall函数(kern/syscall.c)中添加新的系统调用)。运行kern / init.c中的user / spawnhello程序来测试您的代码,该程序将尝试从文件系统中生成/ hello。使用make grade来测试代码.

spawn.c的注释给的很详细,通读一遍,很容易能了解它的思路和工作流程。思路和流程大概是这样的:

1.我们先前实现了文件系统,所以在父进程中能够通过read等命令对硬盘上的可执行文件进行读取。 2. 将文件读取到内存并检查其合法性后,调用sys_exofork创建一个子进程 3. 父进程为子进程初始化堆栈,并调用sys_page_map为子进程建立堆栈和物理页间的映射关系并解除自己与物理页的映射 4. 根据读取的ELF文件头设置子进程的eip等相关参数 5. 根据Proghdr对每个段进行分类处理,可写段每个进程都要分配独立的物理页,只读段可以共享 6. 调用sys_env_set_trapframe为子进程设置相关权限 7. 调用sys_env_set_status将子进程状态设置为ENV_RUNNABLE,使其有机会上处理机运行。

sys_env_set_trapframe按注释要求写就行了,比较简单:

static int sys_env_set_trapframe(envid_t envid, struct Trapframe *tf) { // LAB 5: Your code here. // address! //panic("sys_env_set_trapframe not implemented"); int r; struct Env* env; //检查ENV是否存在且当前进程有权限获取该进程id if((r = envid2env(envid,&env,1))<0) return r; // Remember to check whether the user has supplied us with a good //检查代入的tf参数是否合法 user_mem_assert(env,tf,sizeof(struct Trapframe),PTE_U); tf->tf_cs |= 3;//CPL 3 tf->tf_eflags &= ~FL_IOPL_3; //设置IOPL为0 tf->tf_eflags |= FL_IF; //开中断 env->env_tf = *tf; return 0; }

make grade测试:

Sharing library state across fork and spawn

UNIX文件描述符是一个通用概念,还包含管道,控制台I / O等。在JOS中,这些设备类型中的每一个都有对应的struct Dev,以及指向实现读/写/等功能的指针。针对该设备类型。 lib / fd.c在此之上实现了通用的类似UNIX的文件描述符接口。每个struct Fd都指示其设备类型,并且lib / fd.c中的大多数函数只是将操作分派给相应的struct Dev中的函数。

lib / fd.c还从FDTABLE开始在每个应用程序进程的地址空间中维护文件描述符表区域。该区域为应用程序可以同时打开的最多MAXFD(当前为32个)文件描述符中的每个文件保留一个页面的地址空间(4KB)。在任何给定时间,当且仅当使用相应的文件描述符时,才会映射特定的文件描述符表页面(映射的文件描述符页面与文件系统进程的文件描述符页面共享同一个物理页)。每个文件描述符在从FILEDATA开始的区域中还具有一个可选的“数据页”,设备可以选择使用它们。

我们希望跨fork和spawn共享文件描述符状态,但是文件描述符状态保留在用户空间内存中。现在,在fork时,内存将被标记为写时复制,因此状态将被复制而不是共享。 (这意味着进程将无法在自身未打开的文件中进行搜索,并且管道将无法在fork上工作,我的理解是复制了新页导致子进程的文件描述符页面不再与文件系统进程的文件描述符页面共享更新),在spawn时,内存根本不会被复制。 (spawn创建的子进程在开始时没有打开的文件描述符。)

我们将更改fork,以确定“library operating system”使用的内存区域应该总是共享的。我们将在页表条目中设置一个未使用的位,而不是在某个地方硬编码一个区域列表(就像我们在fork中使用PTE_COW位一样)。 我们在inc / lib.h中定义了一个新的PTE_SHARE位。该位是Intel和AMD手册中标记为“可用于软件使用”的三个PTE位之一。我们将建立一个约定,如果页表条目设置了该位,则应该在fork和spawn中将PTE直接从父进程复制到子进程。请注意,这与将其标记为“写时复制”不同:如第一段所述,我们要确保共享页面更新。

练习8.在lib / fork.c中更改duppage以遵循新约定。如果页表项设置了PTE_SHARE位,则只需直接复制映射。 (应该使用PTE_SYSCALL而不是0xfff来掩盖页表条目中的相关位。0xfff也会设置访问位和脏位。) 同样,在lib / spawn.c中实现copy_shared_pa​​ges。它应该遍历当前进程中的所有页表条目(就像fork一样),将所有已设置PTE_SHARE位的页映射复制到子进程中。 键入make run-testpteshare来检查代码是否正确,正确时应该会看到如下两行信息:

“fork handles PTE_SHARE right” “spawn handles PTE_SHARE right”.

代码如下: duppage:

duppage(envid_t envid, unsigned pn) //复制映射关系 { // LAB 4: Your code here. //panic("duppage not implemented"); int err; int curEnvId = sys_getenvid(); //如果该页是可写页或COW页且该页不共享 if(uvpt[pn]&(PTE_W|PTE_COW) && !(uvpt[pn]&PTE_SHARE)) //lab5练习8修改处 { //复制映射关系,并标记子进程中该页为COW页 err = sys_page_map(curEnvId,(void*)(pn*PGSIZE),envid,(void*)(pn*PGSIZE),PTE_P|PTE_U|PTE_COW); if(err<0) //panic("duppage:sys_page_map failed when copy mappings from W/COW page - %e\n", err); return err; //标记父进程为COW页 err = sys_page_map(curEnvId,(void*)(pn*PGSIZE),curEnvId,(void*)(pn*PGSIZE),PTE_P|PTE_U|PTE_COW); if(err <0) return err; } else //该页是只读页或共享页 { int perm = uvpt[pn]&PTE_SHARE ? uvpt[pn]&PTE_SYSCALL : PTE_P|PTE_U; //lab5练习8修改处 err = sys_page_map(curEnvId,(void*)(pn*PGSIZE),envid,(void*)(pn*PGSIZE),perm); //err = sys_page_map(curEnvId,(void*)(pn*PGSIZE),envid,(void*)(pn*PGSIZE),(uvpt[n]<<20)>>20); if(err <0) //panic("duppage:sys_page_map failed when copy mappings from R-ONLY page - %e\n", err); return err; } return 0; }

copy_shared_pages:

static int copy_shared_pages(envid_t child) { // LAB 5: Your code here. for(int s = 0;s*PGSIZE<USTACKTOP;s++) { //将共享页的映射关系复制到子进程中 //uvpd = 0x3bd<<22|0x3bd<<12 uvpt = 0x3bd<<22; if((uvpd[(s>>10)]&PTE_P)&&(uvpt[s]&PTE_P) && (uvpt[s]&PTE_SHARE)) { //srcEnvid使用sys_getenvid或者0都可以,envid2env中会做判断,代入id是0时,会获取curenv int r = sys_page_map(0,(void*)(s*PGSIZE),child,(void*)(s*PGSIZE),uvpt[s]&PTE_SYSCALL); if(r < 0) return r; } } return 0; }

测试通过:

键入make run-testfdsharing来检查文件描述符是否被正确的共享了. 正确时应当会看到如下两行信息:

“read in child succeeded” “read in parent succeeded”.

测试通过:

The keyboard interface

要让shell工作,我们需要一种方法来键入它。QEMU一直在显示我们写入到CGA显示器和串行端口的输出,但到目前为止,我们只在内核监视器中接受输入。在QEMU中,在图形化窗口中键入的输入显示为从键盘到JOS的输入,而在控制台中键入的输入显示为串行端口上的字符。kern/console.c已经包含了自lab1以来内核监视器一直使用的键盘和串行驱动程序,但是现在需要将它们附加到系统的其他部分。

练习9. 在kern/trap.c中,调用kbd_intr处理trap IRQ_OFFSET+IRQ_KBD, 调用serial_intr处理IRQ_OFFSET+IRQ_SERIAL. 我们在lib/console.c中实现控制台输入/输出文件类型。kbd_intr和serial_intr用最近读取的输入填充缓冲区,而控制台文件类型耗尽缓冲区 (控制台文件类型默认用于stdin/stdout,除非用户重定向它们) 通过运行make run-testkbd并键入几行代码来测试。系统应该在您键入行之后将您的输入返回给您。如果有可用的控制台和图形窗口,请同时在这两个窗口中输入。 代码如下:

// LAB 5: Your code here. if(tf->tf_trapno == IRQ_OFFSET + IRQ_KBD) { kbd_intr(); return; } if(tf->tf_trapno == IRQ_OFFSET + IRQ_SERIAL) { serial_intr(); return; }

The Shell

运行make run-icode or make run-icode-nox。这将运行内核并执行用户程序user/icode。icode执行init,它将把控制台设置为文件描述符0和1(标准输入和标准输出)。然后它会spawn创建子进程执行sh,也就是shell。你应该能运行以下命令:

echo hello world | cat cat lorem |cat cat lorem |num cat lorem |num |num |num |num |num lsfd

Exercise 10. shell不支持I/O重定向。运行sh <script会很好,而不必像上面那样手动在脚本中输入所有命令。在user/sh.c添加<的IO重定向功能. 运行make run-testshell来测试shell,test只是将上面的命令(可以在fs/testshell.sh中找到)提供给shell,然后检查输出是否匹配fs/testshell.key. 代码按注释写就行了,比较简单:

// LAB 5: Your code here. //panic("< redirection not implemented"); int fd = open(t,O_RDONLY); if(fd<0) { cprintf("case <:open err - %e\n",fd); exit(); } else if(fd) { dup(fd,0); //将fd与其struct fd的映射关系复制到fd 0处 close(fd);//关闭fd } break;

执行make run-testshell测试,输出是没问题的

但是make grade却老是死在超时这里,不知道问题出在哪。。。 在testshell中加了调试代码,testshell.out和testshell.key的内容也完全一样呀: 调试代码:

输出:

不过比较testshell.out和testshell.key的过程确实耗时很久,是不是哪个地方出了问题导致read耗时变长了呢?比如频繁发生页面中断导致从磁盘中调页? 具体问题出在哪还有待以后排查。总之Lab5到这就先告一段落了。

最新回复(0)