CSAPP深入理解计算机系统

文章目录

  • CSAPP深入理解计算机系统
    • 信息
      • 信息存储
        • 字长
        • 字节序
      • 位运算
        • 位运算符
        • 逻辑运算
        • 移位运算
      • 整数
        • 补码规则
        • 表示范围
        • 整数运算
      • 浮点数
        • 编码
        • 规格化数
          • 阶码
          • 尾数
        • 非规格化数
          • 阶码
          • 尾数
        • 特殊值
        • 向偶数舍入
        • 计算
    • 汇编
      • 顺序执行
        • 指令
        • 寄存器
        • 计算指令
      • 控制
        • 条件码
        • 跳转
        • 循环
        • switch语句
      • 过程
        • 栈帧
        • 转移控制
        • 数据传送
        • 数组
        • 结构体
        • 联合体
        • 数据对齐
        • 指针
        • 缓冲区溢出
    • 处理器
      • 指令集体系结构
        • CISC
        • RISC
      • 顺序执行CPU
        • SEQ 硬件结构
        • SEQ 时序
          • 组合逻辑
          • 状态单元
          • 时序过程
        • SEQ 阶段实现
      • 流水线
        • 吞吐量和延迟
        • 流水线示例
        • 局限性
        • 五阶段流水线
          • 全流程图
          • PC预测
        • 流水线冒险
          • 暂停
          • 转发
          • 加载互锁
          • 避免控制冒险
        • 性能分析
          • CPI 计算
    • 链接
      • 程序的诞生
        • 过程
        • 静态链接
        • 目标文件
          • 可重定位目标文件
          • 可执行目标文件
      • 符号解析
        • 解析多重定义
      • 重定位
      • 动态链接共享库
      • 库打桩技术
    • 异常控制流
      • 异常
        • 异常号
        • 异常表
        • 异常类型
          • 中断
          • 陷阱和系统调用
          • 故障
          • 终止
      • 进程
        • 逻辑控制流
        • 并发流
        • 用户模式和内核模式
        • 上下文切换
    • 存储器
      • 存储技术
        • 随机访问存储器
        • 磁盘存储
      • 局部性
      • 存储器层次结构
        • 缓存
          • 缓存命中
          • 缓存不命中
        • 数据与指令
        • 性能
        • 缓存友好的代码
      • 高速缓存存储器
        • 直接映射高速缓存
        • 组相联高速缓存
        • 全相联高速缓存
        • 写数据
    • 虚拟内存
      • 地址空间和寻址
      • 页表
        • 页命中
        • 缺页
        • 分配页面
      • 内存管理与保护
      • 地址翻译
        • 名词解释
        • 映射方法
        • 过程
          • 页面命中
          • 页面不命中
        • 快表 TLB
        • 多级页表
      • 结合SRAM和DRAM

信息

信息存储

字长

每个计算机都有一个字长的属性,是指针数据的标称大小(虚拟地址宽度)

目前64位字长是主流,寻址能力达到了18EiB

注:1EiB = 1024PiB = 1024 * 1024TiB

C语言数据类型位宽

数据类型 32位系统 64位系统 x86-64
char 1 1 1
short 2 2 2
int 4 4 4
long 4 8 8
float 4 4 4
double 8 8 8
long double - - 10/16
pointer 4 8 8

字节序

字节序只是字节的顺序,与每个字节中位的顺序无关

小端序

Little endian (Intel)

低地址存放低位数据,高地址存放高位数据

大端序

Big endian (IBM, Sun Microsystem)

低地址存放高位数据,高地址存放低位数据

例如:0x1234567

Little endian 小端序

0x100 0x101 0x102 0x103
67 45 23 01

Big endian 大端序

0x100 0x101 0x102 0x103
01 23 45 67

注:字符串的表示与字节序无关,大小端兼容

位运算

位运算符

| Or 位或
& And 位与
~ Not 位非
^ exclusive-Or 位异或

逻辑运算

|| logical or 逻辑或
&& logical and 逻辑与
! logical not 逻辑非

短路效应

  • x && 5/x 避免除0运算
  • p && *p++ 避免空指针运算
  • 5 || x=y 赋值语句不会执行

移位运算

右移运算有逻辑移位和算数移位

逻辑移位:左侧补0

算数移位:左侧补原最高位值

对于无符号数,右移一定是逻辑的
对于有符号数,默认算数右移

优先级:位运算优先级最低,注意加括号

整数

C语言中,有符号数与无符号数混用,默认将有符号数作为无符号数处理

补码规则

非负:补码等于二进制数,位长不够补0

负:绝对值的二进制数逐位取反加1,符号位置1

表示范围

无符号数 0 ∼ 2 w − 1 0\sim 2^w-1 0∼2w−1

有符号数 − 2 w − 1 ∼ 2 w − 1 − 1 -2^{w-1}\sim 2^{w-1}-1 −2w−1∼2w−1−1

  • 等价性:无符号数和有符号数非负值编码相同
  • 唯一性:每个编码对应唯一整数,每个整数有唯一编码
  • 可以反向映射: U 2 B ( x ) = B 2 U − 1 ( x ) U2B(x)=B2U^{-1}(x) U2B(x)=B2U−1(x), T 2 B ( x ) = B 2 T − 1 ( x ) T2B(x)=B2T^{-1}(x) T2B(x)=B2T−1(x)

∣ T M i n ∣ = T M a x + 1 \left | T_{Min} \right | = T_{Max}+1 ∣TMin​∣=TMax​+1

U M a x = 2 × T M a x + 1 U_{Max} = 2 \times T_{Max}+1 UMax​=2×TMax​+1

Value 8 16 32 64
U M a x w UMax_w UMaxw​ 0xFF 0xFFFF 0xFFFFFFFF 0xFFFFFFFFFFFFFFFF
255 65535 4 × 1 0 9 4 \times 10^{9} 4×109 1.8 × 1 0 19 1.8 \times 10^{19} 1.8×1019
T M i n w TMin_w TMinw​ 0x80 0x8000 0x80000000 0x80000000000000
-128 -32768 − 2 × 1 0 9 -2 \times 10^{9} −2×109 − 9 × 1 0 18 -9 \times 10^{18} −9×1018
T M a x w TMax_w TMaxw​ 0x7F 0x7FFF 0x7FFFFFFF 0x7FFFFFFFFFFFFFFF
127 32767 2 × 1 0 9 2 \times 10^{9} 2×109 − 9 × 1 0 18 -9 \times 10^{18} −9×1018
-1 0xFF 0xFFFF 0xFFFFFFFF 0xFFFFFFFFFFFFFFFF
0 0x00 0x0000 0x00000000 0x0000000000000000

扩展:无符号数零扩展,有符号数符号位扩展

截断:多余的位被直接丢弃

整数运算

加法

编码的二进制加法,然后按位长截断

判断 u + v = s u+v=s u+v=s 是否溢出

OF = (u<0 == v<0) && (u<0 != s<0)

相反数

减法运算就是加上它的相反数

计算相反数的结果是其二进制逐位取反加一 − b = ∼ b + 1 -b =\ \sim b + 1 −b= ∼b+1

− 2 w − 1 -2^{w-1} −2w−1 相反数是其本身,其二进制表示除第一位为 1,其余全是 0

乘法

两个 w w w 位数的乘法,结果是 2 w 2w 2w 位,否则无法精确表示结果

否则会发生截断,丢掉高位

short a, b;
int c;
c = a * b;

移位

用来实现除 2 n 2^{n} 2n

浮点数

IEEE754 规范

数字的表示形式:

( − 1 ) s × M × 2 E (-1)^s \times M \times 2^E (−1)s×M×2E

  • 符号位s:决定数字正负
  • 尾数M:一个小数,通常在[1.0, 2.0)或[0.0, 1.0)
  • 阶码E:浮点数的权重,2的E次幂

编码

符号 s exp frac
单精度 1 8-bits 23-bits
双精度 1 11-bits 52-bits

规格化数

exp不等于全 0 或全 1

e x p ≠ 000 … 0 a n d e x p ≠ 111 … 1 exp \ne 000\dots 0 \quad and \quad exp\ne 111\dots 1 exp=000…0andexp=111…1

阶码

阶码是一个有偏置的指数

Exp: exp域无符号数编码值

E = E x p − B i a s E = Exp - Bias E=Exp−Bias

b i a s = 2 k − 1 − 1 bias = 2^{k-1} - 1 bias=2k−1−1 k是exp的位宽

符号 bias Exp E = Exp - Bias
单精度 127 1 ~ 254 -126 ~ 127
双精度 1023 1 ~ 2046 -1022 ~ 1023
尾数

尾数编码包含一个隐式前置的1,这个1始终存在,因此在frac中不需要包含

M = 1. x x x … x 2 M=1.xxx \dots x_2 M=1.xxx…x2​

x x x … x xxx\dots x xxx…x 为frac域的各位的编码bits of frac

  • 最小值frac = 000…0 (M = 1.0)
  • 最大值frac = 111…1 (M = 2.0 - )

非规格化数

e x p = 000 … 0 exp = 000\dots 0 exp=000…0

阶码

E = − B i a s + 1 E = - Bias + 1 E=−Bias+1

float double
E=-126 E=-1022
尾数

尾数编码包含一个隐式前置的 0

M = 0. x x x … x 2 M=0.xxx \dots x_2 M=0.xxx…x2​

x x x … x xxx\dots x xxx…x 为frac域的各位的编码bits of frac

表示0

e x p = 000 … 0 a n d f r a c = 000 … 0 exp = 000\dots 0 \quad and \quad frac = 000\dots 0 exp=000…0andfrac=000…0

非常接近0.0

e x p = 000 … 0 a n d f r a c ≠ 000 … 0 exp = 000\dots 0 \quad and \quad frac\ne 000\dots 0 exp=000…0andfrac=000…0

表示非常接近0.0的数字,是等间距的

特殊值

e x p = 111 … 1 exp = 111\dots 1 exp=111…1

表示无穷∞

e x p = 111 … 1 a n d f r a c = 000 … 0 exp = 111\dots 1 \quad and \quad frac = 000\dots 0 exp=111…1andfrac=000…0

意味着运算出现了溢出,正向溢出或负向溢出,如: 1.0 0.0 = − 1.0 − 0.0 = + ∞ , − 1.0 0.0 = 1.0 − 0.0 = − ∞ \frac{1.0}{0.0}=\frac{-1.0}{-0.0}=+\infty,\quad\frac{-1.0}{0.0}=\frac{1.0}{-0.0}=-\infty 0.01.0​=−0.0−1.0​=+∞,0.0−1.0​=−0.01.0​=−∞

表示NaN

e x p = 111 … 1 a n d f r a c ≠ 000 … 0 exp = 111\dots 1 \quad and \quad frac \ne 000\dots 0 exp=111…1andfrac=000…0

不是一个数字,表示数值无法确定,如 − 1 , ∞ − ∞ , ∞ × 0 \sqrt{-1}, \infty -\infty,\infty\times0 −1 ​,∞−∞,∞×0

向偶数舍入

几种舍入模式

  • 向下舍入,舍入结果不大于实际结果
  • 向上取整,舍入结果不小于实际结果
  • 向0舍入,舍入结果向0的方向靠近
  • 向偶数舍入

浮点数运算默认向偶数舍入,其他的舍入模式都会统计偏差,如一组正数的总和将总被高估或低估

对于十进制数:当需要舍入的数字处于恰好5时,其后方无更小的有效位,向保证舍入后保留的最后一位是偶数的方向进行舍入,其他情况按照四舍五入计算

向偶数舍入 说明
1.3499999 1.3 四舍
1.3500001 1.4 五入
1.3500000 1.4 中间,向上舍入,偶数方向
1.4500000 1.4 中间,向下舍入,偶数方向

对于二进制数:如果舍入部分为 1000 … 2 1000\dots_2 1000…2​,则舍入方向为舍入后最低位为0

舍入结果 舍入说明
10.00011 10.00 Down
10.0011 10.01 Up
10.1110 11.00 Up
10.1010 10.10 Down

计算

基本思想

  • 先计算出精确的值
  • 将结果调整至目标精度(注意舍入、溢出)

( − 1 ) s 1 M 1 ⋅ 2 E 1 × ( − 1 ) s 2 M 2 ⋅ 2 E 2 (-1)^{s1}M_1\cdot 2^{E1} \times (-1)^{s2}M_2\cdot 2^{E2} (−1)s1M1​⋅2E1×(−1)s2M2​⋅2E2

( − 1 ) s 1 M 1 ⋅ 2 E 1 + ( − 1 ) s 2 M 2 ⋅ 2 E 2 (-1)^{s1}M_1\cdot 2^{E1} + (-1)^{s2}M_2\cdot 2^{E2} (−1)s1M1​⋅2E1+(−1)s2M2​⋅2E2

汇编

顺序执行

程序计数器PC,将要执行的下一条指令地址,即%rip

C代码中,内联汇编

指令

    操作码  源操作数, 目标操作数

操作数可以是:

  • 寄存器
  • 立即数
  • 内存寻址

立即数只可以作为源操作数
源操作数和目标操作数不可以都是内存寻址

寄存器

寄存器类型 寄存器
参数 %rdi; %rsi; %rdx; %rsx; %r8; %r9
被调用者保存 %rbx; %rbp; %r12; %r13; %r14; %r15
调用者保存 %r10; %r11
返回值 %rax
栈指针 %rsp

32位操作符,将目标64位寄存器高4字节置0

movq,只以32位补码立即数作源操作数,符号扩展为64位

movabsq,任意64位立即数作源操作数,以寄存器为目的

movz_ _零扩展,movs_ _符号扩展

不存在movzlq,因为movl就是这个作用

cltp寄存器符号扩展,指令无操作数,只作用于%eax%rax

栈向下增长,栈顶地址更低

pushq S%rsp减8后将S存入%rsp存储的地址

popq D%rsp存储地址的值赋给D,%rsp再加8

leaq,源操作数只能是内存地址或寄存器,目的数只能是寄存器

lea_只计算地址,不进行内存访问,有时候甚至计算的不是地址,只是将源操作数进行简单的加法和乘法运算存入目的数

计算指令

一些计算指令,都有b/w/l/q的不同版本:

 inc_ , dec_ , neg_ , not_add_ , sub_ , imul_ , xor_ , or_ , and_sal_ , shl_ , sar_ , shr_

对应的功能:

 ++, --, -D, ~D+, -, *, ^, |, &左移,左移,算数右移,逻辑右移

关于减法:
目标操作数 - 源操作数
注意顺序

关于64位乘法:
第一种是 imul_,双操作数
第二种是单操作数,分为有符号 imulq和无符号 mulq
另一个参数存在 %rax(低64位),并用 %rdx(高64位)扩展为全128位

关于除法:
idivl单操作数,有符号除法
%rax(低64位)和 %rdx(高64位)中的128位作为被除数,操作数作为除数,商存储在 %rax,余数存储在 %rdx

cqto,符号扩展,将R[%rax]扩展为R[%rdx]:R[%rax]

控制

条件码

​ CF(carry flag),最高位进位,无符号操作溢出
​ ZF(zero flag),零标志,结果0则标志置1
​ SF(sign flag),符号标志,0正1负
​ OF(overflow flag),补码溢出,正溢出或负溢出
​ PF(parity flag),奇偶标志,0偶1奇

cmp_ S1, S2 考察S2-S1
test_ S1, S2 考察S1&S2
这两个指令不改变寄存器,只改变条件码
test_常被用来判断是否为0

testq %rax, %rax
je zero

访问条件码,通过set_ D指令

相等与否(equal,zero),负数与否(sign)

sete, setz ZF
setne, setnz ~ZF
sets SF
setns ~SF

有符号大于等于小于(greater,equal,less)
setg, setnle ~(SF^OF)&~ZF
setge,setnl ~(SF^OF)
setl, setnge SF^OF
setle, setng (SF^OF) | ZF

无符号超过相等低于(above,equal,below)
seta, setnbe ~CF & ~ZF
setae, setnb ~CF
setb, setnae CF
setbe, setna CF | ZF

跳转

直接跳转

jmp .L1

间接跳转

jmp *%rax,jmp *(%rax)

条件跳转
相等与否(equal,zero),负数与否(sign)
je, jz ZF
jne, jnz ~ZF
js SF
jns ~SF

有符号大于等于小于(greater,equal,less)
jg, jnle ~(SF^OF)&~ZF
jge, jnl ~(SF^OF)
jl, jnge SF^OF
jle, jng (SF^OF) | ZF

无符号超过相等低于(above,equal,below)
ja, jnbe ~CF & ~ZF
jae, jnb ~CF
jb, jnae CF
jbe, jna CF | ZF

注:条件跳转只能是直接跳转

跳转指令寻址:
PC相对寻址,PC为跳转指令下一条的地址,加上偏移量(编码为1/2/4字节),得到目标地址

条件控制实现条件分支

 t = test-expr;if(!t)goto false;//then-statementgoto done;
false://else-statement
done:

分支预测错误处罚
T a v g ( p ) = T O K + p T M P T_{avg}(p) = T_{OK} + pT_{MP} Tavg​(p)=TOK​+pTMP​

条件传送
cmov__ S, R

源寄存器或内存地址S,目的寄存器R,S和R可以是16/32/64位长,不可以是8位

相等与否(equal,zero),负数与否(sign)
cmove, cmovz ZF
cmovne, cmovnz ~ZF
cmovs SF
cmovns ~SF

有符号大于等于小于(greater,equal,less)
cmovg, cmovnle ~(SF^OF)&~ZF
cmovge, cmovnl ~(SF^OF)
cmovl, cmovnge SF^OF
cmovle, cmovng (SF^OF) | ZF

无符号超过相等低于(above,equal,below)
cmova, cmovnbe ~CF & ~ZF
cmovae, cmovnb ~CF
cmovb, cmovnae CF
cmovbe, cmovna CF | ZF

条件传送实现条件分支
使用数据的条件转移,即算一个条件操作的两种结果,再根据条件是否满足从中选一

原始语句形式

v = test-expr ? then-expr : else-expr;

基于条件传送

v = then-expr;
ve = else-expr;
t = test-expr;
if(!t) v = ve;

循环

do-while循环
基本形式

do// body-statementwhile (test-expr);

条件和goto形式

loop:// body-statementt = test-expr;
if (t)goto loop;

while循环

基本形式

while (test-expr);// body-statement

方法一:jump to middle 跳转到中间

先跳转到结尾,执行初始的测试,再根据条件进入循环

 goto test;
loop:// body-statement
test:t = test-expr;if (t)goto loop;

方法二:guarded-do

先用条件分支,初始条件不成立就跳过循环,再把while循环变换成do-while循环

t = test-expr;
if (!t)goto done;
loop://body-statementt = test-expr;if (t)goto loop;
done:

for循环

标准C语言形式

for (init-expr; test-expr; update-expr)// body-statement

转化成标准的while形式

init-expr;
while (test-expr) {// body-statementupdate-expr;
}

跳转到中间策略的goto代码

 init-expr;goto test;
loop:// body-statementupdate-expr;
test:t = test-expr;if (t)goto loop;

guarded-do策略的goto代码

 init-expr;t = test-expr;if (!t)goto done;
loop:// body-statementupdate-expr;t = test-expr;if (t)goto loop;
done:

switch语句

跳转表jump table,程序通过开关索引值来执行跳转表内的数组引用,确定跳转目标

应用情况:开关数量较多,值的范围跨度较小

C语言下switch

switch (n)
{case 100:
case 102:// codebreak;
case 103:// codebreak;
case 104://  codebreak;

扩展的C语言

 static void *jt [5] = {&&loc_A, &&loc_def, &&loc_A, &&loc_B, &&loc_C};unsigned long index = n - 100;if (index > 6)goto loc_def;goto *jt [index];loc_A:// case 100, 102goto done;
loc_B:// case 103goto done;
loc_C://case 104goto done;
loc_def://def
done:// after all

过程

P调用Q,Q返回P

  1. 传递控制:进入Q前PC设为Q地址,返回P前PC设为P的下一条地址
  2. 传递数据:P向Q多个,Q返回P最多一个
  3. 分配和释放内存

栈帧

过程的栈帧,在栈上为过程分配的空间

转移控制

过程调用
call Label 直接调用
call *Operand 间接调用

从过程调用中返回
ret

注:callq和retq的q是反汇编产生的,用来表示是x86-64版本的调用和返回

执行call,将返回地址压入栈,并将PC设为跳转地址

数据传送

寄存器最多传递6个整形参数(整数或指针)按照顺序使用

bit 1 2 3 4 5 6
64 %rdi %rsi %rdx %rcx %r8 %r9
32 %edi %esi %edx %ecx %r8d %r9d
16 %di %si %dx %cx %r8w %r9w
8 %dil %sil %dl %cl %r8b %r9b

参数大于6个:1~6存入对应寄存器,7~n

P调用Q

callee被调用者保存:%rbx,%rbp,%r12~%r15由Q来保存

caller调用者保存:除了栈指针%rsp以外其他所有寄存器,P来保存

变长栈帧

%rbp基指针(base pointer),帧指针(frame pointer)

数组

T A[N]

起始位置 x A x_{A} xA​,首先在内存中分配一个 L ⋅ N L \cdot N L⋅N字节的连续区域(L是数据类型T的大小),其次引入标识符A,可用来作为指向数组开头的指针 x A x_{A} xA​

假设E是int型的数组,想计算E[i]

E的地址存在%rdx中,i存储在%rcx

movl (%rdx, %rcx, 4), %eax

指针运算

x p + L ⋅ i x_p+L \cdot i xp​+L⋅i

其中L是数据类型T的大小

单操作数操作符

&产生指针:给出该对象地址的指针

*产生间接引用指针:给出该地址处的值

嵌套数组

T D[R][C];

数组元素D[i][j]的内存地址是:

& D [ i ] [ j ] = x D + L ( C ⋅ i + j ) \& D[i][j]=x_D+L(C \cdot i+j) &D[i][j]=xD​+L(C⋅i+j)

定长数组

#define N 16
typedef int fix_matrix[N][N]

变长数组

允许数组的维度是表达式,在数组被分配时才计算出来

int A[expr1][expr2]
int val_ele(long n, int A[n][n], long i, long j) {return A[i][j];
}

结构体

structure

(*rp).width
rp->width

两种表达方式等价,间接引用了这个指针

struct rec {int i;int j;int a[2];int *p;
}
0 4 8 12 16 20 24
i i i j j j a [ 0 ] a[0] a[0] a [ 1 ] a[1] a[1] p p p

将 r → i r→i r→i 复制到 r → j r→j r→j:

movl (%rdi), %eax
movl %eax, 4(%rdi)

要产生一个指向结构内部对象的指针,只需要将结构的地址加上该字段的偏移量

联合体

union

用不同的字段引用相同的内存块

struct S {char c;int i[2];double v;
};
union U {char c;int i[2];double v;
};

下表展示了 SU 各字段的偏移量和完整大小

类型 c i v 大小
S 0 4 16 24
U 0 0 0

用途:

事先知道一个数据结构中两个不同字段的使用是互斥的,使用联合可以减小分配空间的总量;

用来访问不同数据类型的位模式,如以double存储,以unsigned long long读取

数据对齐

对齐原则:任何 K K K字节的基本对象的地址必须是 K K K的倍数

K K K 类型
1 char
2 short
4 int, float
8 long, double, char*

指明全局数据所需的对齐的汇编代码命令

.align 8

对于包含structure的代码,编译器可能会在字段的分配中插入间隙,以保证每个结构元素都满它的对齐要求

struct S {int i;char c;int j;
};

编译器在 c c c 和 j j j 字段中间插入一个3字节的间隙

i (0-3) c (4-7) j (8-12)
■■■■ ■□□□ ■■■■

结构的末尾也需要填充,来满足对齐要求

struct S {int i;int j;char c;
};

编译器在 c c c 字段结尾插入一个3字节的间隙

i (0-3) j (4-7) c (8-12)
■■■■ ■■■■ ■□□□

每个结构体有一个对齐要求K

K为结构体中所有元素中的最大对齐需求,结构体的初始地址和大小必须为K的整数倍

指针

每个指针都对应一种类型

将指针从一种类型转化为另一种类型,只改变指针的类型,不改变指针的值

(注:强制类型转换的优先级高于加法)

指针也可以指向函数

int fun(int x, int *p);int (*fp)(int, int *);
fp = fun;int y = 1;
int result = fp(3, &y);

函数指针的值是该函数机器代码表示中第一条指令的地址

缓冲区溢出

在栈中分配的字符数组,保存的字符串长度超过为数组分配的空间

echo:subq  $24, %rspmovq  %rsp, %rdicall  getsmovq  %rsp, %rdicall  putsaddq  $24, %rspret
输入字符串数量 附加的被破坏的状态
0~7
9~23 未被使用的栈空间
24~31 返回地址
32+ caller中保存的状态

对抗缓冲区溢出攻击

  1. 栈随机化

    程序开始时,在栈上分配一段0~n字节的随机大小的空间,程序不使用这段空间,但会导致程序每次执行时后续栈位置发生变化

    地址空间布局随机化

    Address-Space Layout Randomization

  2. 栈破坏检测

    在代码中加入栈保护者stack protector

    其思想是在栈帧中任何局部缓冲区与栈状态之间存储金丝雀值canary,也被称为哨兵值guard value

    该值是在程序每次运行时随机产生的

  3. 限制可执行代码区域

    rwx读、写、执行

处理器

指令集体系结构

指令集体系结构 ISA

Instruction-Set Architecture指一个处理器支持的指令和指令的字节级编码

CISC

复杂指令集计算机 Complex instruction set computer

x86家族:IA32 (x86-32), x86-64
System/360, PDP-11, VAX, Data General Nova
嵌入式处理器:Motorola 6800, Zilog Z80, 8051-family

以IA32为例

  1. 面向栈的指令集:
  • 使用栈传递参数,保存程序计数器
  • 显式的入栈和出栈指令
  1. 算术运算指令可以直接访问内存
addq %rax, 12(%rbx, %rcx, 8)
  • 包含了存储器的读和写
  • 包含了复杂的地址计算
  1. 条件码:可以通过算数逻辑运算的指令的副作用设置

  2. 设计理念:使用指令实现典型的任务

RISC

精简指令集计算机 Reduced instruction set computer

IBM/Freescale Power, ARM, MIPS, LoongISA, SPARC, RISC-V

以MIPS为例

  1. 更少的,更简单的指令
  • 需要花费更多的指令
  • 可在更小更快的硬件上执行
  • 对于嵌入式处理器,RISC更有意义
  1. 面向寄存器的指令集
  • 更多的寄存器(典型值32)
  • 用于传递参数,返回地址,临时数据
  1. 只有加载和存储指令可以访问内存
lw $t1, 0($s0)
sw $s0, 0($sp)
  1. 没有条件码
  • 测试指令将返回结果0/1写入寄存器

x86-64借鉴了很多RISC的特征,如更多的寄存器,用来传递参数

x86是CISC,但仅有一个CISC的壳,内部核心是RISC的

顺序执行CPU

阶段 英文 执行操作
取指 fetch 从指令存储器读取指令
译码 decode 读取寄存器
执行 execute ALU计算值和地址
访存 memory 从内存读数据 或 向内存写数据
写回 write back 写程序相关的寄存器
PC更新 PC update 更新程序计数器到下一条指令

SEQ 硬件结构

SEQ Sequential Logic:时序逻辑

ALU 算数/逻辑单元
CC 条件码寄存器
PC 程序计数器
Register 寄存器文件
内存:指令内存/数据内存

SEQ 时序

考虑两类存储设备:

  • 时钟寄存器(寄存器):PC,CC
  • 随机访问存储器(内存):虚拟内存,寄存器文件

一个时钟变化会引发一个经过组合逻辑的流,来执行整个指令

组合逻辑

组合逻辑(如ALU)不需要任何时序或控制,只要输入变化,值就通过逻辑门网络传播

读随机访问存储器,可看作组合逻辑。指令内存只有读操作

寄存器文件和内存的读操作可看作是组合逻辑,而写操作是由时钟控制的

状态单元

还有四个硬件需要对时序进行明确控制,这些单元通过一个时钟信号来控制

  • 程序计数器 PC
  • 条件码寄存器 CC
  • 数据内存
  • 寄存器文件

要控制处理器中活动的时序,只需要对寄存器和内存的时钟控制

在时钟上升开始下一周期时,处理器同时执行寄存器写和内存写

PC、CC、Register、数据内存,所有状态更新同时发生

时序过程

从不回读原则

处理器从来不需要为了完成一条指令的执行而去读由该指令更新了的状态

一个时钟周期

在进入一个时钟周期时,状态单元保持的是上一条指令更新过的状态,这些状态单元由上一条指令在这个时钟周期波形的上升段进行更新。此时组合逻辑尚未对变化了的状态作出反应

时钟周期开始时,地址载入状态单元中的程序计数器,这个更新由上一个指令执行。接着就会取出和处理当前这条指令。值沿着组合逻辑流动,包括读随机访问存储器。

在这个时钟周期的末尾,组合逻辑产生了新的条件码、程序寄存器的更新值、程序计数器的新值,此时组合逻辑已经根据当前这条指令被更新了,但是状态单元还是保持着上一条指令更新后的状态

当时钟上升开始下一个周期时,会更新程序计数器、条件码寄存器、寄存器文件、数据内存

SEQ 阶段实现

1. 取指阶段

P C ↓ p c 增加 ⟶ [ 指令内存 ] { B y t e 0 ⟶ [ S p l i t ] ⟶ i c o d e ⟶ i f u n B y t e 1 − 9 ⟶ [ A l i g n ] ⟶ r A ⟶ r B ⟶ v a l C \begin{matrix}PC \\ \downarrow \\ pc增加 \end{matrix}\longrightarrow \begin{bmatrix} \\指令内存 \\ \\ \end{bmatrix}\left\{ \begin{matrix}Byte 0\longrightarrow\begin{bmatrix}\\Split\\\\\end{bmatrix}\begin{matrix}\longrightarrow icode \\ \\ \longrightarrow ifun\end{matrix}\\ \\ \ Byte 1-9 \longrightarrow \begin{bmatrix}\\Align\\\\\end{bmatrix} \begin{matrix} \longrightarrow rA \\ \longrightarrow rB \\ \longrightarrow valC \end{matrix} \end{matrix}\right. PC↓pc增加​⟶ ​指令内存​ ​⎩ ⎨ ⎧​Byte0⟶ ​Split​ ​⟶icode⟶ifun​ Byte1−9⟶ ​Align​ ​⟶rA⟶rB⟶valC​​

以PC作为第一个字节的地址(字节0),一次从内存取出10个字节

第一个字节由Splic单元分割,然后标号为icodeifun的控制逻辑模块计算出指令和功能码

当地址不合法时,产生信号imem_error,上述值被设置为nop指令

根据icode的值,可以计算三个一位的信号:

instr_valid 这个字节是否是合法的指令

need_regids 这个指令包括寄存器指示符字节吗

need_calC 这个指令包括常数吗

当指令地址越界时会产生的信号instr_validimem_error在访存阶段被用来产生状态码

2. 译码和写回阶段

设寄存器文件有四个端口,两个读两个写

指令字段译码,产生寄存器文件使用的四个地址的寄存器标识符(两个读,两个写)

3. 执行阶段

执行阶段包括算术/逻辑单元,执行阶段的第一步就是每条指令的ALU计算

4. 访存阶段

访存阶段的任务就是读或者写程序数据,两个控制块产生内存地址和内存输入的值,另外两个块产生表明应该执行读操作还是写操作的控制信号

5. 更新PC阶段

SEQ中的最后一个阶段会产生程序计数器的新值,依据指令的类型和是否要选择分支

流水线

流水线化的一个重要特征,就是提高了系统的吞吐量,也会轻微的增加延迟

吞吐量和延迟

假设组合逻辑需要 300ps,而加载寄存器需要 20ps,从头到尾执行一条指令所需要的时间称为延迟,在此系统中延迟为320 PS,也就是吞吐量的倒数

如下图的这样一个非流水线化的计算机硬件,每320ps的周期内,系统用300ps计算组合逻辑函数,20ps将结果存到输出寄存器中

→ [ 组合逻辑 A 300 p s ] → ∣ 寄 存 器 20 p s ∣ \to \begin{bmatrix}\\组合逻辑A\\300ps\\\\\end{bmatrix}\to \begin{vmatrix}寄\\存\\器\\20ps\end{vmatrix} → ​组合逻辑A300ps​ ​→ ​寄存器20ps​ ​

时钟 ⊓ ⊔ \sqcap \sqcup ⊓⊔ ⊓ ⊔ \sqcap \sqcup ⊓⊔ ⊓ ⊔ \sqcap \sqcup ⊓⊔
I1 A
I2 B
I3 C

延迟

指令延迟 = 时钟周期 * 阶段数

描述单位ps,微微秒或皮秒,picosecond

上图非流水线的延迟:320ps

吞吐量

吞吐量描述单位 GIPS,每秒千兆条指令,即每秒十亿条指令

上图非流水线的吞吐量:

吞吐量 = 1 条指令 ( 20 + 300 ) p s ⋅ 1000 p s 1 n s ≈ 3.12 G I P S 吞吐量=\frac{1条指令}{(20+300)ps} \cdot \frac{1000ps}{1ns} \approx 3.12\ GIPS 吞吐量=(20+300)ps1条指令​⋅1ns1000ps​≈3.12 GIPS

流水线示例

假设将系统执行的计算分成三个阶段,A, B, C每个阶段需要100ps,然后各个阶段之间放上流水线寄存器 pipeline register,这样每条指令都会按照三步经过这个系统,从头到尾需要三个完整的时钟周期

这样的时钟周期设置为 100+20=120ps,得到的吞吐量大约为8.33GIPS

→ [ 组合逻辑 A 100 p s ] → ∣ 寄 存 器 20 p s ∣ → [ 组合逻辑 B 100 p s ] → ∣ 寄 存 器 20 p s ∣ → [ 组合逻辑 C 100 p s ] → ∣ 寄 存 器 20 p s ∣ \to \begin{bmatrix}组合逻辑A\\100ps\end{bmatrix}\to \begin{vmatrix}寄\\存\\器\\20ps\end{vmatrix}\to \begin{bmatrix}组合逻辑B\\100ps\end{bmatrix}\to \begin{vmatrix}寄\\存\\器\\20ps\end{vmatrix}\to \begin{bmatrix}组合逻辑C\\100ps\end{bmatrix}\to \begin{vmatrix}寄\\存\\器\\20ps\end{vmatrix} →[组合逻辑A100ps​]→ ​寄存器20ps​ ​→[组合逻辑B100ps​]→ ​寄存器20ps​ ​→[组合逻辑C100ps​]→ ​寄存器20ps​ ​

时钟 ⊓ ⊔ \sqcap \sqcup ⊓⊔ ⊓ ⊔ \sqcap \sqcup ⊓⊔ ⊓ ⊔ \sqcap \sqcup ⊓⊔ ⊓ ⊔ \sqcap \sqcup ⊓⊔ ⊓ ⊔ \sqcap \sqcup ⊓⊔
I1 A B C
I2 A B C
I3 A B C
时间线 0 ⟼ 0\longmapsto\quad 0⟼ 120 ⟼ 240 120\longmapsto\quad240 120⟼240 240 ⟼ 360 240\longmapsto\quad360 240⟼360 360 ⟼ 480 360 \longmapsto\quad480 360⟼480 $480 \longmapsto\quad 600 $
时钟 ⊓ ⊔ \sqcap \sqcup ⊓⊔ ⊓ ⊔ \sqcap \sqcup ⊓⊔
I1 B C
I2 A B
I3 A
时间线 120 ⟼ 120\longmapsto\quad 120⟼ ⊢ 240 ⟼ 360 ⊣ \vdash240\longmapsto\quad\quad\quad360\dashv ⊢240⟼360⊣
关注点 ↗ ① \begin{matrix}\nearrow\\① &\ \end{matrix} ↗①​ ​ ↖ ↑ ↗ ② ③ ④ \begin{matrix}\nwarrow&&\uparrow&&\nearrow\\&②&③&④&\end{matrix} ↖​②​↑③​④​↗​

① 阶段 A 中计算的指令 I2 的值已经达到第一个流水线寄存器的输入,但是该寄存器的状态和输出还保持为指令 I1 在阶段 A 中计算的值,指令 I1 在阶段 B 中计算的值已经达到第二个流水线寄存器的输入

② 当时钟上升时,这些输入被加载到流水线寄存器中,成为寄存器的输出

③ 阶段 A 的输入被设置成发起指令 I3 的计算,然后信号传播通过各个阶段的组合逻辑

④ 在时刻360之前,结果值到达流水线寄存器的输入,当时刻360时钟上升时,各条指令会前进,经过一个流水线阶段

减缓时钟不会影响流水线的行为信号传播到流水线寄存器的输入,但是直到时钟上升时才会改变寄存器的状态

局限性

不一致的划分

运行时钟的速率是由最慢的阶段的,延迟限制的而不同阶段的延迟从50ps到150ps不等

处理器中的某些硬件单元如ALU和内存是不能被划分成多个延迟较小的单元的,这就导致创建一组平衡的阶段非常困难

流水线过深,收益反而下降

由于通过流水线的寄存器的延迟,设置过多的阶段,吞吐量并不会有相应的增加

五阶段流水线

为实现流水线化,更新pc阶段将在一个时钟周期开始时执行

在命名系统中大写的前缀 F, D, E, M, W 指的是流水线寄存器,小写的前缀 f, d, e, m, w 是指流水线的阶段

  • F 保存程序计数器PC预测值
  • D 位于取值和译码阶段之间,保存关于最新取出的指令的信息,即将由译码阶段进行处理
  • E 位于译码和执行阶段之间,保存关于最新译码的指令和从寄存器文件读出的值的信息,即将由执行阶段进行处理
  • M 位于执行和访存阶段之间,保存最新的指令执行结果,即将由访存阶段进行处理,还保存关于用于处理条件转移的分支条件和分支目标的信息
  • W 位于访存阶段和反馈路径之间,反馈路径将计算出来的值提供给计算器文件写,而当完成 ret 指令时,他还要向PC选择逻辑提供返回地址
代码 指令 1 2 3 4 5 6 7 8 9T
movq $1, %rax I1 F D E M W
movq $2, %rbx I2 F D E M W
movq $3, %rcx I3 F D E M W
movq $4, %rdx I4 F D E M W
halt I5 F D E M W
全流程图

∣ F ∣ → [ p r e d P C ] → S e l e c t P C ↗ ↘ [ 指令 内存 ] { s t a t i c o d e i f u n r A r B v a l C [ P C 增加 ] → v a l P v a l C , v a l P → < P r e d i c t P C > → [ p r e d P C ] ⏟ 取指 → ∣ D ∣ → [ 寄存器 文件 ] → ↙ v a l P [ S e l + F w d A ] → v a l A ⟶ [ F w d B ] → v a l B ⟶ s t a t , i c o d e , i f u n , v a l C ⟶ [ d s t E ] , [ d s t M ] ⟶ ⟨ d _ s r c A ⟩ , ⟨ d _ s r c B ⟩ → s r c A , s r c B → ⏟ 译码 → ∣ E ∣ → v a l C → v a l A → [ A L U A ] → v a l B → [ A L U B ] → [ A L U ] → [ C C ] → C n d ⟶ v a l E ⟶ s t a t , i c o d e , v a l A , d s t E , d s t M ⟶ [ S e l + F w d A ] [ F w d B ] ← v a l E ⏟ 执行 → ∣ M ∣ → v a l E → v a l A → [ A d d r ] v a l A ⟶ } ⟶ [ 数据 内存 ] ↑ ↓ M e m . c o n t r o l → v a l M ⟶ s t a t , i c o d e , v a l E , d s t E , d s t M ⟶ [ S e l + F w d A ] [ F w d B ] ← M _ v a l E , m _ v a l M ⟨ S e l e c t P C ⟩ ← M _ v a l A ⏟ 访存 → ∣ W ∣ → v a l E → [ W _ v a l E ] → v a l M → [ W _ v a l M ] → { [ 寄存器 文件 ] [ S e l + F w d A ] [ F w d B ] s t a t → [ s t a t ] ⏟ 写回 \begin{vmatrix}\\\\\\\\F\\\\\\\\\\\end{vmatrix}\to \underbrace{\begin{matrix} [ predPC ]\to Select PC\begin{matrix} \nearrow \\\searrow \end{matrix}\begin{matrix}\begin{bmatrix}指令\\内存\end{bmatrix}\left\{\begin{matrix}stat\\icode\\ifun\\rA\\rB\\valC\end{matrix}\right.\\\\\begin{bmatrix}PC\\增加\end{bmatrix}\to valP\\\\\end{matrix}\\valC,valP\to \left < PredictPC \right >\to \left [ predPC \right ]\\\\\end{matrix}}_{取指}\to \begin{vmatrix}\\\\\\\\D\\\\\\\\\\\end{vmatrix}\to \underbrace{\begin{matrix}\begin{bmatrix}\\\\寄存器\\文件 \\\\\\\end{bmatrix}\begin{matrix}\\\to \begin{matrix}\swarrow valP \\\begin{bmatrix}Sel+Fwd\\A\end{bmatrix}\end{matrix}\to valA \\\\\longrightarrow \begin{bmatrix}Fwd\\B\end{bmatrix}\to valB \\\\\end{matrix}\\\longrightarrow stat,icode,ifun,valC\longrightarrow \\\\\left [ dstE \right ] ,\left [ dstM \right ] \longrightarrow \\\left \langle d\_srcA \right \rangle ,\left \langle d\_srcB \right \rangle \to srcA,srcB\to \\\end{matrix}}_{译码}\to \begin{vmatrix}\\\\\\\\E\\\\\\\\\\\end{vmatrix}\to \underbrace{\begin{matrix}\\\begin{matrix}\\\\\begin{matrix}\begin{matrix}valC\to \\valA\to \end{matrix}\begin{bmatrix}ALU\\A\end{bmatrix}\to \\valB\to\begin{bmatrix}ALU\\B\end{bmatrix}\to \end{matrix}\begin{bmatrix}\\ALU\\\\\end{bmatrix}\begin{matrix}\to \begin{bmatrix}CC\end{bmatrix}\to Cnd\\\\\longrightarrow valE\end{matrix}\end{matrix}\\\\\longrightarrow stat,icode,valA,dstE,dstM\longrightarrow \\\\\left [ Sel+Fwd\ A \right ] \left [ Fwd B \right ] \gets valE\\ \\\end{matrix}}_{执行}\to \begin{vmatrix}\\\\\\\\M\\\\\\\\\\\end{vmatrix}\to \underbrace{\begin{matrix}\left.\begin{matrix}\\\begin{matrix}\begin{matrix}valE\to \\valA\to \end{matrix}\left [ Addr\right ] \\\\valA\longrightarrow \end{matrix}\end{matrix}\right\}\longrightarrow \begin{matrix}\begin{bmatrix}数据\\内存\end{bmatrix}\\\uparrow \downarrow \\Mem. control\end{matrix}\to valM\\\\\\\longrightarrow stat,icode,valE,dstE,dstM\longrightarrow \\\\\left [ Sel+Fwd\ A \right ] \left [ Fwd B \right ] \gets M\_valE,m\_valM\\ \left \langle SelectPC \right \rangle \gets M\_valA\\\\\end{matrix}}_{访存}\to \begin{vmatrix}\\\\\\\\W\\\\\\\\\\\end{vmatrix}\to \underbrace{\begin{matrix}\begin{matrix}\\\\\\valE\to \left [ W\_valE \right ] \to \\\\valM\to \left [ W\_valM \right ] \to \\\\\\\\\end{matrix}\left\{\begin{matrix}\begin{bmatrix}&寄存器&\\&文件&\\ \end{bmatrix}\\\\\begin{bmatrix}Sel+Fwd\\A \end{bmatrix}\begin{bmatrix}Fwd\\B \end{bmatrix}\\\end{matrix}\right.\\stat\to \left [ \quad stat\quad \right ] \\\\\\\end{matrix}}_{写回} ​F​ ​→取指 [predPC]→SelectPC↗↘​[指令内存​]⎩ ⎨ ⎧​staticodeifunrArBvalC​[PC增加​]→valP​valC,valP→⟨PredictPC⟩→[predPC]​​​→ ​D​ ​→译码 ​寄存器文件​ ​→↙valP[Sel+FwdA​]​→valA⟶[FwdB​]→valB​⟶stat,icode,ifun,valC⟶[dstE],[dstM]⟶⟨d_srcA⟩,⟨d_srcB⟩→srcA,srcB→​​​→ ​E​ ​→执行 valC→valA→​[ALUA​]→valB→[ALUB​]→​ ​ALU​ ​→[CC​]→Cnd⟶valE​​⟶stat,icode,valA,dstE,dstM⟶[Sel+Fwd A][FwdB]←valE​​​→ ​M​ ​→访存 valE→valA→​[Addr]valA⟶​​⎭ ⎬ ⎫​⟶[数据内存​]↑↓Mem.control​→valM⟶stat,icode,valE,dstE,dstM⟶[Sel+Fwd A][FwdB]←M_valE,m_valM⟨SelectPC⟩←M_valA​​​→ ​W​ ​→写回 valE→[W_valE]→valM→[W_valM]→​⎩ ⎨ ⎧​[​寄存器文件​​][Sel+FwdA​][FwdB​]​stat→[stat]​​​

PC预测

条件分支指令,指令通过执行阶段之后才能知道是否选择分支

ret指令,通过访存阶段才能确定返回地址

call和jmp(无条件转移),下一条指令地址就是指令中的常数字valC,对于其他指令就是valP

对于条件转移,预测选择了分支,则新PC是valC;若没选择分支,则新PC是valP

分支预测

  • 总是选择策略always taken,成功率60%
  • 从不选择策略never taken,NT,成功率40%
  • 反响选择、正向不选择backward taken, forward not-taken, BTFNT,成功率65%,即循环是由后向分支结束的,前向分支用于条件操作,循环通常会执行多次

栈的返回地址预测

高性能处理器在取址单元放入一个硬件栈,保存过程调用 指令产生的返回地址

流水线冒险

将流水线技术引入一个带反馈的系统,当相邻指令间存在相关时,会导致出现问题,这些相关有两种形式

  • 数据相关,下一条指令会用到这一条指令计算出的结果
  • 控制相关,一条指令要确定下一条指令的位置,例如执行跳转,调用或返回指令

冒险也可以分为两类,数据冒险控制冒险

解决方案

  • 暂停Stalling
  • 旁路Bypassing
  • 乱序执行out-of-order execution
暂停

暂停技术就是让一组指令阻塞在他们所处的阶段,而允许其他指令继续通过流水线

每次要把一条指令阻塞在译码阶段,就在执行阶段插入一个气泡,气泡就像一个自动产生的 nop 指令,不会改变寄存器、内存、条件码或程序状态

地址 指令 1 2 3 4 5 6 7 8 9 10 11
0x000 movq $10,%rdx F D E M W
0x00a movq $3,%rax F D E M W
bubble E M W
bubble | E M W
bubble | | E M W
0x014 addq %rdx,rax F D D D D E M W
0x016 halt F F F F D E M W
转发

将结果值直接从一个流水线阶段传到较早阶段的技术称为数据转发data forwarding,简称转发,有时称为旁路bypassing,它使得指令能通过流水线而不需要任何暂停

一共有五个不同的转发源以及两个不同的转发目的

转发源:

  • e_valE
  • m_valM
  • M_valE
  • W_valM
  • W_valE

转发目的:

  • valA
  • valB

1. W→D

在译码阶段,从寄存器文件读入源操作数,但是对这些源寄存器的写有可能在写回阶段才能进行,与其暂停直到写完成,不如简单地将写值传到流水线寄存器 E 作为原操作数

如下表指令实现了:

[ D _ v a l A ] [ D _ v a l B ] ⟵ W _ v a l E \begin{matrix}\left [ D\_valA \right ] \\\left [ D\_valB \right ]\end{matrix} \longleftarrow W\_valE [D_valA][D_valB]​⟵W_valE

地址 指令 1 2 3 4 5 6 7 8 9 10
0x000 movq $10,%rdx F D E M W
0x00a movq $3,%rax F D E M W
0x014 nop |
0x015 nop
0x016 addq %rdx,rax F D E M W
0x018 halt F D E M W

2. W,M→D

当访存阶段中,有对寄存器未进行的写时,也可以使用数据转发

如下表指令实现了:

[ D _ v a l A ] [ D _ v a l B ] ⟵ W _ v a l E M _ v a l E \begin{matrix}\left [ D\_valA \right ] \\\left [ D\_valB \right ]\end{matrix} \longleftarrow \begin{matrix}W\_valE\\M\_valE \end{matrix} [D_valA][D_valB]​⟵W_valEM_valE​

地址 指令 1 2 3 4 5 6 7 8 9
0x000 movq $10,%rdx F D E M W
0x00a movq $3,%rax F D E M W
0x014 nop
0x015 addq %rdx,rax F D E M W
0x017 halt F D E M W

3. M,E→D

为充分利用数据转发技术,还可以将新计算出来的值从执行阶段转到译码阶段

如下表指令实现了:

[ D _ v a l A ] [ D _ v a l B ] ⟵ M _ v a l E e _ v a l E \begin{matrix}\left [ D\_valA \right ] \\\left [ D\_valB \right ]\end{matrix} \longleftarrow \begin{matrix}M\_valE\\e\_valE \end{matrix} [D_valA][D_valB]​⟵M_valEe_valE​

地址 指令 1 2 3 4 5 6 7 8
0x000 movq $10,%rdx F D E |M| W
0x00a movq $3,%rax F D |E| M W
0x014 addq %rdx,rax F |D| E M W
0x016 halt F D E M W
加载互锁

有一类数据冒险不能单纯用转发来解决,因为内存读在流水线发生的比较晚。加载使用冒险其中一条指令从内存中读出寄存器的值,而下一条指令需要该值作为源操作数

可以将暂停和转发结合起来避免加载使用数据冒险

地址 指令 1 2 3 4 5 6 7 8 9 10 11 12
0x000 movq $128,%rdx F D E M W
0x00a movq $3,%rcx F D E M W
0x014 movq %rcx,0(%rdx) F D E M W
0x01e movq $10,%rbx F D E M W
0x028 movq 0(%rdx),%rax F D E M W
bubble E M W
0x032 addq %rbx,%rax F D E M W
0x034 halt F D E M W

这种使用暂停来处理,加载使用冒险的方法称为加载互锁 load interlock,加载互锁和转发技术结合起来,足以处理所有可能类型的数据冒险

只有加载互锁会降低流水线的吞吐量

避免控制冒险

当处理器无法根据处于取指阶段的当前指令来确定下一条指令的地址时,就会出现控制冒险

控制冒险只会发生在ret指令和跳转指令,而跳转指令只有在条件跳转方向预测错误时,才会造成麻烦

1. ret 指令处理

当 ret 指令经过译码执行和访存阶段时,流水线应该暂停,在处理过程中插入三个气泡,一旦 ret 指令到达写回阶段,PC选择逻辑就会选择返回地址作为指令的取指地址,然后取指阶段就会取出位于返回点处的 movq 指令

地址 指令 1 2 3 4 5 6 7 8 9 10 11
0x000 movq Stack,%rsp F D E M W
0x00a call proc F D E M W
0x020 ret F D E M W
bubble F D E M W
bubble F D E M W
bubble F D E M W
0x013 movq $10,%rdx F D E M W

2. 预测错误的分支指令处理

暂停和向流水线中插入气泡的技术,可以动态调整流水线的流程

如下表,指令按照他们进入流水线的顺序列出,而不是按照他们出现在程序中的顺序,因为跳转指令会选择分支

  • 周期3 中会取出位于跳转目标处的指令,而周期4 会取出该指令后的那条指令
  • 周期4 分支逻辑发现不应该选择分支之前已经取出了两条指令,这两条指令不该继续执行
  • 此时,两条错误指令并未到执行阶段,因此没有发生程序员可见的状态改变(如执行阶段指令会改变条件码)
  • 因此,需要在下个周期向译码和执行阶段插入气泡,并同时取出跳转后面的那条指令,这样可以取消两条预测错误的指令,有时也称为指令排除 instruction squashing
地址 指令 1 2 3 4 5 6 7 8 9 10
0x000 xorq %rax,%rax F D E M W
0x002 jne target # Not taken F D E M W
0x016 movl $2,%rdx # Target F D
bubble E M W
0x020 movl $3,%rbx # Target+1 F
bubble D E M W
0x00b movq $1,%rax # Fall through F D E M W
0x015 halt F D E M W

性能分析

CPI,即每指令周期数Cycles Per Instruction

这种衡量值是流水线平均吞吐量的倒数,不过时间单位是时钟周期

符号 英文 中文 解释
C P I CPI CPI Cycles Per Instruction 每指令周期数 流水线平均吞吐量的倒数,单位时钟周期
C i C_i Ci​ 执行指令数
C b C_b Cb​ 插入气泡数
l p lp lp load penalty 加载处罚 加载使用冒险造成暂停时插入气泡的平均数
m p mp mp mispredicted branch penalty 预测错误分支处罚 预测错误,取消指令时插入气泡的平均数
r p rp rp return penalty 返回处罚 RET指令造成暂停时插入气泡的平均数
CPI 计算

C P I = C i + C b C i = 1.0 + C b C i CPI=\frac{C_i+C_b}{C_i}=1.0+\frac{C_b}{C_i} CPI=Ci​Ci​+Cb​​=1.0+Ci​Cb​​

C P I = 1.0 + l p + m p + r p CPI=1.0+lp+mp+rp CPI=1.0+lp+mp+rp

例:用以下这组频率计算CPI

原因 名称 指令频率 条件频率 气泡 乘积
加载/使用 l p lp lp 0.25 0.20 1 0.05
预测错误 m p mp mp 0.20 0.40 2 0.16
返回 r p rp rp 0.02 1.00 3 0.06
总处罚 CPI
0.27 1.27

链接

程序的诞生

编译器驱动程序

语言预处理器,编译器,汇编器,链接器

过程

  1. 预处理

    C预处理器(cpp)将C的源程序main.c翻译成一个ASCII码的中间文件main.i

cpp [other arguments] main.c main.i
  1. 编译

    C编译器(ccl)将main.i翻译成一个ASCII汇编语言文件main.s

 ccl main.i -Og [other arguments] -o main.s
  1. 汇编

    汇编器(as)将main.s翻译成一个可重定位目标文件 (relocatable object file)main.o

    as [other arguments] -o main.o main.s
  1. 链接

    运行链接器程序ld,将main.o和其他可重定位目标文件(例如other.o)以及一些必要的系统目标文件组合起来,创建一个可执行目标文件 (executable object file)

    ld -o prog [system object files and args] main.o other.o
  1. 运行
    linux> ./prog

静态链接

符号解析

目标文件定义和引用符号,每个符号对应一个函数,全局变量,静态变量

符号解析的目的是将每个符号引用和一个符号定义关联起来

重定位

编译器和汇编器生成的代码和数据节地址从0开始,链接器重定位这些节,就是把每个符号定义和一个内存位置关联起来,然后修改所有对这些符号的引用,使得他们指向这个内存位置

  1. 静态变量就是C语言中用 static 属性声明的变量,只在本文件中起作用

  2. 函数的局部变量不被考虑,它们只存在于栈中

目标文件

  1. 可重定位目标文件

    可在编译时与其他可重定位目标文件合并,创建一个可执行目标文件

  2. 可执行目标文件

    可直接被复制到内存并执行

  3. 共享目标文件

    可以在加载或者运行时被动态的加载进内存并链接

可重定位目标文件
介绍
ELF头 用于描述系统字的大小和字节顺序的16字节,以及帮助链接器语法分析和解释目标文件的信息
.text 已编译程序的机器代码
.rodata read only只读数据,如printf格式串和跳转表
.data 已初始化的全局变量和静态C变量
.bss Block Storage Start未初始化的和被初始化为0的全局变量和静态C变量,可记忆为Better Save Space
.symtab 符号表,存放程序中定义和引用的函数和全局变量信息
.rel.text .text节中位置的列表,存放需要在可执行目标文件中被修改的指令地址信息
.rel.data 被模块引用的全局变量的重定位信息,在合并后的可执行文件中需要修改的数据所在的地址
.debug 调试符号表,-g生成,包含:局部变量和类型定义,程序中定义和引用的全局变量,原始C文件
.line C源程序行号和.text机器指令的映射,-g生成
.strtab 字符串表,就是以null结尾的字符串序列,包括.symtab.debug节中的符号表,以及节头部中的节名字
节头部表 描述不同节的位置和大小
可执行目标文件
介绍 读写
ELF头 描述文件的总体格式,以及程序的入口点entry point 只读
段头部表 将连续的文件节映射到运行时内存段 只读
.init 一个叫_init的小函数,程序的初始化代码会调用 只读
.text 同上,机器代码 只读
.rodata 同上,只读数据 只读
.data 同上,初始化数据 读/写
.bss 同上,未初始化及赋0 读/写
.symtab 同上 不加载
.debug 同上 不加载
.line 同上 不加载
.strtab 同上 不加载
节头部表 描述目标文件的节 不加载

注:可执行目标文件不再需要.rel节,因为可执行文件是完全链接的,即已被重定位的

Linux x86-64 运行时内存映像

内存地址 内存映像 备注
2 48 − 1 2^{48}-1 248−1 内核内存 ↑ \uparrow ↑对用户代码不可见的内存
↑ \uparrow ↑ 用户栈(运行时创建) ↙ % r s p \swarrow \quad \%rsp ↙%rsp
↓ ↑ \begin{matrix} \downarrow \\ \uparrow \end{matrix} ↓↑​
共享库的内存映射区域
↑ \begin{matrix} \ \\ \uparrow \end{matrix}  ↑​
运行时堆(malloc) ↖ b r k \nwarrow \quad brk ↖brk
↑ \uparrow ↑ 读\写段(.data, .bss) 从可执行文件中加载
0x400000 只读代码段(.init, .text, .rodata) 从可执行文件中加载
0

符号解析

链接器解析符号引用的方法是将每个引用与它输入的可重定位目标文件的符号表中的一个确定的符号定义关联起来

全局符号

  • 由模块m定义的并能被其他模块所引用

  • 例如:非static的C函数和非static的全局变量

外部符号

  • 由其他模块定义并被模块m中引用的全局符号

局部符号

  • 只能被模块m定义和引用的符号

  • 局部符号不是局部变量

  • 例如:使用static关键字修饰的C函数和变量

局部非static的C变量:存储在栈中

局部static的C变量:存储在.bss或.data中

int f() {static int x = 0;return x;
}
int g() {static int x = 1;return x;
}
  • 编译器为每个x在:data节中分配空问

  • 使用唯一的名称在符号表中创建局部符号,例如:x.1和x.2

解析多重定义

强符号

函数和已初始化的全局变量

弱符号

未初始化的全局变量

规则

  1. 不允许有多个重名的强符号
  2. 如果有一个强符号和多个弱符号同名,对弱符号的引用解析为强符号
  3. 如果多个弱符号同名,任意选择之一

例如

  • 链接错误:两个同名强符号 (p1)
//file1.cint x;p1 () {}//file2.cp1 () {}
  • 引用x将指向相同的未初始化int型变量
//file1.cint x;p1 () {}//file2.cint x;p2 () {}
  • 在p2中向x中写入值,可能会覆盖y
//file1.cint x; int y;p1 () {}//file2.cdouble x;p2 () {}
  • 在p2中向x中写入值将会覆盖y
//file1.cint x = 7;int y = 5;p1 () {}//file2.cdouble x;p2 () {}
  • 对x的引用都指向相同的已初始化变量
//file1.cint x = 7; p1 () {}//file2.cint x;p2() {}

噩梦场景:两个相同的弱符号结构体,由不同的编译器使用不同的对齐规则进行编译

重定位

链接器一旦完成了符号解析,就将代码中的每个符号引用和正好一个符号定义关联起来,此时就可以开始重定位步骤了。重定位主要有两步:

  1. 重定位节和符号定义

链接器将所有同类型的节合并为同一类型的新的聚合节,然后链接器将运行时内存地址赋值给新的聚合节、输入模块定义的每个节、输入模块定义的每个符号

这一步完成后,程序中每条指令和全局变量都有唯一的运行时内存地址了

  1. 重定位节中的符号引用

链接器修改代码节和数据节中对每个符号的引用,使他们指向正确的运行时地址

执行这一步,链接器依赖可重定位目标模块中称为重定位条目relocation entry的数据结构

代码的重定位条目放在.rel.text中,已初始化数据的重定位条目放置在.rel.data

typedef struct {long offset;long type:32,symbol:32;long addend;
} Elf64_Rela;

offset 需要被修改的引用的节偏移
symbol 被修改的引用应该指向的符号
type 告知连接器如何修改新的引用,ELF定义了32种,其中最基本两种如下:

  1. R_X86_64_PC32: 重定位一个使用32位PC相对地址的引用
  2. R_X86_64_32: 重定位一个使用32位绝对地址的引用

addend 有符号常数,对被修改引用的值做偏移调整

动态链接共享库

KaTeX parse error: Undefined control sequence: \hfill at position 49: …}\begin{matrix}\̲h̲f̲i̲l̲l̲ ̲main.c\longrigh…

库打桩技术

库打桩

library interpositioning 允许截获对共享库函数的调用,取而代之执行自己的代码

使用打桩机制,可以追踪某个特殊库函数的调用次数,验证和追踪它的输入输出值,甚至替换成一个完全不同的实现

打桩可以发生在编译时链接时、或当程序被加载和执行的运行时

基本思想

  1. 给定一个需要打桩的目标函数
  2. 创建一个包装函数,它的原型与目标函数完全一样
  3. 使用打桩机制,欺骗系统调用包装函数而非目标函数

异常控制流

控制转移

假设程序计数器按照 a 0 , a 1 , … , a n − 1 a_0, a_1, \dots,a_{n-1} a0​,a1​,…,an−1​,每次从 a k a_k ak​到 a k + 1 a_{k+1} ak+1​的过渡称为控制转移control transfer

控制流

这样的控制转移序列成为处理器的控制流flow of control control flow

异常控制流 ECF

现代系统通过使控制流发生突变来对系统状态的变化做出反应,一般把这种突变称为异常控制流Exceptional Control Flow

  1. 硬件层,硬件检测到的事件会触发控制,突然转移到异常处理程序
  2. 操作系统层,内核通过上下文切换,将控制从一个用户进程转移到另一个用户进程
  3. 应用层,一个进程可以发送信号到另一个进程,而接收者会将控制突然转移到它的一个信号处理程序,一个程序可以通过回避通常的栈规则,并执行到其他函数中任意位置的非本地跳转来对错误做出反应

异常

exception异常是控制流中的突变,用来响应处理器状态中的某些变化,一部分由硬件实现,一部分由操作系统实现

事件

event处理器状态变化

假设事件发生时的指令是 I c u r r I_{curr} Icurr​,下一条指令是 I n e x t I_{next} Inext​

异常号

exception number系统中可能的每种类型的异常都被分配了一个唯一的非负整数的异常号

1. 处理器的设计者分配的

  • 零除
  • 缺页
  • 内存访问违例
  • 断点
  • 算术运算溢出

2. 操作系统内核设计者分配的

  • 系统调用
  • 来自外部的 I/O 设备的信号

异常表

序号 内容
0 处理异常程序 0 的代码
1 处理异常程序 1 的代码
2 处理异常程序 2 的代码
n − 1 n-1 n−1 处理异常程序 n-1 的代码

操作系统启动时,操作系统分配和初始化一张称为异常表的跳转表

表目 k k k 包含异常 k k k 的处理程序的地址,运行时处理器检测到了发生一个事件,并确定了相应的异常号 k k k ,随后处理器触发异常方法是执行间接的过程调用,通过异常表的表目 k k k 转到相应的处理程序,异常表的起始地址放在异常表基址寄存器 exception table base register的特殊CPU寄存器里

当处理器检测到有事件发生时,会通过异常表进行一个间接过程,调用到专门设计用来处理这类事件的操作系统子程序,异常处理程序完成处理后,根据引起异常的事件类型选择:

  • 处理程序将控制返回给当前指令 I c u r r I_{curr} Icurr​,即当前事件发生时正在执行的指令
  • 处理程序将控制返回给 I n e x t I_{next} Inext​,如果没有发生异常,将继续执行下一条指令
  • 处理程序终止被中断的程序

异常类型

类别 原因 异步/同步 返回行为
中断 来自的 I/O 设备的信号 异步 总是返回到下一条指令
陷阱 有意的异常 同步 总是返回到下一条指令
故障 潜在可恢复的错误 同步 可能返回到当前指令
终止 不可恢复的错误 同步 不会返回

异步异常

异步异常是由处理器外部的I/O设备中的事件产生的

同步异常

同步异常是执行的一条指令的直接产物

同步中断是执行当前指令的结果,把这类指令称作故障指令

中断

中断是异步发生的,是来自处理器外部的 I/O 设备的信号的结果,硬件中断不是任何一条专门的指令造成的,硬件中断的异常处理程序常常称作中断处理程序 interrupt handler

I/O设备通过向处理器芯片上的一个引脚发信号,并将异常号放在系统总线上来触发中断,这个异常号标识了引起中断的设备

陷阱和系统调用

陷阱是有意的异常,是执行一条指令的结果,陷阱处理程序将控制返回到下一条指令。陷阱最重要的用途是在用户程序和内核之间提供一个像过程一样的接口,叫做系统调用

用户程序向内核请求服务,例如:

  • 读文件 read
  • 创建新进程 fork
  • 加载新程序 execve
  • 终止当前进程 exit

处理器提供一个特殊的 syscall n 指令,用户程序想要请求服务 n 时执行这条指令,会导致一个到异常处理程序的陷阱,该程序解析参数并调用适当的内核程序

普通的函数运行在用户模式中,限制了函数可以执行的指令类型,只能访问与调用函数相同的站系统,调用运行在内核模式中,允许系统调用执行特权指令并访问定义在内核中的栈

故障

故障由错误情况引起,可能能够被故障处理程序修正

如果故障处理程序能够修正,错误情况就将控制返回到引起故障的指令,从而重新执行它

否则,处理程序返回到内核中的 abort 例程,终止引起故障的应用程序

例如缺页异常,当指令引用一个虚拟地址,而其物理页面不在内存中,因此必须从磁盘中取出时就会发生缺页故障

终止

终止是不可恢复的致命错误造成的结果,通常是一些硬件错误

例如 DRAM 或者 SRAM 位被损坏时发生的奇偶错误,处理程序将控制返回给一个 abort 例程,该例程会终止这个应用程序

x86-64系统中异常示例

异常号 描述 异常类别
0 除法错误 故障
13 一般保护故障 故障
14 缺页 故障
18 机器检查 终止
32~255 操作系统定义的异常 中断或陷阱

进程

进程的经典定义是一个执行中的程序实例

异常是允许操作系统内核提供进程概念的基本构造块

  • 一个独立的逻辑控制流,提供一种程序独占使用处理器的假象,

  • 一个私有的地址空间,提供程序独占内存系统的假象

上下文 context

系统中的每个程序都运行在某个进程的上下文中,上下文是由程序正确运行所需的状态组成的,包括:

  • 存放在内存中的程序和代码数据
  • 通用目的寄存器的内容
  • 程序计数器
  • 环境变量
  • 打开文件描述符的集合

逻辑控制流

进程为每个程序提供了一种假象,好像程序在独占的使用处理器

每个程序的程序计数器PC值的序列叫做逻辑控制流,简称逻辑流

进程轮流使用处理器,每个进程执行它的流的一部分,然后被抢占 preempted ,暂时挂起,轮到其他进程

并发流

一个逻辑流的执行在时间上,与另一个流重叠,称为并发流 concurrent flow ,这两个流被称为并发的运行,更准确的说是流X与Y互相并发

多个流并发的执行的一般现象被称为并发 concurrency

一个进程和其他进程轮流运行的概念,称为多任务 multitasking

一个进程执行它的控制流的一部分的每一段时间叫做时间片 time slice ,因此多任务也叫做时间分片 time slicing

并行流

并发流可以运行在同一个处理器上,如果两个流并发的运行在不同的处理器或者计算机上,则称之为并行流 parallel flow ,他们并行的运行,且并行的执行

用户模式和内核模式

处理器通常使用某个控制寄存器中的一个模式位,来限制一个应用可以执行的指令及它可访问的地址空间范围

该寄存器描述了进程当前享有的特权,当设置了模式位时,进程就运行在内核模式中,有时叫超级用户模式

没有设置模式位时,进程就运行在用户模式中

上下文切换

操作系统内核使用一种称为上下文切换 context switch 的较高层形式的异常控制流来实现多任务

内核为每一个进程维持一个上下文 context ,上下文就是内核重新启动一个被抢占的进程所需的状态,由一些对象的值组成:

  • 通用目的寄存器
  • 浮点寄存器
  • 程序计数器
  • 用户栈
  • 状态寄存器
  • 内核栈
  • 各种内核数据结构
    • 页表
    • 进程表
    • 包含进程已打开文件的信息的文件表

内核可以决定抢占当前进程并重新开始一个先前被抢占了的进程,这种决策就叫做调度 scheduling ,是由内核中称为调度器 scheduler 的代码处理的

调度器使用上下文切换机制来将控制转移到新的进程:

  • 保存当前进程的上下文
  • 恢复某个先前被抢占的进程被保存的上下文
  • 将控制传递给这个新恢复的进程

引发上下文切换的原因:

  • 系统调用,因为等待某个事件发生而阻塞,如read系统调用需要访问磁盘
  • sleep
  • 系统调用
  • 中断
进程A 进程B 运行模式
↓ \downarrow ↓ 用户模式
↘ \searrow ↘ 内核模式 上下文切换
↓ \downarrow ↓ 用户模式
↙ \swarrow ↙ 内核模式 上下文切换
↓ \downarrow ↓ 用户模式

存储器

存储技术

随机访问存储器

RAM Random-Access Memory

​ SRAM 静态RAM,每个位存储在双稳态存储器单元里,六个晶体管电路

​ 应用在高速缓存存储器

​ DRAM 动态RAM,每个位存储为对一个电容的充电,对干扰敏感,周期性刷新

​ 应用在主存,帧缓冲区

传统DRAM

DRAM中的位被分成 d d d 个超单元,每个超单元由 w w w 个 DRAM 单元组成, d × w d \times w d×w 的 DRAM 存储了 d w dw dw 位信息。

超单元被组织成了 r 行 c 列的阵列, r c = d rc = d rc=d ,每个超单元有坐标 ( i , j ) (i, j) (i,j)

内存控制电路一次可以传送 w w w 位,为读取坐标 ( i , j ) (i, j) (i,j),内存请求坐标 ( i , j ) (i, j) (i,j), i i i 和 j j j,行地址 i i i 称为RAS请求Row Access Strobe,列地址 j j j 称为CAS请求Column Access Strobe,两者共享 DRAM 地址引脚

DRAM 内有一个内部行缓冲区,内存控制器先发出行坐标,DRAM 将整行超单元复制到内部行缓冲区,内存控制器再发出列坐标,DRAM 再从行缓冲区复制出超单元中的 w w w 位

多个 DRAM并联读取数据,

非易失性存储器

ROM Read-Only Memory

PROM Programmable ROM

EPROM Erasable Programmable ROM

EEPROM Electrically Erasable PROM

SSD Silid State Disk

磁盘存储

盘片platter,面surface,磁道track,扇区sector,间隙gap,柱面cylinder

寻道时间 seek time

读/写头定位到包含目标位置的扇区磁道上,移动传动臂时间

T s e e k T_{seek} Tseek​

旋转时间 rotational latency

目标扇区第一个位旋转到读/写头下

T m a x r o t a t i o n = 1 R P M × 60 s 1 m i n T_{max\ rotation} = \frac{1}{RPM} \times \frac{60s}{1min} Tmax rotation​=RPM1​×1min60s​

T a v g r o t a t i o n = 1 2 × T m a x r o t a t i o n T_{avg\ rotation} = \frac{1}{2} \times T_{max\ rotation} Tavg rotation​=21​×Tmax rotation​

传送时间 transfer time

依赖于旋转速度和每条磁道的扇区数目

T a v g t r a n s f e r = 1 R P M × 1 a v g s e c t o r p e r t r a c k × 60 s 1 m i n T_{avg\ transfer} = \frac{1}{RPM} \times \frac{1}{avg\ sector\ per\ track} \times \frac{60s}{1min} Tavg transfer​=RPM1​×avg sector per track1​×1min60s​

访问时间 access time

T a c c e s s = T a v g s e e k + T a v g r o t a t i o n + T a v g t r a n s f e r T_{access} = T_{avg\ seek} + T_{avg\ rotation} + T_{avg\ transfer} Taccess​=Tavg seek​+Tavg rotation​+Tavg transfer​

直接内存访问

DMA Direct Memory Access

磁盘控制器收到来自CPU的读命令后,将逻辑块号翻译成扇区地址,读扇区内容后直接传给主存,不需要CPU干涉,称为DMA传送DMA transfer

DMA传送完成后,磁盘控制器给CPU发送中断信号

局部性

局部性原理

程序倾向于引用邻近于其他最近引用过的数据项,或最近引用过的数据项本身

时间局部性:同一内存位置较短时间内多次引用

空间局部性:被引用内存位置的附近短时间内被引用

顺序引用模式:步长为1的引用模式(相对于元素大小)

另外,步长为k的引用模式每隔k个元素进行访问

取指令的局部性

循环的时间和空间局部性更好,循环体越小,迭代次数越多,局部性越好

存储器层次结构

层次 存储器 数据来源
L0 寄存器 L1→CPU
L1 高速缓存 L1(SRAM) L2→L1
L2 高速缓存 L2(SRAM) L3→L2
L3 高速缓存 L3(SRAM) L4→L3
L4 主存 (DRAM) L5→L4
L5 本地二级存储 L6→L5
L6 远程二级存储 L6

缓存

高速缓存 cache 读作cash,使用高速缓存的过程称为缓存 caching 读作cashing

第 k + 1 k+1 k+1 层存储器被划分成连续的数据对象组块chunk ,称为块block,地址或名字唯一

第 k k k 层被划分成较少的块的集合,每个块大小与第 k + 1 k+1 k+1 层相同,包含第 k + 1 k+1 k+1 层块的一个子集的副本

缓存命中

程序需要第 k + 1 k+1 k+1 层的数据 d d d ,要先在第 k k k 层寻找,找到了就是缓存命中(cache hit)

缓存不命中

第 k k k 层缓存从第 k + 1 k+1 k+1 层取出 d d d ,若第 k k k 层满了就覆盖一个现存块,即 “替换” 或 “驱逐” ,被替换的叫 “牺牲块”,决定的叫 “替换策略”

缓存不命中种类

  1. 强制性不命中(冷不命中):第 k k k 层缓存为空

  2. 冲突不命中:第 k + 1 k+1 k+1 层的某块限制放置在第 k k k 层的部分块中,这种限制性的放置策略导致的不命中

  3. 容量不命中:工作集大小超过缓存大小(缓存太小了)

数据与指令

只保存指令的高速缓存: i − c a c h e i-cache i−cache

只保存数据的高速缓存: d − c a c h e d-cache d−cache

指令和数据都保存的:统一的高速缓存(unified cache)

i-cache通常是只读的,因此较简单

例如某多核处理器,每个核有私有的L1 i-cache,L1 d-cache,L2统一的高速缓存;所有核共享L3统一的高速缓存;所有的SRAM高速缓存存储器都在CPU芯片上

性能

不命中率

m i s s r a t e = 不命中数量 引用数量 \ miss\ rate =\frac{不命中数量}{引用数量}  miss rate=引用数量不命中数量​

命中率

h i t r a t e = 1 − 不命中率 \ hit\ rate=1-不命中率  hit rate=1−不命中率

命中时间 h i t t i m e \ hit\ time  hit time

对L1来说数量级是几个时钟周期

不命中处罚 m i s s p e n a l t y \ miss\ penalty  miss penalty

通常L1不命中后,从L2中得到服务处罚10个时钟周期,从L3是50个周期,从主存是200个周期

平均内存访问时间

( 1 − 未命中率 ) × 命中时间 + 未命中率 × 未命中惩罚 (1 - 未命中率) \times 命中时间 + 未命中率 \times 未命中惩罚 (1−未命中率)×命中时间+未命中率×未命中惩罚

缓存友好的代码

可以让程序在常见的场景下更快的运行

  • 关注核心函数的内部循环

减少内部循环中的缓存末命中

  • 对变量更好的重复利用(时间局部性)

  • 尽量使用步长为1的模式访问存储器(空间局部性)

核心思想:通过理解高速缓存的工作机制,量化我们对局部性 原理的定性认知

高速缓存存储器

设每个存储器地址有 m m m 位,形成 M = 2 m M=2^m M=2m 个不同的地址

高速缓存,被组织成有 S = 2 s S=2^s S=2s 个高速缓存组的数组

每个组包含 E E E 个高速缓存行

每行由一个 B = 2 b B=2^b B=2b 字节的数据块组成

现代处理器(如Core i7)高数缓存块包含64个字节,L1和L2是8路组相联,L3是16路组相联的

高速缓存大小

C 数据字节,(S, E, B, m) 的通用组织

C = B × E × S C=B \times E \times S C=B×E×S

S = 2 s S=2^s S=2s组 组号 1个有效位 t个标记位 高速缓存块 B = 2 b B=2^b B=2b 字节 每组E行
组0 有效 标记 0, 1, 2, … , B-1 0 0 0
有效 标记 0, 1, 2, … , B-1 ~
组0 有效 标记 0, 1, 2, … , B-1 E − 1 E-1 E−1
组1 有效 标记 0, 1, 2, … , B-1 0 0 0
有效 标记 0, 1, 2, … , B-1 ~
组1 有效 标记 0, 1, 2, … , B-1 E − 1 E-1 E−1
组S-1 有效 标记 0, 1, 2, … , B-1 0 0 0
有效 标记 0, 1, 2, … , B-1 ~
组S-1 有效 标记 0, 1, 2, … , B-1 E − 1 E-1 E−1

地址 ( m m m位)

标记 组索引 块偏移
( m − 1 ) (m-1) (m−1) ~ ( s + b ) (s+b) (s+b) ( s + b − 1 ) (s+b-1) (s+b−1) ~ b b b ( b − 1 ) (b-1) (b−1) ~ 0 0 0
t t t 位 s s s 位 b b b 位

符号及参数

参数 描述
S = 2 s S=2^s S=2s 组数
E E E 每个组的行数
B = 2 b B=2^b B=2b 块大小(字节)
m = l o g 2 ( M ) m=log_2(M) m=log2​(M) 主存物理地址位数
M = 2 m M=2^m M=2m 内存地址最大数量
s = l o g 2 ( S ) s=log_2(S) s=log2​(S) 组索引位数量
b = l o g 2 ( B ) b=log_2(B) b=log2​(B) 块偏移位数量
t = m − ( s + b ) t=m-(s+b) t=m−(s+b) 标记位数量
C = B × E × S C=B \times E \times S C=B×E×S 高速缓存有效大小

直接映射高速缓存

直接映射高速缓存,每个组只有一行,即 E = 1 E=1 E=1 的高速缓存

步骤

  1. 组选择

    从字 w w w 的地址中抽取 s s s 个组索引位,解释成对应组号的无符号整数,映射为高速缓存中对应的组

  2. 行匹配

    假设已经选择了组 i i i,当该行设置了有效位,且高速缓存行中标记与地址中标记匹配,则 w w w 就在此行中,则缓存命中;否则缓存不命中

  3. 字抽取

    块偏移位:所需要字节的第一个字节的偏移

0 1 2 3 4 5 6 7
w 0 w_0 w0​ w 1 w_1 w1​ w 2 w_2 w2​ w 3 w_3 w3​

则上图示例中(假设字长为4字节),块偏移为 10 0 2 100_2 1002​

运行过程

地址被划分成三个部分

标记位 索引位 偏移位

索引位用来确定缓存中的组号,地址中间的一段而非前几位作为索引位,这样连续的一段内存会映射到不同的组,从而调用连续的一段内存时缓存会频繁不命中

标记位用来在同一个组中唯一的确定一个块,映射到这个组的内存块有相同的索引位,但标记位不同,这样就能唯一的确定出一个块

偏移位用来确定已经存入缓存的一个块中,需要的数据具体的位置,该字在块中的偏移量

运行步骤:

  1. 读取地址,分成标记位,索引位,偏移位
  2. 在高速缓存中查找组,索引位就是组号偏移量
  3. 查看有效位和标记位,有效位1,标记位相同则命中
  4. 缓存不命中则先加载,缓存命中则直接读取
  5. 返回偏移位对应偏移量的数据

抖动

高速缓存反复地加载和驱逐相同的高速缓存块的组

程序访问大小为2的幂的数组时,两个数组映射在相同的组中,直接映射高速缓存中通常会发生冲突不命中

组相联高速缓存

组相联高速缓存,每组都保存有多于一个的高速缓存行

一个 1 < E < C B 1 < E < \frac{C}{B} 1<E<BC​ 的高速缓存通常称为 E E E路组相联高速缓存

例如下图2路组相联高速缓存

组号 有效位 标记位 数据
组0 有效 标记 高速缓存块
组0 有效 标记 高速缓存块
组1 有效 标记 高速缓存块
组1 有效 标记 高速缓存块
组S-1 有效 标记 高速缓存块
组S-1 有效 标记 高速缓存块

步骤

  1. 组选择

    与直接映射高速缓存的组选择一样,组索引位标识组

  2. 行匹配和字选择

    需检查多个行的标记位和有效位

    可以理解为一个 (key, value) 对的数组,key是标记和有效位,value是块的内容

    组中的任何一行都可以包含任何映射到这个组的内存块,高速缓存必须搜索组中的每一行,寻找有效行,其标记与地址中的标记相同,找到则命中

    字选择与直接映射高速缓存相同

行替换

最不常试用策略LFU Least-Frequently-Used

最近最少使用策略LRU Least-Recently-Used

全相联高速缓存

全相联高速缓存是由一个包含了所有高速缓行的组所组成 E = C B E=\frac{C}{B} E=BC​

组号 有效位 标记位 数据
组0 有效 标记 高速缓存块
组0 有效 标记 高速缓存块
组0 有效 标记 高速缓存块

E = E = E= 唯一的一组中有 E = C B E=\frac{C}{B} E=BC​行

标记 块偏移
r r r位 b b b位

地址中没有组索引位,只被划分成了一个标记位和一个块偏移,默认总是选择组0

行匹配和字选择与组相联高速缓存一样

全相联高速缓存成本高,只适合做小的高速缓存,如:

虚拟内存系统中的翻译备用缓冲器 TLB,用于缓存页表项

写数据

假设需要写一个已经缓存了的字 w w w (写命中write hit)

直写

立即将 w w w 的高速缓存块写回到紧接着的低一层中

写回

尽可能的推迟更新,只有当替换算法要驱逐这个更新过的块时才把它写到紧接着的低一层中

由于局部性,写回能显著减少总线流量,缺点是增加了复杂性,需要为每个高速缓存行维护一个额外的修改位dirty bit

另一个问题是处理写不命中

写分配

write-allocate,先加载低一层后更新,每次不命中都会导致块的传送

非写分配

not-write-allocate,避开高速缓存,直接把这个字写到低一层中

直写高速缓存通常是非写分配的,写回高速缓存通常是写分配的(局部性)

虚拟内存

地址空间和寻址

虚拟寻址 virtual addressing

CPU通过内存管理单元 MMU 将生成的虚拟地址 VA 转换为物理地址 PA

优势:
更加有效的利用主存
简化内存管理
独立的地址空间

地址空间:非负整数地址的有序集合

虚拟地址空间

CPU从一个有 $ N=2^{n}$ 个地址的地址空间中生成虚拟空间
{ 0 , 1 , 2 , … , N − 1 } \{0, 1, 2, \dots, N-1\} {0,1,2,…,N−1}

物理地址空间

对应系统中物理内存的M个字节,M不要求是2的幂,但假设 M = 2 m M=2^{m} M=2m
{ 0 , 1 , 2 , … , M − 1 } \{0, 1, 2, \dots, M-1\} {0,1,2,…,M−1}

通常,SRAM高速缓存使用物理地址进行访问

虚拟页Virtual Page

VP 虚拟页,VM系统将虚拟内存分割,每个虚拟页大小 P = 2 p P=2^{p} P=2p 字节

物理页Physical Page

PP 物理页,大小也是P字节 P = 2 p P=2^{p} P=2p ,物理页也被称为页帧page frame

使用SRAM表示L1, L2, L3高速缓存
使用DRAM表示虚拟内存系统的缓存,在主存中缓存虚拟页

DRAM的未命中要由磁盘服务,因此未命中惩罚大:

  1. 虚拟页很大(4KB~2MB)
  2. DRAM缓存是全相联的,即任何虚拟页都可以放置在任何的物理页中
  3. 复杂精密的替换算法
  4. DRAM使用回写,而非直写

页表

页表是一个PTE页表项(页表条目)的数组,用于建立虚拟页到物理页的映射,常驻内存

每个PTE由一个有效位和一个n位地址字段组成(物理页号或磁盘地址)

由于DRAM缓存是全相联的,所以任意物理页都可以包含任意虚拟页

操作系统为每个进程提供了一个独立的页表,也就是一个独立的虚拟地址空间,多个虚拟页面可以映射到同一个共享物理页面上

有效位

valid表明该虚拟页当前是否缓存在DRAM中

虚拟页面 valid 状态
未分配的 0 VM系统未分配或创建的页,不占磁盘空间,非法地址
未缓存的 0 地址已分配,数据未缓存
缓存的 1 地址已分配,数据已缓存在DRAM中
条目号 有效位 物理页号或磁盘地址
PTE0 0 null
PTE1 1 VP1 在内存中起始地址
PTE2 1 VP2 在内存中起始地址
PTE3 0 VP3在磁盘中起始地址
PTE4 1 VP4在内存中起始地址
PTE5 0 null
PTE6 0 VP6在磁盘中起始地址
PTE7 1 VP7 在内存中起始地址

页命中

通过虚拟地址构造物理地址

MMU将虚拟地址作为索引来定位PTE,因为设置了有效位,MMU就知道该虚拟页缓存在内存中,于是使用PTE中的物理内存地址构造出这个字的物理地址

缺页

DRAM缓存不命中

地址翻译硬件从内存中读取PTE,从有效位推断出该VP未被缓存,触发缺页异常,调用内核缺页异常处理程序

程序选择一个牺牲页,即存在物理内存某PP中的VP,若该VP已修改,则将其内容复制回磁盘。从磁盘中复制新的页到内存中,并更新页表,随后返回

重新启动导致缺页的指令,该指令重新将虚拟地址发送给地址翻译硬件,此时可正常读取

所有现代系统都使用按需页面调度,即直到有不命中发生时,才将页面从磁盘换入DRAM

统计缺页次数:Linux的getrusage函数

分配页面

例如调用malloc,分配VP

VP的分配过程,是在磁盘上创建空间并更新PTE,使它指向磁盘上这个新创建的页面

内存管理与保护

内存管理

  1. 简化链接

  2. 简化加载

  3. 简化共享

  4. 简化内存分配

    例如调用malloc,分配VP

    VP的分配过程,是在磁盘上创建空间并更新PTE,使它指向磁盘上这个新创建的页面

    在虚拟内存分配 k k k个连续的页面,其映射到物理内存的 k k k个物理页面可以随机分散不必连续

内存保护

通过在PTE上添加一些额外的许可位,来控制对一个虚拟页面内容的访问

例如下图带许可位的页表

SUP READ WRITE 地址
VP0: PP1
VP1: PP3
VP2: PP2

SUP:是否必须运行在内核(超级用户)模式下才能访问该页

READ/WRITE:控制对页面的读和写访问

指令违反许可则CPU触发一个一般保护故障,段错误segmentation fault

地址翻译

地址翻译是一个 N N N元素的虚拟地址空间(VAS)中的元素和一个 M M M元素的物理地址空间(PAS)中元素之间的映射

M A P : V A S → P A S ∪ ∅ MAP:VAS \rightarrow PAS\ \cup\ \varnothing MAP:VAS→PAS ∪ ∅

名词解释

缩写 英文 中文译名
VP Virtual Page 虚拟页
PP Physical Page 物理页
PTE Page Table Entry 页表条目
TLB Translation Lookaside Buffer 快表
PTBR Page Table Base Register 页表基地址寄存器
符号 英文 描述
N = 2 n N=2^{n} N=2n none 虚拟地址空间中的地址数量
M = 2 m M=2^{m} M=2m none 物理地址空间中的地址数量
P = 2 p P=2^{p} P=2p none 页的大小,单位字节
VPO VP Offset 虚拟页面偏移量,单位字节
VPN VP Number 虚拟页号
TLBI TLB Index TLB索引
TLBT TLB Tag TLB标记
PPO PP Offset 物理页面偏移量,单位字节
PPN PP Number 物理页号
CO Cache Offdet 缓冲块内的字节偏移量
CI Cache Index 高速缓存索引
CT Cache Tag 高速缓存标记

映射方法

虚拟地址

n − 1 n-1 n−1 ~ p p p p − 1 p-1 p−1 ~ 0 0 0
虚拟页号(VPN 虚拟页偏移量(VPO

页表

有效位 物理页号(PPN)
————————————————
————————————————
————————————————

物理地址

m − 1 m-1 m−1 ~ p p p p − 1 p-1 p−1 ~ 0 0 0
物理页号(PPN 物理页偏移量(PPO

页表基地址寄存器

Page Table Base Register CPU中的一个控制寄存器,记录了页表在内存中的基地址,指向当前页表

n n n 位的虚拟地址包括两个部分, p p p 位的虚拟页面偏移量VPO和 ( n − p ) (n-p) (n−p) 位的虚拟页号VPN

m m m 位的物理地址也同样包括了两个部分, p p p 位的物理页面偏移量PPO和 ( m − p ) (m-p) (m−p) 位的物理页号PPN

其中虚拟页面偏移量和物理页面偏移量相同

虚拟页号作为基于页表基地址的偏移量,用于查找对应的PTE页表条目,若该条目有效,则条目记录了对应的物理页号

将查找到的物理页号和原虚拟地址中的偏移量拼接就组成了对应的物理地址

过程

页面命中
  1. 处理器生成虚拟地址,将虚拟地址传送给MMU
  2. MMU生成PTE地址,从高速缓存或主存中请求得到PTE
  3. 高速缓存或主存向MMU返回PTE
  4. MMU构造出物理地址,并将构造后的物理地址传送给高速缓存或主存
  5. 高速缓存或主存返回请求的数据字给处理器
页面不命中
  1. 处理器生成虚拟地址,将虚拟地址传送给MMU
  2. MMU生成PTE地址,从高速缓存或主存中请求得到PTE
  3. 高速缓存或主存向MMU返回PTE
  4. PTE中有效位为0,MMU触发异常,CPU跳转到操作系统内核中的缺页异常处理程序
  5. 缺页异常处理程序确定物理内存中的牺牲页,若已修改则将其换出到磁盘
  6. 缺页处理程序调入新的页,并更新PTE
  7. 缺页处理程序返回到原来的进程,再次执行导致缺页的指令,此次正确执行

快表 TLB

快表 TLB

Translation Lookaside Buffer 是在 MMU 中包括的一个小的、虚拟寻址的缓存,其中每一行都保存着一个由单个 PTE 组成的块,TLB通常有着高度的相联度

n − 1 n-1 n−1 ~ p + t p+t p+t p + t − 1 p+t-1 p+t−1 ~ p p p p − 1 p-1 p−1 ~ 0 0 0
VPN VPN VPO
TLB标记(TLBT TLB索引 t t t位(TLBI 偏移量

用于组选择和行匹配的索引和标记字段虚拟地址的虚拟页号中提取出来的

以下步骤中的地址翻译部分在芯片上的MMU中执行:

  1. CPU产生一个虚拟地址
  2. MMU从TLB取出相应的PTE
  3. MMU将虚拟地址翻译成物理地址,并发送到高速缓存或主存
  4. 高速缓存或主存将所请求的数据字返回给CPU

TLB不命中时,MMU需要从L1缓存中取出相应的PTE,新取出PTE放入TLB,依据情况执行相应的覆盖

多级页表

虚拟地址的虚拟页号 V P N VPN VPN 被拆分成 k k k 个部分,从高到低每一部分作为一级页表的索引

每级页表由虚拟地址中的 V P N i VPN\ i VPN i 作为索引,查找到对应的条目PTE,每个PTE记录的是下一级页表的基地址

最后一级页表记录了物理页表的物理页号 P P N PPN PPN,它和虚拟地址中的偏移 ( V P O = P P O VPO=PPO VPO=PPO) 共同组成了物理地址

虚拟地址

n − 1 ↦ n-1\mapsto n−1↦ p − 1 p-1 p−1 ~ 0 0 0
VPN 1 VPN 2 VPN k VPO

多级页表

注:为方便表示,这里把不同级的页表画在了一起,实际上他们在内存里是随机分布的

1 级页表 2 级页表 k 级页表
VPN 1作为索引在本页表查找条目 VPN 2作为索引 VPN k作为索引
每个PTE对应一个 2 级页表基址 每个PTE对应一个 3 级页表基址 每个PTE记录一个PPN

结合SRAM和DRAM

A L U { — V A ⟶ M M U { — P T E A → ⟵ P T E — — P A — ⟶ —— ⟵ — D a t a —— ⟵ —— ⏟ C P U [ P T E A m i s s P T E A h i t P A h i t P A m i s s ] ⏟ C a c h e − P T E A ⟶ ⟵ P T E — — P A — ⟶ ⟵ D a t a — } [ D R A M ] \underbrace{ALU \begin{cases} — VA\longrightarrow MMU \begin{cases} — PTEA \to \\ \longleftarrow PTE — \\ —PA —\longrightarrow \end{cases} \\ ——\longleftarrow—Data ——\longleftarrow—— \end{cases} }_{CPU} \underbrace{ \begin{bmatrix} \quad PTEA\ miss \\ PTEA\ hit \\ PA\ hit\\ \quad PA\ miss \end{bmatrix} }_{Cache} \left.\begin{matrix} -PTEA\longrightarrow \\ \longleftarrow PTE — \\ —PA—\longrightarrow \\ \longleftarrow Data —\end{matrix}\right\} \begin{bmatrix} \\ DRAM\\ \\ \end{bmatrix} CPU ALU⎩ ⎨ ⎧​—VA⟶MMU⎩ ⎨ ⎧​—PTEA→⟵PTE——PA—⟶​——⟵—Data——⟵——​​​Cache ​PTEA missPTEA hitPA hitPA miss​ ​​​−PTEA⟶⟵PTE——PA—⟶⟵Data—​⎭ ⎬ ⎫​ ​DRAM​ ​

大多数系统的高速缓存选择物理寻址

上图展示了物理寻址的高速缓存如何与虚拟内存结合,主要思路是地址翻译发生在高速缓存查找之前

页表条目也可以像其他数据一样缓存

【收藏】CSAPP深入理解计算机系统三万字长文解析相关推荐

  1. 万字长文解析“数据中台”的硅谷实践(文末有福利!)

    4月18日下午,智领云联合创始人&CTO,前EA(艺电)大数据平台高级工程经理宋文欣博士首度在智领云技术直播中开讲,向参加直播的数百位观众讲述了硅谷"数据中台"的故事.实际 ...

  2. Python可视化应用实战-三万字长文(建议收藏)matplotlib可视化实例,实操有效

    前言 以下是我为大家准备的几个精品专栏,喜欢的小伙伴可自行订阅,你的支持就是我不断更新的动力哟! MATLAB-30天带你从入门到精通 MATLAB深入理解高级教程(附源码) tableau可视化数据 ...

  3. 重磅综述:三万字长文读懂单细胞RNA测序分析的最佳实践教程 (原理、代码和评述)

    原文链接: https://www.embopress.org/doi/10.15252/msb.20188746 主编评语 这篇文章最好的地方不只在于推荐了工具,提供了一套分析流程,更在于详细介绍了 ...

  4. 18个月自学AI,2年写就三万字长文,过来人教你如何掌握这几个AI基础概念

    来源:机器之心 本文约30000字,建议阅读10分钟. 这是一篇真正针对初学者的 AI 教程,不只讲概念,还讲概念的底层原理. David Code 有多个身份:他是旅行作家,通晓多国语言,他还是一名 ...

  5. mongodb还不会?万字长文解析揉碎了给你讲,收藏这一篇就够了

    大家好,我是辣条. 目录 Mongodb的的增删改查 1. mongodb插入数据 2. mongodb的保存 3 mongodb的查询 4 mongodb的更新 5 mongodb的删除 mongo ...

  6. 全面理解-Flutter(万字长文,【性能优化实战】

    从目前行业的产品,以及社区生态来说,React Native 整体还是胜出 Flutter 一筹.毕竟早出来几年,市场占有率和行业积累还是在的.但是长远来看,技术发展也有它的必然规律,Flutter ...

  7. CSAPP(深入理解计算机系统)

    前言 自己这段时间上了微机原理,想起来这本书也看完了,就一同综合做个笔记.因而有部分是只属于MIPS的,我会标注出来,如果不需要应付考试的话我是不推荐读里面相关段落的一个字的,而为应付考试的话标注属于 ...

  8. csapp 深入理解计算机系统 csapp.h csapp.c文件配置

    转载自   http://condor.depaul.edu/glancast/374class/docs/csapp_compile_guide.html Compiling with the CS ...

  9. csapp深入理解计算机系统实验

    文章目录 实验0 环境配置 虚拟机下载 ubuntu系统 下载gcc gdb 和 vim 下载csapp实验包 实验1 datalab-handout 实验0 环境配置 配置了一下午加一晚上的环境,遇 ...

最新文章

  1. python 调用linux命令-Python 调用系统命令
  2. 病毒木马防御与分析实战
  3. PAT甲级1093 Count PAT‘s :[C++题解]DP、状态机模型dp
  4. 161. Leetcode 55. 跳跃游戏 (贪心算法-贪心区间)
  5. 半导体芯片原厂涨价及调价声明新增了这些!
  6. Java 类的特性1
  7. 怎么使用Eclipse默认的keystore签名打包成Apk
  8. [转载] python将图片进行base64编码, 解码
  9. KEIL中遇到WARNING: MULTIPLE CALL TO SEGMENT的解决方法
  10. 单片机:DS1302时钟
  11. Github上优秀的开源项目
  12. 区块链技术介绍PPT
  13. IOS开发Swift笔记19-扩展(Extension)
  14. Dell 笔记本鼠标莫名乱跑
  15. 【网络安全专栏目录】--企鹅专栏导航
  16. 【杂烩】Tesla M40 训练机组装与散热改造
  17. 移动应用广告对接:为什么SDK是最佳选择?
  18. 兵士不克不及怂就是干!美服龙战上传说--新浪炉石传说专区
  19. matlab瘦脸大眼的代码,OpenGL ES 实现瘦脸大眼效果
  20. 带你了解网络解说--链路聚合技术

热门文章

  1. CentOS8.1部署Gitlab+Jenkins持续集成(CI/CD)环境之Jenkins安装(二)
  2. ICLR 2022哪篇论文最火?这个「集邮」狂魔放出3400篇大礼包
  3. 面试-计算机网络-物理层-数据链路层-网络层-应用层-网络安全
  4. 小米4联通版刷flyme,工程模式无法选择网络
  5. ubuntu 命令永久修改屏的翻转
  6. HarmonyOS Data Ability关系数据库
  7. xeon e5-2400 系列处理器能做四路服务器吗?,至强E5-1600系列处理器细节大揭秘
  8. 2021.12.21【读书笔记】| 在Liunx中替换windows格式文本回车符
  9. chrome 莫名其妙的弹出广告
  10. MobileNets:用于移动视觉应用的高效卷积神经网络