3、CPU子系统
主要讲授CPU的结构和发展历程,指令系统,MIPS32指令集,单周期和多周期MIPS32处理器设计,提升CPU性能的一些高级技术,如多核技术等。
Cpu子系统用于控制cgroup中所有进程可以使用的cpu时间片。附加了cpu子系统的hierarchy下面建立的cgroup的目录下都有一个cpu.shares的文件,对其写入整数值可以控制该cgroup获得的时间片。例如:在两个cgroup中都将cpu.shares设定为1的任务将有相同的CPU时间,但在cgroup中将cpu.shares设定为2的任务可使用的CPU时间是在cgroup中将cpu.shares设定为1的任务可使用的CPU时间的两倍。
cpu子系统是通过Linux CFS调度器实现的。所以在介绍cpu子系统之前,先简单说一下CFS调度器。按照作者Ingo Molnar的说法:“CFS百分之八十的工作可以用一句话概括:CFS在真实的硬件上模拟了完全理想的多任务处理器”。在“完全理想的多任务处理器”下,每个进程都能同时获得CPU的执行时间。当系统中有两个进程时,CPU的计算时间被分成两份,每个进程获得50%。然而在实际的硬件上,当一个进程占用CPU时,其他进程就必须等待。所以CFS将惩罚当前进程,使其它进程能够在下次调度时尽可能取代当前进程。最终实现所有进程的公平调度。
CFS调度器将所有状态为RUNABLE的进程都插入红黑树。在每个调度点,CFS调度器都会选择红黑树的最左边的叶子节点作为下一个将获得CPU的进程。那红黑树的键值是怎么计算的呢?红黑树的键值是进程所谓的虚拟运行时间。一个进程的虚拟运行时间是进程时间按整个红黑树中所有的进程数量normalized的结果
每次tick中断,CFS调度器都要更新进程的虚拟运行时间,然后调整当前进程在红黑树中的位置,调整完成后如果发现当前进程不再是最左边的叶子,就标记need_resched标志,中断返回时就会调用scheduler()完成进程的切换。
最后再说一下,进程的优先级和进程虚拟运行时间的关系。前面提到了,每次tick中断,CFS调度器都要更新进程的虚拟运行时间。那这个时间是怎么计算的呢?CFS首先计算出进程的运行时间delta_exec,然后计算normalized后的delta_exec_weighted,最后再将delta_exec_weighted加到进程的虚拟运行时间上。跟进程优先级有关的就是delta_exec_weighted,delta_exec_weighted=delta_exec_weighted * NICE_O_LOAD / se->load,其中NICE_O_LOAD是个常量,而se->load跟进程的nice值成反比,因此进程优先级越高则se->load越大,则计算出来的delta_exec_weighted越小,这样进程优先级高的进程就可以获得更多的cpu时间。
介绍完CFS调度器,我们开始介绍cpu子系统是如何通过CFS调度器实现的。CFS调度器不仅支持基于进程的调度,还支持基于进程组的组调度。CFS中定义了一个task_group的数据结构来管理组调度。
struct task_group { struct cgroup_subsys_state css; #ifdef CONFIG_FAIR_GROUP_SCHED /* schedulable entities of this group on each cpu */ struct sched_entity **se; /* runqueue "owned" by this group on each cpu */ struct cfs_rq **cfs_rq; unsigned long shares; atomic_t load_weight; #endif #ifdef CONFIG_RT_GROUP_SCHED struct sched_rt_entity **rt_se; struct rt_rq **rt_rq; struct rt_bandwidth rt_bandwidth; #endif struct rcu_head rcu; struct list_head list; struct task_group *parent; struct list_head siblings; struct list_head children; #ifdef CONFIG_SCHED_AUTOGROUP struct autogroup *autogroup; #endif struct cfs_bandwidth cfs_bandwidth; };
task_group中内嵌了一个cgroup_subsys_state,也就是说进程可以通过cgroup_subsys_state来获取它所在的task_group,同样地cgroup也可以通过cgroup_subsys_state来获取它对应的task_group,因此进程和cgroup都存在了一组cgroup_subsys_state指针。
struct sched_entity **se是一个指针数组,存了一组指向该task_group在每个cpu的调度实体。
struct cfs_rq **cfs_rq也是一个指针数组,存在一组指向该task_group在每个cpu上所拥有的一个可调度的进程队列。
parent、sibling和children三个指针负责将task_group连成一棵树,这个跟cgroup树类似。
有了这个数据结构,我们来看CFS在调度的时候是怎么处理进程组的。我们还是从CFS对tick中断的处理开始。
CFS对tick中断的处理在task_tick_fair中进行,在task_tick_fair中有:
for_each_sched_entity(se) { cfs_rq = cfs_rq_of(se); entity_tick(cfs_rq, se, queued); }
首先看下在组调度的情况下,for_each_sched_entity是怎么定义的:
#define for_each_sched_entity(se) \ for (;se;se = se->parent)
即从当前进程的se开始,沿着task_group树从下到上对se调用entity_tick,即更新各个se的虚拟运行时间。
在非组调度情况下:
#define for_each_sched_entity(se) \ for (; se; se = NULL)
即只会对当前se做处理。CFS处理完tick中断后,如果有必要就会进行调度,CFS调度时通过pick_next_task_fair函数选择下一个运行的进程的。在pick_next_task_fair中有:
do { se = pick_next_entity(cfs_rq); set_next_entity(cfs_rq, se); cfs_rq = group_cfs_rq(se); } while (cfs_rq);
在这个循环中,首先从当前的队列选一个se,这个跟非组调度一样的(红黑树最左边的节点),再将se设置成下一个运行的se,再从该se获取该se对应的task_group拥有的cfs_rq(如果该se对应一个进程而非一个task_group的话,cfs_rq会变成NULL),继续这个过程直到cfs_rq为空,即当se对应的是一个进程。
简而言之,同一层的task_group跟进程被当成同样的调度实体来选择,当被选到的是task_group时,则对task_group的孩子节点重复这个过程,直到选到一个运行的进程。因此当设置一个cgroup的shares值时,该cgroup当做一个整体和剩下的进程或其他cgroup分享cpu时间。比如,我在根cgroup下建立cgroup A,将其shares值设为1024,再建立cgroup B,将其shares值设为2048,再将一些进程分别加入到这两个cgroup中,则长期调度的结果应该是A:B:C = 1:2:1(C是系统中未加入到A或B的进程)。
引起CFS调度的除了tick中断外,还有就是有新的进程加入可运行队列这种情况。CFS处理这个情况的函数时enqueue_task_fair,在enqueue_task_fair中有:
for_each_sched_entity(se) { if (se->on_rq) break; cfs_rq = cfs_rq_of(se); enqueue_entity(cfs_rq, se, flags); flags = ENQUEUE_WAKEUP; }
我们前面已经看过for_each_sched_entity在组调度下的定义了,这里是将当前se和se的直系祖先节点都加入到红黑树,而在非组调度情况下,只需要将当前se本身加入即可。造成这种差异的原因,在于在pick_next_task_fair中选择se时,是从上往下的,如果一个se的祖先节点不在红黑树中,它永远都不会被选中。而在非组调度的情况下,se之间并没有父子关系,所有se都是平等独立,在pick_next_task_fair,第一次选中的肯定就是进程,不需要向下迭代。
类似的处理还发生在讲一个se出列(dequeue_task_fair)和put_prev_task_fair中。
以上是cpu系统通过CFS调度器实现以cgroup为单位的cpu时间片分享,下面我们来看一下cpu子系统本身。cpu子系统通过一个cgroup_subsys结构体来管理:
struct cgroup_subsys cpu_cgroup_subsys = { .name = "cpu", .create = cpu_cgroup_create, .destroy = cpu_cgroup_destroy, .can_attach = cpu_cgroup_can_attach, .attach = cpu_cgroup_attach, .exit = cpu_cgroup_exit, .populate = cpu_cgroup_populate, .subsys_id = cpu_cgroup_subsys_id, .early_init = 1, };
cpu_cgroup_subsys其实是对抽象的cgroup_subsys的实现,其中的函数指针指向了特定于CPU子系统的实现。这里再说一下,cgroups的整体设计。当用户使用cgroup文件系统,创建Cgroup的时候,会调用cgroup目录操作的mkdir指针指向的函数,该函数调用了cgroup_create,而cgroup_create会根据该cgroup关联的子系统,分别调用对应的子系统实现的create指针指向的函数。即做了两次转换,一次从系统调用命令到cgroup文件系统,另一次从cgroup文件系统到特定的子系统实现。
cgroup中除了通用的控制文件外,每个子系统还有自己的控制文件,子系统也是通过cftype来管理这些控制文件。cpu子系统很重要的一个文件就是cpu.shares文件,以为就是通过这个文件的数值来调节Cgroup所占用的cpu时间。shares文件对应的cftype结构为:
#ifdef CONFIG_FAIR_GROUP_SCHED { .name = "shares", .read_u64 = cpu_shares_read_u64, .write_u64 = cpu_shares_write_u64, }, #endif
当对cgroup目录下的文件进行操作时,该结构体重定义的函数指针指向的函数就会被调用,下面我们就再看看这两个函数的实现,从而发现shares文件的值时如何起作用的。
static u64 cpu_shares_read_u64(struct cgroup *cgrp, struct cftype *cft) { struct task_group *tg = cgroup_tg(cgrp); return (u64) scale_load_down(tg->shares); }
比较简单,简单的读取task_group中存储的shares就行了
static int cpu_shares_write_u64(struct cgroup *cgrp, struct cftype *cftype, u64 shareval) { return sched_group_set_shares(cgroup_tg(cgrp), scale_load(shareval)); }
设定cgroup对应的task_group的shares值。
那这个shares值是怎么起作用的呢?在sched_group_set_shares中有:
tg->shares = shares; for_each_possible_cpu(i) { cfs_rq_set_shares(tg->cfs_rq[i], 0); set_se_shares(tg->se[i], shares); }
cfs_rq_set_shares强制做一次cpu SMP负载均衡。真正起作用的是在set_se_shares中,它调用了
__set_se_shares,在__set_se_shares中有:
se->load.weight = shares; se->load.inv_weight = 0;
根据之前我们分析的CFS的调度原理可以知道,load.weight的值越大,算出来的虚拟运行时间越小,进程能使用的cpu时间越多。这样一来,shares值最终就是通过调度实体的load值来起作用的。
4、存储子系统
主要讲授存储系统的层次和存储器的类型及技术指标,半导体存储原理及存储器,磁表面存储原理及磁盘,光存储原理及器件,存储系统性能的改进措施。
第一节 存储器概述
什么是存储器
存储器(Memory)是计算机系统中的记忆设备,用来存放程序和数据。计算机中的全部信息,包括输入的原始数据、计算机程序、中间运行结果和最终运行结果都保存在存储器中。它根据控制器指定的位置存入和取出信息。
存储器的构成
构成存储器的存储介质,目前主要采用半导体器件和磁性材料。存储器中最小的存储单位就是一个双稳态半导体电路或一个CMOS晶体管或磁性材料的存储元,它可存储一个二进制代码。由若干个存储元组成一个存储单元,然后再由许多存储单元组成一个存储器。一个存储器包含许多存储单元,每个存储单元可存放一个字节。每个存储单元的位置都有一个编号,即地址,一般用十六进制表示。一个存储器中所有存储单元可存放数据的总和称为它的存储容量。假设一个存储器的地址码由20位二进制数(即5位十六进制数)组成,则可表示220,即1M个存储单元地址。每个存储单元存放一个字节,则该存储器的存储容量为1KB。
存储器的分类
按存储介质分
- 半导体存储器:用半导体器件组成的存储器。U盘是半导体存储器,U盘内集成的是Flash芯片,存储介质为半导体。
- 磁表面存储器:用磁性材料做成的存储器。
按存储方式分
- 随机存储器:任何存储单元的内容都能被随机存取,且存取时间和存储单元的物理位置无关。
- 顺序存储器:只能按某种顺序来存取,存取时间和存储单元的物理位置有关。
按存储器的读写功能分
- 只读存储器(ROM):存储的内容是固定不变的,只能读出而不能写入的半导体存储器。
- 随机读写存储器(RAM):既能读出又能写入的半导体存储器。
按信息的可保存性分
- 非永久记忆的存储器:断电后信息即消失的存储器。
- 永久记忆性存储器:断电后仍能保存信息的存储器。
按在计算机系统中的作用分
根据存储器在计算机系统中所起的作用,可分为主存储器、辅助存储器、高速缓冲存储器、控制存储器等。为了解决对存储器要求容量大,速度快,成本低三者之间的矛盾,目前通常采用多级存储器体系结构,即使用高速缓冲存储器、主存储器和外存储器。
存储器的层次结构
在一个过程与 SPI 管理器联接之前,当前存储器环境是上层执行器环境,所以所有由过程自身通过 palloc/repalloc 或通过 SPI 应用函数在联接到 SPI 管理器之前分配的存储器都在这个环境里.
按照与CPU的接近程度,存储器分为内存储器与外存储器,简称内存与外存。内存储器又常称为主存储器(简称主存),属于主机的组成部分;外存储器又常称为辅助存储器(简称辅存),属于外部设备。CPU不能像访问内存那样,直接访问外存,外存要与CPU或I/O设备进行数据传输,必须通过内存进行。在 80386以上的高档微机中,还配置了高速缓冲存储器(cache),这时内存包括主存与高速缓存两部分。对于低档微机,主存即为内存。
存储器的性能指标
1.存储容量:存储字数×字长
2.单位成本:每位价格=总成本/总容量
3.存储速度:数据传输率=数据的宽带/存储周期
存储周期=存取时间+恢复时间
把存储器分为几个层次主要基于下述原因:
1.合理解决速度与成本的矛盾,以得到较高的性能价格比。半导体存储器速度快,但价格高,容量不宜做得很大,因此仅用作与CPU频繁交流信息的内存储器。磁盘存储器价格较便宜,可以把容量做得很大,但存取速度较慢,因此用作存取次数较少,且需存放大量程序、原始数据(许多程序和数据是暂时不参加运算的)和运行结果的外存储器。计算机在执行某项任务时,仅将与此有关的程序和原始数据从磁盘上调入容量较小的内存,通过CPU与内存进行高速的数据处理,然后将最终结果通过内存再写入磁盘。这样的配置价格适中,综合存取速度则较快。
为解决高速的CPU与速度相对较慢的主存的矛盾,还可使用高速缓存。它采用速度很快、价格更高的半导体静态存储器,甚至与微处理器做在一起,存放当前使用最频繁的指令和数据。当CPU从内存中读取指令与数据时,将同时访问高速缓存与主存。如果所需内容在高速缓存中,就能立即获取;如没有,再从主存中读取。高速缓存中的内容是根据实际情况及时更换的。这样,通过增加少量成本即可获得很高的速度。
2.使用磁盘作为外存,不仅价格便宜,可以把存储容量做得很大,而且在断电时它所存放的信息也不丢失,可以长久保存,且复制、携带都很方便。
存储器管理
服务器在存储器环境按这样的方法分配存储器:在某个环境分配的存储器可以被环境析构器释放而不会影响其他环境中分配的存储器.所有存储器分配(通过 palloc 等)都被当作在当前环境的区域中分配存储器.如果你试图释放(或再分配)不在当前环境的存储器,你将得到不可预料的结果.创建存储器环境和切换存储器环境是 SPI 管理器中存储器管理器的任务.SPI 过程处理两种存储器环境:上层执行器存储器环境和过程存储器环境(如果已联接).在一个过程与 SPI 管理器联接之前,当前存储器环境是上层执行器环境,所以所有由过程自身通过 palloc/repalloc 或通过 SPI 应用函数在联接到 SPI 管理器之前分配的存储器都在这个环境里.在进行 SPI_connect 调用之后,当前环境是过程自身所有的.通过 palloc/repalloc 或通过 SPI 应用函数分配的存储器(除了 SPI_copytuple,SPI_modifytuple,SPI_palloc 和 SPI_repalloc 以外)都在这个环境中分配.当进程与 SPI 管理器断开(通过调用 SPI_finish)后,当前环境恢复为上层执行器环境并且所有在过程存储器环境分配的存储器都被释放,并且不可继续使用!如果你想返回一些东西给上层执行器,那么你必须为此在上层环境分配一片存储器!SPI 不能自动释放在上层执行器环境里分配的存储器!SPI 在查询完成后自动释放查询执行期间的存储器分配!
数码相机存储器
是一张数码存储卡,可以是活动的,也可以是固定的,用于保存图像和视频。
CF闪存卡
一种紧凑型闪存卡,(Compact Flash Card)。像PC卡那样插入数码相机,它可用适配器,(又称转接卡),使之适应标准的PC卡阅读器或其他的PC卡设备。CF存储卡的部分结构采用强化玻璃及金属外壳,CF存储卡采用Standard ATA/IDE接口界面,配备有专门的PCMCIA适配器(转接卡),笔记本电脑的用户可直接在PCMCIA插槽上使用,使数据很容易在数码相机与电脑之间传递。
SD闪存卡
即SecureDigital Card(加密数字卡), 尺寸大小为:32mm×24mm×2.1mm ,存储的速度快,体积小巧,目前市面上较多数数码相机使用这种格式的存储卡,市场占有率较高。
Micro SD卡(TF卡)
Micro SD卡又称TF卡,是更小的SD卡,尺寸大小为:15mm×11mm×1mm,也能以转接器来连接于SD卡插槽中使用。目前市面上较多应用在网络监控摄像头以及一些小型便携设备中。
嵌入式应用中存储器类型的选择技巧
存储器的类型将决定整个嵌入式系统的操作和性能,因此存储器的选择是一个非常重要的决策。无论系统是采用电池供电还是由市电供电,应用需求将决定存储器的类型(易失性或非易失性)以及使用目的(存储代码、数据或者两者兼有)。另外,在选择过程中,存储器的尺寸和成本也是需要考虑的重要因素。对于较小的系统,微控制器自带的存储器就有可能满足系统要求,而较大的系统可能要求增加外部存储器。为嵌入式系统选择存储器类型时,需要考虑一些设计参数,包括微控制器的选择、电压范围、电池寿命、读写速度、存储器尺寸、存储器的特性、擦除/写入的耐久性以及系统总成本。
第二节 主存储器
计算机的主存储器是指ROM和RAM,主存储器是计算机硬件的一个重要部件,其作用是存放指令和数据,可分为只读存储器【ROM】和随机存储器【RAM】两大类。
主存储器(Main memory)简称主存。是计算机硬件的一个重要部件,其作用是存放指令和数据,并能由中央处理器(CPU)直接随机存取。
主存与cpu联系
cpu来读写主存,如果执行读操作,cpu首先将内存单元地址送入MAR地址寄存器,然后由MAR将地址送到地址总线,然后cpu给出读命令,主存接受到读命令,根据地址总线地址取出相应内存单元数据放入数据总线,送至MDR数据寄存器,然后由cpu决定MDR中数据去向。
主存中存储单元分配
大端方式与小端方式 大端高位在低地址,小端高位在高地址
主存技术指标
- 存储容量 :主存中存放二进制代码的总位数 存储容量=存储单元数*存储字长
- 存储速度:存取时间 存储器的访问时间 读出时间 写入时间
- 存取周期 :连续两次独立的存储器操作所需的最小间隔时间
- 存储器带宽 :位/秒
随机存取存储器(RAM)既可向指定单元存入信息又可从指定单元读出信息。任何RAM中存储的信息在断电后均会丢失,所以RAM是易失性存储器。
静态RAM SRAM
静态RAM基本电路
静态RAM使用触发器来存储信息,读出后信息不丢失,但断电会丢失。
读操作
行地址选择,使得T5,T6打开 列地址选择,使得T7,T8打开 读选择有效 VA由T6,T8,读放输出到Dout
写操作
行地址选择,使得T5,T6打开 列地址选择,使得T7,T8打开 写选择有效,数据由DIn输入 原数据由T8,T6写入A 原数据非由T7,T5写入A非,完成写入操作
动态RAM DRAM
使用电容存储电荷来存储信息,有电荷1,无电荷0,基本单元电路有单体式和三管式。
对于三管式,预充电信号使T4导通,读选择线有效则T2导通,此时如果Cg中有电荷,则T1导通,VDD,T2,T1,Cg,地开始放电,则读数据线此时为低电平;如果Cg没有电荷,则读数据线为高电平。因此Cg中有电荷输出为0,无电荷输出为1;写入时,T3导通,写数据线给Cg充电。对于单管式,根据数据线有无电流来决定01.输出时字线有效,则Cs如果有电荷则数据线显示高电平,如果无电荷则为低电平。
三管动态RAM Intel 1103读
给定行地址与列地址,选择指定的存储单元,然后由读写控制电路将数据输出到D。
三管动态RAM Intel 1103写
行地址,列地址 数据D 读写控制电路 列单元 数据写入
单管动态RAM 4116 结构
4116 读
给出行地址和列地址,某一指定单元电平由度放大器,读写线,IO缓冲 输出驱动输出到Dout.
4116 写
给出行列地址,导通某一存储单元,数据由Din,数据输入,IO缓冲,读写数据线,来决定对于指定单元的电容充放电。
动态RAM刷新问题
动态RAM使用电容存储电荷来存储信息,电容随着时间推移,电荷会慢慢流逝,因此需要定时刷新。
刷新实质:读出原信息,刷新放大器形成原信息,写入原信息
刷新周期,再生周期:在指定时间内,将动态RAM所有基本单元电路都刷新一次。
刷新方式:集中刷新 分散刷新 异步刷新
刷新是按行进行
集中刷新
在一个刷新周期内,对所有存储单元进行集中按行刷新,此时停止读写操作。
分散刷新
每行存储单元的刷新分散到某个存储周期,一个存储周期 前一段负责读写,后一段负责刷新。
RAM和DRAM比较
静态RAM速度要快,集成度低,价格高,一般用作缓存;动态RAM一般用作主存。
只读存储器(ROM)以非破坏性读出方式工作,只能读出无法写入信息。信息一旦写入后就固定下来,即使切断电源,信息也不会丢失,所以又称为固定存储器。ROM所存数据通常是装入整机前写入的,整机工作过程中只能读出,不像随机存储器能快速方便地改写存储内容。ROM所存数据稳定 ,断电后所存数据也不会改变,并且结构较简单,使用方便,因而常用于存储各种固定程序和数据。
MROM 只读
行列选择线交叉处有MOS管为1,无MOS管为0
PROM 一次性编程
EPROM
可编程的只读存储器(PROM)一次性写入后只能读不能修改。
可擦除可编程的只读存储器(EP ROM )用紫外线擦除内容的PROM,擦除后可再写入内容。
可电擦除的可编程只读存储器(E2 PROM)用电擦除。
按信息的保存性是否长久来分:
- 易失性存储器:断电后,存储信息即消失的存储器。
- 非易失性存储器:断电后信息仍然保存的存储器。RAM属于易失性存储器 ROM、PROM、EP ROM、 E2 PROM 属于非易失性存储器。
性能指标:
存储容量:指主存所能容纳的二进制信息总量。
第三节 主存储器与CPU的连接
主存储器(简化结构)
主存简单模型
连接原理:数据总线、地址总线、控制总线
从存储器中读出一个信息字:
- 首先CPU把这个信息字的地址送到MAR
- 然后经过地址总线到主存
- 在通过控制总线发出读命令
- 主存接到读命令后,就知道把这个地址的数据读出
- 根据CPU决定将数据送到哪,通过数据总线
写一个数据到主存:
- CPU要把信息字所在的主存单元的地址经过MAR送到地址总线
- 然后在把这个信息字送到MDR
- 然后向主存发送一个写命令
- 主存收到写命令后
- 把数据总线上的信息写到相应地址线指出的储存单元中
主存地址单元分配(比较重要)
主存地址分配
总容量1KB按字节寻址:1K个单元,每个单元1B,1K=1024,十根地址线按字寻址:256个单元,每个单元4B,高位看成组号,后面跟两位按半子寻址:512个单元,每个单元2B按双字寻址:128个单元,每个单元8B
如何存放一个字?如12345678H
- 大端方式:高位字(12)节作为字地址
- 小端方式:低位字(78)节做为字地址
主存储容量的扩展
由于单个芯片的容量总是有限的。需要在字和位两个方面扩充。位扩展法,字扩展法,字位扩展法。
这个是8K*1位,即13根地址线和1位数据线。CPU要求:8位的数据线,16位的地址线
如何进行连接?
CS是高电平有效,所以要选择到他要给个1,这样显然是不够的,cpu有8位的数据线,而存储芯片只有1位,不满足存储容量的要求。在加一块芯片:
8块芯片进行扩展
相当于并联在一起。
一个8K*8位的存储器。
字扩展
存储芯片是8K*8位的,CPU也是8位的数据线
会不知道哪一个数据,使用片选线来进行控制。
但是还是会遇到同样的问题,如果是11的话,两个的数据都会被操作了。
线选法:A14 A13只能为01或10,n条线->n个选片信号
地址:01x xxxx xxxx xxxx、10x xxxx xxxx xxxx
改进:用一个非门
地址1x xxxx xxxx xxxx、0x xxxx xxxx xxxx
译码片选法:n条线->2^n个选片信号
译码器。
译码器
高电平有效,1
低电平有效,0
位地址选8块芯片,注意圈圈;使能端、EN、还有可能有多个使能端,使译码器工作。
将译码器利用到字扩展
00,01,10,11对比
字位同时拓展
例题:
系统程序区用ROM,用户程序区用RAM;确认地址线、数据线、选择存储芯片;数据线:CPU数据线8根 -> 存储器位数应扩展为8位。
第四节 外部存储器*
一、磁盘存储器
优点:存储容量大,价格低,长期保存而不丢失。
缺点:存取速度慢,机械结构复杂,对环境要求高。
磁盘最小的读写单位是一个扇区。
二、固态存储器SSD(新增考点)
优点:读写速度快。若要写的页有数据,则不能写入,需要将块内其他页全部复制到一个新的块中,再写入新的页。
缺点:价格高,一个块被写入多次可能会坏掉(采用平均磨损,对我们来说仍然很耐用)而磁盘不会。
第五节 高速缓冲存储器(重点)*
一、什么是Cache,为什么要引入Cache?
Cache存储器也被称为高速缓冲存储器,位于CPU和主存储器之间。之所以在CPU和主存之间要加cache是因为现代的CPU频率大大提高,内存的发展已经跟不上CPU访存的速度。在2001 – 2005年间,处理器时钟频率以每年55%的速度增长,而主存的增长速度只是7%。在现在的系统中,处理器需要上百个时钟周期才能从主存中取到数据。如果没有cache,处理器在等待数据的大部分时间内将会停滞不动。
二、原理
采用了程序访问的时间局部性原理和空间局部性原理
时间局部性:如果一个数据现在被访问了,那么以后很有可能也会被访问
空间局部性:如果一个数据现在被访问了,那么它周围的数据在以后可能也会被访问
三、多级Cache的由来?
cache分为L1,L2,L3甚至L4等多级。为什么不能把L1的容量做大,不要其它的cache了?原因在于性能/功耗/面积(PPA)权衡考虑。L1 cache一般工作在CPU的时钟频率,要求的就是够快,可以在2-4时钟周期内取到数据。L2 cache相对来说是为提供更大的容量而优化的。虽然L1和L2往往都是SRAM,但构成存储单元的晶体管并不一样。L1是为了更快的速度访问而优化过的,它用了更多/更复杂/更大的晶体管,从而更加昂贵和更加耗电;L2相对来说是为提供更大的容量优化的,用了更少/更简单的晶体管,从而相对便宜和省电。在有一些CPU设计中,会用DRAM实现大容量的L3 cache。
四、如何区分Cache和主存的数据块对应关系?
每次被访问的主存块,一定会被立即调入Cache,而且是以块为单位进行调入。
那是采用什么方式将主存块号调入到Cache呢?有三种方式
①全相联映射——主存块可以放在Cache的任意位置。
那它是如何来访问主存的呢?
对以上图只要能看懂,对于全相联映射就没什么问题了。做几点说明,CPU在访问主存时,会先对比Cache所有块中的标记Tag,Tag就是在主存中的主存块号,占22位。
②直接映射——每个主存块只能放在一个特定的位置。Cache块号=主存块号%Cache块总数
做以下几点说明
- 相对于全相联映射,直接映射对Tag进行了优化,因为主存块号最后三位地址就是Cache中的位置,所以将主存块号其余位作为标记即可。
- 若Cache总块数= 2n ,则主存块号末尾n位直接反映它在Cache的位置,所以将主存块号其余位作为标志位即可。
③组相联映射——Cache块分为若干组,每个主存块可以放到特定分组中的任意一个位置。组号=主存块号%分组数
做以下几点说明
- 相对于全相联映射,直接映射对Tag进行了优化,因为主存块号最后两位地址就是Cache中的位置,所以将主存块号其余位作为标记即可。
- 一个组内有几个Cache块就成为几路相联映射
④三种映射方法对比总结
全相联 | 直接 | 组相联 | |
特点 | 任意位置 | 特定位置 | 分组中的任意位置 |
主存地址结构 | 标记+块内地址 | 标记+行号+块内地址 | 标记+组号+块内地址 |
优点 | Cache存储空间利用充分 | 对任意地址,执行对比一个Tag,速度快 | 折中办法 |
缺点 | 可以会对比所有行的标记,速度慢 | Cache空间利用不充分 | / |
五、Cache很小,而主存很大,如果Cache满了,是利用了什么替换算法?
替换条件:对于全相联映射,需要在全局中选择替换哪一块,对于直接映射,若非空,则直接替换,对于组相联,组内满了,则在组内选择替换哪一块。
Ⅰ、随机算法(RAND)
随机,随便,随意,换哪一个都行。实现简单,但完全没有考虑局部性原理,命中率低,实际效果很不稳定。
可能会导致,换出的块,下一次又需要访问。就会多次访问内存块。导致抖动现象。
Ⅱ、先进先出算法(FIFO)
替换最先进入的块。同样实现简单,但仍然没有考虑到局部性原理,最先被调入Cache块可能是被访问最频繁的。
Ⅲ、近期最少使用(LRU)
为每个Cache块设置一个”计数器“,用于记录每个Cache块多久没有被访问了。然后替换”计数器“值最大的。
- 计数器的位数=Cache块的总数= 2n ,只需要n位,且Cache装满后所有计数器的值一定不重复。
- 基于局部性原理,近期被访问的主存块,未来可能仍会被使用,LRU算法实际运行效果优秀。
- 若频繁访问的主存块数量>Cache行的数量,则有可能发生”抖动“
Ⅳ、最近不经常使用(LFU)
为每个Cache设置一个”计数器“,用于记录Cache被访问过几次,然后替换”计数器“值最小的(访问次数最少的)
曾经被经常访问的主存块不一定在未来会被用到。并没有很好的遵循局部性原理,因此实际运行效果不如LRU。
六、Cache写策略——CPU修改了Cache中的数据副本,如何确保主存中数据母本一致性?
Ⅰ、写命中——写入的时候,在Cache中
①回写法:当CPU对Cache写命中时,只修改Cache的内容,而不立即写入主存,只有当次块被换出时才写回主存。减少了访存次数,但存在数据不一致的隐患。
被换出时,看”脏位“是否知道是否被修改。
②全写法:当CPU对Cache写命中时,必须把数据同时写入Cache和主存,一般使用写缓冲。访存次数增加,速度变慢,但是能保证数据的一致性。无脏位。
Ⅱ、写不命中——写入的时候,不在Cache中
①写分配法——当CPU对Cache不命中时,把主存中的块调入Cache,在Cache中修改。通常搭配回写法使用,改完后要被换出,才在主存中修改。
②非写分配法——当CPU对Cache写不命中时,只写入主存,不调入Cache,搭配全写法使用。
第六节 虚拟存储器*
虚拟存储器:在操作系统的管理下,只把当前需要的部分数据调入主存,暂不需要的部分留在辅存中。在用户看来,似乎获得了一个超大的主存。(虚拟性)
一、页式虚拟存储器
背景:CPU执行的机器指令中,使用的是”逻辑地址“,因此需要通过”页表“将逻辑地址转为物理地址。
一个程序在逻辑上被分为若干个大小相等的”页面“,”页面“大小与”块“的大小相同。每个页面可以离散的存放在不同主存块中。
页表的作用:记录了每个逻辑页面存放在哪个主存块中。
无快表:
- 逻辑地址=逻辑页号+页内地址
- 物理地址=主存块号+页内地址
增加快表(存放在Cache中,先访问快表,若未命中,则去访问主存中的慢表)
- 快表查询速度很快,若快表中无,则会去慢表中查找,会把相应的内容存入快表中
清楚整个查找流程
二、段式虚拟存储(按功能拆分成大小不同的模块)
按照功能模块拆分不同的模块大小。
虚拟地址:段号+段内地址
优点:段的分界与程序的自然分界相对应,因而具有逻辑独立性,使得它易于编译、管理、修改和保护。
缺点:段的长度可变,分配空间不便,容易留下碎片,不好利用,造成浪费。
三、段页式虚拟存储
把程序按逻辑结构分段,每段在分固定大小的页,主存空间也划分为大小相等的页,每个程序对应一个段表,每段对应一个页表。
虚拟地址:段号+段内地址+页内地址
优点是兼具段式和页式的优点缺点是需要查两次表,系统开销较大。
四、虚拟存储器与Cache的比较
Cache | 虚拟存储器 |
解决CPU与主存速度不匹配的问题 | 解决主存容量的问题 |
全由硬件组成,对所有程序员透明 | 由OS和硬件组成,逻辑上存储器对系统程序员不透明 |
不命中影响小 | 不命中影响大 |
不命中时,主存直接与CPU通信 | 不命中时,不能直接和CPU通信,要先硬盘调入主存 |
题目总结:
【2015统考真题】假定主存地址为32位,按字节编址,主存和Cache之间采用直接映射方式,主存块大小为4个字,每字32位,采用回写方式,则能存放4K字数据的Cache的总容量的位数至少是()
- Cache的容量分为两个部分一个是数据存储容量+标记阵列容量
- 标记阵列中一定包含有效位和标记位,若为回写法,则还存在一位的”脏位“,若为LRU、LFU替换算法,则还存在替换算法位(计数器)位数为 log2n ,n为Cache的个数。
- 本题按照字节编址,则块内地址占4位,采用直接映射方法中的标志位为32-4-10=18,Tag=18。
- 采用回写法,有一位脏位,故最终标记项有18+1+1=20
- 标记阵列容量为 210 ×20=20K,数据储存容量为4K×32=128K,故总的为148K。