BUAA-OS-Probe-Lab5-Part3
用户态定义与接口
在之前的分析中,我们已经完成了文件管理进程中的函数实现。现在我们来分析用户态中用户直接可用的函数接口与数据结构定义
我们还是从距离文件系统最近的函数与文件开始:user/lib/fsipc.c
和 user/include/fsreq.h
首先明确用户进程和文件管理进程使用 IPC 通信进行交互。在文件管理进程的运行的核心、分发函数 serve
中,我们通过 ipc_recv
从用户进程获取到了一片虚拟地址 REQVA
,并且针对不同的信息,将 REQVA
转换成了不同的数据结构,进行处理。这些数据结构就定义在 fsreq.h
文件中。
|
篇幅所限不再全部排列。
那这片 va
到底是谁发送来的呢?在用户态中存在一个专门的分发函数:fsipc
fsipc.c
- 文件请求服务函数
- 我们规定在 MOS 系统中,文件管理进程必须为第二个进程(
envs[1]
),以保证传输正确性 - 可以看出,本接口通过
fsreq
参数传输页面,type
参数传输申请的服务类型
// Overview: |
在每一个用户态的每一个请求服务函数中,即 fsipc_*
函数,都会用到一个临时的页面用来传输数据:
u_char fsipcbuf[BY2PG] __attribute__((aligned(BY2PG))); |
通过修改页面内的数据,搭配不同的服务函数,让文件管理进程以不同的方式去解析这片地址空间,以实现传输相同页面却能实现不同功能的效果。例如我们要实现的函数 fsipc_remove
:
// Overview: |
实际上每个函数中,对 fsipc_req
这片第地址的类型转换都不同,也会写入不同的信息。
file.c
- 纯文件操作函数
再向上一层,调用这些用户态中的请求服务函数 fsipc_*
的函数位于 user/lib/file.c
中。
它们是用户态中能直接执行特定文件操作的函数。
// Returns the file descriptor, 也就是 Fd 号 |
int remove(const char *path) { |
但我们在用户程序中通常不使用这些函数,具体的原因在下一个文件中说。
fsipc.c & file.c
fd.c
- 文件系统顶层函数
struct Dev
对于整个文件系统而言,总共能操作的设备共有三种:(纯)文件设备、控制台和管道。而我们在 file.c
,甚至 Lab5 之前部分中实现的都是对文件设备的操作,而剩余两个部分是 Lab6 的内容了。
其实本来是没有括号这个“纯”字的,但是个人觉得比较好区分“操作 fd “和”操作 file “两个层次,于是就瞎分了分
上面提到的这三类设备,都是 dev (即 file device
)的一种,我们在 fd.c
的最顶层函数中也是直接对 dev
进行对应的操作,表层并无法分辨“文件”这一概念。所以应该对每个操作涉及到的不同设备类型进行分发,让每个设备对应一个执行任务的函数,再把分发函数封装在执行功能的函数内。
这三类设备平等,并且在文件系统中视角来看,都属于 Fd
,即 file descriptor 的一种(继承?)。每类设备拥有自己的功能函数以处理对应的请求,结构体定义中的 3-7 字段就是存放功能函数的指针。三种设备的信息统一存放在一个 Dev
数组内,设备具体又分别定义在各自功能函数的头文件内。
// fd.h |
struct Fd
其次是文件描述符 Fd
,每一个用户进程可以同时操控多个设备,为了对这些设备加以区分,就引入了文件描述符的定义。通过访问对应设备的文件描述符,就能得知它的设备种类(dev_lookup
),因此我们只需要传入文件描述符,就能自动地获得 Dev
中存放的函数指针并执行。这也许是一种面向对象的处理方式。
同时,每个进程的文件描述符上限为 32 个,每个描述符单独占用一个虚拟页,并且还对应一个 PDMAP
大小的 data 空间处理该设备打开后处理的内容。
// file descriptor |
我们对文件描述符的访问,通过它在空间中的顺序(下标)实现。对外界,文件描述符相当于一个整数,服务函数 fd_lookup
、fd2data
等和宏定义通过“翻译”这个整数,获得文件描述符的地址和它对应的 data
空间。存放的地址如下:
为了我们更容易实现对纯文本的操作,所以又再次对 Fd
结构进行了封装,实现了一个专门为 file 使用的 FileFd
结构,它包含了更多的信息,能够更容易地处理 file 类的操作。
// file descriptor + file |
实际上可以直接把 FileFd
当作普通的 Fd
用,因为字段原因,所以内存上读起来是这样的效果
我们之前提到, Open
结构体类似于一个文件服务进程使用的“窗口”,实际上文件描述符 Fd
也类似一个在用户态中的窗口,只不过它的范围更广,可以包含除了 File
以外的其他设备,但同时它也只为自己的进程而服务。
如何根据 Fd
执行功能函数?
- 根据
Fd
号找到对应的Fd
数据 - 判断其
Dev
种类,获取执行该操作的函数指针 - 直接调用函数指针,实现该
Dev
自行实现的底层函数,完成操作。
我们以 read
一个设备(Fd)为例,分析以上过程
// Overview: |
大致思路如上,所以我们实际上只需要准备好这个 dev_*
函数即可完成功能。
接着以定义在 file.c
中的 devfile
为例,由于 .dev_read = file_read
,所以我们之前编写的读 file 的函数 file_read
就会在顶层的 read
中被直接执行。
struct Dev devfile = { |
终于,我们在用户态直接对某个设备调用顶层的功能函数,就能直接获得响应了。
一言
纵观 Lab5,MOS 在其中为我们构造了一个通配的文件系统。
它通过统一的结构体 Fd
层次化地管理所用可用的设备。向上,在 fd.c
中为用户提供统一的操作函数;向下,不同设备通过 implements 实现 Dev
的功能函数,达到同一调用的效果。
在此之下,file.c
、console.c
、pipe.c
三文件实现了 Dev
中的”抽象函数“。为了优化函数实现的效果,我们建立了一个为所有进程管理纯文件 file 的文件服务进程 serv.c
,并通过 fsipc.c
借助 进程间 IPC 通信机制实现数据传输。
对文件服务进程而言,它通过 IPC 通信与用户进程(请求方)通信,利用文件级、磁盘块级的交互函数 fs.c
与磁盘进行交互,并利用块缓存的机制对磁盘块进行管理,以满足请求者对指定文件的特定文件块的访问。
再细化到最后一步,所有的磁盘交互函数都是利用系统调用访问 KSEG1
段实现与外设的直接交互,并在最底层的函数中实现了以块、甚至扇区为单位的读写,即 ide.c
。