计算机原理笔记整理

本文是读书笔记,大部分摘抄于极客时间:计算机原理。
系统大纲:
image

1. 计算机组成

  1. CPU: 计算机所有的计算都是由CPU来进行的。CPU从内存中读取数据进行计算,然后在写回内存
  2. 内存:你撰写的程序、打开的浏览器、运行的游戏,都要加载到内存里才能运行。程序读取的数据、计算得到的结果,也都要放在内存里。内存越大,能加载的东西自然也就越多。
  3. 主板:主板是一个有着各种各样,有时候多达数十乃至上百个插槽的配件。我们的CPU要插在主板上,内存也要插在主板上。主板的芯片组(Chipset)和总线(Bus)解决了 CPU 和内存之间如何通信的问题。芯片组控制了数据传输的流转,也就是数据从哪里到哪里的问题。总线则是实际数据传输的高速公路。因此,总线速度(BusSpeed)决定了数据能传输得多快。
  4. 显卡,GPU 可以做各种“计算”的工作,CPU有调度与运算的能力,对于需要巨大计算量的场景,CPU不能单独进行处理,此时需要GPU混合进行处理。对于图形渲染和深度学习,一般需要独立的显卡进行支持
  5. 主板的南桥:鼠标、键盘以及硬盘,这些都是插在主板上的。作为外部 I/O设备,它们是通过主板上的南桥(SouthBridge)芯片组,来控制和CPU之间的通信的。“南桥”芯片的名字很直观,一方面,它在主板上的位置,通常在主板的“南面”。另一方面,它的作用就是作为“桥”,来连接鼠标、键盘以及硬盘这些外部设备和 CPU 之间的通信。在以前还有北桥,北桥用来连接CPU,内存和显卡中间的通信,但是现在都移动到来CPU内部,所以已经没了

2. 性能

  1. 性能:性能一般通过俩个指标来进行衡量,一个是响应时间(跑得快),一个是吞吐量(搬的多)。一般性能用响应时间的倒数来表示
  2. 计算机计时单位:用时间来衡量性能有一个问题,那就是时间不准,某个程序执行时,CPU可能去执行其他程序了,或者又可能从硬盘或者内存中取数据。解决:统计的时候,需要将这些时间去除。可以通过time 命令来查看。
time ls 
real	0m0.002s   //总的
user	0m0.001s   //用户态执行指令的时间
sys	0m0.001s       //内核态执行指令的时间
总的时间为  user + sys

程序的 CPU 执行时间 =CPU 时钟周期数×时钟周期时间

时钟周期时间即执行一条简单指令的时间,为主频的倒数
CPU 时钟周期数 = 指令数×每条指令的平均时钟周期数(Cycles Per Instruction,简称 CPI)。不同的指令需要的Cycles不同,对于加法和乘法都是对应一个CPU指令,但是cycles 乘法就比加法的多。

程序的 CPU 执行时间 = 指令数×CPI×Clock Cycle Time

性能提升:

  • 提高时钟周期,也就是主频
  • 每条指令的平均时钟周期数 CPI,就是一条指令到底需要多少 CPU Cycle。
  • 指令数
  1. 提升主频:CPU计算是通过晶体管里面的“开关”不断地去“打开”和“关闭”,来组合完成各种运算和功能
  • 通过提升晶体管密度,也就是在同样的面积下放置更多的晶体管
  • 让晶体管打开和关闭的快一点。
  • 通过并行处理,即增加核数
  1. 功耗
    过多的提升会导致功耗的增加,而功耗是有一定极限的。
    功耗 ~= 1/2 ×负载电容×电压的平方×开关频率×晶体管数量
    从这个公式可以看出,降低电压是比较可行的方案,但是电压不能够无限低,由于各个元器件有电阻的存在。

3.指令

1. 基础
  1. 程序执行: 程序-> 汇编语言-> 机器码
  2. CPU指令分类:
  • 第一类是算术类指令。我们的加减乘除,在 CPU 层面,都会变成一条条算术类指令。
  • 第二类是数据传输类指令。给变量赋值、在内存里读写数据,用的都是数据传输类指令。
  • 第三类是逻辑类指令。逻辑上的与或非,都是这一类指令。
  • 第四类是条件分支类指令。日常我们写的“if/else”,其实都是条件分支类指令。
  • 最后一类是无条件跳转指令。写一些大一点的程序,我们常常需要写一些函数或者方法。在调用函数的时候,其实就是发起了一个无条件跳转指令。
  1. 寄存器是有n 个触发器(数字电路组成的逻辑门)组成的,比如说是有64个组成的,那么就是64位寄存器
  2. 指令执行过程: cpu从PC寄存器中取地址,找到地址对应的内存,取出其中指令送入指令寄存器执行,然后PC寄存器自增,重复操作。跳转指令就是当前指令修改了当前PC寄存器中所保存的下一条指令的地址,从而实现了跳转
  3. 函数调用:通过栈来实现。每调用一次函数,先进行当前现场的保存,然后将调用函数的栈帧(入参,返回地址)压入栈中。函数执行完,会将栈帧出栈,并且将PC寄存器将地址设置为栈帧的返回地址,在进行现场的恢复。
2. 电报

电报分为俩中信号,点信号(代表1,比较短促)和划信号(代表0,比较长一些),对于电报来说,按的短一点就是点,按的长一点就是划。
image

电报的原型是一个“蜂鸣器 + 电线 + 开关”形成的一个闭环电路。当开关按下是,蜂鸣器就会响,长短不同代表不同的信号。
image

但是如果距离特别长的话,这样就不合适了,此时可以通过继电器来实现放大的功能。

3. 浮点数

浮点数表示公式:

-1^s * 1.f * 2^e

0.5=(-1)^0 * 1.0 *2^-1
0.3=(-1)^0 * 1

对于32位单精度来说, s 表示符号位,占一位bit , f 为指数位,占8个bit, 表示范围位-127~128; f 为有效位,占23个bit,注意这里的 1 是必须有的。

当e 和 f 都为0时,表示0

对于平时的0.1, 0.2 这样的数,只有0.5是可以精确表示的。

十进制和二进制小数转换

十进制-> 二进制
小数部分转换成二进制是用一个相似的反方向操作,就是乘以 2,然后看看是否超过 1。如果超过 1,我们就记下 1,并把结果减去 1,进一步循环操作
对于0.1 记过转换是 0.0 0011 0011 后面的0011 是无限循环的

二进制-> 十进制
和整数相反,我们把小数点后的每一位,都表示对应的 2 的 -N 次方
0.1001 转换十进制公式: 0.5625

对于9.1 二进制表示:

1001.000110011 .... ->1.001000110011^3

对于公式表示来说,s为0; f 为00100011001100110011 001 ,转化为十进制为1.137499928474426291,多于23位后被截断,e为3

= -1^0 * 2^3 * 1.137499928474426291 = 9.09999942779541015625

由此可以得出之所以精度会丢失是因为浮点数转化为二进制会丢失部分有效数位超出23的部分;如果没有超出,精度应该是正确的。

小数加法:先对齐,在计算
两个浮点数的指数位可能是不一样的,所以我们要把两个的指数位,变成一样的,然后只去计算有效位的加法就好了。

0.1+0.25 如下图:
image

可以看出对于加法来说,如果俩个指数的位差的越大,位移的位数越大,可能丢失的精度也就越大。当然,也有可能你的运气非常好,右移丢失的有效位都是 0。这种情况下,对应的加法虽然丢失了需要加的数字的精度,但是因为对应的值都是 0,实际的加法的数值结果不会有精度损失。

4 处理器设计

1.多级流水线设计

一条 CPU 指令的执行,是由“取得指令(Fetch)- 指令译码(Decode)- 执行指令(Execute) ”这样三个步骤组成的。这个执行过程,至少需要花费一个时钟周期。因为在取指令的时候,我们需要通过时钟周期的信号,来决定计数器的自增。

很自然地,我们希望能确保让这样一整条指令的执行,在一个时钟周期内完成。这样,我们一个时钟周期可以执行一条指令,CPI 也就是 1,看起来就比执行一条指令需要多个时钟周期性能要好。采用这种设计思路的处理器,就叫作单指令周期处理器(Single Cycle Processor),也就是在一个时钟周期内,处理器正好能处理一条指令。

时钟周期是固定的。不同指令的执行时间不同,但是我们需要让所有指令都在一个时钟周期内完成,那就只好把时钟周期和执行时间最长的那个指令设成一样。这就好比学校体育课 1000 米考试,我们要给这场考试预留的时间,肯定得和跑得最慢的那个同学一样。因为就算其他同学先跑完,也要等最慢的同学跑完间,我们才能进行下一项活动。

image

现在CPU采用了多级流水线设计。我们可以把这个执行的执行拆分为“取址” , “译码”,“执行”
这三个步骤,可能还包含访存(从内存中或寄存器读取数据)和写回(将数据写回到内存中或寄存器)操作。每个步骤对应一个时钟周期,当一个指令的一个步骤执行完,立马执行下一个指令的同样步骤。这样我们就不用把时钟周期设置成整条指令执行的时间,而是拆分成完成这样的一个一个小步骤需要的时间。同时,每一个阶段的电路在完成对应的任务之后,也不需要等待整个指令执行完成,而是可以直接执行下一条指令的对应阶段。这样每个CPU指令对应CPI 会变大,但是时钟周期变小了。

image

这里我们以一个功能的开发来讲。其中涉及到几个步骤,为需求-》 开发-》测试-》上线,这里我们假设其中每个步骤需要花费2天。

对应于单指令周期处理器,需要花费8天,且每个阶段在执行过程中,其他阶段的参与者是属于不工作的状态的。

对于流水线来说,只需要花费俩天就可以了。当我们将需要给开发人员进行开发时,此时产品经理会进行新需求的整理;开发完成后,提交测试,此时开发又会进行下一个需求的开发,然后重复此步骤。 从结果上看,最后每个功能的开发时间约等于每个步骤的时间。

对于处理器来说,每个步骤为一个时钟周期,每个指令执行的的时间为最复杂的步骤的时间。
有几个步骤就叫做几级流水线。我们不需要确保最复杂的那条指令在时钟周期里面执行完成,而只要保障一个最复杂的流水线级的操作,在一个时钟周期内完成就好了。每个流水线之间是可以并行执行的。

超长流水线瓶颈:增加流水线深度,其实是有性能成本的

我们用来同步时钟周期的,不再是指令级别的,而是流水线阶段级别的。每一级流水线对应的输出,都要放到流水线寄存器(Pipeline Register)里面,然后在下一个时钟周期,交给下一个流水线级去处理。所以,每增加一级的流水线,就要多一级写入到流水线寄存器的操作。虽然流水线寄存器非常快,比如只有 20 皮秒(ps,10−12 秒)。

但是,如果我们不断加深流水线,这些操作占整个指令的执行时间的比例就会不断增加。最后,我们的性能瓶颈就会出现在这些 overhead 上。如果我们指令的执行有 3 纳秒,也就是 3000 皮秒。我们需要 20 级的流水线,那流水线寄存器的写入就需要花费 400 皮秒,占了超过 10%。如果我们需要 50 级流水线,就要多花费 1 纳秒在流水线寄存器上,占到 25%。这也就意味着,单纯地增加流水线级数,不仅不能提升性能,反而会有更多的 overhead 的开销。所以,设计合理的流水线级数也是现代 CPU 中非常重要的一点。
image

对于某些指令有相互依赖关系的,是不能享受流水线带来的益处的。

int a = 10 + 5; // 指令1
int b = a * 2; // 指令2
float c = b * 1.0f; // 指令3

对于上述代码来说,每个指令都依赖前一个指令的执行结果,假设每个指令执行时间是1os ,那么总的就是30S 。

对应于java来说,我们知道jvm会进行指令重排序,至于原因呢,就是因为这样可以使用流水线来进行优化。而volitile 会禁止指令集重排序,也就是禁止了流水线优化。

2. 冒险

多级流水线带来吐吞量的同时,也会带来一些风险。一般有三大冒险:分别是
结构冒险(Structural Hazard)、数据冒险(Data Hazard)以及控制冒险(Control Hazard)

1. 结构冒险

结构冒险一般发生在硬件层面上。本质上是一个硬件层面的资源竞争问题,也就是一个硬件
电路层面的问题。
CPU 在同一个时钟周期,同时在运行两条计算机指令的不同阶段。但是这两个不同的阶段,可
能会用到同样的硬件电路。

可以看到,在第 1 条指令执行到访存(MEM)阶段的时候,流水线里的第 4 条指令,在执行取指
令(Fetch)的操作。访存和取指令,都要进行内存数据的读取。我们的内存,只有一个地址译
码器的作为地址输入,那就只能在一个时钟周期里面读取一条数据,没办法同时执行第 1 条指令
的读取内存数据和第 4 条指令的读取指令代码。
image

类似的资源冲突,其实你在日常使用计算机的时候也会遇到。最常见的就是薄膜键盘的“锁键”问题。常用的最廉价的薄膜键盘,并不是每一个按键的背后都有一根独立的线路,而是多个键共用一个线路。如果我们在同一时间,按下两个共用一个线路的按键,这两个按键的信号就没办法都传输出去。

解决:

  • 方法就是增加资源。对于键盘这种情况,我们可以为每个键都设置一个独立的线路,这样就不会冲突了。机械键盘就是这样的。对于内存来说,我们可以将内存分为俩部分,一部分存储指令,一部分存储数据(哈弗体系)。现在技术采用的是将CPU高速缓存分为指令高速缓存和数据缓存部分。

  • NOP操作和指令对齐。这里以五级流水线为例。对于ADD指令,他不需要用到访存这个指令,但是他又不能直接跳过,跳过的话,很容易产生结构冒险。这时可以通过插入一个NOP(即什么也不做的指令)来使的执行步骤是一致的。

2.数据冒险

数据冒险,其实就是同时在执行的多个指令之间,有数据依赖的情况。这些数据依赖,我们可以分成三大类,分别是先写后读(Read After Write,RAW)、先读后写(Write After Read,
WAR)和写后再写(Write After Write,WAW)

解决:

  • 通过流水线停顿来实现。如果我们发现了后面执行的指令,会对前面执行的指令有数据层
    面的依赖关系,那最简单的办法就是“再等等”。我们在进行指令译码的时候,会拿到对应指令所需要访问的寄存器和内存地址。所以,在这个时候,我们能够判断出来,这个指令是否会触发数据冒险。如果会触发数据冒险,我们就可以决定,让整个流水线停顿一个或者多个周期。我们并不是让流水线停下来,而是在执行后面的操作步骤前面,插入一个 NOP 操作,也就是执行一个其实什么都不干的操作。
    image

  • 操作数前推。过多的NOP操作会导致CPU处于空转的情况,CPU得不到很好的使用,而操作数前推可以减少NOP的数量。通过在硬件层面制造一条旁路,让一条指令的计算结果,可以直接传输给下一条指令,而不再需要“指令1写回寄存器,指令再读取寄存器“这样多此一举的操作。这样直接传输带来的好处就是,后面的指令可以减少,甚至消除原本需要通过流水线停顿,才能解决数据冒险问题

  • 指令乱序。无论是流水线停顿,还是操作数前推,归根到底,只要前面指令的特定阶段
    还没有执行完成,后面的指令就会被“阻塞”住。
    但是这个“阻塞”很多时候是没有必要的。因为尽管你的代码生成的指令是顺序的,但是如果后面的指令不需要依赖前面指令的执行结果,完全可以不必等待前面的指令运算完成。对于以下代码就可以使用指令乱序。第三条指令并不需要等待前面的指令完成。

a=a+b;
b=a+1;
c=x+y
3.控制冒险

cmp 比较指令、jmp 和 jle 这样的条件跳转指令。可以看到,在 jmp 指令发生的时候,CPU 可能会跳转去执行其他指令。jmp后的那一条指令是否应该顺序加载执行,在流水线里面进行取指令的时候,我们没法知道。要等jmp指令执行完成,去更新了PC寄存器之后,我们才能知道,是否执行下一条指令,还是跳转到另外一个内存地址,去取别的指令。

这种为了确保能取到正确的指令,而不得不进行等待延迟的情况,就是今天我们要讲的控制冒险(Control Harzard)。这也是流水线设计里最后一种冒险。控制冒险也可以理解为对分支进行合理的选择。

解决:

  • 缩短分支延迟,我们可以将条件判断、地址跳转,都提前到指令译码阶段进行,而不需要放在指令执行阶段。对应的,我们也要在CPU里面设计对应的旁路,在指令译码阶段,就提供对应的判断比较的电路。
  • 分支预测。可以对判断条件进行预测,假设他一定不发生,这样有50%的概率猜中。如果最后预测的结果不同,把已经编译好的指令舍弃
  • 动态分支预测。类似于我们预测天气。有一个简单的策略,就是完全根据今天的天气来猜。如果今天下雨,我们就预测明天下雨。如果
    今天天晴,就预测明天也不会下雨。这是一个很符合我们日常生活经验的预测。因为一般下雨
    天,都是连着下几天,不断地间隔地发生“天晴 - 下雨 - 天晴 - 下雨”的情况并不多见。
3. 提升CPU性能黑科技
  • 超标量 超标量(Superscalar)技术能够让取指令以及指令译码也并行进行
  • 超长指令字(VLIW)技术可以搞定指令先后的依赖关系,使得一次可以取一个指令包。
  • 超线程。每个CPU核心同一时间只能运行一个线程的指令。而超线程可以使的每个CPU核心运行多个线程的命令(一般是2个)。超线程的目的,是在一个线程A的指令,在流水线里停顿的时候,让另外一个线程去执行指令。因为这个时候,CPU的译码器和ALU就空出来了,那么另外一个线程 B,就可以拿来干自己需要的事情。这个线程B可没有对于线程A里面指令的关联和依赖。我们通过CPU-Z工具查看IntelCPU,可以看到有Cores和Threads,cores 就是CPU核心数,Threads就是超线程数。
  • 单指令多数据流(SingleInstruction Multiple Data)SIMD 在获取数据和执行指令的时候,都做到了并
    行。一方面,在从内存里面读取数据的时候,SIMD 是一次性读取多个数据。执行指令也是一次执行多个指令。得益于更大的寄存器,可以同时加载更多的数据;以及多个数据互不影响,可以并行执行
4. 异常

这里的异常指的是硬件方面的异常,而不是java中的软件异常。

异常分类:

类型原因示例处理后操作
中断IO设备信号用户输入键盘数据执行下一条指令
陷阱由程序刻意触发,是主动的程序进行系统调用,如读取文件下一条指令
故障程序加载出错,是被动的程序的缺页错误当前指令
中止故障无法恢复ECC内存校验失败退出程序

关于异常,最有意思的一点就是,它其实是一个硬件和软件组合到一起的处理过程。异常的前半
生,也就是异常的发生和捕捉,是在硬件层面完成的。但是异常的后半生,也就是说,异常的处
理,其实是由软件来完成的。

计算机会为每一种可能会发生的异常,分配一个异常代码(Exception Number)。有些教科书会
把异常代码叫作中断向量(Interrupt Vector)。异常发生的时候,通常是 CPU 检测到了一个特殊
的信号。比如,你按下键盘上的按键,输入设备就会给 CPU 发一个信号。或者,正在执行的指
令发生了加法溢出,同样,我们可以有一个进位溢出的信号。这些信号呢,在组成原理里面,我
们一般叫作发生了一个事件(Event)。CPU 在检测到事件的时候,其实也就拿到了对应的异常
代码。

这些异常代码里,I/O 发出的信号的异常代码,是由操作系统来分配的,也就是由软件来设定
的。而像加法溢出这样的异常代码,则是由 CPU 预先分配好的,也就是由硬件来分配的。这又
是另一个软件和硬件共同组合来处理异常的过程。

拿到异常代码之后,CPU 就会触发异常处理的流程。计算机在内存里,会保留一个异常表
(Exception Table)。也有地方,把这个表叫作中断向量表(Interrupt Vector Table),好和上面
的中断向量对应起来,存放的是不同的异常
代码对应的异常处理程序(Exception Handler)所在的地址。
我们的 CPU 在拿到了异常码之后,会先把当前的程序执行的现场,保存到程序栈里面,然后根
据异常码查询,找到对应的异常处理程序,最后把后续指令执行的指挥权,交给这个异常处理程序。(上下文切换)

5. 指令架构

复杂指令集(Complex Instruction
Set Computing,简称 CISC)CPU 的指令集里的机器码是固定长度
精简指令集(Reduced Instruction Set Computing,简称 RISC):
CPU的指令集里的机器码是可变长度

一开始都是CISC指令集,但后来通过研究发现,CPU 80%的时间都是在执行20%的简单指令,而复杂指令集可以通过简单指令集组合,RISC架构开始成为主流。

RAM为什么会成为移动端主流:RAM是RISC架构,并且功耗低,价格低。

6. GPU

3D图像渲染步骤:

  • 顶点处理:将我们图像的三维坐标转化为二维坐标
  • 图元处理:将坐标进行连线,构成一个图形
  • 栅格化:将每个图形用像素点来表示
  • 片段处理:为每个像素点上色
  • 像素操作:我们就要把不同的多边形的像素点“混合(Blending)”到一起。可能前面的多边形可能是半透明的,那么前后的颜色就要混合在一起变成一个新的颜色;或者前面的多边形遮挡住了后面的多边形,那么我们只要显示前面多边形的颜色就好了。最终,输出到显示设备

上述5个步骤的渲染流程呢,一般也被称之为图形流水线(Graphic Pipeline)。

在上世纪 90 年代的时候,屏幕的分辨率还没有现在那么高。一般的 CRT 显示器也就是 640×480的分辨率。这意味着屏幕上有30万个像素需要渲染。为了让我们的眼睛看到画面不晕眩,我们希望画面能有 60帧。于是,每秒我们就要重新渲染60次这个画面。也就是说,每秒我们需要完成 1800 万次单个像素的渲染。从栅格化开始,每个像素有3个流水线步骤,即使每次步骤只有1 个指令,那我们也需要 5400 万条指令,也就是 54M 条指令。

90 年代的 CPU 的性能是多少呢?93 年出货的第一代 Pentium 处理器,主频是 60MHz,后续逐步推出了 66MHz、75MHz、100MHz 的处理器。以这个性能来看,用 CPU 来渲染 3D 图形,基本上就要把CPU的性能用完了。因为实际的每一个渲染步骤可能不止一个指令,我们的 CPU 可能根本就跑不动这样的三维图形渲染。

既然图形渲染的流程是固定的,那我们直接用硬件来处理这部分过程,不用 CPU来计算是不是就好了?很显然,这样的硬件会比制造有同样计算性能的 CPU 要便宜得多。因为整个计算流程是完全固定的,不需要流水线停顿、乱序执行等等的各类导致 CPU 计算变得复杂的问题。我们也不需要有什么可编程能力,只要让硬件按照写好的逻辑进行运算就好了。

由于现代GPU不需要像CPU那样需要高速缓存,分支预测,乱序执行以及一些其他的逻辑操作,只需要专注于计算,导致算力很强大。而现在的深度学习需要大量的计算,GPU刚好适合。使用GPU可以将计算的时间缩短一个量级。

7. 虚拟机技术
1. 解释性虚拟机

通过开发一个应用程序,跑在我们的操作系统上。这个应用程序呢,可以识别我们想要模拟的、计算机系统的程序格式和指令,然后一条条去解释执行,最后交由操作系统去执行。我们常用的安卓模拟器以及JVM都是该类型的。

缺点:

  • 做不到精确模拟,毕竟要模拟的系统可能需要特定的电路才能实现
  • 性能差,由于是对要模拟的系统的每条指令都进行解释执行,性能会变得特别差
2. Type-1 和 Type-2

所以,首先我们需要一个“全虚拟化”的技术,也就是说,我们可以在现有的物理服务器的硬件和操作系统上,去跑一个完整的、不需要做任何修改的客户机操作系统(Guest OS)。那么,我们怎么在一个操作系统上,再去跑多个完整的操作系统呢?答案就是,我们自己做软件开发中很常用的一个解决方案,就是加入一个中间层。在虚拟机技术里面,这个中间层就叫作虚拟机监视器,英文叫 VMM(Virtual Machine Manager)或者 Hypervisor。

我们的虚拟机和VMM进行交互,而不是直接和操作系统进行交互。我们跑在上面的虚拟机呢,会把整个的硬件特征都映射到虚拟机环境里,这包括整个完整的CPU指令集、I/O操作、中断等等。

我们实际的指令是怎么落到硬件上去实际执行的呢?这里有两种办法,也就是 Type-1 和 Type-2 这两种类型的虚拟机。

Type-2 和解释性虚拟机类似,我们上面说的虚拟机监视器好像一个运
行在操作系统上的软件。你的客户机的操作系统呢,把最终到硬件的所有指令,都发送给虚拟机
监视器。而虚拟机监视器,又会把这些指令再交给宿主机的操作系统去执行。常用于个人电脑。

在数据中心里面用的虚拟机,我们通常叫作 Type-1 型的虚拟机。这个时候,客户机的指令交给
虚拟机监视器之后呢,不再需要通过宿主机的操作系统,才能调用硬件,而是可以直接由虚拟机
监视器去调用硬件

另外,在数据中心里面,我们并不需要在 Intel x86 上面去跑一个 ARM 的程序,而是直接在 x86
上虚拟一个 x86 硬件的计算机和操作系统。所以,我们的指令不需要做什么翻译工作,可以直接
往下传递执行就好了,所以指令的执行效率也会很高

所以,在 Type-1 型的虚拟机里,我们的虚拟机监视器其实并不是一个操作系统之上的应用层程
序,而是一个嵌入在操作系统内核里面的一部分。无论是 KVM、XEN 还是微软自家的 Hyper-V,
其实都是系统级的程序。

3. docker

省去了单独的操作系统,对资源和环境进行隔离。

5. 存储

1. 存储结构

image

容量越小的设备速度越快,而且,CPU 并不是直接和每一种存储器设备打交道,而是每
一种存储器设备,只和它相邻的存储设备打交道。比如,CPU Cache 是从内存里加载而来的,
或者需要写回内存,并不会直接写回数据到硬盘,也不会直接从硬盘加载数据到 CPU Cache
中,而是先加载到内存,再从内存加载到 Cache 中。

在 CPU 里,通常会有 L1、L2、L3 这样三层高速缓存。每个 CPU 核心都有一块属于自己的 L1
高速缓存,通常分成指令缓存和数据缓存,分开存放 CPU 使用的指令和数据。
L1 的 Cache 往往就嵌在 CPU 核心的内部。

L2 的 Cache 同样是每个 CPU 核心都有的,不过它往往不在 CPU 核心的内部。所以,L2 Cache的访问速度会比 L1 稍微慢一些。而 L3 Cache,则通常是多个 CPU 核心共用的,尺寸会更大一些,访问速度自然也就更慢一些。
image

2. 局部性原理

时间局部性:如果某个数据被访问过,那么这个数据在短时间内还会被访问
空间局部性:如果某个数据被访问过,那么与它相邻的数据也很快被访问过

3.CPU cache

当CPU将数据从内存读入CPU cache 中,是一块一块进行读取。这个块叫做缓存块,在常用的CPU里,这个缓存块的大小一般为64字节。

1.CPU读取数据过程:

image

问题来了,CPU 如何知道要访问的内存数据,存储在 Cache 的哪个位置呢?接下来,我就从最
基本的直接映射 Cache(Direct Mapped Cache)说起,带你来看整个 Cache 的数据结构和访问
逻辑。

2. CPU cache 块组成
1.索引

CPU 访问内存数据,是一小块一小块数据来读取的,这个块我们叫做缓存行,一般64位计算机的缓存行大小为64 byte。对于读取
内存中的数据,我们首先拿到的是数据所在的内存块(Block)的地址。而直接映射 Cache 采用
的策略,就是确保任何一个内存块的地址,始终映射到一个固定的 CPU Cache 地址(Cache
Line)。而这个映射关系,通常用 mod 运算(求余运算)来实现

比如说,我们的主内存被分成 0~31 号这样 32 个块。我们一共有 8 个缓存块。用户想要访问第
21 号内存块。如果 21 号内存块内容在缓存块中的话,它一定在 5 号缓存块(21 mod 8 = 5)中。实际计算中,我们可以将缓存块设置为2的N次方,这样我们直接通过去地址块的低N位即可。

2.组标记

取 Block 地址的低位,就能得到对应的 Cache Line 地址,除了 21 号内存块外,13 号、5 号等很
多内存块的数据,都对应着 5 号缓存块中。既然如此,假如现在 CPU 想要读取 21 号内存块,在
读取到 5 号缓存块的时候,我们怎么知道里面的数据,究竟是不是 21 号对应的数据呢?

这个时候,在对应的缓存块中,我们会存储一个组标记(Tag)。这个组标记会记录,当前缓存
块内存储的数据对应的内存块,而缓存块本身的地址表示访问地址的低 N 位。就像上面的例子,21 的低 3 位 101,缓存块本身的地址已经涵盖了对应的信息、对应的组标记,我们只需要记录21 剩余的高 2 位的信息,也就是 10 就可以了。通俗点说,组标记代表内存块的地址。

3.有效位

除了组标记信息之外,缓存块中还有两个数据。一个自然是从主内存中加载来的实际存放的数
据,另一个是有效位(validbit)。啥是有效位呢?它其实就是用来标记,对应的缓存块中的数据是否是有效的,确保不是机器刚刚启动时候的空数据。如果有效位是 0,无论其中的组标记和Cache Line 里的数据内容是什么,CPU都不会管这些数据,而要直接访问内存,重新加载数据。

4.偏移量

CPU 在读取数据的时候,并不是要读取一整个 Block,而是读取一个他需要的数据片段。这样的
数据,我们叫作 CPU 里的一个字(Word)。具体是哪个字,就用这个字在整个 Block 里面的位
置来决定。这个位置,我们叫作偏移量(Offset)。

总结一下,一个内存的访问地址,最终包括高位代表的组标记、低位代表的索引,以及在对应的
Data Block 中定位对应字的位置偏移量。

3.CPU访问CPU cache 过程
  1. 据内存地址的低位,计算在 Cache 中的索引;
  2. 判断有效位,确认 Cache 中的数据是有效的(0为无效);
  3. 对比内存访问地址的高位,和 Cache 中的组标记,确认 Cache 中的数据就是我们要访问的内存
    数据,从 Cache Line 中读取到对应的数据块(Data Block);
  4. 根据内存地址的 Offset 位,从 Data Block 中,读取希望读取到的字。
  5. 如果在 2、3 这两个步骤中,CPU 发现,Cache 中的数据并不是要访问的内存地址的数据,那
    CPU 就会访问内存,并把对应的 Block Data 更新到 Cache Line 中,同时更新对应的有效位和组
    标记的数据。
4.MESI
1.CPU 缓存写入数据
  1. 写直达,比较慢,因为无论如何都需要写入内存中
graph TD
    st[内存写入请求]-->a
    a{缓存命中}
    a-->|命中|b1(数据写到CPU cache 块)
    a-->|未命中|b2(数据写到主内存)
    b1-->b2
  1. 写回
graph TD
    st[内存写入请求]-->a
    a{缓存命中}
    a-->|命中|b1{cache 块数据是否为脏}
    b1-->|是|b3{将cache block 数据写回内存}
    b1-->|否|b4{从主内存中读取数据到 cache block,这里需要判断一下是否是当前内存}
    b3-->b4
    a-->|未命中|c2(写入数据到cache block)
    b4-->c2
    c2-->c3(将cache block 标记为脏)
    c3-->c4(FINISH)

写回策略的过程是这样的:如果发现我们要写入的数据,就在 CPU Cache 里面,那么我们就只
是更新 CPU Cache 里面的数据。同时,我们会标记 CPU Cache 里的这个 Block 是脏(Dirty)
的。所谓脏的,就是指这个时候,我们的 CPU Cache 里面的这个 Block 的数据,和主内存是不
一致的。

如果我们发现,我们要写入的数据所对应的 Cache Block 里,放的是别的内存地址的数据,那么
我们就要看一看,那个 Cache Block 里面的数据有没有被标记成脏的。如果是脏的话,我们要先
把这个 Cache Block 里面的数据,写入到主内存里面。然后,再把当前要写入的数据,写入到
Cache 里,同时把 Cache Block 标记成脏的。如果 Block 里面的数据没有被标记成脏的,那么我
们直接把数据写入到 Cache 里面,然后再把 Cache Block 标记成脏的就好了。

在用了写回这个策略之后,我们在加载内存数据到 Cache 里面的时候,也要多出一步同步脏
Cache 的动作。如果加载内存里面的数据到 Cache 的时候,发现 Cache Block 里面有脏标记,
我们也要先把 Cache Block 里的数据写回到主内存,才能加载数据覆盖掉 Cache。

可以看到,在写回这个策略里,如果我们大量的操作,都能够命中缓存。那么大部分时间里,我
们都不需要读写主内存,自然性能会比写直达的效果好很多

5.MESI

MESI 是用来解决缓存一致性问题。即同样的数据,在不同核心的缓存中不一致问题。

解决缓存一致性问题,首先要解决CPU核心之间的数据传播问题。最常见的方案就是总线嗅探。

这个策略,本质上就是把所有的读写请求都通过总线(Bus)广播给所有的 CPU 核心,然后让各个核心去“嗅探”这些请求,再根据本地的情况进行响应。总线本身就是一个特别适合广播进行数据传输的机制,所以总线嗅探这个办法也是我们日常使用的 Intel CPU 进行缓存一致性处理的解决方案。

解决问题:

  1. 第一点叫写传播(Write Propagation)。写传播是说,在一个 CPU 核心里,我们的 Cache 数据更新,必须能够传播到其他的对应节点的 Cache Line里。

  2. 第二点叫事务的串行化(Transaction Serialization),事务串行化是说,我们在一个 CPU 核心里
    面的读取和写入,在其他的节点看起来,顺序是一样的。也就是说对同一个数据的操作顺序在其他节点看来是一样的

MESI 协议,是一种叫作写失效(Write Invalidate)的协议。在写失效协议里,只有一个 CPU 核
心负责写入数据,其他的核心,只是同步读取到这个写入。在这个 CPU 核心写入 Cache 之后,
它会通过总线去广播一个“失效”请求告诉所有其他的 CPU 核心。其他的 CPU 核心,只是去判断自己是否
也有一个“失效”版本的 Cache Block,然后把这个也标记成失效的就好了。

MESI 协议的由来呢,来自于我们对 Cache Line 的四个不同的标记,分别是:

  • M:代表已修改(Modified)
  • E:代表独占(Exclusive)
  • S:代表共享(Shared)
  • I:代表已失效(Invalidated)

我们先来看看“已修改”和“已失效”,这两个状态比较容易理解。所谓的“已修改”,就是我们上一讲
所说的“脏”的 Cache Block。Cache Block 里面的内容我们已经更新过了,但是还没有写回到主内
存里面。而所谓的“已失效“,自然是这个 Cache Block 里面的数据已经失效了,我们不可以相信
这个 Cache Block 里面的数据。

然后,我们再来看“独占”和“共享”这两个状态。这就是 MESI 协议的精华所在了。无论是独占状态
还是共享状态,缓存里面的数据都是“干净”的。这个“干净”,自然对应的是前面所说的“脏”的,也
就是说,这个时候,Cache Block 里面的数据和主内存里面的数据是一致的。
那么“独占”和“共享”这两个状态的差别在哪里呢?这个差别就在于,在独占状态下,对应的
Cache Line 只加载到了当前 CPU 核所拥有的 Cache 里。其他的 CPU 核,并没有加载对应的数
据到自己的 Cache 里。这个时候,如果要向独占的 Cache Block 写入数据,我们可以自由地写
入数据,而不需要告知其他 CPU 核。

在独占状态下的数据,如果收到了一个来自于总线的读取对应缓存的请求,它就会变成共享状
态。这个共享状态是因为,这个时候,另外一个 CPU 核心,也把对应的 Cache Block,从内存
里面加载到了自己的 Cache 里来。

而在共享状态下,因为同样的数据在多个 CPU 核心的 Cache 里都有。所以,当我们想要更新
Cache 里面的数据的时候,不能直接修改,而是要先向所有的其他 CPU 核心广播一个请求,要
求先把其他 CPU 核心里面的 Cache,都变成无效的状态,然后再更新当前 Cache 里面的数据。
这个广播操作,一般叫作 RFO(Request For Ownership),也就是获取当前对应 Cache Block
数据的所有权。

image

5. disrupor

disrupor 是一个高性能的队列。充分利用了CPU优化特性。

用到的CPU特点:

  • 移除伪共享。我们知道CPU读取内存是通过缓存行来进行读取的,那么对于一个数据来说,即使他自己并没有修改过,但是和它同处一个缓存行内的数据修改的话,那么这个数据还是需要到内存中去读的。而disrupor为其中一个long 类型数据前后各填充了7个long 类型,使得这个数据所在的缓存行无论何时都只有他一个数据
  • 数据结构由链表改为数组,通过环型数组来实现。可以充分利用CPUcache特性。而且遍历数据的时候也会有巨大的优势,就是CPU层面的分支预测会特别准确。这可以使我们更好的利用CPU的多级流水线,使程序跑的更快。
  • 无锁设计。前面说过,上下文切换需要把当前寄存器里的信息保存到线程栈里,这也就意味着,已经加载到CPU cache 里的数据或指令,又回到了内存中,当恢复现场的时候,又需要重新加载,这回导致比较慢。Java 里用的LinkedBolckingQueue 采用的重入锁。而disruptor采用的是CAS 无锁设计。
4.内存
  1. 虚拟地址 :

每个程序装载到内存中,都对应一段内存地址来执行,但是每个程序的地址可能会冲突,即自己想要的内存地址已经被其他程序占用了。

如何解决呢:我们可以在内存里面,找到一段连续的内存空间,然后分配给装载的程序,然后把这段连续的内存空间地址,和整个程序指令里指定的内存地址做一个映射。我们把指令里用到的地址叫做虚拟内存地址,实际在硬件里面的地址叫物理地址。

对于每个程序来说,他只需要关注虚拟地址就可以了。每个程序看到的都是相同的地址,我们维护一个虚拟地址和物理地址之间的映射表。这样实际程序指令执行的时候,会通过虚拟内存地址,找到对应的物理内存地址,然后执行。因为是连续的内存地址空间,所以我们只需要维护映射关系的起始地址和对应的空间大小就可以了。

  1. 内存分段与内存交换

这种找出一段连续的物理内存和虚拟内存地址进行映射的方法,我们叫分段。会产生内存碎片。

我们来看这样一个例子。我现在手头的这台电脑,有 1GB 的内存。我们先启动一个图形渲染程序,占用了 512MB 的内存,接着启动一个 Chrome 浏览器,占用了 128MB 内存,再启动一个 Python 程序,占用了 256MB 内存。这个时候,我们关掉 Chrome,于是空闲内存还有 1024 - 512 - 256 = 256MB。按理来说,我们有足够的空间再去装载一个 200MB 的程序。但是,这 256MB 的内存空间不是连续的,而是被分成了两段 128MB 的内存。因此,实际情况是,我们的程序没办法加载进来。当然,这个我们也有办法解决。解决的办法叫内存交换(Memory Swapping)。

image

我们可以把 Python 程序占用的那 256MB 内存写到硬盘上,然后再从硬盘上读回来到内存里面。不过读回来的时候,我们不再把它加载到原来的位置,而是紧紧跟在那已经被占用了的 512MB 内存后面。这样,我们就有了连续的 256MB 内存空间,就可以去加载一个新的 200MB 的程序。如果你自己安装过 Linux 操作系统,你应该遇到过分配一个 swap 硬盘分区的问题。这块分出来的磁盘空间,其实就是专门给 Linux 操作系统进行内存交换用的。

内存交换并不是万能的,会涉及到磁盘的访问,如果交换的内存特别的大,会导致这个机器都会变卡。

  1. 内存分页

既然问题出现在内存碎片和内存交换的空间太大上,那么解决问题的办法就是,少出现一些内存碎片。另外,当需要进行内存交换的时候,让需要交换写入或者从磁盘装载的数据更少一点,这样就可以解决这个问题。这个办法,在现在计算机的内存管理里面,就叫作内存分页

和分段这样分配一整段连续的空间给到程序相比,分页是把整个物理内存空间切成一段段固定尺寸的大小。而对应的程序所需要占用的虚拟内存空间,也会同样切成一段段固定尺寸的大小。这样一个连续并且尺寸固定的内存空间,我们叫页(Page)。从虚拟内存到物理内存的映射,不再是拿整段连续的内存的物理地址,而是按照一个一个页来的。页的尺寸一般远远小于整个程序的大小。在 Linux 下,我们通常只设置成 4KB。你可以通过命令看看你手头的 Linux 系统设置的页的大小。

$ getconf PAGE_SIZE

由于内存空间都是预先划分好的,也就没有了不能使用的碎片,而只有被释放出来的很多 4KB 的页。即使内存空间不够,需要让现有的、正在运行的其他程序,通过内存交换释放出一些内存的页出来,一次性写入磁盘的也只有少数的一个页或者几个页,不会花太多时间,让整个机器被内存交换的过程给卡住。

更进一步地,分页的方式使得我们在加载程序的时候,不再需要一次性都把程序加载到物理内存中。我们完全可以在进行虚拟内存和物理内存的页之间的映射之后,并不真的把页加载到物理内存里,而是只在程序运行中,需要用到对应虚拟内存页里面的指令和数据时,再加载到物理内存里面去。

实际上,我们的操作系统,的确是这么做的。当要读取特定的页,却发现数据并没有加载到物理内存里的时候,就会触发一个来自于 CPU 的缺页错误(Page Fault)。我们的操作系统会捕捉到这个错误,然后将对应的页,从存放在硬盘上的虚拟内存里读取出来,加载到物理内存里。这种方式,使得我们可以运行那些远大于我们实际物理内存的程序。同时,这样一来,任何程序都不需要一次性加载完所有指令和数据,只需要加载当前需要用到就行了。

4.虚拟内存和物理地址转化

  1. 通过映射表。

这个映射表,能够实现虚拟内存里面的页,到物理内存里面的页的一一映射。这个映射表,在计算机里面,就叫作页表(Page Table)。

页表这个地址转换的办法,会把一个内存地址分成页号(Directory)和偏移量(Offset)两个部分。这么说太理论了,我以一个 32 位的内存地址为例,帮你理解这个概念。

其实,前面的高位,就是内存地址的页号。后面的低位,就是内存地址里面的偏移量。做地址转换的页表,只需要保留虚拟内存地址的页号和物理内存地址的页号之间的映射关系就可以了。同一个页里面的内存,在物理层面是连续的。以一个页的大小是 4K 字节(4KB)为例,我们需要 20 位的高位,12 位的低位。

image

总结一下,对于一个内存地址转换,其实就是这样三个步骤:

  • 把虚拟内存地址,切分成页号和偏移量的组合;
  • 从页表里面,查询出虚拟页号,对应的物理页号;
  • 直接拿物理页号,加上前面的偏移量,就得到了物理内存地址。

缺点:对于一个32位的机器来说,如果每个内存块大小为4KB,每个页号用32位来表示,那么每个应用需要的页表空间大小为:

2^32 /2^10 /2^2 * 2^2 =  2^22  = 4M

如果每个应用都维护自己的一个页表,会导致空间比较大。

  1. 多级页表(主流选择)

仔细想一想,我们其实没有必要存下这 2^20 个物理页表啊。大部分进程所占用的内存是有限的,需要的页也自然是很有限的。我们只需要去存那些用到的页之间的映射关系就好了。

我们以一个 4 级的多级页表为例,来看一下。同样一个虚拟内存地址,偏移量的部分和上面简单页表一样不变,但是原先的页号部分,我们把它拆成四段,从高到低,分成 4 级到 1 级这样 4 个页表索引。

对应的,一个进程会有一个 4 级页表。我们先通过 4 级页表索引,找到 4 级页表里面对应的条目(Entry)。这个条目里存放的是一张 3 级页表所在的位置。4 级页面里面的每一个条目,都对应着一张 3 级页表,所以我们可能有多张 3 级页表。

找到对应这张 3 级页表之后,我们用 3 级索引去找到对应的 3 级索引的条目。3 级索引的条目再会指向一个 2 级页表。同样的,2 级页表里我们可以用 2 级索引指向一个 1 级页表。而最后一层的 1 级页表里面的条目,对应的数据内容就是物理页号了。

在拿到了物理页号之后,我们同样可以用“页号 + 偏移量”的方式,来获取最终的物理内存地址。我们可能有很多张 1 级页表、2 级页表,乃至 3 级页表。但是,因为实际的虚拟内存空间通常是连续的,我们很可能只需要很少的 2 级页表,甚至只需要 1 张 3 级页表就够了。

事实上,多级页表就像一个多叉树的数据结构,所以我们常常称它为页表树(Page Table Tree)。因为虚拟内存地址分布的连续性,树的第一层节点的指针,很多就是空的,也就不需要有对应的子树了。所谓不需要子树,其实就是不需要对应的 2 级、3 级的页表。找到最终的物理页号,就好像通过一个特定的访问路径,走到树最底层的叶子节点。

image

缺点:原本一次访问内存变为多次访问内存
优点:占用空间小

为什么不适用hash来存储呢:

  • 容易hash冲突
  • 大部分的空间访问都是连续的,使用hash 后需要每次都计算地址。
  1. 加速地址转换

由于采用了多集页表的形式,每次地址都需要进行转换,比价麻烦,通过采用缓存形式来存储映射关系。

计算机工程师们专门在 CPU 里放了一块缓存芯片。这块缓存芯片我们称之为 TLB,全称是地址变换高速缓冲(Translation-Lookaside Buffer)。这块缓存存放了之前已经进行过地址转换的查询结果。这样,当同样的虚拟地址需要进行地址转换的时候,我们可以直接在 TLB 里面查询结果,而不需要多次访问内存来完成一次转换。

5. 总线

计算机各个设备之间并不能只能进行通信,而是通过总线形式来进行通信的。降低了系统之间的复杂度,原本需要N*N的复杂的,现在只需要N的复杂度。
image
https://static001.geekbang.org/resource/image/af/58/afdf06aeb84a92a9dfe5e9d2299e6958.jpeg
总线,其实就是一组线路。我们的 CPU、内存以及输入和输出设备,都是通过这组线路,进行相互间通信的。总线的英文叫作 Bus,就是一辆公交车。这个名字很好地描述了总线的含义。我们的“公交车”的各个站点,就是各个接入设备。要想向一个设备传输数据,我们只要把数据放上公交车,在对应的车站下车就可以了。类似于事件驱动模型。

6. cpu 是如何访问IO设备的

和访问我们的主内存一样,使用“内存地址”。为了让已经足够复杂的 CPU 尽可能简单,计算机会把 I/O 设备的各个寄存器,以及 I/O 设备内部的内存地址,都映射到主内存地址空间里来。主内存的地址空间里,会给不同的 I/O 设备预留一段一段的内存地址。CPU 想要和这些 I/O 设备通信的时候呢,就往这些地址发送数据。这些地址信息,就是通过上一讲的地址线来发送的,而对应的数据信息呢,自然就是通过数据线来发送的了。

而我们的 I/O 设备呢,就会监控地址线,并且在 CPU 往自己地址发送数据的时候,把对应的数据线里面传输过来的数据,接入到对应的设备里面的寄存器和内存里面来。CPU 无论是向 I/O 设备发送命令、查询状态还是传输数据,都可以通过这样的方式。这种方式呢,叫作内存映射IO(Memory-Mapped I/O,简称 MMIO)。

9. 磁盘
1.机械磁盘

磁盘分为俩种,一种是HDD(机械硬盘),使用的是SATA 接口,一种是固态硬盘,使用的是SATA 接口和PCI Express接口。SATA 接口速率大约在6Gb/s,相当于 768M/s
,而SSD 速率可以达到2GB/S。

从速率上来说,SSD 一般是HDD的十倍,从响应时间来说,SSD是HDD的几十倍到几百倍。

对于硬盘的指标,我们一般看的是随机读,上面的速率是顺序读。

通过AS SSD 检测磁盘的时候,有一个指标为seq ,就是顺序的速率;

4K 指的是随机读4K的数据,看一秒内可以读取多少数据。

IO_WAIT指的是CPU等待IO操作。我们使用Top 命令查看Linux负载的时候,有一个wa 指标就代表io_wait

机械磁盘为什么随机读比较慢:

image

机械磁盘寻找数据分为俩步:

  • 寻道,也就是将磁臂移动到对应的磁道上,磁道即图中不同颜色区域,每个区域类似于甜甜圈,每一圈都为不同的磁道。目前磁盘的平均寻道时间一般在3-15ms
  • 旋转延迟 (Trotation) 是指盘片旋转将请求数据所在扇区移至读写磁头下方所需要的时间。旋转延迟取决于磁盘转速,通常使用磁盘旋转一周所需时间的1/2表示。比如,7200 rpm的磁盘平均旋转延迟大约为60*1000/7200/2 = 4.17ms,而转速为15000 rpm的磁盘其平均旋转延迟为2ms
  • 对于7200 转的磁盘来说,旋转延迟为4ms,寻到时间为4-10ms。最大IOPS为:
E = 1s/(4+4)ms = 125

最小为:

E = 1s/(4+10)ms = 70 
2. 固态磁盘

SSD 通过电容的形式来存储数据。与HDD差别如下:
image

对于 SSD 硬盘来说,数据的写入叫作 Program。写入不能像机械硬盘一样,通过覆写(Overwrite)来进行的,而是要先去擦除(Erase),然后再写入。

SSD 的读取和写入的基本单位,不是一个比特(bit)或者一个字节(byte),而是一个页(Page)。SSD 的擦除单位就更夸张了,我们不仅不能按照比特或者字节来擦除,连按照页来擦除都不行,我们必须按照块来擦除。而且,你必须记住的一点是,SSD 的使用寿命,其实是每一个块(Block)的擦除的次数。你可以把 SSD 硬盘的一个平面看成是一张白纸。我们在上面写入数据,就好像用铅笔在白纸上写字。如果想要把已经写过字的地方写入新的数据,我们先要用橡皮把已经写好的字擦掉。但是,如果频繁擦同一个地方,那这个地方就会破掉,之后就没有办法再写字了。

SLC 的芯片,可以擦除的次数大概在 10 万次,MLC 就在 1 万次左右,而 TLC 和 QLC 就只在几千次了。这也是为什么,你去购买 SSD 硬盘,会看到同样的容量的价格差别很大,因为它们的芯片颗粒和寿命完全不一样。

当SSD使用时间过长,会导致产生大量的数据空洞,为了解决这个问题,需要进行磁盘碎片整理。而为了不得罪消费者,SSD生产商会预留部分空间,导致磁盘写不满的问题。像一块240G的SSD,实际空间大小为256G,其中的16G是预留空间。

综上所述,SSD适用于读多写少的场景。

那么SSD是如何解决坏快的出现呢:

其实我们要的就是想一个办法,让 SSD 硬盘各个块的擦除次数,均匀分摊到各个块上。这个策略呢,就叫作磨损均衡(Wear-Leveling)。实现这个技术的核心办法,和我们前面讲过的虚拟内存一样,就是添加一个间接层。这个间接层,就是我们上一讲给你卖的那个关子,就是 FTL 这个闪存转换层。

为什么删除的文件可以被找回来:

这是因为当我们删除后,只是操作系统的操作,而不是硬盘的删除,只有对这个地址进行写入的时候,才知道该文件已经被删除了。

3. DMA

本质上,DMA 技术就是我们在主板上放一块独立的芯片。在进行内存和 I/O 设备的数据传输的时候,我们不再通过 CPU 来控制数据传输,而直接通过 DMA 控制器(DMA Controller,简称 DMAC)。这块芯片,我们可以认为它其实就是一个协处理器(Co-Processor)

  • 0
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
《王道计算机组成原理》是计算机相关专业经典教材之一,详细介绍了计算机组成原理的相关知识。GoodNotes是一款非常优秀的电子笔记应用程序,可以帮助用户实现快速、便捷的笔记整理。结合这两者,可以利用GoodNotes来记录和整理学习《王道计算机组成原理》的内容。 首先,在GoodNotes创建一个新笔记本,主题可以是《王道计算机组成原理》或者其他与该教材相关的标题。然后,可以根据教材的章节内容,将每个章节都拆分为一个笔记页面,以便进行更好的整理和查找。 接下来,可以使用GoodNotes的手写功能,使用手写笔或手指来记录关键概念、重要公式和定义。可以选择使用不同颜色的笔来区分不同的内容,例如用红色笔标记重要的内容,用蓝色笔绘制图表与示意图,用绿色笔标记关键词等等。 除了手写内容外,GoodNotes还支持插入图片和PDF文件。如果王道计算机组成原理教材有重要的图表或示意图,可以通过GoodNotes将其插入到相应的笔记页面上,以便更好地理解和复习。 在学习过程,可以使用GoodNotes的标签功能,给重要的笔记页面添加适当的标签,方便以后的查找和回顾。同时,可以通过GoodNotes的目录功能对整个笔记本进行分类和组织,按照教材的章节顺序或自己的学习进度进行整理。 此外,GoodNotes还提供了多种其他的功能,如文本输入、录音等,可以根据个人需要选择合适的功能来增强学习效果。 总结起来,使用GoodNotes作为《王道计算机组成原理》的笔记工具,可以有效地记录、整理和回顾教材内容,提高学习效果和效率。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值