Introduction
在这次实验中将会实现创建进程并调用库函数装载和运行磁盘上的可执行文件。同时实现在操作系统内核的console上运行shell。这些特点都需要实现文件系统,在这里我们将实现1个简单可读写的文件系统。
本次实验新增加的文件如下:
fs/fs.c 操作文件系统在磁盘上的结构。
fs/bc.c 基于用户级页错误处理机制的块缓存。
fs/ide.c 最简单基于PIO的IDE磁盘驱动。
fs/serv.c 文件系统与客户端进程进行交互的服务端代码
lib/fd.c 实现传统UNIX文件描述符接口。
lib/file.c 磁盘类型的文件系统驱动
lib/console.c console类型的文件系统驱动
lib/spawn.c spawn系统调用实现
File system preliminaries
这部分内容主要介绍了一般文件系统的结构,包括扇区、块、超级块、块位图、文件元数据和目录的概念。后面JOS实现的文件系统设计到了这些东西,需要仔细阅读。
The File System
本次实验的目的不是让你实现整个文件系统,而是只要实现关键部分。尤其是如何读取块到块缓存并写回磁盘;映射文件偏移到磁盘块;实现文件的读取、写入和打开IPC接口调用。
Disk Access
JOS的文件系统需要能够访问磁盘,但是我们现在还没在内核实现访问磁盘。为了简化,这里我们抛弃传统单内核操作系统将磁盘驱动作为系统调用的实现方式,将磁盘驱动作为用户进程访问磁盘来实现。
这将很简单,通过轮询而不是中断来实现在用户空间进行磁盘访问。在x86处理器中可以通过设置EFLAGS寄存器中的IOPL位来允许用户态进程执行IO指令比如in和out。
Exercise 1:
i386_init函数中会创建1个文件系统进程,通过传递ENV_TYPE_FS标志给env_create函数,需要在该函数中允许文件系统进程执行IO指令。
回答:
在env_create函数中修改进程的eflag值。
if (type == ENV_TYPE_FS)
e->env_tf.tf_eflags |= FL_IOPL_MASK;
Question 1:
在进程切换时如何保证IO特权设置被保存和重载?
回答:
在进程切换时调用了env.pop_tf函数,其中进行了寄存器的恢复,在iret指令中恢复了eip,cs,eflags等寄存器。
The Block Cache
在JOS中,实现了1个简单的磁盘块缓存机制。该机制支持的磁盘大小最大为3GB,可以使用类似Lab 4中实现fork的COW页面机制。
其实现机制如下:
1、用文件系统服务进程的虚拟地址空间(0x10000000 (DISKMAP)到0xD0000000 (DISKMAP+DISKMAX))对应到磁盘的地址空间(3GB)。
2、初始文件系统服务进程不映射页面,如果要访问1个磁盘的地址空间,则发生页错误。
3、在页错误处理程序中,在内存中申请一个块的空间映射到相应的文件系统虚拟地址,然后去实际的物理磁盘上读取这个区域的数据到该内存区域,最后恢复文件系统服务进程。
Exercise2:
实现bc_pgfault和flush_block函数,其中bc_pgfault是页错误处理程序,作用是从磁盘上装载页。
回答:
主要是实现磁盘块缓冲的页面处理和写回部分,主要用到跟磁盘直接交互的IDE驱动函数。
int ide_read(uint32_t secno, void *dst, size_t nsecs)
int ide_write(uint32_t secno, void *dst, size_t nsecs)
secno对应IDE磁盘上的扇区编号,dst为当前文件系统服务程序空间中的对应地址,nsecs为读写的扇区数。
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);
// 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.
addr = ROUNDDOWN(addr, PGSIZE);
if ((r = sys_page_alloc(0, addr, PTE_U | PTE_P | PTE_W)) < 0)
panic("in bc_pgfault, sys_page_alloc: %e", r);
if ((r = ide_read(blockno * BLKSECTS, 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);
if (bitmap && block_is_free(blockno))
panic("reading free block %08x\n", blockno);
}
先根据地址计算出对应的blockno,然后检查正确性包括地址是否在映射范围内、对应的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);
int r;
addr = ROUNDDOWN(addr, PGSIZE);
if (va_is_mapped(addr) && va_is_dirty(addr)) {
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)
panic("in flush_block, sys_page_map: %e", r);
}
}
先根据地址计算对应的blockno,然后然后检查正确性,最后判断是否是脏块,如果是则写回磁盘并清除dirty位。
The Block Bitmap
在fs_init函数设置块位图之后,我们就能将位图当做位数组来对待。
Exercise 3:
以free_block为参考实现alloc_block,功能是在位图中查找1个空闲磁盘块,标记为占用并返回块序号。当你分配1个块时,为了维护文件系统的一致性,你需要快速地使用flush_block函数写回你对位图的修改。
回答:
这部分比较简单,参考free_block函数的实现即可。
int
alloc_block(void)
{
// The bitmap consists of one or more blocks. A single bitmap block
// contains the in-use bits for BLKBITSIZE blocks. There are
// super->s_nblocks blocks in the disk altogether.
uint32_t blockno;
for (blockno = 0; blockno < super->s_nblocks; blockno++) {
if (block_is_free(blockno)) {
bitmap[blockno/32] ^= 1<<(blockno%32);
flush_block(bitmap);
return blockno;
}
}
return -E_NO_DISK;
}
File Operations
JOS在fs/fs.c中已经提供了1系列函数来操作和管理文件结构,浏览和管理目录,解析文件名。具体各个函数的功能如下:
file_block_walk(struct File *f, uint32_t filebno, uint32_t **ppdiskbno, bool alloc):
寻找一个文件结构f中的第fileno个块指向的磁盘块编号放入ppdiskbno。如果filebno小于NDIRECT,则返回属于f.direct[NDIRECT]中的相应链接,否则返回f_indirect中查找的块。如果alloc为真且相应磁盘块不存在,则分配1个。
dir_lookup(struct File *dir, const char *name, struct File **file):
在目录dir中查找名字为name的文件,如果找到则让file指向该文件结构体。
dir_alloc_file(struct File *dir, struct File **file):
在dir对应的File结构体中分配1个File的指针连接给file,用于添加文件的操作。
skip_slash(const char *p):
用于路径中的字符串处理,跳过斜杠。
walk_path(const char *path, struct File **pdir, struct File **pf, char *lastelem):
path为从绝对路径的文件名,如果成功找到该文件,则把相应的文件结构体赋值给pf,其所在目录的文件结构体赋值给pdir,lastlem为失效时最后剩下的文件名。
file_free_block(struct File *f, uint32_t filebno):
释放1个文件中的第filebno个磁盘块。此函数在file_truncate_blocks中被调用。
file_truncate_blocks(struct File *f, off_t newsize):
将文件设置为缩小后的新大小,清空那些被释放的物理块。
Exercise 4:
实现file_block_walk函数和file_get_block函数。
回答:
file_block_walk函数寻找一个文件结构f中的第fileno个块指向的磁盘块编号放入ppdiskbno。
static int
file_block_walk(struct File *f, uint32_t filebno, uint32_t **ppdiskbno, bool alloc)
{
int r;
if (filebno >= NDIRECT + NINDIRECT)
return -E_INVAL;
if (filebno < NDIRECT) {
if (ppdiskbno)
*ppdiskbno = f->f_direct + filebno;
return 0;
}
if (!alloc && !f->f_indirect)
return -E_NOT_FOUND;
if (!f->f_indirect) {
if ((r = alloc_block()) < 0)
return -E_NO_DISK;
f->f_indirect = r;
memset(diskaddr(r), 0, BLKSIZE);
flush_block(diskaddr(r));
}
if (ppdiskbno)
*ppdiskbno = (uint32_t*)diskaddr(f->f_indirect) + filebno - NDIRECT;
return 0;
}
file_get_block函数先调用file_walk_block函数找到文件中的目标块,然后将其转换为地址空间中的地址赋值给blk。
int
file_get_block(struct File *f, uint32_t filebno, char **blk)
{
// LAB 5: Your code here.
int r;
uint32_t *ppdiskbno;
if ((r = file_block_walk(f, filebno, &ppdiskbno, 1)) < 0)
return r;
if (*ppdiskbno == 0) {
if ((r = alloc_block()) < 0)
return -E_NO_DISK;
*ppdiskbno = r;
memset(diskaddr(r), 0, BLKSIZE);
flush_block(diskaddr(r));
}
*blk = diskaddr(*ppdiskbno);
return 0;
}
The file system interface
现在我们已经实现了文件系统必要的函数,需要让它能被其它进程调用使用。我们将使用Lab4中实现的IPC方式来让其它进程来与文件系统服务进程交互来进行文件操作。
在虚线下的部分是普通进程如何发送一个读请求到文件系统服务进程的机制。首先read操作文件描述符,分发给合适的设备读函数devfile_read 。devfile_read函数实现读取磁盘文件,作为客户端文件操作函数。然后建立请求结构的参数,调用fsipc函数来发送IPC请求并解析返回的结果。
文件系统服务端的代码在fs/serv.c中,服务进程循环等待直到收到1个IPC请求。然后分发给合适的处理函数,最后通过IPC发回结果。对于读请求,服务端会分发给serve_read函数
在JOS实现的IPC机制中,允许进程发送1个32位数和1个页。为了实现发送1个请求从客户端到服务端,我们使用32位数来表示请求类型,存储参数在联合Fsipc位于共享页中。在客户端我们一直共享fsipcbuf所在页,在服务端我们映射请求页到fsreq地址(0x0ffff000)。
服务端也会通过IPC发送结果。我们使用32位数作为函数的返回码。FSREQ_READ 和FSREQ_STAT函数也会返回数据,它们将数据写入共享页返回给客户端。
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];
};
这里需要了解一下union Fsipc,文件系统中客户端和服务端通过IPC进行通信,通信的数据格式就是union Fsipc,它里面的每一个成员对应一种文件系统的操作请求。每次客户端发来请求,都会将参数放入一个union Fsipc映射的物理页到服务端。同时服务端还会将处理后的结果放入到Fsipc内,传递给客户端。文件服务端进行的地址空间布局如下:
OpenFile结构是服务端进程维护的一个映射,它将一个真实文件struct File和用户客户端打开的文件描述符struct Fd对应到一起。每个被打开文件对应的struct Fd都被映射到FILEEVA(0xd0000000)往上的1个物理页,服务端和打开这个文件的客户端进程共享这个物理页。客户端进程和文件系统服务端通信时使用0_fileid来指定要操作的文件。
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
};
文件系统默认最大同时可以打开的文件个数为1024,所以有1024个strcut Openfile,对应着服务端进程地址空间0xd0000000往上的1024个物理页,用于映射这些对应的struct Fd。
struct Fd是1个抽象层,JOS和Linux一样,所有的IO都是文件,所以用户看到的都是Fd代表的文件。但是Fd会记录其对应的具体对象,比如真实文件、Socket和管道等等。现在只用文件,所以union中只有1个FdFile。
struct Fd {
int fd_dev_id;
off_t fd_offset;
int fd_omode;
union {
// File server files
struct FdFile fd_file;
};
};
Exercise 5:
实现fs/serv.c文件中的serve_read函数。
回答:
首先需要弄清楚服务端进程的内部结构,工作机制。
void
serve(void)
{
uint32_t req, whom;
int perm, r;
void *pg;
while (1) {
perm = 0;
req = ipc_recv((int32_t *) &whom, fsreq, &perm);
if (debug)
cprintf("fs req %d from %08x [page %08x: %s]\n",
req, whom, uvpt[PGNUM(fsreq)], fsreq);
// All requests must contain an argument page
if (!(perm & PTE_P)) {
cprintf("Invalid request from %08x: no argument page\n",
whom);
continue; // just leave it hanging...
}
pg = NULL;
if (req == FSREQ_OPEN) {
r = serve_open(whom, (struct Fsreq_open*)fsreq, &pg, &perm);
} else if (req < NHANDLERS && handlers[req]) {
r = handlers[req](whom, fsreq);
} else {
cprintf("Invalid request code %d from %08x\n", req, whom);
r = -E_INVAL;
}
ipc_send(whom, r, pg, perm);
sys_page_unmap(0, fsreq);
}
}
服务端主循环会使用轮询的方式接受客户端进程的文件操作请求。每次操作如下:
1、从IPC接受1个请求类型req以及数据页fsreq
2、然后根据req来执行相应的服务端处理函数
3、将相应服务端函数的执行结果(如果产生了数据也则有pg)通过IPC发送回调用进程
4、将映射好的物理页fsreq取消映射
服务端函数定义在handler数组,通过请求号进行调用。
typedef int (*fshandler)(envid_t envid, union Fsipc *req);
fshandler handlers[] = {
// Open is handled specially because it passes pages
/* [FSREQ_OPEN] = (fshandler)serve_open, */
[FSREQ_READ] = serve_read,
[FSREQ_STAT] = serve_stat,
[FSREQ_FLUSH] = (fshandler)serve_flush,
[FSREQ_WRITE] = (fshandler)serve_write,
[FSREQ_SET_SIZE] = (fshandler)serve_set_size,
[FSREQ_SYNC] = serve_sync
};
#define NHANDLERS (sizeof(handlers)/sizeof(handlers[0]))
对于读文件请求,调用serve_read函数来处理。
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);
struct OpenFile *o;
int r, req_n;
if ((r = openfile_lookup(envid, req->req_fileid, &o)) < 0)
return r;
req_n = req->req_n > PGSIZE ? PGSIZE : req->req_n;
if ((r = file_read(o->o_file, ret->ret_buf, req_n, o->o_fd->fd_offset)) < 0)
return r;
o->o_fd->fd_offset += r;
return r;
}
先从Fsipc中获取读请求的结构体,然后在openfile中查找fileid对应的Openfile结构体,紧接着从openfile长相的o_file中读取内容到保存返回结果的ret_buf中,并移动文件偏移指针。
然后我们可以看一下用户进程发送读取请求的函数devfile_read,主要操作是封装Fsipc设置请求类型为FSREQ_READ,在接受到返回后,将返回结果拷贝到自己的buf中。
static ssize_t
devfile_read(struct Fd *fd, void *buf, size_t n)
{
int r;
fsipcbuf.read.req_fileid = fd->fd_file.id;
fsipcbuf.read.req_n = n;
if ((r = fsipc(FSREQ_READ, NULL)) < 0)
return r;
assert(r <= n);
assert(r <= PGSIZE);
memmove(buf, fsipcbuf.readRet.ret_buf, r);
return r;
}
Exercise 6:
模仿read请求实现serve_write函数和devfile_write函数。
回答:
实现与read请求类似。
// fs/serv.c
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);
struct OpenFile *o;
int r, req_n;
if ((r = openfile_lookup(envid, req->req_fileid, &o)) < 0)
return r;
req_n = req->req_n > PGSIZE ? PGSIZE : req->req_n;
if ((r = file_write(o->o_file, req->req_buf, req_n, o->o_fd->fd_offset)) < 0)
return r;
o->o_fd->fd_offset += r;
return r;
}
// lib/file.c
static ssize_t
devfile_write(struct Fd *fd, const void *buf, size_t n)
{
int r;
if (n > sizeof(fsipcbuf.write.req_buf))
n = sizeof(fsipcbuf.write.req_buf);
fsipcbuf.write.req_fileid = fd->fd_file.id;
fsipcbuf.write.req_n = n;
memmove(fsipcbuf.write.req_buf, buf, n);
if ((r = fsipc(FSREQ_WRITE, NULL)) < 0)
return r;
return r;
}
Spawning Processes
在lib/spawn.c中已经实现了spawn函数来创建1个新进程并从文件系统中装载1个程序运行,然后父进程继续执行。这与UNIX的fork函数类似在创建新进程后马上执行exec。
Exercise 7:
spawn函数依赖于新的系统调用sys_env_set_trapframe来初始化新创建进程的状态。实现sys_env_set_trapframe函数(不要忘记在syscall中分发对应的调用号)。
回答:
sys_env_set_trapframe函数实现简单,主要是用来拷贝父进程的寄存器。
static int
sys_env_set_trapframe(envid_t envid, struct Trapframe *tf)
{
struct Env *e;
int r;
if ((r = envid2env(envid, &e, true)) < 0)
return -E_BAD_ENV;
user_mem_assert(e, tf, sizeof(struct Trapframe), PTE_U);
e->env_tf = *tf;
e->env_tf.tf_cs |= 3;
e->env_tf.tf_eflags |= FL_IF;
return 0;
}
Sharing library state across fork and spawn
UNIX文件描述符包括pipe,console I/O。 在JOS中,这些设备类型都有1个与与它关联的struct Dev,里面有实现read/write等文件操作的函数指针。在lib/fd.c中实现了传统UNIX的文件描述符接口。
在lib/fd.c中也包括每个客户进程的文件描述符表布局,开始于FSTABLE。这块空间为每个描述符保留了1个页的地址空间。 在任何时候,只有当文件描述符在使用中才在文件描述符表中映射页。
我们想要共享文件描述符状态在调用fork和spawn创建新进程。当下,fork函数使用COW会将状态复制1份而不是共享。在spawn中,状态则不会被拷贝而是完全舍弃。
所以我们将改变fork来共享状态。在inc/lib.h中新定义了PTE_SHARE位来标识页共享。当页表入口中设置了该位,则应该从父进程中拷贝PTE映射到子进程在fork和spawn时。
Exercise 8:
改变duppage函数实现上述变化,如果页表入口有设置PTE_SHARE位,那么直接拷贝映射。类似地,实现copy_shared_pages函数。
回答:
static int
duppage(envid_t envid, unsigned pn)
{
int r;
void *addr;
pte_t pte;
int perm;
addr = (void *)((uint32_t)pn * PGSIZE);
pte = uvpt[pn];
if (pte & PTE_SHARE) {
if ((r = sys_page_map(sys_getenvid(), addr, envid, addr, pte & PTE_SYSCALL)) < 0) {
panic("duppage: page mapping failed %e", r);
return r;
}
}
else {
perm = PTE_P | PTE_U;
if ((pte & PTE_W) || (pte & PTE_COW))
perm |= PTE_COW;
if ((r = sys_page_map(thisenv->env_id, addr, envid, addr, perm)) < 0) {
panic("duppage: page remapping failed %e", r);
return r;
}
if (perm & PTE_COW) {
if ((r = sys_page_map(thisenv->env_id, addr, thisenv->env_id, addr, perm)) < 0) {
panic("duppage: page remapping failed %e", r);
return r;
}
}
}
return 0;
}
在原先的基础上新添加PTE_SHARE位判断。
copy_shared_pages(envid_t child)
{
int i, j, pn, r;
for (i = PDX(UTEXT); i < PDX(UXSTACKTOP); i++) {
if (uvpd[i] & PTE_P) {
for (j = 0; j < NPTENTRIES; j++) {
pn = PGNUM(PGADDR(i, j, 0));
if (pn == PGNUM(UXSTACKTOP - PGSIZE))
break;
if ((uvpt[pn] & PTE_P) && (uvpt[pn] & PTE_SHARE)) {
if ((r = sys_page_map(0, (void *)PGADDR(i, j, 0), child, (void *)PGADDR(i, j, 0), uvpt[pn] & PTE_SYSCALL)) < 0)
return r;
}
}
}
}
return 0;
}
跟ofrk类似遍历进程页表所有入口,拷贝设置了PTE_SHARE位的页映射到子进程。
The keyboard interface
为了是shell能工作,我们需要实现键盘中断和串口中断。在ilib/console.c文件中已经实现了console类型的input/output文件。kbd_intr和serial_intr会用最近读取的输入填充console文件类型的buffer。
Exercise 9:
在kern/trap.c中调用kbd_intr来处理IRQ_OFFSET+IRQ_KBD中断和调用serial_intr来处理IRQ_OFFSET+IRQ_SERIAL中断。
回答:
具体的kbd_intr和serial_intr函数未具体研究。
if (tf->tf_trapno == IRQ_OFFSET + IRQ_KBD) {
kbd_intr();
return;
}
if (tf->tf_trapno == IRQ_OFFSET + IRQ_SERIAL) {
serial_intr();
return;
}
The Shell
运行icode进程,它先会运行init进程,将console作为输入输出文件描述符,然后运行spawn脚本即shell。我们需要实现shell中的一些特性,类似于homework 2。
Exercise 10:
实现shell支持IO重定向。
回答:
与homework 2类似,改变输入输出文件描述符对应的文件。
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‘.
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;
至此,整个Lab 5就完成了,但是里面还有一些具体细节,在这里没有描述清楚。
MIT6.828 Lab 5: File system, Spawn and Shell
原文:http://blog.csdn.net/bysui/article/details/51868917