0%

2023开源操作系统训练营第二阶段总结报告-Lucifer

首先,很感谢各位老师和同学提供如此详尽的Rust写操作系统的学习资料,本人收益良多,收获满满。下面是我本次训练营的总结。

第一阶段:Rust初识

这一阶段,我第一次学习Rust语言。相较于之前学习的C 、C++等,最大的感受就是严格。Rust的语法要求非常严格,一不小心就会导致编译器报错。但是,早点出错是好事。经过了数次编译器的毒打之后,我开始体会到了Rust的用意。因为Rust的安全性,可以提早的发现很多难以发现的Bug,从而大大提高了程序的安全性。在这一周的学习中还算比较轻松。

第二阶段:

第一章:应用程序与基本执行环境

这一章,我们要实现一个可以在裸机上跑的程序。 10月24日

第一个其实就是一个最小的可执行文件,很简单。顺便复习一下之前学的细节。

  • 首先,一个简单的println!()程序的运行离不开运行时的支持。其中我们使用的rustcx86_64-unknown-linux-gnu 可以看出,依赖的东西有linux操作系统,库函数。因此,完成我们的目标需要脱离这两者。我们需要使用riscv64gc-unknown-none-elf
  • 之后,我们得移除std库、println!() ,加上panic_handler。另外由于需要在执行程序之前做初始化工作,所以默认的入口是_start,因此,我们要添加#![no_main],把初始化的工作交给我们,而不是由编译器自动进行。
  • 另外,我们要使用Qemu来运行程序。要指定第一条指令的位置,同时将可执行文件中元数据丢掉,才可以顺利加载(Qemu的问题,功能有限)。当我们要将这个可执行文件和Qemu链接时,默认的可执行文件内存布局不正确。我们需要自己写一个链接器来调整可执行文件的空间布局,并设置好地址。并且最后丢掉元数据。
  • 上一节我们成功在 Qemu 上执行了内核的第一条指令,它是我们在 entry.asm 中手写汇编代码得到的。然后我们想将程序的控制权交给rust语言编写的内核入口函数,而不是之前的asm。所以,我们在entry.asm文件中先完成相关的初始化,然后再将控制权交给入口函数。
  • 内核函数中要完成的第一件事情就是清除.bss段。之后的内容就很常规了,自己通过core库和SBI造轮子。

第二章:批处理系统

这一章,我们需要实现批处理系统。10月25~10月26日

看代码的日子颇不轻松,总是有各种各样的细节不得而知。好在动手画图顺利盘清楚了所有的逻辑。但依旧离完全复现所有的代码还有一段距离。

目标:实现操作系统自动的控制程序的运行,不需要人为的控制程序。

工具:操作系统和应用程序加载到一起。利用异常机制完成不同特权级的转换。处理上下文。

步骤:

  • 首先处理应用程序。所有的应用程序和第一章的一样,自己写一个运行时库。syscall则直接使用汇编指令ecalleret来实现。其他的系统调用都是包装syscall。同时自己手写一个linker,这里我们先将所有的程序都放在0x0位置,等会由操作系统放在合适的位置。最后就是得到合适的二进制文件
  • 之后,我们处理操作系统。和第一章一样,我们需要将操作系统放在QEMU指定的位置。然后将操作系统和应用程序打包放在一起。并且通过操作系统中的一个数据结构来存储应用程序的所有信息。
  • 最后就是,我们的应用程序启动环节。在这里有几个很重要的阶段。初始化:我们需要从内核态到用户态,我们构建一个用户态程序的上下文,放入内核栈中,确定好CSR中各个寄存器的值,之后就可以顺利还原现场,进入用户态。处理非结束的系统调用:这里操作系统使handler去修改上下文中的值,然后返回上下文的栈顶。其实就是修改一些值,然后就恢复现场。处理结束的系统调用:这里其实和初始化很像。因为他们都有一个特点,操作系统需要提供一个全新的用户程序的上下文,之前的上下文是上一个程序的或者空,所以同样的,压一个新的上下文,然后恢复现场,进入用户态。
  • 另外,需要注意的一点,特权级的切换还离不开硬件的参与。而这里我们只是研究一个os,硬件相关的代码由Qemu模拟器去实现,具体参照南大Nemu,所以有些细节也不得而知。

第三章:多道程序与分时多任务

这一章,我们将更加详细的完成实现现代操作系统的抢占式分时多任务。10月26日~10月27日。10月27日下午和晚上休息。过程比较顺利,一气呵成。

小坑:碰到了之前clone https的坑,不过好在后面解决了。

这一章是在上一章的基础上完成,所以上一章的逻辑盘完整了,这一章就比较好处理了。

目标:上一章批处理系统有一个问题,如果程序陷入了死循环,则cpu会一直浪费资源计算这个死循环。所以,我们这一章负责解决这个问题。有两个方法。首先,如果我们可以在程序访问I/O时,主动的将cpu的资源解放出来就好了,于是有了yield系统调用。但是,这个还不够好,因为这个调用需要程序员自己写,大家要有共同的高素质。所以,我们不期待每个程序员都是圣人,而是由操作系统解决这个问题。我们用时间片的机制,实现操作系统定时的切换各种程序。与此同时,计算机的内存空间越来越大,我们可以将所有的程序都加载到内存中,从而更快的切换内存。

工具:引入sys_yield()系统调用;在内核中引入任务切换;

步骤:

  • 修改linker文件,使得每个文件都可以放在内存中,并且每个文件都知道自己的位置(这是个不好的处理),操作系统将其加载在合适的位置。
  • 每次处理trap时,会涉及两个上下文切换,首先是内核态与用户态之间的上下文切换,之后是任务的上下文切换。同样的,这个需要考虑初始化的情况。每个任务都要给它分配一个默认的Trap上下文内容和任务的上下文内容,这样才可以正常的完成第一次切换操作。
  • 中断和时间的处理都比较正常,没有太复杂的地方。

注:看懂逻辑,不代表完成消化了所有的代码。所以要提高对自己的标准,要有尝试复现整个代码的想法。最后,纸上得来终觉浅,绝知此事要躬行。

第四章:地址空间

这一章,我们引入了虚拟地址空间的概念。10月28~10月30日,有些复杂,这一章的内容光看个概念+理解代码就花了我两天的时间,做题又花了一天,而且过程还是比较艰辛。还是有很多细节没有完全清楚。

自从引入了虚拟地址空间之后,我们的操作系统的复杂度上升了一个量级。接下来我们简要的概括一下在有虚拟地址之后,我们应该如何进行以下的操作:

  • 如何引入并且管理虚拟地址?
  • 如何加载相对应的程序?
  • 如何进行Trap上下文切换和任务的上下文切换?
  • 如何实现虚拟地址和物理地址的流畅切换?

整个的流程:

  • 根据链接文件,布置好整个os的结构。手动的放入两个汇编代码,一个找到入口,一个用来加载程序。

  • 进入os内核中,清除bss段,加载相关的log信息。

  • 初始化地址空间 —— 初始化堆,物理帧,内核空间。

    堆:使用buddy_system_allocator中的heap,创建一个一定大小的堆。不过堆放在哪里?在bss段中,因为初始化时。数组的值都为0。HEAP_ALLOCATOR仅负责内核内部的动态内存分配。这个堆只给自己的代码中的变量使用。

    物理帧:运行时创建一个实例,指向内存的某个位置。所有的信息都是物理页号,而不是真的内存地址,这样做的好处是当真正需要访问地址的时候,再转换。提供frame_alloc 和 frame_dealloc两个对外的接口。

    内核空间:分为两个,一个是操作系统内核地址空间,一个是应用的内核空间。

    • 操作系统的地址空间:MemorySet,包含了多个逻辑段和一个多级页表。运行时初始化实例,并且没有包括内核栈。trampoline,逻辑段没有真的被加入,而是单纯一个映射。
    • 应用内核空间:这里通过应用编号找到对应的elf文件,根据文件中的内容进行物理空间的分配。

    内核空间实际上是多个逻辑段组合而成的。每个逻辑段有对应的区间位置,数据对应物理帧,映射方式和相关权限。内核空间提供逻辑段加入维护多级页表 的功能,同时还允许逻辑段加入时,进行初始化。

    这里顺便提多级页表,首先我们每个地址空间都会有一个多级页表,初始化的时候,我们只保存根所在的物理页号,其他的需要时,再创建。对外提供映射找并创建节点的接口。还可以得到root所在的页号

  • 初始化Trap。

    一开始在内核代码中,所以将trap的入口设置在一个会panic的函数入口中,因为我们不处理内核中发生的中断或者异常。

  • 设置时间相关的数值。可以满足实现一个时间片。

  • 开始从内核态到第一个应用程序。

    这里我们调用了TASK_MANAGER,如果是第一次调用,则会在运行时初始化。这里会把所有的应用都放在TaskControlBlock中,即读取elf文件,完成物理内存空间的布局,将相关的信息放入Task数据结构中。同时,我们需要准备用户空间中的上下文内容,并放入相关的物理页中。正式进入相关函数,我们得到TaskManager的控制权,然后找到下一个可以加载的内存,先进行任务切换,结束switch之后,就进入ra寄存器中的地址。也就是换栈。之后就是特权级的切换。ra中位置应该是跳板位置,也就是栈顶。跳板中就是之前的保存并恢复上下文的汇编代码。

第五章:进程

10月31号~11月1号,又花了两天的时间把代码和大部分的逻辑全部盘清除了。总算是弥补了之前遗留下来的很多问题。

11月2号,很多细节和需要仔细学习的地方都是一代而过。

这里我在1号的时候就把所有的逻辑都理清楚了。总的来说,要做的东西难度不大,主要是理解整个代码框架比较花时间。这里还遗留下来一个问题:我没有实现stride,确可以跑所有的样例,真的找不到问题了:sweat:。都没有有用的调试信息(可能有,只是我懒,没去用)。

总结:

对于一个在校大学生来说,这种教程非常适合学习操作系统。同时,也要有一定的基础准备,不然也难以上手。