0%

2021操作系统夏令营第一阶段的一些尝试和探索-czy

2021年操作系统夏令营学习报告

学习rust

第一步是学习rust语言。我过了一遍Tour of rust.这是一份很友好的快速上手教程。下面列出的是我觉得rust语言比较重要或是独特的地方:

  • 变量

    • 命名:snake_case
    • 可以对变量多次重复赋值(类型也可以变),这就是变量隐藏:
      let x = 3;
      let x = 1.4;
    • 如何指定变量的类型
      • 大部分情况下rust能自动推断出类型。但是也可以手动指定类型。
        let x = 5;
        let x:f64 = 3.14159
        • 类型有哪些:
          u8, u16, u32……. 无符号整数
          i8, i16, i32……. 有符号整数
          (item1,item2)…… 元组。用于在栈上传递(函数间传递)一系列固定长度的值。item1和item2可以是不同类型
          [item1,item2]…… 数组。和c的数组类似,item1和item2要是同一类型的。
          切片……………. 数组的一部分。
    • 也可以先声明后初始化,但是很少这么做。
      let x;
      x = 0;
    • rust非常关注哪些变量是可修改的,哪些变量是不可修改的。
  • dyn 动态调用(其实不写也可以)

  • Option, Result<T2,T3> T1指Some()的类型(不是Some的话返回None) T2指OK()的类型 T3指Err()的类型

  • 方法和函数的区别就是方法的第一个参数是实例的引用(不可变用&self,可变用&mut self)

  • lazy_static的作用
    static指静态。
    静态变量活在程序的整个声明周期。
    我们使用lazy_static!宏,可以使得变量推迟(第一次访问到或者其它特定时间)初始化。

  • 枚举类型
    一个枚举类型对应着多个可能的类型。
    详见https://kaisery.github.io/trpl-zh-cn/ch06-01-defining-an-enum.html

  • 迭代器
    简单来说,迭代器就是一个方法,你每次叫唤这个方法,它都返回给你这个容器里的下一个元素。

  • The underscore ( _ ) is a reserved identifier in Rust and serves different purposes depending on the context. It usually means that something is ignored.2020年7月30日

  • The bitwise OR assignment operator |=.

  • 支持c格式的结构体

    1
    #[repr(C)]

对操作系统的大致理解

在看了学堂在线的操作系统网课,阅读了相关资料(rCore-Tutorial, 操作系统真象还原)之后,我对操作系统有了一个大致的理解:
我们知道CPU不停地执行fetch-decode-execute的循环。没有操作系统的时候,cpu不停地重复fetch-decide-execute的循环执行一个程序,如果执行完就关机,如果半路出错就停止程序。
之后CPU多了一个功能叫中断。这个功能往简单了来说,就是中断触发(触发中断的方式有很多种,比如执行一个特殊指令,往某些寄存器里放一些值,或者说事先让CPU倒计时,时间一到CPU就自动触发中断….注意中断机制是由硬件支持的。)后,我们的pc指针就跳转到另一处,同时在特定的某几个寄存器里存储一些有关这次中断的信息。
pc指针跳转到另一处,这另一处往往就存放着另一个程序。这另一个程序往往就是操作系统。操作系统在中断后可以根据之前提到的几个寄存器中有关中断的信息来进行一些处理,主要是保存上下文,进行一些处理(我们操作系统中的大部分代码都是有关这个阶段的),最终回到原来的程序。
当然,除了回到之前触发中断的程序,我们也可以回到另一个程序,这样就实现了运行多个程序。
那么当我们可以选择运行多个程序中的一个时,我们运行哪一个呢?这就涉及进程调度算法了。
所以,操作系统的代码看似在描绘一个连贯的处理流程,实际执行的时候是断断续续的。只有每次中断被出发了,操作系统的代码才会开始执行。

关于中断

中断可以由操作系统内核引发。注意要区分的陷入(Trap)、中断(Interrupt)和异常(Exception)它们经常都被称为中断,陷入是通过指令(如早期的系统调用常用int $0x80)主动引发的,中断一般是硬件(如键盘、定时器)引发的,而异常一般是执行某些指令失败(如缺页、除0)导致的。用户程序可以引发异常,以及主动产生中断表中权限位设了用户位的陷入(比如早期用陷入实现的系统调用就是把0x80号中断设置为用户可访问的)。内核可以直接使用int指令主动触发所有中断。
作者:arrayJY
链接:https://www.zhihu.com/question/471713271/answer/1993262517
来源:知乎

特权级切换的硬件控制机制

当 CPU 执行完一条指令并准备从用户特权级 Trap 到 S 特权级的时候,硬件会自动帮我们做这些事情:

sstatus 的 SPP 字段会被修改为 CPU 当前的特权级(U/S)。

sepc 会被修改为 Trap 回来之后默认会执行的下一条指令的地址。当 Trap 是一个异常的时候,它实际会被修改成 Trap 之前执行的最后一条 指令的地址。

scause/stval 分别会被修改成这次 Trap 的原因以及相关的附加信息。

CPU 会跳转到 stvec 所设置的 Trap 处理入口地址,并将当前特权级设置为 S ,然后开始向下执行。

注解:stvec 相关细节

在 RV64 中, stvec 是一个 64 位的 CSR,在中断使能的情况下,保存了中断处理的入口地址。它有两个字段:

MODE 位于 [1:0],长度为 2 bits;

BASE 位于 [63:2],长度为 62 bits。

当 MODE 字段为 0 的时候, stvec 被设置为 Direct 模式,此时进入 S 模式的 Trap 无论原因如何,处理 Trap 的入口地址都是 BASE<<2 , CPU 会跳转到这个地方进行异常处理。本书中我们只会将 stvec 设置为 Direct 模式。而 stvec 还可以被设置为 Vectored 模式, 有兴趣的读者可以自行参考 RISC-V 指令集特权级规范。
  • 而当 CPU 完成 Trap 处理准备返回的时候,需要通过一条 S 特权级的特权指令 sret 来完成,这一条指令具体完成以下功能:
    • CPU 会将当前的特权级按照 sstatus 的 SPP 字段设置为 U 或者 S ;
    • CPU 会跳转到 sepc 寄存器指向的那条指令,然后开始向下执行。

从上面可以看出硬件主要负责特权级切换、跳转到异常处理入口地址(要在使能异常/中断前设置好)以及在 CSR 中保存一些只有硬件才方便探测到的硬件内的 Trap 相关信息。这基本上都是硬件不得不完成的事情,剩下的工作都交给软件,让软件能有更大的灵活性。

尝试在rCore上实现fat32文件系统

rCore实现了类似ext2的easy-fs文件系统,我尝试在rCore上移植fat32文件系统。我有很多思路,但是由于自己分析代码的能力不够,遇到的障碍比较多。一下是我实现文件系统时的一些思考和探索。

  • 如何进行测试
    如果可以使用easy-fs-fuse的话,是最方便的办法。
    另一个办法是手动生成一个装有测试用例的fat32磁盘,然后测试。

  • 如何支持一簇多块
    easy-fs是一簇一块。
    但是fat32存储设备常有一簇多块的情况,因此要支持。

  • 用伪代码构思如何编写文件系统
    先把自己的思路用假的代码写一写:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    pub sturct myFAT
    {
    BPB={};

    }
    impl myFAT{
    pub fn init(){
    BPB_init();
    }
    pub fn BPB_init(){//学过java的话,pub和没加pub的区别就很好理解了(就是谁能访问到这个方法)
    //注意文件系统是在操作系统里运行的
    此处新建一个数组用来放置read_block读取的信息
    read_block(0,bpb_raw);
    BPB.where_is_FAT = bpb_raw[];
    BPB.where_is_root= bpb_raw[];

    }
    pub fn read_root(){
    read_block(BPB.where_is_root);

    }

    pub fn read_whole_thing(){

    }

    pub fn FAT_query(){

    }

    }
    • 文件系统如何启动,初始化

      1. 读取BPB
        1. 读取第一个扇区,将该扇区的生内容存入某个数组待处理
        2. 处理存有扇区内容的数组,解析出需要的内容:
          • 一个簇有多少个块
          • BPB占多少个块
          • FAT有几个(一般是两个)
          • FAT从哪开始,占几个区块
          • 根目录在哪,占几个区块
    • 文件系统的各种方法

      • 创建文件系统
      • ls
      • read_at
        基本上模仿easy-fs::efs

      好了,其实这东西并不复杂,无非读一个数据块再写回去罢了(过几天的我:并不是!!!!!)

  • 尝试移植embedded-sdmmc-rs

    • 选择这个库来移植的优势
      • 这个库也用BlockDevice。把rCore里sd卡驱动的BlockDevice trait稍微改一改就能用。向下移植的难度较低。
    • 移植的思路
      • 向下移植
        • 修改sd卡驱动的BlockDevice trait的实现
      • 向上移植
  • 尝试移植rust-fatfs

    • 为什么选择rust-fatfs

      • 用rust实现
      • 支持no_std
      • 支持一个簇里有多个块
    • 对接的思路

      • 向下:BufStream->BlockDevice
      • 向上:对接rCore内核
        • 虚拟出inode
          • 在现有的fat32库里增加一个inode.rs虚拟出inode
            • 其中Inode结构体接收一个文件系统和BlockDevice
        • 实现File trait
    • 遇到的麻烦
      代码有一些东西看不懂。先去学rust.
      学习了之后感觉代码好懂多了,现在应该梳理一下这个库的代码。

    • rust-fatfs代码梳理

      • boot_sector.rs
        • pub(crate) struct BiosParameterBlock
          这个结构体实例化出的对象有BPB中常需访问的数据,例如每个扇区多少字节,每个簇多少个扇区等。
          那么我们如何从硬盘中得到BPB的数据并将BPB中的数据:
          • fn deserialize<R: Read>(rdr: &mut R) -> Result<Self, R::Error>
  • 尝试独立编写fat32文件系统

    • 思路

      • 向上与内核对接的思路

        • 先按自己的思路写出fat32的一些基本功能,然后再模仿easy-fs,写出Inode和EasyFileSystem,对接上rCore
      • 向下与sd卡对接的思路

        • 文件系统的代码最终都要落到BlockDevice trait里。只要模仿easy-fs的用法就好了。BlockDevice和块缓存层先不用修改。

        • easy-fs的用法:
          用户自己去调用BlockDevice
          方法要传递进BlockDevice.
          easy-fs 库的块缓存层会调用BlockDevice trait里的两个方法,进行块缓存的管理
          举个例子:

          1
          2
          // easy-fs/src/layout.rs
          pub fn clear_size(&mut self, block_device: &Arc<dyn BlockDevice>) -> Vec<u32> {
    • 实践

      • planA
        打算先不要写那么多层抽象。
        先用最粗糙的方式写,先跑起来再说,然后逐渐抽象。
        大致的步骤:

        1. 写出rCore中调用到的函数

        2. 无视rCore的“五层抽象”,用最简单的办法实现步骤1写出的函数

          • 缓存层模仿rCore的用法
          • 除了Inode和EasyFAT32,其他东西全部用函数和结构体写,不要用任何高级的东西写。为什么要把简单的东西变复杂?
        3. 整理代码,逐步抽象

          “粗糙版本”使用到的技术:

        • 如何使用BlockDevice trait
          1. 定义块缓存(在计算机的内存里,不在磁盘里)
          2. 传入一个实现了BlockDevice的对象的指针
          3. 对象.read_block(块号,块缓存)
        • 如何建立类并实例化类(其实不是类,但是我就这么叫了)
          pub struct aaa{
          bb:u32
          }
          impl aaa{
          pub fn init()->Self{
              Self{
                  bb:3
              }
          }
          }
          my_aaa:aaa = aaa::init();
        • 通过移位将两个u8拼接成一个u16
      • planB
        我打算借用少数的UltraOS的内容,来改造已有的easy-fs。
        直接在easy-fs里面改。
        我们从修改File trait开始入手。
        先找到inode中impl File for OSInode的地方。这里面用到了哪些内部的方法?
        用到了inode.read_at和inode.write_at.
        先来看看inode.read_at.

        可能自下而上改心里会更安稳。所以先看看最底层应该是什么。
        对于文件系统来说,最底层是BlockDevice.
        来看看UltraOS和rCore的BlockDevice有什么区别。->

  • 除了easy-fs文件夹,以下这些地方(即使用了文件系统相关内容的地方)也要做尽量少的改动:
    • 最后改动了27个文件并commit。具体见git历史提交。
    • 其实大部分时间都在读懂别人的代码,自己写的非常少………
  • 直接用十六进制编辑器看fat32文件系统镜像的发现
    • 奇怪的是,会出现两个BPB。fat32标准里没有这个内容。个人怀疑是mkfs.vfat干的。

尝试读懂rCore

我发现要是想移植文件系统,不能孤立地只盯着文件系统的相关知识,而是应该做到胸中有os,对这个复杂软件要有整体的把握。因此我开始尝试分析rCore的源码。

  • os
    这是存放操作系统主要代码的目录。
    我们首先看os/src目录下的main.rs和entry.asm

    • build.rs

    • monitor.rs(UltraOS新增。monitor顾名思义是监视器的意思。主要提供了gdb调试的相关功能。)

    • Cargo.toml

    • Makefile

    • src

    • config.rs

    • console.rs

    • drivers(这个文件夹存放的是有关sd卡和qemu的VirtioIO的驱动程序。主要目的是实现BlockDevice trait.)

      • block
        • mod.rs(将不同平台上的块设备全局实例化为 BLOCK_DEVICE 提供给其他模块使用。引用了virtio_blk.rs里的VirtIOBlck)
        • sdcard.rs(K210 平台上的 microSD 块设备, Qemu不会用)
        • virtio_blk.rs(Qemu 平台的 virtio-blk 块设备)
      • mod.rs
    • entry.asm(第一行表示链接的时候链接到.text.entry段。然后定义_start语义项。接着把sp指向栈顶并调用rust_main函数。最后定义两个32k的栈。)

    • fs(fs文件夹下是有关文件系统的内容。)

      • inode.rs(inode.rs里是simple-fat32的核心内容。提供里OSInode结构体,而OSInode里有一个VFile结构体的引用。这个VFile对应着rCore里的Inode结构体。)

        并实现 fs 子模块的 File Trait)
      • mod.rs
        本文件是fs模块的主干。
        对外暴露出:

        枚举类型FileClass

        ![[操作系统学习笔记.学习rust#枚举类型,1]]

        • FileClass枚举类型里包含两个类型:

          • File类型。它是OSInode结构体的引用。
          • Abstr抽象类型。它是抽象文件的引用。抽象文件实现了File Send Sync特性。抽象文件其实就是通过下面那个[[File trait|操作系统学习笔记.读懂rCore.os.fs.mod_rs#file-trait]]实现的。
            • 这是很独特的设计。

              File trait

        • 这个trait继承了sendsynctrait.

          • 其中send表示这个类型可以安全地在进程间传送。
          • Sync表示这个类型可以安全地在进程引用。
        • 这个trait里面有四个函数签名:

          • readable
          • writable
          • read()
            传入UserBuffer,返回usize. 我猜usize是读取的字节数。
          • write()
            传入UserBuffer,返回usize. 我猜usize是写入的字节数。

          还有一堆pub use,好多 :(

          按照UltraOS的文档,ultraOS的文件系统也是分五层实现的。

          我们一行一行来看。UltraOS的文件系统有很多独特的地方,值得仔细研究。

          ![[mount_rs|操作系统学习笔记.读懂rCore.os.fs.mount_rs]]

          ![[操作系统学习笔记.读懂rCore.os.fs.inode_rs]]

        Tour of Rust中提到过,Rust很少隐藏实现功能的底层细节,看来确实如此:
        ![[操作系统学习笔记.daily#不用怕,1]]


    -  pipe.rs
    -  stdio.rs
    -  mount.rs(UltraOS新增)

        `mount::MNT_TABLE` 是文件挂载表。MNT_TABLE是个MountTable类型的实例。MNT_TABLE里只有一个叫mnt_list的向量。这个向量里的每一个元素都是一个元组。每一个元组里都有三个字符串。根据代码中的注释,它们分别表示special,dir和fstype。
        由于UltraOS不支持设备管理,这个挂载表目前只支持把一个目录挂载到另一个目录下(那能不能挂载目录自己?:) )
        注意,MNT_TABLE被lazy_static!宏包裹着。这意味着需要它的时候它才会出现。

        这套了几层了?五层?
        可能作者在大气层。

-  lang_items.rs
-  link_app.S
-  linker-k210.ld

    os文件夹下实际上有两个链接脚本:linker-k210.ld和linker-qemu.ld。二者唯一差别是BASE_ADDRESS不同。我们可以发现.text.entry段是被放在BASE_ADDRESS上的。

-  linker-qemu.ld
-  loader.rs
-  main.rs
    操作系统从这里开始(一定程度上,之前还有一些准备工作,先不写了)。


    1-8行先是一些标签。
    10行 使用lazy_static库。
    ![[操作系统学习笔记.学习rust#lazy_static的作用,1]]
    11行使用sbi模块里的sbi_send_ipi。看来除了rCore提供的抽象之外,UltraOS也有直接去操作底层硬件。
    12行用了spin库。应该是为了用mutex.

    一些例行公事的内容就跳过不讲了。


    35,36行通过global_asm!宏引入了两个汇编程序。entry.asm

    ## rust_main()
    接着清空bss段。
    接着启动内存管理模块,通过remap_test()来检测多级页表是否被正确设置。
    接着开启中断,启动时钟中断。
    接着启动文件系统模块,列出已有的应用程序。
        - 我们来看看这个list_apps()函数干了什么:
    接着启动第一个进程:加入初始进程的进程控制块。
    然后开始运行进程。

    ## UltraOS增加了多核检测,太强了(+_+)b
    除此之外,main.rs中穿插着对双核的支持。
-  mm
    -  address.rs
    -  frame_allocator.rs
    -  heap_allocator.rs
    -  memory_set.rs
    -  mod.rs
    -  page_table.rs
        ## UserBuffer
        
1
2
3
pub struct UserBuffer {
pub buffers: Vec<&'static mut [u8]>,
}
我们可以发现,UserBuffer类型的实例有一个向量类型的成员buffers.buffers是一个向量。向量中的每一个元素都是 ## u8数组的静态可变的引用还加一撇.....那个撇号是干什么的来着...... 好像和所有权还是生命周期有关。菜鸟教程里面就有,等下看看。 ¿居然没限定每个u8数组应该多长? - UserBuffer有两个方法: - new() 新建并返回一个UserBuffer实例。 - 这是一个静态方法。参数里面没有self的可变或不可变引用。 - len() 根据UserBuffer的迭代器迭代的次数来返回UserBuffer的长度。 - 这是一个实例方法。第一个参数就是&self,表示UserBuffer类型生成的实例本身。 UserBuffer还有个迭代器: ## UserBuffer的迭代器 我们可以发现迭代器其实是很直白的一个东西。 ![[操作系统学习笔记.学习rust#迭代器,1]] - sbi.rs - syscall - fs.rs 本文件实现了与文件系统关系密切的系统调用。 文件的架构非常简洁,就是并排一堆函数。 UltraOS与rCore的区别在于,很多系统调用使用了UserBuffer::write()方法。 它的作用是将一个Buffer的数据写入一个UserBuffer的实例. - mod.rs 这个文件将系统调用分发给fs.rs和process.rs。在分析代码时发现rCore已经实现了最基本最重要的一些操作系统功能了。我们往往只需要拼接现有的操作系统功能就可以实现很多系统调用。 - process.rs - task - context.rs - manager.rs - mod.rs - pid.rs - processor.rs - switch.rs - switch.S - task.rs - timer.rs - trap - context.rs - mod.rs - trap.S

学习UltraOS

rust-fatfs和sdmmc-embedded-rs的移植进度都不乐观。主要的原因是自己对文件系统的掌握并不全面,rust编程经验也不够,这些通用库哪里需要改,各模块的目的都不太明确。所以想到看看今年参加操作系统竞赛的同学是怎么实现fat32的。

我决定学习哈工深UltraOS队的提交作品。
UltraOS实现了fat32文件系统simple_fat32

  • simple_fat32暴露了哪些:

    • BlockDevice
    • VFile <-> Inode
    • simple_fat32
  • 牵一发而动全身
    想要移植simple-fat32的话,还是要对相关的内存管理,进程管理知识有全面的认识。所以弄明白代码背后的操作系统知识(特别是rCore-Tutorial里写的)才是最重要的。

成功移植fat32

以下是移植过程的记录。

  1. ✅ 将simple_fat32和dependency文件夹拷贝到rCore根目录。
  2. ✅ 在os目录下的cargo.toml加入simple_fat32, 去掉easy-fs.
  3. ✅ 根据报错找到需要更改的地方,参考UltraOS修改成需要的样子。如果需要更改其它模块的话尽量不要去更改,想想能不能用别的方式。
    • 错误出现在drivers文件夹和fs文件夹。先看drivers,因为它更底层。
      • drivers/block/mod.rs 报错找不到BlockDevice.
        • 把use easy_fs::BlockDevice;改成use simple_fat32::BlockDevice;
          • [ ] 注意simple_fat32和easy-fs的BlockDevice有一点区别。后面得回来处理这一点。
    • 再看看fs文件夹。错误全部出在inode.rs。
      • 因为我在cargo.toml里把easy-fs去掉了,自然这个地方会报错找不到easy-fs。先把easy_fs改成simple-fat32.
        • 接着报错找不到EasyFileSystem和Inode。显然simple-fat32没有这俩玩意。我们来看看UltraOS在这里是怎么解决的。
          • UltraOS的inode.rs有458行,是rCore的inode.rs的三倍。看来多了不少东西。一点一点看。
            • 首先看use。UltraOS多use了这些:
              1
              2
              3
              4
              5
              6
              7
              8
              9
              10
              11
              12
              13
              14
              15
              16
              use simple_fat32::{
              ATTRIBUTE_ARCHIVE, //一个和读取文件有关的常量。值为0x20u8
              ATTRIBUTE_DIRECTORY, //一个和读取目录有关的常量。值为0x20u8
              //以上二者都在simple_fat32/src/layout.rs里
              FAT32Manager,
              VFile
              };
              use super::{
              File, //为了让OSInode也加入文件的家族
              Dirent, Kstat, DT_DIR, DT_REG, DT_UNKNOWN//这五个都来自于finfo.rs.打开看看。
              //看不懂。看看文档里是否有解释?
              //如果没有解释也没关系。先把finfo.rs加进去。finfo.rs没有引用其它东西。
              };
              use crate::drivers::println; // 输出语句先不管。删掉。
              use crate::color_text;//支持显示彩色文字。我觉得相关的输出语句先删掉把。
              use _core::usize; //TODO 这个不知道干啥的。先不管。
            • 处理完use后,我们发现最核心的两个结构体就是FAT32Manager和Vfile。它们类似于easy-fs里的EasyFileSystem和Inode.接下来要做的,就是参照UltraOS的做法来在inode.rs里使用FAT32Manager和VFile.
              • 先来分析下二者整体架构的异同。
                • rCore的设计相对简单。
                  • OSInode结构体。当应用程序请求读取某个文件的时候,操作系统就会返回给应用程序一个OSInode。
                    • OSInode用互斥锁套着一个OSInodeInner结构体.
                      • OSInodeInner结构体里面有一个指向Inode的指针。
                    • OSInode有如下方法:
                      • new()
                        • 这是一个静态方法(直接叫函数感觉也行)。输入readable writable两个布尔值以及一个指向Inode的指针,返回一个OSInode结构体的实例
                      • read_all()
                        • 这是一个实例方法。输入实例本身,返回一个向量,向量中每个元素都是u8(也就是一个字节的数据)。这个向量就是这个inode对应的文件的全部内容。
                  • 提供了两个OSInode相关的函数
                    • open_file()
                      • 输入文件名(easy-fs所有文件都在根目录下)和OpenFlags。输出这个文件对应的OSInode的指针。
                    • list_apps()
                      • 输出根目录下的所有文件。
                  • 提供ROOT_INODE
                  • OpenFlags结构体。打开文件的的时候要用bitflags来标注一些信息(是否可读,是否可写…..)
                  • 最后为OSInode实现了File接口。
                  • 直觉告诉我应该去吃饭了。我先去吃饭。如果你看到这里,说明你也该吃饭了,或者稍稍休息一下:)
                  • 吃完了。饭食不错,就是食堂小情侣有点多。
              • 再来看看UltraOS在此基础上新增、修改了哪些内容:
                • 新增枚举类型DiskInodeType。里面有文件和目录两种类型/
                • 新增枚举类型SeekWhence:

                  1
                  2
                  3
                  4
                  5
                  6
                  7
                  8
                  9
                  10
                  11
                  12
                  13
                  14
                  15
                  16
                  17
                  18
                  19
                  20
                  21
                  22
                  23
                  24
                  25
                  26
                  27
                  28
                  29
                  30
                  31
                  32
                  33
                  34
                  35
                  36
                  37
                  38
                  39
                  40
                  41
                  42
                  43
                  44
                  45
                  46
                  47
                  48
                  49
                  50
                  51
                  52
                  53
                                                  #[derive(PartialEq,Copy,Clone)]
                  pub enum SeekWhence{
                  SeekSet, // 将offset设为新的读写位置
                  SeekCur, // 将当前读写位置往后增加offset个偏移量
                  SeekEnd, // 将读写位置设为末尾,然后增加offset偏移量(此时offset可以<0)
                  }
                  - OSInode新增方法:
                  - find() 根据给的路径找到路径对应文件的OSInode
                  - getdirent()
                  - get_fstat()
                  - get_size()
                  - create()
                  - clear()
                  - delete()
                  - set_offset()
                  - ROOT_INODE和lisp_apps()做了简单修改.
                  - list_files()
                  - 输入两个字符串的引用,一个叫work_path,另一个叫path。啥区别?
                  - 输出(直接在终端输出,不是返回某个值)当前目录下的文件/目录名。
                  - open()函数是open_file()修改得来的。
                  - ch_dir() 切换目录。
                  - 其余的内容基本保持不变。看得出来VFile努力做到与Inode兼容。我们把UltraOS的inode.rs复制过来看看。
                  - 报缺少color_text!宏。先去掉这个宏。没有更多报错了。
                  - 最后看mod.rs。
                  - pub use open报错,我们现在只有open_file函数。看看open_file和open是否兼容。兼容的话直接改名字即可。
                  - 这两个函数的参数都不一样。看来用到open_file()的地方都要仔细修改。
                  4. ✅ simple_fat32做到了90%的api都和easy-fs兼容,剩下的api的使用方法是不一样的。我们要为simple_fat32适配内核中所有使用了文件系统的地方。根据报错,目前有以下三处:
                  - syscall/process/rs 缺失open_file
                  - 我们保证rCore已有的函数/方法能够正常运行即可。
                  - syscall/fs.rs 缺失open_file
                  - task/mod.rs 缺失open_file
                  5. ✅ 根据调用链完善一下剩余的少数缺失的函数和方法,我们成功地移植出了fat32文件系统,并且在qemu和k210板子上做了测试。rCore自带的usertests全部通过。



                  # 更进一步-适配busybox
                  ## busybox二进制程序是如何得到的
                  我没有自己去编译。
                  UltraOS里面有一个stripped的busybox,有1.1m.比unstripped的(1.7m)小一些.
                  qq群里有同学上传过编译好的busybox。有静态链接的和动态链接的。但是rCore的userlib还没细看做到什么程度,动态链接不知道行不行。先试试静态链接。但是静态链接的程序有点大,不知道能不能跑。

                  我尝试直接把busybox二进制程序丢进sd卡运行。
                  ## 尝试运行busybox的结果
                  出了点问题。

                  ```shell
                  # 静态链接的busybox
                  >> busybox
                  [kernel] Exception(LoadFault) in application, bad addr = 0x13cbb6eb08, bad instruction = 0xcb608, core dumped.
                  Shell: Process 2 exited with code -2
                  >> busybox ls
                  [kernel] Exception(LoadFault) in application, bad addr = 0x1b4000, bad instruction = 0x116fb0, core dumped.
                  Shell: Process 2 exited with code -2

在查错之前,我先试试动态链接的busybox和静态链接的lua.

1
2
3
4
5
6
7
# 静态链接的lua
>> lua
[kernel] Exception(StoreFault) in application, bad addr = 0x0, bad instruction = 0x2f6a0, core dumped.
Shell: Process 2 exited with code -2
# 动态链接的busybox
>> busybox
[rustsbi] panicked at 'invalid instruction! mepc: 0000000000000000, instruction: 464c457f', platform/k210/src/main.rs:501:17

还有动态链接的lua没试,但是先不试了。

动态链接的库出现了rustsbi的报错。这应该是动态链接产生的问题。先不管。先来看静态链接的。

LoadFault和StoreFault是什么?

  • 静态链接的busybox报LoadFault.
  • busybox ls的话,bad addr会不一样。这可能涉及rCore对参数的处理。
  • 静态链接的lua报StoreFault。

先来弄清楚LoadFault和StoreFault是什么。
发现了在做rCore实验的同学的笔记:)
https://zhuanlan.zhihu.com/p/350697212

  • LoadFault和StoreFault都是rCore起的名字。
    • 如果程序访问不存在的地址,会得到 Exception::LoadFault
    • 查rCore-Tutorial知道LoadFault和StoreFault都是scause的一员,而scause又是mcause的子集。二者分别表示取/存到错误的内存地址。
  • 现在要分两条路:
    • 第一条是自己编译一次busybox,
    • 第二条是弄清楚rCore加载程序的流程

LoadFault和StoreFault报错的是物理地址还是虚拟地址?

我们能从源代码中找到线索:

1
2
3
4
5
6
println!(
"[kernel] {:?} in application, bad addr = {:#x}, bad instruction = {:#x}, core dumped.",
scause.cause(),
stval,
current_trap_cx().sepc,
);

stval我们很熟悉。
查rCore-Tutorial,果然有。介绍得很完善。这是第二章的内容,我应该是仔细读过的,可惜只留下了模糊的印象:
![[特权级切换的硬件控制机制|操作系统学习笔记.硬件知识#特权级切换的硬件控制机制]]

  • 因此我们可以知道,
    • scause中保存着中断的原因(这次我们触发了LoadFault和StoreFault)
    • stval保存着中断的附加信息(这回是bad addr)
    • current_trap_cx().sepc 中
      • current_trap_cx 看名字都能知道是干啥的了:这次中断的上下文。
      • sepc是个寄存器。这个寄存器中保存的是触发中断之后默认继续执行的那个指令的的地址。在我们这个情况下,这个中断是一个异常,所以sepc被设置为触发中断之前执行的最后一条指令的地址。
    • 不论是stval还是sepc显然都是物理地址。
      • rCore-Tutorial 第二章根本就还没实现虚拟地址。
      • 这是一个CPU负责的操作。

继续推理。bad addr说的应该就是:
我们访问了一个非法的物理地址(这个地址存储在stval寄存器里)造成了错误。这个指令本身的地址存储在sepc寄存器里。
恭喜!我们成功分析出了这个错误。

接下来要找到造成这个错误的深层原因。

会不会是必要的系统调用没实现?

我怀疑出错的原因单纯是有些系统调用没实现。我去把没实现的系统调用都改成输出看看。
不对,rCore已经帮我们做到了这点……

1
_ => panic!("Unsupported syscall_id: {}", syscall_id),

我们并没有发现过Unsupported syscall_id:这个报错,看来问题不在这里。

会不会是给程序分配的内存太小?毕竟两个可执行程序都有上百k。

不太对……

会不会是elf解析出了问题

对照一下UltraOS的代码和我们的代码即可发现问题。

会不会我根本猜不出来原因?

群里问问吧。这样瞎猜瞎摸也不是办法。
好多人都遇到了类似的情况。

  • 有同学提到这个问题一般是因为栈里面参数环境变量分布与Linux不一致引起的(那个busybox就是为linux on riscv64 编译的),
    • 那什么是参数环境变量?又怎么调整到和linux一致?

      有同学提到要先这样设置用户栈 [argc|argv数组|环境变量数组|辅助数组],
      还有同学提到初始的sp低4位要为0。

尝试的一些解决思路

  • 虽然busybox不能运行,但是rCore自带的几个测试用例是可以运行的。土(又简单又蠢但是非常有用的)办法,用十六进制编辑器打开看看busybox和它们有什么区别。
    • 看不出来。他们都是risc-v架构的elf格式,按理来说不会出错。
  • elf的构建是编译器做的,出错概率很低。elf方面唯一容易出错的是我们操作系统的elf解析流程,但是rCore自己的测试用例又是可以运行的, 所以elf解析出错的概率也很低。但是也许可以用objdump来分析一下
  • 自己编译一个弱智程序看看能不能跑

学习过程中遇到的一些问题和解决办法

  • 如何快速比较两份代码的异同?

    • 运用vscode自带diff工具很好用。
      1. 在一个窗口里同时打开两个文件夹
      2. 右键第一份代码->选择以进行比较
      3. 右键第二份代码->与已选项目进行比较
  • rust-objdump 找不到?

    • cargo install 会默认将二进制文件添加到 ${HOME}/.cargo/bin 中,我们将这个路径加入到 $PATH 环境变量中之后就能找到需要的 rust-objdump 命令了。
  • 治各种文件锁

    •   rm ~/.cargo/.package-cache
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
          - 有的时候什么都不做,等一会文件锁就会自动解除了。

      - 虚拟机网络不能用
      - /etc/proxychains.conf ip改成设置->wifi里面的那个ip

      - dd resource busy
      https://unix.stackexchange.com/questions/271471/running-dd-why-resource-is-busy

      unmount就行了。

      Apple court, Apple rules. Try diskutil:

      $ diskutil list
      ...

      \# if mounted somewhere
      $ sudo diskutil unmount $device

      \# all the partitions (there's also a "force" option, see the manual)
      $ sudo diskutil unmountDisk $device

      \# remember zip drives? this would launch them. good times!
      $ sudo diskutil eject $device
      (In the case of a disk image, the hdiutil command may also be of interest. You can also click around in Disk Utility.app.)

      - rust-analyzer的错误报错
      - user/src/lib.rs 1-1 can't find crate for 'test'
      - os/src/drivers/block/mod.rs
      `BlockDeviceImpl`
      failed to resolve: use of undeclared type `BlockDeviceImpl`
      use of undeclared type `BlockDeviceImpl`rustcE0433
      {unknown}
      - os/src/mm/memory_set.rs
      `MMIO`
      unresolved import rust-analyzerunresolved-import
      unresolved import `crate::config::MMIO`
      no `MMIO` in `config`
      - os/src/main.rs 1-1
      can't find crate for `test`
      can't find craterustcE0463
      - os/src/timer.rs
      ```use crate::config::CLOCK_FREQ;
      unresolved importrust-analyzerunresolved-import unresolved import `crate::config::CLOCK_FREQ` no `CLOCK_FREQ` in `config`rustcE0432
  • /dev下哪个是我的k210开发版??????????????

    • 它一般叫cu.usbserial-1110
  • grub不小心按下esc然后进入了bash怎么办

    • 输入normal回车

开发过程日常记录

感觉这样的流水账很少愿意回去读,复用的价值没发挥出来,所以有的时候不太想记。其实每天都学操作系统知识,但是不是每天都记录。也许我能想到一种能让流水账发挥它的作用的方法。

7月13日

  • 想通过练习的形式再强化下rust。

    • rust教程往往涵盖某些方面。我应该先列出自己需要的方面:
      • 泛型
      • struct
    • 也许我需要的是rustlings和tour of rust.做做看。
      • 先试试tour of rust. 笔记见![[操作系统学习笔记.学习rust]]
      • 除智能指针(rCore中除了Arc用得都比较少),文本,项目管理以外的章节都看完了。
  • 仓库链接的事

    • 群里问如何补交仓库链接
    • 补交
    • 大创同学提醒一下也要这么提交仓库链接
    • 记得定时(特别是os-summer-2021结束之前)更新仓库
      • 懒得用git的话,直接拖拽上传也可以

7月15日

写读懂rCore。
[[读懂rCore|操作系统学习笔记.读懂rCore]]

7月16日

动手写自己的fat32。 -> [[在rCore上实现fat32文件系统|操作系统学习笔记.在rCore上实现fat32文件系统]]
记得把自己的各种思考都记下来,尝试在屏幕上用键盘和自己思考对话。操作系统很复杂,脑子的工作记忆很可能不够用。半小时就改晕了。
基本思路:
缓存,什么时候写
找到UltraOS怎么用->ATOM里面。
如果涉及其它库,弄懂UltraOS的方法的原理,然后看看能不能曲线救国。即patch on patch(用现有的接口实现没实现的接口)
simple-fat32的所有抽象层都提供了对外接口。所以可以用任何想要的粒度改。

不用担心,我们只需要记住:
![[操作系统学习笔记#不用怕,1]]

7月17日

昨天晚上把fat32调通了,rCore自带的几个程序也能运行了(stripped后的不能运行,只有直接编译出来的不带.bin的才可以,不知道为什么)!太感谢UltraOS团队了
现在尝试支持busybox.全新挑战
要支持busybox和lua,首先要支持sh.
我的思路是通过rCore自带的shell启动sh。这很ok,linux上也经常出现通过sh启动bash的情况。
但是这个busybox是一个整体(就一个二进制文件叫busybox)。我怎么启动busybox中的某一个小工具?
应该要用到某些参数。rCore的shell支持参数,也许可以碰个运气。
这种知识往往youtube上有简单易懂的介绍。去看看。
https://www.youtube.com/watch?v=wWA6SvzvElU
确实有。讲解了busybox的
- 内容
- 把很多常用linux工具集合在一个二进制文件里
- 用法
- busybox [function] [arguments]
- 用处
- 性能有限的嵌入式设备
- 小linux发行版
- 在ubuntu里/bin/下有很多常用工具,而在Alpine Linux下/bin/下这些工具都被simlink(这是硬链接还是软链接?)到busybox里(工具名通过管道传输给busybox)
看来busybox的用法也很直接。
具体适配过程见[[适配Busybox|操作系统学习笔记.适配busybox]]

7月19日

其实只要弄清楚一个问题,我的笔记系统就完善了:知识有架构,有双向链接,粒度也控制得很好,读起来写起来都舒服,但是对于分析问题的记录简直就是一团浆糊,写出来一大坨根本没有可读性。线性的就不说了,树状的更没有可读性了。
想到一个可能有用的办法,尽量把笔记搬到知识的相关页面。。分析问题的过程尽量精简得只剩下指向各知识页面的链接。
还有一个方法,用mermaid的graph。对于问题的分析往往是网状的,直接用网状的东西表达出来。
本质上来说,就是起标题提炼内容,二是分解内容控制粒度。

其他体会

学习方法

要相信只要不停地对自己使用费恩曼学习法(对自己教学)而不是被动地阅读,一定是可以快速理解资料或者找到不会的知识点的。

看代码,修改rCore也是一样的道理,不要被动地或者纯粹顺着习惯地去做事,应该假装自己是个老师在交别人做事。

记笔记的好处

写这种笔记本质上是在和自己的思想对话。好处是我可以自己控制想什么,想到什么程度(掌控全局避免钻牛角尖),而且书写、整理的过程加深了理解记忆。
建议先去看[[读懂rCore|操作系统学习笔记.读懂rCore]]
也许是理科版的卢曼卡片笔记吧 :)

我可能在不同主题中游荡,有时在某些主题中磨蹭很久,写很多的笔记。这磨蹭的过程就是在和这个主题的知识进行交互,形成这个知识点的组块。组块形成之后,我们就可以游刃有余地使用这个组块(往往就是游刃有余地使用某个struct)

不要太完美主义。前期做的东西不满意不要全部推倒重来,而是从中寻找到有用的部分继续拿来用。

人脑的工作记忆有点像cpu的寄存器,是有限的。
假设我们的工作记忆就是五个组块。下图中一个节点代表一个组块。
如果我们不记笔记,在分析问题a的时候我们考虑到b,c,d,e四个思路。此时脑子很可能已经被这四个思路占满了,即使想顺着思路c分析下去,工作记忆可能已经满了,这时由于能用的工作记忆不足,再分析下去容易出现思路的混乱。
如果我们把思考的过程都记录下来,我们就可以暂时忘记a,b,d,e(需要的时候可以即刻从笔记中获取,”获取“多了,见多了,它们就形成组块进入长期记忆,不占用工作记忆)专注于c节点的分析,此时我们可以容易地容下新想到的f,g,h节点。如果此时发现c思路不对也不要紧,把fgh都记录下来,然后从笔记中找回b,d,e节点的思路,再选一个进行分析。万一过一阵子又需要fgh节点了,也可以随意回去读取。

这就是记笔记促进思考,分析,创新的原理:自由无限制地(主要指记忆容量上)和过去任何时刻的自己对话。

    ___b
   |           ___f
a--|-------c--|___g
   |          |___h
   |___d
   |___e

还有一个好处,我称作确定性。比如你脑子里有a->b,c,d,e的时候,对于bcde四个思路你踌躇不定,对于每个点子,我们都给予25%的权重,但是分析到f g h 的时候f g h 只能得到25%*33%=8%的权重了。这么低的权重,我们的思维不会放很大精力在这上面。
而如果记了笔记,由于关于abde的思维已经完全被记录在笔记里,在分析c的时候我们可以将它们彻底遗忘,稍后再根据笔记恢复相关思路,此时可以把精力完全放倒c思路的分析上,f g h都可以得到33%的权重了。
说白了就是,记笔记之后,惦记的东西就少了,可以专注。