Contents

OS-Lab5实验报告

思考题

Thinking 5.1

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

如果使用cache,数据的读写很有可能不会及时同步到外设,可能会出现数据永远不会更新、外设数据出现变化却永远读不到新数据等情况。

Thinking 5.2

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

1024 1024 4MB

Thinking 5.3

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

1GB

Thinking 5.4

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

//user/include/fs.h
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[FILE_STRUCT_SIZE - MAXNAMELEN - (3 + NDIRECT) * 4 - sizeof(void *)];
} __attribute__((aligned(4), packed));

文件控制块(File结构体)。该结构体是文件系统操作文件的基本单位。

// user/include/fd.h
struct Fd {
	u_int fd_dev_id;
	u_int fd_offset;
	u_int fd_omode;
};
struct Filefd {
	struct Fd f_fd;
	u_int f_fileid;
	struct File f_file;
};

fd结构体是文件描述符的基本结构,是暴露给用户、用于索引文件的核心结构体。且通过指定fd_dev_id,并对文件、管道、控制台分别定义结构体,并包含该fd结构体,实现了面向对象中继承的思想,并在后续操作中通过判断fd_dev_id可以实现父类Fd和子类Filefd等的转换,即等价于面向对象的继承和多态。

#define FS_MAGIC 0x68286097 // Everyone's favorite OS class

小彩蛋,源自MIT6.828和MIT6.097(也就是MIT的OS课程代码和2002年以前的旧代码),根据学长的断代工程,可以看到这行代码在可考证的历史中最早来自2003年的JOS源代码,尽管在2006年的JOS中该魔数已经被替换成0x4A0530AE(注释为related vaguely to ‘J\0S!’),但由于MOS是参考了老版本的JOS,因此这行代码也一直被保留了下来,在近二十年历届学长的维护和修缮工作中仍然完整的传承了下来。这样一份历史悠久的大工程代码中,总能在某些地方找到前人的痕迹(当然也可能是没人敢动的屎山),或许这种超时空对话也是计算机的浪漫之处吧。

Thinking 5.5

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

会,因为文件描述符存储在[FDTABLE, FILEBASE)空间中,位于USTACKTOP下,fork时duppage()函数会将这部分页表项进行拷贝。

int main(){
    int fd;
    int r;
    char buf[1000];
    fd = open("/forkTest", 0);
    if((r = fork()) < 0){
        user_panic("Error with fork");
    }else if(r == 0){
        if((r = read(fd, buf, 30)) != 30){
            user_panic("Error with child read forkTest");
        }
        debugf("child read is good with  \"%s\"\n", buf);
    }else{
        if((r = read(fd, buf, 28)) != 28){
            user_panic("Error with father read forkTest");
        }
        debugf("father read is good with \"%s\"\n", buf);
    }
    return 0;
}

// in forkTest file
we share the file descriptor and the file offset pointer.

//output
child read is good with  "we share the file descriptor "
father read is good with "and the file offset pointer."

Thinking 5.6

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

// /user/include/fs.h
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; // 父目录的File结构体
	char f_pad[FILE_STRUCT_SIZE - MAXNAMELEN - (3 + NDIRECT) * 4 - sizeof(void *)]; //占位
} __attribute__((aligned(4), packed));

// /user/include/fd.h
struct Fd {
	u_int fd_dev_id; // 设备(文件/管道/控制台)
	u_int fd_offset; // 偏移 
	u_int fd_omode; // 打开状态
};

// /user/include/fd.h
// file descriptor + file
struct Filefd {
	struct Fd f_fd; // Fd结构体
	u_int f_fileid; // id
	struct File f_file; // File副本
};

File是用于真实与磁盘进行交互的信息,Fd是进程管理文件的核心接口,Filefd类似于Fd的子类,可以认为是文件类型的file descriptor。除去File对应磁盘上的物理实体(被加载到内存中)外,其他完全是内存中的概念。File在file server中用于文件的读写等,而Fd/Filefd则是暴露给用户态的接口。

Thinking 5.7

图5.9 中有多种不同形式的箭头,请解释这些不同箭头的差别,并思考我们的操作系统是如何实现对应类型的进程间通信的。 https://gitee.com/michsong/blog-images/raw/master/1779974636014.png

  • 同步消息:黑箭头+黑实线
  • 异步消息:开箭头+黑实线
  • 返回消息:开箭头+黑虚线

操作系统在init时,首先创建用户进程,其次创建fs进程;fs进程初始化后进入serv()函数,阻塞等待IPC;user进程向fs发送请求,等待fs进程返回后继续工作,fs等待下一次请求。

难点分析

服务调用链

文件服务的调用链极深,这里以一个例子来说明。
对于打开文件操作:

  • 用户首先调用user/lib/file.c中的open()函数
  • 该函数在准备好文件描述符指针后调用user/lib/fsipc.c中的fsipc_open()函数,准备好请求体之后调用fsipc()函数,由该函数统一发送ipc请求。
  • fs进程中,fs/serv.c下的serve()函数负责接收用户传来的请求,并进行请求的分发。
  • 分发至serve_open()函数,该函数调用fs/fs.c中的file_open()函数。
  • 该函数负责根据路径找到具体文件的File结构体指针并返回。
  • serve_open()函数将Open结构体赋值后,将其中的Filefd再次ipc_send()给用户进程。
  • 用户进程拿到Filefd后,逐级返回至open()函数,使用拿到的fileid进行fsipc_map()(链路相同)以将文件内容映射到Filefd。
  • 最后将该文件描述符的num返回。

实验体会

Lab5中,我们学习了文件系统的基本实现,同时对封装、统一接口设计等理念有了更深刻的理解。

原创说明

本文均为原创,部分内容参考了AI的回答以及TripleCameratheUHO学长的博客。