Lv的杂货铺

我们都是阴沟里的虫子,但总还是得有人仰望星空

0%

Lab 5: File system, Spawn and Shell

lab5将要实现文件系统的一部分,整个文件系统的代码骨架已经给出了,我们需要的只是补充其中的一小部分。

Exercise 1

在JOS中,文件系统是作为一个用户进程运行,它进行磁盘读写,其他进程通过IPC来与文件系统交互。在i386中,处理器利用EFLAGS寄存器的IOPL位来表示是否允许进行IO端口的读写操作,为3时可以读写端口。默认权能级别为0,即用户空间不可读写,所以需要更改这个比特,允许文件系统进程可以读写。更改env.c中的env_create()

1
2
3
4
5
6
7
8
	// If this is the file server (type == ENV_TYPE_FS) give it I/O privileges.
// LAB 5: Your code here.
struct Env* env;
env_alloc(&env, 0);
load_icode(env, binary);
env->env_type = type;
if(type == ENV_TYPE_FS)
env->env_tf.tf_eflags |= FL_IOPL_3;

Question

Do you have to do anything else to ensure that this I/O privilege setting is saved and restored properly when you subsequently switch from one environment to another? Why?

从一个进程切换到另一个进程时,ELFAGS寄存器的内容会被保存在struct Trapframe中,而返回该进程时,又回通过iret指令把ELFAGS从struct Trapframe还原,因此不必再做额外的工作保证。

在这个文件系统的实现中,将整个磁盘的内容映射到了文件系统进程的虚拟地址空间 0x10000000 (DISKMAP) ~ 0xD0000000 (DISKMAP+DISKMAX)中,对磁盘块的读写导致page fault,然后进入处理程序,将相应的块加载到物理内存中。显然,这种做法只能在32位的CPU中支持3GB的磁盘大小,但对于JOS来说足够了。

Exercise 2

exercise 2需要补全两个函数bc_pgfault()flush_block()。其中,bc_pgfault()用于处理读某个磁盘块对应读虚拟地址导致读page fault,所以它要做的很简单,申请一块内存,使用提供的磁盘读函数读取相应的内容到申请的内存即可。

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
29
30
31
32
33
34
35
36
37
38
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:

uint32_t blockbase = blockno * PGSIZE + DISKMAP;
r = sys_page_alloc(0, (void*)blockbase, PTE_U | PTE_P | PTE_W);
if(r < 0)
panic("fs pgfault error in page alloc%e\n", r);
r = ide_read(blockno * 8, (void*)blockbase, 8);
if(r < 0)
panic("fs pgfault error while reading ide%e\n", 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);

对于注释中提问的问题:

exercise for the reader: why do we do this after reading the block in?

对于普通的磁盘块,是“before”还是“after”都没什么影响,但是对于bitmap所在的块(位于blockno 2)来说就不一样了,如果先判断再读入的话,一开始,bitmap所在块的内容并不在内存中,会产生page fault,而该page fault在处理时,又一次进入了bc_pgfault(),同样是先判断再读入,那么还是会产生page fault,这样循环下去….

flush_block()用于将相应地址处的块写入磁盘中。当然,也不会全部都写,只有当一个块在自上次从磁盘读取或写入后被更改过(也就是,与磁盘中的不一致了)才需要写入磁盘,判断是否被更改过使用va_is_dirty(),它则是判断页表中的PTE_D位来实现的。代码如下:

1
2
3
4
5
6
7
8
9
10
11
	// LAB 5: Your code here.

if(!va_is_mapped(addr) || !va_is_dirty(addr))
return;

uint32_t blockbase = blockno * PGSIZE + DISKMAP;
int r = ide_write(8 * blockno, (void*)blockbase, 8);
if(r < 0)
panic("flush block error while writing to disk%e\n", r);

sys_page_map(0, (void*)blockbase, 0, (void*)blockbase, PTE_SYSCALL);

Exercise 3

bitmap是一个uint32_t的数组,整个数组的每一比特代表那一块是否已经被分配,比如,如果blockno为32的块是未分配的,那么bitmap[1]的最低比特就是1,以此类推。exercise 3要求做的便是参考free_block()来完成alloc_block()。它从bitmap中找到一个未分配的块,将其分配,返回相应的blockno号。逻辑比较简单,只看代码就行了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
	// LAB 5: Your code here.

uint32_t blockno;

for(blockno = 1; blockno < super->s_nblocks; blockno++)
{
if(block_is_free(blockno))
{
bitmap[blockno/32] &= ~(1<<(blockno%32));
flush_block(&bitmap[blockno/32]);
return blockno;
}
}

return -E_NO_DISK;

Exercise 4

exercise 4需要完成两个函数file_block_walk()file_get_block()file_block_walk()用于给出一个文件以及一个块的偏移量filebno,返回其对应的blockno的槽,换句话说,即给一个文件,以及n,找出文件第n个block的对应的槽,与pgdir_walk()很类似。

如果filebno小于NDIRECT,那么f->f_direct[filebno]的地址即为所求。如果大于等于NDIRECT,那么就需要用到struct Filef_indirect字段了。如果当前文件还未分配一个块,即f_indirect字段为0,则根据情况选择是否分配一个块,如果函数的参数要求分配一个块,那么便分配一个块,然后该块的第filebno - NDIRECT个槽的地址即为所求;如果已经有一个indirect块了,则就直接求得相应地址即可。(可能直接看代码比看这段解释更加易懂

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
29
30
31
32
33
      // LAB 5: Your code here.

if(filebno >= NDIRECT + NINDIRECT)
return -E_INVAL;

if(filebno < NDIRECT)
{
*ppdiskbno = f->f_direct + filebno;
return 0;
}

if(!f->f_indirect)
{
if(alloc)
{
int r = alloc_block();
if(r < 0)
return r;

f->f_indirect = r;
memset(diskaddr(r), 0, BLKSIZE);
flush_block(diskaddr(r));
*ppdiskbno = ((uint32_t*)diskaddr(f->f_indirect)) + filebno - NDIRECT;
return 0;
}
else
return -E_NOT_FOUND;
}
else
{
*ppdiskbno = ((uint32_t*)diskaddr(f->f_indirect)) + filebno - NDIRECT;
return 0;
}

file_get_block()则是取得一个文件f的第filebno个块,其blockno所对应的虚拟地址。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
       // LAB 5: Your code here.

uint32_t* t;
int r = file_block_walk(f, filebno, &t, 1);
if(r < 0)
return r;

if(*t == 0)
{
r = alloc_block();
if(r < 0)
return r;

*t = r;
*blk = diskaddr(r);
return 0;
}
else
{
*blk = diskaddr(*t);
return 0;
}

文件系统的整体结构如图所示:

img

Exercise 5

这里需要完成serve_read(),它是文件系统进程的RPC调用的接口。它负责解析用户进程通过IPC发送过来的文件读请求,然后进行相应的操作。可以参考上面的serve_set_size()的模式。serve_read()需要做的是:从打开文件中找到相应的文件,参考上图的流程,调用file_read()读取文件内容,然后更改文件的seek指针。

1
2
3
4
5
6
7
8
9
10
11
12
13
	// Lab 5: Your code here:
struct OpenFile *o;
int r, tmp;
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;

if((tmp = seek(fd2num(o->o_fd), o->o_fd->fd_offset + r)) < 0)
return tmp;

return r;

Exercise 6

exercise 6中需要完成的serve_write()非常类似,就不多解释了,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
	// LAB 5: Your code here.

struct OpenFile *o;
int r, tmp;
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;

if((tmp = seek(fd2num(o->o_fd), o->o_fd->fd_offset + r)) < 0)
return tmp;

return r;

Exercise 7

exercise 7需要实现sys_env_set_trapframe()。它用于更改一个环境的trapframe。实现还是比较简单的,不要忘了使用user_mem_assert()来确认传入的参数是否是一个用户有权限访问的有效地址。代码是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
	// LAB 5: Your code here.
// Remember to check whether the user has supplied us with a good
// address!

user_mem_assert(curenv, tf, sizeof(struct Trapframe), PTE_U | PTE_P);
struct Env * env;
int r = envid2env(envid, &env, 1);
if(r < 0)
return r;

memcpy(&env->env_tf, tf, sizeof(struct Trapframe));
env->env_tf.tf_eflags &= ~FL_IOPL_3;
env->env_tf.tf_eflags |= FL_IF;
env->env_tf.tf_cs |= 0x3;
env->env_tf.tf_ss |= 0x3;

return 0;

另外,还需要在syscall()中添加相应的case:

1
2
case SYS_env_set_trapframe: 
return sys_env_set_trapframe(a1, (struct Trapframe*)a2);

Exercise 8

回想一下,fork()函数会创建新的子进程,子进程会共享与父进程同样的文件描述符。而每一个文件描述符独占一个页,那么很自然地就能想到,可以利用虚拟内存系统,将文件描述符对应的物理页同时映射到父子进程,来实现文件描述符的共享。要求修改之前的duppage(),如果一个页置了PTE_SHARE位,则直接将这个页映射到相应地址。

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
29
30
31
static int
duppage(envid_t envid, unsigned pn)
{
int r;

// LAB 4: Your code here.

pte_t src_pte = uvpt[pn];

if(src_pte)
{
if(src_pte & PTE_SHARE)
{
if(sys_page_map(0, (void*)(pn * PGSIZE), envid, (void*)(pn * PGSIZE), src_pte & PTE_SYSCALL) < 0)
panic("duppage error on page map share\n");
}
else if((src_pte & PTE_W) || (src_pte & PTE_COW))
{
if( (sys_page_map(0, (void*)(pn * PGSIZE), envid, (void*)(pn * PGSIZE), (src_pte & 0xfff & ~(PTE_W)) | PTE_COW) < 0) ||
(sys_page_map(0, (void*)(pn * PGSIZE), 0, (void*)(pn * PGSIZE), (src_pte & 0xfff & ~(PTE_W)) | PTE_COW) < 0) )
panic("duppage error on page map COW\n");
}
else
{
if(sys_page_map(0, (void*)(pn * PGSIZE), envid, (void*)(pn * PGSIZE), src_pte & 0xfff) < 0)
panic("duppage error on page map\n");
}
}

return 0;
}

另外,还有copy_shared_pages()。它做的就是将当前进程中置PTE_SHARE位的页映射给相应的子进程。代码比较简单:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
	// LAB 5: Your code here.

for(uint32_t i = 0; i < USTACKTOP / PGSIZE; i++)
{
if(uvpd[i / NPDENTRIES])
{
pte_t src_pte = uvpt[i];
if(src_pte & PTE_SHARE)
{
if(sys_page_map(0, (void*)(i * PGSIZE), child, (void*)(i * PGSIZE), src_pte & PTE_SYSCALL) < 0)
panic("duppage error on page map share\n");
}
}
}
return 0;

Exercise 9

exercise 9非常简单,只需要修改trap_dispatch()来处理来自键盘和串口的中断即可。需要添加的代码是

1
2
3
4
5
6
7
8
9
10
11
12
13
14
	// Handle keyboard and serial interrupts.
// 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;
}

Exercise 10

最后一个exercise是实现shell的IO重定向功能。这个其实已经在我的这篇文章的第二部分讲过了,原理都是一样的,因此就不多解释了,直接上代码:

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
case '<':	// Input redirection
// Grab the filename from the argument list
if (gettoken(0, &t) != 'w') {
cprintf("syntax error: < not followed by word\n");
exit();
}
// Open 't' for reading as file descriptor 0
// (which environments use as standard input).
// We can't open a file onto a particular descriptor,
// so open the file as 'fd',
// then check whether 'fd' is 0.
// If not, dup 'fd' onto file descriptor 0,
// then close the original 'fd'.

// 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);
close(fd);
}

break;

case '>': // Output redirection
// Grab the filename from the argument list
if (gettoken(0, &t) != 'w') {
cprintf("syntax error: > not followed by word\n");
exit();
}
if ((fd = open(t, O_WRONLY|O_CREAT|O_TRUNC)) < 0) {
cprintf("open %s for write: %e", t, fd);
exit();
}
if (fd != 1) {
dup(fd, 1);
close(fd);
}
break;

至此,lab 5就全部完成了。

img

lab的完整代码在我的GitHub中可以找到

(完)