我看远山,远山悲悯
写在前面
博文内容涉及缺页异常简单认知
以及通过 BPF 工具 stackcount,trace,faults 等工具对缺页异常进行跟踪统计
理解不足小伙伴帮忙指正 :),生活加油
我看远山,远山悲悯
持续分享技术干货,感兴趣小伙伴可以关注下 ^_^
缺页异常概速 当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
跟踪缺页异常
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 ┌──[root@liruilongs.github.io]-[~/bpfdemo] └─$perf stat -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 都基于这个程序
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 ┌──[root@liruilongs.github.io]-[~/bpfdemo] └─$cat anon2mmap.c 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 是虚拟内存分配的角度去分析内存用量,缺页异常会直接影响系统常驻内存的的增长,也就是物理内存的增长。
跟踪方式主要利用内核静态跟踪点以及软件跟踪点
1 2 3 4 5 ┌──[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]
软件跟踪点,实际上也是基于内核静态跟踪点,对多种缺页异常进行统计
1 2 3 4 5 ┌──[root@liruilongs.github.io]-[~/bpfdemo] └─$ ┌──[root@liruilongs.github.io]-[~/bpfdemo] └─$perf list | grep page-faults page-faults OR faults [Software event]
stackcount stackcount 可能是我们用的最多的一个 BPF 工具,用于对特定函数进行跟踪,可以是静态跟踪点,也可以是动态跟踪点,下面的命令, -p 指定进程ID,后面为内核静态跟踪点的表达式,这里跟踪用户态的缺页异常 page_fault_user
1 2 3 4 5 6 7 8 9 10 11 12 ┌──[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
选项,只输出用户态的调用栈数据,但是这里的用户态调用栈没有解析出函数名
1 2 3 4 5 6 7 8 9 10 11 ┌──[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 trace 也是一个比较常用的 BPF 工具,用于跟踪函数调用时函数签名相关信息,通过 trace 我们可以获取用户态的调用栈,解决上面的问题,运行程序 Demo
1 2 3 4 5 6 7 8 9 10 11 12 13 ┌──[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 跟踪可以看到很多数据,这里我们只展示部分
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 ┌──[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 命令可以实时的观察 物理内存得变化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 ┌──[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 faults 是一个 bpftrace 工具,通过统计软件跟踪点,对缺页异常进行统计,同时会输出缺页异常的调用栈,可以看作是上面两个工具的结合
下面的代码地址
https://github.com/brendangregg/bpf-perf-tools-book/blob/master/originals/Ch07_Memory/faults.bt
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 ┌──[root@liruilongs.github.io]-[/usr/share/bpftrace/tools] └─$cat ./faults.bt /* * 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] └─$
输出跟踪结果,返回用户态函数调用栈,以及缺页函数调用次数
1 2 3 4 5 6 7 8 9 10 11 ┌──[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] └─$
ffaults faults(8) 也是一个 bpftrace 工具,根据文件名来跟踪缺页错误
,这里的文件名,是一些文件映射内存的场景,如果使用匿名内存是无法跟踪的。
代码地址:
https://github.com/brendangregg/bpf-perf-tools-book/blob/master/originals/Ch07_Memory/ffaults.bt
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 ┌──[root@liruilongs.github.io]-[/usr/share/bpftrace/tools] └─$cat ffaults.bt /* * 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. */ 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] └─$
我们使用之前的程序测试,跟踪发现无法获取文件名,应该是匿名内存,但是可以统计缺页函数调用次数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 ┌──[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
脚本做简单的修改
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 ┌──[root@liruilongs.github.io]-[/usr/share/bpftrace/tools] └─$cat ffaults1.bt 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] └─$
再次运行,我们可以获取到匿名内存对应的进程相关的数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 ┌──[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)