序言

Table of Contents

本书(简称 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” 操作系统。

我们假设你能访问一台这样的机器,对 C 和 C++ 有一定的了解。

如果你以前只有 Java 经验,那么你需要付出更多的努力来完成这种转换,不过我们也会帮助你。Java 和 C 有相似的语法和控制语句,不过,有一些 C 语言的特性(特别是指针、显式的动态内存分配和格式化 I/O)在 Java 中都是没有的。所幸的是,C 是一个较小的语言,在 Brian Kernighan 和 Dennis Ritchie 经典的 “K&R” 文献中得到了清晰优美的描述。

如果你只有使用解释性语言经验,如 Python 、Ruby 或 Perl ,那么在使用本书这前,需要花费一些时间来学习 C 。

本书的前几章提示了 C 语言程序和它们相对应的机器语言程序之间的交互作用。机器 语言示例都是用运行在 x86-64 处理器上的 GNU GCC 编译器上生成的。我们不需要你以前有任何硬件、机器语言或是汇编语言编程的经验。

如何阅读此书

从程序员的角度学习计算机系统是如何工作的会非常有趣,主要是因为你可以主动地做这件事情。无论何时你学到的一些新的东西,都可以马上实验并且直接看到运行结果。事实上,我们相信学习系统的唯一方法就是 做(do) 系统,即在真正的系统 上解决具体的问题,或是编写和运行程序。

这个主题观念贯穿全书。当引入一个新概念时,将会有一个或多个练习题紧随其后,你应该马上做一做来检验你的理解。这些练习题的解答在每章的末尾。当你阅读时,尝试自己来解答每个问题,然后再查阅答案,看自己的答案是否正确。除第一章外,每章后面都有难度不同的家庭作业。对每个家庭作业题,我们标注了难度等级:

\*     只需要几分钟。几乎或完全不需要编程
\**    只能需要将近 20 分钟。通常包括编写和测试一些代码;
\***   需要很大的努力,也许是 1~2 个小时,一般包括编写和测试大量的代码;
\****  一个实验作业,需要近 10 个小时。

文中每段代码示例都是由经过 GCC 编译的 C 程序直接生成并在 Linux 系统上进行了测试,没有任何人为的改动。所有的源程序代码都可以从 csapp.cs.cmu.edu 上的 CS:APP 主页上获取。

什么是旁注?

旁注是附加说明,能使你对当前讨论的主题多一些了解。旁注可以有很多用处,一些是小的历史故事,例如, C 语言、Linux 和 Internet 是从何而来的?有些旁注则是用来澄清学生们经常感到疑惑的问题,例如,高速缓存的行、组和块有什么区别?还有新旁注给出了一些现实世界的例子,例如,一个浮点错误怎么毁掉了法国的一枚火箭,或是给出市面上出售的一个磁盘驱动器的几何和运行参数。最后,还有一些旁注仅仅就是上些有趣的内容,例如,什么是“hoinky”?

本书概述

本书的第 1 版于 2003 年出版,第 2 版在 2011 年出版。

考虑到计算机技术发展如此迅速,这本书的内容还算是保持得很好。事实证明 Intel x86 的机器上运行 Linux(以及相关操作系统),加上采用 C 语言编程,是一种能够涵盖当今许多系统的组合。然而,硬件技术、编译器和程序库接口的变化,以及很多教师教授这些内容的经验,都促使我们做了大量的修改。

第 2 版以来的最大整体变化是,我们的介绍从以 IA32 和 x86-64 为基础,转变为完全以 x86-64 为基础。这种重心的转移影响了很多章节的内容。后面我们会列出一些明显的变化。

本书由 12 章组成,旨在阐述计算机系统的核心概念。内容概述如下:

第 1 章:计算机系统漫游

这一章通过研究 “hello, world” 这个简单程序的生命同期,介绍计算机系统的主要概念和主题。

我们将第 5 章对 Amdahl 定理的讨论移到了本章。

第 2 章:信息的表示和处理

我们讲述了计算机的算术运算,重点描述了会对程序员有影响的无符号数和数的补码表示的特性。

我们考虑数字是如何表示的,以及由此确定对于一个给定的字长,其可能编码值的范围。我们探讨有符号和无符号数字之间类型转换的效果,还阐述算术运算的数学特性。

菜鸟级程序员经常很惊奇地了解到(用补码表示的)两个正数的和或者积可能为负。另一方面,补码的算术运算满足很多整数运算的代数特性,因此,编译器可以很安全地把一个常量乘法转化为一系列的移位和加法。

我们用 C 语言的位级操作来说明布尔代数的原理和应用。

我们从两个方面讲述了 IEEE 标准的浮点格式:一是如何用它来表示数值,一是浮点运算的数学属性。

对计算机的算术运算有深刻的理解是写出可靠程序的关键。

比如,程序员和编译器不能用表达式( x-y<0 )来替代( x<y ),因为前者可能会产生溢出。甚至也不能用表达式( -y<-x )来替代,因为在补码表示中负数和正数的范围是不对称的。 算术溢出是造成程序错误和安全漏洞的一个常见根源 ,然而很少有书从程序员的角度来讲述计算机算术运算的特性。

读者和评论家的反馈是一致的,本章的一些内容有点令人不知所措。因此,我们澄清了一些知识点,用更加数学的方式来描述,使得这些内容更容易理解。这使得读者能先略过数学细节,获得高层次的总体概念,然后回过头来进行更细致深入的阅读。

第 3 章:程序的机器级表示

我们教读者如何阅读由 C 编译器生成的 x86-64 机器代码。

  • 我们说明为不同控制结构(比如条件、循环和开关语句)生成的基本指令模式;
  • 我们还讲述过程的实现,包括栈分配、寄存器使用惯例和参数传递;
  • 我们讨论不同数据结构(如结构、联合和数组)的分配和访问方式;
  • 我们还说明实现整数和浮点数算术运算的指令;
  • 我们还以分析程序在机器级的样子作为途径,来理解常见的代码安全漏洞(例如缓冲区溢出),以及理解程序员、编译器和操作系统可以采取的减轻这些威胁的措施。

学习本章的概念能够帮助读者成为更好的程序员,因为你们懂得程序在机器上是如何表示的,另外一个好处就在于读者会对指针有非常全面而具体的理解。

我们将之前基于 IA32 和 x86-64 的表现形式转换为完全基于 x86-64 ,还更新了近期版本 GCC 产生的代码。其结果是大量的重写工作,包括修改了一些概念提出的顺序。同时,我们还首次介绍了对处理浮点数据的程序的机器级支持。由于历史原因,我们给出了一个网络旁注描述 IA32 机器码。

第 4 章:处理器体系结构

这一章讲述基本的组合和时序逻辑元素,并展示这些元素如何在数据通路中组合到一起,来执行 x86-64 指令集的一个称为 “Y86-64” 的简化子集。

我们从设计单时钟周期数据通路开始。这个设计概念上非常简单,但是运行速度不会太快。然后我们引入 流水线 的思想,将处理一条指令所需要的不同步骤实现为独立的阶段。这个设计中,在任何时刻,每个阶段都可以处理不同的指令。我们的五阶段处理器流水线更加实用。

本章中处理器设计的控制逻辑是用一种称为 HCL 的简单硬件描述语言来描述的。用 HCL 写的硬件设计能够编译和链接到本书提供的模拟器中,还可以根据这些设计生成 Verilog 描述,它适合合成到实际可以运行的硬件上去。

我们将之前基于 32 位架构的处理器设计修改为支持 64 位字的操作的设计。

第 5 章:优化程序性能

在这一章里,我们介绍了许多提高代码性能的技术,主要思想就是让程序员通过使编译器能够生成更有效的机器代码来学习编写 C 代码。

我们一开始介绍的是减少程序需要做的工作的变换,这些是在任何机器上写任何程序时都应该遵循的。然后讲的是增加生成的机器代码中指令级并行度的变换,因而提高了程序在现代“超标量”处理器上的性能。

为了解释这些变换行之有效的原理,我们介绍了一个简单的操作模型,它描述了现代乱序处理器是如何工作的,然后给出了如何根据一个程序的图形化表示中的关键路径来测量一个程序可能的性能。

你会惊讶于对 C 代码做一些简单的变换能给程序带来多大的速度提升。

我们更新了内容以反映最近几代 x86-64 处理器的性能。通过引入更多的功能单元和更复杂的控制逻辑,我们开发的基于程序数据流表示的程序性能模型,其性能预测变得比之前更加可靠。

第 6 章:存储器层次结构

对应用程序员来说,存储器系统是计算机系统中最直接可见的部分之一。

到目前为止,读者一直认同这样一个存储器系统概念模型,认为它是一个有一致访问时间的线性数组。实际上,存储器系统是一个由不同容量、造价和访问时间的存储设备组成的层次结构。

我们讲述不同类型的随机存取存储器(RAM)和只读存储器(ROM),以及磁盘和固态硬盘的几何形状和组织构造。我们描述这些存储设备是如何放置在层次结构中的,讲述访问局部性是如何使这种层次结构成为可能的。

我们通过一个独特的观点使这些理论具体化,那就是将存储器系统视为一个“存储器山”,山脊是时间局部性,而斜坡是空间局部性。

最后,我们向读者阐述如何通过改善程序的时间局部性和空间局部性来提高应用程序的性能。

我们对内容进行了更新,以反映更多的近期技术。

第 7 章:链接

本章讲述静态和动态链接,包括的概念有可重定位的和可执行的目标文件、符号解析、重定位、静态库、共享目标库、位置无关代码,以及库打桩。

大多数讲述系统的书中都不讲链接,我们要讲述它是出于以下原因。

第一,程序员遇到的最令人迷惑的问题中,有一些和链接时的小故障有关,尤其是对那些大型软件包来说;第二,链接器生成的目标文件是与一些像加载、虚拟内存和内存映射这样的概念相关的。

针对 x86-64 ,我们重写了本章,扩充了关于用 GOT 和 PLT 创建位置无关代码的讨论,新增了一节描述更加强大的链接技术,比如 库打桩

第 8 章:异常控制流

在本书的这个部分,我们通过介绍异常控制流(即除正常分支和过程调用以外的控制流的变化)的一般概念,打破单一程序的模型。我们给出存在于系统所有层次的异常控制流的例子,从底层的硬件异常和中断,到并发进程的上下文切换,到由于接收 Linux 信号引起的控制流突变,到 C 语言中破坏栈原则的非本地跳转。

在这一章,我们介绍 进程 的基本概念,进程是对一个正在执行的程序的一种抽象。

读者会学习进程是如何工作的,以及如何在应用程序中创建和操纵进程。我们会展示应用程序员如何通过 Linux 系统调用来使用多个进程。

学完本章之后,读者就能够编写带作业控制的 Linux shell 了。同时,这里也会向读者初步展示程序的并发执行会引起不确定的行为。

我们增加了对信号处理程序更细致的描述,包括异步信号安全的函数,编写信号处理程序的具体指导原则,以及用 sigsuspend 等待处理程序。

第 9 章:虚拟内存

我们讲述虚拟内存系统是希望读者对它是如何工作的以及它的特性有所了解。

我们想让读者了解为什么不同的并发进程各自都有一个完全相同的地址范围,能共享某些页,而又独占另外一些页。我们还讲了一些管理和操纵虚拟内存问题。特别地,我们讨论了存储分配操作,就像标准库的 mallocfree 操作。

阐述这些内容是出于下面几个目的。

它加强了这样的一个概念,那就是 虚拟内存空间只是一个字节数组 ,程序可以把它划分成不同的存储单元。它可以帮助读者理解当程序包含存储泄漏和非法指针引用等内存引用错误时的后果。最后,许多应用程序员编写自己的优化了的存储分配操作来满足应用程序的需要和特性。

这一章比其他任何一章都更能展现将计算机系统中的硬件和软件结合起来阐述的优点。而传统的计算机体系结构和操作系统书籍都只讲述虚拟内存的某一方面。

本章变化不大。

第 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 服务器。

我们介绍了采用最新 getaddrinfogetnameinfo 函数的、与协议无关和线程安全的网络编程,取代过时的、不可重入的 gethostbynamegethostbyaddr 函数。

第 12 章:并发编程

这一章以 Internet 服务器设计为例介绍了并发编程。

我们比较对照了三种编写并发程序的基本机制(进程、I/O 多路复用和线程),并且展示如何用它们来建造并发 Internet 服务器。

我们探讨了用 P、V 信号量操作来实现同步、线程安全和可重入、竞争条件以及死锁等的基本原则。对大多数服务器应用来说,写并发代码都是很关键的。

我们还讲述了线程级编程的使用方法,用这种方法来表达应用程序中的并行性,使得程序在多核处理器上能执行得更快。使用所有的核解决同一个计算问题需要很小心谨慎地协调并发线程,既要保证正确性,又要争取获得高性能。

我们扩充了利用线程级并行性使得程序在多核机器上更快运行的内容。

本书的起源

……

Date: 2020-12-05 Sat 21:20

Author: Jack Liu

Created: 2021-01-01 Fri 14:35

Validate