【MIT 6.828 Lab5】 文件系统与 Shell

340 阅读5分钟

MIT 6.828 Lab5 文件系统与 Shell

lab 5 开始文件系统部分的内容,并增加了从文件系统执行程序的 spawn 以及 Shell 。其中文件系统主要是内存空间与磁盘空间的映射,通过 block_cache 缓存来将磁盘空间中的内容加载至内存,通过这一层缓存加用户态缺页处理 bc_pgfault 可以实现非常灵活的磁盘内容访问。

image-20210901170014203

在 JOS 中磁盘的地址空间与内存地址空间的映射为双射,可以通过计算得到对应的地址,例如 bc_pgfault 中的如下代码,首先拿到触发缺页中断的地址,然后将其转换为地址空间中的 blockno,然后通过对应的 ide_write 将对应磁盘 block 的内容加载至内存。

void *addr = (void *) utf->utf_fault_va;
uint32_t blockno = ((uint32_t)addr - DISKMAP) / BLKSIZE;
ide_read(blockno*BLKSECTS,(void *)addr,BLKSECTS)

1. 文件系统

文件结构体 File 通过数组 direct blocks 与 链表 indirect block 的方式来组织磁盘块。

File structure

文件系统按树的方式来组织,根节点为 superblock,目录的 block 内容指向文件,可以通过绝对路劲来逐级向下判断文件是否存在,类比于向量的加法。

Disk layout

文件系统这边还需要负责磁盘空间的管理以及缓存相关的信息,JOS 也提供了相应的实现,分别通过位图 bitmap 与虚拟内存标志位的方式 PTE_D

lab 5 中的两个核心方法分别为 file_block_walk 与 walk_path 函数,分别负责:文件与 block 的映射,文件系统路径的查找。

Exercise 1

lab 5 的文件系统通过用户态环境来实现,默认无IO权限,所以要修改 env_create 文件,修改相关的权限位

image-20211014100838270

Exercise 2

完成 bc_pgfault 与 flush_block 函数,这两个函数负责内存与磁盘的交互,读写

  • bc_pgfault,用户态缺页处理程序,将导致缺页的地址翻译为磁盘空间地址,申请页,然后使用 ide_read 从磁盘读入。
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);
	// LAB 5: you code here:
	addr = (void *)ROUNDDOWN((uintptr_t)addr,PGSIZE);
	if((r=sys_page_alloc(0,(void *)addr,PTE_SYSCALL))<0){
		panic("in bc_pgfault, sys_page_alloc: %e", r);
	}
	//block与sector之间的转换
	// BLKSECTS为8,而非4
	if((r=ide_read(blockno*BLKSECTS,(void *)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;
	if (addr < (void*)DISKMAP || addr >= (void*)(DISKMAP + DISKSIZE))
		panic("flush_block of bad va %08x", addr);
	// LAB 5: Your code here.
	addr = (void *)ROUNDDOWN((uintptr_t)addr,PGSIZE);
	int r;
	if(va_is_mapped(addr) && va_is_dirty(addr)){
		if((r=ide_write(blockno*BLKSECTS,(void *)addr,BLKSECTS))<0){
			panic("flush_block error! %08x", addr);
		}
		if ((r = sys_page_map(0, addr, 0, addr, uvpt[PGNUM(addr)] & PTE_SYSCALL)) < 0)
			panic("in flush_block, sys_page_map: %e", r);
	}
}

Exercise 3

  • alloc_block,参考 free_block 与 lab 2 的内存管理
int
alloc_block(void)
{
	//BLKBITSIZE与super->s_nblocks,选择第二个单位
	uint32_t bucket_index;
	uint32_t bit_index_in_bucket;
	for(uintptr_t i=0;i<super->s_nblocks;i++){
		bucket_index = i/32;
		bit_index_in_bucket = i%32;
		//判断位图中是否置位,1为空闲,0为占位
		if(bitmap[bucket_index] & (1<< bit_index_in_bucket)){
			//空闲置位
			bitmap[bucket_index] &= ~(1<< bit_index_in_bucket);
			flush_block(bitmap);
			return i;
		}
	}
	return -E_NO_DISK;
}

Exercise 4

文件操作,核心为函数 file_block_walk,类比于虚拟内存部分的 pgdir_walk

  • file_block_walk,注意此处的二级指针 ppdiskbno,使用二级指针是为了方便后续的 file_get_block 函数申请 block 赋值,不要使用 本地变量的地址为二级指针赋值(栈帧为临时存在),直接取内存相关结构体的地址即可。
static int
file_block_walk(struct File *f, uint32_t filebno, uint32_t **ppdiskbno, bool alloc)
{
	   if(filebno>=NDIRECT + NINDIRECT){
		   return -E_INVAL;
	   }
	   //全局的block no,0作为null
	   uint32_t g_blkno;
	   int r;
	   //基数与序数,Direct
	   if(filebno<NDIRECT){
		   g_blkno = f->f_direct[filebno]; 
		   //XX,按需映射,(未映射,Direct不需要alloc,直接返回)
		   *ppdiskbno = &(f->f_direct[filebno]);
		   return 0;
	   //Indirect
	   }else{
		   g_blkno = f->f_indirect;
		   	//Indirect Block未映射
	   	   	if(g_blkno==0){
				if(alloc){
					if((g_blkno=alloc_block())<0){
						return-E_NO_DISK;
					}
					//缺一步直接赋值,以及置0
					f->f_indirect = g_blkno;
					memset(diskaddr(g_blkno), 0, BLKSIZE);
				}else{
					return -E_NOT_FOUND;
				}
	   	   	}
			//切换索引
			filebno -= NDIRECT;
			//转换为地址,访问内容
			uint32_t* ind_addr = (uint32_t *)diskaddr(g_blkno);
			g_blkno =ind_addr[filebno];
            //*ppdiskbno =(void *)g_blkno;
		    // ppdiskbno = &g_blkno;
		    // ppdiskbno = g_blkno;
		    //*ppdiskbno =&g_blkno;
			//*ppdiskbno =(void *)g_blkno;
			*ppdiskbno = &(ind_addr[filebno]);
			return  0;  	
	   }
}
  • file_get_block,完成 block 地址的赋值,按需申请 block
int
file_get_block(struct File *f, uint32_t filebno, char **blk)
{
	   int r;
	   uint32_t *ppdiskbno;
	   if((r=file_block_walk(f,filebno,&ppdiskbno,1))<0){
		   return r;
	   }
	   //XX ppdiskbno传递参数,所以做转换即可,不需要二次解
	   //与file_flush等保持一致,ppdiskbno可能为0
	   if(*ppdiskbno){
			*blk = diskaddr(*ppdiskbno);
	   }else{
			//return -E_NOT_FOUND;
			if((r=alloc_block())<0){
				return-E_NO_DISK;
			}
			*ppdiskbno = r;
		    memset(diskaddr(*ppdiskbno),0,BLKSIZE);
			//返回值赋值
			*blk = diskaddr(*ppdiskbno);
	   }	
	   return 0;
}

2. 文件系统API

文件系统维护两套映射关系,fdnum->Fd,Fd->OpenFile,并提供了设备的抽象(读写函数的封装),示意图如下:

image-20210901170228905

Exercise 5

  • serve_read,注意返回值为实际读取的长度,而不是通常表示成功的 0 。
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);
	int r;
	struct OpenFile *o;
	if ((r = openfile_lookup(envid, req->req_fileid, &o)) < 0)
		return r;
	struct File * dst_file = o->o_file;
	// 读取文件,seek position在Fd中
	struct Fd *fd = o->o_fd;
	if((r=file_read(dst_file,ret->ret_buf,req->req_n,fd->fd_offset)) < 0){
		return r;
	}
	//count可能较大,需要返回实际读取内容
	fd->fd_offset+=r;
	return r;
}

Exercise 6

  • 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);
	int r;
	struct OpenFile *o;
	if ((r = openfile_lookup(envid, req->req_fileid, &o)) < 0)
		return r;
	struct File * dst_file = o->o_file;
	// 读取文件,seek position在Fd中
	struct Fd *fd = o->o_fd;
	if((r=file_write(dst_file,req->req_buf,req->req_n,fd->fd_offset))<0){
		return r;
	}
	fd->fd_offset+=r;
	return r;
}
  • devfile_write,参数设置,然后发起 IPC 等待结果返回
static ssize_t
devfile_write(struct Fd *fd, const void *buf, size_t n)
{
	int r;
	fsipcbuf.write.req_fileid = fd->fd_file.id;
	fsipcbuf.write.req_n = n;
	//fsipcbuf.write.req_buf = (char *)buf;
	assert(n<=(PGSIZE - (sizeof(int) + sizeof(size_t))));
	memmove(fsipcbuf.write.req_buf,buf,n);
	if ((r = fsipc(FSREQ_WRITE, NULL)) < 0)
		return r;
	return r;
}

3. Spawn

Spawn 类似于 fork,但不同的点在于子进程为全新的 CPU 状态,而非父进程的简单复制,代码是从磁盘加载,栈也由 spawn 进行初始化,示意图如下:

image-20210916105030570

关于可变参数,可以参考:Variadic Functions

以下参考C文档:[Variadic Functions](https://link.zhihu.com/?target=https%3A//en.cppreference.com/w/c/variadic) 带有可变参数的函数,参数列表以`...`结尾。编译器提供了一定机制,帮助我们取出`...`中包含的参数。由于给函数的参数在编译时就已经确定,这样的语言特性才是可以实现的。 
使用可变参数的模式非常固定,如下: 
va_list args; // 准备接受参数的列表对象 
va_start(args, fmt); // 从`...`中取出参数到args中,并指定...之前的参数 
vprintf(fmt, args); // 将取出的参数列表传给真正的实现函数 
va_end(args); // 释放参数列表 
在实现函数中,使用`va_arg(args, int)`取出变量,指定了类型`int`,代表以`int`类型解析当前参数。再次调用`va_arg`时,取出的参数是传入参数中的下一个。

Exercise 7

  • sys_env_set_trapframe,按提示设置状态位
// tf is modified to make sure that user environments always run at code
// protection level 3 (CPL 3), interrupts enabled, and IOPL of 0.
static int
sys_env_set_trapframe(envid_t envid, struct Trapframe *tf)
{
	struct Env *target_env;
	int res_code;
	if((res_code = envid2env(envid,&target_env,1))!=0){
		return res_code;
	}
	// CPL 3
	tf->tf_cs |=3;
	tf->tf_ss |=3;
	tf->tf_eflags |= FL_IF;
	// 清0
	tf->tf_eflags  &= ~FL_IOPL_3;
	target_env->env_tf = *tf;
	return 0;
}

Exercise 8

  • duppage,在 duppage 中新增对 PTE_SHARE 的处理

image-20211014112704369

  • copy_shared_pages
// Copy the mappings for shared pages into the child address space.
static int
copy_shared_pages(envid_t child)
{
	unsigned pn;
	int r;
	for (pn=PGNUM(UTEXT); pn<PGNUM(USTACKTOP); pn++){ 
        if ((uvpd[pn >> 10] & PTE_P) && (uvpt[pn] & PTE_P)){
			if((uvpt[pn] & PTE_SHARE) == PTE_SHARE){
				if((r=sys_page_map(0,(void *)(pn*PGSIZE),child,(void *)(pn*PGSIZE),uvpt[pn]&PTE_SYSCALL))<0){
					panic("copy_shared_pages for child!va:%08x,error:%08x\n",(pn*PGSIZE),r);
					return r;
				}
			}
		}
	}
	return 0;
}

Exercise 9

新增对键盘中断的处理,这部分的处理逻辑是将来自键盘与串口的输入存储至环形 buffer 中,通过读写指针来管理。

static struct {
	uint8_t buf[CONSBUFSIZE];
	uint32_t rpos;
	uint32_t wpos;
} cons;

image-20211014113048596

4. Shell

参数解析

其中值得一提的是,shell 这边的参数解析部分,arg.c 将输入的可变参数数目 argc 与 字符串指针数组 argv 封装为 Argstate,这部分的内容可以参考 args.h 。

struct Argstate {
	int *argc;
	const char **argv;
	const char *curarg;
	const char *argvalue;
};

解析流程的过程类似于队列,每次都处理队头的字符串,处理完就将其出队,图示如下:

image-20211015141200860

有两个细节容易让人迷惑,如下:

  • 指针的加法,以及指针的取值,遇到 [] * 符号为取引用值,其余部分按照指针类型进行计算即可

如下这部分代码,args->argv[1] 拿到的是字符串首地址,然后+1,代表取字符串的第二个字符。

// Shift arguments down one
args->curarg = args->argv[1] + 1;
memmove(args->argv + 1, args->argv + 2, sizeof(const char *) * (*args->argc - 1));
(*args->argc)--;
  • 空字符串的布尔值,空字符串的 ASCII 码为0,而0的 ASCII 码为48
#与\r,\n类似
char a = '\0';
char b = '0';
printf("a = %d,b = %d\n",a,b);
输出:a = 0,b = 48

lab 5 中使用指令 make run-icode 即可打开 shell 交互

调用路径如下:icode->init->sh

  • icode,调用 init 派生子进程 init,icode.c 传参如下:
r = spawnl("/init", "init", "initarg1", "initarg2", (char*)0)
  • init,循环派生子进程,spawnl+wait

image-20211014114931103

  • sh,fork子进程 runcmd,父进程等待子进程结束 wait

其中 gettoken 负责解析命令行参数,使用 np1,np2 两个指针分别记录 token 所在区间的索引,左闭右开。

整体流程类似于队列,二级指针 p1 负责记录 token 所在的索引。

// Get the next token from string s.
// Set *p1 to the beginning of the token and *p2 just past the token.
// Returns
//	0 for end-of-string;
//	< for <;
//	> for >;
//	| for |;
//	w for a word.
// Eventually (once we parse the space where the \0 will go),
// words get nul-terminated.
#define WHITESPACE " \t\r\n"
#define SYMBOLS "<|>&;()"
int _gettoken(char *s, char **p1, char **p2)

Pipe

pipe 这个地方的权限位增加了 PTE_SHARE ,spawn.c 以及 fork.c 中都针对这个权限位进行了特殊处理,通过 sys_page_map 进行跨进程的映射拷贝,否则缺页触发 COW 的话共享数据的管道就无从谈起。

结合 Fd 的 dup 函数,达到多个进程共享 Pipe 的效果。

image-20211018114220066

image-20211018105526562

#define PIPEBUFSIZ 32		// small to provoke races
struct Pipe {
	off_t p_rpos;		// read position
	off_t p_wpos;		// write position
	uint8_t p_buf[PIPEBUFSIZ];	// data buffr
};

Exercise 10

增加 IO 重定向功能

		case '<':	// Input redirection
			// Grab the filename from the argument list
			if (gettoken(0, &t) != 'w') {
				cprintf("syntax error: < not followed by word\n");
				exit();
			}
			if ((r = open(t, O_RDONLY)) < 0){
				cprintf("oepn %s errr,%e",t,r);
				exit();
			}
			fd = r;
			if(fd!=0){
				if((r=dup(fd,0)<0)){
					panic("duplicate error!");
				}
				close(fd);
			}
			break;

参考资料:

NULL,0,'\0',“0”,"\0"你真的分得清吗? - 守望的文章 - 知乎 zhuanlan.zhihu.com/p/79210633