计算机基础知识

一、CPU是什么

CPU,(Central Processing Unit)也叫做中央处理器,是一台计算机的运算核心(Core)和控制核心( Control Unit)。是计算机内的电子电路,通过执行由指令指定的基本算术、逻辑、控制和输入/输出(I/O)操作来执行计算机程序的指令。计算机工业至少从20世纪60年代初就使用了术语“中央处理单元”。主存储器和I/O电路。

CPU包括运算逻辑部件、寄存器部件和控制部件等,英文Logic components;运算逻辑部件,可以执行定点或浮点算术运算操作、移位操作以及逻辑操作,也可执行地址运算和转换。

二、CPU的制作

简易流程: 一堆沙子+一堆铜+一堆胶水+特定金属添加+特殊工艺

详细流程: 沙子脱氧->石英-→二氧化硅-→>提纯->硅锭->切割->晶圆->涂抹光刻胶-→>光刻→>蚀刻->清除光刻胶->电镀->抛光->铜层->测试-切片->封装

CPU的制作; CPU是如何制造的?动画演示:一堆沙子到一颗CPU的加工过程,涨姿势了!,科技,数码,好看视频

CPU的核心: 谈科技丨CPU核心到底是什么?,科学,科普,好看视频

三、晶体管, 晶圆, 芯片

3.1、什么是晶体管

晶体管(transistor)是一种固体半导体器件(包括二极管三极管场效应管晶闸管等,有时特指双极型器件),具有检波、整流、放大、开关、稳压、信号调制等多种功能。晶体管作为一种可变电流开关,能够基于输入电压控制输出电流。与普通机械开关(如Relay、switch)不同,晶体管利用电信号来控制自身的开合,所以开关速度可以非常快,实验室中的切换速度可达100GHz以上。

3.2、二极管的制成

二极管是由半导体材料(硅、硒、锗等)制成的。

二极管的主要原理就是利用PN结的单向导电性,在PN结上加上引线和封装就成了一个二极管。

硅->加入特殊元素->P半导体N半导体->PN结-→>二极管->场效应晶体管->逻辑开关

3.3、相关概念和内容

与门或门非门或非门(异或)->基础逻辑电路

加法器累加器锁存器…

实现手动计算(通电一次,运行一次位运算)

加入内存实现自动运算(每次读取内存指令,(高电低电))

晶体管的工作流程: 超形象真人演示,五分钟讲解晶体管工作原理,一看就懂_哔哩哔哩_bilibili

3.4、什么是晶圆

晶圆是指制作硅半导体电路所用的硅晶片,其原始材料是。高纯度的多晶硅溶解后掺入硅晶体晶种,然后慢慢拉出,形成圆柱形的单晶硅。硅晶棒在经过研磨,抛光,切片后,形成硅晶圆片,也就是晶圆。国内晶圆生产线以 8英寸和 12 英寸为主。

3.5、什么是芯片

集成电路英语:integrated circuit,缩写作 IC;或称微电路(microcircuit)、微芯片(microchip)、晶片/芯片(chip)在电子学中是一种将电路(主要包括半导体设备,也包括被动组件等)小型化的方式,并时常制造在半导体晶圆表面上。

芯片上集成了许多二极管, 而芯片的制作是在晶圆上完成的.

芯片的制作过程: 三分钟讲解:芯片制造过程,,,好看视频

四、汇编语言

底层的硬件只能分辨高电平, 低电平就是我们传统的(1,0二进制), 但是0,1二进制形成的机器语言及其难以记忆和编写. 由此汇编语言诞生了.

4.1、什么是汇编语言

汇编语言(Assembly Language)是任何一种用于电子计算机微处理器微控制器或其他可编程器件的低级语言,亦称为符号语言。在汇编语言中,用助记符代替机器指令操作码,用地址符号或标号代替指令或操作数的地址。在不同的设备中,汇编语言对应着不同的机器语言指令集,通过汇编过程转换成机器指令。特定的汇编语言和特定的机器语言指令集是一一对应的,不同平台之间不可直接移植

汇编就是二进制指令的文本形式

举个例子: 加法在计算机中是通过00000011来表示的, 在汇编中就是通过ADD来表示.

通过汇编语言这种形式, 使其编写程序成为可能. 但其实汇编语言的编写也是一件非常困难和繁琐的事情, 由此就出现了后面的高级语言(c, c++, java, python等等). 但是高级语言是不可以直接被CPU所看懂的, 所以执行过程就变成了(高级语言--->(编译器/解释器)--->机器语言(就是二进制的0,1形式))

五、计算机的组成

PC: CPU中的程序计数器, 它用于存放下一条要执行的指令的内存地址。

Registers: CPU中的寄存器, 暂时存放参与运算的数据和运算结果,具有接收数据、存放数据和输出数据的功能. CPU大致有六种寄存器(①指令寄存器(IR);②程序计数器(PC);③数据地址寄存器(AR);④缓冲寄存器(DR);⑤通用寄存器(R0~R3);⑥状态字寄存器(PSW)), 汇编语言的编写者需要详细了解每个寄存器的作用.

ALU它代表算术逻辑单元,执行算术和逻辑运算。负责程序的计算.

cache: CPU缓存是位于CPU与内存之间的临时存储器,它的容量比内存小的多但是交换速度却比内存要快得多。高速缓存的出现主要是为了解决CPU运算速度与内存读写速度不匹配的矛盾,高速缓存分为(L1,L2,L3)

存储器的层次结构

寄存器的运行速度大概是内存的100倍. 

L1, L2, L3大致图解

由图可知, 每一个CPU的核都有L1, L2级缓存, 多个核共享L3级缓存. 一个CPU有一个L3级缓存

程序运行大致流程, 首先PC根据存储的下一条要执行的内存地址去高速缓存中查找, 先去L1中查找, 没有就去L2, 一直找到L3, 如果在高速缓存中找到了相对应的数据, 则将其数据放到Registers中, 如果高速缓存中都没有找到就去内存中查找, 找到了之后呢, 将其数据放到Registers中, 然后ALU进行运算, Registers得出结果, 最后将结果返回给内存.

六、超线程的概念

在多线程的状态下, 每个线程都需要夺取CPU发布的时间片, 谁得到了就执行谁. 如果一个CPU是单核的, 且只有一组PC, Registers.假设PC, Registers先执行线程1, 过了一会, 时间片被线程2抢走了(线程切换), 但此时线程1还未执行完, 则会将原来(线程1)PC, Registers中存储的地址, 数据存储到一个地方(可能是高速缓存也可能是存放到内存当中), 然后PC, Registers就开始运行线程2, 后面如果线程1再次夺取了时间片则将之前线程1的地址和数据进行恢复, 继续执行. 这个行为叫恢复现场. 这种来回切换线程, 数据来回恢复的过程其实是很耗费时间和效率的. 由此就提出了超线程的概念.

超线程简单来讲就是一个核中有多组PC, Registers, 如果是多线程运行的情况下, 只需要ALU来回切换执行Registers, 而不需要来回将PC, Registers中的数据存储起来, 又进行恢复, 提高了运行效率, 和减少了不必要的操作. (简单来讲就是一个运算单元对应多组线程对应的资源单元(PC, Registers))

超线程: 一个ALU对应多个PC | Registers  (所谓的四核八线程)

七、cache line概念 应用

如图 

当寄存器想拿取数据x, 它首先会从L1缓存中查找, 如果L1缓存没有在去L2缓存中查找, L2没有则去L3缓存中查找, 如果L3也没有则去内存中取, 取到之后还会将数据缓存起来(它取的数据不是一个一个的, 而是一整行/块, 如图就是x,y整块), 先缓存到L3, 然后给到L2, 在接着给到L1, 这个时候x,y这块数据就被缓存到了L1, 下次寄存器想要获取y了, 就可以直接从缓存L1中获取, 这大大的节省了时间.

这里如果x, y都被volatile修饰, 且x, y数据被两个cpu内核使用, 那么它到底是如何保证数据的一致性?(缓存行对齐)

当内核1, 内核2中都开始使用x,y数据, 且这组数据都被缓存到了各自内核的L1, 如果内核1使用x, 并对x数据进行了修改, 内核2使用的是y, 而因为x, y都被volatile修饰了, 所以就必须保证可见性(就是内核1对x进行了修改, 那么内核2必须立马知道, 因为内核2L1中缓存的缓存行也包括x, 内核1对缓存行进行了修改, 需要把修改后的数据更新到内存当中. 内核2的L1缓存行必须进行更新, 重新去内存中获取这个缓存行), 为了保持缓存行的一致性, 它必须使用一种方式保证.在intel的CPU中它就是使用的MESI来保证的.

了解MESI

MESI是一种缓存一致性协议,cpu每个cache line标记四种状态(额外两位)(modified被修改的, exclusive独有的的, shared共享的, invalid无效的)

拿上面那个例子举例, 如果内核1对x进行了修改, 则这个x, y缓存行就是modified状态(把最新的缓存行数据更新到内存当中), 通过内核之间的总线通知内核2, 而内核2的x,y缓存行就成了invalid状态, 然后内核2就重新去内存中更新缓存行.

那么内核之间是如何通知的呢?

是在硬件上通过缓存锁实现的, 但是有些无法被缓存的数据或者跨越多个缓存行的数据, 就会用到总线锁.(如果CPU是单核的, 就压根不需要考虑这些问题)

拓展

还有那些方式保证缓存一致性协议

MSI MESI MOSI Synapse Firefly Dragon, 不同的CPU选择不一样.

这里再提出一个问题, 如果两个volatile修饰的变量靠的非常近的话, 且都在一个缓存行中, 则程序的运行是会变得很慢的, 为什么呢?

当内核1对缓存行的x进行了修改, 则内核2必须得保持同步, 而内核2又需要对缓存行的y进行修改, 那么内核1也必须保持同步, 这么不断地一来一回就会浪费资源, 减低效率.

这里其实可以用代码来验证这种情况, 代码如下

首先需要知道intel的CPU中缓存行一般大小为64个字节(byte)

代码举例1: 

package com.softeem.wolf.cpu;

/**
 * Created by 苍狼
 * Time on 2022-10-30
 */
public class To1_CacheLinePadding {
    private static class T {
        public volatile long x = 0L;
    }

    public static T[] arr = new T[2];

    static{
        arr[0] = new T();
        arr[1] = new T();
    }

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            for (long i = 0; i < 1000_0000L; i++) {
                arr[0].x = i;
            }
        });
        Thread t2 = new Thread(() -> {
            for (long i = 0; i < 1000_0000L; i++) {
                arr[1].x = i;
            }
        });
        final long start = System.nanoTime();
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println((System.nanoTime() - start) / 100_0000);
    }
}

运行结果

多次测试, 结果大致在200多毫秒. 

这个代码怎么理解呢?

首先这两个线程都对x进行修改, 我这里为了好区分就把线程1操作的x定义为x1, 线程2操作的x定义为x2. 因为intel的cpu缓存行的大小大致为64个字节, 而x1是long类型, 字节大小为8, 那么一个CPU缓存行是可以放下x1, x2两个变量的. 所以x1, x2就是在一个缓存行中, 这也就是上面我为什么把x1, x2画在一起的原因.

代码举例2:

package com.softeem.wolf.cpu;

/**
 * Created by 苍狼
 * Time on 2022-10-30
 */
public class To2_CacheLinePadding {
    private static class Padding {
        public volatile long p1, p2, p3, p4, p5, p6, p7;
    }
    private static class T extends Padding {
        public volatile long x = 0L;
    }
    public static T[] arr = new T[2];

    static
    {
        arr[0] = new T();
        arr[1] = new T();
    }

    public static void main(String[] args) throws Exception {
        Thread t1 = new Thread(() -> {
            for (long i = 0; i < 1000_0000L; i++) {
                arr[0].x = i;
            }
        });

        Thread t2 = new Thread(() -> {
            for (long i = 0; i < 1000_0000L; i++) {
                arr[1].x = i;
            }
        });

        final long start = System.nanoTime();
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println((System.nanoTime() - start)/ 100_0000);
    }
}

运行结果

多次测试, 大致结果稳定在80左右

这个代码怎么理解呢?

首先p1-p7为long类型共占据56个字节, 再加上x1刚好64个字节, 大致就是一个intelCPU缓存行的数据, 所以两个线程分别操作的缓存行不一样, 所以不需要来回反复的保持缓存一致性. 从而提高了代码的运行效率, 减少了不必要的资源消耗.

八、CPU的指令重排序

CPU是支持指令重排序, 目的是提高CPU的运行效率(有的时候不需要因为等待某一条指令的执行(可能这条指令的io时间较长等待), 而让CPU一直处于等待状态)

读指令的同时可以同时执行不影响的其他指令而写的同时可以进行合并写

代码举例

package com.softeem.wolf.cpu;

/**
 * Created by 苍狼
 * Time on 2022-10-30
 */
public class T04_Disorder {
    private static int x = 0, y = 0;
    private static int a = 0, b = 0;

    public static void main(String[] args) throws InterruptedException {
        int i = 0;
        for (; ; ) {
            i++;
            x = 0;
            y = 0;
            a = 0;
            b = 0;
            Thread one = new Thread(new Runnable() {
                public void run() {
                    //由于线程one先启动,下面这句话让它等一等线程two.读着可根据自己l / shortwait(100000 ) ;
                    a = 1;
                    x = b;
                }
            });

            Thread other = new Thread(new Runnable() {
                public void run() {
                    b = 1;
                    y = a;
                }
            });
            one.start();
            other.start();
            one.join();
            other.join();
            String result = "第" + i + "次(" + x + "," + y + ") ";
            if(x==0 && y==0) {
                System.err.println(result);
                break;
            }else{
//                System.out.println( result);
            }
        }
    }
}

如果CPU不具备乱序执行, 则永远不可能出现if(x==0 && y==0), 程序永远会执行下去.

运行结果

由此可以证明CPU是具备乱序执行的能力的. 但是乱序执行也会导致一些问题, 可能最终运行结果跟预期不一样, 特别是多颗CPU下运行. 在java层面解决这个问题的关键字就是volatile(禁止指令重排). 

九、DCL单例要不要加volatile

首先答案是肯定要的

看DCL单例代码

package com.company.singleton;
 
/**
 * lazy loading
 * 也称懒汉式
 * 虽然达到了按需初始化的目的,但却带来线程不安全的问题
 * 可以通过synchronized解决,但也带来效率下降
 */
public class Mgr06 {
    private static volatile Mgr06 INSTANCE; //JIT
 
    private Mgr06() {
    }
 
    public static Mgr06 getInstance() {
        if (INSTANCE == null) {
            //双重检查
            synchronized (Mgr06.class) {
                if(INSTANCE == null) {
                    try {
                        Thread.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    INSTANCE = new Mgr06();
                }
            }
        }
        return INSTANCE;
    }
 
    public void m() {
        System.out.println("m");
    }
 
    public static void main(String[] args) {
        for(int i=0; i<100; i++) {
            new Thread(()->{
                System.out.println(Mgr06.getInstance().hashCode());
            }).start();
        }
    }
}

那么为什么需要加volatile呢? 看着不加volatile也没有什么问题啊?

这里先分析一段简单的代码

package com.softeem.wolf.volatileTest;

/**
 * Created by 苍狼
 * Time on 2022-10-30
 */
public class Test01 {
    int num = 10;

    public void show(){
        System.out.println("hello world");
    }

    public static void main(String[] args) {
        Test01 test01 = new Test01();
    }
}

 利用jclasslib这个插件查看main方法的字节码

分析:  main方法的执行过程, 首先看字节码的new代表在堆中分配一块内存, 且成员变量为默认值0, 当执行到了字节码中的invokespecial时代表给成员变量值初始化为10, 调用构造方法. astore_1代表将引用对象test01指向堆中的对象. 最后return返回该对象.

那么这个跟我们说的DCL要不要加volatile有什么关系呢?

我们看这三个主要的步骤, 我们看了上面的分析已经知道了这三个步骤主要是干嘛的了. 之前我们已经讲过了CPU有指令重排这个特点. 如果CPU因为指令重排把2,3步骤互换了怎么办, 首先在堆中分配了一块地址, 然后成员变量设定默认值为0, 然后直接将test01引用指向堆中的对象, 就跳过了调用初始化成员变量为10, 和构造器的调用.(半初始化状态的对象)

这里回到DCL代码, 我们创建Mgr06 INSTANCE对象加上了volatile, 禁止指令重排, 这里就可以防止出现上述的问题, 它运行的原理是在jvm层面上堆分配的内存中加上内存屏障. 如果没有加上这个volatile, 则可能在并发量很大的情况下有那么几次可能会出现指令重排的情况, 如果这是在实际的业务代码中就会导致数据为空.

十、CPU中WCBuffers(合并写)

WCBuffers是WriteCombinbingBuffers的缩写

CPU的寄存器L1缓存, L2缓存之间有一个WCBuffer,有四个字节大小。数据读写先存入WCBuffer,然后再在寄存器和L1之间交换。当WCBuffers存储的内容满了(4个字节)则就会将数据写入L1缓存/(L2缓存).

正常情况下WCBuffers的速度比L1还快, L1的速度比L2快, L2的速度比L3快. 

问题是,现在有长度为6的数据,是一次性操作6个比较快,还是分成两组,一组3个,每组再凑一个字节变成4个长度 的方式快。

代码验证

package com.softeem.wolf.cpu;

/**
 * Created by 苍狼
 * Time on 2022-10-31
 */
public class WCBuffersTest {
    private static final int ITERATIONS = Integer.MAX_VALUE;
    private static final int ITEMS = 1 << 24;
    private static final int MASK = ITEMS - 1;

    private static final byte[] arrayA = new byte[ITEMS];
    private static final byte[] arrayB = new byte[ITEMS];
    private static final byte[] arrayC = new byte[ITEMS];
    private static final byte[] arrayD = new byte[ITEMS];
    private static final byte[] arrayE = new byte[ITEMS];
    private static final byte[] arrayF = new byte[ITEMS];

    public static void main(final String[] args){
        for (int i = 0; i < 5; i++) {
            System.out.println(i+" SingleLoop duraption (ns) = " +runCaseOne());
            System.out.println(i+" SingleLoop duraption (ns) = " +runCaseTwo());
        }
    }
    public static long runCaseOne(){
        long start = System.nanoTime();
        int i = ITERATIONS;
        //第一个循环,尝试一次改动6个位置
        while (--i != 0){
            int slot = i & MASK;
            byte b = (byte) i;  //b也占用一个位置
            arrayA[slot] = b;
            arrayB[slot] = b;
            arrayC[slot] = b;
            arrayD[slot] = b;
            arrayE[slot] = b;
            arrayF[slot] = b;
        }
        return System.nanoTime() - start;
    }
    public static long runCaseTwo(){
        long start = System.nanoTime();
        int i = ITERATIONS;
        //分成两个循环,一个循环负责改其中的3个位置
        while (--i != 0){
            int slot = i & MASK;
            byte b = (byte) i;  //b也占用一个位置,与这三个位置,共同构成4个位置
            arrayA[slot] = b;
            arrayB[slot] = b;
            arrayC[slot] = b;
        }
        while (--i != 0){
            int slot = i & MASK;
            byte b = (byte) i;
            arrayA[slot] = b;
            arrayB[slot] = b;
            arrayC[slot] = b;
        }
        return System.nanoTime() - start;
    }
}

运行结果

由此可以证明CPU中合并写的存在, 且对于CPU速度的优化是有一定作用的.

十一、计算机中bug的起源

Bug一词的原意是“昆虫”或“虫子”;而在电脑系统或程序中隐藏着的一些未被发现的缺陷或问题,人们也叫它“bug”。

这其实是有一个故事的

Bug的创始人格蕾丝·赫柏(Grace Murray Hopper),是一位为美国海军工作的电脑专家,也是最早将人类语言融入到电脑程序的人之一。而代表电脑程序出错的“bug” 这名字,正是由赫柏所取的。

1945年9月9日,下午三点。哈珀中尉正领着她的小组构造一个称为“马克二型”的计算机。这还不是一个完全的电子计算机,它使用了大量的继电器,一种电子机械装置。第二次世界大战还没有结束。哈珀的小组日以继夜地工作。机房是一间第一次世界大战时建造的老建筑。那是一个炎热的夏天,房间没有空调,所有窗户都敞开散热。

突然,马克二型死机了。技术人员试了很多办法,最后定位到第70号继电器出错。哈珀观察这个出错的继电器,发现一只飞蛾躺在中间,已经被继电器打死。她小心地用摄子将蛾子夹出来,用透明胶布帖到“事件记录本”中,并注明“第一个发现虫子的实例。”

从此以后,人们将计算机错误戏称为虫子(bug),而把找寻错误的工作称为(debug)

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值