lab5实验报告
思考题
Thinking 5.1
如果通过 kseg0 读写设备,那么对于设备的写入会缓存到 Cache 中。这是一种错误 的行为,在实际编写代码的时候这么做会引发不可预知的问题。请思考:这么做这会引发什么问题?对于不同种类的设备(如我们提到的串口设备和 IDE 磁盘)的操作会有差异吗?可以从缓存的性质和缓存更新的策略来考虑。
当程序需要读取设备的数据时,若数据已经被写入了Cache,则会从Cache中直接读取,而此时若外部设备的数据发生变化,则会产生错误的行为。
Thinking 5.2
查找代码中的相关定义,试回答一个磁盘块中最多能存储多少个文件控制块?一个目录下最多能有多少个文件?我们的文件系统支持的单个文件最大为多大?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 #define BLOCK_SIZE PAGE_SIZE #define FILE_STRUCT_SIZE 256 struct File { char f_name[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)); #define FILE2BLK (BLOCK_SIZE / sizeof(struct File)) #define FTYPE_REG 0 #define FTYPE_DIR 1
由代码可知,一个磁盘块最多能存储4096 B 256 B = 16 \frac{4096B}{256B}=16 2 5 6 B 4 0 9 6 B = 1 6 个文件控制块。
若f_type == FTYPE_DIR
,其中的指针最多可以记录1024 1024 1 0 2 4 个磁盘块,故一个目录下最多有1024 × 16 = 16384 1024 \times 16 = 16384 1 0 2 4 × 1 6 = 1 6 3 8 4 个文件。
若f_type == FTYPE_REG
,则同理可知单个文件最大为1024 × 4 K B = 4 M B 1024 \times 4KB = 4MB 1 0 2 4 × 4 K B = 4 M B 。
Thinking 5.3
请思考,在满足磁盘块缓存的设计的前提下,我们实验使用的内核支持的最大磁盘大小是多少?
1 2 3 4 5 6 #define DISKMAP 0x10000000 #define DISKMAX 0x40000000
显然,支持的最大磁盘大小为0 x 4000 _ 0000 B = 1 G B 0{\rm x}4000\_0000B=1GB 0 x 4 0 0 0 _ 0 0 0 0 B = 1 G B
Thinking 5.4
在本实验中,fs/serv.h、user/include/fs.h 等文件中出现了许多宏定义,试列举你认为较为重要的宏定义,同时进行解释,并描述其主要应用之处。
代码中的注释写得很详细,这些宏大部分指代不同数据结构的大小,如
1 2 3 4 5 6 7 8 9 10 11 12 13 #define FILE2BLK (BLOCK_SIZE / sizeof(struct File)) u_int nblock; nblock = ROUND(dir->f_size, BLOCK_SIZE) / BLOCK_SIZE; for (int i = 0 ; i < nblock; i++) { void *blk; try(file_get_block(dir, i, &blk)); struct File *files = (struct File *)blk; for (struct File *f = files; f < files + FILE2BLK; ++f) { } }
Thinking 5.5
在 Lab4“系统调用与 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 #include <lib.h> static char *msg = "This is the NEW message of the day!\n" ;static char *diff_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 (r = fork()) { n = read(fdnum, buf, 10 ); debugf("father: \'%s\'\n" , buf); } else { n = read(fdnum, buf, 10 ); debugf("child: \'%s\'\n" , buf); } }
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 29 30 31 32 33 34 35 36 37 38 39 #define MAXNAMELEN 128 #define NDIRECT 10 #define FILE_STRUCT_SIZE 256 struct File { char f_name[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)); 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 ; };
Thinking 5.7
图 5.9 中有多种不同形式的箭头,请解释这些不同箭头的差别,并思考我们的操作系统是如何实现对应类型的进程间通信的。
图中实线箭头表示调用另一个程序中的对应函数,虚线箭头表示使用IPC获取调用结果。
文件系统服务进程在完成初始化(serv_init,fs_init
)后,调用serve
函数(一个死循环),然后反复调用ipc_recv
等待用户进程发出操作请求,并根据请求的不同类型执行相应的文件操作,完成服务后使用ipc_send
返回结果。
用户进程针对不同的请求类型,通过ipc_send
发送必要的参数(将请求的内容放在结构体中),然后等待被文件系统反馈的结果(ipc_send
)唤醒。
难点分析
外设控制
CPU 通过读写设备控制器上的寄存器实现对设备的控制和通信。而在MIPS体系结构下,我们使用 MMIO机制访问设备寄存器。
MMIO (内存映射IO):使用不同的物理内存地址 为设备寄存器编址,将一部分对物理内存的访问 “重定向” 到设备地址空间中。
偏移
寄存器功能
数据位宽
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
读:获取设备状态;写:配置设备工作状态
1 字节
使用系统调用sys_write_dev
和sys_read_dev
读写kseg1
段内核数据,来实现设备读写操作。
文件系统结构
磁盘结构
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 struct Block { uint8_t data[BLOCK_SIZE]; uint32_t type; } disk[NBLOCK]; struct Super { uint32_t s_magic; uint32_t s_nblocks; struct File s_root ; }; void init_disk () { int i, diff; disk[0 ].type = BLOCK_BOOT; nbitblock = (NBLOCK + BLOCK_SIZE_BIT - 1 ) / BLOCK_SIZE_BIT; nextbno = 2 + nbitblock; for (i = 0 ; i < nbitblock; ++i) { disk[2 + i].type = BLOCK_BMAP; } for (i = 0 ; i < nbitblock; ++i) { memset (disk[2 + i].data, 0xff , BLOCK_SIZE); } if (NBLOCK != nbitblock * BLOCK_SIZE_BIT) { diff = NBLOCK % BLOCK_SIZE_BIT / 8 ; memset (disk[2 + (nbitblock - 1 )].data + diff, 0x00 , BLOCK_SIZE - diff); } 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, "/" ); }
文件系统
见Thinking 5.6
用户接口
引入文件描述符(file descriptor)作为用户程序管理、操作文件的基础,隔离底层的文件系统实现,抽象地表示对一个文件进行的操作。
关于Fd
和Filefd
可看Thinking 5.6
我们将一个进程所有的文件描述符存储在[FDTABLE, FILEBASE)
这一地址空间中(使用fd_alloc
分配),在FILEBASE
之上存储对应的Filefd
。
1 2 3 4 5 6 7 void *fd2data (struct Fd *fd) { return (void *)INDEX2DATA(fd2num(fd)); } int fd2num (struct Fd *fd) { return ((u_int)fd - FDTABLE) / PTMAP; }
文件操作如何进行
随后在函数fsipc
中使用IPC向文件服务系统发送请求,在serve_*
中调用相应的fs_*
对文件和磁盘数据进行操作
实验体会
lab5总体看虽然代码量大很吓人,但实际上通过阅读指导书了解了MOS文件系统的精巧设计思路,仿照lab4的思路去理解各个函数之间的调用关系,还是可以比较轻松地完成任务的。只不过确实这次课下没有付出相应的充足时间,导致课上很不熟练、bug频出,最后也没时间做extra了。
然后关于课上debug,一点惨痛教训:指针的类型强制转换,是改变了该指针对一段内存的解释方式,一定要搞清楚要用这个指针去读取什么类型的数据!