BUAA-OS-lab5
Lab5实验报告
思考题
Thinking 5.1
如果通过 kseg0 读写设备,那么对于设备的写入会缓存到 Cache 中。这是一种错误的行为,在实际编写代码的时候这么做会引发不可预知的问题。
请思考:这么做这会引发什么问题?对于不同种类的设备(如我们提到的串口设备和 IDE 磁盘)的操作会有差异吗?可以从缓存的性质和缓存更新的策略来考虑。
- 解:
- 当外部设备产生中断信号或者更新数据时,此时Cache中之前旧的数据可能刚完成缓存,那么完成缓存的这一部分无法完成更新,则会发生错误的行为。
- 对于串口设备来说,读写频繁,信号多,在相同的时间内发生错误的概论远高于IDE磁盘。
Thinking 5.2
查找代码中的相关定义,试回答一个磁盘块中最多能存储多少个文件控制块?一个目录下最多能有多少个文件?我们的文件系统支持的单个文件最大为多大?
-
解:
1
char f_pad[BY2FILE - MAXNAMELEN - (3 + NDIRECT) * 4 - sizeof(void *)];
- 文件控制块结构体中,有这样的一个属性
f_pad
,大小为BY2FILE
即 ,则代表一个文件控制块的大小为 ,那么一个磁盘块 最多存储 个文件控制块。 - 一个目录 = 一个磁盘块全部用来存目录项,又一个目录项;则一个目录 ,一个目录中有 个目录项;又一个目录项指向一个存有文件内容的 磁盘块,一个磁盘块中有16个文件控制块,则一个目录下最多有 个文件。
- 一个文件控制块,描述一个文件;一个文件有 直接指针 + 间接指针 共1024个 ,每个指针指向一个磁盘块,存储着该文件的一部分文件数据。文件系统支持的单个文件最大,则表示1024个指针全部有效,一共指向了1024个磁盘块存着文件数据,又一个磁盘块 ,则单个文件最大 。
- 文件控制块结构体中,有这样的一个属性
Thinking 5.3
请思考,在满足磁盘块缓存的设计的前提下,我们实验使用的内核支持的最大磁盘大小是多少?
-
解:
- 由于将
DISKMAP ~ DISKMAP+DISKMAX
这一段虚存地址空间作为缓冲区,DISKMAX
= ,因此最多处理 。
- 由于将
Thinking 5.4
在本实验中, fs/serv.h、 user/include/fs.h 等文件中出现了许多宏定义,试列举你认为较为重要的宏定义,同时进行解释,并描述其主要应用之处。
-
解:
-
user/include/fd.h
这两个宏用来找fd
对应的文件信息页面和文件缓存区地址1
2 -
文件服务函数调用号,可以重用 $user/lib/fsipc.c $ 的
fsipc()
函数,作为进程间通信的值,用户进程传给文件服务系统的 的serve()
1
2
3
4
5
6
7
-
Thinking 5.5
在 Lab4“系统调用与 fork”的实验中我们实现了极为重要的 fork 函数。那么 fork 前后的父子进程是否会共享文件描述符和定位指针呢?请在完成上述练习的基础上编写一个程序进行验证。
-
解:
-
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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
static char *msg = "This is the NEW message of the day!\n\n";
static char *diff_msg = "This is a different massage of the day!\n\n";
void umain()
{
int r;
int fdnum;
char buf[512];
int n;
if ((r = open("/newmotd", O_RDWR)) < 0) {
user_panic("open /newmotd: %d", r);
}
fdnum = r;
writef("open is good\n");
if ((n = read(fdnum, buf, 511)) < 0) {
user_panic("read /newmotd: %d", r);
}
if (strcmp(buf, diff_msg) != 0) {
user_panic("read returned wrong data");
}
writef("read is good\n");
int id;
if ((id = fork()) == 0) {
if ((n = read(fdnum, buf, 511)) < 0) {
user_panic("child read /newmotd: %d", r);
}
if (strcmp(buf, diff_msg) != 0) {
user_panic("child read returned wrong data");
}
writef("child read is good && child_fd == %d\n",r);
struct Fd *fdd;
fd_lookup(r,&fdd);
writef("child_fd's offset == %d\n",fdd->fd_offset);
}
else {
if((n = read(fdnum, buf, 511)) < 0) {
user_panic("father read /newmotd: %d", r);
}
if (strcmp(buf, diff_msg) != 0) {
user_panic("father read returned wrong data");
}
writef("father read is good && father_fd == %d\n",r);
struct Fd *fdd;
fd_lookup(r,&fdd);
writef("father_fd's offset == %d\n",fdd->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
26
27
28
29
30
31
32
33
34
35
36
37main.c: main is start ...
init.c: mips_init() is called
Physical memory: 65536K available, base = 65536K, extended = 0K
to memory 80401000 for struct page directory.
to memory 80431000 for struct Pages.
pmap.c: mips vm init success
pageout: @@@___0x7f3fe000___@@@ ins a page
pageout: @@@___0x40d000___@@@ ins a page
FS is running
FS can do I/O
pageout: @@@___0x7f3fe000___@@@ ins a page
pageout: @@@___0x407000___@@@ ins a page
superblock is good
diskno: 0
diskno: 0
read_bitmap is good
diskno: 0
alloc_block is good
file_open is good
file_get_block is good
file_flush is good
file_truncate is good
diskno: 0
file rewrite is good
serve_open 00000400 ffff000 0x2
open is good
read is good
father read is good && father_fd == 0
father_fd's offset == 41
[00000400] destroying 00000400
[00000400] free env 00000400
i am killed ...
child read is good && child_fd == 0
child_fd's offset == 41
[00001402] destroying 00001402
[00001402] free env 00001402
i am killed ...
-
Thinking 5.6
请解释 File, Fd, Filefd 结构体及其各个域的作用。比如各个结构体会在哪些过程中被使用,是否对应磁盘上的物理实体还是单纯的内存数据等。说明形式自定,要求简洁明了,可大致勾勒出文件系统数据结构与物理实体的对应关系与设计框架。
-
解:
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
29struct File {
char f_name[MAXNAMELEN]; // 文件名称,最大长度MAXNAMELEN值为128
uint32_t f_size; // 文件的大小,单位:字节
uint32_t f_type; // 文件类型: 普通文件FTYPE_REG,目录FTYPE_DIR
uint32_t f_direct[NDIRECT];// 文件的直接指针,用来记录文件的数据块在磁盘上的位置
uint32_t f_indirect;// 指向一个间接磁盘块,用来存储指向文件内容的磁盘块的指针
struct File *f_dir; // 指向文件所属的文件目录
char f_pad[BY2FILE - MAXNAMELEN - (3 + NDIRECT) * 4 - sizeof(void *)];
//为了让整数个文件结构体占用一个磁盘块,填充结构体中剩下的字节
} __attribute__((aligned(4), packed));
struct Fd {
u_int fd_dev_id; // 外设的id。
//用户使用fd.c的文件接口时,不同的dev_id会调取不同的文件服务函数。
//比如File类型的文件服务函数为user/File.c的file_*()函数。
u_int fd_offset; // 读写的偏移量
//seek()时修改。
//offset会被用来找起始filebno文件块号。
u_int fd_omode; // 打开方式,包括只读、只写、读写
//req和open结构体都会用到
};
struct Filefd {
struct Fd f_fd; // file descriptor
u_int f_fileid; // 文件的id
//模1024后会用来在opentab[]里索引open结构体
struct File f_file; // 对应文件的文件控制块
};- 结构体均为内存数据,记录了文件信息。
Filefd
以及fd
中的指向的文件控制块File
中记录的磁盘指针对应物理实体。
Thinking 5.7
下图(文件系统服务时序图)中有多种不同形式的箭头,请解释这些不同箭头的差别,并思考我们的操作系统是如何实现对应类型的进程间通信的。
- 解:
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
线程进入阻塞,等待下次被用户唤醒。
实验体会
文件系统概述
- 文件设备(,即狭义的“文件”)、控制台()和管道()
文件系统的设计与实现
核心代码文件的主要功能(协助理解文件系统实现的总体框架):
- 目录中存放的是构建时辅助工具的代码: 工具 ——创建磁盘镜像
- 目录中存放的是文件系统服务进程的代码:通过IPC通信与用户进程 内的通信函数进行交互
- :实现文件系统的基本功能函数
- :通过系统调用与磁盘镜像进行交互
- :进程的主干函数
- 目录下存放了用户程序的库函数:允许用户程序使用统一的接口,抽象地操作磁盘文件系统中的文件,以及控制台和管道等虚拟的文件。
- :实现与文件系统服务进程的交互
- :实现文件系统的用户接口
- :实现文件描述符
一、IDE磁盘驱动
该驱动程序将通过系统调用的方式陷入内核,对磁盘镜像进行读写操作(按一定顺序读写设备寄存器,来实现对外部设备的操作)。
- :一个简单的控制台驱动程序
- :在内核下使用内存映射I/O(MMIO)技术,通过控制台实现字符的输入输出
- 磁盘驱动程序采用 技术编写驱动,且本次要编写的驱动程序完全运行在用户空间
1、MMIO:内存映射I/O
外设是通过读写设备上的寄存器来进行数据通信,外设寄存器也称为 I/O 端口,主要用来访
问 I/O 设备。外设寄存器通常包括控制寄存器、状态寄存器和数据寄存器,这些寄存器被映射
到指定的物理地址空间。 (例如:在 GXemul 中, console 设备被映射到 0x10000000, simulated IDE disk 被映射到 0x13000000,等等 )
在 MIPS 的内核地址空间中(kseg0 和 kseg1 段)实现了硬件级别的物理地址和内核虚拟地址的转换机制,其中,对 段地址的读写不经过 映射,且不使用高速缓存,这正是外部设备驱动所需要的。因为我们是模拟的,所以可以通过简单地读写某些固定的内核虚拟地址来实现驱动程序的功能。
-
物理地址 ==> 段的内核虚拟地址 :
KADDR()
,给物理地址加上 (ULIM的值)物理地址 ==> 段的内核虚拟地址 :给物理地址加上
Exercise 5.1+5.2
IDE 磁盘驱动程序位于用户空间,但是用户进程不可以直接写内核虚拟地址,故而利用系统调用sys_write_dev
和 sys_read_dev
来实现。
-
syscall_write_dev
函数:1
2int syscall_write_dev(void *va, u_int dev, u_int len); //user/lib/syscall_lib.c
int sys_write_dev(u_int va, u_int pa, u_int len); //kern/syscall_all.c-
va
:用户虚拟地址dev
、pa
:设备的物理地址len
:读写的长度(按字节计数) -
使用例子:
1
panic_on(syscall_write_dev(&temp, DEV_DISK_ADDRESS + DEV_DISK_ID, 4)); //fs/ide.c
-
-
syscall_read_dev
函数:1
2int syscall_read_dev(void *va, u_int dev, u_int len); //user/lib/syscall_lib.c
int sys_read_dev(u_int va, u_int pa, u_int len); //kern/syscall_all.c-
va
:用户虚拟地址dev
、pa
:设备的物理地址len
:读写的长度(按字节计数) -
使用例子:
1
panic_on(syscall_read_dev(dst, DEV_DISK_ADDRESS + DEV_DISK_BUFFER, BY2SECT)); //fs/ide.c
-
2、IDE磁盘
磁盘的物理结构
- 扇区 (sector):扇区是磁盘执行读写操作的单位,一般是 512 字节;
- 磁道 (track): 盘片上以盘片中心为圆心,不同半径的同心圆;
- 柱面 (cylinder): 硬盘中,不同盘片相同半径的磁道所组成的圆柱面;
- 磁头 (head): 每个磁盘有两个面,每个面都有一个磁头;
IDE 磁盘操作
GXemul 提供的 的地址是 ,I/O 寄存器各偏移和对应功能如下:
偏移 | 效果 | 数据位宽 |
---|---|---|
0x0000 | 写:设置下一次读写操作时的磁盘镜像偏移的字节数 | 4 字节 |
0x0008 | 写:设置高 32 位的偏移的字节数 | 4 字节 |
0x0010 | 写:设置下一次读写操作的磁盘编号 | 4 字节 |
0x0020 | 写:开始一次读写操作(写 0 表示读操作, 1 表示写操作) | 4 字节 |
0x0030 | 读:获取上一次操作的状态返回值 (读 0 表示失败,非 0 则 表示成功) |
4 字节 |
0x4000~0x41ff | 读/写: 512 字节的读写缓存 | —— |
3、驱动程序编写
-
read_sector
函数:1
extern int read_sector(int diskno, int offset);
函数具体实现:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21# read sector at specified offset from the beginning of the disk image.
LEAF(read_sector)
sw a0, 0xB3000010
# 设置IDE disk的ID,根据函数声明diskno是第一个参数,故而对应$a0寄存器
# 0xB3000010=0x13000000+0x0010+0xA0000000 存储下一次读写操作的磁盘编号
# 这句话表示我们将使用编号为 $a0 的磁盘
sw a1, 0xB3000000
# 设置IDE disk的offset,根据函数声明offset是第二个参数,故而对应$a1寄存器
# 0xB3000000=0x13000000+0xA0000000 存储下一次读写操作时的磁盘镜像偏移的字节数
# 这句话表示我们将在距离磁盘起始处offset 的位置开始进行磁盘操作
li t0, 0
sw t0, 0xB3000020
# 0xB3000020=0x13000000+0x0020+0xA0000000 选择读/写操作
# 这两句话置0,表示我们将读磁盘
lw v0, 0xB3000030
# 0xB3000030=0x13000000+0x0030+0xA0000000 获取上一次操作的状态返回值
# 这句话表示将磁盘操作的状态码放入$v0中
nop
jr ra
nop
END(read_sector)- 当需要从磁盘的指定位置读取一个扇区时,内核驱动会调用
read_sector
函数来将磁盘中对应 sector 的数据读到设备缓冲区中。- 【注意】 所有的地址操作都需要将物理地址转换成虚拟地址。此处设备基地址对应的 的内核虚拟地址是 。
- 通过判断
read_sector
函数的返回值,就可以知道读取磁盘的操作是否成功。如果成功,将这个 sector 的数据 (512 bytes) 从设备缓冲区 (offset ) 中拷贝到目的位置。至此,就完成了对磁盘的读操作。
- 当需要从磁盘的指定位置读取一个扇区时,内核驱动会调用
Exercise 5.3
-
:为什么
ide_read
针对 的操作,是在判断 处状态返回值之后进行;相反ide_write
针对 的操作,是在判断 处状态返回值之前进行呢?答:
- 读的时候 这片 是由硬盘设备完成写入的,检查 是检查硬盘写入 有没有问题;
- 写的时候 这片 是由驱动完成写入的,检查 是检查驱动写入 有没有问题,所以要先等驱动写完。
【这里的实现其实因为是在模拟器里面 所以有简化,实际上读status 它有可能返回一些正在操作中的flag,正确方法应该是循环忙等status】
-
ide_read()
函数(fs/ide.c)-
声明:
1
2
3
4
5void ide_read(u_int diskno, u_int secno, void *dst, u_int nsecs);
//diskno: 待操作的磁盘编号
//secno: 第一个待操作的扇区编号
//dst: 读出的数据存到地址为dst的地方去
//nescs: 待操作的扇区数量 -
作用:读磁盘
-
实现:使用系统调用,参照 I/O 寄存器各偏移和对应功能赋值
-
使用例子:
1
2//fs/fs.c ==> int read_block(u_int blockno, void **blk, u_int *isnew)
ide_read(0, blockno * SECT2BLK, va, SECT2BLK);
-
-
ide_write()
函数(fs/ide.c)-
声明:
1
2
3
4
5void ide_write(u_int diskno, u_int secno, void *src, u_int nsecs);
//diskno: 待操作的磁盘编号
//secno: 第一个待操作的扇区编号
//src: 把地址src处存的内容写到磁盘指定区域去
//nescs: 待操作的扇区数量 -
作用:写磁盘
-
实现:使用系统调用,参照 I/O 寄存器各偏移和对应功能赋值
-
使用例子:
1
2//fs/fs.c ==> void write_block(u_int blockno)
de_write(0, blockno * SECT2BLK, va, SECT2BLK);
-
二、文件系统结构
实现模拟磁盘的驱动程序以及磁盘上和操作系统中的文件系统结构,并实现文件系统操作的相关函数。
- 目录:文件系统服务程序的代码
- 目录的 文件:文件系统的用户库(允许用户程序调用其中
的函数来操作文件系统 )- 实现了与文件系统服务进程基于 IPC 的交互
- 实现了文件系统的用户接口
- 实现了文件描述符相关操作
文件系统服务进程和其他用户进程之间使用 Lab4 中实现的 IPC机制进行通信。
1、磁盘文件系统布局
-
Block 磁盘块:
1
2
3
4struct Block { //tools/fsformat.c
uint8_t data[BY2BLK];
uint32_t type;
} disk[NBLOCK];- 性质:是虚拟概念,是操作系统与磁盘==交互==的最小单位;
- 目的:减小因扇区过多带来的寻址困难;(潜台词磁盘比扇区大)
- 构成:操作系统决定磁盘块的大小(通常为 个扇区),将相邻的 个扇区组合在一起,形成磁盘块进行整体操作;
【区分扇区】:扇区是真实存在的,是磁盘==读写==的基本单位,与操作系统无关。
-
Super Block 超级块:
1
2
3
4
5struct Super {
u_int s_magic; // 魔数: FS_MAGIC(常量),标识本文件系统
u_int s_nblocks; // 1024,为本文件系统的磁盘块个数
struct File s_root; // 根目录,f_type = FTYPE_DIR, f_name = "/"
}; -
管理空闲磁盘资源:位图法(Bitmap)
:创建符合我们定义的文件系统镜像,将多个文件按照内核所定义的文件系统写入到磁盘镜像中 。
-
init_disk
函数( tools/fsformat.c )-
声明:
1
void init_disk();
-
作用:将所有的块都标记为空闲块
-
实现:计算所有磁盘块数量 ==> 把位图块置1 ==> 把最后一块位图块的靠后一部分置0 ==> 手动初始化值
【注】:如果位图还有剩余,不能将最后一块位图块中靠后的一部分内容标记为空闲,因为这些位所对应的磁盘块并不存在,是不可使用的 。
-
-
block_is_free
函数( fs/fs.c )-
声明:
1
int block_is_free(u_int blockno);
-
作用:判断指定磁盘磁盘块是否被占用
-
实现:通过位图中的特定位来判断
-
使用例子:
1
2
3
4//fs/fs.c ==> int read_block(u_int blockno, void **blk, u_int *isnew)
if (bitmap && block_is_free(blockno)) {
user_panic("reading free block %08x\n", blockno);
}
-
Exercise 5.4
-
:为什么参数
blockno
的值不能为 0?答:因为0号磁盘块存的是磁盘的信息 ,不可以被清除,也就是不能释放置为空闲。
-
free_block
函数(fs/fs.c)-
声明:
1
void free_block(u_int blockno);
-
作用:释放磁盘块号为
blockno
的磁盘块,置为空闲,即清除 -
实现:
1
2bitmap[blockno / 32] |= (1 << (blockno % 32)); //把blockno磁盘块的bitmap置空闲
bitmap[blockno / 32] &= (~(1 << (blockno % 32))); //把blockno磁盘块的bitmap置已用 -
使用例子:
1
free_block(bno); //fs/fs.c ==> int alloc_block(void)
-
2、文件系统详细结构
-
文件控制块: 结构体(用于描述和管理文件)
1
2
3
4
5
6
7
8
9
10
11struct File {
char f_name[MAXNAMELEN]; // 文件名称,最大长度MAXNAMELEN值为128
uint32_t f_size; // 文件的大小,单位:字节
uint32_t f_type; // 文件类型: 普通文件FTYPE_REG,目录FTYPE_DIR
uint32_t f_direct[NDIRECT];// 文件的直接指针,用来记录文件的数据块在磁盘上的位置
uint32_t f_indirect;// 指向一个间接磁盘块,用来存储指向文件内容的磁盘块的指针
struct File *f_dir; // 指向文件所属的文件目录
char f_pad[BY2FILE - MAXNAMELEN - (3 + NDIRECT) * 4 - sizeof(void *)];
//为了让整数个文件结构体占用一个磁盘块,填充结构体中剩下的字节
} __attribute__((aligned(4), packed));-
f_type
:- 普通文件
FTYPE_REG
:其指向的磁盘块存储着文件内容 - 目录
FTYPE_DIR
:其指向的磁盘块存储着该目录下各个文件对应的的文件控制块
- 普通文件
-
f_direct[NDIRECT]
:每个文件控制块设有 10 个直接指针;
每个磁盘块的大小为 4KB,所以上述 10 个直接指针能够表示最大 40KB 的文件,而当文件的大小大于 40KB 时,就需要用到间接指针。
-
f_indirect
:为了简化计算,规定不使用间接磁盘块的前十个指针(10个直接指针)。
-
使用方法:
1
2
3
4
5
6//int i; int bno; struct File *dirf;
if (i < NDIRECT) {
bno = dirf->f_direct[i];
} else {
bno = (disk[dirf->f_indirect].data)[i];
}
-
-
-
查找某个文件:
- 从超级块中读取根目录的文件控制块;
- 沿着目标路径,挨个查看当前目录所包含的文件是否与下一级目标文件同名,如此便能查找到最终的目标文件。
-
镜像文件 作用:模拟与真实的磁盘文件设备的交互
Exercise 5.5
-
write_file
函数(tools/fsformat.c)【了解即可】-
声明:
1
void write_file(struct File *dirf, const char *path);
-
作用:
用于将宿主机上路径为
path
的文件写入磁盘镜像中dirf
所指向的文件控制块所代表的目录下 -
使用例子:
1
2//tools/fsformat.c ==> void write_directory(struct File *dirf, char *path)
write_file(pdir, buf);
-
-
write_directory
函数(tools/fsformat.c)【了解即可】-
声明:
1
void write_directory(struct File *dirf, char *path);
-
作用:
用于将宿主机上路径为
path
的目录(及其下所有目录和文件)写入磁盘镜像中dirf
所指向的文件控制块所代表的目录下 -
使用例子:
1
2//tools/fsformat.c ==> int main(int argc, char **argv)
write_directory(&super.s_root, name);
-
-
create_file
函数(tools/fsformat.c)-
声明:
1
struct File *create_file(struct File *dirf);
-
作用:
在
dirf
指向的文件控制块所代表的目录下分配一个文件控制块,并返回指向新分配的文件控制块的指针 -
实现:
- 遍历
dirf
所代表的目录拥有的每一个磁盘块(包括直接和间接指向的磁盘块); - 对每一个磁盘块,从头遍历该磁盘块上的所有文件控制块,找到第一个空的文件控制块并返回该文件控制块指针(空的文件控制块即文件控制块中
f->f_name[0] == '\0'
) 。
- 遍历
-
使用例子:
1
2//tools/fsformat.c ==> void write_directory(struct File *dirf, char *path)
struct File *pdir = create_file(dirf);
-
-
: 如何将一个文件或指定目录下的文件按照目录结构写入到 ?
答:上面的三个函数。
-
make_link_block
函数(tools/fsformat.c)-
作用:申请一个新的磁盘块,返回它的
blockno
(副作用:已经自动将其归属于dirf
目录) -
使用例子:
int blockno = make_link_block(dirf, nblk);
其中,
dirf
是目录的文件控制块;nblk
是前面目录中目前已有的磁盘块数量。
-
-
next_block
函数(tools/fsformat.c)- 作用:得到下一个空闲磁盘块的
blockno
,并将该磁盘块的type
设置成传入参数的值 - 使用例子:
int bno = next_block(BLOCK_FILE);
- 作用:得到下一个空闲磁盘块的
-
save_block_link
函数(tools/fsformat.c)- 作用:将
blockno
对应的磁盘块,连接归属到dirf
目录中 (【注意】后续需要我们自己额外手动改变dirf->f_size += BY2BLK
的大小) - 使用例子:
save_block_link(dirf, nblk, bno);
nblk
是dirf
中目前已有的磁盘块数量
- 作用:将
3、块缓存
借助虚拟内存来实现磁盘块缓存的设计,文件系统服务 是一个用户进程,一个进程可以拥有 的虚拟内存空间,将 DISKMAP ~ DISKMAP+DISKMAX
这一段虚存地址空间 (0x10000000-0x4fffffff) 作为缓冲区,当磁盘读入内存时,缓冲区用来映射相关的页。
建立起磁盘地址空间和进程虚存地址空间之间的缓存映射:
-
read_block
函数(fs/fs.c)-
声明:
1
int read_block(u_int blockno, void **blk, u_int *isnew);
-
作用:
- 将指定编号的磁盘块读入到内存中,并将
blockno
对应磁盘块的虚拟地址存到(*blk)
当中; - 如果
isnew
是非0的有效地址,就进行操作: 当blockno
对应磁盘块原本就已经mapped
的时候,就让*isnew = 0;
,反之,让*isnew = 1;
- 将指定编号的磁盘块读入到内存中,并将
-
实现:首先,检查这块磁盘块是否已经在内存中,如果不在,先用
syscall_mem_alloc
分配一页物理内存; 然后调用ide_read
函数来读取磁盘上的数据到对应的虚存地址处。 -
使用例子:
1
2
3
4//fs/fs.c ==> void read_super(void)
if ((r = read_block(1, &blk, 0)) < 0) {
//...
}
-
-
write_block
函数(fs/fs.c)-
声明:
1
void write_block(u_int blockno);
-
作用:把内存中的
block
的内容写到磁盘disk
对应的磁盘块里(两个东东用的是同一个blockno
)(Write the current contents of the block out to disk.)注意区分 block 和 disk
-
实现:函数
diskaddr
和ide_write
-
使用例子:
1
write_block(blockno);
-
-
file_get_block
函数(fs/fs.c)-
声明:
1
int file_get_block(struct File *f, u_int filebno, void **blk);
-
作用:令
(*blk)
赋值为文件控制块f
对应的文件 的第filebno
个磁盘块 (即将某个指定的文件指向的磁盘块读入内存) -
实现:首先为即将读入内存的磁盘块利用函数
file_map_block
分配物理内存,然后使用read_block
函数将磁盘内容以块为单位读入内存中的相应位置。 -
使用例子:
1
2
3
4//fs/fs.c ==> int dir_alloc_file(struct File *dir, struct File **file)
if ((r = file_get_block(dir, i, &blk)) < 0) {
//...
}
-
-
file_map_block
函数(fs/fs.c)-
声明:
1
int file_map_block(struct File *f, u_int filebno, u_int *diskbno, u_int alloc);
-
作用:令
(*diskbno)
赋值为 文件控制块f
对应的文件 的第filebno
个磁盘块的 “disk block number” 。如果前面的磁盘块还不存在,且alloc == 1
则当场给它分配一个。 -
使用例子:
1
2
3
4//fs/fs.c ==> int file_get_block(struct File *f, u_int filebno, void **blk)
if ((r = file_map_block(f, filebno, &diskbno, 1)) < 0) {
//...
}
-
-
file_block_walk
函数(fs/fs.c)-
声明:
1
int file_block_walk(struct File *f, u_int filebno, uint32_t **ppdiskbno, u_int alloc);
-
作用:找 + 可能申请 maybe?
-
-
alloc_block
函数(fs/fs.c)-
声明:
1
int alloc_block(void);
-
作用:Allocate a block – first find a free block in the bitmap, then map it into memory
-
-
block_is_mapped
函数(fs/fs.c)-
声明:
1
void *block_is_mapped(u_int blockno);
-
作用:若
blockno
已经mapped,就返回它的虚拟缓存地址;反之,返回NULL
-
使用例子:
void *va = block_is_mapped(blockno);
-
Exercise 5.6
-
diskaddr
函数(fs/fs.c)-
声明:
1
void *diskaddr(u_int blockno);
-
作用:根据一个块的序号 (block number),计算这一磁盘块对应的虚存的起始地址
-
使用例子:
1
2//fs/fs.c ==> void *block_is_mapped(u_int blockno)
void *va = diskaddr(blockno);
-
Exercise 5.7
-
map_block
函数(fs/fs.c)-
声明:
1
int map_block(u_int blockno);
-
作用:分配映射磁盘块需要的物理页面
-
实现:
- 先使用
block_is_mapped
查看blockno
对应磁盘块是否已被映射到内存,若已经建立了映射,直接返回 0 即可; - 若没有建立映射,则调用
syscall_mem_alloc
函数分配一页物理页面。注意使用diskaddr
函数将blockno
转换为对应虚拟地址后再传入syscall_mem_alloc
函数
- 先使用
-
使用例子:
1
2
3
4//fs/fs.c ==> int alloc_block(void)
if ((r = map_block(bno)) < 0) {
//...
}
-
-
unmap_block
函数(fs/fs.c)-
声明:
1
int unmap_block(u_int blockno);
-
作用:回收用来映射磁盘块的物理页面
-
实现:
- 首先使用
block_is_mapped
函数获取对应磁盘块的虚拟起始地址; - 如果该磁盘块不是空闲的(调用
block_is_free
函数)并且该磁盘块缓存被写过(调用
block_is_dirty
函数),就调用write_block
函数将磁盘块缓存写入磁盘disk。 - 最后调用
syscall_mem_unmap
函数解除对应磁盘块缓存虚拟页面到物理页面的映射
- 首先使用
-
使用例子:同
map_block()
-
Exercise 5.8
-
dir_lookup
函数(fs/fs.c)-
声明:
1
int dir_lookup(struct File *dir, char *name, struct File **file);
-
作用:在
dir
指向的文件控制块所代表的目录下寻找名为name
的文件,如果找到了,就把它记录给(*file)
-
实现:对每一个
dir
指向的文件控制块所管理的磁盘块进行以下操作:- 使用
file_get_block
函数获取该磁盘块; - 遍历该磁盘块上存储的所有文件控制块,如果存在某个文件控制块的
f_name
属性与name
相同,就将*file
设为该文件控制块,并返回 0。
- 使用
-
使用例子:
1
2
3
4//fs/fs.c ==> int walk_path(char *path, struct File **pdir, struct File **pfile, char *lastelem)
if ((r = dir_lookup(dir, name, &file)) < 0) {
//...
}
-
三、文件系统的用户接口
为用户提供接口和机制使得用户程序能够使用文件系统,这主要通过一个用户态的文件系统服务来实现。
1、文件描述符
引入文件描述符等结构来抽象地表示一个进程所打开的文件,而不必关心文件实际的物理表示,是用户程序管理、操作文件的基础。
-
即 ,文件描述符
是系统给用户提供的整数,用于其在描述符表 () 中进行索引。
-
我们在作为操作系统的 使用者 进行文件 I/O 编程时,使用
open
在描述符表的指定位置存放被打开文件的信息;使用close
将描述符表中指定位置的文件信息释放;在write
和read
时修改描述符表指定位置的文件信息。这里的【指定位置】即【文件描述符 fd】。 -
当用户进程试图打开一个文件时,文件系统服务进程需要一个文件描述符来存储文件的基本信息和用户进程中关于文件的状态;同时,文件描述符也起到描述用户对于文件操作的作用。
当用户进程向文件系统发送打开文件的请求时,文件系统进程会将这些基本信息记录在内存中, 然后由操作系统将用户进程请求的地址映射到同一个存储了文件描述符的物理页上(此部分代码位于 的
serve_open
函数内),因此==一个文件描述符至少需要独占一页的空间==。当用户进程获取了文件大小等基本信息后,再次向文件系统发送请求将文件内容映射到指定内存空间中。
-
-
struct Fd
结构体 和struct Filefd
结构体1
2
3
4
5
6
7
8
9
10
11struct 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;
};Filefd
结构体的第一个成员就是Fd
,因此指向 Filefd 的指针同样指向这个 Fd 的起始位置,故可以进行强制转换。而转换的结果==仅仅是改变了该指针对一段内存的解释方式==。
Exercise 5.9
-
open
函数(user\lib\file.c)-
声明:
1
int open(const char *path, int mode);
-
作用:接受文件路径
path
和模式mode
作为输入参数,并返回文件描述符的编号 -
实现:
- 首先使用
fd_alloc
和fsipc_open
函数分配一个文件描述符; - 之后使用
fd2data
函数获取文件描述符对应的数据缓存页地址,并从Filefd
结构体中获取文件的size
和fileid
属性; - 之后遍历文件内容,使用
fsipc_map
函数为文件内容分配页面并映射; - 最后使用
fd2num
函数返回文件描述符的编号。
- 首先使用
-
使用例子:
1
2//tools/fsformat.c ==> void write_file(...)
int fd = open(path, O_RDONLY);
-
Exercise 5.10
在多次读写同一文件描述符期间,我们希望能够从文件中前一次读写完毕的位置开始,继续读写数据。因此,文件描述符中需要维护一个指针,以帮助我们在文件中定位;在 实现 read
、write
和 seek
等操作时,也需要更新该指针的值。
-
write
函数(user\lib\fd.c)-
声明:
1
int write(int fdnum, const void *buf, u_int n);
-
作用:
-
实现:
-
使用例子:
-
-
read
函数(user\lib\fd.c)-
声明:
1
int read(int fdnum, void *buf, u_int n);
-
作用:接收一个文件描述符编号
fdnum
,一个缓冲区指针buf
和一个最大读取字节数n
作为输入参数,返回成功读取的字节数,并更新文件描述符的偏移量。 -
实现:
- 首先使用
fd_lookup
和dev_lookup
函数获取文件描述符和设备结构体。如果查找失败,则返回错误码; - 接下来,函数检查文件描述符的打开模式是否允许读取操作;
- 然后,函数调用设备的读取函数
dev_read
从文件当前的偏移位置读取数据到缓冲区中; - 最后,如果读取操作成功,则函数更新文件描述符的偏移量并返回成功读取的字节数;
- 首先使用
-
使用例子:
1
2
3
4
5//user/lib/fd.c ==> int readn(int fdnum, void *buf, u_int n)
for (tot = 0; tot < n; tot += m) {
m = read(fdnum, (char *)buf + tot, n - tot);
//...
}
-
2、文件系统服务
MOS 操作系统中的文件系统服务通过 的形式供其他进程调用,进行文件读写操作。
具体来说,在内核开始运行时,就启动了文件系统服务进程 ENV_CREATE
( fs_serv
),用户进程需要进行文件操作时,使用 ipc_send
、ipc_recv
与 fs_serv
进行交互,完成操作。
fs/serv.c
中服务进程的主函数首先调用了 serve_init
函数准备好全局的文件打开记录表 opentab
;然后调用 fs_init
函数来初始化文件系统;执行完文件系统的初始化后,调用 serve
函数,文件系统服务开始运行,等待其他程序的请求。
-
fs_init
函数首先通过读取超级块的内容获知磁盘的基本信息;然后检查磁盘是否能够正常读写;最后调用read_bitmap
函数检查磁盘块上的位图是否正确。 -
用户程序在发出文件系统操作请求时,将请求的内容放在对应的结构体中进行消息的传递,
fs_serv
进程收到其他进行的 请求后, 传递的消息包含了 请求的类型 和 其他必要的参数,根据请求的类型执行相应的文件操作(文件的增、删、改、查等),将结果重新通过 反馈给用户程序。
Exercise 5.11 + 5.12 + 5.13
-
user/lib/fsipc.c
:定义了请求文件系统时用到的 操作user/lib/file.c
:定义了用户程序读写、创建、删除和修改文件的接口 -
serve_remove
函数 (fs/serv.c)-
声明:
1
void serve_remove(u_int envid, struct Fsreq_remove *rq);
-
作用:Serve to remove a file specified by the path in
rq
.
-
-
fsipc_remove
函数 (user/lib/fsipc.c)-
声明:
1
int fsipc_remove(const char *path);
-
作用:实现了一个客户端向
fs_serv
进程发送删除文件请求的功能。它接收一个文件路径字符串作为参数,并返回删除操作的结果。 -
实现:
- 首先检查路径字符串的长度是否在0到 MAXPATHLEN 之间。如果不是,则返回错误码,表示路径字符串不合法;
- 接下来,函数将 IPC 缓冲区
fsipcbuf
视为一个Fsreq_remove
结构体,代表将要发送的是remove
请求,并将路径字符串复制到结构体的路径字段中; - 最后,函数使用
fsipc
函数将删除请求发送到fs_serv
进程,并返回删除操作的结果。
-
难点分析
-
文件控制块——
BY2FILE
=扇区——
BY2SECT
=磁盘块——
BY2BLK
=BY2PG
=一个磁盘块中的扇区数量——
SECT2BLK
=(BY2BLK / BY2SECT)
=缓冲区——
DISKMAP
~DISKMAP+DISKMAX
= -
一个目录 = 一个磁盘块全部用来存目录项
一个目录项 = 指向一个磁盘块
一个磁盘块 = 16个文件控制块
一个文件控制块 = 描述一个文件
一个文件有 直接指针 + 间接指针 共1024个 ,每个指针指向一个磁盘块 = 存储着该文件的一部分文件数据
-
由
struct File *dirf
(指向的文件控制块代表一个目录)引发的一系列操作: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
40
41struct File *dirf; //假设已知 【dir_lookup是个好东西】
//该目录下磁盘块的数量
int nblk = dirf->f_size / BY2BLK;
//遍历该目录下所有的磁盘块,得到对应的blockno
for (int i = 0; i < nblk; ++i) {
int bno;
if (i < NDIRECT) {
bno = dirf->f_direct[i];
} else {
bno = (disk[dirf->f_indirect].data)[i];
}
//......
}
//遍历该目录下所有的磁盘块,得到对应磁盘的文件控制块
for (int i = 0; i < nblk; ++i) {
void *blk;
try(file_get_block(dirf, i, &blk));
//......
}
//由blockno得到对应磁盘的文件控制块
struct File *blk = (struct File *)(disk[bno].data);
//由blk,遍历磁盘块上的所有文件控制块
for (struct File *f = blk; f < blk + FILE2BLK; ++f) {
//对f......
}
//根据某磁盘的文件控制块,判断该文件是否为空
if (f->f_name[0] == '\0') {
//...
}
//申请一个新的磁盘块
struct File *blk = (struct File *)(disk[make_link_block(dirf, nblk)].data);
//由指向某磁盘块的文件控制块,得到该磁盘块中的第一个文件控制块
struct File *f = blk; -
一个
blockno
对应了两个地方:-
用户进程 文件系统服务 的虚拟缓存,里面有一块内存写的
blockno
磁盘块的临时内容;用
void *va = diskaddr(blockno);
-
同时,
disk
磁盘中,对应的blockno
磁盘块,是它实际模拟的硬件部分;用
blockno * SECT2BLK
表示它的第一个扇区
合作使用有:
ide_write(0, blockno * SECT2BLK, va, SECT2BLK);
【但还是直接用write_block
吧】 -
-
下部分函数的调用关系:【文件系统结构中的部分函数】
-
下部分函数调用关系:【文件系统的用户接口中的部分函数】
往年题lab5-1-exam
花叶学长的答案:
1 | //fs/ide.c |
往年题lab5-1-extra
花叶学长的答案:
1 | //fs/ide.c |
课上测试
lab5-1-Exam
考察类似于 ide_read()
仿写,题目给的步骤很清晰,一步一步使用 syscall_read_dev()
或 syscall_write_dev()
系统调用实现即可。
lab5-1-Extra
本题考察 ide_read()
和 ide_write()
函数的应用,实现一个简单的 ,具体题目见文章 Lab5-1-Extra-SSD题干。
引入三个全局变量,分别表示闪存映射表、物理块位图、物理块累计擦除次数表即可。
-
我的答案:【又一次感激涕零~ 题干描述清晰无歧义,也贴心地告诉了我们要“怎么实现?” “实现注意点?” 贴心得不得了~~】
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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82u_int ssdtable[32];
u_int ssdbitmap[32];//1 可写
u_int ssdnum[32];
void ssd_init() {
for (u_int i = 0; i < 32; i++) {
ssdtable[i] = 0xffffffff;
ssdbitmap[i] = 1;
ssdnum[i] = 0;
}
}
int ssd_read(u_int logic_no, void *dst) {
if (ssdtable[logic_no] == 0xffffffff) {
return -1;
}
ide_read(0, ssdtable[logic_no], dst, 1);
return 0;
}
void ssd_write(u_int logic_no, void *src) {
if (ssdtable[logic_no] != 0xffffffff) {
ssd_erase(logic_no);
}
//alloc----------------------
u_int physical_no = 0xffffffff;
for (u_int i = 0; i < 32; i++) {
if (ssdbitmap[i] == 1) {
if (physical_no == 0xffffffff) {
physical_no = i;
} else {
if (ssdnum[i] < ssdnum[physical_no])
physical_no = i;
}
}
}
if (ssdnum[physical_no] >= 5) {
u_int help_no = 0xffffffff;
u_int help_logic = 0xffffffff;
for(u_int i = 0; i < 32; i++) {
if (ssdbitmap[i] == 0) {
if (help_no == 0xffffffff) {
help_no = i;
} else {
if (ssdnum[i] < ssdnum[help_no])
help_no = i;
}
}
}
for (u_int i = 0; i < 32; i++)
if (ssdtable[i] == help_no) {
help_logic = i;
break;
}
u_int help_data[128];
if (ssd_read(help_logic, (void *)help_data) != 0)
user_panic("wrong in ssd_write's help_data\n");
ide_write(0, physical_no, (void *)help_data, 1);
ssdbitmap[physical_no] = 0;
ssd_erase(help_logic);
ssdtable[help_logic] = physical_no;
physical_no = help_no;
}
//---------------------------
ssdtable[logic_no] = physical_no;
ide_write(0, physical_no, src, 1);
ssdbitmap[physical_no] = 0;
}
void ssd_erase(u_int logic_no) {
if (ssdtable[logic_no] == 0xffffffff) {
return;
}
//all 0 in 物理块------------
u_int zero[128];
for (u_int i = 0; i < 128; i++)
zero[i] = 0;
ide_write(0, ssdtable[logic_no], (void *)zero, 1);
//---------------------------
ssdnum[ssdtable[logic_no]]++;
ssdbitmap[ssdtable[logic_no]] = 1;
ssdtable[logic_no] = 0xffffffff;
}
lab5-2-Exam
我们在课下已经实现了函数 int open(const char *path, int mode)
,利用绝对路径(相对于根目录的路径)path 定位文件。在实际的用户程序中,完成同一任务时打开的多个文件往往存储在同一目录下,然而系统每次打开文件时都需要从根目录开始查找路径,从而重复访问相同的目录。
在本次题中,要求实现 int openat(int dirfd, const char *path, int mode)
函数,利用相对于目录 dirfd
的相对路径 path
定位并打开文件,其中文件描述符 dirfd
指向已通过 open()
打开的目录。相对路径 path
也可能包含路径分隔符 /
,表示查找目录 dirfd
下嵌套的目录中的文件。
-
实现思路:
-
在 user/include/fsreq.h 中增加一个对于文件系统的请求类型
#define FSREQ_OPENAT 8
和请求结构体:1
2
3
4
5struct Fsreq_openat {
u_int dir_fileid;
char req_path[MAXPATHLEN];
u_int req_omode;
}; -
在 user/lib/fsipc.c 中仿照
fsipc_open
实现int fsipc_openat(u_int dir_fileid, const char *path, u_int omode, struct Fd *fd)
,完成对Fsreq_openat
各个字段的赋值,并与文件系统服务进程进行通信。 -
在 user/lib/file.c 中仿照
open
函数实现int openat(int dirfd, const char *path, int mode)
,实现这一函数的相关提示:- 调用
fd_lookup
利用dirfd
查找dirfd
的文件描述符struct Fd *dir
- 将
struct Fd *dir
指向的类型转换为struct Filefd
后获得dirfd
对应的fileid
- 调用
fsipc_openat
打开文件并完成对指针fd
的赋值。
- 调用
-
在 fs/fs.c 中,仿照
walk_path
实现int walk_path_at(struct File *par_dir, char *path, struct File **pdir, struct File **pfile, char *lastelem)
;在
par_dir
目录下按相对路径path
查找文件,并仿照file_open
实现int file_openat(struct File *dir, char *path, struct File **file)
调用walk_path_at
函数。 -
在 fs/serv.c 中仿照
serve_open
实现serve_openat
函数,并在serve
函数中增加关于openat
请求的判断。-
提示:可以参考以下实现,利用
dir_fileid
查找已经被打开的dirfd
对应的文件控制块:1
2
3
4
5
6struct Open *pOpen;
if ((r = open_lookup(envid, rq->dir_fileid, &pOpen)) < 0) {
ipc_send(envid, r, 0, 0);
return;
}
struct File *dir = pOpen->o_file;
-
-
上述函数中,需要在 user/include/lib.h 中增加
int openat(int dirfd, const char *path, int mode)
、int fsipc_openat(u_int, const char *, u_int, struct Fd *)
函数声明,在 fs/serv.h 中增加int file_openat(struct File *dir, char *path, struct File **pfile)
函数声明。
-
lab5-2-Extra
本题考察修改镜像文件 fsformat
的方式,实现符号链接(Symbolic link),具体题目见文章 Lab5-2-Extra-Symbolic link题干。
【后来发现自己的错误在:题干给的提示没有让修改 user/lib/file.c 中的 open
函数,于是我就没有想到过要修改这个,然后不管我怎么测试修改都不正确,哭唧唧~ 这个教训是不要寄所有希望于题干】
-
鹿煜恒大佬的答案:
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//tools/fsformat.c
void write_symlink(struct File *dirf, const char *path) {
int iblk = 0, r = 0, n = sizeof(disk[0].data);
struct File *target = create_file(dirf);
char targetpath[2048] = {0};
int len = readlink(path, targetpath, 2047);
/* in case `create_file` is't filled */
memcpy(disk[nextbno].data, targetpath, len);
disk[nextbno].data[len]='\0';
// Get file name with no path prefix.
const char *fname = strrchr(path, '/');
if (fname) {
fname++;
} else {
fname = path;
}
strcpy(target->f_name, fname);
target->f_size = 2048;
target->f_type = FTYPE_LNK;
save_block_link(target, 0 , next_block(BLOCK_DATA));
}
//user/lib/file.c
int open(const char *path, int mode) {
//......略
if(ffd->f_file.f_type == FTYPE_LNK){
return open(fd2data(fd), mode);
}else{
return fd2num(fd);
}
}
体会与感想
lab5 是OS课程的最后一个有上机考试的单元,终于结束了,十分感动ing~
本单元我们成功搭建了一个简单的文件系统,值得注意的是,我们学会了修改镜像文件 fsformat
,也明白了:文件系统服务 本身也是一个用户进程。当然,我们实现的仅仅是一个非常简单的系统,实际工程设计中的文件系统远比这个要复杂很多,未来如果想要在这个领域有所建树,还是需要投入大量的时间精力进行学习探究。