OS - Lab5实验报告

归档于2025年7月8日。

思考题

5.1

这跟Cache的更新策略有一些关系。

若Cache采用了写回策略,则:由kseg0写设备,向设备传递的数据会被存入Cache,而不会真正到达设备;仅在对应Cache块被清除掉时,向设备传输的数据才能到达设备。这会导致设备的延迟响应,甚至不响应。

Cache的更新策略,也与这个问题有关。如果Cache中缓存了设备的状态寄存器,那么:众所周知,设备的状态往往实时变化,若要保持Cache与设备状态寄存器的一致,我们必须时刻更新Cache,其性能开销极大,完全背离了Cache的设计本意;不维持一致性,则在Cache中读出的设备状态信息与实际的设备状态往往不符,会引起程序逻辑的问题。

这样做对不同设备的影响当然是不同的。对于IDE硬盘,可能是一次读/写请求迟迟不响应,导致程序长时间阻塞;对于显示设备(串口输出/显示器/…),可能是迟迟不显示该显示的内容。

5.2

答案在user/include/fs.h中:

  • FILE_STRUCT_SIZE:文件控制块大小,为256
  • BLOCK_SIZE:磁盘块大小;与PAGE_SIZE相等,为4096

由此得知,一个磁盘块,最多可存储4096/256 = 16个文件控制块。

剩下两个问题,需要看f_(in)direct能指向多少个磁盘块;答案是10 + (BLOCKSIZE / 4 - 10) = 1024个。

对于文件,文件系统支持的单个文件大小,为4096 * 1024byte = 4MB。

对于目录,其下可以有1024 * 16 = 16k 个文件。

5.3

分析可知,我们的块缓存,采用了类似Cache中直接映射的思路,故缓冲区大小即等于最大硬盘容量。

查询serv.h得知,我们得知这一片空间大小为0x40000000byte;换算过来,就是1GB.

这些宏定义能不能起个好点的名字?DISKMAX一晃眼还以为是地址上限,结果仔细一看才发现是空间大小…况且之前都是用上限来界定空间,这里突然换成基地址+空间大小又是个什么意思

5.4

要我说,其实都重要…

  • BLOCK_SIZE:硬盘块大小
  • N_DIRECT:文件控制块中,直接指向硬盘块的指针个数
  • FILE2BLK:一个硬盘块里,可存文件控制块的个数
  • SECT_SIZE:一个扇区的大小
  • SECT2BLK:一个硬盘块可以存多少个扇区;注意这二者是不一样的
  • DISKMAP/MAX:指示内存中,硬盘缓冲区的起始位置与大小

5.5

在动手写程序之前,我们不妨先分析一番。

众所周知,fork()会将父进程的内存映射复制给子进程;此处指向Fd的内存映射,应该也是在被复制的内存映射范围内的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
int main(){  
int fd;
int r;
char buf[512];
fd = open("/test",O_RDONLY);
r = fork();
if(r == 0){
if((r = read(fd,buf,3))!=3){
user_panic("read error");
}
debugf("child read: %s\n",buf);
}else{
if((r = read(fd,buf,3))!=3){
user_panic("read error");
}
debugf("parent read: %s\n",buf);
}
return 0;
}

测试下来,确实也是如此。

编写的时候需要注意:

  • open()返回的是fd编号
  • read()/write()中的参数,传入的都是int fdnum,而非fd本身
  • file_read/write()是给驱动用的,用户态只需要调用open/write()

5.6

先看fd.h里两个:

1
2
3
4
5
6
  34 // file descriptor  
  35 struct Fd {
>> 36     u_int fd_dev_id; // 设备ID;对于文件则为可以简单理解为硬盘ID
>> 37     u_int fd_offset; // 当前读写位置,距起始位置的偏移
>> 38     u_int fd_omode; // 文件读写模式
  39 };
1
2
3
4
5
6
  49 // file descriptor + file  
  50 struct Filefd {
  51     struct Fd f_fd; // 文件对应的描述符
>> 52     u_int f_fileid; // 文件ID
  53     struct File f_file; // 文件控制块
  54 };

再看FIle结构体:

1
2
3
4
5
6
7
8
9
10
  26 struct File {  
  27     char f_name[MAXNAMELEN]; // filename
  28     uint32_t f_size;     // file size in bytes
  29     uint32_t f_type;     // file type
  30     uint32_t f_direct[NDIRECT]; // 指向硬盘块的直接指针
  31     uint32_t f_indirect; // 间接硬盘块指针
  32  
  33     struct File *f_dir; // 文件所在文件夹
  34     char f_pad[FILE_STRUCT_SIZE - MAXNAMELEN - (3 + NDIRECT) * 4 - sizeof(void *)]; // padding
  35 }

顺手提一下,Fd是怎么变成FileFd的:

  • serv.cserve_open()中,先file_create()创建文件控制块,
  • file_open(),将文件加载进文件控制块;
  • 最后,配置好FileFd,由fsipc,将ffd传回给fsipc_open(path, mode, fd)中的fd,配合强制类型转化完成。

5.7

怎么OS也要跟UML语义纠缠

一共有这三种:

  • Found Message(如ENV_CREATE):无触发对象消息
  • 同步消息:发送后,发送者停止自己的活动,如ipc_send(fsreq),直到收到返回消息
  • 返回消息:如ipc_send(dst_va)

具体实现:

  • Found Message:将信息存在一个指定空间内,发送者就什么都不做了;ENV_CREATE就是将进程放进调度队列里
  • 同步消息:类似于握手机制,在一个ipc_send()后,立即执行ipc_recv();在未收到信息时等待,收到后则程序恢复执行
  • 返回信息:就是普通的ipc_send(),只是为了配合同步消息的实现。

难点分析

文件系统

Pasted image 20250506194608.png

按照教程中示意图,我们可以得知:

  • 用户进程通过调用库,请求文件服务,实现文件操作
  • file_read()/write()用户库完成,而非直接调用文件服务
  • 文件服务单独为一个进程
  • 用户进程由进程间通信,与服务进程交互
  • 服务进程由中断进入内核,进行外部设备的文件交互

MMIO

malta.h中,诸如0x18000000一类的地址,指向的是物理地址

在内核中,欲通过MMIO对这些外设进行交互,则需要访问一个映射到设备物理地址的虚拟地址;这个虚拟地址在kseg1内,物理地址加上kseg1的偏移,即可得到对应的虚拟地址

对于MMIO的内存操作,指针建议都带上volatile修饰符,以避免编译器错误优化,带来预期外的结果:
Pasted image 20250506202229.png

kseg1看这名字就知道是内核态的地址空间;我们的IDE驱动程序在用户态,故我们需要一个系统调用,来对这个地址进行访问。

文件系统的空闲位图

Pasted image 20250507080102.png

  • 位图的起点在Block[2]处,也就是Super Block的下一个Block
  • 每一个字节设为0xff,即全设为1;一个块的字节大小即BLOCK_SIZE
  • 第三步的if,即“根据实际情况,将不存在部分设为0”
  • fs/fs.c中,由read_bitmap(),将位图读入bitmap[]中。
  • *bitmap是一个uint32_t *,也就是一单位的bitmap[]有32个硬盘块的空闲位,这就是为什么下面free有关函数,要用bitmap[blockNo / 32]的形式访问位图。
  • free_block()中,若blockno为0,则会将分区表和引导扇区free掉。参考删掉pagefile.sys会发生什么(笑)

create_file()

这个函数,实际做的是:

  • 为文件分配一块空闲的空间。
  • 首先,遍历目标目录;
  • 先看是否有空闲File块,有则复用
  • 完全遍历后,没有空闲File块,则为目标目录分配一个新的块;
  • 新块的起始位置,就是目标File块的位置。

map_block()

不要忘了,page_alloc是内核用的;用户态分配页,是通过syscall_mem_alloc()来实现的。

也别忘了,参数中envid = 0,指示当前进程。(syscall流程见本人Lab4文档…)

思考题用户态程序的编写

注意到,实验环境中已有的环境,已经生成了二进制机器码。

本人并没有找到效仿之,并在init.cENV_CREATE的较简便方法,遂选择直接修改init.c进行运行。

小细节

我记得最深的,就是设备物理地址是否合法的检验。

指导书里是这么写的(指导书给的范围没错):

1
2
3
同时还要检查物理地址的有效性,在实验中允许访问的地址范围为: 
console: [0x180003F8, 0x18000418), disk: [0x180001F0, 0x180001F8),
当出现越界时, 应返回指定的错误码。

然而直接按照上面的范围,把条件写成if ((pa >= 0x180003f8 && pa + len < 0x18000418)是错的,条件应该是if ((pa >= 0x180003f8 && pa + len <= 0x18000418)

为什么?考虑基地址为0,空间大小为4;那么我们可访问的地址,应该是[0x0, 0x3],对吧?

但在上面,使用pa + len的情况下,0x0 + 4 = 0x4;此时我们应该是允许等于0x4的。

一个基本的数学小问题,需要细心,不能看到指导书就直接照搬。

思考题中提到的

比如说进程通信(fsipc),Fd到Filefd的转换过程等等,我认为有难度的地方,我都在思考题部分提到了;若您感兴趣,还请重新翻到上文阅读。

实验体会

本次实验虽然花了比我预期要多的时间(),但是难度比我预期要低。

我的感受就是,遇到不懂的,就边读边记,实在搞不懂就参考前人的经验。像FdFileFd的转换,我不边读源码,边在报告里记录其流程,我自己真不一定能搞定。

至于参考前人经验…就比如说上面提到的,设备物理地址合法性检验的问题,在我没查看MOS的开源代码前,我真没有意识到这个问题,花了不少时间找bug。当然,参考不等于抄,参考之后自己一定要理解。

原创声明

本文绝大部分内容为本人原创,但在实验过程中,确定部分细节是否实现正确时,本人难免参考了以下资料:

作者

LajiPZ

发布于

2025-07-08

更新于

2025-07-09

许可协议

评论