我的博客:https://www.luozhiyun.com/

内存

内存是五大组成部分里面的存储器,我们的指令和数据,都需要先加载到内存里面,才会被CPU拿去执行。

我们的内存需要被分成固定大小的页(Page),然后再通过虚拟内存地址(Virtual Address)到物理内存地址(Physical Address)的地址转换(Address Translation),才能到达实际存放数据的物理内存位置。而我们的程序看到的内存地址,都是虚拟内存地址。

页表

想要把虚拟内存地址,映射到物理内存地址,最直观的办法,就是来建一张映射表。虚拟内存里面的页,到物理内存里面的页的一一映射。这个映射表,在计算机里面,就叫作页表(Page Table)。

页表这个地址转换的办法,会把一个内存地址分成页号(Directory)和偏移量(Offset)两个部分。

对于一个内存地址转换,其实就是这样三个步骤:

  1. 把虚拟内存地址,切分成页号和偏移量的组合;
  2. 从页表里面,查询出虚拟页号,对应的物理页号;
  3. 直接拿物理页号,加上前面的偏移量,就得到了物理内存地址;

多级页表(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)。

读取数据,其实就是两个步骤。

  1. 把盘面旋转到某一个位置。在这个位置上,我们的悬臂可以定位到整个盘面的某一个子区间。
  2. 把我们的悬臂移动到特定磁道的特定扇区,也就在这个“几何扇区”里面,找到我们实际的扇区。找到之后,我们的磁头会落下,就可以读取到正对着扇区的数据。

进行一次硬盘上的随机访问,需要的时间由两个部分组成。

第一个部分,叫作平均延时(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设备来说呢,它又变成了一个主设备。

我们下面看一张图:

  1. 首先,CPU还是作为一个主设备,向DMAC设备发起请求。这个请求,其实就是在DMAC里面修改配置寄存器。
  2. CPU修改DMAC的配置的时候,会告诉DMAC这样几个信息:
    • 首先是源地址的初始值以及传输时候的地址增减方式

      所谓源地址,就是数据要从哪里传输过来。如果我们要从内存里面写入数据到硬盘上,那么就是要读取的数据在内存里面的地址。

    • 其次是目标地址初始值和传输时候的地址增减方式
    • 第三个是要传输的数据长度
  3. 设置完这些信息之后,DMAC就会变成一个空闲的状态(Idle)。
  4. 如果我们要从硬盘上往内存里面加载数据,这个时候,硬盘就会向DMAC发起一个数据传输请求。这个请求并不是通过总线,而是通过一个额外的连线。
  5. 然后,我们的DMAC需要再通过一个额外的连线响应这个申请。
  6. 于是,DMAC这个芯片,就向硬盘的接口发起要总线读的传输请求。数据就从硬盘里面,读到了DMAC的控制器里面。
  7. 然后,DMAC再向我们的内存发起总线写的数据传输请求,把数据写入到内存里面。
  8. DMAC会反复进行上面第6、7步的操作,直到DMAC的寄存器里面设置的数据长度传输完成。
  9. 数据传输完成之后,DMAC重新回到第3步的空闲状态。

版权声明:本文为luozhiyun原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://www.cnblogs.com/luozhiyun/p/12384340.html