操作系统内核的绝佳学习材料——JOS

操作系统内核的绝佳学习材料——JOS


前言:关于JOS和一些经验之谈

这一学期的操作系统课使用的是MIT用于教学的JOS操作系统,并且StonyBrook在其基础上做了大量改动,最重要的变化就是从32位移植到了64位。因为个人之前曾系统学习过Linux 0.11内核(《操作系统内核Hack:(四)内核雏形》,实现到时钟中断部分停下了),深知自己从零开始实现内核的工作量。即便是如我个人实现的MiniOS这种简单的不能再简单的,也是需要花费很多时间和精力的。虽然这些付出非常值得(在上这门课时给我带来了巨大的帮助),但是对于想直接上手的同学来说会很打击积极性。

既然强烈推荐JOS,那它有哪些好处呢?

  1. 环境搭建简单:JOS依赖的软件较少,基本就是GCC、Qemu、GDB这一套。以后如果有人能够做一个Docker镜像的话,那就更方便了。
  2. 引导性强:不管是Lab中的文字还是代码里的注释,JOS的作者真的很贴心,几乎所有习题都不会从零开做。尤其是代码中的注释,给出了详尽的指导,很多还都有Hint提示。这对自学的同学来说真的太重要的了。
  3. 自动打分:每个Lab都有一个Grade脚本,因为代码中编写了大量的测试断言代码(有的Lab感觉测试代码甚至都要超过功能代码了,非常的全面)。
  4. 挑战题目:如果你觉得太简单,每个Lab还有少量的挑战题目,有些可以一试,有些对于我是挺难的。这学期大概做了有十几个相对容易一些的Challenge题,感觉还不到总体数量的一半。从Challenge中真的能学到很多,推荐大家都试试,我后面会给出个人认为非常好的题目。

以下是个人的一点经验,感觉应该比较通用,严格遵守的话能节约很多时间:

  • 注释:如果Lab文字不想仔细看的话,那么也行,碰到问题回头再翻。但是:一定要仔细看注释里的提示,差一句话可能就少实现了一种Case,就埋下了一颗“定时炸弹”!
  • Bug:不要以为某个Lab满分就OK了,其中隐藏的Bug可能在后面的Lab爆发。这也是JOS实验有趣的地方,这一套代码完全是你自己的,从第一个Lab到最后,你都要完全负责。
  • 调试:底层开发的调试工作绝对是考验功力的时刻。有时异常了,有时panic了,有时虚拟机triple error直接崩溃了。这时就需要凭借你的经验和强大的GDB像福尔摩斯一样去找线索,有时通过crash前的寄存器内容能预判,有时先大块定位后再逐行调试,有时凭感觉做出几种可能的猜测再去验证等等

所以,个人以为JOS是个非常不错的选择,虽然它的确有的地方有些简陋,与真实的Linux有很大差别,但能系统地做完6个Lab的练习的话,对个人绝对是巨大的提升!本文不会“剧透”任何答案,只是总结一些经验心得和最重要的知识点,大家可以放心观看~

如果本文提到的题目、知识点在MIT的Lab中找不到的话,请搜索Stony Brook的Lab进行学习。


Lab 1: x86 Assembly and Bootloader

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给我的“下马威”。总结一下我做这个最终练习时犯的错误,有些是旧知识忘记了,有些是新知识:

  1. 指针(地址)和值的转换:例如rbp要追溯到上一个栈帧的栈基址时可以这样:*((int *)(ebp))。
  2. 栈的方向弄反了:栈从高地址向低地址生长。ebp下的低地址是被调用函数的局部变量,ebp上的高地址是调用者的返回地址(原eip)、入参。
  3. ebp和eip配对错了:当前ebp和eip是一对,表示当前的数据位置和代码位置。而当前ebp所指位置里的值,与该位置紧挨着的高地址位置保存的返回地址,则是又一对。
  4. 64位的调用惯例:前面说到ebp上保存着返回地址和入参,这其实只对32位有效。64位的返回地址的确还是紧挨着ebp,但入参不是必须压栈的。因为64位机器的寄存器很多,从rdi、rsi、rdx、rcx、r8、r9可以保存6个入参,所以只有要调用的函数入参超过6个时才会压栈。

Lab 1只有一个Challenge,就是让输出到控制台的字体改变颜色。要求中介绍了一种通用的控制方法,就是ANSI的ESC序列(Escape sequence),语法是”ESC[Value;…;Valuem”。曾经在设法使MakeFile输出不同颜色字体时遇到过,例如echo -e ‘\033[31;1mHelloWorld!\033[0m’(先修改成红色,输出后立即复位)。所以整体思路是:内核接收到ESC序列字符串后,解析其内容并更改内部的输出模式,于是后续输出的字符就改变颜色。


Lab 2: Virtual memory

Lab 2开始难度陡增,个人感觉2和3可能是最难的两个Lab了。也可能是刚开始,一切还都不熟悉,所以感觉比较难。最烧脑的就是虚拟地址和物理地址的转换,以前从来没觉得这块知识很难,但在JOS里这绝对是最难的一块内容!


2.1 引导过程简介

因为之前在《操作系统内核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要补上的最重要工作就是:设置页表,开启分页机制。下面进入本文的重点,页式管理。


2.2 内存分配的本质

谈到内存分配,第一反应就是C语言里的malloc(),以及高级语言C++/Java里的new关键字。可我们现在要写的是系统内核,还没有malloc()库函数(在《操作系统内核Hack》系列里曾讲过,开发内核时是不能随便引用标准库的),更没有高级语言里的new。就在迷茫的时刻,才会思考问题的本质。我们说内存分配时到底在说什么?其实对于内核来说,内存分配就是“随意”地返回一个地址给调用方使用,只要你保证这个地址不被其他人使用,那就是一次成功的内存分配了。所以,我们一般说的不管是JVM也好还是malloc也好,内存分配和释放的消耗其实都是内存管理器复杂管理的代价,如果我们用最最简单的Bump Allocator的话,内存分配的本质真的就像上面说的那样简单、原始!


2.3 虚拟地址 vs. 物理地址

现在就来看最为头疼的页式管理吧。这绝对是实验二的最大难点了!哪里是虚拟地址?哪里是物理地址?什么时候会发生转换?cr3以及每一级页表里存的是虚拟还是物理地址?GDB打印的又是什么地址?一个个问题搞得我晕头转向。现在终于有一些“清醒”了,就谈谈我对这些困惑问题的理解:

  1. 所有编译器“生成”的都是虚拟地址,例如:int a = &p,通过&p得到的地址就是虚拟地址。
  2. 所有内存访问都会发生地址转换,例如:a[i]*p,这两种形式的解引用都要求变量是虚拟地址。如果是自己手动赋的物理地址,就会导致MMU翻译时找不到对应的页表项而报错。
  3. 物理地址只能做二次转换获得,例如:a = PADDR(&p)这种转换可行的前提是你知道当前页是如何映射的,即页表内容。这在内核中是可以办到的,也是我们在JOS实验二中做的事儿。但是未来你想要在某个用户进程中得到一个变量实际存在哪里了,这几乎是不可能的,因为操作系统已经对你屏蔽了这些东西。
  4. cr3里存的是物理地址,而且每一级页表pml4e、pdpe、pde、pte的表项里存的都是物理地址。否则就会出现“死循环”的效果了。假如pml4e[5]里存的是某一pdp表的虚拟地址,那么MMU又要拿着这个虚拟地址重新从pml4e来一遍…… 所以页表的内容类似于:pml4e[1]=0x1000(pdpe) => pdpe[0]=0x2000(pdp) => pde[11]=0x3000……
  5. 用GDB打印指针的地址是虚拟地址,但可以通过指定物理地址查看内存。例如,p/x a看到的就是虚拟地址,而x/10x 0x1000查看的就是物理地址0x1000位置的内容。

如果大家修改到那几个walk()函数时可以会有疑问:为什么继续向下一级页表递归时要将页表地址通过KADDR()转为虚拟地址呢?因为每一级页表的walk()函数处理时都有pml4e[i]、pdpe[i]、pte[i]。不要忘记前面说过的,只要解引用就会发生地址转换!硬件逐级递归时是没有这个问题的,而我们的walk()函数是用软件模拟,所以一定要转为虚拟地址再递归!


Lab 3: Processes/environments

Lab 3的难度与Lab 2相当,最大难点就是中断(Interrupt)了。这两个Lab中涉及到的内存管理和中断可以说是最核心的知识,挺过这两个Lab后面就一马平川了!既然中断这么重要,那肯定不是三言两语可以说清楚的,强烈建议大家按照JOS的Instruction去实现,并且遇到问题一遍遍调试。这样整个中断的执行流程就会在你脑海里强化记忆,加深理解。


Lab 4: Multiprogramming and fork

经过了前面三个Lab的洗礼,Lab 4开始就是按部就班就可以了,个人感觉没有特别难的地方了。Lab 4首先让大家熟悉多核环境,我们要做的就是初始化好多核的运行环境,主要是多个内核栈和对应的TSS配置等。


4.1 OS中枢:调度器

OS的调度是由一个Timer发起的,引发中断进入内核态后,调用中断处理函数进行处理。因为JOS的中断过程是由BKL(Big Kernel Lock)保证的,所以中断处理函数运行时“整个世界都清净了”。不管有几个核心,此时世界静止,等待我们进行调度,在返回用户态的前一刻才会放开BKL。中断处理函数可做的事情很多,像简单的RR、复杂的类CFS、有趣的Lottery调度等等,大家尽可以根据自己兴趣去实验各种调度方式。像控制了OS的大脑中枢神经一般,控制一切的感觉还是很爽的!


4.2 COW Fork

本Lab的第二个核心就是实现拥有Copy-on-Write能力的Fork。要注意的是因为JOS采取的是Microkernel架构,所以Fork是在用户态配合几个系统调用完成的。因为要遍历进程地址空间决定是否要Copy还是Share,所以Instruction中给出了一种貌似很神奇的办法:顺序遍历一个数组。一直理解得不是很好,个中奥秘还是大家自己去探索吧!


4.3 IPC通信

因为JOS的Microkernel架构的缘故,在Lab 5和Lab 6中的FS和网络大量使用IPC通信,所以一定要对IPC有清晰的理解才能做好下两个Lab。其实并不难,IPC接收方调用receive()函数挂起自己,发送方调用send()进行数据通信后,内核唤醒接收方继续处理。


Lab 5: File System and Shell

Lab 5的核心就是一个:FS。JOS的FS对Linux的VFS进行了大量简化,最重要的改变就是去掉了inode,直接通过dentry管理文件和元数据。个人觉得,这个改动非常不好!一是因为inode真的太重要了,这个改动却导致了我们失去深入理解它的机会。二就是没了inode加上各种数据结构命名的不同,将JOS的FS部分与Linux的VFS对比学习时会产生很大困惑。强烈推荐大家看一下《Linux Kernel Architecture》之后,做一下实现inode的那个Challenge,具体请参见最后一部分。


Lab 6: Network Driver

这是我们的Final Lab,可以选网络驱动、虚拟化和自选题目。因为可能在MIT的原始材料里没有对应,所以就不详细说了。主要目的就是实现一个网络驱动,接收和发送网络包。难度不是很大,但因为是最终的Lab所以给出的Hint比较少,需要认真读Instruction和Intel的手册,总体上还是蛮有趣的!


福利:Challenges You Cannot Miss

本节给大家推荐一些我认为非常不错的Challenge,当然了,我没全做出来。因为有的真的工作量挺大的,也因为时间真的太紧了,而老师给Challenge的分数权重又比较低。但我觉得Challenge真的挺重要的,因为做正规题目时有很多Instruction和Hint,有时你做完后并不是理解的很透彻。而Challenge则完全只有挑战内容,没有太多的帮助。所以如果有时间多做一些的话,真能让你学的更扎实、更上一层楼!

  • Lab 2 - Challenge 2! Homemade Debugger (强烈推荐):通过这个Challenge你也许会开始思考GDB,曾经理所当然的工具,它内部究竟是如何实现的呢?要想实现简单的断点和内存查看其实不太难,难的是像GDB一样根据编译器给出的调试信息进行反汇编。
  • Lab 2 - Challenge 4! General Memory Allocator (Slab):题目要求实现一个能分配4K的整数倍的通用内存管理器,但个人觉得实现成Slab也许收获更大,但可能难度也更高。如果你真的实现了的话,后面很多Lab要给内核数据结构分配内存时就都可以用你自己的通用管理器了,真爽!
  • Lab 3 - Challenge 3! Faster System Call (Sysenter/Sysexit):现在Linux中只要是环境允许的话,就会用这种所谓的快速系统调用。因为与传统的将所有上下文信息都压栈保存的方式有很大不同,所以要实现的话还是有些工作量的。
  • Lab 4 - Challenge 1! Fine-grained Lock:JOS中为了简化,用的是BKL(Big Kernel Lock),只要进入内核态就一把大锁直接锁死,等回到用户态再放开。这就极大简化了多CPU时,内核数据结构的并行访问问题,当然也降低了并行性。BKL是Linux的技术债务,虽然并未完全移除,但在后续代码中是绝对不会使用它了。
  • Lab 4 - Challenge 2! Scheduling Policy (强烈推荐):JOS只要求我们实现的是最简单的RoundRobin策略,这个Challenge让我们实现更有趣的调度,比如Lottery调度。利用随机数决定下一个运行的进程,允许进程具有不同的优先级的同时又无需做Bookkeeping,真是巧妙!具体可以参考《Three Easy Pieces》的精彩介绍!要注意的是:随机数生成器也要我们自己实现,因为内核里没有类似rand()这种函数,Wiki一下你会发现奇妙的东西。
  • Lab 4 - Challenge 6! Share-Everything Fork (Threading!) (强烈推荐):JOS只要求实现了进程调度,通过这个Challenge你会Wow,原来这就是Thread啊!具体请参考Linux内核的clone()函数。
  • Lab 5 - Challenge 1! Interrupt-driven IDE Driver:JOS目前用的是最简单的PIO(Programming I/O)方式与磁盘交互,本Challenge要实现中断式的方式,更贴近现实世界里的Linux。
  • Lab 5 - Challenge 2! Page Cache Eviction:默认Page Cache是没有Evict功能的,这里我们可以实现一个简单的替换算法,例如Second-Chance。
  • Lab 5 - Challenge 3! Journaling (强烈推荐):要为JOS的FS加上Journaling功能,工作量不是特别大(相比下一道Challenge),但需要对Ext3有比较清晰的了解,又是《Three Easy Pieces》,讲解的真的非常棒!
  • Lab 5 - Challenge 4! inode Implementation (强烈推荐):这道题可谓是我做过的Challenge里工作量之最了,修改了将近15个文件,又新增了用户空间的测试程序,但收获也是巨大了!做完此题,你会对dentry、inode、file三大VFS核心对象有彻底清晰的认识,对in-memory和on-disk数据结构有全新的了解,对JOS的FS和Linux的VFS有充分的对比,此题真的不容错过!推荐《Linux Kernel Architecture》对应VFS几章精彩的讲解,这一部分的精彩程度真的完爆《Understanding the Linux Kernel》。
  • Lab 5 - Challenge 5! Implement Unix-style exec:不借助内核帮助,在parent和child进程的用户空间里实现exec真是挺麻烦的,因为你已经进入child用户空间了,还要为child真正的ELF文件指定的main做准备工作再跳转,这些工作正常来说都是在内核态中完成的。至少我最后也没做出来……
  • Lab 5 - Challenge 6! Implement mmap-style Memory-mapped Files:mmap的实现到了最后我也是云里雾里的,一定要抽时间好好理清一下。
时间: 2024-10-19 04:10:42

操作系统内核的绝佳学习材料——JOS的相关文章

AACOS:基于编译器和操作系统内核的算法设计与实现

AACOS:基于编译器和操作系统内核的算法设计与实现 [计算机科学技术] 谢晓啸 湖北省沙市中学 [关键词]: 编译原理,操作系统内核实现,算法与数据结构,算法优化 0.索引 1.引论 1.1研究内容 1.2研究目的 1.3研究提要 正文 2.1研究方法 2.2编译器部分 2.2.1从计算器程序中得到的编译器制作启示 2.2.2在编译器中其它具体代码的实现 2.2.3编译器中栈的高级应用 2.2.3编译器中树的高级应用 2.2.4编译器与有限状态机 2.3操作系统内核部分 2.3.1操作系统与底

操作系统内核Hack:(三)BootLoader制作

操作系统内核Hack:(三)BootLoader制作 关于本文涉及到的完整源码请参考MiniOS的v1_bootloader分支. 1.制作方法 现在我们已经了解了关于BootLoader的一切知识,让我们开始动手做一个BootLoader吧!但真正开始之前,我们还要做出一个选择,在之前的讨论中我们曾说过,有两种学习和制作引导程序和操作系统内核的路线:1)<Orange's:一个操作系统的实现>书中的路线:2)Linux 0.11的路线. 1.1 两种实现思路 具体来说,第一种路线就是将Boo

操作系统是如何工作的————一个精简的操作系统内核(20135304 刘世鹏)

操作系统是如何工作的————一个精简的操作系统内核 作者:20135304 刘世鹏 原创作品转载请注明出处 <Linux内核分析>MOOC课程http://mooc.study.163.com/course/USTC-1000029000 实验过程 使用实验楼虚拟机打开shell,加载实验所需linux内核,执行搭建好的系统 cd LinuxKernel/linux-3.9.4 qemu -kernel arch/x86/boot/bzImage 一直在执行mystartkernel,交替执行

AndroidDiskLruCache源码解析硬盘缓存的绝佳方案(转载)

AndroidDiskLruCache源码解析硬盘缓存的绝佳方案 转载请标明出处: http://blog.csdn.net/lmj623565791/article/details/47251585: 本文出自:[张鸿洋的博客] 一.概述 依旧是整理东西,所以近期的博客涉及的东西可能会比较老一点,会分析一些经典的框架,我觉得可能也是每个优秀的开发者必须掌握的东西:那么对于Disk Cache,DiskLruCache可以算佼佼者了,所以我们就来分析下其源码实现. 对于该库的使用,推荐老郭的bl

操作系统内核Hack:(一)实验环境搭建

操作系统内核Hack:(一)实验环境搭建 三四年前,心血来潮,入手<Orange's:一个操作系统的实现>学习操作系统内核,还配套买了王爽的<汇编语言(第二版)>和<80X86汇编语言程序设计教程>,虽然Orang's只看了不到三分之一,但当时还是很认真的,练习也做了不少.唯一遗憾的就是没有留下文字记录,导致现在忘得差不多一干二净了,后悔不已!如今想再捡起来,弥补当时的懒惰,虽然困难重重,但这么优秀的国产书怎么能看完就算了呢!而且当年还是在Windows下练习的,现在终

系统架构领域的一些学习材料

标签:架构学习材料系统systemresearch 转载:http://qing.blog.sina.com.cn/2244218960/85c41050330031zq.html?utm_source=tuicool&sudaref=www.tuicool.com 系统架构是一个工程和研究相结合的领域,既注重实践又依赖理论指导,入门容易但精通很难,有时候还要讲点悟性,很具有"伪科学"的特征.要在此领域进阶,除了要不断设计并搭建实际系统,也要注意方法论和设计理念的学习和提炼.

Python Selenium入门学习材料整理

Python Selenium入门学习材料整理一.简介及环境搭建1.selenium 介绍:selenium 是一个 web 的自动化测试工具,可以自动打开浏览器执行页面打开.页面内容抓取.页面元素搜索,是相对好上手的网页爬取工具.2.安装selenium:pip install selenium3.安装webdriver:selenium打开网页需要有webdriver来调用浏览器.Firefox:https://github.com/mozilla/geckodriver/releases/

Unix操作系统内核结构报告

Unix操作系统内核结构报告 1.有一个程序的代码如下: main() { int i ; for(i=0; i<3; i++) fork(); } 请问该程序运行时共建立了多少个进程?请用进程家族树来画出父子进程之间的关系. 解:一共建立了7个进程. 2.UNIX 系统中用“最近最少使用(LRU)” 算法来构建数据缓冲池.如果核心采用“先进先出(FIFO)”算法来构建缓冲池,则对缓冲区算法 getblk 来说,会造成功能上的区别主要有哪些? 解:getblk是把缓冲区分配给磁盘块的一个算法.用

Font Awesome奥森图标一套绝佳的图标字体库和CSS框架

迄今最完好的翻译版本-一套绝佳的图标字体库和CSS框架(奥森图标) Font Awesome 逐浪CMS官方版正式开放啦! 免费获取和使用就在:http://code.zoomla.cn/boot/font.html(非广告官方翻译版本,希望对广大开发者有用). 逐浪CMS已经完全支持Font Awesome奥森图标! Font Awesome为您提供可缩放的矢量图标,您可以使用CSS所提供的所有特性对它们进行更改,包括:大小.颜色.阴影或者其它任何支持的效果. 一个字库,479个图标 仅一个F