Lab5实验报告

BUAA 2023 Operating System Lab5

(一)思考题

Thinking 5.1

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

缓存内存中映射的外部设备的内容时,如果外设对于内存进行更新,但是此时的cache没有更新,会造成数据旧的问题。这是由于cache的更新机制导致的,cache的更新只在CPU写那个地址或者没有相应地址的缓存的时才会进行。

IDE磁盘发生的可能性小一些,因为磁盘不会去主动修改内存的内容,但是外部设备可能会修改内存的内容导致错误。

Thinking 5.2

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

一个磁盘块的大小为4KB,一个文件控制块的大小为256Byte,一个磁盘块中最多有2^12^/2^8^=2^4^=16个文件控制块。

一个目录下最多指向10+1024-10=1024个磁盘块,每一个磁盘块有最多有16个文件控制块,所以一个目录最多有1024*16=16K个文件。

单个文件系统支持的最大文件为4096*4096/4=2^22^B=4KB。

Thinking 5.3

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

1GB。因为磁盘的缓存范围是0x10000000到0x50000000。

image-20230520163614153

Thinking 5.4

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

serv.h

在该文件中比较重要的宏定义有PTE_DIRTYDISKMAPDISKMAX

PTE_DIRTY,用于标注页面是否被修改,主要用于最终对于修改的磁盘镜像的写回。

DISKMAP表示磁盘镜像缓存的起始地址,DISKMAX表示磁盘缓存的最大大小,在fs.c中的disaddr()中有使用。

fs.h

BY2BLKBIT2BLK分别以字节和位为单位描述了⼀个磁盘块的大小, BIT2BLK用于位图相关操作。

MAXPATHLEN是路径的最长长度。MAXNAMELEN是f_name的最长长度加一,因为最后一位要用来存’\0’。

Thinking 5.5

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

#include <lib.h>
int main(){
    int fd;
    int envid=fork();
    fd=open("/test",O_RDWR);
	debugf("envid=%d,fd=%d\n",envid,fd);
}

image-20230520172802051

从结果可以看出父子进程拥有相同的fdnum,并且由 INDEX2FD 宏可看出,⽂件描述符和 fdnum 是⼀⼀对应的,所以 fork 前后的父子进程确实会共享文件描述符。对于同⼀⽂件描述符结构体,其成员变量 fd_offset 即定位指针也当然是相同的。

Thinking 5.6

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

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

File对应的是文件控制块,储存了文件的名字,大小,是否是目录,以及文件所处的目录和文件内容所处的磁盘块,应该对应的是磁盘中的内容,因为内存会断电丢失数据,文件应该保存在磁盘中。在fs.c中file_*的函数中都会使用到File结构体。

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和FileFd都处于内存中,从结构体内容可看出 Fd结构体存储映射到⽂件控制符的磁盘⽂件的基本信息,包括⽂件所在设备的 id ,在文件中定位的指针,以及文件的打开方式openmode,Filefd结构体除此以外还存储了文件的id和文件控制块。这两个结构体主要在serv.c,file.c中出现,在打开文件的open过程中,首先调用fsipc_open,传入fd结构体指针,接下来使用调用fsipc将打开文件的信息存入到fd中,之后就以fd结构体中得到的数据,对于文件内容进行映射,进行接下来的操作。

Thinking 5.7

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

红色进程是操作系统进程,黄色进程是文件服务进程,绿色进程是用户进程。红色进程指向文件服务进程和用户进程的两个箭头表示的是创建相应的进程。

接下来,从用户进程指向文件服务进程的箭头表示请求服务,该图以open为例,首先调用open方法,在open方法下首先会调用fd_alloc,接下里会调用fdipc_open,并且传入刚才新分配的fd,在fsipc_open函数中调用的fsipc在设置好请求服务类型时会调用ipc_send方法,将请求传输给文件服务进程。

而文件服务进程指向用户进程的箭头就表示文件服务进程把用户进程请求的服务信息传回,在open方法中就是将要打开文件的相关信息都传入fd结构体中。

(二)实验难点

Exercise 5.1

这里的难点在于要注意可写地址的上限,起始地址加上最大偏移的地方也是可以读或者写的,所以在判断地址是否有效时要用小于等于号,第二个点在于,我们对于物理地址在512MB~1GB范围内的地址操作时,要给物理地址加上KSEG1转换为内核的虚拟地址进行操作。

Exercise 5.3

在做这个练习时,代码没有太多的解释,这时我们就需要根据上面给的一段读写磁盘的汇编代码进行转化,我们要使用syscall_read_dev()和syscall_write_dev()进行对于特定设备IO地址的写和读,这就是比较简单的翻译,但是需要注意的是,我们在向磁盘的偏移的位置写入时,要写入绝对偏移,即:

uint32_t curoff=begin+off;
syscall_write_dev(&curoff, DEV_DISK_ADDRESS + DEV_DISK_OFFSET,sizeof(off));

Exercise 5.4

难点在于对于bitmap[blockno / 32] |= (1 << (blockno % 32)) 的理解,

blockno/32用于表示blockno的磁盘块的表示位在第几个字中,4B也就是32位,然后1 << (blockno % 32)用于获取blockno磁盘块在该字中的表示位,然后将之设置为1表示空闲。

Exercise 5.5

难点在于要理解目录文件控制块的索引形式,对于直接索引,只有10个,每一个存着所对应的磁盘块的块号,当文件的大小大于10个磁盘块的大小时,需要使用间接索引,将磁盘号为f_indirect的磁盘作为一个大的”数组“,用来存储间接的磁盘号,由于我们规定了对于间接索引的前十个位置不使用,那就很好的拓展了文件控制块,给我们的感觉就是一个文件控制块里拥有连续的1024个磁盘块的索引,所以下面的代码就很好解释了。

if(i<NDIRECT){
	bno=dirf->f_direct[i];
}
else{
	bno=(disk[dirf->f_indirect].data)[i];
}

Exercise 5.8

难点在于理解这个函数的运行逻辑,我们得到了目录的文件指针,首先我们算出这个目录所存的文件占用磁盘块的数量,然后通过遍历磁盘块(使用file_get_block()实现)在每一个磁盘块里寻找文件名,最终得到目标文件名的文件控制块。

该函数的功能是在 dir 指向的文件控制块所代表的目录下寻找名为 name 的文件。 对每一个 dir 指向的文件控制块所管理的磁盘块: 使用 file_get_block 函数获取该磁盘块 遍历该磁盘块上存储的所有文件控制块,如果存在某个文件控制块的 f_name 属性与 name 相同,就将 *file 设为该文件控制块,并返回 0。

Exercise 5.9

函数接受文件路径 path 和模式 mode 作为输入参数,并返回文件描述符的编号。 首先使用 fd_alloc 和 fsipc_open 函数分配一个文件描述符。 之后使用 fd2data 函数获取文件描述符对应的数据缓存页地址,并从 Filefd 结构体中获取文件的 size 和 fileid 属性。 之后遍历文件内容,使用 fsipc_map 函数为文件内容分配页面并映射。 最后使用 fd2num 函数返回文件描述符的编号。

Exercise 5.10

难点在于理解整个过程。函数接收一个文件描述符编号 fdnum ,一个缓冲区指针 buf 和一个最大读取字节数 n 作为输入参数,返回成功读取的字节数并更新文件描述符的偏移量。 首先使用 fd_lookup 和 dev_lookup 函数获取文件描述符和设备结构体。如果查找失败,则返回错误码。 接下来,函数检查文件描述符的打开模式是否允许读取操作。 然后,函数调用设备的读取函数 dev_read 从文件当前的偏移位置读取数据到缓冲区中。 最后,如果读取操作成功,则函数更新文件描述符的偏移量并返回成功读取的字节数。

Exercise 5.11

难点在于理解这是文件服务进程的函数,serve_remove首先调用file_remove删除path中的文件,并且存储返回值,最后将返回值通过ipc传回请求文件系统服务的原进程去,即ipc_send。

Exercise 5.12

难点在于理解用户进程调用文件系统服务的整个过程,对于请求remove来说,首先将所需要的数据准备好即要删除文件的path,接下来通过fsipc将请求传输到文件服务进程进行相应的操作。

文件服务进程的调用流程

文件系统服务通过IPC的形式供其他进程调用,进行文件读写操作。 用户程序在发出文件系统操作请求时,将请求的内容放在对应的结构体中进行消息的传递, fs_serv 进程收到其他进行的 IPC 请求后,根据请求的类型执行对应文件操作,将结果重新通过 IPC 反馈给用户程序。

(三)实验体会

在这次的实验中,代码的量以及理解难度又有了一定程度的上升,在这一阶段的学习中,我有两点学习收获:

  • 通过ide部分的学习,我学习到了和外界设备进行交互的方法,即通过编写驱动程序,向特定的地址写入控制信号,以此来实现磁盘的读写。

  • 除此以外,我学习到了微内核下文件系统的实现。在这个过程中,我们需要理解许多函数的作用,但是我们还要再全局上想明白,这次的实验分为两个部分,第一个部分是文件系统如何操作文件,第二个部分是用户进程如何调用文件系统服务,其中的交互关系也是需要我们思考清楚的。这种功能分开的机制体现了我们设计时常说的”模块化设计”,还有所谓的高内聚,低耦合的特征。

Search by:GoogleBingBaidu