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中有相似之处。
先看一下文件结构:
也就是说,前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
前面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
文件保存结构
首先画出上面这张整体图。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,前面已说到。
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_create
和 file_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)
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。大致看一下这个函数都做了什么(具体可以去看这个函数中的注释):
-
打开文件,并读取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; }
-
调用sys_exofork()创建环境(lab4)
if ((r = sys_exofork()) < 0) return r; child = r;
-
调用刚写的**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);
-
根据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;
-
设置运行状态为可运行
这个函数使得用户环境可以加载文件系统中的程序并运行,而不是只能嵌入在内核中创建(像以往那样)。实现起来还是比较复杂,需要综合使用前面的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 Super
在inc / 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过程看详细流程。这边复制个大图(里面的数字不是顺序是标注用的):
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部分。(图中共享页描述欠妥,还是只有一个用户进程使用,不过文件系统也映射了那里)
这样实验就完成了。