BUAA-OS-Lab5 : 文件管理系统
一、实验难点
- lab5新增加的虚拟地址的映射布局
- 需要明确:广义用户进程是相对于内核态的,lab5的文件服务进程和用户进程都是用户态下的进程。但是以下涉及的lab5的“用户进程”都是相对服务进程而言的普通用户进程。尽管文件服务进程也属于广义用户进程。
- 用户进程:文件映射区
- 文件服务进程:块缓存
- lab5新增加的物理空间和虚拟空间的对应
- 磁盘-文件系统缓存-用户进程的文件区
- 用户读写文件时通过fsipc,把文件块在fs进程对应的块缓存读到用户的文件映射区,而fs进程的块缓存通过ide与磁盘交互。
- 多角色间的协作
- 在做完lab4梳理系统调用、fork、页缺失处理的时候,反观出现在lab4的ipc通信机制,我总觉得它和Lab4的内容格格不入。因为感觉Lab4的内核态和用户态通过系统调用浑然一体地联系了起来,包办好了这三项任务,而此时出现的负责用户进程和用户进程地通信机制ipc看上去是那么不合时宜。
所以个人觉得 lab4的两次extra都是关于ipc的考题,是否有些重点偏了呢(x - 是在lab5通过文件系统服务的流程梳理,明确了用户进程和服务进程两个用户态进程,才理清楚进程之间ipc机制的流程。
- 以下是fs进程的初始化:
- 在做完lab4梳理系统调用、fork、页缺失处理的时候,反观出现在lab4的ipc通信机制,我总觉得它和Lab4的内容格格不入。因为感觉Lab4的内核态和用户态通过系统调用浑然一体地联系了起来,包办好了这三项任务,而此时出现的负责用户进程和用户进程地通信机制ipc看上去是那么不合时宜。
- 两次ipc机制
- 第一次ipc:
- 用户进程fsipc()的ipc_send()唤醒被ipc_recv()阻塞的fs进程。
- 用户进程ipc_send()的页面为req结构体,把用户进程的请求信息传递给fs进程,写进fs进程的dstva为fs进程的REQVA页。
- 此后fs进程执行文件服务函数,用户进程被fsipc()的ipc_recv()阻塞,此时的待写入页面为INDEX2FD(fd)页面,此页面需要fs进程执行完文件服务后填入文件的相关信息。
- 第二次ipc:
- fs进程执行过程中,把获取到的文件信息保存在
struct Open o = opentab[i]
结构体中的o_ff页面。(此页面为open_alloc时使用系统调用alloc)。 - fs进程执行完文件服务后,ipc_send()唤醒被ipc_recv()阻塞的用户进程,把写入了文件信息的页面o->o_ff作为ipc的srcva,映射给用户进程的待写入页面dstva:INDEX2FD(fd)。
- 这也是理解难度最大的一个页面:实际上
opentab[i].o_ff,fd,ffd
这一些虚拟页面都指向同一个物理页面。由于这个页面先是在opentab[i]里判断o_ff地址的pp_ref,再进入没有break的两个case分支,使得o_ff地址在fs进程里alloc出物理页面p,再在fs进程和用户进程之间以ipc映射。映射给用户进程的dstva。dstva在用户进程下的一个名字是fd,另一个名字是ffd。实际上fd和ffd不仅所对物理页面一样,他们作为指针保存的虚拟地址dstva也是一样的。这个页面在两个进程里都不好理解,所以可以说是lab5最大的难点吧…
- fs进程执行过程中,把获取到的文件信息保存在
- 第一次ipc:
- 用户文件读写流程、用户通过fsipc机制调用的文件服务接口
二、思考题
Thinking 5.4
查找代码中的相关定义,试回答一个磁盘块中最多能存储多少个文件控制块?一个目录下最多能有多少个文件?我们的文件系统支持的单个文件最大为多大?
BY2FILE = 256,一个文件控制块为256B。
BY2BLK = 4096,一个磁盘块4KB。
因此一个磁盘块中包含4KB / 256B = 16个文件控制块。
一个目录包含1024个指向磁盘块的指针,即最多有1024 * 16 = 16384个文件。
1024个磁盘块共1024 * 4KB = 4MB。单个文件最大为4MB。
Thinking 5.5
请思考,在满足磁盘块缓存的设计的前提下,我们实验使用的内核支持的最大磁盘大小是多少?
DISKMAX = 0x40000000,因此支持的最大磁盘大小为1GB。
Thinking 5.7
在lab5 中,fs/fs.h、include/fs.h 等文件中出现了许多宏定义,试列举你认为较为重要的宏定义,并进行解释,写出其主要应用之处。
- user/fd.h 这两个宏用来找fd对应的文件信息页面和文件缓存区地址
#define INDEX2FD(i) (FDTABLE+(i)*BY2PG) #define INDEX2DATA(i) (FILEBASE+(i)*PDMAP)
- 文件服务函数调用号,可以重用user/fsipc.c的fsipc()函数,作为进程间通信的value,用户进程传给文件服务系统的fs/serve()
#define FSREQ_OPEN 1 #define FSREQ_MAP 2 #define FSREQ_SET_SIZE 3 #define FSREQ_CLOSE 4 #define FSREQ_DIRTY 5 #define FSREQ_REMOVE 6 #define FSREQ_SYNC 7
Thinking 5.8
阅读 user/file.c ,你会发现很多函数中都会将一个 struct Fd * 型的指针转换为 struct Filefd * 型的指针,请解释为什么这样的转换可行。
在结构体Filefd中储存的第一个元素就是struct Fd*,因而对于相匹配的一对struct Fd和struct Filefd,他们的指针实际上指向了相同的虚拟地址INDEX2FD(fd)
Thinking 5.9
在 lab4 的实验中我们实现了极为重要的 fork 函数。那么 fork 前后的父子进程是否会共享文件描述符和定位指针呢?请在完成上述练习的基础上编写一个程序进行验证。
fork 前后的父子进程共享文件描述符和定位指针。
验证程序:
#include "lib.h"
static char *msg = "This is the NEW message of the day!\n\n";
static char *diff_msg = "This is a different massage of the day!\n\n";
void umain()
{
int r;
int fdnum;
char buf[512];
int n;
if ((r = open("/newmotd", O_RDWR)) < 0) {
user_panic("open /newmotd: %d", r);
}
fdnum = r;
writef("open is good\n");
if ((n = read(fdnum, buf, 511)) < 0) {
user_panic("read /newmotd: %d", r);
}
if (strcmp(buf, diff_msg) != 0) {
user_panic("read returned wrong data");
}
writef("read is good\n");
int id;
if ((id = fork()) == 0) {
if ((n = read(fdnum, buf, 511)) < 0) {
user_panic("child read /newmotd: %d", r);
}
if (strcmp(buf, diff_msg) != 0) {
user_panic("child read returned wrong data");
}
writef("child read is good && child_fd == %d\n",r);
struct Fd *fdd;
fd_lookup(r,&fdd);
writef("child_fd's offset == %d\n",fdd->fd_offset);
}
else {
if((n = read(fdnum, buf, 511)) < 0) {
user_panic("father read /newmotd: %d", r);
}
if (strcmp(buf, diff_msg) != 0) {
user_panic("father read returned wrong data");
}
writef("father read is good && father_fd == %d\n",r);
struct Fd *fdd;
fd_lookup(r,&fdd);
writef("father_fd's offset == %d\n",fdd->fd_offset);
}
}
结果:
main.c: main is start ...
init.c: mips_init() is called
Physical memory: 65536K available, base = 65536K, extended = 0K
to memory 80401000 for struct page directory.
to memory 80431000 for struct Pages.
pmap.c: mips vm init success
pageout: @@@___0x7f3fe000___@@@ ins a page
pageout: @@@___0x40d000___@@@ ins a page
FS is running
FS can do I/O
pageout: @@@___0x7f3fe000___@@@ ins a page
pageout: @@@___0x407000___@@@ ins a page
superblock is good
diskno: 0
diskno: 0
read_bitmap is good
diskno: 0
alloc_block is good
file_open is good
file_get_block is good
file_flush is good
file_truncate is good
diskno: 0
file rewrite is good
serve_open 00000400 ffff000 0x2
open is good
read is good
father read is good && father_fd == 0
father_fd's offset == 41
[00000400] destroying 00000400
[00000400] free env 00000400
i am killed ...
child read is good && child_fd == 0
child_fd's offset == 41
[00001402] destroying 00001402
[00001402] free env 00001402
i am killed ...
Thinking 5.10
请解释 Fd, Filefd, Open 结构体及其各个域的作用。比如各个结构体会在哪些过程中被使用,是否对应磁盘上的物理实体还是单纯的内存数据等。说明形式自定,要求简洁明了,可大致勾勒出文件系统数据结构与物理实体的对应关系与设计框架。
struct Fd {
u_int fd_dev_id; // 外设的id。
//用户使用fd.c的文件接口时,不同的dev_id会调取不同的文件服务函数。
//比如File类型的文件服务函数为user/File.c的file_*()函数。
u_int fd_offset; // 读写的偏移量
//seek()时修改。
//offset会被用来找起始filebno文件块号。
u_int fd_omode; // 打开方式,包括只读、只写、读写
//req和open结构体都会用到
};
struct Filefd {
struct Fd f_fd; // file descriptor
u_int f_fileid; // 文件的id
//模1024后会用来在opentab[]里索引open结构体
struct File f_file; // 对应文件的文件控制块
};
struct Open {
struct File *o_file; // 指向打开的文件
u_int o_fileid; // 打开文件的id
int o_mode; // 打开方式
struct Filefd *o_ff; // 指向读写的位置(偏移)
};
- 结构体均为内存数据,记录了文件信息。
- Filefd以及Open中的指向的文件控制块File中记录的磁盘指针对应物理实体。
Thinking 5.11
上图中有多种不同形式的箭头,请结合 UML 时序图的规范,解释这些不同箭头的差别,并思考我们的操作系统是如何实现对应类型的进程间通信的。
- ENV_CREATE(user_env)和ENV_CREATE(fs_serv)都是异步消息,由init()发出创建消息后,init()函数即可返回执行后续步骤,由fs和user线程执行自己的初始化工作。
- fs线程初始化serv_init()和fs_init()完成后,进入serv()函数,被ipc_receive()阻塞为ENV_NOT_RUNNABLE,直到收到User线程的ipc_send(fsreq)被唤醒。
- User线程向fs线程ipc_send(fsreq)发送请求为同步消息,发送后自身进入阻塞ENV_NOT_RUNNABLE等待被唤醒的fs线程服务结束时ipc_send(dst_va),用户线程接收到数据后继续运行,此后fs线程进入阻塞,等待下次被用户唤醒。
Thinking 5.12
阅读serv.c/serve函数的代码,我们注意到函数中包含了一个死循环for (;;) {…},为什么这段代码不会导致整个内核进入 panic 状态?
serve调用ipc_recv函数后会将自身状态变为ENV_NOT_RUNNABLE,进入等待状态。其他进程发出文件系统请求后才被唤醒并开始服务。
三、体会与感想
- lab5的感觉是填空代码量锐减,但是需要阅读的代码量骤增,一个文件的open操作的函数嵌套层数也是让我大开眼界:)。所以一开始区区18页的指导书让我以为lab5很简单:) 我觉得lab5的复杂度和lab4不相上下,尽管理解上比中断、页缺失容易很多。
- lab5的局部代码理解起来比前边lab更快,可能是因为内核态用户态的接口切换 和 线程间的通信协作在lab4有过涉及,文件索引函数等与lab2的内存管理大体结构比较相似,对于宏函数、指针、结构体以及函数间的调用和异常的写法在前边的lab都有了很好的练习。
- 从lab4到lab5更加体会到MOS操作系统设计方面的精巧,各lab之间环环相扣,借助之前lab的代码实现本次lab的新功能,看着自己写的操作系统一步步有了更加完善的功能是非常有趣的一种感受。