Ch8 编译原理 I ⁄ O设备:汇编语言视角

I ⁄ O device:from the prespective of assembly language

Posted by R1NG on December 24, 2020 Viewed Times

Ch8 I/O 设备: 汇编语言视角

Motivation:

  1. 了解程序与外设通讯的主要方式: 轮询和中断
  2. 明确中断发生时程序所需要进行的操作

1. 外设与轮询通讯方式

1.1 内存映射

一种使计算机和外设能够实现双向通讯的方式是内存映射 Memory Mapping:

  1. 将外设本身通过某种方式映射到一个不被占用的内存地址上, 而处理器从外设读取数据或传输数据到外设的行为被抽象为对这个内存地址上的内存位置的读取
  2. 由于该方法将外设抽象为了一个内存位置, 因此对该外设的操作理论上速度等同于对内存读写的速度 下面以键盘上的一个按键为例: 我们将键盘上的某个特定按键和一个不被占用的内存地址所绑定, 并且将键盘编程为: 当它检测到地址总线上出现了这个内存地址时, 就将按键输出连接到数据总线中传递给处理器:

20210106151550

在上图所示的例子中, 我们将键盘按键 q 和内存地址 $10100000_2$ 绑定. 当这个内存地址出现在内存总线上时, 逻辑电路就会将数据总线和键盘的数据链路链接. 而当这个按键被按下时, 开关闭合, 接地链路的电位被拉高, 因此在数据总线上传输 $01110001_2$.

从物理上说, 这样的电路和通讯方式完全是可行的. 但如果采取这样的通讯方式的话, 即使对键盘而言, 我们也需要为每一个按键甚至每一种组合键分配一个单独的内存地址; 并且每一次对键盘的 “检查” 都需要依次在内存总线上传输这些被分配的内存地址.

我们不难发现: 实际上, 为每一个按键分配一个单独的内存地址是毫无必要的, 我们只需要为键盘这一个外设分配内存地址, 并且让键盘在响应每一个按键按下 (物理) 事件时向数据总线传输一个不同的值, 比如具有唯一性的 ASCII 码值, 通过检测这个传入的码值判断按下了哪一个按键即可.


1.2 轮询和握手

在基于处理器主动定时检查键盘上的按键是否被按下 (即采用 轮询 的方法收集外设数据) 的前提下, 处理器将会执行以下的操作:

1
2
3
4
5
6
7
8
9
10
11
12
loop    ADR     R1, Status_Reg     ;load R1 to Status_Reg
        LDRB    R0, [R1]           ;read status
        TST     R0, #0x80          ;test the ready bit (bit7) 
        BEQ     loop               ;try again if not ready

        ADR     R1, Data_Reg       ;if ready, store R1 to Data_Reg
        LDRB    R0, [R1]           ;ready for reading the key code

...
; then do what needs to be done
...
        B       loop                ;and branch back to check again...

这一方法即称为 轮询方法:

  1. 处理器定期检查外设
  2. 若在检查时检测到外设传入了数据或某种状态发生了变化, 则依照实际情况执行响应的操作

然而, 处理器的运行速度比人们使用键盘输入信息的速度快得多, 并且还存在着键盘不被使用的情况. 也就是说, 在处理器对键盘进行轮询时, 实际上有大量的检测是被浪费的, 而每一次轮询中对外设的检测都会消耗本可以被用于执行其他程序的处理器时间, 这些处理器时间在大量无意义的轮询中被浪费了. 要避免这些无意义的浪费, 我们有两个方法:

  1. 握手: 为外设专门分配一个状态寄存器用于告知处理器是否有必要执行轮询操作:
    还是基于键盘的例子. 我们可以为键盘分配一个寄存器用来存储按键按下的状态, 该状态寄存器的值将会在键盘按键被按下时存储被按下按键的编码, 而在处理器读取完成后清零.

然而, 在这一方法中, 外设需要被分配一个独立的状态寄存器, 并且要具有和处理器进行双向数据传输的能力. 一般来说, 这样的设备需要具备多个寄存器, 比如一个数据寄存器 (基于具体情况, 这个数据寄存器还可以是双向的), 一个控制寄存器用于控制数据传输方向, 外设行为和设备界面, 一个状态寄存器. 尽管用于和这样的设备交互的程序相对简单, 但这样的交互方式对硬件提出了不小的要求.

总的来说, 轮询的交互方式具有以下特点:

  1. 轮询消耗和浪费大量的处理器时间
  2. 基于轮询的交互方式在软件实现方面要求很低, 程序简单
  3. 轮询用于一些构造简易的系统中
  4. 轮询不适用于较复杂和现代化的系统

握手方法并不能真正解决轮询的局限性. 要彻底解决处理器时间浪费的问题, 处理器需要使用不同的方法和外设进行交互, 这就是 中断.


2. 中断

2.1 中断及其原理

在轮询交互方式中, 检测事件发生的主动权在处理器一方, 这正是处理器时间浪费的原因由来. 在中断交互方式中, 处理器只负责接收和处理事件, 而事件的发生与否由外设本身负责. 在没有任何事件发生时, 处理器持续执行当前的任务而不去主动检测外设; 而当事件发生时, 外设将会向处理器发送中断信号. 处理器在接收到中断信号后, 在暂停手头的任务之后对这一事件进行处理, 在处理完事件后继续回头处理之前的任务. 只要事件不发生, 处理器就不会消耗时间在任何一个外设上.


2.2 中断执行流程和状态保护

在本文中, 我们主要讨论硬件中断. 这样的中断由用户和外部世界发起, 具有不确定性. 下面我们讨论处理器和程序在接收中断/被中断时, 所需要执行的操作是什么:

下面我们来看一个简单的例子:

20210106154414

从软件执行的层面上看, 中断的全过程可以被视为发生了一次函数跳转. 因此, 在执行函数跳转时, 就需要进行状态保护 (包括状态保存和状态恢复):

  1. 在完成中断处理后, 处理器必须返回执行原来的程序, 并且确保中断处理的流程对原程序的执行不造成任何影响
  2. BL 指令所产生的效果不同, 中断造成的跳转必须保护和恢复包括状态寄存器 CPSR, 程序计数器 PC 在内的所有寄存器状态. 这一步骤一般交由处理器和中断服务进程执行.

下面, 我们讨论外设是如何从硬件层面 “发起” 中断的:
在中断交互方式下, 处理器不能主动向设备发起检测, 外设是通过发送 IRQ (Interrupt ReQuest) 信号来 “引起处理器的注意” 的:

20210106160931

我们继续讨论, 硬中断请求优先级别处理程序 (IRQ Handler) 如何处理多设备中断:
若多个设备同时发起了中断请求, 硬中断请求优先级别处理程序需要获取发起中断的外设的设备类型, 并基于某种优先级决定哪些设备的中断请求将会优先受理.

下面是一个实现多设备中断的简单硬件:

20210106165404

IEn, IRq 均为连接到数据总线上的, 映射至内存的寄存器. 通过向 IEn 中写入数据, 处理器可以选择性地允许某些设备的中断请求; 通过读取 IRq 的值, 处理器可以确定是什么设备 (或者哪些设备) 导致了中断发生, 并清除该寄存器.

相应地, 硬中断请求优先级别处理程序 (IRQ Handler) 必须在确定哪个设备造成了中断的前提下, 服务这个设备的中断请求. 这需要我们为每一个 (每一类) 不同的设备单独创建一个中断服务程序. 和硬中断请求优先级别处理程序一样, 每一个中断服务程序也要能够保护任何它将使用的寄存器.

基于上面的例子, 在处理多设备中断时, 硬中断请求优先级别处理程序 (IRQ Handler) 首先会从 IRq 寄存器中读取数据以确定哪一个 (哪一类) 设备造成了中断, 随后基于所谓的 “中断向量表” (Interrupt Vector Table) 或一个存有程序指针的数组, 跳转到设备特定的中断服务程序:

当跳转到设备特定的中断服务程序时, 服务程序自身首先会保护所有将要使用的寄存器, 随后处理中断请求, 再清空 IRq 寄存器中的内容, 恢复被保护的寄存器, 最后跳转回硬中断请求优先级别处理程序 (IRQ Handler) 的退出窗口 (exit code).

20210106170729


2.3 其余的 ARM 硬件特征

ARM 架构规定, 当处理器重置时, 状态寄存器 CPSRIRQ 中断禁止位 <7>被设为 $1$:

20210106161143

这意味着, 处理器在满足相关条件, 进入可以接收中断的状态前, 所有传入的中断请求都会被忽略.

中断禁止位的存在非常重要. 一方面, 一些操作可能会无条件地收到传入的中断请求的影响而无法完成, 因此在这一情形下必须忽略一切可能传入的中断; 从另一角度看, 由于计算机往往会外接大量不同的外设, 中断静止位提供了一种避免外设间产生冲突的方法. 不过在本文中, 我们只考虑简化的情况: 计算机只外接一个设备.

总的来说, 当中断发生时, 处理器将会执行以下的操作:

  1. 完成当前进行的这条指令
  2. 将链接寄存器內的值 $+4$, 保证在处理完中断操作跳转回来时程序执行无缝衔接
  3. 将状态寄存器 CPSR 的值存储至它的物理备份寄存器 SPSR
  4. CPSRIRQ 中断禁止位设为 $1$, 忽略一切中断请求
  5. 将程序计数器指向 $00000018$.

[注]

  1. 中断请求被视为一种 “异常” (Exception), 在这一异常发生时, 处理器始终会转去执行位于内存地址 0x18 处所存储的指令 (实际上就是非向量中断).
  2. 0x18 是一个单字指针, 一般包含一个类似于 B IRQ_Handler 的指令, 处理器在执行后将会跳转到真正的硬中断请求优先级别处理程序进行下一步的操作.
  3. 硬中断请求优先级别处理程序需要具备以下的功能:
    • 保护所有自身将要使用的寄存器
    • 检测和确定引起中断的是哪一个设备
    • 处理中断请求
    • 恢复被保护的寄存器
    • 恢复状态寄存器 CPSR
    • 跳转回原程序, 结束中断.

20210106163259


3. 直接内存访问 DMA

直接内存访问为外设存储/读取大量数据提供了便利. 下面观察一个在不使用直接内存访问的情况下, 将大量数据从磁盘写入到内存中的过程:

  1. 首先在内存中预留一段用于承接数据的空间
  2. 初始化目标内存区块的起始地址 (内存指针)
  3. 磁盘发起中断请求并输出 (提供) 一个位
  4. 中断服务程序 ISR 接收并将这个位存入内存中
  5. 内存指针递增, 并结束中断处理返回原程序
  6. 重复步骤 $3-6$, 直到所有数据全部写入完成

不难看出, 在这一情形下, 数据仅仅经由处理器而不被其处理, 处理器所做的仅仅是负责数据传输的过程这一简单枯燥的任务而已, 但文件转移任务仍然会占用处理器时间. 即使我们利用某些指令可以同时转移多个位或多个字 (如 LDM/STM), 但不会改变问题的本质.

直接内存访问方法提供的解决思路是, 使用专用电路来承接和处理数据传输, 实现对数据总线和内存地址总线的控制. 在不经过处理器的情况下, 外设也可以经过这一专门电路实现对内存的读取和写入. 这一电路被称为 DMA 控制器 (DMA Controller). 在它的帮助之下, 外设可以更快的访问和读取内存, 并且节约处理器时间, 但在控制器接管总线的情况下, 处理器往往会同时失去对总线的控制权.

一个含有 DMA 控制器的系统拓扑图如下所示:

20210106164326

一个典型的直接内存访问序列如下:

  1. 外设向 DMA 控制器发出一个发起 DMA 数据传输的请求
  2. DMA 控制器接收并转发该请求至控制链的最高优先级-处理器
  3. 在条件允许的情况下 (如对处理器而言此时总线的控制权是非必要的), 处理器许可总线控制权的移交
  4. 总线开关状态变化, 处理器将总线控制权暂时移交给 DMA 控制器
  5. DMA 控制器许可并处理直接内存访问

值得注意的是:

  1. 在外设进行直接内存访问期间, 处理器不具有对数据总线和内存地址总线的访问权, 因此无法执行内存的读写操作.
  2. 要将一个 DMA 体系扩展到具有承接多个设备进行直接内存访问的机能, 就将导致系统复杂性的增加, 并且增加处理器无法执行内存读写操作的几率.
  3. 外设可能在中断执行期间或正在为 SVC (SuperVisor Call) 调用提供服务时发起 DMA 请求. 这些情况都需要被妥善处置.

4. 内存保护和硬件保护

赋予用户直接控制硬件的权限是危险的. 但是, 硬件中断由处理器负责, 被中断的却是用户执行的程序. 因此, 在硬件中断发生时, 处理器将会将其模式从 “用户模式” (User Mode) 切换到 “中断模式” (IRQ Mode). 在中断模式下, 处理器拥有直接访问硬件的权限, 而用户模式下则受限.

除此之外, 还需要在处理器和内存之间增设一个额外的硬件 MMU (Memory Management Unit) 用于控制和限制处理器在不同模式下可访问的内存空间.

ARM 架构中所规定的处理器状态寄存器 CPSR 內的 $0-4$ 位被称为 “模式位” (Mode Bits): $10000$ 即为用户模式, $10010$即为中断模式. 当中断发生时, CPSR 中的值将首先被储存至其物理备份寄存器 SPSR 中, 随后其值被更改, 处理器进入中断模式. 而当跳出中断模式时, CPSR 中的值将从其物理备份寄存器中恢复, 并切换至相应的状态. (重新允许接收中断并切换为用户模式)

下面我们从操作系统的角度思考硬件保护:
操作系统应当有能力控制单一用户可访问的内存范围, 并在允许用户使用某些外设的同时限制用户对硬件的访问权限. MMU 从硬件上防止了用户程序对硬件的直接访问, 而操作系统有能力对硬件进行完整控制. 因此, 用户程序在执行特定操作时必须向操作系统获得许可. 为了允许用户访问部分可信的系统资源, ARM 定义了监视模式 SuperVisor Mode, 软中断 (SoftWare Interrupt) 函数即在该模式下被执行.

ARM 架构为处理器定义了以下的几种模式, ARM v4 以上版本的处理器任何时刻必定处于以下 $7$ 种执行模式之一:

  • User Mode:
    用户模式, 操作系统的 Task 一般以这种模式执行. 这是 ARM 唯一的非特权模式. 在该模式下处理器无法执行部分指令, 由此操作系统的资源得以保护.

  • System Mode:
    这是 ARM v4 及更高版本中所引入的特权模式.

  • IRQ Mode:
    中断模式. 中断 (不包括软中断) 处理函数在这种模式下执行.

  • FIQ Mode:
    快速中断模式. 除了多了几个寄存器外, 其他同 IRQ 一致.

  • Supervisor Mode:
    监视模式, 软中断处理函数在这种模式下执行.

  • Abort Mode:
    所有同内存保护相关的异常均在这种模式下执行.

  • Undefined Mode:
    处理无效指令的异常处理函数在这种模式下执行.

程序可以通过读取 CPSRMODE 域来判断处理器当前的执行模式. 每一种模式都具有相应的内存空间. MMU 基于所读取的处理器执行模式控制用户对内存的访问.

以上的数个模式共用绝大多数的寄存器, 但 IRQ ModeSupervisor Mode 拥有独立的状态寄存器物理备份寄存器 SPSR, 链接寄存器 LR 以及 SP.

下面以 SVC 0 (Character Printing) 为例, 观察指令在监视模式下的执行:

SVC 本质上是软中断. 软中断由用户程序引发, 是可预测的, 在执行时, 处理器执行模式会从用户模式切换至监视模式, 跳转返回的目标地址被存储在监视模式的独立链接寄存器中, CPSR 的数据也被存储在监视模式的独立 SPSR 中.

任何类型的中断在 ARM 架构中均被定义为异常 (Exception). SVC 执行时, 处理器恒跳转至内存地址 0x08 , 并立刻进而跳转执行 SVC 处理程序. 在监视模式下, SVC 指令会基于下列的规则被解读并执行:

20210106201341

在执行结束后, CPSR等寄存器的值将会被恢复, 处理器切换回用户模式.

20210106201221