前言 我是来自华中科技大学计算机科学与技术专业的一名本科生,课堂上做过类似的操作系统实验,但当时忙于其他事情,只是草草应付了事,所以想趁着这个机会,重新详细认识一下操作系统的基本逻辑,也学习一下rust这门语言。
第一阶段 主要参考资料:https://course.rs/basic/intro.html
之前日常学习都是c和c++写得多,习惯了各种指针等非常自由的操作,遇到rust确实非常不适应,感觉编译器时时刻刻都要和我作对,我明明知道这么写没问题,但编译器就是不让。 但是随着我对于所有权、借用引用、生命周期这些核心概念的了解,慢慢我也体会到了rust这种“安全编程”带来的好处(后面写操作系统实验时,也比之前课堂上用c写bug少了很多)。 此外,“万物皆是模式匹配”的思想,也让整个编程风格看起来优雅了许多。
第二阶段 Lab 1 一开始接触这个系统,主要还是不太了解rust的rs文件之间是如何包含的,在引用一个外部模块时,实际上这个查询路径是怎样的。
如果是在c语言中,就是很自然地,只要有对应的PATH环境变量,根据对应的相对路径去include就可以了,但在rust里面,是通过各个目录下一个名为mod.rs的文件去形成一个文件树的,各种文件以什么程度可见,都由mod.rs控制。
具体见:https://skyao.io/learning-rust/docs/build/module/rust-module-system-explanation/
核心在于这句话:
当执行mod my_module;则编译器可以在同一目录下寻找到 my_module.rs 或 my_module/mod.rs 。
Lab2 在这个实验被一个小问题卡了很久。
1 2 3 4 5 let start: usize = 0x10000000 ;let len: usize = 4096 ;let prot: usize = 3 ;assert_eq! (0 , mmap(start, len, prot));assert_eq! (mmap(start + len, len * 2 , prot), 0 );
上面的代码会在第二个mmap是触发如下错误:
1 2 3 4 5 let pte = self .page_table.find_pte(start);if pte.is_some() { println! ("conflict_vpn: {:?}" , start); return -1 ; }
当时百思不得其解,自己造了一些其他测试,发现下面这样的操作居然不会报错:
1 2 3 4 5 let start: usize = 0x10000000 ;let len: usize = 4096 ;let prot: usize = 3 ;assert_eq! (0 , mmap(start - len, len, prot));assert_eq! (mmap(start, len * 2 , prot), 0 );
这两个测试有什么区别,我仔细思考了一下虚拟地址转换为物理地址的过程,怎么使用多级页表一步步得到最终的物理页,结合这个如此“整数”的start,我发现了它们的区别很有可能在于,第二个测试,两个请求会处在不同的子页表中,而第一个测试则都在同一个子页表。因此,我终于开始详细看find_pte的流程,发现它是只要找到对应页表项就会返回,不会判断该页表项是否Valid。
那么此时,如果某个子页表已经分配,只是对应页表项不Valid,那么conflict的判断就会失误。
这个故事告诉我们:不要主观臆测一个函数的用途,在使用一个函数之前,一定要确切知道它的输入输出表示什么意思。
此外,也不应该使用find_pte函数,本来这个函数就不是pub的,有一个判断了是否Valid的封装好了的函数供使用。
Lab3 这个lab挺简单的,就看懂fork和exec的执行逻辑之后,找到那些二者都改了的数据,只执行exec的对应改动,减少无意义的赋值。
stride算法,我使用了一个溢出标记的方法来防止溢出,即计算某个进程的stride值加完之后溢出了,就把该进程标记一下,之后都不考虑调度它,直到所有进程都溢出了。其实这样徒增了很多计算量,问答作业中的方法其实才是比较好的解法:
思想大致如下,没实际运行过,可能有一些细节上的问题:
1 2 3 4 5 6 7 8 9 10 11 impl PartialOrd for Stride { fn partial_cmp (&self , other: &Self ) -> Option <Ordering> { let a = (self .0 & 0xff ) as u16 ; let b = (other.0 & 0xff ) as u16 ; if (a > b && a < b + 255 / 2 ) || (a < b && a + 255 / 2 ) < b { Some (Ordering::Greater) } else { Some (Ordering::Less) } } }
Lab4 这个实验做的时候遇到很多多重借用的错误,一个比较好的编程习惯是,在函数A调用一个函数B之前,先看看B里面有没有对什么进行了借用,如果有,注意先在调用之前把对应的借用drop掉。
Lab5 银行家算法,增减数据的时候记住,allocated + remian = all,有bug的时候就多看看这个规则有没有违背。以及记得在获得对应数据之后减need。
总结 这些实验确实都聚焦在了操作系统中基本且核心的问题,而且架构也是循序渐进,让人能够慢慢了解虚拟化、进程线程、文件系统这些都是怎么一步步得来的,感觉受益匪浅。