前一篇计算机系统006 - 硬件组件之RAM中讲完五大组件中的RAM部分,剩下最后的硬骨头CPU还未说明,本篇就试着对其进行尽可能完整的剖析。
开篇之前,放一张CPU全貌镇场。
工艺很好,封装很完整,除了一排排针脚,再看不出来其他东西,索性透过外壳,一探究竟(以Intel Haswell为例)。
说实在的,要是不标英文注释,我也不知道这里面每个区域放的是什么。不过既然有,总比没有好,这里暂不讨论商业化成熟的CPU方案所有内容,只关注如下两个部分:
SHARED L3 CACHE
如果还能记得上一期所述的存储器层级图的话,那么你应该能注意到,SHARED L3 CACHE下面一级是MEMORY CONTROLLER IO,也就是说,这一级可以视为Cache,而下一级连接的是RAM,它的上一级又是CORE。由于之前已经对Cache的来由和原理做了讲解,这里就不在赘述。CORE
CORE实际上是CPU的基础计算单元(注意这里说的是计算单元,而非控制单元或处理单元),它可以运行单个进程,维护进程状态、寄存器值、确保正确的执行次序,以及通过ALU执行操作。一个CPU通常由一至多个CORE,俗称“多核”,上图所示的CPU中就含有4个CORE,因此也称4核。
既然每个CORE可以单独运行进程,那么在预算范围内自然是越多越好。不过也应当知道,4核CPU整体性能并不能简单换算成单核乘以4,先不论软件上是否做了支持以便可以均摊在4核上跑满,单说4核相互间也必须通过L3 Cache交换信息,就可以知道在交换上会耗费一部分开销。
1. CORE
在第4篇 中介绍CPU部分时,给出了下面这张图。
从图中可以看出,CPU是由ALU、CU、Memory三部分组成,而CORE可以单独执行整个进程,且CPU从一开始也是单核形式存在,所以这里可以等同认为CORE也是由ALU、CU、Memory三部分组成。
1.1 ALU(Arithmetic logic unit)
ALU也称为算术逻辑单元,是用于执行二进制表示整型的算术和按位运算组合数字电路。ALU是许多计算电路的基础模块,与之相对的是FPU(Floating-point unit)浮点单元,FPU处理的是浮点数值。
以计算“1+2”为例,其原理如下图所示:
Integer Operand A, B
A,B均为输入运算对象,分别为 1(01),2(10)Opcode
运算符,作用于运算对象。此处为‘+’。虽然opcode与机器语言opcode在感念上有所区别,但通常会等同使用。Status(可选)
可选状态值,输入输出均为状态寄存器,在ALU进行计算时,可能需要参考其中标志如CF判断是否有进位,同时在结果中设置该flagInteger Result
整型结果,输出门电路交换后值,此处为3(11)
包括上述加法操作,ALU一共支持如下类型操作:
- 逻辑运算,包括AND、OR、NOT、XOR、NOR、NAND等
- 移位操作,左右移位
- 数学运算,位加减法,也可进一步支持乘、除法
每一个Opcode都对应着不同的电路实现,相互间存在着逻辑门、触发器等各种实现差异。而从ALU的使用原理上可以看出,要想完成计算,需如下前提条件:
- 输入端加载两个二进制整型值
- 提供运算符
- 可选地提供状态值
为了提供这三类值,就必须先和寄存器打交道。
1.2 Memory - Register
Register也称为寄存器,总的来讲,Register是Memory中SRAM的一种,优势在于快,劣势在于贵。
快的原因在于无机械部件、无电容充放电,存取的所有过程均基于电路交换,也就是说电路频率有多快,理论上他就可以做到多快。不过高频电路中无论是元器件焊点,还是布线都有可能对集成后电路整体性能和稳定性产生干扰,因此在频率上也常常有所限制。
设计寄存器时,并非所有Register都以相同电路实现,而是会以所要实现的功能为目标进行取舍。通常,寄存器可分为如下几类:
MAR(Memory Address Register)
内存地址寄存器,保存数据或指令在内存中地址,挂载在地址总线上,用于在指令执行期间获取数据或指令。例如CPU希望在内存中保存或获取内存中某一数据时,可将对应地址放在MAR中PC(Program Counter)
程序计数器,在Intel X86中也称为指令指针IP(instruction pointer),始终指向下一条要执行的指令。AC(Accumulator) Register
累加寄存器,保存CPU计算所得结果。MDR(Memory Data Register)
内存数据寄存器,与MAR类似,区别在于挂载在数据总线上。MDR就可以从数据总线加载数据,也可以存储CPU中数据,就像一个Buffer一样保存着送往解码器(见1.3小节)前的信息。
MDR是配合MAR使用的,读取某一内存地址数据前,现在MAR中存放读取地址,发送读信号,就可以在MDR中获取目标值;同样在MAR中存入目标地址,MDR中存放要写入数据后,发送写信号,即可往内存中写出数据。也就是说,MDR和MAR共同组成了CPU对内存部分的访问接口。Index Register
变址寄存器,存放从基址起要偏移的地址数,用于在程序运行过程中调整操作符地址。MBR(Memory Buffer Register)
缓存寄存器,存放已经读取或写入内存的数据或指令内容,起缓存作用。Data Register
微型计算机中用于临时存放传输给,或读取自其他外围设备数据的数据。
当然,上述寄存器大多是控制单元CU内部使用,对于用户,可见的寄存器类型为通用寄存器,以8086为例,可见的寄存器如下:
累加寄存器 | AX |
基址寄存器 | BX |
计数寄存器 | CX |
数据寄存器 | DX |
栈指针寄存器 | SP |
基址指针寄存器 | BP |
源变址寄存器 | SI |
目的编制寄存器 | DI |
事实上,每个寄存器的背后都有独立的电路实现,而不同类型的寄存器实现往往各不相同。因此,编程时并非强制让你使用某个寄存器,而是由于该寄存器设计时电路实现所限,它只支持部分功能。例如寻址时只能使用BX、BP、SI、DI,不是故意刁难,标新立异,而是因为只有这些寄存器在电路实现时对寻址做了支持。
同样,一个进程运行后,会有对应状态值,如指令指针、AF、CF、ZF等等标志位,均存储在对应寄存器中。CPU只会循环读取下一条指令地址,进行计算,因此只要在读取下一条指令地址前,将其替换成新进程的指令指针,即可完成进程切换。当然为了确保能顺利回来,还必须在替换前保存现有进程状态。
1.3 CU(Control Unit)
到现在为止,我们有了支持不同运算操作(Opcode)的ALUs,也有了不同用途存储不同数据的寄存器Registers,为了实现进一步的自动化计算,应该开始考虑控制单元CU的内部逻辑了。
计算的实质是接收任务描述,按照任务描述的步骤,完成计算,并输出结果。回到任务描述本身,用户该如何描述一个任务以使得执行计算的计算机可以理解任务内容?
想象一下现实生活中,需要将某一任务托付给别人时,我们通常需要如下步骤:
- 选择一种双方都可以理解的语言
- 按照逻辑结构提取要点,转化成纲要
- 如有必要,进一步细化纲要,形成内容句子
- 最后以文件或是口述等方式传达给对方
当对方是计算机时,上述步骤就变成:
- 选择机器语言或者可最终转化为机器语言的高级语言,如汇编、C/C++、Java、甚至Python
- 主函数中依次写明主要步骤
- 如有必要,进一步细化主要步骤,形成子函数或其他可调用对象
- 最后以可执行文件方式存储于磁盘中
从前面我们了解了ALU的运算方法和寄存器如何从内存中加载数据,因此这里只考虑寄存器、ALU、CU三者之间的交互。对于此时的CU来讲,寄存器中已经给出了可执行文件在内存中的入口地址,起点已经有了,接下来就是如何按照可执行文件内容顺利执行完整个任务。
CU就像CPU的大脑,负责所有控制行为。通常在CU内部,将计算任务执行过程分为如下三步:
取指(Fetch)
第一步,获取指令。程序内存中指令地址存储在程序计数器PC中,每次获取完指令后,PC自动加上所获取到指令的长度,也就是指向下一条指令处。通常指令来源于相对较慢的内存中,这会阻塞CU直到获取完成,好在现代处理器通过缓存或流水线技术极大缓解了该问题。解码(Decode)
第二步,指令解码,指令解码的最终解释权归CU指令集ISA所有。指令的一部分为操作码,标明应该执行何种操作。其余部分通常会提供该操作所需的额外信息,如操作数等。
CU实现分电路和微编程两种,前者不可修改,但速度比微编程实现更快,由于指令集修改后需要调整电路,代价较高,因此通常用于RISC精简指令集中;另一方面,微编程CU简化了结构,降低了开发难度。
- 执行(Execute)
最后,执行指令。根据CU结构不同,指令中可能包含单个或一系列操作。操作过程中,根据时钟脉冲,CU不同部分可以协同执行操作的所有或一部分。大部分情况下,结果会写入寄存器以便于后续操作快速获取,但有时候也会不得不写入到缓慢的内存中。
简而言之,就是任务描述中的指令(以汇编级别起始)来源于CPU指令集,其中每条指令在从内存中读取后进行解码,解码器将单条指令分解成一至多条微操作(Opcode),通过控制电路信号,选择指定ALU完成目标操作。
同时,需要注意上述三个步骤执行过程中需要参考时钟脉冲,时钟脉冲有特定频率,电路被时钟脉冲的上升沿或下降沿所触发,形成统一步调。
2. CPU
上一节中对单个核心进行了探讨,但随着技术的发展,单核心的速度往往无法满足日益增长的计算需求,因此在CPU发展史上,也陆续出现了如下改进技术。
2.1 流水线 Pipeline
流水线技术的前提是操作可进一步分解,分解后的每个步骤可独立运行。
如上图所示,同一任务在分解为5个步骤后,通过合理安排ALU或其他CORE执行任务,原本10个时钟周期只能完成两次任务,而现在可以完成6次。
不过需要注意的是,流水线技术并不能增加单个任务的处理效率,即每个任务同样还是需要5个时钟周期才能完成,甚至相反,反而会因为进行了任务分解而导致每个步骤间需通过寄存器交换结果。
因此,从本质上来讲,流水技术减少的是任务的平均等待时间,而非单个任务完成时间。
2.2 并行处理 Parallel
通常大的任务可以分解为小的任务,这样就可以同时进行处理以减少整体消耗时长。通常有如下3种并行计算方法:
位级别并行
通过增长处理器字长,减少了同一指令所需操作数目。如8位处理器在进行两个16位整型数加法时,需要先将低8位相加,再将高8位及进位相加,也就是说,需要两个操作才能实现一条指令。而如果使用16位处理器,就可以通过一个操作完成相同指令。这样一来,就减少了一半的执行时间。-
指令级别并行
计算机程序其实就是一些列有序的指令,默认情况下,处理器每个时钟周期内最多只能处理一条指令,而事实上,有的指令重新排序后并列执行并不会影响最终结果,因此完全可以在指令级别实现部分并行以提高速度。
任务级别并行
同一程序的多个进程(程序运行实例,如感兴趣,后续操作系统部分会讲到)可以针对相同数据或不同数据进行并行处理,甚至同一程序也可以将其子任务分发给不同处理器进行处理。
2.3 中断
改进至此,却发现无论CPU如何优化并行、流水线等技术,真正导致程序执行速率缓慢的原因还在于CPU每执行一条指令,至少要与存储器交互一次,寄存器也好,内存也罢,它们的速率即使有了Cache或者分级存储机制,依然不能够有效弥补。为了能够最大限度地使用CPU,就需要对耗时严重的I/O操作进行优化。
传统模型里,执行一次I/O需要三个步骤:
- 准备I/O环境,如为相应设备命令准备参数
- 实际I/O命令,如无中断支持,则CPU必须持续轮询直到结束,耗费CPU资源
- 完善I/O操作,包括设置标志位等
从中可以看出,第一、三个步骤并未直接参与主存读写,耗时远比第二个步骤少,为了减少第二个步骤所消耗的CPU资源,于是从硬件层引入了中断机制。
中断可以通过单独控制器实现,也可以集成进CPU中。每个中断有各自独立的编码,硬件上有具有相互独立的内存单元。当一个中断出现后,控制器在执行完当前指令后将切换程序至内置ISR(Interrupt Service Routine)或指定的中断处理程序,执行完毕后,再重新切回原执行程序。
至于如何完成CPU中断的进程切换,将在后面详细讲述,此处只说明为修改PC地址、重新载入进程被切换前各寄存器状态值即可。
3. 总结
CPU这一篇就写到这里,没有满屏的电路图,也没有什么高深的人与自然。个人见解来讲,理解CPU本质还是要回归到计算本身,包括计算实现、计算表示、以及自动化计算流程的分解。希望能够有所收获,到本篇为止,硬件部分基本讲完,然而有硬件并不能成为计算机,所以下一篇中,将从操作系统开始说起。