通过学习简单的程序'hello.c'整个在计算机中的运行流程来了解计算机系统是如何工作的:
#include <stdio.h>
int main()
{
printf("hello, world
")
}
1.1 信息就是位+上下文
hello程序的生命周期是从一个源程序开始的。源程序实际上是由0,1组成的位(bit)序列,8个位被组织成一组,成为字节。每个字节表示程序中某个文本字符。
大部分的现代系统都是用ASCⅡ标准来表示文本字符,用唯一的单字节大小的整数值来表示每个字符。系统中所有信息都可以这样,通过一串位来表示。区分数据对象的唯一方法是我们读到这些数据对象时的上下文。作为程序员,我们需要了解数字的机器表示方式,因为他们与实际整数和实数是不同的。他们是对真值得有限近似值。
1.2 程序被其他程序翻译成不同格式
hello程序的生命周期是一个高级C语言程序开始的,因为这种形式能够被人读懂。然而,为了在系统上运行hello.c程序,每条C语言都必须被其他程序转化为一系列的低级机器语言指令。然后这些指令按照一种称为可执行目标程序的格式打好包,并以二进制磁盘文件的形式存放起来。目标程序也成为了可执行目标文件。
在Unix系统上,从源文件到目标文件的转化是由编译器驱动程序完成的。GCC编译器驱动程序读取源程序文件hello.c,并把它翻译成一个可执行目标文件hello。这个翻译过程分为四个阶段。执行这四个阶段的程序(预处理器,编译器,汇编器和链接器)一起构成了编译系统(compilation system)。
- 预处理阶段。 预处理器(cpp)根据以字符#开头的命令,修改原始的C程序。比如
hello.c
中第一行# include <stdio>
,告诉预处理器读取系统头文件stdio.h
内容,并把它直接插入到程序文本中。结果就得到了另一个C程序,通常是以.i
作为文件扩展名。 - 编译阶段。 编译器(ccl)将文本文件hello.i翻译成文本文件hello.s,它包含一个汇编语言程序。汇编语言程序中的每条语句都以一种标准的文本格式确切地表述了一条第几机器语言指令。汇编语言为不同高级语言的不同编译器提供了通用的输出语言。
- 汇编阶段。 汇编器(as)将hello.s翻译成机器语言指令,把这些指令打包成一种叫做可重定位目标程序(relocatable object program)的格式,并将结果保存在目标文件hello.c中。hello.o文件是一个二进制文件,它的字节编码是机器语言指令而不是字符。
- 链接阶段。 hello程序调用了printf函数,它是每个C编译器都会提供的标准C库中的一个函数。printf函数存在于一个名为printf.o的单独的预编译好了的目标文件中,而这个文件必须以某种方式合并到我们的hello.o程序中。链接器(ld)就负责处理这种合并。结果就得到hello文件,它是一个可执行目标文件,可以被加载到内存中,由系统执行。
1.3了解编译系统如何工作是大有益处的
- 优化程序性能
- 理解链接时出现的错误。
- 避免安全漏洞
1.4处理器读并结束存储在存储器中的指令
此刻,hello.c源程序已经被编译系统翻译成了可执行目标文件hello,并存放在磁盘上。通过shell执行,shell是个命令行解释器。
1.4.1 系统的硬件组成
典型系统的硬件组织。
1.总线
贯穿整个系统的事一组电子管道,称作总线,他携带信息子杰并负责在各个部件间传递。通常总线被设计成传送定长的字节块,也就是字(word)。字中字节数(字长)是一个基本的系统参数,在各个系统中的情况都不尽相同。现在的大多数机器字长有的4字节(32位),有的是8个字节(64bit)。
2.I/O设备
每个I/O设备都通过一个控制器或适配器与I/O总线相连。控制器和适配器之间的区别主要在于他们封装的方式。控制器是置于I/O设备本身或者系统的主板上的芯片组,而适配器则是一块插在主板插槽上的卡。
3.主存
是一个临时存储设备,在处理器执行程序时,用来存放程序和程序处理的数据。从物理上来说,主存是由一组动态随机存取存储器(DRAM)芯片组成。从逻辑上来说,存储器是一个线性的字节数组,每个字节都有其唯一的地址(即数组索引),这些地址从零开始的。一般来说,组成程序的每条机器指令都由不同数量的字节构成。与C程序变量相对应的数据项的大小是根据类型变化的。
4.处理器
中央处理单元(cpu),是解释(或执行)存储在主存中指令的引擎。处理器的核心是一个字长的存储设备(或寄存器),称为程序计数器(PC)。在任何时刻,PC都指向主存中的某条机器语言指令(即含有该条指令的地址)。
通电开始,cpu就要一直在不断地执行程序计数器指向的指令,再更新程序计数器,使其指向下一条指令。处理器看上去是按照一个非常简单的指令执行模型来操作的,这个模型是由指令集结构决定的。处理器从程序计数器(pc)指向的存储器处读取指令,解释指令中的位,执行该指令指示的简单操作,然后更新PC,使其指向下一条指令,而这条指令并不一定与存储器中刚刚执行的指令相邻。
操作主要是围绕着主存、寄存器文件(register file)和算术/逻辑单元(ALU)进行的。寄存器文件是一个小的存储设备,由一些1字长的寄存器组成,每个寄存器都有唯一的名字。ALU计算的新的数据和地址值。CPU在指令的要求下可能会执行以下操作:
* 加载:把一个字节或者一个字从主存复制到寄存器,以覆盖寄存器原来的内容。
* 存储:把一个字节或者一个字从寄存器复制到主存的某个位置,以覆盖这个位置上原来的内容。
* 操作:把两个寄存器的内容复制到ALU,进行算术操作,并将结果存放到另一个寄存器中,以覆盖该寄存器中原来的内容。
* 跳转:从指令本身抽一个字,并将这个字复制到程序计数器(PC)中,以覆盖PC中原来的值。
指令集结构描述的是每条机器指令代码的效果;而微体系结构描述的是处理器实际上是如何实现的。
1.4.2 运行hello程序
一旦目标文件hello中的代码和数据被加载到主存,处理器就开始执行hello程序的main程序中的机器语言指令。这些指令将字符串中的字节从主存复制到寄存器文件,再从寄存机文件复制到显示设备。
1.5 高速缓存至关重要
系统花费了大量的时间把信息从一个地方挪到另一个地方。hello程序的机器指令最初是存放在磁盘上的,当程序被加载时,它们被复制到主存;当处理器运行程序时,指令又从主存复制到处理器。相似地,数据串'hello,world/n'初始在磁盘上,也经过复制到主存的过程,最后从主存复制到显示设备上。复制=开销。因此,系统设计者的一个主要目标就是使这些复制操作尽可能快地完成。
根据机械原理,较大的存储设备要比较小的存储设备运行得慢。针对这种处理器与主存之间的差异,系统设计者采用了更小、更快的存储设备,即高速缓存存储器,作为暂时的集结区域,用来存放处理器近期可能会需要的信息。
1.6存储设备形成层次结构
在处理器和一个又大又慢的设备(例如主存)之间插入一个更小更快的存储设备(例如告诉缓存)的想法已经成为了一个普遍的观念。每个计算机系统的存储设备都被组织成了一个存储器层次结构。
存储器层次结构的主要思想是一层上的存储器作为低一层存储器的高速缓存。因此,寄存器文件就是L1的高速缓存,L1是L2的高速缓存,以此类推。
1.7操作系统管理软件
当shell加载和运行hello程序,以及hello程序输出自己的消息时,shell和hello程序都没有直接访问键盘、显示器、磁盘或者主存。取而代之的是,它们依靠操作系统提供服务。可以看做上层应用和底层硬件中夹了一层软件。
操作系统有两个基本功能:1)防止硬件被失控的应用程序滥用。2)向应用程序提供简单一致的机制来控制复杂而又通常大相径庭的低级硬件设备。操作系统通过几个基本的抽象概念(进程、虚拟存储器和文件)来实现这两个功能。
1.7.1 进程
进程是操作系统对一个正在运行得程序的一种抽象。在一个系统上可以同时运行多个进程,而每个进程都好像在独占地使用硬件。而并发运行,则是说一个进城的指令和另一个进程的指令是交错执行的。无论是在单核还是多核系统中,一个CPU看上去都像是在并发地执行多个进程,这是通过处理器在进程间切换来实现的。操作系统实现这种交错执行的机制称为**上下文切换**。
操作系统保持跟踪进程运行所需要的的所有状态信息。这种状态,也就是上下文,它包括许多信息,例如PC和寄存器文件的当前值,以及主存的内容。当操作系统决定要把控制权从当前进程转移到某个新进程时,就会进行上下文切换,即保存当前进程的上下文、恢复新进程的上下文,然后将控制权传递到新进程。新进程就会从上次停止的地方开始。
示例中有两个并发的进程:shell进程和hello进程。起初,只有shell在运行,当我们让它运行程序时,shell通过调用一个专门的函数(system call),系统调用会将控制权传递给新的hello进程。hello进程终止后,操作系统回复shell进程上下文,并将控制权给它。
1.7.2 线程
一个进程实际由多个线程的执行单元组成,线程运行在进程的上下文中,并共享同样的代码和全局数据。
1.7.3虚拟存储器
是一个抽象概念,它为每个进程提供了一个假象,即每个进程都在独占地使用主存。每个进程看到的是一致的存储器,成为虚拟地址空间。图示为Linux进程的虚拟地址空间。图中地址是从下往上增大的。
每个进程看到的虚拟地址空间由大量准确定义的区构成,每个区都有专门的功能。
- 程序代码和数据。对于所有进程来说,代码是从同一固定地址开始,紧接着的是和C全局变量相对应的数据位置。代码和数据区是直接按照可执行目标文件的内容初始化的,
- 堆。代码和数据区后紧随着的运行时堆。代码和数据区是在进程一开始运行时就被规定了大小,与此不同,当调用如malloc和free这样的C标准库函数时,堆可以在运行时动态的扩展和收缩。
- 共享库。大约在地址空间的中间部分是一块用来存放像C标准库和数学库这样共享库的代码和数据的区域。共享库概念非常强大,也相当难懂。
- 栈。 位于用户虚拟内存空间顶部的是用户栈,编译器用它实现函数调用。它也可以动态的扩展和收缩。
- 内核虚拟存储器。 内核总是驻留在内存中,是操作系统的一部分。地址空间顶部的区域是为内核保留的,不允许应用程序读写这个区域的内容或者直接调用内核代码定义的函数。
虚拟存储器基本思想是把一个进程虚拟存储器的内容存储在磁盘上,然后用主存作为磁盘的高速缓存。
1.7.4 文件
文件就是字节序列。
1.8系统之间利用网络通信
现代系统是通过网络连接到一起的。网络也可视为一个I/O设备。
我们可以使用telnet应用在一个远程主机上运行hello程序。
1.9重要主题
我们在强调几个贯穿计算机系统所有方面的重要概念作为本章的结束。我们还会再本书中的多处讨论这些概念的重要性。
1.9.1并发和并行
并发:指一个同时具有多个活动的系统;
并行:用并发使一个系统运行得更快。并行可以在计算机系统的多个抽象层次上运用。在此,我们按照系统层次结构由高到低强调三个层次。
##1.线程级并发
![](https://img2020.cnblogs.com/blog/1229991/202007/1229991-20200703194750870-1541846896.png)
## 2.指令级并行
在较低的抽象层次上,现代处理器可以同时执行多条指令的属性成为指令集并行。
## 3.单指令、多数据并行
在最低层次上,许多现代处理器拥有特殊的硬件,允许一条指令产生多个可以并行执行的操作,这种方式称为单指令、多数据,即SIMD并行。
1.9.2 计算机系统抽象的重要性
文件是对I/O的抽象,虚拟存储器是对程序存储器的抽象,而进程是对一个正在运行的程序的抽象。虚拟机:对整个计算机(包括OS,CPU,程序)的抽象。