关键词:文件系统,IDE磁盘,磁盘驱动

实验内容

概览

  1. 外部存储设备驱动:实现 IDE 磁盘的用户态驱动程序,该驱动程序将通过系统调用的方式陷入内核,对磁盘镜像进行读写。
  2. 文件系统结构:实现模拟磁盘的驱动程序以及磁盘上和操作系统中的文件系统结构,并实现文件系统操作的相关函数。
  3. 文件系统的用户接口:为用户提供接口和机制使得用户程序能够使用文件系统,这主要通过一个用户态的文件系统服务来实现。

文件系统

尽管进程运行时可以在它的地址存储空间存储一定量的信息,但存在:(1) 存储容量受虚拟地址空间大小限制,(2) 进程终止后信息丢失,(3) 不能让多个进程并发访问相关信息,等缺陷。而文件是对磁盘的建模,可以实现长期存储信息。

设备,MMIO与操作系统对设备的控制

  • CPU(程序) 通过读写设备上的寄存器来和设备进行通信

  • MIPS 体系结构使用 **MMIO(内存映射 IO)**机制访问设备寄存器

    • MMIO 使用不同的物理内存地址给设备寄存器编址。这种编址方式将一部分对物理内存的访问"重定向"到设备地址空间中, CPU尝试访问这部分物理内存的时候, 实际上最终是访问了相应的设备
  • MIPS 体系结构中程序需要通过 KSEG1 段访问不可被缓存的外设

  • 优点:可以通过和访问内存相同的方式进行对外设的访问,简化编程接口;可以直接访问外设寄存器,无需中间层来进行访问。

image-20230507094110821

以下是 GXemul的仿真设备的说明:

consoles 0x10000000 A simple console device, for writing characters to the controlling terminal and receiving keypresses.
disk 0x13000000 Disk controller, which can read from and write to emulated IDE disks. It does not use interrupts; read and write operations finish instantaneously.
rtc 0x15000000 A Real-Time Clock, used to retrieve the current time and to cause periodic interrupts.

IDE 磁盘驱动

IDE磁盘驱动位于用户空间,用户态进程直接读写 kseg1 会引发错误,需要通过以下两个系统调用实现用户态读写设备。

系统调用实现设备读写

// va: 用户虚拟地址; pa: 设备的物理地址; len: 读写的长度
int sys_write_dev(u_int va, u_int pa, u_int len)
{
/* Exercise 5.1: Your code here. (1/2) */
// Step1: 检查虚拟地址和物理地址的有效性
if (is_illegal_va_range(va, len) || is_illegal_pa_range(pa, len)) {
return -E_INVAL;
}
// Step2: 把 [va, va+len) 的数据复制到物理地址 pa 处(memcpy, KEG1)
memcpy((void *)(KSEG1 + pa), (void *)va, len);
return 0;
}
int sys_read_dev(u_int va, u_int pa, u_int len) {
/* Exercise 5.1: Your code here. (2/2) */
if (is_illegal_va_range(va, len) || is_illegal_pa_range(pa, len)) {
return -E_INVAL;
}
memcpy((void *)va, (void *)(KSEG1 + pa), len);
return 0;
}

磁盘用户态驱动程序

扇区是磁盘读写的基本单位,在本实验中扇区大小为512字节。GXemul 提供的 Simulated IDE disk 的地址是 0x13000000。

image-20230507102855190

当需要从磁盘的指定位置读取一个扇区时,驱动程序会调用 ide_read 函数来讲磁盘中对应的 sector 的数据读到设备缓冲区中,读取流程如下:

​ 遍历所有要读取的扇区,进行以下操作:

  1. 设置 IDE disk 的 ID,即将 diskno 写入 0xB3000010;
  2. 将相对于磁盘起始位置的偏移量写入 0xB3000000,注意此处偏移量为 curoff=begin+offcuroff = begin + off
  3. 向内存 0xB3000020写入0表示磁盘开始读操作(若写入1,表示磁盘开始写操作);
  4. 从 0xB3000030获取操作的状态返回值,若成功(非0),就将这个sector的数据从设备缓冲区(0xB3004000~0xB30041ff)中拷贝到目标位置(dst + off)。(写操作,需要先将数据放入设备缓存区,然后后向地址 0xB3000020 处写入 1 来启动操作,并从 0xB3000030 处获取写磁盘操作的返回值。)
// diskno: disk的ID; secno: 起始扇区号; dst: 从磁盘读取的数据存储目标位置; nsecs: 读取的扇区数量
void ide_read(u_int diskno, u_int secno, void *dst, u_int nsecs) {
u_int begin = secno * BY2SECT; // 每个扇区512字节
u_int end = begin + nsecs * BY2SECT;

for (u_int off = 0; begin + off < end; off += BY2SECT) {
uint32_t temp = diskno;
u_int curoff = begin + off; // 偏移量
/* Exercise 5.3: Your code here. (1/2) */
// Step1: 设置下一次读操作的磁盘编号
panic_on(syscall_write_dev(&temp, DEV_DISK_ADDRESS + DEV_DISK_ID, sizeof(uint32_t)));
// Step2: 设置下一次读操作时的磁盘镜像偏移的字节数
panic_on(syscall_write_dev(&curoff, DEV_DISK_ADDRESS, sizeof(u_int)));
temp = DEV_DISK_OPERATION_READ;
// Step3: 写 0 开始读磁盘
panic_on(syscall_write_dev(&temp, DEV_DISK_ADDRESS + DEV_DISK_START_OPERATION, sizeof(uint32_t)));
// Step4: 获取读操作的状态返回值
panic_on(syscall_read_dev(&temp, DEV_DISK_ADDRESS + DEV_DISK_STATUS, sizeof(uint32_t)));
if (temp) { // 成功
// Step5: 将读到的数据放入目标虚拟地址
panic_on(syscall_read_dev((dst + off), DEV_DISK_ADDRESS + DEV_DISK_BUFFER, DEV_DISK_BUFFER_LEN));
}
else {
user_panic("ide read failed");
}
}
}
void ide_write(u_int diskno, u_int secno, void *src, u_int nsecs) {
u_int begin = secno * BY2SECT;
u_int end = begin + nsecs * BY2SECT;

for (u_int off = 0; begin + off < end; off += BY2SECT) {
uint32_t temp = diskno;
u_int curoff = begin + off;
/* Exercise 5.3: Your code here. (2/2) */
panic_on(syscall_write_dev(&temp, DEV_DISK_ADDRESS + DEV_DISK_ID, sizeof(uint32_t)));
panic_on(syscall_write_dev(&curoff, DEV_DISK_ADDRESS, sizeof(u_int)));
panic_on(syscall_write_dev((src + off), DEV_DISK_ADDRESS + DEV_DISK_BUFFER, DEV_DISK_BUFFER_LEN));
temp = DEV_DISK_OPERATION_WRITE;
panic_on(syscall_write_dev(&temp, DEV_DISK_ADDRESS + DEV_DISK_START_OPERATION, sizeof(uint32_t)));
panic_on(syscall_read_dev(&temp, DEV_DISK_ADDRESS + DEV_DISK_STATUS, sizeof(uint32_t)));
if (!temp)
user_panic("ide write failed");
}
}

文件系统

image-20230523001708002

image-20230523001852036

思考题

5.1

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

Cache 缓存采用的是写回(Write-Back)策略。当 CPU 需要写入数据时,Cache 缓存会将数据先写入 Cache 缓存中,并标记该数据已修改。当 Cache 缓存需要淘汰该数据时,才将数据写回到内存中。

引发的问题:1. 如果对设备控制寄存器进行了缓存,那么第一次引用的时候就把它放入了缓存,以后再对它的引用都是从高速缓存中取值。例如在设备读取操作中,当程序从设备读取数据时,若该数据已存在于Cache中,程序会直接从Cache读取,此时若设备的数据被其他程序或设备修改,就会导致数据不一致。2. 如果程序意外崩溃或者系统意外重启,Cache中的数据可能会丢失,导致设备数据丢失。

不同种类的设备:串口设备(Serial Port Device)是一种通过串行通信接口与计算机进行数据传输的外设设备。串口通信是一种基于一根数据线和一根时钟线进行数据传输的通信方式,它可以实现两台设备之间的数据传输和通信。串口设备传输速率较低,以字符为单位进行读写,更容易引发问题。而IDE磁盘出现错误的可能性较小。

5.2

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

一个磁盘块大小为 4KB,一个文件控制块大小为256B,则一个磁盘块最多能存储16个文件控制块。文件系统支持的单个文件最大为4MB。一个目录下最多有1024个磁盘块,可以存储4MB内容,最多有16K个文件。

5.3

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

0x500000000x10000000=0x40000000,0x40000000B=1GB0x50000000-0x10000000=0x40000000, 0x40000000B = 1GB

5.4

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

fs/serv,h PTE_DIRTY : 标记缓存在虚拟内存空间的磁盘块是否被修改。

fs.h FILE2BLK (BY2BLK / sizeof(struct File)) : 一个磁盘块可以存储的文件结构数。

fs.h BY2BLK : 一个磁盘块大小

fs.h BIT2BLK : 空闲磁盘块的位图标记

fs.h NINDIRECT : 文件指向的间接磁盘块中块指针的数量

fs.h MAXFILESIZE : 文件最大大小

fs.h BY2FILE : 文件结构体的大小

5.5

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

直接对 test lab=5_4 进行修改:

#include <lib.h>

int main() {
int r;
int fdnum;
char buf[512];
int n;

if ((r = open("/newmotd", O_RDWR)) < 0) {
user_panic("open /newmotd: %d", r);
}
fdnum = r;
debugf("open is good\n");

if ((r = fork()) == 0) {
n = read(fdnum, buf, 5);
debugf("[child] buffer is \'%s\'\n", buf);
} else {
n = read(fdnum, buf, 5);
debugf("[father] buffer is \'%s\'\n", buf);
}

return 0;
}

结果如下:

image-20230522170343963

说明父子进程共享文件描述符和定位指针。

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];// / 文件的直接指针,记录文件的数据块在磁盘上的位置,每个文件控制块设有 10 个直接指针 4KB*10
uint32_t f_indirect;//指向间接磁盘块,存储指向文件内容的磁盘块的指针,不适用前10个指针
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));//编译器将该结构体按4字节对齐,并且取消结构体的填充字节,使结构体大小紧凑

// 文件描述符:允许用户程序使用统一的接口,抽象地操作磁盘文件系统中的文件,以及控制台和管道等虚拟的文件。单纯的内存数据
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; // 对应文件的文件控制块
};

5.7

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

image-20230514211928154

实现箭头表示调用另一个程序中的函数,虚线表示获取调用返回值。

通过 ipc_send()

课上

lab5-2-exam 卡在了 openat 函数忘记 fd_alloc ,等 de 完这个 bug 后来不及写 extra 了。