目录
硬件对于强隔离的支持
用户态/内核态
宏内核/微内核
进程
第一个进程
操作系统需要强隔离性
- 应用与应用之间
- 应用与操作系统之间
假设没有操作系统(破坏隔离性)
- (CPU角度)CPU从一个程序切换到另外一个程序,如果一个CPU并在上面运行shell,所以缺少OS情况下,shell时不时需要释放CPU资源;Shell中某个函数有死循环,其他程序没法运行或者杀死shell程序。此时无法实现Multiplexing。
- (内存角度)如果没有OS,应用程序直接运行在硬件上,物理内存一部分被Shell使用,一部分被echo使用,echo可以将数据存储在shell的内存地址导致覆盖shell内容,所以我们希望不同应用程序内存隔离。
所以,使用OS主要是为了实现multiplexing和内存隔离。
Unix接口抽象硬件资源
Unix接口被设计来实现资源的强隔离,接口通过抽象硬件资源,从而使得提供强隔离性成为可能。
这里理解为:OS通过文件描述符和IO多路复用实现了Multiplexing;内存隔离采用虚拟内存等技术。
硬件对于强隔离的支持
硬件对强隔离的支持包括:user/kernel mode 和 虚拟内存
- user/kernel mode(还有一种machine mode)
user mode执行普通权限指令:ADD、SUB、JR等
kernel mode执行特权指令:设置page table寄存器、关闭始终中断
判断当下是什么mode是通过处理器中的一个bit。 - 虚拟内存(内存强隔离)
基本所有的CPU支持虚拟内存。
处理器包含page table,而page table将虚拟内存地址与物理内存地址做了对应。
每个进程有自己独立的page table,这样每个进行只能访问出现在自己page table中的物理内存
概括来说,page table定义了对内存的视图,让每个用户程序有自己对于内存的独立视图。例如两个程序ls
、echo
,每个程序都有一个虚拟内存地址,从0~2^n;OS做的就是将两个程序的内存地址0映射到不同的物理内存地址。所以ls和echo不能访问对方的内存。(内核与应用程序也同理独立)
用户态/内核态
用户空间运行程序在user mode,内核空间程序运行在kernel mode。操作系统位于内核空间。
ls
运行程序,调用write
/read
系统调用;shell
调用fork
或者exec
- 如何让应用程序将控制权转移给内核?
RISC-V,有一个ecall
指令,ecall
接收数字参数,数字表示应用程序想调用的syscall
举个例子,shell
在用户空间执行fork
,并不是直接调用操作系统对应函数;而是调用ecall
,将fork
对应的数字作为参数传给ecall,在通过ecall
跳转到内核再调用fork
。
宏内核/微内核
- 宏内核:将操作系统的所以核心功能和服务(文件系统、驱动程序、网络协议等)包含在一个单一大内核
包含:Linux、Windows、BSD、xv6 - 微内核:功能最小化,文件系统、驱动程序、网络协议等运行在用户空间
进程
隔离的单元叫做进程,一个进程不能够破坏或者监听另外一个进程的内存、CPU、文件描述符,也不能破坏kernel本身。
为了实现进程隔离,一个进程为一个程序提供私有内存系统(或address space),其他进程不能读写该内存。xv6使用页表给每个进程分配自己的address space,即进程自己认为的虚拟地址,映射到RISC-V实际操作的物理地址。
虚拟地址从0开始向上依次:指令、全局变量、栈、堆。RISC-V指针为64位,xv6使用低38位,因此最大地址2^38-1=0x3fffffff=MAXVA
进程最重要的内核状态:1. 页表 p->pagetable
,2. 内存堆栈 p->kstack
,3. 运行状态p->state
每个进程中都有线程,是执行进程命令的最小单元,可以暂停/继续。
每个进程有两个堆栈,用户堆栈(user stack)和内核堆栈(kernel stack),进程在user space使用前者,kernel space 使用后者。
Code:第一个进程
简单描述如下:
加载xv6内核
-> _entry设置栈区并调用start()
-> start()进行配置并切换管理者模式,跳转到main() - kernel/main.c
-> main()初始化子设备等并运行inituser()
-> inituser() 创建第一个进程(initcode.S)
-> 执行exec(init, argv) -> init.c创建一个新的控制台设备文件,然后以文件描述符0、1和2打开它
-> 控制台运行一个shell
详细:
- RISC-V计算机上电,初始化自己并运行一个存储在只读内存的引导加载程序,
引导加载程序将xv6内核加载到内存
。在机器模式下,中央处理器从_entry(kernel/entry.S:6)开始运行xv6。 - 加载程序将xv6加载到物理位置为0x80000000的内存,前面的地址为I/O设备
_entry 指令设置一个栈区
,这样xv6可以运行C代码。xv6在start.c(kernel/start.c:11)文件中为初始栈stack0声明空间。由于RISC-V上的栈是向下拓展,所以_entry的代码将stack0+4096加载到栈顶指针寄存器sp中,现在有了栈区,_entry调用C代码start
kernel/entry.S
6: _entry:
…
10: # sp = stack0 + (hartid * 4096)
…
18: call start
- 函数start执行一些仅在机器模式下允许的
配置
,然后切换到管理模式
。RISC-V提供指令mret以进入管理模式,该指令最常用于将管理模式切换到机器模式的调用中返回。而start并非从这样的调用返回,而是执行以下操作:它在寄存器mstatus中将先前的运行模式改为管理模式,它通过将main函数的地址写入寄存器mepc将返回地址设为main,它通过向页表寄存器satp写入0来在管理模式下禁用虚拟地址转换
,并将所有的中断和异常委托给管理模式。 - 在进入管理模式之前,start还要执行另一项任务:对时钟芯片进行编程以产生计时器中断。清理完这些“家务”后,
start通过调用mret“返回”到管理模式
。这将导致程序计数器(PC)的值更改为main(kernel/main.c:11)
函数地址。
kernel/start.c
void start(){
…
47: // switch to supervisor mode and jump to main().
48: asm volatile(“mret”);
}
注:mret执行返回,返回到先前状态,由于start函数将前模式改为了管理模式且返回地址改为了main,因此mret将返回到main函数,并以管理模式运行
- 在main(kernel/main.c:11)初始化几个设备和子系统后,便通过
调用userinit
(kernel/proc.c:212)创建第一个进程,第一个进程执行一个用RISC-V程序集写的小型程序:initcode. S
(user/initcode.S:1),它通过调用exec系统调用重新进入内核
。正如我们在第1章中看到的,exec用一个新程序(本例中为 /init)替换当前进程的内存和寄存器。一旦内核完成exec,它就返回/init进程
中的用户空间。如果需要,init(user/init.c:15)将创建一个新的控制台设备文件
,然后以文件描述符0、1和2打开它。然后它在控制台上启动一个shell。系统就这样启动了。
kernel/main.c
void main(){
…
31: userinit(); // first user process
…
}
kernel/proc.c:212
void usreinit(void){…}
user/initcode.S
# exec(init, argv)
.globl start
start:
la a0, init
la a1, argv
li a7, SYS_exec
ecall
user/init.c
…
if(open(“console”, O_RDWR) < 0){
mknod(“console”, CONSOLE, 0);
open(“console”, O_RDWR);
}
dup(0); // stdout
dup(0); // stderr
…
if(pid == 0){
exec(“sh”, argv);
printf(“init: exec sh failed\n”);
exit(1);
}