0%

契机

在学长介绍下看到了有操作系统训练营,想系统学一下Rust与操作系统,也想实操拓展一下自己在操作系统上的知识因此加入到了训练营中

第一阶段

之前使用Rust编写过一些程序,这个阶段过的稍微轻松一些,但也遇到了一些之前没有仔细阅读思考的地方:

  1. 智能指针之前只了解了一些常用的如Box,Arc。其他的指针暂时不清楚如内部可变的智能指针如RefCell之前都是靠Mutex或者SpinLock来解决的,在学习中也了解了这些指针以及学习了一下Rust智能指针的内存布局,有一个视频我觉得讲的还挺好的
  2. 生命周期。Rust对生命周期的隐藏太好了,基本上开发中都不会用到,因此我没怎么学习这方面知识,开发中比较排斥在结构体中使用引用,因为要涉及到生命周期标注。这一次也是好好学习了一下

第二阶段

之前看过rCore的文档和代码,这一次回顾的时候也发现了之前漏看或者以为自己看懂了的地方,最明显的是中断那一部分,之前看的都挺迷糊,也有现成代码就胡乱略过去了,实际上没怎么看懂具体流程以及中断的代码实现,现在再次看能更全面的了解RISCV上的中断机制。还有rCore上的文件系统也是接触较少的一部分

ch3

ch3只用在TCB里面加上一些数据即可,难度不高

ch4

涉及到内存管理了,这一方面难点主要集中在map和unmap的边界处理上,但多画图注意一下也是可以过

ch5

主要实现spawn系统调用,创建一个空白子进程然后加载对应程序即可,

ch6

需要对文件系统进行修改,原本想在unlink的时候缩减根目录所占大小,但试了一下感觉较难,采取了一个取巧的方式,把目标DirEntry直接清空了。实现获取文件信息时刚开始也把一些信息给硬编码了,但后来熟悉了一下文件系统后,又把这些给接上文件系统信息了,也算是一个自己不太好的地方,容易取巧。

ch8

涉及到死锁检查,难点主要是在信号量死锁上,需要预防资源循环依赖

总结

第二阶段将系统中的部分小模块拆分开,难度不会太高的前提下也能让人感觉到系统功能在一步步变多,正反馈还是挺多的,一个个test也能让人产生动力去添加功能,并找到代码的问题,这次编写的过程学习补充到了一些知识及代码实现,就是有一些功能不需要自己添加,比如直接跳过了中断,虽然中断代码一般固定后就不会修改了,但加上一些可能会让人印象更深刻一些。

Lab1

编程题

获取任务信息

思路:题目要求查询的信息都是全局持久化的,因此需要在TaskControlBlock中添加这些信息。

task_status已经存在,不需要添加

task_syscall_times需要在sys_call函数中拦截,根据系统调用的id进行桶计数即可

task_runtime比较麻烦,有两种思路:

  1. 保存任务第一次调度的时间,在sys_task_info调用时将当前时间和第一次的时间相减(我的做法)
  2. 分别记录任务在用户态和系统态下的时间,在sys_task_info调用时将两者相加。(此思路源于参考)
    1. 内核态时间.在 run_first_task 以及 mark_current_exited, mark_current_suspend 中更新信息, 另外需要在 task 退出时打印耗时。
    2. 用户态时间.用户态和内核态的分界处就是 trap, 因而在 trap_handler 的起始位置和末尾位置可分别作为 user time 的开始以及 user time 的结束。

Lab2

编程题

重写 sys_get_time 和 sys_task_info

由于内核和应用地址空间的隔离, 系统调用 不再能够直接访问位于应用空间中的数据,而需要手动查页表才能知道那些 数据被放置在哪些物理页帧上并进行访问。

可以参考文档中sys_write的重新实现。基本原理就是使用函数translated_byte_buffer可以将应用地址空间中一个缓冲区转化为在内核空间中能够直接访问的形式,然后再进行操作即可。

指针的copy操作可以使用core::ptr::copy_nonoverlapping

在taskinfo踩了坑,同样的代码可以过ch3的测试却没法过ch4的测试,因为使用了get_time_ms函数,有一些精度问题,后来改成get_time_us() / 1000 即可

mmap 和 munmap 匿名映射

mmap函数的作用和签名可参考
参考一
参考二

比较麻烦的是要弄清申请页和释放页边界,统一使用左闭右开的范围。检查范围和权限是否合法 使用 基本的数值运算或位运算即可;申请空间和释放空间要求 理解memset和pagetable的api作用,合理地使用即可

在样例ch4_umap卡了很久,因为当时mmap和munmap申请内存的范围边界没有理清,注意测试样例有一个地方申请了 PAGE_SIZE + 1的空间,这个申请应该向上取整,申请2页内存

Lab3

编程题

进程创建

实现spawn系统调用,spawn函数的作用简单来说就是创建一个子进程,并在子进程中加载执行传入的可执行文件。通过它的作用我们可以意识到它其实是fork和exec两个函数的结合,因此spawn的具体实现就可以在fork和exec函数里左借鉴右借鉴,这就是——拿来主义。

  1. spawn会根据传入的文件名,解析elf文件,获取elf文件的地址空间、用户栈等。和exec类似
  2. spawn会创建新的子进程,需要申请新的pid和内核栈。和fork类似
  3. 根据解析exec得到的信息以及父进程的信息初始化 子进程的TCB和Trap Context。和exec、fork类似
  4. 将新的进程加入队列

stride 调度算法

这题相对比较简单,根据题目要求为TCB加入pass、priority、stride字段即可,BIG_PASS随便设置一个比较大的整数就行。
需要修改的函数就是获取下一个执行任务的函数——fetch函数,之前采用的调度策略就是先进先出。stride算法的实现根据题目要求写一个for循环寻找stride值最小的task即可。

引言

我是在某 912 考研群里看到群友发了这次操作系统训练营的宣传图才进来的,事实证明,这是一次非常正确的决定,因为确实挺有趣的,一方面有了个在专业指导下实际上手折腾操作系统的机会,另一方面也确实见识到了挺多东西。

顺便,我还拉了几个朋友进来一起,可惜他们都比较忙,只是堪堪过了一阶段,在二阶段时正好与他们的考试周和大作业冲突了,于是在二阶段折戟沉沙,只能等下一次训练营时继承这次的存档继续打了(

感悟(正文)

操作系统训练营,那自然是以深入了解操作系统为目的的,若是要展示自己在这方面的感悟,那么我想,我可以通过使用伪代码来描述一个简单操作系统所做的事情,以此来反映我在这次训练营中所学到的。

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
struct SimpleSYSTEM{
pub hardware_resources:RefMut<HardWareResource>, //作为一种特殊的软件,操作系统同时对用户的软件与硬件进行操作,在大多数场景下,用户并不会直接的操作硬件,而是要通过操作系统来交互。
pub state:OSSTATE, //万物都是状态机啊状态机
}

enum OSSTATE{
//首先,很显然的,泛泛而谈的来说,操作系统要么在处理自己的数据,要么在处理程序的数据
OS_OPERATION(OS_OP_STATE),
RUNNING_TASK,
}

enum OS_OP_STATE{
//那么操作系统在不允许用户程序的时候会做什么事呢?
SELECT_AND_SWITCH_STATE, //这个状态表示控制权刚刚回到操作系统手上,这是需要操作系统来决定下一步做什么。
MANAGE_MEMORY,//由于虚拟内存的存在,一切的数据,不论在什么地方,最终都会映射到内存中,可以说操作系统做的只有两件事:操作内存与操作硬件,既然如此,操作系统也得负责管理内存
HANDLE_INT, //中断,是硬件给予操作系统反馈的桥梁,有了它我们才能更方便的建立硬件与软件的双向链接
}

impl UseAble for SimpleSYSTEM{
pub fn iter(&self){
loop{
match self.state {
OS_OP_STATE(operation) => match operation{
SELECT_AND_SWITCH_STATE => {
//首先,检查自己是如何进入基础状态的,也就是说如果发生了中断,我们需要第一时间处理,因此我们如此编排顺序:
if self.hardware_resources.interrupted(){
self.state = OS_OP_STATE(HANDLE_INT);
continue; // 进入下次循环,下次循环就会转到处理中断的操作。
}
if time_to_manage_mem(self.hardware_resources){ //除了被动的申请/释放内存,操作系统也需要时不时主动的去检查各个进程的内存空间以及整体的内存空间,以及时发现问题。
self.state = OS_OP_STATE(MANAGE_MEMORY);
continue;
}
self.state = RUNNING_TASK; //没有任何异常,那么是时候运行一会程序了!事实上这才是大多数时候的情况

}
MANAGE_MEMORY => {
manage(self.hardware_resources.managed_memory());//管理内存,检查错误并修正错误
self.state = OS_OP_STATE(SELECT_AND_SWITCH_STATE); //回到基础状态
}
HANDLE_INT => {
handle(self.hardware_resources.int_info()); // 根据硬件请求信号做出回应
self.state = OS_OP_STATE(SELECT_AND_SWITCH_STATE); //回到基础状态
}
},
RUNNING_TASK => {
(addition_attr,operations_collection:address) = generate_operation(self.hardware_resources.cache); //为什么是address?因为程序也存放在内存中。
actual_operation_stream:RefCell<dyn [operation]> = setTimer(self.hardware_resources.timers,operations_collection) // 这里不一定得是字面意思上的Timer,也可以是某种约束,比如我们所约定俗成的,放置在程序栈底部的返回地址,放置一连串操作在等,都可以算是对于程序运行时间的干涉。
self.state = OS_OP_STATE(SELECT_AND_SWITCH_STATE); //运行一段时间后操作系统提前在operations_collection/硬件计时器中埋下的操作会触发,将运行资源交还给系统,也就是说会重新进入这个大循环,因此我们可以在这里提前做好状态的设置。
//于是经过了处理后,我们得到了真正的一串精心调整过的操作序列;
self.hardware_resources.load_operations(actual_operation_stream); // 在这里,操作系统的工作暂时会停止,直到这段操作序列运行结束,或者运行时出现了情况,触发中断。
}
}
}
}
}

如上,这就是在我脑海中操作系统所做的事情,当然,这只是个泛泛而谈的框架,其中每个分支中所进行的操作都包含着值得深入学习的问题,比如 MANAGE_MEMORY 的情况下,我们可能需要进行进程资源的检查,以发现死锁/僵尸进程的情况等,学习这些内容就是参加训练营的目的;

日程

  • 第一阶段:rust 入门 & rustling: https://github.com/LearningOS/rust-rustlings-2023-autumn-dbydd

    由于我在此之前就自学过 rust,因此第一阶段对于我来说比较轻松,每天抽点时间刷一下 rustling 就能愉快的完成任务了,事实上我在第三天时就已经把 rustlings 刷完了。因此这个阶段实在是没什么好说的…

  • 第二阶段:rcore 实操训练:https://github.com/LearningOS/2023a-rcore-dbydd

    在我的印象中,rcore 是 ucore 的后辈,两个操作系统都是由清华大学的师生开发出来作为操作系统课的教学工具的;这不正巧,由于下定决心明年要冲 912,我在暑假的时候就先刷了一遍清华的网课,其中就包括了陈老师的操作系统课程,刷完后也瞄了两眼 ucore 的实验部分,由于脑中的知识很新,因此对于实验到还算是得心应手,反而大部分时间都是在与 rust 的语法做斗争.但是不巧的是也遇到了考试周,此外还要准备校内计算机社团的讲课工作与数学建模的备赛,就算是我也感到了分身乏术,进度也就这样被拖慢了下来,直到我写下这段文字时(11.1),也只是将实验推进到了 ch6,好在后续又有通知,这次的二阶段只要完成前三个实验就能进入三阶段,剩下的两个实验在三阶段补齐就行.这样一来或许我也有机会一命通关?整挺好.

    • 二阶段开始-2023.10.25 做完了 ch3

    由于是第一个实验,难度并不是很高,只需要给进程附加上一些信息记录,修改很少的代码就能完成。全过程思路如下:首先是要对进程做操作,因此应当去进程相关的代码下做修改。TaskInner 记录的是进程的运行时的程序部分,在这里添加自己的信息不是很合适,于是只剩下了一个可选项:在 TaskControlBlock 中塞东西。

    • 2023.10.25-2023.10.31,做完了 ch4

    ch4 主要是关于内存的管理,这部分可以说是一个操作系统的 1/2 个核心部分,在我看来最精简的操作系统,其实就是一个工作在内存上的输入输出机:输入当前的内存,输出特定长度的操作序列至某一片连续内存,并将当前 cpu 的 pc 指向哪个内存的首地址。当那一片连续内存执行完后就回到内核态(也就是继续由操作系统进行一次输出),如此反复…

    总之,我们只要实现一个虚拟内存的申请和释放就可以了,这部分主要的难点就是可能会一次申请/释放一大片内存,从而需要在多个页表项上进行操作,不过实际实现起来只需要稍加思索就能写出很自然的代码。

    • 2023.10.31-2023.11.1 做完了 ch5

    ch5 主要是对进程的管理,自很久以前时分复用的概念被提出开始,任何操作系统都会支持多进程的操作,这甚至不是一件需要特意提及的事情。问题在于:我们要如何设计一个合理的进程系统,并不出差错且用得顺手?

    实验部分有两个,一个是实现 spawn 方法,实验指导文档上给出了标准的 posix_spawn 方法的参考链接。一言以概括之:spawn 方法就是创建子进程用的,那么既然知道了要做什么,怎么做也就非常的显而易见了。

    另一个任务是实现一个简单的进程调度算法:stride 算法,这个算法的想法很自然:给进程加上动态变化的权值,如果一个进程刚刚被执行过,那么它应当不会在短期内被连续执行,除非他的优先值很高,能让他进行连续的执行。

    *未完待续

2023开源操作系统训练营第二阶段总结报告-baobaomaomeng.md

2023rCore训练营第二阶段总结

lab1

要求我们实现一个sys_task_info的系统调用,用于获取进程的信息。主要关键点在于如何获取task运行时间与系统调用次数。

这要求我们在TCB结构体中添加两个参数,一个记录系统调查的次数,一个记录运行时间。

运行次数在每次调用syscall时增加,所以直接根据调用syscall的id参数增加对应位置数组的值即可

运行时间要求我们记录第一次运行的时间,所以我们在切换记录时判断它是不是未被记录时间,然后记录它调用时间,当查询时候返回当前时间和起始时间差即可

lab2

要求重写sys_get_time 和 sys_task_info两个系统调用。

由于我们引入了虚拟存储机制,所以要把数据写到任务地址空间需要根据我们的页表得到其实际物理地址再写入。mmp与munmap都已在memroy_set中帮我们实现,我们只需要简单封装一下即可,同时我们要注意访问权限的管理

lab3

首先要迁移lab2中的实现,这一部分建议手动完成。

先讲进程调度,这个非常简单,push进队列的时候寻找位置插入即可,BIG_STRIDE设置为1e9+7

spawn,调用接口把父节点的数据拷贝即可,测试用例挺弱的

这是我第一次参加这种形式的训练营,感觉相当于收获而已,这种体验更让我有强烈的兴奋感,希望借此机会多接触开源,在开源社区中不断精进自己的技术。
在lab1中,利用结构体TaskInfo来追踪一个进程的任务状态,分别调用了哪些系统调用,以及对应的次数,距离任务第一次被调用的时长,然后来完成sys_task_info,这个相对简单,只要了解系统调用的大致流程就行,知道add_syscalltimes要放在syscall函数体内,知道大致代码框架就能写出来。
但是最重要的是,问答题检测了你对于特权级切换的理解,这里才是本章的重点和难点。利用汇编语言操作寄存器。
在lab2中,先是要重写sys_get_time和sys_task_info,因为一旦引入虚存机制以后,就需要将用户态的虚拟地址根据当前的进程页表来转换成实际的物理地址,
这里需要去调用获取用户根页表地址的接口,然后解引用指针修改就可以了,
接下来mmap和munmap匿名映射,除了一些错误要特殊判断以外,主要是利用insert_framed_area函数,在一些循环判断逻辑段是否有效(find_pte函数)就能解决。
在lab3中,sys_spawn主要看fork+exec来仿照写,这里不必像fork一样复制父进程的地址空间,是因为exec本身就要替代原进程(包括地址空间),没必要了。
然后stride调度算法,首先在TaskBlock添加优先级和stride,首先要把所有
初始化和要更新的加上一个stride更新,其次在task.add中,维护一个单调递增的队列。

2023rCore 训练营第二阶段总结

其实很早以前就知道 rcore 这个项目了,之前学 rust 时做过 Stanford 的 CS 110L
不过由于之前做过 MIT 的 xv6,在操作系统 lab 方面定位有些重合,就没有去做 rcore。

在今年暑假想要找些 rust 项目看时,在 github 上找到 arceos,后面才知道 rcore 只是社区众多项目中的其中一个,这次报名也是希望能进一步参与后面的项目。

实验

rcore 的实验其实挺简单,重要的是理解系统概念以及现有的 api,这一点其实和 xv6 有点相似。
然而这也对应了另一个问题,只做实验其实不一定有很好的学习效果,有可能只根据类型试着调用 api 就能通过测试,学习的深度某种程度上只能靠自觉来保证。

另外,感觉各个 lab 的测试样例并不算强,其实编写 kernel 是很容易在 corner case 上出各种漏洞的,希望自己的实现没太大问题。

由于 rust 本身的约束,产生漏洞的风险比 c 要小,但是在使用 unsafe 时,由于 rust 本身做了更多抽象,直接接触底层时就会有更大的潜在隐患。

比如在 sys_task_info 中,需要将 &TaskInfo 转换为 u8 的 buffer,我使用了 as 来直接进行转换

1
2
3
4
5
6
let k_ti = &TaskInfo {
status: get_task_status(),
syscall_times: get_task_syscall_times(),
time: get_time_ms(),
} as *const TaskInfo
as *const [u8; mem::size_of::<TaskInfo>() / mem::size_of::<u8>()];

转换为裸指针是 safe 的,但是访问裸指针时很容易触发未定义行为,unsafe 里有很多隐秘的 unsound 情况。

实际上,对于 TaskInfo 数组:

1
2
3
4
5
6
7
8
9
/// Task information
pub struct TaskInfo {
/// Task status in it's life cycle
status: TaskStatus,
/// The numbers of syscall called by task
syscall_times: [u32; MAX_SYSCALL_NUM],
/// Total running time of task
time: usize,
}

其内存布局是不确定的,编译器甚至不保证 statussyscall_timestime 是按顺序排布的,添加 #[repr(C)] 才能保证会是我们想象的类似 C 语言的内存布局。

同时,这个结构体中的元素比较简单所以没问题,但是直接裸指针转换是很不好的风格,在面对内部可变性、内存对齐、未初始化元素时极易访问未初始化内存,对未初始化内存的任何写入和读取都是UB

bytemuck 提供了对于类型转换的更好方式。

类似这种,其实很有多潜在的需要解决的问题都被忽略了,实验中保留了比较核心的概念和问题,但是仔细挖掘文档和现有的代码的话,会有更多的值得学习和思考的问题。

总结

rcore 是个很好的 rust 和 OS 项目,国内的开源教学项目实在很少,大部分学生都会去学习 MIT、CMU 等学校的开源课程,不过国外的英文课程对国内学生有一定门槛,会有一些额外的负担。
我之前写过的 Stanford 的 rust 课程在 20 年后的版本都不公开代码了,难免说开源的行为会持续多久,所以国内还是很需要这种优质的开源课程来弥补空白的,在此感谢各位老师和助教的付出,也希望未来在各个方面能有更多的优质开源课程。

楔子

第一次看到这个训练营是在QQ频道上面的rust频道, 正巧当时有人在说OS训练营的问题, 正巧也确实对实现操作系统很感兴趣就参加了。 在此之前其实已经有rust的基础和使用rust开发项目的基础。

关于rust

rust是我非常喜欢的语言, 可以说是目前最喜欢的语言也不为过。诸如C, C++, java, python, go, kotlin, js这些语言我都学过, 也用它们或多或少的写过东西, 但是其中rust是最深得我心的。

比如说它的枚举中可以存放关联值, 这样就可以通过枚举的类型来进行分发; 还有它强大的泛型能力, 在学习rust之前其实我对泛型编程的理解一直不太深, 直到我学习了rust, 参考了rust标准库的源码, 对泛型的理解才更上了一层。

一阶段的许多东西都在圣经里面有所提及, 但是多出的那几个test确实是我不曾知道的, 后面我又去了解了一些rust插件开发, 对于有反射的语言, 可以使用反射动态读入来进行插件化。 而没有反射的诸如rust这样的编译型语言, 可以利用FFI进行插件化, 也就是把rust打包成动态链接库, 比如说用#[no_mangle]来阻止混淆函数名称, 然后通过调用库的方式调用链接库中的函数, 这样就可以实现插件化。 毕竟不能添加用户自定义的插件什么应用是完整的呢?, 所以一阶段确实是对我也很有帮助的。

关于二阶段

一阶段我用一个晚上速通之后就开始看文档, 也大概花了两周时间。

对于这个阶段我最大的感想就是, 理论需要与实践相结合。 在学习也学过操作系统这门通识课程, 当时也觉得课程里面说的什么比如特权级, 中断, 分页分段之类的学习起来并不难, 但是要是让我用代码来实现, 那必然是抠破脑袋都想不出来的。 直到现在我虽然是把所有的实验都完成了, 文档也看完了, 但是如果要我独立写一个操作系统, 也不一定可以。

我看的主要是v3的文档, 于我而言最有难度的是第四章和第六章, 分页管理和文件系统, 真的和其他章节不是一个难度。 不然即使如此, 要做完实验并不需要完全理解文档, 实验相对来说还是比较容易的。

关于各个实验

ch3

ch3是实现获取taskinfo的系统调用, 主要想法就是在运行过程中维护调用计数。 第一次遇到了爆栈空间的问题, 所以就给entry.asm的栈空间调大了, 事实证明也确实可以

ch4

ch4实现sys_mmap的方面, 我是通过参数分配对应的页表, 生成一块MapArea然后插入到MemorySet里面, sys_umap也只需要从MemorySet里面删除

ch5

ch5实现sys_spawn, 我觉得它与fork + exec的区别就是不用复制父进程的空间, 那么生成一块空白的空间按照exec处理即可

ch6

ch6实现sys_linkat, 在索引中加一个entry即可, 而sys_unlink我认为不容易删除一个entry, 我选择的是向里面写了一些非法数据, 让文件名读取不到这个节点

ch8

ch8实现死锁检测, 银行家算法需要提前获知需要的资源总数, 对于互斥锁我想资源数量就是1, 对于信号量就是对应的值

总体

rCore逐步递进, 从一个功能较少的小系统成为一个功能较多的大系统, 但是我感觉文档里面还有很多东西没有说的很清楚, 比如说rust函数和汇编中函数的互相调用, 参数和返回值传递之类的不是很明白。 除此之外文档还是非常全面的, 我感觉我主要的问题就是不太理解汇编和rust的交互。

另外就是在调试方面有一些问题, 不容易以代码级的方式调试, gdb调试也不太直观。

2023秋冬rCore训练营第二阶段总结

以“进化”的形式来讲述操作系统确实很有意思,同时能让了解到一些os的历史(但代价是什么:每章都要合并代码啊喂

Lab1-ch3

这个syscall还是很直观的,加一些标志然后相应写一些函数就行了。(没想到几行代码合了三回
但是直接给个大数组感觉还是太粗糙了,拷贝一下感觉开销也不小的样子,但是也没想到其他好办法。
简答作业要求理解trap的过程,细读,细读。(写过pa的看到这个有种亲切感

Lab2-ch4

由于rcore用了双页表,内核代码也没法直接掏到应用的数据,得手动查一轮;sys_get_timesys_task_info都需要往用户的某个地址写数据,都得重写一下儿(Nanos-lite:陌生的设计

mmapmunmap大都可以复用或者照猫画虎现有api搞出来。

Lab3-ch5

sys_spawn的处理就是新建进程但是要维护父子关系。(文档同时解答了我对于“为什么先得来个fork再exec”的疑问
stride算法也比较形象,和sys_taskinfo一样有想法了就容易实现,简单但是有效√

总结

总的来说二阶段读大于写,理解要干什么比较重要,有想法以后码一码调一调还是比较容易成的。(感觉rust比较难
rust也能保证不会出现太玄学的错误我才没有在ch4乱玩指针导致segmentfault捏

非常好rcore,使我的qemu spin√