以下是一段简单的 hello.c 程序,这一章通过跟踪该程序的生命周期,简要介绍计算机系统的关键概念、专业术语和组成部分。
|
|
1. 信息就是位+上下文
hello程序本质上是由 值0 和 1 组成的位(bit),8个 bit 为1 字节。
每个字节都有一个整数值,通过 ASCII 标准,它们表示不同的编码,如 105 表示字符‘i’,每行的换行符‘\n’对应的整数值为 10,由这些字符构成的 hello.c 成为 文本文件,其他文件成为二进制文件。
无论在何种介质上传输的数据,本质上都是由一串比特位表示的,相同的字节序列可能表示一个整数、浮点数、字符串乃至机器指令,取决于其上下文。
2. 程序被其他程序翻译为不同的格式
hello.c 之类的高级 c语言程序是被人读懂的,但要在操作系统上运行 hello.c 程序,则需要将其转化为低级机器语言指令。
|
|
GCC 编译器读取 hello.c 将其转化为可执行文件 hello,以下为其执行的4个阶段(预处理器、编译器、汇编器和链接器)。
- 预处理(预处理器 cpp):修改原始 c 程序,读取头文件,扩展宏等操作;
- 编译(编译器 ccl):将 hello.i 翻译为汇编程序 hello.s;
- 汇编(汇编器 as):将汇编程序翻译为机器指令,并将其打包进 relocatable object program (可重定位目标程序),即 hello.o;
- 链接(链接器 ld):将包含 printf 函数的标准 c 库中的 printf.o 的预编译目标文件链接到 hello.o 中,得到 hello
至此,hello 文件即可被加载到内存中,由系统运行。
3. 了解编译系统工作的作用
- 优化程序性能
- 理解链接时出现的错误
- 避免安全漏洞
4. 处理器读并解释储存在内存中的指令
通过以下命令将 hello 程序加载到内存中运行
|
|
shell是一个命令行解释器,其本身也是一个可执行程序。shell 等待一个命令行,然后执行该命令,执行完 hello 后在屏幕输出它的信息后,继续等待下一个输入的命令行。
系统硬件组成
总线
贯穿整个系统,通常被设计为传送定长的字节块,即字(word),字的字节数(字长)在大多数机器为 4个字节(32bit),8个字节(64bit)。
I/O 设备
I/O设备是系统与外部设备链接的通道,如键盘,显示器,磁等,其通过特定的控制器(通常为主板上的芯片组)或适配器(主板插槽上的卡)与 I/O 总线相连。
主存
用来临时存放程序和程序处理的数据,其由一组 DRAM(动态随机存取存储器) 芯片组成。
处理器(CPU)
解释或执行存储在主存中指令的引擎,核心为一个大小为 一个字的 PC(程序计数器),PC 在任一时刻都指向主存中存储某条指令的地址(该地址存储某条机器语言指令)。
以下是一个典型系统的硬件组成:
运行 hello 程序
初始时 shell 程序将键盘键入的字符读入寄存器,再存放到内存,当遇到结束命令(回车)时,shell 将 hello 目标程序从磁盘复制到主存,之后处理器开始执行 hello 目标程序中的 main 程序中的机器语言指令,最终将 "hello world\n" 字符从主存复制到寄存器,再通过寄存器文件复制到显示设备,最终输出到屏幕。
5. 高速缓存至关重要
当程序加载时,从磁盘复制到主存,再从主存复制到寄存器,开销是很大的,因此系统设计者设计了 cache memory(高速缓存存储器,cache) ,缓存处理器近期可能处理的信息,减少频繁复制带来的开销。系统利用了高速缓存的局部性原理,即程序具有访问局部区域里的数据和代码的趋势。
高速缓存存储器是由 SRAM(静态随机访问存储器) 实现的,现代计算机包括多级缓存(L1,L2,L3......)。
6. 存储设备形成层次结构
其主要思想是上一层存储器作为低一层存储器的高速缓存。
7. 操作系统管理硬件
hello 以及 shell 程序并没有直接访问硬件,而是通过操作系统提供的服务间接访问实现效果。
操作系统提供了两个功能:
- 防止硬件被失控的程序滥用
- 向应用程序提供简单一致的机制控制复杂而又大不相同的低价硬件设备(通过抽象进程、虚拟内存、文件等概念实现),如文件是对 I/O 设备的抽象,虚拟内存是对主存和磁盘 I/O 的抽象,进程则是对处理器、主存和 I/O 设备之间关系的抽象。
进程
系统对 正在运行的程序 抽象出进程的概念,使程序看起来是独占处理器、主存及 I/O 设备。大多数的操作系统中 CPU 看上去是并发地执行多个程序(实际上单处理器在任一时刻只能执行一个进程的代码),其是由处理器在进程间切换实现的(即 上下文切换)。
从一个进程到另一个进程的转换是由 操作系统内核(kernel) 管理的,其是系统管理全部进程所用代码和数据结构的集合。内核是操作系统代码常驻内存的部分。
应用程序通过 system call 指令将控制权转给内核,内核执行完请求的操作并返回应用程序。
线程
线程是运行在进程上下文中,共享进程代码和全局数据的执行单元。线程之间比进程更容易共享数据,因此比进程更高效,当有多处理器时,多处理器可以更高效地处理多线程程序的运行。
虚拟内存
进程认为自身独占主存,每个进程看到的内存是一致的,称为虚拟地址空间。下图为 linux 进程的虚拟地址空间,注意其地址是从下往上增大的。
- 程序代码和数据:所有进程的代码都是从同一固定地址开始
- 堆:运行时堆,当调用 malloc 和 free 之类的 C 标准库函数时,堆可以在运行时动态扩展和收缩
- 共享库: C 标准库和数学库等
- 栈:用户栈,编译器用它实现函数调用,和堆一样能进行动态扩展和收缩
- 内核虚拟内存:内核保留空间,应用程序必须调用内核来读写该区域的内容或执行内核代码定义的函数
文件
文件就是字节序列,I/O 设备都可以看作是文件,系统通过一组 UNIX I/O 系统函数调用读写文件。
文件为应用程序提供了统一视图,因此例如同一个程序可以运行在使用不同的磁盘技术的不同系统上。
8. 系统之间利用网络通信
现代系统通过网络和其他系统连接在一起,网络本身也可视为一个 I/O 设备。
下图为通过 telnet 应用在一个远程主机运行 hello 程序的步骤:
9. 重要主题
Amdahl 定律
当对系统某个部分进行加速时,其对系统整体性能的影响取决于该部分的重要性和加速程度,这称为 Amdahl‘s law(Amdahl 定律)。
若系统执行某应用程序需要时间为:\( T_{old} \),假设系统某部分所需执行时间与该时间比例为 \({\alpha}\) ,而该部分性能提升比例为 \(k\)。即该部分初始所需时间为 ${\alpha}T_{old}$ ,现在所需时间为 \(({\alpha}T_{old})/k\),因此,总执行时间为:
\[ T_{new} = (1-{\alpha})T_{old} + ({\alpha}T_{old})/k = T_{old}[(1-{\alpha}) + {\alpha}/k] \]
由此计算出加速比 \(S = T_{old}/T_{new}\),为:
\[ S = \frac{1}{(1+{\alpha}) + {\alpha}/k} \]
Amdahl定律的主要观点是:要想显著加速整个系统,必须提升全系统中相当大的部分的速度。当 k 无限趋向于 \(\infty\) 时,即该部分花费时间忽略不计,于是得到:
\( S_{\infty} = \frac{1}{(1-{\alpha})} \) 根据上式,将 60% 的系统加速到不花时间的程度,获得的净加速比仍然只有 2.5X(倍)。
并发和并行
并发(concurrency)指一个同时具有多个活动的系统。
并行(parallelism)指用并发来使一个系统运行得更快。
两个概念在系统中的多个抽象层次运用:
- 线程级并发
多核处理器和超线程(hyperthreading)的出现,减少了执行多个任务时模拟并发的需要,再者使 CPU 对于线程的运行效率变高,用多线程编写的应用程序运行得更快,从而提高系统性能。
- 指令级并行
如果处理器可以达到比一个周期一条指令更快的执行效率,则称为超标量(super-scalar)处理器。
- 单指令、多数据并行
许多处理器出现允许一条指令产生多个可以并行执行的操作,称为单指令,多数据,即 SIMD 并行。
计算机系统中抽象的重要性
抽象是计算机科学最重要的概念,如应用程序接口(API),Java 类的声明,C 语言的函数原型等。
在处理器里,指令集架构提供了对实际处理器硬件的抽象,只要指令执行模型一致,不同的处理器实现也能执行同样的机器代码。
以下是操作系统抽象的四个概念:
其中虚拟机是对整个计算机的抽象。