BUAA_OS lab5实验报告
一、思考题
1. 思考5.1
查阅资料,了解 Linux/Unix 的 /proc 文件系统是什么?有什么作用? Windows 操作系统又是如何实现这些功能的?proc 文件系统这样的设计有什么好处和可以改进的地方?
Linux系统上的/proc是一种伪文件系统(虚拟文件系统),存储的是当前内核运行状态的一系列特殊文件。
用户可以通过这些文件查看有关系统硬件及当前正在运行进程的信息,甚至可以通过更改其中某些文件来改变内核的运行状态。因为系统信息如进程是动态改变的,所以用户或应用程序读取proc文件时,proc文件系统是动态从系统内核读出所需信息并提交的。
Windows通过调用Win32 API函数实现与内核模式的交互。
proc文件系统的设计将内核交互变成了简单的修改文件,大大简化了交互的过程。另一方面,Windows的多个磁盘意味着有多个根目录,而proc文件系统只有一个根目录,这是一个相对不足的地方。
2. 思考5.2
如果我们通过 kseg0 读写设备,我们对于设备的写入会缓存到 Cache 中。通过 kseg0 访问设备是一种错误的行为,在实际编写代码的时候这么做会引发不可预知的问题。请你思考:这么做这会引起什么问题?对于不同种类的设备(如我们提到的串口设备和 IDE 磁盘)的操作会有差异吗?可以从缓存的性质和缓存刷新的策略来考虑。
缓存机制是为了提高效率而设计的,具体体现为数据发生改变时并不立即写入内存,而是在cache发生替换时才写入。而我们的控制台需要实时的交互,因此若写入kseg0部分,数据会经过cache,甚至可能很久都不被真正写入,因此我们看到的输出可能并不是该部分实际的内容,基于这种历史数据很可能给出错误的交互行为。
不同种类的设备操作有差异。串口相对于IDE磁盘速度更快,因此操作串口时可以考虑提高缓存的刷新频率。相比之下,可能是由于涉及到多个线路的数据冲突问题,IDE的操作更加复杂。
3. 思考5.3
一个磁盘块最多存储 1024 个指向其他磁盘块的指针,试计算我们的文件系统支持的单个文件的最大大小为多大?
因为BY2BLK = 4096,即一个磁盘块大小为4KB,所以1024个磁盘块共1024 * 4KB = 4MB。单个文件最大为4MB。
4. 思考5.4
查找代码中的相关定义,试回答一个磁盘块中最多能存储多少个文件控制块?一个目录下最多能有多少个文件?
BY2FILE = 256,即一个文件控制块为256B。BY2BLK = 4096,即一个磁盘块4KB。因此一个磁盘块中包含4KB / 256B = 16个文件控制块。
一个目录包含1024个指向磁盘块的指针,即最多有1024 * 16 = 16384个文件。
5. 思考5.5
请思考,在满足磁盘块缓存的设计的前提下,我们实验使用的内核支持的最大磁盘大小是多少?
DISKMAX = 0x40000000,因此支持的最大磁盘大小为1GB。
6. 思考5.6
如果将 DISKMAX 改成 0xC0000000, 超过用户空间,我们的文件系统还能正常工作吗?为什么?
若文件过大,将不能正常工作。地址超过用户空间后,若进行alloc是没有权限的,此时程序会直接终止。
7. 思考5.7
阅读 user/file.c,你会发现很多函数中都会将一个 struct Fd* 型的 指针转换为 struct Filefd* 型的指针,请解释为什么这样的转换可行。
注意到,两个结构体的定义如下:
struct Fd {
u_int fd_dev_id;
u_int fd_offset;
u_int fd_omode;
};
struct Filefd {
struct Fd f_fd;
u_int f_fileid;
struct File f_file;
};
Filefd结构体的第一个成员就是Fd,因此指向Filefd的指针同样指向这个Fd的起始位置,故可以强制转换。
8. 思考5.8
请解释 Fd, Filefd, Open 结构体及其各个域的作用。比如各个结构体会在哪些过程中被使用,是否对应磁盘上的物理实体还是单纯的内存数据等。说明形式自定,要求简洁明了,可大致勾勒出文件系统数据结构与物理实体的对应关系与设计框架。
Fd用于记录文件的基本信息,需要单独占用一页:
// file descriptor
struct Fd {
u_int fd_dev_id; // 外设的id
u_int fd_offset; // 读写的偏移量
u_int fd_omode; // 打开方式,包括只读、只写、读写
};
Filefd用于记录文件的详细信息,Fd也存储在其中:
// file descriptor + file
struct Filefd {
struct Fd f_fd; // file descriptor
u_int f_fileid; // 文件的id
struct File f_file; // 真正的文件本身
};
Open是打开文件行为的抽象:
struct Open {
struct File *o_file; // 指向打开的文件
u_int o_fileid; // 打开文件的id
int o_mode; // 打开方式
struct Filefd *o_ff; // 指向读写的位置(偏移)
};
9. 思考5.9
阅读serve函数的代码,我们注意到函数中包含了一个死循环for (;;) {...},为什么这段代码不会导致整个内核进入panic 状态?
因为serve调用ipc_recv函数后会将自身状态变为ENV_NOT_RUNNABLE,进入等待状态。从某种意义上它更像是一个后台程序,在其他进程发出文件系统请求后才被唤醒并开始服务。
二、实验难点
本次实验代码量较大,难度也体现在了阅读课程组提供的代码上,我阅读了主要代码部分,并对各个函数的功能做了理解与记录:
用户进程
fd.c
-
dev_lookup(int dev_id, struct Dev **dev):查找id对应的dev
-
fd_alloc(struct Fd **fd):遍历寻找一个未使用的fd
-
fd_close(struct Fd *fd):调用syscall_mem_unmap解除fd对应地址所在页的映射
-
fd_lookup(int fdnum, struct Fd **fd):根据fdnum查找对应的fd
-
fd2data(struct Fd *fd):返回fd对应的存储数据的首地址
-
fd2num(struct Fd *fd):返回fd对应的fdnum
-
num2fd(int fd):返回fdnum对应的fd的地址(实名吐槽参数命名为fd)
-
close(int fdnum):根据fdnum查找到相应的fd,调用dev对应的close函数关闭文件描述符,之后调用fd_close解除映射
-
close_all(void):对所有fd调用close函数关闭
-
dup(int oldfdnum, int newfdnum):调用syscall_mem_map将旧文件复制到新文件
-
read(int fdnum, void *buf, u_int n):
-
调用fd_lookup查找fdnum对应的fd
-
调用dev_lookup查找dev_id对应的dev
-
调用dev_read从offset处读取n个字节到buf,更新offset
-
buf以'\0'结尾
-
-
readn(int fdnum, void *buf, u_int n):反着读n个字节到进buf
-
write(int fdnum, const void *buf, u_int n):
-
调用fd_lookup查找fdnum对应的fd
-
调用dev_lookup查找dev_id对应的dev
-
调用dev_write将buf中的n个字节写到offset位置,更新offset
-
-
seek(int fdnum, u_int offset):根据fdnum查找到相应的fd,设置offset
file.c
-
open(const char *path, int mode):
-
调用fd_alloc分配出一个空闲的fd,用于记录即将打开的文件
-
调用fsipc_open打开path路径上的文件(由serve进程将file信息加载到fd上)
-
调用fd2data获取fd对应的用于存储数据的首地址
-
for循环遍历,调用syscall_mem_alloc以va起始开辟存储空间,调用fsipc_map(由serve进程将硬盘上的文件数据读取到va位置)
-
返回文件对应的fdnum
-
-
file_close(struct Fd *fd):
-
调用fd2data获得fd对应的存储数据的首地址
-
调用fsipc_dirty由serve进程将文件数据对应页标记dirty
-
调用fsipc_close由serve进程关闭文件
-
将数据页面解除映射
-
-
file_read(struct Fd *fd, void *buf, u_int n, u_int offset):从fd对应的offset位置读取n个字节到buf
-
read_map(int fdnum, u_int offset, void **blk):找到fdnum对应的fd中offset偏移位置的虚拟地址,并传递给blk
-
file_write(struct Fd *fd, const void *buf, u_int n, u_int offset):从buf中写n个字节到fd的offset偏移位置,如果文件大小不够,调用ftruncate函数扩容
-
ftruncate(int fdnum, u_int size):
-
调用fsipc_set_size由serve进程设置新的文件大小
-
若增加文件大小,调用fsipc_map由serve进程将新增页面映射到va起始的相应地址
-
若减小文件大小,调用syscall_mem_unmap解除多余页面的映射
-
-
remove(const char *path):调用fsipc_remove由serve进程删除文件
fsipc.c
-
fsipc(u_int type, void *fsreq, u_int dstva, u_int *perm):进程通信
-
将请求发送给serve进程(传递了fsreq),serve进程拿到了之后开始按类别处理
-
设置用户进程接收信息的地址,允许接收,此时接收serve处理后传回的信息
-
-
fsipc_open(const char *path, u_int omode, struct Fd *fd):将打开文件的路径、打开方式通过“工具人”页面传递给serve进程,请求服务
-
fsipc_map(u_int fileid, u_int offset, u_int dstva):将需要映射的文件id、偏移offset通过“工具人”页面传递给serve进程,请求服务
-
fsipc_set_size(u_int fileid, u_int size):将文件id、需要设置的文件大小通过“工具人”页面传递给serve进程,请求服务
-
fsipc_close(u_int fileid):将需要关闭的文件id通过“工具人”页面传递给serve进程,请求服务
-
fsipc_dirty(u_int fileid, u_int offset):将文件id、需要设置的文件大小通过“工具人”页面传递给serve进程,请求服务
-
fsipc_remove(const char *path):将需要删除的文件路径通过“工具人”页面传递给serve进程,请求服务
fs进程
fs.c
-
file_dirty(struct File *f, u_int offset):调用file_get_block获得offset处对应的block,写入原有内容使其标记为dirty
-
dir_lookup(struct File *dir, char *name, struct File **file):从dir目录下找到名为name的文件
-
dir_alloc_file(struct File *dir, struct File **file):在dir目录下开辟一个新的file结构体
-
skip_slash(char *p):跳过'/'
-
walk_path(char *path, struct File **pdir, struct File **pfile, char *lastelem):沿路径path查找文件,若存在,存到pfile中,若不存在,将不存在的部分存入lastelem中
-
file_open(char *path, struct File **file):沿路径找到这一文件
-
file_create(char *path, struct File **file):在path路径下创建文件,调用dir_alloc_file开辟新的文件空间,将文件名称赋给新开辟的文件
-
file_truncate(struct File *f, u_int newsize):截断文件大小,若大小小于NDIRECT,顺便将f_indirect清空。调用file_clear_block释放数据块(在位图里置位1)
-
file_set_size(struct File *f, u_int newsize):设置file的新大小,若大小变小,调用file_truncate截断,之后将文件写回磁盘
-
file_flush(struct File *f):将文件中的内容写回到磁盘中
-
file_close(struct File *f):关闭文件,将数据写回(包括文件和文件所在目录)
-
file_remove(char *path):walk_path找到路径上的文件,调用file_truncate将文件大小设为0,将文件和目录内容写回磁盘
serv.c
-
serve_init(void):初始化opentab
-
open_alloc(struct Open **o):找一块新的空间存储Open结构体(分配一个新的页面)
-
open_lookup(u_int envid, u_int fileid, struct Open **po):查找fileid对应的open文件
-
serve_open(u_int envid, struct Fsreq_open *rq):
-
从rq中回去请求信息
-
调用open_alloc分配打开文件
-
调用file_open按路径打开文件
-
将文件信息传递给打开文件
-
将打开文件通过进程通信传递给用户进程
-
-
serve_map(u_int envid, struct Fsreq_map *rq):
-
调用opne_lookup查找打开的文件
-
调用file_get_block查找文件对应的数据块
-
通过进程通信传递这一数据块
-
-
serve_set_size(u_int envid, struct Fsreq_set_size *rq):
-
调用opne_lookup查找打开的文件
-
调用file_set_size改变文件大小
-
-
serve_close(u_int envid, struct Fsreq_close *rq):
-
调用opne_lookup查找打开的文件
-
调用file_close关闭打开的文件
-
-
serve_remove(u_int envid, struct Fsreq_remove *rq):调用file_remove根据path删除文件
-
serve_dirty(u_int envid, struct Fsreq_dirty *rq):
-
调用opne_lookup查找打开的文件
-
调用file_dirty将文件占用的页面标记dirty
-
-
serve(void):
-
调用ipc_recv从用户进程接收请求
-
根据req类型调用不同的serve函数
-
系统调用接触REQVA地址的映射
-
用户进程和文件系统的逻辑
以open函数的调用为例:
三、体会与感想
本次lab看上去似乎并不是很难,当然代码量还是有些大,需要认真阅读,理解调用逻辑。总花费时间约30小时,看上去各个lab用时还是很平均的。在lab4发现GitHub上有往届的测试代码后,我仿佛发现了新世界,于是每次课上测试之前都试图通过往届代码获取一些提示,但事实上lab5的两次上机题目都和往届有很大的差异,不得不说课上看到题的时候还是很懵的,但万幸过了exam?有惊无险。
lab5-2课上的时候似乎全程在模仿课程组提供的open逻辑,可能也是因为紧张,对每一步都模棱两可的,所以似乎更不如说是靠运气过了。现在想来虽然课下有尝试讲出用户进程请求文件系统服务的逻辑链,但具体细节却没有注意到(比如通过ipc通信传递异常值),只有到课上测试卡bug了才暴露出来,纸上谈兵啊。
OS上机的漫漫长路过去了,然而整个过程还是充满了忐忑,4次extra,挂了两次lab,惊险收支平衡,看来还是没有很好地掌握很多内容的运行逻辑,需要更好地理解,做到举一反三。
四、残留难点
本次lab暂无残留难点。