不奢望岁月静好 只希望点滴积累

0%

应用系统的大部分瓶颈在IO上、不是所有的问题都可以利用内存或者CPU Cache做缓存解决、Mysql单表几千万的数据、早已不是罕见现象了、意味着, 使用内存当缓存、存储空间是不够用的. 大部分请求还是会打到硬盘上.

IO性能、顺序和随机访问

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
从硬盘厂商的性能报告上、通常可以看到两个指标: 响应时间(Response Time)和数据传输率(Data Transfer Rate)

现在常用的硬盘有两种: HDD硬盘(机械硬盘) 和 SSD硬盘(固态硬盘). 现在的HDD硬盘使用的是 SATA 3.0 的接口.
SSD硬盘有两种接口: SATA 3.0 和 PCI Express 接口

SATA 3.0 接口、带宽是6Gb/s, b是bit、相当于每秒传输 768M 数据; 日常使用的HDD硬盘的数据传输率差不多200MB/s
SSD的硬盘、数据传输率差不多500MB/s, 实际SSD的硬盘可以更快. 可以换用PCI Experss的三星SSD硬盘、
它的数据传输率、在读取的时候可以达到2GB/s 左右、差不多是HDD硬盘的10倍、写入时也有1.2GB/s.

除了数据传输率、我们还关心响应时间. SSD硬盘大概在几十微秒、HDD大概在几ms到十几ms、差异在几十倍甚至上百倍.

单看响应时间和吞吐率、硬盘性能还可以、基本上在ms时间内可以返回, 1s内可传输的数据也有200MB左右、
db一条记录、一般也就1kb大小、差不多每秒可写入 200M*1024 / 120w条数据、似乎和平时经验不符 ?

因为硬盘顺序读写和随机读写的性能差异很大.
SSD随机读写的时候、数据传输率只有40MB/s、只有顺序读写情况的几十分之一、按照每次读取4KB计算、
40MB/s / 4kb = 10000次、即: 每秒随机读取1万次、写入会多一些、1s大概90MB、即: 2w多次
这个每秒读写的次数、称为IOPS、即: 每秒输入输出操作的次数

HDD的IOPS通常只有100左右、而不是20w次、这个100怎么得到的呢 ?如何优化 ?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
一块机械硬盘由盘面、磁头和悬臂三个部件组成.
盘面: 就是实际存储数据的盘片、通常是使用铝、玻璃或者陶瓷制成的光滑盘片, 盘面有一层磁性涂层
数据就存储在磁性的涂层上、中间有一个受电机控制的转轴, 用来控制盘面旋转.

硬盘一个很重要的指标是转速, 通常有5400转、7200转、10000转的、这个转速指的就是转轴的旋转速度、
英文单位 RPM, 即: 每分钟的旋转圈数、7200转为例, 折算到s、就是120

磁头: 数据是通过磁头从盘面读取、然后通过电力信号传输给控制电路、接口、再到总线的
通常一个盘面在正反两面有连你刚刚磁头、且: 一块硬盘也不是只有一个盘面、而是上下堆叠了很多个盘面、
各个盘面是平行的、每个盘的正反两面都有磁头.

悬臂: 连接在磁头上、在一定范围内把磁头定位到某个特定的磁道上.

盘面通常是圆形的、多个同心圆组成、每个磁道有自己的编号, 悬臂用来控制读哪个磁道的数据
磁道会分成一个个扇区、上下平响的一个个盘面的相同扇区叫一个柱面、

读取数据分两步:
1. 将盘面旋转到某一个位置、在这个位置上、悬臂可以定位到整个盘面的任意子区间、这个区间称为`几何扇区`
2. 将悬臂移动到特定磁道的特定扇区、找到之后磁头落下、读取数据
所以, 需要的时间也由两个部分构成:
1. 平均延时, 就是将几何扇区对准悬臂的时间
7200转为例, 1s可以旋转240个半圈、即: 1s/240 = 4/71ms
2. 平均询道时间, 即 悬臂定位到扇区的时间
HDD硬盘一般在4-10ms、
这样随机数据访问延时一般在8-14ms, 那么
1s/8ms = 125 IOPS 或者 1s/14ms = 70 IOPS

硬盘物理结构.png

如何定位 IO_WAIT

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
由上边可以看到、即使是PCI Express接口的SSD硬盘、IOPS也只有2w左右, CPU 的主频通常在2GHZ以上、
也就是每秒可以做20亿次操作. 即使一条指令需要多个时钟周期、一秒内CPU可执行的指令数和硬盘能进行的操作次数
也有好几个数量级的差异, 所以常听到性能瓶颈在IO上的说法、因为CPU发出指令之后、需要等待IO操作完成.

如何判断程序性能问题是否真的来源于IO瓶颈呢 ?
1. top指令
top指令的输出结果中、有一行 %CPU 开头、有一个 wa 的指标、就代表iowait, 即: CPU等待io操作完成花费的时间占CPU的百分比

iostat可以看到实际的情况

avg-cpu: %user %nice %system %iowait %steal %idle
17.02 0.01 2.18 0.04 0.00 80.76
Device: tps kB_read/s kB_wrtn/s kB_read kB_wrtn
sda 1.81 2.02 30.87 706768 10777408

tps: 对应的就是硬盘的 IOPS 指标
KB_read/s 和 kb_write/s 对应的就是 数据传输指标
iotop 可以看到具体哪一个进程占用了大量的io

一、程序装载的挑战
在运行可执行文件的时候、装载器会把对应的指令和数据加载到内存中、让CPU执行、装载器需要满足两个条件:

  1. 可执行程序加载后占用的空间是连续的、
  2. 需要同时加载多个程序、并且不能让程序自己规定在内存中加载的位置
    因为想要的地址可能已经被其它程序给占了

解决:

在内存里找到连续内存空间分配给装载的程序、然后把这段连续的内存地址和整个程序指令里的指定内存地址做映射

虚拟内存地址 Virtual Memory Address指令里用到的地址
物理内存地址 Physical Memory Address在内存硬件里的地址

程序中有指令和各种内存地址、我们只需要关心虚拟内存地址就行了、对于任何一个程序来说、它看到的都是同样的内存地址、我们维护一个虚拟内存到物理内存的映射表、这样实际程序指令执行时、会通过虚拟内存地址、找到对应的物理内存地址、然后执行.
因为地址是连续的、只需要维护映射关系的起始地址和对应的空间大小就可以了

二、内存分段
分段 找出一段连续的物理内存和虚拟内存地址进行映射的方法
image.png

分段解决了程序本身不需要关心具体内存地址的问题、也有一些不足之处、内存碎片

内存交换 Memory Swapping 将程序写回硬盘、再从硬盘读回、读回时放到连续空间位置上、这样剩余空间就是连续的了、解决了内存碎片 的问题
但: 若内存交换的时候交互的是一个内存占用很大的程序、机器会十分卡顿

三、内存分页

既然问题出在内存碎片和交换空间太大上、那么解决办法就是: 少程序内存碎片、另外 当进行内存交换的时候、让需要交换的数据更少、那么问题就可以解决
这个办法在计算机的内存管理里边叫内存分页

内存分页 是把整个物理内存空间切成一段固定尺寸的大小、这样一个连续并且尺寸固定的内存空间叫页(Page)

1
getconf PAGE_SIZE # 查看页大小、默认4k

由于内存是预先划分好的、就没有了不能使用的碎片、只有被释放出来的很多4kb的页、即使内存空间不够、需要让现有的正在运行的程序通过内存交互、释放出来一些内存空间、一次写入磁盘的也只有少数一个或者几个页、不会花很多时间、不会卡顿

image.png

进一步、程序加载的时候不是一次性全部加载、二手在运行时、需要用到对应虚拟内存页里的指令和数据时才加载、读取特定页时、发现数据未加载到物理内存时会触发一个来自CPU的缺页错误(Page Fault) 系统捕获这个错误、将对应页从虚拟内存读取加载到物理内存

四、链接: 动态链接和静态链接
动态链接 Dynamic Link 链接的不是存储在硬盘上的目标文件代码、二手加载到内存的共享库(shared Libiaries)
静态链接 之前说到的合并代码段的方式

在win下、共享库文件是.dll 文件、也就是 Dynamic-Link Library(DLL, 动态链接库).
在Linux下、就是.so 文件、(Shared Object)

五、电信号&门电路

远古传信: 人工、速度慢
-> 金、鼓: 距离有限
-> 烽火台: 光信号、不能传递复杂信息
-> 电报: 传输距离增加、输入信号速度加快
-> 继电器: 解决更远距离传输

多核CPU里的每一个CPU核、都有独立属于自己的L1 Cache 和 L2 Cache、多个CPU之间、只是共用L3 Cache 和 主内存、CPU的每个核之间都有各自的缓存、相互之间的操作又是独立的、就会带来缓存一致性问题

image.png

1
2
3
4
5
示例: 如上图有两个CPU核心、CPU1、CPU2
假设1号核心要将降价信息写入到内存、但使用的是写回策略、先写入到L2 Cache
但并未同步到L3 Cache或者主内存、只有在这个Cache Block被交换出去的时候、才会写到主内存
此时L2 Cache得到的依然是原始价格、CPU1 和 CPU2 Cache里的数据都是读的自身Cache、是不一致的
这就是缓存不一致性

思考:
缓存要保证一致性、需要满足什么条件呢 ?

1
2
3
4
1. 写传播(Write Propagation). 在一个CPU核心里、Cache数据更新、必须能传播到其它节点对应的Cache Line里
2. 事务的串行化(Transaction Serialization): 在一个CPU核心里的读取和写入、在其它的节点看起来、顺序是一样的
(eg. L1 核心先操作了降价为6000、L2核心又操作了降价为5000、若不能保证事务的串行化, 可能L3核心
得到的顺序是L1 -> L2 最终为5000, 而L4得到的顺序为 L2 -> L1 最终为6000、j出现不一致)

总线嗅探机制和MESI协议

1
2
3
4
5
6
要解决缓存一致性问题、首先要解决的是多个CPU核心之间的数据传播问题、最常见的解决方案叫 `总线嗅探`
其实是把所有的读写请求都通过总线Bus广播给所有的CPU核心、然后让各个核心去嗅探这些请求、再根据本地情况进行响应

由于总线本身就是一个特别适合广播进行数据传输的机制、所以、也是日常使用的Intel CPU采用的方案

基于总线嗅探机制、可以分成很多种不同的缓存一致性协议、最常用的就是`MESI`协议

MESI协议(写失效协议)

1
2
3
4
5
6
7
8
9
`MESI`协议、是一种叫`写失效`(`Write Invalidate`)的协议、在写失效协议里、只有一个CPU核心负责写入数据、
其它的核心、只是同步读取到这个写入、在这个CPU核心写入Cache之后、它会广播一个`失效`请求告诉其它核心
其它核心只是判断自己是否也有一个`失效`版本的Cache Block、然后相应标记为`失效`

MESI 协议由来是对应于 Cache Line的四个不同的标记
M: 已修改 Modified
E: 代表独占 Exclusive
S: 代表共享 Shard
I: 代表已失效 Invalidated

独占和共享的差别在哪里呢 ?

1
2
3
4
5
6
7
8
独占状态下的数据、对应的CPULine只加载到了当前CPU所拥有的Cache里、其它的CPU核、
并没有加载对应的数据到自己的Cache里、此时若向独占的Cache Block写入数据、可自由操作、无需通知其它CPU

独占状态下的数据、若收到来自总线的读取缓存的请求、就变成共享状态、因为另一个CPU也把对应的Cache Block加载到了自己的Cache

共享状态下、同样的数据多个CPU核心的Cache里都有、更新Cache时不可直接修改、
而是要先向其它的CPU广播一个请求、要求先把其它CPU Cache变成无效状态、再更新当前Cache里的数据
这个广播操作叫`RFO`(Request For Ownership)获取当前对应Cache Block 数据的所有权

MESI状态流转.png

写广播协议

1
2
3
写广播协议会将一个写入请求广播到所有的CPU核心、同时更新各个核心里的Cache
在实现上比较简单、但要占用更多的总线带宽、写失效只是告诉其它CPU核心、哪个地址的缓存失效了、
但是写广播协议把对应的数据传播给其它的CPU核心

内存需要被分成固定大小的页(Page),通过虚拟地址到物理地址的转换、才能找到实际物理地址、程序看到的地址都是虚拟地址

简单页表

1
2
3
4
5
6
7
8
9
虚拟内存和物理内存映射、最直观的方法就是建立一张映射表、实现虚拟内存到物理内存的一一映射
其实就是页表(Page)

这种转换会把一个内存地址分成页号Directory和偏移量Offset两个部分
前边的高位、就是内存地址的页号; 后边的地位就是内存地址里的偏移量.
地址转换的页表、只保留页号之间的映射关系即可.

同一个页面的内存、在物理层面是连续的、eg. 一个页大小为4K
需要20位的高位和12位的地位

image.png

内存地址转换:

  1. 将虚拟内存地址、切分成页号和偏移量的组合
  2. 从页表里边、查询出虚拟页号对应的物理页号
  3. 拿物理页号 + 偏移量 -> 物理内存地址

思考: 这样一个页表需要多大空间 ?

1
2
3
4
32位的内存地址空间、需要记录2^20大小的数组、一个页号是完整的32位的4字节、这样一个页表就需要4M的空间
并且: 每个进程都有属于自己的虚拟地址空间、也就是、每个进程都需要这样一个页表.
不管进程本身是只有几KB大小的程序、还是需要几GB这样的内存空间、都需要这样一个页表
现在的内存大多超过了4G、若使用上边的数据结构来保存页面、内存占用会更大、如何处理呢 ?

多级页表

1
2
3
4
5
6
7
8
9
10
大部分进程占用的内存是有限的、需要的页自然也是有限的、只保存 `用到的页`
之间的映射关系即可

那、是不是可以选择hash表呢 ?其实采用的是多级页表. 为什么呢 ?

在整个进程的内存地址空间、通常是 `两头实、中间空`. 在程序运行的时候、内存地址从顶部往下、
不断分配占用的桟的空间. 而堆的空间、则是从底部往上、不断分配占用的

所以、在一个世纪的程序进程里、虚拟内存占用的地址空间、通常是两段连续的空间、而不是完全散落的随机的内存地址.
而多级页表、就很适合这样的内存地址分布

以一个4级的多级页表为例: 同一个虚拟内存地址、偏移量的部分和简单页表一样不变、但原有的页号部分、拆成4段、从低到高、分成4级到1级这样4个页表索引

image.png

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
对应的、一个进程会有一个4级页表、先通过4级页表索引、找到4级页表里对应的条目Entry. 存放的是一张3级页表所在的位置
4级页表的每一个条目、都对应这一张3级页表、所以可能有多张3级页表

找到对应的3级页表之后、用3级页表找对应3级索引的条目.
3级页表的索引会指向一个2级页表、同样、二级页表使用索引指向1级页表
1级页表的条目对应的数据内容是物理页号.
拿到物理页号之后、使用 `页号 + 偏移量 ` 得到最终的物理内存地址

可能有很多张1级页表、2级页表甚至3级页表、但: 因为实际的虚拟内存地址通常是连续的、可能只需要很少的2级页表、甚至只需要一张3级页表就可以了
事实上、多级页表就像一个多叉树、常称为页表树(Page Table Tree)

因为虚拟地址分布的连续性、树的第一层节点的指针、很多是空的、无需对应子树、找最终物理页号
就类似通过一个特定的访问路径、走到树最底层的叶子节点.

这样分成4级的多级页表来看、若每一级都用5个bit表示、每一张某一级的页表、只需要2^5=32个条目
若每个条目还是4个字节、共需要128字节、而一个一级索引表、对应324KB的、即共128KB的大小
一个填满的2级索引、对应321级索引、即4MB的大小

若一个进程占用8MB的内存空间、分成24MB的连续空间、则它需要2个独立的、填满的2级索引表、
641级索引表、2个独立的3级索引表、14级索引表、共需要64+2+2+1=69个索引表、每个128字节(9KB)
差不多只有简单页表的1/500、不过也带来了时间上的开销

原本只需要一次内存访问就可以找到物理页号、算出物理内存地址、使用4级页表的话、就需要4次内存访问、才能找到物理页号

思考:

内存访问比Cache的性能要差很多、原本只是要做一个简单地址转换、反而要访问好几次内存、时间性能的损耗如何优化呢 ?

— 下文分析

机器指令里的地址都是虚拟内存地址、程序里的每一个进程、都有属于自己的内存地址空间
可通过地址转换得到最终的实际物理地址. 指令都放在内存里、数据页都在内存、地址转换是一个高频动作、必须保证性能问题

加速地址转换TLB

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
从虚拟地址到物理地址的转换、可通过页表来处理、为节约存储空间、使用多级页表
内存访问比Cache慢很多、简单的地址转换按照多级页表的设计会有很性能损失、怎么办呢? -- 加个缓存试试呢 ?

程序需要的指令、都顺序放在虚拟内存里、执行的指令也是一条条顺序执行的
即:对于指令地址的访问、存在局部性(空间局部性和时间局部性)、需要的数据也是一样的
连续执行5条指令、因为地址都是连续的、所以这5条指令通常在同一个虚拟页里
因此、连续5次的内存地址转换、其实都来自于同一个虚拟页号、转换的结果自然是同一个物理页号、
就可以将之前的内存地址转换缓存下来、不需要反复访问内存来进行内存地址的转换

于是、专门在CPU里放了一块缓存芯片、称为`TLB`(Translation-Lookaside Buffer)地址变换高速缓存
存放已进行转换过的结果、同样的虚拟地址需进行地址转换的时候、可直接在TLB里查询结果、无需多次访问内存来完成转换

TLB和CPU的高速缓存类似、可分为指令的TLB和数据的TLB、即: ITLB
和 DTLB、也可分级、变成L1、L2这样多层的TLB

需要脏标记这样的标记位来实现`写回`这样缓存管理策略、也和高速缓存很类似

为了性能、整个内存转换过程也由硬件来执行、CPU芯片里、封装了内存管理单元MMU(Memory Management Unit)来完成地址转换

安全性和内存保护

1
2
3
4
虽然现代操作系统和CPU、有各种权限管控、通过虚拟内存和物理内存的区分、隔离了各个进程
但: 无论CPU这样的硬件还是操作系统这样的软件、都很复杂、难免会被黑客找到各种漏洞

计算机最底层的安全保护机制称为内存保护、如`可执行空间保护`和`地址空间布局随机化`等

可执行空间保护

1
2
3
4
5
6
7
8
对一个进程使用的内存、只把指令部分设置成`可执行`的、其它、如数据部分、不给予执行权限
因为无论指令还是数据、在CPU看来、都是二进制的数据、直接将数据部分拿给CPU、数据解码后、也是合理的指令、即可执行的

此时黑客们就想到了在程序的数据区、放入一些要执行的指令编码后的数据、让CPU当成指令去加载、
CPU就能执行想执行的指令了、对内存空间的执行权限进行控制、就使得CPU只能执行指令区的代码、
对数据区的内容、即时找到了其它漏洞想装成指令被执行也会因为没权限而被阻挡掉
eg. php进行web开发时、通常会禁用php执行eval函数的执行权限
sql注入攻击

地址空间布局随机化

1
2
3
4
5
6
7
8
内存层面的安全保护核心策略、是在可能有漏洞的情况下进行安全预防、可执行空间保护就是很好的例子、
但内存层面的漏洞还存在其它的可能性:
eg. 其它的人、进程和程序修改调特定进程的指令、数据、让当前进程执行特定的指令和数据、造成破坏

原先、进程的内存布局空间是固定的、任何第三方很容易知道指令在哪里、程序桟、堆、数据又在哪里
为人文破坏创造了很大的便利、`地址空间布局随机化`就是让位置不固定、让内存空间随机分配这些进程里
不同部分所在的额内存空间地址、让破坏者猜不出来、减小破坏性
eg. 密码登录

大多数计算机使用8位的块、或者字节byte 作为最小的可寻址的内存单位、而不是访问内存中独立的位、机器级的程序将内存视为一个很大的字节数组、称为 虚拟内存(virtual memory), 内存的每个字节由一个唯一的数字来标识、所有可能地址的集合称为 虚拟地址空间(virtual address space)

每个计算机都有一个字长 word size, 表明指针数据的标称大小, 对于宇哥w位的机器而言、虚拟地址的范围是 0~2m-1

32位字长限制虚拟地址空间为4GB、64位为16EB(1.84*1019)字节

大多数64位机器可以运行32位的程序、

eg. gcc -m32 prog.c 编译出来的程序可在32位机器上运行、

​ gcc -m64 prog.c 编译出来只可在64位机器上运行

so. 将程序称为32位或者64位程序时、区别在于程序是如何编译的、而不是运行时机器的类型

为了避免依赖大小和不同编译器设置带来的差异、ISO C99标准引入了 int32_t 和 int64_t 两种固定长度的数据类型

大端法: 高地址存储高位数

小端法则反之

对大多数程序来说、字节顺序完全不可见、但:

A机器产生的数据通过网络发送到B机器时、B机器会按照字节的内部表示来进行转换、可能会出问题、eg. add %eax, 0x2000b43(%rip) 、字节顺序就会导致数据解析错误

so. 网络应用程序的代码必须遵守已建立的关于字节顺序的规则

一、在软硬件接口中、CPU帮我们做了什么事?
硬件角度 CPU就是一个超大规模集成电路、通过电路实现加法、乘法及各种各样的处理逻辑

软件角度 CPU就是执行各种计算机指令(机器语言: Machine Language)的逻辑机器

不同CPU有不同的指令集、所以 电脑上的程序简单的复制下装载到手机上是不能正常运行的、而复制到另外一台计算器上就可以正常运行

存储程序型计算机 程序是由成千上万条指令组成的、但是CPU里不能一直放着所有指令、平时是存储在存储器的、这种指令存储在存储器的计算机叫存储程序型计算机

二、从编译到汇编、代码怎么变成机器码?
高级语言 -> 编译 -> 得到汇编代码
汇编代码 -> 汇编 -> 机器码(计算机可识别的指令)

三、解析指令和机器码
常见指令:
算术指令 加减乘除等、在CPU层面、都会变成一条算术指令
数据传输指令 给变量赋值、在内存里读写数据等、用的都是数据传输类指令
逻辑类指令 逻辑上的与或非、都是这类指令
条件分支指令 if/else 等都是分支类指令
无条件跳转指令 函数调用

四、CPU是如何执行指令的?
拿Intel CPU来说、里边包含几百亿个晶体管、实际上指令执行起来非常复杂、对软件程序员来说、只需要知道指令是顺序执行就可以了

CPU其实是一堆寄存器组成的、而寄存器就是CPU内部由多个触发器(Flip-Flop)或者锁存器(Latches) 组成的简单电路

N个寄存器或者锁存器可以组成一个N位(bit) 的寄存器、能够保持N位的数据、eg. 我们使用64位Intel的服务器、寄存器就是64位的

image.png

CPU中不同功能的寄存器会有很多、有三种比较特殊的:
PC寄存器 Program Counter Register, 也叫地址寄存器 Instruction Address Register 用来存放下一条需要执行的计算机指令的内存地址

指令寄存器 Instruction Register 用来存放当前正在执行的指令

条件码寄存器 Status Register 用里边的标记位Flag, 存放CPU进行算术或者逻辑运算的结果

除了这些特殊的寄存器、CPU还有更多用来存储数据和内存地址的寄存器. 这样的寄存器每类通常有很多、会根据它们的存放内容取名字、eg. 整数寄存器, 浮点数寄存器, 向量寄存器地址寄存器 等,有些既可以存放数据、也可以存放地址, 我们称为 通用寄存器

image.png

实际上、程序运行的时候、CPU会根据PC寄存器里边的地址、从内存把需要执行的指令读取到寄存器里边执行、然后根据指令长度自增、读取下一条指令, 可以看到指令在内存中是连续保存的、也会顺序加载
而有些特殊指令、eg. J指令, 也就是跳转指令 会修改PC寄存器里边的地址值、这样下一条要执行的指令就不是才能够内存顺序加载的了、因此才能使用 if...else / while / for 等语句

五、周期概念
指令周期 Fetch->Decode -> Execute (获取指令 -> 指令译码 -> 执行指令 ) 的循环称为一个指令周期(Instruction Cycle)

CPU周期 Machine Cycle CPU的内部操作很快、但是内存访问速度很慢、每一条指令都需要从内存里加载而来、把从内存中读取一条指令的最短时间称为一个CPU周期

一个指令周期 包含多个 cpu周期,
一个cpu周期 包含多个 时钟周期

六、建立数据通路

数据通路就是我们的处理器单元、通常由两类元件组成:
1.操作元件(组合逻辑元件) 其实就是ALU、在特定输入下、根据组合电路的逻辑生成特定输出
2.存储元件: 状态元件 eg. 计算过程中用到的寄存器、无论是通用寄存器还是状态寄存器、都是存储元件
通过数据总线将操作元件和存储元件连接、就可以完成数据的存储和传输、就是所谓的建立数据通路

七、CPU所需要的硬件电路

1
2
3
4
1. ALU: 无状态的、根据输入得到输出结果的电路
2. 寄存器:能够进行状态读写的电路元件. 常见的有 锁存器(Latch), D触发器(Data/Delay Flip-flop) 电路
3. 自动实现pc寄存器自增的电路
4. 译码电路: 实现对指令进行decode、对拿到的地址去获取对应的数据或者指令等

组合逻辑电路 给定输入、得到固定输出. 无法完成复杂工作
时序逻辑电路 解决了自动运行存储问题

时钟信号: 通过一个电磁电路实现、不断切换信号

反馈电路: 把当前电路的输出信号作为输入信号、再返回到当前电路

总线: 一组电子管道, 携带信息字节、并负责在各个组件间传递

每个IO设备通过一个控制器或者适配器与IO总线相连

控制器和适配器的区别是: 封装方式的不同、但功能都是在IO总线和IO设备之间传递信息

控制器: IO设备本身或者系统的主板上的芯片组

适配器: 是一块插在主板槽上的卡

主存: 是一个临时性的存储设备、在处理器执行程序时、用来存放程序和程序处理的数据

从物理上来说、主存是由一组动态随机存取存储器芯片组成的

从逻辑上来说、存储器是一个线性的字节数组、每个字节有唯一地址(数组索引)、地址从0开始、

处理器: 中央处理单元CPU、简称处理器、是解释(或执行)存储在主存中指令的引擎、

处理器的核心是一个大小为1个字的存储设备(或寄存器)、即: 程序计数器PC

在任何时刻、PC都指向主存中的某条机器语言指令(即: 含有该指令的地址)

处理器从程序计数器指向的内存处读取指令、解释指令中的位、执行该指令的简单操作、然后更新PC, 使其指向下一条指令(不一定和内存中刚刚执行的指令相邻、比如 jump)

这样的简单操作不多、围绕 主存、寄存器文件 和 算术/逻辑单元 ALU 进行. eg. 如下操作

加载: 从主存复制一个字节或者一个字到寄存器、以覆盖寄存器原来的内容

存储: 从寄存器复制一个字节或者一个字到主存的某个位置、以覆盖这个位置上原来的内容

操作: 把两个寄存器的内容复制到ALU, ALU对这两个寄存器的字进行算术运算、并将结果存放到一个寄存器中、覆盖该寄存器原来的内容

跳转: 从指令本身抽取一个字、并将这个字复制到程序计数器PC中、以覆盖PC中原来的值

处理器的指令架构集描述的是每条机器代码指令的效果

微体系结构描述的是处理器实际上如何实现的

处理器从寄存器文件中读数据比从主存中读取几乎要快100倍、所以系统设计者采用了更小更快的存储设备 高速缓存存储器 cache 来存放近期需要的数据

位于处理器芯片上的L1高速缓存 容量可以达到数万字节、访问速度几乎和访问寄存器问你件一样快

一个容量为数十万到数百万字节的L2高速缓存 通过一条特殊的总线连接到处理器、进程访问L2高速缓存的时间要比访问L1高速缓存的时间长5倍、但仍比访问主存快5~10倍

L1和L2高速缓存是使用 静态随机访问存储器SRAM的硬件技术实现的

新的系统出现了L3缓存、系统可以获得一个更大的存储器、同时访问素的也更快、是利用了高速缓存的局部性原理、使程序具有访问局部区域里的数据和代码的趋势、通过让高速缓存里存放可能经常性访问的数据、

程序不会直接操作硬件、而是依靠操作系统提供的服务

可以将操作系统看成应用程序和硬件之间插入的一层软件

操作系统: 1.防止硬件被失控的应用程序滥用 2.向应用程序提供简单一致的机制来控制硬件设备(通过 进程、虚拟内存、文件来实现)

进程:

并发运行: 一个进程的指令和另一个进程指令是交错执行的、大多数系统中可以运行的进程数多于CPU个数、传统系统 一个时刻只能执行一个程序、多核处理器可以同时执行多个程序

无论是单核还是多核处理器、一个CPU看上去都像是在并发执行多个进程、是通过进程间切换实现的。这种交错执行的机制称为 上下文(包括PC、寄存器文件当前值及主存内容等)切换

从一个进程到另一个进程的切换是操作系统内核管理的, 内核是操作系统代码常驻主存部分

注意: 内核不是一个独立的奖金池、而是系统管理全部进程所用代码和数据结构的集合

线程:

进程实际上由多个线程执行单元组成、每个线程运行在进程上下文中、并共享同样的代码和全局数据

虚拟内存;

虚拟内存是一个抽象概念、为每个进程提供了独占使用主存的假象、每个进程看到的内存是一致的、称为虚拟地址空间

Linux中、地址空间的最上边的区域是给操作系统代码和数据的、底部存放用户进程的代码和数据、地址从下-> 上增加

每个进程看到的虚拟地址空间由大量准确定义的区构成、每个区有专门的功能(下述从最低地址向上)

  • 程序代码和数据: 对所有进程来说、代码从同一个固定地址开始、接着是和C全局变量对应的数据位置
  • 堆: 代码和数据区后紧随着是运行时 堆、代码和数据区在进程开始运行时就指定了大小、堆是在运行时可动态扩展和收缩的空间
  • 共享库: 大概地址空间的中间是一块用来存放像C标准库和数学库这样的共享库代码和数据的区域
  • 桟: 位于用户虚拟地址顶部的是用户桟、编译器用它来实现函数调用
  • 内存虚拟内存: 不允许应用程序读写这个区域的内容或者直接调用年内和定义的函数

文件:

文件就是文字序列、每个io设备、包括磁盘、键盘、显示器 甚至网络都可以看成文件

系统中的输出输入都是通过Unix io的系统函数调用读写文件来实现的

Amdahl定律:

Amdahl定律: 对系统某部分性能的提升取决于该部分的重要性和加速程度、

eg. 某系统初始耗时 60%、加速比例 k=3 则加速为 1/((1-0.6)+0.6/3) = 1.67倍

并发:

线程级并行:

1.多个CPU

2.超线程 又称:同时多线程、是一项允许一个CPU执行多个控制流的技术, 它涉及CPU某些硬件有多个备份、eg. 程序计数器和寄存器文件、而其它的硬件只有一份、eg. 执行浮点数运算的单元, 常规的处理器大约需要20000个时钟周期做线程切换、而超线程的处理器可以在单个周期的基础上决定执行哪个线程

指令级并行

早期处理器一个指令需要多个时钟周期来执行、现代处理器可以同时执行多条指令的属性称为: 指令级并行

单指令、多数据并行

现代处理器允许一条指令参数多个并行执行的操作、称为: 单指令、多数据并行 即:SIMD并行

提高这些SIMD指令是为了提高处理影像、声音和视频数据应用的执行速度、虽然有些编译器会试图从C程序中自动抽取SIMD并行性、但更可靠的方法是用编译器支持的特殊向量数据类型来写程序、eg. GCC 就支持向量数据类型

几个抽象:

文件是对IO设备的抽象

虚拟内存是对程序存储器的抽象

进程是对一个正在运行的程序的抽象

虚拟机是对整个计算机的抽象、包括 操作系统、处理器和程序

https://time.geekbang.org/column/article/91427

图片发自简书App

极课时间上的课程是我觉得质量最高的课程、很多课程忍不住想要记下笔记、如果觉得好、还希望大家多支持作者、到极课时间购买课程

推荐书籍汇总:

入门书籍:

  1. 程序是怎样跑起来的
  2. 计算机是怎样跑起来的

深入学习书籍:

  1. 计算机设计与组成: 硬件/软件接口
  2. 深入理解计算机系统
  3. 计算机体系结构: 量化研究方法

课外阅读:

  1. What Every Programmer Should Kown About Memory
  2. 编码: 隐匿在计算机软硬件背后的语言
  3. 程序员的自我修养: 链接、装载和库
  4. https://www.bilibili.com/video/av24540152/

阅读推荐

https://people.freebsd.org/~lstewart/articles/cpumemory.pdf

image

1
2
3
4
存储程序计算机: 可存储程序、可编程

1. 不可编程: eg. 早期的计算器、只能进行简单算术运算、功能不可被修改
2. 不可存储: eg. 早期的插线板式计算机、每次写好的程序不能存储下来供下次使用、需要相同程序的时候、也必须重新插板子、重新编程

计算机组成
image.png

所有的计算机程序、都可以抽象为从输入设备读取输入程序,通过运算器控制器来执行存储在存储器里的程序、最终把结果输出到输出设备
图片来源: 极课时间.png