[OS_20] 设备和驱动程序 | GPIO | IPP | PCIe总线 | ioctl
虚拟化和并发 为我们展示了操作系统为应用程序提供的各类 API——我们可以通过系统调用和基于系统调用封装的库函数创建进程和线程使用多个处理器、访问文件系统中的操作系统对象等。
是时候回到 “everything is a file” 的 “everything” 了。
本文内容:计算机系统的最后一块拼图:I/O 设备原理、构造与实现,包括键盘、鼠标、打印机、显卡……我们会感到 “实现计算机系统” 真的是可以做到的。
教材参考:https://pages.cs.wisc.edu/~remzi/OSTEP/file-devices.pdf
20.1 输入/输出设备
Everything is a File
文件:有 “名字” 的数据对象
- 字节流 (终端,random)
- 字节序列 (普通文件)
文件描述符
- 指向操作系统对象的 “指针”
-
- Everything is a file
- 通过指针可以访问 “一切”
- 对象的访问都需要指针
-
- open, close, read/write (解引用), lseek (指针内赋值/运算), dup (指针间赋值)
没那么简单!
让我们插个优盘试试……
- 优盘的文件系统会自动 “出现”
- 但你是专业人士
-
- 看看 /dev/ 是不是发生了一些什么变化
水面下的冰山
- /dev/ 下的对象不会凭空创建
-
- udev - /lib/udev/rules.d
- udisks2 - 这才是真正执行 mount 的程序
今天的主角:输入输出设备
你实际上 “看到” 的计算机
“计算” 和 “物理世界” 之间的桥梁
I/O 设备 = 一个能与 CPU 交换数据的接口/控制器
- 就是 “几组约定好功能的线” (寄存器)
-
- 通过握手信号从线上读出/写入数据
- 给寄存器 “赋予” 一个内存地址 (Address Decoder)
-
- CPU 可以直接使用指令 (in/out/MMIO) 和设备交换数据
- 是的,就这么简单
理解:硬件--API--OS--API--软件
实现输入/输出
例子:发射核弹
- 使计算机能感知外部状态 (眼睛、耳朵)、对外实施动作 (手)
答案:一根线、一条指令 (树莓派上就有)
GPIO (General Purpose Input/Output)
- 极简的模型:Memory-mapped I/O 直接读取/写入电平信号
GPIO: 一根可以读写数据的线
led = LED(2)
led.on(); time.sleep(0.03); led.off()
- 真正的核弹发射器也是类似的原理……
当然可以!我们来一起看看什么是 GPIO,我会用一个非常生活化的比喻,让你一听就懂。
🌟 GPIO 是什么?
可以把 GPIO 想象成一根“智能电线”。
这根电线很特别,它有两个本事:
- 能读电的信号(输入) —— 就像你的眼睛看到东西一样,它可以“看到”电是高还是低。
- 能发电的信号(输出) —— 就像你的手去按开关一样,它可以“发出”电流来控制别的设备。
🔤 GPIO 的全称是:General Purpose Input/Output
中文就是:“通用输入输出引脚”。
🧠 类比理解:GPIO 像灯的开关和灯泡
想象你家里有一个灯泡和一个开关:
- 如果你用 GPIO 来当开关:
-
- 你让 GPIO 输出高电平,就像按下开关,灯亮了。
- 你让 GPIO 输出低电平,就像松开开关,灯灭了。
- 如果你用 GPIO 来当传感器检测器:
-
- 有人打开门,触动了一个小按钮,电压变了。
- GPIO 能“读”到这个变化,就知道“啊,门被打开了”。
📌 实际应用举例:
GPIO 广泛用于单片机、树莓派(Raspberry Pi)、ESP32、Arduino 这些小型电脑或控制器上。比如:
功能 | 使用方式 |
控制 LED 灯亮灭 | 把 LED 接到 GPIO 上,程序控制高低电平点亮或熄灭 |
读取按钮是否被按下 | 按钮一端接到 GPIO,另一端接地,GPIO 读状态判断是否按下 |
驱动电机、风扇等设备 | 通过电路连接 GPIO 到继电器或驱动芯片,实现控制 |
连接传感器(如温度传感器) | 传感器把数据通过电信号传给 GPIO,程序读取这些数据 |
🔌 图形化示意(简化版)
GPIO 引脚│├───────► 输出(我可以发电)│└───────► 输入(我可以看电有没有)
✅ 总结一句话:
GPIO 就是一根可以被程序控制的电线,既可以当“开关”,也可以当“眼睛”。
20.2 输入/输出设备案例
#1: 串口 (UART)
“COM1” (Communication 1)
#define COM1 0x3f8static int uart_init() {outb(COM1 + 2, 0); // 控制器相关细节outb(COM1 + 3, 0x80);outb(COM1 + 0, 115200 / 9600);...
}static void uart_tx(AM_UART_TX_T *send) {outb(COM1, send->data);
}static void uart_rx(AM_UART_RX_T *recv) {recv->data = (inb(COM1 + 5) & 0x1) ? inb(COM1) : -1;
}
#2: 键盘控制器
IBM PC/AT 8042 PS/2 (Keyboard) Controller
- Port 0x60 (data), 0x64 (status/command)
- command = 0xED → LED 灯控
- command = 0xF3 → 设置重复速度和重复延迟
-
- PS/2 接口的 6 根线分别是什么作用?
#3: 磁盘控制器
ATA (Advanced Technology Attachment)
- IDE 接口磁盘 (40pin data 很 “肥” 的数据线 + 4pin 电源)
-
- primary: 0x1f0 - 0x1f7; secondary: 0x170 - 0x177
void readsect(void *dst, int sect) {waitdisk();out_byte(0x1f2, 1); // sector count (1)out_byte(0x1f3, sect); // sectorout_byte(0x1f4, sect >> 8); // cylinder (low)out_byte(0x1f5, sect >> 16); // cylinder (high)out_byte(0x1f6, (sect >> 24) | 0xe0); // driveout_byte(0x1f7, 0x20); // command (write)waitdisk();for (int i = 0; i < SECTSIZE / 4; i ++)((uint32_t *)dst)[i] = in_long(0x1f0); // data
}
#打印机
打印机是个怎样的设备?
- 打印机将字节流描述的文字/图形打印到纸张上
PostScript 和打印机
PostScript: 一种描述页面布局的 DSL
- 类似于汇编语言 (由 “编译器”,如 latex,生成)
-
- PDF 是 PostScript 的 superset
打印机 (没错,实现自己的打印机没有那么困难)
- 将汇编语言翻译成机械部件动作的设备
-
- PCL, PostScript, IPP (AirPrint)
<ESC>*t300R // Set resolution to 300 DPI
<ESC>*r1A // Start raster graphics
<ESC>*b100W // Set width of raster data (100 bytes)
<ESC>*b0M // Set compression mode (0 = uncompressed)
<ESC>*b100V // Send 100 bytes of raster data
<binary raster data> // Actual image data
<ESC>*rB // End raster graphics
当然可以!下面我用简单、易懂、简洁的方式,帮你理解这三个听起来很专业的术语:PCL、PostScript、IPP(AirPrint)。
📄 它们都是“翻译官”——把电脑里的内容翻译成打印机能懂的语言
你的电脑或手机要打印一份文档时,它说的是“数字语言”,而打印机是“机械动作”。
所以需要一个翻译官,把你要打印的内容,翻译成打印机能看懂的指令。
这就叫页面描述语言(Page Description Language)。
我们来认识三位翻译官👇
1. PCL(Printer Command Language)
- 🔤 全称:打印机命令语言
- 🏢 厂家:HP(惠普)
- 🧾 特点:
-
- 快速、轻量级
- 适合普通办公打印
- 不太讲究“高颜值”,但效率高
📌 就像一个办事利索的文员,能快速完成表格、文字打印。
2. PostScript
- 🎨 特点:
-
- 功能强大,擅长处理图片、字体和复杂排版
- 常用于专业印刷、设计领域
- 比 PCL 更“讲究”
- 💻 最初由 Adobe 发明
📌 就像一位设计师,追求细节和美感,做海报、画册它最在行!
3. IPP(Internet Printing Protocol) / AirPrint
- 🌐 是一种网络打印协议
- 🍏 Apple 的 AirPrint 就是基于 IPP 实现的
- ✨ 特点:
-
- 支持无线打印(Wi-Fi 或互联网)
- 手机、平板也能直接打印
- 简单方便,不用装驱动
📌 就像你用微信发个文件给打印机:“嘿,帮我打一下这个!”
打印机说:“收到,马上安排。”
✅ 总结:
名字 | 干啥的 | 像谁 |
PCL | 快速打印文字表格 | 利索的文员 |
PostScript | 高质量图文印刷 | 设计师 |
IPP (AirPrint) | 手机/电脑无线打印 | 微信里秒回的靠谱朋友 |
如果想要 “无穷无尽” 的 I/O 设备?
计算机硬件生态的 “扩展性”
- 想卖大价钱的 “大型机”:IBM, DEC, ...
- 车库里造出来的 “微型机”:名垂青史的梦想家
-
- IBM PC/AT: ISA (Industry Standard Architecture) 总线
- Apple II: 50-pin slot connector (Apple II Bus)
#5: 总线
提供设备的 “虚拟化”:注册和转发
- 把收到的地址 (总线地址) 和数据转发到相应的设备上
- 例子: port I/O 的端口就是总线上的地址(和网络的端口设计 感觉有异曲同工之妙~)
-
- IBM PC 的 CPU 其实只看到这一个 I/O 设备
win98-scanner
(回看计算机的历史,会发现非常的有趣~)
PCIe 总线
今天获得 “CPU 直连” 的标准设备
- 接口
-
- 75W 供电
-
-
- 所以我们需要 6-pin, 8-pin 的额外供电
-
- 数据传输
-
- PCIe 6.0 x16 带宽达到 128GB/s
-
-
- 于是我们有了 800Gbps 的网卡
-
-
- 总线自带 DMA (专门执行 memcpy 的处理器)
- 中断管理
-
- 将设备中断转发到操作系统 (Message-signaled Interrupts)
PCIe 总线 (cont'd)
支撑了现代 I/O 设备的体系
- 高速设备都是直插 PCIe 的
-
- FPGA
- 显卡
- 网卡
- ……
- USB Bridge
- NVMe
🚀 什么是 PCIe 总线?
你可以把 PCIe 总线 想象成一条“高速公路”,它是连接电脑内部各种高速设备和 CPU 的主干道。
它不是普通的路,而是像“京港澳高速”一样又快又宽,专门用来跑那些对速度要求很高的“高级车”——比如显卡、网卡、固态硬盘等。
🔌 接口与供电:不只是数据通道,还能供电
- PCIe 插槽本身能提供最多 75W 的电力。
- 但有些设备太耗电了(比如高端显卡),75W 不够用。
- 所以它们会额外加一些电源接口,比如:
-
- 6-pin
- 8-pin
📌 就像一辆电动车插在充电桩上充电,但它功率太大,还需要再插一个快充头。
📦 数据传输:超级快!
- PCIe 6.0 x16 带宽已经达到了 128GB/s
-
- 这个速度有多快?相当于你一秒就能复制一部 4K 蓝光电影!
- 因为这么快的速度,我们现在才能看到:
-
- 800Gbps 网卡(超高速网络)
- NVMe 固态硬盘(秒开系统、秒加载游戏)
🎯 并且它自带一个“搬运工”:DMA(直接内存访问)
- 它就像一个专职快递员,可以在不打扰 CPU 的情况下,自己把数据从设备搬到内存里。
⚡ 中断管理:设备也能“敲门”找操作系统
- 设备完成任务后,需要告诉操作系统:“我做好啦!”
- PCIe 支持一种叫 MSI(消息中断) 的技术,可以让设备通过“发消息”的方式通知操作系统。
📌 相当于你在排队办事时,不是大声喊“到我了!到我了!”,而是按了个按钮,系统就知道轮到你了。
🧱 PCIe 总线支撑了哪些现代设备?
很多高性能设备都直接插在 PCIe 总线上:
设备 | 功能 |
显卡 GPU | 负责图形渲染、玩游戏、AI 计算 |
NVMe 固态硬盘 | 极速读写,开机、加载程序飞快 |
网卡 | 高速联网,支持 10G/25G/100G/800G 网络 |
FPGA | 可编程芯片,用于特殊计算任务 |
USB 控制器(Bridge) | 把 USB 接口接到主板上 |
✅ 总结一句话:
PCIe 是现代电脑的“信息高速公路”,速度快、带宽大、还能供电,让各种高速设备直接连接 CPU,实现极速响应和数据传输。
🛣️ PCIe 版本的区别
PCIe 标准随着时间的发展不断更新,每个新版本都会带来更快的速度和更高的效率。以下是几个关键版本的主要区别:
- PCIe 3.0:
-
- 每条通道(lane)的数据传输速率为 8 Gbps。
- 对于 x16 插槽(16 条通道),最大带宽为 128 Gbps。
- PCIe 4.0:
-
- 数据传输速率翻倍至每条通道 16 Gbps。
- 对于 x16 插槽,最大带宽可达 256 Gbps。
- PCIe 5.0:
-
- 再次翻倍到每条通道 32 Gbps。
- x16 插槽的最大带宽达到了 512 Gbps。
- PCIe 6.0(最新标准):
-
- 这次不仅速度加倍到每条通道 64 Gbps,而且采用了新的编码技术(PAM4),进一步提高了效率。
- x16 插槽的最大带宽可高达 1024 Gbps。
这意味着,随着版本的升级,数据传输能力显著增强,允许更快速度的数据交换,比如支持更高分辨率的游戏画面或更快的存储设备读写速度。
🚂 工作原理:如何实现高效的数据传输?
PCIe 使用一种称为“串行点对点连接”的方式来传输数据。与早期的并行总线不同,它通过一对发送和接收线路(即一个 lane)进行通信。
更多的 lanes(如 x4, x8, x16)意味着更高的总带宽。
- Lane(通道):基本单元,单向传输路径。
- 多 Lane 并行工作:多个 lanes 可以同时工作,提高整体带宽。
例如,一个 PCIe x16 卡使用 16 条 lanes 同时传输数据,因此其带宽是单 lane 的 16 倍。
📤 DMA(直接内存访问)
DMA 是 PCIe 设备中的一项关键技术,它允许设备直接与系统内存交互而不必每次都经过 CPU。这大大减轻了 CPU 负担,提升了系统效率。
- 当你需要从硬盘读取大量数据到内存时,DMA 控制器可以直接操作,无需 CPU 介入,直到传输完成才通知 CPU。
🔗 中断管理:让设备主动报告状态
PCIe 支持消息信号中断(MSI),这是一种让硬件设备能够直接向操作系统发送信号的方法,告知某个事件已经发生(如数据传输完成)。这种方式比传统的轮询更加高效,因为它减少了不必要的 CPU 检查,只有在需要时才会触发响应。
无论是了解其版本间的性能提升,还是其背后的工作机制,都是为了理解为什么 PCIe 对现代计算机如此重要。
https://github.com/lvy010/operating-system_code/tree/main/postscript
PostScript: PostScript是一种页面描述语言 (PDL),由 Adobe Systems 在 1980 年代初开发。
它是一种编程语言,专门用于描述图形和文本的布局和外观,主要用于打印和显示系统。PostScript 文件包含了详细的指令,告诉打印机或显示设备如何生成页面上的每一个元素,包括字体、图形、颜色和图像。
Prompt: NVMe 是如何接入 PCIe 总线的?
NVMe(Non-Volatile Memory Express)是一种为高速存储器(如SSD)设计的主机控制器接口协议。
- 它直接通过PCIe(Peripheral Component Interconnect Express)总线与主板相连
- 绕过传统的SATA或SAS通道,大幅提升数据传输速率和并发处理能力。
详细描述如下:
- 物理层面:NVMe设备通常为M.2、U.2或PCIe扩展卡形态,插入主板相应的PCIe插槽。这样,存储器与CPU之间建立起物理连接,使用PCIe通道(如x2、x4、x8)。
- 链路层与传输层:NVMe利用PCIe的高速点对点通道,支持多条并行通路,实现大带宽、低延迟的数据传输。NVMe协议使SSD能充分利用PCIe 3.0、4.0或更高版本的带宽。
- 协议层面:主机操作系统通过NVMe驱动,与SSD上的NVMe控制器通信。PCIe作为物理和数据链路的载体, NVMe作为协议负责命令、队列管理和数据传输。
- 并发处理:NVMe支持多队列(主机和控制器可各自支持多至64K队列,每队列64K命令),显著提升并发和效率,这是基于PCIe总线的多通道特性实现的。
总结:NVMe设备直接插接在PCIe总线上,利用PCIe的高速通道和NVMe协议实现高效的数据交互,显著提升SSD的性能。
什么是 DMA(直接内存访问)?
想象一下,你有一个大箱子(CPU),里面装满了各种小物件(数据)。
现在你需要把这些小物件搬到另一个房间(设备,比如硬盘)。
通常情况下,你会亲自去搬这些物件,这会占用你很多时间。
但是,如果你有一个助手(DMA控制器),他可以帮你把物件从一个地方搬到另一个地方,而你就可以去做其他事情了。
图片中的流程
- CPU 的工作:
-
- CPU 开始时在执行一些任务(标记为“1”)。
- 当需要将数据传输到磁盘时,CPU 告诉 DMA 控制器:“我需要把这部分数据(标记为“2”)复制到磁盘上。”
- 然后 CPU 就可以继续做其他的事情(继续执行“1”的任务)。
- DMA 的工作:
-
- DMA 控制器接到命令后,开始处理数据传输的任务(标记为“c”)。
- 它会自动地将数据从内存中复制到磁盘上,不需要 CPU 的干预。
- 磁盘的状态:
-
- 在 DMA 复制数据之前,磁盘是空的(标记为“0”)。
- 当 DMA 完成数据复制后,磁盘上就有了新的数据(标记为“2”)。
时间线分析
- CPU 的时间线:CPU 在大部分时间里都在执行“1”的任务,只有在需要传输数据时才会告诉 DMA 控制器要做什么。当 DMA 完成任务后,CPU 才知道数据已经传输完毕。
- DMA 的时间线:DMA 只在 CPU 需要传输数据时才开始工作,它会专注于完成这个任务,直到数据完全复制到磁盘上。
- 磁盘的时间线:磁盘一开始是空的,但在 DMA 完成任务后,它就拥有了新的数据。
为什么这样设计?
通过这种方式,CPU 不需要亲自参与数据的复制过程,它可以腾出手来做其他更重要的事情(比如运行其他进程,标记为“Process 2”)。这样不仅提高了 CPU 的效率,也加快了整个系统的运行速度。(还是 我们一层不行,就再加一层的思想)
总结
简单来说,DMA 就是一个专门负责数据搬运的助手,它可以让 CPU 腾出更多的时间去做更重要的事情,从而提高整个系统的效率。希望这个解释能帮助你更好地理解图片中的内容!如果有任何问题,欢迎继续提问。
20.3 设备驱动程序
用程序访问设备
程序不能直接访问寄存器
- 设备是可以在程序之间共享的
仔细想:CPU 和内存也都是 “设备”
- 操作系统实现了虚拟化
- 我们也实现设备的虚拟化就行了!
Everything is a File
File = 实现了文件操作的 “Anything”
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 *);ssize_t (*read_iter) (struct kiocb *, struct iov_iter *);ssize_t (*write_iter) (struct kiocb *, struct iov_iter *);int (*iopoll)(struct kiocb *kiocb, struct io_comp_batch *, uint flags);int (*iterate_shared) (struct file *, struct dir_context *);__poll_t (*poll) (struct file *, struct poll_table_struct *);long (*unlocked_ioctl) (struct file *, uint, ul);long (*compat_ioctl) (struct file *, uint, ul);int (*mmap) (struct file *, struct vm_area_struct *);ul mmap_supported_flags;int (*open) (struct inode *, struct file *);int (*flush) (struct file *, fl_owner_t id);int (*release) (struct inode *, struct file *);int (*fsync) (struct file *, loff_t, loff_t, int datasync);int (*fasync) (int, struct file *, int);int (*lock) (struct file *, int, struct file_lock *);ul (*get_unmapped_area)(struct file *, ul, ul, ul, ul);int (*check_flags)(int);int (*flock) (struct file *, int, struct file_lock *);ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, uint);ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, uint);void (*splice_eof)(struct file *file);int (*setlease)(struct file *, int, struct file_lease **, void **);long (*fallocate)(struct file *file, int mode, loff_t offset, loff_t len);void (*show_fdinfo)(struct seq_file *m, struct file *f);ssize_t (*copy_file_range)(struct file *, loff_t, struct file *, loff_t, size_t, uint);loff_t (*remap_file_range)(struct file *file_in, loff_t pos_in, struct file *file_out,loff_t pos_out, loff_t len, uint remap_flags);int (*fadvise)(struct file *, loff_t, loff_t, int);int (*uring_cmd)(struct io_uring_cmd *ioucmd, uint issue_flags);int (*uring_cmd_iopoll)(struct io_uring_cmd *, struct io_comp_batch *,uint poll_flags);
} __randomize_layout;
设备驱动程序
一个 struct file_operations 的实现
- 把系统调用 “翻译” 成与设备能听懂的数据
-
- 就是一段普通的内核代码
例子
- devfs 中的 “虚拟” 文件
-
- /dev/pts/[x] - pseudo terminal
- /dev/zero, /dev/null (实现), /dev/random, ...
- procfs 中的 “虚拟文件”
-
- 只要实现读/写操作即可
- 例子:/proc/stat 的实现
- 在 linux 内核专栏中,有分析过这部分代码
驱动 Nuclear Launcher
我们也可以实现一个
- 把对 /dev/nuke0 “路由” 我们的 file_operations
- 向 GPIO 的 memory-mapped address 写入正确的电平
- (当然,我们只是模拟一下)
配置设备
设备不仅仅是数据,还有配置
- 打印机的卡纸、清洁、自动装订……
-
- 一台几十万的打印机可不是那么简单
- 键盘的跑马灯、重复速度、宏编程……
- 磁盘的健康状况、缓存控制……
两种实现方法
- 控制作为数据流的一部分 (ANSI Escape Code)
- 提供一个新的接口 (request-response)
ioctl
The ioctl() system call manipulates the underlying device parameters of special files. In particular, many operating characteristics of character special files (e.g., terminals) may be controlled with ioctl() requests. The argument fd must be an open file descriptor.
“非数据” 的设备功能几乎全部依赖 ioctl
- “Arguments, returns, and semantics of ioctl() vary according to the device driver in question”
ioctl (cont'd)
堆叠的💩山
- 设备的复杂性是无法降低的
-
- “就是有那么多功能”
- UNIX 的负担:复杂的 “hidden specifications”
-
-
- 另一个负担:procfs
-
例子
- 终端:为什么 libc 能 “智能” 实现 buffer mode?
- 网卡,GPU,……
- KVM Device (代码示例)
https://github.com/lvy010/operating-system_code/tree/main/launcher
一个设备驱动程序:理解了设备驱动程序的职责是把系统调用 “翻译” 成与设备能听懂的数据,我们也可以实现 file_operations 中相应的操作,从而模拟一个设备。
https://github.com/lvy010/operating-system_code/tree/main/ioctl
隐藏在 libc 中的设备查询: libc (musl libc) 会根据是否输出到 tty 控制缓冲行为;glibc 则是使用 fstat。功能的增加势必带来了操作系统和应用程序的复杂性。
https://github.com/lvy010/operating-system_code/tree/main/kvm
KVM Device: KVM 设备提供了硬件虚拟化的机制,允许我们在用户空间通过 /dev/kvm 在虚拟化的环境中运行一段代码直到 VM Exit。
20.4 总结
Take-away messages: 输入/输出设备可以说是五花八门,你也看到越来越多的设备上甚至 “自带电脑”。
- GPIO:一种允许微控制器或芯片与外部设备进行简单输入输出通信的接口。
- IPP:一种网络打印协议,让计算机能够通过网络连接并管理打印机作业。
- PCIe总线:用于内部组件间高速数据传输的扩展插槽和接口标准,支持如显卡、固态硬盘等高性能硬件直接与主板通信。
- ioctl:操作系统提供的一种接口,允许用户空间程序向设备驱动发送命令以控制硬件设备的操作
但无论如何,操作系统都把它们抽象成一个可以读写、可以控制的,实现了 struct file_operations 的文件 (操作系统对象)