我看青山,青山悲悯
持续分享技术干货,感兴趣小伙伴可以关注下 ^_^
当Linux 启动一个程序时,会先给程序分配合适的虚拟地址空间
,也就是我们申请的内存大小,不会把所有虚拟地址空间都映射到物理内存,而是把程序在运行中需要的数据,映射到物理内存,需要时可以再动态映射分配物理内存
因为每个进程都维护着自己的虚拟地址空间
,每个进程都有一个页表来定位虚拟内存到物理内存的映射
,每个虚拟内存也在表中都有一个对应的条目
当进程访问虚拟地址,但是在映射的页面中查不到对应的物理地址时,内核就会产生一个缺页异常(Page Fault)
,此时会重新分配物理内存,更新映射页表
。
在内存访问中,在验证页表项通过之后,查询页表数据标记为不存在
,会促发缺页中断
,会重新分配物理页帧
(从空闲内存或通过页面置换算法如 LRU 淘汰旧页),或者磁盘(如交换分区或文件)加载数据到物理页帧,更新页表项,标记为有效,重新执行触发缺页的指令。
通过页表项获得物理页帧基地址
,加上虚拟地址中的页内偏移
,可以得到最终物理地址。MMU
将物理地址发送到内存总线,CPU
读取或写入物理内存,同时会更新 TLB
,下次使用直接读取 TLB
的数据。
内核产生一个 page fault
异常事件分为两种:
minor fualt
当进程缺页事件发生在第一次访问虚拟内存时
,虚拟内存已分配但未映射(如首次访问、写时复制、共享内存同步)物理地址,内核会产生一个 minor page fualt
,并分配新的物理内存页。minor page fault
产生的开销比较小,minor page fualt 典型场景:
首次访问
:进程申请内存后,内核延迟分配物理页(Demand Paging)
,首次访问时触发。写时复制(COW)
:fork()
创建子进程时共享父进程内存,子进程写操作前触发
共享库加载
:动态链接库
被多个进程共享,首次加载到物理内存时触发,即会共享页表major fault
当物理页未分配且需从磁盘(Swap分区或文件)加载数据,内核就会产生一个 majorpage fault
,比如内核通过Swap分区,将内存中的数据交换出去放到了硬盘,需要时从硬盘中重新加载程序或库文件的代码到内存。涉及到磁盘I/O,因此一个major fault
对性能影响比较大,典型场景有
Swap In
:物理内存不足时,内核将内存页换出到 Swap 分区,再次访问需换回。文件映射(mmap)
:通过 mmap 映射文件到内存,首次访问文件内容需从磁盘读取。Minor Fault
是内存层面的轻量级操作,涉及到实际的物理内存分配,也是今天我们要跟踪的,Major Fault
是涉及磁盘I/O
的重型操作。频繁的 Major Fault
就需要考虑性能问题, 对于缺页异常,我们可以通过传统工具比如 ps、vmstat、perf
等工具来定位性能瓶颈
下面是我们实验用到的一个 Demo ,通过 perf
跟踪缺页异常
┌──[root@liruilongs.github.io]-[~/bpfdemo]
└─$perfstat -e minor-faults,major-faults ./anon2mmap
PID = 13619
Allocated 0 GB
Allocated 1 GB
Allocated 2 GB
Allocated 3 GB
Allocated 4 GB
Allocated 5 GB
Allocated 6 GB
Allocated 7 GB
Total iterations: 2097152
Successfully mapped 8 GB
^C./anon2mmap: Interrupt
Performance counter stats for'./anon2mmap':
4152 minor-faults
0 major-faults
22.012862749 seconds time elapsed
0.034524000 seconds user
3.493099000 seconds sys
┌──[root@liruilongs.github.io]-[~/bpfdemo]
└─$
anon2mmap
通过 mmap
分配了8GB匿名内存,可以看到用户态CPU耗时 0.03,内核态 CPU 时间 3.49,缺页异常主要发生在 minor,实际中当前的生产环境中,考虑 交换分区的性能问题,一般在会准备机器的时候关闭交换分区。在内存使用中通过 Cgroup 对资源进行限制。通过 Qos 合理控制内存的超售问题
下面是我们测试用的 Demo,通过 mmap 分配一大块匿名内存,然后填充数据触发缺页异常,下面所有的Demo 都基于这个程序
┌──[root@liruilongs.github.io]-[~/bpfdemo]
└─$cat anon2mmap.c
#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <unistd.h>
#define GB ((long long) 1024 * 1024 * 1024 )
int main() {
printf("PID = %d\n", getpid());
//sleep(30);
long long size = 8 * GB; // 映射64MB内存
void *ptr = mmap(NULL, size, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if (ptr == MAP_FAILED) {
perror("mmap failed");
return 1;
}
// 填充数据以触发实际内存分配
for (long long i = 0; i < size; i += 4096) {
((char *)ptr)[i] = 'A';
if (i % (GB) == 0) { //
printf("Allocated %lld GB\n", i / GB);
}
}
printf("Successfully mapped %lld GB\n", size / GB);
munmap(ptr, size);
return 0;
}
┌──[root@liruilongs.github.io]-[~/bpfdemo]
└─$
跟踪缺页错误和对应的调用栈信息,可以为内存用量分析提供一个新的视角,不同于我们之前讲的 brk 和 mmap 是虚拟内存分配的角度去分析内存用量,缺页异常会直接影响系统常驻内存的的增长,也就是物理内存的增长。
跟踪方式主要利用内核静态跟踪点以及软件跟踪点
┌──[root@liruilongs.github.io]-[~/bpfdemo]
└─$sudo perf list | grep page_fault
exceptions:page_fault_kernel [Tracepoint event] #用户态触发的缺页异常
exceptions:page_fault_user [Tracepoint event] #内核态触发的缺页异常
iommu:io_page_fault [Tracepoint event] #IOMMU(输入输出内存管理单元)触发的缺页异常(常见于虚拟化或设备直通场景)
软件跟踪点,实际上也是基于内核静态跟踪点,对多种缺页异常进行统计
┌──[root@liruilongs.github.io]-[~/bpfdemo]
└─$
┌──[root@liruilongs.github.io]-[~/bpfdemo]
└─$perf list | grep page-faults
page-faults OR faults [Software event]
stackcount 可能是我们用的最多的一个 BPF 工具,用于对特定函数进行跟踪,可以是静态跟踪点,也可以是动态跟踪点,下面的命令, -p 指定进程ID,后面为内核静态跟踪点的表达式,这里跟踪用户态的缺页异常 page_fault_user
┌──[root@liruilongs.github.io]-[~/bpfdemo]
└─$/usr/share/bcc/tools/stackcount -p 9147 t:exceptions:page_fault_user
Tracing 1 functions for "t:exceptions:page_fault_user"... Hit Ctrl-C to end.
^C
exc_page_fault
exc_page_fault
asm_exc_page_fault
[unknown]
[unknown]
4096
Detaching...
默认情况下会同时输出 用户态和内核态的调用栈,内核态调用栈显示缺页异常由 asm_exc_page_fault(汇编层入口)
触发,最终调用exc_page_fault(缺页处理函数)
。[unknown] 表示用户态调用栈未捕获或符号解析失败,4096 表示该调用路径发生了 4096 次缺页事件。
添加 -U
选项,只输出用户态的调用栈数据,但是这里的用户态调用栈没有解析出函数名
┌──[root@liruilongs.github.io]-[~/bpfdemo]
└─$/usr/share/bcc/tools/stackcount -p 9190 -U t:exceptions:page_fault_user
Tracing 1 functions for "t:exceptions:page_fault_user"... Hit Ctrl-C to end.
^C
[unknown]
[unknown]
4096
Detaching...
┌──[root@liruilongs.github.io]-[~/bpfdemo]
└─$
trace 也是一个比较常用的 BPF 工具,用于跟踪函数调用时函数签名相关信息,通过 trace 我们可以获取用户态的调用栈,解决上面的问题,运行程序 Demo
┌──[root@liruilongs.github.io]-[~/bpfdemo]
└─$./anon2mmap
PID = 9261
Allocated 0 GB
Allocated 1 GB
Allocated 2 GB
Allocated 3 GB
Allocated 4 GB
Allocated 5 GB
Allocated 6 GB
Allocated 7 GB
Total iterations: 2097152
Successfully mapped 8 GB
通过 trace 来跟踪缺页函数调用,通上面的 stackcount 工具我们可以知道调用了 4096 次缺页分配函数,所以通过 teace 跟踪可以看到很多数据,这里我们只展示部分
┌──[root@liruilongs.github.io]-[~/bpfdemo]
└─$/usr/share/bcc/tools/trace -p 9261 -U t:exceptions:page_fault_user
PID TID COMM FUNC
....................
9261 9261 anon2mmap page_fault_user
main+0x99 [anon2mmap]
__libc_start_call_main+0x80 [libc.so.6]
9261 9261 anon2mmap page_fault_user
main+0x99 [anon2mmap]
__libc_start_call_main+0x80 [libc.so.6]
9261 9261 anon2mmap page_fault_user
main+0x99 [anon2mmap]
__libc_start_call_main+0x80 [libc.so.6]
9261 9261 anon2mmap page_fault_user
main+0x99 [anon2mmap]
__libc_start_call_main+0x80 [libc.so.6]
9261 9261 anon2mmap page_fault_user
main+0x99 [anon2mmap]
__libc_start_call_main+0x80 [libc.so.6]
9261 9261 anon2mmap page_fault_user
main+0x99 [anon2mmap]
__libc_start_call_main+0x80 [libc.so.6]
9261 9261 anon2mmap page_fault_user
main+0x99 [anon2mmap]
__libc_start_call_main+0x80 [libc.so.6]
9261 9261 anon2mmap page_fault_user
main+0x99 [anon2mmap]
__libc_start_call_main+0x80 [libc.so.6]
9261 9261 anon2mmap page_fault_user
main+0x99 [anon2mmap]
__libc_start_call_main+0x80 [libc.so.6]
............................................
9261 9261 anon2mmap page_fault_user
main+0x99 [anon2mmap]
__libc_start_call_main+0x80 [libc.so.6]
9261 9261 anon2mmap page_fault_user
main+0x99 [anon2mmap]
__libc_start_call_main+0x80 [libc.so.6]
9261 9261 anon2mmap page_fault_user
main+0x99 [anon2mmap]
__libc_start_call_main+0x80 [libc.so.6]
PID 9261(进程名 anon2mmap)
频繁触发用户态缺页异常(page_fault_user)
,每次缺页异常的调用栈完全相同,表明所有缺页均源于 main 函数的同一代码位置(偏移 0x99),可能是循环或重复操作中访问未映射的内存区域,
通过 free 命令可以实时的观察 物理内存得变化
┌──[root@liruilongs.github.io]-[~]
└─$free -h -s 0.1 -c 1000
total used free shared buff/cache available
Mem: 15Gi 856Mi 14Gi 11Mi 649Mi 14Gi
Swap: 2.0Gi 0B 2.0Gi
total used free shared buff/cache available
Mem: 15Gi 2.0Gi 12Gi 11Mi 649Mi 13Gi
Swap: 2.0Gi 0B 2.0Gi
total used free shared buff/cache available
Mem: 15Gi 3.3Gi 11Gi 11Mi 649Mi 12Gi
Swap: 2.0Gi 0B 2.0Gi
total used free shared buff/cache available
Mem: 15Gi 4.4Gi 10Gi 11Mi 649Mi 10Gi
Swap: 2.0Gi 0B 2.0Gi
total used free shared buff/cache available
Mem: 15Gi 5.6Gi 9.3Gi 11Mi 649Mi 9.7Gi
Swap: 2.0Gi 0B 2.0Gi
total used free shared buff/cache available
Mem: 15Gi 6.6Gi 8.3Gi 11Mi 649Mi 8.7Gi
Swap: 2.0Gi 0B 2.0Gi
total used free shared buff/cache available
Mem: 15Gi 7.7Gi 7.2Gi 11Mi 649Mi 7.6Gi
Swap: 2.0Gi 0B 2.0Gi
total used free shared buff/cache available
Mem: 15Gi 8.5Gi 6.4Gi 11Mi 649Mi 6.8Gi
Swap: 2.0Gi 0B 2.0Gi
total used free shared buff/cache available
Mem: 15Gi 852Mi 14Gi 11Mi 649Mi 14Gi
Swap: 2.0Gi 0B 2.0Gi
faults 是一个 bpftrace 工具,通过统计软件跟踪点,对缺页异常进行统计,同时会输出缺页异常的调用栈,可以看作是上面两个工具的结合
下面的代码地址
https://github.com/brendangregg/bpf-perf-tools-book/blob/master/originals/Ch07_Memory/faults.bt
┌──[root@liruilongs.github.io]-[/usr/share/bpftrace/tools]
└─$cat ./faults.bt
#!/usr/bin/bpftrace
/*
* faults - Count page faults with user stacks.
*
* See BPF Performance Tools, Chapter 7, for an explanation of this tool.
*
* Copyright (c) 2019 Brendan Gregg.
* Licensed under the Apache License, Version 2.0 (the "License").
* This was originally created for the BPF Performance Tools book
* published by Addison Wesley. ISBN-13: 9780136554820
* When copying or porting, include this comment.
*
* 27-Jan-2019 Brendan Gregg Created this.
*/
software:page-faults:1
{
@[ustack,pid, comm] = count();
}
┌──[root@liruilongs.github.io]-[/usr/share/bpftrace/tools]
└─$
输出跟踪结果,返回用户态函数调用栈,以及缺页函数调用次数
┌──[root@liruilongs.github.io]-[/usr/share/bpftrace/tools]
└─$./faults.bt
Attaching 1 probe...
^C
@[
main+153
__libc_start_call_main+128
, 9684, anon2mmap]: 4096
┌──[root@liruilongs.github.io]-[/usr/share/bpftrace/tools]
└─$
faults(8) 也是一个 bpftrace 工具,根据文件名来跟踪缺页错误
,这里的文件名,是一些文件映射内存的场景,如果使用匿名内存是无法跟踪的。
代码地址:
https://github.com/brendangregg/bpf-perf-tools-book/blob/master/originals/Ch07_Memory/ffaults.bt
┌──[root@liruilongs.github.io]-[/usr/share/bpftrace/tools]
└─$cat ffaults.bt
#!/usr/bin/bpftrace
/*
* ffaults - Count page faults by filename.
*
* See BPF Performance Tools, Chapter 7, for an explanation of this tool.
*
* Copyright (c) 2019 Brendan Gregg.
* Licensed under the Apache License, Version 2.0 (the "License").
* This was originally created for the BPF Performance Tools book
* published by Addison Wesley. ISBN-13: 9780136554820
* When copying or porting, include this comment.
*
* 26-Jan-2019 Brendan Gregg Created this.
*/
#include <linux/mm.h>
kprobe:handle_mm_fault
{
$vma = (struct vm_area_struct *)arg0;
$file = $vma->vm_file->f_path.dentry->d_name.name;
@[str($file)] = count();
}
┌──[root@liruilongs.github.io]-[/usr/share/bpftrace/tools]
└─$
我们使用之前的程序测试,跟踪发现无法获取文件名,应该是匿名内存,但是可以统计缺页函数调用次数
┌──[root@liruilongs.github.io]-[~/bpfdemo]
└─$./anon2mmap
PID = 14284
Allocated 0 GB
..............
Total iterations: 2097152
Successfully mapped 8 GB
┌──[root@liruilongs.github.io]-[~/bpfdemo]
└─$
┌──[root@liruilongs.github.io]-[/usr/share/bpftrace/tools]
└─$./ffaults.bt
Attaching 1 probe...
^C
@[]: 4096
对上面的 bpftrace
脚本做简单的修改
┌──[root@liruilongs.github.io]-[/usr/share/bpftrace/tools]
└─$cat ffaults1.bt
#!/usr/bin/bpftrace
kprobe:handle_mm_fault
{
$vma = (struct vm_area_struct *)arg0;
// 关键修复:检查指针有效性需用 != 0 而非隐式判断 [1,3](@ref)
if ($vma->vm_file != 0) {
$file = str($vma->vm_file->f_path.dentry->d_name.name);
} else {
$file = "anonymous"; // 标记匿名内存(堆/栈)
}
@[comm, pid, $file] = count();
}
END {
printf("%-16s %-8s %-40s %s\n", "COMM", "PID", "FILE", "FAULTS");
//print(@);
}
┌──[root@liruilongs.github.io]-[/usr/share/bpftrace/tools]
└─$
再次运行,我们可以获取到匿名内存对应的进程相关的数据
┌──[root@liruilongs.github.io]-[/usr/share/bpftrace/tools]
└─$./ffaults1.bt
Attaching 2 probes...
^CCOMM PID FILE FAULTS
@[anon2mmap, 14655, ld.so.cache]: 1
@[bash, 14655, ld-linux-x86-64.so.2]: 2
@[bash, 13566, libc.so.6]: 2
@[bash, 13566, bash]: 5
@[bash, 14655, libc.so.6]: 5
@[anon2mmap, 14655, anon2mmap]: 6
@[bash, 14655, bash]: 9
@[anon2mmap, 14655, ld-linux-x86-64.so.2]: 9
@[bash, 14655, anonymous]: 10
@[anon2mmap, 14655, libc.so.6]: 27
@[bash, 13566, anonymous]: 76
@[anon2mmap, 14655, anonymous]: 4109
┌──[root@liruilongs.github.io]-[/usr/share/bpftrace/tools]
└─$
© 文中涉及参考链接内容版权归原作者所有,如有侵权请告知 :)
《BPF Performance Tools》
© 2018-至今 liruilonger@gmail.com, 保持署名-非商用-相同方式共享(CC BY-NC-SA 4.0)