【操作系统】操作系统的组织结构
今天主要讨论四个话题,分别是:
solation。隔离性是设计操作系统组织结构的驱动力。Kernel和User mode。这两种模式用来隔离操作系统内核和用户应用程序。System calls。系统调用是你的应用程序能够转换到内核执行的基本方法,这样你的用户态应用程序才能使用内核服务。在学习之前你应该知道的:
首先我们来看一个我们常用的编程语言概念:库函数。
库函数通常用于调用另外一个文件里的函数和用法,来运行程序。然而,它也有缺点:多个应用程序运行时,必须使它们表现良好,那么就会定期舍弃一些东西 以保证良好运行
那么当我们没有操作系统的时候,应用程序就会通过类似库函数的东西来与硬件直接交互,那么CPU就会不断地在多个应用程序之间切换,为了保证它们都表现良好,从而会显得CPU很“忙”。
并且,假如说CPU在运行一个函数,这个函数是一个死循环,那么就永远不会把CPU释放出去,进而导致其他应用程序不能运行。
除此之外,还有可能:由于应用程序直接运行在硬件资源之上,那么每个应用程序的文本,代码和数据都直接保存在物理内存中,进而导致两个应用程序之间没有边界,可能出现覆盖的情况,那很坏了。也就是说应用程序可能会“干坏事",干扰别的程序。
我们称这种直接接触的做法为弱隔离性,隔离性不好,那么交互就会进行不好。
所以使用操作系统,来作为一个中间商,提高隔离性。那么具体应该怎么做呢?
我们需要禁止应用程序直接访问敏感地硬件资源,而是将资源抽象为服务,通过操作系统来进行交互操作。
Unix进程中很多交互形式都是通过文件描述符实现,文件描述符说到底也就是一种规则,通过代码实现其调用(它抽象了很多细节,简化交互)
同时我们需要注意的是:进程本身不是CPU,而是它们对应了、抽象化了CPU
也就是说:应用程序不能直接与CPU交互,而是与进程交互
举个关于隔离性的例子,我们认为exec抽象了内存。当我们在执行exec系统调用的时候,我们会传入一个文件名,而这个文件名对应了一个应用程序的内存镜像。内存镜像里面包括了程序对应的指令,全局的数据。应用程序可以逐渐扩展自己的内存,但是应用程序并没有直接访问物理内存的权限,例如应用程序不能直接访问物理内存的1000-2000这段地址。不能直接访问的原因是,操作系统会提供内存隔离并控制内存,它会在应用程序和硬件资源之间提供一个中间层。exec是这样一种系统调用,它表明了应用程序不能直接访问物理内存。
对于files的调用来说也是,它抽象了磁盘。在Unix中,用户唯一与存储系统进行交互的方式就是通过files。
准确来说,操作系统将一个一个与硬件的操作抽象成了接口的概念,供用户程序使用。就像是厨师给你做了一道菜,你不需要知道它怎么做的,你只需要知道它好不好吃就可以了。
实际上防御性的概念是与隔离性同一进行的。
操作系统需要确保所有的组件都能工作,所以它需要做好准备抵御来自应用程序的攻击。攻击或许包括以下两种但不限于这两种。
一般我们会通过硬件来实现隔离性和防御性,包括以下两种:
接下来进行介绍。
处理器通常会有三种处理模式,即用户模式、管理模式还有机器模式,在这里我们主要讨论前两种模式。
首先我们介绍下几种指令。
普通权限的指令都是一些你们熟悉的指令,例如将两个寄存器相加的指令ADD、将两个寄存器相减的指令SUB、跳转指令JRC、BRANCH指令等等。这些都是普通权限指令,所有的应用程序都允许执行这些指令。
特殊权限指令主要是一些直接操纵硬件的指令和设置保护的指令,例如设置page table寄存器、关闭时钟中断。在处理器上有各种各样的状态,操作系统会使用这些状态,但是只能通过特殊权限指令来变更这些状态。
机器模式下:主要用于配置计算机;
管理模式下,CPU被允许执行特权指令;
用户模式下,应用程序被允许执行普通指令(只能),此时处于管理模式下的软件可以执行特权指令。
如果用户想要执行或者调用内核函数,那么就必须要实现到内核的过渡,(当cpu从用户模式切换到管理模式时(通过内核指定的入口点进入),内核可以决定是否允许其进行管理操作。)
基本上所有的CPU都支持虚拟内存。一般来说页表(Page Table)将虚拟内存地址与物理内存地址进行了对应, 这里称为 映射。
每一个进程都会有自己独立的page table,这样的话,每一个进程只能访问出现在自己page table中的物理内存。这样使得每一个进程都有不重合的物理内存,这样一个进程就不能访问其他进程的物理内存,因为其他进程的物理内存都不在它的page table中。一个进程甚至都不能随意编造一个内存地址,然后通过这个内存地址来访问其他进程的物理内存。这样就给了我们内存的强隔离性。

如果把一个框看作是一个页表,也就是说每个接口都有一个自己的框,同时它也不能进入别人的框。
必须要有一种方式可以使得用户的应用程序能够将控制权以一种协同工作的方式转移到内核,这样内核才能提供相应的服务。
在RISC-V中,有一个专门的指令用来实现这个功能,叫做ECALL。ECALL接收一个数字参数,当一个用户程序想要将程序执行的控制权转移到内核,它只需要执行ECALL指令,并传入一个数字。这里的数字参数代表了应用程序想要调用的System Call。
当我们执行一些应用层面的程序时,我们并不是直接调用操作系统中对应的函数,而是调用类似于ECALL的指令,并将对应的数字参数传给它,通过它跳转到内核。
需要注意的是,一般这种调用指令应该尽量做到稀少甚至是唯一,这样才能保证强隔离性和防御性。
我们知道了内核是操作系统概念中极其重要的一环,因为它具备系统调用的作用,可以转移控制权,那么它就掌握了计算机系统的“生死大权”。所以内核必须是要有信用的,一般称为可被信任的计算空间(Trusted Computing Base),在一些安全的术语中也被称为TCB。
那么问题来了,是什么来运行内核呢?我们可以说操作系统是作为一个中间商来进行交互,但是并没有说它就有完全的权力来运行内核模式。当然,也不是不行。这就涉及到内核的组织模式了。
一般来说,内核组织分为 宏内核和 微内核,接下来分别进行介绍。
整个操作系统代码都运行在kernel mode中。
这样所有系统调用都以管理模式运行。
优点就是:在这种组织中,整个操作系统拥有完整的硬件特权,无需顾虑哪里不需要完全的硬件特权。
另一方面,如果你去看一个操作系统,它包含了各种各样的组成部分,比如说文件系统,虚拟内存,进程管理,这些都是操作系统内实现了特定功能的子模块。宏内核的优势在于,因为这些子模块现在都位于同一个程序中,它们可以紧密的集成在一起,这样的集成提供很好的性能。例如Linux,它就有很不错的性能。
缺点是:宏内核太过于放任权利,稍有错误就可能会导致内核运行失败。
减少内核中的代码,但是不完全减没。
最大限度减少管理模式下运行的操作系统代码量,并在用户模式下执行大部分操作系统,出现了微内核组织。
通常对于微内核,会将操作系统最基本也是最核心的部分放在内核中,其他绝大部分功能放在微内核外的进程中实现。
优点:减少了bug
缺点:与宏内核对比,在宏内核中如果一个应用程序需要与文件系统交互,只需要完成1次用户空间<->内核空间的跳转,但是微内核的的跳转是宏内核的两倍。因为它权限变少了,那么流程自然而然也就变多了。
我们来看一个例子。
假设我们需要让Shell能与文件系统交互,比如Shell调用了exec,必须有种方式可以接入到文件系统中。通常来说,这里工作的方式是,Shell会通过内核中的IPC系统发送一条消息,内核会查看这条消息并发现这是给文件系统的消息,之后内核会把消息发送给文件系统。(一次跳转) 文件系统会完成它的工作之后会向IPC系统发送回一条消息说,这是你的exec系统调用的结果,之后IPC系统再将这条消息发送给Shell。(两次跳转)
这样一来,性能自然而然就会降低。同时各个组成部分之间的连结性会降低,使得共享更难实现。
大多数经典的Unix系统都是宏内核设计,然而也有例如Minix等嵌入式系统使用微内核设计,这取决于操作系统的需求。
接下来我们使用xv6来看一下具体的实现流程。
首先代码结构通常包含啊三个部分:kernel、user、mkfs

kernel。我们可以ls kernel的内容,里面包含了基本上所有的内核文件。因为XV6是一个宏内核结构,这里所有的文件会被编译成一个叫做kernel的二进制文件,然后这个二进制文件会被运行在kernle mode中。user。这基本上是运行在user mode的程序。这也是为什么一个目录称为kernel,另一个目录称为user的原因。mkfs。它会创建一个空的文件镜像,我们会将这个镜像存在磁盘上,这样我们就可以直接使用一个空的文件系统。Makefile(XV6目录下的文件)会读取一个C文件,例如proc.c;之后调用gcc编译器,生成一个文件叫做proc.s,这是RISC-V 汇编语言文件;之后再走到汇编解释器,生成proc.o,这是汇编语言的二进制格式。

Makefile会为所有内核文件做相同的操作,比如说pipe.c,会按照同样的套路,先经过gcc编译成pipe.s,再通过汇编解释器生成pipe.o。

之后,系统加载器(Loader)会收集所有的.o文件,将它们链接在一起,并生成内核文件。

pe.c,会按照同样的套路,先经过gcc编译成pipe.s,再通过汇编解释器生成pipe.o。
[外链图片转存中…(img-ZpFrO0lN-1749619632448)]
之后,系统加载器(Loader)会收集所有的.o文件,将它们链接在一起,并生成内核文件。
[外链图片转存中…(img-TjBHgobZ-1749619632448)]
这里生成的内核文件就是我们将会在QEMU中运行的文件。同时,为了方便,Makefile还会创建kernel.asm,这里包含了内核的完整汇编语言,可以通过查看它来定位究竟是哪个指令导致了Bug。