BUAAOS-Lab05实验报告
思考题
Thinking 5.1
如果通过 kseg0 读写设备,那么对于设备的写入会缓存到 Cache 中。这是一种错误的行为,在实际编写代码的时候这么做会引发不可预知的问题。请思考:这么做会引发什么问题?对于不同种类的设备(如我们提到的串口设备和 IDE 磁盘)的操作会有差异吗?可以从缓存的性质和缓存更新的策略来考虑。
对于这个问题,可以分别站在读请求和写请求两个角度来考虑。
对于写请求,如果使用Write-Back策略对Cache进行更新,且通过kseg0段进行设备读写的话,假如说对某一个设备有连续的写入请求,那么在第一次读该设备I/O端口之前,我们的计算机所做的就只是不断地将Cache中映射到I/O端口的’dirty’表项进行更新,而不会真正写入到设备的I/O端口中去。
而对于读请求,以实验中的IDE磁盘为例,在对磁盘进行第n次连续写入的请求后(根据上述推理可知实际上并未进行这个写入操作),然后对上一次操作的状态返回值进行读取,而这时候可能会错误地从设备中返回一个非0值,而我们的写入操作并未实际执行,这样就产生了错误。
Thinking 5.2
查找代码中的相关定义,试回答一个磁盘块中最多能存储多少个文件控制块?一个目录下最多能有多少个文件?我们的文件系统支持的单个文件最大为多大?
查阅代码可知有#define BY2BLK BY2PG
,也就是说一个磁盘块大小和一个物理页大小相等,为4096byte。而代码对一个磁盘块中最多含有的文件控制块数有如下定义:
1 |
通过在测试程序中编写语句:
1 | debugf("FILE2BLK is: %d \n", FILE2BLK); |
输出得到:FILE2BLK is: 16
,也就是说,一个磁盘块最多含有16个文件控制块。
其实观察宏定义也不难得出一个文件控制块的大小:#define BY2FILE 256
,而File
结构体是这样定义的:
1 | struct File { |
可见该结构体被f_pad
填充为了256字节
而一个目录最多能够指向4096/4 = 1024
个磁盘块,也就是说一个目录下最多有1024 * 16 = 16384
个文件。
而我们的文件系统支持的单个文件最大为1024 * 4KB = 4MB
。宏定义里也给出了:#define MAXFILESIZE (NINDIRECT * BY2BLK)
Thinking 5.3
请思考,在满足磁盘块缓存的设计的前提下,我们实验使用的内核支持的最大磁盘大小是多少?
由#define DISKMAX 0x40000000
可知我们实验使用的内核支持的 最大磁盘大小为1GB。
Thinking 5.4
在本实验中,fs/serv.h、user/include/fs.h 等文件中出现了许多宏定义,试列举你认为较为重要的宏定义,同时进行解释,并描述其主要应用之处。
在我看来,这些头文件中的与文件控制块FILE
和磁盘控制块有关的宏较为重要,能帮助我们理解相关概念。
首先,“磁盘块”这个概念是我们对磁盘的抽象,一个磁盘块(block)的字节大小为BY2BLK = 4096byte
(定义在user/include/fs.h中)。
而定义在user/include/fs.h中的File
结构体则是我们所谓的文件控制块,这是操作系统用于存储文件相关信息并对文件进行管理的数据结构。而同文件下的宏FILE2BLK
是用来表示一个磁盘块最多存储的文件控制块数,我们就可以得知在磁盘中,是“文件控制块装在磁盘控制块里”这样一个结构。
Thinking 5.5
在 Lab4“系统调用与 fork”的实验中我们实现了极为重要的 fork 函数。那么 fork 前后的父子进程是否会共享文件描述符和定位指针呢?请在完成上述练习的基础上编写一个程序进行验证。
考虑如下用户态程序:
1 | int r, fdnum, n; |
而文件/newmotd
内容为:
1 | This is a NEW message of the day! |
运行输出结果如下:
1 | The buffer of father is "This " |
可见fork前后的父子进程共享了文件描述符中的fd_offset
定位指针,从而可以证明父子进程共享了文件描述符和文件定位指针。
Thinking 5.6
请解释 File, Fd, Filefd 结构体及其各个域的作用。比如各个结构体会在哪些过程中被使用,是否对应磁盘上的物理实体还是单纯的内存数据等。说明形式自定,要求简洁明了,可大致勾勒出文件系统数据结构与物理实体的对应关系与设计框架。
1 | struct File { // 既是磁盘上的物理实体,又是文件系统服务进程中的内存数据 |
Thinking 5.7
图5.7中有多种不同形式的箭头,请解释这些不同箭头的差别,并思考我们的操作系统是如何实现对应类型的进程间通信的。
单就箭头形状而言,图5.7中有两种箭头,分别是实心箭头和虚线箭头,查阅资料可知实心箭头代表同步消息而虚线箭头则代表返回消息。
站在更进一步的角度考虑,从init
进程生命线出发的两个ENV_CREAT
箭头代表了init
进程对fs_serv
和user_env
进程的创建,而IPC
框内的两个箭头则代表了用户进程和文件系统进程之间的交互,即用户进程的请求与文件系统对请求的响应。
我们的操作系统设计进程间通信主要涉及到如下文件:user/lib/file.c
,user/lib/fsipc.c
,user/lib/serv.c
。
调用基本流程是:首先,用户态程序调用user/lib/file.c
中的如open
等文件操作函数,再由这些函数调用user/lib/fsipc.c
中对应的fsipc_*
函数,这些fsipc_*
再进一步调用fsipc
函数并传入请求的操作类型的类型号,由fsipc
函数使用ipc_*
与文件系统进程进行进程通信从而进行交互。
而对于文件系统服务进程fs_serv
,它在一个for(;;)
的大循环中接收各个进程通过ipc_send
发来的请求并根据传入的类型号,由一个switch-case
结构来确定由哪一个serve_*
函数来处理这个文件操作请求,最终通过ipc_send
向用户进程返回操作结果。
实验难点
- 新增代码量较大,实验练习填写的代码只占新增代码的很小一部分,若想对
mos
的文件系统结构,运作流程以及磁盘的结构有更深一步的理解,需要在完成代码填写的基础上对代码进行整体性的阅读与梳理。 - 在用户进程与文件系统交互这一部分,函数调用链很深,需要跨很多文件去理解交互和调用的过程,其实在lab5-2的exam的文件修改量就可以看出来这次的复杂度上升的很陡峭。
心得体会
- 应该是几次实验里面需要阅读的代码量最大的一次了,引入了诸如文件描述符,磁盘块等一系列概念,需要认真阅读下发的代码头文件以及注释,结合指导书进行理解。
- 对于用户进程向文件系统发送请求以及文件系统响应请求的过程,除了认真阅读代码外,画出大致的草图也可以帮助我们对这个过程的调用链进行理解。