操作系统底层工作的整体认识
一、冯诺计算详解
1、冯诺计算机的工作原理
程序将指令加载到内存,通过控制器的译码,将指令的第一条加载到cpu的寄存器中,然后cpu进行相关的运算,如、加、乘、除、位、异或等运算。然后将运算得到的结果在次加载到内存中。一直重复以上的动作,直到内存中的指令全部执行完成(遇到停止指令),这就是冯诺计算机的原理。
程序与数据一样存贮,按程序编排的顺序,一步一步地取出指令,自动地完成指令规定的操作是计算机最基本的工作模型。这一原理最初是由美籍匈牙利数学家冯.诺依曼于1945年提出来的,故称为冯.诺依曼计算机模型。而现代计算机是基于冯诺计算机来实现的。
2、计算机的五大核心组成部分
1)输入:输入设备是计算机组成的重要部分,输入设备和输出设备合称为计算机的外部设备,简称外设。**输入的作用是将程序、原始数据、字符、字节、控制命令或者现场采集的数据等信息输入到计算机。**常见的输入设备有键盘、鼠标、光电输入机、磁带机、磁盘机、光盘机等。
2)存储器:存储器的功能是存储程序、指令、和各种控制信息,并在需要的时候提供这些信息。
3)运算器:运算器的功能是各种数据进行相关的运算(算术运算和逻辑运算),即处理数据。
4)控制器:是整个计算机的中枢神经,其功能是对程序规定的控制信息进行解释,根据其要求进行控制,调度程序、数据、地址,协调计算机各部分工作及内存与外设的访问等。
5)输出:输出设备同样是计算机组成的重要部分,**他的作用是将计算机处理的中间结果或者最后结果、计算机内的各种数据符号和控制信号等信息输出来。**微机常用的输出设备有显示终端CRT、打印机、激光印字机、绘图仪及磁带、光盘机等。
作为java开发人员,我们更多的只关注存储器和运算器,因为只有把这两个理解透彻,才能更好的进行并发编程。
3、冯诺计算机的抽象模型图解
二、冯诺计算机在当代计算机中的具体应用的结构图
1、现代计算机当中的硬件结构设计图
在上述的硬件结构图中,与java息息相关的就两个:CPU和内存,所以我们只需要着重学习CPU和内存即可。
2、CPU的组成结构:CPU=控制单元+运算单元+存储单元
CPU内部结构图:
**控制单元**
**控制单元是整个CPU的指挥中心(调度中心),由指令寄存器(Instruction Register)、指令译码器ID(Instruction Decoder)和操作控制器OC(Operation Contoller)等组成,对协调整个计算机工作极其重要。**它根据用户预编译好的程序,依次从存储器中取出各条指令,放在指令寄存器IR中,通过指令译码器ID分析确定什们操作,然后通过操作控制器OC,按确定的时序,向相应的部件发出微操作控制信号。操作控制器OC中包括:节拍脉冲发生器、控制矩阵、时钟脉冲发生器、复位电路和启停电路等控制逻辑。
运算单元
**运算单元是运算器的核心。可以执行算术运算(加减乘除等基本运算)和逻辑运算(包括移位、逻辑测试或两个值比较)。**相对控制单元而言,运算器接受控制单元的命令而进行动作,即运算单元所进行的全部操作都是由控制单元发出的控制信号来指挥的,所以它是执行部件。
存储单元
存储单元包括 CPU 片内缓存Cache和寄存器组,是 CPU 中暂时存放数据的地方,里面保存着那些等待处理的数据,或已经处理过的数据,CPU 访问寄存器所用的时间要比访问内存的时间短。 寄存器是CPU内部的元件,寄存器拥有非常高的读写速度,所以在寄存器之间的数据传送非常快。采用寄存器,可以减少 CPU 访问内存的次数,从而提高了 CPU 的工作速度。寄存器组可分为专用寄存器和通用寄存器。专用寄存器的作用是固定的,分别寄存相应的数据;而通用寄存器用途广泛并可由程序员规定其用途。
3、CPU的缓存结构
现代的CPU为了提升CPU工作效率,减少了CPU与内存的交互(交互越多,效率越低,主要要原因是内存处理数据的速度没有CPU快,并且内存离CPU相对于寄存器来说,较远,也影响了执行效率),通常采用的是CPU的三级缓存结构。
L1 Cache (256kb):分为数据缓存和指令缓存,逻辑核独占
L2 Cache(2MB):物理核独占,逻辑核共享
L3 Cache(6MB):所有物理核共享
注意:其中每级缓存的大小与计算有关,不同的计算机和不同的厂商,L1,L2、L3的大小也有所不同。L1、L2是CPU内核私有的,而L3是CPU内核共享的,但是L1、L2、L3都是CPU私有的,也就是说不能跨CPU访问L1、L2、L3。
存储器存储空间的大小:内存>L3>L2>L1>寄存器
存储器速度快慢排序:寄存器>L1>L2>L3>内存
需要注意的一点是:缓存是由最小的存储块(cacheLine)即缓存行组成的,一个缓存行最多存储64byte(字节),如果数据太大,怎么存储呢?我们都知道,实质上我们存储的数据都是聚合量,计算机在存储聚合量的时候,都会分解成标量存储,一个很大聚合量可以被分解成很多标量存储,当一个缓存行存储不下的时候,就会使用多个缓存行存储。简而言之就是“先分在合”。缓存行是什么意思呢?比如L1的大小是256kb,那么就有256*1024/64个缓存行。
4、CPU读取存储器数据的过程(五种情况)
1、读取寄存器中x的值:直接读取。
2、读取L1 cache中x的值:需要1-3步,或者更多步。把cacheLine锁住,把x的数据拿过来,如果没有锁住,就比较慢了。
3、读取L2 cache中x的值:先到L1 cache中取,如果L1 cache中没有,在到L2 cache中,L2 cache中加锁,加锁以后,把x的数据复制到L1 cache中,然后在执行cpu从L1 cache中读取数据的过程,最后在解锁。
4、读取L3 cache中x的值:CPU取L3 cache的也是一样,只不过先由L3复制到L2,从L2复制到L1,从L1到CPU。
5、读取内存中x的值:CPU取内存则最复杂->通知内存控制器占用总线带宽,通知内存加锁,发起内存读请求,等待回应,回应数据保存到L3(如果没有就到L2),再从L3/2到L1,再从L1到CPU,之后解除总线锁定。
5、为什么CPU要有高速缓存
CPU在摩尔定律的指导下以18个月翻一番的速度在发展(处理速度也在翻一番)在发展,然而内存和硬盘的发展速度根本不及CPU。这就造成了高性能的内存和高昂的硬盘价格。为了解决这个问题,各个厂商在CPU内部内置了少量的告诉缓存来解决CPU处理速度与I/O速度不匹配的问题。
在CPU访问存储设备时,无论是存取数据亦或是存取指令,**都趋于聚集在一片连续的区域中,这就是局部性原理。**局部性原理分为时间局部性和空间局部性
时间局部性(Temporal Locality):如果一个信息正在被访问,那么它近期可能还会被访问。比如循环,递归,重复调用的函数等。
空间局部性(Spatial Locality):如果一个存储器的位置被引用,那么近期它附近的位置也会被引用。比如顺序执行的代码、数组、连续创建的两个对象等。
举个空间局部性原理的例子:二维数组求和
1、按行求和,数组是连续的地址,满足空间局部性原理,用的时间比较少,因为连续的数组元素的地址引用被加载到cpu缓存,处理效率更高。
2、按列求和,按列的时候,不是连续的地址,不满足空间的局部性原理,加载到cpu缓存的数组元素的地址引用比较少,处理效率较低。
package com.bugchen.spring.test.audition.supervene;
public class TwoArraySum {
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) {
//初始化数组
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("初始化数组完成");
//按行求和
long sum = 0L, start = System.currentTimeMillis();
for (int r = 0; r < RUNS; r++) {
for (int row = 0; row < DIMENSION_1; row++) {
for (int col = 0; col < DIMENSION_2; col++) {
sum += longs[row][col];
}
}
}
System.out.println("spend_time_row:" + (System.currentTimeMillis() - start));
System.out.println("sum_row:" + sum);
//按列求和
sum = 0L;
start = System.currentTimeMillis();
for (int r = 0; r < RUNS; r++) {
for (int col = 0; col < DIMENSION_2; col++) {
for (int row = 0; row < DIMENSION_1; row++) {
sum += longs[row][col];
}
}
}
System.out.println("spend_time_col:" + (System.currentTimeMillis() - start));
System.out.println("sum_col:" + sum);
}
}
测试结果:
由此可以看出,遵循空间局部性编码,性能将近提升了4倍之多。
5、带有高速缓存的CPU执行计算流程
1、程序及数据加载到主内存
2、指令和数据被加载CPU告诉缓存区(L1、L2、L3、寄存器)
3、CPU执行指令,将计算后的结果写回高速缓存区
4、告诉缓存去把结果写回主内存
6、CPU运行的安全级别:ring0,ring1,ring2,ring3
1)安全级别:ring0>ring1>ring2>ring3
2)Linux与Windows只用到了2个级别:ring0、ring3,**操作系统内部内部程序指令通常运行在ring0级别,操作系统以外的第三方程序运行在ring3级别,第三方程序如果要调用操作系统内部函数功能,由于运行安全级别不够,必须切换CPU运行状态,从ring3切换到ring0,然后执行系统函数,说到这里相信同学们明白为什么JVM创建线程,线程阻塞唤醒是重型操作了,因为CPU要切换运行状态。**这里就是用户态和内核态的相互转化。
下面我大概梳理一下JVM创建线程CPU的工作过程:
step1:CPU从ring3切换ring0创建线程
step2:创建完毕,CPU从ring0切换回ring3
step3:线程执行JVM程序
step4:线程执行完毕,销毁还得切会ring0
7、操作系统管理内存
1)执行空间保护
操作系统有用户空间与内核空间两个概念,目的也是为了做到程序运行安全隔离与稳定,以32位操作系统4G大小的内存空间为例:
Linux为内核代码和数据结构预留了几个页框,这些页永远不会被转出到磁盘上。从 0x00000000 到 0xC0000000(PAGE_OFFSET) 的线性地址可由用户代码 和 内核代码进行引用(即用户空间)。从0xC0000000(PAGE_OFFSET)到 0xFFFFFFFFF的线性地址只能由内核代码进行访问(即内核空间)。内核代码及其数据结构都必须位于这 1 GB的地址空间中,但是对于此地址空间而言,更大的消费者是物理地址的虚拟映射。
这意味着在 4 GB 的内存空间中,只有 3 GB 可以用于用户应用程序。进程与线程只能运行在用户方式(usermode)或内核方式(kernelmode)下。用户程序运行在用户方式下,而系统调用运行在内核方式下。在这两种方式下所用的堆栈不一样:用户方式下用的是一般的堆栈(用户空间的堆栈),而内核方式下用的是固定大小的堆栈(内核空间的对战,一般为一个内存页的大小),即每个进程与线程其实有两个堆栈,分别运行与用户态与内核态。
由空间划分我们再引深一下,CPU调度的基本单位线程,也划分为:
1、内核线程模型(KLT)
2、用户线程模型(ULT)
内核线程模型:
内核线程(KLT):系统内核管理线程(KLT),内核保存线程的状态和上下文信息,线程阻塞不会引起进程阻塞。在多处理器系统上,多线程在多处理器上并行运行。线程的创建、调度和管理由内核完成,效率比ULT要慢,比进程操作快。
用户线程模型:
用户线程(ULT):用户程序实现,不依赖操作系统核心,应用提供创建、同步、调度和管理线程的函数来控制用户线程。不需要用户态/内核态切换,速度快。内核对ULT无感知,线程阻塞则进程(包括它的所有线程)阻塞。JVM采用的是KLT模型。
8、进程与线程
1)什么是进程?
现代操作系统在运行一个程序时,会为其创建一个进程;例如,启动一个Java程序,操作系统就会创建一个Java进程。进程是OS(操作系统)资源分配的最小单位。
2)什么是线程?
线程是OS(操作系统)调度CPU的最小单元,也叫轻量级进程(Light Weight Process),在一个进程里可以创建多个线程,这些线程都拥有各自的计数器、堆栈和局部变量等属性,并且能够访问共享的内存变量。CPU在这些线程上高速切换,让使用者感觉到这些线程在同时执行,即并发的概念,相似的概念还有并行!
线程上下文切换过程:
个进程;例如,启动一个Java程序,操作系统就会创建一个Java进程。进程是OS(操作系统)资源分配的最小单位。
2)什么是线程?
线程是OS(操作系统)调度CPU的最小单元,也叫轻量级进程(Light Weight Process),在一个进程里可以创建多个线程,这些线程都拥有各自的计数器、堆栈和局部变量等属性,并且能够访问共享的内存变量。CPU在这些线程上高速切换,让使用者感觉到这些线程在同时执行,即并发的概念,相似的概念还有并行!
线程上下文切换过程: