MIT 6.1810 Lab: Summary
实验笔记
具体每个实验的笔记如下:
MIT 6.1810 Lab: Xv6 and Unix utilities
MIT 6.1810 Lab: Copy-on-Write Fork for xv6
MIT 6.1810 Lab: Multithreading
总结
xv6是一个十分精巧的操作系统,虽然它的每个模块十分简单,却又保留各种操作系统设计的理念。通过对xv6的源码进行学习,可以真正理解操作系统的原理与底层的代码逻辑。
内核
对于内核的理解一直是让人困惑的。一方面,内核在系统调用时接管,代进程执行;另一方面,内核维护着整个系统运作,处理中断与外设,像是一个庞大的后台进程。内核可以被调度、可以被中断、中断又是内核在执行……让人困惑内核究竟是怎样的存在。学习操作系统时总是会有各种各样的概念、设计、方法,书本和教师总会说将内核看作什么,然而看作是“像”而不是“质”,如果仅从一个角度将其理解为像什么,但从另一个角度看问题这种“像”又产生了矛盾,那么学习者就会陷入百思不解的漩涡之中,而且如果不能真正的理解内核,就难以将操作系统的学习连成一个有机的整体。
xv6是一个精简版的操作系统,可能与现在Linux内核有很大的不同,但却对我对内核的理解有了很大的启发。我们知道内核往往是由Bootloader在开机加电后塞到内存中,然后程序流跳转的内核的入口,内核进行初始化,这段时间内核表现像是CPU的唯一进程。此时内核位于内存的一块固定区域,有自己的程序区、栈区,常量区等,这一块区域属于内核初始化程序,由内核永久性占有,后续物理页的获取的不会使用这段区域,例如为新进程分配的进程控制块以及进程控制块中的那些字段直接或间接的空间。对于对称多CPU的处理器,每个CPU核心开始都会跳转到内核的初始化程序处,即同一行代码,但是这段程序往往会根据获取CPU编号,只用一个核心完成初始化。
初始化的最后,这里内核变成永远循环的调度程序,从属于每个CPU核心,当其调度程序让出CPU时,其保上下文保存在内核的数据结构中,即永久占有的那一段区域。调度程序会不停的将CPU跳转到某一进程,进程又会因时钟中断将CPU跳转到调度程序。在此之后,内核的逻辑就发生了转变,就要将内核理解为常驻内存的一段程序。当发生系统调用、外部中断,CPU就会按照预先的设置,执行一部分内核代码,并最终返回,而在这个过程中使用的栈则是从属用户进程的内核栈。在这个阶段,内核失去了其完全的主动性,而是由事件(中断)驱动着硬件(CPU),执行内核的相应代码,实现内核的管理功能。
进程上下文、中断上下文这些概念往往是指,特定状态下,内核实现某一功能的逻辑程序流,不能孤立的认为某一段程序是某上下文。
此外还有内核线程,它的组织与用户进程一样,但是永远执行内核程序,与用户进程一起被调度。
进程调度
xv6中不区分进程与线程。进程是xv6调度的单位,进程运行在CPU上,时钟中断使进程进入内核态,时钟中断会将激活调度程序。从进程切换到调度程序的最后,就是将当前的上下文(此时位于内核,保存的是内核上下文,用户程序的上下文在发生中断时,保存在进程的虚拟地址空间中)保存在当前进程的进程描述符中,切换前的最后一条语句是ret
,将控制流指向ra
寄存器指向的位置,ra
即调用切换寄存器函数的函数下一句,因为切换寄存器函数是被调用的,因此在切换寄存器函数中,ra
存储的是调用函数的下一句。
中断
在XV6中,在用户空间和内核空间发生中断时,处理的逻辑是不同的。发生中断时,CPU会根据寄存器中存储的中断向量,跳转到指定的位置。因此在用户空间和内核空间中,中断寄存器中保存的地址是不同的,这个寄存器的修改会在特权空间切换时修改。
在用户空间,系统调用又称软中断,与硬件中断的处理方式相同。中断发生时,硬件关中断、改特权、保存PC、跳转程序。在XV6中首先跳转到uservec
,在这里保存用户上下文到进程地址空间的保留区域,最后切换内核页表,由内核完成相应操作。最终会由userret
恢复用户上下文。
在内核空间,发生中断时会将当前的内核上下文保存在内核栈中(内核栈属于当前内核上下文的从属进程)。其中kernelvec
保存内核上下文,kernelret
恢复内核上下文。
时钟中断无论发生在用户空间还是内核空间都会激活调度程序,两者的区别是,用户空间发生时钟中断时,会先保存用户上下文陷入内核,后续除了调用的trap
函数不同外,处理逻辑基本一致。
系统调用
严格意义上的系统调用,是内核提供给用户程序的接口,当用户程序向寄存器或栈中存入参数,执行trap
指令,于是将产生了一个软中断,cpu转而执行内核程序,最终将结果写入用户程序,例如可以是用户程序传入的一个参数为期待结果写入的地址。对于用户程序的感知而言,当执行了trap
指令后,我期待的结果地址就已经有了结果,这也就是所谓的透明。
然而我们编写程序时,很少会直接写汇编,因此语言库函数就会将这几句汇编写在一个函数中,那当我们执行这些函数中,我们就变相的执行了系统调用。语言库的编写者往往是精明的,一些简单的操作可能看起来是系统调用函数,但没有真的执行系统调用,因为陷入内核是费事费力的过程,这些系统调用函数可以称作广义的系统调用。
在xv6中添加一个系统调用需要以下几步。规定一个系统调用号,编写系统调用的c库函数。编写一个内核函数,建立系统调用号与内核函数的映射关系。当执行软中断指令后,首先执行中断处理,即保存用户上下文等,中断处理程序发现这是一个软中断就会执行系统调用处理逻辑,通过用户传递的系统调用号可以知道执行哪一个内核中的系统调用函数,执行结束后,然而逐层返回到用户空间。