编辑推荐: |
本文主要介绍了Linux内存缓存:提升性能的关键相关内容。 希望对你的学习有帮助。
本文来自于微信公众号深度Linux,由火龙果软件Linda编辑、推荐。 |
|
在深入探索 Linux 系统的奇妙世界时,内存管理无疑是一个至关重要的领域。而在 Linux 内存体系中,Cache 扮演着举足轻重的角色。它就像是一位默默奉献的幕后英雄,为系统的高效运行立下汗马功劳。那么,Linux 内存中的 Cache 究竟是什么?它又有着怎样神奇的作用呢?让我们一同揭开它的神秘面纱。
一、Cache简介
在 Linux 内存中,Cache 起着至关重要的作用。Cache 存储器是位于 CPU 和主存储器之间的高速缓冲存储器,通常由速度极快的
SRAM(静态存储器)组成。它的规模相对较小,但速度却非常快;Cache 的主要功能是提高 CPU
数据输入输出的速率。由于内存速度通常较低,而 CPU 速度极快,两者之间存在巨大的速度差异。通过引入
Cache,系统可以在一定程度上缓解这种速度不匹配的问题。
当 CPU 需要访问数据时,会首先在 Cache 中查找。如果数据在 Cache 中命中,即找到了所需的数据,那么
CPU 可以直接从 Cache 中快速获取数据,大大减少了访问主存的时间。如果 Cache 未命中,CPU
则需要从主存中读取数据,并将其存入 Cache 中,以便下次访问时能够更快地获取。
以一个实际的例子来说明,假设我们的程序需要频繁访问某个特定的数据。如果没有 Cache,每次访问都需要从相对较慢的主存中读取,这将导致程序运行速度变慢。而有了
Cache 后,第一次访问该数据时可能需要从主存中读取,但随后的访问就可以直接从 Cache 中获取,极大地提高了程序的运行效率。
在 Linux 系统中,Cache 可以分为多级结构。一般来说,等级越高的 Cache 速度越慢,但容量越大。例如,L1
cache 速度最快,但容量较小;L2 cache 速度稍慢于 L1 cache,但容量更大;L3
cache 则在速度和容量上进一步扩展。这种多级 Cache 的设计可以更好地满足不同程序对数据访问速度和容量的需求。
二、Cache的工作方式
2.1与主存及 CPU 的交互
当 CPU 访问存储器时,会同时把地址(虚拟地址)发送给 MMU 中的 TLB 以及 Cache。CPU
会在 TLB 中查找最终的 RPN(Real Page Number),也就是真实的物理页面。若找到了,就会返回相应的物理地址。同时,CPU
通过 cache 编码地址中的 Index,也可以很快找到相应的 Cache line 组。但这个
cache line 中存储的数据不一定是 CPU 所需要的,所以需要进一步检查。如果 TLB 命中后,会返回一个真实的物理地址,将
cache line 中存放的地址和这个转换出来的物理地址进行比较,如果相同并且状态位匹配,那么就会发生
cache 命中。如果 cache miss,那么 CPU 就需要重新从存储器中获取数据,然后再将其存放在
cache line 中。
Cache 和主存之间以块为单位进行数据交换,如下图所示:
Cache 和 CPU 之间以字为单位进行数据交换:
2.2程序运行中的角色
在程序运行过程中,Cache 起着关键的作用。以程序运行流程为例,当我们需要运行一个进程的时候,首先会从磁盘设备(如
eMMC、UFS、SSD 等)中将可执行程序 load 到主存中,然后开始执行。在 CPU 内部存在一堆通用寄存器,如果
CPU 需要将一个变量加 1,一般分为三个步骤:CPU 从主存中读取地址的数据到内部通用寄存器;通用寄存器进行加
1 操作;CPU 将通用寄存器的值写入主存。然而,现实中 CPU 通用寄存器的速度和主存之间存在着太大的差异。例如,CPU
register 的速度一般小于 1ns,主存的速度一般是 65ns 左右,速度差异近百倍。所以,步骤
1 和步骤 3 实际上速度很慢。当 CPU 试图从主存中 load/store 操作时,由于主存的速度限制,CPU
不得不等待这漫长的时间。而有了 Cache 后,当 CPU 试图从主存中加载 / 存储数据的时候,CPU
会首先从 cache 中查找对应地址的数据是否缓存在 cache 中。如果数据缓存在 cache 中,那么直接从
cache 中拿到数据并返回给 CPU。这样就大大提高了程序的运行效率。
我们先思考一个问题:我们的程序是如何运行起来的?
我们应该知道,程序是运行在RAM之中,RAM就是我们常说的DDR,我们称之为main memory(主存)。当我们需要运行一个进程的时候,首先会从磁盘设备(eMMC,UFS,SSD等)中将可执行程序load到主存中,然后开始执行。在CPU内部存在一堆通用寄存器,如果CPU需要将一个变量(假设地址是A)加1,一般分为下面上个步骤:
CPU从主存中读取地址A的数据到内部通用寄存器x0
通用寄存器x0加1
CPU将通用寄存器x0的值写入主存。
但是在现实中,CPU通用寄存器的速度和主存之间存在着太大的差异。
因此,上面举例的三个步骤中,步骤1和步骤3实际上速度很慢(和主存相关)。当CPU试图从主存中load/store操作时,由于主存的速度限制,CPU不得不等待这漫长的65ns时间。如果我们可以提升主存的速度,那么系统将会获得很大的性能提升。
但是如今的DDR存储设备,动不动就是几个GB,如果我们采用更快的材料制作更快速度的主存,并且拥有几乎差不多的容量,那么成本将会大幅度上升,又想要大容量,又想要高速率,还想要低成本,这根本就是一个鱼和熊掌不可兼得的问题。所以,我们有了一种折中的方法,那就是制作一块速度极快,但是容量极小的存储设备,我们称之为cache
memory。
在硬件上,我们将cache放置 在CPU和主存之间,作为主存数据的缓存。当CPU试图从主存中加载/存储数据的时候,CPU会首先从cache中查找对应地址的数据是否缓存在cache中,如果数据缓存在cache中,那么直接从cache中拿到数据并返回给CPU。当存在cache的时候,以上程序运行流程就变成了如下:
CPU和主存之间直接数据传输的方式转变成了CPU和cache之间直接数据传输。cache负责和主存之间的数据传输。
2.3多级 cache 存储结构
为了进一步提升性能,引入了多级 cache。前面提到的 cache,称为 L1 cache(第一级
cache)。在 L1 cache 后面连接 L2 cache,在 L2 cache 和主存之间连接
L3 cache。等级越高,速度越慢,容量越大。例如,一般情况下 cache 的速度可以达到 1ns,几乎可以和
CPU 寄存器速度媲美。但当 cache 中没有缓存我们想要的数据的时候,依然需要漫长的等待从主存中
load 数据。通过多级 cache 的设计,可以更好地解决速度与容量的矛盾。首先引入两个名字概念,命中和缺失。CPU
要访问的数据在 cache 中有缓存,称为命中,反之称为缺失。当 CPU 试图从某地址载入数据时,首先从
L1 cache 中查询是否命中,如果命中则把数据返回给 CPU,如果 L1 cache 缺失,则继续从
L2 cache 中查找。当 L2 cache 命中时,数据会返回给 L1 cache 及 CPU。如果
L2 cache 中也缺失,就需要从主存中加载数据,将数据返回给 L2 cache、L1 cache
和 CPU。这种多级 cache 的工作方式称为 inclusive cache(某一地址的数据可能存在多级缓存中)。与
inclusive cache 对应的是 exclusive cache,这种 cache 保证某一地址的数据缓存只会存在于多级
cache 的其中一级,也就是说任意地址的数据不可能同时在 L1 和 L2 cache 中。
三、Cache的结构与判断命中方式
3.1基本结构组成
Cache 由组、路和 cache line 组成。组是相同索引域的 cache line 组成的一个集合;路是在组相连结构的
cache 中,cache 被分成的相同大小的块;cache line 是 cache 中最小的访问单元。
Cache 的基本参数如下:
导出的基本参数如下:
Cache 的地址编码分为三部分,分别是偏移域(offset)、索引域(index)和标记域(tag)。偏移域用于查找某一个
cache line 中的具体字节;索引域用于查找数据是在 cache 中的哪一个组中;标记域用于判断
cache line 中存放的数据是否和处理器想要的一致,每一个 cache line 都有它唯一的一个
tag 值。
Cache 的整体结构如下:
如下图所示,这个cache中有4路(块),每一路有4个cache line,总共有4组。
组:相同索引域的cache line组成一个组
路:在组相连结构的cache中,cache被分成几个相同大小的块
cache line:cache line是cache中最小的访问单元
除了上面的内容,cache中还有几个概念,是由于查询cache是否命中的重要概念:
cache地址编码:处理器访问cache,就需要靠cache的地址编码,分成三部分,分别是偏移域(offset)、索引域(index)和标记域(tag)
偏移域(offset):用于查找某一个cache line中的具体字节
索引域(index):用于查找数据是在cache中的哪一个组中
标记(tag):用于判断cache line中存放的数据是否和处理器想要的一致,每一个cache
line都有它唯一的一个tag值。
例如,在一个 32KB 的 4 路组相连的 cache 中,每一路的大小就是 cache 的总大小除以路数,即
32KB/4 = 8KB。每一路包含的 cache line 数目等于块大小除以 cache line
大小,假设 cache line 为 32Byte,那么每一路包含的 cache line 数目为
8KB/32Byte = 256。在 cache 编码地址中,bit [4:0] 用于选择 cache
line 中的数据,其中 bit [4:2] 可以用于寻址 8 个字,bit [1:0] 可以用于寻址每个字中的字节。bit
[12:5] 用于索引(Index)选择每一路上 cache line,其余的 bit [31:13]
用作标记位(Tag)。
3.2判断命中方法
如果CPU从0x0654地址读取一个字节,cache控制器是如何判断数据是否在cache中命中的呢?我们如何根据地址在有限大小的cache中查找数据呢?
如果 CPU 从 0x0654 地址读取一个字节,cache 控制器会通过地址编码中的索引域和标记域来判断数据是否在
cache 中命中。首先,利用地址中的索引域找到对应的 cache line 组。然后,取出当前 cache
line 对应的 tag,与地址中的 tag 进行比较。如果相等,就说明 cache 命中了,否则就是缺失。
以一个有 8 行 cache line,cache line 大小是 8 Bytes 的例子来说明。可以利用地址低
3 bits(蓝色部分)用来寻址 8 bytes 中某一个字节,这部分 bit 组合为 offset。同理,为了覆盖所有
cache line,需要 3 bits(黄色部分)查找某一行,这部分地址称为 index。当两个地址的
bit3、bit4、bit5 的值完全一样,那么说明这两个地址都会找到同一个 cache line。但是,找到
cache line 之后,只代表我们访问的地址对应的数据可能存在这个 cache line 中。所以,引入了
tag array 区域。每一个 cache line 都对应唯一的一个 tag,tag 中保存的是整个地址位宽减去
index 和 offset 的长度。这样 tag、index、offset 三者组合就可以确定唯一的地址了。
如上图,我们一共有8行cache line,cache line大小是8 Bytes。所以我们可以利用地址低3
bits(蓝色部分)用来寻址8 bytes中某一个字节,我们称这部分bit组合为offset。同理,为了覆盖所有cache
line,我们需要3 bits(黄色部分)查找某一行,这部分地址称为index。所以,当两个地址的bit3、bit4、bit5的值完全一样,那么说明这两个地址都会找到同一个cache
line,所以,当我么找到了cache line之后,只代表我们访问的地址对应的数据可能存在这个cache
line中。所以,我们又引入了tag array区域。每一个cache line都对应唯一的一个tag,tag中保存的是整个地址位宽减去index和offset的长度。这样tag、index、offset三者组合就可以确定唯一的地址了。
所以,当我们根据地址中index位找到cache line后,取出当前cache line对应的tag,然后和地址中的tag进行比较,如果相等,就说明cache命中了,否则就是缺失。上面我们提出了一个问题,为什么硬件cache
line不做成一个字节?这样会导致硬件成本的上升,原本8字节对应一个tag,现在需要8个tag,占用了很多内存。
3.3cache的映射方式
⑴直接映射缓存
直接映射的方式比较简单,我们先看一张图:
我们把整个cache只分成一个块,那么一个组就只有一个cache line,这种方式就叫做直接映射方式。
上面那个图,假设cache只有4个cache line。那么直接映射的地址就是0x0到0x30,这段内存地址直接映射到cache中。如果cpu要访问0x40到0x70,那么又会把0x40到0x70的地址直接映射到cache里,这个时候,0x0到0x30这段内存地址的数据就需要从cache里面踢出去,否则0x40到0x70的地址就没办法映射。cache的直接映射有可能会发生严重的高速缓存颠簸,性能会很差。
void
add_array(int *data1, int *data2, int result,
int size)
{
int i;
for (i == 0; i < size; i++) {
result = data1[i] + data2[i];
}
} |
假使上面的程序,result、data1、data2分别指向0x00,0x40和0x80的地址,并且它们都会使用到同一个cache
line
第一次读取data1的值即0x40地址的值时,由于不在cache里面,所以读取从0x40到0x4f地址的数据填充到cache里面
当读取data2的值即0x80地址的值时,由于不在cache里面,所以需要读取0x80到0x8f地址的数据填充到cache里面,由于之前cache里面已经保存了0x40地址的数据,所以数据发生了替换
当data1和data2相加后,需要把结果赋值给result,写入到0x00中,这时cache又发生了替换
所以当cache使用直接映射缓存时,会发生严重的cache颠簸(不断发生cache line替换),严重影响性能。
⑵全关联
当cache只有一个组,即主存中只有一个地址与n个cache line对应,称为全关联
⑶组相连
直接映射他的缺点就是在有些情况下会导致cache的“颠簸”。所以后来就想到了让同一个index的缓存行对应到两个缓存行上就形成了cache的组的概念,即index相同的cache称为一个组(set),这样一个index就会对应到组宽度n个cache-line,如果n为2则前面颠簸换出的概念就是原来的1/n即0.5这就是组映射。既然同一个index对应到不止一个cacae-line所以就会加上TAG的概念来区分同一个组的cache-line。组映射这里最后还有一个概念就是way,他就是一个cache所能映射的index区域,即如果index有5bit则一个way就是五个cache-line如下示意图所示的:
3.4虚拟cache和物理cache
⑴物理cache
当处理器查询MMU和TLB得到物理地址之后,只用物理地址去查询高速缓存,我们称为物理高速缓存。使用物理高速缓存的缺点就是处理器在查询MMU和TLB之后才能够访问高速缓存,增加了延迟。
⑵虚拟cache
CPU使用虚拟地址来寻址高速缓存,我们成为虚拟高速缓存。处理器在寻址时,首先把虚拟地址发送到高速缓存中,若在高速缓存里找到需要的数据,那么就不再需要访问TLB和物理内存。处理器在寻址时,首先把虚拟地址发送到高速缓存中,若在高速缓存里找到需要的数据,那么就不再需要访问TLB和物理内存。
3.5cache的分类
在查询cache的时候使用了index和tag,那么查询cache时用的是虚拟地址还是物理地址的index?当找到cache组的时候,我们用的是虚拟地址还是物理地址的tag来匹配cache
line呢?
cache可以设计成通过虚拟地址来访问,也可以设计成通过物理地址来访问,这个在CPU设计时就确定下来了,并且对cache的管理有很大的影响。cache可以分成以下三类:
VIVT(Virtual Index Virtual Tag) : 使用虚拟地址的index和tag,相当于虚拟高速缓存
PIPT(Physical Index Physical Tag) : 使用物理地址的index和tag,相当于物理高速缓存
VIPT(Virtual Index Physical Tag) : 使用虚拟地址的index和物理地址的tag
在早期的ARM处理器中采用的是VIVT的方式,不经过MMU的翻译,直接使用虚拟地址的index和tag来查找cache
line,这种方式会导致cache别人的问题。也就是一个物理地址的内容可能出现在多个cache line中,当系统改变了虚拟地址到物理地址的映射时,需要清洗和无效这些cache,导致系统性能下降
3.6VIPT的工作原理
现在很多cortex系列的处理器的L1 data cache采用VIPT方式,即CPU输出的虚拟地址同时会发送到TLB/MMU单元进行地址翻译,以及在高速缓存中进行索引和查询高速缓存。在TLB/MMU单元里,会把虚拟页帧号(VPN)翻译成物理页帧号(PFN),与此同时,虚拟地址的索引域和偏移会用来查询高速缓存。这样高速缓存和TLB/MMU可以同时工作,当TLB/MMU完成地址翻译后,再用物理标记域来匹配高速缓存行。采用VIPT方式的好处之一是在多任务操作系统中,修改了虚拟地址到物理地址映射关系,不需要把相应的高速缓存进行无效操作。
⑴VIVT(虚拟高速缓存)造成的重名同名问题
重名问题:(不同虚拟地址指向相同的物理地址)
重名问题是怎么产生的呢?我们知道,在操作系统中,多个不同的虚拟地址有可能映射相同的物理地址。由于采用VIPI架构,那么这些不同的虚拟地址会占用高速缓存中不同的高速缓存行(cache
line),但是它们对应的是相同的物理地址,这样会引发问题:一是浪费了高速缓存空间,造成高速缓存等效容量的减少,减低整体性能;第二,在执行写操作的时候,只更新其中一个虚拟地址对应的高速缓存,而其他虚拟地址对应的高速缓存并没有更新。那么处理器访问其他虚拟地址可能得到旧数据。
举个例子,比如我们的cache使用的是VIPI,VA1映射到PA,VA2也映射到PA,那么在cache中有可能同时缓存了VA1和VA2两个虚拟地址。当程序往VA1虚拟地址写入数据的时候,PA的内容会被更改,但是虚拟地址VA2对应的cache里面还保存着旧数据,当CPU去读取VA2的值时,读到就是旧地址。一个物理地址在VIPI中就保存了两份数据,这样会产生歧义。
同名问题:(相同的虚拟地址指向不同的物理地址)
同名问题是怎么产生的呢?同名问题指的是相同的虚拟地址对应着不同的物理地址。因为操作系统中不同的进程会存在很多相同的虚拟地址,而这些相同的虚拟地址在经过MMU转换后得到不同的物理地址,这样就产生了同名问题。
同名问题最常出现的地方就是进程切换。当一个进程切换到另一个进程时,新进程使用虚拟地址来访问cache的话,新进程会访问到旧进程遗留下来的高速缓存,这些高速缓存数据对于新进程来说是错误和没用的,解决办法就是在进程切换时把旧进程遗留下来的高速缓存都设置为无效,这样就能保证新进程执行时得到一个干净的虚拟高速缓存。同样,TLB也需要设置为无效,因为新进程在切换后得到一个旧进程使用的TLB,里面存放了旧进程和虚拟地址到物理地址的转换结果。
重名问题实际上是多个虚拟地址映射到同一个物理地址引发的歧义问题,而同名问题是一个虚拟地址可能因为进程切换等原因映射到不同的物理地址而引发的问题。
⑵VIPT的重名问题
采用VIPT方式也有可能导致高速缓存别人的问题,使用虚拟地址的index来查找高速缓存的cache
line,这时有可能导致多个高速缓存组映射到同一个物理地址上,以Linux内核为例,它是以4KB大小为一个页面进行管理的,那么对于一个页来说,虚拟地址和物理地址的低。
3.7cache一致性
⑴什么是cache一致性?
cache一致性,需要保证系统中所有的CPU、所有的bus主从,例如GPU、DMA等,他们观察到的内存是一直的。举个例子,外设都用DMA,如果你的软件通过CPU来产生一些数据,然后相通过DMA来搬移这些数据到外设,如果CPU和DMA看到的数据不一致,比如CPU产生的数据还在cache里,而DMA却从内存中直接去搬移数据,那么DMA就会看到一个旧的数据,那么就产生了数据的不一致性。
一般情况下实现系统cahce一致性有三种方案:
①关闭cache
这是最简单的办法,但是它会严重影响性能。以上面那个例子为例,CPU产生数据,然后把数据先放入到DMA
buffer里,如果采用关闭cache的方式,那么CPU在产生数据的过程中,CPU不能利用cache,这就会严重影响到性能
②软件管理cache一致性
这是最常用的方式。软件需要在合适的时候去clean or flush dirty cache,或者invalidata
old data
优点:硬件RTL实现简单
缺点:软件复杂度增加,软件需要手动clean/flush cache或者invalidate cache
增加调试难度
降低性能,增加功耗
③硬件管理cache一致性
对于多核之间的cache一致性,通常的做法就是在多核里实现一个MESI协议,实现一种snoop的控制单元。
⑵MESI协议
目前,ARM或者x86等处理器广泛使用MESI协议来维护高速缓存一致性。MESI协议的名字源于该名字使用修改(Modified,
M),独占(Exclusive, E),共享(Shared,S)和失效(Invalid, I)四个状态。高速缓存中的状态比如是上述四个状态中的一个。MESI状态机的转换是硬件自动实现的
高速缓存行(cache line)中有两个标志:脏(dirty)和干净(valid)。它们很好地描述了高速缓存和内存之间的数据关系,如数据是否有效、数据是否被修改过。在MESI协议中,每个高速缓存行有四个状态,可以使用高速缓存行中的2位地址来表示这些状态(00
01 10 11)。
⑶MESI的操作
总线读 总线侦听到一个来自其他CPU的读缓存请求。收到信号的CPU先检查自己的高速缓存中是否有缓存该数据,然后广播应答信号
总线写 总线侦听到一个来自其他CPU的写缓存请求。收到信号的CPU先检查自己的高速缓存中是否有缓存该数据,然后广播信号
总线更新 总线侦听到更新请求,请求其他CPU做一些额外事情。其他CPU收到请求后,若CPU上有缓存副本,则需要做额外一些更新操作,比如无效本地的高速缓存行等
刷新 总线侦听到刷新请求。收到请求的CPU把自己的高速缓存行的内容写回到主内存中
刷新到总线 收到该请求的CPU会把高速缓存行内容发送到总线上,这样发送请求的CPU就可以获取到这个高速缓存行的内容
四、Cache的产生与释放
4.1读写文件与元信息
在 Linux 系统中,Cache 的产生与文件的读写以及元信息的处理密切相关。通过实验可以清晰地看到这种关系。例如,当进行文件写操作时,我们可以观察到
Cache 在不停地增长,而 Buffer 基本保持不变。以生成一个 500MB 大小的文件为例,在执行
dd if=/dev/urandom of=/tmp/file bs=1M count=500 命令时,通过观察
vmstat 的输出,我们发现 Cache 的增长是由于文件内容被缓存起来,以便后续可能的读取操作能够更快地响应。而
Buffer 在这个过程中基本保持不变,说明在文件写操作中,Buffer 并不是主要的缓存角色。
在文件读操作中,同样可以看到 Cache 的重要作用。当执行 dd if=/tmp/file of=/dev/null
命令读取文件数据时,观察 vmstat 的输出,会发现读取文件时(也就是 bi 大于 0 时),Buffer
保持不变,而 Cache 则在不停增长。这说明文件的读写都要经过 Cache,Cache 会缓存从文件读取的数据,以便下次访问时能够快速获取。
对于文件元信息,如文件的属性信息、目录结构等,也会被缓存到 Cache 中。这样,当需要访问文件元信息时,系统可以直接从
Cache 中获取,提高了文件管理的效率。
4.2清理与释放机制
在 Linux 系统中,可以通过特定的命令来清理和释放 Cache。其中,常用的方法是使用 echo
3 > /proc/sys/vm/drop_caches 命令。这个命令会清理文件页、目录项、Inodes
等各种缓存。
其原理是通过写入特定的值到 /proc/sys/vm/drop_caches 文件,触发内核的缓存清理机制。具体来说,写入
1 表示清除 pagecache(页面缓存),即缓存从磁盘读取的文件数据;写入 2 表示清除回收 slab
分配器中的对象,包括目录项缓存和 inode 缓存;写入 3 表示清除 pagecache 和 slab
分配器中的缓存对象。
在执行清理缓存的命令之前,最好先执行 sync 命令,以防止数据丢失。sync 命令会将内存中的数据强制写入磁盘,确保数据的一致性。
需要注意的是,清理 Cache 可能会导致系统在短时间内性能下降,因为后续的文件访问需要重新从磁盘读取数据。但是,在某些情况下,如内存紧张或者需要确保数据的一致性时,清理
Cache 是必要的操作。
五、Cache与Buffer的区别与联系
5.1功能差异
Cache 主要用于加速 “读” 操作,它保存从磁盘上读出的数据,或者是 CPU 刚用过的数据或循环使用的部分数据,目的是减少
CPU 等待数据的时间,提高系统性能。例如,在现代计算机中,CPU 通常拥有多级 Cache,L1
Cache 用于存储最常用的数据和指令,L2 Cache 存储次频繁访问的数据,L3 Cache 存储更大规模的数据。当
CPU 需要访问数据时,会首先在 Cache 中查找,如果命中则可以快速获取数据,大大提高了系统的响应速度。
而 Buffer 主要用于缓冲 “写” 操作,它是一个临时存储区域,用于临时保存数据,以便进行高效的输入
/ 输出操作。Buffer 通常用于写入磁盘的操作,例如,当某个进程要求多个字段被读入,在所有字段被读入之前已经读入的字段会先放到
buffer 中,待所有字段读入后再一次性写入磁盘,以减少 I/O 操作,提高效率。
5.2应用场景
在文件读写场景中,Cache 主要发挥加速读取文件的作用。当多个进程要访问某个文件时,可以把此文件读入
Cache 中,这样下一个进程获取 CPU 控制权并访问此文件直接从 Cache 读取,提高系统性能。例如,在数据库系统中,经常访问的数据可以被缓存在
Cache 中,以加快查询相同数据的速度,避免频繁地从磁盘或网络中读取数据,提高响应时间。
而在文件读写中,Buffer 则在写入文件时发挥重要作用。数据首先从磁盘读入 Buffer,再从
Buffer 写入磁盘,可以减少磁盘的访问次数,提高数据传输速度。在网络通信场景中,Buffer 可以将接收到的数据暂存起来,当接收到足够多的数据或达到一定条件时再进行处理和响应,以平衡数据传输速度。
在磁盘 IO 场景中,Cache 可以缓存磁盘上的文件数据,减少对磁盘的访问次数,提高系统性能。例如,在存储系统中,文件系统有
cache,存储有 cache,RAID 控制器上有 cache,磁盘上也有 cache。为了提高性能,Oracle
的一个写操作,很有可能写在存储的 cache 上就返回了。
而 Buffer 在磁盘 IO 中主要用于缓冲写入操作。例如,在写磁盘时,虽然 buffer 和
cache 都在增长,但是明显 buffer 的增长幅度更大。这与 Buffer 在写磁盘中起作用相符,它将分散的写操作集中进行,减少磁盘碎片和硬盘的反复寻道,从而提高系统性能。
六、Cache对系统性能的影响
6.1占用过大的影响
当 Cache 占用过大时,会对系统性能产生多方面的负面影响。
首先,内存压力会显著增加。在 Linux 系统中,Cache 是存储最近使用的文件和数据的机制,当它占用过大时,系统的可用内存会减少。这可能导致系统在处理其他任务时面临内存不足的情况。例如,当一个内存需求较大的应用程序启动时,可能由于可用内存不足而无法正常运行,甚至可能导致系统变得缓慢,严重时出现内存溢出的情况。根据相关数据统计,当
Cache 占用超过系统总内存的一定比例(如 50%)时,系统出现内存不足问题的概率会大幅增加。
其次,磁盘 IO 负载也会加重。当 Cache 占用过大时,系统可能会频繁地将数据从内存中写回到磁盘,以释放内存空间。这会增加磁盘的
IO 负载,导致磁盘响应变慢。例如,在一个频繁进行文件读写操作的系统中,如果 Cache 占用过大,磁盘的平均响应时间可能会从几十毫秒增加到几百毫秒甚至更长,进而影响系统的整体性能。
最后,进程性能会下降。当 Cache 占用过大时,系统可能需要频繁地从内存中清理 Cache,以释放内存空间给其他进程使用。这会导致系统中的进程性能下降,因为它们需要等待
Cache 清理完成才能继续执行。例如,在一个多进程的服务器环境中,如果 Cache 占用过大,进程的平均响应时间可能会增加
20% 至 30%。
6.2优化性能的关键
提高数据缓存命中率是充分发挥 Cache 对提升系统性能积极作用的关键。
一方面,可以通过数据局部性优化来提高缓存命中率。空间局部性优化要求优化数据访问模式,使得数据访问在空间上连续。比如,通过循环展开和数据重排,使得程序在访问数据时更有可能命中
Cache。例如,在处理一个大型数组时,按照数组在内存中的连续布局顺序访问元素,可以大大提高缓存命中率。时间局部性优化则确保数据在被访问后不久再次被访问,比如通过循环重排和数据重用。如果一个程序频繁地访问某些特定的数据,通过合理安排程序的执行流程,使得这些数据在短时间内被多次访问,可以提高缓存命中率。
另一方面,合理选择缓存替换策略也能提高缓存利用率。常见的缓存替换策略有 LRU(最近最少使用)、LFU(最少频率使用)或
ARC(自适应替换缓存)等。例如,在一个 Web 服务器环境中,如果采用 LRU 策略,当 Cache
空间不足时,会优先替换最近最少使用的网页内容,这样可以保证经常被访问的网页内容始终在 Cache 中,提高用户访问网页的响应速度。
此外,调整缓存大小也是优化性能的重要手段。根据应用需求和系统资源,调整缓存大小。较大的缓存可以提高命中率,但也可能增加缓存未命中时的延迟。例如,在一个数据库服务器中,根据数据库的大小和访问模式,合理调整缓存大小,可以显著提高数据库查询的性能。
通过以上方法,可以显著提高 Cache 的命中率,从而提升整个系统的运行效率。
|