第一章 计算机系统漫游
以hello程序的生命周期为例来开始对系统的学习
1 |
|
1.1 信息就是位+上下文
hello.c程序是以字节序列的方式存储在文件中的,大部分计算机系统都用ASCII标准

系统中的所有信息都是由一串比特表示的。区分不同数据对象的唯一方法是我们读到这些数据对象时的上下文。
1.2 程序被其他程序翻译成不同的格式
GCC读取hello.c文件并将其翻译为可执行目标文件hello共分为4个阶段,执行这4个阶段的程序(预处理器、编译器、汇编器和链接器)一起构成了编译系统

预处理阶段 :预处理器(cpp)根据以字符
#开头的命令,修改原始的C程序,得到另一个.i文件。编译阶段 :编译器(ccl)将文本文件
hello.i翻译成文本文件hello.s,它包含一个汇编语言程序。汇编阶段 :汇编器(as)将
hello.s文件翻译成机器语言指令,把这些指令打包成可重定位目标程序的格式,并将结果保存在hello.o中链接阶段 :
hello程序调用了printf函数,它是每个C编译器都提供的标准C库中的一个函数。printf函数存在于一个名为printf.o的单独的预编译好了的目标文件中,而这个文件必须以某种方式合并到我们的hello.o程序中。链接器(ld)就负责处理这种合并。结果就得到hello文件,它是一个可执行目标文件(可执行文件),可以被加载到内存中,由系统执行。
1.3 了解编译系统如何工作是大有益处的
优化程序性能
理解链接时出现的错误
避免安全漏洞
1.4 处理器读并解释储存在内存中的指令
1.4.1 系统的硬件组成

总线 :贯串整个系统的一组电子管道。它携带信息字节并负责在各个部件间传递。通常总线被设计成传送定长的字节块,也就是字 ,字中的字节数(字长 )是一个基本的系统参数,大多数为64位。
I/O设备 :每个I/O设备都通过一个控制器 或适配器 与I/O总线相连。控制器和适配器的区别主要在于它们的封装方式,控制器是I/O本身或者系统的主印制电路板(主板 )上的芯片组,适配器是一块插在主板插槽上的卡。
主存 :主存是一个临时存储设备,在处理器执行程序时,用来存放程序和程序处理的数据。从物理上来说,主存是由一组动态随机存取存储器 (dynamic random access memory,DRAM)芯片组成的;从逻辑上来说,存储器是一个线性的字节数组,每个字节都有其唯一的地址(数组索引)。
处理器 :中央处理单元 (CPU),简称处理器,是解释或执行存储在主存中指令的引擎。处理器的核心是一个大小为一个字的存储设备(或寄存器),称为程序计数器 (PC)。在任何时候,PC都指向主存中的某条机器语言指令,即含有该条指令的地址。
从系统通电开始,直到系统断电,指令按照严格的顺序执行,而执行一条指令包含执行一系列的步骤。处理器从程序计数器指向的内存处读取指令,解释指令中的位,执行该指令指示的简单操作,然后更新PC,使其指向下一条指令,而这条指令并不一定和在内存中刚刚执行的指令相邻。
1.4.2 运行hello程序
初始时,在Linux系统中,shell程序执行它的指定,等待输入命令。当在键盘上输入字符串”./hello”后,shell程序将字符逐一读入寄存器,再把它存放到内存中。

在键盘敲回车键,即结束命令输入后,shell执行一系列将hello目标文件中的代码和数据从磁盘复制到主存。利用直接存储器存取(DMA)技术,数据可以不通过处理器而直接从硬盘到达主存,这个步骤如下图所示。
一旦目标文件hello中的代码和数据被加载到主存,处理器就开始执行hello程序的main程序中的机器指令,将“hello, world\n”字符串中的字节从主存复制到寄存器文件,再从寄存器文件中复制到显示设备,最终显示在屏幕上,该步骤如下图所示。
1.5 高速缓存至关重要
针对处理器和主存之间的差异,系统设计者采用更小更快的存储设备,称为高速缓存存储器 (cache memory,简称为cache或高速缓存),作为暂时的集结区域,存放处理器可能会需要的信息。处理能力强的系统可能会有三级高速缓存:L1、L2和L3使系统获得更大的存储器,同时访问速度更快,原因是利用了高速缓存的局部性 原理,即程序具有访问局部区域里的数据和代码的趋势。通过让高速缓存里存放可能经常访问的数据,大部分的内存操作都能在快速的高速缓存中完成。

1.6 存储设备形成层次结构
每个计算机系统中的存储设备都被组织成了一个存储器层次结构 ,在这个结构中,从上至下,设备的访问速度越来越慢,容量越来越大,并且每字节的造价也越来越便宜。
存储器层次结构的主要思想是上一层的存储器作为低一层存储器的高速缓存。
1.7 操作系统管理硬件
在hello例子中,shell和hello程序都没有直接访问键盘、显示器、磁盘或主存,它们依靠操作系统 提供的服务。可以把操作系统看作是应用程序和硬件之间插入的一层软件,所有应用程序对硬件的操作尝试都必须通过操作系统。

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

1.7.1 进程
进程是操作系统对一个正在运行的程序的一种抽象。在一个系统上可以运行多个进程,而每个进程都好像在单独地使用硬件。而并发运行 ,则是说一个进程的指令和另一个进程的指令是交错执行的。一个CPU看上去都像是在并发的执行多个进程,这是通过处理器在进程间切换实现的。操作系统实现这种交错执行的机制称为上下文切换 (上下文是指操作系统保持跟踪进程运行所需的所有状态信息)。下图展示了hello程序运行场景的基本理念。
示例场景中有两个并发的进程:shell进程和hello进程。最开始,只有shell进程在运行,即等待命令行上的输入。当让它运行hello程序时, shell通过调用一个专门的函数,即系统调用,来执行我们的请求,系统调用会将控制权传递给操作系统。操作系统保存shell进程的上下文,创建一个新的hello进程及其上下文,然后将控制权传给新的hello进程。hello进程终止后,操作系统恢复shell进程的上下文,并将控制权传回给它, shell进程会继续等待下一个命令行输入。
从一个进程到另一个进程的转换是由操作系统内核 管理的。内核是操作系统代码常驻主存的部分。当应用程序需要操作系统的某些操作时,比如读写文件,它就执行一条特殊的系统调用 指令,将控制权传递给内核。然后内核执行被请求的操作并返回应用程序。内核不是一个独立的进程,它是系统管理全部进程所用代码和数据结构的集合。
1.7.2 线程
1.一个进程实际上可以由多个称为线程 的执行单元组成,每个线程都运行在进程的上下文中,并共享同样的代码和全局数据。
2.因为多线程之间比多进程之间更容易共享数据,也因为线程一般来说都比进程更高效。
1.7.3 虚拟内存
虚拟内存为每个进程提供了一个假象,即每个进程都在独占地使用主存。每个进程看到的内存都是一致的,称为虚拟地址空间 。下图是Linux进程的虚拟地址空间,地址空间最上面的区域是保留给操作系统中的代码和数据的,这对所有的进程来说都是一样的;底部区域存放用户进程定义的代码和数据。
程序代码和数据 对所有的进程来说,代码是从同一固定地址开始,紧接着的是和C全局变量相对应的数据位置。代码和数据区是直接按照可执行目标文件的内容初始化的,在示例中就是可执行文件
hello。堆 代码和数据区后紧随着的是运行时堆。代码和数据区在进程一开始运行时就被指定了大小,与此不同,当调用像
malloc和free这样的C标准库函数时,堆可以在运行时动态地扩展和收缩。共享库 大约在地址空间的中间部分是一块用来存放像C标准库和数学库这样的共享库的代码和数据的区域。
栈 位于用户虚拟地址空间顶部的是用户栈 ,编译器用它来实现函数调用。和堆一样,用户栈在程序执行期间可以动态地扩展和收缩。特别地,每当调用一个函数时,栈就会增长;从一个函数返回时,栈就会收缩。
内核虚拟内存 地址空间顶部的区域是为内核保留的。不允许应用程序读写这个区域的内容或者直接调用内核代码定义的函数。相反,它们必须调用内核来执行这些操作。
虚拟内存运行的基本思想是把一个进程虚拟内存的内容存储在磁盘上,然后用主存作为磁盘的高速缓存。
1.7.4 文件
文件就是字节序列。每个I/O设备都可以看成是文件。系统中所有输入输出都是通过使用一小组称为Unix I/O的系统函数调用读写文件来实现的。
1.8 系统之间利用网络通信
对于hello程序,可以用telnet应用在一个远程主机上运行hello程序。假设用本地主机上的telnet客户端连接远程主机上的telnet服务器,在登陆到远程主机并运行shell后的步骤如下图所示。

1.9 重要主题
1.9.1 Amdahl定律
Amadahl定律的主要思想是,当对系统的某部分加速时,其对系统整体性能的影响取决于该部分的重要性和加速程度。
设系统执行某应用程序需要时间为
可以看到,虽然对系统一个主要部分做出重大改进,但获得的系统加速比却远小于这部分的加速比,这就是Amdahl定律的主要观点——要想显著加速整个系统,必须提升全系统中相当大的部分的速度。
1.9.2 并发和并行
并发(concurrency)是指一个同时具有多个活动的系统;并行(parallelism)指的是用并发来使一个系统运行得更快。
下面按照系统层次结构由高到低顺序强调并行的三个层次:
1. 线程级并发
如下图,多核处理器是将多个CPU(称为“核”)集成到一个集成电路芯片上。下图中的微处理器芯片有4个CPU核,每个核都有自己的L1和L2高速缓存,其中L1高速缓存分为两个部分,一个保存最近取到的指令,另一个放数据。这些核共享更高层次的高速缓存,以及到主存的接口。
超线程(有时称为同时多线程)是一项允许一个CPU执行多个控制流的技术。它涉及CPU某些硬件有多个备份,比如程序计数器和寄存器;而其他的硬件部分只有一份,比如执行浮点算数运算的单元。常规的处理器需要大约20000个时钟周期做不同线程间的转换,而超线程的处理器可以在单个周期的基础上决定要执行哪一个线程。这使得CPU能够更好地利用它的处理资源。比如,假设一个线程必须等到某些数据被装载到高速缓存中,那CPU就可以继续去执行另一个线程。
多处理器的使用从(1)减少在执行多个任务时模拟并发的需要;(2)使应用程序运行的更快 两个方面提高系统性能。
2. 指令级并行
在较低的抽象层次上,现代处理器可以同时执行多条指令的属性称为指令级并行 。
在流水线 中,将执行一条指令所需要的活动划分成不同的步骤,将处理器的硬件组织成一系列的阶段,每个阶段执行一个步骤。这些阶段可以并行地操作,用来处理不同指令的不同部分。
如果处理器可以达到比一个周期一条指令更快的执行速率,就称之为超标量 处理器。
3. 单指令、多数据并行
在最低层次上,许多现代处理器拥有特殊的硬件,允许一条指令产生多个可以并行执行的操作,这种方式称为单指令、多数据 ,即SIMD并行。
提供这些SIMD指令多是为了提高处理影像、声音和视频数据应用的执行速度。
可靠的方法是用编译器支持的特殊的向量数据类型来写程序,比如GCC就支持向量数据类型。
1.9.3 计算机系统中抽象的重要性
指令集架构 提供了对实际处理器硬件的抽象。使用这个抽象,机器代码程序表现得就好像运行在一个一次只执行一条指令的处理器上;
文件 是对I/O设备的抽象;
虚拟内存 是对程序存储器的抽象;
进程 是对一个正在运行的程序的抽象;
虚拟机 提供对整个计算机的抽象,包括操作系统、处理器和程序
1.10 小结
计算机系统是由硬件和系统软件组成的,它们共同协作以运行应用程序。计算机内部的信息被表示为一组组的位,它们依据上下文有不同的解释方式。程序被其他程序翻译成不同的形式,开始时是ASCII文本,然后被编译器和链接器翻译成二进制可执行文件。
处理器读取并解释存放在主存里的二进制指令。因为计算机花费了大量的时间在内存、I/O设备和CPU寄存器之间复制数据,所以将系统中的存储设备划分成层次结构——CPU寄存器在顶部,接着是多层的硬件高速缓存存储器、DRAM主存和磁盘存储器。在层次模型中,位于更高层的存储设备比低层的存储设备要更快,单位比特造价也更高。层次结构中较高层次的存储设备可以作为较低层次设备的高速缓存。通过理解和运用这种存储层次结构的知识,程序员可以优化C程序的性能。
操作系统内核是应用程序和硬件之间的媒介。它提供三个基本的抽象:
文件是对I/O设备的抽象;
虚拟内存是对主存和磁盘的抽象;
进程是处理器、主存和I/O设备的抽象。
最后,网络提供了计算机系统之间通信的手段。从特殊系统的角度来看,网络就是一种I/O设备。