背景

很多人 (你们的同学们、家长们) 都有一个认识

  • “计算机系就是装 (修) 电脑的”
  • 但上了计算机系发现根本不是这么回事啊
    • 因为大家对电脑的印象只有 I/O 设备

但不应该是这样的!

  • 学 “计算机” 的人不仅会修电脑,还会造电脑!

本次课内容与目标

理解 “I/O 设备是什么”

  • 键盘
  • 磁盘
  • 中断控制器
  • 总线
  • DMA
  • GPU

学习 I/O 设备在操作系统中的抽象

  • 设备 = 可以读、写、控制的对象
  • “设备驱动程序”

I/O 设备

孤独的 CPU

CPU 只是 “无情的指令执行机器”

  • 取指令、译码、执行

Altair-8800 (1975), with Intel 8080A; 256B 板卡 RAM

(你需要在面板上手工输入执行指令的起始地址)

CPU 眼中的 I/O 设备

I/O 设备是一个能与 CPU 交换数据的接口


说人话

  • 就是 “几组线”
    • 每一组线有约定的功能 (RTFM)
    • CPU 通过握手信号从线上读出/写入数据
  • 每一组线有自己的地址
    • CPU 可以直接使用指令 (in/out/MMIO) 和设备交换数据

例子 (1): 键盘控制器

IBM PC/AT 8042 PS/2 (Keyboard) Controller

  • “硬编码” 到两个 I/O port: 0x60 (data), 0x64 (status/command)
Command Byte Use 说明
0xED LED 灯控 ScrollLock/NumLock/CapsLock
0xF3 设置重复速度 30Hz - 2Hz; Delay: 250 - 1000ms
0xF4/0xF5 打开/关闭 N/A
0xFE 重新发送 N/A
0xFF RESET N/A

参考 AbstractMachine 的键盘部分实现

例子 (2): 磁盘控制器

ATA (Advanced Technology Attachment)

  • IDE (Integrated Drive Electronics) 接口磁盘
    • primary: 0x1f0 - 0x1f7; secondary: 0x170 - 0x177
void readsect(void *dst, int sect) {
  waitdisk();
  out_byte(0x1f2, 1);          // sector count (1)
  out_byte(0x1f3, sect);       // sector
  out_byte(0x1f4, sect >> 8);  // cylinder (low)
  out_byte(0x1f5, sect >> 16); // cylinder (high)
  out_byte(0x1f6, (sect >> 24) | 0xe0); // drive
  out_byte(0x1f7, 0x20);       // command (write)
  waitdisk();
  for (int i = 0; i < SECTSIZE / 4; i ++)
    ((uint32_t *)dst)[i] = in_long(0x1f0); // data
}

例子 (3): 中断控制器

CPU 有一个中断引脚

  • 收到某个特定的电信号会触发中断
    • 保存 5 个寄存器 (cs, rip, rflags, ss, rsp)
    • 跳转到中断向量表对应项执行

系统中的其他设备可以向中断控制器连线

  • Intel 8259 PIC
    • programmable interrupt controller
    • 可以设置中断屏蔽、中断触发等……
  • APIC (Advanced PIC)
    • local APIC: 中断向量表, IPI, 时钟, ……
    • I/O APIC: 其他 I/O 设备

例子 (4): 总线

越来越多的 I/O 设备

如果你只造 “一台计算机”

  • 随便给每个设备定一个端口/地址,用 mux 连接到 CPU 就行
    • 如果你们自己做实验,你们真的是在这么做

但如果你希望给未来留点空间?

  • 想卖大价钱的 “大型机”
    • IBM, DEC, ...
  • 车库里造出来的 “微型机”
    • 名垂青史的梦想家
  • 都希望接入更多 I/O 设备
    • 甚至是未知的设备

总线:一个特殊的 I/O 设备

提供地址到设备的转发

  • 把收到的地址 (总线地址) 和数据转发到相应的设备上
  • 例子: port I/O 的端口就是总线上的地址
    • IBM PC 的 CPU 其实只看到这一个 I/O 设备

这样 CPU 只需要直连一个总线 (例如今天是 PCI) 就行了

  • 总线可以桥接其他总线 (例如 PCI → USB)
  • lspci -tvlsusb -tv: 查看系统中总线上的设备
    • 概念简单,实际非常复杂……
      • 电气特性、burst 传输、中断……

例子:PCI Device Probe

pci-probe.c (AbstractMachine, x86-64/i386)

  • 试着给 QEMU 增加 -soundhw ac97 的运行选项
for (int bus = 0; bus < 256; bus++)
  for (int slot = 0; slot < 32; slot++) {
    uint32_t info = pciconf_read(bus, slot, 0, 0);
    uint16_t id   = info >> 16, vendor = info & 0xffff;
    if (vendor != 0xffff) {
      printf("%02d:%02d device %x by vendor %x", bus, slot, id, vendor);
      if (id == 0x100e && vendor == 0x8086) {
        printf(" <-- This is an Intel e1000 NIC card!");
      }
      printf("\n");
    }
  }

例子 (5): DMA

中断没能解的问题

假设程序希望写入 1 GB 的数据到磁盘

  • 即便磁盘已经准备好,依然需要非常浪费缓慢的循环
  • out 指令写入的是设备缓冲区,需要去总线上绕一圈
    • cache disable; store 其实很慢的
for (int i = 0; i < 1 GB / 4; i++) {
  outl(PORT, ((u32 *)buf)[i]);
}

能否把 CPU 从执行循环中解放出来?

  • 比如,在系统里加一个 CPU,专门复制数据?
  • 好像 memcpy_to_port(ATA0, buf, length);

Direct Memory Access (DMA)

DMA: 一个专门执行 “memcpy” 程序的 CPU

  • 加一个通用处理器太浪费,不如加一个简单的

支持的几种 memcpy

  • memory → memory
  • memory → device (register)
  • device (register) → memory
    • 实际实现:直接把 DMA 控制器连接在总线和内存上
    • Intel 8237A

PCI 总线支持 DMA

  • 这就是为什么 CPU 会有 PCIe lanes

例子 (6): GPU

用于 “加速图形显示” 的硬件

一切皆可计算

for (int i = 1; i <= H; i++) {
  for (int j = 1; j <= W; j++)
    putchar(j <= i ? '*' : ' ');
  putchar('\n');
}

难办的是性能

  • NES: 6502 @ 1.79Mhz; IPC = 0.43
    • 屏幕共有 256 x 240 = 61K 像素 (256 色)
    • 60FPS → 每一帧必须在 ~10K 条指令内完成
      • 如何在有限的 CPU 运算力下实现 60Hz?

既然能做一个 memcpy 的硬件,为什么不能做一个画图的硬件?

NES Picture Processing Unit (PPU)

76543210
||||||||
||||||++- Palette
|||+++--- Unimplemented
||+------ Priority
|+------- Flip horizontally
+-------- Flip vertically

CPU 只描述 8x8 “贴块” 的摆放方法

  • 背景是 “大图” 的一部分
    • 每行的前景块不超过 8 个
  • PPU 完成图形的绘制

现代 GPU: 一个通用计算设备

一个完整的众核多处理器系统

  • 注重大量并行相似的任务
    • 程序使用例如 OpenGL, CUDA, OpenCL, ... 书写
  • 程序保存在内存 (显存) 中
    • nvcc: 把 main 编译/链接成 ELF; kernel 编译成 GPU 指令
  • 数据也保存在内存 (显存) 中
    • 可以输出到视频接口 (DP, HDMI, ...)
    • 也可以通过 DMA 传回系统内存

例子:PyTorch 炼丹

class NeuralNetwork(nn.Module):
    def __init__(self):
        super(NeuralNetwork, self).__init__()
        self.flatten = nn.Flatten()
        self.linear_relu_stack = nn.Sequential(
            nn.Linear(28*28, 512),  nn.ReLU(),
            nn.Linear(512, 512),    nn.ReLU(),
            nn.Linear(512, 10),     nn.ReLU(), )
    ...
model = NeuralNetwork().to('cuda')

炼丹的基本数据 tensor (stride, sparse, ...)

tensor = torch.rand(128, 96)

谁说计算机系的同学不会造/修计算机?

Image by Rodrigo Copetti; 以及电路图

I/O 设备的抽象

I/O 设备:应该是操作系统中的什么对象?

无论何种 I/O 设备,都是可以读 (read) 写 (write) 的字节序列 (流或数组)


操作系统:设备 = 支持以下三种操作的对象 (文件)

  • read: 从设备某个指定的位置读出数据
  • write: 向设备某个指定位置写入数据
  • ioctl: 读取/设置设备的状态

设备驱动程序:实现抽象

把对设备的读/写/ioctl 系统调用 “翻译” 成设备的寄存器命令序列

  • 以 “面向对象” 的方式访问 I/O 设备
    • 设备 = 支持 read, write, ioctl, ... 功能的对象

例子:/dev/ 中的对象

  • /dev/pts/[x] - pseudo terminal
  • /dev/zero - “零” 设备
  • /dev/null - “null” 设备
  • /dev/random, /dev/urandom - 随机数生成器
    • 试一试:head -c 512 [device] | xxd
    • 以及观察它们的 strace
      • 能看到访问设备的系统调用

例子: Lab 2 硬件抽象层

做了很多不合理的简化假设

  • 设备从系统启动时就存在且不会消失
  • 只支持读/写两种操作
typedef struct devops {
  int (*init)(device_t *dev);
  int (*read) (device_t *dev, int offset, void *buf, int count);
  int (*write)(device_t *dev, int offset, void *buf, int count);
} devops_t;

设备驱动程序:将设备抽象为一个对象和操作

  • 未必一定要有物理的设备
    • /dev/null, /dev/urandom, ...
    • 你也能理解它们怎么实现!

道理简单,但写代码就麻烦了

不就是把 read/write/ioctl 翻译成设备认识的指令吗?

  • port I/O; memory-mapped I/O; 总线; DMA; 中断...

难点

  • I/O 设备看起来是个 “黑盒子”
  • 写错任何代码就 simply “not work”
  • I/O 设备偏偏还提供巨多的功能
    • GPU 自带处理器、内存、编译器、散热管理、……
    • 为 “跑马灯” 编程、调教 GPU 参数……
  • 设备驱动:Linux 内核中最多也是质量最低的代码

Linux 设备驱动:设备操作 = 文件操作

struct file_operations {
  struct module *owner;
  loff_t (*llseek) (struct file *, loff_t, int);
  ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
  ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
  int (*mmap) (struct file *, struct vm_area_struct *);
  unsigned long mmap_supported_flags;
  int (*open) (struct inode *, struct file *);
  int (*release) (struct inode *, struct file *);
  int (*flush) (struct file *, fl_owner_t id);
  int (*fsync) (struct file *, loff_t, loff_t, int datasync);
  int (*lock) (struct file *, int, struct file_lock *);
  ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
  long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
  long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
  int (*flock) (struct file *, int, struct file_lock *);
  ...

为什么有两个 ioctl?

long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
  • unlocked_ioctl: BKL (Big Kernel Lock) 时代的遗产
    • 单处理器时代只有 ioctl
    • 之后引入了 BKL, ioctl 执行时默认持有 BKL
    • (2.6.11) 高性能的驱动可以通过 unlocked_ioctl 避免锁
    • (2.6.36) ioctlstruct file_operations 中移除
  • compact_ioctl: 机器字长的兼容性
    • 32-bit 程序在 64-bit 系统上可以 ioctl
    • 此时应用程序和操作系统对 ioctl 数据结构的解读可能不同 (tty)
    • (调用此兼容模式)

存储设备的抽象

磁盘 (存储设备) 的访问特性

  1. 以数据块 (block) 为单位访问
    • 传输有 “最小单元”,不支持任意随机访问
  2. 大吞吐量
    • 使用 DMA 传送数据
  3. 应用程序不直接访问
    • 访问者通常是文件系统 (维护磁盘上的数据结构)
    • 大量并发的访问 (操作系统中的进程都要访问文件系统)

对比一下终端和 GPU,的确是很不一样的设备

  • 终端:小数据量、直接流式传输
  • GPU:大数据量、DMA 传输

Linux Block I/O Layer

文件系统和磁盘设备之间的接口

  • 包含 “I/O 调度器”
    • 曾经的 “电梯” 调度器

块设备:持久数据的可靠性

Many storage devices, ... come with volatile write back caches

  • the devices signal I/O completion to the operating system before data actually has hit the non-volatile storage
  • this behavior obviously speeds up various workloads, but ... data integrity...

我们当然可以提供一个 ioctl

  • 但 block layer 提供了更方便的机制
    • 在 block I/O 提交时
      • | REQ_PREFLUSH 之前的数据落盘后才开始
      • | REQ_FUA (force unit access),数据落盘后才返回
    • 设备驱动程序会把这些 flags 翻译成磁盘 (SSD) 的控制指令

在 Block I/O API 基础上构造文件系统

对磁盘做了一个不同的抽象

  • 读/写一些以固定大小 (如 4KB) 的数据块

文件系统:在 bread/bwrite/bflush 的基础上创建一个数据结构

  • 支持文件/目录操作
    • 你可能已经想到应该怎么做了

总结

总结

本次课内容与目标

  • 理解 “什么是 I/O 设备”
    • 终端、键盘、鼠标、总线、DMA、GPU……
  • 理解 “I/O 设备在操作系统中的抽象”
    • 可以读/写/控制的对象

Takeaway messages

  • 如果你 “自己造一台计算机”,你会发现这一切都是自然的
    • “不容易理解” 的部分是随时间积累的复杂性