1.7 我是一个键盘
把设备事件、控制器缓冲、总线、轮询、中断和 DMA 放进同一个输入传递合同。
学习目标
- 能沿键盘、控制器、总线、驱动和进程追踪一次输入事件
- 能比较轮询、中断和 DMA 的 CPU 成本、延迟、缓冲和责任边界
- 能用正常、边界和单一故障样本定位丢键、重复消费、溢出和通知丢失
1.7 我是一个键盘
本页依据刘欣《码农翻身》(2018 年第 1 版)及出版社公开书目信息,独立重构 1.7 我是一个键盘。正文、代码、图示、实验和练习都是本课程重新设计的教学材料,不复制原书正文、插图、练习答案或代码。
键盘不是 CPU 的一个变量,而是一条设备输入链:按键产生事件,控制器暂存,设备通过总线和端口暴露状态,内核驱动搬运并解释,进程最终消费。轮询让 CPU 主动检查,中断在事件到达时通知,DMA 适合批量搬运;三者都不能跳过驱动、权限和缓冲边界。
三个会让设备输入失真的陷阱
五个词组成输入事件合同
<Term def="由键盘或其他设备产生、带有时间和状态的可观察输入变化,而不是应用已经解释后的字符。">设备事件</Term>是链路起点。事件可能丢失、重复或合并,必须有序列和缓冲证据。
<Term def="保存设备状态和待处理输入,并由驱动按协议读取、确认和释放的硬件或内核结构。">控制器缓冲</Term>决定突发输入能否被吸收。缓冲满时要有明确丢弃或背压规则。
<Term def="连接设备、控制器、CPU 和内存的传输路径,以及设备状态可访问的端口或寄存器边界。">总线和端口</Term>定义谁能读写什么。它不是应用可以随意调用的函数。
<Term def="由 CPU 主动重复检查设备状态并读取数据的输入方式;成本取决于检查次数、单次成本和事件密度。">轮询</Term>适合简单或高频场景,但要显式控制忙等和空缓冲。
<Term def="设备完成或需要服务时通知处理器,再由内核驱动确认、搬运和唤醒消费者的机制。">中断与 DMA</Term>把通知和批量搬运分开:DMA 不负责解释应用语义,中断也不自动交付数据给进程。
五个词形成输入链:设备事件进入控制器缓冲,经总线和端口可见,再由轮询或中断触发驱动处理,必要时用 DMA 批量搬运,最后交给进程消费。
目录节点到机制证据
二等公民
设备在计算机系统中不是“附带的输入源”,它拥有自己的时序、缓冲和故障边界。二等公民的视角要求我们承认设备、驱动和进程各有责任,不把 CPU 的视角当成全系统真相。
总线和端口
总线和端口定义数据、状态和控制如何跨越设备与主机边界。复核时记录端口、寄存器、方向、权限和确认顺序,避免把硬件状态与用户字符混成一层。
轮询(程序式I/O)
轮询(程序式I/O)由 CPU 主动检查设备。它可以简单、可预测,但在事件稀疏时浪费检查,在突发输入时仍需要缓冲和溢出处理;CPU 成本可用检查次数乘单次检查成本估计。
中断
中断在设备需要服务时通知处理器,内核再进入驱动路径。中断不是应用回调,通知到达也不保证数据已被安全消费;驱动要确认、读取、排队并唤醒正确的进程。
DMA
DMA 让设备和内存之间批量搬运数据,减少 CPU 逐字节参与。它要求明确缓冲区地址、长度、方向、完成通知、缓存可见性和所有权交接。
输入传递合同
type InputTransfer = {
eventId: string;
controllerBuffer: string;
transport: "poll" | "interrupt" | "dma";
driverAction: string;
consumer: string;
firstDeviation: string;
recovery: string;
decision: "consume" | "backpressure" | "drop";
};
function canDeliver(input: InputTransfer) {
return (
input.eventId.length > 0 &&
input.controllerBuffer.length > 0 &&
input.driverAction.length > 0 &&
input.consumer.length > 0 &&
input.firstDeviation.length > 0
);
}合同把通知、搬运和消费分开。只要事件身份、缓冲、驱动动作或消费者不清楚,就选择 backpressure 或 drop,并保留恢复证据;不能因为中断发生或 DMA 完成就宣布应用已经收到正确字符。
四步复核设备输入
1. 捕获事件并检查控制器
固定按键序列、时间、控制器缓冲和端口状态,预测空缓冲、单事件和突发事件会怎样变化。先确认事件身份,不从最终字符倒推输入。
Lab
输入事件样本台
只改变一个条件,观察事件身份、缓冲状态、通知和所有权如何改变判定。
三次按键,三次有序消费
event-17 → buffer-17 → driver-17 → process-17
判定
consume:保留顺序和完成记录
当前样本:正常输入;复核时只改变一个条件,并保存首个异常前后的队列记录。
正常、边界与单一故障证据
| 样本 | 只改变的变量 | 预期判定 | 必存证据 |
|---|---|---|---|
| 正常 | 事件、缓冲、通知、驱动和消费完整 | 字符顺序可解释,CPU 成本可估计 | 事件、队列、通知、消费记录 |
| 边界 | 突发速率、缓冲容量、轮询间隔或批量大小越过阈值 | 回压或明确丢弃,不假装全收 | 阈值、丢失策略、恢复结果 |
| 单一故障 | 一个端口、通知、DMA 批次或驱动确认失效 | 首个输入异常暴露并恢复 | 首差、所有权、回放日志 |
最小输入证据包与反例
证据包包含设备事件序列、控制器和端口状态、传输方式、CPU 检查或中断记录、DMA 缓冲、驱动版本、进程队列、消费顺序、丢失与恢复策略。最终字符正确或 CPU 占用下降,只能辅助定位,不能独立证明事件链可靠。
反例可以是高频输入下轮询丢键,也可以是 DMA 完成通知先于缓存可见性导致进程读旧数据。发现反例后,保留事件和缓冲轨迹,调整间隔、批量、确认或所有权,再重放;不要用平均吞吐掩盖少数输入损失。
术语表
名词解释
本章出现的专业名词,用大白话再讲一遍。
- 设备事件
由设备产生、带有时间和状态的可观察输入变化。
- 控制器缓冲
保存设备状态和待处理输入,并由驱动按协议读取、确认和释放的结构。
- 总线和端口
连接设备、控制器、CPU 和内存的传输路径及设备状态边界。
- 轮询
由 CPU 主动重复检查设备状态并读取数据的输入方式。
- 中断与 DMA
设备通知处理器并在需要时批量搬运数据的两类协作机制。
练习
练习
问题 1: 低频按键用轮询时 CPU 占用很高,但改成中断后偶尔丢输入。你先看哪些证据?
问题 2: DMA 把一批数据搬完了,但进程读到旧内容。可能缺哪类合同?
问题 3: 控制器缓冲容量小于突发输入,系统不能保证全部按键都保存。如何做出诚实的决定?
本页小结
1.7 我是一个键盘的关键不是记住某个设备名词,而是能从设备事件、控制器缓冲、总线和端口一路追到轮询或中断、DMA、驱动和进程消费。面对突发、溢出和通知故障,保留队列、所有权和首差证据,才能诚实决定消费、回压还是丢弃。