深入理解计算机系统 —— 计算机系统漫游

系列 - 深入理解计算机系统 阅读笔记

本章通过跟踪 hello 程序的生命周期来开始对计算机系统的学习。

C

// hello.c
#include <stdio.h>

int main()
{
    printf("hello, world\n");
    return 0;
}

hello.c 源程序实际上是一个由值 0 和 1 组成的位(比特)序列。8 个位被组成一组,称为字节。hello.c 程序以字节序列的方式存储在文件中。

计算机中表示信息的基本思想:系统中的所有信息——包括磁盘文件、内存中的程序、内存中存放的用户数据以及网络上传送的数据,都是由一串比特表示的。区分不同数据对象的唯一方法是我们读到这些数据对象时的上下文


为了在系统上执行 hello.c 程序,每条 C 语句都必须被其他程序转化为一系列的低级机器语言指令。这些指令按照可执行目标程序的格式打包,以二进制磁盘文件的形式存放。

编译系统
编译系统

翻译过程分为四个阶段。执行这四个阶段的程序(预处理器、编译器、汇编器和链接器)一起构成了编译系统

  • 预处理阶段。预处理器根据以字符“#”开头的命令,修改原始的 C 程序。以“.i ”作为扩展名。比如#include <stdio.h>命令告诉预处理器读取系统头文件“stdio.h”,并把它的内容直接插入程序文本中。

  • 编译阶段。编译器将文本文件 hello.i 翻译成文本文件 hello.s:

    text

    main:
        subq    $8, %rsp
        movl    $.LC0, %edi
        call    puts
        movl    $0, %eax
        addq    $8, %rsp
        ret
  • 汇编阶段。汇编器将 hello.s 翻译成机器语言指令,把指令打包成可重定位目标程序(relocatable object program)的格式,并将结果保存在 hello.o 二进制文件中。

  • 链接阶段。hello 程序调用了 printf 函数,它标准 C 库中的一个函数,存在于 printf.o 的预编译好的目标文件中。这个文件必须以某种方式合并到 hello.o 程序中。链接器负责处理这种合并,生成 hello 的可执行目标文件,可以被加载到内存中,由系统执行。

此时,hello.c 源程序被编译系统翻译成了可执行目标文件 hello,并被存放在磁盘上。


为了理解运行 hello 程序时发生了什么,需要了解一个典型系统的硬件组织。

一个典型系统的硬件组成
一个典型系统的硬件组成
  • 总线。贯穿整个系统的是一组电子管道,称作总线,它携带信息字节并负责在各个部件间传输。

  • I/O 设备。I/O 设备是系统与外部世界的联系通道。每个 I/O 设备都通过一个控制器适配器与 I/O 总线相连。控制器是 I/O 设备本身或者系统主板上的芯片组;适配器是一块插在主板插槽上的卡。

  • 主存。主存是临时存储设备,在处理器执行程序时,用来存放程序和程序处理的数据。主存是由一组动态随机存取存储器(DRAM)芯片组成的。

  • 处理器。即中央处理单元(CPU),是解释(或执行)存储在主存中指令的引擎。处理器的核心是程序计数器(PC)。在任何时刻 PC 指向主存中的某条机器语言指令。

简单描述运行 hello 程序时到底发生了什么:

  1. 在 shell 程序中输入 “./hello” 后,shell 程序将字符逐一读入寄存器,再把它存放到内存中;

    从键盘上读取 hello 命令
    从键盘上读取 hello 命令

  2. 敲下回车键,shell 加载可执行的 hello 文件,利用直接存储器存取(DMA)技术,直接将 hello 目标文件中的代码和数据从磁盘复制到主存;

    从磁盘加载可执行文件到主存
    从磁盘加载可执行文件到主存

  3. 目标文件 hello 加载到主存,处理器执行 hello 程序的 main 程序的机器指令,指令将 “hello, world\n” 字符串从主存复制到寄存器文件,再从寄存器文件中复制到显示设备,最终显示在屏幕上。

    将输出字符串从存储器写到显示器
    将输出字符串从存储器写到显示器


处理器从寄存器文件中读取数据比从主存中读取几乎快 100 倍,针对这种差异,引入了高速缓存存储器(cache memory)。

利用高速缓存的局部性原理,程序具有访问局部区域里的数据和代码的趋势。通过让高速缓存里存放可能经常访问的数据,可以提高程序性能。

CPU 芯片的高速缓存
CPU 芯片的高速缓存

L1、L2、L3 高速缓存是用一种叫做静态随机访问存储器(SRAM)的硬件技术实现的。

每个计算机系统中的存储设备都被组织成了一个存储器层次结构。存储器层次结构的主要思想是上一层的存储器作为低一层存储器的高速缓存。

存储器层次结构示例
存储器层次结构示例

所有应用程序对硬件的操作都必须通过操作系统。操作系统有两个基本的功能:

  1. 防止硬件被失控的应用程序滥用;
  2. 向应用程序提供简单一致的机制来控制复杂而又通常大不相同的低级硬件设备。

操作系统通过几个基本的抽象概念来实现这两个功能,文件是对 I/O 设备的抽象表示,虚拟内存是对主存和磁盘 I/O 设备的抽象表示,进程是对处理器、主存和 I/O 设备的抽象表示。

操作系统提供的抽象表示
操作系统提供的抽象表示

进程是操作系统对一个正在运行的程序的一种抽象。好似程序是独占使用处理器、主存和 I/O 设备一样。

一个 CPU 看上去都是在并发地执行多个进程,是通过上下文切换实现的。一个进程到另一个进程的转换是由操作系统内核管理的。

进程的上下文切换
进程的上下文切换

虚拟内存为每个进程提供了一个假象,即每个进程都在独占地使用主存。每个进程看到的内存都是一致的,称为虚拟地址空间

进程的虚拟地址空间
进程的虚拟地址空间

进程虚拟地址空间从最低地址向上,逐步为:

  • 程序代码和数据。代码是从一固定地址开始,紧接着的是和 C 全局变量相对应的数据位置。代码和数据区直接按照可执行目标文件的内容初始化。该区域在进程一开始运行时就被指定大小。

  • 堆。当调用 malloc 和 free 这样的 C 标准库时,该区域可以在运行时动态扩展和收缩。

  • 共享库。大约在地址空间的中间部分,用来存放像 C 标准库和数学库这样的共享代码的数据和区域。

  • 栈。用户虚拟地址空间顶部是用户栈,编译器用栈来实现函数调用,每次调用一个函数,栈就会增长;从一个函数返回,栈就会收缩。

  • 内核虚拟内存。地址空间顶部区域是为内核保留的,不允许应用程序读写这个区域的内容。

技巧
关于代码和数据区的存放位置,可以参考之前整理的 汇编语言 —— 第一个完整的程序 —— 执行

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


现代系统通过网络和其他系统连接到一起。

网络也是一种 I/O 设备
网络也是一种 I/O 设备

当我们对系统的某个部分加速时,其对系统整体性能的影响取决于该部分的重要性和加速程度。

并发指一个同时具有多个活动的系统;并行指的是用并发来使一个系统运行得更快。

并行可以在计算机系统的多个抽象层次上运用,按照系统层次结构由高到低重点强调三个层次。

使用线程能够在一个进程中执行多个控制流。对于单处理器系统来说,并发执行是模拟出来的,是通过使一台计算机在它正在执行的进程间快速切换来实现的。

不同处理器分类
不同处理器分类

多处理器系统是由单个操作系统内核控制多个处理器的系统。多核处理器分为多核和超线程。一个典型多核处理器组织结构如下图所示:

多核处理器组织结构,4个处理器核集成在一个芯片上
多核处理器组织结构,4个处理器核集成在一个芯片上

其中微处理器芯片有 4 个 CPU 核,每个核有自己的 L1、L2 高速缓存,L1 高速缓存分为两个部分——一个保存最近取到的指令,另一个存放数据。

超线程,又称同时多线程,允许一个 CPU 执行多个控制流。

多处理器可以从两方面提高系统性能:

  1. 减少在执行多任务时模拟并发的需要;
  2. 使多线程应用程序运行得更快。

现代处理器可以同时执行多条指令的属性称为指令级并行。实现指令级并行的技术有:流水线。

如果处理器可以达到比一个周期一条指令更快的执行速率,称之为超标量处理器

许多现代处理器拥有特殊的硬件,允许一条指令产生多个可以并行执行的操作,这种方式称为单指令、多数据,即 SIMD 并行。比如现代 Intel 和 AMD 处理器都具有并行对 8 对单精度浮点数做加法的指令。

抽象是计算机科学中最为重要的概念之一。

在处理器里,指令集架构提供了对实际处理器硬件的抽象。虚拟机,提供对整个计算机的抽象,包括操作系统、处理器和程序。

操作系统提供的一些抽象
操作系统提供的一些抽象

  1. 了解一个系统的硬件组成,以及这些硬件如何协作来执行一个程序;
  2. 理解程序执行的局部性原理,引入高速缓存的原因,并理解存储设备的层次结构;
  3. 理解计算机系统中抽象的重要性,理解虚拟机、指令集架构、进程、虚拟内存和文件的抽象概念。