一直想从用户层了解CPU的运作模式以及这样运作的原因,于是找到了《深入理解计算机系统》这本书,希望可以为自己解决很多困惑==
这本书的第一章是通过跟踪一个最简单的"hello, world"(hello.c)程序的生命周期来开始学习,从它被程序员创建,到在系统上运行,输出简单的消息,然后终止。程序具体有多简单呢,我们一起来看一下~
#include <stdio.h>
int main()
{
printf("hello, world\n");
return 0;
}
hello.c 的创建
程序员通过编辑器创建并保存的文本文件hello.c, 实际在存储时就是一个由0/1组成的序列, 大部分的现代计算机都使用ASCII标准表示文本字符。
程序的编译
之所以程序用C语言来写,是因为C语言比较容易读懂,但是机器却无法识别这种高级语言,所以就需要编译系统将这种高级语言的源文件转换成机器可以识别的可执行文件。 编译系统主要由下面几个组件组成: 预处理阶段:这个阶段可以简单理解为将现有的C语言程序展开,就是说预处理器看到*#include <stdio.h>后读取系统头文件stdio.h的内容,把头文件直接展开插入到C程序中,这样就得到一个完整的C程序,通常以.i作为文本名。 编译阶段:编译器将hello.i文件转换成汇编语言文件hello.s*,该程序包含main函数的定义,如下: 汇编阶段:汇编器将hello.s翻译成机器语言指令,并把这些指令打包成可重定位目标程序的格式,并将结果保存在目标文件hello.o中。 链接阶段:由于hello程序调用了printf函数,这是每个C编译器都提供的标准C库中的函数。这个阶段就是要把这种库函数所在的printf.o的单独预编译好的目标文件和hello.o程序合并,最终得到一个hello文件,这是最终的可执行文件。将这个文件加载到内存中,系统就可以执行了。
处理器读并“解释”存储在内存中的指令
如上面所说,现在这个hello.c文件已经被转换成可执行文件hello并且存放在内存中了,接下来处理器要如何运行这个程序呢?这事就需要一个解释器!对于Unix系统,这个解释器就是shell。我们只需在shell里面敲上下面的代码:
linux> ./hello
hello, world
linux>
shell是一个命令行解释器,它输出一个提示符,等待用户输入一个命令行,然后执行这个命令。如果该命令行的第一个单词不是一个内置的shell命令,那么shell就会假设这是一个可执行文件的名字,它将加载并运行这个文件。 具体如何运行这个文件呢?这就需要硬件的帮助了!主要分为以下步骤:
- 当我们在键盘上输入字符串"./hello"后,shell程序将字符逐一读入寄存器,再放到内存里。
利用DMA技术(直接存储器存取),数据可以不通过处理器而直接从磁盘到达主存。 - 一旦目标文件hello中的代码和数据被加载到主存,处理器就开始执行hello程序中的main程序中的机器语言指令。这些指令将"hello, world\n" 字符串中的字节从主存复制到寄存器文件,再从寄存器文件中复制到显示设备,最终显示在屏幕上。
计算机为了加速处理器读写内存的速度,就引入了高速缓存,变成了下面的结构。
操作系统的作用
我们来思考另一个问题,当shell加载和运行hello程序以及hello程序输出自己的消息时,shell和hello程序其实并没有直接访问键盘、显示器、磁盘或主存。那到底是如何运行的呢?就是操作系统!可以把操作系统当成应用程序和硬件之间的插入一个软件,说白了就是中介,所有应用程序对硬件的操作都必须经过操作系统。
操作系统有2个基本功能:首先,防止硬件被失控的应用程序滥用;其次,向应用程序提供简单一致的机制来控制硬件设备。(不同处理器硬件设计都有很大区别的,想象下,如果每个应用程序要执行一个print函数都要根据不同的硬件设备改变实现方法,那简直是灾难。。如果有了操作系统的控制,只需要写一个print函数就可以,管它硬件怎么实现!)
- 操作系统引入了进程
像hello这样的程序在系统上运行时,操作系统提供一种假象,好像系统上只有这个程序在运行。程序看上去是独占的使用处理器、主存和I/O设备。处理器看上去是不间断的一条一条的执行程序中的指令,即该程序的代码和数据是系统内存中唯一的对象。这些假象就是通过进程的概念来实现的。 进程是操作系统对一个正在运行的程序的一种抽象。一个系统上可以同时运行多个进程,而每个进程都好像在独占地使用硬件。并发运行,是指一个进程的指令和另一个进程的指令是交错执行的。大多数系统中,需要运行的进程数是可以多于可以运行它们的CPU个数的。单核处理器一个时刻只能执行一个程序,而先进的多核处理器同时能够执行多个程序。无论单核还是多核,一个CPU看上去都像是在并发的执行多个进程,这是通过处理器在进程间切换实现的,操作系统实现这种交错执行的机制称为上下文切换。 操作系统保持跟踪进程运行所需的所有状态信息,这种状态就是上下文,包括PC,寄存器文件的当前值以及主存的内容等。在任何一个时刻,单处理器系统都只能执行一个进程的代码。当操作系统决定要把控制权从当前进程转移到某个新进程时,就会进行上下文切换,即保存当前进程的上下文,恢复新进程的上下文,然后将控制权传递到新进程,新进程就会从它上次停止的地方开始。 示例场景中有2个并发的进程:shell进程和hello进程。最开始,只有shell进程在运行,即等待命令行上的输入。当我们让它运行hello程序时,shell通过调用一个专门的函数,即系统调用,来执行我们的请求,系统调用会将控制权传递给操作系统。操作系统保存shell进程的上下文,创建一个新的hello进程及上下文,并将控制权传回给它,shell进程会继续等待下一个命令行输入。 从一个进程到另一个进程的转换是由操作系统内核管理的,当应用程序需要操作系统的某些操作时,比如读写文件,它就执行一条特殊的系统调用指令,将控制权传递给内核。然后内核执行被请求的操作并返回应用程序。 - 虚拟内存
虚拟内存是一个抽象概念,它为每个进程提供一个假象,即每个进程都独占使用主存。每个进程看到的内存都是一致的,称为虚拟内存空间。下图所示是Linux进程的虚拟地址空间,最上面的区域是保留给操作系统中的代码和数据的,对所有进程来说都一样。底部存放用户进程定义的代码和数据,图中地址是从下往上增大的。 - 程序代码和数据。对进程来说,代码是从同一固定地址开始,紧接着是C全局变量和相对应的数据位置。代码和数据区是直接按照可执行目标文件(hello)的内容初始化的。
- 堆。代码和数据区在进程一开始运行时就被指定了大小,与此不同,当调用像malloc和free这样的C标准库函数时,堆可以在运行时动态地扩展和收缩。
- 共享库。用来存放C标准库和数学库这样的共享库的代码和数据的区域。
- 栈。编译器用它来实现函数调用,在程序执行期间也可以动态地扩展和收缩。每次我们调用一个函数,栈就会增长,从函数返回时,栈就会收缩。
- 内核虚拟内存。地址空间顶部区域是为内核保留的,不允许应用程序读写这个区域的内容或者调用内核代码定义的函数,必须调用内核来执行这些操作。
几个重要概念
- 线程级并发:超线程。一个CPU可以执行多个控制流,CPU某些硬件有多个备份,比如PC计数器和寄存器文件,其他的硬件部分只有一份,比如浮点算数单元。常规处理器需要很长时间做线程切换,但超线程处理器可以在单周期的基础上决定要执行哪个线程。
- 指令级并行:流水线和超标量
- SIMD并行
总结
这一部分主要是一些介绍性的概念,但是包含了整本书的框架和脉络。很多知识还了解的不是很细节,需要后续章节的继续学习!
|