我们通过跟踪这个hello程序来系统性的了解一个程序是如何被系统执行的。
#include <stdio.h>
int main(){
printf("hello world");
}
一、信息的本质
一个C语言程序的生命周期从一个源程序开始,程序员用编辑器创建并保存的文本文件,文件后缀为.c
。这个源程序本质上是一个由0和1组成的位(也叫bit)序列,8个一组,被称为字节。每个字节表示一个字符。(注:现在的大部分系统都用ASCII标准来表示字符。)
该图是hello程序的ASCII文本表示
系统中的所有信息都是如hello程序一样由一串位组成的——包括磁盘文件、存储器中的程序、存储器中存放的用户数据以及网络上传输的数据。因此,我们可以说信息的本质就是一串位。(注:在不同的上下文中,一个相同的字节序列可能表示不同的意思。比如,一个数字在不同的上下文中可以表示为整数、浮点数等。)
二、被翻译成不同形式的hello程序
hello程序被创建之初是一个高级的C语言程序,这种形式可以被我们读懂,但无法被计算机读懂。因此,hello程序必须被翻译成计算机能够“明白”的形式。每条C语句都要被转化为一系列的低级机器语言指令。这些指令按照可执行目标程序的格式打包,然后以二进制磁盘文件的形式保存起来。该文件也被称为可执行目标文件。
这个翻译过程分为四个阶段,由四个程序执行——预处理器、编译器、汇编器、链接器,它们一起构成了编译系统(compilation system)。
- 预处理阶段。预处理器(即cpp)根据以
#
号开头的命令,修改源代码。如hello.c中第一行#include <stdio.h>
,预处理器通过按照该命令读取头文件stdio.h
中的内容,插入源代码中,得到hello.i。 - 编译阶段。编译器(即ccl)将hello.i翻译成汇编语言程序文件hello.s。汇编语言是二进制指令的文本形式,汇编语言程序中的每条语句都以一种标准的文本格式表示了一条低级机器指令。它为不同高级语言的不同编译器提供了通用的输出语言。
- 汇编阶段。汇编器(即as)将hello.s语言翻译成机器语言指令,这些指令最终会被打包成***可重定位目标程序(relocatable object program)***的格式,保存在二进制文件hello.o中。它的字节编码是机器语言指令。
- 链接阶段。在我们的hello程序中,我们调用了
printf
函数,该函数在一个名为printf.o的目标文件(注:已经预编译好了)中。链接器(即ld)将这个文件合并到我们的hello.o中,就得到了一个可执行文件hello。该文件将加载到内存中,由系统执行。
三、处理器读取并解释指令
现在,我们的hello.c源程序已经被翻译成立可执行文件hello,保存在磁盘上。接下来就是执行,比如,在linux中我们通过shell(外壳)执行:
Linux> ./hello
hello world
Linux>
注:shell是一个命令行解释器,我们输入一个命令行,shell会执行这个命令。Shell的本质是一个应用程序,它连接了用户和 Linux 内核,让用户能够更加高效、安全、低成本地使用 Linux 内核。
在解释系统是如何执行可执行文件hello前,我们需要了解系统的硬件组成。
1、系统的硬件组成
我们通过一张图来解释系统的硬件组成。
- 总线。一个贯穿系统的电子管道,它的主要作用是在各个部件之间传递信息。一般来说,总线会传输固定的字节块,也就是字(word)。字中的字长(即字节数)是一个基本的系统参数。主要有4个字节(32位)与8个字节(64)两种。
- I/O设备。I/O设备是系统与外部的连接通道。一般我们的电脑有四个I/O设备——键盘、鼠标、显示器以及磁盘。可执行文件hello就存放在磁盘上。
- 主存。主存是一个临时存储设备,用来存储程序与程序处理的数据。从逻辑上来说,主存是一个线性的字节数组,每个字节都有唯一的地址(从0开始)。
- 处理器。处理器也叫CPU,是执行存储在主存中指令的引擎。该部件核心是一个存储设备(即寄存器),或者叫程序计数器(即PC)。程序计数器的主要作用是指向主存中的一条机器指令。处理器会不断的执行程序计数器指向的指令。
注:如果你对系统稍微有点了解的话,你会知道系统分为32位于64位。CPU一次处理数据的能力是32位还是64位,关系着系统需要安装32位还是64位的系统。
在了解到上述的硬件知识后,我们现在可以真正执行hello程序了。
2、执行hello程序
在一开始,我们通过shell执行 ./hello
命令。而在之后,shell会将字符一个一个读入寄存器,再加载到主存中。一旦hello中的代码与数据被加载到主存中,处理器就开始执行hello程序中的main程序中的机器语言指令。执行后,得到"hello world"
字符串,这些字符串将从主存复制到寄存器中,再从寄存器中复制到显示器,最终我们就看到了。
四、高速缓存
现在,hello程序的整个生命周期就已经结束了。但是我们会发现,在这个例子中,系统把大量的时间用在传输信息中。这本身没什么问题,但是在CPU、主存、磁盘巨大的访问速度的差距下,就会造成CPU被大幅度浪费。
针对这种处理器与主存之间的访问速度的差距,高速缓存存储器出现了。高速缓存可作为暂时的存储空间,用来存储处理器最近会使用的数据。一般来说,我们有L1、L2以及L3三种高速缓存。
- L1高速缓存一般位于处理器上,它的访问速度可以与寄存器一样快,当然容量也会比L2高速缓存要小。
- L2高速缓存容量可以从数十万到数百万,一般通过一条特殊的总线连接到处理器。访问速度要比L1高速缓存慢5倍,但是仍然比访问主存要快5~10倍。
- 处理能力更强大的系统在有L1高速缓存与L2高速缓存后,还有L3高速缓存。L3高速缓存是一个很大的存储器,访问速度也很快,它使用了高速缓存的局部性原理,即程序可以访问局部区域里的数据和代码。
注:在处理器与主存之间插入一个高速缓存已经是一个普遍的做法了。目前的计算机系统的存储设备已经组织成了一个存储器层次结构。
五、操作系统的基本功能
我们再回到hello程序中来讨论另一个问题。当hello被加载和执行时,以及hello程序输出时,hello程序没有直接访问I/O设备或者主存,而是通过操作系统提供的服务。因此,我们可以把操作系统当做是硬件与程序之间的一层软件,如下图所示。所有的程序都需要通过操作系统才能对硬件进行操作。
由此,我们可以总结出操作系统的两大基本功能:1)防止硬件被程序随意滥用。2)屏蔽掉低级硬件设备的复杂性,向程序提供一个简单一致的机制来控制硬件设备。
操作系统实现这两个功能主要是通过三个基本抽象概念——进程、虚拟存储器和文件。其中,文件是对I/O设备的抽象表示,虚拟存储器是对主存与磁盘I/O设备的抽象表示,进程则是对处理器、主存、I/O设备的抽象表示。如下图。
接下来,我们将依次讨论三个抽象概念。
1、进程
未完待续