0%

1
make -C /home/linux/vscode/arceos A=/home/linux/vscode/my_arceos_app ARCH=aarch64 LOG=debug SMP=1 run

U.1.0 HelloWorld

![![[Pasted image 20241111091146.png]]](<2024年开源操作系统训练营第三阶段总结-silent12rt/Pasted image 20241111091146.png>)
该图展示了一个应用程序(hello_world)在系统中各个模块(如axruntimeaxhalaxstdarceos)之间的结构和交互关系。以下是各个部分的功能说明:

  1. axhal(硬件抽象层):该层对硬件细节进行抽象,为更高层提供一个屏蔽底层硬件的基础,使上层不需要直接管理底层硬件。

  2. axruntime(运行时环境):该层管理应用程序的运行时环境,包括内存分配、线程调度和其他运行时服务,是应用程序正常运行的基础。

  3. ulib(axstd):这是一个标准库,为应用层提供通用功能和实用工具,可能包含基本的I/O操作、数据处理等辅助功能。

  4. api(arceos):这是与底层操作系统(ArceOS)的应用编程接口,允许应用程序执行系统级操作,如文件管理、进程间通信等。

  5. app(hello_world):这是用户的应用程序,利用axstdarceos提供的库和接口来执行特定任务。

  6. 执行流程

    • 左侧的准备环境(蓝色箭头)表示系统的准备阶段,在此阶段,配置和初始化必要的资源和环境。
    • 右侧的调用功能(橙色箭头)表示应用程序在运行时与axstdarceos进行的交互,通过这些库和API执行特定功能。

U.2.0 Collections

Buddy System(伙伴系统)

  1. 分配内存单元
    设置最小分配单元(通常是 2 的幂次方大小),而不是按1字节来分配。这种划分可以提高分配效率,并且降低管理开销。例如,如果分配单元是 8 字节,最小分配的内存块将是 8 字节。在 Buddy System 中,不同大小的块按 2 的幂次划分(即 8、16、32、64 等),每个大小被称为一个 order。每个 order 是一种特定大小的块。
  2. 分配过程
    • 寻找最小满足请求的块。当程序请求内存时,分配器首先确定需要分配的块的大小(例如 64 字节)。然后分配器会在内存池中找到最小的能满足此请求的 order 块(例如 128 字节的块,若没有 64 字节的块)。
    • 二分切割。如果找到的 order 大于所需的大小,那么分配器将不断地对该块进行 二分切割,直到得到匹配所需大小的块。
    • 返回分配的块:分配器返回一个与请求大小匹配的块,并将它从空闲列表中移除。此时,程序可以使用该块。
  3. 释放过程
    • 检查是否有空闲的邻居块:当程序释放某块内存时,分配器会检查该块是否有“伙伴”块(即同一级的邻居块)也是空闲的。两个邻居块的地址通常具有某种关系,使分配器可以根据地址快速定位伙伴块。
    • 合并到高 order:如果找到空闲的伙伴块,分配器会将两个相邻的空闲块合并成一个更大的块。这个合并过程会尽量继续进行,直到不能再合并为止。
    • 挂到 Order List:如果无法进一步合并,最终的空闲块会挂到相应的空闲列表(Order List)中,以备后续分配使用。

内存分配算法-Slab

![![[Pasted image 20241111191653.png]]](<2024年开源操作系统训练营第三阶段总结-silent12rt/Pasted image 20241111191653 1.png>)
分配过程

  1. 找到合适的OrderList
    根据请求的内存大小,找到合适的 OrderList。OrderList 会匹配内存大小,确保分配合适的 Slab。
  2. 从 Slab 的空闲块链表中获取 block
    • 从空闲块链表 (Free Block List) 中弹出一个 block,完成分配。
    • 如果空闲块链表中没有可用的 block,则进入下一步。
  3. 调用 BuddyAllocator 分配新块:
    • 当空闲链表中没有足够的块时,向 Buddy Allocator 请求一个较大的内存块。
    • 将分配到的较大内存块切分为符合 Slab 需求大小的 block,然后加入到该 Slab 的空闲块链表。
    • 最终,分配请求从空闲块链表中取出一个 block 返回。

释放过程

  1. 释放 block 到空闲块链表
    • 释放时,将 block 放回对应 Slab 的空闲块链表。
    • 这样,当后续需要分配类似大小的块时,可以直接从该空闲块链表中分配,避免重复分配和释放较大块的开销。
  2. 管理内存回收
    • 如果某个 Slab 变得完全空闲(即所有 block 都释放),可以选择将该 Slab 的内存归还给 Buddy Allocator,以释放更多内存供其他用途。

U.3.0 Collections

U.4.0 Collections

核心算法:context_switch

任务上下文Context: 保存任务状态的最小的寄存器状态集合。
![![[Pasted image 20241112144224.png]]](<2024年开源操作系统训练营第三阶段总结-silent12rt/Pasted image 20241112144224.png>)
ra: 函数返回地址寄存器,这个切换实现了任务执行指令流的切换。
sp: 任务即线程,这个是线程栈
s0~s11:按照riscv规范,callee不能改这组寄存器的信息,所以需要保存。

抢占式调度算法ROUND_ROBIN

在协作式调度FIFO的基础上,由定时器定时递减当前任务的时间片,耗尽时允许调度,一旦外部条件符合,边沿触发抢占,当前任务排到队尾,如此完成各个任务的循环排列。
![![[Pasted image 20241112151551.png]]](<2024年开源操作系统训练营第三阶段总结-silent12rt/Pasted image 20241112151551.png>)

抢占式调度算法CFS(Completely Fair Scheduler)

![![[Pasted image 20241112151735.png]]](<2024年开源操作系统训练营第三阶段总结-silent12rt/Pasted image 20241112151735.png>)

前言

我是一位已经参加工作有12年的资深工程师, 职业生涯中曾经担任过 CTO, 也做过总架构师. 精通 5+ 门编程语言, 对 10+ 门编程语言有过万行生产环境编码经验.
但因为自己毕业于一所大专院校, 在大专学习期间学校并没有开设过这些计算机系基础课程. 在多年工作中我时常好奇:

  1. 应用程序的内存是如何分配的
  2. 应用程序代码是如何启动的
  3. 应用如何与操作系统进行交互
  4. 操作系统是如何管理硬件资源的
  5. 多线程是如何实现的
    等等问题.
    抱着求知的心态, 我报名参加了这次的操作系统训练营.
    这些基本能力, 对应用开发者来说就像超市货架上每天都能“生长”出来的食物, 操作系统是如何把他们实现的, 我想要了解这个过程.

我曾在2021年参加过一次学堂在线的操作系统课程, 但是因为工作和家庭琐事繁忙, 最终没有完成. 这次我希望通过这次训练营, 继续学习操作系统的知识, 并且完成所有的实验.

我始终贯彻着一句话: 计算机是一门实践工程学科, 不管看的听的再怎么醍醐灌顶, 写不出来就是没学会.
学校的操作系统课程, 大多数都是讲理论, 缺乏实践. 这次训练营, 我希望能开发自己的内核程序, 并烧录到自己的硬件上运行.

第二阶段

Lab 1

这个实验主要是实现一个简单的多任务系统. 通过这个实验, 我理解了硬件是如何在不同的特权级之间切换的, 以及操作系统是如何管理这些特权级的.

Lab 2

这个实验启用了分页机制, 我学习到了地址空间的概念, 应用程序只需要关心自己的地址空间, 而不需要关心其他应用程序的地址空间. 同时我也学习到了内核是如何管理这些地址空间的.

Lab 3

进程: 学习到了进程是如何创建的, 以及进程是如何执行的.

Lab 4

文件系统: 学习到了文件系统是如何与物理存储设备交互的, 块存储设备是如何存储文件与数据的.

Lab 5

多线程: 学习到了操作系统是如何实现多线程的, 以及多线程之间如何通过锁来通讯.
通过课程实验: 我学习了如何用银行家算法检测死锁.

总结

通过这门课程我学习到了很多操作系统的基础知识, 包括:

  • 裸机程序是如何启动的
  • 应用程序内存是如何分配与隔离的
  • 操作系统是如何操作硬件的
  • 应用程序是如何与操作系统进行交互的
  • 文件系统是如何实现的
  • 等等…
    同时我也发现课程框架代码存在许多值得改进的地方, 所以我在尝试编写自己的内核程序.

阶段1:语言学习

rust 是一门非常有意思的语言,它吸收了很多现代编程语言的特性,特别是来自函数式编程的许多特性,比如 默认不可变,模式匹配,流 api,以及 trait,可以以一种不同的方式去抽象与编码。
rust 的“人体工程学”做的也非常不错,有许多语法糖与内置宏,匿名函数的写法也足够简洁,在学习过程中感觉非常有趣。

在语言学习过程中重点学习了基本的语法以及一些库函数的使用,对语言周边,比如 clippy 的使用还不够好,build.rs 也不太会写,多线程编程也不够熟练,希望接下来的学习当中能再注意一下重点学一下这些东西。

阶段2:rcore labs 学习

rcore 今年秋冬的任务是除了进程间通信以及io设备没有做过多要求,其余的每章都有一个编程练习,在我看来,编程练习的难度是适中的,但是依然要求完整的理解整个代码框架,对学习操作系统内核很有帮助。

前三章作为引子一步一步从裸机程序到一个批处理裸机程序的内核,后面几章的顺序是:虚拟内存(地址空间),进程,文件(持久化存储),再到并发。课程顺序与传统授课顺序不太一样,在我个人看来是一种由易到难的渐进学习过程:因为第八章的线程和并发编程真的感觉好难 debug 😂,在这章上浪费的时间也比较多。

印象最深刻的是对文件系统的讲解,将文件系统作为一个库抽象出来,内核也只是管理文件系统提供的 Inode 接口,以此来区分在内存中的 inode 和磁盘中的 inode,这样一层从 BlockDevice 到 FileSystem 再到 rcore-kernel 最后被封装为一些系统调用,这样层层抽象的写法看起来真的挺赏心悦目的哈哈,而且对知识的学习与综合起来也方便许多。

另外还有虚拟内存,内存模型也是内容量非常庞大的一章,需要理解 memory-set, map-aera, pagetable-entry, 以及物理地址和虚拟地址,物理页号和虚拟页号之间的关系,

抽象之下是和底层处理器的操作,内联汇编,riscv 库,以及 C ffi,rust 都支持的很好。处理 trap 的汇编函数也很有意思,以及一些 riscv csr,学习的同时也去了解了一下 riscv。不过本次学习,对rv要求的不是很多,更多的是以问答作业的形式去提问,不知道后面的阶段会不会有深入的机会。

2024二阶段总结

在这一阶段,我对我的学习可以划分两个比较明显的时期,一个是前期比较懵懂地入门,一个是中后期的速通。

Read more »

Rustlings

这是我第一次刷Rustlings,还是很有收获的。最后新增的一些算法题也很有意思,不过整体来说还是适合有Rust语法知识基础的人学习。最后几个算法题刷的很慢,我的数据结构和算法学的不是很好,在这里有学到很多。不过我也参考了许多资料。如果有人看的话,也算一点收获叭。整体来说Rust写起来很爽!

参考资料

Rust语法基础

初学者应该主要从三个方面了解Rust:Rust语法基础,Cargo以及Rust的标准库和官方文档。

Rust 程序设计语言 - Rust 程序设计语言 简体中文版

这是社区推荐的的The Rust Programming Language的中文翻译版本。主要讲Rust语法基础,国内有开源作者撰写了Rust语言圣经两本书的内容比较相似,后者语言比较生动,内容也比较丰富,前者的话语言精炼一些,个人比较推荐去读前者,读不懂的时候再去看Rust语言圣经的版本,会有新的收获。

The Cargo Book

Introduction - The Cargo Book

The Cargo Book是关于Cargo的一本书,初学者可能只会使用到Cargo的一少部分命令和参数,但实际上,Rust受到广泛关注的一个原因,就来自于强大的构建和包管理工具Cargo,值得注意的是这本书的中文翻译版本最后的更新时间是2019年,相关的内容和英文最新版差别比较大,最好读英文版本。

Rust标准库文档

List of all items in this crate

Rust的标准库文档涵盖了基础阶段大部分的内容,结构体、宏、智能指针等等都在标准库文档中有详细的说明,Rust程序相比其他许多语言确实比较在学习和编写上难度更大 ,但是Rust设计者们也在极力减少开发者的心智负担,对于一些数据类型和结构,标准库中定义了一些好用的方法和属性,方便大家学习和使用。另外,标准库中也定义了一些基本的API接口,方便开发。总之Rust标准库是一座宝库。

Rust

crates.io: Rust Package Registry

crate.io是提供了诸多Rust开发者开发的库,可以直接在cargo.toml里面配置库名和版本就能使用,很方便,基础阶段Rust标准库文档是小宝库,在后续进阶开发阶段,crate.io就是名副其实的大宝库。言下之意是,基础阶段暂时不用看这个。

Youtube

Rustlings 5.0 | Intro | Learn Rust Interactively

**目前国内的Rust学习资源还在初级阶段,B站上暂时没有很完善的教程,推荐一个油管博主的视频,讲到比较陌生不好理解的地方,博主会把Rust基础教程和标准库文档贴出来,对着讲解,还是很有收获的,还能教你怎么快速找到自己需要查的知识点。但是这个博主是之前录的视频,没有训练营版本Rustlings最后一些练习的讲解。

Rustlings

这是我第一次刷Rustlings,还是很有收获的。最后新增的一些算法题也很有意思,不过整体来说还是适合有Rust语法知识基础的人学习。最后几个算法题刷的很慢,我的数据结构和算法学的不是很好,在这里有学到很多。不过我也参考了许多资料。如果有人看的话,也算一点收获叭。整体来说Rust写起来很爽!

参考资料

Rust语法基础

初学者应该主要从三个方面了解Rust:Rust语法基础,Cargo以及Rust的标准库和官方文档。

Rust 程序设计语言 - Rust 程序设计语言 简体中文版

这是社区推荐的的The Rust Programming Language的中文翻译版本。主要讲Rust语法基础,国内有开源作者撰写了Rust语言圣经两本书的内容比较相似,后者语言比较生动,内容也比较丰富,前者的话语言精炼一些,个人比较推荐去读前者,读不懂的时候再去看Rust语言圣经的版本,会有新的收获。

The Cargo Book

Introduction - The Cargo Book

The Cargo Book是关于Cargo的一本书,初学者可能只会使用到Cargo的一少部分命令和参数,但实际上,Rust受到广泛关注的一个原因,就来自于强大的构建和包管理工具Cargo,值得注意的是这本书的中文翻译版本最后的更新时间是2019年,相关的内容和英文最新版差别比较大,最好读英文版本。

Rust标准库文档

List of all items in this crate

Rust的标准库文档涵盖了基础阶段大部分的内容,结构体、宏、智能指针等等都在标准库文档中有详细的说明,Rust程序相比其他许多语言确实比较在学习和编写上难度更大 ,但是Rust设计者们也在极力减少开发者的心智负担,对于一些数据类型和结构,标准库中定义了一些好用的方法和属性,方便大家学习和使用。另外,标准库中也定义了一些基本的API接口,方便开发。总之Rust标准库是一座宝库。

Rust

crates.io: Rust Package Registry

crate.io是提供了诸多Rust开发者开发的库,可以直接在cargo.toml里面配置库名和版本就能使用,很方便,基础阶段Rust标准库文档是小宝库,在后续进阶开发阶段,crate.io就是名副其实的大宝库。言下之意是,基础阶段暂时不用看这个。

Youtube

Rustlings 5.0 | Intro | Learn Rust Interactively

**目前国内的Rust学习资源还在初级阶段,B站上暂时没有很完善的教程,推荐一个油管博主的视频,讲到比较陌生不好理解的地方,博主会把Rust基础教程和标准库文档贴出来,对着讲解,还是很有收获的,还能教你怎么快速找到自己需要查的知识点。但是这个博主是之前录的视频,没有训练营版本Rustlings最后一些练习的讲解。

有关于Rust基础,是笔者在做毕设时因需要而学的,故而阶段一没有花费什么精力。但是对于阶段二,情况则截然不同。笔者没有听从项目文档的建议,选择了单枪匹马完成所有5个任务,期间也出现了许多迷之错误…

对于Rust这门编程语言来说,笔者个人认为,虽然它可以被用来编写底层的代码,包括bootloader、UEFI、OS内核等等,但是它的编译器已经决定了Rust的上限。笔者不才,曾经研究过一部分Rust的反编译分析,发现Rust对于栈内存的使用似乎非常奢侈。与C语言不同,Rust对于Shadowing的处理不是复用原来的栈内存空间,而是直接使用新的空间。另外Rust的结构也相比C臃肿很多,这就导致Rust作为底层架构的开发语言时,使用的内存空间要远多于C语言,粗略估计,实现同一种功能的情况下,Rust平均需要比C至少多1倍的栈内存空间。在时间效率上,Rust要实现与C语言相同的时间效率,编写的unsafe代码定然不少,对于开发人员的要求也必然不低,在这一点上,Rust相较于C可能并没有提升太多的编程效率。因此,使用Rust与C编写底层代码各有千秋,Rust定然无法取代C,而C也必然要接受Rust的发展。当前,Rust底层生态严重不足,导致上层建筑难以跟进,一旦Rust适配了足够多的ISA与外设,将大大方便底层中较高级别的编程开发,但对于MCU等内存需求、效率需求高的场景,C依然是不可替代的选择。

在此附上笔者的blog网址,其中有之前编写的9篇Rust逆向分析blog:网址界面丑了点,不要在意

根据笔者对于Rust反编译的研究理解发现,Rust的各类结构具有一定的特征,在进行调试时,如果能够识别出此类特征,就能将汇编代码与源代码相对应,从而大致恢复源代码的执行逻辑,甚至直接逆向还原出源代码。正是因为笔者对Rust汇编的理解,在本次实验阶段二中的调试才能更快地找到问题所在。

rCore 总结

实验文档的第一句说:从零开始写一个内核,但后来发现并不是让学生真的从零开始写,只是在写好的内核上添加功能,有一点点失望,但还是学到了很多。

本次实验让我学到了:

  • Rust 的一些编程技巧,如 UPSafeCell。
  • RISC-V 的特权级机制
  • 使用上下文保存/恢复来进行多道程序调度
  • SV39 多级页表机制及在内核中的实现
  • shell 的原理:fork + exec
  • 文件系统的原理:inode + …
  • 并发同步互斥的原理

虽然没有感觉理解得非常深入,但感谢 rCore 带我在操作系统领域入了门,让我对之后的阶段有更多期待。

Rustlings 总结

在本次训练营之前都是写的 C/C++,也接触过一点 Rust,读了点官方教程,尝试用它写过几道力扣,感觉非常的痛苦,后来就没有再用了。

这次训练营第一次知道有 Rustlings 这个东西,通过 100 多道题来慢慢带新手入门,合理的难度设置加上之前的一点基础,让我基本熟悉了 Rust 的使用。

现在已经能够熟练编写 Rust 基础代码,使用所有权、泛型、Trait 等特性。但是标准库和各种高级用法如宏定义等还不太熟悉,用来完成 rCore 是够用了。

这次训练营的确让我喜欢上了 Rust,写代码不用考虑悬垂指针、内存泄露,以及优秀的包管理,写起来真是太爽了。期待以后使用 Rust 进行更多的项目!

总结

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

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 从队列中取出任务进行调度。

文件系统

目录项

目录项

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 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 接口的要求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 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,
)
}
}
}