汇编语言(机器语言)的执行过程
汇编语言的本质: 机器语言的助记符 其实它就是机器语言
计算机通电->CPU读取内存中程序(电信号输入)->时钟发生器不断震荡通电->推动CPU内部一步一步执行
(执行多少次取决于指令需要的时钟周期)->计算完成->写回(电信号)->写给显卡输出(sout,或者图形)
计算机的组成
pc(程序计数器,记录当前指令地址): 存储的是内存中程序下一条指令的地址,从内存中取指令,如果是数据,存储在寄存器中
Registers(寄存器): 暂时存储CPU计算需要用到的数据
ALU(算术逻辑运算单元): 逻辑运算单元
CU->control unit: 控制单元
MMU->memory management Unit 内存管理单元
通过PC的值,找到对应的指令,通过总线把这条指令拿到CPU中,发现还需要数据,就把数据从内存中拿进来存储在CPU的Registers中来做计算,ALU运算单元才开始运行,运算完将结果写回到内存中去
cache
超线程:一个核有两对PC和Registers
合并写技术:在寄存器与L1之间有一个只有4个字节的WCBuffer,WCBuffer写满以后一次性写入内存
用的缓存一致性协议来保持缓存行中数据一致
对于伪共享
jdk7多用缓存行对齐来提高效率
jdk8,加入了@Contended注解,需要加上: JVM -XX:-RestrictContended
乱序执行
CPU的乱序执行
CPU层面如何禁止指令重排
内存屏障: 对某部分内存做操作时,前后添加的屏障,屏障前后的操作不可以乱序执行
Intel的lfence mfence sfence原语, 也可以使用总线锁来解决
有序性保证, X86CPU内存屏障
sfence: 在sfence指令前的写操作应当必须在sfence指令后的写操作前完成
lfence: 在lfence指令前的读操作应当必须在lfence指令后的读操作前完成
mfence: 在mfence指令前的读写操作应当必须在mfence指令后的读写操作前完成
有序性保证, Intel lock汇编指令
原子指令,如x86上的"lock ..."指令是一个Full Barrier,执行时会锁住内存子系统来确保执行顺序,甚至跨多个CPU.Software Locks通常使用了内存屏障或原子指令来实现变量可见性和保持程序顺序
JVM层面如何禁止指令重排
JVM层级 JSR内存屏障
LoadLoad屏障: 上面一条Load指令,下面一条Load指令,这两条Load指令不可互换,下面一样
StoreStore屏障
LoadStore屏障
StoreLoad屏障
happens-before原则(JVM规定重排序必须遵守的规则)
就是规定在哪种情况下不能重排序
as if serial
不管如何重排序,单线程执行结果不会改变,看上去像是serial
volatile的实现细节 JVM层面
StoreStoreBarrier LoadLoadBarrier
volatile 写操作 volatile 读操作
StoreLoadBarrier LoadStoreBarrier
WC - Write Combining 合并写技术
为了提高写效率: CPU在写入L1时,同时用WC写入L2
一般是四个字节
由于ALU速度太快,所以在写入L1的同时,写入一个WC Buffer,满了之后,再直接更新到L2
UMA和NUMA
UMA: 统一内存访问
缺点: 不易扩展
CPU数量增多后引起内存访问冲突加剧
CPU的很多资源花在争抢内存地址上
4颗CPU比较合适
NUMA: 对于自己插槽上的内存是有优先级的
在我的主板插槽上,一组CPU和内存放在一起的,有自己的专属内存
他们之前是通过总线相连接的,访问其他内存也是没有问题的,通过总线去访问就可以了
ZGC的NUMA Aware
分配内存会优先分配该线程所在CPU的最近内存
在分配内存的时候,优先在当前线程所在CPU组的内存进行分配,可以显著提高效率
操作系统的启动
通电->BIOS uefi工作->自检->到硬盘固定位置加载BootLoader->读取可配置信息->CMOS
内核分类
宏内核 - PC phone
微内核 -弹性部署 5G loT
外核 -科研 实验中 为应用定制操作系统(多租户 request-base GC JVM)
VMM 硬件资源过剩
用户态与内核态
CPU分不同的指令级别
Linux内核跑在ring 0级,用户程序跑在ring3,对于系统的关键访问,需要经过kernel的同意,保证系统健壮性
内核执行的操作 -> 200多个系统调用 sendfile read write pthread fork
JVM -> 站在OS的角度,就是个普通程序
进程 线程 纤程
进程和线程有什么区别?
进程是一个程序运行起来的状态,线程是一个进程中不同的执行路径.专业:进程是OS分配资源的基本单位,线程是执行调度的基本单位.分配资源最重要的是:独立的内存空间,线程调度执行(线程共享进程的内存空间)
进程
Linux中也称为task,是系统分配资源的基本单位
资源:独立的地址空间 内核数据结构 (进程描述符...) 全局变量 数据段...
进程描述符: PCB(Process Control Block)
线程
线程在Linux中的实现:就是一个普通进程,只不过和其他进程共享资源(内存空间 全局数据等...)
其他系统都有各自的所谓LWP的实现 Light Weight Process
高层面理解: 一个进程中不同的执行路径
纤程或协程
用户态的线程,线程中的线程,切换和调度不需要经过OS
优势: 1.占用资源很少 OS:线程1M Fiber 4k
2.:切换比较简单
3:启动很多歌10W+
纤程的应用场景
纤程VS线程池: 很短的计算任务,不需要和内核打交道,并发量高!
内核线程
内核启动之后经常需要做一些后台操作,这些由Kernal Thread来完成,只在内核空间运行
进程的创建和启动
系统函数fork() exec() 从A中fork B的话,A称为B的父进程,fork()函数内部调用clone()函数
僵尸进程
ps -ef | grep defuct
父进程产生子进程之后,会维护子进程的一个PCB结构,子进程退出,由父进程释放
如果父进程没有释放,那么子进程成为一个僵尸进程
孤儿进程
子进程结束之前,父进程已经退出
孤儿进程会成为init进程的孩子,由1号进程维护
进程调度
内核进程调度器决定:该哪一个进程运行?何时开始?运行多长时间?
多任务
非抢占式(cooperative multitasking)
除非进程主动让出CPU(yielding),否则将一直运行
抢占式(preemptive multitasking)
由进程调度器强制开始或暂停(抢占)某一进程的执行
Linux采用的调度策略
Linux2.5 经典Unix O(1)调度策略,偏向服务器,但对交互不友好
Linux2.6.23 采用CFS完全公平调度算法Completely Fair Scheduler
CFS调度算法
按优先级分配时间片的比例,记录每个进程的执行时间,如果有一个进程执行时间不到它应该分配的比例,优先执行
进程调度基本概念
进程类型
IO密集型 大部分时间用于等待IO
CPU密集型 大部分时间用于闷头计算
进程优先级
实时进程 > 普通进程 (0-99)
普通进程nice值 (-20-99)
时间分配
Linux采用按优先级的CPU时间比
其他系统多采用按优先级的时间片
Linux默认的调度策略
实时(急诊): 优先级分高低-FIFO,优先级一样 --RR(轮询)
普通: CFS
对于实时进程: 使用SCHED_FIFO和SCHED_RR两种
对于普通进程: 使用CFS
其中等级最高的是FIFO,这种进程除非自己让出CPU,否则Linux会一直执行它,除非更高级别的FIFO和RR抢占它
RR只是这种线程中同级别FIFO中的平均分配
只有实时进程主动让出,或者执行完毕后,普通进程才有机会运行
中断
硬件中断(硬中断)
在键盘上按下了任意一个键,它会找到中断控制器,中断控制器发现有一个信号传了过来;这个中断控制器会告诉CPU,CPU有一个针脚就是专门接受中断信号的
先判断是哪种类型的中断,然后去查这个中断向量表中的处理程序是的哪一个
CPU到内存的固定位置去找处理程序,由操作系统管理
来了一个信号,交给内核,找到对应的中断处理程序,然后开始处理
硬中断完整流程
按下一个键信号之后,会交给中断处理器,中断处理器会通知CPU说有一个键盘信号来了,而CPU会到某个一固定位置,找到执行程序,这个执行程序会通知内核,说有一个中断信号来了,
内核会根据中断信号找到已经写好的一堆中断处理程序;
内部处理完成后,我自己会直到有哪些程序正在运行着,比如说那个程序在最前排,比如说这个office它在等在着一个键盘的输入,
内核就会把这个信号传递给这个office,然后由这个office对这个中断信号进行处理
软中断(0x80中断)
要调用系统函数的时候触发
软中断详细过程
系统调用: int 0x80或sysenter原语,通过ax寄存器填入调用号
参数通过bx cx dx si di传入内核,返回值通过ax返回
java读网路-jvm read()-c库read()->内核空间->system_call() (系统调用处理程序)->sys_read()
内存管理
内存管理的发展历程
DOS时代-同一时间只能有一个进程在运行(也有一些特殊算法可以支持多进程)
Windows9x-多个进程装入内存
早期系统:多个进程全部装入内存,出现两个问题
1.内存撑爆
2.互相打扰 不小心访问到别人的空间
为了解决这两个问题,诞生了现在的内存管理系统:
现代内存管理系统
虚拟地址 分页装入 软硬件结合 寻址
解决内存撑爆问题: 分块儿装入页框中(内存页 4K标准页)(局部性原理: 时间局部性-指令旁边的指令很快执行 空间局部性-数据旁边的数据很快用到)内存满了,进行交换分区(LRU算法)
1. 分页(解决内存不够用),内存中分成固定大小的页框(4K),把程序(磁盘上)分成4K大小的块,用到哪一块,加载哪一块,加载的过程中,如果内存已经满了,会把最不常用的一块放到swap分区,把最新的一块加载进来,这就是著名的LRU
2. 虚拟内存(解决相互打扰问题)
每一个进程都虚拟的独占整个CPU,进程内部分段,段内部分页,需要该页的时候加载到页框
为了保证互不影响-让进程工作在虚拟空间,程序中用到的空间地址不再是直接的物理地址,而是虚拟的地址,这样,A进程永远不可能访问到B进程的空间
虚拟空间多大呢?寻址空间-64位系统 2^64byte,比物理空间大很多
站在虚拟的角度,进程是独享整个系统 + CPU
内存映射:偏移量 + 段的基地址 = 线性地址 (虚拟空间)
线性地址通过 OS + MMU(硬件 Memory Management Unit)映射到物理地址
3. 缺页中断(不是很重要)
需要用到页面内存中没有,产生缺页异常(中断),由内核处理并加载
ZGC
算法叫做:Colored Pointer
GC信息记录在指针上,不是记录在头部, immediate memory use
42位指针 寻址空间4T JDK13 -> 16T 目前为止最大16T 2^44
CPU如何区分一个立即数 和 一条指令
总线内部分为:数据总线 地址总线 控制总线
地址总线目前:48位
颜色指针本质上包含了地址映射的概念
内核同步机制
关于同步理论的一些基本概念
•临界区(critical area): 访问或操作共享数据的代码段 简单理解:synchronized大括号中部分(原子性)
•竞争条件(race conditions)两个线程同时拥有临界区的执行权
•数据不一致:data unconsistency 由竞争条件引起的数据破坏
•同步(synchronization)避免race conditions
•锁:完成同步的手段(门锁,门后是临界区,只允许一个线程存在) 上锁解锁必须具备原子性
•原子性(象原子一样不可分割的操作)
•有序性(禁止指令重排)
•可见性(一个线程内的修改,另一个线程可见)
互斥锁 排他锁 共享锁 分段锁
内核同步常用方法
1.原子操作 – 内核中类似于AtomicXXX,位于<linux/types.h>
2.自旋锁 – 内核中通过汇编支持的cas,位于<asm/spinlock.h>
3.读-写自旋 – 类似于ReadWriteLock,可同时读,只能一个写 读的时候是共享锁,写的时候是排他锁
4.信号量 – 类似于Semaphore(PV操作 down up操作 占有和释放) 重量级锁,线程会进入wait,适合长时间持有的锁情况
5.读-写信号量 – downread upread downwrite upwrite (多个写,可以分段写,比较少用)(分段锁)
6.互斥体(mutex) – 特殊的信号量(二值信号量)
7.完成变量 – 特殊的信号量(A发出信号给B,B等待在完成变量上) vfork() 在子进程结束时通过完成变量叫醒父进程 类似于(Latch)
8.BKL:大内核锁(早期,现在已经不用)
9.顺序锁(2.6): – 线程可以挂起的读写自旋锁 序列计数器(从0开始,写时增加(+1),写完释放(+1),读前发现单数, 说明有写线程,等待,读前读后序列一样,说明没有写线程打断)
10.禁止抢占 – preempt_disable()
11.内存屏障 – 见volatile