这一学期的操作系统课使用的是MIT用于教学的JOS操作系统,并且StonyBrook在其基础上做了大量改动,最重要的变化就是从32位移植到了64位。因为个人之前曾系统学习过Linux 0.11内核(《操作系统内核Hack:(四)内核雏形》,实现到时钟中断部分停下了),深知自己从零开始实现内核的工作量。即便是如我个人实现的MiniOS这种简单的不能再简单的,也是需要花费很多时间和精力的。虽然这些付出非常值得(在上这门课时给我带来了巨大的帮助),但是对于想直接上手的同学来说会很打击积极性。
既然强烈推荐JOS,那它有哪些好处呢?
以下是个人的一点经验,感觉应该比较通用,严格遵守的话能节约很多时间:
所以,个人以为JOS是个非常不错的选择,虽然它的确有的地方有些简陋,与真实的Linux有很大差别,但能系统地做完6个Lab的练习的话,对个人绝对是巨大的提升!本文不会“剧透”任何答案,只是总结一些经验心得和最重要的知识点,大家可以放心观看~
如果本文提到的题目、知识点在MIT的Lab中找不到的话,请搜索Stony Brook的Lab进行学习。
Lab 1的特点是阅读量大,但习题很少,毕竟刚开始还是让大家热身适应一下。不要看文字很多很烦,如果之前没有过内核或者嵌入式开发基础的话,其中一些预备知识还是很重要的,否则后面的Lab你会很痛苦。Lab 1涉及到的预备知识有:Git、Qemu、GDB、Inline汇编、C指针。
最主要的工作就是练习11开始实现backtrace函数的栈打印。之前觉着ebp(rbp)没什么用,真正控制着栈的是esp(esp),甚至像在《CSAPP缓冲区溢出攻击实验(下)》中ebp是错误的也没什么。而在这次实验的最终几道题目中,ebp却发挥了巨大作用,原来esp是在一个栈帧内随着压栈和出栈不断移动,而ebp就像是不同火车车厢(栈帧)之间连接的“钩子”。虽然函数返回时不断出栈,esp最终能够自己正确地返回到调用者。但当我们调试时,例如在GDB中执行bt命令时,是不能改变esp位置去追溯的,这时ebp就该出场了!
关于完成练习所需的代码其实JOS中的DWARF都给了,直接调用就能得到我们想要的东西。但要注意的就是:64位与32位汇编编程的不同!这也是JOS给我的“下马威”。总结一下我做这个最终练习时犯的错误,有些是旧知识忘记了,有些是新知识:
Lab 1只有一个Challenge,就是让输出到控制台的字体改变颜色。要求中介绍了一种通用的控制方法,就是ANSI的ESC序列(Escape sequence),语法是”ESC[Value;…;Valuem”。曾经在设法使MakeFile输出不同颜色字体时遇到过,例如echo -e ‘\033[31;1mHelloWorld!\033[0m’(先修改成红色,输出后立即复位)。所以整体思路是:内核接收到ESC序列字符串后,解析其内容并更改内部的输出模式,于是后续输出的字符就改变颜色。
Lab 2开始难度陡增,个人感觉2和3可能是最难的两个Lab了。也可能是刚开始,一切还都不熟悉,所以感觉比较难。最烧脑的就是虚拟地址和物理地址的转换,以前从来没觉得这块知识很难,但在JOS里这绝对是最难的一块内容!
因为之前在《操作系统内核Hack:(三)引导程序制作》花了几个月时间详细研究了系统启动过程,所以这一部分驾轻就熟,节约了不少时间。不太了解这部分背景知识的同学,可以看一下本人的《操作系统内核Hack》系列文章,写的应该还算比较清楚。要注意的一点区别就是:JOS采用AT&T汇编语法格式而不是比较流行的NASM,详细区别可以参考《Brennan’s Guide to Inline Assembly》,差别其实不大。
第一阶段引导:JOS的启动方式比较标准,boot文件夹下boot.S首先负责读取内存信息,以Multiboot格式存储在multiboot_info位置。这个信息是留给内核后续使用的,要详细了解Multiboot格式的话请参考Multiboot Specification。之后JOS开始加载GDT描述符,并进入保护模式。进入保护模式之后,迅速进入到C环境进行第二阶段引导。这一点比我之前学习的Linux内核要快很多,在Linux 0.11中这一部分的很多工作都是在汇编环境下完成的。也许是出于快点进入C编程环境,降低学生上手的难度吧,毕竟启动部分的确是非常复杂!
第二阶段引导:进入到boot/bootmain()后开始第二阶段引导。最主要的工作就是:从磁盘上读取内核文件,将控制转移到内核代码。关于如何读取磁盘找到内核并加载到内存,就不细说了,因为很枯燥啊,看看Orange’s读取FAT16的代码有多麻烦就知道了。关于找到内核入口的方式,JOS采取的策略类似于Orange’s,解析ELF文件头,找到内核的第一条指令。而Linux 0.11的方式则是将内核编译为纯二进制,并去掉没用的信息,所以内核的第一个字节就是第一条指令。
进入Kernel:因为前面说了,JOS“过快地”进入了C环境,而有些工作只能用汇编实现。“欠下的账”终究要还,所以内核的entry起始部分(kern/bootstrap.S)又再次进入汇编环境。把欠下的账补上后,才能完全进入C语言的世界。而kern/bootstrap.S要补上的最重要工作就是:设置页表,开启分页机制。下面进入本文的重点,页式管理。
谈到内存分配,第一反应就是C语言里的malloc(),以及高级语言C++/Java里的new关键字。可我们现在要写的是系统内核,还没有malloc()库函数(在《操作系统内核Hack》系列里曾讲过,开发内核时是不能随便引用标准库的),更没有高级语言里的new。就在迷茫的时刻,才会思考问题的本质。我们说内存分配时到底在说什么?其实对于内核来说,内存分配就是“随意”地返回一个地址给调用方使用,只要你保证这个地址不被其他人使用,那就是一次成功的内存分配了。所以,我们一般说的不管是JVM也好还是malloc也好,内存分配和释放的消耗其实都是内存管理器复杂管理的代价,如果我们用最最简单的Bump Allocator的话,内存分配的本质真的就像上面说的那样简单、原始!
现在就来看最为头疼的页式管理吧。这绝对是实验二的最大难点了!哪里是虚拟地址?哪里是物理地址?什么时候会发生转换?cr3以及每一级页表里存的是虚拟还是物理地址?GDB打印的又是什么地址?一个个问题搞得我晕头转向。现在终于有一些“清醒”了,就谈谈我对这些困惑问题的理解:
int a = &p
,通过&p得到的地址就是虚拟地址。a[i]
、*p
,这两种形式的解引用都要求变量是虚拟地址。如果是自己手动赋的物理地址,就会导致MMU翻译时找不到对应的页表项而报错。a = PADDR(&p)
。这种转换可行的前提是你知道当前页是如何映射的,即页表内容。这在内核中是可以办到的,也是我们在JOS实验二中做的事儿。但是未来你想要在某个用户进程中得到一个变量实际存在哪里了,这几乎是不可能的,因为操作系统已经对你屏蔽了这些东西。p/x a
看到的就是虚拟地址,而x/10x 0x1000
查看的就是物理地址0x1000位置的内容。如果大家修改到那几个walk()函数时可以会有疑问:为什么继续向下一级页表递归时要将页表地址通过KADDR()转为虚拟地址呢?因为每一级页表的walk()函数处理时都有pml4e[i]、pdpe[i]、pte[i]。不要忘记前面说过的,只要解引用就会发生地址转换!硬件逐级递归时是没有这个问题的,而我们的walk()函数是用软件模拟,所以一定要转为虚拟地址再递归!
Lab 3的难度与Lab 2相当,最大难点就是中断(Interrupt)了。这两个Lab中涉及到的内存管理和中断可以说是最核心的知识,挺过这两个Lab后面就一马平川了!既然中断这么重要,那肯定不是三言两语可以说清楚的,强烈建议大家按照JOS的Instruction去实现,并且遇到问题一遍遍调试。这样整个中断的执行流程就会在你脑海里强化记忆,加深理解。
经过了前面三个Lab的洗礼,Lab 4开始就是按部就班就可以了,个人感觉没有特别难的地方了。Lab 4首先让大家熟悉多核环境,我们要做的就是初始化好多核的运行环境,主要是多个内核栈和对应的TSS配置等。
OS的调度是由一个Timer发起的,引发中断进入内核态后,调用中断处理函数进行处理。因为JOS的中断过程是由BKL(Big Kernel Lock)保证的,所以中断处理函数运行时“整个世界都清净了”。不管有几个核心,此时世界静止,等待我们进行调度,在返回用户态的前一刻才会放开BKL。中断处理函数可做的事情很多,像简单的RR、复杂的类CFS、有趣的Lottery调度等等,大家尽可以根据自己兴趣去实验各种调度方式。像控制了OS的大脑中枢神经一般,控制一切的感觉还是很爽的!
本Lab的第二个核心就是实现拥有Copy-on-Write能力的Fork。要注意的是因为JOS采取的是Microkernel架构,所以Fork是在用户态配合几个系统调用完成的。因为要遍历进程地址空间决定是否要Copy还是Share,所以Instruction中给出了一种貌似很神奇的办法:顺序遍历一个数组。一直理解得不是很好,个中奥秘还是大家自己去探索吧!
因为JOS的Microkernel架构的缘故,在Lab 5和Lab 6中的FS和网络大量使用IPC通信,所以一定要对IPC有清晰的理解才能做好下两个Lab。其实并不难,IPC接收方调用receive()函数挂起自己,发送方调用send()进行数据通信后,内核唤醒接收方继续处理。
Lab 5的核心就是一个:FS。JOS的FS对Linux的VFS进行了大量简化,最重要的改变就是去掉了inode,直接通过dentry管理文件和元数据。个人觉得,这个改动非常不好!一是因为inode真的太重要了,这个改动却导致了我们失去深入理解它的机会。二就是没了inode加上各种数据结构命名的不同,将JOS的FS部分与Linux的VFS对比学习时会产生很大困惑。强烈推荐大家看一下《Linux Kernel Architecture》之后,做一下实现inode的那个Challenge,具体请参见最后一部分。
这是我们的Final Lab,可以选网络驱动、虚拟化和自选题目。因为可能在MIT的原始材料里没有对应,所以就不详细说了。主要目的就是实现一个网络驱动,接收和发送网络包。难度不是很大,但因为是最终的Lab所以给出的Hint比较少,需要认真读Instruction和Intel的手册,总体上还是蛮有趣的!
本节给大家推荐一些我认为非常不错的Challenge,当然了,我没全做出来。因为有的真的工作量挺大的,也因为时间真的太紧了,而老师给Challenge的分数权重又比较低。但我觉得Challenge真的挺重要的,因为做正规题目时有很多Instruction和Hint,有时你做完后并不是理解的很透彻。而Challenge则完全只有挑战内容,没有太多的帮助。所以如果有时间多做一些的话,真能让你学的更扎实、更上一层楼!