java程序员应该熟悉的底层知识

不求甚解,观其大略!

汇编语言(机器语言)的执行过程

汇编语言的本质:机器语言的助记符号,就是机器语言

​ 比如 move --> 10001000 在汇编语言中的move在执行的时候就会在对应的表中找到对应的记录并变成10001000 的value值

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

量子计算机

量子比特,同时表示1,0

其实我不太理解为什么同时表示1和0就能那么夸张的比只表示1或者0快

不是代表一个固定态,而是代表了所有的可能态。

传统的计算机都是使用二进制的,一个比特就是0或者1,而量子比特也是使用二进制,但它特别就在一个量子比特可以同时是0或者1,这就叫量子叠加。所以,两个量子比特就可以同时表示00、01、10、11这四个值。

具体就不再深入了,不是特别能理解。但是到此为止。

CPU的组成

PC -> Program Counter 程序计数器(记录当前指令地址)

Registers -> 暂存器,暂时存储CPU计算要用到的数据

ALU -> 逻辑运算单元 Arithmetic & Logic Unit

CU -> 控制单元 Control Unit

MMU -> 内存管理单元 Memory Management Unit

缓存

缓存一致性协议:

https://www.cnblogs.com/z00377750/p/9180644.html

MESI协议

CPU中每个缓存行都使用4中状态进行标记(额外使用2位)

M:被修改(Modified)

该缓存行只被缓存在该CPU这个呢,并且是被修改过的(dirty),即与主存中的数据不一致,该缓存行需要在未来的某一个时间点(允许其他CPU读取主存中相应内存之前)写回到主存 。当被写会到主存之后。状态会改为E(独享的)

E:独享的(Exclusive)

该缓存行只被缓存在该CPU中,他是未被修改过的(clean),与主存中的数据一致。该状态可以在任何时刻当有其他CPU读取时变成共享状态S(shard)

同样的,当CPU修改了缓存行中的内容时,该状态可以变成M(被修改)状态

S:共享的(Shared)

该状态意味着该缓存行被多个CPU缓存,并且各个缓存的数据与主存数据一致(clean),当有一个CPU修改缓存行中的值。其他的CPU中的该缓存行可以呗作废(变成无效状态I(invalid)

I:无效的(invalid)

该缓存是无效的(可能是其他的CPU修改了该缓存行)

CPU读请求:除了invalid状态,其他的状态都能满足CPU的读请求,如果是invalid,那么会在主存中读取,并且变成shared或者exclusive

CPU写请求:只能在modified或者exclusive状态下能进行,如果是shared状态,需要变成invalid(不允许不同CPU修改同一个缓存行,即使数据处于不同的地方)。改操作通常使用广播的方式来完成

缓存能将任何一个非M状态直接作废,但是如果是M状态,那么必须先写会到主存

如果有CPU读取M对应的主存(M的缓存行负责监听),那么会先将M写会到主存在执行其他CPU读取M对应的主存的操作

处于S的缓存行会监听该缓存行变成invalid的操作或变成E的操作,并且会把自己的缓存行变成invalid

处于E的缓存行会监听主存中对应的位置,如果有其他的CPU读取会将状态变成shared

缓存行

缓存行越大,局部性空间效率越高,但是读取时间越慢

缓存行越小,局部性空间效率越低,但是读取时间越快

Intel CPU选取了一个折中值 64字节

package com.example.demo;

/**
 * 测试缓存行 64字节 位于同一个缓存行
 *
 * @Author: xiaobin
 * @Date: 2020/5/26 12:58
 */
public class T03_CacheLinePadding {

    public static volatile long[] arr = new long[2];

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 10_0000_0000L; i++) {
                arr[0] = i;
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 10_0000_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);
    }

}
package com.example.demo;

/**
 * 位于不同缓存行
 *
 * @Author: xiaobin
 * @Date: 2020/5/26 13:02
 */
public class T04_CacheLinePadding {
    public static volatile long[] arr = new long[16];

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 10_0000_0000L; i++) {
                arr[0] = i;
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 10_0000_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);
    }
}

但是我在以上跑,前者时间仅仅是后者时间的1/3。比较奇怪

缓存行对齐

对于有些特别敏感的数字,会存在线程高竞争的访问,为了保证不发生伪共享,可以使用缓存行对齐的编程方式

在jdk1.7中,很多采用long padding提高效率。就是前面加8个long的声明,后面加8个long的声明

jdk1.8加入了@Contended注解。需要加上:JVM -XX:-RestrictContended

乱序执行

计算机为了执行效率,可能会发生指令重拍,没有依赖关系的命令可能重新排序

禁止乱序

CPU层面:Intel -> 原语(mfence Ifence sfence)或者锁总线

JVM层次:8个hanppens-before原则 4个内存屏障(LL LS SL SS)

as-if-serial:不管硬件什么顺序,单线程执行的结果不变,看上去就像serial

合并写

Write Combining Buffer

一般是4个字节

由于ALU的速度太快,所以在写入L1的同时,写入一个WC buffer,满了之后,再直接更新到L2

NUMA

Non Uniform Memory Access

ZGC -NUMA aware

分配内存会优先分配给靠近CPU的内存

OS

启动过程

通电 -> bios uefi工作 -> 自检 -> 到硬盘的固定位置加载bootloader(一般是磁盘的第一个扇区)-> 读取可配置信息 -> CMOS

内核分类

微内核 -弹性部署 5G 物联网(The Internet of Things,简称lot)

宏内核 -pc phone

外核 -科研 实验中 为应用定制操作系统(不懂)

用户态和内核态(重点)

CPU分不同的指令级别

Linux内核跑在ring 0级,用户程序跑在ring 3级,对于系统的关键访问,需要经过kernel的同意,保证系统的健壮性

内核执行操作 --> 200多个系统调用 read write fork…

JVM --> 站在OS的角度看,就是一个普通的进程

进程 线程 纤程 和中断

面试高频:进程和线程有什么区别

答案:进程就是一个程序运行起来的状态,线程就是一个进程中的不同执行路径

专业回答:进程就是OS分配资源的基本单位,线程就是执行调度的基本单位。分配资源最重要的就是:独立的内存空间,线程调度执行(线程共享进程的内存空间,没有自己独立的内存空间)

纤程:用户态的线程,线程中的线程,切换和调度不需要经过OS

优势:

  1. 占用资源少 OS:线程 1M 纤程:4k
  2. 切换比较简单
  3. 可以启动非常多10W+

目前支持内置纤程的语言:go kotlin scala python(lib)

Java中对于纤程的支持:没有内置,盼望内置

利用Quaser库(不成熟)

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>mashibing.com</groupId>
    <artifactId>HelloFiber</artifactId>
    <version>1.0-SNAPSHOT</version>

    <dependencies>
        <!-- https://mvnrepository.com/artifact/co.paralleluniverse/quasar-core -->
        <dependency>
            <groupId>co.paralleluniverse</groupId>
            <artifactId>quasar-core</artifactId>
            <version>0.8.0</version>
        </dependency>
    </dependencies>

</project>
package com.example.demo;

/**
 * @Author: xiaobin
 * @Date: 2020/5/26 19:18
 */
public class HelloFiber {
    public static void main(String[] args) throws InterruptedException {
        long start = System.currentTimeMillis();
        Runnable r = new Runnable() {
            @Override
            public void run() {
                calc();
            }
        };
        int size = 20000;
        Thread[] threads = new Thread[size];
        for (int i = 0; i < threads.length; i++) {
            threads[i] = new Thread(r);
        }

        for (int i = 0; i < threads.length; i++) {
            threads[i].start();
        }
        for (int i = 0; i < threads.length; i++) {
            threads[i].join();
        }
        long end = System.currentTimeMillis();
        System.out.println(end - start);
    }

    private static void calc() {
        int result = 0;
        for (int m = 0; m < 10000; m++) {
            for (int i = 0; i < 200; i++) {
                result += i;
            }
        }
    }
}

package com.example.demo;

import com.sun.xml.internal.ws.api.pipe.Fiber;

/**
 * @Author: xiaobin
 * @Date: 2020/5/26 19:23
 */
public class HelloFiber2 {
    public static void main(String[] args) throws  Exception {
        long start = System.currentTimeMillis();


        int size = 10000;

        Fiber<Void>[] fibers = new Fiber[size];

        for (int i = 0; i < fibers.length; i++) {
            fibers[i] = new Fiber<Void>(new SuspendableRunnable() {
                public void run() throws SuspendExecution, InterruptedException {
                    calc();
                }
            });
        }

        for (int i = 0; i < fibers.length; i++) {
            fibers[i].start();
        }

        for (int i = 0; i < fibers.length; i++) {
            fibers[i].join();
        }

        long end = System.currentTimeMillis();
        System.out.println(end - start);


    }

    static void calc() {
        int result = 0;
        for (int m = 0; m < 10000; m++) {
            for (int i = 0; i < 200; i++) result += i;

        }
    }
}

目前是10000个fiber对应一个线程,还可以通过将其分成10份对应10个线程来提高效率

纤程的应用场景

纤程 vs 线程池 :很短的计算任务,不需要和内核打交道,并发量高!

僵尸进程

#include 
#include 
#include 
#include 
#include 
#include 

int main() {
        pid_t pid = fork();

        if (0 == pid) {
                printf("child id is %d\n", getpid());
                printf("parent id is %d\n", getppid());
        } else {
                while(1) {}
        }
}

子进程已经死了,并且已经回收了子进程的系统资源,但是父进程还只有子进程的引用

孤儿进程

#include 
#include 
#include 
#include 
#include 
#include 

int main() {
        pid_t pid = fork();

        if (0 == pid) {
                printf("child ppid is %d\n", getppid());
                sleep(10);
                printf("parent ppid is %d\n", getppid());
        } else {
                printf("parent id is %d\n", getpid());
                sleep(5);
                exit(0);
        }
}

父进程死了,但是子进程还在,一般会挂到进程id为1的进程下,或者图形界面就会挂到图形界面的进程下

进程调度

2.6采用CFS调度策略: Completely Fair Scheduler

按照优先级分配时间片的比例,记录每个进程的执行时间,如果有一个进程执行时间不到他应该分配的比例,优先执行

默认调度策略:

实时优先级分高低: FIFO (优先级高的先执行)优先级一样 RR(Round Robin)

普通进程:CFS

中断

硬件和操作系统内核打交道的一种方式

软终端(80中断) == 系统调用

系统调用: int 0x80 或者 sysenter原语

通过ax寄存器填入调用号

参数通过 bx cx dx si di传入内核

返回值通过ax返回

Java 读网络 -> Jvm read() --> c read() --> 内核空间 --> System_call()(系统调用处理程序) --> sys_read()

从汇编的角度理解软中断

yum install nasm

;hello.asm
;write(int fd, const void *buffer, size_t nbytes)
;fd 文件描述符 file descriptor - linux下一切皆文件

section data
    msg db "Hello", 0xA
    len equ $ - msg

section .text
global _start
_start:

    mov edx, len
    mov ecx, msg
    mov ebx, 1 ;文件描述符1 std_out
    mov eax, 4 ;write函数系统调用号 4
    int 0x80

    mov ebx, 0
    mov eax, 1 ;exit函数系统调用号
    int 0x80

编译:nasm -f elf hello.asm -o hello.o

链接:ld -m elf_i386 -o hello hello.o

一个程序的执行,要么处于用户态,要么处于内核态

内存管理

内存管理的发展历程

DOS时代: 同一时间只能有一个进程在运行(也有一些特殊的算法可以支持多进程)

windows 9x 多个进程装入内存,但是存在 内存不够 相互打扰

为了解决这两个问题,诞生了现再的内存管理系统:虚拟地址 分页装入 软硬件结合寻址

1.分页(解决内存不够用问题)内存中分成固定大小的页框(4K),把程序(硬盘)分成4K大小的块,用到哪一块就将哪一块加载到内存,如果加载的过程中内存满了,就将最不常用的那一块放到swap分区,把新的一块加载进来,这个就是著名的LRU算法

  1. LRU算法 LeetCode 146题,头条要求手撕,阿里去年也要求手撕
  2. Least Recently Used 最不常用
  3. 哈希表(保证查找操作 O(1) + 链表(保证排序操作和新增操作O(1)))
  4. 双向链表(保证左边指针指向右边块)

2.虚拟内存(解决相互打扰的问题)

1. DOS Win31 ...相互干掉,甚至干掉操作系统
2. 为了保证不相互影响,让进程工作在虚拟内存,程序使用的是虚拟内存,而不是直接的物理地址,这样A进程就永远不知道B进程的空间
3. 虚拟内存有多大? 寻址空间 = 2^64 ,比物理内存大很多 单位是byte
4. 站在虚拟的角度,进程是独享整个内存空间的
5. 内存映射: 偏移量+段的基地址 = 线性地址(虚拟地址)
6. 线性地址通过OS +MMU 内存管理单元 (硬件 Memory Management Unit)找到物理地址

3.缺页中断(不是很重要)

  1. 需要用到的页内存中没有,产生缺页中断(异常) ,有内核处理并加载

ZGC

算法叫做:Colored Pointer

GC信息记录在指正上,而不是记录在头部 immediate memory use

42位指针 寻址空间4T JDK13 -> 16T 目前为止最大16T 2^44

CPU如何区分一个立即数 和 一条指令

总线内部分为:数据总线 地址总线 控制总线

地址总线目前:48位

颜色指针本质上包含了地址映射的概念

内核同步机制

关于同步理论的一些基本概念

  • 临界区(critical area):访问或操作共享数据的代码段。简易理解synchronized大括号中的部分(原子性)
  • 竞争条件(race conditions):两个线程同时拥有临界区的执行权
  • 数据不一致(data unconsistency ): 由竞争条件引起的数据破坏
  • 同步(synchronized):避免竞争条件
  • 锁:完成同步的手段,上锁解锁必须具备原子性
  • 原子性
  • 有序性(禁止指令重拍)
  • 可见性(一个线程内的修改,另外一个线程可见)

互斥锁 排它锁 共享锁 分段锁

内核同步的常用方法

  1. 原子操作 -内核中类似于AtomicXXX,位于
  2. 自旋锁 -内核中通过汇编支持cas,位于
  3. 读-写自旋 -类似于ReadWriteLock,可同时读,只能一个写,读的时候是共享锁,写的时候是排它锁
  4. 信号量 -类似于Semaphore(PV操作 down up操作 占有和释放) 重量级锁,线程会进入wait,适合长时间持有锁的情况
  5. 读-写信号量(多个写,可以分段写,比较少用)(分段锁)
  6. 互斥体(mutex) – 特殊的信号量(二值信号量)
  7. 完成变量 – 特殊的信号量(A发出信号给B,B等待在完成变量上) vfork() 在子进程结束时通过完成变量叫醒父进程 类似于(Latch)
  8. BKL:大内核锁(早期,现在已经不用)
  9. 顺序锁(2.6): -线程可以挂起的读写自旋锁 序列计数器(从0开始,写时增加(+1),写完释放(+1),读前发现单数, 说明有写线程,等待,读前读后序列一样,说明没有写线程打断)
  10. 禁止抢占 – preempt_disable()
  11. 内存屏障 – 见volatile

你可能感兴趣的:(内核)