跳转至

preview*

实验目的

  • 了解基本的文件系统系统调用的实现方法;
  • 了解一个基于索引节点组织方式的 Simple FS 文件系统的设计与实现;
  • 了解文件系统抽象层-VFS 的设计与实现;

实验七完成了在内核中的同步互斥实验。本次实验涉及的是文件系统,通过分析了解 ucore 文件系统的总体架构设计,完善读写文件操作,从新实现基于文件系统的执行程序机制(即改写 do_execve),从而可以完成执行存储在磁盘上的文件和实现文件读写等功能。

文件系统的设计与实现*

ucore 文件系统总体介绍*

操作系统中负责管理和存储可长期保存数据的软件功能模块成为文件系统。在本次实验中,主要侧重文件系统的涉及实现和对文件系统执行流程的分析与理解。

ucore 的文件系统模型源于 Havard 的 OS161 的文件系统和 Linux 文件系统。但其实这二者都是源于传统的 UNIX 文件系统设计。UNIX 提出了四个文件系统抽象概念:文件(file)、目录项(dentry)、索引节点(inode)和安装点(mount point)。

  • 文件:UNIX 文件中的内容可理解为是一有序字节 buffer,文件都有一个方便应用程序识别的文件名称(也称文件路径名)。典型的文件操作由读、写、创建和删除等。
  • 目录项:目录项不是目录(又称文件路径),而是目录的组成部分。在 UNIX 中目录被看作一种特定的文件,而目录项是文件路径中的一部分。一般而言,目录项包含目录项的名字(文件名或目录名)和目录项的索引节点位置。
  • 索引节点:UNIX 将文件的相关元数据信息(如访问控制权限、大小、拥有者、创建时间、数据内容等等信息)存储在一个单独的数据结构中,该结构被称为索引节点。
  • 安装点:在 UNIX 中,文件系统被安装在一个特定的文件路径位置,这个位置就是安装点。所有的已安装文件系统都作为根文件系统树中的叶子出现在系统中。

上述抽象概念形成了 UNIX 文件系统的逻辑数据结构,并需要通过一个具体文件系统的架构涉及与实现把上述信息映射并储存到磁盘介质上,从而在具体文件系统的磁盘布局(即数据在磁盘上的物理组织)上具体体现出抽象概念。比如文件元数据信息存储在磁盘块中的索引节点上。当文件被载入内存时,内核需要使用磁盘块中的索引点来构造内存中的索引节点。

ucore 模仿了 UNIX 的文件系统设计,ucore 的文件系统架构主要由四部分组成:

  • 通用文件系统访问接口层:该层提供了一个从用户空间到文件系统的标准访问接口。这一层访问接口让应用程序能够通过一个简单的接口获得 ucore 内核的文件系统服务;
  • 文件系统抽象层:向上提供一个一致的接口给内核其他部分(文件系统相关的系统调用实现模块和其他内核功能模块)访问。向下提供一个同样的抽象函数指针列表和数据结构屏蔽不同文件系统的实现细节。
  • Simple FS 文件系统层:一个基于索引方式的简单文件系统实例。向上通过各种具体函数实现以对应文件系统抽象层提出的抽象函数。向下访问外设接口。
  • 外设接口层:向上提供 device 访问接口屏蔽不同的硬件细节。向下实现访问各种具体设备驱动的接口。

文件系统的访问处理过程:加入应用程序操作文件,首先需要通过文件系统的通用文件系统访问接口层给用户空间提供的访问接口进入文件系统内部,接着由文件系统抽象层把访问请求转发个某一具体文件系统,具体文件系统把应用程序的访问请求转化为对磁盘上 block 的处理请求,并通过外设接口层交给磁盘驱动例程来完成具体的磁盘操作。

ucore 文件系统总体结构

主要包含四类主要的数据结构:

  • 超级块(SuperBlock),主要从文件系统的全局角度描述特定文件系统的全局信息,作用范围是整个 OS 空间。
  • 索引节点(inode),主要从文件系统的单个文件的角度描述文件的各种属性和数据所在位置。作用范围是整个 OS 空间。
  • 目录项(dentry),主要从文件系统的文件路径的角度描述了文件路径中的一个特定的目录项。作用范围是整个 OS 空间。对于 SFS 而言,incode 对应于物理磁盘上的具体对象,dentry 是一个内存实体,其中的 ino 成员指向对应的 inode number,另一个成员是 file name。
  • 文件(file),主要从进程的角度描述了一个进程在访问文件时需要了解的文件标识,文件读写的位置,文件引用情况等信息。作用范围是某一具体进程。

通用文件系统访问接口*

对文件操作最基本的相关函数是 open、close、read、write。在读写一个文件之前,首先要用 open 系统调用打开。open 的第一个参数指定文件的路径名,可使用绝对路径名;第二个参数指定打开的方式,可设置为只读、只写和读写。在打开一个文件后,还要用 close 系统调用把它关闭,其参数就是文件描述符 fd。

读写文件内容的系统调用是 read 和 write。read 系统调用由三个参数:一个指定所操作的文件描述符,一个指定读取数据的存放地址,最后一个指定读多少字节。read(fd, buffer, nbytes) 返回值为实际读到的字节数,由于参数无效或磁盘访问错误等原因,使得此次调用无法完成,则返回 -1。write 参数与之完全相同。

对目录而言,最常用的操作是跳转到某个目录,这里对应的用户函数是 chdir。然后就需要读目录的内容了,即列出目录中的文件或目录名,这在处理上与读文件类似,即需要通过 opendir 打开目录,通过 readdir 获取目录中的文件信息,读完还要通过 closedir 函数来关闭目录。目录作为一种特殊的文件,则 open 和 close 操作与文件操作相同,readdir 操作需要调用获取目录项内容的特殊系统调用 sys_getdirentry。而且没有写目录的操作,在目录中增加内容其实就是在此目录中创建文件,需要用到创建文件的函数。

Simple FS 文件系统*

ucore 内核把所有文件都看作是字节流,任何内部逻辑结构都是专用的,由应用程序负责解释。但是 ucore 区分文件的物理结构。ucore 目前支持如下几种类型的文件:

  • 常规文件:文件中包括的内容信息是由应用程序输入。SFS 文件系统在普通文件上不强加任何内部结构,把其文件内容信息看作为字节。
  • 目录:包括一系列的 entry,每个 entry 包括文件名和指向与之相关联的索引节点(index node)的指针。目录是按层次结构组织的。
  • 链接文件:实际上一个链接文件是一个已经存在的文件的另一个可选择的文件名。
  • 设备文件:不包含数据,但是提供了一个映射物理设备到一个文件名的机制,可通过设备文件访问外围设备。
  • 管道:管道是进程间通讯的一个基础设置。管道缓存了其输入端所接受的数据,以便在管道输出端读的进程能一个先进先出的方式来接收数据。

lab8 中关注的主要是 SFS 支持的常规文件、目录和链接中的 hardlink 的涉及实现。SFS 文件系统中目录和常规文件具有共同的属性,而这些属性保存在索引节点中。SFS 通过索引节点来管理目录和常规我呢见,索引节点包含操作系统所需要的关于某个文件的关键信息,比如文件的属性、访问许可权以及其他控制信息都保存在索引节点中。可以有多个文件名可指向一个索引节点。

文件系统的布局*

文件系统通常保存在磁盘上。在本实验中,第三个磁盘即 disk0,前两个磁盘分别是 ucore.img 和 swap.img 用于存访一个 SFS 文件系统。通常文件系统中,磁盘的使用是以扇区为单位的,但是为了实现简便,SFS 中以 block 为基本单位。

第 0 个块是超级快,它包含了关于文件系统的所有关键参数,当计算机被启动或文件系统被首次接触时,超级快的内存就会被装入内存。定义如下:

struct sfs_super {
    uint32_t magic;                            /* magic number, should be SFS_MAGIC */
    uint32_t blocks;                           /* # of blocks in fs */
    uint32_t unused_blocks;                    /* # of unused blocks in fs */
    char info[SFS_MAX_INFO_LEN + 1];           /* infomation for sfs  */
};

包含一个成员变量魔术 magic,其值为 0x2f8dbe2a,内核通过它来检查磁盘镜像是否是合法的 SFS img;成员变量 blocks 记录 SFS 中所有 blocks 的数量,即 img 的大小;成员变量 unuserd_block 记录了 SFS 中还没有被使用的 block 的数量;成员变量 info 包含了字符串 "simple file system"。

第 1 个块放了一个 root-dir 的 inode,用来记录根目录的相关信息。通过这个 root-dir 的 inode 信息就可以定位并查找到根目录下的所有文件信息。

从第 2 个块开始,根据 SFS 中所有块的数量,用 1 为表示一个块的占用和未被占用的情况。这个区域成为 SFS 的 freemap 区域,这将占用若干个块空间。为了更好地记录和管理 freemap 区域,专门提供了 ./kern/fs/sfs/bitmap.c 完成根据一个块号查看或设置对应位的值。

最后在剩余的磁盘空间中,存访了所有其他目录和文件的 inode 信息和内容数据信息。需要注意到的是虽然 inode 的大小小于一个快的大小,但为了实现简单,每个 inode 都占用了一个完整的 block。

在 sfs_fs.c 文件中的 sfs_do_mount 函数中,完成了加载位于硬盘上的 SFS 文件系统的超级块 superblock 和 freemap 的工作。这样,在内存中就有了 SFS 文件系统的全局信息。

实验流程概述*

lab8 增加了文件系统,并因此实现了通过文件系统来加载可执行文件到内存中运行的功能,导致对进程管理相关的实现比较大的调整。

在 kern_init 函数中,增加了 fs_init 函数的调用。fs_init 函数就是文件系统初始化的总控函数,它进一步调用了虚拟文件系统初始化函数 vfs_init,与文件相关的设备初始化函数 dev_init 和 SFS 文件系统的初始化函数 sfs_init。三个初始化函数联合在一起,协同完成了整个虚拟文件系统、SFS 文件系统和文件系统对应的设备的初始化工作。

文件操作实现*

打开文件*


最后更新: November 26, 2020