标签: CSAPP
从某种意义上来说,本书的目的就是为了帮助你了解当你在系统上执行 hello 程序时,系统发生了什么以及为什么会这样。
1. 一个 hello 程序的执行过程
1.0 准备知识
为了理解
hello
程序运行时发生什么,需要先对一个典型系统的硬件组织有一个大致的了解。如下图所示:
系统的硬件组成包括:
- 总线
贯穿整个系统的一组电子管道,负责在各个部件之间传递信息。总线通常被设计成传送定长的字节块,也就是 字(word)。 - I/O 设备
I/O 设备是系统与外部世界的联系通道。
每个 I/O 设备都通过一个 控制器 或 适配器 与 I/O总线相连。
控制器与适配器之间的区别主要在于它们的封装方式。控制器是 I/O设备本身或者系统的主板上的芯片组,而适配器是一块插在主板插槽上的卡。
- 总线
- 主存
主存是一个临时存储设备,在处理器执行程序时,用来存放程序和程序处理的数据。
物理上,存储器是由一组 DRAM 芯片 组成。逻辑上,存储器是一个线性的字节数组,每个字节有唯一的地址。 - 处理器(CPU)
是执行存储在主存中指令的引擎,核心是 程序计数器(PC),在任何时刻,PC 都指向主存中的某条机器指令。 1.1 源代码
此例子中的源代码如下:
#include<stdio.h>
int main()
{
printf("hello world\n");
return 0;
}
每个程序都是由源代码开始的,源代码即文本文件。
只由 ASCII 字符构成的文件称为 文本文件,所有其他文件称为 二进制文件。
1.2 源程序到可执行文件
为了在系统上运行 hello.c 程序,每条 C 语句都必须被其他程序转化为一系列的
低级 机器语言 指令,然后按照一种称为 可执行目标程序 的格式打包起来,并以二进制磁盘文件的形式存放起来。
在 Unix 系统上,从源文件到目标程序的转化是由 编译器驱动程序 完成的。编译过程如下:
编译过程由四个阶段组成:
- 预处理阶段
预处理器(cpp)根据以字符 # 开头的命令(头文件、宏定义等),修改原始的 C 程序。比如hello.c
中第一行的#include<stdio.h>
命令告诉预处理器读取系统头文件stdio.h
的内容,并把它直接 插入 程序文本中。结果就得到了另一个 C 程序hello.i
。 编译阶段
编译器(ccl)将文本文件hello.i
翻译成文本文件hello.s
,它包含一个 汇编语言程序。如下:main: subq $8, %rsp movl $.LCO, %edi call puts movl $0, %eax addq $8, %rsp ret
汇编阶段
汇编器(as)将hello.s
翻译成机器语言指令打包成 可重定位目标程序 保存为二进制文件hello.o
,包含了函数main
的指令编码。链接阶段
hello
程序调用了printf
函数,这是标准 C 库中的一个函数,保存在一个名为printf.o
的单独预编译好了的目标文件中,链接器以某种方式将这个文件合并到hello.o
中,结果得到名为hello
的可执行目标文件。
- 预处理阶段
1.3 运行
- 执行
hello
程序时,hello
目标文件中的代码和数据从磁盘被复制到主存,然后处理器开始执行main
程序中的机器语言指令。
2.其他概念
2.1 高速缓存
- 为了解决处理器和主存之间巨大的速度差异,利用程序的局部性原理,在主存和处理器之间设计一级或者多级的 高速缓存存储器(cache),通过让 cache 里存放可能经常访问的数据,大部分的内存操作能在快速的 cache 中完成。
2.2 存储设备的层次结构
2.3 操作系统管理硬件
- 操作系统是应用程序和硬件之间插入的一层软件,所有应用程序对硬件的操作尝试都必须通过操作系统。
2.3.1 进程
一个程序在现代操作系统上运行时,操作系统会提供一种假象,就好像系统上只有这个程序在运行。程序看上去像是独占地使用处理器、主存和I/O设备。处理器看上去就像是在不间断地一条接一条地执行程序中的指令。
并发运行:一个进程的指令和另一个进程的指令是交错执行的。
单处理器系统每次只能执行一个进程的代码,并发运行需要在多个进程之间来回切换,切换时,操作系统要保存当前进程的状态信息,称为上下文切换,状态信息即 上下文(context)。
2.3.2 线程
- 在现代操作系统中,一个进程实际上可以由多个称为 线程 的执行单元组成,每个线程都运行在进程的上下文中,并共享同样的代码和全局数据。
2.3.3 虚拟内存
- 虚拟内存 是一个抽象概念,它为每一个进程提供了一个假象,即每个进程都在独占地使用主存。
- 每个进程看到的内存都是一致的,称为 虚拟地址空间。
下图中的地址从下往上增大。
- 从低地址从下往上分别是:
- 程序代码和数据
对所有进程来说,代码是从同一固定地址开始,紧接着是和 C 全局变量相对应的数据位置。这段对应的就是 可执行文件 hello。 - 堆
可以在运行时动态地扩展和收缩。 - 共享库
存放像 C 标准库和数学库这样的共享库的代码和数据。 - 栈
程序用栈来实现函数调用,和堆一样,可以在程序运行时动态地扩展和收缩。比如,调用一个函数时,栈就会增长;从一个函数返回时,栈就会收缩。 - 内核虚拟内存
不允许程序读写这个区域或者直接调用内核代码定义的函数。相反,它们必须调用内核来执行这些操作。
- 程序代码和数据
2.3.4 文件
- 文件就是字节序列,仅此而已。
- 每个 I/O设备 都可以看成是文件。
- 系统中所有的输入输出都是通过使用一小组称为 Unix I/O的系统调用函数调用读写文件来实现的。
2.4 Amdahl 定律
- 该定律的主要思想是,当我们对系统的某个部分加速时,其对系统整体性能的影响取决于该部分的重要性和加速程度。
若系统执行某应用程序需要时间 Tol−d ,假设系统某部分所需执行时间与该时间的比例为 a ,而该部分性能提升比例为
k ,即该部分初始所需时间为 aTold ,现在所需时间为 (aTold)/k 。因此,总的执行时间应为
Tnew=(1−a)Told+(aTold)/k=Told[(1−a)+a/k]
由此,可以计算加速比 S=Told/Tnew 为
S=1/[(1−a)+a/k]由公式可以看出,要想加速整个系统,必须提升全系统中相当大部分的速度。
2.5 并发与并行
- 并发:同时具有多个活动的系统,如处理器不同的核心之间同时工作。
- 并行:用并发来使一个系统运行得更快,如处理器得一个核心同时并发运行多个程序。
- 三个层次:
- 线程级并发
- 指令级并行
- 单指令、多数据并行
2.6 计算机中得抽象
- 文件 是对 I/O设备得抽象。
- 虚拟内存 是对程序存储器的抽象。
- 进程 是对一个正在运行的程序的抽象。
- 虚拟机 是对整个计算机的抽象,包括操作系统、处理器和程序。
3. 小结
计算机是由硬件和系统软件组成的,它们共同协作以运行应用程序。计算机内部的信息被表示为一组组的位,它们依据上下文有不同的解释方式。程序被其他程序翻译成不同的形式,开始时是 ASCII 文本,然后被编译器和链接器翻译成二进制可执行文件。
处理器读取并解释存放在主存里的二进制指令。因为计算机花费了大量时间在内存、I/O设备和CPU寄存器之间复制数据,所以将系统中的存储设备划分成层次结构————CPU寄存器在顶端接着是多层的硬件高速缓存存储器、DRAM主存和磁盘存储器。在层次模型中,位于更高层的存储设备比低层的存储设备要快,单位比特造价也高。层次结构中较高层次的存储设备可以作为较低层次的存储设备的高速缓存。通过理解和运用这种存储层次结构的知识,程序员可以优化C程序的性能。
操作系统内核是应用程序和硬件之间的媒介。它提供三个基本抽象:1)文件是对I/O设备的抽象;2)虚拟内存是对主存和磁盘的抽象;3)进程是处理器、主存和I/O设备的抽象。
最后,网络提供了计算机系统之间通信的手段。从特殊角度看,网络就是一种I/O设备。