6.S081 2. 内核的隔离性(isolation)
备注,很多内容复制于MIT6.S081
本节课的后续实验:6.S081 Lab00 xv6启动过程(从代码出发,了解操作系统启动过程)
文章目录
1. isolation
防止内存空间被覆盖:使用操作系统的一个原因,甚至可以说是主要原因就是为了实现multiplexing和内存隔离。
如果我们从隔离的角度来稍微看看Unix接口,那么我们可以发现,接口被精心设计以实现资源的强隔离,也就是multiplexing和物理内存的隔离。接口通过抽象硬件资源,从而使得提供强隔离性成为可能。
之前通过fork创建了进程。进程本身不是CPU,但是它们对应了CPU,它们使得你可以在CPU上运行计算任务。所以你懂的,应用程序不能直接与CPU交互,只能与进程交互。操作系统内核会完成不同进程在CPU上的切换。所以,操作系统不是直接将CPU提供给应用程序,而是向应用程序提供“进程”,进程抽象了CPU,这样操作系统才能在多个应用程序之间复用一个或者多个CPU。
2. defensive
应用程序不能够打破对它的隔离。应用程序非常有可能是恶意的,它或许是由攻击者写出来的,攻击者或许想要打破对应用程序的隔离,进而控制内核。一旦有了对于内核的控制能力,你可以做任何事情,因为内核控制了所有的硬件资源。
通常来说,需要通过硬件来实现这的强隔离性。我们这节课会简单介绍一些硬件隔离的内容,但是在后续的课程我们会介绍的更加详细。这里的硬件支持包括了两部分,第一部分是user/kernel mode,kernel mode在RISC-V中被称为Supervisor mode但是其实是同一个东西;第二部分是page table或者虚拟内存(Virtual Memory)。
所有的处理器,如果需要运行能够支持多个应用程序的操作系统,需要同时支持user/kernle mode和虚拟内存
硬件隔离:
-
user/kernle mode
-
Virtual Memory (age table)
3. User / Kernel mode
处理器会有两种操作模式,第一种是user mode,第二种是kernel mode。当运行在kernel mode时,CPU可以运行特定权限的指令(privileged instructions);当运行在user mode时,CPU只能运行普通权限的指令(unprivileged instructions)。
User mode 可以运行的指令有 add, jmp, sub, branch, loop …
Kernel mode 的特殊指令只要是一些直接操纵硬件和设置保护的指令,比如设置page table寄存器、关闭时钟中断。(应用程序不应该执行这些指令,这些指令只能被内核执行)
在处理器的一个bit (-- flag),当它为1的时候是user mode,当它为0时是kernel mode
用户是如何获得(OS给的)CPU等资源的?-- ECALL触发软中断。当用户程序执行系统调用,会通过ECALL触发一个软中断(software interrupt),软中断会查询操作系统预先设定的中断向量表,并执行中断向量表中包含的中断处理程序。中断处理程序在内核中,这样就完成了user mode到kernel mode的切换,并执行用户程序想要执行的特殊权限指令。
(还有一小部分讲page table,这个先不写了,等page table篇统一写)
4. User / Kernel mode 切换
需要有一种方式能够让应用程序可以将控制权转移给内核(Entering Kernel) – 就像3. 中最后所讲的那样。
有一个专门的指令用来实现这个功能,叫做ECALL。ECALL接收一个数字参数,当一个用户程序想要将程序执行的控制权转移到内核,它只需要执行ECALL指令,并传入一个数字。这里的数字参数代表了应用程序想要调用的System Call。 – 我猜:这个数字应该是中断向量表中的偏移。(本科的微机原理 + 汇编)
在内核侧,有一个位于syscall.c的函数syscall,每一个从应用程序发起的系统调用都会调用到这个syscall函数,syscall函数会检查ECALL的参数,通过这个参数内核可以知道需要调用的是fork
学生提问:当应用程序表现的恶意或者就是在一个死循环中,内核是如何夺回控制权限的?
Frans教授:内核会通过硬件设置一个定时器,定时器到期之后会将控制权限从用户空间转移到内核空间,之后内核就有了控制能力并可以重新调度CPU到另一个进程中。我们接下来会看一些更加详细的细节。
5. Monolithic Kernel vs Micro Kernel (微内核vs宏内核)
微内核的目的在于将大部分的操作系统运行在内核之外。(只支持一些信息传递,pagetable,CPU分时复用等)(更少的代码意味着更少的Bug) – 在user/kernel mode反复跳转带来的性能损耗。(嵌入式多用)–在一个类似宏内核的紧耦合系统,各个组成部分,例如文件系统和虚拟内存系统,可以很容易的共享page cache。而在微内核中,每个部分之间都很好的隔离开了,这种共享更难实现。进而导致更难在微内核中得到更高的性能。
宏内核就是整个OS都运行在kernel mode(整体性更好) – 比如Linux (但是OS的bug会带来kernel的bug)
6. Kernel的编译运行 + QEMU
xv6目录下有三个主要文件夹kernel + user + mkfs(它会创建一个空的文件镜像,我们会将这个镜像存在磁盘上,这样我们就可以直接使用一个空的文件系统。)
1. kernel 的编译
-
选择pipe.c 调用gcc编译器,生成一个文件叫做 pipe.s (编译,生成汇编语言)
-
之后再走到汇编解释器,生成pipe.o (汇编,生成二进制机器语言)
-
链接(Loader):将pipe.o 和 各种.o链接起来,比如proc.o(由proc.c生成得到) – 最终形成Kernel
Makefile还会创建kernel.asm,这里包含了内核的完整汇编语言,你们可以通过查看它来定位究竟是哪个指令导致了Bug。
Kernel.asm 的部分代码如下所示(总共有14839行)
kernel/kernel: file format elf64-littleriscv
Disassembly of section .text:
0000000080000000 <_entry>:
80000000: 0000b117 auipc sp,0xb
80000004: 80010113 addi sp,sp,-2048 # 8000a800 <stack0>
80000008: 6505 lui a0,0x1
8000000a: f14025f3 csrr a1,mhartid
8000000e: 0585 addi a1,a1,1
80000010: 02b50533 mul a0,a0,a1
80000014: 912a add sp,sp,a0
80000016: 070000ef jal ra,80000086 <start>
000000008000001a <junk>:
8000001a: a001 j 8000001a <junk>
000000008000001c <timerinit>:
// which arrive at timervec in kernelvec.S,
// which turns them into software interrupts for
// devintr() in trap.c.
void
timerinit()
{
8000001c: 1141 addi sp,sp,-16
8000001e: e422 sd s0,8(sp)
80000020: 0800 addi s0,sp,16
// which hart (core) is this?
static inline uint64
r_mhartid()
第一个指令位于地址0x80000000,对应的是一个RISC-V指令:auipc指令。所以这里0x0000a117就是auipc,这里是二进制编码后的指令。
2. QEMU – RISCV的软件模拟器(有点像本科的嵌入式,先软件模拟,然后烧板子)
RISCV的系统架构图↑
这个图里面有:
- 4个核:U54 Core 1-4
- L2 cache:Banked L2
- 连接DRAM的连接器:DDR Controller
- 各种连接外部设备的方式,比如说UART0,一端连接了键盘,另一端连接了terminal。
- 以及连接了时钟的接口:Clock Generation
在内部,在QEMU的主循环中,只在做一件事情:
-
读取4字节或者8字节的RISC-V指令。
-
解析RISC-V指令,并找出对应的操作码(op code)。我们之前在看kernel.asm的时候,看过一些操作码的二进制版本。通过解析,或许可以知道这是一个ADD指令,或者是一个SUB指令。
-
之后,在软件中执行相应的指令。
QEMU:
for (;;) {
read instruction;
decode instruction; (into asm: add / sub / jmp ...)
execute instruction;
}
7. XV6启动过程
准备当成一个实验来做,可以看后面的博客