在上一篇文章中,介绍了计算机核心组件之一负责存储的内存,在这篇文章中,将会介绍另外一个核心:负责计算的CPU。这个由一个个晶体管组成的每秒可以进行数十亿次,上百亿次计算的计算机的发动机。
1 CPU 的组成
1.1 从晶体管到ALU和信息存储
在大学数电的课上,晶体管组成的与门,或门,非门,与非门等等构成的逻辑电路组合,只要巧妙安排,就能够设计成很多复杂的电路,完成计算。这些晶体管组成了CPU 中专门负责运算的模块,这就是算术逻辑单元ALU。
同样,晶体管通过巧妙的设计组合,也能完成信息存储。
这样具备存储能力的组合电路就是我们在上几篇文章中都有提到的寄存器。在此基础上搭建起更加复杂的电路存储更多的信息,同时提供寻址功能,能够读取信息,这样,内存也诞生了。
同时,也可以看出,寄存器,内存的本质都是电路,也就是说,一断电,保存的信息都丢掉了。这也就是一些计算机在没保存文件的情况下断电重启后,数据丢失的原因。
1.2 软件与硬件的接口:指令集
指令集就是定义了一台计算机能够理解和执行的指令集合。 每个指令都告诉计算机执行特定的操作,例如执行算术运算、逻辑运算、内存访问、分支和跳转等。
指令集是计算机的底层编程接口,它确定了计算机能够执行的所有操作。 换句话说,指令集是软件与硬件的交汇点,在指令集之上,是软件的世界,在指令集之下,是硬件的世界。
1.3 时钟信号
现在,我们的电路具备了计算能力,储存能力,还可以通过指令告诉电路应该执行什么操作,但是现在,我们要靠什么协调或者靠什么来同步各个部分的电路让它们协同工作呢?
答案就是在数电课堂上学过的时钟信号,时钟信号每改变一次电压,整个电路中的各个寄存器也就是整个电路的状态就会更新一下,这样就能确保整个电路都是协同工作。
我们常说的CPU 主频就是在一秒钟内时钟信号的频率,显然主频越高,CPU 在一秒钟内完成的操作也就越多。
从此,有了能够完成各种计算的ALU,可以存储信息的寄存器,以及控制他们协同工作的时钟信号,他们在一起,就成为了CPU,从此,人类拥有了人类的第二个大脑。
2 查看CPU 的状态
在电脑上可以查看自己计算机CPU 的使用情况,以masOS 系统为例子:
打开活动监视器,点进CPU,就可以看到CPU 的使用情况:
- "System" 表示系统进程占用的 CPU 百分比,这些是由 macOS 系统管理的后台任务和进程。
- kernel_task:这是 macOS 内核的一部分,它执行诸如硬件管理、内存管理和系统资源分配之类的任务。kernel_task 通常会占用一定的 CPU 时间,特别是在处理系统级任务时。
- WindowServer:这是负责 macOS 图形用户界面的进程。它处理窗口、显示和用户界面元素的渲染,因此在图形密集型任务时可能会消耗一些 CPU 资源。
- mds 和 mdworker:这些是用于 Spotlight 搜索和文件元数据的进程。它们可以在后台执行,占用一定的 CPU 资源。
- "User" 表示用户进程占用的 CPU 百分比,这些是由应用程序执行的任务和进程。
- Google Chrome:正在使用 Google Chrome 浏览网页,Google Chrome 进程将归类为用户进程,并显示其 CPU 占用百分比。这表示浏览器正在执行任务,如加载网页、运行插件或执行 JavaScript 代码。
- Bilibili:正在使用Bilibili 观看视频,也将归类为用户进程,并显示其 CPU 占用百分比。
- "Idle" 表示 CPU 的空闲时间百分比,即 CPU 未被任何任务或进程使用的时间。
可以看到,CPU 的空闲时间占了最大的部分,即 87.38%。这意味着大部分时间内,CPU 都处于空闲状态,没有被任务或进程占用,这是正常的情况。
Windows 系统也有类似这种任务管理器,可以查看CPU 占用情况。
在Windows 系统下,会有一个专门的“系统空闲进程”,这个进程是使用率最高的线程,就是在CPU 空闲时运行的线程,这样设计是为了避免队列判空,为了能让调度器总能在进程就绪队列中找到一个可供执行的线程。
相比之下,在 macOS 中,系统资源管理的方式略有不同。macOS 使用名为 “Mach”的微内核,它负责管理系统的任务和资源。Mach 内核通过多线程和调度算法来处理资源分配,而不像 Windows 那样专门为“系统空闲进程”创建一个单独的进程。macOS 的资源管理通常更加动态,根据需要自动分配资源,以确保系统响应和性能。
3 CPU:流水线模式执行机器指令
3.1 CPU 处理机器指令
CPU 采用流水线模式执行机器指令的主要目的是提高指令执行的效率和性能。流水线处理是一种将指令执行过程分解为多个连续阶段的设计方法,每个阶段执行特定的任务。
流水线模式并没有减少一条指令执行的时间,但是提高了吞吐量:在流水线中,每个阶段都可以同时处理不同的指令,因此多个指令可以在不同的阶段同时执行。这导致了指令的重叠执行,从而提高了指令吞吐量。所以即使每个指令的执行时间没有变化,整体系统的性能也有所提升。
CPU 执行一条指令有四个步骤:取值,译码,执行,回调。这几个阶段也会由特定的硬件来完成。
- 取指 :从内存中获取下一条指令。
- 译码 :解析指令的操作码和操作数,确定指令要执行的操作。
- 执行 :执行指令中的操作,可能涉及算术运算、逻辑运算等。
- 回调 :将执行结果写回到寄存器文件或内存中。
3.2 if 语句和分支预测
分支预测是CPU 中的一项重要技术,用于提高程序执行的效率。它主要用于解决条件分支语句(if-else语句或循环中的分支)带来的性能问题。
假如有这样一个场景:我们创建了一个大小为10000的数组,再遍历这段数组,对里面如果大于128的数字逐个相加,那么如果这个数组是有序的,比如[1, 2, 3, 4, ..., 10000],那么这段代码的运行时间要比无序的数组,比如[67, 22, 12, 8, ..., 8789]要快。
这就是CPU 以流水线的方式执行机器指令和分支预测导致的。
在流水线模式下,如果遇上了if 语句,那么就有可能if 指令还没有执行完成,后续的指令就要进入流水线了,那么这个时候,为了不浪费CPU 的资源,就会预测后一条指令应该是什么。如果猜对了,那么流水线继续前进,如果猜错了,那么流水线上的已经执行的错误指令全部作废,显然,经常预测错,就会有性能损耗。
这也就是上面那个例子为什么有序的数组比无序数组需要的执行时间更少,有可能代码是这样写的:
if (num[i) > 128) {
sum += num[i];
}
有可能CPU 就会分支预测num[i] 是大于128的,如果是有序数组,当前一个数已经大于128,后面的数全部就会预测成功,但是相反,如果是无序数组,预测的准确度就会大大降低,尤其是这个例子中有10000个数,时间差距会更加大。
所以,我们得到了一个提高程序性能的办法:如果要使用if 语句,那么最好让程序能够猜对这个条件。
4 CPU 的指令集
在刚刚我们提到了指令集是计算机能够理解并且执行的指令的集合,是软件和硬件的接口。那么指令集可以说是CPU 的“能力集合”,这又产生了两种不同的指令集:复杂指令集CISC 和精简指令集RISC。
4.1 复杂指令集CISC
这是最先出现的,是计算机科学家为了使指令能将更加便于编写(那个时候大部分程序都直接使用汇编语言编写),就要使机器指令更加接近人类抽象的语言,也就是使一句指令背后的意思更加“复杂”,就发明了复杂指令集CISC
CISC 有下面几种特点:
- 更加抽象和复杂:一条指令集能够尽可能完成更多的任务
- 机器指令的长度不固定
- 机器指令高度编码,提高代码密度
同时,由于一条指令集能够尽可能完成更多的任务,所以可能需要较少的内存访问来完成一项任务,这在某些情况下可以提高性能。
4.2 精简指令集RISC
这个时代下,程序员越来越多使用高级语言写程序,依靠编译器来自动生成汇编指令,同时有人发现,CISC 有一部分复杂的指令并不经常用到,而且设计编译器的人也更加倾向把高级语言翻译成简单的汇编指令而不是复杂抽象的指令。
因此产生了精简指令集RISC。
相比于CISC,RISC的
- 指令集更加简单:RISC 架构的计算机具有较小、更简单的指令集,这些指令执行的操作通常都非常基本,如加载、存储、算术运算和逻辑运算。每条指令都被设计成在一个时钟周期内执行。
- 执行速度快:由于每个指令的执行时间短且硬件简单,RISC 处理器通常能够以更高的时钟频率运行,从而提供更高的性能。
CISC 架构强调指令集的复杂性和灵活性,而 RISC 架构强调硬件的简洁和执行效率。
在后期,CISC 架构也开始变得像RISC:在编译器生成的指令还是CISC,但是在CPU 内存执行指令时,采取了类似RISC 的方式,这样既能保持CISC 指令向前兼容,又能获取RISC 的好处。
直到如今,Intel 和Windows 的wintel联盟在计算机市场占据着一席之地,以x86为代表的CISC 处理器在服务器端和桌面端占据重要地位。
而基于RISC 的ARM 架构也因为高的执行效率而统治着移动端市场,在桌面端,Apple 也研发了基于RISC 的M1 芯片,在Mac 计算机中也取代了Intel 的芯片。
从可预测的未来来看,CISC 和RISC 将会长久的共存下去。
5 寄存器
在第二篇文章:计算机底层2 程序在运行时发生了什么中提到,PC 寄存器(程序计数器)速度更快,容量也更小,CPU 访问内存的速度大概是访问寄存器速度的1/100,在创建进程时,代码以及代码依赖的数据被加载到内存,执行机器指令时,需要把内存中的数据搬运到寄存器中,供CPU 使用。实际上,寄存器和内存没有什么不同,都是用于储存信息的,只不过寄存器速度更快,造价更高,因此容量较小,是一个临时存放点。
根据用途,寄存器也有好几种。
5.1 栈寄存器(Stack Pointer)
在第三篇文章:计算机底层3 内存中提到,函数在运行时都有一个运行时栈,对于栈来说,最重要的就是栈顶信息,栈顶信息就保存在栈寄存器中,除此之外,栈寄存器中也有局部变量,函数调用和返回地址。
5.2 指令地址寄存器(Program Counter)
也就是我们熟悉的PC 寄存器(程序计数器),当程序启动时,第一条要被执行的指令会被写入PC 寄存器中,这样CPU 需要做的就是根据PC 寄存器中的地址区内存中取出指令并且执行。PC 寄存器中的指令地址也会不断递增,是CPU 下一条指令的地址。
5.3 状态寄存器(Status Register)
状态寄存器是用来保存状态信息的,例如,在算数运算中,可能会产生进位或者溢出,这些信息就保存在状态寄存器中。
除此之外,CPU 执行机器指令时的两种状态:内核态和用户态,状态寄存器中也有特定的比特位来标记当前CPU 正工作在哪种状态下,因此我们就可以知道当前CPU 正工作在那种状态下。
6 CPU借助栈来完成指令
栈是我们熟知的一种先进后出(FILO)的数据结构,CPU 处理任务特别是各种嵌套式的任务,就是巧妙利用了栈。
6.1 函数调用与运行时栈
这是我们在上一篇文章计算机底层3 内存的栈区中详细讲过的,每个函数在运行时都有属于自己的栈帧,当函数A 调用函数B 时,会将运行时信息保存在函数A 的栈帧中,当函数B 运行完后,会根据栈帧中的信息(上下文信息Context)恢复函数A 的运行,这整个调用的顺序,就是栈的先进后出的顺序。
6.2 系统调用与内核态栈
当我们进行读写磁盘或者创建新的线程时,是用户程序通过系统调用操作系统内部通过调用一系列函数来处理请求的,有函数调用就有运行时栈,而操作系统完成系统调用的运行时栈在 内核态栈(Kernel Mode Stack) 中。
每个用户态线程在内核态都有一个对应的内核态栈。开始时,程序运行在用户态,此时运行到了系统调用的代码,系统调用指令的执行将会触发CPU 状态的切换,此时CPU 从用户态切换到内核态,找到该用户态线程对应的内核态栈,这个时候,用户态线程的上下文信息Context 就会被保存在内核态中。
此后,CPU 开始执行内核中的相关代码,后续内核态栈会像用户态运行时栈一样,随着函数的调用和返回增长及减少。
当系统调用执行完后,根据内核态栈中保存的用户态程序上下文Context 恢复CPU 状态,并从内核态切换回用户态,这样用户态线程就可以继续运行了。
6.3 中断与中断函数栈
计算机在运行程序时,也能够处理鼠标点击,键盘按动,接收网络数据等任务,就是通过中断处理函数来完成的。
中断的本质是打断当前CPU 的执行流,跳转到具体的中断处理函数中,当中断处理函数执行完成后,再跳转回来。
中断处理函数有自己的运行时栈吗?
答案是分两种情况:
- 中断处理函数没有自己的运行时栈,这种情况下,中断处理函数依赖内核态栈来完成中断处理。
- 中断处理函数有自己的运行时栈(ISR栈),这种情况下,每个CPU 都有自己的ISR 栈。
简单地以第一种情况为例,中断处理函数与系统调用比较类似,只不过系统调用时用户态程序主动发起的,而中断处理是由外部设备发起的,也就是CPU 在执行任何一条机器指令时,都有可能因中断而暂停当前程序的执行,转而去执行中断函数。
6.4 线程切换与内核态栈
假设现在系统中有两个线程A 和B,现在CPU 需要在两个线程之间切换处理任务。
每个Linux线程中都有一个对应的进程描述符task_struct
,在该结构体内部,有thread_struct
专门用来保存CPU 的上下文信息。
当CPU 从线程A切换到线程B时,就先将执行线程A 的CPU 上下文信息保存到线程A 的描述符中,然后像线程B的描述符中保存的上下文信息恢复到CPU 中,这样,CPU 就能够执行线程B了。
7 总结
从简单的晶体管一步步到复杂的CPU,ALU 带来了计算能力,寄存器带来了存储能力,时钟信号带来了协调统一能力,指令集确定了计算机能够进行的所有操作,是软件与硬件的接口。复杂指令集CISC 和精简指令集RISC 分别统治着桌面端,服务器端和移动端。不同的寄存器有不同的用途,栈寄存器保存着栈最重要的栈顶信息,PC 寄存器保存着CPU 下一条要执行的指令,状态寄存器保存着CPU 目前的状态。函数调用,中断处理,线程切换,系统调用都离不开上下文Context 的保存和恢复,而Context 的保存与恢复又是借助于栈这种数据结构。
8 下一篇文章
9 参考资料
- 陆小风. 计算机底层的秘密. 电子工业出版社, 2023.
- 计算机底层的秘密 gitbook