[十二天学完《深入理解计算机系统》系列] 第一章 计算机系统漫游

一 章节概述


  什么是计算机系统?它和操作系统有何不同?我们为什么要深入理解计算机系统?概括的讲计算机系统是由硬件和系统软件共同组成的,它们通过共同的协作来运行应用程序。而我们在日常编写程序时使用的文件、内存、进程等概念都是为了程序编写的方便,由实际的计算机系统高度抽象而来。这一章中我们使用 c 语言编写简单的 hello 程序,试图通过跟踪 hello 程序的生命周期来窥探计算机系统的全貌。

#include <stdio.h>

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






二 计算机系统漫游


1. 何为信息


  现在人们常见的计算机大多继承了冯诺依曼结构的灵活性,即指令和数据不加区别的存储在存储器中。实际上在存储器中所有文件都是由 0 或 1 这样的位组成的序列,八个位被组成一个字节。同理在 hello 程序的生命周期之初,其源程序也就是 hello.c 是程序员通过编辑器创建的文本文件,文件由二进制位组成的一个个字节组成,每个字节则通过一张字符表表示成对应的字符。
  大部分计算机都使用 ASCII 表来表示字符,这种方法实际上就是用单个字节大小对应的数值来表示单个字符的。下图中给出 hello.c 程序的 ASCII 码表示。

  hello.c 的表示方法表明了一个思想:系统中所有的信息———磁盘文件、内存中的程序、网络传输的数据都是一串比特表示的。一个同样的字节序列可能表示整数,也可能表示浮点数、字符串或指令。像这样区分不同数据对象类别的唯一方法就是根据读这些数据对象时的上下文,就像程序中的类型声明一样。




2. 源程序是如何变成可执行文件的


  hello.c 程序是如何从一个由 c 语言编写的文本文件,变成一个真正的可以运行的程序的呢? c 语言是一门由汇编语言抽象而来的高级语言,而汇编语言则同样是由十六进制组成的低级机器语言抽象而来。在计算机发展历史中,程序语言逐渐有晦涩难懂抽象成接近自然语言,经历了 机器语言->汇编语言->高级语言(如C语言),机器语言在计算机中可以直接执行。
  那么将 hello.c 源程序变成一个可以直接运行的程序需要经历与上述恰恰相反的过程,分为四个阶段:预处理、编译、汇编、链接,将 c 语言逐步翻译成汇编语言,再翻译成机器语言。而这四个阶段就构成了一个编译系统。
下图中展示了 hello.c 的编译过程:

  • 预处理:根据 .c 文本中以 # 开头的命令修改原始 hello.c ,并生成 hello.i 文件。

  • 编译:将 hello.i 翻译成汇编语言代码 hello.s 。

  • 汇编:将汇编语言代码 hello.s 翻译成机器语言指令,打包成一个可重定位目标程序 hello.o 。

  • 链接:将多个 .o 文件链接成可执行文件 hello ,例如 hello.c 中使用了库函数 printf ,而 printf 存在一个名为 printf.o 的目标文件中,链接就是将这些如 hello.o 、printf.o 以 .o 结尾的目标文件合并成可执行文件 hello 。

上述文件中 hello.c hello.i hello.s 均为文本文件,hello.o hello 为二进制文件。




3. 处理器如何读并解释内存中的指令


  上文中我们已经获得了可执行文件 hello 。接下来如果要运行它就会通过在 shell 中输入

linux> ./hello
hello, world

  把它加载到内存中运行,shell 是操作系统中的命令行解释器,它将我们输入的文件名和回车解释为执行 hello 程序。于是 hello 程序将会被加载到内存中去执行,并在执行过程中输出程序中的 “hello, world” ,那么在计算机内程序的执行经历了哪些过程是值得探究的。我们在文章开头提到,计算机系统是由计算机的软硬件共同组成,在这里我们就需要先了解计算机系统的硬件组成。


3.1 计算机系统的硬件组成

计算机硬件由总线I/O设备主存处理器四个主要部分组成。

  • 总线:像图中画的一样是贯穿整个系统的电子管道,用于数据和指令在计算机不同组件之间传输和控制。通常总线被设计成传送定长的字符块,即字。在32位机中一个字4个字节(4 * 8 = 32位),在64位机中一个字8个字节 (8 * 8 = 64位)。

  • I/O设备:I/O(input/output)设备,是系统与外部世界的联系通道,鼠标、键
    盘、显示器、一起磁盘都是I/O设备。(我们的 hello 最开始就存放在磁盘上)

  • 主存:主存就是我们常说的运行内存,由一组动态随机存储器(DRAM)组成。在主存中程序运行时变量的数据大小是根据类型变换的。比如在运行 linux 的 x86-64 机器上,short 类型2个字节、int 和 float 类型四个字节,而 long 和 double 则需8个字节。

  • 处理器:中央处理的单元(CPU),是解释或执行存储在主存中指令的引擎。处理器主要由程序计数器PC算术/逻辑单元ALU还有寄存器组成。程序计数器PC也是一个大小一个字的寄存器,在任何时候PC指向主存中的某条机器语言指令。算术/逻辑单元ALU则来实现算术和逻辑的运算。

处理器的运行过程:

  • 加载:从主存中加载数据到寄存器
  • 存储:从寄存器中复制数据到主存
  • 操作:把两个寄存器的内容复制到ALU做算术运算,将结果存放在一个寄存器中。
  • 跳转:从指令中抽取一个字复制到PC中以覆盖原来内容。


3.2 hello程序在计算机运行经历的过程


  • 首先我们在shell上通过键盘输入"hello"时,shell将"hello"字符串从I/O设备键盘经由I/O总线逐一读到寄存器中,再把它放到内存。
  • 然后当我们在键盘上敲下回车的时候,shell程序便知道我们已经结束命令的输入。于是shell通过一系列命令将hello目标文件从磁盘中复制到主存。
  • 一旦目标文件被加载到主存,CPU便开始执行hello程序中main函数中的机器语言指令。这些指令将"hello, world\n"字符串从主存中复制到寄存器,在从寄存器复制到I/O显示器,最终显示到屏幕上。




4. 计算机中存储器的层次结构


  上文中我们已经介绍了 hello 文件在计算机中的执行过程,不难发现一个问题,即系统花了大量的时间把信息从一个地方挪到另一个地方。根据机械原理,容量越大的存储设备往往速度越慢,而主存和磁盘的读写速度则远小于CPU的处理速度。这样就造成了速度的不匹配,使运行 hello 程序时,在传输数据的过程中浪费了很多时间。
  针对这种差异,于是设计者开发了更小更快的存储设备,成为高速缓存存储器(cache memory),并将其放在处理器中直接与寄存器连接,将处理器常用的数据存放在其中,避免了频繁的从主存或磁盘中传输数据。如现在计算机处理器常有L1和L2高速缓存,处理能力更强大的处理器甚至有三级告诉缓存:L1、L2和L3。
  在计算机中,存储器的速度由快到慢,容量由小到大形成了计算机的存储器层次结构:

  存储器层次结构的主要思想是高一层存储器作为低一层存储器的告诉缓存,因此寄存器是L1的缓存,L1是L2的缓存,L2是L3的缓存,L3是主存的缓存,主存是磁盘的缓存,在某些分布式系统中本地磁盘又是其他系统上磁盘的缓存。善于运用这些缓存可以将计算机的速度提升一个量级。




5. 操作系统对计算机系统使用的抽象


  让我们回到 hello 程序的例子,当shell加载运行hello程序,以及输出显示"hello world",shell和hello程序都没有直接访问主存、磁盘、显示器或键盘。取而代之它们依赖的是操作系统提供的服务,即向应用程序提供的系统调用。

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

  • (1)防止硬件被应用程序滥用。
  • (2)向应用程序提供简单一致的控制复杂又不相同的低级硬件设备的方法。

而操作系统是通过几个基本抽象概念(进程/线程、虚拟内存、文件)来实现功能的,如图:


5.1 操作系统提供的抽象:进程


  像 hello 这样的程序在现代计算机上运行容易造成一种假象,就好像系统上只有这一个程序在运行,程序看上去在独占处理器、主存、和I/O设备。处理器看上去·就像在一条接一条的执行程序中的指令,即 该程序的代码和数据是系统内存中唯一的对象。 这些假象是由进程概念实现的,进程概念是计算机科学中最成功和最重要的概念之一。
  实际上一个计算机上执行了多个进程,但是为了看上去是一个CPU在并发的执行多个进程,操作系统的设计者们通过上下文切换这一概念来实现。操作系统会保存正在运行的进程们的状态信息,并且由操作系统的内核管理从多个进程之间来回切换。内核不是一个进程,内核是操作系统代码常驻主存的部分,是系统管理全部进程所用代码和数据结构的集合。


5.2 操作系统提供的抽象:线程


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


5.3 操作系统提供的抽象:虚拟内存


  虚拟内存同样是一个抽象概念,它为每个进程提供了一个假象,即每个进程在独占的使用主存。每个进程看到的内存都是一样的,称为虚拟地址空间。在Linux中,虚拟地址空间的模型如下:

图中的地址是由下往上增大的,其中

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


5.3 操作系统提供的抽象:文件


  文件就是字节序,仅此而已。每个I/O设备:磁盘、键盘、显示器、甚至网络等等,都可以看成文件。文件的概念是简单而精致并且非常强大的,它为应用程序提供了一个统一的视图来看待系统中各种各样的I/O设备。




6. 网络


  计算机系统漫游至此我们一直把它视为一个孤立的硬件和软件的集合体。实际上现代计算机经常通过网络和其他设备连接到一起。从一个单一的系统来看,网络可以视为一个I/O设备。如图,当系统复制一串字节到网络适配器时,数据流经过网络到达另一台机器,相似的,系统也可以读取从其他机器发来的数据。

  在现代计算机中客户端和服务器之间的交互是很常见的,就像一些连接服务器用的远程客户端,例telnet。我们可以将它连接到服务器并且运行服务器上的 hello 程序。




7. 重要补充


7.1 Amdahl定理


  当我们对系统的某个部分加速时,如何度量系统的性能提升呢。 S = T o l d / T n e w S=T_{old}/T_{new} 是最好的办法, T o l d T_{old} 为原始系统运行的时间, T n e w T_{new} 为修改后系统运行的时间。在Amdahl定理中我们假设原始系统的某一部分执行时间与原始系统整体的执行时间之比为 α \alpha ,而该部分性能提升比例为 k k ,那么提升后系统的执行时间为
T n e w = ( 1 α ) T o l d + ( α T o l d ) / k = T o l d [ ( 1 α ) + α / k ] T_{new}=(1-\alpha)T_{old}+(\alpha T_{old})/k=T_{old}[(1-\alpha)+\alpha/k]
由此可以计算出加速比 S = T o l d / T n e w S=T_{old}/T_{new}
S = 1 ( 1 α ) + α / k S=\frac{1}{(1-\alpha)+\alpha/k}
我们可以看出,即使 k k \to \infty ,加速比仍为 S = 1 1 α S= \frac{1}{1-\alpha} ,也就是说想要显著加速整个系统,必须提升全系统中相当大部分的速度。


7.2 并发和并行


  在计算机的发展中始终有两个需求,做更多和运行更快。当处理器能同时做很多事情时这两个因素都会得到改善。术语并发是一个通用的概念,指具有多个活动的系统;而术语并行指的是用并发来使一个系统运行的更快。并行可以用在计算机系统的多个抽象层次,以下按照计算机系统层次的由高到底重点强调三个层次。

  • 线程级并发
    构建在进程这个抽象之上,我们能够设计出同时有多个程序执行的系统,这就导致了并发。在单处理系统时代,这种多线程并发只是模拟出来的,通过计算机在它执行的进程间快速切换来实现的。直到多核超线程技术的出现,使得了程序可以利用硬件实现真正的线程级并发性。多核处理器时代,处理器将多个CPU集成到一个集成电路上;还出现了超线程技术,超线程有时被成为同时多线程,是一个允许CPU执行多个控制流的技术,例如Intel Core i7处理器可以让每个核执行两个线程。传统处理器线程的切换大约需要20 000个时钟周期,而超线程技术通过对CPU某些硬件的备份,例如PC和寄存器,是CPU能够在单个时钟周期的基础上决定执行那个线程。

  • 指令级并行
    在低一些的层次上,现在处理器可以通过同时执行多条指令的属性称为指令级并行。通过流水线技术,将执行每条指令所需的步骤(取值、译码、执行等)根据它们使用的硬件资源不同将多个指令不发生冲突的步骤放到一起并行执行,使得处理器达到接近一个时钟周期执行一条指令的执行速度。如果处理器可以达到比一个周期一条指令更快的执行速率,就称之为超标量处理器。现代大多数处理器都支持超标量操作。

  • 单指令、多数据并行
    在最低的层次上,许多现代处理器拥有特殊的硬件,允许一条指令产生多个可以并行的操作,这种方式被称为单指令、多数据,即SIMD并行。例如,较新几代的Intel和AMD处理器具有并行的对8对单精度浮点数做加法的指令,可以提高影像、声音和视频数据应用的执行速度。


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


  抽象的使用是计算机科学中最为重要的概念之一。类如程序语言中面向对象的抽象方法。我们在上文中介绍了计算机系统中使用的几个抽象,如图。在处理器,指令集架构提供了对实际处理器硬件的抽象,使用这个抽象,机器代码好像运行在一个一次执行一条指令的处理器上。在学习操作系统时我们还介绍了三个抽象:文件是对I/O设备的抽象;虚拟内存是对程序存储器的抽象,即对I/O设备和主存的抽象;进程是对一个正在执行的程序的抽象。在此我们在增加一个新的抽象:虚拟机,它提供了对整个计算机的抽象,包括操作系统、处理器、和程序。

发布了1 篇原创文章 · 获赞 3 · 访问量 723

猜你喜欢

转载自blog.csdn.net/qq_41948200/article/details/104464102