0%

Rust学习感悟

学习Rust语言可以是一个既挑战又充满成就感的过程。以下是一些常见的学习Rust的感悟:

  1. 安全性:Rust的设计哲学之一是内存安全,它通过所有权、借用和生命周期的概念来保证。学习这些概念可能会有些复杂,但一旦理解,它们会极大地减少内存错误,比如空指针解引用和数据竞争。
  2. 性能:Rust提供了与C/C++相媲美的性能,因为它允许直接的内存操作和避免运行时垃圾回收。理解这一点可以激励你更深入地学习如何编写高效的代码。
  3. 并发编程:Rust的类型系统和所有权模型使得编写无数据竞争的并发代码变得容易。这种安全性在其他语言中通常是通过运行时检查实现的,这可能会影响性能。
  4. 编译时检查:Rust的编译器非常严格,它会在编译时捕获许多潜在的错误。这可能会让初学者感到挫败,但长远来看,它有助于提高代码质量和减少运行时错误。
  5. 学习曲线:Rust有一个陡峭的学习曲线,特别是对于那些习惯于垃圾回收和动态类型语言的开发者。然而,一旦习惯了Rust的思维方式,你会发现它提供了一种更清晰、更可控的编程方式。
  6. 社区和文档:Rust社区以其友好和乐于助人而闻名,官方文档也非常全面和易于理解。这些都是学习新语言时的重要支持。
  7. 工具链:Rust有一个强大的包管理器Cargo,它简化了依赖管理和构建过程。学习如何使用Cargo可以提高开发效率。
  8. 所有权和借用:这是Rust中最具挑战性的概念之一,但也是其核心特性。理解所有权如何工作,以及如何通过借用和生命周期来管理资源,是掌握Rust的关键。
  9. 错误处理:Rust使用ResultOption类型来处理可能失败的操作,这鼓励开发者在代码中显式地处理错误情况,而不是依赖于异常。
  10. 泛型和trait:Rust的泛型和trait提供了强大的抽象能力,允许编写灵活且可重用的代码。
  11. 宏系统:Rust的宏系统非常强大,允许在编译时执行复杂的代码生成。这为元编程提供了强大的工具,但同时也增加了学习的复杂性。
  12. 生态系统:随着Rust生态系统的成熟,越来越多的库和工具被开发出来,这使得Rust在各种领域,如Web开发、系统编程、嵌入式开发等,都变得更加实用。

学习Rust是一个不断进步的过程,随着经验的积累,你会发现自己对语言的掌握越来越深入,同时也能够欣赏到Rust在系统编程领域所带来的独特优势。

rCore学习感想

学习是一个不断探索和发现的过程,它不仅涉及到知识的积累,还包括技能的提升和思维的拓展。最近,我深入学习了 Rust 语言和 rCore 操作系统,这段经历让我对编程和计算机科学有了更深刻的理解。Rust 语言的安全性和性能给我留下了深刻的印象。通过学习 Rust,我学会了如何利用所有权、借用和生命周期这些核心特性来编写既安全又高效的代码。这些概念在一开始可能会让人感到困惑,但随着实践的深入,我开始欣赏它们在防止内存错误和提高代码质量方面的强大能力。rCore 项目让我有机会将 Rust 的理论应用到实际的操作系统开发中。这个过程不仅加深了我对 Rust 语言的理解,还让我对操作系统的工作原理有了更直观的认识。从内存管理到进程调度,每一个组件的实现都是对 Rust 能力的一次考验,也是对我解决问题能力的一次提升。学习过程中遇到的挑战也促使我不断寻找解决方案,这不仅锻炼了我的问题解决能力,也增强了我的自学能力。每当我解决一个难题,那种成就感都是无与伦比的。我意识到学习是一个永无止境的旅程。随着技术的不断发展,总有新的知识等待我去探索。我对 Rust 和 rCore 的学习只是一个开始,我期待着在未来的学习和工作中,将这些知识应用到更广泛的领域中,继续成长和进步。

Rustlings

我使用过一段时间 Rust,但是已经有大约一年时间没使用了,Rustlings 的练习让我重新找回对 Rust 的熟悉感。Rustlings 前面的一些部分都是通过魔改代码,让编译通过并通过测试,从而理解 Rust 的特性。因为魔改的地方就是教学的地方,所以通过魔改少量代码的方式教学,我认为是非常高效的,而 watch 功能则更快捷方便连续地学习,就跟闯关一行。

相比于 Rust-by-Example,Rustlings 更偏向于实践,更方便开发者在了解 C/C++ 等高级语言的基础上,了解 Rust 的内存安全特性,包括所有权、生命周期等机制,迁移到 Rust。Rust 更强大的枚举、元组等类型以及支持更完善的标准库,更方便开发者专注于程序算法本身,而不是对程序的具体过程操心。Rust 的迭代器抽象还能够方便的提供函数式编程,使用一行语句就完成对数组全部操作,在数学上更美观,同时应该也更方便编译器进行优化。

Rustlings 最难的部分应该是一些算法的实现。正如 Rust 的所有权机制,通常对象之间有树形的嵌套关系,而描述双向链表、图这种数据结构则并不适合使用这种关系;其中有一个练习是二叉排序树,但是不要求实现平衡。如果要实现平衡树则要考虑到树的旋转等操作,在这种所有权机制下则更难实现。而对于图,则只能对于整个图整体分配存储、整体管理,最后很容易写出 C 风格的代码,或者使用了大量的 unsafe( algorithm1 的单链表合并中就有大量 unsafe)。我也尝试过使用 Rust 练习 AtCoder,体验堪称坐牢,写 Rust 就是和编译器作斗争。所以使用 Rust 完成漂亮的算法类代码,是一门深刻的艺术。

本人算是对操作系统比较熟悉,之前做过 xv6-riscv 实验,这次体验了 Rust 的操作系统试验。

ch3

ch3 可以说是 rCore 的入门题,帮助上手 rCore 的代码以及 rust 系统编程,还帮助熟悉 risc-v 的特权级指令。

ch4

ch4 主要是熟悉了操作系统的内存管理,从此,用户的地址空间和内核的地址空间隔离开,同时开启了页表。 ch4 实验重点是在内核和用户空间传输数据,因为内核是恒等映射,而用户空间开启了分页。如果二者没有共用页表,那么就需要手动模拟地址转换,并且还需要按照分页去写内存,因为一次返回的数据可能在不同的页中。此外还实现了简单的空间分配和管理,实现了简单的 mmap 和 munmap。

ch5

ch5 主要实现的进程管理和进程调度。把 fork 和 execve 融合魔改出来一个 spawn 能加深对 fork 和 execve 的印象。

ch6

ch6 是完善 easy-fs 的功能,但是我认为 easy-fs 没有解析路径、构建树形目录结构不能让学生了解到一个功能比较正常的文件系统的样子,而在此过早将 easy-fs 与其它内核模块解耦合也带来了增加功能和 debug 上的困难。

ch8

ch8 要求实现死锁检测算法,但是传统的银行家算法要求预先知道各个线程需要的资源,并且没有考虑线程因等待资源而阻塞的情况,这与现实的操作系统有出入。在现实中,可以认为阻塞的线程所需要资源为它所需的全部资源,将检测的申请资源调用后的状态作为最终状态,只需要检测此次申请是否可能死锁。

总结

对 rCore 的学习与实践更深入了解了操作系统的基本原理,也锻炼了 rust 代码能力。

前言

我是来自于海南大学密码科学与技术专业的本科生,对于计算机体系结构方向很感兴趣,但由于学校条件所限,不能满足个人学习需求,所以来参加这个课程

在之前,我一直对一些东西比较好奇

比如

  1. 盗版激活软件所说的模拟硬件激活
  2. 计算机取证中,DMA技术如何用来解密BitLocker的,DMA外挂是如何实现的(当然,这个漏洞已经被修复),(我觉得,作为一个本科生,不应该只知道表面,而是更深入的了解,正所谓,知其然,知其所以然)
  3. 内核的内存管理
  4. 并发
  5. 动态链接的一些细节

等问题.

所以, 我报名参加了这次的操作系统训练营.

第一阶段

我C写的比较多,而且习惯确实不太好,所以在这阶段,也是磕磕绊绊,尤其是在所有权机制上,给了我“不灵活”的印象,我觉得也不算坏事吧,通过尽可能多的约束,把不安全的代码存在的范围尽可能缩小,方便发现问题和解决。

此外 rust的cargo非常好用。

还有,教程使用的rustlings有点旧?我用着是有点不太方便的,貌似没法很好的调试,我依赖于rustings run name这种方式来debug,但是每次都要写那么多,显然有点麻烦

我通过把如下内容加入 .bashrc,简化了这个操作

alias check=’rustlings run’

alias hint=’rustlings hint’

这样,就可以通过check name的方式检查题目了

第二阶段

Lab 1

一个简单的多任务系统,我曾经参加过海南大学南海鲨战队的电控培训,看过ucosii的代码,所以这点对我来说还好

Lab 2

这个实验启用了分页机制, 在这次实验之前,看过xv6 内存管理部分的代码,所以还好(这在之前,逆向程序时的内存地址,让我有不少困惑)

Lab 3

进程: 在这地方,我觉得问题不大

Lab 4

文件系统: 这里问题挺多的,做的有点糊涂,不过不管怎么样,能用

Lab 5

多线程: 了解了线程与并发的相关知识,还有银行家算法

总结

通过这门课程我学习到了很多操作系统的基础知识, 这个学期补补计算机组成原理,算法。rust也不熟练,不优雅,还得多练,下个学期二战。

我在此感谢此训练营的组织者和助教们,你们为很多人打开了一个新的天地。

前言

我自己之前参加过ysyx,有一点操作系统的相关基础知识和riscv指令集基础,pa4后面还没做完,对于很多操作系统基础概念理解还是不够深刻,通过rcore的学习也是补上了这一部分。操作系统作为最和硬件联系紧密的软件,特权、异常、中断等概念的引入,让cpu具有了安全的连续不停运行各种各样程序的能力,这非常美妙😄,这也是我学习操作系统的目的。

个人感悟

通过近一个月的学习,对于操作系统的相关概念和rust这门编程语言有了更深入的了解,之前对于操作系统也只是停留在很基础的部分,对于操作系统的基础概念的认识也是非常模糊,也没有上过操作系统的相关课程,通过这次的学习,也是对操作系统的基础概念(进程管理,地址空间,文件系统,并发等等)有了基础的理解。
在rcore的实验课程中,最让我印象深刻的就是虚拟地址部分,由于之前接触的操作系统全部都是nommu类型,直接访问物理地址的概念非常不容易被打破,并且ch4也是一个门槛,需要非常深入的理解rcore的整体代码框架,对虚拟地址的映射,软硬件协同操作,多级页表,地址空间的概念理解了很久,在不断查找资料,不断理解的情况下,才对虚拟地址的概念有了很浅显的理解,把任务完成。
在完成任务的过程中,其实rcore的整体框架非常完善,作业的部分也就是一些扩展功能,整体还是对rcore的实现的理解,在完成任务时,也是对已有实现进行模仿,理解整体框架,之后调用已有函数或自己根据数据结构实现函数,实现功能。

rCore 总结

这个阶段大约花了3周左右的时间,更多是对OS概念抽象risc-vCPU架构的一些了解。跟rust的关系在我看来不算特别大。
核心的找地址、上下文切换都是c-like/asm实现的,当然这部分代码占比很小很小。这里面内存frame等等几个地方,充分利用了RAII机制,
大大减少了内存释放的心智负担,感觉是rust语言优于传统C的典型佐证。

这个阶段花时间较久是在ch8银行家/死锁检测算法上,因为概念的不熟悉,套用模型产生的死胡同。一直没有找到各个线程对于初始化资源的需求表,所以也就套不进银行家。中间放弃准备用循环图检测,发现不支持数量(权重)。最后看了一下向勇老师的在线课《操作系统-20.4 死锁检测》,找到了灵感,按照这个思路写算法,大概又花了几个小时调试通过。

下面按照实验内容分章节进行总结。

Read more »

前言

本人2024年8月份的时候,工作需要一部分图像处理代码,最后选择用Rust实现,并通过krpc-rust跟java完成调用。后面发现rust的一些理念尤其是RAII确实挺不错,就关注起来了。

后来在9月份的上海Rust大会上,了解到清华rCore项目训练营,感觉挺有意思的,既能学习os又能学习rust,就种草了

Rustlings 总结

基础阶段相对简单,却也兴奋,熟悉了各种rust语言的特征。比如下阶段会频繁用到的extern "C"#[no_mangle]
算法阶段,通过unsafe处理指针、链表,对unsafe有了一些熟悉,尤其是类型转换。还有就是熟悉了堆的实现,加上rust的运算符重载机制,可以灵活的在大小堆之间切换。
另外整个过程中发现rust并没有传说中的所谓曲线陡峭,秉着实用主义的原则,先进行一下元学习,也即了解这门语言的层次结构,函数传值、内存模型等大的层面,其余部分,用多少学多少,在出现问题的时候知道去研究哪部分就可以了。
所以我觉得学习也像小马过河,重在实践。

本人自己其实也在用 c 重写 linux 0.11 的内容,但越往后走,越觉得自己在闭门造车,担心最后做出来的东西已经与目前主流方向脱轨,
偶然的机会接触到 opencamp 这个社区,了解到这个社区现在在做什么。发现与自己所需要的东西强吻合,故毫不犹豫的加入了这次活动。
本人是一名软件工程师,主要语言是c和go,学习rust这门相对比较新的语言其实还是有点吃力。尤其是rust语言中的一些语法糖以及trait,更是
让我这个初学者苦不堪言。由此,我把学习过程记录到了这儿。

rust学习记录

第一阶段,让我对rust的基础语法有了了解,发现rust编译器一直在思考c工程师常考虑的两个事情:这个内存什么时候被申请,这个内存什么时候要释放。
弄清楚这个之后,使用rust就变得不那么困难了,尽管学习完这个课程后还不能写出很优美的rust语法糖,但至少可以保证代码的正确性了。

本人是一名网络软件工程师,从事高性能网络相关的工作,从而有机会接触到内核相关的东西。但工作中学到的东西往往犹如隔靴搔痒,不得痛快。
于是自己也尝试在用c 重写linux 0.11。这里推下我个人的仓库,目前进行到inode操作,与rcore的进度其实高度吻合。

JOS

这个项目还是基于i386架构,在做的过程中让我感受到了与现实世界的差距。也看到过一句话,一个人可以走的快,但一群人可以走的更远。
为了更远的目标以及更贴近生产环境,我参加学习了这个基于risc-v架构rcore的几个基础课程。

虚拟内存管理

个人认为操作系统中最复杂的就是虚拟内存管理,在rcore的课程中,学习了 risc-v 的页表结构,pa 到 va的转换过程,以及va和pa的管理。

调度

这里主要讲了怎么进行上下文的切换(两个kernel stack) 的切换,特权级的切换(S to U/ U to S)。还实现了一个简单的stride调度算法。

并发

学到了怎么创建线程,以及线程间的资源管理逻辑。实现了mutex,信号量,con_var,还实现了一个简单的死锁检测。

文件系统

学了 easy-fs 的文件管理方式,以及通过高速缓冲区实现对磁盘的访问。

Rustlings

关于第一阶段的Rustlings,还是花了很多时间去学习Rust。一开始是直接去看《Rust程序设计语言》,看了大概大半个月吧,把一些较为简单的概念和程序过了一遍。也是第一次接触这类内存安全类语言,第一次看到所有权,引用的时候还有点畏惧,对于没怎么深入学习过C++的人来说学起来还是有些吃力的。后面又去看了《Rust圣经》,发现有趣多了,提供了很多代码案例,很有意思。最后也跟着写了一个rust小项目minigrep。rustlings也是边学边查文档边做,做起来很有意思很有成就感。

rcore实验

rcore批处理系统编译逻辑

  • link.ld链接脚本将程序分成.text、.rodata、.data、.bss。
  • build.py会将app目录下的bin文件进行编译,将程序的text段加载到以0x8040000开始的用来存放app代码的内存空间,且规定每块app空间为0x2000。
  • build.rs会遍历user目录下的build文件夹中刚才通过objcopy生成的bin文件,然后生成对应的link_app.S。其实就是将app下的bin文件进行装载,在每个app的内存空间开始和结尾设置标号,并暴露给os以供调用。

页表机制

单页表:一块地址空间分为用户虚拟地址和内核虚拟地址,内核虚拟地址映射到内核物理地址

单页表会出现熔断漏洞

比如在用户虚拟空间中有一段代码需要访问内核数据空间的页面,因为cpu流水线机制,数据可能已经被放在cache中。但如果这时候我们取值失败了但是由于已经把数据放在了cache中。下一次我们从用户态直接访问这几个页面的时候,总有那么一个页面访问的速度远比其他的页面快。

双页表:分为用户地址空间和内核地址空间,用户地址空间又分内核态代码和用户态代码。

那么当我们在用户态访问内核数据时,其实是不知道数据放在哪的,这样就可以避免熔断漏洞。

exec

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
/// Load a new elf to replace the original application address space and start execution
pub fn exec(&self, elf_data: &[u8]) {
// memory_set with elf program headers/trampoline/trap context/user stack
let (memory_set, user_sp, entry_point) = MemorySet::from_elf(elf_data);
let trap_cx_ppn = memory_set
.translate(VirtAddr::from(TRAP_CONTEXT_BASE).into())
.unwrap()
.ppn();

// **** access current TCB exclusively
let mut inner = self.inner_exclusive_access();
// substitute memory_set
inner.memory_set = memory_set;
// update trap_cx ppn
inner.trap_cx_ppn = trap_cx_ppn;
// initialize base_size
inner.base_size = user_sp;
// initialize trap_cx
let trap_cx = inner.get_trap_cx();
*trap_cx = TrapContext::app_init_context(
entry_point,
user_sp,
KERNEL_SPACE.exclusive_access().token(),
self.kernel_stack.get_top(),
trap_handler as usize,
);
// **** release inner automatically
}

步骤 1:创建新的 MemorySet

1
let (memory_set, user_sp, entry_point) = MemorySet::from_elf(elf_data);
  • 调用 MemorySet::from_elf 解析传入的 ELF 数据,并创建一个新的 MemorySet,即新的地址空间。
  • 该函数返回以下三个值:
    • memory_set:表示该进程的新内存映射集合,包含代码段、数据段、用户栈等信息。
    • user_sp:新用户栈的栈顶地址。
    • entry_point:新程序的入口地址,表示从此处开始执行新的 ELF 程序。

步骤 2:获取新的 trap_cx_ppn

1
2
3
4
let trap_cx_ppn = memory_set
.translate(VirtAddr::from(TRAP_CONTEXT_BASE).into())
.unwrap()
.ppn();
  • 通过 translate 方法,将 TRAP_CONTEXT_BASE 这个虚拟地址转换为物理页号(trap_cx_ppn)。
  • trap_cx_ppn 表示陷入上下文(TrapContext)所在的物理页号,用于进程的系统调用或异常处理。

步骤 3:独占访问当前进程控制块(TCB)

1
let mut inner = self.inner_exclusive_access();
  • 通过 inner_exclusive_access 方法独占访问当前进程的 TaskControlBlockInner 结构体,确保在以下步骤中可以对进程的内部状态进行修改。

步骤 4:替换 MemorySet

1
inner.memory_set = memory_set;
  • 将当前进程的 memory_set 替换为新创建的 memory_set,这样新加载的 ELF 程序就成为该进程的地址空间。
  • 这一步实现了对原应用程序地址空间的替换。

步骤 5:更新 trap_cx_ppn

1
inner.trap_cx_ppn = trap_cx_ppn;
  • 更新 trap_cx_ppn 字段,设置新的 trap_cx_ppn,确保进程的陷入上下文指针正确指向新的物理页。

步骤 6:初始化 base_size

1
inner.base_size = user_sp;
  • 更新 base_size 字段为新的用户栈顶地址 user_sp
  • base_size 用于保存用户栈的初始栈顶,便于栈空间管理。

步骤 7:初始化 trap_cx

1
2
3
4
5
6
7
8
let trap_cx = inner.get_trap_cx();
*trap_cx = TrapContext::app_init_context(
entry_point,
user_sp,
KERNEL_SPACE.exclusive_access().token(),
self.kernel_stack.get_top(),
trap_handler as usize,
);
  • 调用 get_trap_cx 获取当前进程的陷入上下文指针。

  • 使用

    1
    TrapContext::app_init_context

    函数重新初始化陷入上下文,设置新程序的执行信息:

    • entry_point:新程序的入口地址。
    • user_sp:用户栈顶地址。
    • KERNEL_SPACE.exclusive_access().token():内核空间的访问令牌,确保正确的权限。
    • self.kernel_stack.get_top():内核栈的栈顶地址,用于中断或系统调用时的上下文切换。
    • trap_handler as usize:陷入处理函数的地址,用于异常处理。

结尾:释放 inner

inner 独占访问结束时,inner_exclusive_access() 产生的独占访问会自动释放,允许其他任务对该进程进行访问。

总结

exec 方法执行以下步骤来加载和执行一个新的 ELF 程序:

  1. 从 ELF 数据中构建新的 MemorySet、用户栈顶地址、程序入口点。
  2. 获取并设置新的陷入上下文物理页号 trap_cx_ppn
  3. 独占访问当前进程控制块,并逐步替换内存集、更新陷入上下文等信息。
  4. 重新初始化陷入上下文,确保该进程从新的程序入口执行。

rcore调度策略

TaskManager任务管理器管理着一个任务就绪队列(先进先出策略),os初始化过后会在run_tasks中无限循环,取出任务及任务保存的寄存器task_cx,然后通过__switch切换idle_tasknext_task(实际就是task_cx中寄存器的切换),如果没有任务或当前任务释放控制权则会调用schedule切换到idle_task

进程间通信

管道(Pipe)

可表示为两个文件描述符加一段内核空间中的内存

1
2
// 传入数组,转换成管道的读写端的文件描叙符
int pipe(int pipefd[2]);

通过操作文件描述符来分别操作读写端进行进程间通信

  • 如何实现shell中管道符“|”功能

可以fork两个子进程,pid1的执行流可以使用dup2函数将stdout重定向到pipefd[1] (写端),并关闭管道的读写端,执行第一条命令

pid2的执行流使用dup2函数将stdin重定向到pipefd[0] (读端),关闭管道的读写端,执行第二条命令

最后父进程关闭读写端并wait两个子进程

匿名管道:只能是具有血缘关系的进程之间通信;它只能实现一个进程写另一个进程读,而如果需要两者同时进行时,就得重新打开一个管道。

为了使任意两个进程之间能够通信,就提出了命名管道(named pipe 或 FIFO)。
1、与管道的区别:提供了一个路径名与之关联,以FIFO文件的形式存储于文件系统中,能够实现任何两个进程之间通信。而匿名管道对于文件系统是不可见的,它仅限于在父子进程之间的通信。
2、FIFO是一个设备文件,在文件系统中以文件名的形式存在,因此即使进程与创建FIFO的进程不存在血缘关系也依然可以通信,前提是可以访问该路径。
3、FIFO(first input first output)总是遵循先进先出的原则,即第一个进来的数据会第一个被读走。

消息队列

信号(Signal)

在rCore中,当trap发生进入trap_handler函数,其中会调用handle_signals,循环调用check_pending_signals检测进程结构体中的成员来判断是否有signal到来,如果是内核信号,则在内核执行处理函数call_kernel_signal_handler(signal),如果是用户信号则需要返回用户态执行处理函数call_user_signal_handler(sig, signal)