From 0ac9be04e60870a0c02471ee416389e29c721afb Mon Sep 17 00:00:00 2001 From: H Date: Thu, 2 Nov 2023 22:35:52 +0800 Subject: [PATCH] blog commit --- ...73\223\346\212\245\345\221\212-Lucifer.md" | 130 ++++++++++++++++++ 1 file changed, 130 insertions(+) create mode 100644 "source/_posts/2023\345\274\200\346\272\220\346\223\215\344\275\234\347\263\273\347\273\237\350\256\255\347\273\203\350\220\245\347\254\254\344\272\214\351\230\266\346\256\265\346\200\273\347\273\223\346\212\245\345\221\212-Lucifer.md" diff --git "a/source/_posts/2023\345\274\200\346\272\220\346\223\215\344\275\234\347\263\273\347\273\237\350\256\255\347\273\203\350\220\245\347\254\254\344\272\214\351\230\266\346\256\265\346\200\273\347\273\223\346\212\245\345\221\212-Lucifer.md" "b/source/_posts/2023\345\274\200\346\272\220\346\223\215\344\275\234\347\263\273\347\273\237\350\256\255\347\273\203\350\220\245\347\254\254\344\272\214\351\230\266\346\256\265\346\200\273\347\273\223\346\212\245\345\221\212-Lucifer.md" new file mode 100644 index 00000000000..b20a7b89912 --- /dev/null +++ "b/source/_posts/2023\345\274\200\346\272\220\346\223\215\344\275\234\347\263\273\347\273\237\350\256\255\347\273\203\350\220\245\347\254\254\344\272\214\351\230\266\346\256\265\346\200\273\347\273\223\346\212\245\345\221\212-Lucifer.md" @@ -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写操作系统的学习资料,本人收益良多,收获满满。下面是我本次训练营的总结。 + + + +#### 第一阶段: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:。都没有有用的调试信息(可能有,只是我懒,没去用)。 + + + +### 总结: + +对于一个在校大学生来说,这种教程非常适合学习操作系统。同时,也要有一定的基础准备,不然也难以上手。