第一章 计算机系统漫游

第一章 计算机系统漫游

计算机系统是由硬件和系统软件组成的,它们共同工作来运行应用程序。

请注意:本文中所述的计算机系统不是计算机操作系统,而是计算机硬件和软件所共同组成的一个计算机整体的体系架构。

下面我们看一个C语言代码:

#include<stdio.h>

int main() {
	printf("hello, world\n");
	return 0;
}

上述代码是计算机专业相关学生在学习C语言时,所入门的第一个代码,代码是如此的简单,但是计算机想要运行它,为我们呈现最终结果,在背后默默付出了很多、很多……

下面让我们来一起看看,当我们在点击**编译运行**的那一刻,计算机所发生了什么?

我们通过跟踪hello程序的生命周期来开始对系统的学习——从它被程序员创建开始到在系统上运行输出简单的消息,然后终止。我们将沿着这个程序的生命周期,简要地介绍一些逐步出现的关键概念、专业术语和组成部分。后面的章节将围绕这些内容展开。

1.1 信息就是位+上下文

实际上,hello程序的生命周期是从一个源程序(或者说源文件)开始的,我们上面所展示的包含上述代码的一个CPP文件(CPP文件是C语言所编写的代码文件,后缀名一般为.c)就是源文件。源程序实际上就是一个由值0和1组成的位(又称为比特)序列8个位被组织成一组,称为字节。每个字节表示程序中的某些文本字符。我们暂时上面所展示的包含上述代码的一个CPP文件命名为hello.c,在后面介绍中,我们也均以该命名代指。

下面是hello.c文件中每一个字符所对应的ASCII码表

ASCII码:全称美国信息交换标准代码,在计算机中,每一个字符都有其对应的ASCII码值,这个值将被计算机以二进制的形式读取或者使用。

image

hello.c这样只由ASCII字符构成的文件称为文本文件,所有其他文件都称为二进制文件。

1.2 程序被其他程序翻译成不同的格式

程序为了能够在系统上运行必须转化为一系列的低级机器语言指令,然后这些指令按照一种称为可执行目标程序的格式打包,并以二进制磁盘文件的形式存放起来。目标程序也称为可执行目标文件

下面,我们看一下hello.c是如何被翻译成可执行文件的hello的(一般可执行文件后缀为.exe)。

在这里,GCC编译器驱动程序读取源程序文件hello.c,并把它翻译成一个可执行目标文件hello。这个翻译过程可分为四个阶段完成,如下图所示。执行这四个阶段的程序(预处理器、编译器、汇编器和链接器)一起构成了编译系统(compilation system)。

image

  • 预处理阶段预处理器 (cpp) 根据以字符#开头的命令,修改原始的C程序。换句话说,我们可以理解为,预处理器就是处理C程序的#开头的头文件。将头文件所对应的程序内容直接添加到该C程序中。结果就得到了另一个程序,通常是以 .i 作为文件扩展名。

  • 编译阶段编译器(ccl)将预处理阶段得到的文本文件hello.i翻译成文本文件 hello.s,它包含一个汇编语言程序。该程序包含main函数的定义,如下:

    image

  • 汇编阶段汇编器(as)将编译阶段得到的文本文件.s翻译成机器语言指令,将这些指令打包成可重定位目标程序的格式,并保存在目标文件hello.o中。

  • 链接阶段:请注意,hello.o程序调用了printf函数printf函数是C编译器所提供的一个标准库函数,存在于一个名为printf.o的单独的预编译好了的目标文件中,而这个文件必须以某种方式合并到我们的hello.o程序中。**链接器(ld)**就负责处理这种合并。结果就得到hello文件,它是一个可执行目标文件或者简称为可执行文件),可以被加载到内存中,由系统执行。

1.3 了解编译系统如何工作是大有益处的

其实,就我个人而言,无论是前端、后端开发,还是系统架构师,我认为都有必要学习本书,这对于我们以后的深造,就业等有许多潜形的帮助。哪怕只是单纯了解,也应该系统性的阅读本书。

现在抛出我的问题:我们为什么需要了解编译系统?

有很多人认为,我只是一个搞开发的,搞软件设计的,我会用某些语言就行了,我有必要了解底层实现吗?

答案是肯定的,只要从事和计算机相关的行业,了解底层是必要的。

了解编译系统有以下三个好处

  1. 优化程序性能
  2. 理解链接时出现的错误。
  3. 避免安全漏洞。

1.4 处理器读并解释储存在内存中的指令

1.4.1 系统的硬件组成

image

  • CPU:中央处理单元,也称处理器,包含 PC ( 程序计数器)、寄存器堆、ALU(算数/逻辑计算单元)三个部分。
    • 程序计数器 PC:是一个 4 字节或是 8 字节的存储空间,里面存放的是某一条指令的地址。从系统上电的那一瞬间,直到系统断电,处理器就不断地在执行PC 指向的指令,然后更新 PC,使其指向下一条要执行的指令(注意:这个下一条指令与刚刚执行的指令不一定是相邻的)。
    • 寄存器:可以理解为一个临时存放数据的空间。例如计算两个变量 a+b 的和,处理器从内存中读取 a 的值暂存在寄存器 X 中,读取 B 的值暂存在寄存器 Y中,这个操作会覆盖寄存器中原来的数值,处理器完成加载的操作后,ALU会从复制寄存器 X 和 Y 中保存的数值,然后进行算术运算,得到的结果会保存到寄存器 X 或者寄存器 Y 中,此时寄存器中原来的数值会被新的数值覆盖。
    • 算数/逻辑计算单元 ALU:计算速度极快,且专攻算数与逻辑的计算,计算机核心部分。
  • 内存:主存,也称为内存、运行内存,处理器在执行程序时,内存主要存放程序指令以及数据。从物理上讲,内存是由随机动态存储器芯片组成;从逻辑上讲,内存可以看成一个从零开始的大数组,每个字节都有相应地址。
  • 总线:内存和处理器之间通过总线来进行数据传递。实际上,总线贯穿了整个计算机系统,它负责将信息从一个部件传递到另外一个部件。通常总线被设计成传送固定长度的字节块,也就是字,至于这个字到底是多少个字节,各个系统中是不一样的,32 位的机器,一个字长是 4 个字节;而 64 位的机器,一个字长是 8 个字节。
  • 输入输出设备:除了处理器,内存以及总线,计算机系统还包含了各种输入输出设备,例如键盘、鼠标、显示器以及磁盘等等。每一个输入输出设备都通过一个控制器或者适配器与I\O总线相连.

1.4.2 运行hello程序

  1. 通过键盘输入"./hello" 的字符串,shell 程序会将输入的字符逐一读入寄存器,处理器会把 hello 这个字符串放入内存中。

image

  1. 然后执行一系列的指令来来加载可执行文件 hello ,这些指令将 hello 中的数据和代码从磁盘复制到内存。

image

  1. CPU会将"hello, world\n"这个字符串从内存复制到寄存器文件。然后再从寄存器文件复制到显示设备,最终hello,world显示在屏幕上。

    image

1.5 高速缓存至关重要

上面所展示的运行hello程序的示例,揭示了一个重要的问题

即:系统累死累活,花费大量时间和精力只是把一个信息从一个地方挪到了另一个地方。

对于身为程序员的我们来说:这样的行为明显是毫无价值和意义的。为此,系统设计者的一个主要目标就是使这些复制操作尽可能快地完成。

而一个典型的寄存器文件只存储几百字节的信息,而主存里可存放几十亿字节。然而,处理器从寄存器文件中读数据比从主存中读取几乎要快100倍。

解决方案

针对这种处理器与主存之间的差异,系统设计者采用了更小更快的存储设备,称为高速缓存存储器(cache或高速缓存),作为暂时的集结区域存放处理器近期可能会需要的信息。

image

笼统的说,Cache存在的价值和意义就类似于一个能随时打开的高频手册,CPU所需的信息可以优先打开这个手册查看,手册中没有再去主存上找。

1.6 存储设备形成层次结构

今天,在处理器和一个较大较慢的设备(例如主存)之间插入一个更小更快的存储设备(例如高速缓存)的想法已经成为一个普遍的观念。

实际上,每个计算机系统中的存储设备都被组织成了一个存储器层次结构。在这个层次结构中,从上至下,设备的问速度越来越慢、容量越来越大,并且每字节的造价也越来越便宜。

下面的存储器层次结构则是一个很典型的结构:

  • 寄存器文件在层次构中位于最顶部,也就是第0级或记为L0。
  • 这里我们展示的是三层高速缓存L1到L3占据存储器层次结构的第1层到第3层。
  • 主存在第4层,以此类推。

image

上述结构中一共是七级缓存,现代计算机中可能更多,但是基本原理上仍然一致。

这个层次结构的主要思想就是:上一层存储设备是下一层存储设备的高速缓存。例如,寄存器文件就是 L1 的高速缓存,L1 是 L2 的高速缓存,内存是磁盘的高速缓存等等。

1.7 操作系统管理硬件

让我们回到 hello程序的例子。当 shell 加载和运行 hello程序时,以及 hello 程序输出自己的消息时,shell和hello程序都没有直接访问键盘、显示器、磁盘或者主存。

取而代之的是,它们依靠操作系统提供的服务。

下图展示了一个计算机的抽象结构:

image

通过上述结构,我们可以发现,其实操作系统无非就是硬件和应用程序连接的一个平台。

操作系统两个基本功能

所有应用程序对硬件的操作尝试都必须通过操作系统操作系统有两个基本功能

  1. 防止硬件被失控的应用程序滥用。
  2. 向应用程序提供简单一致的机制来控制复杂而又通常大不相同的低级硬件设备。

操作系统通过几个基本的抽象概念(进程、虚拟内存和文件)来实现这两个功能。

下图则是操作系统提供的抽象表示:

image

1.7.1 进程

  • 进程是操作系统对一个正在运行的程序的一种抽象。在一个系统上可以同时运行多个进程,而每个进程就好像独占地使用硬件。
  • 并发运行是指一个进程的指令与另一个进程的指令是交错执行。
  • 无论是在单核还是多核系统,一个CPU看上去都像是在并发的执行多个进程,这是通过处理器在进程间来回切换的。操作系统的这种机制叫做上下文切换。
  • 进程的上下文切换。

下图是进程的上下文切换的抽象表示图:

image

1.7.2 线程

一个进程实际上由多个线程的执行单元组成,每个线程都运行在进程的上下文中,并共享同样的代码和全局的数据,线程一般比进程更高效。

我们一般希望能够同时进行多种线程,而不希望单线程占据全部操作系统。

举个例子:我们一边下载软件,一边视频聊天,再同时刷着抖音,这就是三个线程。

1.7.3 虚拟内存

虚拟内存其实是一个抽象的概念,它为每个进程提供了一种抽象,即每个进程独占的使用主存。每个进程看到的内存地址是一致的,称为虚拟地址空间。

如下图所示的是Linux进程虚拟地址空间。

在Linux中,地址空间最上面的区域是保留给操作系统中的代码和数据的,这对所有进程来说都是一样。)地址空间的底部区域存放用户进程定义的代码和数据。请注意,图中的地址是从下往上增大的。

image

  • 第一个区域是用来存放程序的代码和数据的,这个区域的内容是从可执行目标文件中加载而来的,例如我们多次提到的hello 程序。对所有的进程来讲,代码都是从固定的地址开始。至于这个读写数据区域放的是什么数据呢?例如在C语言中,全局变量就是存放在这个区域
  • 顺着地址增大的方向,继续往上看就是堆(heap),学过C 语言的同学应该用过malloc 函数,程序中malloc 所申请的内存空间就在这个堆中。程序的代码和数据区在程序一开始的时候就被指定了大小,但是堆可以在运行时动态的扩展和收缩
  • 接下来,就是共享库的存放区域。这个区域主要存放像C 语言的标准库和数学库这种共享库的代码和数据,例如hello 程序中的printf 函数就是存放在这里。
  • 继续往上看,这个区域称为用户栈(user stack),我们在写程序的时候都使用过函数调用,实际上函数调用的本质就是压栈。这句话的意思是:每一次当程序进行函数调用的时候,栈就会增长,函数执行完毕返回时,栈就会收缩。需要注意的是栈的增长方向是从高地址到低地址。
  • 最后,我们看一下地址空间的最顶部的区域,这个区域是为内核保留的区域,应用程序代码不能读写这个区域的数据,也不能直接调用内核中定义的函数,也就是说,这个区域对应用程序是不可见的

1.7.4 文件

文件就是字节序列,包括每个I/O设备、键盘、显示器,甚至网络都可以看成是文件。系统中所有的输入输出都是通过使用一小组称为Unix I/O的系统函数调用读写文件来实现的。

1.8 系统之间利用网络通信

实际上,网络也可以可视为一个I/O设备

当系统从主存复制一串字节到网络适配器时,数据流经过网络到达另一台机器,而不是比如说到达本地磁盘驱动器。相似地、系统可以读取从其他机器发送来的数据,并把数据复制到自己的主存。

如下结构图所示:

image

随着Internet这样的全球网络的出现,从一台主机复制信息到另一台主机已经成为计算机系统最重要的用途之一。

比如,像电子邮件、即时通信、万维网、FTP和telnet这样的应用都是基于网络复制信息的功能。

image

1.9 重要主题

1.9.1 Amdahl定律

Amdahl 定律(也叫阿姆达尔定律)的主要思想是:当我们对系统的某个部分加速时,其对系统整体性能的影响取决于该部分的重要性和加速程度。

若系统执行某应用程序需要的时间为 T o l d T_{old} Told。假设系统某部分所需执行时间与该时间的比例为 α α α,而该部分性能提升为 k k k。即该部分初始所需时间为 α T o l d αT_{old} αTold,现在所需时间为 T o l d / k T_{old}/k Told/k。因此,总的执行时间应为:

T n e w = ( 1 − a ) T o l d + ( a T o l d ) / k = T o l d [ ( 1 − a ) + a / k ] \mathrm{T_{new}=(1-a)T_{old}+(aT_{old})/k=T_{old}\left[(1-a)+a/k\right]} Tnew=(1a)Told+(aTold)/k=Told[(1a)+a/k]

由此,可以计算加速比 S = T o l d / T n e w S=T_{old}/T_{new} S=Told/Tnew

S = 1 ( 1 − a ) + a / k \mathrm{S}=\frac{1}{(1-\mathrm{a})+\mathrm{a}/\mathrm{k}} S=(1a)+a/k1

1.9.2 并发和并行

并发:是一个通用的概念,指一个同时具有多个活动的系统;

并行:用并发来使一个系统运行得更快。并行可以在计算机系统的多个抽象层次上的运用。

1.线程级并发:

  • 使用线程可以在一个进程中执行多个控制流。

  • 当构建一个由单操作系统内核控制的多处理器组成的系统时,我们就得到了一个多处理器系统

    image

  • 多核处理器将多个CPU(核)集成到一个电路芯片上。

    image

  • 超线程也称为同时多线程,是一项允许一个CPU执行多个控制流的技术。

2.指令级并行

  • 可以同时执行多条指令
  • 如果处理器可以达到比一个周期一条指令更快的执行速率,就称之为超标量处理器。

3.单指令多数据并行

  • 允许一条指令产生多个可以并行执行的操作,这种方式称为单指令多数据,即是SIMD并行

1.9.3计算机系统中抽象的重要性

  • 在处理器里,指令集架构提供了对实际处理器硬件的抽象。使用这个抽象,机器代码程序表现得就好像运行在一个一次只执行一条指令的处理器上。
  • 文件是对I/O设备的抽象表示,虚拟内存是对主存和磁盘I/O设备的抽象表示,进程则是对一个正在运行的程序的抽象表示。虚拟机提供对整个计算机的抽象。

可以达到比一个周期一条指令更快的执行速率,就称之为超标量处理器。

  • 22
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值