BUAA-OS-Lab5 实验报告

思考题

Thinking 5.1

如果通过 kseg0 读写设备,那么对于设备的写入会缓存到 Cache 中。这是一种错误的行为,在实际编写代码的时候这么做会引发不可预知的问题。请思考:这么做这会引发什么问题?对于不同种类的设备(如我们提到的串口设备和 IDE 磁盘)的操作会有差异吗?可以从缓存的性质和缓存更新的策略来考虑。

当外部设备产生中断信号或者更新数据时,此时Cache中之前旧的数据可能刚完成缓存,那么完成缓存的这一部分无法完成更新,则会发生错误的行为。
缓存机制的设计是为了提高效率,让数据发生改变的时候不立即写入内存,而是等Cache发生替换的时候才写进去。
而对于串口设备这种读写频繁、实时交互的设备来说,如果写入 kseg0 部分,数据可能很久都不被真正写入内存中,引发错误。在相同的时间内,发生读写错误的概率远高于IDE磁盘。

Thinking 5.2

查找代码中的相关定义,试回答一个磁盘块中最多能存储多少个文件控制块?一个目录下最多能有多少个文件?我们的文件系统支持的单个文件最大为多大?

  • 一个磁盘块的大小为4KB,一个文件控制块的大小为256B,一个磁盘块中最多有16个文件控制块。
  • 一个目录大小为4KB,所以一个目录下最多有1024个目录项,也就是最多可以指向1024个磁盘块。每一个磁盘块有最多有16个文件控制块,所以一个目录最多有16K个文件。
  • 一个文件有直接指针+间接指针共1024个。每个指针指向一个磁盘块,存储着该文件的一部分文件数据。所以文件系统支持的单个文件最大为4MB

Thinking 5.3

请思考,在满足磁盘块缓存的设计的前提下,我们实验使用的内核支持的最大磁盘大小是多少?

1
#define DISKMAX 0x40000000

由代码可知最大磁盘大小为1GB。

Thinking 5.4

在本实验中, fs/serv.h、 user/include/fs.h 等文件中出现了许多宏定义,试列举你认为较为重要的宏定义,同时进行解释,并描述其主要应用之处。

1
2
#define INDEX2FD(i) (FDTABLE + (i)*BY2PG)
#define INDEX2DATA(i) (FILEBASE + (i)*PDMAP)

user/include/fs.h里面,这两个宏分别用来找fd对应的文件信息页面文件的缓存区地址

1
#define SECT2BLK (BLOCK_SIZE / SECT_SIZE) /* sectors to a block */

可以实现 sect 号到 block 号的转换。

Thinking 5.5

在 Lab4“系统调用与 fork”的实验中我们实现了极为重要的 fork 函数。那么 fork 前后的父子进程是否会共享文件描述符和定位指针呢?请在完成上述练习的基础上编写一个程序进行验证。

会共享

1
2
3
4
5
6
7
8
fd = open("/testfork", O_RDWR);
if((r = fork()) == 0) { //子进程
n = read(fd, buf, 3);
debugf("child : %s\n", buf);
} else { //父进程
n = read(fd, buf, 3);
debugf("father : %s\n", buf);
}

文件/testfork种的内容为abcdef

而输出为:

1
2
child : abc
father : def

所以文件指针和文件描述符是会共享的

Thinking 5.5

请解释 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
// File结构体表示一个文件或目录的元数据,既存在于磁盘上的物理结构,也存在于文件系统服务进程的内存中。
struct File {
char f_name[MAXNAMELEN]; // 文件名或目录名
uint32_t f_size; // 文件或目录占据的存储空间大小,以字节为单位
uint32_t f_type; // 文件的类型标识,区分文件和目录
uint32_t f_direct[NDIRECT]; // 直接指针数组,指向包含文件内容的磁盘块地址
uint32_t f_indirect; // 间接指针,指向一个磁盘块,该磁盘块存储了指向文件内容的其他磁盘块地址

struct File *f_dir; // 指向该文件所属目录的指针,此字段仅在内存中表示有效
char f_pad[BY2FILE - MAXNAMELEN - (3 + NDIRECT) * 4 - sizeof(void *)]; // 用于字节对齐的填充字段,确保结构体大小为256字节
} __attribute__((aligned(4), packed));

// Fd结构体代表一个文件描述符,它存储在内存中,包含了关于文件操作的上下文信息。
struct Fd {
u_int fd_dev_id; // 设备标识符,用于识别文件所属的外设类型
u_int fd_offset; // 文件偏移量,表示当前文件操作的读写位置
u_int fd_omode; // 文件打开模式,指定了文件的操作权限,如读写、只读、只写等
};

// Filefd结构体结合了文件描述符和文件元数据,提供了对文件操作的完整上下文。
struct Filefd {
struct Fd f_fd; // 文件描述符,包含设备ID、偏移量和打开模式
u_int f_fileid; // 文件标识符,用于唯一标识一个文件
struct File f_file; // 文件元数据,包含了文件名、大小、类型等属性
};

Thinking 5.6

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

在图5.7中,我们可以观察到两种不同类型的箭头,分别是实心箭头和虚线箭头。实心箭头用于表示同步消息,而虚线箭头则表示返回消息。

init 进程创建了fsuser两个进程。用户进程和文件系统之间的箭头表明二者之间存在交互,用户进程发出需求,文件系统实现。

通信的基本流程如下:首先,用户空间调用user/lib/file.c中提供的文件操作函数,如open。这些函数随后调用user/lib/fsipc.c的函数。传递所需的操作类型码,并进行ipc_send,向文件系统发送请求,并用ipc_rcv来接收结果。

文件系统接收到请求后,使用serv.c中的函数来处理请求。处理结束后,再次调用ipc_send把结果传给用户进程,用户进程继续运行。

实验难点

本次实验中需要填补的内容并不是很多,难度并不算大,但是MOS的文件系统本身比较复杂,里面错综复杂的调用关系需要理清楚才行。文件系统中有很多非常长的文件,这些文件本身内部的函数比较多比较复杂,需要对里面的内容进行一个比较清楚的把握。

这些函数调用其实都是实现文件系统的一系列操作,本质上都是一样的,只不过调用是一层一层向里面调用的。

实验体会

Lab5的上机倒是挺煎熬的。exam因为理解的不够到位,导致卡了好久,最后在助教的帮助下才过了,extra简单的看了看,很复杂,不过有一个整体的思路,里面还有很多很多细节。

OS的上机到这就结束了。这种上机节奏感觉还是很不错的,课下的任务给足了提示和解释,简单浏览代码就可以完成;上机要求对代码的整体架构有一个清晰的把握,需要稍微深入地阅读有关的代码。这种方式其实挺好,确保了我们每周都会花一定的时间来浏览整个代码,所以最后对MOS的内部逻辑还是了解的挺好的。