CSAPP-计算机漫游
CSAPP 计算机系统漫游
1 信息就是位+上下文
整个计算机系统中的所有信息都可以用一串比特串的形式表示,区分不同的数据对象的唯一方法就是我们读到的这些对象时的上下文(context)
旁注 编程语言的起源
C 语言是贝尔实验室的 Dennis Ritchie 于 1969 年 ~ 1973 年间创建的。美国国家标准学会(American National Standards Institute,ANSI)在 1989 年颁布了 ANSI C 的标准,后来 C 语言的标准化成了国际标准化组织(International StandardsOrganization,ISO)的责任。这些标准定义了C语言和一系列函数库,即所谓的 C 标准库。Kernighan 和 Ritchie 在他们的经典著作中描述了 ANSI C,这本著作被人们满怀感情地称为 “K&R”【61】。用 Ritchic 的话来说【92】,C 语言是“古怪的、有缺陷的,但同时也是一个巨大的成功”。为什么会成功呢?
C 语言与 Unix 操作系统关系密切。C 从一开始就是作为一种用于 Unix 系统的程序语言开发出来的。大部分 Unix 内核(操作系统的核心部分),以及所有支撑工具和函数库都是用 C 语言编写的。20 世纪 70 年代后期到 80 年代初期,Unix 风行于高等院校,许多人开始接触 C 语言并喜欢上它。因为 Unix 几乎全部是用 C 编写的,它可以很方便地移植到新的机器上,这种特点为 C 和 Unix 赢得了更为广泛的支持。
C 语言小而简单。C语言的设计是由一个人而非一个协会掌控的,因此这是一个简洁明了、没有什么冗赘的设计。K&R 这本书用大量的例子和练习描述了完整的 C 语言及其标准库,而全书不过 261 页。C 语言的简单使它相对而言易于学习,也易于移植到不同的计算机上。
C语言是为实践目的设计的。C 语言是设计用来实现 Unix 操作系统的。后来,其他人发现能够用这门语言无障碍地编写他们想要的程序。
C 语言是系统级编程的首选,同时它也非常适用于应用级程序的编写。然而,它也并非适用于所有的程序员和所有的情况。C 语言的指针是造成程序员困惑和程序错误的一个常见原因。同时,C 语言还缺乏对非常有用的抽象的显式支持,例如类、对象和异常。像 C++ 和 Java 这样针对应用级程序的新程序语言解决了这些问题。
2 程序被其他程序翻译成不同的格式
在 Unix 系统上,从源文件到目标文件的转化是由编译器驱动程序完成的∶
1 | **linux> gcc -o hello hello.c** |
在这里,GCC 编译器驱动程序读取源程序文件 hello.c,并把它翻译成一个可执行目标文件 hello。这个翻译过程可分为四个阶段完成,如图所示。执行这四个阶段的程序(预处理器、编译器、汇编器和链接器)一起构成了编译系统(compilation system)。
详细的说:
预处理阶段。预处理器(cpp)根据以字符 #
开头的命令,修改原始的 C 程序。比如 hello.c 中第 1
行的#include <stdio.h>
命令告诉预处理器读取系统头文件
stdio.h 的内容,并把它直接插入程序文本中。结果就得到了另一个 C
程序,通常是以 .i 作为文件扩展名。
编译阶段。编译器(cc1)将文本文件 hello.i 翻译成文本文件 hello.s,它包含一个汇编语言程序。该程序包含函数 main 的定义,如下所示∶
1 | main: |
定义中 2~7 行的每条语句都以一种文本格式描述了一条低级机器语言指令。汇编语言是非常有用的,因为它为不同高级语言的不同编译器提供了通用的输出语言。例如,C编译器和 Fortran 编译器产生的输出文件用的都是一样的汇编语言。
汇编阶段。接下来,汇编器(as)将 hello.s 翻译成机器语言指令,把这些指令打包成一种叫做可重定位目标程序(relocatable object program)的格式,并将结果保存在目标文件 hello.o 中。hello.o 文件是一个二进制文件,它包含的 17 个字节是函数 main 的指令编码。如果我们在文本编辑器中打开 hello.o文件,将看到一堆乱码。
链接阶段。请注意,hello 程序调用了 printf 函数,它是每个 C 编译器都提供的标准 C 库中的一个函数。printf 函数存在于一个名为 printf.o 的单独的预编译好了的目标文件中,而这个文件必须以某种方式合并到我们的 hello.o 程序中。链接器(ld)就负责处理这种合并。结果就得到 hello 文件,它是一个可执行目标文件(或者简称为可执行文件),可以被加载到内存中,由系统执行。
简略的说:
- 预处理阶段,处理源码的中的预处理语句(比如说#include)
- 编译阶段,将c语言编译成汇编语言
- 汇编阶段,把汇编语言翻译成机器指令
- 链接阶段,把在程序中调用的库函数的相关文件引入
3 了解编译系统如何工作室大有益处的
促使程序员要知道编译系统是如何工作的原因:
- 优化程序性能,我们需要对汇编语言以及编译器如何将不同的C语句转化为汇编语言有基本的了解
- 理解链接时出现的错误
- 避免安全漏洞,其中一个比较典型的是缓冲区溢出错误
4 处理器读并解释存储在存储器中的指令
shell
是一个命令行解释器,它输出一个提示符(>>),等待输入一个命令行,然后执行命令。如果输入的是可执行文件的名字,就运行该文件。
一个计算机系统的硬件主要由以下几个部分组成:
总线,总线一次可以传输一个定长的字节块,称为字。64位系统即总线一次可以传输 64 位(8字节),这里一个字就是 8 字节
I/O设备,每个 I/O 设备通过一个控制器或适配器与 I/O 总线相连。
主存,主存是由一组动态随机存取内存(DRAM)组成的。
从逻辑上看,存储器是一个线性的字节数组,每个字节都有唯一的地址。
处理器,解释(或执行)存储在主存中的指令
处理器是解释存储在主存中指令的引擎。
处理器的核心是一个程序计数器(PC)
程序计数器是一个大小为一个字的存储设备,存储CPU即将执行的下一条指令的地址。
处理器就是在不断执行程序计数器指向的指令。每执行一条,程序计数器更新一次,指向下一条指令。
处理器会按照指令执行模型(指令集架构)解释指令中的位并执行相应操作。
每条指令的操作是围绕主存、寄存器文件、算数/逻辑单元(ALU)进行的。
寄存器文件:单个字长,有唯一的名字。
ALU:计算新的数据和地址值。
几个简单指令的操作:
- 加载:从主存复制一个字或字节到寄存器,覆盖原来内容
- 存储:从寄存器复制一个字或字节到主存,覆盖原来内容
- 操作:把两个寄存器的内容复制到 ALU,ALU 对这两个字做算术运算,并把结果存到一个寄存器中
- 跳转:从指令中抽取一个字复制到程序计数器中,覆盖原来内容。
区分处理器指令集架构和微体系架构:
- 指令集架构:每条机器指令的效果
- 微体系架构:处理器实际上是如何实现的
执行一个hello world
程序的过程有一下几步:
1 |
|
执行目标文件时,shell
程序将位于磁盘目标文件中的字符逐个读入寄存器,然后放到主存中。之后处理器就开始执行目标文件的机器语言指令,从
main
程序开始。
利用直接存储器存取(DMA)可以不通过寄存器,直接将数据从磁盘到达内存。
以输出打印 hello world 为例,处理器将 hello world 的字节复制到寄存器,然后再复制到显示器,最后显示在屏幕上。
整个流程: 读取文件字符到寄存器 → 存储到主存 → 执行指令 → 加载 helloworld 到寄存器 → 复制到显示器 → 显示
5 高速缓存
我们使用的存储设备通常是较大的存储设备比较小的存储设备运行地要慢,所以就使用一个较小的速度较快的存储设备作为CPU和Main Memory交换数据的桥梁,这个设备就是高速缓存(cache memories)
6 形成层次结构的存储结构
存储器层次结构共 7 层,主要思想是上一层的存储器作为低一层的高速缓存。
从上到下,容量更大,运行更慢,每字节价格更便宜。
- 0层:寄存器
- 1层:L1高速缓存(SRAM)
- 2层:L2高速缓存(SRAM)
- 3层:L3高速缓存(SRAM)
- 4层:主存(DRAM)
- 5层:本地二级存储(本地磁盘)
- 6层:远程二级存储(分布式文件系统,Web服务器)
7 操作系统管理硬件
操作系统可以看成是一个应用程序和硬件之间的一个软件,其有两个基本功能: 防止硬件被失控的程序滥用; 为应用程序提供控制硬件的简单一致的方法
7.1 进程
进程可以看成是操作系统对正在运行的程序的一种抽象,在一个系统中可以运行多个进程,这些进程对外表现好像是独占硬件,实际上是通过不同进程之间进程的交互执行实现的,这个过程叫上下文切换(context switch)
7.2 线程
一个进程可以由多个线程组成,运行在一个上下文环境中,共享代码以及全局数据。因为共享数据,使得其比一般的进程更加高效(花在context switch的时间少)。
7.3 虚拟存储器
给进程提供的一个好像自己独占主存的假象,对于进程的所使用的虚拟存储器可以分成一下几个部分:
- 程序代码和数据
对所有进程来说,代码都是从同一个固定地址开始,紧接着是与全局变量对应的数据区。代码和数据区都是按照可执行文件的内容初始化的。代码和数据区在进程开始运行时就被指定了大小。
- 堆
可以动态扩展或者收缩,供像malloc和free这样的C语言中的库进行调用
- 共享库
地址空间的中间部分用来存放共享库的代码和数据。如 C 标准库、数学库等都属于共享库
栈 用户栈和堆一样,在程序执行期间可以动态的扩展和收缩,编译器用它来实现函数调用。当调用函数时,栈增长,从函数返回时,栈收缩
内核虚拟存储器
7.4 文件
文件就是字节序列,仅此而已。
每个 I/O 设备,包括磁盘、键盘、显示器、网络,都可以看成是文件。
8 利用网络系统和其他系统进行通信
从一个单独的系统而言,网络可以视为一个 I/O 设备。
以在一个远端服务器运行程序为例,在本地输入,在远端执行,执行结果发送回本地输出。
9 重要主题
9.1 Amdahl 定律
Amdahl 定律的主要观点:要加速整个系统,必须提升全系统中相当大的部分。
9.2 并发和并行
区分并发与并行:
- 并发:一个通用的概念,指一个同时具有多个活动的系统
- 并行:用并发来使系统运行得更快
1.线程级并行
传统意义上的并发执行是通过单处理器在进程间快速切换模拟出来的。
多处理器系统由一个操作系统控制多个 CPU。结构如下
2. 指令级并行
每条指令从开始到结束一般需要 20 个或更多的时钟周期,通过指令级并行,可以实现每个周期 2~4 条指令的执行速率。
如果比一个周期一条指令更快,就称为超标量处理器,现在一般都是超标量。
3. 单指令、多数据并行
在最低层次上,现代处理器允许一条指令产生多个可以并行执行的操作,称为单指令、多数据并行,即 SIMD 并行。
9.3 计算机系统中抽象的重要性
指令集架构是对 CPU 硬件的抽象,使用这个抽象,CPU 看起来好像一次只执行机器代码程序的一条指令,实际上底层硬件并行地执行多条指令。
虚拟机是对整个计算机系统的抽象,包括操作系统、处理器和程序。
10 小结
计算机系统是由硬件和系统软件组成的,它们共同协作以运行应用程序。计算机内部的信息被表示为一组组的位,它们依据上下文有不同的解释方式。程序被其他程序翻译成不同的形式,开始时是 ASCII 文本,然后被编译器和链接器翻译成二进制可执行文件。
处理器读取并解释存放在主存里的二进制指令。因为计算机花费了大量的时间在内存、I/O 设备和 CPU 寄存器之间复制数据,所以将系统中的存储设备划分成层次结构——CPU 寄存器在顶部,接着是多层的硬件高速缓存存储器、DRAM 主存和磁盘存储器。在层次模型中,位于更高层的存储设备比低层的存储设备要更快,单位比特造价也更高。层次结构中较高层次的存储设备可以作为较低层次设备的高速缓存。通过理解和运用这种存储层次结构的知识,程序员可以优化C程序的性能。
操作系统内核是应用程序和硬件之间的媒介。它提供三个基本的抽象∶1)文件是对 I/O 设备的抽象;2)虚拟内存是对主存和磁盘的抽象;3)进程是处理器、主存和 I/O 设备的抽象。
最后,网络提供了计算机系统之间通信的手段。从特殊系统的角度来看,网络就是一种 I/O 设备。