References
OS Tutorial Summer of Code 2020 详情页面
OS Tutorial Summer of Code 2020 每日学习实践过程记录
我的Daily Schedule:https://github.com/stellarkey/os_summer_project
Rust
“与编译器斗智斗勇……”
https://doc.rust-lang.org/std/index.html Rust官方文档
https://doc.rust-lang.org/reference/introduction.html 语义教程
https://doc.rust-lang.org/1.4.0/book/README.html 抽象功能教程
Rust编程视频教程(基础)–令狐壹冲,Rust编程视频教程(进阶)–令狐壹冲
Tour of Rust 制作太精美辣
Small exercises to get you used to reading and writing Rust code!
简介
rust的通用设计哲学:内存安全、零成本抽象、实用性。
rust采纳了多种语言(C++/Python/Haskell/Ruby…)的特性。
rust语言是一种高效、安全的自动化内存管理的语言。因此非常适合用来编写操作系统。
早期操作系统是用汇编语言编写,后来采用C语言,再后来混合编程……
系统编程语言:用于构建控制底层计算机硬件的软件系统,并提供由用于构建应用程序和服务的更高级应用程序编程语言使用的软件平台。开发操作系统的系统编程语言很多;还离不开汇编语言。
比如:MIT用Go语言开发了Biscuit OS。Stanford用Rust语言开发了tock OS。
环境安装
https://kaisery.github.io/trpl-zh-cn/ch01-01-installation.html
其他安装方法:https://prev.rust-lang.org/zh-CN/other-installers.html#standalone(如:**独立安装程序**)
在Here进行Windows安装程序下载。感觉网络十分不稳定。因为是cmd终端集成式安装,如果要开启代理需要在终端内另外自行开启。
利用热点安装完毕。
linux下安装:https://www.linuxidc.com/Linux/2019-01/156211.htm
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
之后如果要编写操作系统相关内容,那么linux环境可能是比较必要的。
最后暂时选择了在windows下构建环境。
在运行前设置命令行中的科学上网代理:https://rcore-os.github.io/rCore-Tutorial-deploy/docs/pre-lab/env.html
linux:(注意端口与实践软件对应)
# e.g. Shadowsocks 代理 export https_proxy=http://127.0.0.1:1080 export http_proxy=http://127.0.0.1:1080 export ftp_proxy=http://127.0.0.1:1080
更新:
1 | $ rustup update |
卸载:
1 | $ rustup self uninstall |
检查:(版本检查,检测是否安装成功)
1 | $ rustc --version |
rust的版本分为stable、beta、nightly。(从左到右稳定性下降,前沿性加强)
Docker
先不用Docker了。感觉不是很理想。
先用VMWare吧。
集成开发环境
使用VSCode即可。主要是具有rust语法高亮、补全等功能。
如果碰到RLS,则指Rust Language Server,是官方提供的一个标准化的编辑器增强工具。VSCode中集成了这个开源项目,但理论上RLS可以和任何编辑器或IDE配合使用,只要实现它们之间的通信协议即可。
关于C++生成工具
https://blog.csdn.net/coolsoloist/java/article/details/106425656
注意对于windows需要额外安装:
Visual Studio 2019 生成工具Visual Studio 2019 build tools或者微软Visual Studio。下载build tools之后,在Visual studio installer中只选择“C++生成工具”,然后只选择”MSVC v142 - VS2019 C++ x64/x86 生成工具”。安装上去大约需要1.8G硬盘空间。
Hello World
https://kaisery.github.io/trpl-zh-cn/ch01-02-hello-world.html
可以利用VSCode内的终端实现调试。
Hello Cargo
https://kaisery.github.io/trpl-zh-cn/ch01-03-hello-cargo.html
cargo管理项目确实方便。
1 | $ cargo new hello_cargo # 新建项目 |
统一在
src
文件夹中存储代码。Cargo.toml
文件保存开发者信息。
运行其他基于cargo的开源项目:
1 | $ git clone someurl.com/someproject |
工具链
From https://www.bilibili.com/video/BV1ti4y1b7xy, Yuzhuo Jing
Rustc
Rustc:友好的编译检查。
Cargo
Cargo doc
在代码中的///
三斜杠注释(支持Markdown语法),会自动形成文档。
1 | $ cargo doc |
Cargo doctest
在注释中提供测例。
1 | $ cargo test |
Cargo bench
性能测试。
1 | $ cargo bench |
Clippy
Clippy:提供更多代码优化信息。
Rustfmt
format格式化。一键修改代码风格(可自定义)。
Rustup
工具链管理工具。类似于python的anaconda环境管理工具。
Empowering Ecosystem
crate.io
类似python的pip。
Serde
序列化和反序列化工具。
Structopt
结构体模板参数初始化。
Rayon
提高性能(自动多核并发)。
Rocket
网络编程,安全检查。如下图,要求对path进行检查,防止越界。
基本语法纪要
元素
rust的基本元素的昵称相对都比较简洁。
- signed integers:
i8
,i16
,i32
,i64
,i128
andisize
(pointer size)- unsigned integers:
u8
,u16
,u32
,u64
,u128
andusize
(pointer size)- floating point:
f32
,f64
char
Unicode scalar values like'a'
,'α'
and'∞'
(4 bytes each)bool
eithertrue
orfalse
- and the unit type
()
, whose only possible value is an empty tuple:()
比如:(变量必须先声明,这跟python不同)
1 | let a = true; |
跟python的显式指定类型比较像。如果不指定类型,则会自动推断,选择默认类型。
字符串
Rust 语言提供了两种字符串
- 字符串字面量
&str
。它是 Rust 核心内置的数据类型。 - 字符串对象
String
。它不是 Rust 核心的一部分,只是 Rust 标准库中的一个 公开pub
结构体。
集群
rust的序列单位跟python也类似。
- arrays like
[1, 2, 3]
- tuples like
(1, true)
1 | // Fixed-size array (type signature is superfluous) |
迭代器
迭代器是专门针对集群型对象使用的,可以自动地对集群中的每个对象产生作用。
一个迭代器是惰性的,即不使用无消耗。
迭代器的创建:
1 | let v1 = vec![1, 2, 3]; |
迭代器的遍历方法:
1 | for val in v1_iter { // 遍历 |
迭代器适配器
迭代器可以结合迭代器适配器(iterator adaptors)将当前迭代器变为不同类型的迭代器。
1 | fn main() { |
消费适配器
迭代器可以使用一系列消费适配器(consuming adaptors)来获取一个集群上的计算结果。
一次性消费:在消费后迭代器失效,其所有权被转移给消费适配器。
1 | fn iterator_sum() { |
1 | fn main() { |
结构
提供struct和enum。
枚举
https://www.twle.cn/c/yufei/rust/rust-basic-enums.html
注意:枚举是多选一的!
Rust 语言核心和标准库内置了很多枚举,其中有一个枚举我们会经常和它打交道,那就是 Option
枚举。
Option
枚举代表了那种 可有可无 的选项。它有两个枚举值 None
和 Some(T)
。
None
表示可有可无中的 无。Some(T)
表示可有可无中的 有,既然有,那么就一定有值,也就是一定有数据类型,那个 T 就表示有值时的值数据类型。
取出Some值:
unwrap()
:https://www.jianshu.com/p/0fe7435dd40a
作用域
rust使用一对{}
来创建(词法)作用域。在作用域内可以进行局部函数式的操作。
1 | fn main(){ |
作用域的返回值为最后一个不加分号的语句或者单位值。
闭包
在有了作用域以后,可以讨论闭包。闭包相当于匿名函数,或者类似结构体函数。它们都通过对象来实现函数的功能。闭包是函数式编程语言常常会用到的。
闭包的语法如下:
1 | let closure = |para| { |
|...|
为闭包参数表,紧跟一个作用域(相当于函数体),整体声明与变量声明格式一致。
闭关的语法还有进一步的略写:
1 | fn add_one_v1 (x: u32) -> u32 { x + 1 } # 一个【函数】定义 |
闭包可以捕获环境(意味着可以使用同一作用域内的变量),而函数不能。
1 | fn main() { |
宏
宏的语法:(可以发现宏、函数和闭包的调用是很相似的)
1 | // 这是一个简单的宏,名为 `say_hello`。 |
为什么宏是有用的?
- 不写重复代码(DRY,Don’t repeat yourself.)。很多时候你需要在一些地方针对不同 的类型实现类似的功能,这时常常可以使用宏来避免重复代码。
- 领域专用语言(DSL,domain-specific language)。宏允许你为特定的目的创造特定的语法。
- 可变接口(variadic interface)。有时你需要能够接受不定数目参数的接口,比如
println!
,根据格式化字符串的不同,它需要接受任意多的参数。
增加了自由抽象的能力。
1 | let mut a = 1; |
rustlings(练习项目)
需要完成这个项目中的exercise。
基本操作
1 | $ rustlings watch # 实时观察exercise代码(默认按初学者顺序) |
练习记录
首次rustling的界面记录如下:
可以比较清晰地发现代码对应的位置以及出错原因。
改正错误后界面变为:
非常人性化的设计,只需在VSCode中实时修改代码,便可以实现练习的目的。删掉// I AM NOT DONE
语句就可以进入下一个练习。
变量覆盖
Variable4还是比较容易忘记的知识点。
常数声明
You declare constants using the
const
keyword instead of thelet
keyword, and the type of the value must be annotated.
Slice截取
元组下标引用
模块内宏引用
变量初始化
变量的修改必须有初始化。
浮点比较
浮点数不能使用==
,rust对此做了智能的编译提示。
枚举实现链表
Box是指针类型。
1 | pub enum List { |
Complete :)
Rust编程练习
选取Leetcode相关题目。
两数之和
给定一个整数数组 nums 和一个目标值 target,请你在该数组中找出和为目标值的那 两个 整数,并返回他们的数组下标。
你可以假设每种输入只会对应一个答案。但是,数组中同一个元素不能使用两遍。
Python
1 | class Solution: |
Rust
1 | impl Solution { |
发现这个系列的题目有些rust写起来很难。。先做些简单题水一水。。
汉明距离
两个整数之间的汉明距离指的是这两个数字对应二进制位不同的位置的数目。
给出两个整数 x
和 y
,计算它们之间的汉明距离。
Python
1 | class Solution: |
Rust
1 | impl Solution { |
只出现一次的数字
给定一个非空整数数组,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素。
Python
1 | class Solution: |
Rust
1 | impl Solution { |
多数元素
给定一个大小为 n 的数组,找到其中的多数元素。多数元素是指在数组中出现次数大于 ⌊ n/2 ⌋ 的元素。
你可以假设数组是非空的,并且给定的数组总是存在多数元素。
C++
1 | class Solution { |
Rust
1 | impl Solution { |
合并两个有序链表**
将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
Python
1 | # Definition for singly-linked list. |
Rust
rust的链表真复杂。慢慢感觉到rust为了安全性在编程上造就的巨大门槛,这无疑是牺牲。
参考这个写法,模式匹配大法好。
as_mut()
:https://doc.rust-lang.org/std/option/enum.Option.html#method.as_mutConverts from
&mut Option<T>
toOption<&mut T>
.
1 | // Definition for singly-linked list. |
移动零
给定一个数组 nums,编写一个函数将所有 0 移动到数组的末尾,同时保持非零元素的相对顺序。
示例:
输入: [0,1,0,3,12]
输出: [1,3,12,0,0]
说明:
必须在原数组上操作,不能拷贝额外的数组。
尽量减少操作次数。
Python
1 | class Solution: |
Rust
1 | impl Solution { |
找到所有数组中消失的数字
https://leetcode-cn.com/problems/find-all-numbers-disappeared-in-an-array/
给定一个范围在 1 ≤ a[i] ≤ n ( n = 数组大小 ) 的 整型数组,数组中的元素一些出现了两次,另一些只出现一次。
找到所有在 [1, n] 范围之间没有出现在数组中的数字。
您能在不使用额外空间且时间复杂度为O(n)的情况下完成这个任务吗? 你可以假定返回的数组不算在额外空间内。
示例:
输入:
[4,3,2,7,8,2,3,1]
输出:
[5,6]
Python
1 | class Solution: |
Rust
1 | impl Solution { |
动态和
给你一个数组 nums 。数组「动态和」的计算公式为:runningSum[i] = sum(nums[0]…nums[i]) 。
请返回 nums 的动态和。
Python
1 | class Solution: |
Rust
1 | impl Solution { |
又水了一道题~
好数对的数目
给你一个整数数组 nums 。
如果一组数字 (i,j) 满足 nums[i] == nums[j] 且 i < j ,就可以认为这是一组 好数对 。
返回好数对的数目。
Python
1 | class Solution: |
Rust
1 | impl Solution { |
重新排列数组
给你一个数组 nums ,数组中有 2n 个元素,按 [x1,x2,…,xn,y1,y2,…,yn] 的格式排列。
请你将数组按 [x1,y1,x2,y2,…,xn,yn] 格式重新排列,返回重排后的数组。
示例 1:
输入:nums = [2,5,1,3,4,7], n = 3
输出:[2,3,5,4,1,7]
解释:由于 x1=2, x2=5, x3=1, y1=3, y2=4, y3=7 ,所以答案为 [2,3,5,4,1,7]
示例 2:
输入:nums = [1,2,3,4,4,3,2,1], n = 4
输出:[1,4,2,3,3,2,4,1]
示例 3:
输入:nums = [1,1,2,2], n = 2
输出:[1,2,1,2]
Python
1 | class Solution: |
Rust
1 | impl Solution { |
回文数
判断一个整数是否是回文数。回文数是指正序(从左向右)和倒序(从右向左)读都是一样的整数。
Python
1 | class Solution: |
Rust
1 | impl Solution { |
整数反转
给出一个 32 位的有符号整数,你需要将这个整数中每位上的数字进行反转。
Python
1 | class Solution: |
Rust
1 | impl Solution { |
最大子序和
给定一个整数数组 nums
,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
Python
1 | class Solution: |
Rust
1 | impl Solution { |
爬楼梯
假设你正在爬楼梯。需要 n 阶你才能到达楼顶。
每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?
注意:给定 n 是一个正整数。
示例 1:
输入: 2
输出: 2
解释: 有两种方法可以爬到楼顶。
- 1 阶 + 1 阶
- 2 阶
示例 2:
输入: 3
输出: 3
解释: 有三种方法可以爬到楼顶。
- 1 阶 + 1 阶 + 1 阶
- 1 阶 + 2 阶
- 2 阶 + 1 阶
Python
1 | class Solution: |
Rust
1 | impl Solution { |
杨辉三角
给定一个非负整数 numRows,*生成杨辉三角的前 *numRows 行。
在杨辉三角中,每个数是它左上方和右上方的数的和。
Rust
用组合数的思路做了做,发现想多了。。。TLE!
1 | impl Solution { |
接下来用杨辉三角生成的方法:
1 | impl Solution { |
Python
1 | class Solution: |
OS实习第一次交流会
陈向群老师:不忘初心啊!自我管理。完善学习方法。踏实,做好记录,善于总结。
向勇老师:提供了本课程的调查问卷数据分析。去鹏城实验室的概率波动较大,但线上全程参与是完全可行的。只要自己有收获就行了。
李睿老师:鹏城实验室负责老师。两位助教目前正在实地配置环境。5位分享同学。
方澳阳:之前没有了解过。有一定计组基础。开始整天赶进度学习。后面改为每日6小时学习。语法改错,编译器比较智能。然后开始RISC-V的相关学习。语法、理论、实践交替进行。
林可:刚刚入坑。C语言和汇编有一定经验。多线程有基础。看过一些OS资料。每天投入6-7个小时。按自己的节奏来学习。不要着急。
蒋周齐(洛佳):rust社区资深人士。rust有特殊的难度,需要花时间克服。rust运行时小,性能好。有rust嵌入式方向的经验。做操作系统方面经验有限。以前翻译过《用rust写操作系统》。rust语言一直在迭代。rust的宏十分强大灵活。这几天主要在看OS的代码。写OS特别需要调试代码的技巧。
卢弘毅:准备了一个经验分享pdf。Lab的准备内容(rust、RISC-V、OS、github)。lab教程代码一定要敲一遍。多多在github上提issue和PR。单元测试。警惕自动化,rust有相当多特性,但有时会在debug时带来很多困扰。
车春池:rust语言集中看了几天后,开始交替学习。RISV-V某文档第十章特权架构(Manual)。LAB1没有实现中断描述符。在rcore中实现了IDT的数据结构。在实践中理解rust语言。
张汉东老师:rust语言分享。一定要有整体的把握。rust语言集众家之长(基于类型系统的集成)。解决未来互联网的安全问题(类型安全)。C++性能高,但类型不安全(如何理解编程语言的类型安全性?)。Safe rust相比于Safe C的安全性更显然、易差错(不会出现未定义行为)。trait是类型行为的抽象。字符串的设计(为什么这么多种字符串)也是为了保证类型安全,比如&str
胖指针。合类型(enum)与集类型(struct)。【讲的比较细节,没跟上】
王润基:第二阶段zCore的助教。18年开始探索rust写OS。一定要边做边学。勇敢地面对Unsafe。生命周期比较头疼,不要死磕。rust不鼓励全局变量。
吴一凡:rust现代化、易用。边做实验边学习,重点学习系统是如何搭起来的,一开始不用纠结具体实现。多总结,在不同场景下的应用等等。
最后总结了一下第二阶段的相关安排。
RISC-V
“保持简洁,保持功能单一”
RISC-V手册:一本开源指令集的指南 重点是第10章
RIS-V特权指令级规范 重点是与OS相关的特权硬件访问的内容
Berkeley CS61C: Great Ideas in Computer Architecture (Machine Structures)
注意:RISC-V的V
是5的意思。读作:risk-five
。(第五代精简指令集)
简介
商业和生态的区别更多,技术上差异不大。
ARM低功耗,手机上用的多;Intel历史遗留包袱较多。RISC设计更现代,符合操作系统的需求。
RISC发明者是美国加州大学伯克利分校教师David Patterson。
2010年左右开始立项RISC-V,到2015年开始技术和商业上的尝试:
- 技术方向:成立RISC-V基金会,维护指令集架构的完整性和非碎片化。
- 商业方向:成立SiFive公司,推动RISC-V的商业化。
Chisel 硬件架构语言
https://www.chisel-lang.org/index.html
Chisel is a hardware design language that facilitates advanced circuit generation and design reuse for both ASIC and FPGA digital logic designs. Chisel adds hardware construction primitives to the Scala programming language, providing designers with the power of a modern programming language to write complex, parameterizable circuit generators that produce synthesizable Verilog. This generator methodology enables the creation of re-usable components and libraries, such as the FIFO queue and arbiters in the Chisel Standard Library, raising the level of abstraction in design while retaining fine-grained control.
Chisel语言(Constructing Hardware in a Scala Embedded Language,硬件构建语言)。
数字电路设计一般使用verilog(HDL)语言进行开发。
Chisel语言则对上述设计过程进行了一个的优化,用来解决传统硬件设计反馈太慢的问题。
寄存器
RISC-V具有32个寄存器x0x31,其中通用寄存器x1x31;寄存器x0恒为0。
CSR 控制状态寄存器
RISC-V设置CSR(控制状态寄存器)实现隔离:
- 防止应用程序访问设备和敏感的CPU寄存器
- 例如地址空间配置寄存器
页表结构
RISC-V的页表结构也相对来说更加整齐,没有历史包袱。
特权架构
RISC-V采用四个特权级(Ring):
- User (U-mode):
00
。用户/应用程序。最低。 - Supervisor (S-mode):
01
。管理员。 - Hypervisor (H-mode):
10
。Hypervisor。 - Machine (M-mode):
11
。机器。最高。
RISC-V更好地定义了各个层次之间的关系:
高特权模式下的软件授权低特权模式软件处理中断。(x86实现用户态应用程序来响应中断很不方便)
甚至可以实现用户态中断。(信号机制的实现将非常优美)
这部分工作仍然在不断演变中。
语法
其实跟MIPS差不多。。没细看。。
OS实习第二次交流会
开场就放了鹏城实验室的宣传片。。
鹏城实验室杨沙洲老师介绍鹏程实验室系统方面的研究现状
立项两年。以开源作为总的思路。
三款芯片
目前设计了三款芯片:
以上仍然是一个非常传统的架构,没有太多的改进。目前只是一个复现。已经送厂,下个月可以出实体。
这个项目19年年底开始。目前基本上也是照搬Ariane项目。跑起来以后,做了一些简单的性能测试。同时还在增加其他的功能。总的来说是作为视频编码的控制器来设计的。可以做更多的工作,功耗也可以较低。
整个项目是一个生态项目,因此支持国产工艺。但目前技术还不成熟。
在RISC-V芯片里面还算是佼佼者。该芯片的性能与ARM的A72、A76比较接近。
CPU研发环境
OS研发环境
三个研究方向
- Many-Cores(多核、众核)
- 基于OpenPiton正在学习
- 可能会应用到汽车、飞行器等多传感器的场景
- Virtualization(虚拟化)
- 跟进Hypervisor Extension
- CPU里面一般需要实现环境的隔离即可
- 目前还在观望是否有人实现了支持虚拟化的RISC-V芯片
- Interrupts
- 跟进User-level Interrupts(“N”扩展)
- User-level Interrupts:用户态直接接受中断。目前公开的CPU还没有实现。
- 硬件设计上没有太大的难度
- 软件如何去响应这个中断——这方面没有支持
- 好处:1. 信号机制大大简化,提高性能;2. 可以把很多driver放到用户态去执行,不需要转发(把一些不必要的东西放到用户态,因为不需要管理资源);用户进程所在的空间不一定存在,需要一些新的设计确保存在(安全性)。
- 硬件上不做,如果只是软件上去实现其实意义不大。
国科大蒋德钧老师介绍基于RISC-V的OS本科课程教学情况
国科大也弄RISC-V了。。2016年才开始招收本科生。在课程当中使用C语言编写。
(长的好像树莓派。。
然后就是介绍具体的每个部件(bootloader、IPC、需内管理……)。
一些实验注意事项:
- RISC-V的BBL(Berkeley Boot Loader)不支持SD卡读写。
- 需要自行修改设计
- RISC-V中断常常需要sbi调用
- RISC-V C ABI规定SP需要128位对齐
- 内核镜像大于64个secotrs需要多次读取(读SD卡有单次限制)
- 虚存用于标记进程ID的ASID位虽然官方文档有,但是实际上并没有实现
- 页表中的A和D两位,对于不同的环境可能有自动设置,也可能没有
- 虚存开启后的地址模式:
- Machine态必须是实地址
- supervisor和user态都是虚地址
涂轶翔助教介绍rcore tutorial的lab实验练习内容
涂轶翔:目前rcore tutorial第三版的维护者之一。来自于OS课程实验。
具体部署项目见:https://github.com/rcore-os/rCore-Tutorial-deploy
参与项目可以到:https://github.com/rcore-os/rCore-Tutorial
部分实习学生介绍学习经验和体会&学生与助教老师的问答
姚宇飞:根据教程最终的rcore代码实现了一些新功能。
李宇:跨专业考研考生。线段树实验似乎很难。
张文龙:做了一些PAT上的题目。
郑昱笙:。。(没听清)
华中大二:进度慢。慢慢来。不慌。
rCore
视频:半个世纪过去了,是时候用Rust重写操作系统了吗?(CC字幕)
视频+PPT:金枪鱼之夜:陈嘉杰同学介绍 rCore v0.2.0 实现历程和进展, 2019
zCore操作系统内核的设计与实现,潘庆霖本科毕设论文,2020
Redox 开源界完成度最高的RustOS
简介
rCore是用Rust语言实现的小型操作系统。
- 兼容Alpine Linux(musl libc):Busybox,GCC,Nginx……
- 支持四种指令集:x86_64,ARM64,RISC-V,MIPS32。
rCore社区:https://github.com/rcore-os。
uCore回顾
uCore是C语言实现的小型操作系统。主要参考了:xv6(xv6中文文档),OS161,Linux。分为两个版本:
- uCore Lab:用于OS课程实验
- uCore Plus:用于OS课程设计
从uCore到rCore
C语言:内存不安全(SegmentFault),缺少现代语言特性和好用的工具链。
而Rust:内存+线程安全,高层语言特性,友好的工具链,蓬勃发展的社区生态。
在此项目(rCore)开始时,开源界已经有不少RustOS的项目:
- Redox:这是目前完成度最高的RustOS,微内核架构,平台x86_64
- 《Writing an OS in Rust》& blog_os:这是一个从零开始写RustOS的教程,平台x86_64
- rv6:这是一个xv6的Rust移植,然而它止步于内存管理,并且是完全C风格的
- CS140e:这是斯坦福2018年新开的实验性课程,用Rust写的教学OS,平台arm/RaspberryPi
反思rCore
经验杂谈
环境安装
https://rcore-os.github.io/rCore-Tutorial-deploy/docs/pre-lab/env.html
linux环境安装总览:(在这之前先将WSL2和Ubuntu环境装好)
1 | # # bash功能增强(可选,注意!!!发现此功能会导致rustup环境变量索引失效。建议先不使用。) |
Windows WSL && Ubuntu
Dev on Windows with WSL - 在 Windows 上用 WSL 优雅开发
https://www.jianshu.com/p/3e627ff45ccb
Windows Subsystem for Linux(简称WSL)是一个为在Windows 10上能够原生运行Linux二进制可执行文件(ELF格式)的兼容层。它是由微软与Canonical公司合作开发,目标是使纯正的Ubuntu 14.04 “Trusty Tahr”映像能下载和解压到用户的本地计算机,并且映像内的工具和实用工具能在此子系统上原生运行。
我们简单的认为它在 Windows 上安装了一个 Linux 环境就好了。
WSL(Windows Subsystem for Linux)是指 Windows 下构建 Linux 环境。你可以在使用 Windows 的同时,方便地进行 Linux 下的开发,并且 Linux 子系统上可以访问 Windows 的文件系统。但是,WSL 在安装rust时会出现环境配置方面的问题,因此这里我们采用新版的 WSL,即 WSL 2。
WSL 2 和 Ubuntu 环境安装步骤:( https://docs.microsoft.com/en-us/windows/wsl/install-win10)
- 升级 Windows 10 到最新版( Windows 10 版本 18917 或以后的内部版本)
- 如果不是 Windows 10 专业版,可能需要手动更新,在微软官网上下载。否则,可能 WSL 功能不能启动。
- 在 Powershell 中输入
winver
查看内部版本号。
- 「Windows 设置 > 更新和安全 > Windows 预览体验计划」处选择加入,Dev开发者模式
- 打开 PowerShell 终端(管理员),输入:
1 | # 启用windows功能:“适用于Linux的Windows子系统” |
- 如果先装了Ubuntu,则运行:
1 | # <Distro>改为对应版本名,比如: `wsl --set-version Ubuntu 2` |
- 在微软商店(Microsoft Store)中搜索 Ubuntu,安装第一个(或者你想要的版本)
- 在 此处 下载 WSL 2 的 Linux 内核更新包
- 安装完成后,打开 Ubuntu,进行初始化
- 回到 PowerShell 终端,输入:
1 | # 查看 WSL 的版本是否为 2 |
- 若得到的版本信息正确,结束。WSL 2 和 Ubuntu 环境安装完毕。
在构建完成 WSL 2 + Ubuntu 环境后,可以在 Windows 的 Linux 子系统下便捷地部署 Linux 环境。
注意为了装rust,须启用WSL 2!!!(https://docs.microsoft.com/zh-cn/windows/wsl/install-win10)
详见 rust工具链 小节。
微软商店加载页面失败:https://jingyan.baidu.com/article/c45ad29cf41577441753e2db.html。
如果想在 Linux 查看其他分区,WSL 将其它盘符挂载在
/mnt
下。如果想在 Windows 下查看 WSL 文件位置,文件位置在:
C:\Users\用户名\AppData\Local\Packages\CanonicalGroupLimited.Ubuntu18.04onWindows_79rhkp1fndgsc\LocalState\rootfs
下。
WSL的linux命令使用似乎与纯ubuntu有些差异:
1 | $ ls ./ # 查看当前目录下内容。在根目录须使用 ls / |
为了方便访问windows磁盘文件夹,可能需要创建一个或多个软链接放到根目录。
比如:sudo ln -s /mnt/c/Users/your_name/Desktop/ /
。
Windows Terminal
https://dowww.spencerwoo.com/1.1/2-cli/2-1-terminal.html#windows-terminal
WSL项目组开发,可以方面统一管理WSL、Power Shell、Command Prompt等环境。
Windows Terminal 已经可以从 Microsoft Store 中直接下载。
使用该软件还可以解决WSL的配色问题。
https://github.com/microsoft/WSL/issues/5092
https://github.com/microsoft/WSL/issues/4904 因为更新导致的4294967295
solve “process exited with code 4294967295“ , run netsh winsock reset
as Administrator, then reboot your computer.
The result like below:
1 | ❯ netsh winsock reset |
bash / zsh / fish
下载安装的 Windows Subsystem for Linux 默认就是 bash
的 Shell 环境。bash
是 Unix shell 的一种,是我们开发环境的基础。不过 bash
本身仅提供一个非常基础的命令行交互功能,没有类似 zsh
或 fish
等 Shell 的自动补全、命令提示等高阶功能。
zsh
和 fish
,都是 Unix-like 系统中不可或缺的好 Shell,它们都极大的拓展了我们命令行界面的交互体验。在命令行的世界中:
fish
更加注重「开箱即用」的体验,让我们安装完成即拥有一个包含了命令高亮、自动补全等强大功能的 Shell 环境zsh
则更加重视拓展性,借助于社区中优秀的zsh
插件系统 oh-my-zsh 以及无数优秀的插件,zsh
同样能有比肩fish
甚至比fish
更高阶的功能和体验
1 | sudo apt install zsh |
zsh还需要单独安装自定义扩展才能达到较好的效果。也可以安装fish:
1 | sudo apt install fish |
切换回bash:
1 | chsh -s /bin/bash |
此功能会导致rustup索引失效。(https://github.com/rust-lang/rustup/issues/686,https://github.com/rust-lang/vscode-rust/issues/675)
- 解决方案(手动添加rust到环境变量):
export PATH="$HOME/.cargo/bin:$PATH"
- 注意:export只能临时生效。需要修改环境变量文件。(设置环境变量永久生效和临时生效 export PS1)
- linux下ls、pwd等命令显示command not found
- 太烦了,
fish这什么辣鸡语法,逼着我用zsh。 - 还是zsh好,添加环境变量的语法跟bash一致。省得折腾。
XServer for windows(可选)
https://dowww.spencerwoo.com/1.1/4-advanced/4-1-gui.html#%E5%AE%89%E8%A3%85-xserver-for-windows GUI 图形化界面
首先安装:VcXsrv Windows X Server 。并按上述GUI教程配置打开。
1 | sudo apt-get remove --purge openssh-server |
对于IP地址的设置:https://zhuanlan.zhihu.com/p/51270874。
VSCode
1 | curl https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor > microsoft.gpg |
VSCode命令行:https://code.visualstudio.com/docs/editor/command-line
code -h
获得帮助。
- Ctrl+Shift+p,在搜索框中输入“configure display language”
- 选择安装更多语言
->
中文简体 - vs code中使用remote wsl中文乱码问题
Skip WLS check if env var DONT_PROMPT_WSL_INSTALL is set.:
- VSCode启动时总是有提示,专门去分析了一下此处的源码。了解了grep命令和if的用法:
- 对于
if grep -qi Microsoft /proc/version && [ -z "$DONT_PROMPT_WSL_INSTALL" ]; then
grep -qi Microsoft /proc/version
:模式匹配,若在文件vesion中搜索到Microsoft则为true。不区分大小写。[ -z "$DONT_PROMPT_WSL_INSTALL" ]
:當 $str 是 null, 回傳 true.
- 修改并添加了环境变量
DONT_PROMPT_WSL_INSTALL=233
。没有起到预想中的效果。暂时放弃。 - 卸掉完事。
事实证明,根本不需要额外安装WSL里面的VSCode,WSL可以自动启动windows里面已经装好的VSCode!
qemu
下载地址:https://qemu.weilnetz.de/w64/
windows安装后须配置环境变量:将安装目录添加到path
中。
运行:
1 | qemu-system-riscv64 --version |
表明RISC-V 64 虚拟器安装成功。
linux版本按照实验指导书安装即可。
若tar.xz文件下载较慢,可以在https://download.qemu.org/手动科学下载。
ERROR: "cc" either does not exist or does not work
:
- 说明没有安装
gcc
。(https://blog.csdn.net/feiyangyongran/article/details/46414517)- 更换ubuntu软件源镜像:Here
- 运行:
sudo apt install gcc
接下来可能还会有一堆not found和required。按提示依次
sudo apt install ...
即可。
ERROR: glib-2.40 gthread-2.0 is required to compile
:
- 使用
apt-cache search glib2
查看应该安装哪个库。(https://blog.csdn.net/fuxy3/article/details/104732541)sudo apt-get install libglib2.0-dev
- 注:新版ubuntu(Ubuntu 16.04)引入了
apt
代替apt-get
命令
- 注:新版ubuntu(Ubuntu 16.04)引入了
- 注 - 软件包查找方法:
apt-cache search pixman
。sudo apt-get install libpixman-1-dev
QEMU 可以使用 ctrl+a
(macOS 为 control+a
) 再按下 x
键退出。
make
自动化编译工具。
sudo apt install make
。
make[1]: rust-objcopy: Command not found
:
- 缺少binutils 工具集。
cargo install cargo-binutils
rustup component add llvm-tools-preview
rust工具链
首先安装 Rust 版本管理器 rustup 和 Rust 包管理器 cargo,这个windows之前已经安装。
linux版可能再装一次。
1 | export RUSTUP_DIST_SERVER=https://mirrors.ustc.edu.cn/rust-static |
thread panicked while panicking. aborting.
Illegal instruction (core dumped)
:(似乎是用WSL装rust的特有错误)
- https://stackoverflow.com/questions/61603982/thread-main-panicked-at-assertion-failed-left-right-left-22-right
- https://github.com/rust-lang/rustup/issues/2245 WLS 2才能正常装。。
发现自己可能WSL装错了版本。
只有 Windows 10 版本 18917 或以后的版本才能够正常运行 WSL 2。需要明确,WSL 2 目前依旧只能在 Windows 10 预览体验计划的版本中使用,因此你需要在「Windows 设置 > 更新和安全 > Windows 预览体验计划」处选择加入 Fast ring 或 Slow ring,这样才能使用正确的 Windows 10 版本安装 WSL 2。(Here)
https://docs.microsoft.com/zh-cn/windows/wsl/install-win10
https://github.com/Lincyaw/Rust_os_summer/blob/master/readme.md
更新windows。装上了WSL2。发现linux无法切换到WSL2。卸载重装报错:
1 | Installing, this may take a few minutes... |
解决方案:https://github.com/microsoft/WSL/issues/5393
在此处下载WSL2 Linux内核更新包:https://docs.microsoft.com/zh-cn/windows/wsl/wsl2-kernel
更新后问题解决。
然后重装一切。。
在经过漫长的鏖战以后,凌晨三点半,终于,运行成功了!
gdb
运行GDB架构:
1 | gdb --configuration # --target指定可以debug的类型 |
安装依赖:
1 | sudo apt-get install libncurses5-dev python python-dev texinfo libreadline-dev |
按照教程走即可。
error: *** A compiler with support for C++11 language features is required.
:
sudo apt install g++
GDB调试语法
b <函数名>
:在函数进入时设置断点,例如b rust_main
或b os::memory::heap::init
cont
:继续执行n
:执行下一行代码,不进入函数ni
:执行下一条指令(跳转指令则执行至返回)s
:执行下一行代码,进入函数si
:执行下一条指令,包括跳转指令layout
:如果没有安装gdb-dashboard
,可以通过layout
指令来呈现寄存器等信息,具体查看help layout
x/<格式> <地址>
:使用x/<格式> <地址>
来查看内存,例如x/8i 0x80200000
表示查看0x80200000
起始的 8 条指令。具体格式查看help x
gdb-dashboard(可选)
1 | wget -P ~ git.io/.gdbinit |
GDB will automatically load
./.gdbinit
for current debugging.
Connecting to raw.githubusercontent.com (raw.githubusercontent.com)|0.0.0.0|:443... failed: Connection refused.
:(被墙了)
- https://blog.csdn.net/littlehaes/article/details/103638711
- WSL2来了!但是能正常使用并不简单【WSL和V2ray的防火墙设置】
- 记录一次WSL2的网络代理配置【V2ray】
Error parsing proxy URL socks5://172.27.208.1:10808: Unsupported scheme ‘socks5’.
:
- wget不支持socks5。。。
- 搞来搞去。逼得没办法了。分析了一下
wget -P ~ git.io/.gdbinit
的含义,如下- 用wget将网络文件
git.io/.gdbinit
保存到~
根目录下 - ` -P, –directory-prefix=PREFIX save files to PREFIX/..
--cut-dirs=NUMBER ignore NUMBER remote directory components`
- 好了,这就好办,直接手动下载
.gdbinit
放到根目录!mv ./.gdbinit ~/
(https://gitee.com/dongbo_89/gdb-dashboard)- 安装pip:https://pip.pypa.io/en/stable/installing/
- 用wget将网络文件
1 | pip install pygments # Optionally install Pygments to enable syntax highlighting |
https://stackoverflow.com/questions/42870537/zsh-command-cannot-found-pip
python -m pip install pygments
Lab0:了解写RUST写OS的相关综述信息
操作系统(RISC-V)清华在线课程,2020春季 了解一下RISC-V、rCore的知识
Rust语言操作系统的设计与实现,王润基本科毕设论文,2019
zCore操作系统内核的设计与实现,潘庆霖本科毕设论文,2020
视频:半个世纪过去了,是时候用Rust重写操作系统了吗?(CC字幕)
创建项目
1 | mkdir ./rcore_project |
项目结构到此创建完毕。
运行测试:
1 | cd os |
移除标准库依赖
1 |
|
利用#![no_std]
禁用标准库。产生三个error:
1 | error: cannot find macro `println` in this scope |
error1,删去println!宏即可。
error2,自主实现panic函数( panic_handler
):
1 | /// 当 panic 发生时会调用该函数 |
error3,语义项(Language Item)缺失。( panic_handler
也是一个语义项)
eh_personality
:eh 是 Exception Handling 的缩写,它是一个标记某函数用来实现堆栈展开处理功能的语义项。- 在
os/Cargo.toml
中:将 dev 配置和 release 配置的 panic 的处理策略设为直接终止,也就是直接调用我们的panic_handler
而不是先进行堆栈展开等处理再调用。
1 | ... |
运行:
移除运行时环境依赖
运行时系统(Runtime System)可能导致 main
函数并不是实际执行的第一个函数。
Rust 的运行时入口点被 start
语义项标记。Rust 运行时环境的入口点结束之后才会调用 main
函数进入主程序。
- 重写覆盖整个
crt0
入口点。- 加上
#![no_main]
告诉编译器我们不用常规的入口点。 - 实现一个
_start
函数来代替crt0
,并加上#[no_mangle]
告诉编译器对于此函数禁用编译期间的名称重整(Name Mangling)——确保编译器生成一个名为_start
的函数。
- 加上
1 | //! # 全局属性 |
编译为裸机目标
链接错误:链接器的默认配置假定程序依赖于 C 语言的运行时环境,但我们的程序并不依赖于它。
为了解决这个错误,我们需要告诉链接器,它不应该包含 C 语言运行时环境。我们可以选择提供特定的链接器参数(Linker Argument),也可以选择编译为裸机目标(Bare Metal Target),我们将沿着后者的思路在后面解决这个问题,即直接编译为裸机目标不链接任何运行时环境。
rustc --version --verbose
:查看当前系统的目标三元组。
host 字段的值为三元组 x86_64-unknown-linux-gnu,它包含了 CPU 架构 x86_64、供应商 unknown、操作系统 linux 和二进制接口 gnu。
裸机环境:(底层没有操作系统的运行环境。这个其实之前已经装了)
1 | rustup target add riscv64imac-unknown-none-elf |
cargo build --target riscv64imac-unknown-none-elf
:
编译后结果放在了 os/target/riscv64imac-unknown-none-elf/debug
文件夹中。其中有一个名为 os
的可执行文件。它的目标平台是 RISC-V 64,暂时还不能通过我们的开发环境执行它。
在 os
文件夹中创建一个 .cargo
文件夹,并在其中创建一个名为 config
的文件,在其中填入以下内容:
1 | # 编译的目标平台 |
这指定了此项目编译时默认的目标。
以后可以直接使用 cargo build
来编译了。
生成内核镜像
为了查看和分析生成的可执行文件,我们首先需要安装一套名为 binutils 的命令行工具集,其中包含了 objdump 和 objcopy 等常用工具。这在之前已经安装完毕。
查看编译好的os
可执行文件:
1 | file target/riscv64imac-unknown-none-elf/debug/os |
它是一个 64 位的 elf 格式的可执行文件,架构是 RISC-V;链接方式为静态链接;not stripped 指的是里面符号表的信息未被剔除,而这些信息在调试程序时会用到,程序正常执行时通常不会使用。
使用刚刚安装的工具链中的 rust-objdump 工具看看它的具体信息:
1 | vel@LAPTOP-OD50F928 /Linux/rcore_project/os |
按顺序逐个查看:
start address
:程序的入口地址Sections
:从这里我们可以看到程序各段的各种信息。后面以 debug 开头的段是调试信息SYMBOL TABLE
:符号表,从中我们可以看到程序中所有符号的地址。例如_start
函数就位于入口地址上Program Header
:程序加载时所需的段信息- 其中的 off 是它在文件中的位置,vaddr 和 paddr 是要加载到的虚拟地址和物理地址,align 规定了地址的对齐,filesz 和 memsz 分别表示它在文件和内存中的大小,flags 描述了相关权限(r 表示可读,w 表示可写,x 表示可执行)
对于rust-objdump
,-x
来、可以查看程序的元信息,下面我们用 -d
来对代码进行反汇编:
1 | vel@LAPTOP-OD50F928 /Linux/rcore_project/os |
可以看到其中只有一个 _start
函数,里面什么都不做,就一个死循环。
并没有看到类似的东西。
生成镜像
1 | rust-objcopy target/riscv64imac-unknown-none-elf/debug/os --strip-all -O binary target/riscv64imac-unknown-none-elf/debug/kernel.bin |
这里 --strip-all
表明丢弃所有符号表及调试信息,-O binary
表示输出为二进制文件。
至此,我们编译并生成了内核镜像 kernel.bin
文件。接下来,我们将使用 QEMU 模拟器真正将我们的内核镜像跑起来。
调整内存布局
一般来说,一个程序按照功能不同会分为下面这些段:
- .text 段:代码段,存放汇编代码
- .rodata 段:只读数据段,顾名思义里面存放只读数据,通常是程序中的常量
- .data 段:存放被初始化的可读写数据,通常保存程序中的全局变量
- .bss 段:存放被初始化为 0 的可读写数据,与 .data 段的不同之处在于我们知道它要被初始化为 0,因此在可执行文件中只需记录这个段的大小以及所在位置即可,而不用记录里面的数据,也不会实际占用二进制文件的空间
- Stack:栈,用来存储程序运行过程中的局部变量,以及负责函数调用时的各种机制。它从高地址向低地址增长
- Heap:堆,用来支持程序运行过程中内存的动态分配,比如说你要读进来一个字符串,在你写程序的时候你也不知道它的长度究竟为多少,于是你只能在运行过程中,知道了字符串的长度之后,再在堆中给这个字符串分配内存
内存布局,也就是指这些段各自所放的位置。一种典型的内存布局如下:
编写链接脚本
使用链接脚本(Linker Script)来指定程序的内存布局。创建文件 os/src/linker.ld
:
1 | touch ./src/linker.ld |
写入下述内容:
1 | /* 有关 Linker Script 可以参考:https://sourceware.org/binutils/docs/ld/Scripts.html */ |
在 .cargo/config
文件中加入以下配置:
1 | # 使用我们的 linker script 来进行链接 |
重新编译:
1 | cargo build |
重写程序入口点 _start
在 _start
中设置内核的运行环境:(os/src/entry.asm
)
1 | # 操作系统启动时所需的指令以及字段 |
将 os/src/main.rs
里面的 _start
函数删除,并换成 rust_main
。
使用 QEMU 运行内核
运行命令:
1 | $ qemu-system-riscv64 \ |
加入输出代码,以及Makefile。
Makefile:16: *** missing separator. Stop.
:
- https://stackoverflow.com/questions/16931770/makefile4-missing-separator-stop
- makefile has a very stupid relation with tabs, all actions of every rule are identified by tabs. And no, 4 spaces don’t make a tab, only a tab makes a tab.
- Makefile语法不支持4个空格代替Tab。
接口封装和代码整理
使用 OpenSBI 提供的服务
OpenSBI 实际上不仅起到了 bootloader 的作用,还为我们提供了一些底层系统服务供我们在编写内核时使用,以简化内核实现并提高内核跨硬件细节的能力。这层底层系统服务接口称为 SBI(Supervisor Binary Interface),是 S Mode 的 OS 和 M Mode 执行环境之间的标准接口约定。
参考 OpenSBI 文档 ,里面包含了一些以 C 函数格式给出的我们可以调用的接口。
建立os/src/sbi.rs
:
1 | //! 调用 Machine 层的操作 |
把整个 print
和 println
宏按照逻辑写出:
1 | //! 实现控制台的字符输入和输出 |
将 main.rs
中处理 panic 的语义项抽取并完善到 panic.rs
中:
1 | //! 代替 std 库,实现 panic 和 abort 的功能 |
运行:
Lab1:boot与中断
https://github.com/chyyuu/ucore_os_lab/blob/riscv64-priv-1.10/docs/riscv-overview.md
https://github.com/chyyuu/ucore_os_lab/blob/riscv64-priv-1.10/docs/toolchain-overview.md
https://github.com/chyyuu/ucore_os_lab/blob/riscv64-priv-1.10/docs/lab1.md
广义的中断包括异常、系统调用(软中断)、硬件中断。
关于中断的分类:
interrupt
- hardware interrupt (external, async)
- software interrupt (internal, sync)
- syscall/trap (voluntarily yield to os)
- exception (involuntarily caught by os)
中断CSR
发生中断时,硬件自动填写的寄存器:
sepc
:即 Exception Program Counter,用来记录触发中断的指令的地址。和我们之前学的 MIPS 32 系统不同,RISC-V 中不需要考虑延迟槽的问题。但是 RISC-V 中的指令不定长,如果中断处理需要恢复到异常指令后一条指令执行,就需要正确判断将
pc
寄存器加上多少字节。scause
:记录中断是否是硬件中断,以及具体的中断原因。stval
:scause 不足以存下中断所有的必须信息。例如缺页异常,就会将stval
设置成需要访问但是不在内存中的地址,以便于操作系统将这个地址所在的页面加载进来。
指导硬件处理中断的寄存器:
stvec
:设置内核态中断处理流程的入口地址。存储了一个基址 BASE 和模式 MODE:- MODE 为 0 表示 Direct 模式,即遇到中断便跳转至 BASE 进行执行。
- MODE 为 1 表示 Vectored 模式,此时 BASE 应当指向一个向量,存有不同处理流程的地址,遇到中断会跳转至
BASE + 4 * cause
进行处理流程。
sstatus
:具有许多状态位,控制全局中断使能等。sie
:即 Supervisor Interrupt Enable,用来控制具体类型中断的使能,- 例如其中的 STIE 控制时钟中断使能。
sip
:即 Supervisor Interrupt Pending,和sie
相对应,记录每种中断是否被触发。- 仅当
sie
和sip
的对应位都为 1 时,意味着开中断且已发生中断,这时中断最终触发。
- 仅当
sscratch
:在用户态,sscratch
保存内核栈的地址;在内核态,sscratch
的值为 0。- 在内核态中,
sp
可以认为是一个安全的栈空间,sscratch
便不需要保存任何值。此时将其设为 0,可以在遇到中断时通过sscratch
中的值判断中断前程序是否处于内核态。
- 在内核态中,
中断指令
ecall
:触发中断,进入更高一层的中断处理流程之中。用户态进行系统调用进入内核态中断处理流程,内核态进行 SBI 调用进入机器态中断处理流程,使用的都是这条指令。sret
:从内核态返回用户态,同时将pc
的值设置为sepc
。(如果需要返回到sepc
后一条指令,就需要在sret
之前修改sepc
的值)ebreak
:触发一个断点。mret
:从机器态返回内核态,同时将pc
的值设置为mepc
。csrrw dst, csr, src
(CSR Read Write)
同时读写的原子操作,将指定 CSR 的值写入dst
,同时将src
的值写入 CSR。csrr dst, csr
(CSR Read)
仅读取一个 CSR 寄存器。csrw csr, src
(CSR Write)
仅写入一个 CSR 寄存器。csrc(i) csr, rs1
(CSR Clear)
将 CSR 寄存器中指定的位 清零,csrc
使用通用寄存器作为 mask,csrci
则使用立即数。csrs(i) csr, rs1
(CSR Set)
将 CSR 寄存器中指定的位 置 1,csrc
使用通用寄存器作为 mask,csrci
则使用立即数。
上下文
设计Context的结构:
1 | use riscv::register::{sstatus::Sstatus, scause::Scause}; |
在os/Cargo.toml
添加依赖:
1 | [dependencies] |
上下文的保存与恢复:
1 | # 我们将会用一个宏来用循环保存寄存器。这是必要的设置 |
中断处理流程
在os/src/interrupt/handler.rs
中初始化处理器:
1 | use super::context::Context; |
并将之前的所有函数封装:
1 | mod handler; |
在main函数中设置触发器:
1 | ... |
运行:
时钟中断
设计时钟中断处理器:
1 | use crate::sbi::set_timer; |
进行教程中所示的微小调整,引入mod。
1 | mod handler; |
为了在main函数中临时调用timer,我暂时将timer设置为了pub库。得到如下效果:
对比lab1关于context的内容,似乎lab1中的描述少了一部分关于Debug格式的代码。对比之后,我将此部分代码添加到了本地lab1的代码中。
实验一实验题
https://rcore-os.github.io/rCore-Tutorial-deploy/docs/lab-1/practice.html
原理
原理:在 rust_main
函数中,执行 ebreak
命令后至函数结束前,sp
寄存器的值是怎样变化的?
ebreak
命令就是设置断点。它会调用中断服务例程。也就是说,涉及到Context
上下文的存取。我们注意到,sp
是栈指针,如果需要存取Context
上下文时,sp
的值便会发生变化。
- 首先在中断保存
Context
的过程中,sp
减去一个Context
的大小,从而执行中断服务例程将Context
保存到栈中;- 执行中断的过程中,
sp
可能因为局部变量等操作有一些加加减减,但最后仍然总的来说保持不变;- 从中断返回时,执行
_restore
,sp
加上一个Context
的大小,并恢复上下文。
分析
分析:如果去掉 rust_main
后的 panic
会发生什么,为什么?
实际运行时发生如下错误:
这说明
panic!
的返回值是必要的。另外按照实验书的解释,
rust_main
返回后,程序并没有停止。其执行完后会回到entry.asm
中。但是,entry.asm
并没有在后面写任何指令,这意味着程序将接着向后执行内存中的任何指令。执行
rust-objdump -d -S os/target/riscv64imac-unknown-none-elf/debug/os | less
来查看汇编代码,可以发现之后还有很长很长的各种函数。
实验
如果程序访问不存在的地址,会得到 Exception::LoadFault
。模仿捕获 ebreak
和时钟中断的方法,捕获 LoadFault
(之后 panic
即可)。
添加如下match:
在处理异常的过程中,如果程序想要非法访问的地址是 0x0
,则打印 SUCCESS!
。
用
fault
函数类似的机制,单独实现对 LoadFault 的处理函数:
添加或修改少量代码,使得运行时触发这个异常,并且打印出 SUCCESS!
。
- 要求:不允许添加或修改任何 unsafe 代码
这个实在是没什么经验,不过,看了看解答,可以通过汇编代码的方式实现(修改
Context
调用的那个方法虽然有效,但是具有破坏性)。但是,不允许添加或修改任何 unsafe 代码,这个就有点过分。那这样就用不了跳转指令了。
- 解法 1:在
interrupt/handler.rs
的breakpoint
函数中,将context.sepc += 2
修改为context.sepc = 0
(则sret
时程序会跳转到0x0
)- 解法 2:去除
rust_main
中的panic
语句,并在entry.asm
的jal rust_main
之后,添加一行读取0x0
地址的指令(例如jr x0
或ld x1, (x0)
)照着解法2搞了搞没搞出来。照着解法1搞:
没有如预料中的出现
LoadFault
。。可能是因为我用了lab3的代码吧。。用lab1的代码过了。(两种解法均有效)
Lab2:物理内存管理
动态内存分配
内核中需要动态内存分配。典型的应用场景有:
Box<T>
,你可以理解为它和malloc
有着相同的功能;- 引用计数
Rc<T>
,原子引用计数Arc<T>
,主要用于在引用计数清零,即某对象不再被引用时,对该对象进行自动回收; - 一些 Rust std 标准库中的数据结构,如
Vec
和HashMap
等。
动态内存分配需要操作系统的支持,也就需要手动实现。在 Rust 语言中,我们需要实现 Trait GlobalAlloc
,并将这个类实例化,并使用语义项 #[global_allocator]
进行标记。这样的话,编译器就会知道如何使用我们提供的内存分配函数进行动态内存分配。
为了实现Trait GlobalAlloc
,就需要实现以下两个方法:
1 | unsafe fn alloc(&self, layout: Layout) -> *mut u8; // 分配一块虚拟内存 |
Layout
:分配一块连续的、大小至少为size
字节的虚拟内存,且对齐要求为align
。它有两个字段:size
表示要分配的字节数,align
则表示分配的虚拟地址的最小对齐要求,即分配的地址要求是align
的倍数。这里的align
必须是 2 的幂次。https://doc.rust-lang.org/nightly/core/alloc/trait.GlobalAlloc.html
https://doc.rust-lang.org/nightly/core/alloc/struct.Layout.html
Layout的结构摘录如下:
1 | pub struct Layout { |
建立config.rs
,设置堆空间大小:
1 | // 开辟堆空间(8M) |
建立heap.rs
,实现堆空间的管理:
(关于buddy_sysytem:https://github.com/rcore-os/buddy_system_allocator)
1 | use super::config::KERNEL_HEAP_SIZE; |
注意:[LockedHeap
] 已经实现了 [alloc::alloc::GlobalAlloc
] trait(Buddy System Allocator)。查看lab2源代码,发现 heap2.rs
实现了其他分配算法。但是 Trait 就要相应地自己去实现。
注意:这个buddy_system_allocator
要在 Cargo.toml
中引入:(https://github.com/rcore-os/rCore-Tutorial/blob/master/os/Cargo.toml#L13)。我的建议是直接把这部分的配置搬过来,省得之后麻烦:
1 | [dependencies] |
然后就是更新依赖。。。
然后把#![feature(alloc_error_handler)]
添加到main.rs里面,启用相关特性。
分配算法*
操作系统的分配算法当然是很多的。操作系统课上就学了不少了。。
这部分有时间可以写一个看看。有时间可以参考https://github.com/rcore-os/buddy_system_allocator看看。
物理内存探测
发现此处需要用到 address.rs
,但是却没有提到。从终代码中获取了该部分的源码。
若出现以下的引用错误:
注意到super所指的对象是当前目录下的mod.rs
文件,在mod.rs
引用address模块即可。
最终效果:
物理内存管理
注意动态内存分配,管理的是堆中的内存分配问题。而物理内存管理,是整个物理内存的页式存储管理。
实验指导写得不全,导致各种错误。和lab2得代码对比着调了半天过了。
注意到测试代码的内容:
这说明了内存的分配和自动回收是有效的。
实验二实验题
https://rcore-os.github.io/rCore-Tutorial-deploy/docs/lab-2/practice.html
原理
原理:.bss 字段是什么含义?为什么我们要将动态分配的内存(堆)空间放在 .bss 字段?
.bss 段:存放被初始化为 0 的可读写数据,与 .data 段的不同之处在于我们知道它要被初始化为 0,因此在可执行文件中只需记录这个段的大小以及所在位置即可,而不用记录里面的数据,也不会实际占用二进制文件的空间。
并不是必须要将动态分配的内存(堆)空间放在 .bss 字段。任何一个其他的段也都是可以的。但是这样做可能在代码实现上会比较简单。并且保证堆空间在内核的二进制数据之中。
分析
分析:我们在动态内存分配中实现了一个堆,它允许我们在内核代码中使用动态分配的内存,例如 Vec
Box
等。那么,如果我们在实现这个堆的过程中使用 Vec
而不是 [u8]
,会出现什么结果?
- 无法编译?
- 运行时错误?
- 正常运行?
没有看懂这个题。。不过看了解答之后。明白了是递归定义的锅。实现堆的过程中如果又用了堆,那么就会一直递归下去。。
实验
回答:algorithm/src/allocator
下有一个 Allocator
trait,我们之前用它实现了物理页面分配。这个算法的时间和空间复杂度是什么?
这说的哪个算法?
stacked_allocator
吧应该。。那么对于栈来说,时间复杂度O(1),空间复杂度O(n)。
二选一:实现基于线段树的物理页面分配算法(不需要考虑合并分配);或尝试修改 FrameAllocator
,令其使用未被分配的页面空间(而不是全局变量)来存放页面使用状态。
线段树感觉之前的代码好像已经不小心搬运过来了。。
lab2的线段树应该是用位图的方式维护的。我不妨先将这个算法理解一般好了。
虽然肯定比不上自己实现了。在大概理解了线段树的思路之后,随后自己手动实现了一遍:(能通过编译和测试,但是正确性不太好验证)
1 | use super::Allocator; |
我看了看栈分配器,又对比实现了一个队列分配器(虽然实际上没有怎么改动):
1 | //! 提供队列结构实现的分配器 [`QueueAllcator`] |
挑战实验(选做)
既然是选做,那我就暂时不做了。。。QAQ
挑战实验(选做)
- 在
memory/heap2.rs
中,提供了一个手动实现堆的方法。它使用algorithm::VectorAllocator
作为其根本分配算法,而我们目前提供了一个非常简单的 bitmap 算法(而且只开了很小的空间)。请在algorithm
crate 中利用伙伴算法实现VectorAllocator
trait。 - 前面说到,堆的实现本身不能完全使用动态内存分配。但有没有可能让堆能够利用动态分配的空间,这样做会带来什么好处?
Lab3:虚拟内存管理
虚拟内存这一块的东西,包括各种映射、TLB之类的,我还是比较熟悉的。
在实现虚拟地址结构后,调整为虚拟地址空间。
然后是各种映射的函数。。
运行:(注意到输出的是 VirtualAddress,虚拟地址生效了)
页面置换*
页面置换的部分暂时跳过了。
实验三实验题
https://rcore-os.github.io/rCore-Tutorial-deploy/docs/lab-3/practice.html
原理
原理:在 os/src/entry.asm
中,boot_page_table
的意义是什么?当跳转执行 rust_main
时,不考虑缓存,硬件通过哪些地址找到了 rust_main
的第一条指令?
boot_page_table
的意义自然是页表,具体来说,boot_page_table
指的是根页表。第一部分是低地址的恒等映射,用于维护 pc 的值,保证程序正常运行。第二部分是将高地址映射到低地址。然后就不会了。看了看解答,真的多。。
我们在
linker.ld
中指定了起始地址为0xffff_ffff_8020_0000
,操作系统执行文件会认为所有的符号都是在这个高地址上的。但是我们在硬件上只能将内核加载到0x8020_0000
开始的内存空间上,此时的pc
也会调转到这里。执行
jal rust_main
时,硬件需要加载rust_main
对应的地址,大概是0xffff_ffff_802x_xxxx
。
- 页表已经启用,硬件先从
satp
高位置读取内存映射模式,再从satp
低位置读取根页表页号,即boot_page_table
的物理页号- 对于 Sv39 模式,页号有三级共 27 位。对于
rust_main
而言,一级页号是其 [30:38] 位,即 510。硬件此时定位到根页表的第 510 项- 这一项的标志为 XWR,说明它指向一个大页而不是指向下一级页表;目标的页号为
0x8_0000
,即物理地址0x8000_0000
开始的区间;这一项的 V 位为 1,说明目标在内存中。因此,硬件寻址到页基址 + 页内偏移,即0x8000_0000 + 0x2x_xxxx
,找到rust_main
总的来说,就是硬件获取到初始的内存映射模式以后,就通过
rust_main
的虚拟地址,进行一级一级的查表,最后查到了rust_main
的物理地址所在的帧,这样就可以读取rust_main
了。重点就是,这里需要进行虚实转换。我通过反汇编
rust-objdump -d -S ./target/riscv64imac-unknown-none-elf/debug/os >> ../debug.file
,将输出保存文件中,查找到了rust_main
的地址:(左侧即虚拟地址)
分析
分析:为什么 Mapping
中的 page_tables
和 mapped_pairs
都保存了一些 FrameTracker
?二者有何不同?
FrameTracker
的作用:方便管理所有的物理页,我们需要实现一个分配器可以进行分配和回收的操作。这个Tracker
就是一个管理存储页的结构。保存了一些
FrameTracker
,就是保存了一些物理页,也就是使用了一些内存。显然page_tables
和mapped_pairs
使用内存的目的是不同的,page_tables
存放了所有页表所用到的页面,而mapped_pairs
则存放了进程所用到的页面。
分析:假设某进程需要虚拟地址 A 到物理地址 B 的映射,这需要操作系统来完成。那么操作系统在建立映射时有没有访问 B?如果有,它是怎么在还没有映射的情况下访问 B 的呢?
建立映射不需要访问B,这是显然的,因为访问B必然要在得到B的物理地址以后进行。而我们只需要物理地址就可以建立映射,因此后来的访问步骤是不必要的——我们暂时不需要访问页面内的具体内容。
不过,通常程序都会需要操作系统建立映射的同时向页面中加载一些数据。
那么实操来说,还是需要访问B的。
尽管 A→B 的映射尚不存在,因为我们将整个可用物理内存都建立了内核映射,所以操作系统仍然可以通过线性偏移量来访问到 B。
都有物理地址了,有没有映射根本不影响访问嘛!
实验
实验:了解并实现时钟页面置换算法(或任何你感兴趣的算法),可以自行设计样例来比较性能
- 置换算法只需要修改
os/src/memory/mapping/swapper.rs
,你可能需要在其中访问页表项 - 在
main.rs
中调用start_kernel_thread
来创建线程,你可以任意修改其中运行的函数,以达到测试效果
怎么感觉这个东西需要lab4的内容。。。暂时做不了。
有点难度。。有时间再考虑。。
看了看Here,确实虽然时间提前了,但也不必过于搞突击。尽力就好。不过我目前算是没有什么整理总结的压力吧。
Lab4:内核线程&用户进程&调度
线程与进程
线程与进程的一些东西。。之前学过。
每个线程都有自己独立的运行栈是,但它们可以在进程的尺度上共享资源,比如CPU时间、物理内存等等。
线程的表示需要用到控制块,线程如果我没记错的是线程控制块,进程则是PCB(进程控制块),每个控制块里面保存了识别一个线程、进程的关键信息。
比如实验书上提到的:
- 线程 ID:用于唯一确认一个线程,它会在系统调用等时刻用到。
- 运行栈:每个线程都必须有一个独立的运行栈,保存运行时数据。
- 线程执行上下文:当线程不在执行时,我们需要保存其上下文(其实就是一堆寄存器的值),这样之后才能够将其恢复,继续运行。和之前实现的中断一样,上下文由
Context
类型保存。(注:这里的线程执行上下文与前面提到的中断上下文是不同的概念) - 所属进程的记号:同一个进程中的多个线程,会共享页表、打开文件等信息。因此,我们将它们提取出来放到线程中。
- 内核栈:除了线程运行必须有的运行栈,中断处理也必须有一个单独的栈。之前,我们的中断处理是直接在原来的栈上进行(我们直接将
Context
压入栈)。但是在后面我们会引入用户线程,这时就只有上帝才知道发生了什么——栈指针、程序指针都可能在跨国(国 == 特权态)旅游。为了确保中断处理能够进行(让操作系统能够接管这样的线程),中断处理必须运行在一个准备好的、安全的栈上。这就是内核栈。不过,内核栈并没有存储在线程信息中。(注:它的使用方法会有些复杂,我们会在后面讲解。)
注意这里的Range就是分配虚拟地址的范围:
注意到,因为线程一般使用
Arc<Thread>
来保存,它是不可变的,所以其中再用Mutex
来包装一部分,让这部分可以修改。
同理,进程的结构:
在完成了一些工作以后,可以看到输出:(由于各种依赖关系BUG太多,我直接用了最终版的rcore代码来进行测试,不再一个一个文件地修改了)
1 | pub fn test_restore_thread(){ |
在重新部署了lab4的代码之后:
1 | Finished dev [unoptimized + debuginfo] target(s) in 6.98s |
和实验指导上的输出结果不太一样。。不过算勉强可以了吧。。
又用lab-4分支的代码实验了一下,发现直接多了一个user目录。。(这个是lab6的内容。。)最终效果:
实验四实验题
原理
原理:线程切换之中,页表是何时切换的?页表的切换会不会影响程序 / 操作系统的运行?为什么?
页表是在
Process::prepare_next_thread()
中调用Thread::prepare()
,其中换入了新线程的页表。下面是线程中对应方法的实现,可以看到页表的切换过程:
页表的切换不会影响程序 / 操作系统的运行。因为切换过程是通过中断完成的,而中断是操作系统实现的。同时页表切换后,只要之前保存的映射关系有效,程序也可以恢复到之前的状态,从而正确运行。
分析
https://www.bookstack.cn/read/ucore_os_docs/lab6-lab6_3_6_1_basic_method.md
分析:
- 在 Stride Scheduling 算法下,如果一个线程进入了一段时间的等待(例如等待输入,此时它不会被运行),会发生什么?
它被调用的可能性增加。(stride相对降低,因为其他线程会升高)
Stride Scheduling 算法的核心公式是:P.pass =BigStride / P.priority。也就是优先级越高,步长pass越小。
因为Stride Scheduling 算法的核心策略是:重新调度当前stride最小的进程。这样累加的stride越慢,就更加容易被调用。
- 对于两个优先级分别为 9 和 1 的线程,连续 10 个时间片中,前者的运行次数一定更多吗?
不一定。比如,前者的stride现在是1000,而后者的stride是100。假设BigStride = 90; 这样,即使优先级更高,但总的stride仍然太大,后者仍然会被更多地运行。
也可能优先级分别为 9 的线程运行一次就结束了。
- 你认为 Stride Scheduling 算法有什么不合理之处?可以怎样改进?
stride累计对旧进程可能不太友好。如果stride累计太久了,那么新加入的进程将在一个时间段内长期占据CPU,从而让其他的进程无法运行。应该设计一种抑制措施:
- 当一个进程等待时长每超过 T 秒时,此进程的 stride 累计值减半
这样,等待过久的进程的stride值会很快恢复正常。
Stride Scheduling 算法不支持对进程状态的应对,比如优先级高的可能正处于阻塞状态。
设计
设计:如果不使用 sscratch
提供内核栈,而是像原来一样,遇到中断就直接将上下文压栈,请举出(思路即可,无需代码):
- 一种情况不会出现问题
- 一种情况导致异常无法处理(指无法进入
handle_interrupt
) - 一种情况导致产生嵌套异常(指第二个异常能够进行到调用
handle_interrupt
,不考虑后续执行情况) - 一种情况导致一个用户进程(先不考虑是怎么来的)可以将自己变为内核进程,或以内核态执行自己的代码
这个真是不会。。看了解答。
- 只运行一个非常善意的线程,比如
loop {}
jr 0
+jr 2
的那种,这样程序始终运行在局部,当然不会出现问题了- 线程把自己的
sp
搞丢了,比如mv sp, x0
。此时无法保存寄存器,也没有能够支持操作系统正常运行的栈
- 这说明需要一个用户程序不能直接修改的栈来存储中断上下文
- 运行两个线程。在两个线程切换的时候,会需要切换页表。但是此时操作系统运行在前一个线程的栈上,一旦切换,再访问栈就会导致缺页,因为每个线程的栈只在自己的页表中
- 每个栈的访问需要借助虚拟地址
- 如果使用同一个栈,那么切换页表后的映射关系就不对了
- 用户进程巧妙地设计
sp
,使得它恰好落在内核的某些变量附近,于是在保存寄存器时就修改了变量的值。这相当于任意修改操作系统的控制信息
- 还是不安全的问题,因为没有对系统信息进行隔离,同一个栈里面访问控制不好实施
实验
实验:当键盘按下 Ctrl + C 时,操作系统应该能够捕捉到中断。实现操作系统捕获该信号并结束当前运行的线程(你可能需要阅读一点在实验指导中没有提到的代码)
这一题和下一题需要捕捉键盘输入。
首先
handler.rs
中找到外部中断:先了解
Ctrl + C
对应的键值:https://blog.csdn.net/softimite_zifeng/article/details/53259542。从中可以知道,Ctrl+C(3)。故,我们需要特别处理键盘输入键值为3的情况。
实验:实现线程的 clone()
。目前的内核线程不能进行系统调用,所以我们先简化地实现为“按 C 进行 clone”。clone 后应当为目前的线程复制一份几乎一样的拷贝,新线程与旧线程同属一个进程,公用页表和大部分内存空间,而新线程的栈是一份拷贝。
首先,实现键盘中断响应:
然后在进程控制中实现 clone 函数,
最后在线程中具体实现clone功能,(和线程里面的new类似地实现即可)注意:新线程与旧线程同属一个进程,公用页表和大部分内存空间,而新线程的栈是一份拷贝。
BUG:注意不能直接命名为 clone 函数,否则会与现有的克隆函数冲突。重命名为
clone_
。
实验:了解并实现 Stride Scheduling 调度算法,为不同线程设置不同优先级,使得其获得与优先级成正比的运行时间。
https://www.bookstack.cn/read/ucore_os_docs/lab6-lab6_3_6_2_priority_queue.md
Stride Scheduling 调度算法似乎需要用到优先队列结构。不妨先实现朴素的stride算法(无优先队列)。
新建
stride_scheduler.rs
:(有瑕疵)
1 | //! Stride Scheduling的调度器 [`StrideSchduler`] |
Lab5:块设备和文件系统
设备树
在 RISC-V 中,操作系统通过 bootloader,即 OpenSBI 固件完成以设备树的格式管理全部已接入设备信息的功能。它来完成对于包括物理内存在内的各外设的扫描,将扫描结果以设备树二进制对象(DTB,Device Tree Blob)的格式保存在物理内存中的某个地方。
这个结构其实跟文件系统很像。
每个设备在物理上连接到了父设备上最后再通过总线等连接起来构成一整个设备树,在每个节点上都描述了对应设备的信息,如支持的协议是什么类型等等。而操作系统就是通过这些节点上的信息来实现对设备的识别的。
因为整个是一个树结构,所以在解析设备树获取节点信息时,可以直接采用简单的递归函数,即树遍历算法。如下代码所示:(其余代码参考实验书)
1 | /// 递归遍历设备树 |
挂载
QEMU支持挂载设备树。只不过用了一个 virtio
半虚拟化技术架构:
以 virtio 为中心的总线下又挂载了 virtio-blk(块设备)总线、virtio-net(网络设备)总线、virtio-pci(PCI 设备)总线等,本身就构成一个设备树。
当然,要启用QEMU挂载功能,需要调用相应的命令:
1 | # 运行 QEMU |
接着便是一些驱动代码细节。。
思考
为什么物理地址到虚拟地址转换直接线性映射,而虚拟地址到物理地址却要查表?
答案中提到:在内核线程里面,只要一个物理地址加上偏移得到的虚拟地址肯定是可以访问对应的物理地址的。所以,把物理地址转为虚拟地址加个偏移既可。
这说明,如果物理地址有效,那么线性映射是肯定存在的,这时虚拟地址也一定有效(是可能的物理地址对应的虚拟地址之一)。但是反过来,虚拟地址却不一定是最初的线性映射形成的,因此不能直接读写,必须通过页表查询。
驱动、块设备
块设备,即以块为单位进行单次读写操作,这样每次读取的效率更高。因为硬盘的读取具有局部性,如果每次只读一个字节,那么花在寻道时间等等上的成本就会很大。所以,一次读一波~
驱动,就是负责对设备的管理和访问,在驱动中要实现诸如:获取设备类型信息,读取某个块到缓冲区,将缓冲区的数据写入某个块,获取设备树上的设备信息并保存等等。
抽象块设备,其实就是提供给文件系统的一个高级接口,在这一层隐去了驱动、块设备的诸多细节,只保留了几个封装好的函数,供上层文件系统调用。
文件系统
之前我们在加载 QEMU 的时候引入了一个磁盘镜像文件,这个文件的打包是由 rcore-fs-fuse 工具 来完成的,它会根据不同的格式把目录的文件封装成到一个文件系统中,并把文件系统封装为一个磁盘镜像文件。然后我们把这个镜像文件像设备一样挂载在 QEMU 上,QEMU 就把它模拟为一个块设备了。接下来我们需要让操作系统理解块设备里面的文件系统。
由上述可知,我们需要在块设备中分析文件系统。
文件系统已经有了大量前人的实现。所以,采用了一个模板 Simple File System。(前人的分析)
最后加入测试代码,试着运行一下,看看效果:(PROCESSOR.lock().run()
这个方法并没有任何地方实现了,所以要改成其他的方法)
实验五实验题
实验五暂时没有实验题
Lab6:加载执行文件形成用户进程
我们成功单独生成 ELF 格式的用户程序,并打包进文件系统中;同时,从中读取,创建并运行用户进程;而为了可以让用户程序享受到操作系统的功能,我们使用系统调用为用户程序提供服务。
打包
用户程序框架和实验准备中为操作系统「去除依赖」的工作十分类似。只不过需要新建一个 user
专用文件,与 os
文件夹相互独立、并列。
1 | $ cargo new --bin user |
和操作系统一样,我们需要为用户程序移除 std 依赖,并且补充一些必要的功能。
lib.rs
:
#![no_std]
移除标准库#![feature(...)]
开启一些不稳定的功能#[global_allocator]
使用库来实现动态内存分配#[panic_handler]
panic 时终止其他文件:
.cargo/config
设置编译目标为 RISC-V 64console.rs
实现print!
println!
宏
安装 rcore-fs-fuse
工具:
1 | $ cargo install rcore-fs-fuse --git https://github.com/rcore-os/rcore-fs |
在 user/Makefile
里面设置打包的命令:
1 | build: dependency |
用户进程
在之前实现内核线程时,我们只需要为线程指定一个起始位置就够了,因为所有的代码都在操作系统之中。但是现在,我们需要从 ELF 文件中加载用户程序的代码和数据信息,并且映射到内存中。
在 lab6 中,用户程序需要从文件中获取,而不是之前的手动创建了。对应的是 ELF 文件解析器,因为有 xmas-elf
这个 crate 替我们实现了 ELF 的解析,所以直接调用就行了。
读取文件内容:(将文件整个读到一个向量里面然后返回)
1 | fn readall(&self) -> Result<Vec<u8>> { |
然后解析各个字段。。代码有点长就不贴了。
思考:我们在为用户程序建立映射时,虚拟地址是 ELF 文件中写明的,那物理地址是程序在磁盘中存储的地址吗?这样做有什么问题吗?
肯定是不行的,这样搞的话,每次物理地址解析完还要访问磁盘,众所周知,磁盘的读写炒鸡慢。所以这样搞,系统的性能就太低了。所以要将文件内容加载进入内存,并以内存中的物理地址为准。这样,便也就涉及到了页面置换等等优化的问题。
对于一个页面,有其物理地址、虚拟地址和待加载数据的地址。此时,是不是直接从待加载数据的地址拷贝到页面的虚拟地址,如同 memcpy
一样就可以呢?
当然是不行的。因为首先一个页面可能具有多个虚拟地址!那么你要拷贝到那个虚拟地址呢?如果页表加载的不同,那么同一个虚拟地址可能有不同的映射,可能就访问不到页面所在的真正的物理地址了。所以这里必须用物理地址来写入数据,确保正确性。
系统调用
系统调用通过一些中断性的设计来完成一些功能。在后面实验题的部分也有做到。
首先系统调用底层需要用到一定的汇编。可以看指导书。系统调用通常会返回三类处理结果:一个数值、程序进入等待、程序被终止。
后面的条件变量暂时先这样把。。条件变量在这里的作用就是:
为输入流加入条件变量后,就可以使得调用
sys_read
的线程在等待期间保持休眠,不被调度器选中,消耗 CPU 资源。
这似乎是可以解决之前 Stride Scheduling 算法的缺陷,因为之前的调度算法没有对等待期的线程做处理,因此可能出现所有高 stride 的线程被迫等待,CPU 资源闲置的情况。
实验六实验题
https://rcore-os.github.io/rCore-Tutorial-deploy/docs/lab-6/practice.html
原理
原理:使用条件变量之后,分别从线程和操作系统的角度而言读取字符的系统调用是阻塞的还是非阻塞的?
跟解答差不多。对于线程而言,是阻塞的,因为需要等待系统调用结束。对于操作系统,等待输入的时间完全分配给了其他线程,所以对于操作系统来说是非阻塞的(操作系统似乎很难发生阻塞,除非所有的进程都阻塞了,否则总是可以通过调度实现运行)。
设计*
设计:如果要让用户线程能够使用 Vec
等,需要做哪些工作?如果要让用户线程能够使用大于其栈大小的动态分配空间,需要做哪些工作?
首先需要支持用户态的堆空间预分配,然后让
Vec
访问这个堆空间。应当要在用户部分实现 #[global_allocator] :包含 [
alloc::alloc::GlobalAlloc
] trait等要让用户线程能够使用大于其栈大小的动态分配空间,需要设计一个相应的用户进程的堆空间实现。
实验
实验:实现 get_tid
系统调用,使得用户线程可以获取自身的线程 ID。
随便设定一个获取自身的线程 ID 的系统调用号。
在
user/src/syscall.rs
添加对应的系统调用:
1 | const SYSCALL_GETTID: usize = 233; |
然后在
os/src/kernel/syscall.rs
中实现具体的系统调用接口:
1 | pub const SYSCALL_GETTID: usize = 233; |
最后在
os/src/kernel/process.rs
中具体实现系统调用:
1 | use super::*; |
实验:将你在实验四(上)实现的 clone
改进成为 sys_clone
系统调用,使得该系统调用为父进程返回自身的线程 ID,而为子线程返回 0。
同理。随便设定一个
sys_clone
系统调用号。在
user/src/syscall.rs
添加对应的系统调用:
1 | const SYS_CLONE: usize = 110; |
然后在
os/src/kernel/syscall.rs
中实现具体的系统调用接口:
1 | pub const SYS_CLONE: usize = 110; |
最后在
os/src/kernel/process.rs
中具体实现系统调用:
1 | pub(super) fn sys_clone(context: &Context) -> SyscallResult { |
实验:将一个文件打包进用户镜像,并让一个用户进程读取它并打印其内容。需要实现 sys_open
,将文件描述符加入进程的 descriptors
中,然后通过 sys_read
来读取。
随便设定一个
sys_open
系统调用号。在
user/src/syscall.rs
添加对应的系统调用:
1 | const SYSCALL_OPEN: usize = 120; |
然后在
os/src/kernel/syscall.rs
中实现具体的系统调用接口:
1 | pub const SYSCALL_OPEN: usize = 120; |
最后在
os/src/kernel/fs.rs
文件系统中具体实现系统调用:(不太会。。)
1 | use crate::ROOT_INODE; |
将一个文件打包进用户镜像:(根据实验指导书:https://rcore-os.github.io/rCore-Tutorial-deploy/docs/lab-6/guide/part-2.html),编写MAkefile:
1 | TEST_FILE := test.file |
最后在 main 函数中调试,遇到一些格式上的困难。暂时放弃了。
挑战实验
挑战实验:实现 sys_pipe
,返回两个文件描述符,分别为一个管道的读和写端。用户线程调用完 sys_pipe
后调用 sys_fork
,父线程写入管道,子线程可以读取。读取时尽量避免忙等待。
先放着了。。
OS实习第三次交流会
- 老师介绍第二阶段鹏城实验室实习的准备工作
- 15到20人的规模左右
- 28号左右完成一个check(26号提交问卷,27号修改完毕)
- 没有拿到《复课证明》的折衷方案:
- 只要本人被同意而做好规划,但是只有一周的时间(黑客马拉松的形式)
- 有《复课证明》可以待一个月,发工牌
- 每个同学签署自我安全协议书
- 来回车票、食宿报销,有实习劳务费
- 高铁/航班(二等座、经济舱+登机牌)。
- 宿舍两人一间。
- 深圳天气较热。
- 实验室有食堂。
- 只要本人被同意而做好规划,但是只有一周的时间(黑客马拉松的形式)
- 总结报告:今天至少提交一个版本,可以继续更新
- 以后的实习机会优先考虑
- 学生提问与交流
==第一阶段总结==
博客记录:操作系统暑期项目。
简要自身情况介绍:我是计算机科学爱好者,学生,机缘凑巧听说了 rCore 的暑期实习项目,本身也没有别的要紧的事情,于是决定来参与这个活动。可以说在很大的程度上达到了我想要的效果吧,虽然离群里的大佬还有很大的差距,但我对于我自己的收获还是比较认可的。虽然少,但是实在。
总的来说,第一阶段确实学了一些东西,但是相对来说,又学得偏少;Rust 只是掌握了最基础的一些语法,大概是那种能通过编译、有一定正确性的程度,但是离熟练掌握 Rust 还有一定距离。Rust的编译检查在最开始可能确实有些“反人类”,可是做完 rustlings,做完 15 道编程题,慢慢地通过编译变得容易多了,也会更加注重编译出错时地提示,通常这些提示都会很贴心。在这样的“与编译器作斗争”的过程中,编程水平也许有了无形的提高也说不定。虽然我选的编程题比较简单,都是从 LeetCode 上摘取的简单题、中等题。Rust的语法特性有一些确实很好用,比如模式匹配系列(match
,if let
,while let
,……),用得好便会有奇妙的逻辑效果,尤其是与 Option
这种枚举类型配合时,更让人感觉到语言的有美感。Go 语言作为另一门现代系统级编程语言,通常有着非常固定的、专属的编程范式,那么 Rust 语言会不会在将来形成自己的编程范式呢?至少现在来看,fmt
相关的工具只是对代码的样式风格做了标准化,离代码的逻辑风格还差一些。具体的lab实验中,也会用到 Rust 的各种特性。在以后,如果有机会继续深入学习的话,可能会对语言的设计产生更深刻的理解。但是目前,到此为止也还不错。
而RISC-V方面我也只是粗看了皮毛,仗着自己在MIPS和x86汇编方面的知识,倒也暂时没有遇到太多的困难;但是,要说显著的进步,确实没有了。RISC-V作为精简指令集,在设计思路上其实跟 MIPS 的区别不大,可能它的优势就在于历史包袱小、开源,但说实话真正从技术层面上来分析却没有什么特别之处。RISC-V 更像是一个开源运动的产物,就像 Linux 一样,具有广泛的社区和生命力。RISC-V 在陈渝老师这边强调的是特权架构,不过也没感觉到什么特别的地方,跟 x86 的特权级体系还是蛮像的,也许是多了一两个特权级?我这里想到了网络的分层体系,有七层的 OSI 体系,也有五层的 TCP/IP 体系,其实操作系统的特权架构和网络系统的分层两者还是挺像的,从中也可以看出特权级究竟分多少级其实完全取决于现实需求,而没有什么理论上的特别限制,只要做到对不同层级之间的功能的清晰划分便足矣。
在做 rCore 实验方面,由于时间的提前,打乱了之前的计划,在研究了前几个 lab 之后(lab1 - lab3),只好匆匆地跑通后面几个 lab(然后做实验题去了),而实验书的对应章节却几乎来不及看了(得知延期一天,不妨抽点时间浅浅地看一番)。所以在实验方面,有大量的代码细节,没有时间去看,这可能会对第二阶段的 zCore(如果可能的话)产生比较严重的影响。在学习之余,我也参与了少量的微信群讨论和 issue 上的提问与回答,还提交了几个简单的 Pull Request,目前都已经被合并了。因为之前自学过操作系统和 ucore 系列实验,所以在实验的理论方面没有遇到太大的障碍,反倒是代码细节上能力有些捉襟见拙——操作系统真是一个注重实践大于理论的学科。目前来说,我感觉到操作系统的编写过程中,DEBUG 是一个非常要命的事情,我至今还不是很会用 gdb,由于禁用 std,在单元测试的时候也经常被迫只能选择 assert!
之类的断言的形式。至少以我以前维护 Java 项目和 Python 项目的经验来看,Rust 项目的测试功能还是比较麻烦的(也可能是我不熟悉或者不知道更好的 DEBUG 方法)。
这一个月,说快也快。中途还摸鱼划水了一段时间。整体来说,花在操作系统暑期实习上的时间并不是特别多,因此收获相对来说还是对得起付出的时间的。也许多花点精力,可以把实验做完;或者做一点微小的贡献和改进;或者帮助解决更多的问题……但是,时间都已经过去了。现在也只能唏嘘。不管怎么说,这一个阶段也总算结束了。不管之后能不能入选第二个阶段的实习,我都已经很满足了(即使不能入选,也有其它的事情要做,所以并不慌张~)。像我这样佛系的态度来面对科研可能差点火候,但是对于喜欢的、有兴趣的定西,这样的态度不也可能成为长燃的火烛么。许多年以后,我会庆幸自己曾经在某个夏伏天,写过一些文字,写过一些代码……那已经极好了。