os-lab5

本文最后更新于 2024年9月6日 下午

思考题

5.1

引发的问题

  • 当通过 kseg0 对设备进行写操作时,如果写入操作被缓存,那么实际的数据并没有直接写入设备寄存器,而是暂存在缓存中,如果发生断电等情况,缓存中的数据可能会丢失,这可能会导致数据写入失败。

  • 设备物理内存处的数据不只由 CPU 决定,还和对应的外设的行为有关。而缓存只能记录CPU 的读写结果,无法在外设对数据进行改时及时调整。

设备操作的差异

对于串口设备来说,读写频繁,信号多,在相同的时间内发生错误的概论远高于IDE磁盘。
对于磁盘而言,磁盘一次读写的数据量较大。

5.2

根据定义

1
2
3
4
5

#define FILE_STRUCT_SIZE 256

#define BLOCK_SIZE PAGE_SIZE

  • 这代表1个文件控制块大小为256B,而一个磁盘块大小为4096B,一个磁盘块最多可以存的文件控制块:$4096/256=16$

  • 一个目录包含1024个指向磁盘块的指针,即最多有1024 * 16 = 16384个文件。

1
2
3
#define NINDIRECT (BLOCK_SIZE / 4)

#define MAXFILESIZE (NINDIRECT * BLOCK_SIZE)
  • 一个文件控制块有直接指针 + 间接指针共1024个 ,每个指针指向一个磁盘块,存储着该文件的一部分文件数据。文件系统支持的单个文件最大,则表示1024个指针全部有效,一共指向了1024个磁盘块存着文件数据,又一个磁盘块4KB,则单个文件最大为4KB*1024=4MB

5.3

1
2
/* Maximum disk size we can handle (1GB) */
#define DISKMAX 0x40000000

我们实验使用的内核支持的最大磁盘大小为1GB

5.4

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

/*serv.h*/

#define PTE_DIRTY 0x0004 // 文件系统块缓存是脏的
#define SECT_SIZE 512 /* 每个磁盘扇区的字节数 */
#define SECT2BLK (BLOCK_SIZE / SECT_SIZE) /* 扇区转换成块的比率 */

/* 磁盘块n,在内存中,映射到文件系统服务器的地址空间在DISKMAP+(n*BLOCK_SIZE)。*/
#define DISKMAP 0x10000000

/* 我们能处理的最大磁盘大小(1GB) */
#define DISKMAX 0x40000000

/*fs.h*/
#define FILE2BLK (BLOCK_SIZE / sizeof(struct File)) // 文件到块的转换率(一个文件描述符占据的块数)

// 文件描述符中直接块指针的数量

#define NDIRECT 10
#define NINDIRECT (BLOCK_SIZE / 4) // 间接块的数量
#define MAXFILESIZE (NINDIRECT * BLOCK_SIZE) // 文件的最大大小

5.5

fork 前后的父子进程共享文件描述符和定位指针。

验证程序

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#include <lib.h>

int main() {
int fd = open("test.txt", O_RDWR); // 打开(如果需要则创建)一个测试文件
if (fd < 0) {
user_panic("Failed to open file");
return 1;
}

const char *text = "Hello, world!\n";
write(fd, text, strlen(text)); // 向文件写入数据

int pid = fork(); // 创建子进程
if (pid < 0) {
user_panic("fork failed");
close(fd);
return 1;
}
char buf[256];
if (pid == 0) { // 子进程
if (read(fd, buf, 256)<0) {
user_panic("child read : %d", fd);
}
writef("child read is good && child_fd == %d\n",fd);
struct Fd *fdd;
fd_lookup(fd,&fdd);
writef("child_fd's offset == %d\n",fdd->fd_offset);
} else { // 父进程
if(read(fdnum, buf, 511)<0) {
user_panic("father read: %d", fd);
}
writef("father read is good && father_fd == %d\n",r);
struct Fd *fdd;
fd_lookup(fd,&fdd);
writef("father_fd's offset == %d\n",fdd->fd_offset);
}
close(fd);
return 0;
}

5.6

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
struct File {
char f_name[MAXNAMELEN]; // 文件名,长度由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[FILE_STRUCT_SIZE - MAXNAMELEN - (3 + NDIRECT) * 4 - sizeof(void *)];
// 填充字段,用于确保结构体的总大小和对齐
} __attribute__((aligned(4), packed)); // 确保结构体按4字节对齐且打包,无额外空间


struct Fd {
u_int fd_dev_id; // 设备ID,用于标识文件所在的设备
u_int fd_offset; // 文件在设备上的偏移量,用于数据读写位置标识
u_int fd_omode; // 文件或设备的操作模式,例如只读、只写或读写
};

struct Filefd {
struct Fd f_fd; // 包含设备信息和操作模式的文件描述符结构
u_int f_fileid; // 文件ID,用于唯一标识文件
struct File f_file; // 文件的元数据结构体,包含文件名、大小、类型等信息
};

5.7

  • ENV_CREATE(user_env)ENV_CREATE(fs_serv) 由初始化进程 init() 执行以创建用户和文件系统服务环境。init() 启动时创建这些环境,其中 fsuser 环境执行它们的初始化工作。
  • fs 环境初始化后,serv_init()fs_init() 函数运行,进入 serv() 循环,监听 ipc_receive() 的请求并将环境标记为 ENV_NOT_RUNNABLE,直到接收到 user 环境的 ipc_send(fsreq) 请求再变为可运行。
  • user 环境向 fs 环境发送 ipc_send(fsreq) 请求来请求文件访问服务,请求后自身进入 ENV_NOT_RUNNABLE 状态等待响应。响应文件访问请求后,ipc_send(dst_va) 将请求结果发送回 user 环境,此时 fs 环境再次进入 ENV_NOT_RUNNABLE 状态等待下次请求处理。

难点分析

部分代码文件的主要功能(协调理解文件系统的核心框架):

  • tools 目录中的文件是构建时的辅助工具的代码: fsformat 工具 —— 创建磁盘镜像
  • fs 目录中存放的是文件系统处理相关的代码:通过 IPC 通信与用户进程 user/lib/fsipc.c 内的通信函数进行交互
    • fs.c:实现文件系统的基本功能函数
    • ide.c:通过系统调用与磁盘镜像交互
    • serv.c:进程的主干函数
  • user/lib 目录下存放了用户程序的库函数:
    • 系统用户程序库的一部分,抽象操作系统文件系统的文件,以及这些文件和信号源控制的文件。
    • fsipc.c:实现与文件系统服务进程的交互
    • file.c:实现文件系统的用户接口
    • fd.c:实现文件描述符

IDE磁盘驱动(外设控制)

  • 在 MIPS 体系结构下,我们使用 MMIO(内存映射 IO)机制访问设备寄存器。MMIO 使用不同的物理内存地址为设备寄存器编址,将一部分对物理内存的访问 “重定向” 到设备地址空间中。CPU 对这部分物理内存的访问等同于对相应设备的访问。

  • 外设是通过读写寄存器来进行数据通信,设备寄存器通常包括控制寄存器、状态寄存器和数据寄存器,这些寄存器被映射到指定的物理地址空间。

文件系统

  • 磁盘布局:
    MOS 以磁盘最开始的一个磁盘块当作引导扇区和分区表使用。接下来的一个磁盘块作为超级块(Super Block),用来描述文件系统的基本信息。
1
2
3
4
5
struct Super {
uint32_t s_magic; // Magic number: FS_MAGIC
uint32_t s_nblocks; // Total number of blocks on disk
struct File s_root; // Root directory node
};
  • 文件系统结构:

磁盘抽象成由磁盘块组成,每个磁盘块由8个连续的扇区组成,扇区是物理上的结构,而磁盘块是逻辑上存在的。

1
2
3
4
5
6

struct Block { //tools/fsformat.c
uint8_t data[BY2BLK];
uint32_t type;
} disk[NBLOCK];

1
2
3
4
5
6
7
8
9
10
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];
uint32_t f_indirect;

struct File *f_dir; // the pointer to the dir where this file is in, valid only in memory.
char f_pad[FILE_STRUCT_SIZE - MAXNAMELEN - (3 + NDIRECT) * 4 - sizeof(void *)];
} __attribute__((aligned(4), packed));

上述定义了文件控制块,是整个文件系统需要理解的最关键的结构体,其中指明了文件的索引方式,文件控制块的大小,文件控制块的名称

文件系统的用户接口


用户程序在发出文件系统操作请求时,将请求的内容放在对应的结构体中进行消息的传递,fs_serv 进程收到其他进行的 IPC 请求后,IPC 传递的消息包含了请求的类型和其他必要的参数,根据请求的类型执行相应的文件操作(文件的增、删、改、查等),将结果重新通过IPC反馈给用户程序。用户程序在发出文件系统操作请求时,将请求的内容放在对应的结构体中进行消息的传递,fs_serv 进程收到其他进行的 IPC 请求后,IPC 传递的消息包含了请求的类型和其他必要的参数,根据请求的类型执行相应的文件操作(文件的增、删、改、查等),将结果重新通过IPC馈给用户程序。

实验体会

lab5课下总体实验难度较大,涉及较多的结构体。同时综合了lab4的进程ipc问题。
我们首先需要弄明白每个结构体的作用,弄清楚文件系统抽象出来的结构体对应到哪一部分,结构体中的每个属性的作用。
其次,我们需要再次结合lab4的内容,搞清楚文件系统服务的函数调用过程,文件系统服务是一个单独的文件服务进程进行管理的,其他进程需要通过ipc与之通信,才能在进程中对文件进行操作。