IDE即Integrated Drive Electronics,它的本意是指把控制器与盘体集成在一起的硬盘驱动器,IDE是表示硬盘的传输接口。
Introduction
在这个实验室中,您将实现spawn
,一个加载并运行磁盘上可执行文件
的库调用
。然后,您将充实内核和库操作系统,使其足以在控制台上运行shell
。这些特性需要一个文件系统,本实验室介绍了一个简单的读/写文件系统
。
实验室这部分的主要新组件是位于新fs目录
中的文件系统环境
。浏览此目录中的所有文件,了解其中的新内容。此外,在user和lib目录中还有一些与文件系统相关的新源文件:
fs/fs.c Code that mainipulates the file system's on-disk structure.
fs/bc.c A simple 'block cache' built on top of our user-level page fault handling facility.
fs/ide.c Minimal PIO-based (non-interrupt-driven) 'IDE driver code'.最小的基于pio(非中断驱动)IDE驱动程序代码
fs/serv.c The file system server that interacts with client environments(客户端环境) using file system IPCs.
lib/fd.c Code that implements the general UNIX-like 'file descriptor' interface.
lib/file.c The driver for on-disk file type, implemented as a file system IPC client.
lib/console.c The driver for console input/output file type.
lib/spawn.c Code skeleton of the spawn library(衍生库) call.EXS
在合并到新的lab 5代码之后,您应该再次运行lab 4的pingpong, primes, and forktree test。您需要注释掉kern/init.c中的ENV_CREATE(fs_fs)行,因为fs/fs.c
试图执行一些I/O操作,而JOS还不允许这样做。类似地,暂时注释掉lib/exit.c中对close_all()
的调用;这个函数调用您将在稍后的实验中才实现子函数,因此,如果调用它们,将会引起panic。如果您的实验室4代码不包含任何bug,那么测试用例应该可以正常运行。当您开始Exercise 1时,不要忘记取消注释这些行。
The File System
这个实验室的目标不是让您实现整个文件系统,而是只实现某些关键部分。特别是,您将负责将块读入block cache
并将它们刷新回磁盘
;分配磁盘块
;将文件偏移量
映射到磁盘块;并在IPC接口
中实现read, write, and open。因为您不会自己实现所有的文件系统,所以熟悉所提供的代码
和各种文件系统接口
是非常重要的。
Disk Access
操作系统中的文件系统环境需要能够访问磁盘
,但是我们还没有在内核中实现任何磁盘访问功能。我们没有采用传统的“单片”操作系统策略,即在内核中添加IDE磁盘驱动程序以及允许文件系统访问它的必要的系统调用,而是将IDE磁盘驱动程序
实现为用户级文件系统环境的一部分
。我们仍然需要稍微修改内核
,以便进行设置,使文件系统环境
具有实现磁盘访问
本身所需的特权。
只要我们依赖于polling
(轮询)、基于“programmed I/O”(PIO)
的磁盘访问,并且不使用磁盘中断
,就很容易在用户空间
中实现磁盘访问。也可以在用户模式下实现中断驱动的设备驱动程序(例如,L3和L4内核是这样做的),但是难度更大,因为内核必须实现设备中断并将它们分派到正确的用户模式环境中。
x86处理器使用EFLAGS寄存器
中的IOPL位
来确定是否允许保护模式代码
执行特殊的设备I/O指令
,比如IN和OUT指令。由于我们需要访问的所有IDE磁盘寄存器
都位于x86的I/O空间
中,而不是内存映射,所以为了允许文件系统访问这些寄存器,我们只需要给文件系统环境提供“I/O privilege
”。实际上,EFLAGS寄存器中的IOPL位
为内核提供了一个简单的“all-or-nothing
”(全有或全无)方法来控制用户模式代码
是否可以访问I/O空间
。在我们的示例中,我们希望文件系统environment能够访问I/O空间,但是我们根本不希望任何其他environment能够访问I/O空间。
Exercise 1. i386_init通过将类型
ENV_TYPE_FS
传递给环境创建函数env_create
来标识文件系统环境。在env.c中修改env_create,以便它赋予文件系统environment I/O特权
,但永远不要将该特权授予任何其他环境。
确保您可以启动文件环境,而不会导致General Protection fault。make grade时你应该通过“fs i/o” test。
这个地方代码还是比较简单的,毕竟之前为用户环境开中断也是设置的eflags的FL_IF位,这里就是设置eflags的IOPL位
//env.c/env_create()
e->env_type = type;
if(type == ENV_TYPE_FS){
e->env_tf.tf_eflags |= FL_IOPL_MASK;
}
原来make grade是根据conf/lab.mk
来判断对哪个lab进行make grade。我这次没有把lab.mk中lab 4相关代码删掉,结果它还是在对lab 4代码make grade
Question
1.当您随后从一个环境切换到另一个环境时,还需要做什么来确保正确地保存和恢复
这个I/O特权设置吗?为什么?
答:不需要,当环境切换的时候,会根据环境的env_tf
保存或恢复它的上下文,包括eflags,所以不需要额外的操作。
注意,这个lab中的GNUmakefile文件将QEMU设置为使用obj/kern/kernel.img
文件作为disk 0的映像(通常是DOS/Windows下的“Drive C”)???并使用(新的)文件obj/fs/fs.img
作为disk 1(“Drive D”)的映像。在这个lab中,我们的文件系统只接触disk 1
;disk 0只用于引导内核。如果你破坏了其中任何一个磁盘镜像,你可以重置他们为最初的,“原始”的版本,只需键入:
$ rm obj/kern/kernel.img obj/fs/fs.img
$ make
or by doing:
$ make clean
$ make
The Block Cache
在我们的文件系统中,我们将在处理器的虚拟内存系统的帮助下实现一个简单的“buffer cache
”(缓冲区缓存,真的就一个块缓存)。块缓存的代码在fs/bc.c
中。
我们的文件系统将仅限于处理3GB
或更小的磁盘。我们保留文件系统environment
的地址空间的一个大的、固定的3GB区域,从0x10000000 (DISKMAP)
到0xD0000000 (DISKMAP+DISKMAX)
,作为磁盘的“内存映射”版本。例如,disk block 0映射到虚拟地址0x10000000,disk block 1映射到虚拟地址0x10001000(一个块4KB),以此类推。fs/bc.c中的diskaddr函数
实现了从 disk block numbers到虚拟地址的转换
(以及一些完整性(sanity)检查
)。
因为我们的文件系统environment有它自己的虚拟地址空间,独立于所有其他环境系统的虚拟地址空间,所以文件系统environment唯一需要做的事是实现文件访问
,因此用这种方法保留文件系统environment的地址空间的大部分是合理的。对于32位机器上的real文件系统实现来说,这样做会很尴尬,因为现代磁盘大于3GB。在具有64位地址空间的计算机上,这种缓冲区缓存管理方法
可能仍然是合理的。
当然,将整个磁盘读取到内存中要花很长时间,所以我们以请求分页(demand paging
)的形式实现,其中我们只在磁盘映射区域分配页
和从磁盘读取相应的块
来响应一个在这个地区发生的页面错误
。
Exercise 2. 在fs/bc.c中实现
bc_pgfault
和flush_block
函数。bc_pgfault是一个页面错误处理程序,就像您在前面的lab中为copy-on-write fork编写的一样,只是它的工作是从磁盘加载页面
来响应页面错误。写这些代码时,请记住(1)addr可能没有对齐到block边界
,(2)ide_read是操作sectors
而不是blocks。
如果需要,flush_block函数应该将一个块写到磁盘
上。如果块甚至不在block cache中
(也就是说,页面没有映射),或者它不是dirty
,flush_block就不应该执行任何操作。我们将使用VM硬件
跟踪磁盘块自上次从磁盘读取或写入磁盘以来是否被修改
。要查看是否需要写块到磁盘
,我们可以查看是否在uvpt条目中设置了PTE_D“dirty”位
。(PTE_D位由处理器设置,以响应对该页的写入;见386参考手册第5章5.2.4.3。)将块写入磁盘后,flush_block应该使用sys_page_map清除PTE_D位
。
使用make grade测试代码。您的代码应该通过“check_bc”、“check_super”和“check_bitmap”。
fs/fs.c中的fs_init
函数是如何使用block cache
的一个主要示例。在初始化块缓存之后,它简单将指向块缓存的指针存储到super全局变量
中的磁盘映射区域。在这之后,我们可以简单地从super structure中读取,就像它们在内存中一样,并且我们的页面错误处理程序将根据需要从磁盘中读取它们。(这里的宾语都是指的block cache
???)
这里最让我困惑的就是block与sector是怎么对应的,结果就只是blockno8=sectorno,因为blocksize=sectorsize8,之前说数据块是分散在磁盘上的,怎么就这么对上了?还有一点不懂为什么系统调用中envid参数直接设0就行?
bc_pgfault():
// Fault any disk block that is read in to memory by
// loading it from disk.只说从disk又不说disk哪个扇区
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 = (void *)ROUNDDOWN(addr, BLKSIZE);
if((r=sys_page_alloc(0, addr, PTE_P | PTE_U | PTE_W))<0) //为什么这里可以用0?
panic("in bc_pgfault,out of memory: %e", r);
if((r=ide_read(blockno*8, 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;
int r;
if (addr < (void*)DISKMAP || addr >= (void*)(DISKMAP + DISKSIZE))
panic("flush_block of bad va %08x", addr);
// LAB 5: Your code here.
addr = (void *)ROUNDDOWN(addr, BLKSIZE);
if(va_is_mapped(addr) && va_is_dirty(addr)){
if((r=ide_write(blockno*8, 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);
}
return;
}
The Block Bitmap(位图)
在fs_init
设置bitmap指针之后,我们可以将bitmap视为一个打包的位数组,每个位对应磁盘上的每个块
。例如,请参见block_is_free,它只是检查给定块在位图中是否标记为free。
Exercise 3. 使用free_block作为模型在fs/fs中实现
alloc_block
。它应该在位图中找到一个空闲磁盘块,标记它该磁盘块已被使用,并返回该磁盘块号。当您分配一个块时,您应该立即使用flush_block将更改后的位图块刷新到磁盘
,以保持文件系统的一致性。
make grade时应该可以通过“alloc_block”测试
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.
// super->s_nblocks=3G/4K=BLKBITSIZE*24, 所以bitmap块最多可以有24块?
// LAB 5: Your code here.
uint32_t blockno;
// 找到一个空闲块
for(blockno=2; blockno<super->s_nblocks && !block_is_free(blockno); blockno++);
if(blockno >= super->s_nblocks) //检查是否磁盘满了
return -E_NO_DISK;
bitmap[blockno/32] ^= 1<<(blockno%32); //将该空闲块设为used,0=used 位运算符^(“异或”)
flush_block(&bitmap[blockno/32]); //将bitmap对应块刷新到磁盘,此时块可能是disk 1~24
return blockno;
panic("alloc_block not implemented");
}
File Operations
我们在fs/fs.c中提供了各种函数来实现基本的功能,您将需要这些功能来解释和管理File structure
、扫描和管理目录文件的条目
,以及从root目录遍历文件系统以解析绝对路径名
(absolute pathname)。阅读fs/fs.c中的所有代码,确保在继续之前理解每个函数的功能。
Exercise 4. 实现
file_block_walk
和file_get_block
。file_block_walk将文件中的块偏移量
映射到struct File或间接块中的块的指针
,非常类似于pgdir_walk对页表所做的操作。file_get_block进一步映射到实际的磁盘块
,如果需要的话分配一个新的磁盘块。
使用make grade测试代码。您的代码应该通过“file_open”、“file_get_block”、“file_flush/file_truncated/file rewrite”和“testfile”测试。
就是搞不清出f->indirect里存的是块号还是地址?然后ppdiskbno指向什么?
free_block(f->f_indirect);并且free_block(uint32_t blockno)
,所以f->f_indirect存的是块号
Find the disk block number(块号!!!) slot(槽!)
我们是要找到存放块号的那个槽
Set '*ppdiskbno' to point to that slot
说明了*ppdiskbno存的是那个槽的地址
。
还有一点困惑的是,提示里说 but note that *ppdiskbno might equal 0 什么意思,有什么影响???
答:应该是说明可能该槽里还没有块号,对应的块还未被分配
。实际使用我认为是在file_get_block
里。很多同学在file_block_walk里写if(ppdiskbno) *ppdiskbno=…我认为是不对的。
file_block_walk():
找到存着文件f的第filebno块的块号
的槽,将槽地址
赋值给*ppdiskbno。注意:f->f_direct与f->f_indirect里存的都是块号
static int
file_block_walk(struct File *f, uint32_t filebno, uint32_t **ppdiskbno, bool alloc)
{
// LAB 5: Your code here.
int r;
if(filebno >= NDIRECT + NINDIRECT)
return -E_INVAL;
if(filebno < NDIRECT){
*ppdiskbno=&f->f_direct[filebno]; //把f->f_direct第filebno个槽的地址给它
return 0; //这里忘记return了,难怪一直错
}
//cprintf("I'm in the file block walk\n");
if(f->f_indirect == 0){
if(alloc==0)
return -E_NOT_FOUND;
if((r=alloc_block())<0)
return -E_NO_DISK;
f->f_indirect=r;
memset(diskaddr(r), 0, BLKSIZE);
flush_block(diskaddr(r)); //每次对磁盘映射区域的块修改后都应该刷新回磁盘
}
//Find the disk block number(块号!!!) slot(槽!)
//捋一下,现在我们要的是存着f第filebno块块号的那个槽的地址
//即f->f_indirect与f->f_direct都是存着块号,而*ppdiskbno要的是存着块号的那个槽的地址
// *ppdiskbno=(uint32_t *)(diskaddr(f->f_indirect)+filebno*4);
//那为什么不要乘4呢,因为本来uint32_t就是4个字节,如果是char类型才要乘4
//but note that *ppdiskbno might equal 0 什么意思,有什么影响???
//答:应该是说明该槽里还没有块号,对应的块还未被分配,在file_get_block里要用
//*ppdiskbno=(uint32_t *)(diskaddr(f->f_indirect)+filebno-NDIRECT);这样写就错了
*ppdiskbno=(uint32_t *)diskaddr(f->f_indirect)+filebno-NDIRECT;
return 0;
panic("file_block_walk not implemented");
}
file_get_block()
根据槽地址进一步将槽内块号对应的块的地址
(即文件f的第filebno块的块地址
)给*blk
int
file_get_block(struct File *f, uint32_t filebno, char **blk)
{
// LAB 5: Your code here.
int r;
uint32_t *ppdiskbno;
/* 这里不需要判读filebno范围是因为file_block_walk里面会判断
if(filebno<0 || filebno >= NDIRECT + NINDIRECT)
return -E_INVAL;*/
if((r=file_block_walk(f, filebno, &ppdiskbno, 1)<0))
return r;
//ppdiskbno是f的第filebno块的块号所在的槽的地址
//blk要的是这个块映射到内存里的地址
//就算是直接块也是有可能还未分配
// if(filebno < NDIRECT || *ppdiskbno)
// *blk=(char *)(diskaddr(*ppdiskbno));
if(*ppdiskbno==0){
int r;
if((r=alloc_block())<0)
return -E_NO_DISK;
*ppdiskbno=r;
memset(diskaddr(r), 0, BLKSIZE); //我写mommove也是失了智
flush_block(diskaddr(r)); //每次对磁盘映射区域的块修改后都应该刷新回磁盘
}
*blk=diskaddr(*ppdiskbno);
return 0;
panic("file_get_block not implemented");
}
The file system interface
既然我们已经在文件系统environment本身中拥有了必要的功能,那么我们必须让希望使用文件系统的其他environment
也可以访问它。由于其他environment不能直接调用文件系统environment中的函数,所以我们将通过构建在JOS IPC机制
之上的remote procedure call
(远程过程调用)或者RPC、抽象
来公开对文件系统环境的访问。从图形上看,下面是其他environment对 the file system server (比如read)的调用:
Regular env FS env
+---------------+ +---------------+
| read | | file_read |
| (lib/fd.c) | | (fs/fs.c) |
...|.......|.......|...|.......^.......|...............
| v | | | | RPC mechanism
| devfile_read | | serve_read |
| (lib/file.c) | | (fs/serv.c) |
| | | | ^ |
| v | | | |
| fsipc | | serve |
| (lib/file.c) | | (fs/serv.c) |
| | | | ^ |
| v | | | |
| ipc_send | | ipc_recv |
| | | | ^ |
+-------|-------+ +-------|-------+
| |
+--------->---------+
虚线以下的所有内容都是从常规environment向文件系统environment发送一个读请求
的机制。从一开始,read
(我们lib/fd.c中提供的)工作在任何文件描述符上,并简单地分派到适当的device read function
,在本例中是devfile_read(我们可以有更多的设备类型,比如pipes)。devfile_read实现了专门针对磁盘文件的读取。这个函数和lib/file.c中的其他devfile_*函数实现了FS操作的客户端
,它们的工作方式大致相同,在request structure(保存在页面fsipcbuf中)
中绑定参数,调用fsipc发送IPC请求
,然后解包并返回结果。fsipc函数只处理向服务器发送请求和接收响应
的常见细节。
The file system server代码可以在fs/server .c
中找到。它在serve函数
中循环,无休止地通过IPC接收请求
,将该请求发送给适当的处理函数
,并通过IPC将结果发回
。在read示例中,service将分派给serve_read, serve_read将处理与读请求相关的IPC细节,比如解包request structure,最后调用file_read实际执行文件读取。
回想一下,JOS的IPC机制允许环境发送一个32位字
,并且可以选择共享一个页面
。为了将请求从客户机发送到服务器,我们使用32位字作为请求类型
(文件系统服务器RPCs编号,就像syscalls的编号一样),并在通过IPC共享的页面上的union Fsipc
中存储请求的参数。在客户端,我们总是在fsipcbuf
共享页面;在服务器端,我们将传入的请求页面映射到fsreq (0x0ffff000)
。
服务器也是通过IPC发回响应结果。我们使用32位字
作为函数的返回代码。对于大多数RPCs,这就是它们返回的所有内容。FSREQ_READ
和FSREQ_STAT
也返回数据,它们只是将数据写入客户机发送请求的页面。无需在响应IPC中发送此页面,因为客户机与文件系统服务器一开始就共享此页面
。同样,FSREQ_OPEN在响应中与客户端共享一个新的“Fd page”。我们将很快回到文件描述符页面???
Exercise 5. 在fs/ servlet .c中
实现serve_read
serve_read的繁重工作将由fs/fs.c中已经实现的file_read来完成(反过来,它只是对file_get_block
的一系列调用)。serve_read只需要提供用于文件读取的RPC接口
。查看serve_set_size
中的注释和代码,了解应该如何构造server函数
。
使用make grade测试代码。您的代码应该通过“serve_open/file_stat/file_close”和“file_read”,得分为70/150。
Exercise 6. 在fs/server .c中实现
serve_write
,在lib/file.c中实现devfile_write
。
Use make grade to test your code. Your code should pass “file_write”, “file_read after file_write”, “open”, and “large file” for a score of 90/150.
做这个得弄清楚这些概念:
regular进程
访问文件的整个流程。- 在IPC通信过程中,
fsipcbuf
(客户端)与fsreq
(服务端)共享页面。 - 保存着open file基本信息的
Fd page
页面(在内存空间0xD0000000以上) - 服务端的私有结构体
OpenFile
设备结构体dev
,设备有三种,devfile,devpipe,devcons- OpenFile->o_fileid跟OpenFile->o_fd->fd_file.id以及Fsipc->read->req_fileid的关系!
在devfile_read()
里,fsipcbuf.read.req_fileid = fd->fd_file.id;
这是客户端
根据在0xD0000000以上的第fdnum个fd page的fd->fd_file.id告诉服务器端要读的是
id为这个的文件。
在serve_open()
里,o->o_fd->fd_file.id = o->o_fileid;这是服务器端
将open file与它的Fd page对应起来。但是不知道为什么,我把两者都输出却总是不一样
首先来看一下整个read的流程
//inc/fd.h
struct Fd {
int fd_dev_id;
off_t fd_offset;
int fd_omode;
union {
// File server files
// 这应该就是目标文件id,在客户端赋值给了fsipcbuf.read.req_fileid
struct FdFile fd_file; //struct FdFile {int id; };
};
};
//fs/serv.c
struct OpenFile { //This memory is kept private to the file server.
uint32_t o_fileid; // file id。 The client uses file IDs to communicate with the server.
struct File *o_file; // mapped descriptor for open file应该是打开的那个文件的file pointer
int o_mode; // open mode
struct Fd *o_fd; // Fd page是一个专门记录着这个open file的基本信息的页面
};
//inc/fs.h
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.
// 扩展到256字节;必须做算术,以防我们在64位机器上编译fsformat。
uint8_t f_pad[256 - MAXNAMELEN - 8 - 4*NDIRECT - 4];
} __attribute__((packed)); // required only on some 64-bit machines
lib/fd.c/read()
根据fdnum在内存空间0xD0000000以上
找到一个struct Fd页面
命名为fd,页面内保存着一个open file的基本信息。然后根据fd内的fd_dev_id
找到对应设备dev,很明显这里是devfile
,然后调用(*devfile->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);
}
lib/file.c/devfile_read()
通过IPC共享的页面上的union Fsipc
中存储请求的参数。在客户端,我们总是在fsipcbuf
共享页面。设置好fsipcbuf的参数,调用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;//这个id就是指的当前位置?current position?
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()
这个函数就是负责跟文件系统server进程间通信
的。发送请求并接受结果。这里有点困惑的是ipc_find_env(ENV_TYPE_FS);随便找个fs类型的env都行?因为只是需要借助它去访问磁盘文件而已?
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/serve()
ipc_recv的返回值是32位字env_ipc_value
,即fsipc里ipc_send过来的type
,根据这个type判断进入哪个处理函数,这里很明显type==FSREQ_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 < 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);
}
}
fs/serv.c/serve_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;
struct OpenFile *o;
int r;
if (debug)
cprintf("serve_read %08x %08x %08x\n", envid, req->req_fileid, req->req_n);
// Lab 5: Your code here:
// First, use openfile_lookup to find the relevant open file.
// On failure, return the error code to the client with ipc_send.
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; //then update the seek position这个才是位置!
//req->req_fileid = o->o_fd->fd_file.id;
//cprintf("o->o_file:%lld req->req_fileid:%lld o->o_fd->fd_file:%d\n",o->o_fileid, req->req_fileid,o->o_fd->fd_file.id);
return r;
}
fs/fs.c/file_read()
将文件f从offset开始的count个字节读入buf中。但是count可能大于f->f_size-offset,那么最多也只能把文件剩余部分读出。
ssize_t
file_read(struct File *f, void *buf, size_t count, off_t offset)
{
int r, bn;
off_t pos;
char *blk;
if (offset >= f->f_size)
return 0;
count = MIN(count, f->f_size - offset);
for (pos = offset; pos < offset + count; ) {
//将f的第filebno块的虚拟地址存到blk中
if ((r = file_get_block(f, pos / BLKSIZE, &blk)) < 0)
return r;
bn = MIN(BLKSIZE - pos % BLKSIZE, offset + count - pos);
memmove(buf, blk + pos % BLKSIZE, bn);
pos += bn;
buf += bn;
}
return count;
}
练习6
serve_write()
同样先找到req->req_fileid对应的OpenFIle,然后将req->req_buf中req->req_n个字节的内容写到OpenFile的fd_offset处。
但是这里有个值得注意的地方,req_n可能大于req_buf的容量,所以req_n最多只能等于req_buf的大小,即一个PGSIZE。但是实际写入的内容可以少于请求写入的内容,这是允许的。
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.
int r;
struct OpenFile *o;
if ((r = openfile_lookup(envid, req->req_fileid, &o)) < 0)
return r;
// 多于的就扔掉,确实不太合理,感觉应该循环写入的
int 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;
panic("serve_write not implemented");
}
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); //error or success都在r中
assert(r <= n);
assert(r <= PGSIZE);
return r;
panic("devfile_write not implemented");
}
Spawning Processes(衍生程序,派生程序)
我们已经给出了spawn
的代码(参见lib/spawn.c),它创建一个新环境
,从文件系统加载一个程序映像
到其中,然后启动运行
这个程序的子环境。然后父进程继续独立于子进程运行。spawn函数的作用类似于UNIX中的fork,然后在子进程中立即执行exec
。
我们实现了spawn而不是unix风格的exec,因为spawn更容易从用户空间以“exokernel fashion”(一种方式)实现,而不需要内核的特殊帮助。考虑一下要在用户空间中实现exec需要做些什么,并确保您理解为什么这么做更难些。
Exercise 7. pawn依赖于新的系统调用
sys_env_set_trapframe
来初始化新创建环境的状态。在kern/syscall.c中实现sys_env_set_trapframe(不要忘记在syscall()中添加新的系统调用的分派)。
通过从kern/init.c运行user/spawnhello程序来测试代码,它将尝试从文件系统派生/hello。
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;
int r =envid2env(envid, &e, 1);
if(r != 0)
return r;//-E_BAD_ENV在这
user_mem_assert(e, (const void *) tf, sizeof(struct Trapframe), PTE_U);
/*下面这样是错的,提示明确说了tf也要被修改!
e->env_tf = *tf;
e->env_tf.tf_eflags |= FL_IF;
e->env_tf.tf_eflags |= FL_IOPL_0;
e->env_tf.tf_cs |= 3;*/
tf->tf_eflags |= FL_IF;
//e->env_tf.tf_eflags |= FL_IOPL_0;
tf->tf_eflags &= ~FL_IOPL_MASK; //普通进程不能有IO权限
tf->tf_cs |= 3;
e->env_tf = *tf;
return 0;
}
Sharing library state across fork and spawn
UNIX文件描述符是一个通用的概念,它还包括pipes, console I/O等。在JOS中,每种设备类型都有一个对应的struct Dev
,带有指向为该类型实现的read/write等函数的指针
。lib/ fd.c
在此基础上实现了通用的类unix文件描述符接口
。每个struct Fd都指示其设备类型,lib/fd.c中的大多数函数只是简单地将操作分派给适当struct Dev中的函数
。
lib/fd.c还在每个应用程序环境的地址空间中维护从FDTABLE(0xD0000000)
开始的 file descriptor table region
。在这个区域每个struct Fd都保留着一个页。在任何给定时间,只有在使用相应的文件描述符时才映射特定的文件描述符表页。每个文件描述符在从FILEDATA
开始的区域中都有一个可选的“data page”
,设备可以使用这些“data page”。
我们希望跨fork和spawn共享文件描述符状态
,但是文件描述符状态保存在用户空间内存
中。而且在fork时,内存将被标记为copy-on-write,因此状态将被复制而不是共享
。(这意味着环境无法在自己没有打开的文件中进行查找,而且管道也不能跨fork工作)。在spawn时,内存将被留在后面,根本不复制。(实际上,派生的环境一开始没有打开的文件描述符)
我们将更改fork,以确定“library operating system”使用的内存区域应该总是共享的
。我们将在页表条目中设置一个未使用的位,而不是在某个地方hard-code(硬编码)一个区域列表(就像我们在fork中使用PTE_COW位一样)。
我们在inc/lib.h中定义了一个新的PTE_SHARE位
。这个位是三个PTE位之一,在 Intel and AMD manuals中被标记为“available for software use”。我们将建立这样一个约定:如果页表条目设置了这个位,那么PTE(这里PTE指什么???页表中的映射?)应该在fork和spawn时从父环境直接复制
到子环境。注意,这与标记为copy-on-write不同:如第一段所述,我们希望确保共享页面的更新
。
Exercise 8. 更改lib/fork.c中的
duppage
以遵循新的约定。如果页表条目设置了PTE_SHARE位,只需直接复制映射
。(应该使用PTE_SYSCALL,而不是0xfff来屏蔽页表条目中的相关位。0xfff也获取accessed的和dirty位。)
同样,在lib/spawn.c中实现copy_shared_pages
。它应该循环遍历
当前进程中的所有页表条目(就像fork所做的那样),将设置了PTE_SHARE位的任何页映射复制到子进程
中。
Use make run-testpteshare
to check that your code is behaving properly
. You should see lines that say “fork handles PTE_SHARE right” and “spawn handles PTE_SHARE right”.
Use make run-testfdsharing
to check that file descriptors are shared properly
. You should see lines that say “read in child succeeded” and “read in parent succeeded”.
duppage()
if(uvpt[pn] & (PTE_SHARE)){
r = sys_page_map(fu_id, (void *)addr, envid, (void *)addr, uvpt[pn] & PTE_SYSCALL);
if(r!=0)
return r;
}
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 ++){
// uvpd、uvpt应该是个全局数组变量,但是数组元素对应的pde、pte具体是什么应该取决于lcr3设置的是哪个环境的内存空间
if((uvpd[i/1024] & PTE_P) && (uvpt[i] & PTE_P) && (uvpt[i] & PTE_SHARE)){ //i跟pte一一对应,而i/1024就是该pte所在的页表
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;
}
The keyboard interface
要让shell工作,我们需要一种方法来键入它。QEMU一直在显示我们写入到CGA显示器和串行端口的输出,但到目前为止,我们只在内核监视器中接受输入。在QEMU中,在图形化窗口
中键入的输入显示为从键盘到JOS
的输入,而在控制台中键入的输入
显示为串行端口上的字符
。kern/console.c已经包含了自lab 1以来内核监视器一直使用的键盘和串行驱动程序
,但是现在您需要将它们附加到系统的其他部分
。
Exercise 9. 在你的kern/trap.c,调用
kbd_intr
处理trapIRQ_OFFSET+IRQ_KBD
,调用serial_intr
处理trapIRQ_OFFSET+IRQ_SERIAL
。
我们在lib/console.c中为您实现了控制台输入/输出文件类型。kbd_intr和serial_intr
用最近读取的输入填充缓冲区
,而控制台文件类型耗尽缓冲区
(控制台文件类型默认用于stdin/stdout,除非用户重定向它们)。
通过运行make run-testkbd并键入几行代码来测试。系统应该在您写完行之后将您的输入返回给您。如果有可用的控制台和图形窗口,请同时在这两个窗口中输入。
//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_proc_data()是从键盘读入a character就返回,如果没输入就返回-1
void kbd_intr(void){cons_intr(kbd_proc_data);}
//serial_proc_data()很明显就是从串行端口读一个data
void serial_intr(void){
if (serial_exists)
cons_intr(serial_proc_data);
}
static void cons_intr(int (*proc)(void)) //将从键盘读入的一行填充到cons.buf
{
int c;
while ((c = (*proc)()) != -1) {
if (c == 0)
continue;
cons.buf[cons.wpos++] = c;
if (cons.wpos == CONSBUFSIZE)
cons.wpos = 0;
}
}
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
通过在shell中键入sh <script测试您的实现
运行make Run -testshell
来测试您的shell。testshell只是将上面的命令(也可以在fs/testshell.sh中找到)提供给shell,然后检查输出是否匹配fs/testshell.key。
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); //应该是让文件描述符0也作为fd对应的那个open file的struct Fd页面
close(fd);
}
//panic("< redirection not implemented");
break;
遇到的bug
这几个bug卡了我好几天,真是值得在这里写一写。
Protection I/O space 测试一直过不了
后面发现是我sys_env_set_trapframe()里写错了,之前是下面这样写的(上面已更正),这样并未对tf进行修改,不过我还是觉得其实不修改tf也没事,反正后面child_tf也没再使用了,难道是我指针没搞明白,不修改tf也就无法修改e->env_tf?
//kern/syscall.c/sys_env_set_trapframe()
user_mem_assert(e, (const void *) tf, sizeof(struct Trapframe), PTE_U);
e->env_tf = *tf;
e->env_tf.tf_eflags |= FL_IF;
//e->env_tf.tf_eflags |= FL_IOPL_0;
tf->tf_eflags &= ~FL_IOPL_MASK; //普通进程不能有IO权限
e->env_tf.tf_cs |= 3;
big file测试过不了
之前这样写的,后面把blockno从2开始就ok了
//fs/bc.c/alloc_block()
for(blockno=1; blockno<super->s_nblocks && !block_is_free(blockno); blockno++);
testshell 测试过不了
最坑了就是这个了,卡了我好几天,原来是Lab 4里的sys_ipc_try_send()逻辑写错了,难怪我怎么该Lab 5的代码都没用。。。我的if进入条件这样设置的话,那么有可能进入if时perm=0
,这样就会映射一个perm为0的页面到e内存空间,肯定不行啦。
kern/syscall.c/sys_ipc_try_send()
e->env_ipc_perm=0;
if((uintptr_t)srcva <UTOP){
```
r=page_insert(e->env_pgdir, srcpp, e->env_ipc_dstva, perm);
}
自问自答
1.一个磁盘有多少扇区?
答:文件系统环境有个3GB的磁盘映射区域,而Lab 5中只去了一个磁盘disk 1或者disk 0。所以一个磁盘应该是由3GB/4KB=3* 2^18块
磁盘块,一个磁盘块是8个扇区,所以一个磁盘应该有3* 2 ^21
个扇区
2.disk跟disk map region是一一对应的吗?
答:在文件系统环境中我认为是的,在普通环境中肯定不是。
3.为什么要用blk要用char **类型?
答:blk是用来指向对应文件块的虚拟地址的。然后f=(struct File*)blk
,struct File中很多元素是uint8_t、char
的,所以blk设成char类型是合理的
4.什么叫dev?
在本例中是devfile_read(我们可以有更多的设备类型,比如pipes)
答:所以我认为设备其实指的应该是一种工具或者方式
。在JOS中有devfile、devcons(console)、devpipe
5.seek position(请求位置)是什么?什么叫open file?
Read count bytes from f into buf, starting from seek position offset.
ssize_t file_read(struct File *f, void *buf, size_t count, off_t offset)
答: 所以我认为position指的是请求从读/写的目标文件
开始读/写的位置,是一个偏移量,保存在fd->fd_offset
。
open file流程:
客户端
首先在0xD0000000
以上找到一个还未映射物理页
的地址fd
,然后传给server一个path
server
根据path打开或者创建一个open file
,然后传回Fd page
(存着被打开文件的基本信息)给客户端caller,映射在fd上
。
6.如果有多个进程同时开着一个open file,那fd->fd_offset到底记录哪个进程的读取偏移呢?
跟下面的问题11一样,由IPC机制决定
了FS环境一次只能响应一个普通环境的请求
7.Sharing library state across fork and spawn这部分是在干嘛?
答:使父子环境共享文件描述符页
以及每个文件描述符对应的数据页
8.为什么好多函数的envid_t参数总是设成0?
在envid2env()函数中有这样如下定义。所以设成0就e就默认是curenv
// If envid is zero, return the current environment.
if (envid == 0) {
*env_store = curenv;
return 0;
}
9.fsipcbuf是什么时候分配了物理页?
答:应该早就为每个普通环境的内存空间的fsipcbuf分配了物理页,应该不是所有环境的fsipcbuf共享同一物理页,不然同时请求文件读写会乱套。至于具体在哪里分配的,我没找到。。。
10.file_open是去磁盘打开,还是在磁盘在内存中的映射区域
中寻找?
答:做到后面就很清晰了,是在映射区域中寻找
,如果发生页面错误再通过bc_pgfault()
函数从磁盘中读出来,如果对打开的文件进行了修改
,一定要及时刷新回磁盘
,保持一致性
11.如果有多个普通环境想修改一个磁盘File,怎么保证其内容的一致性?
答:普通环境
想修改File,需要通过IPC机制
借助文件系统环境
实现,而IPC机制让文件系统环境一次只能接受一个send,所以很好的避免了同时修改同一文件的情况。只是感觉这样效率好像不高。