《深入理解计算机系统》
第一章 计算机系统漫游
- 计算机系统是由硬件和系统软件组成的,他们共同工作来运行应用程序。
信息就是位+上下文
- 源程序实际上就是一个由值0和1组成的为(又称为比特)序列,8个位组成一组,称为字节。
- 大部分的现代计算机系统都使用 ASCII 标准来表示文本字符,这种方式实际上就是用一个唯一的单字节大小的整数值来表示每个字符。
- 只由 ASCII 字符构成的文件称为文本文件,所有其他文件称为二进制文件。
- 区别不同数据对象的唯一方法是我们读到这些数据对象时的上下文。
程序被其他程序翻译成不同的格式
- 编译系统
- 预处理阶段。预处理器(cpp)根据以字符#开头的命令,修改原始的C程序。通常生成以
.i
为后缀名的文件。 - 编译阶段。编译器(ccl)将文本文件
hello.i
翻译成文本文件hello.s
,它包含一个汇编语言程序。- 汇编语言为不同高级语言的不同编译器提供了通用的输出语言。
- 汇编阶段。汇编器(as)将
hello.s
翻译成及其语言指令。 - 链接阶段。链接器将关于涉及到的函数的预编译文件整合到目标文件中,得到
hello
文件(可执行文件)。
- 预处理阶段。预处理器(cpp)根据以字符#开头的命令,修改原始的C程序。通常生成以
了解编译系统如何工作是大有益处的
- 优化程序性能
- 理解链接时出现的错误
- 避免安全漏洞
处理器读并解释储存在内存中的指令
shell是一个命令行解释器,它输出一个提示符,等待输入一个命令行,然后执行这个命令。
系统的硬件组成
总线:贯穿整个系统的是一组电子管道,它携带信息字节并负责在各个部件间传递。通常总线被设计成传送字长的字节块,也就是字。字中的字节数(即字长)是一个基本的系统参数,各个系统中都不尽相同。现在的大多数机器字长要么是4个字节(32位),要么是8个字节(64位)。
I/O设备是系统和外部世界的联系通道。每个I/O设备通过一个控制器或适配器与I/O总线相连。
- 控制器和适配器之间的区别主要在于它们的封装方式。
- 控制器是I/O设备本身或者系统的主印制电路板(通常称作主板上)的芯片组。
- 适配器是一块插在主板插槽上的卡。
主存是一个临时存储设备,在处理器执行程序时,用来存放程序和程序处理的数据。
- 从物理上来说,主存是由一组动态随机存储器(DRAM)芯片组成的。
- 从逻辑上来说,存储器是一个线性的字节数组,每个字节都有其唯一的地址(数组索引),这些地址是从零开始的。
处理器是中央处理单元(CPU)的简称,是解释(或执行)存储在主存中指令的引擎。处理器的核心是一个大小为一个字的存储设备(或寄存器),称为程序计数器(PC)。
在任何时刻,PC都指向主存中的某条及其语言指令(即含有该条指令的地址)。而PC更新时指向的下一条指令并不一定和在内存中刚刚执行的指令相邻。
- 加载:从主存复制一个字节或者一个字到寄存器,以覆盖寄存器原来的内容。
- 存储:从寄存器复制一个字节或者一个字到主存的某个位置,以覆盖这个位置上原来的内容。
- 操作:把两个寄存器的内容复制到ALU,ALU对这两个字做算术运算,并将结果存放到一个寄存器中,以覆盖该寄存器中原来的内容。
- 跳转:从指令本身中抽取一个字,并将这个字复制到程序计数器(PC)中,以覆盖PC中原来的值。
指令集架构描述的是每条机器代码指令的效果;而微体系结构描述的是处理器实际上是如何实现的。
运行 hello 程序
- 初始时,shell程序执行它的指令,等待我们输入一个指令。当我们在键盘上输入字符串
./hello
后,shell程序将字符逐一读入寄存器,再把它存放内存中。 - 当我们在键盘上敲回车键时,shell程序就知道我们已经结束了命令的输入。然后shell执行一系列指令来加载可执行的
hello
文件。这些指令将hello
目标文件中的代码和数据从磁盘复制到主存。数据包括最终会被输出的字符串"hello, world\n"
。- 利用直接存储器存取(DMA)技术,数据可以不通过处理器而直接从磁盘到达主存。
- 一旦目标位文件中的
hello
中的代码和数据被加载到主存,处理器就开始执行hello
程序的main
程序中的机器语言指令。这些指令将"hello, world\n"
字符串中的字节从主存赋值到寄存器文件,再从寄存器文件复制到显示设备,最终显示在屏幕上。
- 初始时,shell程序执行它的指令,等待我们输入一个指令。当我们在键盘上输入字符串
高速缓存至关重要
- 较大的存储设备要比较小的存储设备运行得慢,而快速设备的造价远高于同类的低速设备。
- 针对这种处理器和主存之间的差异,系统设计者采用了更小更快的存储设备,称为高速缓存存储器(cache memory,简称为cache或高速缓存),作为暂时的集结区域,存放处理器近期可能会需要的信息。
存储设备形成层次结构
- 每个计算机系统中的存储设备都被组织成了一个存储器层次结构。
- 存储器层次结构的主要思想是上一层的存储器作为低一层存储器的高速缓存。
- 操作系统的两个基本功能:
- 防止硬件被失控的应用程序滥用。
- 向应用程序提供简单一致的机制来控制复杂而又通常大小不相同的低级硬件设备。
- 文件是对I/O设备的抽象。
- 文件就是字节序列,每个 I/O 设备,包括磁盘、键盘、显示器,甚至网络,都可以看成是文件。
- 虚拟内存是对主存和磁盘 I/O 设备的抽象表示。
- 每个进程看到的内存都是一致的,称为虚拟地址空间。它们可分为以下区域:
- 程序代码和数据。对所有的进程来说,代码是从同一固定地址开始,紧接着的是和C全局变量相对应的数据位置。代码和数据区是直接按照可执行目标文件的内容初始化的。
- 堆。代码和数据区后紧随着的是运行时堆。代码和数据区在进程的一开始运行时就被指定了大小,而堆可以在运行时动态地扩展和收缩。
- 共享库。大约在地址空间的中间部分是一块用来存放像C标准库和数学库这样的共享库的代码和数据的区域。
- 栈。位于用户虚拟地址空间顶部的是用户栈,编译器用它来实现函数调用。和堆一样,用户栈在程序执行期间可以动态地扩展和收缩。
- 内核虚拟内存。地址空间顶部的区域是为内核保留的。不允许应用程序读写这个区域的内容或者直接调用内核代码定义的函数。相反,它们必须调用内核来执行这些操作。
- 每个进程看到的内存都是一致的,称为虚拟地址空间。它们可分为以下区域:
- 进程是对处理器、主存和 I/O 设备的抽象表示。
- 进程是操作系统对一个正在运行的程序的一种抽象。
- 并发运行是说一个进程的指令和另一个进程的指令是交错执行的。
- 传统系统在一个时刻只能执行一个程序,而先进的多核处理器同时能执行多个程序。
- 无论是单核还是多核系统中,一个 CPU 看上去都像是在并发地执行多个进程,这是通过处理器在进程间切换来实现的。操作系统实现这种交错执行的机制称为上下文切换。
- 操作系统保持跟踪进程运行所需的所有状态信息。这种状态,也就是上下文,包括许多信息,比如PC和寄存器文件的当前值,以及主存的内容。当操作系统决定要把控制权从当前进程转移到某个新进程时,就会进行上下文切换,即保存当前进程的上下文、恢复新进程的上下文,然后将控制权传递到新进程。
- 从一个进程到另一个进程的转换是由操作系统内核管理的。内核不是一个独立的进程。相反,它是系统管理全部进程所用代码和数据结构的集合。
- 一个进程实际上可以由多个称为线程的执行单元组成,每个线程都运行在进程的上下文中,并共享同样的代码和全局数据。
系统之间利用网络通信
- 现代系统经常通过网络和其他系统连接到一起。
重要主题
- Amdahl定律的主要思想是当我们对系统的某个部分加速时,其对系统整体性能的影响取决于该部分的重要性和加速程度。
- 并发是一个通用的概念,指一个同时具有多个活动的系统。
- 并行指的是用并发来使一个系统运行得更快。
- 处理必须在多个任务间切换,大多数实际的计算也都是由一个处理器来完成的。这种配置称为单处理系统。
- 当构建一个由但操作系统内核控制的多处理器组成的系统时,我们就得到了一个多处理系统。
- 多核处理器是将多个 CPU(称为“核”)集成在一个集成电路芯片上。
- 超线程,有时称为多线程,是一项允许一个 CPU 执行多个控制流的技术。
- 多处理器的使用可以从两方面提高系统性能。首先,它减少了在执行多个任务时模拟并发的需要。其次,它可以使应用程序执行地更快。
- 在较低的抽象层次上,现代处理器可以同时执行多条指令的属性称为指令级并行。
- 在流水线中,将执行一条指令所需要的活动划分成不同的步骤,将处理器的硬件组织成一系列的阶段,每个阶段执行一个步骤。
- 如果处理器可以达到比一个周期一条指令更快的执行速率,就称之为超标量。
- 允许一条指令产生多个可以并行执行的操作,这种方式称为单指令、多数据,即SIMD并行。
- 虚拟机提供对整个计算机的抽象,包括操作系统、处理器和程序。
第二章 信息的表示与处理
- 现代计算机存储和处理的信息以二进制信号表示(位),他们形成了数字革命的基础。
- 当把位组合在一起,再加上某种解释,我们就可以描述任何有限集合的元素。
- 无符号编码基于传统的二进制表示法,表示大于或等于零的数字。
- 补码编码是表示有符号整数的最常见的方式,有符号整数就是可以为正或者为负的数字。
- 浮点数编码是表示实数的科学记数法以2为基数的版本。
- 当结果太大以至于不能表示时,某些运算就会溢出。
- 整数的表示虽然只能编码一个相对较小的数值范围,但是这种表示是精准的;而浮点数虽然可以编码一个较大的数值范围,但这种表示只是近似的。浮点数的计算可能因为运算过程的不同而影响正常结果,整数则不会。
信息存储
- 大多数计算机使用8位的块(字节 byte),作为最小的可寻址的内存单位,而不是位。
- 机器级程序将内存视为一个非常大的字节数组,称为虚拟内存。
- 内存的每一个字节都由一个唯一的数字来标识,称为它的地址。
- 所有可能的地址的集合就称为虚拟地址空间。
十六进制表示法
- 一个字节由8位组成。在二进制表示法中,它的值域是000000002 ~ 111111112。如果看成十进制整数,它的值域就是010 ~ 25510。
- 二进制和十六进制的之间的转换可以通过展开每个十六进制数字,将它转换为二进制格式。
- 将一个十进制数字 x 转换为十六进制,可以反复地用16除 x。
字数据大小
- 每台计算机都有一个字长(word size),指明指针数据的标称大小(nominal size)。字长决定的最重要的系统参数就是虚拟地址空间的最大大小。
- 对于一个字长为 ω 位的机器而言,虚拟地址的范围为0 ~ 2ω - 1。
- 大多数64位机器也可以运行为32位机器编译的程序,这是一种向后兼容。
- 数据类型 long 一般在32位程序中为4字节,在64位程序中则为8字节。为了避免由于依赖“典型”大小和不同编译器设置带来的奇怪行为,ISO C99引入一类数据类型,其数据大小是固定的,不随编译器和机器设置而变化,其中就有数据类型 int32_t 和 int64_t,它们分别为4个字节和8个字节。
寻址和字节顺序
- 小端法:最低有效字节在最前面的方式。
- 大端法:最高有效字节在最前面的方式。
- 许多比较新的微处理器是双端法,可以把配置成作为大端或者小端的机器运行。
- 反汇编器:一种确定可执行程序文件所表示的指令序列的工具。
表示字符串
- 在使用 ASCII 码作为字符码的任何系统上都将得到相同的结果,与字节顺序和字大小无关。因而,文本数据比二进制数据具有更强的平台独立性。
表示代码
- 不同的机器类型使用不同且不兼容的指令和编码方式。
布尔代数简介
位向量就是固定长度为 ω 、由0和1组成的串。
~ & 0 1 | 0 1 ^ 0 1 0 1 0 0 0 0 0 1 0 0 1 1 0 1 0 1 1 1 1 1 1 0
C 语言的位级运算
- C 语言的一个有用的特性就是它支持布尔运算。
- 位级运算的一个常见用法就是实现掩码运算,这里掩码是一个位模式,表示从一个字中选出的位的集合。
C语言中的逻辑计算
- C语言还提供了一组逻辑运算符 || 、&& 和 !,分别对应 OR 、AND 和 NOT运算。逻辑运算认为所有非零的参数都表示 TRUE,而参数0表示 FALSE。它们返回1或者0,分别表示结果为 TRUE 或者为 FALSE。
- 逻辑运算符 && 和 || 与它们对应的位级运算 & 和 | 之间第二个重要的区别是,如果对第一个参数求值就能确定表达式的结果,那么逻辑运算符就不会对第二个参数求值。
C语言中的移位运算
- C 语言还提供了一组移位运算,向左或者向右移动位模式。C 表达式 x<<k 会生成一个值,x 向左移动 k 位,丢弃最高的 k 位,并在右端补 k 个0。移位运算时从左至右可结合的。
- 一般而言,机器支持两种形式的右移:逻辑右移和算术右移。
- 逻辑右移是在左端补 k 个0。
- 算术右移是在左端补 k 个最高有效位的值,它对有符号整数数据的运算非常有用。
- C 语言标准并没有明确定义对于有符号数应该使用哪种类型的右移——算术右移或者逻辑右移都可以。
- 对于无符号数,右移必须是逻辑的。
- 与 C 相比,Java 对于如何进行右移有明确的定义。表达是 x>>k 会将 x 算术右移 k 个位置,而 x>>>k 会对 x 做逻辑右移。