Skip to content

Commit

Permalink
Lab filesystem (#23)
Browse files Browse the repository at this point in the history
* 增加DragonOS教案配置

* 虚拟文件系统

---------

Co-authored-by: Yuzizhou <[email protected]>
  • Loading branch information
Yuzizhou999 and Yuzizhou authored Sep 11, 2023
1 parent ab4559f commit 884416a
Show file tree
Hide file tree
Showing 3 changed files with 292 additions and 0 deletions.
Binary file added docs/.vuepress/public/DragonOS.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/.vuepress/public/vfs_archi_design.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
292 changes: 292 additions & 0 deletions docs/DragonOS/Lab/Lab1.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,292 @@
---
sidebar: auto
---

# 文件系统

## 本章导读

本章意在介绍文件系统相关知识,包括虚拟文件系统、ramfs以及如何在用户程序对自己写的文件系统进行测试

## DragonOS文件系统的架构设计

首先,我们来总览一下DragonOS文件系统的架构设计。相关的机制主要包括以下几个部分:

>- 系统调用接口
>
>- 虚拟文件系统
>
>>文件抽象(File)
>>
>>挂载文件系统(MountFS)
>
>- 具体的文件系统
如图所示:

![图片1](../.vuepress/public/../../../.vuepress/public/DragonOS.png '文件系统架构设计')

其中我们可以看到,中间部分作为接口对多个并行的物理文件系统实例(每一个都叫做文件系统的实现)提供支持。这就是虚拟文件系统。

## 虚拟文件系统

`虚拟文件系统`(Virtual File System,缩写为`VFS`)是计算机操作系统中的一个抽象层,它提供了文件系统的标准化接口,使得应用程序可以通过统一的方式访问各种不同类型的文件系统。

其设计目的是将文件系统的实现细节与应用程序分离,使得应用程序可以独立于底层文件系统的具体实现方式。它定义了一组通用的文件操作接口,如打开文件、关闭文件、读取文件、写入文件等,应用程序可以通过这些接口来访问和操作文件,而无需关心文件系统的底层细节。例如,一个应用程序可以通过虚拟文件系统接口打开一个文件,而不需要关心这个文件是存储在本地磁盘上的普通文件,还是位于远程服务器上的网络文件。虚拟文件系统会将这些不同类型的文件系统映射到一个统一的文件层次结构中,使得应用程序可以以相同的方式访问这些文件。

*DragonOS*中,VFS作为适配器,遮住了具体文件系统之间的差异,对外提供统一的文件操作接口抽象。

VFS是DragonOS文件系统的核心,它提供了一套统一的文件系统接口,使得DragonOS可以支持多种不同的文件系统。VFS的主要功能包括:

>提供统一的文件系统接口
>
>提供文件系统的挂载和卸载机制(MountFS)
>
>提供文件抽象(File)
>
>提供文件系统的抽象(FileSystem)
>
>提供IndexNode抽象
## VFS的架构设计

![图片2](../.vuepress/public/../../../.vuepress/public/vfs_archi_design.png 'vfs架构设计')

### File

`File`结构体是VFS中最基本的抽象,它代表了一个打开的文件。每当进程打开了一个文件,就会创建一个File结构体,用于维护该文件的状态信息。

```rust
/// @brief 抽象文件结构体
pub struct File {
inode: Arc<dyn IndexNode>,
/// 对于文件,表示字节偏移量;对于文件夹,表示当前操作的子目录项偏移量
offset: usize,
/// 文件的打开模式
mode: FileMode,
/// 文件类型
file_type: FileType,
/// readdir时候用的,暂存的本次循环中,所有子目录项的名字的数组
readdir_subdirs_name: Vec<String>,
pub private_data: FilePrivateData,
}
```

### Traits

对于每个具体文件系统,都需要实现以下的trait:

FileSystem:表明某个struct是一个文件系统

```rust
pub trait FileSystem: Any + Sync + Send + Debug {
/// @brief 获取当前文件系统的root inode的指针
fn root_inode(&self) -> Arc<dyn IndexNode>;

/// @brief 获取当前文件系统的信息
fn info(&self) -> FsInfo;

/// @brief 本函数用于实现动态转换。
/// 具体的文件系统在实现本函数时,最简单的方式就是:直接返回self
fn as_any_ref(&self) -> &dyn Any;
}
```

`IndexNode`: 表明某个struct是一个索引节点

```rust
pub trait IndexNode: Any + Sync + Send + Debug {
/// 以下陈列部分函数

/// @brief 打开文件
/// @return 成功:Ok()
/// 失败:Err(错误码)
fn open(&self, _data: &mut FilePrivateData, _mode: &FileMode) -> Result<(), SystemError> {
// 若文件系统没有实现此方法,则返回“不支持”
return Err(SystemError::EOPNOTSUPP_OR_ENOTSUP);
}

/// @brief 关闭文件
/// @return 成功:Ok()
/// 失败:Err(错误码)
fn close(&self, _data: &mut FilePrivateData) -> Result<(), SystemError> {
// 若文件系统没有实现此方法,则返回“不支持”
return Err(SystemError::EOPNOTSUPP_OR_ENOTSUP);
}

/// @brief 在inode的指定偏移量开始,读取指定大小的数据
/// @param offset 起始位置在Inode中的偏移量
/// @param len 要读取的字节数
/// @param buf 缓冲区. 请注意,必须满足@buf.len()>=@len
/// @param _data 各文件系统系统所需私有信息
/// @return 成功:Ok(读取的字节数)
/// 失败:Err(Posix错误码)
fn read_at(
&self,
offset: usize,
len: usize,
buf: &mut [u8],
_data: &mut FilePrivateData,
) -> Result<usize, SystemError>;

/// @brief 在inode的指定偏移量开始,写入指定大小的数据(从buf的第0byte开始写入)
/// @param offset 起始位置在Inode中的偏移量
/// @param len 要写入的字节数
/// @param buf 缓冲区. 请注意,必须满足@buf.len()>=@len
/// @param _data 各文件系统系统所需私有信息
/// @return 成功:Ok(写入的字节数)
/// 失败:Err(Posix错误码)
fn write_at(
&self,
offset: usize,
len: usize,
buf: &[u8],
_data: &mut FilePrivateData,
) -> Result<usize, SystemError>;

/// @brief 在当前目录下创建一个新的inode,并传入一个简单的data字段,方便进行初始化。
/// @param name 目录项的名字
/// @param file_type 文件类型
/// @param mode 权限
/// @param data 用于初始化该inode的数据。(为0则表示忽略此字段)对于不同的文件系统来说,代表的含义可能不同。
/// @return 创建成功:返回Ok(新的inode的Arc指针)
/// @return 创建失败:返回Err(错误码)
fn create_with_data(
&self,
_name: &str,
_file_type: FileType,
_mode: u32,
_data: usize,
) -> Result<Arc<dyn IndexNode>, SystemError> {
// 若文件系统没有实现此方法,则返回“不支持”
return Err(SystemError::EOPNOTSUPP_OR_ENOTSUP);
}

...
}
```

一般情况下,FileSystem和IndexNode是一对一的关系,也就是,一个文件系统对应一种IndexNode。但是,对于某些特殊的文件系统,比如DevFS,根据不同的设备类型,会有不同的IndexNode,因此,FileSystem和IndexNode是一对多的关系。

### MountFS

`挂载文件系统`是将一个磁盘分区或者远程的存储设备连接到计算机文件系统中的一个过程。当我们需要访问一个磁盘分区或者远程存储设备时,首先要确定其所属的文件系统类型,然后再将它挂载到指定的目录下。这样,在访问该目录时就可以直接读取或写入外部设备上的数据了。

```rust
pub struct MountFS {
// MountFS内部的文件系统
inner_filesystem: Arc<dyn FileSystem>,
/// 用来存储InodeID->挂载点的MountFS的B树
mountpoints: SpinLock<BTreeMap<InodeId, Arc<MountFS>>>,
/// 当前文件系统挂载到的那个挂载点的Inode
self_mountpoint: Option<Arc<MountFSInode>>,
/// 指向当前MountFS的弱引用
self_ref: Weak<MountFS>,
}
```

```rust
/// @brief MountFS的Index Node
/// 注意,这个IndexNode只是一个中间层。它的目的是将具体文件系统的Inode与挂载机制连接在一起。
pub struct MountFSInode {
/// 当前挂载点对应到具体的文件系统的Inode
inner_inode: Arc<dyn IndexNode>,
/// 当前Inode对应的MountFS
mount_fs: Arc<MountFS>,
/// 指向自身的弱引用
self_ref: Weak<MountFSInode>,
}
```

虽然其实现了FileSystem和IndexNode这两个trait,但它并不是一个“文件系统”,而是一种机制,用于将不同的文件系统挂载到同一个文件系统树上。所有的文件系统要挂载到文件系统树上,都需要通过`MountFS`来完成。也就是说,挂载树上的每个文件系统结构体的外面,都套了一层MountFS结构体。

对于大部分的操作,MountFS都是直接转发给具体的文件系统,而不做任何处理。同时,为了支持跨文件系统的操作,比如在目录树上查找,每次`lookup`操作或者是`find`操作,都会通过`MountFSInode`的对应方法,判断当前inode是否为挂载点,并对挂载点进行特殊处理。如果发现操作跨越了具体文件系统的边界,MountFS就会将操作转发给下一个文件系统,并执行Inode替换。这个功能的实现,也是通过在普通的Inode结构体外面,套一层MountFSInode结构体来实现的。

## 实验内容

ramfs的基本功能在/kernel/src/filesystem/ramfs/mod.rs中,实现过程中请参考文件../vfs/mod.rs以及已实现函数的相关代码和注释。在实现基本的文件操作之前,需要先实现创建文件结构的辅助函数:create_with_data。其用于创建文件时,在父目录下创建带初始化数据的inode。

>练习1:实现位于/kernel/src/filesystem/ramfs/mod.rs中的 create_with_data。
```rust
// 该函数用于在当前目录下创建一个新的inode,并传入一个简单的data字段,方便进行初始化。
// 需要判断当前inode是否是文件且是否重名,接着创建inode进行初始化。
fn create_with_data(
&self,
name: &str,
file_type: FileType,
mode: u32,
data: usize,
) -> Result<Arc<dyn IndexNode>, SystemError> {
// 获取当前inode
let mut inode = self.0.lock();

// LAB TODO BEGIN

// LAB TODO END

// 初始化inode的自引用的weak指针
result.0.lock().self_ref = Arc::downgrade(&result);

// 将子inode插入父inode的B树中
inode.children.insert(String::from(name), result.clone());

return Ok(result);
}
```

文件读写是文件系统的基本功能,ramfs的读写操作会将文件数据块中内容读入内存缓冲区,或将缓冲区内容写入对应文件数据块。read_at和 write_at两个函数分别用于以一定偏移量读取和写入一段长度的数据,并且返回实际的读写字节长度 (读取不能超过文件大小)。

>练习2:实现位于/kernel/src/filesystem/ramfs/mod.rs中的 read_at和 write_at。
```rust
// 该函数用于实现对文件以一定偏移量读取一段长度的数据,并且返回实际的读字节长度。
// 首先检查当前inode是否为一个文件,然后计算读文件的偏移量,最后拷贝数据(copy_from_slice)。
fn read_at(
&self,
offset: usize,
len: usize,
buf: &mut [u8],
_data: &mut FilePrivateData,
) -> Result<usize, SystemError> {
if buf.len() < len {
return Err(SystemError::EINVAL);
}
// 加锁
let inode: SpinLockGuard<RamFSInode> = self.0.lock();

// LAB TODO BEGIN

// LAB TODO END

return Ok(src.len());
}

// 该函数用于实现对文件以一定偏移量写一段长度的数据,并且返回实际的写字节长度。
// 首先检查当前inode是否为一个文件,如果文件大小比原来的大,那就resize这个数组,最后将数据写入(copy_from_slice)。
fn write_at(
&self,
offset: usize,
len: usize,
buf: &[u8],
_data: &mut FilePrivateData,
) -> Result<usize, SystemError> {
if buf.len() < len {
return Err(SystemError::EINVAL);
}

// 加锁
let mut inode: SpinLockGuard<RamFSInode> = self.0.lock();

// 检查当前inode是否为一个文件夹,如果是的话,就返回错误
if inode.metadata.file_type == FileType::Dir {
return Err(SystemError::EISDIR);
}

// LAB TODO BEGIN

// LAB TODO END

return Ok(len);
}
```

0 comments on commit 884416a

Please sign in to comment.