北京大学 Computer Organization 学习笔记

492 阅读1小时+

计算机起初只是方便数学家进行算数运算。

冯诺依曼参与到了第一台通用计算机ENIAC计算机的研发中。 并从中提出自己心中计算机的物理结构预想。第一份草案

冯诺依曼体系结构

关于《EDVAC》的报告草案,其中内容是冯诺依曼和负责设计ENIAC的其他小组人员共同商讨的结果,针对ENIAC可以改进的点,不过冯诺依曼后来发表出来只署名了自己。并未完稿,101页。

论述了两个重要思想:

  1. 存储程序:控制计算及运行的程序应该存储在存储器中,而不是通过开关连线,会大大降低计算机的执行效率
  2. 计算机应该采用二进制而不是十进制,十进制会使计算机内部执行逻辑变得异常复杂。

明确了计算机的五个组成部分:运算器,控制器,存储器,输入设备,输出设备。

71F743F0-9426-4A29-B1C0-4257F4A81D60.png

4者构成芯片组

  • 4001: 断电数据不会丢失
  • 4002:可以方便读写,断电数据会丢失
  • 4003:用来进行数据的输入输出

CPU和主存储器通过系统总线进行连接。

036C8138-C280-42E3-BCE6-7ED4FA24C5CD.png

现代主存一般一个内存内容是一个字节。

5E4C5D00-6099-4BD8-913E-4F3FACFB7C85.png

冯诺依曼结构的计算机就好比这个餐馆, CPU就是厨房, 而主存就是仓库, 仓库里的货架就好比主存当中的存储单元, 我们为货架的每一格都编上了一个序号,相当于主存当中的地址,货架上存放的物品相当于存储单元当中的内容,主存中会存放着计算机的指令,还有数据,对于这个餐馆来说就是厨师需要执行的任务和做菜所用的原料,那厨房当中承担主要控制任务的就是这位大厨了, 它也是控制器,而运算器则是厨具。那我们还需要一些附加的设备。这个厨师记性很不好,他必须要在身边 有一张纸,上面写着下一张任务单的位置,这里这张纸上写了1,厨师就知道我下一张任务单放在仓库的第一格, 那么如果把任务单取回来以后还需要放在身边随时的查看所以他还可以有一个放置当天任务单的位置。如果取来了任务单就放在这。 那厨师做菜,做完了菜放哪呢,不能直接端着锅冲到仓库去吧。所以他身边操作台上总得放几个盘子,相当于CPU当中我们就称这些盘子为通用寄存器。用于临时存放计算器的运算结果或者要送到运算器的操作数。我们有可能从主存当中事先会取来一些操作数,当然也需要执行指令去取来一些操作数,先放到通用寄存器当中。

计算机执行一条指令的主要步骤包括如下四步

  1. 取址
  2. 译码
  3. 执行
  4. 回写

现在我们假设这个餐馆处在这样一个状态,厨师第一步要做的事情就是查看下一张任务单的位置在哪,我要去取这张任务单了,我一查,发现是1, 我向主存发送了请求我需要第一格的物品,他并不知道第一格存放的是什么,他只是根据这里的编号向主存 发出申请,哎,主存并不简单是一些货架,他还是要有一些控制逻辑,那么这类控制逻辑就会响应控制器的请求, 需要第一格的物品,他就找到了第一格的物品,把第一格的物品给送回去了,厨师收到了任务单了,就把他放在身边存放当前任务单的位置, 这就完成了第一步,取任务单的工作。而实际上这一步呢还需要有一项任务。就是更新下一张任务单的位置。第一格取完了等这件事做完之后下一次再去取任务单的时候再去取下一格,就把它更新为2,那现在不会取第二格了,只是先更新, 等一会去执行下一条指令的时候再根据这个位置去向主存发出请求,好,那这样的话取任务单的这一步就算真正的完成了。

第二步,在计算机当中称之为译码,对于这个厨师来说,就是分析刚才拿到的任务单。这个任务单上写了这么几条。第一呢说明了这项任务所需要用的方法。第二呢说明了这项任务所需要的原料。看来这个原料有两个位置,一个在仓库,一个在他身边的盘子里。最后这项任务完成了以后,看起来是炒一盘菜,那么炒完之后呢还得把成果存放在某个地方,这个任务单上也指定了,存放在A号的盘子里。好,这个任务就分析完了。控制器就把他转换成若干组的控制信号一一完成,这样译码这个阶段就算完成了。

第三步,对计算机执行指令是其核心的步骤,就叫做执行。对程序来说一样,执行这个已经分析完的任务,这个任务一共有4步,你先完成第一步,第一步是要去取第六格的物品。好,与刚才一样,向主存发出请求说我需要第六格的物品,主存会响应,把第六格的物品送过来。由于这次是取运算要用的操作数,所以返回的物品控制器就会把它放在运算器的其中一个入口,然后第二部是取这个运算的第二个操作数。其实这是放在A号盘子里的,就在厨师身边,很快,马上就可以拿到。那么第三步是执行这项运算,那么控制器会给出对应的信号给运算器,让它执行对应的运算,经过短暂的时间之后呢运算就完成了,运算结果也产生了。 执行阶段就到此结束了。

但是现在运算结果还放在运算器的这个输出端口上。我们必须要把它转移到另外的地方,根据这条指令的要求,运算的结果应该是存放在A号的通用计算器里。那我们在最后一步回写也就是保存结果的时候呢,用控制器把这个运算结果存放在A号的通用器当中, 这样我们就完成了执行指令的全部过程。做完了这条指令,控制器也不会闲着,接着来执行下一条指令。它就会去查看下一张任务单的位置,跟刚才执行前一条指令的第一步是一样的,先去查看下一张任务单的位置,发现是2,那么就向主存发出所需要第二格的物品,然后主存就会把第二格的任务单返回来,注意我们取回来以后呢要自动更新成下一张任务单的位置,之后再做13,这个更新完以后我们就像刚才一样,继续分析当前取回来这张任务单并完成它所指定的任务,最后保存结果, 然后再去取下一条指令。不断的重复,计算机就会自动的运转起来了。

FA102A9F-ED1D-4388-841B-510612544AC7.png

CPU和存储器通过系统总线相连,系统总线分为

  • 控制总线
  • 地址总线
  • 数据总线

656A9D07-78EB-49AC-9F6F-C2894A4634D1.png

49E0AFCC-BD23-42B8-9ED7-989DF686B86F.png

8C1DA4DC-37C0-4FED-8383-B8D5A0E2E740.png

EE76D817-29F6-415F-88FF-D7482ED06122.png

26C9E198-D384-470B-87DD-BEFEE142E066.png

Tips: 在通用寄存器和ALU以及控制器当中的若干寄存器之间要传递数据的话, 就必须要经过CPU的内部总线

8BD052E2-D1B1-4BDE-B964-48E3DF48E6D0.png

742DE42A-FB22-4D32-B194-555AC2FB2FCB.png

1246735C-CD21-4049-8D9D-04E875AEE1BC.png

假设模型机的当前状态已经有了一些初始值,我们需要注意的是CPU中的R0所保存的值是00000011,这等于10进制的3。 而存储单元地址为6的存储单元的内容是00000010,相当于10进制的2。那么我们所要做的,就是把这个存储单元的数和R0当中的数相加,最后再放到R0中。我们还看到PC寄存器也就是指向下一条指令地址的寄存器,保存的值是0001,那么,他所指向的内层单元,这里,所以这个内层单元中其实是保存了当前所需要执行的那条指令。 我就用10101010这么一个值来指代, 这仅仅是指代而已。

指令执行的第一步,取指。控制器将指令的地址送往存储器,存储器按给定的地址读出指令内容,送回控制器。具体的操作步骤:

  1. 首先请注意控制器会发出控制信号将PC寄存器中的内容通过内部总线传送到MAR中。现在MAR寄存器中也保存了0001。
  2. MAR将会把这个地址送到地址总线上,与此同时,控制电路会在控制总线上发出相应的控制信号,代表这次访问存储器的操作是要读数据。
  3. 存储器的MAR寄存器就会收到地址总线上传送来的地址并把它保存下来。存储器中的控制逻辑,也会受到控制总线中传送来的控制信号 得知这次仿真操作是一次读操作。

这样存储器通过地址译码器就可以查找到对应地址001的存储单元的内容;并将该存储单元的内容送到MDR寄存器中。然后存储器的控制逻辑,会通过控制总线向CPU反馈当前的传输已经准备好了,同时,MDR之中的内容也会送数据总线上。

随后,CPU中的控制电路检测到来自控制总线的准备好的ready信号,就知道当前数据总线上已经准备好了数据。 因此 MDR寄存器就会将当前数据总线上传送来的数值保存下来,这就获得了我们所要取的指令。当然,仅到如此是不够的。MDR中的内容还不需要传送到指令寄存器中,也就是IR寄存器。当指令的编码已经保存到IR寄存器中时,还需要再做一件事,就是把PC寄存器更新为下一条指令所需要访问的地址。 刚才是0001,现在变成0010,也就是10进制的2。更新完以后我们才可以做效益不的工作。那么,取指阶段到此正式完成。

第二部是译码。 在一步控制器会分析指令的操作性质并向相关的部件发出指令所需的控制信号。当前的指令计算器IR中指令编码会送到指令译码部件。指令译码部件根据指令编码很快会发现这是一条加法指令。而且,是要把R0和存储器中地址为6的单元的内容相加,并把结果存放到R0中。然後,控制电路就据此产生对应的控制信号,发到相关部件中将译码阶段就到此完成了。第三步是执行。控制器会从通用寄存器或者存储器中取出操作数,并命令运算器对操作数进行指令规定的运算。首先,根据这条指令我们会发现,还是需要去取操作数,其中一个操作数在存储器里中。因此,这一步会在MAR中放置要访问的存储器的地址,0110。就是10进制的6。那么,随后的过程类似于取指阶段的操作。MAR将地址发到地址总线, 同时,控制电路在控制总线发出读操作的控制信号。存储器的MAR控制逻辑会接收到相应的信号,然后查找到对应的地址,这次要查找的地址是0110,对应的内容会送到MDR这个存储器。然後,控制逻辑会向CPU反馈当前数据已经准备好了的信号。 然後,MDR的内容也会被放置到数据总线上,CPU会接收数据并保存在MDR寄存器中。

由于这个数据要进行加法运算,所以控制器会进一步将MDR中的数据传送到ALU的输入目前会暂存到Y寄存器中。这一个操作数现在就准备好了, 那么另一个操作数是放在R0中;因此,控制器还会将R0中的数据传送到ALU的另一个输入端,也就是x寄存器。

现在,两个操作数都准备好了,在控制电路的控制下ALU就会进行运算再将x、y中的内容执行加法并计算出结果。结果就是00000101也就是10进制的5。 到此,执行阶段就完成了。

第四步是回写。也就是将运算结果写入到通用寄存器或者储存器。现在,运算结果还在ALU的输出端,也就是Z寄存器中, 控制电路会给出相应的控制信号将Z寄存器当中的内容传送到R0中。R0目前的内容是之前的原操作数, 也就是00000011。随后,会被新的结果所覆盖。这样,我们这个加法运算的结果就已经保存到了R0寄存器中, 回写阶段到此完成。这条指令你就执行完毕了。然後,CPU就会自动执行下一条指令。它们是和刚才一样,先将PC中的内容传送到MAR寄存器当中,然後MAR会将这个地址传送到地址总线上依次的执行下一条指令。

6EC4C536-264E-4BEA-AD84-B11AA227FC02.png

AD12F6DD-ADC0-49BD-9323-A8A33571F039.png

那CPU是如何获取指令,开始执行的呢。 这块主板上采用的是个人计算机上长期占据主导地位的南北桥结构。 所以CPU对外会连接的一块芯片,叫做北桥。 这是在主板上非常明显的一块芯片,CPU想要访问主存, 就得通过北桥芯片。

但是我们还要注意,计算机刚启动的时候,主存里面是没有信息的,因为当计算机断电之后,主存的信息都会丢失。那在系统启动之后,CPU从哪里获得指令呢?我们也可以看到北桥下方,还有一块比较大的芯片。它就是南桥。我们已经知道南桥内部,集成了很多输入输出设备的控制器,其中就包括硬盘的控制器。 所以硬盘实际上是和南桥芯片相连的。 而硬盘,我们也已经知道了,它是属于外部记录介质, 它内部保存着计算机系统所需的程序和数据。 那CPU所需要的指令是否就直接从硬盘取得呢。 其实也不是。因为要从硬盘中访问数据,相对比较复杂。 对硬盘进行配置和访问,本身还需要程序来控制。 这也就是所谓的硬盘驱动程序。 所以在计算机刚启动时,CPU是无法从硬盘中, 获得自己想要的第一条指令的

那第一条指令在哪呢? 实际上在主板上,还会有一个很小的芯片,它会与南桥相连。一般我们称之为BIOS芯片,这个芯片存储容量很小,** 会保存的一段比较简单但十分重要的程序,包括检查主板上都有哪些设备,这些设备工作是否正常。这个芯片实际上是一颗只读存储器。当系统断电之后,只读存储器中保存的信息是不会丢失的。所以当计算器启动后,CPU可以依次通过北桥, 南桥,访问到BIOS芯片,从里面取得指令。** 所以这颗BIOS芯片,虽然是一颗只读存储器,但是从冯·诺依曼结构原理上来看,它并不属于存储器的范畴,而是和硬盘一样,属于外部记录介质, 它和早年计算机的穿孔纸带也属于同一类别。严格地说,BIOS是指这颗只读存储器当中存储的软件。 它是基本输入输出系统的缩写。 那通常我们把保存了BIOS这个软件的只读存储器 成为BIOS芯片。

现在计算机主板上的BIOS芯片,有很多种封装形式,位置也有不同。但大多会在芯片旁边的主板上,标出BIOS的字样CPU从BIOS中取出指令后,会依次地检查主板上的各个设备,包括有显卡,通过显卡点亮显示器,检查键盘的存在,之后我们就可以通过键盘进行输入。 这些都是与I/O设备。此外,现在的显卡功能越来越复杂,性能也越来越强大,尤其是其中的图形处理单元。 我们也常称为GPU。GPU往往支持很多复杂的数学运算,拥有很强的运算能力。所以有时候,也会让GPU承担一些运算的工作。 实际上在现在高性能的超级计算机中, 经常会采用大量的GPU来进行运算。所以从这个意义上讲, GPU也承担了运算器和控制器的一些功能。

1AFFACB5-4510-47A5-8941-1741D8E72B6B.png

那在这个结构当中,只有北桥芯片的特征比较特殊一些。实际上,它提供了各个芯片之间互相访问的通道。当然,我们也不用太纠结,北桥到底对应着哪个结构。因为整个南北桥的结构也在迅速地发生着变化。这还是刚才那块主板,最上方是CPU。 这颗芯片中,往往包含着多个CPU核。 往下,它连接着北桥。 北桥中,主要会有主存控制器,对外连接着主存,那 现在的显卡,一般采用PCIE接口,所以在北桥中,会集成PCIE控制器,对外通过PCIE的插槽,连接PCIE的显卡。当然如果你对显示的性能要求不高, 也可以通过北桥内部自带的集成显卡,直接连接显示器。那北桥还连接着南桥,南桥中,集中了大多数 I/O设备的控制器,如右图。CPU对外的访问都需要通过北桥, 而其中对传输的性能要求最高的就是与主存的这条 通道。主存通道的性能也成为了整个计算机性能的关键瓶颈。 除此之外,另一条对性能影响很大的通道就是显示通道。那么这就是PCIE的插槽,这上面可以插上PCIE显卡。再有一条重要的通道, 就是北桥与南桥的连接,通过这条通道,外部的输入输出设备可以对主存进行访问。那在这里,红颜色的箭头表示传输压力很大的通道,而南北桥之间的绿色箭头表示这条通道传输压力相对较小。那在这种情况下,首先要解决对性能影响最大的关键问题。 这就是CPU访问主存的通道。

142CC084-482A-4762-BC1C-A47D1196DF19.png

所以在南北桥架构上,发生的第一个重要的变化,就是北桥中的主存控制器被移到了CPU芯片当中。通过上图可以看出来, CPU的插槽与主存的插槽变得更近了,这就是因为现在CPU是直接访问主存,而不需要再通过北桥了。这样可以大大地提高数据的传输率,进而提升系统的性能。现在,显示的这条通道,就成为了下一个要解决的问题。而南桥中连接的这些设备,暂时还不关键。 因此,在最新的个人计算机的CPU设计中,不仅包括了主存控制器,还包含了PCIE控制器,在外部可以直接连接显卡。 这样北桥当中最重要的一些部件,都被集成到了CPU芯片当中,那北桥就没有存在的必要了。

于是将北桥中剩余的一些功能,和南桥芯片整合到一起之后,所谓南北桥的架构,其实已经消失了。整个个人计算机的主板由三片式的架构缩减成了两片式。由于没有了北桥,南桥这个名字也显得有些奇怪了。所以它现在往往有一些不同的名字。如上图现在CPU直接连接着主存,还直接连接着显卡,还与原先的南桥直接相连,这样就将原先通过主板的复杂的通路,大多移到了芯片的内部。 而芯片内部的数据传输率,是远远高于主板的。 这样就大大地提升了系统的性能。 那既然从三片缩减成了两片,我们可不可以进一步缩减呢? 其实已经有人这么做了,这就是系统芯片的概念。 系统芯片, 可以认为是将计算机系统,或其它电子系统集成到一个单一的芯片上。如现在的消费电子设备就是一块大的系统芯片

C6369D39-9662-4C98-8659-F79C6216887C.png

这是一颗硬币大小的芯片,这个框图就是这颗芯片中所包含的功能模块。这部分是一个四核的CPU。这一部分是显卡的功能模块, 而这些是南桥中数量繁多的 各种接口的控制器,而且这个芯片还采用了一些特殊的方法,把主存也集中在了一起。 这样,就在这一颗芯片内部,集中了iii结构当中的所有组件。所以我们可以用这样的芯片制造出非常小巧的计算机。你看这个计算机的主板大约只有手掌大小,却拥有非常丰富的外部接口,与一个普通的计算机并没有什么差别。那这样高集成度的优势就让系统芯片的技术 广泛应用于智能手机,平板电脑等移动计算设备上

指令系统体系结构

x86 复杂指令集 CISC

84E91E5D-42CD-4F5B-BABE-B6C1B2A2B276.png

F844668D-0489-4CF5-8CC3-AEA21FE203D9.png

A115301F-A26E-492C-B23B-25FDE4FC7742.png

740522B8-63B1-4D1D-A688-6C23B5C96A67.png

为什么PC寄存器当中的地址是这个呢? 其实这是不一定的,这也是我们进行指令系统体系结构设计时。必须要约定的一个内容。 就是CPU在启动时,或者说在复位完成之后,第一条指令从哪里开始取出。 这也是最开始,软硬件双方必须商量好的事情。 至于这个地址到底应该是什么,并没有明确的规则。 但通常情况下,我们会约定为这个体系结构所能访问的存储单元 的最小地址,也就是0,或者是接近最高地址的地方。

E693577B-3210-4653-8821-4D5772CA27E0.png

23F455CE-F6E8-4904-8A41-2310C9FE38B1.png

最早推出来的8086是一款16位的CPU,所谓16位CPU,主要是指CPU当中的运算部件可以支持16位数据的运算。因为运算当中所需要的数据,一般会放在通用寄存器中。所以通用寄存器的位宽通常和运算单元的位宽是相同的。而运算单元产生的数据又经常会用做访问存储器的地址。 所以CPU访问存储器地址的宽度,也常常和运算单元的位宽相同。

对于8086来说,它是一个16位的CPU,它内部的通用寄存器也是16位的。 但是它连接存储器的地址线的宽度却是20位的。 那么它生成访问存储器的地址就需要采用一些特殊的方式。这就是8086体系结构所规定的寄存器。都是16位宽的,主要可分为这几类:通用寄存器、 指令指针寄存器、标志寄存器,还有段寄存器。 我们首先来看通用寄存器,结合我们之前说过的模型机的例子, 通用寄存器就在这里,CPU从存储器当中取回一个数, 很可能就会放在某一个通用寄存器当中。 而CPU执行运算指令,其操作数的来源也往往会在寄存器中。 对于8086来说,这些用于存储数据的通用寄存器, 主要有这四个:AX、BX、CX和DX。 这四个寄存器都是16位寄存器。 但是这些16位寄存器还可以被分为两个8位的寄存器来使用。

574C5BFF-B978-4753-BB27-2395208B024B.png

大多数的算术运算和逻辑运算的指令,都可以使用这些数据寄存器。 那么这些寄存器除了可以一般性的存放数据之外, 还会有一些专门的用途。除了这四个寄存器还有SP、BP、SI、DI这四个通用寄存器。 它们在早期都有一些特殊的用途。 而随着X86体系结构的不断更新,它们也大多成为了可以用于保存普通数据的寄存器。

86205DCC-E08A-4393-AA97-9A1AB3366D15.png

标志寄存器:之前分析模型机时提到,当执行运算指令时,ALU会将X和Y两个寄存器当中的内容相加,并将运算结果放在Z这个寄存器中。 同时将运算结果的一些特性保持在标志寄存器中。例如这个加法运算如果产生了进位,那就可以将这个进位保持在标志寄存器中, 以免丢失,后续的运算也可以知道之前的运算产生了这样一个进位。

6C72404D-1BA1-4889-B7A4-CCC4D6F74C4A.png

8086中,也有这样一个标志寄存器。称为FLAGS,这个寄存器当中,包含了若干个标志位。主要可以分为两大类,一类称为状态标志,它反映的是CPU的工作状态。另一大类称为控制标志。这是对CPU的运行起到特定的控制的作用。那8086的这个标志寄存器也是16位的。但实际只有其中一部分有具体的含义。在图中标为红色的都是状态标志,标为紫色的这三个是控制标志。 例如我们刚才提到的加法的进位标志。 当CPU执行完一条加法指令, 而这次加法运算的结果产生了一个进位, 那CPU内部除了将加法的运算结果保存到对应的寄存器之外, 还同时会将这个CF标志置为1, 这个动作是由硬件自动完成的,不需要由编程人员来设置。

B6B1718C-E212-44BA-AC27-D314665AE608.png

在模型机上,CPU要去取下一条指令之前,都会先从PC寄存器当中,取出下一条指令的地址,将这个地址发到存储器中,才能取回下一条指令的编码。 那在8086当中,这个寄存器称为IP寄存器。IP是指令指针的缩写。编程人员是不能直接修改IP寄存器的, 除了顺序取出指令,IP寄存器会自动增加以外,如果遇到了转移指令,这些会改变程序流向的指令,那CPU会自动修改IP寄存器的内容。 这里我们还需要注意一个问题,因为IP寄存器是16位宽的,所以它能够指向的内存单元的数量是2的16次方。 也就是64K个字节单元,那么即使在那个时代,64K的内存也是太小了, 无法满足当时大多数程序的需求。 因此,实际上8086在外部连接的是一个1兆字节的内存。这样就需要8086对外有20位的地址线。那多出来的这4位地址线,从哪里来呢? 那8086采用的是一个很巧妙,也很繁琐的解决方案。这就是用段寄存器的方式,段寄存器是用来和其它寄存器一起联合生成存储器地址的, 8086当中有4个段寄存器。 CS是代码段寄存器,DS是数据段寄存器。ES是附加段寄存器,SS是堆栈段寄存器。

D9DF70D8-DC11-4870-88DF-73B6E1EE3544.png

2A1BBE35-5546-4CF3-9871-46E4ED94B4BA.png

以代码段寄存器为例,来看一看地址生成的方式。假设8086CPU要从这个1M的内存中取出一条指令,那就需要现在段寄存器当中保存这个地址的一部分。然后地址的另一部分根据这个程序的本身来产生。这样的组合,就称为逻辑地址。 我们假设已经在这个代码段寄存器当中存放了一个16位数,那用16进制来表示,就是2000H,而根据程序运行的状况,当前IP寄存器当中的值是3000H, 那下一条指令的地址是怎么产生的呢? 那在CPU内部就会有一个硬件单元,负责移位, 先将段寄存器当中的16位数向左移4位。 那新产生的这个数用16进制来表示就是20000H, 然后再将这个移位后的数与IP寄存器当中的内容相加, 这又需要用到一个加法器。 相加之后就得到一个20位的地址, 在这里就是23000H,这时CPU才 可以将这个地址发送到存储器去,从而取回下一条指令的编码。 而这个地址则被称为物理地址,从逻辑地址到物理地址,就是用段寄存器当中的内容乘以16再加上程序中产生的偏移地址。

9B494A1D-40C7-4F2A-9DDD-61FD28D24E76.png

假设这时CPU已经把下一条指令的编码取回了,放在IR寄存器当中,那这条指令是要将3000H所指向的内存地址当中的数取出来,放在AX寄存器当中。 那如果是在我们之前讲过的模型机上运行,CPU就会将3000H这个数,放到MIR寄存器当中去,然后再传送到地址总线上。 但是对于8086来说,它要发出的是一个20位的地址,必须先要用段加偏移的方式进行计算。那我们假设之前已经在DS寄存器当中保存了2000H这个数,那CPU的硬件就会将DS当中的数取出来。送到一个移位的部件,向左移4位。 然后再和3000H相加。 这样就得到了一个20位的地址,23000H,然后才能把这个生成的地址放在MIR寄存器当中,再传送到地址总线上。 然后存储器则会返回23000H这个地址所对应的内容,并放到数据总线上,进一步保存到了MDR寄存器中,最后CPU的硬件 会将MDR当中的内容,再传送到AX计算器当中,从而完成了这条指令所执行的操作。

那么结合上一页我们所介绍的内容,我们会发现,对于8086来说,它在取指令的时候,就要执行一次段加偏移的这样的计算。 那么在执行指令的时候,还要执行这样一次计算,那它执行一条指令的过程就要复杂得多了。 当然,虽然很繁琐, 但在那个时期,确实在一定程度上解决了16位地址空间太小的问题。但是想要提供更高的性能,以满足当时蓬勃发展的个人计算机的需要, 还是要从体系结构上做大的改进。

而1985年推出的80386就是这样一款跨时代的作品。80386是x86系列当中第一款32位的微处理器,也就是说它的运算部件可以支持32位数据的运算, 同时也提供32位的通用寄存器,那么自然它也可以产生32位的地址,从而可以指向2的32次方,也就是4G字节的内存空间。 这样大容量的内存空间在之后相当长的时间里,都让编程几乎不受内存空间的限制。而英特尔也凭借80386确立了它在个人计算机CPU领域的优势地位。此外,80386还对运行模式进行了改进,以便更好和更稳定地支持操作系统,以及越来越丰富的软件。32位x86的体系结构也被称为IA-32,它所提供的32位寄存器是在8086的16位寄存器的基础上扩展而来的。 例如8086中的AX寄存器在为它增加了16位之后就变成了32位的EAX寄存器。在指令中如果使用EAX,就是指这个32位的寄存器,但与此同时,指令中还可以继续使用AX来指定其中的低16位。同样,也可以继续使用AH和AL这两个8位的寄存器编号, 那这样IA-32中就有了8个32位的通用寄存器,还有一个32位的标志寄存器。指令指针寄存器也扩展到了32位, 用这个寄存器就可以指向2的32次方,也就是4G字节的内存空间。从这里看来,386只要使用这个EIP寄存器就足够了, 但实际上386不但保留了原先的4个段寄存器,还增加了2个段寄存器。而运行在保护模式下,这些段寄存器的使用方法是不一样的

3DDCFD1D-09B1-4DB6-A9AB-B5F7C0239580.png

CB354163-60E4-4CDA-993C-56A28876CF9E.png

那到了上世纪90年代后期,即使在个人计算机领域,32位CPU也逐渐出现了难以满足性能需求的情况,尤其是4G内存的空间限制了大规模程序的应用, 那在这时,一贯主导x86体系结构改进的英特尔,它提出了名为IA-64的体系结构。这个64位的体系结构和之前的x86体系结构并不兼容,那由于种种原因,这个新的结构并未获得成功。那趁着这个机会,AMD后来居上, 提出了与原先兼容的64位的x86的方案, 从而在64位的时代占据了先机, 当然后来英特尔也转回来支持这个兼容的方案。 那这个方案有很多不同的名字, 比如说AMD64,Intel64, 通常我们更多地把它称为x86-64。 那x86-64的寄存器模型 则是在IA-32的32位寄存器模型的基础上进行了扩展。那与之前类似,在原先32位的EAX寄存器的基础上再增加32位,形成了64位的RAX寄存器。 而指令指针寄存器也被扩展到了64位,因此理论上我们就可以访问2的64次方个字节这么大的内存空间。 此外,因为把常用的操作数放在寄存器当中比放在存储器当中性能要好得多。因此有更多的寄存器,编程就会更加地方便。 那么在x86-64当中,另外还新增了8个64位的通用寄存器, 这8个新增的寄存器的名称依次为R8,R9,一直到R15。 因为之前我们就已经有了8个通用寄存器, 如果要给它们编号的话,就正好是从R0到R7, 所以新增的寄存器就从R8开始编号, 这就是x86体系结构从16位直到64位的大致情况。

7AB9CEA6-0DF0-4707-AD4B-F85CC646AF5F.png

MIPS 精简指令集 RISC

RISC是精简指令系统计算机的简称,与之相对,之前的计算机上的指令系统就被称为复杂指令系统,X86就是其中的代表。现在MIPS处理器已经不再应用在计算机产品中了,但是在广义的计算设备包括数字电视,游戏机,网络设备等领域仍然有广泛的应用。

MIPS全称的含义是一个流水线不会互锁的微处理器,流水线是现代微处理器为提高性能而采用的一项技术,而流水线中的互锁则是导致流水线性能降低的一个非常重要的因素。从这个名称也可以看出,MIPS的指导思想是希望其指令的设计能让微处理器运行的更快,性能更好。 所以它主要的关注点是减少指令的类型,并且降低指令的复杂度,所以在MIPS指令系统当中, 指令的总数是很少的。 而且每条指令都比较简单

它的主要目的就是希望可以用一个非常简单的CPU来支持这样的指令系统。而CPU越简单 就可以运行的更快。假设要编写程序完成同样的任务,用MIPS指令编写,其指令数量是X-86指令的5倍, 但是如果MIPS的CPU能够做到比X86 CPU快10倍,那它仍然获得了明显的性能优势。

这就是MIPS,同时也是RISC的设计思想。 那MIPS的指令是怎么体现它这样的设计思想的呢?

  1. MIPS固定了指令的长度,都是32个比特,也就说MIPS中的一个WORD,我们要注意这和X86中一个WORD是16位是不同的。固定的指令长度,大大简化了CPU从存储器中取指令的工作。 不用像X86CPU那样需要判断每条指令的长度。
  2. MIPS采用了非常简单的寻址模式,相比于X86提供的复杂多样的寻址模式,虽然给编程带来了不变,但是大大简化了CPU访问存储器的控制逻辑。
  3. MIPS指令的数量比较少,每条指令的工作也很简单,基本上一条指令只完成一个操作,不像X86的指令,一条指令往往完成丰富的功能, 这样可以简化指令的执行过程。不但简化了CPU的控制逻辑,而且可以方便的实现各种让指令并行执行的技术,从而提高CPU的性能
  4. 在MIPS指令系统中只允许LOAD和STALL这两种指令访问存储器,而不支持X86指令中这些让算术指令访问存储器的操作。 因为访存是一个相对复杂的工作, 这种限制就可以让运算指令的实现变得非常的简单,但是我们要注意,MIPS 的这些特点让直接使用MIPS指令进行编程变得非常的困难,因此,想要有高效率的MIPS程序,必须要有优秀的编译器的支持。

MIPS指令的基本格式就分为这三种,R型,I型和J型。R型指的是寄存器型, I型指的是立即数型,J型指的是转移型

BC6ABB58-7877-4DFD-9CAB-0326E423D07F.png

93E57DDF-4C77-4DE2-9581-CA709926F3B1.png

78786D62-F069-44B8-9D12-D814E851F16B.png

327867B9-9A68-42E4-B2E7-2151EF3EB8F7.png

C20E5C96-B7CA-4825-8E91-DE5588094BAF.png

算术逻辑单元

8E569D05-5D79-4A40-B9D8-EE764BB28722.png

现代集成电路中通常使用MOS晶体管。

29033145-724B-4C6F-956D-06334EAF8F2B.png

在CPU当中用来存放信息的非常重要的部件就是通用寄存器, 比如说0号通用寄存器,在mips的体系结构中,它就是一个32位的寄存器, 从电路实现上来说这32个比特都是同样的。它就可以用这样一个结构来实现。 这个结构就是我们将要介绍的D触发器。

触发器是具有存储信息能力的基本单元, 它有很多种类型,d触发器是其中一种。 仅就D触发器而言,也有很多的实现方式

8C5430A6-579C-4285-8AA1-4C774F7C5A6B.png

这个D触发器主要有这三个接孔,一个数据输入, 一个数据输出和一个时钟输入。 它的功能表现是这样的,在时钟clock的上升沿,也就是clock从低电平变为高电平, 也可以说是从0变到1的时候,这是一个很短的时间, 在这时D触发器会采样输入端口D的值, 经过一段很短的时间会将这个值传送到输出端口Q, 在其它的时候也就是不在时钟信号的上升沿的时候, 无论输入端口D如何变化,其输出端口Q的值都是保持不变的。

DD188DB9-3122-45C9-ACA0-029EAFD8E807.png

第一行是时钟信号,它是有规律的进行变化,两个上升沿之间的间隔时间就称为时钟周期,输入信号D则可能在任何时候发生变化, 比如在图示这个时候它由0变到了1,但是因为时钟上升沿没有到来,所以输出端Q并没有发生变化,直到时钟上升沿到来的时候,D触发器会采样输入端的信号, 并经过很短的clock to Q的时间在输出端体现出来,因为这时候输入端是1,所以输出端也变成了1,然后时间再继续,在这个时钟周期内,输入D又发生了变化,由1变成了0, 但是同样输出端Q没有发生变化,直到下一个时钟上升沿到来,采样到了新的输入端的指令,再经过Clock to Q输出端也变成了0这就是一个D触发器的基本工作行为。

那我们如果把很多个D触发器 组合起来,比如就是这32个D触发器,那就可以构成一个32位的寄存器,当然这只是一个很简单的原理性实现。 用这样一个32位的寄存器就可以做成CPU当中的一个通用寄存器,用同样的方法可以作出其它的通用寄存器以及PC,IR这样的寄存器,再将这些寄存器于其它的由逻辑门构成的电路相连, 就构成了我们这个复杂的CPU了

BC33CA6C-32AD-4215-8E00-3E68DBE37DA1.png

就好比我们平常使用的照相机,我们把镜头比作d触发器的输入端D,拍照的快门比作时钟端clock,这台相机内部带有无线传输的模块,可以将其拍摄的照片传送到一个显示器上,这个显示器就好比D触发器的输出端Q,那这样我们就把D触发器比作这个照相机和这个显示器。 当按下照相机的快门后,照相机会拍一张照片,过一秒钟后显示器上显示出这张照片,对于D触发器来说,这就叫做clock to Q的时间,也就是从时钟的上升沿到来开始直到数据出现在输出端为止,这个时间是属于D触发器自身的特性,那对于这个D触发器所在的整个芯片还有一个重要的特性就是时钟频率,在这里就好比我们约定每十秒钟会来按一次快门,那这个系统的时钟频率就是零点一赫兹。

需要注意的是就和现实中的照相一样,在按动快门的前后很短的时间内通过镜头看到的画面不应该发生变动,否则就可能造成拍出的照片是模糊的。 对于D触发器来说,在时钟上升沿前后很短的时间内,输入端的信号也不能发生变化,否则就可能造成无法正确的采样。 那么这也是D触发器的一个重要的特性,要求输入信号在时钟上升沿之前有一段很短的稳- 定时间, 很短的稳定时间,称为set up时间,在时钟上升沿之后也需要有一段很短的稳定时间,称为hold时间。

逻辑运算的实现

A1B399BB-885B-4DBA-8CBC-7E02669A44EA.png

在ALU当中,实际上是包含了多钟不同的功能部件。包括32位的与运算,32位的或运算,还会有其它的逻辑运算以及算术运算单元。 那它们是怎样合成一个整体的呢?对于这个整体的运算单元,它需要有一个32位的输入,然后在运算单元的内部, 分别连接到各个不同的运算功能部件的A输入端口,然后将另一个32位的输入也在运算单元的内部分别连接到各个不同功能单元的B输入端口。 这样每个功能部件都按照各自的功能完成对应的与操作,或操作,以及其他的操作,并产生对应的运算结果。 那现在的问题是,我们到底需要哪个运算结果作为输出呢?

这就还需要增加一个部件就是多选器。这里我们假定这个运算单元当中包含四种功能,所以我们会有四个运算的结果,要经过一个四选一的多选器。 那样从四个选择当中选出一个来,我们就需要一个两位的选择信号,当这个选择信号为00时, 在这个图中就会选择与运算功能部件输出的结果。

如果它是01时,就会选择这个或运算功能部件输出的结果。这个多选器实际上也是由若干个门组成的。当然如果这个运算单元中包含着更多数量功能单元,比如说8个,那么就要使用一个8选1的多选器, 而运算类型的选择信号也要扩大为3个bit。 那好经过这个多选器之后,我们就可以产生一个32位的输出。那对于这一个完整运算单元来说,当我们通过这个选择信号选择了对应的运算类型之后, 从运算单元的输出端口,我们就可以看到经过指定运算之后产生的输出

实际上是在控制电路的控制下 将9号和10号寄存器的内容分别连接到ALU的两个输入端,这里我们需要进行的与运算。如果按照上一页给出的例子, 这时候控制电路给出的选择信号应该是00。最后控制电路还会将ALU的输出与8号寄存器的输入相连, 这就相当于左边这张图所显示的电路的连接。最上面是由32个D触发器组成的8号寄存器,中间是9号寄存器,然后是10号寄存器。9号,10号寄存器的Q端的输出会被链接到ALU的两个输入,同时ALU的功能选择信号输入了与运算所对应的编码。 然后ALU的输出会被连接到8号寄存器的输入D端,所以在某一个时钟周期内,ALU会按照输入的要求完成相关的运算,并将运算结果送到输出信号上。 输入结果经过连线的传送, 会送到8号寄存器的输入端, 等到下一个时钟上升沿来临的时候, 8号寄存器就会将输入端的信号采样,存入到寄存器内部,并会在输出端表现出来。之后的运算如果使用8号寄存器作为输入的话,就会使用这个新的值。

E27F8CF9-4DF7-4CC0-B87A-4143CE8742BE.png

半加器缺陷:它不能将第一位产生的进位作为输入参与运算

3A31D277-FFB0-4336-9583-0DA15B7E186B.png

全加器实际上是两个半加器加一个或门组合而成。可以很好的弥补半加器的缺点

305543E2-5745-44AF-A4D2-AB04FC4576CE.png

和这个四位的加法器一样,我们可以很容易的构建出32位的 加法器。也就是用32个全加器串联而成

79B8F5B8-9F41-4250-A7FF-2E9F9E9C12AC.png

它的输入是两个32位数A和B在加法器的内部,会分别连接到了32个全加器的A输入端口和B输入端口。输入全加器的输出成为一个32位的加法器的输出。 整个加法器的进位输入连接到了最低位的全加器。而最高位全加器的进位输出作为整个加法器的进位输出。 这样的加法器就可以满足加法运算指令的需求,对于这道指令我们只需要将rs所指向的寄存器和rt所指向的寄存器的内容分贝连接到A端口和B端口并将S送到RD所指向的寄存器这就完成了对应的加法运算。那么ui的指令类似 addu的指令也完成了同样的运算功能,这两条指令的区别就在于对溢出的处理不同。溢出是指运算结果超出了正常的表示范围。 由于在计算机中我们总是用有限的二进制位来表示一个数,因此加法运算的结果有可能或超出这个有限的位数所能表示的范围。 溢出这种情况仅是针对有符号运算数来说的。 具体表现就是如果两个正数相加,结果变成了一个负数; 或者两个负数相加结果变成了一个正数这显然是不正确的 进位必然正负性取反。

进位是很容易判断的,加法器本身就提供了这样的机制。最高位的全加器的进位输出就是整个加法器的进位。溢出就是当最高位的全加器的进位输入不等于它的进位输出的时候,这就是发生溢出了。像如下示例逻辑也很清晰,可以根据最高数值位是否产生进位,以及符号位是否产生进位来判断是否有溢出发生,即如果最高位的进位输入和最高位的进位输出不相等的话,就表示发生了溢出。

两个有符号数相加产生溢出的条件:

  • 正数+正数:如果结果为负数,则产生了溢出;
  • 负数+负数:如果结果为正数,则产生了溢出;
  • 正数-负数:如果结果为负数,则产生了溢出;
  • 负数-正数:如果结果为正数,则产生了溢出;

A7D5D58A-E4CE-43FA-9C22-95970066DA51.png

这是刚才的全加器,这个c31就是最高位的全加器的进位输入。C(out),就是最高位全加器的进位输出。把这两个信号连出来,判断它们是否不相等。用一个异或门就可以了。当它们不相等时,异或门的输出为1。当他们两个都为0或者都为1时,异或门的输出为0。因此,就可以用这个信号来表示是否发生了溢出。

对于 一个加法器的硬件实现,它并不关心这两个输入数是有符号数还是无符号数。或者说,它对于有符号数和无符号数的处理都是一样的。 全都是通过这套同样的硬件逻辑,进行运算,产生相同的结果。至于参与运算的数到底是有符号数还是无符号数,取决于编程人员如何去看待它。因此,是不是要处理溢出以及如何处理溢出,就不能只交给硬件来做。不同体系结构有不同的方法。

我们先来看MIPS处理溢出的方式。它提供了两种不同的指令。如果编程人员想将操作数看作有符号数,需要处理溢出,则需要使用add和addi这样的指令。 当这样的运算发生溢出时,会产生异常。就是说,控制电路会检查加法器产生的overflow的信号。 如果overflow信号有效,控制电路就会当做一个异常的情况进行处理。如果编程人员希望将操作数看作无符号数也就是不对溢出进行处理。 那就需要使用这两条指令,addu和addiu。它们分别和上面这两条指令是对应的。唯一的区别就是,在执行这两条指令时,控制电路不会检查加法器输出的overflo-w信号。所以说,MIPS处理溢出的方式是提前做准备。按照是否要处理溢出,采用不同的指令进行运算。

而x86则采用了另一种方式。它并没有根据是否处理溢出分为两种运算指令,x86的运算指令,如果产生了溢出,并不会直接由控制电路检查到并进行处理。 而是将加法器产生的溢出信号传送到了标志寄存器。 如果发生溢出,则会致标志寄存器当中的OF位为1.如果没有发生溢出,则致OF位为0。这就是x86的标志寄存器,其中第十一位是OF位,这就是溢出标志。则在后续的指令中,需要检查标志寄存器的OF位是否为1并进行相应的操作。

E68E7EA3-D43C-4BFF-A6DD-473F52BE15B0.png

补码表示二进制数取相反数,有这样一个转换的规则。 叫做按位取反,末位加一。

原理:例如我们有一个数X,那么所谓按位取反,就是将X中的每一位由0变成1,由1变成0,那么得到了X按位取反以后的值。如果我们把这两个值相加, 那么它们的和,显而易见,每一位都是1. 在补码表示中,全1的这个二进制数,就代表着-1 。那么由这个运算我们可以得到,如果将X和它按位取反后的值相加, 就等于-1,我们把这个等式进行一些变换, 就可以得到,X相反数,就等于对X进行按位取反。然后再加上1。那么我们在加法器的基础上,用这样的方式,就可以很容易的实现减法器。

那么我们应该对加法器进行怎样的改造就能实现这样的操作呢?我们来看,这就是刚才的那个加法器。原来的输入A和B都不变,我们增加了一个新的输入,sub-mode只有一个比特,称为减法模式。它首先控制了一个二选一的多选器,如果这个信号为0代表是执行加法操作,那么会将多选器会将B传送的下面的这些全加器。这就跟刚才的加法运算是一样的。而且我们还注意到这个选择信号还连接到了最低位全加-器的进位输入,但是因为它现在是0,所以仍然和刚才的加法操作是一样的,这时候就该执行一个加法的运算。如果这个信号为1,代表要执行一个减法的运算。那这个二选一的多选器会将B这个信号的输入每一位都接上一个非门,相当于按位取反,将按位取反的B送到每一个全加器与A相加,我们还要注意,因为这个选择信号为1,所以最低位的全加器的进位输入也是1。这样就实现了对B进行按位取反,末位加一的操作,于是这个加法器也就变成了减法器。这样我们通过这个改动,这个功能部件又能实现加法,又能实现减法,通过这个选择信号来进行选择。

CBACACD0-F971-48BF-A82B-B043076843F4.png

59BD16F6-D368-4955-A19B-91207E0EF726.png

由于电路中An和Bn都是一开始可以确定的。且电路中C0的值一开始也是确定的,所以电路中所有值都可以通过这三个值进行确立。

3978D9BD-1E65-4B5A-A72E-D20F9BF727AA.png

DE6DA306-4B46-4E6F-9739-E54E2C637791.png

乘法器和除法器

乘法器

ECF4339D-566A-4ABA-8F44-939AE24B398E.png

CA8A45EC-48D6-4105-88F4-84B3CE7A6FFA.png

F83CA6E2-89FF-4093-AFCC-E445A57C1CC1.png

41A56711-B97A-4EF8-B3A0-3197C07A79AD.png

要实现一个四位的乘法器, 首先我们需要一个寄存器来保存被乘数,被乘数寄存器是一个八位的寄存器。而且带有左移的功能。 它有一个左移的控制信号输入,当外部的控制逻辑将这个信号视为有效时,在下一个时钟顺延到来的时候,被乘数寄存器当中的内容就会向左移动一位。

第二个寄存器是乘积寄存器,这也是一个八位的寄存器。 用来保存运算的结果。 被乘数寄存器当中的内容和乘积寄存器当中的内容需要进行加法运算, 这里我们就需要进行一个八位的加法器。 它会将被乘数寄存器当中的内容和乘积寄存器当中的内容进行相加,并将结果再送到乘积寄存- 器当中。

另外,我们还需要一个寄存器来 保存乘数。 这个寄存器只要四位就可以了。但有一点比较特殊,乘数寄存器的最低位被连到了控制逻辑, 也就说控制逻辑可以观察当前乘数寄存器的最低位,并据此来生成相关的控制信号, 这些控制信号就包括是否要让加法器进行 加法运算,以及是否要让乘积寄存器保存当前的运算结果。 另外,这个乘数寄存器还需要有向右移位的功能, 同样也是由控制逻辑发出的信号来进行控制的。 现在我们就有了一个完整的乘法器。

步骤如下:

  1. 首先是检查乘数寄存器的最低位,看是否为1,现在发现它是1,那么就需要将被乘数寄存器当中的内容和当前乘积寄存器当中的内容进行相加, 我们把这一步命名为第一步之后的一个小的步骤,称为1A, 在这一步控制逻辑会控制加法器,将 加法器的两个输入,也就是被乘数寄存器的内容和乘积寄存器当中的内容进行加法运算,然后将加法器的运算结果送入乘积寄存器,在下一个时钟上来临的时候,因为乘积寄存器的写入信号是有效的,所以它就会采样输入端的数据,饼保存到寄存器当中。 这样在这个时钟上升元之后,乘积寄存器当中的内容 就变成了00001000
  2. 对被乘数进行左移,以便为下一次运算进行准备。所以控制逻辑会给出左移的信号,那么在下一个iii来临的时候,被乘数寄存器就会完成一次左移, 最左端的0被丢弃了,在最右端补入一个0
  3. 乘数寄存器是有右移功能的,控制逻辑会给出右移的控制信号,当下一个时钟上升来临的时候,乘数寄存器就会向右移动一位。所以在做完加法运算之后的 第二步和第三步,实际上是在为下一轮的工作做准备。
  4. 进行一个判断。那就是到底有没有下一轮。由控制逻辑进行判断,现在是否已经完成了运算,对这个四位的乘法器来说, 一共要产生四个中间结果并将他们相加,所以与刚才类似这样的工作一共要执行四轮,简单的计数对硬件来说是非常容易的, 所以控制逻辑可以很容易的判断出当前是否已经是第四次循环了。

这种结构的乘法器工作时,每执行一轮,都需要3个时钟周期那如果是一个32位的乘法器,那就大约需要100个时钟周期。上述4步,虽然我们在介绍流程时这些步骤是一步一步进行的,这样只是便于我们理解。在实际的实现上,它们并不需要一定有一个先后的顺序。需要清楚的知道的是寄存器的内容只会在时钟上升沿来临的时候发生变化而在其他时候,无论输入端如何变化,寄存器的内容都不会发生变化

假设现在时钟上升沿还没有来临,那“被乘数寄存器”的输出就是它当前所保存的内容00001000,这八位信号会被送到加法器的输入端,而加法器的另一个输入端连接的是“乘积寄存器”的输出端,因此,现在的信号值是全0。而当前“乘积寄存器”的最低位为1,控制逻辑会据此 产生相关的控制信号,包括让加法器进行加法运算这样加法器就会产生对应的运算结果。与此同时,控制逻辑还会给出写输入信号但是现在时钟上升沿还没有来,所以“乘积寄存器”其实什么也不会做。那在这个时候,控制逻辑实际上可以同时给出“被乘数寄存器”的移位信号还可以给出“乘数寄存器”的移位信号。那现在,控制逻辑就将刚才流程图当中的第1a步第2步和第3步所对应的控制信号都置为有效了。但是因为时钟上升沿还没有来,所以这些寄存器的内容都不会发生变化。 那么当时钟上升沿到来的时候,这些寄存器就会根据输入改变其内容。“乘积寄存器”会将输入端的数保存起来,“被乘数寄存器”会向左移动一位,“乘数寄存器“会向右移动一位。而这些操作都是在同一个时钟上升沿到来的时候完成的,又因为在时钟上升沿之后很短的一个时间寄存器所改变的内容就会传送到输出端。因此我们可以注意到,”被乘数寄存器“的内容现在已经改变,因此很快,它的输出也会变成00010000,并传递到加法器。

但是我们可以放心的是,即使这个信号经过了加法器,并产生了新的运算结果这个运算结果也不会改变”乘积寄存器“的内容。 因为等这个信号传递到了”乘积寄存器“的输入端时已经过去了一段时间,这时候”乘积寄存器“对输入端的采样工作已经完成其输入端信号的改变不会影响这个寄存器的内容了。这样我们就在同一个时刻完成了所有的加法和移位的操作。那我们再回到刚才的那个流程图。在这个流程图当中,这3个顺序执行的操作就可以进行优化而我们这32位的乘法,也就不应该再需要100个时钟周期了。经过优化以后的工作流程,在最开始的第1步,和最后的第4步,还是跟原来一样的而中间的这3步,我们可以把它并排地放在一起。当第1步检查”乘数寄存器“的最低位,如果最低位是1的时候第1a步、第2步和第3步将同时进行 如果最低位为0,则通过控制信号的不同,不执行第1a步,而同时执行第2步和第3步完成之后,就直接进入第4步。这样我们每一次循环只需要一个周期 我们用这个很简单的优化,就把性能提升为了之前的3倍。

对于CPU这样的集成电路芯片来说, 它的价格的一个重要因素就是其中晶体管的数量。 或者说是芯片的面积。

乘法器最终优化结果:

  1. 被乘数寄存器,位数等于实际位数,不需要左移操作
  2. 乘机寄存器,位数等于被乘数位数加乘数位数。初始值放在半高位,为了弥补被乘数无法左移对其运算,增加右移功能
  3. 加法器,被乘数寄存器的这几位和乘积寄存器的半高位进行运算
  4. 乘数寄存器移除,内容放在乘机寄存器的半低位。有效位随着每轮乘机寄存器右移而改变。

乘积寄存器初始值的放置位置也发生了一点变化,我们把它放置在高四位的地方,并在这个寄存器中间加一条虚拟的线来标志高四位和第四位的分界。

乘积寄存器的最低位连接到控制逻辑,这也就是我们当前所需要观察的乘数的对应位,如果发现这位为1,控制逻辑就需要向加法运算的控制信号,并向乘积寄存器发出写入的控-制信号,并向乘积寄存器发出写入的控制信号,但这个写入只发生在 但这个写入只发生在这个寄存器的高四位,然后每个周期会发出右移的控制信号,那么随着乘积寄存器的右移,原先存放在第四位的乘数就在逐渐的被移出,而原先只在高四位的乘积则逐渐占满了乘积寄存器。而在这个过程中,控制逻辑每一轮都可以观察到 乘数的对应位,从而产生对应的控制信号,因此这个结构可以完成刚才我们所介绍的那个乘法运算流程的所有工作。

E58F90A2-8599-4999-8073-B8AB3C852AE3.png

除法器

对于除法操作,本质上你被除数是在不断的和除数进行减法的操作,最后产生了余数。如果我们将每次减法运算的结果都放回到被除数的寄存器当中, 那么这个寄存器又可以被称为余数寄存器。现在我们就有了第一个部件,就是一个8位宽的余数寄存器。 然后我们还需要一个部件来保存除数,还需要一个部件来保存商。除法中这个位宽和乘法运算正好是相反的,在乘法当中,我们一般会约定乘数和被乘数的宽度是一样的,如果两个都是4位宽的,则乘积最多为8位宽,所以在除法当中,我们也用了类似的约定。 如果被除数是8位宽,则除数和商都约定为4位宽。

4D1CEDD9-661E-4783-9E5B-9078F911F379.png

2EE85422-4AB3-415D-BDCF-67634EB8577A.png

除法器初始化流程

除法器面积优化

处理器的设计步骤

指令系统,是在处理器设计之前,就由软件和硬件的设计人员共同协商决定的

609C162E-E41F-4D39-A103-87B6799C2B3A.png

在现代计算机当中,内存就是这样一个指令和数据统一存放的存储器但是在处理器内部,现代的设计往往会设置高速缓存, 也就是Cache这样的部件,用来保存内存当中的一部分数据。高速缓存这个结构,是会被分成指令和数据两个部分

45681FB5-C5CD-47C8-A983-F4B70EC4FAC2.png

6E889F3B-1F47-4BC5-8A50-0905D4D20DD4.png

3550E72C-F558-4CE5-89C3-6A30A6162A87.png

A7E680EB-D2E9-4130-A222-F50E2BFC0A6C.png

如上图,这个RegWr信号等于1的时候,寄存器堆就会采样busW信号上的内容,并将其存入到Rb这个信号所指定的寄存器当中去。然后我们将busA和busB连到ALU的输入端,并且我们根据指令编码当中的操作码和功能位域,就可以知道当前是加法还是减法指令, 我们要通过一个控制信号,来选择当前ALU提供的运算的类型也就是ALUCtr指令。

05D7D9B5-3234-491B-AE2A-59C6B887F1BA.png

同一功能部件扩展而来

  • RegDst:1/2选择器
  • ALUsrc:1/2选择器
  • ZeroExt:0高位扩展器

32C1F202-BC49-425C-B7EB-51164197C270.png

Store指令是将rt所指定的寄存器当中的内容存放到数据存储器当中去。rt所指定的寄存器的内容会从busB信号上出来,那我们就需要将这个信号连接到数据存储器的数据输入端,但是对于除了Store之外的其他指令,我们都不希望将busB上的信号写入到数据存储- 器当中, 所以我们还需要给数据存储器连接一个控制信号,只有在这控制信号有效的时候,才会进行写入的操作,这个信号就是图中MemWr,这样我们就满足了Store指令的需求。

流水线处理器

流水线作为一种生产管理的模式,对于 提高生产效率有着非常大的帮助,最早是兴起于汽车制造厂。

48D55AB8-B6B1-469E-992C-7060453B7E0F.png

53536319-1321-477A-B86C-FDE247D7347E.png

5AC42151-18EE-4C4C-A65D-B61132592698.png

解决的根本方式是讲一整个流程细分,各阶段分布执行,并且互不制约影响。犹如现阶段一部iPhone是全世界几十上百家公司共同合作而成。这也是流水线工程的一个缩影。

68A127EB-3030-4B3A-AC97-A330BEB12DFB.png

9A9D5FAD-24C1-4F28-A53F-C1AF79EF2E2D.png

其实从上述厨师做菜的分解步骤可以很明显的看出,和处理器各处理过程中不同阶段所用到的硬件资源一样,基本上是相互独立的。如果我们能把指令存储器输出的指令编码事先保存下来,那我们就可以提前更新PC寄存器的值,并用这新的值去指令存储器当中取出一个新的指令,而在取新指令的同时,刚才取出的那条指令的编码就会被分解成不同位域,而寄存器堆也会根据输入送出对应寄存器的内容,所以跟刚才的流水线原理的分析类似,如果我们想把这些硬件资源充分地利用起来,我们就需要把它拆分成若干个阶段。那在这个电路的结构上,要进行拆分,我们就在每一个阶段之间添加上寄存器,这就被称为流水线寄存器这些寄存器用于保存前一个阶段要向后一个阶段传送的所有的信息

我们还是以取指到译码的这个阶段为例,我们将指令存储器的输出接到一个寄存器上,那当一个时钟上升沿来临的时候,指令存储器输出的指令编码就会被保存到这个寄存器当中,那么在这个上升沿之后,指令存储器的地址输入如果发生改变,随之影响的指令存储器的输出,也不会被存到这个寄存器当中去,所以在这个时候,我们可以用新的PC来访问这个指令存储器。 从而得到下一条指令的二进制编码,而在这个同时,前一条指令的编码已经在这个流水线寄存器的输出上,并且经过相应的电路,切分成不同的位域,那其中有一个位域就会通过rs连到了寄存器堆,并且选中对应的寄存器,把其中的内容放到busA这根信号上, 而这根信号也会被接到一个流水线的寄存器上。那么当下一个时钟上升沿来临的时候,当前这条指令所需要的rs寄存器的值,就会被保存到这个流水线寄存器当中,与此同时,下一条指令的二进制编码也会保存到这个流水线寄存器中。那么在很短的 Clock-to-U时间之后,译码阶段所看到的指令的编码就已经变成第二条指令了。所以很快,寄存器堆得到的rs的寄存器编号也发生了改变,但是这没有关系,第一条指令所需的寄存器的值已经保存到了这个流水线寄存器当中, 而且在这个时候,也应该会被送到了ALU的输入端,所以这样通过添加流水线寄存器,我们就先从大体上把这个单周期处理器改造成了一个流水线的处理器。

CDC1CCCC-DD01-4626-BEB3-FF2C6D3397EE.png

对于这样一个流水线处理器,它的时钟周期可以设为200ps。因此,这个处理器的主频就是刚才这个单周期处理器的5倍。 当然这只是理想情况,现实中的性能提升幅度并没有这么大,其中一个原因就是这些新插入的流水线寄存器,它自身也会带来一些新的延迟, 我们假设这些寄存器的延迟是50ps。

这是刚才没有考虑流水线寄存器延迟的情况下分析的性能表现,那如果我们加上流水线寄存器的延迟,同样还是执行这几条指令,那就需要每隔250ps 才可以开始一条新的指令,所以时钟周期应该设为250ps,而且对于每条指令本身来说,需要花1250ps才能够完成。 在这一点上,是比刚才在单周期处理器还要更慢一些的。因此对于流水线处理器来说,因为各个处理部件可以并行 工作,从而可以使得整个程序的执行时间缩短,但是流水线并不会缩短单条指令的执行时间,相反,还会增加这个时间。 因此,采用流水线的方式,实际上是提高了指令的吞吐率,从而从整体上缩短了程序的执行时间,提高了系统的性能。

9B28F324-D07C-46DB-88E8-0CF3694C51A5.png

如果仅仅按照指令执行的步骤去切分流水线的话,不能够充分利用流水线这项技术的优势。对于流水线技术,很难做到每一个阶段恰好花同样的时间。,因为我们这个流水线的时钟周期必须按照各流水级当中时间最长的那一级来确定。但这又带来一个问题,只有时间最长的那一层流水线是工作饱和的,其他级流水线都是工作短暂的时间。流水线平衡性问题。

如果是对于流水线处理器来说,不平衡的流水线对于整体的指令吞吐率,和单独一条指令执行时间,都有非常不好的影响。解决办法就是将需要花费较长时间的流水线阶段继续拆分流水线层数。

E628B374-6335-4E25-A403-665289C9E670.png

3FE8C458-32DF-41CD-AEFE-9ADD7C118BDB.png

对于五级流水线来说,其执行单条指令的延迟是1250ps,而对于这个十级流水线,它执行单条指令的延迟就变成了1500ps, 因此切分流水线之后,提高了时钟的频率,从而也提高了指令的吞吐率,但是单条指令的执行时间确实变长了的,这是因为增加了更多的流水线寄存器,在五级流水线当中,流水线寄存器的延迟大约占20%的比例,而在十级流水线当中,因为每级的组合逻辑电路的延迟减半了,但是流水线寄存器的延迟是不会发生变化的,因此,流水线级数划分的越多,流水线寄存器的延迟所占的比例就会越高,从而导致单条指令的延迟越来越大而且不仅如此,当流水线级数变多之后,填满一个流水线所需要的指令就会变多,而这些同时处在流水线当中的指令,他们之间的关系也就会变得更加复杂,从而会带来更多的负面影响。

流水线的级数并不是越多越好,过深的流水线反而会降低性能。流水线加深之后,可以带来的一个很明显的变化—时钟频率的提升,从而提升指令的吞吐率,不过这个方法也有很大的限制虽然我们现在知道了频率高不代表性能好。现在的主流处理器,其流水线深度基本维持在15级左右。

超标量流水线

81ADCEAF-4308-46EE-B33F-19D205B478DB.png

BF314279-5964-437D-9F1B-059FA7D03CEE.png

C09D5B66-567F-4261-9913-8531213CF74E.png

6253B6DD-61A0-49FC-BB44-1B386A970792.png

如图的4发射超标量处理器,它每个时钟周期可以发射四条指令,根据指令的不同,总共会经过八到十一级流水线。与奔腾类似的是在流水线的前端比如说取指,译码并没有分成多条流水线而是采用统一的部件。当然我们要知道这些部件虽然看上去是一个,但它实际上比标量流水线要大得多, 比如说取指部件至少一次要能取来四条指令甚至更多, 而译码部件一次也至少应该完成四条指令的译码,而到了流水线的后端,才会从结构表示上体现出多条并行流水线的形态。

556C0947-58C8-4A46-852B-398FBFF5B645.png

629D156F-EFCC-4077-BBB8-BB0C504A245F.png

超标量流水线与多核的关系。从原理上讲它们都是在空间并行性方面寻求的优化。处理器核。这部分实际上就包含了那些数据通路控制信号等等。 当然还需要包含指令和数据的高度缓存对应了原理结构当中的指令存储器和数据存储器。那为了提高性能现在的处理器当中一般还配备了二级的高速缓存。

前面的看到那个四发射十六级流水线的结构图就是在只这么一个处理器核内部的结构。可以说这一个处理器核就是一个超标量流水线的处理器核, 而在单核的时代这个部分结构就单独制造出了一个芯片,就是以前的单核CPU,那现在我们把这样同样的结构复制多份,然后再加上一些共享的存储部件 就构成了一个多核的CPU。

流水线技术之所以能提高性能究其本质是利用了时间上的并行性,那它让原本应该先后执行的指令在时间上一定程度的并行起来,然而这也会带来一些冲突和矛盾,进而可能引发错误

149A09EE-F7A7-4CD9-B315-4288AD29844C.png

在流水线当中我们希望每一个时钟周期都有一条指令进入流水线开始执行,但是在某些情况下下一条指令无法按照预期开始执行, 那这种情况就被称为冒险

冒险分为三种:

  1. 结构冒险。在这里结构是指硬件电路当中的某部件,如果这条指令所需要的硬件部件还在为之前的指令工作无法为这条指令提供服务, 那就产生了结构冒险。
  2. 数据冒险, 如果这条指令需要某个数据而之前的指令正在操作这个数据,那这条指令就无法执行,这种情况称为数据冒险。
  3. 控制冒险,如果现在要执行哪条指令,是由之前指令的运行结果来决定的,而现在之前指令的结果还没有产生,那就导致了控制冒险。

如下处理器各阶段受制约的部件:

  • 取指阶段需要用到指令存储器,
  • 译码阶段需要用到寄存器堆,
  • 执行阶段需要用ALU,
  • 访存阶段需要用数据存储器,
  • 而写回阶段也需要用寄存器堆。

3A2C713F-B38A-4F84-A385-D2455984D365.png
7DC1CB2E-A28C-489F-96FD-2C4EDF0199B4.png

在现在的处理器当中我们通常还是将指令和数据分别放在不同的存储器当中,就是靠在存储器当中设置独立的指令高度缓存和数据高度缓存来实现的。 我们还是要强调的在计算机中主存储器也就是内存是统一存放指令和数据的,这也是冯诺依曼结构的要求,只是在CPU当中的一级高速缓存会采用指令和数据分别存放的方式

8FD94A70-CC4C-4E4B-8533-BC713FC3E173.png

怎么来判断存在数据冒险呢。 所谓数据冒险,就是当前有一条指令要读寄存器,而它之前的指令要写寄存器,但又没有完成, 所以我们只用检查,在译码这个阶段,需要读的寄存器的编号,这个通过链接在寄存器读口的信号就可以得到。然后我们再检查后面各个阶段, 其实在每一级,都有些信号能够表明这条指令是否要写某个寄存器,以及要写哪个寄存器。因此,只需要检查后面每一个阶段所要写的寄存器的编号,和当前译码阶段,所要读读寄存器的编号,是否有相同。如果存在相同,那就是有数据冒险。那只要出现来数据冒险,我们就在流水线中插入空泡。这样我们就能通过硬件来解决数据冒险的问题

停顿对于流水线处理性能太差。所以需要数据前递。也就是上一条指令将自己的运算结果往前传递到下一条指令去,如图在600频秒的时候,ALU的输出结果已经是t0的值了,那在600频秒的这个时钟上前过去之后,t0的这个值会被保存到执行和访存之间的这个流- 水线寄存器当中去。我们如果把它传递给ALU的输入,就可以正确的完成后面这条加法运算了。

对于数据冒险,继续增加流水线的深度, 或者扩展成超标量流水线,又会出现新的数据冒险的情况。

D99F2D00-CB82-496D-BD12-5AC2E2701015.png

像Core i7是4发射16级流水, 我们可以简单地认为,流水线在充满的时候,可能会有4乘以16,总共64条指令在流水线中。而再看智能手机当中经常使用的ARM Contex-A15处理器, 这是一个3发射15级流水线的处理器,那我们也可以简单地认为 在流水线中,总共有3乘以15,45条指令在同时执行。

存储层次结构

存储层次结构概况

bios芯片和硬盘是非易失性的存储器,所以在系统通电之后CPU必须要从bios芯片开始执行程序,然后这段程序把硬盘等设备配置好之后再将更多的程序和数据从硬盘搬运到内存, 之后CPU才可以在内存里执行程序。

硬盘和主存都是可读可写的,而bios芯片则是一个只读的芯片, 倒不是说它完全不支持写操作,而是对它的写需要 借助特殊的设备或者特殊的操作过程,非常的麻烦,无法支持经常性地写入数据。

计算机对BISO芯片的读,和主存的读都是支持随机访问。所谓随机访问是指对存储器当中任何一个数据的访问所花费的时间与这个数据所在的位置没有关系。而硬盘内部实际上是由多个盘片构成,这些盘片处于高速旋转的状态,并由一个机械的读写头去寻找需要访问的数据的位置。这就不是随机访问的模式,而且由于期中有机器部件的存在,速度就变得非常的慢。

CPU的运行速度很快在它需要访问存储器的时候,最好能在一个时钟周期内就完成数据的访问,不然就会阻碍CPU后续的操作。 而储存器的速度是明显高于硬盘的速度的,所以总体看来,如果我们能找到一个存储器支持随机读写而且是非易失性的,访问时间也很短, 那么就可以考虑只用这样一个存储器和CPU连接,当然我们还要考虑到是否有足够大的容量以装下所以我们需要的程序和数据, 还有价格是否能够承受,以及功耗是否合适等等。那既然现状我们是使用了这么多种不同的存储器,虽然是因为没有一个唯一而完美的解决方案。

F27BF5BC-9A85-4372-8860-0D9D62E1DDDB.png

如图,80年的8080,时钟频率大约为一兆赫兹,其时钟周期是一千纳秒。90年我们选取了386的一个版本,时钟周期是五十纳秒。 而2000年选择的是奔腾二,时钟周期大约为一点六纳秒。2010年选择的是Corei7,时钟周期大约是0.24纳秒。当然我们要注意因为这个时候已经是四核了,如果每个核,每个时钟周期都需要对外传输数据,那其实相当于每零点一纳秒就需要传输一个数据。

硬盘的访问时间在80年的时候大约是87毫秒,毫秒和纳秒差了一百万倍,所以虽然这30年来硬盘的访问速度也有所提升,大概提升了29倍,但它和CPU的时钟周期完全没有可比性,相差数百上千万倍。所以仅从访问时间这一项来看硬盘从一开始就不具备直接和CPU进行交互的能力,当然它的优势在于容量大而且便宜。在80年的时候大约每兆字节需要五百美元,那时典型的硬盘容量是一兆bit,而在这30年中硬盘单位容量的价格在迅速的下降,下降幅度超过160万倍,而与此同时硬盘的容量也在迅速的增长,上升的幅度也有150万倍。这样我们就可以存放更多的程序和数据。这是硬盘技术进步带来的最大的好处。当然另外还有一件很有意思的事情,我们发现价格下降的幅度和容量上升的幅度基本相当,也就是说现在和30年前相比虽然硬盘的容量和单个字节的价格有了巨大的变化,但是整个硬盘的价格却基本保持着不变。那好硬盘作为一个非易失性的存储器自然有它自己的作用

但是要想和CPU直接交互还得看其他的设备,这就是DRAM。现在的存储主要是采用DRAM实现。它的访问时间在1980年大约是375纳秒,这个时候DRAM实际上比CPU的运行的速度还要快一些,所以这时候并不用担心内存无法及时给CPU提供数据的事情。而到了90年DRAM的速度已经比CPU的速度慢了,而且后来这个差距越来越大,到2010年,即使只考虑单核CPU的需求,DRAM的访问时间也和CPU的时钟周期相差一百倍。那在这同样的这30年里CPU的时钟频率提升了2500,而DRAM的访问速度却只提升了9倍,所以这个差距明显是在拉大的。不过DRAM的进步也同样体现在其容量和价格上,这30年来其容量足足提升了有12万倍之多,而成本也几乎有同样比例的压缩,也就是说我们可以与30年前用同样的价格买到12万倍的容量的内存。容量扩大自然是一件好事,但是这个性能的差距到底会带来什么样的影响呢?

EC908FEB-949C-47FB-9780-3EF8879843EE.png

在CPU内部需要花一个周期产生访问存储器的地址,而读取存储器需要等待一百个时钟周期才能得到这条指令的编码,如果CPU真是这么运行的,那一个1G赫兹的CPU只相当于时钟主频只有5兆赫兹的CPU的性能了。这样显然是无法接受的。所以我们必须要考虑如何提升CPU访问存储器的性能。

可以考虑在CPU和DRAM之间,加上一个速度更快的SRAM,那如果我们能让CPU所需要的程序和数据大部分时间都存放在这个SRAM当中,那CPU就可以获得快得多的存储器访问时间。这样一个SRAM的部件,就称为cashe,也就是高速缓存。cache对指令执行时间的影响:SRAM的访问时间大约可以认为是3个时钟周期,那上面这个指令执行的过程就会变成,所有读存储器的100个时钟周期都会缩短为大约3个时钟周期,这样大约只需要总共10个时钟周期就可以完成一条指令了。相比于之前的性能,有了巨大的提升。

CCC201C2-BF21-40F0-9A4A-53DB9547B70D.png

从价格因素 来考虑,SRAM也无法取代DRAM用来作为内存,而是只能用一个小容量的SRAM作为高速缓存,保存最常用的程序和数据,以达到性能和价格的平衡。 但是386CPU芯片内部并没有设计cache这个部件,后来也发现这对性能有很大的影响,所以当时是采用在芯片外再增加一块SRAM芯片作为cache来解决这个问题。那么到了486的时候,就已经把cache集成到了CPU芯片的内部,从而缓解CPU和DRAM主存之间的性能差距,因此,现代的计算机当中都采用了这样层次化的存储结构。在这个层次结构中,越往上的部件,容量越小,但速度更快,而单位字节的成本更高,越靠下的部件,容量更大,但速度更慢,而单位字节的成本更低

1DBD630D-557F-4426-984A-44FF04D6A98B.png

DRAM和SRAM

DRAM芯片以一个存储阵列为核心,这个存储阵列以行列的形式组织,那么行列的交点就是一个存储单元。每个存储单元都有唯一的一组行列地址指定。那么这样一个存储单元一般由若干个比特构成,常见的有4比特或者8比特,而每一个比特都是由这样的一个基本电路构成。通常情况下,DRAM芯片的外观是这样的。通常情况下,DRAM芯片的外观是这样的。而在这里,有8个DRAM芯片焊接在这样一块绿色的小电路板上, 构成了一个内存模组,这也就是我们通常所说的内存条。

因此,从外部给入了行地址和列地址之后,这些地址会同时送到每一个DRAM芯片,从而在每个DRAM芯片当中选中对应的一个存储单元。如果每个DRAM芯片送出8个比特,那它就可以向外同时送出64个比特。因此,如果从CPU送到内存条一组行列地址,那内存条就可以返回这组地址所对应的,一个64位的数。这就是一个数据在内存当中大致的存放方式。

当然我们也可以不用内存条的形式,尤其是对于平板电脑和智能手机,它们本来体积就很小,很难容纳内存插条这样大个的组件,而且它们也不一定需要这么多的DRAM芯片,所以往往是在它们的主板上直接焊接DRAM芯片。但不管是哪种情况,一般都是由若干个DRAM芯片构成了计算机的主存储器, 也被称为内存。

BF287D8A-E473-41D5-B473-685600EAEF73.png

5F1D80A3-8979-442C-ADA9-6C452FD8E723.png

因为晶体管的开关速度远比电容充放电的速度要快,所以相对于DRAM,SRAM有速度快的优势但是我们也看到SRAM中要存储一个比特就需要用六个晶体管,晶体管数量多就会造成芯片的面积大,从而带来集成度低和价格较高的问题。同时每个晶体管都是要耗电的,晶体管越多功耗也就会越高,这些都是SRAM的缺点

现在CPU当中的高速缓存一般都是用SRAM来实现的。比如这就是一颗四核CPU的版图,**在这个芯片当中这些大面积的看起来非常规整的电路实际上都是SRAM,所以仅从制造成本上来看这些作为高速缓存SRAM往往要占到整个CPU的一半或者更多。而且由于高速缓存大多和CPU采用相等或者接近的时钟频率,所以它们的功耗也非常高,这都是在使用SRAM实现高速缓存时需要注意的问题。 **

6DFF6DE2-A98A-4286-B8DF-E870E98406B6.png

那么综合比较来看DRAM的 主要优势在于集成度高,功耗低,价格低,而SRAM的优势则于速度快,而且不像DRAM需要经常进行刷新, 所以我们需要根据它们不同的特点,用在不同的场合上。

主存的工作原理

E554FBC3-70D1-4E54-8BB4-8AD4114E77DD.png

在计算机内部,CPU通过系统总线连接到了内存控制器,而内存控制器再通过系统总线连接到了内存条那实际上内存控制器会把相关的地址线、数据线连接到内存条上的各个DRAM芯片,那当CPU需要访问存储器时,那首先要申请系统总线,在获得总线控制权后会将地址发到内存控制器中,对于一个32位CPU,那这个地址一般就是32位的,在这个时候,地址并不会分成行地址和列地址,而是只有一个地址。然后内存控制器会将这个地址进行分解,形成行地址和列地址等多个部分。然后内存控制器就会向DRAM芯片发起访存操作

在这一步,可能会包括两个部分,一是称为预充电的这个操作。这是一个可能有也可能没有的操作。然后进行行访问,也就是发出行地址。 通过存储总线发出的行地址会被DRAM芯片当中的行译码器接收到,就会在存储阵列中选中对应的那一行,然后这一行当中的所有的存储单元的信号都会被经过放大之后放入到一个缓冲区当中,那这个过程就会被称为激活,或者是行访问的过程。那么只有等这个行缓冲区的信号都稳定了,我们才可以进行下一步的操作。 因此,我们需要关注的一个时间,就叫做tRCD,这是从行选 到列选的延迟。

如这个时序图。假设这个时钟上升沿,发出了行地址,那么必须等待tRCD这么长的时间,才可以去进行下一步的操作,也就是发出列地址。那这个时间的长短是由这个DRAM芯片本身的特性决定的。一般来说,它的质量越好,这个时间就越短。而对同样一个DRAM芯片,它的工作环境越好,这个时间也会越短。那么等tRCD这段时间过去之后,我们就可以发出列地址了。

7CB4FF4A-4B61-42A9-BD1C-627D487F13D2.png

这时内存控制器就会把事先准备好的列地址发到DRAM芯片,由列译码器接收,那么列译码器收到列地址之后,就会从缓冲区中选出对应的那一列,如果现在要进行读操作,那被选中的这个存储单元的数,就会送到数据输出接口上去,而从发出列地址,到选出对应的存储单元的数这个过程,就被称为列访问的过程。列访问也是需要时间的。这还是刚才那个时序图。从发出行地址到可以发列地址,中间要等待tRCD这么长的时间,然后从发出列地址到选中的存储单元的数可以输出,也需要等待一段时间,这段时间以CL标记,也就是从列选到数据输出的延迟。

通常情况下,访问内存都不会只读一个数,而是会连续读出多个数,那么这些数会每一个时钟周期输出一个,依次地送到数据线上。也正是因为如此,我们要事先把一整行的数,都读到缓冲区里,因为它可以把每一个存储单元同时都连到了缓冲区中,读出一个存储单元也是读,读出一整行来也是读,所花的时间并没有明显的差别。所以不如一次把一整行都读出来,然后从中选择需要的连续的若干数据送出去,而且这样还有另外一个好处,如果下一次访存还是在这同一行,那就不需要重复发这个行地址了。因为对应的行已经在缓冲区当中,只要直接发列地址就可以。 这样就可以大大地缩短访存的时间。

当DRAM芯片送出数据之后, 内存控制器就会采样对应的数据,然后将采样到的数据再送回到CPU当中去,那过一段时间CPU又会发出访存的地址,那如果这次要访问的数据和刚才要访问的数恰好在同一行,那就不需要再重新发行地址,只需要直接发列地址,从缓冲区中选出对应的单元就可以了。如果下一次访问所要的数据不是这一行,那么就需要把激活的这一行关闭,这个过程我们称为预充电, 实际上预充电最早可以在前一次传输,最后一个数据即将送出的时候开始,因为我们不确定下一次传输到底会不会在同一行。所以我们两种可以选择的策略:

  1. 是等到新的传输开始,如果发现要访问的数据不在已经被激活的这一行,那时再进行预充电,这也就是刚才在步骤二中,我们提到的那个可能没有的预充电操作。
  2. 是在一次传输结束后就进行预充电。这样在下一次的传输是同一行的概率不高的情况下,反而会获得更好的性能。那么预充电也是需要花一定时间的,这个时间我们记为tRP,从内存控制器发出预充电的命令到DRAM芯片可以接收下一次行地址, 这段时间就被称为tRP。

665B89DB-7F50-464F-BA18-74E37C38D86C.png

从这里也可以看出,虽然内存的时钟频率是7.5纳秒,但并不意味着只需要7.5纳秒就可以得到想要的数据。CPU和内存之间的速度差距越来越大。SDR是指毎个时钟的上升沿传输数据,接收端也用时钟的上升沿采样数据,而DDR则是在时钟的上升沿和下降沿都传输数据,这样就可以在同样的时钟频率下传输双倍的数据。需要说明的是,DDR指的是这种传输方式,运用在SDRAM内存上,就有了DDRSDRAM,但它不仅仅用在内存上,还用在其他很多领域。所以DDR只是一种传输数据的方式,不能将它等价于我们现在用的内存。

高速缓存的工作原理

计算机中运行程序的一个特点, 这个特点被称为程序的局部性原理。

EC0C32E6-CD06-42DE-B7C9-CD2CBD683A83.png

这是一段很常见的程序, 有两层循环,对一个二位数组进行累加,如果sum这个变量是保存在内存中的,那它所在的这个存储单元,就会不断的被访问,这就称为时间局部性,这些对循环变量进行判断和对循环变量进行递增的指令,也都会被反复执行,而另一点,叫作空间局部性,指的是正在被访问的存储器单元附近的那些数据,也很快会被访问到。那么就来看这个数组,它在内存当中,是连续存放的,从a00,a01,a02,这样一个接一个的存放下去,那么在这段循环访问它的时候,访问了a00之后,很快就会访问a01,然后很快会访问-a02,这样的特征,就被称为空间局部性,那Cache就是利用了程序的局部性原理,而设计出来的。

F351F60B-C3C8-4059-9142-EB39768A3DAA.png

当CPU要访问主存时,把地址发给了Cache,最开始,Cache里面是没有数据的,所以Cache会把地址再发给主存,然后从主存中取得对应的数据,但Cache并不会只取回CPU当前需要的那个数据,而是会把与这个数据,位置相邻的主存单元里的数据,一并取回来,这些数据就称为一个数据块,那么Cache会从主存里,取回这么一个数据块,存放在自己内部,然后再选出CPU需要的那个数据送出去,那过一会儿,CPU很可能就会需要刚才那个数据附近的其它数据,那这时候,这些数据已经再Cache当中了,就可以很快的返回,从而提升了访存的性能。因为这个数据块暂时会保存在Cache当中,那CPU如果要再次用到刚才用过的那个存储单元,那Cache也可以很快的提供这个单元的数据,这就是Cache对程序局部性的利用,我们要注意,这些操作都是由硬件完成的,对于软件编程人员来说,他编写的程序代码当中,只是访问了主存当中的某个地址,而并不知道这个地址对应的存储单元,到底是放在主存当中,还是在Cache当中。流程如下:

56E87157-106A-45EB-A9A2-388FF7D73F4C.png

Cache主要组成部分是一块SRAM,当然还有配套的一些控制逻辑电路,那这个SRAM的组织形式,像这个表格,它会分为很多行,那么在这个事例的结构当中,一共有16行,每一行当中有一个比特,是有效位,还有若干个比特是标签,然后其它的位置,都是用来存放从内存取回来的数据块。

那么现在来执行这四条指令,第一步,我们要访问2011H这个内存地址,并取出对应的字节,放在AL寄存器当中去,那CPU就会把这个地址发给Cache,那因为现在Cache全是空的,所以显然没有命中,那Cache就会向内存发起一次读操作的请求。因为Cache一次要从内存中读出一个数据块,而现在这个Cache的结构,一个数据块是16个字节,所以它发出的地址,都是16个字节对齐的,所以这时,Cache向主存发出的地址,是2010H,这个地址是16个字节对齐的,而且从它开始的16个字节的这个数据块当中,包含了2011H这个地址单元,当Cache把这个数据块读回来之后,会分配到表项1中,那么在这个表项当中,这个字节就是2010所对应的数据,这个字节就是2011所对应的数据,所以Cache会将这个字节返回给CPU,但是Cache为什么要把这个数据块,放在了表项1当中呢?CPU在执行这条指令的时候,Cache收到的地址,实际上是2011H,那因为现在一个数据块当中,包括16个字节,那最后的这个16进制数,正好用来指令这16个字节当中的哪一个字节,是当前所需的,因此,我们取回的这个数据块,要放在哪一个表项当中,就要靠前面的一个地址来决定。 现在还剩下8位的地址,我们也必须记录下来,不然以后就搞不清楚,这个Cache行里存放的数据,到底是对应哪一个地址的,所以剩下的地址,不管有多少,都要存放在标签这个域当中。把数据块取回之后,还需要把这个有效位置为1,那这样,我们通过这个表格,就可以明确的知道,当前的这个数据块,是从2010这个地址读出来的,那在这个Cache行中的第一个字节,就是CPU现在做需要的那个字节了。那把这个字节取出来,交给CPU,这条指令对应的读操作就完成了。

E6186F1F-2067-4C65-883F-9CBFC16F950F.png

3C5DF0EA-2EED-4E0E-884E-E281F49785C7.png

当CPU要写一个数据的时候,也会先送到Cache,那这时也会有命中和失效两种情况。如果Cache命中,可以采用一种叫写穿透的策略,把这个数据写入Cache中命中的那一行的同时,也写入主存当中对应的单元,那这样就保证了Cache中的数据和主存中的数据始终是一致的。但是因为访问主存比较慢,这样的操作效率是比较低的

另一种策略叫做写返回,那这时只需要把数据写到Cache当中,而不写回主存,只有当这个数据块被替换的时候,才把数据写回主存。那这样做的性能显然会好一些,因为有可能会对同一个单元连续进行多次的写,那这样只用将最后一次写的结果在替换时,写回主存就可以了,大大减少了访问主存的次数。但是要完成这样的功能,Cache这个部件就会变得复杂得多。

同样地,在Cache失效的时候,也有两种写策略,一种叫做“写不分配”,那因为Cache失效,所以要写的那个存储单元不在Cache当中,写不分配的策略就是直接将这个数据写到对应的主存单元里;而另一种策略叫“写分配”,那就是先将这个数据所在的数据块读到Cache里以后,再将数据写到Cache里。那写不分配的策略实现起来是很简单的,但是性能并不太好,而写分配的策略,虽然第一次写的时候操作复杂一些,还是要将这个块读到Cache里以后再写入,看起来比写不分配要慢一点,但是根据局部性的原理,现在写过的这个数据块过一会很可能会被使用,所以提前把它分配到Cache当中后,会让后续的访存性能大大提升

在现代的Cache设计当中,写穿透和写不分配这两种比较简单的策略往往是配套使用的,用于那些对性能要求不高,但是希望设计比较简单的系统;而大多数希望性能比较好的Cache,都会采用写返回和写分配这一套组合的策略。那除此之外,在对Cache进行写的过程中,如何去查找分配和替换Cache中的表项,都是和刚才介绍过的读操作的情形是一样的,就不再重复描述了。

58B49201-2282-459A-AD89-F166132D0E93.png

E847F422-0C87-482E-8A8A-DF426535CAAA.png

平均访存时间就等于命中时间,加上失效代价乘以失效率。

  • Miss Penalty:失效代价
  • Miss Rate:失效率
  • Hit Time:命中率

想要降低命中时间,就要尽量将Cache的容量做得小一些,Cache的结构也不要做得太复杂。但是小容量的结构简单的Cache,又很容易发生失效,这样就会增加平均访存时间。其中如果要减少失效代价,要么是提升主存的性能,要么是在当前的高速缓存和主存之间再增加一级高速缓存。那在新增的那级高速缓存当中。也需要面临这些问题。所以这三个途径并不是独立的,它们是交织在一起,相互影响。

002737E9-5D6A-46D0-ADB0-18C575D9C6D1.png

B82FB7AE-32AF-4C92-B39A-E06C948377BF.png

在不增加 Cache总的容量情况下,我们可以将这8个Cache行分为两组,这就是二路组相联的Cache。如果这个Cache总共只有8行,而我们把它分成八路组相联,那也就是说,内存当中任一个数据块都可以放到这个Cache当中的任何一个行中, 而不用通过地址的特征来约定固定放在哪一个行。那这样结构的Cache就叫做全相联的Cache。这样的设计灵活性显然是最高的,但是它的控制逻辑也会变得非常的复杂。

我们假设CPU发了一个地址,Cache要判断这个地址是否在自己内部,那它就需要把可能包含这个地址的Cache行当中的标签取出来进行比较,对于直接映射的Cache,只需要取一个标签来比较就行,二路组相联的时候,就需要取两个标签同时进行比较,四路组相联的时候就需要取出四个标签来比较,而在全相联的情况下,那就需要把所有行当中的标签都取出来比较。这样的比较需要选用大量的硬件电路,既增加了延迟,又增加了功耗,如果划分的路数太多,虽然有可能降低了失效率,但是却增加了命中时间,这样就得不偿失了。而且话又说回来,增加了路数,还不一定能够降低失效率,因为在多路组相联的Cache当中,同一个数据块可以放在不同的地方,那如果这些地方都已经被占用了,就需要去选择一行替换出去,这个替换算法设计得好不好,就对性能有很大的影响。

A6669F17-D8D7-4F14-A839-C36129E1C5C5.png

性能比较好的替换算法,是最近最少使用的替换算法,简称为LRU,那它需要额外的硬件来记录访问的历史信息,在替换的时候,选择距离现在最长时间没有被访问的那个Cache行进行替换,在使用中,这种方法的性能表现比较好,但是其硬件的设计也相当的复杂。所以映射策略和替换算法都需要在性能和实现代价之间进行权衡。

ABBED58B-B1D1-48B8-B2AE-79018D6DBFF4.png

较先进的Core i7,它内部采用了多级Cache的结构,其中一级Cache是指令和数据分离的各32K个Byte,采用了8路组相联的形式,命中时间是4个周期,所以在CPU的流水线当中,访问Cache也需要占多个流水席。

那么在这个4核的i7当中,每个处理器核还有自己独享的二级Cache,二级Cache就不再分成指令和数据两个部分了,因为它的容量比较大,指令和数据之间的相互影响就不那么明显。但是二级Cache的命中时间也比较长,需要11个周期,那i7CPU的流水线总共也就16级左右,肯定是没有办法和二级Cache直接协同工作的。这也是为什么一级Cache不能做得很大的一个重要原因。 ==CPU和一级Cache直接交互,二三级Cache辅助一级Cache==。

三级Cache,由四个核共享的,总共8兆个字节,三级Cache采用了16路组相联的结构,而且容量也很大,达到了8兆个字节,这又导致它的命中时间很长,需要30到40个周期,但它这样的结构命中率会很高,这样就很少需要去访问主存了

A5AA91DE-24FD-46A0-8E5E-C4A9BF698B80.png

中断和异常

UNIVAC对异常处理的方式。当算术运算溢出的时候,UNIVAC就转向地址0去取出指令。在那里会执行两条修复指令。

8086是一个十六位的cpu。它内部有四个16位的通用寄存器。对外则有16根数据线,但是它的地址显要更多一些,一共有20根。这样可以寻址的内存空间就是2的20次方,也就是一兆个byte。那由于它内部的寄存器与运算器都是16位宽的。要生成20位宽的地址,就得用一定的转换方法。8086采用的是“段加偏移”的方式。

K(38$F}RL%(8MLJR6YNKCXY.png

对于这一兆的空间不都是可以任意使用的。有两个区域保留作专门的用途。在这一兆字节的内存空间中,最低的1K个字节,保留作中断向量表区。而最高的十六个字节保留为初始化程序区。用8086cpu复位之后,它第一次取指令的操作发出的地址是四个F一个0。这个地址就是在这一兆内存空间的最高的16个字节的这个地方。那这个区域实际上是很小的,只能放很少的几条指令。通常放在这里的是一条无条件的转移指令。转移到内存空间当中的另一个地方。在那个地方存放着后续的系统程序。

这个1K的字节被用作中断向量表区,它一共存放了256个中断向量。每个中断向量占四个字节。这样正好就是1K个字节。那除了这两块专用的区域,其他区域就可以用来存储一般的程序指令和数据。那在这块区域,还有那些用于进行中断处理的程序。这些程序就被称为中断服务程序。而这些程序代码起始地址则被称为中断服务程序的入口地址。 这就是中断程序向量的定义。

RY~A{J~`B{6UAMG}WBC2CJE.png

现在的CPU一般都能够处理多种不同的中断类型。那么每个中断类型就对应一个中断向量,一共4个字节。这四个字节当中,前两个字节用于存放中断服务程序入口的偏移量。而且是低字节在前,高字节在后。因此对于这个中断向量,这两个字节就会被存放到而且前一个字节是寄存器当中的低字节,后一个字节是寄存器当中的高字节。那么中断向量当中的后两个字节,则对应了终端服务器入口的地址的段基址。用来存放到代码段计算器,也就是CS计算器。

那么同样,前面那个字节对应了寄存器当中的低字节,后面的字节对应了寄存器当中的高字节。那么在8086当中或者是后来X86处理器的实模式下,就需要用CS和IP这一对寄存器,来指定一个内存的地址。这个地址的产生方式就是叫做段加偏移

这两个16位的地址就构成了逻辑地址。那通常的表示方式就是用一个冒号分隔开这两个16位数。在cpu中会将段寄存器当中的数左移4位,然后加上偏移量,这样加法运算的结就是20位的物理地址。这就是逻辑地址生成物理地址的方式。实际上是段基值乘以十六加上偏移量。那对于二进制来说左移四位就相当于乘以16了。

基于这样的方式,每个中断向量都由两个段基值和两个向量的偏移地址组成,还因为每一个中断向量占四个字节。 在整个中断向量表中,一共有256个中断向量。分别命名为0号、1号、一直到255号中断。那这个中断向量表要在系统里面启动时进行初始化。 那么假设1号向量的初始值是这样的,那当cpu接收到中断时,如果发现时1号中断。那因为各个中断向量放置的地址是固定的,那cpu不需要通过执行指令,直接通过硬件电路的设置,就可以发出内存访问来读取这四个字节的内容。然后将其中高两个字节送到CS寄存器当中去,低两个字节送到IP寄存器当中去。那对于8086来说,这两个寄存器的功能就相当于我们在之前介绍处理器内部结构时,提到的PC寄存器

所以这两个寄存器的值一旦发生改变,下一个周期cpu就会从那个新的地址开始取下一条指令。也就是当这两个寄存器的值一旦发生改变,那CPU下条指令的运行地址就是CS寄存器加IP寄存器指向的地址,而不是PC寄存器的值,那根据段加偏移的计算方法,cpu发出的地址均是43006,。因此也就是说,在遇到一号中断时,cpu就会转到43006这个地址开始执行程序

所以需要实现把一号中断的服务程序存放在这里。与此类似,我们还会把0号中断的服务程序放在存储器的另一个地方。然后将中断程序的起始地址放在段基值和偏移地址,存放在0号中断向量所在的位置。那当cpu遇到中断时,如果发现是0号中断,则会将0号中断向量对应的内容取出,分别填到CS和IP寄存器当中去。这样cpu就会从0号中断符合的起始地址,开始对指令进行执行。这些中断服务程序在内存当中的存放顺序并没有要求。并不需要按照中断类型的顺序。

那对于8086的中断向量表cpu已经固定使用了前五个类型的中断。之后的27个中断也是保留给后续的cpu使用的。而除了前20个中断,之后的224个的中断则是交给使用cpu的用户自行定义。

22996A62-2CD0-4969-A526-850AABDB5E93.png

当CPU执行到某条指令,如果此时发生中断,CPU内部就会产生中断信号,相关的中断处理电路会判断中断的来源,并产生中断类型号。CPU的硬件电路会将CS和IP寄存器压栈,这样就保存好了处理完中断后要返回的地址同时硬件上还会将FLAGS寄存器压栈,以便保存好当前的各项标志,以免中断处理程序当中,有些指令会改变程序的标志位在硬件上还会清除IF标志位,以起到关中断的作用。然后根据中断类型号,找到对应的中断向量,也就是新的CS和IP的值,并以此更新CPU当中的CS和IP寄存器。当完成这个操作后,CPU就会转到中断服务程序开始执行在中断服务程序中,也可以执行STI指令,以开放中断当完成了中断服务程序之后,最后一条就是执行中断返回指令这条指令会从存储器当中将刚才压栈的三个字弹出来,并按照对应的顺序,存到CS、IP和FLAGS寄存器当中去,这样就完成了返回主程序的动作。这就是中断处理过程的六个主要的步骤

9AF1750C-6A8B-404C-883B-A05C65557D9E.png

4号中断,这个中断叫作溢出中断,也就是因为算术运算发生了溢出,而引起的中断,这个中断的产生要借助一条特殊的指令,也就INTO指令,当执行这条指令时,硬件电路会去检查溢出标志位OF是否位1,如果为1,则会引起类型为4的内部中断

比如这条加法指令,在执行时,就有可能发生了溢出,那么运算器在运行完这个加法后,会去设置标志寄存器当中的标志位,也就是第11号溢出标志位,但这个操作本身并不会引发中断,只是将标志位置1,但如果之后执行了INTO指令,这条指令是会去检查OF标志位,如果这时OF标志位为1,那就引起了4号中断,但是如果INTO指令执行时,OF标志位为0,那就什么也不会发生,这条INTO指令就相当于一条空操作指令,所以INTO指令通常会安排在算术运算指令之后,用来检查这个运算是否发生了溢出,并且, 在发生溢出时,就调用中断程序进行处理

很多时候,加法运算的溢出,并不需要进行处理,如果每一次溢出,都要引发中断,反而可能影响程序的性能,所以在指令系统设计的时候,就把是否要检查这种溢出的情况,交给程序员来进行判断。

类型1中断称为单步中断,要引发这个中断,需要将标志寄存器当中的TF位置1,这时CPU就处于单步工作方式。在单步工作方式下,CPU每执行完一条指令,就会自动的产生一个类型1的中断,然后进入类型1中断服务程序。这种工作方式主要是用来进行错误调试的,比如说,你发现CPU执行一段程序有错误,但是又不清楚这个错误具体发生在什么地方,那就可以将TF标志位置为1,在单步工作方式下进行调试。通常情况下,我们会在这个类型1的中断服务程序当中,将CPU当中的各个寄存器的内容,在屏幕上显示出来,这样CPU每执行一条指令,我们就可以在屏幕上看见CPU当前正在执行的,是哪一条指令,这条指令的地址是什么,执行这条指令的前后,那些通用寄存器又有什么样的变化,这样我们就有可能发现,到底在哪一步,发生了不符合我们预期的行为。这个方式对于调试是很有用的,但是CPU每执行完一条指令,就要产生一个中断,那程序执行的速度,就是非常慢的,如果想要调试一个很大的程序,仅用单步中断就会变得比较困难。

所以还有一个用于调试的中断,就是类型3,断点中断。断点中断通常和单步中断配合使用,在调试一个很大的程序时,一般我们会先通过断点中断,将错误定位在这个程序的一小段代码中,然后,再对这一小段代码,用单步的方式,进行跟踪调试,这样就可以大大提升调试的效率

850642F1-D606-4A91-BD68-D72FCBC9A94F.png

无论是BIOS中断,还是DOS中断,或者是其它类似的中断方式,其本质并不是计算机运行当中发生了异常的情况,而是利用了现有的中断这种机制,来实现一些系统函数,代码的调用,以便向高层的软件屏蔽底层硬件的细节,从而提高编程的便利性,正确性,和可抑制性

输入输出设备

AC24782A-D9A1-4561-8F4C-DE815203CBDA.png

有一条指令就是读取1111这个地址单元。那么当CPU执行了这条指令的时候,就会在地址总线上发出这个地址,与此同时,在控制总线上发出表示当前是读操作的信号,那这个输入输出设备收到这样地址和控制信号之后,就会从1111这个单元,取出对应的内容,然后把它送到数据总线上去,而CPU这时会采样数据总线上的信号,得到这个数值,然后这条指令应该是把这个数,保存到某个通用寄存器当中去,这样后续的程序也就可以对这个数进行操作了,这就完成了一个输入的动作。

6B849992-0916-404D-8C7F-0BD26F459A3D.png

那现在CPU执行到某一条指令,是想点亮这8个灯泡当中的某几个,那这条指令就会在地址总线上送出1110,然后在控制总线上送出写的控制信号,与此同时,还需要在数据总线上,送出要写的数据,这个输入输出设备,就会根据控制总线发现是一次写操作,就找到地址总线上的信号对应的单元,并将数据总线上对应的信号写进去,于是1110这个单元,就被写入了11001100这个数,然后这个单元的输出,就直接通过物理的连线,连接到8个小灯泡上。

用于输入输出的,可以是拨码开关、LED管,这样的的简单的设备,也可能是比较复杂的设备,像打印机、硬盘,那现在的计算机系统当中,输入输出设备变得越来越多,功能也非常的丰富。这些设备的差异非常大,有些设备要求很高的数据传输率,比如说显示器,有些设备的速度却很慢,比如键盘和鼠标,而且有一些接口是串行的,有一些是并行的,有数字电路的接口,也有模拟电路的接口,如此千差万别的设备,就没有办法直接和CPU这一个芯片进行连接,因此我们就需要在CPU和这些设备之间,设置一个中转站, 这就是输入输出接口,也被称为I/O接口

I/O接口主要提供了这些功能,1是数据缓冲,用于解决高速的CPU和低速的外设之间的差距。第2是提供联络信息,比如打印机什么时候能够接收数据。第3是提供格式上的转换,比如模拟信号和数字信号之间的转换,串行信号和并行信号之间的转换,不同电平之间的转换。第4,一个接口可能连接了多个设备,比如说有多个硬盘,那这个I/O接口还需要提供设备选择的功能。

F91DFDA2-97C6-49F5-A2B6-3CF664698A5F.png

I/O接口, 这可能是插在计算机主板上的一块插卡,也有可能是主板上的一个芯片,它内部会有一些寄存器,CPU可以通过系统总线,去访问I/O接口当中的这些寄存器,而这个I/O接口芯片,还会有一些管角,与外部的设备相连。在现代的计算机当中,这种并行接口电路,算是最简单的I/O接口了,它和许多其它更为复杂的I/O接口,都会在集成在南桥芯片当中,而还有少数对性能要求比较高的接口,则会采用独立的芯片,或者板卡的形式,而在一些紧凑型的设备中,比如说平板电脑和智能手机,这些I/O接口甚至会和CPU一起,集成在一个芯片当中。那不管是哪种形式,这些I/O接口的功能都是独立存在的,而且它们也需要各自的管角、连线,与对应的外设相连,从而让CPU可以与外部进行交互。

在系统当中通常会有多个I/O接口,每个I/O接口内部都有若干个寄存器,这些寄存器一般被称为I/O端口,这个端口指的并不是我们计算机上的USB接口、网线接口这样实实在在的接口,而是一个抽象的概念,它实际上指的就是这些在I/O接口芯片内部的寄存器它们就像在存储器当中的一个个存储单元一样,CPU要访问它们,就得有特定的地址,因此每个寄存器,也就是每个I/O端口都需要有自己的地址,这称为端口地址,也叫做端口号。那在计算机系统中,如何去设定这些端口号就称为I/O端口的编址方式。

常见的I/O端口编址方式有两种:第一种是I/O端口和存储器分开编址,又被称为I/O映像的方式,x86体系结构就采用了这种方式。另一种常见的方式是I/O端口和存储器统一编址,又称为存储器映像的I/O方式。ARM、MIPS、PowerPC等体系结构都采用了这样的方式。

56E48E53-19AA-4F68-80A9-0CA8383F059F.png

那我们先来看分开编址的方式。我们假设这个体系结构地址的宽度为3,那它一共可以访问的地址单元就是2的三次方,总共8个,如果每个单元是一个字节,那它的存储器最大就是8个字节,然后我们需要在这个计算机系统当中增加一些I/O端口,那I/O端口的地址是重新编排的,和存储器地址无关。一般情况下,我们需要的I/O端口的数量都比存储器单元要少得多,比如在这个事例的系统当中,我们需要四个I/O端口,那我们就给它分配四个端口号,0、1、2、3,这样的编址方式就称为分开编址。在这种编址方式下,要访问I/O端口需要用特殊的指令,x86提供了IN和OUT这两条指令,IN指令用于把I/O端口的内容输入到CPU当中的寄存器, 而OUT指令则是把CPU寄存器当中的内容输出到I/O端口中。

分开编制的方式存储器和IO接口的端口号会有地址重复情况,所以单凭这个地址,系统总线是无法判定要访问哪个设备的。因此,CPU发出的信号中,除了地址,还应该有一个别的信号,这个信号指明了当天要访问的是存储器还是I/O接口。在x86的CPU当中,这个信号叫做M/IO,当这个信号为0的时候,表明当前在访问I/O接口,而这个信号为1的时候,表明在 访问存储器。

在系统总线上采样到这个地址和一个数据之后,就会在内部找到对应的端口号。我们要注意的一点是,这些I/O接口内部一般只有少数几个端口,所以它只会采样地址的低几位,然后用这个低位在内部进行索引。在这个例子当中,21H的这个2是系统总线用来找到这个I/O接口的,而这个I/O接口只用接收到地址的低位这个1,然后在内部找到对应的端口就可以了。最后这个I/O接口将从数据总线上采样到数据,也就是AL寄存器当中的内容,保存到它内部的数据输出寄存器中,这就完成了这条OUT指令所需的操作。

I/O端口和存储器统一编址的情况。还是假设地址宽度为3,那在这个统一编址的体系结构当中,总共就只有8个单元,然后根据需要,其中有一部分用来作为I/O端口的地址, 其他部分用来作为存储单元的地址。

18791F2B-192A-4950-B30A-BCFE90124768.png

中断控制方式

9Y8H37N)6LI9`CMRLJLY6ZO.png

V@8PA9{@O~NEFH1N(TJVH4D.png

KAS0B6CKNCCT~8_@4XKQO28.png

D{)X0$OUKDL_LU9EAE2OTLN.png

直接存储访问方式 DMA

现在的计算机中有很多复杂的外设比如像显示器,网络,硬盘,这些外设需要传输的数据量很大,而且对传输的速率也有很高的要求,如果这些数据都要靠CPU一个一个来搬运的话,那恐怕就难以应对了,所以这就需要用到DMA这种IO控制方式。DMA就是直接存储器访问的简称。

那如果采用DMA方式进行I/O数据的传送在传送的过程中是不需要CPU干预的,这个数据传送的工作是由一个专门的硬件电路控制,可以直接将外设的数据传到存储器或者将存储器中的数据传到外设,而这个专门的硬件控制电路就称为DMA控制器,简称为DMAC,其实DMA控制器本身也是一个I/O接口,和其他I/O接口类似,它早期也是采用独立芯片的形式, 而现在通常是寄存在其他多功能的芯片当中。

如图m和s这两种标记,m是master的缩写,表示这个部件可以在系统总线上主动发起传输,比如CPU就是这样的部件,它可以在系统总线上主动发起读写的传输。而s是slave的缩写,它表示这个部件只能被动地接受来自系统总线的传输。所以CPU在一次配置完之后,后续的工作都由DMA控制器的内部硬件自动完成,不再需要CPU的干预了。这就是所谓直接存储器访问的含义。

随着计算机的发展有一些I/O接口的速度越来越快,对DMA传输的要求也越来越高。那多个I/O接口共享一个独立的DMA控制器的方式可能就没有办法满足部分I/O接口的需求了。这时就出现了自带DMA控制器的I/O接口,那这样的I/O接口内部带有一个专属的DMA控制器,只为这个I/O接口提供服务,那这个I/O接口现在也有了master的总线接口。那在显卡、网卡、硬盘控制器这些对传输力要求很高的I/O接口中一般都会自带DMA控制器,那在系统初始化时,CPU要配置好各个DMA控制器,然后外设有传输需求时这些DMA控制器就可以自动地开始工作了。

大部分对数据传输率有比较高要求的设备都会自带DMA控制器,而其他对数据传输率要求比较低的设备则可以共享系统中独立的DMA控制器另外这个独立的DMA控制器一般还会提供从内存到内存传送的服务。那当我们编程时需要将内存中的一大块数据复制到内存的另一个区域的时候,虽然不涉及输入输出,但是也可以享受到DMA带来了好处。也不是所有的输入输出设备都需要使用DMA的方式。毕竟增加一个DMA控制器需要增加制造的成本,而且CPU来配置DMA控制器以及进行后续的处理还是要靠执行程序来完成的,也都需要花时间。如果要传输的数据量很小,性能反而会变差了。