计算机常识
汇编语言
汇编语言的本质:机器语言的助记符,气死他就是机器语言
计算机通电—>CPU读取内存中程序(电信号输入)—》时钟发生器不断震荡通断电—》推动CPU内部一步一步执行(执行多少取决于指令需要的时钟周期)—》计算完成—》写回(电信号)—》给显卡输出(sout,或者图形)
CPU的组成
1、PC->Program Counter 程序计数器(记录当前指令地址)
2、Register—>暂时存储CPU计算需要用到的数据
3、ALU—》Arithmetic&Login Unit运算单元
4、CU->Control Unit控制单元
5、MMU—>Memory Management Unit内存管理单元
6、cache缓存
缓存
一致性协议:https://www.cnblogs.com/z00377750/p/9180644.html
缓存行:
缓存行越大,局部性空间效率越高,但读取时间慢
缓存行越小,局部性空间效率越低,但读取时间快
取一个折中值,目前多用:
64字节
package com.mashibing.juc.c_028_FalseSharing;
public class T03_CacheLinePadding {
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);
}
}
package com.mashibing.juc.c_028_FalseSharing;
public class T04_CacheLinePadding {
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);
}
}
缓存行对齐:对于有些特别敏感的数字,会存在线程高竞争的访问,为了保证不发生伪共享,可以使用缓存航对齐的编程方式
JDK7中,很多采用long padding提高效率
JDK8,加入了@Contended注解(实验)需要加上:JVM -XX:-RestrictContended
乱序执行
https://preshing.com/20120515/memory-reordering-caught-in-the-act/
jvm/jmm/Disorder.java
乱序执行,一定程度上提高了CPU的运行效率,但是也会产生一些问题,需要我们手动解决
禁止乱序(关系到JVM层面知识,需要我学习JVM知识)
CPU层面:Intel -> 原语(mfence lfence sfence) 或者锁总线
JVM层级:8个hanppens-before原则 4个内存屏障 (LL LS SL SS)
as-if-serial : 不管硬件什么顺序,单线程执行的结果不变,看上去像是serial
NUMA
每个CPU都划分了一部分属于自己的内存,当通过cpu操作的时候,优先放在最近的内存中,当内存不足时,通过与其他cpu协调调用内存
Non Uniform Memory Access
ZGC - NUMA aware
分配内存会优先分配该线程所在CPU的最近内存
操作系统
内核分类
微内核-弹性部署,5G IoT微服务 类似于通过设备来协调各种家电的执行
宏内核-目前我们一直用的,所有东西都由内核控制,如内存,文件管理等
外核 - 科研 实验中 为应用定制操作系统 (多租户 request-based GC JVM)
微内核和宏内核的区别:微内核相当于一个信息交换中心,自身可以实现的功能较少,他的主要职责是传递一个请求,一个A模块对其他模块功能的请求;而宏内核相当于一个是一个中央集权控制中心,把内存管理,文件管理等功能全部管理。
用户态与内核态
cpu分不同的指令级别
linux内核跑在ring 0级,用户程序跑在ring3,对于系统的关键访问,需要经过kernel的同意,保证系统健壮性,类似于一个靶环,里面的环可以跑到外面的环,但是外面的环不能访问里面的环
内核执行的操作—>200多个系统调用 sendfile read write pthread fork等
JVM—>站在OS老大的角度,就是个普通程序
进城 线程 线程 中断
进城和线程有什么区别
答案:进程就是一个程序运行起来的状态,线程是一个进程中的不同的执行路径。专业:进程是OS分配资源的基本单位,线程是执行调度的基本单位。分配资源最重要的是:独立的内存空间,线程调度执行,我的理解为,以一碗饭举例,进程就类似于碗和饭整个的样子,线程就类似于这碗饭的成分,如米是一个线程,水分是一个线程,土豆是一个线程等等等(线程共享进程的内存空间,没有自己独立的内存空间)
纤程:用户态的线程,线程中的线程,切换和调度不需要经过OS
优势:1:占有资源很少 OS:线程1M Fiber:4k 2:切换比较简单 3:启动很多个:10w+
目前2020 年支持内置纤程的语言:go
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>
import co.paralleluniverse.fibers.Fiber;
import co.paralleluniverse.fibers.SuspendExecution;
import co.paralleluniverse.strands.SuspendableRunnable;
public class HelloFiber {
public static void main(String[] args) throws Exception {
long start = System.currentTimeMillis();
Runnable r = new Runnable() {
@Override
public void run() {
calc();
}
};
int size = 10000;
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);
}
static void calc() {
int result = 0;
for (int m = 0; m < 10000; m++) {
for (int i = 0; i < 200; i++) result += i;
}
}
}
import co.paralleluniverse.fibers.Fiber;
import co.paralleluniverse.fibers.SuspendExecution;
import co.paralleluniverse.strands.SuspendableRunnable;
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 -> 1个JVM线程,想办法提高效率,10000Fiber -> 10份 -> 10Threads
纤程的应用场景
纤程vs纤程池:很短的计算任务,不需要和内核打交道,并发量高!
僵尸进程
当子进程结束并且释放了资源,但是父进程依旧为子进程提供这pcb的数据结构,这就是僵尸进程
僵尸进程通过kill杀不死,因为已经结束
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <assert.h>
#include <sys/types.h>
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 <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <assert.h>
#include <sys/types.h>
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);
}
}
进程调度
2.6采用CFS调度策略:Completely Fair Scheduler
按优先级分配时间片的比例,记录每个进程的执行时间,如果有一个进程执行时间不到他应该分配的比例,优先执行,不管他是否需要,都要执行完
默认调度策略:
实时 (急诊) 优先级分高低 - FIFO (First In First Out),优先级一样 - RR(Round Robin)轮询, 普通: CFS
CFS举例:当我饿了,每人都是1斤粮食,有的人不需要1斤,当它饱了的时候多余的资源就可以拿来给其他人分配,但是当这个人又饿了,他刚刚又没有吃完,那么,优先解决他的需求
中断
硬中断
当硬件如键盘进行敲击操作,敲击的中断信息会根据中断控制器进入cpu,cpu通过内存的固定位置查询到该中断信息的操作以及在这个软件上进行反馈的这些信息,打包给kernel,kernel查询中断处理程序获得要运行的函数开始运行,操作系统进行上半场的中断处理,对应的软件进行下半场的中断处理
软终端
线程调用系统内核的函数,0x80的16进制传入内核,kernel查询自己的函数,让该线程传入参数,然后kernel运行函数
硬件跟操作通内核打交道的一种机制
软中断(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时代-同一时间只能有一个进程在运行(也有一些特殊算法可以支持多进程)
windows9x-多个进程装入内存 1:内存不够用 2:互相打扰
为了解决这两个问题,诞生了现在的内存管理系统:虚拟地址 分页装入 软硬件结合寻址
1、分页(内存不够用)
数据库,内存的读取基本单位都是4k,是可以通过读取页的整数倍扩展的,如inodb默认就是16k
内存中分成固定大小的页框(4k),把程序(硬盘上)分成4k大小的宽,用到哪一块,加载到那一块,加载的过程中,如果内存已经慢了,会把最久不用的一块放到swap分区,把最新的一块加载进来,这个就是著名的LRU算法
1、LRU 最久不用
2、哈希表(保证 查找操作O(1))+链表(保证 排序操作和新增操作O(1))
3、双向链表(保证 左边指针 指向右边块)
2、虚拟内存
1、DOS Win31、、、互相干掉
2、为了保证互不影响-让程序工作在虚拟空间,程序中用到的地址不再是直接的物理地址,而是虚拟的地址,这样,A进程永远不可能访问到B进程的空间
3、虚拟空间多大呢?寻址空间 -64位系统2^64,比物理空间大很多,单位是byte
4、站在虚拟的角度,进程是独享整个系统+CPU
5、内存映射:偏移量+断的基地址=线性地址(虚拟空间)
6、线性地址通过OS+MMU()
3、缺页中断
1、需要用到页面内存中没有,产生缺页异常(中断),由内核处理并加载
ZGC
算法叫做:Colored Pointer
GC信息记录在指针上,不是记录在头部, immediate memory use
42位指针 寻址空间4T JDK13 -> 16T 目前为止最大16T 2^44
最大原因是因为商家为了节约成本,提供了最少的足够性能的硬件条件
CPU如何区分一个立即数 和 一条指令
总线内部分为:数据总线 地址总线 控制总线
地址总线目前:48位
颜色指针本质上包含了地址映射的概念