BUAA OS Lab5-2分析
BUAA OS Lab5-2分析
前言
说实话,当我做完Lab5-1,我以为Lab5仅仅如此,后面又看到Lab5-2的指导书部分内容比较少,使我更坚信了Lab5是一个轻松的Lab,然而当我读完指导书,对后面几个exercise无从入手时,我才知道我错了。。。
Lab5的代码填写量非常少,但是需要自己阅读数量巨大的代码,同时还要理解各个文件的关系。我尝试着梳理了一下Lab5中几个文件和主要函数的调用关系,尽量从一个比较宏观的视角来看待整个文件系统,希望能够对大家理解Lab5有帮助。
当然,其中很多内容仅仅是基于我自己的理解,可能有错误之处,希望能够谅解并指正!
一、总览
我们实现的文件系统主要由三部分组成:用户进程部分user、服务进程部分serv、底层部分fs。这些部分之间具有明显的抽象层次,每一部分都是对较低一个部分的抽象和封装。其中,用户进程部分是最高级的抽象。每个部分由于其抽象程度不同,文件的抽象形式也不同,因此需要比较清楚地理解文件在各个部分的抽象形式。我们从高级抽象一步步向下展开。
(一)用户进程 user
用户进程部分封装了一系列常见的操作文件的接口(user/fd.c 和部分user/file.c),如open、close、read、write、seek等,这些是最高级的抽象。实际上,这些操作的对象都是文件描述符,因为在用户进程中,文件的抽象形式就是文件描述符Fd。这些顶级抽象接口需要依赖低级一些的抽象(user/file.c),因为他们会调到对应的文件控制符Fd中的dev_id的dev的对应函数,在文件系统中也就是file_close、file_write等等。
需要注意的是,该部分文件的抽象形式就是文件描述符Fd(user的文件描述符Fd来源于serv通过ipc的页面共享ffd),因此内存空间中有专门的地址存放文件描述符,也就是FDTABLE,这里可以看成一个数组,每个Fd占一个Page大小,可以直接用fdnum作为下标获得;此外,文件的具体数据存在FILEBASE部分的内存空间,每个文件的数据最多占PDMAP大小,也可以实现fdnum到文件数据的一一映射(FILEBASE + PDMAP * fdnum)。
struct Fd {
u_int fd_dev_id;
u_int fd_offset; // 定位指针
u_int fd_omode; // 打开形式
};
struct Dev devfile = {
.dev_id = 'f',
.dev_name = "file",
.dev_read = file_read,
.dev_write = file_write,
.dev_close = file_close,
.dev_stat = file_stat,
};
而这些file_xxx中,file_open和file_close需要涉及到和文件服务进程的交互。因为当打开一个文件时,需要从服务进程中读取相应的文件描述符到用户空间,此时要调用fsipc_open;而当file_close时,需要fsipc_close来写回服务进程和磁盘。当然在其他的file_xxx,如file_remove,file_sync中,由于这些函数直接涉及了文件内容的修改,因此需要写回,从而同样要调用fsipc_xxx来与服务进程交互。
那么,fsipc_xxx又干什么呢?fsipc_xxx其实就是找到相应的文件描述符Fd(毕竟这时还在用户进程中),然后构造相应类别的请求req,再通过ipc将该req发送给服务进程(其实就是把req的页面共享给服务进程),最后通过ipc_recv从服务进程中收到相应的结果,以及一些额外的页面映射,如对于fsipc_open,会将服务进程中的文件描述符共享映射给用户空间的文件描述符;fsipc_map则会将服务进程中相应的Block映射给用户进程。
此外,虽然文件的抽象形式是Fd,但是在与serve交互时,会把整个Filefd映射过来,因此可以直接用一个Filefd的指针指过去,从而获取更多Filefd的信息。
(二)服务进程 serve
首先我们需要明确,文件在服务进程抽象存在形式是Open,其中有一个Filefd的指针,也存储了文件的相关信息。而在fs/serv.c中,有一个Open的数组opentable,这正是服务进程中的关键。
其次需要明确,文件的数据在服务进程中是以块缓存的形式存储的(也就是Block和va的映射)。
struct Open {
struct File *o_file; // 对应的文件控制块
u_int o_fileid; // 文件id(可以看做当前打开的文件,每个都有个id)
int o_mode; // 打开形式
struct Filefd *o_ff; // Filefd
};
struct Filefd {
struct Fd f_fd; // 文件描述符
u_int f_fileid; // 文件id(同上)
struct File f_file; // 文件控制块
};
struct Fd {
u_int fd_dev_id;
u_int fd_offset; // 定位指针
u_int fd_omode; // 打开形式
};
服务进程中除了对Open最基本的操作,如open_alloc和open_lookup外,最主要的部分就是各个serve_xxx函数。这些正是接收到用户进程请求时需要进行的操作。serve_xxx首先是从请求req中提取有关信息,如fileid等,从而找到对应的Open(如果是serve_open则是分配Open)。找到对应的Open,就相当于找到了服务进程中对应的文件,这时候就可以调用底层的函数(fs/fs.c)中来把相应的Block从磁盘读入服务进程的内存空间中(也就是上图的内存分布)或对相应的Block进行操作。
(三)底层部分 fs
上面我们提到,服务进程需要通过调用底层函数来操作文件对应的Block。而在fs/fs.c中,也就是本文说的底层部分,这里封装了许多可以直接根据文件修改对应的Block的函数,如file_get_block、file_flush、file_set_size等,这些使得我们可以很方便地迅速获得文件的某一Block,或直接对该文件的Block进行操作,以及和磁盘的交互(如磁盘的读取与回写)。
在这一部分,文件的抽象形式是文件控制块File,文件数据则是存在Block中。
需要注意的是,这部分其实也属于文件系统的服务进程,只是在抽象层面上划分开了,因此这部分实现的Block的读入等同样修改的是服务进程的内存空间。
二、函数关系
lab5涉及到的函数数量巨大,这里只给出部分主要函数之间的调用关系以及对应的功能。从整个文件系统的更高层面上看待这些函数之间的协作、封装,可以避免纠结于某个文件或者某个函数。或许先从宏观上了解了整个结构,再去了解具体的细节实现,能够更好地理解文件系统。
当然,如果要深入理解每个函数的功能,还是需要好好读代码的(尽管一下子读这么多代码确实挺恶心的)
该图的结构参考了博客:https://hyggge.github.io/2022/05/30/OS-Lab5函数解读/
其中,箭头上的注释主要表示调用这个函数的主要作用是什么,或许可以对理解函数的功能起到一定帮助。
三、其他
这里整理了一些可能比较难理解的点(慢慢补充)
- 在user空间中,为什么一个Fd占一个Page大小:因为serv在通过ipc给user共享映射Fd时只能映射一整个Page
- 在fsipc_map通过ipc共享页面的时候,共享的是Block还是Page:既是Block也是Page,因为一个Block的大小和一个Page的大小一样(BY2BLK = BY2PG = 4KB)
- user在用户空间修改了文件内容比如write,serv和磁盘怎么同步:文件数据是user和serv共享映射的(在user/file.c的open函数中就通过fsipc_map共享了),自然serv能同步;serv是一个缓存,flush时再写回磁盘
- user中文件内容在哪:在FILEBASE段,每个文件有PDMAP的空间,同上,是和serv共享映射的