操作系统MIT6.S081:[xv6参考手册第2章]->操作系统组织结构

本系列文章为MIT6.S081的学习笔记,包含了参考手册、课程、实验三部分的内容,前面的系列文章链接如下
操作系统MIT6.S081:[xv6参考手册第1章]->操作系统接口
操作系统MIT6.S081:P1->Introduction and examples
操作系统MIT6.S081:Lab1->Unix utilities


前言

操作系统需要满足三大要求:多路复用、隔离、交互。

  • 操作系统的一个关键要求是支持并发。例如,通过使用系统调用接口fork,一个进程可以创建出一个新的子进程。
  • 操作系统必须在进程之间分时共享计算机资源。例如,即使进程数多于CPU的核心数,操作系统也必须确保所有进程都有机会执行。
  • 操作系统还必须使进程之间具有隔离性,也就是说如果一个进程发生错误或故障,它不应该影响其它不依赖于该错误进程的其它进程。
  • 完全的隔离也不太合适,因为进程之间有时需要交互,例如管道。

前言

①本章概述了如何组织操作系统来实现这三个要求。事实证明有很多方法可以做到这一点,但本文重点关注宏内核这一主流设计(许多Unix操作系统都使用宏内核)。本章还对xv6的进程(xv6中的隔离单元)和xv6启动时第一个进程的创建过程进行了概述。
②xv6运行在一颗多核RISC-V微处理器上,它的许多低级功能(如进程实现)是特定于RISC-V的。RISC-V是64位CPU,xv6是用LP64 C写的,这意味着C语言中的long(L)和pointer§是64位,而int是32位。本书假设读者已经对某些架构进行了一些机器级编程,并将在这些架构出现时介绍RISC-V特定的想法。
③计算机中的CPU被其所支持的硬件所包围,其中大部分以I/O接口的形式存在。xv6是以qemu的-machine virt选项模拟的硬件上编写的。这包括RAM、包含引导代码的 ROM、与用户键盘/屏幕的串行连接以及用于存储的磁盘。


一、抽象物理资源

应用程序直接与硬件交互

遇到操作系统时,人们可能会问的第一个问题是为什么需要它?也就是说,可以将图1.2中的系统调用实现为一个库,将应用程序与之链接。在这种设计中,每个应用程序甚至可以有自己的库来满足其需求。应用程序可以直接与硬件资源交互,并以最优的方式使用这些资源(例如实现高性能或可预测的性能)。一些嵌入式设备或实时系统的操作系统就是以这种方式组织的。
在这里插入图片描述
这种方法的缺点: 如果有多个应用程序在运行,则应用程序必须表现良好。例如,每个应用程序必须定期释放对CPU的占用,以便其他应用程序可以运行。如果所有应用程序相互信任并且没有错误,这种协作分时方案可能是可行的。但是一般来说,应用程序之间不信任彼此并且容易存在bug,因此需要为这些应用程序提供强隔离性。


抽象硬件资源

①为了实现强隔离、禁止应用程序直接访问敏感的硬件资源,将硬件资源抽象为服务是很有帮助的。例如,Unix的应用程序仅通过文件系统的openreadwriteclose系统调用与存储硬件交互,而不是让应用程序直接读写磁盘。这为应用程序提供了便利的路径名,并允许操作系统(作为接口的实现者)管理磁盘。即使隔离性不是问题,有意交互的程序(或只是希望彼此远离)也可能会发现使用文件系统比直接使用磁盘更方便和抽象。
②同样,Unix透明地在进程之间切换CPU,并根据需要来保存和恢复寄存器状态,因此应用程序不必知道时间共享。即使某些应用程序处于无限循环中,这种透明性也允许操作系统共享CPU。
③另一个例子,Unix进程使用exec来构建它们的内存图像,而不是直接与物理内存交互。这允许操作系统决定进程在内存中存放的位置。如果内存资紧张,操作系统甚至可能将一些进程的数据存储在磁盘上。exec还为用户提供了便利的文件系统来存储可执行程序图像。
④Unix进程之间的交互形式大部分都是通过文件描述符实现的。文件描述符不仅抽象了许多细节(例如,管道或文件中的数据存储在哪里),还简化了交互的方式。例如,如果管道中的一个应用程序出错,内核会为管道中的下一个进程生成文件结束信号。
最后: 图1.2中的系统调用接口经过精心设计,既方便了程序员,又提供了强隔离性。Unix接口不是抽象资源的唯一方法,但它已被证明是一种非常好的方法。


二、用户模式、管理员模式、系统调用

实现强隔离需要应用程序和操作系统之间有硬边界。如果一个应用程序出错,我们不希望操作系统或其他应用程序跟着出错。相反,操作系统应该能够清理出错的应用程序并继续运行其它的应用程序。为了实现强隔离,操作系统必须让应用程序不能修改(甚至读取)操作系统的数据结构和指令,并且应用程序不能访问其他进程的内存。


CPU为强隔离提供硬件支持。例如,RISC-V的CPU能以三种模式来执行指令:机器模式管理员模式用户模式

  • 在机器模式下执行的指令具有所有权限,CPU以机器模式启动。机器模式主要用于配置计算机,xv6在机器模式下执行一些代码后就切换到管理员模式。

  • 在管理员模式下,CPU允许执行特权指令:例如启用和禁用中断、读取和写入保存页表地址的寄存器等。如果处于用户模式的应用程序尝试执行特权指令,则CPU不会执行该指令,而是切换到管理员模式并终止该应用程序,因为它做了不应该做的事情。=

  • 应用程序只能在用户空间中运行,执行用户模式指令(例如添加数字等),而管理员模式的软件在内核空间中运行,执行特权指令。在内核空间(或管理员模式)中运行的软件称为内核。

  • 应用程序如果想要调用内核函数(例如xv6中的read系统调用),则必须转换到内核中。CPU提供了一条特殊指令能将CPU从用户模式切换到管理员模式,并从内核指定的入口点进入内核。(RISC-V为此提供了ecall指令)。

  • 一旦CPU切换到管理员模式,内核就可以验证系统调用的参数,决定是否允许应用程序执行请求的操作。从用户模式过渡到管理员模式的入口点内核控制,这一点很重要,因为如果应用程序可以决定内核入口点,恶意程序就可以跳过需要验证参数的入口点而直接进入内核。


三、内核组织结构

宏内核

一个关键的设计问题是操作系统中的哪些部分应该在管理员模式下运行。一种方案是让整个操作系统都驻留在内核中,因此所有系统调用都在管理员模式下运行,这种组织结构称为宏内核
①在宏内核这种组织结构中,整个操作系统拥有完整的操作硬件的权限。这种组织结构很方便,因为操作系统设计者不必决定操作系统的哪些部分不需要完整的硬件特权。此外,操作系统的不同部分之间更容易协作。例如,一个操作系统可能有一个缓冲区高速缓存,它可以由文件系统和虚拟内存系统共享。
②宏内核的一个缺点是操作系统的不同部分之间的接口通常很复杂(正如我们将在本文的其余部分看到的那样),因此操作系统开发人员很容易犯错。在宏内核中,任何错误都是致命的,因为管理员模式下的一个错误往往会导致内核出现故障。如果内核出现故障,计算机将停止工作,所有应用程序也会崩溃,计算机必须重新启动。


微内核

为了降低内核中出错的风险,操作系统设计人员可以最大限度地减少在管理员模式下运行的操作系统代码量,并在用户模式下运行大部分的操作系统代码。这种内核组织结构称为微内核
①图2.1展示了这种微内核设计。在图中,文件系统作为用户级进程运行。作为进程运行的操作系统服务称为服务。为了允许应用程序与文件服务交互,内核提供了一种进程间通信机制来将消息从一个用户模式进程发送到另一个用户模式进程。例如,像shell这样的应用程序如果想要读取或写入文件,它会向文件服务发送消息并等待响应。
在这里插入图片描述
②在微内核中,内核接口由一些低级函数组成,用于启动应用程序、发送消息、访问设备硬件等。微内核相对简单,因为操作系统大部分功能实现在用户级服务。


xv6的组织结构

与大多数Unix操作系统一样,xv6采用宏内核的组织结构。因此,xv6内核接口对应操作系统接口,内核实现了完整的操作系统。由于xv6不提供很多服务,它的内核比一些微内核还小,但从概念上讲xv6是宏内核。


四、代码:xv6组织方式

xv6内核源代码位于kernel/子目录中,源代码按照模块化的概念分为多个文件。下表列出了这些文件,模块间的接口在defs.h(kernel/defs.h)中定义。

文件描述
bio.c文件系统的磁盘块缓存
console.c连接到用户的键盘和屏幕
entry.S首次启动指令
exec.cexec()系统调用
file.c文件描述符支持
fs.c文件系统
kalloc.c物理页面分配器
kernelvec.S处理来自内核的陷入指令以及计时器中断
log.c文件系统日志记录以及崩溃修复
main.c在启动过程中控制其他模块初始化
pipe.c管道
plic.cRISC-V中断控制器
printf.c格式化输出到控制台
proc.c进程和调度
sleeplock.c睡眠锁
spinlock.c自旋锁
start.c早期机器模式启动代码
string.c字符串和字节数组库
swtch.c线程切换
syscall.c调用系统调用
sysfile.c文件相关的系统调用
sysproc.c进程相关的系统调用
trampoline.S用于在用户和内核之间切换的汇编代码
trap.c对陷入指令和中断进行处理并返回的C代码
uart.c串口控制台设备驱动程序
virtio_disk.c磁盘设备驱动程序
vm.c管理页表和地址空间

五、进程概述

  • xv6中的隔离单元(与其他Unix操作系统一样)是进程。将进程抽象可以防止一个进程破坏或监视另一个进程的内存、CPU、文件描述符等。它还可以防止一个进程破坏内核本身,这样一个进程就不会破坏内核的隔离机制。内核必须小心地实现进程抽象,因为有缺陷或恶意的应用程序可能会诱使内核或硬件做坏事(例如绕过隔离)。内核用来实现进程的机制包括用户/管理员模式标志、地址空间、线程的时间切片。

  • 为了帮助实现强隔离,进程抽象为程序提供了一种错觉,即它拥有一台属于自己的私有计算机。进程为程序提供了一个看起来像私有内存系统或地址空间的东西,其他进程无法读取或写入。进程还为程序提供了似乎是它自己的CPU来执行程序的指令。

  • xv6使用页表(由硬件实现)为每个进程提供自己的地址空间。RISC-V页表将虚拟地址(RISC-V指令操作的地址)转换(映射)为物理地址(CPU芯片发送到主存储器的地址)。

  • xv6为每个进程维护一个单独的页表,这个页表定义了进程的地址空间。如图2.3所示,地址空间包括从虚拟地址0开始的进程的用户内存。首先是指令区,然后是全局变量区,然后是栈区,最后是进程可以根据需要扩展的堆区(用于malloc)。有许多因素限制了进程地址空间的最大大小:RISC-V的指针是64位,硬件在页表中查找虚拟地址时只使用低39位,而xv6仅使用这39位中的38位。因此,最大地址为 2 38 − 1 = 0 x 3 f f f f f f f f f \rm{2^{38} - 1 = 0x3fffffffff} 2381=0x3fffffffff,即MAXVA(kernel/riscv.h:348)。在地址空间的顶部,xv6为trampoline(用于在用户和内核之间切换)和映射进程切换到内核的trapframe分别保留了一个页面,正如我们将在第4章中解释的那样。
    在这里插入图片描述

  • xv6内核为每个进程维护许多状态片段,并将它们收集到一个结构体proc (kernel/proc.h:86)中。一个进程最重要的内核状态片段是它的页表、内核栈、运行状态。我们将使用p->xxx表示proc结构体的元素。例如,p->pagetable表示指向进程页表的指针。

  • 每个进程都有一个执行线程(或简称线程)来执行进程的指令。线程可以被暂停并在稍后恢复运行。为了在进程之间透明地切换,内核暂停当前正在运行的线程并让另一个进程的线程恢复运行。线程的大部分状态(局部变量、函数调用返回地址)都存储在线程的栈中。每个进程有两个栈:用户栈和内核栈(p->kstack)。当进程在执行用户指令时,只使用它的用户栈,它的内核栈是空的。当进程进入内核(用于系统调用或中断)时,内核代码在进程的内核栈上执行。当一个进程在内核中运行时,它的用户栈仍然包含保存的数据,但没有被使用。一个进程的线程在主动使用它的用户栈和内核栈之间交替执行。内核栈是独立的(并且不受用户代码的影响),因此即使进程破坏了其用户栈,内核也可以执行。

  • 进程可以通过RISC-V的ecall指令来执行系统调用。该指令提高了硬件权限级别并将程序计数器更改为内核定义的入口点。入口点的代码切换到内核栈中并执行实现系统调用的内核指令。系统调用完成后,内核切换回用户栈,通过调用sret指令返回用户空间,降低硬件权限,在系统调用指令执行后恢复执行用户指令。进程的线程可以在内核中阻塞以等待I/O,并在I/O完成后从中断处继续执行。

  • p->state表示进程是否已分配、准备运行、正在运行、等待I/O或退出。

  • p->pagetable以RISC-V硬件期望的格式保存进程的页表。xv6导致分页硬件在用户空间中执行该进程时使用进程的p->pagetable。进程的页表也用作分配用于存储进程内存的物理页的地址记录。


六、代码:启动xv6和第一个进程

为了更具体地描述xv6,我们将概述内核如何启动和运行第一个进程,后续章节将更详细地描述本概述中展示的机制。

  • 当RISC-V计算机开机时,它会自行初始化并运行存储在只读存储器中的引导加载程序,引导加载程序将xv6内核加载到内存中。然后在机器模式下,CPU从_entry (kernel/entry.S:6)开始执行xv6。RISC-V从禁用分页硬件开始:虚拟地址直接映射到物理地址。

  • 加载程序将xv6内核加载到物理地址0x80000000的内存中。将内核放置在0x80000000而不是0x0的原因是因为地址范围0x0:0x80000000包含I/O设备。

  • _entry处的指令设置了一个栈,以便xv6可以运行C代码。xv6在文件start.c(kernel/start.c:11)中为初始堆栈stack0声明了空间。- - _entry处的代码将栈顶地址stack0+4096加载到栈指针寄存器sp,这是因为RISC-V的栈是向下扩展的。现在内核有了栈区,- - _entry调用C代码start(kernel/start.c:21)

  • 函数start执行一些仅在机器模式下允许的配置,然后切换到管理员模式。要进入管理员模式,RISC-V 提供了指令mret。该指令最常用于从前面将管理员模式切换机器模式的调用中返回。start并不是从这样的调用返回,而是执行以下操作:它在寄存器mstatus中将先前的权限模式设置为管理员模式,它通过将main函数的地址写入寄存器mepc将返回地址设置为main,通过将0写入页表寄存器satp来禁用管理员模式下的虚拟地址转换,并将所有中断和异常委托给管理员模式。

  • 在跳转到管理员模式之前,start执行了另外一项任务:对时钟芯片进行编程以产生定时器中断。处理完这些事务后,通过调用mret返回到管理员模式。这会导致程序计数器的值变为main(kernel/main.c:11)函数地址。

  • 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,系统就这样启动起来了。


七、真实世界

在现实世界中,我们可以在许多地方可以看到宏内核和微内核这两种组织方式。 许多 Unix采用宏内核的。 例如,Linux是宏内核架构,尽管Linux的一些操作系统功能作为用户级服务运行(例如窗口系统)。而如L4、Minix和QNX等内核被组织为带有多个服务的微内核,并在嵌入式环境中得到广泛应用。大多数操作系统都采用了进程的概念,并且大多数进程看起来与xv6的相似。然而,现代操作系统在一个进程中支持多个线程,以允许单个进程利用多个CPU。在一个进程中支持多个线程涉及到很多xv6所没有的机制,包括潜在的接口更改(例如Linux的clonefork的变体),以控制进程线程共享哪些内容。

  • 5
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

知初与修一

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值