使用文件系统

打包磁盘文件

首先我们将所有编译出来的用户程序放在 usr/build/riscv64/rust 文件夹下,并将 usr/build/riscv64 文件夹里面的内容使用 rcore-fs-fuse工具 打包成一个磁盘文件,由于选用不同的文件系统磁盘文件的布局会不同,我们这里选用一个简单的文件系统 SimpleFileSystem(简称SFS)。

磁盘文件布局为:里面只有一个 rust 文件夹,里面放着若干用户程序。

我们写一个 Makefile 来完成编译及打包操作:

  1. # usr/Makefile
  2. target := riscv64imac-unknown-none-elf
  3. mode := debug
  4. rust_src_dir := rust/src/bin
  5. rust_target_dir := rust/target/$(target)/$(mode)
  6. rust_srcs := $(wildcard $(rust_src_dir)/*.rs)
  7. rust_targets := $(patsubst $(rust_src_dir)/%.rs, $(rust_target_dir)/%, $(rust_srcs))
  8. out_dir := build/riscv64
  9. sfsimg := build/riscv64.img
  10. .PHONY: rcore-fs-fuse rust user_img clean
  11. rcore-fs-fuse:
  12. ifeq ($(shell which rcore-fs-fuse),)
  13. @echo Installing rcore-fs-fuse
  14. @cargo install rcore-fs-fuse --git https://github.com/rcore-os/rcore-fs --rev 7f5eeac
  15. endif
  16. rust:
  17. @cd rust && cargo build
  18. @echo targets includes $(rust_targets)
  19. @rm -rf $(out_dir)/rust && mkdir -p $(out_dir)/rust
  20. @rm -f $(sfsimg)
  21. @cp $(rust_targets) $(out_dir)/rust
  22. $(sfsimg): rcore-fs-fuse rust
  23. @rcore-fs-fuse --fs sfs $@ $(out_dir) zip
  24. user_img: $(sfsimg)
  25. clean:
  26. @rm -rf build/

上面的脚本如没理解,没有关系,只要我们知道使用 make user_img 即可将磁盘打包到 usr/build/riscv64.img

随后,将内核的 Makefile 中链接的文件从原来的可执行改为现在的磁盘镜像,这样就可以把 OS 和riscv64.img文件系统合并在一起了。

  1. # Makefile
  2. # export USER_IMG = usr/rust/target/riscv64imac-unknown-none-elf/debug/hello_world
  3. # 改成:
  4. export USER_IMG = usr/build/riscv64.img

实现磁盘设备驱动

首先引入 rust 文件系统的 crate :

  1. // Cargo.toml
  2. rcore-fs = { git = "https://github.com/rcore-os/rcore-fs", rev = "7f5eeac" }
  3. rcore-fs-sfs = { git = "https://github.com/rcore-os/rcore-fs", rev = "7f5eeac" }

我们知道文件系统需要用到块设备驱动来控制底层的块设备(比如磁盘等)。但是这里我们还是简单暴力的将磁盘直接链接到内核中,因此这里的磁盘设备其实就是一段内存模拟的。这可比实现真实磁盘驱动要简单多了!但是,我们还是需要按照Device接口read_atwrite_atsync去实现。

  1. // src/fs/device.rs
  2. pub struct MemBuf(RwLock<&'static mut [u8]>); //一块用于模拟磁盘的内存
  3. impl MemBuf {
  4. // 初始化参数为磁盘的头尾虚拟地址
  5. pub unsafe fn new(begin: usize, end: usize) -> Self {
  6. use core::slice;
  7. MemBuf(
  8. // 我们使用读写锁
  9. // 可以有多个线程同时获取 & 读
  10. // 但是一旦有线程获取 &mut 写,那么其他所有线程都将被阻塞
  11. RwLock::new(
  12. slice::from_raw_parts_mut( begin as *mut u8, end - begin)))
  13. ...
  14. // 作为文件系统所用的设备驱动,只需实现下面三个接口
  15. // 而在设备实际上是内存的情况下,实现变的极其简单
  16. impl Device for MemBuf {
  17. fn read_at(&self, offset: usize, buf: &mut [u8]) -> Result<usize> {
  18. let slice = self.0.read();
  19. let len = buf.len().min(slice.len() - offset);
  20. buf[..len].copy_from_slice(&slice[offset..offset + len]);
  21. Ok(len)
  22. }
  23. fn write_at(&self, offset: usize, buf: &[u8]) -> Result<usize> {
  24. let mut slice = self.0.write();
  25. let len = buf.len().min(slice.len() - offset);
  26. slice[offset..offset + len].copy_from_slice(&buf[..len]);
  27. Ok(len)
  28. }
  29. fn sync(&self) -> Result<()> {
  30. Ok(())
  31. }
  32. }

打开 SFS 文件系统

在运行 OS 之前,我们已经通过rcore-fs-fuse工具 把包含用户程序的多个文件打包成一个 SimpleFileSystem格式的磁盘文件riscv64.img。bootloader 启动后,把 OS 和 riscv64.img 加载到内存中了。在初始化阶段,OS 为了能够读取riscv64.img,需要使用rcore_fs_sfs::SimpleFileSystem::open(device)方法打开磁盘并进行初始化,这样后续就可以读取文件系统中的目录和文件了。

  1. // src/fs/mod.rs
  2. lazy_static! {
  3. pub static ref ROOT_INODE: Arc<dyn INode> = {
  4. // 创建内存模拟的"磁盘"设备
  5. let device = {
  6. ...
  7. let start = _user_img_start as usize;
  8. let end = _user_img_end as usize;
  9. Arc::new(unsafe { device::MemBuf::new(start, end) })
  10. };
  11. // 由于我们在打包磁盘文件时就使用 SimpleFileSystem
  12. // 所以我们必须使用简单文件系统 SimpleFileSystem 打开该设备进行初始化
  13. let sfs = SimpleFileSystem::open(device).expect("failed to open SFS");
  14. // 返回该文件系统的根 INode
  15. sfs.root_inode()
  16. };
  17. }
  18. pub trait INodeExt {
  19. fn read_as_vec(&self) -> Result<Vec<u8>>;
  20. }
  21. impl INodeExt for dyn INode {
  22. // 将这个 INode 对应的文件读取到一个数组中
  23. fn read_as_vec(&self) -> Result<Vec<u8>> {
  24. let size = self.metadata()?.size;
  25. let mut buf = Vec::with_capacity(size);
  26. unsafe { buf.set_len(size); /*???*/ }
  27. self.read_at(0, buf.as_mut_slice())?;
  28. Ok(buf)
  29. }
  30. }
  31. pub fn init() {
  32. println!("available programs in rust/ are:");
  33. let mut id = 0;
  34. // 查找 rust 文件夹并返回其对应的 INode
  35. let mut rust_dir = ROOT_INODE.lookup("rust").unwrap();
  36. // 遍历里面的文件并输出
  37. // 实际上打印了所有 rust 目录下的用户程序
  38. while let Ok(name) = rust_dir.get_entry(id) {
  39. id += 1;
  40. println!(" {}", name);
  41. }
  42. println!("++++ setup fs! ++++")
  43. }

lazy_static!

这里的 lazy_static! 宏指的是等到实际用到的时候再对里面的全局变量进行初始化,而非在编译时初始化。

这通常用于不可变的某全局变量初始化依赖于运行时的某些东西,故在编译时无法初始化;但是若在运行时修改它的值起到初始化的效果,那么由于它发生了变化不得不将其声明为 static mut,众所周知这是 unsafe 的,即使不会出问题也很不优雅。在这种情况下,使用 lazy_static! 就是一种较为理想的方案。

加载并运行用户程序

那么现在我们就可以用另一种方式加载用户程序了!

  1. // src/process/mod.rs
  2. use crate::fs::{
  3. ROOT_INODE,
  4. INodeExt
  5. };
  6. pub fn init() {
  7. ...
  8. let data = ROOT_INODE
  9. .lookup("rust/hello_world")
  10. .unwrap()
  11. .read_as_vec()
  12. .unwrap();
  13. let user_thread = unsafe { Thread::new_user(data.as_slice()) };
  14. CPU.add_thread(user_thread);
  15. ...
  16. }

当然,别忘了在这之前初始化文件系统!

  1. // src/init.rs
  2. #[no_mangle]
  3. pub extern "C" fn rust_main() -> ! {
  4. crate::interrupt::init();
  5. extern "C" {
  6. fn end();
  7. }
  8. crate::memory::init(
  9. ((end as usize - KERNEL_BEGIN_VADDR + KERNEL_BEGIN_PADDR) >> 12) + 1,
  10. PHYSICAL_MEMORY_END >> 12
  11. );
  12. crate::fs::init();
  13. crate::process::init();
  14. crate::timer::init();
  15. crate::process::run();
  16. loop {}
  17. }

我们使用 make run 运行一下,可以发现程序的运行结果与上一节一致。

如果运行有问题的话,可以在这里找到代码。

只不过,我们从文件系统解析出要执行的程序。我们可以看到 rust 文件夹下打包了哪些用户程序:

磁盘打包与解析

  1. available programs in rust/ are:
  2. .
  3. ..
  4. model
  5. hello_world

但是现在问题在于我们运行什么程序是硬编码到内核中的。我们能不能实现一个交互式的终端,告诉内核我们想要运行哪个程序呢?接下来我们就来做这件事情!