程序执行和Cache

来源:互联网 发布:数字签名常用的算法有 编辑:程序博客网 时间:2024/04/17 04:24

对于程序员而言,我们编写的程序都是用某一种程序设计语言进行编写,比如C语言,这些语言对于计算机而言是无法直接理解的,那么计算机是如何理解我们的程序,然后按照程序的要求去执行的呢?

为了统一说明,本文假设使用C语言进行程序设计,当然,C语言中最为著名的莫过于HelloWord了,源码如下:

#include<stdio.h>int hello(){       printf("HelloWorld!\n");}

         假设该文件被保存于hello.c的文本文件中,本文使用RedHat Linux作为操作系统。

1.     编译系统

    在hello程序的生命周期的开始阶段,它由能够被程序员所理解的高级语言所编写,为了使该程序能够运行,就必须将其转换成一系列低级的机器语言指令,然后这些指令按照一种被称为可执行目标程序executableobject program)的格式进行打包,并以二进制的形式存放于计算机的存储设备上。可执行目标程序也被称为可执行目标文件executableobject file)。

   Linux系统上,将hello.c转换成可执行目标文件是由编译器驱动程序complierdriver)完成。

   gcc –o hellohello.c

       gccLinuxUnix平台下的C编译器,在Linux平台下安装好gcc后,运行上述命令,gcc编译驱动程序会读取程序文件hello.c,并将其转换成一个可执行的目标文件hello,我们将这个从文本文件到可执行目标文件的转换过程称为编译,编译一段程序可以分为四个阶段:

1.1预处理阶段

预处理器(cpp)根据以字符#开头的命令(directive),修改原始C程序。比如hello.c中第一条#include<stdio.h>指令告诉预处理器,读取系统头文件stdio.h的内容,并将它直接插入到hello.c文本文件中去,结果就将原始的hello.c转换成了另一个C程序文件,在Linux/Unix下,该文件通常以.i作为文件的扩展名。

1.2编译阶段

编译器(ccl)将文本文件hello.i翻译成hello.s,它包含一个汇编语言程序。汇编语言中的每一条语句都以一种标准的文本格式确切的描述了一条低级机器语言的指令,汇编语言是很有用的,因为它为不同高级语言的不同编译器提供了通用的输出语言,比如C语言和Java语言的编译器产生的输出文件都是用一样的汇编语言。

1.3汇编阶段

汇编器(as)将hello.s翻译成机器指令语言,把这些指令打包成为一个可重定位(relocatable)的目标程序的格式,并将结果保存在目标文件hello.o中,hello.o文件是一个二进制文件,他的字节码是机器语言指令,而不是文本字符。如果将hello.o用文本编辑器打开,会出现一堆乱码。

1.4链接阶段

由于hello.c中调用了printf函数,该函数式一个标准C库中的函数,每个C编译器都会提供,printf函数位于一个printf.o的目标文件中,而这个文件必须以某种方式并入到hello.o中,否则hello.o就不知道在哪个地方去调用printf函数了。该工作就交由链接器(ld)完成,链接器将printf.o中的printf接口并入hello.o中后,会生成gcc命令-o所指定的hello文件,该文件就是一个可执行的目标文件(或者成为可执行文件)。通过调用hello命令,该可执行文件会被加载到存储器,然后加由系统负责执行。

2.     程序执行

   待得到hello可执行文件后,在linuxshell上就可以通过运行./hello命令在屏幕上打印出“Hello World!”了。

2.1硬件组成

为了了解运行hello的时,系统究竟发生了怎样的操作流程,我们有必要在回顾一下上一章讲解的计算机系统的硬件组成:

                       

图表21现代计算机系统硬件组成

上图中可以看出,计算机主要由IO设备、存储器、CPU三个核心模块组成,这三个核心模块通过总线进行信息交互和传递。

2.1.1      总线

负责各个部件的信息传递工作,将计算机的各个部件连接成为一个完整的计算机系统。

2.1.2      I/O设备

它是系统与外部连接的通道。在上图中包含四个I/O设备:作为输入的键盘和鼠标,作为输出的显示器,长期存储数据的磁盘驱动器,我们的hello.c和经过gcc编译生成的可执行目标文件hello就存放在磁盘驱动器上。

可以看出,每个I/O设备都是通过控制器或者适配器与I/O总线连接起来的。控制器和适配器的主要区别在于他们有不同的组成方式。控制器是I/O设备本身中或者系统中的主印刷电路板(通常成为主板)上的芯片组,而适配器则是一块插在主板插槽上的卡,以供其他I/O设备连入计算机系统。无论如何,他们的功能都是在I/O总线和I/O设备之间传递信息。

2.1.3      主存

计算机系统的主存是一个临时存储设备,它里面的信息存储需要依靠电力来维持,在处理器执行程序的饿时候,它被用于存放程序和程序处理的数据。从物理上讲,主存是由一组DRAMDynamicRandom Access Memory 动态随机访问存储器)的芯片组组成。从冯·诺依曼体系接口的逻辑上来看,主存是由一个连续的字节数组构成,每个字节都有自己唯一的地址(数组索引,因此地址是按照字节为单位进行编码的,而不是按照二进制位为单位进行编码的),这些地址从零开始编码。一般来说,组成程序的每一条机器指令都是有不定量的字节构成。C程序中不同数据类型所对应数据大小会有所不同,比如LinuxIntel 32位机器上,short类型需要2个字节表示,intfloatlong需要4个字节表示,而double需要8个字节表示。

2.1.4      CPU

CPU是计算机的大脑,用于解释或者执行存在主存中的指令的引擎。处理器的核心是一个被称为程序计数器(PC)的字长大小的存储设备(一般都是寄存器)。在任何一个时间点,PC都指向主存中的某条机器语言指令(内含有其地址)。

从系统通电开始,直到系统断电,处理器一直在不假思索的重复执行相同的任务:从程序计数器(PC)指向的存储器读取指令,解释指令中的二进制位,执行指令所指示的简单操作(Operation),然后更新程序计数器指向下一条指令,而这条指令并不一定在存储器中和刚刚执行的指令相邻。

这样的简单操作都是在主存、寄存器堆(register file)、算术逻辑单元(ALU)之间循环。寄存器堆是一个很小的存储设备,在位置上非常靠近ALU,它由一些字长大小的寄存器组成,每个寄存器都有一个唯一的名字。ALU从寄存器中取出数据和地址,并计算这些数据和地址,CPU的执行操作大致如下:

l 加载:从主存中拷贝一个字节或者一个字到寄存器中,覆盖寄存器中原来的内容。

l 存储:从寄存器拷贝一个字节或者一个字到主存某个位置,覆盖掉主存上原来的内容。

l 更新:拷贝两个寄存器的内容到ALU中,ALU将两个字相加,然后将结果存放到一个寄存器中,覆盖掉该寄存器中原来的内容。

l I/O:从一个I/O设备中拷贝一个字节或者一个字到寄存器中。

l I/O:从一个寄存器中拷贝一个字节或者一个字到一个I/O设备中。

l 跳转:从指令本身中抽取一个字,并将这个字拷贝到程序计算器(PC)中,覆盖掉PC中原来的值。

2.2程序执行

当我们在键盘上敲入./hello命令后,shell程序就会执行它的指令,然后逐一读取字符到寄存器,再将它们存放到主存中。

                 

图表22从键盘输入读取./hello命令到主存中

当命令输入完毕,我们敲下回车后,shell程序就知道我们已经结束了命令的输入,应该去执行命令了,shell就会执行一系列指令,将hello目标文件中的代码和数据从磁盘中拷贝到主存中,从而实现hello程序的加载,数据包括将被输出的字符串“Hello World!\n”。

在加载的时候,可以利用直接存储器访问(DMA Direct Memory Access)的技术,数据可以不通过处理器而直接从磁盘到达主存中。

                   

 图表23从磁盘上加载可执行文件到主存中

一旦hello目标文件中的代码和数据都被加载到了主存储器后,处理器就开始执行hello程序的主程序中的机器语言指令,这些指令将“hello world!\n”串中的字节从主存中拷贝到寄存器堆,然后再从寄存器堆中拷贝到显示设备上,最终显示在屏幕上。

                                  

图表24从存储器写出串到输出设备上

3.     Cache

   随着计算机技术的发展,人们对计算机的性能要求也越来越高,因此,像上面这种将大量时间都花费在将信息从一个地方挪动到另一个地方的工作模式势必会极大的影响计算机的执行性能,那有没有一种有效的手段来解决这个问题呢?

   分析上面的问题我们不难发现,可执行文件和数据最初都是存放于磁盘驱动器上的,当程序加载的时候,可执行程序中的指令从磁盘拷贝到主存上,当处理器运行程序的时候,这些指令又从主存拷贝到处理器的寄存器中。相似的,hello程序的数据“HelloWorld!\n”开始从磁盘加载到主存,然后又从主存拷贝到显示设备上,站在程序员的角度来看,大量的拷贝肯定会影响程序的实际工作效率,因此,对于系统设计者而言,他们的一个主要的目标就是尽可能的减少拷贝操作或者使这些拷贝操作的效率更高,速度更快。

   但是,就现代计算机系统而言,执行一个程序,就必须要有一定的数据和指令的拷贝量,因此在减少拷贝次数行不通的情况下,就只能够考虑如何提升这些拷贝的效率。

   我们知道,磁盘的存储容量比主存大多了,但是磁盘的运行速度却比主存要慢很多,磁盘的单位造价也远比主存要低很多,这是磁盘和主存相比较,而主存和寄存器之间也存在类似的关系,一个典型的寄存器堆仅仅只有几百个字节的容量,而目前主存的容量目前却能够达到GB的数量级,更可怕的是,随着半导体技术的进步,这种处理器与主存之间的速度差异(processor-memory gap)还在持续增大。

   针对处理器与主存的这种差异,计算机科学家们提出了比主存更小但是更快的存储设备,称为高速缓存(cache memory,它介于处理器和主存之间,作为临时的数据存放区域,用于存放处理器在不久的将来可能需要的信息。

   下图展示了一个典型的现代处理器的高速缓存的体系结构,处理器芯片中自带的高速缓存被称为一级缓存(L1 Cache),它的容量可以达到数万字节,而且访问速度几乎和寄存器的访问速度一样快。

   与此同时,一个容量更大的,访问速度稍微比L1慢,但是还是远远快于主存的二级缓存(L2 Cache)被通过一条特殊的总线连接到处理器中,L2的容量通常可以达到数十万到数百万个字节。虽然进程访问L1的开销要比访问L2的开销要大大概5倍左右,但是这仍然比直接访问主存的开销要快510倍。

                       

图表31高速缓存存储器结构

    L1L2高速缓存是用一种被称之为静态随机访问存储器(SRAM)的硬件技术实现的。随着芯片技术的发展,出现了多核处理器,在L1L2高速缓存的基础上甚至出现了L3 Cache,它介于L2 Cache和主存之间,L3 Cache的容量又比L2 Cache的容量大出一个数量级,访问速度也略微慢一点儿,但是仍然比直接访问主存的速度要高出许多,这样,L2 Cache就缓存来自L3 Cache中的缓存行,而L3 Cache就缓存来自主存中的缓存行了,其中L1 CacheL2 Cache为处理器单个核所拥有,L3 Cache为所有核所共享,下图为Intel Core i7Cache结构:

  

图表32 Intel Core i7四核的Cache结构

   本文仅仅列举L1 CacheL2 Cache,想了解L3 Cache的更多知识,读者可以查阅相关资料。

3.1存储器的层次结构

通过对寄存器、Cache、主存、外部存储器、网络存储设备等进程比较,不难发现,它们之间通过明确的层次关系构成现代计算机的存储层次结构,如下图所示:

                              

图表33现代计算机系统存储器的层次结构

在整个层次结构中,从上至下,设备变得更慢、更大,并且每个字节的造价会变得更便宜。其中,寄存器堆在层次结构的最顶层,也就是第0级,记为L0L1 Cache位于第一层,L2 Cache位于第二层,主存位于第三层。

在存储器的层次结构中,一个层次上册存储器是作为下一层存储器的高速缓存。就像上图所示,L0寄存器堆是L1的高速缓存,L0中缓存的是取自L1中的字,L1 CacheL2 Cache的高速缓存,L1中缓存的是取自L2中的缓存行,而L2 Cache有是主存的高速缓存,L2 Cache缓存的是取自主存的缓存行,依次类推,主存是本地磁盘的高速缓存,而在一些分布式系统中,本地磁盘就是其他网络磁盘上存储的数据的高速缓存。

程序员在编写程序的时候,就可以通过对整个计算机系统的存储器的层次结构的理解,运用L1L2等这些缓存的思想来提升自己的程序运行性能(比如在现代数据库的实现的时候,会在主存中开辟一块缓存区存储磁盘上的数据)。

在了解了现代计算机系统的程序执行和Cache以及计算机的整个体系结构以后,下面,我们粗略的看一下操作系统的硬件管理、进行和线程、虚拟存储器以及网络通信等知识。

4.     再看OS

   在了解了程序的执行和存储结构后,我们有必要来回顾一下前面一章所讲解的操作系统,不过这里,我们主要从逻辑上了解一下,在操作系统的眼里,这些计算机系统硬件究竟是些什么,他究竟是如何对这些硬件进行管理的,与此同时,我们有必要大致的了解一下操作系统中的进程和线程、虚拟存储器、网络通信等概念,为后续更加深入对这些有关计算机系统的核心基础知识的讲解作铺垫。

4.1OS看硬件

在前面的hello程序当中,当我们用shell加载和运行hello程序的时候,hello程序会按照我们的要求打印出我们需要的字符串,但是整个过程中,hello程序都没有直接访问硬件,取而代之的是,它依靠操作系统所提供的服务间接的对计算机系统的硬件资源进行访问,因此我们也就不难理解前面章节所讲解的:操作系统是应用程序和计算机硬件进行交互的媒介,所有应用程序和用户对计算机硬件的操作访问都依赖于操作系统提供的服务,如下图所示:

  

图表41现代计算机结构视图

从上图可以看出,操作系统处于计算机硬件和应用程序的中间层,一方面,操作系统会防止计算机硬件被失控的应用程序滥用;另一方面,操作系统为应用程序提供操纵各种不同低级计算机硬件设备简单一致的方法。

在操作系统的逻辑上,它会将这些硬件资源抽象成进程、虚拟存储器和文件等概念来实现这两个功能,如下如所示:

    

图表42操作系统对计算机硬件资源的抽象

如上图所示,操作系统中的进程是对处理器、主存、I/O设备的抽象,虚拟存储器是对主存、I/O设备的抽象,而文件就是对I/O设备的抽象,这也符合了我们前面的章节所讲解的文件的概念。

4.2进程和线程

4.2.1      进程

当形如hello这样的程序在现代操作系统上运行的时候,操作系统会提供一种假象,看上去整个操作系统上就只有这一个程序在运行,该程序看上去会独占操作系统的所有硬件资源,而处理器看上去就像在不间断的一条接一条的执行程序中的指令,直到程序执行完毕。该程序的代码和数据看上去好像是系统所存储的唯一对象,而造成这种假象的原因是因为操作系统通过进程的概念来管理计算机的硬件资源,可以说,进程的提出是计算机科学中最为重要的概念之一。

可以看出进程是操作系统对运行程序的一种抽象。在现代的多任务操作系统中,可以允许同时运行多个进程,而每个进程都看似独占操作系统的硬件资源,这种现象我们称为并发运行,但是实际上,一个进程的指令和另外其他进程的指令在系统中是交错执行的,而实现这种交错执行的机制被称为上下文切换(context switching

操作系统可以保存进程在运行时的所有状态信息,这些状态信息被称为该进程的上下文(context,上下文中包含了许多的进行信息(PC和寄存器堆的当前值、主存中的内容等)。在任何一个时刻,系统上都只有一个进程在运行,当操作系统需要将当前进程的控制权转移到一个新的进程时,就会进行上下文切换,也即保存当前进程的上下文信息,并恢复新进程的上下文信息,然后将控制权转移到新进程上,这样新进程就会从它上次停止的地方开始继续运行,具体如下图所示:

                     

图表43 shell进程和hello进程上下文切换

为了便于理解,我们就Linux/Unix上的Shell进程和hello进程的运行过程为例,最开始的时候,系统上面只有shell进行(上图Process A)运行,当我们在shell命令窗口输入./hello并回车后,系统就知道我们此时需要运行hello程序(上图Process B)了,此时shell会通过一个专门的函数调用(也称为系统调用 system call),来执行我们运行hello程序的请求,系统调用会将控制权传递给操作系统,操作系统保存shell进程的上下文,创建一个新的hello进行的上下文,然后将控制权交给新的hello进程。在hello进程执行完毕后(有可能在hello进程还没有执行完就需要将控制权交给另外一个进程),操作系统恢复shell进程的上下文,并将控制权交回给shell,等待shell上下一个命令行的输入。

进程的抽象概念是需要操作系统和计算机硬件的紧密结合方可实现,至于如何实现,后续会有专门的章节进行讲解。由上图可知,由于存在进程的上下文切换,必然会打乱进程的执行时间,这就使得程序员很难获取进程运行时间的准确和重复测试的值,因此程序员就必须采用其他的办法来获取,至于方法,我们后续谈到时间的时候再讨论。

4.2.2      线程

通常情况下,我们认为一个进程只有单一的控制流,只要操作系统将控制权交给这个进程,那么控制权就会被进程中的单个流所独占,但是,在现代的计算机系统上,一个进程实际上可以存在多个控制流,也就是说,一个进程可以有多个被称为线程的执行单元组成,每个线程都运行于进程的上下文中,并共享同样的代码和全局数据。

由于网络服务器对并行处理的要求越来越高,而且多线程环境通常比多进程环境更容易实现数据共享而使得多线程一般比多进程具有更高的效率,因此线程也就成为编程模型中越来越重要的概念。

关于进程与线程的细节,我们后续会有专门的章节进行讲解。

4.3虚拟存储器

虚拟存储器是一个抽象的概念,他为每个进程提供一种假象,好像每个进程都独占的使用主存,因此每个进程所看到的存储器都是一致的,我们称之为虚拟地址空间Linux/Unix的虚拟地址空间如下图所示:

                      

图表44进程的虚拟地址空间

如上图所示,地址空间的最上面的部分是预留给操作系统的代码和数据的,这个对所有的进程都是一样,余下底下的部分是预留给用户进程的代码和数据使用的,该图中的地址是从下到上递增。

每个进程所看到的虚拟地址空间都有大量准确定义的区域(well-defined areas)构成,每个区域都有专门的功能,具体如下:

l 程序代码和数据:对所有进程来说,程序的代码都是从一个固定的地址开始,紧跟着就是和C的全局变量相对应的数据区域,代码和数据区域是由可执行目标文件的内容直接初始化的,比如我们示例中的hello可执行文件。

l :在代码和数据后面紧跟着就是堆,代码和数据区域在进程一开始的时候就会被指定大小,但是堆不同,堆是作为调用像mallocfree等这些C标准库函数的结果,它可以在运行时动态的扩展和收缩。

l 共享库:在地址空间的中间部分,是用来存放像C标准库等这些共享库的代码和数据的区域,共享库的是一个很强大但是很难理解的概念,后续会有专门的讲解。

l :在虚拟地址空间较为顶端的部分是用户,编译器通过它来实现函数的调用。和堆一样,用户可以在程序的执行期间动态的扩展和收缩,比如,每次我们调用一个函数的时候,就会增长,每次从一个函数返回的时候,就会收缩。

l 内核虚拟存储器:虚拟地址空间的顶部区域是预留给操作系统的内核区域,内核是操作系统常驻存储器中的部分,内核区域是不允许被应用程序进行读写,内核代码定义的函数也不允许被应用程序直接调用。

虚拟存储器的运作需要计算机系统的硬件和操作系统软件之间的精密复杂的相互合作,包括对由处理器生成的每个地址的硬件翻译,其基本思想就是:将进程的虚拟存储器的内容存储在磁盘上,然后用主存作为磁盘的高速缓存。后续对此后专门的讲解。

4.4文件

Linux/Unix中的文件就是字节序列。每个I/O设备(磁盘、键盘、鼠标、显示器、网络等)都可以被看成是文件。系统中的所有输入输出都是通过被称为Unix I/O小组系统函数的调用来读写文件实现的。

文件是一种简单而精致的概念,也是一个非常强大的计算机系统设计思想,它使得应用程序可以按照一个统一的标准去看待计算机系统中所有可能出现的I/O设备,比如,应用程序员在处理磁盘内容的时候,就可以不必要去了解具体的磁盘技术了,也即是说,同一个应用程序可以在使用不用磁盘技术的不同系统上正常的运行了。

4.5网络通信

随着网络技术的发展,现代计算机系统就不能够作为一个孤立的软硬件的结合体进行工作了,它有可能需要通过网络世界和其他的计算机系统连接到一起,但是,从一个单独的计算机系统来看,网络也未尝不可被看成是该系统的又一个I/O设备,如下图所示:

 

图表45网络也是一种I/O设备

   如上图所示,当系统从主存拷贝一串字符串到网络适配器时,该字符串数据流通过网络到达另一台机器,而不是本地磁盘驱动器,相似的,系统可以从网络读取从其他机器发送而来的信息,并将这些信息拷贝到自己的主存,然后对他们进行处理;随着Internet全球网络的出现,从一条主机拷贝信息到另一台主机已经成为计算机重要的通信手段之一(比如:电子邮件、即时消息、FTPtelnet等都是通过网络信息拷贝实现的)。

5.     抽象

   讲到这里,我们不难发现,在计算机的世界里,进程、虚拟存储器、文件等,都是操作系统对计算机硬件资源的一种逻辑上的抽象:进程,是操作系统对处理器、I/O设备、主存的抽象;虚拟存储器,是操作系统对I/O设备、主存的抽象;而文件,是操作系统对I/O设备的抽象。

   通过对计算机系统不同层级的抽象,我们可以对下层硬件的具体复杂实现机制进行很好的隐藏,这样,各个层级之间的联系就变得简单明了,抽象的思想也就成为了计算机系统当中一个重要的主题,稍后我们可以看到,不仅在计算机的操作系统当中存在着抽象,而且在处理器的设计当中也存在着抽象,甚至在具体的程序设计语言中,抽象的思想也无处不在。

   其实,早在上世纪60~70年代,IBM公司就提出了另一种抽象:虚拟机(Virtual Machine,它是对整个计算机,包括操作系统、处理器以及应用程序抽象。

   虽然虚拟机的抽象概念在很早就被提了出来,但是在最近才得到突出的应用。我们可以通过虚拟器去管理那些被设计成在多个操作系统上都能够运行的程序(比如java程序的跨平台性,就是通过一种Java虚拟机(Java Virtual Machine)的思想去实现的,这样同一个Java程序就可以在多个操作系统上运行);与此同时,虚拟机还提供了一种方法去,这种方法使得同一个程序能够在同一个操作系统的不同版本上都能够很好的运行。有了虚拟机的概念后,计算机系统的抽象层次如下如所示:  


图表51现代计算机系统的一些抽象

在计算机的程序设计语言中,抽象的思想也得到了充分的利用,利用抽象的思想,我们可以为一些功能性的函数规划出某些应用程序接口(API),这些接口提供给程序员调用,但是程序员可以仅仅只是停留在调用的层面上,而不必去挖掘这些接口的内部的具体实现细节就能够实现他们想要的功能,不同的程序设计语言会对抽象提供不同格式不同层级的支持,比如C语言的函数原型和Java语言的接口声明。

在处理器的设计方面,抽象的思想也有体现,指令集结构(instruction set architecture就是对实际的处理器硬件的抽象,有了这个抽象,在一个处理器上运行的机器码程序表面上看起来就好像它一次只执行一条指令,但实际上,下层的硬件则要复杂得多,它们需要并行的执行多条指令,但是这些指令都是在一个统一的模型下简单有序的执行。有了这个统一的执行模型,同一段机器码就能够在不同的处理器的实现上运行,只是会在代价和性能上有所差异。

我们都知道,计算机系统最为主要的目的就是对信息进行处理,那么在我们从整体上了解了计算机系统后,就有必要去深入理解一下在计算机的世界里,信息究竟是如何表示的,计算机究竟是如何对信息进行处理的,这就是我们下一章节需要了解的信息:计算机信息的处理。

0 0
原创粉丝点击