这一章在做什么

回答两个问题:内存是不是真的不够,如果不够或一直在涨,是谁、哪段代码在用。先分清「用掉了」和「被缓存占着、随时能让出来」,再看内存紧张的信号:换页、页扫描、PSI、OOM 杀进程。然后落到进程:VSZ 和 RSS 的区别,缺页为什么就是 RSS 在长,free 之后 RSS 为什么不降。最后是几个调优旋钮:swappiness、overcommit、大页、NUMA 绑定和 cgroup 限额。

一台 8 GB 机器的物理内存,按内核的账本拆开 进程的匿名内存 堆、栈,加上内核不能回收的 页缓存 + 可回收的 slab free 的 buff/cache 列 完全空闲 free 列 free:看着很少 available:能马上拿来用的(估计值) 有一小段缓存正在用,让不出来 内存真不够的信号不是 free 小,而是换页(si / so)、页扫描、PSI 在涨、OOM 杀进程
本站画的示意,比例是编的。开机一段时间后 free 列缩到很小是正常的:空着的内存被拿去缓存文件了,应用要用时内核会回收(§7.2.6)。

什么时候翻这一章

CPU 缓存和 TLB 的硬件细节在第 6 章;页缓存(文件系统缓存)和 free 命令在第 8 章(§8.6.2);容器的内存限额在第 11 章。这一章的方法框架来自第 2 章的 USE 方法。

先用中文过一遍

虚拟内存和按需分页:VSZ 大不等于占内存

每个进程看到的是自己的一大片线性地址空间,这就是虚拟内存(virtual memory)。它不是真的内存:malloc 或 mmap 只是在地址空间里划出一段,内核这时一页物理内存都没给。等程序第一次写这一页,MMU 查不到映射,触发缺页(page fault),内核才找一页空闲的物理内存挂上去。这叫按需分页(demand paging)(§7.2.3)。

所以进程有两个「大小」。VSZ(虚拟内存大小)是划出来的全部地址,碰没碰过都算;RSS(驻留集大小)是此刻真正在物理内存里的页。缺页分两种:能直接从内存满足的叫次缺页(minor fault),比如给新页分配内存;要读磁盘的叫主缺页(major fault),比如访问被换出到 swap 的页、或者还没读进来的映射文件。对排查最有用的一条推论是:次缺页的次数就是 RSS 增长的足迹,匿名内存大致每 4 KB 一次。想知道内存是被哪段代码长起来的,就去采缺页的调用栈(§7.5.10)。

一页虚拟内存的一生 未分配 已分配 还没碰过,没有物理页 在物理内存里 RAM 在 swap 上 磁盘 malloc 第一次写 内存紧张 次缺页:不碰磁盘 再访问:主缺页 线程等磁盘读回来 VSZ:后三种都算,ps 的 VSZ、top 的 VIRT RSS:只算这一种
本站画的示意。主缺页要等一次磁盘 IO,比次缺页慢几个数量级。文件映射的页被回收时不进 swap:干净的直接丢掉,下次从文件读回来。

VSZ 和 RSS 差得远是常态:线程栈、分配器给各线程预留的区域、映射了却没读的文件,都先占地址不占内存。Linux 还允许过量分配(overcommit):申请的总量可以超过物理内存加 swap,赌的是程序不会把申请的全用上(§7.2.4)。代价是到真的用不起时,malloc 不会失败,而是由 OOM killer 收场。

内存紧张时,内核按什么顺序腾地方

空闲页从空闲链表(free list)里拿。空闲页低于某条水位线,后台的 kswapd 被唤醒,开始扫描 LRU 链表找能回收的页(页扫描,page scanning),回收到更高的一条水位才停。最先腾的是页缓存:干净的文件页直接丢,脏页要先写回磁盘。再不够,有 swap 的话就把不常用的匿名页换出去,Linux 把这叫 swapping(§7.3.2)。vm.swappiness(默认 60;书里写的范围是 0 到 100,新内核允许设到 200)决定更偏向回收页缓存还是换出应用内存。

如果分配来得比 kswapd 回收得快,空闲页掉到最低水位,申请内存的线程就得自己动手回收,这叫直接回收(direct reclaim):线程在分配内存时被卡住,延迟直接算在请求上。最后一步是 OOM killer:挑一个进程杀掉,系统日志里留下「Out of memory」。这几步每一步都是内存饱和的信号,对应的指标也不同。

空闲页往下掉,内核依次做什么 high low min 空闲页 空闲页充足:直接从空闲链表分配 free、available kswapd 后台回收:先丢页缓存,再换出匿名页 回收到 high 才停;swappiness 定偏向 sar -B 的 pgscank vmstat 的 si / so 直接回收:申请内存的线程自己回收,被卡住 pgscand、PSI、drsnoop 还不够:OOM killer 挑一个进程杀掉 dmesg、oom_kill 计数
本站画的示意,不是精确的内核流程:真实的回收按 NUMA 节点和内存区域分别算水位,cgroup 到了限额也会在组内走同样几步(§7.3.2)。三条水位的位置由 vm.min_free_kbytes 等参数决定(§7.6.1)。

有没有 swap 决定了内存一直涨的服务怎么出事。有 swap:先是换页、变慢,你有时间在现场查;没有 swap(很多云主机和 ARM 板):没有缓冲期,用完就被杀,问题可能要跑几个小时才冒出来。书里提到 Netflix 的云主机就不配 swap,宁可让一台实例被杀、流量转走,也不要它慢慢换页。哪种好要看你的部署:一块独自干活的板子,没有「别的实例」可以接流量。

堆只涨不落:泄漏、增长和分配器

用户态程序的 malloc 和 free 不直接对着内核,中间隔着分配器(glibc malloc、jemalloc、TCMalloc)。分配器向内核要来大块内存,切小了发给程序;程序 free 之后,内存多半回到分配器自己的空闲链表里留着下次用,不一定还给内核。所以 RSS 只涨不落,可能完全正常(§7.3.3)。

书里把「内存一直涨」分成两种:泄漏(leak),内存不再用了却没释放,是代码的 bug;增长(growth),程序正常地用内存,只是用得比你想要的多,比如缓存设得太大、还在预热(§7.4.6)。两种的修法不同,后者常常被误判成前者。glibc 的规矩大致是:大块分配(默认从 128 KB 起,会动态调高)直接用 mmap,free 时立刻 munmap 还给内核;小块从堆里切,堆顶空出来足够多时才用 brk 缩回去。只要堆顶还有一块在用,下面空了再多也缩不回去。

只留最顶上一块,堆就缩不回去 free 了 99.99% 的小块之后 已释放:留在 glibc 的空闲链表里,物理页还挂着 用 brk RSS 不降:堆顶被最后一块占着,brk 退不回去 再调一次 malloc_trim(0) 整页还给了内核:地址还在,物理页没了 用 RSS 降下来,VSZ 基本不变。下面「动手」的第二个实验就是这张图
本站画的示意。glibc 2.8 以后的 malloc_trim 会把各个 arena 里空闲块中的整页都还回去,不只是堆顶(出处:malloc_trim(3) 手册)。别的分配器有自己的办法,书里提到 TCMalloc 会定期把内存收回中央堆(§7.3.3)。

大页和 NUMA:两种和硬件有关的调优

MMU 把虚拟地址翻译成物理地址,结果缓存在 TLB 里。TLB 条目不多,页越大,同样多的条目能覆盖越多的内存,TLB 未命中就越少。Linux 的大页(huge pages)常见是 2 MB,可以预先配好给程序用,也可以交给透明大页(THP)自动合并(§7.6.2)。多插槽的服务器还有 NUMA:每颗 CPU 挂着自己的内存,访问别的插槽上的内存要经过互连,延迟更高(§7.3.1)。内核会尽量就近分配,numastat 看它做得怎么样,numactl 可以把进程和它的内存绑在一个节点上(§7.6.4)。单插槽的机器和多数 ARM 板只有一个节点,这一段可以先跳过。

英文中文一句话
virtual memory虚拟内存每个进程自己的一片地址空间,不是真内存,按需映射到物理页 §7.2.1 术语页
RSS (resident set size)驻留集大小此刻在物理内存里的页;含共享库,各进程相加会超过总内存 §7.2.3 术语页
VSZ (virtual memory size)虚拟内存大小划出来的全部地址,没碰过的也算,常常远大于 RSS §7.2.3
PSS (proportional set size)按比例分摊的驻留大小私有页加上共享页除以共享它的进程数,相加不会重复计算 §7.2.9 术语页
page fault (minor / major)缺页(次缺页 / 主缺页)访问还没映射的页;不读盘是次缺页,读盘是主缺页 §7.2.3 术语页
anonymous memory匿名内存没有对应文件的内存:堆和栈;被回收时只能换出到 swap §7.1
swapping换出(Linux 的用法)Linux 指把匿名页换到 swap;传统 Unix 指把整个进程换出 §7.1 术语页
overcommit过量分配允许申请超过物理内存加 swap 的总量,靠按需分页兜着 §7.2.4 术语页
OOM killerOOM 杀手内存实在腾不出来时,内核挑一个进程杀掉 §7.3.2 术语页
kswapd / page scanning换页守护进程 / 页扫描空闲页低于水位时在后台扫描 LRU 链表回收页 §7.3.2
direct reclaim直接回收空闲页太少时,申请内存的线程自己回收,被阻塞 §7.3.2 术语页
WSS (working set size)工作集大小进程经常访问的那部分内存;工具一般只给 RSS,量它要专门的办法 §7.2.10 术语页
slab / SLUBslab 分配器内核按对象大小缓存内存块,slabtop 看它占了多少 §7.3.3
huge pages / THP大页 / 透明大页2 MB 等大尺寸的页,扩大 TLB 覆盖范围 §7.6.2 术语页
NUMA非一致内存访问多插槽机器上,访问本地内存比远程内存快 §7.3.1 术语页

方法:按什么顺序查

书里列了十种内存的方法,排查时作者建议先做三样:性能监控、USE 方法、刻画内存用量(§7.4)。USE 里又建议先查饱和度,因为持续的饱和就是内存出了问题的标志(§7.4.2)。本站把它们排成下面几步:前三步回答「够不够」,后三步回答「谁、为什么」。

  1. 内存真的不够吗?

    free -m 看 available,不看 free;cat /proc/pressure/memory 看有没有任务在等内存。available 还多、PSI 是 0,就不是内存的事。§7.2.6§7.5.2

  2. 有没有饱和:换页、扫描、OOM?

    vmstat 1 的 si、so 持续不为零是在换页;sar -B 1 的 pgscand/s 不为零说明有线程在直接回收;dmesg | grep -i 'out of memory' 查有没有杀过进程。§7.4.2§7.5.4

  3. 有没有限额先到了?

    容器和 systemd 服务都在 cgroup 里:看所在组的 memory.max、memory.current、memory.events。宿主机再空,组内到了限额照样回收、照样杀。§7.4.2§7.6.5

  4. 谁在用?

    ps aux --sort=-rss | head 或 top 里按 M。几十个进程共享同一批库时,RSS 相加会超过总内存,按 PSS 看(/proc/PID/smaps_rollup、pmap -X)。内核自己用的看 /proc/meminfo 的 Slab、slabtop。§7.5.7§7.5.9§7.5.5

  5. 它在涨吗?涨的是哪一段?

    pidstat -r -p PID 60 长期记 RSS;pmap -x PID 隔一段时间比一次,看是堆、匿名映射还是文件映射在长。先问「它本来就该这样吗」:看配置里的缓存大小,看涨到某个值会不会停。§7.4.5§7.4.6

  6. 为什么涨:哪段代码?

    缺页就是 RSS 在长:perf record -e page-faults -g 采缺页时的调用栈,画成火焰图。疑似泄漏用 BCC 的 memleak 看哪些分配没释放(开销不小)。也可以先做普通的 CPU 剖析,找分配函数所在的路径,最便宜。§7.5.10§7.4.6

  7. 配置对不对?

    overcommit、swappiness、THP、有没有 swap、NUMA 怎么分布、应用自己配了多少缓存。这些在出事之前就能查。§7.4.7§7.6.1

工具速查

想知道敲什么看哪里节
还剩多少能用free -m
grep MemAvailable /proc/meminfo
available;free 列小是正常的。used 的算法随版本变:procps-ng 4.0.1 起是 total − available,更早是 total − free − buff/cache§7.4.2
有没有任务在等内存cat /proc/pressure/memorysome 是有任务卡住的时间比例,full 是所有可运行任务都卡住;avg10 比 avg300 高说明在变糟§7.5.2
在不在换页vmstat 1si、so 持续不为零;swpd 大但 si、so 为零,说明以前换出过、现在没在换§7.5.1
配了多少 swapswapon --show
cat /proc/swaps
没有输出就是没配 swap,内存用完直接 OOM§7.5.3
页扫描和直接回收sar -B 1pgscank/s 后台扫描,pgscand/s 直接回收(不为零就有线程被卡);%vmeff 低于 30% 说明回收很吃力§7.5.4
杀过谁dmesg -T | grep -iE 'out of memory|oom-kill'
grep oom_kill /proc/vmstat
被杀的进程名、anon-rss(它自己的匿名内存);计数在涨说明还在杀§7.4.1
谁占得最多ps aux --sort=-rss | head
top,按 M
RSS(top 里是 RES);VSZ / VIRT 大不说明问题§7.5.7
某个进程在长吗pidstat -r -p PID 10RSS 的走向;minflt/s 不为零就是在长新页,majflt/s 不为零是在等磁盘§7.4.5
进程的内存分在哪pmap -x PID
cat /proc/PID/smaps_rollup
哪段映射的 RSS 最大,[ anon ] 多半是堆;Pss 是扣掉共享后的份额§7.5.9
内核 slab 占了多少slabtop -sc
grep -E 'Slab|SUnreclaim' /proc/meminfo
哪个缓存最大、在不在一直涨;SUnreclaim 一直涨可能是内核里的泄漏§7.5.5
NUMA 分得好不好numastat
numactl --hardware
numa_miss、numa_foreign 相对 numa_hit 的比例;只有一个 node 就不用看§7.5.6
哪段代码让 RSS 长perf record -e page-faults -g -p PID -- sleep 30缺页最多的调用栈;栈断在 libc 里时改用 --call-graph dwarf§7.5.10
疑似泄漏memleak-bpfcc -p PID 30一段时间里分配了没释放的调用栈和字节数;分不清泄漏和正常增长,要你读代码判断§7.4.6
直接回收卡了多久drsnoop-bpfcc -T每次直接回收的进程和 LAT(ms);量出内存紧张对应用的实际影响§7.5.11
容器的限额cat /sys/fs/cgroup/组路径/memory.max
cat …/memory.events
memory.current 离 memory.max 多远;memory.events 的 oom_kill 计数§7.6.5

free、vmstat、ps、top、pmap、slabtop 在 procps 包里;pidstat、sar 在 sysstat;numastat 在 numactl 包;perf 在 linux-perf;带 -bpfcc 后缀的在 bpfcc-tools。slabtop 要读 /proc/slabinfo,要 root;BCC 工具都要 root。工具名链接到本站的工具卡:free、vmstat、sar、ps、pidstat、pmap、slabtop、numastat、perf、memleak、drsnoop。

动手:在自己的机器上试

分配了不碰,再一点点碰:看 VSZ、RSS 和缺页

先猜:malloc(200 MB) 返回之后,RSS 涨多少?每写 50 MB,会发生多少次缺页?下面这个程序自己打印 /proc/self/status 里的两个数和累计的次缺页数,每一步停两秒,方便你从外面看。整个实验最多用 200 MB 内存,不会引起换页。

// grow.c:先要 200 MB 不碰,再每两秒碰 50 MB,最后释放
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/resource.h>
#define MB (1024L * 1024)

static void show(const char *what) {
    char line[128]; long vsz = 0, rss = 0;
    FILE *f = fopen("/proc/self/status", "r");
    while (fgets(line, sizeof line, f)) {
        sscanf(line, "VmSize: %ld", &vsz);
        sscanf(line, "VmRSS: %ld", &rss);
    }
    fclose(f);
    struct rusage ru; getrusage(RUSAGE_SELF, &ru);
    printf("%-16s VSZ %4ld MB  RSS %4ld MB  minflt %6ld\n",
           what, vsz / 1024, rss / 1024, ru.ru_minflt);
}

int main(void) {
    printf("pid %d\n", getpid());
    show("start");
    char *p = malloc(200 * MB);
    show("malloc 200 MB");
    sleep(2);
    for (int i = 0; i < 4; i++) {
        memset(p + i * 50 * MB, 1, 50 * MB);   // 第一次写,才真正分到物理页
        show("touch +50 MB");
        sleep(2);
    }
    free(p);
    show("free");
    sleep(2);
    return 0;
}
$ gcc -O2 -g -fno-omit-frame-pointer grow.c -o grow
$ ./grow
# 另开一个终端,在它跑的时候看
$ pidstat -r -p $(pgrep -n grow) 1
$ ps -o pid,vsz,rss,min_flt,maj_flt,comm -p $(pgrep -n grow)
$ pmap -x $(pgrep -n grow) | grep -E 'Address|anon'

本站在 WSL2 上看到的(只留了关键几行):

start            VSZ    2 MB  RSS    1 MB  minflt     88
malloc 200 MB    VSZ  202 MB  RSS    1 MB  minflt     89
touch +50 MB     VSZ  202 MB  RSS   51 MB  minflt  12891
touch +50 MB     VSZ  202 MB  RSS  101 MB  minflt  25691
touch +50 MB     VSZ  202 MB  RSS  201 MB  minflt  51291
free             VSZ    2 MB  RSS    1 MB  minflt  51291

pidstat:  minflt/s  majflt/s     VSZ     RSS  Command
          12800.00      0.00  207364   52736  grow
pmap:     Kbytes     RSS   Dirty  Mode   Mapping
          204816   51212   51212  rw---  [ anon ]
  1. malloc 之后 VSZ 涨了 200 MB,RSS 一点没动:只划了地址,没给物理页。
  2. 每写 50 MB,次缺页加 12800 次左右:50 MB ÷ 4 KB = 12800,一页一次。pidstat 的 minflt/s 在写的那一秒正好是 12800,majflt/s 是 0(没碰磁盘)。
  3. pmap 里那一段 [ anon ] 有 200 MB 的地址,只有 50 MB 驻留:这就是一个正在「长」的进程在 pmap 里的样子。
  4. free 之后 VSZ 和 RSS 一起回落:200 MB 远超 glibc 的 mmap 阈值,这块是单独 mmap 来的,free 直接还给了内核。下一个实验看小块为什么不一样。

再用 perf 数缺页、找代码。-g(帧指针)在这里栈会断:缺页发生在 libc 的 memset 里,perf 顺着帧指针走不出 libc(本站这台机器上的 libc 没有保留帧指针),回不到 main。改用 DWARF 回溯就看得到:

$ perf stat -e page-faults,minor-faults,major-faults ./grow
$ perf record -e page-faults --call-graph dwarf ./grow
$ perf report --stdio --no-children
51,261  page-faults:u
51,261  minor-faults:u
     0  major-faults:u

99.85%  grow  libc.so.6  [.] 0x000000000017dc40
        ---0x754872844c40
           main
           __libc_start_main

把 memset 换成你的服务,这就是书里说的缺页火焰图的原料:哪条调用栈触发的缺页多,RSS 就是被谁长起来的(§7.5.10)。注意它只记「第一次碰」,已经驻留的内存被反复使用不会出现在这里。

WSL2
本站实测过上面所有命令(Debian 13,glibc 2.41,perf 6.12),不用 root。本机的透明大页设置是 madvise(看 /sys/kernel/mm/transparent_hugepage/enabled),所以每 4 KB 一次缺页;设成 always 的机器上,大块匿名内存可能按 2 MB 一次缺页,次数会少很多。
服务器
对正在跑的服务,pidstat -r -p PID 60 挂一天,看 RSS 的走向比看一眼准。perf record -e page-faults -g -p PID -- sleep 30 要 root 或调低 kernel.perf_event_paranoid;DWARF 回溯生成的数据文件大,采样时间别太长。
ARM 板
程序本身只依赖 /proc/self/status 和 getrusage,交叉编译就能跑。没有 pidstat 时,隔几秒读一次 /proc/PID/status 的 VmRSS,和 /proc/PID/stat 的第 10、12 个字段(累计的次缺页、主缺页)。busybox 的 ps、top 显示哪些内存列,看它的编译选项。

free 之后 RSS 为什么不降

这是「服务内存只涨不落」最常见的样子。程序分配 20 万个 1000 字节的小块(约 200 MB),释放掉除了最后一块以外的全部,再调一次 malloc_trim(0)。先猜:释放之后 RSS 是多少?

// frag.c:20 万个 1000 字节的小块,释放掉除最后一个以外的全部
#include <malloc.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define N 200000

static long rss_mb(void) {
    char line[128]; long rss = 0;
    FILE *f = fopen("/proc/self/status", "r");
    while (fgets(line, sizeof line, f)) sscanf(line, "VmRSS: %ld", &rss);
    fclose(f);
    return rss / 1024;
}

int main(void) {
    static char *blk[N];
    for (int i = 0; i < N; i++) { blk[i] = malloc(1000); memset(blk[i], 1, 1000); }
    printf("allocated      RSS %4ld MB\n", rss_mb());
    for (int i = 0; i < N - 1; i++) free(blk[i]);   // 只留最后一块
    printf("freed all but 1 RSS %4ld MB\n", rss_mb());
    malloc_trim(0);
    printf("malloc_trim(0) RSS %4ld MB\n", rss_mb());
    return 0;
}
$ gcc -O2 frag.c -o frag
$ ./frag
allocated      RSS  194 MB
freed all but 1 RSS  195 MB
malloc_trim(0) RSS    2 MB
  1. 释放了 99.99% 的块,RSS 一点没降:这些小块是从堆里切的,最后一块压在堆顶,brk 退不回去,空出来的内存留在 glibc 手里等下次分配。从外面看,这和泄漏一模一样。
  2. malloc_trim(0) 之后 RSS 回到 2 MB:它把空闲块里的整页还给了内核,不只是堆顶(glibc 2.8 以后的行为,见 malloc_trim(3) 手册)。
  3. 所以看到 RSS 不回落,先别下结论。下一步是看它会不会继续涨:涨到某个值就平了,多半是分配器留着;一直涨,才去找泄漏(memleak、valgrind、分配器的调试模式)。
  4. 把最后一块也释放了再试试:在本站的 glibc 2.41 上 RSS 仍然停在 195 MB,关掉每线程缓存(tcache)也一样;本站没有追查原因。分配器的细节会随版本变,结论要自己量,别靠推理。
WSL2
本站实测过(glibc 2.41),不用 root,峰值约 200 MB。
服务器
服务里可以在空闲时调 malloc_trim(0)(它本身要遍历空闲块,有开销),或者换 jemalloc、TCMalloc:书里说可以用 LD_PRELOAD 在启动时换,不用改代码(§7.6.3)。换之前用这个程序对比一下行为。
ARM 板
很多 BSP 用的不是 glibc,而是 musl 或 uClibc,它们的分配器策略不同,有没有 malloc_trim 也要查自己的 C 库。把这个程序交叉编译过去跑一遍,就知道你板子上的分配器会不会把内存还回去。

成书之后

内核里只剩 SLUB 一种 slab 分配器

书里
介绍了 slab 和 SLUB 两种内核分配器,说 Linux 后来把 SLUB 作为选项或默认(§7.3.3)。
今天
SLAB 在 Linux 6.5 被标为弃用,6.8 从内核里删掉了,SLUB 成了唯一的通用对象分配器。/proc/slabinfo 和 slabtop 照常能用,看的就是 SLUB 的缓存。
核对于 2026-10。出处:Phoronix《Linux 6.8 To Drop The SLAB Allocator, SLUB Optimizations Coming Too》;内核邮件列表上的「[GIT PULL] slab updates for 6.8」。

内存限额改用 cgroup v2 的文件

书里
列的是 cgroup v1 内存控制器的文件:memory.limit_in_bytes、memory.oom_control 等(§7.6.5)。
今天
Debian 11 起 systemd 默认用 cgroup v2(统一层级)。对应的文件是 memory.max(硬上限,到了会在组内触发 OOM)、memory.high(软上限,超过就被限流、加紧回收,不触发 OOM)、memory.events(含 oom_kill 计数)、memory.pressure(这个组自己的 PSI)。本站在 WSL2 的 Debian 13 上看到的就是这一套,路径用 cat /proc/self/cgroup 找。
核对于 2026-10。出处:内核文档 admin-guide/cgroup-v2.rst;Debian systemd 软件包的 NEWS.Debian(bullseye 起默认统一层级)。

分节地图

这一章的骨架和第 6 章一样:背景(§7.1–7.2)、架构(§7.3)、方法(§7.4)、工具(§7.5)、调优(§7.6)。上手只需要概念里讲按需分页、换页、缓存的几节,§7.3.2 里内核怎么腾内存(含 OOM),方法里的 USE 和泄漏检测,加上 vmstat、PSI、ps、pmap 四个工具。硬件、各个跟踪工具和调优参数用到时再查。

  1. System main memory stores application and…约 300 词开篇内存不够的两种后果:换页变慢、进程被杀。扫一眼当目录。
  2. Terminology约 300 词术语swapping 在 Linux 和传统 Unix 里意思不同,读工具输出前看一眼。
  3. Concepts约 100 词概念一句引子。
  4. Virtual Memory约 300 词虚拟内存虚拟内存为什么能超过物理内存;「虚拟内存耗尽」是什么意思。
  5. Paging约 500 词换页文件页换页和匿名页换页,为什么后者才伤性能。
  6. Demand Paging约 400 词按需分页缺页怎么发生,次缺页和主缺页,RSS 和虚拟大小的定义。
  7. Overcommit约 100 词过量分配为什么 malloc 很少失败,代价落在哪里。
  8. Process Swapping约 200 词整进程换出历史背景,Linux 根本不这样做。
  9. File System Cache Usage约 100 词文件系统缓存占用开机后空闲内存越来越少为什么是正常的。
  10. Utilization and Saturation约 100 词利用率和饱和度缓存算不算「用了」;哪些动作说明内存饱和。
  11. Allocators约 100 词分配器分配器能让程序变快,也能因碎片浪费内存。
  12. Shared Memory约 100 词共享内存共享库算谁的;PSS 怎么分摊。
  13. Working Set Size约 100 词工作集大小真正常用的那部分内存,概念有用但难量。
  14. Word Size约 200 词字长32 位地址空间的限制;做 32 位 ARM 的读者知道 3 GB 用户空间这回事即可。
  15. Architecture约 100 词架构硬件和软件两部分的引子。
  16. Hardware约 1200 词硬件DRAM、UMA 和 NUMA、DDR 带宽、MMU 和 TLB。查 NUMA、大页时回来看。
  17. Software约 1200 词软件内存不够时内核按什么顺序腾地方:页缓存、换出、收缩、OOM;kswapd 和水位。这一章最该精读的一节。
  18. Process Virtual Address Space约 1100 词进程虚拟地址空间各段的用途;堆为什么只涨不落;slab、glibc、jemalloc 等分配器。
  19. Methodology约 100 词方法十种方法的总表和作者建议先做的三种。
  20. Tools Method约 300 词工具法在 Linux 上挨个看哪些工具、看哪个数。
  21. USE Method约 400 词USE 方法内存的利用率、饱和度、错误各看什么;为什么先看饱和度;限额环境怎么算。
  22. Characterizing Usage约 400 词刻画内存用量用几句话描述内存去了哪;一张深入分析时可以照着问的清单。
  23. Cycle Analysis约 100 词周期分析从 IPC 看负载有多依赖内存,指向第 6 章。
  24. Performance Monitoring约 100 词性能监控长期记哪几个数;按进程记能看出泄漏的速度。
  25. Leak Detection约 300 词泄漏检测泄漏和增长的区别,先问哪个问题,memleak 能做什么不能做什么。
  26. Static Performance Tuning约 200 词静态性能调优一张检查内存配置的问题清单。
  27. Resource Controls约 100 词资源控制内存和虚拟内存的上限,细节在 §7.6。
  28. Micro-Benchmarking约 100 词微基准测试测内存速度和缓存大小,排查内存用量时用不到。
  29. Memory Shrinking约 100 词内存收缩实验故意压缩内存估计工作集,作者自己说不要在生产上用。
  30. Observability Tools约 100 词观测工具工具总表,从全系统到进程再到跟踪。
  31. vmstat约 300 词vmstatswpd、free、buff、cache、si、so 各是什么,哪一列持续不为零要警惕。
  32. PSI约 100 词PSI内存压力的 some 和 full 两行怎么读,看趋势。
  33. swapon约 100 词swapon配了哪些 swap、用了多少。
  34. sar约 300 词sar换页、页扫描、大页、swap 的统计;%vmeff 和 pgscand 两个最有用。
  35. slabtop约 100 词slabtop内核各个 slab 缓存占了多少。
  36. numastat约 200 词numastat多插槽机器上内存分配有多少落在了本地节点。
  37. ps约 200 词ps%MEM、RSS、VSZ;为什么 RSS 相加会超过总内存。
  38. top约 200 词top按内存排序,各个内存列的含义去看手册。
  39. pmap约 200 词pmap进程的每段映射占多少,PSS 从哪里看。
  40. perf约 500 词perf缺页采样和缺页火焰图:找出 RSS 是哪段代码长起来的。
  41. drsnoop约 100 词drsnoop量每次直接回收让进程等了多久。
  42. wss约 300 词wss用页表的访问位估计工作集;实验性质,先读警告。
  43. bpftrace约 600 词bpftrace跟踪 malloc、brk、缺页、回收;跟踪 malloc 开销很大。
  44. Other Tools约 300 词其他工具memleak、oomkill 等 BPF 工具,dmesg、valgrind、/proc 下的几个文件。
  45. Tuning约 100 词调优最重要的调优是让应用待在内存里、不换页。
  46. Tunable Parameters约 400 词可调参数脏页、水位、overcommit、swappiness、NUMA 平衡等参数。改之前读。
  47. Multiple Page Sizes约 200 词多种页大小配置大页的几种方式和透明大页。
  48. Allocators约 100 词分配器用 LD_PRELOAD 换分配器。
  49. NUMA Binding约 100 词NUMA 绑定numactl 把进程的内存和 CPU 绑在一个节点。
  50. Resource Controls约 100 词资源控制ulimit 和 cgroup 的内存限额(书里是 v1 的文件名)。
  51. Exercises约 300 词习题习题;动手部分本页已经给了替代。

词与短语

只收先读和用到再查两档的节。斜体那一行是原句里连续的几个词,在电子书里搜它就能找到那一句。

§0 开篇

orders of magnitude
… which operate orders of magnitude more slowly …
(慢)几个数量级
一个数量级是十倍。磁盘比内存慢的不是几倍,是成千上万倍。

§7.1 Terminology

non-contended
… is (almost) infinite and non-contended.
没有争用的
每个进程都觉得整片地址空间归自己,不用和别人抢。
confusing and incorrect
(which is confusing and incorrect)
既容易混淆又不对
作者在点名批评:有些工具把虚拟内存叫成 swap。读到「available swap」之类的字样要多想一步。

§7.2.1 Virtual Memory

at first glance
… can be confusing at first glance …
乍一看

§7.2.2 Paging

fine-grained
… paging is a fine-grained approach …
细粒度的
以 4 KB 的页为单位,而不是整个进程。
gets a bit tricky
… where the terminology gets a bit tricky …
(说法)有点绕
tricky 不是「狡猾」,是不好弄清楚。这里说的是 page-out 不一定伴随写盘。
block on
… they block on the disk I/O …
阻塞在……上
block 作不及物动词,是「卡住等待」,不是「挡住磁盘 IO」。

§7.2.4 Overcommit

use it sparsely
… later use it sparsely on demand …
(申请得多)用得少
和前面的 generously 对着说:大方地申请,用到哪页才碰哪页。

§7.2.6 File System Cache Usage

naïve users
This can distress naïve users who …
不了解内情的用户
naïve 在技术文章里是「不明就里、按字面理解」,不是「天真可爱」。distress 是「让人发慌」。

§7.2.7 Utilization and Saturation

(confusingly) called
… sometimes (confusingly) called available swap.
(叫法容易让人搞混地)被称为
括号里的副词是作者的评语:这个叫法不好,「可用 swap」其实指的是可用的虚拟内存。

§7.3.2 Software

general order
… in the general order in which …
大致的顺序
原文 general 是斜体,强调「大致」:这几种回收方式不是严格一个接一个,会有重叠。
sacrificial process
… killing a sacrificial process, found using …
被牺牲掉的进程
sacrificial 是「献祭用的」:杀掉它,换别的进程活下来。
warm / cold
… preserving warm file system cache while …
常用的(热的)/ 不常用的(冷的)
和温度无关。warm cache 是最近常被访问、留着有用的缓存。
grace period
… there is no paging grace period …
宽限期
本义是还款的宽限期。这里指有 swap 时,从开始换页到被杀之间那段「变慢但还活着」的时间。
hysteresis
… two thresholds to provide hysteresis …
滞回(两道门槛防止来回抖)
低于一条线才开始回收,高于另一条线才停,免得在一条线附近反复开关。
in the foreground
… kswapd runs in the foreground …
同步地(当场)做
不是「在前台显示」。意思是回收不再在后台悄悄进行,而是在申请内存的路径上当场完成,调用方要等。
outpace
… allocation bursts that outpace kswap reclamation …
比……快

§7.3.3 Process Virtual Address Space

a common source of confusion
A common source of confusion is …
常常让人困惑的一件事
magazine
… cache of objects called a magazine …
弹匣
不是「杂志」。引文自己点明了是拿自动武器作比:每个 CPU 一个装满对象的弹匣,打空了整个换一个。

§7.4.1 Tools Method

coarse
A cheaper, though coarse, solution …
粗略的
用 CPU 剖析找分配路径,便宜但不精确。

§7.4.2 USE Method

may not have felt it necessary
… developers may not have felt it necessary …
可能觉得没必要
委婉的批评:有了 overcommit,malloc 几乎不失败,很多程序干脆不检查返回值、不报告分配失败。
unreproducible
… may experience unexplained, unreproducible crashes …
无法复现的
内存硬件出错的典型表现:随便哪个程序莫名其妙地崩,换个时间又好了。

§7.4.6 Leak Detection

Is it supposed to do that?
… Is it supposed to do that? …
它本来就该这样吗?
be supposed to 是「按设计应该」,不是「假设」。先确认是不是设计如此,再去找 bug。
cache warmup
… observed growth may be cache warmup.
缓存预热

§7.5.7 ps

overcounting
… due to overcounting of this shared memory.
重复计算

§7.5.9 pmap

the first wave of
… shown as the first wave of …
(输出里)头一批
wave 在这里是「一连串、一批」,指 pmap 输出里连着出现的那一串匿名映射。

§7.5.12 wss

better than no tool
… experimental tool with warnings is better …
(带警告的实验工具)总比没有强
作者在为一个不成熟的工具辩护,同时提醒你它有风险。后面一整段警告要读。

§7.5.13 bpftrace

get a handle on
… to get a handle on allocation …
大致摸清
handle 是「把手」:先抓住个把手,对分配路径有个数,不是「处理」。

§7.6.1 Tunable Parameters

resort to
… and resort to using OOM sooner.
(不得已)只好动用
resort to 是走投无路才用的手段。min_free_kbytes 调得太低,内核更早地被逼到杀进程。
at the expense of
… at the expense of the page cache.
以牺牲……为代价

§7.6.2 Multiple Page Sizes

hopefully
These issues have hopefully been fixed.
但愿(已经修好了)
作者不打包票。透明大页过去出过性能问题,开之前自己量。

诊断题:换成你来查

场景和输出都是本站编的。先自己判断,再点选项看解释。

同事看到这个报警:内存只剩 312 MB 了。要处理吗?§7.2.6

场景:一台 8 GB 的服务器跑你的 C++ 日志收集服务,一直在读写大量文件。

$ free -m
       total   used   free   shared   buff/cache   available
Mem:    7821   2411    312       18         5369        5410
Swap:      0      0      0

一个一直在读写文件的服务,空闲内存被页缓存占掉是正常的,书里说空闲内存在开机后缩到接近零会吓到不明就里的用户,但不影响应用(§7.2.6)。available 才是不换页就能拿到的估计值。清缓存只会让后面的读重新打到磁盘上,反而变慢。页缓存涨不是泄漏,泄漏看的是某个进程的 RSS 或内核的不可回收部分。报警该改成基于 available 或 PSI。

这个服务在泄漏内存吗?§7.4.6

场景:一个 C++ 写的网关,平时 RSS 约 300 MB。周一流量高峰时涨到 900 MB,之后流量回落,RSS 停在 900 MB 不下来。接下来一周它一直是 900 MB 左右,没有再涨。

pidstat -r(流量回落后):
  minflt/s   majflt/s      RSS   Command
      0.40       0.00   921344   gateway

泄漏的特征是一直涨;这里涨到一个值就平了,次缺页接近零说明它也没在长新页。分配器(这里是 glibc)在 free 之后不一定把内存还给内核,进程的驻留内存因此只涨不落;书里说,对简单的分配器这是正常的(§7.3.3)。要它降下来,可以在空闲时 malloc_trim(0),或者试别的分配器。如果一周后发现它每天都多几十 MB,那才去用 memleak 或缺页采样找代码路径。page cache 不算在进程的 RSS 里(mmap 的文件页除外)。

板子上的摄像头进程被杀了,下一步做什么?§7.3.2

场景:一块 512 MB 内存、没有 swap 的 ARM 板,camd 跑了大约两天后消失。dmesg 里有:

Out of memory: Killed process 812 (camd)
total-vm:402112kB, anon-rss:318420kB

anon-rss 是进程自己的匿名内存(堆、栈、匿名 mmap),318 MB 在 512 MB 的板子上占了六成多。跑两天才出事,像是缓慢增长或泄漏,先每隔几分钟读一次 /proc/PID/status 的 VmRSS,画出走向。免杀只是让内核去杀别的进程,内存照样不够,下一个被杀的可能是更要紧的服务。清页缓存腾不出匿名内存。关掉 overcommit 后 malloc 会更早失败,camd 要是不检查返回值照样崩,只是换了一种死法。没有 swap 就没有先变慢的缓冲期(§7.3.2),所以要靠监控提前发现。

服务变慢了,这组数说明什么?§7.5.1

场景:一台 16 GB 的服务器,配了 4 GB swap,同时跑你的服务和一个新上的批处理任务。

vmstat 1:
   swpd    free    si    so   us  sy  id  wa
 812044   98120  2310  4120    8  12  41  39
/proc/pressure/memory:
 full avg10=18.40 avg60=11.02

si、so 每秒几 MB(vmstat 默认以 KiB 为单位),持续不为零就是在换匿名页,书里说这是内存压力的明确信号(§7.5.1)。线程访问被换出的页要等磁盘,wa 高是这个后果,不是原因,换更快的盘只能缓解。PSI 的 full 不为零更严重:那段时间所有可运行的任务都在等内存(§7.5.2)。下一步 ps aux --sort=-rss | head 看是不是批处理吃掉了内存,限它的内存或者错开时间。

运维说这个进程占了 36 GB,可机器只有 16 GB。怎么回事?§7.2.3

场景:一个 64 线程的 C++ 服务,跑在 16 GB 的机器上,一切正常。

$ ps -o pid,vsz,rss,comm -p 2741
  PID       VSZ      RSS   COMMAND
 2741  37748736   624380   server

按需分页让「分配」和「占用」分开:地址划出来不碰就不占物理页(§7.2.3)。线程多的程序 VSZ 动辄几十 GB,这在 Linux 上很常见,overcommit 也允许这样。被换出的页确实算在 VSZ 里,但一台 16 GB 的机器很少配几十 GB 的 swap,服务又一切正常,用 vmstat 看一眼 si、so 就能排除。top 的 VIRT 和 ps 的 VSZ 是同一个数。要看真实占用,看 RSS;多进程共享库多的,看 PSS。

值得停下来的句子

Memory growth issues are often misidentified as memory leaks. The first question to ask is: Is it supposed to do that?— §7.4.6。本站译:内存增长的问题常常被误判成内存泄漏。第一个要问的问题是:它本来就该这样吗?

「内存一直涨」报到你这里时,最省时间的一步不是开 valgrind,而是去看配置:缓存上限设了多少、连接池多大、队列有没有上限。很多「泄漏」是缓存在按设计填满,或者分配器留着已经 free 的内存。把这个问题问在前面,排查就分成两条清楚的路:设计如此,就改配置或改用法;不该如此,才去找没释放的代码路径。

With swap, this is likely to first become a performance issue due to paging, which is an opportunity to debug the issue live.— §7.3.2。本站译:有 swap 时,这类问题多半先表现为换页带来的性能问题,这让你有机会在现场调试。

这句话说的是内存一直涨的应用。对 ARM 板的读者,它解释了为什么问题总是「跑了两天突然没了」:没有 swap,就没有那段慢下来的预警期,等你看到时进程已经被杀、现场也没了。能做的是把预警挪到前面:定时记关键进程的 RSS、盯 /proc/pressure/memory,或者在 OOM 之前由自己的看门狗按阈值重启并留下记录。要不要为此配 swap(比如 zram),要看你的存储寿命和延迟要求,书里没有替你决定。

带走的几条

一句话:先看换页和压力判断够不够,再看 RSS 的走向判断是不是在涨,最后用缺页采样找是谁长起来的。

看 available,不看 free

free 列小是缓存在干活。内存真不够的信号是 si / so 不为零、页扫描、PSI 在涨、dmesg 里有 OOM。

§7.4.2
VSZ 不是占用

malloc 只划地址,第一次写才给物理页。占内存看 RSS,共享库多时看 PSS(smaps_rollup、pmap -X)。

§7.2.3
次缺页就是 RSS 在长

匿名内存大致每 4 KB 一次。pidstat -r 的 minflt/s 看速度,perf record -e page-faults 看是哪条调用栈。

§7.5.10
RSS 不回落不等于泄漏

分配器 free 后不一定还给内核。看它会不会继续涨:平了多半是分配器留着,一直涨才找泄漏。

§7.4.6
没有 swap 就没有预警期

板子和很多云主机不配 swap,内存用完直接 OOM。靠定时记 RSS 和 PSI 提前发现。

§7.3.2
容器按限额算

宿主机内存再多,cgroup 到了 memory.max 照样回收、照样杀。看 memory.events 的 oom_kill。

§7.6.5