Lab 5: File system, Spawn and Shell
1. File system preliminaries
在lab中我们要使用的文件系统比大多数“真实”文件系统更简单,包括XV6 UNIX的文件系统,但它足以提供基本功能:创建,读取,写入和删除在分层目录结构中组织的文件。
我们仅开发一个单用户操作系统, 因此,我们的文件系统不支持文件所有权或权限。 我们的文件系统目前也不支持硬链接,符号链接,时间戳或大多数UNIX文件系统的特殊设备文件。
1. On-Disk File System Structure
大多数UNIX文件系统将可用磁盘空间分为两种主要类型的区域:inode区域和数据区域。 UNIX文件系统为文件系统中的每个文件分配一个inode;文件的inode保存关于文件的关键元数据,例如其stat属性和指向其数据块的指针。数据区域被划分成更大(通常为8KB或更多)的数据块,文件系统在其中存储文件数据和目录元数据。目录条目包含文件名和指向inode的指针;如果文件系统中的多个目录条目引用该文件的inode,则文件被称为硬链接。由于我们的文件系统不支持硬链接,所以我们不需要这种级别的重定向,因此可以方便的简化:我们的文件系统根本不会使用inode,而只是在(唯一)的目录条目中存储所有的文件(或子目录)的元数据。
文件和目录逻辑上都是由一系列数据块组成的,这些数据块可能散布在整个磁盘上,就像用户环境的虚拟地址空间的页面可以分散在整个物理内存中一样。文件系统环境隐藏数据块布局的细节,仅呈现在文件任意偏移量处读/写字节序列的接口。文件系统环境将对目录的所有修改作为文件创建和删除等操作内部处理的一部分。我们的文件系统允许用户环境直接读取目录元数据(例如,read),这意味着用户环境可以自己执行目录扫描操作(例如,实现ls程序),而不必依赖额外特殊的对文件系统的调用。对目录扫描方法的缺点,以及大多数现代UNIX变体阻止它的原因在于它使应用程序依赖于目录元数据的格式,使得在不更改或至少重新编译应用程序的情况下难以更改文件系统的内部布局。
简单来讲,我们文件系统就只有一个数据结构保存文件,没有索引。
1.1 Sectors and Blocks
大多数磁盘不能以字节粒度执行读取和写入,而是以扇区为单位执行读取和写入操作。在JOS中,扇区为512字节。文件系统实际上以块为单位分配和使用磁盘存储。请注意两个术语之间的区别:扇区大小是磁盘硬件的属性,而块大小是操作系统使用磁盘的一个方面。文件系统的块大小必须是底层磁盘扇区大小的倍数。
UNIX xv6
文件系统使用512字节的块大小,与底层磁盘的扇区大小相同。然而,大多数现代文件系统使用更大的块大小,因为存储空间已经变得更便宜,并且以更大的粒度来管理存储效率更高。我们的文件系统将使用4096字节的块大小,方便地匹配处理器的页面大小。
简单来讲,磁盘默认512字节是一个扇区,我们系统4096字节一个块,也就是8个扇区一个块。
1.2 Superblocks
文件系统通常将某些磁盘块保留在磁盘上的“易于查找”位置(例如起始或最后),以保存描述整个文件系统属性的元数据,例如块大小,磁盘大小,找到根目录所需的任何元数据,文件系统上次挂载的时间,文件系统上次检查错误的时间等等。这些特殊块称为超级块。
我们的文件系统将只有一个超级块,它将始终位于磁盘上的块1。它的布局由struct Super在inc/fs.h中定义。块0通常保留用于保存引导加载程序和分区表,因此文件系统通常不使用第一个磁盘块。许多“真正的”文件系统具有多个超级块,这几个副本在磁盘的几个广泛间隔的区域,以便如果其中一个被损坏或磁盘在该区域中产生媒体错误,则仍然可以找到其他超级块,并将其用于访问文件系统。
其中具体块的数据结构如下
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
};
1.3 File Meta-data
描述文件系统中的文件的元数据的布局由inc/fs.h
中的struct File
定义。该元数据包括文件的名称,大小,类型(常规文件或目录)以及指向包含该文件的块的指针。如上所述,我们没有inode,所以元数据存储在磁盘上的目录条目中。与大多数“真实”文件系统不同,为简单起见,我们将使用这个struct File
来表示在磁盘和内存中出现的文件元数据。
struct File
中的f_direct
数组包含存储文件前10个(NDIRECT)块的块号的空间,这前10个块被称之为文件的直接块。对于大小为10 * 4096 = 40KB
的小文件,这意味着所有文件块的块号将直接适用于struct File本身。然而,对于较大的文件,我们需要一个地方来保存文件的其他块号。因此,对于大于40KB的任何文件,我们分配一个额外的磁盘块,称为文件的间接块,最多容纳4096/4 = 1024个附加块号。因此,我们的文件系统允许文件的大小可达1034个块,或者刚刚超过四兆字节大小。为了支持更大的文件,“真实”文件系统通常也支持双重和三重间接块。
struct File {
char f_name[MAXNAMELEN]; // filename 文件名
off_t f_size; // file size in bytes 文件大小
uint32_t f_type; // file type 文件类型
// Block pointers.
// A block is allocated iff its value is != 0.
uint32_t f_direct[NDIRECT]; // direct blocks 直接块
uint32_t f_indirect; // indirect block 间接块
// Pad out to 256 bytes; must do arithmetic in case we‘re compiling
// fsformat on a 64-bit machine.
uint8_t f_pad[256 - MAXNAMELEN - 8 - 4*NDIRECT - 4];
} __attribute__((packed)); // required only on some 64-bit machines
1.4 Directories versus Regular Files
我们的文件系统中的struct File
可以表示常规文件或目录;这两种类型的“文件”通过struct File
中的类型字段进行区分。文件系统以完全相同的方式管理常规文件和目录文件,除了它不解释与常规文件相关联的数据块的内容,而文件系统将目录文件的内容解释为一系列描述目录中的文件和子目录的struct File
// File types
#define FTYPE_REG 0 // Regular file 文件
#define FTYPE_DIR 1 // Directory 目录
我们的文件系统中的超级块包含一个struct File(其实struct Super中的根字段),它保存文件系统根目录的元数据。根目录文件的内容是描述位于文件系统根目录下的文件和目录的struct File序列。根目录中的任何子目录可以依次包含表示子子目录的更多的struct File,依此类推。
2. The File System
本lab
的目标不是实现整个文件系统,而是仅实现某些关键组件。特别是,需要实现将块读入块高速缓存
并将其刷新回磁盘
;分配磁盘块
;将文件偏移映射到磁盘块
;并在IPC接口中中实现读,写和打开
。因为你不会自己实现所有的文件系统,所以你需要熟悉提供的代码和各种文件系统接口。
2.1 Disk Access
x86处理器使用EFLAGS寄存器中的IOPL位来确定是否允许保护模式代码执行特殊的设备I/O指令,如IN和OUT指令。由于我们需要访问的所有IDE磁盘寄存器位于x86的I/O空间中,而不是内存映射,因此为文件系统环境提供“I/O特权”是我们唯一需要做的,以便允许文件系统访问这些寄存器。实际上,EFLAGS寄存器中的IOPL位为内核提供了一种简单的“全或无”方法来控制用户态代码能否访问I/O空间。在我们的实现中,我们希望文件系统环境能够访问I/O空间,但是我们不希望任何其他环境能够访问I/O空间。
所以对于第一个Exercise1是非常简单的,只需要一行代码。
不过这里我遇到了一个问题
这个问题我修了一个小时。。fuck
后面发现是make
参数的问题。本来的makefile
文件设置了Werror
参数。这表示会把warning当作error来处理。后面把它删掉就没问题了
// If this is the file server (type == ENV_TYPE_FS) give it I/O privileges.
// LAB 5: Your code here.
if (type == ENV_TYPE_FS) {
env->env_tf.tf_eflags |= FL_IOPL_MASK;
}
3. The Block Cache
在我们的文件系统中,我们将在处理器的虚拟内存系统的帮助下实现一个简单的“缓冲区缓存”(真正只是一个块缓存)。 块缓存的代码在fs/bc.c
中。
我们的文件系统将仅限于处理小于等于3GB的磁盘。 我们为文件系统预留了固定的3GB区域。从0x10000000(diskmap)到0xd0000000(diskmap + diskmax),作为磁盘的“内存映射”版本。 例如,磁盘块0映射在虚拟地址0x10000000,磁盘块1映射在虚拟地址0x10001000等等。
Exercise 2
实现bc_pgfault()
和flush_block()
。
bc_pgfault()是文件系统中的进程缺页处理函数,负责将数据从磁盘读取到对应的内存
参考注释的提示也是不难实现
bc_pgfault()
实现
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.
//
// LAB 5: you code here:
addr = ROUNDDOWN(addr,PGSIZE);
sys_page_alloc(0,addr,PTE_U | PTE_W | PTE_P);
if ((r = ide_read(blockno * BLKSECTS,addr,BLKSECTS)) < 0) {
panic("ide_read error : %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("sys_page_map error : %e",r);
}
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;
int r;
if (addr < (void*)DISKMAP || addr >= (void*)(DISKMAP + DISKSIZE))
panic("flush_block of bad va %08x", addr);
// LAB 5: Your code here.
// round addr down
addr = ROUNDDOWN(addr,PGSIZE);
if (!va_is_mapped(addr) || ! va_is_dirty(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 ) {
panic("sys_page_map error : %e",r);
}
}
4. The Block Bitmap
fs_init()中已经初始化了bitmap,我们能通过bitmap访问磁盘的block 1,也就是位数组,每一位代表一个block,1表示该block未被使用,0表示已被使用。我们实现一系列管理函数来管理这个位数组。
Exercise 3
实现fs/fs.c中的alloc_block(),该函数搜索bitmap位数组,返回一个未使用的block,并将其标记为已使用。
这里主要仿照free_block.c
函数来写.
根据blockno
来清楚bitmap数组。因此当我们设置bitmap数组的时候,只需要做取反操作即可
// Mark a block free in the bitmap
void
free_block(uint32_t blockno)
{
// Blockno zero is the null pointer of block numbers.
if (blockno == 0)
panic("attempt to free zero block");
bitmap[blockno/32] |= 1<<(blockno%32);
}
因此alloc_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.
// LAB 5: Your code here.
for (int i = 3; i < super->s_nblocks; i++) {
if (block_is_free(i)) {
bitmap[i/32] &= ~(1<<(i%32));
return i;
}
}
return -E_NO_DISK;
}
5. File Operations
fs/fs.c文件提供了一系列函数用于管理File结构,扫描和管理目录文件,解析绝对路径。
基本的文件系统操作:
1. static int file_block_walk(struct File *f, uint32_t filebno, uint32_t **ppdiskbno, bool alloc){}
2. int file_get_block(struct File *f, uint32_t filebno, char **blk)
解析路径path,填充pdir和pf地址处的File结构。比如/aa/bb/cc.c那么pdir指向代表bb目录的File结构,pf指向代表cc.c文件的File结构。又比如/aa/bb/cc.c,但是cc.c此时还不存在,那么pdir依旧指向代表bb目录的File结构,但是pf地址处应该为0,lastelem指向的字符串应该是cc.c。
3. static int walk_path(const char **path*, struct *File* ***pdir*, struct *File* ***pf*, char **lastelem*)
该函数寻找dir指向的文件内容。并寻找制定name的file结构保存到file指针处
4. static int dir_lookup(struct File *dir, const char *name, struct File **file)
在dir目录文件的内容中寻找一个未被使用的File结构,将其地址保存到file的地址处
5. static int dir_alloc_file(struct File *dir, struct File **file)
基本的文件操作
在给定path创建file,如果创建成功pf指向新创建的File指针
1. int file_create(const char *path, struct File **pf)
寻找path对应的File结构地址,保存到pf地址处。
2. int file_open(const char *path, struct File **pf)
从文件f的offset字节处读取count字节到buf处
3. size_t* file_read(struct *File* **f*, void **buf*, *size_t* *count*, *off_t* *offset*)
将buf处的count字节写到文件f的offset开始的位置。
4. int file_write(struct File *f, const void *buf, size_t count, off_t offset)
Exercise 4
实现file_block_walk()和file_get_block()。
file_block_walk():
该函数查找f指针指向文件结构的第filebno
个block
的存储地址,保存到ppdiskbno
中。
- 如果
filebno > NDIRECT
则需要去checkf_indirect
。如果还未创建indirect块。但是alloc
为真,那么将分配要给新的block作为该文件的f->f_indirect。类比页表管理的pgdir_walk()。 - 最简单的情况是可以在
NDIRECT
中获取到对应的块
static int
file_block_walk(struct File *f, uint32_t filebno, uint32_t **ppdiskbno, bool alloc)
{
// LAB 5: Your code here.
// first do sanity check
if (filebno >= NDIRECT + NINDIRECT) {
return -E_INVAL;
}
uintptr_t *block_addr = NULL;
if (filebno < NDIRECT) {
block_addr = &f->f_direct[filebno];
} else {
int r;
if (f->f_indirect == 0) {
if (alloc) {
r = alloc_block();
if (r < 0) {
return r;
}
memset(diskaddr(r),0,BLKSIZE);
f->f_indirect = r;
} else {
return -E_NOT_FOUND;
}
}
uint32_t *indir = (uint32_t *)diskaddr(f->f_indirect);
block_addr = indir[filebno - NDIRECT];
}
*ppdiskbno = block_addr;
return 0;
}
file_get_block
- 这个就是获取制定的
block
- 首先调用我们之前实现的
file_block_walk
函数。如果没问题则直接ok - 否则的话会分配一个新的block,注意这里要把它flush到磁盘里
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;
}
int blockno;
if (*ppdiskbno == 0) {
if ((blockno = alloc_block()) < 0) {
return blockno;
}
*ppdiskbno = blockno;
flush_block(diskaddr(blockno));
}
*blk = diskaddr(*ppdiskbno);
return 0;
}
?
6. The file system interface
当然其他env也有对于文件系统环境的请求。这时候我们需要有类似下面的机制
Exercise 5
Implement serve_read
in fs/serv.c
.
serve_read 的繁重工作将由 fs/fs.c 中已经实现的 file_read 完成(反过来,它只是对 file_get_block 的一堆调用)。 serve_read 只需要提供用于文件读取的 RPC 接口。 查看 serve_set_size 中的注释和代码,以大致了解服务器功能的结构。
那我们在写这个代码之前首先就要剖析一下整个ipc的发生过程
-
在
regular_env
中调用read函数这个函数首先根据fd_lookup找到对应fdnum的fd结构
随后根据dev_lookup找到对应的dev信息
然后调用dev_read(fd,buf,n)
ssize_t read(int fdnum, void *buf, size_t n) { int r; struct Dev *dev; struct Fd *fd; if ((r = fd_lookup(fdnum, &fd)) < 0 || (r = dev_lookup(fd->fd_dev_id, &dev)) < 0) return r; if ((fd->fd_omode & O_ACCMODE) == O_WRONLY) { cprintf("[%08x] read %d -- bad mode\n", thisenv->env_id, fdnum); return -E_INVAL; } if (!dev->dev_read) return -E_NOT_SUPP; return (*dev->dev_read)(fd, buf, n); } // 涉及到的三个不同的dev; static struct Dev *devtab[] = { &devfile, &devpipe, &devcons, 0 };
-
随后是调用
lib/file.c
的- 这里是把请求参数存储在fsipcbuf.read中
- 然后调用fsipc去
向服务器端发送read请求
。请求成功后结果也是保存在共享页面fsipcbuf中,然后读到指定的buf就行。
static ssize_t devfile_read(struct Fd *fd, void *buf, size_t n) { // Make an FSREQ_READ request to the file system server after // filling fsipcbuf.read with the request arguments. The // bytes read will be written back to fsipcbuf by the file // system server. 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; }
-
看
lib/file.c/fsipc()
函数- 找到第一个fs类型的env
- 然后调用ipc_send发送ipc信号
- 利用ipc_recv得到返回结果
static int fsipc(unsigned type, void *dstva) { 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); }
-
接下来看
fs/serv.c/server
函数这里有一个while循环等到请求的接受,对于上面发送的ipc请求这里会被接收到
这里的逻辑还是非常简单的
- 当ipc_recv得到结果之后,就会往下执行
- 然后根据请求的类型做出不同的处理。对于read操作而言的话
- 就会执行server_read函数来处理这个ipc请求
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 < ARRAY_SIZE(handlers) && 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); } }
-
server_read
函数这个函数就是我们要实现的函数。根据提示我们来实现一下
- 首先找到ipc->read->req_fileid对应的OpenFile。
- 然后调用
file_read
去读内容到ipc->readRet->ret_buf
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; if ((r = openfile_lookup(envid, req->req_fileid, &o)) < 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; }
Exercise6
Implement serve_write
in fs/serv.c
and devfile_write
in lib/file.c
.
这个的调用逻辑是和上面一样的,所以就不分析了。那我们直接看写操作是如何实现的
首先实现server_write
这个和server_read
基本上完全一致的
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 req_n = req->req_n > PGSIZE ? PGSIZE : req->req_n;
int r;
if ((r = openfile_lookup(envid, req->req_fileid, &o)) < 0)
return r;
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;
}
然后实现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 csode here
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);
return fsipc(FSREQ_WRITE, NULL);
}
7. Spawning Processes
我们已经为您提供了 spawn 的代码(参见 lib/spawn.c),它创建一个新环境,将文件系统中的程序映像加载到其中,然后启动运行该程序的子环境。 然后父进程独立于子进程继续运行。 spawn 函数的作用类似于 UNIX 中的 fork,然后是子进程中的 exec。
我们实现了 spawn 而不是 UNIX 风格的 exec,因为 spawn 更容易以“外内核方式”从用户空间实现,而无需内核的特殊帮助。 考虑一下为了在用户空间中实现 exec 必须做什么。
Exercise7
spawn 依赖于新的系统调用 sys_env_set_trapframe 来初始化新创建环境的状态。 在 kern/syscall.c
中实现 sys_env_set_trapframe
(不要忘记在 syscall() 中调度新的系统调用)。
这个系统调用其实也不难实现.可以参考env_alloc()
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!
struct Env *e;
int32_t ret;
if ((ret = envid2env(envid, &e, 1)) < 0) {
return ret; // -E_BAD_ENV
}
if ((ret = user_mem_check(e, tf, sizeof(struct Trapframe), PTE_U)) < 0) {
return ret;
}
memmove(&e->env_tf, tf, sizeof(struct Trapframe));
e->env_tf.tf_ds = GD_UD | 3;
e->env_tf.tf_es = GD_UD | 3;
e->env_tf.tf_ss = GD_UD | 3;
e->env_tf.tf_cs = GD_UT | 3;
e->env_tf.tf_eflags |= FL_IF;
e->env_tf.tf_eflags &= ~FL_IOPL_MASK; //普通进程不能有IO权限
return 0;
}
8. Sharing library state across fork and spawn
我们希望在 fork 和 spawn 之间共享文件描述符状态,但文件描述符状态保存在用户空间内存中。在 fork 上,内存将被标记为 copy-on-write,因此状态将被复制而不是共享。 (这意味着环境将无法在它们自己没有打开的文件中查找,并且管道无法跨分支工作。)在spawn中,内存将被共享,根本不会被复制。 (有效地,spawn的environment不会打开任何文件描述符.)
我们将更改fork
以了解某些内存区域已由“操作系统库”使用,并且应始终共享。 与其在某处硬编码区域列表,不如在页表条项设置一个otherwise-unused
的位(就像我们对fork中的PTE_COW位所做的那样)。
我们在inc / lib.h
中定义了一个新的PTE_SHARE
位。 该位是Intel和AMD手册中标记为“可用于软件使用”的三个PTE位之一。 我们将建立一个约定,如果页表项设置了该位,则应该在fork和spawn中将PTE直接从父级复制到子级。 请注意,这不同于将其标记为“copy-on-write”:
如第一段所述,我们要确保共享页面更新。
Exercise8
更改 lib/fork.c
中的 duppage
以遵循新约定。 如果页表条目设置了 PTE_SHARE
位,则直接复制映射。 (您应该使用 PTE_SYSCALL
,而不是 0xfff
,来屏蔽页表条目中的相关位。0xfff 也拾取访问的位和脏位。)
同样,在lib/spawn.c
中实现 copy_shared_pages
。 它应该遍历当前进程中的所有页表条目(就像 fork 所做的那样),将任何设置了 PTE_SHARE
位的页映射复制到子进程中。
1. 修改 lib/fork.c
加入针对于PTE_SHARE
的处理
perm = pte & PTE_SYSCALL;
if ((r = sys_page_map(srcid, (void*)addr, envid, (void*)addr, perm)) < 0) {
panic("sys_page_map: %e\n", r);
}
2. 实现copy_shared_pages
static int
copy_shared_pages(envid_t child)
{
// LAB 5: Your code here.
int r,i;
for (i = 0; i < PGNUM(USTACKTOP); i ++){
// Attention! i跟pte一一对应,而i/1024就是该pte所在的页表
if((uvpd[i/1024] & PTE_P) && (uvpt[i] & PTE_P) && (uvpt[i] & PTE_SHARE)){
if ((r = sys_page_map(0, PGADDR(i/1024, i%1024, 0), child,PGADDR(i/1024, i%1024, 0), uvpt[i] & PTE_SYSCALL)) < 0)
return r;
}
}
return 0;
}
9. The keyboard interface
要让shell工作,我们需要一种方法来键入它。QEMU一直在显示我们写入到CGA显示器和串行端口的输出,但到目前为止,我们只在内核监视器中接受输入。在QEMU中,在图形化窗口中键入的输入显示为从键盘到JOS的输入,而在控制台中键入的输入显示为串行端口上的字符。kern/console.c已经包含了自lab 1以来内核监视器一直使用的键盘和串行驱动程序,但是现在您需要将它们附加到系统的其他部分。
Exercise 9.
在你的kern/trap.c
,调用kbd_intr
处理trap`` IRQ_OFFSET+IRQ_KBD
,调用serial_intr
处理trap IRQ_OFFSET+IRQ_SERIAL。
我们在lib/console.c
中为您实现了控制台输入/输出文件类型。kbd_intr和serial_intr用最近读取的输入填充缓冲区,而控制台文件类型耗尽缓冲区(控制台文件类型默认用于stdin/stdout,除非用户重定向它们)。
其实这里非常简单就是添加两个新的trap
处理函数
//kern/trap.c/trap_dispatch()
if (tf->tf_trapno == IRQ_OFFSET + IRQ_KBD){
kbd_intr();
return;
}
else if (tf->tf_trapno == IRQ_OFFSET + IRQ_SERIAL){
serial_intr();
return;
}
然后我们来看一下这两个陷阱处理函数是怎么做的
-
kbd_intr()
kbd_proc_data()是从键盘读入a character就返回,如果没输入就返回-1*
void kbd_intr(void) { cons_intr(kbd_proc_data); }
-
cons_intr
serial_proc_data()很明显就是从串行端口读一个
void serial_intr(void){ if (serial_exists) cons_intr(serial_proc_data); }
这两个函数都调用了
cons_intr
函数 -
cons_intr函数
将从键盘读入的一行填充到cons.buf
static void cons_intr(int (*proc)(void)) { int c; while ((c = (*proc)()) != -1) { if (c == 0) continue; cons.buf[cons.wpos++] = c; if (cons.wpos == CONSBUFSIZE) cons.wpos = 0; }
10. The Shell
Run make run-icode
or make run-icode-nox
。这将运行内核并启动user/icode。icode执行init,它将把控制台设置为文件描述符0和1(标准输入和标准输出)
。然后它会spawn sh,也就是shell
。你应该能够运行以下命令:
echo hello world | cat
cat lorem |cat
cat lorem |num
cat lorem |num |num
|num |num |num lsfd
注意,用户库例程cprintf直接打印到控制台,而不使用文件描述符代码。这对于调试非常有用,但是对于piping into other programs
却不是很有用。要将输出打印到特定的文件描述符(例如,1,标准输出),请使用fprintf(1, “…”, …)。 printf("…", …)
是打印到FD 1的捷径。有关示例,请参见user/lsfd.c。
Exercise 10.
shell不支持I/O重定向。如果能运行sh <script
就更好,而不是像上面那样手工输入script
中的所有命令。将<
的I/O重定向添加到user/sh.c
case ‘<‘: // Input redirection
// Grab the filename from the argument list
if (gettoken(0, &t) != ‘w‘) {
cprintf("syntax error: < not followed by word\n");
exit();
}
// LAB 5: Your code here.
if ((fd = open(t, O_RDONLY)) < 0) {
cprintf("open %s for read: %e", t, fd);
exit();
}
if (fd != 0) {
dup(fd, 0);
close(fd);
}
break;
11. Summary
好了828暂时也告一段落了,lab6就不写了。。我认识的好多人都没写,我也就暂时不写了。(lab5写的确实有点草率,主要是中间间隔了太久。。。。赶上暑假了)后面应该是会去看一下xv6的源码。然后看完leveldb源码。后面会写一个DDIA的阅读笔记,然后就是偶尔会更新刷题的总结博客啦。