思考题

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
#define FILE2BLK (BY2BLK / sizeof(struct File))

通过在测试程序中编写语句:

1
debugf("FILE2BLK is: %d \n", FILE2BLK);

输出得到:FILE2BLK is: 16 ,也就是说,一个磁盘块最多含有16个文件控制块。

其实观察宏定义也不难得出一个文件控制块的大小:#define BY2FILE 256,而File结构体是这样定义的:

1
2
3
4
5
6
7
8
9
10
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[BY2FILE - MAXNAMELEN - (3 + NDIRECT) * 4 - sizeof(void *)];
} __attribute__((aligned(4), packed));

可见该结构体被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
2
3
4
5
6
7
8
9
10
int r, fdnum, n;
char buf[100];
fdnum = open("/newmotd", O_RDWR);
if((r = fork()) == 0){ //子进程
n = read(fdnum, buf, 5);
debugf("The buffer of child is \"%s\"\n", buf);
} else {
n = read(fdnum, buf, 5);
debugf("The buffer of father is \"%s\"\n", buf);
}

而文件/newmotd内容为:

1
This is a NEW message of the day!

运行输出结果如下:

1
2
The buffer of father is "This "
The buffer of child is "is a "

可见fork前后的父子进程共享了文件描述符中的fd_offset定位指针,从而可以证明父子进程共享了文件描述符和文件定位指针。

Thinking 5.6

请解释 File, Fd, Filefd 结构体及其各个域的作用。比如各个结构体会在哪些过程中被使用,是否对应磁盘上的物理实体还是单纯的内存数据等。说明形式自定,要求简洁明了,可大致勾勒出文件系统数据结构与物理实体的对应关系与设计框架。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
struct File { // 既是磁盘上的物理实体,又是文件系统服务进程中的内存数据
char f_name[MAXNAMELEN]; // 文件名
uint32_t f_size; // 文件/目录占据存储空间的大小
uint32_t f_type; // 类型:{文件 or 目录}
uint32_t f_direct[NDIRECT]; // 直接指向包含文件内容的磁盘块的10个直接指针
uint32_t f_indirect; // 指向存储指向其它文件内容磁盘块的磁盘块的间接指针

struct File *f_dir; // 指向该文件所属目录的指针, 只在内存中有效.
char f_pad[BY2FILE - MAXNAMELEN - (3 + NDIRECT) * 4 - sizeof(void *)]; // 用于保证该结构体是256字节对齐的pad字段
} __attribute__((aligned(4), packed));


// file descriptor
struct Fd { // 内存数据
u_int fd_dev_id; // 标识外设类型的字段,在语义上和`Dev`结构体中的`dev_id`字段是等价的
u_int fd_offset; // 指示文件读写偏移量的字段,指示当前读写到哪里的一个“指针”,可以被`seek`,`read`,`write`等函数修改
u_int fd_omode; // 表示文件打开的操作权限,查阅代码可知具体有取值集合{O_RDWR,O_WRONLY,O_RDONLY}三种分别对应读写,只读,只写
};

// file descriptor + file 实际上是对`Fd`指针的另一种解释形式
struct Filefd { // 内存数据
struct Fd f_fd;
u_int f_fileid; // 指文件本身
struct File f_file; // 指文件id
};

Thinking 5.7

图5.7中有多种不同形式的箭头,请解释这些不同箭头的差别,并思考我们的操作系统是如何实现对应类型的进程间通信的。

单就箭头形状而言,图5.7中有两种箭头,分别是实心箭头和虚线箭头,查阅资料可知实心箭头代表同步消息而虚线箭头则代表返回消息

站在更进一步的角度考虑,从init进程生命线出发的两个ENV_CREAT箭头代表了init进程对fs_servuser_env进程的创建,而IPC框内的两个箭头则代表了用户进程和文件系统进程之间的交互,即用户进程的请求与文件系统对请求的响应

我们的操作系统设计进程间通信主要涉及到如下文件:user/lib/file.cuser/lib/fsipc.cuser/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的文件修改量就可以看出来这次的复杂度上升的很陡峭。

心得体会

  • 应该是几次实验里面需要阅读的代码量最大的一次了,引入了诸如文件描述符,磁盘块等一系列概念,需要认真阅读下发的代码头文件以及注释,结合指导书进行理解。
  • 对于用户进程向文件系统发送请求以及文件系统响应请求的过程,除了认真阅读代码外,画出大致的草图也可以帮助我们对这个过程的调用链进行理解。