计算机组成原理(四)
我的博客:https://www.luozhiyun.com/
内存
内存是五大组成部分里面的存储器,我们的指令和数据,都需要先加载到内存里面,才会被CPU拿去执行。
我们的内存需要被分成固定大小的页(Page),然后再通过虚拟内存地址(Virtual Address)到物理内存地址(Physical Address)的地址转换(Address Translation),才能到达实际存放数据的物理内存位置。而我们的程序看到的内存地址,都是虚拟内存地址。
页表
想要把虚拟内存地址,映射到物理内存地址,最直观的办法,就是来建一张映射表。虚拟内存里面的页,到物理内存里面的页的一一映射。这个映射表,在计算机里面,就叫作页表(Page Table)。
页表这个地址转换的办法,会把一个内存地址分成页号(Directory)和偏移量(Offset)两个部分。
对于一个内存地址转换,其实就是这样三个步骤:
- 把虚拟内存地址,切分成页号和偏移量的组合;
- 从页表里面,查询出虚拟页号,对应的物理页号;
- 直接拿物理页号,加上前面的偏移量,就得到了物理内存地址;
多级页表(Multi-Level Page Table)
大部分进程所占用的内存是有限的,需要的页也自然是很有限的。我们只需要去存那些用到的页之间的映射关系就好了。
在整个进程的内存地址空间,通常是“两头实、中间空”。在程序运行的时候,内存地址从顶部往下,不断分配占用的栈的空间。而堆的空间,内存地址则是从底部往上,是不断分配占用的。
所以,在一个实际的程序进程里面,虚拟内存占用的地址空间,通常是两段连续的空间。
我们以一个4级的多级页表为例,来看一下。
对应的,一个进程会有一个4级页表。我们先通过4级页表索引,找到4级页表里面对应的条目(Entry)。这个条目里存放的是一张3级页表所在的位置。4级页面里面的每一个条目,都对应着一张3级页表,所以我们可能有多张3级页表。
找到对应这张3级页表之后,我们用3级索引去找到对应的3级索引的条目。3级索引的条目再会指向一个2级页表。同样的,2级页表里我们可以用2级索引指向一个1级页表。
而最后一层的1级页表里面的条目,对应的数据内容就是物理页号了。在拿到了物理页号之后,我们同样可以用“页号+偏移量”的方式,来获取最终的物理内存地址。
TLB加速地址转换
程序里面的每一个进程,都有一个属于自己的虚拟内存地址空间。我们可以通过地址转换来获得最终的实际物理地址。我们每一个指令都存放在内存里面,每一条数据都存放在内存里面。
“地址转换”是一个非常高频的动作,“地址转换”的性能就变得至关重要了。
多级页表让原本只需要访问一次内存的操作,变成了需要访问4次内存,才能找到物理页号。
程序所需要使用的指令,都顺序存放在虚拟内存里面。我们执行的指令,也是一条条顺序执行下去的。
于是,计算机工程师们专门在CPU里放了一块缓存芯片。这块缓存芯片我们称之为TLB,全称是地址变换高速缓冲(Translation-Lookaside Buffer)。这块缓存存放了之前已经进行过地址转换的查询结果。这样,当同样的虚拟地址需要进行地址转换的时候,我们可以直接在TLB里面查询结果,而不需要多次访问内存来完成一次转换。
TLB和我们前面讲的CPU的高速缓存类似,可以分成指令的TLB和数据的TLB,也就是ITLB和DTLB。
为了性能,我们整个内存转换过程也要由硬件来执行。在CPU芯片里面,我们封装了内存管理单元(MMU,Memory Management Unit)芯片,用来完成地址转换。和TLB的访问和交互,都是由这个MMU控制的。
I/O
我们先来看一个固态硬盘的Benchmark图:
“4K”的指标就是我们的程序,去随机读取磁盘上某一个4KB大小的数据,一秒之内可以读取到多少数据。
我们拿这个40MB/s和一次读取4KB的数据算一下。 40MB / 4KB = 10,000 也就是说,一秒之内,这块SSD硬盘可以随机读取1万次的4KB的数据。如果是写入的话呢,会更多一些,90MB /4KB 差不多是2万多次。
这个每秒读写的次数,我们称之为IOPS,也就是每秒输入输出操作的次数。
DTR(Data Transfer Rate,数据传输率)
我们在实际的应用开发当中,对于数据的访问,更多的是随机读写,而不是顺序读写。
诊断 I/O瓶颈
首先看一下CPU有没有在等待io操作。
# top
top - 06:26:30 up 4 days, 53 min, 1 user, load average: 0.79, 0.69, 0.65
Tasks: 204 total, 1 running, 203 sleeping, 0 stopped, 0 zombie
%Cpu(s): 20.0 us, 1.7 sy, 0.0 ni, 77.7 id, 0.0 wa, 0.0 hi, 0.7 si, 0.0 st
KiB Mem: 7679792 total, 6646248 used, 1033544 free, 251688 buffers
KiB Swap: 0 total, 0 used, 0 free. 4115536 cached Mem
wa的指标,这个指标就代表着iowait,也就是CPU等待IO完成操作花费的时间占CPU的百分比。
如果iowait很大,那么就可以去看看实际的I/O操作情况是什么样的。使用iostat,就能够看到实际的硬盘读写情况。
$ 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_wrtn/s指标,就对应着我们的数据传输率的指标。
使用iotop找出到底是哪一个进程是这些I/O读写的来源。
$ iotop
Total DISK READ : 0.00 B/s | Total DISK WRITE : 15.75 K/s
Actual DISK READ: 0.00 B/s | Actual DISK WRITE: 35.44 K/s
TID PRIO USER DISK READ DISK WRITE SWAPIN IO> COMMAND
104 be/3 root 0.00 B/s 7.88 K/s 0.00 % 0.18 % [jbd2/sda1-8]
383 be/4 root 0.00 B/s 3.94 K/s 0.00 % 0.00 % rsyslogd -n [rs:main Q:Reg]
1514 be/4 www-data 0.00 B/s 3.94 K/s 0.00 % 0.00 % nginx: worker process
硬盘
机械硬盘
一块机械硬盘是由盘面、磁头和悬臂三个部件组成的。
首先,自然是盘面(Disk Platter)。盘面其实就是我们实际存储数据的盘片。
我们的硬盘有5400转的、7200转的,乃至10000转的。这个多少多少转,指的就是盘面中间电机控制的转轴的旋转速度,英文单位叫RPM,也就是每分钟的旋转圈数(Rotations Per Minute)。
磁头:数据并不能直接从盘面传输到总线上,而是通过磁头,从盘面上读取到,然后再通过电路信号传输给控制电路、接口,再到总线上的。通常,我们的一个盘面上会有两个磁头,分别在盘面的正反面。
悬臂链接在磁头上,并且在一定范围内会去把磁头定位到盘面的某个特定的磁道(Track)上。
一个盘面通常是圆形的,由很多个同心圆组成,每一个同心圆都是一个磁道。每个磁道都有自己的一个编号。
一个磁道,会分成一个一个扇区(Sector)。上下平行的一个一个盘面的相同扇区呢,我们叫作一个柱面(Cylinder)。
读取数据,其实就是两个步骤。
- 把盘面旋转到某一个位置。在这个位置上,我们的悬臂可以定位到整个盘面的某一个子区间。
- 把我们的悬臂移动到特定磁道的特定扇区,也就在这个“几何扇区”里面,找到我们实际的扇区。找到之后,我们的磁头会落下,就可以读取到正对着扇区的数据。
进行一次硬盘上的随机访问,需要的时间由两个部分组成。
第一个部分,叫作平均延时(Average Latency)。这个时间,其实就是把我们的盘面旋转,把几何扇区对准悬臂位置的时间。这个时间很容易计算,它其实就和我们机械硬盘的转速相关。
随机情况下,平均找到一个几何扇区,我们需要旋转半圈盘面。上面7200转的硬盘,那么一秒里面,就可以旋转240个半圈。那么,这个平均延时就是:1s / 240 = 4.17ms
第二个部分,叫作平均寻道时间(Average Seek Time),也就是在盘面选转之后,我们的悬臂定位到扇区的的时间。我们现在用的HDD硬盘的平均寻道时间一般在4-10ms。
SSD硬盘
现在新的大容量SSD硬盘是由很多个裸片(Die)叠在一起的,就好像我们的机械硬盘把很多个盘面(Platter)叠放再一起一样,这样可以在同样的空间下放下更多的容量。
一张裸片上可以放多个平面(Plane),一般一个平面上的存储容量大概在GB级别。一个平面上面,会划分成很多个块(Block),一般一个块(Block)的存储大小, 通常几百KB到几MB大小。一个块里面,还会区分很多个页(Page),就和我们内存里面的页一样,一个页的大小通常是4KB。
对于SSD硬盘来说,数据的写入叫作Program。写入不能像机械硬盘一样,通过覆写(Overwrite)来进行的,而是要先去擦除(Erase),然后再写入。
SSD的读取和写入的基本单位,不是一个比特(bit)或者一个字节(byte),而是一个页(Page)。SSD的擦除单位必须按照块来擦除。
SSD的使用寿命,其实是每一个块(Block)的擦除的次数。
SLC的芯片,可以擦除的次数大概在10万次,MLC就在1万次左右,而TLC和QLC就只在几千次了。
SSD读写的生命周期
白色代表这个页从来没有写入过数据,绿色代表里面写入的是有效的数据,红色代表里面的数据,在我们的操作系统看来已经是删除的了。
一开始,所有块的每一个页都是白色的。随着我们开始往里面写数据,里面的有些页就变成了绿色。
然后,因为我们删除了硬盘上的一些文件,所以有些页变成了红色。但是这些红色的页,并不能再次写入数据。因为SSD硬盘不能单独擦除一个页,必须一次性擦除整个块,所以新的数据,我们只能往后面的白色的页里面写。这些散落在各个绿色空间里面的红色空洞,就好像硬盘碎片。
如果有哪一个块的数据一次性全部被标红了,那我们就可以把整个块进行擦除。它就又会变成白色,可以重新一页一页往里面写数据。
在快要没有白色的空页去写入数据的时候,SSD会做一次类似于Windows里面“磁盘碎片整理”或者Java里面的“内存垃圾回收”工作。找一个红色空洞最多的块,把里面的绿色数据,挪到另一个块里面去,然后把整个块擦除,变成白色,可以重新写入数据。
DMA
为什么要发明DMA技术?
就目前而言I/O速度如何提升,比起CPU,总还是太慢。如果我们对于I/O的操作,都是由CPU发出对应的指令,然后等待I/O设备完成操作之后返回,那CPU有大量的时间其实都是在等待I/O设备完成操作。
但是,这个CPU的等待,在很多时候,其实并没有太多的实际意义。我们对于I/O设备的大量操作,其实都只是把内存里面的数据,传输到I/O设备而已。
因此,计算机工程师们,就发明了DMA技术,也就是直接内存访问(Direct Memory Access)技术,来减少CPU等待的时间。
DMA有什么用?
本质上,DMA技术就是我们在主板上放一块独立的芯片。在进行内存和I/O设备的数据传输的时候,我们不再通过CPU来控制数据传输,而直接通过DMA控制器(DMA Controller,简称DMAC)。
当传输大量数据的时候,DMAC可以等数据到齐了,再发送信号,给到CPU去处理,而不是让CPU在那里忙等待。
DMAC是怎么控制数据传输的?
DMAC其实也是一个特殊的I/O设备,它和CPU以及其他I/O设备一样,通过连接到总线来进行实际的数据传输。总线上的设备呢,其实有两种类型。一种我们称之为主设备(Master),另外一种,我们称之为从设备(Slave)。
想要主动发起数据传输,必须要是一个主设备才可以,CPU就是主设备。而我们从设备(比如硬盘)只能接受数据传输。
DMAC它既是一个主设备,又是一个从设备。对于CPU来说,它是一个从设备;对于硬盘这样的IO设备来说呢,它又变成了一个主设备。
我们下面看一张图:
- 首先,CPU还是作为一个主设备,向DMAC设备发起请求。这个请求,其实就是在DMAC里面修改配置寄存器。
- CPU修改DMAC的配置的时候,会告诉DMAC这样几个信息:
-
首先是源地址的初始值以及传输时候的地址增减方式。
所谓源地址,就是数据要从哪里传输过来。如果我们要从内存里面写入数据到硬盘上,那么就是要读取的数据在内存里面的地址。
- 其次是目标地址初始值和传输时候的地址增减方式。
- 第三个是要传输的数据长度
-
- 设置完这些信息之后,DMAC就会变成一个空闲的状态(Idle)。
- 如果我们要从硬盘上往内存里面加载数据,这个时候,硬盘就会向DMAC发起一个数据传输请求。这个请求并不是通过总线,而是通过一个额外的连线。
- 然后,我们的DMAC需要再通过一个额外的连线响应这个申请。
- 于是,DMAC这个芯片,就向硬盘的接口发起要总线读的传输请求。数据就从硬盘里面,读到了DMAC的控制器里面。
- 然后,DMAC再向我们的内存发起总线写的数据传输请求,把数据写入到内存里面。
- DMAC会反复进行上面第6、7步的操作,直到DMAC的寄存器里面设置的数据长度传输完成。
- 数据传输完成之后,DMAC重新回到第3步的空闲状态。