1、名词解释
| 名词 | 白话解释 |
|---|---|
| 程序是什么 | 指示计算机每一步动作的一组指令 |
| 程序是由什么组成 | 指令和数据 |
| 什么是机器语言 | CPU可以直接识别并使用的语言 |
| 什么是内存地址 | 内存中用来表示命令和数据存储位置的数值 |
| 进程是什么 | 是指被加载到内存中,可被CPU调度的程序 |
2、CPU简介
以下摘自百度:central processing unit,中央处理器,作为计算机系统的运算和控制核心,是信息处理、程序运行的最终执行单元。白话来说,CPU就是计算机的大脑。
那CPU主要做什么工作呢?很简单,就4步:从内存取指令放到寄存器中->翻译指令->执行指令->指令执行结果再放回到内存。
看到上文的简要流程后,是不是脑子里产生了更多的问号?
CPU执行过程真这么简单?为啥感觉大学时计组学不明白呢?于是又有好多问题冒了出来,CPU的架构到底是怎么样的?有哪些模块组成?各模块间是如何协作的?哪个模块是取指令的?哪个又是计算的?寄存器又是什么?不急,我们一步步来,下面先解释一下CPU的构成和各个模块都是做什么的,然后再详细介绍工作流程。
3、CPU的构成
CPU由寄存器、控制器、运算器、时钟4个部分组成
●控制器:负责把内存上的指令、数据读入寄存器,并根据指令的执行结果来控制计算机;
●运算器:负责运算从内存读入寄存器的数据;
●时钟:产生中断用的,时钟是通过硬件的晶振定期触发,传送电流给CPU后,CPU回调操作系统内核,由操作系统内核根据中断去做进程/线程切换,来实现cpu抢占式调度用,在下文介绍上下文切换中会详细说明。
●寄存器:存放指令和数据的,常见的寄存器主要就四种:
a. 通用寄存器:用来存放需要进行运算的数据,比如需要进行加和运算的两个数据;
b. 程序计数器:用来存储CPU要执行下一条指令的内存的地址(存储的是指令内存的地址,不是指令);
c. 指令寄存器:用来存放程序计数器所指向的内存中的指令,在指令被cpu执行完成并写回内存前,指令都是放在这里的;
d. 标志寄存器:主要存放条件判断后的结果,供CPU做条件判断用,存放的值有正数、负数、0三种类型,例如:if(5>3) 在CPU内部就是5-3后的结果存放到标志寄存器中。
4、CPU的工作原理
CPU做的事情比较简单,主要就是从内存取指令放到寄存器中->翻译指令->根据指令取数据或计算->计算结果再放回到内存。
无论是做数据的运算还是存取,CPU都是没有自主控制能力的,完全是根据内存中的指令来按部就班的执行的,那内存中的指令又是什么的?内存中的指令其实就是我们所写的程序,转换成CPU能识别的机器码后存放到内存的,这里有一个问题:那JAVA的程序是怎么放到内存的?JVM和内存又是什么关系?这块在后面的文章会有详细介绍,这里不过多描述,继续看CPU的工作原理。
我们以下面程序的为例,对照着图看一下cpu是怎么运行的:
int a = 1;
int i = 2;
int j = 3;
if(a > 0) {
result = i + j;
}
下文是对上图的详细介绍,在介绍上图的流程前,有必要先说一下上图中的内存结构和内存中的指令分别代表什么意思。
4.1、内存区域分块和指令介绍
内存中主要包括两块区域,一块是存放指令的区域,一块是存放操作数的区域。指令区存放的是操作指令,数据区存放的是数据,指令区可以通过指令让CPU读取数据区中的数据并存放到CPU寄存器中。
上图中内存中的指令我写的是简易的汇编代码,目的是为了方便理解指令的具体内容,事实上指令的内容是一串二进制数字的机器码,每条指令都有对应的机器码,CPU 通过解析机器码来知道指令的内容。
下表是对内存中的指令的翻译说明:
| 地址 | 指令 | 说明 |
|---|---|---|
| 0X200 | load 0x100 ->R0 | 把数据区0x100中的数据放入寄存器R0 |
| 0X204 | cmp R0 > 0 jne | 比较寄存器R0和0的值,R0-0的结果放到标示寄存器中,如果R0 >0 执行JNE跳转指令,把CPU程序计数器的值改为0X20C |
| 0X20C | load 0x104->R1 | 把0X104中的数据放入寄存器R1 |
| 0X20F | load 0x108->R2 | 把0X108的数据放入寄存器R2中 |
| 0X304 | add R1 R2 R3 | 把R1寄存器和R2寄存器中的数据相加,存放到R3中。 |
| 0X308 | set R3 -> 0x10C | 把R3中的数据回写到内存中 |
4.2、内存区域分块和指令介绍
下文是对上图中的CPU工作流程的详细说明:
1)CPU的控制器会先去程序计数器中取得需要取指令的内存地址;
注:对于一个程序加载到内存转换成进程后,操作系统会把 CPU 程序计数器的地址指向程序的开始位置,如何识别程序的开始位置?答:通过入口函数,所以对于任何一个程序都要有入口方法,就像 JAVA 的 main 方法一样;
2)去相应的地址中获取指令;
3)读到指令后存放到指令寄存器中;
4)指令存放到寄存器后,由指令译码器对指令进行翻译,根据翻译结果进行不同的操作;
5)如果是存取数据则直接把数据存放到寄存器组中R0-R7;
6)如果是计算指令,是直接根据指令调用逻辑运算单元进行计算,例如1+2,这里会在逻辑运算单元进行计算,计算后会把结果再放回寄存器,如果是条件运算,则会把条件比较的结果放到标示寄存器中,标志寄存器中根据A-B的结果,存放正数、负数、0三个值,这里需要注意的是逻辑运算单元只能从寄存器中取数据进行运算;
7)如果是逻辑运算的话,那CPU控制器会根据标示寄存器中的值结合内存中的JNE指令,修改程序计数器的值,正常情况下程序计数器上顺序累加执行,当碰到条件判断或是循环时就会执行JNE指令,进行跳转。
5、CPU的进程(上下文)切换
5.1、为什么要做上下文切换
我们编写的代码只是一个存储在硬盘的静态文件,通过编译后就会生成二进制可执行文件,当我们运行这个可执行文件后,它会被装载到内存中,接着 CPU 会执行程序中的每一条指令,那么这个运行中的程序,就被称为「进程」(Process)。
现在我们考虑有一个会读取硬盘文件数据的程序被执行了,那么当运行到读取文件的指令时,就会去从硬盘读取数据,但是硬盘的读写速度是非常慢的,那么在这个时候,如果 CPU 傻傻的等硬盘返回数据的话,那 CPU 的利用率是非常低的,所以,当进程要从硬盘读取数据时CPU不需要阻塞等待数据的返回,而是去执行另外的进程,当硬盘数据返回时,CPU会收到中断(上文在CPU的概念中有介绍过)后回调系统内核,系统内核再去调度进程。
CPU什么时候会进行上下文切换?对于抢占式操作系统而言,大体有几种:
●当前任务的时间片用完之后,系统CPU正常调度下一个任务;
●当前任务碰到IO阻塞,调度线程将挂起此任务,继续下一个任务;
●多个任务抢占锁资源,当前任务没有抢到,被调度器挂起,继续下一个任务;
●用户代码挂起当前任务,让出CPU时间;
●硬件中断。
当自愿上下文切换变多了,说明进程都在等待资源,有可能发生了 I/O 等其他问题;
非自愿上下文切换变多了,说明进程都在被强制调度,也就是都在争抢 CPU,说明 CPU 的确成了瓶颈。
5.2、上下文切换多为什么会影响性能
为什么CPU过于频繁的切换,往往会引发性能问题?由于cpu是按照时间片抢占的方式来执行的,当cpu处理能力不变的情况下,当进程/线程过多的时候,就会导致CPU处理单个进程的时间单位变少,为什么会变少?主要两个原因:
1.CPU处理的总量不变,进程/线程越来越多,那分给每个进程/线程的时间自然会变少;
2.上下文频繁切换占用CPU执行时间:当进程/线程切换过多时,就会导致CPU把越来越多的时间用在上下文切换上,而不是程序的实际执行上,从而导致每个进程/线程的时间变短,从而降低程序执行效率。
切换是需要保存和恢复现场的,当发生切换时,操作系统会把CPU当前执行的指令,寄存器中的数据还有程序计数器的值都保存到当前进程的上下文(PCB)中,然后回写到内存,待下次再切换回来时,操作系统会把这些数据再都初始化到CPU中,而进程/线程切换也是包括在CPU执行的时间片中的,类似于公式:CPU执行的时间片时间=进程下文切换时间+进程执行时间。
5.3、线程上下文切换
线程与进程最大的区别在于:线程是调度的基本单位,而进程则是资源拥有的基本单位。
所以,所谓操作系统的任务调度,实际上的调度对象是线程,而进程只是给线程提供了虚拟内存、全局变量等资源。
对于线程和进程,我们可以这么理解:
●当进程只有一个线程时,可以认为进程就等于线程;
●当进程拥有多个线程时,这些线程会共享相同的虚拟内存和全局变量等资源,这些资源在上下文切换时是不需要修改的;
另外,线程也有自己的私有数据,比如栈和寄存器等,这些在上下文切换时也是需要保存的。
线程上下文切换的是什么?
这还得看线程是不是属于同一个进程:
当两个线程不是属于同一个进程,则切换的过程就跟进程上下文切换一样;
当两个线程是属于同一个进程,因为虚拟内存是共享的,所以在切换时,虚拟内存这些资源就保持不动,只需要切换线程的私有数据、寄存器等不共享的数据;
所以,线程的上下文切换相比进程,开销要小很多。
5.4、CPU使用率和负载
5.4.1 使用率
以上截取自man top中对于CPU使用率的定义,总结来说某个进程的CPU使用率就是这个进程在一段时间内占用的CPU时间占总的CPU时间的百分比。
当有一个死循环并且是计算密集行的进程在的时候,那很快就会占满CPU使用,不是CPU是按时间片运行的吗?他可以切换到其它进程运行,为什么死循环的CPU占用率会高呢?。
答:死循环的时候程序不像其它的程序那样可能在某处被阻塞,比如sleep了,等IO了,这些可能被阻塞的程序在进程睡眠期间都不会被唤醒的,不会占用CPU的,而死循环则不会让出CPU,所以表现出CPU占用率高。
5.4.2 负载
负载就是cpu在一段时间内正在处理以及等待cpu处理的进程数之和的统计信息,也就是cpu使用队列的长度统计信息,这个数字越小越好(如果超过CPU核心*0.7就是不正常),CPU负载过高主要有以下几种情况:
1)并发量大;
2)过多的进程被阻塞处于等待中,需要重点关注IO读取、进程休眠、死锁等情况。