第1章 计算机系统漫游

参考资料

计算机系统漫游

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

#include<stdio.h>

int main()
{
	printf("hello world\n");
	return 0;
}
1
2
3
4
5
6
7

信息就是位 + 上下文

hello程序的生命周期从源程序(源文件)开始,即程序员通过编辑器创建并保存的文本文件,文件名为hello.c。源程序实际上就是由 0 和 1 组成的位(比特)序列,8个位被组织成一组,称为字节。每个字节表示程序中的某些文本字符。

信息就是 位 + 上下文

大部分的现代计算机系统都使用 ASCII 标准来表示文本字符,实际上是用一个唯一的单字节大小的整数值来表示每个字符。hello.c程序的ASCII码表示如下:

hello的ASCII码表示

hello.c程序是以字节序列的方式储存在文件中的。每个字节都有一个整数值,对应于某些字符。源文件中每个文本行都是以看不见的 '\n' 结束的。只由 ASCII 字符组成的文件成为文本文件,其他都是二进制文件。.cpp 文件就是文本文件。

hello.c的表示方法说明了一个基本思想:系统中所有的信息——包括磁盘文件、内存数据、以及网络数据等,都是由一串比特表示。区分不同数据对象的唯一方法是我们读到这些数据对象时的上下文。

比如,在不同的上下文中,一个同样的字节序列可能表示一个整数、浮点数、字符串或者机器指令。

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

gcc编译器读取hello.c文件,并翻译成一个可执行目标文件hello,需要经历四个步骤:

编译系统

  • 预处理阶段:预处理器(cpp)根据 # 开头的命令修改原始的 c 程序。比如根据 #include<stdio.h> 命令把头文件 stdio.h 的内容直接插入到程序文件中。结果就得到了另一个C程序,通常是以.i作为文件扩展名。
  • 编译阶段。编译器(cc1)将文本文件hello.i翻译成文本文件hello.s,它包含一个汇编程序语言。
  • 汇编阶段。汇编器(as)将hello.s翻译成机器语言指令,把这些指令打包成一种叫做可重定位目标文件的格式,并将结果保存在目标文件hello.o中。
  • 链接阶段。hello程序调用的 printf 函数是一个标准 C 库函数,存在于 printf.o 中,这是一个单独的预编译好了的目标文件。链接器将其与汇编得到的二进制文件合并得到可执行目标文件。

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

了解编译系统是如何工作的作用:

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

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

shell 是一个命令行解释器,它输出一个提示符(>>),等待输入一个命令行,然后执行命令。如果命令行的第一个单词不是内置的shell命令,则shell假设其为可执行文件的名称,并加载该文件。

系统的硬件组成

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

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

  2. I/O 设备 每个 I/O 设备通过一个控制器或适配器与 I/O 总线相连。控制器是 I/O 设备本身或主板上的芯片组,适配器则是一块插在主板上的卡。

扩展槽——可以接入千兆网卡,用于机械臂和相机的控制

典型系统的硬件组成

  1. 主存 从物理上看,主存是由一组动态随机存取内存(DRAM)组成的。 从逻辑上看,存储器是一个线性的字节数组,每个字节都有唯一的地址。

  2. 处理器 中央处理单元(CPU),简称处理器,是解释存储在主存中指令的引擎。 处理器的核心是一个程序计数器(PC):程序计数器是一个大小为一个字的存储设备,存储CPU即将执行的下一条指令的地址。 处理器就是在不断执行程序计数器指向的指令。每执行一条,程序计数器更新一次,指向下一条指令。 处理器会按照指令执行模型(指令集架构)解释指令中的位并执行相应操作。 每条指令的操作是围绕主存、寄存器文件、算数/逻辑单元(ALU)进行的。 寄存器文件:单个字长,有唯一的名字。 ALU:计算新的数据和地址值。 几个简单指令的操作: 加载:从主存复制一个字或字节到寄存器,覆盖原来内容 存储:从寄存器复制一个字或字节到主存,覆盖原来内容 操作:把两个寄存器的内容复制到 ALU,ALU 对这两个字做算术运算,并把结果存到一个寄存器中 跳转:从指令中抽取一个字复制到程序计数器中,覆盖原来内容。 区分处理器指令集架构和微体系架构: 指令集架构:每条机器指令的效果 微体系架构:处理器实际上是如何实现的

运行 hello 程序

初始时,shell程序执行它的指令,等待输入命令。当在键盘输入字符串 “./hello”后,shell程序将字符逐一读入寄存器,再把它放到内存中。如下图所示:

从键盘读取hello命令

当键盘上敲回车键时,shell程序知道用户命令输入完毕。然后shell执行一系列指令加载可执行的hello文件,这些指令将hello目标文件中的代码和数据从磁盘复制到主存。数据包括最终会被输出的字符串“hello, world\n”。

利用直接存储器存取(DMA)技术,数据可以不通过处理器而直接从磁盘到达主存。

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

一旦目标文件hello中的代码和数据被加载到主存,处理器就开始执行hello程序的main程序中的机器语言指令。这些指令将“hello, world\n”从主存复制到寄存器文件,再从寄存器文件复制到显示设备,最终显示在屏幕上。

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

整个流程:shell输入指令 → 读取文件字符到寄存器 → 存储到主存 → 点击回车,shell程序加载可执行文件 → 完成从磁盘读取代码和数据加载到主存 → 中央处理器(CPU)执行机器指令 → CPU计算得到结果 → 将结果从主存复制到寄存器 → 从寄存器复制到显示器 → 显示结果。

高速缓存至关重要

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

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

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

  • L1 高速缓存位于 CPU 上,容量为数万字节(几十 MB)。L1 比 L2 快 5 倍。
  • L2 高速缓存通过一条特殊的总线与 CPU 连接,容量为数十万到数百万字节(几百 MB 到 几 GB)。L2 比 主存快 5~10 倍
  • 新的系统还有 L3。

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

即原先只有寄存器文件与总线接口传递数据,现在可以通过高速存储器来处理。

高速缓存器

存储设备形成层次结构

每个计算机系统中的存储设备都被组织成一个存储器层次结构。在这个层次结构中,从上至下,设备的访问速度越来越慢、容量越来越大,并且每字节的造价也越来越便宜。 存储器层次结构

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

操作系统管理硬件

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

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

计算机系统的分层视图

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

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

操作系统提供的抽象表示

进程

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

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

一个系统可以同时运行多个进程,实际上这些进程是并发运行的。操作系统通过上下文切换来实现并发运行。

操作系统保持跟踪进程运行所需的所有状态信息,这种状态就是上下文,包括PC、寄存器文件值、主存等。任何时刻,单处理器只能执行一个进程的代码。当操作系统决定要把控制权从当前进程转移到某个新进程时,就会进行上下文切换,即保存当前进程的上下文、恢复新进程的上下文,然后将控制权传递到新进程。新进程就会从上次它停止的地方开始。

shell进程和hello进程的切换示例:

进程的上下文切换

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

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

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

线程

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

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

虚拟内存

机器级程序将内存视为一个庞大的字节数组,称为虚拟内存。内存的每个字节由地址来标识,所有可能地址的集合就是虚拟地址空间。

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

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

  • 程序代码和数据:对所有进程来说,代码都是从同一个固定地址开始,紧接着是与全局变量对应的数据区。代码和数据区都是按照可执行文件的内容初始化的。代码和数据区在进程开始运行时就被指定了大小。
  • 堆(运行时堆):运行时堆是根据 malloc 和 free 函数的调用在运行时动态地扩展和收缩的。
  • 共享库:地址空间的中间部分用来存放共享库的代码和数据。如 C 标准库、数学库等都属于共享库。
  • 栈(用户栈):用户栈和堆一样,在程序执行期间可以动态的扩展和收缩,编译器用它来实现函数调用。当调用函数时,栈增长,从函数返回时,栈收缩
  • 内核虚拟内存:地址空间顶部的区域是为内核保留的。不允许应用程序读写这个区域 的内容或者直接调用内核代码定义的函数。

进程的虚拟地址空间

文件

文件就是字节序列,仅此而已。每个 I/O 设备,包括磁盘、键盘、显示器、网络,都可以看成是文件。系统中所有输入输出都是通过使用一小组 称为 Unix I/O 的系统函数调用读写文件来实现的。

系统之间利用网络通信

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

网络IO设备

随着Internet这样的全球网络的出现,从一台主机复制信息到另外一台主机已经成为计算机系统的最重要的用途之一。例如,像电子邮件、即时通讯、万维网、FTP和telnet这样的应用都是基于网络复制信息的功能。

重要主题

Amdahl 定律

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

主要观点:要想显著加速整个系统,必须提升全系统中相当大的部分的速度。

并发和并行

区分并发与并行:

  • 并发:一个通用的概念,指一个同时具有多个活动的系统
  • 并行:用并发来使系统运行得更快。并行可以在多个抽象层次上运用。从高到低有以下三个层次
  1. 线程级并行 使用线程,能够在一个进程中执行多个控制流。

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

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

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

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

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

小结

  • 计算机系统由硬件和系统软件组成的,它们共同协作以运行应用程序
  • 处理器读取并解释存放在主存里的二进制指令。
  • 操作系统内核是应用程序和硬件之间的媒介,它提供了三个基本的抽象:文件、虚拟内存、进程。