不求甚解,觀其大略!
彙編語言(機器語言)的執行過程
彙編語言的本質:機器語言的助記符號,就是機器語言
比如 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
優勢:
- 佔用資源少 OS:線程 1M 纖程:4k
- 切換比較簡單
- 可以啓動非常多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 <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);
}
}
父進程死了,但是子進程還在,一般會掛到進程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算法
- LRU算法 LeetCode 146題,頭條要求手撕,阿里去年也要求手撕
- Least Recently Used 最不常用
- 哈希表(保證查找操作 O(1) + 鏈表(保證排序操作和新增操作O(1)))
- 雙向鏈表(保證左邊指針指向右邊塊)
2.虛擬內存(解決相互打擾的問題)
1. DOS Win31 ...相互幹掉,甚至幹掉操作系統
2. 爲了保證不相互影響,讓進程工作在虛擬內存,程序使用的是虛擬內存,而不是直接的物理地址,這樣A進程就永遠不知道B進程的空間
3. 虛擬內存有多大? 尋址空間 = 2^64 ,比物理內存大很多 單位是byte
4. 站在虛擬的角度,進程是獨享整個內存空間的
5. 內存映射: 偏移量+段的基地址 = 線性地址(虛擬地址)
6. 線性地址通過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 ): 由競爭條件引起的數據破壞
- 同步(synchronized):避免競爭條件
- 鎖:完成同步的手段,上鎖解鎖必須具備原子性
- 原子性
- 有序性(禁止指令重拍)
- 可見性(一個線程內的修改,另外一個線程可見)
互斥鎖 排它鎖 共享鎖 分段鎖
內核同步的常用方法
- 原子操作 -內核中類似於AtomicXXX,位於<Linux/types.h>
- 自旋鎖 -內核中通過彙編支持cas,位於<asm/spinlock.h>
- 讀-寫自旋 -類似於ReadWriteLock,可同時讀,只能一個寫,讀的時候是共享鎖,寫的時候是排它鎖
- 信號量 -類似於Semaphore(PV操作 down up操作 佔有和釋放) 重量級鎖,線程會進入wait,適合長時間持有鎖的情況
- 讀-寫信號量(多個寫,可以分段寫,比較少用)(分段鎖)
- 互斥體(mutex) – 特殊的信號量(二值信號量)
- 完成變量 – 特殊的信號量(A發出信號給B,B等待在完成變量上) vfork() 在子進程結束時通過完成變量叫醒父進程 類似於(Latch)
- BKL:大內核鎖(早期,現在已經不用)
- 順序鎖(2.6): -線程可以掛起的讀寫自旋鎖 序列計數器(從0開始,寫時增加(+1),寫完釋放(+1),讀前發現單數, 說明有寫線程,等待,讀前讀後序列一樣,說明沒有寫線程打斷)
- 禁止搶佔 – preempt_disable()
- 內存屏障 – 見volatile