简介
这一章以一个简单的C语言程序hello.c
的完整生命周期来介绍计算机系统的关键概念、专业术语和组成部分。
#include <stdio.h>
int main()
{
printf("hello, world\n");
return 0;
}
1.信息就是位+上下文(计算机如何存储信息?)
系统中的所有信息——包括磁盘文件、内存中的程序、内存中存放的用户数据以及网络上传送的数据,都是由一串比特(bit)表示的。区分不同数据对象的唯一方法是我们读到这些这些数据对象时的上下文(context)。
#include <stdio.h>
#include <stdlib.h>
#include <limits.h>
#include <float.h>
int main()
{
int a = 100;
printf("a在计算机中的存储为: %#x\n", a);
printf("以 整数型作为上下文(context)时(int a): %d\n", a);
printf("以浮点数型作为上下文(context)时(float a): %f\n", a*1.0);
printf("在计算机中存储最大值为: %#x\n", INT_MAX);
printf("以 整数型作为上下文(context)时(int): %d\n", INT_MAX);
printf("以浮点数型作为上下文(context)时(float a): %e\n", DBL_MAX);
return 0;
}
输出为:
a在计算机中的存储为: 0x64
以 整数型作为上下文(context)时(int a): 100
以浮点数型作为上下文(context)时(float a): 100.000000
在计算机中存储最大值为: 0x7fffffff
以 整数型作为上下文(context)时(int): 2147483647
以浮点数型作为上下文(context)时(float a): 1.797693e+308
在计算机中a
以0x64=>0000 0000 0000 0000 0000 0000 0000 0064
的形式存储,但是对a以不同的数据类型(上下文context)进行读取将获得不同的结果。
2.程序被其他程序翻译成不同的格式(代码如何成为一个程序)
代码是由文本文件构成的C语言程序,但是计算机不能直接理解文本,因此需要通过编译程序将C语言程序编译为一个可执行目标文件(二进制文件)。
编译的过程包括预处理、编译、汇编、链接四个阶段,结合给出的第一个例子可以很容易的理解整个编译过程。
#include <stdio.h>
int main()
{
printf("hello, world\n");
return 0;
}
- 首先是预处理,预处理器(cpp)的功能就是根据C语言程序中
#
开头的命令修改C语言源程序。比如在hello.c
文件中#include <stdio.h>
命令是告诉预处理器读取系统的文件stdio.h
并将其内容插入到第一行的位置,得到以.i
结尾的文本文件。gcc -E hello.c -o hello.i // `gcc编译器使用`-E`命令可以预处理C语言程序获得`.i`结尾的文本文件。
- 经过预处理后的文件依然是文本文件,还需要通过编译器(ccl)翻译获得汇编语言程序。汇编语言更加接近底层的硬件操作,直接包含对CPU、内存等硬件的操作命令。
// gcc编译器使用-S命令可以翻译.c或.i文件,获得.s文件 gcc -S hello.i -o hello.s
// 输出结果中main函数的汇编程序部分: main: .LFB0: .cfi_startproc endbr64 pushq %rbp .cfi_def_cfa_offset 16 .cfi_offset 6, -16 movq %rsp, %rbp .cfi_def_cfa_register 6 leaq .LC0(%rip), %rdi call puts@PLT movl $0, %eax popq %rbp .cfi_def_cfa 7, 8 ret .cfi_endproc
- 汇编语言程序依然是文本文件,汇编器(as)将汇编文件翻译成机器语言指令,并将这些文件打包成可重定位目标程序,以
.o
结尾的二进制文件。// gcc编译器使用-c命令可以将.c、.i或.s文件编译为.o文件 gcc -c hello.s -o hello.o
- 到这里程序依然无法直接执行因为此时的
hello.o
文件虽然将代码翻译成了二进制文件,
但是在hello.c
中使用了库函数printf
,这个函数并未在hello.c
文件中定义。
这就需要链接器(ld)将库函数中的printf.o
文件合并到hello.o
文件中,最终得到可执行目标文件hello
。// gcc编译器使用-o命令可以实现链接功能 gcc -o hello hello.c
3.了解编译系统如何工作是大有益处的(为什么要了解汇编)
- 优化程序性能
- 理解链接时出现的错误
- 避免安全漏洞
初学编程实际上对于书中对这方面的描述是难以看懂的,随着对计算机组成原理、软件工程、操作系统等课程的学习以及编程实践经验的增加,这部分读起来非常有味道。
4.处理器读并解释存储在内存中的指令(从硬件的角度看程序)
4.1 系统的硬件组成(计算机组成原理有详细介绍)
总线
总线指的是贯穿整个系统的一组电子管道,在传递信息时以字节块,也就是字word的形式定长传输数据。
比如32位总线就是字长8个字节,64位总线就是字长16个字节。
I/O设备
I/O设备是计算机系统与外部联系的通道。图中的键盘和鼠标、显示器、磁盘驱动器(磁盘)等。
注意:I/O设备并不是完全由CPU控制的,I/O设备的硬件由控制器或适配器与I/O总线相连。当CPU调度I/O设备时,首先将指令通过总线传到控制器或适配器,控制器或适配器将
数据从I/O设备中整理好后经过I/O总线传输到CPU或内存中。
主存
主存是临时存储处理器执行程序时的程序和程序处理的数据的设备。
程序首先从磁盘中加载到内存中,处理器再按照内存中程序的命令执行程序
处理器
中央处理单元(CPU),简称处理器,是解释(或执行)存储在主存中的指令的引擎。简单来说CPU根据程序计数器(PC)确定下一条执行指令的内存地址,从电脑开机开始
CPU不断根据PC去取指令、解释指令、执行指令,不同的CPU架构由不同的指令集架构。原书在这里主要区分指令集架构和微体系架构,前者指CPU采用的指令集中指令
的效果,后者指CPU在硬件上如何实现。
4.2 运行hello程序(从硬件的角度分析hello程序)
原书中用三张图从整体上介绍了hello
程序是如何运行。
5. 高速缓存至关重要(cache的意义)& 6.存储设备形成层次结构
根据上一节的三幅图可以看出,在程序运行的过程中,程序及其数据从磁盘=>内存=>寄存器,数据的传输占据了大量的时间,真正CPU的工作时间相比数据传输很小。
而根据机械原理,快速设备的造价远高于低速设备,因此开发人员开发出了多层存储的结构,在CPU中设置L1、L2、L3缓存,CPU访问缓存的速度大致比内存快100倍,而CPU访问内存的速度大致比访问机械硬盘的速度快上万倍。
缓存技术能够提高程序效率的根本原因是局部性原理,即程序具有访问局部区域里的数据和代码的趋势。可以简单理解为,缓存中的数据相比内存中其他数据更可能在短时间内再次被访问,而内存中的数据相比硬盘中的其他数据短时间内被访问的概率更大,更高效的设备将更多次被访问,从而提高程序效率。
7.操作系统管理硬件(操作系统简介)
操作系统是调度计算机硬件资源的程序,可以将操作系统看成是应用程序与硬件之间的一层中间软件,如图1-10所示。
操作系统具有两个基本的功能:
(1)防止硬件被失控的应用程序滥用;
(2)向应用程序提供简单一致的机制来控制复杂而又通常大小不同的低级硬件设备。
操作系统通过几个基本的抽象概念(进程、虚拟内存和文件)实现上述功能。
7.1 进程
进程是操作系统对一个正在运行的程序的一种抽象。(程序是一个存放在硬盘中的二进制文件,当一个程序被加载到内存中运行后就成为了进程,进程也可以理解为一个程序运行的过程)
并发指的是一段时间内两个程序同时在运行时,但是实际上两个程序交替在时间段内执行。
并行指的是两个程序同时(每分每秒)都在不同的CPU上运行。
计算机为什么可以并发运行多个程序?原因是操作系统对系统资源进行调度,多个程序在同一个CPU上交错执行,不停切换。操作系统实现这种交错执行的机制称为上下文切换。
进程运行过程中所需要的所有状态信息就是上下文,比如程序计数器(PC)、CPU中寄存器的值、主存中的内容。当操作系统决定把硬件资源从当前的进程切换到某个新进程时,就会进行上下文切换,保存当前进程上下文=>恢复新进程上下文=>执行新进程。
观察图1-12可以发现,进程切换是有操作系统内核(kernel)管理的。内核是操作系统代码常驻的部分,当应用程序需要操作系统进行某些操作时,应用程序需要执行系统调用指令,将控制权传递给系统内核,然后操作系统执行请求并返回应用程序。内核不是一个独立进程,而是系统管理全部进程所用的代码和数据结构的集合。
7.2 线程
进程可以由多个线程构成,同一个进程的不同线程共享进程除CPU外的所有资源,例如代码和全局数据。线程之间的数据共享相比进程更容易实现,进程之间进行通信有多种方式,但效率相比线程更低。
7.3 虚拟内存
虚拟内存是一个抽象的概念,它为每一个进程提供了一个假象,即每个进程都在独占地使用主存。每个进程看到的内存都是一样的,成为虚拟地址空间。
Linux的虚拟地址空间如图1-13所示,包括:
- 程序代码和数据
- 堆
- 共享库
- 栈
- 内核虚拟内存
虚拟内存的基本思想是把一个进程虚拟内存的内容存储在磁盘上,然后用主存作为磁盘的高速缓存。
7.4 文件
文件就是字节序列,但这个简单而精致的概念非常强大,它向应用程序提供了一个统一的模式,来看待系统中可能含有的所有不同的I/O设备。
8.系统之间利用网络通信
从单独的一个系统来看,可以将网络视为一个I/O设备。
当系统从主存复制一串字节到网络适配器时,数据流经网络到达另一台机器,而不是直接到达另一台机器的磁盘中。
图1-15展示了一个在远程服务器上运行hello
程序的例子。
9. 重要主题(计算机中的重要概念)
计算机系统不仅仅是硬件,而是硬件和软件相互交织的集合体。
9.1 Amdahl定律
Amdahl定律是指,当我们对系统的某个部分加速时,其对系统整体性能的影响取决于该部分的重要性与加速程度。
9.2 并发和并行
并发concurrency是一个通用的概念,指一个同时具有多个活动的系统
并行化parallelism指的是用并发来使一个系统运行得更快。
并行化可以在计算机系统的多个抽象层次上运用,由高到低顺序强调三个层次:
- 线程级并发
基于进程的概念,可以设计出同时有多个程序执行的系统。在进程中引入线程,可以在同一个进程执行多个控制流。
同一个处理器在多个任务间切换,实际计算由一个处理器完成的配置成为单处理器系统。
单个操作系统内核控制多个处理器组成的系统为多处理器系统。多核处理器是将多个CPU集成到一个集成电路芯片上。
超线程也叫同时多线程simultaneousmulti-threading,是一项允许一个CPU执行多个控制流的技术。其硬件基础是CPU设计有多个硬件备份,例如多个程序计数器和寄存器文件,而其他硬件只有一份,在线程切换时大幅缩减时间。
- 指令级并行
一条指令的执行其实可以划分为多个步骤,例如取指令、分析指令、执行指令。指令流水线pipelining可以将一条指令划分为不同步骤,将处理器的硬件组织成一系列的阶段,这些阶段可以并行地操作,用来处理指令的不同部分。
- 单指令、多数据并行(SIMD)
SIMD指的是处理器允许一条指令产生多个可以并行执行的操作,例如对8对单精度浮点数同时进行加法操作。
9.3 计算机系统中抽象的重要性
抽象的概念贯穿计算机科学。
在处理器中,指令集架构提供了对处理器硬件工作的抽象,基于这个抽象,机器代码的运行表现为每次都在一个一次只执行一条指令的处理器上,实际上底层的硬件在同时执行多条指令。
在学习操作系统的过程中,文件是对I/O设备的抽象,虚拟内存是对程序存储器的抽象,而进程是对一个正在运行的程序的抽象。虚拟机是对整个计算机的抽象,包括操作系统、处理器和程序。
10. 总结
第一章从一个C语言程序出发,由顶层向下介绍了代码如何编译成为可执行文件,然后从这个可执行文件如何在计算机硬件上执行介绍了计算机的硬件组成(CPU、层次存储结构等)。根据程序执行的进一步细化,引入操作系统的介绍,并穿插了计算机网络的概念。这一章覆盖了计算机组成原理、操作系统和编译原理的关键概念。
11. 参考文献
布赖恩特 深入理解计算机系统 = Computer Systems a Programmer's Perspective. 机械工业出版社, 2016. Print. 计算机科学丛书