MIT6.S081操作系统学习-1
操作系统
最近也是在学习MIT6.s081,学了一段时间后,知识点很多也很杂,今天在复习的同时也顺带将这些知识点串一下。
首先,我们需要知道什么是操作系统?操作系统有什么用?
那么我认为,操作系统它也是一个程序,只不过它是一个特殊的程序。为什么呢?操作系统能够执行一些特殊的指令,比如创建进程,分配内存等。而普通的程序没有执行这些操作的权限,那有人可能会说,不对啊,我平时使用java等编程语言也能创建进程啊。别急,等下会介绍这个话题。这是因为操作系统能够直接操控最底层的硬件资源,一般的操作系统会将这些硬件资源进行抽象,比如将磁盘等存储器抽象为文件,将CPU抽象为进程。为什么要设计操作系统呢?一方面,是为了方便用户更好的使用计算机,因为直接和硬件打交道太麻烦了,用户必须得输入二进制指令才能和计算机进行交互,有了操作系统后,可以基于操作系统开发出更加高级的语言来和计算机进行交互,比如汇编,C,C++等;但其实最重要的一点是,设计操作系统是为了保护底层硬件,用户可以直接操控底层硬件是一种非常可怕且危险的形式,有些不怀好意的人他们编写的程序可能会使你的计算机崩溃,这不是一种好的设计。因此操作系统必须具有很强的隔离性,它能够阻止恶意程序攻击你的设备。但与此同时,操作系统也必须拥有良好的交互性,比如刚才就有人提问使用java也能够创建进程这其实就是和操作系统间的交互。或许这听起来很矛盾,但一个真正的操作系统确实如此,这也是设计一个好的操作系统的难点。接下来,我们来看一下操作系统的组织结构,可能会让你了解操作系统是如何保证强隔离性的同时又有一定的交互性。
操作系统的组织结构
上一节中讲到,一个好的操作系统必须提供极强的隔离性,从而保证底层硬件的安全,同时它又得提供一定得交互性,那么本节我们来探讨在xv6操作系统中是如何实现的。我们先来介绍一下隔离性,其实在设计操作系统时,主要通过两种措施来实现其隔离性。
######建立一堵墙:
这是一个很好的想法,如果我们想把两样东西分隔开来,我们可以在它们中间设立一些障碍,至少柏林曾经有过一段时间就被一堵墙分隔成了两部分,不是吗。那么在操作系统中这堵墙就叫做user/kernel mode,也就是说操作系统将计算机分成了两部分,上面的那部分叫做user space用户区,下面那部分叫做kernel space内核区,内核区中存放着操作系统的所有功能模块(但其实这是宏内核的划分方法,而xv6系统使用的就是宏内核,在微内核系统中,操作系统中部分功能模块也位于用户区)。一切特殊权限指令都将也必须在内核区执行,这保证了操作系统的隔离性
独立的空间:
这也是一个很好的想法,当我们将两个人安置在不同的房间里,他们自然也无法干扰对方了。在操作系统中我们将这个空间叫做页(大小一般为4KB)。每个程序(进程)都只会在自己的页面中活动,而操作系统需要保证每个程序的房间里不会出现其他程序的物品,实现这项功能的技术称为虚拟内存和内存管理系统(MMU)。在计算机中每条地址指令的内容都指的是虚拟地址,需要通过转化才能得到其真正的物理地址。在操作系统中提供了一种数据结构来方便进行这种转化——页表,页表中存放着每个页面对应的物理地址,其实就是一种映射结构。在xv6系统中,每条虚拟地址需要使用64位二进制数字,但实际有效的是后39位,其中最后12位表示偏移量(offset),27位表示用于转化为页面。你们或许会问,既然只使用39位,为啥需要64位二进制。我想说的是,这不是我们能够决定的,64位二进制是由底层硬件决定的,你的存储器芯片具有64位二进制,那么你的地址就是64位,你当然可以将这64位二进制都用掉,但是一般我们不会这么做,因为我们需要保留一部分以供未来进行扩展使用。但主要的原因是,xv6系统只是一个小型操作系统,39位已经足以支撑我们整个系统结构了。

上图展示的是xv6系统中每个进程所拥有的虚拟地址空间,可以看到地址是从0开始的,接下来这部分内存用于存储用户数据比如一些全局变量等,然后是栈空间。还有相当大的一部分空间:heap,这部分空间用于该进程进行内存扩展。
接下来,我们仔细研究一下页表这个结构,很显然,页表必须存储在内存中某个位置。既然我们的虚拟地址中有27位用于表示页表,那也就意味着页表中需要存放2^27条记录,而在xv6系统中每条记录需要使用64位二进制,这是一块相当大的内存,可能在某些页表占用内存较小的系统中是这么设计的(将所有页面与物理地址的映射关系存放在一个页表中)。但显然我们不能这么设计,一种更好的做法是设置多级页表,什么意思呢?也就是说在更高一级的页表中存放着比它低一级页表的位置,而在最低一级页表中存放着该虚拟地址对应的物理地址。这听起来或许很抽象,让我们用一张图来说明这一切。
通过这张图,我们可以了解xv6系统中虚拟地址是如何转化为其物理地址的。在xv6系统中设置了3级页表,当接收到一个虚拟地址时,先通过satp(位于CPU中的一个寄存器,存放着当前进程最高一级页表的地址),获取最高一级页表的首地址,通过偏移量(L2)获取该地址对应的第二级页表的首地址,再通过偏移量(L1)获取该地址对应的最低级页表的首地址,然后通过偏移量(L0)找到该地址对应的物理地址的首地址(再计算机中地址都是以块的形式存在的,块的大小和页的大小一致),最后通过偏移量(offset)获取该地址中存放的数据。尽管这个过程十分繁琐,有点类似链表,但是经过这种操作,页表所需要的内存确实降低了:每个页表中只需要存放2^9条数据,总共有3*2 ^9条数据。但是这种做法有一个明显的缺点:每次转化都需要访问内存3次,在效率上是个问题。
一种提高效率的方法是:设置缓存,即每次转化,先从缓存中查看是否已经有该数据,如果有则直接返回结果;如果没有,再利用上述过程得到其物理地址,并将其存放入缓存中。至于缓存中存在的一些问题,比如:数据一致性问题,大家有兴趣的可以自行了解,这里不再深入讨论。
需要注意的是,每个进程都有自己的页表,因此当CPU进行进程切换时,会加载对应进程的页表地址到satp寄存器中,并且清空缓存。
再上述图片中我们还可以了解到的是,在xv6系统中页表中存放的每条数据只使用了54位二进制(剩下10位作为保留位),其中44位用于表示内存(难道xv6的内存空间有几TB那么大?),10位用作标志位。在这10位标志位中,最低位表示该条记录是否是有效记录,接下来3位分别表示是否可以对该记录进行读、写、执行操作。
至此,我们终于初步了解了操作系统是如何保证其强隔离性的。我们用几句话简单概括一下,通过设置user/kernel mode将用户区和内核区隔离,保证用户程序无法直接执行特殊指令,从而提供了指令隔离;通过虚拟内存技术,保证了各个程序间内存上互不干扰,从而提供了内存隔离。
一定的交互性
经过前几节的学习,我相信你已经对操作系统的隔离性有了一定的认识,由于这些知识点非常重要也是操作系统中比较难的一部分,因此我们花了比较多的时间来学习,我相信这些时间是值得的。好了,接下来我们放松点,来了解一下操作系统是如何提供交互的,当然,这个知识点也很重要,希望你们也能够好好听讲。
一个完全封闭的操作系统是没有意义的,因为没有用户可以使用它。因此操作系统必须具有一定的交互能力,一种想法就是在墙的两边安装电话线,通过电话实现交流。在两边分别有两个接线员,用户区的负责人叫做ECALL,内核区的负责人叫做syscall,而电话线叫做中断程序。操作系统给用户区提供了许多电话号码(也就是系统调用),当用户拨打了这个电话号码,ECALL会为其接线,当syscall收到后,它首先会打开某个开关(一个标志位,表示系统现在的状态),表示系统现在进入到内核区,然后它会查看该用户的请求是否合理,如果合理再由它打电话给相应人员去完成;否则它会拒绝该请求,完成后它会关掉开关进入用户模式。
以上这个例子是我对操作系统进行交互时的理解,比较学术的说法就是。操作系统提供了许多系统调用,所谓系统调用就是一些接口,其具体实现都位于内核区。当用户程序使用了这些系统调用时,ECALL函数将该系统调用作为参数传入(在xv6系统中这些系统调用都是由一个数字表示,比如read对应的数字为0)。然后会触发一个软中断,该中断程序位于内核,因此就成功从用户区切换为内核区。位于内核区的syscall函数会检查请求是否合理,并调用真正的系统函数来完成请求。
以上就是对操作系统的交互性的认识,我们也稍微总结一下。首先操作系统通过提供系统调用接口来实现和用户区的交互,用户区通过ECALL来实现内核区的转换,有一个标志位用于标识当前是位于用户区还是内核区,当然该标志位由内核控制,最后在用户区调用的系统函数,最终是在内核区执行的。