lab5实验报告

思考题

Thinking 5.1

题面

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

回答

  1. 缓存只能记录CPU的读写结果,却无法在外设对数据进行修改时及时调整。当外设产生中断信号或更新数据时,此时对应的Cache中此前旧的数据很可能刚刚完成缓存,则完成缓存的这一部分并无法完成相应的更新,将发生错误的行为,总之就是与外设交互是实时性很强的行为,采用缓存到Cache中的方式将引发错误。
  2. 对于串口设备而言,读写操作频繁,接发收信号多,在相同时间内发生错误的概率远远高于IDE磁盘。

Thinking 5.2

题面

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

回答

1.
代码如下:

1
2
3
4
5
6
7
//user/include/fs.h
...
#define BLOCK_SIZE PAGE_SIZE
...
#define FILE_STRUCT_SIZE 256
//include/mmu.h
#define PAGE_SIZE 4096

BLOCK_SIZE/FILE_STRUCT_SIZE = 4096 / 256 = 16,即可得到一个磁盘块中最多能存储16个文件控制块。
2. 对于目录文件来说,其指向的磁盘块存储着该目录下各个文件的文件控制块,而若刚好该目录指向的是存储指向文件内容的磁盘块的指针,此时该目录能够包含的文件数目最多,目录指向的磁盘块中存储的指向最终文件的文件控制块的指针,而该指针一共32位,只需4B。因此目录指向的磁盘块可以存储4096/4 = 1024个指向存储文件的文件控制块的磁盘块的指针,而一个磁盘块可以存储16个文件控制块,因此,一个目录下最多能有1024*16=16384个文件。
3.
代码如下:

1
2
3
4
5
//user/include/fs.h
...
#define NINDIRECT (BLOCK_SIZE / 4)
#define MAXFILESIZE (NINDIRECT * BLOCK_SIZE)
...

因此一个文件的最大容量为4096*4096/4 B = 4MB。
也就是因为直接指针和间接指针一共有1024个,而每一个指针都指向一块磁盘块,每个磁盘块的大小为4KB,因此最大容量即为这些之和,即1024 * 4KB = 4MB.

Thinking 5.3

题面

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

回答

因为从将DISKMAP 到 DISKMAP+DISKMAX 这一段虚存地址空间(0x10000000-0x4FFFFFFF) 作为缓冲区,而DISKMAX = 0x40000000,因此最大磁盘大小应为1GB.

Thinking 5.4

题面

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

回答

较为重要的宏定义如下:

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
//fs/serv.h
#define PTE_DIRTY 0x0004 // file system block cache is dirty

#define SECT_SIZE 512 /* Bytes per disk sector */
#define SECT2BLK (BLOCK_SIZE / SECT_SIZE) /* sectors to a block */

/* Disk block n, when in memory, is mapped into the file system
* server's address space at DISKMAP+(n*BLOCK_SIZE). */
#define DISKMAP 0x10000000

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

//user/include/fs.h
#define BLOCK_SIZE PAGE_SIZE
...
// Maximum size of a filename (a single path component), including null
#define MAXNAMELEN 128
// Maximum size of a complete pathname, including null
#define MAXPATHLEN 1024

// Number of (direct) block pointers in a File descriptor
#define NDIRECT 10
#define NINDIRECT (BLOCK_SIZE / 4)

#define MAXFILESIZE (NINDIRECT * BLOCK_SIZE)

#define FILE_STRUCT_SIZE 256
...
#define FILE2BLK (BLOCK_SIZE / sizeof(struct File))

// File types
#define FTYPE_REG 0 // Regular file
#define FTYPE_DIR 1 // Directory

其中,
PTE_DIRTY表示页表项中的脏位,当其被置为1时,对应的缓存块的数据不可用,即需要重写来保证数据的有效性。
SECT_SIZE BLOCK_SIZEMAXFILESIZEFILE_STRUCT_SIZE分别代表扇区、磁盘块、文件最大存储和文件控制块的大小,都是以字节为单位的。
SECT2BLKFILE2BLK都表示一个磁盘块能够存储的某种单位的个数,前者表示一个磁盘块中总扇区数目,而后者则代表一个磁盘块中能够存储的文件控制块的个数。
DISKMAP代表磁盘块在虚拟空间中的映射的首地址,通过将内存与相应磁盘块映射的虚地址建立映射完成磁盘块与内存的建立映射。DISKMAX代表总的磁盘块大小。
MAXNAMELENMAXPATHLEN分别表示文件名和文件路径的最大长度,字符长度。
NDIRECTNINDIRECT分别表示一个文件控制块中直接指针和间接指针的个数。
FTYPE_REGFTYPE_DIR分别代表两种不同的文件类型,前者是普通文件而后者则是目录文件,用于区分不同类型的文件。

Thinking 5.5

题面

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

回答

仿造lab5_4的测试,搭建测试文件lab5_fork,文件树如下:

1
2
3
4
5
6
7
8
~/23371355/tests
|-lab5_fork
|-rootfs
|-motd
|-newmotd
|-fork_check.c
|-Makefile
|-kernel.mk

其中,rootfs文件夹内内容未做任何修改,Makefilekernel.mk文件做了文件名上的微调。
fork_check.c文件内容如下:

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
#include <lib.h>

static char *msg = "This is a different message of the day!\n";

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

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

if ((n = read(fdnum, buf, 511)) < 0) {
user_panic("cannot read /newmotd: %d!!!", n);
}
if (strcmp(buf, msg) != 0) {
user_panic("read returned wrong data!!!");
}
debugf("read is good!!!\n");
int pid;

if ((pid = fork()) == 0) {
if ((n = read(fdnum, buf, 511)) < 0) {
user_panic("child read /newmotd error: %d", r);
}
if (strcmp(buf, msg) != 0) {
user_panic("child read's return error");
}
debugf("child read is good && child's fd == %d!!!\n", r);
struct Fd * childFd;
fd_lookup(r, &childFd);
debugf("child's fd's offset == %d!!!\n", childFd->fd_offset);
}
else {
if ((n = read(fdnum, buf, 511)) < 0) {
user_panic("father read /newmotd error: %d", r);
}
if (strcmp(buf, msg) != 0) {
user_panic("father read's return error");
}
debugf("father read is good && father's fd == %d!!!\n", r);
struct Fd * fatherFd;
fd_lookup(r, &fatherFd);
debugf("father's fd's offset == %d!!!\n", fatherFd->fd_offset);
}
return 0;
}

输出台结果如下:

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
init.c: mips_init() is called
Memory size: 65536 KiB, number of pages: 16384
to memory 80430000 for struct Pages.
pmap.c: mips vm init success
FS is running
superblock is good
read_bitmap is good
open is good!!!
read is good!!!
father read is good && father's fd == 0!!!
father's fd's offset == 40!!!
child read is good && child's fd == 0!!!
child's fd's offset == 40!!!
[00000800] destroying 00000800
[00000800] free env 00000800
i am killed ...
[00001802] destroying 00001802
[00001802] free env 00001802
i am killed ...
panic at sched.c:46 (schedule): schedule: no runnable envs

ra: 800260b0 sp: 803ffe80 Status: 00008000
Cause: 00000420 EPC: 00403080 BadVA: 7fd7f004
curenv: NULL
cur_pgdir: 83fce000

结果说明fork函数前后的父子进程会共享文件描述符和定位指针。

Thinking 5.6

题面

1
请解释 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
29
30
31
//File
//本质上是磁盘的实体数据,指向了磁盘上的文件数据
struct File {
char f_name[MAXNAMELEN]; //文件名称,其最大长度MAXNAMELEN为128
uint32_t f_size; //文件大小,字节为单位
uint32_t f_type; //文件类型,分为普通文件(FTYPE_REG)和目录(FTYPE_DIR)两种
uint32_t f_direct[NDIRECT]; //文件的直接指针,每个文件控制块有10个来记录文件的数据块在磁盘上的位置
uint32_t f_indirect; //指向一个间接磁盘块来存储指向文件内容的磁盘块的指针,为了简化计算,不使用间接磁盘块的前十个指针
struct File *f_dir; //指向文件所属的文件目录
char f_pad[FILE_STRUCT_SIZE - MAXNAMELEN - (3 + NDIRECT) * 4 - sizeof(void *)]; //为了让整数个文件结构体占用一个磁盘块来填充结构体中剩下的字节
} __attribute__((aligned(4), packed));

//Fd
//文件描述符
//用于描述文件来源,表示的是去哪打开以及如何打开某个文件,并不是磁盘上的实体,只是内存上文件系统进程的一个结构
struct Fd {
u_int fd_dev_id; //文件来源外设的id,用于确定去哪个地方找文件
//用户调用fd.c中的文件接口时,不同的dev_id会调取不同的外设下的不同的文件服务函数
//如File类型文件服务函数为user/lib/file.c中的file_*函数
u_int fd_offset; //读写的偏移量,指出现在的读写指针位置,在seek()函数中会被修改
u_int fd_omode; //权限位,一共有0-R,1-W和2-RW三种
};

//Filefd
//在Fd的基础上增加了部分新的文件描述,也是内存数据而非磁盘数据
struct Filefd {
struct Fd f_fd; //文件描述符
u_int f_fileid; //描述已经打开的文件
//在user/lib/fsipc.c中的fsipc()函数中会通过将以把f_fileid打包进fsreq的请求类型调用ipc_send(),进而完成相应的文件操作服务
struct File f_file; //文件控制块,用于描述相应文件的内容
};

Thinking 5.7

题面

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

回答

图 5.9
其中ENV_CREATE(user_env)ENV_CREATE(fs_serv)都是异步完成的,且前者一定在后者之前完成,即后者一定是第二个完成,这两者分别创建了用户进程和文件系统服务进程,两者分别执行初始化操作。
其中fs进程即文件系统服务进程,分别调用serv_init()fs_init()进行初始化,其中后者调用read_super()check_write_block()read_bitmap()函数完成对于超级块和位图的读取以及检查。后续进入死循环函数serve()中被ipc_recv()置为阻塞态,直至用户进程通过ipc_send(fsreq)唤醒,并通过ipc_send(dst_va)发送相应操作处理结果给用户进程。
用户进程再调用fd.c提供的函数接口后,调用fsipc.c服务程序中的交互函数,并通过fsipc()函数将请求通过ipc_send(fsreq)的方式传递给文件系统服务进程,此后,用户进程被ipc_recv()阻塞,直到文件系统服务进程通过ipc_send(dst_va)发送相应信息而重新运行。
因而,MOS操作系统通过IPC机制完成文件系统中进程的通信。

lab5课下练习

Exercise 5.1

题面

1
2
3
请根据 kern/syscall_all.c 中的说明,完成 sys_write_dev 函数以及sys_read_dev 函数的实现。
编写这两个系统调用时需要注意物理地址与内核虚拟地址之间的转换。
同时还要检查物理地址的有效性,在实验中允许访问的地址范围为:console: [0x180003F8, 0x18000418), disk: [0x180001F0, 0x180001F8),当出现越界时,应返回指定的错误码。

回答

代码如下:

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

//判断调用系统调用提供的pa是否符合合法
//也就是判断相应地址是否与需要读写长度是否对齐,比如:若地址以0x10为结尾则其只能读写len==1或len==2的位置
//和[pa, pa + len)是否内含于提供的物理设备的地址范围,满足其中一个即合法
static inline int is_illegal_dev_address(u_long pa, u_int len) {
//make the address aligned
if ((pa % 2 != 0 && len != 1) || (pa % 4 != 0 && len != 1 && len != 2)) {
return 1;
}
u_long pa_end = pa + len;
if ((pa >= 0x180003F8 && pa_end <= 0x18000418) || (pa >= 0x180001F0 && pa_end <= 0x180001F8)) {
return 0;
}
return 1;
}

int sys_write_dev(u_int va, u_int pa, u_int len) {
/* Exercise 5.1: Your code here. (1/2) */
//分别判断[va, va + len)和[pa, pa + len)是否合法同时va需要与len对齐,否则可能出现读写错误
if (is_illegal_va_range(va, len) || is_illegal_dev_address(pa, len) || (va % len != 0)) {
return -E_INVAL;
}
if (len == 1) {
//len为1时,利用iowrite8()写入1字节(8 bits)内容
uint8_t data = *(uint8_t *)va;
iowrite8(data, pa);
}
else if (len == 2) {
//len为2时,利用iowrite16()写入半字内容
uint16_t data = *(uint16_t *)va;
iowrite16(data, pa);
}
else if (len == 4) {
//len为4时,利用iowrite32()写入1个字内容
uint32_t data = *(uint32_t *)va;
iowrite32(data, pa);
}
else {
//否则返回-E_INVAL
return -E_INVAL;
}
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_dev_address(pa, len) || (va % len != 0)) {
return -E_INVAL;
}
if (len == 1) {
//len为1,利用ioread8()读出1字节内容写入va实现读出内容的操作
uint8_t data = ioread8(pa);
*(uint8_t *)va = data;
}
else if (len == 2) {
//len为2,利用ioread16()读出半字内容写入va实现读出内容的操作
uint16_t data = ioread16(pa);
*(uint16_t *)va = data;
}
else if (len == 4) {
//len为4,利用ioread32()读出1字内容写入va实现读出内容的操作
uint32_t data = ioread32(pa);
*(uint32_t *)va = data;
}
else {
return -E_INVAL;
}
return 0;
}

Exercise 5.2

题面

1
在 user/lib/syscall_lib.c 中完成用户态的相应系统调用的接口。

回答

代码如下,按照用户态调用相应的系统调用的格式语法即可:

1
2
3
4
5
6
7
8
9
int syscall_write_dev(void *va, u_int dev, u_int size) {
/* Exercise 5.2: Your code here. (1/2) */
return msyscall(SYS_write_dev, va, dev, size);
}

int syscall_read_dev(void *va, u_int dev, u_int size) {
/* Exercise 5.2: Your code here. (2/2) */
return msyscall(SYS_read_dev, va, dev, size);
}

Exercise 5.3

题面

1
参考以上介绍以及内核态磁盘驱动实现,使用系统调用完成 fs/ide.c 中的ide_read 和 ide_write 函数,实现用户态下对磁盘的读写操作。教程中展示的驱动函数只能运行于内核态,你需要通过系统调用的方式来实现其对应的用户态版本。例如,wait_ide函数的用户态版本为 fs/ide.c 中的 wait_ide_ready。

回答

代码如下:

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
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
void ide_read(u_int diskno, u_int secno, void *dst, u_int nsecs) {
uint8_t temp;
//offset代表每个扇区的首地址相对于dst的偏移
//max是读取扇区号的最大值+1(从0开始计)
u_int offset = 0, max = nsecs + secno;
//由于MOS实验只挂载了0、1号两块磁盘,因此当磁盘号大于1时非法
panic_on(diskno >= 2);
// Read the sector in turn
while (secno < max) {
//等待IDE设备就绪
temp = wait_ide_ready();
// Step 1: Write the number of operating sectors to NSECT register
//写入操作扇区数为1
temp = 1;
panic_on(syscall_write_dev(&temp, MALTA_IDE_NSECT, 1));
// Step 2: Write the 7:0 bits of sector number to LBAL register
//写入扇区号的[7:0]位
temp = secno & 0xff;
panic_on(syscall_write_dev(&temp, MALTA_IDE_LBAL, 1));
// Step 3: Write the 15:8 bits of sector number to LBAM register
/* Exercise 5.3: Your code here. (1/9) */
//写入[15:8]位
temp = (secno >> 8) & 0xff;
panic_on(syscall_write_dev(&temp, MALTA_IDE_LBAM, 1));
// Step 4: Write the 23:16 bits of sector number to LBAH register
/* Exercise 5.3: Your code here. (2/9) */
//写入[23:16]位
temp = (secno >> 16) & 0xff;
panic_on(syscall_write_dev(&temp, MALTA_IDE_LBAH, 1));
// Step 5: Write the 27:24 bits of sector number, addressing mode
// and diskno to DEVICE register
//写入[27:24]位
temp = ((secno >> 24) & 0x0f) | MALTA_IDE_LBA | (diskno << 4);
panic_on(syscall_write_dev(&temp, MALTA_IDE_DEVICE, 1));
// Step 6: Write the working mode to STATUS register
//写入IDE设备的状态为读取状态
temp = MALTA_IDE_CMD_PIO_READ;
panic_on(syscall_write_dev(&temp, MALTA_IDE_STATUS, 1));
// Step 7: Wait until the IDE is ready
//等待设备就绪
temp = wait_ide_ready();
// Step 8: Read the data from device
//进行读取操作
for (int i = 0; i < SECT_SIZE / 4; i++) {
//将第diskno号磁盘的第secno号扇区的数据读入dst偏移offset的地址中
panic_on(syscall_read_dev(dst + offset + i * 4, MALTA_IDE_DATA, 4));
}
// Step 9: Check IDE status
//检查IDE设备状态
panic_on(syscall_read_dev(&temp, MALTA_IDE_STATUS, 1));
//offset增加一个扇区的大小,扇区号自增一,表示进入处理下一个扇区的循环
offset += SECT_SIZE;
secno += 1;
}
}

//大部分操作与ide_read()一致
void ide_write(u_int diskno, u_int secno, void *src, u_int nsecs) {
uint8_t temp;
u_int offset = 0, max = nsecs + secno;
panic_on(diskno >= 2);
// Write the sector in turn
while (secno < max) {
temp = wait_ide_ready();
// Step 1: Write the number of operating sectors to NSECT register
/* Exercise 5.3: Your code here. (3/9) */
temp = 1;
panic_on(syscall_write_dev(&temp, MALTA_IDE_NSECT, 1));
// Step 2: Write the 7:0 bits of sector number to LBAL register
/* Exercise 5.3: Your code here. (4/9) */
temp = secno & 0xff;
panic_on(syscall_write_dev(&temp, MALTA_IDE_LBAL, 1));
// Step 3: Write the 15:8 bits of sector number to LBAM register
/* Exercise 5.3: Your code here. (5/9) */
temp = (secno >> 8) & 0xff;
panic_on(syscall_write_dev(&temp, MALTA_IDE_LBAM, 1));
// Step 4: Write the 23:16 bits of sector number to LBAH register
/* Exercise 5.3: Your code here. (6/9) */
temp = (secno >> 16) & 0xff;
panic_on(syscall_write_dev(&temp, MALTA_IDE_LBAH, 1));
// Step 5: Write the 27:24 bits of sector number, addressing mode
// and diskno to DEVICE register
/* Exercise 5.3: Your code here. (7/9) */
temp = ((secno >> 24) & 0x0f) | MALTA_IDE_LBA | (diskno << 4);
panic_on(syscall_write_dev(&temp, MALTA_IDE_DEVICE, 1));
// Step 6: Write the working mode to STATUS register
/* Exercise 5.3: Your code here. (8/9) */
temp = MALTA_IDE_CMD_PIO_WRITE;
panic_on(syscall_write_dev(&temp, MALTA_IDE_STATUS, 1));
// Step 7: Wait until the IDE is ready
temp = wait_ide_ready();
// Step 8: Write the data to device
for (int i = 0; i < SECT_SIZE / 4; i++) {
/* Exercise 5.3: Your code here. (9/9) */
//将src偏移offset地址上的数据写入第diskno号磁盘的第secno号扇区上
panic_on(syscall_write_dev(src + offset + i * 4, MALTA_IDE_DATA, 4));
}
// Step 9: Check IDE status
panic_on(syscall_read_dev(&temp, MALTA_IDE_STATUS, 1));
offset += SECT_SIZE;
secno += 1;
}
}

Exercise 5.4

题面

1
2
3
4
5
6
7
8
9
10
文件系统需要负责维护磁盘块的申请和释放,在回收一个磁盘块时,需要更改位图中的标志位。如果要将一个磁盘块设置为 free,只需要将位图中对应位的值设置为 1 即可。请完成 fs/fs.c 中的 free_block 函数,实现这一功能。同时思考为什么参数blockno 的值不能为 0
1 // Overview:
2 // Mark a block as free in the bitmap.
3 void free_block(u_int blockno) {
4 // You can refer to the function 'block_is_free' above.
5 // Step 1: If 'blockno' is invalid (0 or >= the number of blocks in 'super'), return.
6
7 // Step 2: Set the flag bit of 'blockno' in 'bitmap'.
8 // Hint: Use bit operations to update the bitmap, such as b[n / W] |= 1 << (n % W).
9 }

回答

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
void free_block(u_int blockno) {
// You can refer to the function 'block_is_free' above.
// Step 1: If 'blockno' is invalid (0 or >= the number of blocks in 'super'), return.
/* Exercise 5.4: Your code here. (1/2) */
if (!blockno || blockno >= super->s_nblocks) {
return ;
}
// Step 2: Set the flag bit of 'blockno' in 'bitmap'.
// Hint: Use bit operations to update the bitmap, such as b[n / W] |= 1 << (n % W).
/* Exercise 5.4: Your code here. (2/2) */
bitmap[blockno / 32] |= 1 << (blockno % 32);
}

根据提示完成即可,其中需要理解位图的原理,将一个字的每个位想象成一块磁盘块的使用情况表征,那么第33块磁盘块的相应使用情况就将由bitmap[1]二进制表示下第1位来存储。
至于为什么参数不能为0,那是因为第0块磁盘提供给引导扇区和分区表使用的,将永远保持占用状态,无法对其进行free操作,因此对器进行free操作是非法的。

Exercise 5.5

题面

1
参照文件系统的设计,完成 fsformat.c 中的 create_file 函数,并阅读write_directory 函数(代码已在源文件中给出,不作为考查点),实现将一个文件或指定目录下的文件按照目录结构写入到 target/fs.img 的功能。

回答

代码如下:

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
void save_block_link(struct File *f, int nblk, int bno) {
assert(nblk < NINDIRECT); // if not, file is too large !

if (nblk < NDIRECT) {
f->f_direct[nblk] = bno;
} else {
if (f->f_indirect == 0) {
// create new indirect block.
f->f_indirect = next_block(BLOCK_INDEX);
}
((uint32_t *)(disk[f->f_indirect].data))[nblk] = bno;
}
}
// Make new block contains link to files in a directory.
int make_link_block(struct File *dirf, int nblk) {
int bno = next_block(BLOCK_FILE);
save_block_link(dirf, nblk, bno);
dirf->f_size += BLOCK_SIZE;
return bno;
}

struct File *create_file(struct File *dirf) {
//获取当前目录一共占用了多少块磁盘块
int nblk = dirf->f_size / BLOCK_SIZE;
// Step 1: Iterate through all existing blocks in the directory.
for (int i = 0; i < nblk; ++i) {
int bno; // the block number
// If the block number is in the range of direct pointers (NDIRECT), get the 'bno'
// directly from 'f_direct'. Otherwise, access the indirect block on 'disk' and get
// the 'bno' at the index.
/* Exercise 5.5: Your code here. (1/3) */
//遍历每一个磁盘块寻找空的文件控制块来填入新文件
if (i >= 0 && i < NDIRECT) {
//遍历到直接指针处
//直接从目录的文件控制块中获取相应的磁盘块号
bno = dirf->f_direct[i];
}
else {
//遍历到间接指针处
//从指向的磁盘块的数据中获取相应的磁盘块号,通过解引用的方式
bno = *((uint32_t *)(disk[dirf->f_indirect].data) + i);
}
// Get the directory block using the block number.
//获取对应磁盘块中存储的第一个文件控制块指针
struct File *blk = (struct File *)(disk[bno].data);
// Iterate through all 'File's in the directory block.
for (struct File *f = blk; f < blk + FILE2BLK; ++f) {
// If the first byte of the file name is null, the 'File' is unused.
// Return a pointer to the unused 'File'.
/* Exercise 5.5: Your code here. (2/3) */
//遍历当前磁盘块中的所有文件控制块指针,若其文件名为空,说明
//该文件控制块为空,可以分配出去,返回相应指针
if (f->f_name[0] == '\0') {
return f;
}
}
}

// Step 2: If no unused file is found, allocate a new block using 'make_link_block' function
// and return a pointer to the new block on 'disk'.
/* Exercise 5.5: Your code here. (3/3) */
//若当前目录占有的所有磁盘块对应文件控制块都满了,则利用make_link_block()新申请一块磁盘块
int bno = make_link_block(dirf, nblk);
if (bno >= 0 && bno < NBLOCK) {
//新申请的磁盘块号在满足磁盘块号范围的前提下返回该磁盘块的第一个文件控制块指针
//新分配出来的肯定是空闲的啦!
return (struct File *)(disk[bno].data);
}
//否则返回NULL
return NULL;
}

Exercise 5.6

题面

1
fs/fs.c 中的 disk_addr 函数用来计算指定磁盘块对应的虚存地址。完成disk_addr 函数,根据一个块的序号 (block number),计算这一磁盘块对应的虚存的起始地址。(提示:fs/serv.h 中的宏 DISKMAP 和 DISKMAX 定义了磁盘映射虚存的地址空间)。

回答

代码如下:

1
2
3
4
5
void *disk_addr(u_int blockno) {
/* Exercise 5.6: Your code here. */
//根据分布图从DISKMAP以BLOCK_SIZE为单位偏移得到的就是对应的磁盘块首地址
return (void *)(DISKMAP + BLOCK_SIZE * blockno);
}

Exercise 5.7

题面

1
实现 map_block 函数,检查指定的磁盘块是否已经映射到内存,如果没有,分配一页内存来保存磁盘上的数据。相应地,完成 unmap_block 函数,用于解除磁盘块和物理内存之间的映射关系,回收内存。(提示:注意磁盘虚拟内存地址空间和磁盘块之间的对应关系)。

回答

代码如下:

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
int map_block(u_int blockno) {
// Step 1: If the block is already mapped in cache, return 0.
// Hint: Use 'block_is_mapped'.
/* Exercise 5.7: Your code here. (1/5) */
//利用block_is_mapped函数判断给定的磁盘号是否已经存在与内存的映射关系
if (block_is_mapped(blockno)) {
return 0;
}
// Step 2: Alloc a page in permission 'PTE_D' via syscall.
// Hint: Use 'disk_addr' for the virtual address.
/* Exercise 5.7: Your code here. (2/5) */
//利用syscall_mem_alloc()系统调用获取一块内存
try(syscall_mem_alloc(0, disk_addr(blockno), PTE_D));
return 0;
}

void unmap_block(u_int blockno) {
// Step 1: Get the mapped address of the cache page of this block using 'block_is_mapped'.
void *va;
/* Exercise 5.7: Your code here. (3/5) */
//获取建立了映射关系的磁盘对应的虚地址
va = block_is_mapped(blockno);
// Step 2: If this block is used (not free) and dirty in cache, write it back to the disk
// first.
// Hint: Use 'block_is_free', 'block_is_dirty' to check, and 'write_block' to sync.
/* Exercise 5.7: Your code here. (4/5) */
//在相应内存块不为free并且被置了脏位的情况下,回写回磁盘
if (!block_is_free(blockno) && block_is_dirty(blockno)) {
write_block(blockno);
}
// Step 3: Unmap the virtual address via syscall.
/* Exercise 5.7: Your code here. (5/5) */
//利用syscall_mem_unmap()系统调用解除虚地址与内存的映射关系
//进而解除了与磁盘块的映射关系
//因为磁盘块与虚地址存在映射关系
//而内存通过与虚地址建立映射关系达到磁盘块与内存建立映射关系的效果
panic_on(syscall_mem_unmap(0, va));
user_assert(!block_is_mapped(blockno));
}

Exercise 5.8

题面

1
2
补全 dir_lookup 函数,查找某个目录下是否存在指定的文件。
(使用 file_get_block 函数)

回答

代码如下:

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
int dir_lookup(struct File *dir, char *name, struct File **file) {
// Step 1: Calculate the number of blocks in 'dir' via its size.
u_int nblock;
/* Exercise 5.8: Your code here. (1/3) */
//获得当前目录文件一共占用了多少块磁盘块
nblock = dir->f_size / BLOCK_SIZE;
// Step 2: Iterate through all blocks in the directory.
for (int i = 0; i < nblock; i++) {
// Read the i'th block of 'dir' and get its address in 'blk' using 'file_get_block'.
void *blk;
/* Exercise 5.8: Your code here. (2/3) */
//利用file_get_block(struct File * f, u_int fileno, void ** blk)
//将f文件控制块下占有的第fileno块磁盘块的首地址在不发生错误的情况下写入*blk中
try(file_get_block(dir, i, &blk));
struct File *files = (struct File *)blk;

// Find the target among all 'File's in this block.
for (struct File *f = files; f < files + FILE2BLK; ++f) {
// Compare the file name against 'name' using 'strcmp'.
// If we find the target file, set '*file' to it and set up its 'f_dir'
// field.
/* Exercise 5.8: Your code here. (3/3) */
//找到同名文件,完成相应的修改f_dir和赋值的要求
//正常返回0
if (!strcmp(name, f->f_name)) {
f->f_dir = dir;
*file = f;
return 0;
}
}
}
return -E_NOT_FOUND;
}

Exercise 5.9

题面

1
请完成 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
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
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
//user/include/fd.h
#define INDEX2FD(i) (FDTABLE + (i) * PTMAP)
#define INDEX2DATA(i) (FILEBASE + (i) * PDMAP)

//user/lib/fd.c
int fd_alloc(struct Fd **fd) {
u_int va;
u_int fdno;
for (fdno = 0; fdno < MAXFD - 1; fdno++) {
va = INDEX2FD(fdno);

if ((vpd[va / PDMAP] & PTE_V) == 0) {
*fd = (struct Fd *)va;
return 0;
}

if ((vpt[va / PTMAP] & PTE_V) == 0) { // the fd is not used
*fd = (struct Fd *)va;
return 0;
}
}
return -E_MAX_OPEN;
}
...
void *fd2data(struct Fd *fd) {
return (void *)INDEX2DATA(fd2num(fd));
}

int fd2num(struct Fd *fd) {
return ((u_int)fd - FDTABLE) / PTMAP;
}

//user/lib/fsipc.c
static int fsipc(u_int type, void *fsreq, void *dstva, u_int *perm) {
u_int whom;
// Our file system server must be the 2nd env.
ipc_send(envs[1].env_id, type, fsreq, PTE_D);
return ipc_recv(&whom, dstva, perm);
}
...
int fsipc_open(const char *path, u_int omode, struct Fd *fd) {
u_int perm;
struct Fsreq_open *req;
req = (struct Fsreq_open *)fsipcbuf;
// The path is too long.
if (strlen(path) >= MAXPATHLEN) {
return -E_BAD_PATH;
}
strcpy((char *)req->req_path, path);
req->req_omode = omode;
return fsipc(FSREQ_OPEN, req, fd, &perm);
}
...
int fsipc_map(u_int fileid, u_int offset, void *dstva) {
int r;
u_int perm;
struct Fsreq_map *req;
req = (struct Fsreq_map *)fsipcbuf;
req->req_fileid = fileid;
req->req_offset = offset;
if ((r = fsipc(FSREQ_MAP, req, dstva, &perm)) < 0) {
return r;
}
if ((perm & ~(PTE_D | PTE_LIBRARY)) != (PTE_V)) {
user_panic("fsipc_map: unexpected permissions %08x for dstva %08x", perm, dstva);
}
return 0;
}


//user/lib/file.c
int open(const char *path, int mode) {
int r;
// Step 1: Alloc a new 'Fd' using 'fd_alloc' in fd.c.
// Hint: return the error code if failed.
struct Fd *fd;
/* Exercise 5.9: Your code here. (1/5) */
//通过fd_alloc()函数获得一个新的文件描述符,并将其赋给fd变量
try(fd_alloc(&fd));
// Step 2: Prepare the 'fd' using 'fsipc_open' in fsipc.c.
/* Exercise 5.9: Your code here. (2/5) */
//尝试使用fsipc_open()函数以mode模式打开path路径下的文件
//其实就是利用ipc通信机制,通过文件系统服务进程进行相应的文件操作
//具体就是文件系统服务进程根据相应的请求号调用请求表中相应的serve_*()函数
//例如:此处调用的就是serve_open()函数
try(fsipc_open(path, mode, fd));
// Step 3: Set 'va' to the address of the page where the 'fd''s data is cached, using
// 'fd2data'. Set 'size' and 'fileid' correctly with the value in 'fd' as a 'Filefd'.
char *va;
struct Filefd *ffd;
u_int size, fileid;
/* Exercise 5.9: Your code here. (3/5) */
//调用fd2data()函数获取文件描述符表中fd对应的数据缓存页地址
//此处的fd只是一个文件信息的虚地址所在地
va = fd2data(fd);
//将fd强制类型转换为表征内容更多的(struct Filefd *)类型
ffd = (struct Filefd *)fd;
fileid = ffd->f_fileid;
size = ffd->f_file.f_size;
// Step 4: Map the file content using 'fsipc_map'.
for (int i = 0; i < size; i += PTMAP) {
/* Exercise 5.9: Your code here. (4/5) */
//遍历文件的所有内容,使用fsipc_map()函数为文件内容分配页面并映射
try(fsipc_map(fileid, i, va + i));
}
// Step 5: Return the number of file descriptor using 'fd2num'.
/* Exercise 5.9: Your code here. (5/5) */
//返回文件描述符在文件描述符表中的索引
return fd2num(fd);
}

Exercise 5.10

题面

1
参考 user/lib/fd.c 中的 write 函数,完成 read 函数。

回答

代码如下:

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
//user/lib/fd.c
int dev_lookup(int dev_id, struct Dev **dev) {
for (int i = 0; devtab[i]; i++) {
if (devtab[i]->dev_id == dev_id) {
*dev = devtab[i];
return 0;
}
}
*dev = NULL;
debugf("[%08x] unknown device type %d\n", env->env_id, dev_id);
return -E_INVAL;
}

int fd_lookup(int fdnum, struct Fd **fd) {
u_int va;
if (fdnum >= MAXFD) {
return -E_INVAL;
}
va = INDEX2FD(fdnum);
if ((vpt[va / PTMAP] & PTE_V) != 0) { // the fd is used
*fd = (struct Fd *)va;
return 0;
}
return -E_INVAL;
}

int write(int fdnum, const void *buf, u_int n) {
int r;
struct Dev *dev;
struct Fd *fd;
if ((r = fd_lookup(fdnum, &fd)) < 0 || (r = dev_lookup(fd->fd_dev_id, &dev)) < 0) {
return r;
}

if ((fd->fd_omode & O_ACCMODE) == O_RDONLY) {
return -E_INVAL;
}

r = dev->dev_write(fd, buf, n, fd->fd_offset);
if (r > 0) {
fd->fd_offset += r;
}

return r;
}

int read(int fdnum, void *buf, u_int n) {
int r;
// Similar to the 'write' function below.
// Step 1: Get 'fd' and 'dev' using 'fd_lookup' and 'dev_lookup'.
struct Dev *dev;
struct Fd *fd;
/* Exercise 5.10: Your code here. (1/4) */
//先使用fd_lookup()和dev_lookup()函数获取文件描述符和设备结构体分别存储在fd和dev中
//若查找失败,返回错误码
try(fd_lookup(fdnum, &fd));
try(dev_lookup(fd->fd_dev_id, &dev));
// Step 2: Check the open mode in 'fd'.
// Return -E_INVAL if the file is opened for writing only (O_WRONLY).
/* Exercise 5.10: Your code here. (2/4) */
//检查文件描述符的打开模式是否允许读取操作
//也就是不能是O_WRONLY模式
if ((fd->fd_omode & O_ACCMODE) == O_WRONLY) {
return -E_INVAL;
}
// Step 3: Read from 'dev' into 'buf' at the seek position (offset in 'fd').
/* Exercise 5.10: Your code here. (3/4) */
//调用设备的读取函数dev_read()从文件当前的偏移位置fd_offset读取数据到缓冲区中
r = dev->dev_read(fd, buf, n, fd->fd_offset);
// Step 4: Update the offset in 'fd' if the read is successful.
/* Hint: DO NOT add a null terminator to the end of the buffer!
* A character buffer is not a C string. Only the memory within [buf, buf+n) is safe to
* use. */
/* Exercise 5.10: Your code here. (4/4) */
//若操作成功,即r>0,修改相应的文件当前的偏移位置
if (r > 0) {
fd->fd_offset += r;
}
//返回读取字节数,若失败则返回错误码
return r;
}

Exercise 5.11

题面

1
完成 fs/serv.c 中的 serve_remove 函数。

回答

代码如下:

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
//fs/fs.c
int file_remove(char *path) {
int r;
struct File *f;
// Step 1: find the file on the disk.
if ((r = walk_path(path, 0, &f, 0)) < 0) {
return r;
}
// Step 2: truncate it's size to zero.
file_truncate(f, 0);
// Step 3: clear it's name.
f->f_name[0] = '\0';
// Step 4: flush the file.
file_flush(f);
if (f->f_dir) {
file_flush(f->f_dir);
}
return 0;
}

//fs/serv.c
void serve_remove(u_int envid, struct Fsreq_remove *rq) {
// Step 1: Remove the file specified in 'rq' using 'file_remove' and store its return value.
int r;
/* Exercise 5.11: Your code here. (1/2) */
//调用了fs/fs.c中的file_remove()函数完成文件的删除
r = file_remove(rq->req_path);
// Step 2: Respond the return value to the caller 'envid' using 'ipc_send'.
/* Exercise 5.11: Your code here. (2/2) */
//使用ipc_send()函数返回用户进程相应的文件操作结果
ipc_send(envid, r, 0, 0);
}

Exercise 5.12

题面

1
完成 user/lib/fsipc.c 中的 fsipc_remove 函数。

回答

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
int fsipc_remove(const char *path) {
// Step 1: Check the length of 'path' using 'strlen'.
// If the length of path is 0 or larger than 'MAXPATHLEN', return -E_BAD_PATH.
/* Exercise 5.12: Your code here. (1/3) */
//先判断路径是否为空以及超出最大路径长度
//若不成立,返回错误码说明路径非法
uint32_t len = strlen(path);
if (len == 0 || len >= MAXPATHLEN) {
return -E_BAD_PATH;
}
// Step 2: Use 'fsipcbuf' as a 'struct Fsreq_remove'.
//将fsipcbuf视为一个Fsreq_remove结构体,代表要发送的remove请求
struct Fsreq_remove *req = (struct Fsreq_remove *)fsipcbuf;
// Step 3: Copy 'path' into the path in 'req' using 'strcpy'.
/* Exercise 5.12: Your code here. (2/3) */
//将要删除的路径名复制到结构体对应的字段中
strcpy((char *)req->req_path, path);
// Step 4: Send request to the server using 'fsipc'.
/* Exercise 5.12: Your code here. (3/3) */
//调用fsipc()函数,通过ipc机制同文件系统服务进程通信实现remove的操作
return fsipc(FSREQ_REMOVE, req, 0, 0);
}

Exercise 5.13

题面

1
完成 user/lib/file.c 中的 remove 函数。

回答

代码如下:

1
2
3
4
5
6
7
int remove(const char *path) {
// Call fsipc_remove.
/* Exercise 5.13: Your code here. */
//调用与文件系统服务进程交互的fsipc.c中的相应函数
//来完成相应的文件操作,实现了用户层面的简捷操作
return fsipc_remove(path);
}

lab5课下难点

文件系统

借由指导书提供的总览图,以下探讨皆以实验所使用的MOS操作系统为例:
文件系统总览图
,在我看来,文件系统无非是为了照顾用户而诞生的。也就是为了方便用户使用,毕竟你也不想打开个文件还要去记忆那繁琐的文件号吧,但是计算机并不能完全理解每个用户个性化的使用方式,因此就需要通过文件系统将这两者分隔开,也就是用户需要打开、关闭或者读写文件只需要去调用提供给用户的封装在user/lib/fd.c相应函数即可,而相应的文件操作根据操作的文件不同(在操作系统眼中一切皆是文件,filepipeconsole等)进行相应不同的下一层函数调用,例如:如果文件类型是file,则用户调用read()函数,将会调用file_read()函数。
其中,大多数操作是可以在用户进程只需要调用用户库(fd.cfile.cconsole.cpipe.cfsipc.c等文件)中的函数即可完成,但是还是有部分操作(主要是涉及对于file的操作,如:open()remove()等)是需要通过借由user/lib/fsipc.c中的中介函数,并调用user/lib/ipc.cipc_send()ipc_recv()函数完成与fs/fs.c下实现的文件系统服务进程通过IPC机制进行交互,进而完成对应的文件操作。
fs/文件夹下存放着用来完成文件系统服务进程的相关操作的代码(serv.cfs.cide.c等),其中serv.c主要进行与用户进程的交互,也就是接收用户进程通过调用syscall_ipc_send()系统调用发送的操作请求并完成相应的分发(分发到相应的服务函数serve_*中),而在fs.c中就包含了实现serv.c中具体操作的函数以及为了实现前者的与磁盘块进行交互的函数,以及最终对磁盘进行相应操作的函数ide_read()ide_write()都是保存在ide.c中,它们通过调用syscall_read_dev()syscall_write_dev()系统调用最终完成对于磁盘的读写。
上述借由指导书提供的图自顶向下的剖析了整个文件系统的操作流程,接下来让我们自底向上进行理解。

IDE磁盘驱动

近乎每种外设都通过读写设备上寄存器进行数据通信,这些寄存器称为I/O端口,包括控制寄存器、状态寄存器和数据寄存器,映射到指定物理地址空间,如:MALTA中,console设备映射到0x180003F8,IDE控制器映射到0x180001F0等。
以串口设备驱动为例,MALTA提供的console设备是一个典型的NS16550设备,基地址为0x180003F8.

设备寄存器映射如下表:

偏移 效果
0x00 读:非阻塞的读取一个字符,若没未读取字符返回上次读取的结果
0x00 写:输出一个字符ch
0x05 读:获取设备状态,返回1说明设备上有未读取的数据就绪可用

向内存的(0x180003F8+0xA0000000))写入字符,即可在shell中显示输出。
这也就是kern/machine.cprintcharc函数的实现:

1
2
3
4
5
6
7
8
9
10
11
12
void printcharc(char ch) {
if (ch == '\n') {
printcharc('\r');
}
while (!(*((volatile uint8_t *)(KSEG1 + MALTA_SERIAL_LSR)) & MALTA_SERIAL_THR_EMPTY)) {
}
//KSEG1 + MALTA_SERIAL_DATA,其中KSEG1为0xA0000000、MALTA_SERIAL_DATA为0x180003F8,此处定位写入地址
//强制转化为char型指针
//用volatile限定的原因是若不使用,编译器可能会对该地址数据的访问做优化,使得对I/O设备地址访问不直接与设备交互
//这与预期每次操作都直接与设备进行交互不符
*((volatile uint8_t *)(KSEG1 + MALTA_SERIAL_DATA)) = ch;
}

此次编写的IDE磁盘驱动程序在用户空间,因此对于设备的读写要通过系统调用(sys_write_dev 和 sys_read_dev)实现,否则会引发报错。而这两个系统调用正是后续读写磁盘的操作密切相关。
再者就是磁盘的一些概念,例如:扇区、磁道、柱面和磁头等(相信理论课上都已经学的很好了吧),以及一些磁盘操作,这与ide_read()ide_write()与磁盘交互的函数至关重要。
MALTA的PIIX4控制器提供了对磁盘读写的基本单位——扇区的基本方法,读写其特定寄存器可实现对扇区的读写,相应基地址为0x180001F0,相应寄存器功能如下表:

偏移 功能 位宽
0x0 读/写:向磁盘读写数据,从0字节开始 4B
0x1 读:设备错误信息;写:设置IDE命令的特定参数 1B
0x2 写:设置一次需操作的扇区数量 1B
0x3 写:设置目标扇区号的[7:0]位(LBAL) 1B
0x4 写:设置目标扇区号的[15:8]位(LBAM) 1B
0x5 写:设置目标扇区号的[23:16]位(LBAH) 1B
0x6 写:设置目标扇区号的[27:24]位(CHS/LBA),配置扇区寻址模式和要操作的磁盘编号(磁盘号) 1B
0x7 读:获取设备状态;写:配置设备工作状态 1B

采用逻辑块寻址(LBA)方式进行扇区寻址,此模式下,磁盘被视为线性的字节序列,每个扇区都有唯一编号,设置目标扇区编号即可完成寻址。
参数和寄存器映射关系
也就是在进行读写磁盘的操作前需要先将读写方式、读写磁盘号等信息整合形成相应的虚地址,最终直接根据读写模式读写相应虚地址对应的磁盘即可。

调用ide_read()ide_write()将磁盘扇区数据读到外设缓冲区中或反过来写入。类似控制台串口,磁盘操作中所有地址操作都要将物理地址转换为虚地址

由于IDE外设一般不能立即完成数据操作,因此需要CPU反复检查IDE状态并等待操作完成,通过如下的wait_ide_ready()函数来完成:

IDE就绪后,即可进行相应的读写磁盘操作。

  1. 设置操作扇区数。
  2. 设置扇区号[7:0]位
  3. 设置[15:8]位
  4. [23:16]位
  5. [27:24]位,设置扇区寻址模式和磁盘号
  6. 设置IDE设备状态为读或写
  7. 等待IDE设备就绪
  8. 读取或写入数据
  9. 检查IDE设备状态
  10. 完成

分别设置参数位是由于IDE设备无法一次性写入操作扇区号,需单独设置扇区号的各个位。

读取和写入数据时,由于实验的IDE设备每次仅能操作4B,因此需要通过循环完成。

例如:

1
2
3
4
5
6
7
8
for(int i = 0; i < SECT_SIZE; i += 4) {
//内核态下读取diskno号磁盘上的secno号扇区数据到dst处
memcpy(dst + i; (void *)(MALTA_IDE_DATA + 0xA0000000), 4);
/
//内核态下写入src指向扇区数据到diskno磁盘的
memcpy((void *)(MALTA_IDE_DATA + 0xA0000000), src + i, 4);

}

文件系统结构

磁盘文件系统布局和文件系统详细结构

将磁盘想象成顺序结构的话,其基本布局如下:
磁盘空间布局图
图中的Block是磁盘块,不同于扇区,是虚拟概念,是OS与磁盘交互的最小单位。类似于内存管理中页和段的概念,OS将相邻扇区重组成磁盘块操作,减小了寻址的困难。
图中可知,MOS操作系统将磁盘一开始的一个磁盘块当作引导扇区和分区表使用(MBR?),接下来一个磁盘块作为超级块来存储描述文件系统基本信息,如魔数、磁盘大小及根目录位置等
MOS中super block结构体如下:

1
2
3
4
5
6
//user/include/fs.h
struct Super {
u_int s_magic; //魔数,用于标识文件系统的常量
u_int s_nblocks; //当前文件系统磁盘块数目
struct File s_root; //根目录,其中其f_type为FTYPE_DIR,f_name为"/"
}

此处采用位图(Bitmap)管理空闲的磁盘资源,即用一个二进制位bit标识磁盘中一个磁盘块的使用情况(1空闲,0占用)。
写入文件前,利用tools/fsformat.c中的init_disk()函数将所有块标为空闲块并完成相应的初始化工作:

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
void init_disk() {
int i, diff;
// Step 1: Mark boot sector block.
disk[0].type = BLOCK_BOOT;
// Step 2: Initialize boundary.
//nbitblock是为了用bitmap标识正磁盘所有块使用情况所需的磁盘块数量。
nbitblock = (NBLOCK + BLOCK_SIZE_BIT - 1) / BLOCK_SIZE_BIT;
nextbno = 2 + nbitblock;
// Step 2: Initialize bitmap blocks.
for (i = 0; i < nbitblock; ++i) {
disk[2 + i].type = BLOCK_BMAP;
}
for (i = 0; i < nbitblock; ++i) {
//此处0xff代表1,设置磁盘块处于空闲状态
memset(disk[2 + i].data, 0xff, BLOCK_SIZE);
}
//若位图仍有剩余,不能将最后一块位图块中靠后的一部分内容标记为空闲,因为这些位对应磁盘块压根不存在,不可用
//此部分需要设为0
if (NBLOCK != nbitblock * BLOCK_SIZE_BIT) {
diff = NBLOCK % BLOCK_SIZE_BIT / 8;
memset(disk[2 + (nbitblock - 1)].data + diff, 0x00, BLOCK_SIZE - diff);
}
// Step 3: Initialize super block.
disk[1].type = BLOCK_SUPER;
super.s_magic = FS_MAGIC;
super.s_nblocks = NBLOCK;
super.s_root.f_type = FTYPE_DIR;
strcpy(super.s_root.f_name, "/");
}

文件控制块结构体如下:

1
2
3
4
5
6
7
8
9
10
struct File {
char f_name[MAXNAMELEN]; //文件名称,其最大长度MAXNAMELEN为128
uint32_t f_size; //文件大小,字节为单位
uint32_t f_type; //文件类型,分为普通文件(FTYPE_REG)和目录(FTYPE_DIR)两种
uint32_t f_direct[NDIRECT]; //文件的直接指针,每个文件控制块有10个来记录文件的数据块在磁盘上的位置
uint32_t f_indirect; //指向一个间接磁盘块来存储指向文件内容的磁盘块的指针,为了简化计算,不使用间接磁盘块的前十个指针,这样一来相应的磁盘块号0-9对应的是直接指针,而10-1023则对应的是存储在一个磁盘块上的间接指针

struct File *f_dir; //指向文件所属的文件目录
char f_pad[FILE_STRUCT_SIZE - MAXNAMELEN - (3 + NDIRECT) * 4 - sizeof(void *)]; //为了让整数个文件结构体占用一个磁盘块来填充结构体中剩下的字节
} __attribute__((aligned(4), packed));

普通文件的FCB中指向的磁盘块存储着文件内容,而目录文件指向的存储目录下各个文件对应的FCB
当查找某个文件时,先从super block读取根目录的FCB,然后沿着目标路径,依次查看当前目录所包含文件是否与下一级目标文件同名,进而查找到最终的目标文件。

块缓存

借助虚存实现磁盘块缓存。由于对磁盘I/O操作单次只能读取少量数据,同时频繁读写磁盘将拖慢系统,使用缓存可通过将数据暂存于内存,减少磁盘访问次数,加快数据读取
要达到通过虚拟地址来启动对磁盘的访问,那么在虚拟空间上为磁盘划分相应的空间就必不可少了:
块缓存布局图
文件系统服务进程作为一个用户进程,将DISKMAPDISKMAP+DISKMAX这一段虚存地址空间(0x10000000-0x4FFFFFFF) 作为缓冲区,当磁盘读入内存时,用来映射相关的页。

1
2
#define DISKMAP 0x10000000
#define DISKMAX 0x40000000

fs/fs.c中的map_block()函数和unmap_block()函数实现了当把一个磁盘块中内容载入内存中为之分配对应物理内存和结束使用某一磁盘块释放对应物理内存的操作。
read_block()write_block()函数用于读写磁盘块。前者将指定编号磁盘块读入内存,其先检查该磁盘块是否已在内存中,不在则先分配物理内存,然后调用ide_read()读取磁盘上数据到对应虚存地址上。后者同理。
file_get_block()将某个指定的文件指向的磁盘读入内存:先用file_map_block()为将读入内存的磁盘块分配物理内存,再使用read_block()将磁盘内容一块为单位读入内存相应位置。

文件系统的用户接口

为了便于用户操作文件系统,还要向用户提供相关使用接口。也就是用户库的层面。

文件描述符

fd——File Discripter、系统提供给用户的用于在描述索引表中索引的整数。在进行I/O编程时,用openclosewriteread来进行文件操作时处理的都是fd

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
struct Fd {
u_int fd_dev_id; //文件对应的设备id
u_int fd_offset; //文件的偏移量,在实验中代表读写指针(这里貌似读写指针共用)
u_int fd_omode; //文件打开的模式
//共有以下三种形式
/*
#define O_RDONLY 0x0000 // open for reading only
#define O_WRONLY 0x0001 // open for writing only
#define O_RDWR 0x0002 // open for reading and writing
*/
};

//此外还有如下结构体来表示具有更加详细内容的文件信息
//由于结构体的存储是顺序存储,因此这两个结构体之间是可以通过强制转换的方式相互转换的,
//并且通过Filefd结构体仍可以访问到Fd结构体的成员变量;反过来,多出来的部分Fd结构体无法访问到
struct Filefd {
struct Fd f_fd; //文件描述符
u_int f_fileid; //描述已经打开的文件
//在user/lib/fsipc.c中的fsipc()函数中会通过将以把f_fileid打包进fsreq的请求类型调用ipc_send(),进而完成相应的文件操作服务
struct File f_file; //文件控制块,用于描述相应文件的内容
}

用户进程尝试打开文件时,文件系统服务进程需要一个fd来存储文件基本信息和用户进程中文件的状态,且fd还有描述用户对文件操作的作用。
用户进程发生打开文件请求时,文件系统进程将基本信息记在内存中,再由OS将用户进程请求地址映射到相应存储了fd的物理页上,因此一个fd至少独占一页
用户进程获取了文件基本信息后再向文件系统发送请求将文件内容映射到指定内存空间中。

文件系统服务

文件系统服务通过IPC形式为其他进程提供服务。
具体:内核始运行时启动文件系统服务进程ENV_CREATE(fs_serv),进行文件操作时,使用ipc_send()ipc_recv()fs_serv()交互。fs/serv.c中服务进程主函数调用serve_init()准备好全局文件打开记录表opentab(),再调用fs_init()初始化文件系统,其先通过读取super block内容获取磁盘基本信息,再检查磁盘能否正常读写,最后调用read_bitmap检查位图是否正确。此后,调用serve开始文件系统服务,等待其他程序请求。
文件系统支持的请求类型如下:

1
2
3
4
5
6
7
8
9
10
enum {
FSREQ_OPEN,
FSREQ_MAP,
FSREQ_SET_SIZE,
FSREQ_CLOSE,
FSREQ_DIRTY,
FSREQ_REMOVE,
FSREQ_SYNC,
MAX_FSREQNO,
};

用户程序发生相应请求时,请求内容放在对应结构体中进行消息传递,fs_serv()进程收到进行的IPC请求后,根据传递的请求类型和必要参数执行相应的文件操作,最后将结果通过IPC反馈回用户程序。
open()函数的执行过程为例:
图 5.9
其中ENV_CREATE(user_env)ENV_CREATE(fs_serv)都是异步完成的,且前者一定在后者之前完成,即后者一定是第二个完成,后者在MOS中一定占据着进程就绪队列中第二个位置,也就是envs[1],这两者分别创建了用户进程和文件系统服务进程,两者分别执行初始化操作。
其中fs进程即文件系统服务进程,分别调用serv_init()fs_init()进行初始化,其中后者调用read_super()check_write_block()read_bitmap()函数完成对于超级块和位图的读取以及检查。后续进入死循环函数serve()中被ipc_recv()置为阻塞态,直至用户进程通过ipc_send(fsreq)唤醒,并通过ipc_send(dst_va)发送相应操作处理结果给用户进程。
用户进程再调用fd.c提供的函数接口后,调用fsipc.c服务程序中的交互函数,并通过fsipc()函数将请求通过ipc_send(fsreq)的方式传递给文件系统服务进程,此后,用户进程被阻塞,直到文件系统服务进程通过ipc_send(dst_va)发送相应信息而重新运行。
更具体的流程图如下:


因而,MOS操作系统通过IPC机制完成了文件系统中进程的通信,进而完成相应的文件操作。

心得体会

这次的lab函数众多,各种调用链层出不穷,重要的是理解其运作机制,还需要认真掌握每一个函数的用法(至少实验填空涉及到的都要掌握清楚,不然课上怎么考你)。以我为鉴,没有认真理解fs/fs.c中的walk_path()函数,导致函数调用出错,一直报address too low,导致助教一直以为是我后面的访问越界出问题了,以至于两个助教都没看出来,其实walk_path()调用就有问题,也是长教训了,没认真理解函数就动手敲代码是为大忌,其实但凡当时现场认真看应该也不会出错,害,就应该模仿file_open()的对于walk_path()的用法,自作聪明

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
// Overview:
// Open "path".
//
// Post-Condition:
// On success set *pfile to point at the file and return 0.
// On error return < 0.
int file_open(char *path, struct File **file) {
return walk_path(path, 0, file, 0);
}

// Overview:
// Evaluate a path name, starting at the root.
// Post-Condition:
// On success, set *pfile to the file we found and set *pdir to the directory
// the file is in.
// If we cannot find the file but find the directory it should be in, set
// *pdir and copy the final path element into lastelem.
//通俗的来说,就是该函数是用来找path对应的文件的,(目录也是文件,当时我理解错的核心就在于我把目录文件和普通文件区分开来了,认为pdir存相应目录,pfile存普通文件,而题目又保证传入的是目录,我自然而然的以为只需要传入pdir就行,是我没认真看这个函数的实现了)
//本质就是pfile是对应的找到的文件,而pdir是pfile所在的目录文件,因此重要的应该是pfile参数,也就是一定要确保这个参数有传入值,不能是NULL
//一定要先认真理解函数功能再动手
int walk_path(char *path, struct File **pdir, struct File **pfile, char *lastelem) {
char *p;
char name[MAXNAMELEN];
struct File *dir, *file;
int r;

// start at the root.
path = skip_slash(path);
file = &super->s_root;
dir = 0;
name[0] = 0;

if (pdir) {
*pdir = 0;
}

*pfile = 0;

// find the target file by name recursively.
while (*path != '\0') {
dir = file;
p = path;

while (*path != '/' && *path != '\0') {
path++;
}

if (path - p >= MAXNAMELEN) {
return -E_BAD_PATH;
}

memcpy(name, p, path - p);
name[path - p] = '\0';
path = skip_slash(path);
if (dir->f_type != FTYPE_DIR) {
return -E_NOT_FOUND;
}

if ((r = dir_lookup(dir, name, &file)) < 0) {
if (r == -E_NOT_FOUND && *path == '\0') {
if (pdir) {
*pdir = dir;
}

if (lastelem) {
strcpy(lastelem, name);
}

*pfile = 0;
}

return r;
}
}
if (pdir) {
*pdir = dir;
}
*pfile = file;
return 0;
}

还能说什么呢?认真看了那么久调用链,却没有注意到这么小的问题,阅读代码的能力有待提升,只能继续努力了:(!