Chapter 2 复用,隔离和交互——操作系统的三大要点


Chapter 2: Operating System Organization provides a deeper look at how an operating system, particularly xv6, is structured to manage processes, resources, and ensure isolation while enabling interaction between processes. Here’s a breakdown based on your request:

重要要点:

  • 进程管理与资源共享:操作系统需要管理多个并发进程,并确保每个进程有机会执行,即使 CPU 数量少于进程数。操作系统必须平衡 复用(共享资源)、隔离(防止进程互相干扰)、交互(允许进程间的通信)。
  • 内核结构与设计:本章介绍了 xv6 的内核组织,着重于单片内核(monolithic kernel)设计。虽然有些操作系统采用微内核设计(microkernel),xv6 选择了简单且高效的整体内核设计。

关键概念和名词:

  • Monolithic Kernel:一种将操作系统所有功能集成在一个内核中的设计。xv6 使用这种设计,其中所有的系统服务都运行在管理模式下。
  • Microkernel:与单片内核相对的设计,核心功能最小化,很多服务在用户空间运行。
  • System Calls:系统调用接口,允许用户进程请求内核提供服务,如 forkexec 等。
  • Page Table:页表是用于管理进程虚拟内存的结构,它帮助将虚拟地址映射到物理内存地址。
  • User Mode & Kernel Mode:两种CPU执行模式,用户模式下的应用程序无法直接访问操作系统资源,而管理模式(内核模式)下的操作系统可以访问所有硬件资源。
  • Fork & Execfork 用于创建新进程,exec 用于加载新程序并替换当前进程的内存映像。

需要了解的背景知识:

  • 进程管理:理解如何创建和管理进程,以及如何通过系统调用实现进程间的调度与通信。
  • 内存管理:掌握虚拟内存和物理内存之间的关系,理解如何通过页表进行内存映射。
  • RISC-V 架构:本章介绍了与 RISC-V 架构相关的一些概念,如不同的 CPU 执行模式(用户模式、管理员模式、机器模式),以及如何在不同模式下切换。

学习目标:

  • 了解操作系统如何组织资源和管理多个并发进程。
  • 能够理解进程的隔离、共享和交互机制。
  • 理解 xv6 如何实现进程的内存管理和调度,并能够跟踪系统调用的执行过程。
  • 掌握系统调用的实现与内核模式的切换机制。

重要代码和函数:

  • fork():创建一个新进程并复制父进程的内存空间,返回新进程的 PID 给父进程,返回 0 给子进程。
  • exec():加载并执行指定的可执行文件,替换当前进程的内存映像。
  • page_table:维护每个进程的虚拟内存空间,确保进程内存的隔离。
  • ecall & mretecall 指令用于从用户模式切换到内核模式,mret 用于从内核模式返回用户模式。

学习建议:

  • 实验实践:在理解这些概念后,进行实验并通过代码实现系统调用、进程管理等功能。
  • 文档阅读:细读 xv6 的源代码,特别是涉及进程创建、系统调用处理和内存管理的部分。

2.1抽象物理资源

应用程序通过与系统调用形成的库链接,然后调用硬件资源。
操作系统所承担的是中间层的责任。

所谓分层的思想,本质上是提供服务和隔离. 下层通过向上层暴露接口来为上层提供服务。 上下两层负责不同的任务,也将任务拆分成了两个部分,两层之中有一层出问题,另一层受到的影响也是有限的

因为需要通过库来使用硬件资源,这就要求一个进程不能长时间地占用硬件服务。也就是需要实现进程间的time-sharing
但是在每个进程看来自己的优先级是最高的,这也就需要一个大家长来实现资源的分配,也就需要进程间的隔离

而通过将硬件资源抽象为服务,可以实现强隔离,禁止应用程序直接访问硬件资源

Unix 应用程序仅通过文件系统的 open , read , write 和 close 系统调用, 而不是直接读写磁盘。这为应用程序提供了路径名的便利,并且允许操作系统(作为接口 的实现者)管理磁盘。即使隔离不是一个问题,有意交互(或只是希望不妨碍彼此)的程 序也可能会发现文件系统是比直接使用磁盘更方便的抽象。

如何实现time-sharing

通过给每个进程赋予不同的时间片。
使得看起来好像多个进程在同时运行,但本质上是串行运行

Unix 在进程之间透明地切换硬件 CPU,根据需要保存和恢复寄存器状态

2.2 用户模式、管理员模式和系统调用

强隔离需要应用程序和操作系统之间有硬边界。
CPU 为强隔离提供硬件支持。例如,RISC-V 具有三种 CPU 执行指令的模式:machine
mode , supervisor mode 和 user mode 。在机器模式下执行的指令具有完全特权;CPU 以机
器模式启动。机器模式主要用于配置计算机。Xv6 在机器模式下执行几行,然后更改为管
理模式。

使用多种模式的本质是权力的拆分,也就是隔离。

想要执行特权指令,调用内核函数的应用程序。
都需要切换到管理员模式,从userspace转移到kernelspace

应用程序不能直接调用内核函数。CPU 提供了一种特殊的指令,可以将 CPU 从用户模式切换到
管理模式,并在内核指定的入口点进入内核。(RISC-V 提供 ecall 指令用于此目的。)一旦 CPU 切换到管理模式,内核就可以验证系统调用的参数(例如,检查传递给系统调用的
地址是否是应用程序内存的一部分),决定应用程序是否允许执行请求的操作(例如,检
查应用程序是否允许写入指定的文件),然后拒绝或执行它。内核控制转换到管理模式的
入口点非常重要;例如,如果应用程序可以决定内核入口点,则恶意应用程序可以在跳过
参数验证的点进入内核。

带有文件系统服务器的微内核

2.3 内核组织

  • 一个关键的设计问题:操作系统的那一部分放到内核之中

monolithic kernel整体内核

  • 将整个整个操作系统驻留在内核中,以便所有系统调用的实现都在管理程序模式下运行。

优点:有一个大家长负责包办所有的事情,不必要区分什么是要做的,什么是不要做的。

缺点:内部接口复杂,把复杂性全部集中到了内核上,遇到问题,更容易树倒猢狲散。

microkernel微内核

为了降低内核出错的风险,操作系统设计者可以最大限度地减少在管理模式下运行的操作系统代码量,并在用户模式下执行操作系统的大部分代码。这个内核组织称为
microkernel 。

两种内核并不存在哪一个种更好的结论

整体内核速度更快,微内核更加稳定。

2.5 流程概览

进程抽象

在 xv6 和其他类 Unix 操作系统中,进程是最基本的隔离单位。进程的抽象防止一个进程能够破坏或监视其他进程的内存、CPU 和文件描述符等资源。此外,进程抽象还能够防止进程影响操作系统内核本身,因此,内核需要谨慎实现进程抽象,因为恶意应用程序可能会试图规避隔离措施。

进程抽象通过以下几种机制来实现:

  • 用户/管理程序模式标志:CPU 会通过标志位来区分代码是处于用户模式还是管理模式。
  • 地址空间:每个进程都有自己的虚拟地址空间,防止进程间相互访问。
  • 时间分片:操作系统对进程进行时间分配,保证每个进程都有机会执行。
为什么需要进程

进程抽象通过为每个进程提供独立的“幻觉”,使得每个进程都认为自己是唯一在运行的程序。实际上,操作系统通过时间分片技术,确保多个进程能并发执行,从而有效利用 CPU。

进程的实现

在 xv6 中,内核为每个进程维护许多状态信息,保存在 struct proc 中。这些状态信息包括进程的页表、内核堆栈和运行状态。

如何实现看似独立的内存

xv6 为每个进程维护一个单独的 页表,以定义该进程的地址空间。进程的虚拟内存从虚拟地址 0 开始,包括以下部分:

  • 指令区:存储程序代码。
  • 全局变量:存储全局变量的内存区域。
  • 堆栈区:存储局部变量和函数调用的堆栈。
  • 堆区:用于动态分配内存(如通过 malloc)。

为了支持从用户空间切换到内核空间,xv6 使用两个特殊的页面:

  • trampoline 页:用于代码转换和进程从用户模式返回到内核模式。
  • trapframe 页:用于保存和恢复用户进程的状态,以便进行上下文切换。
进程虚拟地址空间的布局
如何实现独有的 CPU

每个进程都有一个执行线程,并且进程在运行时会切换线程的上下文。线程的状态(如局部变量、函数调用的返回地址)会保存在 堆栈 中。进程切换时,内核保存当前进程的状态,并加载另一个进程的状态。

每个进程都有两个堆栈:用户堆栈内核堆栈。当进程执行用户代码时,仅使用用户堆栈;当进入内核模式(如进行系统调用时),则使用内核堆栈。

通过使用 ecall 指令,进程可以从用户模式切换到内核模式,执行内核中的系统调用。

总结

进程抽象包含两种重要设计思想:

  1. 地址空间:为每个进程提供了独立的内存空间,使得它们的内存不会互相干扰。
  2. 线程:为每个进程提供了独立的执行线程,使得每个进程能够独立执行。

通过这两种机制,操作系统能够有效地管理多个并发进程,并确保它们之间的隔离和独立性。


2.6 启动 xv6:第一个进程和系统调用

1. 计算机启动和引导加载程序

当 RISC-V 计算机启动时,首先进行自我初始化。启动过程从只读存储器(ROM)中的 引导加载程序 开始,作用是将操作系统内核(xv6)加载到内存中。内核被加载到物理地址 0x80000000 处,而 0x0 到 0x80000000 地址空间被预留给 I/O 设备。

2. 从机器模式开始执行内核

RISC-V 计算机在机器模式下启动,并执行引导加载程序的 _entry 函数。此时,虚拟地址直接映射到物理地址,分页硬件被禁用。

在内核代码执行前,需要设置初始堆栈空间。在 kernel/start.c 中,_entry 为堆栈声明空间并设置堆栈指针。

3. 从机器模式切换到管理员模式

一旦堆栈设置完成,_entry 会调用 C 函数 start,并完成一些初始化任务。随后,通过 RISC-V 的 mret 指令切换到 管理模式(supervisor mode),为后续的操作做准备。

4. 进入管理模式并进行配置

在管理模式下,内核继续进行系统配置,例如配置时钟芯片以触发定时器中断,为进程调度提供支持。

5. 初始化设备并创建第一个进程

main 函数中,内核初始化各种设备和子系统,然后调用 userinit 创建第一个用户进程。该进程通过执行 initcode.S 加载并执行 /init 程序。

6. 执行 exec 系统调用

initcode.S 中,通过 exec 系统调用加载 /init 程序,并替换当前进程的内存映像。

7. /init 程序和 Shell 启动

一旦 /init 被执行,内核返回到 /init 进程的用户空间。/init 会设置必要的设备文件并启动一个 shell,系统就此完全启动。


文章作者: MIKA
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 MIKA !
  目录