MIT 6.828 Lab5 文件系统与 Shell
lab 5 开始文件系统部分的内容,并增加了从文件系统执行程序的 spawn 以及 Shell 。其中文件系统主要是内存空间与磁盘空间的映射,通过 block_cache 缓存来将磁盘空间中的内容加载至内存,通过这一层缓存加用户态缺页处理 bc_pgfault 可以实现非常灵活的磁盘内容访问。
在 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 的方式来组织磁盘块。
文件系统按树的方式来组织,根节点为 superblock,目录的 block 内容指向文件,可以通过绝对路劲来逐级向下判断文件是否存在,类比于向量的加法。
文件系统这边还需要负责磁盘空间的管理以及缓存相关的信息,JOS 也提供了相应的实现,分别通过位图 bitmap 与虚拟内存标志位的方式 PTE_D 。
lab 5 中的两个核心方法分别为 file_block_walk 与 walk_path 函数,分别负责:文件与 block 的映射,文件系统路径的查找。
Exercise 1
lab 5 的文件系统通过用户态环境来实现,默认无IO权限,所以要修改 env_create 文件,修改相关的权限位
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,并提供了设备的抽象(读写函数的封装),示意图如下:
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 进行初始化,示意图如下:
关于可变参数,可以参考: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 的处理
- 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;
4. Shell
参数解析
其中值得一提的是,shell 这边的参数解析部分,arg.c 将输入的可变参数数目 argc 与 字符串指针数组 argv 封装为 Argstate,这部分的内容可以参考 args.h 。
struct Argstate {
int *argc;
const char **argv;
const char *curarg;
const char *argvalue;
};
解析流程的过程类似于队列,每次都处理队头的字符串,处理完就将其出队,图示如下:
有两个细节容易让人迷惑,如下:
- 指针的加法,以及指针的取值,遇到
[],*符号为取引用值,其余部分按照指针类型进行计算即可
如下这部分代码,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
- 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 的效果。
#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