计算机系统是由硬件和软件系统组成的,它们共同工作来运行应用程序。作为程序员,也需要了解这些组件是如何工作的,以及这些组件是如何影响程序的正确性和性能的。我们从最简单的 hello 程序开始,通过跟踪 hello 程序的生命周期(从被创建,到运行,输出,然终止),来开始对计算机系统的学习。
hello.c
1 |
|
hello 程序的生命周期是从源程序 hello.c 开始的,它实际上是由 0 和 1 组成的位(bit)序列,8 位称为一个字节。根据 ASCII 标准,每个字节表示一个特定的文本字符,如下图所示,hello.c 程序中的每个字符都对应一个单字节。
hello.c 的表示方法说明了一个基本思想:计算机系统中的所有信息,包括磁盘中的文件、存储器中的程序和数据、网络上传的数据,都是一串由 0 和 1 组成的位(bit)序列,区分不同数据对象的唯一方法是我们读到这些数据对象时的上下文。
前面创建的 hello.c 程序是一个高级 C 语言程序,我们能够读懂,但计算机系统不懂。必须把高级语言转化为计算机系统能够执行的低级机器语言指令,得到的是可执行目标文件 hello。从源程序文件 hello.c 到可执行目标文件 hello,编译过程分为四个阶段:预处理阶段、编译阶段、汇编阶段和链接阶段,这四个阶段分别由预处理器、编译器、汇编器和链接器完成,它们一起构成了编译系统。
此时,hello.c 源程序已经被编译系统翻译成可执行目标文件 hello,存放在磁盘上。在 Linux 系统的外壳(shell)中可以执行 hello。为了理解 hello 执行的时候发生了什么,我们先对一个典型系统的硬件组成进行简单介绍。
如图所示,典型系统的硬件组成包括总线、I/O设备、主存和处理器。
加载 | 存储 | 操作 | 跳转 |
---|---|---|---|
把一个字节/字从主存复制到寄存器,覆盖寄存器原来的内容。 | 把一个字节/字从寄存器复制到主存的某个位置。 | 把两个寄存器的内容复制到ALU,ALU它们进行运算,结果保存到一个寄存器中。 | 从指令本身抽取一个字,将其复制到 PC中。(即改变 PC 的地址实现跳转) |
首先我们在外壳(shell)输入字符串“./hello”以执行 hello 程序,外壳程序将字符逐一从输入设备读入 CPU 的寄存器,再把它放到主存中。
键入回车,外壳会执行一系列代码加载 hello,将 hello 的代码和数据从磁盘复制到主存,利用 DMA(直接存储器存取技术),数据可以不通过处理器而直接从磁盘达到主存。
hello 加载完之后,处理器开始执行 hello 程序中的 main 程序,将“hello. world\n”字符串从主存复制到寄存器文件,再从寄存器文件复制到显示设备,最终显示在屏幕上。
看完上面 hello 的执行过程,可以发现,系统花费了大量时间把信息从一个地方传到另一个地方,系统设计者的一个主要目标是使这些复制操作尽可能快地完成。针对处理器与主存之间速度的差异,系统设计者用更小、更快的存储设备,即高速缓存存储器,作为暂时的集结区域,用于存放处理器近期可能会需要的信息。
上图展示了一个典型系统的中高速缓存,处理器芯片上的 L1 高速缓存大小可达数万字节,访问速度几乎和访问寄存器文件一样快。还有 L2 ,甚至 L3 高速缓存,随着容量增大,访问速度变慢。高速缓存是用 SRAM (静态随机访问存储器)的硬件技术实现的。有了高速缓存,程序的执行性能可以提高一个数量级。
实际上,除了在处理器和主存之间插入多级高速缓存,每个计算机系统中还有更复杂的存储器层次结构,如下图所示,这个层次结构从上到下,访问速度越来越慢,容量越来越大,并且每个字节的造价也越来越便宜。L0 为寄存器文件,L1 到 L3 为三级高速缓存,然后是主存、本地磁盘,最后是远程二级存储。存储器层次结构的主要思想是:一层上的存储器是低一层存储器的高速缓存,如寄存器文件是 L1 的高速缓存,L1 是 L2 的高速缓存等。
当我们加载和运行 hello 程序,以及 hello 程序输出时,外壳和 hello 程序都没有直接访问键盘、显示器、磁盘和主存。它们依靠操作系统来管理硬件。操作系统有两个基本功能:(1)防止硬件被失控的应用程序滥用;(2)向应用程序提供了简单一致的机制来控制复制而又大相径庭的硬件设备。操作系统是通过几个基本的抽象概念来实现这两个功能的。
hello 程序运行时,操作系统会提供一个假象,就好像系统上只有这个程序在运行。这是通过进程的概念来实现的,进程是计算机科学中最重要和最成功的概念之一。进程是操作系统对正在运行程序的一种抽象,一个系统可以同时运行多个进程,这种并发运行时通过处理器在进程间的切换(称为上下文切换)实现的。实际上,在任何时刻,单处理器系统都只能执行一个进程的代码。
如上图所示,有两个进程:外壳进程和 hello 进程。一开始只有外壳进程在运行,输入“./hello”命令后,系统调用将控制权传递给操作系统,操作系统保存当前外壳进程的上下文,并创建新的 hello 进程及其上下文,将控制权传递给新的 hello 进程。hello 进程终止后,操作系统恢复外壳进程上下文,并将控制权传回给它,外壳进程等待下一个命令输入。
通常我们认为一个进程只有单一控制流,如 hello 进程只有 main 控制流。但是在现代系统中,一个进程可以由多个称为线程的执行单元组成,每个线程都运行在进程的上下文中,并共享同样的代码和全局数据。一般来说,线程比进程更高效,所以多线程之间比多进程之间更容易共享数据。
虚拟存储器是一个抽象的概念,它为每个进程提供了一个假象:每个进程都在独占使用主存。每个进程看到的是一致的存储器,称为虚拟地址空间。
每个进程的虚拟地址空间由大量准确定义的区构成,每个区都有专门的功能。
虚拟存储器的运作需要硬件和操作系统之间精密复杂的交互,其基本思想是把一个进程虚拟存储器的内容存储在磁盘上,然后用主存作为磁盘的高速缓存。
文件就是字节序列,仅此而已。每个 I/O 设备,包括磁盘、键盘、显示器,甚至网络,都可以视为文件。它向应用程序提供了一个统一的视角,来看待系统中可能含有的所有各式各样的 I/O 设备。
在上述计算机系统漫游中,我们得出重要结论:计算机系统不仅仅只是硬件。系统是硬件和软件互相交织的集合体,它们必须共同协作以达到运行应用程序的最终目的。我们来再来讨论几个贯穿计算机系统的重要概念作为结尾。
我们需要计算机做得更多,也需要它运行得更快,怎么办呢?因此引入了并发,并发是一个通用的概念,指一个同时具有多个活动的系统;并行是指用并发使一个系统运行得更快。并行有三个层次,由高到低分别是:线程级并发;指令级并行;单指令多数据并行。
抽象是计算机科学中最为重要的概念之一。例如,在编程中可以为函数提供抽象的接口,程序员无需了解函数内部工作机制就可以使用这些代码。如文件是对 I/O 设备的抽象;虚拟存储器是对主存和磁盘 I/O 的抽象;进程是对一个正在运行的程序的抽象;而虚拟机是对整个计算机(包括操作系统、处理器和程序)的抽象。
由一个 hello 程序的生命周期对整个计算机系统进行了快速的漫游。从中我们知道了,计算机系统是由硬件和软件组成的,共同协作以运行程序。计算机内部的信息实际上是一系列位序列,根据上下文有不同的解释方式。编译系统将源程序经过预处理、编译、汇编和链接四个步骤翻译成可执行目标文件。由于程序运行时,信息常常由一个地方被复制到另一个地方,为了提高传递速度,提出了存储器层次结构,由高到低分别是寄存器文件,L1 高速缓存,L2 高速缓存,L3 高速缓存,主存,磁盘,远程存储器等,由上到下,容量依次变大,但速度越来越慢,每个字节的造价也越来越便宜。
另外,应用程序并不是直接操纵计算机硬件,而是通过操作系统代为管理,这样可以避免失控的应用程序对系统硬件的滥用,同时为应用程序提供了简单统一的机制来控制复杂多样的硬件。具体是通过进程、虚拟存储器和文件等抽象概念实现的。最后还讨论了三个不同层次的并发机制,并说明了计算机系统中抽象的重要性。
参考:《Computer Systems A Programmer’s Perspective》