BUAA-OS-2023-Lab5-Report

Thinking 5.1

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

如果使用 kseg0 段读写设备,那么对于外设而言,在系统读取外设时就会不可避免地先在 Cache 中查找设备对应的地址,如果查询到就会返回缓存的值。但如果在上一次缓存过后,设备的值已经发生了改变,这时我们从 Cache 中读取到的就是过时的错误信息。

对于写入也是同理,我们会优先写入到 Cache 中对应的地址处,那么下一次写就会覆盖上一次写的结果,导致外设并不能及时、正确地读取到我们写入的值。

对于不同的外设种类而言,这个现象会有些微差异。串口设备由于其即时性与高使用频率,会更容易出现这样的错误,对于磁盘而言则不太容易。

Thinking 5.2

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

首先根据定义 BY2BLK 可知一个磁盘块大小为 4096 Byte。同时一个文件控制块 File 大小为 256 Byte。则一个磁盘块中最多能存储 $4096 / 256 = 16$ 个文件控制块。

一个目录可以通过 f_indirect 字段指向 1024 个指向其内包含的磁盘块的指针,那么一个目录下最多有 $1024 * 16 = 16384$ 个文件。

对于单个文件也是同理,f_indirect 字段会指向 1024 个包含其内容的磁盘块指针,这样一个文件最大大小就是 $1024 * 4KB = 4MB$

Thinking 5.3

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

由于 MOS 中一个进程可以拥有 4 GB 的虚拟内存空间,并且对于文件管理进程而言, DISKMAPDISKMAP + DISKMAX 这一段虚存地址空间 (0x10000000-0x4fffffff) 会作为作为磁盘块的缓冲区。那么最大的磁盘大小就是 DISKMAP,即 1 GB。

Thinking 5.4

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

其实文件控制块和超级块都比较好理解

struct File {		//文件控制块,是文件系统用于管理文件的数据结构
u_char f_name[MAXNAMELEN]; // filename
u_int f_size; // file size in bytes
u_int f_type; // file type
u_int f_direct[NDIRECT];
u_int f_indirect;
struct File *f_dir; // the pointer to the dir where this file is in, valid only in memory.
u_char f_pad[BY2FILE - MAXNAMELEN - 4 - 4 - NDIRECT * 4 - 4 - 4];
};
#define FS_MAGIC 0x68286097 // Everyone's favorite OS class

struct Super { // 根目录块
u_int s_magic; // Magic number: FS_MAGIC
u_int s_nblocks; // Total number of blocks on disk
struct File s_root; // Root directory
};
// fsreq 的请求分类
#define FSREQ_OPEN 1
#define FSREQ_MAP 2
#define FSREQ_SET_SIZE 3
#define FSREQ_CLOSE 4
#define FSREQ_DIRTY 5
#define FSREQ_REMOVE 6
#define FSREQ_SYNC 7

struct Fsreq_open {
char req_path[MAXPATHLEN];
u_int req_omode;
};

struct Fsreq_map {
int req_fileid;
u_int req_offset;
};

struct Fsreq_set_size {
int req_fileid;
u_int req_size;
};
// continue
//fs/serv.c
void serve(void) {
u_int req, whom, perm;

for (;;) {
perm = 0;

req = ipc_recv(&whom, (void *)REQVA, &perm);

// All requests must contain an argument page
if (!(perm & PTE_V)) {
debugf("Invalid request from %08x: no argument page\n", whom);
continue; // just leave it hanging, waiting for the next request.
}

switch (req) {
case FSREQ_OPEN:
serve_open(whom, (struct Fsreq_open *)REQVA);
break;

case FSREQ_MAP:
serve_map(whom, (struct Fsreq_map *)REQVA);
break;

case FSREQ_SET_SIZE:
serve_set_size(whom, (struct Fsreq_set_size *)REQVA);
break;

case FSREQ_CLOSE:
serve_close(whom, (struct Fsreq_close *)REQVA);
break;

case FSREQ_DIRTY:
serve_dirty(whom, (struct Fsreq_dirty *)REQVA);
break;

case FSREQ_REMOVE:
serve_remove(whom, (struct Fsreq_remove *)REQVA);
break;

case FSREQ_SYNC:
serve_sync(whom);
break;

case FSREQ_OPENAT:
serve_openat(whom, (struct Fsreq_openat *)REQVA);
break;

default:
debugf("Invalid request code %d from %08x\n", whom, req);
break;
}

syscall_mem_unmap(0, (void *)REQVA);
}
}

这里就是我们进行ipc通讯后,对数据进行分发的场所,我们会根据收获到的req的不同,将用于接受信息(文件系统接受,由用户发来)的REQVA解读为不同的结构体,并传入不同的服务函数进行实现。

整个分发实现是文件系统与外界的接口,文件系统内部的实现是相对自由的(你可以进行系统调用、使用用户态函数、操作磁盘等等),只要反馈给用户进程的结果对就行

Thinking 5.5

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

fdnum = open("/newmotd", O_RDWR);
if ((r = fork()) == 0) {
n = read(fdnum, buf, 4);
debugf("[child] buffer is \'%s\'\n", buf);
} else {
n = read(fdnum, buf, 4);
debugf("[father] buffer is \'%s\'\n", buf);
}
// file here







welcome to MOS with a file system.

// output here







[father] buffer is 'welc'
[child] buffer is 'ome '

显然,父子进程也会同样共享相同的文件描述符和定位指针。其原因在于页面是 PTE_LIBRARY 的而非 PTE_COW 的。

Thinking 5.6

  • 请解释 File, Fd, Filefd 结构体及其各个域的作用。比如各个结构体会在哪些过程中被使用,是否对应磁盘上的物理实体还是单纯的内存数据等。说明形式自定,要求简洁明了,可大致勾勒出文件系统数据结构与物理实体的对应关系与设计框架。
struct Fd { //文件描述符;用户用于描述该文件,单纯的内存数据,关机就消失;
u_int fd_dev_id; //该文件对应的设备id
u_int fd_offset; //读写偏移量
u_int fd_omode; //允许用户进程对文件的操作权限
};

struct Filefd { //单纯的内存数据,关机就消失,是便于文件系统查看的结构;
struct Fd f_fd; //文件描述符
u_int f_fileid; //文件系统为打开的文件进行的编号
struct File f_file; //对应文件的文件控制块
};

struct Open { // 文件系统用来保存已打开的文件信息
struct File *o_file; // 指向文件控制块的指针
u_int o_fileid; // 文件打开后的编号
int o_mode; // 允许用户进程对文件的操作权限
struct Filefd *o_ff; // 指向该文件描述符的指针
};

  • 这部分的部分内容借鉴了学长的报告

Thinking 5.7

  • 图 5.7 中有多种不同形式的箭头,请解释这些不同箭头的差别,并思考我们的操作系统是如何实现对应类型的进程间通信的。
  1. 同步消息,用黑三角箭头搭配黑实线表示:

image-20230511110441745

同步意义:消息的发送者把进程控制传递给消息的接收者,然后暂停活动等待消息接收者的回应消息。

  1. 返回消息,用开三角箭头搭配黑色虚线表示:

    image-20230511110455875

返回消息和同步消息结合使用,因为异步消息不进行等待,所以不需要知道返回值。

对于文件系统,通过特定调用号使得文件系统知道请求者有何种需求,然后文件系统进入相应处理函数中处理,将结果通过ipc_send传回用户进程。

小结

纵观 Lab5,MOS 在其中为我们构造了一个通配的文件系统。

它通过统一的结构体 Fd 层次化地管理所用可用的设备。向上,在 fd.c 中为用户提供统一的操作函数;向下,不同设备通过 implements 实现 Dev 的功能函数,达到同一调用的效果。

在此之下,file.cconsole.cpipe.c 三文件实现了 Dev 中的”抽象函数“。为了优化函数实现的效果,我们建立了一个为所有进程管理纯文件 file 的文件服务进程 serv.c,并通过 fsipc.c 借助 进程间 IPC 通信机制实现数据传输。

对文件服务进程而言,它通过 IPC 通信与用户进程(请求方)通信,利用文件级、磁盘块级的交互函数 fs.c 与磁盘进行交互,并利用块缓存的机制对磁盘块进行管理,以满足请求者对指定文件的特定文件块的访问。

再细化到最后一步,所有的磁盘交互函数都是利用系统调用访问 KSEG1 段实现与外设的直接交互,并在最底层的函数中实现了以块、甚至扇区为单位的读写,即 ide.c