一 CPU组成结构
CPU主要包括四个组成部分,控制器、运算器、寄存器、时钟。
1.1控制单元
控制单元主要包括取指令、分析指令和执行指令以及对返回的结果进行时序控制。它主要使用到的寄存器包括IR(指令控制器)
1.1.1 负责取指令、分析指令和执行指令
第一: 通过Mem[PC++]获取指令
会把在磁盘上的编译后的机器指令放到内存中,然后CPU开始读取指令,比如指令是8位的 LOAD_A_8。此时的指令是按照操作码和地址码存在的,比如0100 0121
第二:指令被取出放到控制器中的IR指令寄存器中
第三:控制器根据操作码分析是什么指令,这里假如是d_load_A,表示需要根据地址码从内存load数据
第四:然后根据指令中的地址码0120从内存load数据,获取的数据会放在寄存器中
1.1.2 指令的时序控制
对于乱序执行的处理器,控制器还需要控制乱序执行的指令在回写寄存器的时候顺序的控制。比如对于一个读指令,当处理器在等待数据从缓存或者主存返回的时候,它是一直处于等待状态还是继续执行别的指令?这需要根据不同的处理器来决定,如果处理器可以不用等待前面指令的结果就继续执行后面的指令,我们把这种处理器称之为乱序处理器。
乱序处理器指的是可以不同步等待执行结果而执行其他的指令,但是不代表返回的结果在寄存器中也是乱序的,这样的话程序肯定就有问题,这时候有就需要控制器通过访存的顺序将指令结果放入寄存器中。
1.1.3 中断逻辑
有时候中断源发出了中断信号,比如缺页中断或者I/O中断等等,也是通过控制器来控制的
1.2 运算器
运算器主要是对根据指令,然后对寄存中取出的数据进行运算,它主要使用的寄存器有通用寄存器和状态寄存器。通用寄存器用来存储从内存取回的数据,状态寄存器主要是程序执行时候的状态记录。
1.3 寄存器
寄存器主要包括程序计数器PC、数据寄存器(MDR)、地址寄存器(MAR)、指令寄存器(IR)、累加寄存器(AC,现在一般没有了)、PSW(程序状态字)等等;
1.4 时钟
时钟负责发出时钟信号,用于计算时间分片,时钟信号频率越高,CPU运行速度越快。
二 CPU的高速缓存
2.1 什么是高速缓存,为什么需要高速缓存?
高速缓存是位于处理器附近的SRAM芯片(静态随机存取存储), CPU可以先从缓存读取,高速缓存没有则在向主存取数据。
它的存在是因为减小CPU和主存之间的速度差异,根据时间局部性和空间局部性原理,某一时间段访问过的数据在不久之后可能还会被访问,或者某地址上的数据被访问之后紧接着还有可能继续被访问,所以高速缓存就是为了缓存这些数据,避免CPU频繁和主存交互,影响系统性能。
当前的计算机主要包括2级缓存,有的是3级缓存,那么CPU会拿着地址先从L1去找,如果没有命中再从L2找,如果还没有再向L3去找,如果L3还没有则向主存取数据。
2.2 高速缓存和内存的映射以及高速缓存的替换策略
我们知道,高速缓存是比较小的,一般的话L1只有256KB, L2只有1M,L3只有8M。那么主存是比较大的,那么肯定就会遇到缓存容量满了的问题,那么缓存容量满了,应该使用什么样的替换策略呢?这就涉及到高速缓存的映射。
2.2.1 缓存行 (Cache Line)
高速缓存和主存的交互的最小单位是缓存行,站在主存的角度看就是数据块。即意味着CPU发送读取数据的指令,从主存不是只返回该条数据,而是返回这个数据所在的数据块。然后高速缓存会将这些缓存块进行缓存。一般情况下,缓存行大小是64字节。在Linux可以通过命令查看:less /sys/devices/system/cpu/cpu0/cache/index0/coherency_line_size
那缓存行又长什么样子呢? 我们进行分析一下:
首先,我们知道缓存肯定不只是64字节,不止才一个缓存行,那样做的意义也不大,肯定是有多个的,则是以数组形式存在的,叫做缓存表CacheEntry。如果L1大小是256K,缓存行大小是64字节,那么这个缓存项就是4096大小。
其次,既然是缓存,那势必会缓存数据,所以缓存项肯定需要存储数据。
再者,高速缓存怎么知道这个缓存行是主存中什么位置呢?所以需要标识主存位置。
然后,主存的数据怎么知道哪些缓存项是空的,可以放缓存行呢? 如果瞎放,有可能把本身还有存储空间但是把已经存在的缓存行给替换了,就无法保证缓存的性能,所以需要知道哪些缓存项已经有数据了,哪些还没有。
最后,如果缓存表满了,新来的缓存如何知道应该替换哪些缓存行呢? 如果正在使用的命中率较高的缓存行,给替换了,反而命中率不高的还留着,那肯定不好啊,所以我们最好是淘汰那些发生修改的数据行,将他们刷入缓存,然后替换掉。
所以,缓存行主要是由标记、数据和有效标记位以及是否是脏数据的标记位组成。如图所示:
2.2.2 映射策略
映射只的就是高速缓存可以根据某些标记就定位到主存块,在高速缓存有三种映射策略:直接映射、全关联映射、组相联映射。
在这里映射的核心就是计算tag,不同的映射策略,那么tag值也是不一样的,比如全映射,tag值只需要记录主存块的块号(主存块的大小占据低位,剩余的高位就是需要记录的tag值);又比如直接映射,因为一个缓存行可能对应多个主存块,但是缓存号永远是不变的,那么不变的部分就是主存块大小占据的低位加上缓存行号占据的位置,剩余的部分才有可能不一样,所以只需要记录剩余不放即可。
2.2.2.1 全相连映射
全相连映射:主存块可以在缓存项中没有被占用的空行随意放置。
那么缓存控制器如何知道CPU要读取的地址在缓存行中是否存在呢? 所以需要将主存块中抛开低地址部分剩余高位部分记录在缓存行的tag中,因为缓存行和主存块都是相同的字节,所以低地址部分都是一样的,剩余的高位部分可以直接定位到具体主存块。
优点:空行的利用率高,只要有空行,就可以放缓存行
缺点:缓存行中需要记录的tag信息,即高位部分位数较多(和直接映射比)
2.2.2.2 直接映射
直接映射:主存块号可以根据某种算法可以拿和缓存行号一一对应。比如主存块号通过对缓存行号取模。
对于直接映射,缓存行号抛开低地址部分的剩余高位部分就是行号,这个行号和主存块的块号中对应的高位部分是一样的,所以这部分这部分无需记录在tag中,只需要记录主存块-地位地址部分-缓存行号部分高位即可。如图所示:
优点: Tag只需要存主存块号 – 低地址部分 – 缓存行号部分,所以存储再cache line中的额外信息比较少
缺点: 因为是直接映射,所以即使别的缓存行是空的,也不可以去存放,空行的利用率低
对于0号缓存行,行号是000,主存块对应部分也是000,那么记录在cache line中tag部分就是00000000000;对于1号缓存行号是001,主存块对应部分也是001,那么记录在cache line中tag就是00000000000;对于8号主存块,因为也会映射在第0号缓存行,主存块中缓存行号就是000,把剩余部分00000000001,记录在tag中。
那么CPU需要根据地址取数据的时候,交给缓存的时候,缓存控制器根据地址判断是否有相同的tag, 如果有再获取对应的缓存行号,去缓存里面把数据返回;如果访问的地址tag中不存在,则直接向上走,交给下一个缓存或者主存去取数据。
2.2.2.3 组相连映射
组相连映射: 按照缓存行号分组,组内随意放。它综合了全关联映射和直接映射的策略。
计算缓存行好分组,一般分组都是按照2的指数倍进行分组,这样方便分组。计划分成2^n组,那么就需要将缓存号高n位部分作为组号;需要将主存块号将紧挨着低位部分的高n位作为组号,这样将主存块组号之前的部分高位就可以作为tag放在缓存行里。如图所示:
2.2.3 淘汰策略
缓存行满了,那么此时应该如何淘汰哪一个缓存行呢? 直接映射是直接替换映射的缓存行即可,而全相联映射和分组映射需要根据一定的淘汰算法策略去淘汰或者替换存在的缓存行。
2.2.3.1 随机算法(RAND)
就是随机选一个缓存行进行淘汰,全相联可以在所有缓存行中选择一个,而组相连只能在组内随机选择一个淘汰
2.2.3.2 先进先出算法(FIFO)
先分配的缓存行先被淘汰
2.2.3.3 最近很少使用算法(LRU)
淘汰近期内长时间没有被访问过的行
2.2.3.4 最不经常使用(LFU)
淘汰使用最少的行
2.2.4 写策略
2.2.4.1 write through (写通)
写通策略:如果CPU写缓存命中,那么需要修改缓存的数据,并且写回主存。
但是因为CPU写缓存的速度远远快于写主存的速度,如果同步等待的话,会影响CPU的性能,因此这种写策略效率低。 如果使用写通策略,一般会在缓存和内存之间设置一个缓冲区,叫做写缓冲区(write buffer), write buffer一般是放在缓存这边的,而且是最外层的缓存,如果CPU只有L1那么L1就有这个write buffer,如果最外层是L2,那么L2就有这个write buffer, 如果最外层是L3,那么L3就有这个write buffer。如图所示:
当然这个还是有一定问题的,如果CPU写入速度快于write buffer处理速度,write buffer可能会溢出。
优点: 不存在缓存和主存数据一致性问题
缺点:效率低,写缓冲区还可能发生溢出的情况
2.2.4.2 write back (写回)
写回策略:也被称之为回写策略,指的是CPU写数据命中缓存,则将数据写入缓存,就认为写成功了,并不会立即把数据写回主存,至于什么时候缓存将发生修改的数据写入主存,取决于缓存行的淘汰策略和时机,比如缓存行满了,需要淘汰一个缓存行,这时候就有可能被写入主存。所以存在缓存和主存数据一致性的问题。
它的实现机制主要是通过设置脏数据标识,如果数据发生了修改,脏数据位置为1,那么这个发生了修改的缓存行在需要淘汰的时候就有可能被写入主存。如图示:
优点: 可以不用立即将数据写回主存,不影响性能,也不存在写缓冲区溢出的情况
缺点:存在缓存和主存数据一致性问题
2.2.4.3 写未命中写分配法(write allocate)
写未命中写分配法: 指的是当CPU写数据时,缓存没有命中,此时会将主存数据读取出来,放入到缓存行中;然后通过写回法,修改缓存行中的数据,CPU就认为写成功了。所以在这里和写回法比较,是多了一步先读取主存的步骤。
2.2.4.4 写未命中非写分配法(no write allocate)
写未命中非写分配法:指的是当CPU写数据时,缓存没有命中,此时直接会将数据写入主存,而不会写入高速缓存。这种策略也被称之为绕写。
一般情况下,会采用命中和未命中两者搭配的方式,比如write through + no write allocate,即命中则写入缓存,且写入主存;未命中则直接写入主存。又比如write back + write allocate, 即命中写入缓存,等待某个时机缓存脏数据写入主存;未命中则先从主存读取数据到缓存,然后再写入缓存,然后等待某个时机,缓存脏数据写入主存。
2.3 高速缓存的结构
在现代计算机中,缓存一般有三级缓存,一级缓存(L1)、二级缓存(L2)每一个核心私有,三级缓存(L3)是处于同一个插槽的所有核心共享。其中L1 又主要分为数据缓存(L1-d-cache) 和 指令缓存(L1-i-cache)。缓存级别越高,离CPU越近,容量越小,速度越快。缓存通过缓存控制器(Cache Controller) 判断是否缓存命中,如果没有命中则从下一个缓存或者主存中load数据。缓存架构图:
三 CPU原子性和可见性
3.1 原子性
3.1.1 总线锁
在多核的处理器中,可能存在不同核的线程同时并发修改内存数据的情况。所以,所以该CPU通过发送LOCK#信号,当总线传输LOCK#信号的时候,其他CPU的请求将会被阻塞,此时该CPU独占主存。
缺点:总线锁锁住了其他CPU和主存的通信,在锁定期间,导致其他CPU无法操作内存数据,所以总线锁粒度太大,导致CPU利用率低。
3.1.2 缓存锁
因为总线锁粒度太大,导致CPU利用率低,在后来CPU不再使用总线锁来保证原子性,而是通过缓存锁。CPU通过对缓存中对应地址的数据进行修改,则通过缓存一致性协议保证其他CPU持有这数据的缓存行失效,这样就保证了原子性,不会存在同时多个CPU修改数据的情况。
3.2 可见性
可见性,我们也可以理解为缓存的一致性,即当别的缓存发生了变化,如何知道别人发生了变化。
我们知道,当多核CPU某一个CPU发生了修改,修改了主存或者缓存,对于其他CPU来说是不可见的。因为CPU要读取数据首先是从缓存中读取的,如果缓存命中,则直接从缓存获取数据了,而不是从主存或者其他缓存获取数据。那如何才能实现CPU的可见性呢? CPU有两种机制可以实现可见性:基于总线的嗅探机制(Bus Snooping)和基于目录实现的缓存一致性(Directory-Based Cache Coherence).
3.2.1 总线嗅探(Bus Snooping)
我们知道,高速缓存有缓存控制器,然后用来监视总线,如果某一个CPU想要修改共享缓存的数据,它会先进行广播,其他的CPU就会嗅探到这个事务或者广播。然后他们会判断自己本地高速缓存是否也有这样的数据副本,如果没有则不用理会。
总线嗅探机制一般分为两种协议,一种是写失效协议(write-invalidate),一个核心写入缓存后,会广播一个失效请求,其他核心嗅探到则将自己缓存中对应的缓存行标记为失效; 另一种是写更新协议(write-update),写入缓存的核心除了要广播一个失效请求外,还要广播数据内容,把对应数据传输给其他CPU。如果有则通常让他们无效(write-invalidate)或者更新(write-update)。但是对于总线嗅探来说,一般都是使用write-invalidate,让这些数据无效,因为write-update会产生数据拷贝,产生总线流量。
3.2.2 基于目录实现缓存一致性(Directory-Based Cache Coherence)
在共享缓存中维护一个目录或者表格,其中记录所有缓存行,任何读写操作都需要先查询该目录是否有对应的记录。如果发生修改,则先查询有哪些行也需要失效,则直接向对应的CPU发送invalidate消息,避免在总线上广播。
四 我们的程序是怎么运行的
4.1 代码编译,编译成机器指令
4.2 执行程序,操作系统会把程序从磁盘上加载到内存
4.3 操作系统创建进程,分配进程标识符,为进程分配地址空间,初始化进程控制块,然后将进程放入到就绪队列
4.4 获取CPU执行权,开始执行程序
4.4.1 取指令阶段
4.4.1.1 获取进程的开始位置的地址,初始化CPU的程序计数器
4.4.1.2 控制单元控制PC将要执行的下一个指令发送到MAR(主存地址寄存器)
4.4.1.3 MAR将地址发送到地址总线
4.4.1.4 控制单元发出读控制信号
4.4.1.5 检查缓存
从缓存L1查询是否存在这个地址,L1发现不存在则缓存控制器回去找L2缓存,L2控制器发现没有则找L3缓存,L3没有则从主存获取
4.4.1.6 获取对应的缓存行,放入缓存中
4.4.1.7 CPU从对应的缓存行中获取对应地址的数据,然后放入MDR主存数据寄存器
4.4.1.8 然后主存数据寄存器MDR把得到的指令传给IR指令寄存器
4.4.1.9 指令寄存器IR得到指令,指令寄存器将指令操作码部分交给控制单元
4.4.1.10 然后程序计数器PC+1 准备下一个指令
4.4.2 分析指令
4.4.2.1 指令寄存器将指令操作码部分交给控制单元
4.4.2.2 控制单元译码和发出控制信号
控制单元经过指令译码器进行译码,然后发出控制信号,可能是微操作(比如ALU计算、或者寄存器拷贝)也可能是向控制总线发送的控制信号比如读、写