Lab-5实验报告

Lab-5实验报告

20373915-朱文涛


思考题


  • Thinking 5.1

查阅资料,了解 Linux/Unix 的 /proc 文件系统是什么?有什么作用? Windows 操作系统又是如何实现这些功能的?proc 文件系统这样的设计有什么好处和可以改进的地方?

 

/proc是一种伪文件系统。存储的是当前内核运行状态的一系列特殊文件,用户可以通过这些文件查看有关系统硬件及当前正在运行进程的信息,甚至可以通过更改其中某些文件来改变内核的运行状态。Windows通过提供相关的系统调用改变内核运行状态和查看进程信息。

好处:对系统调用进行了更多的抽象,并将其整合到了文件操作上,降低操作的复杂度。

缺点:需要在内存中实现,占用内存空间。

 

  • Thinking 5.2

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

 

当外部设备产生中断信号或者更新数据时,此时Cache中之前旧的数据可能刚完成缓存,那么完成缓存的这一部分无法完成更新,则会发生错误的数据和行为。

对于串口设备来说,读写频繁,信号多,在相同的时间内发生错误的概论远高于IDE磁盘。

 

  • Thinking 5.3

比较 MOS 操作系统的文件控制块和 Unix/Linux 操作系统的 inode 及相关概念,试述二者的不同之处。

 

 
 
 
 
 
 
 
 struct inode_operations {
     int (*create) (struct inode *,struct dentry *,int);
     struct dentry * (*lookup) (struct inode *,struct dentry *);
     int (*link) (struct dentry *,struct inode *,struct dentry *);
     int (*unlink) (struct inode *,struct dentry *);
     int (*symlink) (struct inode *,struct dentry *,const char *);
     ...
 };

 struct file_operations {
     struct module *owner;
     loff_t (*llseek) (struct file *, loff_t, int);
     ssize_t (*read) (struct file *, char *, size_t, loff_t *);
     ssize_t (*write) (struct file *, const char *, size_t, loff_t *);
     ...
 };

 struct inode {
     ...
     unsigned long       i_ino;
     atomic_t            i_count;
     kdev_t              i_dev;
     umode_t             i_mode;
     nlink_t             i_nlink;
     uid_t               i_uid;
     gid_t               i_gid;
     kdev_t              i_rdev;
     loff_t              i_size;
     time_t              i_atime;
     time_t              i_mtime;
     time_t              i_ctime;
     ...
     struct inode_operations *i_op;
     struct file_operations  *i_fop;
     struct super_block      *i_sb;
     ...
 };
 

Linux系统下FCB储存了文件的所有信息,但索引结点下的FCB 进行了改进,除了文件名之外的文件描述信息都放到索引节点里,这样一来该结构体大小就可以匹配磁盘块的大小,大大提升文件检索速度。如此一来inode模块则封装了很多东西,我们应该重点关注 i_opi_fop 这两个成员。i_op 成员定义对目录相关的操作方法列表,譬如 mkdir()系统调用会触发 inode->i_op->mkdir() 方法,而 link() 系统调用会触发 inode->i_op->link() 方法。而 i_fop 成员则定义了对打开文件后对文件的操作方法列表,譬如 read() 系统调用会触发 inode->i_fop->read() 方法,而 write() 系统调用会触发 inode->i_fop->write() 方法。

 
 
 
 
 
 
 
 struct File {
     u_char f_name[MAXNAMELEN];  // filename
     u_int f_size;           // file size in bytes
     u_int f_type;           // file type
     u_int f_direct[NDIRECT];
     u_int f_indirect;

     struct File *f_dir;     
     // the pointer to the dir where this file is in, valid only in memory.
     u_char f_pad[BY2FILE - MAXNAMELEN - 4 - 4 - NDIRECT * 4 - 4 - 4];
 };
 // file descriptor + file
 struct Filefd {
     struct Fd f_fd;
     u_int f_fileid;
     struct File f_file;
 };
 // file descriptor
 struct Fd {
     u_int fd_dev_id;
     u_int fd_offset;
     u_int fd_omode;
 };
 struct Dev {
     int dev_id;
     char *dev_name;
     int (*dev_read)(struct Fd *, void *, u_int, u_int);
     int (*dev_write)(struct Fd *, const void *, u_int, u_int);
     int (*dev_close)(struct Fd *);
     int (*dev_stat)(struct Fd *, struct Stat *);
     int (*dev_seek)(struct Fd *, u_int);
 };
 

我们实验所编写的MOS文件控制块不是叫的FCB,而是File,对文件进行操作的函数却全都实现在了Dev结构体的接口里。此外我们对文件的操作是依靠进程间通信来完成的,而Linux是直接系统调用完成的。总的来说我们的MOS文件系统性能变低了,但用户空间实现使得可靠性变高了。

 

  • Thinking 5.4

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

 

我们现看include/fs.h中的相关定义:

 
 
 
 
 
 
 
#define BY2BLK    BY2PG
#define BY2PG     4096
#define BY2FILE   256
#define FILE2BLK  (BY2BLK/sizeof(struct File))
 

可知一个磁盘块中最多能存储多少个文件控制块个数为4096/256 = 16个。

我们在观察File结构体定义:

 
 
 
 
 
 
 
struct File {
  u_char f_name[MAXNAMELEN];  // filename
  u_int f_size;     // file size in bytes
  u_int f_type;     // file type
  u_int f_direct[NDIRECT];
  u_int f_indirect;
  struct File *f_dir;
  u_char f_pad[BY2FILE - MAXNAMELEN - 4 - 4 - NDIRECT * 4 - 4 - 4];
};
#define NDIRECT   10
 

首先f_direct[NDIRECT]包含10个文件指针,同时f_indirect指向一个间接磁盘块,用来存储指向文件内容的磁盘块的指针,一个指针4B,一个磁盘块内容4096B,故可以存放1024个指针,而由于实验平台不使用间接磁盘块的前十个指针(方便计算),故我们一个文件夹可以有1024个文件。

那么一个文件最大也就是:1024 * 4kB = 4MB。

 

  • Thinking 5.5

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

 

DISKMAX = 0x40000000,因此支持的最大磁盘大小为1GB。

 

  • Thinking 5.6

如果将 DISKMAX 改成 0xC0000000, 超过用户空间,我们的文件系统还能正常工作吗?为什么?

 

我们的文件系统是放在用户态而不是内核态,而用户态不能访问用户空间以上的内核间,因此文件系统不能正常工作。(panic)

 

  • Thinking 5.7

在 lab5 中,fs/fs.h、include/fs.h 等文件中出现了许多结构体和宏定义,写出你认为比较重要或难以理解的部分,并进行解释。

 

include/fs:

 
 
 
 
 
 
 
#define FSREQ_OPEN  1
········
#define FSREQ_SYNC  7
struct Fsreq_open {
  char req_path[MAXPATHLEN];
  u_int req_omode;
};
···········
struct Fsreq_remove {
  u_char req_path[MAXPATHLEN];
};
 

这是为了文件系统实现用户进程实现各种请求而定义的特定请求结构体,例如Fsreq_open用于处理打开文件请求。fs/fs.h中各种定义看起来也不难理解,不在此赘述。

 

  • Thinking 5.8

阅读 user/file.c,你会发现很多函数中都会将一个 struct Fd* 型的 指针转换为 struct Filefd* 型的指针,请解释为什么这样的转换可行。

 

结构体Filefd的第一个成员就是结构体Fd的变量。因此每个Fd对应一个Filefd,而Filefd其他成员变量在内存上紧跟着Fd,因此转换可行。

 

  • Thinking 5.9

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

 

我们的操作系统在父进程 fork 了子进程后,两个进程会共享文件描述符表,也同时会共用文件描述符的偏移指针,示例如下:

文件内容如下:

 
 
 
 
 
 
 
BUAA OS is my shit love!
 

代码如下:

 
 
 
 
 
 
 
int r, 
fdnum, n; 
char buf[200]; 
fdnum = open("/newmotd", O_RDWR); 
if ((r = fork()) == 0) { 
    n = read(fdnum, buf, 4); 
    writef("[child] buffer is \'%s\'\n", buf); 
} else { 
    n = read(fdnum, buf, 4); 
    writef("[father] buffer is \'%s\'\n", buf); 
}
 

输出如下:

 
 
 
 
 
 
 
[father] buffer is 'BUAA'
[child] buffer is ' OS '
 

 

  • Thinking 5.10

请解释Fd, Filefd, Open 结构体及其各个域的作用。比如各个结构体会在哪些过程中被使用,是否对应磁盘上的物理实体还是单纯的内存数据等。说明形式自定,要求简洁明了,可大致勾勒出文件系统数据结构与物理实体的对应关系与设计框架。

 

Fd用于记录文件的基本信息,需要单独占用一页:

 
 
 
 
 
 
 
 // file descriptor
 struct Fd {
     u_int fd_dev_id;     // 外设的id
     u_int fd_offset;     // 读写的偏移量
     u_int fd_omode;      // 允许用户进程对文件的操作权限,包括只读、只写、读写
 };
 

Filefd用于记录文件的详细信息,Fd也存储在其中:

 
 
 
 
 
 
 
 // file descriptor + file
 struct Filefd {
     struct Fd f_fd;     // 文件描述符
     u_int f_fileid;     // 文件的id
     struct File f_file; // 对应文件的文件控制块
 };
 

Open是打开文件行为的抽象:

 
 
 
 
 
 
 
 struct Open {
     struct File *o_file;    // 指向该文件的文件控制块的指针
     u_int o_fileid;         // 打开文件的id
     int o_mode;             // 允许用户进程对文件的操作权限
     struct Filefd *o_ff;    // 指向该文件描述符的指针
 };
 

 

  • Thinking 5.11

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

  1. 同步消息,用黑三角箭头搭配黑实线表示:

image

同步意义:消息的发送者把进程控制传递给消息的接收者,然后暂停活动等待消息接收者的回应消息。

  1. 异步消息,用开三角箭头和黑色实线表示:

    image

     

异步的意义:消息的发送者将消息发送给消息的接受者后,不用等待回应的消息,即可开始另一个活动

  1. 返回消息,用开三角箭头搭配黑色虚线表示:

    image

返回消息和同步消息结合使用,因为异步消息不进行等待,所以不需要知道返回值。

  1. 创建消息,用开三角箭头搭配黑色虚线表示,其下面特别注明 <<create>>

    image

创建消息用来创建一个实例。

  1. 摧毁消息,用黑三角箭头搭配黑实线表示,其下面特别注明 <<destroy>>

    image

摧毁消息用来摧毁一个实例,生命线上会出现一个 X 表示结束。

  1. Lost and Found Message,这类消息的特点是它可能没有发送者或者接收者,用一个黑色实心的

点和黑色实心三角箭头黑实线表示:

image

 

对于文件系统,通过特定调用号使得文件系统知道请求者有何种需求,然后文件系统进入相应处理函数中处理,将结果通过ipc_send传回用户进程。

 

  • Thinking 5.12

阅读serv.c/serve函数的代码,我们注意到函数中包含了一个死循环for (;;) {...},为什么这段代码不会导致整个内核进入panic 状态?

 

serve进程随着系统运行开始时开始执行,每次循环都调用了ipc_recv,该进程会进入NOT_RUNNABLE状态,随时相应用户进程发出的文件请求,不会一直占用CPU,直到结束系统杀死进程。此进程为用户态进程,不会导致内核进程陷入panic。

实验难点


文件系统层次关系梳理

文件系统可以分成四个层次:

文件系统用户接口、文件系统抽象层、文件系统具体实现、文件系统设备接口。

文件系统的逐级调用关系可以用下图表示:

img

 

 

多级目录与多级索引

多级目录用来管理文件间的层次关系,多级索引用来管理单个文件的数据。两者并不是一个概念。

在我们的实验中,目录文件的内容是文件控制块struct File,目录文件的每一个数据块由16个文件目录项组成,找到相应的文件名,找到对应文件的FCB,进而找到该文件的数据块。

多级索引用来管理文件的数据块,我们的实验中使用二级索引机制。在FCB中可以有一级索引数组f_direct[NDIRECT],和一个二级索引f_indirect,二级索引找到的磁盘块为索引块,这个磁盘块可以存储1024的FCB。

目录文件综合体现了这一点,以下图来说明:

img

 

文件系统的IPC机制

文件系统通过IPC进行用户进程与文件系统进程的通信,很好地体现了OS多进程的处理过程。

文件系统的通信过程如下所示:

  1. 用户在通过用户接口最终调用相应的fsipc_xxxx()函数,

  2. 最终调用fsipc()将消息发送给文件系统进程。

  3. 文件系统进程中serve()函数调用ipc_recv接收用户传递来的信息,

  4. 根据req类型调用相应的serve_xxxx()函数,

  5. 对应的serve_xxxx()处理完毕后通过ipc_send()将信息发送给用户进程。

  6. 用户进程中最终又通过ipc_recv()接收文件系统进程发送回来的消息,最终完成一次完整的ipc通信过程。

 

设备驱动

设备驱动的难点其一在于要找到不同的设备寄存器对应的虚拟地址,其二是要对设备寄存器进行正确顺序的读写。

设备寄存器的虚拟地址分为三个部分:

/*	BASE:物理地址到虚拟地址的转换,本次实验中我们映射外设使用的是不经过cache的kseg1地址段,这个地址是0xa0000000
PHY:设备的物理基地址,不同的设备有不同的物理地址,在ide磁盘中,这个地址是0x13000000
OFFSET:设备不同的寄存器对应的物理地址偏移不同,具体情况需要查阅手册 */

虚拟地址通过BASE + PHY + OFFSET计算得到。

其次需要对设备寄存器进行合理顺序的读写,以写磁盘为例:

  1. 将写入数据写入扇区数据缓冲区

  2. 写入disk编号

  3. 写入写的偏移

  4. 写入“磁盘写”的标记,使磁盘开始写

  5. 读出状态,判断写入操作是否成功

读磁盘与写磁盘过程类似。

体会与感想


本次lab花费20h。一部分原因主要是提示太少;一部分原因是MOS的代码结构比较混乱,让我阅读起来力不从心。对于文件系统接口部分内容,MOOC说的过于简洁了,导致我在实现的时候根本无从下手,于是又花了好长时间去了解文件接口以及IPC相关的机制,确实挺累的。

不过通过这段时间的学习,我对C语言的了解更加充分、更加透彻,对操作系统的工作原理有了具体的理解,而不是最开始简单的几行文字。

残留疑点


  • nbitblock = (NBLOCK + BIT2BLK - 1) / BIT2BLK;这是用于计算表示磁盘块空闲状态所需要的磁盘块的式子,不太理解。

  • 代码中多次出现f->file_size / BY2BLK求文件的块数,由于文件的size只能以BY2BLK为单位增长减少,所以这样计算没有错误,但是为了严谨,最好改写成向上取整,即(f->file_size + BY2BLK - 1) / BY2BLK

posted @ 2022-07-06 22:26  `Demon  阅读(65)  评论(0编辑  收藏  举报