MIT6.828学习之Lab5_实验过程

IDE即Integrated Drive Electronics,它的本意是指把控制器与盘体集成在一起的硬盘驱动器,IDE是表示硬盘的传输接口。

Introduction

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

实验室这部分的主要新组件是位于新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'.最小的基于pio(非中断驱动)IDE驱动程序代码
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.EXS

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

The File System

这个实验室的目标不是让您实现整个文件系统,而是只实现某些关键部分。特别是,您将负责将块读入block cache并将它们刷新回磁盘;分配磁盘块;将文件偏移量映射到磁盘块;并在IPC接口中实现read, write, and open。因为您不会自己实现所有的文件系统,所以熟悉所提供的代码和各种文件系统接口是非常重要的。

Disk Access

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

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

x86处理器使用EFLAGS寄存器中的IOPL位来确定是否允许保护模式代码执行特殊的设备I/O指令,比如IN和OUT指令。由于我们需要访问的所有IDE磁盘寄存器都位于x86的I/O空间中,而不是内存映射,所以为了允许文件系统访问这些寄存器,我们只需要给文件系统环境提供“I/O privilege”。实际上,EFLAGS寄存器中的IOPL位为内核提供了一个简单的“all-or-nothing”(全有或全无)方法来控制用户模式代码是否可以访问I/O空间。在我们的示例中,我们希望文件系统environment能够访问I/O空间,但是我们根本不希望任何其他environment能够访问I/O空间。

Exercise 1. i386_init通过将类型ENV_TYPE_FS传递给环境创建函数env_create来标识文件系统环境。在env.c中修改env_create,以便它赋予文件系统environment I/O特权,但永远不要将该特权授予任何其他环境。
确保您可以启动文件环境,而不会导致General Protection fault。make grade时你应该通过“fs i/o” test。

这个地方代码还是比较简单的,毕竟之前为用户环境开中断也是设置的eflags的FL_IF位,这里就是设置eflags的IOPL位

//env.c/env_create()
e->env_type = type;
if(type == ENV_TYPE_FS){
	e->env_tf.tf_eflags |= FL_IOPL_MASK;
}

原来make grade是根据conf/lab.mk来判断对哪个lab进行make grade。我这次没有把lab.mk中lab 4相关代码删掉,结果它还是在对lab 4代码make grade

Question
1.当您随后从一个环境切换到另一个环境时,还需要做什么来确保正确地保存和恢复这个I/O特权设置吗?为什么?
答:不需要,当环境切换的时候,会根据环境的env_tf保存或恢复它的上下文,包括eflags,所以不需要额外的操作。

注意,这个lab中的GNUmakefile文件将QEMU设置为使用obj/kern/kernel.img文件作为disk 0的映像(通常是DOS/Windows下的“Drive C”)???并使用(新的)文件obj/fs/fs.img作为disk 1(“Drive D”)的映像。在这个lab中,我们的文件系统只接触disk 1;disk 0只用于引导内核。如果你破坏了其中任何一个磁盘镜像,你可以重置他们为最初的,“原始”的版本,只需键入:

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

or by doing:

$ make clean
$ make

在这里插入图片描述

The Block Cache

在我们的文件系统中,我们将在处理器的虚拟内存系统的帮助下实现一个简单的“buffer cache”(缓冲区缓存,真的就一个块缓存)。块缓存的代码在fs/bc.c中。

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

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

当然,将整个磁盘读取到内存中要花很长时间,所以我们以请求分页(demand paging)的形式实现,其中我们只在磁盘映射区域分配页和从磁盘读取相应的块来响应一个在这个地区发生的页面错误

Exercise 2. 在fs/bc.c中实现bc_pgfaultflush_block函数。bc_pgfault是一个页面错误处理程序,就像您在前面的lab中为copy-on-write fork编写的一样,只是它的工作是从磁盘加载页面来响应页面错误。写这些代码时,请记住(1)addr可能没有对齐到block边界,(2)ide_read是操作sectors而不是blocks。
如果需要,flush_block函数应该将一个块写到磁盘上。如果块甚至不在block cache中(也就是说,页面没有映射),或者它不是dirty,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”。

fs/fs.c中的fs_init函数是如何使用block cache的一个主要示例。在初始化块缓存之后,它简单将指向块缓存的指针存储到super全局变量中的磁盘映射区域。在这之后,我们可以简单地从super structure中读取,就像它们在内存中一样,并且我们的页面错误处理程序将根据需要从磁盘中读取它们。(这里的宾语都是指的block cache???)

这里最让我困惑的就是block与sector是怎么对应的,结果就只是blockno8=sectorno,因为blocksize=sectorsize8,之前说数据块是分散在磁盘上的,怎么就这么对上了?还有一点不懂为什么系统调用中envid参数直接设0就行?

bc_pgfault():
// Fault any disk block that is read in to memory by
// loading it from disk.只说从disk又不说disk哪个扇区
static void
bc_pgfault(struct UTrapframe *utf)
{
	void *addr = (void *) utf->utf_fault_va;
	uint32_t blockno = ((uint32_t)addr - DISKMAP) / BLKSIZE;
	int r;

	// Check that the fault was within the block cache region
	if (addr < (void*)DISKMAP || addr >= (void*)(DISKMAP + DISKSIZE))
		panic("page fault in FS: eip %08x, va %08x, err %04x",
		      utf->utf_eip, addr, utf->utf_err);

	// Sanity check the block number.
	if (super && blockno >= super->s_nblocks)
		panic("reading non-existent block %08x\n", blockno);

	// Allocate a page in the disk map region, read the contents
	// of the block from the disk into that page.
	// Hint: first round addr to page boundary. fs/ide.c has code to read
	// the disk.
	//
	// LAB 5: you code here:
	addr = (void *)ROUNDDOWN(addr, BLKSIZE);
	if((r=sys_page_alloc(0, addr, PTE_P | PTE_U | PTE_W))<0) //为什么这里可以用0?
		panic("in bc_pgfault,out of memory: %e", r);
	if((r=ide_read(blockno*8, addr, BLKSECTS))<0)
		panic("in bc_pgfault, ide_read: %e", r);
	// Clear the dirty bit for the disk block page since we just read the
	// block from disk
	if ((r = sys_page_map(0, addr, 0, addr, uvpt[PGNUM(addr)] & PTE_SYSCALL)) < 0)
		panic("in bc_pgfault, sys_page_map: %e", r);

	// Check that the block we read was allocated. (exercise for
	// the reader: why do we do this *after* reading the block
	// in?)
	if (bitmap && block_is_free(blockno))
		panic("reading free block %08x\n", blockno);
}
flush_block():
void
flush_block(void *addr)
{
	uint32_t blockno = ((uint32_t)addr - DISKMAP) / BLKSIZE;
	int r;
	if (addr < (void*)DISKMAP || addr >= (void*)(DISKMAP + DISKSIZE))
		panic("flush_block of bad va %08x", addr);
	
	// LAB 5: Your code here.
	addr = (void *)ROUNDDOWN(addr, BLKSIZE);
	if(va_is_mapped(addr) && va_is_dirty(addr)){
		if((r=ide_write(blockno*8, addr, BLKSECTS))<0)
			panic("in flush_block, ide_write: %e", r);
		if((r=sys_page_map(0, addr, 0, addr, uvpt[PGNUM(addr)] & PTE_SYSCALL))<0)
			panic("in flush_block, sys_page_map: %e", r);
	}
	return;
}

The Block Bitmap(位图)

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

Exercise 3. 使用free_block作为模型在fs/fs中实现alloc_block。它应该在位图中找到一个空闲磁盘块,标记它该磁盘块已被使用,并返回该磁盘块号。当您分配一个块时,您应该立即使用flush_block将更改后的位图块刷新到磁盘,以保持文件系统的一致性。
make grade时应该可以通过“alloc_block”测试

alloc_block():
int
alloc_block(void)
{
	// The bitmap consists of one or more blocks.  A single bitmap block
	// contains the in-use bits for BLKBITSIZE blocks.  There are
	// super->s_nblocks blocks in the disk altogether.
	
	// super->s_nblocks=3G/4K=BLKBITSIZE*24, 所以bitmap块最多可以有24块?
	// LAB 5: Your code here.
	uint32_t blockno;
	// 找到一个空闲块
	for(blockno=2; blockno<super->s_nblocks && !block_is_free(blockno); blockno++);
	if(blockno >= super->s_nblocks) //检查是否磁盘满了
		return -E_NO_DISK;
	bitmap[blockno/32] ^= 1<<(blockno%32); //将该空闲块设为used,0=used  位运算符^(“异或”)
	flush_block(&bitmap[blockno/32]); //将bitmap对应块刷新到磁盘,此时块可能是disk 1~24
	return blockno;
	panic("alloc_block not implemented");
	
}

File Operations

我们在fs/fs.c中提供了各种函数来实现基本的功能,您将需要这些功能来解释和管理File structure扫描和管理目录文件的条目,以及从root目录遍历文件系统以解析绝对路径名(absolute pathname)。阅读fs/fs.c中的所有代码,确保在继续之前理解每个函数的功能。

Exercise 4. 实现file_block_walkfile_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”测试。

就是搞不清出f->indirect里存的是块号还是地址?然后ppdiskbno指向什么?
free_block(f->f_indirect);并且free_block(uint32_t blockno),所以f->f_indirect存的是块号
Find the disk block number(块号!!!) slot(槽!)我们是要找到存放块号的那个槽
Set '*ppdiskbno' to point to that slot说明了*ppdiskbno存的是那个槽的地址

还有一点困惑的是,提示里说 but note that *ppdiskbno might equal 0 什么意思,有什么影响???
答:应该是说明可能该槽里还没有块号,对应的块还未被分配。实际使用我认为是在file_get_block里。很多同学在file_block_walk里写if(ppdiskbno) *ppdiskbno=…我认为是不对的。

file_block_walk():

找到存着文件f的第filebno块的块号的槽,将槽地址赋值给*ppdiskbno。注意:f->f_direct与f->f_indirect里存的都是块号

static int
file_block_walk(struct File *f, uint32_t filebno, uint32_t **ppdiskbno, bool alloc)
{
       // LAB 5: Your code here.
       int r;
	   if(filebno >= NDIRECT + NINDIRECT)
	   		return -E_INVAL;
	   		
	   if(filebno < NDIRECT){
	   		*ppdiskbno=&f->f_direct[filebno]; //把f->f_direct第filebno个槽的地址给它
			return 0; //这里忘记return了,难怪一直错
	   }
	   //cprintf("I'm in the file block walk\n");
	   if(f->f_indirect == 0){
	   		if(alloc==0)
				return -E_NOT_FOUND;
			if((r=alloc_block())<0)
				return -E_NO_DISK;
			f->f_indirect=r;
			memset(diskaddr(r), 0, BLKSIZE); 
			flush_block(diskaddr(r)); //每次对磁盘映射区域的块修改后都应该刷新回磁盘
	   }
	   //Find the disk block number(块号!!!) slot(槽!)
	   //捋一下,现在我们要的是存着f第filebno块块号的那个槽的地址
	   //即f->f_indirect与f->f_direct都是存着块号,而*ppdiskbno要的是存着块号的那个槽的地址
	   
	   // *ppdiskbno=(uint32_t *)(diskaddr(f->f_indirect)+filebno*4);
	   //那为什么不要乘4呢,因为本来uint32_t就是4个字节,如果是char类型才要乘4
	   //but note that *ppdiskbno might equal 0 什么意思,有什么影响???
	   //答:应该是说明该槽里还没有块号,对应的块还未被分配,在file_get_block里要用
	   
	   //*ppdiskbno=(uint32_t *)(diskaddr(f->f_indirect)+filebno-NDIRECT);这样写就错了
	   *ppdiskbno=(uint32_t *)diskaddr(f->f_indirect)+filebno-NDIRECT;
	   return 0;
       panic("file_block_walk not implemented");
}
file_get_block()

根据槽地址进一步将槽内块号对应的块的地址(即文件f的第filebno块的块地址)给*blk

int
file_get_block(struct File *f, uint32_t filebno, char **blk)
{
       // LAB 5: Your code here.
       int r;
	   uint32_t *ppdiskbno;
       /* 这里不需要判读filebno范围是因为file_block_walk里面会判断
       if(filebno<0 || filebno  >= NDIRECT + NINDIRECT)
	   		return -E_INVAL;*/
	   if((r=file_block_walk(f, filebno, &ppdiskbno, 1)<0))
	   		return r;
	   //ppdiskbno是f的第filebno块的块号所在的槽的地址
	   //blk要的是这个块映射到内存里的地址
	   
	   //就算是直接块也是有可能还未分配
	  // if(filebno < NDIRECT || *ppdiskbno)
	   //		*blk=(char *)(diskaddr(*ppdiskbno));
	   if(*ppdiskbno==0){
	   		int r;
			if((r=alloc_block())<0)
				return -E_NO_DISK;
			*ppdiskbno=r;
			memset(diskaddr(r), 0, BLKSIZE); //我写mommove也是失了智
			flush_block(diskaddr(r)); //每次对磁盘映射区域的块修改后都应该刷新回磁盘
			
	   }
	   *blk=diskaddr(*ppdiskbno);
	   return 0;
       panic("file_get_block not implemented");
}

The file system interface

既然我们已经在文件系统environment本身中拥有了必要的功能,那么我们必须让希望使用文件系统的其他environment也可以访问它。由于其他environment不能直接调用文件系统environment中的函数,所以我们将通过构建在JOS IPC机制之上的remote procedure call(远程过程调用)或者RPC、抽象来公开对文件系统环境的访问。从图形上看,下面是其他environment对 the file system server (比如read)的调用:

					  Regular env           FS env
				   +---------------+   +---------------+
				   |      read     |   |   file_read   |
				   |   (lib/fd.c)  |   |   (fs/fs.c)   |
				...|.......|.......|...|.......^.......|...............
				   |       v       |   |       |       | RPC mechanism
				   |  devfile_read |   |  serve_read   |
				   |  (lib/file.c) |   |  (fs/serv.c)  |
				   |       |       |   |       ^       |
				   |       v       |   |       |       |
				   |     fsipc     |   |     serve     |
				   |  (lib/file.c) |   |  (fs/serv.c)  |
				   |       |       |   |       ^       |
				   |       v       |   |       |       |
				   |   ipc_send    |   |   ipc_recv    |
				   |       |       |   |       ^       |
				   +-------|-------+   +-------|-------+
				           |                   |
				           +--------->---------+

虚线以下的所有内容都是从常规environment向文件系统environment发送一个读请求的机制。从一开始,read(我们lib/fd.c中提供的)工作在任何文件描述符上,并简单地分派到适当的device read function,在本例中是devfile_read(我们可以有更多的设备类型,比如pipes)。devfile_read实现了专门针对磁盘文件的读取。这个函数和lib/file.c中的其他devfile_*函数实现了FS操作的客户端,它们的工作方式大致相同,在request structure(保存在页面fsipcbuf中)中绑定参数,调用fsipc发送IPC请求,然后解包并返回结果。fsipc函数只处理向服务器发送请求和接收响应的常见细节。

The file system server代码可以在fs/server .c中找到。它在serve函数中循环,无休止地通过IPC接收请求,将该请求发送给适当的处理函数,并通过IPC将结果发回。在read示例中,service将分派给serve_read, serve_read将处理与读请求相关的IPC细节,比如解包request structure,最后调用file_read实际执行文件读取。

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

服务器也是通过IPC发回响应结果。我们使用32位字作为函数的返回代码。对于大多数RPCs,这就是它们返回的所有内容。FSREQ_READFSREQ_STAT也返回数据,它们只是将数据写入客户机发送请求的页面。无需在响应IPC中发送此页面,因为客户机与文件系统服务器一开始就共享此页面。同样,FSREQ_OPEN在响应中与客户端共享一个新的“Fd page”。我们将很快回到文件描述符页面???

Exercise 5. 在fs/ servlet .c中实现serve_read
serve_read的繁重工作将由fs/fs.c中已经实现的file_read来完成(反过来,它只是对file_get_block的一系列调用)。serve_read只需要提供用于文件读取的RPC接口。查看serve_set_size中的注释和代码,了解应该如何构造server函数
使用make grade测试代码。您的代码应该通过“serve_open/file_stat/file_close”和“file_read”,得分为70/150。

Exercise 6. 在fs/server .c中实现serve_write,在lib/file.c中实现devfile_write
Use make grade to test your code. Your code should pass “file_write”, “file_read after file_write”, “open”, and “large file” for a score of 90/150.

做这个得弄清楚这些概念:

  • regular进程访问文件的整个流程。
  • 在IPC通信过程中,fsipcbuf(客户端)与fsreq(服务端)共享页面。
  • 保存着open file基本信息的Fd page页面(在内存空间0xD0000000以上)
  • 服务端的私有结构体OpenFile
  • 设备结构体dev,设备有三种,devfile,devpipe,devcons
  • OpenFile->o_fileid跟OpenFile->o_fd->fd_file.id以及Fsipc->read->req_fileid的关系!
    devfile_read()里,fsipcbuf.read.req_fileid = fd->fd_file.id;这是客户端根据在0xD0000000以上的第fdnum个fd page的fd->fd_file.id告诉服务器端要读的是id为这个的文件。
    serve_open()里,o->o_fd->fd_file.id = o->o_fileid;这是服务器端将open file与它的Fd page对应起来。但是不知道为什么,我把两者都输出却总是不一样
首先来看一下整个read的流程
//inc/fd.h
struct Fd {
	int fd_dev_id;
	off_t fd_offset;
	int fd_omode;
	union {
		// File server files
		// 这应该就是目标文件id,在客户端赋值给了fsipcbuf.read.req_fileid
		struct FdFile fd_file; //struct FdFile {int id; };
	};
};

//fs/serv.c
struct OpenFile { //This memory is kept private to the file server.
	uint32_t o_fileid;	// file id。 The client uses file IDs to communicate with the server.
	struct File *o_file;	// mapped descriptor for open file应该是打开的那个文件的file pointer
	int o_mode;		// open mode
	struct Fd *o_fd;	// Fd page是一个专门记录着这个open file的基本信息的页面
};

//inc/fs.h
struct File {
	char f_name[MAXNAMELEN];	// filename
	off_t f_size;			// file size in bytes
	uint32_t f_type;		// file type

	// Block pointers.
	// A block is allocated iff its value is != 0.
	// 这里存的是块号还是块的地址?
	uint32_t f_direct[NDIRECT];	// direct blocks
	uint32_t f_indirect;		// indirect block

	// Pad out to 256 bytes; must do arithmetic in case we're compiling
	// fsformat on a 64-bit machine.
	// 扩展到256字节;必须做算术,以防我们在64位机器上编译fsformat。
	uint8_t f_pad[256 - MAXNAMELEN - 8 - 4*NDIRECT - 4];
} __attribute__((packed));	// required only on some 64-bit machines
lib/fd.c/read()

根据fdnum在内存空间0xD0000000以上找到一个struct Fd页面命名为fd,页面内保存着一个open file的基本信息。然后根据fd内的fd_dev_id找到对应设备dev,很明显这里是devfile,然后调用(*devfile->dev_read)(fd, buf, n)。该函数返回读到的字节总数

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
	    || (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);
}
lib/file.c/devfile_read()

通过IPC共享的页面上的union Fsipc中存储请求的参数。在客户端,我们总是在fsipcbuf共享页面。设置好fsipcbuf的参数,调用fsipc向服务器端发送read请求。请求成功后结果也是保存在共享页面fsipcbuf中,然后读到指定的buf就行。

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.read.req_fileid = fd->fd_file.id;//这个id就是指的当前位置?current position?
	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;
}
lib/file.c/fsipc()

这个函数就是负责跟文件系统server进程间通信的。发送请求并接受结果。这里有点困惑的是ipc_find_env(ENV_TYPE_FS);随便找个fs类型的env都行?因为只是需要借助它去访问磁盘文件而已?

static int fsipc(unsigned type, void *dstva)
{
	static envid_t fsenv;
	if (fsenv == 0)
		fsenv = ipc_find_env(ENV_TYPE_FS);

	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);
}
fs/serv.c/serve()

ipc_recv的返回值是32位字env_ipc_value,即fsipc里ipc_send过来的type,根据这个type判断进入哪个处理函数,这里很明显type==FSREQ_READ

void serve(void)
{
	uint32_t req, whom;
	int perm, r;
	void *pg;

	while (1) {
		perm = 0;
		req = ipc_recv((int32_t *) &whom, fsreq, &perm);
		if (debug)
			cprintf("fs req %d from %08x [page %08x: %s]\n",
				req, whom, uvpt[PGNUM(fsreq)], fsreq);

		// All requests must contain an argument page
		if (!(perm & PTE_P)) {
			cprintf("Invalid request from %08x: no argument page\n",
				whom);
			continue; // just leave it hanging...
		}

		pg = NULL;
		if (req == FSREQ_OPEN) {
			r = serve_open(whom, (struct Fsreq_open*)fsreq, &pg, &perm);
		} else if (req < ARRAY_SIZE(handlers) && handlers[req]) {
			r = handlers[req](whom, fsreq);
		} else {
			cprintf("Invalid request code %d from %08x\n", req, whom);
			r = -E_INVAL;
		}
		ipc_send(whom, r, pg, perm);
		sys_page_unmap(0, fsreq);
	}
}
fs/serv.c/serve_read()

首先找到ipc->read->req_fileid对应的OpenFile,然后调用file_read去读内容到ipc->readRet->ret_buf

int serve_read(envid_t envid, union Fsipc *ipc)
{
	struct Fsreq_read *req = &ipc->read;
	struct Fsret_read *ret = &ipc->readRet;
	struct OpenFile *o;
	int r;
	
	if (debug)
		cprintf("serve_read %08x %08x %08x\n", envid, req->req_fileid, req->req_n);

	// Lab 5: Your code here:
	// First, use openfile_lookup to find the relevant open file.
	// On failure, return the error code to the client with ipc_send.
	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; //then update the seek position这个才是位置!
	//req->req_fileid = o->o_fd->fd_file.id;
	//cprintf("o->o_file:%lld req->req_fileid:%lld o->o_fd->fd_file:%d\n",o->o_fileid, req->req_fileid,o->o_fd->fd_file.id);
	return r;
}
fs/fs.c/file_read()

将文件f从offset开始的count个字节读入buf中。但是count可能大于f->f_size-offset,那么最多也只能把文件剩余部分读出。

ssize_t
file_read(struct File *f, void *buf, size_t count, off_t offset)
{       
        int r, bn;
        off_t pos;
        char *blk;
 
        if (offset >= f->f_size)
                return 0;
        
        count = MIN(count, f->f_size - offset);
        
        for (pos = offset; pos < offset + count; ) {
        		//将f的第filebno块的虚拟地址存到blk中
                if ((r = file_get_block(f, pos / BLKSIZE, &blk)) < 0)
                        return r;
                bn = MIN(BLKSIZE - pos % BLKSIZE, offset + count - pos);
                memmove(buf, blk + pos % BLKSIZE, bn);
                pos += bn;
                buf += bn;
        }       

        return count;
}
练习6
serve_write()

同样先找到req->req_fileid对应的OpenFIle,然后将req->req_buf中req->req_n个字节的内容写到OpenFile的fd_offset处。
但是这里有个值得注意的地方,req_n可能大于req_buf的容量,所以req_n最多只能等于req_buf的大小,即一个PGSIZE。但是实际写入的内容可以少于请求写入的内容,这是允许的。

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.
        int r;
        struct OpenFile *o; 
        if ((r = openfile_lookup(envid, req->req_fileid, &o)) < 0)
                return r;
        // 多于的就扔掉,确实不太合理,感觉应该循环写入的
        int req_n = req->req_n > PGSIZE ? PGSIZE : req->req_n;
        if((r = file_write(o->o_file, req->req_buf, req_n, o->o_fd->fd_offset))<0)
                return r;
        o->o_fd->fd_offset += r;
        return r;
        panic("serve_write not implemented");
}
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;
        n = n > sizeof(fsipcbuf.write.req_buf) ? sizeof(fsipcbuf.write.req_buf):n;
        fsipcbuf.write.req_n = n;
        memmove(fsipcbuf.write.req_buf, buf, n);
        r = fsipc(FSREQ_WRITE, NULL); //error or success都在r中
        assert(r <= n);
        assert(r <= PGSIZE);
        return r;
        panic("devfile_write not implemented");
}           

Spawning Processes(衍生程序,派生程序)

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

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

Exercise 7. pawn依赖于新的系统调用sys_env_set_trapframe来初始化新创建环境的状态。在kern/syscall.c中实现sys_env_set_trapframe(不要忘记在syscall()中添加新的系统调用的分派)。
通过从kern/init.c运行user/spawnhello程序来测试代码,它将尝试从文件系统派生/hello。

static int
sys_env_set_trapframe(envid_t envid, struct Trapframe *tf)
{
	// LAB 5: Your code here.
	// Remember to check whether the user has supplied us with a good
	// address! 告诉你这里要检查了还不看???
	struct Env *e;
	int r  =envid2env(envid, &e, 1);
	if(r != 0)
		return r;//-E_BAD_ENV在这
	user_mem_assert(e, (const void *) tf, sizeof(struct Trapframe), PTE_U);
	/*下面这样是错的,提示明确说了tf也要被修改!
	e->env_tf = *tf;
	e->env_tf.tf_eflags |= FL_IF;
	e->env_tf.tf_eflags |= FL_IOPL_0;
	e->env_tf.tf_cs |= 3;*/
	tf->tf_eflags |= FL_IF;
	//e->env_tf.tf_eflags |= FL_IOPL_0;
	tf->tf_eflags &= ~FL_IOPL_MASK;         //普通进程不能有IO权限
	tf->tf_cs |= 3;
	e->env_tf = *tf;
	return 0; 
}
Sharing library state across fork and spawn

UNIX文件描述符是一个通用的概念,它还包括pipes, console I/O等。在JOS中,每种设备类型都有一个对应的struct Dev,带有指向为该类型实现的read/write等函数的指针lib/ fd.c在此基础上实现了通用的类unix文件描述符接口。每个struct Fd都指示其设备类型,lib/fd.c中的大多数函数只是简单地将操作分派给适当struct Dev中的函数

lib/fd.c还在每个应用程序环境的地址空间中维护从FDTABLE(0xD0000000)开始的 file descriptor table region。在这个区域每个struct Fd都保留着一个页。在任何给定时间,只有在使用相应的文件描述符时才映射特定的文件描述符表页。每个文件描述符在从FILEDATA开始的区域中都有一个可选的“data page”,设备可以使用这些“data page”。

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

我们将更改fork,以确定“library operating system”使用的内存区域应该总是共享的。我们将在页表条目中设置一个未使用的位,而不是在某个地方hard-code(硬编码)一个区域列表(就像我们在fork中使用PTE_COW位一样)。

我们在inc/lib.h中定义了一个新的PTE_SHARE位。这个位是三个PTE位之一,在 Intel and AMD manuals中被标记为“available for software use”。我们将建立这样一个约定:如果页表条目设置了这个位,那么PTE(这里PTE指什么???页表中的映射?)应该在fork和spawn时从父环境直接复制到子环境。注意,这与标记为copy-on-write不同:如第一段所述,我们希望确保共享页面的更新

Exercise 8. 更改lib/fork.c中的duppage以遵循新的约定。如果页表条目设置了PTE_SHARE位,只需直接复制映射。(应该使用PTE_SYSCALL,而不是0xfff来屏蔽页表条目中的相关位。0xfff也获取accessed的和dirty位。)
同样,在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”.

duppage()
if(uvpt[pn] & (PTE_SHARE)){
		r = sys_page_map(fu_id, (void *)addr, envid, (void *)addr, uvpt[pn] & PTE_SYSCALL);
		if(r!=0)
			return r;
	}
copy_shared_pages()
static int
copy_shared_pages(envid_t child)
{
	// LAB 5: Your code here.
	int r,i;
	for (i = 0; i < PGNUM(USTACKTOP); i ++){ 
	// uvpd、uvpt应该是个全局数组变量,但是数组元素对应的pde、pte具体是什么应该取决于lcr3设置的是哪个环境的内存空间
		if((uvpd[i/1024] & PTE_P) && (uvpt[i] & PTE_P) && (uvpt[i] & PTE_SHARE)){ //i跟pte一一对应,而i/1024就是该pte所在的页表
			if ((r = sys_page_map(0, PGADDR(i/1024, i%1024, 0), child,PGADDR(i/1024, i%1024, 0), uvpt[i] & PTE_SYSCALL)) < 0)
				return r;
		}
	}
	return 0;
	
}

The keyboard interface

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

Exercise 9. 在你的kern/trap.c,调用kbd_intr处理trap IRQ_OFFSET+IRQ_KBD,调用serial_intr处理trap IRQ_OFFSET+IRQ_SERIAL

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

通过运行make run-testkbd并键入几行代码来测试。系统应该在您写完行之后将您的输入返回给您。如果有可用的控制台和图形窗口,请同时在这两个窗口中输入。

//kern/trap.c/trap_dispatch()
if (tf->tf_trapno == IRQ_OFFSET + IRQ_KBD){
	kbd_intr();
	return;
} 
else if (tf->tf_trapno == IRQ_OFFSET + IRQ_SERIAL){
	serial_intr();
	return;
}

稍微看一下这两个函数

//kbd_proc_data()是从键盘读入a character就返回,如果没输入就返回-1
void kbd_intr(void){cons_intr(kbd_proc_data);} 

//serial_proc_data()很明显就是从串行端口读一个data
void serial_intr(void){
	if (serial_exists)
		cons_intr(serial_proc_data);
}

static void cons_intr(int (*proc)(void)) //将从键盘读入的一行填充到cons.buf
{
	int c;

	while ((c = (*proc)()) != -1) {
		if (c == 0)
			continue;
		cons.buf[cons.wpos++] = c;
		if (cons.wpos == CONSBUFSIZE)
			cons.wpos = 0;
	}
}

The Shell

Run 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

注意,用户库例程cprintf直接打印到控制台,而不使用文件描述符代码。这对于调试非常有用,但是对于piping into other programs却不是很有用。要将输出打印到特定的文件描述符(例如,1,标准输出),请使用fprintf(1, “…”, …)。 printf("…", …)是打印到FD 1的捷径。有关示例,请参见user/lsfd.c

Exercise 10. shell不支持I/O重定向。如果能运行sh <script就更好,而不是像上面那样手工输入script中的所有命令。将<的I/O重定向添加到user/sh.c
通过在shell中键入sh <script测试您的实现
运行make Run -testshell来测试您的shell。testshell只是将上面的命令(也可以在fs/testshell.sh中找到)提供给shell,然后检查输出是否匹配fs/testshell.key。

	case '<':	// Input redirection
		// Grab the filename from the argument list
		if (gettoken(0, &t) != 'w') {
			cprintf("syntax error: < not followed by word\n");
			exit();
		}
		// LAB 5: Your code here.
		if ((fd = open(t, O_RDONLY)) < 0) {
			cprintf("open %s for read: %e", t, fd);
			exit();
		}
		if (fd != 0) {
			dup(fd, 0); //应该是让文件描述符0也作为fd对应的那个open file的struct Fd页面
			close(fd);
		}
		//panic("< redirection not implemented");
		break;

遇到的bug

这几个bug卡了我好几天,真是值得在这里写一写。

Protection I/O space 测试一直过不了

在这里插入图片描述
后面发现是我sys_env_set_trapframe()里写错了,之前是下面这样写的(上面已更正),这样并未对tf进行修改,不过我还是觉得其实不修改tf也没事,反正后面child_tf也没再使用了,难道是我指针没搞明白,不修改tf也就无法修改e->env_tf?

//kern/syscall.c/sys_env_set_trapframe()
user_mem_assert(e, (const void *) tf, sizeof(struct Trapframe), PTE_U);
e->env_tf = *tf;
e->env_tf.tf_eflags |= FL_IF;
//e->env_tf.tf_eflags |= FL_IOPL_0;
tf->tf_eflags &= ~FL_IOPL_MASK;         //普通进程不能有IO权限
e->env_tf.tf_cs |= 3;

big file测试过不了

之前这样写的,后面把blockno从2开始就ok了

//fs/bc.c/alloc_block()
for(blockno=1; blockno<super->s_nblocks && !block_is_free(blockno); blockno++);

testshell 测试过不了

在这里插入图片描述
最坑了就是这个了,卡了我好几天,原来是Lab 4里的sys_ipc_try_send()逻辑写错了,难怪我怎么该Lab 5的代码都没用。。。我的if进入条件这样设置的话,那么有可能进入if时perm=0,这样就会映射一个perm为0的页面到e内存空间,肯定不行啦。

kern/syscall.c/sys_ipc_try_send()
e->env_ipc_perm=0;
if((uintptr_t)srcva <UTOP){
	```
	r=page_insert(e->env_pgdir, srcpp, e->env_ipc_dstva, perm);
}

自问自答

1.一个磁盘有多少扇区?
答:文件系统环境有个3GB的磁盘映射区域,而Lab 5中只去了一个磁盘disk 1或者disk 0。所以一个磁盘应该是由3GB/4KB=3* 2^18块磁盘块,一个磁盘块是8个扇区,所以一个磁盘应该有3* 2 ^21个扇区

2.disk跟disk map region是一一对应的吗?
答:在文件系统环境中我认为是的,在普通环境中肯定不是。

3.为什么要用blk要用char **类型?
答:blk是用来指向对应文件块的虚拟地址的。然后f=(struct File*)blk,struct File中很多元素是uint8_t、char的,所以blk设成char类型是合理的

4.什么叫dev?

在本例中是devfile_read(我们可以有更多的设备类型,比如pipes)

答:所以我认为设备其实指的应该是一种工具或者方式。在JOS中有devfile、devcons(console)、devpipe

5.seek position(请求位置)是什么?什么叫open file?

Read count bytes from f into buf, starting from seek position offset.
ssize_t file_read(struct File *f, void *buf, size_t count, off_t offset)

答: 所以我认为position指的是请求从读/写的目标文件开始读/写的位置,是一个偏移量,保存在fd->fd_offset
open file流程:

  1. 客户端首先在0xD0000000以上找到一个还未映射物理页的地址fd,然后传给server一个path
  2. server根据path打开或者创建一个open file,然后传回Fd page(存着被打开文件的基本信息)给客户端caller,映射在fd上

6.如果有多个进程同时开着一个open file,那fd->fd_offset到底记录哪个进程的读取偏移呢?
跟下面的问题11一样,由IPC机制决定了FS环境一次只能响应一个普通环境的请求

7.Sharing library state across fork and spawn这部分是在干嘛?
答:使父子环境共享文件描述符页以及每个文件描述符对应的数据页

8.为什么好多函数的envid_t参数总是设成0?
在envid2env()函数中有这样如下定义。所以设成0就e就默认是curenv

// If envid is zero, return the current environment.
   if (envid == 0) {
           *env_store = curenv;
           return 0;
   }

9.fsipcbuf是什么时候分配了物理页?
答:应该早就为每个普通环境的内存空间的fsipcbuf分配了物理页,应该不是所有环境的fsipcbuf共享同一物理页,不然同时请求文件读写会乱套。至于具体在哪里分配的,我没找到。。。

10.file_open是去磁盘打开,还是在磁盘在内存中的映射区域中寻找?
答:做到后面就很清晰了,是在映射区域中寻找,如果发生页面错误再通过bc_pgfault()函数从磁盘中读出来,如果对打开的文件进行了修改,一定要及时刷新回磁盘,保持一致性

11.如果有多个普通环境想修改一个磁盘File,怎么保证其内容的一致性?
答:普通环境想修改File,需要通过IPC机制借助文件系统环境实现,而IPC机制让文件系统环境一次只能接受一个send,所以很好的避免了同时修改同一文件的情况。只是感觉这样效率好像不高。

参考

谢谢 bysui
谢谢 Gatsby123

具体代码

见我的GItHUb

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值