0%

第一阶段 rust基础

rustlings比较顺利,已关注rust好几年了,也一直跟进rust的发展和变化,用了大概几天的时间突击完成

第二阶段 rcore

这是我第一次接触操作系统,印象还都停留在原来的书本上,概念上,都是理论,没有真正接触操作系统是什么样的,所以,这个机会能让我了解探究操作系统内部真正的原理和运行逻辑,解开我心中的多年的疑惑,很是开心到起飞。

总体印象最深的就是操作系统内核是以什么形态存在的,上下文如何切换,进程空间如何形成,页表是如何实现的,跳板页又是怎么回事

第三阶段 ArceOS

这个和我预期不一样,没有沿着rcore继续走,这是一个全新的设计,一时间,有点慌乱,不能和rcore相关的思路很自然的顺承下来,显然这个更具有前瞻性,为此我也花了好多时间来梳理这个逻辑和rcore都关联起来。这个模块化设计的理念很突出,相互之间的关联及细节,需要仔细的研读和体会。

第三阶段的具体case

color

对println!() 把颜色表示直接加入后,可以显示颜色,
如在当前文件写了一个宏print_with_color!,让后让println!去去调用此宏,就得不到正确结果,提示找不到这个宏,查了资料,和 $crate有关,宏的暴露方式有关,引用路径,由于时间关系,后面调研

HashMap

开始的时候,从rust官方移植,这个太痛苦了,依赖太多了,最后放弃。
自己写个简单的,只是利用了官方的
use core::hash::Hasher;
use core::hash::Hash;
这两个hash算法,主体采用了最简单的线性插入算法

bumpallocator

需要仔细理解题意和上下文,思路选用:
申请一直从可用空间起始向前申请,
释放如果全部的空间都释放了,就把下一个可用空间调整到开始

rename

采用递归的方式,一层一层的先找到所在当前目录
增加当前目录的,更改命名的方法,查找相应的文件,删除后,再插入一个,因为当前存储使用的是BtreeMap,不能更改index

hv

这个耗费了我好多时间,主要是因为运行例程会卡住,后来发现可能是qemu的版本问题,升级到9.2后,还是一样卡住,当时环境为windows11,wsl2,卡在这里,不同的情况卡的还不一样:

1
Write file 'payload/skernel2/skernel2' into disk.img

最后没有办法,找一台空闲机器,实在不行,我就安装裸机linux系统,所以先尝试装了另一种虚拟机,virtualbox7.18,也花了些时间,配置好环境后,默认的qemu为6.x时,还是会出错,不过不是卡主的问题,是会触发异常访问,升级到9.2.x后,按照预期执行了,难道说,wsl2在某些情况就是不行,我真是难过。

之后遇到提交github,出错,musl.cc被block,

下载不下来:
wget https://musl.cc/riscv64-linux-musl-cross.tgz

下载不下来,终于等来了替代方案

wget https://musl.cc.timfish.dev/riscv64-linux-musl-cross.tgz
最后,终于得以解决

sys_mmap

参数和返回值我理解出现偏差,

1
2
3
4
5
6
7
8
fn sys_mmap(
addr: *mut usize,
length: usize,
prot: i32,
flags: i32,
fd: i32,
_offset: isize,
) -> isize

参数addr为0时要特别注意,需要寻找一空间,还有地址和size的对齐。返回值,当开始时认为0是成功,为负时,返回失败原因,后来再三确认失败返回0,成功返回地址,isize作为地址返回,有点不符合直觉?

Next

期待第四阶段

前三阶段总结

主要收获

在第四阶段中,更多的时间留给了自由探索.虽然起初缺少具体的目标有些令人摸不着头脑,不过跟随老师的引导,也一步步确立了整个阶段的目标:基于 uring^1 机制实现异步系统调用.尽管最后只实现了基于 uring 的异步 IPC 机制,一路上走来也有许多收获.

Rust 的异步机制

虽然有一些 Rust 异步编程经验,但尚未从更底层的角度了解过 Rust 的异步模型.在第一周中,通过动手实现一个简易的异步运行时 local-executor,认识到 Rust 的异步原语 Future 是如何与运行时交互的.以及深入到内存安全层面上,了解了 Rust 中如何通过 pin 语义来巧妙的保证自引用结构体的安全性.尽管这并不是一个完美的模型——几乎所有涉及到 pin 语义的数据结构和函数都需要 unsafe 代码,而这些 unsafe 代码所需的诸多安全性保证又着实有些令人头大.因为,Rust 提供了静态安全性,而编译器会基于这些安全保证进行比较“激进”的优化.所以,Rust 中的 unsafe 要比其他生来便“不安全”的语言更加不安全,对开发者的要求也更高.

在第三周的探索中,又了解到一个之前从未考虑过的问题——Future 的终止安全性^2.而这对于实现基于共享内存的异步通信机制来说尤其关键,稍有不慎就会引发难以察觉的漏洞.在后来着手实现异步通信机制的时候,又对这个问题进行了更深入的思考,并在现有方案的基础上提出了另外几个可行的思路

原子类型和内存排序

尽管曾了解过原子类型和内存排序相关的知识,但从未真正彻底搞懂过,直到在第二周的探索中发现了一本优秀的电子书 Rust Atomics and Locks^3.这本书从抽象的并发模型深入到具体的硬件细节,比较全面的介绍了几种原子操作和内存排序的设计初衷以及对应的汇编层面实现.结合这本书和自己的思考,又经过悉心整理最终形成了一篇比较详实的学习笔记.尽管在实践时还不能完全掌握各种内存排序的选择,通过翻看笔记以及参考相似场景下现有项目的做法,也都能找到一个安全正确的选项.

基于 uring 的异步通信

经过两周的调查和学习,最终在第三周完成了基于 uring 的异步通信框架 evering,同时利用 GitHub Pages 部署了它详细的设计文档

evering 最重要的两个数据结构是用来管理消息队列的 Uring 和用来管理操作生命周期的 DriverUring 的实现借鉴了 io_uring 的做法^4,但结合 Rust 的特性做了一些简化.比如,io_uring 支持 IOSQE_IO_LINK 来要求响应侧顺序处理请求.而在 Rust 中,每个异步请求都被封装为 Future,故可以利用 .await 来在请求侧实现顺序请求.Driver 的实现则借鉴了 tokio-uringringbahn.但相比后两者,evering 提供了更灵活、通用的异步操作管理机制.

不过,目前 evering 相对简陋,仅支持 SPSC,因此请求侧或响应侧只能在单线程上使用.也许未来可以实现 MPSC 的队列,以便于更好的与现有的异步生态(比如 tokio)兼容.

基于 evering 的异步 IPC

经过三周的铺垫,第四周正式开始实践跨进程的异步通信.在第三周中,基于 evering 实现了简易的跨线程异步通信 evering-threaded,而对跨进程来说,主要的难点就是内存的共享.好在 Linux 提供了易于使用的共享内存接口,基于 shm_open(3)memfd_create(2)mmap(2) 可以轻松在不同进程之间建立共享内存区.而 ftruncate(3p) 配合缺页延迟加载机制,使得程序启动后仅需一次初始化就能配置好可用的共享内存区间.不过,目前 evering 只能做到基础的“对拍式”的通信方式.而近期字节跳动开源的 shmipc 则是一个相对成熟、全面的异步通信框架,这对未来 evering 的改进提供了方向.

基于 evering 的异步系统调用

由于时间相对仓促,加之备研要占用大量的时间,遗憾的是,在第四阶段并没有完成最初的目标——实现基于 uring 的异步系统调用.与 用户线程 <-> 用户线程 的通信相比,用户线程 <-> 内核线程 的通信要额外处理内核任务的调度和用户进程的生命周期管理.即如何处理多个不同用户进程的请求,以及用户进程意外退出后对应内核任务的清理.而就共享内存而言,由于用户对内核单向透明,这看起来似乎比 IPC 的共享内存更容易解决.

用户态线程与协程的调度

去年的夏令营中,embassy-preempt 实现了内核中线程和协程的混合调度.那么用户态的协程能否被内核混合调度呢?在实现异步系统调用的前提下,当用户态线程由于内核尚未完成调用处理而让权(通过 sched_yield(2) 等系统调用)时,实际上,内核可以获知该线程应何时被唤醒.这就与 Rust 协程中的 Waker 机制非常相似,而用户态的让权又与 .await 很类似.基于这些,那么可以将一个实现异步系统调用的用户线程转换为一个用户协程.此后,内核就充当了这个协程的运行时和调度器的角色.

而相比用户态的线程,使用协程的一个显著优点是,对用户任务的唤醒实际上相当于执行一次 Future::poll.这意味着,当用户主动让权时,它不需要保存任何上下文——用户任务的唤醒本质上变成了函数调用,而主动让权表示该函数的返回.如此便能够进一步减少用户和内核切换的开销,以及系统中所需执行栈的数量.当然,当用户协程被抢占时,它便回退成了类似线程的有独立执行栈和上下文的存在.

总结

经过近两个月的学习,对操作系统和异步编程的许多方面都有了一些相对清晰的认知.非常感谢夏令营中各位老师的付出和历届同学的努力,学习的过程中让我切身的感受到操作系统发展到现在那段波澜壮阔的历史,以及在不断推陈出新的技术潮流中一点微不足道的参与感.尽管最后没能完成目标有些遗憾,不过,这也为将来再次参加夏令营留下了充足的理由 :P

2025 春夏季开源操作系统训练营 学习总结

1
2
3
4
5
// 实验环境
Mac mini Apple M4
MacOS 15.4.1
// 备注(在国外的同学可以忽略)
在配置实验环境时,一定要更换Homebrew、rustup和Cargo的镜像源。

第一阶段:Rust编程

由于参与过 http://opencamp.cn/ 的其他 Rust 训练营,本阶段相当于是复习阶段。然而笔者对很多 Rust 语法还是理解的不够,需要通过更多实际项目来加深学习。

第二阶段:OS设计实现

本阶段主要参考指导书完成实验 https://learningos.cn/rCore-Tutorial-Guide-2025S/

1. Apple Silicon 相关问题

在第零章,指导书如是说:
经初步测试,使用 M1 芯片的 macOS 也可以运行本实验的框架,即我们的实验对平台的要求不是很高。但我们仍建议同学配置 Ubuntu 环境,以避免未知的环境问题。

笔者在实验中确实遇到了一些问题,但都并非无法解决,具体情况如下:

1.1 安装 qemu (可能需要 sudo)

1
2
brew install qemu
qemu-img --version

如果显示qemu-img version 9.2.3则说明安装成功。

1.2 载入rustsbi-qemu.bin时卡死

@lurenjia1213 修复了该问题,需要 Clone 下面的项目,重新编译,然后拷贝生成的rustsbi-qemu.bin 并覆盖实验项目中的文件。
https://github.com/lurenjia1213/rustsbi-qemu/tree/main

1.3 qemu 模拟器无法正确退出

在ch3中,qemu模拟器无法正确退出,需要拷贝ch2中的./src/boards/qemu.rs到ch3。通过对比ch2和ch3的区别来修复问题,从而可以学习正确退出模拟器的方法。

ch4 通过同样的方法在 BASE=1 的测试中还是无法正确退出,需要以后解决。

1.4 MacOS 没有 timeout 指令导致测例无法通过

安装 coreutils, 用 gtimeout 来替代。

1
2
3
brew install coreutils
// 需要将下面这条指令写到保存环境变量的那个文件中
alias timeout=gtimeout

2. 学习心得

由于没有系统学习过操作系统的理论知识,学的比较慢,需要参考指导书和学习资源中的视频才慢慢掌握。虽然操作系统相关的内容学的不够扎实,但是对于 rust 的语法慢慢熟悉了起来。抱着学习 rust 的心态还学习到了操作系统的底层运行,很有收获。

第三阶段:项目基础阶段 - 组件化操作系统

  • 尽管是第一次接触操作系统,在完成第二阶段以后,第三阶段给笔者的感觉不是很陌生(至少知道大概都在干什么)
  • 课程视频和课件也很详细的给出了任务和学习目标,但是第三阶段的项目相比第二阶段要大很多
  • 有时候不知道要去哪里干什么,只能通过给出的题目反向查找相关的内容。

1.遇到的问题和解决思路

  • 在UniKernel部分, 笔者在 MacOS 下面完成了任务,到了宏内核的部分涉及到了交叉编译的部分,似乎 MacOS 变得复杂了起来。折腾了一段时间之后,还是在一台 Linux 服务器上用 docker 完成了后续的任务。
  • 这是笔者第一次使用 docker,遇到不懂的就让 Chatgpt 来生成指令,但是还是遇到了很多问题:
    1. hub.docker.com 被墙,根本创建不了 Linux 容器, 后来找到 nvcr.io/nvidia/pytorch:25.05-py3 解决了问题: https://catalog.ngc.nvidia.com/orgs/nvidia/containers/pytorch
    2. 以为 root 之后就万事大吉,但是在运行容器的时候要加上 --privieged, 不然 mount 指令无法正确的找到目录的位置
    3. 运行容器的时候千万不要加 -rm, 不然停止容器的时候就永远地消失了

      2. 学习心得

      由于上述笔者遇到的问题,反反复复搭环境搭了 3 次, 对各种 Linux 指令,以及各种组件的作用有了更深的理解。相比第二阶段,第三阶段的练习难度反而有所下降,但是 ArceOS 本身的内容是相当多的,需要更加系统的学习。 希望能在第四阶段有所收获。

rust基础的总结

一.基本数据类型与所有权

所有权系统核心规则

  1. 移动语义(Move)
    1
    2
    3
    let s1 = String::from("hello");  // 堆分配
    let s2 = s1; // 所有权转移
    // println!("{}", s1); // 错误!s1 已失效
  2. 借用规则
    • 任意时刻:一个可变引用 多个不可变引用
    • 引用必须始终有效(悬垂指针禁止)
  3. 生命周期标注
    1
    2
    3
    fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() { x } else { y }
    }

Slice 类型

  • 无所有权引用
    1
    2
    3
    4
    let s = String::from("hello world");
    let hello: &str = &s[0..5]; // 字符串切片
    let a = [1, 2, 3, 4];
    let slice: &[i32] = &a[1..3]; // 数组切片

二.Crate 与模块系统

Crate 类型

类型 文件扩展名 特点
二进制 Crate main.rs 可执行程序
库 Crate lib.rs 可复用代码库

模块可见性规则

1
2
3
4
5
6
7
8
mod front_of_house {
pub mod hosting { // pub 使模块公有
pub fn add_to_waitlist() {}
}
}

// 使用绝对路径访问
crate::front_of_house::hosting::add_to_waitlist();

使用外部 Crate

1
2
3
# Cargo.toml
[dependencies]
rand = "0.8.5" # 语义化版本
1
2
3
4
5
6
// main.rs
use rand::Rng;

fn main() {
let num = rand::thread_rng().gen_range(1..101);
}

三. Option 与错误处理

Option 枚举

1
2
3
4
5
6
7
8
9
10
11
enum Option<T> {
Some(T),
None,
}

// 安全解包
let x: Option<i32> = Some(5);
match x {
Some(i) => println!("Value: {}", i),
None => println!("Missing value"),
}

Result<T, E> 错误处理

1
2
3
4
5
6
7
8
9
10
11
fn read_file(path: &str) -> Result<String, io::Error> {
fs::read_to_string(path)
}

// 错误传播简写
fn read_config() -> Result<String, io::Error> {
let mut file = File::open("config.toml")?;
let mut contents = String::new();
file.read_to_string(&mut contents)?;
Ok(contents)
}

错误处理最佳实践

  1. 优先使用 Result 而非 panic
  2. 使用 ? 操作符传播错误
  3. 自定义错误类型实现 std::error::Error

四. Trait 与泛型

Trait 定义与实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
trait Summary {
fn summarize(&self) -> String;
}

struct NewsArticle {
headline: String,
location: String,
}

impl Summary for NewsArticle {
fn summarize(&self) -> String {
format!("{} ({})", self.headline, self.location)
}
}

泛型函数

1
2
3
4
5
6
7
8
9
fn largest<T: PartialOrd>(list: &[T]) -> &T {
let mut largest = &list[0];
for item in list {
if item > largest {
largest = item;
}
}
largest
}

Trait Bound 语法糖

1
2
3
// 以下两种写法等价
fn notify<T: Display + Clone>(item: &T) {...}
fn notify(item: &(impl Display + Clone)) {...}

生命周期进阶

生命周期标注必要性

  1. 结构体持有引用时必须显式标注生命周期,确保引用的有效性
  2. 方法实现中:
    • &self 参数隐含 &'a self 生命周期
    • 返回值关联结构体生命周期(通过生命周期消除规则第三项)
  3. 遵循Rust生命周期消除三规则:
    • 每个输入引用自动获得独立生命周期
    • 单个输入引用时所有输出引用与其生命周期对齐
    • 方法签名中 &self 使输出引用与结构体生命周期对齐

错误

1
2
3
4
fn dangling_reference() -> &str {
let s = String::from("temporary");
&s[..] // 错误!返回局部变量引用
} // s离开作用域被丢弃
1
2
3
4
5
6
7
8
9
10
struct ImportantExcerpt<'a> {
part: &'a str,
}

impl<'a> ImportantExcerpt<'a> {
fn announce_and_return(&self, announcement: &str) -> &str {
println!("Attention: {}", announcement);
self.part
}
}

五. 智能指针

常用智能指针对比

类型 所有权 线程安全 使用场景
Box<T> 单一 堆分配、递归类型
Rc<T> 共享 单线程引用计数
Arc<T> 共享 多线程引用计数
RefCell<T> 可变 运行时借用检查

使用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Box 用于递归类型
enum List {
Cons(i32, Box<List>),
Nil,
}

// Rc 共享所有权
use std::rc::Rc;
let a = Rc::new(5);
let b = Rc::clone(&a);

// RefCell 运行时借用检查
use std::cell::RefCell;
let c = RefCell::new(42);
*c.borrow_mut() += 10;

六.迭代器与闭包

闭包类型推断

1
2
let add_one = |x| x + 1;         // 类型自动推导
let print = || println!("hello"); // 无参闭包

闭包捕获模式

捕获方式 关键字 所有权
不可变借用 `
可变借用 ` mut
值捕获 move 转移

迭代器适配器

1
2
3
4
5
6
7
let v = vec![1, 2, 3, 4];

// 链式调用
let sum: i32 = v.iter()
.map(|x| x * 2) // 加倍
.filter(|x| x % 4 == 0) // 过滤4的倍数
.sum(); // 求和

自定义迭代器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct Counter {
count: u32,
}

impl Iterator for Counter {
type Item = u32;

fn next(&mut self) -> Option<Self::Item> {
if self.count < 5 {
self.count += 1;
Some(self.count)
} else {
None
}
}
}

七.并发与异步编程

线程创建

1
2
3
4
5
6
7
use std::thread;

let handle = thread::spawn(|| {
println!("From spawned thread");
});

handle.join().unwrap();

通道通信 (mpsc)

1
2
3
4
5
6
7
8
9
use std::sync::mpsc;

let (tx, rx) = mpsc::channel();

thread::spawn(move || {
tx.send("Message").unwrap();
});

println!("Received: {}", rx.recv().unwrap());

共享状态 (Mutex)

1
2
3
4
5
6
7
8
9
10
11
use std::sync::{Arc, Mutex};

let counter = Arc::new(Mutex::new(0));

let handles: Vec<_> = (0..10).map(|_| {
let c = Arc::clone(&counter);
thread::spawn(move || {
let mut num = c.lock().unwrap();
*num += 1;
})
}).collect();

异步编程 (async/await)

1
2
3
4
5
6
7
8
9
10
11
12
async fn fetch_data() -> Result<String, reqwest::Error> {
reqwest::get("https://api.example.com/data")
.await?
.text()
.await
}

#[tokio::main]
async fn main() {
let data = fetch_data().await.unwrap();
println!("Data: {}", data);
}

八.常用集合类型

Vec 动态数组

1
2
3
4
5
6
7
8
9
10
11
let mut v = Vec::with_capacity(10);
v.extend([1, 2, 3]);

// 安全访问
if let Some(val) = v.get(1) {
println!("Second element: {}", val);
}

// 所有权注意事项
let first = &v[0];
// v.push(4); // 编译错误!存在不可变引用时禁止修改

HashMap<K, V> 哈希表

1
2
3
4
5
6
7
8
use std::collections::HashMap;

let mut scores = HashMap::new();
scores.insert("Blue", 10);

// Entry API 安全更新
scores.entry("Yellow").or_insert(50);
scores.entry("Blue").and_modify(|e| *e += 1);

阶段二,os基础

lab1

与上下文, 特权级有关的寄存器

  • sstatus:包含了处理器的状态信息,包括特权级别和中断使能状态。恢复 sstatus 的值确保在返回用户态时,处理器的特权级别和中断状态与陷阱发生前一致。
  • sepc:保存了中断或异常发生时的程序计数器值。恢复 sepc 的值确保在返回用户态时,处理器能够从中断或异常发生的地方继续执行。
  • sscratch:保存了用户栈指针。在切换到用户态之前,将用户栈指针保存到 sscratch 寄存器中,以便在用户态下使用。
  • sret根据sstatus中的SPP位指示切换为用户态。(寄存器中的一个位,0,u_mode;1_,s_mode,s_mode)
  • scause: Trap原因/种类
  • stvec: trap_handle地址

lab2

SV39

  • virtual page 39位, 38-12为虚拟页号
  • 页表项PTE: Reserver: 10, PPN2: 26, PPN1: 9, PPN0: 9, RSW: 2, DAGUXWRV

分页

  • MMU地址转换
  • kernel address space最高位为 “跳板”, app ks, guard page
  • app address space, 最高位为跳板, TrapContext, UserStack, GP, Framed

    跳板意义:

    satp, 切换后,地址映射不同, 例如:上下文切换的restore, 在更改satp指令后, 保证下一条指令在不同的地址映射下能被正确寻址,保证指令的连续执行

TrapContext新增字段

在进行特权级转换时, 需要相应的sp以及satp的token

  • pub kernel_satp: usize, 内核地址空间的 token
  • pub kernel_sp: usize, 当前应用在内核地址空间中的内核栈栈顶的虚拟地址
  • pub trap_handler: usize, 内核中 trap handler 入口点的虚拟地址

lab3

fork

  • 获得父进程的地址空间
  • sepc + 4
  • a0返回参数更改,父子进程不相同
  • 维护父子进程关系
  • fd, 死锁检测等

功能实现

  • stride算法:

    • 为TCB加上schedule块(struct), 同时预留了pass设置的接口
    • 为sys_set_priority加入了对priority的设置
    • 将TaskManager块改为了用binaryheap存储, 并为TCB分配了Ord特性,每次选取都会取stride最小的调度
  • 向前兼容

    • 重写mmap和munmap(用到了remove_area_with_start_vpn)
    • 重写了sys_get_time,用到了translate_va

lab4

文件系统

文件系统本质上是一堆块上的抽象, 在内存中有缓存块对其进行映射.

进程维护一个文件描述符表,可映射到对应的缓存块

1
2
3
4
5
6
7
8
pub struct BlockCache {
cache: [u8; BLOCK_SZ],
block_id: usize,
block_device: Arc<dyn BlockDevice>,
modified: bool,
}

//提供对应的接口调用

easy-fs磁盘布局

  • 超级块 (Super Block),用于定位其他连续区域的位置,检查文件系统合法性。

  • 索引节点位图,长度为若干个块。它记录了索引节点区域中有哪些索引节点已经被分配出去使用了。

  • 索引节点区域,长度为若干个块。其中的每个块都存储了若干个索引节点。

    1
    2
    3
    4
    5
    6
    7
    8
    #[repr(C)]
    pub struct DiskInode {
    pub size: u32,
    pub direct: [u32; INODE_DIRECT_COUNT],
    pub indirect1: u32,
    pub indirect2: u32,
    type_: DiskInodeType,
    }
  • 数据块位图,长度为若干个块。它记录了后面的数据块区域中有哪些已经被分配出去使用了。

  • 数据块区域,其中的每个被分配出去的块保存了文件或目录的具体内容。

lab5

在引入线程后, 调度机制本质上是在线程块上进行切换. 会区分主线程和子线程

  • 创建线程不需要要建立新的地址空间
  • 能够访问到进程所拥有的代码段, 堆和其他数据段
  • 专有的用户态栈

实现功能

  • 在ProcessControlBlockInner加入了对mutex和sem的死锁检查块(all[], ava[], need[])
  • 检测前对相应资源的need[] + 1
  • 实现is_safe检测函数, 对finish==false和need <= work的块, 回收allocation和finish=true,对标记flag=true, 当finish没有任何改变, 即本次循环flag==false时退出loop, 利用闭包all,检测finish所有线程是否全是true
  • 若为unsafe, 则回退need, 返回-0xdead
  • 若为safe, 则在down和lock之前drop(process_inner),防止线程堵塞无法释放资源, 在down和lock之后同时更新检查块中的矩阵
  • 为up和unlock加上检查块的更新

Mutex实现问题

  • Mutex1的lock里,会一直尝试获取锁, 具体逻辑为当无法获得锁时,直接阻塞,让出cpu,直到被唤醒, 再重新尝试获得锁, unlock中释放锁,并且唤醒一个线程去竞争这个锁.
  • Mutex的lock,在无法获得锁时,直接堵塞,在unlock时,只有等待队列为空才释放锁.
    - 这里的unlock本质是锁资源的转移, A不释放锁, 而是唤醒一个直接使用这个资源的B线程(它醒来后直接运行临界区后的代码)

第三阶段

一, 组件化内核基础与 Unikernel 模式

组件化内核介绍

Unikernel 模式

  • 特点
    • 应用与内核合一:编译为一个 Image,共享同一特权级(内核态)和地址空间。
    • 无用户态 / 内核态切换:简单高效,但安全性较低(应用可直接访问内核资源)。

核心组件

组件名称 功能描述 在实验中的作用
axhal 硬件抽象层,屏蔽不同架构差异(如 Riscv64/ARM) 初始化串口、内存等硬件,提供底层 IO 接口
axruntime 内核运行时环境,负责引导流程、内存初始化、任务调度框架 执行内核启动流程,调用应用层代码
axstd 内核态标准库,提供基础数据结构和工具函数(如 println!) 实现字符终端输出功能
arceos_api 内核公共接口,定义组件间通信协议 统一组件间调用规范

Unikernel 的启动链

  • 硬件启动:通过 OpenSBI(Riscv 固件)加载内核 Image 到内存。
  • 引导阶段(axhal)
    • 初始化 CPU 寄存器、MMU 分页(早期恒等映射)。
    • 建立内核栈,为 Rust 运行时做准备。
  • 运行时阶段(axruntime)
    • 初始化内存分配器、日志系统。
    • 调用应用层 main 函数,执行具体功能。

实验

1. 主函数 src/main.rs

1
2
3
4
5
6
7
8
#![cfg_attr(feature = "axstd", no_main)] // 若启用 axstd,不使用标准库的 main 入口
#[cfg(feature = "axstd")] // 根据 feature 条件编译
use axstd::println; // 使用 axstd 的打印函数

#[cfg_attr(feature = "axstd", no_mangle)] // 避免符号名被修改
fn main() {
println!("Hello, ArceOS!"); // 调用 axhal 提供的串口输出功能
}

2. 依赖管理 Cargo.toml

1
2
3
[dependencies]
axstd = { workspace = true } // 引入 axstd 组件,支持标准库功能
arceos_api = { workspace = true } // 引入内核公共接口

3. features 动态配置

  • 作用:通过编译参数控制组件的启用,实现 “按需构建”。
  • 示例
    • axstd 组件通过 feature = “axstd” 控制是否包含。
    • 实验中默认启用 axstd,因此能使用 println!。

println!

通过更改ulib下axstd,macros文件中的println!

hashmap

1
2
#[cfg(feature = "alloc")]
pub mod collections;

暴露自己写的collections

1
2
3
[dependencies.hashbrown]
version = "0.14"
default-features = false

用了官方库的core版本

二, 内存管理与多任务基础

1. 分页的两个阶段

阶段 目标 实现方式 关键组件
早期启用(必须) 快速建立基本映射,保证内核启动 1GB 恒等映射(虚拟地址 = 物理地址) axhal 中的 BOOT_PT_SV39 页表
后期重映射(可选,需 paging feature) 扩展地址空间,支持设备 MMIO 细粒度权限控制(如只读、可执行) axmm 中的 AddrSpace、PageTable

2. 算法

算法 原理
TLSF 两级 Bitmap + 链表管理空闲块
Buddy 基于 2 的幂次分裂 / 合并空闲块
Slab 为特定大小对象创建缓存池

3.

全局分配器:通过 #[global_allocator] 声明,实现 GlobalAlloc trait。

1
2
#[cfg_attr(all(target_os = "none", not(test)), global_allocator)]
static GLOBAL_ALLOCATOR: GlobalAllocator = GlobalAllocator::new();

任务数据结构 TaskInner

1
2
3
4
5
6
7
8
struct TaskInner {
id: TaskId, // 唯一标识
name: String, // 任务名称(调试用)
state: AtomicU8, // 状态(Running/Ready/Blocked/Exited)
kstack: Option<TaskStack>, // 任务栈(类似线程栈)
ctx: UnsafeCell<TaskContext>, // 上下文(保存寄存器状态)
// 其他字段:调度相关(如时间片、优先级)
}

协作式调度

FIFO 队列:任务按 “先到先服务” 原则执行,当前任务需主动让出 CPU(调用 yield_now())。

组件
组件 功能
axsync 同步原语(自旋锁、互斥锁)
axtask 调度接口(spawn/yield_now 等)

实现

EarlyAllocator实现要求比较低

byte
  • alloc
    • 注意每次分配内存时候的对齐
    • 预分配,检查是否与p_pos重叠
    • 为count++
      • 注意每次分配内存时候的对齐
      • 预分配,检查是否与p_pos重叠
      • 为count++
  • dealloc
    • 单纯的count–
    • count==0时,就可以重置b_pos了
page
  • alloc
    • 检查alignment是否有效
    • 获取分配的size进行对齐,同时检查是否越界
    • 更新数据
      • 检查alignment是否有效
      • 获取分配的size进行对齐,同时检查是否越界
      • 更新数据
  • dealloc
    • 不要求实现

三、调度,块设备,文件系统

时钟中断:

代码(Riscv64 中断初始化)

1
2
3
4
5
6
// axhal/src/platform/riscv64_qemu_virt/mod.rs
axhal::irq::register_handler(TIMER_IRQ_NUM, || {
update_timer(); // 更新系统时间
axtask::on_timer_tick(); // 触发调度器更新
});
axhal::arch::enable_irqs(); // 开中断

块设备驱动:

Trait:BlockDriverOps

1
2
3
4
5
trait BlockDriverOps {
fn num_blocks(&self) -> u64; // 磁盘总块数
fn block_size(&self) -> usize; // 块大小(512 字节)
fn read_block(&mut self, block_id: u64, buf: &mut [u8]) -> DevResult; // 读块
}

文件系统:

抽象

文件系统(FileSystem):如 FAT32、EXT4。

目录(Dir):存储文件 / 子目录元数据。

文件(File):存储具体数据,支持读写操作。

接口

1
2
3
4
trait VfsOps {
fn root_dir(&self) -> &DirNode; // 获取根目录
fn lookup(&self, path: &str) -> Option<FileNode>; // 解析路径
}

加载流程

块设备读取:通过 VirtIO Blk 驱动读取磁盘前 512 字节(引导扇区)。

解析 BPB:获取 FAT 表起始地址、簇大小等参数。

挂载文件系统:将 FAT32 的根目录挂载到 VFS 的 / 节点。

应用加载示例(U.8 实验)

1
2
3
4
5
6
// 从 FAT32 文件系统加载应用程序
fn load_app(path: &str) -> Result<Vec<u8>> {
let root = vfs.root_dir();
let file = root.lookup(path).ok_or("文件不存在")?;
file.read_to_end() // 读取文件内容到内存
}

实验实现

寻找ing

在axfs_ramf中实现

1
impl VfsNodeOps for DirNode{...}

文件时通过封装的BTreeMap管理的, 替换相应键值对即可

注意

1
fn rename(&self, src_path: &str, dst_path: &str)

src和dst_path路径层级不一样
我使用了split_path_to_end来获取最终的文件名

四, 地址空间管理

缺页异常处理

1
2
// 关键修改:init_user_stack的lazy参数设为false
let ustack_top = init_user_stack(&mut uspace, false).unwrap(); // 延迟映射

缺页异常处理流程

  • 异常触发:用户态访问未映射地址(如栈写入),CPU 陷入内核。
  • 处理逻辑:
    1. 通过handle_page_fault函数申请物理页帧(alloc_frame)
    2. 在页表中建立虚拟地址与物理页帧的映射(pt.remap)
1
2
3
4
5
fn handle_page_fault(...) -> bool {
let frame = alloc_frame(true); // 申请物理页
pt.remap(vaddr, frame, orig_flags); // 建立映射
tlb.flush(); // 刷新TLB
}

ELF 格式解析

关键段:
LOAD 段:包含代码段(R E标志)和数据段(RW标志)。
BSS 段:未初始化数据,ELF 文件不存储,内核需预留空间并清零。
加载逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
for segment in elf.segments {
if segment.type == LOAD {
let vaddr = segment.virt_addr;
let phys_frame = alloc_frame(segment.mem_siz);
map_virtual_to_physical(vaddr, phys_frame, segment.flags);
if segment.has_data {
copy_file_data(vaddr, segment.file_offset, segment.file_siz);
} else {
zero_memory(vaddr, segment.mem_siz); // BSS段清零
}
}
}

实验实现

得到aspace->分配内存->将文件信息写入

1
2
3
4
5
6
const MAP_SHARED = 1 << 0;    // 共享映射,对映射区域的修改会反映到文件中
const MAP_PRIVATE = 1 << 1; // 私有映射,对映射区域的修改不会反映到文件中
const MAP_FIXED = 1 << 4; // 必须使用指定的映射地址
const MAP_ANONYMOUS = 1 << 5; // 匿名映射,不与文件关联
const MAP_NORESERVE = 1 << 14; // 不保留交换空间
const MAP_STACK = 0x20000; // 用于栈分配

这里只处理MAP_PRIVATE,
同时addr.is_null(),可通过aspace.find_free_area寻找内存

五, Hypervisor

Hypervisor

1.1 定义

Hypervisor(虚拟机监控器)是运行在物理硬件与虚拟机之间的虚拟化层软件,允许多个虚拟机共享物理资源,每个虚拟机拥有独立的虚拟硬件环境(如vCPU、vMem、vDevice)。

1.2 核心功能

  • 资源虚拟化:模拟CPU、内存、设备等硬件资源
  • 隔离与调度:确保虚拟机之间资源隔离,并高效调度物理资源
  • 模式切换:在Host(Hypervisor)与Guest(虚拟机)之间双向切换

1.3 与模拟器的区别

维度 Hypervisor 模拟器(Emulator)
ISA一致性 虚拟环境与物理环境ISA一致 可模拟不同ISA(如x86模拟ARM)
指令执行 大部分指令直接在物理CPU执行 全部指令需翻译/解释执行
性能目标 高效(虚拟化开销低) 侧重仿真效果,性能要求低

1.4 虚拟化类型

  1. I型Hypervisor:直接运行在硬件上(如Xen、KVM),性能高
  2. II型Hypervisor:运行在宿主OS上(如VirtualBox),依赖宿主资源管理

二. Riscv64虚拟化扩展(H扩展)

2.1 特权级扩展

新增特权级:

  • HS(Hypervisor Supervisor):Host域的管理级,负责虚拟化控制
  • VS(Virtual Supervisor):Guest域的内核级,运行Guest OS内核
  • VU(Virtual User):Guest域的用户级,运行Guest应用

特权级关系:

1
2
物理机:M(最高) > HS > U
虚拟机:VS(Guest内核) > VU(Guest用户)

2.2 关键寄存器

  • hstatus:控制Host与Guest的模式切换
    • SPV位:指示进入HS前的模式(0:非虚拟化模式;1:来自Guest的VS模式)
    • SPVP位:控制HS是否有权限操作Guest的地址空间
  • vs[xxx]/hs[xxx]:分别用于Guest和Host的上下文管理
  • misa:标识是否支持H扩展(bit7=1表示支持)

3. 模式切换机制

3.1 从Host到Guest(run_guest函数)

1
2
3
4
5
6
// 保存Host寄存器状态
sd ra, (hyp_ra)(a0) // 保存返回地址
// 加载Guest寄存器状态
ld sstatus, guest_sstatus(a0)
// 执行sret指令切换到VS模式
sret

可参考guest.s

  • a0指向的guest_reg区域 与 当前reg的替换

    3.2 VM-Exit处理(以SBI调用为例)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    match scause.cause() {
    Trap::Exception(Exception::VirtualSupervisorEnvCall) => {
    let sbi_msg = SbiMessage::from_regs(ctx.guest_regs.gpr);
    if let Some(SbiMessage::Reset(Shutdown)) = sbi_msg {
    ax_println!("Shutdown vm normally!");
    // 清理Guest资源
    }
    }
    }

实现

根据结果硬编码,更改guest_reg的值

Stage 1

这个阶段主要是学习Rust语法,因为之前有报名过训练营,所以做起来比较顺手,把基础语法又复习了一遍。

Stage 2

这个阶段主要是阅读实验指导书和源码。实验指导书非常重要,如果没有看明白的话对做实验有很大影响,所以要细心耐心看。时间有限的话,看精简版指导书即可。
完成实验部分需要重点理解几个点:

  1. 任务切换机制,保存切换前后程序上下文
  2. 地址空间,多级页表机制
  3. 文件系统,操作与管理

Stage 3

这个阶段主要是看视频和PPT,并通过做6个实验来熟悉ArceOS的设计思想。相对于上一个阶段的实验会简单一些。

总结

之前有报名过两次训练营,但都没有坚持下来。对于如何平衡学习、工作和自我提升之间的平衡,是一个我现在以及将来都需要仔细思考的问题。

Prologue

这样的总结应该从何开始?我是从Bilibili刷视频偶然了解到与训练营相关的信息的。使用Rust语言编写操作系统的实践,我太喜欢这个方向了。因为我正学过一点Rust,也经学校老师的推荐看过CSAPP并完成了大多数的实验。其中我最喜欢的便是shlabattacklab────写一个shell!实在是有趣不过,如果再写一个操作系统呢?好吧,我应该没有与之匹配的实力,不过开源操作系统训练营就这样给了我一个类似的机会。报名人数破千!全程免费!还有什么好说的呢,杀😡。

Stage 1

110道Rustling编程题,并没有耗费我太多功夫,更多的是重新熟悉一下语法。我觉得,学习Rust不仅是学会如何使用一门编程语言,更是了解更多的编程范式。例如trait背后的组合大于继承;函数式编程对现代编程语言深刻的影响:默认不可变、闭包、HOFs、链式操作等;所有权与生命周期机制,这种RAII思想是C++首创的(但是opt-out)。Rust编译器就是你最好的老师,更别说还有满地走的各式AI(本总结经AI辅助完成),2025年的今天,学习Rust不应该再是一件难事🥳。

Stage 2

到了OS设计实现,主要是完成5道rCore操作系统大实验编程题。我是提前进入该阶段,所以全程并没有看过相关学习视频,而是跟随rCore-Tutorial-Guide文档完成的🤓。

这阶段最耗时的是lab2───地址空间和lab5───并发,这两个不管哪个太痛苦了😭。lab2是因为分页机制本身就相对复杂,层层抽象,读内核新增的代码就花了我很久时间(光论这一点文件系统其实不遑多让,不过到这里我的读代码能力已经得到显著锻炼了,所以带给我的痛苦远不及地址空间🥱)。而lab5,单纯是我因为技术路线的左右互博而无限拖缓了进度,我一直在对死锁检测资源的获取上究竟是现场构建还是跟随进程保存之间反复横跳。倒不如说,是因为我在实现这两个的时候遭遇多方掣肘,导致我不停怀疑我自己,不停的重构。使用Rust编程不就是戴着脚镣在跳舞吗?我现在水平还不够,只能写出不够优美的实现,但是我不会放弃的😡。

Stage 3

组件化操作系统,这大概是最各显神通的阶段了。我对这阶段的印象其实是一点草台味🤯,遇到各方面奇怪的问题,测试脚本死活不通过,各种不同的资料,到底要实现在哪里,我要怎么修改一个crate依赖的代码?我是个不撞南墙不愿意问别人的人,所以我全部都闭门造车自己解决了所有问题(真的吗?至少测例说我通过了)。但实际上,在讨论群里大家都很乐意回答别人的提问,每个人都有自己的“奇技淫巧”,应该让大家全都热烈讨论遇到的问题,才能让训练营变得更好😈。

Conclusion

写到这里我已经有点精疲力竭。我在参与前三个阶段的过程中收获颇多,不只是对整个组件化操作系统的认识。还有各种在学习过程中对工具的使用,helixZellij,这些工具,我很早就下载了,只是因为它是Rust重写的老工具。现在呢?我需要helix丰富的快捷键,我需要Zellij的分屏。我开始熟悉,正是我开始迈出一步,参加了这次,2025 春夏季开源操作系统训练营

完成了三阶段的任务,我也疲惫了,进入了一种拖延的状态。五月二十二号,新建文件夹,想要完成这篇总结报告。一直到今天,我终于又想重新出发了。希望到了第四阶段,我可以找到新的方向。

编程的乐趣:⭐️⭐️⭐️⭐️
挑战的难度:⭐️⭐️⭐️
开源训练营:⭐️⭐️⭐️⭐️⭐️

RUST学习总结

函数:

  • 函数名和变量名使用蛇形命名法(snake case),例如 fn add_two() -> {}
  • 函数的位置可以随便放,Rust 不关心我们在哪里定义了函数,只要有定义即可
  • 每个函数参数都需要标注类型

所有权

基础类型:不会转移所有权,属于复制变量的值

复合类型:会转移所有权,相当于重新绑定变量

(深拷贝:复合类型变量名.clone(),不转移所有权)

引用

  • &表示引用,以*表示解引用
  • 可变引用首先要求变量可变,引用时也要写成&mut 变量名,否则是可变变量的不可变引用
  • 一个变量的可变引用同时只能存在一个,可变与不可变引用不可同时存在
  • “同时”指引用的作用域,为引用”从创建开始,一直持续到它最后一次使用的地方“

复合类型

字符串

切片:对string类型中某一部分的引用,即&变量名[开始……终止],切片类型为&str

string&str的转化:

&str化成string: String::from("字符串字面量")/"字符串字面量".to_string()

string化成&str: 取切片

操作字符串(针对于string

  • 追加:push(字符)/push_str(字符串字面量(不能是string类型)) 改变原有的字符串(不返回新值,必须mut可变)
  • 插入:insert()/insert_str() 需要传入两个参数,第一个是插入位置索引,第二个是插入内容 改变原有字符串
  • 替换:replace(被替换的字符串,新的字符串) 返回新的字符串(需要新变量接收)
1
2
let string_replace = String::from("I like rust. Learning rust is my favorite!");
let new_string_replace = string_replace.replace("rust", "RUST");

replacen(被替换的字符串,新的字符串,替换的个数) 返回新的字符串

replace_range(要替换的范围,新的字符串) 改变原有的字符串

  • 删除:pop() 删除并返回最后一个字符 改变原有的字符串

    remove(字符起始索引) 删除并返回指定位置的字符 改变原有的字符串

    truncate(字符起始索引) 删除指定位置至结尾的所有字符 改变原有字符串

    clear() 清空字符串

  • 连接:+/+= 相当于调用函数add(self, s:&str……) 第一个参数是string,其所有权会被转移,后面的参数需要&str类型 '+'返回新的字符串

    format!("{}", s) 用法与println!类似, 返回新的字符串

注:此处所有涉及索引的方法(包括切片),都是以字节为单位处理数据;对于UTF-8类型字符非常容易出错

结构体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
1、// 定义字段
struct 结构体名称 {
字段名称1: 类型 //结构体字段
}
gree
2、// 初始化:每个字段都要初始化,但顺序不一定一样
let 变量名 = 结构体名称 {
字段名称1: 值
}
let 变量名 = 结构体名称 {
字段名称1: 值
..另一个同类型变量2名的名称 // 剩余自动从另一变量中获取(该语句必须位于尾部)
// 同时变量2部分字段会发生所有权转移
}

3、// 访问字段
变量名.字段名

4、// 元组结构体、单元结构体
let a : (i32, f64, u8) = (500, 6.4, 1); // (i32, f64, u8)是元组
struct Color(i32, i32, i32); // 元组结构体,适用于结构体有名称,字段没有的情况
struct AlwaysEqual; // 单元结构体,没有属性与字段

5、 // 结构体数据所有权:字段值最好不要基于引用,否则需要加上生命周期

6、 // 正常情况无法{}打印,需要在开头加上#[derive(Debug)],使用{:?}或{:#?}来打印

枚举

枚举类型是一个类型,它会包含所有可能的枚举成员,而枚举值是该类型中的具体某个成员的实例

1
2
3
4
5
6
7
8
// 枚举变体携带数据
enum PokerCard {
Clubs(u8),
Spades(u8),
Diamonds(char),
Hearts(char),
} // 任何类型的数据都可以放入枚举成员中,包括另一个枚举或者结构体
let c1 = PokerCard::Spades(5);

数组

分为静态的array和动态数组vector,先看array

array可以正常使用下标访问,可以使用{:?}打印

1
2
3
4
5
6
let a = [1, 2, 3, 4, 5]; // 定义
let a: [i32; 5] = [1, 2, 3, 4, 5]; // 需要声明类型时
let a = [3; 5]; // 某个值重复出现
let arrays: [[u8; 3]; 4] = [one, two, blank1, blank2]; // 二维数组

let slice: &[i32] = &a[1..3]; // 数组切片

流程控制

if: if语句块是表达式,可以有返回值

for

1
2
3
4
5
6
7
8
9
10
11
12
for 元素 in 集合/0..集合.len() { // 注意,此处集合需要使用引用,否则所有权会被转移(如需更改加上mut)
}

// 想要获取元素的索引
let a = [4, 3, 2, 1];
for (i, v) in a.iter().enumerate() {
println!("第{}个元素是{}", i + 1, v); // .iter()方法把 `a` 数组变成一个迭代器
}

// 只在意循环次数
for _ in 0..10 {
}

continuebreak依然存在

while

1
while 条件 {}

loop

无条件循环,必须搭配break

break类似于return,可以单独使用也可以带回来一个返回值;

loop同样是表达式,可以返回一个值)

模式匹配

match和if let

match: 非常类似于switch(但匹配后只会执行当前分支,而不会往下”贯穿“)

match同样是表达式,可以有返回值

1
2
3
4
5
6
7
8
9
10
match target {
模式1 => 表达式1, // =>代替了:
模式2 | 模式3 => { // X|Y
语句1;
语句2;
表达式2 // 注意,语句同样可以返回()
},
_ => 表达式3 // _代替了default,必须穷尽所有情况否则会报错
//或者 任意无关变量名 => 表达式3 // 此时就可以对该变量操作,不操作记得使用_开头
}

模式绑定(从匹配到的分支中取出绑定的值)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
enum Coin {
Penny,
Nickel,
Dime,
Quarter(UsState), // 25美分硬币
}
match coin {
Coin::Penny => 1,
Coin::Nickel => 5,
Coin::Dime => 10,
Coin::Quarter(state) => {
println!("State quarter from {:?}!", state); // 可以取出绑定的具体state值
25
},
}

if let: 适用于只需要判断一个模式是否匹配的情况,比if更适用于匹配

1
2
3
4
let some_value = Some(5);
if let Some(v) = some_value {
println!("Value is: {}", v);
}

while let: while和let的总和,即如果满足条件就可循环,同样可以从模式匹配中拆出值

注:match/if let/while let都会转移被匹配值的借用值的所有权,需要使用ref抵消(ref只在左侧生效)

1
2
3
if let Some(ref x) = value
match opt {
Some(ref s) => println!("Got a reference to string: {}", s),

Option<T>

表示一个值是否存在的枚举(Some<T>T不是同一类型)

对于SomeNone可以不加Option::前缀

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
enum Option<T> {
Some(T), // 表示有值
None, // 表示无值
}

//存储
let x: Option<i32> = Some(42); // Some(42) 代表 x 里面存了 42
let y: Option<i32> = None; // None 代表没有值

//解构
match x {
Some(v) => println!("Value is: {}", v), // 取出 v
None => println!("No value"),
}
if let Some(v) = x {
println!("Value is: {}", v);
}

方法

impl中存储方法与struct中声明字段分开,同时一个结构体可以有多个impl

1
2
3
4
5
6
7
8
9
10
struct Rectangle {
width: u32,
height: u32,
}

impl Rectangle {
fn area(&self) -> u32 {
self.width * self.height
}
} // &self代替了self:&Self

注:

  • self 表示 Rectangle 的所有权转移到该方法中,这种形式用的较少

    &self 表示该方法对 Rectangle 的不可变借用

    &mut self 表示可变借用

  • 允许方法名和字段名相同

  • 在调用方法时只有.没有->

  • 枚举同样可以定义方法

关联函数

定义在结构体impl且没有self的函数

不能使用变量.函数()的方法调用,只能使用结构体名称::函数名(参数)来调用

比如String::from()

泛型

为了抽象不同的类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
fn 函数名<T>(变量名: T) -> T { // 函数泛型
struct Point<T> {
x: T,
y: T,
} // 结构体泛型,多个类型也可以声明如struct Point<T,U>
enum Result<T, E> {
Ok(T),
Err(E),
} // 枚举泛型,可以根据返回值的类型判断是否成功
struct Point<T, U> {
x: T,
y: U,
}

impl<T, U> Point<T, U> {
fn mixup<V, W>(self, other: Point<V, W>) -> Point<T, W> {
Point {
x: self.x,
y: other.y,
}
}
} // 结构体泛型,impl处需要另外声明,impl中的方法可以拥有自己的泛型
//对于结构体泛型,还可以为特定的泛型单独声明方法
  • 在使用T前需要先声明<T>T的名字可以随便取
  • 有时在调用泛型函数时需使用函数名::<具体类型>()来显式指定T的类型

const泛型

允许常量值成为泛型变量,语法为const N: usize,表示const泛型N,它的值基于usize

1
2
3
struct Buffer<T, const N: usize> {
data: [T; N], // N 作为数组大小
}

const fn: 在函数声明前加上const关键字

注:const泛型与const fn都需要在编译时确定,const fn就可以用于给const泛型赋值

特征

定义了一组可以被共享的行为,只要实现了特征,你就能使用这组行为(类似于接口)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
pub trait Summary {
fn summarize(&self) -> String; // 只是一个抽象接口,而不具体实现
} // 定义特征

// 为每个需要的类单独实现特征
impl Summary for Post {
fn summarize(&self) -> String {
format!("文章{}, 作者是{}", self.title, self.author)
}
}
impl Summary for Weibo {
fn summarize(&self) -> String {
format!("{}发表了微博{}", self.username, self.content)
}
}

注:

  • 孤儿规则:如果你想要为类型 A 实现特征 T,那么 A 或者 T 至少有一个是在当前作用域中定义的(另一个可以在其他库中引入)
  • 默认实现:可以在特征中定义具有默认实现的方法,这样其它类型无需再实现该方法,或者也可以选择重载该方法(默认实现允许调用特征中其他方法,哪怕这个方法没有默认实现)

特征约束

特征作为函数参数:

1
2
3
pub fn notify(item: &impl Summary) { // 实现了Summary特征 的 item 参数
println!("Breaking news! {}", item.summarize());
}

语法:

1
2
3
4
5
6
pub fn notify<T: Summary>(item: &T) {
println!("Breaking news! {}", item.summarize());
}
// 对于结构体方法
impl<T: Display> ToString for T {
}

形如 T: Summary 被称为特征约束

多重约束:

1
2
3
// 要求同时实现了两个特征的参数
pub fn notify(item: &(impl Summary + Display)) {}
pub fn notify<T: Summary + Display>(item: &T) {}

where约束

1
2
3
4
5
fn some_function<T: Display + Clone, U: Clone + Debug>(t: &T, u: &U) -> i32 {}
fn some_function<T, U>(t: &T, u: &U) -> i32
where T: Display + Clone,
U: Clone + Debug
{}

函数返回值:

通过 impl Trait 来说明一个函数返回了一个类型,该类型实现了某个特征

1
fn returns_summarizable() -> impl Summary { // 返回一个实现了Summary特征的类型

特征对象

特征约束 特征对象
impl Trait dyn Trait
接收所有实现了Trait的类型 接收所有实现了Trait的类型
认为是不一样的类型,不能一起存储 认为是相同的类型,可以一起存储
静态分发,编译时确定 动态分发,运行时确定

允许你使用 不同类型 但 实现了相同特征 的对象,使它们可以在 同一个变量、参数或返回值 中使用

1
2
3
4
5
6
// 语法
&dyn 特征名 // 必须要使用指针,否则无法确定大小
Box<dyn 特征名> // 智能指针

// 动态数组
Vec<Box<dyn 特征名>>

集合类型

动态数组Vector

使用Vec<T>表示,只能存储相同类型的数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// 创建数组
let v: Vec<i32> = Vec::new();
let mut v = Vec::new(); // 在添加元素后会自动推导
let mut v: Vec<i32> = Vec::with_capacity(5); // 预先分配空间

let v = vec![1, 2, 3]; // 宏vec!可以给予初始值
let v = vec![0; 3]; // 默认值为 0,初始长度为 3
let v_from = Vec::from([0, 0, 0]);

// 更新(需要为mut)
v.push(n); // 可变引用,不能与其他引用同时存在

// 访问元素
v[下标] // 越界不会检查
v.get(下标) // 返回Option<T>,需要match来解构出值 确保不会越界

// 遍历
for i in &(mut) v {}

// 常见方法
v.is_empty()
v.insert(pos, val) // 在指定索引pos处插入数值val
v.remove(pos) // 删除在pos处的数并返回该数
v.pop() // 删除尾部的数并返回(返回的是Option<T>的枚举值)
v.clear()
v.append(&mut v1) // v1所有数据全部转入v,v1被清空

// 排序
sort/sort_unstable() // 默认按照升序类型,且要元素可比较
sort_by/sort_unstable_by(闭包实现) // 可以自定义比较规则来实现多种类型的比较

注:可以通过使用枚举类型和特征对象来实现不同类型元素的存储

KV存储HashMap

需要使用use std::collections::HashMap;来引入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 创建与插入
let mut my_gems = HashMap::new();
my_gems.insert("红宝石", 1);

HashMap::with_capacity(capacity)

let teams_list = vec![
("中国队".to_string(), 100),
("美国队".to_string(), 10),
("日本队".to_string(), 50),
];
let teams_map: HashMap<_,_> = teams_list.into_iter().collect(); // 从动态数组转化为hashmap

// 在表中查询元素
let score: Option<&value类型> = 表名.get(key的引用); // 注意返回的是Option<T>类型

// 遍历
for (key, value) in &表名 {
println!("{}: {}", key, value);
}

// 更新表中的值
let old = scores.insert("Blue", 20); // 会直接覆盖旧值,返回Some(旧值)/None
let v = scores.entry("Yellow").or_insert(5); // 查询Yellow对应的值,若不存在则插入新值;返回存储值的可变引用

HashMap 的所有权规则与其它 Rust 类型没有区别:

  • 若类型实现 Copy 特征,该类型会被复制进 HashMap,因此无所谓所有权
  • 若没实现 Copy 特征,所有权将被转移给 HashMap 中(使用引用要确保其生命周期足够长)

生命周期

变量的生命周期声明方式:

1
2
&'a i32     // 具有显式生命周期的引用
&'a mut i32 // 具有显式生命周期的可变引用

函数中的生命周期

需要标注生命周期的情况如下:

  • 首先返回值必须是引用类型,可能会出现悬垂引用错误
  • 存在多个参数时,如果编译器无法确定返回值需要跟随哪个参数的生命周期(哪怕这两个参数的生命周期是一样的),那么不标注就会报错
  • 标注之后,编译时就会检查返回值使用会不会超出某个参数,如果发现超出就会报错(标注生命周期实际上不会更改任何返回值或者变量的真实生命周期,只是告诉编译器当返回值的生命周期不与较短的参数生命周期一致时,不予通过)
1
2
3
4
5
6
7
8
9
10
// 用'a显式表示生命周期,此处的'a表示两个参数中较短的生命周期,需要提前标注
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {

// 特殊情况:返回值永远只和一个参数有关/返回值与参数无关
fn longest<'a>(x: &'a str, y: &str) -> &'a str { // 只与一个有关就只标注一个
x
}
fn longest(_x: &str, _y: &str) -> String { // 与新建变量有关尽量不返回引用
String::from("really long string")
}

结构体中的生命周期

如果结构体的字段值类型为引用型,也需要标注生命周期'aa可以任意替换)

作用是避免编译器报错、同时(提醒编译器)在编译时就检查其是否不超过原变量的生命周期

1
2
3
struct ImportantExcerpt<'a> {
part: &'a str,
} // 只要在结构体每一个引用标注上生命周期即可,此处也需要提前声明<>

生命周期声明消除

为何在只有一个参数时可以不标注生命周期?

存在以下三个步骤可以省略生命周期声明(函数中参数的生命周期是输入生命周期,返回值为输出):

  1. 每一个引用参数都会获得独自的生命周期(所以不声明则多个参数有各自的生命周期声明)

    1
    fn foo<'a, 'b>(x: &'a i32, y: &'b i32) // 所以不显式标出不知道跟随a还是b
  2. 若只有一个输入生命周期(函数参数中只有一个引用类型),那么该生命周期会被赋给所有的输出生命周期,也就是所有返回值的生命周期都等于该输入生命周期

    1
    fn foo<'a>(x: &'a i32) -> &'a i32 // 所以单个参数可以省略
  3. 若存在多个输入生命周期,且其中一个是 &self&mut self,则 &self 的生命周期被赋给所有的输出生命周期

方法中的生命周期

  • 类似于泛型结构体
  • 方法签名中一般不需要标注,因为有&self参数(根据以上第三条规则)
1
2
3
4
5
6
7
8
9
struct ImportantExcerpt<'a> {
part: &'a str,
}

impl<'a> ImportantExcerpt<'a> {
fn level(&self) -> i32 {
3
}
}

静态生命周期

拥有'static生命周期声明的引用生命周期是整个程序

1
let s: &'static str = "我没啥优点,就是活得久,嘿嘿";

属性

属性是一种元数据,用于修改编译器的行为、提供额外信息或影响代码生成方式

使用#[]语法

常见类型

#[derive()] 自动派生特征

用于让编译器自动为结构体或枚举实现特定的 trait(特征),如 DebugClone

注意只针对结构体与枚举,同时在实现某特征时(比如Copy)结构体中不能够有String这种无法自动实现Copy的字段

#[cfg(...)] 条件编译

用于根据特定 条件选择性地编译代码,例如目标平台:

1
2
3
4
5
6
7
8
9
#[cfg(target_os = "linux")]
fn platform_specific() {
println!("Running on Linux!");
} // 只在linux上面编译

#[cfg(feature = "logging")]
fn log_message() {
println!("Logging is enabled");
} // 启用了feature特征才能编译(feature特征是cargo.toml中定义的)

#[test] Rust 测试函数

用于标记测试函数,让 cargo test 自动运行它

错误处理

panic

  • 标识不可恢复错误
  • 有被动与主动触发两种情况

主动触发:使用panic!

1
2
3
fn main() {
panic!("crash and burn");
} // 会打印出一个错误信息,展开报错点往前的函数调用堆栈,最后退出程序

Result

标识可恢复的错误

1
2
3
4
enum Result<T, E> {
Ok(T),
Err(E),
}

返回了该枚举类型之后就可以使用match来匹配解析

1
2
3
4
5
6
7
8
9
10
let f = match f {
Ok(file) => file,
Err(error) => match error.kind() {
ErrorKind::NotFound => match File::create("hello.txt") {
Ok(fc) => fc,
Err(e) => panic!("Problem creating the file: {:?}", e),
},
other_error => panic!("Problem opening the file: {:?}", other_error),
},
}; // 一个打开文件的返回处理

如果不需要处理错误情况(即要么Ok()要么panic(),就使用unwrap()/expect

1
2
3
4
5
6
7
8
let f = File::open("hello.txt").unwrap();
// 要么返回正确值要么直接panic

let f = File::open("hello.txt").expect("Failed to open hello.txt");
// 与unwrap()一样,只不过会报出里面的信息

// 改变错误类型:假设有f1(T)返回值T1类型,f2(F)返回值F2类型
let n: u8 = "1".parse().map(f1).map_err(f2) //原本返回T/F,现在返回T1/F1

传播错误

如果需要上级来处理这个函数中出现的错误呢?

返回Result<, >类型

  • 使用match匹配,用分支来操作/返回
  • 使用宏?

?功能类似于match

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// match写法
let f = File::open("hello.txt");
let mut f = match f {
Ok(file) => file,
Err(e) => return Err(e),
};
let mut s = String::new();
match f.read_to_string(&mut s) {
Ok(_) => Ok(s),
Err(e) => Err(e),
}

// ?写法:Err则该函数返回,Ok则语句返回正确值
let mut f = File::open("hello.txt")?;
let mut s = String::new();
f.read_to_string(&mut s)?;
Ok(s)
// ?同时可以进行类型提升,把所有的错误类型都提升为std::error::Error
// 此时就是返回Result<Ok值, Box<dyn std::error::Error>>

// ?可以链式调用
let mut s = String::new();
File::open("hello.txt")?.read_to_string(&mut s)?;
Ok(s)

注:

  • ?操作符一定需要一个变量来承接正确的值
  • 函数一定要是Result<, >返回值

OptionResult的转换

1
2
3
Option`转`Result`: 使用`.ok_or()`或`.ok_or_else()
// Option<T> Result<T, E>
let res1: Result<T, E> = Option类型值.ok_or(E类型值);
1
2
3
4
5
6
Result`转`Option`: 丢弃错误使用`ok()`,丢弃成功值使用`.err()
// Option<T> Result<T, E>
let opt1: Option<T> = Result类型值.ok();

// Option<E> Result<T, E>
let opt1: Option<E> = Result类型值.err(); // 如果Result类型值是ok()则丢弃

包与模块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
my_project/
├── Cargo.toml
└── src/
├── lib.rs # library crate (名为 my_project)
├── main.rs # binary crate (名为 my_project)
└── bin/
├── tool1.rs # binary crate (名为 tool1)
└── tool2.rs # binary crate (名为 tool2)
├── tests # 集成测试文件
│ └── some_integration_tests.rs
├── benches # 基准性能测试文件
│ └── simple_bench.rs
└── examples # 项目示例
└── simple_example.rs
1
Package` => `Crate` => `mod

Package(包)

一个Package就是一个项目,包含一个或多个Crate(最多一个)

每个 Package 必须包含一个 Cargo.toml 文件来描述包的元信息和依赖

Crate(单元/箱)

  • crate 是一个 Rust 项目或库的最小单元,即需要一起编译不可继续拆分
  • 分为lib单元(入口文件一般为src/lib.rs;编译为库文件.rlib;不可单独执行,可以为其他项目提供依赖)和二进制单元(入口文件一般为src/main.rs或者在 src/bin/ 目录下;编译为可执行文件)
  • 一个Package最多可以包含一个库单元和多个二进制单元,也可以只包含一个库单元/一个或几个二进制单元
  • 对于二进制单元,src/main.rs是默认的crate,其他的crate都在src/bin/(或其他)目录下,且文件可以单独编译(一个文件就是一个crate

考虑划分多个 crate 当:

  1. 部分代码需要作为独立库被其他项目使用
  2. 项目包含多个独立可执行工具
  3. 某些功能需要单独编译和测试
  4. 需要减少编译时间(修改一个 crate 不会导致其他 crate 重新编译)

Mod(模块)

使用模块只是为了更好地组织代码,同时控制它们的可见性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 定义语法
mod A {
mod B {fn B1(){}} //可以嵌套
mod C {fn C1(){}}
}

// 路径引用
fn D() {
// 绝对路径
crate::A::B::B1();
//相对路径:只能以super/self/模块名或Crate开头
A::B::B1(); // 在同一个Crate根部的相对路径可以直接这么写
}

// 可见性设置
pub mod hosting { // 模块写pub仅代表其可被访问,而其中的函数等还是对外界不可见
pub fn add_to_waitlist() {} // 函数也需以pub开头
}
  • 一个Crate是一棵模块树,而src/main.rssrc/lib.rs就是该树的根
  • 模块A包含模块B,则A是B的父模块,B是A的子模块
  • 模块中可以定义各种Rust类型,如函数、结构体、枚举、特征等
  • 在同一个Crate根下的模块,相互引用的相对路径可以直接以对方模块名称开头;在同一父模块下的两个子模块,若在同文件中实现则也可以以对方模块名称开头,否则需要通过super::来使用父模块中转
  • 将结构体设置为 pub,但它的所有字段依然是私有的;将枚举设置为 pub,它的所有字段也将对外可见
  • 可以把模块实现放入对应等级的*.rs文件中,*要等同于模块名(文件中便不必再写),模块的定义/声明还是在父文件/模块中

use

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 基本引用方式:绝对或相对路径
use crate::front_of_house::hosting; // 引入模块
use front_of_house::hosting::add_to_waitlist; // 引入函数

// as别名
use std::fmt::Result;
use std::io::Result as IoResult;

// 引入再导出
pub use crate::front_of_house::hosting;

// 简化引入
use std::collections::{HashMap,BTreeMap,HashSet};
use std::{cmp::Ordering, io};

use std::io;
use std::io::Write;
use std::io::{self, Write};

use std::collections::*; // 引入模块下所有项

注:

  • 如果引入的函数存在同名的情况时,需使用模块名::函数名的方式或者as别名的方式来区分

限制可见性

  • pub 意味着可见性无任何限制
  • pub(crate) 表示在当前包可见
  • pub(self) 在当前模块可见
  • pub(super) 在父模块可见
  • pub(in <path>) 表示在某个路径代表的模块中可见,其中 path 必须是父模块或者祖先模块

函数式编程

简单来说,迭代器/高阶函数是“流水线模板”,提供规范流程(比如map\filter等等);闭包是“可替换的工具”,即灵活调整传入的参数;而这两者都需要满足“不可变性”的安全要求

闭包

闭包是一种匿名函数,它可以赋值给变量也可以作为参数传递给其它函数,不同于函数的是,它允许捕获调用者作用域中的值

闭包语法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 定义闭包
|param1, param2,...| {
语句1;
语句2;
返回表达式
}
|param1| 返回表达式 // 只需要有一个表达式时
|| {} // 如果不需要参数时

// 结构体中的闭包
struct Cacher<T>
where T: Fn(u32) -> u32,
{
query: T,
} // 等价于struct Cacher<T: Fn(u32) -> u32>,query字段同样也可以使用一个符合的函数作为值

注:

  • 闭包函数中是否标注类型皆可(如果未使用过则需要标注),同样可以以此省略返回值
  • 闭包函数中的类型不可以是泛型,所以每次使用参数要求同类型

三种Fn特征

FnOnce: 强制需要闭包所捕获变量的所有权

FnMut: 用于闭包函数内需要改变被捕获变量的值的情况,需要闭包和捕获变量都有mut声明

Fn: 以不可变借用的方式捕获环境中的值(与FnMut不兼容,即不可改变捕获函数的值)

注:

  • FnOnce作为传入闭包的特征约束时,传入闭包和其捕获函数的所有权都会在第一次调用时被消耗;特殊情况:同时要求FnOnceCopy(闭包会实现Copy,而其捕获的变量也会尽量实现Copy)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    fn main() {
    let x = vec![1, 2, 3];
    fn_once(|z|{z == x.len()})
    }

    fn fn_once<F>(func: F)
    where
    F: FnOnce(usize) -> bool,
    {
    println!("{}", func(3)); // 捕获的Vec的所有权,闭包与变量一起消耗
    println!("{}", func(4));
    }

    fn fn_once<F>(func: F)
    where
    F: FnOnce(usize) -> bool + Copy,// 改动在这里
    {
    println!("{}", func(3)); // 闭包实现Copy,不消耗;尽可能捕获可Copy的值如x.len(),没有则会在编译报错
    println!("{}", func(4));
    }
  • 由上所知,闭包的捕获行为会根据上下文约束来调整

  • 闭包自动实现Copy特征的规则是,只要闭包捕获的类型都实现了Copy特征的话,这个闭包就会默认实现Copy特征

  • FnOnce会消耗闭包的所有权;但无论按值还是按引用传递,Fn/FnMut通常都不会消耗闭包的所有权。即在传入一个有Fn(Mut)特征约束的函数之后,一个闭包函数的变量还可以继续使用

  • 所有的闭包都自动实现了 FnOnce 特征,因此任何一个闭包都至少可以被调用一次;没有移出所捕获变量的所有权的闭包自动实现了 FnMut 特征;不需要对捕获变量进行改变的闭包自动实现了 Fn 特征

move

1
let update_string =  move || println!("{}",s); // move强制闭包获取变量所有权

闭包作为函数返回值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
fn factory() -> Fn(i32) -> i32 {
let num = 5;
|x| x + num
} // 报错,特征不是类型,需要其他辅助声明

fn factory(x:i32) -> impl Fn(i32) -> i32 {
let num = 5;
if x > 1{
move |x| x + num
} else {
move |x| x - num
}
} // 报错,返回的内容要求是同一类型,此处虽然满足同一特征,但属于不同类型

fn factory(x:i32) -> Box<dyn Fn(i32) -> i32> {
let num = 5;
if x > 1{
Box::new(move |x| x + num)
} else {
Box::new(move |x| x - num)
}
} // 正确,使用智能指针将其视为同一类型

迭代器Iterator

迭代器允许我们迭代一个连续的集合,例如数组、动态数组 VecHashMap 等,在此过程中,只需关心集合中的元素如何处理,而无需关心如何开始、如何结束、按照什么样的索引去访问

1、.next是迭代器中取下一个值的方式,返回Option<T>

1
2
3
4
5
pub trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
// 省略其余有默认实现的方法
} // 迭代器实现的特征Interator

2、将数组转化为迭代器的三种方式(Vec动态数组实现的IntoIterator中的函数):

  • into_iter 会夺走所有权
  • iter 是借用
  • iter_mut 是可变借用(next方法返回的&mut

3、迭代器的消费者与适配器(都是迭代器特征中的方法)

  • 消费者:消费掉迭代器,返回一个值

    会拿走迭代器的所有权,即调用它之后迭代器无法再使用

  • 适配器:返回一个新的迭代器,是链式调用的基础

    因此在链式调用末尾需要一个消费者来收尾用以返回一个值

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    //例1
    let v1: Vec<i32> = vec![1, 2, 3];

    let v2: Vec<_> = v1.iter().map(|x| x + 1).collect();
    // collect():消费掉迭代器,把值收集成特定类型(需要显式注明)
    // .map():对迭代器的每一个值操作,换为另一个新值

    assert_eq!(v2, vec![2, 3, 4]);

    //例2
    let names = ["sunface", "sunfei"];
    let ages = [18, 18];
    let folks: HashMap<_, _> = names.into_iter().zip(ages.into_iter()).collect();
    // .zip():将两个迭代器压缩在一起,形成Iterator<Item=(ValueFromA, ValueFromB)> 这样的新的迭代器

    //例3:闭包用作适配器参数
    fn shoes_in_size(shoes: Vec<Shoe>, shoe_size: u32) -> Vec<Shoe> {
    shoes.into_iter().filter(|s| s.size == shoe_size).collect()
    } // filter():对迭代器每个值进行过滤,若满足则保留
    // 此处闭包同样可以捕捉环境变量

深入类型

类型转换

as转换

1
2
3
let a = 3.1 as i8;
let b = 100_i8 as i32;
let c = 'a' as u8; // 将字符'a'转换为整数,97

注:

转换不具有传递性:就算 e as U1 as U2 是合法的,也不能说明 e as U2 是合法的(e 不能直接转换成 U2

TryInto转换

1
2
3
let a: u8 = 10;
let b: u16 = 1500;
let b_: u8 = b.try_into().unwrap(); //尝试进行一次转换,并返回一个 Result

注:try_into 转换会捕获大类型向小类型转换时导致的溢出错误

FromInto特征

  • From<T>:定义如何从类型 T 转换到当前类型。
  • Into<T>:自动为实现了 From 的类型生成反向转换。
1
2
3
4
5
6
7
8
9
10
impl From<i32> for MyType {
fn from(value: i32) -> Self {
MyType(value)


}
}

let a = MyType::from(42); // 显式调用
let b: MyType = 42.into(); // 自动推导(需类型注解)

newtype

使用元组结构体的方式将已有的类型包裹起来:struct Meters(u32);,那么此处 Meters 就是一个 newtype

  • 自定义类型可以让我们给出更有意义和可读性的类型名,例如与其使用 u32 作为距离的单位类型,我们可以使用 Meters,它的可读性要好得多
  • 对于某些场景,只有 newtype 可以很好地解决
  • 隐藏内部类型的细节

为外部类型实现外部特征

孤儿规则:要为类型 A 实现特征 T,那么 A 或者 T 必须至少有一个在当前的作用范围内

1
2
3
4
5
6
7
8
9
10
// 例:想为Vec实现Display特征,但这两个都在标准库中
use std::fmt;
struct Wrapper(Vec<String>);
impl fmt::Display for Wrapper {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "[{}]", self.0.join(", "))
}
}

// 注:包裹一样类型的newtype是不同的类型,newtype与其内部包裹的类型同理

类型别名

1
type Meters = u32

注:类型别名仅仅为了更好的可读性,与原类型没有任何区别

1
2
3
4
5
6
7
8
// 应用:减少代码模板的使用
let f: Box<dyn Fn() + Send + 'static> = Box::new(|| println!("hi"));

type Thunk = Box<dyn Fn() + Send + 'static>;
let f: Thunk = Box::new(|| println!("hi"));

//常用于简化Result<T, E> 枚举中
type Result<T> = std::result::Result<T, std::io::Error>; // 此处为std::io库中Error类型的简化

不定长类型DST

定长类型:基础类型、集合 VecStringHashMap 等(其在栈上拥有固定大小的指针)

不定长类型:str、特征对象

1
2
3
fn foobar_1(thing: &dyn MyThing) {}     // OK
fn foobar_2(thing: Box<dyn MyThing>) {} // OK
fn foobar_3(thing: MyThing) {} // ERROR!

注:只能间接使用DST,通过引用或Box来使用

Sized特征

怎么保证泛型参数是固定大小的类型?

1
2
fn generic<T(: Sized)>(t: T) { // 自动补全了Sized特征
}

枚举与整数

枚举到整数很容易,但反过来需要借助三方库来实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
enum MyEnum {
A = 1,
B,
C,
}
fn main() {
// 将枚举转换成整数,顺利通过
let x = MyEnum::C as i32;
// 将整数转换为枚举,失败
match x {
MyEnum::A => {}
MyEnum::B => {}
MyEnum::C => {}
_ => {}
}
}

// 使用num-traits/num-derive库
use num_derive::FromPrimitive;
use num_traits::FromPrimitive;

match FromPrimitive::from_i32(x) {
Some(MyEnum::A) => println!("Got A"),
Some(MyEnum::B) => println!("Got B"),
Some(MyEnum::C) => println!("Got C"),
None => println!("Couldn't convert {}", x),
}

智能指针

特性 引用(&T/&mut T 智能指针(如 Box<T>Rc<T>
所有权关系 无所有权,仅是借用 通常拥有数据的所有权
可变性控制 分为共享引用(&T)和可变引用(&mut T 通过内部可变性(如 RefCell<T>)或类型设计实现
生命周期 必须显式或隐式标注生命周期 通常管理数据的整个生命周期(如 Box 负责释放)
动态行为 仅提供访问,无额外逻辑 可附加逻辑(如引用计数、自动释放、线程安全)
常见类型 &T, &mut T Box<T>, Rc<T>, Arc<T>, RefCell<T>

智能指针与普通自定义结构体区别:实现了DerefDrop特征

智能指针用于一些较引用更复杂的场景

Box<T>堆对象分配

Box 简单的封装,用于将值存储在堆上

使用场景:

  • 特意的将数据分配在堆上
  • 数据较大时,又不想在转移所有权时进行数据拷贝
  • 类型的大小在编译期无法确定,但是我们又需要固定大小的类型时
  • 特征对象,用于说明对象实现了一个特征,而不是某个特定的类型
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 将数据存储在堆上
let a = 3; // a在栈上
let a = Box::new(3); // 在堆上

// 避免栈上数据拷贝
let arr = [0;1000];
let arr1 = arr; // 此时两份数据,是深拷贝

let arr = Box::new([0;1000]);
let arr1 = arr; // 所有权顺利转移给 arr1,arr 不再拥有所有权

// 提供固定大小
enum List {
Cons(i32, List), // 递归类型:无法确定大小,因为DST报错
Nil,
}
enum List {
Cons(i32, Box<List>),
Nil,
}

// 特征对象
// 想实现不同类型组成的数组只有两个办法:枚举和特征对象
// 特征对象其实就是把DST类型的特征转为固定大小

另:Box::leak可以真正将一个运行期的值转化为'static,如果只标注'static可能无法成功

Deref解引用

1
Deref` 可以让智能指针像引用那样工作,这样你就可以写出同时支持智能指针和引用的代码,例如 `*T

\*: 对常规引用使用*操作符,即可以通过解引用的方式获取到内存地址对应的数据值

智能指针解引用: 在使用给定的智能指针时,直接使用*解引用即可

在智能指针解引用时,实际上调用了*(y.deref())方法:y.deref()先返回了值的常规引用

1
2
3
4
5
6
7
8
// 如果要实现自己的智能指针同样要实现Deref特征
use std::ops::Deref;
impl<T> Deref for MyBox<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.0
}
} // 实现该特征后才能使用*解引用

函数/方法中的隐式Deref转换

函数和方法的传参中有Deref的隐式转换。

若一个类型实现了 Deref 特征,那它的引用在传给函数或方法时,会根据参数签名来决定是否进行隐式的 Deref 转换(Deref支持连续的隐式转换)

总结

  • 一个类型为 T 的对象 foo,如果 T: Deref<Target=U>,那么,相关 foo 的引用 &foo 在应用的时候会自动转换为 &U
  • 在解引用时自动把智能指针和 &&&&v 做引用归一化操作,转换成 &v 形式,最终再对 &v 进行解引用(即将智能指针脱壳为内部的引用类型即&v, 把多级引用归一为一级&v

Drop释放资源

指定在一个变量超出作用域时,执行一段特定的代码,最终编译器将帮你自动插入这段收尾代码(无需在每一个使用该变量的地方,都写一段代码来进行收尾工作和资源释放)

1
2
3
4
5
impl Drop for Foo {
fn drop(&mut self) { // 传入的是可变借用
println!("Dropping Foo!")
}
}

Drop 的顺序

  • 变量级别,按照逆序的方式(_x_foo 之前创建,因此 _x_foo 之后被 drop
  • 结构体内部,按照顺序的方式

注:

  • Rust 自动为几乎所有类型都实现了 Drop 特征(除了栈上的简单类型)
  • 不允许显式地调用析构函数变量名.drop(),但可以调用函数drop(变量名)drop()函数会拿走目标值的所有权)
  • CopyDrop互斥,不会在一种类型上面出现(为了防止重复释放内存)

Rc

通过引用计数的方式,允许一个数据资源在同一时刻拥有多个所有者

实现机制就是 RcArc,前者适用于单线程,后者适用于多线程

Rc<T>

引用计数:通过记录一个数据被引用的次数来确定该数据是否正在被使用。当引用次数归零时,就代表该数据不再被使用,因此可以被清理释放

当我们希望在堆上分配一个对象供程序的多个部分使用且无法确定哪个部分最后一个结束时,就可以使用 Rc 成为数据值的所有者

1
2
let a = Rc::new(String::from("hello, world")); // 创建时引用计数+1,此时Rc::strong_count(&a) 返回的值是 1
let b = Rc::clone(&a); // clone 仅仅复制了智能指针并增加了引用计数,并没有克隆底层数据;同样可以使用a.clone()

注:

  • 这几个智能指针都是相同的所以Rc::strong_count(&a/b/c)皆可
  • 当其中一个变量离开作用域被销毁后,计数-1,但只有当计数为0时,这个指针和指向的底层数据才会销毁
  • Rc<T>指向的是底层数据的不可变应用(相当于有多个不可变引用)
  • 实现了Deref特征,可以直接使用里面的数值

Arc

  • 原子化的 Rc<T> 智能指针,保证我们的数据能够安全的在线程间共享
  • Rc的API完全相同
  • ArcRc 并没有定义在同一个模块,前者通过 use std::sync::Arc 来引入,后者通过 use std::rc::Rc

CellRefCell

解决问题(相较于引用):

  • 可以通过不可变引用来修改数据
  • 绕过编译期借用检查
  • 实现了部分可变性(比如标定结构体某个字段为内部可变)
操作 Cell<T> RefCell<T>
获取不可变访问 get()T(复制) borrow()Ref<T>
获取可变访问 set(new_value) borrow_mut()RefMut<T>
运行时检查 有(可能 panic)
适用类型 T: Copy(如 i32 任意 T(如 String

Cell

CellRefCell 在功能上没有区别,区别在于 Cell<T> 适用于 T 实现 Copy 的情况

1
2
3
4
5
let c = Cell::new(42);
let val = c.get(); // 复制值(42)
c.set(100); // 替换新值,仍然拥有所有权不会报错

let c = Cell::new(String::from("asdf")); // 这样会报错

RefCell

  • 允许通过不可变引用 (&T) 修改内部数据(内部可变性)。
  • 在运行时(而非编译期)检查借用规则,违反规则时触发 panic
1
let s = RefCell::new(String::from("hello, world")); // s为RefCell<T>类型
方法 行为
borrow() 获取不可变引用 (Ref<T>),增加不可变借用计数。若已有可变借用,则 panic
borrow_mut() 获取可变引用 (RefMut<T>),标记独占借用。若已有任何借用,则 panic
1
2
3
4
5
6
7
8
9
10
struct Logger {
logs: RefCell<Vec<String>>, // 内部可变
}

impl Logger {
fn log(&self, message: &str) {
// 通过不可变的 &self 修改 logs!
self.logs.borrow_mut().push(message.to_string());
}
}

注:RefCell 的核心机制是,将一个本应可变的数据(如 String)包裹在“壳子”(RefCell)里,然后通过这个壳子的不可变引用(&RefCell<T>),在运行时安全地修改内部数据

循环引用与自引用

面临问题:当使用RefCell<Rc<List>>时,可以a指向b,b再指向a,出现循环引用,最后Rc计数无法归0

Weak

仅保存一份指向数据的弱引用,不保证引用关系依然存在,无法阻止所引用的内存值被释放

Weak Rc
不计数 引用计数
不拥有所有权 拥有值的所有权
不阻止值被释放(drop) 所有权计数归零,才能 drop
引用的值存在返回 Some,不存在返回 None 引用的值必定存在
通过 upgrade 取到 Option<Rc<T>>,然后再取值 通过 Deref 自动解引用,取值无需任何操作

Weak 通过 use std::rc::Weak 来引入,它具有以下特点:

  • 可访问,但没有所有权,不增加引用计数,因此不会影响被引用值的释放回收
  • 可由 Rc<T> 调用 Rc::downgrade 方法转换成 Weak<T>
  • Weak<T> 可使用 upgrade 方法转换成 Option<Rc<T>>,如果资源已经被释放,则 Option 的值是 None
  • 常用于解决循环引用的问题

多线程并发编程

并发:同时存在多个动作

并行:可以同时执行多个动作

关系:并发程序可以由人编写,但只有有多个CPU内核时才可以并行执行;

并行一定并发,但只有多核时并发才能够并行

使用线程

风险

由于多线程的代码是同时运行的,因此我们无法保证线程间的执行顺序,这会导致一些问题:

  • 竞态条件(race conditions),多个线程以非一致性的顺序同时访问数据资源
  • 死锁(deadlocks),两个线程都想使用某个资源,但是又都在等待对方释放资源后才能使用,结果最终都无法继续执行
  • 一些因为多线程导致的很隐晦的 BUG,难以复现和解决

创建线程:thread::spawn

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
use std::thread;
use std::time::Duration;

fn main() {
let handle =
thread::spawn(|| {
for i in 1..10 {
println!("hi number {} from the spawned thread!", i);
thread::sleep(Duration::from_millis(1)); // thread::sleep 会让当前线程休眠指定的时间,随后其它线程会被调度运行
}
}); // 线程内部的代码使用闭包来执行

handle.join().unwrap(); // 让当前线程阻塞,直到它等待的子线程的结束

for i in 1..5 {
println!("hi number {} from the main thread!", i);
thread::sleep(Duration::from_millis(1));
}
} // main 线程一旦结束,程序就立刻结束,因此需要保持它的存活,直到其它子线程完成自己的任务

注:

  • 线程的启动结束时间点都是不固定的
  • 由上一条,为了保证子线程中的变量一直有效,在子线程的闭包中捕获了环境变量时,需要使用move来转移所有权
  • 主线程(main)退出时,会强制终止所有子线程(无论它们是否在运行);父线程(非主线程)退出时,不会影响它创建的子线程
  • thread::spawn 的返回值是std::thread::JoinHandle类型,表示对线程的控制权,允许主线程通过 join() 等待子线程结束,同样可以使用数组收集

多线程的性能

当任务是 CPU 密集型时,就算线程数超过了 CPU 核心数,也并不能帮你获得更好的性能

当你的任务大部分时间都处于阻塞状态时,就可以考虑增多线程数量(典型就是网络 IO 操作)

线程屏障Barrier

让多个线程都执行到某个点后,才继续一起往后执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
use std::sync::{Arc, Barrier};
use std::thread;

fn main() {
let barrier = Arc::new(Barrier::new(3));
let mut handles = vec![];

for i in 0..3 {
let barrier = barrier.clone();
handles.push(thread::spawn(move || {
println!("线程 {}: 阶段1", i);
barrier.wait(); // 等待所有线程完成阶段1
println!("线程 {}: 阶段2", i);
}));
}

for handle in handles {
handle.join().unwrap();
}
}

注:

  • 需要Arc智能指针,作用是允许多个线程同时拥有同一数据(跨线程Rc
  • Barrier::new(n)中的n值一定要与实际调用wait()的线程数相等

多线程局部变量

标准库thread_local

1
2
3
4
5
6
7
8
9
10
11
12
// 定义
thread_local! {
static MY_TLS: 类型 = 初始化值; // 必须使用static声明为全局变量,一般使用RefCell/Cell/Mutex包裹
}

// 语法:变量名.with(|绑定名| { 操作 }); 闭包传入参数即为局部变量
thread::spawn(|| {
// 每个线程独立操作 COUNTER
COUNTER.with(|c| {
*c.borrow_mut() += 1;
println!("Thread {:?}: {}", thread::current().id(), c.borrow());
});

注:如果想使用多个局部变量的闭包函数,使用嵌套

同样还有使用use thread_local::ThreadLocal;引用的三方库,这个库不仅仅使用了值的拷贝,而且还能自动把多个拷贝汇总到一个迭代器中,最后进行求和

条件控制线程的挂起和执行:let pair = Arc::new((Mutex::new(false), Condvar::new()));

只会调用一次的函数:static INIT: Once =Once::new();

1
INIT.call_once(|| {unsafe {VAL = 2;}});

线程同步

消息传递

线程通过发送和接收消息来通信,而非直接共享内存

标准库工具mpsc,允许多发送者,单接收者

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
use std::sync::mpsc;

// 创建一个消息通道, 返回一个元组:(发送者,接收者)
let (tx, rx) = mpsc::channel();

// 创建线程,并发送消息
thread::spawn(move || {
tx.send(1).unwrap(); // 发送一个数字1, send方法返回Result<T,E>,通过unwrap进行快速错误处理

// 下面代码将报错,因为编译器自动推导出通道传递的值是i32类型,那么Option<i32>类型将产生不匹配错误
// tx.send(Some(1)).unwrap()
});

// 在主线程中接收子线程发送的消息并输出
println!("receive {}", rx.recv().unwrap());

// 尝试接收一次消息,不会阻塞线程,当通道中没有消息时,它会立刻返回一个错误
println!("receive {:?}", rx.try_recv());


// 连续接收消息
thread::spawn(move || {
let vals = vec![
String::from("hi"),
String::from("from"),
String::from("the"),
String::from("thread"),
];
for val in vals {
tx.send(val).unwrap();
thread::sleep(Duration::from_secs(1));
}
});

for received in rx {
println!("Got: {}", received);
}

// 使用多发送者:克隆发送者,其余的线程拿走拷贝
let tx1 = tx.clone();
thread::spawn(move || {
tx1.send(String::from("hi from cloned tx")).unwrap();
});
for received in rx {
println!("Got: {}", received);
}// 需要所有的发送者都被drop掉后,接收者rx才会收到错误,进而跳出for循环,最终结束主线程;
// 两个子线程谁先创建完成是未知的,哪条消息先发送也是未知的

// 同步通道设置
let (tx, rx)= mpsc::sync_channel(n); // n用来指定同步通道的消息缓存条数
  • tx,rx对应发送者和接收者,它们的类型由编译器自动推导: tx.send(1)发送了整数,因此它们分别是mpsc::Sender<i32>mpsc::Receiver<i32>类型,(一旦类型被推导确定,该通道就只能传递对应类型的值)
  • 接收消息的操作rx.recv()会阻塞当前线程,直到读取到值,或者通道被关闭
  • 需要使用movetx的所有权转移到子线程的闭包中
  • 使用通道来传输数据,一样要遵循 Rust 的所有权规则:
    • 若值的类型实现了Copy特征,则直接复制一份该值,然后传输过去,例如之前的i32类型
    • 若值没有实现Copy(如String类型),则它的所有权会被转移给接收端,在发送端继续使用该值将报错
  • 异步中只有接收者会被阻塞,同步中发送者也会因为接收者接收不到消息被阻塞
  • 所有发送者被drop或者所有接收者被drop后,通道会自动关闭
锁、Condvar

使用共享内存来实现同步性

面临问题:多个线程同时修改同一数据时,结果不可预测;线程执行顺序影响最终结果

互斥锁Mutex

同一时间,只允许一个线程A访问该值,其它线程需要等待A访问完成后才能继续

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// 创建实例:锁的容器
let m = Mutex::new(5);
{
// lock返回的是Result
let mut num = m.lock().unwrap(); // .lock()向m申请锁的所有权(简称“锁”):在获取锁之前会阻塞线程。同时只能有一个线程获得锁
// 当拥有锁的线程panic,其他线程永远得不到这个锁
*num = 6;
// 锁自动被drop
}

// 多线程中使用锁
use std::sync::{Arc, Mutex};
use std::thread;

let counter = Arc::new(Mutex::new(0));
let mut handles = vec![];

for _ in 0..10 {
let counter = Arc::clone(&counter);
// 创建子线程,并将`Mutex`的所有权拷贝传入到子线程中,子线程需要通过move拿走锁的所有权
let handle = thread::spawn(move || {
let mut num = counter.lock().unwrap();
*num += 1;
});
handles.push(handle);
}
// 等待所有子线程完成
for handle in handles {
handle.join().unwrap();
}
  • m.lock()返回一个智能指针MutexGuard<T>,拥有Deref特征(自动解引用获取引用类型)与Drop特征(超出作用域自动释放锁)
  • Rc<T>/RefCell<T>用于单线程内部可变性, Arc<T>/Mutex<T>用于多线程内部可变性

死锁

  • 在另一个锁还未被释放时去申请新的锁,就会触发
  • 当我们拥有两个锁的容器,且两个线程各自使用了其中一个锁,然后试图去访问另一个锁时,就可能发生死锁
  • try_lock(): 尝试去获取一次锁,如果无法获取会返回一个错误,因此不会发生阻塞
1
2
let guard = MUTEX2.lock().unwrap();
let guard = MUTEX2.try_lock(); // 返回错误

读写锁RwLock

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
Mutex`会对每次读写都进行加锁,但某些时候,我们需要大量的并发读,`Mutex`就无法满足需求了,此时就可以使用`RwLock
use std::sync::RwLock;

fn main() {
let lock = RwLock::new(5);

// 同一时间允许多个读
{
let r1 = lock.read().unwrap();
let r2 = lock.read().unwrap();
assert_eq!(*r1, 5);
assert_eq!(*r2, 5);
} // 读锁在此处被drop

// 同一时间只允许一个写
{
let mut w = lock.write().unwrap();
*w += 1;
assert_eq!(*w, 6);

// 以下代码会阻塞发生死锁,因为读和写不允许同时存在
// 写锁w直到该语句块结束才被释放,因此下面的读锁依然处于`w`的作用域中
// let r1 = lock.read();
// println!("{:?}",r1);
}// 写锁在此处被drop
}

我们也可以使用try_writetry_read来尝试进行一次写/读,若失败则返回错误

1
Err("WouldBlock")

条件变量Condvar控制线程同步

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
use std::sync::{Arc, Mutex, Condvar};

let pair = Arc::new((Mutex::new(false), Condvar::new()));
let (lock, cvar) = &*pair;

// 消费者线程(等待条件成立)
let consumer = thread::spawn(move || {
let mut condition = lock.lock().unwrap();
while !*condition {
condition = cvar.wait(condition).unwrap(); // 释放锁并等待
}
println!("Condition is now true!");
});

// 生产者线程(修改条件并通知)
thread::spawn(move || {
let mut condition = lock.lock().unwrap();
*condition = true; // 修改条件
cvar.notify_one(); // 唤醒消费者
});

consumer.join().unwrap();

消费者线程需等待条件成立才可执行:获取互斥锁检查条件是否成立 => 不成立则进入循环 => cvar.wait(condition).unwrap()执行时立即释放互斥锁(交由其他线程修改) => 其他线程修改后使用cvar.notify_one()唤醒该线程 => 返回条件重新检查

注:

  • notify_one()
    
    1
    2

    随机唤醒一个正在
    wait()
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18

    的线程(由操作系统调度决定)。

    - 如果有多个线程在等待,不保证顺序(可能是最早等待的,也可能是随机的)。
    - 如果没有线程在等待,这次通知会被丢弃(无效果)。

    - `notify_all()`: 唤醒所有正在 `wait()` 的线程,它们会竞争锁并依次检查条件。

    ##### `Atomic`原子类型与内存顺序

    为解决锁的性能问题而生,通过 CPU 的原子指令实现无锁线程安全操作

    | 特性 | 说明 |
    | ---------------- | ------------------------------------------------------------ |
    | **不可分割性** | 操作一旦开始,不会被其他线程或 CPU 中断。 |
    | **线程安全** | 多线程同时执行同一原子指令时,结果依然正确 |
    | **硬件直接支持** | 由 CPU 通过特定指令(如 `LOCK` 前缀)实现,而非软件模拟。 |
    | **内存顺序控制** | 通过 `Ordering` 参数指定操作前后的指令重排规则(如 `SeqCst`)。 |
    use std::sync::atomic::AtomicUsize;

let counter = AtomicUsize::new(0); // 常用场景是作为全局变量

counter.store(100, Ordering::Relaxed); // 存储值(写入)

let current = counter.load(Ordering::SeqCst);
println!(“Current value: {}”, current); // 加载值(读取)

let old = counter.fetch_add(10, Ordering::SeqCst); // 原子加法(返回旧值)旧值=100,新值=110

counter.fetch_sub(5, Ordering::Relaxed); // 原子减法 新值=105

counter.fetch_or(0b1, Ordering::Relaxed); // 原子位操作 按位或

1
2
3
4

**内存顺序**

面临问题:编译器可能导致指令重排

X = 1;
Y = 3;
X = 2; // 直接变成

X = 2;
Y = 3;

1
2
3
4
5
6
7
8
9

内存顺序指定

| **Ordering** | 作用 |
| ------------ | ------------------------------------------------------------ |
| `Relaxed` | 仅保证原子性,不保证顺序(性能最高) |
| `Release` | 写入操作:确保之前的指令不会被重排到它之后,(在这条指令前写入的数据)对其他线程可见 |
| `Acquire` | 读取操作:确保之后的指令不会被重排到它之前,能读到其他线程的修改 |
| `SeqCst` | 严格顺序一致性(性能最低,但最安全) |

use std::sync::atomic::{AtomicBool, Ordering};

let ready = AtomicBool::new(false);
let data = 42;

// 线程1:发布数据
thread::spawn(move || {
data = 100; // 非原子写入
ready.store(true, Ordering::Release); // 保证 data 写入对其他线程可见
});

// 线程2:读取数据
thread::spawn(move || {
while !ready.load(Ordering::Acquire) {} // 等待并同步内存
println!(“Data: {}”, data); // 保证看到 data=100
});

// 多线程需要用到Arc与clone

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15



### 全局变量

全局变量的生命周期肯定是`'static`,但是不代表它需要用`static`来声明

- 编译期初始化的全局变量,`const`创建常量,`static`创建静态变量,`Atomic`创建原子类型
- 运行期初始化的全局变量,`lazy_static`用于懒初始化,`Box::leak`利用内存泄漏将一个变量的生命周期变为`'static`

#### 编译期初始化



**静态常量**

const MAX_ID: usize = usize::MAX / 2;

1
2
3
4
5
6
7
8
9

- 关键字是`const`而不是`let`
- 定义常量必须指明类型(如 i32)不能省略
- 定义常量时变量的命名规则一般是全部大写
- 常量可以在任意作用域进行定义,其生命周期贯穿整个程序的生命周期。编译时编译器会尽可能将其内联到代码中,所以在不同地方对同一常量的引用并不能保证引用到相同的内存地址
- 常量的赋值只能是常量表达式/数学表达式,也就是说必须是在编译期就能计算出的值,如果需要在运行时才能得出结果的值比如函数,则不能赋值给常量表达式
- 对于变量出现重复的定义(绑定)会发生变量遮盖,后面定义的变量会遮住前面定义的变量,常量则不允许出现重复的定义

**静态变量**

static mut REQUEST_RECV: usize = 0;

1
2
3
4
5
6
7

- 必须使用`unsafe`语句块才能访问和修改`static`变量
- 定义静态变量的时候必须赋值为在编译期就可以计算出的值(常量表达式/数学表达式),不能是运行时才能计算出的值(如函数)

**原子类型**

想要全局计数器、状态控制等功能,又想要线程安全的实现

use std::sync::atomic::{AtomicUsize, Ordering};
static REQUEST_RECV: AtomicUsize = AtomicUsize::new(0);

1
2
3
4
5
6
7
8
9
10



#### 运行期初始化

解决问题:无法用函数进行静态初始化

**`lazy_static`**

用于懒初始化(直到使用时才开始初始化)静态变量的宏,允许我们在运行期初始化静态变量

use lazy_static::lazy_static;
lazy_static! {
static ref NAMES: Mutex = Mutex::new(String::from(“Sunface, Jack, Allen”));
}

1
2
3
4
5
6

`lazy_static`宏,匹配的是`static ref`,所以定义的静态变量都是不可变引用

**`Box::leak`**

将一个变量从内存中漏出来,变为`'static'`生命周期

#[derive(Debug)]
struct Config {
a: String,
b: String
}
static mut CONFIG: Option<&mut Config> = None;

fn main() {
let c = Box::new(Config {
a: “A”.to_string(),
b: “B”.to_string(),
});

unsafe {
    // 将`c`从内存中泄漏,变成`'static`生命周期(正常情况下,一个局部变量不可赋给全局变量)
    CONFIG = Some(Box::leak(c));
    println!("{:?}", CONFIG);
}

}

1
2
3
4
5
6
7
8
9
10
11
12
13



### 错误处理

#### 组合器

**`or()`和`and()`**

对两个表达式做逻辑组合,最终返回 `Option` / `Result`

- `or()`,表达式按照顺序求值,若任何一个表达式的结果是 `Some` 或 `Ok`,则该值会立刻返回
- `and()`,若两个表达式的结果都是 `Some` 或 `Ok`,则第二个表达式中的值被返回。若任何一个的结果是 `None` 或 `Err` ,则立刻返回。

let s1 = Some(“some1”);
let s2 = Some(“some2”);
let n: Option<&str> = None;
assert_eq!(s1.or(s2), s1);

1
2
3
4
5
6

注:`or/and()`的两个表达式要是同一类型,不能一边是`Option`一边是`Result`

**`or_else()和and_then()`**

跟 `or()` 和 `and()` 类似,但第二个表达式是一个闭包

let s1 = Some(“some1”);
let fn_some = || Some(“some2”);
let fn_none = || None;
assert_eq!(s1.or_else(fn_some), s1);

1
2
3
4

**`fliter`**

用于对 `Option` 进行过滤

let s1 = Some(3);
let n = None;
let fn_is_even = |x: &i8| x % 2 == 0;
assert_eq!(s1.filter(fn_is_even), n);

1
2
3
4
5
6
7
8

**`map()`和`map_err()`**

`map` 可以将 `Some` 或 `Ok` 中的值映射为另一个(转化容器内的值)

如果`a`的值是`Some(n)`,`a.map(f)`将`a`的值变为`Some(f(n))`

用 `map_err`将 `Err` 中的值进行改变(效果同上)

let s1 = Some(“abcde”);
let s2 = Some(5);
let fn_character_count = |s: &str| s.chars().count();
assert_eq!(s1.map(fn_character_count), s2);

let e1: Result<&str, &str> = Err(“404”);
let e2: Result<&str, isize> = Err(404);
let fn_character_count = |s: &str| -> isize { s.parse().unwrap() };
assert_eq!(e1.map_err(fn_character_count), e2);

1
2
3
4
5
6
7
8



**`map_or()`和`map_or_else()`**

`map_or` 在 `map` 的基础上提供了一个默认值

`map_or_else` 与 `map_or` 类似,但是它是通过一个闭包来提供默认值

const V_DEFAULT: u32 = 1; // 默认值
let s: Result<u32, ()> = Ok(10);
let fn_closure = |v: u32| v + 2;
assert_eq!(s.map_or(V_DEFAULT, fn_closure), 12);

let s = Some(10);
let fn_closure = |v: i8| v + 2;
let fn_default = || 1; // 默认值
assert_eq!(s.map_or_else(fn_default, fn_closure), 12);

1
2
3
4
5
6

**`ok_or()`和`ok_or_else`**

可以将 `Option` 类型转换为 `Result` 类型

`ok_or` 接收一个默认的 `Err` 参数,`ok_or_else` 接收一个闭包作为 `Err` 参数

const ERR_DEFAULT: &str = “error message”;
// let fn_err_message = || “error message”;

assert_eq!(s.ok_or(ERR_DEFAULT), o); // Some(T) -> Ok(T)
assert_eq!(n.ok_or(ERR_DEFAULT), e); // None -> Err(default)

1
2
3
4



#### 自定义错误类型

use std::fmt::{Debug, Display};

pub trait Error: Debug + Display {
fn source(&self) -> Option<&(Error + ‘static)> { … }
}

1
2
3
4
5
6

当自定义类型实现该特征后,该类型就可以作为 `Err` 来使用,同时可以归一化为`Box<dyn std::error:Error>`

**将其他错误类型转化为自定义错误类型**

只要实现`From`特征,即可使用`?`强制把返回的错误类型转换(同时返回)

// std::convert::From特征
pub trait From: Sized {
fn from(_: T) -> Self;
} // T为原本的错误类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40

**归一化不同错误类型**

面临问题:要在一个函数中返回不同的错误

解决方案:将不同的错误类型归一化为一种

- 使用特征对象 `Box<dyn Error>`:需要实现`Debug + Display` 特征(存在问题:一个没有`Error`特征的类型同样可以用作`Result<T, E>`中的`E`)
- 自定义错误类型:需要实现`Error`特征才能被转换出来
- 使用 `thiserror`(一种三方库函数)

### `UnSafe`

面临问题:

- 编译器过强且保守
- 特定功能如底层硬件操作本就不安全

`unsafe`功能

- 解引用裸指针
- 调用一个 `unsafe` 或外部的函数
- 访问或修改一个可变的静态变量
- 实现一个 `unsafe` 特征
- 访问 `union` 中的字段

#### 功能解析

##### 解引用裸指针

裸指针在功能上跟引用类似,同时它也需要显式地注明可变性。

`*const T` 和 `*mut T`分别代表了不可变和可变(`*` 是类型名称的一部分而非解引用)

裸指针的功能(类似于C的指针):

- 可以绕过 Rust 的借用规则,可以同时拥有一个数据的可变、不可变指针,甚至还能拥有多个可变的指针
- 并不能保证指向合法的内存
- 可以是 `null`
- 没有实现任何自动的回收 (drop)

// 基于引用创建裸指针
let mut num = 5;
let r1 = &num as *const i32;
let r2 = &mut num as *mut i32;

// 使用*解引用
unsafe {
println!(“{}”, *r1);
}

// 基于智能指针创建裸指针
let a: Box = Box::new(10);
let b: const i32 = &a; // 需要先解引用a
let c: *const i32 = Box::into_raw(a); // 使用 into_raw 来创建

1
2
3
4
5
6
7

注:

- 创建裸指针是安全的行为,使用不是
- 使用裸指针可以创建两个可变指针都指向同一个数据(需要自己处理数据竞争)

##### 调用`unsafe`函数或方法

// unsafe函数:外表唯一不同就是需要unsafe fn来定义,在调用时需要放在unsafe块
// 在unsafe函数中使用unsafe来注明块是多余的行为
unsafe fn dangerous() {}
fn main() {
unsafe {
dangerous();
}
}

// 在函数中使用了unsafe声明块不代表函数要声明为unsafe fn:同样可以使用用安全的抽象包裹unsafr代码

1
2
3
4
5
6
7
8
9

##### `FFI`

用来与其它语言进行交互

面临问题:使用一个其他语言编写的库

- 对该库进行重写或者移植
- 使用 `FFI`

// 调用C标准库中的abs函数
extern “C” { // C定义了外部函数所使用的应用二进制接口ABI
fn abs(input: i32) -> i32;
}
fn main() {
unsafe { // 必须使用unsafe
println!(“Absolute value of -3 according to C: {}”, abs(-3));
}
}

1
2
3
4

##### 访问`union`中的字段

`union`主要用于和`C`代码交互,访问其字段是不安全的

#[repr(C)]
union MyUnion {
f1: u32,
f2: f32,
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28



### `Macro`宏编程

宏的参数可以使用 `()`、`[]` 以及 `{}`:虽然三种使用形式皆可,但是 Rust 内置的宏都有自己约定俗成的使用方式,例如 `vec![...]`、`assert_eq!(...)` 等

宏分为两类:

- 声明式宏
- 三种过程宏
- `#[derive]`,在之前多次见到的派生宏,可以为目标结构体或枚举派生指定的代码,例如 `Debug` 特征
- 类属性宏(Attribute-like macro),用于为目标添加自定义的属性
- 类函数宏(Function-like macro),看上去就像是函数调用

#### 宏与函数的区别

元编程:通过一种代码来生成另一种代码,可以帮我们减少所需编写的代码,也可以一定程度上减少维护的成本

可变参数:相比于`Rust`中函数参数个数的固定,宏的参数个数可变

宏展开:宏展开过程是发生在编译器对代码进行解释之前,即编译期前;函数直到运行时才调用

#### 声明式宏`macro_rules`

声明式宏用来编写可以生成代码的代码,即可以编写自己的宏

类似于`match`进行模式匹配,类似于函数可以传入参数

// 基本形式
macro_rules! macro_name {
(pattern) => { expansion };
// 可以有多个匹配模式
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

**模式匹配**的模式可以包括:字面量、元变量(以 `$` 开头的捕获,如 `$x:expr`)、重复(使用 `$(...)*` 或 `$(...)+` 等表示重复)

**元变量类型**(类似于函数定义中形参的类型声明)

- `expr`:表达式
- `ident`:标识符(变量名、函数名等)
- `ty`:类型
- `path`:路径(如 `std::collections::HashMap`)
- `pat`:模式
- `stmt`:语句
- `block`:代码块
- `item`:项(函数、结构体、模块等)
- `meta`:元项(`#[...]` 和 `#![...]` 属性内部的内容)
- `tt`:标记树(单个标记或括号内的标记)

**重复操作符**

- `*`:0 次或多次
- `+`:1 次或多次
- `?`:0 次或 1 次

#[macro_export] // 将宏进行了导出,其它的包就可以将该宏引入到当前作用域中
macro_rules! create_function { // 宏的名称是c_f,在调用时才需要加上!
($func_name:ident) => {
fn $func_name() {
println!(“You called {}”, stringify!($func_name));
}
};
}
// 使用
create_function!(foo); // 传入一个合法标识符,创建了一个函数
foo(); // 输出: You called foo

// 重复模式
#[macro_export]
macro_rules! vec {
( $( $x:expr ),* ) => {
{
let mut temp_vec = Vec::new();
$(
temp_vec.push($x);
)* // 此处相当于一个循环
temp_vec
}
};
}
// 使用
let v = vec!(“a”, “b”, “c”);

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17



#### 过程宏

1. **派生宏(Derive Macros)** *"自动为结构体/枚举生成 trait 实现的代码扩展器。"* → **用途**:如 `#[derive(Serialize)]` 为类型实现序列化逻辑。
2. **属性宏(Attribute Macros)** *"编译时代码加工器,能修改或增强被标记的项(如函数/结构体)。"* → **用途**:如 `#[tokio::main]` 将普通函数异步化。
3. **函数式宏(Function-like Macros)** *"将其他语法编译时转换为 Rust 代码的翻译器。"* → **用途**:如 `sql!(SELECT * FROM table)` 生成类型安全的查询构建器。

##### 自定义`derive`过程宏

注:目前只能在单独的包中定义宏,包名以`derive`为后缀

假设有一个特征 `HelloMacro`,现在有两种方式让用户使用它:

- 为每个类型手动实现该特征,就像之前特征章节所做的
- 使用过程宏来统一实现该特征,这样用户只需要对类型进行标记即可:`#[derive(HelloMacro)]`

// hello_macro项目目录
hello_macro
├── Cargo.toml
├── src
│ ├── main.rs
│ └── lib.rs
└── hello_macro_derive // 此包中实现宏
├── Cargo.toml
├── src
└── lib.rs

1
2
3
4
5

在项目的`src/main.rs`中引用宏包中的内容:

- 将 `hello_macro_derive` 发布到 `crates.io` 或 `GitHub` 中(类似于正常的依赖)
- 使用相对路径引入的本地化方式

// 修改 hello_macro/Cargo.toml 文件添加以下内容
[dependencies]
hello_macro_derive = { path = “../hello_macro/hello_macro_derive” }

也可以使用下面的相对路径

hello_macro_derive = { path = “./hello_macro_derive” }

1
2

定义过程宏的过程

// 1、在 hello_macro_derive/Cargo.toml 文件中添加
[lib]
proc-macro = true

[dependencies]
syn = “1.0”
quote = “1.0” // 这两个依赖包是定义中必须的

// 2、在 hello_macro_derive/src/lib.rs 中添加
extern crate proc_macro; // 过程宏核心库,提供 TokenStream 类型(表示宏的输入/输出)
use proc_macro::TokenStream;
use quote::quote;
use syn;
use syn::DeriveInput;

#[proc_macro_derive(HelloMacro)]
pub fn hello_macro_derive(input: TokenStream) -> TokenStream {
// 基于 input 构建 AST 语法树
let ast:DeriveInput = syn::parse(input).unwrap();

// 构建特征实现代码
impl_hello_macro(&ast)

}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33





# RCore学习记录

顺序:`bulid.rs`文件会生成一个`link_app.S`(里面包含有各个程序标识起始地址的变量、名称和完整的ELF文件嵌入),这个文件由`linker.S`塞入内核镜像(编译链接后的程序文件),最后由`load`来执行

任务切换的实质:切换任务上下文(即更改对应的寄存器为想执行的任务上下文中保存的数值)



### 批处理系统

**系统调用的基础函数:**

`syscall` (用户文件中,标准库函数的基础) 和`sbicall` (系统文件中)

```rust
3fn syscall(id: usize, args: [usize; 3]) -> isize {
4 let mut ret: isize;
5 unsafe {
6 core::arch::asm!(
7 "ecall",
8 inlateout("x10") args[0] => ret,
9 in("x11") args[1],
10 in("x12") args[2],
11 in("x17") id
12 );
13 }
14 ret
15}

批处理系统的应用管理器:

(从系统文件中一个记录了应用数量、各应用起始位置、最后一个应用结束位置的link_app.S中获取)

1
2
3
4
5
struct AppManager {
num_app: usize,
current_app: usize,
app_start: [usize; MAX_APP_NUM + 1],
}

方法: print_app_info/get_current_app/move_to_next_app

load_app将参数 app_id 对应的应用程序的二进制镜像加载到物理内存以 0x80400000 起始的位置

batch子模块暴露的接口

  • init :初始化 APP_MANAGER
  • run_next_app :加载并运行下一个应用程序

用户栈与内核栈

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
 1// os/src/batch.rs
2
3#[repr(align(4096))]
4struct KernelStack {
5 data: [u8; KERNEL_STACK_SIZE],
6}
7
8#[repr(align(4096))]
9struct UserStack {
10 data: [u8; USER_STACK_SIZE],
11}
12
13static KERNEL_STACK: KernelStack = KernelStack {
14 data: [0; KERNEL_STACK_SIZE],
15};
16static USER_STACK: UserStack = UserStack {
17 data: [0; USER_STACK_SIZE],
18};

实现了 get_sp 方法来获取栈顶地址

特权级切换

CSR 名 该 CSR 与 Trap 相关的功能
sstatus SPP 等字段给出 Trap 发生之前 CPU 处在哪个特权级(S/U)等信息
sepc 当 Trap 是一个异常的时候,记录 Trap 发生之前执行的最后一条指令的地址
scause 描述 Trap 的原因
stval 给出 Trap 附加信息
stvec 控制 Trap 处理代码的入口地址

硬件自动完成:

  • sstatusSPP 字段会被修改为 CPU 当前的特权级(U/S)。
  • sepc 会被修改为 Trap 处理完成后默认会执行的下一条指令的地址。
  • scause/stval 分别会被修改成这次 Trap 的原因以及相关的附加信息。
  • CPU 会跳转到 stvec 所设置的 Trap 处理入口地址,并将当前特权级设置为 S ,然后从Trap 处理入口地址处开始执行。

处理完成后通过S特权级sret指令:

  • CPU 会将当前的特权级按照 sstatusSPP 字段设置为 U 或者 S ;
  • CPU 会跳转到 sepc 寄存器指向的那条指令,然后继续执行。

Trap上下文

1
2
3
4
5
6
7
8
9
1// os/src/trap/context.rs
2
3#[repr(C)]
4pub struct TrapContext {
5 pub x: [usize; 32],
6 pub sstatus: Sstatus,
7 pub sepc: usize,
8} // 包含所有的通用寄存器 `x0~x31` ,还有 `sstatus` 和 `sepc`
// 从内存栈底分配34个空间保存

实现 Trap 上下文保存和恢复的汇编代码os/src/trap/trap.S,用(汇编中的)外部符号 __alltraps__restore 标记为函数

Trap 处理的总体流程如下:首先通过 __alltraps 将 Trap 上下文保存在内核栈上,然后跳转到使用 Rust 编写的 trap_handler 函数 完成 Trap 分发及处理。当 trap_handler 返回之后,使用 __restore 从保存在内核栈上的 Trap 上下文恢复寄存器。最后通过一条 sret 指令回到应用程序执行

使用sp表示当前的栈,sscratch代表另一个栈

方法:

1
2
3
4
5
impl TrapContext {
pub fn set_sp(&mut self, sp: usize)
pub fn app_init_context(entry: usize, sp: usize) -> Self
// 修改其中的 sepc 寄存器为应用程序入口点 entry, sp 寄存器为我们设定的 一个栈指针,并将 sstatus 寄存器的 SPP 字段设置为 User
}

分时多任务

user/build.py为每个应用定制各自的起始地址,.text 段的地址为 0x80400000 + app_id * 0x20000

batch 被拆分为 loadertask , 前者负责启动时加载应用程序,后者负责切换和调度。

loader 模块的 load_apps 函数负责将所有用户程序在内核初始化的时一并加载进内存

任务切换

当一个应用在内核态时,其 Trap 控制流可以调用一个特殊的 __switch 函数,函数调用时运行另一个任务,返回后运行原来的任务

__switch 中保存 CPU 的某些寄存器,它们就是任务上下文

函数拥有两个参数:

1
2
3
4
 __switch(
current_task_cx_ptr: *mut TaskContext,
next_task_cx_ptr: *const TaskContext
)

内核先把 current_task_cx_ptr 中包含的寄存器值逐个保存,再把 next_task_cx_ptr 中包含的寄存器值逐个恢复

1
2
3
4
5
6
7
1// os/src/task/context.rs
2#[repr(C)]
3pub struct TaskContext {
4 ra: usize, // 任务恢复后执行地址
5 sp: usize, // 该任务内核栈地址(当前是内核态)
6 s: [usize; 12], // 被调用者保存寄存器
7} // TaskContext 里包含的寄存器

管理多道程序

  • 任务运行状态:未初始化、准备执行、正在执行、已退出
  • 任务控制块:维护任务状态和任务上下文
  • 任务相关系统调用:程序主动暂停 sys_yield 和主动退出 sys_exit
1
2
3
4
5
6
7
8
9
10
11
pub enum TaskStatus { // 任务状态
UnInit, // 未初始化
Ready, // 准备运行
Running, // 正在运行
Exited, // 已退出
}

pub struct TaskControlBlock { // 任务控制块
pub task_status: TaskStatus, // 任务状态
pub task_cx: TaskContext, // 任务上下文
}

全局的任务管理器

1
2
3
4
5
6
7
8
9
pub struct TaskManager {
num_app: usize,
inner: UPSafeCell<TaskManagerInner>,
}

struct TaskManagerInner {
tasks: [TaskControlBlock; MAX_APP_NUM],
current_task: usize,
}

TaskManager方法:mark_current_suspended(暂停当前程序)/ mark_current_exited / run_next_task / find_next_task(找到下一个Ready状态的应用)

时钟中断

处理器维护时钟计数器 mtime,还有另外一个 CSR mtimecmp 。 一旦计数器 mtime 的值超过了 mtimecmp,就会触发一次时钟中断

1
2
3
4
5
6
7
8
9
10
pub fn get_time() -> usize { // 获得mtime值
time::read()
}

pub fn set_timer(timer: usize) {
sbi_call(SBI_SET_TIMER, timer, 0, 0);
}
pub fn set_next_trigger() { // 设置时钟中断
set_timer(get_time() + CLOCK_FREQ / TICKS_PER_SEC);
}

页表机制

非叶节点(页目录表,非末级页表)的表项标志位含义和叶节点(页表,末级页表)相比有一些不同:

  • V 为 0 的时候,代表当前指针是一个空指针,无法走向下一级节点,即该页表项对应的虚拟地址范围是无效的;
  • 只有当 V 为1 且 R/W/X 均为 0 时,表示是一个合法的页目录表项,其包含的指针会指向下一级的页表;
  • 注意: 当 V 为1 且 R/W/X 不全为 0 时,表示是一个合法的页表项,其包含了虚地址对应的物理页号。

物理地址与物理页号转换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
 3impl PhysAddr {
4 pub fn page_offset(&self) -> usize { self.0 & (PAGE_SIZE - 1) } // 从自己的物理地址0x12345000得到偏移为0x000(末三位)
5}
6
7impl From<PhysAddr> for PhysPageNum {
8 fn from(v: PhysAddr) -> Self {
9 assert_eq!(v.page_offset(), 0);
10 v.floor()
11 } // 只有偏移为0(即对齐)才能从0x12345000得到物理页号0x12345
12}
13
14impl From<PhysPageNum> for PhysAddr {
15 fn from(v: PhysPageNum) -> Self { Self(v.0 << PAGE_SIZE_BITS) }
16} // 从物理页号0x12345还原回物理地址0x12345000

页表项的数据结构抽象与类型定义

页表项共8个字节:

  • V(0) 仅当 V(Valid) 位为 1 时,页表项才是合法的;
  • R(1)/W(2)/X(3) R/W/X 分别控制索引到这个页表项的对应虚拟页面是否允许读/写/取指;
  • U(4) U 控制索引到这个页表项的对应虚拟页面是否在 CPU 处于 U 特权级的情况下是否被允许访问;
  • G(5) G 我们不理会;
  • A(6) A(Accessed) 记录自从页表项上的这一位被清零之后,页表项的对应虚拟页面是否被访问过;
  • D(7) D(Dirty) 则记录自从页表项上的这一位被清零之后,页表项的对应虚拟页表是否被修改过。
  • RSW(8-9)
  • PPN[0] (10-18)
  • PPN[1] (19-27)
  • PPN[2] (28-53)
  • Reserved(54-63)

前八位的实现:

1
2
3
4
5
6
7
8
9
10
11
12
bitflags! { // 用来表示比特位的宏
pub struct PTEFlags: u8 {
const V = 1 << 0;
const R = 1 << 1;
const W = 1 << 2;
const X = 1 << 3;
const U = 1 << 4;
const G = 1 << 5;
const A = 1 << 6;
const D = 1 << 7;
}
}

结构体的实现与方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
 3#[derive(Copy, Clone)]
4#[repr(C)]
5pub struct PageTableEntry { // 一个页表项
6 pub bits: usize,
7}
8
9impl PageTableEntry {
10 pub fn new(ppn: PhysPageNum, flags: PTEFlags) -> Self {
11 PageTableEntry {
12 bits: ppn.0 << 10 | flags.bits as usize,
13 }
14 }
15 pub fn empty() -> Self {
16 PageTableEntry {
17 bits: 0,
18 }
19 }
20 pub fn ppn(&self) -> PhysPageNum {
22 }
23 pub fn flags(&self) -> PTEFlags {
25 }
pub fn is_valid(&self) -> bool {
}
26}

页帧管理器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
pub struct StackFrameAllocator {
current: usize,
end: usize,
recycled: Vec<usize>,
}
// 方法
fn alloc(&mut self) -> Option<PhysPageNum> // 分配物理页
fn dealloc(&mut self, ppn: PhysPageNum) // 回收物理页

// 用以调用的接口
pub fn frame_alloc() -> Option<FrameTracker>
fn frame_dealloc(ppn: PhysPageNum)

// 代表一个物理页帧
pub struct FrameTracker {
pub ppn: PhysPageNum,
}
impl FrameTracker {
pub fn new(ppn: PhysPageNum) -> Self {
// 将这个物理页帧上的所有字节清零
}
fn drop(&mut self) {
frame_dealloc(self.ppn);
}
}

多级页表

正常情况可以依靠MMU直接翻译,手动翻译是由于操作系统是不能直接靠MMU来访问用户地址程序的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 一个应用所有的页表
pub struct PageTable {
root_ppn: PhysPageNum, // 某应用的根节点物理页号
frames: Vec<FrameTracker>, // 页表中所有节点(包括根)的物理页帧,不包括叶节点指向的
}
impl PageTable {
pub fn new() -> Self { // 新建时只需要分配一个根节点
}
}

// 页表:维护虚拟物理地址的映射
// 方法
impl PageTable {
pub fn map(&mut self, vpn: VirtPageNum, ppn: PhysPageNum, flags: PTEFlags);
pub fn unmap(&mut self, vpn: VirtPageNum);
}

访问特定物理页帧

1
2
3
4
5
6
7
8
impl PhysPageNum {
pub fn get_pte_array(&self) -> &'static mut [PageTableEntry] {
} // 返回的是一个页表项定长数组的可变引用
pub fn get_bytes_array(&self) -> &'static mut [u8] {
} // 返回的是一个字节数组的可变引用
pub fn get_mut<T>(&self) -> &'static mut T {
} // 获取一个恰好放在一个物理页帧开头的类型为 T 的数据的可变引用
}

建立/拆除虚实地址映射

1
2
3
4
5
6
7
8
impl VirtPageNum {
pub fn indexes(&self) -> [usize; 3] { // 返回虚拟页号的三级索引
}
}

impl PageTable {
fn find_pte_create(&mut self, vpn: VirtPageNum) -> Option<&mut PageTableEntry> // 给定虚拟页号,找到/创建页表项及中间层页表
} // 注意,这个只返回最后一级的页表项,所以还要通过map/unmap来判断或创建最后的物理地址

只查询,不建立

1
2
3
4
5
6
7
8
impl PageTable {
pub fn from_token(satp: usize) -> Self {} // 从satp寄存器中读此时的根页表地址
fn find_pte(&self, vpn: VirtPageNum) -> Option<&PageTableEntry> {} // 查找对应的页表是否存在(没有也不新建)
pub fn translate(&self, vpn: VirtPageNum) -> Option<PageTableEntry> {} // 如果存放就返回
}

// 用户虚拟地址 => 可写物理内存
pub fn translated_byte_buffer(token: usize, ptr: *const u8, len: usize) -> Vec<&'static mut [u8]> {}

地址空间抽象

逻辑段:虚拟地址连续,虚拟地址映射到物理地址的方式相同(物理页帧具有的属性相同而非物理地址连续)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 逻辑段结构体
pub struct MapArea {
vpn_range: VPNRange, // 一段虚拟页号的连续区间,是一个迭代器
data_frames: BTreeMap<VirtPageNum, FrameTracker>, // 绑定生命周期,到期自动回收
map_type: MapType,
map_perm: MapPermission,
}
pub enum MapType { // 逻辑段内虚拟页面映射的方式
Identical, // 恒等映射
Framed, // 分配物理页帧(随机)
}
bitflags! { // 该逻辑段的访问方式
pub struct MapPermission: u8 {
const R = 1 << 1;
const W = 1 << 2;
const X = 1 << 3;
const U = 1 << 4;
}
}

// 方法
impl MapArea {
pub fn new( start_va: VirtAddr, end_va: VirtAddr, map_type: MapType, map_perm: MapPermission ) -> Self {};
pub fn map_one(&mut self, page_table: &mut PageTable, vpn: VirtPageNum) {}; // 在确定了虚拟页号之后,先在逻辑的页表管理MapArea里面加入虚拟页号和StackFrameAllocator分配的物理页帧,然后在pagetable里完善各级页表里面的指向路径
pub fn unmap_one(&mut self, page_table: &mut PageTable, vpn: VirtPageNum) {};
pub fn map(&mut self, page_table: &mut PageTable) {} // 把当前结构体中的所有虚拟地址挨个map_one物理地址
pub fn unmap(&mut self, page_table: &mut PageTable) {}
pub fn copy_data(&mut self, page_table: &PageTable, data: &[u8]) {} // 复制data到逻辑段开头
}

地址空间:一个进程能够访问的所有内存地址的集合,通常被组织为多个逻辑段

pagetable 实际上查找虚拟/物理地址映射的方法(存储各级节点,可以手动搜索),供CPU/MMU使用

areas 逻辑上管理的方法(管理虚拟内存、映射数组来直接寻找)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 某个地址空间的结构体
pub struct MemorySet { // PageTable 下挂着所有多级页表的节点所在的物理页帧,而每个 MapArea 下则挂着对应逻辑段中的数据所在的物理页帧,这两部分合在一起构成了一个地址空间所需的所有物理页帧(绑定,自动回收)
page_table: PageTable,
areas: Vec<MapArea>,
}
// 方法
impl MemorySet {
pub fn new_bare() -> Self
fn push(&mut self, mut map_area: MapArea, data: Option<&[u8]>) // 插入新的逻辑段,还可以在物理页帧上写入data
pub fn insert_framed_area( &mut self, start_va: VirtAddr, end_va: VirtAddr, permission: MapPermission ) // 新建并插入一段Framed方式映射的逻辑段
pub fn new_kernel() -> Self; // 生成内核的地址空间
pub fn from_elf(elf_data: &[u8]) -> (Self, usize, usize); // 分析应用的ELF文件(并且根据这个文件建立新的地址空间,返回栈顶和执行入口)
pub fn activate(&self) {} // 第一次使用会开启分页,并且进入当前地址空间的页表
fn map_trampoline(&mut self) {} // 直接插入一个空间地址中最高位置到跳板的键值对(注:所有地址空间中的跳板都是指向trap.S文件)
}

内核的地址空间排布

跳板、各应用的内核栈(栈间有空洞区域防溢出)

四个逻辑段.text/.rodata/.data/.bss(恒等映射)、恒等映射(除之前内核已使用)所有物理页帧的页表 (即是内核页表)(注:后面两项都是恒等映射建立的三级页表MapArea

应用程序的地址空间排布

跳板、trap上下文(用户不可访问)

用户栈、保护页guard page、各逻辑段

1
2
3
// 批处理系统升级:加载应用进入内存
pub fn get_num_app() -> usize {} // 获取程序数量
pub fn get_app_data(app_id: usize) -> &'static [u8] {} // 获得某个应用的全部数据

基于空间地址的分时多任务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
KERNEL_SPACE: Arc<UPSafeCell<MemorySet>> // 内核地址全局实例

pub struct TrapContext { // trap上下文
pub x: [usize; 32],
pub sstatus: Sstatus,
pub sepc: usize,
// 以下为新增字段,在初始化时写入,便于保存完成寄存器后切换
pub kernel_satp: usize,
pub kernel_sp: usize, // 本来sp指向用户栈,sscratch指向内核栈,交换后在内核栈中保存上下文;但现在由于要更新页表寄存器不够,sscratch指向用户空间中trap上下文处保存,然后根据保存的内存栈顶直接更改sp
pub trap_handler: usize,
}

3impl TrapContext {
4 pub fn set_sp(&mut self, sp: usize) { }
5 pub fn app_init_context( entry: usize, sp: usize, kernel_satp: usize, kernel_sp: usize, trap_handler: usize ) -> Self {} // 从任务上下文中初始化trap上下文
}

注:跳板就是执行trap时保存上下文的汇编代码_alltraps_restore,由于其在内核与应用地址空间的位置相同,所以无论哪种页表都可以在同一位置访问

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 任务上下文
pub struct TaskControlBlock {
pub task_status: TaskStatus,
pub task_cx: TaskContext,
pub memory_set: MemorySet, // 应用地址空间
pub trap_cx_ppn: PhysPageNum, // 应用trap上下文的实际物理页帧
pub base_size: usize, // 应用地址空间大小
}

impl TaskControlBlock {
pub fn new(elf_data: &[u8], app_id: usize) -> Self {}
pub fn get_trap_cx(&self) -> &'static mut TrapContext {}
}

// 全局应用管理器
struct TaskManagerInner {
tasks: Vec<TaskControlBlock>,
current_task: usize,
}

lazy_static! {
pub static ref TASK_MANAGER: TaskManager = {}; // 初始化时根据loader提供的app数量和ELF文件来初始化所有任务上下文
}

进程

新增系统调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/// 功能:当前进程 fork 出来一个子进程。
/// 返回值:对于子进程返回 0,对于当前进程则返回子进程的 PID 。
/// syscall ID:220
pub fn sys_fork() -> isize;

/// 功能:当前进程等待一个子进程变为僵尸进程,回收其全部资源并收集其返回值。
/// 参数:pid 表示要等待的子进程的进程 ID,如果为 -1 的话表示等待任意一个子进程;
/// exit_code 表示保存子进程返回值的地址,如果这个地址为 0 的话表示不必保存。
/// 返回值:如果要等待的子进程不存在则返回 -1;否则如果要等待的子进程均未结束则返回 -2;
/// 否则返回结束的子进程的进程 ID。
/// syscall ID:260
pub fn sys_waitpid(pid: isize, exit_code: *mut i32) -> isize;

/// 功能:将当前进程的地址空间清空并加载一个特定的可执行文件,返回用户态后开始它的执行。
/// 参数:path 给出了要加载的可执行文件的名字;
/// 返回值:如果出错的话(如找不到名字相符的可执行文件)则返回 -1,否则不应该返回。
/// syscall ID:221
pub fn sys_exec(path: &str) -> isize;

/// 功能:从文件中读取一段内容到缓冲区。
/// 参数:fd 是待读取文件的文件描述符,切片 buffer 则给出缓冲区。
/// 返回值:如果出现了错误则返回 -1,否则返回实际读到的字节数。
/// syscall ID:63
pub fn sys_read(fd: usize, buffer: &mut [u8]) -> isize;

在用户级中,在最最开始(即在main函数中)会初始化一个initproc用户初始进程,

其只会初始化一个shell进程,之后就持续循环+时间片轮转来回收进程(注:所有父进程被回收的进程都会变成其子进程)

1
2
3
4
// 基于应用名的应用加载器
static ref APP_NAMES: Vec<&'static str>

pub fn get_app_data_by_name(name: &str) -> Option<&'static [u8]> // 根据名字查找ELF数据

进程标识符

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 进程标识符PID
pub struct PidHandle(pub usize);
impl Drop for PidHandle {
fn drop(&mut self) { // 自动回收
PID_ALLOCATOR.exclusive_access().dealloc(self.0);
}
}

// 进程分配器,类似于FrameAllocator
struct PidAllocator {
current: usize, // 当前已分配到多少
recycled: Vec<usize>,
}
impl PidAllocator {
pub fn new() -> Self { // current为0,数组为空
}
pub fn alloc(&mut self) -> PidHandle { // 分配一个新的PIDHandle(其他啥也没有),优先从回收中选取
}
pub fn dealloc(&mut self, pid: usize) { // 在PID已分配、未回收的情况下丢进回收
}
}
static ref PID_ALLOCATOR : UPSafeCell<PidAllocator> // 实例化

内核栈

原本每个程序一个,固定大小按程序顺序排列,中间穿插守护页防止溢出

现在将应用编号替换为进程标识符PTD,在内核栈中保存

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
pub struct KernelStack {
pid: usize,
}

impl KernelStack {
pub fn new(pid_handle: &PidHandle) -> Self { // 为一个已分配的进程标识符生成内核栈
}
pub fn push_on_top<T>(&self, value: T) -> *mut T where
T: Sized, { // 将一个类型为 T 的变量压入内核栈顶并返回其裸指针
}
pub fn get_top(&self) -> usize { // 获取当前内核栈顶在内核地址空间中的地址
}
}
impl Drop for KernelStack {
fn drop(&mut self) {
}
}

pub fn kernel_stack_position(app_id: usize) -> (usize, usize) {
// 得到pid进程内核栈的起始/终止虚拟地址
}

进程控制块

之前的TaskControlBlock分离为

Processor处理器管理结构:管理CPU正在运行的任务

TaskManager任务管理器:管理未在运行的所有任务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
pub struct TaskControlBlock { // 不可变
// 不可变
pub pid: PidHandle,
pub kernel_stack: KernelStack,
// 可变
inner: UPSafeCell<TaskControlBlockInner>,
}
impl TaskControlBlock {
pub fn inner_exclusive_access(&self) -> RefMut<'_, TaskControlBlockInner> { // 内层 TaskControlBlockInner 的可变引用
self.inner.exclusive_access()
}
pub fn getpid(&self) -> usize {
self.pid.0
}
pub fn new(elf_data: &[u8]) -> Self {...} // 仅用于内核中手动创建唯一一个初始进程 initproc
pub fn exec(&self, elf_data: &[u8]) {...}
pub fn fork(self: &Arc<TaskControlBlock>) -> Arc<TaskControlBlock> {...}
}

pub struct TaskControlBlockInner {
pub trap_cx_ppn: PhysPageNum, // 应用地址空间中的 Trap 上下文被放在的物理页帧的物理页号
pub base_size: usize, // 应用数据仅有可能出现在应用地址空间低于 base_size 字节的区域中
pub task_cx: TaskContext, // 暂停的任务的任务上下文
pub task_status: TaskStatus, // 当前进程的执行状态
pub memory_set: MemorySet, // 应用地址空间
pub parent: Option<Weak<TaskControlBlock>>, // Arc的非拥有引用,可访问但不拥有
pub children: Vec<Arc<TaskControlBlock>>, // 多个Arc拥有一个不可变值,计数为0时才释放
pub exit_code: i32,
}
impl TaskControlBlockInner {
pub fn get_trap_cx(&self) -> &'static mut TrapContext {
self.trap_cx_ppn.get_mut()
}
pub fn get_user_token(&self) -> usize {
self.memory_set.token()
}
fn get_status(&self) -> TaskStatus {
self.task_status
}
pub fn is_zombie(&self) -> bool {
self.get_status() == TaskStatus::Zombie
}
}

任务管理器与处理器管理器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// 任务管理器
pub struct TaskManager {
ready_queue: VecDeque<Arc<TaskControlBlock>>, // 双端队列
}
impl TaskManager {
pub fn new() -> Self { // 新建一个空任务表
}
pub fn add(&mut self, task: Arc<TaskControlBlock>) { // 在末尾新增一个任务
}
pub fn fetch(&mut self) -> Option<Arc<TaskControlBlock>> { // 从开头拿取一个任务
}
}

// 处理器管理结构
pub struct Processor {
current: Option<Arc<TaskControlBlock>>,
idle_task_cx: TaskContext, // 当前处理器上的 idle 控制流的任务上下文(调度器上下文)
}
impl Processor {
pub fn new() -> Self { // 初始化为None
}
pub fn take_current(&mut self) -> Option<Arc<TaskControlBlock>> {
} // 取出当前正在执行的任务
pub fn current(&self) -> Option<Arc<TaskControlBlock>> {
} // 返回当前执行的任务的一份拷贝,可以用于获得token/trap_cx等等
fn get_idle_task_cx_ptr(&mut self) -> *mut TaskContext {
} // 获取当前处理器上的 idle 控制流的任务上下文指针
}
pub static ref PROCESSOR: UPSafeCell<Processor> // 单核只创建一个

// 进程转换
pub fn run_tasks() { // 负责在taskManager中获取要执行的任务并执行
} // 持续循环,只要进程让出CPU回到调度器中
pub fn schedule(switched_task_cx_ptr: *mut TaskContext) {
} // 在进程交出控制权后,用于回到run_tasks()执行循环中的函数

注:

  • Processer的任务上下文分离并且单独存储,是为了把调度和存储分离开,并无其他意义
  • Processer由内核中的结构体和上下文组成,相当于一个没有进程控制块的进程来用于调度

进程机制实现

  • 创建初始进程:创建第一个用户态进程 initproc
  • 进程调度机制:当进程主动调用 sys_yield 交出 CPU 使用权或者内核把本轮分配的时间片用尽的进程换出且换入下一个进程;
  • 进程生成机制:进程相关的两个重要系统调用 sys_fork/sys_exec 的实现;
  • 进程资源回收机制:当进程调用 sys_exit 正常退出或者出错被内核终止之后如何保存其退出码,其父进程通过 sys_waitpid 系统调用收集该进程的信息并回收其资源。
  • 字符输入机制:为了支持shell程序-user_shell获得字符输入,介绍 sys_read 系统调用的实现;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
// 创建初始进程
pub static ref INITPROC: Arc<TaskControlBlock> // 懒加载全局的initproc进程控制块
pub fn add_initproc() { // 在taskManager中加入initproc
}

// 进程调度机制
pub fn suspend_current_and_run_next() {
} // Process::取出当前执行的任务,修改状态为Ready后放入taskmanager末尾;然后schedule触发调度并切换任务

// 进程生成机制
impl MapArea {
pub fn from_another(another: &MapArea) -> Self { // 创建一模一样的结构体
}
}
impl MemorySet {
pub fn from_existed_user(user_space: &MemorySet) -> MemorySet {
} // 创建一模一样的结构体,并且物理地址按页复制数据
}
impl TaskControlBlock {
pub fn fork(self: &Arc<TaskControlBlock>) -> Arc<TaskControlBlock> {
} // 与new类似,不过不是从ELF中获取信息而是从memory_set复制后获取
}
pub fn sys_fork() -> isize {
} // 完全复制任务控制块,但是物理页帧不能直接复制,而且新的线程trap_cx中的a[0](返回值寄存器)要改成0

impl TaskControlBlock {
pub fn exec(&self, elf_data: &[u8]) {
} // 从一个ELF文件中获得Memory全部当前memory_set,然后更改控制块中信息
}
pub fn translated_str(token: usize, ptr: *const u8) -> String {
} // 传递给调用的只有要执行的应用的名称字符串的起始地址,要以此在内存中查询得到整个字符串
pub fn sys_exec(path: *const u8) -> isize {
}

// 进程退出机制
pub fn sys_exit(exit_code: i32) -> ! {
} // 调用exit_current_and_run_next()
pub fn exit_current_and_run_next(exit_code: i32) {
}

pub fn sys_waitpid(pid: isize, exit_code_ptr: *mut i32) -> isize {
} // 等待某个子进程退出,返回它的 pid,并把它的退出码写到指定地址
pub fn wait(exit_code: &mut i32) -> isize {
} // 调用sys_waitpid,成功则返回退出码;失败(返回-2)则yield继续等待

在执行exit_current_and_run_next(exit_code: i32)

  • 修改当前进程控制块的状态为TaskStatus::Zombie 即僵尸进程
  • 将退出码传入控制块等待父进程收集
  • 将所有子进程挂载在initproc下面
  • 对当前进程早期回收(回收Memory_set中的areas即数据页,不回收pagetable即页表页)

user_shell读入机制

1
2
pub fn sys_read(fd: usize, buf: *const u8, len: usize) -> isize {
} // 仅支持从标准输入 FD_STDIN 即文件描述符 0 读入,且单次读入的长度限制为 1

文件系统

文件与文件描述符

1
2
3
4
5
6
7
// 文件接口:只要实现了这个接口,就是文件
pub trait File : Send + Sync {
fn readable(&self) -> bool;
fn writable(&self) -> bool;
fn read(&self, buf: UserBuffer) -> usize;
fn write(&self, buf: UserBuffer) -> usize;
}

用户缓冲区:用户进程在系统调用的时候传给内核的地址空间(以内核为中转,用户缓冲区 <=> 文件)

1
2
3
4
5
6
7
8
9
10
11
pub fn translated_byte_buffer(token: usize, ptr: *const u8, len: usize) -> Vec<&'static mut [u8]>;

pub struct UserBuffer { // 将t_b_b中的切片进一步包装,即几页数据的数组
pub buffers: Vec<&'static mut [u8]>,
}
impl UserBuffer {
pub fn new(buffers: Vec<&'static mut [u8]>) -> Self { // 传入数据来新建缓冲区
}
pub fn len(&self) -> usize { // 获得总字节数
}
}

标准输入/输出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
pub struct Stdin;
impl File for Stdin {
fn readable(&self) -> bool { true }
fn writable(&self) -> bool { false }
fn read(&self, mut user_buf: UserBuffer) -> usize {
} // 缓冲区只能为一个字节大小,循环读取标准输入中的char,读取到则跳出循环写入缓冲区,未读取到则挂起继续循环,返回1
fn write(&self, _user_buf: UserBuffer) -> usize {
} // 直接panic
}

pub struct Stdout;
impl File for Stdout {
fn readable(&self) -> bool { false }
fn writable(&self) -> bool { true }
fn read(&self, _user_buf: UserBuffer) -> usize{
} // 直接panic
fn write(&self, user_buf: UserBuffer) -> usize {
} // 缓冲区无大小限制,读出数据并print!,返回缓冲区字节数
}

文件描述符:(文件描述符首先是一个非负整数)对于某一个进程,代表了其打开的一个文件对象,在要对文件进行操作时传入该整数即可(由内核来分配和记录,因为所有文件都在内核中)

文件描述符表:每个进程都有,记录所有其打开且可读写的文件集合(可以以表的下标作为描述符)

1
2
3
4
5
6
7
8
9
pub struct TaskControlBlockInner {
……
pub fd_table: Vec<Option<Arc<dyn File + Send + Sync>>>,
}
impl TaskControlBlock {
pub fn new(elf_data: &[u8]) -> Self {
} // 建立新的TCB时要创建三个fd_table:
// vec![Some(Arc::new(Stdin)),Some(Arc::new(Stdout)),Some(Arc::new(Stdout))] 0,1,2
}

此处的数据结构:

  • Vec 无需设置一个固定的文件描述符数量上限;

  • Option 区分一个文件描述符当前是否空闲:当它是 None 的时候是空闲的,而 Some 则代表它已被占用;

  • Arc 提供了共享引用能力:可能会有多个进程共享同一个文件对它进行读写;

    被它包裹的内容会被放到内核堆而不是栈上,不需要在编译期有确定的大小
  • dyn 关键字表明 Arc 里面的类型实现了 File/Send/Sync 三个 Trait ,

    编译期无法知道它具体是哪个类型需要等到运行时才能知道它的具体类型。
1
2
3
4
5
pub fn sys_write(fd: usize, buf: *const u8, len: usize) -> isize {
}
pub fn sys_read(fd: usize, buf: *const u8, len: usize) -> isize {
}
// 加锁使用write/read访问当前进程符表中标识符(下标)为fd的文件,返回值为w/r的返回值

文件系统接口

1
2
3
4
5
6
7
8
9
10
11
12
// 打开/读写的系统调用
fn sys_openat(dirfd: usize, path: &str, flags: u32, mode: u32) -> isize
// 打开文件并返回描述符,否则返回-1;dirfd/mode无视;path为文件名;flags描述打开文件的标志
bitflags! {
pub struct OpenFlags: u32 {
const RDONLY = 0; // 只读
const WRONLY = 1 << 0; // 只写
const RDWR = 1 << 1; // 可读写
const CREATE = 1 << 9; // 在找不到时允许创建
const TRUNC = 1 << 10; // 打开时清空文件并将大小归零
}
}

简易文件系统

  • easy-fs 是简易文件系统的本体
  • easy-fs-fuse 是能在开发环境(如 Ubuntu)中运行的应用程序,用于将应用打包为 easy-fs 格式的文件系统镜像,也可以用来对 easy-fs 进行测试

文件系统层次化(共分为五层,上层可以调用下层的接口):

1、磁盘块设备接口层:以块为单位对磁盘块设备进行读写的 trait 接口

1
2
3
4
pub trait BlockDevice : Send + Sync + Any {
fn read_block(&self, block_id: usize, buf: &mut [u8]); // 将编号为 block_id 的块从磁盘读入内存中的缓冲区 buf
fn write_block(&self, block_id: usize, buf: &[u8]); // 将内存中的缓冲区 buf 中的数据写入磁盘编号为 block_id 的块
} // 以实现了该特征的结构体为磁盘外设的模拟,在实现该特征时写入真正对硬件的操作

2、块缓存层:在内存中缓存磁盘块的数据,避免频繁读写磁盘

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// 块缓存
pub const BLOCK_SZ: usize = 512;
pub struct BlockCache {
cache: [u8; BLOCK_SZ], // 512字节数组,内存中的缓冲区
block_id: usize, // 这个块的编号
block_device: Arc<dyn BlockDevice>, // 块所属的底层设备
modified: bool, // 从磁盘进入内存后是否被修改
}
impl BlockCache {
pub fn new(block_id: usize, block_device: Arc<dyn BlockDevice>) -> Self {
} // 新建时从某个磁盘中读入一个块
}
impl BlockCache {
fn addr_of_offset(&self, offset: usize) -> usize {
} // 获得cache中偏移量offset的地址
pub fn get_ref<T>(&self, offset: usize) -> &T where T: Sized {
} // 获取缓冲区中偏移量为offset的一个T类型值的不可变引用
pub fn get_mut<T>(&mut self, offset: usize) -> &mut T where T: Sized {
} // 获取可变引用
pub fn read<T, V>(&self, offset: usize, f: impl FnOnce(&T) -> V) -> V {
} // 把闭包函数传入这个函数,在这个函数里面调用闭包直接返回其返回值,unsafe的T指针就不会给出去
pub fn modify<T, V>(&mut self, offset:usize, f: impl FnOnce(&mut T) -> V) -> V {
}
fn drop(&mut self) {
} // 缓冲区被回收,由modified决定是否写回磁盘
}

// 块缓存全局管理器:一个磁盘拥有一个
pub struct BlockCacheManager {
queue: VecDeque<(usize, Arc<Mutex<BlockCache>>)>, // 共享引用&互斥访问 磁盘块编号&块缓存
}
impl BlockCacheManager {
pub fn get_block_cache(&mut self,block_id: usize,block_device: Arc<dyn BlockDevice>,) -> Arc<Mutex<BlockCache>> {
} // 如果这里存储了block_id的块则返回其副本,没找到且队列未满则新建并插入返回,已满则丢出去未被使用(引用计数为1)的第一个块然后插入返回,否则panic
}

3、磁盘数据结构层:磁盘上的超级块、位图、索引节点、数据块、目录项等核心数据结构和相关处理

easy-fs 磁盘按照块编号从小到大顺序分成 5 个连续区域:

  • 第一个区域只包括一个块,它是超级块,用于定位其他连续区域的位置,检查文件系统合法性
  • 第二个区域是一个索引节点位图,长度为若干个块:记录索引节点区域中有哪些索引节点已经被分配出去使用了(每个bit表示一个节点)
  • 第三个区域是索引节点区域,长度为若干个块,其中的每个块都存储了若干个索引节点(每个节点描述一个“文件”/“目录”)
  • 第四个区域是一个数据块位图,长度为若干个块:记录后面的数据块区域中有哪些已经被分配出去使用
  • 最后的区域则是数据块区域:每个被分配出去的块保存了文件或目录的具体内容
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
// 第一个区域:超级块
pub struct SuperBlock {
magic: u32, // 魔数:验证有效性
pub total_blocks: u32, // 文件系统总块数
pub inode_bitmap_blocks: u32, // 索引节点位图块数
pub inode_area_blocks: u32, // 索引节点区域块数
pub data_bitmap_blocks: u32, // 数据块位图区域块数
pub data_area_blocks: u32, // 数据库区域块数
}
impl SuperBlock {
pub fn initialize(&mut self, total_blocks: u32, inode_bitmap_blocks: u32, inode_area_blocks: u32, data_bitmap_blocks: u32, data_area_blocks: u32 ); // 更上层初始化
pub fn is_valid(&self) -> bool { // 根据魔数判断合法
}
}

// 第二/四个区域:位图-索引节点 / 位图-数据块
pub struct Bitmap { // 位图区域管理器:区域起始编号+块数
start_block_id: usize,
blocks: usize,
}
type BitmapBlock = [u64; 64]; // 一个块数据(划分方便操作)
const BLOCK_BITS: usize = BLOCK_SZ * 8; // 一个块中有多少个bit
impl Bitmap {
pub fn alloc(&self, block_device: &Arc<dyn BlockDevice>) -> Option<usize> {
} // 遍历Bitmap中每个块,再遍历这个块中的每一个[u64]找到第一个为 0 的位置,该位置置1,返回该位的位置(在位图中的第n个比特位),以此作为分配的索引节点/数据块的编号;否则返回None
}

// 第三个区域:索引节点(包含文件/目录的元数据,以下仅写“文件”)
const INODE_DIRECT_COUNT: usize = 28;
pub struct DiskInode {
pub size: u32, // 文件内容的字节数
pub direct: [u32; INODE_DIRECT_COUNT], // 直接索引,数据块里是文件数据
pub indirect1: u32, // 一级间接索引,该块上每一个u32指向一个文件数据块
pub indirect2: u32, // 二级间接索引,该块上每一个指向一个一级间接索引
type_: DiskInodeType, // 索引节点类型
}
pub enum DiskInodeType {
File,
Directory,
}
impl DiskInode {
pub fn initialize(&mut self, type_: DiskInodeType) {
} // 一 / 二级索引全部置为0
pub fn is_dir(&self) -> bool {
}
pub fn is_file(&self) -> bool { // 确定文件类型
}
pub fn get_block_id(&self, inner_id: u32, block_device: &Arc<dyn BlockDevice>) -> u32 { // 获得索引中的第inner_id数据块的block_id
}
pub fn data_blocks(&self) -> u32 { // 返回文件内容字节数
}
fn _data_blocks(size: u32) -> u32 { // 容纳size个字节需要多少数据块
}
pub fn total_blocks(size: u32) -> u32 { // 返回文件块+索引块数量
}
pub fn blocks_num_needed(&self, new_size: u32) -> u32 {
} // 扩容到new_size个字节需要新增多少数据块
pub fn increase_size(&mut self, new_size: u32, new_blocks: Vec<u32>, block_device: &Arc<dyn BlockDevice>);
// 扩容函数,size为扩容后大小,blocks为扩容所需块编号(由上层分配)
pub fn clear_size(&mut self, block_device: &Arc<dyn BlockDevice>) -> Vec<u32>; //清空文件并且回收数据块
pub fn read_at(&self, offset: usize, buf: &mut [u8], block_device: &Arc<dyn BlockDevice>) -> usize {
} // 将文件内容从 offset 字节开始的部分读到内存中的缓冲区 buf 中,返回读到字节数
pub fn write_at()
}

// 目录项结构(文件项无结构)
const NAME_LENGTH_LIMIT: usize = 27;
pub struct DirEntry { // 该结构保存在数据块中
name: [u8; NAME_LENGTH_LIMIT + 1], // 这一级目录/这个文件的名字
inode_number: u32, // 这一级目录/这个文件的索引序号
}
pub const DIRENT_SZ: usize = 32; // 每个数据块可以存储16个
impl DirEntry {
pub fn empty() -> Self;
pub fn new(name: &str, inode_number: u32) -> Self;
pub fn name(&self) -> &str;
pub fn inode_number(&self) -> u32
}
pub fn as_bytes(&self) -> &[u8] { // 为了符合read/write_at接口
}
pub fn as_bytes_mut(&mut self) -> &mut [u8] {
}
}

4、磁盘块管理器层:合并了上述核心数据结构和磁盘布局所形成的磁盘文件系统数据结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 一个文件系统对应一个磁盘(分区)
pub struct EasyFileSystem {
pub block_device: Arc<dyn BlockDevice>,
pub inode_bitmap: Bitmap, // 索引节点位图
pub data_bitmap: Bitmap, // 数据块位图
inode_area_start_block: u32, // 索引区域起始块编号
data_area_start_block: u32, // 数据块区域起始块编号
}
impl EasyFileSystem {
pub fn create(block_device: Arc<dyn BlockDevice>, total_blocks: u32, inode_bitmap_blocks: u32,) -> Arc<Mutex<Self>> {
} // 传入 总共的块数量/索引位图的块数量 规划好整个磁盘,创建新文件系统并清理内存
// 同时初始化内存中的超级块、创建根目录
pub fn open(block_device: Arc<dyn BlockDevice>) -> Arc<Mutex<Self>> {
} // 从一个写入了easy-fs镜像(即按照该格式排布的数据块集合)的块设备block_device上打开文件系统
pub fn get_disk_inode_pos(&self, inode_id: u32) -> (u32, usize) {
} // 根据索引的编号,返回它所在磁盘块编号block_id和块内偏移
pub fn get_data_block_id(&self, data_block_id: u32) -> u32 {
} // 得到某数据块的磁盘块编号
pub fn alloc_inode(&mut self) -> u32 {
} // 分配一个索引返回其编号
pub fn alloc_data(&mut self) -> u32 {
} // 分配一个数据块返回其磁盘块编号
pub fn dealloc_data(&mut self, block_id: u32) {
}
}

注意:只要知道了数据所在的具体磁盘块号和块内偏移,可以以任意结构体方式操作这一段数据get_block_cache(block_id, device).lock().modify(offset, |变量名: &mut 结构体| {})

5、索引节点层:管理索引节点,实现了文件创建/文件打开/文件读写等成员函数

便于直接看到目录树结构中逻辑上的文件和目录

  • DiskInode 放在磁盘块中比较固定的位置
  • Inode 是放在内存中的记录文件索引节点信息的数据结构
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
pub struct Inode {
block_id: usize,
block_offset: usize, // 前两个用于记录具体位置
fs: Arc<Mutex<EasyFileSystem>>, // 便于操作传参
block_device: Arc<dyn BlockDevice>,
}

// 简化 get_block_cache.lock.read/modify 流程
impl Inode {
fn read_disk_inode<V>(&self, f: impl FnOnce(&DiskInode) -> V) -> V {
} // 读取数据并且用闭包f处理
fn modify_disk_inode<V>(&self, f: impl FnOnce(&mut DiskInode) -> V) -> V { // 读取可修改数据并且用闭包f处理(都是更改索引节点 DiskInode 的值)
}
}

impl EasyFileSystem {
pub fn root_inode(efs: &Arc<Mutex<Self>>) -> Inode {
} // 获取根目录
}

impl Inode {
pub fn new(block_id: u32, block_offset: usize, fs: Arc<Mutex<EasyFileSystem>>, block_device: Arc<dyn BlockDevice>) -> Self {
} // 创建
// 文件索引(仅根目录调用)
pub fn find(&self, name: &str) -> Option<Arc<Inode>> {
} // 返回本索引指向的目录下名称为name的文件的索引的新建Inode
fn find_inode_id(&self, name: &str, disk_inode: &DiskInode) -> Option<u32> { // 找到传入的索引节点指向的目录下名称为name的文件的索引序号
}
// 文件列举(仅根目录调用)
pub fn ls(&self) -> Vec<String> {
} // 返回该目录下所有文件的文件名的数组
// 文件创建(仅根目录调用)
pub fn create(&self, name: &str) -> Option<Arc<Inode>> {
} // 创建新的文件并返回其Inode,若存在则返回None
// 文件清空
pub fn clear(&self) {
} // 以某些方式打开时需要先传入文件Inode清空
// 文件读写
pub fn read_at(&self, offset: usize, buf: &mut [u8]) -> usize {
} // 从offset偏移处开始读
pub fn write_at(&self, offset: usize, buf: &[u8]) -> usize {
} // 注意:在写之前需要先扩容为offset+buf.len()容量
fn increase_size(&self, new_size: u32, disk_inode: &mut DiskInode, fs: &mut MutexGuard<EasyFileSystem>) {
}
}

内核中的easy-fs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
// 内核块设备实例
type BlockDeviceImpl = virtio_blk::VirtIOBlock; // 一个实现了BlockDevice类特征的结构体
lazy_static! {
pub static ref BLOCK_DEVICE: Arc<dyn BlockDevice> = Arc::new(BlockDeviceImpl::new());
}

// 内核索引节点层
pub struct OSInode { // 表示进程中一个被打开的常规文件或目录
readable: bool,
writable: bool,
inner: UPSafeCell<OSInodeInner>,
}
pub struct OSInodeInner {
offset: usize, // 读写过程中的偏移量
inode: Arc<Inode>,
}
impl File for OSInode {
fn readable(&self) -> bool { self.readable }
fn writable(&self) -> bool { self.writable }
fn read(&self, mut buf: UserBuffer) -> usize {
} // 直接使用inode的read/write_at
fn write(&self, buf: UserBuffer) -> usize {
}
}

// 内核文件系统实现
// 文件系统初始化
lazy_static! {
pub static ref ROOT_INODE: Arc<Inode> = {
}; // 打开efs并且获取根目录
}
// 打开文件
bitflags! {
pub struct OpenFlags: u32 { // 打开文件的方式
const RDONLY = 0;
const WRONLY = 1 << 0;
const RDWR = 1 << 1;
const CREATE = 1 << 9;
const TRUNC = 1 << 10;
}
}
impl OpenFlags {
pub fn read_write(&self) -> (bool, bool) {
} // 返回(可读,可写)
}
pub fn open_file(name: &str, flags: OpenFlags) -> Option<Arc<OSInode>> {
} // 根据flags的要求打开文件并且返回inode

进程间通信

管道

1
2
3
// 系统调用:为当前进程打开一个管道(包含一个只读、一个只写文件)
pub fn sys_pipe(pipe: *mut usize) -> isize;// pipe:应用地址空间中一个长度为2的usize数组的起始地址,内核负责讲管道读端、写端的文件描述符写入
pub fn sys_close(fd: usize) -> isize; // 关闭一个文件

基于文件的管道

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
// 管道的一端(读/写):会实现File特征,作为文件访问
pub struct Pipe {
readable: bool,
writable: bool,
buffer: Arc<Mutex<PipeRingBuffer>>,
}
impl Pipe {
pub fn read_end_with_buffer(buffer: Arc<Mutex<PipeRingBuffer>>) -> Self {}
pub fn write_end_with_buffer(buffer: Arc<Mutex<PipeRingBuffer>>) -> Self {} // 根据一个已有的管道创建其读端和写端
}
impl File for Pipe {
fn read(&self, buf: UserBuffer) -> usize {
} // 从文件中最多读取应用缓冲区大小那么多字符:如果文件中没有字符且没有写端则返回,否则任务挂起
}

// 管道:
enum RingBufferStatus { FULL, EMPTY, NORMAL }
pub struct PipeRingBuffer {
arr: [u8; RING_BUFFER_SIZE],
head: usize,
tail: usize, // 维护循环队列
status: RingBufferStatus, // 缓冲区状态
write_end: Option<Weak<Pipe>>, // 写端的弱引用计数,来确认是否还有写端
}
impl PipeRingBuffer {
pub fn new() -> Self { // 全部置为0,写端置为None
}
pub fn set_write_end(&mut self, write_end: &Arc<Pipe>) {
} // 保留其写端的弱引用计数
pub fn read_byte(&mut self) -> u8 {
} // 读取管道中的一个字节,并且更新队头(如果与队尾重合则改为empty)
pub fn available_read(&self) -> usize {
} // 计算管道中还有多少个字节可以读取
pub fn all_write_ends_closed(&self) -> bool {
} // 判断管道的所有写端是不是都被关闭了
}

pub fn make_pipe() -> (Arc<Pipe>, Arc<Pipe>) { // 创建一个管道并返回其读写端
}

impl TaskControlBlockInner {
pub fn alloc_fd(&mut self) -> usize {
} // 在进程控制块中分配一个最小的空闲文件描述符来访问新打开的文件
}

命令行参数与标准I/O重定向

命令行参数

在user_shell中读取一行后,根据空格分隔成Vec<String>,然后手动在每个字符串后面加上\0,在最后加上0 as *const u8,将字符串数组的起始地址传入sys_exec()内核调用

1
2
3
4
5
6
7
8
// 系统调用更新
pub fn sys_exec(path: *const u8, mut args: *const usize) -> isize {
} // 依次转换地址并取出参数,在调用TCB::exec时传入

impl TaskControlBlock {
pub fn exec(&self, elf_data: &[u8], args: Vec<String>) {
} // 在从ELF文件创建进程后把参数压入用户栈(此时用户栈为空)
}

根据TCB创建时从ELF中读出来的内容,所有进程第一次进入用户态都是从_start进入:这个函数会依次取出命令行参数并且放入一个数组中

标准输入输出重定向

1
2
// 系统调用:将进程中一个已经打开的文件复制一份并分配到一个新的文件描述符中
pub fn sys_dup(fd: usize) -> isize; //(在符表中分配一个新的描述符指向一个复制)

在用户态的user_shell程序中,要检查是否存在通过</>进行输入输出重定向:

存在则移除,并记录输入/输出的文件名并打开;

这时候关掉0/1的文件描述符,给打开的文件dup一个新的,由于alloc_fd()一定会分配最小的可用文件描述符(先扫描符表中有无可用的,再push一个),所以这个文件就可以顶替掉0/1

并发

  • 存在线程前:进程是程序的基本执行实体,是程序关于某数据集合上的一次运行活动,是 系统进行资源(处理器、 地址空间和文件等)分配和调度的基本单位。
  • 存在线程后:进程是线程的资源容器, 线程成为了程序的基本执行实体。

并发相关术语

  • 共享资源(shared resource):不同的线程/进程都能访问的变量或数据结构。
  • 临界区(critical section):访问共享资源的一段代码。
  • 竞态条件(race condition):多个线程/进程都进入临界区时,都试图更新共享的数据结构,导致产生了不期望的结果。
  • 不确定性(indeterminate): 多个线程/进程在执行过程中出现了竞态条件,导致执行结果取决于哪些线程在何时运行, 即执行结果不确定,而开发者期望得到的是确定的结果。
  • 互斥(mutual exclusion):一种操作原语,能保证只有一个线程进入临界区,从而避免出现竞态,并产生确定的执行结果。
  • 原子性(atomic):一系列操作要么全部完成,要么一个都没执行,不会看到中间状态。在数据库领域, 具有原子性的一系列操作称为事务(transaction)。
  • 同步(synchronization):多个并发执行的进程/线程在一些关键点上需要互相等待,这种相互制约的等待称为进程/线程同步。
  • 死锁(dead lock):一个线程/进程集合里面的每个线程/进程都在等待只能由这个集合中的其他一个线程/进程 (包括他自身)才能引发的事件,这种情况就是死锁。
  • 饥饿(hungry):指一个可运行的线程/进程尽管能继续执行,但由于操作系统的调度而被无限期地忽视,导致不能执行的情况。

线程(一个进程在一个时刻有多个执行点)

  • 程序计数器寄存器来记录当前的执行位置
  • 一组通用寄存器记录当前的指令的操作数据
  • 一个栈来保存线程执行过程的函数调用栈和局部变量
1
2
3
4
5
// 系统调用
pub fn sys_thread_create(entry: usize, arg: usize) -> isize
// 创建新的线程(entry:入口函数地址, arg:参数)
pub fn sys_waittid(tid: usize) -> i32
// 等待线程结束:进程/主线程回收资源(tid:线程id,由主线程调用)

线程管理由进程而来

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
pub struct TaskControlBlock {
pub process: Weak<ProcessControlBlock>,
pub kernel_stack: KernelStack,
inner: UPSafeCell<TaskControlBlockInner>, // 可变
}
pub struct TaskControlBlockInner {
pub trap_cx_ppn: PhysPageNum, // 应用地址空间中线程的 Trap 上下文被放在的物理页帧的物理页号
pub task_cx: TaskContext, // 暂停线程的线程上下文,用于线程切换
pub task_status: TaskStatus, // 当前线程的执行状态
pub exit_code: Option<i32>, // 线程退出码
pub res: Option<TaskUserRes>,
}
pub struct TaskUserRes {
pub tid: usize, // 线程标识符
pub ustack_base: usize, // 线程栈顶
pub process: Weak<ProcessControlBlock>, // 所属进程
}
进程控制块
1
2
3
4
5
6
7
8
9
pub struct ProcessControlBlock {
pub pid: PidHandle,
inner: UPSafeCell<ProcessControlBlockInner>,
}
pub struct ProcessControlBlockInner {
...
pub tasks: Vec<Option<Arc<TaskControlBlock>>>,
pub task_res_allocator: RecycleAllocator,
}

相关数据结构:使用锁来包裹共享资源

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
pub struct ProcessControlBlock {
pub pid: PidHandle,
inner: UPSafeCell<ProcessControlBlockInner>,
}
pub struct ProcessControlBlockInner {
...
pub mutex_list: Vec<Option<Arc<dyn Mutex>>>, // 进程可能存在多个互斥资源
}
pub trait Mutex: Sync + Send { // 互斥锁特征
fn lock(&self);
fn unlock(&self);
}
pub struct MutexBlocking { // 实现互斥锁特征的结构
inner: UPSafeCell<MutexBlockingInner>,
}
pub struct MutexBlockingInner {
locked: bool, // 是否上锁
wait_queue: VecDeque<Arc<TaskControlBlock>>, // 上锁的等待队列
}

相关系统调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
pub fn sys_mutex_create(blocking: bool) -> isize {
} // 创建一个新的锁:当前mutex_list是否有空位/锁的类型来创建

pub fn sys_mutex_lock(mutex_id: usize) -> isize {
} // 只负责调用当前线程的第id个锁的lock()方法
impl Mutex for MutexBlocking {
fn lock(&self) {
} // 如果已经上锁则加入等待队列,否则上锁
}

pub fn sys_mutex_unlock(mutex_id: usize) -> isize {
}// 只负责调用当前线程的第id个锁的unlock()方法
impl Mutex for MutexBlocking {
fn unlock(&self) {
} // 如果有等待的线程则唤醒,否则释放锁
}

信号量:适用于一个共享资源可以被有限个线程同时访问的情况(互斥锁即为N=1)

P操作:尝试进入,失败则阻塞

V操作:信号量的值+1,如果有线程等待则唤醒

(注意,以上两个操作都应该有原子性)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
pub struct ProcessControlBlock {
pub pid: PidHandle,
inner: UPSafeCell<ProcessControlBlockInner>,
}
pub struct ProcessControlBlockInner {
...
pub semaphore_list: Vec<Option<Arc<Semaphore>>>, // 进程可能有多个信号量
}

pub struct Semaphore {
pub inner: UPSafeCell<SemaphoreInner>,
}
pub struct SemaphoreInner {
pub count: isize, // 信号量值(count <= 0时代表有线程在等待)
pub wait_queue: VecDeque<Arc<TaskControlBlock>>, // 等待序列
}
impl Semaphore {
pub fn new(res_count: usize) -> Self {
} // 创建新的信号量,置放初始信号值
pub fn up(&self) {
} // V操作:count++, count <= 0时从等待队列中取队头唤醒
pub fn down(&self) {
} // P操作:count--, count < 0时进入等待队列
}

条件变量

线程在检查满足某一条件后才会执行(条件变量时一个线程等待队列)

wait操作:释放锁 => 挂起自己 => 被唤醒后获取锁

signal操作:找到挂在条件变量上面的线程并唤醒

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
pub struct ProcessControlBlock {
pub pid: PidHandle,
inner: UPSafeCell<ProcessControlBlockInner>,
}
pub struct ProcessControlBlockInner {
...
pub condvar_list: Vec<Option<Arc<Condvar>>>, // 进程中的条件变量列表
}
pub struct Condvar {
pub inner: UPSafeCell<CondvarInner>,
}
pub struct CondvarInner {
pub wait_queue: VecDeque<Arc<TaskControlBlock>>, // 条件变量:等待队列
}
impl Condvar {
pub fn new() -> Self {
} // 创建空的等待队列
pub fn signal(&self) {
} // 唤醒条件变量的等待队列的队头
pub fn wait(&self, mutex:Arc<dyn Mutex>) {
} // 释放互斥锁(线程在进入前获取),挂起当前进程,恢复后获取锁
}

ArceOS学习记录

第三阶段时间比较紧张,就只能写写测试写不了挑战题了

第一部分UniKernel

print_with_color

在输出println宏的实现处加上标识颜色的ASCⅡ码即可,需要注意(本人踩过坑)的是如果不想引入format!来直接把颜色字符拼接进入要打印字符中而是分别打印,需要注意字符打印导致的换行问题,这个会导致后边测试脚本在读取数据时检测不通过的问题

support_hashmap

这个去网上查了一下资料,还以为有什么高级实现的方法

结果最后还是使用了最朴实无华的取模插入

果然所有的数据结构都很难想

alt_alloc

在怎么实现上还是纠结了挺久的,是存储字符数转化成页还是存储页转化成字符、如果有新加入的内存怎么在其中表示……后面不得不去找了一下,发现原来可以不支持新加入内存()

其实还是学到了很多的,在上操作系统理论课的时候根本没有想过这些内存是由一个统一初始化的内存管理器来进行管理,就只是单纯的知道了一下页表是什么

第二部分宏内核

sys_mmap

难度还可以,好像在这里没有花很久时间

ramfs_rename

因为没有什么大项目的经验导致对依赖很不敏感,在实现了之后一直进入不了我想用的DirNode的trait里面

后面不断调试才终于在偶然中发现如果不在根目录的cargo.toml中使用patch的话需要改两个cargo.toml

第三部分Hypervisor

simple_hv

这个其实挺简单的,但是卡了我很久很久。遇到的问题是我在一开始就触发不了panic程序会直接卡死,然后依然是不断通过打印去定位错误,竟然发现卡死在了_run_guest的汇编代码里面,就硬着头皮去看汇编。还是一直发现不了卡死的原因……

总之就是试了很久,分析qemu的日志才发现是store_page_fault和时钟错误交替出现,拷打了一下AI之后才发现是没有

Lfan-ke:第三阶段总结报告

about-me: heke1228@gitee, heke1228@atom, Lfan-ke@github, heke1228@codeberg

内核发展史

很早很早以前~,并不区分应用与操作系统。所有功能都绑定在一起,负责开发的也是同一批人。某些底层功能被频繁复用,慢慢的常用功能形成了特定的模块,通过接口与其他模块交互。比如:储存模块就是一个大数组。那么,为什么不能把系统作为一个库呢?

多道操作系统通过分时复用的方式在一台计算机上同时运行多个应用程序。但是出现了安全问题:如果每个应用都可以控制全局资源,如何保证不同应用之间的隔离?不会出现A想格式化,B想重启……所以必须限制应用不允许直接改变全局的系统状态。所以应用与系统分离,至少需要两种权限。

低权限的不允许改变全局系统状态,用来运行应用,高权限的集中运行能改变全局系统状态的操作(特权操作),分化出操作系统内核。

CPU对软件提供的接口:ISA - Instruction Set Architecture - 指令集架构 ::: RV64、x86、mips、LoongArch等等。软硬件的分界线以及交互规范标准。

RISC-V指令集 = 基础指令集 + 标准扩展指令集 + 用户自定义扩展指令集,比如RV32IM就是RV32的拥有整数以及乘除法指令的配置。RV64GC的G是一个省略书写,实际GC = IMAFD + C。RV32E是一个16个寄存器的嵌入式精简指令集。在gcc编译时使用march指定。使用mabi指定应用程序二进制接口对应类型的字长,比如ilp指的是int/long/usize_t(void*ptr)为32位,lp64f指的是int32位,long/usize_t为64位,支持单精度浮点采用浮点寄存器传递,但是双精度浮点仍然采用栈传递(即硬件指令集得支持F/D扩展,不然你硬件只有整数寄存器,没有对应精度的浮点寄存器)。在未支持的扩展,比如RV64I中书写乘法,则会以软件替代的形式出现,比如使用循环和移位的函数__mulsi3来替代(当然,如果target=RVxIM但是运行在RVxI,M系指令也会通过trap的形式硬件兜底执行,但是效率低,其他扩展类似)。2020年,RISC-V发展的优先级从体系结构驱动切换为软件驱动。

1
2
3
4
5
6
7
8
9
10
11
12
13
riscv64-unknown-elf-gcc -march=rv32im -mabi=ilp32 -o program.elf program.c
riscv64-unknown-elf-g++ -march=rv64gc -mabi=lp64 -o program.elf program.cpp
rustc --target=riscv32imac-unknown-none-elf -C target-feature=+m,+a,+c program.rs
GOOS=linux GOARCH=riscv64 go build -o program program.go
tinygo build -target=riscv32-unknown-elf -o program.elf program.go
# cargo.toml:
[build]
target="riscv64gc-unknown-none-elf"
[target.riscv64gc-unknown-none-elf]
rustflags = [
"-C", "link-arg=-Tlink.x", # 可选:自定义链接脚本
"-C", "target-feature=+m,+a,+f,+d,+c", # 显式启用扩展
]

ISA包含:指令、寄存器等软件可见可操作的接口,从上到下切换的过程通常被称为陷入(trap),比如S-sbicall->M/U-syscall->S/V<-syscall->H都是ecall和trap。其中trap是由硬件监测的,比如检测到某些错误就陷入M-Mode:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
module trap_detector (
input logic clk,
input logic rst,
input logic [31:0] current_pc,
input logic [31:0] instruction,
input logic page_fault, // 页错误
input logic illegal_instr, // 非法指令
input logic timer_interrupt, // 定时器中断
...
output logic [3:0] trap_cause // 陷阱原因(scause 编码)
);

always_comb begin
trap_triggered = 0;
trap_cause = 0;

// 同步异常检测 - 异常 - Exception
if (xxx异常) begin
trap_triggered = 1;
trap_cause = 2;
end else ...

// 异步中断检测 - 中断 - Interrupt
else if (计时器等等) begin
trap_triggered = 1;
trap_cause = {1'b1, timer_interrupt ? 3'd5 : 3'd9};
end
end
endmodule

// 检测到之后trap_handler触发硬件(trap)特权级切换,但是像跳板页是软件(ecall)特权级切换
// 补充:trap - riscv将ECF(异常控制流(Exceptional Control Flow))统称为trap
// 补充:无论是ecall还是trap,都是增删改部分csr与pc,处理结束后通常会逆增删改回到原位/+4
// 补充:ecall-U->S -> crud: scause/sepc等等 ecall-S->M -> crud: mcause/mepc等等 trap -> crud: mcause/mepc等等
// 可以是硬件处理,也可以是软件处理,比如编写中断处理函数并将地址刷入中断向量表
// 主打一个硬件可以做的软件也可以做,软件可以做的硬件也可以做,软慢成本低,硬快成本高……

操作系统在启动前需要先对部分寄存器赋予初值(初始化),以及传递设备等等硬件信息,这部分工作由SBI来完成。OS若需要M支持需要调用sbicall(比如操作定时器、关闭中断等等)。

定义底层固件与OS的接口:SBI - Supervisor Binary Interface - 监管层二进制接口 ::: x86-UEFI/BIOS-Grub、RV-opensbi/rustsbi等等。

在SBI初始化结束,PC会被安置到OS的执行入口,开始操作系统初始化,比如开启页表、虚拟空间映射、启动第一个进程等等。比如RISC-V架构通常被放置在0x8020_0000

操作系统:OS - 用来管理硬件资源并向上层应用提供统一的服务。除了最基础的存储、运行外,还有诸多如:网络、设备、显示等等扩展,以及:运行时环境、集成开发环境、基础库、编译工具链、编程语言、调试工具等等基础设施。

操作系统启动之后,如RISC-V架构,通常为三个特权级:MSU(虚拟化有五个,新增:VS VU,原S->HS)。用户程序运行在最上层。用户程序若需要S特权级支持需要调用syscall。基于操作系统的标准库,如GNUlibC,通常封装好了一部分syscall的便捷的调用方式,比如fork、printf -> write -> sys_write等等。

操作系统进行了内外两种演化:

  • 外部接口:POSIX接口演化、系统调用的增删改、分布式软总线(鸿蒙)等等
  • 内部架构:宏、微、外、多,以及在扩展性安全性性能等等方面的改进

宏内核:整个系统分为内核和应用两层,常见比如Linux的主体是宏内核。用户进程通过系统调用使用内核的各项功能。但是系统过于庞大。大量共享状态位于内核态。

微内核:最小化内核功能,将操作系统功能迁移到用户态,称之为”服务“,用户模块之间使用消息传递机制通信。常见比如:WinNT。WinNT实际上是混合内核,但其大部分功能采用微内核实现。共享数据状态部分在内核态,部分在用户态。

混内核:比如上述的WinNT以及MacOS/iOS,将需要性能的模块(线程调度、虚拟内存、IPC-进程间通信、图形子系统)放在内核态,扩展功能(文件系统、网络协议栈)放于用户态。

外核库:Exokernel不管理资源,只管理应用(计算资源、隔离等等),库OS则对硬件的抽象以库的形式提供,不通应用可以使用不通的LibOS。将管理与保护分离。

单内核:Unikernel(单/联内核),使用组件扩展操作系统的功能,在编译时确定系统组件。可看作虚拟化环境下的LibOS。应用与内核位于同一特权级。通过扩展,联内核可扩展为宏内核以及支持虚拟化。常见比如:ArceOS、Rumprun、Drawbridge、OSv等等。以及一个可以将Linux作为联内核的项目:LKL

多内核:又称:复内核。OS整体是一个分布式系统,应用程序仍然运行在OS之上。默认的状态是划分而非共享,显式的核间通信机制。支持设备(比如NPU/GPU等等)上的异构CPU。常见的如:Barrelfish、Popcorn Linux。

Read more »

阶段二

这个阶段给我最大的感受是学习,因为很多函数都被事先书写好了,这几个实验基本都是在已有的框架上进行相关的函数调用即可,在明白每个函数的实现逻辑以及每个lab实验要实现的功能即可完成实验。同时这个阶段给我的感受是有许多深层次的知识仍需要学习,有许多一知半解的知识需要实践,看到学习群里面大家对于实验的讨论的诸多见解,受益匪浅,完完全全的拓宽了我的眼界,许多从未听过的名词出现在我的面前,只感到纸上得来终觉浅,绝知此事要躬行。可惜时间所迫,没办法对于每个lab实验进行进一步的深究。
第二阶段我重点理会到了操作系统的一步步发展以及实现的功能,在进行每个实验的时候,感受到学校里面的讲解十分片面,并且浅薄(也是我没有认真学习的缘故,学校里面的课检验的只有期末突击而已)。
非常重要的一点是这次的理论和实践一起进行,让我极大的认识到了抽象和具体之间的联系,有时候理论上很复杂并且难以理解的东西,转化到实践上面竟然可能只是一个数组,一个函数栈而已,这让欠缺实践的我大开眼界。
非常期待第三阶段带来的挑战,也非常希望可以通过第三阶段(哈哈哈)。

阶段三

这个阶段我做的十分仓促,并且没有详细做挑战题的内容,arceos组件化的设计思路让我受益匪浅,项目的设计理念和一些细节都要比rcore要好很多,也复杂很多。四阶段项目阶段我没有什么了解,只能希望自己可以投入进去,获得一些成果吧。
临近毕业,对于未来仍然十分迷茫,写下三阶段总结时,我刚刚完成我的本科毕业设计,对未来没有一个清晰的规划,没想到这么快就要毕业了。原计划我准备这个训练营和毕业一起做完,可惜并没有达成我原先的目标,最后一个月的时间,希望可以再接再厉吧,这可能也是我仅剩的校园时光了…