This commit is contained in:
Exusial 2021-08-13 21:55:55 +08:00
parent d9f628d44e
commit 9d21552db1
3 changed files with 495 additions and 1723 deletions

View File

@ -25,8 +25,6 @@
.. code-block:: console
$ git clone https://github.com/rcore-os/rCore-Tutorial-v3.git
$ cd rCore-Tutorial-v3
$ git checkout ch7
在 qemu 模拟器上运行本章代码:
@ -96,173 +94,22 @@
Shell: Process 2 exited with code 0
>>
本章代码树
-----------------------------------------
.. code-block::
:linenos:
:emphasize-lines: 50
./os/src
Rust 32 Files 2893 Lines
Assembly 3 Files 88 Lines
./easyfs/src
Rust 7 Files 908 Lines
├── 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(修改:新增 Qemu 和 K210 两个平台的块设备驱动依赖 crate)
│   ├── Makefile(修改:新增文件系统的构建流程)
│   └── src
│   ├── config.rs(修改:新增访问块设备所需的一些 MMIO 配置)
│   ├── console.rs
│   ├── drivers(修改:新增 Qemu 和 K210 两个平台的块设备驱动)
│   │   ├── 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
│   ├── sbi.rs
│   ├── syscall
│   │   ├── fs.rs(修改:新增 sys_open/sys_dup)
│   │   ├── 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.rs(新增)
│   ├── cmdline_args.rs(新增)
│   ├── exit.rs
│   ├── fantastic_text.rs
│   ├── filetest_simple.rs(新增:简单文件系统测例)
│   ├── 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 和 sys_dup)
本章代码导读
-----------------------------------------------------
本章涉及的代码量相对较多且与进程执行相关的管理还有直接的关系。其实我们是参考经典的UNIX基于索引的文件系统设计了一个简化的有一级目录并支持创建/打开/读写/关闭文件一系列操作的文件系统。这里简要介绍一下在内核中添加文件系统的大致开发过程。
本章涉及的代码量相对较多且与进程执行相关的管理还有直接的关系。其实我们是参考经典的UNIX基于索引的文件系统设计了一个简化的有一级目录并支持创建/打开/读写/关闭文件一系列操作的文件系统也就是说本章。本章采用的文件系统和ext4文件系统比较类似。其中也涉及到了inode这个概念。进入本章之后我们的测例文件一开始是存放在我们生成的“磁盘”上的需要我们实现磁盘的读写来进行操作了。我们实现了一个简单的nfs文件系统具体的结构将在下面的章节中说明。大家可以看一看我们本章对makefile文件的改动.
第一步是能够写出与文件访问相关的应用。这里是参考了Linux的创建/打开/读写/关闭文件的系统调用接口,力图实现一个 :ref:`简化版的文件系统模型 <fs-simplification>` 。在用户态我们只需要遵从相关系统调用的接口约定,在用户库里完成对应的封装即可。这一过程我们在前面的章节中已经重复过多次,读者应当对其比较熟悉。其中最为关键的是系统调用可以参考 :ref:`sys_open 语义介绍 <sys-open>` ,此外我们还给出了 :ref:`测例代码解读 <filetest-simple>`
.. code-block:: Makefile
QEMU = qemu-system-riscv64
QEMUOPTS = \
-nographic \
-smp $(CPUS) \
-machine virt \
-bios $(BOOTLOADER) \
-kernel kernel \
+ -drive file=$(U)/fs-copy.img,if=none,format=raw,id=x0 \ # 以 user/fs-copy.img 作为磁盘镜像
+ -device virtio-blk-device,drive=x0,bus=virtio-mmio-bus.0 # 虚拟 virtio 磁盘设备
第二步就是要实现 easyfs 文件系统了。由于 Rust 语言的特点,我们可以在用户态实现 easyfs 文件系统,并在用户态完成文件系统功能的基本测试并基本验证其实现正确性之后,就可以放心的将该模块嵌入到操作系统内核中。当然,有了文件系统的具体实现,还需要对上一章的操作系统内核进行扩展,实现与 easyfs 文件系统对接的接口,这样才可以让操作系统拥有一个简单可用的文件系统。从而,内核可以支持允许文件读写功能的更复杂的应用,在命令行参数机制的加持下,可以进一步提升整个系统的灵活性,让应用的开发和调试变得更为轻松。
需要注意一定要确保user测例生成的img文件在对应的位置否则会make run失败。
easyfs 文件系统的整体架构自下而上可分为五层。它的最底层就是对块设备的访问操作接口。在 ``easy-fs/src/block_dev.rs`` 中,可以看到 ``BlockDevice`` trait 代表了一个抽象块设备,该 trait 仅需求两个函数 ``read_block````write_block`` 分别代表将数据从块设备读到内存中的缓冲区中或者将数据从内存中的缓冲区写回到块设备中数据需要以块为单位进行读写。easy-fs 库的使用者需要负责为它们看到的实际的块设备具体实现 ``BlockDevice`` trait 并提供给 easy-fs 库的上层,这样的话 easy-fs 库的最底层就与一个具体的执行环境对接起来了。至于为什么块设备层位于 easy-fs 的最底层,是因为文件系统仅仅是在块设备上存储的结构稍微复杂一点的数据,但无论它的操作变换如何复杂,从块设备的角度终究可以被分解成若干次块读写。
尽管在最底层我们就已经有了块读写的能力,但从编程方便性和性能的角度,仅有块读写这么基础的底层接口是不足以实现如此复杂的文件系统的,虽然它已经被我们大幅简化过了。比如,将一个块的内容读到内存的缓冲区,对缓冲区进行修改,并尚未写回的时候,如果由于编程上的不小心再次将该块的内容读到另一个缓冲区,而不是使用已有的缓冲区,这将会造成不一致问题。此外还有可能增加很多不必要的块读写次数,大幅降低文件系统的性能。因此,通过程序自动而非程序员手动对块的缓冲区进行统一管理也就势在必行了,该机制被我们抽象为 easy-fs 自底向上的第二层,即块缓存层。在 ``easy-fs/src/block_cache.rs`` 中, ``BlockCache`` 代表一个被我们管理起来的块的缓冲区,它带有缓冲区本体以及块的编号等信息。当它被创建的时候,将触发一次 ``read_block`` 将数据从块设备读到它的缓冲区中。接下来只要它驻留在内存中,便可保证对于同一个块的所有操作都会直接在它的缓冲区中进行而无需额外的 ``read_block`` 。块缓存管理器 ``BlockManager`` 在内存中管理有限个 ``BlockCache`` 并实现了类似 FIFO 的缓存替换算法,当一个块缓存被换出的时候视情况可能调用 ``write_block`` 将缓冲区数据写回块设备。总之,块缓存层对上提供 ``get_block_cache`` 接口来屏蔽掉相关细节,从而可以透明的读写一个块。
有了块缓存我们就可以在内存中方便地处理easyfs文件系统在磁盘上的各种数据了这就是第三层文件系统的磁盘数据结构。easyfs文件系统中的所有需要持久保存的数据都会放到磁盘上这包括了管理这个文件系统的 **超级块 (Super Block)**,管理空闲磁盘块的 **索引节点位图区****数据块位图区** ,以及管理文件的 **索引节点区** 和 放置文件数据的 **数据块区** 组成。
easyfs文件系统中管理这些磁盘数据的控制逻辑主要集中在 **磁盘块管理器** 中,这是文件系统的第四层。对于文件系统管理而言,其核心是 ``EasyFileSystem`` 数据结构及其关键成员函数:
- EasyFileSystem.create创建文件系统
- EasyFileSystem.open打开文件系统
- EasyFileSystem.alloc_inode分配inode dealloc_inode未实现所以还不能删除文件
- EasyFileSystem.alloc_data分配数据块
- EasyFileSystem.dealloc_data回收数据块
对于单个文件的管理和读写的控制逻辑主要是 **索引节点** 来完成,这是文件系统的第五层,其核心是 ``Inode`` 数据结构及其关键成员函数:
- Inode.new在磁盘上的文件系统中创建一个inode
- Inode.find根据文件名查找对应的磁盘上的inode
- Inode.create在根目录下创建一个文件
- Inode.read_at根据inode找到文件数据所在的磁盘数据块并读到内存中
- Inode.write_at根据inode找到文件数据所在的磁盘数据块把内存中数据写入到磁盘数据块中
上述五层就构成了easyfs文件系统的整个内容。我们可以把easyfs文件系统看成是一个库被应用程序调用。而 ``easy-fs-fuse`` 这个应用就通过调用easyfs文件系统库中各种函数并用Linux上的文件模拟了一个块设备就可以在这个模拟的块设备上创建了一个easyfs文件系统。
第三步我们需要把easyfs文件系统加入到我们的操作系统内核中。这还需要做两件事情第一件是在Qemu模拟的 ``virtio`` 块设备上实现块设备驱动程序 ``os/src/drivers/block/virtio_blk.rs`` 。由于我们可以直接使用 ``virtio-drivers`` crate中的块设备驱动所以只要提供这个块设备驱动所需要的内存申请与释放以及虚实地址转换的4个函数就可以了。而我们之前操作系统中的虚存管理实现中以及有这些函数导致块设备驱动程序很简单具体实现细节都被 ``virtio-drivers`` crate封装好了。
第二件事情是把文件访问相关的系统调用与easyfs文件系统连接起来。在easfs文件系统中是没有进程的概念的。而进程是程序运行过程中访问资源的管理实体这就要对 ``easy-fs`` crate 提供的 ``Inode`` 结构进一步封装,形成 ``OSInode`` 结构,以表示进程中一个打开的常规文件。对于应用程序而言,它理解的磁盘数据是常规的文件和目录,不是 ``OSInode`` 这样相对复杂的结构。其实常规文件对应的 OSInode 是文件在操作系统内核中的内部表示,因此需要为它实现 File Trait 从而能够可以将它放入到进程文件描述符表中,并通过 sys_read/write 系统调用进行读写。这样就建立了文件与 ``OSInode`` 的对应关系,并通过上面描述的三个步骤完成了包含文件系统的操作系统内核,并能给应用提供基于文件的系统调用服务。
完成包含文件系统的操作系统内核后我们在shell程序和内核中支持命令行参数的解析和传递这样可以让应用根据灵活地通过命令行参数来动态地表示要操作的文件。这需要扩展对应的系统调用 ``sys_exec`` ,主要的改动就是在创建新进程时,把命令行参数压入用户栈中,这样应用程序在执行时就可以从用户栈中获取到命令行的参数值了。
在上一章,我们提到了把标准输出设备在文件描述符表中的文件描述符的值规定为 1 ,用 Stdin 表示;把标准输入设备在文件描述符表中的文件描述符的值规定为 0用 stdout 表示 。另外,还有一条文件描述符相关的重要规则:即进程打开一个文件的时候,内核总是会将文件分配到该进程文件描述符表中编号 最小的 空闲位置。利用这些约定,只实现新的系统调用 ``sys_dup`` 完成对文件描述符的复制,就可以巧妙地实现标准 I/O 重定向功能了。
具体思路是,在某应用进程执行之前,父进程(比如 user_shell进程要对子应用进程的文件描述符表进行某种替换。以输出为例父进程在创建子进程前提前打开一个常规文件 A然后 ``fork`` 子进程,在子进程的最初执行中,通过 ``sys_close`` 关闭 Stdout 文件描述符,用 ``sys_dup`` 复制常规文件 A 的文件描述符,这样 Stdout 文件描述符实际上指向的就是常规文件A了这时再通过 ``sys_close`` 关闭常规文件 A 的文件描述符。至此,常规文件 A 替换掉了应用文件描述符表位置 1 处的标准输出文件,这就完成了所谓的 **重定向** ,即完成了执行新应用前的准备工作。
接下来是子进程调用 ``sys_exec`` 系统调用,创建并开始执行新子应用进程。在重定向之后,新的子应用进程认为自己输出到 fd=1 的标准输出文件,但实际上是输出到父进程(比如 user_shell进程指定的文件A中。文件这一抽象概念透明化了文件、I/O设备之间的差异因为在进程看来无论是标准输出还是常规文件都是一种文件可以通过同样的接口来读写。这就是文件的强大之处。
我们OS的读写文件操作均在内核态进行由于不确定读写磁盘的结束时间这意味着我们需要新的中断方式——外部中断来提醒OS读写结束了。而要在内核态引入中断意味着我们不得不短暂开启在内核态的嵌套中断。一旦OS打开了文件那么我们就可以获得文件对应的fd了(实际上lab6中我们做了类似的事情就可以使用sys_write/sys_read对文件进行读写操作。

View File

@ -14,7 +14,7 @@
在操作系统的用户看来,常规文件是保存在持久存储设备上的一个字节序列,每个常规文件都有一个 **文件名** (Filename) ,用户需要通过它来区分不同的常规文件。方便起见,在下面的描述中,“文件”有可能指的是常规文件、目录,也可能是之前提到的若干种进程可以读写的 标准输出、标准输入、管道等I/O 资源,请读者自行根据上下文判断取哪种含义。
在 Linux 系统上, ``stat`` 工具可以获取文件的一些信息。下面以我们项目中的一个源代码文件 ``os/src/main.rs`` 为例:
在 Linux 系统上, ``stat`` 工具可以获取文件的一些信息。下面以我们项目中的一个源代码文件 ``os/main.c`` 为例:
.. code-block:: console
@ -29,9 +29,9 @@
Change: 2021-02-28 23:32:50.133927136 +0800
Birth: -
``stat`` 工具展示了 ``main.rs`` 的如下信息:
``stat`` 工具展示了 ``main.c`` 的如下信息:
- File 表明它的文件名为 ``main.rs``
- File 表明它的文件名为 ``main.c``
- Size 表明它的字节大小为 940 字节。
- Blocks 表明它占据 8 个 **块** (Block) 来存储。在文件系统中,文件的数据以块为单位进行存储,在 IO Block 可以看出在 Ubuntu 系统中每个块的大小为 4096 字节。
- regular file 表明这个文件是一个常规文件。事实上,其他类型的文件也可以通过文件名来进行访问。
@ -41,24 +41,7 @@
- Uid 给出该文件的所属的用户 ID Gid 给出该文件所属的用户组 ID 。Access 的其中一种表示是一个长度为 10 的字符串(这里是 ``-rw-r--r--`` ),其中第 1 位给出该文件的类型,这个文件是一个常规文件,因此这第 1 位为 ``-`` 。后面的 9 位可以分为三组,分别表示该文件的所有者/在该文件所属的用户组内的其他用户以及剩下的所有用户能够读取/写入/将该文件作为一个可执行文件来执行。
- Access/Modify 分别给出该文件的最近一次访问/最近一次修改时间。
如果我们使用 ``stat`` 工具查看我们构建的一个能在我们的内核上执行的应用 ELF 可执行文件:
.. code-block:: console
$ cd user/target/riscv64gc-unknown-none-elf/release/
$ stat user_shell
File: user_shell
Size: 85712 Blocks: 168 IO Block: 4096 regular file
Device: 801h/2049d Inode: 1460936 Links: 2
Access: (0755/-rwxr-xr-x) Uid: ( 1000/ oslab) Gid: ( 1000/ oslab)
Access: 2021-03-01 11:21:34.785309066 +0800
Modify: 2021-03-01 11:21:32.829332116 +0800
Change: 2021-03-01 11:21:32.833332069 +0800
Birth: -
从中可以看出我们构建的应用体积大概在数十 KiB 量级。它的 Access 指出所有用户均可将其作为一个可执行文件在当前 OS 中加载并执行。然而这仅仅是能够通过权限检查而已,这个应用只有在我们自己的内核上才能真正被加载运行。
用户常常通过文件的 **拓展名** (Filename extension) 来推断该文件的用途,如 ``main.rs`` 的拓展名是 ``.rs`` ,我们由此知道它是一个 Rust 源代码文件。但从内核的角度来看,它会将所有文件无差别的看成一个字节序列,文件内容的结构和含义则是交给对应的应用进行解析。
用户常常通过文件的 **拓展名** (Filename extension) 来推断该文件的用途,如 ``main.c`` 的拓展名是 ``.c`` ,我们由此知道它是一个 C 代码文件。但从内核的角度来看,它会将所有文件无差别的看成一个字节序列,文件内容的结构和含义则是交给对应的应用进行解析。
目录
+++++++++++++++++++++++++++++++++++++++++++++++++
@ -137,14 +120,14 @@ Blocks 给出 ``os`` 目录也占用 8 个块进行存储。实际上目录也
在读写一个常规文件之前,应用首先需要通过内核提供的 ``sys_open`` 系统调用让该文件在进程的文件描述符表中占一项,并得到操作系统的返回值--文件描述符,即文件关联的表项在文件描述表中的索引值:
.. code-block:: rust
.. code-block:: c
/// 功能:打开一个常规文件,并返回可以访问它的文件描述符。
/// 参数path 描述要打开的文件的文件名(简单起见,文件系统不需要支持目录,所有的文件都放在根目录 / 下),
/// flags 描述打开文件的标志,具体含义下面给出。
/// 返回值:如果出现了错误则返回 -1否则返回打开常规文件的文件描述符。可能的错误原因是文件不存在。
/// syscall ID56
pub fn sys_open(path: *const u8, flags: u32) -> isize;
int open(int dirfd, char* path, unsigned int flags, unsigned int mode);
目前我们的内核支持以下几种标志(多种不同标志可能共存):
@ -152,99 +135,14 @@ Blocks 给出 ``os`` 目录也占用 8 个块进行存储。实际上目录也
- 如果 ``flags`` 第 0 位被设置0x001表示以只写模式 *WRONLY* 打开;
- 如果 ``flags`` 第 1 位被设置0x002表示既可读又可写 *RDWR*
- 如果 ``flags`` 第 9 位被设置0x200表示允许创建文件 *CREATE* ,在找不到该文件的时候应创建文件;如果该文件已经存在则应该将该文件的大小归零;
- 如果 ``flags`` 第 10 位被设置0x400则在打开文件的时候应该清空文件的内容并将该文件的大小归零也即 *TRUNC*
- 如果 ``flags`` 第 10 位被设置0x400则在打开文件的时候应该清空文件的内容并将该文件的大小归零也即 *TRUNC*我们本章不涉及这个flags。
注意 ``flags`` 里面的权限设置只能控制进程对本次打开的文件的访问。一般情况下,在打开文件的时候首先需要经过文件系统的权限检查,比如一个文件自身不允许写入,那么进程自然也就不能以 *WRONLY**RDWR* 标志打开文件。但在我们简化版的文件系统中文件不进行权限设置,这一步就可以绕过。
在用户库 ``user_lib`` 中,我们将该系统调用封装为 ``open`` 接口:
.. code-block:: rust
// user/src/lib.rs
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_open(path, flags.bits)
}
借助 ``bitflags!`` 宏我们将一个 ``u32`` 的 flags 包装为一个 ``OpenFlags`` 结构体更易使用,它的 ``bits`` 字段可以将自身转回 ``u32`` ,它也会被传给 ``sys_open``
.. code-block:: rust
// user/src/syscall.rs
const SYSCALL_OPEN: usize = 56;
pub fn sys_open(path: &str, flags: u32) -> isize {
syscall(SYSCALL_OPEN, [path.as_ptr() as usize, flags as usize, 0])
}
我们在 ``sys_open`` 传给内核的两个参数只有待打开文件的文件名字符串的起始地址(和之前一样,我们需要保证该字符串以 ``\0`` 结尾)还有标志位。由于每个通用寄存器为 64 位,我们需要先将 ``u32````flags`` 转换为 ``usize``
文件的顺序读写
++++++++++++++++++++++++++++++++++++++++++++++++++
在打开一个文件之后,我们就可以用之前的 ``sys_read/sys_write`` 两个系统调用来对它进行读写了。需要注意的是,常规文件的读写模式和之前介绍过的几种文件有所不同。标准输入输出和匿名管道都属于一种流式读写,而常规文件则是顺序读写和随机读写的结合。由于常规文件可以看成一段字节序列,我们应该能够随意读写它的任一段区间的数据,即随机读写。然而用户仅仅通过 ``sys_read/sys_write`` 两个系统调用不能做到这一点。
事实上,进程为每个它打开的常规文件维护了一个偏移量,在刚打开时初始值一般为 0 字节。当 ``sys_read/sys_write`` 的时候,将会从文件字节序列偏移量的位置开始 **顺序** 把数据读到应用缓冲区/从应用缓冲区写入数据。操作完成之后,偏移量向后移动读取/写入的实际字节数。这意味着,下次 ``sys_read/sys_write`` 将会从刚刚读取/写入之后的位置继续。如果仅使用 ``sys_read/sys_write`` 的话,则只能从头到尾顺序对文件进行读写。当我们需要从头开始重新写入或读取的话,只能通过 ``sys_close`` 关闭并重新打开文件来将偏移量重置为 0。为了解决这种问题有另一个系统调用 ``sys_lseek`` 可以调整进程打开的一个常规文件的偏移量,这样便能对文件进行随机读写。在本教程中并未实现这个系统调用,因为顺序文件读写就已经足够了。顺带一提,在文件系统的底层实现中都是对文件进行随机读写的。
在打开一个文件获得其fd之后我们就可以用之前的 ``sys_read/sys_write`` 两个系统调用来对它进行读写了。需要注意的是,常规文件的读写模式和之前介绍过的几种文件有所不同。标准输入输出和匿名管道都属于一种流式读写,而常规文件则是顺序读写和随机读写的结合。由于常规文件可以看成一段字节序列,我们应该能够随意读写它的任一段区间的数据,即随机读写。然而用户仅仅通过 ``sys_read/sys_write`` 两个系统调用不能做到这一点。大家应该使用C时应该知道读写文件都是有一个偏移量的即下一次读写的起始位置是由上一次读写的结束位置决定的。我们可以使用lseek函数来改变这个偏移的位置本章不需实现。顺带一提在文件系统的底层实现中都是对文件进行随机读写的。
.. _filetest-simple:
下面我们从本章的测试用例 ``filetest_simple`` 来介绍文件系统接口的使用方法:
.. code-block:: rust
:linenos:
// user/src/bin/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
}
- 第 20~25 行,我们打开文件 ``filea`` ,向其中写入字符串 ``Hello, world!`` 而后关闭文件。这里需要注意的是我们需要为字符串字面量手动加上 ``\0`` 作为结尾。在打开文件时 *CREATE* 标志使得如果 ``filea`` 原本不存在,文件系统会自动创建一个同名文件,如果已经存在的话则会清空它的内容。而 *WRONLY* 使得此次只能写入该文件而不能读取。
- 第 27~32 行,我们以只读 *RDONLY* 的方式将文件 ``filea`` 的内容读取到缓冲区 ``buffer`` 中。注意我们很清楚 ``filea`` 的总大小不超过缓冲区的大小,因此通过单次 ``read`` 即可将 ``filea`` 的内容全部读取出来。而更常见的情况是需要进行多次 ``read`` 直到它的返回值为 0 才能确认文件的内容已被读取完毕了。
- 最后的第 34~38 行我们确认从 ``filea`` 读取到的内容和之前写入的一致,则测试通过。

File diff suppressed because it is too large Load Diff