实验文档-lab5
一、思考题汇总
思考1:
查阅资料,了解 Linux/Unix 的 /proc 文件系统是什么?有什么作用? Windows 操作系统又是如何实现这些功能的?proc 文件系统这样的设计有什么好处和可以改进的地方?
答:/proc文件系统是一个虚拟文件系统,通过它可以使用一种新的方法在Linux内核空间和用户空间之间进行通信。在/proc文件系统中,我们可以将对虚拟文件的读写作为对内核中实体进行通信的一种手段,与普通文件不同的是,这些虚拟文件的内容为动态创建。
/proc文件系统是一种伪文件系统,存储的是当前内核运行状态的一系列文件。用户可以通过这些文件查看有关的硬件以及当前进程的信息,甚至通过改变其中文件来改变内核运行状态。
在windows系统中,通过Win32 API函数调用来完成与内核的交互。
proc文件系统的设计将对内核信息的访问交互抽象为对文件的访问修改,简化了交互过程。
思考2:
如果我们通过 kseg0 读写设备,我们对于设备的写入会缓存到 Cache 中。通过 kseg0 访问设备是一种错误的行为,在实际编写代码的时候这么做会引发不可预知的问题。请你思考:这么做这会引起什么问题?对于不同种类的设备(如我们提到的串口设备和 IDE 磁盘)的操作会有差异吗?可以从缓存的性质和缓存刷新的策略来考虑。
答:kseg0是存放内核的区域,一般通过cache访问。如果在写设备的时候将写入缓存到cache,会导致之后想访问内核时错误访问cache中写入设备的内容。
此外,cache在被置换时写回。console这类外设有实时交互型。若通过cache实现一个向console的输出,那么只有在cache被替换时才能在console上看到输出。
思考3:
一个磁盘块最多存储 1024 个指向其他磁盘块的指针,试计算我们的文件系统支持的单个文件的最大大小为多大?
答:
4KB * 1024 = 4MB
思考4:
查找代码中的相关定义,试回答一个磁盘块中最多能存储多少个文件控制块?一个目录下最多能有多少个文件?
答:
一个磁盘块中有FILE2BLK
= 16个文件控制块。一个目录最多指向1024个磁盘块,因此一共目录中最多1024*16=16384个子文件。
思考5:
请思考,在满足磁盘块缓存的设计的前提下,我们实验使用的内核支持的最大磁盘大小是多少?
答:最大磁盘大小为DISKMAX
= 0x40000000,即1GB。
思考6:
如果将DISKMAX
改成0xC0000000, 超过用户空间,我们的文件系统还能正常工作吗?为什么?
答:不能正常工作,因为如果这样设计,写磁盘的过程中会覆盖掉内核的关键内容,可能会令操作系统运行异常。
思考7:
阅读 user/file.c,思考文件描述符和打开的文件分别映射到了内存的哪一段空间。
在打开文件时,文件描述符的位置被函数fd_alloc
安排。在该函数中,我们发现文件描述符被放置在内存中的FDTABLE
域。即0x60000000
以下的一段空间。
打开的文件与文件描述符被共同装在一个Filefd
结构体中,位于文件描述符所独占的那一页中。
思考8:
阅读 user/file.c,你会发现很多函数中都会将一个struct Fd\*
型的 指针转换为struct Filefd\*
型的指针,请解释为什么这样的转换可行。
答:在user/fd.h
中,对于Filefd
结构体:
struct Filefd {
struct Fd f_fd;
u_int f_fileid;
struct File f_file;
};
会发现Filefd
结构体的第一个元素就是Fd
结构体,因此这样的轻质转化的目的是将Filefd
的f_fd
域进行赋值。
C语言是一种弱类型语言,指针指向的是一个地址,只要能取出合法的元素,就可以在各种类型之间相互进行转化。
思考9:
请解释 Fd, Filefd, Open 结构体及其各个域的作用。比如各个结构体会在哪些过程中被使用,是否对应磁盘上的物理实体还是单纯的内存数据等。说明形式自定,要求简洁明了,可大致勾勒出文件系统数据结构与物理实体的对应关系与设计框架。
答:
Fd
为文件描述符结构体,其中:fd_dev_id
:指外设id,也就是外设类型。fd_offset
:读写的当前位置(偏移量),类似“流”的当前位置。fd_omode
:指文件打开方式,如只读,只写等。
Filefd
为记录文件详细信息的结构体,其中:f_fd
:即文件描述符。f_fileid
:指文件本身的id。f_file
:指文件本身。
Open
用于抽象化记录打开文件这一行为,其中:o_file
:指向具体的file
。o_fileid
:即file
的id。o_mode
:打开方式,指只读,只写等。o_ff
:读或写的当前位置,指偏移量。
思考10:
阅读serv.c/serve函数的代码,我们注意到函数中包含了一个死循环for (;;) {...}
,为什么这段代码不会导致整个内核进入 panic 状态?
答:该函数是一个后台进程,在有文件请求时才会继续执行死循环,其余时间在等待,因此不会让整个内核panic
。
二、实验难点图示
难点1:设备驱动,即进行设备读写
在我们的实验中,要实现的设备驱动主要有三种:即console
,IDE
和rtc
。在内存中的布局如下表所示。
device |
start addr |
length |
---|---|---|
console |
0x10000000 |
0x20 |
IDE |
0x13000000 |
0x4200 |
rtc |
0x15000000 |
0x200 |
其中:
console
为控制台终端,即我们在编写程序时用于输入输出的地方。IDE
即为磁盘,用于存储文件等。rtc
为实时时钟终端,用于获取当前时间等信息。
在用户态对上述外部设备进行读写等操作时,需要用到系统调用,在本次实验中实现的系统调用为syscall_write_dev
与syscall_read_dev
。这两个函数实现的是将某段内存中的信息拷贝到外部设备的相应内存区域或将外部设备内存中的信息拷贝到某段内存中。
难点2:IDE磁盘IO
不同于console
与rtc
,IDE
磁盘的读写不能简单将内容往特定区域直接放置,而是有一套严格的IO驱动程序操作。
#define IDE_START_A 0x13000000
#define IDE_OFFSET_A (IDE_START_A + 0x0000)
#define IDE_OFFSET_HIGH_A (IDE_START_A + 0x0008)
#define IDE_ID_A (IDE_START_A + 0x0010)
#define IDE_OP_A (IDE_START_A + 0x0020)
#define IDE_STATUS_A (IDE_START_A + 0x0030)
#define IDE_BUFFER_A (IDE_START_A + 0x4000)
#define BUFFER_SIZE 0x0200
其中:
IDE_START_A
是磁盘的起始地址。IDE_OFFSET_A
用于存储读写的偏移位置。IDE_OFFSET_HIGH_A
用于存储读写偏移的高位。IDE_ID_A
用于存储磁盘ID。IDE_OP_A
用于存储操作符,即“读”或“写”。IDE_STATUS_A
用于存储读写的状态,即操作是否成功。IDE_BUFFER_A
用于存储缓冲数据。BUFFER_SIZE
指一块缓冲区的大小,与一个扇区的大小相同。
在读/写磁盘时,首先应通过系统调用函数将偏移,磁盘ID以及OP写入,再对BUFFER来进行读/写,最后读取STATUS
来判断磁盘读/写是否成功。
难点3:文件系统结构
这一部分与lab2中内存管理的部分有相似处,有较多的结构体等,且都有特定的结构特征。
-
Block
结构体:struct Block { uint8_t data[BY2BLK]; uint32_t type; };
指磁盘中的磁盘块,其中包含的元素为此磁盘块存储的数据
data
以及此磁盘块的类型type
。其中
type
包含以下的种类:enum { BLOCK_FREE = 0, BLOCK_BOOT = 1, BLOCK_BMAP = 2, BLOCK_SUPER = 3, BLOCK_DATA = 4, BLOCK_FILE = 5, BLOCK_INDEX = 6, };
类型 具体涵义 BLOCK_FREE
空闲磁盘块 BLOCK_BOOT
磁盘的启动磁盘块和分区 BLOCK_BMAP
位图表,存储位图信息 BLOCK_SUPER
超级块,存储文件系统基本信息 BLOCK_DATA
文件数据块,存储文件或目录的数据 BLOCK_FILE
存储文件指针的磁盘块 BLOCK_INDEX
索引磁盘块,其中的数据是指向其他磁盘块的指针 有关的参数:
BY2BLK
:每个磁盘块的大小,为4096
,即4KB
。BIT2BLK
:每个磁盘块有多少位,为8*BY2BLK
。BY2SECT
:每个扇区的大小,为512
。SECT2BLK
:每个磁盘块包含几个扇区,为8
。
-
disk
数组:struct Block { uint8_t data[BY2BLK]; uint32_t type; } disk[NBLOCK];
一个存放有
Block
结构体的数组,即为本次实验中的磁盘。有关的参数:
NBLOCK
:每个磁盘有多少个磁盘块,为1024
。DISKMAP
:磁盘的空间地址,为0x10000000
。DIXKMAX
:磁盘大小,为0x40000000
。
-
File
结构体: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_int f_printcount; u_int f_modifycount; u_char f_pad[BY2FILE - MAXNAMELEN - 4 - 4 - NDIRECT * 4 - 4 - 4 - 4 - 4]; };
f_name
:文件名。f_size
:文件大小。f_type
:文件类型,包括FTYPE_REG
与FTYPE_DIR
。f_direct[]
:文件的直接指针。f_indirect
:文件的间接指针。f_dir
:文件所在文件夹。f_pad
:填充符。
此为文件结构体,其中
f_direct
中存储的是10个磁盘块编号。f_indirect
存储一个磁盘块编号,该磁盘块是INDEX
型的磁盘块,用于存储1024个磁盘块编号。有关的参数:
FILE2BLK
:一个磁盘块中可存储多少个File
结构体,为16
。BY2FILE
:一个File
结构体的大小,为256
。NDIRECT
:File
结构体直接指针的个数,为10
。NINDIRECT
:File
结构体间接指针的个数,为1024
,与NBLOCK
相当。
难点4:文件系统用户接口
在file.c
中定义了许多用户态对文件的操作,这些操作主要分为两种,一种通过系统调用IPC
机制实现,例如open
,另一种不通过系统调用实现,如file_read
。
文件系统的用户请求IPC主要有以下几种:
#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
请求的发送由file/fsipc.c
函数fsipc
实现,其接受的参数type
即为请求的类型。
当用户态需要进行文件操作请求时,进入file/fsipc
中的fsipc_xxx
,在进行一系列准备操作后,将相应请求码传入fsipc
函数中,进行ipc
操作。
在发送这些请求时,与用户进程进行IPC交互的进程为fs_serv
,其核心代码在serv.c
中,其会根据传递来的各种请求执行相应的操作。
三、体会与感想
lab5(×)
lab2plus(√)
虽然之前听学长说lab5的难度在后面几个lab中是最低的,但对于我个人而言,实际操作起来还是很艰难的。
主要在于这次的实验又开始涉及许多有关存储的知识体系,而我在前面的几个lab中,对于lab2的掌握是最差的,因此在填写lab5的部分代码时,有一种“梦回lab2”的感觉,对于磁盘,磁盘块,扇区,文件结构等等概念梳理不清,导致没有什么头绪。尤其是在create_file
函数的填写中十分艰难,几度不知道该从何下手。之后花了很长时间阅读各种代码,宏定义以及指导书,才大致勾勒出这一个版块各个结构之间的关系。
这一lab的内容综合性较强,将前面几个lab的知识体系串联在一起,从lab2的内存结构到lab4的IPC,每一步都需要深入理解才能帮助我们顺利完成lab5的实验。在此次实验中又发现了一个lab2的祖传bug,让我的程序出现“加上无关紧要的代码后行为突变”的情况,最后通过修复lab2来解决可能是一个代码段加载的问题。
此外,这次的课上测试让我认识到我们不能只关注实验中提供的代码文件,对于我们的小操作系统的实现也要进行掌握。在lab5-1-exam中,对Gxemul官方文档的不熟悉让我在读取时间时出现的很大的阻碍。
四、残留难点
实验在对文件分类时仅仅分成了目录和普通文件两种,而在实际的计算机中文件往往有很多中,有些还能作为一段程序来运行等等,对于用户态如何按照规则使用这些特殊文件的功能不了解。