File OS

在这里插入图片描述

文章目录

【实验目标】

【以往实验目标】

  • Trilobita OSLibOS):让APP与HW隔离,简化应用访问硬件的难度和复杂性
  • Dunkleosteus OSBatchOS):让APP与OS隔离,加强系统安全,提高执行效率
  • multiprog & time-sharing OS:让APP有效共享CPU,提高系统总体性能和效率
  • Ankylosauridae OSAddress Space OS):APP不用考虑其运行时的起始执行地址,隔离APP访问的内存地址空间
  • Process OS:增强进程管理和资源管理

【本实验目标】

  • 文件形式保存持久数据,并能进行文件数据读写
  • 进程成为文件资源的使用者
  • 能够在应用层面发出如下系统调用请求:open/read/write/close

在这里插入图片描述

【文件系统访问接口】

在这里插入图片描述

【文件系统的数据结构】

在这里插入图片描述

【实验步骤】

第一步:是能够写出与文件访问相关的应用

第二步:就是要实现 easyfs 文件系统

第三步:把easyfs文件系统加入内核中

  • 编译:内核独立编译,单独的内核镜像
  • 编译:应用程序编译后,组织形成文件系统镜像
  • 构造:进程的管理与初始化,建立基于页表机制的虚存空间
  • 构造:构建文件系统
  • 运行:特权级切换,进程与OS相互切换
  • 运行:切换地址空间,跨地址空间访问数据
  • 运行:从文件系统加载应用,形成进程
  • 运行:数据访问:内存—磁盘,基于文件的读写

【代码架构】

├── bootloader
│   ├── rustsbi-k210.bin
│   └── rustsbi-qemu.bin
├── Dockerfile
├── easy-fs(新增:从内核中独立出来的一个简单的文件系统 EasyFileSystem 的实现)
│   ├── Cargo.toml
│   └── src
│       ├── bitmap.rs(位图抽象)
│       ├── block_cache.rs(块缓存层,将块设备中的部分块缓存在内存中)
│       ├── block_dev.rs(声明块设备抽象接口 BlockDevice,需要库的使用者提供其实现)
│       ├── efs.rs(实现整个 EasyFileSystem 的磁盘布局)
│       ├── layout.rs(一些保存在磁盘上的数据结构的内存布局)
│       ├── lib.rs
│       └── vfs.rs(提供虚拟文件系统的核心抽象,即索引节点 Inode)
├── easy-fs-fuse(新增:将当前 OS 上的应用可执行文件按照 easy-fs 的格式进行打包)
│   ├── Cargo.toml
│   └── src
│       └── main.rs
├── LICENSE
├── Makefile
├── os
│   ├── build.rs
│   ├── Cargo.toml(修改:新增 QemuK210 两个平台的块设备驱动依赖 crate)
│   ├── Makefile(修改:新增文件系统的构建流程)
│   └── src
│       ├── config.rs(修改:新增访问块设备所需的一些 MMIO 配置)
│       ├── console.rs
│       ├── drivers(修改:新增 QemuK210 两个平台的块设备驱动)
│       │   ├── block
│       │   │   ├── mod.rs(将不同平台上的块设备全局实例化为 BLOCK_DEVICE 提供给其他模块使用)
│       │   │   ├── sdcard.rs(K210 平台上的 microSD 块设备, Qemu不会用)
│       │   │   └── virtio_blk.rs(Qemu 平台的 virtio-blk 块设备)
│       │   └── mod.rs
│       ├── entry.asm
│       ├── fs(修改:在文件系统中新增常规文件的支持)
│       │   ├── inode.rs(新增:将 easy-fs 提供的 Inode 抽象封装为内核看到的 OSInode
│       │   │            并实现 fs 子模块的 File Trait)
│       │   ├── mod.rs
│       │   ├── pipe.rs
│       │   └── stdio.rs
│       ├── lang_items.rs
│       ├── link_app.S
│       ├── linker-k210.ld
│       ├── linker-qemu.ld
│       ├── loader.rs(移除:应用加载器 loader 子模块,本章开始从文件系统中加载应用)
│       ├── main.rs
│       ├── mm
│       │   ├── address.rs
│       │   ├── frame_allocator.rs
│       │   ├── heap_allocator.rs
│       │   ├── memory_set.rs(修改:在创建地址空间的时候插入 MMIO 虚拟页面)
│       │   ├── mod.rs
│       │   └── page_table.rs(新增:应用地址空间的缓冲区抽象 UserBuffer 及其迭代器实现)
│       ├── sbi.rs
│       ├── syscall
│       │   ├── fs.rs(修改:新增 sys_open)
│       │   ├── mod.rs
│       │   └── process.rs(修改:sys_exec 改为从文件系统中加载 ELF,并支持命令行参数)
│       ├── task
│       │   ├── context.rs
│       │   ├── manager.rs
│       │   ├── mod.rs(修改初始进程 INITPROC 的初始化)
│       │   ├── pid.rs
│       │   ├── processor.rs
│       │   ├── switch.rs
│       │   ├── switch.S
│       │   └── task.rs
│       ├── timer.rs
│       └── trap
│           ├── context.rs
│           ├── mod.rs
│           └── trap.S
├── README.md
├── rust-toolchain
├── tools
│   ├── kflash.py
│   ├── LICENSE
│   ├── package.json
│   ├── README.rst
│   └── setup.py
└── user
   ├── Cargo.lock
   ├── Cargo.toml
   ├── Makefile
   └── src
      ├── bin
      │   ├── cat_filea.rs(新增:显示文件filea的内容)
      │   ├── cmdline_args.rs(新增)
      │   ├── exit.rs
      │   ├── fantastic_text.rs
      │   ├── filetest_simple.rs(新增:创建文件filea并读取它的内容 )
      │   ├── forktest2.rs
      │   ├── forktest.rs
      │   ├── forktest_simple.rs
      │   ├── forktree.rs
      │   ├── hello_world.rs
      │   ├── initproc.rs
      │   ├── matrix.rs
      │   ├── pipe_large_test.rs
      │   ├── pipetest.rs
      │   ├── run_pipe_test.rs
      │   ├── sleep.rs
      │   ├── sleep_simple.rs
      │   ├── stack_overflow.rs
      │   ├── user_shell.rs
      │   ├── usertests.rs
      │   └── yield.rs
      ├── console.rs
      ├── lang_items.rs
      ├── lib.rs(修改:支持命令行参数解析)
      ├── linker.ld
      └── syscall.rs(修改:新增 sys_open)

【文件系统接口(应用层)】

在用户态我们只需要遵从相关系统调用的接口约定,在用户库里完成对应的封装即可。

首先以Linux 上的常规文件和目录为例,站在访问文件的应用的角度,介绍文件中值得注意的地方及文件使用方法。由于 Linux 上的文件系统模型还是比较复杂,在内核实现中对它进行了很大程度的简化,最后介绍内核上应用的开发者应该如何使用我们简化后的文件系统。

【文件和目录】

【常规文件】

操作系统的用户看来,常规文件是保存在持久存储设备上的一个字节序列,每个常规文件都有一个 文件名 (Filename) ,用户需要通过它来区分不同的常规文件

在 Linux 系统上, stat 工具可以获取文件的一些信息

DESKTOP-OBNAKTV# stat main.rs
  File: main.rs
  Size: 1657            Blocks: 8          IO Block: 4096   regular file
Device: 810h/2064d      Inode: 548658      Links: 1
Access: (0644/-rw-r--r--)  Uid: (    0/    root)   Gid: (    0/    root)
Access: 2022-11-24 17:53:01.473233338 +0800
Modify: 2022-11-24 17:52:46.753233521 +0800
Change: 2022-11-24 17:52:46.753233521 +0800
 Birth: 2022-11-24 17:52:46.753233521 +0800

stat 工具展示了 main.rs 的如下信息:

  • File :表明它的文件名为 main.rs 。
  • Size :表明它的字节大小为 940 字节。
  • Blocks :表明它占据 8 个 (Block) 来存储。在文件系统中,文件的数据以块为单位进行存储
  • IO Block :可以看出,在 Linux操作系统中的Ext4文件系统的每个块的大小为 4096 字节
  • regular file :表明这个文件是一个常规文件。事实上,其他类型的文件也可以通过文件名来进行访问。
  • 当文件是一个特殊文件(如块设备文件或者字符设备文件)的时候,Device 将指出该特殊文件的 major/minor ID 。对于一个常规文件,我们无需关心它。
  • Inode :表示文件的底层编号。在文件系统的底层实现中,并不是直接通过文件名来索引文件,而是首先需要将文件名转化为文件的底层编号再根据这个编号去索引文件。目前我们无需关心这一信息。
  • Links :给出文件的硬链接数。同一个文件系统中如果两个文件(目录也是文件)具有相同的inode号码,那么就称它们是“硬链接”关系。这样links的值其实是一个文件的不同文件名的数量。
  • Uid :给出该文件的所属的用户 ID
  • Gid :给出该文件所属的用户组 ID
  • Access 的其中一种表示是一个长度为 10 的字符串(这里是 -rw-r--r-- ),其中第 1 位给出该文件的类型,这个文件是一个常规文件,因此这第 1 位为 -后面的 9 位可以分为三组,分别表示该文件的所有者/在该文件所属的用户组内的其他用户以及剩下的所有用户能够读取/写入/将该文件作为一个可执行文件来执行。
  • Access/Modify :分别给出该文件的最近一次访问/最近一次修改时间。

用户常常通过文件的 拓展名 (Filename extension) 来推断该文件的用途,如 main.rs 的拓展名是 .rs ,我们由此知道它是一个 Rust 源代码文件。但从内核的角度来看,它会将所有文件无差别的看成一个字节序列,文件内容的结构和含义则是交给对应的应用进行解析。

【目录】

结合用户和用户组的概念,目录的存在也使得文件访问权限控制更加容易,只需要对于目录进行设置就可以间接设置用户/用户组对该目录下所有文件的访问权限,这使得操作系统能够更加安全的支持多用户情况下对不同文件的访问。

DESKTOP-OBNAKTV# stat os6
  File: os6
  Size: 4096            Blocks: 8          IO Block: 4096   directory
Device: 810h/2064d      Inode: 548636      Links: 4
Access: (0755/drwxr-xr-x)  Uid: (    0/    root)   Gid: (    0/    root)
Access: 2022-11-24 17:53:00.743233347 +0800
Modify: 2022-11-24 17:52:46.753233521 +0800
Change: 2022-11-24 17:52:46.753233521 +0800
 Birth: 2022-11-24 17:52:46.753233521 +0800
  • directory :表明 os 是一个目录,从 Access 字符串的首位 d 也可以看出这一点。
  • 对于目录而言, Accessrwx 含义有所不同:
    • r 表示是否允许获取该目录下有哪些文件和子目录
    • w 表示是否允许在该目录下创建/删除文件和子目录
    • x 表示是否允许“通过”该目录。

目录也可以看作一种文件,它也有属于自己的底层编号,它的内容中保存着若干 目录项 (Dirent, Directory Entry)。

目录树 (Directory Tree) :树中的每个节点都是一个文件或目录,一个目录下面的所有的文件和子目录都是它的孩子。

根目录:目录树的根节点

./ 表示当前目录,而 ../ 表示当前目录的父目录,这在通过相对路径进行索引的时候非常实用。

【文件系统】

文件系统负责将逻辑上的目录树结构(包括其中每个文件或目录的数据和其他信息)映射到持久存储设备上,决定设备上的每个扇区应存储哪些内容。反过来,文件系统也可以从持久存储设备还原出逻辑上的目录树结构。

文件系统有很多种不同的实现,每一种都能将同一个逻辑上目录树结构转化为一个不同的持久存储设备上的扇区布局。最著名的文件系统有 Windows 上的 FAT/NTFS 和 Linux 上的 Ext3/Ext4/Btrfs 等。

在一个计算机系统中,可以同时包含多个持久存储设备,它们上面的数据可能是以不同文件系统格式存储的。为了能够对它们进行统一管理,在内核中有一层 虚拟文件系统 (VFS, Virtual File System) ,它规定了逻辑上目录树结构的通用格式及相关操作的抽象接口,只要不同的底层文件系统均实现虚拟文件系统要求的那些抽象接口,再加上 挂载 (Mount) 等方式,这些持久存储设备上的不同文件系统便可以用一个统一的逻辑目录树结构一并进行管理。

虚拟文件系统在上层做了抽象,当进行移植的时候可以保证上层逻辑方面代码的不变性,只需要修改底层文件系统的具体实现就行,将其实现对应到接口上便完成了移植。也方便了后续存储的扩容。

【简化的文件与目录抽象】

  • 扁平化:仅存在根目录 / 一个目录,剩下所有的文件都放在根目录内。在索引一个文件的时候,我们直接使用文件的文件名而不是它含有 / 的绝对路径。
  • 权限控制:我们不设置用户和用户组概念,全程只有单用户。同时根目录和其他文件也都没有权限控制位,即完全不限制文件的访问方式,不会区分文件是否可执行。
  • 不记录文件访问/修改的任何时间戳
  • 不支持软硬链接
  • 只实现了简单的系统调用。

【打开、关闭与读写文件的系统调用】

【文件打开与关闭 -> user/src/syscall.rs -> user/src/lib.rs】

在读写一个常规文件之前,应用首先需要通过内核提供的 sys_openat 系统调用让该文件在进程的文件描述符表中占一项,并得到操作系统的返回值文件描述符,即文件关联的表项在文件描述表中的索引值:

在打开文件,对文件完成了读写操作后,还需要关闭文件,这样才让进程释放被这个文件所占用的内核资源close 的调用参数是文件描述符,但文件被关闭后,文件在内核中的资源会被释放,文件描述符会被回收。这样,进程就不能继续使用该文件描述符进行文件读写了。

user/src/syscall.rs应用层的底层系统调用部分,通过其与系统进行连接。

//! user/src/syscall.rs
use crate::TaskInfo;

use super::{Stat, TimeVal};

pub const SYSCALL_OPENAT: usize = 56;
pub const SYSCALL_CLOSE: usize = 57;

pub fn syscall(id: usize, args: [usize; 3]) -> isize {
    let mut ret: isize;
    unsafe {
        core::arch::asm!(
            "ecall",
            inlateout("x10") args[0] => ret,
            in("x11") args[1],
            in("x12") args[2],
            in("x17") id
        );
    }
    ret
}

pub fn syscall6(id: usize, args: [usize; 6]) -> isize {
    let mut ret: isize;
    unsafe {
        core::arch::asm!("ecall",
            inlateout("x10") args[0] => ret,
            in("x11") args[1],
            in("x12") args[2],
            in("x13") args[3],
            in("x14") args[4],
            in("x15") args[5],
            in("x17") id
        );
    }
    ret
}

/// 功能:打开一个常规文件,并返回可以访问它的文件描述符。
/// 参数:path 描述要打开的文件的文件名(简单起见,文件系统不需要支持目录,所有的文件都放在根目录 / 下),
/// flags 描述打开文件的标志,具体含义下面给出。
/// dirfd 和 mode 仅用于保证兼容性,忽略
/// 返回值:如果出现了错误则返回 -1,否则返回打开常规文件的文件描述符。可能的错误原因是:文件不存在。
/// syscall ID:56
pub fn sys_openat(dirfd: usize, path: &str, flags: u32, mode: u32) -> isize {
    syscall6(
        SYSCALL_OPENAT,
        [
            dirfd,
            path.as_ptr() as usize,
            flags as usize,
            mode as usize,
            0,
            0,
        ],
    )
}

/// 功能:当前进程关闭一个文件。
/// 参数:fd 表示要关闭的文件的文件描述符。
/// 返回值:如果成功关闭则返回 0 ,否则返回 -1 。可能的出错原因:传入的文件描述符并不对应一个打开的文件。
pub fn sys_close(fd: usize) -> isize {
    syscall(SYSCALL_CLOSE, [fd, 0, 0])
}
...
  • 第 45 - 57 行:在应用层实现了sys_openat系统调用,可以打开一个常规文件,并返回可以访问它的文件描述符。目前我们的内核支持以下几种标志(多种不同标志可能共存):
    • 如果 flags 为 0,则表示以只读模式 RDONLY 打开
    • 如果 flags 第 0 位被设置(0x001),表示以只写模式 WRONLY 打开
    • 如果 flags 第 1 位被设置(0x002),表示既可读又可写 RDWR
    • 如果 flags 第 9 位被设置(0x200),表示允许创建文件 CREATE ,在找不到该文件的时候应创建文件;如果该文件已经存在则应该将该文件的大小归零;
    • 如果 flags 第 10 位被设置(0x400),则在打开文件的时候应该清空文件的内容并将该文件的大小归零,也即 TRUNC
  • 第 62 - 64 行:在应用层实现了sys_close系统调用,可以关闭一个常规文件,释放被这个文件所占用的内核资源。

user/src/lib.rs文件在syscall.rs的基础上对其进行了封装,方便上层应用的调用。

//! user/src/lib.rs
#![no_std]
#![feature(linkage)]
#![feature(panic_info_message)]
#![feature(alloc_error_handler)]

#[macro_use]
pub mod console;
mod lang_items;
mod syscall;

bitflags! {
    pub struct OpenFlags: u32 {
        const RDONLY = 0;
        const WRONLY = 1 << 0;
        const RDWR = 1 << 1;
        const CREATE = 1 << 9;
        const TRUNC = 1 << 10;
    }
}

pub fn open(path: &str, flags: OpenFlags) -> isize {
    sys_openat(AT_FDCWD as usize, path, flags.bits, OpenFlags::RDWR.bits)
}

pub fn close(fd: usize) -> isize {
    if fd == STDOUT {
        console::flush();
    }
    sys_close(fd)
}
...
  • 第 12 - 20 行:借助 bitflags! 宏我们将一个 u32flags 包装为一个 OpenFlags 结构体更易使用,它的 bits 字段可以将自身转回 u32 ,它也会被传给 sys_openat。通过bitflags!宏方便对u32flags各个位进行修改,实现权限的设置。
  • 第 22 - 24 行:在用户库中,将sys_openat封装为open函数。
  • 第 26 - 31 行:在用户库中,将sys_close封装为close函数。

【顺序读写文件 -> user/src/bin/ch6b_filetest_simple.rs】

在打开一个文件之后,我们就可以用之前的 sys_read/sys_write 两个系统调用来对它进行读写了。在文件系统的底层实现中都是对文件进行随机读写的,而本文只考虑顺序读写。

用测试用例 ch6b_filetest_simple.rs 来介绍文件系统接口的使用方法:

//! user/src/bin/ch6b_filetest_simple.rs
#![no_std]
#![no_main]

#[macro_use]
extern crate user_lib;

use user_lib::{
    open,
    close,
    read,
    write,
    OpenFlags,
};

#[no_mangle]
pub fn main() -> i32 {
    let test_str = "Hello, world!";
    let filea = "filea\0";
    let fd = open(filea, OpenFlags::CREATE | OpenFlags::WRONLY);
    assert!(fd > 0);
    let fd = fd as usize;
    write(fd, test_str.as_bytes());
    close(fd);

    let fd = open(filea, OpenFlags::RDONLY);
    assert!(fd > 0);
    let fd = fd as usize;
    let mut buffer = [0u8; 100];
    let read_len = read(fd, &mut buffer) as usize;
    close(fd);

    assert_eq!(
        test_str,
        core::str::from_utf8(&buffer[..read_len]).unwrap(),
    );
    println!("file_test passed!");
    0
}
  • 第 18 行:创建写入文件的字符串。
  • 第 19 行:创建新建的文件的名字。这里需要注意的是我们需要为字符串字面量手动加上 \0 作为结尾。
  • 第 20 行:我们以 (只写 + 创建) 的模式打开文件 filea ,并得到它的文件描述符。
  • 第 21 行:判断文件是否打开正确。
  • 第 23 行:向其中写入字符串 Hello, world!
  • 第 24 行:关闭文件。
  • 第 26 行:以只读的方式打开文件filea
  • 第 27 行:判断文件是否打开正确。
  • 第 29 行:设定接收buff
  • 第 30 行:从文件中读取数据。filea 的总大小不超过缓冲区的大小,因此通过单次 read 即可将内容全部读出来而更常见的情况是需要进行多次 read ,直到返回值为 0 才能确认文件已被读取完毕。
  • 第 31 行:关闭文件。
  • 第 33 - 36 行:我们确认从 filea 读取到的内容和之前写入的一致,则测试通过。

文件系统接口(应用层)小结:对于应用层来说,并不需要明白系统层到底做了点什么,它只需要明白系统层提供了哪些预留的接口。应用层可以通过这几个接口完成相应的应用需求即可。例如系统层提供了读写文件的功能,则应用层便可通过该接口实现向文件中写入读出数据从而保存数据的功能。

【简易文件系统 easy-fs(简易文件系统的实现)】

【松耦合模块化设计思路】

我们让内核的各个部分之间尽量松耦合,所以easy-fs 被从内核中分离出来,它的实现分成两个不同的 crate :

  • easy-fs简易文件系统的本体,它是一个库形式 crate,实现一种简单的文件系统磁盘布局;
  • easy-fs-fuse 是一个能在开发环境(如 Ubuntu)中运行的应用程序,它可以对 easy-fs 进行测试,或者将为我们内核开发的应用打包为一个 easy-fs 格式的文件系统镜像。

整个easy-fs文件系统的设计开发可以按照应用程序库的开发过程来完成。而且在开发完毕后,可直接放到内核中,形成有文件系统支持的新内核。

easy-fs与底层设备驱动之间通过抽象接口 BlockDevice 来连接,避免了与设备驱动的绑定。easy-fs通过Rust提供的alloc crate来隔离了操作系统内核的内存管理,避免了直接调用内存管理的内核函数。在底层驱动上,采用的是轮询的方式访问 virtio_blk 虚拟磁盘设备,从而避免了访问外设中断的相关内核函数。easy-fs在设计中避免了直接访问进程相关的数据和函数,从而隔离了操作系统内核的进程管理。

easy-fs crate 大致可以分成五个不同的层次

  1. 索引节点层管理索引节点(即文件控制块)数据结构,并实现文件创建/文件打开/文件读写等成员函数来向上支持文件操作相关的系统调用
  2. 磁盘块管理器层:合并了核心数据结构和磁盘布局所形成的磁盘文件系统数据结构,以及基于这些结构的创建/打开文件系统的相关处理和磁盘块的分配和回收处理
  3. 磁盘数据结构层:磁盘上的超级块、位图、索引节点、数据块、目录项等核心数据结构和相关处理
  4. 块缓存层:在内存中缓存磁盘块的数据,避免频繁读写磁盘
  5. 磁盘块设备接口层:定义了以块大小为单位对磁盘块设备进行读写的trait接口

在这里插入图片描述

【块设备接口层 -> /easy-fs/src/block_dev.rs】

定义**设备驱动需要实现的块读写接口 BlockDevice trait**的块设备接口层代码在 block_dev.rs 中。

//! easy-fs/src/block_dev.rs
use core::any::Any;

/// Trait for block devices
/// which reads and writes data in the unit of blocks
pub trait BlockDevice : Send + Sync + Any {
    fn read_block(&self, block_id: usize, buf: &mut [u8]);
    fn write_block(&self, block_id: usize, buf: &[u8]);
}
  • 第 6 - 9 行:声明了一个块设备的抽象接口 BlockDevice,它需要实现两个抽象方法。
    • 第 7 行:read_block 将编号为 block_id 的块从磁盘读入内存中的缓冲区 buf
    • 第 8 行:write_block 将内存中的缓冲区 buf 中的数据写入磁盘编号为 block_id 的块。

easy-fs 中并没有一个实现了 BlockDevice Trait 的具体类型。因为块设备仅支持以块为单位进行随机读写,所以需要由具体的块设备驱动来实现这两个方法,实际上这是需要由文件系统的使用者(比如操作系统内核或直接测试 easy-fs 文件系统的 easy-fs-fuse 应用程序)提供并接入到 easy-fs 库的。 **easy-fs 库的块缓存层会调用这两个方法,进行块缓存的管理。**这也体现了 easy-fs 的泛用性:它可以访问实现了 BlockDevice Trait 的块设备驱动程序。

块与扇区:块和扇区是两个不同的概念。 扇区 (Sector) 是块设备随机读写的数据单位,通常每个扇区为 512 字节。而块是文件系统存储文件时的数据单位,每个块的大小等同于一个或多个扇区。

个人理解:操作系统原本有磁盘,现在想要更好的管理磁盘,所以提出了文件系统的概念。文件系统中,文件的数据以块为单位进行存储。所以要管理的设备需要实现具体的对块的读写操作这两种方法,后续才能交给文件系统进行管理。

【块缓存层】

由于操作系统频繁读写速度缓慢的磁盘块会极大降低系统性能,因此常见的手段是先通过 read_block 将一个块上的数据从磁盘读到内存中的一个缓冲区中,这个缓冲区中的内容是可以直接读写的,那么后续对这个数据块的大部分访问就可以在内存中完成了如果缓冲区中的内容被修改了,那么后续还需要通过 write_block 将缓冲区中的内容写回到磁盘块中

当磁盘上的数据结构比较复杂的时候,很难通过应用来合理地规划块读取/写入的时机。这不仅可能涉及到复杂的参数传递,稍有不慎还有可能引入同步性问题:即一个块缓冲区修改后的内容在后续的同一个块读操作中不可见,这很致命但又难以调试。

我们的做法是将缓冲区统一管理起来。当我们要读写一个块的时候,首先就是去全局管理器中查看这个块是否已被缓存到内存缓冲区中。如果是这样,则在一段连续时间内对于一个块进行的所有操作均是在同一个固定的缓冲区中进行的,这解决了同步性问题。此外,通过 read/write_block 进行块实际读写的时机完全交给块缓存层的全局管理器处理,上层子系统无需操心。全局管理器会尽可能将更多的块操作合并起来,并在必要的时机发起真正的块实际读写。

【块缓存 -> easy-fs/src/block_cache.rs】

理解:块缓存使得我们减少读写磁盘的时间开销,将原来读磁盘修改数据后写磁盘,变为读磁盘到块缓存后通过对块缓存进行操作最终所有操作完了后写入到磁盘,达到同样的效果。

easy-fs/src/block_cache.rs文件主要是关于块缓存层方面的代码实现和数据结构。

//! easy-fs/src/block_cache.rs
use super::{
    BLOCK_SZ,
    BlockDevice,
};
use alloc::collections::VecDeque;
use alloc::sync::Arc;
use lazy_static::*;
use spin::Mutex;

/// Cached block inside memory
pub struct BlockCache {
    /// cached block data
    cache: [u8; BLOCK_SZ],
    /// underlying block id
    block_id: usize,
    /// underlying block device
    block_device: Arc<dyn BlockDevice>,
    /// whether the block is dirty
    modified: bool,
}

impl BlockCache {
    /// Load a new BlockCache from disk.
    pub fn new(
        block_id: usize,
        block_device: Arc<dyn BlockDevice>
    ) -> Self {
        let mut cache = [0u8; BLOCK_SZ];
        block_device.read_block(block_id, &mut cache);
        Self {
            cache,
            block_id,
            block_device,
            modified: false,
        }
    }
    /// Get the address of an offset inside the cached block data
    fn addr_of_offset(&self, offset: usize) -> usize {
        &self.cache[offset] as *const _ as usize
    }

    pub fn get_ref<T>(&self, offset: usize) -> &T where T: Sized {
        let type_size = core::mem::size_of::<T>();
        assert!(offset + type_size <= BLOCK_SZ);
        let addr = self.addr_of_offset(offset);
        unsafe { &*(addr as *const T) } 
    }

    pub fn get_mut<T>(&mut self, offset: usize) -> &mut T where T: Sized {
        let type_size = core::mem::size_of::<T>();
        assert!(offset + type_size <= BLOCK_SZ);
        self.modified = true;
        let addr = self.addr_of_offset(offset);
        unsafe { &mut *(addr as *mut T) }
    }

    pub fn read<T, V>(&self, offset: usize, f: impl FnOnce(&T) -> V) -> V {
        f(self.get_ref(offset))
    }

    pub fn modify<T, V>(&mut self, offset:usize, f: impl FnOnce(&mut T) -> V) -> V {
        f(self.get_mut(offset))
    }

    pub fn sync(&mut self) {
        if self.modified {
            self.modified = false;
            self.block_device.write_block(self.block_id, &self.cache);
        }
    }
}

impl Drop for BlockCache {
    fn drop(&mut self) {
        self.sync()
    }
}

/// Use a block cache of 16 blocks
const BLOCK_CACHE_SIZE: usize = 16;

pub struct BlockCacheManager {
    queue: VecDeque<(usize, Arc<Mutex<BlockCache>>)>,
}

impl BlockCacheManager {
    pub fn new() -> Self {
        Self { queue: VecDeque::new() }
    }

    pub fn get_block_cache(
        &mut self,
        block_id: usize,
        block_device: Arc<dyn BlockDevice>,
    ) -> Arc<Mutex<BlockCache>> {
        if let Some(pair) = self.queue
            .iter()
            .find(|pair| pair.0 == block_id) {
                Arc::clone(&pair.1)
        } else {
            // substitute
            if self.queue.len() == BLOCK_CACHE_SIZE {
                // from front to tail
                if let Some((idx, _)) = self.queue
                    .iter()
                    .enumerate()
                    .find(|(_, pair)| Arc::strong_count(&pair.1) == 1) {
                    self.queue.drain(idx..=idx);
                } else {
                    panic!("Run out of BlockCache!");
                }
            }
            // load block into mem and push back
            let block_cache = Arc::new(Mutex::new(
                BlockCache::new(block_id, Arc::clone(&block_device))
            ));
            self.queue.push_back((block_id, Arc::clone(&block_cache)));
            block_cache
        }
    }
}

lazy_static! {
    /// The global block cache manager
    pub static ref BLOCK_CACHE_MANAGER: Mutex<BlockCacheManager> = Mutex::new(
        BlockCacheManager::new()
    );
}

/// Get the block cache corresponding to the given block id and block device
pub fn get_block_cache(
    block_id: usize,
    block_device: Arc<dyn BlockDevice>
) -> Arc<Mutex<BlockCache>> {
    BLOCK_CACHE_MANAGER.lock().get_block_cache(block_id, block_device)
}

/// Sync all block cache to block device
pub fn block_cache_sync_all() {
    let manager = BLOCK_CACHE_MANAGER.lock();
    for (_, cache) in manager.queue.iter() {
        cache.lock().sync();
    }
}
  • 第 12 - 21 行:定义了块缓存 BlockCache

    • cache :是一个 512 字节的数组,表示位于内存中的缓冲区
    • block_id :记录了这个块缓存来自于磁盘中的块的编号
    • block_device :是一个底层块设备的引用,可通过它进行块读写
    • modified :记录这个块从磁盘载入内存缓存之后,它有没有被修改过
  • 第 23 - 72 行:为块缓存 BlockCache实现了方法:

    • 第 25 - 37 行:实现了new方法,当我们创建一个 BlockCache 的时候,这将触发一次 read_block 将一个块上的数据从磁盘读到缓冲区 cache 。一旦磁盘块已经存在于内存缓存中,CPU 就可以直接访问磁盘块数据了。(依据传入的id将一个块上的数据从磁盘读到缓冲区块创建BlockCache

    • 第 39 - 41 行:实现了addr_of_offset方法,可以得到一个 BlockCache 内部的缓冲区中指定偏移量 offset 的字节地址。(获取指定偏移量的地址)

    • 第 43 - 48 行:get_ref 是一个泛型方法,它可以获取缓冲区中的位于偏移量 offset 的一个类型为 T 的磁盘上数据结构的不可变引用。该泛型方法的 Trait Bound 限制类型 T 必须是一个编译时已知大小的类型。(获取指定偏移量的一个类型值)

      • 第 44 行:我们通过 core::mem::size_of::<T>() 在编译时获取类型 T 的大小,这里编译器会自动进行生命周期标注,约束返回的引用的生命周期不超过 BlockCache 自身。
      • 第 45 行:确认该数据结构被整个包含在磁盘块及其缓冲区之内。
      • 第 46 行:获取该起始字节地址。
      • 第 47 行:将其进行返回。
    • 第 50 - 56 行:get_mutget_ref 的不同之处在于, get_mut 会获取磁盘上数据结构的可变引用,由此可以对数据结构进行修改。代码上,两者的唯一不同在于,由于这些数据结构目前位于内存中的缓冲区中,我们需要BlockCachemodified 标记为 true 表示该缓冲区已经被修改之后需要将数据写回磁盘块才能真正将修改同步到磁盘。(获取指定偏移量的一个类型值的可变引用)

    • 第 58 - 60 行:实现了read方法,在 BlockCache 缓冲区偏移量为 offset 的位置获取一个类型为 T 的磁盘上数据结构的不可变引用,并让它执行传入的闭包 f 中所定义的操作。注意 read/modify 的返回值是和传入闭包的返回值相同的,因此相当于 read/modify 构成了传入闭包 f 的一层执行环境,让它能够绑定到一个缓冲区上执行。(将指定偏移量的一个类型值作为参数执行闭包)

      这里我们传入闭包的类型为 FnOnce ,这是因为闭包里面的变量被捕获的方式涵盖了不可变引用/可变引用/和 move 三种可能性,故而我们需要选取范围最广的 FnOnce 。参数中的 impl 关键字体现了一种类似泛型的静态分发功能。

    • 第 62 - 64 行:实现了modify方法,与read方法类似,在 BlockCache 缓冲区偏移量为 offset 的位置获取一个类型为 T 的磁盘上数据结构的可变引用,并让它执行传入的闭包 f 中所定义的操作。(将指定偏移量的一个可变类型值作为参数执行闭包)

    • 第 66 - 72 行:实现了sync方法,该方法通过查看块是否被修改决定是否写回到磁盘上。(对BlockCache数据进行判断写回磁盘)

    • 第 74 - 78 行:实现了drop方法,在 BlockCachedrop 的时候,它会首先调用 sync 方法,如果自身确实被修改过的话才会将缓冲区的内容写回磁盘。

      sync 并不是只有在 drop 的时候才会被调用。在 Linux 中,通常有一个后台进程负责定期将内存中缓冲区的内容写回磁盘。另外有一个 sys_fsync 系统调用可以让应用主动通知内核将一个文件的修改同步回磁盘

  • 第 81 行:BLOCK_CACHE_SIZE表明了内存中同时驻留磁盘块缓冲区的数量。为了避免在块缓存上浪费过多内存,我们希望内存中同时只能驻留有限个磁盘块的缓冲区

  • 第 83 - 85 行:定义了块缓存管理结构体BlockCacheManager。队列 queue 中管理的是块编号和块缓存的二元组。块编号的类型为 usize ,而块缓存的类型则是一个 Arc<Mutex<BlockCache>>

  • 第 87 - 122 行:为BlockCacheManager实现了方法。

    • 第 88 - 90 行:实现了new方法,创建一个队列。(创建块缓存管理结构体)

    • 第 92 - 121 行:get_block_cache 方法尝试从块缓存管理器中获取一个编号为 block_id 的块的块缓存,如果找不到,会从磁盘读取到内存中,还有可能会发生缓存替换

      • 第 97 行:遍历整个队列试图找到一个编号相同的块缓存,如果找到了,会将块缓存管理器中保存的块缓存的引用复制一份并返回。

      • 第 101 行:对应找不到的情况,此时必须将块从磁盘读入内存中的缓冲区。

      • 第 103 行:在实际读取之前,需要判断管理器保存的块缓存数量是否已经达到了上限。如果达到了上限(第 103 行)才需要执行缓存替换算法,丢掉某个块缓存并空出一个空位。

      • 第 105 行:从队头遍历到队尾找到第一个强引用计数恰好为 1 的块缓存。

        这里使用一种类 FIFO 算法:每加入一个块缓存时要从队尾加入;要替换时则从队头弹出。但此时队头对应的块缓存可能仍在使用:判断的标志是其强引用计数 ≥2 ,即除了块缓存管理器保留的一份副本之外,在外面还有若干份副本正在使用。因此,我们的做法是从队头遍历到队尾找到第一个强引用计数恰好为 1 的块缓存并将其替换出去。

      • 第 110 行:队列已满且其中所有的块缓存都正在使用的情形,则直接Panic

      • 第 115 -117 行:开始我们创建一个新的块缓存(会触发 read_block 进行块读取)

      • 第 118 - 119 行:将新创建的块缓存加入到队尾,最后返回给请求者。

  • 第 124 - 129 行:创建 BlockCacheManager 的全局实例。

  • 第 132 - 137 行:将get_block_cache方法封装成函数,就可以直接通过 get_block_cache 方法来请求块缓存了。

  • 第 140 - 145 行:将缓存的块全部写入到磁盘中。

【块缓存全局管理器】

块缓存全局管理器的功能是:当我们要对一个磁盘块进行读写时,首先看它是否已经被载入到内存缓存中了,如果已经被载入的话则直接返回否则需要先读取磁盘块的数据到内存缓存中。此时,如果内存中驻留的磁盘块缓冲区的数量已满,则需要遵循某种缓存替换算法将某个块的缓存从内存中移除,再将刚刚读到的块数据加入到内存缓存中。我们这里使用一种类 FIFO 的简单缓存替换算法,因此在管理器中只需维护一个队列。

在这里插入图片描述

【磁盘布局及磁盘上数据结构】

对于一个文件系统而言,最重要的功能是如何将一个逻辑上的文件目录树结构映射到磁盘上决定磁盘上的每个块应该存储文件相关的哪些数据。为了更容易进行管理和更新,我们需要将磁盘上的数据组织为若干种不同的磁盘上数据结构,并合理安排它们在磁盘中的位置。

【easy-fs 磁盘布局概述】

easy-fs 磁盘布局中,按照块编号从小到大顺序地分成 5 个不同属性的连续区域

  • 最开始的区域的长度为一个块,其内容是 easy-fs 超级块 (Super Block)。超级块内以magic的形式提供了文件系统合法性检查功能,同时还可以定位其他连续区域的位置
  • 第二个区域是一个索引节点位图,长度为若干个块。它记录了后面的索引节点区域中有哪些索引节点已经被分配出去使用了,而哪些还尚未被分配出去。
  • 第三个区域是索引节点区域,长度为若干个块。其中的每个块都存储了若干个索引节点
  • 第四个区域是一个数据块位图,长度为若干个块。它记录了后面的数据块区域中有哪些数据块已经被分配出去使用了,而哪些还尚未被分配出去。
  • 最后的区域则是数据块区域,顾名思义,其中的每一个已经分配出去的块保存了文件或目录中的具体数据内容

索引节点 (Inode, Index Node) 是文件系统中的一种重要数据结构。逻辑目录树结构中的每个文件和目录都对应一个 inode ,我们前面提到的文件系统实现中,文件/目录的底层编号实际上就是指 inode 编号。在 inode 中不仅包含了我们通过 stat 工具能够看到的文件/目录的元数据(大小/访问权限/类型等信息),还包含实际保存对应文件/目录数据的数据块(位于最后的数据块区域中)的索引信息,从而能够找到文件/目录的数据被保存在磁盘的哪些块中。从索引方式上看,同时支持直接索引和间接索引。

【easy-fs 超级块 -> easy-fs/src/layout.rs】

  • 超级块的结构体为SuperBlock,它描述了磁盘上数据的存储结构。
  • easy-fs/src/layout.rs该文件主要描述了磁盘上的数据结构和布局。包括超级块的数据结构和方法、**磁盘inode**数据结构和方法、文件目录数据结构和方法、数据块结构。
//! easy-fs/src/layout.rs
use core::fmt::{Debug, Formatter, Result};
use super::{
    BLOCK_SZ,
    BlockDevice,
    get_block_cache,
};
use alloc::sync::Arc;
use alloc::vec::Vec;

/// Magic number for sanity check
const EFS_MAGIC: u32 = 0x3b800001;
/// The max number of direct inodes
const INODE_DIRECT_COUNT: usize = 28;
/// The max length of inode name
const NAME_LENGTH_LIMIT: usize = 27;
/// The max number of indirect1 inodes
const INODE_INDIRECT1_COUNT: usize = BLOCK_SZ / 4;
/// The max number of indirect2 inodes
const INODE_INDIRECT2_COUNT: usize = INODE_INDIRECT1_COUNT * INODE_INDIRECT1_COUNT;
/// The upper bound of direct inode index
const DIRECT_BOUND: usize = INODE_DIRECT_COUNT;
/// The upper bound of indirect1 inode index
const INDIRECT1_BOUND: usize = DIRECT_BOUND + INODE_INDIRECT1_COUNT;
/// The upper bound of indirect2 inode index
#[allow(unused)]
const INDIRECT2_BOUND: usize = INDIRECT1_BOUND + INODE_INDIRECT2_COUNT;

/// Super block 超级块
#[repr(C)]
pub struct SuperBlock {
    magic: u32,
    pub total_blocks: u32,
    pub inode_bitmap_blocks: u32,
    pub inode_area_blocks: u32,
    pub data_bitmap_blocks: u32,
    pub data_area_blocks: u32,
}

impl Debug for SuperBlock {
    fn fmt(&self, f: &mut Formatter<'_>) -> Result {
        f.debug_struct("SuperBlock")
            .field("total_blocks", &self.total_blocks)
            .field("inode_bitmap_blocks", &self.inode_bitmap_blocks)
            .field("inode_area_blocks", &self.inode_area_blocks)
            .field("data_bitmap_blocks", &self.data_bitmap_blocks)
            .field("data_area_blocks", &self.data_area_blocks)
            .finish()
    }
}

impl SuperBlock {
    /// Initialize a super block
    pub fn initialize(
        &mut self,
        total_blocks: u32,
        inode_bitmap_blocks: u32,
        inode_area_blocks: u32,
        data_bitmap_blocks: u32,
        data_area_blocks: u32,
    ) {
        *self = Self {
            magic: EFS_MAGIC,
            total_blocks,
            inode_bitmap_blocks,
            inode_area_blocks,
            data_bitmap_blocks,
            data_area_blocks,
        }
    }
    /// Check if a super block is valid using efs magic
    pub fn is_valid(&self) -> bool {
        self.magic == EFS_MAGIC
    }
}

/// Type of a disk inode
#[derive(PartialEq)]
pub enum DiskInodeType {
    File,
    Directory,
}

/// A indirect block
type IndirectBlock = [u32; BLOCK_SZ / 4];
/// A data block
type DataBlock = [u8; BLOCK_SZ];

/// 磁盘inode数据结构
#[repr(C)]
pub struct DiskInode {
    pub size: u32,
    pub direct: [u32; INODE_DIRECT_COUNT],
    pub indirect1: u32,
    pub indirect2: u32,
    type_: DiskInodeType,
}

impl DiskInode {
    /// Initialize a disk inode, as well as all direct inodes under it
    /// indirect1 and indirect2 block are allocated only when they are needed
    pub fn initialize(&mut self, type_: DiskInodeType) {
        self.size = 0;
        self.direct.iter_mut().for_each(|v| *v = 0);
        self.indirect1 = 0;
        self.indirect2 = 0;
        self.type_ = type_;
    }
    /// Whether this inode is a directory
    pub fn is_dir(&self) -> bool {
        self.type_ == DiskInodeType::Directory
    }
    /// Whether this inode is a file
    #[allow(unused)]
    pub fn is_file(&self) -> bool {
        self.type_ == DiskInodeType::File
    }
    /// Get the number of data blocks corresponding to size
    pub fn data_blocks(&self) -> u32 {
        Self::_data_blocks(self.size)
    }
    fn _data_blocks(size: u32) -> u32 {
        (size + BLOCK_SZ as u32 - 1) / BLOCK_SZ as u32
    }
    /// Get the number of data blocks required for the given size of data
    pub fn total_blocks(size: u32) -> u32 {
        let data_blocks = Self::_data_blocks(size) as usize;
        let mut total = data_blocks as usize;
        // indirect1
        if data_blocks > INODE_DIRECT_COUNT {
            total += 1;
        }
        // indirect2
        if data_blocks > INDIRECT1_BOUND {
            total += 1;
            // sub indirect1
            total += (data_blocks - INDIRECT1_BOUND + INODE_INDIRECT1_COUNT - 1) / INODE_INDIRECT1_COUNT;
        }
        total as u32
    }
    /// Get the number of data blocks that have to be allocated given the new size of data
    pub fn blocks_num_needed(&self, new_size: u32) -> u32 {
        assert!(new_size >= self.size);
        Self::total_blocks(new_size) - Self::total_blocks(self.size)
    }
    /// Get id of block given inner id
    pub fn get_block_id(&self, inner_id: u32, block_device: &Arc<dyn BlockDevice>) -> u32 {
        let inner_id = inner_id as usize;
        if inner_id < INODE_DIRECT_COUNT {
            self.direct[inner_id]
        } else if inner_id < INDIRECT1_BOUND {
            get_block_cache(self.indirect1 as usize, Arc::clone(block_device))
                .lock()
                .read(0, |indirect_block: &IndirectBlock| {
                    indirect_block[inner_id - INODE_DIRECT_COUNT]
                })
        } else {
            let last = inner_id - INDIRECT1_BOUND;
            let indirect1 = get_block_cache(
                self.indirect2 as usize,
                Arc::clone(block_device)
            )
            .lock()
            .read(0, |indirect2: &IndirectBlock| {
                indirect2[last / INODE_INDIRECT1_COUNT]
            });
            get_block_cache(
                indirect1 as usize,
                Arc::clone(block_device)
            )
            .lock()
            .read(0, |indirect1: &IndirectBlock| {
                indirect1[last % INODE_INDIRECT1_COUNT]
            })
        }
    }
    /// Inncrease the size of current disk inode
    pub fn increase_size(
        &mut self,
        new_size: u32,
        new_blocks: Vec<u32>,
        block_device: &Arc<dyn BlockDevice>,
    ) {
        let mut current_blocks = self.data_blocks();
        self.size = new_size;
        let mut total_blocks = self.data_blocks();
        let mut new_blocks = new_blocks.into_iter();
        // fill direct
        while current_blocks < total_blocks.min(INODE_DIRECT_COUNT as u32) {
            self.direct[current_blocks as usize] = new_blocks.next().unwrap();
            current_blocks += 1;
        }
        // alloc indirect1
        if total_blocks > INODE_DIRECT_COUNT as u32{
            if current_blocks == INODE_DIRECT_COUNT as u32 {
                self.indirect1 = new_blocks.next().unwrap();
            }
            current_blocks -= INODE_DIRECT_COUNT as u32;
            total_blocks -= INODE_DIRECT_COUNT as u32;
        } else {
            return;
        }
        // fill indirect1
        get_block_cache(
            self.indirect1 as usize,
            Arc::clone(block_device)
        )
        .lock()
        .modify(0, |indirect1: &mut IndirectBlock| {
            while current_blocks < total_blocks.min(INODE_INDIRECT1_COUNT as u32) {
                indirect1[current_blocks as usize] = new_blocks.next().unwrap();
                current_blocks += 1;
            }
        });
        // alloc indirect2
        if total_blocks > INODE_INDIRECT1_COUNT as u32 {
            if current_blocks == INODE_INDIRECT1_COUNT as u32 {
                self.indirect2 = new_blocks.next().unwrap();
            }
            current_blocks -= INODE_INDIRECT1_COUNT as u32;
            total_blocks -= INODE_INDIRECT1_COUNT as u32;
        } else {
            return;
        }
        // fill indirect2 from (a0, b0) -> (a1, b1)
        let mut a0 = current_blocks as usize / INODE_INDIRECT1_COUNT;
        let mut b0 = current_blocks as usize % INODE_INDIRECT1_COUNT;
        let a1 = total_blocks as usize / INODE_INDIRECT1_COUNT;
        let b1 = total_blocks as usize % INODE_INDIRECT1_COUNT;
        // alloc low-level indirect1
        get_block_cache(
            self.indirect2 as usize,
            Arc::clone(block_device)
        )
        .lock()
        .modify(0, |indirect2: &mut IndirectBlock| {
            while (a0 < a1) || (a0 == a1 && b0 < b1) {
                if b0 == 0 {
                    indirect2[a0] = new_blocks.next().unwrap();
                }
                // fill current
                get_block_cache(
                    indirect2[a0] as usize,
                    Arc::clone(block_device)
                )
                .lock()
                .modify(0, |indirect1: &mut IndirectBlock| {
                    indirect1[b0] = new_blocks.next().unwrap();
                });
                // move to next
                b0 += 1;
                if b0 == INODE_INDIRECT1_COUNT {
                    b0 = 0;
                    a0 += 1;
                }
            }
        });
    }
    /// Clear size to zero and return blocks that should be deallocated
    /// and clear the block contents to zero later
    pub fn clear_size(&mut self, block_device: &Arc<dyn BlockDevice>) -> Vec<u32> {
        let mut v: Vec<u32> = Vec::new();
        let mut data_blocks = self.data_blocks() as usize;
        self.size = 0;
        let mut current_blocks = 0usize;
        // direct
        while current_blocks < data_blocks.min(INODE_DIRECT_COUNT) {
            v.push(self.direct[current_blocks]);
            self.direct[current_blocks] = 0;
            current_blocks += 1;
        }
        // indirect1 block
        if data_blocks > INODE_DIRECT_COUNT {
            v.push(self.indirect1);
            data_blocks -= INODE_DIRECT_COUNT;
            current_blocks = 0;
        } else {
            return v;
        }
        // indirect1
        get_block_cache(
            self.indirect1 as usize,
            Arc::clone(block_device),
        )
        .lock()
        .modify(0, |indirect1: &mut IndirectBlock| {
            while current_blocks < data_blocks.min(INODE_INDIRECT1_COUNT) {
                v.push(indirect1[current_blocks]);
                //indirect1[current_blocks] = 0;
                current_blocks += 1;
            }
        });
        self.indirect1 = 0;
        // indirect2 block
        if data_blocks > INODE_INDIRECT1_COUNT {
            v.push(self.indirect2);
            data_blocks -= INODE_INDIRECT1_COUNT;
        } else {
            return v;
        }
        // indirect2
        assert!(data_blocks <= INODE_INDIRECT2_COUNT);
        let a1 = data_blocks / INODE_INDIRECT1_COUNT;
        let b1 = data_blocks % INODE_INDIRECT1_COUNT;
        get_block_cache(
            self.indirect2 as usize,
            Arc::clone(block_device),
        )
        .lock()
        .modify(0, |indirect2: &mut IndirectBlock| {
            // full indirect1 blocks
            for i in 0..a1 {
                v.push(indirect2[i]);
                get_block_cache(
                    indirect2[i] as usize,
                    Arc::clone(block_device),
                )
                .lock()
                .modify(0, |indirect1: &mut IndirectBlock| {
                    for j in 0..INODE_INDIRECT1_COUNT {
                        v.push(indirect1[j]);
                        //indirect1[j] = 0;
                    }
                });
                //indirect2[i] = 0;
            }
            // last indirect1 block
            if b1 > 0 {
                v.push(indirect2[a1]);
                get_block_cache(
                    indirect2[a1] as usize,
                    Arc::clone(block_device),
                )
                .lock()
                .modify(0, |indirect1: &mut IndirectBlock| {
                    for j in 0..b1 {
                        v.push(indirect1[j]);
                        //indirect1[j] = 0;
                    }
                });
                //indirect2[a1] = 0;
            }
        });
        self.indirect2 = 0;
        v
    }
    /// Read data from current disk inode
    pub fn read_at(
        &self,
        offset: usize,
        buf: &mut [u8],
        block_device: &Arc<dyn BlockDevice>,
    ) -> usize {
        let mut start = offset;
        let end = (offset + buf.len()).min(self.size as usize);
        if start >= end {
            return 0;
        }
        let mut start_block = start / BLOCK_SZ;
        let mut read_size = 0usize;
        loop {
            // calculate end of current block
            let mut end_current_block = (start / BLOCK_SZ + 1) * BLOCK_SZ;
            end_current_block = end_current_block.min(end);
            // read and update read size
            let block_read_size = end_current_block - start;
            let dst = &mut buf[read_size..read_size + block_read_size];
            get_block_cache(
                self.get_block_id(start_block as u32, block_device) as usize,
                Arc::clone(block_device),
            )
            .lock()
            .read(0, |data_block: &DataBlock| {
                let src = &data_block[start % BLOCK_SZ..start % BLOCK_SZ + block_read_size];
                dst.copy_from_slice(src);
            });
            read_size += block_read_size;
            // move to next block
            if end_current_block == end { break; }
            start_block += 1;
            start = end_current_block;
        }
        read_size
    }
    /// Write data into current disk inode
    /// size must be adjusted properly beforehand
    pub fn write_at(
        &mut self,
        offset: usize,
        buf: &[u8],
        block_device: &Arc<dyn BlockDevice>,
    ) -> usize {
        let mut start = offset;
        let end = (offset + buf.len()).min(self.size as usize);
        assert!(start <= end);
        let mut start_block = start / BLOCK_SZ;
        let mut write_size = 0usize;
        loop {
            // calculate end of current block
            let mut end_current_block = (start / BLOCK_SZ + 1) * BLOCK_SZ;
            end_current_block = end_current_block.min(end);
            // write and update write size
            let block_write_size = end_current_block - start;
            get_block_cache(
                self.get_block_id(start_block as u32, block_device) as usize,
                Arc::clone(block_device)
            )
            .lock()
            .modify(0, |data_block: &mut DataBlock| {
                let src = &buf[write_size..write_size + block_write_size];
                let dst = &mut data_block[start % BLOCK_SZ..start % BLOCK_SZ + block_write_size];
                dst.copy_from_slice(src);
            });
            write_size += block_write_size;
            // move to next block
            if end_current_block == end { break; }
            start_block += 1;
            start = end_current_block;
        }
        write_size
    }
}

///文件目录数据结构
#[repr(C)]
pub struct DirEntry {
    name: [u8; NAME_LENGTH_LIMIT + 1],
    inode_number: u32,
}

/// Size of a directory entry
pub const DIRENT_SZ: usize = 32;

impl DirEntry {
    /// Create an empty directory entry
    pub fn empty() -> Self {
        Self {
            name: [0u8; NAME_LENGTH_LIMIT + 1],
            inode_number: 0,
        }
    }
    /// Crate a directory entry from name and inode number
    pub fn new(name: &str, inode_number: u32) -> Self {
        let mut bytes = [0u8; NAME_LENGTH_LIMIT + 1];
        bytes[..name.len()].copy_from_slice(name.as_bytes());
        Self {
            name: bytes,
            inode_number,
        }
    }
    /// Serialize into bytes
    pub fn as_bytes(&self) -> &[u8] {
        unsafe {
            core::slice::from_raw_parts(
                self as *const _ as usize as *const u8,
                DIRENT_SZ,
            )
        }
    }
    /// Serialize into mutable bytes
    pub fn as_bytes_mut(&mut self) -> &mut [u8] {
        unsafe {
            core::slice::from_raw_parts_mut(
                self as *mut _ as usize as *mut u8,
                DIRENT_SZ,
            )
        }
    }
    /// Get name of the entry
    pub fn name(&self) -> &str {
        let len = (0usize..).find(|i| self.name[*i] == 0).unwrap();
        core::str::from_utf8(&self.name[..len]).unwrap()
    }
    /// Get inode number of the entry
    pub fn inode_number(&self) -> u32 {
        self.inode_number
    }
}
  • 第 14 行:INODE_DIRECT_COUNT表明直接索引的最大数量为28。

  • 第 16 行:NAME_LENGTH_LIMIT表示目录项 Dirent 最大允许保存长度为 27 的文件/目录名(数组 name 中最末的一个字节留给 \0 )。

  • 第 18 行:INODE_INDIRECT1_COUNT表明indirect1一级间接索引的最大数量。

  • 第 20 行:INODE_INDIRECT2_COUNT表明indirect2二级间接索引的最大数量。

  • 第 31 - 38 行:定义了超级块 SuperBlock结构。

    • 第 32 行:magic 是一个用于文件系统合法性验证的魔数。
    • 第 33 行:total_block 给出文件系统的总块数。注意这并不等同于所在磁盘的总块数,因为文件系统很可能并没有占据整个磁盘。
    • 第 34 - 37 行:easy-fs 布局中后四个连续区域的长度各为多少个块。
  • 第 40 - 50 行:为SuperBlock实现了Debug方法。

  • 第 52 - 75 行:为SuperBlock结构实现了initializeis_valid方法。

    • 第 54 - 70 行:在创建一个 easy-fs 的时候对超级块进行初始化,注意各个区域的块数是以参数的形式传入进来的,它们的划分是更上层的磁盘块管理器需要完成的工作。

    • 第 72 - 74 行:is_valid 则可以通过魔数判断超级块所在的文件系统是否合法

      SuperBlock 是一个磁盘上数据结构,它就存放在磁盘上编号为 0 的块的起始处。

  • 第 78 - 82 行:定义了文件类型枚举,目前仅支持文件 File 和目录 Directory 两种类型。

  • 第 87 行:定义了数据块的结构体,其为u8类型的数组。

  • 第 91 - 97 行:定义了DiskInode 形式的数据结构,每个文件/目录在磁盘上均以一个 DiskInode 的形式存储

    • size:表示文件/目录内容的字节数

    • type_ :表示索引节点的类型 DiskInodeType ,目前仅支持文件 File 和目录 Directory 两种类型。

    • direct:是存储文件内容/目录内容的数据块的索引,这也是索引节点名字的由来。

    文件很小的时候,只需用到直接索引direct 数组中最多可以指向 INODE_DIRECT_COUNT 个数据块,当取值为 28 的时候,通过直接索引可以找到 14KiB 的内容。

    • indirect1:为一级间接索引。

      当文件比较大的时候,不仅直接索引的 direct 数组装满,还需要用到一级间接索引 indirect1它指向一个一级索引块,这个块也位于磁盘布局的数据块区域中。这个一级索引块中的每个 u32 都用来指向数据块区域中一个保存该文件内容的数据块,因此,最多能够索引 512/4=128 个数据块,对应 64KiB 的内容。(4 = u32/8)

    • indirect2:为二级间接索引。

      文件大小超过直接索引和一级索引支持的容量上限 78KiB 的时候,就需要用到二级间接索引 indirect2 。它指向一个位于数据块区域中的二级索引块。二级索引块中的每个 u32 指向一个不同的一级索引块,这些一级索引块也位于数据块区域中。因此,通过二级间接索引最多能够索引 128×64KiB=8MiB 的内容。

    为了充分利用空间,我们DiskInode 的大小设置为 128 字节每个块正好能够容纳 4 个 DiskInode 。在后续需要支持更多类型的元数据的时候,可以适当缩减直接索引 direct 的块数,并将节约出来的空间用来存放其他元数据,仍可保证 DiskInode 的总大小为 128 字节。

  • 第 99 - 422 行:为DiskInode实现了方法。

    • 第 102 - 108 行:通过 initialize 方法可以初始化一个 DiskInode 为一个文件或目录

      • 第 103 行:文件大小设为0。
      • 第 104 行:直接索引 direct 被清零。
      • 第 105 - 106 行:indirect1/2 均被初始化为 0 ,因为最开始文件内容的大小为 0 字节,并不会用到一级/二级索引。为了节约空间,内核会按需分配一级/二级索引块。
      • 第 107 行:设置文件的类型为传入的类型。
    • 第 110 - 112 行:is_dir函数会确定inode是否为目录

    • 第 115 - 117 行:is_file函数会确定inode是否为文件

    • 第 119 - 121 行:data_blocks 方法可以计算为了容纳自身 size 字节的内容需要多少个数据块。计算的过程只需用 size 除以每个块的大小 BLOCK_SZ 并向上取整。

    • 第 122 - 124 行:_data_blocks方法计算 size 除以每个块的大小 BLOCK_SZ 并向上取整。也就是需要多少数据块的内部简单实现

    • 第 126 - 140 行:total_blocks方法统计总共需要多少块(数据块+索引块)。计算的方法也很简单,先调用 data_blocks 得到需要多少数据块,再根据数据块数目所处的区间统计索引块即可。

    • 第 142 - 145 行: blocks_num_needed 可以计算将一个 DiskInodesize 扩容到 new_size 需要额外多少个数据和索引块。这只需要调用两次 total_blocks 作差即可。

    • 第 147 - 176 行:get_block_id 方法体现了 DiskInode 最重要的数据块索引功能,它可以从索引中查到它自身用于保存文件内容的第 block_id 个数据块的块编号,这样后续才能对这个数据块进行访问。

      • 第 149 行:判断block_id是否属于direct的范围。
      • 第 150 行:是的话通过直接索引返回对应的数据块ID
      • 第 151 行:判断block_id是否属于indirect1一级索引的范围。
      • 第 152 - 156 行:访问一级索引直接获取对应的数据块ID
      • 第 157 行:剩下的便是属于二级索引的范围。
      • 第 158 - 175 行:通过一层一层的索引得到数据块ID。需要先查二级索引块找到挂在它下面的一级索引块,再通过一级索引块找到数据块。
    • 第 178 - 258 行:increase_size方法实现了索引的扩容。传入的参数new_size 表示容量扩充之后的文件大小new_blocks 是一个保存了本次容量扩充所需块编号的向量,这些块都是由上层的磁盘块管理器负责分配的。

      • 第 184 行:首先获取当前已经存在了多少blocks
      • 第 185 行:修改size的大小。
      • 第 186 行:通过新的size,确定扩容后需要的总共blocks
      • 第 189 - 192 行:先将直接索引direct补充满。
      • 第 194 行:判断是否直接索引direct补满后也不够,不够则创建一级索引indirect1
      • 第 198 - 199 行:修改current_blockstotal_blocks,将其去掉direct直接索引的大小。
      • 第 200 - 202 行:如果没有剩余需要录入的了,也就是直接索引direct能够满足了则直接返回。
      • 第 204 - 214 行:将剩余的blocks ID一个个写到一级索引indirect1所指向的里头。
      • 第 216 行:判断是否一级索引indirect1补满后也不够,不够则创建二级索引indirect2
      • 第 220 - 221 行:修改current_blockstotal_blocks,将其去掉indirect1一级索引所能存储的块大小。
      • 第 222 - 224 行:如果没有剩余需要录入的了,也就是一级索引indirect1能够满足了则直接返回。
      • 第 226 - 257 行:将剩余的blocks ID一个个写到二级索引indirect2所指向的里头。

      其思路是按照直接索引、一级索引再到二级索引的顺序进行扩充。上一级阔满了向下一级进行增加。

    • 第 261 - 346 行:实现了clear_size 方法,清空文件的内容并回收所有数据和索引块。它会将回收的所有块的编号保存在一个向量中返回给磁盘块管理器。它的实现原理和 increase_size 一样也分为多个阶段层层回收。

    • 第 348 - 384 行:通过 DiskInode读取它索引的那些数据块中的数据。这些数据可以被视为一个字节序列,而每次都是选取其中的一段连续区间进行操作。传入的参数为offset读取字节、所要存储块内容的buf

      • 第 354 行:设置从传入的偏移量开始读取。
      • 第 355 行:读取的数据缓存buff为最大的读取数据量。
      • 第 359 行:计算开始读取的块。start_block 维护着目前是文件内部第多少个数据块。
      • 第 360 行:设置已经读取大小为0。
      • 第 361 - 382 行:循环遍历位于字节区间 start,end 中间的那些块,将它们视为一个 DataBlock (也就是一个字节数组),并将其中的部分内容复制到缓冲区 buf 中适当的区域。
      • 第 383 行:返回最终读取的自己数量。
    • 第 387 - 421 行:实现了write_at方法。write_at 的实现思路基本上和 read_at 完全相同。但不同的是 write_at 不会出现失败的情况;只要 Inode 管理的数据块的大小足够,传入的整个缓冲区的数据都必定会被写入到文件中。当从 offset 开始的区间超出了文件范围的时候,就需要调用者在调用 write_at 之前提前调用 increase_size ,将文件大小扩充到区间的右端,保证写入的完整性。

  • 第 426 行:定义了目录项的结构体。每个目录项都是一个二元组,且它自身占据空间 32 字节,每个数据块可以存储 16 个目录项。

    • 第 427 行:name为目录下面的一个文件(或子目录)的文件名(或目录名)。
    • 第 428 行:inode_number是文件(或子目录)所在的索引节点编号。
  • 第 434 - 478 行:为目录项 DirEntry 实现了方法。

    • 第 436 - 441 行:实现了empty方法,内部空间赋值为0。

    • 第 443 - 450 行:实现了new方法,依据传入的名字和索引节点编号产生目录项结构体。

      从目录的内容中读取目录项或者是将目录项写入目录的时候,我们需要将目录项转化为缓冲区(即字节切片)的形式来符合索引节点 Inode 数据结构中的 read_atwrite_at 方法接口的要求

    • 第 452 - 459 行:实现了as_bytes方法,将目录项转化为bytes的不可变引用。

    • 第 461 - 468 行:实现了as_bytes_mut方法,将目录项转化为bytes的可变引用。

    • 第 470 - 473 行:实现了name方法,取出目录项的名字。

    • 第 475 - 477 行:实现了inode_number方法,取出文件(或子目录)所在的索引节点编号。

【位图 -> easy-fs/src/bitmap.rs】

  • 在 easy-fs 布局中存在两类不同的位图,分别对索引节点数据块进行管理。
  • 每个位图都由若干个块组成,每个块大小为 512 bytes,即 4096 bits。
  • 每个 bit 都代表一个索引节点/数据块的分配状态, 0 意味着未分配,而 1 则意味着已经分配出去。位图所要做的事情是通过基于 bit 为单位的分配(寻找一个为 0 的bit位并设置为 1)和回收(将bit位清零)来进行索引节点/数据块的分配和回收。
  • easy-fs/src/bitmap.rs该文件主要保存的是位图相关的结构和方法实现
//! easy-fs/src/bitmap.rs
use alloc::sync::Arc;
use super::{
    BlockDevice,
    BLOCK_SZ,
    get_block_cache,
};

/// A bitmap block
type BitmapBlock = [u64; 64];

/// Number of bits in a block
const BLOCK_BITS: usize = BLOCK_SZ * 8;

/// A bitmap
pub struct Bitmap {
    start_block_id: usize,
    blocks: usize,
}

/// Decompose bits into (block_pos, bits64_pos, inner_pos)
fn decomposition(mut bit: usize) -> (usize, usize, usize) {
    let block_pos = bit / BLOCK_BITS;
    bit = bit % BLOCK_BITS;
    (block_pos, bit / 64, bit % 64)
}

impl Bitmap {
    /// A new bitmap from start block id and number of blocks
    pub fn new(start_block_id: usize, blocks: usize) -> Self {
        Self {
            start_block_id,
            blocks,
        }
    }
    /// Allocate a new block from a block device
    pub fn alloc(&self, block_device: &Arc<dyn BlockDevice>) -> Option<usize> {
        for block_id in 0..self.blocks {
            let pos = get_block_cache(
                block_id + self.start_block_id as usize,
                Arc::clone(block_device),
            ).lock()
             .modify(0, |bitmap_block: &mut BitmapBlock| {
                if let Some((bits64_pos, inner_pos)) = bitmap_block
                    .iter()
                    .enumerate()
                    .find(|(_, bits64)| **bits64 != u64::MAX)
                    .map(|(bits64_pos, bits64)| {
                        (bits64_pos, bits64.trailing_ones() as usize)
                    }) {
                    // modify cache
                    bitmap_block[bits64_pos] |= 1u64 << inner_pos;
                    Some(block_id * BLOCK_BITS + bits64_pos * 64 + inner_pos as usize)
                } else {
                    None
                }
            });
            if pos.is_some() {
                return pos;
            }
        }
        None
    }
    /// Deallocate a block
    pub fn dealloc(&self, block_device: &Arc<dyn BlockDevice>, bit: usize) {
        let (block_pos, bits64_pos, inner_pos) = decomposition(bit);
        get_block_cache(
            block_pos + self.start_block_id,
            Arc::clone(block_device)
        ).lock().modify(0, |bitmap_block: &mut BitmapBlock| {
            assert!(bitmap_block[bits64_pos] & (1u64 << inner_pos) > 0);
            bitmap_block[bits64_pos] -= 1u64 << inner_pos;
        });
    }
    /// Get the max number of allocatable blocks
    pub fn maximum(&self) -> usize {
        self.blocks * BLOCK_BITS
    }
}
  • 第 10 行:BitmapBlock 是一个磁盘数据结构,它将位图区域中的一个磁盘块解释为长度为 64 的一个 u64 数组, 每个 u64 打包了一组 64 bits,于是整个数组包含 64×64=4096 bits,且可以以组为单位进行操作。

  • 第 13 行:定义一个块中bits的位数。代表一个bitmap块涵盖了多少可用块。

  • 第 16 - 19 行:定义了Bitmap结构体用来表示位图。

    • 第 17 行:start_block_id表明了它所在区域的起始块编号
    • 第 18 行:blocks表明了区域的长度为多少个块。
  • 第 22 - 26 行:decomposition函数bit编号分解为区域中的块编号 block_pos 、块内的组编号 bits64_pos 以及组内编号 inner_pos 的三元组

  • 第 28 - 78 行:为Bitmap实现了newallocdeallocmaximum这四个方法。

    • 第 30 - 35 行:为Bitmap实现了new方法。通过new方法可以创建一个位图,参数为外部传进来的。

    注意 Bitmap 自身是驻留在内存中的,但是它能够表示索引节点/数据块区域中的那些磁盘块的分配情况。磁盘块上位图区域的数据则是要以磁盘数据结构 BitmapBlock 的格式进行操作

    • 第 37 - 62 行:实现了alloc方法,通过该方法从块设备上得到一个新的块。

      • 第 38 行:枚举区域中的每个块(编号为 block_id ),在循环内部我们需要读写这个块,在块内尝试找到一个空闲的bit并置 1 。一旦涉及到块的读写,就需要用到块缓存层提供的接口:

      • 第 39 行:我们调用 get_block_cache 获取块缓存,注意我们传入的块编号是区域起始块编号 start_block_id 加上区域内的块编号 (偏移block_id 得到的块设备上的块编号。

      • 第 42 行:我们通过 .lock() 获取块缓存的互斥锁从而可以对块缓存进行访问。

      • 第 43 行:我们使用到了 BlockCache::modify 接口。它传入的偏移量 offset 为 0,这是因为整个块上只有一个 BitmapBlock ,它的大小恰好为 512 字节。因此我们需要从块的开头开始才能访问到完整的 BitmapBlock 。同时,传给它的闭包需要显式声明参数类型为 &mut BitmapBlock ,不然的话, BlockCache 的泛型方法 modify/get_mut 无法得知应该用哪个类型来解析块上的数据。在声明之后,编译器才能在这里将两个方法中的泛型 T 实例化为具体类型 BitmapBlock

        这里 modify 的含义就是:从缓冲区偏移量为 0 的位置开始将一段连续的数据(数据的长度随具体类型而定)解析为一个 BitmapBlock 并要对该数据结构进行修改。在闭包内部,我们可以使用这个 BitmapBlock可变引用 bitmap_block 对它进行访问

      • 第 44 - 56 行:为闭包的主体,它尝试在 bitmap_block找到一个空闲的bit并返回其位置,如果不存在的话则返回 None

        • 第 45 - 50 行:遍历每 64 bits构成的组(一个 u64 ),如果它并没有达到 u64::MAX (即 2^64 −1 ),则通过 u64::trailing_ones 找到最低的一个 0 并置为 1 。如果能够找到的话,bit组的编号将保存在变量 bits64_pos 中,而分配的bit在组内的位置将保存在变量 inner_pos 中。

        • 第 52 行:将找到的bits位设置为1。

        • 第 53 行:返回对应的块,该计算方式是从大到小计算。首先依据Bitmap块号得到大体的偏移量+每个Bitmap块分成64usize类型的组所对应当前组的偏移量+对应的位号。位图块如下所示:

          在这里插入图片描述

        • 第 58 - 60 行:我们一旦在某个块中找到一个空闲的bit并成功分配,就不再考虑后续的块,则提前返回。

        alloc方法主要思路为:遍历区域中的每个块,再在每个块中以bit组(每组 64 bits)为单位进行遍历,找到一个尚未被全部分配出去的组,最后在里面分配一个bit。它将会返回分配的bit所在的位置,等同于索引节点/数据块的编号。如果所有bit均已经被分配出去了,则返回 None

    • 第 65 - 74 行:实现了dealloc方法,该方法通过传入的bit

      • 第 66 行:将传入的bit通过decomposition转化为三元组。
      • 第 67 - 74 行:将确定的bit位清零即可。
    • 第 76 - 79 行:实现了maximum方法,该方法返回总共有多少bits,也代表最大的块的数量。

【磁盘上索引节点】

在磁盘上的索引节点区域,每个块上都保存着若干个索引节点 DiskInode 。对于索引节点的具体内部实现则体现在easy-fs/src/layout.rs文件中。

在这里插入图片描述

【数据块与目录项】

作为一个文件而言,它的内容在文件系统看来没有任何既定的格式,都只是一个字节序列。因此每个保存内容的数据块都只是一个字节数组。然而,目录的内容却需要遵从一种特殊的格式。其内部保存了文件或子目录的文件名文件所在索引节点的编号。对于数据块与目录项的具体内部实现则体现在easy-fs/src/layout.rs文件中。

在这里插入图片描述

在这里插入图片描述

【磁盘块管理器 -> easy-fs/src/efs.rs】

上面介绍了 easy-fs 的磁盘布局设计以及数据的组织方式 — 即各类磁盘数据结构。但是它们都是以比较零散的形式分开介绍的,并没有体现出磁盘布局上各个区域是如何划分的。实现 easy-fs 的整体磁盘布局,将各段区域及上面的磁盘数据结构结构整合起来就是简易文件系统

EasyFileSystem 知道每个布局区域所在的位置,磁盘块的分配和回收也需要经过它才能完成,因此某种意义上讲它还可以看成一个磁盘块管理器

easy-fs/src/efs.rs该文件主要描述了磁盘块管理器的组成和一些方法,通过磁盘块管理器efs可以将磁盘布局上的数据结构进行整合起来。

//! easy-fs/src/efs.rs
use alloc::sync::Arc;
use spin::Mutex;
use super::{
    BlockDevice,
    Bitmap,
    SuperBlock,
    DiskInode,
    DiskInodeType,
    Inode,
    get_block_cache,
    block_cache_sync_all,
};
use crate::BLOCK_SZ;

/// An easy fs over a block device
pub struct EasyFileSystem {
    pub block_device: Arc<dyn BlockDevice>,
    pub inode_bitmap: Bitmap,
    pub data_bitmap: Bitmap,
    inode_area_start_block: u32,
    data_area_start_block: u32,
}

/// A data block of block size
type DataBlock = [u8; BLOCK_SZ];

impl EasyFileSystem {
    /// Create a filesystem from a block device
    pub fn create(
        block_device: Arc<dyn BlockDevice>,
        total_blocks: u32,
        inode_bitmap_blocks: u32,
    ) -> Arc<Mutex<Self>> {
        // calculate block size of areas & create bitmaps
        let inode_bitmap = Bitmap::new(1, inode_bitmap_blocks as usize);
        let inode_num = inode_bitmap.maximum();
        let inode_area_blocks =
            ((inode_num * core::mem::size_of::<DiskInode>() + BLOCK_SZ - 1) / BLOCK_SZ) as u32;
        let inode_total_blocks = inode_bitmap_blocks + inode_area_blocks;
        let data_total_blocks = total_blocks - 1 - inode_total_blocks;
        let data_bitmap_blocks = (data_total_blocks + 4096) / 4097;
        let data_area_blocks = data_total_blocks - data_bitmap_blocks;
        let data_bitmap = Bitmap::new(
            (1 + inode_bitmap_blocks + inode_area_blocks) as usize,
            data_bitmap_blocks as usize,
        );
        let mut efs = Self {
            block_device: Arc::clone(&block_device),
            inode_bitmap,
            data_bitmap,
            inode_area_start_block: 1 + inode_bitmap_blocks,
            data_area_start_block: 1 + inode_total_blocks + data_bitmap_blocks,
        };
        // clear all blocks
        for i in 0..total_blocks {
            get_block_cache(
                i as usize,
                Arc::clone(&block_device)
            )
            .lock()
            .modify(0, |data_block: &mut DataBlock| {
                for byte in data_block.iter_mut() { *byte = 0; }
            });
        }
        // initialize SuperBlock
        get_block_cache(0, Arc::clone(&block_device))
        .lock()
        .modify(0, |super_block: &mut SuperBlock| {
            super_block.initialize(
                total_blocks,
                inode_bitmap_blocks,
                inode_area_blocks,
                data_bitmap_blocks,
                data_area_blocks,
            );
        });
        // write back immediately
        // 创建根inode "/"
        assert_eq!(efs.alloc_inode(), 0);
        let (root_inode_block_id, root_inode_offset) = efs.get_disk_inode_pos(0);
        get_block_cache(
            root_inode_block_id as usize,
            Arc::clone(&block_device)
        )
        .lock()
        .modify(root_inode_offset, |disk_inode: &mut DiskInode| {
            disk_inode.initialize(DiskInodeType::Directory);
        });
        block_cache_sync_all();
        Arc::new(Mutex::new(efs))
    }
    /// Open a block device as a filesystem
    pub fn open(block_device: Arc<dyn BlockDevice>) -> Arc<Mutex<Self>> {
        // read SuperBlock
        get_block_cache(0, Arc::clone(&block_device))
            .lock()
            .read(0, |super_block: &SuperBlock| {
                assert!(super_block.is_valid(), "Error loading EFS!");
                let inode_total_blocks =
                    super_block.inode_bitmap_blocks + super_block.inode_area_blocks;
                let efs = Self {
                    block_device,
                    inode_bitmap: Bitmap::new(
                        1,
                        super_block.inode_bitmap_blocks as usize
                    ),
                    data_bitmap: Bitmap::new(
                        (1 + inode_total_blocks) as usize,
                        super_block.data_bitmap_blocks as usize,
                    ),
                    inode_area_start_block: 1 + super_block.inode_bitmap_blocks,
                    data_area_start_block: 1 + inode_total_blocks + super_block.data_bitmap_blocks,
                };
                Arc::new(Mutex::new(efs))
            })
    }
    /// Get the root inode of the filesystem
    pub fn root_inode(efs: &Arc<Mutex<Self>>) -> Inode {
        let block_device = Arc::clone(&efs.lock().block_device);
        // acquire efs lock temporarily
        let (block_id, block_offset) = efs.lock().get_disk_inode_pos(0);
        // release efs lock
        Inode::new(
            block_id,
            block_offset,
            Arc::clone(efs),
            block_device,
        )
    }
    /// Get inode by id
    pub fn get_disk_inode_pos(&self, inode_id: u32) -> (u32, usize) {
        let inode_size = core::mem::size_of::<DiskInode>();
        let inodes_per_block = (BLOCK_SZ / inode_size) as u32;
        let block_id = self.inode_area_start_block + inode_id / inodes_per_block;
        (block_id, (inode_id % inodes_per_block) as usize * inode_size)
    }
    /// Get data block by id
    pub fn get_data_block_id(&self, data_block_id: u32) -> u32 {
        self.data_area_start_block + data_block_id
    }
    /// Allocate a new inode
    pub fn alloc_inode(&mut self) -> u32 {
        self.inode_bitmap.alloc(&self.block_device).unwrap() as u32
    }
    /// Allocate a data block
    pub fn alloc_data(&mut self) -> u32 {
        self.data_bitmap.alloc(&self.block_device).unwrap() as u32 + self.data_area_start_block
    }
    /// Deallocate a data block
    pub fn dealloc_data(&mut self, block_id: u32) {
        get_block_cache(
            block_id as usize,
            Arc::clone(&self.block_device)
        )
        .lock()
        .modify(0, |data_block: &mut DataBlock| {
            data_block.iter_mut().for_each(|p| { *p = 0; })
        });
        self.data_bitmap.dealloc(
            &self.block_device,
            (block_id - self.data_area_start_block) as usize
        )
    }
}
  • 第 17 - 23 行:定义了EasyFileSystem,简易磁盘块管理器数据结构。

    • 第 18 行:保留块设备的一个指针 block_device ,在进行后续操作的时候,该指针会被拷贝并传递给下层的数据结构,让它们也能够直接访问块设备
    • 第 19 行:inode_bitmap索引节点位图
    • 第 20 行:data_bitmap数据块位图
    • 第 21 行:inode_area_start_block索引节点区域起始块编号方便确定每个索引节点在磁盘上的具体位置。
    • 第 22 行:data_area_start_block数据块区域起始块编号方便确定每个数据块在磁盘上的具体位置。
  • 第 28 - 162 行:为EasyFileSystem实现了相应的方法。

    • 第 30 - 92 行:为EasyFileSystem实现了create方法,通过 create 方法可以在块设备上创建并初始化一个 easy-fs 文件系统

      • 第 36 - 47 行:根据传入的总共块数量和索引位图表计算每个区域各应该包含多少块

        首先根据 inode 位图的大小计算 inode 区域至少需要多少个块才能够使得 inode 位图中的每个bit都能够有一个实际的 inode 可以对应,这样就确定了 inode 位图区域和 inode 区域的大小

        剩下的块都分配给数据块位图区域和数据块区域。我们希望数据块位图中的每个bit仍然能够对应到一个数据块,但是数据块位图又不能过小,不然会造成某些数据块永远不会被使用。因此数据块位图区域最合理的大小是剩余的块数除以 4097(4096数据+1位表示) 再上取整,因为位图中的每个块能够对应 4096 个数据块。其余的块就都作为数据块使用。

      • 第 48 - 54 行:创建EasyFileSystem的实例。

      • 第 56 - 65 行:将块设备的前 total_blocks 个块清零,因为 easy-fs 要用到它们,这也是为初始化做准备。

      • 第 67 - 77 行:将位于块设备编号为 0 块上的超级块进行初始化,只需传入之前计算得到的每个区域的块数就行了。

        0号块是超级块,inode0为根目录

      • 第 80 行:调用 alloc_inodeinode 位图中分配一个 inode ,由于这是第一次分配,它的编号固定是 0 。其后开始创建根目录 /

      • 第 81 行:调用 get_disk_inode_pos 来根据 0 编号获取该 inode 所在的块的编号以及块内偏移。

      • 第 82 - 89 行:将分配到的 inode 初始化为 easy-fs 中的唯一一个目录。

      • 第 90 行:将内容同步到磁盘中。

      • 第 91 行:将新创建的磁盘块管理返回。

    • 第 94 - 117 行:实现了open方法,通过 open 方法可以从一个已写入了 easy-fs 镜像的块设备上打开我们的 easy-fs 。它只需将块设备编号为 0 的块作为超级块读取进来,就可以从中知道 easy-fs 的磁盘布局,由此可以构造 efs 实例

    • 第 119 - 130 行:实现了root_inode方法,获取根inode文件系统,也就是0号inode

    • 第 132 - 137 行:实现了get_disk_inode_pos方法,即可以从 inode位图上分配的 bit 编号,来算出各个存储inode的磁盘块在磁盘上的实际位置

      • 第 133 行:首先知道每个inode的大小。
      • 第 134 行:计算每个块上能够存储多少个inode
      • 第 135 行:计算inode到底在哪个块上。
    • 第 139 - 141 行:实现了get_data_block_id方法,从数据块位图获取数据块的磁盘块在磁盘上的实际位置

    • 第 143 - 145 行:实现了alloc_inode方法,分配inode返回的参数都表示数据块在块设备上的编号,而不是在数据块位图中分配的bit编号;

    • 第 147 - 149 行:实现了alloc_data方法,分配数据块

    • 第 151 - 164 行:实现了dealloc_data方法,删除数据块。传入的参数都表示数据块在块设备上的编号。

【索引节点 -> easy-fs/src/vfs.rs】

对于文件系统的使用者而言,他们往往不关心磁盘布局是如何实现的,而是更希望能够直接看到目录树结构中逻辑上的文件和目录。为此需要设计索引节点 Inode 暴露给文件系统的使用者,让他们能够直接对文件和目录进行操作。 InodeDiskInode 的区别从它们的名字中就可以看出: DiskInode 放在磁盘块中比较固定的位置,而 Inode 是放在内存中的记录文件索引节点信息的数据结构

easy-fs/src/vfs.rs在磁盘块管理器的上层进行了封装,主要是关于Inode相关的数据结构和实现。

//! easy-fs/src/vfs.rs
use super::{
    BlockDevice,
    DiskInode,
    DiskInodeType,
    DirEntry,
    EasyFileSystem,
    DIRENT_SZ,
    get_block_cache,
    block_cache_sync_all,
};
use alloc::sync::Arc;
use alloc::string::String;
use alloc::vec::Vec;
use spin::{Mutex, MutexGuard};

/// Virtual filesystem layer over easy-fs
pub struct Inode {
    block_id: usize,
    block_offset: usize,
    fs: Arc<Mutex<EasyFileSystem>>,
    block_device: Arc<dyn BlockDevice>,
}

impl Inode {
    /// Create a vfs inode
    pub fn new(
        block_id: u32,
        block_offset: usize,
        fs: Arc<Mutex<EasyFileSystem>>,
        block_device: Arc<dyn BlockDevice>,
    ) -> Self {
        Self {
            block_id: block_id as usize,
            block_offset,
            fs,
            block_device,
        }
    }
    /// Call a function over a disk inode to read it
    fn read_disk_inode<V>(&self, f: impl FnOnce(&DiskInode) -> V) -> V {
        get_block_cache(
            self.block_id,
            Arc::clone(&self.block_device)
        ).lock().read(self.block_offset, f)
    }
    /// Call a function over a disk inode to modify it
    fn modify_disk_inode<V>(&self, f: impl FnOnce(&mut DiskInode) -> V) -> V {
        get_block_cache(
            self.block_id,
            Arc::clone(&self.block_device)
        ).lock().modify(self.block_offset, f)
    }
    /// Find inode under a disk inode by name
    fn find_inode_id(
        &self,
        name: &str,
        disk_inode: &DiskInode,
    ) -> Option<u32> {
        // assert it is a directory
        assert!(disk_inode.is_dir());
        let file_count = (disk_inode.size as usize) / DIRENT_SZ;
        let mut dirent = DirEntry::empty();
        for i in 0..file_count {
            assert_eq!(
                disk_inode.read_at(
                    DIRENT_SZ * i,
                    dirent.as_bytes_mut(),
                    &self.block_device,
                ),
                DIRENT_SZ,
            );
            if dirent.name() == name {
                return Some(dirent.inode_number() as u32);
            }
        }
        None
    }
    /// Find inode under current inode by name
    pub fn find(&self, name: &str) -> Option<Arc<Inode>> {
        let fs = self.fs.lock();
        self.read_disk_inode(|disk_inode| {
            self.find_inode_id(name, disk_inode)
            .map(|inode_id| {
                let (block_id, block_offset) = fs.get_disk_inode_pos(inode_id);
                Arc::new(Self::new(
                    block_id,
                    block_offset,
                    self.fs.clone(),
                    self.block_device.clone(),
                ))
            })
        })
    }
    /// Increase the size of a disk inode
    fn increase_size(
        &self,
        new_size: u32,
        disk_inode: &mut DiskInode,
        fs: &mut MutexGuard<EasyFileSystem>,
    ) {
        if new_size < disk_inode.size {
            return;
        }
        let blocks_needed = disk_inode.blocks_num_needed(new_size);
        let mut v: Vec<u32> = Vec::new();
        for _ in 0..blocks_needed {
            v.push(fs.alloc_data());
        }
        disk_inode.increase_size(new_size, v, &self.block_device);
    }
    /// Create inode under current inode by name
    pub fn create(&self, name: &str) -> Option<Arc<Inode>> {
        let mut fs = self.fs.lock();
        if self.modify_disk_inode(|root_inode| {
            // assert it is a directory
            assert!(root_inode.is_dir());
            // has the file been created?
            self.find_inode_id(name, root_inode)
        }).is_some() {
            return None;
        }
        // create a new file
        // alloc a inode with an indirect block
        let new_inode_id = fs.alloc_inode();
        // initialize inode
        let (new_inode_block_id, new_inode_block_offset) 
            = fs.get_disk_inode_pos(new_inode_id);
        get_block_cache(
            new_inode_block_id as usize,
            Arc::clone(&self.block_device)
        ).lock().modify(new_inode_block_offset, |new_inode: &mut DiskInode| {
            new_inode.initialize(DiskInodeType::File);
        });
        self.modify_disk_inode(|root_inode| {
            // append file in the dirent
            let file_count = (root_inode.size as usize) / DIRENT_SZ;
            let new_size = (file_count + 1) * DIRENT_SZ;
            // increase size
            self.increase_size(new_size as u32, root_inode, &mut fs);
            // write dirent
            let dirent = DirEntry::new(name, new_inode_id);
            root_inode.write_at(
                file_count * DIRENT_SZ,
                dirent.as_bytes(),
                &self.block_device,
            );
        });

        let (block_id, block_offset) = fs.get_disk_inode_pos(new_inode_id);
        block_cache_sync_all();
        // return inode
        Some(Arc::new(Self::new(
            block_id,
            block_offset,
            self.fs.clone(),
            self.block_device.clone(),
        )))
        // release efs lock automatically by compiler
    }
    /// List inodes under current inode
    pub fn ls(&self) -> Vec<String> {
        let _fs = self.fs.lock();
        self.read_disk_inode(|disk_inode| {
            let file_count = (disk_inode.size as usize) / DIRENT_SZ;
            let mut v: Vec<String> = Vec::new();
            for i in 0..file_count {
                let mut dirent = DirEntry::empty();
                assert_eq!(
                    disk_inode.read_at(
                        i * DIRENT_SZ,
                        dirent.as_bytes_mut(),
                        &self.block_device,
                    ),
                    DIRENT_SZ,
                );
                v.push(String::from(dirent.name()));
            }
            v
        })
    }
    /// Read data from current inode
    pub fn read_at(&self, offset: usize, buf: &mut [u8]) -> usize {
        let _fs = self.fs.lock();
        self.read_disk_inode(|disk_inode| {
            disk_inode.read_at(offset, buf, &self.block_device)
        })
    }
    /// Write data to current inode
    pub fn write_at(&self, offset: usize, buf: &[u8]) -> usize {
        let mut fs = self.fs.lock();
        let size = self.modify_disk_inode(|disk_inode| {
            self.increase_size((offset + buf.len()) as u32, disk_inode, &mut fs);
            disk_inode.write_at(offset, buf, &self.block_device)
        });
        block_cache_sync_all();
        size
    }
    /// Clear the data in current inode
    pub fn clear(&self) {
        let mut fs = self.fs.lock();
        self.modify_disk_inode(|disk_inode| {
            let size = disk_inode.size;
            let data_blocks_dealloc = disk_inode.clear_size(&self.block_device);
            assert!(data_blocks_dealloc.len() == DiskInode::total_blocks(size) as usize);
            for data_block in data_blocks_dealloc.into_iter() {
                fs.dealloc_data(data_block);
            }
        });
        block_cache_sync_all();
    }
}
  • 第 18 - 23 行:定义了Inode结构体。Inode是放在内存中的记录文件索引节点信息的数据结构。

    • 第 19 行:block_id 记录该 Inode 对应的 DiskInode 保存在磁盘上的具体块号。
    • 第 20 行: block_offset 记录该 Inode 对应的 DiskInode 保存在磁盘上对应块号的偏移,通过这两个标识方便我们后续对它进行访问。
    • 第 21 行: fs 是指向 EasyFileSystem 的一个指针,因为对 Inode 的种种操作实际上都是要通过底层的文件系统来完成。
    • 第 22 行:保留块设备的一个指针 block_device
  • 第 25 - 211 行:为Inode结构体实现了各种方法。

    • 第 27 - 39 行:实现了new方法,创建一个新的inodeInode::new 过程中不会尝试获取整个 EasyFileSystem 的锁来查询 inode 在块设备中的位置,而是在调用它之前预先查询并作为参数传过去

    • 第 41 - 46 行:实现了read_disk_inode方法,简化BlockCache::read方法。读取磁盘上的DiskInode

    • 第 48 - 53 行:实现了modify_disk_inode方法,简化BlockCache::modify方法。

    • 第 55 - 78 行:实现了find_inode_id方法,尝试从根目录的 DiskInode 上找到要索引的文件名对应的 inode 编号

      • 第 61 行:首先判断传入进来的磁盘inode是否是个文件。
      • 第 62 行:确定该目录下目前有多少文件,也就是磁盘inode存储文件的大小除以每个文件的大小。
      • 第 64 - 76 行:依据文件的数量、一个个从中获取目录项,并判断获取的目录项的名字是否与需要的名字相同。
    • 第 80 - 94 行:实现了find方法,从根目录下找到索引节点的inode编号

      find 方法只会被根目录 Inode 调用,文件系统中其他文件的 Inode 不会调用这个方法。它首先调用 find_inode_id 方法,尝试从根目录的 DiskInode 上找到要索引的文件名对应的 inode 编号。这就需要将根目录内容中的所有目录项都读到内存进行逐个比对。如果能够找到,则 find 方法会根据查到 inode 编号,对应生成一个 Inode 用于后续对文件的访问

      • 第 81 行:持有 EasyFileSystem 的互斥锁。
      • 第 82 行:读取磁盘上的DiskInode
      • 第 83 行:从根目录的 DiskInode 上找到要索引的文件名对应的 inode 编号。
      • 第 84 - 92 行:根据查到 inode 编号,对应生成一个 Inode 用于后续对文件的访问。
    • 第 94 - 111 行:实现了increase_size方法,增加disk_node的大小

      • 第 102 - 104 行:判断传入的数值有误么。
      • 第 105 行 :查看还需要多少块。
      • 第 106 - 110 行:将需要的数据块新增到disk_inode上。
    • 第 113 - 160 行:实现了create方法,create 方法可以在根目录下创建一个文件

      • 第 114 行:持有 EasyFileSystem 的互斥锁。
      • 第 115 - 122 行:检查文件是否已经在根目录下,如果找到的话返回 None
      • 第 123 - 134 行:为待创建文件分配一个新的 inode 并进行初始化。
      • 第 135 - 148 行:将待创建文件的目录项插入到根目录的内容中,使得之后可以索引到。
      • 第 151 行:刷新磁盘区。
      • 第 153 - 158 行:返回新创建的inode信息。
    • 第 162 - 181 行:实现了ls方法,ls 方法可以收集根目录下的所有文件的文件名并以向量的形式返回

      • 第 163 行:持有 EasyFileSystem 的互斥锁。
      • 第 164 行:读取磁盘上的DiskInode
      • 第 165 行:确定该目录下目前有多少文件,也就是磁盘inode存储文件的大小除以每个文件的大小。
      • 第 166 行:创建返回的向量组。
      • 第 167 - 178 行:将每个文件名读取出来并存到向量组中。

      ls 操作中,我们虽然获取了 efs 锁,但是这里并不会直接访问 EasyFileSystem 实例,其目的仅仅是锁住该实例避免其他核在同时间的访问造成并发冲突。因此,我们将其绑定到以 _ 开头的变量 _fs 中,这样即使我们在其作用域中并没有使用它,编译器也不会报警告。

    • 第 183 - 188 行:实现了read_at方法,其内部调用了disk_inode.read_at方法,读取一片inode数据。

    • 第 190 - 198 行:实现了write_at方法。在执行 DiskInode::write_at 之前先调用 increase_size 对自身进行扩容。其内部调用了disk_inode.write_at方法。

    • 第 199 - 212 行:实现了clear方法,在索引到文件的 Inode 之后,可以调用 clear 方法将该文件占据的索引块和数据块回收

      • 第 202 行:通过modify_disk_inode将磁盘的可变类型值作为参数执行闭包。
      • 第 203 行:首先获得磁盘inode的大小。
      • 第 204 行:通过clear_size方法清空文件的内容并回收所有数据和索引块
      • 第 206 - 208 行:通过dealloc_data方法删除数据块在块设备上的编号
      • 第 210 行:刷新磁盘区。

【获取根目录的 inode】

文件系统的使用者在通过 EasyFileSystem::open 从装载了 easy-fs 镜像的块设备上打开 easy-fs 之后,要做的第一件事情就是获取根目录的 Inode 。因为 EasyFileSystem 目前仅支持绝对路径,对于任何文件/目录的索引都必须从根目录开始向下逐级进行。等到索引完成之后, EasyFileSystem 才能对文件/目录进行操作。 EasyFileSystem 提供了另一个名为 root_inode 的方法来获取根目录的 Inode

因为根目录对应于文件系统中第一个分配的 inode ,因此它的 inode_id 总会是 0 。所以在初始化根目录的inode的时候将block_id直接设置为0就可以了

【文件索引】

EasyFileSystem 是一个扁平化的文件系统,即在目录树上仅有一个目录 — 那就是作为根节点的根目录。所有的文件都在根目录下面。于是,我们不必实现目录索引。文件索引的查找比较简单,仅需在根目录的目录项中根据文件名找到文件的 inode 编号即可。由于没有子目录的存在,这个过程只会进行一次。具体实现参考easy-fs/src/vfs.rs

个人理解:首先从根目录下找目录项,通过目录项找到对应名字的索引节点的inode,再通过对应inode得到数据块。

【文件列举】

ls 方法可以收集根目录下的所有文件的文件名并以向量的形式返回,这个方法只有根目录的 Inode 才会调用,具体实现参考easy-fs/src/vfs.rs

【文件创建】

create方法在根目录下创建一个文件,也就是新创建一个inode并将其插入到根目录下。具体实现参考easy-fs/src/vfs.rs

【文件清空】

清空当前inode的数据。具体实现参考easy-fs/src/vfs.rs

【文件读写】

根目录索引到一个文件之后,可以对它进行读写。和 DiskInode 一样,这里的读写作用在字节序列的一段区间上。具体实现参考easy-fs/src/vfs.rs

【在用户态测试 easy-fs 的功能】

easy-fs 架构设计的一个优点在于它可以在Rust应用开发环境(Windows/macOS/Ubuntu)中,按照应用程序库的开发方式来进行测试,不必过早的放到内核中测试运行。

由于 easy-fs 需要放到在裸机上运行的内核中,使得 easy-fs 只能使用 no_std 模式,不能在 easy-fs 中调用标准库 std 。但是在把 easy-fs 作为一个应用的库运行的时候,可以暂时让使用它的应用程序调用标准库 std ,这也会在开发调试上带来一些方便。

easy-fs测试放在另一个名为 easy-fs-fuse 的应用程序中,不同于 easy-fs ,它是一个可以调用标准库 std 的应用程序 ,能够在Rust应用开发环境上运行并很容易调试。

【在Rust应用开发环境中模拟块设备 -> easy-fs-fuse/src/main.rs】

文件系统的使用者角度来看,它仅需要提供一个实现了 BlockDevice Trait 的块设备用来装载文件系统,之后就可以使用 Inode 来方便地进行文件系统操作了。

在开发环境上,我们如何来提供这样一个块设备呢?答案是用 Linux (当然也可以是Windows/MacOS等其它通用操作系统)上的一个文件模拟一个块设备

//! easy-fs-fuse/src/main.rs
use clap::{App, Arg};
use easy_fs::{BlockDevice, EasyFileSystem};
use std::fs::{read_dir, File, OpenOptions};
use std::io::{Read, Seek, SeekFrom, Write};
use std::sync::Arc;
use std::sync::Mutex;

/// Use a block size of 512 bytes
const BLOCK_SZ: usize = 512;
const BLOCK_NUM: usize = 16384;

/// Wrapper for turning a File into a BlockDevice
struct BlockFile(Mutex<File>);

impl BlockDevice for BlockFile {
    /// Read a block from file
    fn read_block(&self, block_id: usize, buf: &mut [u8]) {
        let mut file = self.0.lock().unwrap();
        file.seek(SeekFrom::Start((block_id * BLOCK_SZ) as u64))
            .expect("Error when seeking!");
        assert_eq!(file.read(buf).unwrap(), BLOCK_SZ, "Not a complete block!");
    }
    /// Write a block into file
    fn write_block(&self, block_id: usize, buf: &[u8]) {
        let mut file = self.0.lock().unwrap();
        file.seek(SeekFrom::Start((block_id * BLOCK_SZ) as u64))
            .expect("Error when seeking!");
        assert_eq!(file.write(buf).unwrap(), BLOCK_SZ, "Not a complete block!");
    }
}

fn main() {
    easy_fs_pack().expect("Error when packing easy-fs!");
}

/// Pack a directory into a easy-fs disk image
fn easy_fs_pack() -> std::io::Result<()> {
    let matches = App::new("EasyFileSystem packer")
        .arg(
            Arg::with_name("source")
                .short("s")
                .long("source")
                .takes_value(true)
                .help("Executable source dir(with backslash)"),
        )
        .arg(
            Arg::with_name("target")
                .short("t")
                .long("target")
                .takes_value(true)
                .help("Executable target dir(with backslash)"),
        )
        .get_matches();
    let src_path = matches.value_of("source").unwrap();
    let target_path = matches.value_of("target").unwrap();
    println!("src_path = {}\ntarget_path = {}", src_path, target_path);
    let block_file = Arc::new(BlockFile(Mutex::new({
        let f = OpenOptions::new()
            .read(true)
            .write(true)
            .create(true)
            .open(format!("{}{}", target_path, "fs.img"))?;
        f.set_len((BLOCK_NUM * BLOCK_SZ) as u64).unwrap();
        f
    })));
    let efs = EasyFileSystem::create(block_file.clone(), BLOCK_NUM as u32, 1);
    let root_inode = Arc::new(EasyFileSystem::root_inode(&efs));
    let apps: Vec<_> = read_dir(src_path)
        .unwrap()
        .into_iter()
        .map(|dir_entry| {
            let mut name_with_ext = dir_entry.unwrap().file_name().into_string().unwrap();
            name_with_ext.drain(name_with_ext.find('.').unwrap()..name_with_ext.len());
            name_with_ext
        })
        .collect();
    for app in apps {
        // load app data (elf) from host file system
        let mut host_file = File::open(format!("{}{}", target_path, app)).unwrap();
        let mut all_data: Vec<u8> = Vec::new();
        host_file.read_to_end(&mut all_data).unwrap();
        // create a file in easy-fs
        let inode = root_inode.create(app.as_str()).unwrap();
        // write data to easy-fs
        inode.write_at(0, all_data.as_slice());
    }
    // list apps
    for app in root_inode.ls() {
        println!("{}", app);
    }
    Ok(())
}

#[test]
fn efs_test() -> std::io::Result<()> {
    let block_file = Arc::new(BlockFile(Mutex::new({
        let f = OpenOptions::new()
            .read(true)
            .write(true)
            .create(true)
            .open("target/fs.img")?;
        f.set_len(BLOCK_NUM * BLOCK_SZ).unwrap();
        f
    })));
    EasyFileSystem::create(block_file.clone(), 4096, 1);
    let efs = EasyFileSystem::open(block_file.clone());
    let root_inode = EasyFileSystem::root_inode(&efs);
    root_inode.create("filea");
    root_inode.create("fileb");
    for name in root_inode.ls() {
        println!("{}", name);
    }
    let filea = root_inode.find("filea").unwrap();
    let greet_str = "Hello, world!";
    filea.write_at(0, greet_str.as_bytes());
    //let mut buffer = [0u8; BLOCK_SZ];
    let mut buffer = [0u8; 233];
    let len = filea.read_at(0, &mut buffer);
    assert_eq!(greet_str, core::str::from_utf8(&buffer[..len]).unwrap(),);

    let mut random_str_test = |len: usize| {
        filea.clear();
        assert_eq!(filea.read_at(0, &mut buffer), 0,);
        let mut str = String::new();
        use rand;
        // random digit
        for _ in 0..len {
            str.push(char::from('0' as u8 + rand::random::<u8>() % 10));
        }
        filea.write_at(0, str.as_bytes());
        let mut read_buffer = [0u8; 127];
        let mut offset = 0usize;
        let mut read_str = String::new();
        loop {
            let len = filea.read_at(offset, &mut read_buffer);
            if len == 0 {
                break;
            }
            offset += len;
            read_str.push_str(core::str::from_utf8(&read_buffer[..len]).unwrap());
        }
        assert_eq!(str, read_str);
    };

    random_str_test(4 * BLOCK_SZ);
    random_str_test(8 * BLOCK_SZ + BLOCK_SZ / 2);
    random_str_test(100 * BLOCK_SZ);
    random_str_test(70 * BLOCK_SZ + BLOCK_SZ / 7);
    random_str_test((12 + 128) * BLOCK_SZ);
    random_str_test(400 * BLOCK_SZ);
    random_str_test(1000 * BLOCK_SZ);
    random_str_test(2000 * BLOCK_SZ);

    Ok(())
}
  • 第 14 行:我们将File包装成 BlockFile 类型来模拟一块磁盘。

  • 第 16 - 31 行:为BlockFile类型实现了BlockDevice 接口,File 本身仅通过 read/write 接口是不能实现随机读写的,在访问一个特定的块的时候,我们必须先 seek 到这个块的开头位置seek函数会重新定位被打开文件的位移量),之后再通过read/write接口实现读写

  • 第 38 - 93 行:实现了easy-fs-fuse 的主体 easy-fs-pack 函数,该函数将应用打包为 easy-fs 镜像

    在前面我们需要将所有的应用都链接到内核中,随后在应用管理器中通过应用名进行索引来找到应用的 ELF 数据。这样做有一个缺点,就是会造成内核体积过度膨胀。在实现了 easy-fs 文件系统之后,终于可以将这些应用打包到 easy-fs 镜像中放到磁盘中,当我们要执行应用的时候只需从文件系统中取出ELF 执行文件格式的应用并加载到内存中执行即可,这样就避免了前面章节的存储开销等问题。

    • 第 39 - 56 行:使用 clap crate 进行命令行参数解析,需要通过 -s-t 分别指定应用的源代码目录和保存应用 ELF 的目录,而不是在 easy-fs-fuse 中硬编码。如果解析成功的话它们会分别被保存在变量 src_pathtarget_path 中。

      clap用于解析并验证用户在运行命令行程序时提供的命令行参数字符串。当clap解析了用户提供的参数字符串,它就会返回匹配项以及任何适用的值。 如果用户输入了错误或错字,clap会通知他们错误并退出。

      • 第 41 行:Arg::with_name()调用创建一个简单的命名参数。我们希望我们的应用程序有一个 sourse 参数。
      • 第 42 行:short(): 提供一个简短的单字母表格。
      • 第 55 行:将sourse刚刚得到的参数保存在src_path中。
    • 第 58 - 66 行:创建 4MiBeasy-fs 镜像文件。

    • 第 67 行:从块设备上打开文件系统。

    • 第 68 行:获取根目录 inode。

    • 第 69 - 77 行:获取源码目录中的每个应用的源代码文件并去掉后缀名,收集到向量 apps 中。

    • 第 78 - 87 行:枚举 apps 中的每个应用,从放置应用执行程序的目录中找到对应应用的 ELF 文件(这是一个 Linux 上的文件),并将数据读入内存。接着需要在 easy-fs 中创建一个同名文件并将 ELF 数据写入到这个文件中。这个过程相当于将 Linux 上的文件系统中的一个个文件复制到我们的 easy-fs 中

    • 第 89 - 91 行:将各个应用名称打出来。

  • 第 96 - 156 行:实现了 efs_test 测试用例efs_test 展示了 easy-fs 库的使用方法。

    • 第 97 - 105 行:第一步我们需要打开虚拟块设备。这里我们在 Linux 上创建文件 easy-fs-fuse/target/fs.img 来新建一个虚拟块设备,并将它的容量设置为 8192 个块4MiB 。在创建的时候需要将它的访问权限设置为可读可写。
    • 第 106 行:在虚拟块设备 block_file 上初始化 easy-fs 文件系统,这会将 block_file 用于放置 easy-fs 镜像的前 4096 个块上的数据覆盖,然后变成仅有一个根目录的初始文件系统
    • 第 107 行:第二步从块设备上打开文件系统。
    • 第 108 行:第三步获取根目录的 Inode
    • 第 109 - 110 行:通过 create 创建文件。
    • 第 111 - 113 行:通过 ls 列举根目录下的文件。
    • 第 114 行:通过 find 根据文件名索引文件
    • 第 116 行:通过 write_at 写文件,注意我们需要将读写在文件中开始的位置 offset 作为一个参数传递进去
    • 第 122 - 144 行:测试方法大概为每次清空文件 filea 的内容,向其中写入一个不同长度的随机数字字符串,然后再全部读取出来,验证和写入的内容一致。在刚开始测试的时候通过 clear 将文件内容清空。
    • 第 146 - 153 行:为具体的测试项。

用户态测试 efs 的实现:

首先efs需要一个实现了 BlockDevice Trait 的块设备用来装载文件系统,而我们选择的块设备呢就是linux下的一个文件模拟块设备。

通过以下三步便可以使用简易文件系统了:

  1. 打开虚拟块设备
  2. 从块设备上打开文件系统
  3. 获取根目录的 Inode
  4. 之后便可以进行各种操作了:创建文件、列举根目录下的文件、根据文件名索引文件、读写文件等。

简易文件系统小结:

在这里插入图片描述

  • 索引节点层Inode):管理索引节点(即文件控制块)数据结构,并实现文件创建/文件打开/文件读写等成员函数来向上支持文件操作相关的系统调用Inode 是放在内存中的记录文件索引节点信息的数据结构
  • 磁盘块管理器层(easyfs-EasyFileSystem):合并了核心数据结构和磁盘布局所形成的磁盘文件系统数据结构,以及基于这些结构的创建/打开文件系统的相关处理和磁盘块的分配和回收处理EasyFileSystem 知道每个布局区域所在的位置,磁盘块的分配和回收也需要经过它才能完成,它可以看成一个磁盘块管理器。)
  • 磁盘数据结构层:磁盘上的超级块、位图、索引节点、数据块、目录项等核心数据结构。(磁盘数据结构层代表了简易文件系统的磁盘存储数据的整体的格式,按照BlockDevice的类型进行磁盘数据的存储,每个块存入怎样的类型;将各段区域及前面磁盘数据结构结构整合起来就是简易文件系统
  • 块缓存层BlockCache):在内存中缓存磁盘块的数据,避免频繁读写磁盘(块缓存层主要是按照一定的调度策略将一些块从磁盘中拷贝到内存中方便直接使用
  • 磁盘块设备接口层BlockDevice):定义了以块大小为单位对磁盘块设备进行读写的trait接口。(文件系统主要负责文件的分配等功能,但内部具体对块的读写操作实现需要块设备提供

所以总结下来就是层层封装 😃

  • 从刚开始只能对磁盘进行读写操作的接口;
  • 再接着到将磁盘的一部分块缓存进内存中进行方便操作;
  • 之后通过efs的架构知道了哪些块内存的哪些东西,哪些块中有没有东西等信息,便可以将磁盘运用起来了,这些架构组成了简易文件系统,更加抽象来讲就是在一个块设备上按照我们新建的规则将其空间用了起来而这个新建的规则则是文件系统其能带来许多的便携好处;
  • 再其上需要有管理层将这个规则进行运行起来,按照这个规则进行文件的分配调度等功能。
  • 最后通过索引节点层将其封装成外部文件操作相关的系统调用交给别的程序来使用。

文件系统就是帮助我们对空间进行管理的一个子系统,用了它可以方便我们对空间的管理,按照文件系统的格式对空间进行管理。没有安装它空间依旧就是空间,可以通过直接访问等形式进行存储,但并没有文件系统那么方便,我们需要自己记住存在了哪里等信息。

【在内核中接入 easy-fs】

我们介绍如何easy-fs 文件系统接入内核中从而在内核中支持常规文件和目录。为此,在操作系统内核中需要有对接 easy-fs 文件系统的各种结构,它们可以分成这样几个层次:

  • 系统调用层:由于引入了常规文件这种文件类型,导致一些系统调用以及相关的内核机制需要进行一定的修改
  • 文件描述符层:常规文件对应的 OSInode 是文件的内核内部表示,因此需要为它实现 File Trait 从而能够可以将它放入到进程文件描述符表中并通过 sys_read/write 系统调用进行读写
  • 内核索引节点层:在内核中需要easy-fs 提供的 Inode 进一步封装成 OSInode ,以表示进程中一个打开的常规文件。由于有很多种不同的打开方式,因此在 OSInode 中要维护一些额外的信息。
  • easy-fs:我们在上一节已经介绍了 easy-fs 文件系统内部的层次划分。这里是站在内核的角度只需知道它接受一个块设备 BlockDevice ,并可以在上面打开文件系统 EasyFileSystem ,进而获取 Inode 核心数据结构,进行各种文件系统操作即可。
  • 块设备驱动层:针对内核所要运行在的 qemuk210 平台,我们需要将平台上的块设备驱动起来并实现 easy-fs 所需的 BlockDevice Trait ,这样 easy-fs 才能将该块设备用作 easy-fs 镜像的载体。

【文件简介 -> os/src/fs/mod.rs】

应用程序看到并被操作系统管理的 文件 (File) 就是一系列的字节组合。对文件具体内容的解析是应用程序的任务,操作系统对此不做任何干涉。

有了文件这样的抽象后,操作系统内核就可把能读写并持久存储的数据按文件来进行管理,并把文件分配给进程,让进程以很简洁的统一抽象接口 File 来读写数据

//! os/src/fs/mod.rs
mod stdio;
mod inode;

use crate::mm::UserBuffer;

/// The common abstraction of all IO resources
pub trait File : Send + Sync {
    fn readable(&self) -> bool;
    fn writable(&self) -> bool;
    fn read(&self, buf: UserBuffer) -> usize;
    fn write(&self, buf: UserBuffer) -> usize;
}

/// The stat of a inode
#[repr(C)]
#[derive(Debug)]
pub struct Stat {
    /// ID of device containing file
    pub dev: u64,
    /// inode number
    pub ino: u64,
    /// file type and mode
    pub mode: StatMode,
    /// number of hard links
    pub nlink: u32,
    /// unused pad
    pad: [u64; 7],
}

bitflags! {
    /// The mode of a inode
    /// whether a directory or a file
    pub struct StatMode: u32 {
        const NULL  = 0;
        /// directory
        const DIR   = 0o040000;
        /// ordinary regular file
        const FILE  = 0o100000;
    }
}    

pub use stdio::{Stdin, Stdout};
pub use inode::{OSInode, open_file, OpenFlags, list_apps};
  • 第 8 - 13 行:实现了文件操作的抽象,readwrite 的实现则与文件具体是哪种类型有关,它决定了数据如何被读取和写入。
    • 第 9 行:readable抽象接口,使得在 sys_read 的时候进行简单的访问权限检查。
    • 第 10 行:writable抽象接口,使得在 sys_write的时候进行简单的访问权限检查。
    • 第 11 行:read 指的是从文件中读取数据放到缓冲区中,最多将缓冲区填满(即读取缓冲区的长度那么多字节),并返回实际读取的字节数。
    • 第 12 行: write 指的是将缓冲区中的数据写入文件,最多将缓冲区中的数据全部写入,并返回直接写入的字节数。

【块设备驱动层 -> os/src/drivers/block/mod.rs -> os/Makefile -> os/src/config.rs -> os/src/mm/memory_set.rs】

drivers 子模块中的 block/mod.rs 中,我们可以找到内核访问的块设备实例 BLOCK_DEVICE

//! os/src/drivers/block/mod.rs
mod virtio_blk;

use lazy_static::*;
use alloc::sync::Arc;
use easy_fs::BlockDevice;
type BlockDeviceImpl = virtio_blk::VirtIOBlock;

lazy_static! {
    pub static ref BLOCK_DEVICE: Arc<dyn BlockDevice> = Arc::new(BlockDeviceImpl::new());
}

#[allow(unused)]
pub fn block_device_test() {
    let block_device = BLOCK_DEVICE.clone();
    let mut write_buffer = [0u8; 512];
    let mut read_buffer = [0u8; 512];
    for i in 0..512 {
        for byte in write_buffer.iter_mut() { *byte = i as u8; }
        block_device.write_block(i as usize, &write_buffer);
        block_device.read_block(i as usize, &mut read_buffer);
        assert_eq!(write_buffer, read_buffer);
    }
    println!("block device test passed!");
}
  • 第 7 行:在 qemu,我们使用 VirtIOBlock 访问 VirtIO 块设备
  • 第 9 - 11 行:我们将它全局实例化为 BLOCK_DEVICE ,使内核的其他模块可以访问。

在启动 Qemu 模拟器的时候,我们通过配置参数来添加一块 VirtIO 块设备

#! os/Makefile
FS_IMG := ../user/target/$(TARGET)/$(MODE)/fs.img

run: build
	@qemu-system-riscv64 \
		-machine virt \
		-nographic \
		-bios $(BOOTLOADER) \
		-device loader,file=$(KERNEL_BIN),addr=$(KERNEL_ENTRY_PA) \
		-drive file=$(FS_IMG),if=none,format=raw,id=x0 \
		-device virtio-blk-device,drive=x0,bus=virtio-mmio-bus.0
  • 第 2 行:我们指定了文件系统将应用打包为 easy-fs 镜像的路径。
  • 第 10 行:我们为虚拟机添加一块虚拟硬盘内容为我们之前通过 easy-fs-fuse 工具打包的包含应用 ELFeasy-fs 镜像,并命名为 x0
  • 第 11 行:我们将硬盘 x0 作为一个 VirtIO 总线中的一个块设备接入到虚拟机系统中virtio-mmio-bus.0 表示 VirtIO 总线通过 MMIO 进行控制,且该块设备在总线中的编号为 0 。由于设备驱动的开发过程比较琐碎,这里直接使用已有的 virtio-drivers crate

内存映射 I/O (MMIO, Memory-Mapped I/O) 指通过特定的物理内存地址来访问外设的设备寄存器每个外设的设备寄存器都分布在没有交集的一个或数个物理地址区间中,不同外设的设备寄存器所占的物理地址空间也不会产生交集,且这些外设物理地址区间也不会和RAM的物理内存所在的区间存在交集。从Qemu for RISC-V 64 平台的源码中可以找到 VirtIO 外设总线的 MMIO 物理地址区间为从 0x10001000 开头的 4KiB 。为了能够在内核中访问 VirtIO 外设总线,我们就必须在内核地址空间中对特定内存区域提前进行映射

config 子模块中我们硬编码 Qemu 上的 VirtIO 总线的 MMIO 地址区间(起始地址,长度)。

//! os/src/config.rs
pub const MMIO: &[(usize, usize)] = &[
    (0x10001000, 0x1000),
];

在创建内核地址空间的时候我们需要MMIO建立页表映射。这里我们进行的是透明的恒等映射,让内核可以兼容于直接访问物理地址的设备驱动库。

//! os/src/mm/memory_set.rs
use crate::config::MMIO;
impl MemorySet {
    /// Without kernel stacks.
    pub fn new_kernel() -> Self {
        ...
        for pair in MMIO {
            memory_set.push(
                MapArea::new(
                    (*pair).0.into(),
                    ((*pair).0 + (*pair).1).into(),
                    MapType::Identical,
                    MapPermission::R | MapPermission::W,
                ),
            None);
        }
        memory_set
    }
}

由于目前运行的平台环境为qemu,所以这里块设备通过在qemu上创建一个虚拟磁盘使用,内容为我们之前通过 easy-fs-fuse 工具打包的包含应用 ELFeasy-fs 镜像。块设备驱动使用的是virtio-drivers crate。具体内部代码在os/src/drivers/block/virtio_blk.rs中。由于VirtIO 总线通过 MMIO 进行控制,所以我们将MMIO进行硬编码,并在建立内核地址空间的时候给MMIO建立页表映射。

【内核索引节点层 -> os/src/fs/inode.rs】

进程中也存在着一个文件读写的当前偏移量,它也随着文件读写的进行而被不断更新。这些用户视角中的文件系统抽象特征需要内核来实现,与进程有很大的关系,而 easy-fs 文件系统不必涉及这些与进程结合紧密的属性。因此,我们需要easy-fs 提供的 Inode 加上上述信息,进一步封装为 OS 中的索引节点 OSInode

//! os/src/fs/inode.rs
use easy_fs::{
    EasyFileSystem,
    Inode,
};
use crate::drivers::BLOCK_DEVICE;
use crate::sync::UPSafeCell;
use alloc::sync::Arc;
use lazy_static::*;
use bitflags::*;
use alloc::vec::Vec;
use super::File;
use crate::mm::UserBuffer;

/// A wrapper around a filesystem inode
/// to implement File trait atop
pub struct OSInode {
    readable: bool,
    writable: bool,
    inner: UPSafeCell<OSInodeInner>,
}

/// The OS inode inner in 'UPSafeCell'
pub struct OSInodeInner {
    offset: usize,
    inode: Arc<Inode>,
}

impl OSInode {
    /// Construct an OS inode from a inode
    pub fn new(
        readable: bool,
        writable: bool,
        inode: Arc<Inode>,
    ) -> Self {
        Self {
            readable,
            writable,
            inner: unsafe { UPSafeCell::new(OSInodeInner {
                offset: 0,
                inode,
            })},
        }
    }
    /// Read all data inside a inode into vector
    pub fn read_all(&self) -> Vec<u8> {
        let mut inner = self.inner.exclusive_access();
        let mut buffer = [0u8; 512];
        let mut v: Vec<u8> = Vec::new();
        loop {
            let len = inner.inode.read_at(inner.offset, &mut buffer);
            if len == 0 {
                break;
            }
            inner.offset += len;
            v.extend_from_slice(&buffer[..len]);
        }
        v
    }
}

lazy_static! {
    /// The root of all inodes, or '/' in short
    pub static ref ROOT_INODE: Arc<Inode> = {
        let efs = EasyFileSystem::open(BLOCK_DEVICE.clone());
        Arc::new(EasyFileSystem::root_inode(&efs))
    };
}

/// List all files in the filesystems
pub fn list_apps() {
    println!("/**** APPS ****");
    for app in ROOT_INODE.ls() {
        println!("{}", app);
    }
    println!("**************/");
}

bitflags! {
    /// Flags for opening files
    pub struct OpenFlags: u32 {
        const RDONLY = 0;
        const WRONLY = 1 << 0;
        const RDWR = 1 << 1;
        const CREATE = 1 << 9;
        const TRUNC = 1 << 10;
    }
}

impl OpenFlags {
    /// Get the current read write permission on an inode
    /// does not check validity for simplicity
    /// returns (readable, writable)
    pub fn read_write(&self) -> (bool, bool) {
        if self.is_empty() {
            (true, false)
        } else if self.contains(Self::WRONLY) {
            (false, true)
        } else {
            (true, true)
        }
    }
}

/// Open a file by path
pub fn open_file(name: &str, flags: OpenFlags) -> Option<Arc<OSInode>> {
    let (readable, writable) = flags.read_write();
    if flags.contains(OpenFlags::CREATE) {
        if let Some(inode) = ROOT_INODE.find(name) {
            // clear size
            inode.clear();
            Some(Arc::new(OSInode::new(
                readable,
                writable,
                inode,
            )))
        } else {
            // create file
            ROOT_INODE.create(name)
                .map(|inode| {
                    Arc::new(OSInode::new(
                        readable,
                        writable,
                        inode,
                    ))
                })
        }
    } else {
        ROOT_INODE.find(name)
            .map(|inode| {
                if flags.contains(OpenFlags::TRUNC) {
                    inode.clear();
                }
                Arc::new(OSInode::new(
                    readable,
                    writable,
                    inode
                ))
            })
    }
}

impl File for OSInode {
    fn readable(&self) -> bool { self.readable }
    fn writable(&self) -> bool { self.writable }
    fn read(&self, mut buf: UserBuffer) -> usize {
        let mut inner = self.inner.exclusive_access();
        let mut total_read_size = 0usize;
        for slice in buf.buffers.iter_mut() {
            let read_size = inner.inode.read_at(inner.offset, *slice);
            if read_size == 0 {
                break;
            }
            inner.offset += read_size;
            total_read_size += read_size;
        }
        total_read_size
    }
    fn write(&self, buf: UserBuffer) -> usize {
        let mut inner = self.inner.exclusive_access();
        let mut total_write_size = 0usize;
        for slice in buf.buffers.iter() {
            let write_size = inner.inode.write_at(inner.offset, *slice);
            assert_eq!(write_size, slice.len());
            inner.offset += write_size;
            total_write_size += write_size;
        }
        total_write_size
    }
}
  • 第 17 - 21 行:OSInode 结构体表示进程中一个被打开的常规文件或目录

    • 第 18 行:readable表明该文件是否允许通过 sys_read 进行读写
    • 第 19 行:writable 表明该文件是否允许通过 sys_write 进行读写
    • 第 20 行:在 sys_read/write 期间被维护偏移量 offset 和它在 easy-fs 中的 Inode 则加上一把互斥锁放到 OSInodeInner
  • 第 24 - 27 行:OSInodeInner结构体定义了在 sys_read/write 期间需要知道的信息。包括读写offsetinode

  • 第 46 - 59 行:实现了read_all方法。将该数据块的数据512个字节全部读到一个向量中

  • 第 62 - 68 行:初始化easy-fseasy-fs 接入到我们的内核中。

    • 第 65 行:从块设备 BLOCK_DEVICE 上打开文件系统。
    • 第 66 行:从文件系统中获取根目录的 inode 。之后就可以使用根目录的 inode ROOT_INODE ,在内核中进行各种 easy-fs 的相关操作了
  • 第 71 - 77 行:实现了list_apps函数。可以在内核主函数 rust_main 中调用 list_apps 函数来列举文件系统中可用的应用的文件名

  • 第 79 - 88 行:在内核中也定义一份打开文件的标志 OpenFlags

  • 第 90 - 103 行:为OpenFlags实现了read_write 方法,可以根据标志的情况返回要打开的文件是否允许读写。这里假设标志自身一定合法。

  • 第 106 - 141 行:实现 open_file 内核函数,可根据文件名打开一个根目录下的文件

    • 第 107 行:查看文件的读写权限。
    • 第 108 行:查看flags 参数包含 CREATE 标志位则允许创建文件。
    • 第 109 - 116 行:如果文件已经存在,则清空文件的内容,我们将从 OpenFlags 解析得到的读写相关权限传入 OSInode 中。
    • 第 117 - 127 行:如果文件不存在,我们将从 OpenFlags 解析得到的读写相关权限传入 OSInode 的创建过程中。
    • 第 128 - 140 行:如果flags 参数不包含 CREATE 标志位,如果文件已经存在,则清空文件的内容,我们将从 OpenFlags 解析得到的读写相关权限传入 OSInode 中。
  • 第 143 - 170 行:为OSInode实现了File Trait。因为 OSInode 也是一种要放到进程文件描述符表中文件,并可通过 sys_read/write 系统调用进行读写操作,因此我们也需要为它实现 File Trait

    • 第 144 行:实现了readable抽象接口,从而sys_read 的时候进行简单的访问权限检查
    • 第 145 行:实现了writable抽象接口,从而sys_write 的时候进行简单的访问权限检查
    • 第 146 - 158 行:实现了read方法,遍历 UserBuffer 中的每个缓冲区片段,调用 Inode 写好的 read_at 接口向外读出数据。
    • 第 159 - 169 行:实现了write方法,遍历 UserBuffer 中的每个缓冲区片段,调用 Inode 写好的 write_at 接口向里面写入数据。

    注意 read_at/write_at 的起始位置是在 OSInode 中维护的 offset ,这个 offset 也随着遍历的进行被持续更新。在 read/write 的全程需要获取 OSInode 的互斥锁,保证两个进程无法同时访问同个文件。

【文件描述符层】

一个进程可以访问多个文件,所以在操作系统中需要有一个管理进程访问多个文件的结构,这就是 文件描述符表 (File Descriptor Table) ,其中的每个 文件描述符 (File Descriptor) 代表了一个特定读写属性的I/O资源

每个进程都带有一个线性的 文件描述符表 ,记录该进程请求内核打开并读写的那些文件集合。而 文件描述符 (File Descriptor) 则是一个非负整数,表示文件描述符表中一个打开的 文件描述符 所处的位置(可理解为数组下标)。进程通过文件描述符,可以在自身的文件描述符表中找到对应的文件记录信息,从而也就找到了对应的文件,并对文件进行读写。OSInode 也是一种要放到进程文件描述符表中文件,并可通过 sys_read/write 系统调用进行读写操作。

【文件描述符表 -> os/src/task/task.rs】

为了支持进程对文件的管理,我们需要在进程控制块中加入文件描述符表的相应字段

//! os/src/task/task.rs

use super::TaskContext;
use super::{pid_alloc, KernelStack, PidHandle};
use crate::config::TRAP_CONTEXT;
use crate::mm::{MemorySet, PhysPageNum, VirtAddr, KERNEL_SPACE};
use crate::sync::UPSafeCell;
use crate::trap::{trap_handler, TrapContext};
use alloc::sync::{Arc, Weak};
use alloc::vec::Vec;
use core::cell::RefMut;
use crate::fs::{File, Stdin, Stdout};
use alloc::string::String;
use crate::mm::translated_refmut;

/// Structure containing more process content
///
/// Store the contents that will change during operation
/// and are wrapped by UPSafeCell to provide mutual exclusion
pub struct TaskControlBlockInner {
    /// The physical page number of the frame where the trap context is placed
    pub trap_cx_ppn: PhysPageNum,
    /// Application data can only appear in areas
    /// where the application address space is lower than base_size
    pub base_size: usize,
    /// Save task context
    pub task_cx: TaskContext,
    /// Maintain the execution status of the current process
    pub task_status: TaskStatus,
    /// Application address space
    pub memory_set: MemorySet,
    /// Parent process of the current process.
    /// Weak will not affect the reference count of the parent
    pub parent: Option<Weak<TaskControlBlock>>,
    /// A vector containing TCBs of all child processes of the current process
    pub children: Vec<Arc<TaskControlBlock>>,
    /// It is set when active exit or execution error occurs
    pub exit_code: i32,
    pub fd_table: Vec<Option<Arc<dyn File + Send + Sync>>>,
}
  • 第 39 行:在TaskControlBlockInner中新增了**文件描述符表fd_table**字段。 fd_table 的类型包含多层嵌套:

    • Vec 的动态长度特性使得我们无需设置一个固定的文件描述符数量上限,我们可以更加灵活的使用内存,而不必操心内存管理问题;
    • Option 使得我们可以区分一个文件描述符当前是否空闲,当它是 None 的时候是空闲的,而 Some 则代表它已被占用;
    • Arc 首先提供了共享引用能力。后面我们会提到,可能会有多个进程共享同一个文件对它进行读写。此外被它包裹的内容会被放到内核堆而不是栈上,于是它便不需要在编译期有着确定的大小;
    • dyn 关键字表明 Arc 里面的类型实现了 File/Send/Sync 三个 Trait ,但是编译期无法知道它具体是哪个类型(可能是任何实现了 File Trait 的类型如 Stdin/Stdout ,故而它所占的空间大小自然也无法确定),需要等到运行时才能知道它的具体类型,对于一些抽象方法的调用也是在那个时候才能找到该类型实现的方法并跳转过去。

在编程语言中, 多态 (Polymorphism) 指的是在同一段代码中可以隐含多种不同类型的特征。在 Rust 中主要通过泛型和 Trait 来实现多态。

泛型是一种 编译期多态 (Static Polymorphism),在编译一个泛型函数的时候,编译器会对于所有可能用到的类型进行实例化并对应生成一个版本的汇编代码,在编译期就能知道选取哪个版本并确定函数地址,这可能会导致生成的二进制文件体积较大

Trait 对象(也即上面提到的 dyn 语法)是一种 运行时多态 (Dynamic Polymorphism),需要在运行时查一种类似于 C++ 中的 虚表 (Virtual Table) 才能找到实际类型对于抽象接口实现的函数地址并进行调用,这样会带来一定的运行时开销,但是更省空间且灵活。

【应用访问文件的内核机制实现】

应用程序在访问文件之前,首先需要完成对文件系统的初始化和加载。我们这里的选择是让操作系统直接完成。

  • 可以通过操作系统来完成
  • 可以让应用程序发出文件系统相关的系统调用(如 mount 等)来完成

应用程序如果要基于文件进行I/O访问,大致就会涉及如下一些系统调用:

  • 打开文件(sys_open):进程只有打开文件,操作系统才能返回一个可进行读写的文件描述符给进程,进程才能基于这个值来进行对应文件的读写
  • 关闭文件(sys_close):进程基于文件描述符关闭文件后,就不能再对文件进行读写操作了,这样可以在一定程度上保证对文件的合法访问
  • 读文件(sys_read):进程可以基于文件描述符来读文件内容到相应内存中
  • 写文件(sys_write):进程可以基于文件描述符来把相应内存内容写到文件中

【文件系统初始化】

为了使用 easy-fs 提供的抽象和服务,我们需要进行一些初始化操作才能成功将 easy-fs 接入到我们的内核中。

  1. 打开块设备。从本节前面可以看出,我们已经打开并可以访问装载有 easy-fs 文件系统镜像的块设备 BLOCK_DEVICE
  2. 从块设备 BLOCK_DEVICE 上打开文件系统
  3. 从文件系统中获取根目录的 inode

【基于文件来加载并执行应用 -> os/src/syscall/process.rs -> os/src/task/mod.rs】

在有了文件系统支持之后,我们sys_exec 所需的应用的 ELF 文件格式的数据就不再需要通过应用加载器从内核的数据段获取,而是从文件系统中获取,这样内核与应用的代码/数据就解耦了。

//! os/src/syscall/process.rs

pub fn sys_exec(path: *const u8) -> isize {
    let token = current_user_token();
    let path = translated_str(token, path);
    if let Some(app_inode) = open_file(path.as_str(), OpenFlags::RDONLY) {
        let all_data = app_inode.read_all();
        let task = current_task().unwrap();
        task.exec(all_data.as_slice());
        0
    } else {
        -1
    }
}
  • 第 4 行:获取当前应用的token
  • 第 5 行:它调用 translated_str 找到要执行的应用名
  • 第 6 行:调用 open_file 函数,以只读的方式在内核中打开应用文件并获取它对应的 OSInode
  • 第 7 行:通过 OSInode::read_all 将该文件的数据全部读到一个向量 all_data 中。
  • 第 8 - 9 行:从向量 all_data 中拿到应用中的 ELF 数据,当解析完毕并创建完应用地址空间后该向量将会被回收。

在内核中创建初始进程 initproc 也需要替换为基于文件系统的实现,找到对应应用的索引,获取内部的数据块,再开始创建应用。

//! os/src/task/mod.rs
mod context;
mod manager;
mod pid;
mod processor;
mod switch;
#[allow(clippy::module_inception)]
mod task;

use alloc::sync::Arc;
use lazy_static::*;
use manager::fetch_task;
use switch::__switch;
use crate::mm::VirtAddr;
use crate::mm::MapPermission;
use crate::config::PAGE_SIZE;
use crate::timer::get_time_us;
pub use crate::syscall::process::TaskInfo;
use crate::fs::{open_file, OpenFlags};
pub use task::{TaskControlBlock, TaskStatus};

pub use context::TaskContext;
pub use manager::add_task;
pub use pid::{pid_alloc, KernelStack, PidHandle};
pub use processor::{
    current_task, current_trap_cx, current_user_token, run_tasks, schedule, take_current_task,
};

lazy_static! {
    /// Creation of initial process
    ///
    /// the name "initproc" may be changed to any other app name like "usertests",
    /// but we have user_shell, so we don't need to change it.
    pub static ref INITPROC: Arc<TaskControlBlock> = Arc::new({
        let inode = open_file("ch6b_initproc", OpenFlags::RDONLY).unwrap();
        let v = inode.read_all();
        TaskControlBlock::new(v.as_slice())
    });
}

在文件系统:

在这里插入图片描述

  • App通过libc库实现关于文件或设备相关的操作。
  • libc库提供统一的API给上层应用提供支持。而其内部的实现是通过系统调用的接口将系统层提供的方法进行封装给上层应用。类似于将sys_open封装成open提供给App
  • 关于文件系统调用的接口则由进程控制块提供的,其内部OSInode将内存中的Inode进行了封装增加了系统相关的类似于当前偏移量等信息能更好的对上层提供支持。
  • 而对于进程控制块内部OSInode成员的方法支持则由vfs提供。

总体而言就是层层支持最终支持到了应用层。

最后我们再重新来看一下这幅图

在这里插入图片描述

File OSProcess OS的基础上增加了个文件系统,文件系统使得我们对于磁盘等存储设备的管理更加的优化了。而新增加的文件系统的好处在于:Process OS中我们需要将所有的应用都链接到内核中,随后在应用管理器中通过应用名进行索引来找到应用的 ELF 数据。这样做有一个缺点,就是会造成内核体积过度膨胀。在实现了文件系统之后,终于可以将这些应用打包到 easy-fs 镜像中放到磁盘中,当我们要执行应用的时候只需从文件系统中取出ELF 执行文件格式的应用并加载到内存中执行即可,这样就避免了前面章节的存储开销等问题。

文件系统帮助我们更好的管理我们外部的存储空间,可以通过文件系统将需要的文件放到内存进行使用,不用的再写回磁盘中。大大的减少了内存空间的使用。

个人理解:随着存储空间的增大,我们将一部分存储丢到外部的磁盘中,为了管理这个存储空间,我们给这个空间上了文件系统。应用是感受不到的,因为文件真正的交互操作部分都是在内核空间来完成了,但是应用感受到了存储空间的增大。通过文件系统的使用可以方便系统对于外部存储数据(目前这里是各种应用的源文件)的管理。个人认为文件系统是介于设备驱动与内核系统之间的一个层次,总体来说与内核共同组成一个系统,只不过总系统将这部分的管理交给了它。😃

在这里插入图片描述

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值