1
2
3
4
5
6
7
作者:李晓辉

联系方式:

1. 微信:Lxh_Chat

2. 邮箱:939958092@qq.com
  1. Linux 性能调优系列(一) 建立科学的性能分析思路
  2. Linux 性能调优系列(二) 掌握基础性能监控命令
  3. Linux 性能调优系列(三) sysstat实现性能数据持久采集
  4. Linux 性能调优系列(四) 别只会用 sysstat,PCP 才是 Linux 性能监控的进阶玩法
  5. Linux 性能调优系列(五) 给你的服务器做一次全面体检
  6. Linux 性能调优系列(六) 想调内核参数?先搞懂 /proc、sysctl 和 sysfs
  7. Linux 性能调优系列(七) 还在手动改 sysctl?TuneD 智能性能调优实战来了
  8. Linux 性能调优系列(八) 进程太能吃资源怎么办?用 ulimit 和 systemd 给它加个"上限"
  9. Linux 性能调优系列(九) cgroup v2实战:防止一个烂进程干崩整台服务器
  10. Linux 性能调优系列(十) CPU 100% 到底是谁在忙?用 perf 把"元凶"揪出来
  11. Linux 性能调优系列(十一) 程序为什么卡住了?strace 和 ltrace 带你找到答案
  12. Linux 性能调优系列(十二) 服务器又卡了?SystemTap 带你杀到内核看看为啥
  13. Linux 性能调优系列(十三) 从 SystemTap 到 eBPF,Linux 性能追踪进入新时代
  14. Linux 性能调优系列(十四) CPU 这么忙,到底是谁决定“让谁先跑”?一文搞懂 Linux 调度器
  15. Linux 性能调优系列(十五) CPU 到底该先给谁?搞懂 Linux 实时调度
  16. Linux 性能调优系列(十六) Linux CPU 隔离实战,为什么实时线程绑核后延迟还会抖?
  17. Linux 性能调优系列(十七) CPU 占用不高却跑的慢?深挖 CPU 缓存性能瓶颈
  18. Linux 性能调优系列(十八) 程序申请了1GB内存,为什么系统却说它没用?
  19. Linux 性能调优系列(十九) 服务器硬盘慢得像蜗牛?先别急着换硬盘,这四招先把瓶颈找出来
  20. Linux 性能调优系列(二十) 磁盘 I/O 调度器怎么选?一文搞懂 mq-deadline、kyber、bfq 和 none
  21. Linux 性能调优系列(二十一) 磁盘为什么慢?从文件系统到 I/O 调优,一次讲透
  22. Linux 性能调优系列(二十二) Linux 网络调优别乱改 sysctl!先找瓶颈再调 TCP 缓冲区
  23. Linux 性能调优系列(二十三) MTU 从 1500 到 9000,巨帧真的能提升性能吗?
  24. Linux 性能调优系列(二十四) 从 CPU 省电到网卡卸载,聊聊 Linux 网络调优
  25. Linux 性能调优系列(二十五) 聊聊VirtIO、TuneD、virtio-mem 调优

上一篇我们讲了 CPU 缓存:缓存没命中,请求才会一路走到内存。那么问题来了:走到内存以后,又发生了什么?

先看一个现象。你在 8 GB 的机器上,让程序 malloc 了 1 GB,ps 一看:

1
2
VSZ  1050000
RSS 1200

VSZ 一个多 GB,RSS 才 1 MB 出头。这 1 GB 到底有没有”拿到”?

这一篇我们就从这个现象出发,把内存这一层讲清楚:地址怎么翻译、缺页是怎么回事、为什么 TLB 会拖慢数据库、大页怎么用、又该怎么限制一个进程的内存。

进程为什么不直接碰物理内存

内核把内存切成固定大小的块,叫页(page)。x86_64 上标准页是 4 KiB。物理内存里装一页数据的位置,叫页框(page frame)。关键点:进程看到的地址全是虚拟的。 每个进程都有一套私有的虚拟地址空间,内核用一张页表,把”虚拟页”映射到”物理页框”。

flowchart LR
    subgraph A["进程 A 的虚拟地址空间"]
        A1["虚拟地址 0x7f00...1000"]
    end
    subgraph B["进程 B 的虚拟地址空间"]
        B1["虚拟地址 0x7f00...1000"]
    end
    A1 -- "进程 A 的页表" --> P1["物理页框 #1001"]
    B1 -- "进程 B 的页表" --> P2["物理页框 #2087"]

两个进程里完全相同的虚拟地址,落在不同的物理页框上,互相看不见。这套机制一举解决了两件事:

  • 隔离:进程只能访问内核映射给它的页框,越界直接触发异常;
  • 灵活:进程拿到的是一大片”连续”的虚拟地址,物理上可以东一块西一块,也可以暂时不在内存里。

虚拟地址空间分成两半:低半部分是用户空间,放进程自己的代码和数据;高半部分是内核空间,放内核。

老资料常说”内核映射进每个进程,所以系统调用开销小”。这在今天要打个折扣:开启了 KPTI(防 Meltdown)的 CPU 上,用户态页表里基本不再映射内核,只留一小段入口。可以这样查你的机器:

1
2
3
[root@localhost ~]# cat /sys/devices/system/cpu/vulnerabilities/meltdown
Not affected
# Not affected:不需要 KPTI;Mitigation: PTI:已开启

虚拟地址空间到底有多大

很多资料会说”64 位系统的地址空间是 2⁶⁴,也就是 16 EiB”。这是指针的宽度,不是你能用的空间。

x86_64 实际只使用其中一部分:

模式有效虚拟地址位总虚拟空间其中用户态页表层级
四级分页(主流)48 位256 TiB128 TiB4 级
五级分页(新 CPU)57 位128 PiB64 PiB5 级

剩下的高位不是随便”保留”,而是要求做符号扩展(规范地址):高位必须和最高有效位一致,否则 CPU 直接报异常。所以你会看到用户地址总是 0x00007f...,内核地址总是 0xffff...。

查看你的机器:

1
2
3
4
5
6
[root@localhost ~]# lscpu | grep -i 'address sizes'
Address sizes: 45 bits physical, 48 bits virtual


[root@localhost ~]# grep -o -m1 la57 /proc/cpuinfo
# 有输出,说明 CPU 支持五级分页,我的没输出,就说明不支持

两个小提醒:

  • 物理位数和虚拟位数是两回事。 45 bits physical 表示这台机器最多能寻址 32 TiB 物理内存;48 bits virtual 表示用四级分页。装多少内存和虚拟地址空间多大,没有关系;
  • 即使 CPU 和内核支持五级分页,默认 mmap 也只会返回 47 位以内的地址,是为了兼容那些把高位拿来存标记的程序。应用要主动请求高地址,才会用到更大的空间。

一个虚拟地址是怎么拆开的

以 4 KiB 页为例,一个虚拟地址分成两部分:

部分位数作用
虚拟页号(VPN)36 位(四级)或 45 位(五级)找到是哪一页
页内偏移12 位(2¹² = 4096)页内的第几个字节

页号部分再按每 9 位切一段,每一段对应一级页表:

flowchart LR
    VA["虚拟地址<br/>9 + 9 + 9 + 9 + 12 位"] --> L4["PML4 表<br/>CR3 寄存器指向它"]
    L4 --> L3["PDPT 表"]
    L3 --> L2["PD 表"]
    L2 --> L1["PT 表"]
    L1 --> PG["物理页框 4 KiB<br/>加上 12 位页内偏移"]

五级分页就是在最顶上再多一层 PML5。每一张表有 512 项(2⁹),每项 8 字节,刚好占满一个 4 KiB 页。

页表为什么要分层

如果不分层,一张平铺的页表要多大?算一下:

  • 四级:2³⁶ 个条目 × 8 字节 = 2³⁹ 字节 = 512 GiB;
  • 五级:2⁴⁵ 个条目 × 8 字节 = 2⁴⁸ 字节 = 256 TiB。

每个进程一张这样的表,显然不可能。所以内核用多级结构:只为进程真正用到的那部分地址范围,才分配对应的目录和页表。一个只用了几十 MB 的进程,页表也就几十 KB。

代价是:翻译一次地址,要逐级查表,四级就是最多 4 次内存访问。这很慢。

TLB:给地址翻译加一层缓存

为了避免每次都查四级表,CPU 里有一块专用的硬件缓存,叫 TLB(转换后备缓冲区),缓存最近用过的”虚拟页到物理页”映射。

  • TLB 命中:直接拿到物理地址,几乎没有额外开销;
  • TLB 未命中:硬件要走一遍多级页表(page walk),再把结果填回 TLB。

程序有局部性,连续访问同一页的数据,第二次起就能命中。但如果程序在很大的内存范围里到处跳,TLB 就会频繁未命中。

可以这样观察:

1
2
3
4
5
6
7
8
9
10
11
12
[root@localhost ~]# perf stat -e dTLB-load-misses,iTLB-load-misses sleep 1000000
^Csleep: Interrupt

Performance counter stats for 'sleep 1000000':

0 dTLB-load-misses
0 iTLB-load-misses

8.438337754 seconds time elapsed

0.000000000 seconds user
0.000925000 seconds sys

和上一篇一样:虚拟机里需要开启 vPMU,否则这些事件显示 <not supported>,并不代表没有未命中。

进程占了多少内存:VSZ、RSS、PSS

回到开头的问题。进程申请内存时,内核只是给了它一段虚拟地址,并不会立刻分配物理页框。等进程真正去读写这块内存时,才会触发分配。

所以工具里有两个口径:

指标含义
VSZ / VIRT进程申请的虚拟内存总量
RSS / RES当前真正映射在物理内存里的部分
1
2
3
[root@localhost ~]# ps -o pid,vsz,rss,minflt,majflt,comm -p 1019
PID VSZ RSS MINFLT MAJFLT COMMAND
1019 8792 6260 856 6 sshd

动手做个实验

写一个”先申请、后使用”的小程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// vm_demo.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

int main(void) {
size_t sz = 1UL << 30; // 1 GiB
char *p = malloc(sz); // 只是申请,没有使用
printf("pid=%d,已申请 1 GiB,还没碰\n", getpid());
fflush(stdout);
sleep(30);

memset(p, 1, sz / 2); // 真正写入一半
printf("已写入 512 MiB\n");
fflush(stdout);
sleep(30);
return 0;
}
1
2
3
4
5
6
7
8
9
10
[root@localhost ~]# gcc -O1 -o vm_demo vm_demo.c
[root@localhost ~]# ./vm_demo
pid=186943,已申请 1 GiB,还没碰

# 立刻看一次
[root@localhost ~]# ps -o pid,vsz,rss,minflt,majflt,comm -p 186943
PID VSZ RSS MINFLT MAJFLT COMMAND
186943 2444 1708 86 0 vm_demo

# 30 秒后再看一次

你会看到:

  • 第一次:VSZ 约 1 GiB,RSS 只有几 MB;
  • 第二次:RSS 涨到约 512 MiB,minflt(次要缺页)也随之增加。

如果你的 THP 是 always,第二次的 minflt 增量可能比”512 MiB ÷ 4 KiB ≈ 13 万次”小得多,因为一次缺页就能拿到一个 2 MiB 大页。这个现象后面讲 THP 时你会再遇到,具体数字以你的实测为准。

为什么申请了这么多也不报错

1
2
[root@localhost ~]# cat /proc/sys/vm/overcommit_memory
0

默认值 0 是启发式超配:内核只拒绝”明显离谱”的申请,其余先答应下来。所以才有**”申请成功,用的时候才 OOM”**这种情况。

RSS 的一个陷阱:共享页重复计数

多个进程共用同一个库,这个库的物理页只有一份,但每个进程的 RSS 里都会把它算一遍。所以:

把所有进程的 RSS 加起来,往往比真实占用大。

想看进程的”公平份额”,用 PSS(共享页按使用者数量均摊):

1
2
3
4
5
6
7
8
9
10
11
12
13
[root@localhost ~]# grep -E '^(Rss|Pss|Shared_|Private_)' /proc/1019/smaps_rollup
Rss: 6264 kB
Pss: 1841 kB
Pss_Dirty: 1176 kB
Pss_Anon: 1176 kB
Pss_File: 665 kB
Pss_Shmem: 0 kB
Shared_Clean: 4752 kB
Shared_Dirty: 0 kB
Private_Clean: 336 kB
Private_Dirty: 1176 kB
Shared_Hugetlb: 0 kB
Private_Hugetlb: 0 kB

限制进程的内存:cgroup v2 与 systemd

RHEL 9、10 用的是 cgroup v2,内存限制的参数是这几个:

参数作用
MemoryHigh软上限:超过后被限流、加快回收,不杀进程
MemoryMax硬上限:超过且回收不下来,触发 cgroup 内的 OOM
MemorySwapMax限制可使用的 swap
MemoryLow / MemoryMin保护:内存紧张时尽量不回收这部分

老资料里的 MemoryLimit= 是 cgroup v1 的参数,已经被弃用,别再用了。数值支持 K、M、G、T 后缀,以 1024 为基数。

对现有服务设置

1
2
3
4
5
6
7
8
9
# 立即生效并持久化(写入 /etc/systemd/system.control/)
systemctl set-property myapp.service MemoryMax=1G MemoryHigh=900M

# 只想临时生效,重启后恢复
systemctl set-property --runtime myapp.service MemoryMax=1G

# 验证
systemctl show myapp.service -p MemoryMax -p MemoryHigh
cat /sys/fs/cgroup/system.slice/myapp.service/memory.max

set-property 会立即生效,不需要 daemon-reload。如果你是手写 drop-in 文件,才需要 systemctl daemon-reload。

经验:优先配 MemoryHigh 让它先被限流,再用 MemoryMax 兜底。只设 MemoryMax 的话,进程会直接撞墙被杀,没有缓冲。

缺页:不是错误,是常态

进程访问一个页表里还没有有效映射的虚拟页,CPU 会触发缺页异常,进入内核处理。名字叫”错误”,其实大多数时候是正常流程。

flowchart TD
    A["进程访问虚拟地址"] --> B{"TLB 命中?"}
    B -- 是 --> Z["直接得到物理地址"]
    B -- 否 --> C{"页表里有有效映射?"}
    C -- 是 --> D["硬件填充 TLB,继续执行"]
    C -- 否 --> E["触发缺页异常,进入内核"]
    E --> F{"数据在哪里?"}
    F -- "新分配一页,或已在页缓存" --> G["次要缺页:建立映射即可"]
    F -- "在磁盘或 swap 上" --> H["主要缺页:先做磁盘 I/O"]
    F -- "地址非法" --> I["SIGSEGV,进程崩溃"]

次要缺页(minor)

不需要磁盘 I/O。典型情况:

  • 首次访问刚申请的内存,内核分配一个新页框;
  • 数据其实已经在内存里(比如页缓存),只是这个进程的页表还没建立映射;
  • 写时复制(fork 之后第一次写)。

开销小,一般是微秒量级。

主要缺页(major)

必须从磁盘把数据读回来:页面被换出到 swap,或者程序、库的代码还没加载进内存。要等磁盘,开销是毫秒量级,比次要缺页高几个数量级。

怎么观察

1
2
3
4
5
6
7
8
9
10
11
12
# 每个进程累计的缺页数(从进程启动开始计)
ps -o pid,minflt,majflt,comm -p <PID>

# 一个命令跑完后的统计
/usr/bin/time -v ./your_app 2>&1 | grep -i 'page faults'

# 系统级,每秒的缺页(sysstat)
sar -B 1
# 关注 pgfault/s 和 majflt/s

# perf,软件事件,虚拟机里也能用
perf stat -e minor-faults,major-faults ./your_app

判断标准:minflt 很高通常不用担心,那是程序在正常地”开荒”;majflt 持续偏高才是危险信号,说明系统在频繁地等磁盘,常见原因是内存不足引发了 swap。

为什么要用大页

回到 TLB。TLB 的条目数是固定且很小的,通常只有几百到一两千条。每条只管一页:

1
TLB 覆盖的内存 = 条目数 × 页大小

假设某个 TLB 有 1500 条(实际因 CPU 型号和页大小而异,这里只是帮你建立量级概念):

页大小TLB 能覆盖的内存
4 KiB约 6 MiB
2 MiB约 3 GiB

数据库这类进程动辄占几十 GB,用 4 KiB 页时 TLB 几乎永远覆盖不过来,未命中非常多。换成 2 MiB 页,同样的 TLB 条目能覆盖 512 倍的内存。

大页还有第二个好处:页表查得更短。

  • 2 MiB 页:页目录(PD)的条目直接指向这个页,少查一级 PT,页内偏移是 21 位;
  • 1 GiB 页:PDPT 的条目直接指向,少查两级,页内偏移是 30 位。

x86_64 支持 2 MiB 和 1 GiB 两种大页,RHEL 默认大页大小是 2 MiB:

1
2
grep -i hugepagesize /proc/meminfo
# Hugepagesize: 2048 kB

RHEL 有两种大页机制:静态大页(HugeTLB)和透明大页(THP)。

静态大页 HugeTLB:先预留,再使用

静态大页要提前预留,而且只有显式请求的进程才能用。预留出来的内存,普通进程就用不了了,free 里会把它算作 used。

方式一:启动时预留(推荐)

启动时内存还没碎片化,成功率最高。用 grubby 写进所有内核条目:

1
2
# 预留 10 个默认大小(2 MiB)的大页
grubby --update-kernel=ALL --args="hugepages=10"

要同时预留 2 MiB 和 1 GiB 两种大页:

1
2
grubby --update-kernel=ALL \
--args="hugepagesz=2M hugepages=10 hugepagesz=1G hugepages=1"

每个 hugepages= 都归属于它前面最近的 hugepagesz=,所以顺序很重要。漏写第二个 hugepagesz=1G,那个 hugepages=1 就会被当成继续设置 2 MiB 页,这是个很隐蔽的坑。

重启后验证:

1
2
cat /proc/cmdline
grep -i huge /proc/meminfo

1 GiB 大页需要 CPU 支持,可用 grep -o -m1 pdpe1gb /proc/cpuinfo 确认。1 GiB 大页也强烈建议只在启动时预留,运行时很难凑出连续的 1 GiB 空间。

方式二:运行时预留

1
2
3
# 全局设置:预留 20 个 2 MiB 页
sysctl vm.nr_hugepages=20
grep -i huge /proc/meminfo

预期能看到:

1
2
3
4
HugePages_Total:      20
HugePages_Free: 20
Hugepagesize: 2048 kB
Hugetlb: 40960 kB

在多 NUMA 节点的机器上,可以按节点分别预留:

1
echo 20 > /sys/devices/system/node/node2/hugepages/hugepages-2048kB/nr_hugepages

注意两点:

  • 写入的是总数,不是增量。 再写一次 20,结果还是 20,不会变成 40;
  • 预留可能不足额。 内存碎片多时,内核凑不出足够的连续空间,实际数量会少于你写的。写完一定要读回确认。

用 numastat 看每个节点的情况:

1
numastat -cm | egrep 'Node|Huge'

这里的单位是 MB,20 个 2 MiB 的大页显示为 40。

持久化运行时预留:

1
2
echo "vm.nr_hugepages = 20" > /etc/sysctl.d/90-hugepages.conf
sysctl --system

进程怎么用大页

进程要显式请求,常见方式:

  • mmap() 加 MAP_HUGETLB 标志(匿名映射,不需要挂载 hugetlbfs);
  • 在 hugetlbfs 上创建文件再 mmap(),这种才需要挂载,RHEL 上 systemd 会自动挂在 /dev/hugepages;
  • shmget() 加 SHM_HUGETLB。

动手验证:没预留会怎样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// hugetlb_demo.c
#define _GNU_SOURCE
#include <stdio.h>
#include <string.h>
#include <sys/mman.h>
#include <unistd.h>

int main(void) {
size_t sz = 4UL << 20; // 4 MiB,即 2 个大页
void *p = mmap(NULL, sz, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS | MAP_HUGETLB, -1, 0);
if (p == MAP_FAILED) { perror("mmap"); return 1; }
memset(p, 1, sz);
printf("已使用 %zu MiB 大页,pid=%d\n", sz >> 20, getpid());
fflush(stdout);
sleep(60);
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
gcc -O1 -o hugetlb_demo hugetlb_demo.c

# 第一步:没有预留,直接跑
sysctl vm.nr_hugepages=0
./hugetlb_demo # 报 mmap: Cannot allocate memory

# 第二步:预留后再跑,另开一个终端观察
sysctl vm.nr_hugepages=20
./hugetlb_demo &
grep -E 'HugePages_(Total|Free|Rsvd)' /proc/meminfo
# HugePages_Free 会比 Total 少 2

这个实验能直观说明:静态大页是”要才有,没预留就没有”,不会像普通内存那样自动兜底。

透明大页 THP:内核自动帮你做

THP 不需要预留,内核会自动给符合条件的内存区域用 2 MiB 页,应用不用改代码。它主要作用于匿名内存。

1
2
cat /sys/kernel/mm/transparent_hugepage/enabled
# [always] madvise never 方括号里是当前模式

RHEL 9 默认是 always。RHEL 10 请以你机器上的实际输出为准。

三种模式

模式含义
always内核尽可能为符合条件的区域使用大页
madvise只对进程用 madvise(MADV_HUGEPAGE) 显式标记的区域使用
never禁用

临时切换与持久化

1
2
3
4
5
6
# 临时切换,重启失效
echo madvise > /sys/kernel/mm/transparent_hugepage/enabled

# 持久化:写进内核启动参数
grubby --update-kernel=ALL --args="transparent_hugepage=madvise"
reboot

怎么确认 THP 真的在工作

1
2
3
4
5
6
7
8
# 系统总量
grep AnonHugePages /proc/meminfo

# 某个进程
grep AnonHugePages /proc/<PID>/smaps_rollup

# 内核统计:成功分配多少、回退到小页多少、压缩停顿多少
grep -E 'thp_fault_alloc|thp_fault_fallback|compact_stall' /proc/vmstat

thp_fault_fallback 很高,说明内核想给大页但凑不出连续的 2 MiB,退回了小页,这通常是内存碎片的信号。

THP 的代价:延迟抖动

THP 的优点是省心,缺点是不可预测:

  • 同步压缩:缺页时如果凑不出连续 2 MiB,内核可能当场整理内存,进程就卡在缺页里。这是延迟尖刺的常见来源;
  • 内存膨胀:只用了几 KiB,却被分配了一整个 2 MiB 页。

控制压缩行为的是 defrag:

1
cat /sys/kernel/mm/transparent_hugepage/defrag
取值行为
always缺页时一定同步压缩,延迟风险最大
defer不同步压缩,交给后台线程慢慢整理
madvise只对 madvise 标记的区域同步压缩(常见默认值)
never从不同步压缩,凑不出就用小页

对延迟敏感的场景,可以保留 THP,但把同步压缩关掉:

1
echo never > /sys/kernel/mm/transparent_hugepage/defrag

数据库该怎么办

很多数据库厂商会建议关闭 THP,原因就是上面的延迟抖动。但”关了之后用什么”因产品而异:

  • Oracle、PostgreSQL 等支持静态 HugeTLB,可以关 THP 并配置静态大页;
  • Redis、MongoDB 一般只是建议把 THP 设成 never,并不使用静态大页。

以你用的数据库的官方文档为准,不要一刀切。

怎么选:一张决策图

flowchart TD
    A["应用占内存大,<br/>且 TLB 未命中偏高?"] -- 否 --> N["保持默认,不要折腾"]
    A -- 是 --> B{"对延迟抖动敏感?<br/>或厂商要求关闭 THP?"}
    B -- 是 --> C["THP 设 madvise 或 never<br/>应用支持的话用静态 HugeTLB<br/>启动时预留"]
    B -- 否 --> D["THP 用 always 或 madvise<br/>压测前后对比再决定"]

记住一点:大页不是万能加速器。 如果 TLB 未命中本来就不高,换大页收益很小,还可能引入新问题。先测量,再调。

常见误区

误区一:VSZ 大说明内存吃紧。
VSZ 只是申请量,看 RSS,更准确地看 PSS。

误区二:把所有进程的 RSS 加起来就是总占用。
共享页会被重复计算,求和会偏大。

误区三:缺页多就是有问题。
次要缺页是正常的”开荒”,持续的主要缺页才值得警惕。

误区四:MemoryLimit= 还能用。
那是 cgroup v1 的旧参数,现在用 MemoryMax= 和 MemoryHigh=。

误区五:nr_hugepages 写多少就加多少。
写入的是总数,而且可能因碎片而不足额。

误区六:预留了大页,所有进程都能用。
只有显式请求的进程才能用,预留的内存对普通进程不可用。

误区七:THP 一定更快。
它可能带来同步压缩的延迟尖刺。

命令速查

目的命令
虚拟、物理地址位数lscpu | grep -i 'address sizes'
是否支持五级分页grep -o -m1 la57 /proc/cpuinfo
进程内存与缺页ps -o pid,vsz,rss,minflt,majflt,comm -p PID
进程 PSSgrep -E '^(Rss|Pss)' /proc/PID/smaps_rollup
系统缺页速率sar -B 1
命令的缺页统计/usr/bin/time -v cmd
TLB 未命中perf stat -e dTLB-load-misses cmd
设置内存上限systemctl set-property svc MemoryMax=1G
验证内存上限systemctl show svc -p MemoryMax
大页大小与数量grep -i huge /proc/meminfo
运行时预留大页sysctl vm.nr_hugepages=N
启动时预留大页grubby --update-kernel=ALL --args="hugepages=N"
各 NUMA 节点大页numastat -cm | egrep 'Node|Huge'
THP 模式cat /sys/kernel/mm/transparent_hugepage/enabled
THP 使用量grep AnonHugePages /proc/meminfo
THP 压缩行为cat /sys/kernel/mm/transparent_hugepage/defrag

课后思考

  1. 程序 malloc 了 1 GiB 但没有使用,为什么 RSS 几乎不涨?什么时候才会涨?
  2. 为什么把所有进程的 RSS 相加,可能大于物理内存?该用什么指标代替?
  3. 一次五级页表的地址翻译,最多要访问几次内存?用 2 MiB 大页后变成几次?
  4. 运行时执行 echo 20 > nr_hugepages 两次,最终是 20 还是 40?读回来发现少于 20,原因是什么?
  5. 某数据库偶尔出现几十毫秒的延迟尖刺,THP 是 always,你会先看哪几个指标来判断是不是 THP 引起的?

参考手册

1
2
3
4
5
6
7
man 5 proc               # smaps_rollup、meminfo 等字段
man 7 cgroups
man systemd.resource-control
man perf-stat
man numastat
man mmap
man madvise