CPU对我们来说既熟悉又陌生,熟悉的是我们知道代码是被CPU执行的,当我们的线上服务出现问题时可能首先会查看CPU负载情况。陌生的是我们并不知道CPU是如何执行代码的,它对我们的代码做了什么。本文意在简单解释我们代码的生命周期,以及代码是如何在CPU上跑起来的。
一个漂亮 control+c 加上一个漂亮的 control+v,啪~,我们愉快的写下了代码,当代码被保存后,它就被存在我们磁盘的某个地方,它可能是像java或者python这些高级语言写的,也可能是像c这种古老语言写的,但是现在它肯定没法被运行,因为计算机不认识它们,计算机只认识0、1这样的二进制,简称机器码,那为什么我们不直接写机器码?如果你有这样的思考,我只能呵呵了,请你帮我翻译下以下机器码:
001010100101001001001
100100101000101010101
很明显作为高质量人类的我们也无法识别出这段代码写的是什么,于是出现类似java这样的高级语言,它们给机器码穿上了一层外衣,然后交给伟大的程序员来创造未来。
所以反过来我们的代码需要被替换成机器码,这样才能被计算机认识,计算机才能帮我们干事。这个转换的过程我们通常叫「编译」。
#include <studio.h>
int main()
{
printf("Hello World\n");
return 0;
}
这是一段应该每个程序员都写过的代码(hello.c),在Linux下,当我们使用GCC来编译Hello World程序时,只需要最简单的命令:
gcc hello.c
./hello
# Hello World
看似很简单的一行,但是其实编译的过程很复杂,并不是我们想象中的编译,真实是分为4个步骤,分别是预处理(Prepressing)、编译(Compliation)、汇编(Assertmbly)和链接(Linking)。
好吧,原来编译是这么回事,通过这一整套的编译操作,我们代码终于能执行了,我们简简单单的运行./hello.out
即可输出Hello World。等等,这个简简单单的过程发生了什么?
ok,ok,通过编译,我们的程序终于能执行了,接下来让我们站在CPU的视角来看看Hello World是如何被打印出来的。
首先编译好的文件是存在磁盘上的,得先加载到内存中,这里你可能会问:为什么CPU不能直接读取磁盘的程序运行而要经过内存?答案是慢,缓慢的磁盘会影响我们程序执行的速度,因此需要更加快速、离CPU更近的存储,那就是内存。
内存是一大块存储空间,可以存储很多数据信息,那么如何找到我们要写的程序呢?答案是地址,其实每个字节在内存中都有一个地址,这样当CPU去内存中读我们的程序时,只需要根据对应的地址就可以知道我们程序的具体内容。
等等...,这里似乎又有个问题,CPU是如何与我们的内存、磁盘通信的?应该有个媒介之类的吧。没错,这个媒介就是主板上的总线和芯片组,总线好理解,就像高速公路,数据信息可以通过这条高速公路传递到CPU中,这个芯片组是个什么玩意?电脑主板上芯片很多,这里说的主要是南桥芯片和北桥芯片。先来个解释:
嗯... 为什么CPU与高速设备、低速设备之间的通信需要这两个芯片?CPU自己不能干吗?这里还是类似拆分任务的功能,如果把所有的任务都交给CPU来处理,CPU会太忙了,还有比较重要的一点,如果南桥芯片坏了,那么我们可以直接更换南桥,而不用换掉整个CPU。
终于CPU通过总线和芯片打通了磁盘、内存之间的通信了,接下来的一切开始交给CPU。
CPU全称是Central Processing Unit,即中央处理单元,它的本质就是一块超大规模的集成电路。从逻辑上来分,它的内部是由寄存器、控制器、运算器和时钟组成的,下面来解释下各个组成是干什么的。
a = 1 #0x0010
b = 2 #0x0011
if a > b { #0x0012
printf("%s","a") #0x0013
} else {
add(a,b) #0x0014
}
printf("%s","end") #0x0017
func add(int a,int b) { #0x0020
return a+b
}
这是段非常简单的伪代码,有分支判断、有函数跳转。我们来从CPU的角度看看它是如何执行的:
2 . 把a=1这个数字读入通用寄存器中,程序计数器(PC寄存器)自动加1,即指向下一条指令 0x0011
3 . 指令寄存器拿到程序计数器的指令地址,把b=2这个数字读入通用寄存器中,程序计数器(PC寄存器)自动加1,即指向下一条指令0x0012
4 . 指令寄存器发现此处是比较逻辑,会执行a-b,此时可能会有三个结果分别是大于0,等于0,小于0,然后把这个结果存到标志寄存器里,这里有个小知识,我们经常说的是CPU是64位或者32位,其实也表示了标志寄存器的长度
5 . 很明显,a是小于b的,CPU根据标志寄存器的状态值应该跳转到else里面,注意这时程序计数器的值不是加1,而是设置成else的地址 0x0014,当执行到0x0015的时候,需要发生函数跳转,程序计数器会被设置成 0x0020,但是这里并不是简单的函数跳转(专业术语叫做call),因为在函数执行完毕之后,还要返回,也就是程序计数器需要从0x0020再变成0x0017。call执行的时候会把后续要执行的指令地址0x0017存到栈中。
6 . 当我们的add函数执行完毕之后,会有个return,return的时候会把上一步骤存入栈中的地址0x0017写入程序计数器中
7 . 指令寄存器根据程序计数器当前的地址执行最后的打印(end),结束。
顺序执行的指令代码,程序计数器会自动累加(当然不一定累加的是1),然后找到下一条要执行的指令。
分支判断的时候,程序计数器不是简单的累加地址,需要地址的跳转。
函数调用不仅仅需要跳转地址,还要把函数执行完毕之后要执行的地址存下来,方便折回继续执行。
其实还有个循环执行,也就是我们代码中的for、while之类的,这时程序计数器会不停的在某些地址之间来回切换。
Copyright© 2013-2020
All Rights Reserved 京ICP备2023019179号-8