深入理解计算机系统笔记

来源:互联网 发布:淘宝怎么刷关键词2017 编辑:程序博客网 时间:2024/04/30 10:22

 

 本书适合那些想要写出更快、更可靠程序的程序员阅读。通过掌握程序是如何映射到系统上,以及程序是如何执行的,读者能够更好的理解程序的行为为什么是这样的,以及效率低下是如何造成。粗略来看,计算机系统包括处理器和存储器硬件、编译器、操作系统及网络互联环境。而通过程序员的视角,读者可以深深地体会到学习计算机系统的内部工作原理会对他们今后作为计算机科学研究者和工程师的工作有进一步的帮助。它还有助于为进一步学习计算机体系结构、操作系统、编译器和网络打下基础。

本书的主要论题包括:数据显示、C程序的机器级表示、处理器结构、程序优化、存储器层次结构、链接、异常控制流、虚拟存储器和存储器管理、系统级I/O、网络编程和并发编程。

目录
1,计算机系统漫游
2,信息的表示和处理    -- 第一部分 程序结构和执行
3,程序的机器级表示
4,处理器体系结构
5,优化程序性能
6,存储器层次结构
7,链接                -- 第二部分 在系统上运行程序
8,异常控制流
9,测量程序执行时间
10,虚拟存储器
11,系统级I/O          -- 第三部分 程序间的交互和通信
12,网络编程
13,并发编程

我的感觉:这是一本回答“你知道程序是怎么在计算机上运行的吗?”这个问题的书。

 

1,计算机系统漫游

让我们以一个hello程序为例:
Java代码 复制代码
  1. #include <stdio.h>   
  2.   
  3. int main()   
  4. {   
  5.   printf("hello, world/n");   
  6. }  


1.1 信息就是位+上下文
hello程序的生命周期是从源程序hello.c开始的
该源程序实际上就是一个由0和1组成的位(比特)序列,每8个一组,称为字节
大部分现在系统使用ASCII标准来表示文本字符
hello.c程序是以字节序列的方式存储在文件中的,每个字节都有一个整数值来对应于某个字符
如#是35,i是105,换行符/n是10
hello.c这种由ASCII字符构成的文件称为文本文件,其他则称为二进制文件

hello.c的表示方法说明了一个基本的思想:系统中所有的信息--包括磁盘文件、存储器中的程序、存储器中存储的用户数据以及网络上传输的数据,都是由一串比特表示的
区分不同数据对象的唯一方法是我们读到这些数据对象时的上下文,比如在不同的上下文中,同样的字节序列可能表示一个整数、浮点数、字符串或者机器指令

1.2 程序被其他程序翻译成不同的格式
为了在系统上运行hello.c程序,每条C语句都必须被其他程序转化为一系列的低级机器语言指令
这些指令按照一种称为可执行目标程序的格式打好包,并以二进制磁盘文件的形式存放起来,目标程序也称为可执行目标文件
Java代码 复制代码
  1. unix> gcc -o hello hello.c  

这个过程分四个阶段,执行这四个阶段的程序(预处理器、编译器、汇编器和链接器)构成编译系统

1,预处理阶段根据#开头的命令修改原始的C程序,#include <stdio.h>告诉预处理器读取系统头文件stdio.h的内容并插入到程序文本中,从而得到另一个C程序,通常以.i作为文件扩展名
2,编译阶段将文本文件hello.i翻译成文本文件hello.s,它为一个汇编语言程序。汇编语言为不同高级语言的不同编译器提供了通用的输出语言,如C编译器和Fortran编译器产生的输出文件用的是一样的汇编语言
3,汇编阶段将hello.s翻译成机器语言指令,把这些指令打包成一种叫可重定位目标程序的格式,结果保存在目标文件hello.o中,hello.o是二进制文件,它的字节编码是机器语言指令而不是字符
4,链接阶段由于hello程序调用了printf这个标准C库中的一个函数,而printf函数存在于一个名为printf.o的单独预编译目标文件中,链接器就负责将它并入到我们的hello.o程序中,结果就得到hello文件,它是一个可执行目标文件,可执行文件加载到存储器后由系统负责执行

1.3 了解编译系统如何工作室大有益处的
一些重要原因促使程序员必须知道编译系统是如何工作的:
1,优化程序性能
switch和if-then-else、while和do、指针引用和数组索引
2,理解链接时出现的错误
链接器报告说无法解析一个引用、静态变量和全局变量、静态库和动态库
3,避免安全漏洞
缓冲区溢出错误

1.4 处理器读并解释储存在存储器中的指令
Java代码 复制代码
  1. unix> ./hello   
  2. hello, world   
  3. unix>  

shell等待命令输入并执行,如果命令的第一个单词不是一个内置的shell命令,那么shell就会假设这是一个可执行文件的名字,要加载和执行该文件
这里shell将加载和执行hello程序,然后等待程序终止,hello程序在屏幕上输出信息,然后终止

系统的硬件组成:
总线 -- 贯穿整个系统的是一组电子管道,称做总线,它携带信息字节并负责在各个部件间传递,通常总线被设计成传送定长的字节块,也就是字,字中字节数(字长)是基本的系统参数
I/O设备 -- 系统与外界的联系通道,例如用户输入的键盘和鼠标、用户输出的显示器、长期存储数据和程序的磁盘,每个I/O设备都是通过一个控制器或适配器与I/O总线连接起来,它们在I/O总线和I/O设备之间传递信息
主存 -- 主存是一个临时存储设备,在处理器执行程序时,它被用来存放程序和程序处理的数据,物理上来说就是DRAM芯片,逻辑上来说存储器是由一个线性的字节数组组成的,每个字节都有自己唯一的地址(数组索引),这些地址从零开始
处理器 -- 中央处理单元(CPU)简称处理器,是解释(或执行)存储在主存中指令的引擎,处理器的核心是一个被称为程序计数器(PC)的字长大小的存储设备(寄存器),任何时间点上,PC都指向主存中的某条机器语言指令(内含其地址)。从系统通电开始直到系统断电,处理器一直重复执行相同的基本任务:从程序计数器指向的存储器处读取指令,解释指令中的位,执行指令指示的简单操作,然后更新程序计数器指向下一条指令,而这条指令并不一定在存储器中和刚刚执行的指令相邻
操作在主存、寄存器文件和算术逻辑单元(ALU)之间循环
CPU在指令要求下可能会执行这些操作:
加载:从主存拷贝一个字节或者一个字到寄存器,覆盖寄存器原来的内容
存储:从寄存器拷贝一个字节或者一个字到主存的某个位置,覆盖这个位置上原来的内容
更新:拷贝两个寄存器的内容到ALU,ALU将两个字相加,并将结果存放到一个寄存器中,覆盖该寄存器中原来的内容
I/O读:从一个I/O设备中拷贝一个字节或者一个字到一个寄存器
I/O写:从一个寄存器中拷贝一个字节或者一个字到一个I/O设备
转移:从指令本身中抽取一个字,并将这个字拷贝到程序计数器中,覆盖PC中原来的值

程序执行过程:
shell等待我们输入字符串"./hello"后,shell逐一读取字符到寄存器,然后放到存储器中
当我们敲回车键时,shell知道我们已经结束了命令的输入,然后shell执行一系列指令
这些指令将hello目标文件的代码和数据从磁盘拷贝到主存,从而加载hello文件,数据包括最终被输出的字符串"hello, world/n"
利用DMA(直接存储器存取)技术,数据可以不通过处理器而直接从磁盘到达主存
一旦hello目标文件中的代码和数据被加载到存储器,处理器就开始执行hello程序的主程序中的机器语言指令
这些指令将"hello, world/n"串中的字节从存储器中拷贝到寄存器文件,再从寄存器文件拷贝到显示设备,最终显示在屏幕上

1.5 高速缓存
上面的例子可以看出,系统花费了大量的时间把信息从一个地方挪到另一个地方
hello程序最初位于磁盘上,程序加载时拷贝到主存,处理器运行程序时,指令拷贝到处理器
类似的,数据串"hello, world/n"开始在磁盘上,再被拷贝到主存,然后拷贝到显示设备
磁盘驱动器可能比主存大100倍,对处理器而言,从磁盘读取一个字的时间开销要比从主存读取的开销大1000万倍,处理器从寄存器文件中读数据比主存中读取则要快几乎100倍
针对这种处理器与主存之间的差异,系统设计者采用了更小更快的存储设备,称为高速缓存存储器(cache memories,简称高速缓存)
位于处理器芯片上的L1高速缓存的容量可达数万字节,访问速度几乎和寄存器文件一样快
容量为数十万到数百万的更大的L2高速缓存是通过一条特殊的总线连接到处理器的
访问L2的时间开销比L1大5倍,但仍然比访问主存快5~10倍
L1和L2高速缓存是用一种叫静态随机访问存储器(SRAM)的硬件技术实现的

1.6 形成层次结构的存储设备
每个计算机的存储设备组织成一个存储器层次模型,从上至下设备变得更慢、更大,并且每字节的造价也更便宜
寄存器文件位于层次模型的最顶部,第0级记为L0,L1高速缓存为第一层,L2高速缓存为第二层,主存为L3,本地磁盘等本地二级存储为L4,分布式文件系统、Web服务器等远程二级存储为L5
层次结构的主要思想是一个层次上的存储器作为下一层次上存储器的高速缓存,寄存器文件是L1的高速缓存,L1是L2的高速缓存,L2是主存的高速缓存,主存是磁盘的高速缓存,本地磁盘是分布式文件系统的高速缓存

1.7 操作系统管理硬件
当shell加载和运行hello程序时,当hello程序输出自己的消息时程序并没有直接访问键盘、显示器、磁盘或者主存储器,而是依靠操作系统提供的服务
操作系统可以看作是应用程序和硬件之间插入的一层软件,所有应用程序对硬件的操作尝试都必须通过操作系统
操作系统的两个基本功能:访问硬件被失控的应用程序滥用;在控制复杂而又通常广泛不同的低级硬件设备访问为应用程序提供简单一致的方法
操作系统通过几个基本的抽象概念(进程、虚拟存储器和文件)来实现这两个功能
文件是对I/O设备的抽象表示,虚拟存储器是对主存和磁盘I/O设备的抽象表示,进程则是对处理器、主存和I/O设备的抽象表示

进程是操作系统对运行程序的一种抽象,在一个系统上可以同时运行多个进程,而每个进程都好像在独占地使用硬件,我们称之为并发运行,实际上是说一个进程的指令和另一个进程的指令是交错执行的
操作系统实现这种交错执行的机制称为上下文切换(context switching)
操作系统保存进程运行所需的所有状态信息,这种状态就是上下文(context),包括许多信息,比如PC和寄存器文件的当前值,以及主存的内容
在任何一个时刻,系统都只有一个进程正在运行
当操作系统决定从当前进程转移控制权到某个新进程时,它就会进行上下文切换,即保存当前进程的上下文、恢复新进程的上下文,然后将控制权转移到新进程,新进程就会从它上次停止的地方开始
进程这个抽象概念还暗示着由于不同的进程交错执行,打乱了时间的概念,使得程序员很难获得运行时间的准确和可重复测量

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

虚拟存储器是一个抽象概念,它为每个进程提供了一个假象,好像每个进程都在独占地使用主存
每个进程看到的存储器都是一致的,称之为虚拟地址空间,地址从小到大为:
1,程序代码和数据
2,运行时堆(运行时动态地扩展和收缩)
3,共享库(C标准库和数学库等)
4,栈(编译器用它来实现函数调用,调用一个函数时栈就会增长,从函数返回时栈就会收缩)
5,内核虚拟存储器(操作系统驻留在存储器中的部分,地址空间顶部四分之一的部分为内核预留,应用程序不允许读写这个区域的内容或者直接调用内核代码定义的函数)

文件就是字节序列,每个I/O设备,包括磁盘、键盘、显示器甚至网络都可以看成文件
系统中所有输入输出都是通过使用称为Unix I/O的以小组系统函数调用读写文件来实现的
文件这个简单而精致的概念非常强大,因为它使得应用程序能够统一地看待系统中可能含有的所有各式各样的I/O设备,如处理磁盘文件内容的程序员可以非常幸福地无需了解具体的磁盘技术

1.8 利用网络系统和其他系统通信
从一个单独的系统来看,网络可以视为又一个I/O设备
当系统从主存拷贝一串字符到网络适配器时,数据流经过网络到达另一台机器,而不是到达本地磁盘驱动器
相似地,系统可以读取从其他机器发送来的数据,并把数据拷贝到自己的主存
例如telnet连接远程服务器并运行hello程序的例子,命令和结果在网络I/O中传输

小结
计算机系统由硬件和系统软件组成,它们共同协作以运行应用程序
计算机内部的信息被表示为一组组的位,它们依据不同的上下文又有不同的解释方式
程序被其他程序翻译成不同的形式,开始时是ASCII文本,然后被编译器和链接器翻译成二进制可执行文件

处理器读取并解释存放在主存里的二进制指令
计算机花费了大量的时间在存储器、I/O设备和CPU寄存器之间拷贝数据,所以系统中的存储设备就被按层次排列,CPU寄存器在顶部,接着是多层的硬件高速缓存存储器、DRAM主存储器和磁盘存储器
在层次模型中位于更高层的存储设备比低层的存储设备要快,单位比特造价也更高
程序员通过理解和运用这种存储层次结构的知识,可以优化他们C程序的性能

操作系统内核是应用程序和硬件之间的媒介,它提供三个基本的抽象概念:文件是对I/O设备的抽象概念;虚拟存储器是对主存和磁盘的抽象概念;进程是对处理器、主存和I/O设备的抽象概念

网络提供了计算机系统之间通信的手段,从某个系统的角度来看,网络就是一种I/O设备

 

 

2. 信息的表示和处理

计算机将信息编码为位(比特),通常组织成字节序列。有不同的编码方式用来表示整数、实数和字符串。不同的计算机模型在编码数字和多字节数据中的字节顺序上使用不同的约定。

C语言被设计成包容多种不同字长和数字编码的实现。虽然高端机器逐渐开始使用64位字长,但是目前大多数机器仍使用32位字长。大多数机器对整数使用二进制补码编码,而对浮点数使用IEEE编码。在位级上理解这些编码,并且理解算术运算的数学特性,对于编写能在全部数值范围上正确计算的程序来说,是很重要的。

C语言的标准规定在无符号和有符合整数之间进行强制类型转换时,基本的位模式不应该改变。在二进制补码机器上,对于一个w位的值,这种行为是由函数T2Uw和U2Tw来描述的。C语言隐式的强制转换会得到许多程序员无法预计的结果,常常导致程序错误。

由于编码的长度有限,计算机运算与传统整数和实数运算相比,具有非常不同的属性。当超出表示范围时,有限长度能够引起数值溢出。当浮点数非常接近于0.0,从而转换成零时,浮点数也会外溢。

和大多数其他程序语言一样,C语言实现的有限整数运算和真实的整数运算相比有一些特殊的属性。例如,由于溢出,表达式x*x能够得出负数。但是无符号数和二进制补码的运算都满足环的属性。这就允许编译器做很多的优化。例如,用(x<<3)-x取代表达式7*x时,我们就利用了结合性、交换性和分配性,还利用了移位和乘以2的幂之间的关系。

我们已经看到了几种使用位级运算和算术运算组合的聪明方法。例如,我们看到,使用二进制补码运算,~x+1是等价于-x的。另外一个例子,假设我们想要一个形如[0,...,0,1,...,1]的位模式,由w-k个0后面紧跟着k个1组成。这些位模式对于掩码运算是很有用的。这种模式能够通过C表达式(1<<k)-1生成,利用的是这样一个属性,即我们想要的位模式的数值为2k-1。例如,表达式(1<<8)-1将产生位模式0xFF。

浮点表示通过将数字编码为x*2y的形式来近似地表示实数。最常见的浮点表示方式是由IEEE标准754定义的。它提供了几种不同的精度,最常见的单精度(32位)和双精度(64位)。IEEE浮点也能够表示特殊值无穷大8和NaN。

必须非常小心地使用浮点运算,因为浮点运算的范围和精度有限,而且浮点运算并不遵守普遍的算术属性,比如结合性。
 
原创粉丝点击