《深入理解计算机系统(原书第3版)》前言
date
Aug 8, 2022
slug
computer-systems-a-programmer’s-perspective-perface
status
Published
tags
Computer Systems: A Programmer's Perspective
summary
一本书最终要的部分是前言,其次是目录。
type
Post
Created Time
Oct 28, 2023 01:45 PM
Updated Time
Oct 28, 2023 01:45 PM
AI summary
Status
本书(简称 CS:APP)的主要读者是计算机科学家、计算机工程师,以及那些想通过学习计算机系统的内在运作而能够写出更好程序的人。
我们的目的是解释所有计算机系统的本质概念,并向你展示这些概念是如何实实在在地影响应用程序的正确性、性能和实用性的。其他的系统类书籍都是从构建者的角度来写的,讲述如何实现硬件或系统软件,包括操作系统、编译器和网络接口。而这本书是从程序员的角度来写的,讲述应用程序员如何能够利用系统知识来编写出更好的程序。当然,学习一个计算机系统应该做些什么,是学习如何构建一个计算机系统的很好的出发点,所以,对于希望继续学习系统软硬件实现的人来说,本书也是一个很有价值的介绍性读物。大多数系统书籍还倾向于重点关注系统的某一个方面,比如:硬件架构、操作系统、编译器或者网络。本书则以程序员的视角统一覆盖了上述所有方面的内容。
如果你研究和领会了这本书里的概念,你将开始成为极少数的“牛人”,这些“牛人”知道事情是如何运作的,也知道当事情出现故障时如何修复。你写的程序将能够更好地利用操作系统和系统软件提供的功能,对各种操作条件和运行时参数都能正确操作,运行起来更快,并能避免出现使程序容易受到网络攻击的缺陷。同时,你也要做好更深入的准备,研究像编译器、计算机体系结构,操作系统、嵌入式系统、网络互联和网络安全这样的高级题目。
读者应具备的背景知识
本书的重点是执行 x86-64 机器代码的系统。对英特尔及其竞争对手而言,x86-64 是他们自 1978 年起,以 8086 微处理器为代表,不断进化的最新成果。按照英特尔微处理器产品线的命名规则,这类微处理器俗称为“x86”。随着半导体技术的演进,单芯片上集成了更多的晶体管,这些处理器的计算能力和内存容量有了很大的增常。在这个过程中,它们从处理 16 位字,发展到引入 IA32 处理器处理 32 位字,再到最近的 x86-64 处理 64 位字。
我们考虑的是这些机器如何在 Linux 操作系统上运行 C 语言程序。Linux 众多继承自最初由贝尔实验室开发的 Unix 的操作系统中的一种,这类操作系统的其他成员包括 Solaris、FreeBSD 和 MacOS X。近年来,由于 Posix 和标准 Unix 规范的标准化努力,这些操作系统保持了高度兼容性。因此,本书内容几乎直接适用于这些“类 Unix”操作系统。
- 文中包含大量已在 Linux 系统上编译和运行过的程序示例,为了更好的学习,你需要拥有了一个“类 Unix”的操作系统,并拥有一定的权限。
- 文中的代码都是由 C 编写的,因此,你需要对 C 和 C++ 有一定的了解。所幸的是,C 是一个较小的语言,在 Brian Kernighan 和 Dennis Ritchie 经典的 “K&R” 文献中得到了清晰优美的描述。
本书的前几章揭示了 C 语言程序和它们相对应的机器语言程序之间的交互作用。机器语言示例都是用运行在 x86-64 处理器上的 GUN GCC 编译器生成的。不需要你以前有任何硬件、机器语言或是汇编语言编程的经验。
如何阅读此书
从程序员的角度学习计算机系统是如何工作的会非常有趣,主要是因为你可以主动地做这件事情。无论何时你学到一些新的东西,都可以马上实验并且直接看到运行结果。事实上,我们相信学习系统的唯一办法就是做(do)系统,即在真正的系统上解决具体的问题,或是编写和运行程序。
这个主题贯穿全书。当引入一个新的概念时,将会有一个或多个练习题紧随其后,你应该马上做一做来检查你的理解。这些练习题的解答在每章的末尾。当你阅读时,尝试自己来解答每个问题,然后在查阅答案,看自己的答案是否正确。对每个家庭作业题,都标注了难度级别:
- * 只需要几分钟,几乎或完全不需要编程。
- ** 可能需要将近 20 分钟。通常包括编写和测试一些代码。(许多都源自考试中的题目)
- *** 需要很大的努力,也许是 1~2 个小时。一般包括编写和测试大量的代码。
- **** 一个实验作业,需要将近 10 个小时。
文中每段代码示例都是由经过 GCC 编译的 C 程序直接生成并在 Linux 系统上进行了测试,没有任何人为改动。如果系统上的 GCC 版本不同,或者根本就是另外一种编译器,那么可能生成不一样的机器代码,但是整体行为表现应该是一样的。所有的源程序代码都可以从 csapp.cs.cmu.edu 上的 CS:APP 主页上获取。
为了避免本书内容过多,本书中添加了许多网络旁注(Web aside),包括一些对本书主要内容的补充资料。所有的网络旁注都可以从 CS:APP 的主页上获取。
本书概述
本书由 12 章组成,旨在阐述计算机系统的核心概念。内容概述如下:
- 第 1 章:计算机系统漫游。这一章通过研究 “hello, world” 这个简单的程序的生命周期,介绍计算机系统的主要概念和主题。
- 第 2 章:信息的表示和处理。本章
- 讲述了计算机的算术运算,重点描述了会对程序员有影响的无符号数和数的补码表示的特性;
- 考虑数字是如何表示的,以及由此确定对于一个给定的字长,其可能编码值的范围;
- 探讨有符号和无符号数字之间类型转换的效果,阐述算术运算的数学特性。菜鸟级程序员经常很惊奇地了解到(用补码表示的)两个正数的和或者积可能为负。另一方面,补码的算术运算满足很多整数运算的代数特性,因此,编译器可以很安全地把一个常量乘法转化为一系列的移位和加法。
- 用 C 语言的位级操作来说明布尔运算的原理和应用。
- 从两个方面讲述了 IEEE 标准的浮点格式:一是如何用它来表述数值,一是浮点运算的数学属性。
对计算机的算术运算有深刻的理解是写出可靠程序的关键。比如,程序员和编译器不能用表达式(x-y<0)来替代(x<y),因为前者可能会产生溢出。甚至也不能用表达式(-y< -x)来替代,因为在补码表示中负数和正数的范围是不对称的。算术溢出是造成程序错误和安全漏洞的一个常见根源,然而很少有书从程序员的角度来讲述计算机算术运算的特性。读者在阅读本章时,可以先略过数学细节,获得高层次的总体概念,然后回过头来进行更细致深入的阅读。
- 第 3 章:程序的机器级表示。这一章会教读者如何阅读由 C 编译器生成的 x86-64 机器代码。进一步
- 说明不同控制结构(比如条件、循环和开关语句)生成的基本指令模式;
- 讲述过程的实现,比如栈分配,寄存器使用惯例和参数传递;
- 讨论不同数据结构(如结构、联合和数组)的分配和访问方式;
- 说明实现整数和浮点数算术运算的指令;
- 分析程序在机器级的样子作为途径,来理解常见的代码安全漏洞(例如缓冲区溢出),进而理解程序员、编译器和操作系统可以采取的减轻这些威胁的措施;
学习本章的概念能够帮助读者成为更好的程序员,因为你们懂得程序在机器上是如何表示的。另外一个好处就在于读者会对指针有非常全面而具体的理解。
- 第 4 章:处理器体系结构。本章
- 讲述基本的组合和时序逻辑元素,并展示这些元素如何在数据通路中组合到一起,来执行 x86-64 指令集的一个称为 “Y86-64” 的简化子集;
- 从设计单时钟周期数据通路开始(这个设计概念上非常简单,但是运行速度不会太快),然后引入流水线的思想,将处理一条指令所需要的不同步骤实现为独立的阶段。在这个设计中,在任何时刻,每个阶段都可以处理不同的指令。你会看到五阶段处理器流水线更加实用;
- 用一种称为 HCL 的简单硬件描述语言来描述处理器设计的控制逻辑。用 HCL 写的硬件设计能够编译和链接到本书提供的模拟器中,还可以根据这些设计生成 Verilog 描述,它适合合成到实际可以运行的硬件上去;
- 第 5 章:优化程序性能。本章
- 介绍了许多提高代码性能的技术,主要思想就是让程序员通过使编译器能够生成更有效的机器代码来学习编写 C 代码。
- 介绍了减少程序需要的做的工作交换(这些是在任何机器上写程序时都应该遵循的);
- 介绍了增加生成的机器代码中指令级并行度的交换,因而提高了程序在现代“超标量”处理器上的性能;
- 介绍了一个简单的操作模型,描述了现代乱序处理器是如何工作的,解释了机器代码中指令级并行度交换的原理;
- 根据一个程序的图形化表示中的关键路径来测量一个程序可能的性能;
你会惊讶于对 C 代码做一些简单的变换能给程序带来多大的速度提升。
- 第 6 章:存储器层次结构。对应用程序员来说,存储器系统是计算机系统中最直接可见的部分之一。本章
- 介绍存储设备组成的层次结构,不同层次的存储设备有不同的容量、造价和访问时间;
- 讲述不同类型的随机存取存储器(RAM)和只读存储器(ROM),以及磁盘和固态硬盘的几何形状和组织构造;
- 描述存储设备是如何放置在层次结构中的,讲述访问局部性是如何使这种层次结构成为可能的;
- 通过一个独特的观点将理论具体化:将存储器系统视为一个“存储器山”,山脊是时间局部性,而斜坡是空间局部性;
- 向读者阐述如何通过改善程序的时间局部性和空间局部性来提高应用程序的性能。
- 第 7 章:链接。本章讲述静态和动态链接,包括的概念有可重定位的和可执行的目标文件、符号解析、重定位、静态库、共享目标库、位置无关代码,以及库打桩。大多数讲述系统的书中都不讲链接,讲述它是出于以下两个原因:第一,程序员遇到的最令人迷惑的问题中,有一些和链接时的小故障有关,尤其是对那些大型软件包来说。第二,链接器生成的目标文件是与一些像加载、虚拟内存和内存映射这样的概念相关的。
- 第 8 章:异常控制流。本章
- 通过介绍异常控制流(即除正常分支和过程调用以外的控制流的变化)的一般概念,打破单一程序的模型;
- 给出存在于系统所有层次的异常控制流的例子,从底层的硬件和异常和中断,到并发进程的上下文切换,到由于接收 Linux 信号引起的控制流突变,到 C 语言中破坏栈原则的非本地跳转;
- 介绍进程的基本概念,进程是对一个正在执行的程序的一种抽象;
- 学习进程是如何工作的,以及如何在应用程序中创建和操作进程;
- 展示应用程序员如何通过 Linux 系统调用来使用多个进程,编写带作业控制的 Linux shell;
- 向读者初步展示程序的并发执行会引起不确定的行为;
- 第 9 章:虚拟内存。本章:
- 讲述虚拟内存系统,使读者对它是如何工作的以及它的特性有所了解;
- 让读者了解为什么不同的并发进程各自有一个完全相同的地址范围,能共享某些页,而又独占另外一些页;
- 讲述了一些管理和操作虚拟内存的问题。特别地,讨论了存储分配操作,就像标准库的 malloc 和 free 操作;
- 加强了这样一个概念,那就是虚拟内存空间只是字节数组,程序可以把它划分成不同的存储单元;
- 帮助读者理解当程序包含存储泄漏和非法指针引用等内存引用错误时的后果;
- 许多应用程序员编写自己的优化了的存储分配操作来满足应用程序的需要和特性;
阐述这些内容是出于下面几个目的:
这一章比其他任何一章都更能展示将计算机系统中的硬件和软件结合起来阐述的优点,而传统的计算机体系结构和操作系统数据都只讲述虚拟内存的某一方面。
- 第 10 章:系统级 I/O。本章:
- 讲述了 Unix I/O 的基本概念,例如文件和描述符;
- 描述如何共享文件,I/O 重定向是如何工作的,还有如何访问文件的元数据;
- 开发了一个健壮的带缓冲区的 I/O 包,可以正确处理一种称为 short counts 的奇特行为,也就是库函数只读取一部分的输入数据;
- 阐述 C 的标准 I/O 库,以及它与 Linux I/O 的关系,重点谈到标准 I/O 的局限性,这些局限性使之不适合网络编程
总的来说,本章的主题是后面两章——网络和并发编程的基础。
- 第 11 章:网络编程。对编程而言,网络是非常有趣的 I/O 设备,它将许多我们学习的概念(比如进程、信号、字节顺序、内存映射和动态内存分配)联系在一起。网络程序还为下一章的主题——并发,提供了一个很令人信服的上下文。文章只是网络编程中一个很小的部分,使读者能够编写一个简单的 Web 服务器。这里还讲述了位于所有网络程序底层的客户端——服务器模型。展现了一个程序员对 Internet 的观点,并且教读者如何用套接字接口来编写 Internet 客户端和服务器,最后,介绍了超文本传输协议(HTTP),并开发了一个迭代式 Web 服务器。
- 第 12 章:并发编程。这一章以 Internet 服务器设计为例介绍了并发编程。
- 比较对着了三种编写并发编程的基本机制(进程、I/O 多路复用和线程),并且展示如何用它们来构造并发 Internet 服务器。
- 探讨了用 P、V 信号量操作来实现同步、线程安全和可重入、竞争条件以及死锁等的基本原则。对大多数服务器应用来说,写并发代码都是很关键的。
- 讲述了线程级编程的使用方法,用这种方法来表达应用程序中的并行性,使得程序在多核处理器上能执行得更快。
使用所有的核解决同一个计算问题需要很小心谨慎地协调并发线程,既要保证正确性,又要争取获得高性能。
一些有趣的实验
- 数据实验。这个实验要求实现简单的逻辑和算术运算函数,但是只能使用一个非常有限的 C 语言子集。比如,只能用位级操作来计算一个数字的绝对值。这个实验可帮助你了解 C 语言数据类型的位级表示,以及数据操作的位级行为。
- 二进制炸弹实验。二进制炸弹是一个作为目标代码文件提供给你的程序。运行时,它提示用户输入 6 个不同的字符串。如果其中的任何一个不正确,炸弹就会“爆炸",打印出一条错误消息,并且在一个打分服务器上记录事件日志。你必须通过对程序反汇编和逆向工程来测定应该是哪 6 个串,从而解除各自炸弹的雷管。该实验能教会你理解汇编语言,并且强制你学习怎样使用调试器。
- 缓冲区溢出实验。它要求你通过利用一个缓冲区溢出漏洞,来修改一个二进制可执行文件的运行时行为。这个实验可教会你栈的原理,并让他们了解写那种易于遭受缓冲区溢出攻击的代码的危险性。
- 体系结构实验。第 4 章的几个家庭作业能够组合成一个实验作业,在实验中,你修改处理器的 HCL 描述,增加新的指令,修改分支预测策略,或者增加、删除旁路路径和寄存器端口。修改后的处理器能够被模拟,并通过运行自动化测试检测出大多数可能的错误。这个实验使你能够体验处理器设计中令人激动的部分,而不需要掌握逻辑设计和硬件描述语言的完整知识。
- 性能实验。你必须优化应用程序的核心函数(比如卷积积分或矩阵转置)的性能。这个实验可非常清晰地表明高速缓存的特性,并带给你低级程序优化的经验。
- cache 实验。这个实验类似千性能实验,你编写一个通用高速缓存模拟器,并优化小型矩阵转置核心函数,以最小化对模拟的高速缓存的不命中次数。使用 Valgrind 为矩阵转置核心函数生成真实的地址访问记录。
- shell 实验。你需要实现自己的带有作业控制的 Unix shell 程序,包括 Ctrl + C 和 Ctrl+Z 按键, fg、bg、jobs 命令。这个过程你能够对 Unix 的进程控制、信号和信号处理有清晰的了解。
- malloc 实验。你需要实现自己的 malloc、free、realloc( 可选)版本。这个实验可让你清晰地理解数据的布局和组织,并且要求评估时间和空间效率的各种权衡及折中。
- 代理实验。你需要实现一个位于浏览器和万维网其他部分之间的并行 Web 代理。这个实验向你揭示了 Web 客户端和服务器这样的主题,并且把课程中的许多概念联系起来,比如字节排序、文件 、进程控制、信号、信号处理、内存映射、套接字和并发。你会很高兴能够看到他们的程序在真实的 Web 浏览器和 Web 服务器之间起到的作用。