现代操作系统:原理与实现(第二章)

第二章 硬件结构


冯 诺依曼结构

主要包含三个部分:

  • 中央处理单元(Central Processing Unit,CPU):主要负责运算与逻辑控制。
  • 存储器(memory unit):负责存储程序指令和数据,以及保存程序执行的中间结果和最终结果。
  • 输入输出(Input and Output ,I/O):负责与外界进行交互,从外界获得输入,将结果向外界输出。

image-20211225154135650

目前主流的计算机依然采用的是冯诺依曼架构。

CPU与指令集架构

指令集架构(Instruction Set Architecture,ISA)是CPU和软件之间的桥梁。

ISA 包括指令集、特权级、寄存器、执行模式、安全扩展、性能加速扩展诸多方面。

指令集

指令集是ISA的重要组成部分,通常包含一系列不同功能的指令,用于数据搬移、计算、内存访问、过程调用等。

CPU在运行操作系统或应用程序时,实际上实在执行它们被编译后所包含的指令。

本书选择AArch64体系结构作为介绍操作系统的主要平台,其指令集属于精简指令集计算机(Reduced Instruction Set Computer,RISC)

AArch64中每条指令的长度固定为4字节,指令类型包括:

  • 数据搬移指令(如mov);
  • 寄存器计算指令(如加法指令add、减法指令sub);
  • 内存读写指令(如内存加载指令ldr、内存写入指令str);
  • 跳转指令(如无条件跳转指令b);
  • 过程调用指令(调用指令bl、返回指令ret);
  • 特权指令(如读写系统)

ARM处理器在嵌入式系统和智能手机等领域被广泛应用,其最新的商用体系结构是ARMv8。ARMv7及之前的ARM体系结构至多只支持32位虚拟地址,而ARMv8支持两种执行模式——AArch32AArch64,前者主要为了向前兼容,本书主要关注后者AArch64,即支持64位虚拟地址的AArch64,也称之为AArch64体系结构。

x86-64体系结构的指令集属于复杂指令集计算机(Complex Instruction Set Computer,CISC)。通常来说,相比于RISC,CISC具有指令数量更多、指令编码长度可变和指令寻址方式多样等特点。

特权级

特权级也是ISA中的重要组成部分,AArch64中的特权级被称为异常级别(Exception Level,EL),共有四种特权级。

image-20211225164935955

  • EL0:最低的特权级,应用程序通常运行在该特权级,也称为用户态
  • EL1:操作系统通常运行在该特权级,也称为内核态
  • EL2:在虚拟化场景下需要,虚拟机监控器(Virtual Machine Monitor,VMM,也称为Hypervisor)通常运行在该特权级
  • EL3:和安全特性TrustZone相关,负责普通世界(normal world)安全世界(secure world)之间的切换。

TrustZone是从ARMv6体系结构开始引进的安全特性,如今已被广泛使用。该特性从逻辑上将整个系统分为安全世界和普通世界,计算资源可以被划分到这两个世界中。安全世界可以不受限制地访问所有的计算资源,而普通世界不能访问被划分到安全世界中的计算机资源。比如说,普通世界不能访问属于安全世界的的物理内存和设备。

CPU在提供四种特权级的同时也提供了特权级之间切换的方式。

通常应用程序运行在EL0,而操作系统运行在EL1,所以这里主要介绍EL0和EL1之间常见切换场景

一般来说,从EL0(应用程序)切换到EL1(操作系统)的可能场景有三种:

  1. 应用程序需要调用操作系统提供的系统调用,此时应用程序会通过执行svc(特权调用,supervisor call)指令将CPU特权级从EL0切换到EL1。
  2. 应用程序执行了一条指令,而该指令触发了异常(exception),该异常导致CPU特权级从EL0切换到EL1。
  3. 应用程序在执行的过程中,CPU收到一个来自外设的中断(interrupt),该中断也会导致CPU特权级从EL0切换到EL1。

前两种场景往往统称为同步的CPU特权级切换,因为他们的特权级都是由CPU中正在执行的指令所导致的。

第三种场景下CPU发生的特权级切换并不是由于CPU运行程序过程中的指令而导致的,属于异步的CPU特权级切换。

下图EL1和EL0之间切换的基本流程,该流程通常由CPU和操作系统内核协同完成。

具体来说,在发生特权级切换的时刻,CPU负责保存当前执行状态,以便操作系统内核在处理异常时使用,并在处理结束后能够快速恢复应用程序的执行。

CPU保存的主要状态包括:

  • 触发异常的指令地址(即当前程序计数器(Program Counter,PC))
  • 异常原因(即异常是由于执行svc指令还是由于访存缺页导致的)
  • CPU将栈指针(Stack Pointer ,SP),从SP_EL0(应用程序使用的栈)切换到SP_EL1(操作系统可以通过设置这个寄存器来配置异常处理过程中使用的栈)
  • CPU还会保持一些其他状态,例如将CPU的相关状态保存在SPER_EL1(已保存的程序状态寄存器,Saved Program Status Register)中,将引发缺页异常的地址保存在FAR_EL1(错误地址寄存器,Fault Address Register)中。

image-20211225212206669

异常处理完成后,操作系统会恢复应用程序的上下文,然后执行eret(异常返回,Exception Return )指令,以恢复CPU自动保存的EL0状态(包括PC和SP等),并切换回EL0,使应用程序从被中断处继续执行。


寄存器

寄存器是ISA的另一个重要组成部分。在AArch64中,有31个64位通用寄存器,被命名为X0~X30。

其中,X29用作帧指针(Frame Pointer,FP)寄存器,按照使用惯例,一般用于保存函数调用过程中栈顶的地址;X30用作链接指针(Link Pointer,LP)寄存器,因为CPU在执行函数调用指令b1时会自动把返回地址保存在其中。

在EL1特权级下有两个页表基地址寄存器(Translation Table Base Register,TTBR),即TTBR0_EL1TTBR1_EL1,它们负责翻译虚拟地址空间中不同地址段,负责的地址范围由另一个控制寄存器TCR_EL1(翻译控制寄存器,Translation Control Register)决定。

操作系统中一种常见的配置是TTBR0_EL1负责 [0, 248 )的地址映射(作为用户地址空间),TTBR1_EL1负责 [248,264)的地址映射(作为操作系统内核地址空间)

image-20211225222924332


物理内存与CPU缓存

CPU在执行过程中,可以通过访存指令向物理内存中写入数据或者从中读取数据。

CPU使用物理内存的方式:通过总线向物理内存发送一个读写请求,其中包括目标地址(若是写请求,则还包括写入值),物理内存在收到请求后进行读写操作(若是读请求,则将读取值发送回CPU)。

因此从CPU的角度,可以把物理内存看做由字节组成的大数组:其中每一个字节拥有一个地址(物理地址),CPU可以在这个数组中存取数据。

CPU的处理速度相比与内存访问速非常快,为了降低CPU访问内存的开销,现在的CPU中引入了缓存(cache),用于存放一部分物理内存中的数据。访问CPU缓存比访问物理内存快得多,一般最快只需要几个时钟周期。当CPU需要向物理内存写入数据的时候,他可以直接写在CPU缓存当中,若缓存已满,则写回内存;当CPU需要从物理内存读取数据时,它可以先在CPU缓存中查找,如果没有找到再去物理内存中获取,并且把取回的数据放入缓存当中,以便加快下次读取速度。

image-20220121212017942

缓存结构

CPU缓存是由若干个缓存行(cache line)组成的。每个缓存行包括:一个有效位(valid bit),用于表示其是否有效;一个标记地址(tag address),用于标识其对应的物理地址;一些其他的状态信息。通常,CPU以缓存行(常见的是64字节)为单位把物理内存中的数据读取到CPU缓存当中;也就是说,即使只需要单个字节的值,该字节对应的缓存行也会全部进入缓存中。同样,将数据写入到物理内存也是以缓存行为单位的。

image-20220121213332084

缓存寻址

以AArch64架构的Cortex-A57 CPU中的L1数据缓存为例,给出CPU缓存查找的一般过程。该CPU缓存的相关参数如下:

  • 物理地址的长度为44位
  • 缓存大小为32KB,缓存行大小为64字节
  • 256组,2路组相联缓存。

由于软件的运行通常具有局部性(时间局部性、空间局部性),因此缓存能够有效提升CPU访问物理内存数据的性能;另外,操作系统和应用程序也可以根据CPU缓存的特点对代码实施优化,从而更好地利用缓存以提升性能。

设备与中断

本节介绍CPU与设备之间的主要交互方式。

内存映射输入输出

内存映射输入输出(Memory-Mapped I/O,MMIO)是一种常见的CPU控制和访问设备的方式

MMIO的原理是:把输入输出设备和物理内存放在同一个地址空间为设备内部的内存和寄存器也分配相应的地址。

CPU可以使用和访问物理内存一样的指令(ldrstr)去读写这些属于设备的地址;设备会通过总线监听CPU分配给自己的地址,然后完成相应的CPU访问请求。

轮询与中断

CPU通过MMIO配置的地址可以获取输入,但是CPU如何才能知道有输入事件发生了?

一种可行的做法是轮询(polling),即让CPU不断去通过MMIO查看UART是否有输入。但是这种做法会使得CPU长时间都处于等待UART输入的状态,造成CPU的浪费。

一种做法是中断(interrupt)机制就赋予了设备通知CPU的能力。设备通过向CPU发出中断来打断CPU的执行,使得CPU去处理这个中断。中断机制除了使得设备能够主动通知CPU外,还包括让一个CPU核心去通知另一个CPU核心等用途。

MMIO使得CPU可以主动地访问设备,中断使得设备能够主动地通知CPU,这两种机制是CPU与设备之间交互的重要方式。

  1. 从应用程序的视角来说,异常与中断的区别是什么?

    CPU在运行的过程中,也会被各种“异常”打断,这些“异常”有:

    ·指令未定义;

    ·指令、数据访问有问题;

    ·SWI(软中断);

    ·快中断;

    ·中断;

    中断也属于一种“异常”,导致中断发生的情况有很多,比如:

    ·按键;

    ·定时器;

    ·ADC转换完成;

    ·UART发送完数据、收到数据;

    这些众多的“中断源”,汇集到“中断控制器”,由“中断控制器”选择优先级最高的中断并通知CPU。

  2. 在发生特权级切换时,CPU会自动保存当前执行状态,包括:程序计数器(Program Counter,PC)、栈指针(Stack Pointer,SP)等。请分析:如果不保存程序计数器和栈指针,会出现什么问题?

​ CPU在处理异常结束后无法恢复到之前应用程序的执行状态。

  1. 正文中介绍,操作系统在异常处理函数开始执行时,通常该函数会先对应用程序的上下文进行保存(比如通用寄存器),这和CPU自动保存执行状态是否有冲突?请分析为什么需要软硬件一起来保存状态。

  2. 假设现在有一个很大的数组(远远大于缓存大小),请思考:在有缓存的情况下,是随机读取在这个数组上一字节的数据一万次,还是连续读取一万字字节块?

连续读取一万字快,连续读取可减小访问内存的次数,减小页面切换的开销。

  1. 轮询是否在任何情况下都比中断差?请结合具体的例子进行分析。
  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!
  • Copyrights © 2022-2024 lk
  • 访问人数: | 浏览次数:

请我喝杯咖啡吧~

支付宝
微信