io

在讨论计算机时,我们实际上在使用的是输入输出设备.对于初次接触计算机的人来说,他们首先会注意到的是显示器上的图像和图形,并通过鼠标和键盘向计算机发出指令.对于终端用户而言,这些输入输出设备构成了他们与计算机交互的直接接口.

20240329102322

然而,当我们深入学习计算机科学,例如计算组成原理或计算机系统基础时,我们会了解到CPU的核心作用, 它就是一个不断执行指令的机器, 取指令、译码和执行.这种认识揭示了一个本质的差异:我们日常使用的设备并不是计算机内部用于计算的核心部件.

计算机要为我们提供服务,就必须配备相应的输入输出设备,这样才能让人们真正地使用它.例如,我们需要打印机来将数据输出到纸张上,需要键盘来输入指令,以及需要显示器来查看信息.

对于任何一个CPU来说,要实现与外部设备的交互就需要精心设计的输入输出设备.这些设备本质上是类似的,它们使得CPU能够与外部世界进行交互.我们可以将输入输出设备视为计算机的"眼睛和耳朵",它们让计算机能够感知外部状态,并对外实施动作.

例如,显示器上每个像素的亮暗变化,都是对外部物理世界的一种影响.同样,键盘上的每一次按键和鼠标的每一次移动,都让计算机能够感知到物理世界中的变化.因此,CPU与外设之间的交互最核心的功能就是数据的交换.这种数据交换是实现计算机功能和用户交互的基础,是计算机系统不可或缺的一部分.

输入输出设备的原理

在讨论计算机系统中的CPU与输入输出(I/O)设备的关系时,我们可以从CPU的角度来理解这些设备的工作原理.对于CPU而言,所有的I/O设备在本质上可以被视为一根线,这根线上承载着数据,从设备端连接到CPU的引脚, 并且以一种与CPU预先约定好的方式进行数据交换.这种交换方式定义了输入(Input)和输出(Output)的概念:输入是将数据送入CPU进行处理,而输出则是将CPU处理后的数据发送到外部设备或控制器.

下图为 intel CPU 的引脚对照图

20240329102957

因此从 CPU 的视角来看, IO设备就是一个能和CPU交换数据的接口/控制器, 通过几组约定好功能的线和 CPU 相连, 通过握手信号从线上读出/写入数据. 每一组线有自己的地址, CPU可以直接使用指令和设备交换数据, 设备会被抽象为一组 (状态, 命令, 数据) 的接口, CPU 完全不管设备是如何实现的

状态寄存器(Status)、命令寄存器(Command)和数据寄存器(Data).状态寄存器用于反映设备的状态,例如磁盘是否正在写数据、设备是否忙碌等.命令寄存器允许我们对设备执行操作,比如发送指令让设备执行特定任务.数据寄存器则用于在CPU和设备之间传输数据.

所以计算机系统中的设备并没有神秘之处,它们的本质是一组寄存器的集合,这些寄存器代表了设备的状态、控制命令和数据传输等功能. CPU 可以向访问内存一样通过一个地址来访问外部设备的寄存器, 这些地址是由硬件厂商和操作系统通过协调统一的.

下图为 PS/2 键盘控制器的接口, 它被硬编码到两个 I/O port: 0x60 (data), 0x64 (status/command), 只要引出线与 CPU 对应的引脚相连, CPU 就可以通过这两个端口向键盘读写数据

20240329103933

但是, 在实际的计算机系统中,我们并不会为每一个I/O设备单独拉出一根线连接到CPU, 随着技术的发展和新设备的出现,如新型打印机、游戏控制器等各种形态的外设层出不穷. 如果为每个设备都单独连接一根线,CPU的结构将变得极其复杂. 需要一种更加灵活和可扩展的方式来管理I/O设备. 也就是总线(bus)

总线是一种特殊的I/O设备,它负责统一管理和协调所有连接到它的其他I/O设备.CPU不再需要直接与每个设备通信,而是通过总线来访问和控制这些设备, 包括设备的注册地址到设备的转发

20240329110305

当多个设备需要与CPU通信时,总线控制器会根据中断优先级来协调,确保CPU能够及时响应最重要的事件.此外,总线控制器还能够识别发生中断的设备,进一步增强了系统的灵活性和响应能力.

今天 PCIe 总线肩负了这个任务. 关于总线的更多讨论见 bus, 这里不再赘述

IO设备

CPU要想控制所链接的设备,不可避免需要通过IO(input/output)与外设打交道,CPU通过IO操纵设备上的寄存器等来实现对设备的控制. 那么 CPU 是如何与设备寄存器和设备数据缓冲区进行通信呢?

存在两个可选的方式:

早期的PC中, 所有的IO设备(除了存储设备之外的设备)的内部存储或者寄存器都只能通过IO地址空间进行访问(方案一).但是这种方式局限性很大,显而易见的问题就是 端口冲突数量不足. 硬编码的端口号在硬件设备确定的情况还勉强可以接受, 但是今天显然限制了可以访问的设备数量和每个设备可以访问的端口数量. 随着系统复杂性的增加,传统的I/O地址空间可能难以扩展以支持更多的设备和更复杂的硬件配置

因此管理访问 IO 设备普遍采用的是方案二 MMIO,即Memory Mapped IO,也就是说把这些IO设备中的内部存储和寄存器都映射到统一的存储地址空间(Memory Address Space)中.

为了兼容一些之前开发的软件,PCIe仍然支持IO地址空间,只是建议在新开发的软件中采用MMIO. PCIe Spec中明确指出,IO地址空间只是为了兼容早期的PCI设备(Legacy Device),在新设计中都应当使用MMIO,因为IO地址空间可能会被新版本的PCI Spec所抛弃.

关于 MMIO 的更多内容见 mmio

关于 PCIe 的更多内容见 pcie

中断

中断控制器是计算机系统中一项特殊而重要的设计,它为操作系统的稳定运行和高效管理提供了基础.在计算机系统中,应用程序不断执行指令,但并不会因此导致系统失去响应,这得益于中断机制的存在.

一个没有中断的计算机体系是决定论的: 得知某个时刻CPU和内存的全部数据状态,就可以推衍出未来的全部过程.这样的计算机无法交互,只是个执行指令的工具.

添加中断后,计算机指定了会兼容哪些外部命令,并设定服务程序,这种服务可能打断当前任务.这使得CPU"正在执行的程序"与"随时可能发生的服务",二者形成了异步关系,外界输入的引入使得计算机程序不再是决定论.由人实时控制的中断输入,是无法预测的.再将中断响应规则化,推广开,非计算机科学人群就能控制计算机,发挥创造力.电竞鼠标微操,数码板绘,音频输入合成,影像后期数值调整,键盘点评天下大势,这些都不是定势流程,是需要人实时创造参与其中的事件,就由中断作为载体,与计算机结合了起来.中断就是处理器的标准输入接口.

因此中断的本质是处理器对外开放的实时受控接口,例如IRQ线,这是一个边缘触发且低电平有效的信号线.当这条线被激活时,CPU必须响应这个特殊的事件,就像核弹发射按钮被按下时,系统必须立即采取行动一样.在这个过程中,中断控制器扮演了一个关键角色.

20240329102848

而负责处理中断的中断控制器内部包含一系列的控制寄存器, 连接到 CPU 的中断引脚. 中断控制器中保存 5 个寄存器 (cs, rip, rflags, ss, rsp), 当设备发送中断请求的时候由中断控制器向 CPU 发送中断信号, CPU 跳转到中断向量表对应项执行.

在我们的计算机系统中,许多外围设备,如键盘、网卡和鼠标等,都有能力产生中断.这些中断实际上是通过各自的信号线来实现的,通常这些线在接收到低电平信号时被激活.当这些设备中的任何一个需要CPU的注意时,它就会通过这根线发送中断请求.所有的这些中断请求线最终都会连接到一个中断控制器上.这个中断控制器是一个可编程的设备(PIC, programmable interrupt controller),它的主要任务是对这些中断请求进行裁决.例如,如果同时有两个或多个中断请求发生,中断控制器会根据每个中断的优先级来决定哪个请求应该首先被处理.这个优先级设置确保了更重要的任务能够优先得到处理.

在实时系统中,这种优先级处理尤为重要,因为系统需要确保关键任务能够及时得到响应.中断控制器还具备中断屏蔽的功能,允许CPU在处理某些紧急任务时暂时忽略那些优先级较低的中断,从而保持系统的稳定性和效率.

例如 Intel 8259 PIC 中断控制器就可以编程控制哪些中断可以屏蔽, 多个中断请求的优先级等等

20240329115227

现如今 8259 PIC 已经远远不能满足日益复杂的 CPU 了(比如说多处理器多核), 诞生了 APIC(Advanced PIC)

APIC是一种高级可编程中断控制器,它提供了比传统中断控制器更为强大和灵活的中断处理能力.APIC通常集成在现代芯片组或北桥中,它支持多处理器系统中的中断管理和协调.APIC的主要特点包括:

  1. 多处理器支持:APIC允许多个CPU之间进行中断通信,这对于确保系统整体性能和响应性至关重要.
  1. 可编程性:APIC提供了丰富的编程接口,允许操作系统根据需要配置中断优先级、中断向量和中断屏蔽等.
  1. 中断向量分配:APIC可以为每个中断源分配唯一的向量号,这有助于操作系统更有效地处理中断.
  1. 性能优化:APIC通过减少CPU之间的中断冲突和优化中断处理流程,提高了系统的整体性能.

例如对于多个 CPU 核心, 每一个核心运行一个线程. 此时如果使用 mmap 分配了一块内存, 此时这块内存地址空间在两个 CPU 上都是可用的. 当某一个 CPU 核心上的线程执行了 munmap 之后, 此时操作系统会让该 CPU 核心采用核间中断(IPI, Inter-Processor Interrupt)的方式向其他执行线程的发送一个 tlb shootdown(详细讨论见 mm/tlb), 之后他们的页表会被同步, 其他CPU上的内存也会被消除映射

LAPIC(local APIC)是APIC的一种,它是集成在每个CPU内部的中断控制器.LAPIC为每个CPU提供了专用的中断处理能力,使得每个处理器都能够独立地处理中断,而不需要主控制器的介入.LAPIC的主要特点包括:

  1. 本地化:每个CPU都有自己的LAPIC,这样可以减少处理器之间的通信开销,提高中断处理速度.
  1. 专用资源:LAPIC为每个CPU提供了专用的资源,如定时器、性能计数器和中断向量表,使得每个处理器都能够独立地配置和管理自己的中断.
  1. 高级中断管理:LAPIC支持高级中断管理功能,如中断优先级、中断屏蔽和中断转发等.
  1. 多线程支持:LAPIC能够支持多线程环境下的中断处理,确保线程间的同步和资源共享.

APIC和LAPIC在现代计算机系统中提供了高效、灵活的中断处理机制,它们使得多处理器系统能够更好地管理和响应各种中断请求,从而提高了系统的整体性能和稳定性.通过这些先进的中断控制器,操作系统能够实现更精细的中断管理,优化资源分配,提升用户体验.

关于中断的更多讨论见 interrupt, 这里不详细展开

DMA

在计算机系统中,当需要传输大量数据时,如写入1GB的数据到磁盘,CPU可能会花费大量时间来处理这个任务.这是因为,即使磁盘已经准备好接收数据,CPU仍需执行一个庞大的循环,逐字节地将数据从内存复制到磁盘上.

假设此时 CPU 想要从磁盘中读出 1GB 的数据, 整个流程如下所示

  1. CPU 向南桥芯片组发送指令, 读取某一个地址的数据, 南桥将操作转发给磁盘控制器
  1. 磁盘将数据读出, 通过 CPU 写入到内存的某段空间中
  1. 循环执行, 全部完成后 CPU 再从内存中读出数据

20240329160646

我们发现最大的问题在于从磁盘读出数据的需要经过 CPU 才能被写入内存, 这个过程不仅耗时,而且会占用CPU的大量资源,使其无法同时处理其他任务.

从内存写入数据的过程类似, 也需要经过 CPU 完成数据的写入指令

为了解决这个问题,我们可以设想一种专门的硬件设备,专门负责处理内存之间的数据传输,从而释放CPU的资源.这种设备被称为DMA(Direct Memory Access)控制器.DMA控制器可以被设计得相对简单,因为它只负责执行内存复制操作, 一共四种情况: 外设到内存;内存到外设;内存到内存;外设到外设. 它不需要像通用CPU那样具备复杂的指令集,而是可以通过硬编码的方式,将内存复制的逻辑直接嵌入到电路中.

DMA控制器的工作原理是使用一个计数器和两个地址指针.在每个时钟周期,它会从源地址读取数据,写入目标地址,然后更新计数器和地址指针.这样的固定逻辑使得DMA控制器非常高效,特别适合处理大量数据传输的任务.

大部分的设备都是 Slave 设备, 也就是只能接受来自其他处理器的信号做出反应. DMA 控制器是一个 Master 设备, 它可以主动发出指令执行操作

  1. CPU 向 DMA 发送指令, 告知移动数据的 (src, dst, length), 交由 DMA 处理, CPU 继续工作
  1. DMA 从磁盘中读出数据, 通过 DMA 控制器和内存总线写入内存中的一片区域
  1. DMA 通过中断告知 CPU 已经完成数据传输
  1. CPU 从内存中读出数据

20240329172122

通过DMA,驱动程序可以事先(或在需要的时候)设定一个内存地址,设备就可以绕开CPU直接向内存中复制(或读取)数据.根据发起者不同,DMA可以被分为两种:

Cache一致性

DMA的引入,优点是数据在内存和设备之间的搬运不需要CPU参与,这极大降低了CPU的负荷.但是也引入了新的问题,即cpu读取到的数据不一定是最新的,是因为中间cache的存在导致的一致性问题, 如下图所示

20240720124116

由于 DMA 只负责 device 和 memory 之间的数据迁移, 但是 cpu 读取内存数据时会先判断对应虚拟地址是否被cache命中,如果命中将直接读取cache中的数据内容不会经过内存,但是了cache中的数据并不是最新内存的数据,最新内存的数据已经被 DMA 修改了.

DMA 需要在内存中申请一段内存当做buffer,这段内存用作需要使用DMA读取I/O设备的缓存,或者写入I/O设备的数据.为了避免cache的影响,我们可以将这段内存映射 nocache, 即不使用cache. 但是显而易见, 映射的这段内存将不会再享受到 cache 带来的好处, 如果数据量很小还可以接受, 但是如果是频繁访问的数据那么就会导致性能损失

nocache 对应 虚拟地址转换 中页表格式中的 PCD, 在 cache 中的 VIPT 解决方案中也有提到

Linux 的 dma_alloc_coherent() 接口的实现方法即采用的是此方案

为了充分使用cache带来的好处.我们映射依然采用cache的方式.但是我们需要格外小心, DMA通过总线获取数据时应该先检查cache是否命中,如果命中那么应当从 cache 而非内存中获取数据. 为了解决上述DMA与Cache不一致的问题,引入了两种dma机制来处理:一致性DMA(Consistent mapping) 和 流式DMA(Stream mapping)

Cacheline对齐

假设我们有2个全局变量 tempbuffer, buffer用作DMA缓存. temp和buffer变量毫不相关.可能buffer是当前DMA操作进程使用的变量,temp是另外一个无关进程使用的全局变量.

int temp = 5;
char buffer[64] = { 0 };

假设,cacheline大小是64字节.那么temp变量和buffer位于同一个cacheline,buffer横跨两个cacheline.

20240721210419

假设现在想要启动DMA从外设读取数据到buffer中.我们进行如下操作:

  1. 按照前文的理论,我们先invalid buffer对应的2行cacheline.
  1. 启动DMA传输.
  1. 当DMA传输到buff[3]时,程序改写temp的值为6.temp的值和buffer[0]-buffer[60]的值会被缓存到cache中,并且标记dirty bit.
  1. DMA传输还在继续,当传输到buff[50]的时候,其他程序可能读取数据导致temp变量所在的cacheline需要替换,由于cacheline是dirty的.所以cacheline的数据需要写回.此时,将temp数据写回,顺便也会将buffer[0]-buffer[60]的值写回.

在第4步中,就出现了问题.由于写回导致DMA传输的部分数据(buff[3]-buffer[49])被改写(改写成了没有DMA传输前的值).这不是我们想要的结果.因此,为了避免出现这种情况.我们应该保证DMA Buffer不会跟其他数据共享cacheline.所以我们要求DMA Buffer首地址必须cacheline对齐,并且buffer的大小也cacheline对齐.这样就不会跟其他数据共享cacheline.也就不会出现这样的问题.

参考

zood