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