你有没有好奇过这么一件事你敲下一个a屏幕上立刻显示出一个a但你面前的键盘、主板、屏幕、处理器之间到底在这一瞬间里做了多少次“沟通”如果只把计算机当成一台“能算数的电器”那很难解释它是怎么从一堆硅片变成今天这个能跑操作系统、能渲染视频、能连全世界网络的家伙的。很多人学计算机都是从高级语言入手的if、for、函数、对象学得头头是道但一遇到“程序为什么崩了”“为什么数据量大就卡”“为什么内存占用这么高”就会发懵。其实所有问题的答案都藏在这台机器最底层的那几层东西里晶体管、逻辑门、存储器、时钟、指令、操作系统。这篇文章我想顺着一条完整的链路从“一个开关”开始一直讲到“屏幕上那行字是怎么来的”。不堆公式不讲天书用工程思维把计算机从头拆到尾。这套底层原理不光是科班学生需要做应用的工程师、做运维的、甚至写脚本的理解了都会有脱胎换骨的感觉——因为当你知道了“底层原本是什么样”很多上层设计“为什么必须是这样”就完全不需要背了。1. 从开关到0和1计算机的“基本粒子”为什么是二进制1.1 晶体管的本质电压控制的“水龙头”一切要从“开关”说起。计算机里最基础的元件叫晶体管你可以把它想象成一个用电控制的水龙头在它的控制极上施加一定电压源极和漏极之间就导通电流可以通过电压撤掉它就断开电流被截住。水龙头是用手拧晶体管是用电压拧仅此而已。现代一颗处理器里这样的“水龙头”有上百亿个每秒钟开关几十亿次。这听起来很吓人但单个晶体管的逻辑极其简单——开和关二选一。它不关心你在上面跑的是游戏还是记账软件它只认识“有电压”和“没电压”。有趣的是这种“两态”正好和二进制对应高电压记作1低电压记作0。于是一台由无数开关组成的机器天然就是一个二进制机器。如果你试图让它直接处理十进制反而要额外设计能区分十种不同电压的电路不仅复杂、抗干扰差而且速度上完全划不来。1.2 为什么必须是二进制而不是三进制或十进制很多人会问二进制写起来一串0和1又长又难看为什么计算机不用十进制这个问题的答案不在数学里而在物理和工程成本里。第一两态最容易物理实现。晶体管天然就是两态器件做三态、十态电路成本成倍上涨可靠性反而下降。第二抗干扰能力强。数字世界里信号只要还能被区分成“高”和“低”两个区间中间有点噪声都无所谓但如果是十档电压每档之间的安全窗口非常窄稍有干扰就判错。第三二值逻辑可以直接映射到布尔代数。0/1和“真/假”一一对应逻辑运算可以直接用电路搭起来不需要额外的转换层。所以二进制不是“被选中的”而是物理世界给出开关工程师顺着开关做出了二进制。至于“一长串看得很累”这种观感问题是人的问题不是机器的问题。1.3 从比特到信息一切数据都是 0 和 1 的组合单个开关只能表达“开/关”但把很多开关并排放在一起表达能力就爆炸了。8 个开关能表达 2^8 256 种状态这就是一个字节。英文字符用一套叫 ASCII 的编码表映射到字节中文因为字符多用多个字节编码一张图片则是把每个像素的颜色采样成若干个字节一段声音是把振动波形按固定时间间隔采样成数值序列。所以你在电脑里看到的文字、图片、视频、可执行程序本质上都只是内存里一串串0和1。区分它们到底是谁的靠的不是“眼睛看”而是“解释方式”——同样的字节按文本解释是一个意思按图片解释是另一个意思按机器指令解释又会执行不同的动作。这个概念在后面理解“程序是怎么运行的”时会反复出现。2. 逻辑门和加法器算术是如何被电路“雕”出来的2.1 三种基本逻辑门电路里的“乐高积木”有了开关我们就能搭逻辑电路了。最基本的三种逻辑门是与门、或门、非门。用晶体管实现它们非常直观两个开关串联就是“与”的关系——两个都得导通电流才过得去两个开关并联就是“或”的关系——只要有任何一个导通电流就通了。非门稍微复杂一点用一个晶体管把输出拉到相反电平就行。不要小看这三块积木。它们组合在一起能实现任何逻辑异或、多路选择器、译码器、比较器一直到整个 CPU 的运算核心。学数字电路的时候老师会告诉你“一切组合逻辑都可以用与或非表达”这话在工程上的意思是只要你把需求写清楚电路设计者就能用成百上千个逻辑门把它拼出来。计算机之所以能“算数”不是因为它懂数学而是因为它把算术问题先转化成了逻辑问题再用一堆逻辑门硬生生“摆”出了一个答案。2.2 加法器从真值表到二进制加法的完整推导算术里最底层的运算是加法因为减法、乘法都可以往加法上靠。那加法怎么用逻辑门实现呢看一位二进制的加法00001110111算出来的结果是 0但要向高位进一位。把“当前位的加和”和“进位”分开看加和用异或门实现进位用与门实现这组电路就叫半加器。两路输入做异或得到本位做与得到进位逻辑就这么简单。多位加法怎么办把多个全加器串联起来每一位不仅加两个输入还加来自低位的进位。这个链条拉长到 8 位、16 位、64 位就是现代 CPU 里的加法器。如果你真想“从底层理解计算机”我建议你亲自拿几个逻辑门芯片或在线仿真器搭一次 4 位加法器。那种“原来数学运算就是一堆开关互相联动”的感觉不是看多少篇文章能替代的。2.3 补码、减法和 ALU把减法变成加法减法怎么办有一种非常巧妙的思路把减数变成它的补码然后直接做加法。为什么补码能让减法变成加法因为如果所有负数都用补码表示那么“A - B”就等价于“A (B的补码)”而补码的求法又只需要取反加一全是电路容易做的事。这样一来CPU 里根本不需要单独的减法器。加减法都统一成加法乘法则退化成多次移位和加法除法退化成移位和减法。把所有这些算术运算电路再加上与、或、非、异或等逻辑运算电路集中放进一块大电路里用一个“操作码”指定这次到底执行哪个操作这块大电路就叫 ALU算术逻辑单元是 CPU 里干苦力活的核心部分。3. 存储的真相寄存器、缓存与内存为什么差这么多3.1 两种内存的本质电容存和锁存既然 CPU 要算数算到一半的数放在哪儿这就要说存储。存储器的底层也分两派。一派叫 DRAM动态随机存取存储器原理是“一个晶体管加一个电容”电容里有没有电荷代表0还是1。它的优点是一个单元格非常小所以能做很大容量、很便宜缺点是电容会漏电需要每隔一段时间就重新充电刷新一次“动态”俩字就是这么来的。另一派叫 SRAM静态随机存取存储器原理是“六个晶体管搭成一个锁存器”只要不断电状态就一直保持不需要刷新。它的优点是快缺点是一个单元格要 6 个晶体管贵而且占面积。CPU 里面的寄存器、缓存都是用 SRAM 做的电脑插的内存条则是用 DRAM 做的。理解了这两种存储的物理差异你就能明白一个核心事实快和便宜是矛盾的存储必须分层。如果你都用 SRAM 做 32GB 内存价格和体积都不可接受如果你都用 DRAM 做 CPU 缓存CPU 速度会被存储速度拖死。3.2 内存带宽是怎么算出来的内存条上经常看到 DDR4-3200 之类的标号3200 的意思是内存与 CPU 交换数据的额定频率是 3200 MT/s每秒百万次传输大约 3.2 GHz。因为 DDR 内存是“双倍数据速率”在时钟的上升沿和下降沿都要传一次数所以它实际的工作时钟频率是 1600MHz乘 2 变成 3200MT/s。再看数据总线宽度大多数桌面内存总线是 64 bit也就是一次传 8 字节。于是理论带宽 3200MT/s × 8 字节 25600 MB/s约 25.6 GB/s。如果你去买内存条会发现标称带宽和这个算出来的数字非常接近。把带宽换算成延迟效果更直观从内存里读一个数据大约要 80~100 纳秒。CPU 在 3GHz 主频下一个时钟周期才 0.33 纳秒也就是说 CPU 读一次内存要干等两三百个周期。这就是为什么缓存必须存在——它是在用空间换时间把最近用过的数据放在离 CPU 更近、更快的小存储器里。3.3 局部性原理用缓存思维写代码缓存之所以有用靠的是程序访问的局部性时间上刚用过的数据很快还会再用空间上刚访问过的地址附近也会被访问。你写for循环遍历数组就是完美的空间局部性你在循环里反复读同一个变量就是完美的时间局部性。反过来如果你的代码到处乱跳、随机访问大片数据缓存就会频繁失效程序就会一次次回到慢速内存去取数。我在实际工程里见过一个真实案例同样一批数据用连续数组存储比用链表存储快出一个数量级原因不是链表本身慢而是链表节点的内存地址不连续每次访问都打不中缓存被迫走内存。所以写代码时“尽量顺序访问、尽量复用数据、避免不必要的大对象拷贝”看起来像经验之谈实际上背后的依据全在这里。理解了缓存是长什么样的你自然就懂优化点该往哪儿找了。4. 指令、时钟与流水线一行代码到底经历了什么4.1 指令集硬件提供的最底层“API”内存里放着程序的数据也放着程序的代码。代码对计算机而言也是一串字节而这一串字节能执行的唯一原因是 CPU 硬件认识它们。CPU 能认识的每一条基本操作都叫一条指令所有指令的集合叫指令集架构ISA。不同架构的“母语”是不同的桌面服务器常用 x86 架构移动端常见 Arm 架构还有开源的 RISC-V 架构。你在高级语言里写的a b c;经过编译器翻译会变成类似load、add、store这样几条机器指令。CPU 解码后把它们翻译成控制信号去寄存器读b和c让加法器算一下再把结果写回a所在的位置。指令集之于程序员有点像“功能菜单”之于顾客——你不需要知道厨房里怎么炒菜但你知道点的每道菜会有固定做法。CPU 内部怎么实现这条指令的那是微架构的事指令集则是对外保证兼容的接口。4.2 五级流水线与时钟节拍一条指令的执行不是一瞬间完成的要拆成好几个步骤取指从内存拿指令、译码判断这是什么指令、执行在 ALU 里算、访存如果需要读写内存、写回把结果写回寄存器。这五个步骤正好像一个流水线上的五个工位。CPU 的时钟就是给流水线打拍子的节拍器。所有工位的动作对齐到这个时钟的上升沿上保证每一步工人都干完活了再一起动一下。主频高拍子快流水线跑得就快。但主频不能无限提高原因也很朴素越快的开关晶体管翻转越频繁发热越严重另外要把数据在纳秒级时间内送到下一个工位物理距离和信号传播速度都是限制。工程上还有个更绕的问题流水线上后一条指令可能用前一条的结果。比如a b c; d a e;第二条必须等第一条把a算完才能开始流水线只好“泡一泡”。CPU 内部靠“乱序执行”和“数据前递”尽量缓解——只要后面那条不依赖前面就提前先做能提前拿到的结果直接从 ALU 抄近路传给下一条不等它慢慢写回寄存器。4.3 分支预测CPU 是怎么“猜”你的代码的程序里到处是if。如果遇到ifCPU 必须先知道跳转往哪边走才能去取后面的指令。一条指令流水线一共五个阶段如果每次都等判断结果出来再取指整个流水线就被浪费了性能惨不忍睹。于是 CPU 内有一个分支预测器它根据过去这段代码每次往哪边走来猜这一次往哪边走。猜对了流水线顺畅跑下去猜错了流水线里被预取的指令全部作废要清空重来这就是一次“流水线气泡”代价非常大。这也是为什么性能敏感的代码里写if要尽量往“极倾斜”的方向写——比如绝大多数情况都走同一条分支让预测器快速学到一个稳定模式。你写的 switch 分支太多、每次走法太随机预言失败率就会上升程序就会肉眼可见地变慢。把编译器、链接器、加载器这一套链路串起来一条printf才会变成 CPU 里一串真正执行的指令。编译器负责任意翻译链接器把多个编译产物和库拼成一个完整可执行文件加载器把它装进内存CPU 再从第一条指令开始取指执行。5. 操作系统的存在感进程、虚拟内存与硬件的握手5.1 程序在内存里的真实布局一个可执行程序被加载进内存后会分成几个明确的区域代码段放着机器指令数据段放着全局变量堆区供程序动态申请和释放内存栈区用来放函数调用的局部变量、返回地址、参数。每次调用函数栈上就压入一个“栈帧”函数返回栈帧弹出。栈为什么会溢出就是因为无限递归不停压栈把分配给栈的那块空间顶穿了。你可能会发现同一个程序启动两次里面的地址看起来一模一样即使物理内存里两个进程放着不同的地方。这里的秘密叫“虚拟内存”每个进程看到的地址空间都是它独占的假象从 0 开始到很大一个范围背后由内存管理单元MMU把虚拟地址翻译成物理地址。翻译靠的是一张页表按一页一页通常 4KB为单位映射。这套机制带来的巨大好处是进程之间互不干扰。你在一个程序里乱写指针顶多把自己的进程打崩不会把另一个程序的数据踩了同时程序不需要关心物理内存到底够不够大因为操作系统可以把暂时不用的页换到磁盘上等用到时再换回来。5.2 用户态与内核态权限是系统稳定的第一道防线如果没有操作系统你的程序可以直接读写磁盘、控制硬件听起来很自由但任何一个程序写错一个参数整个机器可能就崩溃了。操作系统用“特权级”做隔离内核运行在高特权模式能访问硬件、管理所有资源应用程序运行在低特权模式只能做有限的普通操作。想访问硬件怎么办只能通过系统调用程序主动中断进入内核请内核代办“打开文件”“发送网络包”“分配内存”这类操作。这个“敲门”的动作相当于向操作系统提交了一份申请书内核检查完参数、确认权限才去操作硬件最后把结果返回给应用。真实世界里数据链路非常长比如你调用一个读文件的函数先从应用库函数开始进入系统调用内核找到文件对应的数据块发指令给磁盘驱动磁盘把数据搬到内存再拷给应用。这个过程中每个跳转都是有代价的所以“减少系统调用次数”“批量处理数据”一直都是高性能服务优化的核心方向。5.3 进程调度与上下文切换几十个程序“同时运行”的表演一台电脑同时开着浏览器、音乐播放器、聊天软件而 CPU 通常只有几个核怎么做到“同时”操作系统里的调度器把 CPU 时间切成很小的时间片轮流转给每个进程跑。这个切换动作叫上下文切换先把当前进程的寄存器、程序计数器等状态保存起来再恢复下一个进程的状态。上下文切换不是免费的它要保存/恢复一大堆 CPU 状态还要刷新缓存频繁切换性能会劣化得非常厉害。这也是为什么高并发服务器上进程数不是越多越好线程切换开销也会被量化。能少切换就少切换能一次做一大块工作就一次做。理解了调度器在背后“变戏法”你就明白“负载高了为什么 CPU 使用率 100% 但响应很慢”——因为时间都耗在切换上了。6. 中断与总线CPU 如何与纷繁的外设打交道6.1 轮询不如按铃中断解决了“CPU 等设备”的难题如果 CPU 要等键盘输入怎么可能一边等一边干别的最蠢的方法是轮询CPU 反复去问键盘“你有输入吗”但这会占满 CPU什么都干不了。现实中的解法是中断外设有事件时主动给 CPU 发一个电信号CPU 收到后暂停当前任务跳到一个专门的处理函数处理完再回来继续原来的工作。这个设计非常像餐厅里点菜——客人需要服务时按铃服务员才过来而不是服务员站在桌边干等。CPU 手里维护着一张“中断向量表”每个中断号对应一个处理函数地址。键盘、鼠标、网卡、磁盘、定时器都有自己的中断号。操作系统里大量逻辑也依赖中断定时器周期性中断让调度器决定接下来该轮到哪个进程应用发起系统调用时本质上也是一种“软中断”主动把自己陷入内核。6.2 DMA让数据绕过 CPU 直奔内存外设的数据往往是一大块一大块的。比如从磁盘读一个文件如果每一个字都要 CPU 取一下再放到内存CPU 就彻底沦为搬运工没空算数了。解决方案是 DMA直接内存访问CPU 只需要告诉 DMA 控制器“目标磁盘地址、内存地址、要搬多少字节”然后就去干别的DMA 控制器自己完成搬运搬完再发一个中断来通知“我干完了”。这个流程特别好理解CPU 是工程经理DMA 是货运司机。经理把单子开好就可以去忙别的司机严格按单子把货物从仓库搬到指定地点到了再汇报。读大文件快靠的不仅仅是磁盘本身快更重要的是这套“CPU 不亲自搬货只发指令收完成通知”的机制把宝贵的计算时间省了下来。6.3 总线演进从“共用一条马路”到“每家一条专线”外设、内存、CPU 之间靠总线通信。传统总线像一条窄马路所有设备都挤在一条路上谁要用就先“打招呼占路”设备一多带宽大家一起瓜分慢得不行。后来演进到 PCIe 这类点对点串行总线每个设备都有自己的专用通道而且支持多个通道捆绑带宽可以独自享受。显卡这么吃带宽的部件就是从这代总线开始才真正发挥出性能的。你再看看主板上那些长条插槽密密麻麻的金手指本质上就是 CPU 和外部世界通信的高速公路入口。沿着这些总线把整台机器的信息流串起来你敲下的那个a经历的大致路线是键盘产生中断CPU 响应中断读取按键编码操作系统把它放进输入缓冲应用程序从缓冲取出再把它交给图形接口显卡把a的字体位图写入显示内存显示器刷新屏幕像素——几毫秒内完成而你感知为“即时显示”。整个机器从底到上靠的就是电平跳变、逻辑门组合、存储读写、指令执行这四板斧来回运转。我自己把这些原理通通追了一遍之后最大的体会是过去那些“玄学问题”全都变透明了程序崩溃时看栈回溯能联想到页面映射和栈帧布局查询慢了会条件反射去算缓存命中率并发上不去会去数上下文切换次数。计算机从来不是魔法它只是一层一层的工程每一层都在为上一层擦屁股。把这套逻辑骨架放在心里你再回头看任何语言、框架、中间件都会觉得它们不过是这套底层逻辑披上的不同外衣而已。
阅读完成 · 觉得有帮助?