一切的起源 图灵机
图灵机主要由数据存储单元,控制单元,运算单元和一个可读写外部数据的读写头几部分构成。
图灵机工作需要有一条纸带,纸带上面布满格子,可以在格子上面记录字符,字符可分为数据字符和指令字符;纸带穿过图灵机并不断向前移动;图灵机上的读写头依次读取纸带格子上的字符,根据控制单元区分读取的字符属于数据还是指令,当读到数据字符时,将字符存储到存储单元中,当读到指令字符时,运算单元会将存储单元中的数据读取出来并进行相应运算,并将结果通过读写头写入纸带的下一个格子中。
图灵机的基本工作模式跟如今的计算机是一样的,数据和指令存在存储器中(纸带和存储单元),处理器读取后运算得出结果。计算机中使用cpu进行指令计算,存放数据的存储器我们常听的有磁盘、SSD、内存。但cpu并不直接从这些存储器中读取并执行指令,而是采用分级缓存策略。
存储器分级
为什么需要分级
我们比较熟悉的磁盘,数据在断电之后还能保存着,而且磁盘的存储空间较大,通常能有上T容量,但其数据读取速度极慢;内存的读取速度虽然比磁盘快了将近100倍,但跟cpu的执行速度相比,还是属于”龟速“。此外,内存是按在主板上的,数据通过电路板传输到cpu上,数据传输的耗时相对于cpu执行速度来说也是不可忽视的。
存储器体积越小,其存储容量就会受到限制;读写速度越快,能耗和成本也会越高;其次,存储器距离cpu越远,数据传输也越大。所以,目前而言,使用单一存储器无法让存储器中的数据跟的上cpu的处理速度。
计算机采用的方案是将存储器分级,将cpu使用频率越高的数据,存放在读写速度越快,距离cpu更近的存储器(缓存)中;将使用频率较低的数据存放在读写速度较慢,距离cpu较远,但存储容量较大,成本较低的存储器中。 这样,cpu读取数据时,直接先从缓存中读取,缓存中不存在再从距离更远的存储器中读取。
分级缓存方案的可行性在于计算机存在局部性原理,试想下我们平时写的代码程序,运算用的最多的是for循环,然后对定义的几个变量进行计算读写。所以,cpu执行一个程序的时候,有几个数据区域的读写频率是比较高的。所以,可以将这些「热点」区域的数据缓存起来,下次读取的时候就会快很多。据统计,存储器缓存命中率能达到95%,也就是只有5%的数据会穿透到内存。
存储器分级策略
通常,存储器分成以下几个级别:
-
寄存器
-
CPU cache:
-
L1-cache
-
L2-cache
-
L3-cache
-
内存
-
磁盘/SSD
磁盘/SSD
SSD/磁盘是距离CPU最远,读取速度最慢的一类存储器,优点在于成本较低,断电后数据还在。其中SSD是我们常说的固态硬盘,结构与内存类似,读写速度比内存慢10-1000倍;磁盘读取速度更慢,比内存慢100W倍左右,随着SSD的普及,已经慢慢被取代了。
内存
内存是插在主板上,与CPU有一段距离,CPU通过主板总线读取内存中的数据,造价比磁盘稍贵,但读取速度比磁盘快,速度大概在200-300个CPU周期;容量方面,个人电脑的内存一般是8-16G,服务器上的内存可以达到几个T。
CPU周期:一条指令可分为取指令,执行指令等若干个阶段,每个阶段完成所需的时间成为CPU周期。
CPU cache (CPU 高速缓存)
CPU cache 存在于 CPU 内部,CPU cache 可分为 L1 (一级缓存)、L2 (二级缓存)、L3 (三级缓存),CPU 的每个核都有各自的 L1 和 L2 缓存,同一个 CPU 的多个核共享一个 L3 缓存。
与 CPU 距离:L1 < L2 < L3
容量: L1(几十~几百 KB)<L2 (几百 KB~几 MB) < L3 (几 MB~几十 MB)
读写速度: L1(2-4CPU 周期) > L2 (10-20CPU 周期) > L3 (20-60CPU 周期)
(L1 缓存划分了指令区和数据区,下文会解释)
需要注意的是,cpu 缓存中每个缓存的最小单位是内存的一个内存块,而不是缓存一个变量;cpu 缓存和内存的映射方式有很多种,类似于 cache 行号 = 内存页号 mod cache 总行数;这样,先根据内存地址计算出地址所在内存页号,再通过映射关系算出 cache 行号,如果存在缓存中,直接获取数据即可,如果不存在再到内存中获取。
寄存器
寄存器是 CPU 实际进行指令读写的地方,是距离 CPU 最近的存储器,读写速度也是最快,能在半个 CPU 周期完成读写;一个 CPU 中寄存器数量在几十到几百个之间,每个寄存器容量很小,只能存储一定字节(4-8 个字节)的数据。
32 位 CPU 中大多数寄存器可以存储 4 个字节64 位 CPU 中大多数寄存器可以存储 8 个字节
寄存器根据用途不同,可分为好几类,为了便于后面指令执行过程学习,我们先了解以下几类:
-
通用寄存器:用于存储程序参数数据。
-
指令寄存器:每条 CPU 执行的指令,会先从内存中读入指令寄存器中,然后再让 CPU 读取执行。
-
指令指针寄存器:存放着 CPU 下一条要执行的指令所在的内存地址,CPU 根据指令指针寄存器中的指令内存地址,将指令读入指令寄存器中。指令指针寄存器也成为 IP 寄存器。
-
段寄存器:为了可访问更大的物理空间,CPU 通过基础地址 + 偏移量定位一个物理内存地址。段寄存器中存储的是基地址信息。CS 是存放指令地址的一个段寄存器,与 IP 寄存器一起定位指令在内存中的地址。
假设一个寄存器最大存储 4 字节数据,4 字节 = 4*8=32 位,值表示范围:0~(2^32) -1,换算单位为 4G,也就是这个寄存器最大能查找 0-4G 范围的地址,但我们之前提到的内存容量可达几 T,所以,直接通过一个寄存器无法表示全部范围的内存地址。采用 “基础地址 + 偏移地址 = 物理地址” 的寻址模式,可极大扩大内存寻址能力。例如:32 位的基础地址左移 32 位,再加上 32 位的偏移地址,可表示 64 位(16EiB)的内存地址。需要注意的是,计算机的最终寻址范围是由下面介绍的地址总线决定的。
总线 - CPU 与外界的桥梁
按上面的存储器分级,数据先从磁盘加载到内存中,然后被读取到 CPU 内部的高速缓存和寄存器中,CPU 读取寄存器进行处理。其中,CPU 和 CPU cache 之间的数据读写是在 CPU 内部中完成的,CPU 对内存的读写则是通过主板上的总线完成的。
总线可以看成是多根导线的集合,通过控制导线电压的高低来传递数据,高电压是 1,低电压是 0。
根据传输信息的不同,总线分为地址总线,数据总线和控制总线
试想 “向内存 3 位置读取数据” 这一条读指令包含了几个信息:
-
操作的内存位置是 3(地址信息)
-
操作的命令是读命令(控制信息)
-
数据传输(数据信息)
3 类总线分别负责对应信息的传输:CPU 通过地址总线将要操作的内存地址信息传递给内存;通过控制总线发出内存读命令;内存将数据从数据总线传入 CPU。
图片源自《汇编语言 (第 3 版)》
地址总线
讲地址总线之前,我们先讲讲存储器地址的划分。存储器会被划分为若干个存储单元,存储单元从零开始编号,这些编号可以看做是存储单元在存储器中的地址。
每个存储单元由 8 个位 (bit) 组成,也就是可以存储一个字节的数据;假设一个存储器有 128 个存储单元,可以存储 128 个字节 (Byte)。
CPU 通过地址总线来指定存储单元,地址总线的线数,决定了对内存的寻址范围。比如,一个 CPU 有 16 根地址总线,最多可以寻找 2 的 16 次方个内存单元。
假设一个 16 位的 CPU 有 20 条地址总线,16 位的 CPU 如何一次性给出 20 位的地址呢?
其实答案前面已经给出了,CPU 内部会通过「基础地址」+「偏移地址」的方法合成一个 20 位的地址。
图片源自《汇编语言 (第 3 版)》
数据总线
CPU 与内存或其他器件通过数据总线进行数据传输,数据总线的宽度 (总线条数) 决定了 CPU 与外界的数据传输速度。8 根数据总线一次可传输一个字节 (8bit) 的数据,16 根数据总线一次可传输两个字节 (16bit)。
控制总线
CPU 对外部器件的控制是通过控制总线来进行的,多少根控制总线,意味着 CPU 对外部器件有多少种控制,所以控制总线的宽度决定了 CPU 对外部器件的控制能力。
指令执行
了解了各种存储器和总线,我们再来看看程序是如何从磁盘加载到内存然后被 CPU 执行的。
我们编写的程序需要先经过编译器翻译成 CPU 认识的指令,这个过程称为指令的构造。程序启动时,会将程序的指令和数据分别存在两个内存段中。同时,PC 指针(IP 寄存器 + CS 寄存器)会指到指令段的起始地址(就是将起始地址赋值到 PC 指针上),表示 CPU 将从这个地址开始读取内存中的指令并执行。
指令解析
指令先被读取到指令寄存器中,CPU 取出执行时,需要先对指令进行解析。
我们都知道,内存中存放的内容都是二进制类型(上面的指令我们写成 16 进制),cpu 读取到要执行的指令后,会先对二进制的指令进行解析。以上面 “0x8c400104” 为例,拆分成二进制:
上面指令分成操作码、寄存器编号、内存地址三部分:
-
最左边 6 位,称为操作码,“10011” 表示 load 指令。
-
中间 4 位,指定了寄存器的编号,“0001” 表示 R1 寄存器。
-
最后的 22 位表示要操作的内存地址。
所以,这条指令是指将指定内存地址的内容加载到寄存器 R1 中。
总结一下,程序执行时:
-
程序的指令和数据分别存入内存指令段和数据段中,PC 指针指到指令段的起始地址。
-
CPU 读取 PC 指针指向的指令存入指令寄存器中。
-
CPU 通过地址总线指定要访问的内存地址;通过控制总线发送 “读指令”。
-
内存通过数据总线将数据传入 CPU,CPU 将这个数据存到指令寄存器中。
-
CPU 解析指令寄存器中的指令内容。
-
CPU 通过运算单元和控制单元执行指令。
-
PC 指针自增,指向下一条指令内存地址。
所以,取址、译码、执行,这是一个指令的执行周期,所有指令都会严格按照这个顺序执行
指令预读
CPU 执行指令的速度是非常快的,但内存的读写是非常慢的,所以,如果从内存中一条条读取指令再执行的话,指令执行周期会变得很慢。
前面我们学到,CPU 内部还有三级缓存,所以,我们可以将内存中的多条指令一次性先读到读写速度较快的 L1 缓存中,这样,取址速度就能跟的上 CPU 的指令执行速度了。
同时,为了避免数据缓存覆盖指令缓存影响指令执行,我们可以将 L1 缓存划分为指令区和数据区。
思考下 L2 和 L3 需要划分指令区和数据区吗?其实是不需要的,因为 L2 和 L3 并不需要协助指令预读。
如何更快的执行指令
为了更快的执行指令,我们需要使用 CPU 的指令流水线技术。
在刚才的流程中,取指,解码的时候运算单元是空闲的,为了提高指令处理速度,需要让运算单元就可以一直处于运算中。我们可以使用 CPU 的指令流水线技术,在第一条指令完成取址进行译码时,第二条指令立刻进行取址,依次类推,这样,在上一条指令完成执行后,下一条指令也完成译码可以进行执行了。
图片源自网络
一句话总结
-
程序存储在存储器中,cpu 读取指令并进行执行计算。
-
由于 cpu 的指令执行速度极快,目前没有存储器能同时满足读写速度快,散热小,能耗低,容量大等要求,所以采用存储器分级策略,使用多级缓存来匹配上 cpu 的执行速度。
-
cpu 与内存之间的数据传输通过主板上的总线来完成。通过地址总线将要操作的内存地址信息传递给内存;通过控制总线发出命令类型;内存将数据从数据总线传入 CPU。
-
寄存器是 cpu 直接读取指令和参数数据的存储器。寄存器按用途可分为好几类。对于数据,会先将数据读到通用寄存器中,之后 CPU 从通用寄存器中读写数据;对于指令,CPU 会先根据 CS 段寄存器和指令指针寄存器共同指向的指令内存地址获取指令,并将指令存入指令寄存器中,之后 CPU 再从指令寄存器中读取执行。
-
指令的执行包括取址、译码、执行。为了避免 CPU 每次获取指令都得从内存中获取,可以先将指令预读到 CPU L1-Cache 中;同时,为了让 CPU 的计算单元一直处于运算状态,可以使用流水线技术。
写在最后(点关注,不迷路)
「白嫖不好,创作不易」,希望朋友们可以点赞评论关注三连🙏🙏
最后,再安利一波公众号「跬步匠心」,尽量用大白话讲技术。