认识操作系统

硬件结构

图灵机的⼯作⽅式

图灵的基本思想是⽤机器来模拟⼈们⽤纸笔进⾏数学运算的过程,⽽且还定义了计算机由哪些部分组成,程序⼜是如何执⾏的。

图灵机的基本组成如下:

  • 有⼀条「纸带」,纸带由⼀个个连续的格⼦组成,每个格⼦可以写⼊字符,纸带就好⽐内存,⽽纸带上的格⼦的字符就好⽐内存中的数据或程序;
  • 有⼀个「读写头」,读写头可以读取纸带上任意格⼦的字符,也可以把字符写⼊到纸带的格⼦;
  • 读写头上有⼀些部件,⽐如存储单元、控制单元以及运算单元:
  1. 存储单元⽤于存放数据;
  2. 控制单元⽤于识别字符是数据还是指令,以及控制程序的流程等;
  3. 运算单元⽤于执⾏运算指令;

知道了图灵机的组成后,我们以简单数学运算的1+2作为例⼦,来看看它是怎么执⾏这⾏代码的。

⾸先,⽤读写头把1、2、+这 3 个字符分别写⼊到纸带上的 3 个格⼦,然后读写头先停在 1 字符对应的格⼦上;

接着,读写头读⼊ 1 到存储设备中,这个存储设备称为图灵机的状态;

然后读写头向右移动⼀个格,⽤同样的⽅式把 2 读⼊到图灵机的状态,于是现在图灵机的状态中存储着两个连续的数字,1 和 2;

读写头再往右移动⼀个格,就会碰到 + 号,读写头读到 + 号后,将 + 号传输给「控制单元」,控制单元发现是⼀个 + 号⽽不是数字,所以没有存⼊到状态中,因为 + 号是运算符指令,作⽤是加和⽬前的状态,于是通知「运算单元」⼯作。运算单元收到要加和状态中的值的通知后,就会把状态中的 1 和 2 读⼊并计算,再将计算的结果 3 存放到状态中;

最后,运算单元将结果返回给控制单元,控制单元将结果传输给读写头,读写头向右移动,把结果 3 写⼊到纸带的格⼦中;

通过上⾯的图灵机计算 1+2 的过程,可以发现图灵机主要功能就是读取纸带格⼦中的内容,然后交给控制单元识别字符是数字还是运算符指令,如果是数字则存⼊到图灵机状态中,如果是运算符,则通知运算符单元读取状态中的数值进⾏计算,计算结果最终返回给读写头,读写头把结果写⼊到纸带的格⼦中。

事实上,图灵机这个看起来很简单的⼯作⽅式,和我们今天的计算机是基本⼀样的。接下来,我们⼀同再看看当今计算机的组成以及⼯作⽅式。

冯诺依曼模型

在 1945 年冯诺依曼和其他计算机科学家们提出了计算机具体实现的报告,其遵循了图灵机的设计,⽽且还提出⽤电⼦元件构造计算机,并约定了⽤⼆进制进⾏计算和存储,还定义计算机基本结构为 5 个部分,分别是中央处理器(CPU)、内存、输⼊设备、输出设备、总线。

这 5 个部分也被称为冯诺依曼模型,接下来看看这 5 个部分的具体作⽤。

内存

我们的程序和数据都是存储在内存,存储的区域是线性的。

数据存储的单位是⼀个⼆进制位(bit),即 0 或 1。最⼩的存储单位是字节(byte),1 字节等于 8 位。内存的地址是从 0 开始编号的,然后⾃增排列,最后⼀个地址为内存总字节数 - 1,这种结构好似我们程序⾥的数组,所以内存读写任何⼀个数据的速度都是⼀样的。

中央处理器

中央处理器也就是我们常说的 CPU,32 位和 64 位 CPU 最主要区别在于⼀次能计算多少字节数据:

  • 32 位 CPU ⼀次可以计算 4 个字节;
  • 64 位 CPU ⼀次可以计算 8 个字节;

这⾥的 32 位和 64 位,通常称为 CPU 的位宽。

之所以 CPU 要这样设计,是为了能计算更⼤的数值,如果是 8 位的 CPU,那么⼀次只能计算 1 个字节 0~255 范围内的数值,这样就⽆法⼀次完成计算10000*500,于是为了能⼀次计算⼤数的运算,CPU 需要⽀持多个byte⼀起计算,所以 CPU 位宽越⼤,可以计算的数值就越⼤,⽐如说 32 位 CPU 能计算的最⼤整数是 4294967295 。

CPU 内部还有⼀些组件,常⻅的有寄存器、控制单元和逻辑运算单元等。其中,控制单元负责控制 CPU ⼯作,逻辑运算单元负责计算,⽽寄存器可以分为多种类,每种寄存器的功能⼜不尽相同。

CPU 中的寄存器主要作⽤是存储计算时的数据,为什么有了内存还需要寄存器?原因很简单,因为内存离 CPU 太远了,⽽寄存器就在 CPU ⾥,还紧挨着控制单元和逻辑运算单元,⾃然计算时速度会很快。

常⻅的寄存器种类:

  • 通⽤寄存器,⽤来存放需要进⾏运算的数据,⽐如需要进⾏加和运算的两个数据。
  • 程序计数器,⽤来存储 CPU 要执⾏下⼀条指令「所在的内存地址」,注意不是存储了下⼀条要执⾏的指令,此时指令还在内存中,程序计数器只是存储了下⼀条指令的地址。
  • 指令寄存器,⽤来存放程序计数器指向的指令,也就是指令本身,指令被执⾏完成之前,指令都存储在这⾥。

总线

总线是⽤于 CPU 和内存以及其他设备之间的通信,总线可分为 3 种:

  • 地址总线,⽤于指定 CPU 将要操作的内存地址;
  • 数据总线,⽤于读写内存的数据;
  • 控制总线,⽤于发送和接收信号,⽐如中断、设备复位等信号,CPU 收到信号后⾃然进⾏响应,这时也需要控制总线;

当 CPU 要读写内存数据的时候,⼀般需要通过两个总线:

  • ⾸先要通过「地址总线」来指定内存的地址;
  • 再通过「数据总线」来传输数据;

输⼊、输出设备

输⼊设备向计算机输⼊数据,计算机经过计算后,把数据输出给输出设备。期间,如果输⼊设备是键盘,按下按键时是需要和 CPU 进⾏交互的,这时就需要⽤到控制总线了。

线路位宽与 CPU 位宽

数据是如何通过线路传输的呢?其实是通过操作电压,低电压表示 0,⾼压电压则表示 1。

如果构造了⾼低⾼这样的信号,其实就是 101 ⼆进制数据,⼗进制则表示 5,如果只有⼀条线路,就意味着每次只能传递1 bit的数据,即 0 或 1,那么传输 101 这个数据,就需要 3 次才能传输完成,这样的效率⾮常低。

这样⼀位⼀位传输的⽅式,称为串⾏,下⼀个bit必须等待上⼀个bit传输完成才能进⾏传输。当然,想⼀次多传⼀些数据,增加线路即可,这时数据就可以并⾏传输。

为了避免低效率的串⾏传输的⽅式,线路的位宽最好⼀次就能访问到所有的内存地址。CPU 要想操作的内存地址就需要地址总线,如果地址总线只有 1 条,那每次只能表示 0 或 1 这两种情况,所以 CPU ⼀次只能操作 2 个内存地址,如果想要 CPU 操作 4G 的内存,那么就需要 32 条地址总线,因为 232 = 4G。

知道了线路位宽的意义后,我们再来看看 CPU 位宽。

CPU 的位宽最好不要⼩于线路位宽,⽐如 32 位 CPU 控制 40 位宽的地址总线和数据总线的话,⼯作起来就会⾮常复杂且麻烦,所以 32 位的 CPU 最好和 32 位宽的线路搭配,因为 32 位 CPU ⼀次最多只能操作 32 位宽的地址总线和数据总线。

如果⽤ 32 位 CPU 去加和两个 64 位⼤⼩的数字,就需要把这 2 个 64 位的数字分成 2 个低位 32 位数字和 2 个⾼位 32 位数字来计算,先加个两个低位的 32 位数字,算出进位,然后加和两个⾼位的 32 位数字,最后再加上进位,就能算出结果了,可以发现 32 位 CPU 并不能⼀次性计算出加和两个 64 位数字的结果。

对于 64 位 CPU 就可以⼀次性算出加和两个 64 位数字的结果,因为 64 位 CPU 可以⼀次读⼊ 64 位的数字,并且 64 位 CPU 内部的逻辑运算单元也⽀持 64 位数字的计算。

但是并不代表 64 位 CPU 性能⽐ 32 位 CPU ⾼很多,很少应⽤需要算超过 32 位的数字,所以如果计算的数额不超过 32 位数字的情况下,32 位和 64 位 CPU 之间没什么区别的,只有当计算超过 32 位数字的情况下,64 位的优势才能体现出来。

另外,32 位 CPU 最⼤只能操作 4GB 内存,就算装了 8GB 内存条也没⽤。⽽ 64 位 CPU 寻址范围则很⼤,理论最⼤的寻址空间为 264

程序执⾏的基本过程

在前⾯,我们知道了程序在图灵机的执⾏过程,接下来我们来看看程序在冯诺依曼模型上是怎么执⾏的。

程序实际上是⼀条⼀条指令,所以程序的运⾏过程就是把每⼀条指令⼀步⼀步的执⾏起来,负责执⾏指令的就是 CPU 了。

CPU 执⾏程序的过程如下:

  • 第⼀步,CPU 读取「程序计数器」的值,这个值是指令的内存地址,然后 CPU 的「控制单元」操作「地址总线」指定需要访问的内存地址,接着通知内存设备准备数据,数据准备好后通过「数据总线」将指令数据传给 CPU,CPU 收到内存传来的数据后,将这个指令数据存⼊到「指令寄存器」。
  • 第⼆步,CPU 分析「指令寄存器」中的指令,确定指令的类型和参数,如果是计算类型的指令,就把指令交给「逻辑运算单元」运算;如果是存储类型的指令,则交由「控制单元」执⾏;
  • 第三步,CPU 执⾏完指令后,「程序计数器」的值⾃增,表示指向下⼀条指令。这个⾃增的⼤⼩,由 CPU 的位宽决定,⽐如 32 位的 CPU,指令是 4 个字节,需要 4 个内存地址存放,因此「程序计数器」的值会⾃增 4;

简单总结⼀下就是,⼀个程序执⾏的时候,CPU 会根据程序计数器⾥的内存地址,从内存⾥⾯把需要执⾏的指令读取到指令寄存器⾥⾯执⾏,然后根据指令⻓度⾃增,开始顺序读取下⼀条指令。

CPU 从程序计数器读取指令、到执⾏、再到下⼀条指令,这个过程会不断循环,直到程序执⾏结束,这个不断循环的过程被称为 CPU 的指令周期。

a=1+2 执⾏具体过程

知道了基本的程序执⾏过程后,接下来⽤a =1+2的作为例⼦,进⼀步分析该程序在冯诺伊曼模型的执⾏过程。

CPU 是不认识a=1+2这个字符串,这些字符串只是⽅便我们程序员认识,要想这段程序能跑起来,还需要把整个程序翻译成汇编语⾔的程序,这个过程称为编译成汇编代码。

针对汇编代码,我们还需要⽤汇编器翻译成机器码,这些机器码由 0 和 1 组成的机器语⾔,这⼀条条机器码,就是⼀条条的计算机指令,这个才是 CPU 能够真正认识的东⻄。

下⾯来看看a=1+2在 32 位 CPU 的执⾏过程。

程序编译过程中,编译器通过分析代码,发现 1 和 2 是数据,于是程序运⾏时,内存会有个专⻔的区域来存放这些数据,这个区域就是「数据段」。如下图,数据 1 和 2 的区域位置:

  • 数据 1 被存放到0x104位置;
  • 数据 2 被存放到0x100位置;

注意,数据和指令是分开区域存放的,存放指令区域的地⽅称为「正⽂段」。

编译器会把a=1+2翻译成 4 条指令,存放到正⽂段中。如图,这 4 条指令被存放到了0x200~0x20c的区域中:

  • 0x200的内容是load指令将0x100地址中的数据 2 装⼊到寄存器 R0;
  • 0x204的内容是load指令将0x104地址中的数据 1 装⼊到寄存器 R1;
  • 0x208的内容是add指令将寄存器 R0 和 R1 的数据相加,并把结果存放到寄存器 R2;
  • 0x20c的内容是store指令将寄存器 R2 中的数据存回数据段中的0x108地址中,这个地址也就是变量a内存中的地址;

编译完成后,具体执⾏程序的时候,程序计数器会被设置为 0x200 地址,然后依次执⾏这 4 条指令。

上⾯的例⼦中,由于是在 32 位 CPU 执⾏的,因此⼀条指令是占 32 位⼤⼩,所以你会发现每条指令间隔 4 个字节。

⽽数据的⼤⼩是根据你在程序中指定的变量类型,⽐如int类型的数据则占 4 个字节,char类型的数据则占 1 个字节。

指令

上⾯的例⼦中,图中指令的内容是简易的汇编代码,⽬的是为了⽅便理解指令的具体内容,事实上指令的内容是⼀串⼆进制数字的机器码,每条指令都有对应的机器码,CPU 通过解析机器码来知道指令的内容。

不同的 CPU 有不同的指令集,也就是对应着不同的汇编语⾔和不同的机器码,接下来选⽤最简单的 MIPS 指集,来看看机器码是如何⽣成的,这样也能明⽩⼆进制的机器码的具体含义。

MIPS 的指令是⼀个 32 位的整数,⾼ 6 位代表着操作码,表示这条指令是⼀条什么样的指令,剩下的 26 位不同指令类型所表示的内容也就不相同,主要有三种类型R、I 和 J。

这三种类型的含义:

  • R 指令,⽤在算术和逻辑操作,⾥⾯由读取和写⼊数据的寄存器地址。如果是逻辑位移操作,后⾯还有位移操作的「位移量」,⽽最后的「功能码」则是再前⾯的操作码不够的时候,扩展操作码来表示对应的具体指令的;
  • I 指令,⽤在数据传输、条件分⽀等。这个类型的指令,就没有了位移量和操作码,也没有了第三个寄存器,⽽是把这三部分直接合并成了⼀个地址值或⼀个常数;
  • J 指令,⽤在跳转,⾼ 6 位之外的 26 位都是⼀个跳转后的地址;

接下来,我们把前⾯例⼦的这条指令:「add指令将寄存器 R0 和 R1 的数据相加,并把结果放⼊到 R2」,翻译成机器码。

加和运算add指令是属于 R 指令类型:

  • add对应的 MIPS 指令⾥操作码是000000,以及最末尾的功能码是100000,这些数值都是固定的,查⼀下 MIPS 指令集的⼿册就能知道的;
  • rs代表第⼀个寄存器 R0 的编号,即00000
  • rt代表第⼆个寄存器 R1 的编号,即00001
  • rd代表⽬标的临时寄存器 R2 的编号,即 00010;

因为不是位移操作,所以位移量是00000把上⾯这些数字拼在⼀起就是⼀条 32 位的 MIPS 加法指令了,那么⽤ 16 进制表示的机器码则是0x00011020

编译器在编译程序的时候,会构造指令,这个过程叫做指令的编码。CPU 执⾏程序的时候,就会解析指令,这个过程叫作指令的解码。

现代⼤多数 CPU 都使⽤来流⽔线的⽅式来执⾏指令,所谓的流⽔线就是把⼀个任务拆分成多个⼩任务,于是⼀条指令通常分为 4 个阶段,称为 4 级流⽔线,如下图:

四个阶段的具体含义:

  1. CPU 通过程序计数器读取对应内存地址的指令,这个部分称为Fetch(取得指令);
  2. CPU 对指令进⾏解码,这个部分称为Decode(指令译码);
  3. CPU 执⾏指令,这个部分称为Execution(执⾏指令);
  4. CPU 将计算结果存回寄存器或者将寄存器的值存⼊内存,这个部分称为Store(数据回写);

上⾯这 4 个阶段,我们称为指令周期,CPU 的⼯作就是⼀个周期接着⼀个周期,周⽽复始。

事实上,不同的阶段其实是由计算机中的不同组件完成的:

  • 取指令的阶段,我们的指令是存放在存储器⾥的,实际上,通过程序计数器和指令寄存器取出指令的过程,是由控制器操作的;
  • 指令的译码过程,也是由控制器进⾏的;
  • 指令执⾏的过程,⽆论是进⾏算术操作、逻辑操作,还是进⾏数据传输、条件分⽀操作,都是由算术逻辑单元操作的,也就是由运算器处理的。但是如果是⼀个简单的⽆条件地址跳转,则是直接在控制器⾥⾯完成的,不需要⽤到运算器。

指令的类型

指令从功能⻆度划分,可以分为 5 ⼤类:

  • 数据传输类型的指令,⽐如store/load是寄存器与内存间数据传输的指令,mov是将⼀个内存地址的数据移动到另⼀个内存地址的指令;
  • 运算类型的指令,⽐如加减乘除、位运算、⽐较⼤⼩等等,它们最多只能处理两个寄存器中的数据;
  • 跳转类型的指令,通过修改程序计数器的值来达到跳转执⾏指令的过程,⽐如编程中常⻅的if else、swtich-case、函数调⽤等。
  • 信号类型的指令,⽐如发⽣中断的指令trap
  • 闲置类型的指令,⽐如指令nop,执⾏后 CPU 会空转⼀个周期;

指令的执⾏速度

CPU 的硬件参数都会有 GHz 这个参数,⽐如⼀个 1 GHz 的 CPU,指的是时钟频率是 1 G,代表着 1 秒会产⽣ 1G 次数的脉冲信号,每⼀次脉冲信号⾼低电平的转换就是⼀个周期,称为时钟周期。

对于 CPU 来说,在⼀个时钟周期内,CPU 仅能完成⼀个最基本的动作,时钟频率越⾼,时钟周期就越短,⼯作速度也就越快。

⼀个时钟周期⼀定能执⾏完⼀条指令吗?答案是不⼀定的,⼤多数指令不能在⼀个时钟周期完成,通常需要若⼲个时钟周期。不同的指令需要的时钟周期是不同的,加法和乘法都对应着⼀条 CPU 指令,但是乘法需要的时钟周期就要⽐加法多。

程序执⾏的时候,耗费的 CPU 时间少就说明程序是快的,对于程序的 CPU 执⾏时间,我们可以拆解成 CPU 时钟周期数(CPU Cycles)和时钟周期时间(Clock Cycle Time)的乘积。

时钟周期时间就是我们前⾯提及的 CPU 主频,主频越⾼说明 CPU 的⼯作速度就越快,⽐如电脑的 CPU 是 2.4GHz 四核 Intel Core i5,这⾥的 2.4GHz 就是电脑的主频,时钟周期时间就是 1/2.4G。

要想 CPU 跑的更快,⾃然缩短时钟周期时间,也就是提升 CPU 主频,但是今⾮彼⽇,摩尔定律早已失效,当今的 CPU 主频已经很难再做到翻倍的效果了。

另外,换⼀个更好的 CPU,这个也是我们软件⼯程师控制不了的事情,我们应该把⽬光放到另外⼀个乘法因⼦ —— CPU 时钟周期数,如果能减少程序所需的 CPU 时钟周期数量,⼀样也是能提升程序的性能的。

对于 CPU 时钟周期数我们可以进⼀步拆解成:「指令数 x 每条指令的平均时钟周期数(Cycles Per Instruction,简称 CPI )」,于是程序的 CPU 执⾏时间的公式可变成如下:

因此,要想程序跑的更快,优化这三者即可:

  • 指令数,表示执⾏程序所需要多少条指令,以及哪些指令。这个层⾯是基本靠编译器来优化,毕竟同样的代码,在不同的编译器,编译出来的计算机指令会有各种不同的表示⽅式。
  • 每条指令的平均时钟周期数 CPI,表示⼀条指令需要多少个时钟周期数,现代⼤多数 CPU 通过流⽔线技术(Pipline),让⼀条指令需要的 CPU 时钟周期数尽可能的少;
  • 时钟周期时间,表示计算机主频,取决于计算机硬件。有的 CPU ⽀持超频技术,打开了超频意味着把 CPU 内部的时钟给调快了,于是 CPU ⼯作速度就变快了,但是也是有代价的,CPU 跑的越快,散热的压⼒就会越⼤,CPU 会很容易奔溃。
打赏
  • Copyrights © 2017-2023 WSQ
  • 访问人数: | 浏览次数:

请我喝杯咖啡吧~

支付宝
微信