mit6.828笔记 - lab5(上)- 文件系统

文件系统结构

unix的文件系统相关知识

  • unix将可用的磁盘空间划分为两种主要类型的区域:inode区域数据区域
  • unix为每个文件分配一个inode,其中保存文件的关键元数据,如文件的stat属性和指向文件数据块的指针。
  • 数据区域中的空间会被分成大小相同的数据块(就像内存管理中的分页)。数据块中存储文件数据目录元数据
  • 目录条目包含文件名和指向inode的指针

jos的文件系统
jos对文件系统进行了简化——不使用inode。文件的所有元数据存储在目录条目中。
jos中的数据区域也会被划分为“块”


磁盘上的数据结构

磁盘不可能以字节为单位进行读写,而是以扇区为单位进行读写。jos 中扇区为512字节。
硬件层面,磁盘驱动以扇区为单位进行数据读写。
软件层面,操作系统以“块”为单位进行数据分配。
jos 中块的大小为4096字节。

超级块 superblocks

如上文所述,jos 的数据区域被划分为块。
jos 还会有一个超级块,位于磁盘的第1个块(第0个块中存放 引导加载器和分区表),由 inc/fs.h 中的 struct super 定义。内容如下:

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

其中包含磁盘中存放的块的总量和文件系统的根目录
超级块可能一个块存不下,所以会占据多个块。

文件元数据 File Meta data

文件元数据定义在 inc/fs.h 中的 struct File , 内容如下:

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

其中包含文件名、大小、类型、数据块指针,以及pad(该结构保持256字节大小)。
一个 File struct 对应的数据块分为两种:直接块、间接块。

  • 直接块有10个,可以存储40kb的文件,存储在 File struct 的数据块指针数组 f_direct 中。
  • 间接块最多可以有1024个,如果直接块不够用, File struct 会被分配一个数据块(地址存储在 f_indirect ),里面全都用于保存数据块指针。(一个数据块大小为4096字节,一个数据块指针占据4字节,故间接块可以有4096/4=1024个)
    因此 jos 的文件最多可以 1034个块, 可以存储 $1034 \times 4096kb$ 的数据。
    File的结构体的情况可以用下面的图片来理解:


目录文件和普通文件

jos 的 File struct 既可以用来表示普通文件,也可以代表目录文件,由 File struct 的 f_type 字段来区分。
文件系统管理普通文件和目录文件的方式完全相同,具体来说,文件系统不解析普通文件内的数据,但解析目录文件内的数据,因为其中是该目录下的文件的数据。
超级块中包含一个 File struct , 其中包含了文件系统根目录的元数据。


文件系统


磁盘访问

在之前的 lab 中,我们通过汇编的 inout 指令,向磁盘设备发送读写信号。但是这样果然还是好麻烦, jos 将 IDE 磁盘驱动程序作为用户级进程来实现。(传统的策略是将磁盘驱动加入至内核,然后以系统调用的形式供进程调用)。
为了能够让用户级进程在不使用磁盘中断的前提下,拥有执行特殊设备I/O指令的权限,需要使用 EFLAG 寄存器中的 IOPL 位。
因此,操作系统在创建我们的 用户级文件系统 进程时,应该对其 env struct 中的 eflag 成员置位。因此,我们需要对 lab3 编写的 env_create 进行修改。由于我们只允许 用户及文件系统进程 进行磁盘IO,所以需要将其他进程和 用户级文件系统进程 进行区分, jos 的方案是专为 用户及文件系统进程 设立一个 ENV_TYPE_FS 。具体任务见练习1.

练习1

练习 1. `i386_init` 通过向环境创建函数 `env_create` 传递 `ENV_TYPE_FS` 类型来识别文件系统环境。修改 `env.c` 中的 `env_create`,使其赋予文件系统环境 I/O 权限,但绝不赋予任何其他环境该权限。

确保在启动文件环境时不会导致一般保护故障。你应该通过 `make grade` 中的 "fs i/o "测试。
void
env_create(uint8_t *binary, enum EnvType type)
{
	// LAB 3: Your code here.
	struct Env * new_env;
	envid_t parent_id = 0;
	int r = env_alloc(&new_env, parent_id);
	if(r< 0)
		panic("env_create error: %e", r);
	load_icode(new_env, binary);
	new_env->env_type = type;//顺带一体,env_alloc 默认已将type设为 ENV_TYPE_USER
	
	// 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)
		new_env->env_tf.tf_eflags |= FL_IOPL_MASK;
}

块缓存

在之前的 lab 中,我们访问磁盘是通过分区号来访问的。但是这样果然好麻烦,要是能像内存一样用线性地址访问就好了。

块缓存的编址

为了实现上面的目标, jos 将 用户级文件系统进程的虚拟地址空间中 0x1000_0000 (DISKMAP) 到 0xD000_0000 (DISKMAP+DISKMAX) 部分用于和磁盘的存储空间进行映射。

具体来说,首先将块号和内存地址进行映射,当我们访问块缓存中的地址时,先将地址转化为块号,然后再进行读写。然后将块缓存的地址通过页表、页目录进行管理。顺带一提,DISKMAX 大小为 0xC000_0000B = 3GB , 因此 jos 仅支持 3GB 大小的磁盘存储空间。

块缓存的同步方案

为了同步内存中的磁盘数据,和磁盘中实际存储的数据,我们利用 PTE 的 PTE_D 位追踪数据是否被改动。(当内存地址addr所指页被改动时,MMU会将其对应的PTE中的 PTE_D 置位)。

将整个磁盘都读入内存很浪费时间,我们可以参考“写时复制”实现一种“读时加载”的页错误处理程序。当程序读取的块缓存地址对应的数据尚未被加载时,触发页错误,然后将对应的数据从磁盘中加载到其块缓存地址。

块缓存的是同步的实现,即是练习2的内容。

练习2

练习 2. `bc_pgfault` 是一个页面故障处理程序,就像你在上一个实验室中为 `copy-on-write fork` 编写的程序一样,只不过它的任务是从磁盘加载页面以响应页面故障。在编写这个程序时,请注意:
(1) `addr` 可能不会与块边界对齐;
(2) `ide_read` 是以扇区而不是以块为单位进行操作的。

`flush_block` 函数应该在必要时将数据块写入磁盘。
- 如果数据块甚至不在块缓存中(即页面未被映射),
- 或者数据块并不脏,
那么 flush_block 函数就不会执行任何操作。

我们将使用虚拟机硬件来跟踪磁盘块自上次从磁盘读取或写入磁盘后是否被修改过
。我们只需查看 `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"。

块缓存的实现

jos 对块缓存的实现,主要位于 fs/bc.c 中,代码不多最好都看看。

diskaddr

将块号转化为对应的块缓存地址

void*
diskaddr(uint32_t blockno)
{
	if (blockno == 0 || (super && blockno >= super->s_nblocks))
		panic("bad block number %08x in diskaddr", blockno);
	return (char*) (DISKMAP + blockno * BLKSIZE);
}

va_is_mapped

检查块缓存 va 是否已经被映射到页目录。
具体方法:检查 va 对应的 PDE 和 PTE 的 PTE_P 是否置位。

bool
va_is_mapped(void *va)
{
	return (uvpd[PDX(va)] & PTE_P) && (uvpt[PGNUM(va)] & PTE_P);
}

va_is_dirty

检查块缓存 VA 是否"脏"。脏指的是数据是否被改动。
具体方法:检查 va 对应的 PTE 的 PTE_D 是否置位。

bool
va_is_dirty(void *va)
{
	return (uvpt[PGNUM(va)] & PTE_D) != 0;
}

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;

	// 检查地址是否越界
	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);

	// 检查块号是否越界
	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处
	addr  = ROUNDDOWN(addr, PGSIZE);		//对齐需要读取的地址
	sys_page_alloc(0, addr, PTE_W|PTE_U|PTE_P);	//调用page_alloc的syscall,申请内存页
	// 将磁盘中的数据读入内存
	// 磁盘以扇区为单位读取数据,一个扇区512字节
	// 文件系统以块为单位管理数据,一个块4096字节(一页)
	if((r=ide_read(blockno * BLKSECTS, addr, BLKSECTS))<0)	//从硬盘读取数据到内存页
		panic("ide_read: %e" ,r);
	
	//由于,上面的操作对addr进行内存写操作,PTE_D被置位
	//如果坐视不管,后面会被文件系统认为,需要写回磁盘,这肯定是不合理的
	//因此通过sys_page_map的方式清空脏位(?为什么sys_page_map可以清空脏位)
	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

刷新块缓存地址 addr

void
flush_block(void *addr)
{
	//将addr转换位块号
	uint32_t blockno = ((uint32_t)addr - DISKMAP) / BLKSIZE;

	//检查addr是否越界
	if (addr < (void*)DISKMAP || addr >= (void*)(DISKMAP + DISKSIZE))
		panic("flush_block of bad va %08x", addr);

	// LAB 5: Your code here.
	// panic("flush_block not implemented");
	//addr向下对齐
	addr = ROUNDDOWN(addr, PGSIZE);

	//检查数据是否已经读入,数据页是否有脏位
	if(!va_is_mapped(addr) || !va_is_dirty(addr))
	{
		return ;
	}
	int r = 0;
	//将数据写回至磁盘
	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 bc_pgfault, sys_page_map: %e", r);
}

boid bc_init(void)

初始化块缓存

void
bc_init(void)
{
	struct Super super;
	//设置文件系统的缺页处理函数
	set_pgfault_handler(bc_pgfault);
	check_bc();

	// cache the super block by reading it once
	// 对super块的块缓存地址读取,并给super
	// 首先会触发缺页中断,将block#1加载至diskaddr(1)处
	// 然后再进行memmove,将读取到的数据填充super struct
	memmove(&super, diskaddr(1), sizeof super);
}

块位图

为什么需要块位图?

块缓存实现之后,我们拥有了从磁盘中读写、改写数据块的能力。接下来考虑一个问题,我们如何删除某个块的内容?将其数据用0覆盖吗?磁盘的磁头听到这种方案内心一定是麻的。一种简单的方案是用一个位图来表示所有块的状态,每个位代表一个块,1代表空闲,0代表占用。这样,当我们要"删除"一个块的数据时,只要在位图中将对应位置0即可。

块位图本身也是占用磁盘数据空间的, jos 的块位图存储在磁盘的第2个块以及之后,块位图一个块存不下。(手册中只有一张示意图提到这个点)

jos 的位图是一个u32int型数组,一个u32int有32个位,对应32个块。3GB磁盘空间共有 $$\frac{3\times 1024 \times 1024 \times 1024 B}{4\times 1024 B} = 768\times 1024 个$$一个块共有 $4096\times 8bit = 32 \times 1024 bit$ 因此,bitmap应该占据 $\frac{768}{32} = 24$ 个块
磁盘的存储空间示意图如下:

image.png

维护块位图

当我们想要使用 free 的块时,需要将对应位置0。当我们想释放 non-free 块时,应将对应位置1。 练习3 让我们实现 fs/fs.c 中的 free_blockalloc_block 来实现这些内容。值得注意的是,无论是 free_block 还是 alloc_block 我们一定都会影响 bitmap 占用的block,因此需要调用 flush_block 刷新 bitmap 占用的 block。

练习3

练习 3. 使用 `free_block` 作为模型来实现 `fs/fs.c` 中的 `alloc_block`,它应该在位图中找到一个空闲的磁盘块,标记它已被使用,并返回该块的编号。分配块时,应立即使用 `flush_block` 将已更改的位图块刷新到磁盘上,以保持文件系统的一致性。

使用 `make grade` 测试你的代码。现在你的代码应该能通过 "`alloc_block`"。

free_block

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);			//修改bitmap,标记为未使用
}

free_block 中最核心的就是 bitmap[blockno/32] |= 1<<(blockno%32);
blockno/32 得到 blockno 在bitmap 的哪个uint32_t,因为 bitmap 的定义如下:

//bitmap 是一个uint32_t类型的数组
uint32_t *bitmap;       // bitmap blocks mapped in memory

blockno%32 得到 blockno 位于这个uint32_t 的哪一位

alloc_block

allock_block 根据块位图寻找一个空闲块,返回这个空闲块的块号,并更新块位图

int
alloc_block(void)
{
	uint32_t bitmap_start = 2;
	//一个block大小为4096B,bitmap的一项是uint32_t,大小为4B,故一个block够装下4096/4项
	uint32_t bitmap_item_num_in_block = 4096/4;

	for (uint32_t blockno = 0; blockno < super->s_nblocks; blockno++) {//遍历整个bitmap,寻找空闲块
		if(block_is_free(blockno)){
			bitmap[blockno / 32] &= ~(1 << (blockno % 32));	//将该块置位"已使用"
			
			//blockno/32 得到该block在bitmap的下标位置
			uint32_t bitmap_item = blockno / 32;
			//blockno % 32 得到block在 bitmap_item 中的第几个比特位
			uint32_t bitmap_item_bit = blockno % 32;
			//1 << bitmap_item_bit 得到 bitmap_item_bit 的掩码
			bitmap[bitmap_item] &= ~(1 << bitmap_item_bit);	

			//bitmap_item / bitmap_item_num_in_block 得到 bitmap_item 位于bitmap后的第几个block
			//然后调用flush_block把该项所在的bitmap块写回
			flush_block(diskaddr(bitmap_start + bitmap_item / bitmap_item_num_in_block));
			return blockno;
		}
	}
	//没有找到空闲block, 则返回-E_NO_SIAK
	return -E_NO_DISK;
}

文件操作

有了块缓存和块位图机制,我们现在有了以块为单位的读写硬盘数据的能力。但是块中的数据是以文件的形式存储。jos 在 fs/fs.c 中还有一些函数,用来实现对文件的基本操作。包括:

  • 从块中解析文件结构
  • 扫描目录中的文件条目
  • 从根目录寻找指定文件
    练习4 让我们实现 file_block_walkfile_get_block,并且通读 fs/fs.c

练习4

练习 4. 实现 `file_block_walk` 和 `file_get_block`。`file_block_walk` 从文件中的块偏移映射到结构文件中该块的指针或间接块,非常类似 `pgdir_walk` 对页表所做的工作。`file_get_block` 更进一步,映射到实际的磁盘块,必要时分配一个新块。

使用 `make grade` 测试你的代码。你的代码应该通过 "`file_open`"、"`file_get_block`"、"`file_flush/file_truncated/file rewrite` "和 "`testfile`"。

file_block_walk : 从文件中的快便宜映射到结构文件中该块的指针或间接块
file_get_block : 映射到实际的磁盘块

file_block_walk

根据文件 File f 内的块号 filebno,寻找该块对应的硬盘块号 ppdiskbno
如果 File findirects 没有初始化,且 alloc 置位,则初始化 indirects
如果输入合法,这个函数保证一定完成 filebnodiskbno 的映射,但不保证 diskbno 所指的块已经在块位图中申请。
申请工作由 file_get_block 负责

static int
file_block_walk(struct File *f, uint32_t filebno, uint32_t **ppdiskbno, bool alloc)
{
	uint32_t * indirects;
	if(filebno >= NDIRECT + NINDIRECT)		//检查 filebno 是否超出 File结构体 能存储的上限
		return -E_NO_DISK;

	if(filebno < NDIRECT)
	{
		*ppdiskbno = &(f->f_direct[filebno]);	//如果 filebno 位于 直接块内,直接返回
	}
	else if(f->f_indirect)						// 如果 filebno 位于 非直接块,且该File已经申请了直接块
	{
		indirects = diskaddr(f->f_indirect);	//获取非直接块所在的地址
		*ppdiskbno = &(indirects[filebno - NDIRECT]);
	}
	else if(alloc)								// 如果 filebno 位于 非直接块,且该File未申请非直接块,且alloc置位
	{
		int bn;
		if ((bn = alloc_block()) < 0)
			return bn;		//如果没有剩余空闲块,返回E_NO_DISK
		f->f_indirect = bn;	//初始化 indirect
		flush_block(diskaddr(bn));	//刷新 indirect块 不太理解为什么要刷新这个块,这个块应该是未修改的
		indirects = diskaddr(bn);
		*ppdiskbno = &(indirects[filebno - NDIRECT]);
	}
	else										// 如果 filebno 位于 非直接块,且该File未申请非直接块,且alloc未置位
	{
		return -E_NOT_FOUND;
	}
	return 0;
}

file_get_block

负责读取文件 File f 的第 filebnoe 个块,将数据存储在 blk 所指内存地址。

int
file_get_block(struct File *f, uint32_t filebno, char **blk)
{
	uint32_t * pdiskbno;
	int r;
	//先通过 file_block_walk 获取 filebno 对应的 diskbno
	if((r = file_block_walk(f, filebno, &pdiskbno, true)) < 0)
		return r;
	
	int bn;
	//如果 filebno 对应的块尚未申请,则通过 alloc_block 进行申请
	if(*pdiskbno == 0)	
	{
		if((bn = alloc_block()) < 0)
			return bn;
		*pdiskbno = bn;
		flush_block(diskaddr(bn));
	}
	//将块中的数据读取到 blk 所指空间
	*blk = diskaddr(*pdiskbno);

	return 0;
}

fs/fs.c 中还有一些函数,用来实现对文件的基本操作,可以看看,这里就不赘述。


文件系统接口

jos 的文件系统是一个用户级进程,以 RPC 的形式,开放给客户端调用。这一部分涉及代码较多:
/lib/fd.c : 实现文件描述符机制,对“文件”的高级抽象,文件可以代表设备、终端、管道、普通文件等。
/lib/file.c : 实现了RPC的客户端 sipc 、普通文件的文件操作。
/fs/serv.c : 实现了RPC的付服务端
这些代码一定要都读一遍,接下来直接给出总结。

文件系统架构和文件客户端

首先,文件系统基于 RPC(远程过程调用)设计,如上文所说,jos的文件系统是一个用户级进程 fs。普通用户进程想要对文件进行读写操作,需要和 fs 通信,fs完成后将结果返回给用户进程。fs 充当服务端,用户进程为客户端。进程通信的方式基于lab4最后实现的IPC。

如下图所示:

      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    |
   |       |       |   |       ^       |
   +-------|-------+   +-------|-------+
           |                   |
           +-------------------+

但是呢,jos对文件进行了抽象,“文件”可以代表普通文件、终端、管道、其他设备等。对普通文件、终端、管道、其他设备等的文件操作,也就是对其进行读写。因此 /lib/fd.c 中定义了 readwritestat 等这些文件类型的通用方法。客户端只需要调用这些方法即可。服务端则进一步根据具体的文件类型,来调用对应的文件类型的读写方法。以 read 为例:
read 会根据文件的具体类型,调用不同的处理函数,其工作内容只是简单地将读取任务分发给 与文件描述符类型 对应的设备读取函数。(file 类型的设备读取函数是 devfile_readpipe 类型的设备读取函数是 devpipe_read )如下图:

image.png

总体来看:

image.png

练习5和练习6就是让我们实现文件系统服务中的读写操作:

练习5 & 练习6

练习 5. 在 `fs/serv.c` 中实现 `serve_read`。

`serve_read` 的重任将由 `fs/fs.c` 中已经实现的 `file_read` 来完成(而 `file_read` 只是对 `file_get_block` 的一系列调用)。查看 `serve_set_size` 中的注释和代码,可以大致了解服务器函数的结构。

使用 `make grade` 测试你的代码。你的代码应通过 "`serve_open/file_stat/file_close `"和 "`file_read`",分数为 70/150。
练习6。在 `fs/serv.c` 中实现 `serve_write`,在 `lib/file.c` 中实现 `devfile_write`。

使用 `make grade` 来测试代码。你的代码应该传递"`file_write`", "`file_read` after `file_write`", "`open`"和"large file",得分为90/150。

不过在开搞之前,我还是先看看认真读完 fs/serv.c 的代码

文件系统服务

文件系统的维护三种数据结构: struct Filestruct Fd struct OpenFile

struct File 的结构我们已经很熟悉了,struct File 会被映射到内存中,用于映射磁盘。

struct Fd 位于 /inc/fd.h ,对于每一个 jos 中打开了的文件,文件系统都会为它维护一个 struct Fd,从 FILEVA 0xD000_0000 开始的内存被用来存储 Fd ,定义如下:


struct FdFile {
	int id;
};

struct Fd {
	int fd_dev_id;    //设备类型
	off_t fd_offset;  //文件偏移
	int fd_omode;     //文件模式
	union {
		// File server files
		struct FdFile fd_file;
	};
};

struct OpenFilestruct Filestruct Fd 关联起来。服务器维护一个打开所有文件的数组,通过 file ID 索引起来。客户端使用 file ID 来与服务器通信。 openfile_lookup 可以将文件的 ID 传递给 struct OpenFile

OpenFile 以全局数组的形式定义于 serv.c ,如下:

// Max number of open files in the file system at once
#define MAXOPEN		1024
#define FILEVA		0xD0000000

// initialize to force into data section
struct OpenFile opentab[MAXOPEN] = {
	{ 0, 0, 1, 0 }
};

void serv_init

文件系统在 serve_init从 FILEVA 0xD000_0000 开始的内存被用来存储为 Fd ,定义如下:

void
serve_init(void)
{
	int i;
	uintptr_t va = FILEVA;
	for (i = 0; i < MAXOPEN; i++) {
		opentab[i].o_fileid = i;
		opentab[i].o_fd = (struct Fd*) va;	//没有使用 fd_alloc ,而是直接分配空间
		va += PGSIZE;						//一个fd占用一页
	}
}

int openfile_alloc

serve_init 在初始化 opentab 的时候给每一个 struct OpenFileFd 成员分配地址,但这些地址可能还没有被分配物理页。
openfile_alloc 负责从 opentab 中申请一个空闲的 struct OpenFile , 同时将对应的 Fd 初始化:

  • 如果没有映射物理页,则先申请物理页。
  • 将页面内容置零。
int
openfile_alloc(struct OpenFile **o)
{
	int i, r;

	// Find an available open-file table entry
	for (i = 0; i < MAXOPEN; i++) {
		switch (pageref(opentab[i].o_fd)) {
		case 0://如果这个页还没有映射,说明该 struct OpenFile 是空闲的,先申请物理页
			if ((r = sys_page_alloc(0, opentab[i].o_fd, PTE_P|PTE_U|PTE_W)) < 0)
				return r;
			/* fall through */
		case 1:
			opentab[i].o_fileid += MAXOPEN;
			*o = &opentab[i];
			memset(opentab[i].o_fd, 0, PGSIZE);		//整个内存页清零
			return (*o)->o_fileid;
		}
	}
	return -E_MAX_OPEN;
}

int openfile_lookup

获取 fileid 所指的 struct OpenFile

int
openfile_lookup(envid_t envid, uint32_t fileid, struct OpenFile **po)
{
	struct OpenFile *o;

	o = &opentab[fileid % MAXOPEN];
	if (pageref(o->o_fd) <= 1 || o->o_fileid != fileid)
		return -E_INVAL;
	*po = o;
	return 0;
}

int serve_read (练习 5)

根据 union Fsipc 的参数,读取指定文件的数据

int
serve_read(envid_t envid, union Fsipc *ipc)
{
	struct Fsreq_read *req = &ipc->read;
	struct Fsret_read *ret = &ipc->readRet;
	int r;
	if (debug)
		cprintf("serve_read %08x %08x %08x\n", envid, req->req_fileid, req->req_n);

	// Lab 5: Your code here:
	int fileid = req->req_fileid;
	int count = req->req_n;

	struct OpenFile * o;
	if((r = openfile_lookup(envid, fileid, &o))<0)
		return r;
	
	if((r = file_read(o->o_file, ret->ret_buf, count, o->o_fd->fd_offset))<0)
		return r;
	count = r;
	o->o_fd->fd_offset += count;
	return count;
}

serve_write (练习 6)

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.
	// panic("serve_write not implemented");
	int r;
	struct OpenFile * o;
	if((r = openfile_lookup(envid, o->o_fileid, &o)) < 0)
		return r;

	//调用 file_write 将 req 中所指 buf 的数据写入 File 的 offset 所指内存中
	if((r = file_write(o->o_file, req->req_buf, req->req_n, o->o_fd->fd_offset)) < 0)
		return r;

	o->o_fd->fd_offset += r;
	return r;
}

serve_open

// 以 req->req_omode 模式打开 req->req_path,
// 将 Fd 页面和返回调用环境的权限分别存储在 *pg_store 和 *perm_store 中。
int
serve_open(envid_t envid, struct Fsreq_open *req,
	   void **pg_store, int *perm_store)
{
	char path[MAXPATHLEN];
	struct File *f;
	int fileid;
	int r;
	struct OpenFile *o;

	if (debug)
		cprintf("serve_open %08x %s 0x%x\n", envid, req->req_path, req->req_omode);

	// Copy in the path, making sure it's null-terminated
	memmove(path, req->req_path, MAXPATHLEN);
	path[MAXPATHLEN-1] = 0;

	// Find an open file ID
	// 在文件系统中分配一个fileID
	if ((r = openfile_alloc(&o)) < 0) {
		if (debug)
			cprintf("openfile_alloc failed: %e", r);
		return r;
	}
	fileid = r;

	// Open the file
	// 如果打开模式是 O_CREAT ,则在磁盘中创建一个文件
	if (req->req_omode & O_CREAT) {
		if ((r = file_create(path, &f)) < 0) {
			if (!(req->req_omode & O_EXCL) && r == -E_FILE_EXISTS)
				goto try_open;
			if (debug)
				cprintf("file_create failed: %e", r);
			return r;
		}
	} else {
try_open:
		if ((r = file_open(path, &f)) < 0) {
			if (debug)
				cprintf("file_open failed: %e", r);
			return r;
		}
	}

	// Truncate
	// 如果打开模式是 O_TRUNC,则将文件的偏移指针归0
	if (req->req_omode & O_TRUNC) {
		if ((r = file_set_size(f, 0)) < 0) {
			if (debug)
				cprintf("file_set_size failed: %e", r);
			return r;
		}
	}
	if ((r = file_open(path, &f)) < 0) {
		if (debug)
			cprintf("file_open failed: %e", r);
		return r;
	}

	// Save the file pointer
	o->o_file = f;

	// Fill out the Fd structure
	o->o_fd->fd_file.id = o->o_fileid;
	o->o_fd->fd_omode = req->req_omode & O_ACCMODE;
	o->o_fd->fd_dev_id = devfile.dev_id;
	o->o_mode = req->req_omode;

	if (debug)
		cprintf("sending success, page %08x\n", (uintptr_t) o->o_fd);

	// Share the FD page with the caller by setting *pg_store,
	// store its permission in *perm_store
	*pg_store = o->o_fd;
	*perm_store = PTE_P|PTE_U|PTE_W|PTE_SHARE;

	return 0;
}

理解open的过程

open调用后,会从客户端的 fd_table 中取一个空闲槽 fd_table[n],然后由文件系统的 serv_open 来分配物理页,同时文件西痛的地址空间中也会映射这个 fd 页面。
image.png


posted @ 2024-01-10 09:25  toso  阅读(50)  评论(0编辑  收藏  举报