天天看点

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
           
MIT6.828学习之Lab5_实验过程

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_pgfault

flush_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_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”测试。

就是搞不清出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_READ

FSREQ_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 测试一直过不了

MIT6.828学习之Lab5_实验过程

后面发现是我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 测试过不了

MIT6.828学习之Lab5_实验过程

最坑了就是这个了,卡了我好几天,原来是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