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.c中修改env_create,以便它赋予
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特权设置吗?为什么?
答:不需要,当环境切换的时候,会根据环境的
保存或恢复它的上下文,包括eflags,所以不需要额外的操作。
env_tf
注意,这个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
函数。bc_pgfault是一个页面错误处理程序,就像您在前面的lab中为copy-on-write fork编写的一样,只是它的工作是
flush_block
来响应页面错误。写这些代码时,请记住(1)addr可能没有
从磁盘加载页面
,(2)ide_read是操作
对齐到block边界
sectors
而不是blocks。
如果需要,flush_block函数应该将一个
上。如果块甚至
块写到磁盘
(也就是说,页面没有映射),或者它
不在block cache中
,flush_block就不应该执行任何操作。我们将使用
不是dirty
跟踪磁盘块自上次从磁盘读取或写入磁盘以来
VM硬件
。要
是否被修改
,我们可以查看是否在uvpt条目中设置了
查看是否需要写块到磁盘
。(PTE_D位由处理器设置,以响应对该页的写入;见386参考手册第5章5.2.4.3。)将块写入磁盘后,flush_block应该使用
PTE_D“dirty”位
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中实现。它应该在位图中找到一个空闲磁盘块,标记它该磁盘块已被使用,并返回该磁盘块号。当您分配一个块时,您应该立即使用flush_block将更改后的
alloc_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_block_walk将文件中的
file_get_block
映射到
块偏移量
,非常类似于pgdir_walk对页表所做的操作。file_get_block进一步映射到
struct File或间接块中的块的指针
实际的磁盘块
,如果需要的话分配一个新的磁盘块。
使用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的繁重工作将由fs/fs.c中已经实现的file_read来完成(反过来,它只是对
实现serve_read
的一系列调用)。serve_read只需要提供用于文件读取的
file_get_block
。查看
RPC接口
中的注释和代码,了解应该如何构造
serve_set_size
server函数
。
使用make grade测试代码。您的代码应该通过“serve_open/file_stat/file_close”和“file_read”,得分为70/150。
Exercise 6. 在fs/server .c中实现,在lib/file.c中实现
serve_write
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基本信息的
页面(在内存空间0xD0000000以上)Fd page
- 服务端的私有结构体
OpenFile
-
,设备有三种,devfile,devpipe,devcons设备结构体dev
-
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为这个的文件。
在
里,o->o_fd->fd_file.id = o->o_fileid;这是serve_open()
将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中的以遵循新的约定。如果页表条目设置了PTE_SHARE位,只需
duppage
直接复制映射
。(应该使用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,调用处理trap
kbd_intr
,调用
IRQ_OFFSET+IRQ_KBD
处理trap
serial_intr
。
IRQ_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不支持。如果能运行sh <script就更好,而不是像上面那样手工输入script中的所有命令。将
I/O重定向
添加到
<的I/O重定向
user/sh.c
通过在shell中键入sh <script测试您的实现
运行
来测试您的shell。testshell只是将上面的命令(也可以在fs/testshell.sh中找到)提供给shell,然后检查输出是否匹配fs/testshell.key。
make Run -testshell
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
的地址还未映射物理页
,然后传给server一个fd
path
-
根据path打开或者创建一个server
,然后传回open file
(存着被打开文件的基本信息)给客户端caller,Fd page
。映射在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,所以很好的避免了同时修改同一文件的情况。只是感觉这样效率好像不高。
参考
谢谢 bysui
谢谢 Gatsby123
具体代码
见我的GItHUb