Linux虚拟内存、buffer、cache、缓存命中率、缓存文件大小
物理内存也称 为主存,大多数计算机用的主存都是动态随机访问内存(DRAM)。只有内核才可以直接访问物理内存。
虚拟内存
Linux 内核给每个进程都提供了一个独立的虚拟地址空间,并且这个地址空间是连续的。进程就可以很方便访问虚拟内存。虚拟地址空间的内部又被分为内核空间和用户空间两部分,不同字长(也就是单个 CPU 指令可以处理数据的最大长度)的处理器,地址空间的范围也不同。比如最常见的 32 位系统

- 32 位系统的内核空间占用 1G,位于最高处,剩下的 3G 是用户空间。
- 64 位系统的内核空间和用户空间都是 128T,分别占据整个内存空间的最高和最低处,剩下的中间部分是未定义的。
进程在用户态时,只能访问用户空间内存;
进入内核态后,才可以访问内核空间内存。
虽然每个进程的地址空间都包含了内核空间,但这些内核空间关联的都是相同的物理内存。进程切换到内核态后,就可以很方便地访问内核空间内存。
内存映射
并不是所有的虚拟内存都会分配物理内存,只有那些实际使用的 虚拟内存才分配物理内存,并且分配后的物理内存,是通过内存映射来管理的。
内存映射,其实就是将虚拟内存地址映射到物理内存地址。
为了完成内存映射,****内核为每个进程都维护了一张页表,记录虚拟地址与物理地址的映射关系。页表实际上存储在 CPU 的内存管理单元 MMU 中,这样,正常情况下,处理器就可以直接 通过硬件,找出要访问的内存。

当进程访问的虚拟地址在页表中查不到时,系统会产生一个缺页异常,进入内核空间分配 物理内存、更新进程页表,最后再返回用户空间,恢复进程的运行。
TLB(Translation Lookaside Buffer, 转译后备缓冲器)会影响 CPU 的内存访问性能。 TLB 其实就是 MMU 中页表的高速缓存。由于进程的虚拟地址空间是独立的,而 TLB 的访问速度又比 MMU 快得多,所以,通过减少进程的上下文切换,减少 TLB 的刷新次数,就可以提高 TLB 缓存的使用率,进而提高 CPU 的内存访问性能。
MMU 并不以字节为单位来管理内存,而是规定了一个内存映射的最小单位, 也就是页,通常是 4 KB 大小。这样,每一次内存映射,都需要关联 4 KB 或者 4KB 整数倍 的内存空间。
为什么需要多级页表?
页的大小只有 4 KB ,导致的另一个问题就是,整个页表会变得非常大。比方说,仅 32 位 系统,2的32次幂可以表示的地址是4GB,所以内存最大4GB,而这么大内存可以放下的页面是4GB/4KB = 100 多万个页,也就是MMU 需要保存一百多万页面的页表项,才可以实现整个地址空间的映射。
多级页表解决页表项过多
多级页表就是把内存分成区块来管理,将原来的映射关系改成区块索引和区块内的偏移。由于虚拟内存空间通常只用了很少一部分,那么,多级页表就只保存这些使用中的区块,这样 就可以大大地减少页表的项数。
Linux 用的正是四级页表来管理内存页,虚拟地址被分为 5 个部分,前 4 个表项用于选择页,而最后一个索引表示页内偏移。

大页HugePage解决页表项过多
比普通页更大的内存块,常见的大小有 2MB 和 1GB。大页通常用在使用大量内存的进程上,比如 Oracle、DPDK 等。
虚拟内存空间分布
用户空间内存,从低到高分别是五种不同的内存段。
-
只读段,包括代码和常量等。
-
数据段,包括全局变量等。
3. 堆,包括动态分配的内存,从低地址开始向上增长。
4. 文件映射段,包括动态库、共享内存等,从高地址开始向下增长。
- 栈,包括局部变量和函数调用的上下文等。栈的大小是固定的,一般是 8 MB。
注意内存的地址,下面是低地址,上面是高地址

堆和文件映射段的内存是动态分配的。比如说,使用 C 标准库的 malloc() 或者 mmap() ,就可以分别在堆和文件映射段动态分配内存。
内存分配策略
malloc() 是 C 标准库提供的内存分配函数,对应到系统调用上,有两种实现方式,即 brk() 和 mmap()。
- 对小块内存(小于 128K),C 标准库使用 brk() 来分配,也就是通过移动堆顶的位置来分配内存。这些内存释放后并不会立刻归还系统,而是被缓存起来,这样就可以重复使用。
- brk() 方式的缓存,可以减少缺页异常的发生,提高内存访问效率。不过,由于这些内存没 有归还系统,在内存工作繁忙时,频繁的内存分配和释放会造成内存碎片。
- 大块内存(大于 128K),则直接使用内存映射 mmap() 来分配,也就是在文件映射段找一块空闲内存分配出去。
- mmap() 方式分配的内存,会在释放时直接归还系统,所以每次 mmap 都会发生缺页异常。在内存工作繁忙时,频繁的内存分配会导致大量的缺页异常,使内核的管理负担增大。 这也是 malloc 只对大块内存使用 mmap 的原因。
当这两种调用发生后,其实并没有真正分配内存。这些内存,都只在首次访问时才分配,也就是通过缺页异常进入内核中,再 由内核来分配内存。
整体来说,Linux 使用伙伴系统来管理内存分配。这些内存在 MMU 中 以页为单位进行管理,伙伴系统也一样,以页为单位来管理内存,并且会通过相邻页的合并,减少内存碎片化(比如 brk 方式造成的内存碎片)。
如果遇到比页更小的对象,比如不到 1K 的时候,实际系统运行中,确实有大量比页还小的对象,如果为它们也分配单独的页,那就太浪费内 存了。在用户空间,malloc 通过 brk() 分配的内存,在释放时并不立即归还系统,而是缓 存起来重复利用。在内核空间,Linux 则通过 slab 分配器来管理小内存。你可以把 slab 看 成构建在伙伴系统上的一个缓存,主要作用就是分配并释放内核中的小对象。
对内存来说,如果只分配而不释放,就会造成内存泄漏,甚至会耗尽系统内存。所以,在应 用程序用完内存后,还需要调用 free() 或 unmap() ,来释放这些不用的内存。
内存回收策略--内存不足应对
在发现内存紧张时,系统就会通过一系列机 制来回收内存,比如下面这三种方式:
- 回收缓存,比如使用 LRU(Least Recently Used)算法,回收最近使用最少的内存页 面;
- 回收不常访问的内存,把不常用的内存通过交换分区直接写到磁盘中;
- Swap 其 实就是把一块磁盘空间当成内存来用。它可以把进程暂时不用的数据存储到磁盘中(这个过 程称为换出),当进程访问这些内存时,再从磁盘读取这些数据到内存中(这个过程称为换 入)。
- 通常只在内存不足时,才会发生 Swap 交换。
- 由于磁盘读写的速度远比内存慢,Swap 会导致严重的内存性 能问题。
- 杀死进程,内存紧张时系统还会通过 OOM(Out of Memory),直接杀掉占用大量内 存的进程。
- OOM(Out of Memory),其实是内核的一种保护机制。它监控进程 的内存使用情况,并且使用 oom_score 为每个进程的内存使用情况进行评分:一个进程消耗的内存越大,oom_score 就越大;
- 一个进程运行占用的 CPU 越多,oom_score 就越小。
- 进程的 oom_score 越大,代表消耗的内存越多,也就越容易被 OOM 杀死,从而可 以更好保护系统。
- 管理员可以通过 /proc 文件系统,手动设置进程的 oom_adj ,从而调整进程的 oom_score。oom_adj 的范围是 [-17, 15],数值越大,表示进程越容易被 OOM 杀死;数值越小,表示 进程越不容易被 OOM 杀死,其中 -17 表示禁止 OOM。
# 把 sshd 进程的 oom_adj 调小为 -16,这样, sshd 进程就 不容易被 OOM 杀死。
echo -16 > /proc/$(pidof sshd)/oom_adj
如何查看内存使用情况
free 整个系统的内存使用情况
free 输出的是一个表格,其中的数值都默认以字节为单位。两行分别是物理内存 Mem 和交换分区 Swap 的使用情况

- 第一列,total 是总内存大小;
- 第二列,used 是已使用内存的大小,包含了共享内存;
- 第三列,free 是未使用内存的大小;
- 第四列,shared 是共享内存的大小;
- 第五列,buff/cache 是缓存和缓冲区的大小;
- 最后一列,available 是新进程可用内存的大小。 最后一列的可用内存 available 。不仅包含未使用内存,还包括了可回收的缓存,所以一般会比未使用内存更大。不过,并不是所有缓存都可以回收, 因为有些缓存可能正在使用中。
top / ps进程的内存使用情况
按下 M 切换到内存排序

- VIRT 是进程虚拟内存的大小,只要是进程申请过的内存,即便还没有真正分配物理内 存,也会计算在内。
- RES 是常驻内存的大小,也就是进程实际使用的物理内存大小,但不包括 Swap 和共享 内存。
- SHR 是共享内存的大小,比如与其他进程共同使用的共享内存、加载的动态链接库以及 程序的代码段等。
- %MEM 是进程使用物理内存占系统总内存的百分比。
第一,虚拟内存通常并不会全部分配物理内存。每个进程的虚拟 内存都比常驻内存大得多。
第二,共享内存 SHR 并不一定是共享的,比方说,程序的代码段、非共享的动态链接库, 也都算在 SHR 里。当然,SHR 也包括了进程间真正共享的内存。所以在计算多个进程的内存使用时,不要把所有进程的 SHR 直接相加得出结果。
内存中的Buffer和Cache
free 输出的缓存是 Buffer 和 Cache 两部分的总和 。
Buffer 是缓冲区,而 Cache 是缓存,两者都是数据在内存中的临时存储。
free 数据的来源-- /proc/meminfo

Buffer 和 Cache 是用 free 获得的指标。
man free
Buffers 是内核缓冲区用到的内存,对应的是 /proc/meminfo 中的 Buffers 值。
Cache 是内核页缓存和 Slab 用到的内存,对应的是 /proc/meminfo 中的 Cached 与 SReclaimable 之和。
这些数值都来自 /proc/meminfo。
同一个指 标的具体含义,就可能因为内核版本、性能工具版本的不同而有挺大差别。
proc 文件系统
/proc 是 Linux 内核提供的一种特殊文件系统,是 用户跟内核交互的接口。用户可以从 /proc 中查询内核的运行状态和配置选项,查询进程的运行状态、统计数据等,也可以通过 /proc 来修改内核的配置。
proc 文件系统同时也是很多性能工具的最终数据来源
执行 man proc ,搜索一下(比如搜索 meminfo),以便更快定位到内存部 分。
Buffers 是对原始磁盘块的临时存储,也就是用来缓存磁盘的数据,通常不会特别大 (20MB 左右)。这样,内核就可以把分散的写集中起来,统一优化磁盘的写入,比如可以把多次小的写合并成单次大的写等等。
Cached 是从磁盘读取文件的页缓存,也就是用来缓存从文件读取的数据。这样,下次访 问这些文件数据时,就可以直接从内存中快速获取,而不需要再次访问缓慢的磁盘。
SReclaimable 是 Slab 的一部分。Slab 包括两部分,其中的可回收部分,用 SReclaimable 记录;而不可回收部分,用 SUnreclaim 记录。
- Buffer 的文档没有提到这是磁盘读数据还是写数据的缓存,而在很多网络搜 索的结果中都会提到 Buffer 只是对将要写入磁盘数据的缓存。那反过来说,它会不会也缓 存从磁盘中读取的数据呢?
- Cache 是对从文件读取数据的缓存,那么它是不是也会缓存写文件的数据呢?
实验
环境准备
- sysstat , 用到 vmstat ,来观察 Buffer 和 Cache 的变化情 况。虽然从 /proc/meminfo 里也可以读到相同的结果,但毕竟还是 vmstat 的结果更加直 观。
- dd 来模拟磁盘和文件的 I/O,
- 为了减少缓存的影响,运行下面的命令来清理 系统缓存:写入 3 表示清理文件页、目录项、Inodes 等各种缓存。
echo 3 > /proc/sys/vm/drop_caches
场景 1:磁盘和文件写案例

- buff 和 cache 就是我们前面看到的 Buffers 和 Cache,单位是 KB。
- bi 和 bo 则分别表示块设备读取和写入的大小,单位为块 / 秒。因为 Linux 中块的大小 是 1KB,所以这个单位也就等价于 KB/s。
正常情况下,空闲系统中,你应该看到的是,这几个值在多次结果中一直保持不变。
接下来,到第二个终端执行 dd 命令,通过读取随机设备,生成一个 500MB 大小的文件:

在 dd 命令运行时, Cache 在不停地增长,而 Buffer 基本保持不变。
在 Cache 刚开始增长时,块设备 I/O 很少,bi 只出现了一次 488 KB/s,bo 则只有一次 4KB。而过一段时间后,才会出现大量的块设备写,比如 bo 变成了 122880。
当 dd 命令结束后,Cache 不再增长,但块设备写还会持续一段时间,并且,多次 I/O 写的结果加起来,才是 dd 要写的 500M 的数据。
下面的命令对环境要求很高,需要你的系统配置多块磁盘,并且磁盘分区 /dev/sdb1 还要 处于未使用状态。如果你只有一块磁盘,千万不要尝试,否则将会对你的磁盘分区造成损 坏。如果你的系统符合标准,就可以继续在第二个终端中,运行下面的命令。清理缓存后,向磁 盘分区 /dev/sdb1 写入 2GB 的随机数据:
首先清理缓存
echo 3 > /proc/sys/vm/drop_caches
然后运行 dd 命令向磁盘分区 /dev/sdb1 写入 2G 数据
dd if=/dev/urandom of=/dev/sdb1 bs=1M count=2048
然后,再回到终端一,观察内存和 I/O 的变化情况:虽然同是写数据,写磁盘跟写文件的现象还是不同的。写磁盘时(也就是 bo 大于 0 时),Buffer 和 Cache 都在增长,但显然 Buffer 的增长快得多。
对比两个案例,我们发现,写文件时会用到 Cache 缓存数据,而写磁盘则会用到 Buffer 来 缓存数据。所以,回到刚刚的问题,虽然文档上只提到,Cache 是文件读的缓存,但实际 上,Cache 也会缓存写文件时的数据。
场景 2:磁盘和文件读案例
清理缓存后,从文件 /tmp/file 中,读取数据写入 空设备:
首先清理缓存
echo 3 > /proc/sys/vm/drop_caches 3
运行 dd 命令读取文件数据
dd if=/tmp/file of=/dev/null
然后,再回到终端一,观察内存和 I/O 的变化情况:观察 vmstat 的输出,你会发现读取文件时(也就是 bi 大于 0 时),Buffer 保持不变,而 Cache 则在不停增长。这跟我们查到的定义“Cache 是对文件读的页缓存”是一致的。
磁盘读,回到第二个终端,运行下面的命令。清理缓存后,从磁盘分区 /dev/sda1 中读取数 据,写入空设备:
首先清理缓存
echo 3 > /proc/sys/vm/drop_caches
运行 dd 命令读取文件
dd if=/dev/sda1 of=/dev/null bs=1M count=1024
然后,再回到终端一,观察内存和 I/O 的变化情况:观察 vmstat 的输出,你会发现读磁盘时(也就是 bi 大于 0 时),Buffer 和 Cache 都在 增长,但显然 Buffer 的增长快很多。这说明读磁盘时,数据缓存到了 Buffer 中。
读文件 时数据会缓存到 Cache 中,而读磁盘时数据会缓存到 Buffer 中。
总结
Buffer 既可以用作“将要写入磁盘数据的缓存”,也可以用作“从磁盘读取数据的缓 存”。
Cache 既可以用作“从文件读取数据的页缓存”,也可以用作“写文件的页缓存”。
简单来说,Buffer 是对磁盘数据的缓存,而 Cache 是文件数据的缓存,它们既会用在读 请求中,也会用在写请求中。Buffer 和 Cache 分别缓 存磁盘和文件系统的读写数据。
从写的角度来说,不仅可以优化磁盘和文件的写入,对应用程序也有好处,应用程序可以 在数据真正落盘前,就返回去做其他工作。
从读的角度来说,既可以加速读取那些需要频繁访问的数据,也降低了频繁 I/O 对磁盘 的压力。
指标--缓存命中率
直接通过缓存获取数据的请求 次数,占所有数据请求次数的百分比。命中率越高,表示使用缓存带来的收益越高,应用程序的性能也就越好。
cachestat 和 cachetop
Linux 系统中并没有直接提供这些查询缓存的接口:
cachestat 提供了整个操作系统缓存的读写命中情况。
cachetop 提供了每个进程的缓存命中情况。
这两个工具都是 bcc 软件包的一部分,它们基于 Linux 内核的 eBPF(extended Berkeley Packet Filters)机制,来跟踪内核中管理的缓存,并输出缓存的使用和命中情况。
使用 cachestat 和 cachetop 前,我们首先要安装 bcc 软件包。bcc 提供的所有工具就都安装到 /usr/share/bcc/tools 这个目录中了。 不过这里提醒你,bcc 软件包默认不会把这些工具配置到系统的 PATH 路径中,所以你得 自己手动配置:export PATH=$PATH:/usr/share/bcc/tools
以 1 秒的时间间隔,输出了 3 组缓存统计数据:
cachestat 1 3
TOTAL ,表示总的 I/O 次数;
MISSES ,表示缓存未命中的次数;
HITS ,表示缓存命中的次数;
DIRTIES, 表示新增到缓存中的脏页数;
BUFFERS_MB 表示 Buffers 的大小,以 MB 为单位;
CACHED_MB 表示 Cache 的大小,以 MB 为单位。

cachetop

跟 top 类似,默认按照缓存的命中次数(HITS)排序,展示了每个进程的缓存命 中情况。具体到每一个指标,这里的 HITS、MISSES 和 DIRTIES ,跟 cachestat 里的含义 一样,分别代表间隔时间内的缓存命中次数、未命中次数以及新增到缓存中的脏页数。而 READ_HIT 和 WRITE_HIT ,分别表示读和写的缓存命中率。
指标--文件在内存中的缓存大小
pcstat
使用 pcstat 这个工具,来查看文件在内存中的缓存大小以及缓存比例。pcstat 是一个基于 Go 语言开发的工具,所以安装它之前,你首先应该安装 Go 语言

展示了 /bin/ls 这个文件的缓存情况:Cached 就是 /bin/ls 在缓存中的大小,而 Percent 则是缓存的百分比。你看 到它们都是 0,这说明 /bin/ls 并不在缓存中。

ls 命令,再运行相同的命令来查看的话,就会发现 /bin/ls 都在缓 存中了

实验
新建文件--第一个终端

确认刚刚生成的文件不在缓存中。如果一切正常, 你会看到 Cached 和 Percent 都是 0


读文件--第二个终端
运行 dd 命令测试文件的读取速度:

这个文件的读性能是 33.4 MB/s。由于在 dd 命令运行前我们已经 清理了缓存,所以 dd 命令读取数据时,肯定要通过文件系统从磁盘中读取。
查看第一个终端 缓存命中情况:
cachetop

从 cachetop 的结果可以发现,并不是所有的读都落到了磁盘上,事实上读请求的缓存命中 率只有 50% 。
先切换到第二个终端,再次执行刚才的 dd 命令:

磁盘的读性能居然变成了 4.5 GB/s,比第一次的结果 明显高了太多。
回到第一个终端,看看 cachetop 的情况:这次的读的缓存命中率是 100.0%,也 就是说这次的 dd 命令全部命中了缓存,所以才会看到那么高的性能。

回到第二个终端,再次执行 pcstat 查看文件 file 的缓存情况:

测试文件 file 已经被全部缓存了起来,这跟刚才观察到的缓 存命中率 100% 是一致的。
总结
Buffers 和 Cache 都是操作系统来管理的,应用程序并不能直接控制这些缓 存的内容和生命周期。所以,在应用程序开发中,一般要用专门的缓存组件,来进一步提升 性能。
比如,程序内部可以使用堆或者栈明确声明内存空间,来存储需要缓存的数据。再或者,使 用 Redis 这类外部缓存服务,优化数据的访问效率。
posted on 2025-10-12 21:25 chuchengzhi 阅读(133) 评论(0) 收藏 举报
浙公网安备 33010602011771号