目录
为什么会引出CPU多级高速缓存?[换个问法:CPU为何要有高速缓存???]
对于Java程序中A+B这一运算指令在不同架构下的不同体现:
操作系统底层
冯诺依曼计算机模型详
现代计算机模型是基于-冯诺依曼计算机模型
计算机在运行时,先从内存中取出第一条指令,通过控制器的译码,按指令的要求,从存储器中取出数据进行指定的运算和逻辑操作等加工,然后再按地址把结果送到内存中去。接下来,再取出第二条指令,在控制器的指挥下完成规定操作。依此进行下去。直至遇到停止指令。
程序与数据一样存贮,按程序编排的顺序,一步一步地取出指令,自动地完成指令规定的操作是计算机最基本的工作模型。这一原理最初是由美籍匈牙利数学家冯.诺依曼于1945年提出来的,故称为冯.诺依曼计算机模型。
上面的模型是一个理论的抽象简化模型,它的具体应用就是现代计算机当中的硬件结构设计:
在上图硬件结构当中,配件很多,但最核心的只有两部分:CPU、内存。所以我们重点学习的也是这两部分。
CPU指令结构
CPU内部结构
- 控制单元
- 运算单元
- 数据单元
CPU结构具体分析:
控制单元:像if-else,while这种控制条件判断就是在控制单元所做的事情。控制单元中的指令寄存器保存着运算时所需的指令。
运算单元:对于一些运算,如:1*2 19/2等等,都是在运算单元做的
存储单元:存储一些指令以及运算结果的数据
以上都是自己的一些简略总结,下面是具体概念:
控制单元
控制单元是整个CPU的指挥控制中心,由指令寄存器IR(Instruction Register)、指令译码器ID(Instruction Decoder)和 操作控制器OC(Operation Controller) 等组成,对协调整个电脑有序工作极为重要。它根据用户预先编好的程序,依次从存储器中取出各条指令,放在指令寄存器IR中,通过指令译码(分析)确定应该进行什么操作,然后通过操作控制器OC,按确定的时序,向相应的部件发出微操作控制信号。操作控制器OC中主要包括:节拍脉冲发生器、控制矩阵、时钟脉冲发生器、复位电路和启停电路等控制逻辑。
运算单元
运算单元是运算器的核心。可以执行算术运算(包括加减乘数等基本运算及其附加运算)和逻辑运算(包括移位、逻辑测试或两个值比较)。相对控制单元而言,运算器接受控制单元的命令而进行动作,即运算单元所进行的全部操作都是由控制单元发出的控制信号来指挥的,所以它是执行部件。
存储单元
存储单元是包括CPU片内缓存Cache和寄存器组,是CPU中暂时存放数据的地方,里面保存着那些等待处理的数据,或已经处理过的数据,CPU访问寄存器所使用的时间要比访问内存的时间要短[因为寄存器相比内存而言,离CPU更加的接近,在同一块主板上访问,肯定近的访问的快]
寄存器组可以分为专用寄存器和通用寄存器。专用寄存器的作用是固定的,分别寄存相应的数据;而通用寄存器用途广泛并可由程序员规定其用途。
内存的结构分析:
内存中存储的就是一些指令以及数据段
内存中进程的指令是哪里来的?
eg:Java程序经过javac编译为二进制的class字节码文件,再通过类加载器进行加载到JVM的元空间中,元空间中的元数据就是内存中存储的这些指令以及数据。
CPU缓存结构
现代CPU为了提升执行效率,减少CPU与内存的交互(访问内存是很慢的,因此CPU与内存的交互是影响CPU的效率的),一般我们都在CPU上集成了多级缓存架构,最常见的是三层缓存结构。
- L1 Cache,分为数据缓存和指令缓存,逻辑核独占
- L2 Cache,物理核独占,逻辑核共享
- L3 Cache,所有物理核共享
如下图:电脑CPU图,引出之后的一系列问题。
内核和逻辑处理器的区别
内核就是CPU真正的处理核,每一个核可以执行一个线程任务
逻辑处理器是逻辑上的核,内核在超线程等技术的优化下,可以一个内核处理2个线程任务,那么逻辑上8个内核可以处理16个线程任务。所以逻辑处理器为16,但是这个逻辑处理器不一定真的为16,存在一些误差。
为什么会引出CPU多级高速缓存?[换个问法:CPU为何要有高速缓存???]
CPU在摩尔定律的指导下以每18个月翻一番的速度在发展,然而内存和硬盘的发展速度远远不及CPU。这就造成了高性能能的内存和硬盘价格及其昂贵。然而CPU的高度运算需要高速的数据,但是内存把指令传输到CPU的速度远远不及CPU的高速运转速度。
为了解决这个问题,CPU厂商在CPU中内置了少量的高速缓存以解决I\O速度和CPU运算速度之间的不匹配问题。
根据" 离CPU越近,那么越快。并且空间越大,速度越慢" 得出结论:
存储器存储空间大小:内存>L3>L2>L1>寄存器;
存储器速度快慢排序:寄存器>L1>L2>L3>内存;
分析:
其中L1,L2是CPU中多个内核独享的,每一个内核都有自己独立的L1,L2,也就说每一个内核绑定一个L1,L2。
L3是CPU中多个内核共享的,但是注意:多个CPU的多个内核之间是不共享L3的
为什么L1缓存的空间要小?
因为L1这个缓存区是最靠近CPU核心的,CPU的地方空间是有限的,就这么大,假设说把L1的缓存设置的非常大,那么把CPU的核心资源都占用的太多了,那就危害到CPU核心的其它组件的占用了。
L1缓存分为两部分,一部分为指令缓冲区,一部分为数据缓存区。
缓存行
还有一点值得注意的是:缓存是由最小的存储区块-缓存行(cacheline)组成,缓存行大小通常为64byte。
缓存行是什么意思呢?
比如你的L1缓存大小是512kb,而cacheline = 64byte,那么就是L1里有512 * 1024/64个cacheline
注:1mb = 1024kb = 1024*1024byte = 1024*1024 * 8 bit(bit位即二进制位)
缓存行实际案例:
public class TwoDimensionalArraySum {
private static final int RUNS = 100;
private static final int DIMENSION_1 = 1024 * 1024;
private static final int DIMENSION_2 = 6;
private static long[][] longs;
public static void main(String[] args) throws Exception {
/*
* 初始化数组
*/
longs = new long[DIMENSION_1][];
for (int i = 0; i < DIMENSION_1; i++) {
longs[i] = new long[DIMENSION_2];
for (int j = 0; j < DIMENSION_2; j++) {
longs[i][j] = 1L;
}
}
System.out.println("Array初始化完毕....");
long sum = 0L;
long start = System.currentTimeMillis();
for (int r = 0; r < RUNS; r++) {
for (int i = 0; i < DIMENSION_1; i++) {//DIMENSION_1=1024*1024
for (int j=0;j<DIMENSION_2;j++){//6
sum+=longs[i][j];
}
}
}
System.out.println("spend time1:"+(System.currentTimeMillis()-start));
System.out.println("sum1:"+sum);
sum = 0L;
start = System.currentTimeMillis();
for (int r = 0; r < RUNS; r++) {
for (int j=0;j<DIMENSION_2;j++) {//6
for (int i = 0; i < DIMENSION_1; i++){//1024*1024
sum+=longs[i][j];
}
}
}
System.out.println("spend time2:"+(System.currentTimeMillis()-start));
System.out.println("sum2:"+sum);
}
}
运行结果:
CPU读取存储器数据过程
1、CPU要取寄存器X的值,只需要一步:直接读取。
2、CPU要取L1 cache的某个值,需要1-3步(或者更多):把cache行锁住,把某个数据拿来,解锁,如果没锁住就慢了。
3、CPU要取L2 cache的某个值,先要到L1 cache里取,L1当中不存在,在L2里,L2开始加锁,加锁以后,把L2里的数据复制到L1,再执行读L1的过程,上面的3步,再解锁。
4、CPU取L3 cache的也是一样,只不过先由L3复制到L2,从L2复制到L1,从L1到CPU。
5、CPU取内存则最复杂:通知内存控制器占用总线带宽,通知内存加锁,发起内存读请求,等待回应,回应数据保存到L3(如果没有就到L2),再从L3/2到L1,再从L1到CPU,之后解除总线锁定。
CPU读取存储器数据的过程总结如图:
CPU为何要有高速缓存?
CPU在摩尔定律的指导下以每18个月翻一番的速度在发展,然而内存和硬盘的发展速度远远不及CPU。这就造成了高性能能的内存和硬盘价格及其昂贵。然而CPU的高度运算需要高速的数据。为了解决这个问题,CPU厂商在CPU中内置了少量的高速缓存以解决I\O速度和CPU运算速度之间的不匹配问题。
在CPU访问存储设备时,无论是存取数据抑或存取指令,都趋于聚集在一片连续的区域中,这就被称为局部性原理。
时间局部性(Temporal Locality):如果一个信息项正在被访问,那么在近期它很可能还会被再次访问。
比如循环、递归、方法的反复调用等。
空间局部性(Spatial Locality):如果一个存储器的位置被引用,那么将来他附近的位置也会被引用。所以说,我们要把所取的数据附近的数据也一块从内存中读取出来。
比如顺序执行的代码、连续创建的两个对象、数组等。
带有高速缓存的CPU执行计算的流程
- 程序以及数据被加载到主内存
- 指令和数据被加载到CPU的高速缓存
- CPU执行指令,把结果写到高速缓存
- 高速缓存中的数据写回主内存
CPU运行安全等级
CPU有4个运行级别,分别为:
- ring0 - 内核空间 即内核态
- ring1
- ring2
- ring3 - 用户空间 即用户态
Linux与Windows只使用到了两个级别:ring0,ring3(对于四个运行级别同时使用的难度是很大的)
(1) 操作系统内部程序指令通常运行在ring0级别。ring0级别即是内核空间,即内核态,这个空间内是重要系数较高的空间,涉及到计算机的一些功能设置,如:时钟重置,密码重置等等,操作ring0级别是安全系数较高的操作
(2) 操作系统以外的第三方程序运行在ring3级别。第三方程序(如 360,百度等)如果要进行调用操作系统内部函数功能,由于安全级别不够,必须进行切换CPU的运行状态,从ring3切换到ring0,然后执行操作系统中的内置函数。
说到这里相信应该明白为什么JVM创建线程,线程阻塞唤醒,杀死线程是重型操作了,因为CPU要切换运行状态。
操作系统内存管理
执行空间保护
操作系统有用户空间与内核空间两个概念,目的也是为了做到程序运行安全隔离与稳定,以32位操作系统4G大小的内存空间为例
Linux为内核代码和数据结构预留了几个页框,这些页永远不会被转出到磁盘上。从 0x00000000 到 0xC0000000(PAGE_OFFSET) 的线性地址可由用户代码 和 内核代码进行引用(即用户空间)。从0xC0000000(PAGE_OFFSET)到 0xFFFFFFFFF的线性地址只能由内核代码进行访问(即内核空间)。内核代码及其数据结构都必须位于这 1 GB的地址空间中,但是对于此地址空间而言,更大的消费者是物理地址的虚拟映射。
这意味着在4GB的内存空间中,只有3GB可以用于用户应用程序。进程与线程只能运行在用户方式或内核方式下。用户程序运行在用户方式下,然而系统调用运行在内核方式下。在这两种方式下所使用的堆栈不一样:用户方式下使用的是一般的堆栈(用户空间的堆栈),而内核方式下使用的是固定的堆栈(内核空间的堆栈,一般为一个内存页的大小),即每个进程与线程其实有两个堆栈,分别运行与用户态与内核态。
由空间划分我们再引深一下,CPU调度的基本单位线程,也划分为:
1、内核线程模型(KLT)
2、用户线程模型(ULT)
内核线程模型
内核线程(KLT):系统内核管理线程(KLT),内核保存线程的状态和上下文信息,线程阻塞不会引起进程阻塞。在多处理器系统上,多线程在多处理器上并行运行。线程的创建、调度和管理由内核完成,效率比ULT要慢,比进程操作快。
用户线程模型
用户线程(ULT):用户程序实现,不依赖操作系统核心,应用提供创建、同步、调度和管理线程的函数来控制用户线程。不需要用户态/内核态切换,速度快。内核对ULT无感知,线程阻塞则进程(包括它的所有线程)阻塞。
到这里,大家不妨思考一下,jvm是采用的哪一种线程模型?
KLT线程模型。因为JVM需要从用户态切换到内核态,比如说:jvm执行创建线程的操作就必须从用户态切换到内核态,并且把用户空间的堆栈嵌入到内核空间的堆栈,清空原本用户空间的堆栈数据。在内核态中操作系统进行调用底层的函数库进行创建线程的操作。创建线程完成后,才会从内核态切换到用户态,同理转移嵌入堆栈中的数据。
状态切换
所谓状态切换即是CPU运行安全级别从ring0切换到ring3或从ring3切换到ring0。如下两个案例都是讲解何为状态切换:
JVM创建线程的过程总结:
线程不是JVM创建的,是调用底层操作系统的p_three的库进行创建的
步骤如下:
step1:CPU从ring3切换ring0创建线程
step2:创建完毕,CPU从ring0切换回ring3
step3:线程执行JVM程序
step4:线程执行完毕,销毁还得切回ring0
线程阻塞唤醒的过程总结:
当线程执行阻塞,JVM(ring0)需要从ring0用户态切换到内核态。【详细解释:即是把用户空间嵌入到内核空间中,会把用户空间的堆栈数据(即记录指令运行到了哪一步都会转移给内核态的堆栈)转移到内核态中的堆栈,并且用户空间中的堆栈就先清空不会再有了,操作系统执行线程阻塞底层对应的函数库。】
操作系统底层函数调用阻塞方法进行执行完毕后,操作系统所在的内核态的堆栈数据会嵌入到用户态,从内核态切换回用户态。在用户态中的JVM会进行执行Block操作
状态切换总结:
一个CPU有两个空间:内核空间核用户空间,也即是内核态核用户态,这两个空间都分别具有自己独立的堆栈数据空间。在状态进行切换的时候,假设说从内核态切换为用户态,CPU会把内核态嵌入到用户态,把内核态的堆栈的数据嵌入保存到用户态中,嵌入完成后,内核态中原本的堆栈数据就不再有了,这些数据都已经嵌入到用户态中啦。
进程与线程
进程
现代操作系统在运行一个程序时,会为其创建一个进程。例如:启动一个Java程序,操作系统就会创建一个Java进程。进程是操作系统资源分配的最小单位。
线程
线程是操作系统调度CPU时的最小单元,也叫做轻量级进程,在一个进程里可以创建多个线程,这些线程都会拥有各自的计数器,堆栈和局部变量等属性,并且能够访问共享的内存变量。CPU在这些线程上高速切换,让使用者感觉到这些线程在同时执行,这即是并发的概念。
并发和并行的区别
并发是指在同一时间段内,系统能够同时处理多个任务或请求的能力。这些任务或请求可能是来自不同的用户或应用程序,它们可以是同时发生或部分重叠的。但是系统只有一个处理器,任务之间会交替执行,每个任务只会执行一小段的时间,然后就会切换到下一个任务,给人一种好像同时执行的感觉,这就是并发。并发处理的目的就是提高系统的吞吐量和响应时间,使得多个任务能够同时执行而不互相干扰。常见的并发处理技术包括锁、信号量、消息传递、线程池等。
并行是指在同一时间段内,系统能够处理多个任务或者请求的能力,但是每一个任务都可以拥有自己的处理器。系统给每一个任务对应分配一个处理器,在并行处理中,多个任务可以同时执行,但是每一个任务之间相互独立,互不干扰。并行处理的目的是提高系统的性能和吞吐量,因为每个任务都可以独立运行,不会受到其他任务的影响。常见的并行处理技术包括多核处理器、分布式计算、GPU加速等
因此,两者的区别在于处理方式和目的。并发处理使用单个处理器实现多任务处理,以提高系统的吞吐量和响应时间,而并行处理使用多个处理器实现多任务处理,以提高系统的性能和吞吐量。同时,由于并行处理需要使用多个处理器,所以它比并发处理更加复杂和成本更高。
吞吐量的定义
吞吐量是指在特定时间内通过某个系统、设备或过程的数据量、信息量、物品量等的总量。通常以每秒钟处理的单位数量来衡量,如数据传输速率、网络带宽、生产线生产速率等
例如,在网络通信中,吞吐量可以表示单位时间内传输的数据量,如每秒传输的比特数或字节数。在工厂生产中,吞吐量可以表示生产线每小时或每天生产的产品数量。在数据库系统中,吞吐量可以表示每秒钟处理的查询或事务数量。
因此,吞吐量是评估系统、设备或过程性能的重要指标,它可以帮助我们了解系统的处理能力、容量和效率,同时也可以用来比较不同系统、设备或过程之间的性能差异。
线程上下文切换:
一个CPU内核多个线程执行任务,CPU假设给这两个线程,线程1分配50ms的时间片,线程2分配100ms的时间片,当线程1时间片使用完后发现任务还没有执行完,那么此时就会保存当前运行状态到内存中的TSS空间(全称为Task State Segament,任务状态段) 。然后再把当前运行状态给到线程2,线程2接着线程1执行到任务的那个运行状态接着继续执行任务,这个过程就是线程的上下文切换。
线程上下文切换过程:
虚拟机指令集架构
虚拟机指令集架构主要分为两种:1.栈指令集架构 2.寄存器指令集架构
栈指令集架构
- 设计和实现更简单,适用于资源受限的系统;
- 避开了寄存器的分配难题:使用零地址指令方式分配;
- 指令流中的指令大部分是零地址指令,其执行过程依赖与操作栈,指令集更小,编译器容易实现;
- 不需要硬件支持,可移植性更好,更好实现跨平台。
寄存器指令集架构
- 典型的应用是x86的二进制指令集:比如传统的PC以及Android的Davlik虚拟机。
- 指令集架构则完全依赖硬件,可移植性差。
- 性能优秀和执行更高效。
- 花费更少的指令去完成一项操作。
- 在大部分情况下,基于寄存器架构的指令集往往都以一地址指令、二地址指令和三地址指令为主,而基于栈式架构的指令集却是以零地址指令为主。
Java符合典型的栈指令集架构特征,像Python、Go都属于这种架构
对于Java程序中A+B这一运算指令在不同架构下的不同体现:
栈指令集架构设计:
每一次出栈或者入栈操作都需要CPU对内存的数据进行获取一次或存储一次,这个过程是繁琐的消耗性能的,并且是耗时较长的,所以Java慢一些。
寄存器指令集架构:
见图可知:运算方式简单,速度还快。