0%

前言

前前后后入了几次 Rust 的门,都以失败告终。这学期课排得少,很闲,一开始是选择做 jyy 的 OS 实验,后来在菜鸡取暖群中看到这个训练营的信息,遂跑路开始做这个。
第一阶段的话,因为以前做过 Rustlings 做到差不多一半,所以还是比较轻松的,然后就开始摸鱼了(逃)

第二阶段总结

学校的操作系统课教得比较水,而且以理论偏多,也有我自己不认真学的原因,最后效果比较差。能从理论和代码层面逐渐实现一个操作系统,对我来说是一次崭新的体验。现在磨洋工磨了三个周做完三个实验之后,作关于第二阶段的总结。

一开始配环境的时候碰了点壁,当时想在 Arch 下面做,但是 Qemu 太新了,旧版源码也编译不了,最后还是重回 WSL 的拥抱…

Chapter 1

第一章讲述了怎么写一个 Bare-metal 应用,涉及到了一些内存布局、汇编、链接器之类的知识,好在之前看 CSAPP 是看过相关内容的,所以压力不大。不过一上来面对这些东西,对一些概念还是有些模糊或是错误理解。比如内核栈其实一直都是放在 .bss 段的,我老以为它在别的地方,这一点我直到第三章才弄清楚…

关于脱离操作系统和标准库的依赖来写一个程序这件事,心里却没有太大波澜,因为我之前也看过一点 BlogOS, 那个是在 x86 上实现的。接触一个较新的体系结构对我来说挑战会更大一些。

Chapter 2

第二章,批处理系统。看到这个名词我脑海里浮现的就是那种几十年前的大型机…不扯别的了。这一章的重点是特权级和 Trap 的管理, 这部分的汇编确实得慢慢消化。在看最后一节的前半部分的时候我都在想,要怎么执行第一个应用呢,我的最初想法就是直接把 pc 跳到初始地址,其它的寄存器该保存保存,也不需要转换特权级。看到最后,通过构造特殊的 Trap 上下文来使程序运行的做法给我留下很深印象,这也是特权级的意义所在,用户态就该乖乖跑程序,切换应用是内核该做的事。 (主要当时忽略了内核开始运行时是在 S 态下的,不 __restore 也进不去 U 态)
批处理虽然比较过时了 (个人理解…), 但是这里涉及到了重要的概念,给后面的章节做下铺垫。

Chapter 3 & Lab 1

第三章,从批处理变成了分时多任务,这一次多了任务切换的概念,在两个 Trap 控制流间跳转,达到切换应用的目的。同时也引入了时钟中断,感觉和之前学过的理论知识串起来了。

实验一,这个的实现就是在 TCB 里面加字段,加一个返回相应字段的函数,并在调用 syscall 的时候加个计数,没了,还是比较简单的。

Chapter 4 & Lab 2

第四章,引入了页表。我在这里头疼了很久。页表的设计很巧妙,同时也是在这一章,编程语言带来的优势体现得淋漓尽致。比如同样是 usize, 但是都可以包一层变成不同的类型 (VirtAddr, PhysPageNum, etc…),借助 Trait 机制还可以随意转换。虽然 C 也有 typedef, 但是按这样写的话,代码肯定会相当混乱;Drop Trait 相当好用,RAII 比手动释放资源不知道高到哪里去了。写到这里,Rust 的高贵已经尽数体现了

这一章与上一章不同的是,因为引入了虚拟地址空间,所以在切换任务的时候页表也得一起换;内核和应用地址空间的设计也经过了精心规划,比如跳板的设计等等,以及在内核空间创建的时候就对整个剩余可用的内存段建了一个恒等映射,所以在内核里不用担心地址找不到的问题,很是巧妙。同时,在这一章的代码实现中出现了多个平滑的表述,让我意识到操作系统的编写需要足够细心,能洞察对切换前后系统的状态。尤其是这种涉及 xxx 切换的部分,写出 Bug 很难查。

实验二,自我感觉我的实现很丑陋。先谈重写上个实验两个系统调用的事情,现在是要对一(或两)个 [u8] 上操作。在指针指向的数据跨页的情况下我是真不知道咋办,感觉这种时候可能 C 语言更好用些。 (不过不写也能过用例, 所以 sys_task_info 的我就鸽了)

分配内存的那两个调用,因为限制条件很多,体现在代码上就是一堆 if. 以及 unmap 的实现感觉有点低效,每删一页都要找一下在哪个逻辑段,但也想不到更好的办法了。这里还被小坑了一手,find_pte 就算返回了一个 Some 也不一定有效,还得手动 is_valid 一下。

Chapter 5 & Lab 3

第五章,任务变成了进程,这里开始有一点点知识盲区了,第一次知道 exec 和 “idle task” 的作用。idle 控制流就是不断循环取任务执行,每次有应用交出 CPU 使用权时,就会切换到 idle 控制流并开启新一轮的任务调度,调度算法就可以在这里做文章了。

实验三,代码框架提供的函数非常够用,所以很快就做完了。 (除了一开始忘记维护父子关系有点难绷)

结语

参加本次训练营让我受益匪浅,我是越来越喜欢 Rust 了(同时也越来越不喜欢 Java…),同时对操作系统也有了更立体的认识。写到后面,随着抽象程序越来越高,感觉和写一个普通的程序差别不大了。能优雅地构建一个精密的系统是一件多么令人愉悦的事!

这一次因为前面太偷懒了,用了多达三个周的事件才草草做完三个实验。后面的时间我会做完剩余两个实验,并尽力完成第三阶段的任务。

感谢参与本次训练营的老师和工作人员,谢谢你们!

2023开源操作系统训练营一二阶段总结

Lab1

Lab1要实现获取任务的信息,包括任务使用的系统调用及调用次数、系统调用时刻距离任务第一次被调度时刻的时长,实验内容比较简单,但是本章的知识点却是后几章的基础,需要深刻理解。因此关于Lab1的总结主要分为两部分:

  1. 操作系统特权级切换及任务切换
  2. 实验完成思路

    特权级切换及任务切换

    特权切换

    对于rCore,特权切换发生在系统调用和中断前后,如下图所示:

要实现特权级的切换,需要软硬件协同工作,主要包括两部分:

  1. Trap上下文的保存与恢复
  2. CSR寄存器的设置
    对于Trap上下文的保存与恢复,主要有以下几步:
  3. 系统初始化的时候,修改 stvec 寄存器来指向正确的 Trap 入口
  4. 发生trap时,进入入口函数__alltraps,它做了以下几件事:
    1. 将栈指针指向内核栈
    2. 在内核栈保存通用寄存器和CSR寄存器
    3. 调用trap_handler
  5. 调用完成后,调用__restore,它做以下几件事:
    1. 内核栈顶的 Trap 上下文恢复通用寄存器和 CSR
    2. 将栈指针指向用户栈
    3. sret返回用户态

而任务切换不涉及特权级切换,是在进入到Trap之后,进一步进行的,主要有以下几步:

  1. A 在 A 任务上下文空间在里面保存 CPU 当前的寄存器快照
  2. B 任务上下文,根据 B 任务上下文保存的内容来恢复 ra 寄存器、s0~s11 寄存器以及 sp 寄存器

实验思路

我们需要实现系统调用:

1
fn sys_task_info(ti: *mut TaskInfo) -> isize

其中:

1
2
3
4
5
struct TaskInfo {
status: TaskStatus,
syscall_times: [u32; MAX_SYSCALL_NUM],
time: usize
}

我们需要获得syscall_times和time。

  1. syscall_times: 需要在任务控制块中加入用以记录系统调用及其次数的字段,并且暴露出一个可以更新这个字段的方法,然后在syscall函数里面,通过调用更新方法,维护调用次数。
  2. time:需要在任务控制块中加入用以记录时间的字段,每次调用某任务前,首先查看是不是第一次调用该任务,若是,则记录当前时间。

Lab2

目前的操纵系统存在两个问题:

  1. 用户程序可以通过物理内存修改其他用户程序的代码数据甚至内核的代码数据,这是非常不安全的。
  2. 此外,由于用户程序的加载强依赖于物理地址,因此我们在编写用户程序的时候就需要考虑程序在物理内存中的布局以防止和其他用户程序和内核程序之前的重叠,这给用户程序编写者带来了极大的不便。
    为了解决这两个问题,引入虚拟内存。对于虚拟内存如何翻译到物理内存,这部分比较简单,略过。对于OS代码中如何建立地址空间,着重讲一下,因此关于Lab2的总结主要分为两部分:
  3. 地址空间
  4. 实验完成思路

    地址空间

    如图为应用地址空间

每一段我们抽象成一个MapArea,整个地址空间我们抽象成一个MapArea的数组。因此,建立地址空间就是建立一个个MapArea,然后push到数组里面。在这一章里,有一个很重要的细节点,就是RAII的思想,可以说是贯穿了操作系统内存管理的始终。

实验完成思路

我们需要实现系统调用:

1
fn sys_mmap(start: usize, len: usize, port: usize) -> isize
  1. 首先需要检查错误
  2. 因为memory_set是暴露出来的,我们可以直接访问memory_set里面的相关方法进行一段连续内存的映射

对于取消申请的系统调用,思想同上。

1
fn sys_munmap(start: usize, len: usize) -> isize

Lab3

这一章的重点是如何新建一个进程

  1. 进程生成机制:这主要是指 fork/exec 两个系统调用。
  2. 进程资源回收机制:当一个进程主动退出或出错退出的时候,在 exit_current_and_run_next 中会立即回收一部分资源并在进程控制块中保存退出码;而需要等到它的父进程通过 waitpid 系统调用(与 fork/exec 两个系统调用放在相同位置)捕获到它的退出码之后,它的进程控制块才会被回收,从而该进程的所有资源都被回收。
    关于Lab3的总结主要分为两部分:
  3. 进程生成
  4. 实验完成思路

进程生成

在内核中手动生成的进程只有初始进程 initproc ,余下所有的进程都是它直接或间接 fork 出来的。当一个子进程被 fork 出来之后,它可以调用 exec 系统调用来加载并执行另一个可执行文件。因此, fork/exec 两个系统调用提供了进程的生成机制。

fork

fork主要做了以下几件事:

  1. 复制父进程地址空间
  2. 得到trap上下文物理地址页号
  3. 分配pid及内核栈
  4. 创建进程控制块
  5. 添加子进程至父进程进行管理
  6. 修改trap上下文中的kernel_sp使其指向新建的内核栈

exec

exec主要做了以下几件事:

  1. 从elf新建一个地址空间
  2. 得到trap上下文物理地址页号
  3. 更新地址空间
  4. 更新trap上下文物理地址页号
  5. 更新entry_point, user_sp

实验思路

实现系统调用

1
fn sys_spawn(path: *const u8) -> isize

结合exec和fork方法,糅合即可。

实现系统调用

1
2
3
4
5
// syscall ID:140
// 设置当前进程优先级为 prio
// 参数:prio 进程优先级,要求 prio >= 2
// 返回值:如果输入合法则返回 prio,否则返回 -1
fn sys_set_priority(prio: isize) -> isize;

只需在任务控制块里添加prio、pass、stride字段,将TaskManager里的VecQueue换成BinaryHeap,然后自定义任务控制块的比较方法,使其按照stride进行比较。然后自定义stride的比较方法,以处理溢出问题即可。

总结

学习rCore使我认识到,不论是rust还是操纵系统,我都还有很长的学习之路要走;写总结报告使我认识到,将一个论点清晰地展示给别人,是一件难度不亚于习得该论点的事情。如何高效学习,再如何简洁明了地教给别人,值得我长时间的思考。

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

实验总结

lab1

本次实验要求我们实现一个sys_task_info来获取进程的信息。最开始的时候我的设计是在TASK_MANAGER中存放两个桶分别存放每项任务的调用次数和起始时间,查询运行时间时有当前时间减去起始时间即可得到系统调用时间。但由于调用次数会变化,而TASK_MANAGER是个静态变量。遂放弃调用次数的桶转而在TCB结构体中存放记录。

lab2

本次实验要求重写sys_get_time 和 sys_task_info,同时实现虚拟内存和物理内存的绑定与解绑。
由于虚拟储存机制的引用我选择了重写一个get_kernel_ptr的泛型函数获取物理内核中的指针。但是由于虚拟缓存机制的影响导致我的lab1中的起始时间桶出现了一些奇奇怪怪的问题,我又含泪放弃lab1的起始时间桶,选择把起始时间封装到TCB中。

lab3

万恶之源!!!和前面两个lab兼容性极差,刚切过去的时候让我怀疑人生,我lab2的代码根本找不到原本放置的位置。我需要重新找位置来存放一些函数。又让我重构了大部分lab2的代码。优先级设置较为简单,但还是出了点问题。最开始我是选择在任务队列里按顺序插入,fetch任务时在遍历队列找到stride最短的任务,但是我本地测试电脑风扇轰鸣了二十分钟也没出结果。经过参考其他大佬们的总结我决定在任务进入的时候按照stride长度进行插入,提前将队列有序。重构了代码之后才通过。

个人感想

本次的rcore总结下来并不难,但在写的时候还是挺崩溃的。一是虽然有一阶段的rust语言的学习,但对于多个文件协同运作对我来说还是比较艰难的。二是由于对于测试方式的不太理解,导致我无法通过gdb进行调试,只能通过不断的打印信息来获取bug原因。三是几个lab的兼容较差,需要我们自行向前,没有类似于一些公开课的项目那种前面模块的测试通过之后后续基本可以不用改动前面模块的模式,心智负担极大,希望后续能有所改进。
最后很感谢训练营能为我们提供了这么一个学习平台,让我们有机会接触到操作系统比较底层的内容。让我们对操作系统有了更深刻的体会。

背景

我本人特别看好Rust语言及RISCV指令集架构的,所以也经常会查找一些相关的信息。在看到这次训练营之前,我在网上关注到了rCore的教程。之前也自己跟着教程学习过一段,那次是只完成了前两章,实现了在终端打印彩色字符串。和本次做实验不同的是那次是从 cargo new r_core_study 开始的。所以那时候看到彩色的字符串输出,还是很有成就感的。

这是我之前学习的仓库连接:https://github.com/aLingYun/r-core-study

第一阶段

第一阶段的难度还好,可能是因为我之前有学习过Rust,所以前大半部分的题很快就做完了。后面涉及到Rust的一些复杂特性,做起来要慢很多。需要不断的查教程,理解教程。也是很高效的学习过程。

第二阶段

由于工作关系,第二阶段的第一周基本上什么也没有做。第二周也基本上只有每天晚上十点之后可以学习。

由于时间不较短,我的策略是先看实验题目。以实验题目为目标去看教程,所以速度还可以,但是对知识的理解程度应该会差一些。

完成lab1之后又陷入没有时间学习的窘境,知道第二周周末,我跟让我的家人带孩子。自己全身心的去做剩下的lab2和lab3两个实验。

那两天每天都是到凌晨1点以后。终于还是勉强完成code了,不过实验报告再也没时间写了。

总结

通过这次训练营的一二阶段,对Rust的理解加深了,尤其是对Rust在底层开发的应用。

之前自学时,第三阶段开始感觉到困难,一直没有过这关。这次训练营帮助我跨了这关,所以真的感谢各位老师的辛苦付出。

2023rcore第二阶段学习总结和个人与计算机系统的漫游

初识

rcore开源操作系统训练营 的相识,算是一个很偶然的机会吧。我与计算机结识很晚,在我上大学后,才从一个对计算机连打字都不会的人到慢慢熟练使用以及熟悉各种技术的人。与操作系统(Linux)结识,是大一下学期的 计算机系统基础课(教程是那本鼎鼎大名的 深入了解计算机系统),那节课开启了我Linux的漫游旅途。

兴趣

在我刚接触计算机的时候,一直认为开发出一个web网站或者APP,就是一件特别特别酷的事情。在整个刚接触计算机的事情,写出一个web网页或者APP便是我一直想要做的事。但后来,大一结束的暑假,学习了一些这方面的技术,扒开了web的真实面目,便慢慢失去了很多兴趣,曾经很酷的事情,突然感觉很无味了。所幸的是,在这个时候,学校的 OS 课开了,杨老师是一名非常知识渊博、热爱体系结构的老师,他 OS 第一门课留给我们的作业便是

下载linux内核源码,并往内核中添加自定义系统调用

这算是我开启了我正式与操作系统内核接触的旅途。永远无法忘却第一次下载linux内核源码,然后编译的时候,满屏报错的电脑界面,特别是每次编译的时候,都会让我等待很久,几乎每次都是编译了三十多分钟,然后给我报错,如此循环往复……最后终于把代码编译完成。第一次进入kernel 目录下,进入代码里面,映入眼帘的是 Linus Torvalds 的大名,那是我第二次那么激动(第一次激动的时候是第一次敲出 “Hello, World”)。最后在各种操作之下,各种文件之间来回修改的条件下,我终于让自己自定义的一个系统调用成功运行了起来,那一瞬间,像是打开了潘多拉的魔盒,从此我开始对体系结构、操作系统方向的东西产生了很大兴趣,便也萌生了写一个OS的想法,从此整个想法,便一直根深蒂固着。

遇见rcore

诚恳的说,我是因为心中那个根深蒂固的想法才会有机会遇见rcore,刚开始的时候,我其实知道的是 ucore, 后来因为个人非常喜欢c++,而某段的时间里,网上的各样信息都在告诉我 Rust 是c++的强大竞争者,Rust 是如何的安全,如何的高效。便萌发了我对这一门新型语言的兴趣。

Rust:

第一次用Rust的时候,它的cargo便惊艳了我很久,用c++的时候,每次安装第三方包,亦或是换个平台,编译东西,都会让我折磨很久,总是在各种编译器之间的实现困惑,msvc有的特性,在gcc有时候却无法运行,有时候在gcc能够运行的东西,在clang也无法运行。同样让人痛苦的时候,c++20/c++23都出了很久了,但是不同编译器的支持却是层出不穷……。换到Rust,突然很多东西便让人清爽了许多。也便逐渐开始了学习Rust的旅途。

risc-v:

对于risc-v的了解,在开始rcore之前,我也只知道它是开源的,文档内容少(远没有X86和Arm那样内容复杂和繁冗)。后来了解了一下龙芯,一生一芯等,便也对risc-v有了极大兴趣,恰逢此时,rcore便出现在了我面前。

rcore:

rcore Book 的娓娓道来,特别是以各种史前生物 来描述,增加了一番故事书的趣味。而Guide则能够快速地让我明白了代码地框架,每个文件,每个模块是什么样的功能。但无奈个人基础不好,所以大部分时间还是在看 Book。本次实验让我们实现操作系统核心的几个重要功能:

  1. 多到程序与分时多道任务
  • Lab1 需要完善系统调用。对于 sys_task_info 系统调用,我们在 TCP 添加相应字段处理即
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    pub fn sys_task_info(ti: *mut TaskInfo) -> isize {
    unsafe{
    *ti = TaskInfo{
    status:get_current_status(),
    syscall_times:get_syscall_times(),
    time : (get_time_us() - get_current_start_time()) / 1000

    };
    }
    0
    }
  1. 虚拟内存管理

    这部分的内容中,为 Rcore 引入了虚拟内存,为地址空间加上了一层抽象,# 地址空间
    刚学计算机的时候,个人非常总喜欢将所有代码放在一个文件里,觉得分开各种代码很是麻烦。后来因为学习深入,开始对分离代码,抽象多了很多体会。特别是在学计算机网络的TCP/IP模型和操作系统的时候,对抽象,加层的思想确实是不断体会,不断明白了那句话“在计算机中,没有什么是不能加一层解决不了的”。现在来好好感受在ch4中的抽象加一层。

    1. 为什么要添加一层抽象层:

      • 从应用开发的角度看,需要应用程序决定自己会被加载到哪个物理地址运行,需要直接访问真实的物理内存。这就要求应用开发者对于硬件的特性和使用方法有更多了解,产生额外的学习成本,也会为应用的开发和调试带来不便
      • 从内核的角度来看,将直接访问物理内存的权力下放到应用会使得它难以对应用程序的访存行为进行有效管理,已有的特权级机制亦无法阻止很多来自应用程序的恶意行为。
    2. 该抽象层要完成的目标:

      • 透明 :应用开发者可以不必了解底层真实物理内存的硬件细节,且在非必要时也不必关心内核的实现策略, 最小化他们的心智负担;

      • 高效 :这层抽象至少在大多数情况下不应带来过大的额外开销;

      • 安全 :这层抽象应该有效检测并阻止应用读写其他应用或内核的代码、数据等一系列恶意行为。

  2. 进程管理

    • 对于进程、程序、可执行文件等的了解更加深入了
      1. 进程是在操作系统管理下的程序的一次执行过程,程序是一个静态的概念。
      2. 可执行文件是一张“蓝图”:一张编译器解析源代码之后总结出的一张记载如何利用各种硬件资源进行一轮生产流程的 蓝图
      3. 加载同一个可执行文件的两个进程也是不同的:它们的启动时间、占据的硬件资源、输入数据均有可能是不同的,这些条件均会导致它们是不一样的执行过程。
      4. 对于创建进程需要fork()和exec()两个系统调用而不只是一个系统调用。两个组合更加灵活,fork是为了 exec 一个新应用提供空间,然后exec可以读取不同的elf文件,执行不同的操作。
  3. 文件系统(未完待续)

  4. 并发(未完待续)

但很可惜,因为个人基础和时间还有其他各种各样的原因,个人并没有完成五个实验,前面三个实验也只是勉强完成(虽然运行过了,但还是有很多东西之间还不明白)。接下来的时间,我将好好把先前没有弄明白的知识点再好好梳理一遍。并将继续做完还没有先前没有做完的工作。向训练营各位优秀的同学学习,以后要多写博客,多写博客(这次学到的一个优秀习惯),及时梳理知识。纸上得来终觉浅,绝知此事要躬行!!!。

rCore OS 学习总结

本次训练营是 Rust 与操作系统的有机结合。我对 Rust 已经足够熟悉,因此侧重操作系统部分的第二阶段更加吸引我。

从一个裸机程序开始,随着需求的复杂化,我们需要逐步添加各种系统调用的实现,最终完成了一个麻雀虽小五脏俱全的操作系统。

1. 应用程序与基本执行环境

操作系统和应用一样都是软件程序。区别在于,操作系统需要几乎直接与硬件交互,不像普通应用程序可以使用标准库提供的各种功能,毕竟标准库构建在系统调用上,而系统调用又是由操作系统提供的。

在操作系统与硬件之间还有一层 SBI (Supervisor Binary Interface)。在后续的编程实验中,我们直接使用编译好的 RustSBI BootLoader 二进制文件。

RustSBI 规定了 OS 在内存中的位置,那么编译 OS 时需要调整链接器使其生成符合要求的内存布局。

2. 批处理程序

操作系统最主要的作用就是运行应用程序,而且往往是多于一个应用。一种最直接的想法是把操作系统和多个应用打包在一起,输入计算机后,依次执行每一个应用。

我们希望当应用出错时,操作系统可以继续执行剩余的其他任务。由此特权级机制被引入。

3. 多道程序与分时多任务

一个一个运行应用不够灵活,我们希望能够对多任务进行调度。那么必然就需要能够支持任务切换,即保存/加载上下文。

调度则主要有两种情况:一是任务主动让出处理器,sys_yield;二是通过时钟中断定时触发任务的切换。

4. 地址空间

操作系统和应用全都直接访问物理地址通常是低效的。引入虚拟地址机制,让每个应用都拥有逻辑上连续的大量内存,可以使应用的编写更加灵活。

RISC-V 64 平台采用了 SV39 多级页表机制,通过 satp CSR 来控制是否启用。

5. 进程及进程管理

此前,所有任务都是操作系统直接管理的。我们很自然的会希望可以由任务来创建子任务,并且可以管理更多物理/虚拟资源。于是引入进程的概念。

理解进程,最核心的就是相关的系统调用:fork, execwaitpid

6. 文件系统与I/O重定向

文件可代表很多种不同类型的I/O 资源。狭义的文件系统可以对持久存储设备 (Persistent Storage) I/O 资源进行管理。

这一节,我们第一次尝试将一组功能从内核中分离出来成为独立的库,交由内核引入使用。

7. 进程间通信

进程间交换数据大大加强了程序的能力。实现进程间通信的主要方式之一是管道。

8. 并发

操作系统通过不断切换任务实现并发,因为进程间资源是相对隔离的,这种并发容易实现但开销较大。

我们想要在进程内,共享资源的情况下实现低开销的并发,这就引入了线程的概念。

一个进程的多个线程共享资源,但需要能互斥地访问资源,避免数据不一致。为此,我们可以用锁、信号量、条件变量等同步原语。

实验感想

rCore 主要目标还是实现一个 Unix-like 的操作系统,编程实验也基本都是实现 Linux 最关键的一些系统调用。

而 Unix/Linux 本就是与 C 语言一体的。这些 C 风格的系统调用接口,用 Rust 实现起来总是很奇怪,有种削足适履的感觉。

我在思考,如果抛弃这些 C 风格的接口,从 Rust 出发重新设计接口,或许可以更好地利用 Rust 的众多优秀特性。

学习总结

一直以来我对Rust表现出浓厚的兴趣,并且曾经写过一些小工具。在平时的学习中,我也尝试在操作系统方面进行一些探索,但一直没有找到Rust与操作系统如何结合的方法。

从编程语言的角度来看,C语言更像是对汇编语言的一层语法糖,它天然适合操作硬件功能的开发,各种硬件概念可以很好地使用C语言进行抽象。然而,如何使用Rust来完成相同的任务一直让我感到困惑。通过这次的培训,我对使用Rust语言编写操作系统有了初步了解,感觉非常不错。以下是我得出的一些简单结论:

  1. 内存安全:在编程活动中,内存安全问题往往难以调试且容易出现。当使用C语言时,不经意间引入内存安全问题的可能性很高,但在Rust中,由于有编译器的帮助,可以在编译阶段轻松地发现问题。

  2. 所有权系统和生命周期:它们帮助开发人员更好地控制变量的生命周期,尤其是全局变量。它迫使开发者思考如何使用和编写更安全的代码。

  3. 强类型系统:至少在安全代码的部分,可以很好地控制C语言中常见的运行时错误问题。

  4. 语言模块化设计:Rust的模块化设计使得能够更好地对系统进行抽象,将操作系统分解成各个组件,最后将它们组合在一起,使整体结构更加清晰。

  5. 文档系统和构建工具:工具如rustup和Cargo为文档查阅和工程构建提供了出色的基础设施服务。

当然,在学习过程中还遇到了一些问题,主要是因为对Rust语言本身的不够熟悉,有些用法与C语言仍存在较大区别,需要更多的练习和实践。Rust语言引入的高级功能实际上在使用上可能会带来一些成本,就像之前所提到的,C语言就像汇编的语法糖,可以直接操作硬件,而使用Rust则需要重新学习一些新的方法和技巧,学习曲线可能会较陡。

Lab总结

上周,我完成了前三个问题的编码工作,主要是体验了如何使用Rust编写操作系统,并发现了与C语言编写操作系统不同的地方。总的来说,这些都是操作系统的基础概念,在Rust语言的基础上进行了抽象和实现,我感受到了与C语言不同的体验。

关于使用Apple silicon mac完成rcore实验的方法

1.选择docker会比vmware更好

如果使用8g内存的m1 mac来做实验的话推荐使用docker来做,相较于vmware来说docker的内存占用会更低一些(8g内存开个虚拟机和vscode挂几个网页基本上就压力就变黄了,要是再挂着qq、微信可能会直接变红),另外我的vmwaretools安装了也无法正常使用,我想在虚拟机内使用宿主机代理折腾了几天也没能实现,但是在docker上就很容易成功,还有就是arm64linux的软件支持比较少(科学上网方面),docker上的环境配置见 https://docs.qq.com/doc/DWW1GZ3FQekx5dm9T 第24个

2.在mac上直接配置环境(不建议使用)

官方的文档里说可以直接在m1 mac上跑rcore,我成功跑通了,首先需要下载和编译riscvtools,
下载得科学上网并且非常耗费流量(我使用镜像失败了),编译这个过程得花费一到两个小时,我试过使用他们编译好的但是无法运行,具体步骤可以参考 https://cloud.tencent.com/developer/article/1939023  然后就是下载依赖 
brew install gawk gnu-sed gmp mpfr libmpc isl zlib expat
以及qemu建议选择qemu7.0.0(老版本qemu需要补丁 https://github.com/BASARANOMO/xv6-labs-2020/issues/1 )
接下来具体步骤可以参考实验书以及
https://risc-v-getting-started-guide.readthedocs.io/en/latest/linux-qemu.html#prerequisites
make这步由于mac上的make只有老版本所以应该得使用gmake
make -j$(nproc)   //这步需要先安装nproc,如果这步使用make报错了的话使用gmake
接下来就可以克隆仓库运行了,跑ch1没有任何问题但是我运行后面几个实验的时候经常会遇到报错,网上也没有解决办法,所以不建议使用这个方法

ch3

第一个实验要实现sys_task_info系统调用,首先在TaskControlBlock中添加syscall_times和start_time,同时在new中更新添加,接下来要实现系统调用次数信息的更新,我在内核的调度函数 run_next_task 中增加了一个简单的判断逻辑,以确定是否是进程的第一次被调度,并在需要时初始化 start_time。

ch4

第二个实验需要重写 sys_get_time 和 sys_task_info, sys_get_time 的主要功能是获取当前时间并填充传递给系统调用的 TimeVal 结构。首先获取时间戳,然后将其转化为秒和微秒,填充到 TimeVal 结构中,最后将数据复制到用户空间的 ts 指针所指向的内存区域。sys_task_info 用于获取当前任务的信息,包括任务状态、系统调用次数和任务运行时间。首先通过相关函数获取这些信息,然后填充到 TaskInfo 结构中,最后将数据复制到用户空间的 ti 指针所指向的内存区域。

ch5

第三个实验要实现 spawn 系统调用,首先,获取当前任务的控制块 task。使用 translated_str 函数将传递的路径 path 转化为字符串。检查是否可以找到与给定路径匹配的 ELF 数据,如果找到了 ELF 数据,就继续进行后续步骤,否则返回 -1。
用 ELF 数据创建新的内存集合,为新进程分配一个进程 ID(PID)和一个内核堆栈。接下来创建一个新的任务控制块,该控制块包含了新进程的信息,如 PID、内核堆栈、内存集合等。将新任务控制块添加到当前任务的子任务列表中,表示当前任务是新任务的父任务。最后,将新任务添加到任务管理器中,并返回新任务的 PID。关于stride调度算法实现我参考了
https://hangx-ma.github.io/2023/07/07/rcore-note-ch5.html

2023rCore训练营二阶段总结

序言

其实去年刚学 Rust 的时候就有看到这个训练营,感觉用 Rust 写操作系统很有意思,可惜当时没有任何操作系统和体系结构基础,有畏难情绪,所以没有参加。(现在看来其实当时就应该参加,rcore并没有想象中的那么难)

今年正巧在刚做完 xv6 的时候再次看到了这个训练营,想着继续巩固 OS 知识的同时还能重温半年多没写的 Rust,就报名参加了本次训练营。

可惜今年参加的时机并不怎么好,我本人已经找到了Android开发的实习,实习每天工作都挺忙的,只能周末抽时间看看录播,做做 lab。也许 Android 开发投入这么多精力学习 OS 算是有点不务正业,但我还是希望能够在本科阶段尽可能多的学习我感兴趣的 CS 知识。

ch3

ch3 还是很简单的,实现了一个系统调用计数和 task_info。所以这一个 lab 我的侧重于读实验源码。不得不说 rcore 跟 xv6 还是有很多不一样的地方的:

  • ch3 还没有涉及到进程的概念,只是简单区分了分时任务。在做 xv6 时其实我一直在疑惑多线程应当怎样实现,因为在 xv6 中只有进程,并且也只实现了进程的调度。rcore 中对分时任务的区分使我豁然开朗,其实一个线程就是一个分时任务,而进程并不是分时任务,而只是一个存放资源的结构体罢了。

  • ch3 的实验代码实现中并没有一个 shell 用户程序,而是直接将程序分段直接加载到物理内存中执行。这个操作我觉得其实就是在裸机程序的基础上做了一些改良,这么看来相比于 xv6,rcore 确实是在教你从零开始实现一个操作系统。

ch4

ch4 主要是学习了虚拟内存机制,其实之前学习 xv6 的时候就学得不太明白,这次算是整明白了。不过这个 mmap 相对于 xv6 那个 mmap 就比较简单了。让我比较惊讶的是 rcore 中能使用各种需要基于堆内存实现的容器(Vec,BTreeMap等)。

ch5

ch5其实难倒不难,就是需要把前面实验中实现的内容再实现一遍(直接 cherry-pick 代码不行,ch5 实验代码变化较大)感觉有点难受,并且遇到一些比较迷惑的问题时没有方法对源码进行调试(不知道是不是我没有找到,make debug似乎只能调试汇编代码)。

spawn 其实就是 fork + exec,并且不需要复制父进程的内存空间,直接 new 一个新进程的内存空间,设置一下父进程然后直接加载 elf ,搞定后再 add_task 即可。

023秋冬季开源操作系统训练营总结-lieck

在同学的推荐下,我报名参加了训练营。这次经历让我有了第一次通过代码来理解操作系统的感觉。在之前学校的课程中,我对操作系统的认识仅限于文字概念,如进程、页表和文件系统等。

在具体的实验过程中,因为是第一次编写这类 Lab,一开始感觉非常难,但是完成后巩固和掌握了很多 OS 的知识,并且在实践中得到了很大的收益。

Lab1

Lab1 需要完善系统调用。

对于 sys_task_info 系统调用,我们在 TCP 添加相应字段处理即可。

可能存在精度问题,这里我使用了 get_time_us 计算时间。

1
2
3
4
let us = get_time_us();
let sec = us / 1_000_000;
let usec = us % 1_000_000;
let t = (sec & 0xffff) * 1000 + usec / 1000;

Lab2

这部分的内容中,为 Rcore 引入了虚拟内存。

因为 Rcore 中分为内核页表和用户态页表,因此对于sys_task_info 系统调用我们不能直接通过修改参数来完成传值。需要将其转换为物理地址,而内核页表中的虚拟地址和内核地址是对应的。

然后是实现 MMap,通过 VMA 实现。

VMA 记录有关连续虚拟内存地址段信息。对每个 section ,都有一个 VMA 对象。

例如对于 memory mapped file,存在一个 VMA 与之对应,其中包含了文件信息等

mmap 收到范围和 port 后,判断是否冲突或参数错误,然后放入 VMA 数据结构中映射物理页。mummap 也是类似的操作。

在此实验中,测试数据稍弱,并没有要求实现 VMA 分裂的操作。

Lab3

Lab3 需要实现优先级调度和 spawn 系统调用。

spawn 系统调用是 fork 和 exec 的结合。可以分为两部分:

  1. 参考 fork 创建新的进程,但新进程执行的首个函数的调用 exec 的操作
  2. fork 后的子进程执行的第一个操作,用于调用 task.exec

优先级调度较为简单,在 PCB 中维护 stridepass

  • 调用 suspend_current_and_run_next 时增加当前进程的 stride
  • 调用 fetch 会选择下一个要运行的进程,找到当前 stride 最小的进程即可。

因为不要求性能,我们可以简单的遍历的选择当前 stride 的值。