# 从 printf 到屏幕:程序和硬件之间发生了什么

从 printf 到屏幕:程序和硬件之间发生了什么

先看一个问题

c 复制代码
printf("hello")

这句话最终怎么出现在屏幕上的?

为什么要这样设计?

如果程序直接操作硬件

  • 程序A 直接写显存地址 0xB8000
  • 程序B 也写显存地址 0xB8000

结果:

  1. 两程序互相干扰,画面变乱
  2. 恶意程序可随意读写其他程序内存
  3. 换显卡后,所有程序都要重写

那操作系统作为中间层,他可以做到

  1. 统一管理 -> 谁在什么时候能访问哪块硬件,OS 说了算
  2. 安全隔离 -> 程序只能通过 API 申请,不能直接抢
  3. 屏蔽差异 -> 换显卡只需换驱动,程序不用改

in 指令 和 out 指令

虽然说程序不直接访问硬件,但驱动程序最终还是要直接和硬件说话

那驱动是如何做到的,靠的就是: in 和 out

首先来看,端口是什么

从端口读数据:

text 复制代码
in al, 60h  # 从端口 60h(键盘)读一个字节到 al 寄存器

从端口写数据:

text 复制代码
out 378h, al      # 把 al 的内容写到端口 378h(打印机)

Windows 为什么限制这两条指令?

  1. 恶意程序直接 out 到硬盘控制器 强制覆盖硬盘数据,绕过所有文件系统保护
  2. 两程序 out 到同意端口 数据互相干扰

所有 Windows 规定:只有内核态的驱动才能用 in/out,普通程序使用直接报错

内核态和用户态

CPU并不只有一种运行模式,他有两个权限级别

那 用户态程序想用 硬件 怎么办,通过 系统调用,申请内核代劳

为什么分两种模式,简单来说

  • 如果程序都在内核态,一崩全崩,恶意程序随意读写
  • 隔离之后,崩只崩自己,不影响OS,恶意程序只能影响自己的内存范围

新问题来了,CPU 通过端口和外设通信,但 CPU 怎么知道外设"准备好了"?

一般想法, 让CPU从端口中查看某某寄存器中是否有值

这没错,理论可行,这种方法也被称为轮询,如果用代码解释

c 复制代码
while(true){
 //检查键盘端口有没有新数据?
 //检查打印机端口有没有新数据?
}

但是有个严重问题:

键盘每秒最多几个字,但CPU 每秒执行几十亿条指令

这会导致一个现象,CPU绝大部分时间都在查找,是否有新数据

那么就需要一个新的解决办法:中断

中断控制器

它和轮询截然相反,就相当于

  • 轮询:CPU去问,你好了吗?
  • 中断:由你去说:"我准备好啦!"

假如同时有多个设备发出中断,则排队一个个交给CPU处理,由中断控制器负责排列优先级,避免 CPU 同时收到多个中断

问题来了,如果 中断处理程序忘记恢复某个寄存器,会发生什么?

无法回到原先程序?不全对,如果返回地址在栈上,就能回去,但回去之后行为错乱

而且还有个问题,我们知道,一次只处理一个字节,如果我们外设传输量很大,比如1GB,会有什么问题?

这会导致CPU中断次数过于频繁,有多夸张

每中断一次:

  1. 数 10 亿次中断
  2. CPU 全程都在保存/恢复寄存器
  3. 真去处理数据的时间几乎没有

看到这个问题的答案能发现,中断也不够用了,那么,轮到 DMA 登场了

DMA

DMA:让外设直接和内存交换数据,绕过CPU

补充个我当时的疑惑点:为什么要经过 CPU 中转?

简单来说,外设全都是死的,只有 CPU 是唯一能执行指令和做决策的东西

CPU:

  • 执行指令
  • 做判断
  • 协调多个设备
  • 管理内存地址

其他所有东西:

  • 内存:被读被写
  • 硬盘:被读被写
  • 网卡:被告知发什么收什么

显存

显示器并不是从 CPU 那数据,而是专门的一块内存: 显存

在早期 PC 的显存映射:

内存地址:

  • 00000h ~ 9FFFFh → 普通内存(程序用)
  • A0000h ~ BFFFFh → 显存(写这里 = 改变屏幕画面)
  • C0000h 以上 → ROM、其他硬件

CPU 要在屏幕上显示字符,只需要

text 复制代码
 movl  $'A',  0xA0000   # 往显存地址写字符,屏幕上就出现 A

现代显卡:

text 复制代码
	主内存(RAM)              显存(VRAM,显卡上面)
	程序的数据                当前帧的画面数据
	CPU 操作                   GPU操作
	
	数据通过 PCIe 总线传输
	CPU -> 显卡驱动 -> GPU -> 显存 -> 屏幕

本质没变,还是 CPU 决定显示什么,写入显存,GPU负责输出到屏幕

所以,画面流程度不够,可能不只是 GPU 不够好,也可能是你的 GPU 上的显存不够用啊

总结

最后,如果只用一句话去理解 程序与硬件的关系 ,那就是: 程序并不是直接去控制硬件,而是要先经过操作系统这一层,再由操作系统去协调硬件。

这一章最重要的地方,我觉得不是记住 in/out中断DMA 这些名字,而是开始明白一件事: 为什么程序不能随便直接碰硬件。因为一旦让所有程序都直接去抢硬件,马上就会出现互相干扰、安全问题和兼容性问题。

所以后面这一整套设计,其实都是围着这个问题展开的。用户态和内核态 是为了隔离权限,系统调用 是为了让程序通过操作系统申请服务,中断 是为了避免 CPU 一直傻等,DMA 则是在数据量太大时,继续减少 CPU 的负担。

如果说前面几章让我知道程序、操作系统和运行环境各自是什么,那这一章更像是在告诉我: 程序最后要真正影响屏幕、键盘、硬盘这些硬件时,中间到底经过了哪些层,又为什么必须经过这些层。

相关推荐
shepherd1113 小时前
吞吐量提升 10 倍:高并发大批量数据处理任务的架构演进与性能调优
java·后端·架构
禅思院4 小时前
前端部署“三层漏斗”完全指南:从CI/CD到自动回滚的工程化实战【开题】
前端·架构·前端框架
Patrick_Wilson1 天前
幂等到底是什么?从前端视角讲透 SQL、HTTP 与 POST 接口的幂等设计
前端·后端·架构
禅思院1 天前
Vite vs Webpack 深度对比:从启动原理到生产构建,一篇就够了
前端·架构·前端框架
Cerrda2 天前
开发体验升级:UnoCSS 自定义 SVG 图标热更新方案
架构·前端框架
Kstheme2 天前
把任何 GitHub 仓库变成系统设计课:这个开源项目做到了
架构
禅思院2 天前
路由性能高可用架构实战方案
前端·架构·前端框架