# 《CSAPP:第一章》

# 1.1 信息就是位 + 上下文

程序的生命周期从源程序 (源文件) 开始。源程序实际上就是由 0 和 1 组成的位序列。

一般 ASCII 标准来表示文本字符,实际上是用一个字节的整数值来表示一种字符。

源文件中每个文本行都是以看不见的 '\n' 结束的。

只由 ASCII 字符组成的文件成为文本文件,其他都是二进制文件。.cpp 文件就是文本文件。

系统中的所有信息都是由一串比特(bit:位)表示的,区分不同数据对象的唯一方法就是根据上下文。

C 语言的特点

  1. C 语言小而简单
  2. C 语言是为了实现 unix 而设计的
  3. C 语言与 unix 关系密切

C 语言是系统级编程的首选,也非常适用于应用级程序。

# 1.2 程序被其他程序翻译成不同的格式

从源程序到目标程序要经历四个步骤:

  1. 源程序被预处理器处理得到修改了的源程序(文本文件,hello.i)
  2. 再由编译器处理得到汇编程序(文本文件,hello.s)
  3. 汇编程序由汇编器处理得到可重定位目标程序(二进制文件,hello.o)
  4. 最后由链接器链接得到可执行目标程序(二进制文件,hello)

预处理阶段

预处理器根据 # 开头的命令修改原始的 c 程序。比如根据 #include 命令把头文件 stdio.h 的内容直接插入到程序文件中

编译阶段

汇编阶段

链接阶段

比如 printf 函数是一个标准 C 库函数,存在于 printf.o 中,这是一个单独的预编译好了的目标文件。链接器将其与汇编得到的二进制文件合并得到可执行目标文件。

# 1.3 了解编译系统如何工作是大有用处的

用处:

  1. 优化程序性能
  2. 理解链接时出现的错误
  3. 避免安全漏洞

# 1.4 处理器读并解释储存在内存中的指令

shell 是一个命令行解释器,它输出一个提示符(>>),等待输入一个命令行,然后执行命令。如果输入的是可执行文件的名字,就运行该文件。

# 1.4.1 系统的硬件组成

主要包括总线、I/O 设备、处理器、主存储器四个部分

  1. 总线

    总线一次可以传输一个定长的字节块,称为字。64 位系统即总线一次可以传输 64 位(8 字节),这里一个字就是 8 字节

  2. I/O 设备

    每个 I/O 设备通过一个控制器适配器与 I/O 总线相连。

    控制器是 I/O 设备本身或主板上的芯片组,适配器则是一块插在主板上的卡。

  3. 主存

    主存是由一组 ** 动态随机存取内存(DRAM)** 组成的。

    从逻辑上看,存储器是一个线性的字节数组,每个字节都有唯一的地址。

  4. 处理器

    处理器是解释存储在主存中指令的引擎。

    处理器的核心是一个程序计数器(PC)

    程序计数器是一个大小为一个字的存储设备,存储 CPU 即将执行的下一条指令的地址

    处理器就是在不断执行程序计数器指向的指令。每执行一条,程序计数器更新一次,指向下一条指令。

    处理器会按照指令执行模型(指令集架构)解释指令中的位并执行相应操作。

    每条指令的操作是围绕主存、寄存器文件、算数 / 逻辑单元(ALU) 进行的。

  5. 寄存器文件:单个字长,有唯一的名字。

  6. ALU:计算新的数据和地址值。

  7. 几个简单指令的操作:

    • 加载:从主存复制一个字或字节到寄存器,覆盖原来内容
    • 存储:从寄存器复制一个字或字节到主存,覆盖原来内容
    • 操作:把两个寄存器的内容复制到 ALU,ALU 对这两个字做算术运算,并把结果存到一个寄存器中
    • 跳转:从指令中抽取一个字复制到程序计数器中,覆盖原来内容。

区分处理器指令集架构和微体系架构:

  • 指令集架构:每条机器指令的效果
  • 微体系架构:处理器实际上是如何实现的

# 1.4.2 运行 hello 程序

执行目标文件时,shell 程序将位于磁盘目标文件中的字符逐个读入寄存器,然后放到主存中。之后处理器就开始执行目标文件的机器语言指令,从 main 程序开始。

利用直接存储器存取(DMA)可以不通过寄存器,直接将数据从磁盘到达内存。

以输出打印 hello world 为例,处理器将 hello world 的字节复制到寄存器,然后再复制到显示器,最后显示在屏幕上。

整个流程: 读取文件字符到寄存器存储到主存执行指令加载 helloworld 到寄存器 → 复制到显示器 → 显示

# 1.5 高速缓存至关重要

从主存读取一个字比磁盘快 1000 万倍。

从寄存器文件读取比主存块 100 倍,并且差距还在加大。

高速缓存(cache)用来解决处理器与主存间的差异。

L1 高速缓存位于 CPU 上,容量为数万字节(几十 MB)。L1 比 L2 快 5 倍。

L2 高速缓存通过一条特殊的总线与 CPU 连接,容量为数十万到数百万字节(几百 MB 到 几 GB)。L2 比 主存快 5~10

新的系统还有 L3。

通过让高速缓存里存放可能经常访问的数据,让大部分的内存操作都在高速缓存中完成。

# 1.6 存储设备形成层次结构

存储器层次结构共 7 层,主要思想是上一层的存储器作为低一层的高速缓存。

从上到下,容量更大,运行更慢,每字节价格更便宜。

  • 0 层:寄存器
  • 1 层:L1 高速缓存 (SRAM)
  • 2 层:L2 高速缓存 (SRAM)
  • 3 层:L3 高速缓存(SRAM)
  • 4 层:主存(DRAM)
  • 5 层:本地二级存储(本地磁盘)
  • 6 层:远程二级存储(分布式文件系统,Web 服务器)

# 1.7 操作系统管理硬件

操作系统的两个基本功能:

  1. 防止硬件被失控的应用程序滥用
  2. 向应用程序提供简单一致的机制来控制复杂的低级硬件设备

操作系统所应用的三个基本的抽象概念:

  1. 进程:对处理器、主存和 I/O 设备的抽象表示
  2. 虚拟内存:对主存和磁盘的抽象表示
  3. 文件:对 I/O 设备的抽象表示

# 1.7.1 进程

进程:对操作系统正在运行的程序的一种抽象。

并发运行:一个进程的指令和另一个进程的指令是交错执行的。

一个系统可以同时运行多个进程,实际上这些进程是并发运行的。

操作系统通过上下文切换来实现并发运行。上下文是跟踪进程运行所需的所有状态信息,可能存在于 PC、寄存器文件、主存等地方。

任何时刻,单处理器只能执行一个进程的代码。

操作系统内核是操作系统代码常驻主存的部分,从一个进程到另一个进程的转换是由内核管理的。

内核不是一个独立的进程,是一系列代码和数据结构的集合。

当应用程序需要操作系统的某些操作时,就把控制权传递给内核,内核执行完操作后返回应用程序。

# 1.7.2 线程

一个进程由多个线程组成,每个线程都运行在进程的上下文中,共享同样的代码和全局数据。

多线程之间比多进程之间更容易共享数据,且线程一般来说比进程更高效。

# 1.7.3 虚拟内存

机器级程序将内存视为一个庞大的字节数组,称为虚拟内存

内存的每个字节由地址来标识,所有可能地址的集合就是虚拟地址空间。

虚拟内存使每个进程都以为自己独占了主存。每个进程看到的内存都是一致的,即虚拟地址空间

在 linux 中,每个进程看到的虚拟地址空间由以下几个部分组成:

  1. 程序代码和数据
  2. 堆(运行时堆)
  3. 共享库
  4. 栈(用户栈)
  5. 内核虚拟内存

地址从低到高,最高层的内核虚拟内存保存的是操作系统中的代码和数据,这部分每个进程都一样。

程序代码和数据 *

对所有进程来说,代码都是从同一个固定地址开始,紧接着是与全局变量对应的数据区。代码和数据区都是按照可执行文件的内容初始化的。代码和数据区在进程开始运行时就被指定了大小。

而运行时堆是根据 malloc 和 free 函数的调用在运行时动态地扩展和收缩的。

共享库

地址空间的中间部分用来存放共享库的代码和数据。如 C 标准库、数学库等都属于共享库

用户栈和堆一样,在程序执行期间可以动态的扩展和收缩,编译器用它来实现函数调用。当调用函数时,栈增长,从函数返回时,栈收缩

# 1.7.4 文件

文件就是字节序列,仅此而已。

每个 I/O 设备,包括磁盘、键盘、显示器、网络,都可以看成是文件。

# 1.8 系统之间利用网络通信

从一个单独的系统而言,网络可以视为一个 I/O 设备。

以在一个远端服务器运行程序为例,在本地输入,在远端执行,执行结果发送回本地输出。

# 1.9 重要主题

# 1.9.1 Amdahl 定律

Amdahl 定律的主要观点:要加速整个系统,必须提升全系统中相当大的部分。

# 1.9.2 并发和并行

区分并发与并行:

  • 并发:一个通用的概念,指一个同时具有多个活动的系统
  • 并行:用并发来使系统运行得更快

并行可以在多个抽象层次上运用。从高到低有以下三个层次

1. 线程级并行

传统意义上的并发执行是通过单处理器在进程间快速切换模拟出来的。

多处理器系统由一个操作系统控制多个 CPU。结构如下

image-20221005160155790

L1 高速缓存被分为两个部分:一个保存最近取到的指令,一个存放数据。

超线程又称同时多线程,它允许一个 CPU 执行多个控制流。 CPU 有的硬件有多个备份,比如程序计数器和寄存器文件,而其他硬件只有一份,比如浮点算术运算单元。常规 CPU 需要约 20000 个时钟周期来切换线程,超线程 CPU 可以在单个周期的基础上切换线程,比如一个线程在等待数据装在到高速缓存,CPU 就可以去执行另一个线程。

i7 处理器每个核执行两个线程,所以是 4 核 8 线程,8 个线程都并行执行。

2. 指令级并行

每条指令从开始到结束一般需要 20 个或更多的时钟周期,通过指令级并行,可以实现每个周期 2~4 条指令的执行速率。

如果比一个周期一条指令更快,就称为超标量处理器,现在一般都是超标量。

3. 单指令、多数据并行

在最低层次上,现代处理器允许一条指令产生多个可以并行执行的操作,称为单指令、多数据并行,即 SIMD 并行。

# 1.9.3 计算机系统中抽象的重要性

image-20221005160217657

指令集架构是对 CPU 硬件的抽象,使用这个抽象,CPU 看起来好像一次只执行机器代码程序的一条指令,实际上底层硬件并行地执行多条指令。

虚拟机是对整个计算机系统的抽象,包括操作系统、处理器和程序。1.4.1 系统的硬件组成

主要包括总线、I/O 设备、处理器、主存储器四个部分