计算机底层知识(一)

 

硬件基础知识(一)

先说说自己对操作系统的理解,因为windows,安卓,IOS等的OS,由于过早起步,所以对整个市场来说是很大优势,提前建立好了自己的生态这很重要,所以后来出的一些OS效果都不是很好,就是因为生态建立不起来,大家都已经用的习以为常了,很难再去改变。
然后讲讲华为鸿蒙,据我理解华为鸿蒙要做到的东西非常厉害,万物互联,例如家里的任何东西,冰箱,洗衣机,电视,空调等等都能做到互联,虽然现在还没到达那一步,也不知道将来什么时候能完成,反正就是很牛!
之前谷歌发出威胁,不让国内使用安卓还是怎么的,结构鸿蒙一出来,谷歌就怂了,要是让鸿蒙发展好了,生态建立起来了,那将来安卓还能保持领先地位吗?
类似这样的还有很多,例如java和go,oracle,mysql,说说go和java,go语言很厉害,但是职业分布上就是干Java的人多,因为java提前建立好的生态,就是这样的。
其实有一个现象,别人不给我们的东西,我们可以自己造啊,自力更生,鸿蒙不就是我们自己造的啊。

 

CPU制造过程

CPU是一块很小很小的芯片,是计算机的核心,这个不用我多说。在CPU制作过程中,最重要的要数晶体管的发现和使用了,一个晶体管非常小,晶体管在CPU内充当双位,即开(1)和关(0),这非常符合我们计算机的逻辑,0和1。

在当今的工艺上,怎样的CPU才是上好的CPU呢?
最重要的一点,就是如何在相同的CPU面积里放进去更多的晶体管,因为CPU很小,很精密,里面组成了数目相当多的晶体管,人手是绝对不可能完成的,只能通过光刻工艺进行加工的。目前为止,铝是制作CPU内部配件的主要金属,铜正逐渐被淘汰,铝的电迁移特性明显好于铜。
PS:电迁移效应(electro-migrationeffect)是指金属导线中的电子在大电流的作用下,产生电子迁移的现象,可能会引起电路的开路现象。

介绍一下CPU是如何制作的?
其实CPU最开始是由沙子提取而开始制作过程的,没错就是沙子,我们生活中那么不值钱不起眼的沙子,其实是经过精心挑选的沙子,因为沙子通过高温提纯,形成二氧化硅,是CPU的主要材料,可以在上面构建电路,简单来说就是一堆沙子+一堆铜+一堆胶水+特定金属添加+特殊工艺

总的步骤是:
沙子脱氧->石英->二氧化硅->提纯->硅锭->切割->晶圆->涂抹光刻胶->光刻->蚀刻->清除光刻胶->电镀->抛光->铜层->测试->切片->封装
相当的繁琐,每一道工艺都是精益求精,不能有丝毫偏差,就拿光刻来说,就是在CPU上通过特定的光谱刻画在CPU形成基本的电路,一开始全世界只有荷兰掌握这种技术,你想想在CPU上刻画如此细小又众多的电路,这是相当难的,荷兰的技术可以说是遥遥领先,这种光刻机据说几十亿美金,而且是有钱买不到的那种,自己掌握的计算为什么要卖出去呢,对吧。后来中国也有了自己的光刻机,但是具体到多少纳米是比不上荷兰的,结果荷兰又愿意进口给我们了,因为我们自己已经能造出来,以后荷兰想再打入市场就很难了。

PS:具体的CPU是如何制作的可以参考https://www.sohu.com/a/255397866_468626(文字)

CPU原理

计算机需要解决的最根本问题:如何代表数字,所以有了晶体管。
CPU上面覆盖了几十层的电路,每一程都有很多的晶体管。让芯片工作需要一个通电断电的开关,叫做振荡器(也叫时钟发生器,是主板上的芯片)。
CPU计算的时候需要在输入端写入1和0就是通电断电,计算的过程是需要振荡器驱动(通电,断电)一步一步执行,CPU运行不需要手动输入的,最好是把要计算的数据放在内存,内存通过存储电信号以此避免手动输入,CPU会通过总线和内存相连接并去内存读取,如果提前写好在内存,就不需要开始的时候给CPU针脚通电断电。
以前CPU就懂0和1,后来就诞生了汇编。

汇编语言

汇编语言的本质:机器语言的助记符 其实它就是机器语言 

汇编语言的执行过程(时钟发生器 寄存器程序 计数器)

计算机通电->CPU读取内存中的程序(电信号输入)->振荡器不断震荡通电->推动CPU内部一步一步执行计算(执行多少步取决于指令需要的时钟周期,一次通电停电称为时钟周期)->计算完成->写回(电信号)->写给输出(显卡,sout或图形)

如果输出给内存,通电之后那个位置代表1和0,如果是输出给显卡,显卡的缓存代表屏幕上一个点,这是简单的模型,其实显示屏上的每个点都有可能是一个多位的数字代表,通过数字进行物理转换才能看到炫彩的屏幕效果。
有时候我们CPU的参数附带一个xxGHz的信息,此单位也叫时钟周期,如果是1GHz代表一秒钟内有10亿个时钟脉冲,称为主频。

内存的东西发送个显卡:CPU发出指令,内存里的数据通过内存总线到IO总线(DMA机制,Direct Memory Access,直接存储器访问)发送给显卡的缓存,对应屏幕上的点,显卡的频率是Hz,就是每秒几次去读取显卡的内存,然后将效果渲染在显示器上。

GPU,CPU是针对通用计算,GPU用作计算输出到显卡,人工智能大量的计算没有进行优化,GPU适合AI算法上的优化,但是现在一些芯片是AI芯片,底层设计会更适合AI运算,比GPU快。

然后说说java
java是一门解释语言,java编译完是字节码(ByteCode,CPU无法直接执行),当遇到读一个指令的时候是先交给JVM翻译成机器码(计算机认识),CPU能直接读取,JVM充当解释器。

字节码,ByteCode,是java的“汇编语言”,跨平台需要,跟平台不相关,所以设置了字节码中间格式,JVM根据不同的OS翻译成OS能识别的机器码。

C编译完成是ELF(linux)和EXE(windows),放在内存里是机器码,所以CPU直接读取。

计算机的组成

CPU最核心,其次是内存,主板连接各种元器件,硬盘,显卡

总线多数是64bit,系统总线,内存总线,IO总线

CPU的基本组成

PC,Registers,ALU,CU,MMU,Cache

PC,Program Counter,程序计数器,记录当前指令地址。

Registers,寄存器,64位,能够暂时存储CPU计算需要用到的数据,从内存里面拷贝一份保存,是离CPU最近的,RAX,RBX,CPU多少位指的是寄存器一次性能够读取64位的数据。

ALU,Arithmetic & Logic Unit 逻辑运算单元,例如2+3,先将2和3从内存取出保存在寄存器的RAX,RBX,然后读取PC的指令add,然后把2,3交给ALU进行运算,最后将结果放入寄存器RCX,最后把RCX运算结果发送给内存。

CU,Control Unit控制单元

MMU,Memory Management Unit 内存管理单元,最早的MMU是操作系统软件实现,现在是硬件加操作系统实现

Cache,缓存,分L1,L2,L3缓存,一个CPU的每个核都有L1,L2缓存,一个CPU(每个核)共享一个L3缓存

超线程

一个ALU对应多个程序计数器和寄存器,即所谓的四核八线程,一个ALU对应N组寄存器。
一个线程Thread数据存储在寄存器,指令存储在程序计数器,另一个线程来了,需要吧寄存器和程序计数器先找个地方保存,然后切换到第二个线程,切换需要消耗CPU资源。
如果一个ALU对应2组寄存器,就可以非常方便的切换,例如一个房间两个老婆,非常不方便,如果两件房间呢?

数据读取的时间比较
寄存器 < 1ns
L1 约 1ns
L2 约 3ns
L3 约 15ns
主存 约 80ns

多核CPU

一个CPU有N个核,1:N
每个核有自己的L1,L2,N个核即一个CPU共享L3,多个CPU共享主存,每个核跑一个线程

缓存

缓存一致性:是一种广泛使用的支持写回策略的缓存执行协议,MESI(协议中的状态),一致性协议也叫缓存锁
Modified:修改过
Exclusive:独享
Shared:共享
Invalid:失效过期

状态转换:
MESI --> I
I --> MESI
ME --> I
S --> SI

锁总线:CPU需要访问内存的时候先把总线锁了,别的CPU无法访问,是缓存锁实现之一,有些无法被缓存的数据或者跨越多个缓存行的数据依然必须使用总线锁。

按块读取:L1L2L3主存硬盘读数据都是一块一块的读,根据程序局部性原理:读到一个数据的时候应该很快会用到他相邻的数据,可以提高效率,CPU针脚等一次性读取更多数据的能力。

缓存行:
缓存行越大,局部性空间效率越高,但读取时间慢
缓存行越小,局部性空间效率越低,但读取时间快
取一个折中值,目前多用:64字节缓存行对齐:对于有些特别敏感的数字,会存在线程高竞争的访问,为了保证不发生伪共享,可以使用缓存行对齐的编程方式。
JDK7中,采用long padding提高效率
JDK8,加入了@Contended注解 需要加上:JVM -XX:-RestrictContended

public class CacheLinePadding {
    //数组内2个元素保存在一个缓存行内
    public static volatile long[] arr = new long[2];
    public static void main(String[] args) throws Exception {
        Thread t1 = new Thread(()->{
            for (long i = 0; i < 10000_0000L; i++) {
                arr[0] = i;
            }
        });
        Thread t2 = new Thread(()->{
            for (long i = 0; i < 10000_0000L; i++) {
                arr[1] = i;
            }
        });
        final long start = System.nanoTime();
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println((System.nanoTime() - start)/100_0000);
    }
}

public class CacheLinePadding {
    //0-7,8-15分别保存在2个缓存行内
    public static volatile long[] arr = new long[16];
    public static void main(String[] args) throws Exception {
        Thread t1 = new Thread(()->{
            for (long i = 0; i < 10000_0000L; i++) {
                arr[0] = i;
            }
        });
        Thread t2 = new Thread(()->{
            for (long i = 0; i < 10000_0000L; i++) {
                arr[8] = i;
            }
        });
        final long start = System.nanoTime();
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println((System.nanoTime() - start)/100_0000);
    }
}

实验结果是第二个类的效率会高很多

乱序执行

需要知道CPU在进行读等待的同事执行指令,CPU乱序的根源不是乱,而是提高效率!

 

 1 public class Disorder {
 2     private static int x = 0, y = 0;
 3     private static int a = 0, b =0;
 4 
 5     public static void main(String[] args) throws InterruptedException {
 6         int i = 0;
 7         for(;;) {
 8             i++;
 9             x = 0; y = 0;
10             a = 0; b = 0;
11             Thread one = new Thread(new Runnable() {
12                 public void run() {
13                     //由于线程one先启动,下面这句话让它等一等线程two. 读着可根据自己电脑的实际性能适当调整等待时间.
14                     //shortWait(100000);
15                     a = 1;
16                     x = b;
17                 }
18             });
19 
20             Thread other = new Thread(new Runnable() {
21                 public void run() {
22                     b = 1;
23                     y = a;
24                 }
25             });
26             one.start();other.start();
27             one.join();other.join();
28             String result = "第" + i + "次 (" + x + "," + y + ")";
29             if(x == 0 && y == 0) {
30                 System.err.println(result);
31                 break;
32             } else {
33                 //System.out.println(result);
34             }
35         }
36     }
37 
38 
39     public static void shortWait(long interval){
40         long start = System.nanoTime();
41         long end;
42         do{
43             end = System.nanoTime();
44         }while(start + interval >= end);
45     }
46 }

 

测试结果显示,会出现x=0和y=0的情况

乱序执行存在的问题

class T{
    int m = 8;
}
T t = new T();

DCL单例为什么要加volatile?答:要,第一个线程运行的时候,需要检查t是不是为空,new到一半程序为了不等待,发生指令重排,实例化一个区域设置了默认值,还没有调用构造方法进行初始化,先建立起关联了,t此时不为空了,刚好第二个线程来了检查到t不为空,直接使用了半初始化的对象。

因为new 一个对象的时候,对象有一个中间的状态,首先实例化一个区域并且这只默认值,基础变量是默认值例如int是0,引用变量是null,然后再执行构造方法初始化赋值,然后再关联。

汇编码:

0 new #2 <T> //在内存申请一块空间,new完之后在方法的栈帧里面存一个引用指向new出来的对象(没有指向t引用),基础变量的默认值是0,引用变量是null
3 dup //dup就是在栈帧里面复制一个也是指向new 出来的对象,即复制上一步new出来的引用
4 invokespecial #3 <T.<init>> //需要弹出消耗一个引用,就是消耗dup复制出来的对象,并且调用对象执行构造方法,此时内存区域的m=8;
7 astore_1 //栈帧里剩下那个被复制的对象跟t建立关联,此时t指向初始化过的对象
8 return
//dup:duplicate,dup作用是复制一份

CPU层面如何禁止指令重排序?
答:内存屏障,对某部分内存做操作时前后添加的屏障使前后的操作不可以乱序执行

CPU层面底层实现:intel:原语lfence,mfence,sfence,读、读写、写屏障,也可以使用总线锁来解决:实现原子性
volatile底层实现:lock指令,锁屏障,lock addl,在寄存器上加了一个0,其实什么都没加,最终需要执行lock指令,lock指令执行:该指令之前的程序必须全部执行,addl 0x0(esp)
JVM层级:8个hanppens-before原则 4个内存屏障 (LL LS SL SS)
volatile:int->对应的内存空间读写前后加屏障,对象,t->new 出来的对象,实验,是new 出来的T的整个区域读写前后加屏障

有序性保障

1.CPU层面:内存屏障lfence,mfence,sfence,硬件层次这种性能好
2.intel lock汇编指令:hotspot使用
   1.硬件实现:原子指令lock... 执行的时候锁住内存子系统来确保执行顺序,甚至跨多个CPU,全屏障:读写操作都不能指令重排徐
   2.软件实现:通常使用内存屏障或原子指令来实现变量可见性和保持程序顺序
3.JVM规范:8个hanppens-before原则 4个内存屏障 (LL LS SL SS),读写指令之间不可互换
介绍:内存屏障,其实是在写操作或者读操作之前后都加了屏障,JVM要求,具体是用ock指令实现

CPU规则:前后指令没有依赖关系可以发生
JVM规则:hanppens-before原则(JVM规定指令重排序遵守的规则)

问题:有什么办法能让请求一个一个按顺序执行?
答:队列,ThreadPool的SingleThreadPool单线程的队列,队列有长度,不会溢出,队列满了有拒绝策略,无界队列会溢出
一个任务分很多很多子任务用join()没有顺序

Synchronized和ReentrantLock区别:

1.Synchronized是JVM层次的锁实现,ReentrantLock是JDK层次的锁实现;
2.Synchronized的锁状态是无法在代码中直接判断的,但是ReentrantLock可以通过ReentrantLock#isLocked判断;
3.synchronized是非公平锁,ReentrantLock是可以是公平也可以是非公平的;
4.Synchronized是不可以被中断的,而 ReentrantLock#lockInterruptibly方法是可以被中断的;
5.在发生异常时Synchronized会自动释放锁(由javac编译时自动实现),而ReentrantLock需要开发者在finally块中显示释放锁;
6.ReentrantLock获取锁的形式有多种:如立即返回是否成功的tryLock().以及等待指定时长的获取更加寻活:

合并写

简介:由于ALU速度太快,所以在写入L1的同时,写入一个WC Buffer,满了之后,再直接更新到L2,Write Combining,为了提高效率
CPU 提高写效率:CPU在写入L1时,同时写入L2,buffer叫WC buffer
ALU和主存之间有3个cache,L1L2L3,但是ALU和L1之间还有一层缓存叫Load Buffer,Store Buffer,空间特别小,不是所有CPU都有,还有一个WC buffer 4个字节,load buffer一般往L1写,L1总会写入L2,所以同时把WC buffer 4个自己写满,然后一次性的更新到L2,所以WC buffer箭头是指向L2,写满4个字节才会更新到L2

NUMA

简介:分配内存会优先分配该线程所在CPU的最近内存
UMA:多个CPU共享一块主内存,CPU资源很多用作争抢内存地址
NUMA:非统一访问内存:主板分不同的插槽,一组CPU和一组离CPU最近的内存相同插槽,访问速度比访问其他组CPU的内存速度快得多,即对自己相同插槽上的内存有优先级,也可以通过总线访问其他组内存效率稍微低。
ZGC用到NUMA,因为能感知到NUMA结构,系统会分配内存到离CPU近的位置

 

 

 

posted @ 2021-03-14 02:44  康迪小哥哥  阅读(225)  评论(0)    收藏  举报