OS-Lab5实验报告
思考题
Thinking 5.1
如果通过kseg0 读写设备,那么对于设备的写入会缓存到Cache 中。这是一种错误的行为,在实际编写代码的时候这么做会引发不可预知的问题。请思考:这么做这会引发什么问题?对于不同种类的设备(如我们提到的串口设备和IDE 磁盘)的操作会有差异吗?可以从缓存的性质和缓存更新的策略来考虑。
如果使用cache,数据的读写很有可能不会及时同步到外设,可能会出现数据永远不会更新、外设数据出现变化却永远读不到新数据等情况。
Thinking 5.2
查找代码中的相关定义,试回答一个磁盘块中最多能存储多少个文件控制块?一个目录下最多能有多少个文件?我们的文件系统支持的单个文件最大为多大?
1024 1024 4MB
Thinking 5.3
请思考,在满足磁盘块缓存的设计的前提下,我们实验使用的内核支持的最大磁盘大小是多少?
1GB
Thinking 5.4
在本实验中,fs/serv.h、user/include/fs.h 等文件中出现了许多宏定义,试列举你认为较为重要的宏定义,同时进行解释,并描述其主要应用之处。
//user/include/fs.h
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;
struct File *f_dir; // the pointer to the dir where this file is in, valid only in memory.
char f_pad[FILE_STRUCT_SIZE - MAXNAMELEN - (3 + NDIRECT) * 4 - sizeof(void *)];
} __attribute__((aligned(4), packed));文件控制块(File结构体)。该结构体是文件系统操作文件的基本单位。
// user/include/fd.h
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;
};fd结构体是文件描述符的基本结构,是暴露给用户、用于索引文件的核心结构体。且通过指定fd_dev_id,并对文件、管道、控制台分别定义结构体,并包含该fd结构体,实现了面向对象中继承的思想,并在后续操作中通过判断fd_dev_id可以实现父类Fd和子类Filefd等的转换,即等价于面向对象的继承和多态。
#define FS_MAGIC 0x68286097 // Everyone's favorite OS class
小彩蛋,源自MIT6.828和MIT6.097(也就是MIT的OS课程代码和2002年以前的旧代码),根据学长的断代工程,可以看到这行代码在可考证的历史中最早来自2003年的JOS源代码,尽管在2006年的JOS中该魔数已经被替换成0x4A0530AE(注释为related vaguely to ‘J\0S!’),但由于MOS是参考了老版本的JOS,因此这行代码也一直被保留了下来,在近二十年历届学长的维护和修缮工作中仍然完整的传承了下来。这样一份历史悠久的大工程代码中,总能在某些地方找到前人的痕迹(当然也可能是没人敢动的屎山),或许这种超时空对话也是计算机的浪漫之处吧。
Thinking 5.5
在Lab4 “系统调用与fork”的实验中我们实现了极为重要的fork 函数。那么fork 前后的父子进程是否会共享文件描述符和定位指针呢?请在完成上述练习的基础上编写一个程序进行验证。
会,因为文件描述符存储在[FDTABLE, FILEBASE)空间中,位于USTACKTOP下,fork时duppage()函数会将这部分页表项进行拷贝。
int main(){
int fd;
int r;
char buf[1000];
fd = open("/forkTest", 0);
if((r = fork()) < 0){
user_panic("Error with fork");
}else if(r == 0){
if((r = read(fd, buf, 30)) != 30){
user_panic("Error with child read forkTest");
}
debugf("child read is good with \"%s\"\n", buf);
}else{
if((r = read(fd, buf, 28)) != 28){
user_panic("Error with father read forkTest");
}
debugf("father read is good with \"%s\"\n", buf);
}
return 0;
}
// in forkTest file
we share the file descriptor and the file offset pointer.
//output
child read is good with "we share the file descriptor "
father read is good with "and the file offset pointer."Thinking 5.6
请解释File, Fd, Filefd 结构体及其各个域的作用。比如各个结构体会在哪些过程中被使用,是否对应磁盘上的物理实体还是单纯的内存数据等。说明形式自定,要求简洁明了,可大致勾勒出文件系统数据结构与物理实体的对应关系与设计框架。
// /user/include/fs.h
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; // 间接索引
struct File *f_dir; // 父目录的File结构体
char f_pad[FILE_STRUCT_SIZE - MAXNAMELEN - (3 + NDIRECT) * 4 - sizeof(void *)]; //占位
} __attribute__((aligned(4), packed));
// /user/include/fd.h
struct Fd {
u_int fd_dev_id; // 设备(文件/管道/控制台)
u_int fd_offset; // 偏移
u_int fd_omode; // 打开状态
};
// /user/include/fd.h
// file descriptor + file
struct Filefd {
struct Fd f_fd; // Fd结构体
u_int f_fileid; // id
struct File f_file; // File副本
};File是用于真实与磁盘进行交互的信息,Fd是进程管理文件的核心接口,Filefd类似于Fd的子类,可以认为是文件类型的file descriptor。除去File对应磁盘上的物理实体(被加载到内存中)外,其他完全是内存中的概念。File在file server中用于文件的读写等,而Fd/Filefd则是暴露给用户态的接口。
Thinking 5.7
图5.9 中有多种不同形式的箭头,请解释这些不同箭头的差别,并思考我们的操作系统是如何实现对应类型的进程间通信的。
- 同步消息:黑箭头+黑实线
- 异步消息:开箭头+黑实线
- 返回消息:开箭头+黑虚线
操作系统在init时,首先创建用户进程,其次创建fs进程;fs进程初始化后进入serv()函数,阻塞等待IPC;user进程向fs发送请求,等待fs进程返回后继续工作,fs等待下一次请求。
难点分析
服务调用链
文件服务的调用链极深,这里以一个例子来说明。
对于打开文件操作:
- 用户首先调用user/lib/file.c中的
open()函数 - 该函数在准备好文件描述符指针后调用user/lib/fsipc.c中的
fsipc_open()函数,准备好请求体之后调用fsipc()函数,由该函数统一发送ipc请求。 - fs进程中,fs/serv.c下的
serve()函数负责接收用户传来的请求,并进行请求的分发。 - 分发至
serve_open()函数,该函数调用fs/fs.c中的file_open()函数。 - 该函数负责根据路径找到具体文件的File结构体指针并返回。
serve_open()函数将Open结构体赋值后,将其中的Filefd再次ipc_send()给用户进程。- 用户进程拿到Filefd后,逐级返回至
open()函数,使用拿到的fileid进行fsipc_map()(链路相同)以将文件内容映射到Filefd。 - 最后将该文件描述符的num返回。
实验体会
Lab5中,我们学习了文件系统的基本实现,同时对封装、统一接口设计等理念有了更深刻的理解。
原创说明
本文均为原创,部分内容参考了AI的回答以及TripleCamera、theUHO学长的博客。
