计算机原理
CPU 执行过程(简述)
CPU 的本质就是一个执行指令的工人。
上面的概述引发了几个问题,什么是指令?指令从哪里来?CPU 如何执行和加载这些指令?
指令,顾名思义指示和命令。这个命令和指示的对象是 CPU,CPU 可以明白它的意图,并执行。
现代 CPU 识别的指令都是由字节码
组成,即一长串的 0 和 1。
还记得远古计算机是如何编写和执行指令的吗?没错,是一条条穿孔的纸带,带孔为 1,无孔为 0。那个时候就是用这种朴素
的方式来组成一条条指令。但是现在已经被晶体管代替了。
CPU 从内存中读取指令,并执行指令内容。指令执行的中间结果会保存到相应的寄存器中。
Tips: 像这种指令和数据存储在一起的结构称作 冯·诺伊曼结构
程序装载与执行
CPU 能够执行指令,但一般都是给一组指令交给 CPU 去执行。该组指令中可以构成简单的逻辑跳转,例如:C 指令执行成功,则跳转到 F 指令执行,否则跳转到 D 指令。
程序就是一组指令和数据的集合,在 Windows 中就是我们常见的 exe 文件。
当我们在电脑中打开多个记事本时,内存中为多个记事本进程开辟了不同的空间,对于每个记事本来说,自己所处和所使用的的内存地址都是一样的,进程之间互不干扰。但是通过「上帝视角」可以发现每个记事本进程只是占用内存的一段空间而已。
Q: 不同 CPU 架构,为什么不能运行同一个程序 ?
A: 不同 CPU 架构对于,指令集的理解不同。因此程序需要在不同的 CPU 架构中使用不同的指令集。
Q: 同 CPU 架构,不同操作系统,不能运行同一个程序 (Linux 和 Windows)?
A: 同 CPU 架构 对于程序指令集的理解是相同的。
不同之处在于,系统提供的 API 不同。例如,Windows 提供了 createProcess 方法创建进程,但对应 Liunx 上就不存在该方法。
不同系统内存布局也不相同,导致装载程序的方式也存在差异。因此,程序跨平台性存在问题。
解决跨平台
为了解决跨平台问题,于是在操作系统之上维护了一个虚拟机 JVM。
JVM 负责把统一的中间语言,转换成对应操作系统的能识别的语言。而这个中间语言之上就是高级语言,像 Java,Python 等。这些高级语言的作用就是把我们编写的代码,编译成中间语言,然后交给 JVM 执行。这也是为什么 Java 和 Python 具有跨平台性。
但并非所有的语言都需要转化成中间语言,像 Go 编译之后就是 native 的,因此不需要 JVM,直接交给操作系统执行。由于省去了一个 JVM 虚拟机中间操作,因此性能会极大的增强,但跨平台方面就会收到限制,需要对不同操作系统编译。
动态链接库
假如所有程序都需要调用一个指令 A,为了避免重复,可以把 A 和一些经常使用的指令封装成库。这样程序中就不需要完整地写入 A 指令,只需要引用 A 指令即可。这样CPU 就会在程序之外去调用 A 指令,完成操作。
把这个引用库的行为称作「动态链接库」
这样做的好处显而易见,可以省空间,程序都从一个地方调用执行。还有就是,需要升级的话,只需要升级引用的库即可。
缺点也很明显,当程序引用的库,找不到时。就会提示「缺少 xxx」,比如一些游戏启动的时候。存在库,但是不兼容也是很要命,例如程序需要 1.0 版本的库,但是系统只有 2.0 的库。程序在刚开始启动可能不会有问题,但是当执行到需要引用 1.0 版本的库命令时,程序就会崩溃。
程序时分复用
日常生活中,经常会一边听着歌,一边敲着代码。有没想过,为什么听歌和我写代码可以同时执行?
这是因为,虽然 CPU 只有一个,他只能按照顺序一个一个执行,但是架不住他快啊。你这一眨眼的功夫,他已经在几个程序中切换执行了很多遍,给你的感觉这几个程序在同时进行,计算执行效率快的离谱。
时间片
正如上所说,CPU 会再程序中切换执行,相当于每个程序在轮流占用 CPU 时间,占用的时间称为「时间片」。只有线程和进程能占用 CPU 时间。
程序进程能占用 CPU 时间,但是并不能一直占用 CPU,否则其他程序便无法继续执行了。当程序的时间片结束了之后,CPU 便往下执行下一个程序的时间片。
请求慢速设备时,也会例如我们用记事本编写一段文字后,点击「保存」。这个时候需要将我们写的内容存储到磁盘上,由于 IO 操作和 CPU 的执行速度相比实在是太慢了,因此 CPU 并不会傻傻等待 IO 保存,而是继续往下执行下一个指令。
这可能与你理解的不相符,有的应用程序保存后,会弹出「保存成功」的框,如果 CPU 不等待 IO 的话,那他怎么及时处理这个弹框呢?这就涉及到另一个知识了「中断」。CPU 会对中断请求进行「特殊处理」,当 IO 操作结束后,就会向 CPU 发起一个中断请求,CPU 会放下手头的活,优先处理这个中断请求。
总结,以下三种情况会放弃 CPU。当前程序的时间片执行完成之后,当需要请求一个慢速设备时(IO 操作),存在中断请求时。
上下文切换
当占有 CPU 的程序,时间片到了后,CPU 开始执行下一个程序了。当 CPU 再次执行原来的程序时,他是怎么知道原来的程序执行到哪一步了,换句话说,如何恢复程序执行的状态。
时间片快要结束时,CPU 会将当前程序的数据,执行步骤等信息保存起来,当再次执行到该程序时,就会加载保存的数据,恢复之前的状态继续执行。和打游戏一样,打着打着,突然被爸妈喊去吃饭,这个时候就要把游戏存档,等下次打开游戏直接读档,继续游戏即可。这个「存档」和「读档」过程,就叫做上下文切换。
由于 CPU 频繁的切换上下文,这个性能开销虽然对于普通用户感知不到,但是对于 CPU 执行来说确是巨大的。这也是为什么有「用户态线程」的概念,即程序自身去维护内部的线程调度,这样就会大大减少 CPU 上下文切换,这也正是 Go 语言所使用的的协程。