当实现完 Part1 中的函数后,磁盘就通过 fsformat 程序成功地生成了。在继续之前,建议大家看一看指导书的这个图,这个文章的顺序大概是:文件系统服务进程→交互区→用户进程

lab5_total

接下来我们正式进入内核,处理后续工作。

块缓存

  • 块缓存指的是借助虚拟内存来实现磁盘块缓存的设计。

在 MOS 系统中,文件管理“系统”,是以一个进程的形式存在的,它通过从磁盘中读写数据,并与其他用户进程交互来实现文件的管理,这些被使用到的文件就会先被缓存在文件服务进程的进程空间中。类似于批发商从生产商处取货,存储在自己的仓库里,使用时再拿出来给下级的经销商(x)

我们规定,文件管理进程使用大小为 DISKMAX 字节的空间作为磁盘块的缓存区,并且缓存区的存取单位为磁盘块。每个磁盘块都应该在内存中有单独相对应的位置进行缓存,这样就限制了我们内核支持的最大磁盘大小。

#define DISKMAP 0x10000000
#define DISKMAX 0x40000000

为了在磁盘块和内存之间进行交换,我们需要准备一系列辅助和工作函数,而这系列的共 35 个函数都被放置在 fs/fs.c 文件中。(file service?)

fs.c 中的磁盘块操作函数

我们通过简单的函数,一步一步组合执行复杂的功能,有以下函数:

diskaddr - Exercise 5.6

和上文的块缓存相对应,返回某个特定块在文件管理系统进程内存中应该被放置到的虚拟地址,实际上就按照上面提的线性映射就行了,一块一块挨着排

void *diskaddr(u_int blockno) {
/* Exercise 5.6: Your code here. */
u_int addr = DISKMAP + blockno * BY2BLK;
if (addr > DISKMAX + DISKMAP) {
debugf("illegal blockno_addr!\n");
return (void *) -1;
}
return (void *) addr;
}

va_is_mapped & block_is_mapped

va_is_mapped 检查文件管理进程中某个虚拟地址是否被使用

通常情况下不会单独使用,一般配合 diskaddrblock_is_mapped 中使用

block_is_mapped 则检查特定的磁盘块是否使用块缓存装入了内存。其实觉得应该叫 block is mapped at 比较合适,因为返回的是映射所在的地址(返回 NULL 说明映射在空气里了)

由于块缓存是一一对应的,所以只要查虚拟地址的使用,就能获得独一的磁盘块是否有了缓存

// Overview:
// Check if this disk block is mapped in cache.
// Returns the virtual address of the cache page if mapped, 0 otherwise.
void *block_is_mapped(u_int blockno) {
void *va = diskaddr(blockno); // 获取块映射的缓存虚拟地址
if (va_is_mapped(va)) { // 查一下虚拟地址用没用
return va;
}
return NULL;
}
int va_is_mapped(void *va) {
return (vpd[PDX(va)] & PTE_V) && (vpt[VPN(va)] & PTE_V);
}

map_block & unmap_block

光检查还不行,也得有形成/解除映射的函数不是

int map_block(u_int blockno) {
/* Step 1: 如果已经映射,返回 0 */
/* Exercise 5.7: Your code here. (1/5) */
if (block_is_mapped(blockno) != 0) { return 0; }
/* Step 2: 使用系统调用申请一个含 PTE_D 的页面,通过 diskaddr 查询映射的虚拟地址 */
/* Exercise 5.7: Your code here. (2/5) */
return syscall_mem_alloc(syscall_getenvid(), diskaddr(blockno), PTE_D |PTE_V);
}

void unmap_block(u_int blockno) {
/* Step 1: 获得指定块的映射地址,没映射就是 NULL */
void *va;
/* Exercise 5.7: Your code here. (3/5) */
va = block_is_mapped(blockno);
if (va == 0) { return; }
/* Step 2: 如果磁盘中块仍使用,缓存还 DIRTY 了,解除映射之前要先写回 */
/* Exercise 5.7: Your code here. (4/5) */
if (!block_is_free(blockno) && block_is_dirty(blockno)) {
write_block(blockno);
}
/* Step 3: 通过系统调用解除地址映射 */
/* Exercise 5.7: Your code here. (5/5) */
syscall_mem_unmap(syscall_getenvid(), va);

user_assert(!block_is_mapped(blockno));
}

write_block & read_block

形成映射的下一步,就是把数据写到内存之中了

要注意 write_block 是写到磁盘里,read_block 是写到内存里,也就是 write to block & read from block

void write_block(u_int blockno) {
/* Step 1: 检查内存中是否映射,没映射 = 没数据,肯定没法写回 */
if (!block_is_mapped(blockno)) {
user_panic("write unmapped block %08x", blockno);
}

/* Step2: 使用 ide_write 把当前缓存块写回 */
void *va = diskaddr(blockno);
ide_write(0, blockno * SECT2BLK, va, SECT2BLK);
}

// **blk != 0 ,则向 *blk 中存入写入后的虚拟地址
// **isnew != 0,则按照以下规则更新 *isnew:
// *isnew = 0: 虚拟地址原本就被写入过
// *isnew = 1: 虚拟地址此前没有写入

// (Isnew lets callers like file_get_block clear any memory-only
// fields from the disk blocks when they come in off disk.)
//
int read_block(u_int blockno, void **blk, u_int *isnew) {
/* Step 1: 检查 blockno 的合法性 */
if (super && blockno >= super->s_nblocks) { user_panic("reading non-existent block %08x\n", blockno); }
/* Step 2: 检查磁盘中块是否有效,如果磁盘块是 free 的,说明里面没东西 */
// Hint: 读之前要先读一下 bitmap 是不是倒进内存里了
// If the bitmap is NULL, indicate that we haven't read bitmap from disk to memory
// until now. So, before we check if a block is free using `block_is_free`, we must
// ensure that the bitmap blocks are already read from the disk to memory.
if (bitmap && block_is_free(blockno)) { user_panic("reading free block %08x\n", blockno); }
/* Step 3: 找到待写入的虚拟地址 */
void *va = diskaddr(blockno);
/* Step 4: 读入内存,更新 *isnew */
// Hint: 如果已经 mapped 则只更新 isnew,否则先 alloc 页面,再 read
if (block_is_mapped(blockno)) { // 已经 mapped
if (isnew) { *isnew = 0; }
} else { // 未 mapped
if (isnew) { *isnew = 1; }
syscall_mem_alloc(0, va, PTE_D); // 申请页面
ide_read(0, blockno * SECT2BLK, va, SECT2BLK); // 使用 ide_read 读入页面
}
/* Step 5: 赋值 *blk */
if (blk) { *blk = va; }
return 0;
}

block_is_free & free_block

这两个函数改变的是内存中存放的 bitmap 数组,检查磁盘中是否使用了这个块/操作块

// Overview:
// 通过内存中的 bitmap 检查 block 是否在磁盘中有效
int block_is_free(u_int blockno) {
// blockno 不合法,一定不有效
if (super == 0 || blockno >= super->s_nblocks) { return 0; }
// 查 bitmap,如果指定位为 1,说明有效
if (bitmap[blockno / 32] & (1 << (blockno % 32))) { return 1; }
// 其余情况无效
return 0;
}

// Overview:
// Mark a block as free in the bitmap.
void free_block(u_int blockno) {
/* Step 1: 如果 blockno 不合法,不更改 bitmap */
/* Exercise 5.4: Your code here. (1/2) */
// if blockno = 0, the boot sector will have the chance to be overwrited
if (blockno == 0 || blockno > super->s_nblocks) { return; }
/* Step 2: 通过位运算改变 bitmap 的值,使指定位置为 1. */
/* Exercise 5.4: Your code here. (2/2) */
bitmap[blockno / 32] |= 1 << (blockno % 32);
}

释放第 blockno 个磁盘块时,就需要先找到 bitmap 的对应位,bitmap 的类型是 32 位的,也就是说第 blockno 个磁盘块应该位于第 blockno / 32 个数组元素中,再利用位运算把代表 blockno 的那一位置 1 即可。

alloc_block_num & alloc_block

这两个函数主要负责管理内存中的 bitmap,带 num 的函数负责申请块,不带的则直接完成了申请+映射

// Overview:
// Search in the bitmap for a free block and allocate it.
//
// Return -E_NO_DISK if we are out of blocks.
int alloc_block_num(void) {
int blockno;
// walk through this bitmap, find a free one and mark it as used, then sync
// this block to IDE disk (using `write_block`) from memory.
for (blockno = 3; blockno < super->s_nblocks; blockno++) {
if (bitmap[blockno / 32] & (1 << (blockno % 32))) { // the block is free
bitmap[blockno / 32] &= ~(1 << (blockno % 32));
write_block(blockno / BIT2BLK + 2); // write to disk.
return blockno;
}
}
// no free blocks.
return -E_NO_DISK;
}

// Overview:
// Allocate a block -- first find a free block in the bitmap, then map it into memory.
int alloc_block(void) {
int r, bno;
// Step 1: find a free block.
if ((r = alloc_block_num()) < 0) { // failed.
return r;
}
bno = r;

// Step 2: map this block into memory.
if ((r = map_block(bno)) < 0) {
free_block(bno);
return r;
}

// Step 3: return block number.
return bno;
}

read_super & read_bitmap

在这两个函数中,我们完成了文件管理进程的基础数据准备:读取了磁盘的超级块(获得基础信息),同时得到了磁盘的位图 bitmap(占用情况)。

void read_super(void) {
int r;
void *blk;
/* Step 1: 读取 super 块,blockno = 1 */
if ((r = read_block(1, &blk, 0)) < 0) {
user_panic("cannot read superblock: %e", r);
}
super = blk; // 把 super 块缓存地址所在的指针赋给进程中的变量 super,便于后续使用
/* Step 2: 检查 MAGIC NUMBER */
if (super->s_magic != FS_MAGIC) { user_panic("bad file system magic number %x %x", super->s_magic, FS_MAGIC); }
/* Step 3: 检查磁盘大小,超过块缓存允许的大小就会 panic,对应了 Thinking */
if (super->s_nblocks > DISKMAX / BY2BLK) { user_panic("file system is too large"); }
debugf("superblock is good\n");
}

// Overview:
// Read and validate the file system bitmap.
//
// Hint:
// 把磁盘中表示 bitmap 的几个磁盘块全读取到内存中,并设置一个数组指向它们,代表内存中的 bitmap
// For each block i, user_assert(!block_is_free(i))) to check that they're all marked as in use.
void read_bitmap(void) {
u_int i;
void *blk = NULL;
/* Step 1: 计算块数量,并进行 */
u_int nbitmap = super->s_nblocks / BIT2BLK + 1;
for (i = 0; i < nbitmap; i++) {
read_block(i + 2, blk, 0);
}

bitmap = diskaddr(2);

/* Step 2: 确保特殊块为 in-use 状态 */
// Hint: use `block_is_free`
user_assert(!block_is_free(0));
user_assert(!block_is_free(1));

/* Step 3: 确保 bitmap 对应的块为 in-use 状态 */
for (i = 0; i < nbitmap; i++) {
user_assert(!block_is_free(i + 2));
}

debugf("read_bitmap is good\n");
}

va_is_dirty & block_is_dirty

与上文类似,之不过检查的是对应的 PTE_DIRTY 位,也就是检查块缓存是否发生了更改

// Overview:
// Check if this block is dirty. (check corresponding `va`)
int block_is_dirty(u_int blockno) {
void *va = diskaddr(blockno);
return va_is_mapped(va) && va_is_dirty(va);
}
int va_is_dirty(void *va) {
return vpt[VPN(va)] & PTE_DIRTY;
}

dirty_block

既然能够对块缓存进行写入,那就一定要有一个能产生 PTE_DIRTY 的函数

// Overview:
// Mark this block as dirty (cache page has changed and needs to be written back to disk).
int dirty_block(u_int blockno) {
void *va = diskaddr(blockno);
if (!va_is_mapped(va)) { return -E_NOT_FOUND; }
if (va_is_dirty(va)) { return 0; }
return syscall_mem_map(0, va, 0, va, PTE_D | PTE_DIRTY);
}

fs_init

这个函数初始化了文件系统.初始化后,superbitmap 都被缓存到了文件管理进程中

// Overview:
// Initialize the file system.
// Hint:
// 1. read super block.
// 2. check if the disk can work.
// 3. read bitmap blocks from disk to memory.
void fs_init(void) {
read_super(); // 缓存 super 块
check_write_block(); // 一个测试函数
read_bitmap(); // 缓存 bitmap 所用块
}

到这里,与磁盘块相关的函数就结束了。fs.c 中剩余的函数均以文件为读取单位,它们基于我们刚刚分析的读取块的函数而实现,等文件说完再回来看。

struct File

在 MOS 中,描述文件使用文件控制块 File,其定义于 user/include/fs.h,每个控制块的大小为 256 Byte

struct File {
char f_name[MAXNAMELEN]; // filename
uint32_t f_size; // file size in bytes
uint32_t f_type; // file type
uint32_t f_direct[NDIRECT];
uint32_t f_indirect; // points to a block contains pointers

struct File *f_dir; // the pointer to the dir where this file is in, valid only in memory.
char f_pad[BY2FILE - MAXNAMELEN - (3 + NDIRECT) * 4 - sizeof(void *)];
} __attribute__((aligned(4), packed));

#define BY2FILE 256
#define MAXNAMELEN 128

基础的文件结构不再赘述,需要注意文件控制块中包含一个指向目录的控制块指针,可以用来表示位置;同时还使用 f_pad 域将控制块补全至 256 字节,使得一个 Block 能包含整数个文件块。

前面说了那么多,大部分是从物理单位(块、扇区)对磁盘进行操作,实际上我们真正要对磁盘操作时,应该从逻辑单位(文件)操作。说白了,文件/磁盘块有点类似于页式存储中段/页的概念。在 load_icode 中我们以页为单位进行了读入,实际上还是为了把整个二进制 ELF 全部读入;文件也类似,要读写文件,最终也是以块为单位出发去实现的。

serv.c - 文件系统服务进程

与指导书介绍顺序不同,我们这里先讲一下文件系统服务进程。

struct Open

在文件服务进程的源代码中,我们找到了一个新数据结构的定义:Open

struct Open {
struct File *o_file; // mapped descriptor for open file
u_int o_fileid; // file id
int o_mode; // open mode
struct Filefd *o_ff; // va of filefd page
};

这个结构可以看成是文件服务的柜台窗口,文件管理进程想要打开/操作一个文件,必须先获取一个窗口,然后再在窗口上进行操作。其中的 o_ff 字段大小为一页,我个人把他叫做这个窗口的工作区,在工作区中存放了当前操作文件的 Filefd ,并且经常要发生赋值和写入(为了更新文件状态)。

同样的,我们设置了一个管理窗口的数组 opentab,可以对其标识符 o_fileid 进行遍历,从而获取指定窗口。具体存放结构如图:

总而言之,文件系统进程通过 Open 来管理文件的打开与否,但文件信息大多还是需要 File 和 Filefd

插播一个很巧妙的函数:open_alloc,也即申请空闲窗口的函数。

open_alloc

为什么说它巧,因为它利用了 pp_ref 字段判断窗口当前是否处于使用中。

int open_alloc(struct Open **o) {
int i, r;

// Find an available open-file table entry
for (i = 0; i < MAXOPEN; i++) {
switch (pageref(opentab[i].o_ff)) {
case 0:
if ((r = syscall_mem_alloc(0, opentab[i].o_ff, PTE_D | PTE_LIBRARY)) < 0) {
return r;
}
case 1:
opentab[i].o_fileid += MAXOPEN;
*o = &opentab[i];
memset((void *)opentab[i].o_ff, 0, BY2PG);
return (*o)->o_fileid;
}
}

return -E_MAX_OPEN;
}

因为在下面的开启文件的函数中,最后一步是把窗口的工作区映射并共享给请求者,这使得 pp_ref 必然 ≥ 2(自身 + 不少于一个请求者)。

ipc_send(envid, 0, o->o_ff, PTE_D | PTE_LIBRARY); // 把 Filefd 共享给请求进程

当遍历所有窗口的工作区时,一旦发现 pp_ref == 1,则意味着所有请求者都主动释放了该页面(文件不再使用后请求者会自行释放,但文件服务进程永远不会主动释放)。也就意味着当前工作区无人使用,就拿来再次分配了。

文件块 & 磁盘块 & 块缓存

开始还有点不解,不是说每个磁盘块都要装到固定的块缓存中吗,在 file_map_block 中指定了块号为什么随便 alloc 了一个就用了呢?

前半句确实是正确的,这是磁盘块缓存的要求,但是我们这里“指定”的块号是文件块号,它代表文件中的第 filebno 块,而在磁盘中具体怎么存放我们并不关心。

更通俗的讲,可以把文件看成虚拟内存,而磁盘看成物理内存:

  • 文件块是逻辑上连续的,每个文件都是如此
  • 磁盘块是物理上连续的,同时,它在装入块缓存时又是一一对应的
  • 相邻的文件块可以通过 alloc_block 函数映射到不相邻的磁盘块内

但和存储管理不同的是,我们这里并没有什么页表去存放映射关系,保存映射的是文件管理块。所以 pgdir_walk 查的是页表, file_block_walk 查的是文件块和文件块内部的数据,如图:

mapping_relation

fs.c 中的文件操作函数

在文件系统服务函数中,我们可以发现它们大多使用了 file_* 类型的函数,他们来自之前剩一点没讲的 fs.c,剩下的部分是以文件为单位进行交互的函数集合。

现在我们就对这些函数做一些分析。

一图流

包含了前面的块操作函数

fs.c

file_create

  • 创建 path 指向的文件,返回文件控制块
// Overview:
// Create "path".
//
// Post-Condition:
// On success set *file to point at the file and return 0.
// On error return < 0.
int file_create(char *path, struct File **file) {
char name[MAXNAMELEN];
int r;
struct File *dir, *f;
/* Step 1: 主要获取 path 包含的目录,顺便查一下文件是否存在 */
if ((r = walk_path(path, &dir, &f, name)) == 0) { return -E_FILE_EXISTS; }
/* Step 2: 路径、过程中的路径不存在,返回错误 */
if (r != -E_NOT_FOUND || dir == 0) { return r; }
/* Step 3: 在指定目录下申请一个文件 */
if (dir_alloc_file(dir, &f) < 0) { return r; }
/* Step 4: 向文件赋名,返回 */
strcpy(f->f_name, name);
*file = f;
return 0;
}

file_open & walk_path

直接调用了另一个函数:walk_path

// Overview:
// 从根目录开始,搜索 path 指向的文件,返回路径目录的控制块和文件控制块
int walk_path(char *path, struct File **pdir, struct File **pfile, char *lastelem) {
char *p;
char name[MAXNAMELEN];
struct File *dir, *file;
int r;

// start at the root.
path = skip_slash(path);
file = &super->s_root;
dir = 0;
name[0] = 0;

if (pdir) { *pdir = 0; }
*pfile = 0;

// find the target file by name recursively.
while (*path != '\0') {
dir = file;
p = path;
while (*path != '/' && *path != '\0') { path++; }
if (path - p >= MAXNAMELEN) { return -E_BAD_PATH; }
memcpy(name, p, path - p); // 截断路径并存入 name 数组中
name[path - p] = '\0';
path = skip_slash(path);
if (dir->f_type != FTYPE_DIR) { return -E_NOT_FOUND; } // 检查途径目录的状态

if ((r = dir_lookup(dir, name, &file)) < 0) { // 若正常,则应递归查找 path
if (r == -E_NOT_FOUND && *path == '\0') { // dir_lookup:path 结束且找不到文件
if (pdir) { *pdir = dir; }
if (lastelem) { strcpy(lastelem, name); }
*pfile = 0;
}
return r;
}
}

if (pdir) { *pdir = dir; } // 成功递归出 file 的位置
*pfile = file;
return 0;
}

// Overview:
// Open "path".
//
// Post-Condition:
// On success set *pfile to point at the file and return 0.
// On error return < 0.
int file_open(char *path, struct File **file) {
return walk_path(path, 0, file, 0);
}
  • 我们的查找从根目录开始(super),一直到路径结束前,都采用递归的形式对路径中每个用“/”分开的文件夹进行搜索(dir_lookup)
  • dir_lookup:如果进入异常退出的分支,则意味着 r == -E_NOT_FOUND(目录里没有这个文件) && *path == '\0'(找到文件所在的最底一层目录了),也就是文件不存在,应当退出。

file_get_block & file_map_block & file_block_walk

  • file_block_walk:类似 pgdir_walk,获取文件 f 的第 filebno 块在磁盘中的磁盘块号;如果在非直接指针区域、没有间接指针块,会创建一个间接块(但是没有申请 filebno 的块)
  • file_map_block:上一个函数的封装,获取第 filebno 块在磁盘中的块号;如果文件块 → 磁盘块的映射不存在,则会申请磁盘块形成一个映射
  • file_get_block:先获取第 filebno 块在磁盘中的块号,再把数据从磁盘中读到内存中(read_block)

第三个函数是在服务进程中申请某个地址的内容时使用的,用于把指定地址所在的块一并加载,供用户使用。最后实现时需要把直接指针和间接指针块对外包装出同等的访问方式,因此需要在内部处理差异。

前两个是比较底层的工具函数,没有在 fs.c 之外的文件内出现,所以没什么印象也没关系的吧(大嘘

// Overview:
// Set *blk to point at the filebno'th block in file f.
int file_get_block(struct File *f, u_int filebno, void **blk) {
int r;
u_int diskbno;
u_int isnew;

// Step 1: 找到文件 f 第 filebno 个块在磁盘中对应的磁盘号
if ((r = file_map_block(f, filebno, &diskbno, 1)) < 0) {
return r;
}

// Step 2: 从磁盘中读该磁盘号的数据到 blk 中
if ((r = read_block(diskbno, blk, &isnew)) < 0) {
return r;
}
return 0;
}
// OVerview:
// Set *diskbno to the disk block number for the filebno'th block in file f.
// If alloc is set and the block does not exist, allocate it.
// 完整的封装
int file_map_block(struct File *f, u_int filebno, u_int *diskbno, u_int alloc) {
int r;
uint32_t *ptr;

// Step 1: 找到指定块的地址,存在 ptr 里
if ((r = file_block_walk(f, filebno, &ptr, alloc)) < 0) {
return r;
}

// Step 2: 目标块的地址不存在,根据 alloc 决定是否申请
if (*ptr == 0) {
if (alloc == 0) {
return -E_NOT_FOUND;
}

if ((r = alloc_block()) < 0) {
return r;
}
*ptr = r;
}

// Step 3: 类似的,给指针赋值
*diskbno = *ptr;
return 0;
}
// Overview:
// 获取指定文件 f 的指定块在磁盘中的块号,alloc == 1 && NINDIRECT 时需要申请指针块
//
// Post-Condition:
// Return 0 on success, and set *ppdiskbno to the pointer to the target block.
// Return -E_NOT_FOUND if the function needed to allocate an indirect block, but alloc was 0.
// Return -E_NO_DISK if there's no space on the disk for an indirect block.
// Return -E_NO_MEM if there's not enough memory for an indirect block.
// Return -E_INVAL if filebno is out of range (>= NINDIRECT).
int file_block_walk(struct File *f, u_int filebno, uint32_t **ppdiskbno, u_int alloc) {
int r;
uint32_t *ptr;
uint32_t *blk;

if (filebno < NDIRECT) {
// Step 1: 直接指针,则 ptr 直接指向指定的块
ptr = &f->f_direct[filebno];
} else if (filebno < NINDIRECT) {
// Step 2: 非直接指针,根据 alloc 决定是否需要为文件申请一个指针块
if (f->f_indirect == 0) {
if (alloc == 0) {
return -E_NOT_FOUND;
}

if ((r = alloc_block()) < 0) {
return r; // 申请失败,返回
}
f->f_indirect = r; // 申请成功,建立连接
}

// Step 3: 把指针块映射到文件管理进程的内存中,这时并没有为目标块创建块缓存
if ((r = read_block(f->f_indirect, (void **)&blk, 0)) < 0) {
return r;
}
ptr = blk + filebno; // 令 ptr 指向指针块中的第 filebno 项,也就是第 filebno 块
} else {
return -E_INVAL;
}

// Step 4: 保存 ptr 的值,返回
*ppdiskbno = ptr;
return 0;
}

file_set_size & file_clear_block & file_truncate & file_flush

函数的作用是设定指定文件的大小,其中还会调用三个底层函数

  • file_truncate:释放文件缩小后占用多余的块
  • file_clear_block:实际释放块使用的函数
  • file_flush:同步文件内容至磁盘,此处用于更新被修改文件的目录,也就是同步目录中的所有文件
// Overview:
// 设定文件的大小
int file_set_size(struct File *f, u_int newsize) {
/* Step 1: 若文件变小,则需要释放多余的块 */
if (f->f_size > newsize) { file_truncate(f, newsize); }
/* Step 2: 更新文件的大小 */
f->f_size = newsize;
/* Step 3: */
if (f->f_dir) { file_flush(f->f_dir); }

return 0;
}
// Overview:
// 清除文件多余的占用块,同时注意清理间接指针块
void file_truncate(struct File *f, u_int newsize) {
u_int bno, old_nblocks, new_nblocks;

/* Step 1: 设定缩小前后所占用的块数量 */
old_nblocks = f->f_size / BY2BLK + 1;
new_nblocks = newsize / BY2BLK + 1;
if (newsize == 0) { new_nblocks = 0; }

/* Step 2: 释放多余的块缓存;同时如果缩小到不需要使用间接指针块,则释放间接块 */
if (new_nblocks <= NDIRECT) {
for (bno = new_nblocks; bno < old_nblocks; bno++) {
file_clear_block(f, bno);
}
if (f->f_indirect) {
free_block(f->f_indirect);
f->f_indirect = 0;
}
} else {
/* Step 2: 否则只单纯释放块缓存,保留间接块 */
for (bno = new_nblocks; bno < old_nblocks; bno++) {
file_clear_block(f, bno);
}
}
/* Step 3: 更新文件大小 */
f->f_size = newsize;
}

int file_clear_block(struct File *f, u_int filebno) {
int r;
uint32_t *ptr;
/* Step 1: 找到 filebno 指定的块地址 */
if ((r = file_block_walk(f, filebno, &ptr, 0)) < 0) { return r; }
/* Step 2: 释放指定块的块缓存 */
if (*ptr) {
free_block(*ptr);
*ptr = 0;
}
return 0;
}

// Overview:
// 同步块缓存中的文件内容至磁盘
void file_flush(struct File *f) {
// Your code here
u_int nblocks;
u_int bno;
u_int diskno;
int r;

nblocks = f->f_size / BY2BLK + 1;
/* Step 1: 对文件中所有占用的块遍历 */
for (bno = 0; bno < nblocks; bno++) {
/* Step 2: 获取文件中第 bno 块的磁盘块号 diskno */
if ((r = file_map_block(f, bno, &diskno, 0)) < 0) {
continue;
}
/* Step 3: 如果磁盘块在加载到缓存后被更新过,则写回 */
if (block_is_dirty(diskno)) {
write_block(diskno);
}
}
}

file_close

  • 比较简单,但是断档继续写的时候,我已经快忘了 file_flush 是干嘛的了(悲
// Overview:
// Close a file.
void file_close(struct File *f) {
// 将文件本身 + 目录同步回磁盘块中
file_flush(f);
if (f->f_dir) {
file_flush(f->f_dir);
}
}

file_dirty

  • 调用了前面的函数 dirty_block,实现了对指定文件的特定地址的 DIRTY 标识。(在写入 offset 后,为了保证能够同步块缓存和磁盘的数据,需要调用这个函数了)
// Overview:
// 标记指定文件地 offset 所在的块为 DIRTY
int file_dirty(struct File *f, u_int offset) {
int r;
u_int diskbno;
// 使用 file_map_block 找到 offset 对应的磁盘块号
if ((r = file_map_block(f, offset / BY2BLK, &diskbno, 0)) < 0) { return r; }
// 把指定磁盘块的块缓存置为 DIRTY
return dirty_block(diskbno);
}

file_remove

  • 通过字符串展示的路径先查询到文件控制块,然后使用将自身大小减为 0 的方式解除所有占用的内存块;最后同步至磁盘内
// Overview:
// 清除文件
int file_remove(char *path) {
int r;
struct File *f;

// Step 1: 找到要 remove 的文件,指针存到 f 内
if ((r = walk_path(path, 0, &f, 0)) < 0) { return r; }

// Step 2: 将体积缩小为 0
file_truncate(f, 0);

// Step 3: 清除名称,查询是否文件是否占用时看的就是 f_name[0]
f->f_name[0] = '\0';

// Step 4: 同步自身内容和目录内容至磁盘内
file_flush(f);
if (f->f_dir) { file_flush(f->f_dir); }

return 0;
}

file_sync

  • 啊我整个大的.jpg

对于整个块缓存空间来说,同步所有写入过的块

// Overview:
// Sync the entire file system. A big hammer.
void fs_sync(void) {
int i;
for (i = 0; i < super->s_nblocks; i++) {
if (block_is_dirty(i)) {
write_block(i);
}
}
}

dir_lookup - Exercise 5.8

  • 遍历目录中的每个文件,查找名称是否相同;同时,遍历的方式是先块后文件,详细可以看代码
// Overview:
// 查找当前路径下 dir 中字符串 name 指向的文件,返回文件控制块 file
int dir_lookup(struct File *dir, char *name, struct File **file) {
int r;
/* Step 1: 通过占用块计算目录 dir 大小 */
u_int nblock;
/* Exercise 5.8: Your code here. (1/3) */
nblock = dir->f_size / BY2BLK + (dir->f_size % BY2BLK == 0) ? 0 : 1;
/* Step 2: 遍历目录中的每个文件: */
// 2.1: 遍历每个文件块
for (int i = 0; i < nblock; i++) {
// 获取目录中的每个磁盘块
void *blk;
/* Exercise 5.8: Your code here. (2/3) */
file_get_block(dir, i, &blk);
struct File *files = (struct File *)blk;

// 2.2: 遍历块中的每个文件
for (struct File *f = files; f < files + FILE2BLK; ++f) {
// 比较文件名,相同则返回
/* Exercise 5.8: Your code here. (3/3) */
if (strcmp(f->f_name, name) == 0) {
*file = f;
f->f_dir = dir;
return 0;
}
}
}
/* Step 3: 所有文件内均未找到,返回 -E_NOT_FOUND */
return -E_NOT_FOUND;
}

dir_alloc_file

  • 在目录中查找空白文件管理块,并返回这个块。当目录已满时需要自动扩容
// Overview:
// 在给定的路径 dir 中申请一个文件空间,返回文件控制块
int dir_alloc_file(struct File *dir, struct File **file) {
int r;
u_int nblock, i, j;
void *blk;
struct File *f;

nblock = dir->f_size / BY2BLK;
/* Step 1: 遍历所有目录的磁盘块(按文件控制块大小遍历),尝试发现空白的控制块 */
for (i = 0; i < nblock; i++) {
// read the block.
if ((r = file_get_block(dir, i, &blk)) < 0) {
return r;
}

f = blk;

for (j = 0; j < FILE2BLK; j++) {
/* Step 2: 发现空白控制块,直接返回 */
if (f[j].f_name[0] == '\0') {
*file = &f[j];
return 0;
}
}
}

/* Step 3: 目录中已有的所有块都占用,通过 file_get_block 尝试申请新磁盘块,并把文件放在新块里 */
dir->f_size += BY2BLK;
if ((r = file_get_block(dir, i, &blk)) < 0) {
return r;
}
f = blk;
*file = &f[0];

return 0;
}

这个函数把 file_get_block 这个函数用得很灵活,一是执行其最基本的方法,即实现文件块到磁盘块的映射;同时在 Step 3 中还充当了为文件 alloc_block 的磁盘块申请者(映射肯定也同时生成在文件控制块中了)

serv.c 文件系统进程服务函数

里面函数太多了,偷个懒

一图流

serv.c

serve_open

根据输入,申请一个工作区后打开文件并保存文件信息,完成后直接 ipc_send 返回。

重点体现在后面这个保存信息上了:

void serve_open(u_int envid, struct Fsreq_open *rq) {
struct File *f;
struct Filefd *ff;
int r;
struct Open *o;

// Find a file id.
if ((r = open_alloc(&o)) < 0) { ipc_send(envid, r, 0, 0); }
// Open the file.
if ((r = file_open(rq->req_path, &f)) < 0) {
ipc_send(envid, r, 0, 0);
return;
}
// Save the file pointer.
o->o_file = f; // file_open 里获取到的文件指针给窗口保管
// Fill out the Filefd structure
ff = (struct Filefd *)o->o_ff; // 把工作区当成 Filefd 准备写入
ff->f_file = *f; // 工作区存放文件
ff->f_fileid = o->o_fileid; // 文件的 id 来自于窗口的 id
o->o_mode = rq->req_omode; // 窗口工作类型由输入决定
ff->f_fd.fd_omode = o->o_mode; // Filefd 里的工作类型也进行更新
ff->f_fd.fd_dev_id = devfile.dev_id; // 意义不明,待补

ipc_send(envid, 0, o->o_ff, PTE_D | PTE_LIBRARY); // 把 Filefd 共享给请求进程
}

serve_map

  • 将目标文件 offset 处的磁盘块的内容映射(分享)给发起请求的用户进程

用户进程请求获取文件块内容时,文件管理进程会使用这个函数,它先根据文件块和 offset 获取到实际所在的磁盘块,最后再把在文件管理进程中缓存的信息以共享页面的形式发送给用户进程。

这里也是一个有一个从文件块→磁盘块的映射

void serve_map(u_int envid, struct Fsreq_map *rq) {
struct Open *pOpen;
u_int filebno;
void *blk;
int r;
/* Step 1: 查询存放当前文件的窗口 */
if ((r = open_lookup(envid, rq->req_fileid, &pOpen)) < 0) {
ipc_send(envid, r, 0, 0);
return;
}

filebno = rq->req_offset / BY2BLK;
/* Step 2: 获取映射的地址和磁盘块 */
if ((r = file_get_block(pOpen->o_file, filebno, &blk)) < 0) {
ipc_send(envid, r, 0, 0);
return;
}
/* Step 3: 将找到的磁盘块对应的块缓存分享给请求的用户进程 */
ipc_send(envid, 0, blk, PTE_D | PTE_LIBRARY);
}

serve_set_size

void serve_set_size(u_int envid, struct Fsreq_set_size *rq) {
struct Open *pOpen;
int r;
/* Step 1: 查询存放当前文件的窗口 */
if ((r = open_lookup(envid, rq->req_fileid, &pOpen)) < 0) {
ipc_send(envid, r, 0, 0);
return;
}
/* Step 2: 利用窗口中存放的文件控制块更改文件大小 */
if ((r = file_set_size(pOpen->o_file, rq->req_size)) < 0) {
ipc_send(envid, r, 0, 0);
return;
}

ipc_send(envid, 0, 0, 0);
}

其余的 serve_* 函数实际上也类似,大部分都是对文件交互函数 file_* 函数的调用,这里函数充当接口,统一化处理信息。

See Part1 at here

See Part3 at here