0%

第二阶段总结

学习感受

  • 在参加rCore训练营之前,对于操作系统的相关知识我是比较模糊的,在平时的编程中往往只是简单用到一些比如协程、多线程、原子计数、互斥锁等等编程语言提供的编程模型,对于其内在实现基本一知半解,而在这次阅读了rCore相关教材之后,对这些函数背后所隐藏的机制有了更深的了解。在训练营的过程中,因为之前有用Rust经验,所以第一阶段Rustlings能够比较快速的解决,但是到了第二阶段,因为对于相关知识比较陌生所以做的比较慢一些,unsafe Rust、risc-v汇编这些平时接触非常少,所以在阅读相关代码的时候要查阅较多资料。目前虽然完成了第二阶段晋级的目标,但是感觉前方未知的领域更加宽广。
  • rCore依托于Github的授课方式也让我大开眼界,第一次知道Gihub Classroom这个服务,老师们围绕OS的学习建立的这一整套学习方式(Github Actions、排行榜、直播课网站),不仅仅局限于书本,把计算机专业和教学的完美结合到一起。同时rCore的社区感觉也已经很庞大了,已经有许多人做出了自己的贡献,虽然我目前还没有厉害到可以给rCore提PR,但是希望以后可以。

学习总结

  • 其实我学习Rust的开端比较早,就是看到各大视频网站有很多关于Rust如何安全的宣传视频,当时也没有适用场景和刚需,单纯出于好奇就开始了自学,在学完之后也就仅限于用来做一些比如Advent of Code的编程题,但是其间经常由于Rust严格的检查而被Rust折磨🤣。rCore把Rust用到了系统级的编程领域,难度更是加大了,因为要理解这些代码,首先要理解其背后隐含的OS设计思想,这些就是我对于Rust学习的感受。

Lab总结

Ch3 Lab

第一次做OS相关Lab,在起步时熟悉整个代码框架是比较重要的,这样在做题时才能找到相关的接口,Ch3 lab主要关于task info的获取,需要编写函数来获取每个task的相关信息。

  • 为TaskControlBlock增加field

    • start_time: usize
    • syscall_count: [u32: MAX_SYSCALL_NUM]
  • os/src/task/mod.rs

  1. 为TaskManager实现syscall_count_increment()
    主要逻辑:
    • 使用match来匹配syscall id,从而决定增加conut的对象。
    • 并在trap_handler中调用。
  2. 在run_first_task()中使用get_time_ms()获取当前时间
  • os/src/syscall/mod.rs
  1. 为TaskInfo实现reveal()
    主要逻辑:
    • 计算运行时间
    • 从Task Control Block中获取task info
      在sys_task_info()调用

Ch4 Lab

在初读关于内存空间的内容时经常一头雾水,各种术语经常忘了意思,需要经常翻速查表、文档和笔记。
因为地址空间的跳转,所以之前的trap的实现也要修改。

  • os/mm/memory_set.rs
    • mmap
      • 将vpn与ppn关联,从遍历整个VirtualPageNum,若已存在对应pte,则返回失败,若不存在则分配ppn
    • munmap
      • 将vpn从page table移除
  • os/syscall/process.rs
    • sys_get_time()
      • 用 translated_byte_buffer 将用户地址转换为对应的物理地址,然后读取TimeVal大小的内存
    • sys_task_info()
      • 同理先转化为对应物理地址,然后读取TaskInfo大小的内存,就可以获取task_info

Ch5 Lab

Lab 5实现spawn和stride调度算法,spawn一个子线程需要创建一个新的TaskControlBlock,然后再push到当前任务下

  • os/syscall/process.rs

    • sys_spawn()
      • 首先获取app data(get_app_data_by_name), 再根据data生成新的task
    • sys_get_priority()
  • os/task/manager.rs

    • 实现stride算法
      • 对TaskManager的ready_queue进行迭代,使用min_by_key()找到当前最优先的TaskControlBlock

首先得说,rCore-Tutorial-Guide-2023A教程真的太棒了,我敢说,这可能是我遇到的最清晰明了的操作系统教程!

回忆起第一次听到rcore训练营的经历,那是在2023年的Rust大会上,李明老师的精彩演讲为我打开了新世界的大门。尽管我主要在应用层开发上浸淫,但Rust的魅力让我难以抗拒,所以,当知道2023秋季的rcore开营消息后,我和好友毫不犹豫地跃跃欲试。

rustlings部分我觉得挺熟悉的,之前已经玩转了好几回。但真正进入第二阶段,我有点像被冻住的鹿在大灯下,感到前所未有的压力。工作和时间的双重压迫让我迟迟无法启动,但最后的决心驱使我开始啃这块硬骨头。哗啦哗啦地一页页翻,直到lab1的系统调用实现,我才有那种“原来是这样!”的豁然开朗。这也让我真切体会到,教程的设计对新手真的非常友好。

再往后,lab2就开始考验我的耐心了。我承认,我对sys_get_timesys_task_info的实现确实走了点弯路。原先,我采取了translated_byte_buffer来复制数据,试图仿照sys_write。但和友人交流后,我发现其实可以直接通过用户虚拟地址查找物理地址来进行数据复制,真是巧妙极了!同时,mmap和munmap的相关内容,虽然看似复杂,但仔细读一读,原来也不是那么的玄乎。

到了lab3,真的觉得有点上瘾了。这部分主要是spawn的实现。参考了exec和fork,再结合man posix_spawn的文档,整个流程其实很流畅。至于stride调度算法,哦,那真是让人爱不释手。一个简洁的lambda表达式,搞定!虽然说,我的实现可能有点粗糙,但它确实工作得很好。这也提醒了我,在软件开发中,有时候“简洁”和“完美”之间需要做出平衡。这也是我接下来要努力提升的方向。

虽然我对这三个实验的时间安排有点后悔,但回首这次训练营的经历,真的获益匪浅。对于未来的实验,我更加期待,希望能够带给我更多的启示和惊喜。

总之,这次短暂的OS训练营给我留下了深刻的印象。高质量的教程、热心的群友,真的是让人难以忘怀的体验!

前言

偶在某个群里听说了开源公开课的存在,讲OS就带你实现一个小型OS,讲计网就带你实现计网的5层架构。作为一个对底层感兴趣,又忍受不了学校课程的照本宣科的人,心想这样的课程也太有意思了吧,于是开启了我自学公开课的旅途。

关于开源操作系统训练营

说来很巧,在我将要把6.s081刷完的时候,在相关互助群里听说了这个开源操作系统训练营,于是自然地抱着进一步学习的想法就报了名。现在看来这门课确实符合我的期待:

  • 和学校课程最大的不同也是开源课程共同的优点,不会单讲理论,而是结合实践真正地实现一个能够使用的程序
  • 能够学习更加现代化的rust语言,其中的包管理器和所有权确实很实用
  • 由于xv6和rCore都是基于riscv的,很多功能的实现方式都是差不多的,所以学习难度很平缓,又能巩固之前所学
Read more »

契机

在学长介绍下看到了有操作系统训练营,想系统学一下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中,维护一个单调递增的队列。