实验 5:文件系统、Spawn 和 Shell

lab通关记录

MIT-6.828实验通关记录

介绍

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

入门

1
2
$git checkout lab5
$git merge lab4

实验室这部分的主要新组件是文件系统环境,位于新的fs目录中。浏览此目录中的所有文件以了解所有新内容。此外,在userlib目录中有一些新的文件系统相关的源文件,

fs/fs.c mainipulates 文件系统的磁盘结构的代码。
fs/bc.c 一个简单的块缓存建立在我们的用户级页面错误处理设施之上。
fs/ide.c 最小的基于 PIO(非中断驱动)的 IDE 驱动程序代码。
fs/serv.c 使用文件系统 IPC 与客户端环境交互的文件系统服务器。
lib/fd.c 实现通用类 UNIX 文件描述符接口的代码。
lib/file.c 磁盘文件类型的驱动程序,作为文件系统 IPC 客户端实现。
lib/console.c 控制台输入/输出文件类型的驱动程序。
lib/ spawn.c spawn库调用的代码框架。

Exercise 1

首先我们要赋予文件系统I/O特权,作为实现文件系统的第一步。

在i386_init里调用env_create,因此取消这部分注释,并在mmu.h找到相应的I/O权限:

1
2
3
if (type == ENV_TYPE_FS) {
e->env_tf.tf_eflags |= FL_IOPL_MASK;
}

Exercise 2

我们的文件系统也拥有自己的虚拟地址空间,大小为3GB:从 0x10000000 ( DISKMAP) 到 0xD0000000 ( DISKMAP+DISKMAX)。例如,磁盘块 0 映射到虚拟地址 0x10000000,磁盘块 1 映射到虚拟地址 0x10001000,依此类推。由于磁盘本身比3GB大,所以这是一个并不是很完美的想法。

anyway,既然是作为拥有虚拟地址空间的一个env,我们同样要为文件系统环境提供了零拷贝的功能。当PGFLT产生时,我们从磁盘调入内存处理,而不是仅仅分配物理页置0。因此本exercise需要通过DMA方式来完成fs/bc.c中PGFLT处理和写入磁盘。

通过”fs.h”找到相关宏和函数,我们可以发现

1
2
BLKSIZE:PGSIZE
SECTSIZE:512B

bc_pgfault:读取一个磁盘块

1
2
3
4
addr = ROUNDDOWN(addr, PGSIZE);
sys_page_alloc(0, addr, PTE_W|PTE_U|PTE_P);
if ((r = ide_read(blockno * BLKSECTS, addr, BLKSECTS)) < 0)
panic("ide_read failed!\n");

flush_block:写入磁盘

1
2
3
4
5
6
addr = ROUNDOWN(addr, PGSIZE);//hint
if(!va_is_mapped(addr) || !va_is_dirty(addr))return;//hint
if((r = ide_write(blockno * BLKSECTS, addr, BLKSECTS)) < 0)
panic("flush_block: ide_write failed! %e\n", r);
if((r = sys_page_map(0,addr,0,addr, uvpt[PGNUM(addr)] & PTE_SYSCALL)) < 0)//hint
panic("flush_block: sys_page_map failed! %e\n",r);

Exercise 3

我们通过bitmap来访问,这里给出一些信息

1
2
3
4
5
uint32_t *bitmap//定义:4字节,bitmap实际就是bool数组,bitmap[0]就可以访问0-31的block使用情况

bitmap = disaddr(2):return (char*)(DISKMAP + blockno * BLKSIZE)//2的block映射为bitmap

bitmap[i/32] |= 1 << (blockno%32);//free block:0代表use

使用free_block作为一种模式来实现alloc_blockFS / fs.c,应在该位图找到一个免费的磁盘块,将其标记使用,并返回该块的数量。分配块时,应立即使用 将更改的位图块刷新到磁盘flush_block,以帮助文件系统一致性。

alloc_block

1
2
3
4
5
6
7
8
9
//check_bitmap保证了0,1,bitmap所在的块都已经使用
for(uint32_t i = 3; i < super->s_nblocks; ++i){
if(block_is_free(i)){
bitmap[i/32] &= ~(1 << (i%32));
flush_block(&bitmap[i/32]);
return i;
}
}
return -E_NO_DISK;

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”。

file_block_walkfile_get_block是文件系统的主力。例如,file_readfile_write需要 file_get_block来分配块和初始化。

给出文件File的信息

1
2
3
uint32_t f_direct[NDIRECT];//直接块pointer10个
NINDIRECT (BLKSIZE / 4)//非直接块pointer 1024个
uint32_t f_indirect;//非直接块的块号

file_block_walk

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
	//*ppdiskbno我们*之后得到块号,因此我们设置为存储块号的虚拟地址
//filebno是File里面block数组的索引
//注意这里块号和虚拟地址,File直接块里的索引区分
if(filebno >= NDIRECT + NINDIRECT)
return -E_INVAL;

if(filebno < NDIRECT){
*ppdiskbno = f->f_direct + filebno;
}
else{
if(alloc && (f->f_indirect == 0)){
int r;
if((r = alloc_block()) < 0)
return r;
memset(diskaddr(r), 0, BLKSIZE);
f->f_indirect = r;
}
else if(f->f_indirect == 0){
return -E_NOT_FOUND;
}
*ppdiskbno = ((uint32_t *)diskaddr(f->f_indirect)) + filebno - NDIRECT;//diskaddr return char*
}
return 0;
//在间接块分配时,我们将得到一个间接块里存储的块号 的地址,而我们filebno指向的那个块还没分配,暂时是0

file_get_block

1
2
3
4
5
6
7
8
9
10
11
12
13
//这里的二级指针意义是:当我修改一个变量时,我将传入一个指针,当我修改一个指针时,我将传入一个二级指针。
uint32_t *pdiskbno;
int r;
if((r = file_block_walk(f, filebno, &pdiskbno, 1)) < 0)
return r;
if(*pdiskbno == 0){//上文说的0
if((r = alloc_block()) < 0)
return r;
*pdiskbno = r;
}
*blk = (char *)diskaddr(*pdiskbno);
flush_block(*blk);//访问块先flush,或者分配块也先flush(空闲块虚拟地址对应的物理页不一定是空的???,或许只是无用功)
return 0;

至此,我们可以开始准备将分页管理和文件系统结合:通过文件系统环境来管理环境(包括自己:又当裁判又当球员)的内存和磁盘的映射。而我们将通过自定的PGFLT处理来很容易地:页表修改->磁盘写入来完成串行结合。

Exercise 5

由于其他环境无法直接调用文件系统环境中的函数,我们将通过构建在 JOS 的 IPC 机制之上的远程过程调用或 RPC 抽象公开对文件系统环境的访问。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
       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 |
| | | | ^ |
+-------|-------+ +-------|-------+
| |
+-------------------+

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

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

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

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

实现serve_readFS / serv.c

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 的分数。

给出有用的信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
//File
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.
uint8_t f_pad[256 - MAXNAMELEN - 8 - 4*NDIRECT - 4];
}

//OpenFile
struct OpenFile{
uint32_t o_field;//file id
struct File *o_file;//mapped descriptor for open file
int o_mode; //open mode
struct Fd *o_fd;//fd page
}

//called function
ssize_t file_read(struct File *f, void *buf, size_t count, off_t offset);
int openfile_lookup(envid_t envid, uint32_t fileid, struct OpenFile **po);
int file_write(struct File *f, const void *buf, size_t count, off_t offset);

serve_read

1
2
3
4
5
6
7
8
9
//模仿serve_set_size来进行openfile_lookup的调用参数传入
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;

Exercise 6

模仿已有的serve_read即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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.
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)
return r;
o->o_fd->fd_offset += r;
return r;
}

Exercise 7

spawn依靠新的系统调用 sys_env_set_trapframe来初始化新创建环境的状态。sys_env_set_trapframekern/syscall.c 中实现 (不要忘记在 中调度新的系统调用syscall())。

通过运行kern/init.c 中user/spawnhello程序来测试您的代码,该 程序将尝试从文件系统中生成/hello

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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!
int r;
struct Env *e;
if ((r = envid2env(envid, &e, 1)) < 0) {
return r;
}
tf->tf_eflags = FL_IF;
tf->tf_eflags &= ~FL_IOPL_MASK; //普通进程不能有IO权限
tf->tf_cs = GD_UT | 3;
e->env_tf = *tf;
return 0;
}

Exercise 8

变化duppage的lib / fork.c遵循新的约定。如果页表条目PTE_SHARE 设置了位,则直接复制映射。(您应该使用PTE_SYSCALL, not0xfff来屏蔽页表条目中的相关位。也可以0xfff 获取访问过的位和脏位。)

同样,copy_shared_pageslib/spawn.c 中实现。它应该遍历当前进程中的所有页表条目(就像fork 之前所做的那样),将任何PTE_SHARE设置了该位的页映射复制 到子进程中。

使用make run-testpteshare来检查你的代码是否正确行为。您应该会看到写着“ fork handles PTE_SHARE right ”和“ spawn handles PTE_SHARE right ”的行。

使用make run-testfdsharing检查文件描述符正确共享。您应该会看到写着“ read in child successfully ”和“ read in parent successfully ”的行。

Exercise 9

在您的kern/trap.c 中,调用kbd_intr处理陷阱 IRQ_OFFSET+IRQ_KBDserial_intr处理陷阱IRQ_OFFSET+IRQ_SERIAL

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

通过运行make run-testkbd并键入几行来测试您的代码。当您完成它们时,系统应该将您的台词回显给您。尝试在控制台和图形窗口中输入,如果两者都可用的话

copy_shared_pages

1
2
3
4
5
6
7
8
9
10
11
12
13
static int
copy_shared_pages(envid_t child)
{
// LAB 5: Your code here.
uintptr_t addr;
for (addr = 0; addr < UTOP; addr += PGSIZE) {
if ((uvpd[PDX(addr)] & PTE_P) && (uvpt[PGNUM(addr)] & PTE_P) &&
(uvpt[PGNUM(addr)] & PTE_U) && (uvpt[PGNUM(addr)] & PTE_SHARE)) {
sys_page_map(0, (void*)addr, child, (void*)addr, (uvpt[PGNUM(addr)] & PTE_SYSCALL));
}
}
return 0;
}

Exercise 10

shell 不支持 I/O 重定向。运行sh <script而不是像上面那样手动输入脚本中的所有命令会很好 。为 < to 添加 I/O 重定向 user/sh.c

通过sh <script在 shell 中键入内容来测试您的实现

运行make run-testshell以测试您的外壳。 testshell只是将上述命令(也可以在fs/testshell.sh 中找到 )输入 shell,然后检查输出是否与fs/testshell.key匹配。

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

代码在这:Source Code

Donate
  • Copyright: Copyright is owned by the author. For commercial reprints, please contact the author for authorization. For non-commercial reprints, please indicate the source.
  • Copyrights © 2020-2024 环烷烃
  • Visitors: | Views:

我很可爱,请我喝一瓶怡宝吧~

支付宝
微信