本次实验主要实现了MOS操作系统的文件系统
Thinking 5.1
如果通过 kseg0 读写设备,那么对于设备的写入会缓存到 Cache 中。这是 一种错误的行为,在实际编写代码的时候这么做会引发不可预知的问题。请思考:这么做 这会引发什么问题?对于不同种类的设备(如我们提到的串口设备和IDE磁盘)的操作会 有差异吗?可以从缓存的性质和缓存更新的策略来考虑。
- 当外部设备产生中断信号或者更新数据时,此时Cache中之前旧的数据可能刚完成缓存,那么完成缓存的这一部分无法完成更新,则会发生错误的行为。
- 对于串口设备来说,读写频繁,信号多,在相同的时间内发生错误的概论远高于IDE磁盘。
Thinking 5.2
查找代码中的相关定义,试回答一个磁盘块中最多能存储多少个文件控制 ?一个目录下最多能有多少个文件?我们的文件系统支持的单个文件最大为多大?
- 一个磁盘块的大小为 4KB,一个文件控制块的大小为 256B,一个磁盘块中最多有 $2^{12}/2^8=16$ 个文件控制块。
- 一个目录最多指向 1024 个磁盘块,每一个磁盘块有最多有 16 个文件控制块,所以一个目录最多有 1024 * 16 = 16K 个文件。
- 一个文件最多指向 1024 个磁盘块,每个磁盘块 4KB,单个文件系统支持的最大文件为 1024 * 4KB = 4MB。
Thinking 5.3
请思考,在满足磁盘块缓存的设计的前提下,我们实验使用的内核支持的最大磁盘大小是多少?
- 实验中将 DISKMAP ~ DISKMAP+DISKMAX 这一段虚存地址空间作为缓冲区。
- DISKMAX = 0x40000000,因此最多处理 1GB。
Thinking 5.4
在本实验中,fs/serv.h、user/include/fs.h 等文件中出现了许多宏定义, 试列举你认为较为重要的宏定义,同时进行解释,并描述其主要应用之处。
// 磁盘与内存交互相关宏
#define SECT_SIZE 512 // 磁盘扇区大小(硬件最小读写单位,通常为512字节)
#define SECT2BLK (BLOCK_SIZE / SECT_SIZE) // 一个文件系统块对应的磁盘扇区数(用于磁盘IO参数计算)
#define DISKMAP 0x10000000 // 磁盘块在文件系统服务器地址空间的映射基址(块n映射至 DISKMAP + n*BLOCK_SIZE)
// 文件系统块管理相关宏
#define BLOCK_SIZE PAGE_SIZE // 文件系统基本块大小(与内存页大小一致,通常为4096字节,最小管理单元)
#define FILE2BLK (BLOCK_SIZE / sizeof(struct File)) // 单个块可存储的目录项(struct File)数量(用于目录存储管理)
// 文件结构设计相关宏
#define NDIRECT 10 // 文件直接块指针数量(小文件通过直接指针快速访问)
#define NINDIRECT (BLOCK_SIZE / 4) // 间接块可管理的块指针数量(4字节为指针大小,扩展大文件容量)
#define MAXFILESIZE (NINDIRECT * BLOCK_SIZE) // 文件最大大小(由间接块管理的总块数决定)
// 元数据与校验相关宏
#define FS_MAGIC 0x68286097 // 文件系统魔数(超级块标识,用于验证文件系统有效性)
#define PTE_DIRTY 0x0004 // 页表脏标记(标记内存块是否被修改,用于缓存同步策略)
// 文件类型相关宏
#define FTYPE_REG 0 // 普通文件标识
#define FTYPE_DIR 1 // 目录标识
Thinking 5.5
在Lab4“系统调用与fork”的实验中我们实现了极为重要的fork函数。那 么fork前后的父子进程是否会共享文件描述符和定位指针呢?请在完成上述练习的基础上 编写一个程序进行验证。
// 文件 /test 中的内容为 0123456789
fdnum = open("/test", O_RDWR);
if((r = fork()) == 0) { //子进程
n = read(fdnum, buf, 5);
debugf("child : %s\n", buf);
} else {
n = read(fdnum, buf, 5);
debugf("father : %s\n", buf);
}
// 输出为:
// child : 01234
// father : 56789
Thinking 5.6
请解释 File, Fd, Filefd 结构体及其各个域的作用。比如各个结构体会在哪 些过程中被使用,是否对应磁盘上的物理实体还是单纯的内存数据等。说明形式自定,要 求简洁明了,可大致勾勒出文件系统数据结构与物理实体的对应关系与设计框架。
// File结构体表示一个文件或目录的元数据,既存在于磁盘上的物理结构,也存在于文件系统服务进程的内存中。
struct File {
char f_name[MAXNAMELEN]; // 文件名或目录名的字符串数组
uint32_t f_size; // 文件或目录占据的存储空间大小,以字节为单位
uint32_t f_type; // 文件的类型标识,用于区分文件和目录
uint32_t f_direct[NDIRECT]; // 直接指针数组,指向包含文件内容的磁盘块地址
uint32_t f_indirect; // 间接指针,指向一个磁盘块,该磁盘块存储了指向文件内容的其他磁盘块地址
struct File *f_dir; // 指向该文件所属目录的指针,此字段仅在内存中表示有效
char f_pad[BY2FILE - MAXNAMELEN - (3 + NDIRECT) * 4 - sizeof(void *)]; // 用于字节对齐的填充字段,确保结构体大小为256字节
} __attribute__((aligned(4), packed));
// Fd结构体代表一个文件描述符,它存储在内存中,包含了关于文件操作的上下文信息。
struct Fd {
u_int fd_dev_id; // 设备标识符,用于识别文件所属的外设类型
u_int fd_offset; // 文件偏移量,表示当前文件操作的读写位置
u_int fd_omode; // 文件打开模式,指定了文件的操作权限,如读写、只读、只写等
};
// Filefd结构体结合了文件描述符和文件元数据,提供了对文件操作的完整上下文。
struct Filefd {
struct Fd f_fd; // 文件描述符,包含设备ID、偏移量和打开模式
u_int f_fileid; // 文件标识符,用于唯一标识一个文件
struct File f_file; // 文件元数据,包含了文件名、大小、类型等属性
};
Thinking 5.7
图 5.9 中有多种不同形式的箭头,请解释这些不同箭头的差别,并思考我们 的操作系统是如何实现对应类型的进程间通信的。
- ENV_CREATE(user_env) 和 ENV_CREATE(fs_serv) 都是异步消息,由 init() 发出创建消息后, init() 函数即可返回执行后续步骤,由 fs 和 user 线程执行自己的初始化工作。
- fs 线程初始化 serv_init() 和 fs_init() 完成后,进入 serv() 函数,被 ipc_receive() 阻塞为ENV_NOT_RUNNABLE ,直到收到 user 线程的 ipc_send(fsreq) 被唤醒。
- user 线程向 fs 线程 ipc_send(fsreq) 发送请求为同步消息,发送后自身进入阻塞ENV_NOT_RUNNABLE 等待被唤醒的 fs 线程服务结束时 ipc_send(dst_va),用户线程接收到数据后继续运行,此后 fs 线程进入阻塞,等待下次被用户唤醒。
实验难点
- 用户态读写:在用户态实现IDE磁盘驱动,需通过系统调用 sys_read_dev 和sys_write_dev 操作硬件寄存器,需准确理解 MMIO(内存映射I/O)机制,处理物理地址到内核虚拟地址(kseg1段)的转换。
- 文件数据结构:理清 File (文件控制块)、 Fd (文件描述符)、 Filefd (文件描述符与文件元数据结合)等结构体的关系,尤其是 File 中直接指针与间接指针的逻辑,以及目录文件中文件控制块的组织与查找(dir_lookup 函数)。
- 虚拟内存映射:利用虚拟内存实现了磁盘块缓存,减少了磁盘IO次数,在此过程中需处理磁盘块号到虚拟地址的映射、物理页面的动态分配,并确保缓存一致性,值得注意的是当释放一个磁盘块的映射时,如果脏页标记为真,应将内容写回磁盘。
- IPC 机制:文件系统服务本身也是一个用户进程,需通过IPC与用户进程交互。通过同步通信逻辑 ipc_send / ipc_recv,确保数据在不同进程地址空间的正确传递。
实验感想
本次实验的主要目的是搭建一个简单的文件系统,需要理解的是文件系统服务本身也是一个用户进程,其设计层次分明,从磁盘驱动到文件系统核心逻辑,再到用户接口,这样的分层设计降低了复杂度。在这个过程中,我体会到抽象层(如将磁盘块抽象为文件控制块)对上层接口实现的支撑作用,也感受到了操作系统文件管理的复杂性,理解了为何操作系统的 50% 以上代码都用于文件管理。