0%

OS_Lab5-实验报告

OS_Lab5 实验报告

一、思考题

Thinking 5.1

  • 问题:如果通过 kseg0 读写设备,那么对于设备的写入会缓存到 Cache 中。这是一种错误的行为,在实际编写代码的时候这么做会引发不可预知的问题。请思考:这么做这会引发什么问题?对于不同种类的设备(如我们提到的串口设备和 IDE 磁盘)的操作会有差异吗?可以从缓存的性质和缓存更新的策略来考虑。
  • 解答
  • 引发的问题kseg0 是经过 Cache 缓存的内存段,而设备寄存器(MMIO)的值反映的是硬件的实时状态。如果读写经过 Cache,CPU 写入命令时可能只是修改了 Cache 行(Write-back 策略下),而没有真实写到硬件寄存器上,导致设备无法启动;同理,CPU 读取设备状态时,可能会直接读到 Cache 中的旧脏数据,而无法感知到硬件中断或就绪状态的变化。
  • 设备操作的差异
  • 串口设备:流式设备。通过缓存会导致连续写入的字符被合并,或者延迟发送;读取时如果 Cache 未失效,可能导致同一个字符被读取多次或漏读用户的按键输入。
  • IDE 磁盘设备:块设备。磁盘操作需要极为严格的时序控制(如写入 LBA 寄存器、发送 0x20 读命令、轮询 STATUS 寄存器等待忙碌位清零等)。如果 Cache 的乱序执行或合并写发生,会直接破坏与磁盘握手的状态机,导致驱动程序死循环卡死(如 wait_ide_ready 无限等待)或读写到完全错误的数据块。

Thinking 5.2

  • 问题:查找代码中的相关定义,试回答一个磁盘块中最多能存储多少个文件控制块?一个目录下最多能有多少个文件?我们的文件系统支持的单个文件最大为多大?
  • 解答
    通过查阅 fs.h 中的宏定义:
  1. 一个磁盘块存储的文件控制块数量:块大小 BLOCK_SIZE 为 4096 字节(4KB),文件控制块 struct File 的大小被填充限制为 FILE_STRUCT_SIZE(256 字节)。因此,一个块最多存储 4096/256=164096 / 256 = 16 个文件控制块(即 FILE2BLK 宏的值)。
  2. 单个文件最大大小:在 struct File 中,直接指针数组 f_direct 的大小 NDIRECT 为 10,一级间接指针 f_indirect 指向的一个磁盘块可以存放 4096/4=10244096 / 4 = 1024 个指针(即 NINDIRECT)。因此,单个文件最大支持 10+1024=103410 + 1024 = 1034 个磁盘块。最大大小为 1034×4KB=4,235,2641034 \times 4\text{KB} = 4,235,264 字节(约 4.03 MB)。
  3. 一个目录下最多能有的文件数:目录在文件系统中也是一种文件,因此它同样受限于单个文件的最大大小(1034 个块)。由于每个块能存 16 个控制块,所以一个目录文件最多能记录 1034×16=16,5441034 \times 16 = 16,544 个文件。(注:部分 JOS/MOS 实现中通过逻辑限制使得文件最大严格为 1024 块,此时最大文件数为 16384 个)

Thinking 5.3

  • 问题:请思考,在满足磁盘块缓存的设计的前提下,我们实验使用的内核支持的最大磁盘大小是多少?
  • 解答
    我们实验使用的内核支持的最大磁盘大小为 1 GB
  • 原因分析:在 MOS 的微内核文件系统服务(fs_serv)中,为了实现磁盘块缓存,服务进程会将整个磁盘的内容线性地映射到自身的虚拟内存空间中。根据 user/include/fs.h 中的定义,磁盘映射的虚拟基地址为 DISKMAP (0x10000000),而规定映射区的最大容量上限为 DISKMAX (0x40000000)。
    因为 DISKMAX 的十六进制值 0x40000000 刚好等于 1024×1024×10241024 \times 1024 \times 1024 字节,即 1 GB。由于内存映射的范围不能超过这个限制,因此内核所能支持的物理磁盘最大也不能超过 1 GB。

Thinking 5.4

  • 问题:在本实验中,fs/serv.h、user/include/fs.h 等文件中出现了许多宏定义,试列举你认为较为重要的宏定义,同时进行解释,并描述其主要应用之处
  • 解答
  1. BLOCK_SIZE (4096):定义了文件系统的基本物理和逻辑块大小(4KB)。应用之处:所有与文件偏移、磁盘映射、内存分配(按页对齐)相关的计算中均作为基本单位基数。
  2. FILE_STRUCT_SIZE (256):定义了单个文件控制块(FCB)的字节大小。应用之处:用于结构体占位符 f_pad 的计算,保证控制块按 256 字节对齐,使得一块物理块能完美容纳 16 个控制块。
  3. DISKMAP (0x10000000):文件系统服务进程将 IDE 磁盘块缓存到内存中的起始虚拟地址。应用之处:在 disk_addr(blockno) 函数中,用于将物理块号转换为服务进程的虚拟地址。
  4. PTE_LIBRARY (页表标志位):MOS 专有的共享内存标志。应用之处:在 fork 以及文件描述符映射时,带有此标志的页不会被置为写时复制(COW),而是直接父子共享,是实现文件系统 IPC 零拷贝数据传输和文件状态共享的核心。

Thinking 5.5

  • 问题:那么 fork 前后的父子进程是否会共享文件描述符和定位指针呢?请在完成上述练习的基础上编写一个程序进行验证。
  • 解答
    会共享。 因为 fd 所在的页面被设置了 PTE_LIBRARY 标志,fork 时不会进行写时复制,而是使得父子进程的虚拟地址映射到同一块物理内存。定位指针 fd_offset 存放在该页面中,因此也是共享的。
    验证程序如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <lib.h>
int main() {
int fd, r;
char buf[10];
// 假设 motd 文件内容为 "0123456789"
if ((fd = open("/motd", O_RDONLY)) < 0) {
user_panic("open failed");
}

if ((r = fork()) == 0) { // 子进程
read(fd, buf, 5);
buf[5] = '\0';
debugf("Child read: %s\n", buf); // 应输出 "01234"
exit();
}

wait(r); // 父进程等待子进程执行完毕

read(fd, buf, 5); // 接着读
buf[5] = '\0';
debugf("Parent read: %s\n", buf); // 应输出 "56789"

return 0;
}

如果输出为连贯的 0123456789,说明子进程读取后,父进程的 fd_offset 也跟着后移了,证明两者完全共享描述符和指针。

Thinking 5.6

  • 问题:请解释 File, Fd, Filefd 结构体及其各个域的作用。比如各个结构体会在哪些过程中被使用,是否对应磁盘上的物理实体还是单纯的内存数据等。
  • 解答
  • struct File (文件控制块)既是物理实体也是内存数据。它存放在磁盘的目录数据块中,记录了文件的元数据(文件名 f_name、大小 f_size、类型 f_type 以及直接/间接磁盘块指针 f_direct/f_indirect)。服务端 fs_serv 在进行路径解析 (walk_path) 和物理块查找 (file_block_walk) 时重度使用它。
  • struct Fd (通用文件描述符)纯内存数据。存放在用户进程的 FDTABLE 内存区。包含设备号 fd_dev_id、读写光标 fd_offset 和打开模式 fd_omode。它是 VFS (虚拟文件系统) 的抽象,用户在调用通用的 read/write 接口时,系统通过 Fd 的设备 ID 分发到不同的底层驱动。
  • struct Filefd (磁盘文件描述符)纯内存数据。它是 Fd 的超集,在内存布局的头部“继承”了 Fd,并在其后附带了文件专属的 IPC 标识号 f_fileid 和服务端文件状态副本 f_file。当底层调用 devfile_read 时,会将抽象的 Fd 强转回 Filefd,提取 f_fileid 并通过 IPC 向 fs_serv 发送读盘请求。

Thinking 5.7

  • 问题:图 5.9 中有多种不同形式的箭头,请解释这些不同箭头的差别,并思考我们的操作系统是如何实现对应类型的进程间通信的。
  • 解答
    在微内核文件系统的架构图(图5.9)中,主要有三种不同含义的交互箭头:
  1. 普通系统调用箭头(User -> Kernel):代表用户进程向内核陷入。OS 通过软中断(SYSCALL 汇编指令)触发异常,进入内核的 handle_sys 恢复现场并分发执行(如 sys_yield, sys_mem_alloc 等)。
  2. 进程间通信箭头(User Process <-> FS Server):代表独立的两个用户态进程之间的交互。OS 通过在内存中约定共享通信页(fsipcbuf)来实现。发送方打包请求参数,通过内核 sys_ipc_try_send 将物理页面共享映射到接收方的虚拟地址空间,并传递 FSREQ_* 类型码;接收方通过 sys_ipc_recv 阻塞接收,处理完成后同样通过 IPC 共享页发回数据或状态码。
  3. 硬件设备 I/O 箭头(FS Server <-> IDE Disk):代表服务端与底层硬件的物理交互。因为服务端在用户态,OS 为其提供了专门的特权系统调用 sys_read_devsys_write_dev,在此调用中,内核将虚拟地址通过直接寻址非缓存段(如 MIPS 的 kseg1),向真实的 IDE 寄存器(如 LBA 地址和 DATA 端口)发起 MMIO 读写指令,完成磁盘扇区的数据传输。

二、难点分析

本次 Lab5 的核心是从无到有在微内核环境下构建文件系统,其逻辑跨度涵盖了硬件读写到用户态库函数的封装,包含以下三大难点:

多级间接指针的磁盘块映射
这是文件系统中负责空间分配的最难逻辑。文件内容的逻辑块和磁盘物理块可能并不连续,尤其当逻辑块号超出 NDIRECT (10) 时,需要通过一级间接指针 f_indirect 去磁盘上读取存储指针的额外数据块。在实现这部分代码时,对于“查询但不分配(alloc=0)”和“遇到空洞需要分配(alloc=1)”的分支处理必须极其严谨。特别是在创建间接块本身时,需要连续调用两次分配和内存映射,且期间任何一步失败都需要抛出错误,否则会引发严重的磁盘块泄露或越权写错误。

跨进程 IPC 通信及缓存同步问题 (fsipc 机制)
在 Lab5 中,普通进程并不能直接读写文件,一切读写操作都被抽象成了发送给 fs_serv 进程的 FSREQ 消息。在调试 fsipc_map 和 fsipc_dirty 等函数时,很容易因为 fsipcbuf 共享页的数据封装类型错误导致服务端解析失败。此外,普通进程写完映射到自己内存里的文件数据后,如果在关闭前忘记通过 fsipc_dirty 通知服务端标记脏页,那么当服务端执行 fs_sync 或者关闭文件时,数据将不会被持久化到 IDE 磁盘中。

位图与设备控制的底层细节 (ide_read / ide_write 及 Bitmap 机制)
在最低层的硬件交互中,通过 LBA28 模式寻址 IDE 磁盘要求熟练掌握位运算,将 28 位的扇区号切割塞入四个不同的寄存器中。同时,通过位图来标记这几十万个磁盘块的分配状况时,涉及到大量的 %32 和 /32 的边界判断操作,一旦发生越界便会引起 user_panic。在这部分的编写中需要时刻留意字节、扇区(512B)和磁盘块(4KB)三者之间的尺寸转换公式。

三、实验体会

文件描述符与抽象能力的升华
通过亲手将 struct Filefd 与 struct Fd 的头部强制对齐对文件进行表示,并在 struct Dev 中填入专门的读写函数指针,我深刻理解了 Linux/Unix 系统下“一切皆文件”的设计哲学。这一层精妙的面向对象思想不仅简化了应用层的调用接口,更是将控制台的串口数据和磁盘的永久数据通过微内核系统完美的融为一体。

调试追踪能力的长足进步
文件系统的错误通常非常隐蔽。比如在写文件大小或者清空文件时少释放了一个磁盘块,可能在当时不会发生任何报错,直到系统运行了很久以后调用了 fs_check 测试或者是由于磁盘满导致挂载 Panic,才会让你追悔莫及。在此期间,通过在底层关键入口安插 debugf,以及反复跟踪 block_is_free 的断言报错点,极大锻炼了我从复杂的调用堆栈中锁定源头代码缺陷的能力。

“权限隔离”与微内核的优雅
当整个 Lab 跑通,在模拟器终端敲出字符的一瞬间,最让我震撼的是:所有这些操作磁盘块的复杂逻辑,统统是运行在一个无特权的用户态进程中的。内核没有几万行的庞大文件系统驱动,只负责提供一个可以读写特殊内存地址的 sys_read_dev 接口。这让曾经在课堂上学到的“微内核鲁棒性”变得真实可触——文件系统挂了,重启一下服务进程就好,整个操作系统的核心依然安然无恙。