-
Notifications
You must be signed in to change notification settings - Fork 387
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #128 from LittleLucifer1/master
add blog
- Loading branch information
Showing
1 changed file
with
130 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,130 @@ | ||
--- | ||
title: 2023开源操作系统训练营第二阶段总结报告-Lucifer | ||
date: 2023-11-02 22:33:32 | ||
tags: | ||
- author:LittleLucifer1 | ||
- repo:https://github.com/LearningOS/2023a-rcore-LittleLucifer1 | ||
categories: | ||
- blog | ||
--- | ||
|
||
首先,很感谢各位老师和同学提供如此详尽的Rust写操作系统的学习资料,本人收益良多,收获满满。下面是我本次训练营的总结。 | ||
|
||
<!-- more --> | ||
|
||
#### 第一阶段:Rust初识 | ||
|
||
这一阶段,我第一次学习Rust语言。相较于之前学习的C 、C++等,最大的感受就是严格。Rust的语法要求非常严格,一不小心就会导致编译器报错。但是,早点出错是好事。经过了数次编译器的毒打之后,我开始体会到了Rust的用意。因为Rust的安全性,可以提早的发现很多难以发现的Bug,从而大大提高了程序的安全性。在这一周的学习中还算比较轻松。 | ||
|
||
#### 第二阶段: | ||
|
||
### 第一章:应用程序与基本执行环境 | ||
|
||
这一章,我们要实现一个可以在裸机上跑的程序。 10月24日 | ||
|
||
第一个其实就是一个最小的可执行文件,很简单。顺便复习一下之前学的细节。 | ||
|
||
+ 首先,一个简单的`println!()`程序的运行离不开运行时的支持。其中我们使用的`rustc`是`x86_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`则直接使用汇编指令`ecall` 和`eret`来实现。其他的系统调用都是包装`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:。都没有有用的调试信息(可能有,只是我懒,没去用)。 | ||
|
||
|
||
|
||
### 总结: | ||
|
||
对于一个在校大学生来说,这种教程非常适合学习操作系统。同时,也要有一定的基础准备,不然也难以上手。 |