查看原文
其他

程序员应如何理解CPU:下篇

码农的荒岛求生 码农的荒岛求生 2020-12-18

本节属于操作系统第一章即基础篇,在真正开始操作系统相关章节前回顾一些重要的主题,以下是目录,由于本文篇幅较多因此按上篇、下篇两次发布,目录中黑体为本篇内容。




什么是机器指令

程序语言的演变

CPU是如何工作的

寄存器

程序计数器

栈指针

CPU的工作模式

总结



承接上文《程序员应如何理解CPU:上篇


程序计数器(Program Counter)

到目前为止,我们介绍了CPU的工作过程,这个过程非常简单,CPU从内存中取出指令,执行指令然后继续从内存中取出指令。。

但是有个问题我们没有回答,那就是:

CPU是怎么知道从内存中的哪个位置取出指令呢?

原来,CPU中有一个专门的寄存器用来存放下一条指令的内存地址。当CPU执行完一个指令后,就会根据这个寄存器中的内存地址取出下一条指令,取出指令后,该寄存器的内存地址+1,然后指向CPU要执行的下一条指令。这个寄存器在大部分教材中叫做Program Counter,中文译作程序计数器。这个名字不是很贴切,因此也有人称之为指令地址寄存器Instruction Address Register (IAR),或者指令指针Instruction Pointer (IP),后面这两个名字更形象一些。

因此,这这上面张图中,CPU指向内存中指令的地址,其实就是这个寄存器中保存的是CPU要执行的指令的内存地址。

在后面我将继续沿用程序计数器这个名词。不管名字怎样,大家只要能理解这个寄存器的作用就好。

有的同学可能会问,刚才讲到这个寄存器每次都是+1,是不是意味着CPU仅仅沿着内存地址递增的顺序依次执行指令就好了?

其实不是这样的,CPU中有一类跳转指令,比如:jim rax。其中rax中保存是要跳转的内存地址。CPU会根据前面指令的结果来决定要不要跳转,如果计算出结果后需要跳转,那么程序计数器中的内存地址更改为rax,这就是C语言中if是如何实现的。而如果要跳转的内存地址指令是CPU已经执行过的,那么就形成了循环,这就是C语言中for或while循环是如何实现的。

既然我们已经知道了程序计数器的作用,如果想让CPU从某个内存地址开始执行指令那么只需要把CPU的程序计数器设置为相应的地址就可以了。

现在你应该知道我们写的程序是如何被操作系统执行的了吧,操作系统把程序从硬盘拷贝到内存后,找到第一条指令的内存地址,然后将CPU的程序计数器设置为该地址,这样我们的程序就可以开始运行啦。


栈指针(Stack Pointer)

在《程序员应如何理解内存》一节中我们已经知道了,函数调用时会创建栈帧来保存调用函数时的参数以及函数运行过程定义的局部变量。这些栈帧是以先进后出的形式在内存中进行分配释放的,那么CPU是如何能知道当前栈帧在内存中的哪个位置上?换句话说,CPU是如何来跟踪函数调用栈的?

跟踪调用栈是通过CPU中叫做栈指针stack pointer的寄存器来完成的(注意这里的指针不是C语言中的指针,这里的指针指的是寄存器)。我们用示例来说明你就明白这个寄存器的作用了。

还记得我们在《程序员应如何理解内存》一节中那段C语言代码吗,在那一节中我们仅仅展示了代码执行过程中在内存中是什么样子的,现在我们来完善一下,我们把CPU中栈指针这个寄存器也加入到该图中。

从图中,我们可以看到,CPU中的栈指针总是指向当前正在运行的函数栈帧,也就是的栈顶(注意这里的栈是向下增长的)。现在你应该知道这个寄存器的作用了吧。

任何时候,只要通过查看CPU的栈指针,我们就能知道进程中当前正在被执行函数的栈帧在内存中的位置。


CPU的工作模式

我们已经知道,CPU是通过执行机器指令来来处理任务的,我们的程序(用户程序)会被编译成机器指令,操作系统同样需要编译成机器指令,不要忘了操作系统也是一个大程序,CPU通过运行操作系统来控制整个计算机系统。

从这里我们可以看出,如果用户程序对应机器指令的类型和操作系统对应机器指令的类型是一样的话,那么用户程序就能像操作系统一样控制整个计算机系统,这显然是不合理的。如果用户程序可以轻易控制整个计算机的话,那么用户程序中的bug可能会破坏整个计算机系统,但就目前的情况看,如果我们的程序有问题的话,最多就是我们的程序崩溃而不会影响整个系统,那这是怎么做到的呢?

原来CPU可以执行的指令从等级上讲有两类,一类是“特权指令”,这类指令多涉及硬件操作,另一类是普通指令。CPU在执行指令时工作在两种模式下:“内核模式(Kernel mode)”以及“用户模式(User mode)”。CPU在用户模式下只能执行部分整个指令集的一部分,比如在用户模式下不能执行与I/O有关的指令,不能访问整个内存的地址空间,不可以执行特权指令。但是在内核模式下,CPU可以执行其指令集架构允许的任何机器指令包括特权指令,可以访问所有的内存地址,可以执行I/O操作等等。

因此你应该已经猜到了,当执行用户程序时(也就是我们写的程序时),CPU工作在用户模式,这时,CPU的操作是受限的,也就是说用户程序是受到限制的,用户程序不可以任意访问内存,不可以直接向I/O设备发起请求等等。当CPU开始运行操作系统时,CPU工作在内核模式下,这时,CPU可以执行任意操作,操作系统充分信任自己,因此操作系统对自己不会施加限制。

实际上,CPU有多少种工作模式取决于CPU设计者,使用哪些工作模式取决于操作系统设计者

比如实际上x86架构的CPU有四种工作模式,被称为Ring0,Ring1,Ring2,Ring3,受限程度依次增大,在Ring0状态下不受任何限制。流行的操作系统比如Windows,Linux,MacOS就只是使用了其中两种工作模式:内核模式与用户模式。

如果我们写的程序企图在用户模式下执行特权指令(比如通过汇编语言)是会被CPU检测到的,CPU进而产生异常,进而执行操作系统的异常处理逻辑,异常处理逻辑判断出是我们的程序企图执行特权指令,这当然是不被允许的,操作系统将终止掉我们的进程。因此你会看到,之所以划分内核模式和用户模式是为了更好的保护整个计算机系统。

我们已经知道了一般情况下CPU工作在两种模式下,那么如何从普通模式转移到内核模式呢?

从普通的用户模式转到内核模式必须提前定义好,这个转换过程必须由操作系统控制,这是唯一合法的途径,否则用户程序不受限制的转换到内核模式可能会破坏整个计算机系统。在下面的章节中你会看到,这是通过一种叫做“系统调用”的机制来实现的,这也是普通用户程序向操作系统发起请求的唯一合法途径。通过系统调用CPU可以从执行用户程序的用户模式转移到执行操作系统的内核模式。


总结

现在你应该对CPU有很好的理解了,CPU不断从内存中取出指令、执行指令,然后继续取指令重复这一过程。通过CPU的程序计数器我们知道CPU接下来要执行的指令在内存的哪个位置上,通过栈指针我们可以知道当前正在执行函数的栈帧在内存的哪个位置上。一般情况下当CPU执行程序时工作在用户模式,当执行操作系统时工作在内核模式。

从这个过程中我们也可以看到,实际上CPU每次只能执行一个程序。你可能会问,这怎么可能呢,我之前很旧的电脑上只有一个CPU,而且可以同时听音乐上网下载电影,如果CPU每次只能执行一个程序,那么上面说的这些程序是如何同时运行起来的呢?

CPU确实每次只能执行一个程序(进程),但是有了操作系统情况就不太一样了,操作系统的另一项魔法就是让多个程序(进程)同时运行,每个运行中的进程都觉得自己独占CPU,现在操作系统已经拥有两项魔法了。

  • 让每个进程都觉得自己独占CPU

  • 让每个进程都觉得自己独占内存

既然每个进程都有了自己的CPU,那么这些进程就可以同时运行啦,这就是我们熟悉的多任务(MultiTasking)。我们会在后面的文章《操作系统如何管理进程》中详细讲解操作系统是到底如何做到这一点的。


计算机基础决定程序员职业生涯高度
--码农的荒岛求生

操作系统系列

基础篇
1,什么是程序

2,程序?进程?傻傻分不清

3,程序员应如何理解内存:上篇
4,程序员应如何理解内存:中篇
5,程序员应如何理解内存:下篇
6,程序员应如何理解CPU:上篇



    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存