解析`WebGPU`、`Virtio1.2`规范,以及`WGPU`、`Vortex`源码,探索异步操作系统与内核态GPU资源管理的结合方案。
0%
GPU GPGPU GPGPGPU!
2025 oscamp blog of hailong
Posted on
Edited on
第一阶段 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 | fn sys_mmap( |
参数addr为0时要特别注意,需要寻找一空间,还有地址和size的对齐。返回值,当开始时认为0是成功,为负时,返回失败原因,后来再三确认失败返回0,成功返回地址,isize作为地址返回,有点不符合直觉?
Next
期待第四阶段
2025春夏季开源操作系统训练营第四阶段总结报告
前三阶段总结
主要收获
在第四阶段中,更多的时间留给了自由探索.虽然起初缺少具体的目标有些令人摸不着头脑,不过跟随老师的引导,也一步步确立了整个阶段的目标:基于 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 和用来管理操作生命周期的 Driver.Uring 的实现借鉴了 io_uring 的做法^4,但结合 Rust 的特性做了一些简化.比如,io_uring 支持 IOSQE_IO_LINK 来要求响应侧顺序处理请求.而在 Rust 中,每个异步请求都被封装为 Future,故可以利用 .await 来在请求侧实现顺序请求.Driver 的实现则借鉴了 tokio-uring 和 ringbahn.但相比后两者,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年春夏开源操作系统训练营四阶段总结-noah-低侵入式的异步协程运行时
序言
非常高兴能参加到开源操作系统训练营第四阶段的学习,跟大家一起进步。经历完这四个阶段,自己有非常大的收获,让我理解了操作系统内部的运行机制,通过组件化的管理来实现更现代化的操作系统,最终在操作系统中支持异步机制提高操作系统的性能。
在每周的学习过程中,非常感谢向勇老师给与了很多的指导和鼓励,成为了我前进的支柱。同时也要感谢周积萍学长给与了很多帮助和支持以及建设性的意见,让我遇到困难时不迷茫。在第四阶段的过程中,也从其他同学那学到了很多,能便捷的获取到学习资料和代码,当有疑惑了也有人能够理解你,跟你一起深入到技术中进行讨论,那种感觉真是舒服。
每周工作
对于一个程序员来说,时间总是不够的,在四周时间里,我主要做了以下几个方面的工作:
第一周 - 回顾
当我第一周(2025-06-01)加入训练营的时候,发现去年的学长大佬们已经早早的开始了自己的课题。我就从基础慢慢开始,这周主要是把文档中的例会视频都看了一遍,基本了解了协程异步的作用以及目前是如何把异步应用到操作系统内核当中去的。
第二周 - 定目标
在看例会视频的时候,我发现有学长讨论 rust 异步的函数着色问题,碰巧我之前在写 rust 的时候也遇到了这个问题,很多时候一套逻辑代码,要分别实现一个同步版本一个异步版本,从一些学长的代码中看确实是分开写的,比如赵方亮学长的仓库,在 2025-06-07 例会讨论中,有同学在参加 “大学生操作系统大赛” 的时候也遇到了此问题,最近在学习 zig 这门语言,说是能解决这个问题,我便想借着这个机会深入研究下。也是对于函数着色问题的一些自己的尝试,于是便确立了如下目标:
- 长期目标:实现低侵入式的异步协程框架,服务于操作系统内核
- 本期目标:实现简单的异步协程运行时 (zig)
后续在调研的过程中,发现 rust 的异步机制,是基于 Future 来实现的,这是一种无栈协程,跟我之前理解的 Go 语言的那种有栈协程还不一样。
第三周 - 学习和实验
后续的两个周,主要是学习文档中的异步协程资料,编写实验代码,验证自己的想法。
学习内容
- C:实现一个迷你无栈协程框架——Minico - 理解什么是无栈协程
- stack-less rust coroutine 100-loc - 理解 rust 无栈协程
- Rust 圣经 - 手写 Future Runtime - 理解 Waker 机制
- 200 行代码绿色线程 - 如何从零实现协程(绿色线程)
实验部分
基于上面的学习的内容,进行了如下实验来验证想法:
- 实现在 zig 中调用 rust 封装的绿色线程任务
- 实现基于 Future 的异步运行时 (用 zig 语言)
- 用 asm + zig 实现有栈协程切换机制
第四周 - 代码结合
- 异步协程运行时 xasync 框架代码编写
- 基本完成跑通简单测例
- 同时初步解决了函数着色问题
完成上述四周的工作后,基本实现我在开源操作系统训练营本阶段的目标,符合预期。
xasync 异步协程运行时
使用者角度
我在设计 xasync 异步协程的时候,借鉴了 zig 协程 的设计思路,感觉 zig 协程更容易让使用者理解和减轻负担,那么从使用者的角度出发,什么样的协程用起来才是最舒服的,我认为尽量保持一套代码,只通过一些简单的标记就可以实现同步和异步的切换,是更加友好的协程框架实现方式。下面是我理解的伪代码。
1 | var is_async = true // 如果关闭后,底层会走阻塞逻辑 |
通过上面的注释,可以仔细看下调用流程,这是我个人期望的协程框架使用方式的理解。
架构设计

上面是架构设计图,分为前后两部分把 有栈协程 和 无栈协程 结合起来,其中红线理解为前进 蓝线理解为返回,比方说协程切换的前进返回、Future poll 前进和状态返回、协程调度的前进和唤醒的返回等。
目前图中描述的是有三个协程(绿色线程)在需要的场景下不断让出执行权,在异步任务结束后能随时切换到具体的任务上继续执行。这种机制在需要等待返回结果的情况下尤为重要。后续会优化成协程池方便使用。
下面从测例的角度简单剖析下实现代码和原理。
Future
测例代码
1 |
|
上面的代码是把一个 Counter 计数器,改成了异步机制,当 num > max 的时候才会终止运行。在实现的时候利用 zig uinon(enum) 的特性,尽量做到了零成本抽象。
支持组合
还支持了 Then 组合操作,因为在封装 Future 代码的时候可能要把原有的阻塞代码拆成多个 Future 逐步执行。代码如下:
1 | test "counter-chain-counter" { |
后续还会在 Future 上进行扩展支持 Join 等更多组合操作。
Executor
Executor 中有两个队列:
- ready_queue: std.ArrayList(*Future) - 调度队列,供调用者放入 Future 任务
- futs: std.ArrayList(*Future) - 执行队列,实际调度器处理的 Future 任务
Future 先是进入到调度队列,如果调度开始执行后,会从调度队列取出任务放入执行队列,这时候执行队列中可能还有其他未完成的任务,当 Future 结束后会从执行队列中移除,如果执行队列中的所有任务都是等待状态,则 Executor 处于 idle 状态,等待 event_loop 唤醒,具体使用方式在上面的测例代码中已体现。
目前调度策略比较简单,而且没有经过任何优化,后续会不断完善。
Coroutine(绿色线程)
目前已经支持协程间的切换,下面的代码是非对称协程的实现方式:
1 | var base_coro: Coroutine = undefined; |
这个测试就是用协程的方式去执行 addCount 所在的 count_coro 协程,在 addCount 中也可以随时切换调用者协程 base_coro,执行原有逻辑。
还支持了函数参数的传递,在上下文切换的时候,不是两个函数的切换,是通过一个中间函数 call,它会根据汇编传过来的参数指针地址,转换成具体的 *Coroutine,再从其中拿出 func_ptr 和 args_ptr,就相当于中间层转发了一下。从下面的代码看目前参数类型都是定死的,有点牵强,目前够用。
1 | fn call(coro_ptr_int: u64) void { |
Eventloop
事件响应机制也就是 eventloop (reactor 模型),其实是所有异步协程实现的底层支持,我甚至认为就算不用异步,只用事件机制和回调的方式也能做到高性能。这一部分在本期训练营并没有深入的去学习,目前只是实现了一个大概。如果这层封装好了,做成一层统一的抽象去处理 epoll、io_uring、iocp、kqueue 以及中断信号量等,也将会有很大的收获,给自己挖个坑,明年把这部分填上。
eventloop 的核心代码就是用一个循环,不停的调用系统需要等待的函数,等待系统给出响应,这里用的是 epoll_wait,这些系统提供的函数其实在操作系统里面都有自己的实现,一般性能都比较高,而且可以阻塞也可以非阻塞。当系统给出响应后,再触发回调去唤醒 Executor。
1 | pub fn poll(self: *Self, timeout_ms: i32) !usize { |
整体组合 xasync
把上面各部分组合起来,看看能不能达到预期效果。
Timer
这部分注册一个 TimerHandle 到 event loop 当中。
1 | const Timer = struct { |
Sleep
这部分把 Timer 包装成 Future
1 | fn sleep(nanoseconds: u64) void { |
main
整合完毕后对于使用者来说,代码如下:
1 | xasync(delay); |
运行效果
不等待完成
1 | delay comes in |
等待完成
1 | delay comes in |
总结
从目前执行效果和 API 的调用方式看符合预期,基本达成了本期的目标:实现简单的异步协程运行时 (zig),按照这种方式解决函数着色问题是有希望的。
虽然本期目标基本达成,但是中间学习的过程中还是有很多技术细节没有完全搞懂,有些学习资料没有完全看完,后续还要继续努力。
后续规划
- 参数和返回值的类型支持且能自动推导
- 支持线程池 thread pool
- eventloop 完善
- 是否后台调度支持用户配置 - 现在需要改代码来实现
- 支持 rust 调用
- 封装 asyncio
- 集成到 arceos/rcore 中
- 性能对比测试
答疑和思考
为什么要用 zig 写
- 没有任何原因,个人偏好,peace & love.
实现代码在哪里
2025春夏开源操作系统训练营4阶段总结-明扬
Posted on
Edited on
中断驱动的协程异步网络 - 在ArceOS中实现
一、背景
时间过的飞快,转眼间,训练营四阶段已经迎来尾声。
我由于去年已经参加过一届训练营,所以本次我提前一个月晋级到了四阶段,与向勇老师确定了我的课题方向:“中断驱动的协程异步网络”。
在后续周会的沟通中,向老师也给了我更具挑战性的目标:“在VisionFive2开发板上验证我的工作”。
二、历程
在四阶段,我主要完成了以下几个里程碑:
Rust异步运行时
因为我之前曾经在工作中编写过面向 WASM 平台的 Rust 异步运行时,所以第一个里程碑并没有花掉我太多时间。
为了给自己增加一些挑战,我还额外实现了常见的异步运行时辅助数据结构与函数,如:mutex,spawn, block_on, join 等。
异步版本的网络操作
在完成 Rust 异步运行时后,我便开始着手实现异步版本的网络API。
我为所有网络操作中包含block_on的函数都编写了异步版本,并实现了网络操作的异步化。其中包括:
socket.recv_async()socket.send_async()socket.accept_async()socket.connect_async()
PLIC中断驱动的Future唤醒
实现以上异步网络操作后,网络异步仍然是以Poll驱动的,而不是以真实的物理世界事件驱动的。
为了实现真正的异步网络,我需要实现一个中断驱动的Future唤醒机制。
在 RISC-V 架构中,PLIC 是中断控制器,可以用于实现中断驱动的Future唤醒机制。
我为 ArceOS 移植了 PLIC 驱动,并实现了 VirtIO-Net 设备的 PLIC 中断驱动的 Future 唤醒机制。
将ArceOS移植到VisionFive2开发板
在完成以上工作后,距离训练营结束仍然有一个半月的时间。这次组会中,向老师给了我一个更具挑战性的目标:将 ArceOS 移植到 VisionFive2 开发板,并在 VisionFive2 开发板上验证我的工作。我也就正式开始了移植工作。
开发板板载了 u-boot 和 OpenSBI v1.2,我可以直接从 u-boot 启动 ArceOS。
幸好我手中还有 jTag 调试器,向老师也给我提供了其他前辈同学的移植经验。
感谢萧络元同学的代码仓库,我很快便完成了 ArceOS 的移植。
VF2 开发版与常见 RISC-V 开发版有些许不同,例如:
* S 态可用内存起点为 0x4000_0000 而不是 0x8000_0000
* OpenSBI v1.2 支持 SBI v1.0 规范,但未实现 Console Extension。所以日志打印时需要使用 Legacy Console API。
以及 u-boot 启动与 Qemu 启动也有些差异,现代镜像打包时更多采用 itb 格式,同时将 dtb 打包进 itb 中。
总的来说,移植工作并不算太难,但是当系统无法加载又没有任何日志输出的时候,还是需要些裸机的调试手段,例如直接通过汇编调用串口打印,或者 sbi call 字符打印来确定程序执行到了哪里,是否进入内核代码。
为VisionFile2启用中断驱动的异步网络
在完成 ArceOS 的移植后,我便开始着手为 VisionFive2 开发板编写网卡驱动,启用中断功能,复现我在 Qemu 中完成的功能。这是本次训练营中我遇到的最大挑战。
首先,启动真实的网卡设备异常复杂,需要按顺序依次启动不下10个时钟信号与复位信号。
然后,VF2 平台使用的网络设备为 dwmac-5.2,参考 Linux 内核中驱动实现时,其历史悠久,代码量庞大,支持功能多且复杂,兼容设备多,还需要兼容多个平台和历史版本,硬啃 Linux 内核驱动代码基本上不太现实。
在必要时候还需要配合 PHY 芯片的同步配置,与设备寄存器交互通过 MMIO 映射,而与 PHY 芯片交互则需要通过 MDIO 总线,Clause 22/45 协议。
同时还需要学习众多的 MMIO 寄存器偏移量,与每个 bit 对应的功能含义,了解 GMAC/DMA/MTL/PHY 相关寄存器。
也学习了 ring buffer 的实现,以及如何使用 ring buffer 实现网络数据收发。
最终我还是没能在 VF2 平台开启中断功能,根据与厂家工程师的交流,PHY 芯片中断引脚并未接入 PLIC 中断控制器,所以无法使用 PLIC 中断驱动网络功能。
遗憾未能亲眼见到自己的工作在真实硬件上运行,但是通过本阶段的学习,我还是学到了海量的知识。
三、总结
通过本次训练营,我弥补了我知识体系中的许多空白,包括:
- PLIC 中断控制器
- 网卡驱动
- U-Boot 运作原理
- 真实硬件是如何运作的
四、致谢
感谢向勇、陈渝老师,以及所有帮助过我的老师和同学。
2025 春夏季开源操作系统训练营 学习总结
Posted on
Edited on
2025 春夏季开源操作系统训练营 学习总结
1 | // 实验环境 |
第一阶段: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 | brew install qemu |
如果显示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 | brew install coreutils |
2. 学习心得
由于没有系统学习过操作系统的理论知识,学的比较慢,需要参考指导书和学习资源中的视频才慢慢掌握。虽然操作系统相关的内容学的不够扎实,但是对于 rust 的语法慢慢熟悉了起来。抱着学习 rust 的心态还学习到了操作系统的底层运行,很有收获。
第三阶段:项目基础阶段 - 组件化操作系统
- 尽管是第一次接触操作系统,在完成第二阶段以后,第三阶段给笔者的感觉不是很陌生(至少知道大概都在干什么)
- 课程视频和课件也很详细的给出了任务和学习目标,但是第三阶段的项目相比第二阶段要大很多
- 有时候不知道要去哪里干什么,只能通过给出的题目反向查找相关的内容。
1.遇到的问题和解决思路
- 在UniKernel部分, 笔者在 MacOS 下面完成了任务,到了宏内核的部分涉及到了交叉编译的部分,似乎 MacOS 变得复杂了起来。折腾了一段时间之后,还是在一台 Linux 服务器上用 docker 完成了后续的任务。
- 这是笔者第一次使用 docker,遇到不懂的就让 Chatgpt 来生成指令,但是还是遇到了很多问题:
- hub.docker.com 被墙,根本创建不了 Linux 容器, 后来找到 nvcr.io/nvidia/pytorch:25.05-py3 解决了问题: https://catalog.ngc.nvidia.com/orgs/nvidia/containers/pytorch
- 以为 root 之后就万事大吉,但是在运行容器的时候要加上
--privieged, 不然mount指令无法正确的找到目录的位置 - 运行容器的时候千万不要加 -rm, 不然停止容器的时候就永远地消失了
2. 学习心得
由于上述笔者遇到的问题,反反复复搭环境搭了 3 次, 对各种 Linux 指令,以及各种组件的作用有了更深的理解。相比第二阶段,第三阶段的练习难度反而有所下降,但是 ArceOS 本身的内容是相当多的,需要更加系统的学习。 希望能在第四阶段有所收获。
manchangfengxu
Posted on
Edited on
rust基础的总结
一.基本数据类型与所有权
所有权系统核心规则
- 移动语义(Move):
1
2
3let s1 = String::from("hello"); // 堆分配
let s2 = s1; // 所有权转移
// println!("{}", s1); // 错误!s1 已失效 - 借用规则:
- 任意时刻:一个可变引用 或 多个不可变引用
- 引用必须始终有效(悬垂指针禁止)
- 生命周期标注:
1
2
3fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}
Slice 类型
- 无所有权引用:
1
2
3
4let 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 | mod front_of_house { |
使用外部 Crate
1 | # Cargo.toml |
1 | // main.rs |
三. Option 与错误处理
Option 枚举
1 | enum Option<T> { |
Result<T, E> 错误处理
1 | fn read_file(path: &str) -> Result<String, io::Error> { |
错误处理最佳实践
- 优先使用
Result而非 panic - 使用
?操作符传播错误 - 自定义错误类型实现
std::error::Error
四. Trait 与泛型
Trait 定义与实现
1 | trait Summary { |
泛型函数
1 | fn largest<T: PartialOrd>(list: &[T]) -> &T { |
Trait Bound 语法糖
1 | // 以下两种写法等价 |
生命周期进阶
生命周期标注必要性:
- 结构体持有引用时必须显式标注生命周期,确保引用的有效性
- 方法实现中:
&self参数隐含&'a self生命周期- 返回值关联结构体生命周期(通过生命周期消除规则第三项)
- 遵循Rust生命周期消除三规则:
- 每个输入引用自动获得独立生命周期
- 单个输入引用时所有输出引用与其生命周期对齐
- 方法签名中
&self使输出引用与结构体生命周期对齐
错误:
1 | fn dangling_reference() -> &str { |
1 | struct ImportantExcerpt<'a> { |
五. 智能指针
常用智能指针对比
| 类型 | 所有权 | 线程安全 | 使用场景 |
|---|---|---|---|
Box<T> |
单一 | 是 | 堆分配、递归类型 |
Rc<T> |
共享 | 否 | 单线程引用计数 |
Arc<T> |
共享 | 是 | 多线程引用计数 |
RefCell<T> |
可变 | 否 | 运行时借用检查 |
使用示例
1 | // Box 用于递归类型 |
六.迭代器与闭包
闭包类型推断
1 | let add_one = |x| x + 1; // 类型自动推导 |
闭包捕获模式
| 捕获方式 | 关键字 | 所有权 |
|---|---|---|
| 不可变借用 | ` | |
| 可变借用 | ` | mut |
| 值捕获 | move |
转移 |
迭代器适配器
1 | let v = vec![1, 2, 3, 4]; |
自定义迭代器
1 | struct Counter { |
七.并发与异步编程
线程创建
1 | use std::thread; |
通道通信 (mpsc)
1 | use std::sync::mpsc; |
共享状态 (Mutex)
1 | use std::sync::{Arc, Mutex}; |
异步编程 (async/await)
1 | async fn fetch_data() -> Result<String, reqwest::Error> { |
八.常用集合类型
Vec 动态数组
1 | let mut v = Vec::with_capacity(10); |
HashMap<K, V> 哈希表
1 | use std::collections::HashMap; |
阶段二,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 | pub struct BlockCache { |
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 | #![cfg_attr(feature = "axstd", no_main)] // 若启用 axstd,不使用标准库的 main 入口 |
2. 依赖管理 Cargo.toml
1 | [dependencies] |
3. features 动态配置
- 作用:通过编译参数控制组件的启用,实现 “按需构建”。
- 示例:
- axstd 组件通过 feature = “axstd” 控制是否包含。
- 实验中默认启用 axstd,因此能使用 println!。
println!
通过更改ulib下axstd,macros文件中的println!
hashmap
1 | #[cfg(feature = "alloc")] |
暴露自己写的collections
1 | [dependencies.hashbrown] |
用了官方库的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 | #[cfg_attr(all(target_os = "none", not(test)), global_allocator)] |
任务数据结构 TaskInner
1 | struct TaskInner { |
协作式调度
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 | // axhal/src/platform/riscv64_qemu_virt/mod.rs |
块设备驱动:
Trait:BlockDriverOps
1 | trait BlockDriverOps { |
文件系统:
抽象
文件系统(FileSystem):如 FAT32、EXT4。
目录(Dir):存储文件 / 子目录元数据。
文件(File):存储具体数据,支持读写操作。
接口
1 | trait VfsOps { |
加载流程
块设备读取:通过 VirtIO Blk 驱动读取磁盘前 512 字节(引导扇区)。
解析 BPB:获取 FAT 表起始地址、簇大小等参数。
挂载文件系统:将 FAT32 的根目录挂载到 VFS 的 / 节点。
应用加载示例(U.8 实验)
1 | // 从 FAT32 文件系统加载应用程序 |
实验实现
寻找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 | // 关键修改:init_user_stack的lazy参数设为false |
缺页异常处理流程
- 异常触发:用户态访问未映射地址(如栈写入),CPU 陷入内核。
- 处理逻辑:
- 通过handle_page_fault函数申请物理页帧(alloc_frame)
- 在页表中建立虚拟地址与物理页帧的映射(pt.remap)
1 | fn handle_page_fault(...) -> bool { |
ELF 格式解析
关键段:
LOAD 段:包含代码段(R E标志)和数据段(RW标志)。
BSS 段:未初始化数据,ELF 文件不存储,内核需预留空间并清零。
加载逻辑:
1 | for segment in elf.segments { |
实验实现
得到aspace->分配内存->将文件信息写入
1 | const MAP_SHARED = 1 << 0; // 共享映射,对映射区域的修改会反映到文件中 |
这里只处理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 虚拟化类型
- I型Hypervisor:直接运行在硬件上(如Xen、KVM),性能高
- II型Hypervisor:运行在宿主OS上(如VirtualBox),依赖宿主资源管理
二. Riscv64虚拟化扩展(H扩展)
2.1 特权级扩展
新增特权级:
- HS(Hypervisor Supervisor):Host域的管理级,负责虚拟化控制
- VS(Virtual Supervisor):Guest域的内核级,运行Guest OS内核
- VU(Virtual User):Guest域的用户级,运行Guest应用
特权级关系:
1 | 物理机:M(最高) > HS > U |
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 | // 保存Host寄存器状态 |
可参考guest.s
- a0指向的guest_reg区域 与 当前reg的替换
3.2 VM-Exit处理(以SBI调用为例)
1
2
3
4
5
6
7
8
9match 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的值
2025 春夏季开源操作系统训练营阶段总结-joeschmo
Posted on
Edited on
Stage 1
这个阶段主要是学习Rust语法,因为之前有报名过训练营,所以做起来比较顺手,把基础语法又复习了一遍。
Stage 2
这个阶段主要是阅读实验指导书和源码。实验指导书非常重要,如果没有看明白的话对做实验有很大影响,所以要细心耐心看。时间有限的话,看精简版指导书即可。
完成实验部分需要重点理解几个点:
- 任务切换机制,保存切换前后程序上下文
- 地址空间,多级页表机制
- 文件系统,操作与管理
Stage 3
这个阶段主要是看视频和PPT,并通过做6个实验来熟悉ArceOS的设计思想。相对于上一个阶段的实验会简单一些。
总结
之前有报名过两次训练营,但都没有坚持下来。对于如何平衡学习、工作和自我提升之间的平衡,是一个我现在以及将来都需要仔细思考的问题。
2025春夏开源操作系统训练营三阶段总结报告--vipectuSSS
Posted on
Edited on
Prologue
这样的总结应该从何开始?我是从Bilibili刷视频偶然了解到与训练营相关的信息的。使用Rust语言编写操作系统的实践,我太喜欢这个方向了。因为我正学过一点Rust,也经学校老师的推荐看过CSAPP并完成了大多数的实验。其中我最喜欢的便是shlab和attacklab────写一个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
写到这里我已经有点精疲力竭。我在参与前三个阶段的过程中收获颇多,不只是对整个组件化操作系统的认识。还有各种在学习过程中对工具的使用,helix、Zellij,这些工具,我很早就下载了,只是因为它是Rust重写的老工具。现在呢?我需要helix丰富的快捷键,我需要Zellij的分屏。我开始熟悉,正是我开始迈出一步,参加了这次,2025 春夏季开源操作系统训练营。
完成了三阶段的任务,我也疲惫了,进入了一种拖延的状态。五月二十二号,新建文件夹,想要完成这篇总结报告。一直到今天,我终于又想重新出发了。希望到了第四阶段,我可以找到新的方向。
编程的乐趣:⭐️⭐️⭐️⭐️
挑战的难度:⭐️⭐️⭐️
开源训练营:⭐️⭐️⭐️⭐️⭐️
wwj三阶段学习总结
Posted on
Edited on
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 | let string_replace = String::from("I like rust. Learning rust is my favorite!"); |
replacen(被替换的字符串,新的字符串,替换的个数) 返回新的字符串
replace_range(要替换的范围,新的字符串) 改变原有的字符串
删除:
pop()删除并返回最后一个字符 改变原有的字符串remove(字符起始索引)删除并返回指定位置的字符 改变原有的字符串truncate(字符起始索引)删除指定位置至结尾的所有字符 改变原有字符串clear()清空字符串连接:
+/+=相当于调用函数add(self, s:&str……)第一个参数是string,其所有权会被转移,后面的参数需要&str类型'+'返回新的字符串format!("{}", s)用法与println!类似, 返回新的字符串
注:此处所有涉及索引的方法(包括切片),都是以字节为单位处理数据;对于UTF-8类型字符非常容易出错
结构体
1 | 1、// 定义字段 |
枚举
枚举类型是一个类型,它会包含所有可能的枚举成员,而枚举值是该类型中的具体某个成员的实例
1 | // 枚举变体携带数据 |
数组
分为静态的array和动态数组vector,先看array
array可以正常使用下标访问,可以使用{:?}打印
1 | let a = [1, 2, 3, 4, 5]; // 定义 |
流程控制
if: if语句块是表达式,可以有返回值
for
1 | for 元素 in 集合/0..集合.len() { // 注意,此处集合需要使用引用,否则所有权会被转移(如需更改加上mut) |
continue与break依然存在
while
1 | while 条件 {} |
loop
无条件循环,必须搭配break
(break类似于return,可以单独使用也可以带回来一个返回值;
loop同样是表达式,可以返回一个值)
模式匹配
match和if let
match: 非常类似于switch(但匹配后只会执行当前分支,而不会往下”贯穿“)
match同样是表达式,可以有返回值
1 | match target { |
模式绑定(从匹配到的分支中取出绑定的值)
1 | enum Coin { |
if let: 适用于只需要判断一个模式是否匹配的情况,比if更适用于匹配
1 | let some_value = Some(5); |
while let: while和let的总和,即如果满足条件就可循环,同样可以从模式匹配中拆出值
注:match/if let/while let都会转移被匹配值的借用值的所有权,需要使用ref抵消(ref只在左侧生效)
1 | if let Some(ref x) = value |
Option<T>
表示一个值是否存在的枚举(Some<T>与T不是同一类型)
对于Some和None可以不加Option::前缀
1 | enum Option<T> { |
方法
impl中存储方法与struct中声明字段分开,同时一个结构体可以有多个impl块
1 | struct Rectangle { |
注:
self表示Rectangle的所有权转移到该方法中,这种形式用的较少&self表示该方法对Rectangle的不可变借用&mut self表示可变借用允许方法名和字段名相同
在调用方法时只有
.没有->枚举同样可以定义方法
关联函数
定义在结构体impl且没有self的函数
不能使用变量.函数()的方法调用,只能使用结构体名称::函数名(参数)来调用
比如String::from()
泛型
为了抽象不同的类型
1 | fn 函数名<T>(变量名: T) -> T { // 函数泛型 |
- 在使用
T前需要先声明<T>,T的名字可以随便取 - 有时在调用泛型函数时需使用
函数名::<具体类型>()来显式指定T的类型
const泛型
允许常量值成为泛型变量,语法为const N: usize,表示const泛型N,它的值基于usize
1 | struct Buffer<T, const N: usize> { |
const fn: 在函数声明前加上const关键字
注:const泛型与const fn都需要在编译时确定,const fn就可以用于给const泛型赋值
特征
定义了一组可以被共享的行为,只要实现了特征,你就能使用这组行为(类似于接口)
1 | pub trait Summary { |
注:
- 孤儿规则:如果你想要为类型
A实现特征T,那么A或者T至少有一个是在当前作用域中定义的(另一个可以在其他库中引入) - 默认实现:可以在特征中定义具有默认实现的方法,这样其它类型无需再实现该方法,或者也可以选择重载该方法(默认实现允许调用特征中其他方法,哪怕这个方法没有默认实现)
特征约束
特征作为函数参数:
1 | pub fn notify(item: &impl Summary) { // 实现了Summary特征 的 item 参数 |
语法:
1 | pub fn notify<T: Summary>(item: &T) { |
形如 T: Summary 被称为特征约束
多重约束:
1 | // 要求同时实现了两个特征的参数 |
where约束
1 | fn some_function<T: Display + Clone, U: Clone + Debug>(t: &T, u: &U) -> i32 {} |
函数返回值:
通过 impl Trait 来说明一个函数返回了一个类型,该类型实现了某个特征
1 | fn returns_summarizable() -> impl Summary { // 返回一个实现了Summary特征的类型 |
特征对象
| 特征约束 | 特征对象 |
|---|---|
impl Trait |
dyn Trait |
接收所有实现了Trait的类型 |
接收所有实现了Trait的类型 |
| 认为是不一样的类型,不能一起存储 | 认为是相同的类型,可以一起存储 |
| 静态分发,编译时确定 | 动态分发,运行时确定 |
允许你使用 不同类型 但 实现了相同特征 的对象,使它们可以在 同一个变量、参数或返回值 中使用
1 | // 语法 |
集合类型
动态数组Vector
使用Vec<T>表示,只能存储相同类型的数据
1 | // 创建数组 |
注:可以通过使用枚举类型和特征对象来实现不同类型元素的存储
KV存储HashMap
需要使用use std::collections::HashMap;来引入
1 | // 创建与插入 |
注
HashMap 的所有权规则与其它 Rust 类型没有区别:
- 若类型实现
Copy特征,该类型会被复制进HashMap,因此无所谓所有权 - 若没实现
Copy特征,所有权将被转移给HashMap中(使用引用要确保其生命周期足够长)
生命周期
变量的生命周期声明方式:
1 | &'a i32 // 具有显式生命周期的引用 |
函数中的生命周期
需要标注生命周期的情况如下:
- 首先返回值必须是引用类型,可能会出现悬垂引用错误
- 存在多个参数时,如果编译器无法确定返回值需要跟随哪个参数的生命周期(哪怕这两个参数的生命周期是一样的),那么不标注就会报错
- 标注之后,编译时就会检查返回值使用会不会超出某个参数,如果发现超出就会报错(标注生命周期实际上不会更改任何返回值或者变量的真实生命周期,只是告诉编译器当返回值的生命周期不与较短的参数生命周期一致时,不予通过)
1 | // 用'a显式表示生命周期,此处的'a表示两个参数中较短的生命周期,需要提前标注 |
结构体中的生命周期
如果结构体的字段值类型为引用型,也需要标注生命周期'a(a可以任意替换)
作用是避免编译器报错、同时(提醒编译器)在编译时就检查其是否不超过原变量的生命周期
1 | struct ImportantExcerpt<'a> { |
生命周期声明消除
为何在只有一个参数时可以不标注生命周期?
存在以下三个步骤可以省略生命周期声明(函数中参数的生命周期是输入生命周期,返回值为输出):
每一个引用参数都会获得独自的生命周期(所以不声明则多个参数有各自的生命周期声明)
1
fn foo<'a, 'b>(x: &'a i32, y: &'b i32) // 所以不显式标出不知道跟随a还是b
若只有一个输入生命周期(函数参数中只有一个引用类型),那么该生命周期会被赋给所有的输出生命周期,也就是所有返回值的生命周期都等于该输入生命周期
1
fn foo<'a>(x: &'a i32) -> &'a i32 // 所以单个参数可以省略
若存在多个输入生命周期,且其中一个是
&self或&mut self,则&self的生命周期被赋给所有的输出生命周期
方法中的生命周期
- 类似于泛型结构体
- 方法签名中一般不需要标注,因为有
&self参数(根据以上第三条规则)
1 | struct ImportantExcerpt<'a> { |
静态生命周期
拥有'static生命周期声明的引用生命周期是整个程序
1 | let s: &'static str = "我没啥优点,就是活得久,嘿嘿"; |
属性
属性是一种元数据,用于修改编译器的行为、提供额外信息或影响代码生成方式
使用#[]语法
常见类型
#[derive()] 自动派生特征
用于让编译器自动为结构体或枚举实现特定的 trait(特征),如 Debug、Clone 等
注意只针对结构体与枚举,同时在实现某特征时(比如Copy)结构体中不能够有String这种无法自动实现Copy的字段
#[cfg(...)] 条件编译
用于根据特定 条件选择性地编译代码,例如目标平台:
1 | #[cfg(target_os = "linux")] |
#[test] Rust 测试函数
用于标记测试函数,让 cargo test 自动运行它
错误处理
panic
- 标识不可恢复错误
- 有被动与主动触发两种情况
主动触发:使用panic!宏
1 | fn main() { |
Result
标识可恢复的错误
1 | enum Result<T, E> { |
返回了该枚举类型之后就可以使用match来匹配解析
1 | let f = match f { |
如果不需要处理错误情况(即要么Ok()要么panic(),就使用unwrap()/expect)
1 | let f = File::open("hello.txt").unwrap(); |
传播错误
如果需要上级来处理这个函数中出现的错误呢?
返回Result<, >类型
- 使用
match匹配,用分支来操作/返回 - 使用宏
?
?功能类似于match
1 | // match写法 |
注:
?操作符一定需要一个变量来承接正确的值- 函数一定要是
Result<, >返回值
Option与Result的转换
1 | Option`转`Result`: 使用`.ok_or()`或`.ok_or_else() |
1 | Result`转`Option`: 丢弃错误使用`ok()`,丢弃成功值使用`.err() |
包与模块
1 | my_project/ |
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 当:
- 部分代码需要作为独立库被其他项目使用
- 项目包含多个独立可执行工具
- 某些功能需要单独编译和测试
- 需要减少编译时间(修改一个 crate 不会导致其他 crate 重新编译)
Mod(模块)
使用模块只是为了更好地组织代码,同时控制它们的可见性
1 | // 定义语法 |
- 一个
Crate是一棵模块树,而src/main.rs及src/lib.rs就是该树的根 - 模块A包含模块B,则A是B的父模块,B是A的子模块
- 模块中可以定义各种
Rust类型,如函数、结构体、枚举、特征等 - 在同一个
Crate根下的模块,相互引用的相对路径可以直接以对方模块名称开头;在同一父模块下的两个子模块,若在同文件中实现则也可以以对方模块名称开头,否则需要通过super::来使用父模块中转 - 将结构体设置为
pub,但它的所有字段依然是私有的;将枚举设置为pub,它的所有字段也将对外可见 - 可以把模块实现放入对应等级的
*.rs文件中,*要等同于模块名(文件中便不必再写),模块的定义/声明还是在父文件/模块中
use
1 | // 基本引用方式:绝对或相对路径 |
注:
- 如果引入的函数存在同名的情况时,需使用
模块名::函数名的方式或者as别名的方式来区分
限制可见性
pub意味着可见性无任何限制pub(crate)表示在当前包可见pub(self)在当前模块可见pub(super)在父模块可见pub(in <path>)表示在某个路径代表的模块中可见,其中path必须是父模块或者祖先模块
函数式编程
简单来说,迭代器/高阶函数是“流水线模板”,提供规范流程(比如map\filter等等);闭包是“可替换的工具”,即灵活调整传入的参数;而这两者都需要满足“不可变性”的安全要求
闭包
闭包是一种匿名函数,它可以赋值给变量也可以作为参数传递给其它函数,不同于函数的是,它允许捕获调用者作用域中的值
闭包语法:
1 | // 定义闭包 |
注:
- 闭包函数中是否标注类型皆可(如果未使用过则需要标注),同样可以以此省略返回值
- 闭包函数中的类型不可以是泛型,所以每次使用参数要求同类型
三种Fn特征
FnOnce: 强制需要闭包所捕获变量的所有权
FnMut: 用于闭包函数内需要改变被捕获变量的值的情况,需要闭包和捕获变量都有mut声明
Fn: 以不可变借用的方式捕获环境中的值(与FnMut不兼容,即不可改变捕获函数的值)
注:
在
FnOnce作为传入闭包的特征约束时,传入闭包和其捕获函数的所有权都会在第一次调用时被消耗;特殊情况:同时要求FnOnce与Copy(闭包会实现Copy,而其捕获的变量也会尽量实现Copy)1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20fn 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 | fn factory() -> Fn(i32) -> i32 { |
迭代器Iterator
迭代器允许我们迭代一个连续的集合,例如数组、动态数组 Vec、HashMap 等,在此过程中,只需关心集合中的元素如何处理,而无需关心如何开始、如何结束、按照什么样的索引去访问
1、.next是迭代器中取下一个值的方式,返回Option<T>
1 | pub trait Iterator { |
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 | let a = 3.1 as i8; |
注:
转换不具有传递性:就算 e as U1 as U2 是合法的,也不能说明 e as U2 是合法的(e 不能直接转换成 U2)
TryInto转换
1 | let a: u8 = 10; |
注:try_into 转换会捕获大类型向小类型转换时导致的溢出错误
From和Into特征
From<T>:定义如何从类型T转换到当前类型。Into<T>:自动为实现了From的类型生成反向转换。
1 | impl From<i32> for MyType { |
newtype
使用元组结构体的方式将已有的类型包裹起来:struct Meters(u32);,那么此处 Meters 就是一个 newtype
- 自定义类型可以让我们给出更有意义和可读性的类型名,例如与其使用
u32作为距离的单位类型,我们可以使用Meters,它的可读性要好得多 - 对于某些场景,只有
newtype可以很好地解决 - 隐藏内部类型的细节
为外部类型实现外部特征
孤儿规则:要为类型 A 实现特征 T,那么 A 或者 T 必须至少有一个在当前的作用范围内
1 | // 例:想为Vec实现Display特征,但这两个都在标准库中 |
类型别名
1 | type Meters = u32 |
注:类型别名仅仅为了更好的可读性,与原类型没有任何区别
1 | // 应用:减少代码模板的使用 |
不定长类型DST
定长类型:基础类型、集合 Vec、String 和 HashMap 等(其在栈上拥有固定大小的指针)
不定长类型:str、特征对象
1 | fn foobar_1(thing: &dyn MyThing) {} // OK |
注:只能间接使用DST,通过引用或Box来使用
Sized特征
怎么保证泛型参数是固定大小的类型?
1 | fn generic<T(: Sized)>(t: T) { // 自动补全了Sized特征 |
枚举与整数
枚举到整数很容易,但反过来需要借助三方库来实现
1 | enum MyEnum { |
智能指针
| 特性 | 引用(&T/&mut T) |
智能指针(如 Box<T>、Rc<T>) |
|---|---|---|
| 所有权关系 | 无所有权,仅是借用 | 通常拥有数据的所有权 |
| 可变性控制 | 分为共享引用(&T)和可变引用(&mut T) |
通过内部可变性(如 RefCell<T>)或类型设计实现 |
| 生命周期 | 必须显式或隐式标注生命周期 | 通常管理数据的整个生命周期(如 Box 负责释放) |
| 动态行为 | 仅提供访问,无额外逻辑 | 可附加逻辑(如引用计数、自动释放、线程安全) |
| 常见类型 | &T, &mut T |
Box<T>, Rc<T>, Arc<T>, RefCell<T> |
智能指针与普通自定义结构体区别:实现了Deref和Drop特征
智能指针用于一些较引用更复杂的场景
Box<T>堆对象分配
Box 简单的封装,用于将值存储在堆上
使用场景:
- 特意的将数据分配在堆上
- 数据较大时,又不想在转移所有权时进行数据拷贝
- 类型的大小在编译期无法确定,但是我们又需要固定大小的类型时
- 特征对象,用于说明对象实现了一个特征,而不是某个特定的类型
1 | // 将数据存储在堆上 |
另:Box::leak可以真正将一个运行期的值转化为'static,如果只标注'static可能无法成功
Deref解引用
1 | Deref` 可以让智能指针像引用那样工作,这样你就可以写出同时支持智能指针和引用的代码,例如 `*T |
\*: 对常规引用使用*操作符,即可以通过解引用的方式获取到内存地址对应的数据值
智能指针解引用: 在使用给定的智能指针时,直接使用*解引用即可
在智能指针解引用时,实际上调用了*(y.deref())方法:y.deref()先返回了值的常规引用
1 | // 如果要实现自己的智能指针同样要实现Deref特征 |
函数/方法中的隐式Deref转换
函数和方法的传参中有Deref的隐式转换。
若一个类型实现了 Deref 特征,那它的引用在传给函数或方法时,会根据参数签名来决定是否进行隐式的 Deref 转换(Deref支持连续的隐式转换)
总结
- 一个类型为
T的对象foo,如果T: Deref<Target=U>,那么,相关foo的引用&foo在应用的时候会自动转换为&U - 在解引用时自动把智能指针和
&&&&v做引用归一化操作,转换成&v形式,最终再对&v进行解引用(即将智能指针脱壳为内部的引用类型即&v, 把多级引用归一为一级&v)
Drop释放资源
指定在一个变量超出作用域时,执行一段特定的代码,最终编译器将帮你自动插入这段收尾代码(无需在每一个使用该变量的地方,都写一段代码来进行收尾工作和资源释放)
1 | impl Drop for Foo { |
Drop 的顺序
- 变量级别,按照逆序的方式(
_x在_foo之前创建,因此_x在_foo之后被drop) - 结构体内部,按照顺序的方式
注:
- Rust 自动为几乎所有类型都实现了
Drop特征(除了栈上的简单类型) - 不允许显式地调用析构函数
变量名.drop(),但可以调用函数drop(变量名)(drop()函数会拿走目标值的所有权) Copy和Drop互斥,不会在一种类型上面出现(为了防止重复释放内存)
Rc
通过引用计数的方式,允许一个数据资源在同一时刻拥有多个所有者
实现机制就是 Rc 和 Arc,前者适用于单线程,后者适用于多线程
Rc<T>
引用计数:通过记录一个数据被引用的次数来确定该数据是否正在被使用。当引用次数归零时,就代表该数据不再被使用,因此可以被清理释放
当我们希望在堆上分配一个对象供程序的多个部分使用且无法确定哪个部分最后一个结束时,就可以使用 Rc 成为数据值的所有者
1 | let a = Rc::new(String::from("hello, world")); // 创建时引用计数+1,此时Rc::strong_count(&a) 返回的值是 1 |
注:
- 这几个智能指针都是相同的所以
Rc::strong_count(&a/b/c)皆可 - 当其中一个变量离开作用域被销毁后,计数
-1,但只有当计数为0时,这个指针和指向的底层数据才会销毁 Rc<T>指向的是底层数据的不可变应用(相当于有多个不可变引用)- 实现了
Deref特征,可以直接使用里面的数值
Arc
- 原子化的
Rc<T>智能指针,保证我们的数据能够安全的在线程间共享 - 与
Rc的API完全相同 Arc和Rc并没有定义在同一个模块,前者通过use std::sync::Arc来引入,后者通过use std::rc::Rc
Cell和RefCell
解决问题(相较于引用):
- 可以通过不可变引用来修改数据
- 绕过编译期借用检查
- 实现了部分可变性(比如标定结构体某个字段为内部可变)
| 操作 | Cell<T> |
RefCell<T> |
|---|---|---|
| 获取不可变访问 | get() → T(复制) |
borrow() → Ref<T> |
| 获取可变访问 | set(new_value) |
borrow_mut() → RefMut<T> |
| 运行时检查 | 无 | 有(可能 panic) |
| 适用类型 | T: Copy(如 i32) |
任意 T(如 String) |
Cell
Cell 和 RefCell 在功能上没有区别,区别在于 Cell<T> 适用于 T 实现 Copy 的情况
1 | let c = Cell::new(42); |
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 | struct Logger { |
注: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 | use std::thread; |
注:
- 线程的启动结束时间点都是不固定的
- 由上一条,为了保证子线程中的变量一直有效,在子线程的闭包中捕获了环境变量时,需要使用
move来转移所有权 - 主线程(
main)退出时,会强制终止所有子线程(无论它们是否在运行);父线程(非主线程)退出时,不会影响它创建的子线程 thread::spawn的返回值是std::thread::JoinHandle类型,表示对线程的控制权,允许主线程通过join()等待子线程结束,同样可以使用数组收集
多线程的性能
当任务是 CPU 密集型时,就算线程数超过了 CPU 核心数,也并不能帮你获得更好的性能
当你的任务大部分时间都处于阻塞状态时,就可以考虑增多线程数量(典型就是网络 IO 操作)
线程屏障Barrier
让多个线程都执行到某个点后,才继续一起往后执行
1 | use std::sync::{Arc, Barrier}; |
注:
- 需要
Arc智能指针,作用是允许多个线程同时拥有同一数据(跨线程Rc) Barrier::new(n)中的n值一定要与实际调用wait()的线程数相等
多线程局部变量
标准库thread_local
1 | // 定义 |
注:如果想使用多个局部变量的闭包函数,使用嵌套
同样还有使用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 | use std::sync::mpsc; |
tx,rx对应发送者和接收者,它们的类型由编译器自动推导:tx.send(1)发送了整数,因此它们分别是mpsc::Sender<i32>和mpsc::Receiver<i32>类型,(一旦类型被推导确定,该通道就只能传递对应类型的值)- 接收消息的操作
rx.recv()会阻塞当前线程,直到读取到值,或者通道被关闭 - 需要使用
move将tx的所有权转移到子线程的闭包中 - 使用通道来传输数据,一样要遵循 Rust 的所有权规则:
- 若值的类型实现了
Copy特征,则直接复制一份该值,然后传输过去,例如之前的i32类型 - 若值没有实现
Copy(如String类型),则它的所有权会被转移给接收端,在发送端继续使用该值将报错
- 若值的类型实现了
- 异步中只有接收者会被阻塞,同步中发送者也会因为接收者接收不到消息被阻塞
- 所有发送者被
drop或者所有接收者被drop后,通道会自动关闭
锁、Condvar
使用共享内存来实现同步性
面临问题:多个线程同时修改同一数据时,结果不可预测;线程执行顺序影响最终结果
互斥锁Mutex
同一时间,只允许一个线程A访问该值,其它线程需要等待A访问完成后才能继续
1 | // 创建实例:锁的容器 |
m.lock()返回一个智能指针MutexGuard<T>,拥有Deref特征(自动解引用获取引用类型)与Drop特征(超出作用域自动释放锁)Rc<T>/RefCell<T>用于单线程内部可变性,Arc<T>/Mutex<T>用于多线程内部可变性
死锁
- 在另一个锁还未被释放时去申请新的锁,就会触发
- 当我们拥有两个锁的容器,且两个线程各自使用了其中一个锁,然后试图去访问另一个锁时,就可能发生死锁
try_lock(): 尝试去获取一次锁,如果无法获取会返回一个错误,因此不会发生阻塞
1 | let guard = MUTEX2.lock().unwrap(); |
读写锁RwLock
1 | Mutex`会对每次读写都进行加锁,但某些时候,我们需要大量的并发读,`Mutex`就无法满足需求了,此时就可以使用`RwLock |
我们也可以使用try_write和try_read来尝试进行一次写/读,若失败则返回错误
1 | Err("WouldBlock") |
条件变量Condvar控制线程同步
1 | use std::sync::{Arc, Mutex, Condvar}; |
消费者线程需等待条件成立才可执行:获取互斥锁检查条件是否成立 => 不成立则进入循环 => cvar.wait(condition).unwrap()执行时立即释放互斥锁(交由其他线程修改) => 其他线程修改后使用cvar.notify_one()唤醒该线程 => 返回条件重新检查
注:
notify_one()wait()1
2
随机唤醒一个正在use std::sync::atomic::AtomicUsize;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`)。 |
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 |
|
X = 1;
Y = 3;
X = 2; // 直接变成
X = 2;
Y = 3;
1 |
|
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 |
|
const MAX_ID: usize = usize::MAX / 2;
1 |
|
static mut REQUEST_RECV: usize = 0;
1 |
|
use std::sync::atomic::{AtomicUsize, Ordering};
static REQUEST_RECV: AtomicUsize = AtomicUsize::new(0);
1 |
|
use lazy_static::lazy_static;
lazy_static! {
static ref NAMES: Mutex
}
1 |
|
#[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 |
|
let s1 = Some(“some1”);
let s2 = Some(“some2”);
let n: Option<&str> = None;
assert_eq!(s1.or(s2), s1);
1 |
|
let s1 = Some(“some1”);
let fn_some = || Some(“some2”);
let fn_none = || None;
assert_eq!(s1.or_else(fn_some), s1);
1 |
|
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 |
|
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 |
|
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 |
|
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 |
|
use std::fmt::{Debug, Display};
pub trait Error: Debug + Display {
fn source(&self) -> Option<&(Error + ‘static)> { … }
}
1 |
|
// std::convert::From特征
pub trait From
fn from(_: T) -> Self;
} // T为原本的错误类型
1 |
|
// 基于引用创建裸指针
let mut num = 5;
let r1 = &num as *const i32;
let r2 = &mut num as *mut i32;
// 使用*解引用
unsafe {
println!(“{}”, *r1);
}
// 基于智能指针创建裸指针
let a: Box
let b: const i32 = &a; // 需要先解引用a
let c: *const i32 = Box::into_raw(a); // 使用 into_raw 来创建
1 |
|
// unsafe函数:外表唯一不同就是需要unsafe fn来定义,在调用时需要放在unsafe块
// 在unsafe函数中使用unsafe来注明块是多余的行为
unsafe fn dangerous() {}
fn main() {
unsafe {
dangerous();
}
}
// 在函数中使用了unsafe声明块不代表函数要声明为unsafe fn:同样可以使用用安全的抽象包裹unsafr代码
1 |
|
// 调用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 |
|
#[repr(C)]
union MyUnion {
f1: u32,
f2: f32,
}
1 |
|
// 基本形式
macro_rules! macro_name {
(pattern) => { expansion };
// 可以有多个匹配模式
}
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 |
|
// hello_macro项目目录
hello_macro
├── Cargo.toml
├── src
│ ├── main.rs
│ └── lib.rs
└── hello_macro_derive // 此包中实现宏
├── Cargo.toml
├── src
└── lib.rs
1 |
|
// 修改 hello_macro/Cargo.toml 文件添加以下内容
[dependencies]
hello_macro_derive = { path = “../hello_macro/hello_macro_derive” }
也可以使用下面的相对路径
hello_macro_derive = { path = “./hello_macro_derive” }
1 |
|
// 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 |
|
批处理系统的应用管理器:
(从系统文件中一个记录了应用数量、各应用起始位置、最后一个应用结束位置的link_app.S中获取)
1 | struct AppManager { |
方法: print_app_info/get_current_app/move_to_next_app
load_app将参数 app_id 对应的应用程序的二进制镜像加载到物理内存以 0x80400000 起始的位置
batch子模块暴露的接口
init:初始化APP_MANAGERrun_next_app:加载并运行下一个应用程序
用户栈与内核栈
1 | 1// os/src/batch.rs |
实现了 get_sp 方法来获取栈顶地址
特权级切换
| CSR 名 | 该 CSR 与 Trap 相关的功能 |
|---|---|
sstatus |
SPP 等字段给出 Trap 发生之前 CPU 处在哪个特权级(S/U)等信息 |
sepc |
当 Trap 是一个异常的时候,记录 Trap 发生之前执行的最后一条指令的地址 |
scause |
描述 Trap 的原因 |
stval |
给出 Trap 附加信息 |
stvec |
控制 Trap 处理代码的入口地址 |
硬件自动完成:
sstatus的SPP字段会被修改为 CPU 当前的特权级(U/S)。sepc会被修改为 Trap 处理完成后默认会执行的下一条指令的地址。scause/stval分别会被修改成这次 Trap 的原因以及相关的附加信息。- CPU 会跳转到
stvec所设置的 Trap 处理入口地址,并将当前特权级设置为 S ,然后从Trap 处理入口地址处开始执行。
处理完成后通过S特权级sret指令:
- CPU 会将当前的特权级按照
sstatus的SPP字段设置为 U 或者 S ; - CPU 会跳转到
sepc寄存器指向的那条指令,然后继续执行。
Trap上下文
1 | 1// os/src/trap/context.rs |
实现 Trap 上下文保存和恢复的汇编代码os/src/trap/trap.S,用(汇编中的)外部符号 __alltraps 和 __restore 标记为函数
Trap 处理的总体流程如下:首先通过 __alltraps 将 Trap 上下文保存在内核栈上,然后跳转到使用 Rust 编写的 trap_handler 函数 完成 Trap 分发及处理。当 trap_handler 返回之后,使用 __restore 从保存在内核栈上的 Trap 上下文恢复寄存器。最后通过一条 sret 指令回到应用程序执行
使用sp表示当前的栈,sscratch代表另一个栈
方法:
1 | impl TrapContext { |
分时多任务
user/build.py为每个应用定制各自的起始地址,.text 段的地址为 0x80400000 + app_id * 0x20000
batch 被拆分为 loader 和 task , 前者负责启动时加载应用程序,后者负责切换和调度。
loader 模块的 load_apps 函数负责将所有用户程序在内核初始化的时一并加载进内存
任务切换
当一个应用在内核态时,其 Trap 控制流可以调用一个特殊的 __switch 函数,函数调用时运行另一个任务,返回后运行原来的任务
在 __switch 中保存 CPU 的某些寄存器,它们就是任务上下文
函数拥有两个参数:
1 | __switch( |
内核先把 current_task_cx_ptr 中包含的寄存器值逐个保存,再把 next_task_cx_ptr 中包含的寄存器值逐个恢复
1 | 1// os/src/task/context.rs |
管理多道程序
- 任务运行状态:未初始化、准备执行、正在执行、已退出
- 任务控制块:维护任务状态和任务上下文
- 任务相关系统调用:程序主动暂停
sys_yield和主动退出sys_exit
1 | pub enum TaskStatus { // 任务状态 |
全局的任务管理器
1 | pub struct TaskManager { |
TaskManager方法:mark_current_suspended(暂停当前程序)/ mark_current_exited / run_next_task / find_next_task(找到下一个Ready状态的应用)
时钟中断
处理器维护时钟计数器 mtime,还有另外一个 CSR mtimecmp 。 一旦计数器 mtime 的值超过了 mtimecmp,就会触发一次时钟中断
1 | pub fn get_time() -> usize { // 获得mtime值 |
页表机制
非叶节点(页目录表,非末级页表)的表项标志位含义和叶节点(页表,末级页表)相比有一些不同:
- 当
V为 0 的时候,代表当前指针是一个空指针,无法走向下一级节点,即该页表项对应的虚拟地址范围是无效的; - 只有当
V为1 且R/W/X均为 0 时,表示是一个合法的页目录表项,其包含的指针会指向下一级的页表; - 注意: 当
V为1 且R/W/X不全为 0 时,表示是一个合法的页表项,其包含了虚地址对应的物理页号。
物理地址与物理页号转换
1 | 3impl PhysAddr { |
页表项的数据结构抽象与类型定义
页表项共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 | bitflags! { // 用来表示比特位的宏 |
结构体的实现与方法:
1 | 3#[derive(Copy, Clone)] |
页帧管理器
1 | pub struct StackFrameAllocator { |
多级页表
正常情况可以依靠MMU直接翻译,手动翻译是由于操作系统是不能直接靠MMU来访问用户地址程序的
1 | // 一个应用所有的页表 |
访问特定物理页帧
1 | impl PhysPageNum { |
建立/拆除虚实地址映射
1 | impl VirtPageNum { |
只查询,不建立
1 | impl PageTable { |
地址空间抽象
逻辑段:虚拟地址连续,虚拟地址映射到物理地址的方式相同(物理页帧具有的属性相同而非物理地址连续)
1 | // 逻辑段结构体 |
地址空间:一个进程能够访问的所有内存地址的集合,通常被组织为多个逻辑段
pagetable 实际上查找虚拟/物理地址映射的方法(存储各级节点,可以手动搜索),供CPU/MMU使用
areas 逻辑上管理的方法(管理虚拟内存、映射数组来直接寻找)
1 | // 某个地址空间的结构体 |
内核的地址空间排布
跳板、各应用的内核栈(栈间有空洞区域防溢出)
四个逻辑段.text/.rodata/.data/.bss(恒等映射)、恒等映射(除之前内核已使用)所有物理页帧的页表 (即是内核页表)(注:后面两项都是恒等映射建立的三级页表MapArea)
应用程序的地址空间排布
跳板、trap上下文(用户不可访问)
用户栈、保护页guard page、各逻辑段
1 | // 批处理系统升级:加载应用进入内存 |
基于空间地址的分时多任务
1 | KERNEL_SPACE: Arc<UPSafeCell<MemorySet>> // 内核地址全局实例 |
注:跳板就是执行trap时保存上下文的汇编代码_alltraps和_restore,由于其在内核与应用地址空间的位置相同,所以无论哪种页表都可以在同一位置访问
1 | // 任务上下文 |
进程
新增系统调用
1 | /// 功能:当前进程 fork 出来一个子进程。 |
在用户级中,在最最开始(即在main函数中)会初始化一个initproc用户初始进程,
其只会初始化一个shell进程,之后就持续循环+时间片轮转来回收进程(注:所有父进程被回收的进程都会变成其子进程)
1 | // 基于应用名的应用加载器 |
进程标识符
1 | // 进程标识符PID |
内核栈
原本每个程序一个,固定大小按程序顺序排列,中间穿插守护页防止溢出
现在将应用编号替换为进程标识符PTD,在内核栈中保存
1 | pub struct KernelStack { |
进程控制块
之前的TaskControlBlock分离为
Processor处理器管理结构:管理CPU正在运行的任务
TaskManager任务管理器:管理未在运行的所有任务
1 | pub struct TaskControlBlock { // 不可变 |
任务管理器与处理器管理器
1 | // 任务管理器 |
注:
- 将
Processer的任务上下文分离并且单独存储,是为了把调度和存储分离开,并无其他意义 Processer由内核中的结构体和上下文组成,相当于一个没有进程控制块的进程来用于调度
进程机制实现
- 创建初始进程:创建第一个用户态进程
initproc; - 进程调度机制:当进程主动调用
sys_yield交出 CPU 使用权或者内核把本轮分配的时间片用尽的进程换出且换入下一个进程; - 进程生成机制:进程相关的两个重要系统调用
sys_fork/sys_exec的实现; - 进程资源回收机制:当进程调用
sys_exit正常退出或者出错被内核终止之后如何保存其退出码,其父进程通过sys_waitpid系统调用收集该进程的信息并回收其资源。 - 字符输入机制:为了支持shell程序-user_shell获得字符输入,介绍
sys_read系统调用的实现;
1 | // 创建初始进程 |
在执行exit_current_and_run_next(exit_code: i32)时
- 修改当前进程控制块的状态为
TaskStatus::Zombie即僵尸进程 - 将退出码传入控制块等待父进程收集
- 将所有子进程挂载在
initproc下面 - 对当前进程早期回收(回收
Memory_set中的areas即数据页,不回收pagetable即页表页)
user_shell读入机制
1 | pub fn sys_read(fd: usize, buf: *const u8, len: usize) -> isize { |
文件系统
文件与文件描述符
1 | // 文件接口:只要实现了这个接口,就是文件 |
用户缓冲区:用户进程在系统调用的时候传给内核的地址空间(以内核为中转,用户缓冲区 <=> 文件)
1 | pub fn translated_byte_buffer(token: usize, ptr: *const u8, len: usize) -> Vec<&'static mut [u8]>; |
标准输入/输出
1 | pub struct Stdin; |
文件描述符:(文件描述符首先是一个非负整数)对于某一个进程,代表了其打开的一个文件对象,在要对文件进行操作时传入该整数即可(由内核来分配和记录,因为所有文件都在内核中)
文件描述符表:每个进程都有,记录所有其打开且可读写的文件集合(可以以表的下标作为描述符)
1 | pub struct TaskControlBlockInner { |
此处的数据结构:
Vec无需设置一个固定的文件描述符数量上限;Option区分一个文件描述符当前是否空闲:当它是None的时候是空闲的,而Some则代表它已被占用;Arc提供了共享引用能力:可能会有多个进程共享同一个文件对它进行读写;被它包裹的内容会被放到内核堆而不是栈上,不需要在编译期有确定的大小dyn关键字表明Arc里面的类型实现了File/Send/Sync三个 Trait ,编译期无法知道它具体是哪个类型需要等到运行时才能知道它的具体类型。
1 | pub fn sys_write(fd: usize, buf: *const u8, len: usize) -> isize { |
文件系统接口
1 | // 打开/读写的系统调用 |
简易文件系统
easy-fs是简易文件系统的本体easy-fs-fuse是能在开发环境(如 Ubuntu)中运行的应用程序,用于将应用打包为 easy-fs 格式的文件系统镜像,也可以用来对easy-fs进行测试
文件系统层次化(共分为五层,上层可以调用下层的接口):
1、磁盘块设备接口层:以块为单位对磁盘块设备进行读写的 trait 接口
1 | pub trait BlockDevice : Send + Sync + Any { |
2、块缓存层:在内存中缓存磁盘块的数据,避免频繁读写磁盘
1 | // 块缓存 |
3、磁盘数据结构层:磁盘上的超级块、位图、索引节点、数据块、目录项等核心数据结构和相关处理
easy-fs 磁盘按照块编号从小到大顺序分成 5 个连续区域:
- 第一个区域只包括一个块,它是超级块,用于定位其他连续区域的位置,检查文件系统合法性
- 第二个区域是一个索引节点位图,长度为若干个块:记录索引节点区域中有哪些索引节点已经被分配出去使用了(每个
bit表示一个节点) - 第三个区域是索引节点区域,长度为若干个块,其中的每个块都存储了若干个索引节点(每个节点描述一个“文件”/“目录”)
- 第四个区域是一个数据块位图,长度为若干个块:记录后面的数据块区域中有哪些已经被分配出去使用
- 最后的区域则是数据块区域:每个被分配出去的块保存了文件或目录的具体内容
1 | // 第一个区域:超级块 |
4、磁盘块管理器层:合并了上述核心数据结构和磁盘布局所形成的磁盘文件系统数据结构
1 | // 一个文件系统对应一个磁盘(分区) |
注意:只要知道了数据所在的具体磁盘块号和块内偏移,可以以任意结构体方式操作这一段数据get_block_cache(block_id, device).lock().modify(offset, |变量名: &mut 结构体| {})
5、索引节点层:管理索引节点,实现了文件创建/文件打开/文件读写等成员函数
便于直接看到目录树结构中逻辑上的文件和目录
DiskInode放在磁盘块中比较固定的位置Inode是放在内存中的记录文件索引节点信息的数据结构
1 | pub struct Inode { |
内核中的easy-fs
1 | // 内核块设备实例 |
进程间通信
管道
1 | // 系统调用:为当前进程打开一个管道(包含一个只读、一个只写文件) |
基于文件的管道
1 | // 管道的一端(读/写):会实现File特征,作为文件访问 |
命令行参数与标准I/O重定向
命令行参数
在user_shell中读取一行后,根据空格分隔成Vec<String>,然后手动在每个字符串后面加上\0,在最后加上0 as *const u8,将字符串数组的起始地址传入sys_exec()内核调用
1 | // 系统调用更新 |
根据TCB创建时从ELF中读出来的内容,所有进程第一次进入用户态都是从_start进入:这个函数会依次取出命令行参数并且放入一个数组中
标准输入输出重定向
1 | // 系统调用:将进程中一个已经打开的文件复制一份并分配到一个新的文件描述符中 |
在用户态的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 | // 系统调用 |
线程管理由进程而来
1 | pub struct TaskControlBlock { |
进程控制块1 | pub struct ProcessControlBlock { |
锁
相关数据结构:使用锁来包裹共享资源
1 | pub struct ProcessControlBlock { |
相关系统调用
1 | pub fn sys_mutex_create(blocking: bool) -> isize { |
信号量:适用于一个共享资源可以被有限个线程同时访问的情况(互斥锁即为N=1)
P操作:尝试进入,失败则阻塞
V操作:信号量的值+1,如果有线程等待则唤醒
(注意,以上两个操作都应该有原子性)
1 | pub struct ProcessControlBlock { |
条件变量
线程在检查满足某一条件后才会执行(条件变量时一个线程等待队列)
wait操作:释放锁 => 挂起自己 => 被唤醒后获取锁
signal操作:找到挂在条件变量上面的线程并唤醒
1 | pub struct ProcessControlBlock { |
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之后才发现是没有