MIT S6.828 Lab5(实验记录三)

154 阅读4分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第27天,点击查看活动详情 

接着上篇文章,这篇文章主要对 块缓存进行了一系列的总结与实验 注:内容相对枯燥,就是我个人学习过程中的技术笔记与总结思考

块缓存

在我们的文件系统中,我们将在处理器的虚拟内存系统的帮助下实现一个简单的“缓冲区缓存”(实际上只是一个块缓存)。块缓存的代码位于文件fs/bc.c。

我们的文件系统将仅限于处理大小为 3GB 或更小的磁盘。我们保留了文件系统环境地址空间的一个固定的 3GB 大区域,从 0x10000000 (DISKMAP) 到 0xD0000000 (DISKMAP+DISKMAX),作为磁盘的“内存映射”视图。例如,磁盘块 0 在虚拟地址0x10000000映射,磁盘块 1 在虚拟地址0x10001000映射,依此类推。fs/bc.c 中的diskaddr 函数实现了从磁盘块编号到虚拟地址的这种转换(以及一些健全性检查)。

由于我们的文件系统环境有自己的虚拟地址空间,独立于系统中所有其他环境的虚拟地址空间,而文件系统环境唯一需要做的就是实现文件访问,所以以这种方式保留大部分文件系统环境的地址空间是合理的。对于32位计算机上的实际文件系统实现来说,这样做会很尴尬,因为现代磁盘大于3GB。但是在具有 64 位地址空间的计算机上,这种缓冲区缓存管理方法可能仍然合理。

当然,将整个磁盘读入内存需要很长时间,因此我们将实现一种需求分页形式,其中我们只在磁盘映射区域中分配页面,并从磁盘读取相应的块以响应该区域中的页面错误。这样,我们可以假装整个磁盘都在内存中。

Exercise 2. Implement the bc_pgfault() and flush_block() functions in fs/bc.c. bc_pgfault() is a page fault handler, just like the one your wrote in the previous lab for copy-on-write fork, except that its job is to load pages in from the disk in response to a page fault. When writing this, keep in mind that (1) addr may not be aligned to a block boundary and (2) ide_read operates in sectors, not blocks.

The flush_block() function should write a block out to disk if necessary. flush_block shouldn’t do anything if the block isn’t even in the block cache (that is, the page isn’t mapped) or if it’s not dirty. We will use the VM hardware to keep track of whether a disk block has been modified since it was last read from or written to disk. To see whether a block needs writing, we can just look to see if the PTE_D “dirty” bit is set in the uvpt entry. (The PTE_D bit is set by the processor in response to a write to that page; see 5.2.4.3 in chapter 5 of the 386 reference manual.) After writing the block to disk, flush_block() should clear the PTE_D bit using sys_page_map().

首先给出对磁盘操作的常用端口:(ide.c中频繁访问端口以读写硬盘)

端口号R/W含义
1F0R/W传输读/写的数据
1F1R读取错误码
1F2R/W读写的扇区数量
1F3R/W读写的扇区号码
1F4R/W读写柱面的低8位
1F5R/W读写柱面的高2位(高6位恒0)
1F6R/W读写的磁盘号及磁头号
1F7Ride_read中开启第5位(此位恒为1)
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);
    
	// LAB 5: you code here:
	addr = ROUNDDOWN(addr, PGSIZE);
	if ((r = sys_page_alloc(0, addr, PTE_P | PTE_U | PTE_W)) < 0)
		panic("sys_page_alloc fault in bc_pgfault: %e", r);
	
	if ((r = ide_read(blockno * BLKSIZE / SECTSIZE, addr, BLKSIZE / SECTSIZE)) < 0)
		panic("ide_read fault in bc_pgfault: %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 fault in bc_pgfault: %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);
}

首先根据提示,异常地址不一定是页面对齐的,所以我们首先要对地址按页取整对齐。然后分配页面空间。通过ide_read去读硬盘信息,查看ide_read的实现,发现实际上是通过我们上面描述的端口将传入的扇面号和扇区数目来向指定端口写入信息之后再把1F0端口的数据读到目的地址。这里我们使用sys_page_map去清楚PTE_D脏位。这里的问题,为什么先读块然后再检查是否被分配,因为如果检查页面分配失败,那么这时我们不应该直接panic,这样会导致数据丢失。所以要先把内容放在硬盘之后再去检查页面的分配是否成功,如果成功那么数据一定是成功读取到这个页面的,如果失败也不会丢失数据。

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);

	// LAB 5: Your code here.
	int r;
	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("in flush_block, sys_page_map: %e", r);
}

根据给出的提示可以很容易写出这段代码,首先记得对地址进行页面取整。然后检查页面是否映射,地址是否是脏的。这里同样使用IDE硬盘驱动函数去写硬盘,这里的ide_write和前面的ide_read的实现区别仅是使用insl进行读或者outsl进行写。之后再清除脏位即可。

fs/fs.c 中的fs_init函数是使用块缓存的典型示例。初始化块缓存后,它只是将指针存储在super全局变量中的磁盘映射区域中。在这一点之后,我们可以简单地从super结构体中读取它们,就好像它们在内存中一样,我们的页面错误处理程序将根据需要从磁盘读取它们。