北京大学 Computer Organization 学习笔记(二)

由于文章篇幅过长,无法直接发布。接上文 北京大学 Computer Organization 学习笔记(一)

流水线处理器

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

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去取出指令。在那里会执行两条修复指令。

image

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计算器。

image

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

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

image

基于这样的方式,每个中断向量都由两个段基值和两个向量的偏移地址组成,还因为每一个中断向量占四个字节。 在整个中断向量表中,一共有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号中断符合的起始地址,开始对指令进行执行。这些中断服务程序在内存当中的存放顺序并没有要求。并不需要按照中断类型的顺序。

image

那对于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
image
V@8PA9{@O~NEFH1N(TJVH4D.png
D{)X0$OUKDL_LU9EAE2OTLN.png

直接存储访问方式 DMA

image

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

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

image

如图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控制器以及进行后续的处理还是要靠执行程序来完成的,也都需要花时间。如果要传输的数据量很小,性能反而会变差了。

你可能感兴趣的:(北京大学 Computer Organization 学习笔记(二))