Skip to content

Commit

Permalink
2024秋操作系统训练营一二阶段总结 (#514)
Browse files Browse the repository at this point in the history
* 707state rCore blog
  • Loading branch information
707state authored Nov 8, 2024
1 parent 452c27e commit bb3ea76
Show file tree
Hide file tree
Showing 2 changed files with 264 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
---
title: 2024秋操作系统训练营第一阶段总结——707state
date: 2024-11-07 23:32:00
tags:
- author:707state
- repo:https://github.com/LearningOS/2024a-rcore-707state
---

# Rustling总结

我想成为Rustacean,通过rustling的练习,我学到一些没接触过的Rust知识

# repr attribute

在c/c++中,常常使用__attribute__((align))的方式来确保内存对齐,repr在rust的功能久石让每一个数据能够按照k的整数倍分配,k通常是基本类型。


# Rusty 阶乘

```rust
pub fn factorial(num: u64) -> u64 {
(1..=num).fold(1, |acc, x| acc * x)
}
```

范围 (1..=num):

这个表达式创建了一个从 1 到 num 的范围,包括 num 本身。..= 是一个闭区间语法,表示这个范围是包含起始值和结束值的。

fold 方法:

fold 是一个高阶函数,它用于将一个迭代器中的所有元素合并成一个单一的值。
它的第一个参数是初始值,在这里是 1。这个值会作为累加器的初始状态。
第二个参数是一个闭包(匿名函数),它接受两个参数:acc(累加器的当前值)和 x(迭代器中的当前值)。
在每一次迭代中,闭包会将 acc 和 x 相乘,并返回新的累加器值。

# 流敏感分析

依靠 Borrow Checker 确保内存安全,原理如下:

执行路径分析:Rust 编译器在编译过程中会为每个变量跟踪其借用状态或所有权。在程序的不同控制流(如条件判断、循环、函数调用等)中,编译器会检查在这些不同路径下变量的状态变化,并在所有路径中保持一致性。

数据流分析:Rust 编译器通过数据流分析来确定变量的生存期、是否被借用、以及是否存在竞争条件。这个分析是流敏感的,意味着它会根据程序的控制流更新变量状态。

作用域检查:编译器在分析时会检查变量是否在作用域内、是否已经被销毁或者转移所有权。通过流敏感分析,编译器能够精确确定哪些变量在某条执行路径中被有效引用或借用。


# take

在 Rust 中,take() 方法通常用于在某些容器类型(如 Option、Result 等)中“取走”值,将原来的值替换为一个默认值(通常是 None 或 Err),同时返回原来的值。

take() 经常用于以下场景:

转移所有权:从一个可选值中取出所有权,并清空该值,避免复制或克隆。

链表或树结构:当你遍历或修改链表或树结构时,可以用 take() 来“取走”节点的引用并避免借用冲突。

# zero-cost futures

async/await实现的future类型不会引入任何额外的运行时开销。

无运行时依赖:与其他编程语言(如 JavaScript 或 Python)不同,Rust 的 async/await 本身不依赖特定的运行时机制。Future 是惰性的,它本质上是一个状态机,只有在被轮询时才会前进。虽然需要某种形式的运行时(如 Tokio 或 async-std)来调度异步任务,但这些运行时并没有与 async/await 特性本身紧耦合。

# Any Trait

Rust中的Any trait允许在运行时进行类型检查和类型转换。这个类型在处理动态类型时较为有用。

功能:

1. 类型检查:通过is::<T>()方法,可以检查一个值是否是某种特定类型。

2. 类型转换:使用downcast_ref::<T>()和downcast::<T>()方法,可以将一个&dyn Any或者Box<dyn Any>类型的值转换回具体类型T。

注意:Any trait只能用于'static 生命周期的类型,这意味着他不能用于包含非静态引用的类型。

并且,Any会引入运行时开销,因为它依赖动态分派。


# UnsafeCell

Rust 中内部可变性的核心原语。

如果您使用的是 &T,则通常在 Rust 中,编译器基于 &T 指向不可变数据的知识来执行优化。例如通过别名或通过将 &T 转换为 &mut T 来可变的该数据,被认为是未定义的行为。 UnsafeCell<T> 选择退出 &T 的不可变性保证:共享的引用 &UnsafeCell<T> 可能指向正在发生可变的数据。这称为内部可变性。

所有其他允许内部可变性的类型,例如 Cell<T> 和 RefCell<T>,在内部使用 UnsafeCell 来包装它们的数据。

所有其他允许内部可变性的类型,例如 Cell<T> 和 RefCell<T>,在内部使用 UnsafeCell 来包装它们的数据。

UnsafeCell API 本身在技术上非常简单: .get() 为其内容提供了裸指针 *mut T。正确使用该裸指针取决于您。

如果您使用生命周期 'a (&T 或 &mut T 引用) 创建安全引用,那么您不得以任何与 'a 其余部分的引用相矛盾的方式访问数据。 例如,这意味着如果您从 UnsafeCell<T> 中取出 *mut T 并将其转换为 &T,则 T 中的数据必须保持不可变 (当然,对 T 中找到的任何 UnsafeCell 数据取模),直到引用的生命周期到期为止。 同样,如果您创建的 &mut T 引用已发布为安全代码,则在引用终止之前,您不得访问 UnsafeCell 中的数据。



对于没有 UnsafeCell<_> 的 &T 和 &mut T,在引用过期之前,您也不得释放数据。作为一个特殊的例外,给定一个 &T,它在 UnsafeCell<_> 内的任何部分都可能在引用的生命周期期间被释放,在最后一次使用引用之后 (解引用或重新借用)。 因为您不能释放引用指向的部分,这意味着只有当它的每一部分 (包括填充) 都在 UnsafeCell 中时,&T 指向的内存才能被释放。

但是,无论何时构造或解引用 &UnsafeCell<T>,它仍必须指向活动内存,并且如果编译器可以证明该内存尚未被释放,则允许编译器插入虚假读取。

在任何时候,您都必须避免数据竞争。如果多个线程可以访问同一个 UnsafeCell,那么任何写操作都必须在与所有其他访问 (或使用原子) 相关之前发生正确的事件。

为了帮助进行正确的设计,以下情况明确声明为单线程代码合法:

&T 引用可以释放为安全代码,并且可以与其他 &T 引用共存,但不能与 &mut T 共存

&mut T 引用可以发布为安全代码,前提是其他 &mut T 和 &T 都不共存。&mut T 必须始终是唯一的。


请注意,虽然可以更改 &UnsafeCell<T> 的内容 (即使其他 &UnsafeCell<T> 引用了该 cell 的别名) 也可以 (只要以其他方式实现上述不变量即可),但是具有多个 &mut UnsafeCell<T> 别名仍然是未定义的行为。 也就是说,UnsafeCell 是一个包装器,旨在通过 &UnsafeCell<_> 与 shared accesses (i.e. 进行特殊交互 (引用) ; 通过 &mut UnsafeCell<_> 处理 exclusive accesses (e.g. 时没有任何魔术) : 在该 &mut 借用期间, cell 和包装值都不能被别名。

.get_mut() 访问器展示了这一点,该访问器是产生 &mut T 的 safe getter。

UnsafeCell<T> 与其内部类型 T 具有相同的内存表示。此保证的结果是可以在 T 和 UnsafeCell<T> 之间进行转换。 将 Outer<T> 类型内的嵌套 T 转换为 Outer<UnsafeCell<T>> 类型时必须特别小心: 当 Outer<T> 类型启用 niche 优化时,这不是正确的。


# std::mem::MaybeUninit

Rust 编译器要求变量要根据其类型正确初始化。

比如引用类型的变量必须对齐且非空。这是一个必须始终坚持的不变量,即使在 Unsafe 代码中也是如此。因此,零初始化引用类型的变量会导致立即未定义行为,无论该引用是否访问过内存。

编译器利用这一点,进行各种优化,并且可以省略运行时检查。

由调用者来保证MaybeUninit<T>确实处于初始化状态。当内存尚未完全初始化时调用 assume_init() 会导致立即未定义的行为。
```rs

#![allow(unused)]

fn main() {
use std::mem::{self, MaybeUninit};
// 不符合:零初始化引用
let x: &i32 = unsafe { mem::zeroed() }; // undefined behavior! ⚠️
// 等价于 `MaybeUninit<&i32>`:
let x: &i32 = unsafe { MaybeUninit::zeroed().assume_init() }; // undefined behavior!
// 不符合:布尔值必须初始化
let b: bool = unsafe { mem::uninitialized() }; // undefined behavior! ⚠️
// 等价于 `MaybeUninit<bool>`:
let b: bool = unsafe { MaybeUninit::uninit().assume_init() }; // undefined behavior!
// 不符合:整数类型也必须初始化
let x: i32 = unsafe { mem::uninitialized() }; // undefined behavior! ⚠️
// 等价于 `MaybeUninit<i32>`:
let x: i32 = unsafe { MaybeUninit::uninit().assume_init() };

// 不符合:Vec未初始化内存使用 set_len 是未定义行为
let mut vec: Vec<u8> = Vec::with_capacity(1000);
unsafe { vec.set_len(1000); }
reader.read(&mut vec); // undefined behavior!
}
```


主要就是这些收获。
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
---
title: 2024秋操作系统训练营第二阶段总结——707state
date: 2024-11-07 23:40:45
tags:
- author:707state
- repo:https://github.com/LearningOS/2024a-rcore-707state
---

# 总结

去年就已经参加一次了,但是止步第一阶段,今年能做完第二阶段已经出乎我的意料了,收获良多。

# syscall

syscall是怎么发起的,系统调用时发生了哪些事情,这些都是第一次接触。

## 收获

## spawn实现

如果不考虑其他因素,其实spawn类似于vfork+exec。

假定fork采用的是直接复制的策略,那vfork就是采用的阻塞父进程执行、获得父进程地址空间的引用、子进程运行、执行完成、恢复父进程。


## fork 调用

主要部分是调用TaskControlBlock::fork, 返回父进程的TaskControlBlock的拷贝,并分配一个新的Pid。

## Task部分各个结构的关系

### TaskControlBlock

TaskControlBlock 代表一个任务或进程的控制块,用于存储任务的基本信息和状态。它包含了所有不会在运行时改变的内容,如进程ID (pid) 和内核栈 (kernel_stack)。此外,它还包含一个可变的 inner 字段,该字段封装了实际的任务状态信息:

pid:任务的唯一标识符。
kernel_stack:内核栈,用于保存该任务在内核态运行的栈信息。
inner:包含该任务的动态状态信息,用于存储在运行中可能变化的内容,如内存空间、任务上下文、进程树信息等。

### TaskControlBlockInner

TaskControlBlockInner 是 TaskControlBlock 的内部状态结构体,用于存储运行期间动态变化的内容,如任务的上下文、内存管理、父子关系等。每个 TaskControlBlockInner 都包含以下字段:

trap_cx_ppn:存储陷入上下文(Trap Context)的物理页号,用于保存用户态的CPU上下文。
base_size:应用程序的基本大小,用于约束任务在内存中的地址空间。
task_cx:任务上下文,表示当前任务的 CPU 状态。
task_status:当前任务的状态(如 Ready、Running、Zombie)。
memory_set:用于管理该任务的地址空间。
parent 和 children:当前任务的父子进程关系。
exit_code:任务退出时的状态码。
heap_bottom 和 program_brk:用于管理堆内存的范围。

### TaskManager

负责调度所有准备好运行的Task,它维护了一个 ready_queue 队列,包含了所有准备好运行的任务的 TaskControlBlock,从中取出任务并将其交给调度器:

ready_queue:一个队列,存储处于“Ready”状态的任务。

add 和 fetch:add 将任务添加到 ready_queue 中,fetch 从队列中取出任务进行调度。

# 文件系统

## 目录项

目录项

对于文件而言,它的内容在文件系统或内核看来没有任何既定的格式,只是一个字节序列。目录的内容却需要遵从一种特殊的格式,它可以看成一个目录项的序列,每个目录项都是一个二元组,包括目录下文件的文件名和索引节点编号。

```rust
// easy-fs/src/layout.rs

const NAME_LENGTH_LIMIT: usize = 27;

#[repr(C)]
pub struct DirEntry {
name: [u8; NAME_LENGTH_LIMIT + 1],
inode_number: u32,
}

pub const DIRENT_SZ: usize = 32;

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
}
```

在从目录中读取目录项,或将目录项写入目录时,需要将目录项转化为缓冲区(即字节切片)的形式来符合 read_at OR write_at 接口的要求

```rust
// easy-fs/src/layout.rs

impl DirEntry {
pub fn as_bytes(&self) -> &[u8] {
unsafe {
core::slice::from_raw_parts(
self as *const _ as usize as *const u8,
DIRENT_SZ,
)
}
}
pub fn as_bytes_mut(&mut self) -> &mut [u8] {
unsafe {
core::slice::from_raw_parts_mut(
self as *mut _ as usize as *mut u8,
DIRENT_SZ,
)
}
}
}
```

0 comments on commit bb3ea76

Please sign in to comment.