你知道a = 1 + 2
在CPU是怎么执行的吗?32位和64位的操作系统有什么区别?64位的软件可以在32位的操作系统上面运行吗?
图灵机的工作方式
要想了解程序是怎么执行的,我们先来看下图灵机的工作方式。
图灵的基本思想是用机器来模拟人们用纸笔进行数学运算的过程,他把这样的过程看作下列两种简单的动作:
1、在纸上写上或擦除某个符号;
2、把注意力从纸的一个位置移动到另一个位置。
而在每个阶段,人要决定下一步的动作,依赖于 (1) 此人当前所关注的纸上某个位置的符号和(2) 此人当前思维的状态。
那么我们来看下图灵机是怎么执行1 + 2
的。
- 首先,在纸上写入「1、2、+」这三个字符,并读写头停放在1字符对应的格子上。
- 接着,读写头将1写入到存储设备中
- 读写头移动到下一格,读取2到存储设备中
- 读写头移动到下一格,读取+,判断加未运算符指令,变将存储设备中的1和2相加,得到3,并将3写入到存储设备中
- 读写头移动到下一格,将存储设备中的3写入到此格。
现代计算机的工作方式与图灵机执行1+2
的工作方式基本一致,我们来看下冯诺依曼模型。
冯诺依曼模型
冯诺依曼遵循图灵机的设计,使用电子元件构造计算机,约定用二进制进行计算和存储,并定义了冯诺依曼模型。
冯诺依曼模型定义了计算机的基本结构有五个部分,运算器、控制器、 存储器、输入设备、输出设备。
计算器、控制器位于CPU中,并使用总线与存储器和输入/输出设备完成交互。
寄存器是什么
看上面的冯诺依曼模型中,CPU中有个寄存器的角色,他是用来干嘛的呢?我们知道CPU中放着控制单元与逻辑运算单元,而数据需要依靠总线与存储单元、输入输出单元交互来获得,依靠总线传输数据会增加运算时间,那么就需要将正在计算的数据存放到寄存器中,通过减少与总线的交互来提升计算速度。根据存储的数据不同,常见的寄存器有以下几种。
- 通用寄存器,存放进行运算的数据。
- 程序计数器,存放CPU要执行的下一条指令的地址。
- 指令寄存器,存放程序计数器指向的指令。
总线
总线用于CPU与内存以及其他设备之前的通信。分为以下三种:
- 地址总线,用于指定CPU将要操作的内存地址。
- 数据总线,用于读写内存的数据。
- 控制总线,用于发送和接收信号,比如中断、设备复位等。CPU收到信号后自然进行相应,也需要控制总线。
当CPU读写内存时,首先通过地址总线来指定内存地址,然后通过控制总线来控制读或写命令,最后通过数据总线来传输数据。
线路位宽与CPU位宽
CPU通过总线与内存进行交互,那么总线是怎么传输数据的呢?其实就是操作电压,低电压为0,高电压为1,以此来表达二进制数据,从而完成数据传输,那么表示一个11010的信号则需要5条线路并行传输,线路的条数称为位宽,一般也对应CPU位宽。
CPU想要操作内存地址就需要地址总线。
假设有线路位宽为5,此时CPU想访问内存地址6,那么则通过地址总线传输00110即可,如果此时CPU想访问128,那么传输啥都没用,位宽为5,寻址范围只能在0-31上,可见,位宽决定了CPU能操作的内存大小,32位CPU只能操作2^32
(4G)大的内存。
所以不要给32位CPU安装8G内存条哦,4G就够了
线路位宽决定了数据传输的能力,那么CPU位宽呢?
看冯诺依曼模型可知,CPU放着控制单元与逻辑运算单元,CPU位宽决定了这里的计算能力,32位的CPU没法直接加和两个64位大小的数字,需要分高低位加和再组装,而64位计算机则可以直接对64位数字加和,那么CPU位宽则决定了一次能计算多少字节的数据。
- 32位CPU一次可以计算4个字节;
- 64位CPU一次可以计算8个字节;
如果计算的数额不超过32位,此时32位CPU与64位CPU其实没差别
程序执行的基本过程
程序实际上是一条条指令,所以程序的运行过程就是把每一条指令一步步的执行起来,负责执行指令的就是CPU。
CPU读取程序计数器的值,该值即为指令的内存地址,然后控制单元操作地址总线指定需要访问的内存地址,接着通知内存准备数据,数据准备好后通过数据总线传输给CPU,CPU收到数据后将该指令存放到指令寄存器。
CPU分析指令寄存器中的指令,确定指令的类型以及参数,若是计算类型,则交由逻辑运算单元,若是存储类型,则交由控制单元。
当该指令执行完成后,程序计数器自增,自增大小有位宽决定,如果是32位宽,则自增4。
以上过程不断循环,程序便跑起来了,这个循环过程成为CPU的指令周期。
a = 1 + 2执行具体过程
CPU可不认识什么a = 1 + 2,想要程序跑起来,首先要将其翻译成汇编语言,此过程称为汇编代码。
针对汇编代码,我们还需要用汇编器翻译成机器码,即01000101011这样的机器语言,此时才可以交由CPU完成执行。
汇编分为数据段
与指令段
,分别存放数据与指令。
编译器会把a = 1 + 2
翻译成4条指令,存放到正文段中。如图,这 4 条指令被存放到了 0x200 ~ 0x20c 的区域中:
- 0x200 的内容是 load 指令将 0x100 地址中的数据 1 装入到寄存器 R0;
- 0x204 的内容是 load 指令将 0x104 地址中的数据 2 装入到寄存器 R1;
- 0x208 的内容是 add 指令将寄存器 R0 和 R1 的数据相加,并把结果存放到寄存器 R2;
- 0x20c 的内容是 store 指令将寄存器 R2 中的数据存回数据段中的 0x108 地址中,这个地址也就是变量 a 内存中的地址;
指令
编译期在编译程序时,会构造指令,这个过程成为指令的编码。CPU执行程序时,就会解析指令,这个过程称为指令的解码。
现代CPU使用流水线方式执行指令,流水线方式将一条指令拆成四个阶段,如下图:
Fetch -> CPU通过程序计数器读取对应的内存地址的指令(获取指令)
Decode -> CPU对指令进行解码(指令译码)
Execution -> CPU执行指令(执行指令)
Store -> 将计算结果存回到寄存器或者将寄存器的值存回内存(数据回写)
上面4个阶段,称为指令周期,CPU的工作就是一个接着一个执行,周而复始,事实上,不同的阶段有计算机的不同组件完成:
- 取指令的阶段,我们的指令是存放在存储器里的,实际上,通过程序计数器和指令寄存器取出指令的过程,是由控制器操作的;
- 指令的译码过程,也是由控制器进行的;
- 指令执行的过程,无论是进行算术操作、逻辑操作,还是进行数据传输、条件分支操作,都是由算术逻辑单元操作的,也就是由运算器处理的。但是如果是一个简单的无条件地址跳转,则是直接在控制器里面完成的,不需要用到运算器。
指令的类型
指令从功能角度划分,可以分为 5 大类:
- 数据传输类型的指令,比如
store/load
是寄存器与内存间数据传输的指令,mov
是将一个内存地址的数据移动到另一个内存地址的指令; - 运算类型的指令,比如加减乘除、位运算、比较大小等等,它们最多只能处理两个寄存器中的数据;
- 跳转类型的指令,通过修改程序计数器的值来达到跳转执行指令的过程,比如编程中常见的
if-else
、swtich-case
、函数调用等。 - 信号类型的指令,比如发生中断的指令
trap
; - 闲置类型的指令,比如指令
nop
,执行后 CPU 会空转一个周期