计算机系统漫游
计算机系统由硬件和软件组成,它们共同工作来运行应用程序,虽然系统的具体实现方式随着时间不断变化,但是系统内在的概念却没有改变。
实战技巧
- 避免由计算机标表示数字的方式引起的奇怪的数字错误
- 优化C代码,以充分利用现代处理器和存储器系统的设计
- 了解编译器是如何实现过程调用以及如何利用这些知识来避免缓冲区溢出错误
- 识别和避免链接时的错误
- 编写Unix Shell,动态存储分配包,并发等
信息就是位+上下文
源程序:由值0和1组成的位(又称为比特)序列,8个位被组织成一组,称为字节(每个字节表示程序中的某些文本字符)。
大部分现代计算机系统都是用ASCII标准来表示文本字符,这种方式实际上就是用一个唯一的单字节大小的整数值来表示每个字符。
- 文本文件:由ASCII字符构成的文件
- 二进制文件:其他的非文本文件
每个文本行都是以一个看不见的换行符“\n”来结束的,它所对应的整数值为10。
==系统中所有的信息——包括磁盘文件、内存中的程序、内存中存放的用户数据以及网络中传送的数据,都是由一串比特表示的。==
==区分不同数据对象的唯一方法是读取到这些数据对象时的上下文==
C语言的起源
- 1969年贝尔实验室创建
- 1989年美国颁布ANSI C的标准
-
国际标准化组织继续定义C语言的标准和一系列函数库(即C标准库)
- C语言和Unix操作系统关系密切:C从一开始就是作为一种用于Unix系统的程序语言而开发出来的,大部分Unix内核以及所有支撑工具的函数库都是用C语言开发的
- C语言小而简单
- C语言是为实践目的而设计的:C语言是设计用来实现Unix操作系统的
C语言是系统级编程的首选,也非常适合应用级编程。但是,C语言的指针容易造成错误,而且缺少对抽象的显式支持(例如,类、对象和异常等,像C++和Java这样的应用级编程语言解决了这样的问题)。
程序被其他程序翻译成不同的格式
为了在系统上运行C程序:
1. 每条C语言都要被转化为一系列低级机器语言指令
2. 这些指令被按照一种可执行目标程序的格式打包好
3. 以二进制磁盘文件的形式存放起来
在Unix系统上,从源文件到可执行文件的转化过程由编译器驱动程序完成,这个翻译过程可分为四个阶段,执行这四个阶段的程序(预处理器、编译器、汇编器、链接器)。
预处理阶段:预处理器读取头文件或者import的文件内容并直接插入到程序文本中(.i文件)。
编译阶段:编译器将文本文件(.i文件)翻译器包含汇编语言程序的文本文件(.s文件),汇编语言程序中的每一条语句描述了一条低级机器语言指令。
汇编语言为不同高级语言的不同编译器提供了通用的输出语言。
汇编阶段:汇编器将(.s文件)翻译成机器语言指令,将结果保存在(.o文件)中,(.o文件)是一个二进制文件。
链接阶段:链接器将源程序调用的其他标准库函数链接到一起,形成一个可执行目标文件,该文件可以被加载到内存中,由系统执行。
标准库函数保存在(.o文件)
了解编译系统工作原理的好处
优化程序性能:了解机器代码以及编译器将高级程序语言转化为机器代码的方式,有助于我们对循环控制结构的选择,函数的调用,指针和数组的使用。
理解链接时出现的错误:一些令人困惑的程序错误往往是与链接器操作有关的,理解静态变量和动态变量的区别,在不同的源文件中定义相同名字的全局变量会发生什么,在命令行上排列库的顺序有什么影响等。
避免安全漏洞:缓冲区溢出错误是造成大多数网络和Internet服务器上安全漏洞的主要原因。存在这些错误的原因是需要限制从不受信任的源接收数据的数量和格式。
处理器读并解释存储在内存中的指令
shell是一个命令行解释器,它输出一个提示符,等待输入一个命令行,然后执行这个命令。如果该命令行的第一个单词不是一个内置的shell命令,那么shell就会假设这是一个可执行文件的名字,它将加载并运行这个文件。
系统的硬件组成
总线:贯穿整个系统的是一组电子管道,称作总线,它携带信息字节并负责在各个部件间传递。通常总线被设计成传送定长的字节块,也就是字。字中的字节数(即字长)是一个基本的系统参数,现在大多数机器字长要么是4个字节(32为)要么是8个字节(64位)
I/O设备:系统与外部世界的联系通道。每个I/O设备都通过一个控制器或适配器与I/O总线相连。
控制器与适配器的区别主要在于它们的封装形式。它们的功能是在I/O设备和I/O总线之间传递信息。
1. 控制器是I/O设备本身或者系统的主板上的芯片组
2. 适配器是插在主板插槽上的卡
-
主存:一个临时存储设备,在处理器执行程序时,用来存储程序和程序所要处理的数据。
- 从物理上来说,主存是一组动态随机存储器(DRAM)芯片组成
- 从逻辑上来说,主存是一个线性的字节数组,每个字节都有其唯一的地址(数组索引),地址从零开始。
处理器:*处理单元(CPU)是解释(或执行)存储在住主存中指令的引擎,处理器的核心是一个大小为一个字的存储设备(或寄存器),称为程序计数器(PC),在任何时刻,PC都指向主存中的某条机器语言指令(即含有该条指令的地址)
执行一条指令包含执行一系列的步骤,处理器从程序计数器指向的内存处读取指令,解释指令中的位,执行该指令指示的简单操作,然后更新PC,使其指向下一条指令,而这条指令并不一定和在内存中刚刚执行的指令相邻。
CPU在指令的要求下可能会执行这些操作:
- 加载:从主存复制一个字节或者一个字到寄存器,以覆盖寄存器原来的内容
- 存储:从寄存器复制一个字节或者一个字到主存的某个位置,以覆盖这个位置上原来的内容
- 操作:把两个寄存器的内容复制到ALU,ALU对两个字做算术运算,并将结果存放到一个寄存器中,以覆盖该寄存器中原来的内容
- 跳转:从指令本身中抽取一个字,并将这个字复制到程序计数器中,以覆盖PC中原来的值
运行程序
- shell程序执行它的指令,等待输入一个命令
- 在键盘上输入字符串hello(hello为可执行文件)
- 在键盘上敲击回车键时,shell知道命令输入结束,shell程序将字符逐一读入寄存器,再把它存放到内存中
-
shell执行一系列指令来加载可执行文件hello,这些指令将hello可执行文件中的代码和数据从磁盘复制到主存中。
利用直接存储器(DMA)数据可以不经过处理器直接从磁盘到达主存。
可执行文件hello中的代码和数据在主存中加载完毕,处理器开始执行hello程序的main程序中的机器语言指令
6.这些指令将“hello world\n”字符串中的字节从主存复制到寄存器,在从寄存器复制到显示设备,最终显示在屏幕上。
高速缓存至关重要
系统花费了大量的时间把信息从一个地方挪动到另一个地方。
例如,可执行程序最初存放在磁盘上,当程序加载时,被复制到主存中,当处理器运行程序时,指令又从主存复制到处理器。
**系统设计者的一个主要目标就是使信息的挪动操作尽快地完成
机械原理
较大的存储设备要比较小的存储设备运行的慢,快速设备的造价要远高于同类的低速设备。
- 一个典型的寄存器文件只存储几百字节的信息
- 主存可以存放几亿字节的信息
- 处理器从寄存器中读取数据比从主存中读取几乎要快了100倍
- 加快处理器的运行速度比加快主存的运行速度要容易和便宜得多
针对处理器与主存之间的差距,系统设计者采用了更小更快的存储设备,称为高速缓存存储器,作为暂时的集结区域,存放处理器近期可能会需要的信息。
高速缓存(L1,L2,L3)
名称 | 大小 | 位置 | CPU访问它的速度 | 硬件 |
---|---|---|---|---|
L1高速缓存 | 数万字节 | 处理器芯片上 | 和寄存器文件一样快 | 静态随机访问存储器(SRAM) |
L2高速缓存 | 数十万到数百万字节 | 通过总线连接到处理器 | 访问时间是L1的5倍,比CPU访问主存快5~10倍 | 静态随机访问存储器(SRAM) |
利用高速缓存的局部性原理,即程序具有访问局部区域里的数据和代码的趋势,通过让高速缓存里存放可能经常要访问的数据,大部分的内存操作都能在高速缓存中完成。
意识到高速缓存存储器存在的程序员能够利用高速缓存将程序的性能跳一个数量级
存储设备形成层次结构
存储器层次结构的主要思想是:上一层的存储器作为第一层存储器的高速缓存,在具有分布式文件系统的网络系统中,本地磁盘就是存储在其他系统中磁盘上数据的高速缓存。
操作系统管理硬件
操作系统看成是应用程序和硬件之间插入的一层软件,所有应用程序对硬件的操作尝试都要通过操作系统。
操作系统有两个基本功能:
- 防止硬件被失控的应用程序滥用
- 向应用程序提供简单一致的机制来控制复杂而又通常大不相同的低级硬件设备
操作系统通过几个抽象的概念(进程、虚拟内存、文件)来实现这两个功能。
- 进程:对处理器、主存、I/O设备的抽象表示
- 虚拟内存:对主存和磁盘I/O设备的抽象表示
- 文件:对I/O设备的抽象表示
进程
程序在现代系统上运行时,操作系统会提供一种假象,好像系统上只有这个程序在运行。程序看上去是独占地使用处理器、主存和I/O设备,处理器看上去像在不间断地一条一条地执行程序中的指令,即该程序的代码和数据是系统内存中唯一的对象。这些假象是通过进程的概念实现的。
进程是操作系统对一个正在运行的程序的一种抽象。在一个系统上可以同时运行多个进程,而每个进程都好像在独占地使用硬件。
并发运行时说一个进程的指令和另一个进程的指令是交错执行的。
传统系统在一个时刻只能处理一个进程,多核处理器同时能够执行多个进程,无论是单核还是多核,一个CPU看上去都是在并发地执行多个进程,这是通过处理器在进程间切换来实现的(操作系统实现这种交错执行的机制成为上下文切换)。
- 上下文:操作系统保持跟踪进程运行所需的所有状态信息,这种状态称为上下文(包括许多信息,比如PC和寄存器文件的当前值,以及主存的内容)。
当操作系统决定把控制权从当前进程转移到某个新的进程,就会进行上下文切换,即报错当前进程的上下文,恢复新进场的上下文,然后将控制权传递到新进程。
从一个进程到另一个进程的转换是由操作系统内核(kernel)管理的,内核是操作系统代码常驻主存的部分。【内核不是一个独立的进程,它是系统管理全部进程所用代码和数据结构的集合】
线程
一个进程可以由多个称为线程的执行单元组成,每个线程都运行在进程的上下文中,并共享同样的代码和全局数据。由于网络服务器对并行处理的需求,线程称为越来越重要的编程模型,因为多线程之间比多进程之间更容易共享数据,也因为线程一般比进程更高效。
虚拟内存
虚拟内存是一个抽象概念,它为每个进程提供了一个假象,即每个进程都在独占的使用主存。每个进程看到的内存都是一致的,称为虚拟地址空间。
Linux进程的虚拟地址空间:(图中的地址是从下往上增大的)
地址空间最上面的区域是保留给操作系统的代码和数据的,这对所有进程来说都是一样的
地址空间的底部区域存放用户进程定义的代码和数据
每个进程看到的虚拟地址空间由大量准确定义的区构成,每个区有专门的功能。按照上图,从下往上:
程序代码和数据:对所有的进程来说,代码是从同一个固定地址开始,紧接着的是和C全局变量相对应的数据位置,代码和数据区是直接按照可执行目标文件的内容初始化的。
堆:代码和数据区后紧跟着的是运行时堆。代码和数据区在进程一开始运行时就被指定了大小,与此不同,当调用像malloc和free这样的C标准库函数时,堆可以在运行时动态地扩展和收缩。
共享库:大约在地址空间的中间区域是一块用来存放像C标准库和数学库这样的共享库的代码和数据的区域。
栈:位于用户虚拟地址空间顶部的是用户栈,编译器用它来实现函数调用,和堆一样,用户栈在程序执行期间可以动态地扩展和收缩(每次调用一个函数时,栈会增加,从一个函数返回时,栈就会收缩)。
内核虚拟内存: 地址空间顶部的区域是内核保留的。不允许应用程序读写这个区域的内容或者直接调用内核代码定义的函数,相反,他们必须调用内核来执行这些操作。
虚拟内存的运作需要硬件和操作系统软件之间精密复杂的交互,包括对处理器生成的每个地址的硬件翻译。其基本思想是把一个进程虚拟内存的内容存储在磁盘上,然后用主存作为磁盘的高速缓存。
文件
文件就是字节序列。每个I/O设备,都可以看成一个文件。系统中所有输入和输出都是通过使用一小组称为UnixI/O的系统函数调用读写文件来实现的。
文件这个简单而精致的概念向应用程序提供了一个统一的视图,来看待系统中可能含有的所有各式各样的I/O设备。
系统之间利用网络通信
从一个单独的系统来看,网络视为一个I/O设备,当系统从主存复制一串字节到网络适配器时,数据流经过网络到达另一台机器;系统也可以读取从其他机器发送来的数据,并把数据复制到自己的主存。
例子:通过telnet在远程主机上运行hello程序
- 用户在键盘上输入hello
- 客户端想telnet服务器发送字符串“hello”
- 服务器向shell发送字符串“hello”,shell运行hello程序并将输出发送给telnet服务器
- telnet服务器向客户端发送字符串“hello world\n”
- 客户端在显示器上打印“hello world\n”字符串
重要主题
系统是硬件和软件相互交织的集合体,它们必须共同协作以达到运行应用程序的最终目的。
Amdahl定律
当我们对系统的某个部件加速时,其对系统整体性能的影响取决于该部分的重要性和加速程度。
S=1/((1-a)+a/k)
- a:系统某部分所需执行时间与该时间的比例
- k:该部分性能提升的比例
并行与并发
数字计算机的整个历史中,有两个需求是驱动进步的持续动力:
1. 我们想要计算机做的更多
2. 我们想要计算机运行的更快
- 并发:一个同时具有多个活动的系统
- 并行:用并发来使一个系统运行的更快(并行可以在系统的多个抽象层次上运行)
线程级并发
使用线程能够在一个进程中执行多个控制流。
传统意义上,并发执行只是模拟出来的,是通过使一台计算机在它正在执行的进程间快速切换来实现的,这种并发形式上允许多个用户同时与系统交互。
- 多核处理器:将多个CPU(称为“核”)集成到一个集成电路芯片上。
多处理器的使用可以从两方面提高系统性能:
1. 减少了在执行多个任务时模拟并发的需要
2. 使应用程序运行的更快(这要去程序是以多线程方式来编写的)
- 超线程:允许一个CPU执行多个控制流的技术。(它涉及CPU某些硬件有多个备份,比如程序计数器和寄存器文件,而其他的硬件部分只有一份,比如执行浮点算术运算的单元)
常规处理器需要大约20000个时钟周期做不同线程之间的切换,而超线程的处理器可以在单个周期的基础上决定要执行哪一个线程。
指令级并行
在较低的抽象层次上,现代处理器可以同时执行多条指令的属性称为指令级并行。
早期的处理器需要多个(通常是3-10个)时钟周期来执行一条指令,最近的处理器可以保持诶个时钟周期执行2-4条指令。其实每条指令从开始到结束需要长的多的时间,大约20个后者更多时钟周期,但是处理器使用了非常多的技巧(流水线)来同时处理多达100条指令。【超标量处理器,一个时钟周期执行一条以上的指令】
在流水线中,将自行一条指令所需要的活动划分成不同的步骤,将处理器的硬件组织成一系列的阶段,每个阶段执行一个步骤,这些阶段可以并行地操作,用来处理不同指令的不同部分。
单指令、多数据并行
在最低层次上,许多处理器拥有特殊的硬件,允许一条指令产生多个可以并行执行的操作,这种方式称为单指令、多数据,即SIMD并行
计算机系统提供的一些抽象:
- 计算机级别的抽象:虚拟机
- 正在运行程序(处理器、主存、I/O设备)级别的抽象:进程
- 存储器(主存、磁盘)级别的抽象:虚拟内存
- I/O设备级别的抽象:文件