课程链接:深入浅出计算机组成原理_组成原理_计算机基础-极客时间
一、异常
(一)异常
异常其实是一个硬件和软件组合到一起的处理过程。异常的发生和捕捉,是在硬件层面完成的。但是异常的处理,是由软件来完成的。
计算机会为每一种可能会发生的异常,分配一个异常代码(Exception Number),或者叫作中断向量(Interrupt Vector)。异常发生的时候,通常是 CPU 检测到了一个特殊的信号。比如正在执行的指令发生了加法溢出,就会有一个进位溢出的信号。这些信号,在组成原理里面,一般叫作发生了一个事件(Event)。CPU 在检测到事件的时候,其实也就拿到了对应的异常代码。
这些异常代码里,I/O 发出的信号的异常代码,是由操作系统来分配的,也就是由软件来设定的。而像加法溢出这样的异常代码,则是由 CPU 预先分配好的,也就是由硬件来分配的。
拿到异常代码之后,CPU 就会触发异常处理的流程。计算机在内存里,会保留一个异常表(Exception Table),也可以叫作中断向量表(Interrupt Vector Table),存放的是不同的异常代码对应的异常处理程序(Exception Handler)所在的地址。
CPU 在拿到异常码之后,会先把当前的程序执行的现场,保存到程序栈里面,然后根据异常码查询,找到对应的异常处理程序,最后把后续指令执行的指挥权,交给这个异常处理程序。
(二)异常的分类:中断、陷阱、故障和中止
1、中断(Interrupt):程序在执行的过程中被打断。打断执行的信号,来自于 CPU 外部的 I/O 设备,而不是在程序自己执行的过程中,所以可以称之为“异步”类型的异常
2、陷阱(Trap):程序员“故意“触发的异常。就好像在程序里面打了一个断点,这个断点就是设下的"陷阱"。当程序的指令执行到这个位置的时候,就掉到陷阱当中,对应的异常处理程序就会来处理这个"陷阱"当中的猎物。
最常见的一类陷阱,发生在从程序的用户态切换到内核态的时候。应用程序通过系统调用去读取文件、创建进程,也是通过触发陷阱来进行的。这是因为,用户态的应用程序没有权限来做这些事情,需要把对应的流程转交给有权限的异常处理程序来进行。
3、故障(Fault):不是开发的时候计划在内的,一样需要有对应的异常处理程序去处理。故障和陷阱、中断的一个重要区别是,故障在异常程序处理完成之后,仍然回来处理当前的指令,而不是去执行程序中的下一条指令。因为当前的指令因为故障的原因并没有成功执行完成。
4、中止(Abort):与其说是一种异常类型,不如说是故障的一种特殊情况。当 CPU 遇到了故障,但是没有异常处理程序能够处理这种异常,程序就不得不中止了。
陷阱、故障以及中止类型的异常,是在程序执行的过程中发生的,可以称之为“同步“类型的异常。
无论是异步的中断,还是同步的陷阱和故障,都是采用同一套处理流程——保存现场、异常代码查询、异常处理程序调用。
(三)异常处理:上下文切换
切换到异常处理程序的时候,指令的控制权被切换到了另外一个"函数"里面,所以要把当前正在执行的指令去压栈。这样,才能在异常处理程序执行完成之后,重新回到当前的指令继续往下执行。
异常情况往往发生在程序正常执行的预期之外,除了本来程序压栈要做的事情之外,还需要把 CPU 内当前运行程序用到的所有寄存器,都放到栈里面。最典型的就是条件码寄存器里面的内容。
像陷阱这样的异常,涉及程序指令在用户态和内核态之间的切换。对应压栈的时候,对应的数据是压到内核栈里,而不是程序栈里。
像故障这样的异常,在异常处理程序执行完成之后。从栈里返回出来,继续执行的不是顺序的下一条指令,而是故障发生的当前指令。因为当前指令发生故障没有正常执行成功,必须重新去执行一次。
所以,对于异常这样的处理流程,不像是顺序执行的指令间的函数调用关系。而是更像两个不同的独立进程之间在 CPU 层面的切换,所以这个过程我们称之为上下文切换(Context Switch)。
二、CPU指令集
(一)CISC 和 RISC
CPU 的指令集里的机器码是固定长度还是可变长度,也就是复杂指令集(Complex Instruction Set Computing,简称 CISC)和精简指令集(Reduced Instruction Set Computing,简称 RISC)这两种风格的指令集一个最重要的差别。
在计算机历史的早期,所有的 CPU 其实都是 CISC。当时的计算机很慢,存储空间也很小,为了让计算机能够做尽量多的工作,CPU 指令集的设计,需要仔细考虑硬件限制。
为了性能考虑,很多功能都直接通过硬件电路来完成。为了少用内存,指令的长度也是可变的,常用的指令要短一些,不常用的指令可以长一些。那个时候的计算机,想要用尽可能少的内存空间,存储尽量多的指令。
后来,计算机的性能越来越好,存储的空间也越来越大了。 70 年代末,RISC 开始登上了历史的舞台。大卫·帕特森(David Patterson)教授发现,实际在 CPU 运行的程序里,80% 的时间都是在使用 20% 的简单指令。于是,他就提出了 RISC 的理念。自此之后,RISC 类型的 CPU 开始快速蓬勃发展。
RISC 架构的 CPU 的想法其实非常直观。既然我们 80% 的时间都在用 20% 的简单指令,那么,在 RISC 架构里面,CPU 选择把指令“精简”到 20% 的简单指令。而原先的复杂指令,则通过组合简单指令来实现,让软件来实现硬件的功能。这样,CPU 的整个硬件设计就会变得更简单了,在硬件层面提升性能也会变得更容易了。
RISC 的 CPU 里完成指令的电路变得简单了,于是也就腾出了更多的空间。这个空间,常常被拿来放通用寄存器。因为 RISC 完成同样的功能,执行的指令数量要比 CISC 多,所以,如果需要反复从内存里面读取指令或者数据到寄存器里来,那么很多时间就会花在访问内存上。于是,RISC 架构的 CPU 往往就有更多的通用寄存器。
除了寄存器这样的存储空间,RISC 的 CPU 也可以把更多的晶体管,用来实现更好的分支预测等相关功能,进一步去提升 CPU 实际的执行效率。
程序的 CPU 执行时间 = 指令数 × CPI × Clock Cycle Time
CISC 的架构,其实就是通过优化指令数,来减少 CPU 的执行时间。而 RISC 的架构,其实是在优化 CPI。因为指令比较简单,需要的时钟周期就比较少。
(二)微指令架构
微指令(Micro-Instructions/Micro-Ops)架构是为了解决指令集的向前兼容而产生的。在微指令架构的 CPU 里面,编译器编译出来的机器码和汇编代码并没有发生什么变化。但在指令译码的阶段,指令译码器“翻译”出来的,不再是某一条 CPU 指令而是好几条“微指令”。这里的一条条微指令,就不再是 CISC 风格的了,而是变成了固定长度的 RISC 风格的了。微指令架构的引入,让 CISC 和 RISC 的分界变得模糊了。
RISC 风格的微指令,会被放到一个微指令缓冲区里面,然后再从缓冲区里面,分发给到后面的超标量,并且是乱序执行的流水线架构里面。
不过,一个能够把 CISC 的指令译码成 RISC 指令的指令译码器,比原来的指令译码器要复,这也就意味着更复杂的电路和更长的译码时间。
之所以大家认为 RISC 优于 CISC,来自于一个数字统计,那就是在实际的程序运行过程中,有 80% 运行的代码用着 20% 的常用指令。这意味着,CPU 里执行的代码有很强的局部性。而对于有着很强局部性的问题,常见的一个解决方案就是使用缓存。
即在 CPU 里面加了一层Cache用来保存的是指令译码器把 CISC 的指令“翻译”成 RISC 的微指令的结果。这样,在大部分情况下,CPU 都可以从 Cache 里面拿到译码结果,而不需要让译码器进行实际的译码操作。这样不仅优化了性能(译码器的晶体管开关动作变少了),还减少了功耗。
(三)ARM 和 RISC-V
ARM现在的含义,是“Advanced RISC Machines”,ARM的芯片是基于RISC的。
ARM最主要的两个优点:
1、功耗优先。在移动设备上,功耗是一个远比性能更重要的指标。RM 的 CPU,主频更低,晶体管更少,高速缓存更小,乱序执行的能力更弱。所有这些,都是为了功耗所做的妥协。一个 4 核的 Intel i7 的 CPU,设计时功率就是 130W。而一块 ARM A8 的单个核心的 CPU,设计功率只有 2W。
2、低价。ARM 并没有自己垄断 CPU 的生产和制造,只是进行 CPU 设计,然后把对应的知识产权授权出去。苹果、三星、华为,它们都是拿到了基于 ARM 体系架构设计和制造 CPU 的授权。ARM 自己只是收取对应的专利授权费用。多个厂商之间的竞争,使得 ARM 的芯片在市场上价格很便宜。
在 ARM 似乎已经垄断了移动 CPU 市场的时候,开源的 RISC-V 出现了,RISC 概念的发明人,图灵奖的得主大卫·帕特森教授从伯克利退休之后,成了 RISC-V 国际开源实验室的负责人,开始推动 RISC-V 这个“CPU 届的 Linux”的开发。可以想见,未来的开源 CPU,也多半会像 Linux 一样,逐渐成为一个业界的主流选择。