【Ucore操作系统】6. 文件系统与I/O重定向

03-27 1069阅读 0评论

文章目录

  • 【 0. 引言 】
  • 【 1. 文件系统接口 】
    • 1.1 文件和目录
      • 1.1.1 常规文件
      • 1.1.2 目录
      • 1.1.3 文件系统
      • 1.2 简易文件与目录抽象
        • 1.3 打开文件与读写文件的系统调用
          • 1.3.1 文件的打开
          • 1.3.2 文件的顺序读写
          • 【 2. nfs 文件系统 】
            • 2.1 文件系统布局
            • 2.2 virtio 磁盘驱动
            • 2.3 磁盘块缓存
            • 2.4 inode的操作
            • 2.5 文件在进程中的结构
            • 2.6 获取文件对应的inode
            • 2.7 文件关闭

              【 0. 引言 】

              • 文件的最早起源于我们需要把数据持久保存在 Persistent Storage 持久存储设备 上的需求。

                大家不要被 持久存储设备 这个词给吓住了,这就是指计算机远古时代的 卡片、纸带、磁芯、磁鼓,和现在还在使用磁带、磁盘、硬盘,还有近期逐渐普及的 U盘、闪存、固态硬盘 (SSD, Solid-State Drive)等存储设备。我们可以把这些设备叫做 外存 。在此之前我们仅使用一种存储,也就是 内存(或称 RAM)。

                • 相比内存,外存的读写速度较慢,容量较大,但内存掉电后信息会丢失,外存在掉电之后并不会丢失数据。因此,将需要持久保存的数据从内存写入到外存,或是从外存读入到内存 是应用和操作系统必不可少的一种需求。
                  • 文件系统的历史背景

                    文件系统在UNIX操作系统有着特殊的地位,根据史料《UNIX: A History and a Memoir》记载,1969年,Ken Thompson(Unix的作者)在贝尔实验室比较闲,写了PDP-7计算机的磁盘调度算法来提高磁盘的吞吐量。为了测试这个算法,他本来想写一个批量读写数据的测试程序。但写着写着,他在某一时刻发现,这个测试程序再扩展一下,就是一个文件系统了,再再扩展一下,就是一个操作系统了。他的自觉告诉他,他离实现一个操作系统仅有 三周之遥 。一周:写代码编辑器;一周:写汇编器;一周写shell程序,在写这些程序的同时,需要添加操作系统的功能(如 exec等系统调用)以支持这些应用。结果三周后,为测试磁盘调度算法性能的UNIX雏形诞生了。

                    • 本章任务
                      • 本章我们将实现一个简单的文件系统 – easyfs,能够对 持久存储设备 这种 I/O 资源进行管理。对于应用访问持久存储设备的需求,内核需要新增两种文件:常规文件和目录文件,它们均以文件系统所维护的 磁盘文件 形式被组织并保存在持久存储设备上。
                      • 同时,由于我们进一步完善了对 文件 这一抽象概念的实现,我们可以更容易建立 Everything is a file 一切皆文件 的UNIX的重要设计哲学。我们可扩展与应用程序执行相关的 exec 系统调用,加入对程序运行参数的支持,并进一步改进了对shell程序自身的实现,加入对重定向符号 > 、 【 1. 文件系统接口 】
                        • 本节我们首先以 Linux 上的常规文件和目录为例,站在访问文件的应用的角度,介绍文件中值得注意的地方及文件使用方法。由于 Linux 上的文件系统模型还是比较复杂,在我们的内核实现中对它进行了很大程度的简化,我们会对简化的具体情形进行介绍。最后,我们介绍我们内核上应用的开发者应该如何使用我们简化后的文件系统和一些相关知识。

                          1.1 文件和目录

                          1.1.1 常规文件

                          • 在操作系统的用户看来, 常规文件是保存在持久存储设备上的一个字节序列,每个常规文件都有一个 文件名 (Filename) ,用户需要通过它来区分不同的常规文件。方便起见,在下面的描述中,“文件”有可能指的是常规文件、目录,也可能是之前提到的若干种进程可以读写的 标准输出、标准输入、管道等I/O 资源,请读者自行根据上下文判断取哪种含义。
                          • 在 Linux 系统上, stat 工具可以获取文件的一些信息。下面以我们项目中的一个源代码文件 os/main.c 为例,stat 工具展示了 main.c 的如下信息:
                            • File 表明它的文件名为 main.c 。
                            • Size 表明它的字节大小为 491 字节。
                            • Blocks 表明它占据 8 个 块 (Block) 来存储。在文件系统中,文件的数据以块为单位进行存储,在 IO Block 可以看出在 Ubuntu 系统中每个块的大小为 4096 字节。
                            • regular file 表明这个文件是一个常规文件。事实上,其他类型的文件也可以通过文件名来进行访问。
                            • Device 当文件是一个特殊文件(如块设备文件或者字符设备文件的时候),Device 将指出该特殊文件的 major/minor ID 。对于一个常规文件,我们无需关心它。
                            • Inode 表示文件的底层编号。在文件系统的底层实现中,并不是直接通过文件名来索引文件,而是首先需要将文件名转化为文件的底层编号,再根据这个编号去索引文件。然而,用户无需关心这一信息。
                            • Links 给出文件的硬链接数。同一个文件系统中如果两个文件(目录也是文件)具有相同的inode号码,那么就称它们是 硬链接 关系。这样links的值其实是 一个文件的不同文件名的数量。(本章的练习需要你在文件系统中实现硬链接!)
                            • Uid 给出该文件的所属的用户 ID , Gid 给出该文件所属的用户组 ID 。
                            • Access 的其中一种表示是一个长度为 10 的字符串(这里是 -rw-r–r-- ),其中第 1 位给出该文件的类型,这个文件是一个常规文件,因此这第 1 位为 - 。后面的 9 位可以分为三组,分别表示该文件的所有者/在该文件所属的用户组内的其他用户以及剩下的所有用户能够读取/写入/将该文件作为一个可执行文件来执行。
                            • Access/Modify 分别给出该文件的最近一次访问/最近一次修改时间。
                              $ stat os/main.c
                              File: os/main.c
                              Size: 491             Blocks: 8          IO Block: 4096   regular file
                              Device: 805h/2053d      Inode: 4726542     Links: 1
                              Access: (0664/-rw-rw-r--)  Uid: ( 1000/deathwish)   Gid: ( 1000/deathwish)
                              Access: 2021-09-08 17:52:06.915389371 +0800
                              Modify: 2021-09-08 17:52:06.127425836 +0800
                              Change: 2021-09-08 17:52:06.127425836 +0800
                              Birth: -
                              
                              • 用户常常通过文件的 拓展名 (Filename extension) 来推断该文件的用途,如 main.c 的拓展名是 .c ,我们由此知道它是一个 C 代码文件。但从内核的角度来看,它会将所有文件无差别的看成一个字节序列,文件内容的结构和含义则是交给对应的应用进行解析。

                                1.1.2 目录

                                • 最早的文件系统 仅仅通过文件名来区分文件,但是这会造成一些归档和管理上的困难。如今我们的使用习惯是将文件根据功能、属性的不同分类归档到不同层级的目录之下,这样我们就很容易逐级找到想要的文件。结合用户和用户组的概念,目录的存在也使得权限控制更加容易,只需要对于目录进行设置就可以间接设置用户/用户组对该目录下所有文件的访问权限,这使得操作系统能够更加安全的支持多用户。
                                • 同样可以通过 stat 工具获取目录的一些信息:
                                  • directory 表明 os 是一个目录,从 Access 字符串的首位 d 也可以看出这一点。对于目录而言, Access 的 rwx 含义有所不同:r 表示是否允许获取该目录下有哪些文件和子目录;w 表示是否允许在该目录下创建/删除文件和子目录;x 表示是否允许“通过”该目录。
                                  • Blocks 给出 os 目录也占用 8 个块进行存储。实际上目录也可以看作一种常规文件,它也有属于自己的底层编号,它的内容中保存着若干 目录项 (Dirent, Directory Entry) ,可以看成一组映射,根据它下面的文件或子目录的文件名或目录名能够查到文件和子目录在文件系统中的底层编号,即 Inode 编号。但是与常规文件不同的是,用户无法 直接 修改目录的内容,只能通过创建/删除它下面的文件或子目录才能间接做到这一点。
                                    $ stat os
                                    File: os/main.c
                                    Size: 491             Blocks: 8          IO Block: 4096   regular file
                                    Device: 805h/2053d      Inode: 4726542     Links: 1
                                    Access: (0664/-rw-rw-r--)  Uid: ( 1000/deathwish)   Gid: ( 1000/deathwish)
                                    Access: 2021-09-08 17:52:06.915389371 +0800
                                    Modify: 2021-09-08 17:52:06.127425836 +0800
                                    Change: 2021-09-08 17:52:06.127425836 +0800
                                    Birth: -
                                    
                                    • 目录树

                                      有了目录之后,我们就可以将所有的文件和目录组织为一种被称为 目录树 (Directory Tree) 的有根树结构(不考虑软链接)。树中的每个节点都是一个文件或目录,一个目录下面的所有的文件和子目录都是它的孩子。可以看出 所有的文件都是目录树的叶子节点。目录树的根节点也是一个目录,它被称为 根目录 (Root Directory)。

                                    • 绝对路径
                                      【Ucore操作系统】6. 文件系统与I/O重定向,【Ucore操作系统】6. 文件系统与I/O重定向,词库加载错误:未能找到文件“C:\Users\Administrator\Desktop\火车头9.8破解版\Configuration\Dict_Stopwords.txt”。,使用,我们,访问,第1张
                                      (图片来源网络,侵删)

                                      目录树中的每个目录和文件都可以用它的 绝对路径 (Absolute Path) 来进行索引, 绝对路径是目录树上的根节点到待索引的目录和文件所在的节点之间自上而下的路径上的所有节点的文件或目录名两两之间加上路径分隔符拼接得到的。例如,在 Linux 上,根目录的绝对路径是 / ,路径分隔符也是 / ,例如: main.c 的绝对路径是 /home/oslab/workspace/UCORE/uCore-Tutorial-v2/os/main.c

                                    • 相对路径

                                      一般情况下,绝对路径都很长,用起来颇为不便。而且,在日常使用中,我们通常固定在一个工作目录下而不会频繁切换目录。因此更为常用的是 相对路径 (Relative Path) 而非绝对路径。

                                      • 每个进程都会记录自己当前所在的工作目录,当它在索引文件或目录的时候,如果传给它的路径并未以 / 开头则会被内核认为是一个相对于进程当前工作目录的相对路径,这个路径会被拼接在进程当前路径的后面组成一个绝对路径,实际索引的是这个绝对路径对应的文件或目录。其中, ./ 表示当前目录,而 …/ 表示当前目录的父目录,这在通过相对路径进行索引的时候非常实用。
                                      • 在使用终端的时候, pwd 工具可以打印终端进程当前所在的目录,而通过 cd 可以切换终端进程的工作目录。
                                      • 引入目录后文件的索引流程

                                        一旦引入目录之后,我们就不再单纯的通过文件名来索引文件,而是通过路径(绝对或相对)进行索引。在文件系统的底层实现中,也是对应的先将路径转化为一个文件或目录的底层编号,然后再通过这个编号具体索引文件或目录。将路径转化为底层编号的过程是逐级进行的,对于绝对路径的情况,需要从根目录出发,每次根据当前目录底层编号获取到它的内容,根据下一级子目录的目录名查到该子目录的底层编号,然后从该子目录继续向下遍历,依此类推。在这个过程目录的权限控制位将会起到保护作用,阻止无权限用户进行访问。

                                        目录是否有必要存在?

                                        • 基于路径的索引难以并行或分布式化,因为我们总是需要查到一级目录的底层编号才能查到下一级,这是一个天然串行的过程。在一些性能需求极高的环境中,可以考虑弱化目录的权限控制职能,将目录树结构扁平化,将文件系统的磁盘布局变为类键值对存储。

                                          1.1.3 文件系统

                                          • 常规文件和目录都是实际保存在持久存储设备中的。持久存储设备仅支持以扇区为单位的随机读写,这和上面介绍的通过路径即可索引到文件并进行读写的用户视角有很大的不同。负责中间转换的便是 文件系统 (File System) 。具体而言, 文件系统负责将逻辑上的目录树结构(包括其中每个文件或目录的数据和其他信息)映射到持久存储设备上,决定设备上的每个扇区各应存储哪些内容。反过来,文件系统也可以从持久存储设备还原出逻辑上的目录树结构。
                                          • 文件系统有很多种不同的实现,每一种都能将同一个逻辑上目录树结构转化为一个不同的持久存储设备上的扇区布局。最著名的文件系统有 Windows 上的 FAT/NTFS 和 Linux 上的 ext3/ext4 等。
                                          • 在一个计算机系统中,可以同时包含 多个持久存储设备,它们上面的数据可能是以不同文件系统格式存储的。为了能够对它们进行统一管理,在内核中有一层 虚拟文件系统 (VFS, Virtual File System) ,它规定了逻辑上目录树结构的通用格式及相关操作的抽象接口,只要不同的底层文件系统均实现虚拟文件系统要求的那些抽象接口,再加上 挂载 (Mount) 等方式,这些持久存储设备上的不同文件系统便可以用一个统一的逻辑目录树结构一并进行管理。

                                            1.2 简易文件与目录抽象

                                            • 我们的内核实现对于目录树结构进行了很大程度上的简化,这样做的目的是为了能够完整的展示文件系统的工作原理,但代码量又不至于太多。我们进行的简化如下:
                                              • 扁平化:仅存在根目录 / 一个目录,剩下所有的文件都放在根目录内。在索引一个文件的时候,我们直接使用文件的文件名而不是它含有 / 的绝对路径。
                                              • 权限控制:我们不设置用户和用户组概念,全程只有单用户。同时根目录和其他文件也都没有权限控制位,即完全不限制文件的访问方式,不会区分文件是否可执行。
                                              • 不记录文件访问/修改的任何时间戳。
                                              • 不支持软硬链接。
                                              • 除了下面即将介绍的系统调用之外,其他的很多文件系统相关系统调用均未实现。

                                                1.3 打开文件与读写文件的系统调用

                                                1.3.1 文件的打开

                                                • 在读写一个常规文件之前,应用首先需要通过内核提供的 sys_open 系统调用让该文件在进程的文件描述符表中占一项,并得到操作系统的返回值–文件描述符,即文件关联的表项在文件描述表中的索引值:
                                                  /// 功能:打开一个常规文件,并返回可以访问它的文件描述符。
                                                  /// 参数:path 描述要打开的文件的文件名(简单起见,文件系统不需要支持目录,所有的文件都放在根目录 / 下),
                                                  /// flags 描述打开文件的标志,具体含义下面给出。
                                                  /// 返回值:如果出现了错误则返回 -1,否则返回打开常规文件的文件描述符。可能的错误原因是:文件不存在。
                                                  /// syscall ID:56
                                                  int open(int dirfd, char* path, unsigned int flags, unsigned int mode);
                                                  
                                                  • 目前我们的内核支持以下几种标志(多种不同标志可能共存):
                                                    • 如果 flags 为 0,则表示以只读模式 RDONLY 打开;
                                                    • 如果 flags 第 0 位被设置(0x001),表示以只写模式 WRONLY 打开;
                                                    • 如果 flags 第 1 位被设置(0x002),表示既可读又可写 RDWR ;
                                                    • 如果 flags 第 9 位被设置(0x200),表示允许创建文件 CREATE ,在找不到该文件的时候应创建文件;如果该文件已经存在则应该将该文件的大小归零;
                                                    • 如果 flags 第 10 位被设置(0x400),则在打开文件的时候应该清空文件的内容并将该文件的大小归零,也即 TRUNC 。我们本章不涉及这个flags。
                                                    • 注意 flags 里面的权限设置只能控制进程对本次打开的文件的访问。一般情况下,在打开文件的时候首先需要经过文件系统的权限检查,比如一个文件自身不允许写入,那么进程自然也就不能以 WRONLY 或 RDWR 标志打开文件。但在我们简化版的文件系统中文件不进行权限设置,这一步就可以绕过。

                                                      1.3.2 文件的顺序读写

                                                      • 在打开一个文件获得其fd之后,我们就可以用之前的 sys_read/sys_write 两个系统调用来对它进行读写了。需要注意的是,常规文件的读写模式和之前介绍过的几种文件有所不同。标准输入输出和匿名管道都属于一种流式读写,而常规文件则是顺序读写和随机读写的结合:由于常规文件可以看成一段字节序列,我们应该能够随意读写它的任一段区间的数据,即随机读写;然而用户仅仅通过 sys_read/sys_write 两个系统调用不能做到这一点。
                                                      • 大家应该使用C时应该知道,读写文件都是有一个偏移量的,即下一次读写的起始位置是由上一次读写的结束位置决定的。我们可以使用lseek函数来改变这个偏移的位置(本章不需实现)。顺带一提,在文件系统的底层实现中都是对文件进行随机读写的。

                                                        【 2. nfs 文件系统 】

                                                        • 本节我们简单介绍一下我们实现的nfs操作系统。本章中新增加的代码很多,但是大家如果想研读的话理解起来不太困难。很多函数只看名字也可以知道其功效,不需要再探究其实现的方式。

                                                          2.1 文件系统布局

                                                          • 我们的nfs文件系统十分类似ext4文件系统,下面我们可以看一下nfs文件系统的布局:
                                                            // 基本信息:块大小 BSIZE = 1024B,总容量 FSSIZE = 1000 个 block = 1000 * 1024 B。 // Layout: // 0号块留待后续拓展,可以忽略。superblock 固定为 1 号块,size 固定为一个块。 // 其后是储存 inode 的若干个块,占用块数 = inode 上限 / 每个块上可以容纳的 inode 数量, // 其中 inode 上限固定为 200,每个块的容量 = BSIZE / sizeof(struct disk_inode) // 再之后是数据块相关内容,包含一个 储存空闲块位置的 bitmap 和 实际的数据块,bitmap 块 // 数量固定为 NBITMAP = FSSIZE / (BSIZE * 8) + 1 = 1000 / 8 + 1 = 126 块。 // [ boot block | sb block | inode blocks | free bit map | data blocks ]
                                                            
                                                            • 注意:不推荐同学们修改该布局,除非你完全看懂了 fs 的逻辑,所以最好不要改变 disk_inode 这个结构的大小,如果想要增删字段,一定使用 pad。这个布局具体定义的位置在nfs/fs.c之中。
                                                            • 我们定义的inode和data blocks实际上和ext4中同名的结构功能几乎是一样的。索引节点 (Inode, Index Node) 是文件系统中的一种重要数据结构。逻辑目录树结构中的每个文件和目录都对应一个 inode ,我们前面提到的在文件系统实现中文件/目录的底层编号实际上就是指 inode 编号。在 inode 中不仅包含了我们通过 stat 工具能够看到的文件/目录的元数据(大小/访问权限/类型等信息),还包含实际保存对应文件/目录数据的数据块(位于最后的数据块区域中)的索引信息,从而能够找到文件/目录的数据被保存在磁盘的哪些块中。从索引方式上看,同时支持直接索引和间接索引。
                                                            • 下面我们看一下它们在我们C中对应的具体结构体:
                                                              // 超级块位置固定,用来指示文件系统的一些元数据,这里最重要的是 inodestart 和 bmapstart
                                                              struct superblock {
                                                                  uint magic;     // Must be FSMAGIC
                                                                  uint size;      // Size of file system image (blocks)
                                                                  uint nblocks;   // Number of data blocks
                                                                  uint ninodes;   // Number of inodes.
                                                                  uint inodestart;// Block number of first inode block
                                                                  uint bmapstart; // Block number of first free map block
                                                              };
                                                              // On-disk inode structure
                                                              // 储存磁盘 inode 信息,主要是文件类型和数据块的索引,其大小影响磁盘布局,不要乱改,可以用 pad
                                                              struct dinode {
                                                                  short type;             // File type
                                                                  short pad[3];
                                                                  uint size;              // Size of file (bytes)
                                                                  uint addrs[NDIRECT + 1];// Data block addresses
                                                              };
                                                              // in-memory copy of an inode
                                                              // dinode 的内存缓存,为了方便,增加了 dev, inum, ref, valid 四项管理信息,大小无所谓,可以随便改。
                                                              struct inode {
                                                                  uint dev;           // Device number
                                                                  uint inum;          // Inode number
                                                                  int ref;            // Reference count
                                                                  int valid;          // inode has been read from disk?
                                                                  short type;         // copy of disk inode
                                                                  uint size;
                                                                  uint addrs[NDIRECT+1];  // data block num
                                                              };
                                                              // 目录对应的数据块的内容本质是 filename 到 file inode_num 的一个 map,这里为了简单,就存为一个 `dirent` 数组,查找的时候遍历对比
                                                              struct dirent {
                                                                  ushort inum;
                                                                  char name[DIRSIZ];
                                                              };
                                                              // 数据块缓存结构体。
                                                              struct buf {
                                                                  int valid;   // has data been read from disk?
                                                                  int disk;    // does disk "own" buf?
                                                                  uint dev;
                                                                  uint blockno;
                                                                  uint refcnt;
                                                                  struct buf *prev; // LRU cache list
                                                                  struct buf *next;
                                                                  uchar data[BSIZE];
                                                              };
                                                              
                                                              • 注意几个量的概念: - block num: 表示某一个磁盘块的编号。我们操作数据块会把它读入内存的数据块缓存之中,其结构体见上。 - inode num: 表示某一个 inode 在所有 inode 项里的编号。注意 inode blocks 其实就是一个 inode 的大数组。
                                                              • 同时,目录本身是一个 filename 到 file对应的inode_num的map,可以完成 filename 到 inode_num 的转化。
                                                              • OS启动后是没有inode的内存缓存的。下面我们自底向上走一遍OS打开已存在在磁盘上文件的过程,让大家熟悉一下nfs的具体实现方式。

                                                                2.2 virtio 磁盘驱动

                                                                • 注意:这一部分代码不需要同学们详细了解细节,但需要知道大概的过程。在 uCore-Tutorial 中磁盘块的读写是通过中断处理的。在 virtio.h 和 virtio-disk.c 中我们按照 qemu 对 virtio 的定义,实现了 virtio_disk_init 和 virtio_disk_rw 两个函数,前者完成磁盘设备的初始化和对其管理的初始化。virtio_disk_rw 实际完成磁盘IO,当设定好读写信息后会通过 MMIO 的方式通知磁盘开始写。然后,os 会开启中断并开始死等磁盘读写完成。当磁盘完成 IO 后,磁盘会触发一个外部中断,在中断处理中会把死循环条件解除。内核态只会在处理磁盘读写的时候短暂开启中断,之后会马上关闭。
                                                                  virtio_disk_rw(struct buf *b, int write) {
                                                                      /// ... set IO config
                                                                      *R(VIRTIO_MMIO_QUEUE_NOTIFY) = 0;               // notify the disk to carry out IO
                                                                      struct buf volatile * _b = b;                   // Make sure complier will load 'b' form memory
                                                                      intr_on();
                                                                      while(_b->disk == 1);   // _b->disk == 0 means that this IO is done
                                                                      intr_off();
                                                                  }
                                                                  // 开启和关闭中断的函数。
                                                                  static inline void intr_on() { w_sstatus(r_sstatus() | SSTATUS_SIE); }
                                                                  // disable device interrupts
                                                                  static inline void intr_off() { w_sstatus(r_sstatus() & ~SSTATUS_SIE); }
                                                                  
                                                                  • 对于内核中断处理的修改在trap.c之中。之前我们的trap from kernel会直接panic,现在我们需要添加对外部中断的处理。kerneltrap也需要类似usertrap的保存上下文以及回到原处的kernelvec以及kernelret函数。进入内核之后要单独设置stvec指向kernelvec处。
                                                                    # kernelvec.S
                                                                    kernelvec:
                                                                            // make room to save registers.
                                                                            addi sp, sp, -256
                                                                            // save the registers expect x0
                                                                            sd ra, 0(sp)
                                                                            sd sp, 8(sp)
                                                                            sd gp, 16(sp)
                                                                            // ...
                                                                            sd t4, 224(sp)
                                                                            sd t5, 232(sp)
                                                                            sd t6, 240(sp)
                                                                            call kerneltrap
                                                                    kernelret:
                                                                            // restore registers.
                                                                            // 思考:为什么直接就使用了sp?
                                                                            ld ra, 0(sp)
                                                                            ld sp, 8(sp)
                                                                            ld gp, 16(sp)
                                                                            // restore all registers expect x0
                                                                            ld t4, 224(sp)
                                                                            ld t5, 232(sp)
                                                                            ld t6, 240(sp)
                                                                            addi sp, sp, 256
                                                                            sret
                                                                    
                                                                    • kerneltrap具体的修改如下:
                                                                      void kerneltrap() {
                                                                          // 老三样,不过在这里把处理放到了 C 代码中
                                                                          uint64 sepc = r_sepc();
                                                                          uint64 sstatus = r_sstatus();
                                                                          uint64 scause = r_scause();
                                                                          if ((sstatus & SSTATUS_SPP) == 0)
                                                                              panic("kerneltrap: not from supervisor mode");
                                                                          if (scause & (1ULL 
                                                                              // 可能发生时钟中断和外部中断,我们的主要目标是处理外部中断
                                                                              devintr(scause & 0xff);
                                                                          } else {
                                                                              // kernel 发生异常就挣扎了,肯定出问题了,杀掉用户线程跑路
                                                                              error("invalid trap from kernel: %p, stval = %p sepc = %p\n", scause, r_stval(), sepc);
                                                                              exit(-1);
                                                                          }
                                                                      }
                                                                      // 外部中断处理函数
                                                                      void devintr(uint64 cause) {
                                                                          int irq;
                                                                          switch (cause) {
                                                                              case SupervisorTimer:
                                                                                  set_next_timer();
                                                                                  // 时钟中断如果发生在内核态,不切换进程,原因分析在下面
                                                                                  // 如果发生在用户态,照常处理
                                                                                  if((r_sstatus() & SSTATUS_SPP) == 0) {
                                                                                      yield();
                                                                                  }
                                                                                  break;
                                                                              case SupervisorExternal:
                                                                                  irq = plic_claim();
                                                                                  if (irq == UART0_IRQ) {         // UART 串口的终端不需要处理,这个 rustsbi 替我们处理好了
                                                                                      // do nothing
                                                                                  } else if (irq == VIRTIO0_IRQ) {        // 我们等的就是这个中断
                                                                                      virtio_disk_intr();
                                                                                  }
                                                                                  if (irq)
                                                                                      plic_complete(irq);         // 表明中断已经处理完毕
                                                                                  break;
                                                                          }
                                                                      }
                                                                      
                                                                      ullivirtio_disk_intr() 会把 buf->disk 置零,这样中断返回后死循环条件解除,程序可以继续运行。具体代码在 virtio-disk.c 中。
                                                                    • 这里还需要注意的一点是,为什么始终不允许内核发生进程切换呢?只是由于我们的内核并没有并发的支持,相关的数据结构没有锁或者其他机制保护。考虑这样一种情况,一个进程读写一个文件,内核处理等待磁盘相应时,发生时钟中断切换到了其他进程,然而另一个进程也要读写同一个文件,这就可能发生数据访问上的冲突,甚至导致磁盘出现错误的行为。这也是为什么内核态一直不处理时钟中断,我们必须保证每一次内核的操作都是原子的,不能被打断。大家可以想一想,如果内核可以随时切换,当前有那些数据结构可能被破坏。提示:想想 kalloc 分配到一半,进程 switch 切换到一半之类的。

                                                                      2.3 磁盘块缓存

                                                                      • 为了加快磁盘访问的速度,在内核中设置了磁盘缓存 struct buf,一个 buf 对应一个磁盘 block,这一部分代码也不要求同学们深入掌握。大致的作用机制是,对磁盘的读写都会被转化为对 buf 的读写,当 buf 有效时,读写 buf,buf 无效时(类似页表缺页和 TLB 缺失),就实际读写磁盘,将 buf 变得有效,然后继续读写 buf。详细的内容在 buf.h 和 bio.c 中。buf 写回的时机是 buf 池满需要替换的时候(类似内存的 swap 策略) 手动写回。如果 buf 没有写回,一但掉电就 GG 了,所以手动写回还是挺重要的。
                                                                        // os/bio.c
                                                                        struct buf *
                                                                        bread(uint dev, uint blockno) {
                                                                            struct buf *b;
                                                                            b = bget(dev, blockno);
                                                                            if (!b->valid) {
                                                                                virtio_disk_rw(b, R);
                                                                                b->valid = 1;
                                                                            }
                                                                            return b;
                                                                        }
                                                                        // Write b's contents to disk.
                                                                        void bwrite(struct buf *b) {
                                                                            virtio_disk_rw(b, W);
                                                                        }
                                                                        
                                                                        • 读取文件数据实际就是读取文件inode指向数据块的数据。读数据块到缓存的数据需要使用bread,而写回缓存需要用到bwrite函数。文件系统首先使用bget去查缓存中是否已有对应的block,如果没有会分配内存来缓存对应的块。之后会调用bread/bwrite进行从磁盘读数据块、写回数据块。要注意释放块缓存的brelse函数。
                                                                          // os/bio.c
                                                                          void brelse(struct buf *b) {
                                                                              b->refcnt--;
                                                                              if (b->refcnt == 0) {
                                                                                  b->next->prev = b->prev;
                                                                                  b->prev->next = b->next;
                                                                                  b->next = bcache.head.next;
                                                                                  b->prev = &bcache.head;
                                                                                  bcache.head.next->prev = b;
                                                                                  bcache.head.next = b;
                                                                              }
                                                                          }
                                                                          
                                                                          • 需要特别注意的是 brelse 不会真的如字面意思释放一个 buf。它的准确含义是暂时不操作该 buf 了并把它放置在bcache链表的首部,buf 的真正释放会被推迟到 buf 池满,无法分配的时候,就会把最近最久未使用的 buf 释放掉(释放 = 写回 + 清空)。这是为了尽可能保留内存缓存,因为读写磁盘真的太太太太慢了。
                                                                          • 此外,brelse 的数量必须和 bget 相同,因为 bget 会是的引用计数加一。如果没有相匹配的 brelse,就好比 new 了之后没有 delete。千万注意。

                                                                            2.4 inode的操作

                                                                            • 现在我们来看看nfs如何读取磁盘上的dinode到内存之中。我们通过file name对应的inode num去从磁盘读取对应的inode。为了解决共享问题(不同进程可以打开同一个磁盘文件),也有一个全局的 inode table,每当新打开一个文件的时候,会把一个空闲的 inode 绑定为对应 dinode 的缓存,这一步通过 iget 完成。
                                                                              // 找到 inum 号 dinode 绑定的 inode,如果不存在新绑定一个
                                                                              static struct inode *
                                                                              iget(uint dev, uint inum) {
                                                                                  struct inode *ip, *empty;
                                                                                  // 遍历查找 inode table
                                                                                  for (ip = &itable.inode[0]; ip ref > 0 && ip->dev == dev && ip->inum == inum) {
                                                                                          ip->ref++;
                                                                                          return ip;
                                                                                      }
                                                                                  }
                                                                                  // 如果没有对于的,找一个空闲 inode 完成绑定
                                                                                  empty = find_empty()
                                                                                  // GG,inode 表满了,果断自杀.lab7正常不会出现这个情况。
                                                                                  if (empty == 0)
                                                                                      panic("iget: no inodes");
                                                                                  // 注意这里仅仅是写了元数据,没有实际读取,实际读取推迟到后面
                                                                                  ip = empty;
                                                                                  ip->dev = dev;
                                                                                  ip->inum = inum;
                                                                                  ip->ref = 1;
                                                                                  ip->valid = 0;  // 没有实际读取,valid = 0
                                                                                  return ip;
                                                                              }
                                                                              
                                                                              • 当已经得到一个文件对应的 inode 后,可以通过 ivalid 函数确保其是有效的。
                                                                                // Reads the inode from disk if necessary.
                                                                                void ivalid(struct inode *ip) {
                                                                                    struct buf *bp;
                                                                                    struct dinode *dip;
                                                                                    if (ip->valid == 0) {
                                                                                        // bread 可以完成一个块的读取,这个在将 buf 的时候说过了
                                                                                        // IBLOCK 可以计算 inum 在几个 block
                                                                                        bp = bread(ip->dev, IBLOCK(ip->inum, sb));
                                                                                        // 得到 dinode 内容
                                                                                        dip = (struct dinode *) bp->data + ip->inum % IPB;
                                                                                        // 完成实际读取
                                                                                        ip->type = dip->type;
                                                                                        ip->size = dip->size;
                                                                                        memmove(ip->addrs, dip->addrs, sizeof(ip->addrs));
                                                                                        // buf 暂时没用了
                                                                                        brelse(bp);
                                                                                        // 现在有效了
                                                                                        ip->valid = 1;
                                                                                    }
                                                                                }
                                                                                
                                                                                • 在 inode 有效之后,可以通过 writei, readi 完成读写。这又是bwrite和bread的上级接口了。和其他OS支持的文件系统一样,我们首先计算出文件的偏移量,并通过bmap得到对应的block num。之后调用bwrite/bread来进行文件的读写操作。
                                                                                  // 从 ip 对应文件读取 [off, off+n) 这一段数据到 dst
                                                                                  int readi(struct inode *ip, char* dst, uint off, uint n) {
                                                                                      uint tot, m;
                                                                                      // 还记得 buf 吗?
                                                                                      struct buf *bp;
                                                                                      for (tot = 0; tot dev, bmap(ip, off / BSIZE));
                                                                                          // 一次最多读一个块,实际读取长度为 m
                                                                                          m = MIN(n - tot, BSIZE - off % BSIZE);
                                                                                          memmove(dst, (char*)bp->data + (off % BSIZE), m);
                                                                                          brelse(bp);
                                                                                      }
                                                                                      return tot;
                                                                                  }
                                                                                  // 同 readi
                                                                                  int writei(struct inode *ip, char* src, uint off, uint n) {
                                                                                      uint tot, m;
                                                                                      struct buf *bp;
                                                                                      for (tot = 0; tot dev, bmap(ip, off / BSIZE));
                                                                                          m = MIN(n - tot, BSIZE - off % BSIZE);
                                                                                          memmove(src, (char*)bp->data + (off % BSIZE), m);
                                                                                          bwrite(bp);
                                                                                          brelse(bp);
                                                                                      }
                                                                                      // 文件长度变长,需要更新 inode 里的 size 字段
                                                                                      if (off > ip->size)
                                                                                          ip->size = off;
                                                                                      // 有可能 inode 信息被更新了,写回
                                                                                      iupdate(ip);
                                                                                      return tot;
                                                                                  }
                                                                                  
                                                                                  • 其中bmap函数是连接inode和block的重要函数。但由于我们支持了间接索引,同时还设计到文件大小的改变,所以也拉出来看看:
                                                                                    // bn = off / BSIZE
                                                                                    uint bmap(struct inode *ip, uint bn) {
                                                                                        uint addr, *a;
                                                                                        struct buf *bp;
                                                                                        // 如果 bn addr[bn]
                                                                                        if (bn addrs[bn]) == 0)
                                                                                                ip->addrs[bn] = addr = balloc(ip->dev);
                                                                                            return addr;
                                                                                        }
                                                                                        bn -= NDIRECT;
                                                                                        // 间接索引块,那么对应的数据块就是一个大 addr 数组。
                                                                                        if (bn addrs[NDIRECT]) == 0)
                                                                                                ip->addrs[NDIRECT] = addr = balloc(ip->dev);
                                                                                            bp = bread(ip->dev, addr);
                                                                                            a = (uint *) bp->data;
                                                                                            if ((addr = a[bn]) == 0) {
                                                                                                a[bn] = addr = balloc(ip->dev);
                                                                                                bwrite(bp);
                                                                                            }
                                                                                            brelse(bp);
                                                                                            return addr;
                                                                                        }
                                                                                        panic("bmap: out of range");
                                                                                        return 0;
                                                                                    }
                                                                                    
                                                                                    • balloc(位于nfs/fs.c)会分配一个新的buf缓存。而iupdate函数则是把修改之后的inode重新写回到磁盘上。不然掉电了就凉了。
                                                                                      // Copy a modified in-memory inode to disk.
                                                                                      // Must be called after every change to an ip->xxx field
                                                                                      // that lives on disk.
                                                                                      void iupdate(struct inode *ip) {
                                                                                          struct buf *bp;
                                                                                          struct dinode *dip;
                                                                                          bp = bread(ip->dev, IBLOCK(ip->inum, sb));
                                                                                          dip = (struct dinode *) bp->data + ip->inum % IPB;
                                                                                          dip->type = ip->type;
                                                                                          dip->size = ip->size;
                                                                                          memmove(dip->addrs, ip->addrs, sizeof(ip->addrs));
                                                                                          bwrite(bp);
                                                                                          brelse(bp);
                                                                                      }
                                                                                      

                                                                                      2.5 文件在进程中的结构

                                                                                      • inode是由操作系统统一控制的dinode在内存中的映射,但是每个进程在具体使用文件的时候,除了需要考虑使用的是哪个inode对应的文件外,还需要根据对文件的使用情况来记录其它特性,因此,在进程中我们使用file结构体来标识一个被进程使用的文件:
                                                                                         // Defines a file in memory that provides information about the current use of the file and the corresponding inode location
                                                                                        struct file {
                                                                                        enum { FD_NONE = 0,FD_INODE, FD_STDIO } type;
                                                                                        int ref; // reference count
                                                                                        char readable;
                                                                                        char writable;
                                                                                        struct inode *ip; // FD_INODE
                                                                                        uint off;
                                                                                        };
                                                                                        struct file filepool[FILEPOOLSIZE];
                                                                                        
                                                                                        • 我们采用预分配的方式来对file进行分配,每一个需要使用的file都要与filepool中的某一个file完成绑定。file结构中,ref记录了其引用次数,type表示了文件的类型,在本章中我们主要使用FD_NONE和FD_INODE属性,其中FD_INODE表示file已经绑定了一个文件(可能是目录或普通文件),FD_NONE表示该file还没完成绑定,FD_STDIO用来做标准输入输出,这里不做讨论;readbale和writeble规定了进程对文件的读写权限;ip标识了file所对应的磁盘中的inode编号,off即文件指针,用作记录文件读写时的偏移量。
                                                                                        • 分配文件时,我们从filepool中寻找还没有被分配的file进行分配:
                                                                                          // os/file.c
                                                                                          struct file* filealloc() {
                                                                                              for(int i = 0; i  
                                                                                          
                                                                                          • 进程关闭文件时,也要去filepool中放回:(注意需要根据ref来判断是否需要回收该file)
                                                                                            void fileclose(struct file *f)
                                                                                            {
                                                                                                 if (f->ref ref > 0) {
                                                                                                         return;
                                                                                                 }
                                                                                                 switch (f->type) {
                                                                                                 case FD_STDIO:
                                                                                                         // Do nothing
                                                                                                         break;
                                                                                                 case FD_INODE:
                                                                                                         iput(f->ip);
                                                                                                         break;
                                                                                                 default:
                                                                                                         panic("unknown file type %d\n", f->type);
                                                                                                 }
                                                                                                 f->off = 0;
                                                                                                 f->readable = 0;
                                                                                                 f->writable = 0;
                                                                                                 f->ref = 0;
                                                                                                 f->type = FD_NONE;
                                                                                            }
                                                                                            
                                                                                            • 注意文件对于进程而言也是其需要记录的一种资源,因此我们在进程对应的PCB结构体之中也需要记录进程打开的文件信息。我们给PCB增加文件指针数组。
                                                                                              // proc.h
                                                                                              // Per-process state
                                                                                              struct proc {
                                                                                                  // ...
                                                                                              +   struct file* files[16];
                                                                                              };
                                                                                              // os/proc.c
                                                                                              int fdalloc(struct file* f) {
                                                                                                  struct proc* p = curr_proc();
                                                                                                  // fd = 0,1,2 is reserved for stdio/stdout/stderr
                                                                                                  for(int i = 3; i files[i] == 0) {
                                                                                                          p->files[i] = f;
                                                                                                          return i;
                                                                                                      }
                                                                                                  }
                                                                                                  return -1;
                                                                                              }
                                                                                              
                                                                                              • 一个进程能打开的文件是有限的(我们设置为16)。一个进程如果要打开某一个文件,其文件指针数组必须有空位。如果有,就把下标做为文件的fd,并把指定文件指针存入数组之中。

                                                                                                2.6 获取文件对应的inode

                                                                                                • 现在我们回到文件-inode的关系上。我们怎么获取文件对应的inode呢?上文中提到了我们是去查file name对应inode的表来实现这个过程的。这个功能由目录来提供。我们看一下代码是如何实现这个过程的。
                                                                                                • 首先用户程序要打开指定文件名文件,发起系统调用sys_openat:
                                                                                                • 打开文件的方式根据flags有很多种。我们先来看最简单的,就是打开已经存在的文件的方法。fileopen在处理这类打开时调用了namei这个函数。
                                                                                                  // namei = 获得根目录,然后在其中遍历查找 path
                                                                                                  struct inode *namei(char *path) {
                                                                                                  struct inode *dp = root_dir();
                                                                                                      return dirlookup(dp, path, 0);
                                                                                                  }
                                                                                                  // root_dir 位置固定
                                                                                                  struct inode *root_dir() {
                                                                                                      struct inode* r = iget(ROOTDEV, ROOTINO);
                                                                                                      ivalid(r);
                                                                                                      return r;
                                                                                                  }
                                                                                                  // 便利根目录所有的 dirent,找到 name 一样的 inode。
                                                                                                  struct inode *
                                                                                                  dirlookup(struct inode *dp, char *name, uint *poff) {
                                                                                                      uint off, inum;
                                                                                                      struct dirent de;
                                                                                                      // 每次迭代处理一个 block,注意根目录可能有多个 data block
                                                                                                      for (off = 0; off size; off += sizeof(de)) {
                                                                                                          readi(dp, 0, (uint64) &de, off, sizeof(de));
                                                                                                          if (strncmp(name, de.name, DIRSIZ) == 0) {
                                                                                                              if (poff)
                                                                                                                  *poff = off;
                                                                                                              inum = de.inum;
                                                                                                              // 找到之后,绑定一个内存 inode 然后返回
                                                                                                              return iget(dp->dev, inum);
                                                                                                          }
                                                                                                      }
                                                                                                      return 0;
                                                                                                  }
                                                                                                  
                                                                                                  • 由于我们是单目录结构。因此首先我们调用root_dir获取根目录对应的inode。之后就遍历这个inode索引的数据块中存储的文件信息到dirent结构体之中,比较名称和给定的文件名是否一致。dirlookup的逻辑对于我们本章的练习十分重要。
                                                                                                  • fileopen 还可能会导致文件 truncate,也就是截断,具体做法是舍弃全部现有内容,释放inode所有 data block 并添加到 free bitmap 里。这也是目前 nfs 中唯一的文件变短方式。
                                                                                                  • 比较复杂的就是使用fileopen以创建的方式打开一个文件。fileopen函数调用了
                                                                                                    create这个函数。
                                                                                                    static struct inode *
                                                                                                    create(char *path, short type) {
                                                                                                        struct inode *ip, *dp;
                                                                                                        if(ip = namei(path) != 0) {
                                                                                                            // 已经存在,直接返回
                                                                                                            return ip;
                                                                                                        }
                                                                                                        // 创建一个文件,首先分配一个空闲的 disk inode, 绑定内存 inode 之后返回
                                                                                                        ip = ialloc(dp->dev, type);
                                                                                                        // 注意 ialloc 不会执行实际读取,必须有 ivalid
                                                                                                        ivalid(ip);
                                                                                                        // 在根目录创建一个 dirent 指向刚才创建的 inode
                                                                                                        dirlink(dp, path, ip->inum);
                                                                                                        // dp 不用了,iput 就是释放内存 inode,和 iget 正好相反。
                                                                                                        iput(dp);
                                                                                                        return ip;
                                                                                                    }
                                                                                                    // nfs/fs.c
                                                                                                    uint ialloc(ushort type) {
                                                                                                        uint inum = freeinode++;
                                                                                                        struct dinode din;
                                                                                                        bzero(&din, sizeof(din));
                                                                                                        din.type = xshort(type);
                                                                                                        din.size = xint(0);
                                                                                                        winode(inum, &din);
                                                                                                        return inum;
                                                                                                    }
                                                                                                    // os/fs.c
                                                                                                    // Write a new directory entry (name, inum) into the directory dp.
                                                                                                    int
                                                                                                    dirlink(struct inode *dp, char *name, uint inum)
                                                                                                    {
                                                                                                        int off;
                                                                                                        struct dirent de;
                                                                                                        struct inode *ip;
                                                                                                        // Check that name is not present.
                                                                                                        if((ip = dirlookup(dp, name, 0)) != 0){
                                                                                                            iput(ip);
                                                                                                            return -1;
                                                                                                        }
                                                                                                        // Look for an empty dirent.
                                                                                                        for(off = 0; off size; off += sizeof(de)){
                                                                                                            if(readi(dp, 0, (uint64)&de, off, sizeof(de)) != sizeof(de))
                                                                                                            panic("dirlink read");
                                                                                                            if(de.inum == 0)
                                                                                                            break;
                                                                                                        }
                                                                                                        strncpy(de.name, name, DIRSIZ);
                                                                                                        de.inum = inum;
                                                                                                        if(writei(dp, 0, (uint64)&de, off, sizeof(de)) != sizeof(de))
                                                                                                            panic("dirlink");
                                                                                                        return 0;
                                                                                                    }
                                                                                                    
                                                                                                    • ialloc 干的事情:遍历 inode blocks 找到一个空闲的inode,初始化并返回。dirlink对于本章的练习也十分重要。和dirlookup不同,我们没有现成的dirent存储在磁盘上,而是要在磁盘上创建一个新的dirent。他遍历根目录数据块,找到一个空的 dirent,设置 dirent = {inum, filename} 然后返回,注意这一步可能找不到空位,这时需要找一个新的数据块,并扩大 root_dir size,这是由 bmap 自动完成的。需要注意本章创建硬链接时对应inode num的处理。

                                                                                                      2.7 文件关闭

                                                                                                      • 文件读写结束后需要fclose释放掉其inode,同时释放OS中对应的file结构体和fd。其实 inode 文件的关闭只需要调用 iput 就好了,iput 的实现简单到让人感觉迷惑,就是 inode 引用计数减一。诶?为什么没有计数为 0 就写回然后释放 inode 的操作?和 buf 的释放同理,这里会等 inode 池满了之后自行被替换出去,重新读磁盘实在太太太太慢了。对了,千万记得 iput 和 iget 数量相同,一定要一一对应,否则你懂的。
                                                                                                        void
                                                                                                        fileclose(struct file *f)
                                                                                                        {
                                                                                                            if(--f->ref > 0) {
                                                                                                                return;
                                                                                                            }
                                                                                                            // 暂时不支持标准输入输出文件的关闭
                                                                                                           if(f->type == FD_INODE) {
                                                                                                                iput(f->ip);
                                                                                                            }
                                                                                                            f->off = 0;
                                                                                                            f->readable = 0;
                                                                                                            f->writable = 0;
                                                                                                            f->ref = 0;
                                                                                                            f->type = FD_NONE;
                                                                                                        }
                                                                                                        void iput(struct inode *ip) {
                                                                                                            ip->ref--;
                                                                                                        }
                                                                                                        
                                                                                                        【Ucore操作系统】6. 文件系统与I/O重定向,【Ucore操作系统】6. 文件系统与I/O重定向,词库加载错误:未能找到文件“C:\Users\Administrator\Desktop\火车头9.8破解版\Configuration\Dict_Stopwords.txt”。,使用,我们,访问,第2张
                                                                                                        (图片来源网络,侵删)
                                                                                                        【Ucore操作系统】6. 文件系统与I/O重定向,【Ucore操作系统】6. 文件系统与I/O重定向,词库加载错误:未能找到文件“C:\Users\Administrator\Desktop\火车头9.8破解版\Configuration\Dict_Stopwords.txt”。,使用,我们,访问,第3张
                                                                                                        (图片来源网络,侵删)

免责声明
本网站所收集的部分公开资料来源于AI生成和互联网,转载的目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。
文章版权声明:除非注明,否则均为主机测评原创文章,转载或复制请以超链接形式并注明出处。

发表评论

快捷回复: 表情:
评论列表 (暂无评论,1069人围观)

还没有评论,来说两句吧...

目录[+]