二、深入理解计算机系统 第一章 计算机系统漫游

第一章 计算机系统漫游

  • 通过研究“hello world”这个简单程序的生命周期,介绍计算机系统的主要概念和主题
    #include<stdio.h>
    main()
    {
       printf("hello world\n");
    }
    

1-1 信息就是位+上下文

  • 我们的hello程序的生命是从一个源程序(或者说源文件)开始的,该源程序由程序员通过编辑器创建并保存为文本文件,文件名就是hello.c。源程序实际上就是一个由0和1组成的位(又称位比特)序列,这些位被组织成8个一组,称为字节。每个字节都表示程序中某个中文字符。
  • 大部分的现代系统都是使用ASCII标准来表示文本字符,这种方式实际上就是用一个惟一的字节大小的整数值来表示每个字符,hello.c程序是以字节序列的方式存储再文件中的。每个字节都有一个整数值,对应某个字符,例如第一个字节的整数值是35,它对应的就是字符#,第二个字节整数值为105,它对应的字符是i,注意每行文本都是以一个看不见的换行符\n来结束的,它对应的整数值为10,像hello.c这样只有ASCII字符构成的文件称为文本文件,所有其他文件则称为二进制文件。
    在这里插入图片描述
  • hello.c的表示方法说明了一个基本的思想:系统中所有的信息–包括磁盘文件、存储器中的程序、存储中存放的用户数据以及网络上传送的数据,都是由一串比特表示的。区别不同数据对象的唯一方法就是我们读到这些数据对象是的上下文。比如,在不同的上下文中同样的字节序列可能表示一个整数、浮点数或者机器指令。
  • 作为程序员,我们需要了解机器表示方式,因为他们常见的整数和实数是不同的,也有相似。

1-2 程序被其他程序翻译成不同的格式

  • 在hello程序生命周期一开始时是一个可读的高级C程序。然而,为了在系统上运行hello.c程序,每条C语句都被必须被其他程序转化为一系列的低级机器语言指令。然后这些指令按照一种可执行目标程序的格式打好包,并以二进制磁盘文件的形式存放起来。目标程序也称可执行目标文件
  • 在unix系统上,从源文件到目标文件的转化是由编译器驱动程序完成的:unix> gcc -o hello hello.c

    在这里gcc编译器驱动程序读取源文件hello.c,并把它翻译成可执行目标文件hello。这个翻译的过程分为四个阶段完成的,执行四个阶段的程序(预处理、编译器、汇编器和连接器)一起构成了编译系统。
    在这里插入图片描述

    • 预处理阶段。预处理器(cpp)根据以字符#开头的命令(directives),修改原始的C程序。比如hello.c中第一行的#include<stdio.h>指令告诉预处理器读取系统头文件stdio.h的内容并把它直接插入到程序文本中去。结果就得到了另一个C程序,通常是以.i作为文件扩展名
    • 编译阶段。编译器(cc1)将文本文件hello.i翻译成文本文件hello.s,它包含一个汇编语言程序。汇编语言程序中的每条语句都以一种标准的文本格式确切地描述了一条低级机器语言指令。汇编语言是非常有用的,因为它为不同高级语言的不同编译器提供了通用的输出语言。例如,C编译器和Fortran编译器产生的输出文件用的都是一样的汇编语言。
    • 汇编阶段。接下来,汇编器(as)将hello.s翻译成机器语言指令,把这些指令打包成为一种叫做可重定位(relocatable)目标程序的格式,并将结果保存在目标文件belloo中。hello.s文件是一个二进制文件,它的字节编码是机器语言指令而不是字符。如果我们在文本编辑器中打开hello.c文件,呈现的将是一堆乱码。
    • 链接阶段。请注意,我们的hello 程序调用了printf函数,它是标准C库中的一个函数,每个C编译器都提供.printf函数存在于一个名为printf.o的单独的预编译目标文件中,而这个文件必须以某种方式并入到我们的hello.o程序中。链接器(ld)就负责处理这种并入,结果就得到hello文件,它是一个可执行目标文件(或者简称为可执行文件)。可执行文件加载到存储器后,由系统负责执行

1-3 了解编译系统如何工作是大有益处的

  • 对于像hello.c这样简单的程序,我们可以依靠编译系统生成正确有效的机器代码。但是,有一些重要的原因促使程序员必须知道编译系统是如何工作的。
    • 优化程序性能。现代编译器都是成熟的工具,通常可以生成很好的代码。作为程序员,我们无须为了写出高效代码而去了解编译器的内部工作。但是,为了在我们的C程序中做出好的代码选择,我们确实需要对汇编语言以及编译器如何将不同的C语句转化为汇编语言有一些基本的了解。比如,一个switch语句是不是总是比一系列的if-then-else语句高效得多?一个函数调用的代价有多大?while 循环比do循环更有效吗?指针引用比数组索引更有效吗?相对于用通过引用传递过来的参数求和,为什么用本地变量求和的循环,其运行就会快得多呢?为什么两个功能相近的循环的运行时间会有很大差异?
      在第3章中,我们将介绍IntelIA32机器语言,并阐述编译器是如何将不同的C程序结构翻译成机器语言的。在第5章中,你将学习如何通过对C代码做些简单转换,帮助编译器更好地完成工作,从而调整你的C程序的性能。然后在第6章,你将学习存储器系统的层次特性,C编译器是如何将数组存放在存储器中,以及你的C程序又是如何能够利用这些知识从而更高效地运行。
    • 理解链接时出现的错误。根据我们的经验,一些最令人困扰的程序错误往往都与链接器操作有关,尤其是当你试图建立大型的软件系统时。比如,链接器报告说它无法解析一个引用,这是什么意思?静态变量和全周变量的区别是什么?如果你在不同的C文件中定义了名字相同的两个全局变量会发生什么?静态库和动态库的区别是什么?为什么我们在命令行上排列库的顺序是有影响的?最为烦人的是,为什么有些链接错误直到运行时才出现?在第7章中,你将了解到这些问题的答案。
    • 避免安全漏洞。近年来,缓冲区溢出错误造成了大多数网络和Internet服务器上的安全漏洞。这些错误的存在是因为太多的程序员忽视了编译器用来为函数产生代码的堆栈规则。作为学习汇编语言的一部分,我们将在第3章中描述堆栈规则和缓冲区溢出错误。

1-4 处理器读并解释储存在存储器中的指令

  • 此刻,我们的hello.c源程序已经被编译系统转换成了可执行目标文件hello,并被存放在磁盘上。为了在Unix系统上运行该可执行文件,我们将它的文件名输入到称为shell的应用程序中:
    unix>./hello 
    hello, world 
    unix>
    # shell是一种命令行解释器,它输出一个提示符,等待你输入一行命令,然后执行这个命令。
    # 如果该命令行的第一个单词不是一个内置的shell命令,那么shell就会假设这是一个可执行文件的名字要加载和执行该文件。
    # 所以在此例中,shell将加载和执行hello程序,然后等待程序终止。hello 程序在屏幕上输出它的信息,然后终止。
    # shell随后输出一个提示符,等待下一个输入的命令行。
    

(一)系统的硬件组成

  • 为了了解运行时hello程序发生了什么,我们需要理解一个典型系统的硬件组织,如图1.4所示。这张图是IntelPentium系统产品族的模型,但是所有其他系统也有相同的外观和特性。现在不要担心这张图很复杂–我们将在贯穿这本书的课程中分阶段介绍大量的细节。
    在这里插入图片描述
  • CPU:中央处理单元;ALU:算术/逻辑单元:PC:程序计数器;USB:通用串行总线。
    • 总线
      贯穿整个系统的是一组电子管道,称做总线,它携带信息字节并负责在各个部件间传递通常总线被设计成传送定长的字节块,也就是字(word)。字中的字节数(即字长)是一个基本的系统参数,各个系统中也不尽相同。比如,IntelPentium系统的字长为4字节,而服务器类的系统,例如Intel Itaniums和高端的Sun公司的SPARCS的字长为8字节。用于汽车和工业中的嵌入式控制器之类较小的系统的字长往往只有1或2字节。为了便于描述,我们假设字长为4字节,并且假设总线一次只传1个字。
    • I/O 设备
      I/O(输入/输出)设备是系统与外界的联系通道。我们的示例系统包括四个I/O设备:作为用户输入的键盘和鼠标,作为用户输出的显示器,以及用于长期存储数据和程序的磁盘驱动器(简单地说就是磁盘)。最开始,可执行程序hello 就放在磁盘上。
      每个I/O设备都是通过一个控制器或适配器与I/0总线连接起来的控制器和适配器之间的区别主要在于它们的组成方式。控制器是I/0设备本身中或是系统的主印制电路板(通常被称做主板)上的芯片组,而适配器则是一块插在主板插槽上的卡。无论如何,它们的功能都是在I/O 总线和 I/O设备之间传递信息。
      第6章会更多地说明磁盘之类的I/O设备是如何工作的。在第11章中,你将学习如何在应用程序中利用Unix的I/0接口访问设备。我们尤其关注特别有趣的网络类设备,不过这些技术也适用于其他设备。
    • 主存
      主存是一个临时存储设备,在处理器执行程序时,它被用来存放程序和程序处理的数据物理上来说,主存是由一组DRAM(动态随机存取存储器)芯片组成的。逻辑上来说,存储器是由一个线性的字节数组组成的,每个字节都有自己惟一的地址(数组索引),这些地址是从零开始的。一般来说,组成程序的每条机器指令都由不定量的字节构成。与C程序变量相对应的数据项的大小是根据类型变化的。比如,在运行Linux的Intel机器上,short类型的数据需要2字节,int、float 和long类型则需要4字节,而double类型需要8字节。
      第6章具体说明存储技术,比如DRAM是如何工作的,以及它们又是如何组合起来构成主存的。
    • 处理器
      中央处理单元(CPU)简称处理器,是解释(或执行)存储在主存中指令的引擎处理器的核心是一个被称为程序计数器(PC)的字长大小的存储设备(或寄存器)。在任何一个时间点上,PC都指向主存中的某条机器语言指令(内含其地址)。
      从系统通电开始,直到系统断电,处理器一直在不假思索地重复执行相同的基本任务:从程序计数器(PC)指向的存储器处读取指令,解释指令中数器(PC)指向的存储器处读取指令,解释指令中的位,执行指令指示的简单操作,然后更新程序计数器指向下一条指令,而这条指令并不一定在存储器中和刚刚执行的指令相邻。
      这样的简单操作的数目并不多,它们在主存、寄存器文件(registerfile)和算术逻辑单元(ALU)之间循环。寄存器文件是一个小的存储设备,由一些字长大小的寄存器组成,这些寄存器每个都有惟一的名字。ALU计算新的数据和地址值。下面是一些简单操作的例子,CPU在指令的要求下可能会执行这些操作。
  • 一些概念
    • 加载:从主存拷贝一个字节或者一个字到寄存器,覆盖寄存器原来的内容。
    • 存储:从寄存器拷贝一个字节或者一个字到主存的某个位置,覆盖这个位置上原来的内容。
    • 更新:拷贝两个寄存器的内容到ALU,ALU将两个字相加,并将结果存放到一个寄存器中,覆盖该寄存器中原来的内容。
    • I/O读:从一个I/O设备中拷贝一个字节或者一个字到一个寄存器。1/0写:从一个寄存器中拷贝一个字节或者一个字到一个I/O设备。
    • 转移:从指令本身中抽取一个字,并将这个字拷贝到程序计数器(PC)中,覆盖PC中原来的值
      第4章将对处理器的工作原理给予更详细的说明。

(二)执行hello程序

  • 通过对系统的硬件组成和操作的简单学习,我们开始能够了解当我们运行示例程序时发生了什么。在这里我们必须忽略很多细节,稍后会做一些补充,但是现在我们将很满意于这种粗略的描述。hello程序的机器指令最初是存放在磁盘上的。当程序加载时,它们被拷贝到主存。当处理器运行程序时,指令又从主存拷贝到处理器
    • (1) 首先shell程序执行它的指令,等待我们输入命令。当我们在键盘上输入字符串“/hello”后, shell程序就逐一读取字符到寄存器,再把它存放到存储器中
    • (2) 当我们在键盘上敲回车键时,shell就知道我们已经结束了命令的输入。然后shell 执行一系列指令,这些指令将hello 目标文件中的代码和数据从磁盘拷贝到主存,从而加载hello文件。数据包括最终会被输出的字符串“hello,world\n”。
      利用称为DMA(直接存储器存取,将在第6章中讨论)的技术,数据可以不通过处理器而直接从磁盘到达主存
    • (3) 一旦hello 目标文件中的代码和数据被加载到了存储器,处理器就开始执行hello程序的主程序中的机器语言指令。这些指令将“hello,world\n”串中的字节从存储器中拷贝到寄存器文件,再从寄存器中文件拷贝到显示设备,最终显示在屏幕上
      在这里插入图片描述

1-5 高速缓存

  • 通过这个简单的示例我们了解到重要的一课,那就是系统花费了大量的时间把信息从一个地方挪到另一个地方。数据串“hello,world/n”开始时在磁盘上,再被拷贝到主存,然后从主存上拷贝到显示设备。从一个程序员的角度来看,大量的拷贝减慢了程序的实际工作。因此,系统设计者的一个主要目标就是使这些拷贝操作尽可能的快
  • 根据机械原理,较大的存储设备要比较小的存储设备运行得慢,而快速设备的造价远高于低速同类设备。比如说,一个典型系统上的磁盘驱动器可能比主存大100倍,但是对处理器而言,从磁盘驱动器上读取一个字的时间开销要比从主存中读取的开销大1000万倍。类似地,一个典型的寄存器文件只存储几百字节的信息,与此相反,主存里可存放几百万字节。然而,处理器从寄存器文件中读数据比从主存中读取要快几乎100倍。更麻烦的是,随着这些年半导体技术的进步,这种处理器与主存之间的差距还在持续增大。加快处理器的运行速度比加快主存的处理速度要容易和便宜得多。
  • 针对这种外理器与主存之间的差异,系统设计者采用了更小更快的存储设备,称为高速缓存存储器(cache memories,简称高速缓存)它们被用来作为暂时的集结区域存放处理器在不久的将来可能会需要的信息。图18展示了一个典型系统中的高速缓存存储器。位于处理器芯片上的L1高速缓存的容量可以达到数万字节,访问速度几乎和访问寄存器文件一样快。一个容量为数十万到数百万的更大的L2高谏缓存是通过一条特殊的总线连接到外理器的。进程访问L2的时间开销要比访问L1的开销大5倍,但是这仍然比访问主存的时间快5~10倍。L1和L2高速缓存是用一种叫做静态随机访问存储器(SRAM)的硬件技术实现的。
    这本书的重要课题之一就是应用程序员通过理解高速缓存存储器的机理,能够利用这些知识极大地提高程序的性能。你将在第6章里学习这些重要的设备,并学习如何利用它们。
    在这里插入图片描述

1-6 形成层次结构的存储设备

  • 在处理器和一个较大较慢的设备(例如主存)之间插入一个更小、更快的存储设备(例如,高速缓存存储器)的想法成为一个普遍的观念。实际上,每个计算机系统中的存储设备都被组织成一个存储器层次模型,就像图19所展示的那样。在这个层次模型中,从上至下,设备变得更慢、更大,并且每字节的造价也更便宜。寄存器文件在层次模型中位干最顶部,也就是第0级或记为L0。L1高速缓存处在第一层(所以称为L1)L2高速缓存占据第二层,主存在第三层,以此类推。
  • 存储器分层结构的主要思想是一个层次上的存储器作为下一层次上的存储器的高速缓存。因此,寄存器文件就是L1的高速缓存,而L1又是L2的高速缓存,L2是主存的高速缓存,主存是磁盘的高速缓存。在某些带分布式文件系统的网络系统中,本地磁盘就是其他系统中磁盘上被存储数据的高速缓存。
  • 就像程序员可以运用L1和L2的知识来提高程序性能一样,程序员同样可以利用对整个存储器层次模型的理解来提高程序性能。第6章将更详细地讨论这个问题。
    在这里插入图片描述

1-7 操作系统管理硬件

  • 让我们回到hello程序的例子。当shell加载和运行hello程序时,当hello程序输出自己的消息时,程序没有直接访问键盘、显示器、磁盘或者主存储器。取而代之的是,它们依靠操作系统提供的服务。我们可以把操作系统看成是应用程序和硬件之间插入的一层软件,如图1.10所示。所有应用程序对硬件的操作尝试都必须通过操作系统
    在这里插入图片描述
  • 操作系统有两个基本功能:
    • 1.防止硬件被失控的应用程序滥用
    • 2.在控制复杂而又通常广泛不同的低级硬件设备方面,为应用程序提供简单一致的方法
  • 操作系统提供的抽象表示:操作系统通过图1.11中显示的几个基本的抽象概念(进程、虚拟存储器和文件)实现这两个功能。如图1.11所示,文件是对I/O设备的抽象表示,虚拟存储器是对主存和磁盘I/0设备的抽象表示,进程则是对处理器、主存和I/O设备的抽象表示。
    在这里插入图片描述

(一)操作系统提供的抽象表示的解释------进程

  • 像hello 这样的程序在现代系统上运行时,操作系统会提供一种假象,就好像系统上只有这个程序在运行。程序看上去独占地使用处理器、主存和I/0设备,而处理器看上去就像在不间断地一条接一条地执行程序中的指令。该程序的代码和数据就好像是系统存储器中惟一的对象。这些假象是通过进程的概念来实现的,进程是计算机科学中最重要和最成功的概念之一。
  • 进程是操作系统对运行程序的一种抽象。在一个系统上可以同时运行多个进程,而每个进程都好像在独占地使用硬件。我们称之为并发运行,实际上是说一个进程的指令和另一个进程的指令是交错执行的操作系统实现这种交错执行的机制称为上下文切换(context switching)。
  • 操作系统保存进程运行所需的所有状态信息。这种状态,也就是上下文(context),包括许多信息,比如PC和寄存器文件的当前值,以及主存的内容。在任何一个时刻,系统上都只有一个进程正在运行。当操作系统决定从当前进程转移控制权到某个新进程时,它就会进行上下文切换,即保存当前进程的上下文、恢复新进程的上下文,然后将控制权转移到新进程。新进程就会从它上次停止的地方开始。图112展示了我们的示例hello运行的基本场景。
  • 在我们的示例场景中有两个同时运行的进程:shell进程和hello进程。最开始,只有shell进程在运行,等待命令行上的输入。当我们让它运行hello程序时,shell通过调用一个专门的函数,即系统调用,来执行我们的请求,系统调用会将控制权传递给操作系统。操作系统保存shell进程的上下文创建一个新的shell进程及其上下文,然后将控制权传给新的hello进程。在hello进程终止后,操作系统恢复shell进程的上下文,并将控制权传回给它,它会继续等待下一命令行输入。
    在这里插入图片描述
  • 实现进程这个抽象概念需要低级硬件和操作系统软件的紧密合作我们将在第8章中揭示这是如何工作的,以及应用程序是如何创建和控制它们的进程的。
  • 进程这个抽象概念还暗示着由于不同的进程交错执行,打乱了时间的概念,使得程序员很难获得运行时间的准确和可重复测量。第9章讨论了现代系统中的各种时间概念,并描述了用来获得准确测量值的技术。
  • 线程:尽管通常我们认为一个进程只有单一的控制流,但是在现代系统中,一个进程实际上可以由多个称为线程的执行单元组成,每个线程都运行在进程的上下文中,并共享同样的代码和全局数据。由于网络服务器中对并行处理的要求,线程成为越来越重要的编程模型,因为多线程之间比多进程之间更容易共享数据,也因为线程一般都比进程更高效。在第13章中,你将学习到并行的基本概念,也包括线程化的概念。

(二)操作系统提供的抽象表示的解释------虚拟存储器

  • 虚拟存储器是一个抽象概念,它为每个进程提供了一个假象,好像每个进程都在独占地使用主存。每个进程看到的存储器都是一致的,称之为虚拟地址空间。图1.13所示的是Linux进程的虚拟地址空间(其他Unix系统的设计也与此类似)。在Linux中,最上面的四分之一的地址空间是预留给操作系统中的代码和数据的,这对所有进程都一样。底部的四分之三的地址空间用来存放用户进程定义的代码和数据。请注意,图中的地址是从下往上增大的。
  • 每个进程看到的虚拟地址空间由大量准确定义的区(area)构成,每个区都有专门的功能。在本书的后面你将学到更多的有关这些区的知识,但是先简单看看每一个区,从最低的地址开始,逐步向上研究将是非常有益的。
    • 程序代码和数据。代码是从同一固定地址开始,紧接着的是和C全局变量相对应的数据区。代码和数据区是由可执行目标文件直接初始化的,在我们的示例中就是可执行文件hello。在第7章我们介绍链接和加载时,你会学习到更多有关地址空间中这部分的内容。
    • 。代码和数据区后紧随着的是运行时堆。代码和数据区是在进程一日开始运行时就被指定了大小的,与此不同,作为调用像malloc和free这样的C标准库必数的结果,堆可以在运行时动态地扩展和收缩。在第10意学习管理虎拟存储器时,我们将更详细地研究堆。
    • 共享库。在地址空间的中间附近是一块用来存放像C标准库和数学库这样共享库的代码和数据的区域。共享库的概念非常强大,但是也是个相当难懂的概念。在第7章我们学习动态链接时,将学习共享库是如何工作的。
    • 。位于用户虚拟地址空间顶部的是用户栈,编译器用它来实现函数调用。和堆一样,用户栈在程序执行期间可以动态地扩展和收缩。特别地,**每次我们调用一个函数时,栈就会增长。每次我们从函数返回时,找就会收缩。**在第3章中你将学习编译器是如何使用栈的。
    • 内核虚拟存储器。内核是操作系统总是驻留在存储器中的部分。地址空间顶部的四分之一部分是为内核预留的。应用程序不允许读写这个区域的内容或者直接调用内核代码定义的雨数。
      虚拟存储器的运作需要硬件和操作系统软件间的精密复杂的互相合作,包括对处理器生成的每个地址的硬件翻译。基本思想是把一个进程虚拟存储器的内容存储在磁盘上,然后用主存作为磁盘的高
      在这里插入图片描述
  • 虚拟存储器的运作需要硬件和操作系统软件间的精密复杂的互相合作,包括对处理器生成的每个地址的硬件翻译。基本思想是把一个进程虚拟存储器的内容存储在磁盘上,然后用主存作为磁盘的高速缓存。第10章将解释它如何工作,以及它为什么对现代系统的运行如此重要。

(三)操作系统提供的抽象表示的解释-------文件

  • 文件只不过就是字节序列。每个I/O设备,包括磁盘、键盘、显示器,甚至于网络,都可可以被看成是文件。系统中的所有输入输出都是通过使用称为Unix I/O的一小组系统函数调用读写文件来实现的。
  • 文件这个简单而精致的概念是非常强大的,因为它使得应用程序能够统一地看待系统中可能含有的所有各式各样的I/O设备。例如,处理磁盘文件内容的应用程序员可以非常幸福地无需了解具体的磁盘技术。进一步说,同一个程序可以在使用不同磁盘技术的不同系统上运行。你将在第11章中学习Unix /O。
    在这里插入图片描述

1-8 利用网络系统和其他系统通信

  • 系统漫游行之至此,我们一直是把系统视为一个孤立的硬件和软件的集合体。实际上,现代系统经常是通过网络和其他系统连接到一起的。从一个单独的系统来看,网络可被视为又一个I/O设备,如图1.14所示。当系统从主存拷贝一串字符到网络适配器时,数据流经过网络到达另一台机器,而不是到达本地磁盘驱动器。相似地,系统可以读取从其他机器发送来的数据,并把数据拷贝到自己的主存。
  • 随着像Internet这样的全球网络的出现,从一台主机拷贝信息到另外一台主机已经成为计算机系统最重要的用途之一。比如,像电子邮件、即时消息传送、万维网、FTP和telnet这样的应用都是其于通过网络拷贝信息的功能的。
    -
  • 回到我们的hello示例,我们可以使用熟悉的telnet应用在一个远程主机上运行hello程序。假设我们用本地主机上的telnet客户端连接远程主机上的telnet服务器。在我们登录到远程主机并运行shell后,远端的shell就在等待接收输入的命令。从这点上来看,在远端运行hello程序包括如图1.15所示的五个基本步骤。
  • 当我们在telnet客户端键入“hello”串并敲下回车键后,客户端软件就会将这个字符串发送到telnet的服务器。在telnet服务器从网络上接收到这个串后,会把它传递给远端shell程序。接下来,远端shell运行hello程序,并将输出行返回给telnet服务器。最后,telnet服务器通过网络把输出串转发给 telnet客户端,客户端就将输出串输出到我们的本地终端上。
  • 这种在客户端和服务器之间交互的类型在所有的网络应用中是非常典型的。在第12章中,你将学会如何构造网络应用程序,并利用这些知识创建一个简单的Web服务器。
    在这里插入图片描述

1-9 小结

  • 我们旋风式的系统漫游到此就结束了。从这次讨论中要得出一个很重要的观点,那就是系统不仅仅只是硬件系统是互相交织的硬件和系统软件的集合体,它们必须共同协作以达到运行应用程序的最终目的。本书的余下部分将对这个论点进行展开。
  • 1. 计算机系统是由硬件和系统软件组成的,它们共同协作以运行应用程序。计算机内部的信息被表示为一组组的位,它们依据不同的上下文又有不同的解释方式。程序被其他程序翻译成不同的形式,开始时是ASCII文本,然后被编译器和链接器翻译成二进制可执行文件。
  • 2. 处理器读取并解释存放在主存里的二进制指令。因为计算机花费了大量的时间在存储器、I/O设备和CPU寄存器之间拷贝数据,所以系统中的存储设备就被按层次排列,CPU寄存器在顶部,接着是多层的硬件高速缓存存储器、DRAM主存储器和磁盘存储器。在层次模型中位于更高层的存储设备比低层的存储设备要快,单位比特造价也更高。程序员通过理解和运用这种存储层次结构的知识可以优化他们C程序的性能。
  • 3. 操作系统内核是应用程序和硬件之间的媒介。它提供三个基本的抽象概念:文件是对I/O设备的抽象概念;虚拟存储器是对主存和磁盘的抽象概念;进程是处理器、主存和/0设备的抽象概念。
  • 4. 最后,网络提供了计算机系统之间通信的手段。从某个系统的角度来看,网络就是一种I/O设备。

第二章 信息的表示与处理

  • 我们对计算机系统的探索是从学习计算机本身开始的,它由处理器和存储器子系统组成。在核心部分,我们需要方法来表示基本数据类型,比如整数和实数运算的近似值。然后,我们考虑机器级指令如何操作这样的数据,编译器如何将C程序翻译成这样的指令。接下来,我们研究几种实现处理器的方法,来更好地了解如何使用硬件资源来执行指令。一旦我们理解了编译器和机器级代码,我们就能通过编写可以更高效编译的源代码,来分析如何最大化程序的性能。我们以存储器子系统的设计来结束本部分,这是现代计算机系统最复杂的部分之一。
  • 现代计算机存储和处理以二值信号表示的信息。这些普通的二进制数字,或者位(bit),形成了数字革命的基础。大家熟悉的使用了1000多年的十进制(以十为基数,base-10)起源于印度,在12世纪被阿拉伯数学家所改进,并在13世纪被意大利数学家LeonardoPisano(更有名的叫法是 Fibonacci)带到西方。使用十进制表示法对于有十个指头的人类来说是很自然的事情,但是当构造存储和处理信息的机器时,二进制值工作得更好。二值信号能够很容易地表示、存储和传输,例如,可以表示为穿孔卡片上有洞或无洞、导线上的高电压或低电压,或者磁场引起的顺时针或逆时针。基于二值信号的存储和执行计算的电子电路非常简单和可靠,使得制造商能够在一个单独的硅片上集成百万个这样的电路。
  • 单独地来说,单个的位不是非常有用,然而,当我们把位组合在一起,再加上某种解释(interpretation),即给予不同的可能位模式以含意,我们就能够表示任何有限集合的元素,比如,使用一个进制数字系统,我们能够用位组来编码非负数。通过使用标准的字符码,我们能够对一份文档中的字母和符号进行编码。在本章中,我们将讨论这两种编码,以及表示负数的编码和近似实数的编码。
  • 我们考虑三种最重要的数字编码无符号(unsigned)编码是基于传统的二进制表示法的,表示大于或者等于零的数字。二进制补码(twos-complement)编码是表示有符号整数的最常见的方式有符号整数就是为正或者为负的数字。浮点数(floating-point)编码是表示实数的科学记数法的以二为基数的版本。计算机用这些不同的表示方法实现算术运算,例如加法和乘法,类似于相应的整数和实数运算。
  • 浮点运算
    • 计算机的表示法用有限的位数来对一个数字编码,因此,当结果太大以至不能表示时,某些运算就会溢出(overflow) 。这会导致某些令人吃惊的后果。例如,在大多数今天的计算机上,计算表达式200300400*500会得出-884901888这违背了整数运算的属性–计算一组正数的乘积产生了一个为负的结果。
    • 另一方面,整数的计算机运算满足了真正整数运算的许多普通的属性。例如,乘法是可结合的和可交换的,这样一来计算下面任何一个C表达式,都会得出-884901888:
      (500*400)*(300*200) ((500*400)*300)*200 ((200*500)*300)*400 400*(200*(300*500)
    • 计算机可能没有产生这个预期的结果,但是至少它是一致的!
    • 浮点运算有完全不同的数学属性。虽然溢出会产生特殊的值+8,但是一组正数的乘积总是正的另一方面,由于表示的精度有限,浮点运算是不可结合的。例如,在大多数机器上,C表达式(3.14+le20)-1e20求得的值会是0.0,而3.14+(le20-le20)求得的值会是3.14
  • 通过研究实际数字的表示,我们能够了解可以表示的值的范围和不同算术运算的属性。对于编写在全部数值范围内都能正确工作,而目可以跨越不同机器、操作系统和编译器组合的可移植的程序来说,这种了解是非常重要的。计算机用几种不同的二进制表示来编码数值。在第3章中随着你进入机器级编程,你将需要熟悉这些表示方式。在本章中,我们描述这些编码,并给你一些关于数字表示的推理练习。通过直接操作位级的数字表示,我们得到了几种进行算术运算的方式。理解这些技术对于理解编译算术表达式时产生的机器级代码是很重要的。
  • 我们对这些内容的处理是非常精确的。我们从编码的基本定义开始,然后得出一些属性,例如可表示的数字的范围、它们的位级表示以及算术运算的属性。我们相信从这样一一个抽象的观点来分析这些内容,对你来说是很重要的,因为程序员需要对计算机运算和更为人熟悉的整数和实数运算之间的关系有牢固的理解。尽管这看起来很吓人,但精确的处理只需要了解基本的代数知识。我们建议你将练习题作为巩固公式和一些实际生活例子之间联系的一种方法。
  • 旁注:怎样阅读本章

    如果你觉得等式和公式令人生畏,不要让它妨碍你学习本章的内容!为了完整性,我们提供全部的数学概念的推导,但是阅读这些内容的最好方法是在你首次阅读时跳过这些推导。相反,试着完成一些简单的示例(比如,练习题)来建立你的直觉,然后看看数学推导是如何巩固你的直觉的

  • C++编程语言建立在C之上,使用完全相同的数字表示和运算。在本章中关于C的所有内容对 C++都有效。另一方面,Java语言创造了一套新的数字表示和运算标准。C标准被设计为允许多种实现方式,而Java标准在数据的格式和编码上是详细而精确的。在本章中好几个地方我们都突出了 Java支持的表示和运算。

2-1 信息存储

  • 大多数计算机使用8位的块,或叫做字节(byte),来作为最小的可寻址的存储器单位,而不是访问存储器中单独的位。机器级程序将存储器视为一个非常大的字节数组,称为虚拟存储器(virtual memory)存储器的每个字节都由一个惟一的数字来标识,称为它的地址(address),所有可能地址的集合就称为虚拟地址空间(virtual address space)。正如它的名字表明的,这个虚拟地址空间只是一个展现给机器级程序的概念性映像(image)。实际的实现(见第10章)使用的是随机访问存储器 RAM、磁盘存储、特殊硬件和操作系统软件的结合,来为程序提供一个看上去统一的字节数组
  • 编译器和运行时系统的一个任务就是将这个存储器空间划分为更可管理的单元,来存放不同的程序对象(program object),也就是,程序数据、指令和控制信息。有各种机制可以用来分配和管理程序不同部分的存储。这种管理完全是在虚拟地址空间里完成的。例如,C中一个指钟的值(无论它指向一个整数、一个结构或是某个其他程序单元)都是某个存储块的第一个字节的虚拟地址。 C编译器还把每个指针和类型信息联系起来,这样它就可以根据指针值的类型,生成不同的机器级代码来访问存储在指针所指向位置处的值。尽管C编译器维护着这个类型信息,但是它生成的实际机器级程序并没有关于数据类型的信息。它简单地把每个程序对象视为一个字节块,而将程序本身看做一个字节序列。

    给C语言初学者:C中指针的角色
    指针是C的一个重要特性。它提供了引用数据结构的元素(包括数组)的机制。就像一个变量指针也有两个方面:它的值和它的类型。它的值表示的是某个对象的位置,而它的类型表示那个位置上所存储对象的类型(比如,整数或者浮点数)

(一)十六进制表示法

  • 一个字节包括位。在二进制表示法中,它的值域是000000011111111。如果看成**十进制整数**,它的值域就是0255。两种符号表示法对于描述位模式来说都不是非常方便二进制表示法太冗长,而使用十进制表示法,与位模式的互相转化很麻烦。替代的方法是,我们以16为基数或者叫做十六进制(hexadecimal)数,来书写位模式。十六进制(简写为“Hex”)使用数字“0”~ “9”,以及字符“A”~ “F”来表示16个可能的值。图21展示了16个十六进制数字对应的十进制值和二进制值。用十六进制书写,一个字节的取值范围为00~ FF。
  • 在C中,以0x或0X开头的数字常量被认为是十六进制的值。字符“A”~“F”既可以是大写,也可以是小写。例如,我们可以将十六进制FA1D37B,写作0xFAID37B,或者0xfald37h,甚至是大小写混合,比如,0xFa1D37b。在本书中,我们将使用C表示法来表示十六进制值。
    在这里插入图片描述
  • 编写机器级程序的一个常见任务就是手工地在位模式的十进制、二进制和十六进制表示之间转换。二进制和十六进制之间的转换是简单直接的,因为可以一次执行一个十六进制数字的转换。
  • 比如,假设给你一个数字Ox173A4C。可以通过展开每个十六进制数字,将它转换为二进制格式,如下所示:
    在这里插入图片描述
  • 反过来,如果给定个二进制数字1111001010110110110011,你可以通过首先把它分割为每四位一组,来把它转换为十六进制。不过要注意,如果位总数不是四的倍数,最左边的一组可以少于四位,前面用零补足。然后将每个四位组转换为相应的十六进制数字
    在这里插入图片描述
转化二进制十进制十六进制
二进制\2幂次展开式合并相加,python函数int(str_num, 2)将数从右四个一分,不足的左边补零,四个一组一一对应16进制的数,或者通过十进制, python函数hex(int(str_num, 2))
十进制将商连续除以2,余数从下往上合并就是二进制数,python函数bin(num)\将商连续除以16,余数从下往上合并就是十六进制数,python函数hex(num)
十六进制将每一位对应的二进制数合并,或者通过十进制, python函数bin(int(str_num, 16))16幂次展开式合并相加,python函数int(str_num, 16)\

练习:不将数字转换为十进制或者二进制,试着解答下面的算术题,答案要用十六进制表示。提示只要修改你执行十进制加法和减法所使用的方法,以 16为基数。
A.0x502c+0x8= ox5034
B.0x502c-0x30= ox4FFC
C.0x502c+64= ox506C
D.0x50da-0x502c= oxAE
注意进位是以16为基准的,而不是10

(二)字

  • 每台计算机都有一个字长(word size),指明整数和指针数据的标称大小(nominal size)。因为虚拟地址是以这样的字来编码的,所以字长决定的最重要的系统参数就是虚拟地址空间的最大大小。也就是说,对于一个字长为n位的机器而言,虚拟地址的范围为0~2”-1,程序最多访问2字节。
  • 今天大多数计算机的字长都是32位。这就限制了虚拟地址空间为4千兆字节(写作4GB=2**32B),也就是说,刚刚超过4x10字节。虽然对大多数应用而言,这个空间足够大了,但是现在已经有许多大型的科学和数据库应用需要更大的存储了。因此,随着存储器价格的降低,字长为64位的高端机器正逐渐变得普遍起来。

(三)数据大小

  • 不同数据类型分配的字节数随着机器和编译器的不同而不同
  • 计算机和编译器使用不同的方式来编码数字,比如不同长度的整数和浮点数,从而支持多种数字格式。比如,许多机器都有处理单个字节的指令,也有处理表示为两字节、四字节或者八字节整数的指令,还有些指令支持表示为四字节和八字节的浮点数。
  • C语言支持整数和浮点数的多种数据格式。C的数据类型char表示一个单独的字节。尽管“char”这个名字是由于它被用来存储文本串中的单个字符这一事实而来的,但它也能被用来存储整数值。 C的数据类型int之前还能加上限定词long和short,提供各种大小的整数表示。我们展示了两个有代表性的例子:典型的32位机器和CompaqAlpha体系结构针对高端应用的64位机器。
  • 图2.2也说明了指针(例如,一个被声明为类型为“char*”的变量)使用机器的全字长。大多数机器还支持两种不同的浮点格式:单精度(在C中声明为float)和双精度(在C中声明为double)。这些格式分别使用四字节和八字节。
    在这里插入图片描述
  • 程序员应该力图使他们的程序在不同的机器和编译器上可移植。可移植性的一个方面就是使程序对不同数据类型的确切大小不敏感。C标准对不同数据类型的数字范围设置了下界,这点在后面还将讲到,但是却没有上界。因为32位机器在过去20年里一直是标准,许多程序的编写都是以图2.2中“典型的32位机器”列出的分配原则为假设的。在不久的将来,随着64位机器越来越重要,在将这些程序移植到新机器上时,许多隐藏的对字长的依赖就会显现出来,成为错误。比如,许多程序员假设一个声明为int类型的程序对象能被用来存储一个指针。这在大多数32位的机器上工作正常,但是在一台Alpha机器上却会导致问题。

(四)寻址和字节顺序

  • 对于跨越多字节的程序对象,我们必须建立两个规则:这个对象的地址是什么和我们在存储器中如何对这些字节排序。在几乎所有的机器上,多字节对象都被存储为连续的字节序列,对象的地址为所使用字节序列中最小的地址。例如,假设一个类型为int的变量x的地址为0x100,也就是说,地址表达式&x的值为0x100。那么,x的四字节将被存储在存储器的0x1000x101、0x102和0x103位置。
  • 对表示一个对象的字节序列排序,有两个通用的规则。考虑一个w位的整数,有位表示[xw Xw-2,…x,xo],其中xw1是最高有效位,而x0是最低有效位。假设w是8的倍数,这些位就能被分组成为字节,其中最高有效字节包含位[xw-1xw2…xw8],而最低有效字节包含位[x7,X6,… xo],其他字节包含中间的位。一部分机器选择在存储器中按照从最低有效字节到最高有效字节的顺序存储对象,被称为小端法(littleendian),而另一些机器则按照从最高有效字节到最低有效字节的顺序存储,被称为大端法(bigendian)。许多微处理器芯片,包括Alpba和 Motorola的PowerPC,能够运行在任一种模式中,其取决于芯片加电启动时确定的字节顺序规则。
  • 继续我们前面的示例,假设变量x类型为int,位于地址0x100处,有一个十六进制值为0x01234567。地址范围0x100~0x103的字节顺序依赖于机器的类型:在字0x01234567中,高位字节的十六进制值为0x01,而低位字节值为0x67。
    在这里插入图片描述
  • 对于大多数应用程序员来说,他们机器的字节顺序是完全不可见的。无论为哪种类型的机器所编译的程序都会得到同样的结果。不过有时候,字节顺序会成为问题。首先是在不同类型的机器之间通过网络传送二进制数据时。一个常见的问题是当小端法机器产生的数据被发送到大端法机器或者反之时,接收程序会发现,字里的字节成了反序的。为了避免这类问题,网络应用程序的代码编写必须遵守已建立的关于字节顺序的规则以确保发送方机器将它的内部表示转换成网络标准,而接收方机器则将网络标准转换为它的内部表示。我们将在第12章中看到这种转换的例子。
  • 字节顺序变得重要的第二种情况是当阅读表示整数数据的字节序列时。这通常发生在检查机器级程序时。作为一个示例,从某个文件中摘出了下面这行代码,该文件给出了一个针对Intel处理器的机器级代码的文本表示:
    80483bd:01 05 64 94 04 08                 add &eax,0x8049464 
    
  • 这一行是由反汇编器(disassembler)生成的,反汇编器是一种确定可执行程序文件所表示的指令序列的工具。我们将在下一章中学习有关这些工具的更多知识,以及怎样解释像这样的行。而现在,我们只是注意这行表述了十六进制字节串01 05 64 94 04 08是一条指令的字节级表示,这条指令是增加一个字宽的数据到存储在主存地址0x8049464的值上。如果我们取出这个序列的最后四字节:64 94 04 08,并且按照相反的顺序写出,我们得到08 04 94 64。去掉开头的零,我们就得到信0x8049464,就是石边写看的数佰。当阅速像此例中一样的小端法机器生成的机器级程序表示时,经常会将字节按照相反的顺序显示。书写字节序列的自然方式是最低位字节在左边,而最高位字节在右边,但是这和书写数字时最高有效位在左边,最低有效位在右边的通常方式是相反的。
  • 字节顺序变得可见的第三种情况是当编写规避正常的类型系统的程序时。在C语言中,可以通过使用强制类型转换(cast)来允许以一种不同于它被创造时的数据类型来引用一个对象。大多数应用编程都强烈不推荐这种编码技巧,但是它们对系统级编程来说是非常有用,其至是必须的。
  • 图2.3展示了一段C代码,它使用强制类型转换来访问和打印不同程序对象的字节表示。我们用typedef将数据类型byte_pointer定义为一个指向类型为“unsigned char”的对象的指针。这样一个字节指针引用一个字节序列,其中每个字节都被认为是一个非负整数。第一个例程showbytes的输入是一个字节序列的地址(它用一个字节指针来指示)和一个字节数。show_bytes打印出以十六进制表示的字节。C格式化指令“%2x”表示整数必须用至少两个数字的十六进制格式输出。
  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

洋芋本人

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值