深入理解计算机系统之计算机系统漫游(二)

计算机系统漫游

存储设备形成层次结构

在处理器和一个较大较的设备(例如主存)之间插入一个更小更快的存储设备(例如高速缓存)的想法已经成为一个普遍的观念。实际上,每个计算机系统中的存储设备都被组织成一个存储器层次结构,如图1-8所示:

图1-8   一个存储器层次结构的示例

在这个层次结构中,从上至下,设备的访问速度越来越慢、容量越来越大,并且每字节的造价也越来越便宜。寄存器文件在层次结构中位于最顶部,即第0级(或记为L0)。这里我们展示的是三层高速缓存L1到L3,占据存储器结构的第一层到第三层。主存再第四层以此类推。

存储器层次结构的主要思想是上一层存储器作为低一层存储器的高速缓存。因此,寄存器文件就是L1的高速缓存,L1是L2的高速缓存,L2是L3的高速缓存……主存又是磁盘的高速缓存。在某些具有分布式文件系统的网络系统中,本地磁盘就是存储在其他系统磁盘上的数据的高速缓存。

操作系统管理硬件

让我们回到hello程序的例子。当shell加在和运行hello程序时,以及hello程序输出自己的消息时,shell和hello程序都没有直接访问键盘、显示器或者主存,它们依赖的是操作系统提供的服务。我们可以把操作系统看成应用程序和硬件之间插入的一层软件,所有应用程序对硬件的操作尝试都必须通过操作系统,如图1-9所示:

 

图1-9   计算机系统额分层试图

操作系统有两个基本功能:(1)防止硬件被失控的应用程序滥用;(2)向应用程序提供简单一致的机制来控制复杂而又大不相同的低级硬件设备。操作系统通过几个基本的抽象概念(进程、虚拟内存和文件)来实现这两个功能。如图1-10所示:

图1-10   操作系统提供的抽象表示

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

进程

像hello程序在现代系统上运行,操作系统会提供一种假象,好像系统行只有这个程序运行。程序看上去像是独占地使用处理器、主存和I/O设备。处理器看上去就像不间断地一条条执行程序的指令,这种假象是通过进程的概念来实现的。

进程是操作系统对一个正在运行的程序的一种抽象。在一个系统上可以同时运行多个进程,而每个进程好像在独占地使用硬件。而并发运行,则是说一个进程的指令和另一个进程的指令是交错执行的。在大多数系统中,需要运行的进程数是大于它们的CPU个数。传统系统在一个时刻只能执行一个程序,而先进的多核处理器可以同时执行多个程序。无论是在单核还是多核系统中,一个CPU看上去都像是在并发地执行多个进程,这是通过处理器在进程间切换来实现的。操作系统实现这种交错执行的机制称为上下文切换。

操作系统保持跟踪进程运行所需的所有状态信息。这种状态,也就是上下文,包括许多信息,比如PC和寄存器文件当前的值,以及主存的内容。在任何一个时刻,单处理器系统都只能执行一个进程的代码。当操作系统决定要把控制权从当前进程转移到某个新进程时,就会进行上下文切换,即保存当前进程的上下文,恢复新进程的上下文,然后将控制权传递到新进程。新进程就会从它上次停止的地方开始。图1-11展示了hello程序运行场景的基本理念:

图1-11   进程的上下文切换

示例场景有两个并发进程:shell进程和hello进程。最开始,只有shell进程在运行,即等待命令行上的输入。当我们让它运行hello程序时,shell通过调用一个专门的函数,即系统调用,来执行我们的请求,系统调用会将控制权传递给操作系统。操作系统保存shell进程的上下文,创建一个新的hello进程及其上下文,然后将控制权传给hello进程。hello进程终止后,操作系统恢复shell进程的上下文,并将控制权传回给它,shell进程会继续等待下一个命令行的输入。

从一个进程到另一个进程的转换是由操作系统内核管理的。内核是操作系统代码常驻主存的部分,当应用程序需要操作系统的某些操作时,比如读写文件,它就执行一条特殊的系统调用指令,将控制权传递给内核。然后内核执行被请求的操作并返回应用程序。注意,内核不是一个独立的进程。相反,它是系统管理全部进程所用代码和数据结构的集合。

线程

尽管通常我们认为一个进程只有单一的控制流,但是在现代系统中,一个进程中实际上可以由多个称为线程的执行单元组成,每个线程都运行在进程的上下文中,并共享同样的代码和全局数据。由于网络服务器对并行处理的需求,线程成为越来越重要的编程模型,因为多线程之间并多进程之间更容易共享数据,也因为线程一般来说都比进程更高效。当有多处理器可用时,多线程也是一种使得程序可以运行得更快的方法。

虚拟内存

虚拟内存是一个抽象概念,它为每个进程提供一个假象,即每个进程都独占地使用主存。每个进程看到的内存都是一致的,称为虚拟地址空间。图1-12所示的是Linux进程的虚拟地址空间:

图1-12   进程的虚拟地址空间

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

每个进程看到的虚拟地址空间由大量准确定义的区构成,每个区都有专门的功能。我们从最低的地址开始,逐步向上介绍:

程序代码和数据:对所有的进程来说,代码是从同一固定地址开始,紧接着是和C全局变量相对应的数据位置。代码和数据区是直接按照可执行目标文件的内容初始化的,在示例中就是可执行文件hello。

堆:代码和数据区后紧随着的是运行时堆。代码和数据区在进程一开始运行时就被指定了大小,与此不同,当调用像malloc和free这样的C标准库函数时,堆可以在运行时动态地扩展和收缩。

共享库:大约在地址空间的中间部分是一块用来存放像C标准库和数学库这样的共享库的代码和数据的区域。

栈:位于用户虚拟地址空间顶部的是用户栈,编译器用它来实现函数调用。和堆一样,用户栈在程序执行期间可以动态地扩展和收缩。

内核虚拟内存:地址空间顶部的区域是为内核保留的。不允许应用程序读写这个区域的内容或直接调用内核代码定义的函数。相反,它们必须调用内核来执行这些操作。

虚拟内存的运作需要硬件和操作系统软件之间精密复杂的交互,包括对处理器生成的每个地址的硬件翻译。基本思想是把一个进程虚拟内存的内容存储在磁盘上,然后用主存作为磁盘的高速缓存。

文件

文件就是字节序列,每个I/O设备,包括磁盘、键盘、显示器,甚至是网络,都可以看成是文件。系统中所有输入输出都是通过使用一组称为Unix I/O的系统函数调用读写文件来实现的。文件向应用程序提供了一个统一的视图,来看待系统中可能含有的所有各式各样的I/O设备。处理磁盘文件内容的应用程序员无需了解磁盘技术,便可使得自己的程序在使用不同磁盘技术的不同系统上运行。

系统之间利用网络通信

系统漫游至此,我们一直把系统视为一个孤立的硬件和软件的集合体。实际上,现代系统经常通过网络和其他系统连接到一起。从一个单独的系统来看,网络可视为一个I/O设备,如图1-13所示:

图1-13   网络也是一种I/O设备

当系统从主存复制一串字节到网络适配器时,数据流经过网络到达另外一台机器,而不是说到达本地磁盘驱动器。相似地,系统可以读取从其他机器发送来的数据,并把数据复制到自己的主存。

随着Internet这样的全球网络的出现,从一台主机复制信息到另外一台主机已经成为计算机系统最重要的用途之一,比如像电子邮件、即时通信、万维网、FTP、和telnet这样的应用都是基于网络复制信息的功能。

回到hello示例,我们可以使用熟悉的telnet应用在一个远程主机上运行hello程序。假设用本地主机上的telnet客户端连接远程主机上的telnet服务器。在我们登陆到远程主机并运行shell后,远程的shell就在等待接收输入命令。此后在远端运行hello程序包括如图1-14所示的五个基本步骤:

图1-14   利用telnet通过网络远程运行hello

Amdahl定律

当我们对系统的某个部分加速时,其对系统整体性能的影响取决于该部分的重要性和加速程度,若系统执行某应用程序需要时间为Told,假设系统某部分所需执行时间与该时间的比例为α,而该部分性能提升比例为k。即该部分初始所需时间为αTold,现在所需时间为(αTold)/k。因此,总的执行时间为:

由此可以计算加速度比S=Told/Tnew为:

举个栗子,系统的某个部分初始耗时比例为60%(α为0.6),其加速度比例因子为3(k=3)。则我们可以获得的加速度比为1/[0.4+0.6/3]=1.67倍。虽然我们对系统一个主要部分做出改进,但获得的系统加速度比却明显小于该部分的加速比。这就是Amdahl定律的主要观点:想要显著加速整个系统,必须提升全系统中相当大的部分的速度。

并发与并行

线程级并发

使用线程,我们可以在一个进程中执行多个控制流。在以前,即使处理器必须在多个任务间切换,大多数实际的计算也都是由一个处理器来完成的。这种配置称为单处理器系统。当构建一个由单操作系统内核控制的多处理器组成的系统时,我们就得到一个多处理器系统。图1-15给出了这些不同处理器类型的分类:

图1-15   不同的处理器配置分类。随着多核处理器和超线程的出现,多处理器变得普遍了

多核处理器是将多个CPU(称为“核”)集成到一个集成电路芯片上。图1-16描述的是一个典型的多核处理器的组织结构,其中微处理器芯片有四个CPU核,每个核都有自己的L1和L2高速缓存,其中L1高速缓存分为两个部分,一个保存最近取到的指令,另一个存放数据。这些核共享更高层次的高速缓存,以及主到主存的接口。

图1-16   多核处理器的组织结构。四个处理器核集成在一个芯片上

超线程,有时称为同时多线程,是一项允许一个CPU执行多个控制流的技术。它涉及CPU某些硬件有多个备份,比如程序计数器和寄存器文件,而其他的硬件部分只有一份,比如执行浮点算数运算的单元。常规的处理器需要大约20000个时钟周期做不同线程间的转换,而超线程的处理器可以在单个周期的基础上决定要执行哪个线程。使得CPU能够更好地利用它的处理资源。假设一个线程必须等待某些数据被装载到高速缓存中,那CPU就可以去执行另一个线程。

多处理器的使用可以从两个方面提高系统的性能。首先,它减少了在执行多个任务时模拟并发的需要。其次,它可以使应用程序运行得更快,当然,这要求程序以多线程的方式来书写,这些线程可以并行地高效执行。

指令级并行

在较低的抽象层次上,现代处理器可以同时执行多条指令的属性称为指令级并行。早期的微处理器,需要多个(通常3~10)时钟周期来执行一条指令。最近的处理器可以保持每个时钟周期2~4条指令的执行速率。其实每条指令从开始到结束需要长得多的时间,大约20个或更多的周期,但处理器通常使用非常多聪明的技巧来同时处理多达100条指令。如果处理器可以达到比一个周期一条指令更快的执行效率,就称为超标量处理器。大多数现代处理器都支持超标量操作。

单指令、多数据并行

在最低层次上,许多现代处理器都拥有特殊的硬件,允许一条指令产生多个可以并行执行的操作,这种方式称为单指令、多数据,即SIMD并行。例如,较新几代的Intel和AMD处理器都具有并行地对八对单精度浮点数(C语言类型float)做加法的指令。提供这些SIMD指令多是为了提高处理影像、声音和视频数据应用的执行速度。虽然有些编译器会试图从C语言中自动抽取SIMD并行性,但是更可靠的方法是用编译器支持的特殊的向量数据类型写程序,比如GCC就支持向量数据类型。

计算机系统中抽象的重要性

抽象的使用是计算机科学中最为重要的概念之一。例如,为一组函数规定一个简单的应用程序接口(API)就是一个很好的编程习惯,程序员无需了解它内部的工作便可使用这些代码。不同的编程语言提供不同形式和等级的抽象支持,例如Java类的声明和C语言的函数原型。

我们已经介绍了计算机系统中使用的几个抽象,如图1-17所示。在处理器里,指令集架构提供了对实际处理器硬件的抽象。使用这个抽象,机器代码程序表现得就好像运行在一个一次只执行一条指令的处理器上。底层的硬件远比抽象描述的复杂精细,它并行地执行多条指令,但又总是与那个简单有序的模型保持一致。只要执行模型一样,不同的处理器实现也能执行同样的机器代码,而又提供不同的开销和性能。

图1-17   计算机系统提供的一些抽象。计算机系统中一个重大的主题就是提供不同层次的抽象表示,来隐藏实际实现的复杂性

猜你喜欢

转载自www.cnblogs.com/beiluowuzheng/p/11098706.html