BUAA-OS-Lab5 文件系统
指导书梳理
MOS文件系统设计
文件系统将外部设备中的资源抽象为文件,从而可以统一管理外部设备,包括文件设备(file,狭义的“文件”)、控制台(console)和管道(pipe)
构成部分
- 外部存储设备驱动
- 该驱动程序将通过系统调用的方式陷入内核,对磁盘镜像进行读写操作
- 文件系统结构
- MOS 中的文件系统服务进程实际上也是一个运行在用户态下的进程
- 文件系统的用户接口
MOS 微内核设计
- 将文件系统移出内核,使用用户态的文件系统服务程序以及一系列用户库来实现
- 用户进程通过进程间通信(IPC) 来请求文件系统的相关服务
- 将一些内核数据暴露到用户空间,使得进程不需要切换到内核态就能访问
- 将传统操作系统的设备驱动移出内核,内核仅提供读写设备物理地址的系统调用
IDE 磁盘驱动
本次要实现的硬盘驱动程序与已经实现的串口驱动,都采用 MMIO (内存映射IO)技术编写驱动,完全运行在用户空间
内存映射 I/O (MMIO)
- I/O 端口实质上是外设寄存器
- 通常包括控制寄存器、状态寄存器和数据寄存器,这些寄存器被映射到指定的物理地址空间
- 在 MIPS 的内核地址空间中,对 kseg1 段地址的读写不经过 MMU 映射,且不使用高速缓存。且在模拟器上运行操作系统,I/O 设备物理地址完全固定,因此可以通过简单地读写某些固定的内核虚拟地址来实现驱动程序的功能
- 在编写设备驱动的时候,将物理地址转换为 kseg1 段的内核虚拟地址,也就是给物理地址加上 kseg1 的偏移值(0xA0000000)
IDE 磁盘
MALTA 平台上的 PIIX4 磁盘控制器基地址为 0x180001F0,在 MOS 中,我们可以挂载两块 IDE 磁盘,但实际上只用编号为 0 的一块磁盘
由于 CHS 模式不方便寻址,采用逻辑块寻址(LBA)进行扇区寻址。在 LBA 模式下,IDE 设备将磁盘看作一个线性的字节序列,每个扇区都有一个唯一的编号,只需要设置目标扇区编号,就可以完成磁盘的寻址
MOS中扇区编号有 28 位
驱动程序编写
磁盘操作中所有的地址操作都需要将物理地址转换成虚拟地址。此处设备基地址对应的 kseg1 的内核虚拟地址是 0xB80001F0
由于 IDE 外设一般不能立即完成数据操作,需要 CPU 检查 IDE 状态并等待操作完成
读写注意
- 本实验中使用的 IDE 设备无法一次性写入操作扇区号,因此需要单独设置扇区号的各位
- 特别地,在设置操作扇区号的 [27:24] 位时,还需要同时设置扇区寻址模式和磁盘编号,因此需要通过位运算将各值组合,并一齐写入对应地址
- 由于本实验中使用的 IDE 设备每次仅能读取或写入 4 字节,因此需要通过一个循环完成整个扇区的读取或写入,即连续向相同的地址读取或写入 4 字节
- 本实验中使用的 IDE 设备无法一次性写入操作扇区号,因此需要单独设置扇区号的各位
文件系统结构
磁盘文件系统布局
- 磁盘块是一个虚拟概念,是操作系统与磁盘交互的最小单位;操作系统将相邻的扇区组合在一起,形成磁盘块进行整体操作
- MOS 操作系统把磁盘最开始的一个磁盘块(4096 字节)当作引导扇区和分区表使用,接下来的一个磁盘块作为超级块(Super Block)
- 使用位图 (Bitmap) 法来管理空闲的磁盘资源,用一个二进制位 bit 标识磁盘中的一个磁盘块的使用情况(1 表示空闲,0 表示占用)
文件系统详细结构
File
结构体f_direct[NDIRECT]
文件的直接指针每个文件控制块设有 10 个直接指针,用来记录文件的数据块在磁盘上的位置。每个磁盘块的大小为 4KB,能够表示最大 40KB 的文件,大于 40KB 时需要用到间接指针
f_indirect
指向一个间接磁盘块,存储指向文件内容的磁盘块的指针。为简化计算,不使用间接磁盘块的前十个指针f_pad
让整数个文件结构体占用一个磁盘块,填充结构体中剩下的字节对于普通的文件,其指向的磁盘块存储着文件内容,而对于目录文件来说,其指向的磁盘块存储着该目录下各个文件对应的文件控制块
通过 fsformat(由 tools/fsformat.c 编译而成)程序来创建一个磁盘镜像文件 target/fs.img,模拟与真实的磁盘文件设备的交互
块缓存
MOS文件系统服务是一个用户进程,一个进程可以拥有 4GB 的虚拟内存空间,将 DISKMAP 到DISKMAP+DISKMAX 这一段虚存地址空间 (0x10000000-0x4FFFFFFF) 作为缓冲区,当磁盘读入内存时,用来映射相关的页
文件系统的用户接口
文件描述符
- 文件描述符存储文件的基本信息和用户进程中关于文件的状态,也起到描述用户对于文件操作的作用
- 当用户进程向文件系统发送打开文件的请求时,文件系统进程会将这些基本信息记录在内存中,然后由操作系统将用户进程请求的地址映射到同一个存储了文件描述符的物理页上(此部分代码位于 serv.c 的 serve_open 函数内),因此一个文件描述符至少需要独占一页的空间。当用户进程获取基本信息后,再次向文件系统发送请求将文件内容映射到指定内存空间中
文件系统服务
时纪
E 5.1
常用函数
is_illegal_va_range
判断虚拟地址合法性
注意开闭区间
写入的地址区间是
[pa, pa + len)
,其中最高的地址是pa + len - 1
。应当判断pa + len - 1 < PA_BASE + PA_LEN
,也就是pa + len <= PA_BASE + PA_LEN
E 5.4
“当出现错误时返回相应错误值”常用操作用r
记录返回值再判断if(r>0)
- 是判断是否**>0**!!错误值一般都是负值,正常值不一定是0也可能是正数!!
上机准备
简单梳理
文件在各个层次的抽象
用户进程中(非服务进程)—— user文件夹下文件
文件的抽象形式是文件描述符Fd
1
2
3
4
5
6
7
8
9
10
11
12
13
14struct Fd {
u_int fd_dev_id;
u_int fd_offset; // 定位指针
u_int fd_omode; // 打开形式
};
struct Dev devfile = {
.dev_id = 'f',
.dev_name = "file",
.dev_read = file_read,
.dev_write = file_write,
.dev_close = file_close,
.dev_stat = file_stat,
};- user的文件描述符Fd来源于serv通过ipc的页面共享ffd
- 可能会调到对应的Fd中的dev_id的dev的对应函数,在文件系统中也就是file_close、file_write等
- 内存空间中有专门的地址存放文件描述符,也就是FDTABLE,这里可以看成一个数组,每个Fd占一个Page大小,可以直接用fdnum作为下标获得
fsipc_xxx
实现与服务进程的交互其实就是找到相应的文件描述符Fd(毕竟这时还在用户进程中),然后构造相应类别的请求req,再通过ipc将该req发送给服务进程(其实就是把req的页面共享给服务进程),最后通过ipc_recv从服务进程中收到相应的结果
服务进程 —— serve相关
文件在服务进程抽象存在形式是Open,其中有一个Filefd的指针,也存储了文件的相关信息。而在fs/serv.c中,有一个Open的数组opentable
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16struct Open {
struct File *o_file; // 对应的文件控制块
u_int o_fileid; // 文件id(可以看做当前打开的文件,每个都有个id)
int o_mode; // 打开形式
struct Filefd *o_ff; // Filefd
};
struct Filefd {
struct Fd f_fd; // 文件描述符
u_int f_fileid; // 文件id(同上)
struct File f_file; // 文件控制块
};
struct Fd {
u_int fd_dev_id;
u_int fd_offset; // 定位指针
u_int fd_omode; // 打开形式
};serve_xxx
调用底层函数处理用户进程请求首先是从请求req中提取有关信息,如fileid等,从而找到对应的Open(如果是serve_open则是分配Open),就相当于找到了服务进程中对应的文件,这时候就可以调用底层的函数(fs/fs.c)中来把相应的Block从磁盘读入服务进程的内存空间中或对相应的Block进行操作
内核(底层实现)—— fs、kern相关
- 文件的抽象形式是文件控制块File,数据则存在Block中
- 通过调用底层函数来操作文件对应的Block
整体流程
文件服务进程使用 0x10000000 到 0x4fffffff 的地址空间作为磁盘块映射,使用 0x60000000 开始的地址作为所有进程的文件描述符表,用户进程的文件描述符是对这里的表项的共享。而用户进程使用0x60000000 之前的PDMAP再之前的 4M 空间作为文件描述符表,之后的 32(最大文件数目)* 4M 映射文件数据。
当在用户进程下打开文件时,分配 Fd 并传给文件服务进程,后者读取文件,创建 Open 结构体,复制一份文件的 File 结构体拼接成 FileFd 结构体,放在自己的全局文件描述符表中,并将这个共享给用户。用户进程收到之后将让文件服务进程映射全部数据块,并读入数据。最后的结果是文件服务进程,将分散在自己 0x10000000 到 0x4fffffff 的空间的文件的各个数据块,共享到用户进程 0x60000000 之上的属于这个文件的连续的 4M 空间之中。
关闭时,会直接将全部设置为脏,然后全部回写。如果对于打开的文件进行修改大小操作,用户进程会先改文件描述符中 File 结构体的副本,然后让文件服务进程改相应块缓存上的数据