
计算机系统这门学问最怕的就是一上来就扎进寄存器或者指令集里结果学了大半年脑子里还是一堆散装零件拼不出一台能跑的机器。我见过太多人抱着《深入理解计算机系统》啃第一章翻来覆去看了好几遍还是觉得“漫游”这个词太虚——信息、上下文、编译系统、处理器、高速缓存、存储设备、操作系统、进程、线程、虚拟内存、文件、网络通信、并发和并行这些词单拎出来都认识串在一起就不知道在讲什么了。其实第一章的价值恰恰在于它不跟你抠细节而是先给你一张地图。这张地图告诉你你写的那几行代码从键盘敲下去到屏幕上出结果中间到底经过了哪些关卡每一关是谁在干活为什么需要这么多层。我个人的体会是这一章读透了后面章节里那些看似孤立的知识点——比如为什么要有缓存、为什么进程之间不能直接访问对方内存、为什么文件读写有时候快有时候慢——全都能找到根。这篇文章我就按自己的理解把第一章这条线从头到尾捋一遍尽量说人话把每个概念为什么存在、解决什么问题讲清楚。1. 从一段代码到可执行文件编译系统到底在忙什么1.1 信息就是“位上下文”别把它想玄了书里开篇就抛出一个观点信息就是位加上下文。这句话听起来像哲学其实特别实在。计算机里所有东西——你写的代码、存的照片、发的消息——在底层都是一串二进制位也就是0和1。单独看一个0或者一个1没有任何意义。但当你告诉系统“这串位按照ASCII码来解释”它就变成了字符告诉它“按照UTF-8来解释”它可能变成中文告诉它“这是一个32位整数”它就变成一个数值。我刚开始学的时候总觉得“上下文”这个词很虚后来用一个例子就通了同样一串字节01000001如果你把它当无符号整数看它是65如果你把它当ASCII字符看它是大写字母A。位没变变的是解释规则这个解释规则就是上下文。所以书里说文件也好、内存里的数据也好本质上都是字节序列区别只在于我们用什么上下文去解读它。这个认知很重要因为它直接解释了后面很多设计。比如为什么同一个内存地址程序可以把它当整数读也可以当字符读还可以当指令执行——因为硬件层面只认位怎么解释是软件和上下文的事。再比如为什么网络传输的数据要约定协议本质上就是双方约定好用同一套上下文来解释收到的位流否则你发过去的是图片对方按文本解出来的就是乱码。1.2 编译系统的四个阶段预处理、编译、汇编、链接你写一个最简单的C程序比如就打印一行字保存成.c文件。这个文件对人来说是可读的文本但机器不认识。从文本到能跑的可执行文件中间要经过四个阶段书里把它们统称为编译系统。第一个阶段是预处理。预处理器会处理所有以#开头的指令比如#include会把头文件的内容直接插进来#define会把宏替换掉。这一步本质上还是文本操作输出的还是一个C文件只不过所有宏都展开了、头文件都合并了。很多人忽略这一步觉得它不重要但实际上你调试时看到的很多奇怪行号偏移就是预处理展开导致的。第二个阶段是编译。编译器把预处理后的C代码翻译成汇编代码。汇编代码已经是跟具体硬件架构相关的了比如x86和ARM的汇编就不一样。这一步做的是语法分析、语义分析、优化、代码生成。书里强调编译器不仅要翻译还要做优化比如把没用的代码删掉、把循环展开、把变量尽量放在寄存器里。这些优化直接决定了你写的代码跑起来快不快。第三个阶段是汇编。汇编器把汇编代码翻译成机器码打包成可重定位目标文件。这个文件里已经是二进制了但还不能直接跑因为里面有些地址还没确定比如你调用了别的文件里的函数那个函数的地址现在还不知道。第四个阶段是链接。链接器把多个可重定位目标文件和库文件合并成一个可执行文件把那些悬而未决的地址都填上。比如你的程序调用了标准库的printf链接器就会把printf的代码从库里拉过来跟你的代码拼在一起最终生成一个完整的可执行文件。这四个阶段串起来就是一条完整的流水线。我建议每个学计算机的人都亲手跑一遍这条流水线用gcc -E、gcc -S、gcc -c、gcc -o分别看每一步的输出。你会直观地感受到你写的一行printf背后被拆成了多少东西。1.3 为什么要理解编译系统不只是为了考试有人会问我又不当编译器工程师为什么要懂这些我的回答是理解编译系统是为了在出问题的时候知道去哪找。比如你遇到一个“未定义引用”的链接错误如果你知道链接器在干什么你就知道是某个符号没找到可能是库没链、可能是函数名写错了、可能是C和C的名字修饰规则不一样。再比如你遇到一个“段错误”如果你知道可执行文件的内存布局你就知道可能是访问了不该访问的地址。还有一个更实际的理由性能优化。编译器做了很多优化但它的优化是保守的因为它必须保证程序语义不变。如果你懂编译原理你就能写出对编译器友好的代码比如减少别名、用const帮助优化、避免在热循环里做函数调用。这些技巧在写高性能代码时非常关键。2. 处理器、内存和高速缓存数据到底是怎么流动的2.1 处理器读指令执行一个永不停歇的循环处理器的工作模式其实很简单就是一个循环从程序计数器指向的地址读指令解码执行然后更新程序计数器再读下一条。这个循环看起来朴素但现代处理器为了让它跑得快做了极其复杂的优化比如流水线、乱序执行、分支预测。不过书里在第一章没有展开这些它只告诉你最基本的事实处理器只做一件事就是不断地读指令、执行指令。程序计数器是一个寄存器里面存着下一条要执行的指令的地址。处理器每执行完一条指令程序计数器就自动指向下一条。遇到跳转指令程序计数器就被改成跳转目标地址。整个程序的执行本质上就是程序计数器在指令序列里跳来跳去。这里有一个关键点指令和数据在内存里是不区分的都是二进制位。处理器怎么知道它读的是指令还是数据答案是看它从哪个路径读。如果处理器处于取指阶段它就把读到的位当指令解码如果处于执行阶段需要读操作数它就把读到的位当数据。这个设计叫冯·诺依曼结构指令和数据共享同一块内存。它的好处是灵活坏处是可能被恶意利用比如把数据当指令执行这就是很多安全漏洞的根源。2.2 存储设备的层次结构为什么需要高速缓存书里花了不少篇幅讲存储设备的层次结构从寄存器、L1/L2/L3缓存、主存、固态硬盘、机械硬盘一直到网络存储。这个层次结构可以用三个词概括容量越来越大速度越来越慢价格越来越便宜。寄存器最快但容量极小通常只有几十个每个几十字节。L1缓存次之容量几十KB访问延迟几个时钟周期。L2缓存几百KB到几MB延迟十几个周期。L3缓存几MB到几十MB延迟几十个周期。主存几GB到几十GB延迟几百个周期。固态硬盘几百GB到几TB延迟几万到几十万个周期。机械硬盘延迟更高因为要等盘片转。为什么要有这么多层因为程序访问内存的行为有局部性。时间局部性是说你刚访问过的数据很可能马上还会再访问。空间局部性是说你访问了某个地址很可能马上会访问它附近的地址。因为局部性的存在把最近访问过的数据放在更快的存储里就能让大部分访问都落在快的那几层整体速度就上去了。高速缓存就是利用局部性原理在主存和处理器之间加的一层小容量快速存储。它保存最近访问过的内存块的副本。当处理器要读某个地址时先查缓存如果命中就直接返回不用去主存如果不命中就从主存读同时把这一块数据放进缓存以备后用。我个人的经验是理解缓存是理解性能问题的钥匙。你写的程序为什么有时候快有时候慢很多时候就是因为缓存命中率不一样。比如遍历一个二维数组按行遍历和按列遍历速度可能差好几倍因为按行遍历符合空间局部性缓存命中率高按列遍历每次跳一大步缓存命中率低。这个例子书里也提到了非常经典。2.3 缓存命中与不命中一个具体的计算例子假设有一个缓存容量是64字节块大小是16字节那么它能存4个块。现在处理器依次访问地址0、4、8、12、16、20、24、28。地址0到15属于第一个块16到31属于第二个块。访问0时缓存不命中从主存读入第一个块缓存里现在有块0。访问4、8、12时都在块0里全部命中。访问16时不命中读入第二个块。访问20、24、28时都在块1里全部命中。所以8次访问只有2次不命中命中率75%。如果访问模式变成0、16、4、20、8、24、12、28每次访问都跳到不同的块那么每次都不命中命中率0%。这就是为什么访问模式对性能影响巨大。书里还提到了缓存不命中的几种类型冷启动不命中、冲突不命中、容量不命中。冷启动是第一次访问某个块没办法避免。冲突不命中是因为缓存映射策略导致不同块争抢同一个位置。容量不命中是因为缓存太小装不下工作集。理解这些类型有助于你分析程序的性能瓶颈。3. 操作系统硬件之上的第一层抽象3.1 操作系统到底在管什么操作系统是夹在应用程序和硬件之间的一层软件。它的核心任务有两个一是管理硬件资源二是提供抽象接口。管理硬件资源包括管理处理器、内存、磁盘、网络设备等让多个程序能公平、安全地共享这些资源。提供抽象接口包括提供进程、虚拟内存、文件这些概念让程序员不用直接跟硬件打交道。书里把操作系统的功能概括为两个基本目标防止硬件被失控的应用程序滥用以及向应用程序提供一致的接口。这两个目标贯穿整个操作系统设计。比如进程抽象既让每个程序以为自己独占处理器又防止一个程序把另一个程序搞崩。虚拟内存抽象既让每个程序以为自己独占内存又防止程序访问别人的数据。文件抽象既让程序不用关心磁盘的物理细节又提供了统一的读写接口。我个人的理解是操作系统的所有设计几乎都可以归结为“隔离”和“抽象”两个词。隔离是为了安全抽象是为了方便。理解了这两个词再看进程、虚拟内存、文件这些概念就不会觉得它们是凭空冒出来的。3.2 进程与线程程序运行时的两种视角进程是操作系统对一个正在运行的程序的抽象。它给程序一种假象好像自己独占处理器和内存。实际上处理器在多个进程之间快速切换每个进程只运行一小段时间然后被换出去另一个进程被换进来。因为切换速度极快用户感觉不到就以为程序一直在跑。进程有自己的地址空间、寄存器状态、程序计数器、栈、堆、数据段、代码段。操作系统通过上下文切换来保存和恢复这些状态。上下文切换的开销不小因为要保存寄存器、刷新缓存、切换页表。所以高性能服务器通常要减少上下文切换比如用线程池、用异步IO。线程是进程内部的执行单元。一个进程可以有多个线程它们共享同一个地址空间和大部分资源但各自有独立的程序计数器、寄存器、栈。线程切换比进程切换轻量因为不用切换地址空间。但线程之间共享内存也带来了同步问题需要锁、信号量、条件变量来协调。书里在第一章只是简单提了并发和并行没有深入线程细节。但我觉得有必要在这里把进程和线程的关系说清楚因为后面章节会反复用到。进程是资源分配的单位线程是调度的单位。这句话是理解两者的关键。3.3 虚拟内存每个程序都以为自己独占内存虚拟内存是操作系统提供的一个极其重要的抽象。它让每个进程都以为自己拥有一块连续的大内存从某个地址开始一直到某个地址结束。实际上这块内存被映射到物理内存的各个碎片上甚至有一部分被换到磁盘上。虚拟内存的好处有三个一是简化内存管理程序不用关心物理内存的布局二是提供隔离一个进程不能直接访问另一个进程的内存三是支持按需加载程序不用一次性全部载入内存用到哪部分才加载哪部分。虚拟内存的实现依赖页表和MMU。页表记录虚拟地址到物理地址的映射MMU负责在每次内存访问时做地址翻译。为了加速翻译又引入了TLB缓存最近用过的页表项。这一套机制层层嵌套但核心思想很简单用一层间接映射换来灵活性和安全性。我踩过的一个坑是写程序时以为内存是连续的结果在大数组上做指针运算越界了也没报错因为虚拟内存把越界访问映射到了别的页。后来用工具检查才发现问题。所以理解虚拟内存对调试内存相关bug很有帮助。4. 文件、网络与并发系统与外部世界的接口4.1 文件一切皆文件的哲学在Unix/Linux系统里文件是一个核心抽象。它不仅仅指磁盘上的文本或二进制文件还包括设备、管道、套接字等。操作系统把很多资源都抽象成文件提供统一的打开、读、写、关闭接口。这种“一切皆文件”的设计让程序可以用同一套API操作各种资源极大简化了编程。文件本质上是一个字节序列。操作系统负责把字节序列映射到磁盘块处理缓冲、缓存、权限、并发访问。应用程序只需要调用open、read、write、close这些系统调用不用关心磁盘的物理结构。书里在第一章提到文件是为了说明操作系统如何把磁盘这种硬件抽象成程序员友好的接口。我个人的体会是理解文件抽象是理解Unix编程风格的关键。很多工具之所以能组合起来用就是因为它们都遵循文件接口输入输出都是字节流。4.2 网络通信把文件抽象延伸到网络网络通信在Unix里也被抽象成文件。一个网络连接就是一个套接字你可以像读写文件一样读写套接字。发送数据就是往套接字写接收数据就是从套接字读。这种设计让网络编程和文件编程统一起来降低了学习成本。当然网络通信比本地文件复杂得多因为要处理协议、分包、重传、拥塞控制。但操作系统和网络协议栈把这些复杂性都封装起来了应用程序看到的还是一个字节流。书里在第一章只是简单提了网络但我觉得有必要强调网络通信的本质是两个进程通过操作系统提供的接口交换字节流。理解了这一点再看HTTP、RPC这些协议就只是字节流之上的约定而已。4.3 并发和并行两个容易混淆的概念书里最后提到了并发和并行。这两个词经常被混用但严格来说有区别。并发是指多个任务在时间上重叠但不一定同时执行。比如单核处理器上多个进程轮流执行宏观上看是同时进行微观上是交替的。并行是指多个任务真正同时执行需要多核处理器或者多台机器。并发和并行的核心挑战都是共享状态的管理。多个执行流同时访问同一块内存如果不加控制就会产生竞态条件结果不可预测。解决方法是同步机制比如锁、原子操作、无锁数据结构。但同步本身有开销还可能引入死锁、活锁、优先级反转等问题。我个人的经验是并发编程的难点不在于写代码而在于推理。你要能证明在任何执行顺序下程序的行为都是正确的。这需要经验也需要工具比如模型检查、压力测试、竞态检测器。书里后面章节会详细讲并发第一章只是开个头让你知道这个概念的存在。5. 把第一章串起来一条完整的数据流5.1 从键盘输入到屏幕输出走一遍全流程现在我们把第一章的概念串起来走一遍完整流程。假设你在终端里运行一个程序输入一个数字程序计算后输出结果。第一步你在键盘上敲下字符。键盘产生电信号经过键盘控制器转换成扫描码再经过中断控制器通知处理器。处理器收到中断保存当前进程的上下文切换到键盘中断处理程序。中断处理程序从键盘控制器读取扫描码转换成字符放进输入缓冲区。第二步你的程序正在等待输入它调用read系统调用陷入内核。内核检查输入缓冲区如果有数据就复制到用户空间返回如果没有就阻塞把进程状态设为等待调度其他进程运行。第三步数据到达用户空间后程序开始计算。处理器从内存读指令和数据经过缓存层次执行算术运算。计算结果先放在寄存器然后写回内存。第四步程序调用write系统调用把结果写到标准输出。内核把数据从用户空间复制到内核缓冲区然后交给终端驱动程序。终端驱动程序把字符转换成像素写入显存显示器刷新你看到结果。这一整条链路涉及中断、上下文切换、系统调用、缓存、内存管理、设备驱动。每一个环节都是第一章提到的概念。把这条链路走通你就真正理解了“计算机系统漫游”的含义。5.2 为什么这些概念要放在第一章很多人觉得第一章太浅都是概念没有干货。但我的看法相反第一章是全书的地图没有地图后面章节就是迷宫。比如后面讲链接如果你不知道编译系统有四个阶段你就不知道链接器在流水线的哪个位置。后面讲缓存如果你不知道存储层次结构你就不知道为什么要有缓存。后面讲进程如果你不知道操作系统提供抽象你就不知道为什么需要进程。书里把这些概念放在第一章不是让你一次学透而是让你先建立一个框架。后面每学一个细节就往这个框架里填。填到最后框架和细节融为一体你就真正理解了计算机系统。5.3 我建议的学习路径如果你正在学这本书我的建议是第一章至少读三遍。第一遍快速过知道有哪些概念。第二遍慢读把每个概念的定义和动机搞清楚。第三遍合上书自己画一张图把信息、上下文、编译系统、处理器、缓存、存储、操作系统、进程、线程、虚拟内存、文件、网络、并发这些概念串起来。画不出来就回去再读。然后动手做实验。用gcc跑一遍编译流水线用objdump看可执行文件的汇编用strace看系统调用用perf看缓存命中率。这些工具能让你直观感受到书里讲的东西。光看书不动手等于没学。最后不要怕忘。这些概念不是背下来的是用出来的。你在写代码、调性能、查bug的过程中会反复用到它们。用多了自然就记住了。我到现在也不敢说完全理解了这本书但每次重读都有新收获。这大概就是经典教材的魅力。提示第一章的概念密度很高第一遍读不懂很正常。不要停下来死磕先往后读读到具体章节时再回头翻第一章往往会有豁然开朗的感觉。5.4 一个容易被忽略的细节Amdahl定律的启示书里在讲并发和并行时提到了Amdahl定律。这个定律说对系统某一部分加速整体加速比受限于该部分所占的比例。比如一个程序串行部分占20%并行部分占80%那么即使你把并行部分加速到无穷大整体加速比最多也只有5倍。这个定律的启示是优化要抓大头。如果你花大量时间优化一个只占5%时间的函数整体性能提升微乎其微。相反如果你优化一个占50%时间的函数即使只提升一倍整体也能提升33%。我在实际工作中见过太多人把时间花在无关紧要的微优化上结果收益甚微。先测量再优化这是性能优化的第一原则。Amdahl定律还解释了为什么多核处理器不能无限提升性能。因为程序中总有串行部分串行部分的比例决定了加速的上限。所以现代处理器设计不仅要增加核心数还要减少串行瓶颈比如用更快的互连、更大的缓存、更智能的调度。5.5 从第一章看计算机系统的设计哲学把第一章的概念放在一起看能发现几条贯穿全书的设计哲学。第一条是抽象。计算机系统是一层一层抽象堆起来的。硬件抽象成指令集指令集抽象成操作系统操作系统抽象成进程和文件进程和文件抽象成应用程序。每一层都隐藏下层的复杂性提供更友好的接口。理解抽象就理解了计算机系统的组织方式。第二条是缓存。缓存思想无处不在CPU缓存、TLB、磁盘缓存、浏览器缓存、CDN缓存。核心思想都是利用局部性把常用的数据放在快的地方。理解缓存就理解了性能优化的核心。第三条是隔离。进程之间隔离虚拟内存隔离文件权限隔离。隔离是为了安全和稳定。理解隔离就理解了操作系统为什么这么设计。第四条是权衡。没有完美的设计只有权衡。缓存大速度快但贵缓存小便宜但慢。进程隔离安全但切换开销大线程共享方便但同步复杂。理解权衡就理解了工程决策背后的逻辑。这四条哲学贯穿整本书。第一章只是把它们摆出来后面章节会反复验证。我个人的体会是学计算机系统学的不是知识点而是思维方式。知识点会忘思维方式会留下来成为你分析问题、解决问题的本能。