深入理解计算机系统(一)

系统漫游

Hello world

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

1
2
3
4
5
6
7
#include<stdio.h>

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

hello.c程序是以字节序列的方式存储在文件中的,每个字节都有一个整数值,对应于某些字符。例如第一个字节的整数值是35,对应的字符是“#”。需要注意的是,每个文本行都是以一个看不见的换行符“\n”来结束的,像hello.c这样只由ASCII字符构成的文件称为文本文件,所有其他文件都称为二进制文件。

hello.c的表示方法说明了一个基本思想,系统中所有的信息包括磁盘文件、内存中存放的用户数据以及网络上传送的数据,都是由一串比特表示的。区分不同数据对象的唯一方法是我们读到这些数据对象时的上下文。在不同上下文中,一个发相同的字节序列可能表示一个整数、浮点数、字符串或者机器指令。

生命周期

hello程序的生命周期是从一个高级C语言程序开始的,因为这种形式能够被人读懂,然而为了在系统上运行hello.c程序,每条C语言都必须被其他程序转化为一系列的低级机器语言指令。然后这些指令按照一种称为可执行目标程序的格式打好包。并以二进制磁盘文件的形式存放起来,目标程序也被称为可执行目标文件。

在Unix系统上,从源文件到目标文件的转化是由编译器驱动程序完成的。

1

在这里,GCC编译器驱动程序读取源程序文件hello.c,并把它翻译成一个可执行目标文件hello。这个翻译过程可分为以上四个阶段完成。执行这四个阶段的程序一起构成了编译系统。

  • 预处理阶段。预处理器根据以字符#开头的命令,修改原始的C程序。比如hello.c中第一行的命令告诉预处理器读取系统头文件stdio.h的内容,并把它直接插入程序文本中。结果就得到了另一个C程序,通常是以.i作为文件扩展名。
  • 编译阶段。编译器将文本文件hello.i翻译成文本文件hello.s,它包含一个汇编语言程序,该程序包含函数main的定义。
  • 汇编阶段。汇编器将hello.s翻译成机器语言指令,把这些指令打包成一种叫做可重定位目标程序的格式,并将结果保存在目标文件hello.o中,hello.o文件是一个二进制文件,它包含的17个字节是函数main的指令编码。
  • 链接阶段。hello程序调用了printf函数,它是每个C编译器豆提供的标准C库中的一个函数。printf函数存在于一个名为printf.o的单独的预编译好了的目标文件中,而这个文件必须以某种方式合并到我们的hello.o程序中,链接器就负责处理这种合并,结果就得到hello文件,它是一个可执行目标文件,可以被加载到内存中,由系统执行。

系统硬件组成

  • 总线:贯穿整个系统的是一组电子管道,称作总线,它携带信息字节并负责在各个部件间传递。通常总线被设计成传送定长的字节块,也就是字。
  • I/O设备:I/O设备是系统与外部世界的联系通道。,我们的示例系统包括四个I/O设备:作为用户输入的键盘和鼠标,作为用户输出的显示器,以及用于长期存储数据和程序的磁盘启动器,最开始,可执行程序就存放在磁盘上。每个I/O设备都通过一个控制器或适配器与I/O总线相连。控制器和适配器之间的区别主要在于它们的封装方式。
  • 主存:主存是一个临时存储设备。在处理器执行程序时,用来存放程序和程序处理的数据,从物理上来说,主寸是由一组动态随机存取存储器(DRAM)芯片组成的。从逻辑上来说,存储器是一个线性的字节数组,每个字节都有其唯一的地址,这些地址是从零开始的。一般来讲,组成程序的每条机器指令都由不同数量的字节构成。
  • 处理器:处理器是解释存储在主存中指令的引擎,处理器的核心是一个大小为一个字的存储设备,称为程序计数器(PC)。在任何时刻,PC都指向主存中的某条机器语言指令(即含有该条指令的地址)

一个典型系统的硬件组成如下图所示

2

程序的运行

  • 初识时,shell程序执行它的指令,等待我们输入一个命令。当我们在键盘上输入字符串./hello时,shell程序将字符逐一读入寄存器,再把它存放到内存中。

  • 当我们在键盘上敲回车键时,shell程序就知道我们已经结束了命令的输入,然后shell执行一系列指令来加载可执行的hello文件,这些指令将hello目标文件中的代码和数据从磁盘复制到主存。

  • 通过DMA,数据可以不通过处理器而直接从磁盘到达主存。
  • 一旦hello中的代码和数据被加载到主存,处理器就开始执行hello程序中的main程序中的机器语言指令,这些指令将”hello,world\n”字符串中的字节从主存复制到寄存器文件,再从寄存器文件中复制到显示设备。最终显示在屏幕上。

高速缓存的重要性

没有高速缓存,系统将会花费大量的时间把信息从一个地方挪到另一个地方,例如机器指令从磁盘复制到主存,从主存复制到处理器。数据也是如此,从磁盘复制到主存,从主存复制到显示设备。根据机械原理,较大的存储设备要比较小的存储设备运行的慢。类似的,一个典型的寄存器文件只存储几百字节的信息,而主存里可存放几十亿字节,然而,处理器从寄存器文件中读取数据比从主存中读取数据几乎要快100倍。随着近几年半导体技术的进步,这种处理器与主存之间的差距还在持续增大。针对这种处理器与主存之间的差异,系统设计者采用了更小更快的存储设备。称为高速缓存存储器。作为暂时的集结区域,存放处理器近期可能会需要的信息。

3

操作系统管理硬件

我们可以把操作系统看成是应用程序和硬件之间插入的一层软件,所有应用程序对硬件的操作尝试都必须通过操作系统。
操作系统有两个基本功能

  • 防止硬件被失控的应用程序滥用

  • 向应用程序提供简单一致的机制来控制复杂而又通常大不相同的低级硬件设备

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

进程

进程是操作系统对一个正在运行的程序的一种抽象。在一个系统上可以同时运行多个进程,而每个进程都好像在独占地使用硬件。并发运行,则是说一个进程的指令和另一个进程的指令是交错执行的,在大多数系统中,需要运行的进程数是多于可以运行它们的CPU个数的。传统系统在一个时刻只能执行一个程序,而先进的多核处理器同时能够执行多个程序。这具体的表现在,无论是在单核还是在多核系统中,一个CPU看上去都像是在并发的执行多个进程,这是通过处理器在进程间切换来实现的,操作系统实现这种交错执行的机制称为上下文切换。

4

如上图所示,从一个进程到另一个进程的转换是通过操作系统内核来管理的,内核是操作系统代码常驻主存的部分,当应用程序需要操作系统的某些操作时,比如读写文件,它就执行一条特殊的系统调用指令,将控制权传递给内核,然后内核执行被请求的操作并返回应用程序。(内核不是一个独立的进程,它是系统管理全部进程所用代码和数据结构的集合)

线程

一个进程实际上可以由多个称为线程的执行单元组成,每个线程都运行在进程的上下文中,并共享同样的代码和全局数据。多线程之间比多进程之间更容易共享数据,并且线程一般来说都比进程更高效,当有多处理器可用的时候,多线程也是一种使得程序可以运行得更快的方法。

虚拟内存

虚拟内存是一个抽象概念,它为每个进程提供了一个假象,即每个进程都在独占地使用主存,每个进程看到的内存都是一致的,称为虚拟地址空间。在Linux中,地址空间最上面的区域时保留给操作系统中的代码和数据的,这对所有进程来说都是一样的,地址空间的底部区域存放用户进程定义的代码和数据。

5

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

并发和并行

线程级并发

使用线程,我们能够在一个进程汇总执行多个控制流,自20世纪60年代初期出现时间共享以来,计算机系统中就开始有了对并发执行的支持。传统意义上,这种并发执行只是模拟出来的,是通过使一台计算机在它正在执行的进程间快速切换来实现的。在以前,即使处理器必须在多个任务间切换,大多数实际的计算也都是由一个处理器来完成的,这种配置称为单处理器系统。当构建一个由单操作系统内核控制的多处理器组成的系统时,我们就得到了一个多处理器系统。多核处理器是将多个CPU集成到一个集成电路芯片上,如下图所示为多核处理器的组织架构。

6

指令级并行

在较低的抽象层次上,现代处理器可以同时执行多条指令的属性称为指令级并行。通过使用流水线技术,将执行一条指令所需要的活动划分为不同的步骤,将处理器的硬件组织成一系列的阶段,每个阶段执行一个步骤。这些阶段可以并行地操作,用来处理不同指令的不同部分。

单指令、多数据并行

在最低层次上,许多现代处理器拥有特殊的硬件,允许一条指令产生多个可以并行执行的操作,这种方式称为单指令、多数据,即SIMD并行。

计算机系统的抽象表示

7

计算机系统提供了不同层次的抽象表示来隐藏实际实现的复杂性。如上图所示,在处理器里,指令集架构提供了对实际处理器硬件的抽象,使用这个抽象,机器代码程序表现的好像运行在一个一次只执行一条指令的处理器上。对于我们而言,底层的硬件要比抽象描述更加复杂,它并行的执行多条指令,但又总是与那个简单有序的模型保持一致,只要执行模型一样,不同的处理器实现也能执行同样的机器代码,而又提供不同的开销和性能。