Linux 内存调优之 BPF 分析用户态 mmap 大内存分配

我看远山,远山悲悯

写在前面


  • 博文内容涉及利用 BPF 分析用户态 mmap 大内存分配的一些工具分享以及 Demo
  • 主要为 bcc 工具 mmapsnoop以及其变体和 bpftrace 工具 mmapfilesfmapfaults
  • mmap 私有匿名映射,共享映射,文件映射,大页映射等类型内存映射分配的跟踪Demo
  • 理解不足小伙伴帮忙指正 :),生活加油

我看远山,远山悲悯

持续分享技术干货,感兴趣小伙伴可以关注下 ^_^


为什么监控 Linux 用户态大内存分配?

Linux 的内存分配机制整体可分为两大核心部分,brk 和 mmap

brk​ 部分是用于存储用户态程序数据的小内存分配,主要依赖brk调用实现。brk 系统调用通过移动堆顶指针来扩展或收缩堆空间,从而实现小块内存的分配与释放 。它适用于分配内存量较小、频繁申请和释放的场景

例如程序运行过程中临时存储少量数据的变量。在这种机制下,内存管理相对简单直接,但随着程序的复杂和内存需求的增加,可能会出现内存碎片等问题。​

mmap 部分则是基于mmap的大内存分配。当程序需要分配较大块的内存时,mmap 便发挥作用。它通过将文件、设备或匿名内存区域映射到进程的虚拟地址空间,为程序提供连续的、较大规模的内存区域。

brk不同,mmap在内存管理上更为灵活,能有效避免内存碎片问题,且在内存共享、文件映射等场景下具有独特优势。​

通过监控mmap操作,开发者可以追踪大内存的分配与释放情况,及时发现异常的内存占用,从而定位内存泄露的源头,为优化程序性能和稳定性提供有力支持。所以对基于mmap的大内存分配进行监控,在内存泄露分析中有着至关重要的意义。

下面实验使用的Linux环境

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
┌──[root@liruilongs.github.io]-[~/bpfdemo]
└─$hostnamectl
Static hostname: liruilongs.github.io
Icon name: computer-vm
Chassis: vm 🖴
Machine ID: 7deac2815b304f9795f9e0a8b0ae7765
Boot ID: 67932b8e733942d19069be54b76278bd
Virtualization: vmware
Operating System: Rocky Linux 9.4 (Blue Onyx)
CPE OS Name: cpe:/o:rocky:rocky:9::baseos
Kernel: Linux 5.14.0-427.20.1.el9_4.x86_64
Architecture: x86-64
Hardware Vendor: VMware, Inc.
Hardware Model: VMware Virtual Platform
Firmware Version: 6.00
┌──[root@liruilongs.github.io]-[~/bpfdemo]
└─$

如何监控? 这里我们主要使用 mmapsnoopmmapfiles等工具基于 eBPF 技术,对大内存分配进行监控,先来了解一下 mmapsnoop

mmapsnoop

mmapsnoop(8)是一个 BCC 工具,在 《BPF Performance Tools》 一书中首次出现,主要用于跟踪全系统的mmap(2)系统调用并打印出mmap函数调用的详细信息, mmap 是一个非常重要的系统调用,主要用于在进程的虚拟地址空间中创建一个新的内存映射。

在C语言中,mmap的函数原型如下:

1
2
3
#include <sys/mman.h>

void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);

参数解释:

  • addr:指定映射的起始地址,通常设为NULL,让内核自动选择合适的地址。
  • length:映射区域的大小,以字节为单位。
  • prot:内存区域的访问权限,常见的标志有:
    • PROT_READ:可读
    • PROT_WRITE:可写
    • PROT_EXEC:可执行
    • PROT_NONE:不可访问
  • flags:控制映射的类型和行为,常见的标志有:
    • MAP_SHARED:映射区域可被多个进程共享,对映射区域的修改会反映到文件中。
    • MAP_PRIVATE:映射区域是私有的,对映射区域的修改不会反映到文件中。
    • MAP_FIXED:如果addr参数不为NULL,则强制使用指定的地址进行映射。
    • MAP_ANON:创建一个匿名映射,不与任何文件关联。
  • fd:文件描述符,用于指定要映射的文件。如果使用MAP_ANON标志,则该参数可以忽略,通常设为-1
  • offset:文件的偏移量,指定从文件的哪个位置开始映射。

返回值:

  • 成功时,返回映射区域的起始地址。
  • 失败时,返回MAP_FAILED(通常为(void *) -1),并设置errno来指示错误原因。

mmapsnoop 主要功能是跟踪全系统的mmap系统调用,通过eBPF程序跟踪全系统的mmap系统调用,包括调用的进程ID、进程名称、映射的大小、访问权限、映射标志、文件偏移量和文件名等信息。将每个mmap请求的详细信息打印到控制台。

对应的代码地址:

https://github.com/brendangregg/bpf-perf-tools-book/blob/master/originals/Ch07_Memory/mmapsnoop.py

下面为源码的主要代码,主要实现为:

内核态 kprobe 动态跟踪:

  • fd_install 探针 缓存文件描述符(FD)与文件对象的映射关系(fd2file 哈希表)
  • __close_fd 探针 在文件描述符关闭时清理缓存
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
...................
// cache pid+FD -> file for later lookup
// TODO: use a task->files->fdt->fd[] lookup in the mmap tracepoint instead.
// fd_install 探针 缓存文件描述符(FD)与文件对象的映射关系(fd2file 哈希表)
int kprobe__fd_install(struct pt_regs *ctx, int fd, struct file *file)
{
u32 pid = bpf_get_current_pid_tgid() >> 32;
struct fdkey_t key = {.fd = fd, .pid = pid};
fd2file.update(&key, &file);
return 0;
}

// assume this and other events are in PID context
// ​​__close_fd 探针 在文件描述符关闭时清理缓存
int kprobe____close_fd(struct pt_regs *ctx, struct files_struct *files, int fd)
{
u32 pid = bpf_get_current_pid_tgid() >> 32;
struct fdkey_t key = {.fd = fd, .pid = pid};
fd2file.delete(&key);
return 0;
}

内核跟踪点 tracepoint 的静态跟踪: sys_enter_mmap 跟踪点

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
// ​​sys_enter_mmap 跟踪点
TRACEPOINT_PROBE(syscalls, sys_enter_mmap) {
struct task_struct *task;
struct file **fpp, *file;
u32 pid = bpf_get_current_pid_tgid() >> 32;
struct fdkey_t key = {.fd = args->fd, .pid = pid};

fpp = fd2file.lookup(&key);
if (fpp == 0)
return 0;
file = *fpp;

struct mmap_data_t data = {
.len = args->len,
.prot = args->prot,
.flags = args->flags,
.off = args->off,
.pid = pid
};
bpf_get_current_comm(&data.comm, sizeof(data.comm));

struct dentry *de = file->f_path.dentry;
struct qstr d_name = {};
bpf_probe_read(&d_name, sizeof(d_name), (void *)&de->d_name);
bpf_probe_read(&data.path, sizeof(data.path), d_name.name);
mmap_events.perf_submit(args, &data, sizeof(data));

return 0;
}

同时当前实验环境这里的内核态跟踪函数也需要换一下 __close_fd 需要替换为 __close_fd_get_file

1
2
3
4
┌──[root@liruilongs.github.io]-[/usr/share/bcc/tools]
└─$grep __close_fd /proc/kallsyms
ffffffff9a25af20 T __pfx___close_fd_get_file
ffffffff9a25af30 T __close_fd_get_file
1
2
3
// assume this and other events are in PID context
//int kprobe____close_fd(struct pt_regs *ctx, struct files_struct *files, int fd)
int kprobe____close_fd_get_file(struct pt_regs *ctx, struct files_struct *files, int fd)

使用方式相对简单,直接通过命令行即可,示例输出

1
2
3
4
5
6
7
8
9
10
11
12
13
[root@liruilongs.github.io tools]# ./mmapsnoop 
PID COMM PROT MAP OFFS(KB) SIZE(KB) FILE
70696 b'mmapsnoop' RW- S--- 0 260 b'[perf_event]'
70696 b'mmapsnoop' RW- S--- 0 260 b'[perf_event]'
70697 b'crond' R-- -P-- 0 61 b'ld.so.cache'
70697 b'crond' R-E -PF- 0 386 b'libnss_systemd.so.2'
70697 b'crond' RW- -PF- 304 20 b'libnss_systemd.so.2'
70697 b'crond' R-E -PF- 0 640 b'libm.so.6'
70697 b'crond' RW- -PF- 572 8 b'libm.so.6'
...................
70703 b'date' RW- -PF- 1588 24 b'libc.so.6'
70703 b'date' R-- -P-- 0 18591 b'locale-archive'
^C[root@liruilongs.github.io tools]#

部分列说明:

  • PROT:内存区域的访问权限,RWE 对应读写可执行
  • MAP:映射标志,体现内存映射的特性,SPFA 对应 共享私有固定匿名
  • FILE:映射的文件名称
  • SIZE(KB):映射区域的大小,同样以 KB 为单位,明确了此次mmap操作分配的内存空间大小
  • OFFS(KB):文件偏移量

下面我们看一些监控 Demo

mmap私有匿名映射

下面的 Demo 创建一个匿名内存区域,一般这种场景,常用于分配大块的内存(如缓存、数据结构),进程间共享内存(需配合MAP_SHARED,但这里使用MAP_PRIVATE,内存不可共享)mmap仅分配虚拟地址空间,首次访问时才分配物理页(通过缺页中断)

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]
└─$cat anon2mmap.c
#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>

#define GB ((long long) 1024 * 1024 * 1024)

int main() {
long long size = 8 * GB; // 尝试映射8GB
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]
└─$

如果 mmap 没有映射文件或者设备,不使用大页,那么默认使用标准内存页对齐的方式创建虚拟内存

这里通过循环向标准内存页(4KB)写入数据,选择 4KB(4096 字节)作为步长,是为了确保每个内存页只被访问一次。触发实际的物理内存分配(Linux 采用延迟分配策略,mmap仅分配虚拟地址,首次访问时触发缺页异常才分配物理页)

编译之后运行上面的程序,可以看到在物理内存在分配第 4 GB 内存时 触发了 OOM killer

1
2
3
4
5
6
7
8
┌──[root@liruilongs.github.io]-[~/bpfdemo]
└─$./anon2mmap
Allocated 0 GB
Allocated 1 GB
Allocated 2 GB
Allocated 3 GB
Allocated 4 GB
Killed

通过内核日志我们可以验证这一点, anon2mmap(PID=13365)尝试分配大量内存,导致系统内存耗尽。同时展示了,内存分配标志,OOM 评分等,以及触发 OOM killer 的函数调用栈

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
┌──[root@liruilongs.github.io]-[~/bpfdemo]
└─$dmesg --follow -T
[Sat Jun 7 16:54:14 2025] anon2mmap invoked oom-killer: gfp_mask=0x140dca(GFP_HIGHUSER_MOVABLE|__GFP_COMP|__GFP_ZERO), order=0, oom_score_adj=0
[Sat Jun 7 16:54:14 2025] CPU: 1 PID: 13365 Comm: anon2mmap Kdump: loaded Tainted: G OE ------- --- 5.14.0-427.20.1.el9_4.x86_64 #1
[Sat Jun 7 16:54:14 2025] Hardware name: VMware, Inc. VMware Virtual Platform/440BX Desktop Reference Platform, BIOS 6.00 11/12/2020
[Sat Jun 7 16:54:14 2025] Call Trace:
[Sat Jun 7 16:54:14 2025] <TASK>
[Sat Jun 7 16:54:14 2025] dump_stack_lvl+0x34/0x48
[Sat Jun 7 16:54:14 2025] dump_header+0x4a/0x201
[Sat Jun 7 16:54:14 2025] oom_kill_process.cold+0xb/0x10
[Sat Jun 7 16:54:14 2025] out_of_memory+0xed/0x2e0
[Sat Jun 7 16:54:14 2025] __alloc_pages_slowpath.constprop.0+0x6e8/0x960
[Sat Jun 7 16:54:14 2025] __alloc_pages+0x21d/0x250
[Sat Jun 7 16:54:14 2025] __folio_alloc+0x17/0x50
[Sat Jun 7 16:54:14 2025] ? policy_node+0x4f/0x70
[Sat Jun 7 16:54:14 2025] vma_alloc_folio+0xa3/0x390
[Sat Jun 7 16:54:14 2025] do_anonymous_page+0x63/0x520
[Sat Jun 7 16:54:14 2025] __handle_mm_fault+0x32b/0x670
[Sat Jun 7 16:54:14 2025] ? nohz_balancer_kick+0x31/0x250
[Sat Jun 7 16:54:14 2025] handle_mm_fault+0xcd/0x290
[Sat Jun 7 16:54:14 2025] do_user_addr_fault+0x1b4/0x6a0
[Sat Jun 7 16:54:14 2025] ? sched_clock_cpu+0x9/0xc0
[Sat Jun 7 16:54:14 2025] exc_page_fault+0x62/0x150
[Sat Jun 7 16:54:14 2025] asm_exc_page_fault+0x22/0x30
。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。
[Sat Jun 7 16:54:14 2025] Tasks state (memory values in pages):
[Sat Jun 7 16:54:14 2025] [ pid ] uid tgid total_vm rss pgtables_bytes swapents oom_score_adj name
[Sat Jun 7 16:54:14 2025] [ 701] 0 701 12852 192 114688 288 -250 systemd-journal
[Sat Jun 7 16:54:14 2025] [ 715] 0 715 8274 32 98304 512 -1000 systemd-udevd
[Sat Jun 7 16:54:14 2025] [ 887] 0 887 4539 80 57344 672 -1000 auditd
。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。
[Sat Jun 7 16:54:14 2025] [ 13058] 48 13058 374632 38 483328 16256 0 /usr/sbin/httpd
[Sat Jun 7 16:54:14 2025] [ 13364] 0 13364 63300 2813 495616 12512 0 mmapsnoop
[Sat Jun 7 16:54:14 2025] [ 13365] 0 13365 2097810 808956 9932800 424160 0 anon2mmap
[Sat Jun 7 16:54:14 2025] oom-kill:constraint=CONSTRAINT_NONE,nodemask=(null),cpuset=/,mems_allowed=0,global_oom,task_memcg=/user.slice/user-0.slice/session-9.scope,task=anon2mmap,pid=13365,uid=0
[Sat Jun 7 16:54:14 2025] Out of memory: Killed process 13365 (anon2mmap) total-vm:8391240kB, anon-rss:3235696kB, file-rss:128kB, shmem-rss:0kB, UID:0 pgtables:9700kB oom_score_adj:0

虚拟内存总量 total-vm: 8391240kB(约 8GB),对应代码中的 8GB 内存申请

1
2
3
long long size = 8 * GB;  // 尝试映射8GB
void *ptr = mmap(NULL, size, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);

匿名物理内存 anon-rss: 3235696kB(约 3.2GB),对应触发OOM killer 的内存消耗

这里我们用 mmapsnoop 去跟踪 mmap 的匿名内存的分配

1
2
3
4
5
6
7
8
9
10
┌──[root@liruilongs.github.io]-[/usr/share/bcc/tools]
└─$./mmapsnoop -T
TIME PID COMM PROT MAP OFFS(KB) SIZE(KB) FILE
00:42:09 13365 b'anon2mmap' R-- -P-- 0 28 b'ld.so.cache'
00:42:09 13365 b'anon2mmap' R-- -P-- 0 2083 b'libc.so.6'
00:42:09 13365 b'anon2mmap' R-E -PF- 160 1492 b'libc.so.6'
00:42:09 13365 b'anon2mmap' R-- -PF- 1652 352 b'libc.so.6'
00:42:09 13365 b'anon2mmap' RW- -PF- 2004 24 b'libc.so.6'
^C┌──[root@liruilongs.github.io]-[/usr/share/bcc/tools]
└─$

这里脚本有一些问题,可以看到只能跟踪到一些共享库的分配,却看不到实际的匿名内存的分配,这是什么原因?

mmap 映射的匿名内存直接使用的虚拟内存,没有映射文件,所以之前的脚本没有记录这部分, 可以看到代码在 fd2file中没有关联的数据时,直接返回 0,即匿名映射的内存会直接忽略

1
2
3
4
5
6
7
8
9
TRACEPOINT_PROBE(syscalls, sys_enter_mmap) {
......................
fpp = fd2file.lookup(&key);
if (fpp == 0)
return 0;
file = *fpp;
.....................
return 0;
}

这里我们需要修改一下原来的脚本,下面为修改之后的

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
TRACEPOINT_PROBE(syscalls, sys_enter_mmap) {
u32 pid = bpf_get_current_pid_tgid() >> 32;
struct fdkey_t key = {.fd = args->fd, .pid = pid};
const char *anon_name = "[anon]";
// 调试:打印fd值
bpf_trace_printk("DEBUG: mmap fd=%d, pid=%d ---", args->fd, pid);

// 处理匿名内存映射(fd为-1)
long fd = (long)args->fd;
if ( args->fd == 0xFFFFFFFF ) {
struct mmap_data_t data = {
.len = args->len,
.prot = args->prot,
.flags = args->flags,
.off = args->off,
.pid = pid
};
bpf_trace_printk("DEBUG: mmap len=%d, prot=%d ---", args->len, args->prot);
bpf_get_current_comm(&data.comm, sizeof(data.comm));
bpf_probe_read(&data.path, sizeof(data.path), (void *)anon_name);
mmap_events.perf_submit(args, &data, sizeof(data));
return 0;
}

// 处理文件映射
struct file **fpp = fd2file.lookup(&key);
if (fpp == 0)
return 0;
struct file *file = *fpp;

struct mmap_data_t data = {
.len = args->len,
.prot = args->prot,
.flags = args->flags,
.off = args->off,
.pid = pid
};
bpf_get_current_comm(&data.comm, sizeof(data.comm));

struct dentry *de = file->f_path.dentry;
struct qstr d_name = {};
bpf_probe_read(&d_name, sizeof(d_name), (void *)&de->d_name);
bpf_probe_read(&data.path, sizeof(data.path), d_name.name);
mmap_events.perf_submit(args, &data, sizeof(data));

return 0;
}

分配匿名内存的时候 args->fd 的值为 -1, 需要注意 这里的 if ( args->fd == 0xFFFFFFFF ) ,需要用 32 位的 -1 表示

bpf_trace_printk 用于调试,输出会写入 /sys/kernel/debug/tracing/trace_pipe

下面是修改之后的输出

1
2
3
4
5
6
7
8
9
10
11
12
13
┌──[root@liruilongs.github.io]-[/usr/share/bcc/tools]
└─$./ano_mmapsoonp
PID COMM PROT MAP OFFS(KB) SIZE(KB) FILE
49103 ano_mmapsoonp RW- S--- 0 260 [perf_event]
49104 anon2mmap RW- -P-A 0 8
49104 anon2mmap R-- -P-- 0 28 ld.so.cache
49104 anon2mmap R-- -P-- 0 2083 libc.so.6
49104 anon2mmap R-E -PF- 160 1492 libc.so.6
49104 anon2mmap R-- -PF- 1652 352 libc.so.6
49104 anon2mmap RW- -PF- 2004 24 libc.so.6
49104 anon2mmap RW- -PFA 0 51
49104 anon2mmap RW- -P-A 0 12
49104 anon2mmap RW- -P-A 0 8388608

可以看到最后一条日志,对应分配虚拟内存 8388608KB/1024/1024 = 8G

1
`49104  anon2mmap      RW-  -P-A  0        8388608` 

下面为调试日志的输出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
^C┌──[root@liruilongs.github.io]-[/usr/share/bcc/tools]
└─$ cat /sys/kernel/debug/tracing/trace_pipe
ano_mmapsoonp-49103 [000] ....2.1 591493.415773: bpf_trace_printk: DEBUG: mmap fd=-1, pid=49103 ---
ano_mmapsoonp-49103 [000] ....2.1 591493.415802: bpf_trace_printk: DEBUG: mmap len=4096, prot=7 ---
ano_mmapsoonp-49103 [000] ....2.1 591493.415963: bpf_trace_printk: DEBUG: mmap fd=12, pid=49103 ---
ano_mmapsoonp-49103 [000] ....2.1 591493.416216: bpf_trace_printk: DEBUG: mmap fd=13, pid=49103 ---
anon2mmap-49104 [001] ....2.1 591496.676898: bpf_trace_printk: DEBUG: mmap fd=-1, pid=49104 ---
anon2mmap-49104 [001] ....2.1 591496.676906: bpf_trace_printk: DEBUG: mmap len=8192, prot=3 ---
anon2mmap-49104 [001] ....2.1 591496.677042: bpf_trace_printk: DEBUG: mmap fd=3, pid=49104 ---
anon2mmap-49104 [001] ....2.1 591496.677532: bpf_trace_printk: DEBUG: mmap fd=3, pid=49104 ---
anon2mmap-49104 [001] ....2.1 591496.677677: bpf_trace_printk: DEBUG: mmap fd=3, pid=49104 ---
anon2mmap-49104 [001] ....2.1 591496.677715: bpf_trace_printk: DEBUG: mmap fd=3, pid=49104 ---
anon2mmap-49104 [001] ....2.1 591496.678286: bpf_trace_printk: DEBUG: mmap fd=3, pid=49104 ---
anon2mmap-49104 [001] ....2.1 591496.678361: bpf_trace_printk: DEBUG: mmap fd=-1, pid=49104 ---
anon2mmap-49104 [001] ....2.1 591496.678368: bpf_trace_printk: DEBUG: mmap len=53168, prot=3 ---
anon2mmap-49104 [001] ....2.1 591496.679738: bpf_trace_printk: DEBUG: mmap fd=-1, pid=49104 ---
anon2mmap-49104 [001] ....2.1 591496.679747: bpf_trace_printk: DEBUG: mmap len=12288, prot=3 ---
anon2mmap-49104 [001] ....2.1 591496.680823: bpf_trace_printk: DEBUG: mmap fd=-1, pid=49104 ---
anon2mmap-49104 [001] ....2.1 591496.680849: bpf_trace_printk: DEBUG: mmap len=0, prot=3 ---
systemd-journal-701 [000] ....2.1 591502.486094: bpf_trace_printk: DEBUG: mmap fd=22, pid=701 ---
in:imjournal-1173 [001] ....2.1 591502.744724: bpf_trace_printk: DEBUG: mmap fd=8, pid=1144 ---
^C
┌──[root@liruilongs.github.io]-[/usr/share/bcc/tools]
└─$

mmap共享内存

多个进程可以通过 MAP_SHARED 标志共享同一个映射区域,实现进程间的通信。下面是一个对应的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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <sys/wait.h>

#define SHM_NAME "/my_shared_memory" // 共享内存名称
#define SHM_SIZE 4096 // 共享内存大小(字节)

int main() {
// 创建或打开共享内存对象
int fd = shm_open(SHM_NAME, O_CREAT | O_RDWR, 0666);
if (fd == -1) {
perror("shm_open");
exit(EXIT_FAILURE);
}

// 设置共享内存大小
if (ftruncate(fd, SHM_SIZE) == -1) {
perror("ftruncate");
close(fd);
shm_unlink(SHM_NAME); // 删除共享内存对象
exit(EXIT_FAILURE);
}

// 映射共享内存
char *shared_memory = mmap(NULL, SHM_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (shared_memory == MAP_FAILED) {
perror("mmap");
close(fd);
shm_unlink(SHM_NAME);
exit(EXIT_FAILURE);
}

// 关闭文件描述符(映射已建立,不再需要)
close(fd);

// 创建子进程
pid_t pid = fork();
if (pid < 0) {
perror("fork");
munmap(shared_memory, SHM_SIZE);
shm_unlink(SHM_NAME);
exit(EXIT_FAILURE);
}

if (pid == 0) { // 子进程
// 向共享内存写入数据
sprintf(shared_memory, "Hello from child process! PID=%d", getpid());

// 解除映射但不删除共享内存
munmap(shared_memory, SHM_SIZE);
exit(EXIT_SUCCESS);
} else { // 父进程
// 等待子进程完成
wait(NULL);

// 从共享内存读取数据
printf("Parent read: %s\n", shared_memory);

// 解除映射并删除共享内存对象
munmap(shared_memory, SHM_SIZE);
shm_unlink(SHM_NAME); // 确保删除共享内存
}

return 0;
}

执行程序

1
2
3
┌──[root@liruilongs.github.io]-[~/bpfdemo]
└─$./shar2mmap
Parent read: Hello from child process! PID=13375

下面为通过 mmapsnoop 来监控的日志,这里不涉及匿名内存跟踪,fd 的值不为 -1,所以可以直接只用原脚本跟踪

1
2
3
4
5
6
7
8
9
10
11
┌──[root@liruilongs.github.io]-[/usr/share/bcc/tools]
└─$./mmapsnoop -T
TIME PID COMM PROT MAP OFFS(KB) SIZE(KB) FILE
00:46:36 13374 b'shar2mmap' R-- -P-- 0 28 b'ld.so.cache'
00:46:36 13374 b'shar2mmap' R-- -P-- 0 2083 b'libc.so.6'
00:46:36 13374 b'shar2mmap' R-E -PF- 160 1492 b'libc.so.6'
00:46:36 13374 b'shar2mmap' R-- -PF- 1652 352 b'libc.so.6'
00:46:36 13374 b'shar2mmap' RW- -PF- 2004 24 b'libc.so.6'
00:46:36 13374 b'shar2mmap' RW- S--- 0 4 b'my_shared_memory'
^C┌──[root@liruilongs.github.io]-[/usr/share/bcc/tools]
└─$

可以看到最后一行,即为跟踪的共享内存分配,S 标志为共享内存,my_shared_memory对应共享内存名字, 4 为分配的 4KB

1
00:46:36 13374  b'shar2mmap'   RW-  S---  0        4        b'my_shared_memory'

文件映射

将文件的内容映射到进程的内存中,可以直接通过内存操作来访问文件,避免了频繁的readwrite系统调用,提高了文件访问的效率。同时有些进程间交互也使用这种方式

下面是一个 mmap 文件映射的内存的 demo,创建文件、mmap 映射文件到内存、在内存中读写和修改文件内容、同步修改到文件、解除映射以及关闭文件的完整流程

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
54
55
56
57
58
59
60
61
62
63
64
65
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <unistd.h>

#define FILE_NAME "testfile.txt"
#define FILE_SIZE 1024

int main() {
int fd;
char *map;
struct stat sb;
// 打开文件
fd = open(FILE_NAME, O_RDWR | O_CREAT, 0644);
if (fd == -1) {
perror("open");
return EXIT_FAILURE;
}
// 调整文件大小
if (ftruncate(fd, FILE_SIZE) == -1) {
perror("ftruncate");
close(fd);
return EXIT_FAILURE;
}
// 获取文件状态信息
if (fstat(fd, &sb) == -1) {
perror("fstat");
close(fd);
return EXIT_FAILURE;
}
// 进行内存映射
map = mmap(NULL, sb.st_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (map == MAP_FAILED) {
perror("mmap");
close(fd);
return EXIT_FAILURE;
}
// 初始化文件内容
for (int i = 0; i < FILE_SIZE; i++) {
map[i] = 'A' + (i % 26);
}
// 读取文件内容
printf("Original file content:\n");
for (int i = 0; i < FILE_SIZE; i++) {
putchar(map[i]);
}
printf("\n");
// 修改文件内容
for (int i = 0; i < FILE_SIZE; i++) {
map[i] = 'a' + (i % 26);
}
// 同步映射内容到文件
if (msync(map, sb.st_size, MS_SYNC) == -1) {
perror("msync");
}
// 解除内存映射
if (munmap(map, sb.st_size) == -1) {
perror("munmap");
}
// 关闭文件
close(fd);
return EXIT_SUCCESS;
}

编译之后运行上面的Demo

1
2
3
4
5
6
7
8
9
10
11
┌──[root@liruilongs.github.io]-[~/bpfdemo]
└─$vim file2mmap.c
┌──[root@liruilongs.github.io]-[~/bpfdemo]
└─$gcc -g file2mmap.c -o file2mmap
┌──[root@liruilongs.github.io]-[~/bpfdemo]
└─$./file2mmap
当前进程PID: 10581
Original file content:
ABCDE。。。。。。。。。。。。。。。。。。。。。。FGHIJ
┌──[root@liruilongs.github.io]-[~/bpfdemo]
└─$
1
2
3
┌──[root@liruilongs.github.io]-[~/bpfdemo]
└─$cat testfile.txt
abcdefghi。。。。。。。。。。。。。。。。。。。。。efghij

运行 file2mmap,通过 mmapsnoop 命令监控 mmap 的调用

1
2
3
4
5
6
7
8
9
10
11
12
┌──[root@liruilongs.github.io]-[/usr/share/bcc/tools]
└─$./mmapsnoop -T
TIME PID COMM PROT MAP OFFS(KB) SIZE(KB) FILE
00:18:18 10580 b'mmapsnoop' RW- S--- 0 260 b'[perf_event]'
00:18:20 10581 b'file2mmap' R-- -P-- 0 28 b'ld.so.cache'
00:18:20 10581 b'file2mmap' R-- -P-- 0 2083 b'libc.so.6'
00:18:20 10581 b'file2mmap' R-E -PF- 160 1492 b'libc.so.6'
00:18:20 10581 b'file2mmap' R-- -PF- 1652 352 b'libc.so.6'
00:18:20 10581 b'file2mmap' RW- -PF- 2004 24 b'libc.so.6'
00:18:20 10581 b'file2mmap' RW- S--- 0 1 b'testfile.txt'
^C┌──[root@liruilongs.github.io]-[/usr/share/bcc/tools]
└─$

最后一行为跟踪的文件映射内存分配,可以看到大下为我们上面定义的 1kb

1
00:18:20 10581  b'file2mmap'   RW-  S---  0        1        b'testfile.txt'

私有写时复制映射

使用 MAP_PRIVATE 标志创建的映射属于私有写时复制映射。当进程对映射区域进行写操作时,内核会为该进程复制一份物理内存页,这样进程的修改不会影响到其他映射了同一文件或区域的进程。这种映射方式适合于多个进程需要读取相同数据,但可能会各自进行修改的场景。下面是一个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
36
37
38
39
40
41
42
43
44
45
46
47
48
#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <unistd.h>

#define MAP_SIZE 4096

int main() {
int fd;
char *map;
// 创建一个临时文件
fd = open("tempfile", O_RDWR | O_CREAT | O_TRUNC, 0644);
if (fd == -1) {
perror("open");
return EXIT_FAILURE;
}
// 调整文件大小
if (ftruncate(fd, MAP_SIZE) == -1) {
perror("ftruncate");
close(fd);
return EXIT_FAILURE;
}
// 进行内存映射
map = mmap(NULL, MAP_SIZE, PROT_READ | PROT_WRITE, MAP_PRIVATE, fd, 0);
if (map == MAP_FAILED) {
perror("mmap");
close(fd);
return EXIT_FAILURE;
}
// 初始化映射区域
for (int i = 0; i < MAP_SIZE; i++) {
map[i] = 'A';
}
// 读取映射区域的数据
printf("Original value at map[0]: %c\n", map[0]);
// 修改映射区域的数据
map[0] = 'B';
// 再次读取映射区域的数据
printf("Modified value at map[0]: %c\n", map[0]);
// 解除内存映射
if (munmap(map, MAP_SIZE) == -1) {
perror("munmap");
}
// 关闭文件
close(fd);
return EXIT_SUCCESS;
}
1
2
3
4
5
6
7
8
9
10
┌──[root@liruilongs.github.io]-[~/bpfdemo]
└─$vim cow2mmap.c
┌──[root@liruilongs.github.io]-[~/bpfdemo]
└─$gcc -g cow2mmap.c -o cow2mmap
┌──[root@liruilongs.github.io]-[~/bpfdemo]
└─$./cow2mmap
Original value at map[0]: A
Modified value at map[0]: B
┌──[root@liruilongs.github.io]-[~/bpfdemo]
└─$
1
2
3
4
5
6
7
8
9
10
11
┌──[root@liruilongs.github.io]-[/usr/share/bcc/tools]
└─$./mmapsnoop -T
TIME PID COMM PROT MAP OFFS(KB) SIZE(KB) FILE
01:45:48 13519 b'cow2mmap' R-- -P-- 0 28 b'ld.so.cache'
01:45:48 13519 b'cow2mmap' R-- -P-- 0 2083 b'libc.so.6'
01:45:48 13519 b'cow2mmap' R-E -PF- 160 1492 b'libc.so.6'
01:45:48 13519 b'cow2mmap' R-- -PF- 1652 352 b'libc.so.6'
01:45:48 13519 b'cow2mmap' RW- -PF- 2004 24 b'libc.so.6'
01:45:48 13519 b'cow2mmap' RW- -P-- 0 4 b'tempfile'
^C┌──[root@liruilongs.github.io]-[/usr/share/bcc/tools]
└─$

设备映射

设备映射允许进程直接访问硬件设备的内存空间。通过将设备的物理地址映射到进程的虚拟地址空间,进程可以像访问普通内存一样访问设备的寄存器和数据缓冲区,从而实现对设备的直接控制和数据交互。例如,显卡、网络接口卡等设备可以通过设备映射来提高数据传输效率。

其他语言

前面的 Demo都是 C 语言的跟踪,我们来看看其他语言的

Java

下面是一个 Java 的内存文件映射的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
36
37
38
39
40
41
42
43
44
┌──[root@liruilongs.github.io]-[~/bpfdemo]
└─$vim javaMap.java
┌──[root@liruilongs.github.io]-[~/bpfdemo]
└─$java javaMap.java
Hello from Java mmap!
┌──[root@liruilongs.github.io]-[~/bpfdemo]
└─$java javaMap.java
Hello from Java mmap!
┌──[root@liruilongs.github.io]-[~/bpfdemo]
└─$cat javaMap.java
// JavaMmap.java
import java.io.*;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;

public class JavaMmap {
public static void main(String[] args) {
try {
// 创建一个临时文件
File file = new File("tempfile");
RandomAccessFile raf = new RandomAccessFile(file, "rw");
// 获取文件通道
FileChannel channel = raf.getChannel();
// 创建内存映射
MappedByteBuffer buffer = channel.map(FileChannel.MapMode.READ_WRITE, 0, 1024);
// 写入数据
String message = "Hello from Java mmap!";
buffer.put(message.getBytes());
// 读取数据
buffer.position(0);
byte[] data = new byte[message.length()];
buffer.get(data);
System.out.println(new String(data));
// 关闭资源
channel.close();
raf.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}

┌──[root@liruilongs.github.io]-[~/bpfdemo]
└─$

通过 mmapsnoop ,我们可以在下面的日志的最后一行看的跟踪数据,我们申请了 1KB

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
┌──[root@liruilongs.github.io]-[/usr/share/bcc/tools]
└─$./mmapsnoop
PID COMM PROT MAP OFFS(KB) SIZE(KB) FILE
41156 b'mmapsnoop' RW- S--- 0 260 b'[perf_event]'
41157 b'java' R-- -P-- 0 108 b'libjli.so'
。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。
41157 b'java' RW- -PF- 100 8 b'libjli.so'
41157 b'java' R-- -P-- 0 28 b'ld.so.cache'
41157 b'java' R-- -P-- 0 2083 b'libc.so.6'
41157 b'java' R-E -PF- 160 1492 b'libc.so.6'
。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。
41157 b'java' R-- -PF- 16440 2584 b'libjvm.so'
41157 b'java' RW- -PF- 19024 952 b'libjvm.so'
41157 b'java' R-- -P-- 0 28 b'ld.so.cache'
41157 b'java' R-- -P-- 0 872 b'libm.so.6'
。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。
41157 b'java' R-E -PF- 16 80 b'libjimage.so'
。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。.。。。。。。。。。。。。
41157 b'java' RW- -PF- 152 8 b'libjava.so'
41157 b'java' RW- -PF- 4 4432 b'classes.jsa'
。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。
41157 b'java' R-- -PF- 280 548 b'libjsvml.so'
。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。
41157 b'java' R-- -P-- 0 264 b'libnspr4.so'
41157 b'java' R-E -PF- 48 148 b'libnspr4.so'
41157 b'java' R-- -PF- 196 44 b'libnspr4.so'
41157 b'java' RW- -PF- 240 12 b'libnspr4.so'
41157 b'java' RW- S--- 0 1 b'tempfile'
^C┌──[root@liruilongs.github.io]-[/usr/share/bcc/tools]
└─$

python

下面是一个 python 的 Demo,有一个问题,跟踪的内存映射长度为 0,这里有没有小伙伴知道是什么原因,欢迎留言讨论 ^_^

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
┌──[root@liruilongs.github.io]-[~/bpfdemo]
└─$python py_mmap.py
b'Hi,mmapmmap!'
┌──[root@liruilongs.github.io]-[~/bpfdemo]
└─$cat py_mmap.py
# python_mmap.py
import mmap

# 创建一个临时文件
with open("tempfile", "w+b") as f:
f.write(b"Hello, mmap!")
f.flush()

# 创建 mmap 对象(映射整个文件)
mm = mmap.mmap(f.fileno(), 0, access=mmap.ACCESS_WRITE)

# 修改内容
mm[0:7] = b"Hi,mmap"

# 读取内容
print(mm.readline())

# 关闭映射
mm.close()
┌──[root@liruilongs.github.io]-[~/bpfdemo]
└─$

我觉得可能是文件写入的字符串小于 1kb,所以展示为 0

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
┌──[root@liruilongs.github.io]-[/usr/share/bcc/tools]
└─$./mmapsnoop
PID COMM PROT MAP OFFS(KB) SIZE(KB) FILE
41205 b'python' R-- -P-- 0 28 b'ld.so.cache'
41205 b'python' R-- -P-- 0 3467 b'libpython3.9.so.1.0'
41205 b'python' R-E -PF- 360 1748 b'libpython3.9.so.1.0'
...............................................................
41205 b'python' RW- -PF- 2004 24 b'libc.so.6'
41205 b'python' R-- -P-- 0 872 b'libm.so.6'
41205 b'python' R-E -PF- 52 448 b'libm.so.6'
....................................
41205 b'python' RW- -PF- 24 8 b'mmap.cpython-39-x86_64-linux-gnu'
41205 b'python' RW- S--- 0 0 b'tempfile'
^C┌──[root@liruilongs.github.io]-[/usr/share/bcc/tools]
└─$

在前面的博文和小伙伴分享过 mmap 可以使用 大页映射,提高内存访问性能,默认情况下 ,mmap 是使用标准内存页 4kb 来映射内存的,这里我们也看几个 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
┌──[root@liruilongs.github.io]-[~/bpfdemo/mmapdev]
└─$grep -i huge /proc/meminfo | grep -i size
Hugepagesize: 2048 kB
┌──[root@liruilongs.github.io]-[~/bpfdemo/mmapdev]
└─$sysctl -a | grep huge
vm.hugetlb_optimize_vmemmap = 0
vm.hugetlb_shm_group = 0
vm.nr_hugepages = 0
vm.nr_hugepages_mempolicy = 0
vm.nr_overcommit_hugepages = 0
┌──[root@liruilongs.github.io]-[~/bpfdemo/mmapdev]
└─$free -h
total used free shared buff/cache available
Mem: 15Gi 12Gi 2.8Gi 3.0Mi 419Mi 3.0Gi
Swap: 2.0Gi 337Mi 1.7Gi
┌──[root@liruilongs.github.io]-[~/bpfdemo/mmapdev]
└─$sysctl -w vm.nr_hugepages=50
vm.nr_hugepages = 50
┌──[root@liruilongs.github.io]-[~/bpfdemo/mmapdev]
└─$free -h
total used free shared buff/cache available
Mem: 15Gi 12Gi 2.7Gi 3.0Mi 419Mi 2.9Gi
Swap: 2.0Gi 337Mi 1.7Gi
┌──[root@liruilongs.github.io]-[~/bpfdemo/mmapdev]
└─$sysctl -a | grep huge
vm.hugetlb_optimize_vmemmap = 0
vm.hugetlb_shm_group = 0
vm.nr_hugepages = 50
vm.nr_hugepages_mempolicy = 50
vm.nr_overcommit_hugepages = 0
┌──[root@liruilongs.github.io]-[~/bpfdemo/mmapdev]
└─$mkdir -p /dev/hugepages
┌──[root@liruilongs.github.io]-[~/bpfdemo/mmapdev]
└─$mount -t hugetlbfs -o pagesize=2M none /dev/hugepages

上面的命令主要完成了大页内存(Huge Pages)的配置与挂载,具体操作如下:

首先通过grep命令确认系统默认大页大小为2048KB(2MB);接着用sysctl查看大页相关参数,此时vm.nr_hugepages为0,意味着未分配大页内存;随后执行sysctl -w vm.nr_hugepages=50,为系统分配50个大页,每个大小2MB,共100MB内存,此时free -h显示可用内存减少约100MB;

接着通过sysctl -a验证大页分配成功,nr_hugepagesnr_hugepages_mempolicy均变为50;最后创建/dev/hugepages目录,并使用mount命令将大页文件系统(hugetlbfs)挂载到该目录,指定页大小为2MB,以便后续通过该目录使用大页内存。

文件大页映射

下面是一个 文件内存映射使用大页的 Demo,可以看到分配2个大页,一个大页 2MB,两个 4MB

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
54
55
56
57
┌──[root@liruilongs.github.io]-[~/bpfdemo]
└─$cat hug2mmap.c
#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>

#define MAP_SIZE (4 * 1024 * 1024) // 4MB,2个2MB大页

int main() {
int fd;
char *map;
char path[] = "/dev/hugepages/test_huge";

// 确保挂载了 hugetlbfs
//system("mount | grep hugetlbfs || mount -t hugetlbfs -o pagesize=2M none /dev/hugepages");

// 打开 hugetlbfs 文件
fd = open(path, O_CREAT | O_RDWR, 0666);
if (fd == -1) {
perror("open");
return EXIT_FAILURE;
}

// 调整文件大小
if (ftruncate(fd, MAP_SIZE) == -1) {
perror("ftruncate");
close(fd);
return EXIT_FAILURE;
}

// 映射内存(无需 MAP_HUGETLB 标志)
map = mmap(NULL, MAP_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (map == MAP_FAILED) {
perror("mmap");
close(fd);
unlink(path);
return EXIT_FAILURE;
}

// 填充数据(触发大页分配)
memset(map, 'A', MAP_SIZE);

// 验证大页使用
//printf("Huge pages usage:\n");
//system("grep -i huge /proc/meminfo");
//system("cat /sys/kernel/mm/hugepages/hugepages-2048kB/nr_hugepages");

// 清理
munmap(map, MAP_SIZE);
close(fd);
unlink(path);

return 0;
}

执行 Demo后观察 HugePages 相关的指标数据,确认是否使用成功

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
┌──[root@liruilongs.github.io]-[~/bpfdemo]
└─$vim hug2mmap.c
┌──[root@liruilongs.github.io]-[~/bpfdemo]
└─$gcc -g hug2mmap.c -o hug2mmap
┌──[root@liruilongs.github.io]-[~/bpfdemo]
└─$./hug2mmap
┌──[root@liruilongs.github.io]-[~/bpfdemo]
└─$./hug2mmap
hugetlbfs on /dev/hugepages type hugetlbfs (rw,relatime,pagesize=2M)
Huge pages usage:
AnonHugePages: 20480 kB
ShmemHugePages: 0 kB
FileHugePages: 0 kB
HugePages_Total: 50
HugePages_Free: 48
HugePages_Rsvd: 0
HugePages_Surp: 0
Hugepagesize: 2048 kB
Hugetlb: 102400 kB
50

分配了 50 个,使用 2个,空闲大页数为 48 个

1
2
HugePages_Total:      50
HugePages_Free: 48

在 mmapsnoop 的最后一行输出中显示大小为 4096 KB,这与代码中设置的映射 两个大页一致。

1
2
3
4
5
6
7
8
9
10
11
12
┌──[root@liruilongs.github.io]-[/usr/share/bcc/tools]
└─$./mmapsnoop -T
TIME PID COMM PROT MAP OFFS(KB) SIZE(KB) FILE
09:33:46 15674 b'mmapsnoop' RW- S--- 0 260 b'[perf_event]'
09:33:46 15675 b'hug2mmap' R-- -P-- 0 28 b'ld.so.cache'
09:33:46 15675 b'hug2mmap' R-- -P-- 0 2083 b'libc.so.6'
09:33:46 15675 b'hug2mmap' R-E -PF- 160 1492 b'libc.so.6'
09:33:46 15675 b'hug2mmap' R-- -PF- 1652 352 b'libc.so.6'
09:33:46 15675 b'hug2mmap' RW- -PF- 2004 24 b'libc.so.6'
09:33:46 15675 b'hug2mmap' RW- S--- 0 4096 b'test_huge'
^C┌──[root@liruilongs.github.io]-[/usr/share/bcc/tools]
└─$

mmapfiles(8)

mmapfles(8) 是一个 bpftrace 工具,同样用于跟踪 mmap(2) 调用,主要统计映射入内存地址范围的文件频率信息, 可以通过这个命令直观的看到频繁分配的进程数据

对应的代码地址:

https://github.com/brendangregg/bpf-perf-tools-book/blob/master/originals/Ch08_FileSystems/mmapfiles.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
28
29
30
┌──[root@liruilongs.github.io]-[/usr/share/bpftrace/tools]
└─$cat mmapfiles.bt
#!/usr/bin/bpftrace
/*
* mmapfiles - Count mmap(2) files.
*
* See BPF Performance Tools, Chapter 8, 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:do_mmap
{
$file = (struct file *)arg0;
$name = $file->f_path.dentry;
$dir1 = $name->d_parent;
$dir2 = $dir1->d_parent;
@[str($dir2->d_name.name), str($dir1->d_name.name),
str($name->d_name.name)] = count();
}
┌──[root@liruilongs.github.io]-[/usr/share/bpftrace/tools]
└─$

这里我们执行一些前面写的 Demo,看下输出

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]
└─$./mmapfiles.bt
Attaching 1 probe...
^C

@[locale, C.utf8, LC_PAPER]: 1
@[/, /, my_shared_memory]: 1
。。。。。。。。。。。
@[root, bpfdemo, testfile.txt]: 1
。。。。。。。。。。。。。。。。。。。。
@[locale, C.utf8, LC_ADDRESS]: 1
@[anon_hugepage, anon_hugepage, anon_hugepage]: 2
@[root, bpfdemo, file2mmap]: 4
@[root, bpfdemo, anonhag2mmap]: 4
@[usr, lib64, libpcre2-8.so.0.11.0]: 4
@[root, bpfdemo, shar2mmap]: 4
@[usr, lib64, libselinux.so.1]: 4
@[usr, lib64, libcap.so.2.48]: 4
@[usr, bin, ls]: 4
@[root, bpfdemo, hug2mmap]: 4

┌──[root@liruilongs.github.io]-[/usr/share/bpftrace/tools]
└─$

前两个列为 当前程序的两个父目录信息,root, bpfdemo,我们的实验在 root 目录的 bpfdemo 目录下完成,第三列为映射的进程名字,最后一列为映射次数

fmapfaults(8)

fmapfault(8) 跟踪内存映射文件的缺页错误,按进程名和文件名来统计,内存映射了之后只有写入数据才会发生缺页错误,所以如果我们想知道那些进程在分配虚拟内存之后进行了读写操作,那么可以通过 fmapfaults 跟踪。

对应的代码地址:

https://github.com/brendangregg/bpf-perf-tools-book/blob/master/originals/Ch08_FileSystems/fmapfault.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]
└─$vim fmapfault.bt
┌──[root@liruilongs.github.io]-[/usr/share/bpftrace/tools]
└─$chmod +x fmapfault.bt
#!/usr/local/bin/bpftrace
/*
* fmapfault - Count file map faults.
*
* See BPF Performance Tools, Chapter 8, 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:filemap_fault
{
$vf = (struct vm_fault *)arg0;
$file = $vf->vma->vm_file->f_path.dentry->d_name.name;
@[comm, str($file)] = count();
}

这里任然使用之前的 Demo 进行测试

1
2
3
4
5
6
7
8
9
10
11
12
┌──[root@liruilongs.github.io]-[~/bpfdemo]
└─$./shar2mmap
Parent read: Hello from child process! PID=15816
┌──[root@liruilongs.github.io]-[~/bpfdemo]
└─$./hug2mmap
┌──[root@liruilongs.github.io]-[~/bpfdemo]
└─$./file2mmap
当前进程PID: 15818
Original file content:
ABCD.............................IJ
┌──[root@liruilongs.github.io]-[~/bpfdemo]
└─$

需要注意,文件实际的读写操作频率可能高于缺页错误发生的频率。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
┌──[root@liruilongs.github.io]-[/usr/share/bpftrace/tools]
└─$./fmapfault.bt
Attaching 1 probe...
^C

@[file2mmap, libc.so.6]: 1
@[shar2mmap, shar2mmap]: 1
@[file2mmap, file2mmap]: 1
@[shar2mmap, libc.so.6]: 1
@[file2mmap, testfile.txt]: 1
@[hug2mmap, hug2mmap]: 1
@[hug2mmap, libc.so.6]: 1
@[hug2mmap, ld-linux-x86-64.so.2]: 4
@[file2mmap, ld-linux-x86-64.so.2]: 4
@[shar2mmap, ld-linux-x86-64.so.2]: 4
┌──[root@liruilongs.github.io]-[/usr/share/bpftrace/tools]
└─$

博文部分内容参考

© 文中涉及参考链接内容版权归原作者所有,如有侵权请告知 :)


《BPF Performance Tools》


© 2018-至今 liruilonger@gmail.com, 保持署名-非商用-相同方式共享(CC BY-NC-SA 4.0)

发布于

2025-06-16

更新于

2025-06-24

许可协议

评论
加载中,最新评论有1分钟缓存...
Your browser is out-of-date!

Update your browser to view this website correctly.&npsb;Update my browser now

×