Skip to content

OS organizetion

隔离性(Isolation)

隔离性很重要,如果没有实现好隔离性,不同程序可能会影响到其他程序,导致出现一些问题,尤其是影响到了 shell ,shell 如果被影响,它可能就会杀死一些程序。因此,不同应用直接应该存在强隔离性。

即使是用户传递一些奇怪的参数,操作系统也需要能很好的处理,而更重要的是,操作系统不能被程序的影响,因此操作系统和应用程序之间需要更强的隔离性,这就有了用户态和内核态。

假想一下,如果没有操作系统,所有程序都会直接访问硬件,每个程序运行都要访问 CPU ,内存,假设 CPU 是单核的,这一个 CPU 上运行着 shell ,周期性的放弃 CPU ,只访问自己的内存地址,让其他程序也可以运行,这就是协作式调度,如果所有程序都按照这个规则来,不出现 bug 的话是没有问题的。
但是这里没有隔离,其他的程序可以直接访问自己内存地址以外的地址,也没有强制复用,如果 shell 出现了 bug ,一直使用 CPU ,其他程序就永远不能使用 CPU 了,就像站着茅坑不拉屎一样。
因此,操作系统的重要的作用就有复用强隔离

操作系统的接口,不仅方便使用,而且为隔离性提供了可能。它抽象了硬件资源,让程序不能访问硬件,应用程序不能直接访问 CPU ,应用程序只能通过进程的抽象使用 CPU 。 exec 系统调用是对内存的抽象,如果调用 sbrk 扩展数据段,如果扩展了十分大的内存,操作系统会阻止,实际上物理内存中并没有被影响,它只会申请虚拟内存,这是因为操作系统会对内存进行抽象。

操作系统应该具有防御性的,由于它需要保证任何程序都可以运行,因此需要做一些防御,以保证有的程序破坏系统。同时,程序不能打破隔离而获取到内核的控制权。
硬件会通常会实现两种隔离:第一种就是用户态和内核态,内核态在 RISC-V 中也被叫做监管模式(supervised mode);第二种是页表,也就是虚拟内存。

内核/用户态(Kernel/User mode)

内核/用户态

用户态:只能让 CPU 执行非特权指令,非特权指令就是任何用户都可以执行的指令。例如: add(加法)、 sub(减法), jr(跳转)等等,。
内核态: CPU 可以执行特权指令,特权指令就是直接操控硬件的指令。例如:设置保护,配置页表寄存器,设置时钟中断等等。

在处理器中,有一个标志位, 0 表示用户态, 1 表示内核态。每当处理器做解码操作的时候,就会看一下这个标志位,如果操作时特权指令,同时标志为 1 ,就会拒绝该操作。

比较特别的是,在 RISC-V 架构中,有第三种模式,就是是机器态,是 RISC-V 的最高特权模式,负责最底层的硬件初始化、管理、可信根、处理严重错误/NMI、配置和委托控制权给低特权模式。 RISC-V 使用三种模式的主要原因是为了实现硬件抽象/固件分离(提高内核可移植性、安全性、模块化)、支持从简单嵌入式到复杂操作系统/虚拟化的多样化实现、提供更强的安全隔离(最小化 TCB )、以及实现高效的异常/中断委托机制。
同时,在 RISC-V 中,内核态的命名也和其他的架构不同,使用的是 Supervisor Mode ,机器态命名为 Machine Mode

当用户希望执行一条特权指令,也就是系统调用时,它必须通过一条特殊的指令——ecall (Environment Call) 来请求内核的服务,ecall 是用户程序主动触发向更高特权模式切换的唯一合法方式。

虚拟页表 (Virtual memory)

几乎所有的处理器都有一个页表 (page table) 。页表就是将虚拟地址映射到物理地址,基本思想就是给每个进程分配自己的页表,然后进程只能使用自己被分配到的页表对应的物理地址。这样就实现了内存的强隔离。

内核态

因此,内核有时候也被称为可信任计算机基础(Trusted Computer Base) ,在安全术语中称为 TCB

内核必须没有 bug
内核必须将所有的用户程序当成恶意程序

内核设计

一种设计是将整个操作系统都运放在内核中,例如:Linux, Unix ,这种设计被称为宏内核设计(monolithic kernel design),它的特点是:

内核代码量大 => 它比较容易产生 bug
良好的性能 => 良好的性能

另一种设计就是微内核设计(micro kernel design),例如:Minix ,它的目的是让内核态运行的代码尽量减少,内核中只会包含很少的组件,一般是包含 IPC 或者消息传递、一定量的虚拟内存(通常只有页表相关的东西)、一些复用 CPU 的东西,这样可以使大部分 bug 出现在操作内核外面,它的特点是:

内核很小 => bug 少
所有特权操作都是通过消息传递实现 => 特权操作需要比宏内核翻一倍的切换 CPU 状态 => 性能低下

系统调用(System call)

在 RISC-V 中,有一个叫做 ecall 的指令, ecall 会接收一个数字参数 n ,当程序希望进入内核态的时候,就可以调用 ecall 这个函数,然后传递数字,例如 2, 3, 4, 5 ,这里传递的数字就是希望调用的 系统调用(SysCall) 的编号。

ecall 做的就是进入内核中,一个由内核控制的特定位置,例如:某个程序调用了 fork ,它并不会直接调用 fork ,而是会调用 ecal(SYS_FORK) ,这里的 SYS_FORKfork 的编号,然后进入内核中。
在内核中,有一个特别的函数叫做 syscall 的在 syscall.c 中,每次执行系统调用都会用到这个特别的函数。这个 syscall 会查询数字,然后传递给 a0 寄存器,然后系统会去看 a0 寄存器判断执行哪个特权操作。

一个想要调用内核函数的应用程序必须转换到内核;应用程序不能直接调用内核函数。CPU 提供了一条特殊指令(RISC-V 为此目的提供了 ecall 指令),用于将 CPU 从用户模式切换到内核模式,并进入由内核指定的入口点 (entry point)。
CPU 切换到内核模式之后, CPU 就会验证系统调用的参数(例如,检查传递给系统调用的地址是否是应用程序自身内存的一部分),决定该应用程序是否被允许执行所请求的操作(例如,检查该应用程序是否有权写入指定的文件),然后拒绝或执行该操作。

xv6 代码

c 文件首先会通过 gcc 编译成 .s 的汇编文件,然后通过汇编器得到 .o 的链接文件,最后使用加载器将所有的 .o 文件链接到一起生成一个二进制的可执行文件。
宏内核设计就是将系统的所有文件链接成单独的一个二进制可执行文件。