MIT JOS lab5 文件管理

lab5 文件管理

源码资源

完整版已通过代码,附有详细注释,需要自取,https://download.csdn.net/download/qhaaha/14042362

exercise 1

在这里插入图片描述

修改env_create,当创建的是文件系统环境时,赋予IO的权限,就加两行:

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

exercise 2

在这里插入图片描述

本次Lab中假设的磁盘空间只有3G,文件系统环境虚拟地址空间的0x1000_0000~0xd000_0000这3G的地址映射到磁盘空间。

磁盘中的内容肯定不是一次性全部加载到内存,而是采取请求式调页的策略,即当环境用到磁盘中的一个block时,通过缺页中断来完成请求式调页进入内存。

bc_pgfault就是处理磁盘调页的缺页处理函数:

envid_t envid = thisenv->env_id;
    void *blkaddr = ROUNDDOWN(addr, PGSIZE);
 
    if (sys_page_alloc(envid, blkaddr, PTE_SYSCALL) < 0)
    {
        panic("bg_pgfault:can't allocate new page for disk block\n");
    }
 
    if (ide_read(blockno * BLKSECTS, blkaddr, BLKSECTS) < 0)
    {
        panic("bg_pgfault: failed to read disk block\n");
    }

flush_block是用来将内存中的内容写回磁盘的函数,如果block cache中没有这一块或者这一块的脏位为0,就什么都不用做:

addr = ROUNDDOWN(addr, PGSIZE);
	if (!va_is_mapped(addr) || !va_is_dirty(addr)) {		//如果addr还没有映射过或者该页载入到内存后还没有被写过,不用做任何事
		return;
	}
	if ((r = ide_write(blockno * BLKSECTS, 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)	//清空PTE_D位
		panic("in bc_pgfault, sys_page_map: %e", r);

exercise 3

在这里插入图片描述

alloc_block函数用来分配一个空闲的磁盘块,功能如同lab2中的page_alloc。

这里用到了磁盘块的一个“位图”——bitmap,简而言之就是每个块对应1个bit,是1表示free,0表示used。它被放在block2开始的足够容纳它的若干个块中,具体有多少呢?

3G的磁盘空间,每个块212个字节,所以共3*218个块,也就是bitmap中需要3*218个bit,3*215个字节,也就是需要3*2^3 = 24个块。看一下磁盘空间的线性映射图:

alloc_block函数:

	for (uint32_t blockno = 0; blockno < super->s_nblocks; blockno++) {
		if (block_is_free(blockno)) {					
			bitmap[blockno / 32] &= ~(1 << (blockno % 32));		//标记为已使用
			flush_block(diskaddr(2 + (blockno / 32) / NINDIRECT));	//将刚刚修改的bitmap block写到磁盘中
			return blockno;
		}
	}
	
	return -E_NO_DISK;

找到一个空的块,标记为已使用,并将置0的位(所在的块)立刻写回磁盘。注意这里bitmap是一个32位型的数组,而实际上其内容是每个bit对应一个块,所以使用时用了一些技巧。在flush_block中获得对应的虚拟地址时:

(blockno / 32) / NINDIRECT)计算出分配的块对应bitmap位图中的第几个块(前面图中24个中的某一个),也就是我们置0的bit(所在的块),需要立刻写回磁盘,记得还要加2,bitmap是从block2开始的。

exercise 4

在这里插入图片描述

也和lab2中有相似之处。

先看一下文件结构:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HTEgA8gs-1608652759364)(C:\Users\sft\Desktop\os\lab6\lab6 文件管理.assets\image-20201221225749128.png)]

也就是说,前10个block的指针是直接放在struct file中的,那如果大于10个block的文件,怎么办?答案就是通过在磁盘中分配一个Indirect block存放data block的指针,一共可以放1024个指针(每个4B)。由于只支持最多两级的block索引,言外之意就是我们的文件系统只支持最大1034个data block的文件。

file_block_walk(struct File *f, uint32_t filebno, uint32_t **ppdiskbno, bool alloc)函数用于查找文件f的第filebno号的磁盘块,使得指针*ppdiskbno指向(虚拟地址对应的)这一块。 更准确的说,应该是查找文件f 的第filebno号磁盘entry,可以是null,如果还没有分配具体块,并使得*ppdiskbno指向这个entry。也就是说entry只要存在,或者可以被alloc,就会返回成功(0),不在乎究竟有没有具体的磁盘块。代码:

static int
file_block_walk(struct File *f, uint32_t filebno, uint32_t **ppdiskbno, bool alloc)
{
    // LAB 5: Your code here.
	int bn;
	uint32_t *indirects;
	if (filebno >= NDIRECT + NINDIRECT)
		return -E_INVAL;

	if (filebno < NDIRECT) {
		*ppdiskbno = &(f->f_direct[filebno]);
	} else {
		if (f->f_indirect) {
			indirects = diskaddr(f->f_indirect);
			*ppdiskbno = &(indirects[filebno - NDIRECT]);
		} else {
			if (!alloc)
				return -E_NOT_FOUND;
			if ((bn = alloc_block()) < 0)
				return bn;
			f->f_indirect = bn;
			flush_block(diskaddr(bn));
			indirects = diskaddr(bn);
			*ppdiskbno = &(indirects[filebno - NDIRECT]);
		}
	}

	return 0;
}

分成是前十个直接索引块,还是后面的间接块。如果是间接块,但是还没有indirect block,那么就需要分配indirect block用来盛放间接块条目,如果alloc参数为0或者无法分配,则返回错误。

注意!这个函数只会分配indirect block, 不会分配data block!

file_get_block(struct File *f, uint32_t filebno, char **blk)函数比file_block_walk更进一步,使得*blk指向文件 f的第filebno页,此时就不光要求有“entry”了,还需要(如果之前没有)分配实打实的data block。借助file_block_walk,代码:

int
file_get_block(struct File *f, uint32_t filebno, char **blk)
{
       // LAB 5: Your code here.
       int r;
	   	uint32_t *pdiskbno;
	   	if ((r = file_block_walk(f, filebno, &pdiskbno, true)) < 0) {
		  	return r;
	   	}

		int bn;
		if (*pdiskbno == 0) {			//此时*pdiskbno保存着文件f第filebno块block的索引
			if ((bn = alloc_block()) < 0) {
				return bn;
			}
			*pdiskbno = bn;
			flush_block(diskaddr(bn));
		}
		*blk = diskaddr(*pdiskbno);
	   	return 0;
	   
}

exercise 5、6

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ssH6yRTK-1608652759365)(C:\Users\sft\Desktop\os\lab6\lab6 文件管理.assets\image-20201222094910080.png)]

前面4个练习已经完成了文件系统环境的请求式调页功能,但是需要让一般的用户环境能够使用文件系统的功能,就要提供RPC(remote procedure call)机制:在文件系统和用户环境之间有着客户机服务器的关系,使得一般环境得以通过request使用文件系统。

我们利用lab4中已经实现的IPC(进程间通信)机制来实现此RPC,回忆一下,我们提供的IPC机制允许发送一个页映射,我们就利用这个功能,用一个页盛放 union Fsipc作为参数从client(一般环境)传递到server(文件系统环境),Fsipc是一个联合,涵盖了多种请求或者响应的格式,就好像网络协议中的“报文”,后面就这么称呼了。后面用一个PGSIZE大小的数组将此union的大小框定为一页。

union Fsipc {
	struct Fsreq_open {
		char req_path[MAXPATHLEN];
		int req_omode;
	} open;
	struct Fsreq_set_size {
		int req_fileid;
		off_t req_size;
	} set_size;
	struct Fsreq_read {
		int req_fileid;
		size_t req_n;
	} read;
	struct Fsret_read {
		char ret_buf[PGSIZE];
	} readRet;
	struct Fsreq_write {
		int req_fileid;
		size_t req_n;
		char req_buf[PGSIZE - (sizeof(int) + sizeof(size_t))];
	} write;
	struct Fsreq_stat {
		int req_fileid;
	} stat;
	struct Fsret_stat {
		char ret_name[MAXNAMELEN];
		off_t ret_size;
		int ret_isdir;
	} statRet;
	struct Fsreq_flush {
		int req_fileid;
	} flush;
	struct Fsreq_remove {
		char req_path[MAXPATHLEN];
	} remove;

	// Ensure Fsipc is one page
	char _pad[PGSIZE];
};

serve_read是服务端用来响应读请求的,其实主要的功能读文件是已经给出的函数file_read完成的,这个serve_read主要还是处理ipc传进来的“报文”,以及在读结束后完成返回“报文”。

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);

	// Lab 5: Your code here:
	struct OpenFile *o;
	int r;
	r = openfile_lookup(envid, req->req_fileid, &o);
	if (r < 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;
	

}

serve_write处理写请求,还是通过调用更下层的file_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);

	// LAB 5: Your code here.
	struct OpenFile *o;
	int r;
	if ((r = openfile_lookup(envid, req->req_fileid, &o)) < 0) {
		return r;
	}
	int total = 0;
	while (1) {
		r = file_write(o->o_file, req->req_buf, req->req_n, o->o_fd->fd_offset);
		if (r < 0) return r;
		total += r;
		o->o_fd->fd_offset += r;
		if (req->req_n <= total)
			break;
	}
	return total;
	
}

两个服务器端留给我们写的函数完成了,还有一个客户端的 devfile_write(struct Fd *fd, const void *buf, size_t n)功能是写文件。客户端的也很容易,封装“报文”,然后通过fsipc请求服务器端的fs环境处理并等待返回:

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); 
	
	return r;
	
}

这两个练习主要是选取了文件系统rpc机制下两个服务器端(fs/srv.c)和一个客户端(lib/file.c)的函数让我们实现。实际上read/write相关函数是所有“服务”中最简单的——操作的对象是打开好的文件,不涉及目录管理,同时磁盘操作也已经被实现好的函数屏蔽掉。

作为回顾,接下来看一下open文件的过程,感觉open操作更能增强对文件系统的整体理解。

文件Open

文件保存结构

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mB4vP5bB-1608652759365)(C:\Users\sft\Desktop\os\lab6\lab6 文件管理.assets\image-20201222203510133.png)]

首先画出上面这张整体图。1,2部分分别代表着一个普通用户环境和文件系统环境的虚拟地址空间,3代表物理内存空间。

如图1,在用户环境的虚拟地址空间处,从0xd000_0000向上的32个PGSIZE大小的空间被用来盛放32个文件描述符(File Descripter,包含打开模式,偏移等文件信息),(也就是说,在JOS中一个用户进程最多只能同时打开32个文件)。这部分的基址记作FDTABLE,对应地址:0xd000_0000 ~ 0xd002_0000。

在这部分向上的32个PGSIZE,被用来盛放对应文件的数据(并非全部,类似缓冲区),基地址记作FILEDATA,对应地址:0xd002_0000 ~ 0xd004_0000。

再看fs的虚拟地址空间,如图2,前面已经提到过从0x1000_0000到0xd000_0000 的3G大小是这个磁盘的映射,类似kernbase与内存的关系。从0xd000_0000到0xd040_0000的1024*PGSIZE大小,是用来存放所有环境用户环境的文件描述符FD的,其顺序和一会儿提到的opentab数组保持一致。也就是说整个系统最多打开1024个文件。

fs虚拟地址空间还有很重要的一块是opentab–一个OpenFile类型的数组,什么是OpenFile?是文件系统用来管理已打开的文件的,比Struct File更高一层的结构:

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

整体关系:4是OpenFile,5是File,这两个都在内存中(file system能够访问),右下角是磁盘,File结构中含有块的“指针”,指向至多10个直接索引磁盘块,和可能存在的indirect block,前面已说到。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yRNBLkl6-1608652759366)(C:\Users\sft\Desktop\os\lab6\lab6 文件管理.assets\image-20201222204234893.png)]

OpenFile结构中还有指向文件描述符的指针,也就是说,对于一个打开的文件,它的文件描述符对应页在内存中,被(至少)两个环境所指,ppref是2。可参考整体图的红3。图中共享页描述欠妥,还是只有一个用户进程使用

用户环境open

然后看一下一个用户环境open一个file的过程,先看客户机端,open是通过库函数file.c/open(const char *path, int mode)函数,两个参数分别是文件路径和打开模式。

	int r;
	struct Fd *fd;

	if (strlen(path) >= MAXPATHLEN)
		return -E_BAD_PATH;

	if ((r = fd_alloc(&fd)) < 0)
		return r;

	strcpy(fsipcbuf.open.req_path, path);
	fsipcbuf.open.req_omode = mode;

	if ((r = fsipc(FSREQ_OPEN, fd)) < 0) {
		fd_close(fd, 0);
		return r;
	}

	return fd2num(fd);

主要用到fsipc:

static int
fsipc(unsigned type, void *dstva)		//type, fsipcbuf是发送给fs进程的数据。dstava和fsipc()的返回值是从fs进程接收的值
{
	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);
}

fsipc做的事情就是设置好open请求对应的报文参数,通过ipc_send传递页映射的方式把报文传递过去。每个用户环境中定义了全局变量fsipcbuf,大小为一页(页对齐的),用来盛放报文并完成传递:union Fsipc fsipcbuf __attribute__((aligned(PGSIZE)));

然后fsipc会等待接收服务端的回复,在open服务中文件系统回复的页就是请求打开的文件的文件描述符,所以ipc_recv的第二个参数(用来接收传来的页映射),就应该设置为fd–分配出的FDTABLE中的文件描述符页。这是将来服务端的回复:

ipc_send(whom, r, pg, perm); //发送给普通进程,pg只有open操作时才不为NULL,这时pg指向被打开的文件的Fd结构

在服务器端(文件系统),有一个serve函数循环等待请求,open请求分配给serve_open函数来响应:

// Open req->req_path in mode req->req_omode, storing the Fd page and
// permissions to return to the calling environment in *pg_store and
// *perm_store respectively.
int
serve_open(envid_t envid, struct Fsreq_open *req,
	   void **pg_store, int *perm_store)
{
	char path[MAXPATHLEN];
	struct File *f;
	int fileid;
	int r;
	struct OpenFile *o;

	if (debug)
		cprintf("serve_open %08x %s 0x%x\n", envid, req->req_path, req->req_omode);

	// Copy in the path, making sure it's null-terminated
	memmove(path, req->req_path, MAXPATHLEN);
	path[MAXPATHLEN-1] = 0;

	// Find an open file ID
	if ((r = openfile_alloc(&o)) < 0) {					//从opentab数组中分配一个OpenFile结构
		if (debug)
			cprintf("openfile_alloc failed: %e", r);
		return r;
	}
	fileid = r;

	// Open the file
	if (req->req_omode & O_CREAT) {
		if ((r = file_create(path, &f)) < 0) {			//根据path分配一个File结构
			if (!(req->req_omode & O_EXCL) && r == -E_FILE_EXISTS)
				goto try_open;
			if (debug)
				cprintf("file_create failed: %e", r);
			return r;
		}
	} else {
try_open:
		if ((r = file_open(path, &f)) < 0) {
			if (debug)
				cprintf("file_open failed: %e", r);
			return r;
		}
	}

	// Truncate
	if (req->req_omode & O_TRUNC) {
		if ((r = file_set_size(f, 0)) < 0) {
			if (debug)
				cprintf("file_set_size failed: %e", r);
			return r;
		}
	}
	if ((r = file_open(path, &f)) < 0) {
		if (debug)
			cprintf("file_open failed: %e", r);
		return r;
	}

	// Save the file pointer
	o->o_file = f;										//保存File结构到OpenFile结构

	// Fill out the Fd structure
	o->o_fd->fd_file.id = o->o_fileid;
	o->o_fd->fd_omode = req->req_omode & O_ACCMODE;
	o->o_fd->fd_dev_id = devfile.dev_id;
	o->o_mode = req->req_omode;

	if (debug)
		cprintf("sending success, page %08x\n", (uintptr_t) o->o_fd);

	// Share the FD page with the caller by setting *pg_store,
	// store its permission in *perm_store
	*pg_store = o->o_fd;
	*perm_store = PTE_P|PTE_U|PTE_W|PTE_SHARE;

	return 0;
}

首先通过 openfile_alloc从opentab数组中分配一个OpenFile来保存文件,然后借助更底层的 file_createfile_open从通过目录结构查找或创建文件,最后设置该OpenFile对应的文件描述符,并将这一页通过ipc传递给用户进程。前面已经说过,在fs中,1024个FD和opentab中的1024个OpenFile是按顺序对应起来的,初始化的时候完成的:

void
serve_init(void)
{
	int i;
	uintptr_t va = FILEVA;
	for (i = 0; i < MAXOPEN; i++) {
		opentab[i].o_fileid = i;
		opentab[i].o_fd = (struct Fd*) va;
		va += PGSIZE;
	}
}

ipc_send(whom, r, pg, perm); //发送给普通进程,pg只有open操作时才不为NULL,这时pg指向被打开的文件的Fd结构

最后发送回用户环境,就完成了文件打开。

到此为止,在用户环境的视角,只要调用库函数open,获得整数型返回值a。此后第a个文件描述符FDTABLE[a]就代表着打开的文件,相关的文件操作通过这个描述符和rpc机制就能借助文件系统访问到。

使用描述符的例子,文件读:

devfile_read(struct Fd *fd, void *buf, size_t n)

附上很喜欢的一张网图,展示了文件系统rpc机制:(红色那里他写错了,31改成1024)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jOHCzUF7-1608652759366)(C:\Users\sft\Desktop\os\lab6\lab6 文件管理.assets\image-20201222220122753.png)]

reference:https://www.cnblogs.com/gatsby123/p/9950705.html

exercise 7

这个练习本身很简单,完成一个设置trapframe的系统调用,供spawn.c使用,先看这个函数吧:

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;
	}
	user_mem_assert(e, tf, sizeof(struct Trapframe), PTE_W);
	tf->tf_eflags = FL_IF;
	tf->tf_eflags &= ~FL_IOPL_MASK;			//普通进程不能有IO权限
	tf->tf_cs = GD_UT | 3;
	e->env_tf = *tf;
	return 0;
	
}

按照提示不要忘了用user_mem_assert检查参数。

也不要忘了syscall中加一个分支。(助教甚至帮忙写好了,去掉注释就行)

Spawning function

然后来专门读一下本次lab添加的一个新的创建进程的函数:lib/spawn.c中的spawn()创建一个新的进程,从文件系统加载用户程序,然后启动该进程来运行这个程序。简单理解类似linux API中的fork + exec。大致看一下这个函数都做了什么(具体可以去看这个函数中的注释):

  1. 打开文件,并读取elf头

    	if ((r = open(prog, O_RDONLY)) < 0)
    		return r;
    	fd = r;
    
    	// Read elf header
    	elf = (struct Elf*) elf_buf;
    	if (readn(fd, elf_buf, sizeof(elf_buf)) != sizeof(elf_buf)
    	    || elf->e_magic != ELF_MAGIC) {
    		close(fd);
    		cprintf("elf magic %08x want %08x\n", elf->e_magic, ELF_MAGIC);
    		return -E_NOT_EXEC;
    	}
    
  2. 调用sys_exofork()创建环境(lab4)

    if ((r = sys_exofork()) < 0)
    		return r;
    	child = r;
    
  3. 调用刚写的**sys_env_set_trapframe() **设置trapframe,包括通用寄存器、代码寄存器eip、和栈寄存器esp等一系列工作要做。

    // Set up trap frame, including initial stack.
    	child_tf = envs[ENVX(child)].env_tf;
    	child_tf.tf_eip = elf->e_entry;
    	if ((r = init_stack(child, argv, &child_tf.tf_esp)) < 0)
    		return r;
    
    //.........
    	if ((r = sys_env_set_trapframe(child, &child_tf)) < 0)
    		panic("sys_env_set_trapframe: %e", r);
    
  4. 根据elf头中的信息,将segment读入program并映射到正确的虚拟地址(lab2),这个过程相对还是复杂的,用了一些下层的函数。

    // Set up program segments as defined in ELF header.
    	ph = (struct Proghdr*) (elf_buf + elf->e_phoff);
    	for (i = 0; i < elf->e_phnum; i++, ph++) {
    		if (ph->p_type != ELF_PROG_LOAD)
    			continue;
    		perm = PTE_P | PTE_U;
    		if (ph->p_flags & ELF_PROG_FLAG_WRITE)
    			perm |= PTE_W;
    		if ((r = map_segment(child, ph->p_va, ph->p_memsz,
    				     fd, ph->p_filesz, ph->p_offset, perm)) < 0)
    			goto error;
    	}
    	close(fd);
    	fd = -1;
    
  5. 设置运行状态为可运行

这个函数使得用户环境可以加载文件系统中的程序并运行,而不是只能嵌入在内核中创建(像以往那样)。实现起来还是比较复杂,需要综合使用前面的lab中的函数,有空回来再仔细品读一下。

exercise 10

exercise 8,9只需要较少的修改,且助教帮忙完成过了,不再写了。最后一个练习是使得shell中支持IO重定向,简言之就是能读写文件,留了一个“<file”的情况–文件输入留给我们实现, usr/sh.c中加上就好了。

			if ((fd = open(t, O_RDONLY)) < 0) {
				cprintf("open %s for write: %e", t, fd);
				exit();
			}
			if (fd != 0) {
				dup(fd, 0);
				close(fd);
			}
			break;

通过练习6后面写到的open函数打开文件到文件描述符fd处,由于fd是open中自动分配出来的,不保证是0号描述符,按照要求我们需要把重定向到的文件放在0号描述符的地方,所以在fd不是0的情况下把它复制到0,通过dup()函数。

至此练习部分就完成了。

问题回答

(1) 请回答Exercise 1后的Question 1,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?

不需要,在创建文件系统环境时 e->env_tf.tf_eflags |= FL_IOPL_MASK;就已经把环境的eflags修改了,以后环境切换时会在栈帧上保存eflags,将来切换回来时再恢复,所以通过eflags的信息就能知道有无IO权限。

在这里插入图片描述

(2) 详细描述 JOS 中文件存储的结构、打开文件的过程以及往文件中写入数据的过程。

open部分 见前面的文件open

写入数据过程

和open类似,且是lab中刚完成的函数,简述一下。

首先通过lib/file.c中的库函数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); 
	
	return r;
	
}

文件系统通过serve_write处理这个请求,调用下层的file_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);

	// LAB 5: Your code here.
	struct OpenFile *o;
	int r;
	if ((r = openfile_lookup(envid, req->req_fileid, &o)) < 0) {
		return r;
	}
	int total = 0;
	while (1) {
		r = file_write(o->o_file, req->req_buf, req->req_n, o->o_fd->fd_offset);
		if (r < 0) return r;
		total += r;
		o->o_fd->fd_offset += r;
		if (req->req_n <= total)
			break;
	}
	return total;
	
}

可以看一下file_write,是一块一块读入内存,然后完成写操作的。(用到了练习4中的 file_block_walk函数)

int
file_write(struct File *f, const void *buf, size_t count, off_t offset)
{
	int r, bn;
	off_t pos;
	char *blk;

	// Extend file if necessary
	if (offset + count > f->f_size)
		if ((r = file_set_size(f, offset + count)) < 0)
			return r;

	for (pos = offset; pos < offset + count; ) {
		if ((r = file_get_block(f, pos / BLKSIZE, &blk)) < 0)
			return r;
		bn = MIN(BLKSIZE - pos % BLKSIZE, offset + count - pos);
		memmove(blk + pos % BLKSIZE, buf, bn);
		pos += bn;
		buf += bn;
	}

	return count;
}

(3) 对于此JOS,一个磁盘有多少个扇区?

看fs/fs.h中的数据,可以得出扇区大小是512B,磁盘大小3G,所以有3G/512B = 3 * 2 ^21个扇区。

#define SECTSIZE	512			// bytes per disk sector
#define BLKSECTS	(BLKSIZE / SECTSIZE)	// sectors per block

/* Disk block n, when in memory, is mapped into the file system
 * server's address space at DISKMAP + (n*BLKSIZE). */
#define DISKMAP		0x10000000

/* Maximum disk size we can handle (3GB) */
#define DISKSIZE	0xC0000000

(块与扇区关系:扇区大小是磁盘硬件的属性,而块大小是使用磁盘的操作系统的一个方面。文件系统的块大小必须是基础磁盘的扇区大小的倍数。UNIX xv6文件系统使用512字节的块大小,与基础磁盘的扇区大小相同。但是,大多数现代文件系统使用更大的块大小,因为存储空间变得便宜得多,并且以较大的粒度管理存储更为有效。我们的文件系统将使用4096字节的块大小,可以方便地匹配处理器的页面大小。)

(4) 请详细阐述,JOS中superblock的概念,以及superblock的布局和结构。

文件系统通常在磁盘的“易于查找”位置(例如最开始或结尾)保留某些磁盘块,以保存描述整个文件系统属性的元数据。 例如磁盘大小,上次安装文件系统的时间,上次检查文件系统的错误的时间等。这些特殊的块称为超级块

JOS的文件系统只有一个超级块,该超级块将始终位于磁盘上的block 1中。它的布局struct Superinc / fs.h中,去看一下:

struct Super {
	uint32_t s_magic;		// Magic number: FS_MAGIC
	uint32_t s_nblocks;		// Total number of blocks on disk
	struct File s_root;		// Root directory node
};

在文件系统启动后,super block会一直在内存中,方便后续被访问到。这一点不需要专门的操作,初始化时设置了缺页处理函数之后,由于很快就会用到super block,缺页处理函数就会将他载入内存并映射到文件系统虚拟地址空间。

	//fs/fs.c/fs_init

	bc_init();			//设置缺页处理函数

	// Set "super" to point to the super block.
	super = diskaddr(1);			//初始化super指针
	check_super();

(5) a.以open文件为例,阐述regular环境访问磁盘的流程; b.画出对应的流程图; c.fd page是什么时候设置好的?

a. 怎么又是这个open的问题~,不想第二次迁移那部分了,烦请移步问题回答(2)/用户环境open过程看详细流程。这边复制个大图(里面的数字不是顺序是标注用的):
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mB4vP5bB-1608652759365)(C:\Users\sft\Desktop\os\lab6\lab6 文件管理.assets\image-20201222203510133.png)]

b. 流程图,大概就是通过ipc机制,再用一次前面的图:
在这里插入图片描述
c. fd page是什么时候设置好的?——在文件系统中(rpc机制的服务端)

(1) fd——文件描述符,在用户环境试图打开文件时,就先要从自己的FDTABLE分配一页,用来将来盛放这个打开的文件的描述符。

if ((r = fd_alloc(&fd)) < 0)
		return r;

注意这里只是分配了一个“空”的描述符,仅仅是用户环境虚拟地址空间的一页,甚至连对应的物理页都没有(物理页通过page_alloc分配),要等server(文件系统)通过页映射返回真正的文件描述符页。

(2) 当打开文件的请求到达文件系统时,serve_open处理这个请求,其中需要在文件系统虚拟地址空间的1024个FD中分配一个:

// Find an open file ID
	if ((r = openfile_alloc(&o)) < 0) {					//从opentab数组中分配一个OpenFile结构
		if (debug)
			cprintf("openfile_alloc failed: %e", r);
		return r;
	}
	fileid = r;

分配open file结构,其中会分配fd对应的物理页,通过 sys_page_alloc

	switch (pageref(opentab[i].o_fd)) {			//如果fd对应的物理页被2个虚拟地址映射了,那么说明该fd结构已经被分配了。
		case 0:										//2个虚拟地址映射分别出现在fs进程和用户进程的页表中
			if ((r = sys_page_alloc(0, opentab[i].o_fd, PTE_P|PTE_U|PTE_W)) < 0)
				return r;
			/* fall through */
			......

(3)文件系统还负责设置fd

// Fill out the Fd structure
	o->o_fd->fd_file.id = o->o_fileid;
	o->o_fd->fd_omode = req->req_omode & O_ACCMODE;
	o->o_fd->fd_dev_id = devfile.dev_id;
	o->o_mode = req->req_omode;

(4)最后文件系统通过ipc的页映射传递的方式把这个fd页传回用户环境等待接收的fd页中,就完成fd的设置了!可以参考上面大图的1,3,4,5部分。(图中共享页描述欠妥,还是只有一个用户进程使用,不过文件系统也映射了那里

这样实验就完成了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值