01计算机系统漫游
本书的主要作用:帮助我们了解当我们在系统上执行hello程序时,系统发生了什么以及为什么会这样
#include <stdio.h>
int main()
{
printf("hello, world\n");
return 0;
}
信息是「位+上下文」
hello
的生命周期从最初的源程序(源文件) 开始,是一个名为hello.c的文本文件
-
源程序实际上就是一个由值0和1组成的位(也称为比特)序列
-
8个位被组织为一组,称为字节 ,每个字节表示程序中的某些文本字符
-
例如,ASCII标准使用一个唯一的单字节大小的整数值表示每个字符
hello.c
以字节序列的方式存储在文件当中,每一个字节都具有一个整数值
-
例如,
#
对应的整数为35,即0010 0011
-
例如,
i
对应的整数为105,即0110 1001
-
例如,
\n
对应的整数为110,每一行结尾都具有一个以表示换行 -
例如,
SP
对应的整数为32,有空格的地方就具有它
计算机系统中的所有信息,都是由一串比特表示的,并通过上下文进行区分
-
一个同样的字节序列,可能表示一个整数、浮点数、字符串或者机器指令
-
数字的机器表示方式与实际的整数和实数不同,它们是对真值的有限近似
程序被其他程序翻译成不同的格式
hello程序从C语言开始,是适合人类阅读的方式,但最终要化为一系列低级机器语言指令,
-
这些指令按照一种称为可执行目标程序的格式进行打包
-
打包完成以二进制磁盘文件的形式进行存储,目标程序也称为可执行目标文件
-
Unix系统上,整个过程的转化由编译器驱动程序完成
上述过程对应的指令为
gcc -o hello hello.c
整个过程中,由GCC完成将源文件hello.c
翻译为可执行文件hello
翻译的过程可以划分为四个阶段,对应执行的程序如下所示,四者构成了编译系统
-
预处理器(cpp)
-
根据以字符#开头的命令,修改原始程序
-
例如根据
#include <stdio.h>
读取系统头文件stdio.h
的内容,并插入程序中 -
最终的结果以
.i
作为文件扩展名
-
-
编译器(ccl)
-
编译器将文本文件
hello.i
翻译为文本文件hello.s
,包含一个汇编语言程序 -
该程序包含函数
main
的定义,其中的每条语句都以一种文本格式描述了一条低级机器语言指令
-
-
汇编器(as)
-
将
hello.s
翻译成机器语言指令,并打包成可重定位目标程序 ,保存在hello.o
-
该阶段保存的文件是二进制文件,用本文编辑器打开是乱码
-
-
链接器(ld)
-
hello程序调用了
printf
函数,是标准C库中的函数,存在一个名为printf.o的单独编译完成的目标文件当中 -
该文件需要合并到
hello.o
程序中,合并完成得到hello
的可执行目标文件 -
可执行目标文件可以被加载到内存当中,由系统执行
-
处理器读并解释存储在内存中的指令
hello.c
源程序被编译系统翻译为可执行目标文件hello
,并存放在磁盘上。
如果是在Unix系统上想要运行这个程序,当需要将文件名输入到shell的应用程序当中
shell是一个命令行解释器,输出一个提示符,等待输入一个命令行,然后执行这个命令,执行完毕之后继续输出一个提示符,等待下一个输入的命令
系统的硬件组成
-
总线
总线是贯穿整个系统的一组电子管道,携带信息字节并在各个部件之间传递
总线被设计为传送定长的字节块,也就是字(word),64位系统为8个字节,32位系统为4个字节
-
I/O设备
系统与外部世界的联系通道
例如键盘、鼠标、显示器、磁盘等,通过控制器或适配器与I/O总线相连接。
控制器与适配器的差别在于封装方式,功能都是在I/O设备和I/O总线之间传递信息
-
主存
临时存储设备,在处理器执行程序的时候,用来存放程序和程序处理的数据
逻辑上即为一个线性的字节数组,每个字节都有其唯一的地址(数组索引),从零开始
-
处理器
解释(执行)存储在主存中指令的引擎
核心是一个大小为一个字的寄存器,称为程序计数器(PC),在任何时候都指向主存中的某条及其语言指令(即含有该指令的地址)
从系统上电开始,处理器一直在执行PC所指向的指令,再更新PC,指向下一条指令
运行hello程序
shell 是什么?shell 是一个命令解释程序,如果命令行的第一个单词不是内置的 shell 命令,shell 就会对这个文件进行加载并运行。此处,shell 加载并且运行 hello 程序,屏幕上显示 “Hello World” 内容,hello 程序运行结束并退出,shell 继续等待下一个命令的输入。
在 shell 中输入./hello
之后,会将其逐一读入寄存器,处理器会将其再放到内存当中。当按下回车之后,shell 程序就知道我们已经完成了命令的输入, 然后执行一系列的指令来加载可执行文件 hello
这些指令将 hello
目标文件中的代码和数据从磁盘复制到主存,数据就是我们要显示输出的”Hello World\n”
,这个复制的过程将利用 DMA(Direct Memory Access) 技术,数据可以不经过处理器,从磁盘直接到达内存。
当可执行文件 hello 中的代码和数据都加载到主存之后,处理器就开始执行 hello 程序的 main 程序中的机器语言指令,这些指令将需要显示的字符串从主存复制到寄存器上,最终显示在屏幕上
高速缓存至关重要
前面的三个步骤可以看到,系统进行了大量「复制」的操作,从一个地方移动到另一个地方,这些在程序设计角度来说都是开销,要让复制操作尽可能快完成
机械原理说明:
-
较大的存储设备要比较小的存储设备运行地慢
-
高速设备的制造成本要比低速设备要高
例如,处理器从磁盘上读取一个字的时间可能比从主存中读取的时间要大1000万倍。同样寄存器和主存之间的差距也非常大,处理器从寄存器中读取文件速度也比主存要快100倍
-
处理器与主存之间的差距在持续增大
-
加快处理器运行速度比加快主存运行速度要容易和便宜
为了处理处理器与主存之间的差异,引入了高速缓存存储器,存放处理器近期可能会需要的信息,作为暂时的集结区域
通过让高速缓存里存放可能经常访问的数据,大部分的内存操作都能在高速缓存当中完成
存储设备形成层次结构
在处理器和一个较大较慢的设备(例如主存)之前插入一个更小更快的存储设备(高速缓存)成为一个普遍的概念
每个计算机系统中的存储设备都被组织成一个存储器层次结构
存储器层次结构的主要思想:上一层的存储器作为低一层存储器的高速缓存
操作系统管理硬件
在使用 shell 程序和 hello 程序时候,程序都不是直接访问键盘、显示器、磁盘或者主存
操作系统是应用程序和硬件之间的一层软件,完成两个基本功能
-
防止硬件被失控的应用程序滥用
-
向应用程序提供简单一致的机制来控制复杂而又通常大不相同的低级硬件设备
操作系统的抽象
-
文件是对I/O设备的抽象
-
虚拟内存是对主存和磁盘I/O设备的抽象
-
进程是对处理器、主存和I/O设备的抽象
进程
进程是操作系统对一个正在运行的程序的一种抽象,在一个系统上可以同时运行多个进程,每个进程都好像是在独立地使用硬件。
并发运行,是指一个进程的指令和另一个进程的指令是交错执行的。
一个CPU并发执行多个进程,实际上是通过在处理在进程间切换来实现的,实现这种交错执行的机制为上下文切换
线程
一个进程可以由多个线程组成,每个线程都运行在进程的上下文中,共享同样的代码和全局数据。多线程比多进程之间更容易共享数据。
虚拟内存
为每个进程提供了一个假象,级每个进程都在独占地使用主存。每个进程看到的内存都是一致的,称为虚拟地址空间。例如 Linux 虚拟地址空间
-
程序代码和数据
-
堆
运行时动态地扩展和收缩(调用malloc和free这样的C标准库)
-
共享库
存放C标准库和数学库等共享库
-
栈
编译器用它来实现函数调用,可以动态扩展和收缩,每调用一个函数,栈就会增长,每返回一个函数,栈就会收缩
-
内核虚拟内存
为内核保留,不允许程序读写或调用内核代码定义的函数,必须调用内核来执行
文件
文件就是字节序列。每个I/O设备、磁盘、键盘、显示器、网络,都可以看成文件
系统的输入输出都是调用称为 Unix I/O 的系统函数调用读写文件来实现的
文件为应用程序提供了一个统一的视图,来看待系统中可能含有的各式各样的I/O设备
系统之间的利用网络通信
从一个单独的系统来看,网络可以视为一个I/O设备
当系统从主存复制一串字节到网络适配器时,数据流经过网络到达另一台机器;同样的,系统可以读取从其他机器发送来的数据,并复制到自己的主存
重要主题
系统不仅仅是硬件,系统是硬件和系统软件互相交织的集合体
Amdahl定律
当对系统的某个部分加速时,其对系统整体性能的影响取决于该部分的重要性和加速程度
α:系统某部分执行时间与整体执行时间的比值
k:该部分性能提升的比例
T new = ( 1 − α ) T old + ( α T old ) / k = T old [ ( 1 − α ) + α / k ] T_{\text {new }}=(1-\alpha) T_{\text {old }}+\left(\alpha T_{\text {old }}\right) / k=T_{\text {old }}[(1-\alpha)+\alpha / k] Tnew =(1−α)Told +(αTold )/k=Told [(1−α)+α/k]
S = T o l d T n e w = 1 ( 1 − α ) + α / k S=\frac{T_{old}}{T_{new}}=\frac{1}{(1-\alpha)+\alpha / k} S=TnewTold=(1−α)+α/k1
结论:想要显著加速整个系统,必须提升全系统中相当大的部分的速度
并发和并行
并发:同时具有多个活动的系统
并行:用并发来使系统运行地更快
-
线程级并发
计算机的并发是模拟出来的,通过在多个进程之间快速切换实现
可以通过单处理器系统 ,也可以通过多处理器系统 (多核或超线程)实现
-
指令集并行
一个处理器可以同时执行多条指令,例如流水线
-
单指令、多数据并行
允许一条指令产生多个可以并行的操作
计算机系统中抽象的重要性
所谓抽象,就是类似将一个函数的内部结构进行包装后形成API对外开放,使得对内部无需了解即可使用