第23讲 设备管理

从设备抽象到DMA:操作系统如何统一管理外设

引言

计算机可以连接大量功能完全不同的设备:

  • 键盘产生按键事件;
  • 串口按字节传输数据;
  • 硬盘和SSD按照块读写;
  • 网卡收发具有严格格式的网络帧;
  • GPU、摄像头和传感器又拥有各自的寄存器、协议与数据布局。

这些设备不仅用途不同,其速度、数据单位、控制方式和错误处理机制也存在巨大差异。

如果应用程序必须直接控制硬件,那么一次看似简单的“读取数据”,可能要求程序了解:

  • 设备连接在哪条总线上;
  • 应该访问哪个硬件寄存器;
  • 每个寄存器中的比特代表什么;
  • 设备何时能够接收下一条命令;
  • 数据应该搬运到哪段内存;
  • 操作完成后如何发现结果;
  • 发生错误时应如何恢复。

这种设计会让每个应用程序都承担设备驱动的工作,也会使程序与特定硬件紧密绑定。

操作系统因此承担了两项核心任务:

  1. 通过抽象隐藏设备差异,为应用程序提供相对统一的访问接口。
  2. 通过设备驱动、MMIO、DMA和中断等机制,完成CPU、内存与设备之间的实际交互。

整套设备管理体系可以概括为:

1
2
3
4
5
6
7
8
9
物理设备

设备驱动

字符设备、块设备、网络设备等抽象

文件接口、Socket接口

应用程序

而在更底层,数据传输又形成另一条链:

1
2
3
4
5
6
7
8
9
CPU配置设备

设备开始执行任务

PIO或DMA完成数据传输

设备通过中断通知CPU

内核处理结果并唤醒应用

设备抽象解决的是“应用程序怎样使用设备”,PIO与DMA解决的则是“数据在硬件之间究竟怎样移动”。


一、为什么设备管理首先需要抽象

不同设备并不存在天然统一的“读取”含义。

从键盘读取数据,通常意味着取得下一个按键事件;从硬盘读取数据,则需要指定数据所在的位置;从网卡读取数据,还涉及报文边界、网络协议和通信端点。

设备 典型数据单位 访问特征
键盘 扫描码或按键事件 按时间顺序到达
UART串口 字节 连续串行传输
硬盘、SSD 数据块 支持按位置随机访问
网卡 网络帧 具有格式和报文边界
GPIO 电平或位状态 通过寄存器控制

如果所有差异都直接暴露给应用程序,软件开发将极其复杂。

操作系统采用的基本思路是:

把硬件特有的控制过程放进设备驱动,把应用程序常用的操作提炼成稳定接口。

例如,应用程序可能只调用:

1
2
3
4
int fd = open("/dev/example", O_RDWR);
read(fd, buffer, size);
write(fd, buffer, size);
close(fd);

但这些接口最终可能对应完全不同的底层行为:

1
2
3
4
5
6
7
8
9
10
11
read(普通文件)
→ 从页缓存或存储设备取得文件内容

read(键盘)
→ 从键盘驱动的输入队列取得按键事件

read(串口)
→ 从串口接收缓冲区取得字节

read(设备控制节点)
→ 查询设备状态或接收驱动生成的数据

应用程序只需要描述“要做什么”,设备驱动负责决定“怎样让具体硬件完成”。


1. “一切皆文件”的真正含义

Unix和Linux经常使用文件接口访问设备:

1
2
3
4
open
read
write
close

这常被概括为“一切皆文件”。

但这一说法并不意味着键盘、串口和硬盘真的变成了磁盘上的普通文件。它表达的是:

操作系统让普通文件、设备、管道、Socket等对象共享文件描述符体系和部分相似接口。

统一的是访问形式,而不是对象的内部实现。

例如:

1
read(fd, buffer, size);

根据fd所指向对象的不同,内核可以执行:

  • 普通文件读取;
  • 终端输入;
  • 管道读取;
  • Socket接收;
  • 字符设备驱动的读取操作。

文件描述符在这里相当于一种通用句柄。应用程序使用相同的编号和系统调用框架,内核则根据对象类型分派到不同实现。


本节核心结论: 设备抽象不是消除硬件差异,而是把差异隐藏在驱动程序之后,让应用程序面对稳定、简化的接口。


二、字符设备:面向顺序字节流的抽象

字符设备通常把数据表示为连续到达或连续输出的字节流。

典型例子包括:

  • 键盘;
  • 鼠标;
  • UART串口;
  • 终端;
  • 打印机;
  • 部分传感器和GPIO设备。

“字符设备”中的“字符”不能只理解为可打印文字。设备传输的完全可能是二进制数据。

真正关键的是:

数据通常按照先后顺序读取,应用程序不强调随机定位到任意历史位置。


1. 字符设备的访问模型

键盘事件天然具有时间顺序:

1
2
3
4
5
按下A

按下B

按下Enter

应用程序通常关心的是“下一个输入事件”,而不是“读取第1000号按键”。

因此,字符设备一般适合顺序读取:

1
2
3
4
int fd = open("/dev/example", O_RDWR);
read(fd, buffer, size);
write(fd, buffer, size);
close(fd);

数据路径可以概括为:

1
2
3
4
5
6
7
应用程序
↓ read/write
VFS与字符设备接口

字符设备驱动

硬件设备

2. 终端也是字符设备

Linux中的终端设备可以出现在:

1
2
3
/dev/tty
/dev/pts/0
/dev/pts/1

执行:

1
echo "hello" > /dev/pts/1

并不是把字符串保存成普通磁盘文件,而是向对应终端设备发送字符。

终端驱动接收这些字节后,再把它们显示到相关终端窗口中。

这个例子说明,路径位于/dev下并支持write(),并不意味着它具有普通文件的存储语义。


3. 字符设备不代表硬件一次只能传输一个字节

字符设备描述的是操作系统向上提供的访问模型,而不是硬件电路的绝对传输粒度。

例如,一个串口控制器内部可能拥有FIFO缓冲区,一次可以暂存多个字节;USB设备也可能批量传输数据。

即使硬件内部采用批量传输,只要操作系统向应用程序呈现的是顺序字节流,它仍然可以被抽象成字符设备。


三、块设备:面向可寻址数据块的抽象

块设备将存储空间划分为固定大小的数据块,并允许按照块位置访问数据。

典型块设备包括:

  • 机械硬盘;
  • SSD;
  • U盘;
  • 部分闪存设备;
  • 虚拟磁盘。

其基本访问形式类似:

1
2
读取从第N个块开始的一段数据
把一段数据写入第M个块

1. 为什么存储设备需要随机访问

假设一个10GB文件位于存储设备中,程序只需要读取文件中部的4KB。

如果设备只能像字符流一样从头顺序读取,就必须跳过前面数GB内容,效率极低。

块设备则允许文件系统根据文件的块映射,直接定位目标区域。

1
2
3
4
5
6
7
文件逻辑偏移

文件系统块映射

块设备逻辑块号

设备驱动

这里的“随机访问”不是指随机选择数据,而是指:

可以根据指定位置直接访问不同数据块,而不必从设备开头顺序经过所有前置内容。


2. 为什么块设备上方需要缓存

块设备通常比CPU和内存慢得多。

如果程序每次读取几个字节都直接访问设备,将产生大量小型I/O请求。

操作系统通常在块设备之上建立页缓存和I/O调度层:

缓存能够:

  • 避免重复读取相同数据;
  • 把多个小写入合并;
  • 延迟并重新安排I/O;
  • 提高连续访问比例;
  • 减少设备操作次数。

因此,应用程序执行read()时,不一定真正访问设备;数据可能已经位于页缓存中。


3. 块设备与mmap

块设备上的普通文件既可以通过:

1
2
read(fd, buffer, size);
write(fd, buffer, size);

访问,也可以通过:

1
void *address = mmap(...);

映射到进程虚拟地址空间。

mmap()并不意味着设备内容立即全部进入内存。实际数据通常在首次访问映射页面时,通过缺页异常按需加载。

因此,块设备、文件系统、页缓存和虚拟内存共同参与文件访问。


4. 块设备不是一次只能处理一个块

块是寻址和组织单位,不是每次I/O请求的最大单位。

一个请求可以覆盖:

  • 一个块;
  • 多个连续块;
  • 多个经过合并的区域。

操作系统会尽量合并相邻请求,以提高吞吐量。


四、网络设备:面向格式化报文的抽象

网络设备同样收发字节,但不能简单等同于字符设备。

以太网卡收发的是以太网帧,每个帧具有:

  • 目的地址;
  • 源地址;
  • 类型字段;
  • 负载;
  • 校验信息。

在网卡上方,还存在完整协议栈:

1
2
3
4
5
6
7
8
9
应用数据

TCP或UDP

IP

以太网

网卡

网络通信不仅要传递字节,还需要表达:

  • 通信协议;
  • 本地和远程地址;
  • 端口;
  • 连接状态;
  • 报文边界;
  • 路由选择。

因此,操作系统通常为网络通信提供Socket抽象。


1. Socket表示通信端点

应用程序可以调用:

1
2
3
4
int socket_fd = socket(...);
send(socket_fd, buffer, size, ...);
recv(socket_fd, buffer, size, ...);
close(socket_fd);

Socket不是某一张具体网卡的简单句柄,而是一个通信端点。

应用程序指定通信需求后,协议栈可以根据路由表选择实际网卡。

因此,一个Socket可以在不直接感知硬件的情况下完成通信。


2. Socket为什么也有文件描述符

在Unix/Linux中,Socket同样通过文件描述符引用。

因此,部分情况下可以使用:

1
2
3
read(socket_fd, ...);
write(socket_fd, ...);
close(socket_fd);

这再次体现了文件描述符体系的统一性。

但统一文件描述符不代表网络设备与普通文件拥有完全相同的语义。

例如:

  • 普通文件通常具有稳定偏移量;
  • TCP Socket表示连续字节流;
  • UDP Socket保留数据报边界;
  • 网络错误和连接状态也不同于文件读写错误。

五、三类设备抽象的区别

对比点 字符设备 块设备 网络设备
典型对象 键盘、串口、终端 硬盘、SSD、U盘 以太网卡、WiFi
数据组织 顺序字节流 固定大小数据块 格式化报文
访问特点 强调先后顺序 支持按位置随机访问 强调协议与通信端点
常见接口 open/read/write read/write/mmap socket/send/recv
上层机制 设备缓冲区 文件系统、页缓存 网络协议栈
核心问题 下一个数据是什么 数据位于哪个块 数据发给谁、采用什么协议

可以使用三个直观模型帮助区分:

1
2
3
4
5
6
7
8
字符设备:
像一条不断流动的水管,数据依次到来。

块设备:
像带编号的仓库格子,可以直接访问某个格子。

网络设备:
像一封封带地址和格式的信件,需要协议系统处理。

不过,最终分类的依据不是硬件内部电路,而是操作系统向上提供的访问模型。


本节核心结论: 字符、块和网络设备不是简单按硬件外形分类,而是按操作系统希望向应用程序提供的数据组织和访问语义分类。


六、设备驱动怎样控制硬件

设备抽象解决了上层接口问题,但设备最终仍然需要接收具体命令。

硬件设备通常向CPU暴露一组寄存器,例如:

  • 控制寄存器;
  • 状态寄存器;
  • 数据寄存器;
  • 中断配置寄存器;
  • DMA地址寄存器。

驱动程序通过读写这些寄存器来:

  • 启动设备;
  • 查询状态;
  • 提供数据地址;
  • 设置传输长度;
  • 清除中断;
  • 处理错误。

由CPU执行指令直接控制设备寄存器的方式,通常归入Programmed I/O,即编程控制I/O。

需要注意,PIO在不同资料中可能有两种含义:

1
2
3
4
5
Programmed I/O:
CPU通过执行指令直接参与设备操作。

Port I/O:
设备使用独立I/O端口地址空间。

在设备寻址方式上,CPU主要可以通过Port I/O或MMIO访问寄存器。


七、Port I/O:独立的设备端口空间

Port I/O为设备建立独立于内存的I/O地址空间。

CPU通过专用指令访问端口。例如在x86中,可以使用:

1
2
in
out

系统同时存在两套地址空间:

1
2
内存地址空间
I/O端口地址空间

相同的数值地址可以因为使用的指令不同,而访问不同对象。

例如:

1
2
3
4
5
普通load/store访问地址0x60
→ 访问内存地址

in指令访问端口0x60
→ 访问I/O端口

Port I/O的优势是设备与内存边界明确,但需要处理器提供专门指令,编程模型也与普通内存访问不同。


八、MMIO:把设备寄存器映射到地址空间

MMIO是Memory-Mapped I/O,即内存映射I/O。

它把设备寄存器映射到CPU物理地址空间中的特定区域。

CPU可以使用普通的load/store指令访问设备:

1
2
value = *(volatile unsigned int *)DEVICE_STATUS;
*(volatile unsigned int *)DEVICE_CONTROL = command;

形式上,这和读写内存变量很相似。

1
2
3
4
5
CPU发出对某物理地址的访问

地址译码
├─ 普通RAM区域 → 内存
└─ MMIO区域 → 设备寄存器

1. MMIO为什么方便

MMIO使处理器不必为设备访问设计一套完全独立的指令。

驱动程序可以用统一的load/store模型完成:

  • 读取设备状态;
  • 写入控制命令;
  • 设置DMA地址;
  • 读取少量设备数据。

这种模型在许多处理器架构中广泛使用。


2. MMIO不是普通内存

虽然MMIO使用地址和load/store指令,但它与RAM有本质区别。

访问可能具有副作用

读取状态寄存器可能:

  • 清除中断标志;
  • 取走FIFO中的一个数据;
  • 改变设备内部状态。

写入控制寄存器可能:

  • 立即启动设备;
  • 重置设备;
  • 开启中断;
  • 停止当前传输。

因此,MMIO访问不是简单地“从内存取值”或“向内存保存值”。

访问顺序可能不可交换

例如驱动程序可能必须:

  1. 先写DMA地址;
  2. 再写传输长度;
  3. 最后写启动位。

如果CPU或编译器把启动命令提前,设备可能读取到尚未准备好的参数。

所以驱动程序需要使用:

  • 专门的MMIO访问函数;
  • volatile语义;
  • 内存屏障;
  • 架构规定的设备内存属性。

MMIO通常不能像普通内存一样缓存

设备状态可能随时变化。如果CPU把寄存器值长期缓存,驱动程序就无法看到最新状态。

因此,设备地址区域通常具有特殊的缓存和顺序属性。


3. Port I/O与MMIO

对比点 Port I/O MMIO
地址空间 独立I/O端口空间 CPU物理地址空间
指令 专用in/out 普通load/store
编程模型 设备与内存分离 形式上与内存统一
访问对象 I/O端口 映射后的设备寄存器
是否为普通内存 也不是
是否具有副作用 可以有 可以有

二者的共同点是:

CPU都需要执行指令直接读写设备寄存器。

如果CPU还要通过这些寄存器逐字节搬运大量数据,开销就会非常高。


本节核心结论: Port I/O和MMIO都是CPU访问设备寄存器的方法;MMIO只是让设备寄存器使用内存地址形式,并没有把设备变成普通内存。


九、PCI与设备接入

现代计算机中的多种设备通过PCI或PCI Express连接,例如:

  • 网卡;
  • 显卡;
  • 声卡;
  • NVMe控制器;
  • USB控制器。

PCI体系为设备提供统一的:

  • 枚举机制;
  • 配置空间;
  • 地址资源;
  • 中断机制;
  • 总线互连。

PCI设备通常可以通过三元组定位:

1
Bus : Device . Function

例如:

1
03:00.0

表示:

1
2
3
Bus 03
Device 00
Function 0

在Linux中可以使用:

1
lspci

查看PCI设备。

PCI解决的是设备怎样接入计算机并被系统发现的问题。具体设备仍然需要驱动程序配置寄存器、建立DMA队列并处理中断。


十、为什么CPU不能一直负责搬运数据

假设网卡接收到一个数据包,如果完全使用CPU直接搬运,可能执行:

1
2
3
4
5
6
7
从设备数据寄存器读取一小段

数据进入CPU寄存器

CPU把数据写入内存

重复直到全部完成

数据路径为:

1
设备 → CPU寄存器 → 内存

发送数据时则相反:

1
内存 → CPU寄存器 → 设备

CPU在这个过程中主要执行重复搬运动作,并没有完成复杂计算。

如果设备吞吐量很高,例如:

  • 高速网卡;
  • NVMe SSD;
  • GPU;
  • 音视频采集设备;

CPU可能把大量时间消耗在数据复制上,无法运行应用程序和操作系统其他任务。

解决这一问题的关键机制是DMA。


十一、DMA:设备与内存直接传输

DMA全称为Direct Memory Access,即直接内存访问。

DMA允许设备或DMA控制器直接在设备与物理内存之间传输数据:

1
设备 ⇄ 物理内存

CPU不再为每个字节执行读取和写入指令。

其职责从“亲自搬运”变为:

1
2
3
4
5
6
7
准备任务

启动设备

执行其他工作

接收完成通知

1. 一次DMA读取的完整流程

以从存储设备读取数据到内存为例。

准备缓冲区

内核首先准备一段内存,用于接收设备数据。

1
2
缓冲区地址:P
缓冲区长度:N

这段内存必须满足设备和DMA系统的访问要求。

配置DMA参数

CPU通过驱动程序向设备提供:

  • 传输方向;
  • 缓冲区地址;
  • 数据长度;
  • 设备命令;
  • 控制标志。

例如:

1
2
3
方向:设备 → 内存
目标地址:P
长度:N

启动设备

驱动程序写入设备控制寄存器,通知设备开始执行任务。

设备直接传输

设备或DMA控制器取得总线使用权,把数据直接写入内存。

1
2
3
4
5
存储设备

DMA传输

内存缓冲区

CPU不需要对每个字节执行load/store。

发出中断

任务完成后,设备向CPU发送中断:

1
此前提交的I/O请求已经完成

中断处理

CPU进入设备中断处理程序,执行:

  • 读取完成状态;
  • 检查错误;
  • 确认传输结果;
  • 更新请求队列;
  • 唤醒等待线程;
  • 提交后续I/O。

2. DMA省掉了什么

DMA省掉的是:

CPU在大批量数据传输过程中逐字节或逐字搬运数据的工作。

CPU仍然需要:

  • 分配和管理缓冲区;
  • 设置DMA描述符;
  • 配置设备;
  • 启动传输;
  • 处理错误;
  • 接收完成中断;
  • 回收资源。

因此,更准确的说法是:

1
2
CPU负责控制
DMA负责搬运

而不是“DMA完全不需要CPU”。


3. DMA描述符与队列

现代设备通常不会每次只接收一个简单地址和长度。

驱动程序可能在内存中建立DMA描述符,记录:

  • 数据缓冲区地址;
  • 长度;
  • 方向;
  • 状态;
  • 下一个描述符的位置。

多个描述符可以形成环形队列或链表。

例如网卡接收队列可以预先准备多个空缓冲区:

1
2
3
描述符1 → 缓冲区1
描述符2 → 缓冲区2
描述符3 → 缓冲区3

网卡收到数据后,直接选择描述符并把报文写入相应内存。

这样可以降低CPU为每个数据包重新配置设备的开销。


4. 为什么少量数据不一定适合DMA

DMA存在固定管理成本:

  • 准备缓冲区;
  • 建立映射;
  • 填写描述符;
  • 启动控制器;
  • 处理中断;
  • 清理状态。

如果只需要传输几个字节,这些准备成本可能比CPU直接读写设备寄存器更高。

所以通常:

1
2
3
4
5
控制寄存器和少量数据
→ CPU直接访问更简单

大批量、高吞吐数据
→ DMA更高效

5. DMA完成后为什么使用中断

如果没有中断,CPU可能需要不断读取状态寄存器:

1
2
3
while (!device_finished()) {
// 持续查询
}

这种方式称为轮询,会消耗CPU时间。

中断允许CPU在设备工作时执行其他线程。设备完成后再主动通知CPU。

1
2
3
4
5
轮询:
CPU不断询问设备完成没有

中断:
设备完成后主动通知CPU

在高吞吐设备中,也可能结合中断与批量轮询,以减少中断过多带来的开销。


本节核心结论: DMA并不取消CPU对I/O的控制,而是把大量重复数据搬运交给设备完成,CPU只负责配置、启动和处理结果。


十二、IOMMU:约束设备的内存访问

CPU访问内存时,通常使用:

1
2
3
虚拟地址
↓ MMU
物理地址

设备执行DMA时,也需要一个可以使用的地址。

但设备看到的地址不一定直接等于CPU物理地址,设备的地址范围也可能受到限制。

IOMMU,即I/O Memory Management Unit,负责把设备使用的I/O地址转换为物理内存地址:

1
2
3
设备I/O虚拟地址
↓ IOMMU
物理内存地址

1. 地址转换

操作系统可以为设备建立映射:

1
2
3
设备地址0x1000

物理内存地址0x8A001000

驱动程序把设备地址写入DMA描述符,设备不必直接知道真实物理地址。

这使操作系统能够:

  • 处理设备寻址能力限制;
  • 使用不连续物理内存;
  • 重映射DMA缓冲区;
  • 支持虚拟机设备隔离。

2. 访问隔离

DMA设备具有直接访问内存的能力。

如果没有保护,一个故障设备可能:

  • 覆盖内核代码;
  • 读取其他进程数据;
  • 修改页表;
  • 破坏文件缓存;
  • 泄露敏感信息。

IOMMU允许操作系统限制某个设备只能访问授权区域。

1
2
3
4
5
设备A
→ 只能访问缓冲区A

设备B
→ 只能访问缓冲区B

这类似于MMU限制进程只能访问自己的虚拟地址空间。


3. MMU与IOMMU

对比点 MMU IOMMU
服务对象 CPU DMA设备
输入 CPU虚拟地址 设备I/O地址
输出 物理地址 物理地址
主要作用 进程地址转换与隔离 设备地址转换与隔离
转换缓存 TLB IOTLB

IOTLB用于缓存IOMMU地址转换结果,避免设备每次DMA访问都重新查询映射表。


4. IOMMU并非没有代价

IOMMU会带来:

  • 映射建立和撤销开销;
  • 地址转换开销;
  • IOTLB未命中;
  • 驱动和内核管理复杂度。

但在安全、虚拟化和大型DMA系统中,这些代价通常是可以接受的。


本节核心结论: IOMMU为设备DMA提供地址转换和访问隔离,其作用类似于设备侧的MMU。


十三、PIO与DMA的对比

对比点 CPU直接控制的I/O DMA
数据搬运者 CPU 设备或DMA控制器
数据是否逐步经过CPU寄存器 通常是 不需要
CPU开销 随数据量增加 主要是配置与完成处理
适合场景 控制寄存器、少量数据 大规模、高吞吐传输
复杂度 较低 较高
完成检测 轮询或中断 通常通过中断
是否仍需CPU 需要 仍然需要

可以用一个搬运场景理解:

1
2
3
4
5
6
7
CPU直接搬运:
负责人亲自逐箱搬货。

DMA:
负责人填写搬运单,
搬运队负责运输,
完成后向负责人报告。

其中:

  • 负责人是CPU;
  • 搬运队是DMA;
  • 货物是数据;
  • 仓库是内存;
  • 运输目的地是设备;
  • 完成通知是中断。

十四、一次磁盘读取如何贯穿整个操作系统

假设应用程序执行:

1
read(fd, buffer, size);

完整过程大致如下。

1. 进入内核

系统调用使CPU从用户态进入内核态。

2. 根据fd找到文件对象

内核通过进程文件描述符表找到打开文件对象,并确定当前文件偏移量。

3. 文件系统定位数据

VFS调用具体文件系统,文件系统根据inode和块映射确定目标数据块。

4. 检查页缓存

如果目标数据已经在页缓存中,内核可以直接复制或映射数据,无需访问设备。

5. 构造块I/O请求

缓存未命中时,块设备层为目标逻辑块建立I/O请求。

6. 驱动配置设备与DMA

驱动程序:

  • 准备内存页面;
  • 建立DMA映射;
  • 填写描述符;
  • 配置设备寄存器;
  • 启动设备。

7. 应用线程阻塞

如果数据尚未完成,当前线程进入等待状态。调度器可以运行其他线程。

8. 设备执行DMA

设备将数据直接写入物理内存中的页缓存页面。

9. 设备发出中断

传输完成后,设备向CPU通知完成。

10. 内核处理中断

中断处理程序检查结果,更新I/O请求状态,并唤醒等待线程。

11. 返回用户态

数据被复制到用户缓冲区,或者已经通过映射供进程访问。read()返回读取字节数。

这一流程把多个操作系统章节联系起来:

  • 系统调用负责用户态与内核态切换;
  • 文件系统负责把文件偏移映射到数据块;
  • 页缓存减少设备访问;
  • 驱动程序控制硬件;
  • DMA负责传输数据;
  • 中断负责完成通知;
  • 阻塞与调度让CPU不必等待设备;
  • 虚拟内存把物理页面映射给进程。

设备管理不是孤立机制,而是操作系统各子系统共同协作的结果。


十五、几个容易混淆的问题

1. 字符设备不是只能传文本

字符设备可以传输任意二进制字节。分类依据是顺序流式访问模型,而不是内容是否可打印。


2. 块设备不是每次只能读一个块

块只是寻址和组织单位。一次I/O可以覆盖多个连续块。


3. 网络设备不能简单视为字符设备

网络通信需要表达报文边界、地址、协议和连接状态,因此通常由Socket和协议栈提供更高层抽象。


4. 一切皆文件不意味着一切都是普通磁盘文件

它表示不同对象可以复用文件描述符和部分系统调用接口。对象在内核中的语义和实现仍然不同。


5. MMIO不是把设备内容复制到内存

MMIO只是把设备寄存器分配到CPU地址空间。访问该地址时,请求会到达设备,而不是普通RAM。


6. MMIO形式像变量,行为却可能是命令

一次读取可能清除状态,一次写入可能启动设备。因此不能像普通内存变量一样随意缓存、合并或重排。


7. DMA不是完全绕开CPU

CPU仍负责建立任务、提供缓冲区、配置设备、处理中断和错误。DMA只负责大规模数据传输阶段。


8. DMA不一定适合极小数据

DMA具有固定配置成本。少量数据可能直接通过寄存器访问更快、更简单。


9. MMU与IOMMU作用对象不同

MMU管理CPU内存访问,IOMMU管理设备DMA访问。


10. 中断不是数据传输本身

中断主要负责通知CPU“设备状态发生变化”或“任务已经完成”。

真正的数据可能早已由DMA写入内存。


十六、统一理解:抽象、控制与传输

整个设备管理体系可以分成三个层次。

1. 抽象层

回答:

应用程序把设备看成什么?

主要包括:

  • 字符设备;
  • 块设备;
  • 网络设备;
  • 文件描述符;
  • Socket。

2. 控制层

回答:

驱动程序怎样向设备下命令?

主要包括:

  • 设备寄存器;
  • Port I/O;
  • MMIO;
  • PCI配置;
  • 驱动程序。

3. 数据传输层

回答:

大量数据怎样在设备与内存之间移动?

主要包括:

  • CPU直接搬运;
  • DMA;
  • IOMMU;
  • 中断完成通知。

这三个层次共同构成操作系统设备管理的完整逻辑。


结论

设备管理的根本任务,是在高度多样的硬件世界与相对稳定的软件接口之间建立桥梁。

操作系统首先通过设备抽象降低应用程序的复杂度:

  • 字符设备面向顺序字节流;
  • 块设备面向可寻址数据块;
  • 网络设备面向格式化报文和通信端点;
  • 文件描述符体系使多种对象能够共享部分接口。

在更底层,设备驱动通过Port I/O或MMIO访问设备寄存器,向硬件下达命令。

对于少量控制信息,CPU直接访问寄存器已经足够;对于磁盘、网卡和GPU等高吞吐设备,大规模数据搬运通常由DMA完成。

CPU负责准备缓冲区、配置任务和处理结果,设备则直接与内存交换数据,并在完成后通过中断通知CPU。IOMMU进一步为DMA提供地址转换和访问隔离,防止设备任意访问物理内存。

因此,设备管理可以归纳为一句话:

操作系统在上层用抽象统一设备,在底层用驱动控制设备,并通过DMA、中断与地址转换机制高效、安全地完成数据传输。

设备管理的核心不是让所有硬件变得相同,而是让应用程序不必直接面对硬件的不同。

DMA的核心也不是让CPU彻底退出I/O,而是把重复的数据搬运交给设备,使CPU能够专注于控制、调度和计算。