Lecture3

别问为什么第一篇就到3了… Lecture1混进OperatingSys了。

Isolation 隔离

稻草人设计

所谓的操作系统是在硬件和程序之间的一层抽象。

假如说没有了操作系统这一层抽象的话,会发生什么?

首先就是上下文切换这一点,虽然说程序本身可以做到类似主动让步让出系统资源这一操作(yield)。但是假如程序在正常的执行过程中出现了类似死循环,或panic等等异常。此时由于没有操作系统的介入(SysCall)。就会导致无法退出,或者说无法recovery。不能强制的实现一般性多应用程序间切换。multiplexing

另外的话,操作系统保证了空间资源等的分配。假如没有操作系统的分配。应用程序之间有可能会使用到同一份物理空间(不是虚拟内存)。此时数据会被覆盖。影响正常运行等结果不必多说。

两者概括为:

  • 内存隔离

  • multiplexing

*在实时操作系统中,应用程序可能会相互信任,所以不会有这些问题。

所以

Unix interface

Abstract the hardwares

Provide Strong Isolation

To Application

以进程单位抽象了CPU(操作系统以进程为单位抽象CPU的分配)

对于其他的系统调用如

  • exec抽象了内存

应该是说:

exec本质上是执行程序,执行程序的时候会操作内存。而用户不需要跟内存接触,只需要操作exec即可。

  • file抽象了磁盘,用户不需要操作磁盘中的物理块。操作系统提供了文件这一抽象来进行使用。

Defensive 防御性

user / kernel mode

这是最基本的一个构建,通过这个方式隔离应用程序执行一些不能执行的指令。

如何判断什么是特殊权限的指令 什么是普通权限的指令?

会有一个标志位寄存器。特殊置1,普通置0。并且会存在一个本身为特殊权限指令,该指令就能修改这个寄存器,随意置0或1。

这么说有点套娃,既然说本身修改权限位的指令就需要特殊权限。那是否就意味着普通用户不能操作特殊权限指令?

是的,本身这就是为了达成防御性。所设置的特性,或者说是机制。

虚拟内存

操作系统是以进程为单位分配内存的。而分配内存的单位本质上就是页表。而页表存储的本质上是一个虚拟逻辑地址。操作系统会通过这个逻辑地址计算出其实际存储的物理空间。

这就带来内存的强隔离性。

SysCall

通过 user / kernel mode这一模式。我们可以很好的隔离操作系统和应用程序。

但是程序执行类似write read等操作的时候还是需要内核态的权限。所以就会有了一个SysCall。通过这些暴露出来的接口,用户态的应用程序可以操作操作系统内核中的资源。

在xv6中,每一个系统调用都会有一个它自己的编号。

而提供给用户的系统调用接口本质上是一个再封装。真正的执行是通过执行eCall函数,附带对应的系统调用的编号

如何进行鉴权合法性校验等?

在用户态再封装函数的时候就会进行一个前置检查。而在内核端也会再次判断。例如说是write函数的话,会判断write所写的地址是不是合法的(是否重叠其他进程)。

本质上就是说。用户态会有一套校验规则,内核态本身也会有一套后置校验的规则。

综上

内核的安全性非常重要

  • 将应用程序看作是恶意的(有点像面向失败编程

  • 没bug

宏内核 & 微内核

宏内核:将常用的所有都集成到操作系统内核态中。

内核代码量大 -> 容易出bug -> 不安全

模块多 -> 通讯快&效率高

微内核:内核中仅保留必需的模块。尽量将程序放到用户态中。

kernel

通过通信来进行模块之间的交流,但是如图,这样就会导致每一次通信都需要两次内核&用户空间之间的转换。

如何理解因为宏内核的集成,所以它的效率比微内核高呢?

主要是因为有page cache页缓存的存在。多个模块之间可以共享到到这个缓存,就减少了模块见写读的时空成本。 见教材中2.2

xv6的启动过程

entry.S

在操作系统启动的时候,一般来说是引导加载程序(GRUB)设置基本硬件环境,加载内核镜像,跳转内核入口点

在xv6中设置好这三者之后,会跳转到这里的这个kernel/entry.S

.S后缀指的是可以进行预处理的汇编文件.s的进化。通过预处理,可以让这些文件包含类似宏定义,条件编译等功能。

文件扩展名 预处理 描述
.asm 直接写的汇编语言代码,不包含预处理指令,直接交给汇编器处理。
.s 少许 一般是编译器生成的汇编代码,通常不包含预处理指令,直接交给汇编器处理。
.S 包含预处理指令(如宏、条件编译等),需要先经过预处理器(如 cpp)处理,然后交给汇编器处理。

下面结合其中的内容大致解释一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
	# qemu -kernel loads the kernel at 0x80000000
# and causes each CPU to jump there.
# kernel.ld causes the following code to
# be placed at 0x80000000.
.section .text # 标志.text块是代码块
.global _entry # 全局入口
_entry:
# set up a stack for C.
# stack0 is declared in start.c,
# with a 4096-byte stack per CPU.
# sp = stack0 + (hartid * 4096)
la sp, stack0 # 将stack0的地址传到sp 即栈顶寄存器 初始化栈的运行位置
li a0, 1024*4 # 确定栈的大小 4096
csrr a1, mhartid
addi a1, a1, 1
mul a0, a0, a1
add sp, sp, a0
# jump to start() in start.c
call start

# 正常来说 程序会阻塞在start函数启动后 是不会执行到这里的 此处的语句可看作异常处理阻塞
spin:
j spin

此处的stack0没有标注具体的值,怎么得到?

在start.c或其他文件中进行标志。在链接的时候(多个文件的值等链接到一起),entry.S该文件可通过符号引用查找到其对应的值或地址等。

start.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// entry.S jumps here in machine mode on stack0.
void start() {
// 设置MPP值 MPP是其中记录上一个权限模式的字段 决定mret时ret的模式
// 这里手动对他进行设置 设置当前的模式是machine mode 返回的模式则是kernel mode
unsigned long x = r_mstatus(); // 取值
x &= ~MSTATUS_MPP_MASK; // 清除对应字段中的值
x |= MSTATUS_MPP_S; // 设置为Supervisor即kernel模式为MPP中的值
w_mstatus(x); // 写回

// mepc寄存器是机器模式返回时的目标地址
// 这里可以理解为在机器模式中 发横中断之后 会以kernel(supervisor) mode返回到 哪一个位置
w_mepc((uint64)main);

// 禁用页表映射 暂时逻辑地址对应的就是真实的物理地址
w_satp(0);

// 下方有一些是0xffff类似的 这本质上是1 代表将权限设置为内核模式kernel / supervisor进行管理

// 将中断和异常的寄存器内容委托给内核
w_medeleg(0xffff); // 异常寄存器
w_mideleg(0xffff); // 中断寄存器
w_sie(r_sie() | SIE_SEIE | SIE_STIE | SIE_SSIE); // 启用supervisor模式下的各种中断

// 启用;并配置物理内存保护 配置为supervisor模式下可以访问整个物理内存
w_pmpaddr0(0x3fffffffffffffull);
w_pmpcfg0(0xf);

// ask for clock interrupts.
timerinit();

// 分配CPU自己本身的标识id
int id = r_mhartid();
w_tp(id);

// switch to supervisor mode and jump to main().
asm volatile("mret");
}

这个文件本质上是将机器模式的一些权限转交给supervisor模式 注册了定时器机器中断 最后直接以supervisor的权限模式return

以机器模式的身份配置 中断寄存器 异常寄存器等组件的使用权限

并且注册初始化配置定时器机器中断

通过MPP澄清当前为机器模式 并且设置mret之后的模式为supervisor模式 即内核模式

对于timerinit() 内联了一个主动让出线程资源的汇编脚本 以中断为介质 配置:

  1. 配置中断的触发条件(时间片的长度等) :具体方式是会有一个寄存器计算出下次中断的时间 一旦到达这个时间 就会自动触发中断

  2. 需要保存的数据结构scratch

  3. 中断的时候 需要执行的逻辑 :执行内联汇编脚本 让运行中的程序让出进程资源。

  4. enable机器中断

执行完这里 就到了kernel/main.c当中了

在main.c当中 会对各部分进行初始化 并且会创建运行用户空间的第一个进程userinit()

其中会进程uvminit()等工作 这些细说起来太多了 感觉可以单独写一篇

启动的时候会执行initcode.S当中的汇编代码 通过汇编执行init.c单独编译后的二进制可执行文件

这个过程本身进行了系统调用exec 执行失败的时候 会调用exit…

成功执行则启动sh.

启动是通过exec这一系统调用 这一系统调用的话则是使用了ecall 这一RISC-V的汇编命令

调用命令就会 从用户空间陷入内核空间 陷入就会跳转到trampoline.S