通过一个C语言程序的生命周期,了解计算机系统

我们通过跟踪这个hello程序来系统性的了解计算机系统是如何运行的。

#include 

int main(){
	printf("hello world");
}

一、信息的本质

一个C语言程序的生命周期从一个源程序开始,程序员用编辑器创建并保存的文本文件,文件后缀为.c。这个源程序本质上是一个由0和1组成的(也叫bit)序列,8个一组,被称为字节。每个字节表示一个字符。(注:现在的大部分系统都用ASCII标准来表示字符。)

该图是hello程序的ASCII文本表示
通过一个C语言程序的生命周期,了解计算机系统_第1张图片
系统中的所有信息都是如hello程序一样由一串位组成的——包括磁盘文件、存储器中的程序、存储器中存放的用户数据以及网络上传输的数据。因此,我们可以说信息的本质就是一串位。(注:在不同的上下文中,一个相同的字节序列可能表示不同的意思。比如,一个数字在不同的上下文中可以表示为整数、浮点数等。)

二、被翻译成不同形式的hello程序

hello程序被创建之初是一个高级的C语言程序,这种形式可以被我们读懂,但无法被计算机读懂。因此,hello程序必须被翻译成计算机能够“明白”的形式。每条C语句都要被转化为一系列的低级机器语言指令。这些指令按照可执行目标程序的格式打包,然后以二进制磁盘文件的形式保存起来。该文件也被称为可执行目标文件

这个翻译过程分为四个阶段,由四个程序执行——预处理器、编译器、汇编器、链接器,它们一起构成了编译系统(compilation system)。
通过一个C语言程序的生命周期,了解计算机系统_第2张图片

  • 预处理阶段。预处理器(即cpp)根据以#号开头的命令,修改源代码。如hello.c中第一行#include ,预处理器通过按照该命令读取头文件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、系统的硬件组成

我们通过一张图来解释系统的硬件组成。
通过一个C语言程序的生命周期,了解计算机系统_第3张图片

  • 总线。一个贯穿系统的电子管道,它的主要作用是在各个部件之间传递信息。一般来说,总线会传输固定的字节块,也就是字(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设备或者主存,而是通过操作系统提供的服务。因此,我们可以把操作系统当做是硬件与程序之间的一层软件,如下图所示。所有的程序都需要通过操作系统才能对硬件进行操作。
通过一个C语言程序的生命周期,了解计算机系统_第4张图片
由此,我们可以总结出操作系统的两大基本功能:1)防止硬件被程序随意滥用。2)屏蔽掉低级硬件设备的复杂性,向程序提供一个简单一致的机制来控制硬件设备。
操作系统实现这两个功能主要是通过三个基本抽象概念——进程、虚拟存储器和文件。其中,文件是对I/O设备的抽象表示,虚拟存储器是对主存与磁盘I/O设备的抽象表示,进程则是对处理器、主存、I/O设备的抽象表示。如下图。
通过一个C语言程序的生命周期,了解计算机系统_第5张图片

接下来,我们将依次讨论三个抽象概念。

1、进程

进程是一个正在运行的程序的一种抽象表现。在一个系统上,我们可以同时运行多个进程。但是,由于传统CPU一个时刻只能执行一个程序,所以多个进程实际上是交错运行的,也叫做并发运行。现在已经出现了多核CPU,可以同时执行多个程序。然而,无论在是单核还是多核系统中,CPU都要通过在进程之间进行切换来实现并发的执行多个进程。操作系统实现这种交错执行的机制被叫做上下文切换

操作系统会跟踪进程运行并保存进程所有的状态信息。这些状态信息就是上下文,其中主要包括程序计数器和寄存器文件的当前值以及主存的内容。当操作系统进行进程间的切换时,就会保存当前进程的上下文,然后恢复新进程的上下文。新进程按照信息就会从上次停止的地方开始运行。下图展现了hello程序的运行场景。
通过一个C语言程序的生命周期,了解计算机系统_第6张图片
在该场景中,我们假设只有两个进程,进程A为shell进程,进程B为hello进程。如图所示,一开始只有shell进程在运行,即等待命令行上的输入。我们输出执行hello程序的命令时,shell通过系统调用来调用操作系统。操作系统将保存shell进程的上下文,同时创建一个新的hello进程及其上下文,然后hello进程开始运行,即占据CPU。hello进程终止后,操作系统恢复shell进程的上下午,shell进程继续运行,等待下一个命令输入。

注:在现代的系统中,进程实际上由多个线程组成,每个线程共享进程中全局的数据。

2、虚拟存储器

虚拟存储器是一个逻辑模型,当进程向操作系统申请内存,而内存不够时,那么就在磁盘上开辟一块空间当做内存使用。它让每个进程都好像在独占主存。每个进程使用的都是一致的存储器,即虚拟地址空间。下图是Linux进程的虚拟地址空间。
通过一个C语言程序的生命周期,了解计算机系统_第7张图片
在Linux中,最上面的内核虚拟存储器是用来保存操作系统中的代码与数据。底部区域用来存放用户进程的代码与数据。我们简单了解一下每个区。

  • 代码和数据区。对于所有的进程来说,代码是从同一固定地址开始。代码和数据区是直接按照可执行目标文件的内容初始化的,在示例中就是可执行文件hello。
  • 运行时堆。代码和数据区后紧随着的是运行时堆。代码和数据区是在进程一开始运行时就被规定了大小,与此不同,当调用如mallocfree这样的C标准库函数时,堆可以在运行时动态地扩展和收缩
  • 共享库。在地址空间的中间部分是一块用来存放像C标准库数学库这样共享库的代码和数据的区域。
  • 。位于用户虚拟地址空间顶部的是用户栈,编译器用它来实现函数调用。和堆一样,用户栈在程序执行期间可以动态地扩展和收缩。当我们调用一个函数时,栈就会增长。函数返回时,栈就会收缩。
  • 内核虚拟存储器。内核总是驻留在内存中,是操作系统的一部分。地址空间顶部的区域是为内核保留的,不允许应用程序读写这个区域的内容或者直接调用内核代码定义的函数。

注:虚拟存储器的运作的基本思想是把一个进程虚拟存储器的内容存储在磁盘上,然后用主存作为磁盘的高速缓存。

3、文件

文件就是字节序列。每个I/O设备,包括磁盘、键盘、显示器以及网络,都可以视为文件。系统中的所有输入输出都是通过使用UnixI/O的系统函数调用读写文件来实现的。文件这个简单的概念向应用程序提供了一个统一的视角,我们可以通过这一视角来看待系统中各式各样的I/O设备。

六、系统之间的网络通信

在现代系统中,系统并非是一个孤立的软件和硬件的集合体,而是通过网络和其他系统连接到一起。我们可以把网络可视为一个I/O设备。当系统从主存将一串字节复制到网络适配器时,数据流经过网络到达另一台机器的本地磁盘驱动器。同样,系统可以读取从其他机器发送来的数据,并把数据复制到自己的主存。
通过一个C语言程序的生命周期,了解计算机系统_第8张图片
在将网络引入后,我们再用hello程序来讨论系统间的通信。我们可以使用telnet应用在一个远程主机上运行hello程序。现在,本地主机上的telnet客户端连接远程主机上的telnet服务器。如下图所示,远程运行hello程序包括5个步骤。
通过一个C语言程序的生命周期,了解计算机系统_第9张图片
当我们在telnet客户端输入"hello"字符串后,telnet客户端就会将这个字符串发送到telnet服务器。telnet服务器从网络上接收到这个字符串后,会把它传递给远端shell程序。接下来,远端shell程序运行hello程序,并将输出结果返回给telnet服务器。最后,telnet服务器通过网络把输出串转发给telnet客户端,客户端就将输出结果输出到我们的本地终端上。

七、题外话:抽象的重要性

抽象的使用是计算机科学中最为重要的概念之一。例如,为一组函数规定一个简单的应用程序接口(API)就是一个很好的编程习惯,程序员无需了解它内部的工作便可以使用这些接口。不同的编程语言提供不同形式和等级的抽象支持,例如Java类的声明和C语言的函数原型。
在处理器里,指令集结构提供了对实际处理器硬件的抽象。使用这个抽象,机器代码程序表现成它是运行在一个一次只执行一条指令的处理器上。并行地执行多条指令,但又与简单的模型保持一致。只要执行模型一样,不同的处理器实现也能执行同样的机器代码。
通过一个C语言程序的生命周期,了解计算机系统_第10张图片

注:计算机系统中的一个重大作用就是提供不同层次的抽象表示,来隐藏实际实现的复杂性。

八、总结

  • 计算机系统是由硬件和系统软件组成的,它们共同协作来运行程序。计算机内部的信息被表示为一串位,它们依据上下文有不同的意识。程序被翻译成不同的形式,开始时是ASCII文本,然后被编译器和链接器翻译成二进制可执行文件
  • 处理器读取并解释存放在主存里的二进制指令。因为计算机把大量的时间用于在各个部件之间的信息传输,所以多层的硬件高速缓存存储器出现并广泛应用。
  • 操作系统内核是应用程序和硬件之间的媒介。它提供三个基本的抽象:1)文件是对I/O设备的抽象。2)虚拟存储器是对主存和磁盘的抽象。3)进程是对处理器、主存和I/O设备的抽象。
  • 最后,网络提供了计算机系统之间通信的手段。从另一个角度来看,网络就是一种I/O设备。

你可能感兴趣的:(c语言,开发语言,缓存,网络)