【大话操作系统】打开操作系统的大门,这篇就够了

1,053 阅读13分钟

一切的起源 图灵机

图灵机主要由数据存储单元,控制单元,运算单元和一个可读写外部数据的读写头几部分构成。
图灵机工作需要有一条纸带,纸带上面布满格子,可以在格子上面记录字符,字符可分为数据字符和指令字符;纸带穿过图灵机并不断向前移动;图灵机上的读写头依次读取纸带格子上的字符,根据控制单元区分读取的字符属于数据还是指令,当读到数据字符时,将字符存储到存储单元中,当读到指令字符时,运算单元会将存储单元中的数据读取出来并进行相应运算,并将结果通过读写头写入纸带的下一个格子中。

图灵机的基本工作模式跟如今的计算机是一样的,数据和指令存在存储器中(纸带和存储单元),处理器读取后运算得出结果。计算机中使用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 中。

总结一下,程序执行时:

  1. 程序的指令和数据分别存入内存指令段和数据段中,PC 指针指到指令段的起始地址。

  2. CPU 读取 PC 指针指向的指令存入指令寄存器中。

  3. CPU 通过地址总线指定要访问的内存地址;通过控制总线发送 “读指令”。

  4. 内存通过数据总线将数据传入 CPU,CPU 将这个数据存到指令寄存器中。

  5. CPU 解析指令寄存器中的指令内容。

  6. CPU 通过运算单元和控制单元执行指令。

  7. PC 指针自增,指向下一条指令内存地址。

所以,取址、译码、执行,这是一个指令的执行周期,所有指令都会严格按照这个顺序执行

指令预读

CPU 执行指令的速度是非常快的,但内存的读写是非常慢的,所以,如果从内存中一条条读取指令再执行的话,指令执行周期会变得很慢。

前面我们学到,CPU 内部还有三级缓存,所以,我们可以将内存中的多条指令一次性先读到读写速度较快的 L1 缓存中,这样,取址速度就能跟的上 CPU 的指令执行速度了。

同时,为了避免数据缓存覆盖指令缓存影响指令执行,我们可以将 L1 缓存划分为指令区和数据区。

思考下 L2 和 L3 需要划分指令区和数据区吗?其实是不需要的,因为 L2 和 L3 并不需要协助指令预读。

如何更快的执行指令

为了更快的执行指令,我们需要使用 CPU 的指令流水线技术。

在刚才的流程中,取指,解码的时候运算单元是空闲的,为了提高指令处理速度,需要让运算单元就可以一直处于运算中。我们可以使用 CPU 的指令流水线技术,在第一条指令完成取址进行译码时,第二条指令立刻进行取址,依次类推,这样,在上一条指令完成执行后,下一条指令也完成译码可以进行执行了。

                                                          图片源自网络

一句话总结

  1. 程序存储在存储器中,cpu 读取指令并进行执行计算。

  2. 由于 cpu 的指令执行速度极快,目前没有存储器能同时满足读写速度快,散热小,能耗低,容量大等要求,所以采用存储器分级策略,使用多级缓存来匹配上 cpu 的执行速度。

  3. cpu 与内存之间的数据传输通过主板上的总线来完成。通过地址总线将要操作的内存地址信息传递给内存;通过控制总线发出命令类型;内存将数据从数据总线传入 CPU。

  4. 寄存器是 cpu 直接读取指令和参数数据的存储器。寄存器按用途可分为好几类。对于数据,会先将数据读到通用寄存器中,之后 CPU 从通用寄存器中读写数据;对于指令,CPU 会先根据 CS 段寄存器和指令指针寄存器共同指向的指令内存地址获取指令,并将指令存入指令寄存器中,之后 CPU 再从指令寄存器中读取执行。

  5. 指令的执行包括取址、译码、执行。为了避免 CPU 每次获取指令都得从内存中获取,可以先将指令预读到 CPU L1-Cache 中;同时,为了让 CPU 的计算单元一直处于运算状态,可以使用流水线技术。

写在最后(点关注,不迷路)

「白嫖不好,创作不易」,希望朋友们可以点赞评论关注三连🙏🙏

最后,再安利一波公众号「跬步匠心」,尽量用大白话讲技术。