lab5-report

lab5-report

Charles Lv7

lab5_report

lab_thinkings

Thinking 5.1

question

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

answer

当外部设备自身更新数据时,如果此时CPU写入外设的数据还只在缓存中,则缓存的那部分数据就只能在外设自身更新后再写入外设(只有缓存块将要被新进入的数据取代时,缓存数据才会被写入内存),这样就会发生错误的行为。

串口设备读写频繁,而IDE磁盘读写频率相对较小,因此串口设备发生错误的概率要远大于IDE磁盘。

Thinking 5.2

question

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

answer

通过查看文件控制块的定义,我们可以发现,每个文件控制块都被数组f_pad强制对齐为256B

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));
  • 一个磁盘块的容量为4KB,因此最多可以容纳16个文件控制块。
  • 一个目录文件最多可以使用1024个磁盘块存储数据,因此一个目录下最多1024*16 = 16384个文件。
  • 一个文件最多可以使用1024个磁盘块存储数据,因此一个文件最大容量为1024*4KB = 4MB。

Thinking 5.3

question

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

answer

块缓存所在的地址空间为[0x10000000, 0x50000000),因此我们的内核能够支持的磁盘大小为0x40000000,也就是1GB。

Thinking 5.4

question

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

answer

文件控制块(File)定位文件数据的方式比较重要,需要特别关注。每个文件的数据分布在不相邻的一系列磁盘块中,我们可以将其称为文件数据块,每个文件对应的文件控制块里都记录了所有文件数据块的指针(即下标)。文件控制块有10个直接指针,保存在数组f_direct[10]中,另外还有1014个间接指针(前10个保留不用),这些指针储存在f_indirect所指向的磁盘块中。示意图可见”实验难点图示“部分。

Thinking 5.5

question

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

answer

一个进程所有的文件描述符都存储在[FDTABLE, FILEBASE)这一地址空间中。在fork函数执行时,会将这父进程页表中映射一部分地址的页表项拷贝到子进程的页表中,因此fork前后的父子进程会共享文件描述符和定位指针。

Thinking 5.6

question

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

answer

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));

Fd结构体定义及各个域的作用如下所示

1
2
3
4
5
6
// file descriptor
struct Fd {
u_int fd_dev_id;
u_int fd_offset;
u_int fd_omode;
};

该结构体主要用于记录已打开文件的状态,便于用户直接使用文件描述符对文件进行操作、申请服务等等。由于文件描述符主要是为用户所使用,因此它对应的是磁盘映射到内存中的数据。

Filefd结构体定义及各个域的作用如下所示

1
2
3
4
5
6
// file descriptor + file
struct Filefd {
struct Fd f_fd;
u_int f_fileid;
struct File f_file;
};

文件描述符中存储的数据毕竟是有限的,有的时候我们需要将Fd*强制转换为Filefd*从而获取到文件控制块,从而获得更多文件信息,比如文件大小等等。

Thinking 5.7

question

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

image-20230426214400422

answer

我们的操作系统是通过IPC来实现进程间通信的,这种方式传递的信息本质上是一种同步消息。具体流程是:发送方先调用ipc_send函数,该函数通过一个死循环来不断向接收方信息。当接收方成功接收到消息时,ipc_send函数跳出循环并结束,这时发送方再调用ipc_recv函数主动放弃CPU,等待接收返回信息。

整个流程在函数fsipc里得到体现

1
2
3
4
5
6
static int fsipc(u_int type, void *fsreq, void *dstva, u_int *perm) {
u_int whom;
// Our file system server must be the 2nd env.
ipc_send(envs[1].env_id, type, fsreq, PTE_D);
return ipc_recv(&whom, dstva, perm);
}

lab_difficulties

  • tools 目录中存放的是构建时辅助工具的代码。在之前的 Lab 中,我们实现了解析 ELF文件的 readelf 工具; 本 Lab 中,我们将在其中实现 fsformat 工具,并借助它来创建磁盘镜像。请注意,tools 目录下的代码仅用于 MOS 的构建,在宿主 Linux 环境(而非MIPS 模拟器)中运行,也不会被编译进 MOS 的内核、用户库或用户程序中。

  • fs 目录中存放的是文件系统服务进程的代码。我们在 fs.c 中实现文件系统的基本功能函数,在 ide.c 中通过系统调用与磁盘镜像进行交互。该进程的主干函数在 serv.c 中,通过 IPC 通信与用户进程 user/lib/fsipc.c 内的通信函数进行交互。

  • user/lib 目录下存放了用户程序的库函数。在本 Lab 中,该目录下的 fsipc.c 实现了与文件系统服务进程的交互,file.c 实现了文件系统的用户接口;fd.c 中实现了文件描述符,允许用户程序使用统一的接口,抽象地操作磁盘文件系统中的文件,以及控制台和管道等虚拟的文件。

    image-20230427210837592

image-20230427211359029

文件系统的基本概念和作用

**计算机文件系统是一种存储和组织数据的方法,便于访问和查找数据。**文件系统使用文件和树型目录的逻辑抽象屏蔽了底层硬盘和光盘等物理设备基于数据块进行存储和访问的复杂性。用户不必关心数据实际保存在硬盘(或者光盘)的哪个数据块上,只需要记住这个文件的所属目录和文件名。在写入新数据之前,用户不必关心硬盘上的哪个块是空闲的,硬盘上的存储空间管理(分配和释放)由文件系统自动完成,用户只需要记住数据被写入到了哪个文件中即可。

image-20230427092623468

普通磁盘的基本结构和读写方式

扇区 (Sector) 是磁盘读写的基本单位,GXemul 也提供了对扇区进行操作的基本方法。对于 GXemul 提供的模拟 IDE 磁盘(Simulated IDE disk),我们可以把它当作真实的磁盘去读写数据,通过读写特定位置实现数据的读写以及查看读写是否成功。

磁盘的物理结构

下面简单介绍与磁盘相关的基本知识,首先是几个基本概念:

  1. 扇区 (sector): 磁盘盘片被划分成很多扇形的区域,这些区域叫做扇区。扇区是磁盘执行读写操作的单位,一般是 512 字节。扇区的大小是一个磁盘的硬件属性。

  2. 磁道 (track): 盘片上以盘片中心为圆心,不同半径的同心圆。

  3. 柱面 (cylinder): 硬盘中,不同盘片相同半径的磁道所组成的圆柱面。

  4. 磁头 (head): 每个磁盘有两个面,每个面都有一个磁头。当对磁盘进行读写操作时,磁头在盘片上快速移动。

    image-20230427204915911

IDE 磁盘操作

image-20230427205150286

实现设备驱动的方法

IDE 磁盘驱动

实验中使用的 MIPS 体系结构并没有复杂的 I/O 端口的概念,而是统一使用内存映射 I/O的模型。在 MIPS 的内核地址空间中(kseg0 和 kseg1 段)实现了硬件级别的物理地址和内核虚拟地址的转换机制,其中,对 kseg1 段地址的读写不经过 MMU 映射,且不使用高速缓存,这正是外部设备驱动所需要的。由于我们是在模拟器上运行操作系统,I/O 设备的物理地址是完全固定的,因此我们可以通过简单地读写某些固定的内核虚拟地址来实现驱动程序的功能。

而在本次实验中,我们需要编写的 IDE 磁盘驱动程序位于用户空间,用户态进程若是直接读写内核虚拟地址将会由处理器引发一个地址错误(ADEL/S)。所以对于设备的读写必须通过系统调用来实现。这里我们引入了 sys_write_dev 和 sys_read_dev 两个系统调用来实现设备的读写操作。这两个系统调用以用户虚拟地址、设备的物理地址和读写的长度(按字节计数)作为参数,在内核空间中完成 I/O 操作。

驱动程序编写

当需要从磁盘的指定位置读取一个扇区时,内核驱动会调用 read_sector 函数来将磁盘中对应 sector 的数据读到设备缓冲区中。注意,所有的地址操作都需要将物理地址转换成虚拟地址。

相应地,用户态磁盘驱动使用系统调用代替直接对内存空间的读写,从而完成寄存器配置和数据拷贝等功能。

文件系统服务的基本操作

image-20230427205712855

图中出现的 Block 是磁盘块。不同于扇区,磁盘块是一个虚拟概念,是操作系统与磁盘交互的最小单位;操作系统将相邻的扇区组合在一起,形成磁盘块进行整体操作,减小了因扇区过多带来的寻址困难;磁盘块的大小由操作系统决定,一般由 2 的 n 次方个扇区构成。而扇区是真实存在的,是磁盘读写的基本单位,与操作系统无关。

在文件系统中,我们将使用位图 (Bitmap) 法来管理空闲的磁盘资源,用一个二进制位 bit 标识磁盘中的一个磁盘块的使用情况(1 表示空闲,0 表示占用)。

image-20230427210730531

image-20230427210804093

MOS 操作系统中的文件系统服务通过 IPC 的形式供其他进程调用,进行文件读写操作。

image-20230427211335538

微内核的基本设计思想和结构

整个文件系统体现了 MOS 的微内核设计,包括下面三个部分:

  1. 将传统操作系统的文件系统移出内核,使用用户态的文件系统服务程序以及一系列用户库来实现。即使它们崩溃,也不会影响到整个内核的稳定。其他用户进程通过进程间通信(IPC) 来请求文件系统的相关服务。因此,在微内核中进程间通信 (IPC) 是一个十分重要的机制。

  2. 操作系统将一些内核数据暴露到用户空间,使得进程不需要切换到内核态就能访问。MOS将进程页表映射到用户空间,此处文件系统服务进程访问自身进程页表即可判断磁盘缓存中是否存在对应块。

  3. 将传统操作系统的设备驱动移出内核,作为用户程序来实现。微内核体现在,内核在此过程中仅提供读写设备物理地址的系统调用。

lab_summary

本lab相关函数速查:
函数 作用
sys_write_dev 实现设备的写操作(内核态,系统调用)
sys_read_dev 实现设备的读操作(内核态,系统调用)
read_sector 将磁盘中对应 sector 的数据读到设备缓冲区中
ide_write 实现设备的写操作(用户态)
ide_read 实现设备的读操作(用户态)
block_is_free 通过位图中的特定位来判断指定的磁盘块是否被占用
free_block 负责维护磁盘块的释放
create_file 现将一个文件或指定目录下的文件按照目录结构写入到 target/fs.img
diskaddr 计算指定磁盘块对应的虚存地址
map_block 检查指定的磁盘块是否已经映射到内存
unmap_block 解除磁盘块和物理内存之间的映射关系,回收内存
read_block和write_block 读写磁盘块
file_get_block 将某个指定的文件指向的磁盘块读入内存
dir_lookup 查找某个目录下是否存在指定的文件
总结:

本次实验的代码填写比较简单,根据代码注释和指导书的提示就可以轻松完成,评测拿到满分并不困难。

尽管实验任务比较简单,但是这次实验涉及的代码文件更多,新增代码多达千余行,新增结构体也有十几个,给代码的理解带来了很多困难。因此,要想真正理解每个函数的作用、函数之间的调用关系以及用户申请文件系统服务的流程,还需要下一番功夫。

  • Title: lab5-report
  • Author: Charles
  • Created at : 2023-04-26 21:38:59
  • Updated at : 2023-11-05 21:36:18
  • Link: https://charles2530.github.io/2023/04/26/lab5-report/
  • License: This work is licensed under CC BY-NC-SA 4.0.
Comments