ICS(计算机系统导论)|1、计算机系统漫游——从程序的生命周期讲起

计算机系统由硬件和系统软件组成,它们共同工作来运行应用程序。

相信每一个一开始接触编程的同学最先写的就是hello world,这是程序员对计算机世界发出的第一声问候。而本书这一部分解释hello world的运行机制、生命周期,为后续所有要介绍的计算机底层知识做一个铺垫。

#include<stdio.h>

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

 

0.为什么选Linux系统

Linux操作系统是众多继承最初由贝尔实验室开发的Unix的操作系统中的一种,具有高度兼容性。

 

1.信息就是位+上下文

 

hello程序的生命周期是从一个源文件开始的,即程序员通过编辑器创建并保存的文本文件,文件名是hello.c。

 

但我们都知道计算机是二进制的,源程序实际上就是一个由值0和1组成的位(又称为比特)序列,8个组成一组,称为字节,字节表示程序中的文本字符。

 

大部分计算机使用ASCII标准来表示文本字符,这种方法实际上就是用一个整数值来表示每个字符,这点和高中函数是一个道理,即一个数字对应一个字符。将上述的hello程序转换为ASCII文本。

 

https://bkimg.cdn.bcebos.com/pic/e850352ac65c103880a07b53bc119313b17e8941?x-bce-process=image/watermark,g_7,image_d2F0ZXIvYmFpa2UxMTY=,xp_5,yp_5

 

hello.c的程序以字节序列的方式存在文件中,每个字节对应一个字符,每个字符对应一个整数,而这些整数值用二进制储存在电脑里。

 

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

 

2.程序被其他程序翻译成不同格式

为了在系统上运行hello.c程序,每条C语句都必须被其他程序转化为一系列的低级机器语言指令。然而这些指令按照一种可执行目标程序的格式打好包,并以二进制磁盘文件的形式存放出来。

 

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

 

linux> gcc -o hello hello.c

 

在这里,GCC编译器驱动程序读取源程序文件后,把它翻译成一个可执行目标文件hello。这个翻译过程分为四个阶段。

 

 

执行这四个阶段的预处理器、编译器。汇编器和链接器一起构成编译系统

 

预处理阶段:预处理器(cpp)根据以字符#开头的命令,修改原始的C程序。如hello.c中的#include<stdio.h>命令告诉预处理器读取系统头文件stdio.h的内容,并且把它插入到程序文本中。结果就得到了另外一个C程序,这个程序通常以.i为结尾。

 

编译阶段:编译器(ccl)将文本文件hello.i翻译成文本文件hello.s,它包含一个汇编语言程序。其实就是将代码编成汇编代码,到了这一步,抄袭的代码基本上就有相当高的重复度了,这不是改改变量名、代码块顺序能改正的,所以,代码查重时经常用到编译反编译的手段。

 

汇编阶段:这一阶段就是汇编语言变成机器语言的过程。汇编器(as)将hello.s翻译成机器语言指令,把这些指令打包成一种叫做可重定位目标程序的格式,保存到hello.o文件中。

 

链接阶段:这个阶段就是将散落在计算机内的各个要调用的文件链接起来的过程。hello程序调用了printf程序,这是每个C编译器都提供的标准C库的一个函数。此函数存在printf.o的单独的预编译好的目标文件,而这个文件必须以某种方式合并到hello.o程序中。链接器(ld)就负责处理这种合并,得到hello文件。这是一个可执行文件,可以被加载到内存中,由系统执行。

 

3.处理器读并解释存储在内存中的指令

现在有了可执行文件hello,想要在Unix系的系统上运行文件,直接将文件名输入到称为shell的应用程序中即可。

 

linux> ./hello

hello, world

linux>

 

这里shell是一个命令行解释器,它首先输入一个提示符,就是linux>,然后等待用户输入一个命令行,这里是./hello,然后执行这个命令。如果shell的第一个单词不是内置的shell命令,那么shell就会假设这是一个可执行文件的名字。因此,此例中shell加载运行hello程序,然后等待程序停止,程序执行完毕后再次弹出提示符。这一点和Windows的命令行有相似点。

 

到这里,程序hello.c的生命周期完全结束。

 

4.系统的硬件组成

 

hello程序的大体处理流程我们知道了,而计算机是由一系列硬件构成的,对我们而言,了解计算机硬件在这个过程中发挥什么样的作用是有必要的。

 

 

4.1总线

总线是贯穿整个系统的一组电子管道,它携带信息字节并负责在各个部件间传递。一般总线被设计成传送定长的字节块,也称字。字中的字节长度要么4个,要么8个,分别对应32位系统和64位系统。

4.2 I/O设备

I/O(输入/输出)设备是系统与外部世界联系的通道,如我们常用的键鼠、显示屏、U盘。每个I/O设备都通过一个控制器或适配器与I/O总线相连。

控制器和适配器之间的区别主要在于它们的封装方式。控制器是I/O设备本身或者主板的芯片组,而适配器是插在主板上的卡。

4.3内存

内存是一个临时存储设备,在处理器执行程序时,用来存放程序和数据。物理上,内存由一组DRAM(动态随机存储存储器)组成。从逻辑上讲,存储器是一个线性的字节数组,每个字节都有其唯一的地址(这里可以存放数组索引),这些地址从零开始。

4.4处理器CPU

中央处理单元CPU简称处理器,是解释存储在内存中指令的引擎。处理器的核心是一个大小为一个字(就是前面所说的32位或者64位)的寄存器,称为程序计数器(PC)。在任何时刻,PC都只想内存中的某条机器语言指令。

从系统通电开始,到系统断电,CPU一直在不断地执行PC指向的指令,再更新PC指向下一条指令,新的指令不一定与上一条指令在内存中的地址相同,如条件语句时的指令跳转。是不是很简单,而且这种简单操作种类并不多,主要是加载、存储、操作和跳转4种操作。操作围绕内存、寄存器文件和算术/逻辑单元(ALU)进行。寄存器文件是一个小的存储设备,由一组寄存器构成。ALU计算新的数据和地址值。

 

加载:从内存复制一个字节或者一个字到寄存器,以覆盖寄存器原本的内容

存储:从寄存器复制一个字节或者一个字到内存的某个位置,覆盖此位置原来的内容

操作:把两个寄存器的内容复制到ALU,ALU对这两个字做算术运算,并将结果存放到一个寄存器中,以覆盖该寄存器中原来的内容。

跳转:从指令本身中抽取一个字,并将这个字复制到PC中,以覆盖PC中原来的值。

现在回到hello程序,还记得我们输入运行hello的命令到shell中吗?

 

linux> ./hello

hello, world

linux>

 

shell程序将字符一一读入寄存器,再把它放到到内存中。当我们敲入回车键后,shell程序就知道我们已经结束了命令的输入,然后shell执行一些列指令来加载可执行的hello文件,这些指令将hello目标文件中的代码和数据从磁盘复制到内存,此时CPU就开始执行hello程序的main程序中的机器语言指令。这些指令将我们要的结果“hello, world\n”字符串中的字节从内存复制到寄存器文件,再从寄存器文件中复制到显示设备。

 

5.高数缓存至关重要

现在,我们知道了一个程序的运行周期和它在机器上运行时硬件的工作顺序和流程。但是看似合理的过程其实存在一个问题,即系统花费了大量时间将信息从一个地方挪到另外一个地方。

我们来看看hello程序的机器指令迁移过程,首先指令存在磁盘上,当程序加载时,它们被复制到内存;CPU运行时,它们有到了CPU上。这些复制就是开销,在空间和时间上的双重开销,减慢了程序的速度。因此,系统设计者需要通过设计使这些复制尽快完成。

 

根据机械原理,我们知道一个前提,较大的存储设备要比较小的存储设备运行得慢,而快速设备的造价远高于同类的低速设备。一个典型的寄存器文件只存储几百字节的信息,而内存可以存放几十亿字节,然而,CPU从寄存器中读取数据比从内存中读取要快100倍,更麻烦的是,随着技术提高,CPU的运行速度还在迅速提高,但内存运行速度的提高相对而言要慢得多。

 

针对这种处理器与内存之间的差异,系统设计者采用高速缓存存储器,作为暂时的集结区域,存放CPU近期可能会需要的信息。

系统可以获得一个很大的存储器,同时访问速度也很快,原因是利用了高速缓存的局部性原理,即程序具有访问局部区域里的数据和代码的趋势。通过让高速缓存里存放可能经常访问的数据,大部分的内存操作都能在快速的高速缓存中完成。

 

6.存储设备形成层次结构

通过上面的磁盘-内存-缓存结构你想到了什么?在CPU和一个较大较慢的设备中间插入一个更小更快的存储设备的想法已经成为了计算机硬件结构设计中的一个普遍的观念。于是,形成了常见的金字塔结构。

 

 

7.操作系统管理硬件

好了,现在我们回顾一下,计算机是硬件构成的,但是shell加载hello程序时,shell和程序都没有直接访问这些硬件,如键盘、显示器等。取而代之的是,它们依靠操作系统提供的服务实现对硬件的调用。操作系统可以看出是应用程序和硬件之间插入的一层软件,所有应用程序对硬件的操作尝试都必须通过操作系统。

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

  1. 防止硬件被失控的应用程序滥用
  2. 向应用程序提供简单一致的机制来控制复杂而又通常大不相同的低级硬件设备。

而实现这两个功能,操作系统用到了进程、虚拟内存和文件系统。

文件是I/O设备的抽象表示,虚拟内存是对内存和磁盘I/O设备的抽象表示,进程是对CPU、内存和I/O设备的抽象表示。

7.1进程

进程是操作系统对一个正在进行的程序进行的抽象。一个进程上可以同时运行多个进程,而每个进程都好像在独占地使用硬件。进程通俗的讲就是你在电脑上运行不同的软件,每个软件工作占用一个进程。

并发运行,指一个进程的指令和另一个进程的指令是交错执行的。在传统计算机中,一个CPU只能执行一个进程,但是通过并发运行,一个多核CPU可以同时运行多个程序,这是通过CPU进程切换来实现的。

这种交错执行的机制称为上下文切换。操作系统保持跟踪进程运行所需的所有状态信息(上下文),包括PC(程序计数器,还记得吗)和寄存器文件的当前值、内存的内容等。在任何一个时刻,单CPU系统只能执行一个进程,通过频繁切换进程实现CPU对多进程的运行,即并发运行。当操作系统决定要把控制权从当前进程转移到新进程时,就会发生上下文切换——保存当前进程上下文,恢复新进程上下文,控制权传递到新进程,新进程就会从上次停止的地方开始。

不同进程之间的转换,是由操作系统的内核管理的。内核是操作系统代码常驻内存的部分。当应用程序需要操作系统的某些操作时,它就执行系统调用指令,将控制权传给内核,然后内核执行请求的操作并返回应用程序。

7.2线程

在现代系统中,一个进程实际上可以有多个线程的执行单元组成,每个线程都运行在进程的上下文中,并共享同样的代码和全局数据。其实线程很明显,在我们打开应用时,图片往往比文字加载得慢得多,但是先出现了所有文字,而不是按顺序显示,这就是线程在起作用,一个线程显示文字,一个线程显示图片。

7.3虚拟内存

虚拟内存是一个抽象概念,为每一个进程提供了一个想象,即每个进程都在独占地使用主存。每个进程看到的内存都是一致的,称为虚拟地址空间。

在Linux系统中地址空间最上面的区域是保留给操作系统中的代码和数据的,这对所有进程来说都是一样,而地址空间的底部区域存放用户进程定义的代码和数据。

7.4文件

文件就是字节序列,仅此而已。每个I/O设备,包括磁盘、键盘、显示器甚至网络都可以看成文件。(惊不惊喜意不意外,但你仔细想想,确实如此)

文件向应用程序提供了一个统一的视图,来看待系统中可能含有的所有各式各样的I/O设备。

8.系统之间通过网络通信

系统漫游到此结束,而我们可以稍作延伸。前面我们一直将系统和软件当做一个孤立的个体来研究。那么个体多起来之后呢,不同的个体有交流的欲望(想想疫情期间关在家的你),于是网络诞生了。系统通过网络和其他系统连接到一起。网络可以看成一个大型I/O设备。

当系统从内存复制一串字节到网络适配器时,数据流经过网络到达另外一台机器,而不是到本地磁盘。相似的,系统可以接受从其他机器发送过来的数据。

随着Internet这样的全球网络的出现,从一台主机复制信息到另外一台主机已经成为计算机系统最重要的用途之一。

 

本次概略性介绍也到此结束,计算机,从01开始,通过连接、处理,便组成了我们现在丰富多彩的信息世界。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章