Organization and System Calls
概述
核心:操作系统的组织结构。又可分为四个部分:
Isolation,隔离性是设计操作系统组织结构的驱动力;
Kernel和User mode,这两种模式用来隔离操作系统内核和应用程序;
System calls,系统调用可以使应用程序转到内核执行,从而使用内核服务;
如何在
xv6中实现。
操作系统隔离性
不同的应用程序间需要有强隔离性,应用程序与操作系统间也要有强隔离性。而使用操作系统就可以实现multiplexing和Isolation。接口就是multiplexing与物理内存的隔离,通过抽象硬件资源的方式。
举例来说,fork创建进程,进程实际上是CPU资源的抽象,应用程序不能直接与CPU交互,只能与进程交互,操作系统内核会完成不同进程在CPU上的切换。所以,操作系统不是直接向应用程序提供CPU,而是提供进程作为CPU的抽象,这样可以在多个应用程序之间复用一个或多个CPU。
另外,我们可以认为exec抽象了内存,执行exec系统调用的时候,我们传入一个文件名,而这个文件名对应一个应用程序的内存镜像,里面包括了程序对应的指令、全局的数据。可以视为exec是操作系统提供在应用程序给硬件资源(这里是内存)之间的中间层,因为应用程序无法直接访问物理内存。
files也是一个例子,它是磁盘的抽象。应用程序不会直接读写磁盘本身。在Unix中,与存储系统交互的唯一方式就是通过files,这是磁盘的抽象,可以命名文件、读写文件等,由操作系统决定如何将文件与磁盘中的块对应。
操作系统防御性
另一个内核开发时需要考虑的是抵御来自应用程序的攻击,以确保所有组件都能工作。操作系统需要能够应当恶意的应用程序,并且应用程序不能打破对它的隔离。通常这种强隔离由硬件实现,第一部分是user/kernel mode(RISC-V中称为supervisor mode),第二部分是page table或者virtual memory。
硬件对于强隔离的支持
user/kernel mode:处理器主要有两种操作模式,user mode和kernel mode,两种模式下可以运行的指令权限不同。特殊权限指令主要是一些直接操纵硬件的指令和设置保护的指令。
处理器中
kernel mode与user mode是处理器的一个bit,为1是user mode,0是kernel mode。在RISC-V中,如果你在user space尝试执行一条特殊权限指令,用户程序会通过系统调用切换到kernel mode。当用户程序执行系统调用,会通过ECALL触发一个软中断,软中断会查询操作系统预先设定的中断向量表,并执行中断向量表中包含的中断处理程序(在内核中),这样就完成了mode的切换,并执行用户程序希望执行的特殊权限指令。
virtual memory:处理器中的
page table将虚拟内存地址与物理内存地址做了对应。每个进程有自己独立的一个page table,只能访问出现在自己page table里的物理内存。操作系统通过设置page table使每个进程的物理内存不重合,来实现内存的强隔离性。
User/Kernel mode切换
可以将user/kernel mode视为分隔用户空间和内核空间的边界。
在RISC-V中,ECALL指令专门用来实现应用程序将控制权转移给内核(Entering Kernel),它接受一个数字参数,这个参数表示应用程序想要调用的System Call。ECALL会跳转到内核中的一个特定位置,从这个接入点进入内核。在内核侧,有一个位于syscall.c的函数syscall,它会检查ECALL的参数以让内核知道是哪一个系统调用。
而对于每个系统调用,内核会进行特定的检查,例如
write会检查传递给write的地址是否属于用户应用程序,然后再执行。另外,内核会通过硬件设置一个定时器,到期后就会将控制权限从用户空间转移到内核空间。
宏内核Monolithic Kernel与微内核Micro Kernel
内核有时也被称为可信任的计算空间(Trusted Computing Base)即TCB。在xv6中,所有的操作系统服务都运行在kernel mode中,这种形式被称为Monolithic Kernel Design;另一种设计则是减少内核中的代码,将大部分的操作系统运行在内核外,就像一个普通的用户程序,这被称为Micro Kernel Design。宏内核的优势在于各种子模块位于一个程序中,可以很好的集成,而有不错的性能,劣势在于任何操作系统的Bug都会成为漏洞;微内核设计使得内核中的代码量小,意味着更少的Bug,但当应用程序需要与其他系统交互,需要完成两次用户空间-内核空间的跳转,性能更差。
编译运行Kernel
首先看一下代码结构:
kernel中包含基本上所有的内核文件,这些文件会被编译成二进制文件运行在kernel mode中;user中是运行在user mode的程序;mkfs会创建一个空的文件镜像,我们将这个镜像存在磁盘上,这样就可以直接使用一个空的文件系统。
关于内核的编译过程:
Makefile会读取一个C文件,以proc.c为例,调用gcc编译器,生产RISC-V汇编语言文件proc.s,再走到汇编解释器,生成proc.o,这是汇编语言的二进制格式;Makefile会为所有内核文件做这样的操作;系统加载器(Loader)会收集所有
.o文件,将它们链接在一起,并生成内核文件,这是我们将在QEMU运行的文件。同时,Makefile还会创建kernel.asm,这里面包含了内核的完整汇编语言,可以用来查Bug;
运行xv6:
传给QEMU的参数有:
-kernel
这里传递的是内核文件(kernel目录下的kernel文件),这是将在QEMU中运行的程序文件。
-m
这里传递的是RISC-V虚拟机将会使用的内存数量。
-smp
这里传递的是虚拟机可以使用的CPU核数。
-drive
传递的是虚拟机使用的磁盘驱动,这里传入的是fs.img文件。
QEMU
QEMU是RISC-V处理器的C语言仿真,在它的内部主循环执行的主要是以下步骤:
读取4字节或8字节的RISC-V指令;
解析RISC-V指令,并找出对应的操作码(op code);
在软件中执行相应的指令。
XV6启动过程
xv6从entry.s开始启动,这个阶段没有内存分页和隔离,并且运行在machine mode;接下来,
xv6会尽可能快的跳转到kernel mode,在main函数中进行许多初始化的操作,最后一个初始化是userinit函数;userinit函数会创建初始进程(这个进程的初始化在initcode中),返回到用户空间,执行指令,再回到内核空间,这是xv6的第一个系统调用;initcode首先将init中的地址加载到a0,将argv中的地址加载到a1,exec系统调用对应的数字加载到a7,最后调用ECALL,然后将控制权交给操作系统。总之,initcode通过exec调用init程序;
init会为用户进行一些设置,如配置console,调用fork,并在子进程中执行shell,最终效果为运行Shell。
Last updated